Vamos a empezar con un server básico, que es con lo que empecé yo. El código es prácticamente lo mismo que está en el libro que mencioné. Básicamnete es un servidor que sabe recibir mails y guardarlo en maildirs:
from twisted.mail import smtp, maildir
from twisted.internet import protocol, reactor
from zope.interface import implements
import os
from email.Header import Header
class MailDir (object):
"""
handles the local delivery to a maildir inbox
"""
implements (smtp.IMessage)
def __init__ (self, user):
userDir= str (user.dest.local)
# we create a directory for this user
if not os.path.exists (userDir):
os.mkdir (userDir)
inboxDir= os.path.join (userDir, 'Inbox')
self.mailbox= maildir.MaildirMailbox (inboxDir)
self.lines= []
def lineReceived (self, line):
self.lines.append (line)
def eomReceived (self):
# message is complete, store it
self.lines.append ('')
messageData= '\n'.join (self.lines)
return self.mailbox.appendMessage (messageData)
def connectionLost (self):
# unexpected loss of connectio, don't save
del (self.lines)
class MailRouter (object):
implements (smtp.IMessageDelivery)
def __init__ (self, validDomains):
self.validDomains= validDomains
def receivedHeader (self, helo, origin, recipients):
# client is how the client ident'ed itself
# clientIP is the ip of the client side's end
# we could do a reverse DNS lookup and check if it's true
# also check on RBL's and such
client, clientIP= helo
recipient= recipients[0]
# this must be our CNAME
myself= 'localhost'
value= """from %s [%s] by %s with ESMTP for %s; %s""" % (
client, clientIP, myself, recipient, smtp.rfc822date ()
)
return "Received: %s" % Header (value)
def validateFrom (self, helo, originAddress):
self.client= helo
# originAddress is a twisted.mail.smtp.Address
# if the from is invalid, we should
# raise smtp.SMTPBadSender
return originAddress
def validateTo (self, user):
"""
routing is the most complicated part of serving an smtp server
we can be run on a laptop that only wants to send mail
with possibly many source address
we can be run on a server that has a local user database;
it con be a smarthost for otehr machines
it can be a satellite machine with only a smarthost
"""
if user.dest.domain in self.validDomains:
return lambda: Maildir (user)
else:
raise smtp.SMTPBadRcpt (user)
class SMTPFactory (protocol.ServerFactory):
def __init__ (self, validDomains):
self.validDomains= validDomains
def buildProtocol (self, addr):
delivery= MailRouter (self.validDomains)
smtpProtocol= smtp.SMTP (delivery)
smtpProtocol.factory= self
return smtpProtocol
if __name__=='__main__':
import sys
# normal local server
domains= sys.argv[1].split (',')
reactor.listenTCP (2525, SMTPFactory (domains))
reactor.run ()
Tenemos tres clases. La primera es SMTPFactory, la
cual es sólo un factory de protocols. Tiene un método,
buildProtocol() que tiene que construir un protocolo y
devolverlo.
La segunda que veremos es la MailDir. Ésta implementa la
interfaz t.m.s.IMessage, que se usa para la entrega de
un mensaje. En este caso es una entrega local a un maildir, aunque
luego implementaremos la entrega remota con esta misma interfaz. La
clase tiene que implementar tres métodos:
lineReceived() es llamado por cada nueva línea del
mail que llega. No hace diferencias entre si es parte del cuerpo o
del header. Si tuviéramos que hacer algún procesamiento, como
toquetear headers o rechazar el mail por tamaño, lo deberíamos
hacer en este nivel.
eomReceived() se llama cuando todo el mail ha sido
ya entregado a través de lineReceived(). En este caso
escribimos el mail finalmente en un maildir. Fíjense que como
t.m.maildir.MaildirMailbox no tiene esta misma
interfaz, tenemos que acumular la líneas en una lista y pegarlas
todas y dársela de commer a t.m.md.MDMB.append(). Ya
me sentaré a verificar si no hay una mejor API para esto.
Finalmente, connectionLost() se llama si la
conexión se pirde antes de recibir todo el mail.
Hasta ahora todo sencillo, sólo un factory y la implementación
de una entrega local sin muchas luces (no sabe buscar el maildir de
un usuario, sino que asume un directorio propio). Gran parte del
meollo del mail está en la tercera clase que veremos,
MailRouter.
La MailRouter es la que sencargará de definir si el
mail es entregable o no. En nuestro caso inicial, no la picadura
que me estoy rascando, vamos a permitir relaying libre siempre y
sólo localmente a través de la clase MailDir. Antes de
ver lo métodos implementados, veamos una conversación típica en
SMTP:
>>> 220 mustang.grulicueva.net NO UCE NO UBE NO RELAY PROBES
<<< helo gurrumin
>>> 250 mustang.grulicueva.net Hello 127.0.0.1, nice to meet you
<<< mail from: mdione@except.com.ar
>>> 250 Sender address accepted
<<< rcpt to: mdione@localhost
>>> 250 Recipient address accepted
<<< rcpt to: root@localhost
>>> 250 Recipient address accepted
<<< data
>>> 354 Continue
<<< Subject: bongs
<<< To: mdione@whitehouse.gov
<<<
<<<
<<< This is top secret info. So secret we won't even tell you. Sorry.
<<< .
>>> 250 Delivery in progress
<<< quit
>>> 221 See you later
Tenemos marcados con >>> lo que escupe el
server y con <<< lo que manda el cliente.
Básicamente el protocolo se basa en tres fases: presentación,
declaración de entrefa y cuerpo. En la presentación el cliente se
identifica con un nombre. Acá también puede autenticarse con un par
(usuario, passwd), arrancar encripción y todo ese tipo de cosas
relativas a la conexión en si. Luego dice de quién viene y a
quiénes va el mail, y finalmente el mail en sí. Notar que en esta
parte van también los headers (estamos viendo solo dos, un
To y un Subject. Estas dos últimas
estapas se pueden repetir una detrás de la otra las veces que se
quiera.
La clase Mailrouter implementa la interfaz
t.m.s.IMessageDelivery, se crea una por cada conexión
al puerto en el que estamos escuchando, en la que tenemos que
implementar otros tres métodos:
El primero es el validateFrom(), el que es llamado
por cada mail from. Recibe como parametro una tupla de
str. El primer str es el nombre que usó
el cliente en el comando helo y el segundo es el IP
real. También recibe un t.m.s.Address con el from.
Como dicen los comentarios, acá podríamos fijarnos si el nombre y
el ip coinciden, o si está en un RBL o cosas parecidas, o si damos
relay para el from. En este caso aceptamos todo. Notar que lo
devuelto es también un t.m.s.Address. Eventualmente se
podría devolver otro, aunque no se me ocurre en qué casos.
El más importante, el validateTo(). ¿Porqué digo el
más importante? Porque éste es el que se encarga de hacer la
decisión de ruteo, es decir, de decidir qué implementación de
t.m.s.IMessage se va a encargar del delivery basado
tanto en el from como en el to del mail. El parámetro que nos pasan
es un t.m.s.User, el cual contiene ambas direciones en
sus atributos orig y dest
respectivamente. En este caso sólo nos fijamos que el destino esté
entre los dominos al que le hacemos relay local, y si no levantamos
una t.m.s.SMTPBadRcpt, la que se traduce en un mensaje
de no relay al cleinte. Notar que lo que devuelve no es ni una
instancia ni siquiera la clase que va a implementar el delivery,
sino una función que devuelve una instancia. En el próximo post voy
a estar mostrando porqué, y en los subsiguientes posts voy a
complicar este método para lograr las políticas de relaying que
tengo planeado.
Finalmente, receiveHeader() es el más sencillo.
Sólo tenemos que devolver un string con un header Received
apropiado para este delivery. Es éste nos pasan la misma tupla con
dos str que en validateFrom(), la
dirección de origen y la lista de instancias de
t.m.s.User con los recipientes. Ejemplos de estos
headers lo podemos encontrar en cualquier mail:
Received: from [192.168.1.77] (unknown [201.250.21.186])
(using TLSv1 with cipher DHE-RSA-AES256-SHA (256/256 bits))
(No client certificate requested)
by mail.madap.com.ar (Postfix) with ESMTPSA id 978604613
for <mdione@grulic.org.ar>; Mon, 30 Jun 2008 08:16:56 -0300 (ART)
Received: by 10.141.105.17 with HTTP; Wed, 2 Jul 2008 13:55:38 -0700 (PDT)
Received: from localhost ([127.0.0.1] helo=forster.canonical.com)
by forster.canonical.com with esmtp (Exim 4.69 #1 (Debian))
id 1KDHgi-0000eo-Fa
for <mdione@grulic.org.ar>; Mon, 30 Jun 2008 12:36:52 +0100
Bueno, es todo por hoy. Tengan en cuenta que todavía no pretendo que éste sea un tutorial de Twisted, pero si va mostrando cosas que nos vamos a encontrar en muchas de las implementaciones de servicios con este framework.