1
|
|
|
import hashlib |
2
|
|
|
import base64 |
3
|
|
|
|
4
|
|
|
import six |
5
|
|
|
import eventlet |
6
|
|
|
import easyimap |
7
|
|
|
from flanker import mime |
8
|
|
|
|
9
|
|
|
from st2reactor.sensor.base import PollingSensor |
10
|
|
|
|
11
|
|
|
__all__ = [ |
12
|
|
|
'IMAPSensor' |
13
|
|
|
] |
14
|
|
|
|
15
|
|
|
eventlet.monkey_patch( |
16
|
|
|
os=True, |
17
|
|
|
select=True, |
18
|
|
|
socket=True, |
19
|
|
|
thread=True, |
20
|
|
|
time=True) |
21
|
|
|
|
22
|
|
|
DEFAULT_DOWNLOAD_ATTACHMENTS = False |
23
|
|
|
DEFAULT_MAX_ATTACHMENT_SIZE = 1024 |
24
|
|
|
DEFAULT_ATTACHMENT_DATASTORE_TTL = 1800 |
25
|
|
|
|
26
|
|
|
|
27
|
|
|
class IMAPSensor(PollingSensor): |
28
|
|
|
def __init__(self, sensor_service, config=None, poll_interval=30): |
29
|
|
|
super(IMAPSensor, self).__init__(sensor_service=sensor_service, |
30
|
|
|
config=config, |
31
|
|
|
poll_interval=poll_interval) |
32
|
|
|
|
33
|
|
|
self._trigger = 'email.imap.message' |
34
|
|
|
self._logger = self._sensor_service.get_logger(__name__) |
35
|
|
|
|
36
|
|
|
self._max_attachment_size = self._config.get('max_attachment_size', |
37
|
|
|
DEFAULT_MAX_ATTACHMENT_SIZE) |
38
|
|
|
self._attachment_datastore_ttl = self._config.get('attachment_datastore_ttl', |
39
|
|
|
DEFAULT_MAX_ATTACHMENT_SIZE) |
40
|
|
|
self._mailboxes = {} |
41
|
|
|
|
42
|
|
|
def setup(self): |
43
|
|
|
self._logger.debug('[IMAPSensor]: entering setup') |
44
|
|
|
|
45
|
|
|
def poll(self): |
46
|
|
|
self._logger.debug('[IMAPSensor]: entering poll') |
47
|
|
|
|
48
|
|
|
if 'imap_mailboxes' in self._config: |
49
|
|
|
self._parse_mailboxes(self._config['imap_mailboxes']) |
50
|
|
|
|
51
|
|
|
for name, values in self._mailboxes.items(): |
52
|
|
|
mailbox = values['connection'] |
53
|
|
|
download_attachments = values['download_attachments'] |
54
|
|
|
mailbox_metadata = values['mailbox_metadata'] |
55
|
|
|
|
56
|
|
|
self._poll_for_unread_messages(name=name, mailbox=mailbox, |
57
|
|
|
download_attachments=download_attachments, |
58
|
|
|
mailbox_metadata=mailbox_metadata) |
59
|
|
|
mailbox.quit() |
60
|
|
|
|
61
|
|
|
def cleanup(self): |
62
|
|
|
self._logger.debug('[IMAPSensor]: entering cleanup') |
63
|
|
|
|
64
|
|
|
for name, values in self._mailboxes.items(): |
65
|
|
|
mailbox = values['connection'] |
66
|
|
|
self._logger.debug('[IMAPSensor]: Disconnecting from {0}'.format(name)) |
67
|
|
|
mailbox.quit() |
68
|
|
|
|
69
|
|
|
def add_trigger(self, trigger): |
70
|
|
|
pass |
71
|
|
|
|
72
|
|
|
def update_trigger(self, trigger): |
73
|
|
|
pass |
74
|
|
|
|
75
|
|
|
def remove_trigger(self, trigger): |
76
|
|
|
pass |
77
|
|
|
|
78
|
|
|
def _parse_mailboxes(self, mailboxes): |
79
|
|
|
for mailbox, config in mailboxes.items(): |
80
|
|
|
server = config.get('server', 'localhost') |
81
|
|
|
port = config.get('port', 143) |
82
|
|
|
user = config.get('username', None) |
83
|
|
|
password = config.get('password', None) |
84
|
|
|
folder = config.get('mailbox', 'INBOX') |
85
|
|
|
ssl = config.get('ssl', False) |
86
|
|
|
download_attachments = config.get('download_attachments', DEFAULT_DOWNLOAD_ATTACHMENTS) |
87
|
|
|
|
88
|
|
|
if not user or not password: |
89
|
|
|
self._logger.debug("""[IMAPSensor]: Missing |
90
|
|
|
username/password for {0}""".format(mailbox)) |
91
|
|
|
continue |
92
|
|
|
|
93
|
|
|
if not server: |
94
|
|
|
self._logger.debug("""[IMAPSensor]: Missing server |
95
|
|
|
for {0}""".format(mailbox)) |
96
|
|
|
continue |
97
|
|
|
|
98
|
|
|
try: |
99
|
|
|
connection = easyimap.connect(server, user, password, |
100
|
|
|
folder, ssl=ssl, port=port) |
101
|
|
|
except Exception as e: |
102
|
|
|
message = 'Failed to connect to mailbox "%s": %s' % (mailbox, str(e)) |
103
|
|
|
raise Exception(message) |
104
|
|
|
|
105
|
|
|
item = { |
106
|
|
|
'connection': connection, |
107
|
|
|
'download_attachments': download_attachments, |
108
|
|
|
'mailbox_metadata': { |
109
|
|
|
'server': server, |
110
|
|
|
'port': port, |
111
|
|
|
'user': user, |
112
|
|
|
'folder': folder, |
113
|
|
|
'ssl': ssl |
114
|
|
|
} |
115
|
|
|
} |
116
|
|
|
self._mailboxes[mailbox] = item |
117
|
|
|
|
118
|
|
|
def _poll_for_unread_messages(self, name, mailbox, mailbox_metadata, |
119
|
|
|
download_attachments=False): |
120
|
|
|
self._logger.debug('[IMAPSensor]: polling mailbox {0}'.format(name)) |
121
|
|
|
|
122
|
|
|
messages = mailbox.unseen() |
123
|
|
|
|
124
|
|
|
self._logger.debug('[IMAPSensor]: Processing {0} new messages'.format(len(messages))) |
125
|
|
|
for message in messages: |
126
|
|
|
self._process_message(uid=message.uid, mailbox=mailbox, |
127
|
|
|
download_attachments=download_attachments, |
128
|
|
|
mailbox_metadata=mailbox_metadata) |
129
|
|
|
|
130
|
|
|
def _process_message(self, uid, mailbox, mailbox_metadata, |
131
|
|
|
download_attachments=DEFAULT_DOWNLOAD_ATTACHMENTS): |
132
|
|
|
message = mailbox.mail(uid, include_raw=True) |
133
|
|
|
mime_msg = mime.from_string(message.raw) |
134
|
|
|
|
135
|
|
|
body = message.body |
136
|
|
|
sent_from = message.from_addr |
137
|
|
|
sent_to = message.to |
138
|
|
|
subject = message.title |
139
|
|
|
date = message.date |
140
|
|
|
message_id = message.message_id |
141
|
|
|
headers = mime_msg.headers.items() |
142
|
|
|
has_attachments = bool(message.attachments) |
143
|
|
|
|
144
|
|
|
# Flatten the headers so they can be unpickled |
145
|
|
|
headers = self._flattern_headers(headers=headers) |
146
|
|
|
|
147
|
|
|
payload = { |
148
|
|
|
'uid': uid, |
149
|
|
|
'from': sent_from, |
150
|
|
|
'to': sent_to, |
151
|
|
|
'headers': headers, |
152
|
|
|
'date': date, |
153
|
|
|
'subject': subject, |
154
|
|
|
'message_id': message_id, |
155
|
|
|
'body': body, |
156
|
|
|
'has_attachments': has_attachments, |
157
|
|
|
'attachments': [], |
158
|
|
|
'mailbox_metadata': mailbox_metadata |
159
|
|
|
} |
160
|
|
|
|
161
|
|
|
if has_attachments and download_attachments: |
162
|
|
|
self._logger.debug('[IMAPSensor]: Downloading attachments for message {}'.format(uid)) |
163
|
|
|
result = self._download_and_store_message_attachments(message=message) |
164
|
|
|
payload['attachments'] = result |
165
|
|
|
|
166
|
|
|
self._sensor_service.dispatch(trigger=self._trigger, payload=payload) |
167
|
|
|
|
168
|
|
|
def _download_and_store_message_attachments(self, message): |
169
|
|
|
""" |
170
|
|
|
Method which downloads the provided message attachments and stores them in a datasatore. |
171
|
|
|
|
172
|
|
|
:rtype: ``list`` of ``dict`` |
173
|
|
|
""" |
174
|
|
|
attachments = message.attachments |
175
|
|
|
|
176
|
|
|
result = [] |
177
|
|
|
for (file_name, content, content_type) in attachments: |
178
|
|
|
attachment_size = len(content) |
179
|
|
|
|
180
|
|
|
if len(content) > self._max_attachment_size: |
181
|
|
|
self._logger.debug(('[IMAPSensor]: Skipping attachment "{}" since its bigger ' |
182
|
|
|
'than maximum allowed size ({})'.format(file_name, |
183
|
|
|
attachment_size))) |
184
|
|
|
continue |
185
|
|
|
|
186
|
|
|
datastore_key = self._get_attachment_datastore_key(message=message, |
187
|
|
|
file_name=file_name) |
188
|
|
|
|
189
|
|
|
# Store attachment in the datastore |
190
|
|
|
if content_type == 'text/plain': |
191
|
|
|
value = content |
192
|
|
|
else: |
193
|
|
|
value = base64.b64encode(content) |
194
|
|
|
|
195
|
|
|
self._sensor_service.set_value(name=datastore_key, value=value, |
196
|
|
|
ttl=self._attachment_datastore_ttl, |
197
|
|
|
local=False) |
198
|
|
|
item = { |
199
|
|
|
'file_name': file_name, |
200
|
|
|
'content_type': content_type, |
201
|
|
|
'datastore_key': datastore_key |
202
|
|
|
} |
203
|
|
|
result.append(item) |
204
|
|
|
|
205
|
|
|
return result |
206
|
|
|
|
207
|
|
|
def _get_attachment_datastore_key(self, message, file_name): |
208
|
|
|
key = '%s-%s' % (message.uid, file_name) |
209
|
|
|
key = 'attachments-%s' % (hashlib.md5(key).hexdigest()) |
210
|
|
|
return key |
211
|
|
|
|
212
|
|
|
def _flattern_headers(self, headers): |
213
|
|
|
# Flattern headers and make sure they only contain simple types so they |
214
|
|
|
# can be serialized in a trigger |
215
|
|
|
result = [] |
216
|
|
|
|
217
|
|
|
for pair in headers: |
218
|
|
|
name = pair[0] |
219
|
|
|
value = pair[1] |
220
|
|
|
|
221
|
|
|
if not isinstance(value, six.string_types): |
222
|
|
|
value = str(value) |
223
|
|
|
|
224
|
|
|
result.append([name, value]) |
225
|
|
|
|
226
|
|
|
return result |
227
|
|
|
|