Pico W: send email alerts over SMTP
Send email straight from a Pico W over SMTP with STARTTLS in MicroPython. Works with your own mail server or any provider's app password, no cloud IoT service in the middle.
Email is the notification channel that outlives every app. The Pico W has
Wi-Fi, MicroPython has ussl for TLS and a plain socket module, and
SMTP is a text protocol from 1981. That combination is enough to send a
real alert email from a chip that costs $7. No cloud IoT account, no API
key from a startup, no middleman that can shut down.
The trap is the handshake. The first version I wrote connected fine and then hung for 30 seconds on the first send, and the reason was not the code: it was the port and TLS mode pairing. Port 587 with STARTTLS and port 465 with TLS-from-byte-one look identical from the outside (a timeout, nothing else), and the Pico W gives you no error message on either. Pair them wrong and you debug a network that is not broken.
What you need
Needed
- Raspberry Pi Pico W (the plain Pico has no radio, so no SMTP)
- MicroPython firmware v1.20+ (the Wi-Fi and
usslmodules are built in) - An SMTP account. Three routes, in the order I would try:
- Wi-Fi credentials for the Pico W
Nice to have
- A soldering iron and solder (only if you solder the header pins yourself)
- Helping hands or a vise to hold the board while you work
- An anti-static wristband (cheap insurance for the RP2040)
The SMTP conversation, in plain English
SMTP is all text. Knowing the shape helps when it breaks, because the error messages are SMTP error codes and they mean something specific.
| Step | Who | Says |
|---|---|---|
| 1 | Pico W | Connect TCP port 587 |
| 2 | Server | 220 ready |
| 3 | Pico W | EHLO, then STARTTLS |
| 4 | Both | TLS handshake; everything after is encrypted |
| 5 | Pico W | AUTH LOGIN with base64 user and password |
| 6 | Pico W | MAIL FROM, RCPT TO, DATA |
| 7 | Pico W | Headers, blank line, body, then a line with one . |
umail (the MicroPython library below) does the whole dance in three
lines. You are reading the table for the day it breaks and you need to
know which step failed.
Install
MicroPython ships ussl in the firmware. The SMTP helper library
umail is one file. In Thonny:
Thonny >> Tools >> Manage packages >> search micropython-umail >>
install
If the package index does not have it on your firmware version, grab
umail.py from the project’s GitHub and copy it to the Pico’s filesystem
with the Thonny file pane (open the file on disk, File >> Save as >> MicroPython device).
The code
Save this as main.py on the Pico W:
import network
import time
import umail
import ussl # noqa: F401 (umail uses it internally)
SSID = "your-wifi-ssid"
PASSWORD = "your-wifi-password"
SMTP_HOST = "mail.yourdomain.com"
SMTP_PORT = 587 # 587 = STARTTLS, 465 = TLS from byte one
SMTP_USER = "alerts@yourdomain.com"
SMTP_PASS = "your-app-password"
TO_EMAIL = "you@yourdomain.com"
def connect_wifi():
wlan = network.WLAN(network.STA_IF)
wlan.active(True)
wlan.connect(SSID, PASSWORD)
for _ in range(20):
if wlan.isconnected():
break
time.sleep(1)
if not wlan.isconnected():
raise RuntimeError("Wi-Fi failed to connect")
print("Wi-Fi up:", wlan.ifconfig()[0])
def send_alert(subject, body):
# STARTTLS on 587: plaintext hello, then upgrade to TLS
smtp = umail.SMTP(SMTP_HOST, SMTP_PORT, SSL=False)
smtp.login(SMTP_USER, SMTP_PASS)
smtp.to(TO_EMAIL, mail_from=SMTP_USER)
smtp.write("Subject: " + subject + "\r\n")
smtp.write("\r\n") # blank line separates headers from body
smtp.write(body)
smtp.send()
smtp.quit()
print("Alert sent:", subject)
connect_wifi()
send_alert("Pico W is awake",
"The plant monitor just booted. This is the boot-time test send.")
If the connection times out at umail.SMTP(...), the port and TLS mode
are mismatched. For port 465 servers, construct with SSL=True:
smtp = umail.SMTP(SMTP_HOST, 465, SSL=True)
That one-character difference is the trap from the opening paragraph. The pairing of port and TLS mode is the single most common SMTP failure on the Pico, and it fails as a timeout with zero useful information.
The local test rig
Before you fight a real provider’s TLS rules, point the Pico at a fake SMTP server on your LAN. MailHog is a single Go binary that accepts anything and shows it in a web inbox. On a Raspberry Pi:
docker run -d -p 1025:1025 -p 8025:8025 mailhog/mailhog
Point the Pico at the Pi’s IP, port 1025, SSL=False, empty login. You
get instant feedback on whether your code is right, separate from whether
your mail provider is having a day. Then flip to the real server by
changing four constants.
MailHog speaks plaintext SMTP. A real submission server on port 587 demands STARTTLS before it will even accept a password. Testing against MailHog proves your code works; it does not prove the provider will accept you.
App passwords, not your real password
Any major provider now wants an “app password” for non-browser clients
(e.g. Google Account >> Security >> 2-Step Verification >> App
passwords). That string goes in SMTP_PASS. It can be revoked without
touching your real password, which is exactly what you want for a device
sitting in a garage or a crawlspace. If you self-host, create a
dedicated alerts@ account with send-only rights and one revocation
switch.
The 5-second handshake on battery
The TLS handshake is not free. Budget 3 to 5 seconds of Wi-Fi-plus-TLS time at around 150 mA for the whole send-alert cycle. For a battery project that sleeps and wakes to alert, that math goes in the deep-sleep budget up front (e.g. one alert an hour is fine on a 2000 mAh cell; one a minute is a different battery conversation). For high-frequency notification, ntfy is the lighter channel; email is for the events that matter enough to write a paragraph about.
What you learned
- SMTP with STARTTLS on port 587 is the portable pattern; 465 with SSL=True is the other valid pairing, and mixing them fails as a silent timeout.
umailwraps the EHLO, STARTTLS, AUTH LOGIN, and DATA steps.- App passwords scope the credential to the device; MailHog decouples “is my code right” from “is the provider right.”
When something breaks
- Times out on connect with no error. Port and TLS mode are mismatched (587 with SSL=True, or 465 with SSL=False). Check the pairing first; it is the most common failure by a wide margin.
530 Must issue a STARTTLS command first. The server demands TLS and you are speaking plaintext past the hello. That is the SSL flag again, or the wrong port.535 Authentication failed. The provider wants an app password, and you sent the account password. Generate the app password (Google: Google Account >> Security >> 2-Step Verification >> App passwords) and use it. Also confirm the account is not locked to a browser sign-in first.- Wi-Fi drops mid-handshake. The Pico W’s Wi-Fi is less stable than the ESP32’s under sustained load. Add a reconnect loop before the send (the MQTT tutorial has the pattern), and keep the alert body short so the TLS session is brief.
- Mail goes to spam. Your domain’s SPF and DKIM do not cover the sending server. That is a DNS fix, not a Pico fix: add the sending server to your SPF record and try again.
What to build next
- The ntfy tutorial on the ESP32 is the lighter-weight push channel; run both and use email for the weekly digest, ntfy for the doorbell.
- The Pico W MQTT tutorial is the transport for high-frequency sensor data; email is for the one-in-a-thousand events worth a full message.
- The Pico microSD datalogger writes the same events to a card, so you have the full history on the card and only the interesting ones in your inbox.