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

2770

Веб-приложения отправляют электронные письма постоянно, и в этом уроке речь пойдет о том, как добавить инструмент для отправки 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, заполнить форму и отправить ее. В этот раз задержки не будет.

Тест на знание python

Какой будет результат выполнения этого кода?
Что выведет этот код?
Что делает код ниже?
Что выведет этот код?
Как узнать длину списка?
Александр
Я создал этот блог в 2018 году, чтобы распространять полезные учебные материалы, документации и уроки на русском. На сайте опубликовано множество статей по основам python и библиотекам, уроков для начинающих и примеров написания программ. Пишу на популярные темы: веб-разработка, работа с базами данных, data sciense и другие...