Repository navigation
Restow 0.4.4
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
(archiveCaptureon 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 (422archive_capture_no_way). Members and candidates carrycaptureKind(m365,
google,imap,imap_microsoft), andGET /api/v1/backup-jobs/defaults?kind=mailnames
the tenant'smailSourceKinds. - 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/capturereturns them asjobs). - Worker: a capture service keeps one stream per archived mailbox and mailbox way, claimed
across worker processes withFOR UPDATE SKIP LOCKEDand 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 settingsARCHIVE_CAPTURE_CONCURRENCY(16),
ARCHIVE_CAPTURE_TENANT_CONCURRENCY(4) andARCHIVE_BACKFILL_CONCURRENCY(4). Thearchive
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) andGET/PUT /archive/capture/holds/:sourceId, the
confirmation that a retention policy or litigation hold keeps deleted mail in a Microsoft 365
tenant (audited asarchive.hold.confirmedandarchive.hold.unconfirmed).- Business: new capability
archive.mailboxCapture; the gated features
archive.mailboxCaptureandarchive.smtpCapture(API/meand the worker's newfeatures
extension point).archive.journalReceiveralso 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 everyARCHIVE_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/notificationsand/lifecyclefor Graph: the
validation token is echoed, everything else answered 202; only notifications whose
clientStatematches 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/holdsalso returnschecks: 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_syncarchives 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 settingarchiveIdle(auto,off) for
providers with a tight connection limit. Why an account is polled is shown as
imap.idle_unsupported,imap.idle_disabledorimap.idle_budget. - Forwarded copies into the archive (Business; docs/IMAP.md "Weiterleitung"): the receiver takes
plain copies a provider forwards (Postfix BCC maps, mailcow, Sieveredirect :copy, Google
Workspace routing) as sourcesmtp_forward, next to Exchange journal reports. Every IMAP
account can have its own archive addressarchive+<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 routesGET /api/v1/archive/forwarding(accounts, addresses, copies of the
last 7 days; issues nothing),POST /api/v1/archive/forwarding/:objectId/address(issued once,
auditedarchive.forward.address_created) andPOST /api/v1/archive/forwarding/:objectId/rotate
(auditedarchive.forward.address_rotated, fingerprints only). GET /api/v1/archive/journalnamesfirstReportAt, 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 (includeSupersededon
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, Sieveredirect :copyand 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 (includeSupersededin 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 flaggedmessage-id-conflict. Journal reports and
imported messages are still stored as items of their own. - The archive record format gains the optional
bodyHashandsupersedesItemIdand 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 (noX-MS-Journal-Reportheader 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 withmailandcalendar, 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 largemanifest_versionstable 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 onarchive_itemsandbackup_jobs, two indexes onmanifest_versionsfor 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_itemsandmanifest_versionsbeforehand 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
everyARCHIVE_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) andJOURNAL_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_MINUTESand
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).