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().
Python SMTP settings
| Field | Value |
|---|---|
| host | smtp.bulko.io |
| port | 587 with SMTP + starttls() (2525 if blocked); 465 with SMTP_SSL |
| TLS | server.starttls(context=ssl.create_default_context()) on 587/2525 |
| login user | os.environ["SMTP_USERNAME"] (e.g. smtp_xxxxxxxxxxxxxxxx) |
| login password | os.environ["SMTP_PASSWORD"] |
| msg["From"] | An address on your domain, e.g. Your App <[email protected]> |
| timeout | 30 seconds |
Step-by-step setup
- Buy an SMTP package on the Bulko SMTP Relay page and copy the username and password from Dashboard -> SMTP Access.
- Set the environment variables
SMTP_USERNAMEandSMTP_PASSWORDin the shell, systemd unit, container or CI secret store that runs your script. - Build the message with
email.message.EmailMessage: setFrom,ToandSubjectheaders, callset_content()for the plain text part and optionallyadd_alternative(..., subtype="html")for HTML. This replaces the olderMIMEText/MIMEMultipartapproach. - For port 587 open
smtplib.SMTP(HOST, 587, timeout=30)and callstarttls(context=ssl.create_default_context())beforelogin(). The default context verifies the server certificate and hostname. - For port 465 use
smtplib.SMTP_SSL(HOST, 465, context=context)instead and do not callstarttls(), because the connection is already encrypted. - Call
login(USER, PASSWORD)andsend_message(msg). It takes the envelope sender and recipients from the headers (To, Cc and Bcc) and does not transmit the Bcc header. - 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
| Problem | Fix |
|---|---|
| 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 465 | Plain smtplib.SMTP was used on 465, which expects TLS immediately. Switch to SMTP_SSL for 465. |
| SMTPNotSupportedError: SMTP AUTH extension not supported | login() was called before starttls() on port 587. Call starttls() first, then login(). |
| TimeoutError or ConnectionRefusedError | Your 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.
What you can send: Acceptable Use Policy.