summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJames E. Blair <corvus@gnu.org>2019-06-09 10:05:11 -0700
committerJames E. Blair <corvus@gnu.org>2019-06-09 10:07:14 -0700
commit3177ba26421b730bdf475fc2e991eef9ab9ef067 (patch)
treea7cce1756ccb5d6c50b1b56e1b24885ba683851f
parentc44874cfa48bf00b83057033fc7800b35e157702 (diff)
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.
-rw-r--r--README.rst5
-rw-r--r--email_assistant/assistant.py36
-rw-r--r--email_assistant/plugins/__init__.py2
-rw-r--r--email_assistant/plugins/delta.py115
-rw-r--r--email_assistant/plugins/eventbrite.py2
-rw-r--r--email_assistant/plugins/general.py2
-rw-r--r--email_assistant/plugins/marriott.py2
-rw-r--r--email_assistant/plugins/united.py2
8 files changed, 152 insertions, 14 deletions
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.
14 14
15It currently understands emails from the following senders: 15It currently understands emails from the following senders:
16 16
17* United Airlines 17* Delta Air Lines
18* Marriott hotels (when booked directly)
19* Eventbrite 18* Eventbrite
19* Marriott hotels (when booked directly)
20* United Airlines
20 21
21It is simple to add support for more types of emails, and additions 22It is simple to add support for more types of emails, and additions
22are welcome. 23are 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
37# Number of days to look backwards when scanning a mailbox for the first time: 37# Number of days to look backwards when scanning a mailbox for the first time:
38IMAP_BACKFILL = 180 38IMAP_BACKFILL = 180
39 39
40class Mailbox: 40class IMAPMailbox:
41 def __init__(self, name, host, username, password, folders): 41 def __init__(self, name, host, username, password, folders):
42 self.log = logging.getLogger('assistant.mailbox') 42 self.log = logging.getLogger('assistant.mailbox')
43 self.name = name 43 self.name = name
@@ -85,6 +85,21 @@ class Mailbox:
85 with open(self.state_file, 'w') as f: 85 with open(self.state_file, 'w') as f:
86 json.dump(self.uidinfo, f) 86 json.dump(self.uidinfo, f)
87 87
88class DirMailbox:
89 def __init__(self, name, directory):
90 self.log = logging.getLogger('assistant.mailbox')
91 self.name = name
92 self.directory = directory
93
94 def get_messages(self):
95 for fn in os.listdir(self.directory):
96 with open(os.path.join(self.directory, fn), 'rb') as f:
97 msg = f.read()
98 yield msg
99
100 def save(self):
101 pass
102
88class Calendar: 103class Calendar:
89 def __init__(self, url, username, password, calendar): 104 def __init__(self, url, username, password, calendar):
90 self.log = logging.getLogger('assistant.calendar') 105 self.log = logging.getLogger('assistant.calendar')
@@ -116,7 +131,7 @@ class Calendar:
116 131
117class Assistant: 132class Assistant:
118 def __init__(self): 133 def __init__(self):
119 self.log = logging.getLogger('main') 134 self.log = logging.getLogger('assistant.main')
120 self.geolocator = None 135 self.geolocator = None
121 self.tzfinder = None 136 self.tzfinder = None
122 self.plugins = [] 137 self.plugins = []
@@ -151,12 +166,17 @@ class Assistant:
151 for section in config.sections(): 166 for section in config.sections():
152 if section.startswith('mailbox '): 167 if section.startswith('mailbox '):
153 name = section.split()[1] 168 name = section.split()[1]
154 mailboxes[name] = Mailbox( 169 if config[section]['type'].lower() == 'imap':
155 name, 170 mailboxes[name] = IMAPMailbox(
156 config[section]['host'], 171 name,
157 config[section]['username'], 172 config[section]['host'],
158 config[section]['password'], 173 config[section]['username'],
159 config[section]['folders'].split(',')) 174 config[section]['password'],
175 config[section]['folders'].split(','))
176 elif config[section]['type'].lower() == 'dir':
177 mailboxes[name] = DirMailbox(
178 name,
179 config[section]['path'])
160 elif section.startswith('calendar '): 180 elif section.startswith('calendar '):
161 name = section.split()[1] 181 name = section.split()[1]
162 calendars[name] = Calendar( 182 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 @@
21from . import eventbrite 21from . import eventbrite
22from . import marriott 22from . import marriott
23from . import united 23from . import united
24from . import delta
24 25
25plugins = [ 26plugins = [
26 eventbrite.Plugin, 27 eventbrite.Plugin,
27 marriott.Plugin, 28 marriott.Plugin,
28 united.Plugin, 29 united.Plugin,
30 delta.Plugin,
29] 31]
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 @@
1#!/usr/bin/env python3
2
3# Copyright (C) 2019 James E. Blair <corvus@gnu.org>
4#
5# This file is part of Email-assistant.
6#
7# Email-assistant is free software: you can redistribute it and/or
8# modify it under the terms of the GNU Affero General Public License
9# as published by the Free Software Foundation, either version 3 of
10# the License, or (at your option) any later version.
11#
12# Email-assistant is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
15# General Public License for more details.
16#
17# You should have received a copy of the GNU General Public License
18# along with Email-assistant. If not, see
19# <https://www.gnu.org/licenses/>.
20
21import re
22import logging
23import hashlib
24
25from bs4 import BeautifulSoup
26import dateutil.parser
27import dateutil.tz
28import inscriptis
29import vobject
30
31from email_assistant import iata
32from email_assistant import plugin
33
34def parse_dep_arr(dep_date, flight_date, flight_time, code):
35 flight_year = dep_date.year
36 tz = iata.tzmap[code]
37 flight_time = dateutil.parser.parse(flight_date + ' ' + flight_time)
38 if flight_time.year < flight_year:
39 flight_time.replace(year=flight_year)
40 flight_time = flight_time.replace(tzinfo=dateutil.tz.gettz(tz))
41 return flight_time
42
43class Plugin(plugin.Plugin):
44 name = 'delta'
45
46 def match(self, msg):
47 if ('DeltaAirLines@e.delta.com' in msg['From'] and
48 'Your Flight Receipt' in msg['Subject']):
49 return True
50
51 def get_events(self, msg):
52 events = []
53 for part in msg.walk():
54 if part.get_content_type() == 'text/html':
55 soup = BeautifulSoup(part.get_payload(decode=True), 'html.parser')
56 flights = []
57 flight = None
58 dep_date = None
59 start = soup.find(string=re.compile('FLIGHT INFO STARTS'))
60 for row in start.parent.find_all('tr', recursive=False):
61 row = [x.strip() for x in row.strings if x.strip()]
62 if len(row) == 3 and row[1:] == ['DEPART', 'ARRIVE']:
63 dep_date = row[0]
64 continue
65 if dep_date is None: continue
66 f = {
67 'dep_date': dep_date,
68 #'num': row[0],
69 #'cabin': row[1],
70 'dep_city': row[2],
71 'dep_time': row[3],
72 'arr_city': row[4],
73 'arr_time': row[5]}
74 if len(row) > 6:
75 f['arr_date'] = row[6].replace('*', '').strip()
76 else:
77 f['arr_date'] = dep_date
78 flights.append(f)
79 start = soup.find(string=re.compile('Checked Bag Allowance'))
80 while start.name != 'table': start = start.parent
81 start = start.parent
82 while start.name != 'table': start = start.parent
83 index = 0
84 for row in start.find_all('tr', recursive=False):
85 row = [x.strip() for x in row.strings if x.strip()]
86 if len(row) != 3: continue
87 try:
88 dep_date = dateutil.parser.parse(row[0])
89 except Exception:
90 continue
91 f = flights[index]
92 f['dep_code'] = row[1].split()[-1].strip()
93 f['arr_code'] = row[2].split()[-1].strip()
94 f['dep_dt'] = parse_dep_arr(
95 dep_date, f['dep_date'], f['dep_time'], f['dep_code'])
96 f['arr_dt'] = parse_dep_arr(
97 dep_date, f['arr_date'], f['arr_time'], f['arr_code'])
98 index += 1
99 for f in flights:
100 self.log.debug('%s %s %s %s',
101 f['dep_dt'], f['arr_dt'], f['dep_code'], f['arr_code'])
102 cal = vobject.iCalendar()
103 event = cal.add('vevent')
104 event.add('dtstart').value = f['dep_dt']
105 event.add('dtend').value = f['arr_dt']
106 summary = "Flight from %s to %s" % (f['dep_code'], f['arr_code'])
107 event.add('summary').value = summary
108 text = inscriptis.get_text(str(soup))
109 text = re.sub(r'([^ ]+)\s*\n', '\\1\n', text)
110 event.add('description').value = text
111 event.add('location').value = "%s airport" % (f['dep_code'],)
112 uid = hashlib.sha1((str(f['dep_time']) + summary).encode('utf8')).hexdigest()
113 event.add('uid').value = uid
114 events.append(cal)
115 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):
44 events = [] 44 events = []
45 for part in msg.walk(): 45 for part in msg.walk():
46 if part.get_content_type() == 'text/html': 46 if part.get_content_type() == 'text/html':
47 soup = BeautifulSoup(part.get_payload(decode=True).decode('utf8'), 'html.parser') 47 soup = BeautifulSoup(part.get_payload(decode=True), 'html.parser')
48 data = json.loads(soup.find('script', type="application/ld+json").string) 48 data = json.loads(soup.find('script', type="application/ld+json").string)
49 49
50 address = data['reservationFor']['location']['address'] 50 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):
24 log = logging.getLogger('assistant.marriott') 24 log = logging.getLogger('assistant.marriott')
25 for part in msg.walk(): 25 for part in msg.walk():
26 if part.get_content_type() == 'text/html': 26 if part.get_content_type() == 'text/html':
27 soup = BeautifulSoup(part.get_payload(decode=True).decode('utf8'), 'html.parser') 27 soup = BeautifulSoup(part.get_payload(decode=True), 'html.parser')
28 28
29 start = end = location = None 29 start = end = location = None
30 for element in soup.descendants: 30 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):
43 events = [] 43 events = []
44 for part in msg.walk(): 44 for part in msg.walk():
45 if part.get_content_type() == 'text/html': 45 if part.get_content_type() == 'text/html':
46 soup = BeautifulSoup(part.get_payload(decode=True).decode('utf8'), 'html.parser') 46 soup = BeautifulSoup(part.get_payload(decode=True), 'html.parser')
47 summary = soup.find_all('table')[7].a.string.strip() 47 summary = soup.find_all('table')[7].a.string.strip()
48 location = soup.find_all('table')[9].a.string.strip() 48 location = soup.find_all('table')[9].a.string.strip()
49 start = (soup.find('th', string=re.compile('Check-In:')). 49 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):
60 events = [] 60 events = []
61 for part in msg.walk(): 61 for part in msg.walk():
62 if part.get_content_type() == 'text/html': 62 if part.get_content_type() == 'text/html':
63 soup = BeautifulSoup(part.get_payload(decode=True).decode('utf8'), 'html.parser') 63 soup = BeautifulSoup(part.get_payload(decode=True), 'html.parser')
64 # confirmation_number = soup.find(class_="eTicketConfirmation").string 64 # confirmation_number = soup.find(class_="eTicketConfirmation").string
65 65
66 index = 0 66 index = 0