diff options
| -rw-r--r-- | changes/feature_import-maildir | 1 | ||||
| -rw-r--r-- | src/leap/bitmask/app.py | 10 | ||||
| -rw-r--r-- | src/leap/bitmask/services/mail/plumber.py | 114 | ||||
| -rw-r--r-- | src/leap/bitmask/util/__init__.py | 27 | ||||
| -rw-r--r-- | src/leap/bitmask/util/leap_argparse.py | 17 | 
5 files changed, 148 insertions, 21 deletions
| 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 " | 
