File: /volume1/@appstore/MailPlus-Server/bin/vacation
#!/usr/bin/env python3
# --*-- coding:utf8 --*--
import binascii
import syslog
import sys
from email.errors import HeaderParseError
sys.path.append("/var/packages/MailPlus-Server/target/scripts/common/")
from addr_util import *
from send_mail import send_mail
debug = False
#reply once in a week
reply_interval = 86400 * 7
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 as gdbm
last_reply_time = 0
db = None
try:
if os.path.isfile(db_path):
try:
db = gdbm.open(db_path, 'ru', 0o644)
except gdbm.error as e:
if str(e) == 'Malformed database file header':
os.unlink(db_path)
db = gdbm.open(db_path, 'ru', 0o644)
else:
raise
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('Failed to get record [{0}] in db [{1}], error [{2}]'.format(sender, 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 as gdbm
db = None
try:
db = gdbm.open(db_path, 'cu', 0o644)
key = sender + '/' + matched_addr
db[key] = '{0}'.format(time)
except Exception as e:
if debug:
writeLog('Failed to set record [{0}] in db [{1}], error [{2}]'.format(sender, 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])
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, '{0}@{1}'.format(username, main_domain.encode('idna').decode('utf8'))
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('.{0}.msg'.format(punycode_address))
possible_files.append('.{0}.msg'.format(eai_address))
possible_files.append('.{0}.msg'.format(getDomain(punycode_address)))
possible_files.append('.{0}.msg'.format(getDomain(eai_address)))
else:
punycode_address = converToPunycodeAddr(sender_addr)
eai_address = sender_addr
possible_files.append('.{0}.msg'.format(eai_address))
possible_files.append('.{0}.msg'.format(punycode_address))
possible_files.append('.{0}.msg'.format(getDomain(eai_address)))
possible_files.append('.{0}.msg'.format(getDomain(punycode_address)))
possible_files.append('.vacation.msg')
for one_file in possible_files:
file_path = os.path.join(maildir_path, one_file)
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):
import re
from email.header import decode_header
# 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 ''
try:
decoded_subjects = decode_header(subject_header)
except:
return subject_header.replace('\n', ' ').replace('\r', '')
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(' ', subject_item[0]))
return ''.join(all_subjects)
def sendReply(sender, matched_addr, ori_subject, reply_file, db_path):
import time
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')
subject = subject.replace('$SUBJECT', ori_subject).replace('$FROM', eai_sender)
found_subject = True
continue
line = line.replace('$SUBJECT', ori_subject).replace('$FROM', eai_sender)
msg_content += line
try:
send_mail(
subject=subject,
msg=msg_content,
sender=matched_addr,
recipients=sender
)
setRelpyTime(db_path, sender, matched_addr, 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]
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]
maildir_path = getMaildirPath(getFullUsername(username))
if not maildir_path.startswith('/var/spool/mail'):
if debug:
writeLog('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, matched_addr)
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))
if debug:
writeLog('"{0}" fails, error [{1}]'.format(' '.join(sys.argv), e))
exit(1)