Chapter 8: Poczta elektroniczna i SMS

Poczta elektroniczna i SMS

poczta elektroniczna

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:

1
2
3
4
5
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:

1
2
3
4
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

dziennik zdarzeń poczty elektronicznej

W celach debugowania można ustawić

1
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

poczta elektroniczna dla GAE

Wysyłania wiadomości email na koncie Google App Engine wymaga ustawienia:

1
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

PGP
x509

W celu wysyłania wiadomości zaszyfrowanych w standarcie x509 (SMIME) trzeba wykonać następującą konfigurację:

1
2
3
4
5
6
7
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ń:

1
2
3
4
5
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

mail.send
email html
email attachments
wiadomości email w formacie html
załaczniki email

Po zdefiniowaniu mailera mail można wysłać wiadomość stosując:

1
2
3
4
5
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:

1
2
3
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:

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

Poniżej znajdują się przykłady pokazujące użycie mail.send().

Prosta wiadomość tekstowa

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

Wiadomości w formacie HTML

1
2
3
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):

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

Adresy cc i bcc

1
2
3
4
5
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

1
2
3
4
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

1
2
3
4
5
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

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:

1
2
3
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

wiadomosci email

Do wygenerowania wiadomości email można wykorzystać system szablonów. Na przykład, przyjmijmy następującą tabelę bazy danych:

1
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":

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

Można to osiągnąć w następujacy sposób:

1
2
3
4
5
6
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:

1
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:

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

lub

1
<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:

1
2
3
4
5
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ć:

1
2
3
4
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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
## 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.

1
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:

1
2
3
4
# 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:

1
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):

AtrybutTypFormat
imapdb.mailboxesdict{<tablename>: <server native name>, ...}
imapdb.<table>.mailboxstring"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.

1
2
3
# 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:

1
2
3
4
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:

1
rows = imapdb(q).select()

Operatory zapytania są zwykle implementowane, w tym odnoszące się do:

1
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:

1
2
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.

1
2
# 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:

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

W przeciwym razie trzeba użyć id wiadmości:

1
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:

1
2
3
4
5
6
7
{{=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:

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

Oczywiście, możliwe jest zasilenie helpera formularza odpowiednią sekwencję wartości id:

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

Aktualnie obsługiwane adaptery udostępniają następujące pola:

PoleTypOpis
uidstring
answeredbooleanflaga
createddate
contentlist:stringLista porcji tekstowych lub html
tostring
ccstring
bccstring
sizeintegerilość oktetów w wiadomości *
deletedbooleanflaga
draftbooleanflaga
flaggedbooleanflaga
senderstring
recentbooleanflaga
seenbooleanflaga
subjectstring
mimestringdeklaracja nagłówka mime
emailstringkompletny komunikat RFC822 **
attachmentslistkażda nie tekstowa, odkodowana część jako słownik
encodingstringwykryty 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:

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

Tutaj usuwamy wiadomości z bazy danych IMAP, kierowanych do pana Gumby

1
2
3
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:

1
myset.update(deleted=True)
IMAP
 top