Flask-Mail 队列消息被发送到不同的电子邮件

Posted

技术标签:

【中文标题】Flask-Mail 队列消息被发送到不同的电子邮件【英文标题】:Flask-Mail queue messages to be sent to different emails 【发布时间】:2020-11-23 21:14:55 【问题描述】:

我正在为我的 Flask 应用程序使用 Flask-Mail 库,以便在用户注册以添加到时事通讯时向他们发送默认欢迎电子邮件。调试该库后,我发现它一次只能处理一个连接发送消息,然后会自动关闭连接。如果后端在连接仍处于打开状态时向另一个用户发送电子邮件,则会引发此异常:raise SMTPServerDisconnected("Connection unexpectedly closed: " smtplib.SMTPServerDisconnected: Connection unexpectedly closed: [WinError 10054] An existing connection was forcibly closed by the remote host。我希望能够在连接关闭后将邮件库排队以向另一个收件人发送新邮件,但目前当我尝试将函数排队以发送邮件时,它一直抛出我上面提到的错误。

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

_初始化_.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

运行.py

from flask_app import create_app

错误日志:

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

【问题讨论】:

据我所知,通过flask-mail同时发送电子邮件应该没有问题。所以我猜限制器是邮件服务器(mail.privateemail.com)。我对吗?你能用另一个邮件服务器测试代码吗?显然没有排队部分。如果是这种情况,我建议使用其他邮件服务器。但是,如果您仍然想使用这个并解决排队问题,那么我会帮助您做到这一点。 如果我保持连接打开并将多条消息发送到不同的地址,我认为问题不是通过烧瓶同时发送,我认为这与在发送电子邮件时再次调用 post 路由有关连接是打开的,由于某种原因它关闭了连接。但是又是Idk。该电子邮件服务器由我的 DNS 提供商 (NameCheap) 提供。我将尝试通过 gmail 的服务器发送。 我正在本地主机上测试它,它不是通过 SSL/TLS,这可能是问题吗?我确实设法通过了一些电子邮件,但是当尝试在同一会话中发布到 /newsletter-subscribe 路由时,它抛出了我在帖子中提到的错误。 对。同时我的意思是同时发送多个连接。在 localhost 上进行测试时,禁用 SSL/TLS 内容以使其更易于测试。你能提供引用信息吗?你在 localhost 上测试的时候用的是什么软件? 现在似乎一切正常。我认为这是mail.privateemail.com 服务器的问题。我用 CloudFlare 上的 DNS 记录修复了一些问题(我在通过 NameCheap 购买的域上用作 CDN),现在它似乎保持连接打开并同时发送多封电子邮件。感谢您的意见。 【参考方案1】:

在我看来,您的邮件服务器正在关闭连接,因为您发出的请求超出了其配置允许的数量。如果您使用的是第三方邮件提供商,您可能需要检查您使用的服务是否提供任何方式来发送批量电子邮件,例如通过 API 或文件上传。或者他们是否有办法为您更改该配置。

如果不可能:

一种解决方案是拨打阻止电话 (time.sleep()) 以降低发送邮件的频率:

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)

另一种解决方案是将您的代码包装在 try catch 块中,并在出现异常时等待一段时间,然后再尝试再次发送电子邮件。

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
   

【讨论】:

我将尝试这些解决方案,并联系为我的域提供电子邮件服务器的 DNS 提供商,了解可能导致问题的原因。在发送另一封电子邮件之前,我确实看到某个地方让进程休眠 80 秒,但这确实会导致客户端等待响应的问题。我试图在一个线程中发送电子邮件,但遇到了问题。如果你想检查一下,我发了一个不同的堆栈帖子。 你好;关爱去看看。在这种情况下,我认为线程不会改善这种情况,因为它是关闭连接的远程主机,我认为如果有任何线程会恶化你的问题,因为理论上你会以更高的频率发送电子邮件。你是对的,睡眠引入了用户必须等待的问题,尽管我认为 10 秒不应该是太大的担心,除非你正在处理大量的电子邮件。 try except 版本应该只会在异常情况下暂时减慢速度。一个小时发送多少封电子邮件? 我还要强调,与您的提供商一起解决这个问题可能是最好的,因为如果他们发现您的使用不当,您可能会被暂停,即使暂时避免这种情况仍然更好。我会检查您允许发送的电子邮件数量,以及是否可以提高这些费率。希望你能解决这个问题:) 感谢您的回复。我对线程的疑问是,我希望能够响应用户正在通过 XHR 请求发送电子邮件,该请求是在他们发布电子邮件表单时发出的。即使电子邮件没有发送失败,发送也需要一段时间,我希望能够在调用该方法发送电子邮件之前做出响应。但是,如果不使用线程,我不确定如何执行此操作。这就是我提到的其他堆栈帖子所要求的 (***.com/questions/63330734/…)【参考方案2】:

我的提供商 (privateemail) 遇到了类似的问题,我让它工作的一种方法是阻止 time.sleep(),但是这会导致在您发送电子邮件时发送请求,这是相当糟糕的, a 但是如果你不介意的话,你可以做这样的事情

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()

另一个更好的解决方案是使用 celery 进行电子邮件处理,这样您的代码就不会被阻塞

【讨论】:

以上是关于Flask-Mail 队列消息被发送到不同的电子邮件的主要内容,如果未能解决你的问题,请参考以下文章

发送消息时禁用登录 Flask-Mail

使用flask-mail通过gmail发送电子邮件

使用flask-mail发送电子邮件时出现的问题

用Flask-mail发送电子邮件

使用内嵌图像Flask-Mail发送电子邮件?

在 Java 中动态创建异步消息队列