diff options
Diffstat (limited to 'service')
22 files changed, 420 insertions, 127 deletions
@@ -125,6 +125,8 @@ elif [ "$1" == 'coverage_unit' ]; then elif [ "$1" == 'coverage_integration' ]; then runCoverageIntegration "${@:2}" elif [ "$1" == 'coverage_all' ]; then + set -e + runPep8 runCoverageUnitAndIntegration "${@:2}" elif [ "$1" == 'start' ]; then /usr/bin/env pixelated-user-agent "${@:2}" diff --git a/service/pixelated/adapter/model/mail.py b/service/pixelated/adapter/model/mail.py index 8e7b745c..b0b56fec 100644 --- a/service/pixelated/adapter/model/mail.py +++ b/service/pixelated/adapter/model/mail.py @@ -341,7 +341,7 @@ class PixelatedMail(Mail): 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. Subject %s' % self.hdoc.content.get('subject', None)) + logger.warning('Encountered a mail with missing date and received header fields. ID %s' % self.fdoc.content.get('uid', None)) date = pixelated.support.date.iso_now() return dateparser.parse(date).isoformat() except (ValueError, TypeError) as e: diff --git a/service/pixelated/assets/welcome.mail b/service/pixelated/assets/welcome.mail index 346bfaf6..e85694f1 100644 --- a/service/pixelated/assets/welcome.mail +++ b/service/pixelated/assets/welcome.mail @@ -19,10 +19,12 @@ To compose a message look for the big blue button on the top left. You can add t A bit more about Pixelated Pixelated is an open source project licensed under AGPL 3.0. It is composed of 3 main parts, the User Agent (what you are using right now), the Dispatcher (what allows you to log in with different accounts to the same instance) and the Platform (which provides the email service you will use to send and receive messages - the server behind the @ sign on your new mail address). You can learn more by visiting https://pixelated-project.org/. -About this message and understanding examples of message status -This message was not encrypted, in other words, it could have been read by others at some point during transmission. -However you can check the authenticity of this message, because the message has a certified sender. -Whether it is green, it means that the text you are reading has not changed on the way until delivered to you. +About this message and encryption status +This message was not encrypted, in other words, it could have been read by others at some point during transmission, like any other email client. +To send encrypted messages you have to have the public keys of the recipients, in Pixelated you have 2 options: +send an email to another Pixelated account: public keys are exchanged by default. +send an email to another email provider: you should had exchanged public keys with the recipients previously. + Enjoy your secure messaging! @@ -60,10 +62,11 @@ s). You can learn more by visiting <a src=3D"https://pixelated-project.org/= ">https://pixelated-project.org/</a>. </p> <p> -<b>About this message and understanding examples of message status</b><br> -This message was not encrypted, in other words, it could have been read by others at some point during transmission. -However you can check the authenticity of this message, because the message has a certified sender. -Whether it is green, it means that the text you are reading has not changed on the way until delivered to you. +<b>About this message and encryption status</b><br> +This message was not encrypted, in other words, it could have been read by others at some point during transmission like any other email client. +To send encrypted messages you have to have the public Keys of the recipients, in Pixelated you have 2 options: +<p>- sending an email to another Pixelated account: public keys are exchanged by default. +<br>- sending an email to another email provider: you should had exchanged public keys with the recipients previously. </p> <p> Enjoy your secure messaging! diff --git a/service/pixelated/config/__init__.py b/service/pixelated/config/__init__.py index af264c77..f59d684b 100644 --- a/service/pixelated/config/__init__.py +++ b/service/pixelated/config/__init__.py @@ -36,8 +36,10 @@ import pixelated.support.ext_protobuf import pixelated.support.ext_sqlcipher import pixelated.support.ext_esmtp_sender_factory import pixelated.support.ext_fetch +import pixelated.support.ext_sync import pixelated.support.ext_keymanager_fetch_key import pixelated.support.ext_requests_urllib3 +from pixelated.support.error_handler import error_handler def initialize(): @@ -74,6 +76,7 @@ def initialize(): d = deferToThread(init_soledad) d.addCallback(stop_loading_app) + d.addErrback(error_handler) reactor.callWhenRunning(load_app) reactor.run() diff --git a/service/pixelated/config/app_factory.py b/service/pixelated/config/app_factory.py index 5dcf60cb..51f76741 100644 --- a/service/pixelated/config/app_factory.py +++ b/service/pixelated/config/app_factory.py @@ -53,9 +53,8 @@ def init_index_and_remove_dupes(querier, search_engine, mail_service): return wrapper -def update_info_sync_and_index_partial(sync_info_controller, search_engine, mail_service): +def update_index_partial(search_engine, mail_service): def wrapper(soledad_sync_status): - sync_info_controller.set_sync_info(soledad_sync_status) search_engine.index_mails(mails=mail_service.all_mails()) return wrapper diff --git a/service/pixelated/config/args.py b/service/pixelated/config/args.py index d3284fab..a5d19369 100644 --- a/service/pixelated/config/args.py +++ b/service/pixelated/config/args.py @@ -20,18 +20,24 @@ from pixelated.bitmask_libraries.config import DEFAULT_LEAP_HOME def parse(): parser = argparse.ArgumentParser(description='Pixelated user agent.') - parser.add_argument('--debug', action='store_true', help='DEBUG mode.') - parser.add_argument('--dispatcher', help='run in organization mode, the credentials will be read from specified file', metavar='file') - parser.add_argument('--dispatcher-stdin', help='run in organization mode, the credentials will be read from stdin', default=False, action='store_true', dest='dispatcher_stdin') + + parser_add_default_arguments(parser) + parser.add_argument('--host', default='127.0.0.1', help='the host to run the user agent on') parser.add_argument('--port', type=int, default=3333, help='the port to run the user agent on') - parser.add_argument('--home', help='The folder where the user agent stores its data. Defaults to ~/.leap', default=DEFAULT_LEAP_HOME) - parser.add_argument('-c', '--config', metavar='<configfile>', default=None, help='use specified file for credentials (for test purposes only)') parser.add_argument('-sk', '--sslkey', metavar='<server.key>', default=None, help='use specified file as web server\'s SSL key (when using the user-agent together with the pixelated-dispatcher)') parser.add_argument('-sc', '--sslcert', metavar='<server.crt>', default=None, help='use specified file as web server\'s SSL certificate (when using the user-agent together with the pixelated-dispatcher)') - parser.add_argument('-lc', '--leap-cert', metavar='<leap.crt>', default=None, help='use specified file for LEAP cert authority certificate (url https://<provider-domain>/ca.crt)') - parser.add_argument('--leap-cert-fingerprint', metavar='<leap certificate fingerprint>', default=None, help='use specified fingerprint to validate connection with leap provider', dest='leap_cert_fingerprint') parser.add_argument('--register', metavar=('provider', 'username'), - nargs=2, help='register a new username on the desired provider') + nargs=2, help='register a new username on the desired LEAP provider') args = parser.parse_args() return args + + +def parser_add_default_arguments(parser): + parser.add_argument('--debug', action='store_true', help='DEBUG mode.') + parser.add_argument('--dispatcher', help='run in organization mode, the credentials will be read from specified file', metavar='file') + parser.add_argument('--dispatcher-stdin', help='run in organization mode, the credentials will be read from stdin', default=False, action='store_true', dest='dispatcher_stdin') + parser.add_argument('-c', '--config', metavar='<configfile>', default=None, help='use specified file for credentials (for test purposes only)') + parser.add_argument('--home', help='The folder where the user agent stores its data. Defaults to ~/.leap', default=DEFAULT_LEAP_HOME) + parser.add_argument('-lc', '--leap-provider-cert', metavar='<leap-provider.crt>', default=None, help='use specified file for LEAP provider cert authority certificate (url https://<LEAP-provider-domain>/ca.crt)') + parser.add_argument('-lf', '--leap-provider-cert-fingerprint', metavar='<leap provider certificate fingerprint>', default=None, help='use specified fingerprint to validate connection with LEAP provider', dest='leap_provider_cert_fingerprint') diff --git a/service/pixelated/config/leap_cert.py b/service/pixelated/config/leap_cert.py index 22f73720..568f76d9 100644 --- a/service/pixelated/config/leap_cert.py +++ b/service/pixelated/config/leap_cert.py @@ -18,9 +18,9 @@ import pixelated.bitmask_libraries.certs as certs def init_leap_cert(args): - if args.leap_cert_fingerprint is None: - certs.LEAP_CERT = args.leap_cert or True + if args.leap_provider_cert_fingerprint is None: + certs.LEAP_CERT = args.leap_provider_cert or True certs.LEAP_FINGERPRINT = None else: - certs.LEAP_FINGERPRINT = args.leap_cert_fingerprint + certs.LEAP_FINGERPRINT = args.leap_provider_cert_fingerprint certs.LEAP_CERT = False diff --git a/service/pixelated/config/register.py b/service/pixelated/config/register.py index 3f93aa8d..d54b10ff 100644 --- a/service/pixelated/config/register.py +++ b/service/pixelated/config/register.py @@ -13,6 +13,7 @@ # # 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 re from pixelated.bitmask_libraries.leap_srp import LeapAuthException from pixelated.bitmask_libraries.register import register_new_user @@ -20,6 +21,15 @@ from pixelated.bitmask_libraries.register import register_new_user def register(username, server_name): try: + validate_username(username) register_new_user(username, server_name) except LeapAuthException: print('User already exists') + except ValueError: + print('Only lowercase letters, digits, . - and _ allowed.') + + +def validate_username(username): + accepted_characters = '^[a-z0-9\-\_\.]*$' + if not re.match(accepted_characters, username): + raise ValueError diff --git a/service/pixelated/maintenance.py b/service/pixelated/maintenance.py new file mode 100644 index 00000000..8c20e097 --- /dev/null +++ b/service/pixelated/maintenance.py @@ -0,0 +1,169 @@ +# +# 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/>. + +import os +from functools import partial +import sys +import json +import argparse +import email + +from os.path import join +from mailbox import mboxMessage +from pixelated.config.app import App +from pixelated.config import app_factory +from pixelated.config.args import parser_add_default_arguments +from pixelated.config.config_ua import config_user_agent +from pixelated.config.dispatcher import config_dispatcher +from pixelated.config.events_server import init_events_server +from pixelated.config.loading_page import loading +from pixelated.config.register import register +from pixelated.config.logging_setup import init_logging +from pixelated.config.leap_cert import init_leap_cert +from pixelated.config.soledad import init_soledad_and_user_key +from twisted.internet import reactor, defer +from twisted.internet.threads import deferToThread + +from leap.mail.imap.memorystore import MemoryStore +from leap.mail.imap.soledadstore import SoledadStore +from leap.common.events import register, unregister, events_pb2 as proto + +# monkey patching some specifics +import pixelated.support.ext_protobuf +import pixelated.support.ext_sqlcipher +import pixelated.support.ext_esmtp_sender_factory +import pixelated.support.ext_fetch +import pixelated.support.ext_keymanager_fetch_key +import pixelated.support.ext_requests_urllib3 + + +def delete_all_mails(args): + leap_session, soledad = args + generation, docs = soledad.get_all_docs() + + for doc in docs: + if doc.content.get('type', None) in ['head', 'cnt', 'flags']: + soledad.delete_doc(doc) + + return args + + +@defer.inlineCallbacks +def load_mails(args, mail_paths): + leap_session, soledad = args + account = leap_session.account + + for path in mail_paths: + print 'Loading mails from %s' % path + for root, dirs, files in os.walk(path): + mbx = account.getMailbox('INBOX') + for f in files: + with open(join(root, f), 'r') as fp: + m = email.message_from_file(fp) + flags = ("\\RECENT",) + r = yield mbx.addMessage(m.as_string(), flags=flags, notify_on_disk=False) + print 'Added message %s' % m.get('subject') + print m.as_string() + + defer.returnValue(args) + return + + +def dump_soledad(args): + leap_session, soledad = args + + generation, docs = soledad.get_all_docs() + + for doc in docs: + print doc + print '\n' + + return args + + +def initialize(): + parser = argparse.ArgumentParser(description='pixelated maintenance') + parser_add_default_arguments(parser) + subparsers = parser.add_subparsers(help='commands', dest='command') + subparsers.add_parser('reset', help='reset account command') + mails_parser = subparsers.add_parser('load-mails', help='load mails into account') + mails_parser.add_argument('file', nargs='+', help='file(s) with mail data') + + subparsers.add_parser('dump-soledad', help='dump the soledad database') + subparsers.add_parser('sync', help='sync the soledad database') + + args = parser.parse_args() + app = App() + + init_logging(args) + init_leap_cert(args) + + if args.dispatcher or args.dispatcher_stdin: + config_dispatcher(app, args) + else: + config_user_agent(app, args) + + init_events_server() + + def execute_command(): + + def init_soledad(): + return init_soledad_and_user_key(app, args.home) + + def get_soledad_handle(leap_session): + soledad = leap_session.soledad_session.soledad + + return leap_session, soledad + + def soledad_sync(args): + leap_session, soledad = args + + soledad.sync() + + return args + + d = deferToThread(init_soledad) + d.addCallback(get_soledad_handle) + d.addCallback(soledad_sync) + if args.command == 'reset': + d.addCallback(delete_all_mails) + elif args.command == 'load-mails': + d.addCallback(load_mails, args.file) + elif args.command == 'dump-soledad': + d.addCallback(dump_soledad) + elif args.command == 'sync': + # nothing to do here, sync is already part of the chain + pass + else: + print 'Unsupported command: %s' % args.command + d.addCallback(soledad_sync) + d.addCallback(shutdown) + d.addErrback(shutdown_on_error) + + reactor.callWhenRunning(execute_command) + reactor.run() + + +def shutdown(args): + reactor.stop() + + +def shutdown_on_error(error): + print error + reactor.stop() + +if __name__ == 'main': + initialize() diff --git a/service/pixelated/resources/root_resource.py b/service/pixelated/resources/root_resource.py index b45bd00f..4c0c47ac 100644 --- a/service/pixelated/resources/root_resource.py +++ b/service/pixelated/resources/root_resource.py @@ -4,7 +4,6 @@ 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 pixelated.resources.keys_resource import KeysResource from twisted.web.resource import Resource @@ -28,7 +27,6 @@ class RootResource(Resource): self.putChild('attachment', 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(mail_service, draft_service)) self.putChild('mail', MailResource(mail_service)) diff --git a/service/pixelated/resources/sync_info_resource.py b/service/pixelated/resources/sync_info_resource.py deleted file mode 100644 index 791c5add..00000000 --- a/service/pixelated/resources/sync_info_resource.py +++ /dev/null @@ -1,46 +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 <http://www.gnu.org/licenses/>. -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 = [int(x) for x in 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/support/error_handler.py b/service/pixelated/support/error_handler.py new file mode 100644 index 00000000..1a0e1a11 --- /dev/null +++ b/service/pixelated/support/error_handler.py @@ -0,0 +1,27 @@ +# +# 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 requests.exceptions import SSLError + + +def error_handler(excp): + if excp.type is SSLError: + print """ + SSL Error: Please check your certificates or read our wiki for further info: + https://github.com/pixelated-project/pixelated-user-agent/wiki/Configuring-and-using-SSL-Certificates-for-LEAP-provider + Error reference: %s + """ % excp.getErrorMessage() + else: + raise excp diff --git a/service/pixelated/support/ext_sync.py b/service/pixelated/support/ext_sync.py new file mode 100644 index 00000000..d35eed3e --- /dev/null +++ b/service/pixelated/support/ext_sync.py @@ -0,0 +1,23 @@ +import leap.soledad.client as client +import logging +import urlparse +from leap.soledad.client.events import ( + SOLEDAD_DONE_DATA_SYNC, + signal +) + + +def patched_sync(self, defer_decryption=True): + if self._db: + try: + local_gen = self._db.sync( + urlparse.urljoin(self.server_url, 'user-%s' % self._uuid), + creds=self._creds, autocreate=False, + defer_decryption=defer_decryption) + signal(SOLEDAD_DONE_DATA_SYNC, self._uuid) + return local_gen + except Exception as e: + client.logger.error("Soledad exception when syncing: %s - %s" % (e.__class__.__name__, e.message)) + + +client.Soledad.sync = patched_sync diff --git a/service/setup.py b/service/setup.py index ed6be9c0..973cac47 100644 --- a/service/setup.py +++ b/service/setup.py @@ -70,7 +70,8 @@ setup(name='pixelated-user-agent', ], entry_points={ 'console_scripts': [ - 'pixelated-user-agent = pixelated.config:initialize' + 'pixelated-user-agent = pixelated.config:initialize', + 'pixelated-maintenance = pixelated.maintenance:initialize' ] }, include_package_data=True diff --git a/service/test/functional/features/forward_trash_archive.feature b/service/test/functional/features/forward_trash_archive.feature index 6e959c32..1d373b88 100644 --- a/service/test/functional/features/forward_trash_archive.feature +++ b/service/test/functional/features/forward_trash_archive.feature @@ -31,4 +31,4 @@ Feature: forward and deletion When I choose to trash # Then I see that mail under the 'trash' tag When I select the tag 'trash' - And I open the first mail in the mail list
\ No newline at end of file + And I open the first mail in the mail list diff --git a/service/test/functional/features/steps/tag_list.py b/service/test/functional/features/steps/tag_list.py index b3e09c22..443c5173 100644 --- a/service/test/functional/features/steps/tag_list.py +++ b/service/test/functional/features/steps/tag_list.py @@ -21,13 +21,13 @@ def click_first_element_with_class(context, classname): elements[0].click() -def is_side_nax_expanded(context): +def is_side_nav_expanded(context): e = context.browser.find_elements_by_class_name('content')[0].get_attribute('class').count(u'move-right') == 1 return e def expand_side_nav(context): - if is_side_nax_expanded(context): + if is_side_nav_expanded(context): return toggle = context.browser.find_elements_by_class_name('side-nav-toggle')[0] diff --git a/service/test/unit/config/test_register.py b/service/test/unit/config/test_register.py new file mode 100644 index 00000000..7db2b000 --- /dev/null +++ b/service/test/unit/config/test_register.py @@ -0,0 +1,20 @@ +import unittest + +from pixelated.config.register import validate_username + + +class TestRegister(unittest.TestCase): + + def test_username_raises_error_when_it_contains_uppercase_letters(self): + with self.assertRaises(ValueError): + validate_username('INVALIDUSERNAME') + + def test_username_raises_error_when_it_contains_special_characters(self): + with self.assertRaises(ValueError): + validate_username('invalid@username') + + def test_username_pass_when_valid(self): + try: + validate_username('a.valid_username-123') + except: + self.fail('Valid username should not raise an exception') diff --git a/service/test/unit/fixtures/mailset/mbox00000000 b/service/test/unit/fixtures/mailset/mbox00000000 new file mode 100644 index 00000000..3d01c203 --- /dev/null +++ b/service/test/unit/fixtures/mailset/mbox00000000 @@ -0,0 +1,12 @@ +From darby.senger@zemlak.biz +Subject: Itaque consequatur repellendus provident sunt quia. +To: carmel@murazikortiz.name +X-TW-Pixelated-Tags: nite, macro, trash +Date: Tue, 21 Apr 2015 08:43:27 +0000 (UTC) + +Dignissimos ducimus veritatis. Est tenetur consequatur quia occaecati. Vel sit sit voluptas. + +Earum distinctio eos. Accusantium qui sint ut quia assumenda. Facere dignissimos inventore autem sit amet. Pariatur voluptatem sint est. + +Ut recusandae praesentium aspernatur. Exercitationem amet placeat deserunt quae consequatur eum. Unde doloremque suscipit quia. + diff --git a/service/test/unit/fixtures/mailset/mbox00000001 b/service/test/unit/fixtures/mailset/mbox00000001 new file mode 100644 index 00000000..fc76bba2 --- /dev/null +++ b/service/test/unit/fixtures/mailset/mbox00000001 @@ -0,0 +1,12 @@ +From madeline.littel@sanfordruel.com +Subject: Error illum dignissimos autem eos aspernatur. +To: phyllis@stiedemann.net +X-TW-Pixelated-Tags: instadaily, inspiration +Date: Tue, 21 Apr 2015 08:43:27 +0000 (UTC) + +Et inventore placeat aut. Sint eveniet labore perferendis nulla. Maiores rerum sunt perferendis. Voluptate iure hic et ut blanditiis ad veritatis. Labore occaecati rerum. + +Sit fugiat aliquam voluptates ipsum non. Dolor quo sapiente. Itaque sed odit velit. Qui et aspernatur et fugiat voluptas eum est. Et expedita eos rerum nisi ut eum vero. + +Ab et est cumque. Qui nostrum perferendis. Labore est tempora porro est quia deleniti consequatur. Fugit quis ipsa. + diff --git a/service/test/unit/maintenance/__init__.py b/service/test/unit/maintenance/__init__.py new file mode 100644 index 00000000..2756a319 --- /dev/null +++ b/service/test/unit/maintenance/__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/test/unit/maintenance/test_commands.py b/service/test/unit/maintenance/test_commands.py new file mode 100644 index 00000000..f23655d8 --- /dev/null +++ b/service/test/unit/maintenance/test_commands.py @@ -0,0 +1,92 @@ +# +# 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/>. +import unittest +import email + +from pixelated.maintenance import delete_all_mails, load_mails +from pixelated.bitmask_libraries.session import LeapSession +from leap.mail.imap.account import SoledadBackedAccount +from leap.soledad.client import Soledad +from leap.soledad.common.document import SoledadDocument +from mock import MagicMock, ANY +from os.path import join, dirname + + +class TestCommands(unittest.TestCase): + + def setUp(self): + self.leap_session = MagicMock(spec=LeapSession) + self.soledad = MagicMock(spec=Soledad) + self.account = MagicMock(spec=SoledadBackedAccount) + self.mailbox = MagicMock() + self.leap_session.account = self.account + self.account.getMailbox.return_value = self.mailbox + + self.args = (self.leap_session, self.soledad) + + def test_delete_all_mails_supports_empty_doclist(self): + self.soledad.get_all_docs.return_value = (1, []) + + delete_all_mails(self.args) + + self.assertFalse(self.soledad.delete_doc.called) + + def test_delete_all_mails(self): + doc = MagicMock(spec=SoledadDocument) + doc.content = {'type': 'head'} + self.soledad.get_all_docs.return_value = (1, [doc]) + + delete_all_mails(self.args) + + self.soledad.delete_doc.assert_called_once_with(doc) + + def test_only_mail_documents_are_deleted(self): + docs = self._create_docs_of_type(['head', 'cnt', 'flags', 'mbx', 'foo', None]) + self.soledad.get_all_docs.return_value = (1, docs) + + delete_all_mails(self.args) + + for doc in docs: + if doc.content['type'] in ['head', 'cnt', 'flags']: + self.soledad.delete_doc.assert_any_call(doc) + self.assertEqual(3, len(self.soledad.delete_doc.mock_calls)) + + def _create_docs_of_type(self, type_list): + return [self._create_doc_type(t) for t in type_list] + + def _create_doc_type(self, doc_type): + doc = MagicMock(spec=SoledadDocument) + doc.content = {'type': doc_type} + return doc + + def test_load_mails_empty_path_list(self): + load_mails(self.args, []) + + self.assertFalse(self.mailbox.called) + + def test_load_mails_adds_mails(self): + mail_root = join(dirname(__file__), '..', 'fixtures', 'mailset') + + foo = load_mails(self.args, [mail_root]) + + self.assertTrue(self.mailbox.addMessage.called) + self.mailbox.addMessage.assert_any_call(self._mail_content(join(mail_root, 'mbox00000000')), flags=("\\RECENT",), notify_on_disk=False) + self.mailbox.addMessage.assert_any_call(self._mail_content(join(mail_root, 'mbox00000001')), flags=("\\RECENT",), notify_on_disk=False) + + def _mail_content(self, mail_file): + with open(mail_file, 'r') as fp: + m = email.message_from_file(fp) + return m.as_string() diff --git a/service/test/unit/resources/test_sync_info_controller.py b/service/test/unit/resources/test_sync_info_controller.py deleted file mode 100644 index 1285237b..00000000 --- a/service/test/unit/resources/test_sync_info_controller.py +++ /dev/null @@ -1,53 +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 <http://www.gnu.org/licenses/>. -import unittest -import json - -from test.support.test_helper import request_mock -from pixelated.resources.sync_info_resource import SyncInfoResource -from mockito import mock - - -class SyncInfoResourceTest(unittest.TestCase): - - def setUp(self): - self.dummy_request = request_mock() - self.controller = SyncInfoResource() - - def _set_count(self, current, total): - soledad_sync_data = mock() - soledad_sync_data.content = "%s/%s" % (current, total) - self.controller.set_sync_info(soledad_sync_data) - - def get_sync_info(self): - return json.loads(self.controller.render_GET(self.dummy_request)) - - def test_is_not_syncing_if_total_is_equal_to_current(self): - self._set_count(total=0, current=0) - - sync_info = self.get_sync_info() - - self.assertFalse(sync_info['is_syncing']) - - def test_is_syncing_if_total_is_not_equal_to_current_and_adds_count(self): - self._set_count(total=10, current=5) - - sync_info = self.get_sync_info() - - self.assertTrue(sync_info['is_syncing']) - self.assertEquals(5, sync_info['count']['current']) - self.assertEquals(10, sync_info['count']['total']) - self.assertEquals(0.5, sync_info['count']['progress']) |