Skip to content

Exchange journaling

A journal rule in Exchange Online sends a copy of every message, a journal report, to an address that Restow controls, before the user can change or delete the message in the mailbox. Restow archives the original message byte for byte, together with the envelope, including the BCC recipients that only the envelope shows. This copy, made before anyone can touch the mail, is why journaling is the primary capture path of the archive. See Archive and journaling and Archiving for the wider picture. Restow is designed for GoBD-compliant use; your tax advisor confirms whether your setup meets your obligations.

Each tenant has its own journal address. Tenant administrators see it on the Archive page under Exchange journaling, with the status and a short setup guide that links here.

Two people are involved:

  • You, the operator of the Restow installation, set up the receiver, DNS, port 25 and the TLS certificate (sections 1 and 2).
  • The customer’s Microsoft 365 administrator creates the connector and the journal rule (section 4).
  1. Set these in the .env of the release stack:

    Terminal window
    JOURNAL_SMTP_PORT=25
    JOURNAL_SMTP_BIND=0.0.0.0
    JOURNAL_HOSTNAME=archive.example.com

    JOURNAL_HOSTNAME is the host name only: no scheme, path, port or mail address. JOURNAL_SMTP_BIND=0.0.0.0 publishes the port on every address of the host. Without it the release stack maps the port on the loopback interface only, and Exchange Online cannot reach it.

  2. Publish the host name in DNS: an A or AAAA record that points to this server, or an MX record that points to a host that has one. The domain of the journal address must not be an accepted domain of the Microsoft 365 tenant.

  3. Open TCP port 25 inbound from the internet. Exchange Online delivers to port 25 only, and it must reach the receiver directly, not through Caddy. If the receiver listens on another port (JOURNAL_SMTP_PORT), forward port 25 of the public address to that port.

  4. Put the TLS certificate in place (next section), then restart the api role. The receiver reads its port, the edition and the certificate at start.

Restow cannot check DNS or reachability from the outside. The Archive page therefore shows only what follows from the configuration (port, TLS, host name) and what has actually arrived.

Exchange Online delivers journal reports only over TLS: the connector is set to always use TLS. The receiver therefore starts only with a certificate of your own, and it never accepts mail without TLS. It never uses the test certificate that ships with the SMTP library. That certificate’s private key is public knowledge, so a connection “encrypted” with it protects nothing and still looks protected.

Files and requirements

  • JOURNAL_TLS_CERT_PATH: the certificate chain, PEM, leaf first.
  • JOURNAL_TLS_KEY_PATH: the private key, PEM, not encrypted with a passphrase.
  • Both must be set. The certificate must come from a publicly trusted authority and match JOURNAL_HOSTNAME, otherwise Exchange Online refuses to deliver.

In the release stack

The directory JOURNAL_TLS_DIR (default ./journal-tls next to docker-compose.yml) is mounted read-only into the api container at /etc/restow/journal-tls. The directory is mounted, not two single files, so that a renewal client that replaces files is noticed. Put fullchain.pem and privkey.pem there and set:

Terminal window
JOURNAL_TLS_CERT_PATH=/etc/restow/journal-tls/fullchain.pem
JOURNAL_TLS_KEY_PATH=/etc/restow/journal-tls/privkey.pem

Where the certificate comes from

  • The same host as the app (JOURNAL_HOSTNAME equals RESTOW_APP_DOMAIN): copy the certificate that the stack’s Caddy already holds. Caddy renews about 30 days before expiry, and the receiver picks up the copy by itself, so a daily cron job in the directory of docker-compose.yml is enough:

    Terminal window
    docker compose exec caddy ls /data/caddy/certificates # find the ACME directory
    docker compose cp "caddy:/data/caddy/certificates/<ACME directory>/<host>/<host>.crt" ./journal-tls/fullchain.pem
    docker compose cp "caddy:/data/caddy/certificates/<ACME directory>/<host>/<host>.key" ./journal-tls/privkey.pem

    For any other host name, Caddy holds no certificate.

  • Any other host name: issue the certificate on the host with another ACME client (certbot, acme.sh, lego). Use the DNS challenge, because Caddy holds port 80. Let a deploy hook copy the files into JOURNAL_TLS_DIR with cp -L, so you copy the files and not the symlinks of certbot’s live/ directory:

    Terminal window
    cp -L /etc/letsencrypt/live/archive.example.com/fullchain.pem ./journal-tls/fullchain.pem
    cp -L /etc/letsencrypt/live/archive.example.com/privkey.pem ./journal-tls/privkey.pem

Checks at start

Both files must be readable and PEM, the key must belong to the certificate, and the certificate must be valid and not expired. Otherwise no listener starts and port 25 stays closed. The Archive page shows Receiver not running with one of three reasons, and the API log names the file:

  • no TLS certificate configured (tls_not_configured),
  • certificate or key not usable (tls_invalid),
  • certificate expired (tls_expired).

The API keeps running. After you fix the files, restart it.

STARTTLS is mandatory

With a certificate the receiver offers STARTTLS (TLS 1.2 or newer) and refuses every session that has not upgraded, at MAIL FROM, with 530 Must issue a STARTTLS command first. No recipient and no message body is accepted in plain text.

Renewal without a restart

The API re-reads both files every five minutes and uses a new certificate for new sessions as soon as certificate and key are valid together. A half-written or broken replacement changes nothing: the receiver stays on the current certificate and the API log says so. If the certificate expires without a valid replacement, the status changes to certificate expired and returns once the files are renewed.

Local development only

JOURNAL_ALLOW_INSECURE=true starts the receiver without a certificate and without offering STARTTLS. Exchange Online does not deliver to such a receiver, and mail would cross the network in plain text. Never use it in production. It applies only while no certificate is configured: a configured but broken certificate never leads to plain text.

The address is journal+<token>@<journal host>. The token is random per tenant: 32 characters, lowercase letters and digits only, because mail systems do not reliably keep the case of the local part. The first view of the section creates the address and writes archive.journal.address_created to the audit log. If JOURNAL_HOSTNAME is missing or invalid, Restow shows no complete address and says so.

Status Meaning
Receiving The receiver is listening and the last report is at most 24 hours old.
No report for more than 24 hours Reports came, but none any more. For a tenant with mail traffic this is a fault on the way: connector, rule, firewall or certificate.
No reports yet The receiver is listening and nothing has arrived.
Receiver not running Shown with a reason. See Troubleshooting.

The time of the last report and the counts for 24 hours and 7 days come from the archive itself, not from a separate counter.

4. Microsoft 365 (the customer’s administrator)

Section titled “4. Microsoft 365 (the customer’s administrator)”
  1. Connector. In the Exchange admin center, open Mail flow, then Connectors, and add a connector from Office 365 to Partner organization. Use it only for mail sent to the domain of the journal host, route it through the journal host as smart host, and set it to always use TLS with a certificate from a trusted authority whose name matches. Validate the connector: Microsoft asks for an email address, so enter the journal address. The test message arrives as a journal report that cannot be read and is archived with the flag “Journal report could not be parsed” (report-unparseable). That is expected.
  2. Mailbox for undeliverable journal reports. In the Microsoft Purview portal, open Data lifecycle management, then Exchange (legacy), then Journal rules. Under Send undeliverable journal reports to, enter a real mailbox that somebody reads, for example a shared mailbox. Exchange Online sends reports there that it could not deliver to Restow, so a gap shows up here.
  3. Journal rule. On the same page, add a rule that sends journal reports to the journal address, for messages sent or received by everyone, of the type All messages (internal, external and BCC). Turn the rule on.
  4. Test. Send a message inside the organization. The report usually arrives within a few minutes. The status on the Archive page changes to Receiving, the message is in the archive search with the source Exchange Online journal, and the hash chain check stays intact.

Rotate address on the Archive page (with a confirmation) creates a new token. The old address stops working at once: at every RCPT TO the receiver looks the token up in the database and refuses the old address with 550. Change the recipient of the journal rule, and the address you used to validate the connector, to the new address immediately. Until you do, reports are refused and not archived, and Exchange Online returns them to the mailbox for undeliverable journal reports.

Rotate after an exposed token or when the journal host changes. The audit log records archive.journal.address_created and archive.journal.address_rotated with a fingerprint of the token, never the token itself.

  • Journaling captures from the moment the rule is on. What was in a mailbox before comes into the archive through the Graph sync and is marked as captured after the fact.
  • Restow does not check DNS or reachability from the outside, only what the configuration shows and what has arrived.
  • Verification of the sender (SPF, Microsoft address ranges) is not implemented yet. The receiver accepts only known journal addresses, limits the rate per sender IP address and limits the size.
  • Reports up to JOURNAL_MAX_SIZE_MB (default 150 MB) are accepted. Larger reports are refused.
  • The receiver requires STARTTLS and does not start without a valid certificate (see TLS certificate).
  • The status is “Receiver not running”. The page names the reason. JOURNAL_SMTP_PORT not set: set it to 25 and restart the api. The license includes the receiver but the api started before the key was installed: restart it. The listener could not start (port in use, no permission for port 25): the API log names the cause. No certificate, certificate not usable or expired: see TLS certificate. Not started in this process: reload the page, then restart the api and read its log.
  • “No reports yet” although the rule is on. Check in this order: DNS for the host name, TCP port 25 reachable from the internet (firewall, port forwarding, JOURNAL_SMTP_BIND=0.0.0.0), the connector (only for the journal domain, journal host as smart host, always TLS), and that the journal rule is turned on. Validate the connector again with the journal address.
  • Exchange Online reports a TLS or certificate error. The certificate must come from a publicly trusted authority, match JOURNAL_HOSTNAME and be delivered as the full chain, leaf first. A self-signed certificate is refused.
  • “No report for more than 24 hours”. Look into the mailbox for undeliverable journal reports first: what sits there was not delivered to Restow. Then check the certificate expiry, the firewall and the journal rule.
  • Reports are refused with 550. The journal address is unknown. Usually it was rotated and the journal rule still names the old one, or the address has a typo.
  • Reports are refused with 552. A report is larger than JOURNAL_MAX_SIZE_MB.
  • 530 Must issue a STARTTLS command first. The sender did not use TLS. The Exchange Online connector must be set to always use TLS.

See also Environment variables and Ports and roles.