6

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": "test@example.com",
    "MAIL_PASSWORD": os.environ.get("NEWS_MAIL_PASSWORD"),
    "MAIL_DEFAULT_SENDER": "test@example.com"
}

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
nosh
  • 620
  • 3
  • 14
  • 50
  • As far as I know, there shouldn't be a problem with sending emails simultaneously via flask-mail. So I'm guessing the limiter is the mail server (mail.privateemail.com). Am I right? could you test the code with another mail server? Obviously without the queueing parts. If this is the case I'd recommend using another mail server. But if you still wanted to use this one and fix the problem with queueing then I'd help you do it. – mehdy Aug 07 '20 at 19:55
  • I don't think the issue is sending simultaneously through flask if I kept the connection open and sent multiple messages to different addresses, I think it has to do with the post route being called again while an email connection is open and for some reason it closes the connection. But then again Idk. That email server was provided by my DNS provider (NameCheap). I'll try it with sending through gmail's server. – nosh Aug 07 '20 at 22:38
  • I am testing this on localhost which isn't over SSL/TLS, could that be the issue? I did manage to get some emails through, but then when trying to post to the `/newsletter-subscribe` route on the same session, it threw the error that I mentioned in my post. – nosh Aug 07 '20 at 22:41
  • Right. By simultaneously I meant sending with multiple connections as well. When testing on localhost, disable the SSL/TLS stuff to make it easier to test. Could you provide the trackback info? which software did you use when testing on localhost? – mehdy Aug 08 '20 at 10:22
  • Everything seems to work now. I think it was an issue with the `mail.privateemail.com` server. I fixed some things with the DNS records on CloudFlare (I use as a CDN over my domain purchased through NameCheap) and now it seems to keep the connection open and send multiple emails simultaneously. Thanks for your input. – nosh Aug 09 '20 at 18:29
  • Actually the issue is happening again and I think I know why but not sure how to solve. It doesnt have to do with the email server. It seems like it has to do with WTFform. I wanted to have the form object shared by all routes within a blueprint but that seems to be causing the issue with the mail connection. I updated my code – nosh Aug 09 '20 at 21:34
  • Could you elaborate on how it's related to the WTForms? Is there a new error message you could share? – mehdy Aug 10 '20 at 07:38
  • It's the same message error as before. If you look at the code I added `main.routes.py` I have the form inside a method with the `@main.context_processor` decorator so that the form can be used on all routes within the `main` Blueprint. I think that because of this, it's overriding the mail connection which as I posted, results in an error. At this point I dont know what else it could be if I'm wrong. – nosh Aug 10 '20 at 15:15

2 Answers2

2

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
   
alexisdevarennes
  • 5,437
  • 4
  • 24
  • 38
  • I'll try these solutions out, and also contact my DNS provider who provides the email server for my domain to see what might be causing the issue. I did see somewhere to make the process sleep for 80 seconds before sending another email, however this does cause the issue of the client waiting for a response. I tried to send the email in a thread, but getting an issue with that. I made a different stack post if you want to check that out. – nosh Aug 10 '20 at 19:44
  • Hi nenur ; off love to take a look. In this case I don't think threading will improve the situation as it's the remote host that is closing the connection and I think if anything that threading will worsen your problem as you'll in theory be sending out emails at an even higher frequency. You are right that sleep introduces the problem that the user has to wait although I think 10 second shouldn't be too big of a worry unless you're handling a lot of emails. The try except version should only slow down things temporarily on exceptions.How many emails so you send out an hour? – alexisdevarennes Aug 11 '20 at 13:21
  • I will also stress that sorting this out with your provider might be the best as you might be suspended if they find your usage abusive, even if temporarily it's still better to avoid this situation. I'd check the amount of emails you're allowed to send and if you can higher those rates. Hope you get this sorted out :) – alexisdevarennes Aug 11 '20 at 13:25
  • Thank you for your response. My qyestion with the threading is that I wanted to be able to respond to the user that an email is being sent over an XHR request that was made when they posted the email form. Even if the email doesn't fail to send, it takes a while to send and I wanted to be able to respond before I call the method to send the email. However, I am not sure how to do this if not with threads. This is what my other stackpost I mentioned asks (https://stackoverflow.com/questions/63330734/thread-that-calls-function-in-a-http-request-throws-runtimeerror-working-outsid) – nosh Aug 12 '20 at 17:24
1

I've run into a similar issue with my provider (privateemail), one way I got it to work is blocking time.sleep() this however will cause requests to hand while you are sending your e-mails which is rather bad, a but if you don't mind it you can make something like this

try:
   context = ssl.create_default_context()
   server = smtplib.SMTP_SSL(smtp_server, port, context=context)
   server.login(sender_email, password)
   server.sendmail(sender_email, reciever_email, message.as_string())
   server.quit()
except smtplib.SMTPServerDisconnected:
   time.sleep(10)
   context = ssl.create_default_context()
   server = smtplib.SMTP_SSL(smtp_server, port, context=context)
   server.login(sender_email, password)
   server.sendmail(sender_email, reciever_email, message.as_string())
   server.quit()

Another better solution is to make an email process with celery so your code doesn't become blocking

marc_s
  • 732,580
  • 175
  • 1,330
  • 1,459
Patrik Radics
  • 43
  • 1
  • 5