Files
dify/api/libs/mail/oauth_http_client.py
-LAN- 69f712b713 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.
2025-09-23 01:30:06 +08:00

46 lines
1.6 KiB
Python

"""HTTP client abstraction for OAuth requests"""
from abc import ABC, abstractmethod
from typing import Optional, Union
import requests
class OAuthHTTPClientProtocol(ABC):
"""Abstract interface for OAuth HTTP operations"""
@abstractmethod
def post(
self, url: str, data: dict[str, Union[str, int]], headers: Optional[dict[str, str]] = None
) -> dict[str, Union[str, int, dict, list]]:
"""Make a POST request"""
pass
@abstractmethod
def get(self, url: str, headers: Optional[dict[str, str]] = None) -> dict[str, Union[str, int, dict, list]]:
"""Make a GET request"""
pass
class OAuthHTTPClient(OAuthHTTPClientProtocol):
"""Default implementation using requests library"""
def post(
self, url: str, data: dict[str, Union[str, int]], headers: Optional[dict[str, str]] = None
) -> dict[str, Union[str, int, dict, list]]:
"""Make a POST request"""
response = requests.post(url, data=data, headers=headers or {})
return {
"status_code": response.status_code,
"json": response.json() if response.headers.get("content-type", "").startswith("application/json") else {},
"text": response.text,
"headers": dict(response.headers),
}
def get(self, url: str, headers: Optional[dict[str, str]] = None) -> dict[str, Union[str, int, dict, list]]:
"""Make a GET request"""
response = requests.get(url, headers=headers or {})
response.raise_for_status()
json_data = response.json()
return dict(json_data)