Completed
Pull Request — master (#544)
by
unknown
09:52 queued 03:16
created

IMAPSensor   A

Complexity

Total Complexity 29

Size/Duplication

Total Lines 200
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
dl 0
loc 200
rs 10
c 0
b 0
f 0
wmc 29
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