diff options
Diffstat (limited to 'src/leap/bitmask/services/mail')
-rw-r--r-- | src/leap/bitmask/services/mail/conductor.py | 30 | ||||
-rw-r--r-- | src/leap/bitmask/services/mail/connection.py | 2 | ||||
-rw-r--r-- | src/leap/bitmask/services/mail/plumber.py (renamed from src/leap/bitmask/services/mail/repair.py) | 205 |
3 files changed, 183 insertions, 54 deletions
diff --git a/src/leap/bitmask/services/mail/conductor.py b/src/leap/bitmask/services/mail/conductor.py index addf9bef..79f324dc 100644 --- a/src/leap/bitmask/services/mail/conductor.py +++ b/src/leap/bitmask/services/mail/conductor.py @@ -35,6 +35,7 @@ from leap.common.check import leap_assert from leap.common.events import register as leap_register from leap.common.events import events_pb2 as leap_events + logger = logging.getLogger(__name__) @@ -72,6 +73,8 @@ class IMAPControl(object): """ Starts imap service. """ + from leap.bitmask.config import flags + logger.debug('Starting imap service') leap_assert(sameProxiedObjects(self._soledad, None) is not True, @@ -81,16 +84,25 @@ class IMAPControl(object): "We need a non-null keymanager for initializing imap " "service") + offline = flags.OFFLINE self.imap_service, self.imap_port, \ self.imap_factory = imap.start_imap_service( self._soledad, self._keymanager, - userid=self.userid) - self.imap_service.start_loop() + userid=self.userid, + offline=offline) + + if offline is False: + logger.debug("Starting loop") + self.imap_service.start_loop() - def stop_imap_service(self): + def stop_imap_service(self, cv): """ Stops imap service (fetcher, factory and port). + + :param cv: A condition variable to which we can signal when imap + indeed stops. + :type cv: threading.Condition """ self.imap_connection.qtsigs.disconnecting_signal.emit() # TODO We should homogenize both services. @@ -102,7 +114,14 @@ class IMAPControl(object): # Stop listening on the IMAP port self.imap_port.stopListening() # Stop the protocol - self.imap_factory.doStop() + self.imap_factory.theAccount.closed = True + self.imap_factory.doStop(cv) + else: + # main window does not have to wait because there's no service to + # be stopped, so we release the condition variable + cv.acquire() + cv.notify() + cv.release() def fetch_incoming_mail(self): """ @@ -339,7 +358,7 @@ class MailConductor(IMAPControl, SMTPControl): self._mail_machine = None self._mail_connection = mail_connection.MailConnection() - self.userid = None + self._userid = None @property def userid(self): @@ -388,3 +407,4 @@ class MailConductor(IMAPControl, SMTPControl): qtsigs.connecting_signal.connect(widget.mail_state_connecting) qtsigs.disconnecting_signal.connect(widget.mail_state_disconnecting) qtsigs.disconnected_signal.connect(widget.mail_state_disconnected) + qtsigs.soledad_invalid_auth_token.connect(widget.soledad_invalid_auth_token) diff --git a/src/leap/bitmask/services/mail/connection.py b/src/leap/bitmask/services/mail/connection.py index 29378f62..fdc28fe4 100644 --- a/src/leap/bitmask/services/mail/connection.py +++ b/src/leap/bitmask/services/mail/connection.py @@ -93,6 +93,8 @@ class MailConnectionSignals(QtCore.QObject): connection_died_signal = QtCore.Signal() connection_aborted_signal = QtCore.Signal() + soledad_invalid_auth_token = QtCore.Signal() + class MailConnection(AbstractLEAPConnection): diff --git a/src/leap/bitmask/services/mail/repair.py b/src/leap/bitmask/services/mail/plumber.py index 767df1ef..c16a1fed 100644 --- a/src/leap/bitmask/services/mail/repair.py +++ b/src/leap/bitmask/services/mail/plumber.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -# repair.py -# Copyright (C) 2013 LEAP +# plumber.py +# Copyright (C) 2013, 2014 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,20 +15,26 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. """ -Utils for repairing mailbox indexes. +Utils for manipulating local mailboxes. """ -import logging import getpass +import logging import os from collections import defaultdict +from functools import partial +from twisted.internet import defer + +from leap.bitmask.config.leapsettings import LeapSettings from leap.bitmask.config.providerconfig import ProviderConfig -from leap.bitmask.crypto.srpauth import SRPAuth -from leap.bitmask.util import get_path_prefix +from leap.bitmask.provider import get_provider_path from leap.bitmask.services.soledad.soledadbootstrapper import get_db_paths +from leap.bitmask.util import flatten, get_path_prefix -from leap.mail.imap.server import SoledadBackedAccount +from leap.mail.imap.account import SoledadBackedAccount +from leap.mail.imap.memorystore import MemoryStore +from leap.mail.imap.soledadstore import SoledadStore from leap.soledad.client import Soledad logger = logging.getLogger(__name__) @@ -89,69 +95,77 @@ class MBOXPlumber(object): that can be invoked when data migration in the client is needed. """ - def __init__(self, userid, passwd): + def __init__(self, userid, passwd, mdir=None): """ - Initializes the plumber with all that's needed to authenticate + Initialize the plumber with all that's needed to authenticate against the provider. :param userid: user identifier, foo@bar :type userid: basestring :param passwd: the soledad passphrase :type passwd: basestring + :param mdir: a path to a maildir to import + :type mdir: str or None """ self.userid = userid self.passwd = passwd user, provider = userid.split('@') self.user = user + self.mdir = mdir self.sol = None - provider_config_path = os.path.join( - get_path_prefix(), - "leap", "providers", - provider, "provider.json") + self._settings = LeapSettings() + + provider_config_path = os.path.join(get_path_prefix(), + get_provider_path(provider)) provider_config = ProviderConfig() loaded = provider_config.load(provider_config_path) if not loaded: print "could not load provider config!" return self.exit() - self.srp = SRPAuth(provider_config) - self.srp.authentication_finished.connect(self.repair_account) - - def start_auth(self): + def _init_local_soledad(self): """ - returns the user identifier for a given provider. - - :param provider: the provider to which we authenticate against. + Initialize local Soledad instance. """ - print "Authenticating with provider..." - self.d = self.srp.authenticate(self.user, self.passwd) + self.uuid = self._settings.get_uuid(self.userid) + if not self.uuid: + print "Cannot get UUID from settings. Log in at least once." + return False - def repair_account(self, *args): - """ - Gets the user id for this account. - """ - print "Got authenticated." - self.uid = self.srp.get_uid() - if not self.uid: - print "Got BAD UID from provider!" - return self.exit() - print "UID: %s" % (self.uid) + print "UUID: %s" % (self.uuid) - secrets, localdb = get_db_paths(self.uid) + secrets, localdb = get_db_paths(self.uuid) self.sol = initialize_soledad( - self.uid, self.userid, self.passwd, + self.uuid, self.userid, self.passwd, secrets, localdb, "/tmp", "/tmp") + memstore = MemoryStore( + permanent_store=SoledadStore(self.sol), + write_period=5) + self.acct = SoledadBackedAccount(self.userid, self.sol, + memstore=memstore) + return True + + # + # Account repairing + # + + def repair_account(self, *args): + """ + Repair mbox uids for all mboxes in this account. + """ + init = self._init_local_soledad() + if not init: + return self.exit() - self.acct = SoledadBackedAccount(self.userid, self.sol) for mbox_name in self.acct.mailboxes: - self.repair_mbox(mbox_name) + self.repair_mbox_uids(mbox_name) print "done." self.exit() - def repair_mbox(self, mbox_name): + def repair_mbox_uids(self, mbox_name): """ - Repairs indexes for a given mbox + Repair indexes for a given mbox. :param mbox_name: mailbox to repair :type mbox_name: basestring @@ -164,19 +178,21 @@ class MBOXPlumber(object): print "There are %s messages" % (len_mbox,) last_ok = True if mbox.last_uid == len_mbox else False - uids_iter = (doc.content['uid'] for doc in mbox.messages.get_all()) + uids_iter = mbox.messages.all_msg_iter() dupes = self._has_dupes(uids_iter) if last_ok and not dupes: print "Mbox does not need repair." return + # XXX CHANGE? ---- msgs = mbox.messages.get_all() for zindex, doc in enumerate(msgs): mindex = zindex + 1 old_uid = doc.content['uid'] doc.content['uid'] = mindex self.sol.put_doc(doc) - print "%s -> %s (%s)" % (mindex, doc.content['uid'], old_uid) + if mindex != old_uid: + print "%s -> %s (%s)" % (mindex, doc.content['uid'], old_uid) old_last_uid = mbox.last_uid mbox.last_uid = len_mbox @@ -184,7 +200,7 @@ class MBOXPlumber(object): def _has_dupes(self, sequence): """ - Returns True if the given sequence of ints has duplicates. + Return True if the given sequence of ints has duplicates. :param sequence: a sequence of ints :type sequence: sequence @@ -197,12 +213,82 @@ class MBOXPlumber(object): return True return False + # + # Maildir import + # + def import_mail(self, mail_filename): + """ + Import a single mail into a mailbox. + + :param mbox: the Mailbox instance to save in. + :type mbox: SoledadMailbox + :param mail_filename: the filename to the mail file to save + :type mail_filename: basestring + :return: a deferred + """ + def saved(_): + print "message added" + + with open(mail_filename) as f: + mail_string = f.read() + #uid = self._mbox.getUIDNext() + #print "saving with UID: %s" % uid + d = self._mbox.messages.add_msg( + mail_string, notify_on_disk=True) + return d + + def import_maildir(self, mbox_name="INBOX"): + """ + Import all mails in a maildir. + + We will process all subfolders as beloging + to the same mailbox (cur, new, tmp). + """ + # TODO parse hierarchical subfolders into + # inferior mailboxes. + + if not os.path.isdir(self.mdir): + print "ERROR: maildir path does not exist." + return + + init = self._init_local_soledad() + if not init: + return self.exit() + + mbox = self.acct.getMailbox(mbox_name) + self._mbox = mbox + len_mbox = mbox.getMessageCount() + + mail_files_g = flatten( + map(partial(os.path.join, f), files) + for f, _, files in os.walk(self.mdir)) + + # we only coerce the generator to give the + # len, but we could skip than and inform at the end. + mail_files = list(mail_files_g) + print "Got %s mails to import into %s (%s)" % ( + len(mail_files), mbox_name, len_mbox) + + def all_saved(_): + print "all messages imported" + + deferreds = [] + for f_name in mail_files: + deferreds.append(self.import_mail(f_name)) + print "deferreds: ", deferreds + + d1 = defer.gatherResults(deferreds, consumeErrors=False) + d1.addCallback(all_saved) + d1.addCallback(self._cbExit) + + def _cbExit(self, ignored): + return self.exit() + def exit(self): from twisted.internet import reactor - self.d.cancel() - if self.sol: - self.sol.close() try: + if self.sol: + self.sol.close() reactor.stop() except Exception: pass @@ -211,7 +297,8 @@ class MBOXPlumber(object): def repair_account(userid): """ - Starts repair process for a given account. + Start repair process for a given account. + :param userid: the user id (email-like) """ from twisted.internet import reactor @@ -219,7 +306,22 @@ def repair_account(userid): # go mario! plumber = MBOXPlumber(userid, passwd) - reactor.callLater(1, plumber.start_auth) + reactor.callLater(1, plumber.repair_account) + reactor.run() + + +def import_maildir(userid, maildir_path): + """ + Start import-maildir process for a given account. + + :param userid: the user id (email-like) + """ + from twisted.internet import reactor + passwd = unicode(getpass.getpass("Passphrase: ")) + + # go mario! + plumber = MBOXPlumber(userid, passwd, mdir=maildir_path) + reactor.callLater(1, plumber.import_maildir) reactor.run() @@ -228,7 +330,12 @@ if __name__ == "__main__": logging.basicConfig() - if len(sys.argv) != 2: - print "Usage: repair <username>" + if len(sys.argv) != 3: + print "Usage: plumber [repair|import] <username>" sys.exit(1) - repair_account(sys.argv[1]) + + # this would be better with a dict if it grows + if sys.argv[1] == "repair": + repair_account(sys.argv[2]) + if sys.argv[1] == "import": + print "Not implemented yet." |