Chapter 8: Poczta elektroniczna i SMS
Poczta elektroniczna i SMS
Konfiguracja poczty elektronicznej
Platforma web2py udostępnia klasę gluon.tools.Mail
umożliwiającą łatwe wysyłanie wiadomości email z poziomu web2py. Definicja mailera może wyglądać następująco:
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'
Uwaga, jeśli aplikacja stosuje uwierzytelnianie (omówione w następnej części), to obiekt auth
będzie udostępniał własny mailer w metodzie auth.settings.mailer
, tak więc można zamiennie zastosować następujące ustawienia:
mail = auth.settings.mailer
mail.settings.server = 'smtp.example.com:25'
mail.settings.sender = 'you@example.com'
mail.settings.login = 'username:password'
W mail.settings wstaw odpowiednie parametry swojego serwera SMTP. Ustaw mail.settings.login = None
jeśli serwer SMTP nie wymaga uwierzytelnienia. Jeśli nie zamierzasz używać TLS, ustaw mail.settings.tls = False
W celach debugowania można ustawić
mail.settings.server = 'logging'a wiadomości nie będą wysyłane, ale zamiast tego rejestrowane na konsoli.
Konfiguracja poczty elektronicznej dla Google App Engine
Wysyłania wiadomości email na koncie Google App Engine wymaga ustawienia:
mail.settings.server = 'gae'
W czasie pisania tego tekstu web2py nie obsługiwał załączników i szyfrowania wiadomości na Google App Engine. Trzeba mieć na uwadze, że dla GAE nie działa cron ani terminarz.
x509 i szyfrowanie PGP
W celu wysyłania wiadomości zaszyfrowanych w standarcie x509 (SMIME) trzeba wykonać następującą konfigurację:
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'
Istnieje możliwość wysyłania wiadomości szyfrowanych w PGP. Przede wszystkim należy zainstalować pakiet python-pyme. Następnie trzeba użyć GnuPG (GPG) w celu utworzenia pliku klucza dla metody sender
(pobierającej adres wiadomości z mail.settings.sender) oraz umieścić pliki pubring.gpg i secring.gpg w katalogu (np. "/home/www-data/.gnupg").
Użyj następujących ustawień:
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
Wysyłanie wiadomości email
Po zdefiniowaniu mailera mail
można wysłać wiadomość stosując:
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')
Metoda mail.send()
w razie powodzenia zwraca True
a w przeciwnym wypadku False
. Pełny wykaz argumentów tej metody jest następujący:
send(self, to, subject='None', message='None', attachments=[],
cc=[], bcc=[], reply_to=[], sender=None, encoding='utf-8',
raw=True, headers={})
Proszę zwrócić uwagę, że każdy z argumentów to
, cc
i bcc
pobiera listę adresów email.
Argument sender
ma wartość domyślną None
i w takim przypadku sender
wystarczy ustawić na mail.settings.sender
.
Argument headers
jest słownikiem zmieniającym nagłówki wiadomości tuż przed wysłaniem. Na przykład:
headers = {'Return-Path' : 'bounces@example.org'}
Poniżej znajdują się przykłady pokazujące użycie mail.send()
.
Prosta wiadomość tekstowa
mail.send('you@example.com',
'Message subject',
'Plain text body of the message')
Wiadomości w formacie HTML
mail.send('you@example.com',
'Message subject',
'<html>html body</html>')
Jeśli ciało wiadomości rozpoczyna się od <html>
i kończy </html>
, to wiadomość zostanie wysłana w formacie HTML.
Łączenie wiadomości tekstowych i HTML
Wiadomości email mogą być krotkami (text, html):
mail.send('you@example.com',
'Message subject',
('Plain text body', '<html>html body</html>'))
Adresy cc
i bcc
mail.send('you@example.com',
'Message subject',
'Plain text body',
cc=['other1@example.com', 'other2@example.com'],
bcc=['other3@example.com', 'other4@example.com'])
Załączniki
mail.send('you@example.com',
'Message subject',
'<html><img src="cid:photo" /></html>',
attachments = mail.Attachment('/path/to/photo.jpg', content_id='photo'))
Wiele załączników
mail.send('you@example.com',
'Message subject',
'Message body',
attachments = [mail.Attachment('/path/to/fist.file'),
mail.Attachment('/path/to/second.file')])
Wysyłanie wiadomości SMS
Wysyłanie wiadomości SMS z poziomu aplikacji web2py wymaga usługi zewnętrznej, która może przekazywać wiadomości do odbiorcy. Zazwyczaj jest to usługa odpłatna, ale zależy to od kraju. Sprawdziliśmy kilka z nich, niestety ze słabym skutkiem. Firmy telekomunikacyjne blokują wiadomości email wysyłane za ich pomocą, gdyż usługi takie są często źródłem spamu.
Lepszym rozwiązaniem dla wysyłania wiadomości SMS jest wykorzystanie firm telekomunikacyjnych. Bowiem każda z nich przypisuje unikatowy adres email do numeru telefonu komórkowego, dzięki czemu wiadomości SMS mogą być wysyłane jako wiadomości email na telefon komórkowy
web2py dostarczany jest z modułem pomagajacym w tym procesie:
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 to słownik odwzorowujący nazwy głównych firm telekomunikacyjnych na adresy email postfixa. Funkcja sms_email
pobiera numer telefonu (jako ciąg znakowy) i nazwę firmy telekomunikacyjnej, a zwraca adres email telefonu.
Wykorzystywanie systemu szablonów do generowania wiadomości
Do wygenerowania wiadomości email można wykorzystać system szablonów. Na przykład, przyjmijmy następującą tabelę bazy danych:
db.define_table('person', Field('name'))
i to. że każdej osobie zawartej w tej bazie chcemy wysłać wiadomość zapisaną w pliku widoku "message.html":
Dear {{=person.name}},
You have won the second prize, a set of steak knives.
Można to osiągnąć w następujacy sposób:
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)
Większość pracy jest wykonywana przez wyrażenie:
response.render('message.html', context)
Kod ten renderuje widok "message.html" ze zmiennymi zdefiniowanymi w słowniku "context" i zwraca ciąg znakowy z przetworzonym tekstem wiadomości. Argument context
jest słownikiem zawierającym zmienne, które będą widoczne w pliku szablonowym.
Jeśli ciało wiadomości email zaczyna się od <html> i kończy na </html>, to wiadomość będzie miała format HTML.
Proszę zauważyć, że jeśli chce się umieścić w treści HTML odnośnik zwrotny do swojej strony, to można skorzystać z funkcji URL
, ale funkcja URL
domyślnie generuje względny adres URL, który może nie działać w poczcie elektronicznej. Do wygenerowania bezwzględnego adresu URL, należy określić w funkcji URL
argumenty scheme
i host
. Na przykład:
<a href="{{=URL(..., scheme=True, host=True)}}">Click here</a>
lub
<a href="{{=URL(..., scheme='http', host='www.site.com')}}">Click here</a>
Ten sam mechanizm, używany do wygenerowania tekstu wiadomości email, może być również wykorzystany do wygenerowania wiadomości SMS jak i wiadomości każdego innego typu.
Wysyłanie wiadomości email przy użyciu zadań w tle
Operacja wysyłania wiadomości email może potrwać kilka sekund, ponieważ konieczne jest zalogowanie się i nawiązanie łączności ze zdalnym serwerem SMTP, z którego przeważnie się korzysta. Można skrócić czas oczekiwania przez użytkownika na zakończenie tej operacji ustawiając wiadomości email w kolejce, tak aby wysłane one były później przez zadanie działające w tle. Jak opisano w Rozdziale 4, zadanie takie można ustawić samemu tworząc własna kolejkę lub korzystając z terminarza. Poniżej przedstawiamy przykład wykorzystania własnej kolejki zadań.
Najpierw, w pliku modelu aplikacji tworzymy model bazy danych do przechowywania kolejki email:
db.define_table('queue',
Field('status'),
Field('email'),
Field('subject'),
Field('message'))
Potem w kontrolerze ustawiamy w kolejce wiadomości, które trzeba wysłać:
db.queue.insert(status='pending',
email='you@example.com',
subject='test',
message='test')
Następnie tworzymy skrypt działający w tle, który odczytuje kolejkę i wysyła wiadomości:
## 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
Na koniec, tak jak opisano w Rozdziale 4, musimy uruchomić skrypt mail_queue.py w taki sposób, jak by był umieszczony wewnątrz kontrolera aplikacji.
python web2py.py -S app -M -R applications/app/private/mail_queue.py
gdzie -S app
wskazuje konieczność uruchomienia przez web2py "mail_queue.py" jako "app", a -M
jest poleceniem wykonania modeli.
Zakładamy tu, że obiekt mail
do którego odwołuje się skrypt "mail_queue.py" został zdefiniowany w pliku modelu aplikacji a dlatego, że użyto opcję -M, jest on dostępny w skrypcie "mail_queue.py". W celu uniknięcia zablokowania bazy danych przez inny, współbieżny proces, ważne jest, aby wprowadzać jak najszybciej każdą zmianę.
Jak wspomniano w rozdziale 4, ten typ procesu działajacego w tle nie powinien być wykonywany przez cron (może z wyjątkiem cron @reboot
), ponieważ trzeba mieć pewność, że w tym czasie nie jest uruchomionych wiecej instancji niż jedna.
Jedną z wad wysyłania wiadomości email z wykorzystaniem procesu działającego w tle jest to, że trudno jest dostarczyć użytkownikowi informację zwrotną w przypadku niepowodzenia wysyłki. Jeśli wiadomość email zostanie wysłana bezpośrednio z poziomu akcji kontrolera, to można przechwycić błędy i natychmiast zwrócić komunikat o błędzie. Jednakże w przypadku procesu działającego tle, wiadomość email jest wysyłana asynchronicznie już po zwróceniu odpowiedzi przez akcję kontrolera, utrudniając tym samym powiadomienie użytkownika o błędzie.
Odczytywanie i zarządzanie skrzynkami email (eksperymantalnie)
Adapter IMAP jest interfejsem serwerów IMAP wykonującym proste zapytania składni DAL, tak więc wiadomości są odczytywane, wyszukiwane oraz wykonywane są inne usługi związane z pocztą IMAP. Na przykład, z poziomu aplikacji web2py mogą być zarządzane usługi pocztowe zaimplementowane przez Google czy Yahoo.
Adapter ten tworzy "statycznie" własne tabele i nazwy pól, co oznacza, że programista nie powinien ich sam definiować, pozostawiając to zadanie instancji DAL i ograniczając się do wywoływania metody adaptera .define_tables()
. Tabele zostają zdefiniowane z informacjami o liście skrzynek pocztowych serwera IMAP.
Połączenie
Oto zalecany kod rozpoczynający obsługę IMAP w modelu aplikacji dla pojedynczego konta pocztowego:
# Zmień użytkownika, hasło, serwer i port w ciągu połączenia
# Ustaw port 993 dla obsługi SSL
imapdb = DAL("imap://user:password@server:port", pool_size=1)
imapdb.define_tables()
Proszę zwrócić uwagę, że <imapdb>.define_tables()
zwraca słownik łańcuchów odwzorowujących nazwy tabel DAL na nazwy skrzynek pocztowych serwera o następującej strukturze {<tablename>: <server mailbox name>, ...}
, tak więc na serwerze IMAP istnieje możliwość pobrania rzeczywistej nazwy skrzynki pocztowej .
Jeśli chce się ustawić własną konfigurację 'nazwa tabeli – nazwa skrzynki pocztowej' i pominąć automatyczną konfigurację nazw, można przekazać do adaptera własny słownik, w ten sposób:
imapdb.define_tables({"inbox":"MAILBOX", "trash":"SPAM"})
W celu umożliwienia obsługi różnych natywnych nazw skrzynek pocztowych w interfejsie użytkownika, web2py udostępnia następujące atrybuty pozwalające na dostęp do automatycznie odwzorowywanych nazw (gdzie natywnej nazwie skrzynki pocztowej odpowiada nazwa tabeli i odwrotnie):
Atrybut | Typ | Format |
imapdb.mailboxes | dict | {<tablename>: <server native name>, ...} |
imapdb.<table>.mailbox | string | "server native name" |
Pierwszy z tych atrybutów może być przydatny do pobierania zestawów zapytań IMAP przez skrzynkę pocztową usługi poczty elektronicznej.
# mailbox jest ciagiem zawierajacym rzeczywista nazwę skrzynki
tablenames = dict([(v,k) for k,v in imapdb.mailboxes.items()])
myset = imapdb(imapdb[tablenames[mailbox]])
Pobieranie poczty i aktualizowanie flag
Oto lista poleceń IMAP, które można zastosować w kontrolerze. Na potrzeby poniższych przykładów, zakłada się, że usługa IMAP ma skrzynkę pocztową o nazwie INBOX
, tak samo jak w przypadku kont Gmail(r).
Kod zliczajacy dzisiejsze nieprzeczytane wiadomości, mniejsze od 6000 oktetów, w skrzynce pocztowej Inbox
, wygląda tak:
q = imapdb.INBOX.seen == False
q &= imapdb.INBOX.created == request.now.date()
q &= imapdb.INBOX.size < 6000
unread = imapdb(q).count()
Można pobrać wcześniejsze wiadomości wykorzystując zapytanie:
rows = imapdb(q).select()
Operatory zapytania są zwykle implementowane, w tym odnoszące się do:
messages = imapdb(imapdb.INBOX.uid.belongs(<uid sequence>)).select()
Uwaga: W celu uniknięcie zatorów serwera pocztowego w wyniku dużych zapytań wybierających (select), zaleca się, aby utrzymywać wyniki zapytań poniżej określonego poziomu wielkości danych.
W celu przyśpieszenia zapytań, zaleca się przekazywanie przefiltrowanych zestawów pól:
fields = ["INBOX.uid", "INBOX.sender", "INBOX.subject", "INBOX.created"]
rows = imapdb(q).select(*fields)
Adapter wie kiedy ma pobrać cząstkowe porcje wiadomości (pola takie jak content
, size
i attachments
wymagają pobrania jednorazowo wszystkich danych z tych ól).
Wyniki zapytania wybierającego (select) można przefiltrować wykorzystując pola limitby oraz sequences skrzynki pocztowej.
# Wstaw rzeczywiste wartości tych argumentów
myset.select(<fields sequence>, limitby=(<int>, <int>))
Powiedzmy, że chcemy napisać akcję aplikacji wyświetlającą wiadomość ze skrzynki pocztowej. Najpierw pobieramy tą wiadomość - trzeba pobrać wiadomości wykorzystując pole uid
, jeśli usługa IMAP daje taką możliwość, co powala uniknąć odniesień do starej sekwencji:
mymessage = imapdb(imapdb.INBOX.uid == <uid>).select().first()
W przeciwym razie trzeba użyć id
wiadmości:
mymessage = imapdb.INBOX[<id>]
Trzeba pamiętać, że używanie identyfikatora id
wiadomości jako odniesienia nie jest zalecane, gdyż numeracja sekwencji może się zmienić w wyniku operacji konserwacyjnych skrzynki pocztowej, takich jak usuwanie wiadomości. Jeśli jednak wciąż chcesz zarejestrować odniesienie do wiadomości (czyli w polu rekordu innej bazy danych), rozwiązaniem będzie użycie jako odniesienia pola uid, jeśli jest obsługiwane i pobieranie każdej wiadomości z zarejestrowaną wartością.
Na koniec, aby wyświetlić zawartość wiadomości, dodajmy w widoku kod podobny do tego:
{{=P(T("Message from"), " ", mymessage.sender)}}
{{=P(T("Received on"), " ", mymessage.created)}}
{{=H5(mymessage.subject)}}
{{for text in mymessage.content:}}
{{=DIV(text)}}
{{=TR()}}
{{pass}}
Zgodnie z oczekiwaniami, możemy w widoku skorzystać z helpera SQLTABLE do zbudowania listy wiadomości:
{{=SQLTABLE(myset.select(), linkto=URL(...))}}
Oczywiście, możliwe jest zasilenie helpera formularza odpowiednią sekwencję wartości id:
{{=SQLFORM(imapdb.INBOX, <message id>, fields=[...])}}
Aktualnie obsługiwane adaptery udostępniają następujące pola:
Pole | Typ | Opis |
uid | string | |
answered | boolean | flaga |
created | date | |
content | list:string | Lista porcji tekstowych lub html |
to | string | |
cc | string | |
bcc | string | |
size | integer | ilość oktetów w wiadomości * |
deleted | boolean | flaga |
draft | boolean | flaga |
flagged | boolean | flaga |
sender | string | |
recent | boolean | flaga |
seen | boolean | flaga |
subject | string | |
mime | string | deklaracja nagłówka mime |
string | kompletny komunikat RFC822 ** | |
attachments | list | każda nie tekstowa, odkodowana część jako słownik |
encoding | string | wykryty główny zestaw znakowy wiadomości |
*Od strony aplikacji jest to mierzone jako długość łańcucha tekstowego komunikatu RFC822
OSTRZEŻENIE: Ponieważ identyfikatory wierszy są odwzorowywane na numery sekwencji wiadomości, trzeba się upewnić, że klient IMAP aplikacji web2py nie usuwa wiadomości w trakcie wykonywania akcji wybierających i aktualizujących.
Nie są obsługiwane standardowe operacje CRUD na bazie danych. Nie ma możliwości zdefiniowania własnych pól lub tabel i wstawiania odmiennych typów danych, gdyż aktualizacja skrzynek pocztowych usług IMAP jest zazwyczaj zredukowana do publikowania na serwerze flagi aktualizacji. Mimo to, możliwe jest uzyskanie dostępu do poleceń poprzez interfejs IMAP mechanizmu DAL.
Do oznaczenia ostatnich wiadomości jako przeczytanych, wystarczy taki kod:
seen = imapdb(q).update(seen=True)
Tutaj usuwamy wiadomości z bazy danych IMAP, kierowanych do pana Gumby
deleted = 0
for tablename in imapdb.tables():
deleted += imapdb(imapdb[tablename].sender.contains("gumby")).delete()
Możliwe jest również oznaczenie wiadomości do usunięcia, zamiast wymazywać je od razu:
myset.update(deleted=True)