"""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()