"""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:
RCPT TO:
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 .',
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: 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: 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()