#!/usr/bin/env python
"""
Backup files listed on command line to an e-mail account.

- if sending to one email address fails, should report the error but still try the rest
- handle dirname as arg
- don't archive if only 1 file
- option to not encrypt
v way to deal with attachment size limits; eg, cogeco's smtp server seems to have a low 20 MB limit; i could use fastmail's smtp server, but then i'd have to include my password here--bad; so could allow splitting large archives into multiple emails; makes restoring the backup a bit of a chore, but at least i'll rarely need this
? maybe better to store a roughly equal number of bytes in each msg

Benefits:
- off-site
- doesn't replace last backup
- no password needed for remote site -- it's just email
- backups are as easy to access as your email
- dated by email account
- gpg is public key so no other password to worry about or enter
"""

__author__ = 'Patrick Roberts'
__copyright__ = 'Copyright 2009-2010 Patrick Roberts'
__license__ = 'Python'
__version__ = '1.3'


import fnmatch, glob, itertools, math, os, smtplib, subprocess, sys, tempfile, time, zipfile
from email.mime.multipart import MIMEMultipart
from email.mime.base import MIMEBase
from email import encoders


to_addrs = ['me@patrickroberts.ca', 'egofile@gmail.com']
from_addr = to_addrs[0]
smtp_server = 'smtp.cogeco.ca'
recipient = 'Patrick Roberts' # for GPG to find your public key
size_limit = int(10000000 * 8/6) # for encoding overhead


def cleanup(path):
    print 'Removing:', path
    try:
        os.unlink(path)
    except:
        print 'Error cleaning up', path


if __name__ == '__main__':
    parser = __import__('optparse').OptionParser(usage='usage: %prog [options] [file] ...')
    parser.add_option('-a', '--add', action='append', dest='email', help='add an e-mail address to send the backup to')
    options, args = parser.parse_args()
    #print options, args; sys.exit()
    to_addrs += options.email or []

    #names = list(itertools.chain.from_iterable(itertools.imap(glob.iglob, (sys.argv[1:] or itertools.imap('*.%s'.__mod__, 'txt py c cpp h'.split())))))
    names = []
    for dirpath, dirnames, filenames in os.walk(os.getcwd()):
        for pattern in (args or itertools.imap('*.%s'.__mod__, 'txt py c cpp h odt doc rc graphml rtf png bat iss tbs'.split())):
            for filename in filenames:
                #print filename, pattern
                if fnmatch.fnmatch(filename, pattern):
                    names.append(os.path.join(dirpath, filename)[len(os.getcwd()):].lstrip(os.sep))

    #print 'Found:', names
    if names:
        subject = 'Backup of %s' % (names[0] if len(names) == 1 else os.getcwd().replace(os.sep, '-'))
        h, archive_path = tempfile.mkstemp()
        os.close(h) # GPG can't open the file without this
        print 'Temporarily archiving to', archive_path

        zip_file = zipfile.ZipFile(archive_path, 'w', zipfile.ZIP_DEFLATED)

        try:
            #map(zip_file.write, names)
            for name in names:
                print 'Zipping:', name
                zip_file.write(name)
            zip_file.close()

            # Python lacks a std crypto module; TrueCrypt lacks command line volume creation on Windows
            crypt_path = '%s.gpg' % archive_path
            print 'Encrypting to', crypt_path
            subprocess.check_call(['c:\Program Files\GNU\GnuPG\gpg.exe', '-r', recipient, '-e', archive_path])

            try:
                f = file(crypt_path, 'rb')
                part = 1
                parts = math.ceil(os.path.getsize(crypt_path) / float(size_limit))
                while True:
                    attachment = f.read(size_limit)
                    if not attachment:
                        break
                    msg = MIMEMultipart()
                    msg['From'] = from_addr
                    msg['Subject'] = '%s %d/%d' % (subject, part, parts)
                    #msg.attach(MIMEText(msg_en))
                    file_part = MIMEBase('application', 'octet-stream')
                    file_part.set_payload(attachment)
                    encoders.encode_base64(file_part)
                    attachment_name = '%s.zip.gpg' % subject
                    if parts > 1:
                        attachment_name += '.%d' % part
                    file_part.add_header('Content-Disposition', 'attachment; filename="%s"' % attachment_name)
                    msg.attach(file_part)
                    for to_addr in to_addrs:
                        msg['To'] = to_addr
                        while 1:
                            try:
                                print 'Mailing %d/%d %.2f MB to %s' % (part, parts, len(attachment) / 1000000.0, to_addr)
                                smtplib.SMTP(smtp_server).sendmail(from_addr, to_addr, msg.as_string())
                                break
                            except Exception, e:
                                print 'Error sending e-mail:', e
                                time.sleep(5)
                    part += 1
            finally:
                f.close()
                cleanup(crypt_path)

        finally:
            cleanup(archive_path)
    else:
        print 'Found no files to backup'

