v5/common/usr/sbin/MSMilter
Shawn Iverson e67f8a7c0f
Milter refactor (#248)
Large file processing
Move envelope sender to new pre data header
Move blacklist milter scanner check earlier in milter processing
2018-10-13 12:46:02 -04:00

519 lines
17 KiB
Perl

#!/usr/bin/perl -U -I /usr/share/MailScanner/perl
# (c) 2018 MailScanner Project <https://www.mailscanner.info>
# Version 1.1
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
#
# Contributed by Shawn Iverson for MailScanner <shawniverson@efa-project.org>
use strict 'vars';
no strict 'refs';
no strict 'subs';
use POSIX;
require 5.005;
use DirHandle;
use File::Basename;
use File::Copy;
use IO::File;
use IO::Pipe;
use Sendmail::PMilter;
use Socket;
use POSIX qw(strftime);
use Sys::Hostname;
use Time::HiRes qw( gettimeofday );
use MailScanner::Lock;
use MailScanner::Log;
use MailScanner::Config;
use MailScanner::CustomConfig;
use MailScanner::Message;
my $ConfFile;
sub smtp_id {
my $type = MailScanner::Config::Value('msmailqueuetype');
MailScanner::Log::DebugLog("msmailqueuetype = $type");
if ($type =~ /long/i) {
# Long queue IDs
my $seconds=0;
my $microseconds=0;
use Time::HiRes qw( gettimeofday );
($seconds, $microseconds) = gettimeofday;
my $microseconds_orig=$microseconds;
my @BASE52_CHARACTERS = ("0","1","2","3","4","5","6","7","8","9",
"B","C","D","F","G","H","J","K","L","M",
"N","P","Q","R","S","T","V","W","X","Y",
"Z","b","c","d","f","g","h","j","k","l",
"m","n","p","q","r","s","t","v","w","x","y","z");
my $encoded='';
my $file_out;
my $count=0;
while ($count < 6) {
$encoded.=$BASE52_CHARACTERS[$seconds%52];
$seconds/=52;
$count++;
}
$file_out=reverse $encoded;
$encoded='';
$count=0;
while ($count < 4) {
$encoded.=$BASE52_CHARACTERS[$microseconds%52];
$microseconds/=52;
$count++;
}
$file_out.=reverse $encoded;
$file_out.="z";
# File doesn't exist yet, just randomize
my $inode=int(rand 1000000) + 1;
$encoded='';
$count=0;
while ($count < 4) {
$encoded.=$BASE52_CHARACTERS[$inode%51];
$inode/=51;
$count++;
}
$file_out.=reverse $encoded;
# We check the generated ID...
if ($file_out !~ /[A-Za-z0-9]{12,20}/) {
# Something has gone wrong, back to short ID for safety
MailScanner::Log::WarnLog("ERROR generating long queue ID");
$file_out = sprintf("%05X%lX", int(rand 1000000)+1, int(rand 1000000)+1);
}
return $file_out;
} else {
# No file to stat, so just randomly generate
return sprintf("%05X%lX", int(rand 1000000)+1, int(rand 1000000)+1);
}
}
sub connect_callback
{
my $ctx = shift;
my $hostname = shift;
my $sockaddr_in = shift;
my ($port, $iaddr);
my $ip;
my $message_ref = $ctx->getpriv();
my $message = $hostname;
if (defined $sockaddr_in)
{
($port, $iaddr) = sockaddr_in($sockaddr_in);
$ip = inet_ntoa($iaddr);
$message .= ' [' . $ip . ']';
}
# Localhost relaying email? Accept the message for delivery
if ($ip =~ /^127/ || $ip =~ /^::1$/) {
MailScanner::Log::DebugLog("Localhost relay detected");
return Sendmail::PMilter::SMFIS_ACCEPT;
}
MailScanner::Log::DebugLog("connect_callback: ip = $ip");
${$message_ref} = $message;
$ctx->setpriv($message_ref);
Sendmail::PMilter::SMFIS_CONTINUE;
}
sub helo_callback
{
my $ctx = shift;
my $helohost = shift;
my $message_ref = $ctx->getpriv();
my $message = "Received: from $helohost";
# Watch for the second callback
if ( $message ne substr(${$message_ref}, 0, length($message)) ) {
${$message_ref} = $message . ' (' . ${$message_ref} . ')' ."\n";
MailScanner::Log::DebugLog("helo_callback: $message");
}
$ctx->setpriv($message_ref);
Sendmail::PMilter::SMFIS_CONTINUE;
}
sub envrcpt_callback
{
my $ctx = shift;
my @args = @_;
my $message_ref = $ctx->getpriv();
my $symbols = $ctx->{symbols};
my $mailfrom;
# Watch for second callback
if ( ${$message_ref} !~ /MailScanner Milter/ ) {
# Capture the Mail From address for further processing
if (defined($symbols->{'M'}) && defined($symbols->{'M'}->{'{mail_addr}'})) {
$mailfrom=$symbols->{'M'}->{'{mail_addr}'};
} else {
# Null Sender
# RFC 1123, 821
$mailfrom='';
}
${$message_ref} = 'S<' . $mailfrom . ">\n" . ${$message_ref};
my $id = smtp_id;
my $datestring = strftime "%a, %e %b %Y %T %z (%Z)", localtime;
if (defined($symbols->{'H'}) && defined($symbols->{'H'}->{'{tls_version}'}) && defined($symbols->{'H'}->{'{cipher}'}) && defined($symbols->{'H'}->{'{cipher_bits}'})) {
${$message_ref} .= ' (using ' . $symbols->{'H'}->{'{tls_version}'} . ' with cipher ' . $symbols->{'H'}->{'{cipher}'} . ' (' . $symbols->{'H'}->{'{cipher_bits}'} . '/' . $symbols->{'H'}->{'{cipher_bits}'} . ' bits))' . "\n";
MailScanner::Log::DebugLog("envrcpt_callback: ssl/tls detected");
}
if (!defined($symbols->{'H'}->{'{cert_subject}'})) {
${$message_ref} .= ' (no client certificate requested)' . "\n";
MailScanner::Log::DebugLog("envrcpt_callback: no client certificate requested");
}
${$message_ref} .= ' by ' . hostname . ' (MailScanner Milter) with SMTP id ' . $id . "\n";
MailScanner::Log::DebugLog("envrcpt_callback: id = $id datestring = $datestring");
}
# Build original recipient milter header
${$message_ref} = 'O' . $args[0] . "\n" . ${$message_ref};
$ctx->setpriv($message_ref);
Sendmail::PMilter::SMFIS_CONTINUE;
}
sub header_callback
{
my $ctx = shift;
my $headerf = shift;
my $headerv = shift;
my $message_ref = $ctx->getpriv();
${$message_ref} .= $headerf . ': ' . $headerv . "\n";
$ctx->setpriv($message_ref);
Sendmail::PMilter::SMFIS_CONTINUE;
}
sub eoh_callback
{
my $ctx = shift;
my $message_ref = $ctx->getpriv();
my $id;
my $line;
my $buffer;
my $ip;
my @to;
my $from;
# Are we in scanner mode?
my $scannermode = MailScanner::Config::Value('milterscanner');
MailScanner::Log::DebugLog("eom_callback: scannermode = $scannermode");
while (${$message_ref} =~ /([^\n]+)\n?/g) {
my $line = $1;
$buffer .= $1 . "\n";
if ($line =~ /^Received: / ) {
$ip = '127.0.0.1';
if ($line =~ /^Received: .+?\(.*?\[(?:IPv6:)?([0-9a-f.:]+)\]/i) {
$ip = $1;
}
MailScanner::Log::DebugLog("eoh_callback: ip = $ip");
} elsif ( $line =~ m/^.*SMTP id / ) {
$line =~ s/^.*SMTP id //;
$id = $line;
MailScanner::Log::DebugLog("eoh_callback: id = $id");
last;
} elsif ( $line =~ /^O/ ) {
$line =~ s/^O//;
$line =~ s/^\<//;
$line =~ s/\>.*$//;
push @to, $line;
MailScanner::Log::DebugLog("eoh_callback: to = $line");
} elsif ( $line =~ /^F/ ) {
$line =~ s/^F//;
$line =~ s/^\<//;
$line =~ s/\>.*$//;
$from = $line;
MailScanner::Log::DebugLog("eoh_callback: from = $line");
}
}
# Write header to file
my $queuehandle = new FileHandle;
my $incoming = MailScanner::Config::Value('inqueuedir');
MailScanner::Log::DebugLog("eoh_callback: incoming = " . @{$incoming}[0]);
if ($incoming eq '') {
MailScanner::Log::WarnLog("Unable to determine incoming queue!");
Sendmail::PMilter::SMFIS_TEMPFAIL;
return;
}
my $file = @{$incoming}[0] . '/temp-' . $id;
MailScanner::Log::DebugLog("eoh_callback: file = $file");
my $ret;
$ret = MailScanner::Lock::openlock($queuehandle,'>>' . $file, 'w');
if ($ret != 1) {
MailScanner::Log::WarnLog("Unable to to open queue temp file for writing!");
Sendmail::PMilter::SMFIS_TEMPFAIL;
return;
}
$queuehandle->print(${$message_ref});
# Signal end of header
$queuehandle->print("\n");
$queuehandle->flush();
MailScanner::Lock::unlockclose($queuehandle);
# Determine what to do next
if ($scannermode =~ /^yes$/) {
MailScanner::Log::DebugLog("eom_callback: scanner mode enabled");
# Blacklist test
# Build a message object to test against
my $message = {};
if ($from ne '<>') {
$message->{from} = $from;
} else {
$message->{from} = '';
}
@{$message->{to}} = @to;
$message->{clientip} = $ip;
my $user;
my $domain;
($user, $domain) = MailScanner::Message::address2userdomain($message->{from});
$message->{fromuser} = $user;
$message->{fromdomain} = $domain;
my $touser;
my $todomain;
foreach my $msgto (@{$message->{to}}) {
($touser, $todomain) = MailScanner::Message::address2userdomain($msgto);
push @{$message->{touser}}, $touser;
push @{$message->{todomain}}, $todomain;
}
my $test;
# Check whitelist first
$test = MailScanner::Config::Value('spamwhitelist', $message);
# Not in whitelist, then check blacklist
if ($test == 0) {
MailScanner::Log::DebugLog("eom_callback: no whitelist match, proceeding to blacklist test");
# test
$test = MailScanner::Config::Value('spamblacklist', $message);
# Blacklisted, fire a reject
if ($test == 1) {
MailScanner::Log::DebugLog("eom_callback: blacklist match, firing reject");
# Set reject code
$ctx->setreply('554', '5.7.1', 'Message Blacklisted');
# Delete temp file
unlink $file;
return Sendmail::PMilter::SMFIS_REJECT;
}
}
}
MailScanner::Log::DebugLog("eoh_callback: End of Header detected");
# Start storing just the id
${$message_ref} = $id;
$ctx->setpriv($message_ref);
Sendmail::PMilter::SMFIS_CONTINUE;
}
sub body_callback
{
my $ctx = shift;
my $body_chunk = shift;
my $len = shift;
my $message_ref = $ctx->getpriv();
my $queuehandle = new FileHandle;
my $incoming = MailScanner::Config::Value('inqueuedir');
MailScanner::Log::DebugLog("body_callback: incoming = " . @{$incoming}[0]);
if ($incoming eq '') {
MailScanner::Log::WarnLog("Unable to determine incoming queue!");
Sendmail::PMilter::SMFIS_TEMPFAIL;
return;
}
my $file = @{$incoming}[0] . '/temp-' . ${$message_ref};
MailScanner::Log::DebugLog("body_callback: file = $file");
my $ret;
$ret = MailScanner::Lock::openlock($queuehandle,'>>' . $file, 'w');
if ($ret != 1) {
MailScanner::Log::WarnLog("Unable to to open queue temp file for writing!");
Sendmail::PMilter::SMFIS_TEMPFAIL;
return;
}
$queuehandle->print($body_chunk);
$queuehandle->flush();
MailScanner::Lock::unlockclose($queuehandle);
$ctx->setpriv($message_ref);
Sendmail::PMilter::SMFIS_CONTINUE;
}
sub eom_callback
{
my $ctx = shift;
my $message_ref = $ctx->getpriv();
my $incoming = MailScanner::Config::Value('inqueuedir');
MailScanner::Log::DebugLog("eom_callback: incoming = " . @{$incoming}[0]);
if ($incoming eq '') {
MailScanner::Log::WarnLog("Unable to determine incoming queue!");
Sendmail::PMilter::SMFIS_TEMPFAIL;
return;
}
my $file = @{$incoming}[0] . '/temp-' . ${$message_ref};
my $file2 = @{$incoming}[0] . '/' . ${$message_ref};
move($file, $file2);
$ctx->setpriv(undef);
# Send DISCARD signal to accept message and drop from postfix
# for mailscanner processing
Sendmail::PMilter::SMFIS_DISCARD;
}
#
# Create and write a PID file for a given process id
#
sub WritePIDFile {
my($process,$PidFile) = @_;
my $pidfh = new FileHandle;
$pidfh->open(">$PidFile")
or MailScanner::Log::WarnLog("Cannot write pid file %s, %s", $PidFile, $!);
print $pidfh "$process\n";
$pidfh->close();
}
#
# Start logging
#
sub StartLogging {
my($filename) = @_;
my $procname = 'MSMilter';
my $logbanner = "MSMilter Daemon starting...";
MailScanner::Log::Configure($logbanner, 'syslog');
# Need to know log facility *before* we have read the whole config file!
my $facility = MailScanner::Config::QuickPeek($filename, 'syslogfacility');
my $logsock = MailScanner::Config::QuickPeek($filename, 'syslogsockettype');
MailScanner::Log::Start($procname, $facility, $logsock);
}
BEGIN {
chdir('/usr/share/MailScanner/perl');
$ENV{PATH}="/sbin:/bin:/usr/sbin:/usr/bin";
umask 0077;
$ConfFile = $ARGV[0];
# Use the default if we couldn't find theirs. Will save a lot of grief.
$ConfFile = '/etc/MailScanner/MailScanner.conf' if $ConfFile eq "" ||
!(-f $ConfFile);
# Start logging
StartLogging($ConfFile);
MailScanner::Config::initialise(MailScanner::Config::QuickPeek($ConfFile,
'customfunctionsdir'));
# Read Config File
MailScanner::Config::Read($ConfFile);
my $pid = fork;
if (!defined($pid)) {
MailScanner::Log::WarnLog("Milter: Unable to fork!");
exit 1;
}
if ($pid == 0) {
$0 = "MSMilter Daemon";
# Need this new parent process to ignore SIGHUP, and catch SIGTERM
$SIG{'HUP'} = 'IGNORE';
$SIG{'TERM'} = \&ExitParent;
my $PidFile = MailScanner::Config::Value('milterpidfile');
WritePIDFile($$,$PidFile);
my $children = MailScanner::Config::Value('miltermaxchildren');
my $user = MailScanner::Config::Value('runasuser');
my $group = MailScanner::Config::Value('workgroup');
my $bind = MailScanner::Config::Value('milterbind');
my $port = MailScanner::Config::Value('milterport');
MailScanner::Log::Reset();
MailScanner::Log::DebugLog("initialization:\n PidFile = $PidFile\n user = $user\n group = $group\n bind = $bind\n port = $port");
my %my_callbacks =
(
'connect' => \&connect_callback,
'helo' => \&helo_callback,
'envrcpt' => \&envrcpt_callback,
'header' => \&header_callback,
'eoh' => \&eoh_callback,
'body' => \&body_callback,
'eom' => \&eom_callback,
);
my $conn = 'inet:'. $port . '@' . $bind;
$ENV{PMILTER_DISPATCHER} = 'prefork';
my $milter = Sendmail::PMilter->new();
$milter->register('MSMilter',
\%my_callbacks,
Sendmail::PMilter::SMFI_CURR_ACTS
);
$milter->setconn($conn);
$< = $> = getpwnam($user);
$( = $) = getgrnam($group);
$milter->main($children,$children);
# Should never get here, if we did, it didn't work
MailScanner::Log::WarnLog("Unable to spawn milter!");
exit 1
}
# Successful if we reach this point
exit 0;
}
1;