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

import syslog
import sys
sys.path.append("/var/packages/MailPlus-Server/target/scripts/")

from AddrUtil import *

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 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 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 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 decode_header(header):
    """Decode a message header value without converting charset.
    Returns a list of (decoded_string, charset) pairs containing each of the
    decoded parts of the header.  Charset is None for non-encoded parts of the
    header, otherwise a lower-case string containing the name of the character
    set specified in the encoded string.
    An email.errors.HeaderParseError may be raised when certain decoding error
    occurs (e.g. a base64 decoding exception).
    """
    import re
    import email.quoprimime

    SPACE = ' '
    ecre = re.compile(r'''
      =\?                   # literal =?
      (?P<charset>[^?]*?)   # non-greedy up to the next ? is the charset
      \?                    # literal ?
      (?P<encoding>[qb])    # either a "q" or a "b", case insensitive
      \?                    # literal ?
      (?P<encoded>.*?)      # non-greedy up to the next ?= is the encoded string
      \?=                   # literal ?=
      ''', re.VERBOSE | re.IGNORECASE | re.MULTILINE)

    # If no encoding, just return the header
    if not ecre.search(header):
        return [(header, None)]
    words = []
    first_line = True
    for line in header.splitlines():
        parts = ecre.split(line)
        first = True
        while parts:
            unencoded = parts.pop(0)
            if first:
                unencoded = unencoded.lstrip()
                #handle leading spaces after first line
                if not first_line and not unencoded:
                    unencoded = " "
                first = False
            if parts:
                if unencoded:
                    words.append((unencoded, None, None))
                charset = parts.pop(0).lower()
                encoding = parts.pop(0).lower()
                encoded = parts.pop(0)
                words.append((encoded, encoding, charset))
            else:
                #handle trailing space
                unencoded = unencoded.rstrip()
                if unencoded:
                    words.append((unencoded, None, None))
        first_line = False
    # Now loop over words and remove words that consist of whitespace
    # between two encoded strings.
    droplist = []
    for n, w in enumerate(words):
        if n>1 and w[1] and words[n-2][1] and words[n-1][0].isspace():
            droplist.append(n-1)
    for d in reversed(droplist):
        del words[d]

    # The next step is to decode each encoded word by applying the reverse
    # base64 or quopri transformation.  decoded_words is now a list of the
    # form (decoded_word, charset).
    decoded_words = []
    for encoded_string, encoding, charset in words:
        if encoding is None:
            # This is an unencoded word.
            decoded_words.append((encoded_string, charset))
        elif encoding == 'q':
            word = email.quoprimime.header_decode(encoded_string.encode('utf8'))
            decoded_words.append((word, charset))
        elif encoding == 'b':
            paderr = len(encoded_string) % 4   # Postel's law: add missing padding
            if paderr:
                encoded_string += '==='[:4 - paderr]
            try:
                word = email.base64mime.decode(encoded_string)
            except binascii.Error:
                raise HeaderParseError('Base64 decoding error')
            else:
                decoded_words.append((word, charset))
        else:
            raise AssertionError('Unexpected encoding: ' + encoding)
    # Now convert all words to bytes and collapse consecutive runs of
    # similarly encoded words.
    collapsed = []
    last_word = last_charset = None
    for word, charset in decoded_words:
        if last_word is None:
            last_word = word
            last_charset = charset
        elif charset != last_charset:
            collapsed.append((last_word, last_charset))
            last_word = word
            last_charset = charset
        elif last_charset is None:
            last_word += SPACE + word
        else:
            last_word += word
    collapsed.append((last_word, last_charset))
    return collapsed

def convertSubject(subject_header):
    import re
    # convert consecutive space\tab in between the subject item to only one item
    internal_blank_re = re.compile('(?<!^)\s+|\s+(?!$)')
    all_subjects = list()
    if subject_header is None:
        return u''
    try:
        decoded_subjects = decode_header(subject_header)
    except:
        return subject_header.replace('\n', ' ')

    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(internal_blank_re.sub(u' ', 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)
