# Email Flow Toggle — Design **Date:** 2026-06-03 **Status:** Approved (design); pending implementation plan ## Goal A staff slash command to turn the **inbound** email flow on and off at runtime, with the state surviving container restarts. - **ON** — Gmail polling runs as today: reads the inbox, creates/append ticket channels, runs all downstream features. - **OFF** — Polling stops entirely. The mailbox is **never read** (inbox untouched). No new tickets are created from email. - **Outbound is unaffected** in both states — ticket-close emails, Gmail replies, and notification emails still send when staff act on existing tickets. (Decision: "off" scopes to inbound polling only.) - **Persists across restarts** — the off-state is honored on the next boot. (Decision: persisted, not runtime-only.) Discord-originated tickets are independent of email polling and are unaffected by this toggle. ## Decisions (locked) | Decision | Choice | |----------|--------| | Scope of OFF | Inbound polling only; outbound email still sends | | Persistence | Persist to `.env` via existing config-persistence path; honored on boot | | Command shape | New dedicated `/email on\|off\|status` command (Approach A) | | Existing `/gmailpoll` | Guarded so it cannot silently re-enable polling while flow is OFF | Rejected: folding into `/gmailpoll` subcommands (needless churn to a working command); sentinel interval `/gmailpoll 0` (poor discoverability). ## Architecture Single source of truth: a new boolean config flag `GMAIL_POLL_ENABLED` (default **true**). The live poll timer (`gmailPollInterval` in `broccolini-discord.js`) is started/stopped to match the flag. ### 1. Config flag — `GMAIL_POLL_ENABLED` - **`config.js`** — add: ```js GMAIL_POLL_ENABLED: process.env.GMAIL_POLL_ENABLED !== 'false', ``` Undefined/absent → `true`, so existing deployments keep polling with no `.env` change required. - **`services/configSchema.js`** — add `'GMAIL_POLL_ENABLED'` to `ALLOWED_CONFIG_KEYS`. The existing `/ENABLED$/` rule in `inferType()` already classifies it as a boolean validator, so the settings site can also toggle it (bonus, no extra work). ### 2. Boot gate In `broccolini-discord.js` (`client.once('ready')`, currently ~lines 210-211), only start the poll when enabled: ```js if (CONFIG.GMAIL_POLL_ENABLED) { gmailPollInterval = trackInterval(setInterval(() => poll(client), CONFIG.GMAIL_POLL_INTERVAL_MS)); poll(client); } else { console.log('Gmail poll disabled by config (GMAIL_POLL_ENABLED=false)'); } ``` When disabled, `gmailPollInterval` stays `null` — no timer is registered in `activeIntervals`, nothing reads the inbox. ### 3. `/email` command - **Registration** — `commands/register.js`: a `SlashCommandBuilder` named `email` with three subcommands (`on`, `off`, `status`), `setDefaultMemberPermissions(PermissionFlagsBits.ManageMessages)` to match sibling commands. - **Dispatch** — add `email: handleEmail` to `COMMAND_HANDLERS` in `handlers/commands/index.js`. Staff-gated automatically via `requireStaffRole()` at the dispatcher entry. - **Handler** — `handleEmail(interaction)`: - `on`: 1. `applyConfigUpdates({ GMAIL_POLL_ENABLED: true })` (updates runtime `CONFIG` **and** writes `.env`). 2. Clear the auth-suspend latch via `require('../../gmail-poll').setPollSuspended(false)` so a prior `invalid_grant` suspend doesn't keep polling dead. If auth is still broken, the next cycle re-suspends and DMs admin, exactly as today. 3. `setGmailPollInterval(CONFIG.GMAIL_POLL_INTERVAL_MS)` to start the live timer. 4. Reply (ephemeral): "Email flow is now **on**." - `off`: 1. `applyConfigUpdates({ GMAIL_POLL_ENABLED: false })`. 2. `clearGmailPollInterval()`. 3. Reply (ephemeral): "Email flow is now **off** — the inbox will not be polled. Outbound emails still send." - `status`: - Report `CONFIG.GMAIL_POLL_ENABLED`, the current interval (`CONFIG.GMAIL_POLL_INTERVAL_MS / 1000`s), and whether polling is currently suspended by an auth error. - On `on`/`off`, fire `logTicketEvent('Email flow toggled', [...], interaction).catch(() => {})` — fire-and-forget, matching `/gmailpoll`. `applyConfigUpdates` is called in-process (the command runs inside the bot), reusing the same path the internal API uses — no HTTP round-trip. ### 4. Guard `/gmailpoll` against silent re-enable `handleGmailPoll` currently calls `setGmailPollInterval(ms)`, which *starts* the timer. While flow is OFF that would silently re-enable polling. Change it so that when `CONFIG.GMAIL_POLL_ENABLED` is false: - update the interval in memory only (`CONFIG.GMAIL_POLL_INTERVAL_MS = ms`) — matching `/gmailpoll`'s existing runtime-only model (it has never persisted to `.env`), but - do **not** start the live timer, and - reply: "Interval saved (``s), but the inbound email flow is currently **off** — it will apply when you run `/email on`." When flow is ON, `/gmailpoll` behaves exactly as today. ## Data flow ``` /email off ──> applyConfigUpdates({GMAIL_POLL_ENABLED:false}) ──> CONFIG updated + .env written └─> clearGmailPollInterval() ──> live timer stopped, gmailPollInterval=null (no inbox reads) restart ──> config.js reads GMAIL_POLL_ENABLED=false ──> ready gate skips poll start ──> stays off /email on ──> applyConfigUpdates({GMAIL_POLL_ENABLED:true}) ──> CONFIG updated + .env written ├─> setPollSuspended(false) ──> clear prior auth-suspend latch └─> setGmailPollInterval(interval) ──> live timer started, immediate poll ``` ## Error handling - Command runs through `runHandler`, so any throw is logged and the user gets an ephemeral "Something went wrong." - `applyConfigUpdates` returns `{ applied, errors }`; if `GMAIL_POLL_ENABLED` lands in `errors` (should not, given the boolean validator), reply with the error rather than claiming success, and do not flip the live timer. - Logging stays fire-and-forget (`.catch(() => {})`), per Hard Rule 4. ## Files touched | File | Change | |------|--------| | `config.js` | Add `GMAIL_POLL_ENABLED` (default true) | | `services/configSchema.js` | Add `'GMAIL_POLL_ENABLED'` to `ALLOWED_CONFIG_KEYS` | | `broccolini-discord.js` | Gate poll start in `ready` on the flag | | `commands/register.js` | Register `/email on\|off\|status` | | `handlers/commands/index.js` | Add `handleEmail`; guard `handleGmailPoll` | | `.env.example` | Document `GMAIL_POLL_ENABLED` (optional, default true) | No DB schema changes. No destructive data ops. The mailbox is never read while OFF and is never written by this feature. ## Verification - `/email off` → logs show no further poll cycles; `.env` contains `GMAIL_POLL_ENABLED=false`. - Restart container → no polling on boot; `/email status` reports off. - `/email on` → poll resumes (immediate cycle), `.env` flips to `true`. - While OFF, `/gmailpoll 60` → interval saved, no polling starts. - `npm test` (covers `services/configSchema.js`); `node --check` on every edited file.