The quickest way to create an Outlook email from Python is the win32com library, which drives the desktop Outlook app on Windows, but it only works where Outlook is installed, so server-side automation needs SMTP or the Microsoft Graph API instead.
Which route is right depends on one question: does the code run on a Windows desktop with Outlook, or on a server with no Outlook at all.
Table of contents
- Three ways to send mail from Python
- Create the email with win32com
- The new Outlook problem
- Send with SMTP instead
- Use the Microsoft Graph API
- Running it on a schedule
- Personalising and sending in bulk safely
- How this fits the rest of the stack
- FAQ
Three ways to send mail from Python
There are three realistic approaches, and they differ in where they can run.
- win32com and the Outlook desktop app. Python controls an Outlook installation on the same machine through COM automation. It creates a message in the user’s own mailbox, optionally displays it for review, and sends it through the user’s account. It requires Windows, an installed and configured classic Outlook and, usually, a logged-in desktop session.
- SMTP with smtplib. Python talks directly to a mail server. It runs anywhere, needs credentials, and does not need Outlook at all.
- The Microsoft Graph API. A modern HTTP API to Microsoft 365 mailboxes. It runs anywhere, supports app registrations with scoped permissions, and is the direction Microsoft is pushing.
The first is great for personal productivity scripts and one-off tasks: draft twenty personalised emails and let a human review each before sending. The second and third are for applications and scheduled jobs.
Most search results and Stack Overflow answers describe the first, because it is the simplest to start with. That is why the caveat in the next section matters, and why a script that works on your laptop often fails the moment you try to schedule it on a server.
Create the email with win32com
Install the package and write a short script.
pip install pywin32
import win32com.client
outlook = win32com.client.Dispatch("Outlook.Application")
mail = outlook.CreateItem(0) # 0 is a mail item
mail.To = "[email protected]"
mail.CC = "[email protected]"
mail.Subject = "Weekly report"
mail.HTMLBody = "<p>Hi Jane,</p><p>The weekly report is attached.</p>"
mail.Attachments.Add(r"C:\reports\weekly.pdf")
mail.Display() # opens the draft for review
# mail.Send() # sends immediately
The CreateItem(0) call returns a new mail item. You set recipients, subject and body, add attachments by absolute path and then either display it or send it. Display is safer while you are developing, because you can see exactly what would go out.
Use HTMLBody for formatting and Body for plain text. Separate multiple recipients with semicolons. To send from a specific account when the mailbox has several, set the SendUsingAccount property to one of the accounts in the session.
Calling Send triggers real mail immediately. Wrap it in a flag so test runs cannot email customers by accident.
The new Outlook problem
The big gotcha is that this approach drives classic Outlook. Microsoft has been replacing it with a new Outlook that is a web-based client, and COM automation does not work with it. Community threads are full of people whose scripts stopped working after switching, with errors when dispatching the application object.
If your script fails at the Dispatch line, check which Outlook you are running. The new client has a toggle in the corner of the window. Switching back to classic Outlook restores COM support for now, but treat that as temporary, because the long-term direction is away from COM.
Other limits of the desktop route are worth knowing:
- It needs a desktop session. Running as a scheduled task with no logged-in user is unreliable.
- Security prompts may appear when a program accesses address data or sends mail, depending on antivirus and policy.
- It cannot run on Linux, macOS or most containers, so it is not suitable for hosting.
- It sends as the logged-in user, so everything is tied to a person’s mailbox.
If your script is a convenience for one person’s laptop, those are acceptable. If it is meant to be a service, plan the move to SMTP or Graph from the start.
Send with SMTP instead
For server-side use, SMTP is the plain, portable option. The standard library handles it.
import os
import smtplib
from email.message import EmailMessage
msg = EmailMessage()
msg["From"] = "[email protected]"
msg["To"] = "[email protected]"
msg["Subject"] = "Weekly report"
msg.set_content("Hi Jane, the weekly report is attached.")
msg.add_alternative("<p>Hi Jane,</p><p>The report is attached.</p>", subtype="html")
with open("weekly.pdf", "rb") as f:
msg.add_attachment(f.read(), maintype="application", subtype="pdf", filename="weekly.pdf")
with smtplib.SMTP(os.environ["SMTP_HOST"], 587) as server:
server.starttls()
server.login(os.environ["SMTP_USER"], os.environ["SMTP_PASSWORD"])
server.send_message(msg)
Notice that credentials come from environment variables, never from the source. Use port 587 with STARTTLS or port 465 with implicit TLS, never plain port 25 for authenticated mail.
Many mail providers have turned off basic username-and-password authentication for SMTP in favour of modern authentication, so check what your provider allows. A dedicated transactional email service with an API key is often more reliable for application mail than a personal mailbox.
Use the Microsoft Graph API
For Microsoft 365 mailboxes, Graph is the modern route. You register an application in your tenant, grant it a mail-sending permission, obtain a token and call an HTTP endpoint. Python handles it with the requests library plus a token library such as msal.
import os
import msal
import requests
app = msal.ConfidentialClientApplication(
os.environ["CLIENT_ID"],
authority=f"https://login.microsoftonline.com/{os.environ['TENANT_ID']}",
client_credential=os.environ["CLIENT_SECRET"],
)
token = app.acquire_token_for_client(scopes=["https://graph.microsoft.com/.default"])
payload = {
"message": {
"subject": "Weekly report",
"body": {"contentType": "HTML", "content": "<p>Report attached.</p>"},
"toRecipients": [{"emailAddress": {"address": "[email protected]"}}],
}
}
requests.post(
"https://graph.microsoft.com/v1.0/users/[email protected]/sendMail",
headers={"Authorization": "Bearer " + token["access_token"]},
json=payload,
timeout=30,
)
The permission model is stricter and more auditable than desktop automation. An administrator grants exactly the rights the app needs, and you can limit it to a specific mailbox. Handle token expiry, rate limits and error responses, since the API will tell you why a send failed.
The setup takes longer than win32com, and it is the version that survives being moved to a server.
Running it on a schedule
Once the script is more than a one-off, decide where it runs. A laptop that must be on and logged in is a fragile host. A server job is better.
Package the script with a requirements file, read all configuration from environment variables, log what it sends, and exit with a non-zero code on failure so a scheduler notices. Make it idempotent, so a retry does not email people twice. A small record of sent message identifiers is enough.
On RunxBuild, a Python service deploys from a GitHub repository with environment variables set in the dashboard, runtime logs, and rollback to a previous deploy. An SMTP or Graph based sender fits there naturally, and the COM approach does not, since the service is not a Windows desktop. For simple schedules such as a nightly digest, a lightweight loop or an n8n workflow on a managed plan can be easier than a custom service. The Python services docs list the runtime options, and the walkthrough on deploying a Python API shows the repository steps.
Personalising and sending in bulk safely
A common reason to script email is sending many personalised messages, such as invoices or reminders. Build the messages from a template and a data source, and keep the two apart.
from string import Template
template = Template("Hi $name, your invoice $number for $amount is due on $due.")
for row in rows:
body = template.substitute(row)
# create and display or send the message here
Add brakes. Cap how many messages one run can send, sleep briefly between them so you stay under provider rate limits, and write a log line for each recipient. If a run is interrupted, the log tells you where to resume without double-sending.
Respect the rules for bulk mail: only email people who expect it, include a way to opt out for anything promotional, and keep unsubscribes honoured. Personal mailboxes have daily sending limits, and exceeding them can get an account blocked, which is a poor way to learn that a transactional email service exists for exactly this job.
How this fits the rest of the stack
Email automation is cheap per message and the host that runs it is a fixed monthly cost, so size the small service before you build it. Model a Python service and its logs in the RunxBuild hosting calculator. When the script is ready to leave your laptop, the RunxBuild dashboard is where you deploy it.
Useful related references:
- Python Not Equal: != vs is not, and Why the Difference Bites
- Python Integer Division: Why // Floors and Why -7 // 2 Is -4
- Python for Websites: Where It Fits and Where It Does Not
- Python services on RunxBuild
FAQ
How do I create an Outlook email in Python?
On Windows with classic Outlook installed, use win32com: dispatch Outlook.Application, call CreateItem(0), set To, Subject and Body, then call Display or Send. For servers use SMTP or the Microsoft Graph API.
Why does win32com fail with the new Outlook?
COM automation works with classic Outlook, not the new web-based client. Switch back to classic Outlook or move your script to SMTP or the Graph API.
Can I run win32com on a server or in a container?
Not realistically. It needs Windows, an installed Outlook and usually a logged-in desktop session, so it is unsuitable for Linux hosting or unattended jobs.
Should I use SMTP or Microsoft Graph?
Use SMTP for simple sending where your provider allows it, and Graph for Microsoft 365 mailboxes where you want scoped, auditable permissions and modern authentication.
How do I avoid sending test emails to real people?
Use Display instead of Send while developing, add a dry-run flag, and send to a test address until the output is correct.