From 8abb94a88e40fde249b562a841a5b0398582717e Mon Sep 17 00:00:00 2001 From: Duda Dornelles Date: Wed, 21 Jan 2015 18:12:25 -0200 Subject: #224 App is working without klein - migration to twisted "complete" --- service/pixelated/config/app_factory.py | 35 +++--- service/pixelated/config/routes.py | 24 ---- service/pixelated/controllers/__init__.py | 41 ------- .../controllers/attachments_controller.py | 49 -------- .../pixelated/controllers/contacts_controller.py | 31 ----- .../pixelated/controllers/features_controller.py | 32 ------ service/pixelated/controllers/home_controller.py | 42 ------- service/pixelated/controllers/mails_controller.py | 124 -------------------- .../pixelated/controllers/sync_info_controller.py | 41 ------- service/pixelated/controllers/tags_controller.py | 33 ------ service/pixelated/resources/__init__.py | 33 ++++++ .../pixelated/resources/attachments_resource.py | 63 ++++++++++ service/pixelated/resources/contacts_resource.py | 35 ++++++ service/pixelated/resources/features_resource.py | 32 ++++++ service/pixelated/resources/mail_resource.py | 64 +++++++++++ service/pixelated/resources/mails_resource.py | 128 +++++++++++++++++++++ service/pixelated/resources/root_resource.py | 46 ++++++++ service/pixelated/resources/sync_info_resource.py | 46 ++++++++ service/pixelated/resources/tags_resource.py | 38 ++++++ service/pixelated/runserver.py | 13 ++- service/setup.py | 4 +- service/test/functional/features/environment.py | 2 +- .../test/support/integration/app_test_client.py | 2 +- .../test/support/integration/soledad_test_base.py | 2 +- .../test/unit/controllers/mails_controller_test.py | 2 +- .../unit/controllers/sync_info_controller_test.py | 2 +- 26 files changed, 514 insertions(+), 450 deletions(-) delete mode 100644 service/pixelated/config/routes.py delete mode 100644 service/pixelated/controllers/__init__.py delete mode 100644 service/pixelated/controllers/attachments_controller.py delete mode 100644 service/pixelated/controllers/contacts_controller.py delete mode 100644 service/pixelated/controllers/features_controller.py delete mode 100644 service/pixelated/controllers/home_controller.py delete mode 100644 service/pixelated/controllers/mails_controller.py delete mode 100644 service/pixelated/controllers/sync_info_controller.py delete mode 100644 service/pixelated/controllers/tags_controller.py create mode 100644 service/pixelated/resources/__init__.py create mode 100644 service/pixelated/resources/attachments_resource.py create mode 100644 service/pixelated/resources/contacts_resource.py create mode 100644 service/pixelated/resources/features_resource.py create mode 100644 service/pixelated/resources/mail_resource.py create mode 100644 service/pixelated/resources/mails_resource.py create mode 100644 service/pixelated/resources/root_resource.py create mode 100644 service/pixelated/resources/sync_info_resource.py create mode 100644 service/pixelated/resources/tags_resource.py (limited to 'service') diff --git a/service/pixelated/config/app_factory.py b/service/pixelated/config/app_factory.py index 745db937..12830743 100644 --- a/service/pixelated/config/app_factory.py +++ b/service/pixelated/config/app_factory.py @@ -19,9 +19,9 @@ from OpenSSL import SSL from OpenSSL import crypto from twisted.internet import reactor from twisted.internet import ssl +from pixelated.resources.root_resource import RootResource from twisted.web import resource from twisted.web.util import redirectTo -from pixelated.config.routes import setup_routes from pixelated.adapter.services.mail_service import MailService from pixelated.adapter.model.mail import InputMail from pixelated.adapter.services.mail_sender import MailSender @@ -33,7 +33,6 @@ from pixelated.adapter.listeners.mailbox_indexer_listener import MailboxIndexerL import pixelated.bitmask_libraries.session as LeapSession from pixelated.bitmask_libraries.leap_srp import LeapAuthException from requests.exceptions import ConnectionError -from pixelated.controllers import * from pixelated.adapter.services.tag_service import TagService from leap.common.events import ( register, @@ -100,20 +99,16 @@ def init_app(app, leap_home): MailboxIndexerListener.SEARCH_ENGINE = search_engine InputMail.FROM_EMAIL_ADDRESS = leap_session.account_email() - home_controller = HomeController() - features_controller = FeaturesController() - mails_controller = MailsController(mail_service=mail_service, - draft_service=draft_service, - search_engine=search_engine) - tags_controller = TagsController(search_engine=search_engine) - contacts_controller = ContactsController(search_engine=search_engine) - sync_info_controller = SyncInfoController() - attachments_controller = AttachmentsController(soledad_querier) - - register(signal=proto.SOLEDAD_SYNC_RECEIVE_STATUS, - callback=update_info_sync_and_index_partial(sync_info_controller=sync_info_controller, - search_engine=search_engine, - mail_service=mail_service)) + app.resource.initialize(soledad_querier, search_engine, mail_service, draft_service) + + # add root to reactor + + # register(signal=proto.SOLEDAD_SYNC_RECEIVE_STATUS, + # callback=update_info_sync_and_index_partial(sync_info_controller=sync_info_controller, + # search_engine=search_engine, + # mail_service=mail_service)) + + register(signal=proto.SOLEDAD_DONE_DATA_SYNC, callback=init_index_and_remove_dupes(querier=soledad_querier, search_engine=search_engine, @@ -122,11 +117,9 @@ def init_app(app, leap_home): register(signal=proto.SOLEDAD_DONE_DATA_SYNC, uid=CREATE_KEYS_IF_KEYS_DONT_EXISTS_CALLBACK, callback=look_for_user_key_and_create_if_cant_find(leap_session)) - setup_routes(app, home_controller, mails_controller, tags_controller, features_controller, - sync_info_controller, attachments_controller, contacts_controller) - def create_app(app, args): + app.resource = RootResource() if args.sslkey and args.sslcert: listen_with_ssl(app, args) else: @@ -136,7 +129,7 @@ def create_app(app, args): def listen_without_ssl(app, args): - reactor.listenTCP(args.port, Site(app.resource()), interface=args.host) + reactor.listenTCP(args.port, Site(app.resource), interface=args.host) def _ssl_options(args): @@ -152,7 +145,7 @@ def _ssl_options(args): def listen_with_ssl(app, args): - reactor.listenSSL(args.port, Site(app.resource()), _ssl_options(args), interface=args.host) + reactor.listenSSL(args.port, Site(app.resource), _ssl_options(args), interface=args.host) return reactor diff --git a/service/pixelated/config/routes.py b/service/pixelated/config/routes.py deleted file mode 100644 index 5efbbb28..00000000 --- a/service/pixelated/config/routes.py +++ /dev/null @@ -1,24 +0,0 @@ -def setup_routes(app, home_controller, mails_controller, tags_controller, features_controller, sync_info_controller, - attachments_controller, contacts_controller): - # mails - app.route('/mails', methods=['GET'])(mails_controller.mails) - app.route('/mails/unread', methods=['POST'])(mails_controller.mark_many_mail_unread) - app.route('/mails/read', methods=['POST'])(mails_controller.mark_many_mail_read) - app.route('/mail/', methods=['GET'])(mails_controller.mail) - app.route('/mail/', methods=['DELETE'])(mails_controller.delete_mail) - app.route('/mails/delete', methods=['POST'])(mails_controller.delete_mails) - app.route('/mails', methods=['POST'])(mails_controller.send_mail) - app.route('/mail//tags', methods=['POST'])(mails_controller.mail_tags) - app.route('/mails', methods=['PUT'])(mails_controller.update_draft) - # tags - app.route('/tags', methods=['GET'])(tags_controller.tags) - # contacts - app.route('/contacts', methods=['GET'])(contacts_controller.contacts) - # features - app.route('/features', methods=['GET'])(features_controller.features) - # sync info - app.route('/sync_info', methods=['GET'])(sync_info_controller.sync_info) - # attachments - app.route('/attachment/', methods=['GET'])(attachments_controller.attachment) - # static - app.route('/', methods=['GET'], branch=True)(home_controller.home) diff --git a/service/pixelated/controllers/__init__.py b/service/pixelated/controllers/__init__.py deleted file mode 100644 index 6bc8e7c2..00000000 --- a/service/pixelated/controllers/__init__.py +++ /dev/null @@ -1,41 +0,0 @@ -# -# 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 . - - -def respond_json(entity, request, status_code=200): - json_response = json.dumps(entity) - request.responseHeaders.addRawHeader(b"content-type", b"application/json") - request.code = status_code - return json_response - - -def respond_json_deferred(entity, request, status_code=200): - json_response = json.dumps(entity) - request.responseHeaders.addRawHeader(b"content-type", b"application/json") - request.code = status_code - request.write(json_response) - request.finish() - - -import json - -from home_controller import HomeController -from mails_controller import MailsController -from tags_controller import TagsController -from features_controller import FeaturesController -from sync_info_controller import SyncInfoController -from attachments_controller import AttachmentsController -from contacts_controller import ContactsController diff --git a/service/pixelated/controllers/attachments_controller.py b/service/pixelated/controllers/attachments_controller.py deleted file mode 100644 index b3fed903..00000000 --- a/service/pixelated/controllers/attachments_controller.py +++ /dev/null @@ -1,49 +0,0 @@ -# -# 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 io - -import re -from twisted.protocols.basic import FileSender -from twisted.python.log import err - - -class AttachmentsController: - - def __init__(self, querier): - self.querier = querier - - def attachment(self, request, attachment_id): - encoding = request.args.get('encoding', [None])[0] - filename = request.args.get('filename', [attachment_id])[0] - attachment = self.querier.attachment(attachment_id, encoding) - - request.setHeader(b'Content-Type', b'application/force-download') - request.setHeader(b'Content-Disposition', bytes('attachment; filename=' + filename)) - bytes_io = io.BytesIO(attachment['content']) - d = FileSender().beginFileTransfer(bytes_io, request) - - def cbFinished(ignored): - bytes_io.close() - request.finish() - - d.addErrback(err).addCallback(cbFinished) - - return d - - def _extract_mimetype(self, content_type): - match = re.compile('([A-Za-z-]+\/[A-Za-z-]+)').search(content_type) - return match.group(1) diff --git a/service/pixelated/controllers/contacts_controller.py b/service/pixelated/controllers/contacts_controller.py deleted file mode 100644 index 5825b563..00000000 --- a/service/pixelated/controllers/contacts_controller.py +++ /dev/null @@ -1,31 +0,0 @@ -# -# 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.controllers import respond_json_deferred -from twisted.internet.threads import deferToThread - - -class ContactsController: - - def __init__(self, search_engine): - self._search_engine = search_engine - - def contacts(self, request): - query = request.args.get('q', [''])[0] - d = deferToThread(lambda: self._search_engine.contacts(query)) - d.addCallback(lambda tags: respond_json_deferred(tags, request)) - - return d diff --git a/service/pixelated/controllers/features_controller.py b/service/pixelated/controllers/features_controller.py deleted file mode 100644 index b91aa183..00000000 --- a/service/pixelated/controllers/features_controller.py +++ /dev/null @@ -1,32 +0,0 @@ -# -# 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.controllers import respond_json -import os - - -class FeaturesController: - DISABLED_FEATURES = ['draftReply', 'encryptionStatus'] - - def __init__(self): - pass - - def features(self, request): - try: - disabled_features = {'logout': os.environ['DISPATCHER_LOGOUT_URL']} - except KeyError: - disabled_features = {} - return respond_json({'disabled_features': self.DISABLED_FEATURES, 'dispatcher_features': disabled_features}, request) diff --git a/service/pixelated/controllers/home_controller.py b/service/pixelated/controllers/home_controller.py deleted file mode 100644 index ccdad197..00000000 --- a/service/pixelated/controllers/home_controller.py +++ /dev/null @@ -1,42 +0,0 @@ -# -# 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 os - -from twisted.web.static import File - - -class HomeController: - def __init__(self): - self.static_folder = self._get_static_folder() - pass - - def _get_static_folder(self): - - static_folder = os.path.abspath(os.path.join(os.path.abspath(__file__), "..", "..", "..", "web-ui", "app")) - # this is a workaround for packaging - if not os.path.exists(static_folder): - static_folder = os.path.abspath( - os.path.join(os.path.abspath(__file__), "..", "..", "..", "..", "web-ui", "app")) - if not os.path.exists(static_folder): - static_folder = os.path.join('/', 'usr', 'share', 'pixelated-user-agent') - return static_folder - - def home(self, request): - request_type = request.requestHeaders.getRawHeaders('accept')[0].split(',')[0] - response_type = request_type if request_type else "text/html" - - request.setHeader('Content-Type', response_type) - return File('%s/' % self.static_folder, defaultType=response_type) diff --git a/service/pixelated/controllers/mails_controller.py b/service/pixelated/controllers/mails_controller.py deleted file mode 100644 index fb9c9adc..00000000 --- a/service/pixelated/controllers/mails_controller.py +++ /dev/null @@ -1,124 +0,0 @@ -# -# 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 - -from pixelated.adapter.model.mail import InputMail -from pixelated.controllers import respond_json - - -class MailsController: - - def __init__(self, mail_service, draft_service, search_engine): - self._mail_service = mail_service - self._draft_service = draft_service - self._search_engine = search_engine - - def mails(self, request): - mail_ids, total = self._search_engine.search(request.args.get('q')[0], request.args.get('w')[0], request.args.get('p')[0]) - mails = self._mail_service.mails(mail_ids) - - response = { - "stats": { - "total": total, - }, - "mails": [mail.as_dict() for mail in mails] - } - - return json.dumps(response) - - def mail(self, request, mail_id): - mail = self._mail_service.mail(mail_id) - return respond_json(mail.as_dict(), request) - - def mark_many_mail_unread(self, request): - content_dict = json.load(request.content) - idents = content_dict.get('idents') - for ident in idents: - mail = self._mail_service.mark_as_unread(ident) - self._search_engine.index_mail(mail) - return "" - - def mark_many_mail_read(self, request): - content_dict = json.load(request.content) - idents = content_dict.get('idents') - for ident in idents: - mail = self._mail_service.mark_as_read(ident) - self._search_engine.index_mail(mail) - return "" - - def _delete_mail(self, mail_id): - mail = self._mail_service.mail(mail_id) - if mail.mailbox_name == 'TRASH': - self._mail_service.delete_permanent(mail_id) - self._search_engine.remove_from_index(mail_id) - else: - trashed_mail = self._mail_service.delete_mail(mail_id) - self._search_engine.index_mail(trashed_mail) - - def delete_mail(self, request, mail_id): - self._delete_mail(mail_id) - return respond_json(None, request) - - def delete_mails(self, request): - idents = json.loads(request.content.read())['idents'] - for ident in idents: - self._delete_mail(ident) - return respond_json(None, request) - - def send_mail(self, request): - try: - content_dict = json.loads(request.content.read()) - _mail = InputMail.from_dict(content_dict) - draft_id = content_dict.get('ident') - if draft_id: - self._search_engine.remove_from_index(draft_id) - _mail = self._mail_service.send(draft_id, _mail) - self._search_engine.index_mail(_mail) - - return respond_json(_mail.as_dict(), request) - except Exception as error: - return respond_json({'message': self._format_exception(error)}, request, status_code=422) - - def mail_tags(self, request, mail_id): - content_dict = json.loads(request.content.read()) - new_tags = map(lambda tag: tag.lower(), content_dict['newtags']) - try: - self._mail_service.update_tags(mail_id, new_tags) - mail = self._mail_service.mail(mail_id) - self._search_engine.index_mail(mail) - except ValueError as ve: - return respond_json(ve.message, request, 403) - return respond_json(mail.as_dict(), request) - - def update_draft(self, request): - content_dict = json.loads(request.content.read()) - _mail = InputMail.from_dict(content_dict) - draft_id = content_dict.get('ident') - - if draft_id: - if not self._mail_service.mail_exists(draft_id): - return respond_json("", request, status_code=422) - pixelated_mail = self._draft_service.update_draft(draft_id, _mail) - self._search_engine.remove_from_index(draft_id) - else: - pixelated_mail = self._draft_service.create_draft(_mail) - self._search_engine.index_mail(pixelated_mail) - return respond_json({'ident': pixelated_mail.ident}, request) - - def _format_exception(self, exception): - exception_info = map(str, list(exception.args)) - return '\n'.join(exception_info) diff --git a/service/pixelated/controllers/sync_info_controller.py b/service/pixelated/controllers/sync_info_controller.py deleted file mode 100644 index 50e53852..00000000 --- a/service/pixelated/controllers/sync_info_controller.py +++ /dev/null @@ -1,41 +0,0 @@ -# -# 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.controllers import respond_json - - -class SyncInfoController: - def __init__(self): - self.current = 0 - self.total = 0 - - def _get_progress(self): - if self.total == 0: - return 0 - return self.current / float(self.total) - - def set_sync_info(self, soledad_sync_status): - self.current, self.total = map(int, soledad_sync_status.content.split('/')) - - def sync_info(self, request): - _sync_info = { - 'is_syncing': self.current != self.total, - 'count': { - 'current': self.current, - 'total': self.total, - 'progress': self._get_progress() - } - } - return respond_json(_sync_info, request) diff --git a/service/pixelated/controllers/tags_controller.py b/service/pixelated/controllers/tags_controller.py deleted file mode 100644 index b6741dcc..00000000 --- a/service/pixelated/controllers/tags_controller.py +++ /dev/null @@ -1,33 +0,0 @@ -# -# 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.controllers import respond_json_deferred -from twisted.internet.threads import deferToThread - - -class TagsController: - - def __init__(self, search_engine): - self._search_engine = search_engine - - def tags(self, request): - query = request.args.get('q', [''])[0] - skip_default_tags = request.args.get('skipDefaultTags', [False])[0] - - d = deferToThread(lambda: self._search_engine.tags(query=query, skip_default_tags=skip_default_tags)) - d.addCallback(lambda tags: respond_json_deferred(tags, request)) - - return d diff --git a/service/pixelated/resources/__init__.py b/service/pixelated/resources/__init__.py new file mode 100644 index 00000000..a2e4c9d4 --- /dev/null +++ b/service/pixelated/resources/__init__.py @@ -0,0 +1,33 @@ +# +# 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 . + + +def respond_json(entity, request, status_code=200): + json_response = json.dumps(entity) + request.responseHeaders.addRawHeader(b"content-type", b"application/json") + request.code = status_code + return json_response + + +def respond_json_deferred(entity, request, status_code=200): + json_response = json.dumps(entity) + request.responseHeaders.addRawHeader(b"content-type", b"application/json") + request.code = status_code + request.write(json_response) + request.finish() + + +import json diff --git a/service/pixelated/resources/attachments_resource.py b/service/pixelated/resources/attachments_resource.py new file mode 100644 index 00000000..0ab214b9 --- /dev/null +++ b/service/pixelated/resources/attachments_resource.py @@ -0,0 +1,63 @@ +# +# 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 io + +import re +from twisted.protocols.basic import FileSender +from twisted.python.log import err +from twisted.web.resource import Resource + + +class AttachmentResource(Resource): + def __init__(self, attachment_id, querier): + Resource.__init__(self) + self.attachment_id = attachment_id + self.querier = querier + + def render_GET(self, request): + encoding = request.args.get('encoding', [None])[0] + filename = request.args.get('filename', [self.attachment_id])[0] + attachment = self.querier.attachment(self.attachment_id, encoding) + + request.setHeader(b'Content-Type', b'application/force-download') + request.setHeader(b'Content-Disposition', bytes('attachment; filename=' + filename)) + bytes_io = io.BytesIO(attachment['content']) + d = FileSender().beginFileTransfer(bytes_io, request) + + def cb_finished(_): + bytes_io.close() + request.finish() + + d.addErrback(err).addCallback(cb_finished) + + return d + + def _extract_mimetype(self, content_type): + match = re.compile('([A-Za-z-]+\/[A-Za-z-]+)').search(content_type) + return match.group(1) + + +class AttachmentsResource(Resource): + + isLeaf = True + + def __init__(self, querier): + Resource.__init__(self) + self.querier = querier + + def getChild(self, attachment_id, request): + return AttachmentResource(attachment_id, self.querier) diff --git a/service/pixelated/resources/contacts_resource.py b/service/pixelated/resources/contacts_resource.py new file mode 100644 index 00000000..94468a63 --- /dev/null +++ b/service/pixelated/resources/contacts_resource.py @@ -0,0 +1,35 @@ +# +# 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.resources import respond_json_deferred +from twisted.internet.threads import deferToThread +from twisted.web.resource import Resource + + +class ContactsResource(Resource): + + isLeaf = True + + def __init__(self, search_engine): + Resource.__init__(self) + self._search_engine = search_engine + + def render_GET(self, request): + query = request.args.get('q', [''])[0] + d = deferToThread(lambda: self._search_engine.contacts(query)) + d.addCallback(lambda tags: respond_json_deferred(tags, request)) + + return d diff --git a/service/pixelated/resources/features_resource.py b/service/pixelated/resources/features_resource.py new file mode 100644 index 00000000..1784e463 --- /dev/null +++ b/service/pixelated/resources/features_resource.py @@ -0,0 +1,32 @@ +# +# 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.resources import respond_json +import os +from twisted.web.resource import Resource + + +class FeaturesResource(Resource): + DISABLED_FEATURES = ['draftReply', 'encryptionStatus'] + + isLeaf = True + + def render_GET(self, request): + try: + disabled_features = {'logout': os.environ['DISPATCHER_LOGOUT_URL']} + except KeyError: + disabled_features = {} + return respond_json({'disabled_features': self.DISABLED_FEATURES, 'dispatcher_features': disabled_features}, request) diff --git a/service/pixelated/resources/mail_resource.py b/service/pixelated/resources/mail_resource.py new file mode 100644 index 00000000..03873ffb --- /dev/null +++ b/service/pixelated/resources/mail_resource.py @@ -0,0 +1,64 @@ +import json +from pixelated.resources import respond_json +from twisted.web.resource import Resource + + +class MailTags(Resource): + + isLeaf = True + + def __init__(self, mail_id, mail_service, search_engine): + Resource.__init__(self) + self._search_engine = search_engine + self._mail_service = mail_service + self._mail_id = mail_id + + def render_POST(self, request): + content_dict = json.loads(request.content.read()) + new_tags = map(lambda tag: tag.lower(), content_dict['newtags']) + try: + self._mail_service.update_tags(self._mail_id, new_tags) + mail = self._mail_service.mail(self._mail_id) + self._search_engine.index_mail(mail) + except ValueError as ve: + return respond_json(ve.message, request, 403) + return respond_json(mail.as_dict(), request) + + +class Mail(Resource): + + def __init__(self, mail_id, mail_service, search_engine): + Resource.__init__(self) + self.putChild('tags', MailTags(mail_id, mail_service, search_engine)) + + self._search_engine = search_engine + self._mail_id = mail_id + self._mail_service = mail_service + + def render_GET(self, request): + mail = self._mail_service.mail(self._mail_id) + return respond_json(mail.as_dict(), request) + + def render_DELETE(self, request): + self._delete_mail(self._mail_id) + return respond_json(None, request) + + def _delete_mail(self, mail_id): + mail = self._mail_service.mail(mail_id) + if mail.mailbox_name == 'TRASH': + self._mail_service.delete_permanent(mail_id) + self._search_engine.remove_from_index(mail_id) + else: + trashed_mail = self._mail_service.delete_mail(mail_id) + self._search_engine.index_mail(trashed_mail) + + +class MailResource(Resource): + + def __init__(self, mail_service, search_engine): + Resource.__init__(self) + self._mail_service = mail_service + self._search_engine = search_engine + + def getChild(self, mail_id, request): + return Mail(mail_id, self._mail_service, self._search_engine) diff --git a/service/pixelated/resources/mails_resource.py b/service/pixelated/resources/mails_resource.py new file mode 100644 index 00000000..75c73349 --- /dev/null +++ b/service/pixelated/resources/mails_resource.py @@ -0,0 +1,128 @@ +import json +from pixelated.adapter.model.mail import InputMail +from pixelated.resources import respond_json +from twisted.web.resource import Resource + + +def _format_exception(e): + exception_info = map(str, list(e.args)) + return '\n'.join(exception_info) + + +class MailsUnreadResource(Resource): + + isLeaf = True + + def __init__(self, mail_service, search_engine): + Resource.__init__(self) + self._search_engine = search_engine + self._mail_service = mail_service + + def render_POST(self, request): + content_dict = json.load(request.content) + idents = content_dict.get('idents') + for ident in idents: + mail = self._mail_service.mark_as_unread(ident) + self._search_engine.index_mail(mail) + return "" + + +class MailsReadResource(Resource): + + isLeaf = True + + def __init__(self, mail_service, search_engine): + Resource.__init__(self) + self._search_engine = search_engine + self._mail_service = mail_service + + def render_POST(self, request): + content_dict = json.load(request.content) + idents = content_dict.get('idents') + for ident in idents: + mail = self._mail_service.mark_as_read(ident) + self._search_engine.index_mail(mail) + return "" + + +class MailsDeleteResource(Resource): + + isLeaf = True + + def __init__(self, mail_service, search_engine): + Resource.__init__(self) + self._mail_service = mail_service + self._search_engine = search_engine + + def render_POST(self, request): + idents = json.loads(request.content.read())['idents'] + for ident in idents: + self._delete_mail(ident) + return respond_json(None, request) + + def _delete_mail(self, mail_id): + mail = self._mail_service.mail(mail_id) + if mail.mailbox_name == 'TRASH': + self._mail_service.delete_permanent(mail_id) + self._search_engine.remove_from_index(mail_id) + else: + trashed_mail = self._mail_service.delete_mail(mail_id) + self._search_engine.index_mail(trashed_mail) + + +class MailsResource(Resource): + + def __init__(self, search_engine, mail_service, draft_service): + Resource.__init__(self) + self.putChild('delete', MailsDeleteResource(mail_service, search_engine)) + self.putChild('read', MailsReadResource(mail_service, search_engine)) + self.putChild('unread', MailsUnreadResource(mail_service, search_engine)) + + self._draft_service = draft_service + self._mail_service = mail_service + self._search_engine = search_engine + + def render_GET(self, request): + mail_ids, total = self._search_engine.search(request.args.get('q')[0], request.args.get('w')[0], request.args.get('p')[0]) + mails = self._mail_service.mails(mail_ids) + + response = { + "stats": { + "total": total, + }, + "mails": [mail.as_dict() for mail in mails] + } + + return json.dumps(response) + + def render_POST(self, request): + try: + content_dict = json.loads(request.content.read()) + _mail = InputMail.from_dict(content_dict) + draft_id = content_dict.get('ident') + if draft_id: + self._search_engine.remove_from_index(draft_id) + _mail = self._mail_service.send(draft_id, _mail) + self._search_engine.index_mail(_mail) + + return respond_json(_mail.as_dict(), request) + except Exception as error: + return respond_json({'message': _format_exception(error)}, request, status_code=422) + + def render_PUT(self, request): + content_dict = json.loads(request.content.read()) + _mail = InputMail.from_dict(content_dict) + draft_id = content_dict.get('ident') + + if draft_id: + if not self._mail_service.mail_exists(draft_id): + return respond_json("", request, status_code=422) + pixelated_mail = self._draft_service.update_draft(draft_id, _mail) + self._search_engine.remove_from_index(draft_id) + else: + pixelated_mail = self._draft_service.create_draft(_mail) + self._search_engine.index_mail(pixelated_mail) + return respond_json({'ident': pixelated_mail.ident}, request) + + + diff --git a/service/pixelated/resources/root_resource.py b/service/pixelated/resources/root_resource.py new file mode 100644 index 00000000..326654eb --- /dev/null +++ b/service/pixelated/resources/root_resource.py @@ -0,0 +1,46 @@ +import os +from pixelated.resources.attachments_resource import AttachmentsResource +from pixelated.resources.contacts_resource import ContactsResource +from pixelated.resources.features_resource import FeaturesResource +from pixelated.resources.mail_resource import MailResource +from pixelated.resources.mails_resource import MailsResource +from pixelated.resources.sync_info_resource import SyncInfoResource +from pixelated.resources.tags_resource import TagsResource +from twisted.web.resource import Resource +from twisted.web.static import File + + +class RootResource(Resource): + + def __init__(self): + Resource.__init__(self) + self._static_folder = self._get_static_folder() + + def getChild(self, path, request): + if path == '': + return self + return Resource.getChild(self, path, request) + + def initialize(self, querier, search_engine, mail_service, draft_service): + self.putChild('assets', File(self._static_folder)) + self.putChild('attachments', AttachmentsResource(querier)) + self.putChild('contacts', ContactsResource(search_engine)) + self.putChild('features', FeaturesResource()) + self.putChild('sync_info', SyncInfoResource()) + self.putChild('tags', TagsResource(search_engine)) + self.putChild('mails', MailsResource(search_engine, mail_service, draft_service)) + self.putChild('mail', MailResource(mail_service, search_engine)) + + def _get_static_folder(self): + + static_folder = os.path.abspath(os.path.join(os.path.abspath(__file__), "..", "..", "..", "web-ui", "app")) + # this is a workaround for packaging + if not os.path.exists(static_folder): + static_folder = os.path.abspath( + os.path.join(os.path.abspath(__file__), "..", "..", "..", "..", "web-ui", "app")) + if not os.path.exists(static_folder): + static_folder = os.path.join('/', 'usr', 'share', 'pixelated-user-agent') + return static_folder + + def render_GET(self, request): + return open(os.path.join(self._static_folder, 'index.html')).read() \ No newline at end of file diff --git a/service/pixelated/resources/sync_info_resource.py b/service/pixelated/resources/sync_info_resource.py new file mode 100644 index 00000000..5aa94218 --- /dev/null +++ b/service/pixelated/resources/sync_info_resource.py @@ -0,0 +1,46 @@ +# +# 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.resources import respond_json +from twisted.web.resource import Resource + + +class SyncInfoResource(Resource): + + isLeaf = True + + def __init__(self): + Resource.__init__(self) + self.current = 0 + self.total = 0 + + def _get_progress(self): + if self.total == 0: + return 0 + return self.current / float(self.total) + + def set_sync_info(self, soledad_sync_status): + self.current, self.total = map(int, soledad_sync_status.content.split('/')) + + def render_GET(self, request): + _sync_info = { + 'is_syncing': self.current != self.total, + 'count': { + 'current': self.current, + 'total': self.total, + 'progress': self._get_progress() + } + } + return respond_json(_sync_info, request) diff --git a/service/pixelated/resources/tags_resource.py b/service/pixelated/resources/tags_resource.py new file mode 100644 index 00000000..8a8ab81f --- /dev/null +++ b/service/pixelated/resources/tags_resource.py @@ -0,0 +1,38 @@ +# +# 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.resources import respond_json_deferred +from twisted.internet.threads import deferToThread +from twisted.web.resource import Resource +from twisted.web.server import NOT_DONE_YET + + +class TagsResource(Resource): + + isLeaf = True + + def __init__(self, search_engine): + Resource.__init__(self) + self._search_engine = search_engine + + def render_GET(self, request): + query = request.args.get('q', [''])[0] + skip_default_tags = request.args.get('skipDefaultTags', [False])[0] + + d = deferToThread(lambda: self._search_engine.tags(query=query, skip_default_tags=skip_default_tags)) + d.addCallback(lambda tags: respond_json_deferred(tags, request)) + + return NOT_DONE_YET diff --git a/service/pixelated/runserver.py b/service/pixelated/runserver.py index 37a55582..b6762177 100644 --- a/service/pixelated/runserver.py +++ b/service/pixelated/runserver.py @@ -19,10 +19,6 @@ import logging import json import os -from klein import Klein - - -klein_app = Klein() import ConfigParser from twisted.python import log @@ -36,7 +32,14 @@ import pixelated.support.ext_protobuf # monkey patch for protobuf in OSX import pixelated.support.ext_sqlcipher # monkey patch for sqlcipher in debian -app = Klein() +class App: + + def __init__(self): + self.resource = None + self.config = None + pass + +app = App() app.config = {} diff --git a/service/setup.py b/service/setup.py index d50d9a7e..8ef66618 100644 --- a/service/setup.py +++ b/service/setup.py @@ -82,7 +82,7 @@ setup(name='pixelated-user-agent', 'pixelated.config', 'pixelated.certificates', 'pixelated.support', - 'pixelated.controllers' + 'pixelated.resources' ], test_suite='nose.collector', install_requires=[ @@ -97,7 +97,7 @@ setup(name='pixelated-user-agent', 'leap.soledad.common==0.6.3', 'leap.soledad.client==0.6.3', 'leap.mail==0.3.9-1-gc1f9c92', - 'whoosh==2.3.2' + 'whoosh==2.5.7' ], entry_points={ 'console_scripts': [ diff --git a/service/test/functional/features/environment.py b/service/test/functional/features/environment.py index 72140e40..e4c4fa0c 100644 --- a/service/test/functional/features/environment.py +++ b/service/test/functional/features/environment.py @@ -19,7 +19,7 @@ from test.support.dispatcher.proxy import Proxy from test.support.integration import AppTestClient from selenium import webdriver -from pixelated.controllers.features_controller import FeaturesController +from pixelated.resources.features_resource import FeaturesController def before_all(context): diff --git a/service/test/support/integration/app_test_client.py b/service/test/support/integration/app_test_client.py index eab001c6..b032eefd 100644 --- a/service/test/support/integration/app_test_client.py +++ b/service/test/support/integration/app_test_client.py @@ -29,7 +29,7 @@ from pixelated.adapter.services.mail_service import MailService from pixelated.adapter.services.mailboxes import Mailboxes from pixelated.adapter.soledad.soledad_querier import SoledadQuerier from pixelated.adapter.services.tag_service import TagService -from pixelated.controllers import FeaturesController, HomeController, MailsController, TagsController, \ +from pixelated.resources import FeaturesController, HomeController, MailsController, TagsController, \ SyncInfoController, AttachmentsController, ContactsController import pixelated.runserver from pixelated.adapter.model.mail import PixelatedMail diff --git a/service/test/support/integration/soledad_test_base.py b/service/test/support/integration/soledad_test_base.py index 4149462c..5892de60 100644 --- a/service/test/support/integration/soledad_test_base.py +++ b/service/test/support/integration/soledad_test_base.py @@ -15,7 +15,7 @@ # along with Pixelated. If not, see . import unittest -from pixelated.controllers import * +from pixelated.resources import * from test.support.integration.app_test_client import AppTestClient from test.support.integration.model import ResponseMail diff --git a/service/test/unit/controllers/mails_controller_test.py b/service/test/unit/controllers/mails_controller_test.py index 8108bc19..7e5d0e7d 100644 --- a/service/test/unit/controllers/mails_controller_test.py +++ b/service/test/unit/controllers/mails_controller_test.py @@ -20,7 +20,7 @@ from io import BytesIO from klein.test_resource import requestMock from mock import MagicMock from mockito import * -from pixelated.controllers.mails_controller import MailsController +from pixelated.resources.mails_controller import MailsController class TestMailsController(unittest.TestCase): diff --git a/service/test/unit/controllers/sync_info_controller_test.py b/service/test/unit/controllers/sync_info_controller_test.py index cd3aeb02..1fb38822 100644 --- a/service/test/unit/controllers/sync_info_controller_test.py +++ b/service/test/unit/controllers/sync_info_controller_test.py @@ -17,7 +17,7 @@ import unittest import json from mock import MagicMock -from pixelated.controllers import SyncInfoController +from pixelated.resources import SyncInfoController from mockito import * -- cgit v1.2.3