# openai-authcode **Repository Path**: fysics_wjw/openai-authcode ## Basic Information - **Project Name**: openai-authcode - **Description**: No description available - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2026-08-07 - **Last Updated**: 2026-08-07 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # openai-authcode A small Go service that keeps an IMAP connection parked in `IDLE`, extracts the verification code out of every matching email the instant it arrives, and pushes it to anything listening — a browser dashboard, a shell script, or a webhook. Provider-agnostic: Gmail, Outlook/Microsoft 365, Fastmail, iCloud, Yahoo and self-hosted Dovecot all work. The sender and the code pattern are just regexes, so it is not tied to any one service. Run one instance per mailbox. ``` ┌──────────────────────────────────────────────┐ any IMAP server │ container │ ──── IMAP ───────►│ IDLE watcher ──► MIME decode ──► regex │ IDLE push │ │ │ │ │ └────────► hub (fan-out) ◄───┘ │ │ │ │ │ ┌────────────┼────────────┐ │ │ SSE stream JSON API webhook │ └────────┼────────────┼────────────┼───────────┘ ▼ ▼ ▼ dashboard scripts your service ``` A Feishu/Lark bot that fronts several instances at once — chat commands plus push to a person or a group — lives in its own repo: [perd-sub2api-bot](https://gitee.com/fysics_wjw/perd-sub2api-bot). ## What "pushes events" means here The push mechanism is **SSE — Server-Sent Events**, the browser `EventSource` API. It is one-way server→browser over ordinary HTTP/1.1, reconnects on its own, and replays what was missed via `Last-Event-ID`. WebSockets would add a handshake and framing for a channel that never needs to carry traffic in the other direction. ## Quick start The compose file runs two mailboxes. Give each its own env file: ```bash cp .env.example .env.personal && cp .env.example .env.work ``` Fill in `IMAP_HOST`, `IMAP_USER` and a credential in each, then: ```bash docker compose up -d --build ``` Running a single mailbox? Delete the `authcode-work` service. Uncomment a service's `ports:` block to reach its dashboard on . ## Choosing an auth mode | Provider | `IMAP_HOST` | `IMAP_AUTH` | Credential | | --- | --- | --- | --- | | Gmail | `imap.gmail.com` | `plain` | App password (needs 2-Step Verification) | | Outlook / M365 | `outlook.office365.com` | `xoauth2` | OAuth2 — see below | | Fastmail | `imap.fastmail.com` | `plain` | App password | | iCloud | `imap.mail.me.com` | `plain` | App-specific password | | Yahoo | `imap.mail.yahoo.com` | `plain` | App password | | Self-hosted | your host | `plain` | Account password | `plain` is the easy path and covers everything except Microsoft. ### Outlook needs OAuth2, not a password Microsoft **disabled IMAP basic authentication — including app passwords — for personal Outlook.com accounts in September 2024**, and for Microsoft 365 tenants before that. IMAP itself still works; the only credential it accepts is an OAuth2 access token presented over SASL `XOAUTH2`. Any guide telling you to generate an Outlook app password and call `client.Login()` is out of date. Setting it up once: 1. Register a free app at → **App registrations**. - Supported account types: **Personal Microsoft accounts only** - **Authentication → Advanced → Allow public client flows: Yes** 2. Put the Application (client) ID in that account's env file as `OAUTH_CLIENT_ID`, and set `IMAP_AUTH=xoauth2`. 3. Run `docker compose run --rm authcode-personal -devicecode`, open the URL it prints, and approve. The refresh token is written to `/data/token.json` on a named volume. Providers rotate refresh tokens on every use, so the file — not the environment variable — is the source of truth after the first refresh. Adding another OAuth2 provider means adding one case to `Lookup` in [internal/xoauth2/xoauth2.go](internal/xoauth2/xoauth2.go). Google is absent on purpose: Gmail still accepts app passwords, so `IMAP_AUTH=plain` covers it with no OAuth2 setup at all. ## Endpoints | Endpoint | Purpose | | --- | --- | | `GET /` | Dashboard. Live codes, one-click copy, connection state. | | `GET /events` | SSE stream. Events: `code`, `mail`, `status`, `app_error`. | | `GET /api/wait?timeout=2m` | **Blocks** until the next code, then returns it. `408` on timeout. | | `GET /api/latest` | Most recent code as JSON, `404` if none yet. | | `GET /api/events?after=N` | Buffered event history. | | `GET /healthz` | `200` when the IMAP session is up, `503` otherwise. Never requires auth. | ### From a script `/api/wait` is the one you want in a login automation — it holds the connection open instead of hammering the server: ```bash curl -s "http://127.0.0.1:8080/api/wait?timeout=120s" | jq -r .code ``` ### From a browser ```js const es = new EventSource("http://127.0.0.1:8080/events"); es.addEventListener("code", (e) => console.log(JSON.parse(e.data).code)); ``` Note the listener is on `code`, not `message` — named SSE events do not reach `onmessage`. The app's own failures arrive as `app_error` rather than `error`, because `EventSource` already fires a built-in `error` event on connection loss and the two would otherwise be indistinguishable. ## Configuration Everything is environment variables; see [.env.example](.env.example) for the full annotated list. The ones that matter most: | Variable | Default | Notes | | --- | --- | --- | | `IMAP_HOST` | — | Required. No default, so it never guesses a vendor. | | `IMAP_PORT` | `993` | | | `IMAP_USER` | — | Required. | | `IMAP_AUTH` | `plain` | `plain` or `xoauth2`. | | `IMAP_TLS` | `implicit` | `implicit` / `starttls` / `none`. | | `CODE_REGEX` | `(?i)code[^0-9]{0,80}?(\d{6})\b` | Group 1 wins if present. | | `FROM_FILTER` | — | Regex on `From`. **Set this.** | | `API_TOKEN` | — | Required unless bound to loopback. | | `WEBHOOK_URL` | — | Optional outbound `POST` per code. | | `LOOKBACK` | `10m` | Startup catch-up window. | | `MARK_SEEN` | `true` | `false` opens the mailbox read-only. | The default `CODE_REGEX` anchors on the word "code" so that order numbers and tracking IDs elsewhere in the email do not match. Loosen it to `\b(\d{6})\b` if a sender words things unusually. ## Security This service holds mailbox credentials and serves live verification codes, so the defaults are deliberately closed: - The compose file **publishes no ports at all** by default; instances are reachable only on the Docker network. The commented `ports:` blocks bind **`127.0.0.1`**, not `0.0.0.0`. Exposing the port more widely without `API_TOKEN` and TLS hands your login codes to anyone who can reach it — the service logs a warning if you do. - The container runs as **non-root (65532)** with a **read-only root filesystem** and all capabilities dropped. Only `/data` is writable. - `API_TOKEN` is accepted as `Authorization: Bearer …` or `?token=…`; the query form exists because `EventSource` cannot set headers, so prefer the header anywhere the URL might be logged. Comparison is constant-time. - `/healthz` is intentionally unauthenticated — the container healthcheck needs it — and exposes only connection state, never codes. - An app password is a long-lived credential that grants full IMAP access to the whole mailbox and bypasses 2FA. Prefer a dedicated mailbox for this bot over pointing it at your primary one. - `.env*` is gitignored except the `*.example` files. Keep it that way. ## Development ```bash go test ./... ``` The suite runs a real in-memory IMAP server, so `IDLE` delivery, the startup catch-up scan, sender filtering, MIME/base64 decoding, SSE framing, `Last-Event-ID` replay and auth are all covered end to end without a mailbox. ```bash go test -race ./... docker build --network=host -t openai-authcode:latest . ``` `--network=host` is only needed when Docker injects a `127.0.0.1` proxy from `~/.docker/config.json` into build containers, where that address is the container's own loopback. `docker compose build` already sets it. For a fully offline build, run `go mod vendor` and drop the setting. ## Notes on the implementation A few things that differ from the usual snippet found online: - **The message is MIME-decoded before matching.** Fetching `BODY[TEXT]` and running a regex over the raw bytes fails the moment a sender uses base64 or quoted-printable transfer encoding — which OpenAI, Google and Microsoft all do. There is a test for exactly this. - **HTML bodies are rendered two ways**, once with tags replaced by spaces and once with tags removed entirely, so a code split across per-digit ``s still matches. - **IDLE restarts are handled by the library.** `go-imap/v2` re-issues `IDLE` every 28 minutes internally, so the common advice to wire up your own `time.Ticker` is unnecessary. `IDLE_REFRESH` here is a separate safety re-sync that also `NOOP`s and re-checks, in case a server swallows an `EXISTS` notification. - **`SEARCH` is issued without `RETURN (ALL)`.** go-imap emits that modifier whenever `SearchOptions.ReturnAll` is set without checking the capability, but it is only defined by RFC 4731 (ESEARCH) and RFC 9051 (IMAP4rev2). Plain `UID SEARCH` parses into the same UID set and works on every server. - **Messages are tracked by UID, not sequence number**, which is what keeps a concurrent expunge from shifting the bot onto the wrong message. - **Fetches use `BODY.PEEK[]`**, so reading a message never implicitly flags it `\Seen`. `MARK_SEEN` is the only thing that sets that flag.