diff --git a/api/apps/__init__.py b/api/apps/__init__.py index 007e37430..fba5d20b2 100644 --- a/api/apps/__init__.py +++ b/api/apps/__init__.py @@ -29,6 +29,7 @@ from api.db.db_models import close_connection from api.db.services import UserService from api.utils import CustomJSONEncoder, commands +from flask_mail import Mail from flask_session import Session from flask_login import LoginManager from api import settings @@ -40,6 +41,7 @@ __all__ = ["app"] Request.json = property(lambda self: self.get_json(force=True, silent=True)) app = Flask(__name__) +smtp_mail_server = Mail() # Add this at the beginning of your file to configure Swagger UI swagger_config = { @@ -146,16 +148,16 @@ def load_user(web_request): if authorization: try: access_token = str(jwt.loads(authorization)) - + if not access_token or not access_token.strip(): logging.warning("Authentication attempt with empty access token") return None - + # Access tokens should be UUIDs (32 hex characters) if len(access_token.strip()) < 32: logging.warning(f"Authentication attempt with invalid token format: {len(access_token)} chars") return None - + user = UserService.query( access_token=access_token, status=StatusEnum.VALID.value ) diff --git a/api/apps/tenant_app.py b/api/apps/tenant_app.py index 42b4d0a14..63c7f74b7 100644 --- a/api/apps/tenant_app.py +++ b/api/apps/tenant_app.py @@ -18,12 +18,14 @@ from flask import request from flask_login import login_required, current_user from api import settings +from api.apps import smtp_mail_server from api.db import UserTenantRole, StatusEnum from api.db.db_models import UserTenant from api.db.services.user_service import UserTenantService, UserService from api.utils import get_uuid, delta_seconds from api.utils.api_utils import get_json_result, validate_request, server_error_response, get_data_error_result +from api.utils.web_utils import send_invite_email @manager.route("//user/list", methods=["GET"]) # noqa: F821 @@ -78,6 +80,20 @@ def create(tenant_id): role=UserTenantRole.INVITE, status=StatusEnum.VALID.value) + if smtp_mail_server and settings.SMTP_CONF: + from threading import Thread + + user_name = "" + _, user = UserService.get_by_id(current_user.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() + 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/ragflow_server.py b/api/ragflow_server.py index 6d6c72ea1..8d0432012 100644 --- a/api/ragflow_server.py +++ b/api/ragflow_server.py @@ -33,7 +33,7 @@ import uuid from werkzeug.serving import run_simple from api import settings -from api.apps import app +from api.apps import app, smtp_mail_server from api.db.runtime_config import RuntimeConfig from api.db.services.document_service import DocumentService from api import utils @@ -74,11 +74,11 @@ def signal_handler(sig, frame): if __name__ == '__main__': logging.info(r""" - ____ ___ ______ ______ __ + ____ ___ ______ ______ __ / __ \ / | / ____// ____// /____ _ __ / /_/ // /| | / / __ / /_ / // __ \| | /| / / - / _, _// ___ |/ /_/ // __/ / // /_/ /| |/ |/ / - /_/ |_|/_/ |_|\____//_/ /_/ \____/ |__/|__/ + / _, _// ___ |/ /_/ // __/ / // /_/ /| |/ |/ / + /_/ |_|/_/ |_|\____//_/ /_/ \____/ |__/|__/ """) logging.info( @@ -137,6 +137,18 @@ 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/settings.py b/api/settings.py index f45483651..3148633e6 100644 --- a/api/settings.py +++ b/api/settings.py @@ -79,6 +79,16 @@ STRONG_TEST_COUNT = int(os.environ.get("STRONG_TEST_COUNT", "8")) BUILTIN_EMBEDDING_MODELS = ["BAAI/bge-large-zh-v1.5@BAAI", "maidalun1020/bce-embedding-base_v1@Youdao"] +SMTP_CONF = None +MAIL_SERVER = "" +MAIL_PORT = 000 +MAIL_USE_SSL= True +MAIL_USE_TLS = False +MAIL_USERNAME = "" +MAIL_PASSWORD = "" +MAIL_DEFAULT_SENDER = () +MAIL_FRONTEND_URL = "" + def get_or_create_secret_key(): secret_key = os.environ.get("RAGFLOW_SECRET_KEY") @@ -186,6 +196,21 @@ def init_settings(): global SANDBOX_HOST SANDBOX_HOST = os.environ.get("SANDBOX_HOST", "sandbox-executor-manager") + global SMTP_CONF, MAIL_SERVER, MAIL_PORT, MAIL_USE_SSL, MAIL_USE_TLS + global MAIL_USERNAME, MAIL_PASSWORD, MAIL_DEFAULT_SENDER, MAIL_FRONTEND_URL + SMTP_CONF = get_base_config("smtp", {}) + + MAIL_SERVER = SMTP_CONF.get("mail_server", "") + MAIL_PORT = SMTP_CONF.get("mail_port", 000) + MAIL_USE_SSL = SMTP_CONF.get("mail_use_ssl", True) + MAIL_USE_TLS = SMTP_CONF.get("mail_use_tls", False) + MAIL_USERNAME = SMTP_CONF.get("mail_username", "") + MAIL_PASSWORD = SMTP_CONF.get("mail_password", "") + mail_default_sender = SMTP_CONF.get("mail_default_sender", []) + if mail_default_sender and len(mail_default_sender) >= 2: + MAIL_DEFAULT_SENDER = (mail_default_sender[0], mail_default_sender[1]) + MAIL_FRONTEND_URL = SMTP_CONF.get("mail_frontend_url", "") + class CustomEnum(Enum): @classmethod diff --git a/api/utils/web_utils.py b/api/utils/web_utils.py index 7bb25728e..55ce561ea 100644 --- a/api/utils/web_utils.py +++ b/api/utils/web_utils.py @@ -21,6 +21,9 @@ import re import socket from urllib.parse import urlparse +from api.apps import smtp_mail_server +from flask_mail import Message +from flask import render_template_string from selenium import webdriver from selenium.common.exceptions import TimeoutException from selenium.webdriver.chrome.options import Options @@ -31,6 +34,7 @@ from selenium.webdriver.support.ui import WebDriverWait from webdriver_manager.chrome import ChromeDriverManager + CONTENT_TYPE_MAP = { # Office "docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document", @@ -172,3 +176,26 @@ 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 + + +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.

+""" + +def send_invite_email(to_email, invite_url, tenant_id, inviter): + from api.apps import app + with app.app_context(): + msg = Message(subject="RAGFlow Invitation", + recipients=[to_email]) + msg.html = render_template_string( + INVITE_EMAIL_TMPL, + email=to_email, + invite_url=invite_url, + tenant_id=tenant_id, + inviter=inviter, + ) + smtp_mail_server.send(msg) diff --git a/conf/service_conf.yaml b/conf/service_conf.yaml index 9a995c2b2..7b76f2b4f 100644 --- a/conf/service_conf.yaml +++ b/conf/service_conf.yaml @@ -113,3 +113,14 @@ redis: # switch: false # component: false # dataset: false +# smtp: +# mail_server: "" +# mail_port: 465 +# mail_use_ssl: true +# mail_use_tls: false +# mail_username: "" +# mail_password: "" +# mail_default_sender: +# - "RAGFlow" # display name +# - "" # sender email address +# mail_frontend_url: "https://your-frontend.example.com" diff --git a/pyproject.toml b/pyproject.toml index 891861d2b..3b4b46e84 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -130,6 +130,7 @@ dependencies = [ "click>=8.1.8", "python-calamine>=0.4.0", "litellm>=1.74.15.post1", + "flask-mail>=0.10.0", ] [project.optional-dependencies] diff --git a/uv.lock b/uv.lock index c648e1369..53f891047 100644 --- a/uv.lock +++ b/uv.lock @@ -1,4 +1,5 @@ version = 1 +revision = 1 requires-python = ">=3.10, <3.13" resolution-markers = [ "python_full_version >= '3.12' and sys_platform == 'darwin'", @@ -1189,9 +1190,6 @@ name = "datrie" version = "0.8.2" source = { registry = "https://mirrors.aliyun.com/pypi/simple" } sdist = { url = "https://mirrors.aliyun.com/pypi/packages/9d/fe/db74bd405d515f06657f11ad529878fd389576dca4812bea6f98d9b31574/datrie-0.8.2.tar.gz", hash = "sha256:525b08f638d5cf6115df6ccd818e5a01298cd230b2dac91c8ff2e6499d18765d" } -wheels = [ - { url = "https://mirrors.aliyun.com/pypi/packages/44/02/53f0cf0bf0cd629ba6c2cc13f2f9db24323459e9c19463783d890a540a96/datrie-0.8.2-pp273-pypy_73-win32.whl", hash = "sha256:b07bd5fdfc3399a6dab86d6e35c72b1dbd598e80c97509c7c7518ab8774d3fda" }, -] [[package]] name = "debugpy" @@ -1653,6 +1651,19 @@ wheels = [ { url = "https://mirrors.aliyun.com/pypi/packages/59/f5/67e9cc5c2036f58115f9fe0f00d203cf6780c3ff8ae0e705e7a9d9e8ff9e/Flask_Login-0.6.3-py3-none-any.whl", hash = "sha256:849b25b82a436bf830a054e74214074af59097171562ab10bfa999e6b78aae5d" }, ] +[[package]] +name = "flask-mail" +version = "0.10.0" +source = { registry = "https://mirrors.aliyun.com/pypi/simple" } +dependencies = [ + { name = "blinker" }, + { name = "flask" }, +] +sdist = { url = "https://mirrors.aliyun.com/pypi/packages/ba/29/e92dc84c675d1e8d260d5768eb3fb65c70cbd33addecf424187587bee862/flask_mail-0.10.0.tar.gz", hash = "sha256:44083e7b02bbcce792209c06252f8569dd5a325a7aaa76afe7330422bd97881d" } +wheels = [ + { url = "https://mirrors.aliyun.com/pypi/packages/e4/c0/a81083da779f482494d49195d8b6c9fde21072558253e4a9fb2ec969c3c1/flask_mail-0.10.0-py3-none-any.whl", hash = "sha256:a451e490931bb3441d9b11ebab6812a16bfa81855792ae1bf9c1e1e22c4e51e7" }, +] + [[package]] name = "flask-session" version = "0.8.0" @@ -4664,8 +4675,6 @@ wheels = [ { url = "https://mirrors.aliyun.com/pypi/packages/59/fe/aae679b64363eb78326c7fdc9d06ec3de18bac68be4b612fc1fe8902693c/pycryptodome-3.23.0-cp37-abi3-win32.whl", hash = "sha256:507dbead45474b62b2bbe318eb1c4c8ee641077532067fec9c1aa82c31f84886" }, { url = "https://mirrors.aliyun.com/pypi/packages/54/2f/e97a1b8294db0daaa87012c24a7bb714147c7ade7656973fd6c736b484ff/pycryptodome-3.23.0-cp37-abi3-win_amd64.whl", hash = "sha256:c75b52aacc6c0c260f204cbdd834f76edc9fb0d8e0da9fbf8352ef58202564e2" }, { url = "https://mirrors.aliyun.com/pypi/packages/18/3d/f9441a0d798bf2b1e645adc3265e55706aead1255ccdad3856dbdcffec14/pycryptodome-3.23.0-cp37-abi3-win_arm64.whl", hash = "sha256:11eeeb6917903876f134b56ba11abe95c0b0fd5e3330def218083c7d98bbcb3c" }, - { url = "https://mirrors.aliyun.com/pypi/packages/9f/7c/f5b0556590e7b4e710509105e668adb55aa9470a9f0e4dea9c40a4a11ce1/pycryptodome-3.23.0-pp27-pypy_73-manylinux2010_x86_64.whl", hash = "sha256:350ebc1eba1da729b35ab7627a833a1a355ee4e852d8ba0447fafe7b14504d56" }, - { url = "https://mirrors.aliyun.com/pypi/packages/33/38/dcc795578d610ea1aaffef4b148b8cafcfcf4d126b1e58231ddc4e475c70/pycryptodome-3.23.0-pp27-pypy_73-win32.whl", hash = "sha256:93837e379a3e5fd2bb00302a47aee9fdf7940d83595be3915752c74033d17ca7" }, { url = "https://mirrors.aliyun.com/pypi/packages/d9/12/e33935a0709c07de084d7d58d330ec3f4daf7910a18e77937affdb728452/pycryptodome-3.23.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:ddb95b49df036ddd264a0ad246d1be5b672000f12d6961ea2c267083a5e19379" }, { url = "https://mirrors.aliyun.com/pypi/packages/22/0b/aa8f9419f25870889bebf0b26b223c6986652bdf071f000623df11212c90/pycryptodome-3.23.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e95564beb8782abfd9e431c974e14563a794a4944c29d6d3b7b5ea042110b4" }, { url = "https://mirrors.aliyun.com/pypi/packages/d4/5e/63f5cbde2342b7f70a39e591dbe75d9809d6338ce0b07c10406f1a140cdc/pycryptodome-3.23.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14e15c081e912c4b0d75632acd8382dfce45b258667aa3c67caf7a4d4c13f630" }, @@ -4689,8 +4698,6 @@ wheels = [ { url = "https://mirrors.aliyun.com/pypi/packages/48/7d/0f2b09490b98cc6a902ac15dda8760c568b9c18cfe70e0ef7a16de64d53a/pycryptodomex-3.20.0-cp35-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:7a7a8f33a1f1fb762ede6cc9cbab8f2a9ba13b196bfaf7bc6f0b39d2ba315a43" }, { url = "https://mirrors.aliyun.com/pypi/packages/b0/1c/375adb14b71ee1c8d8232904e928b3e7af5bbbca7c04e4bec94fe8e90c3d/pycryptodomex-3.20.0-cp35-abi3-win32.whl", hash = "sha256:c39778fd0548d78917b61f03c1fa8bfda6cfcf98c767decf360945fe6f97461e" }, { url = "https://mirrors.aliyun.com/pypi/packages/b2/e8/1b92184ab7e5595bf38000587e6f8cf9556ebd1bf0a583619bee2057afbd/pycryptodomex-3.20.0-cp35-abi3-win_amd64.whl", hash = "sha256:2a47bcc478741b71273b917232f521fd5704ab4b25d301669879e7273d3586cc" }, - { url = "https://mirrors.aliyun.com/pypi/packages/e7/c5/9140bb867141d948c8e242013ec8a8011172233c898dfdba0a2417c3169a/pycryptodomex-3.20.0-pp27-pypy_73-manylinux2010_x86_64.whl", hash = "sha256:1be97461c439a6af4fe1cf8bf6ca5936d3db252737d2f379cc6b2e394e12a458" }, - { url = "https://mirrors.aliyun.com/pypi/packages/5e/6a/04acb4978ce08ab16890c70611ebc6efd251681341617bbb9e53356dee70/pycryptodomex-3.20.0-pp27-pypy_73-win32.whl", hash = "sha256:19764605feea0df966445d46533729b645033f134baeb3ea26ad518c9fdf212c" }, { url = "https://mirrors.aliyun.com/pypi/packages/eb/df/3f1ea084e43b91e6d2b6b3493cc948864c17ea5d93ff1261a03812fbfd1a/pycryptodomex-3.20.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:f2e497413560e03421484189a6b65e33fe800d3bd75590e6d78d4dfdb7accf3b" }, { url = "https://mirrors.aliyun.com/pypi/packages/c9/f3/83ffbdfa0c8f9154bcd8866895f6cae5a3ec749da8b0840603cf936c4412/pycryptodomex-3.20.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e48217c7901edd95f9f097feaa0388da215ed14ce2ece803d3f300b4e694abea" }, { url = "https://mirrors.aliyun.com/pypi/packages/c9/9d/c113e640aaf02af5631ae2686b742aac5cd0e1402b9d6512b1c7ec5ef05d/pycryptodomex-3.20.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d00fe8596e1cc46b44bf3907354e9377aa030ec4cd04afbbf6e899fc1e2a7781" }, @@ -5293,6 +5300,7 @@ dependencies = [ { name = "flask" }, { name = "flask-cors" }, { name = "flask-login" }, + { name = "flask-mail" }, { name = "flask-session" }, { name = "google-generativeai" }, { name = "google-search-results" }, @@ -5447,6 +5455,7 @@ requires-dist = [ { name = "flask", specifier = "==3.0.3" }, { name = "flask-cors", specifier = "==5.0.0" }, { name = "flask-login", specifier = "==0.6.3" }, + { name = "flask-mail", specifier = ">=0.10.0" }, { name = "flask-session", specifier = "==0.8.0" }, { name = "google-generativeai", specifier = ">=0.8.1,<0.9.0" }, { name = "google-search-results", specifier = "==2.4.2" }, @@ -5492,7 +5501,7 @@ requires-dist = [ { name = "pyicu", specifier = ">=2.13.1,<3.0.0" }, { name = "pymysql", specifier = ">=1.1.1,<2.0.0" }, { name = "pyodbc", specifier = ">=5.2.0,<6.0.0" }, - { name = "pypdf", specifier = "===6.0.0" }, + { name = "pypdf", specifier = "==6.0.0" }, { name = "pypdf2", specifier = ">=3.0.1,<4.0.0" }, { name = "python-calamine", specifier = ">=0.4.0" }, { name = "python-dateutil", specifier = "==2.8.2" }, @@ -5539,6 +5548,7 @@ requires-dist = [ { name = "yfinance", specifier = "==0.2.65" }, { name = "zhipuai", specifier = "==2.0.1" }, ] +provides-extras = ["full"] [package.metadata.requires-dev] test = [