#! /usr/bin/env python
# -*- coding: utf-8 -*-
# Compatible with python3 and python2 (tested with at least 2.4)

# flake8: noqa
# pylint: skip-file
# Originally from https://github.com/LRGH/emlx2eml

import sys
import os
import logging
import struct
import email
import base64
import mimetypes

log = logging.getLogger("emlx2eml")
console_handler = logging.StreamHandler()
console_handler.setFormatter(logging.Formatter("%(levelname)-5s: %(message)s"))
log.addHandler(console_handler)
log.setLevel(logging.DEBUG)
log.setLevel(logging.ERROR)


def find_emlx(input):
    if os.path.islink(input):
        return []
    elif os.path.isdir(input):
        files = []
        for x in os.listdir(input):
            files += find_emlx(os.path.join(input, x))
        return files
    elif input.endswith(".emlx"):
        return [input]
    else:
        return []


# Some definitions, to enforce compatibility with python2 and python3
newline = struct.pack("B", 10)
if sys.version_info[0] == 2:
    message_from_bytes = email.message_from_string
    def message_as_bytes(msg): return msg.as_string(unixfrom=True)
else:
    message_from_bytes = email.message_from_bytes
    def message_as_bytes(msg): return msg.as_bytes(unixfrom=True)


def copy_emlx(emlx, out_dir):
    # Get the numeric id
    id = get_numeric_id(emlx)

    # Create output file
    if not os.path.exists(out_dir):
        os.mkdir(out_dir)
    eml = os.path.join(out_dir, id+".eml")
    log.debug("Extract %s to %s", emlx, eml)
    if os.path.exists(eml):
        log.error("%s already exists", eml)
        return False
    # Parse the EMLX file
    msg = parse_emlx(emlx)
    msg.set_unixfrom("From emlx2eml Thu Apr 19 00:00:00 2012")
    # TODO: generate relevant values for unixfrom
    open(eml, "wb").write(message_as_bytes(msg))


def get_numeric_id(filename):
    id = os.path.basename(filename)
    assert(id.endswith(".emlx"))
    id = id[:-5]
    if id.endswith(".partial"):
        id = id[:-8]
    return id


def parse_emlx(filename):
    # Read file
    content = open(filename, "rb").read()

    # Extract parts
    eol = content.find(newline)
    length = int(content[:eol])
    body = content[eol+1:eol+1+length]
    # TODO: parse the content of 'plist', e.g. using plistlib
    # plist = content[eol+1+length:]
    msg = message_from_bytes(body)

    # Find where attachments may be
    id = get_numeric_id(filename)
    attach_dir = os.path.dirname(filename)
    if attach_dir == "":
        attach_dir = "."
    attach_dir += "/../Attachments/" + id

    # Make complete eml
    parse_msg(attach_dir, msg, [])
    return msg


def parse_msg(attach_dir, msg, depth):
    log.debug("%sPART %s %r of type %s", " "*len(depth),
              ".".join([str(_+1) for _ in depth]), msg, msg.get_content_type())
    if msg.is_multipart():
        for idx, part in enumerate(msg.get_payload()):
            parse_msg(attach_dir, part, depth+[idx])
            include_attachment(attach_dir, part, depth+[idx])


# When the attachment has no explicit filename, Mail.app generates a name
# which we want to guess. The base_filename depends on the OS language at
# the time the mail was downloaded. The list below is extracted by parsing
# /System/Library/PrivateFrameworks/Notes.framework/Versions/A/Resources/*.lproj/MailCore.strings
base_filenames = (
    u"مرفق البريد",  # ar
    u"Adjunt de Mail",  # ca
    u"Příloha pošty",  # cs
    u"Postbilag",  # da
    u"Mail-Anhang",  # de
    u"Συνημμένο Mail",  # el
    u"Mail Attachment",  # en, en_AU, en_GB
    u"Archivo adjunto al mensaje",  # es
    u"Archivo adjunto a un correo",  # es_419
    u"Sähköpostiliite",  # fi
    u"Pièce jointe",  # fr, fr_CA
    u"קובץ מצורף לדואר",  # he
    u"मेल अटैचमेंट",  # hi
    u"E-mail privitak",  # hr
    u"Mail melléklet",  # hu
    u"Lampiran Mail",  # id
    u"Allegato di posta elettronica",  # it
    u"メールの添付ファイル",  # ja
    u"Mail 첨부 파일",  # ko
    u"Lampiran Mail",  # ms
    u"Mail-bijlage",  # nl
    u"E-postvedlegg",  # no
    u"Załącznik poczty",  # pl
    u"Anexo de E-mail",  # pt
    u"Anexo de e‑mail",  # pt_PT
    u"Fișier atașat Mail",  # ro
    u"Вложенный файл Почты",  # ru
    u"Mailová príloha",  # sk
    u"Brevbilaga",  # sv
    u"ไฟล์แนบเมล",  # th
    u"Posta İlişiği",  # tr
    u"Поштове прикріплення",  # uk
    u"Tệp đính kèm của Mail",  # vi
    u"邮件附件",  # yue_CN, zh_CN,
    u"郵件附件",  # zh_HK, zh_TW
)


def mimetypes_guess_extension(mime_type):
    # We don't want to always use mimetypes.guess_extension,
    # because it does not always return what is generated by Mail.app,
    # mainly because multiple extensions can be associated to a single
    # MIME type.
    # We prefer to use a hardcoded table.
    try:
        return {
            "text/calendar":  u".ics",
            "image/png":      u".png",
            "image/x-png":    u"",
            "image/gif":      u".gif",
            "image/jpeg":     u".jpeg",
            "image/pjpeg":    u".jpg",
            "image/jpg":      u".jpg",
            "message/rfc822": u".eml",
        }[mime_type]
    except KeyError:
        log.error("Unknown file extension for %r, making a guess...",
                  mime_type)
        return mimetypes.guess_extension(mime_type)


def include_attachment(attach_dir, part, depth):
    if "X-Apple-Content-Length" not in part:
        return
    dirpath = attach_dir + "/" + ".".join([str(_+1) for _ in depth])
    file = part.get_filename()
    if file is None:
        extension = mimetypes_guess_extension(part.get_content_type())
        for base in base_filenames:
            file = base + extension
            try:
                data = open(dirpath+"/"+file, "rb").read()
                break
            # python2 raises IOError, python3 raises FileNotFoundError
            except (IOError, FileNotFoundError):
                continue
        else:
            log.error("%s  Unnamed attachment of extension %s not found in %s",
                      " "*len(depth), extension, dirpath)
            return
    else:
        try:
            data = open(dirpath+"/"+file, "rb").read()
        except FileNotFoundError:
            log.error("%s  Attachment '%s' not found in %s",
                      " "*len(depth), file, dirpath)
            return
    log.debug("%s  Attachment '%s' found", " "*len(depth), file)
    cte = part["Content-Transfer-Encoding"]
    if cte is None:
        pass
    elif cte == "base64":
        data = base64.b64encode(data)
        data = newline.join([data[i*76:(i+1)*76]
                             for i in range(len(data)//76+1)])
    elif cte == "quoted-printable":
        # The only example I found was not QP-encoded
        pass
    elif cte == "8bit":
        pass
    else:
        log.error("Attachment dir is %s", attach_dir)
        log.error("  File name is %s", file)
        log.error("  CTE %r", cte)
        log.error("  CD  %r", part["Content-Disposition"])
    part.set_payload(data)


if __name__ == "__main__":
    try:
        input, out_dir = sys.argv[1:]
    except ValueError:
        print("Syntax: emlx2eml.py <source> <output_dir>")
        print("    <source> can be an EMLX file, or a directory that will")
        print("    be recursively searched for EMLX files.")
        sys.exit(1)
    log.debug("Input %s; Output %s", input, out_dir)
    for emlx in find_emlx(input):
        copy_emlx(emlx, out_dir)
