Why the old tutorials stopped working
The classic recipe, smtplib against smtp.office365.com with a username and password, died when Microsoft switched 365 tenants off basic authentication. The replacement is OAuth 2.0 token flows against Microsoft Graph or SMTP with modern auth: app registrations, client secrets, tenant IDs, token refresh.
That is a lot of Azure ceremony to send a notification.
Scope ceremony also fails in ways that look like bad credentials. George, our CEO, debugged exactly this in our own provider integration: a connection test probed an endpoint that needs SendGrid's Sender Authentication scope, so a valid least-privilege Mail-Send key returned 403 and was reported invalid. The fix was probing an endpoint any valid key can read. When an OAuth or API-key setup rejects a key you know is right, check what the probe endpoint demands before you rotate anything.
Let your code stop caring about Microsoft's auth
The email API path removes the moving part: your Python authenticates to one stable endpoint with a bearer token, and the platform's infrastructure handles delivery to Outlook, Gmail, and everyone else.
requests.post( "https://api.nitrosend.com/v1/my/messages", headers={"Authorization": "Bearer $NITROSEND_API_KEY"}, json={"from": "[email protected]", "to": "[email protected]", "subject": "Your report", "html": "<p>Attached below.</p>"}, )
The bearer token is a server-side API key. It lives in the environment where the script runs, never in anything you distribute.
That platform infrastructure is strict by design. On our forwarding relay, TCP 2525 is firewall-restricted to our API host, TLS and exact SMTP authentication are mandatory, and every single-recipient route and MX snapshot is HMAC-signed, with unsigned or replayed requests rejected. Microsoft's filters reward exactly that discipline.
Reaching Outlook inboxes then becomes a deliverability question, SPF, DKIM, DMARC on your domain, which the platform configures for you.
And it's worth respecting the scale of the gatekeepers. Apple, Alphabet, and Microsoft now control 77 percent of the world's inbox market between them. You don't out-clever three trillion-dollar filters. You authenticate properly and send mail people want.
Switching providers also buys you a short adjustment window while Outlook and Gmail get used to your mail arriving from a new place, so judge deliverability after the warm-up, not on day one. One more count worth knowing: for all of Outlook's enterprise presence, Microsoft 365 mailboxes are only around 5 percent of where senders' audiences actually live. Gmail dominates. Weight your testing accordingly.
Full parameters and responses live in the REST API docs and the API reference.
Go deeper
The Azure ceremony above is real, but it is not the deeper point. The honest fix for "how do I send email through Outlook in Python" is skipping the token refresh entirely. Describe the send to Claude, Cursor, or Codex and our MCP server handles it on the same account, no app registration and no client secret to rotate.
Gmail version of the same trap: Python via Gmail. The full language guide: send email with Python. Outlook applies its own filtering, so SPF, DKIM and DMARC authentication matters here, and inbox placement is how you tell whether it worked.
FAQ
How do I send email via Outlook in Python?
Modern Microsoft 365 requires OAuth 2.0 (Graph API or SMTP with modern auth): app registration, client secret, token refresh. For product email, an email API with bearer-token auth is one stable endpoint that doesn't chase Microsoft's auth changes.
Why did my smtplib Outlook script break?
Microsoft disabled basic authentication for Microsoft 365 SMTP. Scripts using plain username/password stopped working; the fixes are OAuth token flows or sending through an email platform instead.