IMAP (v4) client for the Cloudflare Workers platform. Do not try to run this on other runtimes, it will not work.
The CFImap class can be created in any part of the code, however it is advised to use the connect() function only in a request handler. That is because the Cloudflare Workers platform limits some functionality (mainly await) outside of handlers.
import { CFImap } from "cf-imap"
const imap = new CFImap({
host: "mail.example.com",
port: 993,
tls: true,
auth: {
username: "user@example.com",
password: "pa$$w0rd"
}
})
const handleRequest = async () => {
await imap.connect()
// ... use the imap instance
await imap.logout()
}// Namespaces (personal/other/shared prefixes and delimiters)
const { personal } = await imap.getNamespaces()
// Folders
const folders = await imap.getFolders(personal[0]?.prefix ?? "", "*")
// Select a folder (returns mailbox info: EXISTS, recent, uidNext, uidValidity, flags, ...)
const mailbox = await imap.selectFolder("INBOX")
// Search: returns sequence numbers (UIDs with useUid: true)
const ids = await imap.searchEmails({ subject: "hello", seen: false })
const uids = await imap.searchEmails({ all: true, useUid: true })
// Fetch: full messages with parsed MIME (body text/html, attachments), or headers-only
const emails = await imap.fetchEmails({ limit: [1, 10], fetchBody: true })
const headersOnly = await imap.fetchEmails({ limit: [1, 10], fetchBody: false })
// Flags, copy, move, expunge, status, append
await imap.storeFlags("1:5", ["Seen"], "add") // add | remove | replace
await imap.copy("INBOX/archive", "1:5") // returns COPYUID mapping when available
await imap.move("INBOX/archive", "1:5", true) // useUid
await imap.expunge({ range: "1:10", useUid: true })
const status = await imap.status("INBOX")
await imap.append("INBOX", rawMessage, ["Seen"]) // returns APPENDUID when available
// Mailbox lifecycle
await imap.examine("INBOX") // read-only select
await imap.closeMailbox() // CLOSE: expunges \Deleted then deselects
await imap.unselect() // UNSELECT: deselect without expunging
// IDLE: push updates; return false from the callback to stop
await imap.idle((item) => {
console.log(item.line) // "* 5 EXISTS", "* 2 EXPUNGE", ...
// return false to leave IDLE
})- TLS:
tls: trueon port 993 uses Implicit TLS (RFC 8314); other ports use theSTARTTLScommand (RFC 9051 §6.2.1) and re-issueCAPABILITYafter upgrading. - Authentication:
AUTHENTICATE PLAINwith a SASL initial response is tried first;LOGINis only used as a last resort and never when the server advertisesLOGINDISABLED(RFC 9051 §6.2.2/§6.2.3). - IMAP4rev2 negotiation:
ENABLE IMAP4rev2is issued automatically when a server advertises bothIMAP4rev1andIMAP4rev2(RFC 9051 Appendix A). - Search: parses both
* SEARCH(IMAP4rev1) and* ESEARCH(IMAP4rev2) responses; non-ASCII queries getCHARSET UTF-8. - Mailbox names: UTF-8 (Net-Unicode) on IMAP4rev2 sessions; automatically converted to/from modified UTF-7 (RFC 2152) on IMAP4rev1-only servers.
check()was removed from IMAP4rev2 and is refused on rev2 sessions — useNOOP/IDLE.- The deprecated
\Recent/NEW/OLD/RECENTsearch keys still work against IMAP4rev1 servers but are not part of IMAP4rev2.
{
uid, seq, flags, internalDate, size,
from: string[], to: string[], cc: string[],
subject, messageID, contentType,
headers: Record<string, string>, // decoded + unfolded
rawHeaders: string,
body: { text?: string, html?: string, raw: string },
attachments: [{ filename, mimeType, size, encoding, content, contentBase64, contentId?, isInline }],
raw: string
}Failed commands throw ImapError (an Error subclass with status, tag, messageText and untagged response items). Read timeouts and connection drops throw regular Errors. Configure the read timeout via the timeoutMs option (default 30000).
The logout() function (alias: close()) logs out of the session and closes the socket. It is recommended to use this as to not run the Worker needlessly (some providers auto-kick you after a while, some keep the connection open indefinitely).
await imap.logout()Documentation can be found here.