#17 Отправка email во Flask

Веб-приложения отправляют электронные письма постоянно, и в этом уроке речь пойдет о том, как добавить инструмент для отправки email в приложение Flask.

В стандартной библиотеке Python есть модуль smtplib, который можно использовать для отправки сообщений. Хотя сам модуль smtplib не является сложным, он все равно требует кое-какой работы. Для облегчения процесса работы с ним было создано расширение Flask-Mail. Flask-Mail построен на основе модуля Python smtplib и предоставляет простой интерфейс для отправки электронных писем. Он также предоставляет возможности по массовой рассылке и прикрепленным к сообщениям файлам. Установить Flask-Mail можно с помощью следующей команды:

(env) gvido@vm:~/flask_app$ pip install flask-mail

Чтобы запустить расширение, нужно импортировать класс Mail из пакета flask_mail и создать экземпляр класса Mail:

#...
from flask_mail import Mail, Message

app = Flask(__name__)
app.debug = True
app.config['SECRET_KEY'] = 'a really really really really long secret key'
app.config['SQLALCHEMY_DATABASE_URI'] = 'mysql+pymysql://root:pass@localhost/flask_app_db'

manager = Manager(app)
manager.add_command('db', MigrateCommand)
db = SQLAlchemy(app)
migrate = Migrate(app,  db)
mail = Mail(app)
#...

Дальше нужно указать некоторые параметры настройки, чтобы Flask-Mail знал, к какому SMTP-серверу подключаться. Для этого в файл main2.py нужно добавить следующий код:

#...
app.config['SECRET_KEY'] = 'a really really really really long secret key'
app.config['SQLALCHEMY_DATABASE_URI'] = 'mysql+pymysql://root:pass@localhost/flask_app_db'
app.config['MAIL_SERVER'] = 'smtp.googlemail.com'
app.config['MAIL_PORT'] = 587
app.config['MAIL_USE_TLS'] = True
app.config['MAIL_USERNAME'] = 'test@gmail.com'  # введите свой адрес электронной почты здесь
app.config['MAIL_DEFAULT_SENDER'] = 'test@gmail.com'  # и здесь
app.config['MAIL_PASSWORD'] = 'password'  # введите пароль

manager = Manager(app)
manager.add_command('db', MigrateCommand)
db = SQLAlchemy(app)
mail = Mail(app)
#...

В данном случае используется SMTP-сервер Google. Стоит отметить, что Gmail позволяет отправлять только 100-150 сообщений в день. Если этого недостаточно, то стоит обратить внимание на альтернативы: SendGrid или MailChimp.

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

Основы Flask-Mail

Для составления электронного письма, нужно создать экземпляр класса Message:

msg = Message("Subject", sender="sender@example.com",  recipients=['recipient_1@example.com'])

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

msg = Message("Subject", recipients=['recipient@example.com'])

Для указания тела письма необходимо использовать атрибут body экземпляра Message:

msg.body = "Mail body"

Если оно состоит из HTML, передавать его следует атрибуту html.

msg.html = "<p>Mail body</p>"

Наконец, отправить сообщение можно, передав экземпляр Message метод send() экземпляра Mail:

mail.send(msg)

Пришло время проверить настройки, отправив email с помощью командной строки.

Отправка тестового сообщения

Откроем терминал, чтобы ввести следующие команды:

(env) overiq@vm:~/flask_app$ python main2.py shell
>>>
>>> from  main2 import  mail,  Message
>>> # введите свою почту
>>> msg = Message("Subject", recipients=["you@mail.com"])
>>> msg.html = "<h2>Email Heading</h2>\n<p>Email Body</p>"
>>>
>>> mail.send(msg)
>>>

Если операция прошла успешно, то на почту должно прийти следующее сообщение с темой “Subject”:

Email Heading
Email Body

Стоит заметить, что отправка через SMTP-сервер Gmail не сработает, если не отключить двухфакторную аутентификацию и не разрешить небезопасным приложениям получать доступ к аккаунту.

Интеграция email в приложение

Сейчас когда пользователь отправляет обратную связь, она сохраняется в базу данных, сам пользователь получает уведомление о том, что его сообщение было отправлено, и на этом все. Но в идеале приложение должно уведомлять администраторов о полученной обратной связи. Это можно сделать. Откроем main2.py, чтобы изменить функцию представления contact() так, чтобы она отправляла сообщения:

#...
@app.route('/contact/', methods=['get', 'post'])
def contact():
    #...
	db.session.commit()
	
	msg = Message("Feedback", recipients=[app.config['MAIL_USERNAME']])
	msg.body = "You have received a new feedback from {} <{}>.".format(name, email)
	mail.send(msg)

	print("\nData received. Now redirecting ...")
    #...

Дальше нужно запустить сервер и зайти на https://localhost:5000/contact/. Заполним и отправим форму. Если все прошло успешно, должен прийти email.

Можно было обратить внимание на задержку между отправкой обратной связи и появлением уведомления о том, что она была отправлена успешно. Проблема в том, что метод mail.send() блокирует исполнение функции представления на несколько секунд. В результате, код с перенаправлением страницы не будет исполнен до тех пор, пока не вернется метод mail.send(). Решить это можно с помощью потоков (threads).

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

Откроем main2.py, чтобы добавить следующий код перед index:

#...
from threading import Thread
#...
def shell_context():
    import os, sys
    return dict(app=app, os=os, sys=sys)

manager.add_command("shell",  Shell(make_context=shell_context))

def async_send_mail(app, msg):
    with app.app_context():
	mail.send(msg)


def send_mail(subject, recipient, template, **kwargs):
    msg = Message(subject,      sender=app.config['MAIL_DEFAULT_SENDER'],  recipients=[recipient])
    msg.html = render_template(template,  **kwargs)
    thr = Thread(target=async_send_mail,  args=[app,  msg])
    thr.start()
    return thr

@app.route('/')
def index():
    return render_template('index.html', name='Jerry')
#...

Было сделано несколько изменений. Функция send_mail() теперь включает в себя всю логику отправки email. Она принимает тему письма, получателя и шаблон сообщения. Ей также можно передать дополнительные аргументы в виде аргументов-ключевых слов. Почему именно так? Дополнительные аргументы представляют собой данные, которые нужно передать шаблону. На 17 строке рендерится шаблон, а его результат передается атрибуту msg.html. На строке 18 создается объект Thread. Это делается с помощью передачи названия функции и аргументов функции, с которыми она должна быть вызвана. Следующая строка запускает потоки. Когда поток запускается, вызывается async_send_mail(). Теперь самое интересное. Впервые в коде работа происходит вне приложения (то есть, вне функции представления) в новом потоке. with app.app_context(): создает контекст приложения, а mail.send() отправляет email.

Дальше нужно создать шаблон для сообщения обратной связи. В папке templates необходимо создать папку mail. Она будет хранить шаблоны для электронных писем. Внутри папки необходимо создать шаблон feedback.html со следующим кодом:

<p>You have received a new feedback from {{ name }} &lt;{{ email }}&gt; </p>

Теперь нужно изменить функцию представления contact(), чтобы использовать функцию send_mail():

После этого нужно снова зайти на https://localhost:5000/contact, заполнить форму и отправить ее. В этот раз задержки не будет.