server/process/orders-accept
2017-01-20 19:43:53 +01:00

374 lines
12 KiB
Python
Executable file
Raw Blame History

#!/usr/bin/env python
# -*- coding: iso-8859-1 -*-
from email.Utils import parseaddr
from email.Parser import Parser
import os
import os.path
import ConfigParser
from re import compile, IGNORECASE
from stat import ST_MTIME
from string import upper, split, replace
import logging
import sys
from sys import stdin
from time import ctime, sleep, time
from socket import gethostname
from rfc822 import parsedate_tz, mktime_tz
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 = gethostname()
orderbase = "orders.dir"
sendmail = True
# maximum number of reports per sender:
maxfiles = 20
# write headers to file?
writeheaders = True
# reject all html email?
rejecthtml = True
def unlock_file(filename):
try:
os.unlink(filename+".lock")
except:
print "could not unlock %s.lock, file not found" % filename
def lock_file(filename):
i = 0
wait = 1
if not os.path.exists(filename):
file=open(filename, "w")
file.close()
while True:
try:
os.symlink(filename, filename+".lock")
return
except:
i = i+1
if i == 5: unlock_file(filename)
sleep(wait)
wait = wait*2
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<74>lt keinen " \
"Text. Evtl. hast Du den Zug als HTML oder als anderweitig " \
"ung<EFBFBD>ltig formatierte Mail ingeschickt. Wir k<>nnen ihn " \
"deshalb nicht ber<65>cksichtigen. Schicke den Zug nochmals " \
"als reinen Text ohne Formatierungen ein.",
"maildate-de":
"Es erreichte uns bereits ein Zug mit einem sp<73>teren " \
"Absendedatum (%s > %s). Entweder ist deine " \
"Systemzeit verstellt, oder ein Zug hat einen anderen Zug von " \
"dir auf dem Transportweg <20>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 = 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):
# print the header first
if writeheaders:
from os.path import split
dirname, basename = split(filename)
dirname = dirname + '/headers'
if not os.path.exists(dirname): os.mkdir(dirname)
outfile = open(dirname + '/' + 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
lock_file(gamedir + "/orders.queue")
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
text_ok = copy_orders(message, filename, email)
unlock_file(gamedir + "/orders.queue")
warning, msg, fail = None, "", False
maildate = message.get("Date")
if maildate != None:
turndate = mktime_tz(parsedate_tz(maildate))
os.utime(filename, (turndate, turndate))
logger.debug("mail date is '%s' (%d)" % (maildate, turndate))
if turndate < maxdate:
logger.warning("inconsistent message date " + email)
warning = " (" + messages["warning-" + locale] + ")"
msg = msg + formatpar(messages["maildate-" + locale] % (ctime(maxdate),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 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)
lock_file(gamedir + "/orders.queue")
maxdate, filename = available_file(savedir, prefix + email)
store_message(message, filename)
unlock_file(gamedir + "/orders.queue")
fail = True
if sendmail and warning is not None:
subject = gamename + " " + messages["subject-"+locale] + warning
mail = "Subject: %s\nFrom: %s\nTo: %s\n\n" % (subject, sender, email) + msg
from smtplib import SMTP
server = SMTP("localhost")
server.sendmail(sender, email, mail)
server.close()
if not sendmail:
print text_ok, fail, email
print filename
if not fail:
lock_file(gamedir + "/orders.queue")
queue = open(gamedir + "/orders.queue", "a")
queue.write("email=%s file=%s locale=%s game=%s\n" % (email, filename, locale, game))
queue.close()
unlock_file(gamedir + "/orders.queue")
logger.info("done - accepted orders from " + email)
return 0
# the main body of the script:
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 = stdin
if len(sys.argv)>3:
infile = open(sys.argv[3], "r")
retval = accept(game, locale, infile, delay)
if infile!=stdin:
infile.close()
sys.exit(retval)