I am using the Flask-Mail library for my Flask application to send a default welcome email to the user when they sign up to be added to the newsletter. After debugging the library I found that it can only handle one connection at a time to send a message and will then automatically close the connection. If the backend sends an email to another user while a connection is still open then it throws this exception: raise SMTPServerDisconnected("Connection unexpectedly closed: " smtplib.SMTPServerDisconnected: Connection unexpectedly closed: [WinError 10054] An existing connection was forcibly closed by the remote host
. I want to be able to queue the mail Mail library to send a new message to another recipient after the connection has closed but currently it keeps throwing the error I mentioned above when I attempt to queue the function to send a message.
worker.py:
import os
import redis
from rq import Worker, Queue, Connection
listen = ['high', 'default', 'low']
redis_url = os.environ.get('REDISTOGO_URL')
conn = redis.from_url(redis_url)
if __name__ == '__main__':
with Connection(conn):
worker = Worker(map(Queue, listen))
worker.work()
user.routes.py
from flask import request, Blueprint, redirect, render_template
from flask_app import mail, db
from flask_app.users.forms import NewsLetterRegistrationForm
from flask_app.models import User
from flask_mail import Message
from rq import Queue
from worker import conn
import os, time
users = Blueprint("users", __name__)
queue = Queue(connection=conn)
@users.route("/newsletter-subscribe", methods=["GET", "POST"])
def newsletter_subscribe():
form = NewsLetterRegistrationForm()
if form.validate_on_submit():
user = User(name=form.name.data, email=form.email.data)
db.session.add(user)
db.session.commit()
queue.enqueue(send_welcome_email(user))
return "Success"
return "Failure"
def send_welcome_email(user):
with mail.connect() as con:
html = render_template("welcome-email.html", name=user.name)
subject = "Welcome!"
msg = Message(
subject=subject,
recipients=[user.email],
html=html
)
con.send(msg)
main.routes.py
from flask import render_template, session, request, current_app, Blueprint, redirect, url_for, json, make_response
from flask_app.users.forms import NewsLetterRegistrationForm
import os
main = Blueprint("main", __name__)
@main.route("/", methods=["GET"])
def index():
return render_template("index.html", title="Home")
@main.route("/example", methods=["GET"])
def example():
return render_template("example.html", title="example")
@main.context_processor
def inject_template_scope():
injections = dict()
form = NewsLetterRegistrationForm()
injections.update(form=form)
return injections
_init_.py
from logging.config import dictConfig
from flask import Flask, url_for, current_app
from flask_bcrypt import Bcrypt
from flask_sqlalchemy import SQLAlchemy
from flask_talisman import Talisman
from flask_compress import Compress
from flask_mail import Mail
import os
config = {
"SECRET_KEY": os.environ.get("SECRET_KEY"),
"DEBUG": True,
"SQLALCHEMY_DATABASE_URI": os.environ.get("DATABASE_URL"),
"SQLALCHEMY_TRACK_MODIFICATIONS": False,
"SQLALCHEMY_ECHO": False,
"MAIL_SERVER": "mail.privateemail.com",
"MAIL_PORT": 587,
"MAIL_USE_SSL": False,
"MAIL_USE_TLS": True,
"MAIL_USERNAME": "[email protected]",
"MAIL_PASSWORD": os.environ.get("NEWS_MAIL_PASSWORD"),
"MAIL_DEFAULT_SENDER": "[email protected]"
}
talisman = Talisman()
db = SQLAlchemy()
bcrypt = Bcrypt()
compress = Compress()
mail = Mail()
app = Flask(__name__)
def create_app():
app.config.from_mapping(config)
talisman.init_app(app)
db.init_app(app)
bcrypt.init_app(app)
compress.init_app(app)
mail.init_app(app)
from flask_app.users.routes import users
app.register_blueprint(users)
with app.app_context():
db.create_all()
return app
run.py
from flask_app import create_app
Error Log:
Traceback (most recent call last):
File "C:\Users\user\AppData\Local\Programs\Python\Python38-32\Lib\smtplib.py", line 391, in getreply
line = self.file.readline(_MAXLINE + 1)
File "C:\Users\user\AppData\Local\Programs\Python\Python38-32\Lib\socket.py", line 669, in readinto
return self._sock.recv_into(b)
ConnectionResetError: [WinError 10054] An existing connection was forcibly closed by the remote host
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
File "C:\Users\user\AppData\Local\Programs\Python\Python38-32\Lib\site-packages\flask\app.py", line 2464, in __call__
return self.wsgi_app(environ, start_response)
File "C:\Users\user\AppData\Local\Programs\Python\Python38-32\Lib\site-packages\flask\app.py", line 2450, in wsgi_app
response = self.handle_exception(e)
File "C:\Users\user\AppData\Local\Programs\Python\Python38-32\Lib\site-packages\flask\app.py", line 1867, in handle_exception
reraise(exc_type, exc_value, tb)
File "C:\Users\user\AppData\Local\Programs\Python\Python38-32\Lib\site-packages\flask\_compat.py", line 39, in reraise
raise value
File "C:\Users\user\AppData\Local\Programs\Python\Python38-32\Lib\site-packages\flask\app.py", line 2447, in wsgi_app
response = self.full_dispatch_request()
File "C:\Users\user\AppData\Local\Programs\Python\Python38-32\Lib\site-packages\flask\app.py", line 1952, in full_dispatch_request
rv = self.handle_user_exception(e)
File "C:\Users\user\AppData\Local\Programs\Python\Python38-32\Lib\site-packages\flask\app.py", line 1821, in handle_user_exception
reraise(exc_type, exc_value, tb)
File "C:\Users\user\AppData\Local\Programs\Python\Python38-32\Lib\site-packages\flask\_compat.py", line 39, in reraise
raise value
File "C:\Users\user\AppData\Local\Programs\Python\Python38-32\Lib\site-packages\flask\app.py", line 1950, in full_dispatch_request
rv = self.dispatch_request()
File "C:\Users\user\AppData\Local\Programs\Python\Python38-32\Lib\site-packages\flask\app.py", line 1936, in dispatch_request
return self.view_functions[rule.endpoint](**req.view_args)
File "C:\User\Work Stuff\example.com\flask_app\users\routes.py", line 18, in newsletter_subscribe
send_welcome_email(user, request.host_url)
File "C:\User\Work Stuff\example.com\flask_app\users\routes.py", line 42, in send_welcome_email
with mail.connect() as con:
File "C:\Users\user\AppData\Local\Programs\Python\Python38-32\Lib\site-packages\flask_mail.py",
line 144, in __enter__
self.host = self.configure_host()
File "C:\Users\user\AppData\Local\Programs\Python\Python38-32\Lib\site-packages\flask_mail.py",
line 158, in configure_host
host = smtplib.SMTP(self.mail.server, self.mail.port)
File "C:\Users\user\AppData\Local\Programs\Python\Python38-32\Lib\smtplib.py", line 253, in __init__
(code, msg) = self.connect(host, port)
File "C:\Users\user\AppData\Local\Programs\Python\Python38-32\Lib\smtplib.py", line 341, in connect
(code, msg) = self.getreply()
File "C:\Users\user\AppData\Local\Programs\Python\Python38-32\Lib\smtplib.py", line 394, in getreply
raise SMTPServerDisconnected("Connection unexpectedly closed: "
smtplib.SMTPServerDisconnected: Connection unexpectedly closed: [WinError 10054] An existing connection was forcibly closed by the remote host
Step 1 − Import Mail and Message class from flask-mail module in the code. Step 2 − Then Flask-Mail is configured as per following settings. Step 3 − Create an instance of Mail class. Step 4 − Set up a Message object in a Python function mapped by URL rule ('/').
The flask-mail module contains definitions of the following important classes. Mail class methods include: send() , connect() and send_message() .
The Flask-Mail extension provides a simple interface to set up SMTP with your Flask application and to send messages from your views and scripts.
It seems to me that your mail server is closing the connection because you are making more requests than its configuration allows. If you are using a third party mail provider you might want to check if the service you use offers any way to send bulk emails, e.g. through an API or file upload. Or if they have a way to change that configuration for you.
If that is not possible:
One solution would be to make a blocking call (time.sleep()
) in order for you to lower the frequency at which you send your mails:
import time
def send_welcome_email(user):
with mail.connect() as con:
html = render_template("welcome-email.html", name=user.name)
subject = "Welcome!"
msg = Message(
subject=subject,
recipients=[user.email],
html=html
)
time.sleep(2) # 2 seconds, modify as you see fit
con.send(msg)
Another solution would be to wrap your code in a try catch block and on exceptions wait for a while before trying to send the email again.
def send_welcome_email(user, is_retry=False):
try:
with mail.connect() as con:
html = render_template("welcome-email.html", name=user.name)
subject = "Welcome!"
msg = Message(
subject=subject,
recipients=[user.email],
html=html
)
con.send(msg)
except SMTPServerDisconnected:
time.sleep(60) # Wait for a while, 1 minute tends to be a good measure as most configurations specify how many requests can be made a minute.
if not is_retry: # Only retry once -> you could modify this to make the use of a counter.
send_welcome_email(user, is_retry=True) # Try again
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With