fix email bot in async + quart
This commit is contained in:
parent
fa7e3307f6
commit
58205f62e9
6 changed files with 76 additions and 69 deletions
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -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"]}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
@ -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...")
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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}"
|
||||
|
||||
|
||||
Loading…
Add table
Reference in a new issue