#!/usr/bin/env perl # # mailmanage - reconfigure Mailman mailing lists # # $Id$ use warnings; use strict; use Getopt::Std; use LWP::Simple; use WWW::Mailman; use JSON; my %opt; getopts( 'ah?l:mp:s:t:u:v', \%opt ) or oomph('error parsing the command line, try -h for help'); my ($me) = $0 =~ m#([^/\\]+)$#; my $default_server = 'listserver.tue.nl'; HELP_MESSAGE() if grep { /^[h?]/ } keys %opt; sub HELP_MESSAGE { print STDERR <head() must return content grep { defined && $_ gt 0 } head( $_[0] ); } returns_content($hostpart) or $hostpart =~ s#^https:#http:#; returns_content($hostpart) or oomph('cannot read main page from server'); ($server) = $hostpart =~ m#.*//([^/]+)#; $server or oomph('cannot determine the server, seems empty'); ( $server !~ m#/# && $server =~ /\./ ) or oomph( 'cannot determine the server, this seems wrong:', $server ); my $email = $opt{u} // $ENV{MAILMAN_USER}; my $password = $opt{p} // $ENV{MAILMAN_PASSWORD}; foreach my $list (@lists) { main($list); } exit(0); sub main { my ($list) = @_; my $mm = WWW::Mailman->new( uri => "$hostpart/listinfo/$list", cookie_file => "$ENV{HOME}/.mailmanrc" ) or oomph("cannot create Mailman interface to $hostpart"); my $mech = $mm->robot; $mm->moderator_password($password) if defined $password; my $uri = $mm->uri(); if ( !successful_response( eval { $mech->get($uri) } ) ) { my $prev_uri = $uri; $uri =~ s#mailman/##; successful_response( eval { $mech->get($uri) } ) or oomph("cannot connect to Mailman interface at $uri or $prev_uri"); } # will *succeed* for nonexistent lists on an existing Mailman installation ehm( 'on listinfo page:', $mm->uri() ); #my $address = sprintf( '%s@%s', $list, $server ); #ehm("looking for link to $address"); #$mech->find_link( text => $address, url => 'mailto:' . $address ) # not good enough for nl-pm@amsterdam.pm.org $mech->find_link( text_regex => qr/^$list\@/, url => qr/^mailto:$list\@/ ) or oomph( 'listinfo page not recognized, list may not exist:', $list ); if ( defined $opt{a} ) { # try to follow the link to the archives my @archive_links = grep { $_->text =~ /archives/i && $_->text !~ /searchable/i } $mech->links() or oomph('cannot find or follow link to mailing list archive page'); successful_response( $mech->get( shift(@archive_links) ) ) or oomph('cannot find or follow link to mailing list archive page'); #ehm('on archive page:', $mm->uri()); # still produces the listinfo URL ehm( 'on archive page:', $mech->uri() ); # do we need to log in? if ( my $form = $mech->current_form() ) { if ( $form->action() eq $mech->uri() # acts to the current URL && $form->find_input('^username') && $form->find_input('^password') ) { # looks like the login form defined($email) && defined($password) or oomph('supply a username and password, use -h to find out how'); $mech->set_fields( 'username' => $email, 'password' => $password ); successful_response( $mech->submit() ) or oomph('did not manage to log in to the mailing list archive'); } } my $total_length; binmode( STDOUT, ':utf8' ); # guessing; seems to work for prom-users, which, without this, # causes 'Wide character in print' warnings if ( my @links = grep { $_->url() =~ /(\.txt)$/ } $mech->links() ) { # assume these are links to ungzipped full message archives ehm( 'outputting', scalar(@links), 'mboxes' ); foreach my $link ( reverse @links ) { if ( successful_response( eval { $mech->get( $link->url() ) } ) ) { my $text = $mech->content; $total_length += length($text); print $text; } } } if ( my @links = grep { $_->url() =~ /(\.gz|downloadmbox)$/ } $mech->links() ) { # assume these are links to gzipped full message archives; # this will also cover the SMART Archiver's interface on listserver.tue.nl # which offers a 'Download this overview as one gzipped mbox' link # (which always produces a gzipped empty document, unfortunately) # and the per-month gzipped archives as offered on e.g. # the Mailman-Users archives # (at https://mail.python.org/pipermail/mailman-users/) # and the freeciv-dev archives # (at https://mail.gna.org/public/freeciv-dev/) ehm( 'gunzipping and outputting', scalar(@links), 'mboxes' ); eval { require IO::Uncompress::Gunzip } or oomph('whoops, -a is broken: cannot load IO::Uncompress::Gunzip'); foreach my $link ( reverse @links ) { if ( successful_response( eval { $mech->get( $link->url() ) } ) ) { my $gzipped = $mech->content; my $gunzipped; IO::Uncompress::Gunzip::gunzip( \$gzipped, \$gunzipped ); $total_length += length($gunzipped); print $gunzipped; } } } if ($total_length) { # yiha, we found an archive } elsif ( $mech->content =~ /archives\s+are\s+currently\s+empty/ ) { oops('warning: message archive claimed to be empty on the archives page'); } else { oops('warning: no message archive(s) found on the archives page'); } exit(0); } if ( defined $opt{m} ) { ehm('printing list of subscribers'); print map { "$_\n" } $mm->roster(); exit(0); } if ( defined $email ) { $mm->email($email); ehm("processing settings for subscriber $email for list $list ..."); my %result = ( 'address' => process_section( 'address', sub { $mm->address(@_) } ), #'changepw' => process_section( 'changepw', sub { $mm->changepw(@_) } ), #'options' => process_section( 'options', sub { $mm->options(@_) } ), #'unsub' => process_section( 'unsub', sub { $mm->unsub(@_) } ) ); # this isn't quite right yet: each section contains *all* sessings! if ( !@ARGV ) { print_as_json( \%result ); } } else { ehm("processing settings for admin for list $list ..."); my @sections = qw( general passwords language nondigest digest bounce archive gateway autoreply contentfilter topics members/list members/add members/remove nondigest privacy/subscribing privacy/sender privacy/recipient privacy/spam ); #my @sections = qw( members/list ); #my @sections = qw( privacy/sender ); my %result = map { $_ => process_section( $_, sub { $mm->admin( $_, @_ ) } ) } @sections; if ( !@ARGV ) { print_as_json( \%result ); } } } sub successful_response { my ($response) = @_; defined($response) && $response->is_success; } sub process_section { my ( $section, $accessor ) = @_; ehm("processing settings in section $section ..."); if ( !@ARGV ) { # return all settings return &$accessor(); } # we must show or set the settings # attempt to set the ones with values that exist in this section if (%settings_with_values) { my $existing_settings = &$accessor(); if ( my @to_set = grep { exists $existing_settings->{$_} } keys %settings_with_values ) { # set them in one POST! (this doesn't appear to work ...) @to_set = sort keys %settings_with_values; ehm( 'setting:', $_, 'to:', $settings_with_values{$_} ) for @to_set; &$accessor( { map { $_ => $settings_with_values{$_} } @to_set } ); } } # show the ones without values that exist in this section if (@settings_wo_values) { my $existing_settings = &$accessor(); if ( my @to_show = grep { exists $existing_settings->{$_} } @settings_wo_values ) { ehm( 'showing:', @to_show ); print_as_json( { $section => { map { $_ => $existing_settings->{$_} } @to_show } } ); } } } sub print_as_json { my $struct = $opt{t} ? using_separator( $_[0] ) : $_[0]; print to_json( $struct, { utf8 => 1, pretty => 1 } ); } sub using_separator { my $value = $_[0]; my $ref = ref($value); if ( $ref eq 'ARRAY' ) { [ map { using_separator($_) } @$value ]; } elsif ( $ref eq 'HASH' ) { my %result = map { using_separator($_) => using_separator( $value->{$_} ) } keys %$value; \%result; } elsif ( $ref eq '' ) { $value =~ s/\n/$opt{t}/g; $value; } else { oomph("don't know how to substitute separator in $value"); } }