Chapter 8: Email and SMS

Emaily a SMS

Mail

Nastavení emailů

Web2py nabízí ke snadnému odesílání mailů z Web2py třídu gluon.tools.Mail. Použijeme ji takto:

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

Jestliže vaše aplikace používá Auth (jak popisujeme v další kapitole), objekt auth už má instanciovaný vlastní mailer (objekt pro odesílání pošty), takže nemusíte vytvářet novou instanci, ale můžete ji převzít:

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

Samozřejmě je třeba uvést správné údaje vašeho poštovního serveru. Nastavte mail.settings.login = None, pokud SMTP server neprovádí autentizaci (zjištění identity). Nechcete-li používat TLS (šifrování), nastavte mail.settings.tls = False

email logging

Pro účely ladění můžete nastavit:

mail.settings.server = 'logging'

a emaily pak nebudou odesílány a místo toho budou logovány na konzoli.

Konfigurace emailu pro Google App Engine

email from GAE

Odesílání emailů z Google App Engine účtu nastavíte takto:

mail.settings.server = 'gae'

V okamžiku psaní dokumentace Web2py nepodporuje přílohy a šifrované maily na Google App Engine. Také připomeňme, že na GAE nebude pracovat cron a scheduler.

x509 a PGP šifrování

PGP
x509

Lze posílat x509 (SMIME) zašifrované emaily pomocí následujících nastavení:

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'

Lze také posílat PGP zašifrované emaily. Nejprve musíte nainstalovat package pyme (python-pyme). Pak můžete používat GnuPG (GPG) a vytvořit soubory klíčů (key-files) pro odesílatele (s emailovou adresou z mail.settings.sender) a uložit soubory pubring.gpg a secring.gpg do adresáře (např. "/home/www-data/.gnupg").

Pak použijte tato nastavení:

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

Odesílání emailů

mail.send
email html
email attachments

Poté, co je založen objekt mail, můžete s jeho pomocí odesílat maily takto:

mail.send(to=['nekomu@priklad.com'],
          subject='nazdar',
          # vynecháte-li reply_to, použije se adresa z mail.settings.sender
          reply_to='odesila@mujserver.com',
          message='zdravím Tě')

Mail vrátí True, jestliže se podařilo zprávu odeslat, jinak vrátí False. Úplný seznam argumentů pro mail.send() je:

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

Do to, cc i bcc se emailové adresy zadají jako seznam (list).

Default pro sender je None, v tom případě se adresa převezme z mail.settings.sender.

headers může být slovník (dictionary) hlaviček k upřesnění hlaviček (headers) těsně před odesláním mailu. Například:

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

Následuje několik příkladů použití mail.send():

Jednoduchý textový email

mail.send('nekomu@priklad.com',
  'Předmět zprávy',
  'Tělo zprávy jako prostý text (plain text)')

HTML emaily

mail.send('nekomu@priklad.com',
  'Předmět zprávy',
  '<html>html body</html>')

Jinak řečeno, jestliže tělo zprávy začíná <html> a končí </html>, bude odesláno jako HTML email.

Skládání textu a HTML

Parametr message také může být dvojice (tuple) (text, html):

mail.send('nekomu@priklad.com',
  'Předmět zprávy',
  ('Prostý text', '<html>html body</html>'))

Příjemci v kopii

mail.send('nekomu@priklad.com',
  'Předmět zprávy',
  'Prostý text',
  cc=['other1@example.com', 'other2@example.com'],
  bcc=['other3@example.com', 'other4@example.com'])

Přílohy

mail.send('nekomu@priklad.com',
  'Předmět zprávy',
  '<html><img src="cid:photo" /></html>',
  attachments = mail.Attachment('/path/to/photo.jpg', content_id='photo'))

Více příloh

mail.send('nekomu@priklad.com',
  'Předmět zprávy',
  'Text zprávy',
  attachments = [mail.Attachment('/path/to/prvni.soubor'),
                 mail.Attachment('/path/to/dalsi.soubor')])

Odesílání SMS zpráv

SMS

Odesílání SMS zpráv z Web2py aplikace vyžaduje použít službu třetí strany, která dopraví zprávu příjemci. To typicky není služba zdarma, i když to se liší v různých zemích. Několik služeb jsme vyzkoušeli, ale s poměrně malým úspěchem. Telefonní společnosti blokují zprávy, které pocházejí z těchto služeb, protože jsou často zdrojem spamu.

Lepší způsob je použít samotné telefonní společnosti k přijetí SMS. Každá telefonní společnost má emailové adresy unikátní ke každému číslu mobilního telefonu, takže SMS zprávy lze posílat jako emaily na nějaké telefonní číslo.

Web2py to usnadňuje pomocí modulu:

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 je slovník (dictionary), který mapuje jména operátorů na postfix emailové adresy. Funkce sms_email vezme telefonní číslo (řetězec) a podle jména telefonní společnosti vrátí emailovou adresu telefonu.

Využití systému šablon (template) ke generování zpráv

emails

Předpokládejme např. tabulku

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

a chcete každé jednotlivé osobě poslat zprávu, uloženou v šabloně "message.html":

Vážený(á) {{=person.name}},
Vyhrál(a) jste druhou cenu, sadu steakových nožů.

Zařídíte to takto:

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

Většinu práce zajistí příkaz

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

Renderuje šablonu "message.html" za pomoci proměnných, definovaných ve slovníku "context", a vrátí řetězec s výsledným textem emailu. context je slovník, který obsahuje proměnné, které budou dostupné pro renderovaný template soubor.

Jak už jsme uvedli, bude-li zpráva začínat <html> a končit </html>, dostaneme HTML email.

Jestliže bychom chtěli do HTML emailu přidat odkaz na naši webovou stránku, můžeme použít funkci URL. Ale funkce URL defaultně generuje relativní URL, která z mailu pochopitelně nebude fungovat. Nesmíme proto zapomenout sestavit absolutní URL, a sice pomocí argumentů scheme a host:

<a href="{{=URL(..., scheme=True, host=True)}}">Klikněte zde</a>

nebo

<a href="{{=URL(..., scheme='http', host='www.site.com')}}">Klikněte zde</a>

Stejný mechanismus jako pro generování personalizovaných emailů lze použít i pro generování SMS nebo jakýchkoli jiných zpráv.

Posílání zpráv pomocí úlohy na pozadí

Operace odeslání mailu může trvat několik vteřin, protože se potřebujete přihlásit k (potenciálně vzdálenému (remote)) SMTP serveru a komunikovat s ním. Abychom nenechávali uživatele čekat, může být rozumné řadit emaily do fronty a odesílat je později za pomoci úlohy na pozadí. V kapitole 4 jsme popsali, že to lze udělat pomocí vlastní fronty úloh nebo pomocí Web2py scheduleru (plánovače). Zde si ukážeme příklad s vlastní frontou úloh.

Nejprve si v databázovém modelu vytvoříme tabulku pro ukládání naší fronty (queue) emailů:

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

Z akce kontroléru pak budeme řadit zprávy do fronty takto:

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

Nyní budeme potřebovat na pozadí běžící skript, který načte neodeslané emaily ve frontě a odešle je:

## v souboru /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) # zjišťuj nové po minutě

Nakonec, jak bylo popsáno v kapitole 4, budeme potřebovat spustit skript mail_queue.py v podobném prostředí, jako by se kód nacházel v kontroléru naší aplikace:

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

kde -S app řekne Web2py, aby skript "mail_queue.py" spustila jako v aplikaci "app", a -M způsobí vykonání modelu(ů) této aplikace.

Zde předpokládáme, že objekt mail, na který se odkazuje skript "mail_queue.py", je definovaný v modelu naší aplikace a tedy z "mail_queue.py" dostupný, díky zavolání s volbou -M. Také si uvědomíme, že je důležité explicitně commitovat (potvrdit) provedené změny, a to co nejdříve, abychom nezamykali databázi na dlouho pro souběžné procesy.

Jak jsme poznamenali v kapitole 4, tento typ procesů na pozadí bychom neměli spouštět cronem (snad s výjimkou cron @reboot), protože si musíme být jistí, že ve stejném okamžiku nepoběží více než jedna instance.

Jedna okolnost hovoří proti odesílání mailů za pomoci procesu na pozadí, a sice že je obtížné dát zpětnou zprávu (feedback) uživateli v případě, že odeslání maliu nakonec selže. Zatímco při přímém odeslání z akce kontroléru můžete chyby odchytit a hned reportovat uživateli, v případě procesu na pozadí se maily posílají asynchronně, se zpožděním za dokončením akce kontroléru a vrácením odpovědi (response), takže je mnohem složitější uživatele informovat o výsledku.

Čtení a správa emailových schránek (experimentální)

IMAP adaptér je zamýšlen jako rozhraní pro emailové IMAP servery k vykonávání jednoduchých dotazů pomocí Web2py DAL dotazovací syntaxe. To znamená, že čtení emailu, vyhledávání nebo jiné IMAP mailové služby (např. v implementaci Googlu nebo Yahoo) mohou být řízeny z Web2py aplikací.

Strukturu tabulek a polí zajistí IMAP adaptér, neboli vývojář má přenechat definici tabulek DAL instanci, a sice tak, že zavolá metodu .define_tables(). Tabulky se vytvoří na základě informace o jednotlivých mailových schránkách účtu, které vrátí IMAP server.

Připojení

Pro připojení k jednomu konkrétnímu mailovému účtu a zahájení IMAP podpory doporučujeme tento model:

# nahraď uživatele (user), heslo (password), server a port konkrétními údaji
# nastav port 993 pro SSL podporu
imapdb = DAL("imap://user:password@server:port", pool_size=1)
imapdb.define_tables()

<imapdb>.define_tables() vrátí slovník, který mapuje jména DAL tabulek na jména poštovních schránek (server mailbox names), má tedy strukturu {<jmeno_tabulky>: <jmeno_schranky_na_serveru>, ...}, takže můžete zjistit jména schránek, vrácená z IMAP serveru.

Můžete místo toho zadat vlastní konfiguraci mapování jmeno_tabulky/jmeno_schranky. Tuto konfiguraci zadáte adaptéru takto:

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

Pro práci se skutečnými jmény schránek v uživatelském rozhraní jsou k dispozici následující atributy:

AtributTypFormát
imapdb.mailboxesdict{<jméno_tabulky>: <serverové_jméno_schránky>, ...}
imapdb.<table>.mailboxstring"serverové_jméno_schránky"

První z nich se hodí, když chcete získat Set objekt za pomoci serverového jména schránky:

# mailbox je řetězec se jménem schránky na serveru
tablenames = dict([(v,k) for k,v in imapdb.mailboxes.items()])
myset = imapdb(imapdb[tablenames[mailbox]])

Načtení (fetching) mailů a aktualizace příznaků

Zde jsou IMAP příkazy, které můžete použít v kontroléru. V příkladech předpokládáme, že IMAP služba má mimo jiné schránku INBOX, jako je tomu v případě účtů na Gmailu.

Pro spočítání dnešních nepřečtených zpráv do určité velikosti:

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

Tytéž zprávy nyní stáhneme:

rows = imapdb(q).select()

Jsou implementovány obvyklé operátory dotazů, včetně belongs

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

Poznámka: Důrazně se doporučuje používat takové dotazy, aby výsledky byly rozumně malé a nedošlo k zahlcení serveru příliš rozsáhlým selectem.

Ke zrychlení je také vhodné omezit seznam načítaných polí:

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

Pole jako content, size nebo attachments vyžadují stažení celé zprávy.

Kromě omezení polí lze použít omezení pomocí limitby

myset.select(<fields sequence>, limitby=(<int>, <int>))

Dejme tomu, že chcete pomocí akce kontroléru zobrazit zprávu ze schránky. Nejprve načteme zprávu. Jestliže to IMAP služba podporuje, stahujme zprávu pomocí pole uid.

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

V opačném případě využijeme id zprávy.

mymessage = imapdb.INBOX[<id>]

Poznamenejme, že používání id jako odkazu na zprávu se nedoporučuje, protože id se mohou změnit při operacích se schránkou jako je třeba rušení zpráv. Pokud tedy chceme udržovat odkazy na zprávy, je potřeba ukládat pole uid field, pokud je to možné a zprávu pak kompletně načítat pomocí uid.

Nakonec přidáme do šablony (view) nějaké formátování pro zobrazení zprávy, např.

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

Pro zobrazení seznamu zpráv můžeme použít helper SQLTABLE:

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

Ke zobrazení zprávy je také možné použít SQLFORM:

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

Adaptér nyní podporuje tato pole:

PoleTypPopis
uidstring
answeredbooleanodpovězeno (příznak)
createddate
contentlist:stringseznam textu nebo html částí
tostringadresát
ccstringadresát kopie
bccstringadresát slepé kopie
sizeintegervelikost zprávy*
deletedbooleanzrušená (příznak)
draftboolean(příznak)
flaggedboolean(příznak)
senderstringodesílatel
recentboolean(příznak)
seenbooleanpřečteno (příznak)
subjectstring
mimestringdeklarace mime hlavičky
emailstringúplná RFC822 zpráva**
attachmentslistpřílohy jako slovník částí
encodingstringdetekovaná znaková sada zprávy

*Na straně aplikace se měří jako délka RFC822 řetězce zprávy

Varování: Protože id jsou mapována na pořadová čísla emailů, nepoužívejte rušení zpráv pomocí IMAP klienta, alespoň ne mezi načtením (select) a následnými aktualizacemi (update). Jinak by se mohlo stát, že aktualizujete nebo zrušíte jinou zprávu (s posunutým id).

Nejsou podporovány standardní CRUD databázové operace. Nelze definovat uživatelská pole nebo tabulky a vkládat data jiných typů, protože aktualizace schránky pomocí IMAP služby je obvykle omezena jen na nastavování příznaků u zpráv. Tyto příznaky můžeme pomocí DAL IMAP rozhraní ovládat:

Např. označíme maily, získané posledním dotazem, jako přečtené:

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

V tomto příkladu smažeme zprávy od pana Gumbyho:

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

Také je možné jen označit zprávy ke zrušení místo okamžitého fyzického rušení:

myset.update(deleted=True)
IMAP
 top