Inbound: - Gmail poll query is:unread in:inbox (was category:primary, which matched nothing on a no-tabs Workspace inbox) Outbound email: - Closed/escalation auto-emails editable via TICKET_CLOSE_MESSAGE and new TICKET_ESCALATION_EMAIL_MESSAGE; drop the staff signature from closing emails - Replies quote the customer's latest message (gmail_quote markup so clients collapse it), embed custom emoji inline via CID attachment, and strip Discord role mentions - Tagline spacing fix in the company signature Discord side: - Suppress all mentions in log + transcript posts (no more pinging on close) - Drop the staff-role ping from new-ticket and follow-up notifications - Ticket channels inherit category permissions instead of setting per-channel overwrites (removes the Manage Roles requirement) Gmail folders: - Folder/label routing (gmailLabels.js) with /folder; close files to Complete Config: - Remove ~56 stale .env keys for long-removed features; refresh stale copy Docs: - Design specs for folder routing, email-flow toggle, and per-staff metrics
125 lines
7.0 KiB
Markdown
125 lines
7.0 KiB
Markdown
# 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 (`<n>`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.
|