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
7.0 KiB
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:Undefined/absent →GMAIL_POLL_ENABLED: process.env.GMAIL_POLL_ENABLED !== 'false',true, so existing deployments keep polling with no.envchange required.services/configSchema.js— add'GMAIL_POLL_ENABLED'toALLOWED_CONFIG_KEYS. The existing/ENABLED$/rule ininferType()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:
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: aSlashCommandBuildernamedemailwith three subcommands (on,off,status),setDefaultMemberPermissions(PermissionFlagsBits.ManageMessages)to match sibling commands. - Dispatch — add
email: handleEmailtoCOMMAND_HANDLERSinhandlers/commands/index.js. Staff-gated automatically viarequireStaffRole()at the dispatcher entry. - Handler —
handleEmail(interaction):on:applyConfigUpdates({ GMAIL_POLL_ENABLED: true })(updates runtimeCONFIGand writes.env).- Clear the auth-suspend latch via
require('../../gmail-poll').setPollSuspended(false)so a priorinvalid_grantsuspend doesn't keep polling dead. If auth is still broken, the next cycle re-suspends and DMs admin, exactly as today. setGmailPollInterval(CONFIG.GMAIL_POLL_INTERVAL_MS)to start the live timer.- Reply (ephemeral): "Email flow is now on."
off:applyConfigUpdates({ GMAIL_POLL_ENABLED: false }).clearGmailPollInterval().- 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 / 1000s), and whether polling is currently suspended by an auth error.
- Report
- On
on/off, firelogTicketEvent('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." applyConfigUpdatesreturns{ applied, errors }; ifGMAIL_POLL_ENABLEDlands inerrors(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;.envcontainsGMAIL_POLL_ENABLED=false.- Restart container → no polling on boot;
/email statusreports off. /email on→ poll resumes (immediate cycle),.envflips totrue.- While OFF,
/gmailpoll 60→ interval saved, no polling starts. npm test(coversservices/configSchema.js);node --checkon every edited file.