OverIQ.com

Sending Email in Flask

Last updated on July 27, 2020


Web applications send email all the time and in this lesson, we will integrate Email sending capability to our Flask application.

Python standard library has a module called smtplib which can be used to send an email. Although, directly using smtplib module is not that complicated but still requires some work on your part. To make the process easier people have created an extension called Flask-Mail. Flask-Mail is built around Python smtplib module and exposes a simple interface for sending email. It also provides support for bulk emails and attachments. Install Flask-Mail using the following command:

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

To initialize the extension import Mail class from flask_mail package and create an instance of Mail class as follows (changes are highlighted):

flask_app/main2.py

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
#...
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)
#...

Next, we have to set some configuration options to let Flask-Mail know which SMTP server to connect to. In the main2.py file add the following configurations (changes are highlighted):

flask_app/main2.py

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
#...
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'] = 'infooveriq@gmail.com'  # enter your email here
app.config['MAIL_DEFAULT_SENDER'] = 'infooveriq@gmail.com' # enter your email here
app.config['MAIL_PASSWORD'] = 'password' # enter your password here

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

Here we are using Gmail SMTP server. Be aware that Gmail only allows you to send 100-150 email per day. If that's not sufficient, you might want to check out alternatives like SendGrid or MailChimp.

Instead of hardcoding email and password in the application as we have done here. A better approach would be to store email and password in the environment variables. That way if email or password changes, we don't have to update our code. We will see how to do this in later lessons.

Flask-Mail Basics #

To compose an email, we create an instance of Message class as follows:

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

If you have set MAIL_DEFAULT_SENDER configuration then you don't need to pass sender explicitly while creating Message instance.

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

To set the mail body use the body attribute of the Message instance:

msg.body = "Mail body"

If mail body is HTML, set it to html attribute instead.

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

At last, we can send mail by passing the Message instance to the send() method of the Mail instance:

mail.send(msg)

Let's now test our configurations by sending an email through the command line.

Sending Test Mail #

Open terminal and enter the following commands:

1
2
3
4
5
6
7
8
9
(env) overiq@vm:~/flask_app$ python main2.py shell
>>>
>>> from main2 import mail, Message
>>>
>>> msg = Message("Subject", recipients=["infooveriq@gmail.com"])
>>> msg.html = "<h2>Email Heading</h2>\n<p>Email Body</p>"
>>>
>>> mail.send(msg)
>>>

On success you will get an email that looks like this:

Note that sending an email via Gmail SMTP server will not work until you disable 2-factor authentication and allow less secure apps to access your account.

Integrating Email with our application #

As things stand, whenever a user submits the feedback it is saved into the database, the user gets a success message and that's all there is to it, pretty dull, don't you think? Ideally, the application should notify administrators or moderators about the feedback. Let's do this. Open main2.py and modify contact() view function to send email as follows (changes are highlighted):

flask_app/main2.py

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
#...
@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 ...")
    #...

Start the server and visit http://localhost:5000/contact/. Fill the form and hit submit. On success, you will get an email like this:

You might have noticed a long delay between when you hit submit button and success message on the subsequent request. The problem is mail.send() method blocks the execution of view function for a few seconds. As a result, code to redirect the page will not be executed until the mail.send() method returns. We can solve this problem using Threads.

While we are at it, we will also refactor the code to send the email. Right now, if we want to send email anywhere else in our code, we have to copy and paste the exact same lines of code. We can save a few lines in our code by wrapping the logic to send email in a function.

Open main2.py and add the following code just before index route as follows (changes are highlighted):

flask_app/main2.py

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#...
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')
#...

We have made lots of improvements here. The send_mail() function encapsulates the logic of sending mail. It accepts subject, recipient, and the email template. You can also pass any additional arguments to it as keyword arguments. Why additional arguments? Additional arguments represent data you want to pass to the template. In line 17, we are rendering a template and assigning its result to the msg.html attribute. In line 18, we are creating a Thread object, passing the name of the function and the arguments function must be called with. The next line starts the threads. When the thread starts, async_send_mail() is invoked. Now comes the interesting part. For the first time in our code, we are working outside of the application (i.e outside of view functions) in a new thread. The with app.app_context(): creates the application context and finally mail.send() sends the email.

Next, we need to create a template for feedback email. In the templates directory create a directory named mail. This directory will store our templates for email. Inside the directory create a template named feedback.html with the following code:

templates/mail/feedback.html

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

Modify the contact() view function to use send_mail() function as follows (changes are highlighted):

flask_app/main2.py

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
@app.route('/contact/', methods=['get', 'post'])
def contact():
        #...
        db.session.commit()
        
        send_mail("New Feedback", app.config['MAIL_DEFAULT_SENDER'], 'mail/feedback.html',
                  name=name, email=email)

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

After these changes visit http://localhost:5000/contact/ again. Fill the form and hit submit. This time you will not experience any delay.