#!/usr/bin/env python # # $Id: greasetrap,v 1.3 2004/03/09 03:20:07 lukem Exp $ # # Copyright 2004 Luke Mewburn # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions # are met: # 1. Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # 2. Redistributions in binary form must reproduce the above copyright # notice, this list of conditions and the following disclaimer in the # documentation and/or other materials provided with the distribution. # 3. The name of the author may not be used to endorse or promote products # derived from this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR # IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES # OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. # IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS # OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR # TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE # USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # # # greasetrap [-b bouncecode] [-d] [message ...] -- # Detect if messages (or stdin) has spam-like attributes # such as bad charsets or attachment names with suspect suffixes, # and exit with a non-zero code if so. # This is suitable for use as a qmail-command(8) filter. # # # Note: # exit status supported by qmail-command(8): # 0 delivery ok # 99 delivery ok, ignore all further instructions (i.e., "eat mail") # 100 delivery permanently failed (hard error) # 111 delivery temporarily failed (soft error, retry) # import sys import email, re, getopt if sys.hexversion < 0x02020000: print >>sys.stderr, sys.argv[0] + ": need python 2.2 or greater" sys.exit(0) debug = file("/dev/null", "w+") # default: ignore errors # regex of bad filenames bad_filenames = r'\.(asd|bat|chm|cmd|com|dll|exe|hlp|hta|js|jse|lnk|ocx|pif|scr|shb|shm|shs|vb|vbe|vbs|vbx|vxd|wsf|wsh|zip)$' bad_filename_re = re.compile(bad_filenames, re.IGNORECASE) # bad charsets bad_charsets = [ 'big5', 'euc-kr', 'gb2312', 'ks_c_5601-1987', # 'euc-jp', # 'gb_2312-80', # 'iso-2022-jp', # 'shift_jis', # 'windows-1254', ] def analyse_mail_message(fd): """Analyse a mail message and ensure it doesn't contain bad things. Analyse the argument as a mail message, ensuring that it doesn't contain: * `Bad' character sets * MIME attachments with `bad' filenames fd may be an open file descriptor, a pathname as string, or "-" for stdin. """ if fd == '-': fd = sys.stdin elif isinstance(fd, str): fd = open(fd) print >>debug, "file:", fd.name msg=email.message_from_file(fd) # print >>debug, "envelope headers:", msg.items() # validate charsets counter = 1 for cs in msg.get_charsets(): print >>debug, "charset ", counter, cs if cs in bad_charsets: raise email.Errors.MessageError, \ "Bad charset `%s'" % cs counter += 1 # validate attachment filenames counter = 1 for part in msg.walk(): filename = part.get_filename() print >>debug, "part", counter print >>debug, " type: ", part.get_content_type() print >>debug, " params: ", repr(part.get_params()) if filename != None: print >>debug, " filename:", filename if bad_filename_re.search(filename): raise email.Errors.MessageError, \ "Bad filename `%s' in MIME part %s." \ % (filename,part.get_content_type()) counter += 1 def usage(): print >>sys.stderr, "Usage: %s [-b bounceexit] [-d] [message ...]" \ % sys.argv[0] sys.exit(0) def main(): global debug try: opts, args = getopt.getopt(sys.argv[1:], "b:d") except getopt.GetoptError: usage() bounceCode = 100 immediatelyFail = False exitCode = 0 for o, a in opts: if o == "-b": bounceCode = int(a) if o == "-d": debug = sys.stderr if len(args) == 0: args = [ sys.stdin ] immediatelyFail = True for f in args: try: analyse_mail_message(f) except email.Errors.MessageError: if f != sys.stdin: print f + ":", print sys.exc_info()[1] exitCode = bounceCode if immediatelyFail: break except SystemExit, rv: if rv.code in [ 99, 100, 111 ]: # supported exit values exitCode = rv.code if immediatelyFail: break # except: # print "unexpected error", sys.exc_info()[0] # sys.exit(111) sys.exit(exitCode) if __name__ == "__main__": main()