pythonpython-3.xemailflaskflask-mail

Flask-Mail [SSL: WRONG_VERSION_NUMBER] wrong version number (_ssl.c:1123)


I'm developing a Flask application that implements a user registration system. The application uses Flask-Mail and itsdangerous to confirm a user's registration and reset their password via email. I configured Flask-Mail to use the recommended server settings provided by the email host that I'm using.

MAIL_PORT = 587
MAIL_USE_SSL = False
MAIL_USE_TLS = True

At first, things were working fine; I could submit emails without any issues. However, seemingly without changing any configuration settings, I now receive the following error when attempting to submit an email using Flask-Mail:

[SSL: WRONG_VERSION_NUMBER] wrong version number (_ssl.c:1123)

I'm not sure where the problem is, and I am wondering if something has changed on the email provider's end? I have tried setting MAIL_PORT = 25 with MAIL_USE_SSL=False and MAIL_USE_TLS=False; and, MAIL_PORT = 465 with MAIL_USE_SSL=True and MAIL_USE_TLS=False as well. Using the former, I receive the same error as with port 587, but using the latter I'm receiving STARTTLS extension not supported by server.

I'm running the Flask app in development mode at localhost:5000. Here's some of my configuration settings and code:

config.py

    SECRET_KEY = 'verysecret'
    MAIL_SERVER = "smtp.mymailservice.com"
    MAIL_PORT = 587
    MAIL_USE_SSL = False
    MAIL_USE_TLS = True
    MAIL_USERNAME = "myemail@myhostname.com"
    MAIL_PASSWORD = "mypassword"
    MAIL_DEFAULT_SENDER = 'Brand <noreply@myhostname.com>'

app/mailing.py

from flask_mail import Message
from flask import current_app
from .extensions import mail


def send_email(to, subject, template):
    msg = Message(
        subject,
        recipients=[to],
        html=template,
        sender=current_app.config["MAIL_DEFAULT_SENDER"]
    )
    mail.send(msg)

app/users/routes.py

(One of the routes where I receive the error)

from flask import (
    render_template, session, request, redirect, url_for, g, jsonify, flash
)

import uuid
from passlib.hash import sha256_crypt

from app.mailing import send_email
from app.extensions import db
from app.users import bp
from app.users.forms import *
from app.users.models import *
from app.users.token import *

@bp.route('/register', methods=['POST', 'GET'])
def register():

    # Initialize the Register Form
    form = RegisterForm()

    # If the submitted form is valid
    if form.validate_on_submit():

        # Check to see if a user already exists with this email address
        user = User.query.filter_by(email=form.email.data).first()

        # If there is not a user with this email address, create a new user
        if not user:
            new_user = User(public_id=str(uuid.uuid4()),
                            email=form.email.data,
                            password=sha256_crypt.encrypt(
                                (form.password.data)),
                            first_name=form.firstname.data,
                            last_name=form.lastname.data

                            )

            db.session.add(new_user)
            db.session.commit()

            token = generate_confirmation_token(new_user.email)
            confirm_url = url_for("users.confirm_email",
                                  token=token, _external=True)
            html = render_template('confirm_email.html',
                                   confirm_url=confirm_url)
            subject = "Please confirm your email"

            try:
                send_email(new_user.email, subject, html)
                flash("A confirmation email has been sent to you. Please verify your email address to activate your account.", category="success")
            except Exception as e:
                flash(
                    "There was a problem sending the confirmation email. Please try again later.", category="danger")
                print(e)

            session["user_id"] = new_user.public_id
            session["email"] = new_user.email
            session["name"] = new_user.first_name

            flash("Thanks for registering!", category="success")

            return redirect(url_for('users.unconfirmed'))
        else:
            flash("There is already an account associated with this email address. Log in, or use a different email address.")

    return render_template("register_user.html", form=form)

app/extensions.py

from flask_mail import Mail
from flask_bootstrap import Bootstrap
from flask_sqlalchemy import SQLAlchemy

db = SQLAlchemy()
bootstrap = Bootstrap()
mail = Mail()

app/init.py

from flask import Flask
from config import Config, DevelopmentConfig

from .errors import (
    page_not_found, forbidden, internal_server_error
)

from .extensions import (
    db, mail, bootstrap
)



def create_app(config_class=DevelopmentConfig):

    app = MyFlask(__name__)

    # Set Configuration
    app.config.from_object(config_class)
    
    # Register extensions
    # Initialize Boostrap-Flask
    bootstrap.init_app(app)

    # Initialize Flask-SQLAlchemy
    db.init_app(app)

    # Initialize Flask-Mail
    mail.init_app(app)

    # Register error views
    app.register_error_handler(404, page_not_found)
    app.register_error_handler(403, forbidden)
    app.register_error_handler(500, internal_server_error)

    with app.app_context():

        # register blueprints
        from app.main import bp as bp_main
        app.register_blueprint(bp_main)

        from app.users import bp as bp_users
        app.register_blueprint(bp_users)

        return app

Solution

  • I figured out what was going on. Evidently, you can pass in non-boolean types for MAIL_USE_TLS and MAIL_USE_SSL when initializing the Mail object from Flask-Mail. This becomes a problem when the Connection object calls configure_host() and conditionally checks if self.mail.use_ssl.

    Thus, as long as self.mail.use_ssl is not None, the method will set host = smtplib.SMTP_SSL(self.mail.server, self.mail.port), which in my case, lead to [SSL: WRONG_VERSION_NUMBER] wrong version number (_ssl.c:1123) because mail.port was set to 587.

    tl;dr Ensure the configuration variables for your Flask app are set to the appropriate type, especially if you are using environment variables, as those will always be of type str when accessing them via the os.environ dict.

    flask_mail.py

    class Connection(object):
        """Handles connection to host."""
    
        def __init__(self, mail):
            self.mail = mail
    
        def __enter__(self):
            if self.mail.suppress:
                self.host = None
            else:
                self.host = self.configure_host()
    
            self.num_emails = 0
    
            return self
    
        def __exit__(self, exc_type, exc_value, tb):
            if self.host:
                self.host.quit()
        
        def configure_host(self):
        ## PROBLEM OCCURRED HERE BECAUSE type(self.mail.use_ssl) = <class 'str'> ##
            if self.mail.use_ssl:
                host = smtplib.SMTP_SSL(self.mail.server, self.mail.port)
            else:
                host = smtplib.SMTP(self.mail.server, self.mail.port)
    
            host.set_debuglevel(int(self.mail.debug))
    
            if self.mail.use_tls:
                host.starttls()
            if self.mail.username and self.mail.password:
                host.login(self.mail.username, self.mail.password)
    
            return host