From 66839f191708a0725c8d9841d5266ad13af8ee81 Mon Sep 17 00:00:00 2001 From: Duda Dornelles Date: Mon, 22 Dec 2014 09:35:01 -0200 Subject: refactoring package structure --- service/pixelated/adapter/model/__init__.py | 15 ++ service/pixelated/adapter/model/mail.py | 361 ++++++++++++++++++++++++++++ service/pixelated/adapter/model/status.py | 42 ++++ service/pixelated/adapter/model/tag.py | 73 ++++++ 4 files changed, 491 insertions(+) create mode 100644 service/pixelated/adapter/model/__init__.py create mode 100644 service/pixelated/adapter/model/mail.py create mode 100644 service/pixelated/adapter/model/status.py create mode 100644 service/pixelated/adapter/model/tag.py (limited to 'service/pixelated/adapter/model') 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 . 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 . +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 . + + +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 . + +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 -- cgit v1.2.3