Python Central HubSign in

Automation and Scripting / Phase 4: Communication Automation

Sending Emails with smtplib

Send plain text emails using Python’s built-in smtplib safely.

Security note

Don’t hardcode passwords.

Prefer:

  • app passwords
  • environment variables
  • secret managers

Plain text email

Python
import os
import smtplib
from email.message import EmailMessage
 
SMTP_HOST = "smtp.gmail.com"
SMTP_PORT = 587
 
 
def send_email(to: str, subject: str, body: str):
    user = os.environ["SMTP_USER"]
    password = os.environ["SMTP_PASS"]
 
    msg = EmailMessage()
    msg["From"] = user
    msg["To"] = to
    msg["Subject"] = subject
    msg.set_content(body)
 
    with smtplib.SMTP(SMTP_HOST, SMTP_PORT) as smtp:
        smtp.starttls()
        smtp.login(user, password)
        smtp.send_message(msg)
 
 
# send_email("to@example.com", "Hello", "Test")

Visualize it

Sending an email with smtplib follows a straightforward compose-connect-send sequence.

diagram Email sending flow mermaid
Steps smtplib takes to compose and deliver an email

How the pieces fit

Sending email from Python uses two standard-library modules.

  • email.message.EmailMessage builds the message: headers (From, To, Subject) and the body. It handles encoding for you, including non-ASCII characters in the subject and body.
  • smtplib delivers the message by talking to a mail server using SMTP.

The steps are always the same: connect to the server, upgrade to an encrypted connection, log in, send, and disconnect. The with statement closes the connection for you.

Ports and encryption

  • Port 587 starts as plain text and upgrades with starttls(). This is what the example uses.
  • Port 465 is encrypted from the start. Use smtplib.SMTP_SSL(host, 465) and skip starttls().
  • Port 25 is for server-to-server mail. Most providers block it for clients.

Never send a password over an unencrypted connection. Always call starttls() before login().

Passwords and Gmail

Do not put your password in the script. Read it from an environment variable, as the example does. Set them in your shell (export SMTP_PASS=... on Linux and macOS, setx SMTP_PASS "..." on Windows), or load them from a .env file that is listed in .gitignore.

Gmail does not accept your normal account password for scripts. Turn on 2-step verification, create an app password, and use that. Other providers, such as Outlook.com, SendGrid, and Mailgun, have their own SMTP settings, and many now require OAuth or an API key instead. Check the provider's documentation for the host, port, and authentication method.

A sender you can test

This version separates building the message from sending it. You can check the message without a server, and send to several people.

Python
import os
import smtplib
from email.message import EmailMessage
 
 
def build_message(sender: str, to: list[str], subject: str, body: str) -> EmailMessage:
    msg = EmailMessage()
    msg["From"] = sender
    msg["To"] = ", ".join(to)
    msg["Subject"] = subject
    msg.set_content(body)
    return msg
 
 
def send(msg: EmailMessage, dry_run: bool = True) -> None:
    if dry_run:
        print(msg)                        # shows headers and body, sends nothing
        return
    with smtplib.SMTP("smtp.gmail.com", 587, timeout=20) as smtp:
        smtp.starttls()
        smtp.login(os.environ["SMTP_USER"], os.environ["SMTP_PASS"])
        smtp.send_message(msg)
 
 
message = build_message("me@example.com", ["you@example.com"], "Hello", "Test message")
send(message, dry_run=True)

dry_run=True is the default, so a mistake never sends mail. Turn it off only after you have read the printed message. Pass timeout so a stalled server does not hang the script.

Safety

Email cannot be recalled. Test with your own address first. If a loop builds messages for many people, limit it while testing, and send at a modest speed: providers cap daily volume and will suspend an account that looks like spam. Do not send bulk unsolicited mail.

Common mistakes

  • Hardcoding the password.
  • Forgetting starttls(), so login fails or sends the password in clear text.
  • Using the account password with Gmail instead of an app password.
  • Building headers by string concatenation. A newline in user-supplied text can inject extra headers. EmailMessage rejects that, so use it.
  • No timeout.
  • No error handling. Catch smtplib.SMTPAuthenticationError and smtplib.SMTPException, and log what failed.

🧪 Try It Yourself

Exercise 1 – List Files with os.listdir

exerciseYour turn

Tab indents. Press Escape, then Tab, to leave the editor. Control plus Enter runs the code; Control plus Shift plus Enter submits it.

Output
Press Run to see what your code prints.

Exercise 2 – Join Paths with os.path.join

exerciseYour turn

Tab indents. Press Escape, then Tab, to leave the editor. Control plus Enter runs the code; Control plus Shift plus Enter submits it.

Output
Press Run to see what your code prints.

Exercise 3 – Write and Read a File

exerciseYour turn

Tab indents. Press Escape, then Tab, to leave the editor. Control plus Enter runs the code; Control plus Shift plus Enter submits it.

Output
Press Run to see what your code prints.

Was this page helpful?

Let us know how we did