Files
TREK/wiki/Booking-Mail-Import.md
T
Maurice 1bfaf4ea98 feat(mail-ingest): read forwarded mails from an IMAP box, a signed webhook and the TREK Cloud relay
The IMAP job reads the box every two minutes through the SSRF guard, keeps a
uid cursor and moves read mails to TREK/processed. The webhook takes raw MIME
signed with HMAC-SHA256 over a timestamp, the recipient and the mail, and is a
404 unless it is the transport. The relay job collects a managed install's mails.
2026-11-13 17:43:49 +01:00

4.8 KiB

Booking Mail Import

Everyone on the instance gets a forwarding address of their own. A booking confirmation forwarded there is read by TREK and added to the matching trip, with its times, places, booking reference and cost, and the mail attached as its document. A booking TREK is not sure about waits in Open bookings with the reason, where it can be added to a trip by hand, filed as a document or discarded. A booking added on its own can be undone with one tap for seven days.

The import is off by default.

How mail reaches TREK

An admin sets it up in Admin → Integrations → Booking mail import. Every field there can also be set by an environment variable, which then wins.

Field What it is
Import on The switch for the whole instance. Off, no address takes mail and the entry points disappear.
Transport IMAP, Webhook, or none (only the .eml upload in the inbox works).
Address template The forwarding address with {token} where each person's token goes, for example trek+{token}@example.com.
AI mails per person and day How many forwarded mails a day an AI reads for one person (default 10). Above it, only machine-readable data is read.

Each person's token is 24 random characters. A person can renew their address at any time; the old one stops taking mail at once. Treat the address like a password: whoever knows it can put bookings into that person's trips.

IMAP

TREK reads one mailbox every two minutes. Point a catch-all, or a mailbox with sub-addressing (trek+anything@), at it and use the matching address template. Read mails move to the folder TREK/processed and are never deleted, so anyone with access to the mailbox can read them.

Field Default
Host
Port 993
Encryption tls (from the first byte) or starttls
User, password
Folder INBOX

A mail server on your own network needs ALLOW_INTERNAL_NETWORK=true (Internal network access). Test connection in the dialog opens the folder the way the import does and says what failed.

Webhook

A mail service posts every mail to POST /api/mail-ingest/inbound, raw, with Content-Type: message/rfc822, at most 10 MB. The route answers 404 unless the webhook is the transport and a secret of at least 32 characters is set.

Each request carries three headers:

Header Value
X-Trek-Timestamp The time of sending, in Unix seconds. Requests more than five minutes off are refused.
X-Trek-Recipient The address the mail was sent to (the envelope recipient). Optional; without it TREK reads Delivered-To, X-Original-To and To.
X-Trek-Signature sha256= followed by the hex HMAC-SHA256 of <timestamp>\n<recipient>\n<raw mail>, keyed with the secret.

A Cloudflare Email Worker that forwards a mail:

export default {
  async email(message, env) {
    const raw = new Uint8Array(await new Response(message.raw).arrayBuffer());
    const timestamp = String(Math.floor(Date.now() / 1000));
    const key = await crypto.subtle.importKey('raw', new TextEncoder().encode(env.TREK_SECRET),
      { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']);
    const head = new TextEncoder().encode(`${timestamp}\n${message.to}\n`);
    const signed = new Uint8Array(head.length + raw.length);
    signed.set(head);
    signed.set(raw, head.length);
    const mac = new Uint8Array(await crypto.subtle.sign('HMAC', key, signed));
    const signature = [...mac].map((b) => b.toString(16).padStart(2, '0')).join('');
    const res = await fetch(`${env.TREK_URL}/api/mail-ingest/inbound`, {
      method: 'POST',
      headers: {
        'Content-Type': 'message/rfc822',
        'X-Trek-Timestamp': timestamp,
        'X-Trek-Recipient': message.to,
        'X-Trek-Signature': `sha256=${signature}`,
      },
      body: raw,
    });
    if (!res.ok) message.setReject(`TREK answered ${res.status}`);
  },
};

Postmark and Mailgun forward inbound mail as JSON, not raw; put a small function like the one above between them and TREK. The answer is {"accepted":true,"duplicate":false}; a mail sent twice is stored once.

Status

The Status card in the dialog shows the last successful read, the last error, and the mails of the last day by outcome, in counts only. No subject, sender or content of any mail is shown there.

Privacy

Forwarded mails are stored encrypted on the server for at most 30 days, and seven days after their booking was added. With an AI connection set up, the mail's text and attachments, plus the title, dates and destinations of the forwarding person's trips, go to that provider; the sender, recipient and forwarding headers are removed first. Without AI, everything stays on the server. Let your users know.