From 1d70a21a4b6476fb69122c0629440815aa793099 Mon Sep 17 00:00:00 2001 From: Duda Dornelles Date: Wed, 2 Sep 2015 16:14:56 -0300 Subject: [feat] adding encryption header to msg before saving This way we can tell if a message was originally encrypted, so that we can show that information to the end user. --- src/leap/mail/incoming/service.py | 11 ++++++++++- src/leap/mail/incoming/tests/test_incoming_mail.py | 23 +++++++++++++++++++++- 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/src/leap/mail/incoming/service.py b/src/leap/mail/incoming/service.py index 2bc6751..57e0007 100644 --- a/src/leap/mail/incoming/service.py +++ b/src/leap/mail/incoming/service.py @@ -90,6 +90,7 @@ class IncomingMail(Service): CONTENT_KEY = "content" LEAP_SIGNATURE_HEADER = 'X-Leap-Signature' + LEAP_ENCRYPTION_HEADER = 'X-Leap-Encryption' """ Header added to messages when they are decrypted by the fetcher, which states the validity of an eventual signature that might be included @@ -99,6 +100,8 @@ class IncomingMail(Service): LEAP_SIGNATURE_INVALID = 'invalid' LEAP_SIGNATURE_COULD_NOT_VERIFY = 'could not verify' + LEAP_ENCRYPTION_DECRYPTED = 'decrypted' + def __init__(self, keymanager, soledad, inbox, userid, check_period=INCOMING_CHECK_PERIOD): @@ -461,6 +464,9 @@ class IncomingMail(Service): d.addCallback(add_leap_header) return d + def _add_decrypted_header(self, msg): + msg.add_header(self.LEAP_ENCRYPTION_HEADER, self.LEAP_ENCRYPTION_DECRYPTED) + def _decrypt_multipart_encrypted_msg(self, msg, encoding, senderAddress): """ Decrypt a message with content-type 'multipart/encrypted'. @@ -503,6 +509,7 @@ class IncomingMail(Service): # all ok, replace payload by unencrypted payload msg.set_payload(decrmsg.get_payload()) + self._add_decrypted_header(msg) return (msg, signkey) d = self._keymanager.decrypt( @@ -537,7 +544,9 @@ class IncomingMail(Service): def decrypted_data(res): decrdata, signkey = res - return data.replace(pgp_message, decrdata), signkey + replaced_data = data.replace(pgp_message, decrdata) + self._add_decrypted_header(origmsg) + return replaced_data, signkey def encode_and_return(res): data, signkey = res diff --git a/src/leap/mail/incoming/tests/test_incoming_mail.py b/src/leap/mail/incoming/tests/test_incoming_mail.py index f43f746..798824a 100644 --- a/src/leap/mail/incoming/tests/test_incoming_mail.py +++ b/src/leap/mail/incoming/tests/test_incoming_mail.py @@ -176,8 +176,22 @@ subject: independence of cyberspace d.addCallback(put_raw_key_called) return d + def testAddDecryptedHeader(self): + class DummyMsg(): + def __init__(self): + self.headers = {} + + def add_header(self, k, v): + self.headers[k]=v + + msg = DummyMsg() + self.fetcher._add_decrypted_header(msg) + + self.assertEquals(msg.headers['X-Leap-Encryption'], 'decrypted') + def testDecryptEmail(self): self.fetcher._decryption_error = Mock() + self.fetcher._add_decrypted_header = Mock() def create_encrypted_message(encstr): message = Parser().parsestr(self.EMAIL) @@ -198,9 +212,16 @@ subject: independence of cyberspace return newmsg def decryption_error_not_called(_): - self.assertFalse(self.fetcher._decyption_error.called, + self.assertFalse(self.fetcher._decryption_error.called, "There was some errors with decryption") + def add_decrypted_header_called(_): + self.assertTrue(self.fetcher._add_decrypted_header.called, + "There was some errors with decryption") + + + + d = self._km.encrypt( self.EMAIL, ADDRESS, OpenPGPKey, sign=ADDRESS_2) -- cgit v1.2.3 From d4c61d7680a726227541daa13e6f72efbbd0fa2e Mon Sep 17 00:00:00 2001 From: Duda Dornelles Date: Wed, 2 Sep 2015 16:20:10 -0300 Subject: [style] fixing pep8 warnings --- src/leap/mail/incoming/service.py | 3 ++- src/leap/mail/incoming/tests/test_incoming_mail.py | 7 ++----- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/leap/mail/incoming/service.py b/src/leap/mail/incoming/service.py index 57e0007..2e953a7 100644 --- a/src/leap/mail/incoming/service.py +++ b/src/leap/mail/incoming/service.py @@ -465,7 +465,8 @@ class IncomingMail(Service): return d def _add_decrypted_header(self, msg): - msg.add_header(self.LEAP_ENCRYPTION_HEADER, self.LEAP_ENCRYPTION_DECRYPTED) + msg.add_header(self.LEAP_ENCRYPTION_HEADER, + self.LEAP_ENCRYPTION_DECRYPTED) def _decrypt_multipart_encrypted_msg(self, msg, encoding, senderAddress): """ diff --git a/src/leap/mail/incoming/tests/test_incoming_mail.py b/src/leap/mail/incoming/tests/test_incoming_mail.py index 798824a..033799d 100644 --- a/src/leap/mail/incoming/tests/test_incoming_mail.py +++ b/src/leap/mail/incoming/tests/test_incoming_mail.py @@ -182,7 +182,7 @@ subject: independence of cyberspace self.headers = {} def add_header(self, k, v): - self.headers[k]=v + self.headers[k] = v msg = DummyMsg() self.fetcher._add_decrypted_header(msg) @@ -217,10 +217,7 @@ subject: independence of cyberspace def add_decrypted_header_called(_): self.assertTrue(self.fetcher._add_decrypted_header.called, - "There was some errors with decryption") - - - + "There was some errors with decryption") d = self._km.encrypt( self.EMAIL, -- cgit v1.2.3 From 862ed843611bbb10664e06460faee48adce9e5aa Mon Sep 17 00:00:00 2001 From: Kali Kaneko Date: Tue, 8 Sep 2015 18:45:28 -0400 Subject: [feature] improve getmail utility So now it: - Accepts credentials in a file pointed by environment variable. - Allows to specify the mailbox to select as a command line flag. - Allows to select a given message by subject. For example: BITMASK_CREDENTIALS=/tmp/bm.secrets ./getmail --mailbox INBOX --subject 'test mail The two flags are case-insensitive. This is intended to be used as a helper in end-to-end tests. Getting a message by subject it's suboptimal, but I think it's good enough for our testing purposes right now. Related: #7427 --- src/leap/mail/imap/tests/getmail | 102 +++++++++++++++++++++++++++++++-------- 1 file changed, 83 insertions(+), 19 deletions(-) diff --git a/src/leap/mail/imap/tests/getmail b/src/leap/mail/imap/tests/getmail index 0fb00d2..dd3fa0b 100755 --- a/src/leap/mail/imap/tests/getmail +++ b/src/leap/mail/imap/tests/getmail @@ -10,6 +10,7 @@ Simple IMAP4 client which displays the subjects of all messages in a particular mailbox. """ +import os import sys from twisted.internet import protocol @@ -20,6 +21,9 @@ from twisted.mail import imap4 from twisted.protocols import basic from twisted.python import log +# Global options stored here from main +_opts = {} + class TrivialPrompter(basic.LineReceiver): from os import linesep as delimiter @@ -70,9 +74,7 @@ class SimpleIMAP4ClientFactory(protocol.ClientFactory): """ Initiate the protocol instance. Since we are building a simple IMAP client, we don't bother checking what capabilities the server has. We - just add all the authenticators twisted.mail has. Note: Gmail no - longer uses any of the methods below, it's been using XOAUTH since - 2010. + just add all the authenticators twisted.mail has. """ assert not self.usedUp self.usedUp = True @@ -159,14 +161,24 @@ def InsecureLogin(proto, username, password): def cbMailboxList(result, proto): """ Callback invoked when a list of mailboxes has been retrieved. + If we have a selected mailbox in the global options, we directly pick it. + Otherwise, we offer a prompt to let user choose one. """ - result = [e[2] for e in result] - s = '\n'.join(['%d. %s' % (n + 1, m) for (n, m) in zip(range(len(result)), result)]) + all_mbox_list = [e[2] for e in result] + s = '\n'.join(['%d. %s' % (n + 1, m) for (n, m) in zip(range(len(all_mbox_list)), all_mbox_list)]) if not s: return defer.fail(Exception("No mailboxes exist on server!")) - return proto.prompt(s + "\nWhich mailbox? [1] " - ).addCallback(cbPickMailbox, proto, result - ) + + selected_mailbox = _opts.get('mailbox') + + if not selected_mailbox: + return proto.prompt(s + "\nWhich mailbox? [1] " + ).addCallback(cbPickMailbox, proto, all_mbox_list + ) + else: + mboxes_lower = map(lambda s: s.lower(), all_mbox_list) + index = mboxes_lower.index(selected_mailbox.lower()) + 1 + return cbPickMailbox(index, proto, all_mbox_list) def cbPickMailbox(result, proto, mboxes): @@ -194,18 +206,34 @@ def cbExamineMbox(result, proto): def cbFetch(result, proto): """ - Display headers. + Display a listing of the messages in the mailbox, based on the collected + headers. """ + selected_subject = _opts.get('subject', None) + index = None + if result: keys = result.keys() keys.sort() - for k in keys: - proto.display('%s %s' % (k, result[k][0][2])) + + if selected_subject: + for k in keys: + # remove 'Subject: ' preffix plus eol + subject = result[k][0][2][9:].rstrip('\r\n') + if subject.lower() == selected_subject.lower(): + index = k + break + else: + for k in keys: + proto.display('%s %s' % (k, result[k][0][2])) else: print "Hey, an empty mailbox!" - return proto.prompt("\nWhich message? [1] (Q quits) " - ).addCallback(cbPickMessage, proto) + if not index: + return proto.prompt("\nWhich message? [1] (Q quits) " + ).addCallback(cbPickMessage, proto) + else: + return cbPickMessage(index, proto) def cbPickMessage(result, proto): @@ -247,16 +275,53 @@ def cbClose(result): def main(): + import argparse + import ConfigParser import sys + from twisted.internet import reactor + + description = ( + 'Get messages from a LEAP IMAP Proxy.\nThis is a ' + 'debugging tool, do not use this to retrieve any sensitive ' + 'information, or we will send ninjas to your house!') + epilog = ( + 'In case you want to automate the usage of this utility ' + 'you can place your credentials in a file pointed by ' + 'BITMASK_CREDENTIALS. You need to have a [Credentials] ' + 'section, with username= and password fields') + + parser = argparse.ArgumentParser(description=description, epilog=epilog) + credentials = os.environ.get('BITMASK_CREDENTIALS') + + if credentials: + try: + config = ConfigParser.ConfigParser() + config.read(credentials) + username = config.get('Credentials', 'username') + password = config.get('Credentials', 'password') + except Exception, e: + print "Error reading credentials file: {0}".format(e) + sys.exit() + else: + parser.add_argument('username', type=str) + parser.add_argument('password', type=str) - if len(sys.argv) != 3: - print "Usage: getmail " - sys.exit() + parser.add_argument('--mailbox', dest='mailbox', default=None, + help='Which mailbox to retrieve. Empty for interactive prompt.') + parser.add_argument('--subject', dest='subject', default=None, + help='A subject for retrieve a mail that matches. Empty for interactive prompt.') + + ns = parser.parse_args() + + if not credentials: + username = ns.username + password = ns.password + + _opts['mailbox'] = ns.mailbox + _opts['subject'] = ns.subject hostname = "localhost" port = "1984" - username = sys.argv[1] - password = sys.argv[2] onConn = defer.Deferred( ).addCallback(cbServerGreeting, username, password @@ -265,7 +330,6 @@ def main(): factory = SimpleIMAP4ClientFactory(username, onConn) - from twisted.internet import reactor if port == '993': reactor.connectSSL( hostname, int(port), factory, ssl.ClientContextFactory()) -- cgit v1.2.3 From 0a03c0a014b1874114eabb855705ca79f09d1982 Mon Sep 17 00:00:00 2001 From: Kali Kaneko Date: Mon, 14 Sep 2015 23:19:58 -0400 Subject: [feat] use async events api in this way, we're using twisted reactor instead of having another thread with zmq's own copy of tornado ioloop. Resolves: #7274 --- src/leap/mail/imap/server.py | 4 ++-- src/leap/mail/imap/service/imap.py | 6 +++--- src/leap/mail/incoming/service.py | 16 ++++++++-------- src/leap/mail/mail.py | 5 ++--- src/leap/mail/outgoing/service.py | 16 ++++++++-------- src/leap/mail/smtp/__init__.py | 6 +++--- src/leap/mail/smtp/gateway.py | 10 +++++----- 7 files changed, 31 insertions(+), 32 deletions(-) diff --git a/src/leap/mail/imap/server.py b/src/leap/mail/imap/server.py index 39f483f..8f14936 100644 --- a/src/leap/mail/imap/server.py +++ b/src/leap/mail/imap/server.py @@ -27,7 +27,7 @@ from twisted.mail import imap4 from twisted.python import log from leap.common.check import leap_assert, leap_assert_type -from leap.common.events import emit, catalog +from leap.common.events import emit_async, catalog from leap.soledad.client import Soledad # imports for LITERAL+ patch @@ -224,7 +224,7 @@ class LEAPIMAPServer(imap4.IMAP4Server): # bad username, reject. raise cred.error.UnauthorizedLogin() # any dummy password is allowed so far. use realm instead! - emit(catalog.IMAP_CLIENT_LOGIN, "1") + emit_async(catalog.IMAP_CLIENT_LOGIN, "1") return imap4.IAccount, self.theAccount, lambda: None def do_FETCH(self, tag, messages, query, uid=0): diff --git a/src/leap/mail/imap/service/imap.py b/src/leap/mail/imap/service/imap.py index c3ae59a..cd31edf 100644 --- a/src/leap/mail/imap/service/imap.py +++ b/src/leap/mail/imap/service/imap.py @@ -28,7 +28,7 @@ from twisted.internet.protocol import ServerFactory from twisted.mail import imap4 from twisted.python import log -from leap.common.events import emit, catalog +from leap.common.events import emit_async, catalog from leap.common.check import leap_check from leap.mail.imap.account import IMAPAccount from leap.mail.imap.server import LEAPIMAPServer @@ -178,10 +178,10 @@ def run_service(store, **kwargs): reactor.listenTCP(manhole.MANHOLE_PORT, manhole_factory, interface="127.0.0.1") logger.debug("IMAP4 Server is RUNNING in port %s" % (port,)) - emit(catalog.IMAP_SERVICE_STARTED, str(port)) + emit_async(catalog.IMAP_SERVICE_STARTED, str(port)) # FIXME -- change service signature return tport, factory # not ok, signal error. - emit(catalog.IMAP_SERVICE_FAILED_TO_START, str(port)) + emit_async(catalog.IMAP_SERVICE_FAILED_TO_START, str(port)) diff --git a/src/leap/mail/incoming/service.py b/src/leap/mail/incoming/service.py index 2e953a7..2a3a86a 100644 --- a/src/leap/mail/incoming/service.py +++ b/src/leap/mail/incoming/service.py @@ -38,7 +38,7 @@ from twisted.internet.task import LoopingCall from twisted.internet.task import deferLater from u1db import errors as u1db_errors -from leap.common.events import emit, catalog +from leap.common.events import emit_async, catalog from leap.common.check import leap_assert, leap_assert_type from leap.common.mail import get_email_charset from leap.keymanager import errors as keymanager_errors @@ -231,7 +231,7 @@ class IncomingMail(Service): except InvalidAuthTokenError: # if the token is invalid, send an event so the GUI can # disable mail and show an error message. - emit(catalog.SOLEDAD_INVALID_AUTH_TOKEN) + emit_async(catalog.SOLEDAD_INVALID_AUTH_TOKEN) def _signal_fetch_to_ui(self, doclist): """ @@ -247,7 +247,7 @@ class IncomingMail(Service): num_mails = len(doclist) if doclist is not None else 0 if num_mails != 0: log.msg("there are %s mails" % (num_mails,)) - emit(catalog.MAIL_FETCHED_INCOMING, + emit_async(catalog.MAIL_FETCHED_INCOMING, str(num_mails), str(fetched_ts)) return doclist @@ -255,7 +255,7 @@ class IncomingMail(Service): """ Sends unread event to ui. """ - emit(catalog.MAIL_UNREAD_MESSAGES, + emit_async(catalog.MAIL_UNREAD_MESSAGES, str(self._inbox_collection.count_unseen())) # process incoming mail. @@ -279,7 +279,7 @@ class IncomingMail(Service): deferreds = [] for index, doc in enumerate(doclist): logger.debug("processing doc %d of %d" % (index + 1, num_mails)) - emit(catalog.MAIL_MSG_PROCESSING, + emit_async(catalog.MAIL_MSG_PROCESSING, str(index), str(num_mails)) keys = doc.content.keys() @@ -329,7 +329,7 @@ class IncomingMail(Service): decrdata = "" success = False - emit(catalog.MAIL_MSG_DECRYPTED, "1" if success else "0") + emit_async(catalog.MAIL_MSG_DECRYPTED, "1" if success else "0") return self._process_decrypted_doc(doc, decrdata) d = self._keymanager.decrypt( @@ -723,10 +723,10 @@ class IncomingMail(Service): listener(result) def signal_deleted(doc_id): - emit(catalog.MAIL_MSG_DELETED_INCOMING) + emit_async(catalog.MAIL_MSG_DELETED_INCOMING) return doc_id - emit(catalog.MAIL_MSG_SAVED_LOCALLY) + emit_async(catalog.MAIL_MSG_SAVED_LOCALLY) d = self._delete_incoming_message(doc) d.addCallback(signal_deleted) return d diff --git a/src/leap/mail/mail.py b/src/leap/mail/mail.py index 540a493..258574e 100644 --- a/src/leap/mail/mail.py +++ b/src/leap/mail/mail.py @@ -28,7 +28,7 @@ from twisted.internet import defer from twisted.python import log from leap.common.check import leap_assert_type -from leap.common.events import emit, catalog +from leap.common.events import emit_async, catalog from leap.common.mail import get_email_charset from leap.mail.adaptors.soledad import SoledadMailAdaptor @@ -736,8 +736,7 @@ class MessageCollection(object): :param unseen: number of unseen messages. :type unseen: int """ - # TODO change name of the signal, independent from imap now. - emit(catalog.MAIL_UNREAD_MESSAGES, str(unseen)) + emit_async(catalog.MAIL_UNREAD_MESSAGES, str(unseen)) def copy_msg(self, msg, new_mbox_uuid): """ diff --git a/src/leap/mail/outgoing/service.py b/src/leap/mail/outgoing/service.py index 838a908..3708f33 100644 --- a/src/leap/mail/outgoing/service.py +++ b/src/leap/mail/outgoing/service.py @@ -31,7 +31,7 @@ from twisted.protocols.amp import ssl from twisted.python import log from leap.common.check import leap_assert_type, leap_assert -from leap.common.events import emit, catalog +from leap.common.events import emit_async, catalog from leap.keymanager.openpgp import OpenPGPKey from leap.keymanager.errors import KeyNotFound, KeyAddressMismatch from leap.mail import __version__ @@ -135,7 +135,7 @@ class OutgoingMail: """ dest_addrstr = smtp_sender_result[1][0][0] log.msg('Message sent to %s' % dest_addrstr) - emit(catalog.SMTP_SEND_MESSAGE_SUCCESS, dest_addrstr) + emit_async(catalog.SMTP_SEND_MESSAGE_SUCCESS, dest_addrstr) def sendError(self, failure): """ @@ -145,7 +145,7 @@ class OutgoingMail: :type e: anything """ # XXX: need to get the address from the exception to send signal - # emit(catalog.SMTP_SEND_MESSAGE_ERROR, self._user.dest.addrstr) + # emit_async(catalog.SMTP_SEND_MESSAGE_ERROR, self._user.dest.addrstr) err = failure.value log.err(err) raise err @@ -178,7 +178,7 @@ class OutgoingMail: requireAuthentication=False, requireTransportSecurity=True) factory.domain = __version__ - emit(catalog.SMTP_SEND_MESSAGE_START, recipient.dest.addrstr) + emit_async(catalog.SMTP_SEND_MESSAGE_START, recipient.dest.addrstr) reactor.connectSSL( self._host, self._port, factory, contextFactory=SSLContextFactory(self._cert, self._key)) @@ -240,7 +240,7 @@ class OutgoingMail: return d def signal_encrypt_sign(newmsg): - emit(catalog.SMTP_END_ENCRYPT_AND_SIGN, + emit_async(catalog.SMTP_END_ENCRYPT_AND_SIGN, "%s,%s" % (self._from_address, to_address)) return newmsg, recipient @@ -248,18 +248,18 @@ class OutgoingMail: failure.trap(KeyNotFound, KeyAddressMismatch) log.msg('Will send unencrypted message to %s.' % to_address) - emit(catalog.SMTP_START_SIGN, self._from_address) + emit_async(catalog.SMTP_START_SIGN, self._from_address) d = self._sign(message, from_address) d.addCallback(signal_sign) return d def signal_sign(newmsg): - emit(catalog.SMTP_END_SIGN, self._from_address) + emit_async(catalog.SMTP_END_SIGN, self._from_address) return newmsg, recipient log.msg("Will encrypt the message with %s and sign with %s." % (to_address, from_address)) - emit(catalog.SMTP_START_ENCRYPT_AND_SIGN, + emit_async(catalog.SMTP_START_ENCRYPT_AND_SIGN, "%s,%s" % (self._from_address, to_address)) d = self._maybe_attach_key(origmsg, from_address, to_address) d.addCallback(maybe_encrypt_and_sign) diff --git a/src/leap/mail/smtp/__init__.py b/src/leap/mail/smtp/__init__.py index 2ff14d7..a77a414 100644 --- a/src/leap/mail/smtp/__init__.py +++ b/src/leap/mail/smtp/__init__.py @@ -24,7 +24,7 @@ from twisted.internet import reactor from twisted.internet.error import CannotListenError from leap.mail.outgoing.service import OutgoingMail -from leap.common.events import emit, catalog +from leap.common.events import emit_async, catalog from leap.mail.smtp.gateway import SMTPFactory logger = logging.getLogger(__name__) @@ -65,12 +65,12 @@ def setup_smtp_gateway(port, userid, keymanager, smtp_host, smtp_port, factory = SMTPFactory(userid, keymanager, encrypted_only, outgoing_mail) try: tport = reactor.listenTCP(port, factory, interface="localhost") - emit(catalog.SMTP_SERVICE_STARTED, str(port)) + emit_async(catalog.SMTP_SERVICE_STARTED, str(port)) return factory, tport except CannotListenError: logger.error("STMP Service failed to start: " "cannot listen in port %s" % port) - emit(catalog.SMTP_SERVICE_FAILED_TO_START, str(port)) + emit_async(catalog.SMTP_SERVICE_FAILED_TO_START, str(port)) except Exception as exc: logger.error("Unhandled error while launching smtp gateway service") logger.exception(exc) diff --git a/src/leap/mail/smtp/gateway.py b/src/leap/mail/smtp/gateway.py index 7dae907..dd110e0 100644 --- a/src/leap/mail/smtp/gateway.py +++ b/src/leap/mail/smtp/gateway.py @@ -37,7 +37,7 @@ from twisted.python import log from email.Header import Header from leap.common.check import leap_assert_type -from leap.common.events import emit, catalog +from leap.common.events import emit_async, catalog from leap.keymanager.openpgp import OpenPGPKey from leap.keymanager.errors import KeyNotFound from leap.mail.utils import validate_address @@ -204,18 +204,18 @@ class SMTPDelivery(object): # verify if recipient key is available in keyring def found(_): log.msg("Accepting mail for %s..." % user.dest.addrstr) - emit(catalog.SMTP_RECIPIENT_ACCEPTED_ENCRYPTED, user.dest.addrstr) + emit_async(catalog.SMTP_RECIPIENT_ACCEPTED_ENCRYPTED, user.dest.addrstr) def not_found(failure): failure.trap(KeyNotFound) # if key was not found, check config to see if will send anyway if self._encrypted_only: - emit(catalog.SMTP_RECIPIENT_REJECTED, user.dest.addrstr) + emit_async(catalog.SMTP_RECIPIENT_REJECTED, user.dest.addrstr) raise smtp.SMTPBadRcpt(user.dest.addrstr) log.msg("Warning: will send an unencrypted message (because " "encrypted_only' is set to False).") - emit( + emit_async( catalog.SMTP_RECIPIENT_ACCEPTED_UNENCRYPTED, user.dest.addrstr) @@ -309,7 +309,7 @@ class EncryptedMessage(object): """ log.msg("Connection lost unexpectedly!") log.err() - emit(catalog.SMTP_CONNECTION_LOST, self._user.dest.addrstr) + emit_async(catalog.SMTP_CONNECTION_LOST, self._user.dest.addrstr) # unexpected loss of connection; don't save self._lines = [] -- cgit v1.2.3 From 64aea2effaa503a31c10f04a6243c6fbc8c7baea Mon Sep 17 00:00:00 2001 From: Kali Kaneko Date: Mon, 14 Sep 2015 23:33:16 -0400 Subject: [pkg] bump leap versions needed --- pkg/requirements-leap.pip | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/requirements-leap.pip b/pkg/requirements-leap.pip index f50487e..feb9f37 100644 --- a/pkg/requirements-leap.pip +++ b/pkg/requirements-leap.pip @@ -1,3 +1,3 @@ -leap.common>=0.4.0 +leap.common>=0.4.3 leap.soledad.client>=0.7.0 leap.keymanager>=0.4.0 -- cgit v1.2.3 From 818ea26ec559174302ddf9095f26ed624b5cd507 Mon Sep 17 00:00:00 2001 From: Ruben Pollan Date: Wed, 16 Sep 2015 12:26:11 +0200 Subject: [bug] don't fail importing mismatched attached key We can't import attached keys with different email address than the sender. Now we don't fail in this case, just log it. - Resolves: #7454 --- src/leap/mail/incoming/service.py | 5 +++++ src/leap/mail/incoming/tests/test_incoming_mail.py | 24 +++++++++++++++++++--- 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/src/leap/mail/incoming/service.py b/src/leap/mail/incoming/service.py index 2a3a86a..8d8f3c2 100644 --- a/src/leap/mail/incoming/service.py +++ b/src/leap/mail/incoming/service.py @@ -686,6 +686,10 @@ class IncomingMail(Service): """ MIME_KEY = "application/pgp-keys" + def failed_put_key(failure): + logger.info("An error has ocurred adding attached key for %s: %s" + % (address, failure.getErrorMessage())) + deferreds = [] for attachment in attachments: if MIME_KEY == attachment.get_content_type(): @@ -694,6 +698,7 @@ class IncomingMail(Service): attachment.get_payload(), OpenPGPKey, address=address) + d.addErrback(failed_put_key) deferreds.append(d) return defer.gatherResults(deferreds) diff --git a/src/leap/mail/incoming/tests/test_incoming_mail.py b/src/leap/mail/incoming/tests/test_incoming_mail.py index 033799d..589ddad 100644 --- a/src/leap/mail/incoming/tests/test_incoming_mail.py +++ b/src/leap/mail/incoming/tests/test_incoming_mail.py @@ -30,6 +30,7 @@ from email.parser import Parser from mock import Mock from twisted.internet import defer +from leap.keymanager.errors import KeyAddressMismatch from leap.keymanager.openpgp import OpenPGPKey from leap.mail.adaptors import soledad_indexes as fields from leap.mail.constants import INBOX_NAME @@ -154,9 +155,6 @@ subject: independence of cyberspace return d def testExtractAttachedKey(self): - """ - Test the OpenPGP header key extraction - """ KEY = "-----BEGIN PGP PUBLIC KEY BLOCK-----\n..." message = MIMEMultipart() @@ -176,6 +174,26 @@ subject: independence of cyberspace d.addCallback(put_raw_key_called) return d + def testExtractInvalidAttachedKey(self): + KEY = "-----BEGIN PGP PUBLIC KEY BLOCK-----\n..." + + message = MIMEMultipart() + message.add_header("from", ADDRESS_2) + key = MIMEApplication("", "pgp-keys") + key.set_payload(KEY) + message.attach(key) + self.fetcher._keymanager.put_raw_key = Mock( + return_value=defer.fail(KeyAddressMismatch())) + self.fetcher._keymanager.fetch_key = Mock() + + def put_raw_key_called(_): + self.fetcher._keymanager.put_raw_key.assert_called_once_with( + KEY, OpenPGPKey, address=ADDRESS_2) + + d = self._do_fetch(message.as_string()) + d.addCallback(put_raw_key_called) + return d + def testAddDecryptedHeader(self): class DummyMsg(): def __init__(self): -- cgit v1.2.3 From 9f3d6812da3b361886cbca421c0f9d050eba3ac5 Mon Sep 17 00:00:00 2001 From: Ruben Pollan Date: Wed, 16 Sep 2015 12:31:50 +0200 Subject: [style] clean up incoming/service.py --- src/leap/mail/incoming/service.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/leap/mail/incoming/service.py b/src/leap/mail/incoming/service.py index 8d8f3c2..c98e639 100644 --- a/src/leap/mail/incoming/service.py +++ b/src/leap/mail/incoming/service.py @@ -21,7 +21,6 @@ import copy import logging import shlex import time -import traceback import warnings from email.parser import Parser @@ -36,7 +35,6 @@ from twisted.python import log from twisted.internet import defer, reactor from twisted.internet.task import LoopingCall from twisted.internet.task import deferLater -from u1db import errors as u1db_errors from leap.common.events import emit_async, catalog from leap.common.check import leap_assert, leap_assert_type @@ -44,7 +42,7 @@ from leap.common.mail import get_email_charset from leap.keymanager import errors as keymanager_errors from leap.keymanager.openpgp import OpenPGPKey from leap.mail.adaptors import soledad_indexes as fields -from leap.mail.utils import json_loads, empty, first +from leap.mail.utils import json_loads, empty from leap.soledad.client import Soledad from leap.soledad.common.crypto import ENC_SCHEME_KEY, ENC_JSON_KEY from leap.soledad.common.errors import InvalidAuthTokenError @@ -248,7 +246,7 @@ class IncomingMail(Service): if num_mails != 0: log.msg("there are %s mails" % (num_mails,)) emit_async(catalog.MAIL_FETCHED_INCOMING, - str(num_mails), str(fetched_ts)) + str(num_mails), str(fetched_ts)) return doclist def _signal_unread_to_ui(self, *args): @@ -256,7 +254,7 @@ class IncomingMail(Service): Sends unread event to ui. """ emit_async(catalog.MAIL_UNREAD_MESSAGES, - str(self._inbox_collection.count_unseen())) + str(self._inbox_collection.count_unseen())) # process incoming mail. @@ -280,7 +278,7 @@ class IncomingMail(Service): for index, doc in enumerate(doclist): logger.debug("processing doc %d of %d" % (index + 1, num_mails)) emit_async(catalog.MAIL_MSG_PROCESSING, - str(index), str(num_mails)) + str(index), str(num_mails)) keys = doc.content.keys() -- cgit v1.2.3 From 36966a403237307a68359cac97a718462b2e02e3 Mon Sep 17 00:00:00 2001 From: Kali Kaneko Date: Mon, 21 Sep 2015 16:37:25 -0400 Subject: [doc] document return values --- src/leap/mail/incoming/service.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/leap/mail/incoming/service.py b/src/leap/mail/incoming/service.py index c98e639..ec85eed 100644 --- a/src/leap/mail/incoming/service.py +++ b/src/leap/mail/incoming/service.py @@ -32,6 +32,7 @@ from urlparse import urlparse from twisted.application.service import Service from twisted.python import log +from twisted.python.failure import Failure from twisted.internet import defer, reactor from twisted.internet.task import LoopingCall from twisted.internet.task import deferLater @@ -183,13 +184,21 @@ class IncomingMail(Service): def startService(self): """ Starts a loop to fetch mail. + + :returns: A Deferred whose callback will be invoked with + the LoopingCall instance when loop.stop is called, or + whose errback will be invoked when the function raises an + exception or returned a deferred that has its errback + invoked. """ Service.startService(self) if self._loop is None: self._loop = LoopingCall(self.fetch) - return self._loop.start(self._check_period) + stop_deferred = self._loop.start(self._check_period) + return stop_deferred else: logger.warning("Tried to start an already running fetching loop.") + return defer.fail(Failure('Already running loop.')) def stopService(self): """ -- cgit v1.2.3 From 5bc9889e3afe9f9bcc39c60b819de18c46d0322a Mon Sep 17 00:00:00 2001 From: Kali Kaneko Date: Mon, 21 Sep 2015 17:22:23 -0400 Subject: [bug] filter out Nones in the sequence of messages --- src/leap/mail/imap/mailbox.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/leap/mail/imap/mailbox.py b/src/leap/mail/imap/mailbox.py index c52a2e3..e73994b 100644 --- a/src/leap/mail/imap/mailbox.py +++ b/src/leap/mail/imap/mailbox.py @@ -558,7 +558,8 @@ class IMAPMailbox(object): def _get_imap_msg(messages): d_imapmsg = [] - for msg in messages: + # just in case we got bad data in here + for msg in filter(None, messages): d_imapmsg.append(getimapmsg(msg)) return defer.gatherResults(d_imapmsg, consumeErrors=True) -- cgit v1.2.3 From e2b3cc5c88dd8f66ec02fe644944218d58e996d8 Mon Sep 17 00:00:00 2001 From: Ruben Pollan Date: Tue, 22 Sep 2015 17:03:51 +0200 Subject: [bug] don't extract openpgp header if valid attached key The key extract should check first for attached keys and if this fails then should try the OpenPGP header. - Resolves: #7480 --- changes/bug-7480_extract_attach_and_openpgp | 1 + src/leap/mail/incoming/service.py | 37 +++++++++------- src/leap/mail/incoming/tests/test_incoming_mail.py | 51 +++++++++++++++++++++- src/leap/mail/tests/__init__.py | 2 +- 4 files changed, 73 insertions(+), 18 deletions(-) create mode 100644 changes/bug-7480_extract_attach_and_openpgp diff --git a/changes/bug-7480_extract_attach_and_openpgp b/changes/bug-7480_extract_attach_and_openpgp new file mode 100644 index 0000000..27f668a --- /dev/null +++ b/changes/bug-7480_extract_attach_and_openpgp @@ -0,0 +1 @@ +- don't extract openpgp header if valid attached key (Closes: #7480) diff --git a/src/leap/mail/incoming/service.py b/src/leap/mail/incoming/service.py index ec85eed..4ae4a40 100644 --- a/src/leap/mail/incoming/service.py +++ b/src/leap/mail/incoming/service.py @@ -304,7 +304,7 @@ class IncomingMail(Service): logger.debug("skipping msg with decrypting errors...") elif self._is_msg(keys): d = self._decrypt_doc(doc) - d.addCallback(self._extract_keys) + d.addCallback(self._maybe_extract_keys) d.addCallbacks(self._add_message_locally, self._errback) deferreds.append(d) d = defer.gatherResults(deferreds, consumeErrors=True) @@ -594,7 +594,8 @@ class IncomingMail(Service): else: return failure - def _extract_keys(self, msgtuple): + @defer.inlineCallbacks + def _maybe_extract_keys(self, msgtuple): """ Retrieve attached keys to the mesage and parse message headers for an *OpenPGP* header as described on the `IETF draft @@ -621,20 +622,19 @@ class IncomingMail(Service): msg = self._parser.parsestr(data) _, fromAddress = parseaddr(msg['from']) - header = msg.get(OpenPGP_HEADER, None) - dh = defer.succeed(None) - if header is not None: - dh = self._extract_openpgp_header(header, fromAddress) - - da = defer.succeed(None) + valid_attachment = False if msg.is_multipart(): - da = self._extract_attached_key(msg.get_payload(), fromAddress) + valid_attachment = yield self._maybe_extract_attached_key( + msg.get_payload(), fromAddress) - d = defer.gatherResults([dh, da]) - d.addCallback(lambda _: msgtuple) - return d + if not valid_attachment: + header = msg.get(OpenPGP_HEADER, None) + if header is not None: + yield self._maybe_extract_openpgp_header(header, fromAddress) - def _extract_openpgp_header(self, header, address): + defer.returnValue(msgtuple) + + def _maybe_extract_openpgp_header(self, header, address): """ Import keys from the OpenPGP header @@ -679,7 +679,7 @@ class IncomingMail(Service): % (header,)) return d - def _extract_attached_key(self, attachments, address): + def _maybe_extract_attached_key(self, attachments, address): """ Import keys from the attachments @@ -689,6 +689,8 @@ class IncomingMail(Service): :type address: str :return: A Deferred that will be fired when all the keys are stored + with a boolean True if there was a valid key attached or + False in other case :rtype: Deferred """ MIME_KEY = "application/pgp-keys" @@ -696,6 +698,7 @@ class IncomingMail(Service): def failed_put_key(failure): logger.info("An error has ocurred adding attached key for %s: %s" % (address, failure.getErrorMessage())) + return False deferreds = [] for attachment in attachments: @@ -705,9 +708,11 @@ class IncomingMail(Service): attachment.get_payload(), OpenPGPKey, address=address) - d.addErrback(failed_put_key) + d.addCallbacks(lambda _: True, failed_put_key) deferreds.append(d) - return defer.gatherResults(deferreds) + d = defer.gatherResults(deferreds) + d.addCallback(lambda result: any(result)) + return d def _add_message_locally(self, msgtuple): """ diff --git a/src/leap/mail/incoming/tests/test_incoming_mail.py b/src/leap/mail/incoming/tests/test_incoming_mail.py index 589ddad..964c8fd 100644 --- a/src/leap/mail/incoming/tests/test_incoming_mail.py +++ b/src/leap/mail/incoming/tests/test_incoming_mail.py @@ -164,7 +164,6 @@ subject: independence of cyberspace message.attach(key) self.fetcher._keymanager.put_raw_key = Mock( return_value=defer.succeed(None)) - self.fetcher._keymanager.fetch_key = Mock() def put_raw_key_called(_): self.fetcher._keymanager.put_raw_key.assert_called_once_with( @@ -184,11 +183,61 @@ subject: independence of cyberspace message.attach(key) self.fetcher._keymanager.put_raw_key = Mock( return_value=defer.fail(KeyAddressMismatch())) + + def put_raw_key_called(_): + self.fetcher._keymanager.put_raw_key.assert_called_once_with( + KEY, OpenPGPKey, address=ADDRESS_2) + + d = self._do_fetch(message.as_string()) + d.addCallback(put_raw_key_called) + return d + + def testExtractAttachedKeyAndNotOpenPGPHeader(self): + KEY = "-----BEGIN PGP PUBLIC KEY BLOCK-----\n..." + KEYURL = "https://leap.se/key.txt" + OpenPGP = "id=12345678; url=\"%s\"; preference=signencrypt" % (KEYURL,) + + message = MIMEMultipart() + message.add_header("from", ADDRESS_2) + message.add_header("OpenPGP", OpenPGP) + key = MIMEApplication("", "pgp-keys") + key.set_payload(KEY) + message.attach(key) + + self.fetcher._keymanager.put_raw_key = Mock( + return_value=defer.succeed(None)) self.fetcher._keymanager.fetch_key = Mock() def put_raw_key_called(_): self.fetcher._keymanager.put_raw_key.assert_called_once_with( KEY, OpenPGPKey, address=ADDRESS_2) + self.assertFalse(self.fetcher._keymanager.fetch_key.called) + + d = self._do_fetch(message.as_string()) + d.addCallback(put_raw_key_called) + return d + + def testExtractOpenPGPHeaderIfInvalidAttachedKey(self): + KEY = "-----BEGIN PGP PUBLIC KEY BLOCK-----\n..." + KEYURL = "https://leap.se/key.txt" + OpenPGP = "id=12345678; url=\"%s\"; preference=signencrypt" % (KEYURL,) + + message = MIMEMultipart() + message.add_header("from", ADDRESS_2) + message.add_header("OpenPGP", OpenPGP) + key = MIMEApplication("", "pgp-keys") + key.set_payload(KEY) + message.attach(key) + + self.fetcher._keymanager.put_raw_key = Mock( + return_value=defer.fail(KeyAddressMismatch())) + self.fetcher._keymanager.fetch_key = Mock() + + def put_raw_key_called(_): + self.fetcher._keymanager.put_raw_key.assert_called_once_with( + KEY, OpenPGPKey, address=ADDRESS_2) + self.fetcher._keymanager.fetch_key.assert_called_once_with( + ADDRESS_2, KEYURL, OpenPGPKey) d = self._do_fetch(message.as_string()) d.addCallback(put_raw_key_called) diff --git a/src/leap/mail/tests/__init__.py b/src/leap/mail/tests/__init__.py index de0088f..71452d2 100644 --- a/src/leap/mail/tests/__init__.py +++ b/src/leap/mail/tests/__init__.py @@ -91,7 +91,7 @@ class TestCaseWithKeyManager(unittest.TestCase, BaseLeapTest): nickserver_url = '' # the url of the nickserver self._km = KeyManager(address, nickserver_url, self._soledad, - ca_cert_path='', gpgbinary=self.GPG_BINARY_PATH) + gpgbinary=self.GPG_BINARY_PATH) self._km._fetcher.put = Mock() self._km._fetcher.get = Mock(return_value=Response()) -- cgit v1.2.3 From 04baa4e421d22b02b259284c3f119d8480a989fa Mon Sep 17 00:00:00 2001 From: Kali Kaneko Date: Tue, 22 Sep 2015 14:45:57 -0400 Subject: [refactor] log the added key explicitely --- src/leap/mail/incoming/service.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/leap/mail/incoming/service.py b/src/leap/mail/incoming/service.py index 4ae4a40..d554c51 100644 --- a/src/leap/mail/incoming/service.py +++ b/src/leap/mail/incoming/service.py @@ -689,12 +689,16 @@ class IncomingMail(Service): :type address: str :return: A Deferred that will be fired when all the keys are stored - with a boolean True if there was a valid key attached or - False in other case + with a boolean: True if there was a valid key attached, or + False otherwise. :rtype: Deferred """ MIME_KEY = "application/pgp-keys" + def log_key_added(ignored): + logger.debug('Added key found in attachment for %s' % address) + return True + def failed_put_key(failure): logger.info("An error has ocurred adding attached key for %s: %s" % (address, failure.getErrorMessage())) @@ -703,12 +707,11 @@ class IncomingMail(Service): deferreds = [] for attachment in attachments: if MIME_KEY == attachment.get_content_type(): - logger.debug("Add key from attachment") d = self._keymanager.put_raw_key( attachment.get_payload(), OpenPGPKey, address=address) - d.addCallbacks(lambda _: True, failed_put_key) + d.addCallbacks(log_key_added, failed_put_key) deferreds.append(d) d = defer.gatherResults(deferreds) d.addCallback(lambda result: any(result)) -- cgit v1.2.3 From 1a1dcbd660536ad6351c6a3746a8d8f8a6a31ef9 Mon Sep 17 00:00:00 2001 From: Folker Bernitt Date: Wed, 16 Sep 2015 10:13:12 +0200 Subject: [style] fix pep8 warnings --- src/leap/mail/outgoing/service.py | 4 ++-- src/leap/mail/smtp/gateway.py | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/leap/mail/outgoing/service.py b/src/leap/mail/outgoing/service.py index 3708f33..3754650 100644 --- a/src/leap/mail/outgoing/service.py +++ b/src/leap/mail/outgoing/service.py @@ -241,7 +241,7 @@ class OutgoingMail: def signal_encrypt_sign(newmsg): emit_async(catalog.SMTP_END_ENCRYPT_AND_SIGN, - "%s,%s" % (self._from_address, to_address)) + "%s,%s" % (self._from_address, to_address)) return newmsg, recipient def if_key_not_found_send_unencrypted(failure, message): @@ -260,7 +260,7 @@ class OutgoingMail: log.msg("Will encrypt the message with %s and sign with %s." % (to_address, from_address)) emit_async(catalog.SMTP_START_ENCRYPT_AND_SIGN, - "%s,%s" % (self._from_address, to_address)) + "%s,%s" % (self._from_address, to_address)) d = self._maybe_attach_key(origmsg, from_address, to_address) d.addCallback(maybe_encrypt_and_sign) return d diff --git a/src/leap/mail/smtp/gateway.py b/src/leap/mail/smtp/gateway.py index dd110e0..c988367 100644 --- a/src/leap/mail/smtp/gateway.py +++ b/src/leap/mail/smtp/gateway.py @@ -204,7 +204,8 @@ class SMTPDelivery(object): # verify if recipient key is available in keyring def found(_): log.msg("Accepting mail for %s..." % user.dest.addrstr) - emit_async(catalog.SMTP_RECIPIENT_ACCEPTED_ENCRYPTED, user.dest.addrstr) + emit_async(catalog.SMTP_RECIPIENT_ACCEPTED_ENCRYPTED, + user.dest.addrstr) def not_found(failure): failure.trap(KeyNotFound) -- cgit v1.2.3 From de07e19f81207084e102801febeaf96680ce5d8a Mon Sep 17 00:00:00 2001 From: Folker Bernitt Date: Wed, 23 Sep 2015 10:42:25 +0200 Subject: [bug] Make _collection_mapping a instance variable As a class variable multiple account instances share mailboxes which is bad if its different users or tests --- src/leap/mail/mail.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/src/leap/mail/mail.py b/src/leap/mail/mail.py index 258574e..fc5abd2 100644 --- a/src/leap/mail/mail.py +++ b/src/leap/mail/mail.py @@ -916,17 +916,19 @@ class Account(object): adaptor_class = SoledadMailAdaptor - # This is a mapping to collection instances so that we always - # return a reference to them instead of creating new ones. However, being a - # dictionary of weakrefs values, they automagically vanish from the dict - # when no hard refs is left to them (so they can be garbage collected) - # This is important because the different wrappers rely on several - # kinds of deferredLocks that are kept as class or instance variables - _collection_mapping = weakref.WeakValueDictionary() - def __init__(self, store, ready_cb=None): self.store = store self.adaptor = self.adaptor_class() + + # this is a mapping to collection instances so that we always + # return a reference to them instead of creating new ones. however, + # being a dictionary of weakrefs values, they automagically vanish + # from the dict when no hard refs is left to them (so they can be + # garbage collected) this is important because the different wrappers + # rely on several kinds of deferredlocks that are kept as class or + # instance variables + self._collection_mapping = weakref.WeakValueDictionary() + self.mbox_indexer = MailboxIndexer(self.store) # This flag is only used from the imap service for the moment. -- cgit v1.2.3 From 309578890ba5d188f3d569473d756c28e06aa7ab Mon Sep 17 00:00:00 2001 From: Kali Kaneko Date: Tue, 22 Sep 2015 15:29:23 -0400 Subject: [docs] update docs to 0.4.0 release --- docs/api/Makefile | 177 ------------------ docs/api/conf.py | 331 ---------------------------------- docs/api/index.rst | 23 --- docs/api/leap.mail.adaptors.rst | 43 +++++ docs/api/leap.mail.adaptors.tests.rst | 28 +++ docs/api/leap.mail.imap.rst | 52 ++++++ docs/api/leap.mail.imap.service.rst | 9 + docs/api/leap.mail.incoming.rst | 20 ++ docs/api/leap.mail.outgoing.rst | 21 +++ docs/api/leap.mail.plugins.rst | 20 ++ docs/api/leap.mail.rst | 105 +++++++++++ docs/api/leap.mail.smtp.rst | 19 ++ docs/api/mail.imap.rst | 118 ------------ docs/api/mail.imap.service.rst | 30 --- docs/api/mail.imap.tests.rst | 38 ---- docs/api/mail.rst | 70 ------- docs/api/mail.smtp.rst | 37 ---- docs/api/mail.smtp.tests.rst | 22 --- docs/api/make.bat | 242 ------------------------- docs/conf.py | 17 +- docs/index.rst | 66 +++++-- docs/intro.rst | 4 - docs/recreate_apidocs.sh | 4 + 23 files changed, 380 insertions(+), 1116 deletions(-) delete mode 100644 docs/api/Makefile delete mode 100644 docs/api/conf.py delete mode 100644 docs/api/index.rst create mode 100644 docs/api/leap.mail.adaptors.rst create mode 100644 docs/api/leap.mail.adaptors.tests.rst create mode 100644 docs/api/leap.mail.imap.rst create mode 100644 docs/api/leap.mail.imap.service.rst create mode 100644 docs/api/leap.mail.incoming.rst create mode 100644 docs/api/leap.mail.outgoing.rst create mode 100644 docs/api/leap.mail.plugins.rst create mode 100644 docs/api/leap.mail.rst create mode 100644 docs/api/leap.mail.smtp.rst delete mode 100644 docs/api/mail.imap.rst delete mode 100644 docs/api/mail.imap.service.rst delete mode 100644 docs/api/mail.imap.tests.rst delete mode 100644 docs/api/mail.rst delete mode 100644 docs/api/mail.smtp.rst delete mode 100644 docs/api/mail.smtp.tests.rst delete mode 100644 docs/api/make.bat delete mode 100644 docs/intro.rst create mode 100755 docs/recreate_apidocs.sh diff --git a/docs/api/Makefile b/docs/api/Makefile deleted file mode 100644 index ebcd0f4..0000000 --- a/docs/api/Makefile +++ /dev/null @@ -1,177 +0,0 @@ -# Makefile for Sphinx documentation -# - -# You can set these variables from the command line. -SPHINXOPTS = -SPHINXBUILD = sphinx-build -PAPER = -BUILDDIR = _build - -# User-friendly check for sphinx-build -ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) -$(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) -endif - -# Internal variables. -PAPEROPT_a4 = -D latex_paper_size=a4 -PAPEROPT_letter = -D latex_paper_size=letter -ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . -# the i18n builder cannot share the environment and doctrees with the others -I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . - -.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext - -help: - @echo "Please use \`make ' where is one of" - @echo " html to make standalone HTML files" - @echo " dirhtml to make HTML files named index.html in directories" - @echo " singlehtml to make a single large HTML file" - @echo " pickle to make pickle files" - @echo " json to make JSON files" - @echo " htmlhelp to make HTML files and a HTML help project" - @echo " qthelp to make HTML files and a qthelp project" - @echo " devhelp to make HTML files and a Devhelp project" - @echo " epub to make an epub" - @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" - @echo " latexpdf to make LaTeX files and run them through pdflatex" - @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" - @echo " text to make text files" - @echo " man to make manual pages" - @echo " texinfo to make Texinfo files" - @echo " info to make Texinfo files and run them through makeinfo" - @echo " gettext to make PO message catalogs" - @echo " changes to make an overview of all changed/added/deprecated items" - @echo " xml to make Docutils-native XML files" - @echo " pseudoxml to make pseudoxml-XML files for display purposes" - @echo " linkcheck to check all external links for integrity" - @echo " doctest to run all doctests embedded in the documentation (if enabled)" - -clean: - rm -rf $(BUILDDIR)/* - -html: - $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html - @echo - @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." - -dirhtml: - $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml - @echo - @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." - -singlehtml: - $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml - @echo - @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." - -pickle: - $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle - @echo - @echo "Build finished; now you can process the pickle files." - -json: - $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json - @echo - @echo "Build finished; now you can process the JSON files." - -htmlhelp: - $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp - @echo - @echo "Build finished; now you can run HTML Help Workshop with the" \ - ".hhp project file in $(BUILDDIR)/htmlhelp." - -qthelp: - $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp - @echo - @echo "Build finished; now you can run "qcollectiongenerator" with the" \ - ".qhcp project file in $(BUILDDIR)/qthelp, like this:" - @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/mail.qhcp" - @echo "To view the help file:" - @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/mail.qhc" - -devhelp: - $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp - @echo - @echo "Build finished." - @echo "To view the help file:" - @echo "# mkdir -p $$HOME/.local/share/devhelp/mail" - @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/mail" - @echo "# devhelp" - -epub: - $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub - @echo - @echo "Build finished. The epub file is in $(BUILDDIR)/epub." - -latex: - $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex - @echo - @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." - @echo "Run \`make' in that directory to run these through (pdf)latex" \ - "(use \`make latexpdf' here to do that automatically)." - -latexpdf: - $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex - @echo "Running LaTeX files through pdflatex..." - $(MAKE) -C $(BUILDDIR)/latex all-pdf - @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." - -latexpdfja: - $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex - @echo "Running LaTeX files through platex and dvipdfmx..." - $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja - @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." - -text: - $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text - @echo - @echo "Build finished. The text files are in $(BUILDDIR)/text." - -man: - $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man - @echo - @echo "Build finished. The manual pages are in $(BUILDDIR)/man." - -texinfo: - $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo - @echo - @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." - @echo "Run \`make' in that directory to run these through makeinfo" \ - "(use \`make info' here to do that automatically)." - -info: - $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo - @echo "Running Texinfo files through makeinfo..." - make -C $(BUILDDIR)/texinfo info - @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." - -gettext: - $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale - @echo - @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." - -changes: - $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes - @echo - @echo "The overview file is in $(BUILDDIR)/changes." - -linkcheck: - $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck - @echo - @echo "Link check complete; look for any errors in the above output " \ - "or in $(BUILDDIR)/linkcheck/output.txt." - -doctest: - $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest - @echo "Testing of doctests in the sources finished, look at the " \ - "results in $(BUILDDIR)/doctest/output.txt." - -xml: - $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml - @echo - @echo "Build finished. The XML files are in $(BUILDDIR)/xml." - -pseudoxml: - $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml - @echo - @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." diff --git a/docs/api/conf.py b/docs/api/conf.py deleted file mode 100644 index 2199c2f..0000000 --- a/docs/api/conf.py +++ /dev/null @@ -1,331 +0,0 @@ -# -*- coding: utf-8 -*- -# -# mail documentation build configuration file, created by -# sphinx-quickstart on Mon Aug 25 19:47:12 2014. -# -# This file is execfile()d with the current directory set to its -# containing dir. -# -# Note that not all possible configuration values are present in this -# autogenerated file. -# -# All configuration values have a default; values that are commented out -# serve to show the default. - -import sys -import os - -# If extensions (or modules to document with autodoc) are in another directory, -# add these directories to sys.path here. If the directory is relative to the -# documentation root, use os.path.abspath to make it absolute, like shown here. -#sys.path.insert(0, os.path.abspath('.')) - -# -- General configuration ------------------------------------------------ - -# If your documentation needs a minimal Sphinx version, state it here. -#needs_sphinx = '1.0' - -# Add any Sphinx extension module names here, as strings. They can be -# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom -# ones. -extensions = [ - 'sphinx.ext.autodoc', - 'sphinx.ext.viewcode', -] - -# Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] - -# The suffix of source filenames. -source_suffix = '.rst' - -# The encoding of source files. -#source_encoding = 'utf-8-sig' - -# The master toctree document. -master_doc = 'index' - -# General information about the project. -project = u'mail' -copyright = u'2014, Author' - -# The version info for the project you're documenting, acts as replacement for -# |version| and |release|, also used in various other places throughout the -# built documents. -# -# The short X.Y version. -version = '' -# The full version, including alpha/beta/rc tags. -release = '' - -# The language for content autogenerated by Sphinx. Refer to documentation -# for a list of supported languages. -#language = None - -# There are two options for replacing |today|: either, you set today to some -# non-false value, then it is used: -#today = '' -# Else, today_fmt is used as the format for a strftime call. -#today_fmt = '%B %d, %Y' - -# List of patterns, relative to source directory, that match files and -# directories to ignore when looking for source files. -exclude_patterns = ['_build'] - -# The reST default role (used for this markup: `text`) to use for all -# documents. -#default_role = None - -# If true, '()' will be appended to :func: etc. cross-reference text. -#add_function_parentheses = True - -# If true, the current module name will be prepended to all description -# unit titles (such as .. function::). -#add_module_names = True - -# If true, sectionauthor and moduleauthor directives will be shown in the -# output. They are ignored by default. -#show_authors = False - -# The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' - -# A list of ignored prefixes for module index sorting. -#modindex_common_prefix = [] - -# If true, keep warnings as "system message" paragraphs in the built documents. -#keep_warnings = False - - -# -- Options for HTML output ---------------------------------------------- - -# The theme to use for HTML and HTML Help pages. See the documentation for -# a list of builtin themes. -html_theme = 'default' - -# Theme options are theme-specific and customize the look and feel of a theme -# further. For a list of options available for each theme, see the -# documentation. -#html_theme_options = {} - -# Add any paths that contain custom themes here, relative to this directory. -#html_theme_path = [] - -# The name for this set of Sphinx documents. If None, it defaults to -# " v documentation". -#html_title = None - -# A shorter title for the navigation bar. Default is the same as html_title. -#html_short_title = None - -# The name of an image file (relative to this directory) to place at the top -# of the sidebar. -#html_logo = None - -# The name of an image file (within the static path) to use as favicon of the -# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 -# pixels large. -#html_favicon = None - -# Add any paths that contain custom static files (such as style sheets) here, -# relative to this directory. They are copied after the builtin static files, -# so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] - -# Add any extra paths that contain custom files (such as robots.txt or -# .htaccess) here, relative to this directory. These files are copied -# directly to the root of the documentation. -#html_extra_path = [] - -# If not '', a 'Last updated on:' timestamp is inserted at every page bottom, -# using the given strftime format. -#html_last_updated_fmt = '%b %d, %Y' - -# If true, SmartyPants will be used to convert quotes and dashes to -# typographically correct entities. -#html_use_smartypants = True - -# Custom sidebar templates, maps document names to template names. -#html_sidebars = {} - -# Additional templates that should be rendered to pages, maps page names to -# template names. -#html_additional_pages = {} - -# If false, no module index is generated. -#html_domain_indices = True - -# If false, no index is generated. -#html_use_index = True - -# If true, the index is split into individual pages for each letter. -#html_split_index = False - -# If true, links to the reST sources are added to the pages. -#html_show_sourcelink = True - -# If true, "Created using Sphinx" is shown in the HTML footer. Default is True. -#html_show_sphinx = True - -# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. -#html_show_copyright = True - -# If true, an OpenSearch description file will be output, and all pages will -# contain a tag referring to it. The value of this option must be the -# base URL from which the finished HTML is served. -#html_use_opensearch = '' - -# This is the file name suffix for HTML files (e.g. ".xhtml"). -#html_file_suffix = None - -# Output file base name for HTML help builder. -htmlhelp_basename = 'maildoc' - - -# -- Options for LaTeX output --------------------------------------------- - -latex_elements = { -# The paper size ('letterpaper' or 'a4paper'). -#'papersize': 'letterpaper', - -# The font size ('10pt', '11pt' or '12pt'). -#'pointsize': '10pt', - -# Additional stuff for the LaTeX preamble. -#'preamble': '', -} - -# Grouping the document tree into LaTeX files. List of tuples -# (source start file, target name, title, -# author, documentclass [howto, manual, or own class]). -latex_documents = [ - ('index', 'mail.tex', u'mail Documentation', - u'Author', 'manual'), -] - -# The name of an image file (relative to this directory) to place at the top of -# the title page. -#latex_logo = None - -# For "manual" documents, if this is true, then toplevel headings are parts, -# not chapters. -#latex_use_parts = False - -# If true, show page references after internal links. -#latex_show_pagerefs = False - -# If true, show URL addresses after external links. -#latex_show_urls = False - -# Documents to append as an appendix to all manuals. -#latex_appendices = [] - -# If false, no module index is generated. -#latex_domain_indices = True - - -# -- Options for manual page output --------------------------------------- - -# One entry per manual page. List of tuples -# (source start file, name, description, authors, manual section). -man_pages = [ - ('index', 'mail', u'mail Documentation', - [u'Author'], 1) -] - -# If true, show URL addresses after external links. -#man_show_urls = False - - -# -- Options for Texinfo output ------------------------------------------- - -# Grouping the document tree into Texinfo files. List of tuples -# (source start file, target name, title, author, -# dir menu entry, description, category) -texinfo_documents = [ - ('index', 'mail', u'mail Documentation', - u'Author', 'mail', 'One line description of project.', - 'Miscellaneous'), -] - -# Documents to append as an appendix to all manuals. -#texinfo_appendices = [] - -# If false, no module index is generated. -#texinfo_domain_indices = True - -# How to display URL addresses: 'footnote', 'no', or 'inline'. -#texinfo_show_urls = 'footnote' - -# If true, do not generate a @detailmenu in the "Top" node's menu. -#texinfo_no_detailmenu = False - - -# -- Options for Epub output ---------------------------------------------- - -# Bibliographic Dublin Core info. -epub_title = u'mail' -epub_author = u'Author' -epub_publisher = u'Author' -epub_copyright = u'2014, Author' - -# The basename for the epub file. It defaults to the project name. -#epub_basename = u'mail' - -# The HTML theme for the epub output. Since the default themes are not optimized -# for small screen space, using the same theme for HTML and epub output is -# usually not wise. This defaults to 'epub', a theme designed to save visual -# space. -#epub_theme = 'epub' - -# The language of the text. It defaults to the language option -# or en if the language is not set. -#epub_language = '' - -# The scheme of the identifier. Typical schemes are ISBN or URL. -#epub_scheme = '' - -# The unique identifier of the text. This can be a ISBN number -# or the project homepage. -#epub_identifier = '' - -# A unique identification for the text. -#epub_uid = '' - -# A tuple containing the cover image and cover page html template filenames. -#epub_cover = () - -# A sequence of (type, uri, title) tuples for the guide element of content.opf. -#epub_guide = () - -# HTML files that should be inserted before the pages created by sphinx. -# The format is a list of tuples containing the path and title. -#epub_pre_files = [] - -# HTML files shat should be inserted after the pages created by sphinx. -# The format is a list of tuples containing the path and title. -#epub_post_files = [] - -# A list of files that should not be packed into the epub file. -epub_exclude_files = ['search.html'] - -# The depth of the table of contents in toc.ncx. -#epub_tocdepth = 3 - -# Allow duplicate toc entries. -#epub_tocdup = True - -# Choose between 'default' and 'includehidden'. -#epub_tocscope = 'default' - -# Fix unsupported image types using the PIL. -#epub_fix_images = False - -# Scale large images. -#epub_max_image_width = 0 - -# How to display URL addresses: 'footnote', 'no', or 'inline'. -#epub_show_urls = 'inline' - -# If false, no index is generated. -#epub_use_index = True diff --git a/docs/api/index.rst b/docs/api/index.rst deleted file mode 100644 index f5531df..0000000 --- a/docs/api/index.rst +++ /dev/null @@ -1,23 +0,0 @@ -.. mail documentation master file, created by - sphinx-quickstart on Mon Aug 25 19:47:12 2014. - You can adapt this file completely to your liking, but it should at least - contain the root `toctree` directive. - -Welcome to mail's documentation! -================================ - -Contents: - -.. toctree:: - :maxdepth: 4 - - mail - - -Indices and tables -================== - -* :ref:`genindex` -* :ref:`modindex` -* :ref:`search` - diff --git a/docs/api/leap.mail.adaptors.rst b/docs/api/leap.mail.adaptors.rst new file mode 100644 index 0000000..472cade --- /dev/null +++ b/docs/api/leap.mail.adaptors.rst @@ -0,0 +1,43 @@ +mail.adaptors package +===================== + +.. automodule:: leap.mail.adaptors + :members: + :undoc-members: + :show-inheritance: + +Subpackages +----------- + +.. toctree:: + + leap.mail.adaptors.tests + +Submodules +---------- + +mail.adaptors.models module +--------------------------- + +.. automodule:: leap.mail.adaptors.models + :members: + :undoc-members: + :show-inheritance: + +mail.adaptors.soledad module +---------------------------- + +.. automodule:: leap.mail.adaptors.soledad + :members: + :undoc-members: + :show-inheritance: + +mail.adaptors.soledad_indexes module +------------------------------------ + +.. automodule:: leap.mail.adaptors.soledad_indexes + :members: + :undoc-members: + :show-inheritance: + + diff --git a/docs/api/leap.mail.adaptors.tests.rst b/docs/api/leap.mail.adaptors.tests.rst new file mode 100644 index 0000000..2ae76e8 --- /dev/null +++ b/docs/api/leap.mail.adaptors.tests.rst @@ -0,0 +1,28 @@ +mail.adaptors.tests package +=========================== + +.. automodule:: leap.mail.adaptors.tests + :members: + :undoc-members: + :show-inheritance: + +Submodules +---------- + +mail.adaptors.tests.test_models module +-------------------------------------- + +.. automodule:: leap.mail.adaptors.tests.test_models + :members: + :undoc-members: + :show-inheritance: + +mail.adaptors.tests.test_soledad_adaptor module +----------------------------------------------- + +.. automodule:: leap.mail.adaptors.tests.test_soledad_adaptor + :members: + :undoc-members: + :show-inheritance: + + diff --git a/docs/api/leap.mail.imap.rst b/docs/api/leap.mail.imap.rst new file mode 100644 index 0000000..bfaa3fd --- /dev/null +++ b/docs/api/leap.mail.imap.rst @@ -0,0 +1,52 @@ +leap.mail.imap package +======================= + +.. automodule:: leap.mail.imap + :members: + :undoc-members: + :show-inheritance: + +Subpackages +----------- + +.. toctree:: + + leap.mail.imap.service + leap.mail.imap.tests + +Submodules +---------- + +leap.mail.imap.account module +------------------------------ + +.. automodule:: leap.mail.imap.account + :members: + :undoc-members: + :show-inheritance: + +leap.mail.imap.mailbox module +------------------------------ + +.. automodule:: leap.mail.imap.mailbox + :members: + :undoc-members: + :show-inheritance: + +leap.mail.imap.messages module +------------------------------ + +.. automodule:: leap.mail.imap.messages + :members: + :undoc-members: + :show-inheritance: + +leap.mail.imap.server module +----------------------------- + +.. automodule:: leap.mail.imap.server + :members: + :undoc-members: + :show-inheritance: + + diff --git a/docs/api/leap.mail.imap.service.rst b/docs/api/leap.mail.imap.service.rst new file mode 100644 index 0000000..2f3ed4b --- /dev/null +++ b/docs/api/leap.mail.imap.service.rst @@ -0,0 +1,9 @@ +leap.mail.imap.service package +=============================== + +.. automodule:: leap.mail.imap.service + :members: + :undoc-members: + :show-inheritance: + + diff --git a/docs/api/leap.mail.incoming.rst b/docs/api/leap.mail.incoming.rst new file mode 100644 index 0000000..4bd1614 --- /dev/null +++ b/docs/api/leap.mail.incoming.rst @@ -0,0 +1,20 @@ +leap.mail.incoming package +========================== + +.. automodule:: leap.mail.incoming + :members: + :undoc-members: + :show-inheritance: + +Submodules +---------- + +leap.mail.incoming.service module +---------------------------------- + +.. automodule:: leap.mail.incoming.service + :members: + :undoc-members: + :show-inheritance: + + diff --git a/docs/api/leap.mail.outgoing.rst b/docs/api/leap.mail.outgoing.rst new file mode 100644 index 0000000..af8c173 --- /dev/null +++ b/docs/api/leap.mail.outgoing.rst @@ -0,0 +1,21 @@ +leap.mail.outgoing package +========================== + +.. automodule:: leap.mail.outgoing + :members: + :undoc-members: + :show-inheritance: + + +Submodules +---------- + +mail.outgoing.service module +---------------------------- + +.. automodule:: leap.mail.outgoing.service + :members: + :undoc-members: + :show-inheritance: + + diff --git a/docs/api/leap.mail.plugins.rst b/docs/api/leap.mail.plugins.rst new file mode 100644 index 0000000..7a5d6b4 --- /dev/null +++ b/docs/api/leap.mail.plugins.rst @@ -0,0 +1,20 @@ +leap.mail.plugins package +========================== + +.. automodule:: leap.mail.plugins + :members: + :undoc-members: + :show-inheritance: + +Submodules +---------- + +leap.mail.plugins.soledad_sync_hooks module +------------------------------------------- + +.. automodule:: leap.mail.plugins.soledad_sync_hooks + :members: + :undoc-members: + :show-inheritance: + + diff --git a/docs/api/leap.mail.rst b/docs/api/leap.mail.rst new file mode 100644 index 0000000..686e648 --- /dev/null +++ b/docs/api/leap.mail.rst @@ -0,0 +1,105 @@ +leap.mail package +================= + +.. automodule:: leap.mail + :members: + :undoc-members: + :show-inheritance: + +Subpackages +----------- + +.. toctree:: + + leap.mail.adaptors + leap.mail.imap + leap.mail.incoming + leap.mail.outgoing + leap.mail.plugins + leap.mail.smtp + leap.mail.tests + +Submodules +---------- + +leap.mail.constants module +--------------------------- + +.. automodule:: leap.mail.constants + :members: + :undoc-members: + :show-inheritance: + +leap.mail.decorators module +--------------------------- + +.. automodule:: leap.mail.decorators + :members: + :undoc-members: + :show-inheritance: + +leap.mail.interfaces module +---------------------------- + +.. automodule:: leap.mail.interfaces + :members: + :undoc-members: + :show-inheritance: + +leap.mail.load_tests module +---------------------------- + +.. automodule:: leap.mail.load_tests + :members: + :undoc-members: + :show-inheritance: + +leap.mail.mail module +--------------------- + +.. automodule:: leap.mail.mail + :members: + :undoc-members: + :show-inheritance: + +leap.mail.mailbox_indexer module +--------------------------------- + +.. automodule:: leap.mail.mailbox_indexer + :members: + :undoc-members: + :show-inheritance: + +leap.mail.size module +---------------------- + +.. automodule:: leap.mail.size + :members: + :undoc-members: + :show-inheritance: + +leap.mail.sync_hooks module +---------------------------- + +.. automodule:: leap.mail.sync_hooks + :members: + :undoc-members: + :show-inheritance: + +leap.mail.utils module +----------------------- + +.. automodule:: leap.mail.utils + :members: + :undoc-members: + :show-inheritance: + +leap.mail.walk module +---------------------- + +.. automodule:: leap.mail.walk + :members: + :undoc-members: + :show-inheritance: + + diff --git a/docs/api/leap.mail.smtp.rst b/docs/api/leap.mail.smtp.rst new file mode 100644 index 0000000..f35d3f9 --- /dev/null +++ b/docs/api/leap.mail.smtp.rst @@ -0,0 +1,19 @@ +leap.mail.smtp package +======================= + +.. automodule:: leap.mail.smtp + :members: + :undoc-members: + :show-inheritance: + +Submodules +---------- + +leap.mail.smtp.gateway module +----------------------------- + +.. automodule:: leap.mail.smtp.gateway + :members: + :undoc-members: + :show-inheritance: + diff --git a/docs/api/mail.imap.rst b/docs/api/mail.imap.rst deleted file mode 100644 index 051ded6..0000000 --- a/docs/api/mail.imap.rst +++ /dev/null @@ -1,118 +0,0 @@ -mail.imap package -================= - -Subpackages ------------ - -.. toctree:: - - mail.imap.service - mail.imap.tests - -Submodules ----------- - -mail.imap.account module ------------------------- - -.. automodule:: mail.imap.account - :members: - :undoc-members: - :show-inheritance: - -mail.imap.fetch module ----------------------- - -.. automodule:: mail.imap.fetch - :members: - :undoc-members: - :show-inheritance: - -mail.imap.fields module ------------------------ - -.. automodule:: mail.imap.fields - :members: - :undoc-members: - :show-inheritance: - -mail.imap.index module ----------------------- - -.. automodule:: mail.imap.index - :members: - :undoc-members: - :show-inheritance: - -mail.imap.interfaces module ---------------------------- - -.. automodule:: mail.imap.interfaces - :members: - :undoc-members: - :show-inheritance: - -mail.imap.mailbox module ------------------------- - -.. automodule:: mail.imap.mailbox - :members: - :undoc-members: - :show-inheritance: - -mail.imap.memorystore module ----------------------------- - -.. automodule:: mail.imap.memorystore - :members: - :undoc-members: - :show-inheritance: - -mail.imap.messageparts module ------------------------------ - -.. automodule:: mail.imap.messageparts - :members: - :undoc-members: - :show-inheritance: - -mail.imap.messages module -------------------------- - -.. automodule:: mail.imap.messages - :members: - :undoc-members: - :show-inheritance: - -mail.imap.parser module ------------------------ - -.. automodule:: mail.imap.parser - :members: - :undoc-members: - :show-inheritance: - -mail.imap.server module ------------------------ - -.. automodule:: mail.imap.server - :members: - :undoc-members: - :show-inheritance: - -mail.imap.soledadstore module ------------------------------ - -.. automodule:: mail.imap.soledadstore - :members: - :undoc-members: - :show-inheritance: - - -Module contents ---------------- - -.. automodule:: mail.imap - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/api/mail.imap.service.rst b/docs/api/mail.imap.service.rst deleted file mode 100644 index c288813..0000000 --- a/docs/api/mail.imap.service.rst +++ /dev/null @@ -1,30 +0,0 @@ -mail.imap.service package -========================= - -Submodules ----------- - -mail.imap.service.imap module ------------------------------ - -.. automodule:: mail.imap.service.imap - :members: - :undoc-members: - :show-inheritance: - -mail.imap.service.manhole module --------------------------------- - -.. automodule:: mail.imap.service.manhole - :members: - :undoc-members: - :show-inheritance: - - -Module contents ---------------- - -.. automodule:: mail.imap.service - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/api/mail.imap.tests.rst b/docs/api/mail.imap.tests.rst deleted file mode 100644 index b6717a3..0000000 --- a/docs/api/mail.imap.tests.rst +++ /dev/null @@ -1,38 +0,0 @@ -mail.imap.tests package -======================= - -Submodules ----------- - -mail.imap.tests.imapclient module ---------------------------------- - -.. automodule:: mail.imap.tests.imapclient - :members: - :undoc-members: - :show-inheritance: - -mail.imap.tests.test_imap module --------------------------------- - -.. automodule:: mail.imap.tests.test_imap - :members: - :undoc-members: - :show-inheritance: - -mail.imap.tests.walktree module -------------------------------- - -.. automodule:: mail.imap.tests.walktree - :members: - :undoc-members: - :show-inheritance: - - -Module contents ---------------- - -.. automodule:: mail.imap.tests - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/api/mail.rst b/docs/api/mail.rst deleted file mode 100644 index 2713207..0000000 --- a/docs/api/mail.rst +++ /dev/null @@ -1,70 +0,0 @@ -mail package -============ - -Subpackages ------------ - -.. toctree:: - - mail.imap - mail.smtp - -Submodules ----------- - -mail.decorators module ----------------------- - -.. automodule:: mail.decorators - :members: - :undoc-members: - :show-inheritance: - -mail.load_tests module ----------------------- - -.. automodule:: mail.load_tests - :members: - :undoc-members: - :show-inheritance: - -mail.messageflow module ------------------------ - -.. automodule:: mail.messageflow - :members: - :undoc-members: - :show-inheritance: - -mail.size module ----------------- - -.. automodule:: mail.size - :members: - :undoc-members: - :show-inheritance: - -mail.utils module ------------------ - -.. automodule:: mail.utils - :members: - :undoc-members: - :show-inheritance: - -mail.walk module ----------------- - -.. automodule:: mail.walk - :members: - :undoc-members: - :show-inheritance: - - -Module contents ---------------- - -.. automodule:: mail - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/api/mail.smtp.rst b/docs/api/mail.smtp.rst deleted file mode 100644 index da67279..0000000 --- a/docs/api/mail.smtp.rst +++ /dev/null @@ -1,37 +0,0 @@ -mail.smtp package -================= - -Subpackages ------------ - -.. toctree:: - - mail.smtp.tests - -Submodules ----------- - -mail.smtp.gateway module ------------------------- - -.. automodule:: mail.smtp.gateway - :members: - :undoc-members: - :show-inheritance: - -mail.smtp.rfc3156 module ------------------------- - -.. automodule:: mail.smtp.rfc3156 - :members: - :undoc-members: - :show-inheritance: - - -Module contents ---------------- - -.. automodule:: mail.smtp - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/api/mail.smtp.tests.rst b/docs/api/mail.smtp.tests.rst deleted file mode 100644 index c313fb3..0000000 --- a/docs/api/mail.smtp.tests.rst +++ /dev/null @@ -1,22 +0,0 @@ -mail.smtp.tests package -======================= - -Submodules ----------- - -mail.smtp.tests.test_gateway module ------------------------------------ - -.. automodule:: mail.smtp.tests.test_gateway - :members: - :undoc-members: - :show-inheritance: - - -Module contents ---------------- - -.. automodule:: mail.smtp.tests - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/api/make.bat b/docs/api/make.bat deleted file mode 100644 index 63cd17f..0000000 --- a/docs/api/make.bat +++ /dev/null @@ -1,242 +0,0 @@ -@ECHO OFF - -REM Command file for Sphinx documentation - -if "%SPHINXBUILD%" == "" ( - set SPHINXBUILD=sphinx-build -) -set BUILDDIR=_build -set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . -set I18NSPHINXOPTS=%SPHINXOPTS% . -if NOT "%PAPER%" == "" ( - set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% - set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% -) - -if "%1" == "" goto help - -if "%1" == "help" ( - :help - echo.Please use `make ^` where ^ is one of - echo. html to make standalone HTML files - echo. dirhtml to make HTML files named index.html in directories - echo. singlehtml to make a single large HTML file - echo. pickle to make pickle files - echo. json to make JSON files - echo. htmlhelp to make HTML files and a HTML help project - echo. qthelp to make HTML files and a qthelp project - echo. devhelp to make HTML files and a Devhelp project - echo. epub to make an epub - echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter - echo. text to make text files - echo. man to make manual pages - echo. texinfo to make Texinfo files - echo. gettext to make PO message catalogs - echo. changes to make an overview over all changed/added/deprecated items - echo. xml to make Docutils-native XML files - echo. pseudoxml to make pseudoxml-XML files for display purposes - echo. linkcheck to check all external links for integrity - echo. doctest to run all doctests embedded in the documentation if enabled - goto end -) - -if "%1" == "clean" ( - for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i - del /q /s %BUILDDIR%\* - goto end -) - - -%SPHINXBUILD% 2> nul -if errorlevel 9009 ( - echo. - echo.The 'sphinx-build' command was not found. Make sure you have Sphinx - echo.installed, then set the SPHINXBUILD environment variable to point - echo.to the full path of the 'sphinx-build' executable. Alternatively you - echo.may add the Sphinx directory to PATH. - echo. - echo.If you don't have Sphinx installed, grab it from - echo.http://sphinx-doc.org/ - exit /b 1 -) - -if "%1" == "html" ( - %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The HTML pages are in %BUILDDIR%/html. - goto end -) - -if "%1" == "dirhtml" ( - %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. - goto end -) - -if "%1" == "singlehtml" ( - %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. - goto end -) - -if "%1" == "pickle" ( - %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle - if errorlevel 1 exit /b 1 - echo. - echo.Build finished; now you can process the pickle files. - goto end -) - -if "%1" == "json" ( - %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json - if errorlevel 1 exit /b 1 - echo. - echo.Build finished; now you can process the JSON files. - goto end -) - -if "%1" == "htmlhelp" ( - %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp - if errorlevel 1 exit /b 1 - echo. - echo.Build finished; now you can run HTML Help Workshop with the ^ -.hhp project file in %BUILDDIR%/htmlhelp. - goto end -) - -if "%1" == "qthelp" ( - %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp - if errorlevel 1 exit /b 1 - echo. - echo.Build finished; now you can run "qcollectiongenerator" with the ^ -.qhcp project file in %BUILDDIR%/qthelp, like this: - echo.^> qcollectiongenerator %BUILDDIR%\qthelp\mail.qhcp - echo.To view the help file: - echo.^> assistant -collectionFile %BUILDDIR%\qthelp\mail.ghc - goto end -) - -if "%1" == "devhelp" ( - %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. - goto end -) - -if "%1" == "epub" ( - %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The epub file is in %BUILDDIR%/epub. - goto end -) - -if "%1" == "latex" ( - %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex - if errorlevel 1 exit /b 1 - echo. - echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. - goto end -) - -if "%1" == "latexpdf" ( - %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex - cd %BUILDDIR%/latex - make all-pdf - cd %BUILDDIR%/.. - echo. - echo.Build finished; the PDF files are in %BUILDDIR%/latex. - goto end -) - -if "%1" == "latexpdfja" ( - %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex - cd %BUILDDIR%/latex - make all-pdf-ja - cd %BUILDDIR%/.. - echo. - echo.Build finished; the PDF files are in %BUILDDIR%/latex. - goto end -) - -if "%1" == "text" ( - %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The text files are in %BUILDDIR%/text. - goto end -) - -if "%1" == "man" ( - %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The manual pages are in %BUILDDIR%/man. - goto end -) - -if "%1" == "texinfo" ( - %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. - goto end -) - -if "%1" == "gettext" ( - %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The message catalogs are in %BUILDDIR%/locale. - goto end -) - -if "%1" == "changes" ( - %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes - if errorlevel 1 exit /b 1 - echo. - echo.The overview file is in %BUILDDIR%/changes. - goto end -) - -if "%1" == "linkcheck" ( - %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck - if errorlevel 1 exit /b 1 - echo. - echo.Link check complete; look for any errors in the above output ^ -or in %BUILDDIR%/linkcheck/output.txt. - goto end -) - -if "%1" == "doctest" ( - %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest - if errorlevel 1 exit /b 1 - echo. - echo.Testing of doctests in the sources finished, look at the ^ -results in %BUILDDIR%/doctest/output.txt. - goto end -) - -if "%1" == "xml" ( - %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The XML files are in %BUILDDIR%/xml. - goto end -) - -if "%1" == "pseudoxml" ( - %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. - goto end -) - -:end diff --git a/docs/conf.py b/docs/conf.py index 95d919b..746f57c 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -18,9 +18,16 @@ import os # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. -sys.path.insert(0, os.path.abspath('../src')) -sys.path.insert(0, os.path.abspath('../src/leap')) -sys.path.insert(0, os.path.abspath('../src/leap/mail')) +#sys.path.insert(0, os.path.abspath('../src')) +#sys.path.insert(0, os.path.abspath('../src/leap')) +#sys.path.insert(0, os.path.abspath('../src/leap/mail')) +#sys.path.insert(0, os.path.abspath('../../leap_common/src/leap/common')) +#sys.path.insert(0, os.path.abspath('../../soledad/client/src/leap/soledad/client')) + +VENV_PATH = os.environ.get('VIRTUAL_ENV') +if VENV_PATH: + sys.path.insert(0, os.path.abspath(VENV_PATH + 'lib/python2.7/site-packages')) + # -- General configuration ------------------------------------------------ @@ -57,7 +64,7 @@ copyright = u'2014-2015, The LEAP Encryption Access Project' # built documents. # # The short X.Y version. -version = '0.4.0alpha1' +version = '0.4.0rc2' # The full version, including alpha/beta/rc tags. release = '0.4.0' @@ -104,7 +111,7 @@ pygments_style = 'sphinx' # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. -html_theme = 'default' +html_theme = 'alabaster' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the diff --git a/docs/index.rst b/docs/index.rst index 8bacc51..a2133f4 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -3,21 +3,50 @@ You can adapt this file completely to your liking, but it should at least contain the root `toctree` directive. -Welcome to leap.mail's documentation! -===================================== +leap.mail +========= -This is the documentation for the ``leap.mail`` module. It is a twisted package -that exposes two services, ``smtp`` and ``imap``, that run local proxies and interact -with a remote ``LEAP`` provider that offers *a soledad syncronization endpoint* -and receive the outgoing email. +*decentralized and secure mail delivery and synchronization* + +This is the documentation for the ``leap.mail`` module. It is a `twisted`_ +package that allows to receive, process, send and access existing messages using +the `LEAP`_ platform. + +One way to use this library is to let it launch two standard mail services, +``smtp`` and ``imap``, that run as local proxies and interact with a remote +``LEAP`` provider that offers *a soledad syncronization endpoint* and receives +the outgoing email. This is what `Bitmask`_ client does. + +From the release 0.4.0 on, it's also possible to use a protocol-agnostic email +public API, so that third party mail clients can manipulate the data layer. This +is what the awesome MUA in the `Pixelated`_ project is using. + +.. _`twisted`: https://twistedmatrix.com/trac/ +.. _`LEAP`: https://leap.se/en/docs +.. _`Bitmask`: https://bitmask.net/en/features#email +.. _`Pixelated`: https://pixelated-project.org/ + +How does this all work? +----------------------- + +All the underlying data storage and sync is handled by a library called +`soledad`_, which handles encryption, storage and sync. Based on `u1db`_, +documents are stored locally as local ``sqlcipher`` tables, and syncs against +the soledad sync service in the provider. + +OpenPGP key generation and keyring management are handled by another leap +python library: `keymanager`_. See :ref:`the life cycle of a leap email ` for an overview of the life cycle of an email through ``LEAP`` providers. -``Soledad`` stores its documents as local ``sqlcipher`` tables, and syncs -against the soledad sync service in the provider. +.. _`Soledad`: https://leap.se/en/docs/design/soledad +.. _`u1db`: https://en.wikipedia.org/wiki/U1DB +.. _`keymanager`: https://github.com/leapcode/keymanager/ +Data model +---------- .. TODO clear document types documentation. The data model at the present moment consists of several *document types* that split email into @@ -25,13 +54,8 @@ different documents that are stored in ``Soledad``. The idea behind this is to keep clear the separation between *mutable* and *inmutable* parts, and still being able to reconstruct arbitrarily nested email structures easily. -In the coming releases we are going to be working towards the goal of exposing -a protocol-agnostic email public API, so that third party mail clients can -manipulate the data layer without having to resort to handling the sql tables or -doing direct u1db calls. The code will be transitioning towards a LEAPMail -public API that we can stabilize as soon as possible, and leaving the IMAP -server as another code entity that uses this lower layer. - +Documentation index +=================== .. .. Contents: @@ -48,14 +72,18 @@ API documentation ----------------- If you were looking for the documentation of the ``leap.mail`` module, you will -find it here. Beware that the public API will still be unstable for the next -development cycles. +find it here. + +Of special interest is the `public mail api`_, which should remain relatively +stable across the next few releases. + +.. _`public mail api`: api/mail.html#module-mail + .. toctree:: :maxdepth: 2 - api/mail - + api/leap.mail diff --git a/docs/intro.rst b/docs/intro.rst deleted file mode 100644 index 6090a90..0000000 --- a/docs/intro.rst +++ /dev/null @@ -1,4 +0,0 @@ -Introduction -============ - -leap.mail intro diff --git a/docs/recreate_apidocs.sh b/docs/recreate_apidocs.sh new file mode 100755 index 0000000..9a79d09 --- /dev/null +++ b/docs/recreate_apidocs.sh @@ -0,0 +1,4 @@ +#!/bin/sh +# Watchout! this will need much manual touches +# to the generated apidocs. Mainly: s/mail/leap.mail/g +sphinx-apidoc -M -o api ../src/leap/mail -- cgit v1.2.3 From fcdc227c23e6958aab902c95aeab35285c9993f0 Mon Sep 17 00:00:00 2001 From: Kali Kaneko Date: Tue, 22 Sep 2015 17:38:48 -0400 Subject: [refactor] avoid circular import due to rfc3156 --- src/leap/mail/imap/mailbox.py | 2 + src/leap/mail/incoming/tests/test_incoming_mail.py | 2 +- src/leap/mail/mail.py | 12 +- src/leap/mail/mailbox_indexer.py | 5 +- src/leap/mail/outgoing/service.py | 12 +- src/leap/mail/outgoing/tests/test_outgoing.py | 2 +- src/leap/mail/rfc3156.py | 390 +++++++++++++++++++++ src/leap/mail/smtp/gateway.py | 5 +- src/leap/mail/smtp/rfc3156.py | 390 --------------------- 9 files changed, 415 insertions(+), 405 deletions(-) create mode 100644 src/leap/mail/rfc3156.py delete mode 100644 src/leap/mail/smtp/rfc3156.py diff --git a/src/leap/mail/imap/mailbox.py b/src/leap/mail/imap/mailbox.py index e73994b..c7accbb 100644 --- a/src/leap/mail/imap/mailbox.py +++ b/src/leap/mail/imap/mailbox.py @@ -215,11 +215,13 @@ class IMAPMailbox(object): but in the future will be useful to get absolute UIDs from message sequence numbers. + :param message: the message sequence number. :type message: int :rtype: int :return: the UID of the message. + """ # TODO support relative sequences. The (imap) message should # receive a sequence number attribute: a deferred is not expected diff --git a/src/leap/mail/incoming/tests/test_incoming_mail.py b/src/leap/mail/incoming/tests/test_incoming_mail.py index 964c8fd..6880496 100644 --- a/src/leap/mail/incoming/tests/test_incoming_mail.py +++ b/src/leap/mail/incoming/tests/test_incoming_mail.py @@ -36,7 +36,7 @@ from leap.mail.adaptors import soledad_indexes as fields from leap.mail.constants import INBOX_NAME from leap.mail.imap.account import IMAPAccount from leap.mail.incoming.service import IncomingMail -from leap.mail.smtp.rfc3156 import MultipartEncrypted, PGPEncrypted +from leap.mail.rfc3156 import MultipartEncrypted, PGPEncrypted from leap.mail.tests import ( TestCaseWithKeyManager, ADDRESS, diff --git a/src/leap/mail/mail.py b/src/leap/mail/mail.py index fc5abd2..c0e16a6 100644 --- a/src/leap/mail/mail.py +++ b/src/leap/mail/mail.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # mail.py -# Copyright (C) 2014 LEAP +# Copyright (C) 2014,2015 LEAP # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -15,7 +15,12 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . """ -Generic Access to Mail objects: Public LEAP Mail API. +Generic Access to Mail objects. + +This module holds the public LEAP Mail API, which should be viewed as the main +entry point for message and account manipulation, in a protocol-agnostic way. + +In the future, pluggable transports will expose this generic API. """ import itertools import uuid @@ -148,6 +153,9 @@ class MessagePart(object): # TODO This class should be better abstracted from the data model. # TODO support arbitrarily nested multiparts (right now we only support # the trivial case) + """ + Represents a part of a multipart MIME Message. + """ def __init__(self, part_map, cdocs={}, nested=False): """ diff --git a/src/leap/mail/mailbox_indexer.py b/src/leap/mail/mailbox_indexer.py index 08e5f10..c49f808 100644 --- a/src/leap/mail/mailbox_indexer.py +++ b/src/leap/mail/mailbox_indexer.py @@ -15,6 +15,8 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . """ +.. :py:module::mailbox_indexer + Local tables to store the message Unique Identifiers for a given mailbox. """ import re @@ -72,10 +74,11 @@ class MailboxIndexer(object): These indexes are Message Attributes needed for the IMAP specification (rfc 3501), although they can be useful for other non-imap store implementations. + """ # The uids are expected to be 32-bits values, but the ROWIDs in sqlite # are 64-bit values. I *don't* think it really matters for any - # practical use, but it's good to remmeber we've got that difference going + # practical use, but it's good to remember we've got that difference going # on. store = None diff --git a/src/leap/mail/outgoing/service.py b/src/leap/mail/outgoing/service.py index 3754650..7cc5a24 100644 --- a/src/leap/mail/outgoing/service.py +++ b/src/leap/mail/outgoing/service.py @@ -36,12 +36,12 @@ from leap.keymanager.openpgp import OpenPGPKey from leap.keymanager.errors import KeyNotFound, KeyAddressMismatch from leap.mail import __version__ from leap.mail.utils import validate_address -from leap.mail.smtp.rfc3156 import MultipartEncrypted -from leap.mail.smtp.rfc3156 import MultipartSigned -from leap.mail.smtp.rfc3156 import encode_base64_rec -from leap.mail.smtp.rfc3156 import RFC3156CompliantGenerator -from leap.mail.smtp.rfc3156 import PGPSignature -from leap.mail.smtp.rfc3156 import PGPEncrypted +from leap.mail.rfc3156 import MultipartEncrypted +from leap.mail.rfc3156 import MultipartSigned +from leap.mail.rfc3156 import encode_base64_rec +from leap.mail.rfc3156 import RFC3156CompliantGenerator +from leap.mail.rfc3156 import PGPSignature +from leap.mail.rfc3156 import PGPEncrypted # TODO # [ ] rename this module to something else, service should be the implementor diff --git a/src/leap/mail/outgoing/tests/test_outgoing.py b/src/leap/mail/outgoing/tests/test_outgoing.py index 2376da9..5518b33 100644 --- a/src/leap/mail/outgoing/tests/test_outgoing.py +++ b/src/leap/mail/outgoing/tests/test_outgoing.py @@ -30,7 +30,7 @@ from twisted.mail.smtp import User from mock import Mock from leap.mail.smtp.gateway import SMTPFactory -from leap.mail.smtp.rfc3156 import RFC3156CompliantGenerator +from leap.mail.rfc3156 import RFC3156CompliantGenerator from leap.mail.outgoing.service import OutgoingMail from leap.mail.tests import ( TestCaseWithKeyManager, diff --git a/src/leap/mail/rfc3156.py b/src/leap/mail/rfc3156.py new file mode 100644 index 0000000..7d7bc0f --- /dev/null +++ b/src/leap/mail/rfc3156.py @@ -0,0 +1,390 @@ +# -*- coding: utf-8 -*- +# rfc3156.py +# Copyright (C) 2013 LEAP +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +""" +Implements RFC 3156: MIME Security with OpenPGP. +""" + +import base64 +from StringIO import StringIO + +from twisted.python import log +from email.mime.application import MIMEApplication +from email.mime.multipart import MIMEMultipart +from email import errors +from email.generator import ( + Generator, + fcre, + NL, + _make_boundary, +) + + +# +# A generator that solves http://bugs.python.org/issue14983 +# + +class RFC3156CompliantGenerator(Generator): + """ + An email generator that addresses Python's issue #14983 for multipart + messages. + + This is just a copy of email.generator.Generator which fixes the following + bug: http://bugs.python.org/issue14983 + """ + + def _handle_multipart(self, msg): + """ + A multipart handling implementation that addresses issue #14983. + + This is just a copy of the parent's method which fixes the following + bug: http://bugs.python.org/issue14983 (see the line marked with + "(***)"). + + :param msg: The multipart message to be handled. + :type msg: email.message.Message + """ + # The trick here is to write out each part separately, merge them all + # together, and then make sure that the boundary we've chosen isn't + # present in the payload. + msgtexts = [] + subparts = msg.get_payload() + if subparts is None: + subparts = [] + elif isinstance(subparts, basestring): + # e.g. a non-strict parse of a message with no starting boundary. + self._fp.write(subparts) + return + elif not isinstance(subparts, list): + # Scalar payload + subparts = [subparts] + for part in subparts: + s = StringIO() + g = self.clone(s) + g.flatten(part, unixfrom=False) + msgtexts.append(s.getvalue()) + # BAW: What about boundaries that are wrapped in double-quotes? + boundary = msg.get_boundary() + if not boundary: + # Create a boundary that doesn't appear in any of the + # message texts. + alltext = NL.join(msgtexts) + boundary = _make_boundary(alltext) + msg.set_boundary(boundary) + # If there's a preamble, write it out, with a trailing CRLF + if msg.preamble is not None: + preamble = msg.preamble + if self._mangle_from_: + preamble = fcre.sub('>From ', msg.preamble) + self._fp.write(preamble + '\n') + # dash-boundary transport-padding CRLF + self._fp.write('--' + boundary + '\n') + # body-part + if msgtexts: + self._fp.write(msgtexts.pop(0)) + # *encapsulation + # --> delimiter transport-padding + # --> CRLF body-part + for body_part in msgtexts: + # delimiter transport-padding CRLF + self._fp.write('\n--' + boundary + '\n') + # body-part + self._fp.write(body_part) + # close-delimiter transport-padding + self._fp.write('\n--' + boundary + '--' + '\n') # (***) Solve #14983 + if msg.epilogue is not None: + self._fp.write('\n') + epilogue = msg.epilogue + if self._mangle_from_: + epilogue = fcre.sub('>From ', msg.epilogue) + self._fp.write(epilogue) + + +# +# Base64 encoding: these are almost the same as python's email.encoder +# solution, but a bit modified. +# + +def _bencode(s): + """ + Encode C{s} in base64. + + :param s: The string to be encoded. + :type s: str + """ + # We can't quite use base64.encodestring() since it tacks on a "courtesy + # newline". Blech! + if not s: + return s + value = base64.encodestring(s) + return value[:-1] + + +def encode_base64(msg): + """ + Encode a non-multipart message's payload in Base64 (in place). + + This method modifies the message contents in place and adds or replaces an + appropriate Content-Transfer-Encoding header. + + :param msg: The non-multipart message to be encoded. + :type msg: email.message.Message + """ + encoding = msg.get('Content-Transfer-Encoding', None) + if encoding is not None: + encoding = encoding.lower() + # XXX Python's email module can only decode quoted-printable, base64 and + # uuencoded data, so we might have to implement other decoding schemes in + # order to support RFC 3156 properly and correctly calculate signatures + # for multipart attachments (eg. 7bit or 8bit encoded attachments). For + # now, if content is already encoded as base64 or if it is encoded with + # some unknown encoding, we just pass. + if encoding in [None, 'quoted-printable', 'x-uuencode', 'uue', 'x-uue']: + orig = msg.get_payload(decode=True) + encdata = _bencode(orig) + msg.set_payload(encdata) + # replace or set the Content-Transfer-Encoding header. + try: + msg.replace_header('Content-Transfer-Encoding', 'base64') + except KeyError: + msg['Content-Transfer-Encoding'] = 'base64' + elif encoding is not 'base64': + log.err('Unknown content-transfer-encoding: %s' % encoding) + + +def encode_base64_rec(msg): + """ + Encode (possibly multipart) messages in base64 (in place). + + This method modifies the message contents in place. + + :param msg: The non-multipart message to be encoded. + :type msg: email.message.Message + """ + if not msg.is_multipart(): + encode_base64(msg) + else: + for sub in msg.get_payload(): + encode_base64_rec(sub) + + +# +# RFC 1847: multipart/signed and multipart/encrypted +# + +class MultipartSigned(MIMEMultipart): + """ + Multipart/Signed MIME message according to RFC 1847. + + 2.1. Definition of Multipart/Signed + + (1) MIME type name: multipart + (2) MIME subtype name: signed + (3) Required parameters: boundary, protocol, and micalg + (4) Optional parameters: none + (5) Security considerations: Must be treated as opaque while in + transit + + The multipart/signed content type contains exactly two body parts. + The first body part is the body part over which the digital signature + was created, including its MIME headers. The second body part + contains the control information necessary to verify the digital + signature. The first body part may contain any valid MIME content + type, labeled accordingly. The second body part is labeled according + to the value of the protocol parameter. + + When the OpenPGP digital signature is generated: + + (1) The data to be signed MUST first be converted to its content- + type specific canonical form. For text/plain, this means + conversion to an appropriate character set and conversion of + line endings to the canonical sequence. + + (2) An appropriate Content-Transfer-Encoding is then applied; see + section 3. In particular, line endings in the encoded data + MUST use the canonical sequence where appropriate + (note that the canonical line ending may or may not be present + on the last line of encoded data and MUST NOT be included in + the signature if absent). + + (3) MIME content headers are then added to the body, each ending + with the canonical sequence. + + (4) As described in section 3 of this document, any trailing + whitespace MUST then be removed from the signed material. + + (5) As described in [2], the digital signature MUST be calculated + over both the data to be signed and its set of content headers. + + (6) The signature MUST be generated detached from the signed data + so that the process does not alter the signed data in any way. + """ + + def __init__(self, protocol, micalg, boundary=None, _subparts=None): + """ + Initialize the multipart/signed message. + + :param boundary: the multipart boundary string. By default it is + calculated as needed. + :type boundary: str + :param _subparts: a sequence of initial subparts for the payload. It + must be an iterable object, such as a list. You can always + attach new subparts to the message by using the attach() method. + :type _subparts: iterable + """ + MIMEMultipart.__init__( + self, _subtype='signed', boundary=boundary, + _subparts=_subparts) + self.set_param('protocol', protocol) + self.set_param('micalg', micalg) + + def attach(self, payload): + """ + Add the C{payload} to the current payload list. + + Also prevent from adding payloads with wrong Content-Type and from + exceeding a maximum of 2 payloads. + + :param payload: The payload to be attached. + :type payload: email.message.Message + """ + # second payload's content type must be equal to the protocol + # parameter given on object creation + if len(self.get_payload()) == 1: + if payload.get_content_type() != self.get_param('protocol'): + raise errors.MultipartConversionError( + 'Wrong content type %s.' % payload.get_content_type) + # prevent from adding more payloads + if len(self._payload) == 2: + raise errors.MultipartConversionError( + 'Cannot have more than two subparts.') + MIMEMultipart.attach(self, payload) + + +class MultipartEncrypted(MIMEMultipart): + """ + Multipart/encrypted MIME message according to RFC 1847. + + 2.2. Definition of Multipart/Encrypted + + (1) MIME type name: multipart + (2) MIME subtype name: encrypted + (3) Required parameters: boundary, protocol + (4) Optional parameters: none + (5) Security considerations: none + + The multipart/encrypted content type contains exactly two body parts. + The first body part contains the control information necessary to + decrypt the data in the second body part and is labeled according to + the value of the protocol parameter. The second body part contains + the data which was encrypted and is always labeled + application/octet-stream. + """ + + def __init__(self, protocol, boundary=None, _subparts=None): + """ + :param protocol: The encryption protocol to be added as a parameter to + the Content-Type header. + :type protocol: str + :param boundary: the multipart boundary string. By default it is + calculated as needed. + :type boundary: str + :param _subparts: a sequence of initial subparts for the payload. It + must be an iterable object, such as a list. You can always + attach new subparts to the message by using the attach() method. + :type _subparts: iterable + """ + MIMEMultipart.__init__( + self, _subtype='encrypted', boundary=boundary, + _subparts=_subparts) + self.set_param('protocol', protocol) + + def attach(self, payload): + """ + Add the C{payload} to the current payload list. + + Also prevent from adding payloads with wrong Content-Type and from + exceeding a maximum of 2 payloads. + + :param payload: The payload to be attached. + :type payload: email.message.Message + """ + # first payload's content type must be equal to the protocol parameter + # given on object creation + if len(self._payload) == 0: + if payload.get_content_type() != self.get_param('protocol'): + raise errors.MultipartConversionError( + 'Wrong content type.') + # second payload is always application/octet-stream + if len(self._payload) == 1: + if payload.get_content_type() != 'application/octet-stream': + raise errors.MultipartConversionError( + 'Wrong content type %s.' % payload.get_content_type) + # prevent from adding more payloads + if len(self._payload) == 2: + raise errors.MultipartConversionError( + 'Cannot have more than two subparts.') + MIMEMultipart.attach(self, payload) + + +# +# RFC 3156: application/pgp-encrypted, application/pgp-signed and +# application-pgp-signature. +# + +class PGPEncrypted(MIMEApplication): + """ + Application/pgp-encrypted MIME media type according to RFC 3156. + + * MIME media type name: application + * MIME subtype name: pgp-encrypted + * Required parameters: none + * Optional parameters: none + """ + + def __init__(self, version=1): + data = "Version: %d" % version + MIMEApplication.__init__(self, data, 'pgp-encrypted') + + +class PGPSignature(MIMEApplication): + """ + Application/pgp-signature MIME media type according to RFC 3156. + + * MIME media type name: application + * MIME subtype name: pgp-signature + * Required parameters: none + * Optional parameters: none + """ + def __init__(self, _data, name='signature.asc'): + MIMEApplication.__init__(self, _data, 'pgp-signature', + _encoder=lambda x: x, name=name) + self.add_header('Content-Description', 'OpenPGP Digital Signature') + + +class PGPKeys(MIMEApplication): + """ + Application/pgp-keys MIME media type according to RFC 3156. + + * MIME media type name: application + * MIME subtype name: pgp-keys + * Required parameters: none + * Optional parameters: none + """ + + def __init__(self, _data): + MIMEApplication.__init__(self, _data, 'pgp-keys') diff --git a/src/leap/mail/smtp/gateway.py b/src/leap/mail/smtp/gateway.py index c988367..45560bf 100644 --- a/src/leap/mail/smtp/gateway.py +++ b/src/leap/mail/smtp/gateway.py @@ -41,10 +41,7 @@ from leap.common.events import emit_async, catalog from leap.keymanager.openpgp import OpenPGPKey from leap.keymanager.errors import KeyNotFound from leap.mail.utils import validate_address - -from leap.mail.smtp.rfc3156 import ( - RFC3156CompliantGenerator, -) +from leap.mail.rfc3156 import RFC3156CompliantGenerator # replace email generator with a RFC 3156 compliant one. from email import generator diff --git a/src/leap/mail/smtp/rfc3156.py b/src/leap/mail/smtp/rfc3156.py deleted file mode 100644 index 7d7bc0f..0000000 --- a/src/leap/mail/smtp/rfc3156.py +++ /dev/null @@ -1,390 +0,0 @@ -# -*- coding: utf-8 -*- -# rfc3156.py -# Copyright (C) 2013 LEAP -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program 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 General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - -""" -Implements RFC 3156: MIME Security with OpenPGP. -""" - -import base64 -from StringIO import StringIO - -from twisted.python import log -from email.mime.application import MIMEApplication -from email.mime.multipart import MIMEMultipart -from email import errors -from email.generator import ( - Generator, - fcre, - NL, - _make_boundary, -) - - -# -# A generator that solves http://bugs.python.org/issue14983 -# - -class RFC3156CompliantGenerator(Generator): - """ - An email generator that addresses Python's issue #14983 for multipart - messages. - - This is just a copy of email.generator.Generator which fixes the following - bug: http://bugs.python.org/issue14983 - """ - - def _handle_multipart(self, msg): - """ - A multipart handling implementation that addresses issue #14983. - - This is just a copy of the parent's method which fixes the following - bug: http://bugs.python.org/issue14983 (see the line marked with - "(***)"). - - :param msg: The multipart message to be handled. - :type msg: email.message.Message - """ - # The trick here is to write out each part separately, merge them all - # together, and then make sure that the boundary we've chosen isn't - # present in the payload. - msgtexts = [] - subparts = msg.get_payload() - if subparts is None: - subparts = [] - elif isinstance(subparts, basestring): - # e.g. a non-strict parse of a message with no starting boundary. - self._fp.write(subparts) - return - elif not isinstance(subparts, list): - # Scalar payload - subparts = [subparts] - for part in subparts: - s = StringIO() - g = self.clone(s) - g.flatten(part, unixfrom=False) - msgtexts.append(s.getvalue()) - # BAW: What about boundaries that are wrapped in double-quotes? - boundary = msg.get_boundary() - if not boundary: - # Create a boundary that doesn't appear in any of the - # message texts. - alltext = NL.join(msgtexts) - boundary = _make_boundary(alltext) - msg.set_boundary(boundary) - # If there's a preamble, write it out, with a trailing CRLF - if msg.preamble is not None: - preamble = msg.preamble - if self._mangle_from_: - preamble = fcre.sub('>From ', msg.preamble) - self._fp.write(preamble + '\n') - # dash-boundary transport-padding CRLF - self._fp.write('--' + boundary + '\n') - # body-part - if msgtexts: - self._fp.write(msgtexts.pop(0)) - # *encapsulation - # --> delimiter transport-padding - # --> CRLF body-part - for body_part in msgtexts: - # delimiter transport-padding CRLF - self._fp.write('\n--' + boundary + '\n') - # body-part - self._fp.write(body_part) - # close-delimiter transport-padding - self._fp.write('\n--' + boundary + '--' + '\n') # (***) Solve #14983 - if msg.epilogue is not None: - self._fp.write('\n') - epilogue = msg.epilogue - if self._mangle_from_: - epilogue = fcre.sub('>From ', msg.epilogue) - self._fp.write(epilogue) - - -# -# Base64 encoding: these are almost the same as python's email.encoder -# solution, but a bit modified. -# - -def _bencode(s): - """ - Encode C{s} in base64. - - :param s: The string to be encoded. - :type s: str - """ - # We can't quite use base64.encodestring() since it tacks on a "courtesy - # newline". Blech! - if not s: - return s - value = base64.encodestring(s) - return value[:-1] - - -def encode_base64(msg): - """ - Encode a non-multipart message's payload in Base64 (in place). - - This method modifies the message contents in place and adds or replaces an - appropriate Content-Transfer-Encoding header. - - :param msg: The non-multipart message to be encoded. - :type msg: email.message.Message - """ - encoding = msg.get('Content-Transfer-Encoding', None) - if encoding is not None: - encoding = encoding.lower() - # XXX Python's email module can only decode quoted-printable, base64 and - # uuencoded data, so we might have to implement other decoding schemes in - # order to support RFC 3156 properly and correctly calculate signatures - # for multipart attachments (eg. 7bit or 8bit encoded attachments). For - # now, if content is already encoded as base64 or if it is encoded with - # some unknown encoding, we just pass. - if encoding in [None, 'quoted-printable', 'x-uuencode', 'uue', 'x-uue']: - orig = msg.get_payload(decode=True) - encdata = _bencode(orig) - msg.set_payload(encdata) - # replace or set the Content-Transfer-Encoding header. - try: - msg.replace_header('Content-Transfer-Encoding', 'base64') - except KeyError: - msg['Content-Transfer-Encoding'] = 'base64' - elif encoding is not 'base64': - log.err('Unknown content-transfer-encoding: %s' % encoding) - - -def encode_base64_rec(msg): - """ - Encode (possibly multipart) messages in base64 (in place). - - This method modifies the message contents in place. - - :param msg: The non-multipart message to be encoded. - :type msg: email.message.Message - """ - if not msg.is_multipart(): - encode_base64(msg) - else: - for sub in msg.get_payload(): - encode_base64_rec(sub) - - -# -# RFC 1847: multipart/signed and multipart/encrypted -# - -class MultipartSigned(MIMEMultipart): - """ - Multipart/Signed MIME message according to RFC 1847. - - 2.1. Definition of Multipart/Signed - - (1) MIME type name: multipart - (2) MIME subtype name: signed - (3) Required parameters: boundary, protocol, and micalg - (4) Optional parameters: none - (5) Security considerations: Must be treated as opaque while in - transit - - The multipart/signed content type contains exactly two body parts. - The first body part is the body part over which the digital signature - was created, including its MIME headers. The second body part - contains the control information necessary to verify the digital - signature. The first body part may contain any valid MIME content - type, labeled accordingly. The second body part is labeled according - to the value of the protocol parameter. - - When the OpenPGP digital signature is generated: - - (1) The data to be signed MUST first be converted to its content- - type specific canonical form. For text/plain, this means - conversion to an appropriate character set and conversion of - line endings to the canonical sequence. - - (2) An appropriate Content-Transfer-Encoding is then applied; see - section 3. In particular, line endings in the encoded data - MUST use the canonical sequence where appropriate - (note that the canonical line ending may or may not be present - on the last line of encoded data and MUST NOT be included in - the signature if absent). - - (3) MIME content headers are then added to the body, each ending - with the canonical sequence. - - (4) As described in section 3 of this document, any trailing - whitespace MUST then be removed from the signed material. - - (5) As described in [2], the digital signature MUST be calculated - over both the data to be signed and its set of content headers. - - (6) The signature MUST be generated detached from the signed data - so that the process does not alter the signed data in any way. - """ - - def __init__(self, protocol, micalg, boundary=None, _subparts=None): - """ - Initialize the multipart/signed message. - - :param boundary: the multipart boundary string. By default it is - calculated as needed. - :type boundary: str - :param _subparts: a sequence of initial subparts for the payload. It - must be an iterable object, such as a list. You can always - attach new subparts to the message by using the attach() method. - :type _subparts: iterable - """ - MIMEMultipart.__init__( - self, _subtype='signed', boundary=boundary, - _subparts=_subparts) - self.set_param('protocol', protocol) - self.set_param('micalg', micalg) - - def attach(self, payload): - """ - Add the C{payload} to the current payload list. - - Also prevent from adding payloads with wrong Content-Type and from - exceeding a maximum of 2 payloads. - - :param payload: The payload to be attached. - :type payload: email.message.Message - """ - # second payload's content type must be equal to the protocol - # parameter given on object creation - if len(self.get_payload()) == 1: - if payload.get_content_type() != self.get_param('protocol'): - raise errors.MultipartConversionError( - 'Wrong content type %s.' % payload.get_content_type) - # prevent from adding more payloads - if len(self._payload) == 2: - raise errors.MultipartConversionError( - 'Cannot have more than two subparts.') - MIMEMultipart.attach(self, payload) - - -class MultipartEncrypted(MIMEMultipart): - """ - Multipart/encrypted MIME message according to RFC 1847. - - 2.2. Definition of Multipart/Encrypted - - (1) MIME type name: multipart - (2) MIME subtype name: encrypted - (3) Required parameters: boundary, protocol - (4) Optional parameters: none - (5) Security considerations: none - - The multipart/encrypted content type contains exactly two body parts. - The first body part contains the control information necessary to - decrypt the data in the second body part and is labeled according to - the value of the protocol parameter. The second body part contains - the data which was encrypted and is always labeled - application/octet-stream. - """ - - def __init__(self, protocol, boundary=None, _subparts=None): - """ - :param protocol: The encryption protocol to be added as a parameter to - the Content-Type header. - :type protocol: str - :param boundary: the multipart boundary string. By default it is - calculated as needed. - :type boundary: str - :param _subparts: a sequence of initial subparts for the payload. It - must be an iterable object, such as a list. You can always - attach new subparts to the message by using the attach() method. - :type _subparts: iterable - """ - MIMEMultipart.__init__( - self, _subtype='encrypted', boundary=boundary, - _subparts=_subparts) - self.set_param('protocol', protocol) - - def attach(self, payload): - """ - Add the C{payload} to the current payload list. - - Also prevent from adding payloads with wrong Content-Type and from - exceeding a maximum of 2 payloads. - - :param payload: The payload to be attached. - :type payload: email.message.Message - """ - # first payload's content type must be equal to the protocol parameter - # given on object creation - if len(self._payload) == 0: - if payload.get_content_type() != self.get_param('protocol'): - raise errors.MultipartConversionError( - 'Wrong content type.') - # second payload is always application/octet-stream - if len(self._payload) == 1: - if payload.get_content_type() != 'application/octet-stream': - raise errors.MultipartConversionError( - 'Wrong content type %s.' % payload.get_content_type) - # prevent from adding more payloads - if len(self._payload) == 2: - raise errors.MultipartConversionError( - 'Cannot have more than two subparts.') - MIMEMultipart.attach(self, payload) - - -# -# RFC 3156: application/pgp-encrypted, application/pgp-signed and -# application-pgp-signature. -# - -class PGPEncrypted(MIMEApplication): - """ - Application/pgp-encrypted MIME media type according to RFC 3156. - - * MIME media type name: application - * MIME subtype name: pgp-encrypted - * Required parameters: none - * Optional parameters: none - """ - - def __init__(self, version=1): - data = "Version: %d" % version - MIMEApplication.__init__(self, data, 'pgp-encrypted') - - -class PGPSignature(MIMEApplication): - """ - Application/pgp-signature MIME media type according to RFC 3156. - - * MIME media type name: application - * MIME subtype name: pgp-signature - * Required parameters: none - * Optional parameters: none - """ - def __init__(self, _data, name='signature.asc'): - MIMEApplication.__init__(self, _data, 'pgp-signature', - _encoder=lambda x: x, name=name) - self.add_header('Content-Description', 'OpenPGP Digital Signature') - - -class PGPKeys(MIMEApplication): - """ - Application/pgp-keys MIME media type according to RFC 3156. - - * MIME media type name: application - * MIME subtype name: pgp-keys - * Required parameters: none - * Optional parameters: none - """ - - def __init__(self, _data): - MIMEApplication.__init__(self, _data, 'pgp-keys') -- cgit v1.2.3 From 36ef9dc7f9894a2578e8697e4b20e7e808c63f5b Mon Sep 17 00:00:00 2001 From: Kali Kaneko Date: Tue, 22 Sep 2015 17:53:40 -0400 Subject: [docs] update interfaces documentation --- src/leap/mail/interfaces.py | 183 ++++++++++++++++++++++++++++++++++---------- 1 file changed, 141 insertions(+), 42 deletions(-) diff --git a/src/leap/mail/interfaces.py b/src/leap/mail/interfaces.py index 899400f..10f5123 100644 --- a/src/leap/mail/interfaces.py +++ b/src/leap/mail/interfaces.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # interfaces.py -# Copyright (C) 2014 LEAP +# Copyright (C) 2014,2015 LEAP # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -24,17 +24,71 @@ class IMessageWrapper(Interface): """ I know how to access the different parts into which a given message is splitted into. + + :ivar fdoc: dict with flag document. + :ivar hdoc: dict with flag document. + :ivar cdocs: dict with content-documents, one-indexed. """ fdoc = Attribute('A dictionaly-like containing the flags document ' '(mutable)') - hdoc = Attribute('A dictionary-like containing the headers docuemnt ' + hdoc = Attribute('A dictionary-like containing the headers document ' '(immutable)') cdocs = Attribute('A dictionary with the content-docs, one-indexed') + def create(self, store, notify_just_mdoc=False, pending_inserts_dict={}): + """ + Create the underlying wrapper. + """ + + def update(self, store): + """ + Update the only mutable parts, which are within the flags document. + """ + + def delete(self, store): + """ + Delete the parts for this wrapper that are not referenced from anywhere + else. + """ + + def copy(self, store, new_mbox_uuid): + """ + Return a copy of this IMessageWrapper in a new mailbox. + """ + + def set_mbox_uuid(self, mbox_uuid): + """ + Set the mailbox for this wrapper. + """ + + def set_flags(self, flags): + """ + """ + + def set_tags(self, tags): + """ + """ + + def set_date(self, date): + """ + """ + + def get_subpart_dict(self, index): + """ + :param index: the part to lookup, 1-indexed + """ + + def get_subpart_indexes(self): + """ + """ + + def get_body(self, store): + """ + """ + -# TODO [ ] Catch up with the actual implementation! -# Lot of stuff added there ... +# TODO -- split into smaller interfaces? separate mailbox interface at least? class IMailAdaptor(Interface): """ @@ -53,64 +107,109 @@ class IMailAdaptor(Interface): :rtype: deferred """ - # TODO is staticmethod valid with an interface? - # @staticmethod def get_msg_from_string(self, MessageClass, raw_msg): """ - Return IMessageWrapper implementor from a raw mail string + Get an instance of a MessageClass initialized with a MessageWrapper + that contains all the parts obtained from parsing the raw string for + the message. :param MessageClass: an implementor of IMessage :type raw_msg: str :rtype: implementor of leap.mail.IMessage """ - # TODO is staticmethod valid with an interface? - # @staticmethod - def get_msg_from_docs(self, MessageClass, msg_wrapper): + def get_msg_from_docs(self, MessageClass, mdoc, fdoc, hdoc, cdocs=None, + uid=None): """ - Return an IMessage implementor from its parts. + Get an instance of a MessageClass initialized with a MessageWrapper + that contains the passed part documents. - :param MessageClass: an implementor of IMessage - :param msg_wrapper: an implementor of IMessageWrapper - :rtype: implementor of leap.mail.IMessage + This is not the recommended way of obtaining a message, unless you know + how to take care of ensuring the internal consistency between the part + documents, or unless you are glueing together the part documents that + have been previously generated by `get_msg_from_string`. + """ + + def get_flags_from_mdoc_id(self, store, mdoc_id): + """ + """ + + def create_msg(self, store, msg): + """ + :param store: an instance of soledad, or anything that behaves alike + :param msg: a Message object. + + :return: a Deferred that is fired when all the underlying documents + have been created. + :rtype: defer.Deferred + """ + + def update_msg(self, store, msg): """ + :param msg: a Message object. + :param store: an instance of soledad, or anything that behaves alike + :return: a Deferred that is fired when all the underlying documents + have been updated (actually, it's only the fdoc that's allowed + to update). + :rtype: defer.Deferred + """ + + def get_count_unseen(self, store, mbox_uuid): + """ + Get the number of unseen messages for a given mailbox. - # ------------------------------------------------------------------- - # XXX unsure about the following part yet ........................... + :param store: instance of Soledad. + :param mbox_uuid: the uuid for this mailbox. + :rtype: int + """ - # the idea behind these three methods is that the adaptor also offers a - # fixed interface to create the documents the first time (using - # soledad.create_docs or whatever method maps to it in a similar store, and - # also allows to update flags and tags, hiding the actual implementation of - # where the flags/tags live in behind the concrete MailWrapper in use - # by this particular adaptor. In our impl it will be put_doc(fdoc) after - # locking the getting + updating of that fdoc for atomicity. + def get_count_recent(self, store, mbox_uuid): + """ + Get the number of recent messages for a given mailbox. - # 'store' must be an instance of something that offers a minimal subset of - # the document API that Soledad currently implements (create_doc, put_doc) - # I *think* store should belong to Account/Collection and be passed as - # param here instead of relying on it being an attribute of the instance. + :param store: instance of Soledad. + :param mbox_uuid: the uuid for this mailbox. + :rtype: int + """ - def create_msg_docs(self, store, msg_wrapper): + def get_mdoc_id_from_msgid(self, store, mbox_uuid, msgid): """ - :param store: The documents store - :type store: - :param msg_wrapper: - :type msg_wrapper: IMessageWrapper implementor + Get the UID for a message with the passed msgid (the one in the headers + msg-id). + This is used by the MUA to retrieve the recently saved draft. """ - def update_msg_flags(self, store, msg_wrapper): + # mbox handling + + def get_or_create_mbox(self, store, name): """ - :param store: The documents store - :type store: - :param msg_wrapper: - :type msg_wrapper: IMessageWrapper implementor + Get the mailbox with the given name, or create one if it does not + exist. + + :param store: instance of Soledad + :param name: the name of the mailbox + :type name: str """ - def update_msg_tags(self, store, msg_wrapper): + def update_mbox(self, store, mbox_wrapper): """ - :param store: The documents store - :type store: - :param msg_wrapper: - :type msg_wrapper: IMessageWrapper implementor + Update the documents for a given mailbox. + :param mbox_wrapper: MailboxWrapper instance + :type mbox_wrapper: MailboxWrapper + :return: a Deferred that will be fired when the mailbox documents + have been updated. + :rtype: defer.Deferred + """ + + def delete_mbox(self, store, mbox_wrapper): + """ + """ + + def get_all_mboxes(self, store): + """ + Retrieve a list with wrappers for all the mailboxes. + + :return: a deferred that will be fired with a list of all the + MailboxWrappers found. + :rtype: defer.Deferred """ -- cgit v1.2.3 From 1b8e9f5d6df6aedd3566069d9d27adc1d8ad771d Mon Sep 17 00:00:00 2001 From: Ivan Alejandro Date: Wed, 23 Sep 2015 15:36:32 -0300 Subject: [feat] disable local-only bind on docker container - Related: #7471 --- changes/feature-7471_disable-local-bind-for-docker | 1 + src/leap/mail/imap/service/imap.py | 8 +++++++- src/leap/mail/smtp/__init__.py | 9 ++++++++- 3 files changed, 16 insertions(+), 2 deletions(-) create mode 100644 changes/feature-7471_disable-local-bind-for-docker diff --git a/changes/feature-7471_disable-local-bind-for-docker b/changes/feature-7471_disable-local-bind-for-docker new file mode 100644 index 0000000..a1ccb67 --- /dev/null +++ b/changes/feature-7471_disable-local-bind-for-docker @@ -0,0 +1 @@ +- disable local only tcp bind on docker containers to allow access to IMAP and SMTP. Related to #7471. diff --git a/src/leap/mail/imap/service/imap.py b/src/leap/mail/imap/service/imap.py index cd31edf..a50611b 100644 --- a/src/leap/mail/imap/service/imap.py +++ b/src/leap/mail/imap/service/imap.py @@ -158,8 +158,14 @@ def run_service(store, **kwargs): factory = LeapIMAPFactory(uuid, userid, store) try: + interface = "localhost" + # don't bind just to localhost if we are running on docker since we + # won't be able to access imap from the host + if os.environ.get("LEAP_DOCKERIZED"): + interface = '' + tport = reactor.listenTCP(port, factory, - interface="localhost") + interface=interface) except CannotListenError: logger.error("IMAP Service failed to start: " "cannot listen in port %s" % (port,)) diff --git a/src/leap/mail/smtp/__init__.py b/src/leap/mail/smtp/__init__.py index a77a414..7b62808 100644 --- a/src/leap/mail/smtp/__init__.py +++ b/src/leap/mail/smtp/__init__.py @@ -19,6 +19,7 @@ SMTP gateway helper function. """ import logging +import os from twisted.internet import reactor from twisted.internet.error import CannotListenError @@ -64,7 +65,13 @@ def setup_smtp_gateway(port, userid, keymanager, smtp_host, smtp_port, userid, keymanager, smtp_cert, smtp_key, smtp_host, smtp_port) factory = SMTPFactory(userid, keymanager, encrypted_only, outgoing_mail) try: - tport = reactor.listenTCP(port, factory, interface="localhost") + interface = "localhost" + # don't bind just to localhost if we are running on docker since we + # won't be able to access smtp from the host + if os.environ.get("LEAP_DOCKERIZED"): + interface = '' + + tport = reactor.listenTCP(port, factory, interface=interface) emit_async(catalog.SMTP_SERVICE_STARTED, str(port)) return factory, tport except CannotListenError: -- cgit v1.2.3