Search code examples
pythonflaskflask-mailitsdangerous

Mail Not Sent In Gmail Account Using Flask-Mail


I am trying to add a functionality of resetting user password by sending email to a Gmail account. I have been following CoreySchafer YouTube Tutorial and Miguel Grinberg Tutorial to achieve this.

The overall idea is user will be prompted with a Password Reset Request Form where they will enter the email address for which they want to reset the password. After clicking the "Request Reset Password", a message will be displayed that an email has been sent to their Gmail account. By clicking the link from the email, the user will be able to reset their password.

The codes with relevant file names are as follows:

File: routes.py

# Reset Password Request Route
@app.route("/reset_password", methods = ["GET", "POST"])
def reset_password_request():

    if current_user.is_authenticated:
        return redirect(url_for("dashboard"))

    form = ResetPasswordRequestForm()

    if form.validate_on_submit():
        user = User.query.filter_by(email = form.email.data).first()
        if user:
            send_password_reset_email(user)
            flash("Please check your email for the instructions to reset your password")
            return redirect(url_for("login"))

    return render_template("reset_password_request.html", title = "Request For Reset Password", form = form)

# Password Reset Route
@app.route("/reset_password/<token>", methods = ["GET", "POST"])
def reset_password(token):
    if current_user.is_authenticated:
        return redirect(url_for("dashboard"))

    user = User.verify_reset_password_token(token)

    if not user:
        flash("Invalid Token")
        return redirect(url_for("reset_password_request"))

    form = ResetPasswordForm()

    if form.validate_on_submit():
        user.set_password(form.password.data)
        db.session.commit()
        flash("Congratulations! Your password has been reset successfully.")
        return redirect(url_for("login"))

    return render_template("reset_password.html", title = "Reset Password", form = form) 

File: forms.py

# Reset Password Request Form
class ResetPasswordRequestForm(FlaskForm):
    email = StringField("Email", validators = [DataRequired(message = "Email Address is required."), Email()], render_kw = {"placeholder": "Email Address"})
    submit = SubmitField("Request Password Reset")

    def validate_email(self, email):
        user = User.query.filter_by(email = email.data).first()
        if user is None:
            raise ValidationError("There is no account with that email. You must register first.")

# Password Reset Form
class ResetPasswordForm(FlaskForm):
    password = PasswordField("Password", validators = [DataRequired(message = "Password is required.")], render_kw = {"placeholder": "Password"})
    confirm_password = PasswordField("Repeat Password", validators = [DataRequired(message = "Password Confirmation is required."), EqualTo("password")], render_kw = {"placeholder": "Confirm Password"})
    submit = SubmitField("Reset Password")

File: email.py

from flask import render_template
from flask_mail import Message
from app import app, mail
from threading import Thread

# Sending Emails Asynchronously
def send_async_email(app, msg):
    with app.app_context():
        mail.send(msg)

# Email Sending Wrapper Function
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
    Thread(target = send_async_email, args = (app, msg)).start()

# Send Password Reset Email Function
def send_password_reset_email(user):
    token = user.get_reset_password_token()

    send_email("【Task Manager】Reset Your Password", 
    sender = app.config["ADMINS"][0], 
    recipients = [user.email], 
    text_body = render_template("email/reset_password.txt", user = user, token = token), 
    html_body = render_template("email/reset_password.html", user = user, token = token))

File: models.py

from itsdangerous import TimedJSONWebSignatureSerializer as Serializer

# Reset Password Support in User Model
def get_reset_password_token(self, expires_sec = 1800):
    s = Serializer(app.config["SECRET_KEY"], expires_sec)
    return s.dumps({"user_id": self.id}).decode("utf-8")

# Verifying Reset Password Token in User Model
@staticmethod
def verify_reset_password_token(token):
    s = Serializer(app.config["SECRET_KEY"])
    try:
        user_id = s.loads(token)["user_id"]
    except:
        return None
    return User.query.get(user_id)

File: reset_password_request.html

{% extends "layout.html" %}

{% block content %}
    <h1>Task Manager</h1>
    <form action="" method="POST">
        {{ form.hidden_tag() }}
        <div>
            {{ form.email(size=64) }}
        </div>
        {% for error in form.email.errors %}
            <span style="color: red;">{{ error }}</span>
        {% endfor %}
        <div>
            {{ form.submit() }}
        </div>
    </form>
{% endblock %}

File: reset_password.html

{% extends "layout.html" %}

{% block content %}
    <h1>Task Manager</h1>
    <form action="" method="POST" novalidate>
        {{ form.hidden_tag() }}
        <div>
            {{ form.password(size=32) }}
        </div>
        {% for error in form.password.errors %}
            <span style="color: red;">{{ error }}</span>
        {% endfor %}
        <div>
            {{ form.confirm_password(size=32) }}
        </div>
        {% for error in form.confirm_password.errors %}
            <span style="color: red;">{{ error }}</span>
        {% endfor %}
        <div>
            {{ form.submit() }}
        </div>
    </form>
{% endblock %}

I have saved the environment variables in .env file in the root directory.

SECRET_KEY="simple-is-better-than-complex"
MAIL_SERVER="smtp.googlemail.com"
MAIL_PORT=587
MAIL_USE_TLS=True
MAIL_USERNAME="jeet.java.13"
MAIL_PASSWORD="pass123"

Also created the config.py file in the project root directory.

from dotenv import load_dotenv

load_dotenv(override=True)

import os

basedir = os.path.abspath(os.path.dirname(__file__))

# Application Configurations
class Config(object):

    # Function: Getting Environment Variables
    def get_env_var(name):
        try:
            return os.environ[name]
        except KeyError:
            message = "Expected Environment Variable '{}' Not Set!".format(name)
            raise Exception(message)

    # SECRET_KEY Configuration
    SECRET_KEY = os.getenv("SECRET_KEY")
    SQLALCHEMY_DATABASE_URI = "sqlite:///" + os.path.join(basedir, "tms.db")

    SQLALCHEMY_TRACK_MODIFICATIONS = False

    # EMAIL CONFIGURATION

    MAIL_SERVER = os.getenv("MAIL_SERVER")
    MAIL_PORT = int(os.getenv("MAIL_PORT")) or 25
    MAIL_USE_TLS = os.getenv("MAIL_USE_TLS")
    MAIL_USERNAME = os.getenv("MAIL_USERNAME")
    MAIL_PASSWORD = os.getenv("MAIL_PASSWORD")
    ADMINS = ["[email protected]"]

Terminal Result:

"POST /reset_password HTTP/1.1" 302 -

I have also turned ON the "Less Secure Apps" for my Gmail Account, but the email still can't be sent. There is no error in the terminal during the execution of the Flask Application.

Looking forward for your kindest support.


Solution

  • Problem solved. While defining the environment variables, MAIL_USERNAME need to be the full email(Gmail) address; not only the Gmail ID.