ADR-0048: Gmail over IMAP as a separate component (kampong/gmail-imap)¶
Status: accepted (KAM-122)
Context¶
support-triage has to read an inbox and leave a reply for a person to send. kampong/gmail (ADR-0030) only
sends, over the Gmail REST API with an OAuth token, which a developer running locally rarely has. The planning
decision (SLICES.md V11, F6) was IMAP with an app password for now, and the Gmail REST API with OAuth as the
later, managed path.
Decision¶
- A new component, not new ops on
kampong/gmail. The two have different credentials (an address and an app password, against a bearer token), a different host (imap.gmail.com, againstgmail.googleapis.com) and a different transport. Folding them together would widenkampong/gmail's permissions for every spec that only sends.kampong/[email protected]is akind: module: IMAP is a stateful socket protocol, not a request template. - Ops:
check_login(the credential probe),list_unseen,get_message,create_draft,mark_seen. Each op connects, does its work and logs out; there is no connection kept between steps. - Nothing is marked read implicitly.
list_unseenandget_messageopen the mailbox read-only and fetch withBODY.PEEK, so a run that fails after reading leaves the message unread and a retry sees it again.mark_seenis an explicit step, placed after the work is done. - Drafts are found by the
\Draftsspecial-use flag, never by name, so the account's language does not matter (Drafts,Entwürfe,[Gmail]/Drafts). An account without the flag is an error rather than a guess. A draft is plain text, base64 encoded, and nothing is sent. Withreply_to_uidit is threaded:In-Reply-Tois the original's Message-ID,Referencesis the original's references plus that id, andtoandsubjectdefault from the original (Reply-ToorFrom, andRe: ...). - Header values are one line.
to,cc,subject,in_reply_toandreferencesrefuse a line break, askampong/gmaildoes, so a value taken from an email cannot add aBccheader. Addresses are checked for shape. - Credentials are two secret slots,
user(GMAIL_ADDRESS) andpassword(GMAIL_APP_PASSWORD), both bound toimap.gmail.com:993. Because both are secrets, a recorded fixture redacts the address as well as the password, so a replayed message shows[REDACTED]where the account's own address was. A refused login carriesstatus: 401sokampong doctor --probecan tell it from an unreachable server. - Dependencies:
imapflowandmailparser, pinned exactly in the manifest'sdepsand inpackages/engine/package.json. The versions are the newest more than two weeks old when this was written (both libraries release almost daily), not the newest.
What is not enforced¶
The module's sockets are opened by imapflow, not by ctx.fetch, so the runtime cannot refuse a host. The
manifest declares egress: [imap.gmail.com:993] and the host is a constant in the code (never read from input), but
that is a statement a reviewer can hold the module to, not a wall (ADR-0031 points 3 and 4). The module still
receives its secrets only through ctx.secrets. A sandboxed runner would close this gap for every module at
once, which is why it stays the open item in ADR-0031.
Consequences¶
- Not verified against a live account. The tests drive the module with a scripted
imapflowclient and parse the MIME it builds with the realmailparser; the library's method names, options andPEEKbehaviour were checked against its shipped type definitions and source. A live smoke test with an app password is still owed, as it is for the other first-party connectors. - An export that uses the component installs the two dependencies (
buildPackageJsonadds a module'sdeps). - The hosted server does not run modules without a sandboxed runner (ADR-0031 point 5), so this is local and export only for now.
- Gmail's REST API with OAuth remains the managed path; it would be a later version of
kampong/gmailor a sibling, with its own credentials.