I was trying to send an S/MIME email created in Python using Microsoft Graph. The email was sent successfully, and both Gmail and the iOS Mail app on my iPhone recognized the sender as trusted with a valid certificate. Everything seemed fine on those platforms.
However, when I opened the same email in Outlook, I encountered the following issues:
This behavior is puzzling since the email is properly recognized on other platforms. Is there something specific about the way Outlook processes S/MIME emails that could cause this issue? Could this be related to the MIME encoding or formatting when sending the email via Graph API?
If you're looking to send MIME content (like HTML and plain text) with Microsoft Graph in Python, here’s a guide on how to sign the MIME message with S/MIME and send it to Microsoft Graph. This code demonstrates how to handle OAuth authentication, sign the email using S/MIME, and encode it for Graph API compatibility.
The process requires:
Here's the full code:
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}")
Explanation
Key Points