Chapter 29 19 min read

Email (SMTP / IMAP / POP3)

Glide's stdlib::mail modules cover the full client side of email: build a message with Mail (RFC 5322 + MIME), send it with SmtpClient / SmtpSession, and read mailboxes with ImapSession (IMAP4rev1) or Pop3Session (POP3). All transports run over plain TCP or TLS via stdlib::net.

Import

Each protocol lives in its own submodule. Pull in what you need:

import stdlib::mail::message::*;   // Mail, Attachment, parse_mail
import stdlib::mail::smtp::*;      // SmtpClient, SmtpSession, SmtpReply
import stdlib::mail::imap::*;      // ImapSession, ImapResponse
import stdlib::mail::pop3::*;      // Pop3Session, Pop3MsgInfo

message is the common dependency: SMTP sends a *Mail, and IMAP/POP3 retrieval pairs with parse_mail to decode what comes back. The mail modules depend on stdlib::net::tcp / stdlib::net::tls, stdlib::base64, and (for message) stdlib::random, stdlib::time, and stdlib::hashmap.

Public surface at a glance

Module Types Functions / methods
message Mail, Attachment Mail::new, Mail.to_rfc5322, parse_mail
smtp SmtpClient, SmtpSession, SmtpReply SmtpClient::new, .send, .connect; SmtpSession.send_mail, .quit, .close
imap ImapSession, ImapResponse ImapSession::connect, .login, .select, .fetch, .store_flag, .search, .search_unseen, .search_from, .append, .idle_start, .idle_next, .idle_stop, .logout, .close
pop3 Pop3Session, Pop3MsgInfo Pop3Session::connect, .login, .stat, .list, .retrieve, .delete, .noop, .quit, .close

The Mail type

A message you build in memory and serialise to wire bytes, or parse back from raw bytes received over IMAP/POP3.

Attachment (struct)

pub struct Attachment {
    pub filename:  string,
    pub mime_type: string,
    pub data:      string,
}

A single MIME attachment. data holds the raw (unencoded) bytes; to_rfc5322 base64-encodes it and wraps the result at 76 columns, with Content-Disposition: attachment; filename="...". There is no constructor — allocate it raw and set the fields, then push it onto mail.attachments:

import stdlib::mail::message::*;

fn main() -> i32 {
    let att: *Attachment = malloc(sizeof(Attachment)) as *Attachment;
    att.filename = "report.csv";
    att.mime_type = "text/csv";
    att.data = "a,b\n1,2";

    let m: *Mail = Mail::new();
    m.from = "[email protected]";
    m.to.push("[email protected]");
    m.subject = "report";
    m.body = "see attached";
    m.attachments.push(att);

    let wire: string = m.to_rfc5322();        // multipart/mixed
    println!("len", wire.len());
    println!("has base64:", wire.contains("base64"));
    return 0;
}

Mail (struct)

pub struct Mail {
    pub from:        string,
    pub to:          *Vector<string>,
    pub cc:          *Vector<string>,
    pub bcc:         *Vector<string>,
    pub subject:     string,
    pub body:        string,             // plain-text body
    pub html:        ?string,            // optional HTML alternative
    pub attachments: *Vector<*Attachment>,
    pub headers:     *HashMap<string>,   // extra headers (Reply-To, etc.)
    pub date:        string,             // empty = auto-fill at to_rfc5322
    pub message_id:  string,             // empty = auto-fill
}
Field Type Notes
from string Sender address. Emitted as From: when non-empty.
to / cc / bcc *Vector<string> Recipient lists. to/cc appear as headers; `bcc` is never written to the headers — it is added as a RCPT TO only (see SMTP below).
subject string Subject: header when non-empty.
body string Plain-text body. ASCII is sent 7bit; non-ASCII is quoted-printable encoded.
html ?string When some(...), the message becomes multipart/alternative (text + HTML).
attachments *Vector<*Attachment> When non-empty, the message becomes multipart/mixed.
headers *HashMap<string> Arbitrary extra headers (e.g. Reply-To). Emitted verbatim after the standard headers.
date string Empty auto-fills the current RFC 5322 date at serialise time.
message_id string Empty auto-generates <random@domain-of-from>.

Mail::new

pub fn new() -> *Mail

Builds an empty Mail with all vectors and the header map allocated, html = none(), and date/message_id empty (auto-filled later). Set the public fields directly, then call to_rfc5322.

Mail.to_rfc5322

pub fn to_rfc5322(self: *Mail) -> string

Serialise to the RFC 5322 wire format: headers, a blank line, then the body or MIME sections. The layout adapts to what is set:

body html attachments Resulting Content-Type
set none empty text/plain; charset=UTF-8
set some empty multipart/alternative (text + HTML)
set none non-empty multipart/mixed (text + attachments)
set some non-empty multipart/mixed wrapping multipart/alternative + attachments

Date, Message-Id, and MIME-Version headers are always emitted; missing Date/Message-Id are auto-filled. Bodies (and HTML parts) that contain non-ASCII characters are quoted-printable encoded automatically; pure-ASCII parts are sent 7bit verbatim. MIME boundaries are random per call (===_glide_outer_…).

import stdlib::mail::message::*;

fn main() -> i32 {
    let m: *Mail = Mail::new();
    m.from = "[email protected]";
    m.to.push("[email protected]");
    m.cc.push("[email protected]");
    m.subject = "hello";
    m.body = "Plain text body.";
    m.html = some("<p>HTML body.</p>");

    let att: *Attachment = malloc(sizeof(Attachment)) as *Attachment;
    att.filename = "doc.txt";
    att.mime_type = "text/plain";
    att.data = "x";
    m.attachments.push(att);

    m.headers.insert("Reply-To", "[email protected]");

    let wire: string = m.to_rfc5322();   // multipart/mixed { alternative, attachment }
    println!("wire bytes:", wire.len());
    return 0;
}

The next program shows the auto-fill behaviour and quoted-printable selection. An empty date/message_id is filled at serialise time; a non-ASCII body flips the encoding from 7bit to quoted-printable. Set the fields explicitly to override:

import stdlib::mail::message::*;

fn main() -> i32 {
    let m: *Mail = Mail::new();
    m.from = "[email protected]";
    m.to.push("[email protected]");
    m.subject = "cafe";
    m.body = "caf\u{00e9}";              // non-ASCII -> quoted-printable
    m.headers.insert("Reply-To", "[email protected]");

    let w1: string = m.to_rfc5322();
    println!("auto date:", w1.contains("Date: "));
    println!("auto msgid:", w1.contains("Message-Id: <"));
    println!("qp encoding:", w1.contains("quoted-printable"));

    m.date = "Tue, 01 Jan 2026 00:00:00 +0000";
    m.message_id = "[email protected]";
    let w2: string = m.to_rfc5322();
    println!("explicit msgid:", w2.contains("Message-Id: <[email protected]>"));
    return 0;
}

parse_mail

pub fn parse_mail(raw: string) -> !*Mail

Parse a raw RFC 5322 message into a Mail. Splits the header block from the body at the first \r\n\r\n, unfolds continuation lines (leading SP/TAB), and decodes the headline fields: From, To, Cc, Subject, Date, and Message-Id (the angle brackets are stripped). Recognised To/Cc lists are comma-split into the recipient vectors. Any other header (including Content-Type / Content-Transfer-Encoding) is preserved in headers. Errors with "mail: missing header/body separator" if no blank line is found.

import stdlib::mail::message::*;

fn main() -> !i32 {
    let raw: string = "From: [email protected]\r\nTo: [email protected], [email protected]\r\nSubject: hi\r\nMessage-Id: <abc@x>\r\nX-Custom: yes\r\n\r\nbody here";
    let m: *Mail = parse_mail(raw)?;
    println!("from:", m.from);
    println!("subject:", m.subject);
    println!("message-id:", m.message_id);   // "abc@x" — brackets stripped
    println!("to count:", m.to.len());        // 2 — comma-split
    println!("custom:", m.headers.get("X-Custom"));
    println!("body:", m.body);

    // Missing blank-line separator → error.
    let bad: !*Mail = parse_mail("From: x\r\nno blank line");
    println!("err surfaces:", !bad.ok);
    return ok(0);
}

SMTP — sending

Connect, optionally STARTTLS-upgrade or wrap from the first byte (port 465), optionally authenticate, then run the MAIL FROM / RCPT TO / DATA / QUIT state machine.

Item Signature Description
SmtpClient::new pub fn new(host: string, port: i32) -> *SmtpClient Build a client; configure fields then send/connect.
SmtpClient.send pub fn send(self: *SmtpClient, mail: *Mail) -> !i32 One-shot: connect, send one message, quit, close.
SmtpClient.connect pub fn connect(self: *SmtpClient) -> !*SmtpSession Open a reusable session positioned before MAIL FROM.
SmtpSession.send_mail pub fn send_mail(self: *SmtpSession, mail: *Mail) -> !i32 Run the per-message conversation.
SmtpSession.quit pub fn quit(self: *SmtpSession) -> !i32 Send QUIT, expect 221.
SmtpSession.close pub fn close(self: *SmtpSession) Tear down the socket; idempotent.

SmtpClient (struct)

pub struct SmtpClient {
    pub host:         string,
    pub port:         i32,
    pub use_tls:      bool,     // implicit TLS from byte one (port 465 / SMTPS)
    pub use_starttls: bool,     // STARTTLS upgrade after EHLO (port 587)
    pub username:     ?string,
    pub password:     ?string,
    pub timeout_ms:   i32,
    pub tls_insecure: bool,     // skip cert validation — local dev only
    pub auth_method:  string,   // "auto" | "plain" | "login"
}
Field Default (after new) Meaning
use_tls false Wrap the socket in TLS immediately. Mutually exclusive with use_starttls.
use_starttls false Send STARTTLS after EHLO, then re-EHLO over the protected channel.
username / password none() When both are some, the session authenticates after (STARTTLS+)EHLO.
timeout_ms 0 Socket read/write timeout; 0 leaves the stream default.
tls_insecure false Disables certificate validation. Use only against local dev servers.
auth_method "auto" "auto" picks from the EHLO advertisement (prefers PLAIN, falls back to LOGIN); "plain" / "login" force a mechanism.

SmtpClient::new

pub fn new(host: string, port: i32) -> *SmtpClient

Build a client with all the booleans false, credentials none(), auth_method = "auto", and timeout_ms = 0. Configure the public fields before calling send or connect.

SmtpClient.send

pub fn send(self: *SmtpClient, mail: *Mail) -> !i32

One-shot: opens a connection, runs the entire SMTP conversation for mail, quits, and closes — returning ok(0) on success. For multiple messages on one connection, use connect instead.

import stdlib::mail::message::*;
import stdlib::mail::smtp::*;

fn main() -> i32 {
    let c: *SmtpClient = SmtpClient::new("smtp.example.com", 587);
    c.use_starttls = true;
    c.username = some("alice");
    c.password = some("hunter2");
    c.timeout_ms = 10000;
    c.auth_method = "auto";

    let m: *Mail = Mail::new();
    m.from = "[email protected]";
    m.to.push("[email protected]");
    m.bcc.push("[email protected]");   // delivered, but hidden from headers
    m.subject = "ping";
    m.body = "hi";

    let r: !i32 = c.send(m);
    if !r.ok { println!(r.err); return 1; }
    return 0;
}

To talk to a local dev server with a self-signed cert and force a specific auth mechanism, set tls_insecure and auth_method:

import stdlib::mail::message::*;
import stdlib::mail::smtp::*;

fn main() -> i32 {
    let c: *SmtpClient = SmtpClient::new("localhost", 587);
    c.use_starttls = true;
    c.tls_insecure = true;        // skip cert validation — dev only
    c.auth_method = "login";      // force AUTH LOGIN
    c.username = some("dev");
    c.password = some("dev");

    let m: *Mail = Mail::new();
    m.from = "dev@localhost";
    m.to.push("inbox@localhost");
    m.subject = "test";
    m.body = "x";

    let r: !i32 = c.send(m);
    if !r.ok { println!(r.err); return 1; }
    return 0;
}

SmtpClient.connect

pub fn connect(self: *SmtpClient) -> !*SmtpSession

Open a session and run TCP connect → read banner → EHLO → optional STARTTLS (+ re-EHLO) → optional AUTH. The returned *SmtpSession is positioned right before MAIL FROM and may be reused across many send_mail calls until you quit / close it.

SmtpReply (struct)

pub struct SmtpReply {
    pub code: i32,      // numeric status (e.g. 250, 354, 535)
    pub text: string,   // last reply line (multi-line replies collapse to the final line)
}

The raw reply value parsed off the wire. You rarely construct it; it surfaces in error messages built from the server's status code and text (e.g. "smtp: 535 5.7.8 auth failed").

SmtpSession (struct)

pub struct SmtpSession {
    pub host:       string,
    // tcp / tls / is_tls are private transport state
    pub auth_mechs: string,   // mechanisms the server advertised at EHLO, lowercased
}

auth_mechs holds the space-separated AUTH mechanisms from the most recent EHLO (e.g. "plain login cram-md5"), or "" if EHLO has not run or the server advertised no AUTH.

SmtpSession.send_mail

pub fn send_mail(self: *SmtpSession, mail: *Mail) -> !i32

Run MAIL FROM / RCPT TO (once per to + cc + bcc recipient) / DATA, then transmit mail.to_rfc5322() (dot-stuffed per RFC 5321 — any body line starting with . is doubled) terminated by \r\n.\r\n. Returns ok(0) on a 2xx final reply, otherwise err("smtp: <code> <text>").

SmtpSession.quit / SmtpSession.close

pub fn quit(self: *SmtpSession) -> !i32
pub fn close(self: *SmtpSession)

quit sends QUIT and expects a 221. close tears down the underlying TCP/TLS socket and frees the session; it is idempotent. Always pair them — quit is the polite protocol shutdown, close reclaims the resource.

import stdlib::mail::message::*;
import stdlib::mail::smtp::*;

fn build(i: i32) -> *Mail {
    let m: *Mail = Mail::new();
    m.from = "[email protected]";
    m.to.push("[email protected]");
    m.subject = "msg";
    m.body = "hello";
    return m;
}

fn main() -> i32 {
    let c: *SmtpClient = SmtpClient::new("smtp.example.com", 465);
    c.use_tls = true;

    let r: !*SmtpSession = c.connect();
    if !r.ok { println!(r.err); return 1; }
    let s: *SmtpSession = r.val;
    println!("host:", s.host);
    println!("advertised:", s.auth_mechs);

    for let i: i32 = 0; i < 3; i++ {
        let m: *Mail = build(i);
        let sr: !i32 = s.send_mail(m);
        if !sr.ok { println!(sr.err); }
    }
    let _q: !i32 = s.quit();
    s.close();
    return 0;
}

IMAP — reading & flagging

A minimal IMAP4rev1 (RFC 3501) client: tag-based LOGIN / SELECT / FETCH / STORE / SEARCH / APPEND / IDLE / LOGOUT over implicit TLS (993) or plain TCP (143). Every command gets a unique tag (A1, A2, …); the session reads untagged * … lines until the matching tagged An OK/NO/BAD line.

ImapSession (struct)

pub struct ImapSession {
    pub host: string,
    // tcp / tls / is_tls / rx_buf / tag_counter / idle_tag are private state
}

ImapResponse (struct)

pub struct ImapResponse {
    pub untagged: *Vector<string>,   // every "* ..." line for the command
    pub tagged:   string,            // the final tagged line ("A3 OK ...")
    pub literal:  string,            // captured {N} literal payload (e.g. FETCH body)
}

The structured result of a command. select, search, and fetch parse this for you; the struct is exposed so you can inspect raw lines if needed.

Connecting & authenticating

Method Signature Description
connect pub fn connect(host: string, port: i32, use_tls: bool) -> !*ImapSession Open a session. use_tls=true → implicit TLS (993); false → plain TCP (143). Validates the * OK banner.
login pub fn login(self: *ImapSession, user: string, pass: string) -> !i32 Authenticate with the LOGIN command. Most providers require an app password.
select pub fn select(self: *ImapSession, mailbox: string) -> !i32 Select a mailbox read-write; returns the message count from * N EXISTS.
logout pub fn logout(self: *ImapSession) -> !i32 Send LOGOUT to release server state.
close pub fn close(self: *ImapSession) Tear down the socket and free the session. Idempotent.

Reading & flagging

Method Signature Description
fetch pub fn fetch(self: *ImapSession, seq: i32) -> !string FETCH the full RFC 5322 body (BODY[]) for sequence number seq. Feed the result to parse_mail.
store_flag pub fn store_flag(self: *ImapSession, seq: i32, flag: string, add: bool) -> !i32 +FLAGS when add=true, -FLAGS when false. Common flags: \\Seen, \\Deleted, \\Flagged.
search pub fn search(self: *ImapSession, criteria: string) -> !*Vector<i32> Run SEARCH; criteria is appended verbatim (RFC 3501 §6.4.4). Returns matching sequence numbers.
search_unseen pub fn search_unseen(self: *ImapSession) -> !*Vector<i32> Convenience for search("UNSEEN").
search_from pub fn search_from(self: *ImapSession, addr: string) -> !*Vector<i32> Convenience for search("FROM \"<addr>\"") (substring match on the From header).
append pub fn append(self: *ImapSession, mailbox: string, flags: string, message: string) -> !i32 APPEND a raw RFC 5322 message into mailbox. flags is space-separated (no parens), "" for none.

search criteria are passed straight through, so you can combine keywords: "ALL", "UNSEEN", "SEEN", "FLAGGED", "DELETED", "RECENT", "FROM \"a@x\"", "SUBJECT \"meeting\"", "SINCE 1-Jan-2026", "BEFORE 31-Dec-2026", "LARGER 1024", "SMALLER 100000", "BODY \"keyword\"", "TEXT \"keyword\"", "FROM x SUBJECT y" (juxtaposition = AND), "OR FROM x FROM y", "NOT DELETED".

import stdlib::mail::message::*;
import stdlib::mail::imap::*;

fn main() -> i32 {
    let r: !*ImapSession = ImapSession::connect("imap.example.com", 993, true);
    if !r.ok { println!(r.err); return 1; }
    let p: *ImapSession = r.val;
    println!("host:", p.host);

    let lr: !i32 = p.login("[email protected]", "app-password");
    if !lr.ok { println!(lr.err); p.close(); return 1; }

    let count: !i32 = p.select("INBOX");
    if count.ok && count.val > 0 {
        let raw: !string = p.fetch(1);
        if raw.ok {
            let m: !*Mail = parse_mail(raw.val);
            if m.ok { println!("subject:", m.val.subject); }
        }
        let _f: !i32 = p.store_flag(1, "\\Seen", true);   // mark as read
    }

    let un: !*Vector<i32> = p.search_unseen();
    if un.ok { println!("unread:", un.val.len()); }

    let fr: !*Vector<i32> = p.search_from("[email protected]");
    if fr.ok { println!("from alice:", fr.val.len()); }

    let hits: !*Vector<i32> = p.search("UNSEEN SUBJECT \"alert\"");
    if hits.ok {
        for let i: i32 = 0; i < hits.val.len(); i++ {
            println!("msg #", hits.val.get(i));
        }
    }

    let _lo: !i32 = p.logout();
    p.close();
    return 0;
}

IDLE (push notifications) & APPEND

Method Signature Description
idle_start pub fn idle_start(self: *ImapSession) -> !i32 Enter IDLE (RFC 2177); the server starts pushing untagged updates until you stop.
idle_next pub fn idle_next(self: *ImapSession) -> !string Block for the next untagged line (e.g. * 5 EXISTS). Honours any socket timeout.
idle_stop pub fn idle_stop(self: *ImapSession) -> !i32 Send DONE and drain back to command mode. Errors if called without idle_start.

append writes a message into a mailbox without sending it — ideal for saving a copy to Sent after an SMTP send, or stashing a Drafts entry. Combine it with to_rfc5322 so the appended bytes match exactly what you would transmit:

import stdlib::mail::message::*;
import stdlib::mail::imap::*;

fn main() -> i32 {
    let r: !*ImapSession = ImapSession::connect("imap.example.com", 993, true);
    if !r.ok { println!(r.err); return 1; }
    let p: *ImapSession = r.val;
    let _l: !i32 = p.login("[email protected]", "pw");

    // APPEND a draft into the Drafts mailbox.
    let m: *Mail = Mail::new();
    m.from = "[email protected]";
    m.to.push("[email protected]");
    m.subject = "draft";
    m.body = "WIP";
    let _a: !i32 = p.append("Drafts", "\\Seen", m.to_rfc5322());

    // Wait for new mail, then return to command mode.
    let _is: !i32 = p.idle_start();
    while true {
        let line: !string = p.idle_next();
        if !line.ok { break; }
        if line.val.contains("EXISTS") { break; }   // new mail arrived
    }
    let _ie: !i32 = p.idle_stop();

    let _lo: !i32 = p.logout();
    p.close();
    return 0;
}

POP3 — single-mailbox retrieval

A POP3 (RFC 1939) client covering the eight standard commands: USER / PASS / STAT / LIST / RETR / DELE / NOOP / QUIT. Multi-line replies (LIST, RETR) terminate at a lone . line and are dot-unstuffed automatically.

Pop3MsgInfo (struct)

pub struct Pop3MsgInfo {
    pub idx:  i32,
    pub size: i32,
}

A per-message descriptor. For list, idx is the message number and size its octet count. For stat, the single returned value reuses the fields as (count, total_size_octets)idx is the message count and size the total byte count.

Pop3Session (struct)

pub struct Pop3Session {
    pub host: string,
    // tcp / tls / is_tls / rx_buf are private transport state
}

Methods

Method Signature Description
connect pub fn connect(host: string, port: i32, use_tls: bool) -> !*Pop3Session Open a session. use_tls=true → TLS from start (995); false → plain TCP (110). Validates the +OK banner.
login pub fn login(self: *Pop3Session, user: string, pass: string) -> !i32 Authenticate via USER + PASS.
stat pub fn stat(self: *Pop3Session) -> !*Pop3MsgInfo STAT: returns (count, total_size) packed into idx / size.
list pub fn list(self: *Pop3Session) -> !*Vector<*Pop3MsgInfo> LIST: one Pop3MsgInfo per message (idx, size).
retrieve pub fn retrieve(self: *Pop3Session, n: i32) -> !string RETR: full RFC 5322 body for message n (dot-unstuffed).
delete pub fn delete(self: *Pop3Session, n: i32) -> !i32 DELE: mark message n for deletion (committed at quit).
noop pub fn noop(self: *Pop3Session) -> !i32 NOOP keep-alive to prevent idle disconnects.
quit pub fn quit(self: *Pop3Session) -> !i32 QUIT and commit pending deletions.
close pub fn close(self: *Pop3Session) Tear down the socket. Idempotent.
import stdlib::mail::message::*;
import stdlib::mail::pop3::*;

fn main() -> i32 {
    let r: !*Pop3Session = Pop3Session::connect("pop.example.com", 995, true);
    if !r.ok { println!(r.err); return 1; }
    let p: *Pop3Session = r.val;
    println!("host:", p.host);

    let _l: !i32 = p.login("[email protected]", "app-password");

    let st: !*Pop3MsgInfo = p.stat();
    if st.ok { println!(st.val.idx, "msgs,", st.val.size, "bytes"); }

    let lst: !*Vector<*Pop3MsgInfo> = p.list();
    if lst.ok {
        for let i: i32 = 0; i < lst.val.len(); i++ {
            let info: *Pop3MsgInfo = lst.val.get(i);
            println!("#", info.idx, "size", info.size);
            let raw: !string = p.retrieve(info.idx);
            if raw.ok {
                let m: !*Mail = parse_mail(raw.val);
                if m.ok { println!("subject:", m.val.subject); }
            }
        }
    }

    let _d: !i32 = p.delete(1);   // committed at quit
    let _n: !i32 = p.noop();
    let _q: !i32 = p.quit();
    p.close();
    return 0;
}

See also

  • net::tcp / net::tls — the transport every mail session is built on; timeout_ms and tls_insecure map onto stream-level controls there.
  • base64 — used internally for attachment encoding and SMTP AUTH PLAIN/LOGIN tokens.
  • timeMail.to_rfc5322 uses it to auto-fill the Date header.