From 7f9fa030ed44a7db6ced5b359c49dadc0a781b8a Mon Sep 17 00:00:00 2001 From: Kali Kaneko Date: Sun, 12 Jan 2014 22:56:31 -0400 Subject: able to import maildir --- changes/feature_import-maildir | 1 + src/leap/bitmask/app.py | 10 ++- src/leap/bitmask/services/mail/plumber.py | 114 +++++++++++++++++++++++++++--- src/leap/bitmask/util/__init__.py | 27 ++++++- src/leap/bitmask/util/leap_argparse.py | 17 +++-- 5 files changed, 148 insertions(+), 21 deletions(-) create mode 100644 changes/feature_import-maildir diff --git a/changes/feature_import-maildir b/changes/feature_import-maildir new file mode 100644 index 00000000..c5d861a3 --- /dev/null +++ b/changes/feature_import-maildir @@ -0,0 +1 @@ +- Add ability to import a maildir into a local mailbox. diff --git a/src/leap/bitmask/app.py b/src/leap/bitmask/app.py index 12d1dadd..d8d1d38a 100644 --- a/src/leap/bitmask/app.py +++ b/src/leap/bitmask/app.py @@ -54,7 +54,7 @@ from leap.bitmask.util import log_silencer from leap.bitmask.util.leap_log_handler import LeapLogHandler from leap.bitmask.util.streamtologger import StreamToLogger from leap.bitmask.platform_init import IS_WIN -from leap.bitmask.services.mail.plumber import repair_account +from leap.bitmask.services.mail import plumber from leap.common.events import server as event_server from leap.mail import __version__ as MAIL_VERSION @@ -176,9 +176,13 @@ def main(): print "leap.mail version: %s" % (MAIL_VERSION,) sys.exit(0) - if opts.acct_to_repair: - repair_account(opts.acct_to_repair) + if opts.repair: + plumber.repair_account(opts.acct) sys.exit(0) + if opts.import_maildir and opts.acct: + plumber.import_maildir(opts.acct, opts.import_maildir) + sys.exit(0) + # XXX catch when import is used w/o acct standalone = opts.standalone offline = opts.offline diff --git a/src/leap/bitmask/services/mail/plumber.py b/src/leap/bitmask/services/mail/plumber.py index 4ecbc361..49514655 100644 --- a/src/leap/bitmask/services/mail/plumber.py +++ b/src/leap/bitmask/services/mail/plumber.py @@ -22,10 +22,13 @@ import getpass 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.util import get_path_prefix +from leap.bitmask.util import flatten, get_path_prefix from leap.bitmask.services.soledad.soledadbootstrapper import get_db_paths from leap.mail.imap.account import SoledadBackedAccount @@ -89,20 +92,23 @@ 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 self._settings = LeapSettings() @@ -116,9 +122,9 @@ class MBOXPlumber(object): print "could not load provider config!" return self.exit() - def repair_account(self, *args): + def _init_local_soledad(self): """ - Gets the user id for this account. + Initialize local Soledad instance. """ self.uuid = self._settings.get_uuid(self.userid) if not self.uuid: @@ -131,8 +137,16 @@ class MBOXPlumber(object): self.sol = initialize_soledad( self.uuid, self.userid, self.passwd, secrets, localdb, "/tmp", "/tmp") - self.acct = SoledadBackedAccount(self.userid, self.sol) + # + # Account repairing + # + + def repair_account(self, *args): + """ + Repair mbox uids for all mboxes in this account. + """ + self._init_local_soledad() for mbox_name in self.acct.mailboxes: self.repair_mbox_uids(mbox_name) print "done." @@ -140,7 +154,7 @@ class MBOXPlumber(object): 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 @@ -159,6 +173,7 @@ class MBOXPlumber(object): print "Mbox does not need repair." return + # XXX CHANGE? ---- msgs = mbox.messages.get_all() for zindex, doc in enumerate(msgs): mindex = zindex + 1 @@ -174,7 +189,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 @@ -187,6 +202,71 @@ 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, uid=uid) + 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 + + self._init_local_soledad() + 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)) + 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 if self.sol: @@ -200,7 +280,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 @@ -212,6 +293,21 @@ def repair_account(userid): 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() + + if __name__ == "__main__": import sys diff --git a/src/leap/bitmask/util/__init__.py b/src/leap/bitmask/util/__init__.py index 85676d51..c35be99e 100644 --- a/src/leap/bitmask/util/__init__.py +++ b/src/leap/bitmask/util/__init__.py @@ -18,19 +18,23 @@ Some small and handy functions. """ import datetime +import itertools import os from leap.bitmask.config import flags from leap.common.config import get_path_prefix as common_get_path_prefix - -def get_path_prefix(): - return common_get_path_prefix(flags.STANDALONE) +# functional goodies for a healthier life: +# We'll give your money back if it does not alleviate the eye strain, at least. def first(things): """ Return the head of a collection. + + :param things: a sequence to extract the head from. + :type things: sequence + :return: object, or None """ try: return things[0] @@ -38,6 +42,23 @@ def first(things): return None +def flatten(things): + """ + Return a generator iterating through a flattened sequence. + + :param things: a nested sequence, eg, a list of lists. + :type things: sequence + :rtype: generator + """ + return itertools.chain(*things) + + +# leap repetitive chores + +def get_path_prefix(): + return common_get_path_prefix(flags.STANDALONE) + + def get_modification_ts(path): """ Gets modification time of a file. diff --git a/src/leap/bitmask/util/leap_argparse.py b/src/leap/bitmask/util/leap_argparse.py index dd0f40f7..56bf26dc 100644 --- a/src/leap/bitmask/util/leap_argparse.py +++ b/src/leap/bitmask/util/leap_argparse.py @@ -69,17 +69,22 @@ Launches the Bitmask client.""", epilog=epilog) parser.add_argument('-o', '--offline', action="store_true", help='Starts Bitmask in offline mode: will not ' 'try to sync with remote replicas for email.') - parser.add_argument('-r', '--repair-mailboxes', metavar="user@provider", + + parser.add_argument('--acct', metavar="user@provider", nargs='?', - action="store", dest="acct_to_repair", + action="store", dest="acct", + help='Manipulate mailboxes for this account') + parser.add_argument('-r', '--repair-mailboxes', default=False, + action="store_true", dest="repair", help='Repair mailboxes for a given account. ' 'Use when upgrading versions after a schema ' - 'change.') + 'change. Use with --acct') parser.add_argument('--import-maildir', metavar="/path/to/Maildir", nargs='?', - action="store", dest="maildir", - help='Import the given maildir. Use with the --mdir ' - 'flag to import to folders other than INBOX.') + action="store", dest="import_maildir", + help='Import the given maildir. Use with the ' + '--to-mbox flag to import to folders other ' + 'than INBOX. Use with --acct') if not IS_RELEASE_VERSION: help_text = ("Bypasses the certificate check during provider " -- cgit v1.2.3