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_msandtls_insecuremap onto stream-level controls there.base64— used internally for attachment encoding and SMTP AUTH PLAIN/LOGIN tokens.time—Mail.to_rfc5322uses it to auto-fill theDateheader.