#!/usr/bin/env python

"""
Parse the XML format SMS and MMS messages generated by Microsoft's Windows
Phone "contacts+message backup" application, formatting them into MIME emails,
before uploading them to GMail.
"""

import datetime
import email
import email.encoders
import email.mime.base
import email.mime.multipart
import email.mime.text
import email.utils
import getpass
import hashlib
import imaplib
import mimetypes
import os.path
import pprint
import shelve
import sys

import imapclient
import keyring
import lxml.etree
import phonenumbers
import phonenumbers.phonenumberutil
import vobject
import vobject.base


DEFAULT_COUNTRY_CODE = 'GB'
UNIX_EPOCH = datetime.datetime(1970, 1, 1)
WTF_EPOCH = datetime.datetime(1601, 1, 1)
MY_NAME = 'David Wilson'
MY_NUMBER = '+447447830171'
MY_EMAIL = 'dw@botanicus.net'
MSGID_FORMAT = '%s@sms.botanicus.net'


def wtftime_to_dt(n):
    return WTF_EPOCH + datetime.timedelta(seconds=(n / 1e7))


def dt_to_epoch(dt):
    return (dt - UNIX_EPOCH).total_seconds()


def parse_attachment(elem):
    b64 = elem.findtext('AttachmentDataBase64String')
    return {
        'content_type': elem.findtext('AttachmentContentType'),
        'data': b64.decode('base64'),
    }


def parse_message(elem):
    atts = elem.xpath('Attachments/MessageAttachment')
    rcpts = elem.xpath('Recepients/string')
    return {
        'recipients': [e.text for e in rcpts],
        'body': elem.findtext('Body'),
        'incoming': elem.findtext('IsIncoming') == 'true',
        'read': elem.findtext('IsRead') == 'true',
        'attachments': [parse_attachment(e) for e in atts],
        'timestamp': wtftime_to_dt(int(elem.findtext('LocalTimestamp'))),
        'sender': elem.findtext('Sender'),
        'raw': lxml.etree.tostring(elem),
    }


def parse_contact(lines):
    try:
        vcard = vobject.readOne(''.join(lines))
    except vobject.base.ParseError:
        return None

    d = {}
    for key in 'tel', 'fn', 'n', 'version':
        d[key] = [' '.join(unicode(dd.value).split())
                  for dd in vcard.contents.get(key, [])]
    return d


def parse_contacts(fp):
    lines = []
    contacts = []

    for line in fp:
        lines.append(line)
        if line.startswith('END:VCARD'):
            c = parse_contact(lines)
            if c is not None:
                contacts.append(c)
            lines = []

    return contacts


def canonical_number(s):
    try:
        pn = phonenumbers.parse(s, DEFAULT_COUNTRY_CODE)
    except phonenumbers.phonenumberutil.NumberParseException:
        return s
    return phonenumbers.format_number(pn, phonenumbers.PhoneNumberFormat.E164)


def build_contact_index(contacts):
    return {canonical_number(tel): contact
            for contact in contacts
            for tel in contact['tel']}


def format_sender(index, number):
    cn = canonical_number(number)
    name = None

    if cn in index:
        name = ''.join(index[cn]['n'])
    if not name:
        name = cn

    addr = MSGID_FORMAT % (cn.lstrip('+'),)
    return '%s <%s>' % (name, addr)


def id_for_message(message):
    h = hashlib.md5()
    stack = [('', message)]
    while stack:
        key, cur = stack.pop()
        h.update(key)
        for key in sorted(cur):
            value = cur[key]
            if isinstance(value, list):
                h.update(repr(key))
                for s in value:
                    h.update(repr(s))
            elif isinstance(value, dict):
                stack.append((key, value))
            else:
                h.update(repr(key))
                h.update(repr(value))

    return '<%s>' % (MSGID_FORMAT % (h.hexdigest(),),)


def attachment_to_part(attch, filename):
    basetype, subtype = attch['content_type'].split('/', 1)
    a = email.mime.base.MIMEBase(basetype, subtype)
    a['Content-Disposition'] = 'attachment; filename="%s"' % (filename,)
    a.set_payload(attch['data'])
    email.encoders.encode_base64(a)
    return a


def message_to_email(index, message):
    if message['incoming']:
        send_from = format_sender(index, message['sender'])
        number = message['sender']
        to = [MY_NAME]
    else:
        send_from = '%s <%s>' % (MY_NAME, MY_EMAIL)
        number = MY_NUMBER
        to = [format_sender(index, r) for r in message['recipients']]

    subj_date = message['timestamp'].strftime('%Y-%m-%d')
    subject = 'SMS message on %s' % (subj_date,)
    header_date = email.utils.formatdate(
        dt_to_epoch(message['timestamp']),
        localtime=True)

    if message['body']:
        body = message['body']
        atts = message['attachments']
    else:
        for att in message['attachments']:
            if att['content_type'] == 'text/plain':
                body = att['data'].decode('utf-16')
                atts = message['attachments'][:]
                atts.remove(att)
                break
        else:
            body = message['body']
            atts = message['attachments']

    msg = email.mime.multipart.MIMEMultipart()
    msg['From'] = send_from
    msg['To'] = email.utils.COMMASPACE.join(to)
    msg['Date'] = header_date
    msg['Subject'] = subject

    msg.attach(email.mime.text.MIMEText(
        body.encode('utf-8'),
        _charset='utf-8'))
    for i, dct in enumerate(atts):
        ext = mimetypes.guess_extension(dct['content_type']) or '.dat'
        filename = '%s_%s_attachment%d%s' % (
            subj_date,
            canonical_number(number).lstrip('+'),
            i,
            ext,
        )
        msg.attach(attachment_to_part(dct, filename))

    raw_attachment = {
        'content_type': 'text/xml',
        'data': message['raw']
    }
    msg.attach(attachment_to_part(raw_attachment, filename='msg_raw.xml'))

    msg['Message-Id'] = id_for_message(message)
    return message['timestamp'], msg.as_string()


def open_imap_client():
    kr = keyring.get_keyring()
    while True:
        password = kr.get_password('gmail', MY_EMAIL)
        if not password:
            password = getpass.getpass('Password for %s: ' % (MY_EMAIL,))

        try:
            client = imapclient.IMAPClient('imap.gmail.com', ssl=True)
            # client.debug = 5
            client.login(MY_EMAIL, password)
            break
        except Exception as e:
            print 'Password incorrect: %s' % (e,)
            kr.set_password('gmail', MY_EMAIL, '')

    kr.set_password('gmail', MY_EMAIL, password)
    pprint.pprint(client.list_folders())
    # client.folder = r'[Google Mail]/All Mail'
    client.folder = r'SMS'
    client.select_folder(client.folder)
    return client


def sync_email(client, msgid, timestamp, email):
    uids = client.search(['HEADER', 'Message-ID', msgid])
    if uids:
        print 'deleting old', uids
        print client.delete_messages(uids)

    print client.append(folder=client.folder,
                        msg=email,
                        flags=(r'\Seen',),
                        msg_time=timestamp)


def main():
    mms_path = 'backup+restore/mmsBackup/Mon, Jan 18 2016, 22-26-44 PM.msg'
    sms_path = 'backup+restore/smsBackup/Mon, Jan 18 2016, 22-26-36 PM.msg'
    vcf_path = 'backup+restore/contactsBackup/Mon, Jan 18 2016, 22-25-58 PM.vcf'

    with open(vcf_path, 'rb') as fp:
        contacts = parse_contacts(fp)

    index = build_contact_index(contacts)
    messages = []
    for path in mms_path, sms_path:
        with open(path, 'rb') as fp:
            parser = lxml.etree.XMLParser(recover=True)
            doc = lxml.etree.parse(fp, parser=parser)
            for elem in doc.getroot():
                messages.append(parse_message(elem))

    basename = os.path.basename(sys.argv[0])
    shelve_path = os.path.expanduser('~/.%s_stored.shelve' % (basename,))
    stored = shelve.open(shelve_path)
    client = open_imap_client()
    emails = []

    for i, message in enumerate(messages):
        msgid = id_for_message(message)
        if msgid not in stored:
            print 'do', i, msgid
            sync_email(client, msgid, *message_to_email(index, message))
            stored['msgid'] = 1
            stored.sync()

if __name__ == '__main__':
    main()
