Repository navigation
EN Bridge Connector Email
Français · Type: how-to + reference · Audience: admin · Context: Reality Bridge · Status: ready · Applies from: 4.2.5
The connectors (email, Telnet, SSH, Open Badges) · contract: experimental · verified by CI (support matrix of 4.4.0, 2026-10-23): Node 22 on Ubuntu (Telnet and SSH not on Windows) · automated checks: jobs
connectors,connector-archive-e2e; CI planreality; 3 test files · people's passes:connectors-real-securitynot run (#79) · known limits: none listed on this page
The email connector turns a letter into a signal: a player answers the game by email, and the words of the answer propose the signal the game declares for them. It reads messages from a provider's signed webhook or from a mailbox it polls over IMAP, and sends no reply. It is experimental, like every connector (Connectors overview).
A quest-n-click-connector email process that links a player by a pairing code sent as an email's subject, then
proposes letter.door (or whichever signal the game declares) for each message whose words match an answer, once
per message: a message delivered twice is one signal.
- A connector beside a running Bridge, from 4.1.9; this page applies from 4.2.5 (the
quest-n-click-connectorname). -
Verified on: the webhook mode, as in the steps below, was run on 23 October 2026 against a local Bridge of the
game
signals(macOS, Node 22.14, engine 4.4.0): a pairing letter202 {"status":"paired"}, an answer202 {"status":"accepted"}, the same answer again200 {"status":"duplicate"}. The IMAP mode was not replayed for this page; CI plays both modes from the release archive (npm run connector-archive-e2e), against a small IMAP server of its own, never a real provider.
-
A running Bridge (Install and run the Bridge), and a game whose
reality.connectors.emaildeclares its answers (Signals and the manifest):connectors: { email: { answers: [{ words: ['open', 'door'], signal: 'letter.door' }], otherwise: 'letter.unclear' } }
-
For the webhook mode: a provider (or a small adapter in front of it) able to post the raw message, signed as below. For the IMAP mode: a mailbox reachable over TLS, and its password in a file.
grant where the Bridge's folder is; the connector on the machine that receives the webhook or reaches the mailbox,
in a folder holding its configuration, token and secret (mode 600). npm run connector -- email … in the engine
repository, npx quest-n-click-connector email … with the package quest-n-click-connectors.
-
Grant the connector its token, for the email signals only, with pairing:
# in the engine repository (the Bridge's folder is .cache/bridge) npm run bridge -- grant --connector=gate-mail --source=email --signals=letter.door,letter.unclear --pair > email.token
-
Write the webhook's shared secret to a file (or, for IMAP, the mailbox's password):
# in the connector's folder node -e 'process.stdout.write(require("node:crypto").randomBytes(32).toString("hex"))' > webhook.secret chmod 600 webhook.secret
-
Write its configuration,
email.json. The webhook mode:{ "bridge": { "url": "http://127.0.0.1:8787/", "tokenFile": "email.token" }, "gameId": "signals", "health": { "port": 9302 }, "email": { "mode": "webhook", "domain": "game.example.org", "webhook": { "port": 8789, "secretFile": "webhook.secret" } } }Or the IMAP mode:
{ "bridge": { "url": "http://127.0.0.1:8787/", "tokenFile": "email.token" }, "gameId": "signals", "email": { "mode": "imap", "imap": { "host": "imap.example.org", "user": "gate@example.org", "passwordFile": "imap.password", "pollS": 60 } } }
-
Start it:
# in the engine repository npm run connector -- email --config email.jsonIt logs
{"event":"email.started","connector":"email","mode":"webhook"}.
-
Try the webhook with a signed post. The signature is
sha256=and the HMAC-SHA256, under the secret, of<timestamp>.<raw message>; the timestamp is Unix seconds, within five minutes of the connector's clock:# in the connector's folder, answer.eml holding a raw message (headers, a blank line, the text) node -e ' const { createHmac } = require("node:crypto"); const { readFileSync } = require("node:fs"); const body = readFileSync(process.argv[1]); const secret = readFileSync("webhook.secret", "utf8").trim(); const t = Math.floor(Date.now() / 1000); const signature = "sha256=" + createHmac("sha256", secret).update(`${t}.`).update(body).digest("hex"); fetch("http://127.0.0.1:8789/v1/inbound", { method: "POST", body, headers: { "X-Web-Scumm-Timestamp": String(t), "X-Web-Scumm-Signature": signature } }) .then(async (r) => console.log(r.status, await r.text())); ' answer.eml
A message whose subject is
PAIR ABCD2345(the code the game shows) links its sender:202 {"status":"paired"}. A later message from that sender saying "open the door":202 {"status":"accepted"}. The same message again:200 {"status":"duplicate"}.
-
GET http://127.0.0.1:9302/healthanswers 200{"connector":"email","ok":true,"detail":"webhook on port 8789",…}(or"mailbox polled");GET /metricscounts proposals, acceptances, duplicates, refusals and rejected inputs. - The Bridge logs
"event":"pairing.confirmed", then"event":"signal.accepted"with the signal.
| Symptom | Cause | Fix |
|---|---|---|
email: "mode" is "webhook" or "imap" at start |
No mode | Set email.mode
|
email: webhook mode needs "webhook.secretFile" / email: imap mode needs "imap.passwordFile"
|
The secret is missing | Name its file |
email: "imap.tls": false is allowed for a loopback host only (127.0.0.1, ::1, localhost) |
TLS turned off towards a remote server | Keep TLS |
401 {"error":"stale"}
|
The timestamp is missing or more than five minutes away | Send Unix seconds; check the clocks |
401 {"error":"signature"}
|
Another secret, or the body changed on the way | Sign the exact bytes posted |
422 {"error":"no-player"}
|
The sender sent no pairing code, and no recipient tag of domain names a player |
Pair first, or set domain
|
422 {"error":"attachment"} / "too-large" / "unreadable"
|
A message with an attachment, over 256 KB, or one the reader refused | Plain text, no attachment |
422 {"error":"pairing-pairing"}
|
The pairing code is unknown, expired, or confirmed already | A new code from the game |
429 {"error":"rate"}
|
Over 120 posts a minute from one address | Slow down |
- Webhook mode: nothing written; the message is read in memory and dropped.
- IMAP mode: flags in the mailbox. With
keepDays: 0(the default) a message is deleted as soon as its signal is accepted (or found accepted already); withkeepDays: N, after N days. A message refused for good is flagged (\Flagged) and kept for the operator. A message refused for a reason that may pass (the Bridge away or busy, a quota) stays unseen and is read again at the next poll.
Guarantee ·
bridge-email-unseen-when-away· referenced · scope self-checked by the author (AI), not reviewed A message whose signal could not reach the Bridge (the Bridge away) stays unseen and is read again by a later poll; one refused for good is flagged.
Stop the process (SIGTERM). Revoke its token on the Bridge (revoke --token=<revocation id>) if it leaked, and grant
a new one. Nothing in the game depends on the connector: a game with letter.door optional plays on without it.
The email section:
| Field | Default | Meaning |
|---|---|---|
mode |
(required) |
webhook or imap
|
domain |
none | The domain of per-player addresses <box>+p-<16 hex>@<domain>; without it, tags are not read |
maxBytes |
262 144 (256 KB) | The largest raw message read; never more than 4 MB whatever it says |
linkDays |
30 | How long a sender who sent a pairing code stays linked to its player, in memory |
webhook.port, webhook.host
|
—, 127.0.0.1
|
Where POST /v1/inbound listens |
webhook.secretFile |
(required) | The HMAC secret's file |
webhook.toleranceS |
300 | How far the timestamp may be from now, in seconds |
webhook.perMinutePerIp |
120 | Posts a minute per address |
imap.host, imap.port
|
—, 993 | The mailbox's server, over TLS |
imap.user, imap.passwordFile
|
(required) | Its login |
imap.mailbox |
INBOX |
The folder polled |
imap.tls |
true |
false only towards this machine |
imap.pollS |
60 | Seconds between two polls (at most 50 unseen messages each) |
imap.keepDays |
0 | 0: delete once accepted; N: keep N days |
- In a worker thread with a 64 MB heap and 2 seconds; at most 256 KB, 3 levels of multipart, 64 parts, 200 headers;
HTML becomes inert text; a message with an attachment is refused; a message without a
Message-IDis refused. - The player is the recipient's tag (
gate+p-…@<domain>, withdomainset) or the sender who sent a pairing code (subjectPAIR ABCD2345,LINK …,CODE …or the code alone). - The first answer whose words all appear in the subject or the text (whole words, any case) proposes its signal,
else
otherwise. - The key is
sha256('email:' + Message-ID): a message delivered twice is one signal. The payload is a hash of theMessage-IDand the answer's number, hashed again asevidenceHash. - No reply is sent.
- Webhook mode: nothing; the message is read in memory and dropped.
-
IMAP mode: the message stays in the operator's mailbox, where it arrived, until
keepDaysdeletes it; a refused one stays flagged until the operator deletes it. - A sender who sent a pairing code is remembered as a salted SHA-256 of their address (the salt is random and lives
as long as the process) mapped to the
playerId, in memory, forlinkDays; a restart forgets it. - The address, the subject and the text are never logged and never sent to the Bridge.
- The code:
connectors/src/email/connector.ts,imap.ts,mime.ts. - The threat model:
docs/dev/threat-models/email.md. - Tested by
tests/connectors-email.test.ts.
Generated publication · publication générée — current documentation · documentation courante: quest'n'click 4.4.0 r1, documentation revision · révision de la documentation 8dc717c69acb62c4742b69c54a7e7dfb620f75d8.
Earlier versions and permalinks · versions précédentes et liens permanents: https://wanoo.github.io/quest-n-click/docs/
An edit made in this wiki is overwritten at the next publication · une modification faite dans ce wiki est écrasée à la publication suivante.
Edit the source · Modifier la source · Report an error · Signaler une erreur
Index (6)
Start (6)
Play (9)
Install (17)
- Requirements
- Install from the release archive
- Install from npm, once published
- Install from the repository to work on the engine
- doctor: checking your machine
- Image toolchain
- Audio toolchain: ffmpeg, fluidsynth, Furnace, uv
- Upgrade
- Upgrade notes: 4.4
- Upgrade notes: 4.3
- Upgrade notes: 4.2
- Upgrade notes: 4.1
- Upgrade notes: v3
- Upgrade notes: v2 to v3
- Coming from web-scumm
- Package reference
- Install troubleshooting
Create a game (55)
- Your first game
- Layout of a created project and of the engine repository
- The working loop
- Content principles and the identity contract
- Rooms
- Geometry: placement, walk zones, masks, layers, camera
- Something is visible but unreachable
- Reactions
- Game commands
- Conditions
- Flags, variables and events: which to use
- Custom code
- Characters
- Items and inventory
- World map
- Dialogues
- Scripts and events
- Moving characters: patrols and walks
- Exits
- Chapters and objectives
- State and saves: persistent or temporary, renaming an id, migrations
- Staging: wide rooms and animations
- The stage: lights, particles, floors
- Skin and fonts
- Title and credits
- Heroes: several characters and hero choice
- Built-in minigames
- A custom minigame
- Translations
- Music in stems
- Sound effects
- Voices: per language and per hero
- Images: prompts, cutting sheets, transparency, scale
- Asset provenance
- The storyboard, source of truth
- Puzzle design
- Classic mechanics
- Review pages
- DSL reference
- Checking your game
- What the solver proves
- Solver options
- Playtests
- Remix
- Speedrun for authors
- The sealed ending
- Production template
- Example games
- Working with an AI
- Build and release a game
- Publish on GitHub Pages
- The static game on Clever Cloud
- Other static hosts
- Sub-path, cache and service worker, incomplete deploys, going back
- Testing on a phone over the LAN
Local Studio (24)
- The local Studio: overview and launch
- Edit a room
- Placement and stage
- Preview, apply, undo
- Storyboard
- Assets and cutting
- Check: validator, solver, critical path, heat, health, coverage
- Before and after captures
- Semantic diff
- Play and replay a session
- Rule explainer
- Language and objectives
- Voices and the heroes × languages matrix
- Map
- Notes
- Remix in the Studio
- Music
- The Assistant: bring your own key
- Demo mode and studio-apply
- Local and LAN security
- Studio API
- Local MCP: setting up each client
- MCP tools
- Studio troubleshooting
Team Studio (21)
- What runs where
- Concepts: revision, transaction, span, lease, preview, build
- Starting as an invited dev
- Working together day to day
- Roles and actions: the full matrix
- Invitations
- Managing members
- Write refused on the hosted Studio: stale base revision (409)
- Write refused on the hosted Studio: span locked (423)
- Recovering after a lost lease, a disconnection or a stale preview
- Comments
- Map and semantic diff
- Publishing builds
- Remote MCP
- The game in Git
- Upgrading the engine of a hosted game
- Hosted assets
- The server's command line
- Health and metrics routes
- Trying it locally with compose.yml
- Known limits
Clever Cloud (23)
- Clever Cloud: overview, cost and prerequisites
- Apps and add-ons
- Domains and TLS
- Keycloak
- Generic OIDC and GitLab
- Configuration
- First deployment
- Backups
- RPO and RTO
- Restore the Studio on a fresh installation
- Redeploys
- Upgrading the server and checking state after a migration
- Limits of going back
- Monitoring
- Security
- Rotating keys
- Recovering admin access
- Changing domain or OIDC provider
- Worker, PostgreSQL or Cellar down
- Exporting the game and leaving the installation
- Retention
- Clever Cloud troubleshooting
- Costs and teardown
Reality Bridge and connectors (16)
- The Reality Bridge: what it is
- Quick start
- Guarantees
- Install and run the Bridge
- Backup and restore
- Several tenants
- Signals and the manifest
- The Bridge's command line
- Connectors overview
- Email connector
- Telnet connector
- SSH connector
- Open Badges connector
- The connector SDK
- Security: Biscuit and signatures
- Bridge and connector troubleshooting
Contribute (25)
- Welcome
- Developer setup
- The dev loop
- Before a pull request
- Conventions
- Writing documentation
- Architecture
- Code tour
- Import boundaries
- Which tests run when
- Mutation testing
- The coverage ratchet
- The quality baseline
- CI explained
- Field passes
- Benchmarks
- Releasing
- npm packages
- Decisions and ADRs
- Governance
- Bot pull requests
- Contributing with AI agents
- Security policy
- Code of conduct
- Licences and credits