From 91e4481c450eb7eb928debc1cb7fa59bdb63dd7b Mon Sep 17 00:00:00 2001 From: Kali Kaneko Date: Tue, 25 Jul 2017 11:40:11 -0400 Subject: [pkg] packaging and path changes - move all the pixelated python package under src/ - move the pixelated_www package under the leap namespace - allow to set globally the static folder - add hours and minutes to the timestamp in package version, to allow for several releases a day. --- service/src/pixelated/adapter/services/__init__.py | 15 ++ .../pixelated/adapter/services/draft_service.py | 40 +++++ .../pixelated/adapter/services/feedback_service.py | 36 +++++ .../src/pixelated/adapter/services/mail_sender.py | 106 +++++++++++++ .../src/pixelated/adapter/services/mail_service.py | 172 +++++++++++++++++++++ .../src/pixelated/adapter/services/tag_service.py | 23 +++ 6 files changed, 392 insertions(+) create mode 100644 service/src/pixelated/adapter/services/__init__.py create mode 100644 service/src/pixelated/adapter/services/draft_service.py create mode 100644 service/src/pixelated/adapter/services/feedback_service.py create mode 100644 service/src/pixelated/adapter/services/mail_sender.py create mode 100644 service/src/pixelated/adapter/services/mail_service.py create mode 100644 service/src/pixelated/adapter/services/tag_service.py (limited to 'service/src/pixelated/adapter/services') 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 . 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 . +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 . + +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 . +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 . +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 . +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} -- cgit v1.2.3