usage: imapautofiler [-h] [-c CONFIG_FILE] [--list-mailboxes] optional arguments: -h, --help show this help message and exit -c CONFIG_FILE, --config-file CONFIG_FILE --list-mailboxes instead of processing rules, print a list of mailboxes
server:
hostname: "mail.example.com"
username: "doughellmann@example.com"
cfg = config.get_config(args.config_file)
conn = imapclient.IMAPClient(
cfg['server']['hostname'],
use_uid=True,
ssl=True,
)
username = cfg['server']['username']
password = cfg['server'].get('password')
if not password:
password = getpass.getpass(
'Password for {}:'.format(username))
conn.login(username, password)
How do the rules work?
class Rule(metaclass=abc.ABCMeta):
def __init__(self, rule_data, cfg):
self._data = rule_data
self._cfg = cfg
@abc.abstractmethod
def check(self):
raise NotImplementedError()
def get_action(self):
return self._data.get('action', {})
- headers:
- name: "subject"
substring: "[pyatl]"
action:
name: "move"
dest-mailbox: "INBOX.PyATL"
class HeaderSubString(Rule):
def __init__(self, rule_data, cfg):
super().__init__(rule_data, cfg)
self._header_name = rule_data['name']
self._substring = rule_data['substring'].lower()
def check(self, message):
header_value = message.get(self._header_name, '').lower()
return (self._substring in header_value)
- or:
rules:
- headers:
- name: "to"
substring: "pyatl-list@meetup.com"
- headers:
- name: "cc"
substring: "pyatl-list@meetup.com"
action:
name: "move"
dest-mailbox: "INBOX.PyATL"
class Or(Rule):
"True if any one of the sub-rules is true."
def __init__(self, rule_data, cfg):
super().__init__(rule_data, cfg)
self._sub_rules = [
factory(r, cfg)
for r in rule_data['or'].get('rules', [])
]
def check(self, message):
return any(
r.check(message)
for r in self._sub_rules
)
- recipient:
substring: "pyatl-list@meetup.com"
action:
name: "move"
dest-mailbox: "INBOX.PyATL"
class Recipient(Or):
"True if any recipient sub-rule matches."
def __init__(self, rule_data, cfg):
rules = []
for header in ['to', 'cc']:
header_data = {}
header_data.update(rule_data['recipient'])
header_data['name'] = header
rules.append({'headers': [header_data]})
rule_data['or'] = {
'rules': rules,
}
super().__init__(rule_data, cfg)
def factory(rule_data, cfg):
"""Create a rule processor.
Using the rule type, instantiate a rule processor that can check
the rule against a message.
"""
if 'or' in rule_data:
return Or(rule_data, cfg)
if 'headers' in rule_data:
return Headers(rule_data, cfg)
if 'recipient' in rule_data:
return Recipient(rule_data, cfg)
raise ValueError('Unknown rule type {!r}'.format(rule_data))
def process_rules(cfg, debug, conn):
num_messages = 0
num_processed = 0
for mailbox in cfg['mailboxes']: # multiple mailboxes allowed
mailbox_name = mailbox['name']
conn.select_folder(mailbox_name)
mailbox_rules = [ # convert data to instances
rules.factory(r, cfg)
for r in mailbox['rules']
]
msg_ids = conn.search(['ALL'])
for msg_id in msg_ids:
num_messages += 1
message = get_message(conn, msg_id)
for rule in mailbox_rules:
if rule.check(message):
action = actions.factory(rule.get_action(), cfg)
action.invoke(conn, msg_id, message)
num_processed += 1
break
# Remove messages that we just moved.
conn.expunge()
msg_ids = conn.search(['ALL'])
for msg_id in msg_ids:
num_messages += 1
message = get_message(conn, msg_id)
for rule in mailbox_rules:
if rule.check(message):
action = actions.factory(rule.get_action(), cfg)
action.invoke(conn, msg_id, message)
num_processed += 1
break
# Remove messages that we just moved.
conn.expunge()
class Action(metaclass=abc.ABCMeta):
def __init__(self, action_data, cfg):
self._data = action_data
self._cfg = cfg
@abc.abstractmethod
def invoke(self, conn, message_id, message):
raise NotImplementedError()
action:
name: "move"
dest-mailbox: "INBOX.PyATL"
class Move(Action):
def __init__(self, action_data, cfg):
super().__init__(action_data, cfg)
self._dest_mailbox = self._data.get('dest-mailbox')
def invoke(self, conn, message_id, message):
conn.copy([message_id], self._dest_mailbox)
conn.add_flags([message_id], [imapclient.DELETED])
- headers:
- name: to
substring: plans@tripit.com
action:
name: "trash"
class Trash(Move):
def __init__(self, action_data, cfg):
super().__init__(action_data, cfg)
if self._dest_mailbox is None:
self._dest_mailbox = cfg.get('trash-mailbox')
if self._dest_mailbox is None:
raise ValueError('no "trash-mailbox" set in config')
def factory(action_data, cfg):
"Create an Action instance."
name = action_data.get('name')
if name == 'move':
return Move(action_data, cfg)
if name == 'delete':
return Delete(action_data, cfg)
if name == 'trash':
return Trash(action_data, cfg)
raise ValueError('unrecognized rule action {!r}'.format(action_data))
$ imapautofiler Move: 13704 (Re: livarot france - Google Search) to INBOX.Theresa Trash: 13706 (Help Anne with theme work) to INBOX.Trash Trash: 13707 (Fwd: Change in openstack/oslo.db[master]: Warn on URL without a drivername) to INBOX.Trash Move: 13709 ([oslo][all][logging] logging debugging improvement work status) to INBOX.OpenStack.Dev List Move: 13712 (Re: ) to INBOX.Personal Trash: 13713 (Fwd: [Python-Dev] 2017 Python Language Summit coverage) to INBOX.Trash Move: 13732 (Re: [Openstack-operators] [openstack-dev] [upgrades][skip-level][leapfrog] - RFC - Skipping releases when upgrading) to INBOX.OpenStack.Misc Lists
imapautofiler/imapautofiler
http://imapautofiler.readthedocs.io/
imapautofiler/presentation-organize-email-imapautofiler
https://doughellmann.com/presentations/organize-email-imapautofiler
This work is licensed under a Creativle Commons Attribution 4.0 International License.