我尝试使用 Microsoft Graph 发送在 Python 中创建的 S/MIME 电子邮件。电子邮件已成功发送,Gmail 和我 iPhone 上的 iOS 邮件应用程序都将发件人识别为拥有有效证书的可信发件人。在这些平台上一切看起来都很好。
但是,当我在 Outlook 中打开同一封电子邮件时,遇到了以下问题:
这种行为令人费解,因为该电子邮件在其他平台上可以正确识别。 Outlook 处理 S/MIME 电子邮件的具体方式是否存在可能导致此问题的原因?这是否与通过 Graph API 发送电子邮件时的 MIME 编码或格式有关?
如果您希望使用 Python 中的 Microsoft Graph 发送 MIME 内容(如 HTML 和纯文本),请参阅以下指南,了解如何使用 S/MIME 签署 MIME 消息并将其发送到 Microsoft Graph。此代码演示了如何处理 OAuth 身份验证、使用 S/MIME 签署电子邮件以及对其进行编码以实现 Graph API 兼容性。
流程要求:
完整代码如下:
from azure.identity import CertificateCredential
from azure.core.credentials import AccessToken
import base64
import httpx
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from smail import sign_message
# Step 1: Retrieve OAuth token using Certificate-based credential
def __get_graph_oauth_token(self) -> str:
credential: CertificateCredential = self.__get_graph_credential()
token: AccessToken = credential.get_token(self.scope)
return token.token
def __get_graph_credential(self) -> CertificateCredential:
crypto_utils: CryptoUtils = CryptoUtils()
certificate_data: bytes = crypto_utils.get_pfx_certificate(cert_name=self.domain_cert_name)
return CertificateCredential(
tenant_id=self.tenant_id,
client_id=self.client_id,
certificate_data=certificate_data
)
# Step 2: Create MIME message with S/MIME signing
def create_signed_mime_message(self, subject: str, body: str, to_email: str, from_email: str):
# Retrieve the private key, main certificate, and additional certs in PEM format
private_key_pem, main_certificate_pem, additional_certs_pem = self.__get_auth_priv_rep_certificate_chain()
# Step 2.1: Create inner alternative part for plain text and HTML
inner_alternative = MIMEMultipart("alternative")
inner_alternative.attach(MIMEText(body, "plain", "utf-8"))
inner_alternative.attach(MIMEText(body, "html", "utf-8"))
inner_alternative["From"] = from_email
inner_alternative["To"] = to_email
inner_alternative["Subject"] = subject
# Step 2.2: Sign the message with temporary key and cert files
signed_msg = sign_message(
message=inner_alternative,
key_signer=private_key_pem,
cert_signer=main_certificate_pem,
additional_certs=[additional_certs_pem]
)
return signed_msg
# Step 3: Create a draft email with MIME content using Microsoft Graph
async def create_draft_email_with_mime(self, subject: str, body: str, to_email: str):
access_token: str = self.__get_graph_oauth_token()
url: str = f"https://graph.microsoft.com/v1.0/users/{self.from_email}/messages"
headers = {
"Authorization": f"Bearer {access_token}",
"Content-Type": "text/plain",
"Prefer": 'IdType="ImmutableId"'
}
# Step 3.1: Create and sign MIME message
signed_mime_message = self.create_signed_mime_message(
subject=subject,
body=body,
to_email=to_email,
from_email=self.from_email
)
# **CRUCIAL STEP**: Encode the MIME message in base64 and replace line breaks to ensure compatibility with Graph API
mime_base64 = base64.encodebytes(signed_mime_message.as_string().encode().replace(b'\n', b'\r\n'))
# Step 3.3: Send the signed MIME content as draft to Microsoft Graph
async with httpx.AsyncClient() as client:
response = await client.post(url, headers=headers, content=mime_base64)
if response.status_code == 201:
return response.json()
else:
self.logger.error(f"Error creating draft! Status: {response.status_code}. Response: {response.text}")
解释
要点