# # my version 1.34, 11-16-08 michael@bizsystems.com # -*- Perl -*- #*********************************************************************** # # mimedefang-filter # # Suggested filter for use with SpamCannibal & SpamAssassin # to protect Microsoft Windows clients, plus # # Copyright (C) 2004 - 2008, Michael Robinton, michael@bizsystems.com # Copyright (C) 2002 Roaring Penguin Software Inc. # # This program may be distributed under the terms of the GNU General # Public License, Version 2, or (at your option) any later version. # #*********************************************************************** #*********************** # our stuff $NotifyNoPreamble = 1; #*********************************************************************** # Set administrator's e-mail address here. The administrator receives # quarantine messages and is listed as the contact for site-wide # MIMEDefang policy. A good example would be 'defang-admin@mydomain.com' #*********************************************************************** $AdminAddress = 'postmaster'; $AdminName = "Postmaster"; #*********************************************************************** # Set the e-mail address from which MIMEDefang quarantine warnings and # user notifications appear to come. A good example would be # 'mimedefang@mydomain.com'. Make sure to have an alias for this # address if you want replies to it to work. #*********************************************************************** $DaemonAddress = ''; #*********************************************************************** # If you set $AddWarningsInline to 1, then MIMEDefang tries *very* hard # to add warnings directly in the message body (text or html) rather # than adding a separate "WARNING.TXT" MIME part. If the message # has no text or html part, then a separate MIME part is still used. #*********************************************************************** $AddWarningsInline = 0; #*********************************************************************** # To enable syslogging of virus and spam activity, add the following # to the filter: # md_graphdefang_log_enable(); # You may optionally provide a syslogging facility by passing an # argument such as: md_graphdefang_log_enable('local4'); If you do this, be # sure to setup the new syslog facility (probably in /etc/syslog.conf). # An optional second argument causes a line of output to be produced # for each recipient (if it is 1), or only a single summary line # for all recipients (if it is 0.) The default is 1. # Comment this line out to disable logging. #*********************************************************************** md_graphdefang_log_enable('mail', 1); #*********************************************************************** # Uncomment this to block messages with more than 50 parts. This will # *NOT* work unless you're using Roaring Penguin's patched version # of MIME tools, version MIME-tools-5.411a-RP-Patched-02 or later. # # WARNING: DO NOT SET THIS VARIABLE unless you're using at least # MIME-tools-5.411a-RP-Patched-02; otherwise, your filter will fail. #*********************************************************************** $MaxMIMEParts = 50; #*********************************************************************** # Set various stupid things your mail client does below. #*********************************************************************** # Set the next one if your mail client cannot handle nested multipart # messages. DO NOT set this lightly; it will cause action_add_part to # work rather strangely. Leave it at zero, even for MS Outlook, unless # you have serious problems. $Stupidity{"flatten"} = 0; # Set the next one if your mail client cannot handle multiple "inline" # parts. $Stupidity{"NoMultipleInlines"} = 0; # The next lines force SpamAssassin modules to be loaded and rules # to be compiled immediately. This may improve performance on busy # mail servers. Comment the lines out if you don't like them. if ($Features{"SpamAssassin"}) { spam_assassin_init()->compile_now(1) if defined(spam_assassin_init()); # If you want to use auto-whitelisting: # if (defined($SASpamTester)) { # use Mail::SpamAssassin::DBBasedAddrList; # my $awl = Mail::SpamAssassin::DBBasedAddrList->new(); # $SASpamTester->set_persistent_address_list_factory($awl) if defined($awl); # } } # This procedure returns true for entities with bad filenames. sub filter_bad_filename ($) { my($entity) = @_; my($bad_exts, $re); # Bad extensions $bad_exts = '(ade|adp|app|asd|asf|asx|bas|bat|chm|cmd|com|cpl|crt|dll|exe|fxp|hlp|hta|hto|inf|ini|ins|isp|jse?|lib|lnk|mdb|mde|msc|msi|msp|mst|ocx|pcd|pif|prg|rar|reg|scr|sct|sh|shb|shs|sys|url|vb|vbe|vbs|vcs|vxd|wmd|wms|wmz|wsc|wsf|wsh|\{[^\}]+\})'; # Do not allow: # - CLSIDs {foobarbaz} # - bad extensions (possibly with trailing dots) at end $re = '\.' . $bad_exts . '\.*$'; return 1 if (re_match($entity, $re)); # Look inside ZIP files if (re_match($entity, '\.zip$')) { if ($Features{"Archive::Zip"}) { my $bh = $entity->bodyhandle(); if (defined($bh)) { my $path = $bh->path(); if (defined($path)) { return re_match_in_zip_directory($path, $re); } } } # clobber zip files if we can't look inside the zip archives else { return 1; } } return 0; } #*********************************************************************** # my filter begins here and is a modified version of one of the examples #*********************************************************************** # # flag to discard all tagged mail, none is reported to spamcannibal # $SpamCannibalDropAll = 0; # flag to discard or report virus mail to spamcannibal # $SpamCannibalReportVirus = 1; # address of spamcannibal processing daemon # $SpamCannibalModerator = 'spamtrap@mydomain.com'; # address of spamcannibal robot reader # $SpamCannibalReplyTo = 'spam@mydomain.com'; $SpamCannibalReason = ''; # if you want statistics on mail diversions, path to stats file # DIRECTORY must exist!!, must be writeable by "defang:users" # $SpamCannibalStats = '/etc/mail/sc_stats/sc_mdf_stats.txt'; # white list local networks, localhost, allowed hosts # @Relay_whitelist = ( '127.0.0.1', '192.168.1', '192.168.20', ); # if you want the white list filled from the SpamCannibal IGNORE list # specify the path to the sc_BlackList.conf file. It will be appended to # any IP's listed above # $Relay_Blacklistfile = '/usr/local/spamcannibal/config/sc_BlackList.conf'; # Relay Whitelist cache file directory # must be writable by the 'mimedefang' owner # $Relay_WhiteCache = '/etc/mail/sc_stats'; # a hash of domains => primary.mail.server # where real users might be found # WARNING: ip addresses of hosts in this list that have these # domain names must appear in the whitelist above # %Relay_checklist = ( 'mydomain.com' => 'mail1.mydomain.com', 'hosted-domain.com' => 'mail-outer.mydomain.com', 'otherdomain.net' => 'mail1.mydomain.com', 'anotherdomain.com' => 'mail1.mydomain.com', ); # Relay domain list -- domains for which we are responsible # This list is used to check for bogus HELO # list is valid domains from inner mail concentrator /etc/mail/sendmail.vh @Relay_domain_list = qw| myhosted.domain.com another.hosted.domain.com personal.domain.com another.valid.domain.net |; # Primary mail host for each domain above # you can set this array up manually or # ins a for loop as below. Used in filter_recipient # below to verify the the recipient is valid # $Relay_innerhost = 'inner.mailhost.com'; # add large domain lists to checklist foreach (@Relay_domain_list) { $Relay_checklist{$_} = $Relay_innerhost; } use Net::DNSBL::Utilities qw( list2NetAddr matchNetAddr doINCLUDE ); my $havecache = 0; if ( $Relay_WhiteCache && -d $Relay_WhiteCache ) { $havecache = 1; my $metime = (stat($0))[9]; my $sctime = ($Relay_Blacklistfile && -e $Relay_Blacklistfile) ? (stat($Relay_Blacklistfile))[9] : 0; my $cshtim = (-e $Relay_WhiteCache .'/relaywhitelist.cache') ? (stat($Relay_WhiteCache .'/relaywhitelist.cache'))[9] : 1; if ($metime > $cshtim || $sctime > $cshtim) { my $scSptr = doINCLUDE($Relay_Blacklistfile); # spamcannibal Stuff pointer $scSptr = $scSptr->{IGNORE} if $scSptr; $scSptr = [] unless $scSptr; # must point to some array push @Relay_whitelist, @$scSptr; local *CACHE; if (open (CACHE,'>'. $Relay_WhiteCache .'/relaywhitelist.cache')) { print CACHE << 'EOF'; my $cache = [qw( EOF foreach (@Relay_whitelist) { print CACHE "\t$_\n"; } print CACHE q|)]; |; close CACHE; } else { die "could not open ${Relay_WhiteCache}/relaywhitelist.cache for write"; } } else { $havecache = doINCLUDE($Relay_WhiteCache .'/relaywhitelist.cache'); die "could not open ${Relay_WhiteCache}/relaywhitelist.cache for read" unless $havecache; @Relay_whitelist = @$havecache; } } @NAwhitelist = (); list2NetAddr(\@Relay_whitelist,\@NAwhitelist); # check for and pass white listed mail relay sources # sub filter_relay { my($ip,$name) = @_; # if (grep($ip =~ /^$_/,@Relay_whitelist)) { if (matchNetAddr($ip,\@NAwhitelist)) { sc_profile('WhiteList'); return ('ACCEPT_AND_NO_MORE_FILTERING','ok') } return ('CONTINUE','ok'); } # check for bogus HELO and dynamic IP # # regexp for common rDNS PTR entries that are ASSIGNED by ISP's to # dynamic or non MX static accounts # my $pattern = 'pooles|not-active|ipad|unknown|customer|unused|no-dns|no-rdns|'. 'reverse|wlan|user|usr|nat|catv|modem|cable|modemcable|'. 'cdm|cm\d|client|cust|dhcp|dial|dialuol|dialup|dialip|dip|'. 'docsis|(a|c|s|x|v|)dsl|dyn(amic|dsl|)|host|pool|ppp|in\-addr\.arpa'; # pattern for ip address's of the form n+?n+?n+?n+ or 12 n's # as in 1.2.3.4 => 001002003004 # my $ipattern = '\d+[a-zA-Z_\-\.]\d+[a-zA-Z_\-\.]\d+[a-zA-Z_\-\.]\d+|\d{12}'; # mark known odd domain patterns my @dyn_ok_domains = qw( dsl-only Disetronic ); my @known_bad_domains = ( # these are dynamic patterns for specific domains '^SHASTA\d+', '^FL.+mesh\.ad\.jp', '^pc.+\d+.+\.comcast\.net', '^cncln.online.ln.cn', '^s[a-z0-9]+\..+shawcable\.net', '^nameservices.net', '^no.such', 'adsl$', 'dhcp$', ); sub filter_sender { my($sender,$ip,$hostname,$helo) = @_; if ($helo =~ /\d+\.\d+\.\d+\.\d+/) { # claims to be an IP address my $heloIP = $&; # discard if it claims to be one of our white listed IP addresses # return(sc_profile('bogus_helo','REJECT',"bogus HELO $helo")) if grep(/$heloIP/,@Relay_whitelist); return(sc_profile('bogus_helo','REJECT',"bogus HELO $helo")) if matchNetAddr($heloIP,\@NAwhitelist); } else { return(sc_profile('bogus_helo','REJECT',"bogus HELO $helo")) if grep($helo =~ /$_$/i,keys %Relay_checklist); } # discard bogus known hosts return(sc_profile('bogus_helo','REJECT',"bogus HELO $helo")) if $helo =~ /localhost/i && $ip ne '127.0.0.1'; return(sc_profile('bad_hostname','REJECT',"bogus localhost $ip")) if $hostname eq 'localhost' && $ip ne '127.0.0.1'; return(sc_profile('bad_hostname','REJECT',"bogus host $hostname")) if $hostname =~ /unassigned/i || $hostname =~ /local$/i; # fail known dynamic host patterns return(sc_profile('bad_hostname','REJECT',"dynamic host $hostname")) if grep($hostname =~ /$_/i,@known_bad_domains); # return if not possible dynamic host return ('CONTINUE','ok') if $hostname =~ /^mail/i; # return if known non-dynamic host return ('CONTINUE','ok') if grep ($hostname =~ /$_/i,@dyn_ok_domains); # uncomment this to reject hosts of the form nnn.nnn.nnn.nnn or dashes or whatever in between # return (sc_profile('reverse_IP','REJECT',"bad reverse IP |$&| $hostname")) # if $hostname =~ /$ipattern/o; return ('CONTINUE','ok') unless $hostname =~ /([(.\-]|\b)($pattern).?[.\-\d]/io; my $match = $&; # return if definetly not dynamic host, ends in 'match'.org|com|etc... return ('CONTINUE','ok') if $hostname =~ /(($match)[\.]?[a-z]+)$/io; # must be dynamic or at least a customer line with in-appropriate rDNS return (sc_profile('dynamic_IP','REJECT',"dynamic IP |$match| $hostname")); } # check recipients for our domains # sub filter_recipient { my($recip, $sender, $ip, $host, $first, $helo, $rcpt_mailer, $rcpt_host, $rcpt_addr) = @_; my $cleanrecip = sc_clean_email($recip); # catch email to postmaster and abuse from null sender # if ($sender =~ /^?$/ && $cleanrecip =~ /^(postmaster|abuse)\@/i) { # return (sc_profile("null_${1}_sender","REJECT','553 5.1.7 null sender not acceptable for $1")); # } my @match = grep($cleanrecip =~ /\@$_$/i,keys %Relay_checklist); if (@match) { my($rv,$msg) = md_check_against_smtp_server($sender, $cleanrecip,$helo,$Relay_checklist{$match[0]}); # return error if good connection and recip not found #if($rv eq 'REJECT') { # $msg = "RECIP_CHECK $recip -> $sender|$cleanrecip|$helo|$Relay_checklist{$match[0]}|$rv|, ". $msg; #} return (sc_profile('invalid_recip','REJECT',$msg)) if $rv eq 'REJECT'; } # else always return OK, even on failed connection md_syslog('warning',"DEBUG $recip: mailer=$rcpt_mailer, host=$rcpt_host, addr=$rcpt_addr\n"); return ('CONTINUE','ok'); } ################################################### # SpamCannibal specific functions begin with sc_... # profile the failure reasons in a file # fails silently if file open fails # # input: count_name, # @return_arguments # # returns: @return_arguments # sub sc_profile { my $reason = shift; return @_ unless $SpamCannibalStats; # profiling must be enabled return @_ if -e $CWD.'/stats_counted'; # return if already counted require Fcntl; import Fcntl qw(O_RDWR O_CREAT O_TRUNC O_RDONLY O_WRONLY LOCK_EX); local (*LOCK,*FILE); my $perms = 0644; umask 022; # leave trace file when counting stats to prevent duplicates close FILE if sysopen FILE, $CWD .'/stats_counted',&O_RDWR|&O_CREAT|&O_TRUNC,$perms; unless (sysopen LOCK, $SpamCannibalStats .'.lock', &O_RDWR|&O_CREAT|&O_TRUNC, $perms) { # print STDERR "failed to open lock file ${SpamCannibalStats}.lock\n" if $DEBUG; return @_; } unless (flock(LOCK,&LOCK_EX)) { close LOCK; # print STDERR "failed flock on ${SpamCannibalStats}.lock\n" if $DEBUG; return @_; } unless (sysopen FILE, $SpamCannibalStats, &O_RDONLY|&O_CREAT, $perms) { close LOCK; # print STDERR "failed to open ${SpamCannibalStats} for read\n" if $DEBUG; return @_; } #### read contents of existing file my $sti = '# stats since '. localtime(time) ."\n"; my %counts; foreach() { $sti = $_ if $_ =~ /# stats since/; # use old init time if present next unless $_ =~ /^(\d+)\s+(.+)/; $counts{"$2"} = $1; } close FILE; #### increment or create count if ($counts{$reason}) { $counts{$reason} += 1; } else { $counts{$reason} = 1; } #### write results and release lock unless (sysopen FILE, $SpamCannibalStats .'.tmp', &O_WRONLY|&O_CREAT|&O_TRUNC, $perms) { close LOCK; # print STDERR "failed to open tmp file ${SpamCannibalStats}.tmp\n" if $DEBUG; return @_; } my $savsel = select FILE; $| = 1; select $savsel; print FILE '# last update '. localtime(time) ."\n". $sti; my $total = 0; foreach(sort { $counts{$b} <=> $counts{$a} } keys %counts) { next if $_ =~ /^(White|Passed)/; $total += $counts{$_}; print FILE $counts{$_}, "\t$_\n"; } print FILE "# $total\ttotal rejects\n#\n"; foreach(qw(WhiteList Passed)) { print FILE $counts{$_},"\t$_\n" if exists $counts{$_}; } close FILE; rename $SpamCannibalStats .'.tmp', $SpamCannibalStats; # atomic update #### release lock close LOCK; return @_; } sub sc_discard { $SpamCannibalReason = shift; return action_discard(); } # strip brackets, etc... from email addy for internal use sub sc_clean_email { my $addy = shift; if ($addy =~ /[a-zA-Z0-9\._\-]+\@[a-zA-Z0-9_\-]+\.[a-zA-Z0-9\._\-]+/) { return $&; } return $addy; # return brackets if that's all there is } # routine to discard mail instead of sending to spamcannibal # # returns: undef to drop all # filename virus attached # false send to SC # sub sc_mail_discard { return undef if $SpamCannibalDropAll; return $1 if $SpamCannibalReportVirus && $SpamCannibalReason =~ /bad_filename\s+(.+)\s+\S+$/i; return 0; } #*********************************************************************** # %PROCEDURE: filter_begin # %ARGUMENTS: # None # %RETURNS: # Nothing # %DESCRIPTION: # Called just before e-mail parts are processed #*********************************************************************** sub filter_begin () { $SpamCannibalReason = ''; # clear # ALWAYS drop messages with suspicious chars in headers if ($SuspiciousCharsInHeaders) { md_graphdefang_log('suspicious_chars'); # action_quarantine_entire_message("Message quarantined because of suspicious characters in headers"); # Do NOT allow message to reach recipient(s) return sc_discard('suspicious_chars'); } # Copy original message into work directory as an "mbox" file for # virus-scanning md_copy_orig_msg_to_work_dir_as_mbox_file(); # Scan for viruses if any virus-scanners are installed my($code, $category, $action) = message_contains_virus(); # Lower level of paranoia - only looks for actual viruses $FoundVirus = ($category eq "virus"); # Higher level of paranoia - takes care of "suspicious" objects # $FoundVirus = ($action eq "quarantine"); if ($FoundVirus) { md_graphdefang_log('virus', $VirusName, $RelayAddr); md_syslog('warning', "Discarding because of virus $VirusName"); return sc_discard('virus'); } if ($action eq "tempfail") { action_tempfail("Problem running virus-scanner"); md_syslog('warning', "Problem running virus scanner: code=$code, category=$category, action=$action"); } } #*********************************************************************** # %PROCEDURE: filter # %ARGUMENTS: # entity -- a Mime::Entity object (see MIME-tools documentation for details) # fname -- the suggested filename, taken from the MIME Content-Disposition: # header. If no filename was suggested, then fname is "" # ext -- the file extension (everything from the last period in the name # to the end of the name, including the period.) # type -- the MIME type, taken from the Content-Type: header. # # NOTE: There are two likely and one unlikely place for a filename to # appear in a MIME message: In Content-Disposition: filename, in # Content-Type: name, and in Content-Description. If you are paranoid, # you will use the re_match and re_match_ext functions, which return true # if ANY of these possibilities match. re_match checks the whole name; # re_match_ext checks the extension. See the sample filter below for usage. # %RETURNS: # Nothing # %DESCRIPTION: # This function is called once for each part of a MIME message. # There are many action_*() routines which can decide the fate # of each part; see the mimedefang-filter man page. #*********************************************************************** sub filter ($$$$) { my($entity, $fname, $ext, $type) = @_; return if message_rejected(); # Avoid unnecessary work # Block message/partial parts if (lc($type) eq "message/partial") { md_graphdefang_log('message/partial'); # don't bounce message, send it to spamcannibal # action_bounce("MIME type message/partial not accepted here"); return sc_discard('message/partial'); } if (filter_bad_filename($entity)) { md_graphdefang_log('bad_filename', $fname, $type); # return action_drop_with_warning("An attachment named $fname was removed from this document as it\nconstituted a security hazard. If you require this document, please contact\nthe sender and arrange an alternate means of receiving it.\n"); # discard and send instead to spamcannibal return sc_discard("bad_filename $fname $type"); } # eml is bad if it's not multipart if (re_match($entity, '\.eml')) { md_graphdefang_log('non_multipart'); # return action_drop_with_warning("A non-multipart attachment named $fname was removed from this document as it\nconstituted a security hazard. If you require this document, please contact\nthe sender and arrange an alternate means of receiving it.\n"); # discard and send instead to spamcannibal return sc_discard('non_multipart'); } # Clean up HTML if Anomy::HTMLCleaner is installed. if ($Features{"HTMLCleaner"}) { if ($type eq "text/html") { return anomy_clean_html($entity); } } return action_accept(); } #*********************************************************************** # %PROCEDURE: filter_multipart # %ARGUMENTS: # entity -- a Mime::Entity object (see MIME-tools documentation for details) # fname -- the suggested filename, taken from the MIME Content-Disposition: # header. If no filename was suggested, then fname is "" # ext -- the file extension (everything from the last period in the name # to the end of the name, including the period.) # type -- the MIME type, taken from the Content-Type: header. # %RETURNS: # Nothing # %DESCRIPTION: # This is called for multipart "container" parts such as message/rfc822. # You cannot replace the body (because multipart parts have no body), # but you should check for bad filenames. #*********************************************************************** sub filter_multipart ($$$$) { my($entity, $fname, $ext, $type) = @_; return if message_rejected(); # Avoid unnecessary work if (filter_bad_filename($entity)) { md_graphdefang_log('bad_filename', $fname, $type); action_notify_administrator("A MULTIPART attachment of type $type, named $fname was discarded.\n"); # return action_drop_with_warning("An attachment of type $type, named $fname was removed from this document as it\nconstituted a security hazard. If you require this document, please contact\nthe sender and arrange an alternate means of receiving it.\n"); # discard and send instead to spamcannibal return sc_discard("bad_filename $fname $type"); } # eml is bad if it's not message/rfc822 if (re_match($entity, '\.eml') and ($type ne "message/rfc822")) { md_graphdefang_log('non_rfc822',$fname); # return action_drop_with_warning("A non-message/rfc822 attachment named $fname was removed from this document as it\nconstituted a security hazard. If you require this document, please contact\nthe sender and arrange an alternate means of receiving it.\n"); # discard and send instead to spamcannibal return sc_discard("non_rfc822 $fname"); } # Block message/partial parts if (lc($type) eq "message/partial") { md_graphdefang_log('message/partial'); # action_bounce("MIME type message/partial not accepted here"); # return; # discard and send instead to spamcannibal return sc_discard('message/partial'); } return action_accept(); } #*********************************************************************** # %PROCEDURE: defang_warning # %ARGUMENTS: # oldfname -- the old file name of an attachment # fname -- the new "defanged" name # %RETURNS: # A warning message # %DESCRIPTION: # This function customizes the warning message when an attachment # is defanged. #*********************************************************************** sub defang_warning ($$) { my($oldfname, $fname) = @_; return "An attachment named '$oldfname' was converted to '$fname'.\n" . "To recover the file, right-click on the attachment and Save As\n" . "'$oldfname'\n"; } # If SpamAssassin found SPAM, append report. We do it as a separate # attachment of type text/plain sub filter_end ($) { my($entity) = @_; # If you want quarantine reports, uncomment next line # send_quarantine_notifications(); # IMPORTANT NOTE: YOU MUST CALL send_quarantine_notifications() AFTER # ANY PARTS HAVE BEEN QUARANTINED. SO IF YOU MODIFY THIS FILTER TO # QUARANTINE SPAM, REWORK THE LOGIC TO CALL send_quarantine_notifications() # AT THE END!!! # No sense doing any extra work # return if message_rejected(); # Spam checks if SpamAssassin is installed my $xspamscore = ''; if (! message_rejected() && $Features{"SpamAssassin"}) { if (-s "./INPUTMSG" < 100*1024) { # Only scan messages smaller than 100kB. Larger messages # are extremely unlikely to be spam, and SpamAssassin is # dreadfully slow on very large messages. my($hits, $req, $names, $report) = spam_assassin_check(); my($score); if ($hits < 40) { $score = "*" x int($hits); } else { $score = "*" x 40; } # We add a header which looks like this: # X-Spam-Score: 6.8 (******) NAME_OF_TEST,NAME_OF_TEST # The number of asterisks in parens is the integer part # of the spam score clamped to a maximum of 40. # MUA filters can easily be written to trigger on a # minimum number of asterisks... if ($hits >= $req) { # action_change_header("X-Spam-Score", "$hits ($score) $names"); $xspamscore = "$hits ($score) $names"; md_graphdefang_log('spam', $hits, $RelayAddr); # If you find the SA report useful, add it, I guess... # action_add_part($entity, "text/plain", "-suggest", # "$report\n", # "SpamAssassinReport.txt", "inline"); sc_discard('spamassassin'); } else { # Delete any existing X-Spam-Score header? action_delete_header("X-Spam-Score"); } } } # invoke spamcannibal if (defined $Actions{discard} && $Actions{discard}) { if (defined (my $virus_name = sc_mail_discard())) { if ($virus_name) { sc_profile('virus'); } else { $SpamCannibalReason =~ /\S+/; sc_profile($&); } md_graphdefang_log('sent to spamcannibal',$xx.$report); my $smhelo = $Helo || ''; my $origin = $SendmailMacros{_} || ''; my $if_name = $SendmailMacros{if_name} || ''; my $mail_mailer = uc $SendmailMacros{mail_mailer} || 'SMTP'; my $smid = $SendmailMacros{i} || ''; my $smfor = (@Recipients) ? join(',',@Recipients) : ''; chop $smfor if $smfor =~ /,$/; my $smdate = rfc2822_date(); local(*R,*I); open(R,'>./MY_COPY'); # add current received: from header print R qq |Received: from $smhelo ($origin) by $if_name with $mail_mailer id $smid for $smfor; $smdate |; open(I,'INPUTMSG'); foreach() { print R $_; next unless $virus_name; # strip virus attachment last if $_ =~ /name.+$virus_name/i; } close I; close R; my $new = MIME::Entity->build( From => 'ns2_defang@localhost', To => $SpamCannibalModerator, Subject => '[SPAM] '. $SpamCannibalReason, 'Reply-To' => $SpamCannibalReplyTo, Encoding => 'quoted-printable', Type => 'text/plain', Path => 'MY_COPY', 'X-Spam-Score' => $xspamscore, 'X-Actions' => $xx, ); open(R,'>./MY_REPLACEMENT'); $new->print(\*R); close R; rename 'MY_REPLACEMENT', 'INPUTMSG'; resend_message($SpamCannibalModerator); return; } # else drop the message } else { sc_profile('Passed'); # I HATE HTML MAIL! If there's a multipart/alternative with both # text/plain and text/html parts, nuke the text/html. Thanks for # wasting our disk space and bandwidth... # If you want to strip out HTML parts if there is a corresponding # plain-text part, uncomment the next line. # remove_redundant_html_parts($entity); md_graphdefang_log('mail_in'); # Deal with malformed MIME. # Some viruses produce malformed MIME messages that are misinterpreted # by mail clients. They also might slip under the radar of MIMEDefang. # If you are worried about this, you should canonicalize all # e-mail by uncommenting the action_rebuild() line. This will # force _all_ messages to be reconstructed as valid MIME. It will # increase the load on your server, and might break messages produced # by marginal software. Your call. # action_rebuild(); } } # DO NOT delete the next line, or Perl will complain. 1;