summaryrefslogtreecommitdiff
path: root/service/src/pixelated/adapter/services
diff options
context:
space:
mode:
Diffstat (limited to 'service/src/pixelated/adapter/services')
-rw-r--r--service/src/pixelated/adapter/services/__init__.py15
-rw-r--r--service/src/pixelated/adapter/services/draft_service.py40
-rw-r--r--service/src/pixelated/adapter/services/feedback_service.py36
-rw-r--r--service/src/pixelated/adapter/services/mail_sender.py106
-rw-r--r--service/src/pixelated/adapter/services/mail_service.py172
-rw-r--r--service/src/pixelated/adapter/services/tag_service.py23
6 files changed, 392 insertions, 0 deletions
diff --git a/service/src/pixelated/adapter/services/__init__.py b/service/src/pixelated/adapter/services/__init__.py
new file mode 100644
index 00000000..2756a319
--- /dev/null
+++ b/service/src/pixelated/adapter/services/__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/services/draft_service.py b/service/src/pixelated/adapter/services/draft_service.py
new file mode 100644
index 00000000..504d92db
--- /dev/null
+++ b/service/src/pixelated/adapter/services/draft_service.py
@@ -0,0 +1,40 @@
+#
+# 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/>.
+from twisted.internet import defer
+
+
+class DraftService(object):
+ __slots__ = '_mail_store'
+
+ def __init__(self, mail_store):
+ self._mail_store = mail_store
+
+ @defer.inlineCallbacks
+ def create_draft(self, input_mail):
+ mail = yield self._mail_store.add_mail('DRAFTS', input_mail.raw)
+ defer.returnValue(mail)
+
+ @defer.inlineCallbacks
+ def update_draft(self, ident, input_mail):
+ removed = yield self._mail_store.delete_mail(ident)
+ if removed:
+ new_draft = yield self.create_draft(input_mail)
+ defer.returnValue(new_draft)
+
+ def process_draft(self, ident, input_mail):
+ if ident:
+ return self.update_draft(ident, input_mail)
+ return self.create_draft(input_mail)
diff --git a/service/src/pixelated/adapter/services/feedback_service.py b/service/src/pixelated/adapter/services/feedback_service.py
new file mode 100644
index 00000000..0cc595eb
--- /dev/null
+++ b/service/src/pixelated/adapter/services/feedback_service.py
@@ -0,0 +1,36 @@
+#
+# Copyright (c) 2015 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 os
+import requests
+
+
+class FeedbackService(object):
+ FEEDBACK_URL = os.environ.get('FEEDBACK_URL')
+
+ def __init__(self, leap_session):
+ self.leap_session = leap_session
+
+ def open_ticket(self, feedback):
+ account_mail = self.leap_session.account_email()
+ data = {
+ "ticket[comments_attributes][0][body]": feedback,
+ "ticket[subject]": "Feedback user-agent from {0}".format(account_mail),
+ "ticket[email]": account_mail,
+ "ticket[regarding_user]": account_mail
+ }
+
+ return requests.post(self.FEEDBACK_URL, data=data, verify=False)
diff --git a/service/src/pixelated/adapter/services/mail_sender.py b/service/src/pixelated/adapter/services/mail_sender.py
new file mode 100644
index 00000000..063ea156
--- /dev/null
+++ b/service/src/pixelated/adapter/services/mail_sender.py
@@ -0,0 +1,106 @@
+#
+# 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/>.
+from StringIO import StringIO
+from email.utils import parseaddr
+from copy import deepcopy
+from leap.bitmask.mail.outgoing.service import OutgoingMail
+
+from twisted.internet.defer import Deferred, fail
+from twisted.mail.smtp import SMTPSenderFactory
+from twisted.internet import reactor, defer
+from pixelated.support.functional import flatten
+from twisted.mail.smtp import User
+
+from twisted.logger import Logger
+
+
+logger = Logger()
+
+
+class SMTPDownException(Exception):
+ def __init__(self):
+ Exception.__init__(self, "Couldn't send mail now, try again later.")
+
+
+NOT_NEEDED = None
+
+
+class MailSenderException(Exception):
+
+ def __init__(self, message, email_error_map):
+ super(MailSenderException, self).__init__(message, email_error_map)
+ self.email_error_map = email_error_map
+
+
+class MailSender(object):
+
+ def __init__(self, smtp_config, keymanager):
+ self._smtp_config = smtp_config
+ self._keymanager = keymanager
+
+ @defer.inlineCallbacks
+ def sendmail(self, mail):
+ # message is changed in sending, but should be saved unaltered
+ mail = deepcopy(mail)
+
+ recipients = flatten([mail.to, mail.cc, mail.bcc])
+
+ results = yield self._send_mail_to_all_recipients(mail, recipients)
+ all_succeeded = reduce(lambda a, b: a and b, [r[0] for r in results])
+
+ if not all_succeeded:
+ error_map = self._build_error_map(recipients, results)
+ raise MailSenderException('Failed to send mail to all recipients', error_map)
+
+ defer.returnValue(all_succeeded)
+
+ def _send_mail_to_all_recipients(self, mail, recipients):
+ outgoing_mail = self._create_outgoing_mail()
+ bccs = mail.bcc
+ deferreds = []
+
+ for recipient in recipients:
+ logger.info('_send_mail_to_all_recipients: Sending mail to recipient %s' % recipient)
+ self._define_bcc_field(mail, recipient, bccs)
+ smtp_recipient = self._create_twisted_smtp_recipient(recipient)
+ logger.info('_send_mail_to_all_recipients: Sending mail to smtp_recipient %s' % smtp_recipient)
+ deferreds.append(outgoing_mail.send_message(mail.to_smtp_format(), smtp_recipient))
+
+ return defer.DeferredList(deferreds, fireOnOneErrback=False, consumeErrors=True)
+
+ def _define_bcc_field(self, mail, recipient, bccs):
+ if recipient in bccs:
+ mail.headers['Bcc'] = [recipient]
+ else:
+ mail.headers['Bcc'] = []
+
+ def _build_error_map(self, recipients, results):
+ error_map = {}
+ for email, error in [(recipients[idx], r[1]) for idx, r in enumerate(results)]:
+ error_map[email] = error
+ return error_map
+
+ def _create_outgoing_mail(self):
+ return OutgoingMail(str(self._smtp_config.account_email),
+ self._keymanager,
+ self._smtp_config.cert_path,
+ self._smtp_config.cert_path,
+ str(self._smtp_config.remote_smtp_host),
+ int(self._smtp_config.remote_smtp_port))
+
+ def _create_twisted_smtp_recipient(self, recipient):
+ # TODO: Better is fix Twisted instead
+ return User(str(recipient), NOT_NEEDED, NOT_NEEDED, NOT_NEEDED)
diff --git a/service/src/pixelated/adapter/services/mail_service.py b/service/src/pixelated/adapter/services/mail_service.py
new file mode 100644
index 00000000..e5343997
--- /dev/null
+++ b/service/src/pixelated/adapter/services/mail_service.py
@@ -0,0 +1,172 @@
+#
+# 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/>.
+from email import encoders
+from email.mime.nonmultipart import MIMENonMultipart
+from email.mime.multipart import MIMEMultipart
+from leap.bitmask.mail.mail import Message
+
+from twisted.internet import defer
+
+from pixelated.adapter.model.mail import InputMail
+from pixelated.adapter.model.status import Status
+from pixelated.adapter.services.tag_service import extract_reserved_tags
+from leap.bitmask.mail.adaptors.soledad import SoledadMailAdaptor
+
+
+class MailService(object):
+
+ def __init__(self, mail_sender, mail_store, search_engine, account_email, attachment_store):
+ self.mail_store = mail_store
+ self.search_engine = search_engine
+ self.mail_sender = mail_sender
+ self.account_email = account_email
+ self.attachment_store = attachment_store
+
+ @defer.inlineCallbacks
+ def all_mails(self):
+ mails = yield self.mail_store.all_mails(gracefully_ignore_errors=True)
+ defer.returnValue(mails)
+
+ def save_attachment(self, content, content_type):
+ return self.attachment_store.add_attachment(content, content_type)
+
+ @defer.inlineCallbacks
+ def mails(self, query, window_size, page):
+ mail_ids, total = self.search_engine.search(query, window_size, page)
+
+ try:
+ mails = yield self.mail_store.get_mails(mail_ids)
+ defer.returnValue((mails, total))
+ except Exception, e:
+ import traceback
+ traceback.print_exc()
+ raise
+
+ @defer.inlineCallbacks
+ def update_tags(self, mail_id, new_tags):
+ new_tags = self._filter_white_space_tags(new_tags)
+ reserved_words = extract_reserved_tags(new_tags)
+ if len(reserved_words):
+ raise ValueError('None of the following words can be used as tags: ' + ' '.join(reserved_words))
+ new_tags = self._favor_existing_tags_casing(new_tags)
+ mail = yield self.mail(mail_id)
+ mail.tags = set(new_tags)
+ yield self.mail_store.update_mail(mail)
+
+ defer.returnValue(mail)
+
+ def _filter_white_space_tags(self, tags):
+ return [tag.strip() for tag in tags if not tag.isspace()]
+
+ def _favor_existing_tags_casing(self, new_tags):
+ current_tags = [tag['name'] for tag in self.search_engine.tags(query='', skip_default_tags=True)]
+ current_tags_lower = [tag.lower() for tag in current_tags]
+
+ def _use_current_casing(new_tag_lower):
+ return current_tags[current_tags_lower.index(new_tag_lower)]
+
+ return [_use_current_casing(new_tag.lower()) if new_tag.lower() in current_tags_lower else new_tag for new_tag in new_tags]
+
+ def mail(self, mail_id):
+ return self.mail_store.get_mail(mail_id, include_body=True)
+
+ def attachment(self, attachment_id):
+ return self.attachment_store.get_mail_attachment(attachment_id)
+
+ @defer.inlineCallbacks
+ def mail_exists(self, mail_id):
+ try:
+ mail = yield self.mail_store.get_mail(mail_id, include_body=False)
+ defer.returnValue(mail is not None)
+ except Exception, e:
+ defer.returnValue(False)
+
+ @defer.inlineCallbacks
+ def send_mail(self, content_dict):
+ mail = InputMail.from_dict(content_dict, self.account_email)
+ draft_id = content_dict.get('ident')
+ self._deduplicate_recipients(mail)
+ yield self.mail_sender.sendmail(mail)
+
+ sent_mail = yield self.move_to_sent(draft_id, mail)
+ defer.returnValue(sent_mail)
+
+ def _deduplicate_recipients(self, mail):
+ self._remove_canonical(mail)
+ self._remove_duplicates_form_cc_and_to(mail)
+
+ def _remove_canonical(self, mail):
+ mail.headers['To'] = map(self._remove_canonical_recipient, mail.to)
+ mail.headers['Cc'] = map(self._remove_canonical_recipient, mail.cc)
+ mail.headers['Bcc'] = map(self._remove_canonical_recipient, mail.bcc)
+
+ def _remove_duplicates_form_cc_and_to(self, mail):
+ mail.headers['To'] = list(set(self._remove_duplicates(mail.to)).difference(set(mail.bcc)))
+ mail.headers['Cc'] = list((set(self._remove_duplicates(mail.cc)).difference(set(mail.bcc)).difference(set(mail.to))))
+ mail.headers['Bcc'] = self._remove_duplicates(mail.bcc)
+
+ def _remove_duplicates(self, recipient):
+ return list(set(recipient))
+
+ # TODO removing canocical should, be added back later
+ def _remove_canonical_recipient(self, recipient):
+ return recipient.split('<')[1][0:-1] if '<' in recipient else recipient
+
+ @defer.inlineCallbacks
+ def move_to_sent(self, last_draft_ident, mail):
+ if last_draft_ident:
+ try:
+ yield self.mail_store.delete_mail(last_draft_ident)
+ except Exception as error:
+ pass
+ sent_mail = yield self.mail_store.add_mail('SENT', mail.raw)
+ sent_mail.flags.add(Status.SEEN)
+ yield self.mail_store.update_mail(sent_mail)
+ defer.returnValue(sent_mail)
+
+ @defer.inlineCallbacks
+ def mark_as_read(self, mail_id):
+ mail = yield self.mail(mail_id)
+ mail.flags.add(Status.SEEN)
+ yield self.mail_store.update_mail(mail)
+
+ @defer.inlineCallbacks
+ def mark_as_unread(self, mail_id):
+ mail = yield self.mail(mail_id)
+ mail.flags.remove(Status.SEEN)
+ yield self.mail_store.update_mail(mail)
+
+ @defer.inlineCallbacks
+ def delete_mail(self, mail_id):
+ mail = yield self.mail(mail_id)
+ if mail is not None:
+ if mail.mailbox_name.upper() in (u'TRASH', u'DRAFTS'):
+ yield self.mail_store.delete_mail(mail_id)
+ else:
+ yield self.mail_store.move_mail_to_mailbox(mail_id, 'TRASH')
+
+ @defer.inlineCallbacks
+ def recover_mail(self, mail_id):
+ yield self.mail_store.move_mail_to_mailbox(mail_id, 'INBOX')
+
+ @defer.inlineCallbacks
+ def archive_mail(self, mail_id):
+ yield self.mail_store.add_mailbox('ARCHIVE')
+ yield self.mail_store.move_mail_to_mailbox(mail_id, 'ARCHIVE')
+
+ @defer.inlineCallbacks
+ def delete_permanent(self, mail_id):
+ yield self.mail_store.delete_mail(mail_id)
diff --git a/service/src/pixelated/adapter/services/tag_service.py b/service/src/pixelated/adapter/services/tag_service.py
new file mode 100644
index 00000000..c51da625
--- /dev/null
+++ b/service/src/pixelated/adapter/services/tag_service.py
@@ -0,0 +1,23 @@
+#
+# 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/>.
+from pixelated.adapter.model.tag import Tag
+
+SPECIAL_TAGS = {Tag('inbox', True), Tag('sent', True), Tag('drafts', True), Tag('trash', True), Tag('ALL', True)}
+
+
+def extract_reserved_tags(tags):
+ tags = [tag.lower() for tag in tags]
+ return {tag.name for tag in SPECIAL_TAGS if tag.name in tags}