Sergei Basharov
Sergei Basharov

Reputation: 53950

Run Flask-Mail asynchronously

I am sending emails from my Flask app with Flask-Mail extension. It runs send() method synchronously and I have to wait until it sends the message. How can I make it run in background?

Upvotes: 3

Views: 5091

Answers (3)

daveruinseverything
daveruinseverything

Reputation: 5187

Use Flask-Executor. Full disclosure, I wrote it myself to solve this exact problem.

Why?

  1. Use concurrent.futures to set up a thread pool instead of manually managed threads prevents the creating of arbitrarily large numbers of threads. Instead, a predefined pool of threads is used to run jobs fed from a queue.
  2. Flask-Executor provides submitted tasks with both the current request context and the current app context, so you don't need to write any handling code to manage this

Here's what it looks like:

from flask import Flask, current_app
from flask_executor import Executor
from flask_mail import Mail, Message

app = Flask(__name__)
# Set email server/auth configuration in app.config[]

executor = Executor(app)
mail = Mail(app)


def send_email(to, subject, message_text, message_html):
    msg = Message(subject, sender=current_app.config['MAIL_USERNAME'], recipients=[to])
    msg.body = message_text
    msg.html = message_html
    mail.send(msg)


@app.route('/signup')
def signup():
    # My signup form logic
    future = executor.submit(send_email, '[email protected]', 'My subject', 'My text message', '<b>My HTML message</b>')
    print(future.result())
    return 'ok'

if __name__ == '__main__':
    app.run()

Basically, you write your send_email function as though you were running regular inline logic, and submit it to the executor. However many emails you send, only the max number of threads defined in the executor (5* number of CPU cores by default) will run, and any overflow in requests to send_email will be queued.

Overall your code stays cleaner and you don't need to write a bunch of wrapper code for every async function you want to run.

Upvotes: 4

Dimitrios Filippou
Dimitrios Filippou

Reputation: 465

I would like to simplify Marboni's code so take a look here.

import threading

from flask import copy_current_request_context
from flask_mail import Message
from app import app, mail


def create_message(recipient, subject, body):

    if not recipient:
        raise ValueError('Target email not defined.')

    subject = subject.encode('utf-8')
    body = body.encode('utf-8')

    return Message(subject, [recipient], body, sender=app.config['MAIL_USERNAME'] or "[email protected]")


def send_async(recipient, subject, body):

    message = create_message(recipient, subject, body)

    @copy_current_request_context
    def send_message(message):
        mail.send(message)

    sender = threading.Thread(name='mail_sender', target=send_message, args=(message,))
    sender.start()

Upvotes: 1

Marboni
Marboni

Reputation: 2459

It's not so complex - you need to send mail in another thread, so you will not block the main thread. But there is one trick.

Here is my code that renders template, creating mail body, and allows to send it both synchronously and asynchronously:

mail_sender.py

import threading
from flask import render_template, copy_current_request_context, current_app
from flask_mail import Mail, Message

mail = Mail()

def create_massege(to_email, subject, template, from_email=None, **kwargs):
    if not from_email:
        from_email = current_app.config['ROBOT_EMAIL']
    if not to_email:
        raise ValueError('Target email not defined.')
    body = render_template(template, site_name=current_app.config['SITE_NAME'], **kwargs)
    subject = subject.encode('utf-8')
    body = body.encode('utf-8')
    return Message(subject, [to_email], body, sender=from_email)

def send(to_email, subject, template, from_email=None, **kwargs):
    message = create_massege(to_email, subject, template, from_email, **kwargs)
    mail.send(message)

def send_async(to_email, subject, template, from_email=None, **kwargs):
    message = create_massege(to_email, subject, template, from_email, **kwargs)

    @copy_current_request_context
    def send_message(message):
        mail.send(message)

    sender = threading.Thread(name='mail_sender', target=send_message, args=(message,))
    sender.start()

Pay your attention to @copy_current_request_context decorator. It's required because Flask-Mail inside uses request context. If we will run it in the new thread, context will be missed. We can prevent this decorating function with @copy_current_request_context - Flask will push context when function will be called.

To use this code you also need to initialize mail object with your Flask application:

run.py

app = Flask('app')
mail_sender.mail.init_app(app)

Upvotes: 9

Related Questions