1 # Functions for creating simple mail filtering rules
2 # XXX use same virtualmin spam detection trick for spam module
4 BEGIN { push(@INC, ".."); };
8 do 'autoreply-file-lib.pl';
10 if (&get_product_name() eq 'usermin') {
11 # If configured, check if this user has virtualmin spam filtering
12 # enabled before switching away from root
13 $autoreply_cmd = "$config_directory/forward/autoreply.pl";
15 if ($config{'virtualmin_spam'} &&
16 -x $config{'virtualmin_spam'}) {
17 local $out = &backquote_command(
18 "$config{'virtualmin_spam'} $remote_user ".
19 "</dev/null 2>/dev/null");
22 # Yes - we can show the user this
23 $global_spamassassin = 2;
24 $virtualmin_domain_id = $out;
28 # Copy autoreply.pl to /etc/usermin/forward, while we
30 local $autoreply_src = "$root_directory/forward/autoreply.pl";
31 local @rst = stat($autoreply_src);
32 local @cst = stat($autoreply_cmd);
33 if (!@cst || $cst[7] != $rst[7]) {
34 ©_source_dest($autoreply_src, $autoreply_cmd);
35 &set_ownership_permissions(
36 undef, undef, 0755, $autoreply_cmd);
39 &switch_to_remote_user();
42 &create_user_config_dirs();
43 &foreign_require("procmail", "procmail-lib.pl");
46 # Running under Webmin, so different modules are used
47 &foreign_require("procmail", "procmail-lib.pl");
48 &foreign_require("mailboxes", "mailboxes-lib.pl");
49 &foreign_require("usermin", "usermin-lib.pl");
51 $mailboxes::config{'mail_system'} == 1 ? "postfix" :
52 $mailboxes::config{'mail_system'} == 2 ? "qmailadmin" :
54 $autoreply_cmd = "$config_directory/$mail_system_module/autoreply.pl";
55 $user_autoreply_cmd = "$usermin::config{'usermin_dir'}/forward/autoreply.pl";
58 # list_filters([file])
59 # Returns a list of filter objects, which have a 1-to-1 correlation with
60 # procmail recipes. Any recipes too complex for parsing are not included.
65 local @pmrc = &procmail::parse_procmail_file($file || $procmail::procmailrc);
66 foreach my $r (@pmrc) {
67 # Check for un-supported recipes
68 local @conds = @{$r->{'conds'}};
69 if ($r->{'block'} || $r->{'name'}) {
74 local %flags = map { $_, 1 } @{$r->{'flags'}};
76 # Check for bounce condition
78 if (@conds && $conds[0]->[0] eq '!' &&
79 $conds[0]->[1] =~ /FROM_MAILER/) {
83 next if (@conds > 1); # Multiple conditions are not supported
85 # Work out condition type
86 local ($condtype, $cond);
88 ($condtype, $cond) = @{$conds[0]};
89 if ($condtype && $condtype ne "<" && $condtype ne ">") {
90 # Unsupported conditon type
95 # Work out action type
96 local ($actionspam, $actionreply);
97 if ($r->{'type'} eq '|' &&
98 $r->{'action'} =~ /spamassassin|spamc/) {
101 elsif ($r->{'type'} eq '|' &&
102 ($r->{'action'} =~ /^\Q$autoreply_cmd\E\s+(\S+)/ ||
103 $user_autoreply_cmd &&
104 $user_autoreply_cmd &&
105 $r->{'action'} =~ /^\Q$user_autoreply_cmd\E\s+(\S+)/)) {
108 elsif ($r->{'type'} && $r->{'type'} ne '!') {
109 # Unsupported action type
113 # Finally create the simple object
114 local $simple = { 'condtype' => $condtype,
116 'nocond' => !scalar(@{$r->{'conds'}}),
117 'body' => $flags{'B'},
118 'continue' => $flags{'c'},
119 'actiontype' => $r->{'type'},
120 'action' => $r->{'action'},
121 'nobounce' => $nobounce,
122 'index' => scalar(@rv),
127 $simple->{'actionspam'} = 1;
128 delete($simple->{'actiontype'});
129 delete($simple->{'action'});
132 # Check for throw away
133 if ($simple->{'actiontype'} eq '' &&
134 $simple->{'action'} eq '/dev/null') {
135 $simple->{'actionthrow'} = 1;
136 delete($simple->{'actiontype'});
137 delete($simple->{'action'});
140 # Check for default delivery
141 if ($simple->{'actiontype'} eq '' &&
142 $simple->{'action'} eq '$DEFAULT') {
143 $simple->{'actiondefault'} = 1;
144 delete($simple->{'actiontype'});
145 delete($simple->{'action'});
148 # Read autoreply file
150 $simple->{'actionreply'} = $actionreply;
151 $simple->{'reply'} = { };
152 &read_autoreply($actionreply, $simple->{'reply'});
153 delete($simple->{'actiontype'});
154 delete($simple->{'action'});
157 # Split condition regexp into header and value, if possible
158 if ($simple->{'condtype'} ne '<' && $simple->{'condtype'} ne '>' &&
159 !$simple->{'body'} &&
160 $simple->{'cond'} =~ /^\^?([a-zA-Z0-9\-]+):\s*(.*)/) {
161 local ($h, $v) = ($1, $2);
162 if ($h eq "X-Spam-Status" && $v eq "Yes") {
163 # Special case for spam detection
164 $simple->{'condspam'} = 1;
166 elsif ($h eq "X-Spam-Level" && $v =~ /^(\\\*)+$/) {
167 # Spam above some level
168 $simple->{'condlevel'} = length($v)/2;
171 # Match on some header
172 $simple->{'condheader'} = $h;
173 $simple->{'condvalue'} = $v;
175 delete($simple->{'cond'});
183 # create_filter(&filter)
184 # Create a new filter by adding a procmail recipe
187 local ($filter) = @_;
189 &update_filter_recipe($filter, $recipe);
190 &procmail::create_recipe($recipe);
191 &setup_forward_procmail();
194 # modify_filter(&filter)
195 # Change a filter by modifying the underlying procmail recipe
198 local ($filter) = @_;
199 &update_filter_recipe($filter, $filter->{'recipe'});
200 &procmail::modify_recipe($filter->{'recipe'});
201 &setup_forward_procmail();
204 # insert_filter(&filter)
205 # Like create_filter, but adds to the top of the .procmailrc
208 local ($filter) = @_;
210 &update_filter_recipe($filter, $recipe);
211 local @pmrc = &procmail::parse_procmail_file(
212 $filter->{'file'} || $procmail::procmailrc);
214 &procmail::create_recipe_before($recipe, $pmrc[0]);
217 &procmail::create_recipe($recipe);
219 &setup_forward_procmail();
222 # update_filter_recipe(&filter, &recipe)
223 # Update a procmail recipe based on some filter
224 sub update_filter_recipe
226 local ($filter, $recipe) = @_;
228 # Set condition section
231 if ($filter->{'condspam'}) {
232 @conds = ( [ "", "X-Spam-Status: Yes" ] );
234 elsif ($filter->{'condlevel'}) {
235 local $stars = join("", map { "\\*" } (1..$filter->{'condlevel'}));
236 @conds = ( [ "", "^"."X-Spam-Level: $stars" ] );
238 elsif ($filter->{'condheader'}) {
239 @conds = ( [ "", "^".$filter->{'condheader'}.": ".
240 $filter->{'condvalue'} ] );
242 elsif ($filter->{'condtype'} eq '<' || $filter->{'condtype'} eq '>') {
243 @conds = ( [ $filter->{'condtype'}, $filter->{'cond'} ] );
245 elsif ($filter->{'cond'}) {
246 @conds = ( [ "", $filter->{'cond'} ] );
250 if ($filter->{'actionspam'}) {
251 &foreign_require("spam", "spam-lib.pl");
252 $recipe->{'type'} = '|';
253 $recipe->{'action'} = &spam::get_procmail_command();
254 push(@flags, "f", "w");
256 elsif ($filter->{'actionthrow'}) {
257 $recipe->{'type'} = '';
258 $recipe->{'action'} = '/dev/null';
260 elsif ($filter->{'actiondefault'}) {
261 $recipe->{'type'} = '';
262 $recipe->{'action'} = '$DEFAULT';
264 elsif ($filter->{'actionreply'}) {
265 $recipe->{'type'} = '|';
266 $recipe->{'action'} =
267 "$autoreply_cmd $filter->{'reply'}->{'autoreply'} $remote_user";
268 &write_autoreply($filter->{'reply'}->{'autoreply'},
272 $recipe->{'type'} = $filter->{'actiontype'};
273 $recipe->{'action'} = $filter->{'action'};
274 local $folder = &file_to_folder($filter->{'action'}, [ ], undef, 1);
275 if ($recipe->{'type'} eq '' && $folder->{'type'} == 1) {
276 # Enable locking for file delivery
277 $recipe->{'lockfile'} ||= "";
279 if ($filter->{'actiontype'} eq '!' && $filter->{'nobounce'}) {
280 # Add condition to suppress forwarding of bounces
281 unshift(@conds, [ '!', '^FROM_MAILER' ]);
284 $recipe->{'conds'} = \@conds;
287 push(@flags, "B") if ($filter->{'body'});
288 push(@flags, "c") if ($filter->{'continue'});
289 $recipe->{'flags'} = [ &unique(@flags) ];
292 # delete_filter(&filter)
293 # Delete a filter by removing the underlying procmail rule
296 local ($filter) = @_;
297 &procmail::delete_recipe($filter->{'recipe'});
298 &setup_forward_procmail();
299 if ($filter->{'actionreply'} && !-d $filter->{'actionreply'}) {
300 &unlink_file($filter->{'actionreply'});
304 # swap_filters(&filter1, &filter2)
305 # Swap two filters in the config file
308 local ($filter1, $filter2) = @_;
309 &procmail::swap_recipes($filter1->{'recipe'}, $filter2->{'recipe'});
310 &setup_forward_procmail();
313 # file_to_folder(file, &folders, [homedir], [fake-if-missing])
314 # Given a path like mail/foo or ~/mail/foo or $HOME/mail/foo or
315 # /home/bob/mail/foo, returns the folder object for it.
318 local ($file, $folders, $home, $fake) = @_;
319 $home ||= $remote_user_info[7];
320 $file =~ s/^\~/$home/;
321 $file =~ s/^\$HOME/$home/;
322 if ($file !~ /^\//) {
323 $file = "$home/$file";
325 local ($folder) = grep { $_->{'file'} eq $file ||
326 $_->{'file'}.'/' eq $file } @$folders;
327 if (!$folder && $fake) {
328 # Create a fake folder object to match
329 $folder = { 'file' => $file,
332 if ($folder->{'file'} =~ s/\/$//) {
333 $folder->{'type'} = 2;
335 $folder->{'file'} =~ /\/\.?([^\/]+)$/;
336 $folder->{'name'} = $1;
337 if (lc($folder->{'name'}) eq 'spam') {
338 $folder->{'spam'} = 1;
339 $folder->{'name'} = "Spam";
345 # get_global_spamassassin()
346 # Returns true if spamasassin is run globally
347 sub get_global_spamassassin
349 return $global_spamassassin if ($global_spamassassin);
350 &foreign_require("spam", "spam-lib.pl");
351 local @recipes = &procmail::parse_procmail_file(
352 $spam::config{'global_procmailrc'});
353 return &spam::find_spam_recipe(\@recipes) ? 1 : 0;
356 # get_global_spam_path()
357 # Returns the global path to which spam is delivered, typically by a
358 # Virtualmin per-domain procmail file
359 sub get_global_spam_path
361 &foreign_require("spam", "spam-lib.pl");
362 if ($virtualmin_domain_id) {
363 # Read the Virtualmin procmailrc for the domain
364 local $vmpmrc = "$config{'virtualmin_config'}/procmail/".
365 $virtualmin_domain_id;
366 local @vmrecipes = &procmail::parse_procmail_file($vmpmrc);
367 local $spamrec = &spam::find_file_recipe(\@vmrecipes);
369 return $spamrec->{'action'};
372 # Also check the global /etc/procmailrc
373 local @recipes = &procmail::parse_procmail_file(
374 $spam::config{'global_procmailrc'});
375 local $spamrec = &spam::find_file_recipe(\@recipes);
377 return $spamrec->{'action'};
384 # get_global_spam_delete()
385 # Returns the global score above which spam is deleted, typically by a
386 # Virtualmin per-domain procmail file
387 sub get_global_spam_delete
389 &foreign_require("spam", "spam-lib.pl");
390 if ($virtualmin_domain_id) {
391 # Read the Virtualmin procmailrc for the domain
392 local $vmpmrc = "$config{'virtualmin_config'}/procmail/".
393 $virtualmin_domain_id;
394 local @vmrecipes = &procmail::parse_procmail_file($vmpmrc);
395 local ($spamrec, $level) = &spam::find_delete_recipe(\@vmrecipes);
400 # Also check the global /etc/procmailrc
401 local @recipes = &procmail::parse_procmail_file(
402 $spam::config{'global_procmailrc'});
403 local ($spamrec, $level) = &spam::find_delete_recipe(\@recipes);
414 return &foreign_installed("spam");
417 # get_override_alias()
418 # Check for any mail alias matching this user, which is defined in /etc/aliases
419 # as an entry matching his username.
420 sub get_override_alias
422 local @afiles = split(/\t+/, $config{'alias_files'});
423 foreach my $alias (&list_aliases(\@afiles)) {
424 if ($alias->{'name'} eq $remote_user && $alias->{'enabled'}) {
431 # describe_alias_dest(&values)
432 # Returns a text description of some alias destination
433 sub describe_alias_dest
435 local ($values) = @_;
437 foreach my $v (@$values) {
438 local ($atype, $adesc) = &alias_type($v);
439 if ($atype == 1 && $adesc eq "\\$remote_user") {
440 push(@rv, $text{'aliases_your'});
442 elsif ($atype == 1 && $adesc =~ /^\\(\S+)$/) {
443 push(@rv, &text('aliases_other', "<tt>$1</tt>"));
445 elsif ($atype == 3 && $adesc eq "/dev/null") {
446 push(@rv, $text{'aliases_delete'});
448 elsif ($atype == 4 && $adesc =~ /^(.*)\/autoreply.pl\s+(\S+)/) {
449 # Autoreply from file .. check contents
450 local $auto = &read_file_contents("$2");
452 local @lines = grep { !/^(\S+):/} split(/\r?\n/, $auto);
453 local $msg = join(" ", @lines);
454 $msg = substr($msg, 0, 100)." ..."
455 if (length($msg) > 100);
456 push(@rv, &text('aliases_auto', "<i>$msg</i>"));
459 push(@rv, &text('aliases_type5', "<tt>$2</tt>"));
462 elsif ($atype == 4 && $adesc =~ /^(.*)\/filter.pl\s+(\S+)/) {
464 push(@rv, &text('aliases_type6', "<tt>$2</tt>"));
467 push(@rv, &text('aliases_type'.$atype, $adesc));
473 # is_table_comment(line, [force-prefix])
474 # Returns the comment text if a line contains a comment, like # foo. This is
475 # defined only because functions in aliases-lib.pl call it.
478 local ($line, $force) = @_;
480 return $line =~ /^\s*#+\s*Webmin:\s*(.*)/ ? $1 : undef;
483 return $line =~ /^\s*#+\s*(.*)/ ? $1 : undef;
487 # describe_condition(&filter)
488 # Returns a human-readable description of the filter condition, and a flag
489 # indicating if this is an 'always' condition.
490 sub describe_condition
494 local $lastalways = 0;
495 if ($f->{'condspam'}) {
496 $cond = $text{'index_cspam'};
498 elsif ($f->{'condlevel'}) {
499 $cond = &text('index_clevel', $f->{'condlevel'});
501 elsif ($f->{'condheader'}) {
502 if ($f->{'condvalue'} =~ /^\.\*(.*)\$$/) {
503 $cond = &text('index_cheader2',
504 "<tt>".&html_escape($f->{'condheader'})."</tt>",
505 "<tt>".&html_escape(&prettify_regexp("$1"))."</tt>");
507 elsif ($f->{'condvalue'} =~ /^\.\*(.*)\.\*$/ ||
508 $f->{'condvalue'} =~ /^\.\*(.*)$/) {
509 $cond = &text('index_cheader1',
510 "<tt>".&html_escape($f->{'condheader'})."</tt>",
511 "<tt>".&html_escape(&prettify_regexp("$1"))."</tt>");
513 elsif ($f->{'condvalue'} =~ /^(.*)\.\*$/ ||
514 $f->{'condvalue'} =~ /^(.*)$/) {
515 $cond = &text('index_cheader0',
516 "<tt>".&html_escape($f->{'condheader'})."</tt>",
517 "<tt>".&html_escape(&prettify_regexp("$1"))."</tt>");
520 elsif ($f->{'condtype'} eq '<' || $f->{'condtype'} eq '>') {
521 $cond = &text('index_csize'.$f->{'condtype'},
522 &nice_size($f->{'cond'}));
524 elsif ($f->{'cond'}) {
525 $cond = &text($f->{'body'} ? 'index_cre2' : 'index_cre',
526 "<tt>".&html_escape($f->{'cond'})."</tt>");
529 $cond = $text{'index_calways'};
530 if (!$f->{'continue'} && !$f->{'actionspam'}) {
534 return wantarray ? ( $cond, $lastalways ) : $cond;
537 # prettify_regexp(string)
538 # If a string contains only \ quoted special characters, remove the \s
544 if ($re =~ /^[a-zA-Z0-9_ ]*$/) {
545 $str =~ s/\\(.)/$1/g;
550 # describe_action(&filter, &folder, [homedir])
551 # Returns a human-readable description for the delivery action for some folder
554 local ($f, $folders, $home) = @_;
556 if ($f->{'actionspam'}) {
557 $action = $text{'index_aspam'};
559 elsif ($f->{'actionthrow'}) {
560 $action = $text{'index_athrow'};
562 elsif ($f->{'actiondefault'}) {
563 $action = $text{'index_adefault'};
565 elsif ($f->{'actiontype'} eq '!') {
566 $action = &text('index_aforward',
567 "<tt>$f->{'action'}</tt>");
569 elsif ($f->{'actionreply'}) {
570 $action = &text('index_areply',
571 "<i>".&html_escape(substr(
572 $f->{'reply'}->{'autotext'}, 0, 50))."</i>");
575 # Work out what folder
576 local $folder = &file_to_folder($f->{'action'}, $folders, $home);
578 if (&get_product_name() eq 'usermin') {
579 &foreign_require("mailbox", "mailbox-lib.pl");
580 local $id = &mailbox::folder_name($folder);
581 $action = &text('index_afolder',
582 "<a href='../mailbox/index.cgi?id=$id'>".
583 "$folder->{'name'}</a>");
586 local $id = &mailboxes::folder_name($folder);
587 if (&foreign_available("mailboxes")) {
588 $action = &text('index_afolder',
589 "<a href='../mailboxes/list_mail.cgi?user=".
590 &urlize($folder->{'user'})."&folder=".
591 $folder->{'index'}."'>$folder->{'name'}</a>");
594 $action = &text('index_afolder',
600 $action = &text('index_afile',
601 "<tt>$f->{'action'}</tt>");
604 if ($f->{'continue'}) {
605 $action = &text('index_acontinue', $action);
610 # can_simple_autoreply()
611 # Returns 1 if the current filter rules are simple enough to allow an autoreply
612 # to be added or removed.
613 sub can_simple_autoreply
615 return 1; # Always true for now
618 # can_simple_forward()
619 # Returns 1 if the current filter rules are simple enough to allow a mail
620 # forwarder to be added or removed
621 sub can_simple_forward
623 return 1; # Always can for now
626 # no_user_procmailrc()
627 # Returns 1 if /etc/procmailrc has a recipe to always deliver to the user's
628 # mailbox, which prevents this module from configuring anything useful
629 sub no_user_procmailrc
631 local %sconfig = &foreign_config("spam");
632 local @recipes = &procmail::parse_procmail_file(
633 $sconfig{'global_procmailrc'});
634 local ($force) = grep { $_->{'action'} eq '$DEFAULT' &&
635 !@{$_->{'conds'}} } @recipes;
639 # setup_forward_procmail()
640 # If configured, create a .forward file that runs procmail (if not setup yet)
641 sub setup_forward_procmail
643 return 0 if (!$config{'forward_procmail'});
644 return 0 if (!$module_info{'usermin'});
645 local $fwdfile = "$remote_user_info[7]/.forward";
646 local $procmail = &has_command("procmail");
647 return 0 if (!$procmail);
648 local $lref = &read_file_lines($fwdfile);
650 foreach my $l (@$lref) {
651 if ($l =~ /\Q$procmail\E/) {
656 &unflush_file_lines($fwdfile);
660 push(@$lref, "|$procmail");
661 &flush_file_lines($fwdfile);