summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--changes/feature_import-maildir1
-rw-r--r--src/leap/bitmask/app.py10
-rw-r--r--src/leap/bitmask/services/mail/plumber.py114
-rw-r--r--src/leap/bitmask/util/__init__.py27
-rw-r--r--src/leap/bitmask/util/leap_argparse.py17
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 "