// cd /blog/webmite-gmail

Sending email through Gmail from a WebMite

The WiFi versions of the PicoMite can send email on their own: an alarm from a sensor, a daily log file, a reading from the garden. This article sets up a Gmail account to accept mail from the board, then gives complete MMBasic programs that send a message, with or without an attachment.

In short

  • Gmail accepts mail from the board. The board logs in with your Gmail address and an app password: a separate 16-letter password for one device, which you can delete at any time without changing your main password.
  • The firmware does the encryption. WEB OPEN TLS CLIENT connects to smtp.gmail.com on port 465, which is encrypted from the first byte. Port 587 is not supported, because it needs STARTTLS, which switches to encryption partway through.
  • Sending a message takes under ten exchanges. Each one is a single WEB TCP CLIENT REQUEST, and the program checks Gmail's three-digit reply before going on.
  • Attachments are streamed, not loaded. The file is read 57 bytes at a time, base64 encoded and sent in 4 KB blocks with WEB TCP CLIENT WRITE, so even a Pico W can send a file bigger than its free memory.

On this page

  1. What you need
  2. Step 1: a Gmail app password
  3. How the board talks to Gmail
  4. Step 2: send a message
  5. Step 3: send a file
  6. Checking that it really is Google
  7. When it goes wrong
  8. Good habits

What you need

  • A Raspberry Pi Pico W or Pico 2 W running a WebMite build of the PicoMite firmware, version 6.04 or later. These builds have the TLS (secure connection) commands this needs. The current release is on GitHub.
  • The board joined to your network. If it isn't yet, type OPTION WIFI "network-name", "network-password" at the prompt. The board restarts and connects by itself from then on.
  • A Google account with 2-Step Verification turned on. A free Gmail account set up just for your devices is best: see Good habits.

Step 1: a Gmail app password

Gmail won't let a device log in with your normal password. Instead, you create an app password for it: a random 16-letter password for one app or device, which you can delete without affecting anything else.

  1. Sign in to the Google account you'll send from, at myaccount.google.com.
  2. Turn on 2-Step Verification if it isn't already on: Security, then 2-Step Verification. App passwords are only available with it on.
  3. Open myaccount.google.com/apppasswords. Type a name that tells you which device it's for, such as WebMite greenhouse, and click Create.
  4. Copy the password Google shows you. It appears as four groups of four letters, such as abcd efgh ijkl mnop. Remove the spaces: the program needs the 16 letters run together. Google shows it only once; if you lose it, delete it and create another.
If the App passwords page says the setting isn't available, the usual reasons are that 2-Step Verification is off, or set up with security keys only; that the account uses Google's Advanced Protection; or that it's a work or school account whose administrator has turned app passwords off. Note too that changing the account's main password deletes all its app passwords, so the board stops sending until you give it a new one. No other Gmail setting needs to change.

How the board talks to Gmail

Mail is sent with SMTP, a conversation of short text lines. The board sends a command; Gmail answers with a three-digit code and some text. A code starting with 2 or 3 means carry on, and 4 or 5 means something is wrong.

The board sendsGmail repliesMeaning
connects to port 465220 smtp.gmail.com ESMTPHello: the firmware absorbs this greeting
EHLO webmite250-smtp.gmail.com at your serviceIntroductions, plus a list of what Gmail supports
AUTH LOGIN334 VXNlcm5hbWU6“Username:”, in base64
Gmail address, in base64334 UGFzc3dvcmQ6“Password:”, in base64
App password, in base64235 2.7.0 AcceptedLogged in
MAIL FROM:<you@gmail.com>250 2.1.0 OKSender accepted
RCPT TO:<them@example.com>250 2.1.5 OKRecipient accepted
DATA354 Go aheadSend the message
The message, then a line holding just .250 2.0.0 OKAccepted for delivery

Three details of the WebMite's commands shape the programs below:

  • Port 465, not 587. On port 465 the connection is encrypted before anything is said, which is what WEB OPEN TLS CLIENT does. Port 587 starts unencrypted and switches over with a STARTTLS command, which the firmware doesn't support.
  • No need to read the greeting. Gmail sends its 220 greeting the moment the secure connection is up, and the firmware discards it. The short pause after opening gives it time to arrive, so the program can send EHLO straight away and get EHLO's own reply. (WEB TCP CLIENT READ exists for servers that make you wait for their greeting on a plain connection.)
  • REQUEST for commands, WRITE for the message. WEB TCP CLIENT REQUEST sends up to 255 characters and waits for the reply, which suits the commands. But Gmail says nothing while the message itself arrives, so that goes out with WEB TCP CLIENT WRITE, which sends a LONGSTRING of any length without waiting. The final . goes with REQUEST, to collect Gmail's verdict.

Step 2: send a message

This is the whole program. Put your Gmail address and app password in the two Const lines at the top, put a real address in the SendMail line, and RUN it.

send-mail.basDownload
' send-mail.bas: send an email through Gmail from a WebMite
' https://mather.host/blog/webmite-gmail/
' Needs a WiFi (WebMite) build of the PicoMite firmware, V6.04 or
' later, joined to your network with OPTION WIFI, and a Gmail app
' password.
Option Explicit

Const GmailUser = "your.name@gmail.com"   ' the Gmail account to send from
Const AppPassword = "abcdefghijklmnop"    ' its app password: 16 letters, no spaces
Const CRLF = Chr$(13) + Chr$(10)

Dim Integer rx(512)    ' Gmail's replies: a LONGSTRING of up to 4 KB
Dim Integer msg(512)   ' the outgoing message: a LONGSTRING of up to 4 KB

' Optional: check that the server really is Google (see the article)
' WEB NTP
' WEB TLS CA "gmail-ca.pem"

SendMail "someone@example.com", "Hello from the WebMite", "Sent by a Pico running MMBasic."
Print "Mail sent"
End

' Send a plain-text email. The body can be up to 255 characters.
Sub SendMail(mailto As String, subject As String, body As String)
  WEB OPEN TLS CLIENT "smtp.gmail.com", 465, 20000
  Pause 300
  Smtp "EHLO webmite", "250"
  Smtp "AUTH LOGIN", "334"
  Smtp B64(GmailUser), "334"
  Smtp B64(AppPassword), "235"
  Smtp "MAIL FROM:<" + GmailUser + ">", "250"
  Smtp "RCPT TO:<" + mailto + ">", "250"
  Smtp "DATA", "354"
  LongString Clear msg()
  LongString Append msg(), "From: " + GmailUser + CRLF
  LongString Append msg(), "To: " + mailto + CRLF
  LongString Append msg(), "Subject: " + subject + CRLF
  LongString Append msg(), CRLF
  LongString Append msg(), body + CRLF
  WEB TCP CLIENT WRITE msg()
  Smtp ".", "250"
  WEB CLOSE TCP CLIENT
End Sub

' Send one line to Gmail and check that the reply starts with the
' expected code. On any other reply, show it and stop.
Sub Smtp(cmd As String, code As String)
  WEB TCP CLIENT REQUEST cmd + CRLF, rx(), 20000
  If Replied(code) = 0 Then
    Print "Gmail replied: ";
    LongString Print rx()
    WEB CLOSE TCP CLIENT
    Error "Expected " + code
  EndIf
End Sub

' True if the last reply, or any line of it, starts with code
Function Replied(code As String) As Integer
  If LInStr(rx(), code) = 1 Then Replied = 1
  If LInStr(rx(), Chr$(10) + code) > 0 Then Replied = 1
End Function

' Base64-encode a string, as AUTH LOGIN needs
Function B64(s As String) As String
  Local Integer n = Math(BASE64 ENCODE s, B64)
End Function

When it works, the console shows the firmware's connection messages and then the program's own. The address will be different:

> RUN
Connecting to 142.250.27.109 port 465 (TLS)
Connected
Mail sent

How it works:

  • Smtp sends one line and checks that Gmail's reply starts with the code that should come back. If it doesn't, it prints the reply and stops with an error. Gmail's replies say plainly what's wrong, so read them first: see When it goes wrong.
  • AUTH LOGIN asks for the user name and then the password, each base64 encoded. B64 does this with MMBasic's built-in MATH(BASE64 ENCODE). Base64 is not encryption: the TLS connection is what keeps the password private.
  • The message itself is headers (From:, To:, Subject:), a blank line, then the text. Every line ends with a carriage return and line feed, which is what CRLF holds.

To send mail from your own program, copy in the constants, the two arrays and the four routines, then call SendMail whenever there's something to report:

If pressure < 1.0 Then SendMail "you@example.com", "Boiler alarm", "Pressure is " + Str$(pressure, 1, 2) + " bar"

Step 3: send a file

An attachment turns the message into a MIME message with several parts. The header Content-Type: multipart/mixed names a boundary string, and each part starts with a line of two dashes and that string. The first part is the text. The second is the file, with headers giving its type and name, encoded as base64 text because email can only carry text. The last boundary line has two more dashes at the end.

send-attachment.basDownload
' send-attachment.bas: email a file through Gmail from a WebMite
' https://mather.host/blog/webmite-gmail/
' Needs a WiFi (WebMite) build of the PicoMite firmware, V6.04 or
' later, joined to your network with OPTION WIFI, and a Gmail app
' password.
Option Explicit

Const GmailUser = "your.name@gmail.com"   ' the Gmail account to send from
Const AppPassword = "abcdefghijklmnop"    ' its app password: 16 letters, no spaces
Const CRLF = Chr$(13) + Chr$(10)
Const Q = Chr$(34)                        ' a double quote
Const Boundary = "WebMite-boundary-7d3f9a"  ' separates the parts of the message

Dim Integer rx(512)    ' Gmail's replies: a LONGSTRING of up to 4 KB
Dim Integer msg(512)   ' the outgoing message, sent in blocks of up to 4 KB

' Optional: check that the server really is Google (see the article)
' WEB NTP
' WEB TLS CA "gmail-ca.pem"

SendFile "someone@example.com", "Today's log", "The log file is attached.", "log.csv", "text/csv"
Print "Mail sent"
End

' Send a plain-text email with one file attached. fname can include a
' drive and folder, eg "B:/logs/today.csv". ftype is the file's MIME
' type, eg "text/csv", "image/jpeg" or "application/octet-stream".
Sub SendFile(mailto As String, subject As String, body As String, fname As String, ftype As String)
  Local String chunk, coded, shown
  Local Integer i, n

  ' The name shown in the email: fname without its drive or folder
  shown = fname
  For i = Len(fname) To 1 Step -1
    If Mid$(fname, i, 1) = "/" Or Mid$(fname, i, 1) = ":" Then
      shown = Mid$(fname, i + 1)
      Exit For
    EndIf
  Next i

  WEB OPEN TLS CLIENT "smtp.gmail.com", 465, 20000
  Pause 300
  Smtp "EHLO webmite", "250"
  Smtp "AUTH LOGIN", "334"
  Smtp B64(GmailUser), "334"
  Smtp B64(AppPassword), "235"
  Smtp "MAIL FROM:<" + GmailUser + ">", "250"
  Smtp "RCPT TO:<" + mailto + ">", "250"
  Smtp "DATA", "354"

  ' The headers, then the text part of the message
  LongString Clear msg()
  LongString Append msg(), "From: " + GmailUser + CRLF
  LongString Append msg(), "To: " + mailto + CRLF
  LongString Append msg(), "Subject: " + subject + CRLF
  LongString Append msg(), "MIME-Version: 1.0" + CRLF
  LongString Append msg(), "Content-Type: multipart/mixed; boundary=" + Q + Boundary + Q + CRLF
  LongString Append msg(), CRLF
  LongString Append msg(), "--" + Boundary + CRLF
  LongString Append msg(), "Content-Type: text/plain; charset=UTF-8" + CRLF
  LongString Append msg(), CRLF
  LongString Append msg(), body + CRLF

  ' The attachment's own headers
  LongString Append msg(), "--" + Boundary + CRLF
  LongString Append msg(), "Content-Type: " + ftype + "; name=" + Q + shown + Q + CRLF
  LongString Append msg(), "Content-Transfer-Encoding: base64" + CRLF
  LongString Append msg(), "Content-Disposition: attachment; filename=" + Q + shown + Q + CRLF
  LongString Append msg(), CRLF

  ' The file: every 57 bytes become one 76-character line of base64,
  ' and each block is sent as soon as it holds about 4 KB
  Open fname For Input As #1
  Do While Not Eof(#1)
    chunk = Input$(57, #1)
    n = Math(BASE64 ENCODE chunk, coded)
    LongString Append msg(), coded + CRLF
    If LLen(msg()) > 4000 Then
      WEB TCP CLIENT WRITE msg()
      LongString Clear msg()
    EndIf
  Loop
  Close #1

  ' Close the last part, send what is left, then end the message
  LongString Append msg(), "--" + Boundary + "--" + CRLF
  WEB TCP CLIENT WRITE msg()
  Smtp ".", "250"
  WEB CLOSE TCP CLIENT
End Sub

' Send one line to Gmail and check that the reply starts with the
' expected code. On any other reply, show it and stop.
Sub Smtp(cmd As String, code As String)
  WEB TCP CLIENT REQUEST cmd + CRLF, rx(), 20000
  If Replied(code) = 0 Then
    Print "Gmail replied: ";
    LongString Print rx()
    WEB CLOSE TCP CLIENT
    Error "Expected " + code
  EndIf
End Sub

' True if the last reply, or any line of it, starts with code
Function Replied(code As String) As Integer
  If LInStr(rx(), code) = 1 Then Replied = 1
  If LInStr(rx(), Chr$(10) + code) > 0 Then Replied = 1
End Function

' Base64-encode a string, as AUTH LOGIN needs
Function B64(s As String) As String
  Local Integer n = Math(BASE64 ENCODE s, B64)
End Function

As before, fill in the constants and the SendFile line. The fourth parameter is the file to send. It can include a drive and folder, so "B:/logs/today.csv" attaches a log from the SD card. Only the name, today.csv, appears in the email.

The fifth parameter is the file's MIME type, which tells the recipient's mail program what to do with it:

FileMIME type
.txt, .bastext/plain
.csvtext/csv
.jpgimage/jpeg
.pngimage/png
.bmpimage/bmp
anything elseapplication/octet-stream

Points to note:

  • 57 bytes make one line. Base64 turns every 3 bytes into 4 characters, so 57 bytes become the 76-character lines that email expects.
  • The memory used doesn't grow with the file. The lines collect in a 4 KB LONGSTRING, which is sent with WEB TCP CLIENT WRITE and cleared as soon as it holds more than 4,000 characters. Gmail's limit is 25 MB per message, and base64 makes a file a third bigger than it is.
  • SendFile uses file number 1. If your program keeps #1 open for something else, change the three lines that use it.

Checking that it really is Google

By default, WEB OPEN TLS CLIENT encrypts the connection but doesn't check the server's certificate, so it can't tell Gmail from an impostor on your network. To make sure your app password only ever goes to Google, give the firmware Google's root certificates:

  1. Download Google's GTS Root R1 and GTS Root R4 certificates from Google's own repository: gtsr1.pem and gtsr4.pem. Join them into one file. On Windows, at a command prompt:
    copy /b gtsr1.pem + gtsr4.pem gmail-ca.pem
    On Linux or macOS:
    cat gtsr1.pem gtsr4.pem > gmail-ca.pem
    Two are needed because Gmail sends one of two certificate chains, depending on what the other end supports: an RSA chain that ends at GTS Root R1, or an ECDSA chain that ends at GTS Root R4. Both roots are valid until 2036.
  2. Copy gmail-ca.pem to the WebMite's A: drive, the same way you load programs: with XMODEM RECEIVE "gmail-ca.pem" and your terminal program, or by TFTP, which is on by default in the WiFi builds.
  3. Remove the ' from the two lines near the top of the program:
    WEB NTP
    WEB TLS CA "gmail-ca.pem"

WEB NTP sets the clock, which the check needs because certificates have start and end dates. If your program already sets the clock, for example with WEB NTP and your time zone, leave that line out. WEB TLS CA applies to every later secure connection, including MQTT, until WEB TLS NOVERIFY. So if the program also talks to other secure servers, add their root certificates to the same file.

When it goes wrong

What you seeWhat it means
535-5.7.8 Username and Password not acceptedThe Gmail address or app password is wrong. Check that the spaces are out of the app password. Gmail gives the same reply when the app password has been deleted, which includes after a change to the account's main password.
534-5.7.9 Application-specific password requiredThe account's normal password was used. Create an app password (Step 1).
Failed to convert web addressThe board couldn't look up smtp.gmail.com. Check that it's on your network: PRINT MM.INFO$(IP ADDRESS) should show an address.
No response from TLS server, or another error mentioning TLSThe secure connection didn't come up. Some networks block port 465. With certificate checking on, check that the clock was set and that gmail-ca.pem holds both certificates.
Cannot find fileAfter WEB TLS CA, gmail-ca.pem isn't on the current drive. In SendFile, the file to attach isn't where the program says.
A reply starting 5 after the final .Gmail refused the message itself, for example because it's too big (552) or looks like spam (550). The rest of the reply says why.
The mail is “from” your Gmail address, whatever From: saysGmail always sends as the account that logged in, unless the other address is set up as a “Send mail as” address in Gmail's settings.

Good habits

  • Give your devices their own Gmail account. Anyone with the app password can read and send mail as that account, so keep it away from your personal mailbox.
  • Never share a program with the password still in it. If one gets out, delete the app password at myaccount.google.com/apppasswords and create a new one.
  • Send when something changes, not in a loop. Gmail limits how much an account can send each day, and it may suspend an account that looks like it's sending spam. An alarm when a reading crosses a limit, or one summary a day, is the right pace.
  • Watch for lines that start with a dot. A line holding just . ends the message, so a line of your text that starts with . needs a second one added in front; it's removed again on delivery. Base64 never contains a dot, so attachments are safe.

The WebMite's network commands are described in full in the PicoMite User Manual, and at the prompt with HELP WEB.

← All articles