summaryrefslogtreecommitdiff
path: root/service
diff options
context:
space:
mode:
Diffstat (limited to 'service')
-rwxr-xr-xservice/go5
-rw-r--r--service/pixelated/adapter/model/mail.py60
-rw-r--r--service/pixelated/adapter/search/__init__.py27
-rw-r--r--service/pixelated/adapter/services/mail_sender.py29
-rw-r--r--service/pixelated/adapter/services/mail_service.py17
-rw-r--r--service/pixelated/adapter/services/mailbox.py6
-rw-r--r--service/pixelated/adapter/services/mailboxes.py2
-rw-r--r--service/pixelated/adapter/services/tag_service.py12
-rw-r--r--service/pixelated/adapter/soledad/soledad_facade_mixin.py17
-rw-r--r--service/pixelated/adapter/soledad/soledad_writer_mixin.py9
-rw-r--r--service/pixelated/bitmask_libraries/certs.py20
-rw-r--r--service/pixelated/bitmask_libraries/session.py1
-rw-r--r--service/pixelated/bitmask_libraries/smtp.py3
-rw-r--r--service/pixelated/bitmask_libraries/soledad.py4
-rw-r--r--service/pixelated/certificates/try.pixelated-project.org.ca.crt34
-rw-r--r--service/pixelated/config/__init__.py5
-rw-r--r--service/pixelated/config/app_factory.py4
-rw-r--r--service/pixelated/config/debug.py40
-rw-r--r--service/pixelated/config/logging_setup.py65
-rw-r--r--service/pixelated/resources/sync_info_resource.py2
-rw-r--r--service/pixelated/support/ext_fetch.py33
-rw-r--r--service/pixelated/support/ext_keymanager_fetch_key.py60
-rw-r--r--service/pixelated/support/ext_protobuf.py5
-rw-r--r--service/test/functional/features/compose_save_draft_and_send.feature12
-rw-r--r--service/test/functional/features/environment.py8
-rw-r--r--service/test/functional/features/forward_trash_archive.feature17
-rw-r--r--service/test/functional/features/search_and_destroy.feature9
-rw-r--r--service/test/functional/features/steps/__init__.py15
-rw-r--r--service/test/functional/features/steps/common.py20
-rw-r--r--service/test/functional/features/steps/compose.py20
-rw-r--r--service/test/functional/features/steps/mail_list.py8
-rw-r--r--service/test/functional/features/steps/mail_view.py13
-rw-r--r--service/test/functional/features/steps/tag_list.py19
-rw-r--r--service/test/functional/features/tag_and_reply.feature7
-rw-r--r--service/test/integration/test_contacts.py24
-rw-r--r--service/test/integration/test_delete_mail.py10
-rw-r--r--service/test/integration/test_drafts.py10
-rw-r--r--service/test/integration/test_mark_as_read_unread.py18
-rw-r--r--service/test/integration/test_retrieve_attachment.py2
-rw-r--r--service/test/integration/test_search.py34
-rw-r--r--service/test/integration/test_soledad_querier.py8
-rw-r--r--service/test/integration/test_tags.py20
-rw-r--r--service/test/perf/search/test_Search.py15
-rw-r--r--service/test/support/integration/app_test_client.py71
-rw-r--r--service/test/support/integration/soledad_test_base.py60
-rw-r--r--service/test/support/test_helper.py12
-rw-r--r--service/test/unit/adapter/search/__init__.py15
-rw-r--r--service/test/unit/adapter/search/test_search.py89
-rw-r--r--service/test/unit/adapter/test_draft_service.py2
-rw-r--r--service/test/unit/adapter/test_email_recepient_normalizer.py42
-rw-r--r--service/test/unit/adapter/test_mail.py105
-rw-r--r--service/test/unit/adapter/test_mail_service.py7
-rw-r--r--service/test/unit/adapter/test_mailbox.py9
-rw-r--r--service/test/unit/adapter/test_mailbox_indexer_listener.py2
-rw-r--r--service/test/unit/adapter/test_soledad_querier.py44
-rw-r--r--service/test/unit/resources/test_sync_info_controller.py2
-rw-r--r--service/test/unit/support/test_ext_keymanager_fetch_key.py76
57 files changed, 846 insertions, 439 deletions
diff --git a/service/go b/service/go
index ac953bdd..7bcc0c4b 100755
--- a/service/go
+++ b/service/go
@@ -93,15 +93,18 @@ function runCoverageUnitAndIntegration {
}
if [ "$1" == 'test' ]; then
- runJSHint
+ set -e
runPep8
runUnitTests "${@:2}"
runIntegrationTests "${@:2}"
elif [ "$1" == 'unit' ]; then
+ set -e
runUnitTests
elif [ "$1" == 'integration' ]; then
+ set -e
runIntegrationTests
elif [ "$1" == 'pep8' ]; then
+ set -e
runPep8
elif [ "$1" == 'setuppy' ]; then
setuppy
diff --git a/service/pixelated/adapter/model/mail.py b/service/pixelated/adapter/model/mail.py
index 96f2c81c..f23c2708 100644
--- a/service/pixelated/adapter/model/mail.py
+++ b/service/pixelated/adapter/model/mail.py
@@ -16,6 +16,7 @@
import json
from uuid import uuid4
from email.mime.text import MIMEText
+from email.header import decode_header
from leap.mail.imap.fields import fields
import leap.mail.walk as walk
@@ -26,6 +27,10 @@ from email.MIMEMultipart import MIMEMultipart
from pycryptopp.hash import sha256
import re
from pixelated.support.functional import compact
+import logging
+
+
+logger = logging.getLogger(__name__)
class Mail(object):
@@ -82,7 +87,7 @@ class Mail(object):
def _parse_charset_header(self, charset_header, default_charset='utf-8'):
try:
- return re.compile('.*charset=(.*);').match(charset_header).group(1)
+ return re.compile('.*charset=([a-zA-Z0-9-]+)', re.MULTILINE | re.DOTALL).match(charset_header).group(1)
except:
return default_charset
@@ -232,14 +237,22 @@ class PixelatedMail(Mail):
encoding = part['headers'].get('Content-Transfer-Encoding', '')
content_type = self._parse_charset_header(part['headers'].get('Content-Type'))
- decoding_map = {
- 'quoted-printable': lambda content, content_type: unicode(content.decode('quopri'), content_type),
- 'base64': lambda content, content_type: content.decode('base64').decode('utf-8')
- }
- if encoding:
- return decoding_map[encoding](part['content'], content_type)
- else:
- return part['content']
+ try:
+ decoding_map = {
+ 'quoted-printable': lambda content, content_type: unicode(content.decode('quopri'), content_type),
+ 'base64': lambda content, content_type: content.decode('base64').decode('utf-8'),
+ '7bit': lambda content, content_type: content.encode(content_type),
+ '8bit': lambda content, content_type: content.encode(content_type)
+ }
+ if encoding:
+ return decoding_map[encoding](part['content'], content_type)
+ else:
+ return part['content']
+ except Exception:
+ logger.error('Failed to decode mail part with:')
+ logger.error('Content-Transfer-Encoding: %s' % encoding)
+ logger.error('Content-Type: %s' % part['headers'].get('Content-Type'))
+ raise
@property
def alternatives(self):
@@ -269,14 +282,14 @@ class PixelatedMail(Mail):
hdoc_headers = self.hdoc.content['headers']
for header in ['To', 'Cc', 'Bcc']:
- header_value = hdoc_headers.get(header)
+ header_value = self._decode_header(hdoc_headers.get(header))
if not header_value:
continue
_headers[header] = header_value if type(header_value) is list else header_value.split(',')
- _headers[header] = map(lambda x: x.strip(), compact(_headers[header]))
+ _headers[header] = [head.strip() for head in compact(_headers[header])]
for header in ['From', 'Subject']:
- _headers[header] = hdoc_headers.get(header)
+ _headers[header] = self._decode_header(hdoc_headers.get(header))
_headers['Date'] = self._get_date()
@@ -290,18 +303,34 @@ class PixelatedMail(Mail):
return _headers
+ def _decode_header(self, header):
+ if not header:
+ return None
+ if isinstance(header, list):
+ return [decode_header(entry)[0][0] for entry in header]
+ else:
+ return decode_header(header)[0][0]
+
def _get_date(self):
date = self.hdoc.content.get('date', None)
if not date:
- date = self.hdoc.content['received'].split(";")[-1].strip()
+ received = self.hdoc.content.get('received', None)
+ if received:
+ 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))
+ date = pixelated.support.date.iso_now()
return dateparser.parse(date).isoformat()
@property
def security_casing(self):
casing = {"imprints": [], "locks": []}
casing["imprints"] = self.signature_information
- if self.encrypted:
+ if self.encrypted == "true":
casing["locks"] = [{"state": "valid"}]
+ elif self.encrypted == "fail":
+ casing["locks"] = [{"state": "failure"}]
return casing
@property
@@ -377,8 +406,7 @@ class PixelatedMail(Mail):
@property
def encrypted(self):
- return self.hdoc.content["headers"].get("OpenPGP", None) is not None or \
- self.hdoc.content["headers"].get("X-Pixelated-encryption-status", "false") == "true"
+ return self.hdoc.content["headers"].get("X-Pixelated-encryption-status", "false")
@property
def bounced(self):
diff --git a/service/pixelated/adapter/search/__init__.py b/service/pixelated/adapter/search/__init__.py
index 0b1a1034..91eff4c3 100644
--- a/service/pixelated/adapter/search/__init__.py
+++ b/service/pixelated/adapter/search/__init__.py
@@ -17,10 +17,11 @@
from pixelated.support.encrypted_file_storage import EncryptedFileStorage
import os
+import re
from pixelated.adapter.model.status import Status
from pixelated.adapter.search.contacts import contacts_suggestions
from whoosh.index import FileIndex
-from whoosh.fields import *
+from whoosh.fields import Schema, ID, KEYWORD, TEXT, NUMERIC
from whoosh.qparser import QueryParser
from whoosh.qparser import MultifieldParser
from whoosh import sorting
@@ -116,8 +117,9 @@ class SearchEngine(object):
return FileIndex.create(storage, self._mail_schema(), indexname='mails')
def index_mail(self, mail):
- with self._index.writer() as writer:
- self._index_mail(writer, mail)
+ with self._write_lock:
+ with self._index.writer() as writer:
+ self._index_mail(writer, mail)
def _index_mail(self, writer, mail):
mdict = mail.as_dict()
@@ -125,23 +127,30 @@ class SearchEngine(object):
tags = mdict.get('tags', [])
tags.append(mail.mailbox_name.lower())
bounced = mail.bounced if mail.bounced else ['']
+
index_data = {
- 'sender': unicode(header.get('from', '')),
- 'subject': unicode(header.get('subject', '')),
+ 'sender': self._unicode_header_field(header.get('from', '')),
+ 'subject': self._unicode_header_field(header.get('subject', '')),
'date': milliseconds(header.get('date', '')),
- 'to': u','.join(header.get('to', [''])),
- 'cc': u','.join(header.get('cc', [''])),
- 'bcc': u','.join(header.get('bcc', [''])),
+ 'to': u','.join([h.decode('utf-8') for h in header.get('to', [''])]),
+ 'cc': u','.join([h.decode('utf-8') for h in header.get('cc', [''])]),
+ 'bcc': u','.join([h.decode('utf-8') for h in header.get('bcc', [''])]),
'tag': u','.join(unique(tags)),
'bounced': u','.join(bounced),
'body': unicode(mdict['textPlainBody']),
'ident': unicode(mdict['ident']),
'flags': unicode(','.join(unique(mail.flags))),
- 'raw': unicode(mail.raw)
+ 'raw': unicode(mail.raw.decode('utf-8'))
}
writer.update_document(**index_data)
+ def _unicode_header_field(self, field_value):
+ if not field_value:
+ return None
+
+ return unicode(field_value.decode('utf-8'))
+
def index_mails(self, mails, callback=None):
try:
with self._write_lock:
diff --git a/service/pixelated/adapter/services/mail_sender.py b/service/pixelated/adapter/services/mail_sender.py
index 9f42fbbc..bbcc1721 100644
--- a/service/pixelated/adapter/services/mail_sender.py
+++ b/service/pixelated/adapter/services/mail_sender.py
@@ -14,7 +14,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/>.
from StringIO import StringIO
-import re
+from email.utils import parseaddr
from twisted.internet.defer import Deferred, fail
from twisted.mail.smtp import SMTPSenderFactory
@@ -28,35 +28,22 @@ class SMTPDownException(Exception):
class MailSender(object):
+
def __init__(self, account_email_address, ensure_smtp_is_running_cb):
self.ensure_smtp_is_running_cb = ensure_smtp_is_running_cb
self.account_email_address = account_email_address
- def recepients_normalizer(self, mail_list):
- return set(mail_list)
-
- def get_email_addresses(self, mail_list):
- clean_mail_list = []
- for mail_address in mail_list:
- if "<" in mail_address:
- match = re.search(r'<(.*)>', mail_address)
- clean_mail_list.append(match.group(1))
- else:
- clean_mail_list.append(mail_address)
- return self.recepients_normalizer(clean_mail_list)
-
def sendmail(self, mail):
if self.ensure_smtp_is_running_cb():
recipients = flatten([mail.to, mail.cc, mail.bcc])
- normalized_recipients = self.get_email_addresses(recipients)
- resultDeferred = Deferred()
- senderFactory = SMTPSenderFactory(
+ result_deferred = Deferred()
+ sender_factory = SMTPSenderFactory(
fromEmail=self.account_email_address,
- toEmail=normalized_recipients,
+ toEmail=set([parseaddr(recipient)[1] for recipient in recipients]),
file=StringIO(mail.to_smtp_format()),
- deferred=resultDeferred)
+ deferred=result_deferred)
- reactor.connectTCP('localhost', 4650, senderFactory)
+ reactor.connectTCP('localhost', 4650, sender_factory)
- return resultDeferred
+ return result_deferred
return fail(SMTPDownException())
diff --git a/service/pixelated/adapter/services/mail_service.py b/service/pixelated/adapter/services/mail_service.py
index 5ef0a188..03889f82 100644
--- a/service/pixelated/adapter/services/mail_service.py
+++ b/service/pixelated/adapter/services/mail_service.py
@@ -14,13 +14,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.adapter.model.mail import InputMail
+from pixelated.adapter.services.tag_service import extract_reserved_tags
-class MailService:
- __slots__ = ['leap_session', 'account', 'mailbox_name']
+class MailService(object):
- def __init__(self, mailboxes, mail_sender, tag_service, soledad_querier, search_engine):
- self.tag_service = tag_service
+ def __init__(self, mailboxes, mail_sender, soledad_querier, search_engine):
self.mailboxes = mailboxes
self.querier = soledad_querier
self.search_engine = search_engine
@@ -36,7 +35,7 @@ class MailService:
def update_tags(self, mail_id, new_tags):
new_tags = self._filter_white_space_tags(new_tags)
- reserved_words = self.tag_service.extract_reserved(new_tags)
+ reserved_words = extract_reserved_tags(new_tags)
if len(reserved_words):
raise ValueError('None of the following words can be used as tags: ' + ' '.join(reserved_words))
new_tags = self._favor_existing_tags_casing(new_tags)
@@ -47,16 +46,16 @@ class MailService:
return mail
def _filter_white_space_tags(self, tags):
- return filter(bool, map(lambda e: e.strip(), tags))
+ return [tag.strip() for tag in tags if not tag.isspace()]
def _favor_existing_tags_casing(self, new_tags):
- current_tags = map(lambda tag: tag['name'], self.search_engine.tags(query='', skip_default_tags=True))
- current_tags_lower = map(lambda tag: tag.lower(), current_tags)
+ current_tags = [tag['name'] for tag in self.search_engine.tags(query='', skip_default_tags=True)]
+ current_tags_lower = [tag.lower() for tag in current_tags]
def _use_current_casing(new_tag_lower):
return current_tags[current_tags_lower.index(new_tag_lower)]
- return map(lambda new_tag: _use_current_casing(new_tag.lower()) if new_tag.lower() in current_tags_lower else new_tag, new_tags)
+ return [_use_current_casing(new_tag.lower()) if new_tag.lower() in current_tags_lower else new_tag for new_tag in new_tags]
def mail(self, mail_id):
return self.querier.mail(mail_id)
diff --git a/service/pixelated/adapter/services/mailbox.py b/service/pixelated/adapter/services/mailbox.py
index f934abcc..a4029d78 100644
--- a/service/pixelated/adapter/services/mailbox.py
+++ b/service/pixelated/adapter/services/mailbox.py
@@ -15,7 +15,7 @@
# along with Pixelated. If not, see <http://www.gnu.org/licenses/>.
-class Mailbox:
+class Mailbox(object):
def __init__(self, mailbox_name, querier, search_engine):
self.mailbox_name = mailbox_name
@@ -23,6 +23,10 @@ class Mailbox:
self.search_engine = search_engine
self.querier = querier
+ @property
+ def fresh(self):
+ return self.querier.get_lastuid(self.mailbox_name) == 0
+
def mail(self, mail_id):
return self.querier.mail(mail_id)
diff --git a/service/pixelated/adapter/services/mailboxes.py b/service/pixelated/adapter/services/mailboxes.py
index c761255c..a7a3a591 100644
--- a/service/pixelated/adapter/services/mailboxes.py
+++ b/service/pixelated/adapter/services/mailboxes.py
@@ -17,7 +17,7 @@ from pixelated.adapter.services.mailbox import Mailbox
from pixelated.adapter.listeners.mailbox_indexer_listener import MailboxIndexerListener
-class Mailboxes():
+class Mailboxes(object):
def __init__(self, account, soledad_querier, search_engine):
self.account = account
diff --git a/service/pixelated/adapter/services/tag_service.py b/service/pixelated/adapter/services/tag_service.py
index 601392bb..c51da625 100644
--- a/service/pixelated/adapter/services/tag_service.py
+++ b/service/pixelated/adapter/services/tag_service.py
@@ -15,13 +15,9 @@
# along with Pixelated. If not, see <http://www.gnu.org/licenses/>.
from pixelated.adapter.model.tag import Tag
+SPECIAL_TAGS = {Tag('inbox', True), Tag('sent', True), Tag('drafts', True), Tag('trash', True), Tag('ALL', True)}
-class TagService:
- instance = None
- SPECIAL_TAGS = {Tag('inbox', True), Tag('sent', True), Tag('drafts', True), Tag('trash', True), Tag('ALL', True)}
-
- @classmethod
- def extract_reserved(cls, tags):
- tags = map(lambda tag: tag.lower(), tags)
- return {tag.name for tag in cls.SPECIAL_TAGS if tag.name in tags}
+def extract_reserved_tags(tags):
+ tags = [tag.lower() for tag in tags]
+ return {tag.name for tag in SPECIAL_TAGS if tag.name in tags}
diff --git a/service/pixelated/adapter/soledad/soledad_facade_mixin.py b/service/pixelated/adapter/soledad/soledad_facade_mixin.py
index 1df038ea..280fc81e 100644
--- a/service/pixelated/adapter/soledad/soledad_facade_mixin.py
+++ b/service/pixelated/adapter/soledad/soledad_facade_mixin.py
@@ -21,25 +21,25 @@ class SoledadDbFacadeMixin(object):
return self.soledad.get_from_index('by-type', 'flags')
def get_all_flags_by_mbox(self, mbox):
- return self.soledad.get_from_index('by-type-and-mbox', 'flags', mbox)
+ return self.soledad.get_from_index('by-type-and-mbox', 'flags', mbox) if mbox else []
def get_content_by_phash(self, phash):
- content = self.soledad.get_from_index('by-type-and-payloadhash', 'cnt', phash)
+ content = self.soledad.get_from_index('by-type-and-payloadhash', 'cnt', phash) if phash else []
if len(content):
return content[0]
def get_flags_by_chash(self, chash):
- flags = self.soledad.get_from_index('by-type-and-contenthash', 'flags', chash)
+ flags = self.soledad.get_from_index('by-type-and-contenthash', 'flags', chash) if chash else []
if len(flags):
return flags[0]
def get_header_by_chash(self, chash):
- header = self.soledad.get_from_index('by-type-and-contenthash', 'head', chash)
+ header = self.soledad.get_from_index('by-type-and-contenthash', 'head', chash) if chash else []
if len(header):
return header[0]
def get_recent_by_mbox(self, mbox):
- return self.soledad.get_from_index('by-type-and-mbox', 'rct', mbox)
+ return self.soledad.get_from_index('by-type-and-mbox', 'rct', mbox) if mbox else []
def put_doc(self, doc):
return self.soledad.put_doc(doc)
@@ -51,13 +51,16 @@ class SoledadDbFacadeMixin(object):
return self.soledad.delete_doc(doc)
def idents_by_mailbox(self, mbox):
- return set(doc.content['chash'] for doc in self.soledad.get_from_index('by-type-and-mbox-and-deleted', 'flags', mbox, '0'))
+ return set(doc.content['chash'] for doc in self.soledad.get_from_index('by-type-and-mbox-and-deleted', 'flags', mbox, '0')) if mbox else set()
def get_all_mbox(self):
return self.soledad.get_from_index('by-type', 'mbox')
def get_mbox(self, mbox):
- return self.soledad.get_from_index('by-type-and-mbox', 'mbox', mbox)
+ return self.soledad.get_from_index('by-type-and-mbox', 'mbox', mbox) if mbox else []
+
+ def get_lastuid(self, mbox_doc):
+ return mbox_doc.content['lastuid']
def get_search_index_masterkey(self):
return self.soledad.get_from_index('by-type', 'index_key')
diff --git a/service/pixelated/adapter/soledad/soledad_writer_mixin.py b/service/pixelated/adapter/soledad/soledad_writer_mixin.py
index 869f7c07..9c5eb47a 100644
--- a/service/pixelated/adapter/soledad/soledad_writer_mixin.py
+++ b/service/pixelated/adapter/soledad/soledad_writer_mixin.py
@@ -13,7 +13,6 @@
#
# 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.adapter.model.mail import PixelatedMail
from pixelated.adapter.soledad.soledad_facade_mixin import SoledadDbFacadeMixin
@@ -32,13 +31,13 @@ class SoledadWriterMixin(SoledadDbFacadeMixin, object):
self.put_doc(mail.fdoc)
def create_mail(self, mail, mailbox_name):
- mbox = self.get_mbox(mailbox_name)[0]
- uid = mbox.content['lastuid'] + 1
+ mbox_doc = self.get_mbox(mailbox_name)[0]
+ uid = self.get_lastuid(mbox_doc)
[self.create_doc(doc) for doc in mail.get_for_save(next_uid=uid, mailbox=mailbox_name)]
- mbox.content['lastuid'] = uid
- self.put_doc(mbox)
+ mbox_doc.content['lastuid'] = uid + 1
+ self.put_doc(mbox_doc)
return self.mail(mail.ident)
diff --git a/service/pixelated/bitmask_libraries/certs.py b/service/pixelated/bitmask_libraries/certs.py
index ed597ca8..4ee28a19 100644
--- a/service/pixelated/bitmask_libraries/certs.py
+++ b/service/pixelated/bitmask_libraries/certs.py
@@ -14,6 +14,8 @@
# 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
+import requests
+import json
from leap.common import ca_bundle
@@ -46,7 +48,17 @@ class LeapCertificate(object):
def _local_server_cert(self):
cert_file = os.path.join(self._certs_home, '%s.ca.crt' % self._server_name)
- if os.path.isfile(cert_file):
- return cert_file
- else:
- return None
+ if not os.path.isfile(cert_file):
+ self._download_server_cert(cert_file)
+
+ return cert_file
+
+ def _download_server_cert(self, cert_file_name):
+ response = requests.get('https://%s/provider.json' % self._server_name)
+ provider_data = json.loads(response.content)
+ ca_cert_uri = str(provider_data['ca_cert_uri'])
+
+ response = requests.get(ca_cert_uri)
+ with open(cert_file_name, 'w') as file:
+ file.write(response.content)
+ file.close
diff --git a/service/pixelated/bitmask_libraries/session.py b/service/pixelated/bitmask_libraries/session.py
index 9f21fbe6..b23d964f 100644
--- a/service/pixelated/bitmask_libraries/session.py
+++ b/service/pixelated/bitmask_libraries/session.py
@@ -14,7 +14,6 @@
# 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 errno
-import logging
import traceback
import sys
diff --git a/service/pixelated/bitmask_libraries/smtp.py b/service/pixelated/bitmask_libraries/smtp.py
index d5236e8e..d4f68f94 100644
--- a/service/pixelated/bitmask_libraries/smtp.py
+++ b/service/pixelated/bitmask_libraries/smtp.py
@@ -55,7 +55,6 @@ class LeapSmtp(object):
if not os.path.exists(os.path.dirname(cert_path)):
os.makedirs(os.path.dirname(cert_path))
- session = requests.session()
cert_url = '%s/%s/cert' % (self._provider.api_uri, self._provider.api_version)
cookies = {"_session_id": self._srp_session.session_id}
@@ -94,7 +93,7 @@ class LeapSmtp(object):
if not self._smtp_service:
try:
self.start()
- except Exception as e:
+ except:
logger.warning("Couldn't start the SMTP server now, will try again when the user tries to use it")
return False
return True
diff --git a/service/pixelated/bitmask_libraries/soledad.py b/service/pixelated/bitmask_libraries/soledad.py
index e6607bde..1c46f2ab 100644
--- a/service/pixelated/bitmask_libraries/soledad.py
+++ b/service/pixelated/bitmask_libraries/soledad.py
@@ -18,7 +18,7 @@ import errno
import os
from leap.keymanager import KeyManager
from leap.soledad.client import Soledad
-from leap.soledad.common.crypto import WrongMac, UnknownMacMethod, MacMethods
+from leap.soledad.common.crypto import WrongMac, UnknownMacMethod
from .certs import which_bundle
@@ -69,7 +69,7 @@ class SoledadSession(object):
return Soledad(self.leap_srp_session.uuid, unicode(encryption_passphrase), secrets,
local_db, server_url, which_bundle(self.provider), self.leap_srp_session.token, defer_encryption=False)
- except (WrongMac, UnknownMacMethod, MacMethods), e:
+ except (WrongMac, UnknownMacMethod), e:
raise SoledadWrongPassphraseException(e)
def _leap_path(self):
diff --git a/service/pixelated/certificates/try.pixelated-project.org.ca.crt b/service/pixelated/certificates/try.pixelated-project.org.ca.crt
deleted file mode 100644
index 52f20468..00000000
--- a/service/pixelated/certificates/try.pixelated-project.org.ca.crt
+++ /dev/null
@@ -1,34 +0,0 @@
------BEGIN CERTIFICATE-----
-MIIFnzCCA4egAwIBAgIBATANBgkqhkiG9w0BAQ0FADBiMRUwEwYDVQQKDAxMRUFQ
-X0V4YW1wbGUxKjAoBgNVBAsMIWh0dHBzOi8vdHJ5LnBpeGVsYXRlZC1wcm9qZWN0
-Lm9yZzEdMBsGA1UEAwwUTEVBUF9FeGFtcGxlIFJvb3QgQ0EwHhcNMTQxMjA4MDAw
-MDAwWhcNMjQxMjA4MDAwMDAwWjBiMRUwEwYDVQQKDAxMRUFQX0V4YW1wbGUxKjAo
-BgNVBAsMIWh0dHBzOi8vdHJ5LnBpeGVsYXRlZC1wcm9qZWN0Lm9yZzEdMBsGA1UE
-AwwUTEVBUF9FeGFtcGxlIFJvb3QgQ0EwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAw
-ggIKAoICAQDFkKhtf99Ybl+iLPE/G5XNCuco4rOeF4Br1ezPLSOj34jEwgrVapHj
-xliFTwRE0Hkbohlh2CHDm8+slzgo7v2BN3XUmWy3D6XqfJwAS7UT4SbkLvZ/XFuH
-hUiGxvBk5OSu6oi/qT0mmJaNk4CjbSzxQ2VOoLFpguhgQ5SKMHb3nYpmOg+gxWUz
-tACLqV/33DeJb1bhrqKkfo1WIJ0mAJMq+re1vFYe0J/TdOmTxpXULhAlreg1QDuY
-K1Tm7IWaflxLEVsUG3c8JItR2ksKPs54DxpJdILvauO2oAHdHH+FPnmtiF0fDaJQ
-lNa4GbAOhAe063+vrTgfX+9BREhCU8Pn28ZIcOKyv+qqdgBSZtNyb21Lj+eCRlTA
-8/TIbb29bMDhNJKxarayrfLwe4tx9In0EAhoXYBPvrf11OGuY+wcf2xxXHNMMHwb
-NLr909JYBbHI10VvNUgviEir0h0DYvCAH+nUYi0cateUJG6qCE+cndiVF5VQ4/7y
-7UuGo8rT2nYz06JU7QlKyfTkphc1PkXdCjdhKF4jfsVt84TQGlRr5jzBhz47PQqj
-eZ87IQCkhzYD+XnaD2gT++B9i9wezn8uJo4Kjf+CRU51aKHw5U9DNTdXaAmyfwJ6
-WV1udBbY96gBqyPpGoaSadsG1WlJKYliksh983Bus4negYAIet+NKQIDAQABo2Aw
-XjAdBgNVHQ4EFgQU2mxnw5M2EwE6ibGfBeSF6hwcasYwDgYDVR0PAQH/BAQDAgIE
-MAwGA1UdEwQFMAMBAf8wHwYDVR0jBBgwFoAU2mxnw5M2EwE6ibGfBeSF6hwcasYw
-DQYJKoZIhvcNAQENBQADggIBAGvJMfLvi1tiYDUIQpNE9G9haX2aIwVK0GEMcymW
-g+/59RjhrlprE6yygzzNQ0H4C5L8EYEslEmQ/YJxl2/MzSMhR4AoYocoW5fJkdQk
-P88UQ8fb7DArs6OEnC5AMxG1izFL4CfgqHffcL5DaEPi/i/Xf1M5ZyEZF/Azqn1C
-CVH9VggPpN+ivKy8BBrgabaZpSYs9iVwsRxH+Gorv9mIaxhO2iIwZKk5v9PWISbi
-ukGFT8XkLH9EmWZbZj5IYJkYP0R5q4ljDjd9CUQ9vWoxhgmhV1X3PW/gp+1cZ3ci
-PAgcab6kyfsMjJm0NWnTUmbIcpKapWRP8naxNKboNgNL+GaKv0YgfgmB2gkIjsBM
-yvQ3fdSgs/903JsZmGXEcVzqWDK+5rfZrR7PBwCUyiJmZSR//Xcppts6b/Uh9BSx
-8dXDYTySliP8ncY3O5R/SGv6CHtrVwkjEZwXeghCqYaZjVPHCl3JDdK98SyFCbT5
-iPF9+mfL0lHUzr8tZCkITel0/ci5L7o7PRSFl6z4Vii/tqF+MpM/jdTPCvw91CjQ
-WTFI7iuy+nvjxxB20mUyOl4rP9wfwq+5HprqyaeVZHrp9AvaknOc/nd/UEiYHl5d
-hvg2snkn8lort2HmB1MylrsI9Q4dbtzdugm/dM7+n0Se8HF9lljpIUnocqCnYFtt
-+G0O
------END CERTIFICATE-----
-
diff --git a/service/pixelated/config/__init__.py b/service/pixelated/config/__init__.py
index f9c43153..2045354e 100644
--- a/service/pixelated/config/__init__.py
+++ b/service/pixelated/config/__init__.py
@@ -25,7 +25,7 @@ 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.debug import init_debugger
+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
@@ -36,13 +36,14 @@ 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
def initialize():
args = parse_args()
app = App()
- init_debugger(args)
+ init_logging(args)
init_leap_cert(args)
if args.register:
diff --git a/service/pixelated/config/app_factory.py b/service/pixelated/config/app_factory.py
index f63b49ed..f20b1229 100644
--- a/service/pixelated/config/app_factory.py
+++ b/service/pixelated/config/app_factory.py
@@ -33,7 +33,6 @@ from pixelated.adapter.listeners.mailbox_indexer_listener import MailboxIndexerL
import pixelated.bitmask_libraries.session as LeapSession
from pixelated.bitmask_libraries.leap_srp import LeapAuthException
from requests.exceptions import ConnectionError
-from pixelated.adapter.services.tag_service import TagService
from leap.common.events import (
register,
unregister,
@@ -100,14 +99,13 @@ def init_app(app, leap_home, leap_session):
soledad_querier = SoledadQuerier(soledad=leap_session.account._soledad)
- tag_service = TagService()
search_engine = SearchEngine(soledad_querier, agent_home=leap_home)
pixelated_mail_sender = MailSender(leap_session.account_email(),
lambda: leap_session.smtp.ensure_running())
pixelated_mailboxes = Mailboxes(leap_session.account, soledad_querier, search_engine)
draft_service = DraftService(pixelated_mailboxes)
- mail_service = MailService(pixelated_mailboxes, pixelated_mail_sender, tag_service, soledad_querier, search_engine)
+ mail_service = MailService(pixelated_mailboxes, pixelated_mail_sender, soledad_querier, search_engine)
MailboxIndexerListener.SEARCH_ENGINE = search_engine
InputMail.FROM_EMAIL_ADDRESS = leap_session.account_email()
diff --git a/service/pixelated/config/debug.py b/service/pixelated/config/debug.py
deleted file mode 100644
index d91d3a34..00000000
--- a/service/pixelated/config/debug.py
+++ /dev/null
@@ -1,40 +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 logging
-import sys
-import os
-from twisted.python import log
-
-
-def init_debugger(args):
- debug_enabled = args.debug or os.environ.get('DEBUG', False)
- log.startLogging(sys.stdout)
-
- if debug_enabled:
- logging.basicConfig(level=logging.DEBUG,
- format='%(asctime)s %(name)-12s %(levelname)-8s %(message)s',
- datefmt='%m-%d %H:%M',
- filename='/tmp/leap.log',
- filemode='w') # define a Handler which writes INFO messages or higher to the sys.stderr
-
- console = logging.StreamHandler()
- console.setLevel(logging.DEBUG)
- formatter = logging.Formatter('%(name)-12s: %(levelname)-8s %(message)s')
- console.setFormatter(formatter)
- logging.getLogger('').addHandler(console)
-
- return debug_enabled
diff --git a/service/pixelated/config/logging_setup.py b/service/pixelated/config/logging_setup.py
new file mode 100644
index 00000000..a15413a0
--- /dev/null
+++ b/service/pixelated/config/logging_setup.py
@@ -0,0 +1,65 @@
+#
+# 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 logging
+import socket
+import sys
+import os
+from twisted.python import log
+from twisted.python import util
+
+
+def init_logging(args):
+ debug_enabled = args.debug or os.environ.get('DEBUG', False)
+
+ logging.basicConfig(level=logging.DEBUG if debug_enabled else logging.WARNING,
+ format='[%(asctime)s] ' + socket.gethostname() + ' %(name)-12s %(levelname)-8s %(message)s',
+ datefmt='%m-%d %H:%M:%S',
+ filemode='a')
+
+ if debug_enabled:
+ init_debugger()
+
+ log.startLogging(sys.stdout)
+
+
+def init_debugger():
+ formatter = logging.Formatter('%(name)-12s: %(levelname)-8s %(message)s')
+ console = logging.StreamHandler()
+ console.setLevel(logging.DEBUG)
+ console.setFormatter(formatter)
+ logging.getLogger('').addHandler(console)
+
+
+class PixelatedLogObserver(log.FileLogObserver):
+
+ """ FileLogObserver with a customized format """
+ def emit(self, event):
+ text = log.textFromEventDict(event)
+
+ if text is None:
+ return
+
+ self.timeFormat = '[%Y-%m-%d %H:%M:%S]'
+ time_str = self.formatTime(event['time'])
+
+ fmt_dict = {'text': text.replace('\n', '\n\t')}
+ msg_str = log._safeFormat('%(text)s\n', fmt_dict)
+
+ logging.debug(str(event))
+
+ util.untilConcludes(self.write, time_str + ' ' + socket.gethostname() + ' ' + msg_str)
+ util.untilConcludes(self.flush)
diff --git a/service/pixelated/resources/sync_info_resource.py b/service/pixelated/resources/sync_info_resource.py
index 5aa94218..791c5add 100644
--- a/service/pixelated/resources/sync_info_resource.py
+++ b/service/pixelated/resources/sync_info_resource.py
@@ -32,7 +32,7 @@ class SyncInfoResource(Resource):
return self.current / float(self.total)
def set_sync_info(self, soledad_sync_status):
- self.current, self.total = map(int, soledad_sync_status.content.split('/'))
+ self.current, self.total = [int(x) for x in soledad_sync_status.content.split('/')]
def render_GET(self, request):
_sync_info = {
diff --git a/service/pixelated/support/ext_fetch.py b/service/pixelated/support/ext_fetch.py
index ab0def9f..2db5dd1d 100644
--- a/service/pixelated/support/ext_fetch.py
+++ b/service/pixelated/support/ext_fetch.py
@@ -1,14 +1,35 @@
import leap.mail.imap.fetch as fetch
-def mark_as_encrypted(f):
+def mark_as_encrypted_inline(f):
def w(*args, **kwargs):
- msg, was_decrypted = f(*args)
- msg.add_header('X-Pixelated-encryption-status', 'true' if was_decrypted else 'false')
- return msg, was_decrypted
+ msg, valid_sign = f(*args)
+ is_encrypted = fetch.PGP_BEGIN in args[1].as_string() and fetch.PGP_END in args[1].as_string()
+ decrypted_successfully = fetch.PGP_BEGIN not in msg.as_string() and fetch.PGP_END not in msg.as_string()
+
+ if not is_encrypted:
+ encrypted = 'false'
+ else:
+ if decrypted_successfully:
+ encrypted = 'true'
+ else:
+ encrypted = 'fail'
+
+ msg.add_header('X-Pixelated-encryption-status', encrypted)
+ return msg, valid_sign
+
+ return w
+
+
+def mark_as_encrypted_multipart(f):
+
+ def w(*args, **kwargs):
+ msg, valid_sign = f(*args)
+ msg.add_header('X-Pixelated-encryption-status', 'true')
+ return msg, valid_sign
return w
-fetch.LeapIncomingMail._maybe_decrypt_inline_encrypted_msg = mark_as_encrypted(fetch.LeapIncomingMail._maybe_decrypt_inline_encrypted_msg)
-fetch.LeapIncomingMail._decrypt_multipart_encrypted_msg = mark_as_encrypted(fetch.LeapIncomingMail._decrypt_multipart_encrypted_msg)
+fetch.LeapIncomingMail._maybe_decrypt_inline_encrypted_msg = mark_as_encrypted_inline(fetch.LeapIncomingMail._maybe_decrypt_inline_encrypted_msg)
+fetch.LeapIncomingMail._decrypt_multipart_encrypted_msg = mark_as_encrypted_multipart(fetch.LeapIncomingMail._decrypt_multipart_encrypted_msg)
diff --git a/service/pixelated/support/ext_keymanager_fetch_key.py b/service/pixelated/support/ext_keymanager_fetch_key.py
new file mode 100644
index 00000000..d39d1f96
--- /dev/null
+++ b/service/pixelated/support/ext_keymanager_fetch_key.py
@@ -0,0 +1,60 @@
+#
+# 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 leap.keymanager
+import requests
+import logging
+from leap.keymanager.errors import KeyNotFound
+from leap.keymanager.openpgp import OpenPGPKey
+
+
+logger = logging.getLogger(__name__)
+
+
+def patched_fetch_keys_from_server(self, address):
+ """
+ Fetch keys bound to C{address} from nickserver and insert them in
+ local database.
+
+ Instead of raising a KeyNotFound only for 404 responses, this implementation
+ raises a KeyNotFound exception for all problems.
+
+ For original see: https://github.com/leapcode/keymanager/blob/develop/src/leap/keymanager/__init__.py
+
+ :param address: The address bound to the keys.
+ :type address: str
+
+ :raise KeyNotFound: If the key was not found on nickserver.
+ """
+ # request keys from the nickserver
+ res = None
+ try:
+ res = self._get(self._nickserver_uri, {'address': address})
+ res.raise_for_status()
+ server_keys = res.json()
+ # insert keys in local database
+ if self.OPENPGP_KEY in server_keys:
+ self._wrapper_map[OpenPGPKey].put_ascii_key(
+ server_keys['openpgp'])
+ except requests.exceptions.HTTPError as e:
+ logger.warning("HTTP error retrieving key: %r" % (e,))
+ logger.warning("%s" % (res.content,))
+ raise KeyNotFound(address)
+ except Exception as e:
+ logger.warning("Error retrieving key: %r" % (e,))
+ raise KeyNotFound(address)
+
+
+leap.keymanager.KeyManager._fetch_keys_from_server = patched_fetch_keys_from_server
diff --git a/service/pixelated/support/ext_protobuf.py b/service/pixelated/support/ext_protobuf.py
index 06d7bcea..548f5fd6 100644
--- a/service/pixelated/support/ext_protobuf.py
+++ b/service/pixelated/support/ext_protobuf.py
@@ -28,9 +28,8 @@ if _platform == 'darwin':
try:
func(*args, **kwargs)
pass
- except Exception as e:
- if e.strerror == 'Socket is not connected':
- pass
+ except:
+ pass
return wrapper
diff --git a/service/test/functional/features/compose_save_draft_and_send.feature b/service/test/functional/features/compose_save_draft_and_send.feature
index 10fa1aa2..b24d4c51 100644
--- a/service/test/functional/features/compose_save_draft_and_send.feature
+++ b/service/test/functional/features/compose_save_draft_and_send.feature
@@ -15,16 +15,18 @@
# along with Pixelated. If not, see <http://www.gnu.org/licenses/>.
Feature: compose mail, save draft and send mail
+ As a user of Pixelated
+ I want to save drafts
+ So I can review and send them later
Scenario: user composes and email, save the draft, later sends the draft and checks the sent message
- Given I compose a message with
+ When I compose a message with
| subject | body |
| Pixelated rocks! | You should definitely use it. Cheers, User. |
- # And for the 'To' field I type 'ab' and chose the first contact that shows
- And for the 'To' field I enter 'pixelated@friends.org'
- And I save the draft
+ And for the 'To' field I enter 'pixelated@friends.org'
+ And I save the draft
When I open the saved draft and send it
Then I see that mail under the 'sent' tag
When I open that mail
Then I see that the subject reads 'Pixelated rocks!'
- And I see that the body reads 'You should definitely use it. Cheers, User.'
+ And I see that the body reads 'You should definitely use it. Cheers, User.'
diff --git a/service/test/functional/features/environment.py b/service/test/functional/features/environment.py
index 5e93c840..5969120a 100644
--- a/service/test/functional/features/environment.py
+++ b/service/test/functional/features/environment.py
@@ -14,11 +14,11 @@
# 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 logging
-import time
-from test.support.dispatcher.proxy import Proxy
+from test.support.dispatcher.proxy import Proxy
from test.support.integration import AppTestClient
from selenium import webdriver
+
from pixelated.resources.features_resource import FeaturesResource
@@ -50,10 +50,6 @@ def after_feature(context, feature):
context.browser.quit()
-def take_screenshot(context):
- context.browser.save_screenshot('/tmp/screenshot.jpeg')
-
-
def save_source(context):
with open('/tmp/source.html', 'w') as out:
out.write(context.browser.page_source.encode('utf8'))
diff --git a/service/test/functional/features/forward_trash_archive.feature b/service/test/functional/features/forward_trash_archive.feature
index 91e078ea..85c422d9 100644
--- a/service/test/functional/features/forward_trash_archive.feature
+++ b/service/test/functional/features/forward_trash_archive.feature
@@ -14,18 +14,19 @@
# You should have received a copy of the GNU Affero General Public License
# along with Pixelated. If not, see <http://www.gnu.org/licenses/>.
-Feature: forward_trash_archive
+Feature: forward and deletion
+ As a user of Pixelated
+ I want to forward emails using CC and Bcc features
+ So I can take actions
Scenario: User forwards a mail, add CC and BCC address, later trash the mail
Given I have a mail in my inbox
When I open the first mail in the 'inbox'
- Then I choose to forward this mail
- # And for the 'CC' field I type 'ab' and chose the first contact that shows
- # And for the 'Bcc' field I type 'fr' and chose the first contact that shows
- Given for the 'CC' field I enter 'pixelated@friends.org'
- And for the 'Bcc' field I enter 'pixelated@family.org'
- Then I forward this mail
+ And I choose to forward this mail
+ When for the 'CC' field I enter 'pixelated@friends.org'
+ And for the 'Bcc' field I enter 'pixelated@family.org'
+ And I forward this mail
When I open the first mail in the 'sent'
Then I see the mail has a cc and a bcc recipient
- And I choose to trash
+ When I choose to trash
Then I see that mail under the 'trash' tag
diff --git a/service/test/functional/features/search_and_destroy.feature b/service/test/functional/features/search_and_destroy.feature
index 6efeae8b..4ce37b78 100644
--- a/service/test/functional/features/search_and_destroy.feature
+++ b/service/test/functional/features/search_and_destroy.feature
@@ -16,15 +16,16 @@
# XXX: must implement with HTML content
-Feature: search mail and destroy
+Feature: search mail and deletion
+ As a user of pixelated
+ I want to search for emails
+ So I can manage them
- Scenario: User searches for a mail and deletes it'
+ Scenario: User searches for a mail and deletes it
Given I have a mail in my inbox
When I search for a mail with the words "the body of this message"
When I open the first mail in the mail list
Then I see one or more mails in the search results
-# Then I see if the mail has html content
When I try to delete the first mail
- # Then I learn that the mail was deleted
When I select the tag 'trash'
Then the deleted mail is there
diff --git a/service/test/functional/features/steps/__init__.py b/service/test/functional/features/steps/__init__.py
index e69de29b..2756a319 100644
--- a/service/test/functional/features/steps/__init__.py
+++ b/service/test/functional/features/steps/__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/functional/features/steps/common.py b/service/test/functional/features/steps/common.py
index 558361d0..7848089b 100644
--- a/service/test/functional/features/steps/common.py
+++ b/service/test/functional/features/steps/common.py
@@ -20,8 +20,8 @@ from selenium.common.exceptions import TimeoutException
from hamcrest import *
-def wait_until_element_is_invisible_by_locator(context, locator_tuple):
- wait = WebDriverWait(context.browser, 10)
+def wait_until_element_is_invisible_by_locator(context, locator_tuple, timeout=10):
+ wait = WebDriverWait(context.browser, timeout)
wait.until(EC.invisibility_of_element_located(locator_tuple))
@@ -30,18 +30,18 @@ def wait_until_element_is_deleted(context, locator_tuple, timeout=10):
wait.until(lambda s: len(s.find_elements(locator_tuple[0], locator_tuple[1])) == 0)
-def wait_for_user_alert_to_disapear(context):
- wait_until_element_is_invisible_by_locator(context, (By.ID, 'user-alerts'))
+def wait_for_user_alert_to_disapear(context, timeout=10):
+ wait_until_element_is_invisible_by_locator(context, (By.ID, 'user-alerts'), timeout)
-def wait_until_elements_are_visible_by_locator(context, locator_tuple):
- wait = WebDriverWait(context.browser, 10)
+def wait_until_elements_are_visible_by_locator(context, locator_tuple, timeout=10):
+ wait = WebDriverWait(context.browser, timeout)
wait.until(EC.presence_of_all_elements_located(locator_tuple))
return context.browser.find_elements(locator_tuple[0], locator_tuple[1])
-def wait_until_element_is_visible_by_locator(context, locator_tuple):
- wait = WebDriverWait(context.browser, 10)
+def wait_until_element_is_visible_by_locator(context, locator_tuple, timeout=10):
+ wait = WebDriverWait(context.browser, timeout)
wait.until(EC.visibility_of_element_located(locator_tuple))
return context.browser.find_element(locator_tuple[0], locator_tuple[1])
@@ -93,8 +93,8 @@ def element_should_have_content(context, css_selector, content):
assert_that(e.text, equal_to(content))
-def wait_until_button_is_visible(context, title):
- wait = WebDriverWait(context.browser, 10)
+def wait_until_button_is_visible(context, title, timeout=10):
+ wait = WebDriverWait(context.browser, timeout)
locator_tuple = (By.XPATH, ("//%s[contains(.,'%s')]" % ('button', title)))
wait.until(EC.visibility_of_element_located(locator_tuple))
diff --git a/service/test/functional/features/steps/compose.py b/service/test/functional/features/steps/compose.py
index cf75979e..aeef11c4 100644
--- a/service/test/functional/features/steps/compose.py
+++ b/service/test/functional/features/steps/compose.py
@@ -20,7 +20,7 @@ from common import *
from hamcrest import *
-@given('I compose a message with')
+@when('I compose a message with')
def impl(context):
take_screenshot(context, '/tmp/screenshot.jpeg')
toggle = context.browser.find_element_by_id('compose-mails-trigger')
@@ -31,31 +31,19 @@ def impl(context):
fill_by_xpath(context, '//*[@id="text-box"]', row['body'])
-@given("for the '{recipients_field}' field I type '{to_type}' and chose the first contact that shows")
-def choose_impl(context, recipients_field, to_type):
- browser = context.browser
- browser.find_element_by_css_selector('#recipients-to-area span input.tt-input').click()
- recipients_field = recipients_field.lower()
- css_selector = '#recipients-%s-area' % recipients_field
- recipients_element = browser.find_element_by_css_selector(css_selector)
- recipients_element.find_element_by_css_selector('.tt-input').send_keys(to_type)
- wait_until_element_is_visible_by_locator(context, (By.CSS_SELECTOR, '.tt-dropdown-menu div div'))
- browser.find_element_by_css_selector('.tt-dropdown-menu div div').click()
-
-
-@given("for the '{recipients_field}' field I enter '{to_type}'")
+@when("for the '{recipients_field}' field I enter '{to_type}'")
def enter_address_impl(context, recipients_field, to_type):
_enter_recipient(context, recipients_field, to_type + "\n")
-@then("for the '{recipients_field}' field I type '{to_type}' and chose the first contact that shows")
+@when("for the '{recipients_field}' field I type '{to_type}' and chose the first contact that shows")
def choose_impl(context, recipients_field, to_type):
_enter_recipient(context, recipients_field, to_type)
sleep(1)
find_element_by_css_selector(context, '.tt-dropdown-menu div div').click()
-@given('I save the draft')
+@when('I save the draft')
def save_impl(context):
context.browser.find_element_by_id('draft-button').click()
diff --git a/service/test/functional/features/steps/mail_list.py b/service/test/functional/features/steps/mail_list.py
index 6a764568..4122f065 100644
--- a/service/test/functional/features/steps/mail_list.py
+++ b/service/test/functional/features/steps/mail_list.py
@@ -54,7 +54,7 @@ def impl(context, tag):
context.execute_steps(u'When I open the first mail in the mail list')
-@then('I open the mail I previously tagged')
+@when('I open the mail I previously tagged')
def impl(context):
open_current_mail(context)
@@ -68,3 +68,9 @@ def impl(context):
@then('the deleted mail is there')
def impl(context):
check_current_mail_is_visible(context)
+
+
+@given('I have mails')
+def impl(context):
+ elements = wait_until_elements_are_visible_by_locator(context, (By.XPATH, '//*[@id="mail-list"]//a'))
+ assert len(elements) > 0
diff --git a/service/test/functional/features/steps/mail_view.py b/service/test/functional/features/steps/mail_view.py
index ca0d68dc..98591aa4 100644
--- a/service/test/functional/features/steps/mail_view.py
+++ b/service/test/functional/features/steps/mail_view.py
@@ -46,9 +46,10 @@ def impl(context, tag):
e = wait_until_element_is_visible_by_locator(context, (By.ID, 'new-tag-input'))
e.send_keys(tag)
e.send_keys(Keys.ENTER)
+ wait_until_element_is_visible_by_locator(context, (By.XPATH, '//li[@data-tag="%s"]' % tag))
-@then('I reply to it')
+@when('I reply to it')
def impl(context):
click_button(context, 'Reply')
click_button(context, 'Send')
@@ -72,20 +73,20 @@ def impl(context):
assert_that(e.text, equal_to('Your message was moved to trash!'))
-@then('I choose to forward this mail')
+@when('I choose to forward this mail')
def impl(context):
wait_until_button_is_visible(context, 'Forward')
click_button(context, 'Forward')
-@then('I forward this mail')
+@when('I forward this mail')
def impl(context):
- context.execute_steps(u'Given I save the draft') # FIXME: this won't be necessary after #89 is done
+ context.execute_steps(u'When I save the draft') # FIXME: this won't be necessary after #89 is done
wait_until_button_is_visible(context, 'Send')
click_button(context, 'Send')
-@then('I remove all tags')
+@when('I remove all tags')
def impl(context):
e = find_element_by_css_selector(context, '.tagsArea')
tags = e.find_elements_by_css_selector('.tag')
@@ -94,7 +95,7 @@ def impl(context):
tag.click()
-@then('I choose to trash')
+@when('I choose to trash')
def impl(context):
context.browser.execute_script("$('button#view-more-actions').click()")
click_button(context, 'Delete this message', 'span')
diff --git a/service/test/functional/features/steps/tag_list.py b/service/test/functional/features/steps/tag_list.py
index 62b2571f..348b121a 100644
--- a/service/test/functional/features/steps/tag_list.py
+++ b/service/test/functional/features/steps/tag_list.py
@@ -21,10 +21,25 @@ def click_first_element_with_class(context, classname):
elements[0].click()
+def is_side_nax_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):
+ return
+
+ toggle = context.browser.find_elements_by_class_name('side-nav-toggle')[0]
+ toggle.click()
+
+
@when('I select the tag \'{tag}\'')
def impl(context, tag):
wait_for_user_alert_to_disapear(context)
- click_first_element_with_class(context, 'fake-left-off-canvas-toggle')
- context.browser.execute_script("window.scrollBy(0, -200)")
+ expand_side_nav(context)
+
+ wait_until_element_is_visible_by_locator(context, (By.ID, 'tag-%s' % tag), 20)
+
e = find_element_by_id(context, 'tag-%s' % tag.lower())
e.click()
diff --git a/service/test/functional/features/tag_and_reply.feature b/service/test/functional/features/tag_and_reply.feature
index 8fe4cf84..450cb92d 100644
--- a/service/test/functional/features/tag_and_reply.feature
+++ b/service/test/functional/features/tag_and_reply.feature
@@ -14,14 +14,17 @@
# You should have received a copy of the GNU Affero General Public License
# along with Pixelated. If not, see <http://www.gnu.org/licenses/>.
-Feature: tagging and replying
+Feature: Tag and reply
+ As a user of Pixelated
+ I want to tag my emails
+ So that I can easily find them
Scenario: User tags a mail, replies to it then checks that mail is in the right tag
Given I have a mail in my inbox
When I open the first mail in the 'inbox'
When I add the tag 'website' to that mail
Then I see that mail under the 'website' tag
- And I open the mail I previously tagged
+ When I open the mail I previously tagged
And I reply to it
When I select the tag 'sent'
Then I see the mail I sent
diff --git a/service/test/integration/test_contacts.py b/service/test/integration/test_contacts.py
index 3a510346..f9cde9e5 100644
--- a/service/test/integration/test_contacts.py
+++ b/service/test/integration/test_contacts.py
@@ -22,7 +22,7 @@ class ContactsTest(SoledadTestBase):
def test_TO_CC_and_BCC_fields_are_being_searched(self):
input_mail = MailBuilder().with_tags(['important']).build_input_mail()
- self.client.add_mail_to_inbox(input_mail)
+ self.add_mail_to_inbox(input_mail)
d = self.get_contacts(query='recipient')
@@ -35,7 +35,7 @@ class ContactsTest(SoledadTestBase):
def test_FROM_address_is_being_searched(self):
input_mail = MailBuilder().with_tags(['important']).build_input_mail()
- self.client.add_mail_to_inbox(input_mail)
+ self.add_mail_to_inbox(input_mail)
d = self.get_contacts(query='Sender')
@@ -45,10 +45,10 @@ class ContactsTest(SoledadTestBase):
return d
def test_trash_and_drafts_mailboxes_are_being_ignored(self):
- self.client.add_multiple_to_mailbox(1, mailbox='INBOX', to='recipient@inbox.com')
- self.client.add_multiple_to_mailbox(1, mailbox='DRAFTS', to='recipient@drafts.com')
- self.client.add_multiple_to_mailbox(1, mailbox='SENT', to='recipient@sent.com')
- self.client.add_multiple_to_mailbox(1, mailbox='TRASH', to='recipient@trash.com')
+ self.add_multiple_to_mailbox(1, mailbox='INBOX', to='recipient@inbox.com')
+ self.add_multiple_to_mailbox(1, mailbox='DRAFTS', to='recipient@drafts.com')
+ self.add_multiple_to_mailbox(1, mailbox='SENT', to='recipient@sent.com')
+ self.add_multiple_to_mailbox(1, mailbox='TRASH', to='recipient@trash.com')
d = self.get_contacts(query='recipient')
@@ -69,8 +69,8 @@ class ContactsTest(SoledadTestBase):
formatted_input_mail.with_bcc('Recipient Carbon <recipient@bcc.com>')
formatted_input_mail = formatted_input_mail.build_input_mail()
- self.client.add_mail_to_inbox(input_mail)
- self.client.add_mail_to_inbox(formatted_input_mail)
+ self.add_mail_to_inbox(input_mail)
+ self.add_mail_to_inbox(formatted_input_mail)
d = self.get_contacts(query='Recipient')
@@ -84,17 +84,17 @@ class ContactsTest(SoledadTestBase):
def test_bounced_addresses_are_ignored(self):
to_be_bounced = MailBuilder().with_to('this_mail_was_bounced@domain.com').build_input_mail()
- self.client.add_mail_to_inbox(to_be_bounced)
+ self.add_mail_to_inbox(to_be_bounced)
bounced_mail_template = MailBuilder().build_input_mail()
- bounced_mail = self.client.mailboxes.inbox().add(bounced_mail_template)
+ bounced_mail = self.mailboxes.inbox().add(bounced_mail_template)
bounced_mail.hdoc.content = self._bounced_mail_hdoc_content()
bounced_mail.save()
- self.client.search_engine.index_mail(bounced_mail)
+ self.search_engine.index_mail(bounced_mail)
not_bounced_mail = MailBuilder(
).with_tags(['important']).with_to('this_mail_was_not@bounced.com').build_input_mail()
- self.client.add_mail_to_inbox(not_bounced_mail)
+ self.add_mail_to_inbox(not_bounced_mail)
d = self.get_contacts(query='this')
diff --git a/service/test/integration/test_delete_mail.py b/service/test/integration/test_delete_mail.py
index 91dc0e9e..987cf307 100644
--- a/service/test/integration/test_delete_mail.py
+++ b/service/test/integration/test_delete_mail.py
@@ -14,14 +14,14 @@
# 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 test.support.integration import *
+from test.support.integration import SoledadTestBase, MailBuilder
class DeleteMailTest(SoledadTestBase):
def test_move_mail_to_trash_when_deleting(self):
input_mail = MailBuilder().with_subject('Mail with tags').build_input_mail()
- self.client.add_mail_to_inbox(input_mail)
+ self.add_mail_to_inbox(input_mail)
inbox_mails = self.get_mails_by_tag('inbox')
self.assertEquals(1, len(inbox_mails))
@@ -34,7 +34,7 @@ class DeleteMailTest(SoledadTestBase):
self.assertEquals(1, len(trash_mails))
def test_delete_mail_when_trashing_mail_from_trash_mailbox(self):
- mails = self.client.add_multiple_to_mailbox(1, 'trash')
+ mails = self.add_multiple_to_mailbox(1, 'trash')
self.delete_mails([mails[0].ident])
trash_mails = self.get_mails_by_tag('trash')
@@ -42,7 +42,7 @@ class DeleteMailTest(SoledadTestBase):
self.assertEqual(0, len(trash_mails))
def test_move_mail_to_trash_when_delete_multiple(self):
- mails = self.client.add_multiple_to_mailbox(5, 'inbox')
+ mails = self.add_multiple_to_mailbox(5, 'inbox')
mail_idents = [m.ident for m in mails]
self.delete_mails(mail_idents)
@@ -51,7 +51,7 @@ class DeleteMailTest(SoledadTestBase):
self.assertEquals(0, len(inbox))
def test_delete_permanently_when_mails_are_in_trash(self):
- mails = self.client.add_multiple_to_mailbox(5, 'trash')
+ mails = self.add_multiple_to_mailbox(5, 'trash')
self.delete_mails([m.ident for m in mails])
trash = self.get_mails_by_tag('trash')
diff --git a/service/test/integration/test_drafts.py b/service/test/integration/test_drafts.py
index a5901b67..3a0f120b 100644
--- a/service/test/integration/test_drafts.py
+++ b/service/test/integration/test_drafts.py
@@ -14,8 +14,8 @@
# 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 test.support.integration import *
-from mockito import *
+from test.support.integration import SoledadTestBase, MailBuilder
+from mockito import unstub, when, any
from twisted.internet.defer import Deferred
@@ -27,7 +27,7 @@ class DraftsTest(SoledadTestBase):
def test_post_sends_mail_and_deletes_previous_draft_if_it_exists(self):
# act is if sending the mail by SMTP succeeded
sendmail_deferred = Deferred()
- when(self.client.mail_sender).sendmail(any()).thenReturn(sendmail_deferred)
+ when(self.mail_sender).sendmail(any()).thenReturn(sendmail_deferred)
# creates one draft
first_draft = MailBuilder().with_subject('First draft').build_json()
@@ -56,7 +56,7 @@ class DraftsTest(SoledadTestBase):
def test_post_sends_mail_even_when_draft_does_not_exist(self):
# act is if sending the mail by SMTP succeeded
sendmail_deferred = Deferred()
- when(self.client.mail_sender).sendmail(any()).thenReturn(sendmail_deferred)
+ when(self.mail_sender).sendmail(any()).thenReturn(sendmail_deferred)
first_draft = MailBuilder().with_subject('First draft').build_json()
deferred_res = self.post_mail(first_draft)
@@ -74,7 +74,7 @@ class DraftsTest(SoledadTestBase):
return deferred_res
def post_mail(self, data):
- deferred_res, req = self.client.post('/mails', data)
+ deferred_res, req = self.post('/mails', data)
deferred_res.callback(None)
return deferred_res
diff --git a/service/test/integration/test_mark_as_read_unread.py b/service/test/integration/test_mark_as_read_unread.py
index cc09acec..6119f121 100644
--- a/service/test/integration/test_mark_as_read_unread.py
+++ b/service/test/integration/test_mark_as_read_unread.py
@@ -14,7 +14,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/>.
-from test.support.integration import *
+from test.support.integration import SoledadTestBase, MailBuilder
from pixelated.adapter.model.status import Status
@@ -22,7 +22,7 @@ class MarkAsReadUnreadTest(SoledadTestBase):
def test_mark_single_as_read(self):
input_mail = MailBuilder().build_input_mail()
- self.client.add_mail_to_inbox(input_mail)
+ self.add_mail_to_inbox(input_mail)
mails = self.get_mails_by_tag('inbox')
self.assertNotIn('read', mails[0].status)
@@ -34,7 +34,7 @@ class MarkAsReadUnreadTest(SoledadTestBase):
def test_mark_single_as_unread(self):
input_mail = MailBuilder().with_status([Status.SEEN]).build_input_mail()
- self.client.add_mail_to_inbox(input_mail)
+ self.add_mail_to_inbox(input_mail)
self.mark_many_as_unread([input_mail.ident])
mail = self.get_mails_by_tag('inbox')[0]
@@ -45,8 +45,8 @@ class MarkAsReadUnreadTest(SoledadTestBase):
input_mail = MailBuilder().with_status([Status.SEEN]).build_input_mail()
input_mail2 = MailBuilder().with_status([Status.SEEN]).build_input_mail()
- self.client.add_mail_to_inbox(input_mail)
- self.client.add_mail_to_inbox(input_mail2)
+ self.add_mail_to_inbox(input_mail)
+ self.add_mail_to_inbox(input_mail2)
self.mark_many_as_unread([input_mail.ident, input_mail2.ident])
@@ -59,8 +59,8 @@ class MarkAsReadUnreadTest(SoledadTestBase):
input_mail = MailBuilder().build_input_mail()
input_mail2 = MailBuilder().build_input_mail()
- self.client.add_mail_to_inbox(input_mail)
- self.client.add_mail_to_inbox(input_mail2)
+ self.add_mail_to_inbox(input_mail)
+ self.add_mail_to_inbox(input_mail2)
mails = self.get_mails_by_tag('inbox')
@@ -79,8 +79,8 @@ class MarkAsReadUnreadTest(SoledadTestBase):
input_mail = MailBuilder().build_input_mail()
input_mail2 = MailBuilder().with_status([Status.SEEN]).build_input_mail()
- self.client.add_mail_to_inbox(input_mail)
- self.client.add_mail_to_inbox(input_mail2)
+ self.add_mail_to_inbox(input_mail)
+ self.add_mail_to_inbox(input_mail2)
mails = self.get_mails_by_tag('inbox')
diff --git a/service/test/integration/test_retrieve_attachment.py b/service/test/integration/test_retrieve_attachment.py
index c81b684a..2c446b42 100644
--- a/service/test/integration/test_retrieve_attachment.py
+++ b/service/test/integration/test_retrieve_attachment.py
@@ -28,7 +28,7 @@ class RetrieveAttachmentTest(SoledadTestBase):
'phash': ident,
'content-type': 'text/plain; charset=US-ASCII; name="attachment_pequeno.txt"'}
- self.client.add_document_to_soledad(attachment_dict)
+ self.add_document_to_soledad(attachment_dict)
d = self.get_attachment(ident, 'base64')
diff --git a/service/test/integration/test_search.py b/service/test/integration/test_search.py
index 1de45967..f90ed80f 100644
--- a/service/test/integration/test_search.py
+++ b/service/test/integration/test_search.py
@@ -14,14 +14,14 @@
# 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 test.support.integration import *
+from test.support.integration import SoledadTestBase, MailBuilder
class SearchTest(SoledadTestBase):
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)
+ self.add_mail_to_inbox(input_mail)
d = self.get_tags()
@@ -37,7 +37,7 @@ class SearchTest(SoledadTestBase):
def test_that_tags_are_filtered_by_query(self):
input_mail = MailBuilder().with_tags(['ateu', 'catoa', 'luat', 'zuado']).build_input_mail()
- self.client.add_mail_to_inbox(input_mail)
+ self.add_mail_to_inbox(input_mail)
d = self.get_tags(q=["at"], skipDefaultTags=["true"])
@@ -53,7 +53,7 @@ class SearchTest(SoledadTestBase):
def test_tags_with_multiple_words_are_searchable(self):
input_mail = MailBuilder().with_tags(['one tag four words']).build_input_mail()
- self.client.add_mail_to_inbox(input_mail)
+ self.add_mail_to_inbox(input_mail)
first_page = self.get_mails_by_tag('"one tag four words"', page=1, window=1)
@@ -61,7 +61,7 @@ class SearchTest(SoledadTestBase):
def test_that_default_tags_are_ignorable(self):
input_mail = MailBuilder().with_tags(['sometag']).build_input_mail()
- self.client.add_mail_to_inbox(input_mail)
+ self.add_mail_to_inbox(input_mail)
d = self.get_tags(skipDefaultTags=["true"])
@@ -73,10 +73,10 @@ class SearchTest(SoledadTestBase):
return d
def test_tags_count(self):
- self.client.add_multiple_to_mailbox(num=10, mailbox='inbox', flags=['\\Recent'])
- self.client.add_multiple_to_mailbox(num=5, mailbox='inbox', flags=['\\Seen'])
- self.client.add_multiple_to_mailbox(num=3, mailbox='inbox', flags=['\\Recent'], tags=['important', 'later'])
- self.client.add_multiple_to_mailbox(num=1, mailbox='inbox', flags=['\\Seen'], tags=['important'])
+ self.add_multiple_to_mailbox(num=10, mailbox='inbox', flags=['\\Recent'])
+ self.add_multiple_to_mailbox(num=5, mailbox='inbox', flags=['\\Seen'])
+ self.add_multiple_to_mailbox(num=3, mailbox='inbox', flags=['\\Recent'], tags=['important', 'later'])
+ self.add_multiple_to_mailbox(num=1, mailbox='inbox', flags=['\\Seen'], tags=['important'])
d = self.get_tags()
@@ -91,8 +91,8 @@ class SearchTest(SoledadTestBase):
def test_search_mails_different_window(self):
input_mail = MailBuilder().build_input_mail()
input_mail2 = MailBuilder().build_input_mail()
- self.client.add_mail_to_inbox(input_mail)
- self.client.add_mail_to_inbox(input_mail2)
+ self.add_mail_to_inbox(input_mail)
+ self.add_mail_to_inbox(input_mail2)
first_page = self.get_mails_by_tag('inbox', page=1, window=1)
@@ -101,8 +101,8 @@ class SearchTest(SoledadTestBase):
def test_search_mails_with_multiple_pages(self):
input_mail = MailBuilder().build_input_mail()
input_mail2 = MailBuilder().build_input_mail()
- self.client.add_mail_to_inbox(input_mail)
- self.client.add_mail_to_inbox(input_mail2)
+ self.add_mail_to_inbox(input_mail)
+ self.add_mail_to_inbox(input_mail2)
first_page = self.get_mails_by_tag('inbox', page=1, window=1)
second_page = self.get_mails_by_tag('inbox', page=2, window=1)
@@ -114,7 +114,7 @@ class SearchTest(SoledadTestBase):
def test_page_zero_fetches_first_page(self):
input_mail = MailBuilder().build_input_mail()
- self.client.add_mail_to_inbox(input_mail)
+ self.add_mail_to_inbox(input_mail)
page = self.get_mails_by_tag('inbox', page=0, window=1)
self.assertEqual(page[0].ident, input_mail.ident)
@@ -127,8 +127,8 @@ class SearchTest(SoledadTestBase):
input_mail = MailBuilder().with_date('2014-10-15T15:15').build_input_mail()
input_mail2 = MailBuilder().with_date('2014-10-15T15:16').build_input_mail()
- self.client.add_mail_to_inbox(input_mail)
- self.client.add_mail_to_inbox(input_mail2)
+ self.add_mail_to_inbox(input_mail)
+ self.add_mail_to_inbox(input_mail2)
results = self.get_mails_by_tag('inbox')
self.assertEqual(results[0].ident, input_mail2.ident)
@@ -137,7 +137,7 @@ class SearchTest(SoledadTestBase):
def test_search_base64_body(self):
body = u'bl\xe1'
input_mail = MailBuilder().with_body(body.encode('utf-8')).build_input_mail()
- self.client.add_mail_to_inbox(input_mail)
+ self.add_mail_to_inbox(input_mail)
results = self.search(body)
self.assertGreater(len(results), 0, 'No results returned from search')
diff --git a/service/test/integration/test_soledad_querier.py b/service/test/integration/test_soledad_querier.py
index 9c7f8a81..f4c23961 100644
--- a/service/test/integration/test_soledad_querier.py
+++ b/service/test/integration/test_soledad_querier.py
@@ -17,7 +17,7 @@
import copy
import time
-from test.support.integration import *
+from test.support.integration import SoledadTestBase, MailBuilder
from leap.mail.imap.fields import WithMsgFields
@@ -25,9 +25,7 @@ class SoledadQuerierTest(SoledadTestBase, WithMsgFields):
def setUp(self):
SoledadTestBase.setUp(self)
- self.soledad = self.client.soledad
self.maxDiff = None
- self.soledad_querier = self.client.soledad_querier
def _get_empty_mailbox(self):
return copy.deepcopy(self.EMPTY_MBOX)
@@ -42,7 +40,7 @@ class SoledadQuerierTest(SoledadTestBase, WithMsgFields):
return [m for m in self.soledad.get_from_index('by-type', 'mbox') if m.content['mbox'] == mailbox_name]
def test_remove_dup_mailboxes_keeps_the_one_with_the_highest_last_uid(self):
- self.client.add_multiple_to_mailbox(3, 'INBOX') # by now we already have one inbox with 3 mails
+ self.add_multiple_to_mailbox(3, 'INBOX') # by now we already have one inbox with 3 mails
self._create_mailbox('INBOX') # now we have a duplicate
# make sure we have two
@@ -77,7 +75,7 @@ class SoledadQuerierTest(SoledadTestBase, WithMsgFields):
self.assertEqual(1, len(mails))
def test_get_mails_by_chash(self):
- mails = self.client.add_multiple_to_mailbox(3, 'INBOX')
+ mails = self.add_multiple_to_mailbox(3, 'INBOX')
chashes = [mail.ident for mail in mails]
fetched_mails = self.soledad_querier.mails(chashes)
diff --git a/service/test/integration/test_tags.py b/service/test/integration/test_tags.py
index ad723067..976b6d96 100644
--- a/service/test/integration/test_tags.py
+++ b/service/test/integration/test_tags.py
@@ -15,8 +15,8 @@
# along with Pixelated. If not, see <http://www.gnu.org/licenses/>.
import json
-from test.support.integration import *
-from pixelated.adapter.services.tag_service import TagService
+from test.support.integration import SoledadTestBase, MailBuilder
+from pixelated.adapter.services.tag_service import SPECIAL_TAGS
class TagsTest(SoledadTestBase):
@@ -26,7 +26,7 @@ class TagsTest(SoledadTestBase):
def test_add_tag_to_an_inbox_mail_and_query(self):
mail = MailBuilder().with_subject('Mail with tags').build_input_mail()
- self.client.add_mail_to_inbox(mail)
+ self.add_mail_to_inbox(mail)
self.post_tags(mail.ident, self._tags_json(['IMPORTANT']))
@@ -38,13 +38,13 @@ class TagsTest(SoledadTestBase):
def test_use_old_casing_when_same_tag_with_different_casing_is_posted(self):
mail = MailBuilder().with_subject('Mail with tags').build_input_mail()
- self.client.add_mail_to_inbox(mail)
+ self.add_mail_to_inbox(mail)
self.post_tags(mail.ident, self._tags_json(['ImPoRtAnT']))
mails = self.get_mails_by_tag('ImPoRtAnT')
self.assertEquals({'ImPoRtAnT'}, set(mails[0].tags))
another_mail = MailBuilder().with_subject('Mail with tags').build_input_mail()
- self.client.add_mail_to_inbox(another_mail)
+ self.add_mail_to_inbox(another_mail)
self.post_tags(another_mail.ident, self._tags_json(['IMPORTANT']))
mails = self.get_mails_by_tag('IMPORTANT')
self.assertEquals(0, len(mails))
@@ -55,7 +55,7 @@ class TagsTest(SoledadTestBase):
def test_tags_are_case_sensitive(self):
mail = MailBuilder().with_subject('Mail with tags').build_input_mail()
- self.client.add_mail_to_inbox(mail)
+ self.add_mail_to_inbox(mail)
self.post_tags(mail.ident, self._tags_json(['ImPoRtAnT']))
@@ -70,7 +70,7 @@ class TagsTest(SoledadTestBase):
def test_empty_tags_are_not_allowed(self):
mail = MailBuilder().with_subject('Mail with tags').build_input_mail()
- self.client.add_mail_to_inbox(mail)
+ self.add_mail_to_inbox(mail)
self.post_tags(mail.ident, self._tags_json(['tag1', ' ']))
@@ -80,11 +80,11 @@ class TagsTest(SoledadTestBase):
def test_addition_of_reserved_tags_is_not_allowed(self):
mail = MailBuilder().with_subject('Mail with tags').build_input_mail()
- self.client.add_mail_to_inbox(mail)
+ self.add_mail_to_inbox(mail)
- for tag in TagService.SPECIAL_TAGS:
+ for tag in SPECIAL_TAGS:
response = self.post_tags(mail.ident, self._tags_json([tag.name.upper()]))
self.assertEquals("None of the following words can be used as tags: %s" % tag.name, response)
- mail = self.client.mailboxes.inbox().mail(mail.ident)
+ mail = self.mailboxes.inbox().mail(mail.ident)
self.assertNotIn('drafts', mail.tags)
diff --git a/service/test/perf/search/test_Search.py b/service/test/perf/search/test_Search.py
index 63636789..5e646edd 100644
--- a/service/test/perf/search/test_Search.py
+++ b/service/test/perf/search/test_Search.py
@@ -14,24 +14,25 @@
# 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 funkload.FunkLoadTestCase import FunkLoadTestCase
from funkload.utils import Data
from test.support.integration import AppTestClient
+CLIENT = AppTestClient()
+
+
class Search(FunkLoadTestCase):
def setUpBench(self):
- client = AppTestClient()
# setup data
- client.add_multiple_to_mailbox(10, 'INBOX', to='to@inbox.com', cc='cc@inbox.com', bcc='bcc@inbox.com', tags=['inbox'])
- client.add_multiple_to_mailbox(10, 'TRASH', to='to@trash.com', cc='cc@trash.com', bcc='bcc@trash.com', tags=['trash'])
- client.add_multiple_to_mailbox(10, 'DRAFTS', to='to@drafts.com', cc='cc@drafts.com', bcc='bcc@drafts.com', tags=['drafts'])
+ CLIENT.add_multiple_to_mailbox(10, 'INBOX', to='to@inbox.com', cc='cc@inbox.com', bcc='bcc@inbox.com', tags=['inbox'])
+ CLIENT.add_multiple_to_mailbox(10, 'TRASH', to='to@trash.com', cc='cc@trash.com', bcc='bcc@trash.com', tags=['trash'])
+ CLIENT.add_multiple_to_mailbox(10, 'DRAFTS', to='to@drafts.com', cc='cc@drafts.com', bcc='bcc@drafts.com', tags=['drafts'])
- self.call_to_terminate = client.run_on_a_thread(logfile='results/app.log')
+ self.call_to_terminate = CLIENT.run_on_a_thread(logfile='results/app.log')
def tearDownBench(self):
self.call_to_terminate()
@@ -42,7 +43,7 @@ class Search(FunkLoadTestCase):
self.mails_by_tag_url = self.server_url + '/mails?q=%%22tag:%s%%22&w=25&p=0'
def idents_by_tag(self, tag):
- return list(mail['ident'] for mail in json.loads(self.get(self.mails_by_tag_url % tag, description='Query mails by tag').body)['mails'])
+ return [mail.ident for mail in CLIENT.get_mails_by_tag(tag)]
def test_search(self):
""" Query contacts and tags. Write a new tag, updating index. Query again. """
diff --git a/service/test/support/integration/app_test_client.py b/service/test/support/integration/app_test_client.py
index 474e5fd3..5e52732b 100644
--- a/service/test/support/integration/app_test_client.py
+++ b/service/test/support/integration/app_test_client.py
@@ -35,20 +35,23 @@ from pixelated.adapter.services.draft_service import DraftService
from pixelated.adapter.services.mail_service import MailService
from pixelated.adapter.services.mailboxes import Mailboxes
from pixelated.adapter.soledad.soledad_querier import SoledadQuerier
-from pixelated.adapter.services.tag_service import TagService
from pixelated.config import App
from pixelated.resources.root_resource import RootResource
from test.support.integration.model import MailBuilder
from test.support.test_helper import request_mock
+from test.support.integration.model import ResponseMail
-class AppTestClient:
+class AppTestClient(object):
INDEX_KEY = '\xde3?\x87\xff\xd9\xd3\x14\xf0\xa7>\x1f%C{\x16.\\\xae\x8c\x13\xa7\xfb\x04\xd4]+\x8d_\xed\xd1\x8d\x0bI' \
'\x8a\x0e\xa4tm\xab\xbf\xb4\xa5\x99\x00d\xd5w\x9f\x18\xbc\x1d\xd4_W\xd2\xb6\xe8H\x83\x1b\xd8\x9d\xad'
ACCOUNT = 'test'
MAIL_ADDRESS = 'test@pixelated.org'
def __init__(self):
+ self.start_client()
+
+ def start_client(self):
soledad_test_folder = self._generate_soledad_test_folder_name()
SearchEngine.DEFAULT_INDEX_HOME = soledad_test_folder
@@ -125,8 +128,9 @@ class AppTestClient:
def add_mail_to_inbox(self, input_mail):
mail = self.mailboxes.inbox().add(input_mail)
- mail.update_tags(input_mail.tags)
- self.search_engine.index_mail(mail)
+ if input_mail.tags:
+ mail.update_tags(input_mail.tags)
+ self.search_engine.index_mail(mail)
def add_multiple_to_mailbox(self, num, mailbox='', flags=[], tags=[], to='recipient@to.com', cc='recipient@cc.com', bcc='recipient@bcc.com'):
mails = []
@@ -134,8 +138,8 @@ class AppTestClient:
input_mail = MailBuilder().with_status(flags).with_tags(tags).with_to(to).with_cc(cc).with_bcc(bcc).build_input_mail()
mail = self.mailboxes._create_or_get(mailbox).add(input_mail)
mails.append(mail)
- mail.update_tags(input_mail.tags)
- self.search_engine.index_mail(mail)
+ mail.update_tags(input_mail.tags) if tags else None
+ self.search_engine.index_mails(mails) if tags else None
return mails
def _create_soledad_querier(self, soledad, index_key):
@@ -149,13 +153,64 @@ class AppTestClient:
return mail_sender
def _create_mail_service(self, mailboxes, mail_sender, soledad_querier, search_engine):
- tag_service = TagService()
- mail_service = MailService(mailboxes, mail_sender, tag_service, soledad_querier, search_engine)
+ mail_service = MailService(mailboxes, mail_sender, soledad_querier, search_engine)
return mail_service
def _generate_soledad_test_folder_name(self, soledad_test_folder='/tmp/soledad-test/test'):
return os.path.join(soledad_test_folder, str(uuid.uuid4()))
+ def get_mails_by_tag(self, tag, page=1, window=100):
+ tags = 'tag:%s' % tag
+ return self.search(tags, page, window)
+
+ def search(self, query, page=1, window=100):
+ res, req = self.get("/mails", {
+ 'q': [query],
+ 'w': [str(window)],
+ 'p': [str(page)]
+ })
+ return [ResponseMail(m) for m in res['mails']]
+
+ def get_attachment(self, ident, encoding):
+ res, req = self.get("/attachment/%s" % ident, {'encoding': [encoding]}, as_json=False)
+ return res
+
+ def put_mail(self, data):
+ res, req = self.put('/mails', data)
+ return res, req
+
+ def post_tags(self, mail_ident, tags_json):
+ res, req = self.post("/mail/%s/tags" % mail_ident, tags_json)
+ return res
+
+ def get_tags(self, **kwargs):
+ res, req = self.get('/tags', kwargs)
+ return res
+
+ def get_mail(self, mail_ident):
+ res, req = self.get('/mail/%s' % mail_ident)
+ return res
+
+ def delete_mail(self, mail_ident):
+ res, req = self.delete("/mail/%s" % mail_ident)
+ return req
+
+ def delete_mails(self, idents):
+ res, req = self.post("/mails/delete", json.dumps({'idents': idents}))
+ return req
+
+ def mark_many_as_unread(self, idents):
+ res, req = self.post('/mails/unread', json.dumps({'idents': idents}))
+ return req
+
+ def mark_many_as_read(self, idents):
+ res, req = self.post('/mails/read', json.dumps({'idents': idents}))
+ return req
+
+ def get_contacts(self, query):
+ res, req = self.get('/contacts', get_args={'q': query})
+ return res
+
def initialize_soledad(tempdir):
if os.path.isdir(tempdir):
diff --git a/service/test/support/integration/soledad_test_base.py b/service/test/support/integration/soledad_test_base.py
index 2c8bb023..c49de00a 100644
--- a/service/test/support/integration/soledad_test_base.py
+++ b/service/test/support/integration/soledad_test_base.py
@@ -14,70 +14,16 @@
# 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 twisted.trial import unittest
-from pixelated.resources import *
from test.support.integration.app_test_client import AppTestClient
-from test.support.integration.model import ResponseMail
-class SoledadTestBase(unittest.TestCase):
+class SoledadTestBase(unittest.TestCase, AppTestClient):
# these are so long because our CI is so slow at the moment.
DEFERRED_TIMEOUT = 120
DEFERRED_TIMEOUT_LONG = 300
def setUp(self):
- self.client = AppTestClient()
+ self.start_client()
def tearDown(self):
- self.client.cleanup()
-
- def get_mails_by_tag(self, tag, page=1, window=100):
- tags = 'tag:%s' % tag
- return self.search(tags, page, window)
-
- def search(self, query, page=1, window=100):
- res, req = self.client.get("/mails", {
- 'q': [query],
- 'w': [str(window)],
- 'p': [str(page)]
- })
- return [ResponseMail(m) for m in res['mails']]
-
- def get_attachment(self, ident, encoding):
- res, req = self.client.get("/attachment/%s" % ident, {'encoding': [encoding]}, as_json=False)
- return res
-
- def put_mail(self, data):
- res, req = self.client.put('/mails', data)
- return res, req
-
- def post_tags(self, mail_ident, tags_json):
- res, req = self.client.post("/mail/%s/tags" % mail_ident, tags_json)
- return res
-
- def get_tags(self, **kwargs):
- res, req = self.client.get('/tags', kwargs)
- return res
-
- def get_mail(self, mail_ident):
- res, req = self.client.get('/mail/%s' % mail_ident)
- return res
-
- def delete_mail(self, mail_ident):
- res, req = self.client.delete("/mail/%s" % mail_ident)
- return req
-
- def delete_mails(self, idents):
- res, req = self.client.post("/mails/delete", json.dumps({'idents': idents}))
- return req
-
- def mark_many_as_unread(self, idents):
- res, req = self.client.post('/mails/unread', json.dumps({'idents': idents}))
- return req
-
- 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
+ self.cleanup()
diff --git a/service/test/support/test_helper.py b/service/test/support/test_helper.py
index 54685008..c37c1408 100644
--- a/service/test/support/test_helper.py
+++ b/service/test/support/test_helper.py
@@ -15,8 +15,9 @@
# along with Pixelated. If not, see <http://www.gnu.org/licenses/>.
from datetime import datetime
import io
+from twisted.web.test.test_web import DummyRequest
-from pixelated.adapter.model.mail import InputMail
+from pixelated.adapter.model.mail import InputMail, PixelatedMail
LEAP_FLAGS = ['\\Seen',
@@ -68,6 +69,12 @@ def leap_mail(uid=0, flags=LEAP_FLAGS, headers=None, extra_headers={}, mbox='INB
return (fdoc, hdoc, bdoc)
+def pixelated_mail(uid=0, flags=LEAP_FLAGS, headers=None, extra_headers={}, mbox='INBOX', body='body', chash='chash'):
+ fdoc, hdoc, bdoc = leap_mail(uid, flags, headers, extra_headers, mbox, body, chash)
+
+ return PixelatedMail.from_soledad(fdoc, hdoc, bdoc)
+
+
def input_mail():
mail = InputMail()
mail.fdoc = TestDoc({})
@@ -82,9 +89,6 @@ class TestRequest:
self.json = json
-from twisted.web.test.test_web import DummyRequest
-
-
class PixRequestMock(DummyRequest):
def __init__(self, path):
DummyRequest.__init__(self, path)
diff --git a/service/test/unit/adapter/search/__init__.py b/service/test/unit/adapter/search/__init__.py
new file mode 100644
index 00000000..2756a319
--- /dev/null
+++ b/service/test/unit/adapter/search/__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/adapter/search/test_search.py b/service/test/unit/adapter/search/test_search.py
new file mode 100644
index 00000000..d57b8227
--- /dev/null
+++ b/service/test/unit/adapter/search/test_search.py
@@ -0,0 +1,89 @@
+#
+# 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
+from mockito import mock, when
+from pixelated.adapter.search import SearchEngine
+from tempdir import TempDir
+from test.support import test_helper
+
+INDEX_KEY = '\xde3?\x87\xff\xd9\xd3\x14\xf0\xa7>\x1f%C{\x16.\\\xae\x8c\x13\xa7\xfb\x04\xd4]+\x8d_\xed\xd1\x8d\x0bI' \
+ '\x8a\x0e\xa4tm\xab\xbf\xb4\xa5\x99\x00d\xd5w\x9f\x18\xbc\x1d\xd4_W\xd2\xb6\xe8H\x83\x1b\xd8\x9d\xad'
+
+
+class LockStub(object):
+ def __init__(self):
+ self.called = False
+
+ def __enter__(self):
+ self.called = True
+ return self
+
+ def __exit__(self, type, value, traceback):
+ return False
+
+
+class SearchEngineTest(unittest.TestCase):
+ def setUp(self):
+ self.tempdir = TempDir()
+ self.agent_home = self.tempdir.name
+
+ def tearDown(self):
+ self.tempdir.dissolve()
+
+ def test_index_mail_secured_by_lock(self):
+ # given
+ soledad_querier = mock()
+ lock_stub = LockStub()
+ when(soledad_querier).get_index_masterkey().thenReturn(INDEX_KEY)
+
+ self.assertEqual(INDEX_KEY, soledad_querier.get_index_masterkey())
+ se = SearchEngine(soledad_querier, self.agent_home)
+ se._write_lock = lock_stub
+
+ headers = {
+ 'From': 'from@bar.tld',
+ 'To': 'to@bar.tld',
+ 'Subject': 'Some test mail',
+ }
+
+ # when
+ se.index_mail(test_helper.pixelated_mail(extra_headers=headers))
+
+ # then
+ self.assertTrue(lock_stub.called)
+
+ def test_encoding(self):
+ # given
+ soledad_querier = mock()
+ when(soledad_querier).get_index_masterkey().thenReturn(INDEX_KEY)
+
+ se = SearchEngine(soledad_querier, self.agent_home)
+
+ headers = {
+ 'From': 'foo@bar.tld',
+ 'To': '=?utf-8?b?IsOEw7zDtiDDlsO8w6QiIDxmb2xrZXJAcGl4ZWxhdGVkLXByb2plY3Qub3Jn?=\n =?utf-8?b?PiwgRsO2bGtlciA8Zm9sa2VyQHBpeGVsYXRlZC1wcm9qZWN0Lm9yZz4=?=',
+ 'Cc': '=?utf-8?b?IsOEw7zDtiDDlsO8w6QiIDxmb2xrZXJAcGl4ZWxhdGVkLXByb2plY3Qub3Jn?=\n =?utf-8?b?PiwgRsO2bGtlciA8Zm9sa2VyQHBpeGVsYXRlZC1wcm9qZWN0Lm9yZz4=?=',
+ 'Subject': 'Some test mail',
+ }
+
+ # when
+ se.index_mail(test_helper.pixelated_mail(extra_headers=headers, chash='mailid'))
+
+ result = se.search('folker')
+
+ self.assertEqual((['mailid'], 1), result)
diff --git a/service/test/unit/adapter/test_draft_service.py b/service/test/unit/adapter/test_draft_service.py
index baa07ce0..0dd6cd2a 100644
--- a/service/test/unit/adapter/test_draft_service.py
+++ b/service/test/unit/adapter/test_draft_service.py
@@ -3,7 +3,7 @@ import unittest
from pixelated.adapter.model.mail import InputMail
from pixelated.adapter.services.draft_service import DraftService
import test.support.test_helper as test_helper
-from mockito import *
+from mockito import mock, verify, inorder, when
class DraftServiceTest(unittest.TestCase):
diff --git a/service/test/unit/adapter/test_email_recepient_normalizer.py b/service/test/unit/adapter/test_email_recepient_normalizer.py
deleted file mode 100644
index 79d50273..00000000
--- a/service/test/unit/adapter/test_email_recepient_normalizer.py
+++ /dev/null
@@ -1,42 +0,0 @@
-#
-# Copyright (c) 2014 ThoughtWorks, Inc.
-#
-# Pixelated is free software: you can redistribute it and/or modify
-# it under the terms of the GNU Affero General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# Pixelated is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU Affero General Public License for more details.
-#
-# You should have received a copy of the GNU Affero General Public License
-# along with Pixelated. If not, see <http://www.gnu.org/licenses/>.
-import unittest
-
-from pixelated.adapter.model.mail import PixelatedMail
-from pixelated.adapter.services.mailbox import Mailbox
-from pixelated.adapter.services.mail_sender import MailSender
-from mockito import *
-from test.support import test_helper
-
-
-class PixelatedDuplicateEmailTest(unittest.TestCase):
- def setUp(self):
- self.mail_sender = MailSender(self, "random@gmail.com")
-
- def test_remove_duplicate_mail_recepients(self):
- mail_list = ['simba@gmail.com', 'simba@gmail.com', 'fabio@gmail.com']
- normalized_recepients = self.mail_sender.recepients_normalizer(mail_list)
- self.assertEquals(normalized_recepients, set(['simba@gmail.com', 'fabio@gmail.com']))
-
- def test_get_email_addresses(self):
- mail_list = ['simbarashe<simba@gmail.com>', 'vic@gmail.com', 'Fabio<fabio@gmail.com>', 'slick@gmail.com']
- selected_recepients = self.mail_sender.get_email_addresses(mail_list)
- self.assertEquals(selected_recepients, set(['simba@gmail.com', 'vic@gmail.com', 'fabio@gmail.com', 'slick@gmail.com']))
-
- def test_remove_duplicate_emails_with_routing_format(self):
- mail_list = ['simbarashe<simba@gmail.com>', 'simba<simba@gmail.com>', 'Fabio<fabio@gmail.com>', 'Fabinho<fabio@gmail.com>']
- selected_recepients = self.mail_sender.get_email_addresses(mail_list)
- self.assertEquals(selected_recepients, set(['simba@gmail.com', 'fabio@gmail.com']))
diff --git a/service/test/unit/adapter/test_mail.py b/service/test/unit/adapter/test_mail.py
index 54c421c7..c7910b7f 100644
--- a/service/test/unit/adapter/test_mail.py
+++ b/service/test/unit/adapter/test_mail.py
@@ -17,7 +17,7 @@ import unittest
import pixelated.support.date
from pixelated.adapter.model.mail import PixelatedMail, InputMail
-from mockito import *
+from mockito import mock, unstub, when
from test.support import test_helper
import dateutil.parser as dateparser
import base64
@@ -31,6 +31,9 @@ class TestPixelatedMail(unittest.TestCase):
def setUp(self):
self.querier = mock()
+ def tearDown(self):
+ unstub()
+
def test_parse_date_from_soledad_uses_date_header_if_available(self):
leap_mail_date = 'Wed, 3 Sep 2014 12:36:17 -0300'
leap_mail_date_in_iso_format = "2014-09-03T12:36:17-03:00"
@@ -52,6 +55,17 @@ class TestPixelatedMail(unittest.TestCase):
self.assertEqual(str(mail.headers['Date']), leap_mail_date_in_iso_format)
+ def test_parse_date_from_soledad_fallback_to_now_if_neither_date_nor_received_header(self):
+ leap_mail_date_in_iso_format = "2014-09-03T13:11:15-03:00"
+
+ when(pixelated.support.date).iso_now().thenReturn(leap_mail_date_in_iso_format)
+ fdoc, hdoc, bdoc = test_helper.leap_mail()
+ del hdoc.content['date']
+
+ mail = PixelatedMail.from_soledad(fdoc, hdoc, bdoc, soledad_querier=self.querier)
+
+ self.assertEqual(str(mail.headers['Date']), leap_mail_date_in_iso_format)
+
def test_update_tags_return_a_set_with_the_current_tags(self):
soledad_docs = test_helper.leap_mail(extra_headers={'X-tags': '["custom_1", "custom_2"]'})
pixelated_mail = PixelatedMail.from_soledad(*soledad_docs, soledad_querier=self.querier)
@@ -174,9 +188,20 @@ class TestPixelatedMail(unittest.TestCase):
self.assertRegexpMatches(mail.html_body, '([\s\S]*100%)')
def test_content_type_header_of_mail_part_is_used(self):
- plain_headers = {'Content-Type': 'text/plain; charset=utf-8', 'Content-Transfer-Encoding': 'quoted-printable'}
+ plain_headers = {'Content-Type': 'text/plain; charset=iso-8859-1', 'Content-Transfer-Encoding': 'quoted-printable'}
html_headers = {'Content-Type': 'text/html; charset=utf-8', 'Content-Transfer-Encoding': 'quoted-printable'}
- parts = {'alternatives': [{'content': 'H=C3=A4llo', 'headers': plain_headers}, {'content': '<p>H=C3=A4llo</p>', 'headers': html_headers}]}
+ parts = {'alternatives': [{'content': 'H=E4llo', 'headers': plain_headers}, {'content': '<p>H=C3=A4llo</p>', 'headers': html_headers}]}
+
+ mail = PixelatedMail.from_soledad(None, None, self._create_bdoc(raw='some raw body'), parts=parts, soledad_querier=None)
+
+ self.assertEqual(2, len(mail.alternatives))
+ self.assertEquals(u'H\xe4llo', mail.text_plain_body)
+ self.assertEquals(u'<p>H\xe4llo</p>', mail.html_body)
+
+ def test_multi_line_content_type_header_is_supported(self):
+ plain_headers = {'Content-Type': 'text/plain;\ncharset=iso-8859-1', 'Content-Transfer-Encoding': 'quoted-printable'}
+ html_headers = {'Content-Type': 'text/html;\ncharset=utf-8', 'Content-Transfer-Encoding': 'quoted-printable'}
+ parts = {'alternatives': [{'content': 'H=E4llo', 'headers': plain_headers}, {'content': '<p>H=C3=A4llo</p>', 'headers': html_headers}]}
mail = PixelatedMail.from_soledad(None, None, self._create_bdoc(raw='some raw body'), parts=parts, soledad_querier=None)
@@ -211,6 +236,28 @@ class TestPixelatedMail(unittest.TestCase):
self.assertEquals(body, mail.text_plain_body)
+ def test_that_body_understands_7bit(self):
+ body = u'testtext'
+ encoded_body = body
+
+ fdoc, hdoc, bdoc = test_helper.leap_mail()
+ parts = {'alternatives': []}
+ parts['alternatives'].append({'content': encoded_body, 'headers': {'Content-Transfer-Encoding': '7bit'}})
+ mail = PixelatedMail.from_soledad(fdoc, hdoc, bdoc, soledad_querier=self.querier, parts=parts)
+
+ self.assertEquals(body, mail.text_plain_body)
+
+ def test_that_body_understands_8bit(self):
+ body = u'testtext'
+ encoded_body = body
+
+ fdoc, hdoc, bdoc = test_helper.leap_mail()
+ parts = {'alternatives': []}
+ parts['alternatives'].append({'content': encoded_body, 'headers': {'Content-Transfer-Encoding': '8bit'}})
+ mail = PixelatedMail.from_soledad(fdoc, hdoc, bdoc, soledad_querier=self.querier, parts=parts)
+
+ self.assertEquals(body, mail.text_plain_body)
+
def test_bounced_mails_are_recognized(self):
bounced_mail_hdoc = os.path.join(os.path.dirname(__file__), '..', 'fixtures', 'bounced_mail_hdoc.json')
with open(bounced_mail_hdoc) as f:
@@ -256,9 +303,40 @@ class TestPixelatedMail(unittest.TestCase):
self.content = {'raw': raw}
return FakeBDoc(raw)
+ def test_encoding_special_character_on_header(self):
+ subject = "=?UTF-8?Q?test_encoding_St=C3=A4ch?="
+ email_from = "=?UTF-8?Q?St=C3=A4ch_<stach@pixelated-project.org>?="
+ email_to = "=?utf-8?b?IsOEw7zDtiDDlsO8w6QiIDxmb2xrZXJAcGl4ZWxhdGVkLXByb2plY3Qub3Jn?=\n =?utf-8?b?PiwgRsO2bGtlciA8Zm9sa2VyQHBpeGVsYXRlZC1wcm9qZWN0Lm9yZz4=?="
-class InputMailTest(unittest.TestCase):
- mail_dict = lambda x: {
+ pixel_mail = PixelatedMail()
+
+ self.assertEqual(pixel_mail._decode_header(subject), 'test encoding St\xc3\xa4ch')
+ self.assertEqual(pixel_mail._decode_header(email_from), 'St\xc3\xa4ch <stach@pixelated-project.org>')
+ self.assertEqual(pixel_mail._decode_header(email_to), '"\xc3\x84\xc3\xbc\xc3\xb6 \xc3\x96\xc3\xbc\xc3\xa4" <folker@pixelated-project.org>, F\xc3\xb6lker <folker@pixelated-project.org>')
+ self.assertEqual(pixel_mail._decode_header(None), None)
+
+ def test_headers_are_encoded_right(self):
+ subject = "=?UTF-8?Q?test_encoding_St=C3=A4ch?="
+ email_from = "=?UTF-8?Q?St=C3=A4ch_<stach@pixelated-project.org>?="
+ email_to = "=?utf-8?b?IsOEw7zDtiDDlsO8w6QiIDxmb2xrZXJAcGl4ZWxhdGVkLXByb2plY3Qub3Jn?=\n =?utf-8?b?PiwgRsO2bGtlciA8Zm9sa2VyQHBpeGVsYXRlZC1wcm9qZWN0Lm9yZz4=?="
+ email_cc = "=?UTF-8?Q?St=C3=A4ch_<stach@pixelated-project.org>?="
+ email_bcc = "=?UTF-8?Q?St=C3=A4ch_<stach@pixelated-project.org>?="
+
+ leap_mail = test_helper.leap_mail(extra_headers={'Subject': subject, 'From': email_from, 'To': email_to, 'Cc': email_cc, 'Bcc': email_bcc})
+
+ mail = PixelatedMail.from_soledad(*leap_mail, soledad_querier=self.querier)
+
+ self.assertEqual(str(mail.headers['Subject']), 'test encoding St\xc3\xa4ch')
+ self.assertEqual(str(mail.headers['From']), 'St\xc3\xa4ch <stach@pixelated-project.org>')
+ self.assertEqual(mail.headers['To'], ['"\xc3\x84\xc3\xbc\xc3\xb6 \xc3\x96\xc3\xbc\xc3\xa4" <folker@pixelated-project.org>', 'F\xc3\xb6lker <folker@pixelated-project.org>'])
+ self.assertEqual(mail.headers['Cc'], ['St\xc3\xa4ch <stach@pixelated-project.org>'])
+ self.assertEqual(mail.headers['Bcc'], ['St\xc3\xa4ch <stach@pixelated-project.org>'])
+
+ mail.as_dict()
+
+
+def simple_mail_dict():
+ return {
'body': 'Este \xe9 o corpo',
'header': {
'cc': ['cc@pixelated.org', 'anothercc@pixelated.org'],
@@ -270,7 +348,9 @@ class InputMailTest(unittest.TestCase):
'tags': ['sent']
}
- multipart_mail_dict = lambda x: {
+
+def multipart_mail_dict():
+ return {
'body': [{'content-type': 'plain', 'raw': 'Hello world!'},
{'content-type': 'html', 'raw': '<p>Hello html world!</p>'}],
'header': {
@@ -283,10 +363,13 @@ class InputMailTest(unittest.TestCase):
'tags': ['sent']
}
+
+class InputMailTest(unittest.TestCase):
+
def test_to_mime_multipart_should_add_blank_fields(self):
pixelated.support.date.iso_now = lambda: 'date now'
- mail_dict = self.mail_dict()
+ mail_dict = simple_mail_dict()
mail_dict['header']['to'] = ''
mail_dict['header']['bcc'] = ''
mail_dict['header']['cc'] = ''
@@ -302,24 +385,24 @@ class InputMailTest(unittest.TestCase):
def test_to_mime_multipart(self):
pixelated.support.date.iso_now = lambda: 'date now'
- mime_multipart = InputMail.from_dict(self.mail_dict()).to_mime_multipart()
+ mime_multipart = InputMail.from_dict(simple_mail_dict()).to_mime_multipart()
self.assertRegexpMatches(mime_multipart.as_string(), "\nTo: to@pixelated.org, anotherto@pixelated.org\n")
self.assertRegexpMatches(mime_multipart.as_string(), "\nCc: cc@pixelated.org, anothercc@pixelated.org\n")
self.assertRegexpMatches(mime_multipart.as_string(), "\nBcc: bcc@pixelated.org, anotherbcc@pixelated.org\n")
self.assertRegexpMatches(mime_multipart.as_string(), "\nDate: date now\n")
self.assertRegexpMatches(mime_multipart.as_string(), "\nSubject: Oi\n")
- self.assertRegexpMatches(mime_multipart.as_string(), base64.b64encode(self.mail_dict()['body']))
+ self.assertRegexpMatches(mime_multipart.as_string(), base64.b64encode(simple_mail_dict()['body']))
def test_smtp_format(self):
InputMail.FROM_EMAIL_ADDRESS = 'pixelated@org'
- smtp_format = InputMail.from_dict(self.mail_dict()).to_smtp_format()
+ smtp_format = InputMail.from_dict(simple_mail_dict()).to_smtp_format()
self.assertRegexpMatches(smtp_format, "\nFrom: pixelated@org")
def test_to_mime_multipart_handles_alternative_bodies(self):
- mime_multipart = InputMail.from_dict(self.multipart_mail_dict()).to_mime_multipart()
+ mime_multipart = InputMail.from_dict(multipart_mail_dict()).to_mime_multipart()
part_one = 'Content-Type: text/plain; charset="us-ascii"\nMIME-Version: 1.0\nContent-Transfer-Encoding: 7bit\n\nHello world!'
part_two = 'Content-Type: text/html; charset="us-ascii"\nMIME-Version: 1.0\nContent-Transfer-Encoding: 7bit\n\n<p>Hello html world!</p>'
diff --git a/service/test/unit/adapter/test_mail_service.py b/service/test/unit/adapter/test_mail_service.py
index 98ead126..34fec708 100644
--- a/service/test/unit/adapter/test_mail_service.py
+++ b/service/test/unit/adapter/test_mail_service.py
@@ -18,25 +18,22 @@ from pixelated.adapter.model.mail import InputMail, PixelatedMail
from pixelated.adapter.services.mail_service import MailService
from test.support.test_helper import mail_dict, leap_mail
-from mockito import *
+from mockito import mock, unstub, when, verify, verifyNoMoreInteractions, any
from twisted.internet.defer import Deferred
-from twisted.internet import defer
-
class TestMailService(unittest.TestCase):
def setUp(self):
self.drafts = mock()
self.querier = mock()
self.mailboxes = mock()
- self.tag_service = mock()
self.mailboxes.drafts = lambda: self.drafts
self.mailboxes.trash = lambda: mock()
self.mailboxes.sent = lambda: mock()
self.mail_sender = mock()
self.search_engine = mock()
- self.mail_service = MailService(self.mailboxes, self.mail_sender, self.tag_service, self.querier, self.search_engine)
+ self.mail_service = MailService(self.mailboxes, self.mail_sender, self.querier, self.search_engine)
def tearDown(self):
unstub()
diff --git a/service/test/unit/adapter/test_mailbox.py b/service/test/unit/adapter/test_mailbox.py
index b44f507b..ed634648 100644
--- a/service/test/unit/adapter/test_mailbox.py
+++ b/service/test/unit/adapter/test_mailbox.py
@@ -17,13 +17,12 @@ import unittest
from pixelated.adapter.model.mail import PixelatedMail
from pixelated.adapter.services.mailbox import Mailbox
-from mockito import *
+from mockito import mock, when, verify
from test.support import test_helper
class PixelatedMailboxTest(unittest.TestCase):
def setUp(self):
- self.tag_service = mock()
self.querier = mock()
self.search_engine = mock()
self.mailbox = Mailbox('INBOX', self.querier, self.search_engine)
@@ -35,3 +34,9 @@ class PixelatedMailboxTest(unittest.TestCase):
self.mailbox.remove(1)
verify(self.querier).remove_mail(mail)
+
+ def test_fresh_mailbox_checking_lastuid(self):
+ when(self.querier).get_lastuid('INBOX').thenReturn(0)
+ self.assertTrue(self.mailbox.fresh)
+ when(self.querier).get_lastuid('INBOX').thenReturn(1)
+ self.assertFalse(self.mailbox.fresh)
diff --git a/service/test/unit/adapter/test_mailbox_indexer_listener.py b/service/test/unit/adapter/test_mailbox_indexer_listener.py
index 65ba8966..71c9cd15 100644
--- a/service/test/unit/adapter/test_mailbox_indexer_listener.py
+++ b/service/test/unit/adapter/test_mailbox_indexer_listener.py
@@ -15,7 +15,7 @@
# along with Pixelated. If not, see <http://www.gnu.org/licenses/>.
import unittest
-from mockito import *
+from mockito import mock, when, verify
from pixelated.adapter.listeners.mailbox_indexer_listener import MailboxIndexerListener
diff --git a/service/test/unit/adapter/test_soledad_querier.py b/service/test/unit/adapter/test_soledad_querier.py
index 2cc23750..e5ea457d 100644
--- a/service/test/unit/adapter/test_soledad_querier.py
+++ b/service/test/unit/adapter/test_soledad_querier.py
@@ -104,3 +104,47 @@ class SoledadQuerierTest(unittest.TestCase):
attachment = querier.attachment(u'0400BEBACAFE', 'quoted-printable')
self.assertEquals('esse papo seu ta qualquer coisa', attachment['content'])
+
+ def test_empty_or_null_queries_are_ignored(self):
+ soledad = mock()
+ when(soledad).get_from_index(any(), any(), any()).thenReturn(['nonempty', 'list'])
+ querier = SoledadQuerier(soledad)
+
+ test_parameters = ['', None]
+
+ def call_with_bad_parameters(funct):
+ for param in test_parameters:
+ self.assertFalse(funct(param))
+
+ call_with_bad_parameters(querier.get_all_flags_by_mbox)
+ call_with_bad_parameters(querier.get_content_by_phash)
+ call_with_bad_parameters(querier.get_flags_by_chash)
+ call_with_bad_parameters(querier.get_header_by_chash)
+ call_with_bad_parameters(querier.get_recent_by_mbox)
+ call_with_bad_parameters(querier.idents_by_mailbox)
+ call_with_bad_parameters(querier.get_mbox)
+
+ def test_get_lastuid(self):
+ soledad = mock()
+ mbox = mock()
+ mbox.content = {'lastuid': 0}
+ when(soledad).get_from_index('by-type-and-mbox', 'mbox', 'INBOX').thenReturn([mbox])
+ querier = SoledadQuerier(soledad)
+
+ self.assertEquals(querier.get_lastuid(querier.get_mbox('INBOX')[0]), 0)
+ mbox.content = {'lastuid': 1}
+ self.assertEquals(querier.get_lastuid(querier.get_mbox('INBOX')[0]), 1)
+
+ def test_create_mail_increments_uid(self):
+ soledad = mock()
+ mbox = mock()
+ mail = mock()
+ when(mail).get_for_save(next_uid=any(), mailbox='INBOX').thenReturn([])
+ mbox.content = {'lastuid': 0}
+ when(soledad).get_from_index('by-type-and-mbox', 'mbox', 'INBOX').thenReturn([mbox])
+ querier = SoledadQuerier(soledad)
+ when(querier).mail(any()).thenReturn([])
+
+ self.assertEquals(querier.get_lastuid(querier.get_mbox('INBOX')[0]), 0)
+ querier.create_mail(mail, 'INBOX')
+ self.assertEquals(querier.get_lastuid(querier.get_mbox('INBOX')[0]), 1)
diff --git a/service/test/unit/resources/test_sync_info_controller.py b/service/test/unit/resources/test_sync_info_controller.py
index a91dd386..1285237b 100644
--- a/service/test/unit/resources/test_sync_info_controller.py
+++ b/service/test/unit/resources/test_sync_info_controller.py
@@ -18,7 +18,7 @@ import json
from test.support.test_helper import request_mock
from pixelated.resources.sync_info_resource import SyncInfoResource
-from mockito import *
+from mockito import mock
class SyncInfoResourceTest(unittest.TestCase):
diff --git a/service/test/unit/support/test_ext_keymanager_fetch_key.py b/service/test/unit/support/test_ext_keymanager_fetch_key.py
new file mode 100644
index 00000000..8998198d
--- /dev/null
+++ b/service/test/unit/support/test_ext_keymanager_fetch_key.py
@@ -0,0 +1,76 @@
+#
+# 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
+from mock import MagicMock, patch
+
+from leap.keymanager import KeyManager
+from leap.keymanager.keys import KEY_ADDRESS_KEY, KEY_TYPE_KEY, KEY_ID_KEY, KEY_FINGERPRINT_KEY, KEY_DATA_KEY, KEY_PRIVATE_KEY, KEY_LENGTH_KEY, KEY_EXPIRY_DATE_KEY, KEY_FIRST_SEEN_AT_KEY, KEY_LAST_AUDITED_AT_KEY, KEY_VALIDATION_KEY, KEY_TAGS_KEY
+from leap.keymanager.openpgp import OpenPGPKey
+from leap.keymanager.errors import KeyNotFound
+import pixelated.support.ext_keymanager_fetch_key
+from requests.exceptions import HTTPError
+
+
+class TestDoc(object):
+ def __init__(self, encryption_key):
+ self.content = encryption_key
+
+sample_key = {
+ KEY_ADDRESS_KEY: 'foo@bar.de',
+ KEY_TYPE_KEY: 'type',
+ KEY_ID_KEY: 'key_id',
+ KEY_FINGERPRINT_KEY: 'fingerprint',
+ KEY_DATA_KEY: 'key_data',
+ KEY_PRIVATE_KEY: None,
+ KEY_LENGTH_KEY: 'length',
+ KEY_EXPIRY_DATE_KEY: 'expiry_date',
+ KEY_FIRST_SEEN_AT_KEY: 'first_seen_at',
+ KEY_LAST_AUDITED_AT_KEY: 'last_audited_at',
+ KEY_VALIDATION_KEY: 'validation',
+ KEY_TAGS_KEY: 'tags',
+}
+
+
+class TestExtKeyManagerFetchKey(unittest.TestCase):
+
+ @patch('leap.keymanager.requests')
+ def test_retrieves_key(self, requests_mock):
+ nickserver_url = 'http://some/nickserver/uri'
+ soledad = MagicMock()
+ soledad.get_from_index.side_effect = [[], [TestDoc(sample_key)]]
+
+ km = KeyManager('me@bar.de', nickserver_url, soledad, ca_cert_path='some path')
+
+ result = km.get_key('foo@bar.de', OpenPGPKey)
+
+ self.assertEqual(str(OpenPGPKey('foo@bar.de', key_id='key_id')), str(result))
+
+ @patch('leap.keymanager.requests')
+ def test_http_error_500(self, requests_mock):
+ def do_request(one, data=None, verify=None):
+ response = MagicMock()
+ response.raise_for_status = MagicMock()
+ response.raise_for_status.side_effect = HTTPError
+ return response
+
+ nickserver_url = 'http://some/nickserver/uri'
+ soledad = MagicMock()
+ soledad.get_from_index.side_effect = [[], []]
+ requests_mock.get.side_effect = do_request
+
+ km = KeyManager('me@bar.de', nickserver_url, soledad, ca_cert_path='some path')
+
+ self.assertRaises(KeyNotFound, km.get_key, 'foo@bar.de', OpenPGPKey)