summaryrefslogtreecommitdiff
path: root/service/pixelated/adapter/model/mail.py
diff options
context:
space:
mode:
Diffstat (limited to 'service/pixelated/adapter/model/mail.py')
-rw-r--r--service/pixelated/adapter/model/mail.py378
1 files changed, 69 insertions, 309 deletions
diff --git a/service/pixelated/adapter/model/mail.py b/service/pixelated/adapter/model/mail.py
index 464e0343..b89e511a 100644
--- a/service/pixelated/adapter/model/mail.py
+++ b/service/pixelated/adapter/model/mail.py
@@ -21,21 +21,38 @@ import dateutil.parser as dateparser
from uuid import uuid4
from email import message_from_file
from email.mime.text import MIMEText
-from email.header import decode_header
+from email.header import decode_header, Header
from email.MIMEMultipart import MIMEMultipart
from pycryptopp.hash import sha256
-from leap.mail.imap.fields import fields
+from leap.mail.adaptors import soledad_indexes as fields
import leap.mail.walk as walk
from pixelated.adapter.model.status import Status
from pixelated.support import date
from pixelated.support.functional import compact
+from twisted.internet import defer
+
logger = logging.getLogger(__name__)
+TYPE_KEY = 'type'
+CONTENT_HASH_KEY = 'chash'
+HEADERS_KEY = 'headers'
+DATE_KEY = 'date'
+SUBJECT_KEY = 'subject'
+PARTS_MAP_KEY = 'part_map'
+BODY_KEY = 'body'
+MSGID_KEY = 'msgid'
+MULTIPART_KEY = 'multi'
+SIZE_KEY = 'size'
+
class Mail(object):
@property
+ def from_sender(self):
+ return self.headers['From']
+
+ @property
def to(self):
return self.headers['To']
@@ -48,6 +65,10 @@ class Mail(object):
return self.headers['Bcc']
@property
+ def subject(self):
+ return self.headers['Subject']
+
+ @property
def date(self):
return self.headers['Date']
@@ -61,7 +82,17 @@ class Mail(object):
@property
def mailbox_name(self):
- return self.fdoc.content.get('mbox')
+ # 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):
+ return [self._encode_header_value(v) for v in header_value_list]
+
+ def _encode_header_value(self, header_value):
+ if isinstance(header_value, unicode):
+ return str(Header(header_value, 'utf-8'))
+ else:
+ return str(header_value)
@property
def _mime_multipart(self):
@@ -69,7 +100,10 @@ class Mail(object):
return self._mime
mime = MIMEMultipart()
for key, value in self.headers.items():
- mime[str(key)] = str(value)
+ if isinstance(value, list):
+ mime[str(key)] = ', '.join(self._encode_header_value_list(value))
+ else:
+ mime[str(key)] = self._encode_header_value(value)
try:
body_to_use = self.body
@@ -128,19 +162,19 @@ class InputMail(Mail):
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)
+ fd[fields.MBOX] = mailbox
+ fd[fields.MBOX_UUID] = next_uid
+ fd[fields.CONTENT_HASH] = self._get_chash()
+ fd[SIZE_KEY] = len(self.raw)
+ fd[MULTIPART_KEY] = True
+ fd[fields.RECENT] = True
+ fd[fields.TYPE] = fields.FLAGS
+ fd[fields.FLAGS] = 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))
+ return walk.get_body_phash(self._mime_multipart)
def _hdoc(self):
if self._hd:
@@ -151,15 +185,15 @@ class InputMail(Mail):
headers['From'] = InputMail.FROM_EMAIL_ADDRESS
hd = {}
- hd[fields.HEADERS_KEY] = headers
- hd[fields.DATE_KEY] = headers['Date']
- hd[fields.CONTENT_HASH_KEY] = self._get_chash()
- hd[fields.MSGID_KEY] = ''
- hd[fields.MULTIPART_KEY] = True
- hd[fields.SUBJECT_KEY] = headers.get('Subject')
- hd[fields.TYPE_KEY] = fields.TYPE_HEADERS_VAL
- hd[fields.BODY_KEY] = self._get_body_phash()
- hd[fields.PARTS_MAP_KEY] = \
+ hd[HEADERS_KEY] = headers
+ hd[DATE_KEY] = headers['Date']
+ hd[CONTENT_HASH_KEY] = self._get_chash()
+ hd[MSGID_KEY] = ''
+ hd[MULTIPART_KEY] = True
+ hd[SUBJECT_KEY] = headers.get('Subject')
+ hd[TYPE_KEY] = fields.HEADERS
+ hd[BODY_KEY] = self._get_body_phash()
+ hd[PARTS_MAP_KEY] = \
walk.walk_msg_tree(walk.get_parts(self._mime_multipart), body_phash=self._get_body_phash())['part_map']
self._hd = hd
@@ -172,12 +206,15 @@ class InputMail(Mail):
mime_multipart = MIMEMultipart()
for header in ['To', 'Cc', 'Bcc']:
- if self.headers[header]:
+ if self.headers.get(header):
mime_multipart[header] = ", ".join(self.headers[header])
- if self.headers['Subject']:
+ if self.headers.get('Subject'):
mime_multipart['Subject'] = self.headers['Subject']
+ if self.headers.get('From'):
+ mime_multipart['From'] = self.headers['From']
+
mime_multipart['Date'] = self.headers['Date']
if type(self.body) is list:
for part in self.body:
@@ -207,13 +244,10 @@ class InputMail(Mail):
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'] = date.iso_now()
+ input_mail.headers['Date'] = date.mail_date_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', []))
@@ -222,292 +256,18 @@ class InputMail(Mail):
@staticmethod
def from_python_mail(mail):
input_mail = InputMail()
- input_mail.headers = {key.capitalize(): value for key, value in mail.items()}
- input_mail.headers['Date'] = date.iso_now()
- input_mail.headers['Subject'] = mail['Subject']
- input_mail.headers['To'] = InputMail.FROM_EMAIL_ADDRESS
- input_mail._mime = MIMEMultipart()
+ 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.attach(payload)
+ input_mail._mime_multipart.attach(payload)
if payload.get_content_type() == 'text/plain':
- input_mail.body = payload.as_string()
+ input_mail.body = unicode(payload.as_string())
+ input_mail._mime = input_mail.to_mime_multipart()
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
-
- def _decode_part(self, part):
- encoding = part['headers'].get('Content-Transfer-Encoding', '')
- content_type = self._parse_charset_header(part['headers'].get('Content-Type'))
-
- try:
- decoding_func = self._decoding_function_for_encoding(encoding)
- return self._decode_content_with_fallback(part['content'], decoding_func, content_type)
- except Exception:
- logger.error('Failed to decode mail part with:')
- logger.error('Content-Transfer-Encoding: %s' % encoding)
- logger.error('Content-Type: %s' % part['headers'].get('Content-Type'))
- raise
-
- def _decoding_function_for_encoding(self, encoding):
- decoding_map = {
- 'quoted-printable': lambda content, content_type: content.decode('quopri').decode(content_type),
- 'base64': lambda content, content_type: content.decode('base64').decode('utf-8'),
- '7bit': lambda content, content_type: content.encode(content_type),
- '8bit': lambda content, content_type: content.encode(content_type)
- }
- if encoding in decoding_map:
- return decoding_map[encoding]
- else:
- return decoding_map['8bit']
-
- def _decode_content_with_fallback(self, content, decode_func, content_type):
- try:
- return decode_func(content, content_type)
- # return content.encode(content_type)
- except ValueError:
- return content.encode('ascii', 'ignore')
-
- @property
- def alternatives(self):
- return self.parts.get('alternatives')
-
- @property
- def text_plain_body(self):
- if self.parts and len(self.alternatives) >= 1:
- return self._decode_part(self.alternatives[0])
- else:
- return self.bdoc.content['raw'] # plain
-
- @property
- def html_body(self):
- if self.parts and len(self.alternatives) > 1:
- html_parts = [e for e in self.alternatives if re.match('text/html', e['headers'].get('Content-Type', ''))]
- if len(html_parts):
- return self._decode_part(html_parts[0])
-
- @property
- def headers(self):
- _headers = {
- 'To': [],
- 'Cc': [],
- 'Bcc': []
- }
- hdoc_headers = self.hdoc.content['headers']
-
- for header in ['To', 'Cc', 'Bcc']:
- header_value = self._decode_header(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] = [head.strip() for head in compact(_headers[header])]
-
- for header in ['From', 'Subject']:
- _headers[header] = self._decode_header(hdoc_headers.get(header))
-
- try:
- _headers['Date'] = self._get_date()
- except Exception:
- _headers['Date'] = date.iso_now()
-
- 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 _decode_header_with_fallback(self, entry):
- try:
- return decode_header(entry)[0][0]
- except Exception:
- return entry.encode('ascii', 'ignore')
-
- def _decode_header(self, header):
- if not header:
- return None
- if isinstance(header, list):
- return [self._decode_header_with_fallback(entry) for entry in header]
- else:
- return self._decode_header_with_fallback(header)
-
- def _get_date(self):
- date = self.hdoc.content.get('date', None)
- try:
- if not date:
- received = self.hdoc.content.get('received', None)
- if received:
- date = received.split(";")[-1].strip()
- else:
- # we can't get a date for this mail, so lets just use now
- logger.warning('Encountered a mail with missing date and received header fields. ID %s' % self.fdoc.content.get('uid', None))
- date = date.iso_now()
- return dateparser.parse(date).isoformat()
- except (ValueError, TypeError):
- date = date.iso_now()
- return dateparser.parse(date).isoformat()
-
- @property
- def security_casing(self):
- casing = {"imprints": [], "locks": []}
- casing["imprints"] = self.signature_information
- if self.encrypted == "true":
- casing["locks"] = [{"state": "valid"}]
- elif self.encrypted == "fail":
- casing["locks"] = [{"state": "failure"}]
- 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']
-
- @property
- def flags(self):
- return self.fdoc.content['flags']
-
- 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.flags:
- return self
- self.flags.append(Status.SEEN)
- self.save()
- return self
-
- def mark_as_unread(self):
- if Status.SEEN in self.flags:
- self.flags.remove(Status.SEEN)
- self.save()
- return self
-
- def mark_as_not_recent(self):
- if Status.RECENT in self.flags:
- self.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 signature_information(self):
- signature = self.hdoc.content["headers"].get("X-Leap-Signature", None)
- if signature is None or signature.startswith("could not verify"):
- return [{"state": "no_signature_information"}]
- else:
- if signature.startswith("valid"):
- return [{"state": "valid", "seal": {"validity": "valid"}}]
- else:
- return []
-
- @property
- def encrypted(self):
- return self.hdoc.content["headers"].get("X-Pixelated-encryption-status", "false")
-
- @property
- def bounced(self):
- content_type = self.hdoc.content["headers"].get("Content-Type", '')
- if re.compile('delivery-status').search(content_type):
- bounce_recipient = self._extract_bounced_address(self.hdoc.content)
- bounce_daemon = self.headers["From"]
- return [bounce_recipient, bounce_daemon] if bounce_recipient else False
-
- return False
-
- def _extract_bounced_address(self, part):
- part_header = dict(part.get('headers', {}))
- if 'Final-Recipient' in part_header:
- if self._bounce_permanent(part_header):
- return part_header['Final-Recipient'].split(';')[1].strip()
- else:
- return False
- elif 'part_map' in part:
- for subpart in part['part_map'].values():
- result = self._extract_bounced_address(subpart)
- if result:
- return result
- else:
- continue
- return False
-
- def _bounce_permanent(self, part_headers):
- status = part_headers.get('Status', '')
- return status.startswith('5')
-
- 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,
- 'textPlainBody': self.text_plain_body,
- 'htmlBody': self.html_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.get('From'))
- # Issue #215: Fix for existing mails without any from address.
- if sender_mail is None:
- sender_mail = InputMail.FROM_EMAIL_ADDRESS
-
- 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
-
-
def welcome_mail():
current_path = os.path.dirname(os.path.abspath(__file__))
with open(os.path.join(current_path, '..', '..', 'assets', 'welcome.mail')) as mail_template_file: