"""A SMTP daemon class.""" import string import re import socket import smtplib from SocketServer import * from nettools import * # delimiters LF = '\n' CRLF = '\r\n' # patterns email = r'\s*<?([^\s>]+)>?' from_pat = re.compile('FROM:' + email, re.I) to_pat = re.compile('TO:' + email, re.I) del email class SmtpHandler(StreamRequestHandler): """handles one SMTP session""" version = 'PySMTPD 1.0' helptext = """\ HELP HELO hostname MAIL FROM:<address> RCPT TO:<address> DATA RSET NOOP QUIT Commands are not case sensitive. """ msg_dict = { 214: helptext, 220: '%s ' + version + ' ready', 221: '%s ' + version + ' closing', 250: '%s', 251: 'User not local, will forward to %s', 354: 'Send mail message; end with <CRLF>.<CRLF>', 421: 'Service not available; %s', 500: 'Syntax error in command %s', 501: 'Syntax error in parameter; %s', 502: 'Command %s is not implemented, see HELP', 503: 'Bad command sequence; %s', 550: 'Mailbox unavailable', 551: 'Address refused: %s', 552: 'Maximum message size exceeded', } def puts(self, line): print line if line[-1] != LF: line = line + LF self.wfile.write(string.replace(line, LF, CRLF)) def gets(self): try: line = self.rfile.readline() except EOFError: # socket closed? return None if not line: return None print line, if line[-2:] == CRLF: return line[:-2] if line[-1] in CRLF: return line[:-1] return line def reply(self, code, text): code = `code` if LF in text: # handle multiline responses code = code + '-' text = string.replace(text, LF, LF + code) lastdash = string.rfind(text, LF) + 4 text = text[:lastdash] + ' ' + text[lastdash + 1:] else: code = code + ' ' self.puts(code + text) def reply_msg(self, code, text = None): msg = self.msg_dict[code] if text is not None: msg = msg % text self.reply(code, msg) return 0 # indicates problem/error/failure def reply_ok(self, text = 'OK'): self.reply_msg(250, text) return 1 # indicates success def reply_rc(self, rc): # rc may be a tuple (code, msg) if type(rc) == type(()): self.reply_msg(rc[0], rc[1]) elif type(rc) == type(''): self.reply_ok(rc) else: self.reply_ok() def handle(self): self.client_ip = self.client_address[0] self.client_host = hostname(self.client_ip) print 'Serving', self.client_host self.localhost = self.server.localhost self.sender_host = '' self.rset() self.reply_msg(220, self.localhost) self.running = 1 while self.running: req = self.gets() if req is None: print 'Connection broken' break args = string.split(req, None, 1) if len(args) == 1: cmd, self.parm = args[0], '' else: [cmd, self.parm] = args attr = 'handle_' + string.upper(cmd) if hasattr(self, attr): rc = getattr(self, attr)() if rc is not None: self.reply_rc(rc) else: self.reply_msg(500, cmd) print 'Done with', self.client_host def handle_HELO(self): if self.sender_host: return (503, 'HELO received already') if not self.server.valid_host(self.parm, self.client_address): return (501, 'wrong mail domain') self.sender_host = self.parm if not self.server.connected(): return (421, 'gateway not online') if self.parm != self.client_host: return 'Spoofing "%s"?' % self.client_host return 'Hello, ' + self.client_host def handle_QUIT(self): self.running = 0 return (221, self.localhost) def handle_EXITSERVER(self): # hidden command: terminates the server self.server.stop() return self.handle_QUIT() def handle_NOOP(self): return 1 def handle_HELP(self): self.reply_msg(214) def handle_MAIL(self): if not self.sender_host: return (503, 'HELO expected') m = from_pat.match(self.parm) if not m: return (501, 'MAIL FROM:<sender> expected') sender = m.group(1) if not sender: return (501, self.parm) if not self.server.valid_sender(sender): return (501, sender) self.sender = sender return 1 def handle_RCPT(self): if not self.sender_host: return (503, 'HELO expected') if not self.sender: return (503, 'MAIL FROM: expected') m = to_pat.match(self.parm) if not m: return (500, 'RCPT TO:<recipient> expected') recip = m.group(1) if not recip: return (501, self.parm) if not self.server.valid_recip(recip): return (551, 'no route through this host') if recip in self.recipients: return recip + ' is already a recipient' self.recipients.append(recip) return 'OK, %d recipient(s)' % len(self.recipients) def handle_DATA(self): if not self.sender_host: return (503, 'HELO expected') if not self.sender: return (503, 'MAIL FROM: expected') if not self.recipients: return (503, 'RCPT TO: expected') self.reply_msg(354) mail = self.received() + LF while 1: line = self.gets() if line is None: # socket closed self.running = 0 return if line == '.': break if line[:2] == '..': line = line[1:] mail = mail + line + LF self.mail = mail return self.server.process_mail(self) def handle_RSET(self): self.rset() return 'Starting from scratch' def rset(self): # don't reset self.sender_host! self.sender = '' self.recipients = [] self.mail = '' def received(self): """return a suitable "Received:" header line""" return 'Received: from %s by %s with %s (%s)\n\tfor %s; %s' % ( verbose_hostname(self.client_address[0]), self.localhost, 'SMTP', self.version, joinaddr(self.recipients[0]), rfctime() ) class InterruptibleTCPServer(TCPServer): def __init__(self, server_address, RequestHandlerClass): TCPServer.__init__(self, server_address, RequestHandlerClass) self.running = 1 def serve_forever(self): while self.running: self.handle_request() def stop(self): self.running = 0 SMTP_PORT = socket.getservbyname('smtp', 'tcp') class SmtpGatewayServer(InterruptibleTCPServer): def __init__(self, port = SMTP_PORT): InterruptibleTCPServer.__init__(self, ('', port), SmtpHandler) self.localhost = verbose_localhost() self.smtp_domain = '' self.smtp_host = '' self.online = 0 def config_smtp(self, sdomain, shost): self.smtp_domain = sdomain self.smtp_host = shost def verify_request(self, request, client_address): """Check if we are online""" try: socket.gethostbyname(self.smtp_host) self.online = 1 except socket.error: self.online = 0 return 1 def connected(self): return self.online def valid_host(self, host, addr): return 1 email_pat = re.compile('^[\w\d.-_]+@[\w\d-.]+$', re.I) def valid_email(self, addr): return self.email_pat.match(addr) is not None def valid_sender(self, sender): return self.smtp_domain in ['*', domainpart(sender)] def valid_recip(self, recip): return domainpart(recip) != self.smtp_domain def process_mail(self, req): print req.sender, '==>', req.recipients # forward the message try: s = smtplib.SMTP(self.smtp_host) except socket.error, e: print 'SMTP server', self.smtp_host, 'is not responding:', str(e) return (421, 'gateway not online') # s.set_debuglevel(1) rc = 0 # assume failure try: rejects = s.sendmail(req.sender, req.recipients, req.mail) rc = 1 # success! except smtplib.SMTPSenderRefused, e: rejects = {str(e): self.sender[1]} except smtplib.SMTPDataError, e: rejects = {smtplib.SMTPDataError: ''} except Exception, e: rejects = {'Error sending mail': str(e)} # fake the rejects dict s.quit() # if there were any rejects, notify our postmaster if rejects: print rejects return 1 def main(): smtpd = SmtpGatewayServer() smtpd.config_smtp('earthling.net', 'mail.iname.com') print "Ready for SMTP connections..." smtpd.serve_forever() print "Done." if __name__ == '__main__': main()