From 58205f62e9ae6e06b03741532e6c681da80ef048 Mon Sep 17 00:00:00 2001 From: Billy Bao Date: Thu, 11 Dec 2025 11:20:05 +0800 Subject: [PATCH] fix email bot in async + quart --- api/apps/__init__.py | 1 - api/apps/tenant_app.py | 24 ++++++++++------- api/apps/user_app.py | 40 ++++++++++++++++------------- api/ragflow_server.py | 14 +--------- api/utils/email_templates.py | 16 ++++++------ api/utils/web_utils.py | 50 +++++++++++++++++++++--------------- 6 files changed, 76 insertions(+), 69 deletions(-) diff --git a/api/apps/__init__.py b/api/apps/__init__.py index 9ef2f97d9..5e0796f3f 100644 --- a/api/apps/__init__.py +++ b/api/apps/__init__.py @@ -42,7 +42,6 @@ __all__ = ["app"] app = Quart(__name__) app = cors(app, allow_origin="*") -smtp_mail_server = Mail() # Add this at the beginning of your file to configure Swagger UI swagger_config = { diff --git a/api/apps/tenant_app.py b/api/apps/tenant_app.py index fdb764e65..ae456fb79 100644 --- a/api/apps/tenant_app.py +++ b/api/apps/tenant_app.py @@ -13,7 +13,8 @@ # See the License for the specific language governing permissions and # limitations under the License. # - +import logging +import asyncio from api.db import UserTenantRole from api.db.db_models import UserTenant 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.web_utils import send_invite_email 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("//user/list", methods=["GET"]) # noqa: F821 @@ -80,7 +81,7 @@ async def create(tenant_id): role=UserTenantRole.INVITE, status=StatusEnum.VALID.value) - if smtp_mail_server and settings.SMTP_CONF: + try: from threading import Thread user_name = "" @@ -88,12 +89,17 @@ async def create(tenant_id): if user: user_name = user.nickname - Thread( - target=send_invite_email, - args=(invite_user_email, settings.MAIL_FRONTEND_URL, tenant_id, user_name or current_user.email), - daemon=True - ).start() - + asyncio.create_task( + send_invite_email( + to_email=invite_user_email, + invite_url=settings.MAIL_FRONTEND_URL, + 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 = {k: v for k, v in usr.items() if k in ["id", "avatar", "email", "nickname"]} diff --git a/api/apps/user_app.py b/api/apps/user_app.py index 78407b242..a449a5470 100644 --- a/api/apps/user_app.py +++ b/api/apps/user_app.py @@ -45,7 +45,7 @@ from api.utils.api_utils import ( ) from api.utils.crypt import decrypt 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 ( send_email_html, OTP_LENGTH, @@ -865,12 +865,17 @@ async def forget_get_captcha(): captcha_text = "".join(secrets.choice(allowed) for _ in range(OTP_LENGTH)) 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 image = ImageCaptcha(width=300, height=120, font_sizes=[50, 60, 70]) img_bytes = image.generate(captcha_text).read() - response = await make_response(img_bytes) - response.headers.set("Content-Type", "image/JPEG") - return response + + import base64 + 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 @@ -923,19 +928,18 @@ async def forget_send_otp(): ttl_min = OTP_TTL_SECONDS // 60 - if not smtp_mail_server: - logging.warning("SMTP mail server not initialized; skip sending email.") - else: - try: - send_email_html( - subject="Your Password Reset Code", - to_email=email, - template_key="reset_code", - code=otp, - ttl_min=ttl_min, - ) - except Exception: - return get_json_result(data=False, code=RetCode.SERVER_ERROR, message="failed to send email") + try: + await send_email_html( + subject="Your Password Reset Code", + to_email=email, + template_key="reset_code", + code=otp, + ttl_min=ttl_min, + ) + + except Exception as e: + logging.exception(e) + 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") @@ -1011,4 +1015,4 @@ async def forget(): user.update_date = datetime_format(datetime.now()) user.save() 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) \ No newline at end of file diff --git a/api/ragflow_server.py b/api/ragflow_server.py index 59622fe68..26cd045c4 100644 --- a/api/ragflow_server.py +++ b/api/ragflow_server.py @@ -30,7 +30,7 @@ import threading import uuid 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.services.document_service import DocumentService from common.file_utils import get_project_base_directory @@ -143,18 +143,6 @@ if __name__ == '__main__': else: 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 try: logging.info("RAGFlow HTTP server start...") diff --git a/api/utils/email_templates.py b/api/utils/email_templates.py index 34201ee38..767eb7b92 100644 --- a/api/utils/email_templates.py +++ b/api/utils/email_templates.py @@ -20,18 +20,18 @@ Reusable HTML email templates and registry. # Invitation email template INVITE_EMAIL_TMPL = """ -

Hi {{email}},

-

{{inviter}} has invited you to join their team (ID: {{tenant_id}}).

-

Click the link below to complete your registration:
-{{invite_url}}

-

If you did not request this, please ignore this email.

+Hi {{email}}, +{{inviter}} has invited you to join their team (ID: {{tenant_id}}). +Click the link below to complete your registration: +{{invite_url}} +If you did not request this, please ignore this email. """ # Password reset code template RESET_CODE_EMAIL_TMPL = """ -

Hello,

-

Your password reset code is: {{ code }}

-

This code will expire in {{ ttl_min }} minutes.

+Hello, +Your password reset code is: {{ code }} +This code will expire in {{ ttl_min }} minutes. """ # Template registry diff --git a/api/utils/web_utils.py b/api/utils/web_utils.py index 0f2484c56..684c51028 100644 --- a/api/utils/web_utils.py +++ b/api/utils/web_utils.py @@ -20,9 +20,12 @@ import json import re import socket from urllib.parse import urlparse - -from api.apps import smtp_mail_server +import aiosmtplib +from api.apps import app +from email.mime.text import MIMEText +from email.header import Header from flask_mail import Message +from common import settings from quart import render_template_string from api.utils.email_templates import EMAIL_TEMPLATES 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 except (TypeError, ValueError): 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): - """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): +async def send_invite_email(to_email, invite_url, tenant_id, inviter): # Reuse the generic HTML sender with 'invite' template - send_email_html( - subject="RAGFlow Invitation", + await send_email_html( to_email=to_email, + subject="RAGFlow Invitation", template_key="invite", email=to_email, invite_url=invite_url, @@ -230,4 +240,4 @@ def hash_code(code: str, salt: bytes) -> str: def captcha_key(email: str) -> str: return f"captcha:{email}" - + \ No newline at end of file