commit e0a59ad922857036f04b141ec6b50957c3466f46 Author: Stuart Gathman Date: Tue May 31 18:04:05 2005 +0000 Initial sourceforge import. diff --git a/CREDITS b/CREDITS new file mode 100644 index 0000000..f32fdc3 --- /dev/null +++ b/CREDITS @@ -0,0 +1,29 @@ +Jim Niemira (urmane@urmane.org) wrote the original C module and some quick +and dirty python to use it. Stuart D. Gathman (stuart@bmsi.com) took that +kludge and added threading and context objects to it, wrote a proper OO +wrapper (Milter.py) that handles attachments, did lots of testing, packaged +it with distutils, and generally transformed it from a quick hack to a +real, usable Python extension. + +Other contributors: + +Terence Way + for providing a Python port of SPF +Alexander Kourakos + for plugging several memory leaks +George Graf at Vienna University of Economics and Business Administration + for handling None passed to setreply and chgheader. +Deron Meranda + for IPv6 patches +Jason Erikson + for handling NULL hostaddr in connect callback. +John Draper + for porting Python milter to OpenBSD, and starting to work on tutorials + then pointing out that it would be easier to just write the MTA in Python. +Eric S. Johansson + for helpful design discussions while working on camram +Business Management Systems - http://www.bmsi.com + for hosting the website, and providing paying clients who need milter service + so I can work on it as part of my day job. + +If I have left anybody out, send me a reminder: stuart@bmsi.com diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..8f11081 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,20 @@ +include COPYING +include TODO +include NEWS +include CREDITS +include README +include MANIFEST.in +include testsample.py +include testmime.py +include testbms.py +include testdspam.py +include bms.py +include spf.py +include test.py +include sample.py +include test/* +include *.spec +include start.sh +include milter.rc +include milter.rc7 +include milter.cfg diff --git a/Milter.py b/Milter.py new file mode 100755 index 0000000..d42f3fd --- /dev/null +++ b/Milter.py @@ -0,0 +1,202 @@ + +# Author: Stuart D. Gathman +# Copyright 2001 Business Management Systems, Inc. +# This code is under GPL. See COPYING for details. + +import os +import milter +import thread + +from milter import ACCEPT,CONTINUE,REJECT,DISCARD,TEMPFAIL, \ + set_flags, setdbg, \ + ADDHDRS, CHGBODY, ADDRCPT, DELRCPT, CHGHDRS, \ + V1_ACTS, V2_ACTS, CURR_ACTS + +try: + from milter import QUARANTINE +except: + #print 'No QUARANTINE support' + pass + +_seq_lock = thread.allocate_lock() +_seq = 0 + +def uniqueID(): + """Return a sequence number unique to this process. + """ + global _seq + _seq_lock.acquire() + seqno = _seq = _seq + 1 + _seq_lock.release() + return seqno + +class Milter: + """A simple class interface to the milter module. + """ + def _setctx(self,ctx): + self.__ctx = ctx + if ctx: + ctx.setpriv(self) + + # user replaceable callbacks + def log(self,*msg): + print 'Milter:', + for i in msg: print i, + print + + def connect(self,hostname,unused,hostaddr): + "Called for each connection to sendmail." + self.log("connect from %s at %s" % (hostname,hostaddr)) + return CONTINUE + + def hello(self,hostname): + "Called after the HELO command." + self.log("hello from %s" % hostname) + return CONTINUE + + def envfrom(self,f,*str): + """Called to begin each message. + f -> string message sender + str -> tuple additional ESMTP parameters + """ + self.log("mail from",f,str) + return CONTINUE + + def envrcpt(self,to,*str): + "Called for each message recipient." + self.log("rcpt to",to,str) + return CONTINUE + + def header(self,field,value): + "Called for each message header." + self.log("%s: %s" % (field,value)) + return CONTINUE + + def eoh(self): + "Called after all headers are processed." + self.log("eoh") + return CONTINUE + + def body(self,unused): + "Called to transfer the message body." + return CONTINUE + + def eom(self): + "Called at the end of message." + self.log("eom") + return CONTINUE + + def abort(self): + "Called if the connection is terminated abnormally." + self.log("abort") + return CONTINUE + + def close(self): + "Called at the end of connection, even if aborted." + self.log("close") + return CONTINUE + + # Milter methods which can be invoked from callbacks + def getsymval(self,sym): + return self.__ctx.getsymval(sym) + + def setreply(self,rcode,xcode,msg): + return self.__ctx.setreply(rcode,xcode,msg) + + # Milter methods which can only be called from eom callback. + def addheader(self,field,value): + return self.__ctx.addheader(field,value) + + def chgheader(self,field,idx,value): + return self.__ctx.chgheader(field,idx,value) + + def addrcpt(self,rcpt): + return self.__ctx.addrcpt(rcpt) + + def delrcpt(self,rcpt): + return self.__ctx.delrcpt(rcpt) + + def replacebody(self,body): + return self.__ctx.replacebody(body) + + def quarantine(self,reason): + return self.__ctx.quarantine(reason) + + def progress(self): + return self.__ctx.progress() + +factory = Milter + +def connectcallback(ctx,hostname,family,hostaddr): + m = factory() + m._setctx(ctx) + return m.connect(hostname,family,hostaddr) + +def closecallback(ctx): + m = ctx.getpriv() + if not m: return CONTINUE + rc = m.close() + m._setctx(None) # release milterContext + return rc + +def envcallback(c,args): + """Convert ESMTP parms to keyword parameters. + Can be used in the envfrom and/or envrcpt callbacks to process + ESMTP parameters as python keyword parameters.""" + kw = {} + for s in args[1:]: + pos = s.find('=') + if pos > 0: + kw[s[:pos]] = s[pos+1:] + return apply(c,args,kw) + +def runmilter(name,socketname,timeout = 0): + # This bit is here on the assumption that you will be starting this filter + # before sendmail. If sendmail is not running and the socket already exists, + # libmilter will throw a warning. If sendmail is running, this is still + # safe if there are no messages currently being processed. It's safer to + # shutdown sendmail, kill the filter process, restart the filter, and then + # restart sendmail. + pos = socketname.find(':') + if pos > 1: + s = socketname[:pos] + fname = socketname[pos+1:] + else: + s = "unix" + fname = socketname + if s == "unix" or s == "local": + print "Removing %s" % fname + try: + os.unlink(fname) + except: + pass + + # The default flags set include everything + # milter.set_flags(milter.ADDHDRS) + milter.set_connect_callback(connectcallback) + milter.set_helo_callback(lambda ctx, host: ctx.getpriv().hello(host)) + milter.set_envfrom_callback(lambda ctx,*str: + ctx.getpriv().envfrom(*str)) +# envcallback(ctx.getpriv().envfrom,str)) + milter.set_envrcpt_callback(lambda ctx,*str: + ctx.getpriv().envrcpt(*str)) +# envcallback(ctx.getpriv().envrcpt,str)) + milter.set_header_callback(lambda ctx,fld,val: + ctx.getpriv().header(fld,val)) + milter.set_eoh_callback(lambda ctx: ctx.getpriv().eoh()) + milter.set_body_callback(lambda ctx,chunk: ctx.getpriv().body(chunk)) + milter.set_eom_callback(lambda ctx: ctx.getpriv().eom()) + milter.set_abort_callback(lambda ctx: ctx.getpriv().abort()) + milter.set_close_callback(closecallback) + + milter.setconn(socketname) + if timeout > 0: milter.settimeout(timeout) + # The name *must* match the X line in sendmail.cf (supposedly) + milter.register(name) + start_seq = _seq + try: + milter.main() + except milter.error: + if start_seq == _seq: raise # couldn't start + # milter has been running for a while, but now it can't start new threads + raise milter.error("out of thread resources") diff --git a/NEWS b/NEWS new file mode 100644 index 0000000..2e84905 --- /dev/null +++ b/NEWS @@ -0,0 +1,129 @@ +Here is a history of user visible changes to Python milter. + +0.6.8 Defang message/rfc822 content_type with boundary + Support SPF delegation + Reject neutral SPF result for selected domains + Support SPF default (best_guess) + Don't report "spoofed" unless rcpt looks like SRS + Check for bounce with multiple rcpts + Make dspam see Received-SPF headers +0.6.7 Fix failure to remove explicit unix socket thanks to Alexander again. + Support SRS forgery detection. + Detect thread resource starvation in Milter.py. + Decode obfuscated subject headers. +0.6.6 Another memory leak plugged by Alexander Kourakos. + Support SPF checking: http://spf.pobox.com + Hello blacklist + RPM compiled for python2.3 and sendmail-8.12 +0.6.5 Plug memory leak in wrap_connect thanks to Alexander Kourakos. + Support progress notification. + Log Received header for trusted relay. + Support wildcard user for smart alias. +0.6.4 Exempt entire domains. + Tweak SMTP error codes reported. + Suppress traceback for Dspam lock timeouts. + Dspam internal mail for dspam users. + Match hostname for internal connection test, even if no ipaddr. + Fix for not saving defang of false positive triggered rejecting it + as a virus from self. + Size limit for dspam to work around dspam-2.6.5.2 bug. + (dspam-2.8 still showstopper buggy for libdspam API.) + Whitelist for dspam. + Reject list for dspam (REJECT rather than quarantine SCREENed + spam for listed domains). + Report dspam header changes to sendmail, fix headerChange + to handle deleting absent header. + dspam feature requires pydspam-1.1.5 +0.6.3 dspam screening (with pydspam-1.1.4) + Don't write "defang" file for false positive feedback +0.6.2 Work around email package bug in get_filename(). + add dspam_exempt list to milter.cfg + REJECT messages with missing MIME boundaries (almost always spam) + DISCARD messages which any dspam user flags as spam + start.sh was calling python instead of python2 on Linux +0.6.1 Work with python-2.2.3 + Integrate full dspam application +0.6.0 Use email package in python-2.2.2 +0.5.6 Include dspam interface for Bayesian filtering +0.5.5 Allow passing None to setreply and chgheader thanks to George Graf. + Experimental IPv6 support thanks to Deron Meranda. + Allow removing callbacks by passing None to set_XXX_callback. + Recognize internal connections in bms.py. + Give users a clue when rejecting banned subjects. +0.5.4 Wiretap redirection feature, smart alias feature, QUARANTINE support +0.5.3 Tweak to run under 2.2 in production +0.5.2 Fix and add to unit test another parsing failure. +0.5.1 Properly handle modifications to rfc822 attachments. + Handle encoded rfc822 attachments. +0.5.0 Use config file so users don't have to keep syncing the + bms.py script. Keep bms.py marked as %config for a while + to avoid wiping out their customizations just yet. +0.4.5 Work with sgmlop package to speed up HTML parsing. + Reduce various local hacks to config variables. +0.4.4 Bug fixes for HTML encoding. +0.4.3 Handle quoted-printable HTML attachments. Remove entire + attachment when HTML can't be parsed. +0.4.2 Parse HTML attachments to remove . + Klez virus uses malformed MIME part separators to prevent + the multifile module and other virus scanners from seeing its + HTML attachment (which contains Javascript and VBScript). Outhouse + happily accepts and executes the malformed attachments, but + we still kill the Klez virus because we: + Defang attachment when any Content-Type attribute ends with + a banned extension - one of the Outhouse bugs exploited by the + Klez virus. Outhouse really, really stinks . . . +0.4.1 Bug fix from Jason Erikson for NULL hostaddr in connect callback. +0.4.0 New check_attachments(msg,check) function in mime module allows + filtering based on attachment contents. Distribution now includes + bms.py, an example milter used in production - including use of the + new check_attachments(msg,check) API. + Report hostname in WARNING.TXT. + More parameter list bug fixes. + +0.3.10 Parse quotes in parameter lists to handle embedded ';'. + Move test data to subdirectory, write non-junit output to + log file in test subdirectory. +0.3.9 Handle non-multipart messages with executable content in sample.py, + add more extensions to banned list. +0.3.8 Handle malformed Content-Type in mime.py. Test viruses have + been deactivated by deleting most of the viral code. +0.3.7 Put back hint on running sample.py. Add .bat as banned extension. + More sample spam filtering logic. +0.3.6 Ran through pychecker-0.8.5. Most systems will name the sendmail + user library (used by the milter extension module) 'libsm', but AIX + still needs to call it 'libsmutil' because there is a system library + called 'libsm'. +0.3.5 Enhanced logging. Fix bug in sample milter where headers were + included in body when removing a virus. +0.3.4 Tested distribution on RH6.2 and updated sample.py and docs. + Tested with gcc-2.95.2, python-2.1.1, sendmail-8.11.6-2.6.x + The RH6.2 spec file to enable libmilter for sendmail-8.11.6 + can be obtained from http://www.bmsi.com/linux/sendmail-rhmilter.spec + The SRPM can be obtained from http://www.redhat.com + +0.3.3 Remove reference to sa_len - not supported by linux. + +0.3.2 Rename and add more hints to the sample milter. + +0.3.1 Pass a more useful hostaddr to the connect callback. + +0.3 Interface now uses a milterContext extension object instead of +an index. A PyThreadContext is now created for each milterContext so that +"simultaneously" processing multiple messages at once (as often happens +on a busy server) actually works. + + Many milter methods are now object methods of the milterContext +extension object. No compatibility API is provided for this change due +to the limited user base at this stage. The setname method has been removed, +and the name is now passed to register. + + A simple class to provide an OO wrapper to the milter API is +provided. + + A simple class to parse multipart mime messages into parts and replace +selected parts is provided. The sample filter will eventually use the mimelib +package instead, but mimelib currently requires reading the entire message +into memory. + + A sample filter that replaces attachments with naughty extensions +with a warning message is provided. diff --git a/README b/README new file mode 100644 index 0000000..8f564e8 --- /dev/null +++ b/README @@ -0,0 +1,203 @@ +Abstract +-------- + +This is a python extension module to enable python scripts to attach to +Sendmail's libmilter API, enabling filtering of messages as they arrive. +Since it's a script, you can do anything you want to the message - screen +out viruses, collect statistics, add or modify headers, etc. You can, at +any point, tell Sendmail to reject, discard, or accept the message. + + +Requirements +------------ + +This python milter extension: http://www.bmsi.com/python/milter.html +Python: http://www.python.org +Sendmail: http://www.sendmail.org +NB: From Sendmail's libmilter/README: + +libmilter requires pthread support in the operating system. Moreover, it +requires that the library functions it uses are thread safe; which is true +for the operating systems libmilter has been developed and tested on. On +some operating systems this requires special compile time options (e.g., +not just -pthread). libmilter is currently known to work on (modulo +problems in the pthread support of some specific versions): + +FreeBSD 3.x, 4.x +SunOS 5.x (x >= 5) +AIX 4.3.x +HP UX 11.x +Linux (recent versions/distributions) +OpenBSD +AIX 4.1.5 + +libmilter is currently not supported on: + +IRIX 6.x +Ultrix + +Quick Installation +------------------ + +1. Build and install Sendmail, enabling libmilter (see libmilter/README). +2. Build and install Python, enabling threading. +3. Install this module: python setup.py --help +4. Add these two lines to sendmail.cf: + +O InputMailFilters=pythonfilter +Xpythonfilter, S=local:/home/username/pythonsock + +5. Run the sample.py example milter with: python sample.py +Note that milters should almost certainly not run as root. + +That's it. Incoming mail will cause the milter to print some things, and +some email will be rejected (see the "header" method). Edit and play. See +bms.py for an example milter used in production. + + +Not-so-quick Installation +------------------------- + +First install Sendmail. Make sure you read libmilter/README in the Sendmail +source directory, and make sure you enable libmilter before you build. The +8.11 series had libmilter marked as FFR (For Future Release); 8.12 +officially +supports libmilter, but it's still not built by default. + +Install Python, and enable threading in Modules/Setup. + +Install this miltermodule package; DistUtils Automatic Installation: + +$ python setup.py --help + +For versions of python prior to 2.0, you will need to download distutils +separately or build manually. You will need to download unittest +separately to run the test programs. The bdist_rpm distutils option seems +not to work for python 2.0; upgrade to at least 2.1.1. + +Now that everything is installed, we need to tell sendmail that we're going +to filter incoming email. Add lines similar to the following to +sendmail.cf: + +O InputMailFilters=pythonfilter +Xpythonfilter, S=local:/home/username/pythonsock + +The "O" line tells sendmail which filters to use in what order; here we're +telling sendmail to use the filter named "pythonfilter". + +The next line, the "X" line (for "eXternal"), lists that filter along with +some options associated with it. In this case, we have the "S" option, which +names the socket that sendmail will use to communicate with this particular +milter. This milter's socket is a unix-domain socket in the filesystem. +See libmilter/README for the definitive list of options. +NB: The name is specified in two places: here, in sendmail's cf file, and +in the milter itself. Make sure the two match. +NB: OpenBSD must use an inet socket. See the web page for details. +NB: The above lines can be added in your .mc file with this line: + +INPUT_MAIL_FILTER(`pythonfilter', `S=local:/home/username/pythonsock') + +For versions of sendmail prior to 8.12, you will need to enable +_FFR_MILTER for the cf macros. For example, + +m4 -D_FFR_MILTER ../m4/cf.m4 myconfig.mc > myconfig.cf + + +RedHat 6.2 Notes +---------------- + +The Redhat 6.2 sendmail RPM does not enable milter. You can obtain a +modified spec file at + +http://www.bmsi.com/linux/rh62/sendmail-rhmilter.spec + +use it to rebuild the Redhat 7.2 SRPM. The RH6.2 SRPM does not have +recent sendmail security patches. + +RedHat 7.2 Notes +---------------- + +The Redhat 7.2 sendmail RPM enables milter in sendmail - but does not include +the headers needed for compiling a milter. You can obtain a modified spec +file with a sendmail-devel package that includes the needed static libraries +and headers at + +http://www.bmsi.com/linux/sendmail-rh72.spec + +OpenBSD Notes +------------- + +Sendmail is broken on OpenBSD for unix domain sockets. You must use an +inet socket for milter. The sendmail.cf 'X' config line would look like: + +Xpythonfilter, S=inet:1234@localhost + +and the sample milter needs to be modified accordingly. + +IPv6 Notes +---------- + +IPv6 is still experimental. + +The IPv6 protocol is supported if your operation system supports it +and if sendmail was compiled with IPv6 support. To determine if your +sendmail supports IPv6, run "sendmail -d0" and check for the NETINET6 +compilation option. To compile sendmail with IPv6 support, add this +declaration to your site.config.m4 before building it: + +APPENDDEF(`confENVDEF', `-DNETINET6=1') + +IPv6 support can show up in two places; the communications socket +between the milter and sendmail processes and in the host address +argument to the connect() callback method. + +For sendmail to be able to accept IPv6 SMTP sessions, you must +configure the daemon to listen on an IPv6 port. Furthermore if you +want to allow both IPv4 and IPv6 connections, some operating systems +will require that each listens to different port numbers. For an +IPv6-only setup, your sendmail configuration should contain a line +similar to (first line is for sendmail.mc, second is sendmail.cf): + +DAEMON_OPTIONS(`Name=MTA-v6, Family=inet6, Modify=C, Port=25') +O DaemonPortOptions=Name=MTA-v6, Family=inet6, Modify=C, Port=25 + +To allow sendmail and the milter process to communicate with each +other over IPv6, you may use the "inet6" socket name prefix, as in: + +Xpythonfilter, S=inet6:1234@fec0:0:0:7::5c + +The connect() callback method in the milter class will pass the +IPv6-specific information in the 'hostaddr' argument as a tuple. Note +that the type of this value is dependent upon the protocol family, and +is not compatible with IPv4 connections. Therefore you should always +check the family argument before attempting to use the hostaddr +argument. A quick example showing this follows: + + import socket + ... + class ipv6awareMilter(Milter.Milter): + ... + def connect(self,hostname,family,hostaddr): + if family==socket.AF_INET: + ipaddress, port = hostaddr + elif family==socket.AF_INET6: + ip6address, port, flowinfo, scopeid = hostaddr + elif family==socket.AF_UNIX: + socketpath = hostaddr + +The hostname argument is always safe to use without interpreting the +protocol family. For IPv6 connections for which the hostname can not +be determined the hostname will appear similar to the string +"[IPv6:::1]" with the corresponding hostaddr[0] being "::1". Refer to +RFC 2553 for information on interpreting and using the flowinfo and +scopeid socket attributes, both of which are integers. + +Authors +------- + +Jim Niemira (urmane@urmane.org) wrote the original C module and some quick +and dirty python to use it. Stuart D. Gathman (stuart@bmsi.com) took that +kludge and added threading and context objects to it, wrote a proper OO +wrapper (Milter.py) that handles attachments, did lots of testing, packaged +it with distutils, and generally transformed it from a quick hack to a +real, usable Python extension. diff --git a/TODO b/TODO new file mode 100644 index 0000000..b5d9699 --- /dev/null +++ b/TODO @@ -0,0 +1,42 @@ +Implement RRS - a backdoor for non-SRS forwarders. User lists non-SRS +forwarder accounts, and a util provides a special local alias for the +user to give to the forwarder. Alias only works for mail from that +forwarder. Milter gets forwarder domain from alias and uses it to +SPF check forwarder. + +adapt init script to work on RH9 +Skip dspam when SPF pass? +Report 551 with rcpt on SPF fail? + +Another special dspam user, 'honeypot', can be listed in innoculations. +All email to those addresses is treated as known spam. + +Framework for modular Python milter components within a single VM. +Python milters can be already be composed through sendmail by running each in +a separate process. However, a significant amount of memory is wasted +for each additional Python VM, and communication between milters +is cumbersome (e.g., adding mail headers, writing external files). + +Backup copies for outgoing/incoming mail. + +Allow multiple wiretap groups, each with its own destination. Perhaps +also copy incoming wiretap mail, even though sendmail alias works perfectly +for the purpose, to avoid having to change two configs for a wiretap. + +Provide a way to reload milter.cfg without stopping/restarting milter. + +Allow selected Windows extensions for specific domains via milter.cfg + +Fix setup.py so that _FFR_QUARANTINE is automatically defined when +available in libmilter. + +Keep separate ismodified flag for headers and body. This is important +when rejecting outgoing mail with viruses removed (so as not to +embarrass yourself), and also removing Received headers with hidepath. + +Wrap smfi_setbacklog(int) - but it is only available in sendmail >= 8.12.3, + so how can we detect whether to wrap it? + +Need a test module to feed sample messages to a milter though a live +sendmail and SMTP. The mockup currently used is probably not very accurate, +and doesn't test the threading code. diff --git a/bms.py b/bms.py new file mode 100644 index 0000000..f6e7135 --- /dev/null +++ b/bms.py @@ -0,0 +1,1065 @@ +#!/usr/bin/env python +# A simple milter. +# $Log$ +# Revision 1.96 2004/04/06 03:27:03 stuart +# bugs from Redhat 9 testing +# +# Revision 1.95 2004/04/05 22:37:08 stuart +# Include Received-SPF headers in dspam. +# +# Revision 1.94 2004/04/05 22:16:50 stuart +# Separate check_header method taking decoded header. +# Reject multiple recipients for a bounce. +# +# Revision 1.93 2004/04/01 20:57:45 stuart +# Report only SRS like addresses as spoofed. +# Return TEMPFAIL on SPF error. +# +# Revision 1.92 2004/03/25 17:45:53 stuart +# Make spf_reject_neutral global in bms.py +# +# Revision 1.91 2004/03/25 03:38:02 stuart +# Reject neutral SPF result for selected domains. +# +# Revision 1.90 2004/03/25 03:27:33 stuart +# Support delegation of SPF records. +# +# Revision 1.89 2004/03/23 22:02:49 stuart +# Header decoding bug. +# +# Revision 1.88 2004/03/23 05:08:45 stuart +# Decode headers, indirect srs config. +# +# Revision 1.87 2004/03/18 02:21:16 stuart +# SRS checking +# +# Revision 1.86 2004/03/11 05:00:37 stuart +# Don't wipe out fail messages from SPF records. +# Hello blacklist +# +# Revision 1.85 2004/03/10 01:49:22 stuart +# Enhanced SPF support. +# +# Revision 1.84 2004/03/09 17:04:49 stuart +# Received-SPF header. +# +# Revision 1.83 2004/03/08 20:23:26 stuart +# SPF support +# +# Revision 1.82 2004/03/01 18:56:50 stuart +# Support progress reporting. +# +# Revision 1.81 2004/03/01 18:36:09 stuart +# Trusted relay. +# +# Revision 1.80 2004/01/12 21:10:58 stuart +# Support wildcard user for smart_alias +# +# Revision 1.79 2003/12/04 23:46:06 stuart +# Release 0.6.4 +# +# Revision 1.78 2003/12/04 23:20:24 stuart +# Make headerChange handle deleting absent header +# +# Revision 1.77 2003/12/04 22:01:40 stuart +# Limit size of messages which will be dspammed. This works around a bug +# in dspam-2.6.5.2 where it scans large binary attachments. I've never +# seen really big spam anyway. +# +# Revision 1.76 2003/12/04 21:44:33 stuart +# Pass header changes from Dspam to sendmail +# +# Revision 1.75 2003/11/25 17:43:07 stuart +# Update FAQ. +# +# Revision 1.74 2003/11/25 17:36:58 stuart +# dspam_reject +# +# Revision 1.73 2003/11/24 15:46:00 stuart +# Missing global for dspam_whitelist +# +# Revision 1.72 2003/11/22 02:52:07 stuart +# Handle multiple x-dspam-recipients properly on false positive +# +# Revision 1.71 2003/11/22 02:49:57 stuart +# dspam whitelist +# +# Revision 1.70 2003/11/09 03:53:34 stuart +# Don't block delivery of defanged false positives. +# +# Revision 1.69 2003/11/08 22:47:04 stuart +# Exempt entire domains with '@domain.com' +# +# Revision 1.68 2003/11/02 03:06:16 stuart +# Adjust error codes again. +# +# Revision 1.67 2003/11/02 03:01:46 stuart +# Adjust SMTP error codes after careful reading of standard. +# +# Revision 1.66 2003/11/02 01:56:43 stuart +# Use busy SMTP code. +# +# Revision 1.65 2003/11/02 01:44:11 stuart +# Suppress traceback for Dspam lock timeouts +# +# Revision 1.64 2003/10/28 01:00:19 stuart +# Dspam internal mail for dspam users +# +# Revision 1.63 2003/10/25 02:10:34 stuart +# Match hostname for internal connection test, even if no ipaddr. +# +# Revision 1.62 2003/10/24 04:34:52 stuart +# Fix for not saving defang of false positive triggered rejecting it +# as a virus from self. +# +# Revision 1.61 2003/10/22 22:03:14 stuart +# Apply dspam_exempt to screening +# +# Revision 1.60 2003/10/22 21:58:42 stuart +# Don't save false positives as defang file. +# +# Revision 1.59 2003/10/22 05:02:27 stuart +# Add support for dspam screeners +# +# Revision 1.58 2003/10/16 22:19:24 stuart +# Redirect Dspam logging to bms milter +# +# Revision 1.57 2003/10/10 00:15:04 stuart +# DISCARD message if quarrantined for any recipient. +# +# Revision 1.56 2003/10/06 19:30:27 stuart +# REJECT messages with boundard errors +# +# Revision 1.55 2003/10/03 18:20:31 stuart +# Opt-out feature to exempt certain recipients from header filtering. +# +# Revision 1.54 2003/09/22 13:36:04 stuart +# Release 0.6.1 +# +# Revision 1.53 2003/09/06 07:08:36 stuart +# dspam support improvements. +# +# Revision 1.51 2003/09/02 00:27:27 stuart +# Should have full milter based dspam support working +# +# Revision 1.50 2003/08/26 06:08:17 stuart +# Use new python boolean since we now require 2.2.2 +# +# Revision 1.49 2003/08/26 05:45:51 stuart +# Fix conditional import of dspam. Update web page. +# +# Revision 1.48 2003/08/26 05:10:43 stuart +# Readability tweaks +# +# Revision 1.47 2003/08/26 05:01:38 stuart +# Release 0.6.0 +# +# Revision 1.46 2003/08/26 04:45:16 stuart +# Modest dspam control +# +# Revision 1.43 2003/06/25 17:00:02 stuart +# fix hostaddr test +# +# Revision 1.42 2003/06/25 16:45:59 stuart +# Not using checking hostaddr properly +# +# Revision 1.41 2003/06/25 15:57:54 stuart +# Ready for 5.5 release. +# +# Revision 1.40 2003/06/25 15:41:41 stuart +# recognize internal connections. +# Give legitimate users a clue about banned subject keywords. +# +# Revision 1.39 2002/12/14 00:36:59 stuart +# Smart alias feature +# +# Revision 1.38 2002/11/14 17:52:53 stuart +# Redirection feature for wiretap +# +# Revision 1.37 2002/11/07 23:52:09 stuart +# config fixes +# +# Revision 1.36 2002/10/04 05:27:38 stuart +# Add get_submsg to allow modifying rfc822 attachment. +# +# Revision 1.35 2002/10/03 01:31:18 stuart +# Test encoded rfc822 attachment +# +# Revision 1.34 2002/10/03 00:55:42 stuart +# Decode rfc822 attachments +# +# Revision 1.33 2002/10/02 18:49:02 stuart +# Save and log messages which cause an exception while parsing attachments. +# +# Revision 1.32 2002/09/24 01:38:05 stuart +# Doc updates. +# +# Revision 1.31 2002/09/13 22:14:06 stuart +# Release 0.5.0 wrapup +# +# Revision 1.30 2002/09/13 20:22:37 stuart +# Additional config items +# +# Revision 1.29 2002/08/20 04:40:46 stuart +# Use config file +# +# Revision 1.28 2002/07/12 19:40:38 stuart +# Update docs, minor bugs. +# +# Revision 1.27 2002/06/16 02:06:24 stuart +# SPAM tweaks +# +# Revision 1.26 2002/06/07 22:07:30 stuart +# Isolate local hacks to configuration data. +# +# Revision 1.25 2002/05/02 20:41:00 stuart +# Top level virus needs top level header change. +# +# Revision 1.24 2002/05/02 20:31:43 stuart +# Handle quoted-printable HTML attachments. +# Remove entire attachment when HTML can't be parsed by sgmllib. +# +# Revision 1.23 2002/05/02 03:42:31 stuart +# base64 no longer needed +# +# Revision 1.22 2002/05/02 03:12:39 stuart +# Move check_html to mime module. +# +# Revision 1.21 2002/05/02 02:48:22 stuart +# Remove scripts from HTML even with base64 encoding. +# +# Revision 1.20 2002/05/02 00:21:01 stuart +# Test filtering HTML attachments. +# +# Revision 1.19 2002/05/01 22:12:41 stuart +# Remove scripts from HTML attachments. +# +# Revision 1.18 2002/03/01 20:29:00 stuart +# Ready for release. +# + +# Author: Stuart D. Gathman +# Copyright 2001 Business Management Systems, Inc. +# This code is under GPL. See COPYING for details. + +import sys +import os +import StringIO +import rfc822 +import mime +import email.Errors +import Milter +import tempfile +import ConfigParser +import time +from fnmatch import fnmatchcase +from email.Header import decode_header + +# Import pysrs if available +try: + import SRS + import re + srsre = re.compile(r'^SRS[01][+-=]',re.IGNORECASE) +except: SRS = None +try: import spf +except: spf = None +#import syslog +#syslog.openlog('milter') + +# Thanks to Chris Liechti for config parsing suggestions + +# Global configuration defaults suitable for test framework. +socketname = "/tmp/pythonsock" +reject_virus_from = () +wiretap_users = {} +discard_users = {} +wiretap_dest = None +blind_wiretap = True +check_user = {} +block_forward = {} +hide_path = () +log_headers = False +block_chinese = False +spam_words = () +porn_words = () +scan_html = True +scan_rfc822 = True +internal_connect = () +trusted_relay = () +internal_domains = () +hello_blacklist = () +smart_alias = {} +dspam_dict = None +dspam_users = {} +dspam_userdir = None +dspam_exempt = {} +dspam_whitelist = {} +dspam_screener = None +dspam_internal = True # True if internal mail should be dspammed +dspam_reject = () +dspam_sizelimit = 80000 +srs = None +srs_reject_spoofed = False +spf_reject_neutral = () + +class MilterConfigParser(ConfigParser.ConfigParser): + + def getlist(self,sect,opt): + if self.has_option(sect,opt): + return [q.strip() for q in self.get(sect,opt).split(',')] + return () + + def getaddrset(self,sect,opt): + if not self.has_option(sect,opt): + return {} + s = self.get(sect,opt) + d = {} + for q in s.split(','): + q = q.strip() + if q.startswith('file:'): + domain = q[5:] + d[domain] = d.setdefault(domain,[]) + open(domain,'r').read().split() + else: + user,domain = q.split('@') + d.setdefault(domain,[]).append(user) + return d + + def getaddrdict(self,sect,opt): + if not self.has_option(sect,opt): + return {} + d = {} + for q in self.get(sect,opt).split(','): + q = q.strip() + if self.has_option(sect,q): + l = self.get(sect,q) + for addr in l.split(','): + addr = addr.strip() + if addr.startswith('file:'): + fname = addr[5:] + for a in open(fname,'r').read().split(): + d[a] = q + else: + d[addr] = q + return d + + def getdefault(self,sect,opt,default=None): + if self.has_option(sect,opt): + return self.get(sect,opt) + return default + +def read_config(list): + cp = MilterConfigParser({ + 'tempdir': "/var/log/milter/save", + 'socket': "/var/log/milter/pythonsock", + 'scan_html': 'no', + 'scan_rfc822': 'yes', + 'block_chinese': 'no', + 'log_headers': 'no', + 'blind_wiretap': 'yes', + 'maxage': '8', + 'hashlength': '8', + 'reject_spoofed': 'no' + }) + cp.read(list) + tempfile.tempdir = cp.get('milter','tempdir') + global socketname, scan_rfc822, scan_html, block_chinese + socketname = cp.get('milter','socket') + scan_rfc822 = cp.getboolean('milter','scan_rfc822') + scan_html = cp.getboolean('milter','scan_html') + block_chinese = cp.getboolean('milter','block_chinese') + + global hide_path, block_forward, log_headers + hide_path = cp.getlist('scrub','hide_path') + block_forward = cp.getaddrset('milter','block_forward') + log_headers = cp.getboolean('milter','log_headers') + + global blind_wiretap, wiretap_users, wiretap_dest, discard_users + blind_wiretap = cp.getboolean('wiretap','blind') + wiretap_users = cp.getaddrset('wiretap','users') + discard_users = cp.getaddrset('wiretap','discard') + wiretap_dest = cp.getdefault('wiretap','dest') + if wiretap_dest: wiretap_dest = '<%s>' % wiretap_dest + + global check_user, reject_virus_from, internal_connect, internal_domains + check_user = cp.getaddrset('milter','check_user') + reject_virus_from = cp.getlist('scrub','reject_virus_from') + internal_connect = cp.getlist('milter','internal_connect') + internal_domains = cp.getlist('milter','internal_domains') + + global porn_words, spam_words, smart_alias, trusted_relay, hello_blacklist + trusted_relay = cp.getlist('milter','trusted_relay') + porn_words = cp.getlist('milter','porn_words') + spam_words = cp.getlist('milter','spam_words') + hello_blacklist = cp.getlist('milter','hello_blacklist') + for sa in cp.getlist('wiretap','smart_alias'): + sm = cp.getlist('wiretap',sa) + if len(sm) < 2: + print 'malformed smart alias:',sa + continue + if len(sm) == 2: sm.append(sa) + key = (sm[0],sm[1]) + smart_alias[key] = sm[2:] + + global dspam_dict, dspam_users, dspam_userdir, dspam_exempt + global dspam_screener,dspam_whitelist,dspam_reject,dspam_sizelimit + global spf_reject_neutral,SRS + dspam_dict = cp.getdefault('dspam','dspam_dict') + dspam_exempt = cp.getaddrset('dspam','dspam_exempt') + dspam_whitelist = cp.getaddrset('dspam','dspam_whitelist') + dspam_users = cp.getaddrdict('dspam','dspam_users') + dspam_userdir = cp.getdefault('dspam','dspam_userdir') + dspam_screener = cp.getdefault('dspam','dspam_screener') + dspam_reject = cp.getlist('dspam','dspam_reject') + if cp.has_option('dspam','dspam_sizelimit'): + dspam_sizelimit = cp.getint('dspam','dspam_sizelimit') + + if spf: + spf.DELEGATE = cp.getdefault('spf','delegate') + spf_reject_neutral = cp.getlist('spf','reject_neutral') + srs_config = cp.getdefault('srs','config') + if srs_config: cp.read([srs_config]) + srs_secret = cp.getdefault('srs','secret') + if SRS and srs_secret: + global srs,srs_reject_spoofed + database = cp.getdefault('srs','database') + srs_reject_spoofed = cp.getboolean('srs','reject_spoofed') + maxage = cp.getint('srs','maxage') + hashlength = cp.getint('srs','hashlength') + separator = cp.getdefault('srs','separator','=') + if database: + import SRS.DB + srs = SRS.DB.DB(database=database,secret=srs_secret, + maxage=maxage,hashlength=hashlength,separator=separator) + else: + srs = SRS.Guarded.Guarded(secret=srs_secret, + maxage=maxage,hashlength=hashlength,separator=separator) + + +def parse_addr(t): + if t.startswith('<') and t.endswith('>'): t = t[1:-1] + return t.split('@') + +def parse_header(val): + h = decode_header(val) + if not len(h) or (not h[0][1] and len(h) == 1): return val + try: + u = [] + for s,enc in h: + if enc: + u.append(unicode(s,enc)) + else: + u.append(unicode(s)) + u = ''.join(u) + for enc in ('us-ascii','iso-8859-1','utf8'): + try: + return u.encode(enc) + except UnicodeError: continue + except LookupError: + return val + +class bmsMilter(Milter.Milter): + "Milter to replace attachments poisonous to Windows with a WARNING message." + + def log(self,*msg): + print "%s [%d]" % (time.strftime('%Y%b%d %H:%M:%S'),self.id), + for i in msg: print i, + print + + def __init__(self): + self.tempname = None + self.mailfrom = None # sender in SMTP form + self.canon_from = None # sender in end user form + self.fp = None + self.bodysize = 0 + self.id = Milter.uniqueID() + + # delrcpt can only be called from eom(). This accumulates recipient + # changes which can then be applied by alter_recipients() + def del_recipient(self,rcpt): + rcpt = rcpt.lower() + if not rcpt in self.discard_list: + self.discard_list.append(rcpt) + + # addrcpt can only be called from eom(). This accumulates recipient + # changes which can then be applied by alter_recipients() + def add_recipient(self,rcpt): + rcpt = rcpt.lower() + if not rcpt in self.redirect_list: + self.redirect_list.append(rcpt) + + # addheader can only be called from eom(). This accumulates added headers + # which can then be applied by alter_headers() + def add_header(self,name,val): + self.new_headers.append((name,val)) + self.log('%s: %s' % (name,val)) + + def connect(self,hostname,unused,hostaddr): + self.internal_connection = False + self.trusted_relay = False + self.receiver = self.getsymval('j') + if hostaddr and len(hostaddr) > 0: + ipaddr = hostaddr[0] + for pat in internal_connect: + if fnmatchcase(ipaddr,pat): + self.internal_connection = True + break + for pat in trusted_relay: + if fnmatchcase(ipaddr,pat): + self.trusted_relay = True + break + self.connectip = ipaddr + else: + self.connectip = None + for pat in internal_connect: + if fnmatchcase(hostname,pat): + self.internal_connection = True + break + if self.internal_connection: + connecttype = 'INTERNAL' + else: + connecttype = 'EXTERNAL' + if self.trusted_relay: + connecttype += ' TRUSTED' + self.log("connect from %s at %s %s" % (hostname,hostaddr,connecttype)) + return Milter.CONTINUE + + def hello(self,hostname): + self.hello_name = hostname + self.log("hello from %s" % hostname) + if not self.internal_connection and hostname in hello_blacklist: + self.log("REJECT: spam from self:",hostname) + self.setreply('550','5.7.1','I hate talking to myself.') + return Milter.REJECT + return Milter.CONTINUE + + # multiple messages can be received on a single connection + # envfrom (MAIL FROM in the SMTP protocol) seems to mark the start + # of each message. + def envfrom(self,f,*str): + self.log("mail from",f,str) + self.fp = StringIO.StringIO() + self.tempname = None + self.mailfrom = f + self.forward = True + self.bodysize = 0 + self.hidepath = False + self.discard = False + self.dspam = True + self.reject_spam = True + self.data_allowed = True + self.trust_received = self.trusted_relay + self.redirect_list = [] + self.discard_list = [] + self.new_headers = [] + self.recipients = [] + t = parse_addr(f.lower()) + self.canon_from = '@'.join(t) + self.fp.write('From %s %s\n' % (self.canon_from,time.ctime())) + if len(t) == 2: + user,domain = t + if not self.internal_connection: + for pat in internal_domains: + if fnmatchcase(domain,pat): + self.log("REJECT: spam from self",pat) + self.setreply('550','5.7.1','I hate talking to myself.') + return Milter.REJECT + self.rejectvirus = domain in reject_virus_from + if user in wiretap_users.get(domain,()): + self.add_recipient(wiretap_dest) + if user in discard_users.get(domain,()): + self.discard = True + exempt_users = dspam_whitelist.get(domain,()) + if user in exempt_users or '' in exempt_users: + self.dspam = False + else: + self.rejectvirus = False + if not (self.internal_connection or self.trusted_relay) \ + and self.connectip and spf: + return self.check_spf() + return Milter.CONTINUE + + def check_spf(self): + user,host = spf.split_email(self.canon_from,self.hello_name) + self.sender = '@'.join((user,host)) + res,code,txt = spf.check(self.connectip,self.canon_from,self.hello_name) + if res in ('deny', 'fail'): + self.log('REJECT: SPF %s %i %s' % (res,code,txt)) + # improve default explanation, but don't wipe out text from SPF record + if txt == 'access denied': + txt = 'SPF fail: see http://spf.pobox.com/why.html' + self.setreply(str(code),'5.7.1',txt) + return Milter.REJECT + if res == 'pass': +# Received-SPF: pass (mybox.example.org: domain of +# myname@example.com designates 192.0.2.1 as +# permitted sender); +# receiver=mybox.example.org; +# client_ip=192.0.2.1; +# envelope-from=myname@example.com; + self.add_header('Received-SPF',"""pass (%(receiver)s: domain of + %(sender)s designates %(connectip)s as permitted sender); + receiver=%(receiver)s; client_ip=%(connectip)s; + envelope-from=%(canon_from)s;""" % self.__dict__) + elif res == 'none' or res == 'unknown' and txt == 'no SPF record': +# Received-SPF: none (mybox.example.org: myname@example.com does +# not designated permitted sender hosts) + self.add_header('Received-SPF',"""none (%(receiver)s: %(sender)s does + not designate permitted sender hosts)""" % self.__dict__) + elif res == 'softfail': +# Received-SPF: softfail (mybox.example.org: domain of transitioning +# myname@example.com does not designate +# 192.0.2.1 as permitted sender) + self.add_header('Received-SPF', + """softfail (%(receiver)s: domain of transitioning + %(sender)s does not designate + %(connectip)s as permitted sender)""" % self.__dict__) + elif res == 'neutral': + if host in spf_reject_neutral: + self.log('REJECT: SPF neutral for',self.sender) + self.setreply('550','5.7.1', + 'mail from %s must pass SPF: http://spf.pobox.com/why.html' % host + ) + return Milter.REJECT +# Received-SPF: neutral (mybox.example.org: 192.0.2.1 is neither +# permitted nor denied by domain of +# myname@example.com) + self.add_header('Received-SPF', + """neutral (%(receiver)s: %(connectip)s is neither + permitted nor denied by domain of %(sender)s)""" % self.__dict__) + elif res == 'unknown': +# Received-SPF: unknown -extension:foo (mybox.example.org: domain +# of myname@example.com uses mechanism +# not recognized by this client) + self.spf_mech = txt + self.add_header('Received-SPF', + """unknown %(spf_mech)s (%(receiver)s: domain + of %(sender)s uses mechanism not recognized by this client)""" + % self.__dict__) + elif res == 'error': +# Received-SPF: error (mybox.example.org: error in processing +# during lookup of myname@example.com: DNS +# timeout) + self.add_header('Received-SPF', + """error (%s: error in processing + during lookup of %s: %s)""" % (self.receiver,self.sender,txt)) + self.setreply(str(code),'4.3.0',txt) + return Milter.TEMPFAIL + else: + self.log('SPF: %s %i %s' % (res,code,txt)) + return Milter.CONTINUE + + # hide_path causes a copy of the message to be saved - until we + # track header mods separately from body mods - so use only + # in emergencies. + def envrcpt(self,to,*str): + # mail to MAILER-DAEMON is generally spam that bounced + if to.startswith('': + if self.recipients: + self.log('REJECT: Multiple bounce recipients') + self.setreply('550','5.7.1','Multiple bounce recipients') + return Milter.REJECT + if srs and not (self.internal_connection or self.trusted_relay): + oldaddr = '@'.join(parse_addr(to)) + try: + newaddr = srs.reverse(oldaddr) + self.log("srs rcpt:",newaddr) + except: + if srsre.match(oldaddr): + self.log("srs spoofed:",oldaddr) + self.data_allowed = not srs_reject_spoofed + self.recipients.append('@'.join(t)) + user,domain = t + users = check_user.get(domain) + if self.discard: + self.del_recipient(to) + if users and not user in users: + self.log('REJECT: RCPT TO:',to,str) + return Milter.REJECT + if user in block_forward.get(domain,()): + self.forward = False + exempt_users = dspam_exempt.get(domain,()) + if user in exempt_users or '' in exempt_users: + self.dspam = False + if domain in hide_path: + self.hidepath = True + if not domain in dspam_reject: + self.reject_spam = False + if smart_alias: + cf = self.canon_from + cf0 = cf.split('@',1) + if len(cf0) == 2: + cf0 = '@' + cf0[1] + else: + cf0 = cf + ct = '@'.join(t) + for key in ((cf,ct),(cf0,ct)): + if smart_alias.has_key(key): + self.del_recipient(to) + for t in smart_alias[key]: + self.add_recipient('<%s>'%t) + #rcpt = self.getsymval("{rcpt_addr}") + #self.log("rcpt-addr",rcpt); + return Milter.CONTINUE + + # Heuristic checks for spam headers + def check_header(self,lname,val): + # val is decoded header value + if lname == 'subject': + + # check for common spam keywords + for wrd in spam_words: + if val.find(wrd) >= 0: + self.log('REJECT: %s: %s' % (name,val)) + self.setreply('550','5.7.1','That subject is not allowed') + return Milter.REJECT + + # check for spam that claims to be legal + lval = val.lower().strip() + for adv in ("adv:","adv.","adv ","[adv]","(adv)","advt:","advert:"): + if lval.startswith(adv): + self.log('REJECT: %s: %s' % (name,val)) + self.setreply('550','5.7.1','Advertising not accepted here') + return Milter.REJECT + for adv in ("adv","(adv)","[adv]"): + if lval.endswith(adv): + self.log('REJECT: %s: %s' % (name,val)) + self.setreply('550','5.7.1','Advertising not accepted here') + return Milter.REJECT + + # check for porn keywords + for w in porn_words: + if lval.find(w) >= 0: + self.log('REJECT: %s: %s' % (name,val)) + self.setreply('550','5.7.1','That subject is not allowed') + return Milter.REJECT + + # check for annoying forwarders + if not self.forward: + if lval.startswith("fwd:") or lval.startswith("[fw"): + self.log('REJECT: %s: %s' % (name,val)) + return Milter.REJECT + + # check for invalid message id + if lname == 'message-id' and len(val) < 4: + self.log('REJECT: %s: %s' % (name,val)) + return Milter.REJECT + + # check for common bulk mailers + if lname == 'x-mailer': + mailer = val.lower() + if mailer in ('direct email','calypso','mail bomber') \ + or mailer.find('optin') >= 0: + self.log('REJECT: %s: %s' % (name,val)) + return Milter.REJECT + elif self.trust_received and lname == 'received': + self.trust_received = False + self.log('%s: %s' % (name,val.splitlines()[0])) + return Milter.CONTINUE + + def header(self,name,hval): + if not self.data_allowed: + self.log('REJECT: bounce with no SRS encoding') + self.setreply('550','5.7.1',"spoofed reply address") + return Milter.REJECT + lname = name.lower() + # decode near ascii text to unobfuscate + val = parse_header(hval) + if not self.internal_connection: + # even if we wanted the Taiwanese spam, we can't read Chinese + if block_chinese and lname == 'subject': + if hval.startswith('=?big5') or hval.startswith('=?ISO-2022-JP'): + self.log('REJECT: %s: %s' % (name,hval)) + self.setreply('550','5.7.1',"We don't understand chinese") + return Milter.REJECT + rc = self.check_header(lname,val) + if rc != Milter.CONTINUE: return rc + # log selected headers + if log_headers or lname in ('subject','x-mailer'): + self.log('%s: %s' % (name,val)) + if self.fp: + try: + val = val.encode('us-ascii') + except: + val = hval + self.fp.write("%s: %s\n" % (name,val)) # add header to buffer + return Milter.CONTINUE + + def eoh(self): + if not self.fp: return Milter.TEMPFAIL # not seen by envfrom + for name,val in self.new_headers: + self.fp.write("%s: %s\n" % (name,val)) # add new headers to buffer + self.fp.write("\n") # terminate headers + self.fp.seek(0) + # copy headers to a temp file for scanning the body + headers = self.fp.getvalue() + self.fp.close() + self.tempname = fname = tempfile.mktemp(".defang") + self.fp = open(fname,"w+b") + self.fp.write(headers) # IOError (e.g. disk full) causes TEMPFAIL + # check if headers are really spammy + if dspam_dict and not self.internal_connection: + ds = dspam.dspam(dspam_dict,dspam.DSM_PROCESS, + dspam.DSF_CHAINED|dspam.DSF_CLASSIFY) + try: + ds.process(headers) + if ds.probability > 0.93 and self.dspam: + self.log('REJECT: X-DSpam-HeaderScore: %f' % ds.probability) + self.setreply('550','5.7.1','Your Message looks spammy') + return Milter.REJECT + self.add_header('X-DSpam-HeaderScore','%f'%ds.probability) + finally: + ds.destroy() + return Milter.CONTINUE + + def body(self,chunk): # copy body to temp file + if self.fp: + self.fp.write(chunk) # IOError causes TEMPFAIL in milter + self.bodysize += len(chunk) + return Milter.CONTINUE + + def _headerChange(self,msg,name,value): + if value: # add header + self.addheader(name,value) + else: # delete all headers with name + h = msg.getheaders(name) + if h: + for i in range(len(h),0,-1): + self.chgheader(name,i-1,'') + + def _chk_attach(self,msg): + "Filter attachments by content." + mime.check_name(msg,self.tempname) # check for bad extensions + if scan_html: + mime.check_html(msg,self.tempname) # remove scripts from HTML + # don't let a tricky virus slip one past us + if scan_rfc822: + msg = msg.get_submsg() + if msg: return mime.check_attachments(msg,self._chk_attach) + return Milter.CONTINUE + + def alter_recipients(self,discard_list,redirect_list): + for rcpt in discard_list: + if rcpt in redirect_list: continue + self.log("DISCARD RCPT: %s" % rcpt) # log discarded rcpt + self.delrcpt(rcpt) + for rcpt in redirect_list: + if rcpt in discard_list: continue + self.log("APPEND RCPT: %s" % rcpt) # log appended rcpt + self.addrcpt(rcpt) + if not blind_wiretap: + self.addheader('Cc',rcpt) + + # check spaminess for recipients in dictionary groups + # if there are multiple users getting dspammed, then + # a signature tag for each is added to the message. + + # FIXME: quarantine messages rejected via fixed patterns above + # this will give a fast start to stats + + def check_spam(self): + if not dspam_userdir: return False + ds = Dspam.DSpamDirectory(dspam_userdir) + ds.log = self.log + ds.headerchange = self._headerChange + modified = False + for rcpt in self.recipients: + if dspam_users.has_key(rcpt): + user = dspam_users.get(rcpt) + if user: + try: + self.fp.seek(0) + txt = self.fp.read() + if user == 'spam' and self.internal_connection: + sender = dspam_users.get(self.canon_from) + if sender: + self.log("SPAM: %s" % sender) # log user for FP + ds.add_spam(sender,txt) + txt = None + self.fp = None + return False + elif user == 'falsepositive' and self.internal_connection: + sender = dspam_users.get(self.canon_from) + if sender: + self.log("FP: %s" % sender) # log user for FP + txt = ds.false_positive(sender,txt) + self.fp = StringIO.StringIO(txt) + self.delrcpt('<%s>' % rcpt) + self.recipients = None + self.rejectvirus = False + return True + elif not self.internal_connection or dspam_internal: + if len(txt) > dspam_sizelimit: + self.log("Large message:",len(txt)) + return False + txt = ds.check_spam(user,txt,self.recipients) + if not txt: + # DISCARD if quarrantined for any recipient. It + # will be resent to all recipients if they submit + # as a false positive. + self.log("DSPAM:",user,rcpt) + self.fp = None + return False + self.fp = StringIO.StringIO(txt) + modified = True + except Exception,x: + print x + # screen if no recipients are dspam_users + if not modified and dspam_screener and not self.internal_connection \ + and (self.dspam or self.reject_spam): + self.fp.seek(0) + txt = self.fp.read() + if len(txt) > dspam_sizelimit: + self.log("Large message:",len(txt)) + return False + if not ds.check_spam(dspam_screener,txt,self.recipients, + classify=True,quarantine=not self.reject_spam): + self.fp = None + if self.reject_spam: + self.log("DSPAM:",dspam_screener, + 'REJECT: X-DSpam-Score: %f' % ds.probability) + self.setreply('550','5.7.1','Your Message looks spammy') + return True + self.log("DSPAM:",dspam_screener,"SCREENED") + return modified + + def eom(self): + if not self.fp: + return Milter.ACCEPT # no message collected - so no eom processing + + try: + # analyze external mail for spam + spam_checked = self.check_spam() # tag or quarantine for spam + if not self.fp: + if spam_checked: return Milter.REJECT + return Milter.DISCARD # message quarantined for all recipients + + # analyze all mail for dangerous attachments and scripts + self.fp.seek(0) + msg = mime.MimeMessage(self.fp) + # pass header changes in top level message to sendmail + msg.headerchange = self._headerChange + + # filter leaf attachments through _chk_attach + rc = mime.check_attachments(msg,self._chk_attach) + except: # milter crashed trying to analyze mail + exc_type,exc_value = sys.exc_info()[0:2] + if dspam_userdir and exc_type == dspam.error: + if not exc_value.strerror: + exc_value.strerror = exc_value.args[0] + if exc_value.strerror == 'Lock failed': + self.log("LOCK: BUSY") # log filename + self.setreply('450','4.2.0', + 'Too busy discarding spam. Please try again later.') + return Milter.TEMPFAIL + fname = tempfile.mktemp(".fail") # save message that caused crash + os.rename(self.tempname,fname) + self.tempname = None + self.log("FAIL: %s" % fname) # log filename + if exc_type == email.Errors.BoundaryError: + self.setreply('554','5.7.7', + 'Boundary error in your message, are you a spammer?') + return Milter.REJECT + if exc_type == email.Errors.HeaderParseError: + self.setreply('554','5.7.7', + 'Header parse error in your message, are you a spammer?') + return Milter.REJECT + # let default exception handler print traceback and return 451 code + raise + if rc == Milter.REJECT: return rc; + if rc == Milter.DISCARD: return rc; + + if rc == Milter.CONTINUE: rc = Milter.ACCEPT # for testbms.py compat + + defanged = msg.ismodified() + + if self.hidepath: del msg['Received'] + + if self.recipients == None: + # false positive being recirculated + self.recipients = msg.get_all('x-dspam-recipients',[]) + if self.recipients: + for rcptlist in self.recipients: + for rcpt in rcptlist.split(','): + self.addrcpt('<%s>' % rcpt.strip()) + del msg['x-dspam-recipients'] + else: + self.addrcpt(self.mailfrom) + else: + self.alter_recipients(self.discard_list,self.redirect_list) + for name,val in self.new_headers: + self.addheader(name,val) + + if not defanged and not spam_checked: + os.remove(self.tempname) + self.tempname = None # prevent re-removal + self.log("eom") + return rc # no modified attachments + + # Body modified, copy modified message to a temp file + if defanged: + if self.rejectvirus and not self.hidepath: + self.log("REJECT virus from",self.mailfrom) + self.tempname = None + return Milter.REJECT + self.log("Temp file:",self.tempname) + self.tempname = None # prevent removal of original message copy + out = tempfile.TemporaryFile() + try: + msg.dump(out) + out.seek(0) + msg = rfc822.Message(out) + msg.rewindbody() + while True: + buf = out.read(8192) + if len(buf) == 0: break + self.replacebody(buf) # feed modified message to sendmail + if spam_checked: self.log("dspam") + return rc + finally: + out.close() + return Milter.TEMPFAIL + + def close(self): + sys.stdout.flush() # make log messages visible + if self.tempname: + os.remove(self.tempname) # remove in case session aborted + if self.fp: + self.fp.close() + return Milter.CONTINUE + + def abort(self): + self.log("abort after %d body chars" % self.bodysize) + return Milter.CONTINUE + +def main(): + Milter.factory = bmsMilter + flags = Milter.CHGBODY + Milter.CHGHDRS + Milter.ADDHDRS + if wiretap_dest or smart_alias or dspam_userdir: + flags = flags + Milter.ADDRCPT + if srs or len(discard_users) > 0 or smart_alias or dspam_userdir: + flags = flags + Milter.DELRCPT + Milter.set_flags(flags) + print "bms milter startup" + sys.stdout.flush() + Milter.runmilter("pythonfilter",socketname,600) + print "bms milter shutdown" + +if __name__ == "__main__": + read_config(["milter.cfg"]) + if dspam_dict: + import dspam # low level spam check + if dspam_userdir: + import dspam + import Dspam # high level spam check + try: + dspam_version = Dspam.VERSION + except: + dspam_version = '1.1.4' + assert dspam_version >= '1.1.5' + main() diff --git a/faq.html b/faq.html new file mode 100644 index 0000000..2c249d2 --- /dev/null +++ b/faq.html @@ -0,0 +1,138 @@ + + + +Python Milter FAQ + + +

Python Milter FAQ

+ +
    +

    Compiling Python Milter

    +
  1. Q. I have installed sendmail from source, but Python milter won't +compile. +

    A. Even though libmilter is officially supported in sendmail-8.12, +you need to build and install it in separate steps. Take a look +at the RPM spec file for sendmail-8.12. +The %prep section shows you how to create +a site.config.m4 that enables MILTER. The %build section shows you how +to build libmilter in a separate invocation of make. The %install section +shows you how to install libmilter with a separate invocation of make. +

    + +

  2. Q. Why is mfapi.h not found when I try to compile Python milter on +RedHat 7.2? +

    A. RedHat forgot to include the header in the RPM. See the +RedHat 7.2 requirements. +

    + +

    Running Python Milter

    + +
  3. Q. The sample.py milter prints a message, then just sits there. +
    +To use this with sendmail, add the following to sendmail.cf:
    +
    +O InputMailFilters=pythonfilter
    +Xpythonfilter,        S=local:inet:1030@localhost
    +
    +See the sendmail README for libmilter.
    +sample  milter startup
    +
    +

    A. You need to tell sendmail to connect to your milter. The +sample milter tells you what to add to your sendmail.cf to tell +sendmail to use the milter. You can also add an INPUT_MAIL_FILTER +macro to your sendmail.mc file and rebuild sendmail.cf - see the sendmail +README for milters. +

    + +

  4. Q. I've configured sendmail properly, but still nothing happens +when I send myself mail! +

    A. Sendmail only milters SMTP mail. Local mail is not miltered. +You can pipe a raw message through sendmail to test your milter: +

    +$ cat rawtextmsg | sendmail myname@my.full.domain
    +
    +Now check your milter log. +

    + +

  5. Q. Why do I get this ImportError exception? +
    +File "mime.py", line 370, in ?
    +    from sgmllib import declstringlit, declname
    +    ImportError: cannot import name declstringlit
    +
    +

    A. declstringlit is not provided by sgmllib in all versions +of python. For instance, python-2.2 does not have it. Upgrade to +milter-0.4.5 or later to remove this dependency. +

    + +

  6. Q. Why do I get milter.error: cannot add recipient? +
    +
    +

    A. You must tell libmilter how you might mutate the message with +set_flags() before calling runmilter(). For +instance, Milter.set_flags(Milter.ADDRCPT). You must add together +all of ADDHDRS, CHGBODY, ADDRCPT, DELRCPT, CHGHDRS that apply. +

    + +

  7. Q. Why does sendmail sometimes print something like: +"...write(D) returned -1, expected 5: Broken pipe" +in the sendmail log? +

    A. Libmilter expects "rcpt to" shortly after getting "mail from". +"Shortly" is defined by the timeout parameter you passed to +Milter.runmilter() + or milter.settimeout(). If the timeout is 10 seconds, +and looking up the first recipient in DNS takes more than +10 seconds, libmilter will give up and break the connection. +Milter.runmilter() defaulted to 10 seconds in 0.3.4. In 0.3.5 +it will keep the libmilter default of 2 hours. +

    + +

  8. Q. Why does milter block messages with big5 encoding? What if I +want to receive them? +

    A. sample.py is a sample. It is supposed to be easily modified +for your specific needs. We will of course continue to move generic +code out of the sample as the project evolves. Think of sample.py as +an active config file. +

    + +

  9. Q. Why does sendmail coredump with milters on OpenBSD? +

    A. Sendmail has a problem with unix sockets on OpenBSD. Use +an internet domain socket instead. For example, in sendmail.cf use +

    +Xpythonfilter, S=inet:1234@localhost
    +
    +and change sample.py accordingly. +

    + +

  10. Q. How can I change the bounce message for an invalid recipient? +I can only change the recipient in the eom callback, but the eom callback +is never called when the recipient is invalid! +

    A. Configure sendmail to use virtusertable, and send all unknown +addresses to /dev/null. For example, +

    /etc/mail/virtusertable

    +
    +@mycorp.com	dev-null
    +dan@mycorp.com	dan
    +sally@mycorp.com	sally
    +
    +

    /etc/aliases

    +
    +dev-null:	/dev/null
    +
    +Now your milter will get to the eom callback, and can change the +envelope recipient at will. Thanks to Dredd at +milter.org for this solution. +

    + +

  11. Q. I am having trouble with the setreply method. It always outputs + "milter.error: cannot set reply". +

    A. Check the sendmail log for errors. If sendmail is getting +milter timeouts, then your milter is taking too long and sendmail gave +up waiting. You can adjust the timeouts in your sendmail config. Here +is a milter declaration for sendmail.cf with all timeouts specified: +

    +Xpythonfilter, S=local:/var/log/milter/pythonsock, F=T, T=C:5m;S:20s;R:60s;E:5m
    +
    + +
+ diff --git a/milter.cfg b/milter.cfg new file mode 100644 index 0000000..33094e8 --- /dev/null +++ b/milter.cfg @@ -0,0 +1,109 @@ +# features intended to filter or block incoming mail +[milter] +tempdir = /var/log/milter/save +scan_rfc822 = 1 +# can be CPU intensive +scan_html = 0 +block_chinese = 1 +;block_forward = egghead@mycorp.com, busybee@mycorp.com +log_headers = 0 +# Reject mail for domains mentioned unless user is mentioned here also +;check_user = joe@mycorp.com, mary@mycorp.com, file:bigcorp.com +# porn words are case insensitive +porn_words = penis, breast, pussy, horse cock, porn, xenical, diet pill, d1ck, + vi*gra, vi-a-gra, viag, tits, p0rn, hunza, horny, sexy, c0ck, + p-e-n-i-s, hydrocodone, vicodin, xanax, vicod1n, x@nax +# spam words are case sensitive +spam_words = $$$, !!!, XXX, FREE, HGH + +# connection ips and hostnames are matched against this glob style list +# to recognize internal senders +;internal_connect = 192.168.*.* +# mail that is not an internal_connect and claims to be from an +# internal domain is rejected. +;internal_domains = mycorp.com +# connections from a trusted relay can trust the first Received header +;trusted_relay = 1.2.3.4, 66.12.34.56 +# reject external senders with hello names no legit external sender would use +;hello_blacklist = mycorp.com, 66.12.34.56 + +[srs] +config=/etc/mail/pysrs.cfg +;secret="shhhh!" +;maxage=21 +;hashlength=4 +;database=/var/log/milter/srsdata +;fwdomain = mydomain.com +# turn this on after a grace period +reject_spoofed = 0 + +[spf] +# namespace where SPF records can be supplied for domains without one +# records are search for under _spf.domain.com +;delegate = domain.com +# domains where a neutral SPF result should cause mail to be rejected +;reject_neutral = aol.com + +# features intended to clean up outgoing mail +[scrub] +# domains that stupidly block visible private nodes +;hide_path = jcpenney.com +# block, don't just replace with warning, viruses from these domains +;reject_virus_from = mycorp.com + +# features intended for spying on users and coworkers +[wiretap] +blind = 1 +# +# wiretap lets you surreptitiously monitor a users outgoing email +# (sendmail aliases let you monitor incoming mail) +# +;users = disloyal@bigcorp.com, bigmouth@bigcorp.com +;dest = spy@bigcorp.com +# discard outgoing mail without alerting sender +# can be used in conjunction with wiretap to censor outgoing mail +;discard_users = canned@bigcorp.com +# +# smart aliases trigger on both sender and recipient +# +;smart_alias = copycust,walter +# mail from client@clientcorp.com to sue@bigcorp.com is redirected to +# local alias copycust +;copycust = client@clientcorp.com,sue@bigcorp.com +# mail from cust@othercorp.com to walter@bigcorp.com is redirected to +# boss@bigcorp.com +;walter = cust@othercorp.com,walter@bigcorp.com,boss@bigcorp.com +# additional copies can be added +;walter1 = cust@othercorp.com,walter@bigcorp.com,boss@bigcorp.com, +; walter@bigcorp.com + +[dspam] +# Select a well moderated dspam dictionary to reject spammy headers +# dspam-python must be installed to use: http://bmsi.com/python/dspam.html +# only EXTERNAL messages are dspam filtered +;dspam_dict=/var/lib/dspam/moderator.dict +# Opt-opt recipients from dspam screening and header triage +;dspam_exempt=getitall@mycorp.com +# Do not scan mail (ostensibly) from these senders +;dspam_whitelist=getitall@sender.com +# Reject spam to these domains, perhaps because we are a backup MX server +;dspam_reject=othercorp.com + +# directory for dspam user quarantine, signature db, and dictionaries +# defining this activates the dspam application +# dspam and dspam-python must be installed +;dspam_userdir=/var/lib/dspam + +# Map email addresses and aliases to dspam users +;dspam_users=david,goliath,spam,falsepositive +;david=david@foocorp.com,david.yelnetz@foocorp.com,david@bar.foocorp.com +;goliath=giant@foocorp.com,goliath.philistine@foocorp.com +# address to forward spam to. milter will process these and not deliver +;spam=spam@foocorp.com +# address to forward false positives to. milter will process and not deliver +;falsepositive=ham@foocorp.com +# the dspam_screener is used to screen mail for all recipients who are +# not dspam_users. Spam goes to the screeners quarantine, and the original +# recipients saved so that false positives can be properly delivered. + +# The dspam CGI can also be used: logins must match dspam users diff --git a/milter.html b/milter.html new file mode 100644 index 0000000..931aecd --- /dev/null +++ b/milter.html @@ -0,0 +1,614 @@ + + + + +Python Milters + + +

+ + +Your vote? + + I Disagree + I Agree + + +

+

Sendmail Milters in Python

+

by Jim Niemira + and + Stuart D. Gathman
+This web page is written by Stuart D. Gathman
and
sponsored by +Business Management Systems, Inc.
+Last updated Apr 05, 2004

+ +See the FAQ | Download now | +Subscribe to mailing list +

+A Python +Sendmail introduced a + new API beginning with version 8.10 - +libmilter. The milter module for Python +provides a python interface to libmilter that exploits all its features. +

+Sendmail 8.12 officially releases libmilter. +Version 8.12 seems to be more robust, and includes new privilege +separation features to enhance security. +I recommend upgrading. + +

Bayesian Filtering

+ +I have selected the +dspam bayes filter project and +packaged it for python. +Release 0.6.0 offers a simple application of dspam I call "header triage", +which rejects messages with spammy headers. Since sendmail has to +read the entire message anyway once we start reading headers, it +would probably be better to scan the whole message - except that +we replace dangerous attachments elsewhere in the milter - which screws up the +body statistics for messages with dangerous attachments. +

+Release 0.6.1 adds a full milter based dspam application. +

+To use header triage, you must have DSPAM installed, +and select a dictionary that is well moderated by someone who gets +lots of spam. That dictionary can be used to block spam that is +obvious from the headers (e.g. X-Mailer and Subject) before it ties +up any more resources. I have yet to see any false positives from this +approach (check the milter log), but if there are, the sender will +get a REJECT with the message "Your message looks spammy." + +

Enough Already!

+ +Nearly a dozen people have emailed me begging for a feature to copy +outgoing and/or incoming mail to a backup directory by user. Ok, it +looks like this is a most requested feature for 0.5.6. In the meantime, +here are some things to consider: +
    +
  • If you want to equivalent of a Bcc added to each message, this +is very easy to do in the python code for bms.py. See below. +
  • If you want to copy to a file in a directory (thus avoiding having to +set up aliases), this is slightly more involved. The bms.py milter already +copies the message to a temporary file for use in replacing the message body +when banned attachments are found. You have to open a file, and copy the +Mesage object to it in eom(). +
  • Finally, you are probably aware that most email clients already +keep a copy of outgoing mail? Presumably there is a good reason for +keeping another copy on the server. +
+

+To Bcc a message, call self.add_recipient(rcpt) in envfrom after +determining whether you want to copy (e.g. whether the sender is local). For +example, +

+  def envfrom(...
+    ...
+    if len(t) == 2:
+      self.rejectvirus = t[1] in reject_virus_from
+      if t[0] in wiretap_users.get(t[1],()):
+	self.add_recipient(wiretap_dest)
+      if t[1] == 'mydomain.com':
+        self.add_recipient('<copy-%s>' % t[0])
+      ...
+
+

+To make this a generic feature requires thinking about how the configuration +would look. Feel free to make specific suggestions about config file +entries. Be sure to handle both Bcc and file copies, and designating what +mail should be copied. How should "outgoing" be defined? Implementing it is +easy once the configuration is designed. + +

Overview

+ +This package provides a robust toolkit for Python milters, and the beginnings of a general purpose mail +filtering system written in Python. +

+At the lowest level, the 'milter' module provides a thin wrapper around the + +sendmail libmilter API. This API lets you register callbacks for +a number of events in the +process of sendmail receiving a message via SMTP. +These events include the initial connection from a MTA, +the envelope sender and recipients, the top level mail headers, and +the message body. There are options to mangle all of these components +of the message as it passes through the milter. +

+At the next level, the 'Milter' module (note the case difference) provides a +Python friendly object oriented wrapper for the low level API. To use the +Milter module, an application registers a 'factory' to create an object +for each connection from a MTA to sendmail. These connection objects +must provide methods corresponding to the libmilter callback events. +

+Each event method returns a code to tell sendmail whether to proceed +with processing the message. This is a big advantage of milters over +other mail filtering systems. Unwanted mail can be stopped in its +tracks at the earliest possible point. +

+The Milter.Milter class provides default implementations for event +methods that +do nothing, and also provides wrappers for the libmilter methods to mutate +the message. +

+Finally, the bms.py application is both a sample of how to use the +Milter module, and the beginnings of a general purpose SPAM filtering, +wiretapping, and Win32 virus protection milter. + +

Downloading

+ +The latest stable release is 0.6.6. A stable +release is one which has been installed (and working correctly) on +production systems long enough to convince me that it is stable. As +the package gains more features and complexity, stable will mean no +bug reports from outside users either. +

+The latest version is 0.6.7. See the Change Log. + +

+ +milter-0.6.7.tar.gz Explicit local socket bug, +SRS forgery detection, +thread resource starvation detection. +SRS support requires pysrs. +

+ +milter-0.6.7-3.i386.rpm Binary RPM for Redhat 7.x, now requires + sendmail-8.12 and + python2.3. +
+ +milter-0.6.7-3.src.rpm Source RPM for Redhat 7.x. +Release 0.6.7-3 patches: +

    +
  • Defang message/rfc822 content_type with boundary +
  • Support SPF delegation +
  • Reject neutral SPF result for selected domains +
+

+Stable + +milter-0.6.6.tar.gz Plug another memory leak, +SPF support, hello blacklist. +SPF support requires pydns. +NOTE - the spf.py module included is modified from the official 1.6 +version at wayforward.net. +I neglected to add the CVS log. The changes are expanded result codes +and tolerating common method misspellings in SPF records. I have notified the +author, but haven't heard back. At some point, the RPM will +include the official pyspf tarball and apply patches. +

+ +milter-0.6.6-2.i386.rpm Binary RPM for Redhat 7.x, now requires + sendmail-8.12 and + python2.3. Release 2 fixes sysv init script bug for python2.3. +
+ +milter-0.6.6-2.src.rpm Source RPM for Redhat 7.x +

+ +milter-0.6.5.tar.gz Plug memory leak, progress reporting, trusted relay. +Redhat RPM now requires sendmail-8.12. +

+ +milter-0.6.5-2.i386.rpm Binary RPM for Redhat 7.x +
+ +milter-0.6.5-2.src.rpm Source RPM for Redhat 7.x +

+ +milter-0.6.4.tar.gz Numerous Dspam fixes. Requires +pydspam-1.1.5 and +dspam-2.6.5.2 +for Dspam features. The dspam-python RPM has been replaced by pydspam. +

+ +milter-0.6.4-1.i386.rpm Binary RPM for Redhat 7.x +

+ +milter-0.6.3.1.tar.gz New dspam SCREENER feature with pydspam-1.1.4. +Don't save a defang copy of false positives. Fixed an oops from last fix, +rejecting false positives. BUG: sendmail-8.11 doesn't invoke milter +when sending mail via sendmail from command line (8.12 works). Therefore, +the supplied falsepositive script for milter based dspam doesn't work +with stock RedHat 7.x. I am writing a HOWTO for configuring milter +based dspam that will address this (and a fix in the next version). +

+ +milter-0.6.3-1.i386.rpm Binary RPM for Redhat 7.x +

+ +milter-0.6.2.tar.gz work around email.Message.get_filename bug, +dspam_exempt list, REJECT messages with missing MIME boundaries (which +are almost always spam), +DISCARD messages which any dspam user flags as spam, +start.sh was calling python instead of python2 on Linux. +

+ +milter-0.6.2-1.src.rpm Source RPM for Redhat 7.x (and likely +higher versions) +

+ +milter-0.6.1.tar.gz dspam milter application, python-2.2.3 support. +

+You must have dspam and dspam-python loaded for +the dspam feature to work. Brief instructions for configuring are +in the default config file. This is working at a customer, but I'm +sure a few more iterations will be required to make setup as smooth +as possible. +

+NOTE: Outlook destroys dspam tags when forwarding mail (while converting +HTML to text). Perhaps some config option will turn this abominable +"feature" off. Working around this by making dspam tags visble on +HTML mail is ugly. My suggestion is to not use Outlook, for this and +many other reasons - especially security. Any other suggestions for +those married to Microsoft are welcome. The DSPAM LDA works around this +by making the tags visible in HTML attachments. This is ugly, and +occasionally corrupts attachments. +

+We have to supply workarounds for bugs in the email module (reported +to sourceforge). The workarounds reference some internal variables +which change with python versions. +

+ +milter-0.6.1-1.i386.rpm Binary RPM for Redhat 7.x +

+ +milter-0.6.1-1.src.rpm Source RPM for Redhat 7.x (and likely +higher versions) +

+ +milter-0.6.0.tar.gz simple dspam pre-filtering, use email module, +requires python >= 2.2.2. +

    +
  • The milter.so module from 0.5.4 +is needed to run this release on AIX. Haven't tracked this down yet. +
  • The patches to fix the email packages in mime.py don't work +on python-2.2.3. The email package is still broken in 2.3, and patches +required for that will likely be different still. +
+ +

+ +milter-0.6.0-1.i386.rpm Binary RPM for Redhat 7.x +

+ +milter-0.6.0-1.src.rpm Source RPM for Redhat 7.x (and likely +higher versions) +

+ +milter-0.5.5.tar.gz IPV6 support, passing None to set_XXX_callback, +set_reply, chg_header, detect internal connections. Note, this release +did not work on AIX4.1.5, probably due to IPV6 support breaking something. +The milter.so module from 0.5.4 can be installed to use this release +with AIX. +

+ +milter-0.5.4.tar.gz wiretap, smart alias features, quarantine support. +

+The name of the production "sample" milter "bms.py" now +stands for "Basic Milter System" until someone suggests a better name. +The test coverage is rather + sparse at present. + Please email with proposals for what + to name the milter application. +

NOTES

+
    +
  • + Quarantine support requires that you define _FFR_QUARANTINE + when compiling miltermodule.c. I am not sure how to make setup.py + do that for you iff sendmail was actually compiled with _FFR_QUARANTINE. +
  • + While 0.6.0 will use the new email package in Python-2.2, that + package seems to be buggy in Python-2.2.1. The list example in the docs + doesn't find all MIME parts. Update: Python-2.2.2 has fixed the email + package. It can now parse my test cases. +
  • + Preliminary testing with python-2.2 shows that most things work after + adding self.readahead = "" to mimepart.seek. + Python-2.2 multifile reads one less newline per section than + 2.1. I'm not not sure which is correct. After adding some calls to + rstrip() in testmime.py, all milter modules pass unit testing + with python-2.2. Python-2.2 patches have been released since 0.5.3. +
  • + sgmlop-1.1a3 has a memory leak (at least Python milter has a + memory leak when using sgmlop instead of sgmllib). Do not make Python + milter use sgmlop-1.1a2 or a3 in a production + system unless you can restart your milter periodically. The amount + of memory leaked seems roughly proportional to the amount of HTML + parsed. +
  • + There are a number of ways that malformed MIME attachments + can cause a python traceback. Uncaught exceptions cause a 415 + error to be returned to sendmail. So far, all the malformed messages + I've investigated have been SPAM - so good riddance. I would prefer, + however, that the mime handling libraries were more precise. Beginning + with 0.5.1, bms.py will save messages that cause a traceback during + scanning in the tempfile directory with a ".fail" extension. This + makes it easier to get samples of mail that causes parsing problems + for incorporation into the unit tests. +
+

+ +milter-0.5.2.tar.gz Fix and unittest another HTML parsing bug.
+ +milter-0.5.1.tar.gz Handle encoded rfc822 attachments.
+ +milter-0.5.0.tar.gz Use a config file so users don't have to +keep syncing with bms.py.
+ +milter-0.4.5.tar.gz Work with sgmlop. Reduce local hacks to config variables. +

+Python milter is under GPL. The authors can probably be convinced to +change this to LGPL. + +

What is a milter?

+ +Milters can run on the same machine as sendmail, or another machine. The +milter can even run with a different operating system or processor than +sendmail. +Sendmail talks to the milter via a local or internet socket. +Sendmail keeps the +milter informed of events as it processes a mail connection. At any +point, the milter can cut the conversation short by telling sendmail +to ACCEPT, REJECT, or DISCARD the message. After receiving a complete +message from sendmail, the milter can again REJECT or DISCARD it, but it +can also ACCEPT it with changes to the headers or body. + +

What can you do with a milter?

+ + +
  • A milter can DISCARD or REJECT spam based based on algorithms scripted +in python rather than sendmail's cryptic "cf" language. +
  • A milter can alter or remove attachments from mail that are poisonous to +Windows. +
  • A milter can scan for viruses and clean them when detected. +
  • A milter scans outgoing as well as incoming mail. +
  • A milter can add and delete recipients to forward or secretly +copy mail. +
  • For more ideas, check the Milter Web Page. +
  • + + +Documentation for the C API is provided with sendmail. Miltermodule +provides a thin python wrapper for the C API. Milter.py provides a simple +OO wrapper on top of that. +

    +The Python milter package includes a sample milter that replaces dangerous +attachments with a warning message, discards mail addressed to +MAILER-DAEMON, and demonstrates several SPAM abatement strategies. +The MimeMessage class to do this used to be based on the +mimetools and multifile standard python packages. +As of milter version 0.6.0, it is based on the email standard +python packages, which were derived from the +mimelib project. +The MimeMessage class patches several bugs in the email package, +and provides some backward compatibility. + +

    +The "defang" function of the sample milter was inspired by +MIMEDefang, +a Perl milter with flexible attachment processing options. The latest +version of MIMEDefang uses an apache style process pool to avoid reloading +the Perl interpreter for each message. This makes it fast enough for +production and does not use Perl threading. +

    +mailchecker is +a Python project to provide flexible attachment processing for mail. I +will be looking at plugging mailchecker into a milter. +

    +TMDA is a Python project +to require confirmation the first time someone tries to send to your +mailbox. This would be a nice feature to have in a milter. +

    +There is also a Milter community website +where milter software and gory details of the API are discussed. + +

    Is a milter written in python efficient?

    + +The python milter process is multi-threaded and startup cost is incurred +only once. This is much more efficient than some implementations that +start a new interpreter for each connection. Testing in a production +environment did not use a significant percentage of the CPU. Furthermore, +python is easily extended in C for any step requiring expensive CPU +processing. +

    +For example, the HTML parsing feature to remove scripts from HTML attachments +is rather CPU intensive in pure python. Using the C replacement for sgmllib +greatly speeds things up. + +

    Goals

    + + +
  • Implement RRS - a backdoor for non-SRS forwarders. User lists non-SRS + forwarder accounts (perhaps in ~/.forwarders), and a util + provides a special local alias for the user to give to the forwarder. + Alias only works for mail from that forwarder. Milter gets forwarder + domain from alias and uses it to SPF check forwarder. Requires + milter to have read access to ~/.forwarders or else + a way for user to submit entries to milter database. +
  • The bms.py milter has too many features. Create a framework where + numerous small feature modules can be plugged together in the + configuration. +
  • Create a pure python substitute for miltermodule and libmilter that + implements the + libmilter protocol in python. +
  • Find or write a faster implementation of sgmllib. The + sgmlop package + is not very compatible with + + Python-2.1 sgmllib, but it is a start, and is supported in + milter-0.4.5 or later. +
  • Implement all or most of the features of + MIMEDefang. +
  • Follow the official + Python coding standards more closely. +
  • Make unit test code more like other python modules. +
  • + +

    Confirmed Installations

    + +Please email +me if you successfully install milter on a system not mentioned below. +

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    Operating System Compiler Python Sendmailmilter
    Mandrake 8.0gcc-3.0.12.1.18.12.00.3.3
    Mandrake 8.0gcc-2.962.08.11.20.3.6
    RedHat 6.2egcs-1.1.22.2.28.11.60.5.4
    RedHat 7.1gcc-2.96?8.12.10.3.5
    RedHat 7.2gcc-2.962.1.18.11.60.4.1
    RedHat 7.2gcc-2.962.2.18.11.60.4.5
    RedHat 7.2gcc-2.962.2.28.11.60.5.5
    RedHat 7.2gcc-2.962.3.38.12.100.6.6
    RedHat 7.3gcc-2.962.2.28.11.60.5.5
    RedHat 7.3gcc-2.962.3.38.12.100.6.6
    RedHat 8.0gcc-3.22.2.18.12.60.5.2
    Debian Linuxgcc-2.95.22.1.18.12.00.3.7
    Debian Linuxgcc-3.2.22.2.28.12.70.5.4
    AIX-4.1.5gcc-2.95.22.1.18.11.50.3.3
    AIX-4.1.5gcc-2.95.22.1.18.12.10.3.4
    AIX-4.1.5gcc-2.95.22.1.38.12.30.4.2
    AIX-4.1.5gcc-2.95.22.2.28.12.60.5.4
    Slackware 7.1??8.12.10.3.8
    Slackware 9.0gcc-3.2.22.2.38.12.90.5.4
    OpenBSD?2.1.18.11.60.3.9
    SuSE 7.3gcc-2.95.32.1.18.12.20.3.9
    FreeBSDgcc-2.95.32.2.18.12.30.4.0
    FreeBSDgcc-2.95.32.2.2?0.5.5
    FreeBSD 4.4gcc-2.95.3?8.12.100.6.6
    + +

    Requirements

    + + +
  • While the miltermodule will work with python 1.5, you probably +want to use python 2.0 or better. The python code uses a number of +python 2 features. +
  • Python must be configured with thread support. This is because +sendmail's libmilter requires thread support. +
  • You must compile sendmail with libmilter enabled. In versions of +sendmail prior to 8.12 libmilter is marked FFR (For Future Release) and +is not installed by default. +Sendmail 8.12 still does not enable libmilter by default. You must +explicitly select the "MILTER" option when compiling. +
  • Python milter has been tested against sendmail-8.11 and sendmail-8.12. +
  • Python milter must be compiled for the specific version of sendmail +it will run with. (Since the result is dynamically loaded, there could +conceivably be multiple versions available and selected at startup - but +that will have to wait.) This situation may only exist for sendmail +versions prior to 8.12. The protocol seems designed for backward +compatibility - and 8.12 is the first official milter release. +
  • Mea Culpa! After reading the Python Style guide, I realize that +my Python code is not up to snuff. Apparently mixed tabs and spaces +are anathema to those using Windows editors, where tabs can be expanded using +any arbitrary algorithm. Other than that, my +intuition matched Guido's pretty well - although I like to indent by 2 +rather than 4. I will arrange to have tabs expanded to spaces when +exporting new versions. Until then, beware! +
  • + +

    AIX 4.1.5 Requirements

    +To create sendmail RPMs for AIX, you can download my AIX 4.1.5 spec files +for sendmail-8.11.5 +or sendmail-8.12.3. If you have +not already set it up, I use a dummy RPM package +to represent the stuff that comes with AIX. You might also want +my python-2.1.1 spec file for AIX. It +does not include Tk or curses modules, sorry. If y'all trust me, you can +download rpms for AIX 4.x from my AIX RPM directory. +

    +Sendmail-8.12 renames +libsmutil.a to libsm.a. Unfortunately, libsm.a is an important AIX system +shared library. Therefore, I rename libsm.a back to libsmutil.a for +AIX. This presents a problem for setup.py. + +

    RedHat 7.2 Requirements

    + +If you are running Redhat 7.2, the distributed version of sendmail +now enables libmilter by default. RedHat 7.2 bundles +the development libraries with the main sendmail package, so +there is no sendmail-devel package. However, they forgot to include the +headers! So you'll have to get the SRPM and modify it. I suggest +moving the static libs to a devel package and adding the headers. If +this is too much trouble, you can get the mfapi.h +header for sendmail-8.6.11 from here and manually install it as +/usr/include/libmilter/mfapi.h. +

    +If you do modify the SRPM, I suggest renaming libsmutil.a +to libsm.a - just like sendmail-8.12 will. If you manually install +mfapi.h or don't rename libsmutil.a, you'll +need to force libs = ["milter", "smutil"] in setup.py. +

    +If you have installed python2, and want +python-milter to use python2, add python=python2 to setup.cfg +and build with python2 setup.py bdist_rpm. + +

    Redhat 6.2 Requirements

    + +If you are running Redhat 6.2, the distributed version of sendmail +does not enable libmilter. You can download the Redhat 7.2 sendmail.spec +modified to compile on RedHat 6.2: + +sendmail-rhmilter.spec. The +SRPM for sendmail-8.11.6 is available from +Redhat under + +Errata for RH6.2. But that doesn't include the latest security +patches since RH6.2 is no longer supported. +

    +If y'all trust me, you can pick up source and binary sendmail RPMs for RH6.2 +from my linux downloads directory. +The lastest RPMs were built by taking a RH7.2 SRPMS and removing some +RPM features from the spec file that RH6.2 doesn't support, then +recompiling on RH6.2. You can check this by installing the RH7.2 SRPM, +then diffing my sendmail.spec with theirs. Then run +"rpm -bb sendmail-rhmilter.spec" when you are satisfied. +

    +If you have installed python2, and want +python-milter to use python2, add python=python2 to setup.cfg +and build with python2 setup.py bdist_rpm. +You'll need to install the sendmail-devel package to compile milter. + +


    +

    + + [ Valid HTML 3.2! ] + + [ Powered By Red Hat Linux ] +

    + + diff --git a/milter.rc b/milter.rc new file mode 100755 index 0000000..cd43cf9 --- /dev/null +++ b/milter.rc @@ -0,0 +1,81 @@ +#!/bin/bash +# +# milter This shell script takes care of starting and stopping milter. +# +# chkconfig: 2345 80 30 +# description: Milter is a process that filters messages sent through sendmail. +# processname: milter +# config: /var/log/milter/bms.py +# pidfile: /var/run/milter/milter.pid + +python="python2.3" + +pidof() { + set - "" + if set - `ps -e -o pid,cmd | grep "${python} bms.py"` && + [ "$2" != "grep" ]; then + echo $1 + return 0 + fi + return 1 +} + +# Source function library. +. /etc/rc.d/init.d/functions + +[ -x /var/log/milter/start.sh ] || exit 0 + +RETVAL=0 +prog="milter" + +start() { + # Start daemons. + + echo -n "Starting $prog: " + daemon --check milter --user mail /var/log/milter/start.sh + RETVAL=$? + echo + [ $RETVAL -eq 0 ] && touch /var/lock/subsys/milter + return $RETVAL +} + +stop() { + # Stop daemons. + echo -n "Shutting down $prog: " + killproc milter + RETVAL=$? + echo + [ $RETVAL -eq 0 ] && rm -f /var/lock/subsys/milter + return $RETVAL +} + +# See how we were called. +case "$1" in + start) + start + ;; + stop) + stop + ;; + restart|reload) + stop + start + RETVAL=$? + ;; + condrestart) + if [ -f /var/lock/subsys/milter ]; then + stop + start + RETVAL=$? + fi + ;; + status) + status milter + RETVAL=$? + ;; + *) + echo "Usage: $0 {start|stop|restart|condrestart|status}" + exit 1 +esac + +exit $RETVAL diff --git a/milter.rc7 b/milter.rc7 new file mode 100755 index 0000000..5445ca2 --- /dev/null +++ b/milter.rc7 @@ -0,0 +1,81 @@ +#!/bin/bash +# +# milter This shell script takes care of starting and stopping milter. +# +# chkconfig: 2345 80 30 +# description: Milter is a process that filters messages sent through sendmail. +# processname: milter +# config: /var/log/milter/bms.py +# pidfile: /var/run/milter/milter.pid + +python="python2.3" + +pidof() { + set - "" + if set - `ps -e -o pid,wchan,cmd | grep "rt_sig ${python} bms.py"` && + [ "$3" != "grep" ]; then + echo $1 + return 0 + fi + return 1 +} + +# Source function library. +. /etc/rc.d/init.d/functions + +[ -x /var/log/milter/start.sh ] || exit 0 + +RETVAL=0 +prog="milter" + +start() { + # Start daemons. + + echo -n "Starting $prog: " + daemon --check milter --user mail /var/log/milter/start.sh + RETVAL=$? + echo + [ $RETVAL -eq 0 ] && touch /var/lock/subsys/milter + return $RETVAL +} + +stop() { + # Stop daemons. + echo -n "Shutting down $prog: " + killproc milter + RETVAL=$? + echo + [ $RETVAL -eq 0 ] && rm -f /var/lock/subsys/milter + return $RETVAL +} + +# See how we were called. +case "$1" in + start) + start + ;; + stop) + stop + ;; + restart|reload) + stop + start + RETVAL=$? + ;; + condrestart) + if [ -f /var/lock/subsys/milter ]; then + stop + start + RETVAL=$? + fi + ;; + status) + status milter + RETVAL=$? + ;; + *) + echo "Usage: $0 {start|stop|restart|condrestart|status}" + exit 1 +esac + +exit $RETVAL diff --git a/milter.spec b/milter.spec new file mode 100644 index 0000000..5241c5c --- /dev/null +++ b/milter.spec @@ -0,0 +1,167 @@ +%define name milter +%define version 0.6.8 +%define release 1 +# Redhat 7.x and earlier (multiple ps lines per thread) +#%define sysvinit rc7 +# RH9, other systems (single ps line per process) +%define sysvinit rc +%ifos Linux +%define python python2.3 +%else +%define python python +%endif + +Summary: Python interface to sendmail milter API +Name: %{name} +Version: %{version} +Release: %{release} +Source: %{name}-%{version}.tar.gz +#Patch: %{name}.patch +Copyright: GPL +Group: Development/Libraries +BuildRoot: %{_tmppath}/%{name}-buildroot +Prefix: %{_prefix} +Vendor: Stuart D. Gathman +Packager: Stuart D. Gathman +Url: http://www.bmsi.com/python/milter.html +Requires: %{python} >= 2.2.2, sendmail >= 8.12 +BuildRequires: %{python}-devel >= 2.2.2, sendmail-devel >= 8.12 + +%description +This is a python extension module to enable python scripts to +attach to sendmail's libmilter functionality. Additional python +modules provide for navigating and modifying MIME parts. + +%prep +%setup +#%patch -p1 + +%build +env CFLAGS="$RPM_OPT_FLAGS" %{python} setup.py build + +%install +rm -rf $RPM_BUILD_ROOT +%{python} setup.py install --root=$RPM_BUILD_ROOT --record=INSTALLED_FILES +mkdir -p $RPM_BUILD_ROOT/var/log/milter +mkdir $RPM_BUILD_ROOT/var/log/milter/save +cp bms.py milter.cfg $RPM_BUILD_ROOT/var/log/milter + +# logfile rotation +mkdir -p $RPM_BUILD_ROOT/etc/logrotate.d +cat >$RPM_BUILD_ROOT/etc/logrotate.d/milter <<'EOF' +/var/log/milter/milter.log { + copytruncate + compress +} +EOF + +# purge saved defanged message copies +mkdir -p $RPM_BUILD_ROOT/etc/cron.daily +cat >$RPM_BUILD_ROOT/etc/cron.daily/milter <<'EOF' +#!/bin/sh + +find /var/log/milter/save -mtime +7 | xargs -r rm +EOF +chmod a+x $RPM_BUILD_ROOT/etc/cron.daily/milter + +%ifos aix4.1 +cat >$RPM_BUILD_ROOT/var/log/milter/start.sh <<'EOF' +#!/bin/sh +cd /var/log/milter +# uncomment to enable sgmlop if installed +#export PYTHONPATH=/usr/local/lib/python2.1/site-packages +exec /usr/local/bin/python bms.py >>milter.log 2>&1 +EOF +%else +cat >$RPM_BUILD_ROOT/var/log/milter/start.sh <<'EOF' +#!/bin/sh +cd /var/log/milter +exec >>milter.log 2>&1 +%{python} bms.py & +echo $! >/var/run/milter/milter.pid +EOF +mkdir -p $RPM_BUILD_ROOT/etc/rc.d/init.d +cp milter.%{sysvinit} $RPM_BUILD_ROOT/etc/rc.d/init.d/milter +ed $RPM_BUILD_ROOT/etc/rc.d/init.d/milter <<'EOF' +/^python=/ +c +python="%{python}" +. +w +q +EOF +%endif +chmod a+x $RPM_BUILD_ROOT/var/log/milter/start.sh + +mkdir -p $RPM_BUILD_ROOT/var/run/milter + +%ifos aix4.1 +%post +mkssys -s milter -p /var/log/milter/start.sh -u 25 -S -n 15 -f 9 -G mail || : + +%preun +if [ $1 = 0 ]; then + rmssys -s milter || : +fi +%endif + +%clean +rm -rf $RPM_BUILD_ROOT + +%files -f INSTALLED_FILES +%defattr(-,root,root) +%doc README NEWS TODO CREDITS sample.py +/etc/logrotate.d/milter +/etc/cron.daily/milter +%ifos aix4.1 +%defattr(-,smmsp,mail) +%else +/etc/rc.d/init.d/milter +%defattr(-,mail,mail) +%endif +%dir /var/log/milter +%dir /var/run/milter +%dir /var/log/milter/save +%config /var/log/milter/start.sh +%config /var/log/milter/bms.py +%config /var/log/milter/milter.cfg + +%changelog +* Mon Apr 05 2004 Stuart Gathman 0.6.8-1 +- Don't report spoofed unless rcpt looks like SRS +- Check for bounce with multiple rcpts +- Make dspam see Received-SPF headers +- Make sysv init work with RH9 +* Thu Mar 25 2004 Stuart Gathman 0.6.7-3 +- Forgot to make spf_reject_neutral global in bms.py +* Wed Mar 24 2004 Stuart Gathman 0.6.7-2 +- Defang message/rfc822 content_type with boundary +- Support SPF delegation +- Reject neutral SPF result for selected domains +* Tue Mar 23 2004 Stuart Gathman 0.6.7-1 +- SRS forgery check. Detect thread resource starvation. +- Properly remove local socket with explicit type. +- Decode obfuscated subject headers. +* Wed Mar 11 2004 Stuart Gathman 0.6.6-2 +- init script bug with python2.3 +* Wed Mar 10 2004 Stuart Gathman 0.6.6-1 +- SPF checking, hello blacklist +* Mon Mar 08 2004 Stuart Gathman 0.6.5-2 +- memory leak in envfrom and envrcpt +* Mon Mar 01 2004 Stuart Gathman 0.6.5-1 +- progress notification +- memory leak in connect +- trusted relay +* Thu Feb 19 2004 Stuart Gathman 0.6.4-2 +- smart alias wildcard patch, compile for sendmail-8.12 +* Thu Dec 04 2003 Stuart Gathman 0.6.4-1 +- many fixes for dspam support +* Wed Oct 22 2003 Stuart Gathman 0.6.3 +- dspam SCREEN feature +- streamline dspam false positive handling +* Mon Sep 01 2003 Stuart Gathman 0.6.1 +- Full dspam support added +* Mon Aug 26 2003 Stuart Gathman +- Use New email module +* Fri Jun 27 2003 Stuart Gathman +- Add dspam module diff --git a/miltermodule.c b/miltermodule.c new file mode 100644 index 0000000..54c08d2 --- /dev/null +++ b/miltermodule.c @@ -0,0 +1,1148 @@ +/* Copyright (C) 2001 James Niemira (niemira@colltech.com, urmane@urmane.org) + * + * 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + * + * milterContext object and thread interface contributed by + * Stuart D. Gathman + */ + +/* This is a Python extension to use Sendmail's libmilter functionality. + It is built using distutils. To install it: + +# python setup.py install + + For additional options: + +$ python setup.py help + + You may need to add additional libraries to setup.py. For instance, + Solaris2.6 requires + + libraries=["milter","smutil","resolv"] + + * $Log$ + * Revision 2.27 2004/04/06 03:19:59 stuart + * Release 0.6.8 + * + * Revision 2.26 2004/03/04 21:43:06 stuart + * Fix memory leak by removing unused dynamic template buffer, + * thanks again to Alexander Kourakos. + * + * Revision 2.25 2004/03/01 19:45:03 stuart + * Release 0.6.5 + * + * Revision 2.24 2004/03/01 18:56:50 stuart + * Support progress reporting. + * + * Revision 2.23 2004/03/01 18:36:09 stuart + * Plug memory leak. Thanks to Alexander Kourakos. + * + * Revision 2.22 2003/11/02 03:01:46 stuart + * Adjust SMTP error codes after careful reading of standard. + * + * Revision 2.21 2003/06/24 19:57:04 stuart + * Allow removing a python milter callback by setting to None. + * + * Revision 2.20 2003/02/13 17:08:57 stuart + * IPV6 support + * + * Revision 2.19 2003/02/13 16:58:29 stuart + * Support passing None to setreply and chgheader. + * + * Revision 2.18 2002/12/11 16:44:06 stuart + * Support QUARANTINE if supported by libmilter. + * + * Revision 2.17 2002/04/18 20:20:35 stuart + * Fix for NULL hostaddr in connect callback from Jason Erickson. + * + * Revision 2.16 2001/09/26 13:29:09 stuart + * sa_len not supported by linux. + * + * Revision 2.15 2001/09/25 17:28:40 stuart + * Copyrights, documentation, release 0.3.1 + * + * Revision 2.14 2001/09/25 00:36:57 stuart + * Pass hostaddr to python code in format used by standard socket module. + * + * Revision 2.13 2001/09/24 23:44:55 stuart + * Return old callback from setcallback functions. + * + * Revision 2.12 2001/09/24 20:02:30 stuart + * Remove redundant setpriv + * + * Revision 2.11 2001/09/23 22:26:35 stuart + * Update docs. Streamline Milter.py + * update testbms.py to reflect actual sendmail behaviour with multiple + * messages per connection. + * + * Revision 2.10 2001/09/22 15:33:42 stuart + * More doc comment updates. + * + * Revision 2.9 2001/09/22 14:52:27 stuart + * Actually return retval in _generic_return. + * Go over doc comments. + * + * Revision 2.8 2001/09/22 01:59:32 stuart + * Prevent reentrant call of milter_main, which libmilter doesn't support. + * + * Revision 2.7 2001/09/22 01:47:37 stuart + * Forgot to set milter interp. + * + * Revision 2.6 2001/09/22 01:23:53 stuart + * Added proper threading after research in python docs. + * + * Revision 2.5 2001/09/21 20:08:51 stuart + * Release 0.2.3 + * + * Revision 2.4 2001/09/20 16:18:16 stuart + * libmilter checks in_eom state, so we don't have to. + * + * Revision 2.3 2001/09/19 06:02:33 stuart + * Make more stuff static. + * + * Revision 2.1 2001/09/19 04:24:13 stuart + * Use extension type to track context in python. + * + * Revision 1.4 2001/09/18 18:48:28 stuart + * clear private data reference in _clear_context + * + * Revision 1.3 2001/09/15 04:19:37 stuart + * nasty off by 1 mem overwrite bugs in wrap_env + * generic_set_callback + * + * Revision 1.2 2001/09/15 03:15:39 stuart + * several bugs fixed, works smoothly + * + */ + +#include +#include +#include +#include + +/* See if we have IPv4 and/or IPv6 support in this OS and in + * libmilter. We need to make several macro tests because some OS's + * may define some if IPv6 is only partially supported, and we may + * have a sendmail without IPv4 (compiled for IPv6-only). + */ +#ifdef SMFIA_INET +#ifdef AF_INET +#define HAVE_IPV4_SUPPORT /* use this for #ifdef's later on */ +#endif +#endif + +#ifdef SMFIA_INET6 +#ifdef AF_INET6 +#ifdef IN6ADDR_ANY_INIT +#ifdef INET6_ADDRSTRLEN +#define HAVE_IPV6_SUPPORT /* use this for #ifdef's later on */ +/* Now see if it supports the RFC-2553 socket's API spec. Early + * IPv6 "prototype" implementations existed before the RFC was + * published. Unfortunately I know of now good way to do this + * other than with OS-specific tests. + */ +#ifdef linux +#define HAVE_IPV6_RFC2553 +#include +#endif +#ifdef __HPUX +/* only HP-UX 11.1 or greater supports IPv6 */ +#define HAVE_IPV6_RFC2553 +#endif + +#endif +#endif +#endif +#endif + + +/* Yes, these are static. If you need multiple different callbacks, */ +/* it's cleaner to use multiple filters. */ +static PyObject *connect_callback = NULL; +static PyObject *helo_callback = NULL; +static PyObject *envfrom_callback = NULL; +static PyObject *envrcpt_callback = NULL; +static PyObject *header_callback = NULL; +static PyObject *eoh_callback = NULL; +static PyObject *body_callback = NULL; +static PyObject *eom_callback = NULL; +static PyObject *abort_callback = NULL; +static PyObject *close_callback = NULL; + +staticforward struct smfiDesc description; /* forward declaration */ + +static PyObject *MilterError; +/* The interpreter instance that called milter.main */ +static PyInterpreterState *interp; + +staticforward PyTypeObject milter_ContextType; + +typedef struct { + PyObject_HEAD + SMFICTX *ctx; /* libmilter thread state */ + PyObject *priv; /* user python object */ + PyThreadState *t; /* python thread state */ +} milter_ContextObject; + +/* Return a borrowed reference to the python Context. Create a + new Context if needed. The new Python Context is owned by + the SMFICTX. The python interpreter is locked on successful + return, otherwise not. */ +static milter_ContextObject * +_get_context(SMFICTX *ctx) { + milter_ContextObject *self = smfi_getpriv(ctx); + if (self) { + /* Can't pass on exception since we are called from libmilter */ + if (self->ctx != ctx) return NULL; + PyEval_AcquireThread(self->t); + } + else { + PyThreadState *t = PyThreadState_New(interp); + if (t == NULL) return NULL; + PyEval_AcquireThread(t); /* lock interp */ + self = PyObject_New(milter_ContextObject,&milter_ContextType); + if (!self) { + /* Can't pass on exception since we are called from libmilter */ + PyErr_Clear(); + PyThreadState_Clear(t); + PyEval_ReleaseThread(t); + PyThreadState_Delete(t); + return NULL; + } + self->t = t; + self->ctx = ctx; + Py_INCREF(Py_None); + self->priv = Py_None; /* User Python object */ + smfi_setpriv(ctx, self); + } + return self; +} + +/* Find the SMFICTX from a Python Context. The interpreter must be locked. */ +static SMFICTX * +_find_context(PyObject *c) { + SMFICTX *ctx = NULL; + if (c->ob_type == &milter_ContextType) { + milter_ContextObject *self = (milter_ContextObject *)c; + ctx = self->ctx; + if (smfi_getpriv(ctx) != self) + ctx = NULL; + } + if (ctx == NULL) + PyErr_SetString(MilterError, "bad context"); + return ctx; +} + +/* Release the Python Context for a SMFICTX. */ +static void +_clear_context(SMFICTX *ctx) { + milter_ContextObject *self = smfi_getpriv(ctx); + if (self) { + PyThreadState *t = self->t; + PyEval_AcquireThread(t); + self->t = 0; + self->ctx = 0; + smfi_setpriv(ctx,0); + Py_DECREF(self); + PyThreadState_Clear(t); + PyEval_ReleaseThread(t); + PyThreadState_Delete(t); + } +} + +static void +milter_Context_dealloc(PyObject *s) { + milter_ContextObject *self = (milter_ContextObject *)s; + SMFICTX *ctx = self->ctx; + if (ctx) { + /* Should never happen. If libmilter closes SMFICTX first, then + ctx will be 0. Otherwise, SMFICTX will still hold a reference + to the ContextObject. But if it does, make sure SMFICTX can't + reach us anymore. */ + smfi_setpriv(ctx,0); + } + Py_DECREF(self->priv); + PyObject_DEL(self); +} + +/* Throw an exception if an smfi call failed, otherwise return PyNone. */ +static PyObject * +_generic_return(int val, char *errstr) { + if (val == MI_SUCCESS) { + Py_INCREF(Py_None); + return Py_None; + } else { + PyErr_SetString(MilterError, errstr); + return NULL; + } +} + +static PyObject * +_thread_return(PyThreadState *t,int val,char *errstr) { + PyEval_RestoreThread(t); /* lock interpreter again */ + return _generic_return(val,errstr); +} + +static char milter_set_flags__doc__[] = +"set_flags(int) -> None\n\ +Set flags for filter capabilities; OR of one or more of:\n\ +ADDHDRS - filter may add headers\n\ +CHGBODY - filter may replace body\n\ +ADDRCPT - filter may add recipients\n\ +DELRCPT - filter may delete recipients\n\ +CHGHDRS - filter may change/delete headers"; + +static PyObject * +milter_set_flags(PyObject *self, PyObject *args) { + if (!PyArg_ParseTuple(args, "i", &description.xxfi_flags)) return NULL; + Py_INCREF(Py_None); + return Py_None; +} + +static PyObject * +generic_set_callback(PyObject *args,char *t,PyObject **cb) { + PyObject *callback; + PyObject *oldval; + + if (!PyArg_ParseTuple(args, t, &callback)) return NULL; + if (callback == Py_None) + callback = 0; + else { + if (!PyCallable_Check(callback)) { + PyErr_SetString(PyExc_TypeError, "parameter must be callable"); + return NULL; + } + Py_INCREF(callback); + } + oldval = *cb; + *cb = callback; + if (oldval) + return oldval; + Py_INCREF(Py_None); + return Py_None; +} + +static char milter_set_connect_callback__doc__[] = +"set_connect_callback(Function) -> None\n\ +Sets the Python function invoked when a connection is made to sendmail.\n\ +Function takes args (ctx, hostname, integer, hostaddr) -> int\n\ +ctx -> milterContext for connection, also on remaining callbacks\n\ +hostname -> String - the connecting remote hostname\n\ +integer -> int - the protocol family, one of socket.AF_* values\n\ +hostaddr -> ? - the connecting host address in format used by socket:\n\ + for unix -> pathname - like '/tmp/sockets/s24823'\n\ + for inet -> (ipaddress, port) - like ('10.1.2.3',3701)\n\ + for inet6 -> (ip6address, port, flowlabel, scope) - like ('fec0:0:0:2::4e', 3701, 0, 5)\n\ +\n\ +The return value on this and remaining callbacks should be one of:\n\ +CONTINUE - continue processing\n\ +REJECT - sendmail refuses to accept any more data for message\n\ +ACCEPT - sendmail accepts the message\n\ +DISCARD - sendmail accepts the message and discards it\n\ +TEMPFAIL - milter problem, sendmail will try again later\n\ +\n\ +A python exception encountered in a callback will return TEMPFAIL."; + +static PyObject * +milter_set_connect_callback(PyObject *self, PyObject *args) { + return generic_set_callback(args, + "O:set_connect_callback", &connect_callback); +} + +static char milter_set_helo_callback__doc__[] = +"set_helo_callback(Function) -> None\n\ +Sets the Python function invoked upon SMTP HELO.\n\ +Function takes args (ctx, hostname) -> int\n\ +hostname -> String - the name given with the helo command."; + +static PyObject * +milter_set_helo_callback(PyObject *self, PyObject *args) { + return generic_set_callback(args, "O:set_helo_callback", &helo_callback); +} + +static char milter_set_envfrom_callback__doc__[] = +"set_envfrom_callback(Function) -> None\n\ +Sets the Python function invoked on envelope from.\n\ +Function takes args (ctx, from, *str) -> int\n\ +from -> sender\n\ +str -> Tuple of additional parameters defined by ESMTP."; + +static PyObject * +milter_set_envfrom_callback(PyObject *self, PyObject *args) { + return generic_set_callback(args, "O:set_envfrom_callback", + &envfrom_callback); +} + +static char milter_set_envrcpt_callback__doc__[] = +"set_envrcpt_callback(Function) -> None\n\ +Sets the Python function invoked on each envelope recipient.\n\ +Function takes args (ctx, rcpt, *str) -> int\n\ +tcpt -> string - recipient\n\ +str -> Tuple of additional parameters defined by ESMTP."; + +static PyObject * +milter_set_envrcpt_callback(PyObject *self, PyObject *args) { + return generic_set_callback(args, "O:set_envrcpt_callback", + &envrcpt_callback); +} + +static char milter_set_header_callback__doc__[] = +"set_header_callback(Function) -> None\n\ +Sets the Python function invoked on each message header.\n\ +Function takes args (ctx, field, value) ->int\n\ +field -> String - the header\n\ +value -> String - the header's value"; + +static PyObject * +milter_set_header_callback(PyObject *self, PyObject *args) { + return generic_set_callback(args, "O:set_header_callback", + &header_callback); +} + +static char milter_set_eoh_callback__doc__[] = +"set_eoh_callback(Function) -> None\n\ +Sets the Python function invoked at end of header.\n\ +Function takes args (ctx) -> int"; + +static PyObject * +milter_set_eoh_callback(PyObject *self, PyObject *args) { + return generic_set_callback(args, "O:set_eoh_callback", &eoh_callback); +} + +static char milter_set_body_callback__doc__[] = +"set_body_callback(Function) -> None\n\ +Sets the Python function invoked for each body chunk. There may\n\ +be multiple body chunks passed to the filter. End-of-lines are\n\ +represented as received from SMTP (normally Carriage-Return/Line-Feed).\n\ +Function takes args (ctx, chunk) -> int\n\ +chunk -> String - body data"; + +static PyObject * +milter_set_body_callback(PyObject *self, PyObject *args) { + return generic_set_callback(args, "O:set_body_callback", &body_callback); +} + +static char milter_set_eom_callback__doc__[] = +"set_eom_callback(Function) -> None\n\ +Sets the Python function invoked at end of message.\n\ +This routine is the only place where special operations\n\ +such as modifying the message header, body, or\n\ +envelope can be used.\n\ +Function takes args (ctx) -> int"; + +static PyObject * +milter_set_eom_callback(PyObject *self, PyObject *args) { + return generic_set_callback(args, "O:set_eom_callback", &eom_callback); +} + +static char milter_set_abort_callback__doc__[] = +"set_abort_callback(Function) -> None\n\ +Sets the Python function invoked if message is aborted\n\ +outside of the control of the filter, for example,\n\ +if the SMTP sender issues an RSET command. If the \n\ +abort callback is called, the eom callback will not be\n\ +called and vice versa.\n\ +Function takes args (ctx) -> int"; + +static PyObject * +milter_set_abort_callback(PyObject *self, PyObject *args) { + return generic_set_callback(args, "O:set_abort_callback", &abort_callback); +} + +static char milter_set_close_callback__doc__[] = +"set_close_callback(Function) -> None\n\ +Sets the Python function invoked at end of the connection. This\n\ +is called on close even if the previous mail transaction was aborted.\n\ +Function takes args (ctx) -> int"; + +static PyObject * +milter_set_close_callback(PyObject *self, PyObject *args) { + return generic_set_callback(args, "O:set_close_callback", &close_callback); +} + +/** Report and clear any python exception before returning to libmilter. + The interpreter is locked when we are called, and we unlock it. */ +static int _report_exception(milter_ContextObject *self) { + if (PyErr_Occurred()) { + PyErr_Print(); + PyErr_Clear(); /* must clear since not returning to python */ + PyEval_ReleaseThread(self->t); + smfi_setreply(self->ctx, "451", "4.3.0", "Filter failure"); + return SMFIS_TEMPFAIL; + } + PyEval_ReleaseThread(self->t); + return SMFIS_CONTINUE; +} + +/* Return to libmilter. The ctx must have been initialized or + checked by a successfull call to _get_context(), thereby locking + the interpreter. */ +static int +_generic_wrapper(milter_ContextObject *self, PyObject *cb, PyObject *arglist) { + PyObject *result; + int retval; + + if (arglist == NULL) return _report_exception(self); + result = PyEval_CallObject(cb, arglist); + Py_DECREF(arglist); + if (result == NULL) return _report_exception(self); + retval = PyInt_AsLong(result); + Py_DECREF(result); + if (PyErr_Occurred()) return _report_exception(self); + PyEval_ReleaseThread(self->t); + return retval; +} + +/* Create a string object representing an IP address. + This is always a string of the form 'dd.dd.dd.dd' (with variable + size numbers). Copied from standard socket module. */ + +static PyObject * +makeipaddr(struct sockaddr_in *addr) { + long x = ntohl(addr->sin_addr.s_addr); + char buf[100]; + sprintf(buf, "%d.%d.%d.%d", + (int) (x>>24) & 0xff, (int) (x>>16) & 0xff, + (int) (x>> 8) & 0xff, (int) (x>> 0) & 0xff); + return PyString_FromString(buf); +} + +#ifdef HAVE_IPV6_SUPPORT +static PyObject * +makeip6addr(struct sockaddr_in6 *addr) { + char buf[100]; /* must be at least INET6_ADDRSTRLEN + 1 */ + const char *s = inet_ntop(AF_INET6, &addr->sin6_addr, buf, sizeof buf); + if (s) return PyString_FromString(s); + return PyString_FromString("inet6:unknown"); +} +#endif + +/* These are wrapper functions to call the Python callbacks for each event */ +static int +milter_wrap_connect(SMFICTX *ctx, char *hostname, _SOCK_ADDR *hostaddr) { + PyObject *arglist; + milter_ContextObject *c; + if (connect_callback == NULL) return SMFIS_CONTINUE; + c = _get_context(ctx); + if (!c) return SMFIS_TEMPFAIL; + if (hostaddr != NULL) { + switch (hostaddr->sa_family) { + case AF_INET: + { struct sockaddr_in *sa = (struct sockaddr_in *)hostaddr; + PyObject *ipaddr_obj = makeipaddr(sa); + arglist = Py_BuildValue("(Osh(Oi))", c, hostname, hostaddr->sa_family, + ipaddr_obj, ntohs(sa->sin_port)); + Py_DECREF(ipaddr_obj); + } + break; + case AF_UNIX: + arglist = Py_BuildValue("(Oshs)", c, hostname, hostaddr->sa_family, + hostaddr->sa_data); + break; +#ifdef HAVE_IPV6_SUPPORT + case AF_INET6: + { struct sockaddr_in6 *sa = (struct sockaddr_in6 *)hostaddr; + PyObject *ip6addr_obj = makeip6addr(sa); + long scope_id = 0; +#ifdef HAVE_IPV6_RFC2553 + scope_id = ntohl(sa->sin6_scope_id); +#endif + arglist = Py_BuildValue("(Osh(Oiii))", c, hostname, hostaddr->sa_family, + ip6addr_obj, + ntohs(sa->sin6_port), + ntohl(sa->sin6_flowinfo), + scope_id); + Py_DECREF(ip6addr_obj); + } + break; +#endif + default: + arglist = Py_BuildValue("(OshO)", c, hostname, hostaddr->sa_family, + Py_None); + } + } + else + arglist = Py_BuildValue("(OshO)", c, hostname, 0, Py_None); + return _generic_wrapper(c, connect_callback, arglist); +} + +static int +milter_wrap_helo(SMFICTX *ctx, char *helohost) { + PyObject *arglist; + milter_ContextObject *c; + + if (helo_callback == NULL) return SMFIS_CONTINUE; + c = _get_context(ctx); + if (!c) return SMFIS_TEMPFAIL; + arglist = Py_BuildValue("(Os)", c, helohost); + return _generic_wrapper(c, helo_callback, arglist); +} + +static int +generic_env_wrapper(SMFICTX *ctx, PyObject*cb, char **argv, const char *name) { + PyObject *arglist; + milter_ContextObject *self; + int count = 0; + int i; + char **p = argv; + + if (cb == NULL) return SMFIS_CONTINUE; + + self = _get_context(ctx); + if (!self) return SMFIS_TEMPFAIL; + + /* Count how many strings we've been passed. */ + while (*p++ != NULL) count++; + /* how to build the value in steps? Cheat by copying from */ + /* Python/modsupport.c do_mktuple() and do_mkvalue() */ + if ((arglist = PyTuple_New(count+1)) == NULL) + return _report_exception(self); + /* Add in the context first */ + Py_INCREF(self); /* PyTuple_SetItem takes over reference */ + PyTuple_SetItem(arglist, 0, (PyObject *)self); + /* Now do all the strings */ + for (i=0;it to NULL. + However, first we make it work. */ + _clear_context(ctx); + return r; +} + +static char milter_register__doc__[] = +"register(name) -> None\n\ +Registers the milter name with current callbacks, and flags.\n\ +Required before main() is called."; + +static PyObject * +milter_register(PyObject *self, PyObject *args) { + if (!PyArg_ParseTuple(args, "s:register", &description.xxfi_name)) + return NULL; + return _generic_return(smfi_register(description), "cannot register"); +} + +static char milter_main__doc__[] = +"main() -> None\n\ +Main milter routine. Set any callbacks, and flags desired, then call\n\ +setconn(), then call register(name), and finally call main()."; + +static PyObject * +milter_main(PyObject *self, PyObject *args) { + PyThreadState *_main; + PyObject *o; + if (!PyArg_ParseTuple(args, ":main")) return NULL; + if (interp != NULL) { + PyErr_SetString(MilterError,"milter module in use"); + return NULL; + } + /* libmilter requires thread support */ + PyEval_InitThreads(); + /* let other threads run while in smfi_main() */ + interp = PyThreadState_Get()->interp; + _main = PyEval_SaveThread(); /* must be done before smfi_main() */ + o = _thread_return(_main,smfi_main(), "cannot run main"); + interp = NULL; + return o; +} + +static char milter_setdbg__doc__[] = +"setdbg(int) -> None\n\ +Sets debug level in sendmail/libmilter source. Dubious usefulness."; + +static PyObject * +milter_setdbg(PyObject *self, PyObject *args) { + int val; + if (!PyArg_ParseTuple(args, "i:setdbg", &val)) return NULL; + return _generic_return(smfi_setdbg(val), "cannot set debug value"); +} + +static char milter_settimeout__doc__[] = +"settimeout(int) -> None\n\ +Set the time (in seconds) that sendmail will wait before\n\ +considering this filter dead."; + +static PyObject * +milter_settimeout(PyObject *self, PyObject *args) { + int val; + + if (!PyArg_ParseTuple(args, "i:settimeout", &val)) return NULL; + return _generic_return(smfi_settimeout(val), "cannot set timeout"); +} + +static char milter_setconn__doc__[] = +"setconn(filename) -> None\n\ +Sets the pathname to the unix, inet, or inet6 socket that\n\ +sendmail will use to communicate with this filter. By default,\n\ +a unix domain socket is used. It must not exist,\n\ +and sendmail will throw warnings if, eg, the file is under a\n\ +group or world writable directory. This call is \n\ +mandatory, and is invoked before register() and main().\n\ + setconn('unix:/var/run/pythonfilter')\n\ + setconn('inet:8800') # listen on ANY interface\n\ + setconn('inet:7871@publichost')\n\ + setconn('inet6:8020')"; + +static PyObject * +milter_setconn(PyObject *self, PyObject *args) { + char *str; + if (!PyArg_ParseTuple(args, "s:setconn", &str)) return NULL; + return _generic_return(smfi_setconn(str), "cannot set connection"); +} + +static char milter_stop__doc__[] = +"stop() -> None\n\ +This function appears to be a controlled method to tell sendmail to\n\ +stop using this filter. It will close the socket."; + +static PyObject * +milter_stop(PyObject *self, PyObject *args) { + PyThreadState *t; + if (!PyArg_ParseTuple(args, ":stop")) return NULL; + t = PyEval_SaveThread(); + return _thread_return(t,smfi_stop(), "cannot stop"); +} + +static char milter_getsymval__doc__[] = +"getsymval(String) -> String\n\ +Returns a symbol's value. Context-dependent, and unclear from the dox."; + +static PyObject * +milter_getsymval(PyObject *self, PyObject *args) { + char *str; + SMFICTX *ctx; + + if (!PyArg_ParseTuple(args, "s:getsymval", &str)) return NULL; + ctx = _find_context(self); + if (ctx == NULL) return NULL; + return Py_BuildValue("s", smfi_getsymval(ctx, str)); +} + +static char milter_setreply__doc__[] = +"setreply(rcode, xcode, message) -> None\n\ +Sets the specific reply code to be used in response\n\ +to the active command.\n\ +rcode - The three-digit (RFC 821) SMTP reply code to be returned\n\ +xcode - The extended (RFC 2034) reply code\n\ +message - The text part of the SMTP reply\n\ +These should all be strings."; + +static PyObject * +milter_setreply(PyObject *self, PyObject *args) { + char *rcode; + char *xcode; + char *message; + SMFICTX *ctx; + if (!PyArg_ParseTuple(args, "szz:setreply", &rcode, &xcode, &message)) + return NULL; + ctx = _find_context(self); + if (ctx == NULL) return NULL; + return _generic_return(smfi_setreply(ctx, rcode, xcode, message), + "cannot set reply"); +} + +static char milter_addheader__doc__[] = +"addheader(field, value) -> None\n\ +Add a header to the message. This header is not passed to other\n\ +filters. It is not checked for standards compliance;\n\ +the mail filter must ensure that no protocols are violated\n\ +as a result of adding this header.\n\ +field - header field name\n\ +value - header field value\n\ +Both are strings. This function can only be called from the EOM callback."; + +static PyObject * +milter_addheader(PyObject *self, PyObject *args) { + char *headerf; + char *headerv; + SMFICTX *ctx; + PyThreadState *t; + + if (!PyArg_ParseTuple(args, "ss:addheader", &headerf, &headerv)) return NULL; + ctx = _find_context(self); + if (ctx == NULL) return NULL; + t = PyEval_SaveThread(); + return _thread_return(t,smfi_addheader(ctx, headerf, headerv), + "cannot add header"); +} + +static char milter_chgheader__doc__[] = +"chgheader(field, int, value) -> None\n\ +Change/delete a header in the message. \n\ +It is not checked for standards compliance; the mail filter\n\ +must ensure that no protocols are violated as a result of adding this header.\n\ +field - header field name\n\ +int - the Nth occurence of this header\n\ +value - header field value\n\ +field and value are strings.\n\ +This function can only be called from the EOM callback."; + +static PyObject * +milter_chgheader(PyObject *self, PyObject *args) { + char *headerf; + int index; + char *headerv; + SMFICTX *ctx; + PyThreadState *t; + + if (!PyArg_ParseTuple(args, "siz:chgheader", &headerf, &index, &headerv)) + return NULL; + ctx = _find_context(self); + if (ctx == NULL) return NULL; + t = PyEval_SaveThread(); + return _thread_return(t,smfi_chgheader(ctx, headerf, index, headerv), + "cannot change header"); +} + +static char milter_addrcpt__doc__[] = +"addrcpt(string) -> None\n\ +Add a recipient to the envelope. It must be in the same format\n\ +as is passed to the envrcpt callback in the first tuple element.\n\ +This function can only be called from the EOM callback."; + +static PyObject * +milter_addrcpt(PyObject *self, PyObject *args) { + char *rcpt; + SMFICTX *ctx; + PyThreadState *t; + + if (!PyArg_ParseTuple(args, "s:addrcpt", &rcpt)) return NULL; + ctx = _find_context(self); + if (ctx == NULL) return NULL; + t = PyEval_SaveThread(); + return _thread_return(t,smfi_addrcpt(ctx, rcpt), "cannot add recipient"); +} + +static char milter_delrcpt__doc__[] = +"delrcpt(string) -> None\n\ +Delete a recipient from the envelope.\n\ +This function can only be called from the EOM callback."; + +static PyObject * +milter_delrcpt(PyObject *self, PyObject *args) { + char *rcpt; + SMFICTX *ctx; + PyThreadState *t; + + if (!PyArg_ParseTuple(args, "s:delrcpt", &rcpt)) return NULL; + ctx = _find_context(self); + if (ctx == NULL) return NULL; + t = PyEval_SaveThread(); + return _thread_return(t,smfi_delrcpt(ctx, rcpt), + "cannot delete recipient"); +} + +static char milter_replacebody__doc__[] = +"replacebody(string) -> None\n\ +Replace the body of the message. This routine may be called multiple\n\ +times if the body is longer than convenient to send in one call. End of\n\ +line should be represented as Carriage-Return/Line Feed. This function\n\ +can only be called from the EOM callback."; + +static PyObject * +milter_replacebody(PyObject *self, PyObject *args) { + char *bodyp; + int bodylen; + SMFICTX *ctx; + PyThreadState *t; + + if (!PyArg_ParseTuple(args, "s#", &bodyp, &bodylen)) return NULL; + ctx = _find_context(self); + if (ctx == NULL) return NULL; + t = PyEval_SaveThread(); + return _thread_return(t,smfi_replacebody(ctx, bodyp, bodylen), + "cannot replace message body"); +} + +static char milter_setpriv__doc__[] = +"setpriv(object) -> object\n\ +Associates any Python object with this context, and returns\n\ +the old value or None. Use this to\n\ +provide thread-safe storage for data instead of using global variables\n\ +for things like filenames, etc. This function stores only one object\n\ +per context, but that object can in turn store many others."; + +static PyObject * +milter_setpriv(PyObject *self, PyObject *args) { + PyObject *o; + PyObject *old; + milter_ContextObject *s = (milter_ContextObject *)self; + + if (!PyArg_ParseTuple(args, "O:setpriv", &o)) return NULL; + /* PyArg_ParseTuple's O format does not increase the reference count on + the target. Since we're going to save it and almost certainly assign + to another object later, we incref it here, and only decref it in + the dealloc method. */ + Py_INCREF(o); + old = s->priv; + s->priv = o; + /* We return the old value. The caller will DECREF it if not used. */ + return old; +} + +static char milter_getpriv__doc__[] = +"getpriv() -> None\n\ +Returns the Python object associated with the current context (if any).\n\ +Use this in conjunction with setpriv to keep track of data in a thread-safe\n\ +manner."; + +static PyObject * +milter_getpriv(PyObject *self, PyObject *args) { + PyObject *o; + milter_ContextObject *s = (milter_ContextObject *)self; + + if (!PyArg_ParseTuple(args, ":getpriv")) return NULL; + o = s->priv; + Py_INCREF(o); + return o; +} + +#if _FFR_QUARANTINE +static char milter_quarantine__doc__[] = +"quarantine(string) -> None\n\ +Place the message in quarantine. A string with a description of the reason\n\ +is the only argument."; + +static PyObject * +milter_quarantine(PyObject *self, PyObject *args) { + char *reason; + SMFICTX *ctx; + PyThreadState *t; + + if (!PyArg_ParseTuple(args, "s:quarantine", &reason)) return NULL; + ctx = _find_context(self); + if (ctx == NULL) return NULL; + t = PyEval_SaveThread(); + return _thread_return(t,smfi_quarantine(ctx, reason), + "cannot quarantine message"); +} +#endif + +#if _FFR_SMFI_PROGRESS +static char milter_progress__doc__[] = +"progress() -> None\n\ +Notify the MTA that we are working on a message so it will reset timeouts."; + +static PyObject * +milter_progress(PyObject *self, PyObject *args) { + SMFICTX *ctx; + PyThreadState *t; + + if (!PyArg_ParseTuple(args, ":progress")) return NULL; + ctx = _find_context(self); + if (ctx == NULL) return NULL; + t = PyEval_SaveThread(); + return _thread_return(t,smfi_progress(ctx), "cannot notify progress"); +} +#endif + +static PyMethodDef context_methods[] = { + { "getsymval", milter_getsymval, METH_VARARGS, milter_getsymval__doc__}, + { "setreply", milter_setreply, METH_VARARGS, milter_setreply__doc__}, + { "addheader", milter_addheader, METH_VARARGS, milter_addheader__doc__}, + { "chgheader", milter_chgheader, METH_VARARGS, milter_chgheader__doc__}, + { "addrcpt", milter_addrcpt, METH_VARARGS, milter_addrcpt__doc__}, + { "delrcpt", milter_delrcpt, METH_VARARGS, milter_delrcpt__doc__}, + { "replacebody", milter_replacebody, METH_VARARGS, milter_replacebody__doc__}, + { "setpriv", milter_setpriv, METH_VARARGS, milter_setpriv__doc__}, + { "getpriv", milter_getpriv, METH_VARARGS, milter_getpriv__doc__}, +#if _FFR_QUARANTINE + { "quarantine", milter_quarantine, METH_VARARGS, milter_quarantine__doc__}, +#endif +#if _FFR_SMFI_PROGRESS + { "progress", milter_progress, METH_VARARGS, milter_progress__doc__}, +#endif + { NULL, NULL } +}; + +static PyObject * +milter_Context_getattr(PyObject *self, char *name) { + return Py_FindMethod(context_methods, self, name); +} + +static struct smfiDesc description = { /* Set some reasonable defaults */ + "pythonfilter", + SMFI_VERSION, + SMFI_CURR_ACTS, + milter_wrap_connect, + milter_wrap_helo, + milter_wrap_envfrom, + milter_wrap_envrcpt, + milter_wrap_header, + milter_wrap_eoh, + milter_wrap_body, + milter_wrap_eom, + milter_wrap_abort, + milter_wrap_close +}; + +static PyMethodDef milter_methods[] = { + { "set_flags", milter_set_flags, METH_VARARGS, milter_set_flags__doc__}, + { "set_connect_callback", milter_set_connect_callback, METH_VARARGS, milter_set_connect_callback__doc__}, + { "set_helo_callback", milter_set_helo_callback, METH_VARARGS, milter_set_helo_callback__doc__}, + { "set_envfrom_callback", milter_set_envfrom_callback, METH_VARARGS, milter_set_envfrom_callback__doc__}, + { "set_envrcpt_callback", milter_set_envrcpt_callback, METH_VARARGS, milter_set_envrcpt_callback__doc__}, + { "set_header_callback", milter_set_header_callback, METH_VARARGS, milter_set_header_callback__doc__}, + { "set_eoh_callback", milter_set_eoh_callback, METH_VARARGS, milter_set_eoh_callback__doc__}, + { "set_body_callback", milter_set_body_callback, METH_VARARGS, milter_set_body_callback__doc__}, + { "set_eom_callback", milter_set_eom_callback, METH_VARARGS, milter_set_eom_callback__doc__}, + { "set_abort_callback", milter_set_abort_callback, METH_VARARGS, milter_set_abort_callback__doc__}, + { "set_close_callback", milter_set_close_callback, METH_VARARGS, milter_set_close_callback__doc__}, + { "register", milter_register, METH_VARARGS, milter_register__doc__}, + { "main", milter_main, METH_VARARGS, milter_main__doc__}, + { "setdbg", milter_setdbg, METH_VARARGS, milter_setdbg__doc__}, + { "settimeout", milter_settimeout, METH_VARARGS, milter_settimeout__doc__}, + { "setconn", milter_setconn, METH_VARARGS, milter_setconn__doc__}, + { "stop", milter_stop, METH_VARARGS, milter_stop__doc__}, + { NULL, NULL } +}; + +static PyTypeObject milter_ContextType = { + PyObject_HEAD_INIT(&PyType_Type) + 0, + "milterContext", + sizeof(milter_ContextObject), + 0, + milter_Context_dealloc, /* tp_dealloc */ + 0, /* tp_print */ + milter_Context_getattr, /* tp_getattr */ + 0, /* tp_setattr */ + 0, /* tp_compare */ + 0, /* tp_repr */ + 0, /* tp_as_number */ + 0, /* tp_as_sequence */ + 0, /* tp_as_mapping */ + 0, /* tp_hash */ + 0, /* tp_call */ + 0, /* tp_str */ + 0, /* tp_getattro */ + 0, /* tp_setattro */ + 0, /* tp_as_buffer */ + Py_TPFLAGS_DEFAULT, /* tp_flags */ +}; + +static char milter_documentation[] = +"This module interfaces with Sendmail's libmilter functionality,\n\ +allowing one to write email filters directly in Python.\n\ +Libmilter is currently marked FFR, and needs to be explicitly installed.\n\ +See /libmilter/README for details on setting it up.\n"; + +void +initmilter(void) { + PyObject *m, *d; + + m = Py_InitModule4("milter", milter_methods, milter_documentation, + (PyObject*)NULL, PYTHON_API_VERSION); + d = PyModule_GetDict(m); + MilterError = PyErr_NewException("milter.error", NULL, NULL); + PyDict_SetItemString(d,"error", MilterError); + PyDict_SetItemString(d,"SUCCESS", PyInt_FromLong((long) MI_SUCCESS)); + PyDict_SetItemString(d,"FAILURE", PyInt_FromLong((long) MI_FAILURE)); + PyDict_SetItemString(d,"VERSION", PyInt_FromLong((long) SMFI_VERSION)); + PyDict_SetItemString(d,"ADDHDRS", PyInt_FromLong((long) SMFIF_ADDHDRS)); + PyDict_SetItemString(d,"CHGBODY", PyInt_FromLong((long) SMFIF_CHGBODY)); + PyDict_SetItemString(d,"MODBODY", PyInt_FromLong((long) SMFIF_MODBODY)); + PyDict_SetItemString(d,"ADDRCPT", PyInt_FromLong((long) SMFIF_ADDRCPT)); + PyDict_SetItemString(d,"DELRCPT", PyInt_FromLong((long) SMFIF_DELRCPT)); + PyDict_SetItemString(d,"CHGHDRS", PyInt_FromLong((long) SMFIF_CHGHDRS)); + PyDict_SetItemString(d,"V1_ACTS", PyInt_FromLong((long) SMFI_V1_ACTS)); + PyDict_SetItemString(d,"V2_ACTS", PyInt_FromLong((long) SMFI_V2_ACTS)); + PyDict_SetItemString(d,"CURR_ACTS", PyInt_FromLong((long) SMFI_CURR_ACTS)); +#ifdef SMFIF_QUARANTINE + PyDict_SetItemString(d,"QUARANTINE",PyInt_FromLong((long)SMFIF_QUARANTINE)); +#endif + PyDict_SetItemString(d,"CONTINUE", PyInt_FromLong((long) SMFIS_CONTINUE)); + PyDict_SetItemString(d,"REJECT", PyInt_FromLong((long) SMFIS_REJECT)); + PyDict_SetItemString(d,"DISCARD", PyInt_FromLong((long) SMFIS_DISCARD)); + PyDict_SetItemString(d,"ACCEPT", PyInt_FromLong((long) SMFIS_ACCEPT)); + PyDict_SetItemString(d,"TEMPFAIL", PyInt_FromLong((long) SMFIS_TEMPFAIL)); +} diff --git a/mime.py b/mime.py new file mode 100644 index 0000000..ef98448 --- /dev/null +++ b/mime.py @@ -0,0 +1,607 @@ +# $Log$ +# Revision 1.51 2004/03/25 03:19:10 stuart +# Correctly defang rfc822 attachments when boundary specified with +# content-type message/rfc822. +# +# Revision 1.50 2003/10/15 22:01:00 stuart +# Test for and work around email bug with encoded filenames. +# +# Revision 1.49 2003/09/04 18:48:13 stuart +# Support python-2.2.3 +# +# Revision 1.48 2003/09/02 00:27:27 stuart +# Should have full milter based dspam support working +# +# Revision 1.47 2003/08/26 06:08:18 stuart +# Use new python boolean since we now require 2.2.2 +# +# Revision 1.46 2003/08/26 05:01:38 stuart +# Release 0.6.0 +# +# Revision 1.45 2003/08/26 04:01:24 stuart +# Use new email module for parsing mail. Still need mime module to +# provide various bug fixes to email module, and maintain some compatibility +# with old milter code. +# + +# This module provides a "defang" function to replace naughty attachments +# with a warning message. + +# Author: Stuart D. Gathman +# Copyright 2001 Business Management Systems, Inc. +# This code is under GPL. See COPYING for details. + +import StringIO +import socket +import Milter +import email +import email.Message +from email.Message import Message +from email.Generator import Generator +from email.Utils import quote +from email import Utils + +from types import ListType,StringType + +# Enhance email.Parser +# - Fix _parsebody to decode message attachments before parsing + +from email.Parser import Parser +try: from email.Parser import NLCRE +except: from email.Parser import nlcre as NLCRE + +from email import Errors + +class MimeParser(Parser): + + # This is a copy of _parsebody from email.Parser, with a fix + # for message attachments. I couldn't find a smaller way to patch it + # in a subclass. + + def _parsebody(self, container, fp, firstbodyline=None): + # Parse the body, but first split the payload on the content-type + # boundary if present. + boundary = container.get_boundary() + isdigest = (container.get_content_type() == 'multipart/digest') + # If there's a boundary, split the payload text into its constituent + # parts and parse each separately. Otherwise, just parse the rest of + # the body as a single message. Note: any exceptions raised in the + # recursive parse need to have their line numbers coerced. + if boundary: + preamble = epilogue = None + # Split into subparts. The first boundary we're looking for won't + # always have a leading newline since we're at the start of the + # body text, and there's not always a preamble before the first + # boundary. + separator = '--' + boundary + payload = fp.read() + if firstbodyline is not None: + payload = firstbodyline + '\n' + payload + # We use an RE here because boundaries can have trailing + # whitespace. + mo = re.search( + r'(?P' + re.escape(separator) + r')(?P[ \t]*)', + payload) + if not mo: + if self._strict: + raise Errors.BoundaryError( + "Couldn't find starting boundary: %s" % boundary) + container.set_payload(payload) + return + start = mo.start() + if start > 0: + # there's some pre-MIME boundary preamble + preamble = payload[0:start] + # Find out what kind of line endings we're using + start += len(mo.group('sep')) + len(mo.group('ws')) + mo = NLCRE.search(payload, start) + if mo: + start += len(mo.group(0)) + # We create a compiled regexp first because we need to be able to + # specify the start position, and the module function doesn't + # support this signature. :( + cre = re.compile('(?P\r\n|\r|\n)' + + re.escape(separator) + '--') + mo = cre.search(payload, start) + if mo: + terminator = mo.start() + linesep = mo.group('sep') + if mo.end() < len(payload): + # There's some post-MIME boundary epilogue + epilogue = payload[mo.end():] + elif self._strict: + raise Errors.BoundaryError( + "Couldn't find terminating boundary: %s" % boundary) + else: + # Handle the case of no trailing boundary. Check that it ends + # in a blank line. Some cases (spamspamspam) don't even have + # that! + mo = re.search('(?P\r\n|\r|\n){2}$', payload) + if not mo: + mo = re.search('(?P\r\n|\r|\n)$', payload) + if not mo: + raise Errors.BoundaryError( + 'No terminating boundary and no trailing empty line') + linesep = mo.group('sep') + terminator = len(payload) + # We split the textual payload on the boundary separator, which + # includes the trailing newline. If the container is a + # multipart/digest then the subparts are by default message/rfc822 + # instead of text/plain. In that case, they'll have a optional + # block of MIME headers, then an empty line followed by the + # message headers. + parts = re.split( + linesep + re.escape(separator) + r'[ \t]*' + linesep, + payload[start:terminator]) + for part in parts: + if isdigest: + if part.startswith(linesep): + # There's no header block so create an empty message + # object as the container, and lop off the newline so + # we can parse the sub-subobject + msgobj = self._class() + part = part[len(linesep):] + else: + parthdrs, part = part.split(linesep+linesep, 1) + # msgobj in this case is the "message/rfc822" container + msgobj = self.parsestr(parthdrs, headersonly=1) + # while submsgobj is the message itself + msgobj.set_default_type('message/rfc822') + maintype = msgobj.get_content_maintype() + if maintype in ('message', 'multipart'): + submsgobj = self.parsestr(part) + msgobj.attach(submsgobj) + else: + msgobj.set_payload(part) + else: + msgobj = self.parsestr(part) + container.preamble = preamble + container.epilogue = epilogue + container.attach(msgobj) + elif container.get_main_type() == 'multipart': + # Very bad. A message is a multipart with no boundary! + raise Errors.BoundaryError( + 'multipart message with no defined boundary') + elif container.get_type() == 'message/delivery-status': + # This special kind of type contains blocks of headers separated + # by a blank line. We'll represent each header block as a + # separate Message object + blocks = [] + while True: + blockmsg = self._class() + self._parseheaders(blockmsg, fp) + if not len(blockmsg): + # No more header blocks left + break + blocks.append(blockmsg) + container.set_payload(blocks) + elif container.get_main_type() == 'message': + # Create a container for the payload, but watch out for there not + # being any headers left + container.set_payload(fp.read()) + fp = StringIO.StringIO(container.get_payload(decode=True)) + try: + msg = self.parse(fp) + except Errors.HeaderParseError: + msg = self._class() + self._parsebody(msg, fp) + container.set_payload([msg]) + else: + text = fp.read() + if firstbodyline is not None: + text = firstbodyline + '\n' + text + container.set_payload(text) + +def unquote(str): + """Remove quotes from a string.""" + if len(str) > 1: + if str.startswith('"'): + if str.endswith('"'): + str = str[1:-1] + else: # remove garbage after trailing quote + try: str = str[1:str[1:].index('"')+1] + except: return str + return str.replace('\\\\', '\\').replace('\\"', '"') + if str.startswith('<') and str.endswith('>'): + return str[1:-1] + return str + +from types import TupleType + +def _unquotevalue(value): + if isinstance(value, TupleType): + return value[0], value[1], unquote(value[2]) + else: + return unquote(value) + +email.Message._unquotevalue = _unquotevalue + +def _parseparam(str): + plist = [] + while str[:1] == ';': + str = str[1:] + end = str.find(';') + while end > 0 and (str.count('"',0,end) & 1): + end = str.find(';',end + 1) + if end < 0: end = len(str) + f = str[:end] + if '=' in f: + i = f.index('=') + f = f[:i].strip().lower() + \ + '=' + f[i+1:].strip() + plist.append(f.strip()) + str = str[end:] + return plist + +# Enhance email.Message +# - Fix getparam to parse attributes IE style +# - Provide a headerchange event for integration with Milter +# Headerchange attribute can be assigned a function to be called when +# changing headers. The signature is: +# headerchange(msg,name,value) -> None +# - Track modifications to headers of body or any part independently + +class MimeMessage(Message): + """Version of email.Message.Message compatible with old mime module + """ + def __init__(self,fp=None,seekable=1): + self.headerchange = None + self.submsg = None + Message.__init__(self) + self.fp = fp + if fp: + parser = MimeParser(MimeMessage) + self.startofheaders = fp.tell() + parser._parseheaders(self,fp) + self.startofbody = fp.tell() + parser._parsebody(self,fp) + for part in self.walk(): + part.modified = False + + def rewindbody(self): + return self.fp.seek(self.startofbody) + + # override param parsing to handle quotes + def _get_params_preserve(self,failobj=None,header='content-type'): + "Return all parameter names and values. Use parser that handles quotes." + missing = [] + value = self.get(header, missing) + if value is missing: + return failobj + params = [] + for p in _parseparam(';' + value): + try: + name, val = p.split('=', 1) + name = name.strip() + val = val.strip() + except ValueError: + # Must have been a bare attribute + name = p.strip() + val = '' + params.append((name, val)) + params = Utils.decode_params(params) + return params + + def get_filename(self, failobj=None): + """Return the filename associated with the payload if present. + + The filename is extracted from the Content-Disposition header's + `filename' parameter, and it is unquoted. + """ + missing = [] + filename = self.get_param('filename', missing, 'content-disposition') + if filename is missing: + return failobj + if isinstance(filename, TupleType): + # It's an RFC 2231 encoded parameter + newvalue = _unquotevalue(filename) + if newvalue[0]: + return unicode(newvalue[2], newvalue[0]) + return unicode(newvalue[2]) + else: + newvalue = _unquotevalue(filename.strip()) + return newvalue + + getfilename = get_filename + ismultipart = Message.is_multipart + getheaders = Message.get_all + gettype = Message.get_content_type + getparam = Message.get_param + + def getparams(self): return self.get_params([]) + + def getname(self): + return self.get_param('name') + + def getnames(self): + """Return a list of (attr,name) pairs of attributes that IE might + interpret as a name - and hence decide to execute this message.""" + names = [] + for attr,val in self.get_params([]): + if isinstance(val, TupleType): + # It's an RFC 2231 encoded parameter + newvalue = _unquotevalue(val) + if val[0]: + val = unicode(newvalue[2], newvalue[0]) + else: + val = unicode(newvalue[2]) + else: + val = _unquotevalue(val.strip()) + names.append((attr,val)) + return names + [("filename",self.get_filename())] + + def ismodified(self): + "True if this message or a subpart has been modified." + if not self.is_multipart(): + if self.submsg: + return self.submsg.ismodified() + return self.modified + if self.modified: return True + for i in self.get_payload(): + if i.ismodified(): return True + return False + + def dump(self,file,unixfrom=False): + "Write this message (and all subparts) to a file" + g = Generator(file) + g.flatten(self,unixfrom=unixfrom) + + def getencoding(self): + return self.get('content-transfer-encoding',None) + + # Decode body to stream according to transfer encoding, return encoding name + def decode(self,filter): + try: + filter.write(self.get_payload(decode=True)) + except: + pass + return self.getencoding() + + def get_payload_decoded(self): + return self.get_payload(decode=True) + + def __setitem__(self, name, value): + rc = Message.__setitem__(self,name,value) + self.modified = True + if self.headerchange: self.headerchange(self,name,value) + return rc + + def __delitem__(self, name): + if self.headerchange: self.headerchange(self,name,None) + rc = Message.__delitem__(self,name) + self.modified = True + return rc + + def get_payload(self,i=None,decode=False): + msg = self.submsg + if msg and msg.ismodified(): + self.set_payload([msg]) + return Message.get_payload(self,i,decode) + + def set_payload(self, val, charset=None): + self.modified = True + try: + val.seek(0) + val = val.read() + except: pass + Message.set_payload(self,val,charset) + self.submsg = None + + def get_submsg(self): + if self.get_content_type().lower() == 'message/rfc822': + if not self.submsg: + txt = self.get_payload() + if type(txt) == str: + txt = self.get_payload(decode=True) + parser = MimeParser(MimeMessage) + self.submsg = parser.parsestr(txt) + else: + self.submsg = txt[0] + return self.submsg + return None + +extlist = ''.join(""" +ade,adp,asd,asx,asp,bas,bat,chm,cmd,com,cpl,crt,dll,exe,hlp,hta,inf,ins,isp,js, +jse,lnk,mdb,mde,msc,msi,msp,mst,ocx,pcd,pif,reg,scr,sct,shs,url,vb,vbe,vbs,wsc, +wsf,wsh +""".split()) +bad_extensions = map(lambda x:'.' + x,extlist.split(',')) + +def check_ext(name): + "Check a name for dangerous Winblows extensions." + if not name: return name + lname = name.lower() + for ext in bad_extensions: + if lname.endswith(ext): return name + return None + +virus_msg = """This message appeared to contain a virus. +It was originally named '%s', and has been removed. +A copy of your original message was saved as '%s:%s'. +See your administrator. +""" + +def check_name(msg,savname=None,ckname=check_ext): + "Replace attachment with a warning if its name is suspicious." + for (key,name) in msg.getnames(): + badname = ckname(name) + if badname: + hostname = socket.gethostname() + msg.set_payload(virus_msg % (badname,hostname,savname)) + del msg["content-type"] + del msg["content-disposition"] + del msg["content-transfer-encoding"] + name = "WARNING.TXT" + msg["Content-Type"] = "text/plain; name="+name + break + return Milter.CONTINUE + +import email.Iterators + +def check_attachments(msg,check): + """Scan attachments. +msg MimeMessage +check function(MimeMessage): int + Return CONTINUE, REJECT, ACCEPT + """ + if msg.ismultipart() and not msg.get_content_type() == 'message/rfc822': + for i in msg.get_payload(): + rc = check_attachments(i,check) + if rc != Milter.CONTINUE: return rc + return Milter.CONTINUE + return check(msg) + +# save call context for Python without nested_scopes +class _defang: + def __init__(self,savname,check): + self._savname = savname + self._check = check + self.scan_rfc822 = True + self.scan_html = True + def _chk_name(self,msg): + rc = check_name(msg,self._savname,self._check) + if self.scan_html: + check_html(msg,self._savname) # remove scripts from HTML + if self.scan_rfc822: + msg = msg.get_submsg() + if msg: return check_attachments(msg,self._chk_name) + return rc + +# emulate old defang function +def defang(msg,savname=None,check=check_ext): + """Compatible entry point. +Replace all attachments with dangerous names.""" + check_attachments(msg,_defang(savname,check)._chk_name) + if msg.ismodified(): + return 1; + return 0 + +import sgmllib + +import re +declname = re.compile(r'[a-zA-Z][-_.a-zA-Z0-9]*\s*') +declstringlit = re.compile(r'(\'[^\']*\'|"[^"]*")\s*') + +class SGMLFilter(sgmllib.SGMLParser): + """Parse HTML and pass through all constructs unchanged. It is intended for + derived classes to implement exceptional processing for selected cases. + """ + def __init__(self,out): + sgmllib.SGMLParser.__init__(self) + self.out = out + + def handle_comment(self,comment): + self.out.write("" % comment) + + def unknown_starttag(self,tag,attr): + if hasattr(self,"get_starttag_text"): + self.out.write(self.get_starttag_text()) + else: + self.out.write("<%s" % tag) + for (key,val) in attr: + self.out.write(' %s="%s"' % (key,val)) + self.out.write('>') + + def handle_data(self,data): + self.out.write(data) + + def handle_entityref(self,ref): + self.out.write("&%s;" % ref) + + def handle_charref(self,ref): + self.out.write("&#%s;" % ref) + + def unknown_endtag(self,tag): + self.out.write("" % tag) + + def handle_special(self,data): + self.out.write("" % data) + + def write(self,buf): + "Act like a writer. Why doesn't SGMLParser do this by default?" + self.feed(buf) + + # Python-2.1 sgmllib rejects illegal declarations. Since various Microsoft + # products accept and output them, we need to pass them through - + # at least until we discover that MS will execute them. + # sgmlop-1.1 will not use this method, but calls handle_special to + # do what we want. + def parse_declaration(self, i): + rawdata = self.rawdata + n = len(rawdata) + j = i + 2 + while j < n: + c = rawdata[j] + if c == ">": + # end of declaration syntax + self.handle_special(rawdata[i+2:j]) + return j + 1 + if c in "\"'": + m = declstringlit.match(rawdata, j) + if not m: + # incomplete or an error? + return -1 + j = m.end() + elif c in "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ": + m = declname.match(rawdata, j) + if not m: + # incomplete or an error? + return -1 + j = m.end() + else: + j += 1 + # end of buffer between tokens + return -1 + +class HTMLScriptFilter(SGMLFilter): + "Remove scripts from an HTML document." + def __init__(self,out): + SGMLFilter.__init__(self,out) + self.ignoring = 0 + self.modified = False + self.msg = "" + def start_script(self,unused): + self.ignoring += 1 + self.modified = True + self.out.write(self.msg) + def end_script(self): + self.ignoring -= 1 + def handle_data(self,data): + if not self.ignoring: SGMLFilter.handle_data(self,data) + def handle_comment(self,comment): + if not self.ignoring: SGMLFilter.handle_comment(self,comment) + + +def check_html(msg,savname=None): + "Remove scripts from HTML attachments." + msgtype = msg.get_content_type().lower() + # check for more MSIE braindamage + if msgtype == 'application/octet-stream': + for (attr,name) in msg.getnames(): + if name and name.lower().endswith(".htm"): + msgtype = 'text/html' + if msgtype == 'text/html': + out = StringIO.StringIO() + filter = HTMLScriptFilter(out) + try: + filter.write(msg.get_payload(decode=True)) + filter.close() + #except sgmllib.SGMLParseError: + except: + #mimetools.copyliteral(msg.get_payload(),open('debug.out','w') + filter.close() + hostname = socket.gethostname() + msg.set_payload( + "An HTML attachment could not be parsed. The original is saved as '%s:%s'" + % (hostname,savname)) + del msg["content-type"] + del msg["content-disposition"] + del msg["content-transfer-encoding"] + name = "WARNING.TXT" + msg["Content-Type"] = "text/plain; name="+name + return Milter.CONTINUE + if filter.modified: + msg.set_payload(out) # remove embedded scripts + del msg["content-transfer-encoding"] + email.Encoders.encode_quopri(msg) + return Milter.CONTINUE diff --git a/sample.py b/sample.py new file mode 100644 index 0000000..1b68ce7 --- /dev/null +++ b/sample.py @@ -0,0 +1,181 @@ + +# A simple milter. + +# Author: Stuart D. Gathman +# Copyright 2001 Business Management Systems, Inc. +# This code is under GPL. See COPYING for details. + +import sys +import os +import StringIO +import rfc822 +import mime +import Milter +import tempfile +from time import strftime +#import syslog + +#syslog.openlog('milter') + +class sampleMilter(Milter.Milter): + "Milter to replace attachments poisonous to Windows with a WARNING message." + + def log(self,*msg): + print "%s [%d]" % (strftime('%Y%b%d %H:%M:%S'),self.id), + for i in msg: print i, + print + + def __init__(self): + self.tempname = None + self.mailfrom = None + self.fp = None + self.bodysize = 0 + self.id = Milter.uniqueID() + + # multiple messages can be received on a single connection + # envfrom (MAIL FROM in the SMTP protocol) seems to mark the start + # of each message. + def envfrom(self,f,*str): + self.log("mail from",f,str) + self.fp = StringIO.StringIO() + self.tempname = None + self.mailfrom = f + self.bodysize = 0 + return Milter.CONTINUE + + def envrcpt(self,to,*str): + # mail to MAILER-DAEMON is generally spam that bounced + if to.startswith('= 0 or val.find("XXX") >= 0 \ + or val.find("!!!") >= 0 or val.find("FREE") >= 0: + self.log('REJECT: %s: %s' % (name,val)) + #self.setreply('550','','Go away spammer') + return Milter.REJECT + + # check for spam that pretends to be legal + lval = val.lower() + if lval.startswith("adv:") or lval.startswith("adv.") \ + or lval.find('viagra') >= 0: + self.log('REJECT: %s: %s' % (name,val)) + return Milter.REJECT + + # check for invalid message id + if lname == 'message-id' and len(val) < 4: + self.log('REJECT: %s: %s' % (name,val)) + #self.setreply('550','','Go away spammer') + return Milter.REJECT + + # check for common bulk mailers + if lname == 'x-mailer' and \ + val.lower() in ('direct email','calypso','mail bomber'): + self.log('REJECT: %s: %s' % (name,val)) + #self.setreply('550','','Go away spammer') + return Milter.REJECT + + # log selected headers + if lname in ('subject','x-mailer'): + self.log('%s: %s' % (name,val)) + if self.fp: + self.fp.write("%s: %s\n" % (name,val)) # add header to buffer + return Milter.CONTINUE + + def eoh(self): + if not self.fp: return Milter.TEMPFAIL # not seen by envfrom + self.fp.write("\n") + self.fp.seek(0) + # copy headers to a temp file for scanning the body + headers = self.fp.getvalue() + self.fp.close() + self.tempname = fname = tempfile.mktemp(".defang") + self.fp = open(fname,"w+b") + self.fp.write(headers) # IOError (e.g. disk full) causes TEMPFAIL + return Milter.CONTINUE + + def body(self,chunk): # copy body to temp file + if self.fp: + self.fp.write(chunk) # IOError causes TEMPFAIL in milter + self.bodysize += len(chunk) + return Milter.CONTINUE + + def _headerChange(self,msg,name,value): + if value: # add header + self.addheader(name,value) + else: # delete all headers with name + h = msg.getheaders(name) + cnt = len(h) + for i in range(cnt,0,-1): + self.chgheader(name,i-1,'') + + def eom(self): + if not self.fp: return Milter.ACCEPT + self.fp.seek(0) + msg = mime.MimeMessage(self.fp) + msg.headerchange = self._headerChange + if not mime.defang(msg,self.tempname): + os.remove(self.tempname) + self.tempname = None # prevent re-removal + self.log("eom") + return Milter.ACCEPT # no suspicious attachments + self.log("Temp file:",self.tempname) + self.tempname = None # prevent removal of original message copy + # copy defanged message to a temp file + out = tempfile.TemporaryFile() + try: + msg.dump(out) + out.seek(0) + msg = rfc822.Message(out) + msg.rewindbody() + while 1: + buf = out.read(8192) + if len(buf) == 0: break + self.replacebody(buf) # feed modified message to sendmail + return Milter.ACCEPT # ACCEPT modified message + finally: + out.close() + return Milter.TEMPFAIL + + def close(self): + sys.stdout.flush() # make log messages visible + if self.tempname: + os.remove(self.tempname) # remove in case session aborted + if self.fp: + self.fp.close() + return Milter.CONTINUE + + def abort(self): + self.log("abort after %d body chars" % self.bodysize) + return Milter.CONTINUE + +if __name__ == "__main__": + #tempfile.tempdir = "/var/log/milter" + #socketname = "/var/log/milter/pythonsock" + socketname = os.getenv("HOME") + "/pythonsock" + Milter.factory = sampleMilter + Milter.set_flags(Milter.CHGBODY + Milter.CHGHDRS + Milter.ADDHDRS) + print """To use this with sendmail, add the following to sendmail.cf: + +O InputMailFilters=pythonfilter +Xpythonfilter, S=local:%s + +See the sendmail README for libmilter. +sample milter startup""" % socketname + sys.stdout.flush() + Milter.runmilter("pythonfilter",socketname,240) + print "sample milter shutdown" diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..1b9f4f1 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,4 @@ +[bdist_rpm] +python=python2 +doc_files=README NEWS TODO +packager=Stuart D. Gathman diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..0534203 --- /dev/null +++ b/setup.py @@ -0,0 +1,36 @@ +import os +from distutils.core import setup, Extension + +# FIXME: on some versions of sendmail, smutil is renamed to sm +libs = ["milter", "smutil"] + +setup(name = "milter", version = "0.6.8", + description="Python interface to sendmail milter API", + long_description="""\ +This is a python extension module to enable python scripts to +attach to sendmail's libmilter functionality. Additional python +modules provide for navigating and modifying MIME parts, and +querying SPF records. +""", + author="Jim Niemira", + author_email="urmane@urmane.org", + maintainer="Stuart D. Gathman", + maintainer_email="stuart@bmsi.com", + license="GPL", + url="http://www.bmsi.com/python/milter.html", + py_modules=["Milter","mime","spf"], + ext_modules=[ + Extension("milter", ["miltermodule.c"],libraries=libs), + ], + keywords = ['sendmail','milter'], + classifiers = [ + 'Development Status :: 5 - Production/Stable', + 'Environment :: No Input/Output (Daemon)', + 'Intended Audience :: System Administrators', + 'License :: OSI Approved :: GNU General Public License (GPL)', + 'Natural Language :: English', + 'Operating System :: POSIX', + 'Programming Language :: Python', + 'Topic :: Communications :: Email :: Mail Transport Agents' + ] +) diff --git a/spf.py b/spf.py new file mode 100755 index 0000000..aa11336 --- /dev/null +++ b/spf.py @@ -0,0 +1,729 @@ +#!/usr/bin/env python +"""SPF (Sender-Permitted From) implementation. + +Copyright (c) 2003, Terence Way +This module is free software, and you may redistribute it and/or modify +it under the same terms as Python itself, so long as this copyright message +and disclaimer are retained in their original form. + +IN NO EVENT SHALL THE AUTHOR BE LIABLE TO ANY PARTY FOR DIRECT, INDIRECT, +SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OF +THIS CODE, EVEN IF THE AUTHOR HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH +DAMAGE. + +THE AUTHOR SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +PARTICULAR PURPOSE. THE CODE PROVIDED HEREUNDER IS ON AN "AS IS" BASIS, +AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE, +SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. + +For more information about SPF, a tool against email forgery, see + http://spf.pobox.com + +For news, bugfixes, etc. visit the home page for this implementation at + http://www.wayforward.net/spf/ +""" + +# Changes: +# 9-dec-2003, v1.1, Meng Weng Wong added PTR code, THANK YOU +# 11-dec-2003, v1.2, ttw added macro expansion, exp=, and redirect= +# 13-dec-2003, v1.3, ttw added %{o} original domain macro, +# print spf result on command line, support default=, +# support localhost, follow DNS CNAMEs, cache DNS results +# during query, support Python 2.2 for Mac OS X +# 16-dec-2003, v1.4, ttw fixed include handling (include is a mechanism, +# complete with status results, so -include: should work. +# Expand macros AFTER looking for status characters ?-+ +# so altavista.com SPF records work. +# 17-dec-2003, v1.5, ttw use socket.inet_aton() instead of DNS.addr2bin, so +# n, n.n, and n.n.n forms for IPv4 addresses work, and to +# ditch the annoying Python 2.4 FutureWarning +# 18-dec-2003, v1.6, Failures on Intel hardware: endianness. Use ! on +# struct.pack(), struct.unpack(). +# $Log$ +# Revision 1.5 2004/04/05 22:29:46 stuart +# SPF best_guess, +# +# Revision 1.4 2004/03/25 03:27:34 stuart +# Support delegation of SPF records. +# +# Revision 1.3 2004/03/13 12:23:23 stuart +# Expanded result codes. Tolerate common method misspellings. +# + +__author__ = "Terence Way" +__email__ = "terry@wayforward.net" +__version__ = "1.6: December 18, 2003" +MODULE = 'spf' + +USAGE = """To check an incoming mail request: + % python spf.py {ip} {sender} {helo} + % python spf.py 69.55.226.139 tway@optsw.com mx1.wayforward.net + +To test an SPF record: + % python spf.py "v=spf1..." {ip} {sender} {helo} + % python spf.py "v=spf1 +mx +ip4:10.0.0.1 -all" 10.0.0.1 tway@foo.com a + +To fetch an SPF record: + % python spf.py {domain} + % python spf.py wayforward.net + +To test this script (and to output this usage message): + % python spf.py +""" + +import re +import socket # for inet_ntoa() and inet_aton() +import struct # for pack() and unpack() +import time # for time() + +import DNS # http://pydns.sourceforge.net + +# 32-bit IPv4 address mask +MASK = 0xFFFFFFFFL + +# Regular expression to look for modifiers +RE_MODIFIER = re.compile(r'^([a-zA-Z]+)=') + +# Regular expression to find macro expansions +RE_CHAR = re.compile(r'%(%|_|-|(\{[a-zA-Z][0-9]*r?[^\}]*\}))') + +# Regular expression to break up a macro expansion +RE_ARGS = re.compile(r'([0-9]*)(r?)([^0-9a-zA-Z]*)') + +# Local parts and senders have their delimiters replaced with '.' during +# macro expansion +# +JOINERS = {'l': '.', 's': '.'} + +RESULTS = {'+': 'pass', '-': 'fail', '?': 'neutral', '~': 'softfail', + 'pass': 'pass', 'fail': 'fail', 'unknown': 'unknown', + 'neutral': 'neutral', 'softfail': 'softfail', + 'none': 'none' } + +EXPLANATIONS = {'pass': 'sender SPF verified', 'fail': 'access denied', + 'unknown': 'SPF unknown', 'softfail': 'domain in transition', + 'neutral': 'access neither permitted nor denied', + 'none': 'no SPF records' + } + +# if set to a domain name, search _spf.domain namespace if no SPF record +# found in source domain. + +DELEGATE = None + +# support pre 2.2.1.... +try: + bool, True, False = bool, True, False +except NameError: + False, True = 0, 1 + def bool(x): return not not x +# ...pre 2.2.1 + +# standard default SPF record +DEFAULT_SPF = 'v=spf1 a/24 mx/24 ptr' + +def check(i, s, h,default=None): + """Test an incoming MAIL FROM:, from a client with ip address i. + h is the HELO/EHLO domain name. + + Returns (result, mta-status-code, explanation) where result in + ['pass', 'unknown', 'fail', 'error', 'softfail', 'none', 'neutral' ]. + + Example: + >>> check(i='127.0.0.1', s='terry@wayforward.net', h='localhost') + ('pass', 250, 'local connections always pass') + + #>>> check(i='61.51.192.42', s='liukebing@bcc.com', h='bmsi.com') + + """ + if i.startswith('127.'): + return ('pass', 250, 'local connections always pass') + + try: + q = query(i=i, s=s, h=h) + spf = q.dns_spf(q.d) + if not spf and default: + spf = default + return q.check(spf) + except DNS.DNSError: + return ('error', 450, 'SPF DNS Error') + +def best_guess(i, s, h,spf=DEFAULT_SPF): + q = query(i=i, s=s, h=h) + return q.check(spf) + +class query(object): + """A query object keeps the relevant information about a single SPF + query: + + i: ip address of SMTP client + s: sender declared in MAIL FROM:<> + l: local part of sender s + d: current domain, initially domain part of sender s + h: EHLO/HELO domain + v: 'in-addr' for IPv4 clients and 'ip6' for IPv6 clients + t: current timestamp + p: SMTP client domain name + o: domain part of sender s + + This is also, by design, the same variables used in SPF macro + expansion. + + Also keeps cache: DNS cache. + """ + def __init__(self, i, s, h): + self.i, self.s, self.h = i, s, h + self.l, self.o = split_email(s, h) + self.t = str(int(time.time())) + self.v = 'in-addr' + self.d = self.o + self.p = None + self.cache = {} + + def getp(self): + if not self.p: + p = self.dns_ptr(self.i) + if len(p) > 0: + self.p = p[0] + else: + self.p = self.i + return self.p + + def check(self, spf): + """ + Returns (result, mta-status-code, explanation) where + result in ['fail', 'unknown', 'pass'] + """ + return self.check1(spf, self.d, 0) + + def check1(self, spf, domain, recursion): + # spf rfc: 3.7 Processing Limits + # + if recursion > 10: + return ('unknown', 250, 'SPF recursion limit exceeded') + try: + tmp, self.d = self.d, domain + return self.check0(spf, recursion) + finally: + self.d = tmp + + def check0(self, spf, recursion): + """Test this query information against SPF text. + + Returns (result, mta-status-code, explanation) where + result in ['fail', 'unknown', 'pass', 'none'] + """ + + if not spf: + return ('none', 250, 'no SPF records') + + # split string by whitespace, drop the 'v=spf1' + # + spf = spf.split()[1:] + + # copy of explanations to be modified by exp= + exps = dict(EXPLANATIONS) + redirect = None + + # no mechanisms at all cause unknown result, unless + # overridden with 'default=' modifier + # + default = 'neutral' + + # Look for modifiers + # + for m in spf: + m = RE_MODIFIER.split(m)[1:] + if len(m) != 2: continue + + if m[0] == 'exp': + exps['fail'] = exps['unknown'] = \ + self.get_explanation(m[1]) + elif m[0] == 'redirect': + redirect = self.expand(m[1]) + elif m[0] == 'default': + # default=- is the same as default=fail + default = RESULTS.get(m[1], default) + + # spf rfc: 3.6 Unrecognized Mechanisms and Modifiers + + # Look for mechanisms + # + for mech in spf: + if RE_MODIFIER.match(mech): continue + m, arg, cidrlength = parse_mechanism(mech, self.d) + + # map '?' '+' or '-' to 'unknown' 'pass' or 'fail' + result = RESULTS.get(m[0]) + if result: + # eat '?' '+' or '-' + m = m[1:] + else: + # default pass + result = 'pass' + + if m in ['a', 'mx', 'ptr', 'exists', 'include']: + arg = self.expand(arg) + + if m == 'include': + if arg != self.d: + tmp = self.check1(self.dns_spf(arg), + arg, recursion + 1) + if tmp[0] == 'pass': + break + if tmp[0] != 'fail': + return tmp + + elif m == 'all': + break + + elif m == 'exists': + if len(self.dns_a(arg)) > 0: + break + + elif m == 'a': + if cidrmatch(self.i, self.dns_a(arg), + cidrlength): + break + + elif m == 'mx': + if cidrmatch(self.i, self.dns_mx(arg), + cidrlength): + break + + elif m in ('ip4', 'ipv4') and arg != self.d: + if cidrmatch(self.i, [arg], cidrlength): + break + + elif m in ('ptr', 'prt'): + if domainmatch(self.validated_ptrs(self.i), + arg): + break + + else: + # unknown mechanisms cause immediate unknown + # abort results + return ('unknown', 250, mech) + + else: + # no matches + if redirect: + return self.check1(self.dns_spf(redirect), + redirect, recursion+1) + else: + result = default + + if result == 'fail': + return (result, 550, exps[result]) + else: + return (result, 250, exps[result]) + + def get_explanation(self, spec): + """Expand an explanation.""" + return self.expand(''.join(self.dns_txt(self.expand(spec)))) + + def expand(self, str): + """Do SPF RFC macro expansion. + + Examples: + >>> q = query(s='strong-bad@email.example.com', + ... h='mx.example.org', i='192.0.2.3') + >>> q.p = 'mx.example.org' + + >>> q.expand('%{d}') + 'email.example.com' + + >>> q.expand('%{d4}') + 'email.example.com' + + >>> q.expand('%{d3}') + 'email.example.com' + + >>> q.expand('%{d2}') + 'example.com' + + >>> q.expand('%{d1}') + 'com' + + >>> q.expand('%{p}') + 'mx.example.org' + + >>> q.expand('%{p2}') + 'example.org' + + >>> q.expand('%{dr}') + 'com.example.email' + + >>> q.expand('%{d2r}') + 'example.email' + + >>> q.expand('%{l}') + 'strong-bad' + + >>> q.expand('%{l-}') + 'strong.bad' + + >>> q.expand('%{lr}') + 'strong-bad' + + >>> q.expand('%{lr-}') + 'bad.strong' + + >>> q.expand('%{l1r-}') + 'strong' + + >>> q.expand('%{ir}.%{v}._spf.%{d2}') + '3.2.0.192.in-addr._spf.example.com' + + >>> q.expand('%{lr-}.lp._spf.%{d2}') + 'bad.strong.lp._spf.example.com' + + >>> q.expand('%{lr-}.lp.%{ir}.%{v}._spf.%{d2}') + 'bad.strong.lp.3.2.0.192.in-addr._spf.example.com' + + >>> q.expand('%{ir}.%{v}.%{l1r-}.lp._spf.%{d2}') + '3.2.0.192.in-addr.strong.lp._spf.example.com' + + >>> q.expand('%{p2}.trusted-domains.example.net') + 'example.org.trusted-domains.example.net' + + >>> q.expand('%{p2}.trusted-domains.example.net') + 'example.org.trusted-domains.example.net' + + """ + end = 0 + result = '' + for i in RE_CHAR.finditer(str): + result += str[end:i.start()] + macro = str[i.start():i.end()] + if macro == '%%': + result += '%' + elif macro == '%_': + result += ' ' + elif macro == '%-': + result += '%20' + else: + letter = macro[2].lower() + if letter == 'p': + self.getp() + expansion = getattr(self, letter, '') + if expansion: + result += expand_one(expansion, + macro[3:-1], + JOINERS.get(letter)) + + end = i.end() + return result + str[end:] + + def dns_spf(self, domain): + """Get the SPF record recorded in DNS for a specific domain + name. Returns None if not found, or if more than one record + is found. + """ + a = [t for t in self.dns_txt(domain) if t.startswith('v=spf1')] + if not a and DELEGATE: + a = [t + for t in self.dns_txt(domain+'._spf.'+DELEGATE) + if t.startswith('v=spf1') + ] + if len(a) == 1: + return a[0] + else: + return None + + def dns_txt(self, domainname): + return [t for a in self.dns(domainname, 'TXT') for t in a] + + def dns_mx(self, domainname): + """Get a list of IP addresses for all MX exchanges for a + domain name. + """ + return [a for mx in self.dns(domainname, 'MX') \ + for a in self.dns_a(mx[1])] + + def dns_a(self, domainname): + """Get a list of IP addresses for a domainname.""" + return self.dns(domainname, 'A') + + def dns_aaaa(self, domainname): + """Get a list of IPv6 addresses for a domainname.""" + return self.dns(domainname, 'AAAA') + + def validated_ptrs(self, i): + """Figure out the validated PTR domain names for a given IP + address. + """ + return [p for p in self.dns_ptr(i) if i in self.dns_a(p)] + + def dns_ptr(self, i): + """Get a list of domain names for an IP address.""" + return self.dns(reverse_dots(i) + ".in-addr.arpa", 'PTR') + + def dns(self, name, qtype): + """DNS query. + + If the result is in cache, return that. Otherwise pull the + result from DNS, and cache ALL answers, so additional info + is available for further queries later. + + CNAMEs are followed. + + If there is no data, [] is returned. + + pre: qtype in ['A', 'AAAA', 'MX', 'PTR', 'TXT', 'SPF'] + post: isinstance(__return__, types.ListType) + """ + result = self.cache.get( (name, qtype) ) + cname = None + if not result: + req = DNS.DnsRequest(name, qtype=qtype) + resp = req.req() + for a in resp.answers: + # key k: ('wayforward.net', 'A'), value v + k, v = (a['name'], a['typename']), a['data'] + if k == (name, 'CNAME'): + cname = v + self.cache.setdefault(k, []).append(v) + result = self.cache.get( (name, qtype), []) + if not result and cname: + result = self.dns(cname, qtype) + return result + +def split_email(s, h): + """Given a sender email s and a HELO domain h, create a valid tuple + (l, d) local-part and domain-part. + + Examples: + >>> split_email('', 'wayforward.net') + ('postmaster', 'wayforward.net') + + >>> split_email('foo.com', 'wayforward.net') + ('postmaster', 'foo.com') + + >>> split_email('terry@wayforward.net', 'optsw.com') + ('terry', 'wayforward.net') + """ + if not s: + return 'postmaster', h + else: + parts = s.split('@', 1) + if len(parts) == 2: + return tuple(parts) + else: + return 'postmaster', s + +def parse_mechanism(str, d): + """Breaks A, MX, IP4, and PTR mechanisms into a (name, domain, + cidr) tuple. The domain portion defaults to d if not present, + the cidr defaults to 32 if not present. + + Examples: + >>> parse_mechanism('a', 'foo.com') + ('a', 'foo.com', 32) + + >>> parse_mechanism('a:bar.com', 'foo.com') + ('a', 'bar.com', 32) + + >>> parse_mechanism('a/24', 'foo.com') + ('a', 'foo.com', 24) + + >>> parse_mechanism('a:bar.com/16', 'foo.com') + ('a', 'bar.com', 16) + """ + a = str.split('/') + if len(a) == 2: + a, port = a[0], int(a[1]) + else: + a, port = str, 32 + + b = a.split(':') + if len(b) == 2: + return b[0], b[1], port + else: + return a, d, port + +def reverse_dots(name): + """Reverse dotted IP addresses or domain names. + + Example: + >>> reverse_dots('192.168.0.145') + '145.0.168.192' + + >>> reverse_dots('email.example.com') + 'com.example.email' + """ + a = name.split('.') + a.reverse() + return '.'.join(a) + +def domainmatch(ptrs, domainsuffix): + """grep for a given domain suffix against a list of validated PTR + domain names. + + Examples: + >>> domainmatch(['FOO.COM'], 'foo.com') + 1 + + >>> domainmatch(['moo.foo.com'], 'FOO.COM') + 1 + + >>> domainmatch(['moo.bar.com'], 'foo.com') + 0 + + """ + domainsuffix = domainsuffix.lower() + for ptr in ptrs: + ptr = ptr.lower() + + if ptr == domainsuffix or ptr.endswith('.' + domainsuffix): + return True + + return False + +def cidrmatch(i, ipaddrs, cidr_length = 32): + """Match an IP address against a list of other IP addresses. + + Examples: + >>> cidrmatch('192.168.0.45', ['192.168.0.44', '192.168.0.45']) + 1 + + >>> cidrmatch('192.168.0.43', ['192.168.0.44', '192.168.0.45']) + 0 + + >>> cidrmatch('192.168.0.43', ['192.168.0.44', '192.168.0.45'], 24) + 1 + """ + c = cidr(i, cidr_length) + for ip in ipaddrs: + if cidr(ip, cidr_length) == c: + return True + return False + +def cidr(i, n): + """Convert an IP address string with a CIDR mask into a 32-bit + integer. + + i must be a string of numbers 0..255 separated by dots '.':: + pre: forall([0 <= int(p) < 256 for p in i.split('.')]) + + n is a number of bits to mask:: + pre: 0 <= n <= 32 + + Examples: + >>> bin2addr(cidr('192.168.5.45', 32)) + '192.168.5.45' + >>> bin2addr(cidr('192.168.5.45', 24)) + '192.168.5.0' + >>> bin2addr(cidr('192.168.0.45', 8)) + '192.0.0.0' + """ + return ~(MASK >> n) & MASK & addr2bin(i) + +def addr2bin(str): + """Convert a string IPv4 address into an unsigned integer. + + Examples:: + >>> addr2bin('127.0.0.1') + 2130706433L + + >>> addr2bin('127.0.0.1') == socket.INADDR_LOOPBACK + 1 + + >>> addr2bin('255.255.255.254') + 4294967294L + + >>> addr2bin('192.168.0.1') + 3232235521L + + Unlike DNS.addr2bin, the n, n.n, and n.n.n forms for IP addresses + are handled as well:: + >>> addr2bin('10.65536') + 167837696L + >>> 10 * (2 ** 24) + 65536 + 167837696 + + >>> addr2bin('10.93.512') + 173867520L + >>> 10 * (2 ** 24) + 93 * (2 ** 16) + 512 + 173867520 + """ + return struct.unpack("!L", socket.inet_aton(str))[0] + +def bin2addr(addr): + """Convert a numeric IPv4 address into string n.n.n.n form. + + Examples:: + >>> bin2addr(socket.INADDR_LOOPBACK) + '127.0.0.1' + + >>> bin2addr(socket.INADDR_ANY) + '0.0.0.0' + + >>> bin2addr(socket.INADDR_NONE) + '255.255.255.255' + """ + return socket.inet_ntoa(struct.pack("!L", addr)) + +def expand_one(expansion, str, joiner): + if not str: + return expansion + len, reverse, delimiters = RE_ARGS.split(str)[1:4] + if not delimiters: + delimiters = '.' + expansion = split(expansion, delimiters, joiner) + if reverse: expansion.reverse() + if len: expansion = expansion[-int(len)*2+1:] + return ''.join(expansion) + +def split(str, delimiters, joiner=None): + """Split a string into pieces by a set of delimiter characters. The + resulting list is delimited by joiner, or the original delimiter if + joiner is not specified. + + Examples: + >>> split('192.168.0.45', '.') + ['192', '.', '168', '.', '0', '.', '45'] + + >>> split('terry@wayforward.net', '@.') + ['terry', '@', 'wayforward', '.', 'net'] + + >>> split('terry@wayforward.net', '@.', '.') + ['terry', '.', 'wayforward', '.', 'net'] + """ + result, element = [], '' + for c in str: + if c in delimiters: + result.append(element) + element = '' + if joiner: + result.append(joiner) + else: + result.append(c) + else: + element += c + result.append(element) + return result + +def _test(): + import doctest, spf + return doctest.testmod(spf) + +DNS.DiscoverNameServers() # Fails on Mac OS X? Add domain to /etc/resolv.conf + +if __name__ == '__main__': + import sys + if len(sys.argv) == 1: + print USAGE + _test() + elif len(sys.argv) == 2: + q = query(i='127.0.0.1', s='localhost', h='unknown') + print q.dns_spf(sys.argv[1]) + elif len(sys.argv) == 4: + print check(i=sys.argv[1], s=sys.argv[2], h=sys.argv[3]) + elif len(sys.argv) == 5: + i, s, h = sys.argv[2:] + q = query(i=i, s=s, h=h) + print q.check(sys.argv[1]) + else: + print USAGE diff --git a/test.py b/test.py new file mode 100644 index 0000000..0e12658 --- /dev/null +++ b/test.py @@ -0,0 +1,17 @@ +import unittest +import testbms +import testmime +import testsample +import os + +def suite(): + s = unittest.TestSuite() + s.addTest(testbms.suite()) + s.addTest(testmime.suite()) + s.addTest(testsample.suite()) + return s + +if __name__ == '__main__': + try: os.remove('test/milter.log') + except: pass + unittest.TextTestRunner().run(suite()) diff --git a/test/amazon b/test/amazon new file mode 100644 index 0000000..b679c8c --- /dev/null +++ b/test/amazon @@ -0,0 +1,710 @@ +From stuart@bmsi.com Wed May 1 14:37:14 2002 +Return-Path: +Received: from bmsi.com (IDENT:stuart@localhost [127.0.0.1]) + by gathman.bmsi.com (8.11.6/8.11.6) with ESMTP id g41IbCF01796 + for ; Wed, 1 May 2002 14:37:13 -0400 +Sender: stuart@gathman.bmsi.com +Message-ID: <3CD035D7.18ADF27F@bmsi.com> +Date: Wed, 01 May 2002 14:37:11 -0400 +From: "Stuart D. Gathman" +Organization: Business Management Systems, Inc. +X-Mailer: Mozilla 4.78 [en] (X11; U; Linux 2.4.9-21 i586) +X-Accept-Language: en +MIME-Version: 1.0 +To: stuart@gathman.bmsi.com +Subject: Amazon.com--Earth's Biggest Selection +Content-Type: multipart/mixed; + boundary="------------59A46341C90BA737DD47867B" + +This is a multi-part message in MIME format. +--------------59A46341C90BA737DD47867B +Content-Type: multipart/alternative; + boundary="------------0B098FB91956AC123C61B151" + + +--------------0B098FB91956AC123C61B151 +Content-Type: text/plain; charset=us-ascii +Content-Transfer-Encoding: 7bit + +http://www.amazon.com/exec/obidos/subst/home/redirect.html/103-3111065-2579065 + +-- + Stuart D. Gathman +Business Management Systems Inc. Phone: 703 591-0911 Fax: 703 591-6154 +"Confutatis maledictis, flamis acribus addictis" - background song for +a Microsoft sponsored "Where do you want to go from here?" commercial. + + + +--------------0B098FB91956AC123C61B151 +Content-Type: text/html; charset=us-ascii +Content-Transfer-Encoding: 7bit + + + +http://www.amazon.com/exec/obidos/subst/home/redirect.html/103-3111065-2579065 +
    -- 
    +              Stuart D. Gathman 
    +Business Management Systems Inc.  Phone: 703 591-0911 Fax: 703 591-6154
    +"Confutatis maledictis, flamis acribus addictis" - background song for
    +a Microsoft sponsored "Where do you want to go from here?" commercial.
    +  + +--------------0B098FB91956AC123C61B151-- + +--------------59A46341C90BA737DD47867B +Content-Type: text/html; charset=us-ascii; + name="103-3111065-2579065" +Content-Transfer-Encoding: 7bit +Content-Disposition: inline; + filename="103-3111065-2579065" +Content-Base: "http://www.amazon.com/exec/obidos/subs + t/home/redirect.html/103-3111065-25 + 79065" +Content-Location: "http://www.amazon.com/exec/obidos/subs + t/home/redirect.html/103-3111065-25 + 79065" + + + + + +Amazon.com--Earth's Biggest Selection + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +
    + + + + + + + + + + + +
       + +  +
    + + + + + + + + + + +
    +
    +
    +
    + +Hello, Stuart D. Gathman. +We have recommendations for you. + +(If you're not Stuart D. Gathman, click here.) + +
    + +
    + + + + +
    + + + +
    Search Amazon.com
    +
    + + + +

    + + + + + + +
    Browse Amazon.com
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    • Books
    • Electronics
    • Baby &
       Baby Registry
    • Music
    • Health & Beauty
    • DVD
    • Software
    • Kitchen &
       Housewares
    • Tools &
       Hardware
    • Computers
    • Camera & Photo
    • Movie Showtimes
    • Computer &
       Video Games
    • Toys & Games
    • Cell Phones
       & Service
    • Video
    • Magazine
       Subscriptions
    • Outdoor Living
    • Travel
    • Cars
    • Gifts &
       Gift Certificates
    • Auctions
    • zShops
    • Outlet
    • Corporate
       Accounts
    + +
    +
    Browse Partners
    • Target
    • Toysrus.com
    • Babiesrus.com
    +

    +
    +Special Features
    + + + +

    +
    +Associates
    + +Sell books, music, videos, and more from your +Web site. Start earning +today!
    +
    +

    +

    +
    +

      +
    +
    +

    + +

    + +

    + +Pre-order the Oscar®-winning blockbuster The Lord of the Rings: The Fellowship of the Ring, arriving on DVD and video August 6. +

    +In Gifts
    + +Mother's Day Is May 12
    +We've made it fun and easy to buy the perfect +present for Mom. Shop by recipient +or price, +browse top +sellers, or order flowers. +Visit Gifts for +these and more great ideas for expressing your love and +appreciation.

    +
    +Your Recommendations +
    +War in Heaven + +
    + +Amazon.com
    +"The telephone was ringing wildly," begins Charles Williams's novel War in Heaven, "but without result, since there was no-one in the room but the corpse." From this abrupt--and darkly humorous--start, Williams takes us on a 20th-century version of the Grail quest, with an Archdeacon, a Duke, and an... + +Read more + +| +(Why was I recommended this?) + +
    +
    More Recommendations
    +Icon +Reliable Linux by Iain Campbell + +(Why?) + +
    +Icon +Programming PHP by Rasmus Lerdorf, et al + +(Why?) + +
    +Icon +Descent into Hell by Charles W. Williams + +(Why?) + +
    +Icon +Network Troubleshooting Tools (O'Reilly System Administration) by Joseph D. Sloan + +(Why?) + +
    +

    +Your Music Store
    + +Isaac Freeman, et al, +Beautiful Stars + +
    + +Great African American gospel music has an indisputable power, rooted in the audible faith of its performers and the beauty of their voices. As the bass singer of the Fairfield Four, an a cappella group that started more than a half century ago,... +Read more +
    +
    +
    + + + +
    +

    More Stores: +

    IconYour Electronics Store: iRiver SlimX iMP-350 CD/MP3 Player with 8 minutes ASP and Upgradeable Firmware +by iRiver +
    IconYour Video Store: Ocean's Eleven +VHS ~ George Clooney +
    +

    +Listmania!
    + +(What is this?) +
    + + + + + + +
    + +

    + +Best Linux Security books: A list by J. Parker, Administrator, hacker.
    +(7 item list)
    +

    + +

    + +Networking: A list by gakis, Engineer
    +(13 item list)
    +

    +

    +In Travel
    + +Your Next Vacation Starts +Here
    +Save up to 70% on hotels from Vegas to New York +and everywhere in between on Expedia.com. +Book a flight during Hotwire's major-airline Spring Sale through May 2 and fly the +big-name airlines at no-name airline prices. The +Vacation Store is offering seven-day Holland America +Caribbean cruises from just $599.

    +

    + + +
    +
    +
    +
    +New For You
    +
    + + + + + + +
    +Stuart, check out what's New for You:
    +
    +(If you're not Stuart D. Gathman, click here.) +

    +
    +Your Message Center +
    + + + +
    ! You have 5 new messages. +

    +
    +
    +Your Shopping Cart +
    + + + +
    Shopping CartYou have 0 items in your Shopping Cart.

    +
    + +
    +Your New Releases +
    + + + + + + + +
    +Icon +Pop +
    +Icon +Christian & Gospel +
    +Icon +Computers & Internet +
    +Icon +Cookware +
    +Icon +Action & Adventure +
    More New Releases

    +

    + +
    +Movers & Shakers +
    + + + + + + + + + +
    +Up + +974%
    +Icon + +Dorothy L. Sayers Mysteries (Strong Poison / Have His Carcass / Gaudy Night) + +DVD +
    ~ Dorothy L. Sayers +
    +
    +
    +Up + +2,415%
    +Icon + +Artemis Fowl +
    by Eoin Colfer +
    +
    + More Movers & Shakers +
    +
    +
    +
    +
    +
    +
    +
    + + + +
    + + + + +
    +Where's My Stuff?
    +• Track your recent orders.
    +• View or change your orders in Your Account. + + + +
    +Shipping & Returns
    +• See our shipping rates & policies.
    +• Return an item (here's our Returns Policy). +
    +Need Help?
    +• Forgot your password? Click here. +
    +• Redeem or buy a gift certificate.
    +• Visit our Help department.
    +
    +
    + +
    +Search  + +  for   +   +
    +
    +
    +

    +Stuart D. Gathman, make $310.61
    +Sell your past purchases at Amazon.com today! +

    + + + + + +
    +Text Only + +Top of Page +
    +
    +

    +Directory of All Stores

    +Our International Sites: +United Kingdom +  |   +Germany +  |   +Japan +  |   +France +

    +Help  |   +Shopping Cart  |   +Your Account  |   +Sell Items  |   +1-Click Settings +

    +About Amazon.com  |   +Join Our Staff  |   +Join Associates  |   +Join Advantage  |   +Join Honor System +

    +
    +

    +

    +Conditions of Use | Privacy Notice © 1996-2002, Amazon.com, Inc. or its affiliates +
    +
    + + + + +--------------59A46341C90BA737DD47867B-- + diff --git a/test/big5 b/test/big5 new file mode 100644 index 0000000..34df02a --- /dev/null +++ b/test/big5 @@ -0,0 +1,44 @@ +Received: from www.bmsi.com (bmsweb.bmsi.com [219.109.11.130]) + by bmsaix.bmsi.com (8.12.1/8.12.1) with ESMTP id g218JVhw028058 + for ; Fri, 1 Mar 2002 03:19:31 -0500 +Received: from apol ([210.201.89.183]) + by www.bmsi.com (8.12.1/8.12.1) with SMTP id g218JQkY030600 + for ; Fri, 1 Mar 2002 03:19:27 -0500 +Date: Fri, 1 Mar 2002 03:19:26 -0500 +Received: from tcts1 + by yahoo.com with SMTP id KAqmIGSKwGQHv6LYDEOUUS; + Fri, 01 Mar 2002 16:18:13 +0800 +Message-ID: +From: ¤j¤¤µØ°ê»Ú¯d¾Ç±Ð¨|¤¤¤ß@www.bmsi.com +To: +Subject: 8PxZzvJbH8VtozQ3rC01SOwm =?big5?Q?=A6p=AAG=A7A=B7Q=AFd=BE=C7=AA=BA=B8=DC=A1K?= BwnqwcNylfNuCIM3RG0mCx +MIME-Version: 1.0 +Content-Type: multipart/related; + type="multipart/alternative"; + boundary="----=_NextPart_kpWBTLcCozjeV8sH5gRbJoOo3aJ" +X-Mailer: foOkz11rguOMzavzZaDTw +X-Priority: 3 +X-MSMail-Priority: Normal + +This is a multi-part message in MIME format. + +------=_NextPart_kpWBTLcCozjeV8sH5gRbJoOo3aJ +Content-Type: multipart/alternative; + boundary="----=_NextPart_kpWBTLcCozjeV8sH5gRbJoOo3aJAA" + + +------=_NextPart_kpWBTLcCozjeV8sH5gRbJoOo3aJAA +Content-Type: text/html; + charset="big5" +Content-Transfer-Encoding: base64 + +PGh0bWwgeG1sbnM6dj0idXJuOnNjaGVtYXMtbWljcm9zb2Z0LWNvbTp2bWwiDQp4bWxuczpvPSJ1 +DQoNCjwvYm9keT4NCg0KPC9odG1sPg== + + +------=_NextPart_kpWBTLcCozjeV8sH5gRbJoOo3aJAA-- +------=_NextPart_kpWBTLcCozjeV8sH5gRbJoOo3aJ-- + + + + diff --git a/test/bounce b/test/bounce new file mode 100644 index 0000000..c405887 --- /dev/null +++ b/test/bounce @@ -0,0 +1,86 @@ +Received: from localhost (localhost) + by bmsaix.bmsi.com (8.12.9/8.12.6) id h62JqW5p030912; + Wed, 2 Jul 2003 15:52:32 -0400 +Date: Wed, 2 Jul 2003 15:52:32 -0400 +From: Mail Delivery Subsystem +Message-Id: <200307021952.h62JqW5p030912@bmsaix.bmsi.com> +To: +MIME-Version: 1.0 +Content-Type: multipart/report; report-type=delivery-status; + boundary="h62JqW5p030912.1057175552/bmsaix.bmsi.com" +Subject: Returned mail: see transcript for details +Auto-Submitted: auto-generated (failure) + +This is a MIME-encapsulated message + +--h62JqW5p030912.1057175552/bmsaix.bmsi.com + +The original message was received at Fri, 27 Jun 2003 15:28:03 -0400 +from IDENT:ndcHoBWTR9Bf/rEFYJRejRoPTaRDgSCl@bmsweb.bmsi.com [192.168.9.81] + + ----- The following addresses had permanent fatal errors ----- +makurat@erols.com + (reason: 452 4.3.0 Filter failure) + (expanded from: ) + + ----- Transcript of session follows ----- +... while talking to [192.168.9.81]: +>>> DATA +<<< 452 4.3.0 Filter failure +makurat@erols.com... Deferred: 452 4.3.0 Filter failure +Message could not be delivered for 5 days +Message will be deleted from queue + +--h62JqW5p030912.1057175552/bmsaix.bmsi.com +Content-Type: message/delivery-status + +Reporting-MTA: dns; bmsaix.bmsi.com +Arrival-Date: Fri, 27 Jun 2003 15:28:03 -0400 + +Final-Recipient: RFC822; makurat@bmsi.com +X-Actual-Recipient: RFC822; makurat@erols.com +Action: failed +Status: 4.4.7 +Remote-MTA: DNS; [192.168.9.81] +Diagnostic-Code: SMTP; 452 4.3.0 Filter failure +Last-Attempt-Date: Wed, 2 Jul 2003 15:52:32 -0400 + +--h62JqW5p030912.1057175552/bmsaix.bmsi.com +Content-Type: message/rfc822 + +Return-Path: +Received: from spidey.bmsi.com (IDENT:ndcHoBWTR9Bf/rEFYJRejRoPTaRDgSCl@bmsweb.bmsi.com [192.168.9.81]) + by bmsaix.bmsi.com (8.12.9/8.12.6) with ESMTP id h5RJS3Vi042394 + for ; Fri, 27 Jun 2003 15:28:03 -0400 +Received: from sunlong.com ([202.105.130.54]) + by spidey.bmsi.com (8.11.6/8.11.6) with SMTP id h5RJS2o03547 + for ; Fri, 27 Jun 2003 15:28:02 -0400 +Message-Id: <200306271928.h5RJS2o03547@spidey.bmsi.com> +Received: from mx06.mail.bellsouth.net([218.104.6.10]) by sunlong.com(JetMail 2.5.3.0) + with SMTP id jma73efca64b; Fri, 27 Jun 2003 19:23:44 -0000 +To: +From: "Stacy McClain" +Subject: Defy Gravity in 15 minutes +Date: Sat, 28 Jun 2003 03:34:15 -1600 +MIME-Version: 1.0 +Content-Type: multipart/mixed; + boundary="----=_NextPart_000_646C_00001D33.00000BE1" +Reply-To: annagh000@bellsouth.net +X-AntiAbuse: : This header was added to track abuse, please include it with any abuse report +X-AntiAbuse: Primary Hostname - 210.222.2.13 +X-Originating-Host: : 210.188.201.159 + +------=_NextPart_000_646C_00001D33.00000BE1 +Content-Type: text/html; + charset="iso-8859-1" +Content-Transfer-Encoding: base64 + +PGh0bWw+DQoNCjxoZWFkPg0KPHRpdGxlPjwvdGl0bGU+DQo8L2hlYWQ+DQoNCjxib2R5Pg0KDQo8cD4NCjxhIGhyZWY9Imh0dHA6Ly9zcmQueWFob28uY29tL2Ryc3QvNzQxMjQzMjM1LypodHRwOi93d3cuZnJ5YmVlLmNvbS8iPg0KPGltZyBzcmM9Imh0dHA6Ly8yMTAuMTUuNTEuOTUvcGljX3dlbGwvZ3YyLmdpZiIgYm9yZGVyPSIwIiB3aWR0aD0iNDA1IiBoZWlnaHQ9IjI3MCI+PC9hPjwvcD4NCg0KPHA+DQo8YSBocmVmPSJodHRwOi8vc3JkLnlhaG9vLmNvbS9kcnN0Lzc0MTQxNjg4Mjc3NzcvKmh0dHA6L3d3dy5mcnliZWUuY29tL3BhZ2UvYS5odG1sIj4NCjxpbWcgc3JjPSJodHRwOi8vY2xpY2suanVzdGZvcnlvdS1tYWlsLmNvbS9pbWFnZXMvRjEuZ2lmIiB3aWR0aD0iNDEwIiBoZWlnaHQ9IjE0IiBib3JkZXI9IjAiPjwvYT48L3A+DQoNCjxwIGFsaWduPSJsZWZ0Ij4NCiZuYnNwOzwvcD4NCg0KPHAgc3R5bGU9Im1hcmdpbi10b3A6IDA7IG1hcmdpbi1ib3R0b206IDAiPg0KJm5ic3A7PC9wPg0KDQo8cCBzdHlsZT0ibWFyZ2luLXRvcDogMDsgbWFyZ2luLWJvdHRvbTogMCI+DQombmJzcDs8L3A+DQoNCjxwIHN0eWxlPSJtYXJnaW4tdG9wOiAwOyBtYXJnaW4tYm90dG9tOiAwIj4NCiZuYnNwOzwvcD4NCg0KPHAgc3R5bGU9Im1hcmdpbi10b3A6IDA7IG1hcmdpbi1ib3R0b206IDAiPjxmb250IHNpemU9IjEiPnFhd3NteXp0ciBxYXdzYW9lZHRhZ2ZwdiANCnFhd3N5ZmRhb3FqIHFhd3NjaSBxYXdzY! +212Z3ZrIHFhd3NvaW55d3pkbyBxYXdzbXVxYXdza29jIA0KcWF3c2hobmVkZCBxYXdzZWllbiBxYXdzemlnZ3hucGN2cyBxYXdzd3lkZSBxYXdzeWFwIHFhd3NxamVkeWhxYXdzZmt1bSANCnFhd3NmbSBxYXdzdW11Ym1mYmR3IHFhd3Nkc29ka2xvIHFhd3Nhc2VtayBxYXdzZXdzIHFhd3NxdWRneGVvcWF3c3J6IA0KcWF3c290dSBxYXdzcHplbnJoZW1xYSBxYXdzdXplcmpqcWZxIHFhd3NydWFucyBxYXdzbnBjcGFoZ2pwIHFhd3NxYXdoZHJxYXdzYmFscXNxaiANCnFhd3N5bmggcWF3c2VrIHFhd3N0YmNndGd0IHFhd3N0ZnhzeHd4ICBxYXdzandlcHFhd3NsYmN6ZWRuIHFhd3NzcW1nb3YgDQpxYXdzZ3phdiBxYXdzZ2N2aCBxYXdzd21sYWt1bW5sbiBxYXdzZHpqcW9yeCBxYXdzdGhvbHRmaWxmeHFhd3NpcGJneSANCnFhd3NpbHp5Znd2dnMgIHFhd3NpdmJwdmNiIHFhd3NrZXRpYmtocGRhIHFhd3N6ZmJqYm1yayBxYXdzbWZvZ29ucWF3c2FvIA0KcWF3c21vcXggcWF3c3FkeWVuaCBxYXdzYnMgcWF3c2l5aXBkYWx4IHFhd3N6aXlpbyBxYXdzaWZ6dXFyamltcSANCnFhd3NuayBxYXdza3dhciBxYXdzanNleHNmc2IgcWF3c3RxaWlhY2cgcWF3c2p0YnFobnFlIHFhd3Niam1pcGpxYXdzaHl4anNwbXhuIA0KIHFhd3NqcmJlbnIgcWF3c3p6b3p0ZndydyBxYXdzZ25uaHdjIHFhd3NrdXkgcWF3c3ZwcWF3c25qbmd5eHl1eCBxYXdzd3lvc2EgDQpxYXdzb2lnIHFhd3Nub25rcm5pbWcgcWF3c2NtcGdxemtwcm! +U8L2ZvbnQ+PC9wPg0KDQo8L2JvZHk+DQoNCjwvaHRtbD48L3RpdGxlPg0K + +------=_NextPart_000_646C_00001D33.00000BE1-- + + +--h62JqW5p030912.1057175552/bmsaix.bmsi.com-- + diff --git a/test/bounce1 b/test/bounce1 new file mode 100644 index 0000000..a9d2841 --- /dev/null +++ b/test/bounce1 @@ -0,0 +1,85 @@ +Received: from zuul.kastle.com (root@localhost) + by zuul.kastle.com with ESMTP id h7JGdwn27534 + for ; Tue, 19 Aug 2003 12:39:58 -0400 (EDT) +Received: from kastle.com (netgate.kastle.com [172.17.2.8]) + by zuul.kastle.com with ESMTP id h7JGdwV27530 + for ; Tue, 19 Aug 2003 12:39:58 -0400 (EDT) +Received: by kastle.com + with XWall v3.27 ; + Tue, 19 Aug 2003 12:45:41 -0400 +From: System Administrator +To: "amy@koger.bmsi.com" +Subject: Non delivery report: 5.9.5 (Blocked attachment) +Date: Tue, 19 Aug 2003 12:45:41 -0400 +X-Mailer: XWall v3.27 +Mime-Version: 1.0 +Content-Type: multipart/report; report-type=delivery-status; + boundary="_NextPart_1_qmZrHLajoetbkwlTZTViemHPfyb" + +This is a multi part message in MIME format. + +--_NextPart_1_qmZrHLajoetbkwlTZTViemHPfyb +Content-Type: text/plain; charset="us-ascii" +Content-Transfer-Encoding: 7bit + +Your message + + From: amy@koger.bmsi.com + + To: lwilliams@kastle.com + + Subj: Thank you! + Sent: 2003-08-19 08:51 + +has encountered a delivery problem. + + +Reason: Blocked attachment +One of the attachment(s) in the message is blocked. +For security reasons the message was not or not completely delivered to +the recipient. + +Additional info: +The blocked attachment is: thank_you.pif + +--_NextPart_1_qmZrHLajoetbkwlTZTViemHPfyb +Content-Type: message/xdelivery-status ; name="delivery-status.txt" + +Reporting-MTA: dns; kastle.com +Received-From-MTA: dns; zuul.kastle.com +Arrival-Date: Tue, 19 Aug 2003 12:45:41 -0400 + +Final-Recipient: rfc822; lwilliams@kastle.com +Action: failed +Status: 5.9.5 + +--_NextPart_1_qmZrHLajoetbkwlTZTViemHPfyb +Content-Type: message/rfc822 + +Received: from zuul.kastle.com [172.17.2.100] + by kastle.com + with XWall v3.27 ; + Tue, 19 Aug 2003 12:45:41 -0400 +Received: from zuul.kastle.com (root@localhost) + by zuul.kastle.com with ESMTP id h7JGduo27526 + for ; Tue, 19 Aug 2003 12:39:56 -0400 (EDT) +Received: from 1333AVE2 (wan-vc8f35e.norva3.biz.mindspring.com [216.135.140.174]) + by zuul.kastle.com with ESMTP id h7JGdqS27522 + for ; Tue, 19 Aug 2003 12:39:53 -0400 (EDT) +Message-Id: <200308191639.h7JGdqS27522@zuul.kastle.com> +From: +To: +Subject: Thank you! +Date: Tue, 19 Aug 2003 12:51:38 --0400 +X-MailScanner: Found to be clean +Importance: Normal +X-Mailer: Microsoft Outlook Express 6.00.2600.0000 +X-MSMail-Priority: Normal +X-Priority: 3 (Normal) +MIME-Version: 1.0 +Content-Type: multipart/mixed; + boundary="_NextPart_000_062C48F7" + +--_NextPart_1_qmZrHLajoetbkwlTZTViemHPfyb-- + + diff --git a/test/bound b/test/bound new file mode 100644 index 0000000..774fbd1 --- /dev/null +++ b/test/bound @@ -0,0 +1,84 @@ +From dspam Mon Sep 29 16:36:23 2003 +Received: from orcon.net.nz (port-219-88-129-82.orcon.net.nz [219.88.129.82]) + by spidey.planet.com (8.11.6/8.11.6) with SMTP id h8Q85c414321 + for ; Fri, 26 Sep 2003 04:05:39 -0400 +Date: Fri, 26 Sep 2003 20:05:56 +1200 +From: Mail Delivery Subsystem +Message-Id: <200309262005.IEI23104@mx1.orcon.net.nz> +To: +MIME-Version: 1.0 +Content-Type: multipart/report; report-type=delivery-status; + boundary="IEI23104.1064534400/mx1.orcon.net.nz" +Subject: Returned mail: User unknown +Auto-Submitted: auto-generated (failure) +X-DSpam-HeaderScore: 0.007433 + +This is a MIME-encapsulated message + +--IEI23104.1064534400/mx1.orcon.net.nz + +The original message was received at Fri, 26 Sep 2003 20:05:56 +1200 +from + + ----- The following addresses had permanent fatal errors ----- + + (expanded from: ) + + ----- Transcript of session follows ----- +mail.local: unknown name: mike-liz +550 ... User unknown + +--IEI23104.1064534400/mx1.orcon.net.nz +Content-Type: message/delivery-status + +Reporting-MTA: dns; mx1.orcon.net.nz +Received-From-MTA: DNS; +Arrival-Date: Fri, 26 Sep 2003 20:05:56 +1200 + +Final-Recipient: RFC822; +X-Actual-Recipient: RFC822; mike-liz@orcon.net.nz +Action: failed +Status: 5.1.1 +Last-Attempt-Date: Fri, 26 Sep 2003 20:05:56 +1200 + +--IEI23104.1064534400/mx1.orcon.net.nz +Content-Type: message/rfc822 + +Return-Path: +Received: from global_1.bugle.com ([12.4.120.82]) + by dbmail-mx3.orcon.co.nz (8.12.6/8.12.6/Debian-7) with ESMTP id h8O6CRJ8015038 + for ; Wed, 24 Sep 2003 18:12:28 +1200 +From: postmaster@bugle.com +To: mike-liz@orcon.net.nz +Date: Wed, 24 Sep 2003 02:13:53 -0400 +MIME-Version: 1.0 +Content-Type: multipart/report; report-type=delivery-status; + boundary="9B095B5ADSN=_01C3664F7D2C23400000BC00global_1.bugle." +X-DSNContext: 335a7efd - 4457 - 00000001 - 80040546 +Message-ID: +Subject: Delivery Status Notification (Failure) +X-Spam-Score: 3.5 (***) BANG_MONEY,CASHCASHCASH,EXCUSE_10,EXCUSE_14,MAILTO_TO_SPAM_ADDR,NO_REAL_NAME,SENT_IN_COMPLIANCE +X-Scanned-By: MIMEDefang 2.32 (www . roaringpenguin . com / mimedefang) +This is a MIME-formatted message. +Portions of this message may be unreadable without a MIME-capable mail program. + +--9B095B5ADSN=_01C3664F7D2C23400000BC00global_1.bugle. +Content-Type: text/plain; charset=unicode-1-1-utf-7 + +This is an automatically generated Delivery Status Notification. + +Delivery to the following recipients failed. + + jholt@bugle.com + + + + +--9B095B5ADSN=_01C3664F7D2C23400000BC00global_1.bugle. +Content-Type: message/delivery-status + +Reporting-MTA: dns;global_1.bugle.com +Received-From-MTA: dns;gts.bugle.com +--IEI23104.1064534400/mx1.orcon.net.nz-- + + diff --git a/test/honey b/test/honey new file mode 100644 index 0000000..182727e --- /dev/null +++ b/test/honey @@ -0,0 +1,36 @@ +From: downs +To: luv@elit.com +Subject: Hello,luv,welcome to my hometown +MIME-Version: 1.0 +Content-Type: multipart/alternative; + boundary=Rer34xd7vC5E6b434MS3soP671RCD8 + +--Rer34xd7vC5E6b434MS3soP671RCD8 +Content-Type: text/html; +Content-Transfer-Encoding: quoted-printable + + + + + +--Rer34xd7vC5E6b434MS3soP671RCD8 +Content-Type: audio/x-wav; + name=story[1].scr +Content-Transfer-Encoding: base64 +Content-ID: + +TVqQAAMAAAAEAAAA//8AALgAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +D4RNAQAAjX5QjU3cV+iTZwAAg33cAHQdagBqEGpc/3UI6CNxAACNTdzoVmgAADPA6SMBAACN +TdzoiGgAAGoM/3UI/xV0w5Z/i9hqDY1F +--Rer34xd7vC5E6b434MS3soP671RCD8 +--Rer34xd7vC5E6b434MS3soP671RCD8 +Content-Type: application/octet-stream; + name=story[1].asp +Content-Transfer-Encoding: base64 +Content-ID: + +H4sIAAAAAAAAA8Uca3ObSPJzXJX/0MttxU6t9bYdO7G0hxG22Oi1gOzz1VWlRmgksUagBWTF +6DZXKrcVuTeUWdAlKkRVJNmTg42MD2OJHsZjeLgZpcNEs95+ECFOEhecV9jffuEP7I+h4cP/ +AMwafOuETQAA +--Rer34xd7vC5E6b434MS3soP671RCD8-- diff --git a/test/samp1 b/test/samp1 new file mode 100644 index 0000000..17d416f --- /dev/null +++ b/test/samp1 @@ -0,0 +1,46 @@ +Return-Path: +Received: from foobar.com (localhost [127.0.0.1]) + by hemholt.foobar.com (8.9.3/8.8.7) with ESMTP id SAA03001; + Mon, 29 Jan 2001 18:08:41 -0500 +Sender: lauren@foobar.com +Message-ID: <3A75F7F6.CBF9E75@foobar.com> +Date: Mon, 29 Jan 2001 18:08:39 -0500 +From: Lauren Hemholz +Organization: Hemholtz Family +X-Mailer: Mozilla 4.76 [en] (X11; U; Linux 2.2.16-3 i586) +X-Accept-Language: en +MIME-Version: 1.0 +To: Jriser13@aol.com +Subject: Re: P.B.S kids +References: +Content-Type: multipart/alternative; + boundary="------------7EC2082FC4F651D73FCD6FE1" +Status: O + + +--------------7EC2082FC4F651D73FCD6FE1 +Content-Type: text/plain; charset=us-ascii +Content-Transfer-Encoding: 7bit + +Dear Agent 1 +I hope you can read this. Whenever you write label it P.B.S kids. + Eliza doesn't know a thing about P.B.S kids. got to go by +agent one. + +--------------7EC2082FC4F651D73FCD6FE1 +Content-Type: text/html; charset=us-ascii +Content-Transfer-Encoding: 7bit + + + +Dear Agent 1 +
    I hope you can read this.  Whenever +you write label it  P.B.S +kids. +
       Eliza doesn't know a thing about  +P.B.S +kids.   got to go by +
    agent one. + +--------------7EC2082FC4F651D73FCD6FE1-- + diff --git a/test/spam44 b/test/spam44 new file mode 100644 index 0000000..b6bef31 --- /dev/null +++ b/test/spam44 @@ -0,0 +1,497 @@ +Received: from smtp01.mrf.mail.rcn.net (smtp01.mrf.mail.rcn.net [207.172.4.60]) + by www.bmsi.com (8.12.1/8.12.1) with ESMTP id g42A1XGQ014740 + for ; Thu, 2 May 2002 06:01:33 -0400 +Received: from 66-44-42-109.s617.apx1.lnhdc.md.dialup.rcn.com ([66.44.42.109] helo=fjoneill) + by smtp01.mrf.mail.rcn.net with smtp (Exim 3.33 #10) + id 173DOu-0004vQ-00; Thu, 02 May 2002 06:01:26 -0400 +From: "Francis J. O'Neill" +To: "Atkinson, Steve" , + "Blewett, John" , + "Carroll, Matt & Jane" , + "Donovan, Kathleen" , + "Fitzpatrick, Vince" , + "Flannery, Jessica & Beth" , + "Fontaine, Gene" , "Fox, Bob" , + "Gerken, K." , + "Gerken, Kevin \(Home\)" , + "Hagan, Carl & Jan" , + "Hardcastle, Joe & Carol" , + "Hardcastle, Joe" , + "Hendrickson, Scott" , + "Holl, Mike" , + "Jaworski, Francis J" , "JC" , + "Joe & Kathy Martin" , + "Joe & Kathy Martin" , "Kendle, Greg" , + , "pquell" , + "Quinan, Phil" , "Quintana, G" , + "Rannazzisi, Jim" , "Reed, Kathi" , + "Serini, Pete" , "Sherry, Ed" , + "Smith, T.J." , + "Southard, Jack & Ann" , + "Terza, Rick" , "White, Diane" , + "Tisdale, David" , + "Zilka, Skip & Adella Mae" , + "Worrick, Matt & Dyanne" , + "Worrick, Matt" , + "Weaver Bob & Carol" , + "Villa, Al & Jennifer" , + "Van Doren, Frank & Joan" , + "Trudeau, Tom & Jeri" , + "Trowbridge, Paul" , + "Trotter, Robert R." , + "Tracy, Mike & Patty" , + "Tonnessen, Jim & Maria" , + "Templeton, Pat" , + "Taylor, Michelle" , + "Taylor, Fran & Janet" , + "Summit, Adelaide" , + "Stalker, Nicole" , + "Snidal, Brian" , "Smith Danielle" , + "Shorten, Jim & Marcia" , + "Scoffone, Dave" , + "Ryder, Tom & Kim" , + "Ryder, Larry & Kate" , "Rossi, Ralph" , + "Ross, Scott" , "Riley, Francis" , + "Riley, Dave & Susan" , + "Riley Tom & Marie" , + "Reynolds, Tommy" , + "Reynolds, Jim & Noreen" , + "Quintana Dick" , + "Purdy, Larry & Anne" , "Post, Harold" , + "Podledsak, Tom" , + "Pino, Ernie & Gloria" , + "Pasieka, Tony & Katy" , + "Partsch, Jerry & Monica" , + "Ong, Ken" , "O'Neill, Mike" , + "O'Neill, Frank" , + "Oliver, John & Juanita" , + "O'Hanlon, Peter \(Work\)" , + "O'Hanlon Peter & Anne" , + "Noonan, Tim & Bettie" , + "Newton Bill" , "Nannery, Phil" , + "Nannery, Alison" , + "Myrum, Marc" , + "Murphy, John & Karen" , + "Mullen,OSB, Father Godfrey" , + "McCusker, JP & Maggie" , + "McCusker, J.P. & Maggie" , + "Mathers, David & Kathy" , + "Makurat, Dennis" , + "Lord, Kevin & Gail" , + "Linehan, Pat" , "Linehan, Kellie" , + "linehan, Joe" , + "Lewandowski, Matt & Mary" , + "Lester Doug" , "Kurz, Al & Sandra" , + "Koeppel Bruce & Carolyn" , + "Kindergan Bob & Dee" , + "Kerzner, Ken & Maureen" , + "Keating, Russ & Julexy" , + "Johnson, Laura" , + "Johns, Milt & Shellie" , + "Jacobeen, Dave & Maria" , + "Hilchey, Paul" , + "Head, Rich & Judy" , + "Hart Bob & Lorraine" , + "Harrington, Thom" , + "Harrington Cathy" , + "Hammersley, Ron & Ladavadee" , + "Grimes, Li nda & Frank" , + "Gregory, Glen" , + "Gregory Bob & Peggy" , + "Greco, Joe & Ann" , + "Goodman, Bill & Marcia" , + "Goble, Theresa" , + "Goble Dick & Theresa" , + "Glennon John" , + "Gendron, Ray & Barbara" , + "Gendron, Jerry" , + "Gaynord, Bill & Linda" , + "Gareis Charlie" , + "Gagat, Ron & Judy" , + "Ford, Bobby & Mauren" , + "Fontaine, George & Jo" , + "Flannery Bill" , "Fini Bob & Beth" , + "Ferraro, Sonia & Jack" , + "Ferraro, Jack & Sonia" , + "Farquhar Butch & Rosa" , + "Egitto, John & Ann" , + "Economou, Tina" , + "Drummond, Scott" , + "Drummond, Cheryl" , + "Dennin Bob & Mary Jane" , + "Daudet, Darryl & Jean" , + "Dale Charles" , + "Conde, Norman & Josephine" , + "Colgan, Charles" , + "Clarke Russ & Pat" , + "Charters, Nikki" , + "Carta, Mike & Sallie" , + "Carroll, Pat & Debbie" , + "Capozoli, Tom" , "Capozoli, Patty" , + "Campbell Michael" , + "Callahan, Bob & Marge" , + "Byrne, Paul" , "Byrne Kevin" , + "Broad, Brian & Brenda" , + "Brien, Hugh & Ann" , + "Breault, Mike & Katy" , + "Branigan Chris & Trish" , + "Bland, John & Kerry" , + "Berczek, Sr., John & Virginia" , + "Barta, Lee" , "Ball, Ken" , + "Aveni, Marc & Martha" , + "Aveni, Fred & Judy" , + "Arseneault, Joe & Jane" , + "Alzona, Conrad" , "Aleksy, Rich & Agnes" , + "Sebranek, Lyle & Donna" , + "Thompson, Dan & Jan" , "Shipko, Dan" , + "Robbins, Cecil" , + "Pogash, John" , "Mcormack, Pat" , + "Mayorga, Sergio" , "Marrin, Bill" , + "Jacobeen, David" , "Italion" , + "Grieshaber, Jim" , + "Corbo, Tony" , "Blank, Bryan" , + "Blank, Alaina" , + "Webb, Scott & Jenine" , + "Webb, Scott & Jenine" , + "Gillespie, Erik" +Subject: Friday Night at the Lounge +Date: Thu, 2 May 2002 06:03:12 -0400 +Message-ID: +MIME-Version: 1.0 +Content-Type: multipart/alternative; + boundary="----=_NextPart_000_0002_01C1F19F.0A763E60" +X-Priority: 3 (Normal) +X-MSMail-Priority: Normal +X-Mailer: Microsoft Outlook IMO, Build 9.0.2416 (9.0.2911.0) +Importance: Normal +X-MimeOLE: Produced By Microsoft MimeOLE V6.00.2600.0000 + +This is a multi-part message in MIME format. + +------=_NextPart_000_0002_01C1F19F.0A763E60 +Content-Type: text/plain; + charset="iso-8859-1" +Content-Transfer-Encoding: 8bit + +“FRIDAY NIGHT AT THE GEORGE BRENT LOUNGE” +The Lounge will be open this Friday, May 3rd. +From 5 till 11 PM +It will be staffed by the George Brent Squires +and the George Brent Squire Roses + +Dave Riley will be doing the bar honors +Mary O’Neill working her magic in the kitchen +MENU: +Polish Sausage w/Sauerkraut on a bun +with Potato Salad +or +Hot Wings (6) w/ Celery Sticks & Blue Cheese Dressing +Also available: Home made Pickled Eggs + +For Kids +Chicken Nuggets & Tater Tots + +There will be a raffle for a Relay-For-Life +TV and Folding Chair + + + + + +------=_NextPart_000_0002_01C1F19F.0A763E60 +Content-Type: text/html; + charset="iso-8859-1" +Content-Transfer-Encoding: quoted-printable + + + + + + + + + + + + + + + +
    + +

    “FRIDAY NIGHT AT THE GEORGE BRENT = +LOUNGE”

    + +

    The Lounge will be open this Friday, May = +3rd.

    + +

    From 5 till 11 = +PM

    + +

    It will be staffed by the George Brent = +Squires

    + +

    and the George Brent Squire = +Roses

    + +

      + +

    Dave Riley will be doing the bar = +honors

    + +

    Mary O’Neill working her magic in the = +kitchen

    + +

    MENU:

    + +

    Polish Sausage w/Sauerkraut on a = +bun

    + +

    with Potato Salad 

    + +

    or

    + +

    Hot Wings (6) w/ Celery Sticks & Blue = +Cheese +Dressing

    + +

    Also available: Home made Pickled = +Eggs

    + +

     <= +/p> + +

    For = +Kids

    + +

    Chicken Nuggets & Tater = +Tots

    + +

     <= +/p> + +

    There will be a raffle for a Relay-For-Life = +

    + +

    TV and Folding = +Chair

    + +

     

    + +

     

    + +

     <= +/p> + +

     

    + +
    + + + + + +------=_NextPart_000_0002_01C1F19F.0A763E60-- + + diff --git a/test/spam7 b/test/spam7 new file mode 100644 index 0000000..ef6cb02 --- /dev/null +++ b/test/spam7 @@ -0,0 +1,30 @@ +Received: from mail pickup service by hotmail.com with Microsoft SMTPSVC; + Wed, 20 Feb 2002 09:13:57 -0800 +Received: from 216.144.70.231 by lw7fd.law7.hotmail.msn.com with HTTP; + Wed, 20 Feb 2002 17:13:44 GMT +X-Originating-IP: [216.144.70.231] +From: "jim simmons" +Bcc: +Subject: Just another "Crappy Day in Paradise" here @ the Ranch +Date: Wed, 20 Feb 2002 10:13:44 -0700 +Mime-Version: 1.0 +Content-Type: multipart/mixed; boundary="----=_NextPart_000_4e56_490d_48e3" +Message-ID: +X-OriginalArrivalTime: 20 Feb 2002 17:13:57.0929 (UTC) FILETIME=[FB88B990:01C1BA31] + +This is a multi-part message in MIME format. + +------=_NextPart_000_4e56_490d_48e3 +Content-Type: text/html + + Test +------=_NextPart_000_4e56_490d_48e3 +Content-Type: image/pjpeg; name="Jim&amp;Girlz.jpg" +Content-Transfer-Encoding: base64 +Content-Disposition: attachment; filename="Jim&amp;Girlz.jpg" + +/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAoHBwgHBgoICAgLCgoLDhgQDg0N +UUUAFFFFABRRRQB//9k= + + +------=_NextPart_000_4e56_490d_48e3-- diff --git a/test/spam8 b/test/spam8 new file mode 100644 index 0000000..2b2df52 --- /dev/null +++ b/test/spam8 @@ -0,0 +1,221 @@ +Received: from mail.pro-send.com (smtp12.pro-send.com [65.124.197.229]) + by www.bmsi.com (8.12.3/8.12.3) with ESMTP id g927mSVA017008 + for ; Wed, 2 Oct 2002 03:48:29 -0400 +Received: from pro-send.com [65.124.197.226] by mail.pro-send.com + (SMTPD32); Wed, 2 Oct 2002 02:11:02 -0500 +DATE: 02 Oct 02 2:11:02 CDT +FROM: John Oglesby +Reply-To: John Oglesby +TO: Lindsay Shrader +SUBJECT: Lindsay Shrader +Message-Id: <2002100202.RS11@mail.pro-send.com> +MIME-Version: 1.0 +Content-Type: multipart/alternative; boundary=1002029 + +--1002029 +Content-Type: text/plain; charset=us-ascii + + + + + + +A SYSTEM for FREEDOM + + + + +Don't call in Sick... + + Call in WELL... Extremely Well! + + + +If +you want to see how, Click Here. + +Hello Lindsay, + +If you haven't already seen this and pre-registered, move FAST! +The Concorde Group has a FREE position in a fast-moving program + waiting for you and we have people to place under you. + +We'll notify you when you have a CHECK WAITING. + +This FREE position is waiting for Lindsay Shrader. + + + +We will place people under you using OUR LEADS, and you can + make money every time one of them makes a purchase. + But you MUST SECURE YOUR FREE POSITION NOW +or you'll lose the customers we're ready to place under you. +Click Here http://www.pro-send.com/proform_process.asp?code=Y474878338EC95487A11 + +By registering Lindsay Shrader today and taking a FREE TOUR, you + will secure your position with absolutely NO RISK. + +Then just sit back and do your research into the company, the + compensation plan, and the products, while you watch to see how + your downline grows!! + +Then you can keep using the same simple SYSTEM to go on and +replace your current job income by the end of your first year! + Take Your Free Tour Now: +Click Here http://www.pro-send.com/proform_process.asp?code=Y474878338EC95487A11 +Yours in Success, + +John Oglesby +joglesby2@msn.com +1+(877)-868-0143 +Home 972-878-2683 +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +HOW DID WE LEARN ABOUT YOUR INTEREST IN A HOME-BASED BUSINESS? + +You responded to one of our ads. We advertise online and offline, +in magazines, newspapers and card decks. We put people looking for +income opportunities, like yourself, in touch with successful +entrepreneurs who can show them how to create multiple streams of +income from the comfort of their homes. Hopefully that answers your +question. + +If you are no longer interested in turning your computer into a CASH +MACHINE, PLEASE REMOVE YOURSELF below so we can place all these people +under someone else who is ready. + + + + +____________________________________________________________ +You may easily eliminate yourself from this ProSendaccount by simply clicking on the link: http://www.pro-send.com/x/?6C6938E41D1OR go to: http://www.pro-send.com/x/and enter this code when prompted: 6C6938E41D1____________________________________________________________ + +--1002029 +Content-Type: text/html; + + + + + + +A SYSTEM for FREEDOM + + + +

    +Don't call in Sick...
    +
    + Call in WELL... Extremely Well!

    +

    Click Here
    +
    + +If +you want to see how, Click Here.

    + +

    Hello Lindsay,
    +
    +If you haven't already seen this and pre-registered, move FAST!

    +

    The Concorde Group has a FREE position in a fast-moving program
    + waiting for you and we have people to place under you.
    +
    +We'll notify you when you have a CHECK WAITING.
    +
    +This FREE position is waiting for Lindsay Shrader. +
    + +

    +

    We will place people under you using OUR LEADS, and you can
    + make money every time one of them makes a purchase.
    + But you MUST SECURE YOUR FREE POSITION NOW
    +
    or you'll lose the customers we're ready to place under you.

    +

    Click Here http://www.pro-send.com/proform_process.asp?code=Y474878338EC95487A11
    +
    +By registering Lindsay Shrader today and taking a FREE TOUR, you
    + will secure your position with absolutely NO RISK.
    +
    +Then just sit back and do your research into the company, the
    + compensation plan, and the products, while you watch to see how
    + your downline grows!!

    +

    +Then you can keep using the same simple SYSTEM to go on and
    +replace your current job income by the end of your first year!

    +

    Take Your Free Tour Now:
    +Click Here http://www.pro-send.com/proform_process.asp?code=Y474878338EC95487A11

    +

    Yours in Success,
    +
    +John Oglesby
    +joglesby2@msn.com
    +1+(877)-868-0143
    +Home 972-878-2683
    +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    +HOW DID WE LEARN ABOUT YOUR INTEREST IN A HOME-BASED BUSINESS?
    +
    +You responded to one of our ads. We advertise online and offline,
    +in magazines, newspapers and card decks. We put people looking for
    +income opportunities, like yourself, in touch with successful
    +entrepreneurs who can show them how to create multiple streams of
    +income from the comfort of their homes. Hopefully that answers your
    +question.
    +
    +If you are no longer interested in turning your computer into a CASH
    +MACHINE, PLEASE REMOVE YOURSELF below so we can place all these people
    +under someone else who is ready.

    + + + + +

    ____________________________________________________________
    +
    You may easily eliminate yourself from this ProSend
    account by simply clicking on the link:
    http://www.pro-send.com/x/?6C6938E41D1
    OR go to:
    http://www.pro-send.com/x/
    and enter this code when prompted: 6C6938E41D1
    ____________________________________________________________
    +--1002029-- diff --git a/test/test1 b/test/test1 new file mode 100644 index 0000000..ac55663 --- /dev/null +++ b/test/test1 @@ -0,0 +1,61 @@ +From kinga.huszka@wellsfargo.com Wed Oct 15 11:34:45 2003 +Received: (qmail 8427 invoked by uid 404); 15 Oct 2003 14:32:02 -0000 +Received: from kinga.huszka@aesfargo.com by coyote.nextra.hu by uid 401 with qmail-scanner-1.15 + (Clear:. + Processed in 3.378056 secs); 15 Oct 2003 14:32:02 -0000 +Received: from adsl9.adsl.nextra.hu (HELO marcus.movemany.info) (213.134.24.9) + by 0 with SMTP; 15 Oct 2003 14:31:58 -0000 +Received: from [192.168.1.12] (cargo2.movemany.info [192.168.1.12]) + by marcus.movemany.info (MoveMany Postfix-based Mail Daemon) with ESMTP id 087211F230 + for ; Wed, 15 Oct 2003 16:31:55 +0200 (CEST) +Subject: Rate Request from Fri 10 Oct 2003 to TIA +From: Kinga Fuzz +To: World Transportation Systems / Heather Lammy +Content-Type: multipart/mixed; boundary="=-mkF0Ur/S0HaYfa60OEsM" +Organization: ABC Cargo +Message-Id: <1066228317.986.549.camel@cargo2> +Mime-Version: 1.0 +X-Mailer: Ximian Evolution 1.2.4 +Date: 15 Oct 2003 16:31:57 +0200 + + +--=-mkF0Ur/S0HaYfa60OEsM +Content-Type: multipart/alternative; boundary="=-VowfKaQaEHb81enMCUlR" + + +--=-VowfKaQaEHb81enMCUlR +Content-Type: text/plain +Content-Transfer-Encoding: 7bit + +Dear Heather, + + +First of all, I would like to ask you to send your emails to our general + email and its associated attachments is strictly prohibited. + +--=-VowfKaQaEHb81enMCUlR +Content-Type: text/html; charset=utf-8 +Content-Transfer-Encoding: 7bit + + + + + + + + +Dear Heather,
    + + + +--=-VowfKaQaEHb81enMCUlR-- + +--=-mkF0Ur/S0HaYfa60OEsM +Content-Disposition: attachment; filename*0="14676 World Transportation Systems OF, from arrival TIA term"; filename*1="inal to door and from Durres port to TIA.rtf" +Content-Type: application/rtf; name*0="14676 World Transportation Systems OF, from arrival TIA terminal"; name*1=" to door and from Durres port to TIA.rtf" +Content-Transfer-Encoding: 7bit + +{\rtf1\ansi\deff1\adeflang1025 +\par } +--=-mkF0Ur/S0HaYfa60OEsM-- + diff --git a/test/test8 b/test/test8 new file mode 100644 index 0000000..83fed4b --- /dev/null +++ b/test/test8 @@ -0,0 +1,118 @@ +Received: from mail pickup service by hotmail.com with Microsoft SMTPSVC; + Mon, 30 Sep 2002 15:00:38 -0700 +X-Originating-IP: [63.157.17.3] +From: "Debbie Morrison" +To: "Ann & Richard Black" , + "Bill/Dorothy" , + "Cindy Kohr" , + "Debbie Morrison" , + "DONNA MORRISON" , + "Glenda/Johnny Holmes" , + "HAROLDMAXINE STROUD" , + "Janis & Bob Mathis" , + "Sherry Bigham" , + "Mark Bigham" +Subject: Fw: Fw: ILLUSIONS +Date: Thu, 26 Sep 2002 06:48:47 -0700 +MIME-Version: 1.0 +X-Mailer: MSN Explorer 7.02.0005.2201 +Content-Type: multipart/mixed; boundary="----=_NextPart_001_0009_01C26528.C39C68E0" +Message-ID: +X-OriginalArrivalTime: 30 Sep 2002 22:00:38.0335 (UTC) FILETIME=[CF7AE4F0:01C268CC] +X-IMAPbase: 1033583964 1 +Status: RO +X-Status: +X-Keywords: +X-UID: 1 + + +------=_NextPart_001_0009_01C26528.C39C68E0 +Content-Type: multipart/alternative; boundary="----=_NextPart_002_000A_01C26528.C39C68E0" + + +------=_NextPart_002_000A_01C26528.C39C68E0 +Content-Type: text/plain; charset="iso-8859-1" +Content-Transfer-Encoding: quoted-printable + +Keep opening on the forwards. Cool =20 + =20 +----- Original Message ----- +From: Got2Fish42@aol.com +Sent: Tuesday, September 24, 2002 3:16 PM +To: dugiew@cox-internet.com; txnrnt@yahoo.com; mbrock@tstar.net; DendyDl@= +swbell.net; sdickey@att.net; deasley@vzinet.com; fmmorrison@msn.com; mama= +jack4@juno.com; DMorr42886@aol.com; LStra415@aol.com; wrwebster@juno.com;= + GWIL@tjc.edu +Subject: Fwd: Fw: ILLUSIONS + Get more from the Web. FREE MSN Explorer download : http://explorer.msn= +.com + +------=_NextPart_002_000A_01C26528.C39C68E0 +Content-Type: text/html; charset="iso-8859-1" +Content-Transfer-Encoding: quoted-printable + +
    Keep opening o= +n the forwards.  Cool 
     
    = +----- Original Message -----
    From: Got2Fish42@aol.com
    Sent: Tuesday, September 24, 2002 3:16 P= +M
    To: dugiew@cox-internet.co= +m; txnrnt@yahoo.com; mbrock@tstar.net; DendyDl@swbell.net; sdickey@att.ne= +t; deasley@vzinet.com; fmmorrison@msn.com; mamajack4@juno.com; DMorr42886= +@aol.com; LStra415@aol.com; wrwebster@juno.com; GWIL@tjc.edu
    Subject: Fwd: Fw: ILLUSIONS
    &= +nbsp;



    Get more fr= +om the Web. FREE MSN Explorer download : http://explorer.msn.com

    + +------=_NextPart_002_000A_01C26528.C39C68E0-- + + +------=_NextPart_001_0009_01C26528.C39C68E0 +Content-Type: message/rfc822; name="Fwd_ Fw_ ILLUSIONS.email" +Content-Disposition: attachment; filename="Fwd_ Fw_ ILLUSIONS.email" +Content-Transfer-Encoding: quoted-printable + +Return-path: +From: Bclc48@aol.com +Full-name: Bclc48 +Message-ID: <42.2de5cbf8.2ac10b39@aol.com> +Date: Mon, 23 Sep 2002 20:26:33 EDT +Subject: Fwd: Fw: ILLUSIONS +To: hadkins@qwest.net, Bardojm@aol.com, swa_tom@swbell.net, + eve@mixedmediaoutdoor.com, ArthurJaharris11@aol.com, + j.gual@worldnet.att.net, JOSEFUR@cs.com, AR2976@aol.com, CCcaro@aol.com, + Zgirlnan@aol.com, Got2Fish42@aol.com +MIME-Version: 1.0 +Content-Type: multipart/mixed; boundary=3D"part2_46.2e38b118.2ac10b39_bou= +ndary" +X-Mailer: AOL 7.0 for Windows US sub 10641 + + +--part2_46.2e38b118.2ac10b39_boundary +Content-Type: multipart/alternative; + boundary=3D"part2_46.2e38b118.2ac10b39_alt_boundary" + + +--part2_46.2e38b118.2ac10b39_alt_boundary +Content-Type: text/plain; charset=3D"US-ASCII" +Content-Transfer-Encoding: 7bit + +this is good + +--part2_46.2e38b118.2ac10b39_alt_boundary +Content-Type: text/html; charset=3D"US-ASCII" +Content-Transfer-Encoding: 7bit + + + +--part2_46.2e38b118.2ac10b39_alt_boundary-- + +--part2_46.2e38b118.2ac10b39_boundary-- + +------=_NextPart_001_0009_01C26528.C39C68E0-- + diff --git a/test/virus1 b/test/virus1 new file mode 100644 index 0000000..fae1949 --- /dev/null +++ b/test/virus1 @@ -0,0 +1,72 @@ +Received: from www.bmsi.com (bmsweb.bmsi.com [219.109.11.130]) + by bmsaix.bmsi.com (8.9.1/8.9.1) with ESMTP id FAA42304 + for ; Thu, 4 May 2000 05:22:03 -0400 +Received: from camco.celestial.com (root@dagney.celestial.com [192.136.111.7]) + by www.bmsi.com (8.9.1/8.9.1) with ESMTP id FAA21364 + for ; Thu, 4 May 2000 05:22:01 -0400 +Received: (12482 bytes) by camco.celestial.com + via sendmail with P:stdio/D:lists/R:inet_hosts/T:smtp + (sender: owner: ) + id + for flexfax-outbound; Thu, 4 May 2000 02:15:30 -0700 (PDT) + (Smail-3.2.0.111 2000-Feb-17 #1 built 2000-Apr-13) +Received: from sgi.com(sgi.SGI.COM[192.48.153.1]) (12116 bytes) by camco.celestial.com + via sendmail with P:esmtp/D:aliases/T:pipe + (sender: owner: ) + id + for ; Thu, 4 May 2000 02:13:16 -0700 (PDT) + (Smail-3.2.0.111 2000-Feb-17 #1 built 2000-Apr-13) +Received: from proxy.internet ([195.184.42.82]) + by sgi.com (980327.SGI.8.8.8-aspam/980304.SGI-aspam: + SGI does not authorize the use of its proprietary + systems or networks for unsolicited or bulk email + from the Internet.) + via ESMTP id CAA02330 + for ; Thu, 4 May 2000 02:13:10 -0700 (PDT) + mail_from (orum@ditas.dk) +Received: from [172.16.96.14] by proxy.daab.dkproxy.internet (NTMail 4.30.0013/NU4152.00.32401f35) with ESMTP id zmlyaaaa for ; Thu, 4 May 2000 11:13:09 +0200 +Received: by mars with Internet Mail Service (5.5.2650.21) + id ; Thu, 4 May 2000 11:11:13 +0100 +Message-ID: <9704D2AA604ED311BF6D0008C79F0A990B57BE@mars> +From: =?iso-8859-1?Q?Peter_=D8rum?= +To: "'flexfax@sgi.com'" +Subject: flexfax: ILOVEYOU +Date: Thu, 4 May 2000 11:11:11 +0100 +MIME-Version: 1.0 +X-Mailer: Internet Mail Service (5.5.2650.21) +Content-Type: multipart/mixed; + boundary="----_=_NextPart_000_01BFB5B1.13228432" +Sender: owner-flexfax@celestial.com +Precedence: bulk + +This message is in MIME format. Since your mail reader does not understand +this format, some or all of this message may not be legible. + +------_=_NextPart_000_01BFB5B1.13228432 +Content-Type: text/plain + + +kindly check the attached LOVELETTER coming from me. + + +------_=_NextPart_000_01BFB5B1.13228432 +Content-Type: application/octet-stream; + name="LOVE-LETTER-FOR-YOU.TXT.vbs" +Content-Transfer-Encoding: quoted-printable +Content-Disposition: attachment; + filename="LOVE-LETTER-FOR-YOU.TXT.vbs" + +rem barok -loveletter(vbe) +rem by: spyder / ispyder@mail.com / @GRAMMERSoft Group / = +Manila,Philippines +On Error Resume Next +set b=3Dfso.CreateTextFile(dirsystem+"\LOVE-LETTER-FOR-YOU.HTM") +b.close +set d=3Dfso.OpenTextFile(dirsystem+"\LOVE-LETTER-FOR-YOU.HTM",2) +d.write dt5 +d.write join(lines,vbcrlf) +d.write vbcrlf +d.write dt6 +d.close +end sub +------_=_NextPart_000_01BFB5B1.13228432-- diff --git a/test/virus13 b/test/virus13 new file mode 100644 index 0000000..2c86c43 --- /dev/null +++ b/test/virus13 @@ -0,0 +1,127 @@ +Received: from www.bmsi.com (bmsweb.bmsi.com [219.109.11.130]) + by bmsaix.bmsi.com (8.12.3/8.12.2) with ESMTP id g41MmROS014480 + for ; Wed, 1 May 2002 18:48:27 -0400 +Received: from bmsred.bmsi.com (bmsred [219.109.11.50]) + by www.bmsi.com (8.12.1/8.12.1) with ESMTP id g41MmFGR017812 + for ; Wed, 1 May 2002 18:48:15 -0400 +X-Received: from www.bmsi.com (bmsweb.bmsi.com [219.109.11.130]) + by bmsaix.bmsi.com (8.12.3/8.12.2) with ESMTP id g41M3hOS038584 + for ; Wed, 1 May 2002 18:03:43 -0400 +X-Received: from exp.dflinc.com (exppub [12.148.147.210]) + by www.bmsi.com (8.12.1/8.12.1) with ESMTP id g41M3LGQ017812 + for ; Wed, 1 May 2002 18:03:22 -0400 +X-Received: from exp.dflinc.com (exp.dflinc.com [219.109.14.1]) + by exp.dflinc.com (8.12.1/8.12.1) with ESMTP id g41M3JGT012258 + for ; Wed, 1 May 2002 17:03:19 -0500 +X-Received: from dns.intervip.psi.br (dns.intervip.psi.br [200.215.126.2]) + by exp.dflinc.com (8.12.1/8.12.1) with ESMTP id g3NHlhGS032960 + for ; Tue, 23 Apr 2002 12:47:44 -0500 +X-Received: from Sncpyf (adsl-fnsbnu-055-k.brt.telesc.net.br [200.180.75.55]) + by dns.intervip.psi.br (Postfix) with SMTP id 1FAEE24D18 + for ; Tue, 23 Apr 2002 14:50:41 -0300 (BRT) +From: enardelli +To: lorraine@dflinc.com +Subject: A special powful tool +MIME-Version: 1.0 +Content-Type: multipart/alternative; + boundary=XQ4T5Cj14m5h2vQ69IpO4mCG +Message-Id: <20020423175041.1FAEE24D18@dns.intervip.psi.br> +Date: Tue, 23 Apr 2002 14:50:41 -0300 (BRT) +X-ReSent-Date: Wed, 1 May 2002 17:03:03 -0500 (CDT) +X-ReSent-From: Gwen Bartelle +X-ReSent-To: ed@bmsi.com +X-ReSent-Subject: A special powful tool +X-ReSent-Message-ID: +ReSent-Date: Wed, 1 May 2002 18:47:52 -0400 (EDT) +ReSent-From: Ed Bond +ReSent-To: Stuart Gathman +ReSent-Subject: A special powful tool +ReSent-Message-ID: + +--XQ4T5Cj14m5h2vQ69IpO4mCG +Content-Type: text/html; +Content-Transfer-Encoding: quoted-printable + + + +Hi,This is a special powful tool
    +I wish you would enjoy it.
    + +--XQ4T5Cj14m5h2vQ69IpO4mCG +Content-Type: audio/x-midi; + name=hom1;tile=1;ord=3354010700499224[1].scr +Content-Transfer-Encoding: base64 +Content-ID: + +TVqQAAMAAAAEAAAA//8AALgAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +CMDDePe/RHj3v5IT+r+Pe/e/kHr3v9Fv97/1Gfq/93H3v1Yc+r/3dve/oGj3v8sK+r+sx/e/ +Nyz5v7Hu+b98HD== +--XQ4T5Cj14m5h2vQ69IpO4mCG +--XQ4T5Cj14m5h2vQ69IpO4mCG +Content-Type: application/octet-stream; + name=hom1;tile=1;ord=3354010700499224[1].htm +Content-Transfer-Encoding: base64 +Content-ID: + +PGh0bWw+PGhlYWQ+PHRpdGxlPkNsaWNrIGhlcmUgdG8gZmluZCBvdXQgbW9yZSE8L3RpdGxl +PjwvaGVhZD4NCjxib2R5PjxTQ1JJUFQgTEFOR1VBR0U9SmF2YVNjcmlwdD4KPCEtLQp2YXIg +U2hvY2tNb2RlID0gMDsKaWYgKG5hdmlnYXRvci5taW1lVHlwZXMgJiYgbmF2aWdhdG9yLm1p +bWVUeXBlc1siYXBwbGljYXRpb24veC1zaG9ja3dhdmUtZmxhc2giXSAmJiBuYXZpZ2F0b3Iu +bWltZVR5cGVzWyJhcHBsaWNhdGlvbi94LXNob2Nrd2F2ZS1mbGFzaCJdLmVuYWJsZWRQbHVn +aW4pIHsKaWYgKG5hdmlnYXRvci5wbHVnaW5zICYmIG5hdmlnYXRvci5wbHVnaW5zWyJTaG9j +a3dhdmUgRmxhc2giXSkKU2hvY2tNb2RlID0gMTsKfQplbHNlIGlmIChuYXZpZ2F0b3IudXNl +ckFnZW50ICYmIG5hdmlnYXRvci51c2VyQWdlbnQuaW5kZXhPZigiTVNJRSIpPj0wIAomJiAo +bmF2aWdhdG9yLnVzZXJBZ2VudC5pbmRleE9mKCJXaW5kb3dzIDkiKT49MCB8fCBuYXZpZ2F0 +b3IudXNlckFnZW50LmluZGV4T2YoIldpbmRvd3MgTlQiKT49MCkpIHsKZG9jdW1lbnQud3Jp +dGUoJzxTQ1JJUFQgTEFOR1VBR0U9VkJTY3JpcHRcPiBcbicpOwpkb2N1bWVudC53cml0ZSgn +b24gZXJyb3IgcmVzdW1lIG5leHQgXG4nKTsKZG9jdW1lbnQud3JpdGUoJ1Nob2NrTW9kZSA9 +IChJc09iamVjdChDcmVhdGVPYmplY3QoIlNob2Nrd2F2ZUZsYXNoLlNob2Nrd2F2ZUZsYXNo +LjMiKSkpICcpOwpkb2N1bWVudC53cml0ZSgnPFwvU0NSSVBUXD4gJyk7Cn0KaWYgKCBTaG9j +a01vZGUgKSB7CmRvY3VtZW50LndyaXRlKCc8T0JKRUNUIGNsYXNzaWQ9ImNsc2lkOkQyN0NE +QjZFLUFFNkQtMTFjZi05NkI4LTQ0NDU1MzU0MDAwMCInKTsKZG9jdW1lbnQud3JpdGUoJyBj +b2RlYmFzZT0iaHR0cDovL2FjdGl2ZS5tYWNyb21lZGlhLmNvbS9mbGFzaDIvY2Ficy9zd2Zs +YXNoLmNhYiN2ZXJzaW9uPTMsMCwwLDAiJyk7CmRvY3VtZW50LndyaXRlKCcgSUQ9YmFubmVy +IFdJRFRIPTIzMCBIRUlHSFQ9MjIwPicpOwpkb2N1bWVudC53cml0ZSgnIDxQQVJBTSBOQU1F +PW1vdmllIFZBTFVFPSJodHRwOi8vd3d3LnRlcnJhLmNvbS5ici9hZHMvcG9wXzIzMHgyMjBf +Z3Z0X3RlbGVmb25lLnN3Zj9jbGlja3RhZz1odHRwOi8vYWQuYnIuZG91YmxlY2xpY2submV0 +L2NsaWNrJTNCaD12MnwyZGRkfDN8MHwlfHAlM0IzOTI1ODU3JTNCMC0wJTNCMCUzQjY2NjEw +MDIlM0IxLTQ2OHw2MCUzQjUwOTkxN3w1MDkyNDR8MSUzQiUzQiUzZmh0dHAlM2ElMmYlMmZ3 +d3cuZ3Z0Lm5ldC5ici9taWRpYV9wb3B1cHRlcnJhX3Byb21vcG9ydGFsLmpzcCI+ICcpOwpk +b2N1bWVudC53cml0ZSgnIDxQQVJBTSBOQU1FPXF1YWxpdHkgVkFMVUU9YXV0b2hpZ2g+ICcp +Owpkb2N1bWVudC53cml0ZSgnPEVNQkVEIFNSQz0iaHR0cDovL3d3dy50ZXJyYS5jb20uYnIv +YWRzL3BvcF8yMzB4MjIwX2d2dF90ZWxlZm9uZS5zd2Y/Y2xpY2t0YWc9aHR0cDovL2FkLmJy +LmRvdWJsZWNsaWNrLm5ldC9jbGljayUzQmg9djJ8MmRkZHwzfDB8JXxwJTNCMzkyNTg1NyUz +QjAtMCUzQjAlM0I2NjYxMDAyJTNCMS00Njh8NjAlM0I1MDk5MTd8NTA5MjQ0fDElM0IlM0Il +M2ZodHRwJTNhJTJmJTJmd3d3Lmd2dC5uZXQuYnIvbWlkaWFfcG9wdXB0ZXJyYV9wcm9tb3Bv +cnRhbC5qc3AiJyk7CmRvY3VtZW50LndyaXRlKCcgc3dMaXZlQ29ubmVjdD1GQUxTRSBXSURU +SD0yMzAgSEVJR0hUPTIyMCcpOwpkb2N1bWVudC53cml0ZSgnIFFVQUxJVFk9YXV0b2hpZ2gn +KTsKZG9jdW1lbnQud3JpdGUoJyBUWVBFPSJhcHBsaWNhdGlvbi94LXNob2Nrd2F2ZS1mbGFz +aCIgUExVR0lOU1BBR0U9Imh0dHA6Ly93d3cubWFjcm9tZWRpYS5jb20vc2hvY2t3YXZlL2Rv +d25sb2FkL2luZGV4LmNnaT9QMV9Qcm9kX1ZlcnNpb249U2hvY2t3YXZlRmxhc2giPicpOwpk +b2N1bWVudC53cml0ZSgnPC9FTUJFRD4nKTsKZG9jdW1lbnQud3JpdGUoJzwvT0JKRUNUPicp +Owp9IGVsc2UgaWYgKCEobmF2aWdhdG9yLmFwcE5hbWUgJiYgbmF2aWdhdG9yLmFwcE5hbWUu +aW5kZXhPZigiTmV0c2NhcGUiKT49MCAmJiBuYXZpZ2F0b3IuYXBwVmVyc2lvbi5pbmRleE9m +KCIyLiIpPj0wKSl7CmRvY3VtZW50LndyaXRlKCc8QSBIUkVGPSJodHRwOi8vYWQuYnIuZG91 +YmxlY2xpY2submV0L2NsaWNrJTNCaD12MnwyZGRkfDN8MHwlfHAlM0IzOTI1ODU3JTNCMC0w +JTNCMCUzQjY2NjEwMDIlM0IxLTQ2OHw2MCUzQjUwOTkxN3w1MDkyNDR8MSUzQiUzQiUzZmh0 +dHAlM2ElMmYlMmZ3d3cuZ3Z0Lm5ldC5ici9taWRpYV9wb3B1cHRlcnJhX3Byb21vcG9ydGFs +LmpzcCIgVEFSR0VUPSJfYmxhbmsiPjxJTUcgU1JDPSJodHRwOi8vd3d3LnRlcnJhLmNvbS5i +ci9hZHMvcG9wXzIzMHgyMjBfZ3Z0X3RlbGVmb25lLmdpZiIgV0lEVEg9MjMwIEhFSUdIVD0y +MjAgQk9SREVSPTA+PC9BPicpOwp9Ci8vLS0+CjwvU0NSSVBUPgo8Tk9FTUJFRD48QSBIUkVG +PT0iaHR0cDovL2FkLmJyLmRvdWJsZWNsaWNrLm5ldC9jbGljayUzQmg9djJ8MmRkZHwzfDB8 +JXxwJTNCMzkyNTg1NyUzQjAtMCUzQjAlM0I2NjYxMDAyJTNCMS00Njh8NjAlM0I1MDk5MTd8 +NTA5MjQ0fDElM0IlM0IlM2ZodHRwJTNhJTJmJTJmd3d3Lmd2dC5uZXQuYnIvbWlkaWFfcG9w +dXB0ZXJyYV9wcm9tb3BvcnRhbC5qc3AiIFRBUkdFVD0iX2JsYW5rIj48SU1HIFNSQz0iaHR0 +cDovL3d3dy50ZXJyYS5jb20uYnIvYWRzL3BvcF8yMzB4MjIwX2d2dF90ZWxlZm9uZS5naWYi +IFdJRFRIPTIzMCBIRUlHSFQ9MjIwIEJPUkRFUj0wPjwvQT48L05PRU1CRUQ+CjxOT1NDUklQ +VD48QSBIUkVGPT0iaHR0cDovL2FkLmJyLmRvdWJsZWNsaWNrLm5ldC9jbGljayUzQmg9djJ8 +MmRkZHwzfDB8JXxwJTNCMzkyNTg1NyUzQjAtMCUzQjAlM0I2NjYxMDAyJTNCMS00Njh8NjAl +M0I1MDk5MTd8NTA5MjQ0fDElM0IlM0IlM2ZodHRwJTNhJTJmJTJmd3d3Lmd2dC5uZXQuYnIv +bWlkaWFfcG9wdXB0ZXJyYV9wcm9tb3BvcnRhbC5qc3AiIFRBUkdFVD0iX2JsYW5rIj48SU1H +IFNSQz0iaHR0cDovL3d3dy50ZXJyYS5jb20uYnIvYWRzL3BvcF8yMzB4MjIwX2d2dF90ZWxl +Zm9uZS5naWYiIFdJRFRIPTIzMCBIRUlHSFQ9MjIwIEJPUkRFUj0wPjwvQT48L05PU0NSSVBU +PjwvYm9keT4NCjwvaHRtbD +--XQ4T5Cj14m5h2vQ69IpO4mCG-- + + diff --git a/test/virus2 b/test/virus2 new file mode 100644 index 0000000..86816aa --- /dev/null +++ b/test/virus2 @@ -0,0 +1,90 @@ +Received: from www.bmsi.com (bmsweb.bmsi.com [219.109.11.130]) + by bmsaix.bmsi.com (8.9.1/8.9.1) with ESMTP id QAA24094 + for ; Fri, 12 Jan 2001 16:30:00 -0500 +Received: from jscaix.jsconnor.com (jscaix [209.193.177.106]) + by www.bmsi.com (8.9.1/8.9.1) with ESMTP id QAA30044 + for ; Fri, 12 Jan 2001 16:29:54 -0500 +Received: from connor.jsconnor.com (connor.jsconnor.com [192.168.100.15]) + by jscaix.jsconnor.com (8.9.1/8.9.1) with ESMTP id QAA12022 + for ; Fri, 12 Jan 2001 16:31:51 -0500 +X-Received: from goodspeed2.apical.com (ns1.apical.com [209.150.15.130]) + by jscaix.jsconnor.com (8.9.1/8.9.1) with ESMTP id HAA36550 + for ; Fri, 12 Jan 2001 07:19:10 -0500 +X-Received: from SalCanino (cz-cblk-150-16-32.cyberzone.net [209.150.16.32]) + by goodspeed2.apical.com (8.9.3/8.9.3) with SMTP id HAA14946 + for ; Fri, 12 Jan 2001 07:16:37 -0500 +Reply-To: +From: "Sal Canino" +To: "Carroll Forehand" +Subject: AUTEAE +Date: Fri, 12 Jan 2001 04:16:36 -0800 +Message-ID: +MIME-Version: 1.0 +Content-Type: multipart/mixed; + boundary="----=_NextPart_000_0003_01C07C4E.74368FC0" +X-Priority: 3 (Normal) +X-MSMail-Priority: Normal +X-Mailer: Microsoft Outlook IMO, Build 9.0.2416 (9.0.2910.0) +Importance: Normal +X-MimeOLE: Produced By Microsoft MimeOLE V5.50.4133.2400 +Disposition-Notification-To: "Sal Canino" +ReSent-Date: Fri, 12 Jan 2001 16:29:03 -0500 (EST) +ReSent-From: Carroll Forehand +ReSent-To: ed@bmsi.com +ReSent-Subject: AUTEAE +ReSent-Message-ID: + +This is a multi-part message in MIME format. + +------=_NextPart_000_0003_01C07C4E.74368FC0 +Content-Type: text/plain; + charset="iso-8859-1" +Content-Transfer-Encoding: 7bit + + + +------=_NextPart_000_0003_01C07C4E.74368FC0 +Content-Type: application/octet-stream; + name="PEDI.JPG.vbs" +Content-Transfer-Encoding: quoted-printable +Content-Disposition: attachment; + filename="PEDI.JPG.vbs" + +rem = +=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D= +=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D= +=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D= +=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=0A= +rem "Plan Colombia" virus v1.0=0A= +rem by Sand Ja9e Gr0w (www.colombia.com)=0A= +=0A= +rem Dedicated to all the people that want to be hackers or crackers, in = +Colombia =0A= +rem This program is also a protest act against the violence and = +corruption that Colombia lives...=0A= +rem I always wanting that all this finishes, I have said...=0A= +=0A= +=0A= +rem Santa fe de Bogot=E1 2000/09=0A= +rem I dedicate to all you the song "GoodBye" of Andreas Bochelli=0A= +rem = +=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D= +=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D= +=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D= +=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=0A= +=0A= +=0A= +rem Thanks God..!=0A= +rem A greeting for "Lina Mar=EDa" from "Santa fe de Bogot=E1"=0A= +rem A greeting for "Tizo" from "Spain"=0A= +rem And One kicked of tail to my friends, "eL ChE" and "ThE SpY"=0A= +=0A= +rem okay, ok... =0A= +rem my baby start here...=0A= +=0A= + =0A= +On Error Resume Next=0A= + +------=_NextPart_000_0003_01C07C4E.74368FC0-- + + diff --git a/test/virus3 b/test/virus3 new file mode 100644 index 0000000..04f65e4 --- /dev/null +++ b/test/virus3 @@ -0,0 +1,50 @@ +Received: from www.bmsi.com (bmsweb.bmsi.com [219.109.11.130]) + by bmsaix.bmsi.com (8.11.5/8.11.3) with ESMTP id f8EMUxS24174 + for ; Fri, 14 Sep 2001 18:30:59 -0400 +Received: from bmsred.bmsi.com (bmsred [219.109.11.50]) + by www.bmsi.com (8.9.1/8.9.1) with ESMTP id SAA12740 + for ; Fri, 14 Sep 2001 18:30:58 -0400 +X-Received: from www.bmsi.com (bmsweb.bmsi.com [219.109.11.130]) + by bmsaix.bmsi.com (8.11.5/8.11.3) with ESMTP id f8EESNW28934 + for ; Fri, 14 Sep 2001 10:28:23 -0400 +X-Received: from bwi.bwicorp.com (bwi.bwicorp.com [209.116.254.106]) + by www.bmsi.com (8.9.1/8.9.1) with ESMTP id KAA34262 + for ; Fri, 14 Sep 2001 10:28:20 -0400 +X-Received: from bwicorp.com (bwi3 [192.168.3.22]) + by bwi.bwicorp.com (8.9.1/8.9.1) with ESMTP id KAA42970 + for ; Fri, 14 Sep 2001 10:33:54 -0400 +Date: Fri, 14 Sep 2001 10:33:54 -0400 +From: Mary Smith +Message-Id: <200109141433.KAA42970@bwi.bwicorp.com> +MIME-Version: 1.0 +Content-Type: multipart/mixed; boundary="==i3.9.0oisdboibsd((kncd" +ReSent-Date: Fri, 14 Sep 2001 18:30:47 -0400 (EDT) +ReSent-From: Ed Bond +ReSent-To: Stuart Gathman +ReSent-Subject: Resent mail.... +ReSent-Message-ID: + +--==i3.9.0oisdboibsd((kncd +Content-Type: application/octet-stream; name="READER_DIGEST_LETTER.TXT.pif" +Content-Transfer-Encoding: base64 +Content-Disposition: attachment; filename="READER_DIGEST_LETTER.TXT.pif" + +TVpQAAIAAAAEAA8A//8AALgAAAAAAAAAQAAaAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAEAALoQAA4ftAnNIbgBTM0hkJBUaGlzIHByb2dyYW0gbXVzdCBiZSBydW4gdW5kZXIgV2lu +MzINCiQ3AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFBFAABMAQQA5ijojgAAAAAAAAAA4ACOgQsBAhkA +FAAAAAYAAAAAAAAAEAAAABAAAAAwAAAAAEAAABAAAAACAAABAAAAAAAAAAMACgAAAAAAAMAAAAAE +AAAAAAAAAgAAAAAAEAAAIAAAAAAQAAAQAAAAAAAAEAAAAAAAAAAAAAAAAEAAAIoAAAAAUAAAAAYA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQ09ERQAAAAAA +IAAAABAAAAAUAAAABgAAAAAAAAAAAAAAAAAAIAAA4ERBVEEAAAAAABAAAAAwAAAAAgAAABoAAAAA +AAAAAAAAAAAAAEAAAMAuaWRhdGEAAAAQAAAAQAAAAAIAAAAcAAAAAAAAAAAAAAAAAABAAADALnJz +cmMAAAAAgAAAAFAAAAAwAAAAHgAAAAAAAAAAAAAAAAAAQAAA0AAAAAAAAAAAAAAAAAAAAAAAAAAA +RDY5alLDAJCK/jLsU0G8R03PAwt5DjEcFVK3ICRNw5dh2gxwqg7aZ3VtO1ynbZr2zAD///////// +/////6IDEwBbAAggAAAA + + +--==i3.9.0oisdboibsd((kncd-- + + diff --git a/test/virus4 b/test/virus4 new file mode 100644 index 0000000..b2e6e6c --- /dev/null +++ b/test/virus4 @@ -0,0 +1,60 @@ +From mdb@go2net.com Tue Sep 18 10:31:34 2001 +Received: from www.bmsi.com (bmsweb.bmsi.com [219.109.11.130]) + by bmsaix.bmsi.com (8.11.5/8.11.3) with ESMTP id f8IEVXM42662 + for ; Tue, 18 Sep 2001 10:31:34 -0400 +Received: from STOREULV2 (mail.indexas.no [195.70.182.114]) + by www.bmsi.com (8.9.1/8.9.1) with SMTP id KAA27604 + for ; Tue, 18 Sep 2001 10:31:31 -0400 +Date: Tue, 18 Sep 2001 10:31:31 -0400 +From: mdb@go2net.com +Message-Id: <200109181431.KAA27604@www.bmsi.com> +Subject: udesktopdesktopeksempeleksempeldesktopeksempeldesktopeksempeldesktopdesktopdesktopeksempeleksempeleksempeleksempeldesktopeksempeleksempeleksempeleksempeleksempeleksempeldesktopeksempeleksempeleksempeldesktopeksempeleksempeldesktopdesktopdesktopeksempeldeskmail.bmsi.com.desktop +MIME-Version: 1.0 +Content-Type: multipart/related; + type="multipart/alternative"; + boundary="====_ABC1234567890DEF_====" +X-Priority: 3 +X-MSMail-Priority: Normal +X-Unsent: 1 +Status: RO +X-Status: +X-Keywords: + +--====_ABC1234567890DEF_==== +Content-Type: multipart/alternative; + boundary="====_ABC0987654321DEF_====" + +--====_ABC0987654321DEF_==== +Content-Type: text/html; + charset="iso-8859-1" +Content-Transfer-Encoding: quoted-printable + + + + +--====_ABC0987654321DEF_====-- + +--====_ABC1234567890DEF_==== +Content-Type: audio/x-wav; + name="readme.exe" +Content-Transfer-Encoding: base64 +Content-ID: + +TVqQAAMAAAAEAAAA//8AALgAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAA2AAAAA4fug4AtAnNIbgBTM0hVGhpcyBwcm9ncmFtIGNhbm5vdCBiZSBydW4gaW4gRE9TIG1v +ZGUuDQ0KJAAAAAAAAAA11CFvcbVPPHG1TzxxtU88E6pcPHW1TzyZqkU8dbVPPJmqSzxytU88cbVO +PBG1TzyZqkQ8fbVPPMmzSTxwtU88UmljaHG1TzwAAAAAAAAAAMBEAWMAAAB/UEUAAEwBBQB1Oqc7 +AAAAAAAAAADgAA4BCwEGAABwAAAAYAAAAAAAALN0AAAAEAAAAIAAAAAAFzYAEAAAABAAAAQAAAAA +AAAABAAAAAAAAAAAEAEAABAAAAAAAAACAAAAAAAQAAAQAAAAABAAABAAAAAAAAAQAAAAAAAAAAAA +AACEgQAAUAAAAADgAACIHgAAAAAAAAAAAAAAAAAAAAAAAAAAAQA4CgAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAIQBAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAudGV4dAAAAFZlAAAAEAAAAHAAAAAQAAAAAAAAAAAAAAAAAAAgAABgLnJkYXRhAAAq +CQAAAIAAAAAQAAAAgAAAAAAAAAAAAAAAAAAAQAAAQC5kYXRhAAAAKEcAAACQAAAAIAAAAJAAAAAA +AAAAAAAAAAAAAEAAAMAucnNyYwAAAAAgAAAA4AAAACAAAACwAAAAAAAAAAAAAAAAAABAAABALnJl +bG9jAABGCwAAAAABAAAQAAAA0AAAAAAAAAAAAAAAAAAAQAAAQgAAAAAAAAAAAAAAAAAAAAAAAAAA +AAA= + +--====_ABC1234567890DEF_==== + + diff --git a/test/virus5 b/test/virus5 new file mode 100644 index 0000000..de9142c --- /dev/null +++ b/test/virus5 @@ -0,0 +1,38 @@ +From mdb@go2net.com Tue Sep 18 10:31:34 2001 +Received: from localhost (varna148.pip.digsys.bg [193.68.1.148]) + by danbo.digsys.bg (8.10.1/8.10.1) with SMTP id fAM7FHk06734 + for butchc@trwonnor.com; Thu, 22 Nov 2001 09:15:18 +0200 (EET) +From: POP - interlogvar +Message-Id: <200111220715.fAM7FHk06734@danbo.digsys.bg> +To: butchc@trwonnor.com +Subject: Funny shit to see ?! +Date: Thu,22 Nov 2001 09:16:34 -0000 +MIME-Version: 1.0 +Content-Type: multipart/mixed; + boundary="bound" + X-Priority: 3 + X-MSMail-Priority: Normal + X-Mailer: Microsoft Outlook Express 5.50.4522.1300 + X-MimeOLE: Produced By Microsoft MimeOLE V5.50.4522.1300 + +This is a multi-part message in MIME format. + +--bound +Content-Type: text/html; + charset="iso-8859-1" +Content-Transfer-Encoding: quoted-printable + + +peace + +--bound +Content-Type: audio/x-wav; + name="whatever.exe" +Content-Transfer-Encoding: base64 +Content-ID: + +TVoAAAIAAAACAB4AHgAAAAACAAAAAAAAAAAAAMWnLuEOH7oOALQJ +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAA= + +--bound-- diff --git a/test/virus6 b/test/virus6 new file mode 100644 index 0000000..06afa66 --- /dev/null +++ b/test/virus6 @@ -0,0 +1,27 @@ +From mdb@go2net.com Tue Sep 18 10:31:34 2001 +Received: from aglnss01.grupoagrisal.net ([172.16.0.1]) + by agntss05 (Lotus Domino Release 5.07a) + with ESMTP id 2001120416164050:5294 ; + Tue, 4 Dec 2001 16:16:40 -0600 +Subject: MAEU XSS025786 - ORDER 1251 - CONTAINER MAEU 6053725 +To: kathyp@jsconnor.com +Cc: Blanca@ace-of-hearts.net +X-Mailer: Lotus Notes Release 5.07a May 14, 2001 +Message-ID: +From: sherrera.dco.lc@agrisal.com +Date: Tue, 4 Dec 2001 16:11:48 -0600 +MIME-Version: 1.0 +X-MIMETrack: Serialize by Router on AGLNSS01/AGRISAL(Release 5.07a |May 14, 2001) at 04/12/2001 + 04:11:57 p.m., + Itemize by SMTP Server on aglnss03/Grupo_Agrisal(Release 5.07a |May 14, 2001) at + 12/04/2001 04:16:41 PM, + Serialize by Router on aglnss03/Grupo_Agrisal(Release 5.07a |May 14, 2001) at + 12/04/2001 04:16:51 PM +Content-type: application/octet-stream; + name="FAX20.exe" +Content-Disposition: attachment; filename="FAX20.exe" +Content-Transfer-Encoding: base64 + +TVqQAAMAAAAEAAAA//8AALgAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAKJsVAAAACIAACIAACIBr6AQA + diff --git a/test/virus7 b/test/virus7 new file mode 100644 index 0000000..c4c02c2 --- /dev/null +++ b/test/virus7 @@ -0,0 +1,62 @@ +From pandora.owner@pandora.cz Wed Mar 24 21:02:22 2004 +Received: from pandora.cz (localhost [127.0.0.1]) + by pandora3.mobil.cz (8.12.8/8.12.8) with ESMTP id i2O88iWu021270 + for ; Wed, 24 Mar 2004 09:08:44 +0100 +Message-Id: <200403240808.i2O88iWu021270@pandora3.mobil.cz> +X-Sender: Pandora +MIME-Version: 1.0 +Date: Wed, 24 Mar 2004 09:08:44 +0100 +From: "administrator@pandora.cz" +To: "stuart@bmsi.com" +Subject: Konferenceneexistuje +Content-Type: multipart/mixed; boundary="Pandora3Bndry_1080115724426044878" + + +--Pandora3Bndry_1080115724426044878 +Content-Type: multipart/alternative; boundary="Pandora3Bndry_1080115724783315537" + + +--Pandora3Bndry_1080115724783315537 +Content-Type: text/plain; charset="ISO-8859-2" + +Konference '2003-07-46063' neexistuje. + +--Pandora3Bndry_1080115724783315537 +Content-Type: text/html; charset="ISO-8859-2" + +Konference '2003-07-46063' neexistuje. + +--Pandora3Bndry_1080115724783315537-- + +--Pandora3Bndry_1080115724426044878 +Content-Type: message/rfc822; boundary="----=_NextPart_000_0010_00000FFF.00007545" + +MIME-Version: 1.0 +Date: Wed, 24 Mar 2004 09:03:28 +0100 +From: "" +To: "" <2003-07-46063@pandora.cz> +Subject: =?ISO-8859-2?q?Re=3A_Your_software?= +Content-Type: multipart/mixed; boundary="Pandora3Bndry_10801157231587976770" + + +--Pandora3Bndry_10801157231587976770 +Content-Type: text/plain; charset="Windows-1252" +Content-Transfer-Encoding: 7bit + +See the attached file for details. + + +--Pandora3Bndry_10801157231587976770 +Content-Type: application/octet-stream; name="application.pif" +Content-Disposition: attachment; filename="application.pif" +Content-Transfer-Encoding: base64 + +TVqQAAMAAAAEAAAA//8AALgAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAuAAAAKvnXsbvhjCV74Ywle+GMJVsmj6V44YwlQeZOpX2hjCV74YxlbiGMJVsjm2V +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + +--Pandora3Bndry_10801157231587976770-- + +--Pandora3Bndry_1080115724426044878-- + diff --git a/testbms.py b/testbms.py new file mode 100644 index 0000000..eb70780 --- /dev/null +++ b/testbms.py @@ -0,0 +1,290 @@ +import unittest +import Milter +import bms +import mime +import rfc822 +import StringIO +#import pdb + +class TestMilter(bms.bmsMilter): + + def __init__(self): + bms.bmsMilter.__init__(self) + self.logfp = open("test/milter.log","a") + self._delrcpt = [] # record deleted rcpts for testing + self._addrcpt = [] # record added rcpts for testing + + def log(self,*msg): + for i in msg: print >>self.logfp, i, + print >>self.logfp + + def getsymval(self,name): + if name == 'j': return 'test.milter.org' + return bms.bmsMilter.getsymval(self,name) + + def replacebody(self,chunk): + if self._body: + self._body.write(chunk) + self.bodyreplaced = 1 + else: + raise IOError,"replacebody not called from eom()" + + # FIXME: rfc822 indexing does not really reflect the way chg/add header + # work for a milter + def chgheader(self,field,idx,value): + if not self._body: + raise IOError,"chgheader not called from eom()" + self.log('chgheader: %s[%d]=%s' % (field,idx,value)) + if value == '': + del self._msg[field] + else: + self._msg[field] = value + self.headerschanged = 1 + + def addheader(self,field,value): + if not self._body: + raise IOError,"addheader not called from eom()" + self.log('addheader: %s=%s' % (field,value)) + self._msg[field] = value + self.headerschanged = 1 + + def delrcpt(self,rcpt): + if not self._body: + raise IOError,"delrcpt not called from eom()" + self._delrcpt.append(rcpt) + + def addrcpt(self,rcpt): + if not self._body: + raise IOError,"addrcpt not called from eom()" + self._addrcpt.append(rcpt) + + def setreply(self,rcode,xcode,msg): + self.reply = (rcode,xcode,msg) + + def feedFile(self,fp,sender="spam@adv.com",rcpt="victim@lamb.com"): + self._body = None + self.bodyreplaced = 0 + self.headerschanged = 0 + self.reply = None + msg = rfc822.Message(fp) + rc = self.envfrom('<%s>'%sender) + if rc != Milter.CONTINUE: return rc + rc = self.envrcpt('<%s>'%rcpt) + if rc != Milter.CONTINUE: return rc + line = None + for h in msg.headers: + if h[:1].isspace(): + line = line + h + continue + if not line: + line = h + continue + s = line.split(': ',1) + if len(s) > 1: val = s[1].strip() + else: val = '' + rc = self.header(s[0],val) + if rc != Milter.CONTINUE: return rc + line = h + if line: + s = line.split(': ',1) + rc = self.header(s[0],s[1]) + if rc != Milter.CONTINUE: return rc + rc = self.eoh() + if rc != Milter.CONTINUE: return rc + while 1: + buf = fp.read(8192) + if len(buf) == 0: break + rc = self.body(buf) + if rc != Milter.CONTINUE: return rc + self._msg = msg + self._body = StringIO.StringIO() + rc = self.eom() + if self.bodyreplaced: + body = self._body.getvalue() + else: + msg.rewindbody() + body = msg.fp.read() + self._body = StringIO.StringIO() + self._body.writelines(msg.headers) + self._body.write('\n') + self._body.write(body) + return rc + + def feedMsg(self,fname,sender="spam@adv.com",rcpt="victim@lamb.com"): + fp = open('test/'+fname,'r') + rc = self.feedFile(fp,sender,rcpt) + fp.close() + return rc + + def connect(self,host='localhost'): + self._body = None + self.bodyreplaced = 0 + rc = bms.bmsMilter.connect(self,host,1,('1.2.3.4',1234)) + if rc != Milter.CONTINUE and rc != Milter.ACCEPT: + self.close() + return rc + rc = self.hello('spamrelay') + if rc != Milter.CONTINUE: + self.close() + return rc + +class BMSMilterTestCase(unittest.TestCase): + + def testDefang(self,fname='virus1'): + milter = TestMilter() + rc = milter.connect('testDefang') + self.assertEqual(rc,Milter.CONTINUE) + rc = milter.feedMsg(fname) + self.assertEqual(rc,Milter.ACCEPT) + self.failUnless(milter.bodyreplaced,"Message body not replaced") + fp = milter._body + open('test/'+fname+".tstout","w").write(fp.getvalue()) + #self.failUnless(fp.getvalue() == open("test/virus1.out","r").read()) + fp.seek(0) + msg = mime.MimeMessage(fp) + str = msg.get_payload(1).get_payload() + milter.log(str) + milter.close() + + # test some spams that crashed our parser + def testParse(self,fname='spam7'): + milter = TestMilter() + milter.connect('testParse') + rc = milter.feedMsg(fname) + self.assertEqual(rc,Milter.ACCEPT) + self.failIf(milter.bodyreplaced,"Milter needlessly replaced body.") + fp = milter._body + open('test/'+fname+".tstout","w").write(fp.getvalue()) + milter.connect('pro-send.com') + rc = milter.feedMsg('spam8') + self.assertEqual(rc,Milter.ACCEPT) + self.failIf(milter.bodyreplaced,"Milter needlessly replaced body.") + rc = milter.feedMsg('bounce') + self.assertEqual(rc,Milter.ACCEPT) + self.failIf(milter.bodyreplaced,"Milter needlessly replaced body.") + rc = milter.feedMsg('bounce1') + self.assertEqual(rc,Milter.ACCEPT) + self.failIf(milter.bodyreplaced,"Milter needlessly replaced body.") + milter.close() + + def testDefang2(self): + milter = TestMilter() + milter.connect('testDefang2') + rc = milter.feedMsg('samp1') + self.assertEqual(rc,Milter.ACCEPT) + self.failIf(milter.bodyreplaced,"Milter needlessly replaced body.") + rc = milter.feedMsg("virus3") + self.assertEqual(rc,Milter.ACCEPT) + self.failUnless(milter.bodyreplaced,"Message body not replaced") + fp = milter._body + open("test/virus3.tstout","w").write(fp.getvalue()) + #self.failUnless(fp.getvalue() == open("test/virus3.out","r").read()) + rc = milter.feedMsg("virus6") + self.assertEqual(rc,Milter.ACCEPT) + self.failUnless(milter.bodyreplaced,"Message body not replaced") + self.failUnless(milter.headerschanged,"Message headers not adjusted") + fp = milter._body + open("test/virus6.tstout","w").write(fp.getvalue()) + milter.close() + + def testDefang3(self): + milter = TestMilter() + milter.connect('testDefang3') + # test script removal on complex HTML attachment + rc = milter.feedMsg('amazon') + self.assertEqual(rc,Milter.ACCEPT) + self.failUnless(milter.bodyreplaced,"Message body not replaced") + fp = milter._body + open("test/amazon.tstout","w").write(fp.getvalue()) + # test defanging Klez virus + rc = milter.feedMsg("virus13") + self.assertEqual(rc,Milter.ACCEPT) + self.failUnless(milter.bodyreplaced,"Message body not replaced") + fp = milter._body + open("test/virus13.tstout","w").write(fp.getvalue()) + # test script removal on quoted-printable HTML attachment + # sgmllib can't handle the syntax + rc = milter.feedMsg('spam44') + self.assertEqual(rc,Milter.ACCEPT) + self.failIf(milter.bodyreplaced,"Message body replaced") + fp = milter._body + open("test/spam44.tstout","w").write(fp.getvalue()) + milter.close() + + def testRFC822(self): + milter = TestMilter() + milter.connect('testRFC822') + # test encoded rfc822 attachment + #pdb.set_trace() + rc = milter.feedMsg('test8') + self.assertEqual(rc,Milter.ACCEPT) + self.failUnless(milter.bodyreplaced,"Message body not replaced") + #self.failIf(milter.bodyreplaced,"Message body replaced") + fp = milter._body + open("test/test8.tstout","w").write(fp.getvalue()) + rc = milter.feedMsg('virus7') + self.assertEqual(rc,Milter.ACCEPT) + self.failUnless(milter.bodyreplaced,"Message body not replaced") + #self.failIf(milter.bodyreplaced,"Message body replaced") + fp = milter._body + open("test/virus7.tstout","w").write(fp.getvalue()) + + def testSmartAlias(self): + milter = TestMilter() + milter.connect('testSmartAlias') + # test smart alias feature + key = ('foo@bar.com','baz@bat.com') + bms.smart_alias[key] = ['ham@eggs.com'] + rc = milter.feedMsg('test8',key[0],key[1]) + self.assertEqual(rc,Milter.ACCEPT) + self.failUnless(milter.bodyreplaced,"Message body not replaced") + self.failUnless(milter._delrcpt == ['']) + self.failUnless(milter._addrcpt == ['']) + + def testBadBoundary(self): + milter = TestMilter() + milter.connect('testBadBoundary') + # test rfc822 attachment with invalid boundaries + #pdb.set_trace() + rc = milter.feedMsg('bound') + self.assertEqual(rc,Milter.REJECT) + self.assertEqual(milter.reply[0],'554') + #self.failUnless(milter.bodyreplaced,"Message body not replaced") + self.failIf(milter.bodyreplaced,"Message body replaced") + fp = milter._body + open("test/bound.tstout","w").write(fp.getvalue()) + + def testCompoundFilename(self): + milter = TestMilter() + milter.connect('testCompoundFilename') + # test rfc822 attachment with invalid boundaries + #pdb.set_trace() + rc = milter.feedMsg('test1') + self.assertEqual(rc,Milter.ACCEPT) + #self.failUnless(milter.bodyreplaced,"Message body not replaced") + self.failIf(milter.bodyreplaced,"Message body replaced") + fp = milter._body + open("test/test1.tstout","w").write(fp.getvalue()) + +# def testReject(self): +# "Test content based spam rejection." +# milter = TestMilter() +# milter.connect('gogo-china.com') +# rc = milter.feedMsg('big5'); +# self.failUnless(rc == Milter.REJECT) +# milter.close(); + +def suite(): return unittest.makeSuite(BMSMilterTestCase,'test') + +if __name__ == '__main__': + import sys + if len(sys.argv) > 1: + for fname in sys.argv[1:]: + milter = TestMilter() + milter.connect('main') + fp = open(fname,'r') + rc = milter.feedFile(fp) + fp = milter._body + sys.stdout.write(fp.getvalue()) + else: + unittest.main() diff --git a/testmime.py b/testmime.py new file mode 100644 index 0000000..0f6c934 --- /dev/null +++ b/testmime.py @@ -0,0 +1,115 @@ +import unittest +import mime +import socket +import StringIO +import email + +samp1_txt1 = """Dear Agent 1 +I hope you can read this. Whenever you write label it P.B.S kids. + Eliza doesn't know a thing about P.B.S kids. got to go by +agent one.""" + +hostname = socket.gethostname() + +class MimeTestCase(unittest.TestCase): + + # test mime parameter parsing + def testParam(self): + plist = mime._parseparam( + '; boundary="----=_NextPart_000_4e56_490d_48e3"') + self.failUnless(len(plist)==1) + self.failUnless(plist[0] == 'boundary="----=_NextPart_000_4e56_490d_48e3"') + plist = mime._parseparam('; name="Jim&amp;Girlz.jpg"') + self.failUnless(len(plist)==1) + self.failUnless(plist[0] == 'name="Jim&amp;Girlz.jpg"') + + def testParse(self,fname='samp1'): + msg = mime.MimeMessage(open('test/'+fname,"r")) + self.failUnless(msg.ismultipart()) + parts = msg.get_payload() + self.failUnless(len(parts) == 2) + txt1 = parts[0].get_payload() + self.failUnless(txt1.rstrip() == samp1_txt1,txt1) + + def testDefang(self,vname='virus1',part=1, + fname='LOVE-LETTER-FOR-YOU.TXT.vbs'): + msg = mime.MimeMessage(open('test/'+vname,"r")) + mime.defang(msg) + oname = vname + '.out' + msg.dump(open('test/'+oname,"w")) + msg = mime.MimeMessage(open('test/'+oname,"r")) + parts = msg.get_payload() + txt2 = parts[part].get_payload() + self.failUnless(txt2.rstrip()+'\n' == mime.virus_msg % (fname,hostname,None),txt2) + + def testDefang3(self): + self.testDefang('virus3',0,'READER_DIGEST_LETTER.TXT.pif') + + # virus4 does not include proper end boundary + def testDefang4(self): + self.testDefang('virus4',1,'readme.exe') + + # virus5 is even more screwed up + def testDefang5(self): + self.testDefang('virus5',1,'whatever.exe') + + # virus6 has no parts - the virus is directly inline + def testDefang6(self,vname="virus6",fname='FAX20.exe'): + msg = mime.MimeMessage(open('test/'+vname,"r")) + mime.defang(msg) + oname = vname + '.out' + msg.dump(open('test/'+oname,"w")) + msg = mime.MimeMessage(open('test/'+oname,"r")) + self.failIf(msg.ismultipart()) + txt2 = msg.get_payload() + self.failUnless(txt2 == mime.virus_msg % \ + (fname,hostname,None),txt2) + + # honey virus has a sneaky ASP payload which is parsed correctly + # by email package in python-2.2.2, but not by mime.MimeMessage or 2.2.1 + def testDefang7(self,vname="honey",fname='story[1].scr'): + msg = mime.MimeMessage(open('test/'+vname,"r")) + mime.defang(msg) + oname = vname + '.out' + msg.dump(open('test/'+oname,"w")) + msg = mime.MimeMessage(open('test/'+oname,"r")) + parts = msg.get_payload() + txt2 = parts[1].get_payload() + txt3 = parts[2].get_payload() + self.failUnless(txt2.rstrip()+'\n' == mime.virus_msg % \ + (fname,hostname,None),txt2) + if txt3 != '': + self.failUnless(txt3.rstrip()+'\n' == mime.virus_msg % \ + ('story[1].asp',hostname,None),txt3) + + def testParse2(self,fname="spam7"): + msg = mime.MimeMessage(open('test/'+fname,"r")) + self.failUnless(msg.ismultipart()) + parts = msg.get_payload() + self.failUnless(len(parts) == 2) + name = parts[1].getname() + self.failUnless(name == "Jim&amp;Girlz.jpg","name=%s"%name) + + def testHTML(self,fname=""): + result = StringIO.StringIO() + filter = mime.HTMLScriptFilter(result) + msg = """ + Optional SGML + + """ + script = "" + filter.feed(msg + script) + filter.close() + #print result.getvalue() + self.failUnless(result.getvalue() == msg + filter.msg) + +def suite(): return unittest.makeSuite(MimeTestCase,'test') + +if __name__ == '__main__': + import sys + if len(sys.argv) < 2: + unittest.main() + else: + for fname in sys.argv[1:]: + fp = open(fname,'r') + msg = mime.MimeMessage(fp) diff --git a/testsample.py b/testsample.py new file mode 100644 index 0000000..4ed278a --- /dev/null +++ b/testsample.py @@ -0,0 +1,149 @@ +import unittest +import Milter +import sample +import mime +import rfc822 +import StringIO + +class TestMilter(sample.sampleMilter): + + def __init__(self): + self.logfp = open("test/milter.log","a") + + def log(self,*msg): + for i in msg: print >>self.logfp, i, + print >>self.logfp + + def replacebody(self,chunk): + if self._body: + self._body.write(chunk) + self.bodyreplaced = 1 + else: + raise IOError,"replacebody not called from eom()" + + # FIXME: rfc822 indexing does not really reflect the way chg/add header + # work for a milter + def chgheader(self,field,idx,value): + self.log('chgheader: %s[%d]=%s' % (field,idx,value)) + if value == '': + del self._msg[field] + else: + self._msg[field] = value + self.headerschanged = 1 + + def addheader(self,field,value): + self.log('addheader: %s=%s' % (field,value)) + self._msg[field] = value + self.headerschanged = 1 + + def feedMsg(self,fname): + self._body = None + self.bodyreplaced = 0 + self.headerschanged = 0 + fp = open('test/'+fname,'r') + msg = rfc822.Message(fp) + rc = self.envfrom('') + if rc != Milter.CONTINUE: return rc + rc = self.envrcpt('') + if rc != Milter.CONTINUE: return rc + line = None + for h in msg.headers: + if h[:1].isspace(): + line = line + h + continue + if not line: + line = h + continue + s = line.split(': ',1) + rc = self.header(s[0],s[1].strip()) + if rc != Milter.CONTINUE: return rc + line = h + if line: + s = line.split(': ',1) + rc = self.header(s[0],s[1]) + if rc != Milter.CONTINUE: return rc + rc = self.eoh() + if rc != Milter.CONTINUE: return rc + while 1: + buf = fp.read(8192) + if len(buf) == 0: break + rc = self.body(buf) + if rc != Milter.CONTINUE: return rc + self._msg = msg + self._body = StringIO.StringIO() + rc = self.eom() + if self.bodyreplaced: + body = self._body.getvalue() + else: + msg.rewindbody() + body = msg.fp.read() + self._body = StringIO.StringIO() + self._body.writelines(msg.headers) + self._body.write('\n') + self._body.write(body) + return rc + + def connect(self,host='localhost'): + self._body = None + self.bodyreplaced = 0 + rc = sample.sampleMilter.connect(self,host,1,0) + if rc != Milter.CONTINUE and rc != Milter.ACCEPT: + self.close() + return rc + rc = self.hello('spamrelay') + if rc != Milter.CONTINUE: + self.close() + return rc + +class BMSMilterTestCase(unittest.TestCase): + + def testDefang(self,fname='virus1'): + milter = TestMilter() + rc = milter.connect() + self.failUnless(rc == Milter.CONTINUE) + rc = milter.feedMsg(fname) + self.failUnless(rc == Milter.ACCEPT) + self.failUnless(milter.bodyreplaced,"Message body not replaced") + fp = milter._body + open('test/'+fname+".tstout","w").write(fp.getvalue()) + #self.failUnless(fp.getvalue() == open("test/virus1.out","r").read()) + fp.seek(0) + msg = mime.MimeMessage(fp) + s = msg.get_payload(1).get_payload() + milter.log(s) + milter.close() + + def testParse(self,fname='spam7'): + milter = TestMilter() + milter.connect('somehost') + rc = milter.feedMsg(fname) + self.failUnless(rc == Milter.ACCEPT) + self.failIf(milter.bodyreplaced,"Milter needlessly replaced body.") + fp = milter._body + open('test/'+fname+".tstout","w").write(fp.getvalue()) + milter.close() + + def testDefang2(self): + milter = TestMilter() + milter.connect('somehost') + rc = milter.feedMsg('samp1') + self.failUnless(rc == Milter.ACCEPT) + self.failIf(milter.bodyreplaced,"Milter needlessly replaced body.") + rc = milter.feedMsg("virus3") + self.failUnless(rc == Milter.ACCEPT) + self.failUnless(milter.bodyreplaced,"Message body not replaced") + fp = milter._body + open("test/virus3.tstout","w").write(fp.getvalue()) + #self.failUnless(fp.getvalue() == open("test/virus3.out","r").read()) + rc = milter.feedMsg("virus6") + self.failUnless(rc == Milter.ACCEPT) + self.failUnless(milter.bodyreplaced,"Message body not replaced") + self.failUnless(milter.headerschanged,"Message headers not adjusted") + fp = milter._body + open("test/virus6.tstout","w").write(fp.getvalue()) + milter.close() + +def suite(): return unittest.makeSuite(BMSMilterTestCase,'test') + +if __name__ == '__main__': + unittest.main()