Chapter 8: Email and SMS
Emaily a SMS
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
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
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í
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ů
Poté, co je založen objekt mail
, můžete s jeho pomocí odesílat maily takto:
mail.send(to=['[email protected]'],
subject='nazdar',
# vynecháte-li reply_to, použije se adresa z mail.settings.sender
reply_to='[email protected]',
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' : '[email protected]'}
Následuje několik příkladů použití mail.send()
:
Jednoduchý textový email
mail.send('[email protected]',
'Předmět zprávy',
'Tělo zprávy jako prostý text (plain text)')
HTML emaily
mail.send('[email protected]',
'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('[email protected]',
'Předmět zprávy',
('Prostý text', '<html>html body</html>'))
Příjemci v kopii
mail.send('[email protected]',
'Předmět zprávy',
'Prostý text',
cc=['[email protected]', '[email protected]'],
bcc=['[email protected]', '[email protected]'])
Přílohy
mail.send('[email protected]',
'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('[email protected]',
'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
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
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='[email protected]',
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:
Atribut | Typ | Formát |
imapdb.mailboxes | dict | {<jméno_tabulky>: <serverové_jméno_schránky>, ...} |
imapdb.<table>.mailbox | string | "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:
Pole | Typ | Popis |
uid | string | |
answered | boolean | odpovězeno (příznak) |
created | date | |
content | list:string | seznam textu nebo html částí |
to | string | adresát |
cc | string | adresát kopie |
bcc | string | adresát slepé kopie |
size | integer | velikost zprávy* |
deleted | boolean | zrušená (příznak) |
draft | boolean | (příznak) |
flagged | boolean | (příznak) |
sender | string | odesílatel |
recent | boolean | (příznak) |
seen | boolean | přečteno (příznak) |
subject | string | |
mime | string | deklarace mime hlavičky |
string | úplná RFC822 zpráva** | |
attachments | list | přílohy jako slovník částí |
encoding | string | detekovaná 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)