/* This file is part of mailfrom filter. Copyright (C) 2005, 2006 Sergey Poznyakoff 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, 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 */ /* * mailfromd configuration file for CSL mail gateways * * based on the configuration from CAE by Jeff Ballard * * $Id: mailfromd.rc,v 1.32 2007/09/10 20:38:22 dparter Exp $ * */ #pragma option relay "/etc/mail/relay-domains" #pragma option relay "/etc/mail/local-host-names" #pragma option timeout 10 minutes #pragma option initial-response-timeout 5 minutes #pragma regex +extended +icase #pragma stacksize 8196 #pragma option debug 1 #pragma option state-directory "/var/run/mailfromd" #pragma option pidfile "/var/run/mailfromd/mailfromd.pid" #pragma option port "inet:49314@127.0.0.1" # need group root to read sendmail .db files #pragma option group root #pragma database greylist expire-interval 30 days #require "match_dnsbl" #require "match_rhsbl" #require "heloarg_test" #require "check_bogon" #require "strip_domain_part" /* How many misconfigurations are against this person? */ set misconfigurations 0 set rcpts 0 /* How many rcpts have been done so far? */ set my_name "unknown" /* * check all the MX entries for bad entries or entries that have bad * addresses */ func check_mx_bogon (string domain) returns number do set domain domainpart(%domain) if ( not hasmx(%domain) ) return 0 fi loop for string mxnames getmx(%domain, 1) " " number i index(%mxnames, " "), while %i != -1, set mxnames substr(%mxnames, %i + 1) set i index(%mxnames, " ") do set mxn substr(%mxnames, 0, %i) if ( is_ip(%mxn) ) if (check_bogon(%mxn)) return 1 fi else echo "$i: mx for %domain did not resolve" return 1 fi done return 0 done func on_our_white_list (string ip) returns number do # move this to a module? if (%ip matches "^(144\\.92|128\\.104|128\\.105|146\\.151|192\\.12\\.224|192\\.160\\.134|192\\.172\\.224|198\\.150\\.3|198\\.150\\.4|198\\.150\\.5|198\\.150\\.6|198\\.150\\.9|198\\.150\\.82|72\\.33)\..*" or %ip matches "127.0.0.1" or dbmap("/etc/mailfromd/whitelist.db", %ip) ) return 1 fi return 0 done func log_match_dnsbl (string rbl_domain) returns number do if ( (match_dnsbl($client_addr, %rbl_domain, "ANY")) ) if ( %rcpts = 0 ) echo "$i: RBL: IP Address " $client_addr " is listed on " %rbl_domain add "X-Mailfromd-RBL" "IP Address " $client_addr " is listed on " %rbl_domain fi return 1 fi return 0 done func log_match_rhsbl (string rhsbl_domain) returns number do if ( (match_rhsbl($f, %rhsbl_domain, "ANY")) ) if ( %rcpts = 0 ) echo "$i: RHSBL: Domain of $f is listed on " %rhsbl_domain add "X-Mailfromd-RHSBL" "Domain of $f is listed on " %rhsbl_domain fi return 1 fi return 0 done func check_an_address (string email_address, number lookahead) returns number do # detect loops -- mail generated locally will match here set dpart domainpart %email_address if ((%dpart = %my_name) or ( %dpart = %email_address )) if not %lookahead reject 550 5.7.1 "From address needs to be fully qualified" fi if (%email_address matches '^([^\+]+)\+[^@]+(@[^@]+)$') set email_address \1\2 fi if (%email_address matches '^(.+)\.$') set email_address \1 fi if dbmap("/etc/mail/aliases.db", localpart %email_address, 1) echo "$i: %email_address is in /etc/mail/aliases.db" echo "$i: allowing rcpt: %email_address" continue fi reject 550 5.1.0 "User unknown" /* check the virtual users table */ elif (dbmap("/etc/mail/virtusertable.db", "@" domainpart(%email_address))) if (%email_address matches '^([^\+]+)\+[^@]+(@[^@]+)$') set email_address \1\2 fi if dbmap("/etc/mail/virtusertable.db", %email_address) echo "$i: %email_address is in /etc/mail/virtusertable.db" echo "$i: allowing rcpt: %email_address" continue fi reject 550 5.1.0 "User unknown" else /* We're not local addresses */ if not hasmx( domainpart( %email_address ) ) echo "$i: misconfiguration: " %email_address " does not actually have an MX record." set misconfigurations %misconfigurations + 1 if ( %rcpts = 0 ) add "X-Mailfromd-Misconfiguration" "no MX record for " %email_address "; " fi fi on poll %email_address do when success: pass when not_found: if (%lookahead) reject 550 5.1.1 %email_address " was not known by the next mail server" else #reject 550 5.1.0 "Sender validity not confirmed" echo "$i: misconfiguration: " %email_address " validity not confirmed - not found" set misconfigurations %misconfigurations + 1 add "X-Mailfromd-Misconfiguration" "cannot validate " %email_address " (not found); " fi when failure: if (%lookahead) tempfail 450 4.7.0 "Server said: %last_poll_recv" else #reject 550 5.1.0 "Sender validity not confirmed" echo "$i: misconfiguration: " %email_address " validity not confirmed - failure" set misconfigurations %misconfigurations + 1 add "X-Mailfromd-Misconfiguration" "cannot validate " %email_address " (failure); " fi when temp_failure: if (%lookahead) if (%last_poll_host matches "..") tempfail 450 4.7.0 "MX Server returned: " %last_poll_recv else tempfail 450 4.7.0 "Unable to contact the next MX server for " %email_address ". Try again lager" fi else echo "$i: misconfiguration: " %email_address " validity not confirmed - temp failure" set misconfigurations %misconfigurations + 1 add "X-Mailfromd-Misconfiguration" "cannot validate " %email_address " (temp failure); " fi done fi return 0 done func check_rcpt (string email_address) returns number do return check_an_address(%email_address, 1) done func check_sender (string email_address) returns number do return check_an_address(%email_address, 0) done /* We do some *basic* hello checking. These are the most basic checks (don't claim to be me and don't claim to be somewhere else). */ prog helo do # this assumes ehlo_domain is default # set it here for global use set my_name %ehlo_domain set my_ip resolve(%my_name) if ($client_addr = %my_ip or $client_addr = "127.0.0.1") continue fi if (resolve $s = "127.0.0.1" and not $client_addr = "127.0.0.1") reject 550 5.7.1 "You say you're here. But I look around and I don't see you." fi if (not resolve $s = "") and check_bogon(resolve $s) reject 550 5.7.1 "Your hostname matches a bogon IP." fi if (hostname ${client_addr} matches '^localhost.*') reject 550 5.7.1 "Your PTR record starts with localhost." fi if (on_our_white_list(${client_addr})) continue fi switch heloarg_test($s, $client_addr, %my_ip) do case 1: #reject 550 5.7.1 "You are not me." echo "$i: misconfiguration: " $client_addr ": heloarg_test case 1: $s" set misconfigurations %misconfigurations 1 add "X-Mailfromd-Misconfiguration" "sending server $client_addr claimed to be CS $s; " case 2: #reject 550 5.7.1 "You are are not at that IP address" echo "$i: misconfiguration: " $client_addr ": heloarg_test case 2: $s" set misconfigurations %misconfigurations 1 add "X-Mailfromd-Misconfiguration" "sending server $client_addr claimed to be at IP $s; " case 4: #reject 550 5.7.1 "Only IP addresses can be in square brackets." echo "$i: misconfiguration: " $client_addr ": heloarg_test case 4: $s" set misconfigurations %misconfigurations 1 add "X-Mailfromd-Misconfiguration" "sending server $client_addr had a bad EHLO/HELO $s; " case 5: #reject 550 5.7.1 "Your HELO/EHLO does not resolve." echo "$i: misconfiguration: " $client_addr ": heloarg_test case 5: $s" set misconfigurations %misconfigurations 1 add "X-Mailfromd-Misconfiguration" "sending server $client_addr claimed to be server name $s, but that does not resolve; " case 6: #reject 550 5.7.1 "You are not me." echo "$i: misconfiguration: " $client_addr ": heloarg_test case 6: $s" set misconfigurations %misconfigurations 1 add "X-Mailfromd-Misconfiguration" "sending server $client_addr claimed to be server name $s, but they are not; " done done prog envfrom do set orig_envfrom $1 if not (%orig_envfrom matches '^<.*>$') echo "$i: misconfiguration: MAIL FROM Missing greater and/or less than" set misconfigurations %misconfigurations + 1 add "X-Mailfromd-Misconfiguration" "MAIL FROM missing greater and/or less than; " fi /* If their hostname has something dslish consider that a misconfiguration */ if hostname ${client_addr} matches ".*(adsl|sdsl|hdsl|ldsl|dialin|dialup|ppp|dhcp|dynamic|pool-).*" echo "$i: misconfiguration: this client has a dslish domain." set misconfigurations %misconfigurations + 1 add "X-Mailfromd-Misconfiguration" "DNS record [" ( hostname ${client_addr} ) "] looks like a Broadband/Dialup line; " fi set clienthostname hostname ${client_addr} set clientdname strip_domain_part(%clienthostname, 3) if (($client_addr = "127.0.0.1") or ( relayed %clienthostname ) or ( relayed %clientdname )) continue elif $f = "" if rate($f "-" ${client_addr}, interval("1 minute")) > 1 tempfail 450 4.7.0 "Bounce or probe rate exceeded. Try again later" fi pass elif (check_bogon(resolve hostname ${client_addr})) #reject 550 5.7.1 "Your PTR record resolves to a bogon" echo "$i: misconfiguration: " $client_addr ": PTR record resolves to a bogon" set misconfigurations %misconfigurations + 4 add "X-Mailfromd-Misconfiguration" "DNS record [" ( hostname ${client_addr} ) "] resolves to a BOGON! (4x penalty); " elif (check_bogon(resolve(domainpart($f)))) #reject 550 5.7.1 "The host part of your email address resolves to a bogon" echo "$i: misconfiguration: " $f ": domain resolves to a bogon" set misconfigurations %misconfigurations + 4 add "X-Mailfromd-Misconfiguration" "Email address $f resolves to a BOGON! (4x penalty); " elif ( check_mx_bogon($f) ) echo "$i: misconfiguration: " $f ": mx resolves to a bogon" set misconfigurations %misconfigurations + 4 add "X-Mailfromd-Misconfiguration" "MX resolves to a BOGON (4x penalty); " elif $f mx fnmatches "*.yahoo.*" or $f mx fnmatches "*.namaeserver.com" /* don't check sender for yahoo, they don't give good answers */ pass else set result check_sender($f) fi /* A particular from@address-11.22.33.44 can only send 25 rcvfrom's per minute, but we make it over a longer range so that they can get a few in before being over the limit. */ if not (on_our_white_list(${client_addr})) if rate($f "-" ${client_addr}, interval("10 minute")) > 25 tempfail 450 4.7.0 "Mail sending rate exceeded. Try again later" fi fi done prog envrcpt do set orig_envrcpt $1 if not (%orig_envrcpt matches '^<(.*)>$') echo "$i: misconfiguration: RCPT TO Missing greater and/or less than" set misconfigurations %misconfigurations + 1 add "X-Mailfromd-Misconfiguration" "RCPT TO missing greater and/or less than; " else set orig_envrcpt \1 fi set orig_envrcpt tolower( %orig_envrcpt ) if (%orig_envrcpt matches '^(.+)\.$') set orig_envrcpt \1 fi if ( %orig_envrcpt = "root@silica.cs.wisc.edu" or %orig_envrcpt = "root" ) echo "$i envrcpt: %orig_envrcpt" continue fi /* ALWAYS accept mail to postmaster without delay. */ if (%orig_envrcpt fnmatches "postmaster@*") accept fi /* Greylist spammy-looking things, but nothing from the UW or in the whitelist */ /* ... but people who are on spamfriends do not get greylisting */ if not (on_our_white_list($client_addr) or dbmap("/etc/mailfromd/spamfriends.db", %orig_envrcpt) ) set cooked_ra %orig_envrcpt /* probably don't need this one */ if (%orig_envrcpt matches "^([^@]+)_at_([^@]+)$") set cooked_ra \1 "@" \2 fi /* Check here if this is a UW only email address */ # if (dbmap("/etc/mail/uwonly.db", %cooked_ra) ) # reject 550 5.7.1 "Sorry, %cooked_ra can only be e-mailed from computers on the UW campus (use smtpauth?)" # fi if (check_bogon(resolve(domainpart(%cooked_ra)))) reject 550 5.7.1 "Sorry, %cooked_ra resolves to a bogon?" fi /* First, set the number of blacklists we've got */ set bltotal 0 + (log_match_dnsbl("bl.spamcop.net")) + (log_match_dnsbl("combined.njabl.org")) + (log_match_dnsbl("db.wpbl.info")) + (log_match_dnsbl("dnsbl.ahbl.org")) + (log_match_dnsbl("dnsbl.sorbs.net")) + (log_match_dnsbl("ix.dnsbl.manitu.net")) + (log_match_dnsbl("sbl-xbl.spamhaus.org")) + (log_match_dnsbl("mail-abuse.blacklist.jippg.org")) if not $f = "" set bltotal %bltotal +(log_match_rhsbl("rhsbl.ahbl.org")) fi /* Next, one blacklist gets you a 15 minute greylist. Two or more, 30 minutes times the number of blacklists you are on */ if (%bltotal = 1) set gltime interval("15 minutes") if ( %rcpts = 0 ) add "X-Mailfromd" "Total of 1 RBL listing (15 mins)" fi else set gltime interval("30 minutes") * %bltotal if (%bltotal > 0) if ( %rcpts = 0 ) add "X-Mailfromd" "Total of " %bltotal " RBL listings (30 mins each)" fi fi fi if (%misconfigurations = 1) set gltime %gltime + interval("15 minutes") if ( %rcpts = 0 ) add "X-Mailfromd" "Total of 1 misconfiguration (15 mins)" fi else set gltime %gltime + (interval("30 minutes") * %misconfigurations) if (%misconfigurations > 0) if ( %rcpts = 0 ) add "X-Mailfromd" "Total of " %misconfigurations " misconfigurations (30 mins each)" fi fi fi if (%gltime >= 1) if ( %rcpts = 0 ) add "X-Mailfromd-Greylist-Time" "May have had a total greylist delay of " (%gltime / 60) " minutes" fi if greylist($client_addr "-" $f "-" %orig_envrcpt, %gltime) if %greylist_seconds_left = %gltime set gltime (%gltime / 60) tempfail 450 4.7.0 "You are greylisted for " %gltime " minutes (" %bltotal " RBLs and " %misconfigurations " misconfigurations)" elif %greylist_seconds_left = 0 tempfail 450 4.7.0 "You are greylisted (" %bltotal " RBLs)" else set greylist_seconds_left (%greylist_seconds_left / 60)+1 set minuteses " minute" if (%greylist_seconds_left > 1) set minuteses " minutes" fi tempfail 450 4.7.0 "Still greylisted for another " %greylist_seconds_left %minuteses " (" %bltotal " RBLs and " %misconfigurations " misconfigurations)" fi fi fi fi /* !not wiscnet */ /* Now, that we've got the greylisting out of the way, lets look where this mail goes */ set result check_rcpt(%orig_envrcpt) echo "$i: allowing rcpt: " %orig_envrcpt " (" $rcpt_addr ")" set rcpts %rcpts + 1 done prog eoh do add "X-Seen-By" "%__package__ %__version__ %my_name" done prog eom do if clamav("tcp://127.0.0.1:3310") echo "$i: Infected with %clamav_virus_name" reject 550 5.7.0 "Infected with %clamav_virus_name" fi done