File Coverage

blib/lib/WWW/Mailman.pm
Criterion Covered Total %
statement 83 156 53.2
branch 31 62 50.0
condition 8 28 28.5
subroutine 16 27 59.2
pod 10 10 100.0
total 148 283 52.3


line stmt bran cond sub pod time code
1             package WWW::Mailman;
2             $WWW::Mailman::VERSION = '1.061';
3 4     4   50658 use warnings;
  4         6  
  4         101  
4 4     4   11 use strict;
  4         5  
  4         62  
5              
6 4     4   14 use Carp;
  4         4  
  4         233  
7 4     4   1690 use URI;
  4         20857  
  4         85  
8 4     4   2470 use WWW::Mechanize;
  4         378475  
  4         138  
9 4     4   1859 use HTTP::Cookies;
  4         18392  
  4         360  
10              
11             my @attributes = qw(
12             secure server prefix program list
13             email password moderator_password admin_password
14             );
15              
16             my %default = ( program => 'mailman' );
17              
18             my $action_re = qr/^(?:admin(?:db)?|edithtml|listinfo|options|private)$/;
19              
20             #
21             # ACCESSORS / MUTATORS
22             #
23              
24             # generic accessors
25             for my $attr (@attributes) {
26 4     4   19 no strict 'refs';
  4         5  
  4         3274  
27             *{$attr} = sub {
28 97     97   1204 my $self = shift;
29 97 100 100     373 return defined $self->{$attr} ? $self->{$attr} : $default{$attr} || ''
    100          
30             if !@_;
31 21         36 return $self->{$attr} = shift;
32             };
33             }
34              
35             # specialized accessors
36             sub uri {
37 7     7 1 58 my ( $self, $uri ) = @_;
38 7 100       10 if ($uri) {
39 6         18 $uri = URI->new($uri);
40              
41             # @segments = @prefix, $program, $action, $list, @suffix
42 6         6228 my $program = $self->program;
43 6         25 my ( undef, @segments ) = $uri->path_segments;
44 6         298 my @prefix;
45              
46             # the program name is found in the url
47 6 100       29 if( grep $_ eq $program, @segments ) {
    100          
48 4   66     25 push @prefix, shift @segments
49             while @segments && $segments[0] ne $program;
50 4         6 shift @segments; # drop the program name
51 4 100       10 croak "Invalid URL $uri: no action"
52             if !shift @segments;
53             }
54              
55             # try to autodetect the program name
56             elsif( grep $_ =~ $action_re, @segments ) {
57 1   66     12 push @prefix, shift @segments
58             while @segments && $segments[0] !~ $action_re;
59 1         4 $self->program( pop @prefix ); # get the program name
60 1         1 shift @segments; # drop the action name
61             }
62              
63             # declare FAIL
64             else {
65 1         3 croak "Invalid URL $uri: no program segment found ($program)";
66             }
67              
68             # just keep the bits we need
69 4         14 $self->server( $uri->host );
70 4         17 $self->secure( $uri->scheme eq 'https' );
71 4         16 $self->userinfo( $uri->userinfo );
72 4         11 $self->prefix( join '/', @prefix );
73 4         7 $self->list( shift @segments );
74             }
75              
76             # create a generic listinfo URL
77             else {
78 1         2 $uri = $self->_uri_for('listinfo');
79             }
80 5         22 return $uri;
81             }
82              
83             sub archive_mbox_uri {
84 0     0 1 0 my $self = shift;
85 0         0 return $self->__uri_for( private => ( $self->list . '.mbox' ) x 2 );
86             }
87              
88             sub userinfo {
89 36     36 1 401 my $self = shift;
90 36 100       99 return defined $self->{userinfo} ? $self->{userinfo} : '' if !@_;
    100          
91 13         17 $self->{userinfo} = my $userinfo = shift;
92              
93             # update the credentials stored in the robot
94 13 50       21 if ( $self->robot ) {
95 13 50       56 if ($userinfo) {
96 0         0 $self->robot->credentials( split /:/, $userinfo, 2 );
97             }
98             else {
99 13         19 $self->robot->clear_credentials();
100             }
101             }
102              
103 13         47 return $userinfo;
104             }
105              
106             sub robot {
107 47     47 1 15821 my $self = shift;
108 47 100       163 return defined $self->{robot} ? $self->{robot} : '' if !@_;
    100          
109 9         13 $self->{robot} = shift;
110 9         16 $self->userinfo( $self->userinfo ); # update credentials
111 9         12 return $self->{robot};
112             }
113              
114             push @attributes, qw( uri userinfo robot );
115              
116             #
117             # CONSTRUCTOR
118             #
119              
120             sub new {
121 11     11 1 3650 my ( $class, %args ) = @_;
122              
123             # create the object
124 11         20 my $self = bless {}, $class;
125              
126             # get the rest of attributes
127             $self->$_( delete $args{$_} )
128 11         19 for grep { exists $args{$_} } @attributes;
  132         148  
129              
130             # bring in the robot if needed
131 9 100       19 if ( !$self->robot ) {
132 8         30 my %mech_options = (
133             agent => "WWW::Mailman/$WWW::Mailman::VERSION",
134             stack_depth => 2, # make it a Bear of Very Little Brain
135             quiet => 1,
136             autocheck => 0, # Fancy my making a mistake like that
137             );
138             $mech_options{cookie_jar} = HTTP::Cookies->new(
139             file => delete $args{cookie_file},
140             ignore_discard => 1, # Promise me you'll never forget me
141             autosave => 1,
142 8 100       19 ) if exists $args{cookie_file};
143 8         87 $self->robot( WWW::Mechanize->new(%mech_options) );
144             }
145              
146             # some unknown parameters remain
147 9 100       24 croak "Unknown constructor parameters: @{ [ keys %args ] }"
  1         189  
148             if keys %args;
149              
150 8         15 return $self;
151             }
152              
153             #
154             # PRIVATE METHODS
155             #
156             sub __uri_for {
157 13     13   16 my ( $self, @parts ) = @_;
158 13         30 my $uri = URI->new();
159 13 100       3672 $uri->scheme( $self->secure ? 'https' : 'http' );
160 13 50       3374 $uri->userinfo( $self->userinfo )
161             if $self->userinfo;
162 13         20 $uri->host( $self->server );
163 13   66     522 $uri->path( join '/', $self->prefix || (), $self->program, @parts );
164 13         274 return $uri;
165             }
166              
167             sub _uri_for {
168 13     13   2462 my ( $self, $action, @options ) = @_;
169 13         23 return $self->__uri_for( $action, $self->list, @options )
170             }
171              
172             sub _login_form {
173 0     0     my ($self) = @_;
174 0           my $mech = $self->robot;
175              
176             # shortcut
177 0 0         return if !$mech->forms;
178              
179 0           my $form;
180              
181             # login is required if the form asks for:
182             # - a login/password
183 0 0         if ( $form = $mech->form_with_fields('password') ) {
    0          
184 0           $form->value( email => $self->email );
185 0           $form->value( password => $self->password );
186             }
187              
188             # - an admin (or moderator) password
189             elsif ( $form = $mech->form_with_fields('adminpw') ) {
190 0   0       $form->value( adminpw => $self->admin_password
191             || $self->moderator_password );
192             }
193              
194             # otherwise, no authentication required
195              
196 0           return $form;
197             }
198              
199             sub _load_uri {
200 0     0     my ( $self, $uri ) = @_;
201 0           my $mech = $self->robot;
202 0           $mech->get($uri);
203              
204             # authentication required?
205 0 0         if ( my $form = $self->_login_form ) {
206 0           $mech->request( $form->click );
207 0 0         croak "Couldn't login on $uri" if $self->_login_form;
208             }
209              
210             # get the version if we don't have it yet
211             $self->{version} = $1
212             if !exists $self->{version}
213 0 0 0       && $mech->content =~ /
version (\d+\.\d+\.\d+\w*)
214              
215             # we're on!
216             }
217              
218             #
219             # INTERNAL UTILITY FUNCTIONS
220             #
221             sub _form_data {
222             return {
223             map {
224 0 0 0 0     $_->type eq 'submit' || $_->readonly
  0            
225             ? () # ignore buttons and read-only inputs
226             : ( $_->name => $_->value )
227             } $_[0]->inputs
228             };
229             }
230              
231             #
232             # ACTIONS
233             #
234              
235             # The option form has 5 submit buttons, listed here with their inputs:
236             #
237             # * change-of-address:
238             # - new-address
239             # - confirm-address
240             # - fullname
241             # - changeaddr-globally
242             # * unsub:
243             # - unsubconfirm
244             # * othersubs
245             # * emailpw
246             # * changepw:
247             # - newpw
248             # - confpw
249             # - pw-globally
250             # * options-submit:
251             # - disablemail
252             # - deliver-globally
253             # - digest
254             # - mime
255             # - mime-globally
256             # - dontreceive
257             # - ackposts
258             # - remind
259             # - remind-globally
260             # - conceal
261             # - rcvtopic
262             # - nodupes
263             # - nodupes-globally
264              
265             # most routines will be identical, so generate them:
266             {
267             my %options = (
268             address => 'change-of-address',
269             unsub => 'unsub',
270             changepw => 'changepw',
271             options => 'options-submit',
272             );
273             while ( my ( $method, $button ) = each %options ) {
274 4     4   17 no strict 'refs';
  4         6  
  4         1841  
275             *$method = sub {
276 0     0     my ( $self, $options ) = @_;
277              
278             # select the options form
279 0           my $mech = $self->robot;
280 0   0       $self->_load_uri(
281             $self->_uri_for( 'options', $self->email || '' ) );
282 0           $mech->form_with_fields('fullname');
283              
284             # change of options
285 0 0         if ($options) {
286 0           $mech->set_fields(%$options);
287 0           $mech->click($button);
288 0           $mech->form_with_fields('fullname');
289             }
290              
291 0           return _form_data( $mech->current_form );
292             };
293             }
294             }
295              
296             # emailpw doesn't need any parameter
297             sub emailpw {
298 0     0 1   my ($self) = @_;
299              
300             # no auto-authenticate
301 0           my $mech = $self->robot;
302 0           $mech->get( my $uri = $self->_uri_for( 'options', $self->email ) );
303              
304 0 0         if ( $mech->form_with_fields('emailpw') ) {
    0          
305 0           $mech->click('emailpw');
306             }
307             elsif ( $mech->form_with_fields('login-remind') ) {
308 0           $mech->click('login-remind');
309             }
310             else {
311 0           croak "Unable to find a password email form on $uri";
312             }
313             }
314              
315             # othersubs needs some parsing to be useful
316             sub othersubs {
317 0     0 1   my ($self) = @_;
318 0           my $mech = $self->robot;
319 0           $self->_load_uri( $self->_uri_for( 'options', $self->email ) );
320 0           $mech->form_with_fields('fullname');
321 0           $mech->click('othersubs');
322              
323 0           my $uri = $mech->uri;
324             return
325 0           map { URI->new_abs( $_, $uri ) }
  0            
326             $mech->content =~ m{
  • [^<]+}g;
  • 327             }
    328              
    329             sub roster {
    330 0     0 1   my ($self) = @_;
    331 0           my $mech = $self->robot;
    332 0           $self->_load_uri( $self->_uri_for('roster') );
    333              
    334             # try to detect authentication issues [private_roster]
    335 0 0         if ( $mech->content !~ /
  • / ) {
  • 336              
    337             # authenticate through listinfo
    338 0           $mech->get( $self->_uri_for('listinfo') );
    339 0           my $form = $mech->form_with_fields('roster-pw');
    340              
    341             # in case the roster is reserved to admins,
    342             # we'll try the admin passwords first
    343 0   0       my $password = $self->admin_password || $self->moderator_password;
    344 0 0         $mech->set_fields( 'roster-email' => $self->email ) if !$password;
    345 0   0       $mech->set_fields( 'roster-pw' => $password || $self->password );
    346 0           $mech->click('SubscriberRoster');
    347             }
    348              
    349             # subscriber list may be empty, e.g. for privacy reasons
    350             return
    351              
    352             # TODO: distinguishes types of subscribers
    353 0           map { s/ at /@/; $_ } # [obscure_addresses]
      0            
      0            
    354             $mech->content =~ m{
  • ]*>([^<]*)}g;
  • 355             }
    356              
    357             # most admin routines will be identical...
    358             sub admin {
    359 0     0 1   my ( $self, $section, $options ) = @_;
    360 0           my $mech = $self->robot;
    361 0           $self->_load_uri( $self->_uri_for( admin => $section ) );
    362              
    363             # get the main form
    364 0           $mech->form_number(1);
    365              
    366             # change of options
    367 0 0         if ($options) {
    368 0           $mech->current_form->accept_charset('iso-8859-1');
    369 0           $mech->set_fields(%$options);
    370 0           $mech->click();
    371 0           $mech->form_number(1);
    372             }
    373              
    374 0           return _form_data( $mech->current_form );
    375             }
    376              
    377             # so, use a bit of currying
    378             for my $section (
    379             qw(
    380             general passwords language nondigest digest
    381             bounce archive gateway autoreply contentfilter topics
    382             )
    383             )
    384             {
    385 4     4   19 no strict 'refs';
      4         4  
      4         399  
    386 0     0     *{"admin_$section"} = sub { shift->admin( "$section", @_ ) }
    387             }
    388              
    389             sub version {
    390 0     0 1   my ($self) = @_;
    391 0 0         return $self->{version} if exists $self->{version};
    392              
    393             # get it as part of a page download
    394 0           $self->_load_uri( $self->_uri_for('listinfo') );
    395 0           return $self->{version};
    396             }
    397              
    398             1;
    399              
    400             __END__