Updated 2026-10-08

How to Send Email in Python with smtplib

To send email in Python, build an email.message.EmailMessage, open smtplib.SMTP("smtp.bulko.io", 587), call starttls() with ssl.create_default_context(), log in with your SMTP username and password, and call send_message(). For port 465 use smtplib.SMTP_SSL instead and skip starttls().

Hostsmtp.bulko.io
Port587 · 465 · 2525
EncryptionSTARTTLS · SSL
Loginfrom dashboard

Python SMTP settings

FieldValue
hostsmtp.bulko.io
port587 with SMTP + starttls() (2525 if blocked); 465 with SMTP_SSL
TLSserver.starttls(context=ssl.create_default_context()) on 587/2525
login useros.environ["SMTP_USERNAME"] (e.g. smtp_xxxxxxxxxxxxxxxx)
login passwordos.environ["SMTP_PASSWORD"]
msg["From"]An address on your domain, e.g. Your App <[email protected]>
timeout30 seconds

Step-by-step setup

  1. Buy an SMTP package on the Bulko SMTP Relay page and copy the username and password from Dashboard -> SMTP Access.
  2. Set the environment variables SMTP_USERNAME and SMTP_PASSWORD in the shell, systemd unit, container or CI secret store that runs your script.
  3. Build the message with email.message.EmailMessage: set From, To and Subject headers, call set_content() for the plain text part and optionally add_alternative(..., subtype="html") for HTML. This replaces the older MIMEText/MIMEMultipart approach.
  4. For port 587 open smtplib.SMTP(HOST, 587, timeout=30) and call starttls(context=ssl.create_default_context()) before login(). The default context verifies the server certificate and hostname.
  5. For port 465 use smtplib.SMTP_SSL(HOST, 465, context=context) instead and do not call starttls(), because the connection is already encrypted.
  6. Call login(USER, PASSWORD) and send_message(msg). It takes the envelope sender and recipients from the headers (To, Cc and Bcc) and does not transmit the Bcc header.
  7. Use an address on your own domain in From. Verify the domain under Sending Domains in the Bulko dashboard for aligned SPF, DKIM and DMARC.

Since Python 3.12 the old keyfile and certfile arguments of SMTP_SSL and starttls() are removed, so always pass an ssl.SSLContext through context=. If outbound 587 is blocked, use port 2525 with the same SMTP + starttls() code. See the SMTP ports guide.

send_test.py

# send_test.py  (Python 3.8+, standard library only)
import os
import smtplib
import ssl
from email.message import EmailMessage

HOST = "smtp.bulko.io"
USER = os.environ["SMTP_USERNAME"]
PASSWORD = os.environ["SMTP_PASSWORD"]

msg = EmailMessage()
msg["From"] = "Your App <[email protected]>"
msg["To"] = "[email protected]"
msg["Subject"] = "Python SMTP test"
msg.set_content("It works.")
msg.add_alternative("<p>It works.</p>", subtype="html")

context = ssl.create_default_context()

# Port 587 (or 2525): plain connection, then STARTTLS
with smtplib.SMTP(HOST, 587, timeout=30) as server:
    server.starttls(context=context)
    server.login(USER, PASSWORD)
    server.send_message(msg)

# Port 465: TLS from the first byte
# with smtplib.SMTP_SSL(HOST, 465, context=context, timeout=30) as server:
#     server.login(USER, PASSWORD)
#     server.send_message(msg)

print("Accepted by the relay")

Send a test email

Run SMTP_USERNAME=smtp_xxx SMTP_PASSWORD=yourpass python3 send_test.py with a real recipient in the To header. If the script prints Accepted by the relay without a traceback, the relay accepted the message; check the inbox within a minute or two.

To see the full SMTP conversation, add server.set_debuglevel(1) right after opening the connection. Exceptions such as SMTPAuthenticationError or SMTPDataError carry the server reply code, which you can match with the table below.

Troubleshooting

ProblemFix
smtplib.SMTPAuthenticationError: (535, ...)Wrong username or password, or the environment variables are empty. Copy them again from Dashboard -> SMTP Access. See https://bulko.io/en/smtp-errors/535/
ssl.SSLError: [SSL: WRONG_VERSION_NUMBER]SMTP_SSL was used on port 587. Use smtplib.SMTP with starttls() on 587, or SMTP_SSL on 465.
SMTPServerDisconnected or a hang on port 465Plain smtplib.SMTP was used on 465, which expects TLS immediately. Switch to SMTP_SSL for 465.
SMTPNotSupportedError: SMTP AUTH extension not supportedlogin() was called before starttls() on port 587. Call starttls() first, then login().
TimeoutError or ConnectionRefusedErrorYour host blocks outbound SMTP ports. Try port 2525 with SMTP + starttls(), and keep timeout set.
smtplib.SMTPDataError: (554, b'5.7.1 ...')The AI content filter rejected the message for phishing, malware, impersonation or scam patterns. See https://bulko.io/en/acceptable-use/

FAQ

Should I use SMTP with starttls() or SMTP_SSL?

Use smtplib.SMTP with starttls() on port 587 or 2525, and smtplib.SMTP_SSL on port 465. Both encrypt the session; they differ only in when TLS starts.

Do I need to call ehlo() manually?

Not usually. starttls() and login() send EHLO automatically when needed. Calling ehlo() yourself is harmless.

Can I read replies with Python and this server?

No. Bulko SMTP is outgoing only and has no IMAP. Replies go to the From or Reply-To mailbox, which you can read with imaplib against your own mail provider.

How many emails can a script send per day?

The default is 2,000 emails per day per SMTP account. Reuse one connection for a batch and contact [email protected] if you need a higher limit.

How much does it cost?

One-time packages: $5 for 5,000 emails, $15 for 20,000, $30 for 50,000, $60 for 150,000. Unused emails never expire, and there is no free SMTP trial.

Get SMTP credentials for Python

One-time payment by card or crypto, no subscription. Unused emails never expire. 2,000 emails per day per SMTP account.

SMTP Relay 5K: $5 · 5000SMTP Relay 20K: $15 · 20000SMTP Relay 50K: $30 · 50000SMTP Relay 150K: $60 · 150000
See SMTP relay plans

What you can send: Acceptable Use Policy.