pico intermediate 40 min

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.

Code available for: MicroPython
Published Sep 22, 2026

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 ussl modules 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.

StepWhoSays
1Pico WConnect TCP port 587
2Server220 ready
3Pico WEHLO, then STARTTLS
4BothTLS handshake; everything after is encrypted
5Pico WAUTH LOGIN with base64 user and password
6Pico WMAIL FROM, RCPT TO, DATA
7Pico WHeaders, 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.
  • umail wraps 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.