Skip to content

Restow 0.4.4

Choose a tag to compare

@github-actions github-actions released this 22 Oct 16:39
· 111 commits to main since this release

Restow 0.4.4. Released 2026-10-22, channel beta.

Beta release. Run it alongside your existing backups, not as your only one, until
you have verified restores against your own data.

Summary

Restow 0.4.4 archives mail beyond Exchange journaling (Business): Microsoft 365 mailboxes by
Graph change notifications, IMAP and Google Workspace accounts by IMAP sync with IDLE, and the
copies a mail provider forwards, each with a one-time takeover of the mail already there and
one archived item per mail however many ways captured it. The job editor offers only the
capture ways that fit each kind of mailbox, the archive page shows the capture, the hold
confirmation, ready-made Exchange Online PowerShell commands and the forwarding addresses, and
alerts say when capture stops. The restore explorer opens large mailboxes and OneDrives in
milliseconds instead of seconds to minutes; four database migrations (Upgrade Notes). Mail jobs
that archived before keep doing exactly that after the update: nothing new starts by itself.

Breaking Changes

None. The integration API contract stays at 1.5.0: its archive status and report count
forwarded copies in their totals, but do not list them as a channel of their own yet. In the
session API, mail jobs carry archiveCapture and their members captureKind, and the archive
search hides captures a stronger one superseded unless includeSuperseded=true; a job that
archives without any capture way for a kind of its mailboxes is refused with 422
archive_capture_no_way, which the defaults never cause. The IMAP and Google ways of a job
from before the update may carry pending: true (no way chosen yet): the API accepts such a
job until a way is chosen, keeps the marker only where the job had it, and never sets it.

Added

  • Archive capture foundation for mail beyond Exchange journaling (docs/ARCHIVE.md
    "Erfassungswege"). A mail job's archive keeps its capture ways per kind of mail object
    (archiveCapture on create, change and read of a mail job: Microsoft 365 by Graph and/or
    journaling, IMAP and Google Workspace by IMAP sync and/or a forwarded copy; the defaults of a
    new job are Graph and IMAP sync, jobs that archived before keep journaling only, see Upgrade
    Notes). A job that archives needs at least one way for every kind of mailbox it
    covers (422 archive_capture_no_way). Members and candidates carry captureKind (m365,
    google, imap, imap_microsoft), and GET /api/v1/backup-jobs/defaults?kind=mail names
    the tenant's mailSourceKinds.
  • Archive page (Business): a notice that can be put away names the mail jobs where Graph or
    IMAP archiving could be switched on, each with a link to the job, and the capture status
    lists the jobs whose IMAP or Google Workspace accounts wait for a capture way
    (GET /api/v1/archive/capture returns them as jobs).
  • Worker: a capture service keeps one stream per archived mailbox and mailbox way, claimed
    across worker processes with FOR UPDATE SKIP LOCKED and a lease, reconciled from the mail
    jobs every two minutes without a job row per mail. Pausing a job stops its scheduled backups only;
    capture goes on until archiving or the way is switched off in the job. It idles while the edition does not include
    capture from the mailbox, stops on shutdown and update drain, and pauses a tenant while its
    storage switches targets. New settings ARCHIVE_CAPTURE_CONCURRENCY (16),
    ARCHIVE_CAPTURE_TENANT_CONCURRENCY (4) and ARCHIVE_BACKFILL_CONCURRENCY (4). The archive
    queue has a handler: the one-time takeover of a mailbox's existing mail once its stream is live
    (marked as captured late), and archive schedules of older releases make the named mailboxes
    due. The Graph and IMAP ways plug into this framework.
  • GET /api/v1/archive/capture (state per capture way: availability, streams, how mail is
    noticed, counts of 24 hours and 7 days, takeover progress), GET /archive/capture/mailboxes,
    POST /archive/capture/mailboxes/:objectId/resync (audited as
    archive.capture.resync_requested) and GET/PUT /archive/capture/holds/:sourceId, the
    confirmation that a retention policy or litigation hold keeps deleted mail in a Microsoft 365
    tenant (audited as archive.hold.confirmed and archive.hold.unconfirmed).
  • Business: new capability archive.mailboxCapture; the gated features
    archive.mailboxCapture and archive.smtpCapture (API /me and the worker's new features
    extension point). archive.journalReceiver also covers forwarded copies.
  • Archive capture of Microsoft 365 mailboxes by Graph (docs/MICROSOFT.md "Archiv-Erfassung per
    Graph"): one change notification subscription per archived mailbox (created and renewed in
    $batch, at most 20 per request and one request per second and Microsoft tenant; renewed with
    less than a day left; reauthorized, recreated after removal, after expiry and when the Entra app
    changes), immutable message ids, notified mail fetched within minutes (drafts skipped), a delta
    catch-up over every folder and the recoverable deletions with its own state (every
    ARCHIVE_GRAPH_CATCHUP_MINUTES, default 360, and at once after a missed notification), and the
    one-time takeover of the existing mail in resumable pages. No new Microsoft permission is
    needed. Without a public HTTPS URL the mailboxes are polled every ARCHIVE_GRAPH_POLL_MINUTES
    (default 15) and the capture status names the reason (graph.notification_url_unavailable);
    a subscription Microsoft refuses is retried hourly meanwhile (graph.subscription_failed).
  • Public hooks POST /api/v1/hooks/graph/notifications and /lifecycle for Graph: the
    validation token is echoed, everything else answered 202; only notifications whose
    clientState matches the subscription's stored SHA-256 count, and they only queue a fetch.
    Bodies are limited to 1 MiB, and the hooks answer 404 while the edition does not include
    capture from the mailbox.
  • GET /api/v1/archive/capture/holds also returns checks: read-only Exchange Online and
    Security & Compliance PowerShell commands to check litigation holds and retention policies
    before confirming a hold. Graph v1.0 offers no way to read them (spike result in
    docs/MICROSOFT.md).
  • IMAP archiving (Business; docs/IMAP.md "Archivierung"): the capture way imap_sync archives an
    IMAP account continuously. Activation records where every folder stands, the one-time takeover
    brings in the mail that was there (resumable, flagged captured late), and every pass reads only
    new mail: STATUS per folder, metadata first, sources only of mail the archive does not hold for
    the account yet. All folders but Drafts; on Gmail and Google Workspace only All Mail, Spam and
    Trash. A changed UIDVALIDITY reads the folder again without archiving anything twice. Up to
    ARCHIVE_IMAP_IDLE_MAX (300) accounts per worker are watched with IMAP IDLE, shared between
    worker processes through Postgres locks, with a STATUS round over the other folders every
    ARCHIVE_IMAP_POLL_MINUTES (5); other accounts are polled every
    ARCHIVE_IMAP_LOGIN_POLL_MINUTES (10). New source setting archiveIdle (auto, off) for
    providers with a tight connection limit. Why an account is polled is shown as
    imap.idle_unsupported, imap.idle_disabled or imap.idle_budget.
  • Forwarded copies into the archive (Business; docs/IMAP.md "Weiterleitung"): the receiver takes
    plain copies a provider forwards (Postfix BCC maps, mailcow, Sieve redirect :copy, Google
    Workspace routing) as source smtp_forward, next to Exchange journal reports. Every IMAP
    account can have its own archive address archive+<token>@<journal host>; a copy to it belongs
    to exactly that account, a copy to the tenant's journal address is assigned by its headers and
    MAIL FROM. New routes GET /api/v1/archive/forwarding (accounts, addresses, copies of the
    last 7 days; issues nothing), POST /api/v1/archive/forwarding/:objectId/address (issued once,
    audited archive.forward.address_created) and POST /api/v1/archive/forwarding/:objectId/rotate
    (audited archive.forward.address_rotated, fingerprints only).
  • GET /api/v1/archive/journal names firstReportAt, the first journal report that arrived.
  • Release smoke check 13: IMAP archiving with backfill and IDLE, forwarded copies and their
    dedupe against the synced mail, against the Dovecot of check 5.
  • Job editor: the archive's capture ways per kind of mailbox in the job (Microsoft 365: Graph and
    Exchange journaling; IMAP and Google Workspace: IMAP archiving and forwarding), each labelled
    as captured at transport or near-time, locked with the edition that has it, at least one per
    kind; IMAP accounts on a Microsoft host get the hint to connect a Microsoft 365 source instead.
  • Archive page: capture from the mailboxes (Graph and IMAP archiving: streams, how mail is
    noticed and why a mailbox is checked at intervals, counts, takeover of existing mail), the
    capture per mailbox with a catch-up, and the hold confirmation of each Microsoft 365 source
    with the Exchange Online PowerShell commands that check it. Search and detail name forwarded
    copies, mail captured afterwards and the other ways that captured the same mail again; a
    capture replaced by a stronger one is hidden unless asked for (includeSuperseded on
    GET /api/v1/archive/search), and the detail links both (supersededBy, captures).
  • Business: Exchange journaling as ready-made Exchange Online PowerShell commands (connector,
    mailbox for undeliverable journal reports, journal rule) and a wait for the first report that
    checks every 5 seconds. Forwarding to the archive: an address per IMAP account (created,
    copied, renewed after a question), its status, and instructions for Postfix and mailcow BCC
    maps, Sieve redirect :copy and Google Workspace routing to the tenant's journal address.
  • IMAP source page: the archive setting archiveIdle, IMAP IDLE (automatic) or checks at
    intervals only, saved at once and locked without the edition that archives from the mailbox.
  • Exporting the current archive search leaves out the captures a stronger one superseded, as
    the search does, so the export holds what the search counted; with superseded captures shown,
    the export includes them (includeSuperseded in the export's search filter).
  • Alerts of the archive capture as archive.failed (bell, rules, mail): a mailbox whose capture
    has been in error for more than an hour, an IMAP account whose forwarded copies stopped for
    24 hours while its job wants them, and a journal address without reports for 24 hours, each
    after it received before and once per stretch. The bell and the mail say which of these it
    is and lead to its section of the archive page.

Changed

  • Every archive writer (journal receiver, file import, capture ways) appends through one shared
    path. The original is uploaded before the tenant's chain lock is taken, so a large report no
    longer holds up the other deliveries of the tenant. Mail captured more than once is recognised
    by Message-ID and content: a capture not stronger than what the archive holds becomes a capture
    row on that item instead of a second item, a stronger one names the item it supersedes, and the
    same Message-ID with another body is kept and flagged message-id-conflict. Journal reports and
    imported messages are still stored as items of their own.
  • The archive record format gains the optional bodyHash and supersedesItemId and the source
    smtp_forward (packages/core archive/FORMAT.md); records written before stay as they are.
  • Journal receiver: one SMTP transaction delivers into one tenant's archive; a recipient of a
    second tenant is answered 452 and delivered by the sender separately. The limit per sending IP
    rises from 60 to 600 connections per minute (JOURNAL_RATE_PER_IP), and a new limit per
    archive address (JOURNAL_RATE_PER_ADDRESS, 300 per minute) answers 451. Mail to the journal
    address that is not a journal report (no X-MS-Journal-Report header and not built like one)
    is archived as a forwarded copy with its own headers' envelope instead of as an incomplete
    report.
  • Pausing a mail job stops only its scheduled backups; archiving goes on, and the pause
    question says so. The forwarding page counts the accounts of a paused job as wanting their
    forwarded copy. Switching archiving or a capture way off in the editor asks first: new mail
    of these mailboxes is no longer archived from then on, the archive kept so far stays.
  • docs/ARCHIVE.md describes the capture ways of 0.4.4; docs/ARCHITECTURE.md describes the
    Graph throttling as it is built (no shared token bucket per source).
  • Restore explorer: the restore points of an account load alongside its owner's entry
    instead of after it, one round trip less before a mailbox or OneDrive shows its folders.
  • The Proxmox VE job dialog is twice as wide and lays its settings out in two columns from
    tablet width on (name and schedule with the next runs on the left; scope, restore points,
    restore check, boot test and guests on the right), so a job's settings fit on a desktop screen
    without scrolling; on a phone it stays one column at full width.

Fixed

  • Opening the job editor of a job with only IMAP accounts no longer creates the tenant's
    journal address: it is requested only while Exchange journaling is chosen for Microsoft 365
    mailboxes.
  • The archive chain could fork when two writers of one tenant stamped their items with the same
    time: the predecessor was taken by time alone. Writers now take the last item in the order the
    chain check walks (time, then id) and stamp their item strictly later.
  • Restore explorer: opening a large mailbox or OneDrive no longer takes seconds to minutes
    before its folders show. Every folder listing read all entries of the restore point (every
    message of the mailbox, every file of the OneDrive, with their metadata), twice per request,
    and matched the folders below against them without an index; a folder whose subfolders
    have no entry of their own (the mailbox root with mail and calendar, a OneDrive root)
    grew with the square of the entries and could time out. The listing now reads the folder's
    children and the folders below it from two new indexes (migration
    0061_explorer_parent_index; on a large manifest_versions table the update takes longer,
    docs/UPDATING.md): with 120 000 messages and 200 000 files, the mail folder list went from
    2.5 s to 15 ms, the root of the mailbox and of the OneDrive from over a minute to 15 ms,
    and every OneDrive folder from 1.5–2.5 s to about 10 ms. A folder of 50 000 messages lists
    its first page in about 0.5 s (was 1.9 s). Restore points of 0.3.4 and earlier that the
    worker has not converted yet are listed as before.
  • Restore explorer: the versions OneDrive kept of a file and the calendar's month view read
    the same indexes instead of every entry of the restore point (130 ms and 260 ms to under
    50 ms for the sizes above).
  • Mail preview, attachment download and calendar series: each chunk of the message is read
    from storage with a byte-range request for exactly its bytes instead of downloading its
    whole pack (up to 64 MiB) for every preview; a chunk damaged on every target is still read
    through its whole pack as before. The archive's content check reads the same way.

Security

None.

Upgrade Notes

New images and four database migrations; no new required environment variable, and the
docker-compose.yml of 0.4.0 stays.

  • Migrations 0059 to 0062 run at the api's start: the capture tables, new columns and five
    indexes on archive_items and backup_jobs, two indexes on manifest_versions for the
    restore explorer, and the capture ways of the mail jobs that already archive (below). The indexes are built inside the migration, where writes to the table
    wait; expect about a minute per ten million rows on SSD storage. Count
    archive_items and manifest_versions beforehand and plan a large installation's update
    outside the backup window (docs/UPDATING.md).
  • Capture from the mailbox needs the capability archive.mailboxCapture. Capabilities follow
    the license's edition (ee/licensing), so every Business and Service Provider license
    includes it without a new key; the Community build keeps the capture service idle.
  • Mail jobs that archive keep doing exactly what they did before the update: migration 0062
    gives each of them Exchange journaling only for Microsoft 365 mailboxes and no capture way
    yet for IMAP and Google Workspace accounts (marked as waiting for a choice), so no capture
    and no takeover of existing mail starts by itself. An administrator switches Graph or IMAP
    archiving on in the job; the job editor says at the IMAP card that no way is chosen yet, the
    archive page names the jobs where a way could be switched on (with a notice that can be put
    away, Business only), and the capture status lists the jobs waiting for a choice. Edits that
    do not touch the ways (name, schedule) save as before. Jobs created after the update, and a
    job that switches archiving on, start with the defaults: Graph for Microsoft 365 mailboxes,
    IMAP sync for IMAP and Google Workspace accounts, each with a one-time takeover of the mail
    already in the mailbox (marked as captured late).
  • Graph capture is notified of new mail only when the installation's public URL is https://
    on port 443 and reachable from Microsoft (/api/v1/hooks/graph/*; a reverse proxy of your
    own must pass it through, with at most 1 MiB per request). Otherwise every mailbox is polled
    every ARCHIVE_GRAPH_POLL_MINUTES (15) and the capture status says why. No new Microsoft
    permission is needed.
  • IMAP archiving keeps one connection per account open (IDLE) where the server offers it.
    For providers with a tight connection limit, set the source to "Check at intervals only"
    (archiveIdle: "off").
  • New optional settings: the journal receiver's JOURNAL_RATE_PER_IP (now 600 per minute,
    was a fixed 60) and JOURNAL_RATE_PER_ADDRESS (300), and the capture's
    ARCHIVE_CAPTURE_CONCURRENCY, ARCHIVE_CAPTURE_TENANT_CONCURRENCY,
    ARCHIVE_BACKFILL_CONCURRENCY, ARCHIVE_GRAPH_CATCHUP_MINUTES,
    ARCHIVE_GRAPH_POLL_MINUTES, ARCHIVE_IMAP_IDLE_MAX, ARCHIVE_IMAP_POLL_MINUTES and
    ARCHIVE_IMAP_LOGIN_POLL_MINUTES (.env.example).
  • Rollback to 0.4.3 after the migrations was not tested: take a database backup before the
    update and restore it to go back.

Known Issues

  • Graph capture was tested against a simulated Graph API, IMAP archiving against a simulated
    IMAP server and the Dovecot of the release smoke, forwarding against the receiver itself;
    never against a real Microsoft 365 or Google Workspace tenant, Postfix, mailcow or hoster.
  • Holds in a Microsoft 365 tenant are not detected: Graph v1.0 offers no way to read them, so
    an administrator confirms them per source. Of the Recoverable Items only the deletions
    folder is read; mail purged before Restow fetched it can be missing. Graph cannot export
    mail over about 150 MB; such mail is logged, not archived.
  • The receiver does not check the sender (SPF), and its limits count per api process.
  • IMAP sources with OAuth2 still cannot be created in the API or the interface, so a Google
    Workspace account is recognised by its host or folder list only.
  • The integration API's archive status and report do not list forwarded copies as a channel
    of their own (see Breaking Changes).
  • The known issues of 0.4.3 still apply.

Verification

Before the tag, on the release branch (0.4.3 plus the 0.4.4 workstreams): lint and typecheck
of all workspaces, the unit and Postgres suites of every package on Postgres 16 (with the
Postgres guard), the web DOM tests on happy-dom including the layout-width and green-is-proof
checks, the v1 OpenAPI test, drizzle-kit without drift, gofmt, go vet and the Go tests of the
agents, the Perl tests of the Proxmox VE plugin, the server installer and demo reset tests,
shellcheck, the CI script tests, the smoke runner's unit tests and the third-party notices.
The release smoke, including the new check of the archive capture against Dovecot, runs in
the release pipeline; no real Microsoft 365 tenant, Google Workspace or mail provider was used
before the tag.

Release pipeline, 2026-10-22:

Check Result Duration
1. Install: images, compose up, migrations (new install and upgrade) PASS 94s
2. Health endpoints, worker and scheduler registered PASS 0s
3. Passkey sign-in E2E with a virtual authenticator, tenant creation, i18n de/en PASS 2m 06s
4. Microsoft 365 backup and restore against the dev tenant SKIPPED 0s
5. IMAP backup and restore against Dovecot, hash comparison PASS 8s
6. Journal receipt, chain verification, export PASS 3s
7. Standalone restore with restow-restore from the storage of check 5 PASS 24s
8. Storage targets: S3 (Garage), local path, mounted directory PASS 21s
9. Image scan (Trivy) and dependency audit (pnpm audit) PASS 23s
10. Endpoint backup: the agent enrolls, backs up a folder, restores it PASS 12s
11. Mail import (folder and upload) and export (EML ZIP, MBOX), compared by hash PASS 8s
12. File share backup against Samba: back up, browse, ZIP, restore with permissions, copy job PASS 2m 30s
13. Archive capture: IMAP sync with IDLE and backfill, forwarded copies, dedupe, chain PASS 117s

12 of 13 checks passed in full, 0 partly, 1 skipped, 0 failed. The full report, including the reason for every skipped or partial check, is attached to the release as smoke-report.md.

The Community images were smoke-tested on their own (smoke-report-community.md, smoke-report-community-arm64.md): amd64 PASS with gaps: 9 of 13 checks passed in full, 0 partly, 4 skipped (see the reasons below); arm64 PASS with gaps: 9 of 13 checks passed in full, 0 partly, 4 skipped (see the reasons below).

Images

Two builds of the same release. The full build is the Apache-2.0 core plus the Business and Service Provider modules (Restow Enterprise License), which stay locked until a license key is installed. The Community build is the Apache-2.0 core alone.

ghcr.io/restow-backup/restow:0.4.4
ghcr.io/restow-backup/restow-web:0.4.4
ghcr.io/restow-backup/restow-community:0.4.4
ghcr.io/restow-backup/restow-web-community:0.4.4

The digests of the multi-arch manifests (the opt-in updater verifies the pulled images against the two lines of its build):

restow: sha256:bada07d631927b53e190c90b812d55dd1f8efb70d689e52f744bf5de450ce55f
restow-web: sha256:e5ff2b3f627572c2f31daea08d735fa882b89050ee6f7920d9e2f1080bb3a5ef
restow-community: sha256:a697977db60517e6537432ed5c5cf413e16c0d770df2a4b9f6792d34a32706e3
restow-web-community: sha256:dff9e69e42280d41c9e008237317139da294e1ed83731dd82e691db034faf9dc

All four images are multi-arch (linux/amd64, linux/arm64), signed with cosign (keyless) and carry an SPDX SBOM as attestation. Verify any of them with:

cosign verify ghcr.io/restow-backup/restow:0.4.4 \
  --certificate-identity-regexp '^https://github.com/restow-backup/restow/\.github/workflows/release\.yml@refs/tags/v' \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com
cosign verify ghcr.io/restow-backup/restow-web:0.4.4 \
  --certificate-identity-regexp '^https://github.com/restow-backup/restow/\.github/workflows/release\.yml@refs/tags/v' \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com
cosign verify ghcr.io/restow-backup/restow-community:0.4.4 \
  --certificate-identity-regexp '^https://github.com/restow-backup/restow/\.github/workflows/release\.yml@refs/tags/v' \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com
cosign verify ghcr.io/restow-backup/restow-web-community:0.4.4 \
  --certificate-identity-regexp '^https://github.com/restow-backup/restow/\.github/workflows/release\.yml@refs/tags/v' \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com

The public demo's own images, ghcr.io/restow-backup/restow-demo-seed:0.4.4 and ghcr.io/restow-backup/restow-demo-dovecot:0.4.4 (deploy/demo), carry the same tags, signatures and SBOMs; they are no part of an installation.

docker-compose.yml and env.example are the release stack (deploy/release/); install.sh installs it on a dedicated Linux VM (README.md, "Install with the script"; install.sh.sha256 for sha256sum -c after the download); restow-agent-THIRD_PARTY_NOTICES.txt holds the licenses of the agent binaries and of the restic they run; SHA256SUMS lists every file of this release and is signed (SHA256SUMS.sigstore.json).