wip: register, mailbot
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -13,3 +13,4 @@ docs/build/
|
|||||||
.coverage
|
.coverage
|
||||||
.tox/
|
.tox/
|
||||||
build/
|
build/
|
||||||
|
share/
|
||||||
|
|||||||
@@ -3,3 +3,11 @@
|
|||||||
|
|
||||||
class Callback(object):
|
class Callback(object):
|
||||||
"""Base class for callbacks."""
|
"""Base class for callbacks."""
|
||||||
|
|
||||||
|
def __init__(self, message, rules):
|
||||||
|
self.message = message
|
||||||
|
self.rules = rules
|
||||||
|
|
||||||
|
def check_rules(self):
|
||||||
|
"""Does this message conform to the rules provided?"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|||||||
@@ -1,5 +1,53 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from email import message_from_string
|
||||||
|
|
||||||
|
from imapclient import IMAPClient
|
||||||
|
|
||||||
|
|
||||||
class MailBot(object):
|
class MailBot(object):
|
||||||
"""MailBot mail class, where the magic is happening."""
|
"""MailBot mail class, where the magic is happening.
|
||||||
|
|
||||||
|
Connect to the SMTP server using the IMAP protocol, for each unflagged
|
||||||
|
message check which callbacks should be triggered, if any, but testing
|
||||||
|
against the registered rules for each of them.
|
||||||
|
|
||||||
|
"""
|
||||||
|
imapclient = IMAPClient
|
||||||
|
message_constructor = message_from_string # easier for testing
|
||||||
|
|
||||||
|
def __init__(self, host, username, password, port=None, use_uid=True,
|
||||||
|
ssl=False, stream=False):
|
||||||
|
self.client = self.imapclient(host, port=port, use_uid=use_uid,
|
||||||
|
ssl=ssl, stream=stream)
|
||||||
|
self.client.login(username, password)
|
||||||
|
|
||||||
|
def get_message_ids(self):
|
||||||
|
"""Return the list of IDs of messages to process."""
|
||||||
|
return self.client.search(['UNFLAGGED'])
|
||||||
|
|
||||||
|
def get_messages(self):
|
||||||
|
"""Return the list of messages to process."""
|
||||||
|
ids = self.get_message_ids()
|
||||||
|
return self.client.fetch(ids, ['RFC822'])
|
||||||
|
|
||||||
|
def process_message(self, message, callback_class, rules):
|
||||||
|
"""Check if callback matches rules, and if so, trigger."""
|
||||||
|
callback = callback_class(message, rules)
|
||||||
|
if callback.check_rules():
|
||||||
|
return callback.callback()
|
||||||
|
|
||||||
|
def process_messages(self):
|
||||||
|
"""Process messages: check which callbacks should be triggered."""
|
||||||
|
from . import CALLBACKS_MAP
|
||||||
|
messages = self.get_messages()
|
||||||
|
|
||||||
|
for uid, msg in messages.items():
|
||||||
|
message = self.message_constructor(msg['RFC822'])
|
||||||
|
for callback_class, rules in CALLBACKS_MAP.items():
|
||||||
|
self.process_message(message, callback_class, rules)
|
||||||
|
self.mark_processed(uid)
|
||||||
|
|
||||||
|
def mark_processed(self, uid):
|
||||||
|
"""Mark the message corresponding to uid as processed."""
|
||||||
|
self.client.set_flags([uid], ['FLAGGED'])
|
||||||
|
|||||||
16
mailbot/tests/test_callback.py
Normal file
16
mailbot/tests/test_callback.py
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from . import MailBotTestCase
|
||||||
|
from .. import Callback
|
||||||
|
|
||||||
|
|
||||||
|
class CallbackTest(MailBotTestCase):
|
||||||
|
|
||||||
|
def test_init(self):
|
||||||
|
callback = Callback('foo', 'bar')
|
||||||
|
self.assertEqual(callback.message, 'foo')
|
||||||
|
self.assertEqual(callback.rules, 'bar')
|
||||||
|
|
||||||
|
def test_check_rules(self):
|
||||||
|
callback = Callback('foo', 'bar')
|
||||||
|
self.assertEqual(callback.check_rules(), False)
|
||||||
43
mailbot/tests/test_init.py
Normal file
43
mailbot/tests/test_init.py
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from . import MailBotTestCase
|
||||||
|
from .. import register, CALLBACKS_MAP, RegisterException
|
||||||
|
|
||||||
|
|
||||||
|
class RegisterTest(MailBotTestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super(RegisterTest, self).setUp()
|
||||||
|
|
||||||
|
class EmptyCallback(object):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class WithRulesCallback(object):
|
||||||
|
rules = {'foo': 'bar', 'baz': 'bat'}
|
||||||
|
|
||||||
|
self.empty_callback = EmptyCallback
|
||||||
|
self.with_rules_callback = WithRulesCallback
|
||||||
|
|
||||||
|
def test_register(self):
|
||||||
|
before = len(CALLBACKS_MAP)
|
||||||
|
register(self.empty_callback)
|
||||||
|
self.assertEqual(len(CALLBACKS_MAP), before + 1)
|
||||||
|
|
||||||
|
def test_register_existing(self):
|
||||||
|
register(self.empty_callback)
|
||||||
|
self.assertRaises(RegisterException, register, self.empty_callback)
|
||||||
|
self.assertTrue(register(self.with_rules_callback))
|
||||||
|
|
||||||
|
def test_register_without_rules_callback_with_rules(self):
|
||||||
|
register(self.with_rules_callback)
|
||||||
|
self.assertEqual(CALLBACKS_MAP[self.with_rules_callback],
|
||||||
|
self.with_rules_callback.rules)
|
||||||
|
|
||||||
|
def test_register_with_rules_callback_without_rules(self):
|
||||||
|
register(self.empty_callback, {'one': 'two'})
|
||||||
|
self.assertEqual(CALLBACKS_MAP[self.empty_callback], {'one': 'two'})
|
||||||
|
|
||||||
|
def test_register_with_rules_callback_with_rules(self):
|
||||||
|
register(self.with_rules_callback, {'baz': 'wow'})
|
||||||
|
self.assertEqual(CALLBACKS_MAP[self.with_rules_callback],
|
||||||
|
{'foo': 'bar', 'baz': 'wow'})
|
||||||
@@ -1,43 +1,107 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from mock import patch, sentinel, Mock, DEFAULT, call
|
||||||
|
|
||||||
from . import MailBotTestCase
|
from . import MailBotTestCase
|
||||||
from .. import register, CALLBACKS_MAP, RegisterException
|
from .. import CALLBACKS_MAP, MailBot
|
||||||
|
|
||||||
|
|
||||||
class RegisterTest(MailBotTestCase):
|
class TestableMailBot(MailBot):
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
self.client = Mock()
|
||||||
|
|
||||||
|
|
||||||
|
class MailBotClientTest(MailBotTestCase):
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super(RegisterTest, self).setUp()
|
super(MailBotClientTest, self).setUp()
|
||||||
|
self.bot = TestableMailBot('somehost', 'john', 'doe')
|
||||||
|
|
||||||
class EmptyCallback(object):
|
|
||||||
pass
|
|
||||||
|
|
||||||
class WithRulesCallback(object):
|
class MailBotTest(MailBotClientTest):
|
||||||
rules = {'foo': 'bar', 'baz': 'bat'}
|
|
||||||
|
|
||||||
self.empty_callback = EmptyCallback
|
@patch.multiple('imapclient.imapclient.IMAPClient',
|
||||||
self.with_rules_callback = WithRulesCallback
|
login=DEFAULT, __init__=DEFAULT)
|
||||||
|
def test_init(self, login, __init__):
|
||||||
|
__init__.return_value = None
|
||||||
|
|
||||||
def test_register(self):
|
kwargs = {'port': sentinel.port,
|
||||||
before = len(CALLBACKS_MAP)
|
'ssl': sentinel.ssl,
|
||||||
register(self.empty_callback)
|
'use_uid': sentinel.use_uid,
|
||||||
self.assertEqual(len(CALLBACKS_MAP), before + 1)
|
'stream': sentinel.use_stream}
|
||||||
|
MailBot('somehost', 'john', 'doe', **kwargs)
|
||||||
|
|
||||||
def test_register_existing(self):
|
__init__.assert_called_once_with('somehost', **kwargs)
|
||||||
register(self.empty_callback)
|
login.assert_called_once_with('john', 'doe')
|
||||||
self.assertRaises(RegisterException, register, self.empty_callback)
|
|
||||||
self.assertTrue(register(self.with_rules_callback))
|
|
||||||
|
|
||||||
def test_register_without_rules_callback_with_rules(self):
|
def test_get_message_ids(self):
|
||||||
register(self.with_rules_callback)
|
self.bot.client.search.return_value = sentinel.id_list
|
||||||
self.assertEqual(CALLBACKS_MAP[self.with_rules_callback],
|
|
||||||
self.with_rules_callback.rules)
|
|
||||||
|
|
||||||
def test_register_with_rules_callback_without_rules(self):
|
res = self.bot.get_message_ids()
|
||||||
register(self.empty_callback, {'one': 'two'})
|
|
||||||
self.assertEqual(CALLBACKS_MAP[self.empty_callback], {'one': 'two'})
|
|
||||||
|
|
||||||
def test_register_with_rules_callback_with_rules(self):
|
self.bot.client.search.assert_called_once_with(['UNFLAGGED'])
|
||||||
register(self.with_rules_callback, {'baz': 'wow'})
|
self.assertEqual(res, sentinel.id_list)
|
||||||
self.assertEqual(CALLBACKS_MAP[self.with_rules_callback],
|
|
||||||
{'foo': 'bar', 'baz': 'wow'})
|
def test_get_messages(self):
|
||||||
|
self.bot.get_message_ids = Mock(return_value=sentinel.ids)
|
||||||
|
self.bot.client.fetch.return_value = sentinel.message_list
|
||||||
|
|
||||||
|
messages = self.bot.get_messages()
|
||||||
|
|
||||||
|
self.bot.get_message_ids.assert_called_once_with()
|
||||||
|
self.bot.client.fetch.assert_called_once_with(sentinel.ids, ['RFC822'])
|
||||||
|
self.assertEqual(messages, sentinel.message_list)
|
||||||
|
|
||||||
|
def test_process_message_trigger(self):
|
||||||
|
callback = Mock()
|
||||||
|
callback.check_rules.return_value = True
|
||||||
|
callback.callback.return_value = sentinel.callback_result
|
||||||
|
callback_class = Mock(return_value=callback)
|
||||||
|
|
||||||
|
res = self.bot.process_message(sentinel.message, callback_class,
|
||||||
|
sentinel.rules)
|
||||||
|
|
||||||
|
callback_class.assert_called_once_with(sentinel.message,
|
||||||
|
sentinel.rules)
|
||||||
|
callback.check_rules.assert_called_once_with()
|
||||||
|
callback.callback.assert_called_once_with()
|
||||||
|
self.assertEqual(res, sentinel.callback_result)
|
||||||
|
|
||||||
|
def test_process_message_no_trigger(self):
|
||||||
|
callback = Mock()
|
||||||
|
callback.check_rules.return_value = False
|
||||||
|
callback_class = Mock(return_value=callback)
|
||||||
|
|
||||||
|
res = self.bot.process_message(sentinel.message, callback_class,
|
||||||
|
sentinel.rules)
|
||||||
|
|
||||||
|
callback.check_rules.assert_called_once_with()
|
||||||
|
self.assertEqual(res, None)
|
||||||
|
|
||||||
|
def test_process_messages(self):
|
||||||
|
messages = {1: {'RFC822': sentinel.mail1},
|
||||||
|
2: {'RFC822': sentinel.mail2}}
|
||||||
|
self.bot.get_messages = Mock(return_value=messages)
|
||||||
|
# message constructor will return exactly what it's given
|
||||||
|
# to be used in the "self.bot.process_message.assert_has_calls" below
|
||||||
|
self.bot.message_constructor = Mock(side_effect=lambda m: m)
|
||||||
|
self.bot.process_message = Mock()
|
||||||
|
self.bot.mark_processed = Mock()
|
||||||
|
CALLBACKS_MAP.update({sentinel.callback1: sentinel.rules1,
|
||||||
|
sentinel.callback2: sentinel.rules2})
|
||||||
|
|
||||||
|
self.bot.process_messages()
|
||||||
|
|
||||||
|
self.bot.get_messages.assert_called_once_with()
|
||||||
|
self.bot.process_message.assert_has_calls(
|
||||||
|
[call(sentinel.mail1, sentinel.callback1, sentinel.rules1),
|
||||||
|
call(sentinel.mail2, sentinel.callback1, sentinel.rules1),
|
||||||
|
call(sentinel.mail1, sentinel.callback2, sentinel.rules2),
|
||||||
|
call(sentinel.mail2, sentinel.callback2, sentinel.rules2)],
|
||||||
|
any_order=True)
|
||||||
|
|
||||||
|
def test_mark_processed(self):
|
||||||
|
self.bot.mark_processed(sentinel.id)
|
||||||
|
self.bot.client.set_flags.assert_called_once_with([sentinel.id],
|
||||||
|
['FLAGGED'])
|
||||||
|
|||||||
Reference in New Issue
Block a user