# Email service — write read-status (`PATCH /sync/read-status/...`) Copy everything below the line into any LLM session (or hand it to whoever owns the email microservice) before implementing the write endpoint. --- You are extending the **email microservice** that already exposes the read-status delta and point-lookup APIs documented in `Sync_read.md`. Implement a **write** path that marks a message read (or unread) in the signed-in user's Outlook mailbox via Microsoft Graph. Mirror the existing `/sync/read-status/*` style exactly — same bearer auth, same `:path` id handling, same response shape. ## Why we need this The HR-ATS inbox app learns that a user opened a message before Outlook does. Today that signal dies in our database: we have no Graph write permission and the email service exposes no write endpoint. Without this PATCH, local mark-read and Outlook drift permanently (and a later delta can even revert our flag). ## Requested contract Mirror the existing read endpoints so both parse with one code path: ``` PATCH /sync/read-status/message/{id} Authorization: Bearer Content-Type: application/json { "isRead": true } ``` **200 response** — identical shape to `GET /sync/read-status/message/{id}`: ```jsonc { "id": "AAMk…", "isRead": true, "lastModifiedDateTime": "2026-08-07T10:14:52Z", "subject": "Invoice #421" } ``` Same `:path` converter for Graph ids that contain `/`, `+`, or `=`. Same bearer auth as every other `/sync/*` route. Answer `401` until device-code sign-in completes. ## Required behaviour - **Idempotent.** Re-PATCHing `isRead: true` when already true is a no-op `200` with the current record. - **Must not advance or disturb the delta cursor.** This is a point write, not a sync round. Cursor, watcher, and `/changes` buffer stay untouched. - **404 `ErrorItemNotFound`** for unknown or deleted ids (same as the GET). - **403 surfaced distinctly** if the Graph scope is missing, so callers can tell "not permitted" from "not found". ## Graph scope prerequisite Needs `Mail.ReadWrite`. The service currently signs in read-only. Treat upgrading the consent / device-code scopes as an explicit product decision before shipping the route — not an implementation footnote. ## Optional batch form For bulk reconcile without N round-trips: ``` PATCH /sync/read-status/messages { "ids": ["AAMk…", "AAMk…"], "isRead": true } ``` Return a list of the same per-message records (or per-id errors). Nice-to-have; the single-id PATCH is the hard requirement. ## What the caller will do with it HR-ATS will enqueue one Taskiq task per human mark-read, retried via existing smart-retry middleware. Expected volume is low (opens, not sweeps). After this lands we will stop treating local-only mark-read as a known divergence. ## Out of scope for this request - Changing the delta `/sync/read-status` contract - Push / Graph change-notification subscriptions - Writing any field other than `isRead`