#!/usr/bin/env python # -*- coding: utf-8 -*- import os import os.path import ConfigParser import string import logging import sys import subprocess import time import socket import rfc822 from stat import ST_MTIME from email.Utils import parseaddr from email.Parser import Parser if 'ERESSEA' in os.environ: dir = os.environ['ERESSEA'] elif 'HOME' in os.environ: dir = os.path.join(os.environ['HOME'], 'eressea') else: # WTF? No HOME? dir = "/home/eressea/eressea" if not os.path.isdir(dir): print "please set the ERESSEA environment variable to the install path" sys.exit(1) rootdir = dir game = int(sys.argv[1]) gamedir = os.path.join(rootdir, "game-%d" % (game, )) frommail = 'eressea-server@kn-bremen.de' gamename = 'Eressea' sender = '%s Server <%s>' % (gamename, frommail) inifile = os.path.join(gamedir, 'eressea.ini') if not os.path.exists(inifile): print "no such file: " . inifile else: config = ConfigParser.ConfigParser() config.read(inifile) if config.has_option('game', 'email'): frommail = config.get('game', 'email') if config.has_option('game', 'name'): gamename = config.get('game', 'name') if config.has_option('game', 'sender'): sender = config.get('game', 'sender') else: sender = "%s Server <%s>" % (gamename, frommail) config = None prefix = 'turn-' hostname = socket.gethostname() orderbase = "orders.dir" sendmail = True # maximum number of reports per sender: maxfiles = 30 # write headers to file? writeheaders = True # write received files to database? tooldir = os.path.join(rootdir, 'orders-php') writedb = os.path.exists(tooldir) # reject all html email? rejecthtml = True messages = { "multipart-en" : "ERROR: The orders you sent contain no plaintext. " \ "The Eressea server cannot process orders containing HTML " \ "or invalid attachments, which are the reasons why this " \ "usually happens. Please change the settings of your mail " \ "software and re-send the orders.", "multipart-de" : "FEHLER: Die von dir eingeschickte Mail enthält keinen " \ "Text. Evtl. hast Du den Zug als HTML oder als anderweitig " \ "ungültig formatierte Mail ingeschickt. Wir können ihn " \ "deshalb nicht berücksichtigen. Schicke den Zug nochmals " \ "als reinen Text ohne Formatierungen ein.", "maildate-de": "Es erreichte uns bereits ein Zug mit einem späteren " \ "Absendedatum (%s > %s). Entweder ist deine " \ "Systemzeit verstellt, oder ein Zug hat einen anderen Zug von " \ "dir auf dem Transportweg überholt. Entscheidend für die " \ "Auswertungsreihenfolge ist das Absendedatum, d.h. der Date:-Header " \ "deiner Mail.", "maildate-en": "The server already received an order file that was sent at a later " \ "date (%s > %s). Either your system clock is wrong, or two messages have " \ "overtaken each other on the way to the server. The order of " \ "execution on the server is always according to the Date: header in " \ "your mail.", "nodate-en": "Your message did not contain a valid Date: header in accordance with RFC2822.", "nodate-de": "Deine Nachricht enthielt keinen gueltigen Date: header nach RFC2822.", "error-de": "Fehler", "error-en": "Error", "warning-de": "Warnung", "warning-en": "Warning", "subject-de": "Befehle angekommen", "subject-en": "orders received" } # return 1 if addr is a valid email address def valid_email(addr): rfc822_specials = '/()<>@,;:\\"[]' # First we validate the name portion (name@domain) c = 0 while c < len(addr): if addr[c] == '"' and (not c or addr[c - 1] == '.' or addr[c - 1] == '"'): c = c + 1 while c < len(addr): if addr[c] == '"': break if addr[c] == '\\' and addr[c + 1] == ' ': c = c + 2 continue if ord(addr[c]) < 32 or ord(addr[c]) >= 127: return 0 c = c + 1 else: return 0 if addr[c] == '@': break if addr[c] != '.': return 0 c = c + 1 continue if addr[c] == '@': break if ord(addr[c]) <= 32 or ord(addr[c]) >= 127: return 0 if addr[c] in rfc822_specials: return 0 c = c + 1 if not c or addr[c - 1] == '.': return 0 # Next we validate the domain portion (name@domain) domain = c = c + 1 if domain >= len(addr): return 0 count = 0 while c < len(addr): if addr[c] == '.': if c == domain or addr[c - 1] == '.': return 0 count = count + 1 if ord(addr[c]) <= 32 or ord(addr[c]) >= 127: return 0 if addr[c] in rfc822_specials: return 0 c = c + 1 return count >= 1 # return the replyto or from address in the header def get_sender(header): replyto = header.get("Reply-To") if replyto is None: replyto = header.get("From") if replyto is None: return None x = parseaddr(replyto) return x[1] # return first available filename basename,[0-9]+ def available_file(dirname, basename): ver = 0 maxdate = 0 filename = "%s/%s,%s,%d" % (dirname, basename, hostname, ver) while os.path.exists(filename): maxdate = max(os.stat(filename)[ST_MTIME], maxdate) ver = ver + 1 filename = "%s/%s,%s,%d" % (dirname, basename, hostname, ver) if ver >= maxfiles: return None, None return maxdate, filename def formatpar(string, l=76, indent=2): words = string.split(string) res = "" ll = 0 first = 1 for word in words: if first == 1: res = word first = 0 ll = len(word) else: if ll + len(word) > l: res = res + "\n"+" "*indent+word ll = len(word) + indent else: res = res+" "+word ll = ll + len(word) + 1 return res+"\n" def store_message(message, filename): outfile = open(filename, "w") outfile.write(message.as_string()) outfile.close() return def write_part(outfile, part): charset = part.get_content_charset() payload = part.get_payload(decode=True) if charset is None: charset = "latin1" try: msg = payload.decode(charset, "ignore") except: msg = payload charset = None try: utf8 = msg.encode("utf-8", "ignore") outfile.write(utf8) except: outfile.write(msg) return False outfile.write("\n"); return True def copy_orders(message, filename, sender, mtime): # print the header first dirname, basename = os.path.split(filename) if writeheaders: header_dir = dirname + '/headers' if not os.path.exists(header_dir): os.mkdir(header_dir) outfile = open(header_dir + '/' + basename, "w") for name, value in message.items(): outfile.write(name + ": " + value + "\n") outfile.close() found = False outfile = open(filename, "w") if message.is_multipart(): for part in message.get_payload(): content_type = part.get_content_type() logger.debug("found content type %s for %s" % (content_type, sender)) if content_type=="text/plain": if write_part(outfile, part): found = True else: charset = part.get_content_charset() logger.error("could not write text/plain part (charset=%s) for %s" % (charset, sender)) else: if write_part(outfile, message): found = True else: charset = message.get_content_charset() logger.error("could not write text/plain message (charset=%s) for %s" % (charset, sender)) outfile.close() return found # create a file, containing: # game=0 locale=de file=/path/to/filename email=rcpt@domain.to def accept(game, locale, stream, extend=None): global rootdir, orderbase, gamedir, gamename, sender if extend is not None: orderbase = orderbase + ".pre-" + extend savedir = os.path.join(gamedir, orderbase) # check if it's one of the pre-sent orders. # create the save-directories if they don't exist if not os.path.exists(gamedir): os.mkdir(gamedir) if not os.path.exists(savedir): os.mkdir(savedir) # parse message message = Parser().parse(stream) email = get_sender(message) logger = logging.getLogger(email) # write syslog if email is None or valid_email(email)==0: logger.warning("invalid email address: " + str(email)) return -1 logger.info("received orders from " + email) # get an available filename maxdate, filename = available_file(savedir, prefix + email) if filename is None: logger.warning("more than " + str(maxfiles) + " orders from " + email) return -1 # copy the orders to the file turndate = None maildate = message.get("Date") if maildate is None: turndate = time.time() else: turndate = rfc822.mktime_tz(rfc822.parsedate_tz(maildate)) text_ok = copy_orders(message, filename, email, turndate) warning, msg, fail = None, "", False if not maildate is None: os.utime(filename, (turndate, turndate)) logger.debug("mail date is '%s' (%d)" % (maildate, turndate)) if False and turndate < maxdate: logger.warning("inconsistent message date " + email) warning = " (" + messages["warning-" + locale] + ")" msg = msg + formatpar(messages["maildate-" + locale] % (time.ctime(maxdate), time.ctime(turndate)), 76, 2) + "\n" else: logger.warning("missing message date " + email) warning = " (" + messages["warning-" + locale] + ")" msg = msg + formatpar(messages["nodate-" + locale], 76, 2) + "\n" if writedb: dirname, basename = os.path.split(filename) cli = os.path.join(tooldir, 'cli.php'); dbname = os.path.join(dirname, 'orders.db') subprocess.call(['php', cli, '-d', dbname, 'insert', basename, email]) if not text_ok: warning = " (" + messages["error-" + locale] + ")" msg = msg + formatpar(messages["multipart-" + locale], 76, 2) + "\n" logger.warning("rejected - no text/plain in orders from " + email) os.unlink(filename) savedir = savedir + "/rejected" if not os.path.exists(savedir): os.mkdir(savedir) maxdate, filename = available_file(savedir, prefix + email) store_message(message, filename) fail = True if sendmail and warning is not None: subject = gamename + " " + messages["subject-"+locale] + warning print("mail " + subject) ps = subprocess.Popen(['mutt', '-s', subject, email], stdin=subprocess.PIPE) ps.communicate(msg) if not sendmail: print text_ok, fail, email print filename if not fail: queue = open(gamedir + "/orders.queue", "a") queue.write("email=%s file=%s locale=%s game=%s\n" % (email, filename, locale, game)) queue.close() logger.info("done - accepted orders from " + email) return 0 # the main body of the script: try: os.mkdir(os.path.join(rootdir, 'log')) except: pass # already exists? LOG_FILENAME=os.path.join(rootdir, 'log/orders.log') logging.basicConfig(level=logging.DEBUG, filename=LOG_FILENAME) logger = logging delay = None # TODO: parse the turn delay locale = sys.argv[2] infile = sys.stdin if len(sys.argv)>3: infile = open(sys.argv[3], "r") retval = accept(game, locale, infile, delay) if infile!=sys.stdin: infile.close() sys.exit(retval)