diff options
author | James E. Blair <corvus@gnu.org> | 2019-06-09 10:05:11 -0700 |
---|---|---|
committer | James E. Blair <corvus@gnu.org> | 2019-06-09 10:07:14 -0700 |
commit | 3177ba26421b730bdf475fc2e991eef9ab9ef067 (patch) | |
tree | a7cce1756ccb5d6c50b1b56e1b24885ba683851f | |
parent | c44874cfa48bf00b83057033fc7800b35e157702 (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.rst | 5 | ||||
-rw-r--r-- | email_assistant/assistant.py | 36 | ||||
-rw-r--r-- | email_assistant/plugins/__init__.py | 2 | ||||
-rw-r--r-- | email_assistant/plugins/delta.py | 115 | ||||
-rw-r--r-- | email_assistant/plugins/eventbrite.py | 2 | ||||
-rw-r--r-- | email_assistant/plugins/general.py | 2 | ||||
-rw-r--r-- | email_assistant/plugins/marriott.py | 2 | ||||
-rw-r--r-- | email_assistant/plugins/united.py | 2 |
8 files changed, 152 insertions, 14 deletions
@@ -14,9 +14,10 @@ even if an email is seen multiple times. | |||
14 | 14 | ||
15 | It currently understands emails from the following senders: | 15 | It 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 | ||
21 | It is simple to add support for more types of emails, and additions | 22 | It is simple to add support for more types of emails, and additions |
22 | are welcome. | 23 | 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 | |||
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: |
38 | IMAP_BACKFILL = 180 | 38 | IMAP_BACKFILL = 180 |
39 | 39 | ||
40 | class Mailbox: | 40 | class 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 | ||
88 | class 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 | |||
88 | class Calendar: | 103 | class 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 | ||
117 | class Assistant: | 132 | class 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 @@ | |||
21 | from . import eventbrite | 21 | from . import eventbrite |
22 | from . import marriott | 22 | from . import marriott |
23 | from . import united | 23 | from . import united |
24 | from . import delta | ||
24 | 25 | ||
25 | plugins = [ | 26 | plugins = [ |
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 | |||
21 | import re | ||
22 | import logging | ||
23 | import hashlib | ||
24 | |||
25 | from bs4 import BeautifulSoup | ||
26 | import dateutil.parser | ||
27 | import dateutil.tz | ||
28 | import inscriptis | ||
29 | import vobject | ||
30 | |||
31 | from email_assistant import iata | ||
32 | from email_assistant import plugin | ||
33 | |||
34 | def 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 | |||
43 | class 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 |