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 = 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 = {
|
||||||
|
|
|
||||||
|
|
@ -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"]}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
@ -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...")
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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}"
|
||||||
|
|
||||||
Loading…
Add table
Reference in a new issue