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 = cors(app, allow_origin="*")
smtp_mail_server = Mail()
# Add this at the beginning of your file to configure Swagger UI
swagger_config = {

View file

@ -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("/<tenant_id>/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"]}

View file

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

View file

@ -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...")

View file

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

View file

@ -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}"