fix email bot in async + quart

This commit is contained in:
Billy Bao 2025-12-11 11:20:05 +08:00
parent fa7e3307f6
commit 58205f62e9
6 changed files with 76 additions and 69 deletions

View file

@ -42,7 +42,6 @@ __all__ = ["app"]
app = Quart(__name__) app = Quart(__name__)
app = cors(app, allow_origin="*") app = cors(app, allow_origin="*")
smtp_mail_server = Mail()
# Add this at the beginning of your file to configure Swagger UI # Add this at the beginning of your file to configure Swagger UI
swagger_config = { swagger_config = {

View file

@ -13,7 +13,8 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
# #
import logging
import asyncio
from api.db import UserTenantRole from api.db import UserTenantRole
from api.db.db_models import UserTenant from api.db.db_models import UserTenant
from api.db.services.user_service import UserTenantService, UserService from api.db.services.user_service import UserTenantService, UserService
@ -24,7 +25,7 @@ from common.time_utils import delta_seconds
from api.utils.api_utils import get_data_error_result, get_json_result, get_request_json, server_error_response, validate_request from api.utils.api_utils import get_data_error_result, get_json_result, get_request_json, server_error_response, validate_request
from api.utils.web_utils import send_invite_email from api.utils.web_utils import send_invite_email
from common import settings from common import settings
from api.apps import smtp_mail_server, login_required, current_user from api.apps import login_required, current_user
@manager.route("/<tenant_id>/user/list", methods=["GET"]) # noqa: F821 @manager.route("/<tenant_id>/user/list", methods=["GET"]) # noqa: F821
@ -80,7 +81,7 @@ async def create(tenant_id):
role=UserTenantRole.INVITE, role=UserTenantRole.INVITE,
status=StatusEnum.VALID.value) status=StatusEnum.VALID.value)
if smtp_mail_server and settings.SMTP_CONF: try:
from threading import Thread from threading import Thread
user_name = "" user_name = ""
@ -88,12 +89,17 @@ async def create(tenant_id):
if user: if user:
user_name = user.nickname user_name = user.nickname
Thread( asyncio.create_task(
target=send_invite_email, send_invite_email(
args=(invite_user_email, settings.MAIL_FRONTEND_URL, tenant_id, user_name or current_user.email), to_email=invite_user_email,
daemon=True invite_url=settings.MAIL_FRONTEND_URL,
).start() tenant_id=tenant_id,
inviter=user_name or current_user.email
)
)
except Exception as e:
logging.exception(f"Failed to send invite email to {invite_user_email}: {e}")
return get_json_result(data=False, message="Failed to send invite email.", code=RetCode.SERVER_ERROR)
usr = invite_users[0].to_dict() usr = invite_users[0].to_dict()
usr = {k: v for k, v in usr.items() if k in ["id", "avatar", "email", "nickname"]} usr = {k: v for k, v in usr.items() if k in ["id", "avatar", "email", "nickname"]}

View file

@ -45,7 +45,7 @@ from api.utils.api_utils import (
) )
from api.utils.crypt import decrypt from api.utils.crypt import decrypt
from rag.utils.redis_conn import REDIS_CONN from rag.utils.redis_conn import REDIS_CONN
from api.apps import smtp_mail_server, login_required, current_user, login_user, logout_user from api.apps import login_required, current_user, login_user, logout_user
from api.utils.web_utils import ( from api.utils.web_utils import (
send_email_html, send_email_html,
OTP_LENGTH, OTP_LENGTH,
@ -865,12 +865,17 @@ async def forget_get_captcha():
captcha_text = "".join(secrets.choice(allowed) for _ in range(OTP_LENGTH)) captcha_text = "".join(secrets.choice(allowed) for _ in range(OTP_LENGTH))
REDIS_CONN.set(captcha_key(email), captcha_text, 60) # Valid for 60 seconds REDIS_CONN.set(captcha_key(email), captcha_text, 60) # Valid for 60 seconds
print("\n\nGenerated captcha:", captcha_text, "\n\n")
from captcha.image import ImageCaptcha from captcha.image import ImageCaptcha
image = ImageCaptcha(width=300, height=120, font_sizes=[50, 60, 70]) image = ImageCaptcha(width=300, height=120, font_sizes=[50, 60, 70])
img_bytes = image.generate(captcha_text).read() img_bytes = image.generate(captcha_text).read()
response = await make_response(img_bytes)
response.headers.set("Content-Type", "image/JPEG") import base64
return response base64_img = base64.b64encode(img_bytes).decode('utf-8')
data_uri = f"data:image/jpeg;base64,{base64_img}"
return get_json_result(data=data_uri)
@manager.route("/forget/otp", methods=["POST"]) # noqa: F821 @manager.route("/forget/otp", methods=["POST"]) # noqa: F821
@ -923,19 +928,18 @@ async def forget_send_otp():
ttl_min = OTP_TTL_SECONDS // 60 ttl_min = OTP_TTL_SECONDS // 60
if not smtp_mail_server: try:
logging.warning("SMTP mail server not initialized; skip sending email.") await send_email_html(
else: subject="Your Password Reset Code",
try: to_email=email,
send_email_html( template_key="reset_code",
subject="Your Password Reset Code", code=otp,
to_email=email, ttl_min=ttl_min,
template_key="reset_code", )
code=otp,
ttl_min=ttl_min, except Exception as e:
) logging.exception(e)
except Exception: return get_json_result(data=False, code=RetCode.SERVER_ERROR, message="failed to send email")
return get_json_result(data=False, code=RetCode.SERVER_ERROR, message="failed to send email")
return get_json_result(data=True, code=RetCode.SUCCESS, message="verification passed, email sent") return get_json_result(data=True, code=RetCode.SUCCESS, message="verification passed, email sent")
@ -1011,4 +1015,4 @@ async def forget():
user.update_date = datetime_format(datetime.now()) user.update_date = datetime_format(datetime.now())
user.save() user.save()
msg = "Password reset successful. Logged in." msg = "Password reset successful. Logged in."
return await construct_response(data=user.to_json(), auth=user.get_id(), message=msg) return await construct_response(data=user.to_json(), auth=user.get_id(), message=msg)

View file

@ -30,7 +30,7 @@ import threading
import uuid import uuid
import faulthandler import faulthandler
from api.apps import app, smtp_mail_server from api.apps import app
from api.db.runtime_config import RuntimeConfig from api.db.runtime_config import RuntimeConfig
from api.db.services.document_service import DocumentService from api.db.services.document_service import DocumentService
from common.file_utils import get_project_base_directory from common.file_utils import get_project_base_directory
@ -143,18 +143,6 @@ if __name__ == '__main__':
else: else:
threading.Timer(1.0, delayed_start_update_progress).start() threading.Timer(1.0, delayed_start_update_progress).start()
# init smtp server
if settings.SMTP_CONF:
app.config["MAIL_SERVER"] = settings.MAIL_SERVER
app.config["MAIL_PORT"] = settings.MAIL_PORT
app.config["MAIL_USE_SSL"] = settings.MAIL_USE_SSL
app.config["MAIL_USE_TLS"] = settings.MAIL_USE_TLS
app.config["MAIL_USERNAME"] = settings.MAIL_USERNAME
app.config["MAIL_PASSWORD"] = settings.MAIL_PASSWORD
app.config["MAIL_DEFAULT_SENDER"] = settings.MAIL_DEFAULT_SENDER
smtp_mail_server.init_app(app)
# start http server # start http server
try: try:
logging.info("RAGFlow HTTP server start...") logging.info("RAGFlow HTTP server start...")

View file

@ -20,18 +20,18 @@ Reusable HTML email templates and registry.
# Invitation email template # Invitation email template
INVITE_EMAIL_TMPL = """ INVITE_EMAIL_TMPL = """
<p>Hi {{email}},</p> Hi {{email}},
<p>{{inviter}} has invited you to join their team (ID: {{tenant_id}}).</p> {{inviter}} has invited you to join their team (ID: {{tenant_id}}).
<p>Click the link below to complete your registration:<br> Click the link below to complete your registration:
<a href="{{invite_url}}">{{invite_url}}</a></p> {{invite_url}}
<p>If you did not request this, please ignore this email.</p> If you did not request this, please ignore this email.
""" """
# Password reset code template # Password reset code template
RESET_CODE_EMAIL_TMPL = """ RESET_CODE_EMAIL_TMPL = """
<p>Hello,</p> Hello,
<p>Your password reset code is: <b>{{ code }}</b></p> Your password reset code is: {{ code }}
<p>This code will expire in {{ ttl_min }} minutes.</p> This code will expire in {{ ttl_min }} minutes.
""" """
# Template registry # Template registry

View file

@ -20,9 +20,12 @@ import json
import re import re
import socket import socket
from urllib.parse import urlparse from urllib.parse import urlparse
import aiosmtplib
from api.apps import smtp_mail_server from api.apps import app
from email.mime.text import MIMEText
from email.header import Header
from flask_mail import Message from flask_mail import Message
from common import settings
from quart import render_template_string from quart import render_template_string
from api.utils.email_templates import EMAIL_TEMPLATES from api.utils.email_templates import EMAIL_TEMPLATES
from selenium import webdriver from selenium import webdriver
@ -183,27 +186,34 @@ def get_float(req: dict, key: str, default: float | int = 10.0) -> float:
return parsed if parsed > 0 else default return parsed if parsed > 0 else default
except (TypeError, ValueError): except (TypeError, ValueError):
return default return default
async def send_email_html(to_email: str, subject: str, template_key: str, **context):
body = await render_template_string(EMAIL_TEMPLATES.get(template_key), **context)
msg = MIMEText(body, "plain", "utf-8")
msg["Subject"] = Header(subject, "utf-8")
msg["From"] = f"{settings.MAIL_DEFAULT_SENDER[0]} <{settings.MAIL_DEFAULT_SENDER[1]}>"
msg["To"] = to_email
smtp = aiosmtplib.SMTP(
hostname=settings.MAIL_SERVER,
port=settings.MAIL_PORT,
use_tls=True,
timeout=10,
)
await smtp.connect()
await smtp.login(settings.MAIL_USERNAME, settings.MAIL_PASSWORD)
await smtp.send_message(msg)
await smtp.quit()
def send_email_html(subject: str, to_email: str, template_key: str, **context): async def send_invite_email(to_email, invite_url, tenant_id, inviter):
"""Generic HTML email sender using shared templates.
template_key must exist in EMAIL_TEMPLATES.
"""
from api.apps import app
tmpl = EMAIL_TEMPLATES.get(template_key)
if not tmpl:
raise ValueError(f"Unknown email template: {template_key}")
with app.app_context():
msg = Message(subject=subject, recipients=[to_email])
msg.html = render_template_string(tmpl, **context)
smtp_mail_server.send(msg)
def send_invite_email(to_email, invite_url, tenant_id, inviter):
# Reuse the generic HTML sender with 'invite' template # Reuse the generic HTML sender with 'invite' template
send_email_html( await send_email_html(
subject="RAGFlow Invitation",
to_email=to_email, to_email=to_email,
subject="RAGFlow Invitation",
template_key="invite", template_key="invite",
email=to_email, email=to_email,
invite_url=invite_url, invite_url=invite_url,
@ -230,4 +240,4 @@ def hash_code(code: str, salt: bytes) -> str:
def captcha_key(email: str) -> str: def captcha_key(email: str) -> str:
return f"captcha:{email}" return f"captcha:{email}"