quest'n'click 4.5.0 · r1

Built for quest'n'click 4.5.0, revision r1. Whether it is published, and which one is current, is recorded at publication.json.

Email connector

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 plan reality; 3 test files · people's passes: connectors-real-security not 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).

What you get

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.

Context and versions

Prerequisites

Where to run the commands

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.

Steps

  1. 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
    
  1. 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
    
  1. 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 } }
    }
    
  1. Start it:

    # in the engine repository
    npm run connector -- email --config email.json
    

    It logs {"event":"email.started","connector":"email","mode":"webhook"}.

  1. 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"}.

Expected result, and how to check it

Common errors

SymptomCauseFix
email: "mode" is "webhook" or "imap" at startNo modeSet email.mode
email: webhook mode needs "webhook.secretFile" / email: imap mode needs "imap.passwordFile"The secret is missingName 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 serverKeep TLS
401 {"error":"stale"}The timestamp is missing or more than five minutes awaySend Unix seconds; check the clocks
401 {"error":"signature"}Another secret, or the body changed on the waySign the exact bytes posted
422 {"error":"no-player"}The sender sent no pairing code, and no recipient tag of domain names a playerPair first, or set domain
422 {"error":"attachment"} / "too-large" / "unreadable"A message with an attachment, over 256 KB, or one the reader refusedPlain text, no attachment
422 {"error":"pairing-pairing"}The pairing code is unknown, expired, or confirmed alreadyA new code from the game
429 {"error":"rate"}Over 120 posts a minute from one addressSlow down

Files or data changed

Guarantee · bridge-email-unseen-when-away · referenced · executed on aff6caab7514 · 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.

Recovery or going back

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.

Configuration reference

The email section:

FieldDefaultMeaning
mode(required)webhook or imap
domainnoneThe domain of per-player addresses <box>+p-<16 hex>@<domain>; without it, tags are not read
maxBytes262 144 (256 KB)The largest raw message read; never more than 4 MB whatever it says
linkDays30How long a sender who sent a pairing code stays linked to its player, in memory
webhook.port, webhook.host—, 127.0.0.1Where POST /v1/inbound listens
webhook.secretFile(required)The HMAC secret's file
webhook.toleranceS300How far the timestamp may be from now, in seconds
webhook.perMinutePerIp120Posts a minute per address
imap.host, imap.port—, 993The mailbox's server, over TLS
imap.user, imap.passwordFile(required)Its login
imap.mailboxINBOXThe folder polled
imap.tlstruefalse only towards this machine
imap.pollS60Seconds between two polls (at most 50 unseen messages each)
imap.keepDays00: delete once accepted; N: keep N days

How a message is read

What it keeps

References


Edit the source of this page · Report an error