{"id":51676257,"url":"https://github.com/hstern/go-mailbox-720","last_synced_at":"2026-07-15T06:30:53.410Z","repository":{"id":365974808,"uuid":"1274572135","full_name":"hstern/go-mailbox-720","owner":"hstern","description":"Open-source Go server implementing the mailbox slice of the Microsoft Graph API (early WIP)","archived":false,"fork":false,"pushed_at":"2026-07-03T00:49:25.000Z","size":858,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-07-03T02:24:30.731Z","etag":null,"topics":["email","go","golang","graph-api","mailbox","microsoft-graph","server"],"latest_commit_sha":null,"homepage":null,"language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/hstern.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-06-19T16:49:57.000Z","updated_at":"2026-07-03T00:49:06.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/hstern/go-mailbox-720","commit_stats":null,"previous_names":["hstern/go-mailbox-720"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/hstern/go-mailbox-720","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hstern%2Fgo-mailbox-720","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hstern%2Fgo-mailbox-720/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hstern%2Fgo-mailbox-720/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hstern%2Fgo-mailbox-720/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/hstern","download_url":"https://codeload.github.com/hstern/go-mailbox-720/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hstern%2Fgo-mailbox-720/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":35494971,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-07-15T02:00:06.706Z","response_time":131,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["email","go","golang","graph-api","mailbox","microsoft-graph","server"],"created_at":"2026-07-15T06:30:52.628Z","updated_at":"2026-07-15T06:30:53.401Z","avatar_url":"https://github.com/hstern.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"# go-mailbox-720\n\nAn open-source Go server that implements the **mailbox slice of the Microsoft\nGraph API** — so a self-hosted mail/calendar/contacts backend can answer Graph\nclients.\n\n\u003e **Status: working, still maturing.** Reads and writes for mail, calendar, and\n\u003e contacts are implemented over real IMAP / CalDAV / CardDAV backends, behind\n\u003e OIDC, with `$filter`, `$batch`, delta sync, and change-notification\n\u003e subscriptions. The iTIP/iMIP scheduling engine and several protocol corners are\n\u003e built but not yet fully wired (see [Status detail](#status-detail)). Without a\n\u003e backend configured, an operation returns a Graph `notImplemented` (501).\n\n\"Microsoft Graph\" and \"Microsoft\" are trademarks of Microsoft Corporation, used\nhere descriptively (nominative fair use) to say what this software is compatible\nwith. This project is not affiliated with or endorsed by Microsoft.\n\n## What it is\n\n- **Server only, no client.** The client side is already solved by Microsoft's\n  official, Kiota-generated [`msgraph-sdk-go`](https://github.com/microsoftgraph/msgraph-sdk-go),\n  which also doubles as the conformance harness.\n- **Scope: the Exchange/mailbox slice** — `messages`, `mailFolders`, `events`,\n  `calendars`, `contacts`, under `/me` and `/users/{id}`. Everything else in\n  Graph (Teams, SharePoint, OneDrive, directory, …) is out of scope.\n- **Bring your own backends.** Mail maps onto **IMAP**, calendar onto **CalDAV**,\n  contacts onto **CardDAV** — your existing self-hosted servers (e.g. Dovecot +\n  Radicale). Backend-neutral ports mean a JMAP adapter can drop in later.\n- **OIDC resource server.** Bring your own external IdP (Keycloak, Authentik,\n  Dex, Entra, Kanidm); the server validates bearer tokens and does not issue them.\n\n## Implemented\n\n| Area | Operations |\n| --- | --- |\n| Mail (IMAP) | `GET /me/messages` (with `$filter` → IMAP SEARCH), `GET /me/mailFolders`, `GET /me/messages/{id}`, `PATCH` (isRead) + `DELETE`, `GET /me/messages/delta` (incremental sync) |\n| Calendar (CalDAV) | `GET /me/events`, `GET /me/calendars`, `POST` / `PATCH` / `DELETE /me/events`, `GET /me/events/delta`, `POST /me/events/{id}/accept` / `decline` / `tentativelyAccept` (iMIP reply) |\n| Contacts (CardDAV) | `GET /me/contacts`, `POST` / `PATCH` / `DELETE /me/contacts`, `GET /me/contacts/delta` |\n| Protocol | `POST /$batch` (JSON batching), `POST` / `GET` / `DELETE /v1.0/subscriptions` (change notifications, with IMAP-IDLE-driven delivery) |\n| Auth | JWT (JWKS) + opaque-token (RFC 7662 introspection) validation, RFC 9493 subject identity |\n\nEvent times accept UTC, RFC3339 offsets, and Windows zone names (e.g. `Pacific\nStandard Time`), resolved to the correct instant. Secrets (IMAP/CalDAV/CardDAV\npasswords, the introspection client secret) are read from the environment, never\nflags, so they stay out of the process table.\n\n## Status detail\n\nBuilt but not yet fully wired, or deliberately first-cut:\n\n- **iTIP/iMIP scheduling** (`internal/scheduling`, `internal/itip`, `internal/smtp`,\n  `internal/schedrun`): the full engine (parse, reply, request/cancel, iMIP email\n  composition) plus both directions wired — `POST /me/events/{id}/accept|decline|\n  tentativelyAccept` email an iMIP reply to the organizer (needs `-smtp-addr` +\n  `-mailbox-email`), and an opt-in inbound trigger loop (`-enable-scheduling`) turns\n  REQUEST mail into tentative calendar events. Both honor the RFC 6638\n  `calendar-auto-schedule` capability switch: when the CalDAV server schedules\n  natively the trigger stands down, and accept/decline record the responder's\n  PARTSTAT via CalDAV (the server sends the reply) instead of emailing. Still open:\n  recording `responseStatus` locally on the storage-only (email) path, and an\n  end-to-end test against a real RFC 6638 server (the Docker matrix has none yet).\n- **Delta**: all three delta endpoints (`/me/messages`, `/me/events`,\n  `/me/contacts`) report created/updated items **and** `@removed` tombstones for\n  deletions. Mail uses IMAP **CONDSTORE/QRESYNC** (RFC 7162) — `CHANGEDSINCE` for\n  new + flag/read-state changes, `VANISHED` for expungements — and **requires a\n  QRESYNC-capable server** (it returns 501 otherwise, rather than silently\n  degrading). Calendar/contacts use CalDAV/CardDAV **sync-collection** (RFC 6578).\n  Mail delta consumes QRESYNC client support from a go-imap fork via a `go.mod`\n  replace, pending upstream [emersion/go-imap#757](https://github.com/emersion/go-imap/pull/757).\n- **Subscriptions**: single-tenant in-memory store (per-identity keying is a\n  prerequisite before multi-mailbox use); notification delivery is created-only.\n- **Update PATCH** of events/contacts merges provided fields; partial collection\n  semantics are best-effort.\n\n## Repository hygiene: no Microsoft IP in git\n\nThe Microsoft Graph OpenAPI spec, the pruned subset derived from it, and the\n[`ogen`](https://github.com/ogen-go/ogen)-generated code (a derivative of the\nspec) are **fetched and generated at build time, never committed.** This is why\nyou must run `go generate` before building (below), and it deliberately\noverrides the usual Go convention of committing `go generate` output.\n\n## Build \u0026 run\n\nThe generated API package is produced from the upstream spec at build time, so\ngenerate it first (this fetches the ~30 MB Graph OpenAPI document and runs\n`ogen` — it needs network access):\n\n```sh\ngo generate ./internal/graph\ngo build ./...\ngo test ./...\n```\n\nSome integration tests need Docker and are behind a `dockertest` build tag so the\ndefault `go test ./...` stays fast. They run the adapters against real servers:\n\n```sh\ngo test -tags dockertest ./internal/mail/imap/        # IMAP + delta + IDLE vs Dovecot\ngo test -tags dockertest ./internal/calendar/caldav/  # CalDAV read/write vs Radicale\n```\n\nRun the server against your backends (auth disabled here — local experimentation\nonly). With a backend configured the routes return real data:\n\n```sh\nMAILBOXD_IMAP_PASSWORD=… MAILBOXD_CALDAV_PASSWORD=… MAILBOXD_CARDDAV_PASSWORD=… \\\ngo run ./cmd/mailboxd -addr :8080 \\\n  -mail-imap-addr imap.example.com:993 -mail-imap-username alice \\\n  -cal-caldav-url https://dav.example.com/ -cal-caldav-username alice \\\n  -contacts-carddav-url https://dav.example.com/ -contacts-carddav-username alice\ncurl -i http://localhost:8080/v1.0/me/messages   # 200 with the inbox\n```\n\nRun with OIDC enforced (the production posture):\n\n```sh\ngo run ./cmd/mailboxd \\\n  -addr :8080 \\\n  -auth-issuer https://idp.example.com/realms/main \\\n  -auth-audience mailbox-api \\\n  -auth-scope Mail.Read \\\n  -auth-subject-claim sub\n```\n\n| Flag | Meaning |\n| --- | --- |\n| `-auth-issuer` | Comma-separated trusted OIDC issuer URL(s). Empty disables auth. |\n| `-auth-audience` | Expected token audience (`aud`). |\n| `-auth-scope` | Comma-separated required scopes. |\n| `-auth-scope-claims` | Comma-separated claims that carry granted scopes (default `scope,roles`; Microsoft Entra: `scope,scp,roles`). |\n| `-auth-subject-claim` | Token claim mapped to the mailbox identity (default `sub`). |\n| `-auth-introspect-client-id` | OAuth2 client id enabling RFC 7662 introspection of **opaque** tokens (secret via `MAILBOXD_INTROSPECT_CLIENT_SECRET`). |\n| `-auth-resource` | This resource's identifier URL (RFC 8707); when set, publishes RFC 9728 protected-resource metadata at `/.well-known/oauth-protected-resource`. |\n| `-mail-imap-addr` / `-mail-imap-username` / `-mail-imap-tls` | IMAP mail backend (password via `MAILBOXD_IMAP_PASSWORD`). |\n| `-mail-managesieve-addr` / `-mail-managesieve-starttls` | Inbox rules (mail filters) over ManageSieve (RFC 5804) on the IMAP tier; reuses the IMAP credentials. STARTTLS on by default — disabling it sends the password in the clear. Empty: `messageRules` operations return 501. |\n| `-cal-caldav-url` / `-cal-caldav-username` | CalDAV calendar backend (password via `MAILBOXD_CALDAV_PASSWORD`). |\n| `-contacts-carddav-url` / `-contacts-carddav-username` | CardDAV contacts backend (password via `MAILBOXD_CARDDAV_PASSWORD`). |\n\nJWT bearer tokens are validated locally: the JWS is verified against the issuer's\nJWKS (signature, the issuer's discovery-advertised algorithms, audience, expiry) —\nthe access-control gate. The token's `typ` then selects how strictly it is decoded:\na token that declares itself an **RFC 9068** access token (`typ=at+jwt`) is held to\nthat profile (the §2.2 required claims, incl. `jti`/`client_id`), while a plain\n`typ=JWT` token — as Microsoft Entra and many IdPs issue — is accepted on signature\n+ audience like any bearer JWT. Granted scopes are read from the claims named by\n`-auth-scope-claims` (default `scope` — the RFC 8693 §4.2 / RFC 9068 §2.2.3 claim —\nplus app `roles`). **Microsoft Entra / Azure AD** does not use the standard claim:\nit carries delegated permissions in a non-standard `scp` claim, so an Entra-fronted\ndeployment should set `-auth-scope-claims scope,scp,roles` (`scp` is opt-in because\nit is not an RFC- or IANA-registered claim). Opaque access tokens (e.g. Kanidm's\ndefault) are validated via **RFC 7662**\nintrospection when `-auth-introspect-client-id` is set; for introspected tokens the\naudience is enforced when the introspection response carries an `aud` (otherwise the\nresource server's own introspection credentials are the binding). The mailbox\nidentity is an RFC 9493 `iss_sub` Subject Identifier (issuer + subject), so\nmailboxes stay distinct across multiple issuers. (JWT decode/validation uses\n`hstern/go-access-tokens`; introspection uses `hstern/go-token-introspection`.)\n\nWith one or more issuers configured the server fails closed: it refuses to start\nif an issuer cannot be discovered, and rejects any request without a valid token.\nA rejection carries an **RFC 6750** `WWW-Authenticate: Bearer` challenge\n(`invalid_token` / `insufficient_scope`), and — with `-auth-resource` set — the\nserver publishes **RFC 9728** protected-resource metadata at\n`/.well-known/oauth-protected-resource` so a client can discover the trusted issuers\nand scopes; the challenge then points at it via the §5.1 `resource_metadata`\nparameter. (The challenge is built with `hstern/go-bearer-token`, the metadata served\nand linked by `hstern/go-protected-resource-metadata`.)\n\n## Layout\n\n| Path | Purpose |\n| --- | --- |\n| `cmd/gen-graph-api`, `internal/specsubset` | Build-time codegen: fetch spec → subset → `ogen`. |\n| `internal/graph` | Hosts the `go generate` directive; `internal/graph/api` is the generated (git-excluded) package. |\n| `internal/server` | HTTP server implementing the generated handler (mail/calendar/contacts/delta/batch handlers + Graph↔neutral mapping). |\n| `internal/auth`, `internal/grapherr` | OIDC resource-server middleware; Graph error-object shape. |\n| `internal/odata`, `internal/batch` | `$filter` parsing/translation; `$batch` JSON batching. |\n| `internal/mail` + `internal/mail/imap` | Mail port + IMAP adapter (read/write/delta/IDLE). |\n| `internal/calendar` + `…/caldav` | Calendar port + CalDAV adapter (read/write). |\n| `internal/contacts` + `…/carddav` | Contacts port + CardDAV adapter (read/write). |\n| `internal/subscriptions`, `internal/notify` | Change-notification model/store/validation/delivery; IDLE→delta→deliver loop. |\n| `internal/scheduling`, `internal/itip`, `internal/smtp` | iTIP/iMIP engine, scheduling orchestration, SMTP send port. |\n| `internal/tz` | Graph (Windows/IANA) time-zone resolution. |\n| `cmd/mailboxd` | The server binary. |\n| `test/conformance` | Black-box conformance test driving mailboxd with the official `msgraph-sdk-go` client (separate module). |\n| `test/e2e-oidc` | End-to-end: an IdP matrix (real Kanidm + Zitadel) + Dovecot + Radicale (Docker) — an authenticated client reads mail and calendar through mailboxd. Impersonation-capable tests are gated to IdPs that support RFC 8693 user impersonation (Zitadel; Kanidm excluded). Separate module. |\n\n## License\n\n[Apache License 2.0](LICENSE).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fhstern%2Fgo-mailbox-720","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fhstern%2Fgo-mailbox-720","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fhstern%2Fgo-mailbox-720/lists"}