Chapter 8: E-mails e SMS

 Emails e SMS

Mail

 Configurando email

Web2py fornece a classe gluon.tools.Mail para facilitar o envio de e-mails usando o web2py. Pode-se definir um mailer com

from gluon.tools import Mail
mail = Mail()
mail.settings.server = 'smtp.example.com:25'
mail.settings.sender = 'you@example.com'
mail.settings.login = 'username:password'

Observe, se o seu aplicativo usa Auth  (discutido no próximo capítulo), o objeto auth irá incluir o seu próprio mailer em auth.settings.mailer , então você pode usar isso da seguinte maneira:

mail = auth.settings.mailer
mail.settings.server = 'smtp.example.com:25'
mail.settings.sender = 'you@example.com'
mail.settings.login = 'username:password'

Você precisa substituir o mail.settings pelos parâmetros apropriados para o seu servidor SMTP. Conjunto mail.settings.login = None  se o servidor SMTP não exigir autenticação. Se você não quiser usar o TLS, defina mail.settings.tls = False

email logging

Para fins de depuração, você pode definir

mail.settings.server = 'logging'

e e-mails não serão enviados, mas registrados no console.

 Configurar email para o Google App Engine

email from GAE

Para enviar e-mails da conta do Google App Engine:

mail.settings.server = 'gae'

No momento da redação, o web2py não oferece suporte a anexos e e-mails criptografados no Google App Engine. Observe que o cron e o planejador não funcionam no GAE.

 Encriptação x509 e PGP
PGP
 
x509

É possível enviar e-mails criptografados x509 (SMIME) usando as seguintes configurações:

mail.settings.cipher_type = 'x509'
mail.settings.sign = True
mail.settings.sign_passphrase = 'your passphrase'
mail.settings.encrypt = True
mail.settings.x509_sign_keyfile = 'filename.key'
mail.settings.x509_sign_certfile = 'filename.cert'
mail.settings.x509_crypt_certfiles = 'filename.cert'

É possível enviar e-mails criptografados por PGP. Primeiro de tudo você precisa instalar o pacote python-pyme. Então você pode usar o GnuPG (GPG) para criar os arquivos de chaves para o remetente (pegue o endereço de email de mail.settings.sender) e coloque os arquivos pubring.gpg e secring.gpg em um diretório (por exemplo, "/ home/www-data/.gnupg ").

Use as seguintes configurações:

mail.settings.gpg_home = '/home/www-data/.gnupg/'
mail.settings.cipher_type = 'gpg'
mail.settings.sign = True
mail.settings.sign_passphrase = 'your passphrase'
mail.settings.encrypt = True

 Enviar e-mails
mail.send
 
email html
 
email attachments

Uma vez que mail  está definido, pode ser usado para enviar e-mail via:

mail.send(to=['somebody@example.com'],
          subject='hello',
          # If reply_to is omitted, then mail.settings.sender is used
          reply_to='us@example.com',
          message='hi there')

mail retorna True  se conseguir enviar o email e False  de outra forma. Uma lista completa de argumentos para mail.send()  é o seguinte:

send(self, to, subject='None', message='None', attachments=[],
     cc=[], bcc=[], reply_to=[], sender=None, encoding='utf-8',
     raw=True, headers={})

Note que, to , cc e bcc  cada um pega uma lista de endereços de e-mail.

sender  o padrão é None  e neste caso o remetente será configurado para mail.settings.sender .

headers  é um dicionário de cabeçalhos para refinar os cabeçalhos antes de enviar o email. Por exemplo:

headers = {'Return-Path' : 'bounces@example.org'}

A seguir estão alguns exemplos adicionais demonstrando o uso de mail.send() .

 E-mail de texto simples

mail.send('you@example.com',
  'Message subject',
  'Plain text body of the message')

 E-mails em HTML

mail.send('you@example.com',
  'Message subject',
  '<html>html body</html>')

Se o corpo do email começar com <html>  e termina com </html> , será enviado como um email em HTML.

 Combinando emails de texto e HTML

A mensagem de email pode ser uma tupla (texto, html):

mail.send('you@example.com',
  'Message subject',
  ('Plain text body', '<html>html body</html>'))

  cc  e bcc  e-mails

mail.send('you@example.com',
  'Message subject',
  'Plain text body',
  cc=['other1@example.com', 'other2@example.com'],
  bcc=['other3@example.com', 'other4@example.com'])

 Anexos

mail.send('you@example.com',
  'Message subject',
  '<html><img src="cid:photo" /></html>',
  attachments = mail.Attachment('/path/to/photo.jpg', content_id='photo'))

 Vários anexos

mail.send('you@example.com',
  'Message subject',
  'Message body',
  attachments = [mail.Attachment('/path/to/fist.file'),
                 mail.Attachment('/path/to/second.file')])

 Envio de mensagens SMS

SMS

Enviar mensagens SMS de um aplicativo web2py requer um serviço de terceiros que possa retransmitir as mensagens para o destinatário. Normalmente, isso não é um serviço gratuito, mas difere de país para país. Nós tentamos alguns desses serviços com pouco sucesso. As empresas de telefonia bloqueiam os e-mails originados desses serviços, uma vez que são usados como fonte de spam.

A melhor maneira é usar as próprias empresas de telefonia para transmitir o SMS. Cada empresa de telefonia tem um endereço de e-mail exclusivamente associado a cada número de telefone celular, portanto, as mensagens SMS podem ser enviadas como e-mails para o número de telefone.

O web2py vem com um módulo para ajudar nesse processo:

from gluon.contrib.sms_utils import SMSCODES, sms_email
email = sms_email('1 (111) 111-1111','T-Mobile USA (tmail)')
mail.send(to=email, subject='test', message='test')

SMSCODES é um dicionário que mapeia os nomes das principais empresas de telefonia para o endereço de e-mail postfix. o sms_email  função recebe um número de telefone (como uma string) e o nome de uma companhia telefônica e retorna o endereço de e-mail do telefone.

 Usando o sistema de templates para gerar mensagens

emails

É possível usar o sistema de modelos para gerar e-mails. Por exemplo, considere a tabela do banco de dados

db.define_table('person', Field('name'))

onde você deseja enviar a cada pessoa no banco de dados a seguinte mensagem, armazenado em um arquivo de visualização "message.html":

Dear {{=person.name}},
You have won the second prize, a set of steak knives.

Você pode conseguir isso da seguinte maneira

for person in db(db.person).select():
    context = dict(person=person)
    message = response.render('message.html', context)
    mail.send(to=['who@example.com'],
              subject='None',
              message=message)

A maior parte do trabalho é feito na declaração

response.render('message.html', context)

Ele renderiza a visão "message.html" com as variáveis definidas no dicionário "context", e retorna uma string com o texto do email renderizado. O contexto é um dicionário que contém variáveis que serão visíveis para o arquivo de modelo.

Se a mensagem começar com <html>  e termina com </html> , o email será um email em HTML.

Note que, se você quiser incluir um link para seu site em um e-mail em HTML, poderá usar o URL  função. No entanto, por padrão, o URL  função gera uma URL relativa, que não funcionará a partir de um email. Para gerar um URL absoluto, você precisa especificar o scheme  e host  argumentos para a função URL. Por exemplo:

<a href="{{=URL(..., scheme=True, host=True)}}">Click here</a>

ou

<a href="{{=URL(..., scheme='http', host='www.site.com')}}">Click here</a>

O mesmo mecanismo usado para gerar texto de email também pode ser usado para gerar mensagens SMS ou qualquer outro tipo de mensagem com base em um modelo.

 Enviando mensagens usando uma tarefa em segundo plano

A operação de envio de uma mensagem de e-mail pode levar vários segundos devido à necessidade de fazer login e se comunicar com um servidor SMTP potencialmente remoto. Para evitar que o usuário tenha que aguardar a conclusão da operação de envio, às vezes é desejável enfileirar o email a ser enviado posteriormente por meio de uma tarefa em segundo plano. Conforme descrito no Capítulo 4, isso pode ser feito configurando uma fila de tarefas caseiras ou usando o agendador web2py. Aqui nós fornecemos um exemplo usando uma fila de tarefas caseiras.

Primeiro, em um arquivo de modelo dentro de nosso aplicativo, configuramos um modelo de banco de dados para armazenar nossa fila de email:

db.define_table('queue',
    Field('status'),
    Field('email'),
    Field('subject'),
    Field('message'))

A partir de um controlador, podemos enfileirar mensagens para serem enviadas por:

db.queue.insert(status='pending',
                email='you@example.com',
                subject='test',
                message='test')

Em seguida, precisamos de um script de processamento em segundo plano que leia a fila e envie os e-mails:

## in file /app/private/mail_queue.py
import time
while True:
    rows = db(db.queue.status=='pending').select()
    for row in rows:
        if mail.send(to=row.email,
            subject=row.subject,
            message=row.message):
            row.update_record(status='sent')
        else:
            row.update_record(status='failed')
        db.commit()
    time.sleep(60) # check every minute

Por fim, conforme descrito no Capítulo 4, precisamos executar o script mail_queue.py como se estivesse dentro de um controlador em nosso aplicativo:

python web2py.py -S app -M -R applications/app/private/mail_queue.py

Onde -S app  diz ao web2py para executar "mail_queue.py" como "app", -M  diz ao web2py para executar modelos.

Aqui nós assumimos que o objeto mail referenciado em "mail_queue.py" é definido em um arquivo de modelo em nosso aplicativo e, portanto, está disponível no script "mail_queue.py" por causa do -M  opção. Observe também que é importante realizar qualquer alteração o mais rápido possível para não bloquear o banco de dados em outros processos concorrentes.

Como observado no Capítulo 4, esse tipo de processo em segundo plano não deve ser executado via cron (exceto talvez para o cron @reboot) porque você precisa ter certeza de que não mais de uma instância está sendo executada ao mesmo tempo.

Observe que uma desvantagem de enviar e-mails por meio de um processo em segundo plano é que fica difícil fornecer feedback ao usuário caso o e-mail falhe. Se o email for enviado diretamente da ação do controlador, você poderá detectar qualquer erro e retornar imediatamente uma mensagem de erro ao usuário. Com um processo em segundo plano, no entanto, o email é enviado de forma assíncrona, após a ação do controlador já ter retornado sua resposta, tornando-se mais complexo notificar o usuário sobre uma falha.

 Lendo e gerenciando caixas de email (Experimental)

o IMAP  O adaptador destina-se a ser uma interface com servidores IMAP de e-mail para executar consultas simples no web2py DAL  Sintaxe de consulta, portanto, leitura de email, pesquisa e outros serviços de email IMAP relacionados (como aqueles implementados por marcas como Google (r) e Yahoo (r) podem ser gerenciados a partir de aplicativos web2py.

Ele cria seus nomes de tabela e campo "estaticamente", o que significa que o desenvolvedor deve deixar as definições de tabela e campo para a instância DAL chamando o adaptador .define_tables()  método. As tabelas são definidas com as informações da lista de caixas de correio do servidor IMAP.

 Conexão

Para uma única conta de e-mail, esse é o código recomendado para iniciar o suporte a IMAP no modelo do aplicativo

# Replace user, password, server and port in the connection string
# Set port as 993 for SSL support
imapdb = DAL("imap://user:password@server:port", pool_size=1)
imapdb.define_tables()

Observe que <imapdb>.define_tables()  retorna um dicionário de strings que mapeiam nomes de arquivo DAL para os nomes de caixa de correio do servidor com a estrutura {<tablename>: <server mailbox name>, ...} , para que você possa obter o nome da caixa de correio real no servidor IMAP.

Se você quiser definir sua própria configuração de tablename/mailbox e ignorar a configuração automática de nomes, poderá transmitir um dicionário personalizado para o adaptador desta maneira:

imapdb.define_tables({"inbox":"MAILBOX", "trash":"SPAM"})

Para manipular os diferentes nomes de caixas de correio nativas para a interface do usuário, os seguintes atributos dão acesso aos nomes mapeados da caixa de correio automática do adaptador (cuja caixa de correio nativa possui o nome da tabela e vice-versa):

Atributo Tipo Formato
imapdb.mailboxesdict{<tablename>: <server native name>, ...}
imapdb. <table> .mailboxstring"server native name"

O primeiro pode ser útil para recuperar conjuntos de consultas IMAP pela caixa de correio do serviço de email nativo

# mailbox is a string containing the actual mailbox name
tablenames = dict([(v, k) for k, v in imapdb.mailboxes.items()])
myset = imapdb(imapdb[tablenames[mailbox]])

 Buscando e-mail e atualizando sinalizadores

Aqui está uma lista de comandos IMAP que você pode usar no controlador. Para os exemplos, assume-se que o seu serviço IMAP tem uma caixa de correio denominada INBOX , que é o caso das contas do Gmail (r).

Para contar as mensagens invisíveis de hoje menores que 6000 octetos da caixa de entrada da caixa de entrada

q = imapdb.INBOX.seen == False
q &= imapdb.INBOX.created == request.now.date()
q &= imapdb.INBOX.size < 6000
unread = imapdb(q).count()

Você pode buscar as mensagens de consulta anteriores com

rows = imapdb(q).select()

Operadores de consulta usuais são implementados, incluindo pertence

messages = imapdb(imapdb.INBOX.uid.belongs(<uid sequence>)).select()

Nota: É altamente recomendável que você mantenha os resultados da consulta abaixo de um determinado limite de tamanho de dados para evitar que o servidor fique com grandes comandos de seleção.

Para realizar consultas de email mais rápidas, é recomendável passar um conjunto filtrado de campos:

fields = ["INBOX.uid", "INBOX.sender", "INBOX.subject", "INBOX.created"]
rows = imapdb(q).select(*fields)

O adaptador sabe quando recuperar cargas úteis de mensagens parciais (campos como content , size  e attachments  exigir a recuperação dos dados completos da mensagem)

É possível filtrar resultados de seleção de consulta com limitby e sequências de campos de caixa de correio

# Replace the arguments with actual values
myset.select(<fields sequence>, limitby=(<int>, <int>))

Digamos que você queira que uma ação do aplicativo mostre uma mensagem de caixa de correio. Primeiro, recuperamos a mensagem (se o seu serviço IMAP for compatível, busque mensagens por uid  campo para evitar o uso de antigas referências de seqüência).

mymessage = imapdb(imapdb.INBOX.uid == <uid>).select().first()

Caso contrário, você pode usar a mensagem id .

mymessage = imapdb.INBOX[<id>]

Observe que o uso do ID da mensagem como referência não é recomendado, porque os números de sequência podem mudar com as operações de manutenção de caixa de correio como exclusões de mensagens. Se você ainda quiser gravar referências a mensagens (ou seja, no campo de registro de outro banco de dados), a solução é usar o campo uid como referência sempre que houver suporte e recuperar cada mensagem com o valor registrado.

Por fim, adicione algo como o seguinte para mostrar o conteúdo da mensagem em uma exibição

{{=P(T("Message from"), " ", mymessage.sender)}}
{{=P(T("Received on"), " ", mymessage.created)}}
{{=H5(mymessage.subject)}}
{{for text in mymessage.content:}}
  {{=DIV(text)}}
  {{=TR()}}
{{pass}}

Como esperado, podemos aproveitar o SQLTABLE  ajudante para construir listas de mensagens em visualizações

{{=SQLTABLE(myset.select(), linkto=URL(...))}}

E, claro, é possível alimentar um auxiliar de formulário com o valor de id de seqüência apropriado

{{=SQLFORM(imapdb.INBOX, <message id>, fields=[...])}}

Os campos suportados pelo adaptador atual disponíveis são os seguintes:

Campo TipoDescrição
uidstring
answeredbooleanFlag
createddate
contentlist:stringA list of text or html parts
tostring
ccstring
bccstring
sizeintegerthe amount of octets of the message*
deletedbooleanFlag
draftbooleanFlag
flaggedbooleanFlag
senderstring
recentbooleanFlag
seenbooleanFlag
subjectstring
mimestringThe mime header declaration
emailstringThe complete RFC822 message**
attachmentslistEach non text decoded part as dictionary
encodingstringThe message's main detected charset

* No lado da aplicação é medido como o comprimento do RFC822 string de mensagem

ATENÇÃO: Como os ids de linha são mapeados para números de sequência de e-mail, verifique se o aplicativo web2py do cliente IMAP não exclui mensagens durante ações de seleção ou atualização, para impedir a atualização ou exclusão de mensagens diferentes.

Operações padrão CRUD  de banco de dados não são suportadas. Não há como definir campos ou tabelas personalizadas e fazer inserções com tipos de dados diferentes, pois a atualização de caixas de correio com serviços IMAP é geralmente reduzida para atualizações de sinalizador de postagem no servidor. Ainda assim, é possível acessar esses comandos de sinalização através da interface DAL IMAP

Para marcar as últimas mensagens de consulta conforme visto

seen = imapdb(q).update(seen=True)

Aqui nós excluímos mensagens no banco de dados IMAP que possuem e-mails do sr. Gumby

deleted = 0
for tablename in imapdb.tables():
    deleted += imapdb(imapdb[tablename].sender.contains("gumby")).delete()

Também é possível marcar mensagens para exclusão em vez de apagá-las diretamente com

myset.update(deleted=True)
IMAP
 top