Chapter 8: Работа с Emails и SMS

Emails и SMS

Mail

Настройка электронной почты

Web2py предоставляет класс gluon.tools.Mail для облегчения отправки электронной почты с помощью web2py. Можно определить почтовую программу (mailer):

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'

Обратите внимание, если ваше приложение использует Auth (обсуждается в следующей главе), то объект auth будет включает в себя свою собственную почтовую программу в auth.settings.mailer, так что вы можете воспользоваться этим следующим образом:

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'

Вам нужно заменить mail.settings на соответствующие параметры для вашего SMTP-сервера. Установите mail.settings.login = None если сервер SMTP не требует аутентификации. Если вы не хотите использовать TLS, то установите mail.settings.tls = False

email logging
Для целей отладки вы можете задать
1
mail.settings.server = 'logging'
и электронные письма не будут отправляться, а выполнится авторизация в консоль.

Конфигурирование электронной почты для Google App Engine

email from GAE

Для отправки сообщений электронной почты из аккаунта Google App Engine:

1
mail.settings.server = 'gae'

На момент написания web2py не поддерживает вложения и зашифрованные сообщения электронной почты на Google App Engine. Обратите внимание, что Cron и планировщик не работают на GAE.

Шифрование x509 и PGP

PGP
x509

Существует возможность отправить x509 (SMIME) зашифрованные сообщения электронной почты, используя следующие параметры:

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'

Есть возможность отправлять зашифрованные сообщения PGP. В первую очередь вам необходимо установить пакет python-pyme. Затем вы можете использовать GnuPG (GPG), чтобы создать ключ-файлы для отправителя (взять Email-адрес от mail.settings.sender) и поместить файлы pubring.gpg и secring.gpg в директорию (например "/home/www-data/.gnupg").

Используйте следующие настройки:

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

Отправка сообщений электронной почты

mail.send
email html
email attachments

После того, как mail определен, он может быть использован для отправки электронной почты с помощью:

1
2
3
4
5
mail.send(to=['somebody@example.com'],
          subject='hello',
          # Если reply_to опущен, то используется mail.settings.sender
          reply_to='us@example.com',
          message='hi there')

Mail возвращает True, если удалось отправить почту и False в противном случае. Полный список аргументов для mail.send() выглядит следующим образом:

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

Обратите внимание, что to, cc, и bcc каждый принимает список адресов электронной почты.

sender по умолчанию None и в этом случае отправитель будет установлен в mail.settings.sender.

Headers является словарем заголовков для переопределения заголовков перед отправкой электронной почты. Например:

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

Ниже приведены некоторые дополнительные примеры, демонстрирующие использование mail.send().

Простые текстовые сообщения электронной почты

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

HTML сообщения электронной почты

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

Если тело электронной почты начинается с <html> и заканчивается </html>, то оно будет отправлено как HTML сообщения электронной почты.

Комбинация текстовых и HTML-сообщений электронной почты

Сообщение электронной почты может быть кортежем (text, html):

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

cc и 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'])

Вложения

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'))

Множественные вложения

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')])

Отправка SMS-сообщений

SMS

Отправка SMS-сообщений из приложения web2py требует сервис третьей стороны, который может передавать сообщения получателю. Обычно это не бесплатная услуга, но она отличается от страны к стране. Мы попробовали некоторые из этих сервисов, с небольшим успехом. Телефонные компании блокируют электронные письма, исходящие из этих сервисов так как они в конечном счете, используется в качестве источника спама.

Лучший способ заключается в использовании телефонных компаний для самостоятельной ретрансляции SMS. Каждая телефонная компания имеет адрес электронной почты, однозначно связанный с каким то номером сотового телефона, так что SMS-сообщения могут быть отправлены как сообщения электронной почты на номер телефона.

web2py поставляется с модулем, который помогает в этом процессе:

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 это словарь, который сопоставляет имена крупных телефонных компаний с электронным адресом Postfix. Функция sms_email принимает телефонный номер (как строку) и имя телефонной компании и возвращает адрес электронной почты телефона.

Использование системы шаблонов для генерирования сообщений

emails

Есть возможность использовать систему шаблонов для генерации сообщений электронной почты. Например, рассмотрим таблицу базы данных

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

где вы хотите отправить каждому человеку, в базе данных следующее сообщение, которое хранится в файле представления "message.html":

1
2
Уважаемый {{=person.name}},
Вы выиграли второй приз, набор ножей для стейка.

Вы можете добиться этого следующим образом

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)

Большая часть работы выполняется в операторе

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

Он визуализирует представления "message.html" с переменными, определенными в словаре "context", и он возвращает строку с визуализированным текстовым сообщением электронной почты. Контекст представляет собой словарь, содержащий переменные, которые будут видны в файле шаблона.

Если сообщение начинается с <html> и заканчивается </html>, то электронная почта будет передаваться HTML сообщение электронной почты.

Обратите внимание, если вы хотите включить ссылку на ваш сайт в HTML сообщение электронной почты, то вы можете использовать функцию URL. Тем не менее, по умолчанию, функция URL генерирует относительный URL-адрес, который не будет работать из сообщения электронной почты. Чтобы сгенерировать абсолютный URL-адрес, необходимо задать аргументы scheme и host в функции URL. Например:

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

или

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

Тот же самый механизм, который используется для генерации текстового сообщения электронной почты, также может быть использован для генерирования SMS-сообщения или любого другого типа сообщения, основанного на шаблоне.

Отправка сообщений с использованием фоновой задачи

Операция отправки сообщений электронной почты может занять до нескольких секунд из-за необходимости авторизации и коммуникации с потенциально удаленным сервером SMTP. Чтобы оградить пользователя от необходимости ожидания завершения операции отправки, иногда желательно электронное сообщение поставить в очередь для отправки в более позднее время с помощью фоновой задачи. Как описано в главе 4, это может быть сделано путем создания самодельной очереди задач или с помощью web2py планировщика. Здесь мы приведем пример использования самодельной очереди задач.

Во-первых, в файле модели внутри нашего приложения, мы настраиваем модель базы данных для хранения нашей очереди сообщений электронной почты:

1
2
3
4
5
db.define_table('queue',
    Field('status'),
    Field('email'),
    Field('subject'),
    Field('message'))

Из контроллера, мы сможем поставить в очередь сообщения, которые будут отправлены:

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

Далее, нам нужен сценарий фоновой обработки, который считывает очередь и отправляет сообщения электронной почты:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
## В файле /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

В заключении, как описано в главе 4, нам нужно запустить скрипт mail_queue.py, как если бы он был внутри контроллера нашего приложения:

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

где -S app говорит web2py запустить "mail_queue.py" как "app", -M говорит web2py выполнить модели.

Здесь мы предполагаем, что объект mail, на который ссылается "mail_queue.py", определяется в файле модели нашего приложения и поэтому доступен в сценарий "mail_queue.py" благодаря опции -M. Также обратите внимание на важность выполнения фиксации какого-либо изменения, как можно скорее, чтобы не блокировать базу данных для других параллельных процессов.

Как уже отмечалось в главе 4, этот тип фонового процесса не должна выполняться через cron (за исключением, возможно cron @reboot), потому что вы должны быть уверены, что не более одного экземпляра не работает в одно и то же время.

Обратите внимание, единственным недостатком отправки электронной почты с помощью фонового процесса является то, что трудно обеспечить обратную связь с пользователем в случае, если отправка сообщения электронной почты прошла неудачно. Если сообщение электронной почты отправляется непосредственно из действия контроллера, то вы можете поймать любые ошибки и немедленно вернуть сообщение об ошибке пользователю. С помощью фонового процесса, однако, электронная почта передается асинхронно, после того, как действие контроллера уже вернуло свой ответ, так что уведомление пользователя о неисправности усложняется.

Чтение и управление почтовыми ящиками (экспериментальная)

Адаптер IMAP предназначен в качестве интерфейса взаимодействия с IMAP-серверами электронной почты для выполнения простых запросов в синтаксисе запроса web2py DAL, так что чтением сообщений электронной почты, поиском и другими услугами, связанными с IMAP почтой (как это реализовано в Google и Yahoo) можно управлять из web2py приложений.

Он создает свои имена таблиц и полей "статически", а это означает, что разработчик должен оставить определение таблиц и полей на совести экземпляра DAL путем вызова метода адаптера .define_tables(). Таблицы определяются со списком почтовых ящиков IMAP сервера.

Подключение

Для одной учетной записи электронной почты, это код рекомендуется для запуска поддержки IMAP в модели приложения

1
2
3
4
# Замените пользователя, пароль, сервер и порт в строке соединения
# Установите порт 993 для поддержки SSL
imapdb = DAL("imap://user:password@server:port", pool_size=1)
imapdb.define_tables()

Обратите внимание, что <imapdb>.define_tables() возвращает словарь строк, которые сопоставляют DAL tablenames с именам почтовых ящиков сервера, со структурой {<tablename>: <server mailbox name>, ...}, так что вы можете получить фактическое имя почтового ящика на сервере IMAP.

Если вы хотите установить вашу собственную конфигурацию tablename/mailbox и пропустить автоматическую настройку имени, то вы можете передать пользовательский словарь адаптеру следующим образом:

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

Для обработки различных имен родных почтовых ящиков для пользовательского интерфейса, следующие атрибуты дают доступ к автоматически сопоставленным именам почтового ящика адаптера (что родной почтовый ящик, что имя таблицы и наоборот):

АтрибутТипФормат
imapdb.mailboxesdict{<tablename>: <Родное имя сервера>, ...}
imapdb.<table>.mailboxstring"Родное имя сервера"

Первый может быть полезным для получения IMAP запроса, который задает почтовый ящик родной почтовой службы.

1
2
3
# mailbox строка, содержащая фактическое имя почтового ящика
tablenames = dict([(v,k) for k,v in imapdb.mailboxes.items()])
myset = imapdb(imapdb[tablenames[mailbox]])

Получение почты и обновление флагов

Вот список команд IMAP, используемых в контроллере. В примерах, предполагается, что ваш сервис IMAP имеет почтовый ящик с именем INBOX, как в случае с аккаунтами Gmail.

Для подсчета непросмотренных за сегодня сообщений меньше чем 6000 байт из папки входящих сообщений почтового ящика сделайте следующее

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()

Вы можете получить предыдущие запрошенные сообщения:

1
rows = imapdb(q).select()

Реализуются обычные операторы запросов, в число которых относится:

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

Примечание: Настоятельно рекомендуется держать результаты запроса ниже заданного порогового значения размера данных, чтобы избежать заклинивания сервера при выполнении команд на большую выборку.

Для осуществления более быстрых запросов сообщений электронной почты, рекомендуется передать отфильтрованный набор полей:

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

Адаптер знает, когда с полезной нагрузкой частично извлекать сообщения (поля вроде content, size и attachments требуют получения полных данных сообщения)

Существует возможность отфильтровать запрос, выбрав результаты с помощью ограничения (limitby) и последовательностей (sequences) полей почтового ящика.

1
2
# Замените аргументы на фактические значения
myset.select(<fields sequence>, limitby=(<int>, <int>))

Допустим, вы хотите иметь действие приложения, которое показывает сообщения почтового ящика. Сначала мы извлекаем сообщение (Если ваша служба IMAP поддерживает, то сообщение можно получить через полеuid, чтобы избежать использования старых последовательностей ссылок).

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

В противном случае, вы можете использовать id данного сообщения.

1
mymessage = imapdb.INBOX[<id>]

Обратите внимание, что использование идентификатора сообщения в качестве ссылки не рекомендуется, поскольку порядковые номера могут меняться при таких операциях по обслуживанию почтового ящика как удаление сообщения. Если все же хотите записывать ссылки на сообщения (т.е. записывать в поле другой базы данных), решение использовать поле uid в качестве ссылки всякий раз поддерживается, и позволяет получить каждое сообщение с записанным значением.

В заключение, добавьте что-то вроде нижеследующего, чтобы показать содержание сообщения в представлении

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}}

Как и следовало ожидать, мы можем воспользоваться помощником SQLTABLE для создания списков сообщений в представлениях

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

И, конечно же, можно скормить помощника формы с соответствующим значением id последовательности

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

Данный адаптер поддерживает следующие доступные поля:

ПолеТипОписание
uidstring
answeredbooleanФлаг
createddate
contentlist:stringСписок текстовых или HTML частей
tostring
ccstring
bccstring
sizeintegerколичество октетов сообщения*
deletedbooleanФлаг
draftbooleanФлаг
flaggedbooleanФлаг
senderstring
recentbooleanФлаг
seenbooleanФлаг
subjectstring
mimestringОбъявление mime заголовка
emailstringПолное сообщение RFC822**
attachmentslistКаждая не декодируемая в текст часть как словарь
encodingstringОсновная обнаруженная кодировка данного сообщения

*На стороне приложения измеряется как длина строки RFC822 сообщения

ПРЕДУПРЕЖДЕНИЕ: Так как строки идентификаторов сопоставляются с порядковыми номерами электронной почты, то убедитесь, что ваш клиент IMAP web2py приложения не удаляет сообщения во время действий выбора или обновления, чтобы предотвратить обновление или удаление различных сообщений.

Стандартные операции с базами данных CRUD не поддерживаются. Здесь нет никакого способа определения пользовательских полей или таблиц и сделать вставки с различными типами данных, поскольку обновление почтовых ящиков с IMAP сервисами, как правило, сводится к размещение обновлений флага на сервере. Тем не менее, можно получить доступ к этим командам флага через интерфейс DAL IMAP.

Чтобы отметить последние запрошенные сообщения, смотрите

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

Здесь мы удаляем сообщения в базе данных IMAP, которые имеют письма от mr. Gumby

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

Возможно также пометить сообщения для удаления вместо того, чтобы стереть их непосредственно

1
myset.update(deleted=True)
IMAP
 top