Hoy vamos a ver cómo hacer entrega remota de un mail. Como somos un server que recibió un mail que tiene que ser entregado en otro, hay una serie de pasos que tenemos que hacer. Empecemos con una implementación de t.m.s.IMessage que manda a un smarthost:

class Relay (object):
implements (smtp.IMessage)

    def __init__ (self, router, user):
    # select smarthost based on src domain
    self.user= user
    self.smarthost= 'our.smarthost.com'
    self.lines= []
    self.eom= False
    self.router= router

def lineReceived (self, line):
    self.lines.append (line)

def eomReceived (self):
    return self.send ()

def send (self, mxRecord=None):
    sender= smtp.sendmail (self.smarthost, self.user.orig,
    [self.user.dest], '\n'.join (self.lines), '')
    sender.addCallback (self.sendComplete)
    return sender

def sendComplete (self, *data):
    del self.lines
    return data

def connectionLost (self):
    del self.lines

Una cosa que no expliqué en el post anterior es el valor devuelto por eomReceived(). En ese caso era return self.mailbox.appendMessage (messageData); en éste, después de un par de vueltas, es el resultado de smtp.sendmail(). Lo que estamos devolviendo es un Deferred.

Los Deferreds son una parte importante de Twisted. Son básicamente una promesa de que en algún momento va a haber un valor disponible para devolver, pero que mientras le vamos dadndo esto como para que tenga. El truco es luego conectar con esa promesa nuestros callbacks llamando a addCallback(). Esos callbacks van a ser llamados cuando el valor esté disponible. También se pueden agregar errbacks, que son callbacks que son llamados cuando la operación que pedimos tuvo un error (típicamente una excepción).

Eso es exactamente lo que estamos haciendo en send(). t.m.s.sendmail() nos devuelve un Deferred al que le conectamos nuestro sendComplete() y lo devolvemos. sendComplete() simplemente borra las líneas (aparentemente tarde, ya veremos que nos van a hacer falta) y continúa la cadena de callbacks del deferred; cadena que se va armando de esta forma: cuando se llama a callback() en un deferred, éste llama al primer callback. El resultado de este callback es pasado al siguiente, y así.

Esto así como está manda por un smarthost. La diferencia entre mandar todo por un smarthost y mandar directamente es que esta última requiere un paso extra: averiguar a qué máquina debe entregarse el mail. Me refiero al registro MX. Vamos a tener que hacer una consulta de DNS mientras recibimos el mail. Una vez que tengamos ambos vamos a poder hacer la entrega, y finalizar. Veamos cómo nos las arreglamos:

mxCalc= relaymanager.MXCalculator ()

class Relay (object):
implements (smtp.IMessage)

def __init__ (self, router, user):
    # deliver by ourselves
    self.smarthost= None
    resolver= self.getSMTPServer (user)
    resolver.addCallback (self.send).addErrback (self.queue)
    self.lines= []
    self.eom= False
    self.router= router

def getSMTPServer (self, user):
    return mxCalc.getMX (user.dest.domain)

def lineReceived (self, line):
    self.lines.append (line)

def eomReceived (self):
    self.eom= True
    if self.smarthost is None:
        print "WARN: mail received and no smarthost!"
    else:
        print "mail finished; sending..."
        self.send ()
    self.sentSignal= defer.Deferred ()
    return self.sentSignal

def send (self, mxRecord=None):
    if mxRecord is not None:
        # mxRecord is a dns.*Record instance
        # mxRecord.name is a dns.Name instance
        self.smarthost= mxRecord.name.name
    if self.eom:
        sender= smtp.sendmail (self.smarthost, self.user.orig,
            [self.user.dest], '\n'.join (self.lines), config.heloAs)
        sender.addCallback (self.sendComplete).addErrback (self.queue)

def queue (self, error):
    self.router.queue (error=error, user=self.user, mail=self.lines)
    self.sentSignal.callback (True)

def sendComplete (self, *data):
    del self.lines
    print ignore
    self.sentSignal.callback (True)

def connectionLost (self):
    print "WARN: unfinished mail!"
    del self.lines
    self.sentSignal.errback (False)

Acá hay varias cosas. Por un lado tenemos una función que se encarga de pedir el registro MX, la que devuelve un Deferred al que le enganchamos nuestra función de entrega. Al mismo tiempo vamos recibiendo el mail, y cuando termine también intenta hacer la entrega. Ahora, acá el truco es crear un Deferred y devolverlo inmediatamente en eomReceived(). Cuando el mail es enviado finalmente, nuestro callback sendComplete() es llamado, el que a su vez hace un callback de nuestro Deferred. Por último, si tenemos un error de entrega, enconlamos a través de nuestro router el mail para un posterior intento de entrega.

Lo que vimos en este post es el manejo de Deferreds, y cómo se los usa para prometer volver a llamar cuando el resultado está disponible. Hasta ahora es el único momento en el que realmente he necesitado manejarlos. Supongo que ya volveré a verlos cuando empieze el duro camino de implementar filtros.

twismtpy python twisted