diff options
| -rw-r--r-- | service/pixelated/adapter/search.py | 34 | ||||
| -rw-r--r-- | service/pixelated/config/app_factory.py | 32 | ||||
| -rw-r--r-- | service/pixelated/config/routes.py | 26 | ||||
| -rw-r--r-- | service/pixelated/controllers/__init__.py | 1 | ||||
| -rw-r--r-- | service/pixelated/controllers/contacts_controller.py | 31 | ||||
| -rw-r--r-- | service/pixelated/controllers/features_controller.py | 2 | ||||
| -rw-r--r-- | service/test/integration/contacts_test.py | 22 | ||||
| -rw-r--r-- | service/test/support/integration/app_test_client.py | 8 | ||||
| -rw-r--r-- | service/test/support/integration/soledad_test_base.py | 4 | ||||
| -rw-r--r-- | web-ui/app/js/mail_view/ui/recipients/recipients_input.js | 16 | 
10 files changed, 123 insertions, 53 deletions
diff --git a/service/pixelated/adapter/search.py b/service/pixelated/adapter/search.py index 6c62ccc3..fd4e7c36 100644 --- a/service/pixelated/adapter/search.py +++ b/service/pixelated/adapter/search.py @@ -13,10 +13,12 @@  #  # 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.support.encrypted_file_storage import EncryptedFileStorage  import os  from pixelated.adapter.status import Status +from pixelated.support.functional import flatten  from whoosh.index import FileIndex  from whoosh.fields import *  from whoosh.qparser import QueryParser @@ -44,7 +46,8 @@ class SearchEngine(object):                  continue              if not tags.get(tag): -                tags[tag] = {'ident': tag, 'name': tag, 'default': False, 'counts': {'total': 0, 'read': 0}, 'mails': []} +                tags[tag] = {'ident': tag, 'name': tag, 'default': False, 'counts': {'total': 0, 'read': 0}, +                             'mails': []}              tags[tag]['counts'][count_type] += count      def _search_tag_groups(self, is_filtering_tags): @@ -91,9 +94,9 @@ class SearchEngine(object):          return Schema(              ident=ID(stored=True, unique=True),              sender=ID(stored=False), -            to=ID(stored=False), -            cc=ID(stored=False), -            bcc=ID(stored=False), +            to=KEYWORD(stored=False, commas=True), +            cc=KEYWORD(stored=False, commas=True), +            bcc=KEYWORD(stored=False, commas=True),              subject=TEXT(stored=False),              date=NUMERIC(stored=False, sortable=True, bits=64, signed=False),              body=TEXT(stored=False), @@ -119,9 +122,9 @@ class SearchEngine(object):              'sender': unicode(header.get('from', '')),              'subject': unicode(header.get('subject', '')),              'date': milliseconds(header.get('date', '')), -            'to': unicode(header.get('to', '')), -            'cc': unicode(header.get('cc', '')), -            'bcc': unicode(header.get('bcc', '')), +            'to': u','.join(header.get('to', [''])), +            'cc': u','.join(header.get('cc', [''])), +            'bcc': u','.join(header.get('bcc', [''])),              'tag': u','.join(unique(tags)),              'body': unicode(mdict['body']),              'ident': unicode(mdict['ident']), @@ -179,3 +182,20 @@ class SearchEngine(object):              writer.delete_by_term('ident', mail_id)          finally:              writer.commit() + +    def contacts(self, query): +        if query: +            to = QueryParser('to', self._index.schema) +            cc = QueryParser('cc', self._index.schema) +            bcc = QueryParser('bcc', self._index.schema) +            with self._index.searcher() as searcher: +                to = searcher.search(to.parse("*%s*" % query), limit=None, +                                     groupedby=sorting.FieldFacet('to', allow_overlap=True)).groups() +                cc = searcher.search(cc.parse("*%s*" % query), limit=None, +                                     groupedby=sorting.FieldFacet('cc', allow_overlap=True)).groups() +                bcc = searcher.search(bcc.parse("*%s*" % query), limit=None, +                                      groupedby=sorting.FieldFacet('bcc', allow_overlap=True)).groups() +                return flatten([to, cc, bcc]) + +        return [] + diff --git a/service/pixelated/config/app_factory.py b/service/pixelated/config/app_factory.py index 497ab205..eb31a271 100644 --- a/service/pixelated/config/app_factory.py +++ b/service/pixelated/config/app_factory.py @@ -16,6 +16,7 @@  import sys  from twisted.internet import reactor +from pixelated.config.routes import setup_routes  from pixelated.adapter.mail_service import MailService  from pixelated.adapter.mail import InputMail  from pixelated.adapter.mail_sender import MailSender @@ -57,32 +58,6 @@ def update_info_sync_and_index_partial(sync_info_controller, search_engine, mail      return wrapper -def _setup_routes(app, home_controller, mails_controller, tags_controller, features_controller, sync_info_controller, -                  attachments_controller): -    # mails -    app.route('/mails', methods=['GET'])(mails_controller.mails) -    app.route('/mail/<mail_id>/read', methods=['POST'])(mails_controller.mark_mail_as_read) -    app.route('/mail/<mail_id>/unread', methods=['POST'])(mails_controller.mark_mail_as_unread) -    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/<mail_id>', methods=['GET'])(mails_controller.mail) -    app.route('/mail/<mail_id>', methods=['DELETE'])(mails_controller.delete_mail) -    app.route('/mails', methods=['DELETE'])(mails_controller.delete_mails) -    app.route('/mails', methods=['POST'])(mails_controller.send_mail) -    app.route('/mail/<mail_id>/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) -    # 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/<attachment_id>', methods=['GET'])(attachments_controller.attachment) -    # static -    app.route('/', methods=['GET'], branch=True)(home_controller.home) - -  def init_leap_session(app):      try:          leap_session = LeapSession.open(app.config['LEAP_USERNAME'], @@ -126,6 +101,7 @@ def init_app(app):                                         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) @@ -141,8 +117,8 @@ def init_app(app):      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) +    setup_routes(app, home_controller, mails_controller, tags_controller, features_controller, +                 sync_info_controller, attachments_controller, contacts_controller)  def create_app(app, bind_address, bind_port): diff --git a/service/pixelated/config/routes.py b/service/pixelated/config/routes.py new file mode 100644 index 00000000..6bad8a17 --- /dev/null +++ b/service/pixelated/config/routes.py @@ -0,0 +1,26 @@ +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('/mail/<mail_id>/read', methods=['POST'])(mails_controller.mark_mail_as_read) +    app.route('/mail/<mail_id>/unread', methods=['POST'])(mails_controller.mark_mail_as_unread) +    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/<mail_id>', methods=['GET'])(mails_controller.mail) +    app.route('/mail/<mail_id>', methods=['DELETE'])(mails_controller.delete_mail) +    app.route('/mails', methods=['DELETE'])(mails_controller.delete_mails) +    app.route('/mails', methods=['POST'])(mails_controller.send_mail) +    app.route('/mail/<mail_id>/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/<attachment_id>', methods=['GET'])(attachments_controller.attachment) +    # static +    app.route('/', methods=['GET'], branch=True)(home_controller.home)
\ No newline at end of file diff --git a/service/pixelated/controllers/__init__.py b/service/pixelated/controllers/__init__.py index e1c13515..33da02b8 100644 --- a/service/pixelated/controllers/__init__.py +++ b/service/pixelated/controllers/__init__.py @@ -37,3 +37,4 @@ 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/contacts_controller.py b/service/pixelated/controllers/contacts_controller.py new file mode 100644 index 00000000..f9b0628b --- /dev/null +++ b/service/pixelated/controllers/contacts_controller.py @@ -0,0 +1,31 @@ +# +# 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.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
\ No newline at end of file diff --git a/service/pixelated/controllers/features_controller.py b/service/pixelated/controllers/features_controller.py index 4d375683..f3fa9dff 100644 --- a/service/pixelated/controllers/features_controller.py +++ b/service/pixelated/controllers/features_controller.py @@ -19,7 +19,7 @@ import os  class FeaturesController: -    DISABLED_FEATURES = ['draftReply', 'signatureStatus', 'encryptionStatus', 'contacts'] +    DISABLED_FEATURES = ['draftReply', 'signatureStatus', 'encryptionStatus']      def __init__(self):          pass diff --git a/service/test/integration/contacts_test.py b/service/test/integration/contacts_test.py new file mode 100644 index 00000000..b1db485f --- /dev/null +++ b/service/test/integration/contacts_test.py @@ -0,0 +1,22 @@ +from nose.twistedtools import deferred +from test.support.integration import SoledadTestBase, MailBuilder + + +class SearchTest(SoledadTestBase): + +    def setUp(self): +        SoledadTestBase.setUp(self) + +    @deferred(timeout=SoledadTestBase.DEFERRED_TIMEOUT) +    def test_that_tags_returns_all_tags(self): +        input_mail = MailBuilder().with_tags(['important']).build_input_mail() +        self.client.add_mail_to_inbox(input_mail) + +        d = self.get_contacts(query='recipient') + +        def _assert(contacts): +            self.assertTrue('recipient@to.com' in contacts) +            self.assertTrue('recipient@cc.com' in contacts) +            self.assertTrue('recipient@bcc.com' in contacts) +        d.addCallback(_assert) +        return d
\ No newline at end of file diff --git a/service/test/support/integration/app_test_client.py b/service/test/support/integration/app_test_client.py index dea59399..229d32d6 100644 --- a/service/test/support/integration/app_test_client.py +++ b/service/test/support/integration/app_test_client.py @@ -17,6 +17,7 @@ import json  import multiprocessing  import shutil  import time +from pixelated.config.routes import setup_routes  from klein.test_resource import requestMock, _render  from leap.mail.imap.account import SoledadBackedAccount @@ -30,7 +31,7 @@ from pixelated.adapter.soledad_querier import SoledadQuerier  from pixelated.adapter.tag_service import TagService  from pixelated.config import app_factory  from pixelated.controllers import FeaturesController, HomeController, MailsController, TagsController, \ -    SyncInfoController, AttachmentsController +    SyncInfoController, AttachmentsController, ContactsController  import pixelated.runserver  from pixelated.adapter.mail import PixelatedMail  from pixelated.adapter.search import SearchEngine @@ -70,11 +71,12 @@ class AppTestClient:                                             draft_service=self.draft_service,                                             search_engine=self.search_engine)          tags_controller = TagsController(search_engine=self.search_engine) +        contacts_controller = ContactsController(search_engine=self.search_engine)          sync_info_controller = SyncInfoController()          attachments_controller = AttachmentsController(self.soledad_querier) -        app_factory._setup_routes(self.app, home_controller, mails_controller, tags_controller, -                                  features_controller, sync_info_controller, attachments_controller) +        setup_routes(self.app, home_controller, mails_controller, tags_controller, +                                  features_controller, sync_info_controller, attachments_controller, contacts_controller)      def _render(self, request, as_json=True):          def get_request_written_data(_=None): diff --git a/service/test/support/integration/soledad_test_base.py b/service/test/support/integration/soledad_test_base.py index 2221679f..a9663144 100644 --- a/service/test/support/integration/soledad_test_base.py +++ b/service/test/support/integration/soledad_test_base.py @@ -81,3 +81,7 @@ class SoledadTestBase(unittest.TestCase):      def mark_many_as_read(self, idents):          res, req = self.client.post('/mails/read', json.dumps({'idents': idents}))          return req + +    def get_contacts(self, query): +        res, req = self.client.get('/contacts', get_args={'q': query}) +        return res
\ No newline at end of file diff --git a/web-ui/app/js/mail_view/ui/recipients/recipients_input.js b/web-ui/app/js/mail_view/ui/recipients/recipients_input.js index 8f647d01..78114a39 100644 --- a/web-ui/app/js/mail_view/ui/recipients/recipients_input.js +++ b/web-ui/app/js/mail_view/ui/recipients/recipients_input.js @@ -44,17 +44,7 @@ define([          self;        var extractContactNames = function (response) { -        return _.flatten(response.contacts, function (contact) { -          var filterCriteria = contact.name ? -            function (e) { -              return { value: contact.name + ' <' + e + '>' }; -            } : -            function (e) { -              return { value: e }; -            }; - -          return _.map(contact.addresses, filterCriteria); -        }); +          return _.map(response, function(a) { return { value: a } });        };        function createEmailCompleter() { @@ -70,9 +60,7 @@ define([              filter: extractContactNames            }          }); -        if (features.isEnabled('contacts')) { -          emailCompleter.initialize(); -        } +        emailCompleter.initialize();          return emailCompleter;        }  | 
