#!/usr/bin/perl
#
# Sendmail Milter to perform SPF lookups
#
# (If you use the shebang line, make sure it contains
# a thread-enabled Perl!)
#
# Code by Mark Kramer <admin@asarian-host.net> on December 3, 2003
#
# Version 1.40
#
# Last revision: March 27, 2004
#
# With thanks to Alain Knaff for adding improved "Getopt" functionality,
# waitpid stuff to ensure spf-milter parent does not exit until child
# is really up and running, a new option to kill the milter, and one to
# add local policy.

# Tested under Perl, v5.8.0 built for i386-freebsd-thread-multi,
# using the Sendmail::Milter 0.18 engine.
#
# Licensed under GPL
#
# see: http://www.openspf.org
#      http://www.libsrs2.org/srs/srs.pdf
#
# availability: bundled with Mail::SPF::Query on CPAN
#               or at http://www.openspf.org/downloads.html
#
# this version is compatible with SPF draft 02.9.7.
#

# INSTALLATION:
# =============
#
# Basic INSTALL doc at http://www.openspf.org/sendmail-milter-INSTALL.txt
#
# Adiitional install notes by Alain Knaff:
#
# The milter must be started/stopped explicitly before/after sendmail.
# Add the following to /etc/init.d/sendmail to start it (must be
# before starting sendmail):
#
#   $SPF_MILTER -l 'include:local-forwarders' mail
#
# where local-forwarders is the name of a pseudo-domain holding an SPF
# record describing all hosts allowed to bypass SPF checks (typically,
# foreign hosts on which your users have set up .forwards pointing
# towards addresses hosted by you). If none of your users have set up
# any forwarding, you can leave this away
#
# Add the following to stop it (must be after stopping sendmail):
#
#   $SPF_MILTER -k
#
# Note: This milter looks for the sendmail.cf file in /etc/mail. If
# your sendmail.cf lives elsewhere (SuSE), establish a symlink:
#   ln -s /etc/sendmail.cf /etc/mail/sendmail.cf
#
# ==============

# ----------------------------------------------------------
#                            config
# ----------------------------------------------------------

# where do we store pid, sock, and logs? No trailing / please!
# Set it at will, like '/var/spool/spf-milter', as long as it
# ends in "spf-milter". Sanity check, further down the road,
# will ensure that it does!
#
# If you change $basedir, be sure to make the same change to
# INPUT_MAIL_FILTER in your mc file!

my $basedir = '/var/spf-milter';

# Our main SRS object; adjust this to your server's needs!

my $srs = new Mail::SRS (Secret => 'whateverfloatsyourboat', MaxAge => 4, HashLength => 8, HashMin => 8, AlwaysRewrite => 1, Separator => '+');

# where do we log SPF activity?

my $SPF_LOG_FILENAME = POSIX::strftime ($basedir . "/spflog-%Y%m.log", localtime);

# do we feel a need to flock the SPF logfile?

use constant FLOCK_SPFLOG => 0;

# ----------------------------------------------------------
#          no user-serviceable parts below this line
# ----------------------------------------------------------

use POSIX qw (:sys_wait_h);
use Sendmail::Milter;
use Socket;
use Mail::SPF::Query;
use Mail::SRS;
use threads;
use threads::shared;
use strict;
use Getopt::Std;
use Errno qw (ESRCH EINTR);
require 5.8.0;

use vars qw/$opt_k $opt_l $opt_t $opt_m $opt_S $opt_r $opt_h $opt_T/;

my $pidFile = $basedir . '/spf-milter.pid';
my $sock = $basedir . '/spf-milter.sock';

my @extraParams : shared = ();

my $mx_mode : shared = 0;
my $our_hostname : shared = 0;
my $trust : shared = 1;
my $require_srs_dsn : shared = 0;
my $will_relay_srs1 : shared = 0;
my $tagOnly : shared = 0;

my ($conn, $user, $pid, $login, $pass, $uid, $gid);

# Feel free to replace this with your preferred logging scheme, eg Sys::Syslog or Log::Dispatch

sub write_log : locked {
    open  (SPFLOG, "+>>".$SPF_LOG_FILENAME) || (warn "$0: unable to write to $SPF_LOG_FILENAME: $!" && return);
    if (FLOCK_SPFLOG) {
        flock (SPFLOG, 2);
        seek  (SPFLOG, 0, 2);
    }
    print  SPFLOG localtime () . ": @_\n";
    close (SPFLOG);
}

sub log_error_and_exit : locked {
    write_log (@_);
    print STDERR "spf-milter: @_\n";
    exit 1;
}

# To accomodate the thread-unsafe Socket package, the one
# "socket_call" provides an additional pseudo-lock mechanism for use
# within the same thread. Since socket_call has the 'locked' attribute,
# within a single thread only one call can be made to it at the time. The
# first parameter to the call is either 1 or 2. The former returns the IP
# address of sockaddr_in; the latter does SPF::Query. Thus providing
# exclusivity within the same thread.
#
# Though I know you will try anyway, do NOT remove the 'locked' attribute;
# spf-milter WILL crash, sooner rather than later. The serialization
# effect of the extra locking mechanism is negligible; it will only occur
# when connect_callback and envfrom_callback (from two different threads)
# should wish to access socket_call at the same time. At any rate, I
# designed spf-milter to run super-stable. Adjust the code if your
# priority lies elsewhere.

sub socket_call : locked {
    # usage:
    #  socket_call (0) => undef
    #  socket_call (1, sockaddr_in)
    #  socket_call (2, "1.2.3.4", 'sender@example.com', 'helohostname.example.com')

    my $choice = shift;

    return undef if not $choice;

    if ($choice == 1) {

    # connect_callback parses (defined $sockaddr_in) as first parameter, thus
    # forming choice 1, or none at all. As with all calls to external
    # packages, we run them within an eval {} clause to prevent spf-milter
    # from dying on us.

        my ($port, $iaddr);
        eval {
           ($port, $iaddr) = sockaddr_in (shift);
            $choice = inet_ntoa ($iaddr);
        };
        return ($choice);
    } elsif ($choice == 2) {

        # Here we do SPF::Query. We parse $priv_data along from envfrom_callback,
        # as we want to store $smtp_comment for later use in eom_callback.
        #
        # We will not use the alternate 'best_guess' method here. Risking a 'fail'
        # from best_guess, prior to "Sunrise Date", is too rich for my blood.

        my $priv_data = shift;

        if (my $query = eval {new Mail::SPF::Query (ip => shift, sender => shift, helo => shift, @extraParams)}) {
            my ($call_status, $result, $smtp_comment, $header_comment, $spf_record);

            # In "mx" mode, we make a call to result2 (), instead of to result (),
            # to which we parse an extra parameter, $priv_data->{'to'}, so
            # result2 () can check against secondaries for the recipent.

            if ($mx_mode) {
                $call_status = eval {($result, $smtp_comment, $header_comment, $spf_record) = $query->result2 (shift)};
            } else {
                $call_status = eval {($result, $smtp_comment, $header_comment, $spf_record) = $query->result ()};
            }

            if ($call_status) {

                # Return $smtp_comment, if defined, else the prefab $header_comment.

                $smtp_comment ||= $header_comment;

                # Need to escape unprotected % characters in spf_smtp_comment,
                # or sendmail will use the default "Command rejected" message instead.
                # Noted by Paul Howarth

                $smtp_comment =~ s/%/%%/g;

                # Since $smtp_comment can be whatever is returned, we consider it highly
                # tainted, and first run it through a 'garbage' filter, so as to clear it
                # of weird characters, newlines, etc., that could potentially crash your
                # mailer (possible exploits?).

               ($priv_data->{'spf_smtp_comment'}   = $smtp_comment)   =~ tr/\000-\010\012-\037\200-\377/ /s;
               ($priv_data->{'spf_header_comment'} = $header_comment) =~ tr/\000-\010\012-\037\200-\377/ /s;
                return ($result);
            } else {
                return undef;
            }
        } else {
            return undef;
        }
    } else {
        return undef;
    }
}

# For some reason, the widespread misconception seems to have crept in
# that Sendmail::Milter private data must somehow be "frozen/thawed"
# before processing (a.l.a the namesake FreezeThaw package). This is not
# the case. FreezeThaw, and similar functions, which freeze referenced
# Perl structures into serialized versions, and thaw these serialized
# structures back into references, are ONLY required should you wish to
# transport entire hashes and such. But there is no need to do that. On a
# per-connection basis, at connect_callback, we declare a private hash,
# and set use "$ctx->setpriv" to set the reference to that hash:
#
# my $priv_data = {};
# $ctx->setpriv($priv_data);
#

sub connect_callback : locked {
    my $ctx = shift;
    my $priv_data = {};
    $priv_data->{'hostname'} = shift;
    my $sockaddr_in = shift;
    $priv_data->{'ipaddr'} = socket_call ((defined $sockaddr_in), $sockaddr_in);

    # Our hostname can be extracted from the j macro; idea by Alain Knaff
    # There is no need to reset it on each connection, though. It is now
    # a global variable, and has been taken out of the per-connection hash.

    $our_hostname ||= $ctx -> getsymval ('j');
    $ctx->setpriv($priv_data);
    return SMFIS_CONTINUE;
}

sub helo_callback : locked {
    my $ctx = shift;
    my $priv_data = $ctx->getpriv();
    $priv_data->{'helo'} = shift;

    # We also allow a bypass for STARTTLS authenticated users!

    $priv_data->{'is_authenticated'} = ($ctx -> getsymval ('{verify}') eq 'OK');
    $ctx->setpriv($priv_data);
    return SMFIS_CONTINUE;
}

sub envfrom_callback : locked {
    my $ctx = shift;
    my $priv_data = $ctx->getpriv();
   ($priv_data->{'from'} = lc (shift)) =~ s/[<>]//g;

    # Is this a DSN?

    $priv_data->{'bounce'} = ($priv_data->{'from'} eq '');

    # In case of a valid MAIL FROM: <>, SPF::Query checks against the HELO string,
    # with 'postmaster' as localpart, but will leave an empty $priv_data->{'from'}
    # variable (which, for instance, shows up in $header_comment as a double space
    # after "domain of"). Here we compensate for that.

    $priv_data->{'from'} ||= "postmaster\@$priv_data->{'helo'}";

    # Are we authenticated via SASL? Do not set if
    # we're already STARTTLS authenticated.

    $priv_data->{'is_authenticated'} ||= $ctx -> getsymval ('{auth_authen}');

    # envfrom_callback can be called more than once within the same connection;
    # delete $priv_data->{'spf_result'} on entry!

    delete $priv_data->{'spf_result'};

    # SASL/STARTTLS authenticated IP addresses always pass!

    if ($priv_data->{'is_authenticated'}) {
        $priv_data->{'spf_result'} = "pass";
        $priv_data->{'spf_header_comment'} = "$our_hostname: $priv_data->{'ipaddr'} is authenticated by a trusted mechanism";
        $ctx -> setpriv ($priv_data);
        return SMFIS_CONTINUE;
    }

    $ctx->setpriv($priv_data);

    # Do the Milter equivalent of "PrivacyOptions=needmailhelo". Needed for SPF.

    if (not $priv_data->{'helo'}) {
        $ctx->setreply('503', '5.0.0', "Polite people say HELO first");
        return SMFIS_REJECT;
    }

    # Did we start in "mx" mode? If so, we will delay SPF checks until
    # envrcpt_callback.

    return SMFIS_CONTINUE if ($mx_mode);

    # Make the SPF query, and immediately store the result in our private hash;
    # we may also need it later, at eom_callback.

    if ($priv_data->{'spf_result'} = socket_call (2, $priv_data, $priv_data->{'ipaddr'}, $priv_data->{'from'}, $priv_data->{'helo'})) {
        if ($priv_data->{'spf_result'} eq 'fail') {
            if ($tagOnly) {
                write_log ("SPF \"fail\" from ip=".$priv_data->{'ipaddr'}.
                           " helo=".$priv_data->{'helo'}.
                           " from=".$priv_data->{'from'});
            } else {
                $ctx->setreply('550', '5.7.1', "$priv_data->{'spf_smtp_comment'}");
                return SMFIS_REJECT;
            }
        } elsif ($priv_data->{'spf_result'} eq 'error') {
            $ctx->setreply('451', '4.7.1', "$priv_data->{'spf_smtp_comment'}");
            return SMFIS_TEMPFAIL;
        }
    }

    $ctx -> setpriv ($priv_data);
    return SMFIS_CONTINUE;
}

sub envrcpt_callback : locked {
    my $ctx = shift;
    my $priv_data = $ctx->getpriv();
    my ($envelope_to, $reversed_recipient);

    # Keep the old recipient too, exactly as it appeared
    # in the SMTP dialoge!

   ($priv_data->{'to'} = ($envelope_to = shift)) =~ s/[<>]//g;

    # Are we relaying or receiving? The bulk of our labor is at local delivery.

    if ($ctx -> getsymval ('{rcpt_mailer}') eq 'local') {

        # If we require that all DSN messages are SRS signed (-S option),
        # then here we check whether we have a valid SRS address
        # in case of a DSN.
        #
        # Before you use this option, make sure you are well
        # familiar with its possible consequences! Basically, you
        # will be denying access to ALL non-SRS signed recipients,
        # in case of a DSN. Only use this when you have implemented
        # a SRS signing scheme in your MTA, which will sign ALL outgoing
        # envelope-from addresses. Unfortunately, spf-milter cannot do
        # that for you, as the Milter specs do not allow for a method
        # to change the envelope-from address.
        #
        # Also, be sure to visit:
        #
        #    http://www.libsrs2.org
        #    http://www.openspf.org/srs.html
        #    http://srs-socketmap.info/sendmailsrs.htm
        #
        # The -S option is for people with a specific, deliberate
        # purpose in mind. Do not haphazardly enable this just
        # because the idea of 'signed' addresses makes you feel safer;
        # if you did not specifically set up your MTA for this purpose,
        # then this option is not for you.

        if ($require_srs_dsn) {
            if ($priv_data->{'bounce'}) {

                # First scenario; we receive a SRS0 address; a one-pass
                # reversal should 'eval' to tell us whether it is really
                # ours, and valid.

                if ($priv_data->{'to'} =~ /^SRS0[+-=]/i) {
                    if (not (eval {$reversed_recipient = $srs -> reverse ($priv_data->{'to'})})) {
                        $ctx -> setreply ('550', '5.7.5', "Invalid SRS signature!");
                        $ctx -> setpriv ($priv_data);
                        return SMFIS_REJECT;
                    } else {

                        # We will store reversed recipients in pairs:
                        # the orginal recipient (exactly as it appeared in
                        # the SMTP dialogue) + its reversed counterpart.
                        #
                        # At eom_callback, as per the Milter protocol,
                        # we will avail ourselves of the first best
                        # opportunity to use a corresponding delrcpt/addrcpt
                        # combo to change the recipients in the envelope.

                        $priv_data->{'reversed_recipients'} .= "$envelope_to $reversed_recipient ";
                    }

                # Second scenario; we will use a two-pass reversal on the SRS1 address.
                # If it is still ours thereafter, we will accept it.

                } elsif ($priv_data->{'to'} =~ /^SRS1[+-=]/i) {
                    if (not (eval {$_ = $srs -> reverse ($priv_data->{'to'})})) {
                        $ctx -> setreply ('550', '5.7.5', "Invalid SRS signature!");
                        $ctx -> setpriv ($priv_data);
                        return SMFIS_REJECT;
                    } elsif (not (eval {$reversed_recipient = $srs -> reverse ($_)})) {
                        if (not $will_relay_srs1) {
                            $ctx -> setreply ('551', '5.7.1', "User not local; please try <$_> directly");
                            $ctx -> setpriv ($priv_data);
                            return SMFIS_REJECT;
                        } else {

                            # Since the outer SRS1 address was targeted locally, it did
                            # not trigger sendmail's relay rules. If the reversal of the
                            # SRS1 address appears to be non-local after all, sendmail,
                            # still working under the assumption that this was a local
                            # delivery, will relay without question!
                            #
                            # Please, do not worry about being an open relay, though: SRS1
                            # addresses now have an extra hash to prevent forgery.

                            $reversed_recipient = $_;
                        }
                    }
                    $priv_data->{'reversed_recipients'} .= "$envelope_to $reversed_recipient ";

                # Okay, no SRS address found; and we really should have. If the
                # recipient is not postmaster@ or abuse@ (or abuse-report@, etc),
                # we complain; otherwise, we turn a blind eye.
                #
                # N.B. Future versions of spf-milter may remove this 'bypass'.
                # For now, while SPF is still in the early stages of its
                # adoption phase, we will allow for this exception.

                } elsif (not ($priv_data->{'to'} =~ /^(postmaster|abuse)\b/i)) {
                    $ctx -> setreply ('550', '5.7.5', "Bounce address not SRS signed!");
                    $ctx -> setpriv ($priv_data);
                    return SMFIS_REJECT;
                }

                # We only expect to see SRS in DSN. Mind you, this is a two-way
                # street! We do not accept incoming SRS addresses outside the
                # context of DSN; and, likewise, you cannot send out to (local)
                # SRS recipients, other than using an empty envelope-from!

            } elsif ($priv_data->{'to'} =~ /^SRS[01][+-=]/i) {
                $ctx -> setreply ('550', '5.7.6', "SRS only supported in DSN!");
                $ctx -> setpriv ($priv_data);
                return SMFIS_REJECT;
            }
        }

    # We are relaying. Only a single, outer check here: are
    # we sending to an SRS1 address? If so, a one-pass reversal
    # must 'eval'. The inner reverse may, or may not, 'eval'
    # (in fact, it will probably not, as the result will likely
    # be a third-party SRS0 address).
    #
    # N.B. Please notice the absence of a separate outer SRS0
    # check. We only arrive here in 'relay' mode (which means:
    # any SRS0 target will always have a non-local domain name
    # part, which we will not be able to 'eval' anyway).

    } elsif ($priv_data->{'to'} =~ /^SRS[01][+-=]/i) {
        if (not $priv_data->{'bounce'}) {
            $ctx -> setreply ('550', '5.7.6', "SRS only supported in DSN!");
            $ctx -> setpriv ($priv_data);
            return SMFIS_REJECT;
        } elsif ($priv_data->{'to'} =~ /^SRS1[+-=]/i) {
            if (not (eval {$_ = $srs -> reverse ($priv_data->{'to'})})) {
                $ctx -> setreply ('550', '5.7.5', "Invalid SRS signature!");
                $ctx -> setpriv ($priv_data);
                return SMFIS_REJECT;
            } elsif (not (eval {$reversed_recipient = $srs -> reverse ($_)})) {
                if (not $will_relay_srs1) {
                    $ctx -> setreply ('551', '5.7.1', "User not local; please try <$_> directly");
                    $ctx -> setpriv ($priv_data);
                    return SMFIS_REJECT;
                } else {

                    # Yes, this could be a non-local recipient. Please,
                    # do not worry about being an open relay here;
                    # since the outer SRS1 address was non-local to begin
                    # with, only authorized IP-space can make this relay
                    # happen anyway.

                    $reversed_recipient = $_;
                }
            }
            $priv_data->{'reversed_recipients'} .= "$envelope_to $reversed_recipient ";
        }
    }

    $ctx->setpriv($priv_data);

    # We're done if we're already authenticated.

    return SMFIS_CONTINUE if ($priv_data->{'is_authenticated'});

    # Here we do the opposite check of envfrom_callback: if not "mx" mode,
    # we bale rightaway.

    return SMFIS_CONTINUE if (not $mx_mode);

    # We also need to purge $priv_data->{'spf_result'} for each recipient!

    delete $priv_data->{'spf_result'};

    $ctx->setpriv($priv_data);

    if ($priv_data->{'spf_result'} = socket_call (2, $priv_data, $priv_data->{'ipaddr'}, $priv_data->{'from'}, $priv_data->{'helo'}, $priv_data->{'to'})) {
        if ($priv_data->{'spf_result'} eq 'fail') {
            if ($tagOnly) {
                write_log ("SPF \"fail\" from ip=".$priv_data->{'ipaddr'}.
                           " helo=".$priv_data->{'helo'}.
                           " from=".$priv_data->{'from'}.
                           " to=".$priv_data->{'to'});
            } else {
                $ctx->setreply('550', '5.7.1', "$priv_data->{'spf_smtp_comment'}");
                return SMFIS_REJECT;
            }
        } elsif ($priv_data->{'spf_result'} eq 'error') {
            $ctx->setreply('451', '4.7.1', "$priv_data->{'spf_smtp_comment'}");
            return SMFIS_TEMPFAIL;
        }
    }

    $ctx -> setpriv ($priv_data);
    return SMFIS_CONTINUE;
}

sub eom_callback : locked {
    my $ctx = shift;
    my $priv_data = $ctx->getpriv();

    # Did we get an SPF result? If so, add the appropriate header. There is no
    # longer a need to use the "chgheader" method to replace the first
    # occurance of a Received-SPF header; "addheader" will automatically
    # prepend the new Received-SPF header.

    if ($priv_data->{'spf_result'}) {
        $ctx->addheader('Received-SPF', $priv_data->{'spf_result'} . ' (' . $priv_data->{'spf_header_comment'} . ')');
    }

    # Only at eom_callback can we substitute SRS recipients.

    if ($priv_data->{'bounce'}) {
        my ($old_recipient, $new_recipient);

        # The convenient twin structure of a hash makes it possible
        # to just suck in the entire split string, and have it neatly
        # be distributed over "$old_recipient, $new_recipient" pairs.
        # Cute, eh?

        my %srs = split (/ /, $priv_data->{'reversed_recipients'});
        while (($old_recipient, $new_recipient) = each %srs) {
            $ctx -> delrcpt ($old_recipient);
            $ctx -> addrcpt ($new_recipient);
        }
    }

    $ctx->setpriv($priv_data);

    return SMFIS_CONTINUE;
}

# On RSET, forget everything except the HELO name. Noted by Paul Howarth
#
# (note by me: we also need to preserve the hostname of the sender,
# our own hostname, and the IP address of the sender! Best, therefore, to
# use a negative logic, and just delete the things that need to go)
#
# BTW, we keep 'is_authenticated' in 1.40; during an entire session
# the connection should remain authenticated (unless a new HELO sounds
# the possible start of a new STARTTLS session).

sub abort_callback : locked {
    my $ctx = shift;
    my $priv_data = $ctx->getpriv();
    delete $priv_data->{'spf_result'};
    delete $priv_data->{'from'};
    delete $priv_data->{'to'};
    delete $priv_data->{'bounce'};
    delete $priv_data->{'reversed_recipients'};
    $ctx->setpriv($priv_data);
    return SMFIS_CONTINUE;
}

sub close_callback {
    my $ctx = shift;
    $ctx->setpriv(undef);
    return SMFIS_CONTINUE;
}

my %my_callbacks =
(
    'connect' => \&connect_callback,
    'helo'    => \&helo_callback,
    'envfrom' => \&envfrom_callback,
    'envrcpt' => \&envrcpt_callback,
    'eom'     => \&eom_callback,
    'close'   => \&close_callback,
    'abort'   => \&abort_callback,
);

############################################################
# Main code

# We start spf-milter as root for the same reason we do NOT run spf-milter
# as root: security. And we start it with at least one parameter, the user
# to run as. Spf-milter expects to create/read/write its log, pid, and socket,
# all in /var/spf-milter/, and will itself create the directory, if need be,
# and set all appropriate permissions/ownerships.
#
# Add "mx" as second parameter to run spf-milter in "mx" mode. In "mx" mode
# spf-milter makes its SPF checks at envrcpt_callback, instead of envfrom_callback,
# and calls result2 (), instead of result (), to allow for an early-out for
# secondaries. The default mode performs SPF checks at envfrom_callback.
#
# Per default, spf-milter queries trusted-fowarder.org (on 'fail' only), to
# check whether the trusted-fowarder domain yields a 'pass' after all. You can
# override the default behavior, adding "dt" (disable trust) as second parameter
# (or third, if you run in "mx" mode). You need at least Mail::SPF::Query 1.99
# for this functionality!

getopts("kl:tmSrhT");

sub usage {
    my ($ret) = @_;
    print STDERR "Usage: $0 [-k] [-l local_trust] [-t] [-m] [-S] [-r] [-h] <user> [mx] [dt]\n";
    print STDERR "        -k        kill running milter\n";
    print STDERR "        -l        add local trust record\n";
    print STDERR "        -t        don't add trusted-forwarder.org record\n";
    print STDERR "        -m        trust recipient's MX hosts\n";
    print STDERR "        -S        only allow SRS signed bounces (see documentation!)\n";
    print STDERR "        -r        will relay SRS1\n";
    print STDERR "        -T        don't reject failed messages, tag only\n";
    print STDERR "        -h        print this help message\n";
    print STDERR "        <user>        user to run this script as\n";
    print STDERR "        mx        trust recipient's MX hosts (same as -m)\n";
    print STDERR "        dt        don't add trusted-forwarder.org (same as -t)\n";
    exit ($ret);
}

if ($opt_h) {
    usage (0);
}

# Basic, but vital, sanity-check against $basedir. Since we set
# permissions/ownerships on everything (!) in our $basedir, we
# must avoid disasters, such as setting $basedir to /var/run/.
# Therefore, we require that $basedir ends in "spf-milter".

if (not ($basedir =~ /spf-milter$/i)) {
    die '$basedir' . " ('$basedir') must end in /spf-milter!\n";
}

my $oldPid;
if (-f $pidFile) {
    open (PIDFILE, $pidFile) || die "Could not read pid file: $!\n";
    chomp ($oldPid = <PIDFILE>);
    close (PIDFILE);
}

if (defined $opt_k) {
    die "SPF milter not running\n" if (not $oldPid);

    # We need to kill the milter using signal 3, it apparently doesn't react
    # to more "usual" signals...

    if (not kill (3, $oldPid)) {
        if ($!{ESRCH}) {
            print STDERR "Sendmail milter not running, cleaning files\n";

            # Files will be cleaned by END block

            exit 0;
        } else {

            # Prevent cleaning away of the running milter's files

            $pid = 1;

            die "Could not kill SPF milter: $!\n";
        }
    }

    my $needNl = 0;
    select (STDERR);
    $| = 1;

    # Waiting for milter to die

    for (my $i = 0; $i < 79; $i++) {
        select (undef, undef, undef, 0.25);
        if (not kill (0, $oldPid) && $!{ESRCH}) {
            print STDERR "\n" if ($needNl);
            exit 0; # Milter dead
        }
        print STDERR ".";
        $needNl = 1;
    }

    print STDERR "\nForcefully killing milter\n";
    kill (9, $oldPid);
    exit 0;
}

if ($oldPid) {
    my $r = kill (0, $oldPid);
    if (not $!{ESRCH}) {

        # Prevent cleaning away of the running milter's files

        $pid = 1;

        die "SPF milter already running\n";
    }
}

unlink $sock;
unlink $pidFile;

if (not $user = lc ($ARGV[0])) {
    print STDERR "Missing user\n";
    usage (1);
} elsif ($>) {
    print STDERR "You need to start spf-milter as root!\n";
    exit 1;
}

$mx_mode = 1 if ($opt_m || (lc ($ARGV[1]) eq 'mx'));

$trust = 0 if ($opt_t || (lc ($ARGV[1]) eq 'dt') || (lc ($ARGV[2]) eq 'dt'));
push (@extraParams, trusted => $trust);

if ($opt_l) {
    push (@extraParams, local => $opt_l);
}

if ($opt_T) {
    $tagOnly = 1;
}

$require_srs_dsn = 1 if ($opt_S);
$will_relay_srs1 = 1 if ($opt_r);

# Since we will daemonize, play nice.

chdir ('/') or exit 1;

umask (0077);

if (not (-e $basedir)) {
    if (not mkdir $basedir) {
        print STDERR "Odd; cannot create $basedir/\n";
        exit 1;
    }
}

# The Sendmail::Milter 0.18 engine has a small bug, causing it to extract
# the wrong socket-name when, next to the F flags, there's an additional flag
# in the Milter definition, (see: http://rt.cpan.org/NoAuth/Bug.html?id=3892
# for details). Since the extra flag is useful (T for timeouts), we preset our
# connection string to "local:/var/spf-milter/spf-milter.sock", with "spf-milter"
# as Milter name. A corresponding line in sendmail.cf could look like this:
#
# Xspf-milter, S=local:/var/spf-milter/spf-milter.sock, F=T, T=C:4m;S:4m;R:8m;E:16m

if (not $conn = Sendmail::Milter::auto_getconn ('spf-milter', '/etc/mail/sendmail.cf')) {
    log_error_and_exit ("Milter for 'spf-milter' not found!");
}

if ($conn =~ /^local:(.+)/) {
    if (not Sendmail::Milter::setconn ("local:$sock")) {
        log_error_and_exit ("Failed to set connection information!");
    }

    # Now we set a fairly large timeout. The idea here is to set it so large, that
    # the Milter will not try and compete with the sendmail T= timings, which allow
    # for a more fine-grained tuning.

    if (not Sendmail::Milter::settimeout ('8192')) {
        log_error_and_exit ("Failed to set timeout value!");
    }
    if (not Sendmail::Milter::register ('spf-milter', \%my_callbacks, SMFI_CURR_ACTS)) {
        log_error_and_exit ("Failed to register callbacks!");
    }

    # Get info on the user we want to run as. If $uid is undefined, the user
    # does not exist on the system; if zero, it is the UID of root!

   ($login, $pass, $uid, $gid) = getpwnam ($user);
    if (not defined ($uid)) {
        log_error_and_exit ("$user is not a valid user on this system!");
    } elsif (not $uid) {
        log_error_and_exit ("You cannot run spf-milter as root!");
    }
    write_log ("Starting Sendmail::Milter $Sendmail::Milter::VERSION engine");

    # Set all proper permissions/ownerships, according to the user we run as.

    if ((not chown $uid, $gid, $basedir, glob ($basedir . '/*')) ||
        (not chmod 0700, $basedir)) {
        log_error_and_exit ("Cannot set proper permissions!");
    }

    # Drop the Sendmail::Milter privileges!

    $) = $gid;
    $( = $gid;
    $> = $uid;
    $< = $uid;

    # Give us a pretty proc-title to look at in 'ps ax'. :)

    $0 = 'spf-milter' . (($mx_mode) ? (" [mx mode]") : (""));

    # Fork and give us a pid file.

    if ($pid = fork ()) {
        open (USERLOG, ">". $pidFile) or exit 1;
        flock (USERLOG, 2);
        seek (USERLOG, 0, 0);
        print USERLOG " $pid";
        close (USERLOG);

        # Wait until either milter socket appears or child dies

        my $kid = 0;
        while (not -x $sock) {
            select (undef,undef,undef,0.01);
            $kid = waitpid (-1, WNOHANG);
            if ($kid > 0) {
                $pid = 0; # trigger cleanup
                die "Could not start milter\n";
            }
        }
        exit 0;
    }

    # Redirect all input/output from/to null

    open (STDIN, '/dev/null');
    open (STDOUT, '>/dev/null');

    # Complete de daemonization process.

    POSIX::setsid () or exit 1;

    open (STDERR, '>&STDOUT');

    if (Sendmail::Milter::main ()) {
        write_log ("Successful exit from the Sendmail::Milter engine");
    } else {
        write_log ("Unsuccessful exit from the Sendmail::Milter engine");
    }
} else {
    log_error_and_exit ("$conn is not a valid connection object!");
}

END {

    # On exit (child only!) we clean up the mess.

    if (not $pid) {
        unlink ($pidFile);
        unlink ($sock);
    }
}

exit 0;
