Email in ingresso con Google App Engine

Pubblicato: 9 novembre 2009 in Python
Tag:,

Da circa un mese Google ha messo a disposizione una feature utilissima per la sua omnipiattaforma fasotutomì App Engine: la mail in ingresso (qui l’annuncio sul blog ufficiale).

Siccome sulla documentazione c’è scritto e non c’è scritto, anche se è semplicissimo pubblico qui tutto quanto è necessario alla fruizione di questa utilissima feature, e lo faccio con Python che Java mi fa cagare in Java non lo so fare.

Per prima cosa bisogna annunciare ad App Engine di voler ricevere email e bisogna pure dirgli quale sarà lo script che le andrà a gestire. nulla di nuovo, si fa tutto da app.yaml

application: superfigatadapaura
version: 1
runtime: python
api_version: 1

inbound_services:
- mail

handlers:
- url: /_ah/mail/.+
  script: handle_mail.py

- url: .*
  script: main.py

Con la direttiva ‘inbound_services’ si attiva la mail in ingresso che di default è disabilitata, con il primo url handler si gestiscono le richieste POST che arrivano all’applicazione, sì perché la mail in ingresso viene magicamente trasformata in richieste POST all’url http://appid.appspot.com/_ah/mail/qualchecosa@appid.appspotmail.com. Occhio che gli url handlers vengono gestiti a cascata per cui l’acchiappatutto ‘.*’ deve rimanere per ultimo, pena ore di punti interrogativi che si innestano nelle meningi.

Magia o non magia le richieste POST non vengono però gestite come normali richieste POST ma vengono gestite con un’apposita classe (InboundMailHandler) che si occupa del parsing del messaggio e che noi andremo a derivare in quasi perfetta analogia con quello che capitava con RequestHandler:

import wsgiref.handlers
from google.appengine.ext import webapp
from google.appengine.ext.webapp import mail_handlers

class MailHandler(mail_handlers.InboundMailHandler):
        def receive(self, message):
                # Tutto qui. La variabile message che è di tipo InboundEmailMessage
                # contiene l'email e possiede le seguenti proprietà:
                # message.subject
                # message.sender
                # message.to
                # message.cc
                # message.date
                # message.bodies
                # message.attachments

def main():
        application = webapp.WSGIApplication([('/_ah/mail/.+', MailHandler)], debug=True)
        wsgiref.handlers.CGIHandler().run(application)

if __name__ == '__main__':
        main()

Due ultime osservazioni. La prima è che il metodo di cui fare l’override non è post() ma receive(), la seconda è che ‘bodies’ ritorna una lista di bodies (chi lo avrebbe mai detto) e ciascun body si può estrarre attraverso il metodo bodies() (questa sì che è una filastrocca da insegnare ai bambini!).

Per fare una prova è sufficiente spedire una normale email a graziemikiperilbellissimopost@appid.appspotmail.com

Link alla documentazione ufficiale (dove c’è scritto e non c’è scritto).

Update del 10 novembre 2009:

Sempre a proposito della documentazione un po’ carente, oggi sono venuto alle mani con gli attributi ‘bodies’ e ‘attachments’ che si comportano in modo leggermente diverso da quanto mi aspettavo (attenzione: non necessariamente si comportano diversamente da quello che c’è scritto – anche se -, solo si comportano diversamente da quello che io mi aspettavo).
L’attributo ‘bodies’ ritorna una lista di tuple (e non una lista di bodies), ciascuna delle quali ha come primo membro il content type e come secondo membro un oggetto di tipo google.appengine.api.mail.EncodedPayload che non è mica una stringa come stavate già pensando sfregando tra loro le vostre manine. Per fortuna la stringa la ottengo applicando il metodo decode() a questo oggetto. La cosa triste è che anche bodies() ritorna un generatore che fornisce una tupla di questo tipo.

# ritorna ('text/plain', istanza.di.EncodedPayload)
plaintext = message.bodies(content_type='text/plain')
for body in plaintext:
        fa_qualcosa((body[1].decode()) # metodo decode()

L’attributo ‘attachments’ funziona in modo analogo anche se perfino più a cazzo con le dovute differenze. Anzitutto l’attributo potrebbe non essere presente del tutto, e se abbiamo intenzione di gestire allegati dobbiamo fare subito un controllo. Se l’allegato è uno solo viene ficcato in una tupla (‘nomefile’, contenuto.come.istanza.di.EncodedPayload), se invece sono più di uno viene generata una lista di tuple di questo tipo. Va a finire che bisogna scrivere roba tipo:

allegati = None
if hasattr(message, 'attachments'):
        if isinstance(message.attachments, tuple):
                allegati = [message.attachments]
        else:
                allegati = message.attachments
        for a in allegati:
                # Per ogni allegato abbiamo in
                # a[0] il nome file ed in
                # a[1].decode() il contenuto del file

Update dell’11 novembre 2009:

Lo dicevo che la gestione degli attachments era perfino più a cazzo analoga a quella dei bodies ma con differenze fondamentali ed infatti il metodo decode() non funziona. C’è una segnalazione di bug, la 2289, baco che viene dato per fixed ma che non lo è. Per fare correttamente il decode dell’EncodedPayload che contiene l’allegato, accogliendo il suggerimento presente nella pagina di segnalazione dll’errore, bisogna quindi creare una nostra funzioncina:

def goodDecode(encodedPayload):
        encoding = encodedPayload.encoding
        payload = encodedPayload.payload
        if encoding and encoding.lower() != '7bit':
                payload = payload.decode(encoding)
        return payload

Mettendo tutto assieme avremmo quindi:

import wsgiref.handlers
from google.appengine.ext import webapp
from google.appengine.ext.webapp import mail_handlers

def goodDecode(encodedPayload):
        encoding = encodedPayload.encoding
        payload = encodedPayload.payload
        if encoding and encoding.lower() != '7bit':
                payload = payload.decode(encoding)
        return payload

class MailHandler(mail_handlers.InboundMailHandler):
        def receive(self, message):
                # Tutto qui. La variabile message che è di tipo InboundEmailMessage
                # contiene l'email e possiede le seguenti proprietà:
                # message.subject
                # message.sender
                # message.to
                # message.cc
                # message.date
                # message.bodies
                # message.attachments
                plaintext = message.bodies(content_type='text/plain')
                for body in plaintext:
                           fa_qualcosa((body[1].decode())
                allegati = None
                if hasattr(message, 'attachments'):
                        if isinstance(message.attachments, tuple):
                                allegati = [message.attachments]
                        else:
                                allegati = message.attachments
                        for a in allegati:
                                # Per ogni allegato abbiamo in
                                # a[0] il nome file ed in
                                # goodDecode(a[1]) il contenuto del file

def main():
        application = webapp.WSGIApplication([('/_ah/mail/.+', MailHandler)], debug=True)
        wsgiref.handlers.CGIHandler().run(application)

if __name__ == '__main__':
        main()
commenti
  1. […] e televisione ho aggiornato il postino (inteso come piccolo post) di ieri sulla gestione delle email in ingresso con Google App Engine. Ora il postino è cresciuto ed è diventato una […]

  2. […] lascia un commento » Questo post è un’appendice all’articolo Email in ingresso con Google App Engine. […]

Lascia un commento

Inserisci i tuoi dati qui sotto o clicca su un'icona per effettuare l'accesso:

Logo WordPress.com

Stai commentando usando il tuo account WordPress.com. Chiudi sessione / Modifica )

Foto Twitter

Stai commentando usando il tuo account Twitter. Chiudi sessione / Modifica )

Foto di Facebook

Stai commentando usando il tuo account Facebook. Chiudi sessione / Modifica )

Google+ photo

Stai commentando usando il tuo account Google+. Chiudi sessione / Modifica )

Connessione a %s...