package Mail::Toaster::Setup::Maildrop; use strict; use warnings; our $VERSION = '5.54'; use File::Basename; use English '-no_match_vars'; use Params::Validate ':all'; use lib 'lib'; use parent 'Mail::Toaster::Base'; sub install { my $self = shift; my %p = validate( @_, { $self->get_std_opts },); my $ver = $self->conf->{'install_maildrop'} or do { $self->audit( "skipping maildrop install, not enabled."); return 0; }; my $prefix = $self->conf->{'toaster_prefix'} || "/usr/local"; if ( $ver eq "port" || $ver eq "1" ) { if ( $OSNAME eq "freebsd" ) { # pre-installing pcre supresses a dialog $self->freebsd->install_package( "pcre" ); $self->freebsd->install_port( "pcre", options => '# written by Mail::Toaster # Options for pcre-8.33 _OPTIONS_READ=pcre-8.33 _FILE_COMPLETE_OPTIONS_LIST=STACK_RECURSION OPTIONS_FILE_SET+=STACK_RECURSION ', ); $self->freebsd->install_package( "maildrop" ); $self->freebsd->install_port( "maildrop", flags => "WITH_MAILDIRQUOTA=1",); } elsif ( $OSNAME eq "darwin" ) { $self->darwin->install_port( "maildrop" ); } $ver = "2.7.1"; } $self->util->find_bin( "maildrop", fatal => 0 ) or $self->util->install_from_source( package => 'maildrop-' . $ver, site => 'http://' . $self->conf->{'toaster_sf_mirror'}, url => '/courier', targets => [ './configure --prefix=' . $prefix . ' --exec-prefix=' . $prefix, 'make', 'make install-strip', 'make install-man' ], source_sub_dir => 'mail', ); my $etcmail = "$prefix/etc/mail"; unless ( -d $etcmail ) { mkdir( $etcmail, oct('0755') ) or $self->util->mkdir_system( dir => $etcmail, mode=>'0755' ); } $self->filter(); $self->imap_subscribe(); $self->filter_logs(); }; sub filter { my $self = shift; my %p = validate( @_, { $self->get_std_opts },); my $prefix = $self->conf->{'toaster_prefix'} || "/usr/local"; my $logbase = $self->conf->{'qmail_log_base'}; if ( !$logbase ) { $logbase = -d "/var/log/qmail" ? "/var/log/qmail" : "/var/log/mail"; } my $filterfile = $self->conf->{'filtering_maildrop_filter_file'} || "$prefix/etc/mail/mailfilter"; my ( $file, $path ) = fileparse($filterfile); chop $path; $self->util->mkdir_system( dir => $path ) if ! -d $path; return $self->error( "$path doesn't exist and I couldn't create it.") if ! -d $path; my @lines = $self->filter_file( logbase => $logbase ); my $user = $self->conf->{'vpopmail_user'} || "vpopmail"; my $group = $self->conf->{'vpopmail_group'} || "vchkpw"; # if the mailfilter file doesn't exist, create it if ( -e $filterfile ) { $self->util->file_write( "$filterfile.new", lines => \@lines, mode =>'0600' ); $self->util->install_if_changed( newfile => "$filterfile.new", existing => $filterfile, mode => '0600', clean => 0, notify => 1, archive => 1, ); } else { $self->util->file_write( $filterfile, lines => \@lines, mode => '0600' ); $self->audit("installed new $filterfile, ok"); }; $self->util->chown( $filterfile, uid => $user, gid => $group ); $file = "/etc/newsyslog.conf"; if ( -e $file && ! `grep maildrop $file`) { $self->util->file_write( $file, lines => ["/var/log/mail/maildrop.log $user:$group 644 3 1000 * Z"], append => 1, ); }; return 1; } sub filter_file { my $self = shift; my %p = validate( @_, { 'logbase' => SCALAR, $self->get_std_opts, },); my $logbase = $p{'logbase'}; my $prefix = $self->conf->{'toaster_prefix'} || "/usr/local"; my $filterfile = $self->conf->{'filtering_maildrop_filter_file'} || "$prefix/etc/mail/mailfilter"; my @lines = 'SHELL="/bin/sh"'; push @lines, <<"EOMAILDROP"; import EXT import HOST VHOME=`pwd` TIMESTAMP=`date "+\%b \%d \%H:\%M:\%S"` ## # title: mailfilter-site # author: Matt Simerson # version 2.17 # # This file is automatically generated by toaster_setup.pl, # DO NOT HAND EDIT, your changes may get overwritten! # # Make changes to toaster-watcher.conf, and run # toaster_setup.pl -s maildrop to rebuild this file. Old versions # are preserved as $filterfile.timestamp # # Usage: Install this file in your local etc/mail/mailfilter. On # your system, this is $prefix/etc/mail/mailfilter # # Create a .qmail file in each users Maildir as follows: # echo "| $prefix/bin/maildrop $prefix/etc/mail/mailfilter" \ # > ~vpopmail/domains/example.com/user/.qmail # # You can also use qmailadmin v1.0.26 or higher to do that for you # via it is --enable-modify-spam and --enable-spam-command options. # This is the default behavior for your Mail::Toaster. # # Environment Variables you can import from qmail-local: # SENDER is the envelope sender address # NEWSENDER is the forwarding envelope sender address # RECIPIENT is the envelope recipient address, local\@domain # USER is user # HOME is your home directory # HOST is the domain part of the recipient address # LOCAL is the local part # EXT is the address extension, ext. # HOST2 is the portion of HOST preceding the last dot # HOST3 is the portion of HOST preceding the second-to-last dot # HOST4 is the portion of HOST preceding the third-to-last dot # EXT2 is the portion of EXT following the first dash # EXT3 is the portion following the second dash; # EXT4 is the portion following the third dash. # DEFAULT is the portion corresponding to the default part of the .qmail-... file name # DEFAULT is not set if the file name does not end with default # DTLINE and RPLINE are the usual Delivered-To and Return-Path lines, including newlines # # qmail-local will be calling maildrop. The exit codes that qmail-local # understands are: # 0 - delivery is complete # 111 - temporary error # xxx - unknown failure ## EOMAILDROP $self->conf->{'filtering_verbose'} ? push @lines, qq{logfile "$logbase/maildrop.log"} : push @lines, qq{#logfile "$logbase/maildrop.log"}; push @lines, <<'EOMAILDROP2'; log "$TIMESTAMP - BEGIN maildrop processing for $EXT@$HOST ===" # I have seen cases where EXT or HOST is unset. This can be caused by # various blunders committed by the sysadmin so we should test and make # sure things are not too messed up. # # By exiting with error 111, the error will be logged, giving an admin # the chance to notice and fix the problem before the message bounces. if ( $EXT eq "" ) { log " FAILURE: EXT is not a valid value" log "=== END === $EXT@$HOST failure (EXT variable not imported)" EXITCODE=111 exit } if ( $HOST eq "" ) { log " FAILURE: HOST is not a valid value" log "=== END === $EXT@$HOST failure (HOST variable not imported)" EXITCODE=111 exit } EOMAILDROP2 my $spamass_method = $self->conf->{'filtering_spamassassin_method'}; if ( $spamass_method eq "user" || $spamass_method eq "domain" ) { push @lines, <<"EOMAILDROP3"; ## # Note that if you want to pass a message larger than 250k to spamd # and have it processed, you will need to also set spamc -s. See the # spamc man page for more details. ## exception { if ( /^X-Spam-Status: /:h ) { # do not pass through spamassassin if the message already # has an X-Spam-Status header. log "Message already has X-Spam-Status header, skipping spamc" } else { if ( \$SIZE < 256000 ) # Filter if message is less than 250k { `test -x $prefix/bin/spamc` if ( \$RETURNCODE == 0 ) { log `date "+\%b \%d \%H:\%M:\%S"`" \$PID - running message through spamc" exception { xfilter '$prefix/bin/spamc -u "\$EXT\@\$HOST"' } } else { log " WARNING: no $prefix/bin/spamc binary!" } } } } EOMAILDROP3 } push @lines, <<"EOMAILDROP4"; ## # Include any rules set up for the user - this gives the # administrator a way to override the sitewide mailfilter file # # this is also the "suggested" way to set individual values # for maildrop such as quota. ## `test -r \$VHOME/.mailfilter` if( \$RETURNCODE == 0 ) { log " including \$VHOME/.mailfilter" exception { include \$VHOME/.mailfilter } } ## # create the maildirsize file if it does not already exist # (could also be done via "deliverquota user\@dom.com 10MS,1000C) ## `test -e \$VHOME/Maildir/maildirsize` if( \$RETURNCODE == 1) { VUSERINFO="$prefix/vpopmail/bin/vuserinfo" `test -x \$VUSERINFO` if ( \$RETURNCODE == 0) { log " creating \$VHOME/Maildir/maildirsize for quotas" `\$VUSERINFO -Q \$EXT\@\$HOST` `test -s "\$VHOME/Maildir/maildirsize"` if ( \$RETURNCODE == 0 ) { `/usr/sbin/chown vpopmail:vchkpw \$VHOME/Maildir/maildirsize` `/bin/chmod 640 \$VHOME/Maildir/maildirsize` } } else { log " WARNING: cannot find vuserinfo! Please edit mailfilter" } } EOMAILDROP4 my $head = $self->util->find_bin( 'head' ); push @lines, <<"EOMAILDROP5"; ## # Set MAILDIRQUOTA. If this is not set, maildrop and deliverquota # will not enforce quotas for message delivery. ## `test -e \$VHOME/Maildir/maildirsize` if( \$RETURNCODE == 0) { MAILDIRQUOTA=`$head -n1 \$VHOME/Maildir/maildirsize` } # if the user does not have a Spam folder, create it. `test -d \$VHOME/Maildir/.Spam` if( \$RETURNCODE == 1 ) { MAILDIRMAKE="$prefix/bin/maildirmake" `test -x \$MAILDIRMAKE` if ( \$RETURNCODE == 1 ) { MAILDIRMAKE="$prefix/bin/maildrop-maildirmake" `test -x \$MAILDIRMAKE` } if ( \$RETURNCODE == 1 ) { log " WARNING: no maildirmake!" } else { log " creating \$VHOME/Maildir/.Spam " `\$MAILDIRMAKE -f Spam \$VHOME/Maildir` `$prefix/sbin/subscribeIMAP.sh Spam \$VHOME` } } ## # The message should be tagged, so lets bag it. ## # HAM: X-Spam-Status: No, score=-2.6 required=5.0 # SPAM: X-Spam-Status: Yes, score=8.9 required=5.0 # # Note: SA < 3.0 uses "hits" instead of "score" # # if ( /^X-Spam-Status: *Yes/) # test if spam status is yes # The following regexp matches any spam message and sets the # variable \$MATCH2 to the spam score. if ( /X-Spam-Status: Yes/:h) { if ( /X-Spam-Status: Yes, (hits|score)=([\\d\\.\\-]+)\\s/:h) { EOMAILDROP5 my $discard = $self->conf->{'filtering_spama_discard_score'}; my $pyzor = $self->conf->{'filtering_report_spam_pyzor'}; my $sa_report = $self->conf->{'filtering_report_spam_spamassassin'}; if ($discard) { push @lines, <<"EOMAILDROP6"; # if the message scored a $discard or higher, then there is no point in # keeping it around. SpamAssassin already knows it as spam, and # has already "autolearned" from it if you have that enabled. The # end user likely does not want it. If you wanted to cc it, or # deliver it elsewhere for inclusion in a spam corpus, you could # easily do so with a cc or xfilter command if ( \$MATCH2 >= $discard ) # from Adam Senuik post to mail-toasters { EOMAILDROP6 if ( $pyzor && !$sa_report ) { push @lines, <<"EOMAILDROP7"; `test -x $prefix/bin/pyzor` if( \$RETURNCODE == 0 ) { # if the pyzor binary is installed, report all messages with # high spam scores to the pyzor servers log " SPAM: score \$MATCH2: reporting to Pyzor" exception { xfilter "$prefix/bin/pyzor report" } } EOMAILDROP7 } if ($sa_report) { push @lines, <<"EOMAILDROP8"; # new in version 2.5 of Mail::Toaster mailfiter `test -x $prefix/bin/spamassassin` if( \$RETURNCODE == 0 ) { # if the spamassassin binary is installed, report messages with # high spam scores to spamassassin (and consequently pyzor, dcc, # razor, and SpamCop) log " SPAM: score \$MATCH2: reporting spam via spamassassin -r" exception { xfilter "$prefix/bin/spamassassin -r" } } EOMAILDROP8 } push @lines, <<"EOMAILDROP9"; log " SPAM: score \$MATCH2 exceeds $discard: nuking message!" log "=== END === \$EXT\@\$HOST success (discarded)" EXITCODE=0 exit } EOMAILDROP9 } push @lines, <<"EOMAILDROP10"; log " SPAM: score \$MATCH2: delivering to \$VHOME/Maildir/.Spam" log "=== END === \$EXT\@\$HOST success" exception { to "\$VHOME/Maildir/.Spam" } } else { log " SpamAssassin regexp match error!" } } if ( /^X-Spam-Status: No, (score|hits)=([\\d\\.\\-]+)\\s/:h) { log " message is SA clean (\$MATCH2)" } EOMAILDROP10 if ( $self->conf->{install_dspam} ) { push @lines, <<"EOMAILDROP_DSPAM"; if ( /^X-DSPAM-Result: /:h ) { log " has X-DSPAM-Result header" } else { if ( \$SIZE < 4194304 ) # Filter if message is less than 4MB { `test -x $prefix/bin/dspam` if ( \$RETURNCODE == 0 ) { log `date "+\%b \%d \%H:\%M:\%S"`" \$PID - running message through dspam" exception { xfilter '$prefix/bin/dspam --user \$EXT\@\$HOST --process --deliver=innocent,spam --stdout' } } else { log " WARNING: no $prefix/bin/dspam binary!" } } } ## # Check for DSPAM tag ## # HAM: X-DSPAM-Result: Innocent, probability=0.0000, confidence=0.94 # SPAM: X-DSPAM-Result: Spam, probability=1.0000, confidence=0.99 # if ( /X-DSPAM-Result: /:h) { if ( /X-DSPAM-Result: Spam/:h) { if ( /X-DSPAM-Result: Spam, probability=([\\d\\.]+), confidence=([\\d\\.]+)/:h) { if ( \$MATCH1 == 1 && \$MATCH2 >= .50 ) { if ( /^X-Spam-Status: /:h ) { if ( /X-Spam-Status: Yes/:h) { log " DSPAM: delivering spam (\$MATCH2) to \$VHOME/Maildir/.Spam" log "=== END === \$EXT\@\$HOST success" exception { to "\$VHOME/Maildir/.Spam" } } else { log " DSPAM says spam (\$MATCH2) SA says no" } } } else { log " DSPAM suspects spam (\$MATCH2)" } } else { log " DSPAM regexp match error!" } } else { log " dspam says innocent" } } EOMAILDROP_DSPAM ; }; push @lines, <<"EOMAILDROP11"; ## # Include any other rules that the user might have from # sqwebmail or other compatible program ## `test -r \$VHOME/Maildir/.mailfilter` if( \$RETURNCODE == 0 ) { log " including \$VHOME/Maildir/.mailfilter" exception { include \$VHOME/Maildir/.mailfilter } } `test -r \$VHOME/Maildir/mailfilter` if( \$RETURNCODE == 0 ) { log " including \$VHOME/Maildir/mailfilter" exception { include \$VHOME/Maildir/mailfilter } } log " delivering to \$VHOME/Maildir" # make sure the deliverquota binary exists and is executable # if not, then we cannot enforce quotas. If we do not check # and the binary is missing, maildrop silently discards mail. DELIVERQUOTA="$prefix/bin/deliverquota" `test -x \$DELIVERQUOTA` if ( \$RETURNCODE == 1 ) { DELIVERQUOTA="$prefix/bin/maildrop-deliverquota" `test -x \$DELIVERQUOTA` } if ( \$RETURNCODE == 1 ) { log " WARNING: no deliverquota!" log "=== END === \$EXT\@\$HOST success" exception { to "\$VHOME/Maildir" } } else { exception { xfilter "\$DELIVERQUOTA -w 90 \$VHOME/Maildir" } ## # check to make sure the message was delivered # returncode 77 means that out maildir was overquota - bounce mail ## if( \$RETURNCODE == 77) { #log " BOUNCED: bouncesaying '\$EXT\@\$HOST is over quota'" log "=== END === \$EXT\@\$HOST bounced" to "|/var/qmail/bin/bouncesaying '\$EXT\@\$HOST is over quota'" } else { log "=== END === \$EXT\@\$HOST success (quota)" EXITCODE=0 exit } } log "WARNING: This message should never be printed!" EOMAILDROP11 return @lines; } sub filter_logs { my $self = shift; my $log = $self->conf->{'qmail_log_base'} || "/var/log/mail"; $self->util->mkdir_system( dir => $log, verbose => 0 ) if ! -d $log; $self->util->chown( $log, uid => $self->conf->{'qmail_log_user'} || 'qmaill', gid => $self->conf->{'qmail_log_group'} || 'qnofiles', sudo => $UID == 0 ? 0 : 1, ); my $logf = "$log/maildrop.log"; $self->util->file_write( $logf, lines => ["begin"] ) if ! -e $logf; $self->util->chown( $logf, uid => $self->conf->{'vpopmail_user'} || "vpopmail", gid => $self->conf->{'vpopmail_group'} || "vchkpw", sudo => $UID == 0 ? 0 : 1, ); } sub imap_subscribe { my $self = shift; my $prefix = $self->conf->{'toaster_prefix'} || "/usr/local"; my $sub_file = "$prefix/sbin/subscribeIMAP.sh"; my $sub_bin = $self->util->find_bin( "$prefix/sbin/subscribeIMAP.sh", verbose => 0, fatal => 0 ); return 1 if ( $sub_bin && -e $sub_bin ); my @lines = '#!/bin/sh #!/bin/sh # # This subscribes the folder passed as $1 to courier imap # so that Maildir reading apps (Sqwebmail, Courier-IMAP) and # IMAP clients (squirrelmail, Mailman, etc) will recognize the # extra mail folder. # Matt Simerson - 12 June 2003 # Rob Lensen 29-04-2015 Dovecot changes LIST="$2/Maildir/subscriptions" if [ -f "$LIST" ]; then # if the file exists, check it for the new folder TEST=`cat "$LIST" | grep "$1"` # if it is not there, add it if [ "$TEST" = "" ]; then echo "$1" >> $LIST fi else # the file does not exist so we define the full list # and then create the file. FULL="INBOX\nSent\nTrash\nDrafts\n$1" echo -e $FULL > $LIST /usr/sbin/chown vpopmail:vchkpw $LIST /bin/chmod 644 $LIST fi '; $self->util->file_write( $sub_file, lines => \@lines ); $self->util->chmod( file_or_dir => $sub_file, mode => '0555', sudo => $UID == 0 ? 0 : 1, ); }; 1; __END__;