#!/usr/bin/env python
# --*-- coding:utf8 --*--

import syslog

debug = False
#reply once in a week
reply_interval = 86400 * 7

def writeLog(log_message, priority=syslog.LOG_ERR):
    syslog.syslog(priority, log_message.encode('utf8'))

def parseArgs():
    import optparse

    parser = optparse.OptionParser(usage='%prog [options] username')
    parser.add_option('-b', '--begin_time', dest='begin_time',
                    type=str, help='start time of auto-reply')
    parser.add_option('-e', '--end_time', dest='end_time',
                    type=str, help='end time of auto-reply')
    parser.add_option('-m', '--no_match', dest='no_match',
                    action='store_true', default=False,
                    help='do not match receivere in To: CC: header')
    parser.add_option('-d', '--debug', dest='debug',
                    action='store_true', default=False,
                    help='output debug message')
    options, args = parser.parse_args()
    if len(args) != 1:
        parser.print_usage()
        exit(0)
    return options, args

def parseTime(time_str):
    import time
    try:
        return time.strptime(time_str, '%Y/%m/%d-%H:%M:%S')
    except:
        pass
    try:
        return time.strptime(time_str, '%Y/%m/%d')
    except:
        if debug:
            writeLog('Failed to pares time string [{0}]'.format(time_str))
        return None

def checkInTimeRange(begin_time, end_time):
    import time
    now_time = time.localtime()
    if begin_time is not None:
        begin_time = parseTime(begin_time)
        if begin_time is not None and begin_time > now_time:
            if debug:
                writeLog('Time now is before start time')
            exit(0)
    if end_time is not None:
        end_time = parseTime(end_time)
        if end_time is not None and end_time < now_time:
            if debug:
                writeLog('Time now is after end time')
            exit(0)

def parseMail():
    import sys
    import email.parser as parser
    from email.utils import getaddresses

    try:
        mail_parser = parser.FeedParser()
        for line in sys.stdin:
            if len(line.strip()) == 0:
                break
            mail_parser.feed(line)
        mail = mail_parser.close()
        froms = mail.get_all('from', [])
        tos = mail.get_all('to', [])
        ccs = mail.get_all('cc', [])
        all_senders = getaddresses(froms)
        all_recipients = getaddresses(tos + ccs)
        subject = convertSubject(mail['subject'])
    except Exception as e:
        if debug:
            writeLog('Failed to parse message, error [{0}]'.format(e))
        return [], [], ''

    return all_senders, all_recipients, subject

def execCommand(args):
    from subprocess import Popen, STDOUT, PIPE

    try:
        process = Popen(args, stdout=PIPE, stderr=STDOUT)
        output = process.stdout.read().strip()
        process.communicate()
        ret = process.returncode == 0
        return output, ret
    except:
        if debug:
            writeLog(u'Failed to execute command:{0}'.format(u' '.join(args)))
        return '', False

def getMaildirPath(fullUsername):
    args = ['/var/packages/MailPlus-Server/target/bin/syno_mailserver_backend', '--getMailDir' , fullUsername]
    return execCommand(args)[0]

def mailconfGet(key):
    args = ['/var/packages/MailPlus-Server/target/bin/syno_mailserver_backend', '--getConfKeyVal' , key]
    return execCommand(args)[0]

def getFullUsername(username):
    args = ['/var/packages/MailPlus-Server/target/bin/syno_mailserver_backend', '--getFullUsername', username.encode('utf8')]
    return execCommand(args)[0]

def getUserAddress(fullUsername):
    # Check if we can use set for performance issue
    punycode_address = []
    args = ['/var/packages/MailPlus-Server/target/bin/syno_multiple_domains', 'get_all_mail_addrs', fullUsername]
    for addr in execCommand(args)[0].split('\n'):
        punycode_address.append(converToPunycodeAddr(addr.decode('utf8')).lower())

    return punycode_address

def getLastReplyTime(db_path, sender, matched_addr):
    import os
    import gdbm

    last_reply_time = 0
    db = None
    try:
        if os.path.isfile(db_path):
            db = gdbm.open(db_path, 'ru', 0644)
            key = sender + '/' + matched_addr
            if key in db:
                try:
                    last_reply_time = int(db[key])
                except:
                    last_reply_time = 0
    except Exception as e:
        if debug:
            writeLog(u'Failed to get record [{0}] in db [{1}], error [{2}]'.format(sender.decode('utf8'), db_path, e))
    finally:
        if db is not None:
            db.close()
    return last_reply_time

def setRelpyTime(db_path, sender, matched_addr, time):
    import gdbm
    db = None
    try:
        db = gdbm.open(db_path, 'cu', 0644)
        key = sender + '/' + matched_addr
        db[key] = '{0}'.format(time)
    except Exception as e:
        if debug:
            writeLog(u'Failed to set record [{0}] in db [{1}], error [{2}]'.format(sender.decode('utf8'), db_path, e))
    finally:
        if db is not None:
            db.close()

def checkMatch(username, sender, all_recipients):
    own_addresses = getUserAddress(getFullUsername(username))

    punycode_sender = converToPunycodeAddr(sender)
    if punycode_sender in own_addresses:
        if debug:
            writeLog('We do not do auto-reply to ourself')
        exit(0)
    for recipient in all_recipients:
        punycode_addr = converToPunycodeAddr(recipient[1].decode('utf8'))
        if punycode_addr.lower() in own_addresses:
            #We use punycode address to do auto-reply
            return True, punycode_addr
    main_domain = mailconfGet('smtp_main_domain')
    return False, u'{0}@{1}'.format(username, main_domain.decode('utf8').encode('idna'))

def isAsciiString(s):
    return all(ord(c) < 128 for c in s)

def getDomain(addr):
    at_idx = addr.rfind('@')
    if at_idx == -1:
        return addr
    return addr[at_idx + 1:]

def converToPunycodeAddr(addr):
    at_idx = addr.rfind('@')
    if at_idx == -1:
        return addr
    local_part = addr[:at_idx]
    domain_part = addr[at_idx + 1:]
    return u'{0}@{1}'.format(local_part, domain_part.encode('idna'))

def converToEaiAddr(addr):
    at_idx = addr.rfind('@')
    if at_idx == -1:
        return addr
    local_part = addr[:at_idx]
    domain_part = addr[at_idx + 1:]
    return u'{0}@{1}'.format(local_part, domain_part.decode('idna'))

def findReplyFile(maildir_path, sender_addr):
    import os
    possible_files = list()
    if len(maildir_path) == 0:
        if debug:
            writeLog('Failed to get maildir path')
        exit(0)
    if isAsciiString(getDomain(sender_addr)):
        punycode_address = sender_addr
        eai_address = converToEaiAddr(sender_addr)
        possible_files.append(u'.{0}.msg'.format(punycode_address))
        possible_files.append(u'.{0}.msg'.format(eai_address))
        possible_files.append(u'.{0}.msg'.format(getDomain(punycode_address)))
        possible_files.append(u'.{0}.msg'.format(getDomain(eai_address)))
    else:
        punycode_address = converToPunycodeAddr(sender_addr)
        eai_address = sender_addr
        possible_files.append(u'.{0}.msg'.format(eai_address))
        possible_files.append(u'.{0}.msg'.format(punycode_address))
        possible_files.append(u'.{0}.msg'.format(getDomain(eai_address)))
        possible_files.append(u'.{0}.msg'.format(getDomain(punycode_address)))

    possible_files.append(u'.vacation.msg')

    for one_file in possible_files:
        file_path = os.path.join(maildir_path, one_file).encode('utf8')
        if os.path.isfile(file_path):
            return file_path

    if debug:
        writeLog('Theew is no auto-reply message files')
    exit(0)

def convertSubject(subject_header):
    from email.header import decode_header
    all_subjects = list()
    if subject_header is None:
        return ''
    decoded_subjects = decode_header(subject_header)
    for subject_item in decoded_subjects:
        if subject_item[1] is not None:
            all_subjects.append(subject_item[0].decode(subject_item[1]))
        else:
            all_subjects.append(subject_item[0].decode('utf8'))
    return u''.join(all_subjects)

def sendReply(sender, matched_addr, ori_subject, reply_file, db_path):
    import time
    from email.MIMEText import MIMEText
    from email import Utils, Charset
    import subprocess

    msg_content = ''
    subject = ''
    found_subject = False

    #covert sender to eai address unconditionally
    if isAsciiString(getDomain(sender)):
        eai_sender = converToEaiAddr(sender)
    else:
        eai_sender = sender

    with open(reply_file, 'r') as in_f:
        for line in in_f:
            if not found_subject:
                if line.startswith('Subject:'):
                    subject = line[len('Subject:'):].rstrip('\n').decode('utf8')
                    subject = subject.replace('$SUBJECT', ori_subject).replace('$FROM', eai_sender)
                    found_subject = True
                continue
            line = line.decode('utf8')
            line = line.replace('$SUBJECT', ori_subject).replace('$FROM', eai_sender)
            msg_content += line
    text_subtype = 'plain'
    try:
        Charset.add_charset('utf-8', Charset.QP, Charset.QP, 'utf-8')
        msg = MIMEText(msg_content.encode('utf8'), text_subtype, 'utf8')
        msg['From'] = matched_addr.encode('utf8')
        msg['To'] = sender.encode('utf8')
        msg['Subject'] = subject
        msg['Message-ID'] = Utils.make_msgid()

        args = ['/var/packages/MailPlus-Server/target/sbin/sendmail', '-t', '-f', matched_addr.encode('utf8')]
        sp = subprocess.Popen(args, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
        sp.communicate(msg.as_string())
        sp.wait()
        setRelpyTime(db_path, sender.encode('utf8'), matched_addr.encode('utf8'), int(time.time()))
    except:
        if debug:
            writeLog('Failed to send reply message')

def main():
    import os
    import time

    global debug
    options, args = parseArgs()
    debug = options.debug
    no_match = options.no_match
    matched = False
    matched_addr = ''
    username = args[0].decode('utf8')

    if debug:
        syslog.openlog(os.path.basename(__file__), syslog.LOG_PID, syslog.LOG_MAIL)

    checkInTimeRange(options.begin_time, options.end_time)

    all_senders, all_recipients, subject = parseMail()
    if len(all_senders) == 0:
        if debug:
            writeLog('There is no From: header in message')
        exit(0)

    #we only reply to the first address in From: header
    sender = all_senders[0][1].decode('utf8')

    maildir_path = getMaildirPath(getFullUsername(username))
    if not maildir_path.startswith('/var/spool/mail'):
        if debug:
            writeLog(u'The maildir of user [{0}] does not exist'.format(username))
        exit(0)

    db_path = os.path.join(maildir_path, '.vacation.db')
    matched, matched_addr = checkMatch(username, sender, all_recipients)
    if no_match:
        matched = True
    if not matched:
        if debug:
            writeLog('User is not listed in To: or CC: headers')
        exit(0)

    last_reply_time = getLastReplyTime(db_path, sender.encode('utf8'), matched_addr.encode('utf8'))
    if time.time() < last_reply_time + reply_interval:
        if debug:
            writeLog('You do not need to reply')
        exit(0)

    reply_file = findReplyFile(maildir_path, sender)
    sendReply(sender, matched_addr, subject, reply_file, db_path)

if __name__ == '__main__':
    try:
        main()
    except Exception as e:
        import sys
        sys.stderr.write('"{0}" fails, error [{1}]\n'.format(' '.join(sys.argv), e))
        exit(0)
