#!/usr/bin/python import imaplib import logging import os.path import re import subprocess from collections import namedtuple from ConfigParser import ConfigParser logging.basicConfig(format='%(levelname)s:%(funcName)s:%(message)s', level=logging.WARNING) log = logging.getLogger('check_bogofilter') # imaplib.Debug = 4 pattern_uid = re.compile(r'\d+ \(UID (?P\d+)\)') # Store capabilities present in the current server # Add additional ones as needed AvailableCapabilities = namedtuple('Capas', ['MOVE', 'UIDPLUS']) def parse_uid(data): match = pattern_uid.match(data) return match.group('uid') def move_messages(ids, target): # Eventually we may move messages in groups of say 50 if len(ids) > 0: ids_str = ','.join(ids) client.uid('COPY', ids_str, target) client.uid('STORE', ids_str, '+FLAGS', r'(\Deleted)') if client._features_available.UIDPLUS: client.uid('EXPUNGE', ids_str) def process_folder(proc_fld_name, target_fld, unsure_fld, spam_fld=None, bogofilter_param=['-l']): ham_msgs = [] spam_msgs = [] unsure_msgs = [] client.select(proc_fld_name) _, resp = client.search(None, "UNSEEN") log.debug('resp = %s', resp) messages = resp[0].split() logging.debug('messages = %s', messages) proc_msg_count = len(messages) for msgId in messages: logging.debug('msgId = %s', msgId) typ, msg_data = client.fetch(msgId, '(RFC822)') log.debug('fetch RFC822 result = %s', typ) # The -p (passthrough) option outputs the message with an # X-Bogosity line at the end of the message header. This requires # keeping the entire message in memory when it's read from stdin # (or from a pipe or socket). If the message is read from a file # that can be rewound, bogofilter will read it a second time. # The -e (embed) option tells bogofilter to exit with code 0 if # the message can be classified, i.e. if there is not an error. # Normally bogofilter uses different codes for spam, ham, and # unsure classifications, but this simplifies using bogofilter # with procmail or maildrop. ret = subprocess.Popen(['bogofilter'] + bogofilter_param, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE) out, err = ret.communicate(input=msg_data[0][1]) ret_code = ret.returncode logging.debug("ret.returncode = %s", ret_code) log.debug('----------------------------\nerr:\n%s', err) typ, data = client.fetch(msgId, "(UID)") log.debug('fetch UID result = %s, %s', typ, data) uid = parse_uid(data[0]) log.debug('UID = %s', uid) if ret_code == 0: # spam spam_msgs.append(uid) elif ret_code == 1: # ham ham_msgs.append(uid) elif ret_code == 2: # unsure unsure_msgs.append(uid) else: # 3 or something else I/O error raise IOError('Bogofilter failed with error %d', ret_code) move_messages(unsure_msgs, unsure_fld) log.debug('ham_msgs = %s, spam_msgs = %s' % (ham_msgs, spam_msgs)) if ham_msgs: if spam_fld is None: client.uid('STORE', ','.join(ham_msgs), '-FLAGS', r'(\Seen)') if spam_msgs: if spam_fld is not None: move_messages(spam_msgs, spam_fld) else: client.uid('STORE', ','.join(spam_msgs), '+FLAGS.SILENT', r'(\Deleted \Seen)') client.uid('EXPUNGE', ','.join(spam_msgs)) client.close() return proc_msg_count processedCounter = 0 config = ConfigParser() config.read(os.path.expanduser("~/.bogofilter-imap-train-rc")) login = config.get("imap-training", "login") password = config.get("imap-training", "password") server = config.get("imap-training", "server") client = imaplib.IMAP4_SSL(server) client._features_available = AvailableCapabilities(False, False) client.login(login, password) ok, dat = client.capability() if ok != 'OK': raise client.error(dat[-1]) capas = dat[0].decode() client._features_available = AvailableCapabilities._make( ['MOVE' in capas, 'UIDPLUS' in capas]) processedCounter += process_folder('INBOX', '_suspects', '_unsure', '_spam') client.logout() if processedCounter > 0: logging.info("Processed %d messages.", processedCounter)