通过 Python 和 Microsoft Graph 发送的 S/MIME 电子邮件在 Gmail 中有效,但在 Outlook 中无效

问题描述 投票:0回答:1

我尝试使用 Microsoft Graph 发送在 Python 中创建的 S/MIME 电子邮件。电子邮件已成功发送,Gmail 和我 iPhone 上的 iOS 邮件应用程序都将发件人识别为拥有有效证书的可信发件人。在这些平台上一切看起来都很好。

但是,当我在 Outlook 中打开同一封电子邮件时,遇到了以下问题:

  1. Outlook 显示一条警告,指出证书有问题,内容可能已被更改。
  2. 电子邮件内容已转换为名为 winmail.dat 的附件(内容类型:application/ms-tnef)。
  3. S/MIME 签名未正确显示,电子邮件未通过 DKIM 和 DMARC 检查(dkim=none、dmarc=none)。

这种行为令人费解,因为该电子邮件在其他平台上可以正确识别。 Outlook 处理 S/MIME 电子邮件的具体方式是否存在可能导致此问题的原因?这是否与通过 Graph API 发送电子邮件时的 MIME 编码或格式有关?

python email microsoft-graph-api office365 smime
1个回答
0
投票

如果您希望使用 Python 中的 Microsoft Graph 发送 MIME 内容(如 HTML 和纯文本),请参阅以下指南,了解如何使用 S/MIME 签署 MIME 消息并将其发送到 Microsoft Graph。此代码演示了如何处理 OAuth 身份验证、使用 S/MIME 签署电子邮件以及对其进行编码以实现 Graph API 兼容性。

流程要求:

  1. OAuth 令牌检索:使用基于证书的凭据来 检索访问令牌。
  2. MIME 消息创建:创建 MIME 带有替代纯文本和 HTML 部分的消息。
  3. S/MIME 签名: 使用私钥和证书签署 MIME 消息。
  4. 创建草稿:使用签名的 MIME 内容创建草稿电子邮件 微软图表。

完整代码如下:

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}")

解释

  1. OAuth 令牌生成:函数 __get_graph_oauth_token 使用基于证书的凭据获取 Microsoft Graph 所需的访问令牌。
  2. S/MIME 签名:
    • create_signed_mime_message 函数创建包含纯文本和 HTML 部分的替代 MIME 消息。
    • 然后使用带有 PEM 格式的适当私钥和证书链的 sign_message 对该消息进行签名。
  3. 图表的关键 Base64 编码:
    • 编码行 mime_base64 = base64.encodebytes(signed_mime_message.as_string().encode().replace(b' ',b' ')) 对于确保与 Graph API 的兼容性至关重要。
    • Microsoft Graph 期望使用 base64 格式的 MIME 内容 行结尾,所以这个调整是成功传输的关键。
    • 如果此步骤未正确完成,Outlook 可能无法按预期解释 S/MIME 签名。相反,电子邮件可以转换为 winmail.dat 格式(内容类型:application/ms-tnef),这是 Outlook 的富文本格式。这会导致 S/MIME 签名无法正确显示。此外,身份验证结果可能会显示 dkim=none 和 dmarc=none,这表明 Outlook 未正确解释 S/MIME 签名。

要点

  • S/MIME 签名消息:签名有助于验证发件人的真实性,此代码可确保 MIME 内容得到安全签名。
  • 异步 POST 请求:使用 httpx.AsyncClient 进行高效的非阻塞 HTTP 请求。
  • 错误处理:记录来自 Graph API 的错误响应以进行故障排除。
© www.soinside.com 2019 - 2024. All rights reserved.