Here's a patch to spamcheck.py that allows for multiple recipient support yet preserves per-user pgsql preferences. It's designed to work with postfix, but could probably be used with almost any modern MTA.
It works by checking if each user has any preferences set in the SA userpref database. When a message with multiple recipients is recieved, recipients with spam preferences are processed first. Then the mail is scanned once for all other recipients (using the username 'nobody') and sent on to the lmtp server. Right now, it only works for postgres using the pg python module. (I couldn't find any decent documentation for pgdb) However, it should be easy to port to other db schema. I'd like to get a modified version of this in CVS, if possible, It still needs a little work in order to generalize the DB support and clean out any bugs I've missed. Thoughts? Is this going to be useful to anyone besides us?
*** spamcheck.py_2.43 Thu Oct 24 17:03:56 2002 --- spamcheck.py_pg_2.43_clean Thu Oct 24 17:53:11 2002 *************** *** 1,4 **** ! #!/usr/bin/env python # spamcheck.py: spam tagging support for Postfix/Cyrus # --- 1,4 ---- ! #!/usr/local/bin/python # spamcheck.py: spam tagging support for Postfix/Cyrus # *************** *** 35,40 **** --- 35,41 ---- import sys, string import re, getopt import smtplib, socket + import pg # EX_TEMPFAIL is 75 on every Unix I've checked, but... # check /usr/include/sysexits.h if you have odd problems. *************** *** 43,48 **** --- 44,57 ---- NOUSER = 67 TEMPFAIL = 75 + # Postgres variables + PG_DB = 'DBNAME' + PG_HOSTNAME = 'HOSTNAME.EXAMPLE.NET' + PG_PORT = 5432 + PG_TABLE = 'TABLENAME' + PG_USERNAME = 'USERNAME' + PG_PASSWORD = 'PASSWORD' + # this class hacks smtplib's SMTP class into a shape where it will # successfully pass a message off to Cyrus's LMTP daemon. # Also adds support for connecting to a unix domain socket. *************** *** 171,200 **** # return the spam checked message return fd.read() ! def process_message(spamd_host, spamd_port, lmtp_host, sender, recipient): CHUNKSIZE = 256 * 1024 - # read in the first chunk of the message - - data = sys.stdin.read(CHUNKSIZE) - # is the message smaller than the maximum size? if len(data) < CHUNKSIZE: ! try: ! checked_data = spamcheck(spamd_host, spamd_port, recipient, data) ! # get rid of From ... line if present ! if checked_data[:5] == 'From ': ! nl = string.find(checked_data, '\n') ! if nl >= 0: checked_data = checked_data[nl+1:] ! except: ! sys.stderr.write('%s: %s\n' % sys.exc_info()[:2]) ! checked_data = data # fallback lmtp = LMTP(lmtp_host) code, msg = lmtp.lhlo() if code != 250: sys.exit(TEMPFAIL) #lmtp.set_debuglevel(1) try: ! lmtp.sendmail(sender, recipient, checked_data) except smtplib.SMTPRecipientsRefused: sys.exit(NOUSER) except smtplib.SMTPDataError, errors: --- 180,235 ---- # return the spam checked message return fd.read() ! def process_message(spamd_host, spamd_port, lmtp_host, sender, recipients, users, data): CHUNKSIZE = 256 * 1024 # is the message smaller than the maximum size? if len(data) < CHUNKSIZE: ! if len(recipients) == 1: ! try: ! checked_data = spamcheck(spamd_host, spamd_port, users[0], data) ! # get rid of From ... line if present ! if checked_data[:5] == 'From ': ! nl = string.find(checked_data, '\n') ! if nl >= 0: checked_data = checked_data[nl+1:] ! except: ! sys.stderr.write('%s: %s\n' % sys.exc_info()[:2]) ! checked_data = data # fallback ! else: # More than one recipient ! default_recipients = list() ! conn = pg.connect(PG_DB, PG_HOSTNAME, PG_PORT, None, None, PG_USERNAME, PG_PASSWORD) ! for index in range(len(recipients)): ! recipient = recipients[index] ! user = users[index] ! connstring = 'select * from ' + PG_TABLE + ' where username = \'' + user + '\'' ! result = conn.query(connstring) ! if result.ntuples() != 0: ! try: ! process_message(spamd_host, spamd_port, lmtp_host, sender, [recipient], [user], data) ! except: ! sys.stderr.write('%s: %s\n' % sys.exc_info()[:2]) ! sys.exit(1) ! else: ! default_recipients.append(recipient) ! if len(default_recipients) != 0: ! recipients = default_recipients #Non-default users are done ! try: ! checked_data = spamcheck(spamd_host, spamd_port, 'nobody', data) ! # get rid of From ... line if present ! if checked_data[:5] == 'From ': ! nl = string.find(checked_data, '\n') ! if nl >= 0: checked_data = checked_data[nl+1:] ! except: ! sys.stderr.write('%s: %s\n' % sys.exc_info()[:2]) ! checked_data = data # fallback ! else: # All recipients had custom spam preferences ! sys.exit(0) lmtp = LMTP(lmtp_host) code, msg = lmtp.lhlo() if code != 250: sys.exit(TEMPFAIL) #lmtp.set_debuglevel(1) try: ! lmtp.sendmail(sender, recipients, checked_data) except smtplib.SMTPRecipientsRefused: sys.exit(NOUSER) except smtplib.SMTPDataError, errors: *************** *** 211,218 **** code, msg = lmtp.mail(sender) if code != 250: sys.exit(TEMPFAIL) ! code, msg = lmtp.rcpt(recipient) ! if code != 250: sys.exit(NOUSER) # send the data in chunks lmtp.putcmd("data") --- 246,254 ---- code, msg = lmtp.mail(sender) if code != 250: sys.exit(TEMPFAIL) ! for recipient in recipients: ! code, msg = lmtp.rcpt(recipient) ! if code != 250: sys.exit(NOUSER) # send the data in chunks lmtp.putcmd("data") *************** *** 238,265 **** spamd_port = 783 lmtp_host = None sender = None ! recipient = None try: ! opts, args = getopt.getopt(argv[1:], 's:r:l:') except getopt.error, err: sys.stderr.write('%s: %s\n' % (argv[0], err)) sys.exit(USAGE) for opt, arg in opts: if opt == '-s': sender = arg - elif opt == '-r': recipient = string.lower(arg) elif opt == '-l': lmtp_host = arg else: sys.stderr.write('unexpected argument\n') sys.exit(USAGE) if args: ! sys.stderr.write('unexpected argument\n') ! sys.exit(USAGE) ! if not lmtp_host or not sender or not recipient: sys.stderr.write('required argument missing\n') sys.exit(USAGE) try: ! process_message(spamd_host, spamd_port, lmtp_host, sender, recipient) except SystemExit, status: raise SystemExit, status except: --- 274,318 ---- spamd_port = 783 lmtp_host = None sender = None ! destination = None ! num_recipients = 0 ! recipients = None ! users = None try: ! opts, args = getopt.getopt(argv[1:], 's:l:') except getopt.error, err: sys.stderr.write('%s: %s\n' % (argv[0], err)) sys.exit(USAGE) for opt, arg in opts: if opt == '-s': sender = arg elif opt == '-l': lmtp_host = arg else: sys.stderr.write('unexpected argument\n') sys.exit(USAGE) if args: ! destination = args ! if len(destination) % 2 != 0: ! sys.stderr.write('unexpected argument\n') ! sys.exit(USAGE) ! else: ! numrecipients = len(destination) / 2 ! recipients = destination[0:numrecipients] ! users = destination[numrecipients:] ! if not lmtp_host or not sender or not recipients: sys.stderr.write('required argument missing\n') sys.exit(USAGE) try: ! CHUNKSIZE = 256 * 1024 ! # read in the first chunk of the message ! ! data = sys.stdin.read(CHUNKSIZE) ! ! if len(data) >= CHUNKSIZE: ! process_message(spamd_host, spamd_port, lmtp_host, sender, recipients, users, data) ! else: ! process_message(spamd_host, spamd_port, lmtp_host, sender, recipients, users, data) ! except SystemExit, status: raise SystemExit, status except:
Ted Cabeen http://www.pobox.com/~secabeen [EMAIL PROTECTED] Check Website or Keyserver for PGP/GPG Key BA0349D2 [EMAIL PROTECTED] "I have taken all knowledge to be my province." -F. Bacon [EMAIL PROTECTED] "Human kind cannot bear very much reality."-T.S.Eliot [EMAIL PROTECTED]