feat: Add SMTP OAuth 2.0 support for Microsoft Exchange

Add comprehensive OAuth 2.0 authentication support for SMTP to address
Microsoft's Basic Authentication retirement in September 2025.

Key features:
- OAuth 2.0 SASL XOAUTH2 authentication mechanism
- Microsoft Azure AD integration with client credentials flow
- Backward compatible with existing basic authentication
- Comprehensive configuration options in .env.example files
- Enhanced SMTP client with dependency injection for better testability
- Complete test coverage with proper mocking

Configuration:
- SMTP_AUTH_TYPE: Choose between 'basic' and 'oauth2' authentication
- Microsoft OAuth 2.0 settings for Azure AD integration
- Automatic token acquisition using client credentials flow

Files changed:
- Enhanced SMTP client with OAuth 2.0 support
- New mail module structure under libs/mail/
- Updated configuration system with OAuth settings
- Comprehensive documentation and setup instructions
- Complete test suite for OAuth functionality

This change ensures compatibility with Microsoft Exchange Online
after Basic Authentication retirement.
This commit is contained in:
-LAN-
2025-07-22 03:00:16 +08:00
parent f4522fd695
commit 69f712b713
13 changed files with 1593 additions and 62 deletions

View File

@ -16,7 +16,7 @@ class Mail:
def is_inited(self) -> bool:
return self._client is not None
def init_app(self, app: Flask):
def init_app(self, _: Flask):
mail_type = dify_config.MAIL_TYPE
if not mail_type:
logger.warning("MAIL_TYPE is not set")
@ -40,20 +40,36 @@ class Mail:
resend.api_key = api_key
self._client = resend.Emails
case "smtp":
from libs.smtp import SMTPClient
from libs.mail import SMTPClient
if not dify_config.SMTP_SERVER or not dify_config.SMTP_PORT:
raise ValueError("SMTP_SERVER and SMTP_PORT are required for smtp mail type")
if not dify_config.SMTP_USE_TLS and dify_config.SMTP_OPPORTUNISTIC_TLS:
raise ValueError("SMTP_OPPORTUNISTIC_TLS is not supported without enabling SMTP_USE_TLS")
# Validate OAuth 2.0 configuration if auth_type is oauth2
oauth_access_token = None
if dify_config.SMTP_AUTH_TYPE == "oauth2":
oauth_access_token = dify_config.MICROSOFT_OAUTH2_ACCESS_TOKEN
if not oauth_access_token:
# Try to get token using client credentials flow
if dify_config.MICROSOFT_OAUTH2_CLIENT_ID and dify_config.MICROSOFT_OAUTH2_CLIENT_SECRET:
oauth_access_token = self._get_oauth_token()
if not oauth_access_token:
raise ValueError("OAuth 2.0 access token is required for oauth2 auth_type")
self._client = SMTPClient(
server=dify_config.SMTP_SERVER,
port=dify_config.SMTP_PORT,
username=dify_config.SMTP_USERNAME or "",
password=dify_config.SMTP_PASSWORD or "",
_from=dify_config.MAIL_DEFAULT_SEND_FROM or "",
from_addr=dify_config.MAIL_DEFAULT_SEND_FROM or "",
use_tls=dify_config.SMTP_USE_TLS,
opportunistic_tls=dify_config.SMTP_OPPORTUNISTIC_TLS,
oauth_access_token=oauth_access_token,
auth_type=dify_config.SMTP_AUTH_TYPE,
)
case "sendgrid":
from libs.sendgrid import SendGridClient