1#!/usr/local/bin/perl -U -I /usr/share/MailScanner/perl
2
3# (c) 2019-2020 MailScanner Project <https://www.mailscanner.info>
4#          Version 1.6
5#
6#    This program is free software; you can redistribute it and/or modify
7#    it under the terms of the GNU General Public License as published by
8#    the Free Software Foundation; either version 2 of the License, or
9#    (at your option) any later version.
10#
11#    This program is distributed in the hope that it will be useful,
12#    but WITHOUT ANY WARRANTY; without even the implied warranty of
13#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14#    GNU General Public License for more details.
15#
16#    You should have received a copy of the GNU General Public License along
17#    with this program; if not, write to the Free Software Foundation, Inc.,
18#    51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
19#
20#    Contributed by Shawn Iverson for MailScanner <shawniverson@efa-project.org>
21
22use strict 'vars';
23no strict 'refs';
24no strict 'subs';
25use POSIX;
26require 5.005;
27
28use DirHandle;
29use File::Basename;
30use File::Copy;
31use IO::File;
32use IO::Pipe;
33use Sendmail::PMilter;
34use Socket;
35use Socket qw(inet_ntop);
36use POSIX qw(strftime);
37use Sys::Hostname;
38use Time::HiRes qw( gettimeofday );
39
40use MailScanner::Lock;
41use MailScanner::Log;
42use MailScanner::Config;
43use MailScanner::CustomConfig;
44use MailScanner::Message;
45
46my $ConfFile;
47my $PidFile;
48
49sub smtp_id {
50  my $type = MailScanner::Config::Value('msmailqueuetype');
51  MailScanner::Log::DebugLog("msmailqueuetype = $type");
52  if ($type =~ /long/i) {
53        # Long queue IDs
54        my $seconds=0;
55        my $microseconds=0;
56        use Time::HiRes qw( gettimeofday );
57        ($seconds, $microseconds) = gettimeofday;
58        my $microseconds_orig=$microseconds;
59        my @BASE52_CHARACTERS = ("0","1","2","3","4","5","6","7","8","9",
60                                "B","C","D","F","G","H","J","K","L","M",
61                                "N","P","Q","R","S","T","V","W","X","Y",
62                                "Z","b","c","d","f","g","h","j","k","l",
63                                "m","n","p","q","r","s","t","v","w","x","y","z");
64        my $encoded='';
65        my $file_out;
66        my $count=0;
67        while ($count < 6) {
68                $encoded.=$BASE52_CHARACTERS[$seconds%52];
69                $seconds/=52;
70                $count++;
71        }
72        $file_out=reverse $encoded;
73        $encoded='';
74        $count=0;
75        while ($count < 4) {
76                $encoded.=$BASE52_CHARACTERS[$microseconds%52];
77                $microseconds/=52;
78                $count++;
79        }
80        $file_out.=reverse $encoded;
81
82        $file_out.="z";
83
84        # File doesn't exist yet, just randomize
85        my $inode=int(rand 1000000) + 1;
86        $encoded='';
87        $count=0;
88        while ($count < 4) {
89                $encoded.=$BASE52_CHARACTERS[$inode%51];
90                $inode/=51;
91                $count++;
92        }
93        $file_out.=reverse $encoded;
94
95        # We check the generated ID...
96        if ($file_out !~ /[A-Za-z0-9]{12,20}/) {
97                # Something has gone wrong, back to short ID for safety
98                MailScanner::Log::WarnLog("ERROR generating long queue ID");
99                $file_out = sprintf("%05X%lX", int(rand 1000000)+1, int(rand 1000000)+1);
100        }
101     return $file_out;
102  } else {
103      # No file to stat, so just randomly generate
104      return sprintf("%05X%lX", int(rand 1000000)+1, int(rand 1000000)+1);
105  }
106}
107
108sub connect_callback
109{
110    my $ctx = shift;
111    my $hostname = shift;
112    my $sockaddr_in = shift;
113    my ($port, $iaddr);
114    my $ip;
115    my $message_ref = $ctx->getpriv();
116
117    my $message = $hostname;
118
119    if (defined $sockaddr_in)
120    {
121        my $family = sockaddr_family($sockaddr_in);
122        if ($family eq AF_INET) {
123            ($port, $iaddr) = sockaddr_in($sockaddr_in);
124            $ip = inet_ntop(AF_INET, $iaddr);
125            $message .= ' [' . $ip . ']';
126        } elsif ($family eq AF_INET6) {
127            ($port, $iaddr) = sockaddr_in6($sockaddr_in);
128            $ip = inet_ntop(AF_INET6, $iaddr);
129            $message .= ' [' . $ip . ']';
130        } else {
131            $ip = '';
132        }
133
134    }
135
136    # Localhost relaying email?  Accept the message for delivery if enabled
137    my $loopback = MailScanner::Config::Value('milterignoreloopback');
138    if ($loopback == 1 && ($ip =~ /^127/ || $ip =~ /^::1$/)) {
139        MailScanner::Log::DebugLog("Localhost relay detected");
140        return Sendmail::PMilter::SMFIS_ACCEPT;
141    }
142
143
144    MailScanner::Log::DebugLog("connect_callback: ip = $ip");
145
146    ${$message_ref} = $message;
147
148    $ctx->setpriv($message_ref);
149
150    Sendmail::PMilter::SMFIS_CONTINUE;
151}
152
153sub helo_callback
154{
155    my $ctx = shift;
156    my $helohost = shift;
157    my $message_ref = $ctx->getpriv();
158    my $message = "Received: from $helohost";
159    # Watch for the second callback
160    if ( $message ne substr(${$message_ref}, 0, length($message)) ) {
161        ${$message_ref} = $message . ' (' . ${$message_ref} . ')' ."\n";
162        MailScanner::Log::DebugLog("helo_callback: $message");
163    }
164
165    $ctx->setpriv($message_ref);
166
167    Sendmail::PMilter::SMFIS_CONTINUE;
168}
169
170sub envrcpt_callback
171{
172    my $ctx = shift;
173    my @args = @_;
174    my $message_ref = $ctx->getpriv();
175    my $symbols = $ctx->{symbols};
176    my $mailfrom;
177
178    # Watch for second callback
179    if ( ${$message_ref} !~ /MailScanner Milter/ ) {
180
181        # Is this a subsequent message? Grab previous message id to reconstruct header
182        if ( ${$message_ref} =~ /^[0-9A-F]{7,20}|[0-9B-DF-HJ-NP-TV-Zb-df-hj-np-tv-z]{8,20}$/ ) {
183            # Read envelope
184            my $queuehandle = new FileHandle;
185            my $incoming = MailScanner::Config::Value('inqueuedir');
186            MailScanner::Log::DebugLog("envrcpt_callback: incoming = " . @{$incoming}[0]);
187            if ($incoming eq '') {
188                MailScanner::Log::WarnLog("Unable to determine incoming queue!");
189                Sendmail::PMilter::SMFIS_TEMPFAIL;
190                return;
191            }
192            my $file = @{$incoming}[0] . '/temp-' . ${$message_ref};
193            my $file2 = @{$incoming}[0] . '/' . ${$message_ref};
194
195            MailScanner::Log::DebugLog("envrcpt_callback: file = $file");
196
197            my $ret;
198            $ret = MailScanner::Lock::openlock($queuehandle,'+<' . $file, 'w');
199            if ($ret != 1) {
200                MailScanner::Log::WarnLog("Unable to to open temp queue file for reading!");
201                Sendmail::PMilter::SMFIS_TEMPFAIL;
202                return;
203            }
204
205            my $data;
206            my $pos;
207            # Locate predata header
208            while($data = readline $queuehandle) {
209                last if $data !~ /^(O<|S<|E<)/;
210            }
211
212            ${$message_ref} = $data;
213            MailScanner::Lock::unlockclose($queuehandle);
214
215            # Done with previous message, make ready for mailscanner
216            move($file, $file2);
217        }
218
219
220        # Capture the Mail From address for further processing
221        if (defined($symbols->{'M'}) && defined($symbols->{'M'}->{'{mail_addr}'})) {
222            $mailfrom=$symbols->{'M'}->{'{mail_addr}'};
223        } else {
224                # Null Sender
225                # RFC 1123, 821
226                $mailfrom='';
227        }
228
229        ${$message_ref} = 'S<' . $mailfrom . ">\n" . ${$message_ref};
230
231        if (defined($symbols->{'H'}) && defined($symbols->{'H'}->{'{tls_version}'}) && defined($symbols->{'H'}->{'{cipher}'}) && defined($symbols->{'H'}->{'{cipher_bits}'})) {
232            ${$message_ref} .= "\t(using " . $symbols->{'H'}->{'{tls_version}'} . ' with cipher ' . $symbols->{'H'}->{'{cipher}'} . ' (' . $symbols->{'H'}->{'{cipher_bits}'} . '/' . $symbols->{'H'}->{'{cipher_bits}'} . ' bits))' . "\n";
233                MailScanner::Log::DebugLog("envrcpt_callback: ssl/tls detected");
234        }
235        if (!defined($symbols->{'H'}->{'{cert_subject}'})) {
236            ${$message_ref} .= "\t(no client certificate requested)";
237            MailScanner::Log::DebugLog("envrcpt_callback: no client certificate requested");
238        }
239        if (defined($symbols->{'M'}->{'{auth_type}'}) && defined($symbols->{'M'}->{'{auth_authen}'})) {
240            ${$message_ref} .= "\t(Authenticated sender: " . $symbols->{'M'}->{'{auth_authen}'} . ")\n";
241            MailScanner::Log::DebugLog("envrcpt_callback: Authenticated sender: " . $symbols->{'M'}->{'{auth_authen}'});
242        } else {
243            ${$message_ref} .= "\n";
244        }
245        ${$message_ref} .= "\tby " . hostname . ' (MailScanner Milter) with SMTP id ';
246    }
247
248    my $rcptto = $args[0];
249    my $esmtpnotify = '';
250    # Capture the ESMTP options for pass through MailScanner engine
251    # RFC 3461
252    foreach (@args)
253    {
254        if ($_ =~ /^NOTIFY=/)
255        {
256            MailScanner::Log::DebugLog("envrcpt_callback: NOTIFY argument found: " . $_);
257            $esmtpnotify = $_;
258        }
259    }
260
261    # Build original recipient milter header
262    ${$message_ref} = 'O' . $rcptto . "\n" . ${$message_ref};
263
264    # Add options milter header
265    if ($esmtpnotify ne '')
266    {
267        ${$message_ref} = 'E<' . $esmtpnotify . ">\n" . ${$message_ref};
268    }
269
270    $ctx->setpriv($message_ref);
271
272    Sendmail::PMilter::SMFIS_CONTINUE;
273}
274
275sub header_callback
276{
277    my $ctx = shift;
278    my $headerf = shift;
279    my $headerv = shift;
280    my $message_ref = $ctx->getpriv();
281    my $symbols = $ctx->{symbols};
282    my $id;
283    my $datestring = strftime "%a, %e %b %Y %T %z (%Z)", localtime;
284
285    # Postfix queue id revealed during this phase, capture it if defined
286    # and populate the rest of the Received: header
287    if (${$message_ref} =~ /SMTP\sid\s$/ ) {
288        if (defined($symbols->{'L'}) && defined($symbols->{'L'}->{'i'}) ) {
289            $id = $symbols->{'L'}->{'i'};
290            if ( $id ne '' ) {
291                MailScanner::Log::DebugLog("Postfix ID captured: $id");
292            } else {
293                $id = smtp_id;
294                MailScanner::Log::DebugLog("Postfix id empty, generated one instead: $id");
295            }
296        } else {
297            $id = smtp_id;
298            MailScanner::Log::DebugLog("Unable to capture real postfix id, generated one instead: $id");
299        }
300
301        ${$message_ref} .= $id;
302
303        # Check for only one original recipient and add 'for <>' if so.
304        if (${$message_ref} =~ /^O(<.*>)\n(?!O)/ ) {
305            ${$message_ref} .= "\n\tfor " . $1 . "; ";
306        } else {
307            ${$message_ref} .= ";\n\t";
308        }
309        ${$message_ref} .= "$datestring\n";
310        MailScanner::Log::DebugLog("header_callback: datestring = $datestring");
311    }
312
313
314    ${$message_ref} .= $headerf . ': ' . $headerv . "\n";
315    $ctx->setpriv($message_ref);
316
317    Sendmail::PMilter::SMFIS_CONTINUE;
318}
319
320sub eoh_callback
321{
322    my $ctx = shift;
323    my $message_ref = $ctx->getpriv();
324    my $id;
325    my $line;
326    my $buffer;
327    my $ip;
328    my @to;
329    my $from;
330
331    # Are we in scanner mode?
332    my $scannermode = MailScanner::Config::Value('milterscanner');
333
334    MailScanner::Log::DebugLog("eoh_callback: scannermode = $scannermode");
335
336    while (${$message_ref} =~ /([^\n]+)\n?/g) {
337        my $line = $1;
338        $buffer .= $1 . "\n";
339        if ($line =~ /^Received: / ) {
340            $ip = '127.0.0.1';
341            if ($line =~ /^Received: .+?\(.*?\[(?:IPv6:)?([0-9a-f.:]+)\]/i) {
342                $ip = $1;
343            }
344            MailScanner::Log::DebugLog("eoh_callback: ip = $ip");
345        } elsif ( $line =~ m/^.*SMTP id / ) {
346            # We may have added a trailing ; so remove it if so.
347            $line =~ s/^.*SMTP id ([^;]*)(?:;?)/$1/;
348            $id = $line;
349            MailScanner::Log::DebugLog("eoh_callback: id = $id");
350            last;
351        } elsif ( $line =~ /^O/ ) {
352            $line =~ s/^O//;
353            $line =~ s/^\<//;
354            $line =~ s/\>.*$//;
355            push @to, $line;
356            MailScanner::Log::DebugLog("eoh_callback: to = $line");
357        } elsif ( $line =~ /^F/ ) {
358            $line =~ s/^F//;
359            $line =~ s/^\<//;
360            $line =~ s/\>.*$//;
361            $from = $line;
362            MailScanner::Log::DebugLog("eoh_callback: from = $line");
363        }
364    }
365
366    if ( $id eq '' ) {
367        # Missing a queue id, header_callback possibly skipped, reject
368        MailScanner::Log::WarnLog("Postfix queue id is missing");
369        Sendmail::PMilter::SMFIS_TEMPFAIL;
370        return;
371    }
372
373    # Write header to file
374    my $queuehandle = new FileHandle;
375    my $incoming = MailScanner::Config::Value('inqueuedir');
376    MailScanner::Log::DebugLog("eoh_callback: incoming = " . @{$incoming}[0]);
377    if ($incoming eq '') {
378        MailScanner::Log::WarnLog("Unable to determine incoming queue!");
379        Sendmail::PMilter::SMFIS_TEMPFAIL;
380        return;
381    }
382    my $file = @{$incoming}[0] . '/temp-' . $id;
383
384    MailScanner::Log::DebugLog("eoh_callback: file = $file");
385
386    my $ret;
387    $ret = MailScanner::Lock::openlock($queuehandle,'>>' . $file, 'w');
388    if ($ret != 1) {
389        MailScanner::Log::WarnLog("Unable to to open queue temp file for writing!");
390        Sendmail::PMilter::SMFIS_TEMPFAIL;
391        return;
392    }
393
394    $queuehandle->print(${$message_ref});
395
396    # Signal end of header
397    $queuehandle->print("\n");
398
399    $queuehandle->flush();
400    MailScanner::Lock::unlockclose($queuehandle);
401
402    # Determine what to do next
403    if ($scannermode == 1) {
404        MailScanner::Log::DebugLog("eoh_callback: scanner mode enabled");
405        # Blacklist test
406        # Build a message object to test against
407        my $message = {};
408        if ($from ne '<>') {
409            $message->{from} = $from;
410        } else {
411            $message->{from} = '';
412        }
413        @{$message->{to}} = @to;
414        $message->{clientip} = $ip;
415        my $user;
416        my $domain;
417        ($user, $domain) = MailScanner::Message::address2userdomain($message->{from});
418        $message->{fromuser} = $user;
419        $message->{fromdomain} = $domain;
420
421        my $touser;
422        my $todomain;
423        my $count;
424
425        foreach my $msgto (@{$message->{to}}) {
426            ($touser, $todomain) = MailScanner::Message::address2userdomain($msgto);
427            push @{$message->{touser}}, $touser;
428            push @{$message->{todomain}}, $todomain;
429            $count++;
430        }
431
432        my $test;
433        # Check whitelist first
434        $test = MailScanner::Config::Value('spamwhitelist', $message);
435
436        my $maxrecips = MailScanner::Config::Value('whitelistmaxrecips');
437        $maxrecips = 999999 unless $maxrecips;
438
439        # Not in whitelist or a large number of recipients, then check blacklist
440        if ($test == 0 || $count > $maxrecips) {
441            MailScanner::Log::DebugLog("eom_callback: no whitelist match, proceeding to blacklist test");
442            # test
443            $test = MailScanner::Config::Value('spamblacklist', $message);
444
445            # Blacklisted, fire a reject
446            if ($test == 1) {
447                MailScanner::Log::DebugLog("eom_callback: blacklist match, firing reject");
448                # Set reject code
449                $ctx->setreply('554', '5.7.1', 'Message Blacklisted');
450
451                # Delete temp file
452                unlink $file;
453                return Sendmail::PMilter::SMFIS_REJECT;
454            }
455        }
456    }
457
458    MailScanner::Log::DebugLog("eoh_callback: End of Header detected");
459
460    # Start storing just the id
461    ${$message_ref} = $id;
462
463    $ctx->setpriv($message_ref);
464
465    Sendmail::PMilter::SMFIS_CONTINUE;
466}
467
468sub body_callback
469{
470    my $ctx = shift;
471    my $body_chunk = shift;
472    my $len = shift;
473    my $message_ref = $ctx->getpriv();
474
475    my $queuehandle = new FileHandle;
476    my $incoming = MailScanner::Config::Value('inqueuedir');
477    MailScanner::Log::DebugLog("body_callback: incoming = " . @{$incoming}[0]);
478    if ($incoming eq '') {
479        MailScanner::Log::WarnLog("Unable to determine incoming queue!");
480        Sendmail::PMilter::SMFIS_TEMPFAIL;
481        return;
482    }
483    my $file = @{$incoming}[0] . '/temp-' . ${$message_ref};
484
485    MailScanner::Log::DebugLog("body_callback: file = $file");
486
487    my $ret;
488    $ret = MailScanner::Lock::openlock($queuehandle,'>>' . $file, 'w');
489    if ($ret != 1) {
490        MailScanner::Log::WarnLog("Unable to to open queue temp file for writing!");
491        Sendmail::PMilter::SMFIS_TEMPFAIL;
492        return;
493    }
494    $queuehandle->print($body_chunk);
495
496    $queuehandle->flush();
497    MailScanner::Lock::unlockclose($queuehandle);
498
499    $ctx->setpriv($message_ref);
500
501    Sendmail::PMilter::SMFIS_CONTINUE;
502}
503
504sub eom_callback
505{
506    my $ctx = shift;
507    my $message_ref = $ctx->getpriv();
508
509    # Store reference for subsequent messages in connection or for connection close
510    $ctx->setpriv($message_ref);
511
512    # Send DISCARD signal to accept message and drop from postfix
513    # for mailscanner processing
514    Sendmail::PMilter::SMFIS_DISCARD;
515}
516
517sub close_callback
518{
519    my $ctx = shift;
520    my $message_ref = $ctx->getpriv();
521
522    my $incoming = MailScanner::Config::Value('inqueuedir');
523    MailScanner::Log::DebugLog("eom_callback: incoming = " . @{$incoming}[0]);
524    if ($incoming eq '') {
525        MailScanner::Log::WarnLog("Unable to determine incoming queue!");
526        Sendmail::PMilter::SMFIS_TEMPFAIL;
527        return;
528    }
529    my $file = @{$incoming}[0] . '/temp-' . ${$message_ref};
530    my $file2 = @{$incoming}[0] . '/' . ${$message_ref};
531
532    move($file, $file2);
533
534    $ctx->setpriv(undef);
535
536    Sendmail::PMilter::SMFIS_CONTINUE;
537}
538
539#
540# Create and write a PID file for a given process id
541#
542sub WritePIDFile {
543    my($process,$PidFile) = @_;
544
545    my $pidfh = new FileHandle;
546    $pidfh->open(">$PidFile")
547    or MailScanner::Log::WarnLog("Cannot write pid file %s, %s", $PidFile, $!);
548    print $pidfh "$process\n";
549    $pidfh->close();
550}
551
552#
553# Start logging
554#
555sub StartLogging {
556    my($filename) = @_;
557
558    my $procname = 'MSMilter';
559
560    my $logbanner = "MSMilter Daemon starting...";
561
562    MailScanner::Log::Configure($logbanner, 'syslog');
563
564    # Need to know log facility *before* we have read the whole config file!
565    my $facility = MailScanner::Config::QuickPeek($filename, 'syslogfacility');
566    my $logsock  = MailScanner::Config::QuickPeek($filename, 'syslogsockettype');
567
568    MailScanner::Log::Start($procname, $facility, $logsock);
569}
570
571sub ExitParent {
572    my($sig) = @_; # Arg is the signal name
573
574    # Reap children (wait up to one minute)
575    my $counter=0;
576    while ($counter < 30) {
577    if (wait() > 0) {
578        # Still active children
579        sleep(2);
580        $counter++;
581        MailScanner::Log::NoticeLog("Milter closing, waiting on children to finish...");
582    } else {
583        $counter=30;
584    }
585    }
586
587    # Cleanup partial writes
588    my $incoming = MailScanner::Config::Value('inqueuedir');
589    unlink glob @{$incoming}[0] . "/temp-*";
590
591    unlink $PidFile; # Ditch the pid file
592
593    MailScanner::Log::Stop();
594
595    exit 0;
596}
597
598
599BEGIN {
600    chdir('/usr/share/MailScanner/perl');
601
602    $ENV{PATH}="/sbin:/bin:/usr/sbin:/usr/bin";
603
604    umask 0077;
605
606    $ConfFile = $ARGV[0];
607    # Use the default if we couldn't find theirs. Will save a lot of grief.
608    $ConfFile = '/etc/MailScanner/MailScanner.conf' if $ConfFile eq "" ||
609                                                           !(-f $ConfFile);
610
611    # Start logging
612    StartLogging($ConfFile);
613
614    MailScanner::Config::initialise(MailScanner::Config::QuickPeek($ConfFile,
615                                  'customfunctionsdir'));
616    # Read Config File
617    MailScanner::Config::Read($ConfFile);
618
619    my $pid = fork;
620
621    if (!defined($pid)) {
622        MailScanner::Log::WarnLog("Milter:  Unable to fork!");
623        exit 1;
624    }
625    if ($pid == 0) {
626        $0 = "MSMilter Daemon";
627
628        # Need this new parent process to ignore SIGHUP, and catch SIGTERM
629        $SIG{'HUP'} = 'IGNORE';
630        $SIG{'TERM'} = \&ExitParent;
631
632        my $PidFile = MailScanner::Config::Value('milterpidfile');
633        WritePIDFile($$,$PidFile);
634
635        my $dispatcher = MailScanner::Config::Value('milterdispatcher');
636        my $children = MailScanner::Config::Value('miltermaxchildren');
637        my $user = MailScanner::Config::Value('runasuser');
638        my $group = MailScanner::Config::Value('workgroup');
639        my $bind = MailScanner::Config::Value('milterbind');
640        my $port = MailScanner::Config::Value('milterport');
641
642        MailScanner::Log::Reset();
643
644        MailScanner::Log::DebugLog("initialization:\n PidFile = $PidFile\n user = $user\n group = $group\n bind = $bind\n port = $port");
645
646        my %my_callbacks =
647        (
648            'connect' => \&connect_callback,
649            'helo' =>    \&helo_callback,
650            'envrcpt' => \&envrcpt_callback,
651            'header' =>  \&header_callback,
652            'eoh' =>     \&eoh_callback,
653            'body' =>    \&body_callback,
654            'eom' =>     \&eom_callback,
655            'close' =>   \&close_callback,
656        );
657
658        my $conn = 'inet:'. $port . '@' . $bind;
659        $ENV{PMILTER_DISPATCHER} = $dispatcher;
660        my $milter = Sendmail::PMilter->new();
661        $milter->register('MSMilter',
662                      \%my_callbacks,
663                      Sendmail::PMilter::SMFI_CURR_ACTS
664                     );
665        $milter->setconn($conn);
666        $< = $> = getpwnam($user);
667        $( = $) = getgrnam($group);
668        if ( $dispatcher eq "prefork" ) {
669            $milter->main($children,100);
670        } else {
671            $milter->main();
672        }
673
674        # Should never get here, if we did, it didn't work
675        MailScanner::Log::WarnLog("Unable to spawn milter!");
676        exit 1
677    }
678    # Successful if we reach this point
679    exit 0;
680}
681
6821;
683