diff options
37 files changed, 509 insertions, 314 deletions
@@ -33,4 +33,5 @@ __pycache__/ credentials.ini pixelated.cfg service/_trial_temp/ +_trial_temp web-ui/coverage @@ -12,14 +12,24 @@ Here's a [podcast](https://soundcloud.com/thoughtworks/pixelated-why-secure-comm  +## Try it! + +If you are curious, have a look at the pixelated web client, it as simple as clicking *[here!](https://try.pixelated-project.org:8080/)* + ## Getting started You are most welcome to contribute to the pixelated user agent code base. Please have a look at the [contributions how to](https://github.com/pixelated/pixelated-user-agent/blob/master/CONTRIBUTING.md). +## Installing Pixelated + +You like the idea and you want to run it locally, then before you have to install the following packages: -1) Install [Vagrant](https://www.vagrantup.com/downloads.html) and a vagrant [compatible provider](https://www.vagrantup.com/docs/providers/), e.g. [Virtual Box](https://www.virtualbox.org/wiki/Downloads). Vagrant is a tool that automates the setup of a virtual machine with the development environment in your computer. Inside the virtual machine's filesystem, this repository will be automatically mounted in the `/vagrant` folder. +* [Vagrant](https://www.vagrantup.com/downloads.html), Vagrant is a tool that automates the setup of a virtual machine with the development environment +* A vagrant [compatible provider](https://www.vagrantup.com/docs/providers/), e.g. [Virtual Box](https://www.virtualbox.org/wiki/Downloads). -2) Clone the repo and start the virtual machine (downloads 600MB): +### Option 1: Pixelated User Agent without LEAP provider + +1) Clone the repo and start the virtual machine (downloads 600MB, you may want get a coffee or tea in the meantime): ``` $ git clone https://github.com/pixelated/pixelated-user-agent.git @@ -27,15 +37,15 @@ $ cd pixelated-user-agent $ vagrant up ``` -3) Log into the VM: +2) Log into the VM: ``` $ vagrant ssh ``` -4) Register with a LEAP provider. You can create a developer account at our [Dev Provider](https://dev.pixelated-project.org/). Please contact us at team@pixelated-project.org for an invite code. +3) Register with a LEAP provider. You can create a developer account at our [Dev Provider](https://dev.pixelated-project.org/). Please contact us at team@pixelated-project.org for an invite code. -5) Run the user agent: +4) Run the user agent: Please note: If you don't have an account on any provider, go directly to step 6b). @@ -52,11 +62,11 @@ Type your password: ******** (the one you created in previous step) ``` -6a) Connect to the provider using your credentials, as shown in step 5 above. If the user agent starts up successfully, you will not see any other output. +5a) Connect to the provider using your credentials, as shown in step 5 above. If the user agent starts up successfully, you will not see any other output. **Note**: For more convenience during development, you can also create a config file with your credentials (see **Further Notes**). -6b) If you don't have a `dev.pixelated-project.org` account or just want to connect to our `try.pixelated-project.org` environment, we have some sample configurations for you. +5b) If you don't have a `dev.pixelated-project.org` account or just want to connect to our `try.pixelated-project.org` environment, we have some sample configurations for you. Please navigate to the project root in your vagrant box with: `$ cd /vagrant` @@ -65,9 +75,9 @@ Then you can connect to `try.pixelated-project.org` ... * as Alice via: `$ pixelated-user-agent --host 0.0.0.0 -c try.alice.ini` * as Bob via: `$ pixelated-user-agent --host 0.0.0.0 -c try.bob.ini` -7) Go to [localhost:3333](http://localhost:3333/). You should see a loading screen for a few seconds, then your inbox. If it sticks on the loading screen, check your terminal for errors, then [get help](https://pixelated-project.org/faq/#contact-the-project). +6) Go to [localhost:3333](http://localhost:3333/). You should see a loading screen for a few seconds, then your inbox. If it sticks on the loading screen, check your terminal for errors, then [get help](https://pixelated-project.org/faq/#contact-the-project). -8) If you like console output, you can also run the tests to see if everything went according to plan. +7) If you like console output, you can also run the tests to see if everything went according to plan. ```bash (user-agent-venv)vagrant@jessie:~$ cd /vagrant @@ -97,8 +107,19 @@ To run the functional tests: (user-agent-venv)vagrant@jessie:/vagrant/service$ cd .. ``` -9) You're all set! We've prepared [a couple of issues labeled "Volunteer Task"](https://github.com/pixelated/pixelated-user-agent/labels/Volunteer%20task) that are a good place to dive into the project. Happy Hacking! +7) You're all set! We've prepared [a couple of issues labeled "Volunteer Task"](https://github.com/pixelated/pixelated-user-agent/labels/Volunteer%20task) that are a good place to dive into the project. Happy Hacking! + +## Option 2: Pixelated User Agent + Leap Platform + +You can install the Pixelated User Agent and the Leap Platform at once, just by running the following command on your console (this may take a while, please be patient): + +```bash + curl https://raw.githubusercontent.com/pixelated/puppet-pixelated/master/vagrant_platform.sh | sh +``` + + Once installed, you can create accounts by visiting the LEAP Webapp at [localhost:4443/signup](https://localhost:4443/signup) and see Pixelated in action at [localhost:8080](https://localhost:8080/). + NOTE: Be aware that you will not be able to send mails outside, but you can test sending mails internally from one user to another. ## Running tests inside your local IDE diff --git a/_trial_temp/_trial_marker b/_trial_temp/_trial_marker deleted file mode 100755 index e69de29b..00000000 --- a/_trial_temp/_trial_marker +++ /dev/null diff --git a/service/pixelated/adapter/listeners/mailbox_indexer_listener.py b/service/pixelated/adapter/listeners/mailbox_indexer_listener.py index 74b4f5af..97a887f4 100644 --- a/service/pixelated/adapter/listeners/mailbox_indexer_listener.py +++ b/service/pixelated/adapter/listeners/mailbox_indexer_listener.py @@ -27,9 +27,8 @@ class MailboxIndexerListener(object): @defer.inlineCallbacks def listen(cls, account, mailbox_name, mail_store, search_engine): listener = MailboxIndexerListener(mailbox_name, mail_store, search_engine) - if listener not in (yield account.getMailbox(mailbox_name)).listeners: - mbx = yield account.getMailbox(mailbox_name) - mbx.addListener(listener) + mail_collection = yield account.get_collection_by_mailbox(mailbox_name) + mail_collection.addListener(listener) defer.returnValue(listener) @@ -39,7 +38,7 @@ class MailboxIndexerListener(object): self.search_engine = search_engine @defer.inlineCallbacks - def newMessages(self, exists, recent): + def notify_new(self): try: indexed_idents = set(self.search_engine.search('tag:' + self.mailbox_name.lower(), all_mails=True)) soledad_idents = yield self.mail_store.get_mailbox_mail_ids(self.mailbox_name) @@ -63,6 +62,6 @@ class MailboxIndexerListener(object): @defer.inlineCallbacks def listen_all_mailboxes(account, search_engine, mail_store): - mailboxes = yield account.account.list_all_mailbox_names() + mailboxes = yield account.list_all_mailbox_names() for mailbox_name in mailboxes: yield MailboxIndexerListener.listen(account, mailbox_name, mail_store, search_engine) diff --git a/service/pixelated/bitmask_libraries/session.py b/service/pixelated/bitmask_libraries/session.py index ae3eb992..d575a9c6 100644 --- a/service/pixelated/bitmask_libraries/session.py +++ b/service/pixelated/bitmask_libraries/session.py @@ -24,7 +24,7 @@ from twisted.internet import reactor, defer from pixelated.bitmask_libraries.certs import LeapCertificate from pixelated.adapter.mailstore import LeapMailStore from leap.mail.incoming.service import IncomingMail -from leap.mail.imap.account import IMAPAccount +from leap.mail.mail import Account from leap.auth import SRPAuth from .nicknym import NickNym from .smtp import LeapSMTPConfig @@ -73,7 +73,7 @@ class LeapSession(object): @defer.inlineCallbacks def after_first_sync(self): yield self.nicknym.generate_openpgp_key() - self.account = self._create_account(self.account_email, self.soledad) + yield self._create_account(self.soledad) self.incoming_mail_fetcher = yield self._create_incoming_mail_fetcher( self.nicknym, self.soledad, @@ -81,8 +81,9 @@ class LeapSession(object): self.account_email()) reactor.callFromThread(self.incoming_mail_fetcher.startService) - def _create_account(self, user_mail, soledad): - return IMAPAccount(user_mail, soledad, defer.Deferred()) + def _create_account(self, soledad): + self.account = Account(soledad) + return self.account.deferred_initialization def _set_fresh_account(self, event, email_address): log.debug('Key for email %s has been generated' % email_address) @@ -115,10 +116,10 @@ class LeapSession(object): @defer.inlineCallbacks def _create_incoming_mail_fetcher(self, nicknym, soledad, account, user_mail): - inbox = yield account.callWhenReady(lambda _: account.getMailbox('INBOX')) + inbox = yield account.callWhenReady(lambda _: account.get_collection_by_mailbox('INBOX')) defer.returnValue(IncomingMail(nicknym.keymanager, soledad, - inbox.collection, + inbox, user_mail)) def stop_background_jobs(self): diff --git a/service/pixelated/resources/__init__.py b/service/pixelated/resources/__init__.py index 469c8bc8..77425cc5 100644 --- a/service/pixelated/resources/__init__.py +++ b/service/pixelated/resources/__init__.py @@ -15,6 +15,7 @@ # along with Pixelated. If not, see <http://www.gnu.org/licenses/>. import json +import logging from twisted.web.http import UNAUTHORIZED from twisted.web.resource import Resource @@ -23,6 +24,9 @@ from twisted.web.resource import Resource from pixelated.resources.session import IPixelatedSession from pixelated.support import log_time +from twisted.web.http import INTERNAL_SERVER_ERROR +log = logging.getLogger(__name__) + class SetEncoder(json.JSONEncoder): def default(self, obj): @@ -47,7 +51,17 @@ def respond_json_deferred(entity, request, status_code=200): request.finish() -class BaseResource(Resource): +class GenericDeferredErrorHandler(object): + + @classmethod + def generic_error_handling(cls, e, request): + log.error(e) + request.setResponseCode(INTERNAL_SERVER_ERROR) + request.write('Something went wrong!') + request.finish() + + +class BaseResource(Resource, GenericDeferredErrorHandler): def __init__(self, services_factory): Resource.__init__(self) diff --git a/service/pixelated/resources/login_resource.py b/service/pixelated/resources/login_resource.py index aca266cf..13a78fbc 100644 --- a/service/pixelated/resources/login_resource.py +++ b/service/pixelated/resources/login_resource.py @@ -141,6 +141,7 @@ class LoginResource(BaseResource): d = self._handle_login(request) d.addCallbacks(render_response, render_error) + d.addErrback(self.generic_error_handling, request) return NOT_DONE_YET diff --git a/service/pixelated/resources/logout_resource.py b/service/pixelated/resources/logout_resource.py index 01092b05..c22815ce 100644 --- a/service/pixelated/resources/logout_resource.py +++ b/service/pixelated/resources/logout_resource.py @@ -1,9 +1,24 @@ -from twisted.web.server import NOT_DONE_YET +# +# 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 BaseResource -from twisted.web import util from twisted.internet import defer +from twisted.web import util +from twisted.web.server import NOT_DONE_YET +from pixelated.resources import BaseResource from pixelated.resources.login_resource import LoginResource @@ -25,5 +40,6 @@ class LogoutResource(BaseResource): d = self._execute_logout(request) d.addCallback(_redirect_to_login) + d.addErrback(self.generic_error_handling, request) return NOT_DONE_YET diff --git a/service/pixelated/resources/mail_resource.py b/service/pixelated/resources/mail_resource.py index 37fceb9b..6f9ec828 100644 --- a/service/pixelated/resources/mail_resource.py +++ b/service/pixelated/resources/mail_resource.py @@ -4,7 +4,7 @@ from twisted.python.log import err from twisted.web.resource import Resource from twisted.web.server import NOT_DONE_YET -from pixelated.resources import respond_json_deferred, BaseResource +from pixelated.resources import respond_json_deferred, BaseResource, GenericDeferredErrorHandler from pixelated.support import replier @@ -30,7 +30,7 @@ class MailTags(Resource): return NOT_DONE_YET -class Mail(Resource): +class Mail(Resource, GenericDeferredErrorHandler): def __init__(self, mail_id, mail_service): Resource.__init__(self) @@ -51,6 +51,8 @@ class Mail(Resource): d = self._mail_service.mail(self._mail_id) d.addCallback(lambda mail: populate_reply(mail)) d.addCallback(lambda mail_dict: respond_json_deferred(mail_dict, request)) + d.addErrback(self.generic_error_handling, request) + return NOT_DONE_YET def render_DELETE(self, request): diff --git a/service/pixelated/resources/tags_resource.py b/service/pixelated/resources/tags_resource.py index 6d4b7335..26da9dae 100644 --- a/service/pixelated/resources/tags_resource.py +++ b/service/pixelated/resources/tags_resource.py @@ -16,7 +16,6 @@ from pixelated.resources import respond_json_deferred, BaseResource from twisted.internet.threads import deferToThread -from twisted.web.resource import Resource from twisted.web.server import NOT_DONE_YET @@ -34,5 +33,6 @@ class TagsResource(BaseResource): d = deferToThread(lambda: _search_engine.tags(query=query, skip_default_tags=skip_default_tags)) d.addCallback(lambda tags: respond_json_deferred(tags, request)) + d.addErrback(self.generic_error_handling, request) return NOT_DONE_YET diff --git a/service/pixelated/resources/user_settings_resource.py b/service/pixelated/resources/user_settings_resource.py index 5b3f0051..38997b30 100644 --- a/service/pixelated/resources/user_settings_resource.py +++ b/service/pixelated/resources/user_settings_resource.py @@ -14,7 +14,10 @@ # 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, BaseResource +from pixelated.resources import respond_json_deferred, BaseResource +from twisted.web import server + +FINGERPRINT_NOT_FOUND = 'Fingerprint not found' class UserSettingsResource(BaseResource): @@ -25,4 +28,16 @@ class UserSettingsResource(BaseResource): def render_GET(self, request): _account_email = self.mail_service(request).account_email - return respond_json({'account_email': _account_email}, request) + + def finish_request(key): + _fingerprint = key.fingerprint + respond_json_deferred({'account_email': _account_email, 'fingerprint': _fingerprint}, request) + + def key_not_found(_): + respond_json_deferred({'account_email': _account_email, 'fingerprint': FINGERPRINT_NOT_FOUND}, request) + + d = self.keymanager(request).fetch_key(_account_email) + d.addCallback(finish_request) + d.addErrback(key_not_found) + + return server.NOT_DONE_YET diff --git a/service/test/functional/features/steps/common.py b/service/test/functional/features/steps/common.py index 21794eb7..ccad842c 100644 --- a/service/test/functional/features/steps/common.py +++ b/service/test/functional/features/steps/common.py @@ -168,7 +168,7 @@ def click_button(context, title, element='button'): def mail_list_with_subject_exists(context, subject): - return find_element_by_xpath(context, "//*[@class='subject' and contains(.,'%s')]" % subject) + return find_element_by_xpath(context, "//*[@class='mail-list-entry__item-subject' and contains(.,'%s')]" % subject) def mail_subject(context): diff --git a/service/test/functional/features/steps/mail_list.py b/service/test/functional/features/steps/mail_list.py index d19de6cd..82faa7af 100644 --- a/service/test/functional/features/steps/mail_list.py +++ b/service/test/functional/features/steps/mail_list.py @@ -32,7 +32,7 @@ def open_current_mail(context): def get_first_email(context): - return wait_until_elements_are_visible_by_locator(context, (By.CSS_SELECTOR, '#mail-list li span a'))[0] + return wait_until_elements_are_visible_by_locator(context, (By.CSS_SELECTOR, '.mail-list-entry__item'))[0] @then('I see that mail under the \'{tag}\' tag') @@ -78,13 +78,13 @@ def impl(context): @given('I have mails') @then(u'I have mails') def impl(context): - emails = wait_until_elements_are_visible_by_locator(context, (By.CSS_SELECTOR, '#mail-list li span a')) + emails = wait_until_elements_are_visible_by_locator(context, (By.CSS_SELECTOR, '.mail-list-entry__item')) assert len(emails) > 0 @when('I mark the first unread email as read') def impl(context): - emails = wait_until_elements_are_visible_by_locator(context, (By.CSS_SELECTOR, '#mail-list li')) + emails = wait_until_elements_are_visible_by_locator(context, (By.CSS_SELECTOR, '.mail-list-entry')) for email in emails: if 'status-read' not in email.get_attribute('class'): @@ -98,7 +98,7 @@ def impl(context): @when('I delete the email') def impl(context): def last_email(): - return wait_until_element_is_visible_by_locator(context, (By.CSS_SELECTOR, '#mail-list li')) + return wait_until_element_is_visible_by_locator(context, (By.CSS_SELECTOR, '.mail-list-entry')) mail = last_email() context.current_mail_id = mail.get_attribute('id') mail.find_element_by_tag_name('input').click() @@ -112,7 +112,7 @@ def _wait_for_mail_list_to_be_empty(context): def mail_list_is_empty(_): with ImplicitWait(context, timeout=0.1): try: - return 0 == len(context.browser.find_elements_by_css_selector('#mail-list li')) + return 0 == len(context.browser.find_elements_by_css_selector('.mail-list-entry')) except TimeoutException: return False diff --git a/service/test/functional/features/steps/mail_view.py b/service/test/functional/features/steps/mail_view.py index 565031b5..2db6dfc5 100644 --- a/service/test/functional/features/steps/mail_view.py +++ b/service/test/functional/features/steps/mail_view.py @@ -58,21 +58,13 @@ def impl(context): click_button(context, 'Send') -# NOT BEING USED -@then('I see if the mail has html content') -def impl(context): - e = find_element_by_css_selector(context, '#mail-view .bodyArea') - h2 = e.find_element_by_css_selector("h2[style*='color: #3f4944']") - assert 'cborim' in h2.text - - @when('I try to delete the first mail') def impl(context): context.execute_steps(u"When I open the first mail in the mail list") find_element_by_css_selector(context, '#mail-view #view-more-actions').click() context.browser.execute_script("$('#delete-button-top').click();") - e = find_element_by_css_selector(context, '#user-alerts') + e = find_element_by_css_selector(context, '.message-panel__growl--success') assert 'Your message was moved to trash!' == e.text diff --git a/service/test/functional/features/steps/tag_list.py b/service/test/functional/features/steps/tag_list.py index a3315835..8550a886 100644 --- a/service/test/functional/features/steps/tag_list.py +++ b/service/test/functional/features/steps/tag_list.py @@ -49,7 +49,7 @@ def impl(context, tag): e = find_element_by_id(context, 'tag-%s' % tag) e.click() - wait_until_element_is_visible_by_locator(context, (By.CSS_SELECTOR, "#mail-list li span a[href*='%s']" % tag), timeout=20) + wait_until_element_is_visible_by_locator(context, (By.CSS_SELECTOR, ".mail-list-entry__item[href*='%s']" % tag), timeout=20) success = True except TimeoutException: pass diff --git a/service/test/integration/test_incoming_mail.py b/service/test/integration/test_incoming_mail.py index 682ca118..e413d6c1 100644 --- a/service/test/integration/test_incoming_mail.py +++ b/service/test/integration/test_incoming_mail.py @@ -25,12 +25,12 @@ class IncomingMailTest(SoledadTestBase): @defer.inlineCallbacks def test_message_collection(self): # given - mbx = yield self.account.getMailbox('INBOX') + mail_collection = yield self.account.get_collection_by_mailbox('INBOX') input_mail = MailBuilder().build_input_mail() # when yield MailboxIndexerListener.listen(self.account, 'INBOX', self.mail_store, self.search_engine) - yield mbx.addMessage(input_mail.raw, [], notify_just_mdoc=False) + yield mail_collection.add_msg(input_mail.raw) # then yield self.wait_in_reactor() # event handlers are called async, wait for it diff --git a/service/test/support/integration/app_test_client.py b/service/test/support/integration/app_test_client.py index f3ec5d25..95facec0 100644 --- a/service/test/support/integration/app_test_client.py +++ b/service/test/support/integration/app_test_client.py @@ -25,7 +25,7 @@ import uuid import random -from leap.mail.imap.account import IMAPAccount +from leap.mail.mail import Account from leap.soledad.client import Soledad from mock import Mock from twisted.internet import reactor, defer @@ -77,7 +77,7 @@ class AppTestAccount(object): self.mail_store = SearchableMailStore(LeapMailStore(self.soledad), self.search_engine) self.attachment_store = LeapAttachmentStore(self.soledad) - yield self._initialize_imap_account() + yield self._initialize_account() self.draft_service = DraftService(self.mail_store) self.leap_session = mock() @@ -110,10 +110,9 @@ class AppTestAccount(object): soledad_test_folder = os.path.join(self._leap_home, self._uuid) shutil.rmtree(soledad_test_folder) - def _initialize_imap_account(self): - account_ready_cb = defer.Deferred() - self.account = IMAPAccount(self._user_id, self.soledad, account_ready_cb) - return account_ready_cb + def _initialize_account(self): + self.account = Account(self.soledad) + return self.account.deferred_initialization def _create_mail_service(self, mail_sender, mail_store, search_engine, attachment_store): return MailService(mail_sender, mail_store, search_engine, self._mail_address, attachment_store) diff --git a/service/test/unit/adapter/test_mailbox_indexer_listener.py b/service/test/unit/adapter/test_mailbox_indexer_listener.py index c88ba035..c101aa1a 100644 --- a/service/test/unit/adapter/test_mailbox_indexer_listener.py +++ b/service/test/unit/adapter/test_mailbox_indexer_listener.py @@ -31,7 +31,7 @@ class MailboxListenerTest(unittest.TestCase): def test_add_itself_to_mailbox_listeners(self): self.account.mailboxes = ['INBOX'] mailbox = mock() - when(self.account).getMailbox('INBOX').thenReturn(mailbox) + when(self.account).get_collection_by_mailbox('INBOX').thenReturn(mailbox) mailbox.listeners = set() when(mailbox).addListener = lambda x: mailbox.listeners.add(x) @@ -49,7 +49,7 @@ class MailboxListenerTest(unittest.TestCase): listener = MailboxIndexerListener('INBOX', self.mail_store, search_engine) when(self.mail_store).get_mailbox_mail_ids('INBOX').thenReturn({'ident1', 'ident2', 'missing_ident'}) when(self.mail_store).get_mails({'missing_ident'}, include_body=True).thenReturn([mail]) - listener.newMessages(10, 5) + listener.notify_new() verify(self.mail_store, times=1).get_mails({'missing_ident'}, include_body=True) verify(search_engine).index_mails([mail]) @@ -59,6 +59,6 @@ class MailboxListenerTest(unittest.TestCase): when(logger).error(ANY()).thenReturn(None) listener = MailboxIndexerListener('INBOX', self.mail_store, mock()) - yield listener.newMessages(1, 1) + yield listener.notify_new() verify(logger).error(ANY()) diff --git a/service/test/unit/bitmask_libraries/test_session.py b/service/test/unit/bitmask_libraries/test_session.py index aad2cac2..84f9f023 100644 --- a/service/test/unit/bitmask_libraries/test_session.py +++ b/service/test/unit/bitmask_libraries/test_session.py @@ -29,7 +29,7 @@ class SessionTest(AbstractLeapTest): self.smtp_mock = MagicMock() @patch('pixelated.bitmask_libraries.session.register') - @patch('pixelated.bitmask_libraries.session.IMAPAccount') + @patch('pixelated.bitmask_libraries.session.Account') @defer.inlineCallbacks def test_background_jobs_are_started_during_initial_sync(self, *unused): mailFetcherMock = MagicMock() @@ -41,7 +41,7 @@ class SessionTest(AbstractLeapTest): @patch('pixelated.bitmask_libraries.session.register') @patch('pixelated.bitmask_libraries.session.unregister') - @patch('pixelated.bitmask_libraries.session.IMAPAccount') + @patch('pixelated.bitmask_libraries.session.Account') @defer.inlineCallbacks def test_that_close_stops_background_jobs(self, *unused): with patch('pixelated.bitmask_libraries.session.reactor.callFromThread', new=_execute_func) as _: @@ -153,7 +153,7 @@ class SessionTest(AbstractLeapTest): @patch('pixelated.bitmask_libraries.session.register') @patch('pixelated.bitmask_libraries.session.unregister') - @patch('pixelated.bitmask_libraries.session.IMAPAccount') + @patch('pixelated.bitmask_libraries.session.Account') @defer.inlineCallbacks def test_session_initial_sync_only_triggered_once(self, *unused): mailFetcherMock = MagicMock() diff --git a/service/test/unit/resources/test_logout_resources.py b/service/test/unit/resources/test_logout_resources.py index 312d2ba4..49866057 100644 --- a/service/test/unit/resources/test_logout_resources.py +++ b/service/test/unit/resources/test_logout_resources.py @@ -1,12 +1,16 @@ +import logging + from mock import patch, MagicMock -from twisted.trial import unittest from twisted.internet import defer +from twisted.trial import unittest from twisted.web.error import UnsupportedMethod from twisted.web.test.requesthelper import DummyRequest from pixelated.resources.logout_resource import LogoutResource from test.unit.resources import DummySite +logging.getLogger('pixelated.resources').addHandler(logging.NullHandler()) + class TestLogoutResource(unittest.TestCase): def setUp(self): @@ -40,3 +44,20 @@ class TestLogoutResource(unittest.TestCase): request.method = 'GET' self.assertRaises(UnsupportedMethod, self.web.get, request) + + def test_errback_is_called(self): + request = DummyRequest(['/logout']) + request.method = 'POST' + + session = self.resource.get_session(request) + exception = Exception('') + session.expire = MagicMock(side_effect=exception) + + d = self.web.get(request) + + def assert_500_when_exception_is_thrown(_): + self.assertEqual(500, request.responseCode) + self.assertEqual('Something went wrong!', request.written[0]) + + d.addCallback(assert_500_when_exception_is_thrown) + return d diff --git a/service/test/unit/resources/test_tags_resource.py b/service/test/unit/resources/test_tags_resource.py new file mode 100644 index 00000000..684e95f2 --- /dev/null +++ b/service/test/unit/resources/test_tags_resource.py @@ -0,0 +1,36 @@ +import logging + +from mock import MagicMock +from twisted.trial import unittest +from twisted.web.test.requesthelper import DummyRequest +from pixelated.resources.tags_resource import TagsResource +from test.unit.resources import DummySite + +logging.getLogger('pixelated.resources').addHandler(logging.NullHandler()) + + +class TestTagsResource(unittest.TestCase): + def setUp(self): + self.services_factory = MagicMock() + self.resource = TagsResource(self.services_factory) + + def test_errback_is_called(self): + exception = Exception('') + mock_search_engine = MagicMock() + mock_search_engine.tags = MagicMock(side_effect=exception) + mock_service = MagicMock() + mock_service.search_engine = mock_search_engine + self.services_factory.services.return_value = mock_service + self.web = DummySite(self.resource) + + request = DummyRequest(['/tags']) + request.method = 'GET' + + d = self.web.get(request) + + def assert_500_when_exception_is_thrown(_): + self.assertEqual(500, request.responseCode) + self.assertEqual('Something went wrong!', request.written[0]) + + d.addCallback(assert_500_when_exception_is_thrown) + return d diff --git a/service/test/unit/resources/test_user_settings_resource.py b/service/test/unit/resources/test_user_settings_resource.py new file mode 100644 index 00000000..d69f4895 --- /dev/null +++ b/service/test/unit/resources/test_user_settings_resource.py @@ -0,0 +1,64 @@ +import unittest +import json +import ast + +from pixelated.application import UserAgentMode +from pixelated.resources.user_settings_resource import UserSettingsResource, FINGERPRINT_NOT_FOUND +from mockito import mock, when, any +from test.unit.resources import DummySite +from twisted.web.test.requesthelper import DummyRequest +from leap.keymanager import OpenPGPKey +from twisted.internet import defer +from twisted.python.failure import Failure + +MAIL_ADDRESS = 'some@key' +FINGERPRINT = '4-8-12-13-23-42' + + +class TestUserSettingsResource(unittest.TestCase): + + def setUp(self): + self.services = mock() + self.mail_service = mock() + self.mail_service.account_email = MAIL_ADDRESS + self.keymanager = mock() + self.services_factory = mock() + self.services_factory.mode = UserAgentMode(is_single_user=True) + self.services.mail_service = self.mail_service + self.services.keymanager = self.keymanager + self.services_factory._services_by_user = {'someuserid': self.keymanager} + self.resource = UserSettingsResource(self.services_factory) + when(self.services_factory).services(any()).thenReturn(self.services) + self.web = DummySite(self.resource) + + def test_fingerprint_given(self): + key = OpenPGPKey(MAIL_ADDRESS) + key.fingerprint = FINGERPRINT + request = DummyRequest(['/user-settings']) + when(self.keymanager).fetch_key(MAIL_ADDRESS).thenReturn(defer.succeed(key)) + + d = self.web.get(request) + + def assert_response(_): + response = json.loads(request.written[0]) + self.assertEqual(FINGERPRINT, response['fingerprint']) + self.assertEqual(MAIL_ADDRESS, response['account_email']) + + d.addCallback(assert_response) + return d + + def test_fingerprint_missing(self): + key = OpenPGPKey(MAIL_ADDRESS) + key.fingerprint = FINGERPRINT + request = DummyRequest(['/user-settings']) + when(self.keymanager).fetch_key(MAIL_ADDRESS).thenReturn(defer.fail(Failure)) + + d = self.web.get(request) + + def assert_response(_): + response = json.loads(request.written[0]) + self.assertEqual(FINGERPRINT_NOT_FOUND, response['fingerprint']) + self.assertEqual(MAIL_ADDRESS, response['account_email']) + + d.addCallback(assert_response) + return d diff --git a/web-ui/app/js/helpers/view_helper.js b/web-ui/app/js/helpers/view_helper.js index e8d517a5..7e07df75 100644 --- a/web-ui/app/js/helpers/view_helper.js +++ b/web-ui/app/js/helpers/view_helper.js @@ -98,14 +98,21 @@ define( } + function formatFingerPrint(fingerprint) { + fingerprint = fingerprint || ''; + return fingerprint.replace(/(.{4})/g, '$1 ').trim(); + } + Handlebars.registerHelper('formatDate', formatDate); Handlebars.registerHelper('formatSize', formatSize); Handlebars.registerHelper('formatStatusClasses', formatStatusClasses); + Handlebars.registerHelper('formatFingerPrint', formatFingerPrint); return { formatStatusClasses: formatStatusClasses, formatSize: formatSize, formatMailBody: formatMailBody, + formatFingerPrint: formatFingerPrint, moveCaretToEndOfText: moveCaretToEndOfText, quoteMail: quoteMail, i18n: i18n diff --git a/web-ui/app/js/mail_list/ui/mail_items/mail_item.js b/web-ui/app/js/mail_list/ui/mail_items/mail_item.js index c075b0b5..be664289 100644 --- a/web-ui/app/js/mail_list/ui/mail_items/mail_item.js +++ b/web-ui/app/js/mail_list/ui/mail_items/mail_item.js @@ -68,6 +68,7 @@ define( this.attr.mail.tagsForListView = _.without(this.attr.mail.tags, this.attr.tag); var mailItemHtml = templates.mails[this.attr.templateType](this.attr.mail); this.$node.html(mailItemHtml); + this.$node.addClass("mail-list-entry"); this.$node.addClass(viewHelper.formatStatusClasses(this.attr.mail.status)); if (this.attr.selected) { this.doSelect(); } this.on(this.$node.find('a'), 'click', this.triggerOpenMail); diff --git a/web-ui/app/scss/_security.scss b/web-ui/app/scss/_security.scss deleted file mode 100644 index ff36cb3e..00000000 --- a/web-ui/app/scss/_security.scss +++ /dev/null @@ -1,48 +0,0 @@ -.security-status { - margin: 0 0 5px; - clear: both; - span { - display: inline-block; - padding: 2px 6px; - white-space: nowrap; - background: $success; - color: $white; - border-radius: 12px; - &:before { - font-family: FontAwesome; - } - &.encrypted { - &:before { - content: "\f023"; - } - &.encryption-error { - background: $attention; - &:before { - content: "\f023 \f057"; - } - } - } - &.signed { - &:before { - content: "\f00c"; - } - &.signature-not-trusted { - background: $error; - &:before { - content: "\f05e"; - } - } - } - &[class^=not-], &.signature-expired, &.signature-revoked { - background: $attention; - &:before { - content: "\f05e" - } - } - &.not-encrypted { - &:before { - content: "\f13e "; - } - } - } -} diff --git a/web-ui/app/scss/_styles.scss b/web-ui/app/scss/_styles.scss index b4ffbd75..aacc5dd5 100644 --- a/web-ui/app/scss/_styles.scss +++ b/web-ui/app/scss/_styles.scss @@ -32,6 +32,11 @@ bottom: 33px } + header { + border-bottom: 1px solid white; + margin-bottom: 10px; + } + #user-settings-close { float: right; } @@ -42,6 +47,14 @@ line-height: 1.2em; } + h2 { + font-size: 1.1em; + color: white; + line-height: 1.1em; + display: inline; + margin-left: 5px; + } + i.fa-user { margin-right: 10px; float: left; @@ -54,7 +67,7 @@ } p { - font-size: 1.2em; + font-size: 1.1em; color: $light_orange; } } @@ -128,100 +141,6 @@ } } -@mixin email-list { - ul#mail-list { - clear: both; - li { - position: relative; - padding: 8px 10px 10px 10px; - background: $contrast; - border-bottom: 1px solid white; - cursor: pointer; - font-weight: bold; - transition: background-color 150ms ease-out; - span { - display: inline-block; - vertical-align: top; - &:last-child { - width: 92%; - } - input[type=checkbox] { - @include check-box; - margin-right: 2px; - } - a { - color: $dark_grey; - display: block; - height: 62px; - margin-top: -8px; - padding-top: 3px; - width: 106%; - } - } - - .subject { - display: block; - width: 90%; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - - i.fa-pencil, i.fa-trash-o { - color: $indicator_icon_color; - } - } - - .tags { - @include tags; - - // HACK: overwrite default from ul li property - &-tag { - border-bottom: none; - } - } - - .received-date, .sent-date { - position: absolute; - right: 10px; - font-size: 0.7em; - } - - .attachment-indicator { - margin: 2px 0 0 25px; - font-size: initial; - - i.fa-paperclip { - color: $indicator_icon_color; - } - } - .from { - white-space: nowrap; - font-size: 0.8em; - width: 80%; - overflow: hidden; - text-overflow: ellipsis; - } - - &.status-read { - a { - font-weight: normal; - color: $attachment_text; - } - } - &:hover { - background: darken($contrast, 5%); - } - &.selected { - background: $white; - z-index: 3; - a { - color: $dark_grey; - } - } - } - } -} - @mixin mail-count($bg_color) { background: $bg_color; color: $white; @@ -510,7 +429,6 @@ section { &#middle-pane { background: $contrast; - @include email-list; } &#right-pane { diff --git a/web-ui/app/scss/style.scss b/web-ui/app/scss/style.scss index 4dc79398..db9486fa 100644 --- a/web-ui/app/scss/style.scss +++ b/web-ui/app/scss/style.scss @@ -12,6 +12,8 @@ // mixins @import "mixins/position-helpers"; @import "mixins/tags"; + +// TODO @import "mixins"; // templates @@ -26,6 +28,7 @@ @import "views/security-labels"; @import "views/compose-view"; @import "views/compose-button"; +@import "views/mail-list"; // misc stuff @import "others"; diff --git a/web-ui/app/scss/views/_mail-list.scss b/web-ui/app/scss/views/_mail-list.scss new file mode 100644 index 00000000..417fabc2 --- /dev/null +++ b/web-ui/app/scss/views/_mail-list.scss @@ -0,0 +1,96 @@ +.mail-list-entry { + @include scut-clearfix; + + padding: 8px 10px 10px 10px; + border-bottom: 1px solid white; + transition: background-color 150ms ease-out; + cursor: pointer; + font-weight: bold; + height: 80px; + position: relative; + + // Workaround: + // Foundation is of the opinion that a 1.6 line height for all lists + // is a totally good idea. Please remove when Foundation is gone + line-height: normal; + + &.status-read { + font-weight: normal; + color: $attachment_text; + } + + &.selected { + background: $white; + z-index: 10; // overlay the box-shadow of the right page (z-index: 2) + } + + &:hover { + background: darken($contrast, 5%); + } + + &__checkbox { + margin-right: 5px; + display: block; + float: left; + min-height: 100%; + + & > input[type=checkbox] { + @include check-box; + } + } + + &__item { + display: block; + color: $dark_grey; + padding-left: 24px; + + &-from { + white-space: nowrap; + font-size: 0.8em; + overflow: hidden; + text-overflow: ellipsis; + display: inline-block; + } + + &-date { + font-size: 0.7em; + float: right; + display: inline-block; + } + + &-subject { + display: inline-block; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + width: 85%; + + &-icon { + color: $light_gray; + } + } + + &-attachment { + width: 14px; + text-align: right; + display: inline-block; + float: right; + color: $light_gray; + } + + &-tags { + @include tags; + + // Workaround: + // Foundation is of the opinion that a 1.6 line height and a 0.6 rem margin-bottom + // for all lists is a totally good idea. Please remove when Foundation is gone + line-height: normal; + margin-bottom: 0; + } + + &:hover, &:focus, &:active { + color: $dark_grey; + } + } +} + diff --git a/web-ui/app/templates/mails/draft.hbs b/web-ui/app/templates/mails/draft.hbs index 675f1481..d223c0a4 100644 --- a/web-ui/app/templates/mails/draft.hbs +++ b/web-ui/app/templates/mails/draft.hbs @@ -1,31 +1,41 @@ -<span> - <input type="checkbox"/> -</span> -<span> - <a href="/#/{{ currentTag }}/mail/{{ ident }}"> - <span class="sent-date">{{ formatDate header.date }} - {{#if attachments}} - <div class="attachment-indicator"> - <i class="fa fa-paperclip"></i> - </div> - {{/if}} - </span> +<div class="mail-list-entry__checkbox"> + <input type="checkbox" {{#if isChecked }}checked="true"{{/if}} /> +</div> + +<a class="mail-list-entry__item" href="/#/{{ currentTag }}/mail/{{ ident }}"> + <div> + <div class="mail-list-entry__item-from"> + {{t 'to:'}} + {{#if header.to }} + {{ header.to }} + {{else}} + {{t 'no_recipient'}} + {{/if}} + </div> <!-- /.mail-list-entry__item-from --> - <div class="from"> - {{t 'to:'}} - {{#if header.to }} - {{ header.to }} - {{else}} - {{t 'no_recipient'}} - {{/if}} + <span class="mail-list-entry__item-date">{{ formatDate header.date }}</span> <!-- /.mail-list-entry__item-date --> </div> - <div class="subject"> - <i class="fa fa-pencil"></i> - {{#if header.subject }} - {{header.subject}} - {{else}} - {{t 'no_subject'}} - {{/if}} + <div> + <div class="mail-list-entry__item-subject"> + <i class="mail-list-entry__item-subject-icon fa fa-pencil"></i> + {{#if header.subject }} + {{header.subject}} + {{else}} + {{t 'no_subject'}} + {{/if}} + </div> + + {{#if attachments}} + <div class="mail-list-entry__item-attachment"><i class="fa fa-paperclip"></i></div> + {{/if}} </div> - </a> -</span> + <ul class="mail-list-entry__item-tags"> + {{#each tagsForListView }} + <li class="mail-list-entry__item-tags-tag" data-tag="{{this}}">{{ this }}</li> + {{/each }} + </ul> <!-- /.mail-list-entry__item-tags --> +</a> + + + + diff --git a/web-ui/app/templates/mails/sent.hbs b/web-ui/app/templates/mails/sent.hbs index 86b6e607..a637e3d2 100644 --- a/web-ui/app/templates/mails/sent.hbs +++ b/web-ui/app/templates/mails/sent.hbs @@ -1,35 +1,36 @@ -<span> - <input type="checkbox"/> -</span> -<span> - <a href="/#/{{ currentTag }}/mail/{{ ident }}"> - <span class="sent-date">{{ formatDate header.date }} +<div class="mail-list-entry__checkbox"> + <input type="checkbox" {{#if isChecked }}checked="true"{{/if}} /> +</div> +<a class="mail-list-entry__item" href="/#/{{ currentTag }}/mail/{{ ident }}"> + <div> + <div class="mail-list-entry__item-from"> + {{t 'to:'}} + {{#if header.to }} + {{ header.to }} + {{else}} + {{t 'no_recipient'}} + {{/if}} + </div> <!-- /.mail-list-entry__item-from --> + + <span class="mail-list-entry__item-date">{{ formatDate header.date }}</span> <!-- /.mail-list-entry__item-date --> + </div> + <div> + <div class="mail-list-entry__item-subject"> + {{#if header.subject }} + {{header.subject}} + {{else}} + {{t 'no_subject'}} + {{/if}} + </div> + {{#if attachments}} - <div class="attachment-indicator"> - <i class="fa fa-paperclip"></i> - </div> + <div class="mail-list-entry__item-attachment"><i class="fa fa-paperclip"></i></div> {{/if}} - </span> + </div> + <ul class="mail-list-entry__item-tags"> + {{#each tagsForListView }} + <li class="mail-list-entry__item-tags-tag" data-tag="{{this}}">{{ this }}</li> + {{/each }} + </ul> <!-- /.mail-list-entry__item-tags --> +</a> - <div class="from"> - {{t 'to:'}} - {{#if header.to }} - {{ header.to }} - {{else}} - {{t 'no_recipient'}} - {{/if}} - </div> - <div class="subject"> - {{#if header.subject }} - {{header.subject}} - {{else}} - {{t 'no_subject'}} - {{/if}} - </div> - <ul class="tags"> - {{#each tagsForListView }} - <li class="tags-tag" data-tag="{{this}}">{{ this }}</li> - {{/each }} - </ul> - </a> -</span> diff --git a/web-ui/app/templates/mails/single.hbs b/web-ui/app/templates/mails/single.hbs index 95f9adb7..aaede844 100644 --- a/web-ui/app/templates/mails/single.hbs +++ b/web-ui/app/templates/mails/single.hbs @@ -1,23 +1,28 @@ -<span> - <input type="checkbox" {{#if isChecked }}checked="true"{{/if}} /> -</span> -<span> - <a href="/#/{{ currentTag }}/mail/{{ ident }}"> - <span class="received-date">{{ formatDate header.date }} - {{#if attachments}} - <div class="attachment-indicator"> - <i class="fa fa-paperclip"></i> - </div> - {{/if}} - </span> - <div class="from">{{#if header.from }}{{ header.from }}{{else}}{{t "you"}}{{/if}}</div> - <div class="subject"> - {{ header.subject }} +<div class="mail-list-entry__checkbox"> + <input type="checkbox" {{#if isChecked }}checked="true"{{/if}} /> +</div> +<a class="mail-list-entry__item" href="/#/{{ currentTag }}/mail/{{ ident }}"> + <div> + <div class="mail-list-entry__item-from"> + {{#if header.from }} + {{ header.from }} + {{else}} + {{t "you"}} + {{/if}} + </div> <!-- /.mail-list-entry__item-from --> + + <span class="mail-list-entry__item-date">{{ formatDate header.date }}</span> <!-- /.mail-list-entry__item-date --> </div> - <ul class="tags"> - {{#each tagsForListView }} - <li class="tags-tag" data-tag="{{this}}">{{ this }}</li> - {{/each }} - </ul> - </a> -</span> + <div> + <div class="mail-list-entry__item-subject">{{ header.subject }}</div> + + {{#if attachments}} + <div class="mail-list-entry__item-attachment"><i class="fa fa-paperclip"></i></div> + {{/if}} + </div> + <ul class="mail-list-entry__item-tags"> + {{#each tagsForListView }} + <li class="mail-list-entry__item-tags-tag" data-tag="{{this}}">{{ this }}</li> + {{/each }} + </ul> <!-- /.mail-list-entry__item-tags --> +</a> diff --git a/web-ui/app/templates/mails/trash.hbs b/web-ui/app/templates/mails/trash.hbs index ea8fe07f..f8947b15 100644 --- a/web-ui/app/templates/mails/trash.hbs +++ b/web-ui/app/templates/mails/trash.hbs @@ -1,24 +1,32 @@ -<span> - <input type="checkbox" {{#if isChecked }}checked="true"{{/if}} /> -</span> -<span> - <a href="/#/{{ currentTag }}/mail/{{ ident }}"> - <span class="received-date">{{ header.formattedDate }} - {{#if attachments}} - <div class="attachment-indicator"> - <i class="fa fa-paperclip"></i> +<div class="mail-list-entry__checkbox"> + <input type="checkbox" {{#if isChecked }}checked="true"{{/if}} /> +</div> +<a class="mail-list-entry__item" href="/#/{{ currentTag }}/mail/{{ ident }}"> + <div> + <div class="mail-list-entry__item-from"> + {{#if header.from }} + {{ header.from }} + {{else}} + {{t "you"}} + {{/if}} + </div> <!-- /.mail-list-entry__item-from --> + + <span class="mail-list-entry__item-date">{{ formatDate header.date }}</span> <!-- /.mail-list-entry__item-date --> + </div> + <div> + <div class="mail-list-entry__item-subject"> + <i class="mail-list-entry__item-subject-icon fa fa-trash-o"></i> + {{ header.subject }} </div> - {{/if}} - </span> - <div class="from">{{#if header.from }}{{ header.from }}{{else}}{{t "you"}}{{/if}}</div> - <div class="subject"> - <i class="fa fa-trash-o"></i> - {{ header.subject }} + + {{#if attachments}} + <div class="mail-list-entry__item-attachment"><i class="fa fa-paperclip"></i></div> + {{/if}} </div> - <ul class="tags"> - {{#each tagsForListView }} - <li class="tags-tag" data-tag="{{this}}">{{ this }}</li> - {{/each }} - </ul> - </a> -</span> + <ul class="mail-list-entry__item-tags"> + {{#each tagsForListView }} + <li class="mail-list-entry__item-tags-tag" data-tag="{{this}}">{{ this }}</li> + {{/each }} + </ul> <!-- /.mail-list-entry__item-tags --> +</a> + diff --git a/web-ui/app/templates/page/user_settings_box.hbs b/web-ui/app/templates/page/user_settings_box.hbs index eb227dcf..761bfc16 100644 --- a/web-ui/app/templates/page/user_settings_box.hbs +++ b/web-ui/app/templates/page/user_settings_box.hbs @@ -4,4 +4,7 @@ <h1>User Settings</h1> <i class="shortcut-label"></i> </header> +<i class="fa fa-envelope-o"></i><h2>E-Mail address</h2> <p>{{ account_email }}</p> +<i class="fa fa-key"></i><h2>Public key fingerprint</h2> +<p>{{ formatFingerPrint fingerprint }}</p> diff --git a/web-ui/app/templates/page/version.hbs b/web-ui/app/templates/page/version.hbs index 40804ff3..181d2151 100644 --- a/web-ui/app/templates/page/version.hbs +++ b/web-ui/app/templates/page/version.hbs @@ -1 +1,2 @@ -version: UNKNOWN_VERSION
\ No newline at end of file +version: UNKNOWN_VERSION <br/> +COMMIT_DATE diff --git a/web-ui/config/add_git_version.sh b/web-ui/config/add_git_version.sh index 2732abed..b9cfba72 100755 --- a/web-ui/config/add_git_version.sh +++ b/web-ui/config/add_git_version.sh @@ -3,8 +3,10 @@ TEMPLATE_FILE="app/js/generated/hbs/templates.js" COMMITISH=$(git rev-parse --short HEAD) +COMMITDATE=$(git show -s --format=%cr) perl -pi -e "s/UNKNOWN_VERSION/$COMMITISH/" $TEMPLATE_FILE +perl -pi -e "s/COMMIT_DATE/$COMMITDATE/" $TEMPLATE_FILE if [ ! -f "$TEMPLATE_FILE" ] ; then echo "file $TEMPLATE_FILE not found" 1>&2 diff --git a/web-ui/test/custom_matchers.js b/web-ui/test/custom_matchers.js index d68d21a8..3de7fb5b 100644 --- a/web-ui/test/custom_matchers.js +++ b/web-ui/test/custom_matchers.js @@ -5,10 +5,10 @@ define([], function() { compare: function (mail, node) { var result = {}, equals = {}, subject, tags, from, date, messages = [], notMessages = []; - subject = node.find('#mail-' + mail.ident + ' .subject')[0]; - tags = _.map(node.find('#mail-' + mail.ident + ' .tags .tags-tag'), function (tag) { return tag.textContent; }); - date = node.find('#mail-' + mail.ident + ' .received-date'); - from = node.find('#mail-' + mail.ident + ' .from'); + subject = node.find('#mail-' + mail.ident + ' .mail-list-entry__item-subject')[0]; + tags = _.map(node.find('#mail-' + mail.ident + ' .mail-list-entry__item-tags .mail-list-entry__item-tags-tag'), function (tag) { return tag.textContent; }); + date = node.find('#mail-' + mail.ident + ' .mail-list-entry__item-date'); + from = node.find('#mail-' + mail.ident + ' .mail-list-entry__item-from'); if (subject && subject.textContent.trim() === mail.header.subject) { equals.subject = true; diff --git a/web-ui/test/spec/helpers/view_helper.spec.js b/web-ui/test/spec/helpers/view_helper.spec.js index b2f597c2..19bef15f 100644 --- a/web-ui/test/spec/helpers/view_helper.spec.js +++ b/web-ui/test/spec/helpers/view_helper.spec.js @@ -106,5 +106,11 @@ define(['helpers/view_helper'], function (viewHelper) { expect(window.setTimeout.calls.all()[0].args[1]).toEqual(1); }); + + describe('fingerprint helper', function () { + it('should format fingerprint', function () { + expect(viewHelper.formatFingerPrint('12345678')).toEqual('1234 5678'); + }); + }); }); }); |