The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
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__;