File Coverage

blib/lib/Mail/Salsa/Action/Admin.pm
Criterion Covered Total %
statement 27 319 8.4
branch 0 170 0.0
condition 0 87 0.0
subroutine 9 28 32.1
pod 0 18 0.0
total 36 622 5.7


line stmt bran cond sub pod time code
1             #
2             # Mail/Salsa/Action/Admin.pm
3             # Last Modification: Fri May 28 19:23:49 WEST 2010
4             #
5             # Copyright (c) 2010 Henrique Dias .
6             # All rights reserved.
7             # This module is free software; you can redistribute it and/or modify
8             # it under the same terms as Perl itself.
9             #
10             package Mail::Salsa::Action::Admin;
11              
12 1     1   26500 use 5.008000;
  1         3  
  1         50  
13 1     1   7 use strict;
  1         2  
  1         34  
14 1     1   5 use warnings;
  1         6  
  1         62  
15              
16             require Exporter;
17 1     1   982 use AutoLoader qw(AUTOLOAD);
  1         2252  
  1         6  
18 1     1   838 use MIME::Base64 qw(encode_base64);
  1         936  
  1         116  
19 1     1   542 use Mail::Salsa::Utils qw(file_path create_file generate_id);
  1         4  
  1         119  
20 1     1   6 use Mail::Salsa::Logs qw(logs);
  1         3  
  1         49  
21 1     1   5 use Mail::Salsa::Sendmail;
  1         2  
  1         38  
22 1     1   5 use Mail::Salsa::Template;
  1         2  
  1         9497  
23              
24             our @ISA = qw(Exporter);
25              
26             # Items to export into callers namespace by default. Note: do not export
27             # names by default without a very good reason. Use EXPORT_OK instead.
28             # Do not simply export all your public functions/methods/constants.
29              
30             # This allows declaration use Mail::Salsa ':all';
31             # If you do not need this, moving things directly into @EXPORT or @EXPORT_OK
32             # will save memory.
33             our %EXPORT_TAGS = ( 'all' => [ qw() ] );
34              
35             our @EXPORT_OK = ( @{ $EXPORT_TAGS{'all'} } );
36             our @EXPORT = qw();
37             our $VERSION = '0.06';
38              
39             my @patterns = (
40             '[^\<\>\@\(\)]+',
41             '[^\000-\037\300-\377\@<>(),;:\s]+\@([\w\-]+\.)+[a-zA-Z]{2,4}',
42             '(allow|deny) +(\S+) +to +(post|bounce|proceed) +from +(localnet|anywhere)(.+)?',
43             );
44              
45             sub new {
46 0     0 0   my $proto = shift;
47 0   0       my $class = ref($proto) || $proto;
48 0           my $self = {@_};
49 0           bless ($self, $class);
50 0           $self->process_msg();
51 0           return($self);
52             }
53              
54             sub process_msg {
55 0     0 0   my $self = shift;
56              
57 0           my ($name, $domain) = split(/\@/, $self->{'list'});
58 0           my %files = ();
59 0           for my $file ('stamp.txt', 'ticket.txt', 'configuration.txt', 'restrict.txt', 'attachments.txt', 'information.txt', 'header.txt', 'footer.txt', 'list.txt') {
60 0           $files{$file} = file_path($self->{'list'}, $self->{'list_dir'}, $file);
61             }
62 0 0         if(-e (my $ticketkeyf = join("/", $self->{'tmp_dir'}, 'ticket.txt'))) {
63 0 0         if(&check_ticket($ticketkeyf, $files{'ticket.txt'})) {
64 0           my $result = &file_manager($self->{'tmp_dir'}, \%files);
65 0 0         if(exists($result->{'error'})) {
    0          
66 0           Mail::Salsa::Utils::tplsendmail(
67             smtp_server => $self->{'smtp_server'},
68             timeout => $self->{'timeout'},
69             label => "UPDATE_ERROR",
70             lang => $self->{'config'}->{'language'},
71             vars => {
72             from => "$name\-master\@$domain",
73             to => "$name\-owner\@$domain",
74             file => $result->{'file'},
75             errors => $result->{'error'},
76             }
77             );
78 0           $self->logs(join("", "[update error] from: ", $self->{'from'}), "list");
79             } elsif(exists($result->{'files'})) {
80 0           Mail::Salsa::Utils::tplsendmail(
81             smtp_server => $self->{'smtp_server'},
82             timeout => $self->{'timeout'},
83             label => "UPDATED_FILES",
84             lang => $self->{'config'}->{'language'},
85             vars => {
86             from => "$name\-master\@$domain",
87             to => "$name\-owner\@$domain",
88             files => $result->{'files'},
89             }
90             );
91 0           $self->logs(join("", "[updated files] from: ", $self->{'from'}), "list");
92             } else {
93 0           $self->logs(join("", "[no admin files] from: ", $self->{'from'}), "list");
94             }
95             } else {
96 0           Mail::Salsa::Utils::tplsendmail(
97             smtp_server => $self->{'smtp_server'},
98             timeout => $self->{'timeout'},
99             label => "ADMINTICKET",
100             lang => $self->{'config'}->{'language'},
101             vars => {
102             from => "$name\-master\@$domain",
103             to => "$name\-owner\@$domain",
104             }
105             );
106 0           $self->logs(join("", "[wrong ticket] from: ", $self->{'from'}), "list");
107             }
108             } else {
109 0           my $dir = join("/", $self->{'list_dir'}, $domain, $name);
110 0 0         unless(-d $dir) {
111 0           Mail::Salsa::Utils::make_dir_rec($dir, 0755);
112 0 0         (-d $dir) or die("$!");
113             }
114 0           my $list = $self->{'list'};
115 0 0 0       (-e $files{'stamp.txt'} && -s $files{'stamp.txt'}) or create_file($files{'stamp.txt'}, join("", uc(generate_id(32)), "\n"), 0600);
116 0 0 0       (-e $files{'ticket.txt'} && -s $files{'ticket.txt'}) or create_file($files{'ticket.txt'}, join("", uc(generate_id(32)), "\n"), 0600);
117 0 0 0       (-e $files{'configuration.txt'} && -s $files{'configuration.txt'}) or create_file($files{'configuration.txt'}, &make_config(), 0600);
118 0 0 0       (-e $files{'restrict.txt'} && -s $files{'restrict.txt'}) or create_file($files{'restrict.txt'}, "\# Add here the rules\n\# [allow|deny] [address|subscribers|any] to [post|bounce|proceed] \\\n\# from [localnet|anywhere] with(out) stamp\n\#\n\nallow subscribers to post from anywhere without stamp\ndeny any to proceed from anywhere\n", 0600);
119 0 0 0       (-e $files{'attachments.txt'} && -s $files{'attachments.txt'}) or create_file($files{'attachments.txt'}, "\# Insert here the acl rules.\n\#\n\# [allow|deny] mime/type from [address|domain|subscribers|any]\n\#\n\nallow any/any from any\n", 0600);
120 0 0 0       (-e $files{'information.txt'} && -s $files{'information.txt'}) or create_file($files{'information.txt'}, "Please insert here the information about mailing list.\n", 0600);
121 0 0 0       (-e $files{'header.txt'} && -s $files{'header.txt'}) or create_file($files{'header.txt'}, "Please remove this text and insert your own text header.\n", 0600);
122 0 0 0       (-e $files{'footer.txt'} && -s $files{'footer.txt'}) or create_file($files{'footer.txt'}, "Please remove this text and insert your own text footer.\n", 0600);
123 0 0 0       (-e $files{'list.txt'} && -s $files{'list.txt'}) or create_file($files{'list.txt'}, "\# Add here the addresses of the list\n", 0600);
124 0           $self->sendmail(\%files);
125 0           $self->logs(join("", "[send files to owner] from: ", $self->{'from'}), "list");
126             }
127 0           return();
128             }
129              
130             sub normalize {
131 0     0 0   local $_ = shift;
132              
133 0 0         if(/^($patterns[0]) +<($patterns[1])>\s+/) { return([lc($2), $1]); }
  0            
134 0 0         if(/^?\s+/) { return([lc($1), ""]); }
  0            
135 0           return(["", ""]);
136             }
137              
138             sub update_file {
139 0     0 0   my $newfile = shift;
140 0           my $oldfile = shift;
141              
142 0 0         open(NEW, "<", $newfile) or die("$!");
143 0 0         open(OLD, ">", $oldfile) or die("$!");
144 0           select(OLD);
145 0           while() {
146 0           s/\x0d//g;
147 0           print OLD $_;
148             }
149 0           close(OLD);
150 0           close(NEW);
151              
152 0 0         unlink($newfile) or die("$!");
153 0           return();
154             }
155              
156             sub list2hash {
157 0     0 0   my $file = shift;
158              
159 0           my @error = ();
160 0           my $n = 1;
161 0 0         open(FILE, "<", $file) or die("$!");
162 0           while() {
163 0           my ($addr, $name) = @{&normalize($_)};
  0            
164 0 0         $addr ? ($_[0]->{$addr} = $name) : push(@error, "line $n: $_");
165 0           $n++;
166             }
167 0           close(FILE);
168 0 0         unlink($file) or die("$!");
169              
170 0           return(\@error);
171             }
172              
173             sub update_list {
174 0     0 0   my $list = shift;
175 0           my $hash = shift;
176              
177 0 0         open(OLDLIST, "<", $list) or die("$!");
178 0 0         open(NEWLIST, ">", "$list\.new") or die("$!");
179 0           select(NEWLIST);
180 0           while() {
181 0           my ($addr, $name) = @{&normalize($_)};
  0            
182 0 0         $addr or next;
183 0 0         if(exists($hash->{'unsubscribe'}->{$addr})) {
184 0           delete($hash->{'unsubscribe'}->{$addr});
185 0           next;
186             }
187 0 0         next if(exists($hash->{'subscribe'}->{$addr}));
188 0 0         print NEWLIST $name ? "$name \<$addr\>" : "$addr", "\n";
189             }
190 0           while(my ($addr, $name) = each(%{$hash->{'subscribe'}})) {
  0            
191 0 0         print NEWLIST $name ? "$name \<$addr\>" : "$addr", "\n";
192             }
193 0           close(NEWLIST);
194 0           close(OLDLIST);
195              
196 0           rename("$list\.new", $list);
197 0           return();
198             }
199              
200             sub replace_list {
201 0     0 0   my $newfile = shift;
202 0           my $oldfile = shift;
203              
204 0           my %inserted = ();
205 0 0         open(NEW, "<", $newfile) or die("$!");
206 0 0         open(OLD, ">", $oldfile) or die("$!");
207 0           select(OLD);
208 0           while() {
209 0 0         if(/^\#/) { print OLD $_; next; }
  0            
  0            
210 0 0 0       /[\x0d\x0a]+$/ or $_ .= "\n" if(eof(NEW));
211 0           my ($addr, $name) = @{&normalize($_)};
  0            
212 0 0         $addr or next;
213 0 0         next if(exists($inserted{$addr}));
214 0 0         print OLD $name ? "$name <$addr>" : $addr, "\n";
215 0           $inserted{$addr} = "";
216             }
217 0           close(OLD);
218 0           close(NEW);
219              
220 0 0         unlink($newfile) or die("$!");
221 0           return();
222             }
223              
224             sub check_confkeys {
225 0     0 0   $_ = shift;
226              
227 0 0         /^title *\= *[^\=]{2,60}$/ and return(1);
228 0 0         /^prefix *\= *[^\=]{2,30}$/ and return(1);
229 0 0         /^language *\= *[a-z][a-z]$/ and return(1);
230 0 0 0       /^max_message_size *\= *(\d{1,9})$/ and $1 > -1 and return(1);
231 0 0         /^subscribe *\= *[yn]$/ and return(1);
232 0 0         /^unsubscribe *\= *[yn]$/ and return(1);
233 0 0         /^archive *\= *[yn]$/ and return(1);
234 0 0         /^header *\= *[yn]$/ and return(1);
235 0 0         /^footer *\= *[yn]$/ and return(1);
236 0 0         /^localnet *\= *[\d\.\, ]+$/ and return(1);
237 0 0 0       /^stamp_life *\= *(\d{1,9})[dwmy]$/ and $1 > -1 and return(1);
238              
239 0           return(0);
240             }
241              
242             sub check_rules {
243 0     0 0   my $file = shift;
244              
245 0           my @errors = ();
246 0           my $n = 0;
247 0 0         open(FILE, "<", $file) or die("$!");
248 0           while() {
249 0           $n++;
250 0 0         next if(/^[\#\x0d\x0a]/);
251 0 0         /^(allow|deny) +\w+\/\w+ +from +\S+[\x0d\x0a]+/ or push(@errors, "Line $n: $_");
252             }
253 0           close(FILE);
254 0           return(\@errors);
255             }
256              
257             sub check_config {
258 0     0 0   my $file = shift;
259              
260 0           my @errors = ();
261 0           my $n = 0;
262 0 0         open(FILE, "<", $file) or die("$!");
263 0           while() {
264 0           $n++;
265 0 0         next if(/^[\#\x0d\x0a]/);
266 0           s/[ \t\x0d\x0a]+$//g;
267 0 0         &check_confkeys($_) or push(@errors, "Line $n: $_");
268             }
269 0           close(FILE);
270 0           return(\@errors);
271             }
272              
273             sub check_restrict {
274 0     0 0   my $file = shift;
275              
276 0           my @errors = ();
277 0           my $n = 0;
278 0 0         open(FILE, "<", $file) or die("$!");
279 0           while() {
280 0           $n++;
281 0 0         next if(/^[\#\x0d\x0a]+/);
282 0 0         if(/^$patterns[2]/) {
283 0 0 0       unless($2 eq "any" || $2 =~ /\.[a-zA-Z]{2,4}$/) {
284 0           push(@errors, "Line $n: $_");
285 0           next;
286             }
287 0 0 0       if(defined($5) and ($5 !~ /^ with(out)? +stamp/)) {
288 0           push(@errors, "Line $n: $_");
289 0           next;
290             }
291 0           } else { push(@errors, "Line $n: $_"); }
292             }
293 0           close(FILE);
294 0           return(\@errors);
295             }
296              
297             sub check_address {
298 0     0 0   my $file = shift;
299              
300 0           my $pattern = join("", "^", $patterns[1], "[ \t]*[\x0d\x0a]+");
301 0           my @errors = ();
302 0           my $n = 0;
303 0 0         open(FILE, "<", $file) or die("$!");
304 0           while() {
305 0           $n++;
306 0 0         next if(/^[\#\x0d\x0a]+/);
307 0 0 0       /[\x0d\x0a]+$/ or $_ .= "\n" if(eof(FILE));
308 0 0 0       /^$patterns[0] +<$patterns[1]>[ \t]*[\x0d\x0a]+/ or
      0        
309             /^<$patterns[1]>[ \t]*[\x0d\x0a]+/ or
310             /$pattern/ or push(@errors, "Line $n: $_");
311             }
312 0           close(FILE);
313 0           return(\@errors);
314             }
315              
316             sub file_manager {
317 0     0 0   my $tmpdir = shift;
318 0           my $files = shift;
319              
320 0           my @filesok = ();
321 0           my $file = join("/", $tmpdir, 'configuration.txt');
322 0 0 0       if(-e $file && -s $file) {
323 0           my $errors = &check_config($file);
324             return({
325 0           file => 'configuration.txt',
326 0           error => join("\n", @{$errors})
327 0 0         }) if(scalar(@{$errors}));
328 0           &update_file($file, $files->{'configuration.txt'});
329 0           push(@filesok, 'configuration.txt');
330             }
331              
332 0           $file = join("/", $tmpdir, 'information.txt');
333 0 0 0       if(-e $file && -s $file) {
334 0           &update_file($file, $files->{'information.txt'});
335 0           push(@filesok, 'information.txt');
336             }
337              
338 0           $file = join("/", $tmpdir, 'header.txt');
339 0 0 0       if(-e $file && -s $file) {
340 0           &update_file($file, $files->{'header.txt'});
341 0           push(@filesok, 'header.txt');
342             }
343              
344 0           $file = join("/", $tmpdir, 'footer.txt');
345 0 0 0       if(-e $file && -s $file) {
346 0           &update_file($file, $files->{'footer.txt'});
347 0           push(@filesok, 'footer.txt');
348             }
349              
350 0           $file = join("/", $tmpdir, 'restrict.txt');
351 0 0 0       if(-e $file && -s $file) {
352 0           my $errors = &check_restrict($file);
353             return({
354 0           file => 'restrict.txt',
355 0           error => join("\n", @{$errors})
356 0 0         }) if(scalar(@{$errors}));
357 0           &update_file($file, $files->{'restrict.txt'});
358 0           push(@filesok, 'restrict.txt');
359             }
360              
361 0           $file = join("/", $tmpdir, 'list.txt');
362 0 0 0       if(-e $file && -s $file) {
363 0           my $errors = &check_address($file);
364             return({
365 0           file => 'list.txt',
366 0           error => join("\n", @{$errors})
367 0 0         }) if(scalar(@{$errors}));
368 0           &replace_list($file, $files->{'list.txt'});
369 0           push(@filesok, 'list.txt');
370             }
371              
372 0           $file = join("/", $tmpdir, 'attachments.txt');
373 0 0 0       if(-e $file && -s $file) {
374 0           my $errors = &check_rules($file);
375             return({
376 0           file => 'attachments.txt',
377 0           error => join("\n", @{$errors})
378 0 0         }) if(scalar(@{$errors}));
379 0           &update_file($file, $files->{'attachments.txt'});
380 0           push(@filesok, 'attachments.txt');
381             }
382              
383 0           my %hash = ();
384 0           $file = join("/", $tmpdir, 'subscribe.txt');
385 0 0 0       if(-e $file && -s $file) {
386 0           my $errors = &list2hash($file, $hash{'subscribe'} = ());
387             return({
388 0           file => 'subscribe.txt',
389 0           error => join("\n", @{$errors})
390 0 0         }) if(scalar(@{$errors}));
391             }
392 0           $file = join("/", $tmpdir, 'unsubscribe.txt');
393 0 0 0       if(-e $file && -s $file) {
394 0           my $errors = &list2hash($file, $hash{'unsubscribe'} = ());
395             return({
396 0           file => 'unsubscribe.txt',
397 0           error => join("\n", @{$errors})
398 0 0         }) if(scalar(@{$errors}));
399             }
400 0 0 0       if(exists($hash{'subscribe'}) || exists($hash{'unsubscribe'})) {
401 0           &update_list($files->{'list.txt'}, \%hash);
402 0           push(@filesok, 'list.txt');
403             }
404 0 0         return(scalar(@filesok) ? { files => join("\n", @filesok) } : {});
405             }
406              
407             sub check_ticket {
408 0     0 0   my $outfile = shift;
409 0           my $infile = shift;
410              
411 0           (my $keyout = &get_content($outfile)) =~ s/\s+//g;
412 0           (my $keyin = &get_content($infile)) =~ s/\s+//g;
413 0 0 0       (length($keyout) == 32 && length($keyin) == 32) or return(0);
414 0 0         return(($keyout eq $keyin) ? 1 : 0);
415             }
416              
417             sub get_content {
418 0     0 0   my $file = shift;
419              
420 0           my $data = "";
421 0 0         open(FILE, "<", $file) or die("$!");
422 0           while() {
423 0           s/\x0d//g;
424 0           $data = join("", $data, $_);
425             }
426 0           close(FILE);
427 0           return($data);
428             }
429              
430             sub make_config {
431              
432 0     0 0   my $data =<<"EOF";
433             # Mailing List configuration file
434             # Please don't change any line that starts with "#" character.
435              
436             # Set the title of mailing list.
437              
438             title = My Mailing List Title
439              
440             # Add a prefix to the subject.
441              
442             prefix = [mylist]
443              
444             # Allow/deny the users to subscribe the mailing list.
445             # Choose [y/n]
446              
447             subscribe = y
448              
449             # Allow/deny the users to unsubscribe the mailing list.
450             # Choose [y/n]
451              
452             unsubscribe = y
453              
454             # Set the maximum message size.
455              
456             max_message_size = 0
457              
458             # Specify how long the stamp should be valid.
459             # Stamp expires in n days/weeks/months/years
460             # Choose [number][d/w/m/y]
461              
462             stamp_life = 1m
463              
464             # Save the messages to the archive.
465             # Choose [y/n]
466              
467             archive = n
468              
469             # Set the language.
470              
471             language = en
472              
473             # Add a header information to the message
474             # Choose [y/n]
475              
476             header = n
477              
478             # Add a footer information to the message
479             # Choose [y/n]
480              
481             footer = n
482              
483             # Please enter the IP's for your local network.
484             # Example: 192.168.1., 192.168.2.
485             # 192.168.
486              
487             localnet = 192.168.
488              
489             EOF
490 0           return($data);
491             }
492              
493             sub attach_headers {
494 0     0 0   my $filename = shift;
495 0           my $description = shift;
496 0           my $id = shift;
497              
498 0           my $hdr =<<"EOH";
499             Content-Type: TEXT/plain; name="$filename"
500             Content-Transfer-Encoding: BASE64
501             Content-Description: $description
502             Content-Disposition: attachment; filename="$filename"
503              
504             EOH
505 0           return($hdr);
506             }
507              
508             sub sendmail {
509 0     0 0   my $self = shift;
510 0           my $files = shift;
511              
512 0           my ($name, $domain) = split(/\@/, $self->{'list'});
513 0           my $boundary = join("_", "----=", "NextPart", generate_id(32));
514             my $refsub = sub {
515 0     0     my $handle = shift;
516              
517 0           my $tpl = Mail::Salsa::Template->new(
518             lang => $self->{'lang'},
519             label => "ATTACH_FILES",
520             outfh => $handle
521             );
522 0           $tpl->replace(
523             from => "salsa-master\@$domain",
524             to => "$name-owner\@$domain",
525             admin => "$name-admin\@$domain",
526             boundary => $boundary,
527             origin => $self->{'from'},
528             list => $self->{'list'},
529             );
530 0           print $handle "\n--$boundary\n";
531 0           print $handle &attach_headers('ticket.txt', "Mailing List Administrator Ticket file");
532 0           print $handle join("", encode_base64(&get_content($files->{'ticket.txt'})), "\n", "--$boundary\n");
533              
534 0           print $handle &attach_headers('configuration.txt', "Mailing List Configuration file");
535 0           print $handle join("", encode_base64(&get_content($files->{'configuration.txt'})), "\n", "--$boundary\n");
536              
537 0           print $handle &attach_headers('restrict.txt', "Restrict file");
538 0           print $handle join("", encode_base64(&get_content($files->{'restrict.txt'})), "\n", "--$boundary\n");
539              
540 0           print $handle &attach_headers('attachments.txt', "Attachments ACL file");
541 0           print $handle join("", encode_base64(&get_content($files->{'attachments.txt'})), "\n", "--$boundary\n");
542              
543 0           print $handle &attach_headers('information.txt', "Information file");
544 0           print $handle join("", encode_base64(&get_content($files->{'information.txt'})), "\n", "--$boundary\n");
545              
546 0           print $handle &attach_headers('header.txt', "Header file");
547 0           print $handle join("", encode_base64(&get_content($files->{'header.txt'})), "\n", "--$boundary\n");
548              
549 0           print $handle &attach_headers('footer.txt', "Footer file");
550 0           print $handle join("", encode_base64(&get_content($files->{'footer.txt'})), "\n", "--$boundary\n");
551              
552 0           print $handle &attach_headers('list.txt', "Mailing List file");
553 0 0         open(FILE, "<", $files->{'list.txt'}) or die("$!");
554 0           my $buf = "";
555 0           while(read(FILE, $buf, 60*57)) { print $handle encode_base64($buf); }
  0            
556 0           close(FILE);
557 0           print $handle join("", "\n", "--$boundary--\n");
558 0           };
559 0           my $sm = Mail::Salsa::Sendmail->new(
560             smtp_server => $self->{'smtp_server'},
561             timeout => $self->{'timeout'},
562             );
563 0           $sm->everything(
564             mail_from => "salsa-master\@$domain",
565             rcpt_to => ["$name-owner\@$domain"],
566             data => $refsub
567             );
568 0           return();
569             }
570              
571             # Autoload methods go after =cut, and are processed by the autosplit program.
572              
573             1;
574             __END__