Added Andy Bovett's Thunderbird and Windows fixes

This commit is contained in:
Rui Carmo 2013-12-09 16:23:09 +00:00
parent 066cb8529e
commit 5c7129d24c

View file

@ -1,12 +1,14 @@
#!/usr/bin/env python #!/usr/bin/env python
"""IMAP Incremental Backup Script""" """IMAP Incremental Backup Script"""
__version__ = "1.4e" __version__ = "1.4f"
__author__ = "Rui Carmo (http://the.taoofmac.com)" __author__ = "Rui Carmo (http://the.taoofmac.com)"
__copyright__ = "(C) 2006-2013 Rui Carmo. Code under MIT License.(C)" __copyright__ = "(C) 2006-2013 Rui Carmo. Code under MIT License.(C)"
__contributors__ = "Bob Ippolito, Michael Leonhard, Giuseppe Scrivano <gscrivano@gnu.org>, Ronan Sheth, Brandon Long, Christian Schanz" __contributors__ = "Bob Ippolito, Michael Leonhard, Giuseppe Scrivano <gscrivano@gnu.org>, Ronan Sheth, Brandon Long, Christian Schanz, A. Bovett"
# = Contributors = # = Contributors =
# A. Bovett: Modifications for Thunderbird compatibility and disabling spinner in Windows
# Christian Schanz: added target directory parameter
# Brandon Long (Gmail team): Reminder to use BODY.PEEK instead of BODY # Brandon Long (Gmail team): Reminder to use BODY.PEEK instead of BODY
# Ronan Sheth: hashlib patch (this now requires Python 2.5, although reverting it back is trivial) # Ronan Sheth: hashlib patch (this now requires Python 2.5, although reverting it back is trivial)
# Giuseppe Scrivano: Added support for folders. # Giuseppe Scrivano: Added support for folders.
@ -27,8 +29,6 @@ __contributors__ = "Bob Ippolito, Michael Leonhard, Giuseppe Scrivano <gscrivano
# - Add regex option to filter folders # - Add regex option to filter folders
# - Use a single IMAP command to get Message-IDs # - Use a single IMAP command to get Message-IDs
# - Use a single IMAP command to fetch the messages # - Use a single IMAP command to fetch the messages
# - Add option to turn off spinner. Since sys.stdin.isatty() doesn't work on
# Windows, redirecting output to a file results in junk output.
# - Patch Python's ssl module to do proper checking of certificate chain # - Patch Python's ssl module to do proper checking of certificate chain
# - Patch Python's ssl module to raise good exceptions # - Patch Python's ssl module to raise good exceptions
# - Submit patch of socket._fileobject.read # - Submit patch of socket._fileobject.read
@ -43,6 +43,7 @@ import getpass, os, gc, sys, time, platform, getopt
import mailbox, imaplib, socket import mailbox, imaplib, socket
import re, hashlib, gzip, bz2 import re, hashlib, gzip, bz2
class SkipFolderException(Exception): class SkipFolderException(Exception):
"""Indicates aborting processing of current folder, continue with next folder.""" """Indicates aborting processing of current folder, continue with next folder."""
pass pass
@ -50,25 +51,26 @@ class SkipFolderException(Exception):
class Spinner: class Spinner:
"""Prints out message with cute spinner, indicating progress""" """Prints out message with cute spinner, indicating progress"""
def __init__(self, message): def __init__(self, message, nospinner):
"""Spinner constructor""" """Spinner constructor"""
self.glyphs = "|/-\\" self.glyphs = "|/-\\"
self.pos = 0 self.pos = 0
self.message = message self.message = message
self.nospinner = nospinner
sys.stdout.write(message) sys.stdout.write(message)
sys.stdout.flush() sys.stdout.flush()
self.spin() self.spin()
def spin(self): def spin(self):
"""Rotate the spinner""" """Rotate the spinner"""
if sys.stdin.isatty(): if sys.stdin.isatty() and not self.nospinner:
sys.stdout.write("\r" + self.message + " " + self.glyphs[self.pos]) sys.stdout.write("\r" + self.message + " " + self.glyphs[self.pos])
sys.stdout.flush() sys.stdout.flush()
self.pos = (self.pos+1) % len(self.glyphs) self.pos = (self.pos+1) % len(self.glyphs)
def stop(self): def stop(self):
"""Erase the spinner from the screen""" """Erase the spinner from the screen"""
if sys.stdin.isatty(): if sys.stdin.isatty() and not self.nospinner:
sys.stdout.write("\r" + self.message + " ") sys.stdout.write("\r" + self.message + " ")
sys.stdout.write("\r" + self.message) sys.stdout.write("\r" + self.message)
sys.stdout.flush() sys.stdout.flush()
@ -123,7 +125,8 @@ def download_messages(server, filename, messages, config):
mbox.close() mbox.close()
return return
spinner = Spinner("Downloading %s new messages to %s" % (len(messages), filename)) spinner = Spinner("Downloading %s new messages to %s" % (len(messages), filename),
config['nospinner'])
total = biggest = 0 total = biggest = 0
# each new message # each new message
@ -141,6 +144,10 @@ def download_messages(server, filename, messages, config):
typ, data = server.fetch(messages[msg_id], "RFC822") typ, data = server.fetch(messages[msg_id], "RFC822")
assert('OK' == typ) assert('OK' == typ)
text = data[0][1].strip().replace('\r','') text = data[0][1].strip().replace('\r','')
if config['thunderbird']:
# This avoids Thunderbird mistaking a line starting "From " as the start
# of a new message. _Might_ also apply to other mail lients - unknown
text = text.replace("\nFrom ", "\n From ")
mbox.write(text) mbox.write(text)
mbox.write('\n\n') mbox.write('\n\n')
@ -157,7 +164,7 @@ def download_messages(server, filename, messages, config):
print ": %s total, %s for largest message" % (pretty_byte_count(total), print ": %s total, %s for largest message" % (pretty_byte_count(total),
pretty_byte_count(biggest)) pretty_byte_count(biggest))
def scan_file(filename, compress, overwrite): def scan_file(filename, compress, overwrite, nospinner):
"""Gets IDs of messages in the specified mbox file""" """Gets IDs of messages in the specified mbox file"""
# file will be overwritten # file will be overwritten
if overwrite: if overwrite:
@ -170,7 +177,7 @@ def scan_file(filename, compress, overwrite):
print "File %s: not found" % (filename) print "File %s: not found" % (filename)
return [] return []
spinner = Spinner("File %s" % (filename)) spinner = Spinner("File %s" % (filename), nospinner)
# open the file # open the file
if compress == 'gzip': if compress == 'gzip':
@ -216,10 +223,10 @@ def scan_file(filename, compress, overwrite):
print ": %d messages" % (len(messages.keys())) print ": %d messages" % (len(messages.keys()))
return messages return messages
def scan_folder(server, foldername): def scan_folder(server, foldername, nospinner):
"""Gets IDs of messages in the specified folder, returns id:num dict""" """Gets IDs of messages in the specified folder, returns id:num dict"""
messages = {} messages = {}
spinner = Spinner("Folder %s" % (foldername)) spinner = Spinner("Folder %s" % (foldername), nospinner)
try: try:
typ, data = server.select(foldername, readonly=True) typ, data = server.select(foldername, readonly=True)
if 'OK' != typ: if 'OK' != typ:
@ -319,10 +326,10 @@ def get_hierarchy_delimiter(server):
hierarchy_delim = '.' hierarchy_delim = '.'
return hierarchy_delim return hierarchy_delim
def get_names(server, compress): def get_names(server, compress, thunderbird, nospinner):
"""Get list of folders, returns [(FolderName,FileName)]""" """Get list of folders, returns [(FolderName,FileName)]"""
spinner = Spinner("Finding Folders") spinner = Spinner("Finding Folders", nospinner)
# Get hierarchy delimiter # Get hierarchy delimiter
delim = get_hierarchy_delimiter(server) delim = get_hierarchy_delimiter(server)
@ -340,7 +347,14 @@ def get_names(server, compress):
lst = parse_list(row) lst = parse_list(row)
foldername = lst[2] foldername = lst[2]
suffix = {'none':'', 'gzip':'.gz', 'bzip2':'.bz2'}[compress] suffix = {'none':'', 'gzip':'.gz', 'bzip2':'.bz2'}[compress]
filename = '.'.join(foldername.split(delim)) + '.mbox' + suffix if thunderbird:
filename = '.sbd/'.join(foldername.split(delim)) + suffix
if filename.startswith("INBOX"):
filename = filename.replace("INBOX","Inbox")
else:
filename = '.'.join(foldername.split(delim)) + '.mbox' + suffix
# print "\n*** Folder:", foldername # *DEBUG
# print "*** File:", filename # *DEBUG
names.append((foldername, filename)) names.append((foldername, filename))
# done # done
@ -358,7 +372,6 @@ def print_usage():
print " -z --compress=gzip Use mbox.gz files. Appending may be very slow." print " -z --compress=gzip Use mbox.gz files. Appending may be very slow."
print " -b --compress=bzip2 Use mbox.bz2 files. Appending not supported: use -y." print " -b --compress=bzip2 Use mbox.bz2 files. Appending not supported: use -y."
print " -f --=folder Specifify which folders use. Comma separated list." print " -f --=folder Specifify which folders use. Comma separated list."
print ' -t --target="/path/" Specify target directory.'
print " -e --ssl Use SSL. Port defaults to 993." print " -e --ssl Use SSL. Port defaults to 993."
print " -k KEY --key=KEY PEM private key file for SSL. Specify cert, too." print " -k KEY --key=KEY PEM private key file for SSL. Specify cert, too."
print " -c CERT --cert=CERT PEM certificate chain for SSL. Specify key, too." print " -c CERT --cert=CERT PEM certificate chain for SSL. Specify key, too."
@ -366,6 +379,8 @@ def print_usage():
print " -s HOST --server=HOST Address of server, port optional, eg. mail.com:143" print " -s HOST --server=HOST Address of server, port optional, eg. mail.com:143"
print " -u USER --user=USER Username to log into server" print " -u USER --user=USER Username to log into server"
print " -p PASS --pass=PASS Prompts for password if not specified." print " -p PASS --pass=PASS Prompts for password if not specified."
print " --thunderbird Create Mozilla Thunderbird compatible mailbox"
print " --nospinner Disable spinner (makes output log-friendly)"
print "\nNOTE: mbox files are created in the current working directory." print "\nNOTE: mbox files are created in the current working directory."
sys.exit(2) sys.exit(2)
@ -373,15 +388,17 @@ def process_cline():
"""Uses getopt to process command line, returns (config, warnings, errors)""" """Uses getopt to process command line, returns (config, warnings, errors)"""
# read command line # read command line
try: try:
short_args = "aynzbek:c:s:u:p:f:t:" short_args = "aynzbek:c:s:u:p:f:"
long_args = ["append-to-mboxes", "yes-overwrite-mboxes", "compress=", long_args = ["append-to-mboxes", "yes-overwrite-mboxes", "compress=",
"ssl", "keyfile=", "certfile=", "server=", "user=", "pass=", "folders=", "target="] "ssl", "keyfile=", "certfile=", "server=", "user=", "pass=",
"folders=", "thunderbird", "nospinner"]
opts, extraargs = getopt.getopt(sys.argv[1:], short_args, long_args) opts, extraargs = getopt.getopt(sys.argv[1:], short_args, long_args)
except getopt.GetoptError: except getopt.GetoptError:
print_usage() print_usage()
warnings = [] warnings = []
config = {'compress':'none', 'overwrite':False, 'usessl':False, 'target':""} config = {'compress':'none', 'overwrite':False, 'usessl':False,
'thunderbird':False, 'nospinner':False}
errors = [] errors = []
# empty command line # empty command line
@ -412,10 +429,6 @@ def process_cline():
config['keyfilename'] = value config['keyfilename'] = value
elif option in ("-f", "--folders"): elif option in ("-f", "--folders"):
config['folders'] = value config['folders'] = value
elif option in ("-t", "--target"):
if not value.endswith(os.sep):
value += os.sep
config['target'] = value
elif option in ("-c", "--certfile"): elif option in ("-c", "--certfile"):
config['certfilename'] = value config['certfilename'] = value
elif option in ("-s", "--server"): elif option in ("-s", "--server"):
@ -424,6 +437,10 @@ def process_cline():
config['user'] = value config['user'] = value
elif option in ("-p", "--pass"): elif option in ("-p", "--pass"):
config['pass'] = value config['pass'] = value
elif option == "--thunderbird":
config['thunderbird'] = True
elif option == "--nospinner":
config['nospinner'] = True
else: else:
errors.append("Unknown option: " + option) errors.append("Unknown option: " + option)
@ -548,27 +565,43 @@ def connect_and_login(config):
return server return server
def create_folder_structure(names):
""" Create the folder structure on disk """
for imap_foldername, filename in sorted(names):
disk_foldername = os.path.split(filename)[0]
if disk_foldername:
try:
# print "*** mkdir:", disk_foldername # *DEBUG
os.mkdir(disk_foldername)
except OSError, e:
if e.errno != 17:
raise
def main(): def main():
"""Main entry point""" """Main entry point"""
try: try:
config = get_config() config = get_config()
server = connect_and_login(config) server = connect_and_login(config)
names = get_names(server, config['compress']) names = get_names(server, config['compress'], config['thunderbird'],
config['nospinner'])
if config.get('folders'): if config.get('folders'):
dirs = map (lambda x: x.strip(), config.get('folders').split(',')) dirs = map (lambda x: x.strip(), config.get('folders').split(','))
if config['thunderbird']:
dirs = [i.replace("Inbox", "INBOX", 1) if i.startswith("Inbox") else i
for i in dirs]
names = filter (lambda x: x[0] in dirs, names) names = filter (lambda x: x[0] in dirs, names)
#for n in range(len(names)): # for n, name in enumerate(names): # *DEBUG
# print n, names[n] # print n, name # *DEBUG
create_folder_structure(names)
for name_pair in names: for name_pair in names:
try: try:
foldername, filename = name_pair foldername, filename = name_pair
filename = config['target'] + filename fol_messages = scan_folder(server, foldername, config['nospinner'])
fol_messages = scan_folder(server, foldername) fil_messages = scan_file(filename, config['compress'],
fil_messages = scan_file(filename, config['compress'], config['overwrite']) config['overwrite'], config['nospinner'])
new_messages = {} new_messages = {}
for msg_id in fol_messages: for msg_id in fol_messages:
if msg_id not in fil_messages: if msg_id not in fil_messages: