HOWTO · Python
How to Send Emails to Multiple Addresses Using Python
This article demonstrates how to send emails to multiple addresses using Python.
On this page
Sending the same message to several addresses is a standard-library job in Python: smtplib manages the SMTP session, and EmailMessage from email.message holds the headers and body. Put every address a recipient should see in the To or Cc header, put hidden addresses in Bcc, and call send_message once — the SMTP server takes care of delivering a copy to each mailbox.
How Multiple Email Recipients Work
An email carries two separate recipient lists, and keeping them straight explains most multi-recipient surprises:
- The envelope recipients are the addresses your client hands to the SMTP server in
RCPT TOcommands. Delivery is driven entirely by this list. - The header recipients are the
To,Cc, andBcclines inside the message. They are display hints for mail clients; the server does not route on them.
To and Cc behave the same way for delivery — every recipient can see both lists. Bcc recipients are delivered like anyone else, but send_message removes the Bcc header before transmitting, so nobody ever sees who was blind-copied.
When you call server.send_message(message), smtplib reads the To, Cc, and Bcc headers and sends one RCPT TO per address. You can also pass an explicit list as to_addrs, which overrides the headers for delivery — useful when the visible To line should differ from the actual delivery list, such as for a hidden distribution list.
Every address receives a byte-identical copy of the message. If each person needs different content, build and send a separate EmailMessage per recipient; the last section covers that pattern.
Choose an SMTP Server and Secure Authentication
To reach a real mail provider you need three things: the provider’s SMTP hostname and port, an account name, and a credential. The provider’s documentation is the authority — requirements change, so confirm the current values rather than trusting a copied snippet.
Two encrypted connection styles are common:
- Implicit TLS from the first byte, typically on port
465: usesmtplib.SMTP_SSL(host, port). - A plain connection upgraded with STARTTLS, typically on port
587: usesmtplib.SMTP(host, port), then callserver.starttls()beforeserver.login(...).
Many providers no longer accept the normal account password over SMTP. They issue an app password, an API key used as the password, or require OAuth2 — generate whichever the provider supports. Store the credential outside the source code; environment variables, as the second example below does, are the simplest starting point. Never commit real credentials to a repository.
Build an EmailMessage for Multiple Recipients
EmailMessage behaves like an ordered dictionary of headers plus a body. Set the headers first:
message["From"]— the sender. Most providers require it to match the authenticated account or a verified alias; a mismatch is a frequent cause of rejection.message["To"]— a comma-separated string. Join a Python list with", ".join(recipients)so the program stays easy to extend.message["Cc"]andmessage["Bcc"]— optional, in the same comma-separated format.message["Subject"]— the subject line.
message.set_content("...") fills in a plain-text body and sets Content-Type: text/plain for you. For an HTML version, call message.add_alternative(html, subtype="html") after set_content so clients that prefer text keep a fallback. message.add_attachment(...) adds files such as PDFs or images.
Send the Message with smtplib
send_message transmits the built message once and lets the server fan it out to every envelope recipient. Used as a context manager, the SMTP object closes the session cleanly with QUIT when the with block ends.
Run this complete script. It starts a small SMTP test server on localhost, sends one EmailMessage to a To pair plus Cc and Bcc addresses through smtplib, and prints every envelope recipient the server recorded:
import smtplib
import socket
import threading
from email.message import EmailMessage
recipients_seen = []
def run_server(sock):
conn, _ = sock.accept()
conn.sendall(b"220 localhost ESMTP test server\r\n")
data_mode = False
buffer = b""
while True:
chunk = conn.recv(1024)
if not chunk:
return
buffer += chunk
while b"\r\n" in buffer:
line, buffer = buffer.split(b"\r\n", 1)
if data_mode:
if line == b".":
data_mode = False
conn.sendall(b"250 Message stored\r\n")
continue
command = line.decode().split(" ", 1)[0].upper()
if command == "RCPT":
address = line.decode().rsplit(":", 1)[-1].strip().strip("<>")
recipients_seen.append(address)
conn.sendall(b"250 OK\r\n")
elif command == "DATA":
data_mode = True
conn.sendall(b"354 End data with <CR><LF>.<CR><LF>\r\n")
elif command == "QUIT":
conn.sendall(b"221 Bye\r\n")
return
else:
conn.sendall(b"250 OK\r\n")
listener = socket.socket()
listener.bind(("127.0.0.1", 0))
listener.listen(1)
port = listener.getsockname()[1]
thread = threading.Thread(target=run_server, args=(listener,))
thread.start()
message = EmailMessage()
message["Subject"] = "Team update"
message["From"] = "sender@example.com"
message["To"] = "alice@example.com, bob@example.com"
message["Cc"] = "carol@example.com"
message["Bcc"] = "dave@example.com"
message.set_content("Here is this week's team update.")
with smtplib.SMTP("127.0.0.1", port) as server:
server.send_message(message)
thread.join()
listener.close()
print("Delivered to these envelope recipients:")
for recipient in recipients_seen:
print(recipient)
Delivered to these envelope recipients:
alice@example.com
bob@example.com
dave@example.com
carol@example.com
The test server logged four RCPT TO addresses — one per header address — including dave@example.com, whose Bcc header is stripped from the transmitted copy yet still drives delivery. That is the envelope-versus-header split in action. The local test server speaks plain SMTP, which is why this script uses smtplib.SMTP without encryption.
To send through a real provider, adapt the same message like this. The script reads the connection details from environment variables and reminds you to set them when they are missing:
import os
import smtplib
from email.message import EmailMessage
SMTP_HOST = os.environ.get("SMTP_HOST", "")
SMTP_PORT = int(os.environ.get("SMTP_PORT", "465"))
SMTP_USER = os.environ.get("SMTP_USER", "")
SMTP_PASSWORD = os.environ.get("SMTP_PASSWORD", "")
recipients = [
"alice@example.com",
"bob@example.com",
"carol@example.com",
]
message = EmailMessage()
message["Subject"] = "Team update"
message["From"] = SMTP_USER
message["To"] = ", ".join(recipients)
message.set_content("Here is this week's team update.")
if SMTP_HOST and SMTP_USER and SMTP_PASSWORD:
with smtplib.SMTP_SSL(SMTP_HOST, SMTP_PORT) as server:
server.login(SMTP_USER, SMTP_PASSWORD)
refused = server.send_message(message)
print("Accepted for all recipients" if not refused else f"Refused: {refused}")
else:
print("Set SMTP_HOST, SMTP_PORT, SMTP_USER, and SMTP_PASSWORD to send this message.")
Set SMTP_HOST, SMTP_PORT, SMTP_USER, and SMTP_PASSWORD to send this message.
With the variables set and a reachable provider, the same script logs in with server.login, sends through SMTP_SSL, and prints Accepted for all recipients.
Handle Delivery Errors and Adapt the Example
Failures cluster in a few predictable places:
- Authentication —
server.loginraisessmtplib.SMTPAuthenticationErrorwhen the credential is wrong or when the provider demands an app password or OAuth2 instead of the account password. - Connection — a wrong hostname raises
socket.gaierror; a wrong port or a blocked route surfaces assmtplib.SMTPConnectErroror a timeout. Check that the port matches the encryption style:SMTP_SSLfor465,SMTPplusstarttls()for587. - Recipient rejection — if the server refuses every address,
send_messageraisessmtplib.SMTPRecipientsRefused. If only some are refused, it still sends to the rest and returns a dictionary mapping each refused address to its SMTP status code and message — inspect that return value instead of assuming silence means success.
An SMTP 250 response means the server accepted the message for relaying, not that it landed in an inbox. Providers can still filter, quarantine, or bounce it, so watch the sender mailbox for bounce notices during real sends.
To adapt the example, replace the recipients list with addresses loaded from a file, a database, or an environment variable. For personalized content, keep the connection open in a single with block and loop over send_message with one EmailMessage per person — reusing the session is much faster than reconnecting. For large announcements, Bcc keeps the addresses private, but watch provider rate limits; very large lists belong on a dedicated sending service.