[Перевод] Мега-Учебник Flask, Часть 11: Поддержка e-mail

Это одиннадцатая статья в серии, где я описываю свой опыт написания веб-приложения на Python с использованием микрофреймворка Flask.Цель данного руководства — разработать довольно функциональное приложение-микроблог, которое я за полным отсутствием оригинальности решил назвать microblog.

Оглавление

Краткое повторение В последних уроках мы занимались, в основном, улучшениями связанными с нашей базой данных.

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

Введение в Flask-Mail К счастью для нас, Flask уже имеет расширение обрабатывающее электронную почту, и хоть оно не выполняет 100% задач, оно очень близко к этому.

Установка Flask-Mail в наше виртуальное окружение довольно простая. Пользователи на отличных от Windows системах должны сделать:

flask/bin/pip install flask-mail Для пользователей Windows всё немножко сложней, потому что одна из зависимостей Flask-Mail не работает в этой OS. На Windows вам нужно сделать следующее: flask\Scripts\pip install --no-deps lamson chardet flask-mail Конфигурация Ранее, когда мы добавляли Unit-тестирование мы добавили конфигурацию для Flask в которой указали email на который должны отсылаться уведомления об ошибках в production-версии нашего приложения. Та же информация используется для отправки почты приложением.

Нужно запомнить что нам нужно следующая информация:

сервер через который отправляются email электронный адрес администратора Это то, что мы сделали в предыдущей статье (файл config.py):

# email server MAIL_SERVER = 'your.mailserver.com' MAIL_PORT = 25 MAIL_USE_TLS = False MAIL_USE_SSL = False MAIL_USERNAME = 'you' MAIL_PASSWORD = 'your-password'

# administrator list ADMINS = ['you@example.com'] Разумеется вам придется ввести фактические данные в этот конфиг, для того, чтобы приложение действительно смогло отправлять вам электронные письма. Например, если вы хотите использовать приложение для отправки писем через gmail.com, нужно указать следующее:

# email server MAIL_SERVER = 'smtp.googlemail.com' MAIL_PORT = 465 MAIL_USE_TLS = False MAIL_USE_SSL = True MAIL_USERNAME = 'your-gmail-username' MAIL_PASSWORD = 'your-gmail-password'

# administrator list ADMINS = ['your-gmail-username@gmail.com'] Мы также должны инициализировать объект Mail, т.к. это будет объект, который будет соединяться с SMTP сервером и отправлять электронные письма для нас (файл app/__init__.py):

from flask.ext.mail import Mail mail = Mail (app) Давайте отправим email!

Чтобы понять как работает Flask-Mail работает, нам нужно отправить email из командной строки. Давайте запустим Python из нашего виртуального окружения и наберем следующее:

>>> from flask.ext.mail import Message >>> from app import app, mail >>> from config import ADMINS >>> msg = Message ('test subject', sender = ADMINS[0], recipients = ADMINS) >>> msg.body = 'text body' >>> msg.html = 'HTML body' >>> with app.app_context (): … mail.send (msg) … Фрагмент кода выше отправит письмо списку администраторов, указанных в config.py. Отправителем будет указан первый администратор из списка. Писмо будет иметь текстовую и HTML версии, что вы увидите зависит от настроек вашего почтового клиента. Обратите внимание, нам нужно создать app_context, чтобы отправить email. Последние релизы Flask-Mail это требуют. Контекст создается автоматически, когда запрос обрабатывается Flask.

Поскольку мы не находимся внутри запроса, мы можем создать контекст руками.

Теперь пришло время интегрировать этот код в наше приложение!

Простой email фреймворк Сейчас мы напишем впомогательную функцию, которая отправляет email. Это просто более общая версия вышеуказанного теста. Мы поместим эту функцию в новый файл, который выделим для наших функций связанных с email (файл app/emails.py):

from flask.ext.mail import Message from app import mail

def send_email (subject, sender, recipients, text_body, html_body): msg = Message (subject, sender = sender, recipients = recipients) msg.body = text_body msg.html = html_body mail.send (msg) Обратите внимание что Flask-mail поддерживает больше чем мы используем. Например списки скрытых копий и вложения доступны, но мы не будем использовать их в нашем приложении.

Уведомления о подписках Теперь, когда у нас есть базовый фреймворк для отправки электронной почты, мы можем написать функцию уведомляющую о подписчиках (файл app/emails.py):

from flask import render_template from config import ADMINS

def follower_notification (followed, follower): send_email (»[microblog] %s is now following you!» % follower.nickname, ADMINS[0], [followed.email], render_template («follower_email.txt», user = followed, follower = follower), render_template («follower_email.html», user = followed, follower = follower)) Увидели что-то неожиданное? Наш старый друг — функция render_template создает вид письма. Если вы помните, мы использовали эту функцию чтобы рендерить все HTML шаблоны из нашего представления. Так же как наши HTML, тело письма идеальный кандидат на использование шаблонов. Мы хотим, насколько это возможно, отделить логику от представления, поэтому письма будут идти в папке с шаблонами вместе с другими view.

Итак, сейчас мы напишем шаблоны для текстовой и HTML версий для нашего уведомления. Это текстовая версия (файл app/templates/follower_email.txt):

Dear {{user.nickname}},

{{follower.nickname}} is now a follower. Click on the following link to visit {{follower.nickname}}'s profile page:

{{url_for ('user', nickname = follower.nickname, _external = True)}}

Regards,

The microblog admin Для HTML версии мы можем сделать всё немножко красивей и показывать аватар подписчика и информацию из профиля (файл app/templates/follower_email.html):

Dear {{user.nickname}},

{{follower.nickname}} is now a follower.

{{follower.nickname}}
{{follower.about_me}}

Regards,

The microblog admin

Обратите внимание на _external = True в поле url_for нашего шаблона. По умолчанию, функция url_for генерирует URL’ы относительно текущей страницы. Для примера, code{url_for («index»)} будет /index, в то время, когда мы ожидаем http://localhost:5000/index. В электронной почте нет доменного контекста, поэтому мы должны указывать полные адреса URL, которые включают домен, в этом нам и поможет _external.

Финальным шагом станет подключение отправки электронного письма с функцией представления, которая обрабатывает «Follow» (файл app/views.py):

from emails import follower_notification

@app.route ('/follow/') @login_required def follow (nickname): user = User.query.filter_by (nickname = nickname).first () # … follower_notification (user, g.user) return redirect (url_for ('user', nickname = nickname)) Сейчас вы должны создать двух пользователей (если вы еще не сделали этого) и сделать одного подписчиком другого, чтобы увидеть как работает уведомление по электронной почте. Это то что нужно? Мы закончили?

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

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

Итак, что произошло?

Проблема в том, что Flask-Mail отправляет электронные письма синхронно. Сервер блокируется, пока писмо отправляется и отправляет ответ браузеру, только когда сообщение будет доставлено. Вы можете представить что случится если мы попробуем отправить электронное письмо медленному серверу, или, что еще хуже, выключенному? Это плохо.

Это очень страшное ограничение, отправка электронного письма должна быть фоновой задачей, которая не мешает серверу, поэтому давайте посмотрим как мы можем всё это исправить.

Асинхронные вызовы в Python Мы хотим чтобы функция send_email завершалась мгновенно, пока работа по отправке письма идет в фоне.

Оказывается Python уже поддерживает запуск асинхронных задач, даже более чем одним способом. Модули threading и multiprocessing могут нам помочь.

Запуск нового потока, каждый раз, когда нам нужно отправить письмо, гораздо менее ресурсоемкая операция чем запуск нового процесса, поэтому давайте переместим вызов mail.send (msg) в поток (файл app/emails.py):

from threading import Thread

def send_async_email (msg): mail.send (msg)

def send_email (subject, sender, recipients, text_body, html_body): msg = Message (subject, sender = sender, recipients = recipients) msg.body = text_body msg.html = html_body thr = Thread (target = send_async_email, args = [msg]) thr.start () Если вы тестируете функцию «Follow» вы обратите внимание что браузер показывает обновленную страницу прежде чем писмо будет отправлено.

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

Мы можем реализовать наше решение в виде декоратора. С декоратором код выше изменится на этот:

from decorators import async

@async def send_async_email (msg): mail.send (msg)

def send_email (subject, sender, recipients, text_body, html_body): msg = Message (subject, sender = sender, recipients = recipients) msg.body = text_body msg.html = html_body send_async_email (msg) Гораздо лучше, не правда ли?

Код который делает эту магию, на самом деле очень простой. Мы запишем его в новый файл (файл app/decorators.py):

from threading import Thread

def async (f): def wrapper (*args, **kwargs): thr = Thread (target = f, args = args, kwargs = kwargs) thr.start () return wrapper Сейчас когда мы случайно создали хорошую основу для асинхронных задач мы можем сказать что все сделано!

Ради упражнения давайте рассмотрим как изменилось бы наше решение, если бы мы использовали процессы вместо потоков.Мы не хотим чтобы новый процесс стартовал каждый раз, когда мы отправляем письмо, вместо этого мы можем использовать класс Pool из модуля multiprocessing. Этот класс создает необходимое количество процессов (которые являются форками основного процесса) и все процессы ждут задачи, которые передаются через метод apply_async. Это может быть полезным и интересным для загруженых сайтов, но сейчас мы остановимся на потоках.

Заключительные слова Исходный код обновленного приложения доступен ниже:

Скачать microblog-0.11.zip.

Я получил несколько просьб разместить это приложение на GitHub или похожем сайте, я думаю это очень хорошая идея. Я буду работать над этим в ближайшем будущем. Оставайтесь на связи.

Спасибо за то что следите за серией моих туториалов. Надеюсь увидеть вас в следующих частях.

Miguel

© Habrahabr.ru