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

Flask-Mail queue messages to be sent to different emails

我正在为我的 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

run.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

在我看来,您的邮件服务器正在关闭连接,因为您发出的请求超过其配置所允许的数量。如果您使用的是第三方邮件提供商,您可能需要检查您使用的服务是否提供任何发送批量电子邮件的方式,例如通过 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
   

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

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 进行电子邮件处理,这样您的代码就不会阻塞