Imap Class
Read email over IMAP4rev1 — select a mailbox, search and fetch server-side, with TLS by default and structured message parsing.
Imap connects to an IMAP server over implicit TLS (port 993) by default and authenticates. Unlike Pop3, IMAP is stateful and leaves mail on the server: you select() a mailbox, then search() / fetch() within it. Fetched messages are parsed into the same hash shape as Pop3, plus IMAP identity fields (seq, uid, flags).
Imap.new(host, user, password, opts?)
Imap.new(host, user, password, opts?)
Connects and authenticates, returning a client instance.
Parameters
host : String — IMAP server hostnameuser : String — Mailbox usernamepassword : String — Password or app passwordopts : Hash? — port (default 993), tls (default true)mail = Imap.new("imap.gmail.com", "me@gmail.com", "app-password")
# Plaintext on a custom port (e.g. a local test server)
mail = Imap.new("127.0.0.1", "user", "pass", { "port": 143, "tls": false })
Gmail / 2FA accounts: use an App Password, not your normal password, and enable IMAP access in your account settings.
Instance Methods
mail.select(mailbox = "INBOX") — selects a mailbox and returns its status: { "mailbox", "exists", "recent", "unseen", "uidvalidity", "uidnext", "flags" }. Subsequent calls operate on it.mail.mailboxes() — returns an array of { "name", "delimiter", "flags" } for every mailbox/folder.mail.search(criteria = "ALL") — returns an array of sequence numbers matching an IMAP search key.mail.uid_search(criteria = "ALL") — returns an array of UIDs matching an IMAP search key.mail.fetch(seq) — returns a parsed message hash for a sequence number (see below).mail.fetch_uid(uid) — returns a parsed message hash for a UID.mail.fetch_all() — returns parsed messages from the selected mailbox (capped at 200; raise via SOLI_IMAP_MAX_MESSAGES).mail.mark_seen(seq) / mail.mark_unseen(seq) — toggle the \Seen flag; return true.mail.delete(seq) — marks the message \Deleted (removed on expunge); returns true.mail.expunge() — permanently removes \Deleted messages; returns true.mail.copy(seq, mailbox) — copies a message into another mailbox; returns true.mail.move(seq, mailbox) — moves a message via the RFC 6851 MOVE extension; returns true.mail.logout() — closes the connection; returns true.fetch / fetch_uid use BODY.PEEK[], so reading a message does not mark it \Seen — call mark_seen() explicitly if you want that.
Search Criteria
criteria is passed straight through as an IMAP search key, so any standard expression works:
mail.uid_search("UNSEEN") # unread
mail.search("FROM alice@example.com") # by sender
mail.search("SINCE 1-Jun-2026 SUBJECT invoice") # combine keys
mail.search("ALL") # everything (the default)
Message Hash
Each fetched message is a hash carrying the same fields as Pop3 plus IMAP identity fields (seq, uid, flags). from is a single {name, address} hash (or null); to is an array of them. Missing headers/bodies are null.
{
"seq": 1,
"uid": 4821,
"flags": ["\\Seen", "\\Answered"],
"size": 2048,
"subject": "Hello from Alice",
"from": { "name": "Alice", "address": "alice@example.com" },
"to": [ { "name": "Bob", "address": "bob@example.com" } ],
"date": "2026-06-01T10:00:00Z",
"text_body": "Hi Bob, ...",
"html_body": "<p>Hi Bob, ...</p>",
"attachments": [ { "name": "report.pdf", "content_type": "application/pdf", "size": 51200 } ],
"raw": "From: Alice ..." # full RFC822 source
}
Complete Example
mail = Imap.new("imap.gmail.com", "me@gmail.com", "app-password")
info = mail.select("INBOX")
print("#{info["exists"]} messages, #{info["unseen"]} unread")
# Fetch and mark every unread message as read
for uid in mail.uid_search("UNSEEN")
msg = mail.fetch_uid(uid)
print("#{msg["date"]} — #{msg["from"]["address"]}: #{msg["subject"]}")
for attachment in msg["attachments"]
print(" attachment: #{attachment["name"]} (#{attachment["content_type"]})")
end
mail.mark_seen(msg["seq"])
end
mail.logout()