Feat/support sendgrid (#21011)
Co-authored-by: André de Matteo <andre.matteo@accenture.com> Co-authored-by: crazywoola <100913391+crazywoola@users.noreply.github.com>
This commit is contained in:
@@ -339,9 +339,11 @@ PROMPT_GENERATION_MAX_TOKENS=512
|
|||||||
CODE_GENERATION_MAX_TOKENS=1024
|
CODE_GENERATION_MAX_TOKENS=1024
|
||||||
PLUGIN_BASED_TOKEN_COUNTING_ENABLED=false
|
PLUGIN_BASED_TOKEN_COUNTING_ENABLED=false
|
||||||
|
|
||||||
# Mail configuration, support: resend, smtp
|
# Mail configuration, support: resend, smtp, sendgrid
|
||||||
MAIL_TYPE=
|
MAIL_TYPE=
|
||||||
|
# If using SendGrid, use the 'from' field for authentication if necessary.
|
||||||
MAIL_DEFAULT_SEND_FROM=no-reply <no-reply@dify.ai>
|
MAIL_DEFAULT_SEND_FROM=no-reply <no-reply@dify.ai>
|
||||||
|
# resend configuration
|
||||||
RESEND_API_KEY=
|
RESEND_API_KEY=
|
||||||
RESEND_API_URL=https://api.resend.com
|
RESEND_API_URL=https://api.resend.com
|
||||||
# smtp configuration
|
# smtp configuration
|
||||||
@@ -351,7 +353,8 @@ SMTP_USERNAME=123
|
|||||||
SMTP_PASSWORD=abc
|
SMTP_PASSWORD=abc
|
||||||
SMTP_USE_TLS=true
|
SMTP_USE_TLS=true
|
||||||
SMTP_OPPORTUNISTIC_TLS=false
|
SMTP_OPPORTUNISTIC_TLS=false
|
||||||
|
# Sendgid configuration
|
||||||
|
SENDGRID_API_KEY=
|
||||||
# Sentry configuration
|
# Sentry configuration
|
||||||
SENTRY_DSN=
|
SENTRY_DSN=
|
||||||
|
|
||||||
|
@@ -609,7 +609,7 @@ class MailConfig(BaseSettings):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
MAIL_TYPE: Optional[str] = Field(
|
MAIL_TYPE: Optional[str] = Field(
|
||||||
description="Email service provider type ('smtp' or 'resend'), default to None.",
|
description="Email service provider type ('smtp' or 'resend' or 'sendGrid), default to None.",
|
||||||
default=None,
|
default=None,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -663,6 +663,11 @@ class MailConfig(BaseSettings):
|
|||||||
default=50,
|
default=50,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
SENDGRID_API_KEY: Optional[str] = Field(
|
||||||
|
description="API key for SendGrid service",
|
||||||
|
default=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class RagEtlConfig(BaseSettings):
|
class RagEtlConfig(BaseSettings):
|
||||||
"""
|
"""
|
||||||
|
@@ -54,6 +54,15 @@ class Mail:
|
|||||||
use_tls=dify_config.SMTP_USE_TLS,
|
use_tls=dify_config.SMTP_USE_TLS,
|
||||||
opportunistic_tls=dify_config.SMTP_OPPORTUNISTIC_TLS,
|
opportunistic_tls=dify_config.SMTP_OPPORTUNISTIC_TLS,
|
||||||
)
|
)
|
||||||
|
case "sendgrid":
|
||||||
|
from libs.sendgrid import SendGridClient
|
||||||
|
|
||||||
|
if not dify_config.SENDGRID_API_KEY:
|
||||||
|
raise ValueError("SENDGRID_API_KEY is required for SendGrid mail type")
|
||||||
|
|
||||||
|
self._client = SendGridClient(
|
||||||
|
sendgrid_api_key=dify_config.SENDGRID_API_KEY, _from=dify_config.MAIL_DEFAULT_SEND_FROM or ""
|
||||||
|
)
|
||||||
case _:
|
case _:
|
||||||
raise ValueError("Unsupported mail type {}".format(mail_type))
|
raise ValueError("Unsupported mail type {}".format(mail_type))
|
||||||
|
|
||||||
|
42
api/libs/sendgrid.py
Normal file
42
api/libs/sendgrid.py
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import logging
|
||||||
|
|
||||||
|
import sendgrid # type: ignore
|
||||||
|
from python_http_client.exceptions import ForbiddenError, UnauthorizedError
|
||||||
|
from sendgrid.helpers.mail import Content, Email, Mail, To # type: ignore
|
||||||
|
|
||||||
|
|
||||||
|
class SendGridClient:
|
||||||
|
def __init__(self, sendgrid_api_key: str, _from: str):
|
||||||
|
self.sendgrid_api_key = sendgrid_api_key
|
||||||
|
self._from = _from
|
||||||
|
|
||||||
|
def send(self, mail: dict):
|
||||||
|
logging.debug("Sending email with SendGrid")
|
||||||
|
|
||||||
|
try:
|
||||||
|
_to = mail["to"]
|
||||||
|
|
||||||
|
if not _to:
|
||||||
|
raise ValueError("SendGridClient: Cannot send email: recipient address is missing.")
|
||||||
|
|
||||||
|
sg = sendgrid.SendGridAPIClient(api_key=self.sendgrid_api_key)
|
||||||
|
from_email = Email(self._from)
|
||||||
|
to_email = To(_to)
|
||||||
|
subject = mail["subject"]
|
||||||
|
content = Content("text/html", mail["html"])
|
||||||
|
mail = Mail(from_email, to_email, subject, content)
|
||||||
|
mail_json = mail.get() # type: ignore
|
||||||
|
response = sg.client.mail.send.post(request_body=mail_json)
|
||||||
|
logging.debug(response.status_code)
|
||||||
|
logging.debug(response.body)
|
||||||
|
logging.debug(response.headers)
|
||||||
|
|
||||||
|
except TimeoutError as e:
|
||||||
|
logging.exception("SendGridClient Timeout occurred while sending email")
|
||||||
|
raise
|
||||||
|
except (UnauthorizedError, ForbiddenError) as e:
|
||||||
|
logging.exception("SendGridClient Authentication failed. Verify that your credentials and the 'from")
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
logging.exception(f"SendGridClient Unexpected error occurred while sending email to {_to}")
|
||||||
|
raise
|
@@ -18,4 +18,3 @@ ignore_missing_imports=True
|
|||||||
|
|
||||||
[mypy-flask_restful.inputs]
|
[mypy-flask_restful.inputs]
|
||||||
ignore_missing_imports=True
|
ignore_missing_imports=True
|
||||||
|
|
||||||
|
@@ -81,6 +81,7 @@ dependencies = [
|
|||||||
"weave~=0.51.0",
|
"weave~=0.51.0",
|
||||||
"yarl~=1.18.3",
|
"yarl~=1.18.3",
|
||||||
"webvtt-py~=0.5.1",
|
"webvtt-py~=0.5.1",
|
||||||
|
"sendgrid~=6.12.3",
|
||||||
]
|
]
|
||||||
# Before adding new dependency, consider place it in
|
# Before adding new dependency, consider place it in
|
||||||
# alphabet order (a-z) and suitable group.
|
# alphabet order (a-z) and suitable group.
|
||||||
|
37
api/uv.lock
generated
37
api/uv.lock
generated
@@ -1266,6 +1266,7 @@ dependencies = [
|
|||||||
{ name = "readabilipy" },
|
{ name = "readabilipy" },
|
||||||
{ name = "redis", extra = ["hiredis"] },
|
{ name = "redis", extra = ["hiredis"] },
|
||||||
{ name = "resend" },
|
{ name = "resend" },
|
||||||
|
{ name = "sendgrid" },
|
||||||
{ name = "sentry-sdk", extra = ["flask"] },
|
{ name = "sentry-sdk", extra = ["flask"] },
|
||||||
{ name = "sqlalchemy" },
|
{ name = "sqlalchemy" },
|
||||||
{ name = "starlette" },
|
{ name = "starlette" },
|
||||||
@@ -1442,6 +1443,7 @@ requires-dist = [
|
|||||||
{ name = "readabilipy", specifier = "~=0.3.0" },
|
{ name = "readabilipy", specifier = "~=0.3.0" },
|
||||||
{ name = "redis", extras = ["hiredis"], specifier = "~=6.1.0" },
|
{ name = "redis", extras = ["hiredis"], specifier = "~=6.1.0" },
|
||||||
{ name = "resend", specifier = "~=2.9.0" },
|
{ name = "resend", specifier = "~=2.9.0" },
|
||||||
|
{ name = "sendgrid", specifier = "~=6.12.3" },
|
||||||
{ name = "sentry-sdk", extras = ["flask"], specifier = "~=2.28.0" },
|
{ name = "sentry-sdk", extras = ["flask"], specifier = "~=2.28.0" },
|
||||||
{ name = "sqlalchemy", specifier = "~=2.0.29" },
|
{ name = "sqlalchemy", specifier = "~=2.0.29" },
|
||||||
{ name = "starlette", specifier = "==0.41.0" },
|
{ name = "starlette", specifier = "==0.41.0" },
|
||||||
@@ -1602,6 +1604,18 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/b0/0d/9feae160378a3553fa9a339b0e9c1a048e147a4127210e286ef18b730f03/durationpy-0.10-py3-none-any.whl", hash = "sha256:3b41e1b601234296b4fb368338fdcd3e13e0b4fb5b67345948f4f2bf9868b286", size = 3922 },
|
{ url = "https://files.pythonhosted.org/packages/b0/0d/9feae160378a3553fa9a339b0e9c1a048e147a4127210e286ef18b730f03/durationpy-0.10-py3-none-any.whl", hash = "sha256:3b41e1b601234296b4fb368338fdcd3e13e0b4fb5b67345948f4f2bf9868b286", size = 3922 },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ecdsa"
|
||||||
|
version = "0.19.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "six" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/c0/1f/924e3caae75f471eae4b26bd13b698f6af2c44279f67af317439c2f4c46a/ecdsa-0.19.1.tar.gz", hash = "sha256:478cba7b62555866fcb3bb3fe985e06decbdb68ef55713c4e5ab98c57d508e61", size = 201793, upload-time = "2025-03-13T11:52:43.25Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/cb/a3/460c57f094a4a165c84a1341c373b0a4f5ec6ac244b998d5021aade89b77/ecdsa-0.19.1-py2.py3-none-any.whl", hash = "sha256:30638e27cf77b7e15c4c4cc1973720149e1033827cfd00661ca5c8cc0cdb24c3", size = 150607, upload-time = "2025-03-13T11:52:41.757Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "elastic-transport"
|
name = "elastic-transport"
|
||||||
version = "8.17.1"
|
version = "8.17.1"
|
||||||
@@ -4597,6 +4611,15 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/6a/3e/b68c118422ec867fa7ab88444e1274aa40681c606d59ac27de5a5588f082/python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a", size = 19863 },
|
{ url = "https://files.pythonhosted.org/packages/6a/3e/b68c118422ec867fa7ab88444e1274aa40681c606d59ac27de5a5588f082/python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a", size = 19863 },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "python-http-client"
|
||||||
|
version = "3.3.7"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/56/fa/284e52a8c6dcbe25671f02d217bf2f85660db940088faf18ae7a05e97313/python_http_client-3.3.7.tar.gz", hash = "sha256:bf841ee45262747e00dec7ee9971dfb8c7d83083f5713596488d67739170cea0", size = 9377, upload-time = "2022-03-09T20:23:56.386Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/29/31/9b360138f4e4035ee9dac4fe1132b6437bd05751aaf1db2a2d83dc45db5f/python_http_client-3.3.7-py3-none-any.whl", hash = "sha256:ad371d2bbedc6ea15c26179c6222a78bc9308d272435ddf1d5c84f068f249a36", size = 8352, upload-time = "2022-03-09T20:23:54.862Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "python-iso639"
|
name = "python-iso639"
|
||||||
version = "2025.2.18"
|
version = "2025.2.18"
|
||||||
@@ -5071,6 +5094,20 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/6c/42/cd8dc81f8060de1f14960885ad5b2d2651f41de8b93d09f3f919d6567a5a/scipy_stubs-1.15.3.0-py3-none-any.whl", hash = "sha256:a251254cf4fd6e7fb87c55c1feee92d32ddbc1f542ecdf6a0159cdb81c2fb62d", size = 459062 },
|
{ url = "https://files.pythonhosted.org/packages/6c/42/cd8dc81f8060de1f14960885ad5b2d2651f41de8b93d09f3f919d6567a5a/scipy_stubs-1.15.3.0-py3-none-any.whl", hash = "sha256:a251254cf4fd6e7fb87c55c1feee92d32ddbc1f542ecdf6a0159cdb81c2fb62d", size = 459062 },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sendgrid"
|
||||||
|
version = "6.12.4"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "ecdsa" },
|
||||||
|
{ name = "python-http-client" },
|
||||||
|
{ name = "werkzeug" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/11/31/62e00433878dccf33edf07f8efa417b9030a2464eb3b04bbd797a11b4447/sendgrid-6.12.4.tar.gz", hash = "sha256:9e88b849daf0fa4bdf256c3b5da9f5a3272402c0c2fd6b1928c9de440db0a03d", size = 50271, upload-time = "2025-06-12T10:29:37.213Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c2/9c/45d068fd831a65e6ed1e2ab3233de58784842afdc62fdcdd0a01bbb6b39d/sendgrid-6.12.4-py3-none-any.whl", hash = "sha256:9a211b96241e63bd5b9ed9afcc8608f4bcac426e4a319b3920ab877c8426e92c", size = 102122, upload-time = "2025-06-12T10:29:35.457Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sentry-sdk"
|
name = "sentry-sdk"
|
||||||
version = "2.28.0"
|
version = "2.28.0"
|
||||||
|
@@ -726,10 +726,11 @@ NOTION_INTERNAL_SECRET=
|
|||||||
# Mail related configuration
|
# Mail related configuration
|
||||||
# ------------------------------
|
# ------------------------------
|
||||||
|
|
||||||
# Mail type, support: resend, smtp
|
# Mail type, support: resend, smtp, sendgrid
|
||||||
MAIL_TYPE=resend
|
MAIL_TYPE=resend
|
||||||
|
|
||||||
# Default send from email address, if not specified
|
# Default send from email address, if not specified
|
||||||
|
# If using SendGrid, use the 'from' field for authentication if necessary.
|
||||||
MAIL_DEFAULT_SEND_FROM=
|
MAIL_DEFAULT_SEND_FROM=
|
||||||
|
|
||||||
# API-Key for the Resend email provider, used when MAIL_TYPE is `resend`.
|
# API-Key for the Resend email provider, used when MAIL_TYPE is `resend`.
|
||||||
@@ -745,6 +746,9 @@ SMTP_PASSWORD=
|
|||||||
SMTP_USE_TLS=true
|
SMTP_USE_TLS=true
|
||||||
SMTP_OPPORTUNISTIC_TLS=false
|
SMTP_OPPORTUNISTIC_TLS=false
|
||||||
|
|
||||||
|
# Sendgid configuration
|
||||||
|
SENDGRID_API_KEY=
|
||||||
|
|
||||||
# ------------------------------
|
# ------------------------------
|
||||||
# Others Configuration
|
# Others Configuration
|
||||||
# ------------------------------
|
# ------------------------------
|
||||||
|
@@ -327,6 +327,7 @@ x-shared-env: &shared-api-worker-env
|
|||||||
SMTP_PASSWORD: ${SMTP_PASSWORD:-}
|
SMTP_PASSWORD: ${SMTP_PASSWORD:-}
|
||||||
SMTP_USE_TLS: ${SMTP_USE_TLS:-true}
|
SMTP_USE_TLS: ${SMTP_USE_TLS:-true}
|
||||||
SMTP_OPPORTUNISTIC_TLS: ${SMTP_OPPORTUNISTIC_TLS:-false}
|
SMTP_OPPORTUNISTIC_TLS: ${SMTP_OPPORTUNISTIC_TLS:-false}
|
||||||
|
SENDGRID_API_KEY: ${SENDGRID_API_KEY:-}
|
||||||
INDEXING_MAX_SEGMENTATION_TOKENS_LENGTH: ${INDEXING_MAX_SEGMENTATION_TOKENS_LENGTH:-4000}
|
INDEXING_MAX_SEGMENTATION_TOKENS_LENGTH: ${INDEXING_MAX_SEGMENTATION_TOKENS_LENGTH:-4000}
|
||||||
INVITE_EXPIRY_HOURS: ${INVITE_EXPIRY_HOURS:-72}
|
INVITE_EXPIRY_HOURS: ${INVITE_EXPIRY_HOURS:-72}
|
||||||
RESET_PASSWORD_TOKEN_EXPIRY_MINUTES: ${RESET_PASSWORD_TOKEN_EXPIRY_MINUTES:-5}
|
RESET_PASSWORD_TOKEN_EXPIRY_MINUTES: ${RESET_PASSWORD_TOKEN_EXPIRY_MINUTES:-5}
|
||||||
|
Reference in New Issue
Block a user