summaryrefslogtreecommitdiff
path: root/service/pixelated/adapter/model
diff options
context:
space:
mode:
authorDuda Dornelles <ddornell@thoughtworks.com>2014-12-22 09:35:01 -0200
committerDuda Dornelles <ddornell@thoughtworks.com>2014-12-22 09:38:08 -0200
commit66839f191708a0725c8d9841d5266ad13af8ee81 (patch)
tree337fb19e99625cc3243f998a2351cea123c037a2 /service/pixelated/adapter/model
parenta0cbaf84fd86fc91cba892a7a19b0ade7c017a3c (diff)
refactoring package structure
Diffstat (limited to 'service/pixelated/adapter/model')
-rw-r--r--service/pixelated/adapter/model/__init__.py15
-rw-r--r--service/pixelated/adapter/model/mail.py361
-rw-r--r--service/pixelated/adapter/model/status.py42
-rw-r--r--service/pixelated/adapter/model/tag.py73
4 files changed, 491 insertions, 0 deletions
diff --git a/service/pixelated/adapter/model/__init__.py b/service/pixelated/adapter/model/__init__.py
new file mode 100644
index 00000000..2756a319
--- /dev/null
+++ b/service/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/pixelated/adapter/model/mail.py b/service/pixelated/adapter/model/mail.py
new file mode 100644
index 00000000..0beb2111
--- /dev/null
+++ b/service/pixelated/adapter/model/mail.py
@@ -0,0 +1,361 @@
+#
+# 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 json
+from uuid import uuid4
+from email.mime.text import MIMEText
+
+from leap.mail.imap.fields import fields
+import leap.mail.walk as walk
+import dateutil.parser as dateparser
+from pixelated.adapter.model.status import Status
+import pixelated.support.date
+from email.MIMEMultipart import MIMEMultipart
+from pycryptopp.hash import sha256
+import re
+import base64
+
+
+class Mail(object):
+ @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 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):
+ return self.fdoc.content.get('mbox')
+
+ @property
+ def _mime_multipart(self):
+ if self._mime:
+ return self._mime
+ mime = MIMEMultipart()
+ for key, value in self.headers.items():
+ mime[str(key)] = str(value)
+ mime.attach(MIMEText(self.body, 'plain', self._charset()))
+ self._mime = mime
+ return mime
+
+ def _charset(self):
+ if 'content_type' in self.headers and 'charset' in self.headers['content_type']:
+ return re.compile('.*charset=(.*)').match(self.headers['content_type']).group(1)
+ else:
+ return 'utf-8'
+
+ @property
+ def raw(self):
+ return self._mime_multipart.as_string()
+
+ def _get_chash(self):
+ return sha256.SHA256(self.raw).hexdigest()
+
+
+class InputMail(Mail):
+ FROM_EMAIL_ADDRESS = None
+
+ def __init__(self):
+ self._raw_message = None
+ self._fd = None
+ self._hd = None
+ self._bd = None
+ self._chash = None
+ self._mime = None
+
+ @property
+ def ident(self):
+ return self._get_chash()
+
+ def get_for_save(self, next_uid, mailbox):
+ docs = [self._fdoc(next_uid, mailbox), self._hdoc()]
+ docs.extend([m for m in self._cdocs()])
+ return docs
+
+ def _fdoc(self, next_uid, mailbox):
+ if self._fd:
+ return self._fd
+
+ fd = {}
+ fd[fields.MBOX_KEY] = mailbox
+ fd[fields.UID_KEY] = next_uid
+ fd[fields.CONTENT_HASH_KEY] = self._get_chash()
+ fd[fields.SIZE_KEY] = len(self.raw)
+ fd[fields.MULTIPART_KEY] = True
+ fd[fields.RECENT_KEY] = True
+ fd[fields.TYPE_KEY] = fields.TYPE_FLAGS_VAL
+ fd[fields.FLAGS_KEY] = Status.to_flags(self._status)
+ self._fd = fd
+ return fd
+
+ def _get_body_phash(self):
+ return walk.get_body_phash_multi(walk.get_payloads(self._mime_multipart))
+
+ def _hdoc(self):
+ if self._hd:
+ return self._hd
+
+ hd = {}
+ hd[fields.HEADERS_KEY] = self.headers
+ hd[fields.DATE_KEY] = self.headers['Date']
+ hd[fields.CONTENT_HASH_KEY] = self._get_chash()
+ hd[fields.MSGID_KEY] = ''
+ hd[fields.MULTIPART_KEY] = True
+ hd[fields.SUBJECT_KEY] = self.headers.get('Subject')
+ hd[fields.TYPE_KEY] = fields.TYPE_HEADERS_VAL
+ hd[fields.BODY_KEY] = self._get_body_phash()
+ hd[fields.PARTS_MAP_KEY] = \
+ walk.walk_msg_tree(walk.get_parts(self._mime_multipart), body_phash=self._get_body_phash())['part_map']
+
+ self._hd = hd
+ return hd
+
+ def _cdocs(self):
+ return walk.get_raw_docs(self._mime_multipart, self._mime_multipart.walk())
+
+ def to_mime_multipart(self):
+ mime_multipart = MIMEMultipart()
+
+ for header in ['To', 'Cc', 'Bcc']:
+ if self.headers[header]:
+ mime_multipart[header] = ", ".join(self.headers[header])
+
+ if self.headers['Subject']:
+ mime_multipart['Subject'] = self.headers['Subject']
+
+ mime_multipart['Date'] = self.headers['Date']
+ if type(self.body) is list:
+ for part in self.body:
+ mime_multipart.attach(MIMEText(part['raw'], part['content-type']))
+ else:
+ mime_multipart.attach(MIMEText(self.body, 'plain', 'utf-8'))
+ return mime_multipart
+
+ def to_smtp_format(self):
+ mime_multipart = self.to_mime_multipart()
+ mime_multipart['From'] = InputMail.FROM_EMAIL_ADDRESS
+ return mime_multipart.as_string()
+
+ @staticmethod
+ def from_dict(mail_dict):
+ input_mail = InputMail()
+ input_mail.headers = {key.capitalize(): value for key, value in mail_dict.get('header', {}).items()}
+
+ # XXX this is overriding the property in PixelatedMail
+ input_mail.headers['Date'] = pixelated.support.date.iso_now()
+
+ # XXX this is overriding the property in PixelatedMail
+ input_mail.body = mail_dict.get('body', '')
+
+ # XXX this is overriding the property in the PixelatedMail
+ input_mail.tags = set(mail_dict.get('tags', []))
+
+ input_mail._status = set(mail_dict.get('status', []))
+ return input_mail
+
+
+class PixelatedMail(Mail):
+ @staticmethod
+ def from_soledad(fdoc, hdoc, bdoc, parts=None, soledad_querier=None):
+ mail = PixelatedMail()
+ mail.parts = parts
+ mail.boundary = str(uuid4()).replace('-', '')
+ mail.bdoc = bdoc
+ mail.fdoc = fdoc
+ mail.hdoc = hdoc
+ mail.querier = soledad_querier
+ mail._mime = None
+ return mail
+
+ @property
+ def body(self):
+ if self.parts and len(self.parts['alternatives']) > 1:
+ body = ''
+ for alternative in self.parts['alternatives']:
+ body += '--' + self.boundary + '\n'
+ for header, value in alternative['headers'].items():
+ body += '%s: %s\n' % (header, value)
+ body += '\n'
+ body += alternative['content']
+ body += '\n'
+ body += '--' + self.boundary + '--'
+ return body
+ else:
+ if self.parts and self.parts['alternatives'][0]['headers'].get('Content-Transfer-Encoding', '') == 'base64':
+ return unicode(base64.b64decode(self.parts['alternatives'][0]['content']), 'utf-8')
+ else:
+ return self.bdoc.content['raw']
+
+ @property
+ def headers(self):
+ _headers = {
+ 'To': [],
+ 'Cc': [],
+ 'Bcc': []
+ }
+ hdoc_headers = self.hdoc.content['headers']
+
+ for header in ['To', 'Cc', 'Bcc']:
+ header_value = hdoc_headers.get(header)
+ if not header_value:
+ continue
+ _headers[header] = header_value if type(header_value) is list else header_value.split(',')
+ _headers[header] = map(lambda x: x.strip(), _headers[header])
+
+ for header in ['From', 'Subject']:
+ _headers[header] = hdoc_headers.get(header)
+
+ _headers['Date'] = self._get_date()
+
+ if self.parts and len(self.parts['alternatives']) > 1:
+ _headers['content_type'] = 'multipart/alternative; boundary="%s"' % self.boundary
+ elif self.hdoc.content['headers'].get('Content-Type'):
+ _headers['content_type'] = hdoc_headers.get('Content-Type')
+
+ if hdoc_headers.get('Reply-To'):
+ _headers['Reply-To'] = hdoc_headers.get('Reply-To')
+
+ return _headers
+
+ def _get_date(self):
+ date = self.hdoc.content.get('date', None)
+ if not date:
+ date = self.hdoc.content['received'].split(";")[-1].strip()
+ return dateparser.parse(date).isoformat()
+
+ @property
+ def security_casing(self):
+ casing = {"imprints": [], "locks": []}
+ if self.signed:
+ casing["imprints"].append({"state": "valid", "seal": {"validity": "valid"}})
+ elif self.signed is None:
+ casing["imprints"].append({"state": "no_signature_information"})
+
+ if self.encrypted:
+ casing["locks"].append({"state": "valid"})
+
+ return casing
+
+ @property
+ def tags(self):
+ _tags = self.fdoc.content.get('tags', '[]')
+ return set(_tags) if type(_tags) is list or type(_tags) is set else set(json.loads(_tags))
+
+ @property
+ def ident(self):
+ return self.fdoc.content.get('chash')
+
+ @property
+ def mailbox_name(self):
+ return self.fdoc.content.get('mbox')
+
+ @property
+ def is_recent(self):
+ return Status('recent') in self.status
+
+ @property
+ def uid(self):
+ return self.fdoc.content['uid']
+
+ def save(self):
+ return self.querier.save_mail(self)
+
+ def set_mailbox(self, mailbox_name):
+ self.fdoc.content['mbox'] = mailbox_name
+
+ def remove_all_tags(self):
+ self.update_tags(set([]))
+
+ def update_tags(self, tags):
+ self._persist_mail_tags(tags)
+ return self.tags
+
+ def mark_as_read(self):
+ if Status.SEEN in self.fdoc.content['flags']:
+ return self
+ self.fdoc.content['flags'].append(Status.SEEN)
+ self.save()
+ return self
+
+ def mark_as_unread(self):
+ if Status.SEEN in self.fdoc.content['flags']:
+ self.fdoc.content['flags'].remove(Status.SEEN)
+ self.save()
+ return self
+
+ def mark_as_not_recent(self):
+ if Status.RECENT in self.fdoc.content['flags']:
+ self.fdoc.content['flags'].remove(Status.RECENT)
+ self.save()
+ return self
+
+ def _persist_mail_tags(self, current_tags):
+ self.fdoc.content['tags'] = json.dumps(list(current_tags))
+ self.save()
+
+ def has_tag(self, tag):
+ return tag in self.tags
+
+ @property
+ def signed(self):
+ signature = self.hdoc.content["headers"].get("X-Leap-Signature", None)
+ if signature is None:
+ return None
+ else:
+ return signature.startswith("valid")
+
+ @property
+ def encrypted(self):
+ return self.hdoc.content["headers"].get("OpenPGP", None) is not None
+
+ def as_dict(self):
+ dict_mail = {'header': {k.lower(): v for k, v in self.headers.items()},
+ 'ident': self.ident,
+ 'tags': list(self.tags),
+ 'status': list(self.status),
+ 'security_casing': self.security_casing,
+ 'body': self.body,
+ 'mailbox': self.mailbox_name.lower(),
+ 'attachments': self.parts['attachments'] if self.parts else []}
+ dict_mail['replying'] = {'single': None, 'all': {'to-field': [], 'cc-field': []}}
+
+ sender_mail = self.headers.get('Reply-To', self.headers['From'])
+
+ recipients = [recipient for recipient in self.headers['To'] if recipient != InputMail.FROM_EMAIL_ADDRESS]
+ recipients.append(sender_mail)
+ ccs = [cc for cc in self.headers['Cc'] if cc != InputMail.FROM_EMAIL_ADDRESS]
+
+ dict_mail['replying']['single'] = sender_mail
+ dict_mail['replying']['all']['to-field'] = recipients
+ dict_mail['replying']['all']['cc-field'] = ccs
+ return dict_mail
diff --git a/service/pixelated/adapter/model/status.py b/service/pixelated/adapter/model/status.py
new file mode 100644
index 00000000..5a11ee7b
--- /dev/null
+++ b/service/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/pixelated/adapter/model/tag.py b/service/pixelated/adapter/model/tag.py
new file mode 100644
index 00000000..d75022f9
--- /dev/null
+++ b/service/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:
+
+ @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