From 3177ba26421b730bdf475fc2e991eef9ab9ef067 Mon Sep 17 00:00:00 2001 From: "James E. Blair" Date: Sun, 9 Jun 2019 10:05:11 -0700 Subject: Add support for Delta Also add an undocumented mailbox driver for a directory of files for ease of testing (this could probably become a maildir driver with a bit more work). Remove unecessary decode calls from the message traversal. --- README.rst | 5 +- email_assistant/assistant.py | 36 ++++++++--- email_assistant/plugins/__init__.py | 2 + email_assistant/plugins/delta.py | 115 ++++++++++++++++++++++++++++++++++ email_assistant/plugins/eventbrite.py | 2 +- email_assistant/plugins/general.py | 2 +- email_assistant/plugins/marriott.py | 2 +- email_assistant/plugins/united.py | 2 +- 8 files changed, 152 insertions(+), 14 deletions(-) create mode 100644 email_assistant/plugins/delta.py diff --git a/README.rst b/README.rst index 8715aa6..dd22c95 100644 --- a/README.rst +++ b/README.rst @@ -14,9 +14,10 @@ even if an email is seen multiple times. It currently understands emails from the following senders: -* United Airlines -* Marriott hotels (when booked directly) +* Delta Air Lines * Eventbrite +* Marriott hotels (when booked directly) +* United Airlines It is simple to add support for more types of emails, and additions are welcome. diff --git a/email_assistant/assistant.py b/email_assistant/assistant.py index a784077..d39b016 100644 --- a/email_assistant/assistant.py +++ b/email_assistant/assistant.py @@ -37,7 +37,7 @@ from email_assistant import plugins # Number of days to look backwards when scanning a mailbox for the first time: IMAP_BACKFILL = 180 -class Mailbox: +class IMAPMailbox: def __init__(self, name, host, username, password, folders): self.log = logging.getLogger('assistant.mailbox') self.name = name @@ -85,6 +85,21 @@ class Mailbox: with open(self.state_file, 'w') as f: json.dump(self.uidinfo, f) +class DirMailbox: + def __init__(self, name, directory): + self.log = logging.getLogger('assistant.mailbox') + self.name = name + self.directory = directory + + def get_messages(self): + for fn in os.listdir(self.directory): + with open(os.path.join(self.directory, fn), 'rb') as f: + msg = f.read() + yield msg + + def save(self): + pass + class Calendar: def __init__(self, url, username, password, calendar): self.log = logging.getLogger('assistant.calendar') @@ -116,7 +131,7 @@ class Calendar: class Assistant: def __init__(self): - self.log = logging.getLogger('main') + self.log = logging.getLogger('assistant.main') self.geolocator = None self.tzfinder = None self.plugins = [] @@ -151,12 +166,17 @@ class Assistant: for section in config.sections(): if section.startswith('mailbox '): name = section.split()[1] - mailboxes[name] = Mailbox( - name, - config[section]['host'], - config[section]['username'], - config[section]['password'], - config[section]['folders'].split(',')) + if config[section]['type'].lower() == 'imap': + mailboxes[name] = IMAPMailbox( + name, + config[section]['host'], + config[section]['username'], + config[section]['password'], + config[section]['folders'].split(',')) + elif config[section]['type'].lower() == 'dir': + mailboxes[name] = DirMailbox( + name, + config[section]['path']) elif section.startswith('calendar '): name = section.split()[1] calendars[name] = Calendar( diff --git a/email_assistant/plugins/__init__.py b/email_assistant/plugins/__init__.py index 10b28f0..295852e 100644 --- a/email_assistant/plugins/__init__.py +++ b/email_assistant/plugins/__init__.py @@ -21,9 +21,11 @@ from . import eventbrite from . import marriott from . import united +from . import delta plugins = [ eventbrite.Plugin, marriott.Plugin, united.Plugin, + delta.Plugin, ] diff --git a/email_assistant/plugins/delta.py b/email_assistant/plugins/delta.py new file mode 100644 index 0000000..50da506 --- /dev/null +++ b/email_assistant/plugins/delta.py @@ -0,0 +1,115 @@ +#!/usr/bin/env python3 + +# Copyright (C) 2019 James E. Blair +# +# This file is part of Email-assistant. +# +# Email-assistant is free software: you can redistribute it and/or +# modify it under the terms of the GNU Affero General Public License +# as published by the Free Software Foundation, either version 3 of +# the License, or (at your option) any later version. +# +# Email-assistant is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Email-assistant. If not, see +# . + +import re +import logging +import hashlib + +from bs4 import BeautifulSoup +import dateutil.parser +import dateutil.tz +import inscriptis +import vobject + +from email_assistant import iata +from email_assistant import plugin + +def parse_dep_arr(dep_date, flight_date, flight_time, code): + flight_year = dep_date.year + tz = iata.tzmap[code] + flight_time = dateutil.parser.parse(flight_date + ' ' + flight_time) + if flight_time.year < flight_year: + flight_time.replace(year=flight_year) + flight_time = flight_time.replace(tzinfo=dateutil.tz.gettz(tz)) + return flight_time + +class Plugin(plugin.Plugin): + name = 'delta' + + def match(self, msg): + if ('DeltaAirLines@e.delta.com' in msg['From'] and + 'Your Flight Receipt' in msg['Subject']): + return True + + def get_events(self, msg): + events = [] + for part in msg.walk(): + if part.get_content_type() == 'text/html': + soup = BeautifulSoup(part.get_payload(decode=True), 'html.parser') + flights = [] + flight = None + dep_date = None + start = soup.find(string=re.compile('FLIGHT INFO STARTS')) + for row in start.parent.find_all('tr', recursive=False): + row = [x.strip() for x in row.strings if x.strip()] + if len(row) == 3 and row[1:] == ['DEPART', 'ARRIVE']: + dep_date = row[0] + continue + if dep_date is None: continue + f = { + 'dep_date': dep_date, + #'num': row[0], + #'cabin': row[1], + 'dep_city': row[2], + 'dep_time': row[3], + 'arr_city': row[4], + 'arr_time': row[5]} + if len(row) > 6: + f['arr_date'] = row[6].replace('*', '').strip() + else: + f['arr_date'] = dep_date + flights.append(f) + start = soup.find(string=re.compile('Checked Bag Allowance')) + while start.name != 'table': start = start.parent + start = start.parent + while start.name != 'table': start = start.parent + index = 0 + for row in start.find_all('tr', recursive=False): + row = [x.strip() for x in row.strings if x.strip()] + if len(row) != 3: continue + try: + dep_date = dateutil.parser.parse(row[0]) + except Exception: + continue + f = flights[index] + f['dep_code'] = row[1].split()[-1].strip() + f['arr_code'] = row[2].split()[-1].strip() + f['dep_dt'] = parse_dep_arr( + dep_date, f['dep_date'], f['dep_time'], f['dep_code']) + f['arr_dt'] = parse_dep_arr( + dep_date, f['arr_date'], f['arr_time'], f['arr_code']) + index += 1 + for f in flights: + self.log.debug('%s %s %s %s', + f['dep_dt'], f['arr_dt'], f['dep_code'], f['arr_code']) + cal = vobject.iCalendar() + event = cal.add('vevent') + event.add('dtstart').value = f['dep_dt'] + event.add('dtend').value = f['arr_dt'] + summary = "Flight from %s to %s" % (f['dep_code'], f['arr_code']) + event.add('summary').value = summary + text = inscriptis.get_text(str(soup)) + text = re.sub(r'([^ ]+)\s*\n', '\\1\n', text) + event.add('description').value = text + event.add('location').value = "%s airport" % (f['dep_code'],) + uid = hashlib.sha1((str(f['dep_time']) + summary).encode('utf8')).hexdigest() + event.add('uid').value = uid + events.append(cal) + return events diff --git a/email_assistant/plugins/eventbrite.py b/email_assistant/plugins/eventbrite.py index 9ef073a..afd869f 100644 --- a/email_assistant/plugins/eventbrite.py +++ b/email_assistant/plugins/eventbrite.py @@ -44,7 +44,7 @@ class Plugin(plugin.Plugin): events = [] for part in msg.walk(): if part.get_content_type() == 'text/html': - soup = BeautifulSoup(part.get_payload(decode=True).decode('utf8'), 'html.parser') + soup = BeautifulSoup(part.get_payload(decode=True), 'html.parser') data = json.loads(soup.find('script', type="application/ld+json").string) address = data['reservationFor']['location']['address'] diff --git a/email_assistant/plugins/general.py b/email_assistant/plugins/general.py index c0517b8..e21274d 100644 --- a/email_assistant/plugins/general.py +++ b/email_assistant/plugins/general.py @@ -24,7 +24,7 @@ def get_events(msg): log = logging.getLogger('assistant.marriott') for part in msg.walk(): if part.get_content_type() == 'text/html': - soup = BeautifulSoup(part.get_payload(decode=True).decode('utf8'), 'html.parser') + soup = BeautifulSoup(part.get_payload(decode=True), 'html.parser') start = end = location = None for element in soup.descendants: diff --git a/email_assistant/plugins/marriott.py b/email_assistant/plugins/marriott.py index 52597cd..1f2abab 100644 --- a/email_assistant/plugins/marriott.py +++ b/email_assistant/plugins/marriott.py @@ -43,7 +43,7 @@ class Plugin(plugin.Plugin): events = [] for part in msg.walk(): if part.get_content_type() == 'text/html': - soup = BeautifulSoup(part.get_payload(decode=True).decode('utf8'), 'html.parser') + soup = BeautifulSoup(part.get_payload(decode=True), 'html.parser') summary = soup.find_all('table')[7].a.string.strip() location = soup.find_all('table')[9].a.string.strip() start = (soup.find('th', string=re.compile('Check-In:')). diff --git a/email_assistant/plugins/united.py b/email_assistant/plugins/united.py index 0b33cd9..38de3e2 100644 --- a/email_assistant/plugins/united.py +++ b/email_assistant/plugins/united.py @@ -60,7 +60,7 @@ class Plugin(plugin.Plugin): events = [] for part in msg.walk(): if part.get_content_type() == 'text/html': - soup = BeautifulSoup(part.get_payload(decode=True).decode('utf8'), 'html.parser') + soup = BeautifulSoup(part.get_payload(decode=True), 'html.parser') # confirmation_number = soup.find(class_="eTicketConfirmation").string index = 0 -- cgit v1.2.3