diff options
Diffstat (limited to 'service')
57 files changed, 846 insertions, 439 deletions
@@ -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) |