summaryrefslogtreecommitdiff
path: root/service/src/pixelated/adapter/model
diff options
context:
space:
mode:
Diffstat (limited to 'service/src/pixelated/adapter/model')
-rw-r--r--service/src/pixelated/adapter/model/__init__.py15
-rw-r--r--service/src/pixelated/adapter/model/mail.py224
-rw-r--r--service/src/pixelated/adapter/model/status.py42
-rw-r--r--service/src/pixelated/adapter/model/tag.py73
4 files changed, 354 insertions, 0 deletions
diff --git a/service/src/pixelated/adapter/model/__init__.py b/service/src/pixelated/adapter/model/__init__.py
new file mode 100644
index 00000000..2756a319
--- /dev/null
+++ b/service/src/pixelated/adapter/model/__init__.py
@@ -0,0 +1,15 @@
+#
+# Copyright (c) 2014 ThoughtWorks, Inc.
+#
+# Pixelated 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.
+#
+# Pixelated 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 Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with Pixelated. If not, see <http://www.gnu.org/licenses/>.
diff --git a/service/src/pixelated/adapter/model/mail.py b/service/src/pixelated/adapter/model/mail.py
new file mode 100644
index 00000000..b6a8beb0
--- /dev/null
+++ b/service/src/pixelated/adapter/model/mail.py
@@ -0,0 +1,224 @@
+#
+# Copyright (c) 2014 ThoughtWorks, Inc.
+#
+# Pixelated 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.
+#
+# Pixelated 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 PCULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with Pixelated. If not, see <http://www.gnu.org/licenses/>.
+import re
+import binascii
+from email.mime.text import MIMEText
+from email.header import Header
+from hashlib import sha256
+from email.MIMEMultipart import MIMEMultipart
+from email.mime.nonmultipart import MIMENonMultipart
+
+from twisted.logger import Logger
+
+from leap.bitmask.mail import walk
+
+from pixelated.adapter.model.status import Status
+from pixelated.support import date
+
+
+logger = Logger()
+
+
+class Mail(object):
+ @property
+ def from_sender(self):
+ return self.headers['From']
+
+ @property
+ def to(self):
+ return self.headers['To']
+
+ @property
+ def cc(self):
+ return self.headers['Cc']
+
+ @property
+ def bcc(self):
+ return self.headers['Bcc']
+
+ @property
+ def subject(self):
+ return self.headers['Subject']
+
+ @property
+ def date(self):
+ return self.headers['Date']
+
+ @property
+ def status(self):
+ return Status.from_flags(self.flags)
+
+ @property
+ def flags(self):
+ return self.fdoc.content.get('flags')
+
+ @property
+ def mailbox_name(self):
+ # FIXME mbox is no longer available, instead we now have mbox_uuid
+ return self.fdoc.content.get('mbox', 'INBOX')
+
+ def _encode_header_value_list(self, header_value_list):
+ encoded_header_list = [self._encode_header_value(v) for v in header_value_list]
+ return ', '.join(encoded_header_list)
+
+ def _encode_header_value(self, header_value):
+ if isinstance(header_value, unicode):
+ return str(Header(header_value, 'utf-8'))
+ return str(header_value)
+
+ def _add_message_content(self, mime_multipart, body_to_use=None):
+ body_to_use = body_to_use or self.body
+ if isinstance(body_to_use, list):
+ for part in body_to_use:
+ mime_multipart.attach(MIMEText(part['raw'], part['content-type']))
+ else:
+ mime_multipart.attach(MIMEText(body_to_use, 'plain', self._charset()))
+
+ def _add_body(self, mime):
+ body_to_use = getattr(self, 'body', None) or getattr(self, 'text_plain_body', None)
+ self._add_message_content(mime, body_to_use)
+ self._add_attachments(mime)
+
+ def _generate_mime_multipart(self):
+ mime = MIMEMultipart()
+ self._add_headers(mime)
+ self._add_body(mime)
+ return mime
+
+ @property
+ def _mime_multipart(self):
+ self._mime = self._mime or self._generate_mime_multipart()
+ return self._mime
+
+ def _add_headers(self, mime):
+ for key, value in self.headers.items():
+ if isinstance(value, list):
+ mime[str(key)] = self._encode_header_value_list(value)
+ else:
+ mime[str(key)] = self._encode_header_value(value)
+
+ def _add_attachments(self, mime):
+ for attachment in getattr(self, '_attachments', []):
+ major, sub = attachment['content-type'].split('/')
+ attachment_mime = MIMENonMultipart(major, sub)
+ base64_attachment_file = binascii.b2a_base64(attachment['raw'])
+ attachment_mime.set_payload(base64_attachment_file)
+ attachment_mime['Content-Disposition'] = 'attachment; filename="%s"' % attachment['name']
+ attachment_mime['Content-Transfer-Encoding'] = 'base64'
+ mime.attach(attachment_mime)
+
+ def _charset(self):
+ content_type = self.headers.get('content_type', {})
+ if 'charset' in content_type:
+ return self._parse_charset_header(content_type)
+ return 'utf-8'
+
+ def _parse_charset_header(self, charset_header, default_charset='utf-8'):
+ try:
+ return re.compile('.*charset=([a-zA-Z0-9-]+)', re.MULTILINE | re.DOTALL).match(charset_header).group(1)
+ except:
+ return default_charset
+
+ @property
+ def raw(self):
+ return self._mime_multipart.as_string()
+
+ def _get_chash(self):
+ return sha256(self.raw).hexdigest()
+
+
+class InputMail(Mail):
+ def __init__(self):
+ self._raw_message = None
+ self._fd = None
+ self._hd = None
+ self._bd = None
+ self._chash = None
+ self._mime = None
+ self.headers = {}
+ self.body = ''
+ self._status = []
+ self._attachments = []
+
+ @property
+ def ident(self):
+ return self._get_chash()
+
+ def _get_body_phash(self):
+ return walk.get_body_phash(self._mime_multipart)
+
+ def _add_predefined_headers(self, mime_multipart):
+ for header in ['To', 'Cc', 'Bcc']:
+ if self.headers.get(header):
+ mime_multipart[header] = ", ".join(self.headers[header])
+ for header in ['Subject', 'From']:
+ if self.headers.get(header):
+ mime_multipart[header] = self.headers[header]
+ mime_multipart['Date'] = self.headers['Date']
+
+ def to_mime_multipart(self):
+ mime = MIMEMultipart()
+ self._add_predefined_headers(mime)
+ self._add_body(mime)
+ return mime
+
+ def to_smtp_format(self):
+ mime_multipart = self.to_mime_multipart()
+ return mime_multipart.as_string()
+
+ @staticmethod
+ def delivery_error_template(delivery_address):
+ return InputMail.from_dict({
+ 'body': "Mail undelivered for %s" % delivery_address,
+ 'header': {
+ 'bcc': [],
+ 'cc': [],
+ 'subject': "Mail undelivered for %s" % delivery_address
+ }
+ })
+
+ @staticmethod
+ def from_dict(mail_dict, from_address):
+ input_mail = InputMail()
+ input_mail.headers = {key.capitalize(): value for key, value in mail_dict.get('header', {}).items()}
+
+ input_mail.headers['Date'] = date.mail_date_now()
+ input_mail.headers['From'] = from_address
+
+ input_mail.body = mail_dict.get('body', '')
+ input_mail.tags = set(mail_dict.get('tags', []))
+ input_mail._status = set(mail_dict.get('status', []))
+ input_mail._attachments = mail_dict.get('attachments', [])
+ return input_mail
+
+ @staticmethod
+ def from_python_mail(mail):
+ input_mail = InputMail()
+ input_mail.headers = {unicode(key.capitalize()): unicode(value) for key, value in mail.items()}
+ input_mail.headers[u'Date'] = unicode(date.mail_date_now())
+ input_mail.headers[u'To'] = [u'']
+
+ for payload in mail.get_payload():
+ input_mail._mime_multipart.attach(payload)
+ if payload.get_content_type() == 'text/plain':
+ content_charset = payload.get_content_charset()
+ try:
+ input_mail.body = unicode(
+ payload.get_payload(decode=True), content_charset)
+ except TypeError:
+ input_mail.body = unicode(payload.get_payload(decode=True))
+ input_mail._mime = input_mail.to_mime_multipart()
+ return input_mail
diff --git a/service/src/pixelated/adapter/model/status.py b/service/src/pixelated/adapter/model/status.py
new file mode 100644
index 00000000..5a11ee7b
--- /dev/null
+++ b/service/src/pixelated/adapter/model/status.py
@@ -0,0 +1,42 @@
+#
+# Copyright (c) 2014 ThoughtWorks, Inc.
+#
+# Pixelated 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.
+#
+# Pixelated 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 Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with Pixelated. If not, see <http://www.gnu.org/licenses/>.
+
+
+class Status:
+
+ SEEN = u'\\Seen'
+ ANSWERED = u'\\Answered'
+ DELETED = u'\\Deleted'
+ RECENT = u'\\Recent'
+
+ FLAGS_TO_STATUSES = {
+ SEEN: 'read',
+ ANSWERED: 'replied',
+ RECENT: 'recent'
+ }
+
+ @staticmethod
+ def from_flag(flag):
+ return Status.FLAGS_TO_STATUSES[flag]
+
+ @staticmethod
+ def from_flags(flags):
+ return set(Status.from_flag(flag) for flag in flags if flag in Status.FLAGS_TO_STATUSES.keys())
+
+ @staticmethod
+ def to_flags(statuses):
+ statuses_to_flags = dict(zip(Status.FLAGS_TO_STATUSES.values(), Status.FLAGS_TO_STATUSES.keys()))
+ return [statuses_to_flags[status] for status in statuses]
diff --git a/service/src/pixelated/adapter/model/tag.py b/service/src/pixelated/adapter/model/tag.py
new file mode 100644
index 00000000..ca62a1fe
--- /dev/null
+++ b/service/src/pixelated/adapter/model/tag.py
@@ -0,0 +1,73 @@
+#
+# Copyright (c) 2014 ThoughtWorks, Inc.
+#
+# Pixelated 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.
+#
+# Pixelated 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 Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with Pixelated. If not, see <http://www.gnu.org/licenses/>.
+
+import json
+
+
+class Tag(object):
+
+ @classmethod
+ def from_dict(cls, tag_dict):
+ tag = Tag(tag_dict['name'], tag_dict['default'])
+ tag.mails = set(tag_dict['mails'])
+ return tag
+
+ @classmethod
+ def from_json_string(cls, json_string):
+ tag_dict = json.loads(json_string)
+ tag_dict['mails'] = set(tag_dict['mails'])
+ return Tag.from_dict(tag_dict)
+
+ @property
+ def total(self):
+ return len(self.mails)
+
+ def __init__(self, name, default=False):
+ self.name = name.lower()
+ self.ident = self.name.__hash__()
+ self.default = default
+ self.mails = set()
+
+ def __eq__(self, other):
+ return self.name == other.name
+
+ def __hash__(self):
+ return self.name.__hash__()
+
+ def increment(self, mail_ident):
+ self.mails.add(mail_ident)
+
+ def decrement(self, mail_ident):
+ self.mails.discard(mail_ident)
+
+ def as_dict(self):
+ return {
+ 'name': self.name,
+ 'default': self.default,
+ 'ident': self.ident,
+ 'counts': {'total': self.total,
+ 'read': 0,
+ 'starred': 0,
+ 'replied': 0},
+ 'mails': list(self.mails)
+ }
+
+ def as_json_string(self):
+ tag_dict = self.as_dict()
+ return json.dumps(tag_dict)
+
+ def __repr__(self):
+ return self.name