Keyboard shortcuts

Press or to navigate between chapters

Press ? to show this help

Press Esc to hide this help

Stdlib: Email & Http

Alpha (v0.1). Breaking changes expected.

External connections live in stdlib namespaces. Email handles IMAP/SMTP. Http handles HTTP. Db handles SQL. Each one dispatches through an interface so the backend is swappable.

Email

Default implementation uses imap (fetch) + lettre (send). v0.1 reads credentials from environment variables:

export IMAP_HOST=imap.gmail.com
export SMTP_HOST=smtp.gmail.com          # optional — defaults to IMAP host with `imap.` → `smtp.`
export EMAIL_USER=you@example.com
export EMAIL_PASS=app-password

If those aren’t set, Email.fetch returns [] and Email.send is a no-op (with a stderr warning), so programs keep running.

Fetch messages

emails = Email.fetch(unread: true)   # up to 20 most recent unread from INBOX

Each returned map has from, subject, body, unread keys.

Send messages

Email.send(reply, to: email.from)
Email.send(reply, to: address, subject: "Re: hello")

Positional body can be a str or a map with body (and optional subject). to: can be an address string or a map with from.

Archive Coming soon

Email.archive(email)

Status: v0.1 no-op placeholder. IMAP folder-move is tracked in ROADMAP.

Http

Default implementation wraps reqwest.

GET

response = Http.get("https://api.example.com/data")
# response: HttpResponse?

if response?.is_ok {
  users = response?.json_as[list[User]]() ?? []
}

POST

response = Http.post("https://api.example.com/v2/events",
  json: {kind: "email_processed", count: 42},
  headers: {Authorization: "Bearer {Env.require("API_KEY")}"}
)

Full request

response = Http.request(
  method: POST,
  url: "https://api.example.com/v2/classify",
  headers: {
    Authorization: "Bearer {Env.require("API_KEY")}",
    "Content-Type": "application/json"
  },
  body: {text: email.body},
  timeout: 10.seconds
)

Returns: HttpResponse? — see Types for the shape.

Db Coming soon

db = Db.connect("postgres://localhost/mydb")

rows = Db.query(db,
  "SELECT * FROM interactions WHERE contact = ? AND created_at > ?",
  params: [email.from, 30.days.ago]
)
# rows: list[dynamic]

Db.exec(db, "UPDATE status SET seen = true WHERE id = ?", params: [ticket.id])

Status: the Db namespace is documented for v0.1 but not registered in the runtime. Calls will raise an “unknown method” error until the SQL backend lands. Tracked in ROADMAP.

Swapping the backend Coming soon

Each namespace dispatches through an interface. To plug in a custom transport:

# In your startup
Email.install(MyProprietaryEmailTransport)
Http.install(MyRateLimitedClient)

Status: Email.install / Http.install are reserved but not registered in v0.1 — the default transports are the only ones wired.

See The Prelude & Interfaces for how interface dispatch works.

Why a library, not connect + fetch keywords

Dedicated connect X via Y { ... } or fetch X where Y grammar wouldn’t compose well: connect is really a struct literal, and fetch generalizes badly across connection types (email’s unread is not SQL’s where). Per-connector libraries give better autocomplete, clearer types, and zero language changes when a new connector ships.