Exchange journaling
What journaling does
Section titled “What journaling does”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 up the receiver (operator)
Section titled “1. Set up the receiver (operator)”-
Set these in the
.envof the release stack:Terminal window JOURNAL_SMTP_PORT=25JOURNAL_SMTP_BIND=0.0.0.0JOURNAL_HOSTNAME=archive.example.comJOURNAL_HOSTNAMEis the host name only: no scheme, path, port or mail address.JOURNAL_SMTP_BIND=0.0.0.0publishes 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. -
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.
-
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. -
Put the TLS certificate in place (next section), then restart the
apirole. 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.
2. TLS certificate
Section titled “2. TLS certificate”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:
JOURNAL_TLS_CERT_PATH=/etc/restow/journal-tls/fullchain.pemJOURNAL_TLS_KEY_PATH=/etc/restow/journal-tls/privkey.pemWhere the certificate comes from
-
The same host as the app (
JOURNAL_HOSTNAMEequalsRESTOW_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 ofdocker-compose.ymlis enough:Terminal window docker compose exec caddy ls /data/caddy/certificates # find the ACME directorydocker compose cp "caddy:/data/caddy/certificates/<ACME directory>/<host>/<host>.crt" ./journal-tls/fullchain.pemdocker compose cp "caddy:/data/caddy/certificates/<ACME directory>/<host>/<host>.key" ./journal-tls/privkey.pemFor 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_DIRwithcp -L, so you copy the files and not the symlinks of certbot’slive/directory:Terminal window cp -L /etc/letsencrypt/live/archive.example.com/fullchain.pem ./journal-tls/fullchain.pemcp -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.
3. The journal address and its status
Section titled “3. The journal address and its status”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)”- 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. - 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.
- 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.
- 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.
5. Rotate the address
Section titled “5. Rotate the address”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.
6. Limits
Section titled “6. Limits”- 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).
Troubleshooting
Section titled “Troubleshooting”- The status is “Receiver not running”. The page names the reason.
JOURNAL_SMTP_PORTnot set: set it to25and restart theapi. The license includes the receiver but theapistarted 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 theapiand 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_HOSTNAMEand 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 thanJOURNAL_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.