#!/usr/bin/env python
#
# Copyright 2004-2024 Luke Mewburn <luke@mewburn.net>
# 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()
