From 6259efbc44be3e21e42b29246941cd300e79200f Mon Sep 17 00:00:00 2001 From: Jesse Morgan Date: Tue, 24 Nov 2015 11:12:49 -0800 Subject: Adding emailcanary script Adding the main launch script. Adding the listAddress to the pings table. Fixing the Makefile --- Makefile | 3 +- bin/emailcanary | 105 ++++++++++++++++++++++++++++++++++++++++++++++ emailcanary/canary.py | 18 +++++--- emailcanary/canarydb.py | 30 +++++++------ emailcanary/emailutils.py | 2 +- tests/test_canary.py | 6 +-- tests/test_canarydb.py | 26 +++++++----- 7 files changed, 155 insertions(+), 35 deletions(-) create mode 100755 bin/emailcanary diff --git a/Makefile b/Makefile index d2c655e..cae60cc 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ PYTHON=`which python` NAME=`python setup.py --name` -all: check test source deb +all: check test source init: pip install -r requirements.txt --use-mirrors @@ -19,7 +19,6 @@ rpm: test: unit2 discover -s tests -t . - python -mpytest weasyprint check: find . -name \*.py | grep -v "^test_" | xargs pylint --errors-only --reports=n diff --git a/bin/emailcanary b/bin/emailcanary new file mode 100755 index 0000000..2c3a630 --- /dev/null +++ b/bin/emailcanary @@ -0,0 +1,105 @@ +#!/usr/bin/env python + +import argparse +import sys +import smtplib +from emailcanary import canarydb +from emailcanary import canary + +SUCCESS=0 +FAILURE=1 + +def parse_args(): + parser = argparse.ArgumentParser() + parser.add_argument('-d', '--database', default='/etc/emailcanary.db', help='Specify the database to use.') + parser.add_argument('-s', '--smtp', default='localhost:25', help='SMTP Server to send chirps to.') + parser.add_argument('-f', '--from', dest='fromaddress', help='Specify the email address to send the ping from.') + + group = parser.add_mutually_exclusive_group(required=True) + group.add_argument('--chirp', metavar='listAddress', help='Send an email to the given canary list address.') + group.add_argument('--check', metavar='listAddress', help='Check the recepient addresses for the list address.') + group.add_argument('--add', nargs=4, metavar=('listAddress', 'address', 'imapserver', 'password'), help='Add a new recepient and list.') + group.add_argument('--remove', nargs='+', metavar=('listAddress', 'address'), help='Remove recepients from the list.') + group.add_argument('--list', action='store_true', help='List all configured addresses.') + + args = parser.parse_args() + return args + +def get_smtp(address): + index = address.find(':') + if index == -1: + server = address + port = 465 + else: + (server, port) = address.split(':') + return smtplib.SMTP_SSL(server, port) + +def list(db): + accounts = db.get_accounts() + if len(accounts) == 0: + print('No accounts configured.') + return SUCCESS + print "%-25s %-25s %-25s" % ('List Address', 'Recepient', 'IMAP Server') + print "-" * 80 + for account in accounts: + print "%-25s %-25s %-25s" % (account[0], account[1], account[2]) + return SUCCESS + +def add(db, listAddress, recepient, imapserver, password): + db.add_account(listAddress, recepient, imapserver, password) + return SUCCESS + +def remove(db, listAddress, recepients): + if len(recepients) == 0: + recepients = db.get_accounts(listAddress) + for address in recepients: + db.remove_account(listAddress, address) + return SUCCESS + +def check(db, birdie, listAddress): + missing = birdie.check(listAddress) + if len(missing) == 0: + return SUCCESS + print "list recepient uuid time" + for chirp in missing: + print "%s %s %s %d" % (chirp[0], chirp[1], chirp[2], chirp[3].total_seconds()) + return FAILURE + +def main(): + args = parse_args() + if not args: + return + + smtp = None + db = None + try: + db = canarydb.CanaryDB(args.database) + + if args.list: + return list(db) + elif args.add: + return add(db, args.add[0], args.add[1], args.add[2], args.add[3]) + elif args.remove: + return remove(db, args.remove[0], args.remove[1:]) + else: + smtp = get_smtp(args.smtp) + birdie = canary.Canary(db, smtp, args.fromaddress) + if args.chirp: + birdie.chirp(args.chirp) + return SUCCESS + elif args.check: + return check(db, birdie, args.check) + else: + raise Exception('Unknown action') + + except Exception, e: + sys.stderr.write("Error: %s\n" % (str(e))) + return FAILURE + finally: + if smtp: + smtp.quit() + if db: + db.close() + +if __name__ == "__main__": + sys.exit(main()) diff --git a/emailcanary/canary.py b/emailcanary/canary.py index 49972db..e020349 100644 --- a/emailcanary/canary.py +++ b/emailcanary/canary.py @@ -1,5 +1,5 @@ import uuid, datetime, time -import email, emailutils +import email.message, emailutils import re class Canary: @@ -12,12 +12,20 @@ class Canary: chirpUUID = str(uuid.uuid4()) now = datetime.datetime.now() + receipients = self.db.get_recipients_for_list(listAddress) + if len(receipients) == 0: + raise Exception("No receipients for listAddress '%s'", (listAddress,)) + self.send(listAddress, now, chirpUUID) - for dest in self.db.get_recipients_for_list(listAddress): - self.db.ping(dest, now, chirpUUID) + for dest in receipients: + self.db.ping(listAddress, dest, now, chirpUUID) def check(self, listAddress): - for (listAddress, address, imapserver, password) in self.db.get_accounts(listAddress): + '''Check for messages from listAddress and return a list of missing chirps''' + accounts = self.db.get_accounts(listAddress) + if len(accounts) == 0: + raise Exception("No receipients for listAddress '%s'", (listAddress,)) + for (listAddress, address, imapserver, password) in accounts: mail = emailutils.get_imap(imapserver, address, password) these_subjects = [] for uid in emailutils.get_mail_uids(mail): @@ -25,6 +33,7 @@ class Canary: if self.processMessage(address, message): emailutils.delete_message(mail, uid) emailutils.close(mail) + return self.db.get_missing_pongs(listAddress) def processMessage(self, receipient, msg): match = re.match('Canary Email (.+)', msg['Subject']) @@ -35,7 +44,6 @@ class Canary: return True return False - def send(self, dest, date, chirpUUID): msg = email.message.Message() msg['From'] = self.fromaddress diff --git a/emailcanary/canarydb.py b/emailcanary/canarydb.py index 59a2ea2..980431b 100644 --- a/emailcanary/canarydb.py +++ b/emailcanary/canarydb.py @@ -7,7 +7,7 @@ class CanaryDB: # Create Tables if necessary cursor = self.conn.cursor() - cursor.execute("CREATE TABLE IF NOT EXISTS pings (address, uuid, timesent timestamp, timereceived timestamp)") + cursor.execute("CREATE TABLE IF NOT EXISTS pings (listAddress, address, uuid, timesent timestamp, timereceived timestamp)") cursor.execute("CREATE TABLE IF NOT EXISTS accounts (list, address, imapserver, password)") def close(self): @@ -19,9 +19,9 @@ class CanaryDB: (listAddress, address, imapserver, password)) self.conn.commit() - def remove_account(self, address): + def remove_account(self, listAddress, address): cursor = self.conn.cursor() - cursor.execute("DELETE FROM accounts WHERE address=?", (address,)) + cursor.execute("DELETE FROM accounts WHERE list=? AND address=?", (listAddress, address)) self.conn.commit() def get_accounts(self, listAddress = None): @@ -47,10 +47,10 @@ class CanaryDB: results.append(row[0]) return results - def ping(self, address, time, uuid): + def ping(self, listAddress, address, time, uuid): cursor = self.conn.cursor() - cursor.execute("INSERT INTO pings (address, timesent, uuid) VALUES (?, ?, ?)", \ - (address, time, uuid)) + cursor.execute("INSERT INTO pings (listAddress, address, timesent, uuid) VALUES (?, ?, ?, ?)", \ + (listAddress, address, time, uuid)) self.conn.commit() def pong(self, address, time, uuid): @@ -59,16 +59,20 @@ class CanaryDB: (time, address, uuid)) self.conn.commit() - def get_missing_pongs(self): + def get_missing_pongs(self, listAddress = None): '''Return a list of tupls of missing pongs. - Each tupl contains (uuid, address, time since send)''' + Each tupl contains (listAddress, uuid, address, time since send)''' cursor = self.conn.cursor() - cursor.execute("SELECT uuid, address, timesent FROM pings WHERE timereceived IS NULL"); + if listAddress: + cursor.execute("SELECT listAddress, uuid, address, timesent FROM pings WHERE timereceived IS NULL AND listAddress = ?", (listAddress,)); + else: + cursor.execute("SELECT listAddress, uuid, address, timesent FROM pings WHERE timereceived IS NULL"); now = datetime.now() results = list() for row in cursor: - uuid = row[0] - address = row[1] - delta = now - row[2] - results.append((uuid, address, delta)) + listAddress = row[0] + uuid = row[1] + address = row[2] + delta = now - row[3] + results.append((listAddress, uuid, address, delta)) return results diff --git a/emailcanary/emailutils.py b/emailcanary/emailutils.py index b87f637..83c2f46 100644 --- a/emailcanary/emailutils.py +++ b/emailcanary/emailutils.py @@ -19,7 +19,7 @@ def get_message(mail, uid): return email.message_from_string(raw_email) def delete_message(mail, uid): - result = mail.uid('store', uid, '+FLAGS', '\\Deleted') + result = mail.uid('store', uid, '+FLAGS', '(\Deleted)') def close(mail): mail.expunge() diff --git a/tests/test_canary.py b/tests/test_canary.py index ff104f6..d23f3fe 100644 --- a/tests/test_canary.py +++ b/tests/test_canary.py @@ -40,10 +40,10 @@ class TestCanary(unittest.TestCase): # Assert DB updated self.db.get_recipients_for_list.assert_called_with(LIST_ADDRESS) self.db.ping.assert_has_calls( \ - [mock.call(USER_ADDRESS1, mock.ANY, mock.ANY), \ - mock.call(USER_ADDRESS2, mock.ANY, mock.ANY)]) + [mock.call(LIST_ADDRESS, USER_ADDRESS1, mock.ANY, mock.ANY), \ + mock.call(LIST_ADDRESS, USER_ADDRESS2, mock.ANY, mock.ANY)]) args = self.db.ping.call_args - expectedSubject = "Canary Email " + args[0][2] + expectedSubject = "Canary Email " + args[0][3] # Assert emails were sent self.assertEqual(1, self.smtp.sendmail.call_count) diff --git a/tests/test_canarydb.py b/tests/test_canarydb.py index 8a29d23..a51afdc 100644 --- a/tests/test_canarydb.py +++ b/tests/test_canarydb.py @@ -14,23 +14,25 @@ class TestCanaryDB(unittest.TestCase): shutil.rmtree(self.tempdir) def testPingCheckPong(self): + listAddress = "list@example.com" address = "test@example.com" time = datetime.datetime(2015, 10, 24, 9, 00) uuid = "1234" expectedDelta = datetime.datetime.now() - time # Record a Ping - self.db.ping(address, time, uuid) + self.db.ping(listAddress, address, time, uuid) # Check for missing pongs missing = self.db.get_missing_pongs() self.assertEqual(1, len(missing)) firstMissing = missing[0] - self.assertEqual(3, len(firstMissing)) - self.assertEqual(uuid, firstMissing[0]) - self.assertEqual(address, firstMissing[1]) - delta = firstMissing[2].total_seconds() - expectedDelta.total_seconds() + self.assertEqual(4, len(firstMissing)) + self.assertEqual(listAddress, firstMissing[0]) + self.assertEqual(uuid, firstMissing[1]) + self.assertEqual(address, firstMissing[2]) + delta = firstMissing[3].total_seconds() - expectedDelta.total_seconds() self.assertTrue(delta <= 10) # Record a pong @@ -42,13 +44,14 @@ class TestCanaryDB(unittest.TestCase): self.assertEqual(0, len(missing)) def testCloseReopen(self): + listAddress = "list@example.com" address = "test@example.com" time = datetime.datetime(2015, 10, 24, 9, 00) uuid = "1234" expectedDelta = datetime.datetime.now() - time # Record a Ping - self.db.ping(address, time, uuid) + self.db.ping(listAddress, address, time, uuid) # Close, Reopen self.db.close() @@ -59,10 +62,11 @@ class TestCanaryDB(unittest.TestCase): self.assertEqual(1, len(missing)) firstMissing = missing[0] - self.assertEqual(3, len(firstMissing)) - self.assertEqual(uuid, firstMissing[0]) - self.assertEqual(address, firstMissing[1]) - delta = firstMissing[2].total_seconds() - expectedDelta.total_seconds() + self.assertEqual(4, len(firstMissing)) + self.assertEqual(listAddress, firstMissing[0]) + self.assertEqual(uuid, firstMissing[1]) + self.assertEqual(address, firstMissing[2]) + delta = firstMissing[3].total_seconds() - expectedDelta.total_seconds() self.assertTrue(delta <= 10) def testAccounts(self): @@ -87,7 +91,7 @@ class TestCanaryDB(unittest.TestCase): self.assertEqual(password, accounts[0][3]) # Remove the account - self.db.remove_account(address) + self.db.remove_account(listAddress, address) accounts = self.db.get_accounts() self.assertEqual(0, len(accounts)) -- cgit v1.2.3