### Check binary attestation at [here](${{ steps.attest.outputs.attestation-url }})
================================================
FILE: .github/workflows/scorecard.yml
================================================
# This workflow uses actions that are not certified by GitHub. They are provided
# by a third-party and are governed by separate terms of service, privacy
# policy, and support documentation.
name: Scorecard supply-chain security
on:
# For Branch-Protection check. Only the default branch is supported. See
# https://github.com/ossf/scorecard/blob/main/docs/checks.md#branch-protection
branch_protection_rule:
# To guarantee Maintained check is occasionally updated. See
# https://github.com/ossf/scorecard/blob/main/docs/checks.md#maintained
schedule:
- cron: '31 6 * * 0'
push:
branches: [ "main" ]
# Declare default permissions as read only.
permissions: read-all
jobs:
analysis:
name: Scorecard analysis
runs-on: ubuntu-latest
# `publish_results: true` only works when run from the default branch. conditional can be removed if disabled.
if: github.event.repository.default_branch == github.ref_name || github.event_name == 'pull_request'
permissions:
# Needed to upload the results to code-scanning dashboard.
security-events: write
# Needed to publish results and get a badge (see publish_results below).
id-token: write
# Uncomment the permissions below if installing in a private repository.
# contents: read
# actions: read
steps:
- name: "Checkout code"
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v4.2.2
with:
persist-credentials: false
- name: "Run analysis"
uses: ossf/scorecard-action@4eaacf0543bb3f2c246792bd56e8cdeffafb205a # v2.4.3
with:
results_file: results.sarif
results_format: sarif
# (Optional) "write" PAT token. Uncomment the `repo_token` line below if:
# - you want to enable the Branch-Protection check on a *public* repository, or
# - you are installing Scorecard on a *private* repository
# To create the PAT, follow the steps in https://github.com/ossf/scorecard-action?tab=readme-ov-file#authentication-with-fine-grained-pat-optional.
# repo_token: ${{ secrets.SCORECARD_TOKEN }}
# Public repositories:
# - Publish results to OpenSSF REST API for easy access by consumers
# - Allows the repository to include the Scorecard badge.
# - See https://github.com/ossf/scorecard-action#publishing-results.
# For private repositories:
# - `publish_results` will always be set to `false`, regardless
# of the value entered here.
publish_results: true
# (Optional) Uncomment file_mode if you have a .gitattributes with files marked export-ignore
# file_mode: git
# Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF
# format to the repository Actions tab.
- name: "Upload artifact"
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: SARIF file
path: results.sarif
retention-days: 5
# Upload the results to GitHub's code scanning dashboard (optional).
# Commenting out will disable upload of results to your repo's Code Scanning dashboard
- name: "Upload to code-scanning"
uses: github/codeql-action/upload-sarif@v4
with:
sarif_file: results.sarif
================================================
FILE: .github/workflows/test.yml
================================================
name: Test
on:
workflow_dispatch:
jobs:
style:
name: Check Style
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6.0.1
- name: Check Style
run: cargo fmt --all --check
test:
name: Test
needs: style
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6.0.1
- name: Install dependencies
run: |
sudo apt-get update -y
curl -LO https://github.com/glauth/glauth/releases/download/v2.2.0/glauth-linux-arm64
chmod a+rx glauth-linux-arm64
nohup ./glauth-linux-arm64 -c tests/resources/ldap.cfg &
curl -Lo minio.deb https://dl.min.io/server/minio/release/linux-amd64/archive/minio_20230629051228.0.0_amd64.deb
sudo dpkg -i minio.deb
mkdir ~/minio
nohup minio server ~/minio --console-address :9090 &
curl -LO https://dl.min.io/client/mc/release/linux-amd64/mc
chmod a+rx mc
./mc alias set myminio http://localhost:9000 minioadmin minioadmin
./mc mb tmp
- name: Rust Cache
uses: Swatinem/rust-cache@v2
- name: JMAP Protocol Tests
run: cargo test -p jmap_proto -- --nocapture
- name: IMAP Protocol Tests
run: cargo test -p imap_proto -- --nocapture
- name: Full-text search Tests
run: cargo test -p store -- --nocapture
- name: Directory Tests
run: cargo test -p tests directory -- --nocapture
- name: SMTP Tests
run: cargo test -p tests smtp -- --nocapture
- name: IMAP Tests
run: cargo test -p tests imap -- --nocapture
- name: JMAP Tests
run: cargo test -p tests jmap -- --nocapture
================================================
FILE: .github/workflows/trivy.yml
================================================
# trivy ci workflow
name: trivy
on:
workflow_dispatch:
push:
branches: [ "main" ]
pull_request:
# The branches below must be a subset of the branches above
branches: [ "main" ]
schedule:
- cron: '00 12 * * *'
permissions:
contents: read
jobs:
build:
permissions:
contents: read # for actions/checkout to fetch code
security-events: write # for github/codeql-action/upload-sarif to upload SARIF results
actions: read # only required for a private repository by github/codeql-action/upload-sarif to get the Action run status
name: Check
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v6.0.1
- name: Run Trivy vulnerability scanner
uses: aquasecurity/trivy-action@master
with:
scan-type: 'fs'
ignore-unfixed: true
format: 'sarif'
output: 'trivy-results.sarif'
severity: 'CRITICAL,HIGH'
- name: Upload Trivy scan results to GitHub Security tab
uses: github/codeql-action/upload-sarif@v4
with:
sarif_file: 'trivy-results.sarif'
================================================
FILE: .gitignore
================================================
/target
.vscode
.idea
*.failed
*_failed
run.sh
_ignore
.DS_Store
================================================
FILE: CHANGELOG.md
================================================
# Change Log
All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org/).
## [0.15.5] - 2026-02-14
If you are upgrading from v0.14.x and below, this version includes **multiple breaking changes**. Please read the [upgrading documentation](https://github.com/stalwartlabs/stalwart/blob/main/UPGRADING/v0_15.md) for more information on how to upgrade from previous versions.
If you are upgrading from v0.15.x, replace the binary and update the webadmin.
## Added
## Changed
## Fixed
- IMAP/JMAP: OOM when `mail-parser` returns cyclical MIME structures [CVE-2026-26312](https://github.com/stalwartlabs/stalwart/security/advisories/GHSA-jm95-876q-c9gw).
- Tracing: Fix tracing indexing when using separate stores.
- JMAP: Fix `upToId` computation in `*/queryChanges`.
- JMAP: Include createdIds when the property is present.
- JMAP: Respect query arguments in `Email/queryChanges`.
- JMAP: Return the correct container/item change id when there are no changes.
## [0.15.4] - 2026-01-19
If you are upgrading from v0.14.x and below, this version includes **multiple breaking changes**. Please read the [upgrading documentation](https://github.com/stalwartlabs/stalwart/blob/main/UPGRADING/v0_15.md) for more information on how to upgrade from previous versions.
If you are upgrading from v0.15.x, replace the binary and update the webadmin.
## Added
- IMAP: Map `HEADER SUBJECT/FROM/TO` searches to `SUBJECT/FROM/TO` queries.
- Sieve: Update spam status on user scripts.
## Changed
## Fixed
- Search: Return all document ids when no filters are provided.
- Search: Filters not applied when a single message is in the account.
- IMAP: Return `ALREADYEXISTS` code when creating existing mailboxes.
- IMAP: Do not return quota resources if no quota is set.
- JMAP/changes: Update `newState` with last changeId if an invalid fromChangeId is provided.
- JMAP/CalendarIdentity: Do not update invalid calendar identities.
- AI API: Include request error details if available.
## [0.15.3] - 2025-12-29
If you are upgrading from v0.14.x and below, this version includes **multiple breaking changes**. Please read the [upgrading documentation](https://github.com/stalwartlabs/stalwart/blob/main/UPGRADING/v0_15.md) for more information on how to upgrade from previous versions.
If you are upgrading from v0.15.x, replace the binary and update the webadmin.
## Added
- Polish locale support (contributed by @mrxkp) (#2480)
## Changed
## Fixed
- Meilisearch: Return correct error messages when failing to create indexes (#2574)
- PostgreSQL search: Truncate emails to 650kb for full-text search indexing.
- FoundationDB search: Batch large transactions (#2567).
- Spam filter: Fix training sample size checks
- IMAP: Fix UTF7 encoding with Emojis (contributed by @dojiong) (#2564).
## [0.15.2] - 2025-12-22
If you are upgrading from v0.14.x and below, this version includes **multiple breaking changes**. Please read the [upgrading documentation](https://github.com/stalwartlabs/stalwart/blob/main/UPGRADING/v0_15.md) for more information on how to upgrade from previous versions.
If you are upgrading from v0.15.x, replace the binary and update the webadmin.
## Added
- OAuth: Add device authorization endpoint (#2225).
## Changed
- Antispam: Only auto-learn spam from traps or multiple RBL hits.
## Fixed
- mySQL search: Use `MEDIUMTEXT` field type for email body and attachments (#2544).
- PostgreSQL search: Truncate large text fields.
- ElasticSearch: Implement pagination (#2551).
- Antispam: Fix `NO_SPACE_IN_FROM` spam tag detection logic (#2372).
- IMAP: Fix shared folder double nesting (test suite credits to @ochnygosch) (#2358).
- JMAP: Use latest `Received` header in JMAP `Email/import` (credits to @apexskier) (#2374).
- JMAP: Return unsorted search results when the index is not ready (#2544).
- LDAP: Lowercase attribute comparison (credits to @pdf) (#2363).
- CLI: Fix same-host JMAP redirection on non-standard ports (#2271).
## [0.15.1] - 2025-12-17
This version includes **multiple breaking changes**. If you are upgrading from v0.14.x and below, please read the [upgrading documentation](https://github.com/stalwartlabs/stalwart/blob/main/UPGRADING/v0_15.md) for more information on how to upgrade from previous versions.
## Added
## Changed
## Fixed
- PostgreSQL: Sanitize search index values (#2533)
- Elasticsearch: Ignore `resource_already_exists_exception` errors when creating indexes (#2535)
- Migrate 0.13.x data (#2534)
## [0.15.0] - 2025-12-16
This version includes **multiple breaking changes**. Please read the [upgrading documentation](https://github.com/stalwartlabs/stalwart/blob/main/UPGRADING/v0_15.md) for more information on how to upgrade from previous versions.
## Added
- Linear spam classifier using FTRL-Proximal and feature/cuckoo hashing.
- Meilisearch store backend implementation (#1482).
- PostgreSQL and mySQL native full-text search support.
- Multiple performance improvements and database access optimizations.
- Encryption-at-rest: Spam training privacy setting.
- Enterprise: Undelete e-mail feature now includes From/Subject/Received information.
- IMAP: Implemented new keywords and mailbox attributes described in [draft-ietf-mailmaint-messageflag-mailboxattribute-13](https://datatracker.ietf.org/doc/html/draft-ietf-mailmaint-messageflag-mailboxattribute-13)
## Changed
- IMAP: Always return special use flags in responses.
## Fixed
- JMAP: `FileNode/set` fails to delete files (#2485).
- JMAP: Return error when using `blobId` in JSContact and JSCalendar (#2431).
- Directory: Deletion of list or domain issues (#2415).
- MTA: Headers and body stripped from mail delivery subsystem failure notifications (#2344).
- MTA: Hooks only run if sieve script, milter or rewrite is configured (#2317).
- Autodiscover: Endpoint should be case insensitive (#2440).
- Housekeeper: Panic during DST transition (#2366).
- Import/Export: Fix import/export utility (#1882).
- Enterprise: Remove tenant admin permissions when license is invalid.
## [0.14.1] - 2025-10-28
If you are upgrading from v0.13.4 and below, this version includes **breaking changes** to the internal directory, calendar and contacts. Please read the [upgrading documentation](https://github.com/stalwartlabs/stalwart/blob/main/UPGRADING/v0_14.md) for more information on how to upgrade from previous versions.
## Added
- Autoconfig for CalDAV, CardDAV and WebDAV (#1937)
## Changed
- HTTP: Remove HTTP STS `preload` directive.
## Fixed
- Directory: Keep OTP Auth and AppPasswords unless the remote directory provides new ones (#2319)
- JMAP: Fix `ContactCard/set` and `CalendarEvent/set` destroy methods (#2308).
## [0.14.0] - 2025-10-22
If you are upgrading from v0.13.4 and below, this version includes **breaking changes** to the internal directory, calendar and contacts. Please read the [upgrading documentation](https://github.com/stalwartlabs/stalwart/blob/main/UPGRADING/v0_14.md) for more information on how to upgrade from previous versions.
## Added
- JMAP for Calendars ([draft-ietf-jmap-calendars](https://datatracker.ietf.org/doc/draft-ietf-jmap-calendars/)).
- JMAP for Contacts ([RFC 9610](https://datatracker.ietf.org/doc/rfc9610/)).
- JMAP for File Storage ([draft-ietf-jmap-filenode](https://datatracker.ietf.org/doc/draft-ietf-jmap-filenode/)).
- JMAP Sharing ([RFC 9670](https://datatracker.ietf.org/doc/rfc9670/))
- CalDAV: support for `supported-calendar-component-set` (#1893)
- i18n: Greek language support (contributed by @infl00p)
- i18n: Swedish language support (contributed by @purung)
## Changed
- **Breaking Database Changes** (migrated automatically on first start):
- Internal directory schema changed.
- Calendar and Contacts storage schema changed.
- Sieve scripts storage schema changed.
- Push Subscriptions storage schema changed.
- Replaced `sieve.untrusted.limits.max-scripts` and `jmap.push.max-total` with `object-quota.*` settings.
- Cluster node roles now allow sharding.
## Fixed
- Push Subscription: Clean-up of expired subscriptions and cluster notification of changes (#1248)
- CalDAV: Per-user CalDAV properties (#2058)
## [0.13.4] - 2025-09-30
If you are upgrading from v0.11.x or v0.12.x, this version includes **breaking changes** to the message queue and MTA configuration. Please read the [UPGRADING.md](https://github.com/stalwartlabs/stalwart/blob/main/UPGRADING.md) file for more information on how to upgrade from previous versions.
## Added
## Changed
- JMAP: Protocol layer rewrite for zero-copy deserialization and architectural improvements.
## Fixed
- IMAP: Unbounded memory allocation in request parser ([CVE-2025-61600 ](https://github.com/stalwartlabs/stalwart/security/advisories/GHSA-8jqj-qj5p-v5rr)).
- IMAP: Wrong permission checked for GETACL.
- JMAP: References to previous method fail when there are no results (#1507).
- JMAP: Enforce quota checks on `Blob/copy`.
- JMAP: `Mailbox/get` fails without `accountId` argument (#1936).
- JMAP: Do not return `invalidProperties` when email update doesn't contain changes (#1139)
- iTIP: Include date properties in `REPLY` (#2102).
- OIDC: Do not set `username` field if it is the same as the `email` field.
- Telemetry: Fix `calculateMetrics` housekeeper task (#2155).
- Directory: Always use `rsplit` to extract the domain part from email addresses.
## [0.13.3] - 2025-09-10
If you are upgrading from v0.11.x or v0.12.x, this version includes **breaking changes** to the message queue and MTA configuration. Please read the [UPGRADING.md](https://github.com/stalwartlabs/stalwart/blob/main/UPGRADING.md) file for more information on how to upgrade from previous versions.
## Added
- CLI: Health checks (contributed by @Codekloeppler)
## Changed
- WebDAV: Assisted discovery v2
## Fixed
- iTIP: Do not send a REPLY when deleting an event that was not accepted.
- iTIP: Include event details in REPLY messages (#2102).
- iTIP: Add organizer to iMIP replies if missing to deal with MS Exchange 2010 bug.
- OIDC: Do not overwrite locally defined aliases (#2065).
- HTTP: Scan ban should only be triggered by HTTP parse errors.
- HTTP: Skip scanner fail2ban checks when the proxy client IP can't be parsed (#2121).
- JMAP: Do not allow roles to be removed from system mailboxes (#1977).
- JMAP WS: Fix panic when using invalid server url.
- SMTP: Do no send `EHLO` twice when `STARTTLS` is unavailable (#2050).
- IMAP: Allow `ENABLE UTF8` in IMAPrev1.
- IMAP: Include `administer` permission in ACL responses.
- IMAP: Add owner rights to ACL get responses.
- IMAP: Do not auto-train Bayes when moving messages from Junk to Trash.
- IMAP/ManageSieve: Increase maximum quoted argument size (#2039).
- CalDAV: Limit recurrence expansions in calendar reports ([CVE-2025-59045](https://github.com/stalwartlabs/stalwart/security/advisories/GHSA-xv4r-q6gr-6pfg)).
- WebDAV: Do not fix percent encoding on WebDAV FS (#2036).
## [0.13.2] - 2025-07-28
If you are upgrading from v0.11.x or v0.12.x, this version includes **breaking changes** to the message queue and MTA configuration. Please read the [UPGRADING.md](https://github.com/stalwartlabs/stalwart/blob/main/UPGRADING.md) file for more information on how to upgrade from previous versions.
## Added
- ACME: DeSEC cloud DNS provider support (contributed by @Tyr3al).
- ACME: OVH cloud DNS provider support (contributed by @srachner).
- CalDAV Scheduling: Catalan language support (contributed by @jolupa) (#1873).
- MTA: Allow to send e-mails as group, while member of that group (#485).
- OIDC: Allow local access tokens to be used with third-party OIDC backends (#1311 stalwartlabs/webadmin#52).
## Changed
- IMAP: Return `OK` when moving/copying non-existent messages (#670).
- IMAP: Copy flags when copying/moving messages between accounts.
## Fixed
- MTA: Do not convert e-mail local parts to lowercase (#1916).
- Sieve: `fileinto` should override spam filter (#1917).
- JMAP: Incorrect `accountId` used in email set and import methods (#1777).
- WebDAV: Always return `MULTISTATUS` when calendar-query yields no results.
- LDAP: Only set account name if not returned in LDAP query (#1471).
- Enterprise: Invalidate logo cache when changes are made (#1856).
- Enterprise: Fix tenant quota update API.
## [0.13.1] - 2025-07-16
If you are upgrading from v0.11.x or v0.12.x, this version includes **breaking changes** to the message queue and MTA configuration. Please read the [UPGRADING.md](https://github.com/stalwartlabs/stalwart/blob/main/UPGRADING.md) file for more information on how to upgrade from previous versions.
## Added
- ACME: DigitalOcean cloud DNS provider support (#1667).
## Changed
## Fixed
- Migration: Old queue events not deleted causing high CPU usage in some deployments (#1833).
- MTA: `mta-sts` setting parsing issue (#1830).
- JMAP: `sortOrder` should not be null (#1831).
- Allow invalid TOML when parsing database settings (#1822).
## [0.13.0] - 2025-07-15
If you are upgrading from v0.11.x or v0.12.x, this version includes **breaking changes** to the message queue and MTA configuration. Please read the [UPGRADING.md](https://github.com/stalwartlabs/stalwart/blob/main/UPGRADING.md) file for more information on how to upgrade from previous versions.
## Added
- MTA queue enhancements (#1246 #1035 #457).
- Danish locale support (contributed by @Fadil2k) (#1772).
- DKIM support for `stalwart-cli` (contributed by @rmsc) (#1804).
## Changed
- Invalidate access token caches in a cluster using pub/sub (#1741).
- Allow updating secrets for all directory types.
## Fixed
- WebDAV: Return all shared resources in `calendar-home-set` and `addressbook-home-set` (#1796).
- WebDAV ACL: Fix write permission and `multiget` reports (#1768).
- CalDAV Scheduling: Include `DTSTART`/`DTEND` properties in iMIP `CANCEL` messages (#1775).
- HTTP: Do not include `WWW-Authenticate` headers in API responses (#1795).
- API: Allow API keys to be used with external directories (#1815).
- IMAP: Fix issue creating subfolders under INBOX for group shared folder (#1817).
- IMAP: Custom Name for Shared Folders ignored (#1620).
- LDAP: `local` placeholder should return username when its not an email address (#1784).
## [0.12.5] - 2025-06-25
If you are upgrading from v0.11.x, this version includes **breaking changes** to the database layout and requires a migration. Please read the [UPGRADING.md](https://github.com/stalwartlabs/stalwart/blob/main/UPGRADING.md) file for more information on how to upgrade from previous versions.
## Added
- Calendar Scheduling Extensions to CalDAV - RFC6368 (#1514)
- Calendar E-Mail Notifications (#1514)
- Limited i18n support for calendaring events.
- Assisted CalDAV/CardDAV shared resource discovery (#1691).
## Changed
- JMAP: Allow unauthenticated access to JMAP session object.
## Fixed
- WebDAV: Return NOTFOUND error instead of MULTISTATUS on empty PROPFIND responses (#1657).
- WebDAV: Update account name when refreshing DAV caches (#1694).
- JMAP: Do not include email address in identity names (#1688).
- IMAP: Normalize `INBOX` name when creating/renaming folders (#1636).
- LDAP: Request `secret-changed` attribute in LDAP queries (#1409).
- Branding: Unable to change logos (#1652).
- Antispam: Skip `card-is-ham` override when sender does not pass DMARC (#1648).
- FoundationDB: Renew old/expired FDB read transactions after the `1007` error code is received rather than estimating expiration time.
## [0.12.4] - 2025-06-03
If you are upgrading from v0.11.x, this version includes **breaking changes** to the database layout and requires a migration. Please read the [UPGRADING.md](https://github.com/stalwartlabs/stalwart/blob/main/UPGRADING.md) file for more information on how to upgrade from previous versions.
## Added
- LDAP authentication enhancements (#1269 #1471 #795 #1496).
- MTA: Return Queue IDs during message acceptance (#927).
## Changed
- LDAP: `bind.auth.enable` is now `bind.auth.method`, read the updated [LDAP documentation](https://stalw.art/docs/auth/backend/ldap) for more information.
## Fixed
- DNS: `hickory-resolver` bug hitting 100% CPU usage when resolving DNSSEC records.
- IMAP: Return the message UID in the destination mailbox if the message already exists (#1201).
- MTA: TLS reports being issued for sent TLS reports (infinite loop) (#1301).
- WebDAV: Return `CTag` on `/dav/cal/account` resources to force iOS synchronize.
- CardDAV: Strict vCard parsing (#1607).
- WebDAV: Dead property updates (#1611).
- WebDAV: Use last change id in `CTag`.
## [0.12.3] - 2025-05-30
If you are upgrading from v0.11.x, this version includes **breaking changes** to the database layout and requires a migration. Please read the [UPGRADING.md](https://github.com/stalwartlabs/stalwart/blob/main/UPGRADING.md) file for more information on how to upgrade from previous versions.
## Added
- Store vanished IMAP UIDs and WebDAV paths in the changelog.
## Changed
## Fixed
- XML `CDATA` injection (credits to @andreymal for the report).
- Macro references are replaced with their content when writing config file (#1595).
- Double nested CalDAV and CardDAV property tags (#1591).
- Allow empty properties in PROPPATCH requests (#1580).
## [0.12.2] - 2025-05-27
If you are upgrading from v0.11.x, this version includes **breaking changes** to the database layout and requires a migration. Please read the [UPGRADING.md](https://github.com/stalwartlabs/stalwart/blob/main/UPGRADING.md) file for more information on how to upgrade from previous versions.
## Added
- CardDAV: Legacy vCard 2.1 and 3.0 serialization support.
- WebDAV: Add SRV Records to help DAV autodiscovery (closes #1565).
## Changed
## Fixed
- Report list attempts to deserialize empty values (#1562)
- Refresh expired FoundationDB transactions while retrieving large blobs (#1555).
## [0.12.1] - 2025-05-26
If you are upgrading from v0.11.x, this version includes **breaking changes** to the database layout and requires a migration. Please read the [UPGRADING.md](https://github.com/stalwartlabs/stalwart/blob/main/UPGRADING.md) file for more information on how to upgrade from previous versions.
## Added
## Changed
## Fixed
- Migration tool to generate the correct next id (#1561).
- Failed to parse setting dav.lock.max-timeout (closes #1559).
- Failed to build OpenTelemetry span exporter: no http client specified (#1571).
## [0.12.0] - 2025-05-26
This version includes **breaking changes** to the database layout and requires a migration. Please read the [UPGRADING.md](https://github.com/stalwartlabs/stalwart/blob/main/UPGRADING.md) file for more information on how to upgrade from previous versions.
### Added
- [Collaboration](https://stalw.art/docs/collaboration/overview) features including [Calendars over CalDAV](https://stalw.art/docs/http/calendar/), [Contacts over CardDAV](https://stalw.art/docs/http/contact/) and [File Storage over WebDAV](https://stalw.art/docs/http/file-storage/).
- Peer-to-peer [cluster coordination](https://stalw.art/docs/cluster/coordination/overview) or with Apache Kafka, Redpanda, NATS or Redis.
- Incremental caching of emails, calendars, contacts and file metadata.
- Zero-copy deserialization.
- Train spam messages as ham when the sender is in the user's address book.
- `XOAUTH2` SASL mechanism support (#1194 #1369).
- Support for RFC9698, the `JMAPACCESS` Extension for IMAP.
- Search index for accounts and other principals (#1368).
- Add `description` property to OIDC ID token (#1234).
### Changed
- Deprecated gossip protocol in favor of the new [coordinator](https://stalw.art/docs/cluster/coordination/overview) options.
- Renamed Git repository from `stalwartlabs/mail-server` to `stalwartlabs/stalwart` and the Docker image from `stalwartlabs/mail-server` to `stalwartlabs/stalwart`.
- Renamed multiple settings:
- `server.http.*` to `http.*`.
- `jmap.folders.*` to `email.folders.*`.
- `jmap.account.purge.frequency` to `account.purge.frequency`.
- `jmap.email.auto-expunge` to `email.auto-expunge`.
- `jmap.protocol.changes.max-history` to `changes.max-history`.
- `storage.encryption.*` to `email.encryption.*`.
- Deprecated `lookup.default.*` settings in favor of `server.hostname` and `report.domain`. v0.11 and before supported both, v0.12 will only support the new settings.
### Fixed
- Allow undiscovered UIDs to be used in IMAP `COPY`/`MOVE` operations (#1201).
- Refuse loopback SMTP delivery (#1377).
- Hide the current server version (#1435).
- Use the newest `X-Spam-Status` Header (#1308).
- MySQL Driver error: Transactions couldn't be nested (#1271).
- Spawn a delivery thread for `EmailSubmission/set` requests (#1540).
- ACME: Don't restrict challenge types (#1522).
- Autoconfig: return `%EMAILADDRESS%` if no e-mail address is provided (#1537).
## [0.11.8] - 2025-04-30
To upgrade replace the `stalwart-mail` binary and then upgrade to the latest web-admin.
### Added
### Changed
### Fixed
- Allow undiscovered UIDs to be used in `COPY`/`MOVE` operations (#1201).
## [0.11.7] - 2025-03-23
To upgrade replace the `stalwart-mail` binary and then upgrade to the latest web-admin.
### Added
- LDAP attribute to indicate password change (#1156).
### Changed
- Lazy DKIM key parsing (#1211).
- Enable `edns0` for system resolver by default (#1282).
- Bump FoundationDB to `7.3`.
### Fixed
- Fix incorrect `UIDNEXT` when mailbox is empty (#1201).
- Sender variable not set when evaluating `must-match-sender` (#1294).
- Do not panic when mailboxId is not found (#1293).
- Prioritize local over span keys when serializing webhook payloads (#1250).
- Allow TLS name mismatch as per RFC7671 Section 5.1.
- Try with implicit MX when no MX records are found.
- SQL `secrets` directory query.
## [0.11.5] - 2025-02-01
To upgrade replace the `stalwart-mail` binary and then upgrade to the latest web-admin.
### Added
### Changed
- Open source third party OIDC support.
### Fixed
- Case insensitive flag parsing (#1138).
- BCC not removed from JMAP EmailSubmissions (#618).
- Group pipelined IMAP FETCH and STATUS operations (#1096).
## [0.11.4] - 2025-01-29
To upgrade replace the `stalwart-mail` binary and then upgrade to the latest web-admin.
### Added
- RFC 9208 - IMAP QUOTA Extension (#484).
### Changed
- `session.throttle.*` is now `queue.limiter.inbound.*`.
- `queue.throttle.*` is now `queue.limiter.outbound.*`.
- Changed DNSBL error level to debug (#1107).
### Fixed
- Creating a mailbox in a shared folder results in wrong hierarchy (#1128).
- IMAP LIST-STATUS (RFC 5819) returns items in wrong order (#1129).
- Avoid non-RFC SMTP status codes (#1109).
- Do not DNSBL check invalid domains (#1107).
- Sieve message flag parser (#1059).
- Sieve script import case insensitivity (#962).
- `mailto:` parsing in HTMLs.
## [0.11.2] - 2025-01-17
To upgrade replace the `stalwart-mail` binary and then upgrade to the latest web-admin.
### Added
- Automatic revoking of access tokens when secrets, permissions, ACLs or group memberships change (#649).
- Increased concurrency for local message delivery (configurable via `queue.threads.local`).
- Cluster node roles.
- `config_get` expression function.
### Changed
- `queue.outbound.concurrency` is now `queue.threads.remote`.
- `lookup.default.hostname` is now `server.hostname`.
- `lookup.default.domain` is now `report.domain`.
### Fixed
- Distributed locking issues in non-Redis stores (#1066).
- S3 incorrect backoff wait time after failures.
- Panic parsing broken HTMLs.
- Update CLI response serializer to v0.11.x (#1082).
- Histogram bucket counts (#1079).
- Do not rate limit trusted IPs (#1078).
- Avoid double encrypting PGP parts encoded as plain text (#1083).
- Return empty SASL challenge rather than "" (#1064).
## [0.11.0] - 2025-01-06
This version includes breaking changes to the configuration file, please read [UPGRADING.md](UPGRADING.md) for details.
To upgrade replace the `stalwart-mail` binary and then upgrade to the latest web-admin.
### Added
- Spam filter rewritten in Rust for a significant performance improvement.
- Multiple spam filter improvements (#947) such as training spam/ham when moving between inbox and spam folders (#819).
- Improved distributed locking and handling of large distributed SMTP queues.
- ASN and GeoIP lookups.
- Bulk operations REST endpoints (#925).
- Faster S3-FIFO caching.
- Support adding the `Delivered-To` header (#916).
- Semver compatibility checks when upgrading (#844).
- Sharded In-Memory Store.
### Changed
- Removed authentication rate limit (no longer necessary since there is fail2ban).
- Pipes have been deprecated in favor of MTA hooks.
### Fixed
- OpenPGP EOF error (#1024).
- Convert emails obtained from external directories to lowercase (#1004).
- LDAP: Support both name and email fields to be mapped to the same attribute.
- Admin role can't be assigned if an account with the same name exists.
- Fix macro detection in DNS record generation (#978).
- Use host FQDN in install script (#1003).
## [0.10.7] - 2024-12-04
To upgrade replace the `stalwart-mail` binary and then upgrade to the latest web-admin.
### Added
- Delivery and DMARC Troubleshooting (#420).
- Support for external email addresses on mailing lists (#152).
- Azure blob storage support.
### Changed
### Fixed
- Some mails can't be moved out of the junk folder (#670).
- Out of bound index error on Sieve script (#941).
- Missing `User-Agent` header for ACME (#937).
- UTF8 support in IMAP4rev1 (#948).
- Account alias owner leak on autodiscover.
- Include all events in OTEL traces + Include spanId in webhooks.
- Implement `todo!()` causing panic on concurrency and rate limits.
- Mark SQL store as active if used as a telemetry store.
- Discard empty form submissions.
## [0.10.6] - 2024-11-07
To upgrade replace the `stalwart-mail` binary and then upgrade to the latest web-admin.
### Added
- Enterprise license automatic renewals before expiration (disabled by default).
- Allow to LDAP search using bind dn instead of auth bind connection when bind auth is enabled (#873)
### Changed
### Fixed
- Include `preferred_username` and `email` in OIDC `id_token`.
- Verify roles and permissions when creating or modifying accounts (#874)
## [0.10.5] - 2024-10-15
To upgrade replace the `stalwart-mail` binary.
### Added
- Data store CLI.
### Changed
### Fixed
- Tokenizer performance issue (#863)
- Incorrect AI model endpoint setting.
## [0.10.4] - 2024-10-08
To upgrade replace the `stalwart-mail` binary and then upgrade to the latest web-admin.
### Added
- Detect and ban port scanners as well as other forms of abuse (#820).
- ACME External Account Binding support (#379).
### Changed
- The settings `server.fail2ban.*` have been moved to `server.auto-ban.*`.
- The event `security.brute-force-ban` is now `security.abuse-ban`.
### Fixed
- Do not send SPF failures reports to local domains.
- Allow `nonce` in OAuth code requests.
- Warn when there are errors migrating domains rather than aborting migration.
## [0.10.3] - 2024-10-07
To upgrade replace the `stalwart-mail` binary and then upgrade to the latest web-admin. Enterprise users wishing to use the new LLM-powered spam filter should also upgrade the spam filter rules.
### Added
- AI-powered Spam filtering and Sieve scripting (Enterprise feature).
### Changed
- The untrusted Sieve interpreter now has the `vnd.stalwart.expressions` extension enabled by default. This allows Sieve users to use the `eval` function to evaluate expressions in their scripts. If you would like to disable this extension, you can do so by adding `vnd.stalwart.expressions` to `sieve.untrusted.disabled-capabilities`.
### Fixed
- S3-compatible backends: Retry on `5xx` errors.
- OIDC: Include `nonce` parameter in `id_token` response.
## [0.10.2] - 2024-10-02
To upgrade first upgrade the webadmin and then replace the `stalwart-mail` binary. If you read these instructions too late, you can upgrade to the latest web-admin using `curl -k -u admin:yourpass https://yourserver/api/update/webadmin`.
### Added
- OpenID Connect server (#298).
- OpenID Connect backend support (Enterprise feature).
- OpenID Connect Dynamic Client Registration (#4)
- OAuth 2.0 Dynamic Client Registration Protocol ([RFC7591](https://datatracker.ietf.org/doc/html/rfc7591)) (#136)
- OAuth 2.0 Token Introspection ([RFC7662](https://datatracker.ietf.org/doc/html/rfc7662)).
- Contact form submission handling.
- `webadmin.path` setting to override unpack directory (#792).
### Changed
### Fixed
- Missing `LIST-STATUS` from RFC5819 in IMAP capability responses (#816).
- Do not allow tenant domains to be deleted if they have members (#812).
- Tenant principal limits (#810).
## [0.10.1] - 2024-09-26
To upgrade replace the `stalwart-mail` binary.
### Added
- `OAUTHBEARER` SASL support in all services (#627).
### Changed
### Fixed
- Fixed `migrate_directory` range scan (#784).
## [0.10.0] - 2024-09-21
This version includes breaking changes to how accounts are stored. Please read [UPGRADING.md](UPGRADING.md) for details.
### Added
- Multi-tenancy (Enterprise feature).
- Branding (Enterprise feature).
- Roles and permissions.
- Full-text search re-indexing.
- Partial database backups (#497).
### Changed
### Fixed
- IMAP `IDLE` support for command pipelining, aka the Apple Mail iOS 18 bug (#765).
- Case insensitive INBOX `fileinto` (#763).
- Properly decode undelete account name (#761).
## [0.9.4] - 2024-09-09
To upgrade replace the `stalwart-mail` binary and then upgrade to the latest web-admin.
### Added
- Support for global Sieve scripts that can be used by users to filter their incoming mail.
- Allow localhost to override HTTP access controls to prevent lockouts.
### Changed
- Sieve runtime error default log level is now `debug`.
### Fixed
- Ignore INBOX case on Sieve's `fileinto` (#725)
- Local keys parsing and retrieval issues.
- Lookup reload does not include database settings.
- Account count is incorrect.
## [0.9.3] - 2024-08-29
To upgrade replace the `stalwart-mail` binary and then upgrade to the latest web-admin.
### Added
- Dashboard (Enterprise feature)
- Alerts (Enterprise feature)
- SYN Flood (session "loitering") attack protection (#482)
- Mailbox brute force protection (#688)
- Mail from is allowed (`session.mail.is-allowed`) expression (#609)
### Changed
- `authentication.fail2ban` setting renamed to `server.fail2ban.authentication`.
- Added elapsed times to message filtering events.
### Fixed
- Include queueId in MTA Hooks (#708)
- Do not insert empty keywords in FTS index.
## [0.9.2] - 2024-08-21
To upgrade replace the `stalwart-mail` binary and then upgrade to the latest web-admin.
### Added
- Message delivery history (Enterprise feature)
- Live tracing and logging (Enterprise feature)
- SQL Read Replicas (Enterprise feature)
- Distributed S3 Blob Store (Enterprise feature)
### Changed
### Fixed
- Autodiscover request parser issues.
- Do not create tables when using SQL as an external directory (fixes #291)
- Do not hardcode logger id (fixes #348)
- Include `Forwarded-For IP` address in `http.request-url` event (fixes #682)
## [0.9.1] - 2024-08-08
To upgrade replace the `stalwart-mail` binary and then upgrade to the latest web-admin.
### Added
- Metrics support (closes #478)
- OpenTelemetry Push Exporter
- Prometheus Pull Exporter (closes #275)
- HTTP endpoint access controls (closes #266 #329 #542)
- Add `options` setting to PostgreSQL driver (closes #662)
- Add `isActive` property to defaults on Sieve/get JMAP method (closes #624)
### Changed
- Perform `must-match-sender` checks after sender rewriting (closes #394)
- Only perform email ingest duplicate check on the target mailbox (closes #632)
### Fixed
- Properly parse `Forwarded` and `X-Forwarded-For` headers (fixes #669)
- Resolve DKIM macros when generating DNS records (fixes #666)
- Fixed `is_local_domain` Sieve function (fixes #622)
## [0.9.0] - 2024-08-01
To upgrade replace the `stalwart-mail` binary and then upgrade to the latest web-admin. This version includes breaking changes to the Webhooks configuration and produces a slightly different log output, read [UPGRADING.md](UPGRADING.md) for details.
### Added
- Improved and faster tracing and logging.
- Customizable event logging levels.
### Changed
### Fixed
- ManageSieve: Return capabilities after successful `STARTTLS`
- Do not provide `{auth_authen}` Milter macro unless the user is authenticated
## [0.8.5] - 2024-07-07
To upgrade replace the `stalwart-mail` binary.
### Added
- Restore deleted e-mails (Enterprise Edition only)
- Kubernetes (K8S) livenessProbe and readinessProbe endpoints.
### Changed
- Avoid sending reports for DMARC/delivery reports (#173)
### Fixed
- Refresh old FoundationDB read transactions (#520)
- Subscribing shared mailboxes doesn't work (#251)
## [0.8.4] - 2024-07-03
To upgrade replace the `stalwart-mail` binary.
### Added
### Changed
### Fixed
- Fix TOTP validation order.
- Increase Jemalloc page size on armv7 builds.
## [0.8.3] - 2024-07-01
To upgrade replace the `stalwart-mail` binary and then upgrade to the latest web-admin.
### Added
- Two-factor authentication with Time-based One-Time Passwords (#436)
- Application passwords (#479).
- Option to disable user accounts.
### Changed
- DANE success on EndEntity match regardless of TrustAnchor validation.
### Fixed
- Fix ManageSieve GETSCRIPT response: Add missing CRLF (#563)
- Do not return CAPABILITIES after ManageSieve AUTH=PLAIN SASL exchange (#548)
- POP3 QUIT must write a response (#568)
## [0.8.2] - 2024-06-22
To upgrade replace the `stalwart-mail` binary and then upgrade to the latest web-admin and spam filter versions.
### Added
- Webhooks support (#480)
- MTA Hooks (like milter but over HTTP)
- Manually train and test spam classifier (#473 #264 #257 #471)
- Allow configuring default mailbox names, roles and subscriptions (#125 #290 #458 #498)
- Include `robots.txt` (#542)
### Changed
- Milter support on all SMTP stages (#183)
- Do not announce `STARTTLS` if the listener does not support it.
### Fixed
- Incoming reports stored in the wrong subspace (#543)
- Return `OK` after a successful ManageSieve SASL authentication flow (#187)
- Case-insensitive search in settings API (#487)
- Fix `session.rcpt.script` default variable name (#502)
## [0.8.1] - 2024-05-23
To upgrade replace the `stalwart-mail` binary and then upgrade to the latest web-admin and spam filter versions.
### Added
- POP3 support.
- DKIM signature length exploit protection.
- Faster email deletion.
- Junk/Trash folder auto-expunge and changelog auto-expiry (#403)
- IP allowlists.
- HTTP Strict Transport Security option.
- Add TLS Reporting DNS entry (#464).
### Changed
- Use separate account for master user.
- Include server hostname in SMTP greetings (#448).
### Fixed
- IP addresses trigger `R_SUSPICIOUS_URL` false positive (#461 #419).
- JMAP identities should not return null signatures.
- Include authentication headers and check queue quotas on Sieve message forwards.
- ARC seal using just one signature.
- Remove technical subdomains from MTA-STS policies and TLS records (#429).
## [0.8.0] - 2024-05-13
This version uses a different database layout which is incompatible with previous versions. Please read the [UPGRADING.md](UPGRADING.md) file for more information on how to upgrade from previous versions.
### Added
- Clustering support with node auto-discovery and partition-tolerant failure detection.
- Autoconfig and MS Autodiscover support (#336)
- New variables `retry_num`, `notify_num`, `last_error` add `last_status` available in queue expressions.
- Performance improvements, in particular for FoundationDB.
- Improved full-text indexing with lower disk space usage.
- MTA-STS policy management.
- TLSA Records generation for DANE (#397)
- Queued message visualization from the web-admin.
- Master user support.
### Changed
- Make `certificate.*` local keys by default.
- Removed `server.run-as.*` settings.
- Add Microsoft Office Macro types to bad mime types (#391)
### Fixed
- mySQL TLS support (#415)
- Resolve file macros after dropping root privileges.
- Updated order of SPF Records (#395).
- Avoid duplicate accountIds when using case insensitive external directories (#399)
- `authenticated_as` variable not usable for must-match-sender (#372)
- Remove `StandardOutput`, `StandardError` in service (#390)
- SMTP `AUTH=LOGIN` compatibility issues with Microsoft Outlook (#400)
## [0.7.3] - 2024-05-01
To upgrade replace the `stalwart-mail` binary and then upgrade to the latest web-admin version.
### Added
- Full database export and import functionality
- Add --help and --version command line arguments (#365)
- Allow catch-all addresses when validating must match sender
### Changed
- Add `groupOfUniqueNames` to the list of LDAP object classes
### Fixed
- Trim spaces in DNS-01 ACME secrets (#382)
- Allow only one journald tracer (#375)
- `authenticated_as` variable not usable for must-match-sender (#372)
- Fixed `BOGUS_ENCRYPTED_AND_TEXT` spam filter rule
- Fixed parsing of IPv6 DNS server addresses
## [0.7.2] - 2024-04-17
To upgrade replace the `stalwart-mail` binary and then upgrade to the latest web-admin version.
### Added
- Support for `DNS-01` and `HTTP-01` ACME challenges (#226)
- Configurable external resources (#355)
### Changed
### Fixed
- Startup failure when Elasticsearch is down/starting up (#334)
- URL decode path elements in REST API.
## [0.7.1] - 2024-04-12
To upgrade replace the `stalwart-mail` binary.
### Added
- Make initial admin password configurable via env (#311)
### Changed
- WebAdmin download URL.
### Fixed
- Remove ASN.1 DER structure from DKIM ED25519 public keys.
- Filter out invalid timestamps on log entries.
## [0.7.0] - 2024-04-09
This version uses a different database layout and introduces multiple breaking changes in the configuration files. Please read the [UPGRADING.md](UPGRADING.md) file for more information on how to upgrade from previous versions.
### Added
- Web-based administration interface.
- REST API for management and configuration.
- Automatic RSA and ED25519 DKIM key generation.
- Support for compressing binaries in the blob store (#227).
- Improved performance accessing IMAP mailboxes with a large number of messages.
- Support for custom DNS resolvers.
- Support for multiple loggers with different levels and outputs.
### Changed
### Fixed
- Store quotas as `u64` rather than `u32`.
- Second IDLE connections disconnects the first one (#280).
- Use relaxed DNS parsing, allowing underscores in DNS labels (#172).
- Escape regexes within `matches()` expressions (#155).
- ManageSieve LOGOUT should reply with `OK` instead of `BYE`.
## [0.6.0] - 2024-02-14
This version introduces breaking changes in the configuration file. Please read the [UPGRADING.md](UPGRADING.md) file for more information on how to upgrade from previous versions.
### Added
- Distributed and fault-tolerant SMTP message queues.
- Distributed rate-limiting and fail2ban.
- Expressions in configuration files.
### Changed
### Fixed
- Do not include `STATUS` in IMAP `NOOP` responses (#234).
- Allow multiple SMTP `HELO` commands.
- Redirect OAuth using a `301` instead of a `307` code.
## [0.5.3] - 2024-01-14
Please read the [UPGRADING.md](UPGRADING.md) file for more information on how to upgrade from previous versions.
### Added
- Built-in [fail2ban](https://stalw.art/docs/server/fail2ban) and IP address/mask blocking (#164).
- CLI: Read URL and credentials from environment variables (#88).
- mySQL driver: Add `max-allowed-packet` setting (#201).
### Changed
- Unified storage settings for all services (read the [UPGRADING.md](UPGRADING.md) for details)
### Fixed
- IMAP retrieval of auto-encrypted emails (#203).
- mySQL driver: Parse `timeout.wait` property as duration (#202).
- `X-Forwarded-For` header on JMAP Rate-Limit does not work (#208).
- Use timeouts in install script (#138).
## [0.5.2] - 2024-01-07
Please read the [UPGRADING.md](UPGRADING.md) file for more information on how to upgrade from previous versions.
### Added
- [ACME](https://stalw.art/docs/server/tls/acme) support for automatic TLS certificate generation and renewal (#160).
- TLS certificate [hot-reloading](https://stalw.art/docs/management/database/maintenance#tls-certificate-reloading).
- [HAProxy protocol](https://stalw.art/docs/server/proxy) support (#36).
### Changed
### Fixed
- IMAP command `SEARCH ` is using UIDs rather than sequence numbers.
- IMAP responses to `APPEND` and `EXPUNGE` should include `HIGHESTMODSEQ` when `CONDSTORE` is enabled.
## [0.5.1] - 2024-01-02
### Added
- SMTP smuggling protection: Sanitization of outgoing messages that do not use `CRLF` as line endings.
- SMTP sender validation for authenticated users: Added the `session.auth.must-match-sender` configuration option to enforce that the sender address used in the `MAIL FROM` command matches the authenticated user or any of their associated e-mail addresses.
### Changed
### Fixed
- Invalid DKIM signatures for empty message bodies.
- IMAP command `SEARCH BEFORE` is not properly parsed.
- IMAP command `FETCH` fails to parse single arguments without parentheses.
- IMAP command `ENABLE QRESYNC` should also enable `CONDSTORE` extension.
- IMAP response to `ENABLE` command does not include enabled capabilities list.
- IMAP response to `FETCH ENVELOPE` should not return `NIL` when the `From` header is missing.
## [0.5.0] - 2023-12-27
This version requires a database migration and introduces breaking changes in the configuration file. Please read the [UPGRADING.md](UPGRADING.md) file for more information.
### Added
- Performance enhancements:
- Messages are parsed only once and their offsets stored in the database, which avoids having to parse them on every `FETCH` request.
- Background full-text indexing.
- Optimization of database access functions.
- Storage layer improvements:
- In addition to `FoundationDB` and `SQLite`, now it is also possible to use `RocksDB`, `PostgreSQL` and `mySQL` as a storage backend.
- Blobs can now be stored in any of the supported data stores, it is no longer limited to the file system or S3/MinIO.
- Full-text searching con now be done internally or delegated to `ElasticSearch`.
- Spam databases can now be stored in any of the supported data stores or `Redis`. It is no longer necessary to have an SQL server to use the spam filter.
- Internal directory:
- User account, groups and mailing lists can now be managed directly from Stalwart without the need of an external LDAP or SQL directory.
- HTTP API to manage users, groups, domains and mailing lists.
- IMAP4rev1 `Recent` flag support, which improves compatibility with old IMAP clients.
- LDAP bind authentication, to support some LDAP servers such as `lldap` which do not expose the userPassword attribute.
- Messages marked a spam by the spam filter can now be automatically moved to the account's `Junk Mail` folder.
- Automatic creation of JMAP identities.
### Changed
### Fixed
- Spamhaus DNSBL return codes.
- CLI tool reports authentication errors rather than a parsing error.
## [0.4.2] - 2023-11-01
### Added
- JMAP for Quotas support ([RFC9425](https://www.rfc-editor.org/rfc/rfc9425.html))
- JMAP Blob Management Extension support ([RFC9404](https://www.rfc-editor.org/rfc/rfc9404.html))
- Spam Filter - Empty header rules.
### Changed
### Fixed
- Daylight savings time support for crontabs.
- JMAP `oldState` doesn’t reflect in `*/changes` (#56)
## [0.4.1] - 2023-10-26
### Added
### Changed
### Fixed
- Dockerfile entrypoint script.
- `bayes_is_balanced` function.
## [0.4.0] - 2023-10-25
This version introduces some breaking changes in the configuration file. Please read the [UPGRADING.md](UPGRADING.md) file for more information.
### Added
- Built-in Spam and Phishing filter.
- Scheduled queries on some directory types.
- In-memory maps and lists containing glob or regex patterns.
- Remote retrieval of in-memory list/maps with fallback mechanisms.
- Macros and support for including files from TOML config files.
### Changed
- `config.toml` is now split in multiple TOML files for better organization.
- **BREAKING:** Configuration key prefix `jmap.sieve` (JMAP Sieve Interpreter) has been renamed to `sieve.untrusted`.
- **BREAKING:** Configuration key prefix `sieve` (SMTP Sieve Interpreter) has been renamed to `sieve.trusted`.
### Fixed
## [0.3.10] - 2023-10-17
### Added
- Option to allow invalid certificates on outbound SMTP connections.
- Option to disable ansi colors on `stdout`.
### Changed
- SMTP reject messages are now logged as `info` rather than `debug`.
### Fixed
## [0.3.9] - 2023-10-07
### Added
- Support for reading environment variables from the configuration file using the `!ENV_VAR_NAME` special keyword.
- Option to disable ANSI color codes in logs.
### Changed
- Querying directories from a Sieve script is now done using the `query()` method from `eval`. Your scripts will need to be updated, please refer to the [new syntax](https://stalw.art/docs/smtp/filter/sieve#directory-queries).
### Fixed
- IPrev lookups of IPv4 mapped to IPv6 addresses.
## [0.3.8] - 2023-09-19
### Added
- Journal logging support
- IMAP support for UTF8 APPEND
### Changed
- Replaced `rpgp` with `sequoia-pgp` due to rpgp bug.
### Fixed
- Fix: IMAP folders that contain a & can't be used (#90)
- Fix: Ignore empty lines in IMAP requests
## [0.3.7] - 2023-09-05
### Added
- Option to disable IMAP All Messages folder (#68).
- Option to allow unencrypted SMTP AUTH (#72)
- Support for `rcpt-domain` key in `rcpt.relay` SMTP rule evaluation.
### Changed
### Fixed
- SMTP strategy `Ipv6thenIpv4` returns only IPv6 addresses (#70)
- Invalid IMAP `FETCH` responses for non-UTF-8 messages (#70)
- Allow `STATUS` and `ACL` IMAP operations on virtual mailboxes.
- IMAP `SELECT QRESYNC` without specifying a UID causes panic (#67)
- Milter `DATA` command is sent after headers which causes ClamAV to hang.
- Sieve `redirect` of unmodified messages does not work.
## [0.3.6] - 2023-08-29
### Added
- Arithmetic and logical expression evaluation in Sieve scripts.
- Support for storing query results in Sieve variables.
- Results of SPF, DKIM, ARC, DMARC and IPREV checks available as environment variables in Sieve scripts.
- Configurable protocol flags for Milter filters.
- Fall-back to plain text when `STARTTLS` fails and `starttls` is set to `optional`.
### Changed
### Fixed
- Do not panic when `hash = 0` in reports. (#60)
- JMAP Session resource returns `EmailSubmission` capabilities using arrays rather than objects.
- ManageSieve `PUTSCRIPT` should replace existing scripts.
## [0.3.5] - 2023-08-18
### Added
- TCP listener option `nodelay`.
### Changed
### Fixed
- SMTP: Allow disabling `STARTTLS`.
- JMAP: Support for `OPTIONS` HTTP method.
## [0.3.4] - 2023-08-09
### Added
- JMAP: Support for setting custom HTTP response headers (#52)
### Changed
### Fixed
- SMTP: Missing envelope keys in rewrite rules (#25)
- SMTP: Remove CRLF from Milter headers
- JMAP/IMAP: Successful authentication requests should not count when rate limiting
- IMAP: Case insensitive Inbox selection
- IMAP: Automatically create Inbox for group accounts
## [0.3.3] - 2023-08-02
### Added
- Encryption at rest with **S/MIME** or **OpenPGP**.
- Support for referencing context variables from dynamic values.
### Changed
### Fixed
- Support for PKCS8v1 ED25519 keys (#20).
- Automatic retry for import/export blob downloads (#14)
## [0.3.2] - 2023-07-28
### Added
- Sender and recipient address rewriting using regular expressions and sieve scripts.
- Subaddressing and catch-all addresses using regular expressions (#10).
- Dynamic variables in SMTP rules.
### Changed
- Added CLI to Docker container (#19).
### Fixed
- Workaround for a bug in `sqlx` that caused SQL time-outs (#15).
- Support for ED25519 certificates in PEM files (#20).
- Better handling of concurrent IMAP UID map modifications (#17).
- LDAP domain lookups from SMTP rules.
## [0.3.1] - 2023-07-22
### Added
- Milter filter support.
- Match IP address type using /0 mask (#16).
### Changed
### Fixed
- Support for OpenLDAP password hashing schemes between curly brackets (#8).
- Add CA certificates to Docker runtime (#5).
## [0.3.0] - 2023-07-16
### Added
- **LDAP** and **SQL** authentication.
- **subaddressing** and **catch-all** addresses.
- **S3-compatible** storage.
### Changed
- Merged the `stalwart-jmap`, `stalwart-imap` and `stalwart-smtp` repositories into
`stalwart-mail`.
- Removed clustering module and replaced it with a **FoundationDB** backend option.
- Integrated Stalwart SMTP into Stalwart JMAP.
- Rewritten JMAP protocol parser.
- Rewritten store backend.
- Rewritten IMAP server to have direct access to the message store (no more IMAP proxy).
- Replaced `actix` with `hyper`.
### Fixed
================================================
FILE: CNAME
================================================
get.stalw.art
================================================
FILE: CONTRIBUTING.md
================================================
# Contributing
## Contributions are Temporarily Limited
Thank you for your interest in contributing to Stalwart. We appreciate the support and enthusiasm of the open-source community. However, at this stage of the project, we are **limiting the scope of external contributions**.
Stalwart is currently **not accepting external contributions**, except for bug fixes and small, well-scoped changes. The project is approaching version 1.0, and as we move toward this milestone, development is progressing rapidly. The architecture of Stalwart is still evolving, and many internal components are subject to change.
Due to these ongoing changes and the fast pace of development, we do not have the time or resources to thoroughly review and integrate most pull requests. Accepting broad contributions at this time could lead to confusion and unnecessary rework for both contributors and maintainers.
While we are not accepting most code contributions, you can still support the project in meaningful ways. Reporting bugs, providing feedback, and helping test the software are all valuable forms of participation. If you encounter an issue, please open a detailed report that includes steps to reproduce the problem and any relevant logs or context. We also welcome thoughtful suggestions and questions through our issue tracker or discussion channels.
We plan to open the project to broader contributions once we reach a stable 1.0 release. At that point, with a more mature architecture and clearer development roadmap, we will be better positioned to collaborate with the community. We will update this policy accordingly when the time comes.
Thank you for your understanding and continued support. We’re excited about the future of Stalwart and look forward to working with the community in the near future.
## Code of Conduct
Please note we have a code of conduct, please follow it in all your interactions with the project.
## Licensing
This project is licensed under the Affero General Public License (AGPL) version 3.0. By contributing to this project, you agree that your contributions will be licensed under the AGPL-3.0 license.
## Fiduciary Contributor License Agreement
Before making any contributions, all contributors are required to sign the Fiduciary Contributor License Agreement (FLA). The FLA is a legal agreement that assigns the copyright of contributions to a designated fiduciary, who manages these rights on behalf of the project. This arrangement ensures that the software remains free and open, even as contributors come and go.
Key points of the FLA:
- Ensures the software remains free and open source
- Protects the project from potential copyright issues
- Includes a reversion clause: if the fiduciary violates Free Software principles, rights revert to the original contributors
For more details about FLA, please refer to the [FLA FAQ](https://fsfe.org/activities/fla/fla.en.html).
## Pull Request Process
1. Ensure any install or build dependencies are removed before the end of the layer when doing a
build.
2. Update the README.md with details of changes to the interface, this includes new environment
variables, exposed ports, useful file locations and container parameters.
3. Increase the version numbers in any examples files and the README.md to the new version that this
Pull Request would represent. The versioning scheme we use is [SemVer](http://semver.org/).
4. You may merge the Pull Request in once you have the sign-off of two other developers, or if you
do not have permission to do that, you may request the second reviewer to merge it for you.
## Code of Conduct
We as members, contributors, and leaders pledge to make participation in our community a harassment-free
experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex
characteristics, gender identity and expression, level of experience, education, socio-economic status,
nationality, personal appearance, race, religion, or sexual identity and orientation.
We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive,
and healthy community.
You can read the full Code of Conduct [here](https://github.com/stalwartlabs/.github/blob/main/CODE_OF_CONDUCT.md).
================================================
FILE: Cargo.toml
================================================
[workspace]
resolver = "2"
members = [
"crates/main",
"crates/types",
"crates/http",
"crates/http-proto",
"crates/jmap",
"crates/jmap-proto",
"crates/email",
"crates/imap",
"crates/imap-proto",
"crates/smtp",
"crates/managesieve",
"crates/pop3",
"crates/dav-proto",
"crates/dav",
"crates/groupware",
"crates/spam-filter",
"crates/nlp",
"crates/store",
"crates/directory",
"crates/services",
"crates/utils",
"crates/common",
"crates/trc",
"crates/migration",
"crates/cli",
"tests",
]
[profile.dev]
opt-level = 0
debug = 1
#codegen-units = 4
lto = false
incremental = true
panic = 'unwind'
debug-assertions = true
overflow-checks = false
rpath = false
[profile.release]
opt-level = 3
debug = false
codegen-units = 1
lto = true
incremental = false
panic = 'unwind'
debug-assertions = false
overflow-checks = false
rpath = false
strip = true
[profile.test]
opt-level = 0
debug = 1
#codegen-units = 16
lto = false
incremental = true
debug-assertions = true
overflow-checks = true
rpath = false
[profile.bench]
opt-level = 3
debug = false
codegen-units = 1
lto = true
incremental = false
debug-assertions = false
overflow-checks = false
rpath = false
================================================
FILE: Dockerfile
================================================
# Stalwart Dockerfile
# Credits: https://github.com/33KK
FROM --platform=$BUILDPLATFORM docker.io/lukemathwalker/cargo-chef:latest-rust-slim-trixie AS chef
WORKDIR /build
FROM --platform=$BUILDPLATFORM chef AS planner
COPY . .
RUN cargo chef prepare --recipe-path /recipe.json
FROM --platform=$BUILDPLATFORM chef AS builder
ARG TARGETPLATFORM
RUN case "${TARGETPLATFORM}" in \
"linux/arm64") echo "aarch64-unknown-linux-gnu" > /target.txt && echo "-C linker=aarch64-linux-gnu-gcc" > /flags.txt ;; \
"linux/amd64") echo "x86_64-unknown-linux-gnu" > /target.txt && echo "-C linker=x86_64-linux-gnu-gcc" > /flags.txt ;; \
*) exit 1 ;; \
esac
RUN export DEBIAN_FRONTEND=noninteractive && \
apt-get update && \
apt-get install -yq --no-install-recommends build-essential libclang-19-dev \
g++-aarch64-linux-gnu binutils-aarch64-linux-gnu \
g++-x86-64-linux-gnu binutils-x86-64-linux-gnu
RUN rustup target add "$(cat /target.txt)"
COPY --from=planner /recipe.json /recipe.json
RUN RUSTFLAGS="$(cat /flags.txt)" cargo chef cook --target "$(cat /target.txt)" --release --no-default-features --features "sqlite postgres mysql rocks s3 redis azure nats enterprise" --recipe-path /recipe.json
COPY . .
RUN RUSTFLAGS="$(cat /flags.txt)" cargo build --target "$(cat /target.txt)" --release -p stalwart --no-default-features --features "sqlite postgres mysql rocks s3 redis azure nats enterprise"
RUN RUSTFLAGS="$(cat /flags.txt)" cargo build --target "$(cat /target.txt)" --release -p stalwart-cli
RUN mv "/build/target/$(cat /target.txt)/release" "/output"
FROM docker.io/debian:trixie-slim
WORKDIR /opt/stalwart
RUN export DEBIAN_FRONTEND=noninteractive && \
apt-get update && \
apt-get install -yq --no-install-recommends ca-certificates
COPY --from=builder /output/stalwart /usr/local/bin
COPY --from=builder /output/stalwart-cli /usr/local/bin
COPY ./resources/docker/entrypoint.sh /usr/local/bin/entrypoint.sh
RUN chmod -R 755 /usr/local/bin
CMD ["/usr/local/bin/stalwart"]
VOLUME [ "/opt/stalwart" ]
EXPOSE 443 25 110 587 465 143 993 995 4190 8080
ENTRYPOINT ["/bin/sh", "/usr/local/bin/entrypoint.sh"]
================================================
FILE: Dockerfile.build
================================================
# syntax=docker/dockerfile:1
# check=skip=FromPlatformFlagConstDisallowed,RedundantTargetPlatform
# *****************
# Base image for planner & builder
# *****************
FROM --platform=$BUILDPLATFORM rust:slim-trixie AS base
ENV DEBIAN_FRONTEND="noninteractive" \
BINSTALL_DISABLE_TELEMETRY=true \
CARGO_TERM_COLOR=always \
LANG=C.UTF-8 \
TZ=UTC \
TERM=xterm-256color
# With zig, we only need libclang and make
RUN \
--mount=type=cache,target=/var/cache/apt,sharing=locked \
--mount=type=cache,target=/var/lib/apt,sharing=locked \
rm -f /etc/apt/apt.conf.d/docker-clean && \
echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' >/etc/apt/apt.conf.d/keep-cache && \
apt-get update && \
apt-get install -yq --no-install-recommends curl jq xz-utils make libclang-19-dev
# Install zig
RUN \
ZIG_VERSION=0.13.0 && \
[ ! -z "$ZIG_VERSION" ] && \
curl --retry 5 -Ls "https://ziglang.org/download/${ZIG_VERSION}/zig-linux-$(uname -m)-${ZIG_VERSION}.tar.xz" | tar -J -x -C /usr/local && \
ln -s "/usr/local/zig-linux-$(uname -m)-${ZIG_VERSION}/zig" /usr/local/bin/zig
# Install cargo-binstall
RUN curl --retry 5 -L --proto '=https' --tlsv1.2 -sSf https://raw.githubusercontent.com/cargo-bins/cargo-binstall/main/install-from-binstall-release.sh | bash
# Install cargo-chef & sccache & cargo-zigbuild
RUN cargo binstall --no-confirm cargo-chef sccache cargo-zigbuild
# *****************
# Planner
# *****************
FROM base AS planner
WORKDIR /app
COPY . .
# Generate recipe file
RUN cargo chef prepare --recipe-path recipe.json
# *****************
# Builder
# *****************
FROM base AS builder
WORKDIR /app
COPY --from=planner /app/recipe.json recipe.json
ARG TARGET
ARG BUILD_ENV
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
# Install toolchain and specify some env variables
RUN \
rustup set profile minimal && \
rustup target add ${TARGET} && \
mkdir -p artifact && \
touch /env-cargo && \
if [ ! -z "${BUILD_ENV}" ]; then \
echo "export ${BUILD_ENV}" >> /env-cargo; \
echo "Setting up ${BUILD_ENV}"; \
fi && \
if [[ "${TARGET}" == *gnu ]]; then \
base_arch="${TARGET%%-*}"; \
case "$base_arch" in \
x86_64) \
echo "export FDB_ARCH=amd64" >> /env-cargo; \
;; \
aarch64) \
echo "export FDB_ARCH=aarch64" >> /env-cargo; \
;; \
*) \
exit 1; \
;; \
esac; \
fi
# Install FoundationDB
RUN \
source /env-cargo && \
if [ ! -z "${FDB_ARCH}" ]; then \
curl --retry 5 -Lso fdb-client.deb "$(curl --retry 5 -Ls 'https://api.github.com/repos/apple/foundationdb/releases' | jq --arg FDB_ARCH "$FDB_ARCH" -r '.[] | select(.prerelease == false) | .assets[] | select(.name | test("foundationdb-clients.*" + $FDB_ARCH + ".deb$")) | .browser_download_url' | head -n1)" && \
mkdir -p /fdb && \
dpkg -x fdb-client.deb /fdb && \
mv /fdb/usr/include/foundationdb /usr/include && \
mv /fdb/usr/lib/libfdb_c.so /usr/lib && \
rm -rf fdb-client.deb /fdb; \
fi
# Cargo-chef Cache layer
RUN \
--mount=type=secret,id=ACTIONS_RESULTS_URL,env=ACTIONS_RESULTS_URL \
--mount=type=secret,id=ACTIONS_RUNTIME_TOKEN,env=ACTIONS_RUNTIME_TOKEN \
--mount=type=cache,target=/usr/local/cargo/registry \
--mount=type=cache,target=/usr/local/cargo/git \
source /env-cargo && \
if [ ! -z "${FDB_ARCH}" ]; then \
RUSTFLAGS="-L /usr/lib" cargo chef cook --recipe-path recipe.json --zigbuild --release --target ${TARGET} -p stalwart --no-default-features --features "foundationdb s3 redis nats enterprise"; \
fi
RUN \
--mount=type=secret,id=ACTIONS_RESULTS_URL,env=ACTIONS_RESULTS_URL \
--mount=type=secret,id=ACTIONS_RUNTIME_TOKEN,env=ACTIONS_RUNTIME_TOKEN \
--mount=type=cache,target=/usr/local/cargo/registry \
--mount=type=cache,target=/usr/local/cargo/git \
source /env-cargo && \
cargo chef cook --recipe-path recipe.json --zigbuild --release --target ${TARGET} -p stalwart --no-default-features --features "sqlite postgres mysql rocks s3 redis azure nats enterprise" && \
cargo chef cook --recipe-path recipe.json --zigbuild --release --target ${TARGET} -p stalwart-cli
# Copy the source code
COPY . .
ENV RUSTC_WRAPPER="sccache" \
SCCACHE_GHA_ENABLED=true
# Build FoundationDB version
RUN \
--mount=type=secret,id=ACTIONS_RESULTS_URL,env=ACTIONS_RESULTS_URL \
--mount=type=secret,id=ACTIONS_RUNTIME_TOKEN,env=ACTIONS_RUNTIME_TOKEN \
--mount=type=cache,target=/usr/local/cargo/registry \
--mount=type=cache,target=/usr/local/cargo/git \
source /env-cargo && \
if [ ! -z "${FDB_ARCH}" ]; then \
RUSTFLAGS="-L /usr/lib" cargo zigbuild --release --target ${TARGET} -p stalwart --no-default-features --features "foundationdb s3 redis nats enterprise" && \
mv /app/target/${TARGET}/release/stalwart /app/artifact/stalwart-foundationdb; \
fi
# Build generic version
RUN \
--mount=type=secret,id=ACTIONS_RESULTS_URL,env=ACTIONS_RESULTS_URL \
--mount=type=secret,id=ACTIONS_RUNTIME_TOKEN,env=ACTIONS_RUNTIME_TOKEN \
--mount=type=cache,target=/usr/local/cargo/registry \
--mount=type=cache,target=/usr/local/cargo/git \
source /env-cargo && \
cargo zigbuild --release --target ${TARGET} -p stalwart --no-default-features --features "sqlite postgres mysql rocks s3 redis azure nats enterprise" && \
cargo zigbuild --release --target ${TARGET} -p stalwart-cli && \
mv /app/target/${TARGET}/release/stalwart /app/artifact/stalwart && \
mv /app/target/${TARGET}/release/stalwart-cli /app/artifact/stalwart-cli
# *****************
# Binary stage
# *****************
FROM scratch AS binaries
COPY --from=builder /app/artifact /
# *****************
# Runtime image for GNU targets
# *****************
FROM --platform=$TARGETPLATFORM docker.io/library/debian:trixie-slim AS gnu
WORKDIR /opt/stalwart
RUN export DEBIAN_FRONTEND=noninteractive && \
apt-get update && \
apt-get install -yq --no-install-recommends ca-certificates tzdata
COPY --from=builder /app/artifact/stalwart /usr/local/bin
COPY --from=builder /app/artifact/stalwart-cli /usr/local/bin
COPY ./resources/docker/entrypoint.sh /usr/local/bin/entrypoint.sh
RUN chmod -R 755 /usr/local/bin
CMD ["/usr/local/bin/stalwart"]
VOLUME [ "/opt/stalwart" ]
EXPOSE 443 25 110 587 465 143 993 995 4190 8080
ENTRYPOINT ["/bin/sh", "/usr/local/bin/entrypoint.sh"]
# *****************
# Runtime image for musl targets
# *****************
FROM --platform=$TARGETPLATFORM alpine AS musl
WORKDIR /opt/stalwart
RUN apk add --update --no-cache ca-certificates tzdata && rm -rf /var/cache/apk/*
COPY --from=builder /app/artifact/stalwart /usr/local/bin
COPY --from=builder /app/artifact/stalwart-cli /usr/local/bin
COPY ./resources/docker/entrypoint.sh /usr/local/bin/entrypoint.sh
RUN chmod -R 755 /usr/local/bin
CMD ["/usr/local/bin/stalwart"]
VOLUME [ "/opt/stalwart" ]
EXPOSE 443 25 110 587 465 143 993 995 4190 8080
ENTRYPOINT ["/bin/sh", "/usr/local/bin/entrypoint.sh"]
================================================
FILE: LICENSES/AGPL-3.0-only.txt
================================================
GNU AFFERO GENERAL PUBLIC LICENSE
Version 3, 19 November 2007
Copyright (C) 2007 Free Software Foundation, Inc.
Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed.
Preamble
The GNU Affero General Public License is a free, copyleft license for software and other kinds of works, specifically designed to ensure cooperation with the community in the case of network server software.
The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, our General Public Licenses are intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users.
When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things.
Developers that use our General Public Licenses protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License which gives you legal permission to copy, distribute and/or modify the software.
A secondary benefit of defending all users' freedom is that improvements made in alternate versions of the program, if they receive widespread use, become available for other developers to incorporate. Many developers of free software are heartened and encouraged by the resulting cooperation. However, in the case of software used on network servers, this result may fail to come about. The GNU General Public License permits making a modified version and letting the public access it on a server without ever releasing its source code to the public.
The GNU Affero General Public License is designed specifically to ensure that, in such cases, the modified source code becomes available to the community. It requires the operator of a network server to provide the source code of the modified version running there to the users of that server. Therefore, public use of a modified version, on a publicly accessible server, gives the public access to the source code of the modified version.
An older license, called the Affero General Public License and published by Affero, was designed to accomplish similar goals. This is a different license, not a version of the Affero GPL, but Affero has released a new version of the Affero GPL which permits relicensing under this license.
The precise terms and conditions for copying, distribution and modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU Affero General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based on the Program.
To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work.
A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source.
The Corresponding Source for a work in source code form is that same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures.
When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified it, and giving a relevant date.
b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices".
c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so.
A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways:
a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b.
d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d.
A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product.
"Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made.
If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM).
The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or authors of the material; or
e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors.
All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11).
However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice.
Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party.
If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it.
A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program.
13. Remote Network Interaction; Use with the GNU General Public License.
Notwithstanding any other provision of this License, if you modify the Program, your modified version must prominently offer all users interacting with it remotely through a computer network (if your version supports such interaction) an opportunity to receive the Corresponding Source of your version by providing access to the Corresponding Source from a network server at no charge, through some standard or customary means of facilitating copying of software. This Corresponding Source shall include the Corresponding Source for any work covered by version 3 of the GNU General Public License that is incorporated pursuant to the following paragraph.
Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the work with which it is combined will remain governed by version 3 of the GNU General Public License.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of the GNU Affero General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns.
Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU Affero General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU Affero General Public License, you may choose any version ever published by the Free Software Foundation.
If the Program specifies that a proxy can decide which future versions of the GNU Affero General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program.
Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found.
Copyright (C)
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License along with this program. If not, see .
Also add information on how to contact you by electronic and paper mail.
If your software can interact with users remotely through a computer network, you should also make sure that it provides a way for users to get its source. For example, if your program is a web application, its interface could display a "Source" link that leads users to an archive of the code. There are many ways you could offer source, and different solutions will be better for different programs; see section 13 for the specific requirements.
You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU AGPL, see .
================================================
FILE: LICENSES/LicenseRef-SEL.txt
================================================
Stalwart Enterprise License 1.0 (SELv1) Agreement
=================================================
Last Update: April 29, 2025
PLEASE CAREFULLY READ THIS STALWART ENTERPRISE LICENSE AGREEMENT ("AGREEMENT"). THIS AGREEMENT CONSTITUTES A LEGALLY BINDING AGREEMENT BETWEEN YOU AND Stalwart Labs LLC AND GOVERNS YOUR USE OF THE SOFTWARE (DEFINED BELOW). IF YOU DO NOT AGREE WITH THIS AGREEMENT, YOU MAY NOT USE THE SOFTWARE. IF YOU ARE USING THE SOFTWARE ON BEHALF OF A LEGAL ENTITY, YOU REPRESENT AND WARRANT THAT YOU HAVE AUTHORITY TO AGREE TO THIS AGREEMENT ON BEHALF OF SUCH ENTITY. IF YOU DO NOT HAVE SUCH AUTHORITY, DO NOT USE THE SOFTWARE IN ANY MANNER.
This Agreement is entered into by and between Stalwart Labs LLC and you, or the legal entity on behalf of whom you are acting.
1. DEFINITIONS
1.1. "Software" refers to the Stalwart Server Enterprise Edition software, including all its versions, updates, modifications, accompanying documentation, and related materials.
1.2. "Subscription" refers to the paid access to the Software provided by Licensor to Licensee.
1.3. "Licensor" refers to Stalwart Labs LLC, the entity providing the Software.
1.4. "Licensee" refers to the individual or entity installing, accessing, or using the Software with a valid Subscription.
1.5. "License Key" refers to the unique code provided by Licensor upon purchasing a Subscription which activates the full features of the Software.
1.6. "Source Code" refers to the human-readable version of the Software's code, as opposed to the compiled machine-readable version.
2. GRANT OF LICENSE
2.1. Licensor grants Licensee a revocable, non-exclusive, non-transferable, non-sublicensable, limited license to download, install, and use the Software.
2.2. The use of the Software is conditioned upon Licensee maintaining an active and valid paid subscription with Licensor. The paid subscription covers all versions of the Software and all updates and modifications.
2.3. This license grants Licensee the right to use the Software for both personal and commercial purposes. However, Licensee is expressly prohibited from reselling, leasing, sublicensing, or otherwise redistributing the Software itself.
2.4. This license is further governed by the terms and conditions set forth in any licensing agreements separately executed between Licensor and Licensee. In the event of any conflict between the terms of this Agreement and the terms of a signed licensing agreement, the terms of the signed licensing agreement shall control.
2.5. You are not granted any other rights beyond what is expressly stated herein.
3. LICENSE KEYS
3.1. The Software shall not be used without a valid License Key issued by Licensor.
3.2. Licensee is required to use valid License Keys issued by Licensor to run the Software, including any modified versions. Any attempts to bypass the License Key requirement is a violation of this Agreement.
3.3. Distribution or sharing of License Keys to third parties, not associated with Licensee, is strictly prohibited.
3.4. License Keys are bound to the subscription period. Should your subscription expire, all License Keys will become invalid after 15 days from the subscription expiration date.
3.5. Any instance of the Software using such an expired key will revert to the Community Edition functionality after the aforementioned 15-day period.
4. SOURCE CODE USAGE
4.1. Licensee is permitted to view, copy, and modify the Software's Source Code, as made available by Licensor, solely for Licensee's internal business use and in compliance with this Agreement's terms.
4.2. Any modifications to the Source Code do not grant Licensee any ownership rights to the original Software or any modifications. All rights, title, and interest to the Software and its Source Code remain exclusively with Licensor.
4.3. Licensee is strictly prohibited from altering, removing, or in any way tampering with the License Key validation system within the Software. Any such unauthorized modifications will be considered a material breach of this Agreement and may result in legal action.
4.3. Notwithstanding the availability of the Software's Source Code for review and limited modification, the Software and its Source Code are not open source and remain proprietary to Licensor. The provision of access to the Source Code does not confer any rights typically associated with open source software, including but not limited to the right to freely sublicense, or create derivative works for public distribution. All rights not expressly granted herein are reserved by Licensor.
4.4. Notwithstanding the foregoing, you may copy the Source Code for development and testing purposes, without requiring a Subscription.
5. INTELLECTUAL PROPERTY RIGHTS
5.1. The Licensor retains all rights, title, and interest in and to the Software, including all intellectual property rights therein. This Agreement does not transfer any ownership rights to the Licensee.
5.2. The Licensee must not remove, alter, or obscure any proprietary notices (including copyright and trademark notices) on the Software.
6. TERMINATION
6.1. Licensor reserves the right to terminate this Agreement immediately if the Licensee fails to comply with any terms and conditions of this Agreement.
6.2. In the event of a termination, you will be provided with a written notice, sent to the email address used during your subscription to the Software, outlining the reasons for the termination.
6.3. Upon termination, all rights granted to you under this Agreement will cease, and you must promptly cease all use of the Software.
7. LIMITATION OF LIABILITY
7.1. In no event will the Licensor be liable for any indirect, incidental, special, consequential, or punitive damages, or any loss of profits or revenues, whether incurred directly or indirectly, or any loss of data, use, goodwill, or other intangible losses, resulting from (i) your use or inability to use the Software; (ii) any unauthorized access to or use of our servers and/or any personal information stored therein.
7.2. Except for liability arising from death or personal injury caused by negligence, fraud, or willful misconduct, Licensor's total aggregate liability for any and all claims under this Agreement shall be limited to the total Subscription fees paid by Licensee to Licensor in the twelve (12) months immediately preceding the event giving rise to the claim.
8. GOVERNING LAW & JURISDICTION
This Agreement shall be governed by and construed under the laws of the United Kingdom. Any disputes arising from or related to this Agreement shall be resolved in the jurisdiction of London, UK.
9. DATA PROTECTION & PRIVACY
By using the Software, you consent to the collection, processing, and use of any personal data as required for the functionality of the Software. The specifics of data handling and storage will be outlined in the company's Privacy Policy, which can be accessed on the company's website.
10. ACCEPTANCE
By downloading, installing, or using the Software software, even without explicitly clicking on an "I Agree" button or a similar mechanism, you acknowledge that you have read, understood, and agreed to be bound by the terms and conditions of this Agreement.
11. ASSIGNMENT
This Agreement and the rights granted hereunder may not be transferred or assigned by you but may be assigned by Licensor without restriction.
12. SEVERABILITY
If any provision of this Agreement is held to be unenforceable or invalid for any reason, that provision shall be reformed to the extent necessary to make it enforceable and consistent with the intent of the parties, and the remaining provisions shall remain in full force and effect.
13. ENTIRE AGREEMENT
This Agreement constitutes the entire agreement between the Licensor and the Licensee with respect to the subject matter hereof and supersedes all prior or contemporaneous understandings regarding such subject matter. No amendment to or modification of this Agreement will be binding unless in writing and signed by the Licensor.
14. DISCLAIMERS AND WARRANTIES
The Software is provided "AS IS" and "AS AVAILABLE", without warranty of any kind, either express or implied, including, without limitation, warranties of merchantability, fitness for a particular purpose, and non-infringement. Licensor does not warrant that the Software will be error-free, that access thereto will be uninterrupted, or that defects will be corrected.
15. INDEMNIFICATION
Licensee agrees to indemnify, defend, and hold harmless Licensor, its officers, directors, employees, agents, licensors, suppliers, and any third-party information providers from and against all claims, losses, expenses, damages, and costs, including reasonable attorneys' fees, resulting from any violation of this Agreement or any activity related to your use or misuse of the Software (including negligent or wrongful conduct).
16. FORCE MAJEURE
Neither party shall be in default or otherwise liable for any delay in or failure of its performance under this Agreement if such delay or failure arises by any reason of any event beyond the reasonable control of a party, including acts of God, the elements, earthquakes, floods, fires, epidemics, riots, failures or delays in transportation or communications, or any act or failure to act by the other party or such other party’s officers, employees, agents, or contractors. The parties will promptly inform and consult with each other as to any of the above causes which, in their judgment, may or could be the cause of a delay in the performance of this Agreement.
17. CONTACT INFORMATION
If you have any questions about this Agreement, please contact Stalwart Labs LLC at:
Stalwart Labs LLC
1309 Coffeen Avenue STE 1200
Sheridan, Wyoming 82801
USA
hello@stalw.art
================================================
FILE: README.md
================================================
Secure, scalable mail & collaboration server with comprehensive protocol support 🛡️ (IMAP, JMAP, SMTP, CalDAV, CardDAV, WebDAV)
## Features
**Stalwart** is an open-source mail & collaboration server with JMAP, IMAP4, POP3, SMTP, CalDAV, CardDAV and WebDAV support and a wide range of modern features. It is written in Rust and designed to be secure, fast, robust and scalable.
Key features:
- **Email** server with complete protocol support:
- JMAP:
* [JMAP for Mail](https://datatracker.ietf.org/doc/html/rfc8621) server.
* [JMAP for Sieve Scripts](https://www.ietf.org/archive/id/draft-ietf-jmap-sieve-22.html).
* [WebSocket](https://datatracker.ietf.org/doc/html/rfc8887), [Blob Management](https://www.rfc-editor.org/rfc/rfc9404.html) and [Quotas](https://www.rfc-editor.org/rfc/rfc9425.html) extensions.
- IMAP:
* [IMAP4rev2](https://datatracker.ietf.org/doc/html/rfc9051) and [IMAP4rev1](https://datatracker.ietf.org/doc/html/rfc3501) server.
* [ManageSieve](https://datatracker.ietf.org/doc/html/rfc5804) server.
* Numerous [extensions](https://stalw.art/docs/development/rfcs#imap4-and-extensions) supported.
- POP3:
- [POP3](https://datatracker.ietf.org/doc/html/rfc1939) server.
- [STLS](https://datatracker.ietf.org/doc/html/rfc2595) and [SASL](https://datatracker.ietf.org/doc/html/rfc5034) support as well as other [extensions](https://datatracker.ietf.org/doc/html/rfc2449).
- SMTP:
* SMTP server with built-in [DMARC](https://datatracker.ietf.org/doc/html/rfc7489), [DKIM](https://datatracker.ietf.org/doc/html/rfc6376), [SPF](https://datatracker.ietf.org/doc/html/rfc7208) and [ARC](https://datatracker.ietf.org/doc/html/rfc8617) support for message authentication.
* Strong transport security through [DANE](https://datatracker.ietf.org/doc/html/rfc6698), [MTA-STS](https://datatracker.ietf.org/doc/html/rfc8461) and [SMTP TLS](https://datatracker.ietf.org/doc/html/rfc8460) reporting.
* Inbound throttling and filtering with granular configuration rules, sieve scripting, MTA hooks and milter integration.
* Distributed virtual queues with delayed delivery, priority delivery, quotas, routing rules and throttling support.
* Envelope rewriting and message modification.
- **Collaboration** server:
- Calendaring and scheduling:
- [CalDAV](https://datatracker.ietf.org/doc/html/rfc4791) and [CalDAV Scheduling](https://datatracker.ietf.org/doc/html/rfc6638) support.
- [JMAP for Calendars](https://datatracker.ietf.org/doc/html/draft-ietf-jmap-calendars-24) support.
- Contact management:
- [CardDAV](https://datatracker.ietf.org/doc/html/rfc6352) support.
- [JMAP for Contacts](https://datatracker.ietf.org/doc/html/rfc9610) support.
- File storage:
- [WebDAV](https://datatracker.ietf.org/doc/html/rfc4918) support.
- [JMAP for File Storage](https://datatracker.ietf.org/doc/html/draft-ietf-jmap-filenode-03) support.
- Sharing with fine-grained access controls:
- [WebDAV ACL](https://datatracker.ietf.org/doc/html/rfc3744) support.
- [JMAP Sharing](https://datatracker.ietf.org/doc/html/rfc9670) support.
- **Spam** and **Phishing** built-in filter:
- Comprehensive set of filtering **rules** on par with popular solutions.
- LLM-driven spam filtering and message analysis.
- Statistical **spam classifier** with collaborative filtering, automatic training capabilities and address book integration.
- DNS Blocklists (**DNSBLs**) checking of IP addresses, domains, and hashes.
- Collaborative digest-based spam filtering with **Pyzor**.
- **Phishing** protection against homographic URL attacks, sender spoofing and other techniques.
- Trusted **reply** tracking to recognize and prioritize genuine e-mail replies.
- Sender **reputation** monitoring by IP address, ASN, domain and email address.
- **Greylisting** to temporarily defer unknown senders.
- **Spam traps** to set up decoy email addresses that catch and analyze spam.
- **Flexible**:
- Pluggable storage backends with **RocksDB**, **FoundationDB**, **PostgreSQL**, **mySQL**, **SQLite**, **S3-Compatible**, **Azure** and **Redis** support.
- Full-text search available in 17 languages using the built-in search engine or via **Meilisearch**, **ElasticSearch**, **OpenSearch**, **PostgreSQL** or **mySQL** backends.
- Sieve scripting language with support for all [registered extensions](https://www.iana.org/assignments/sieve-extensions/sieve-extensions.xhtml).
- Email aliases, mailing lists, subaddressing and catch-all addresses support.
- Automatic account configuration and discovery with [autoconfig](https://www.ietf.org/id/draft-bucksch-autoconfig-02.html) and [autodiscover](https://learn.microsoft.com/en-us/exchange/architecture/client-access/autodiscover?view=exchserver-2019).
- Multi-tenancy support with domain and tenant isolation.
- Disk quotas per user and tenant.
- **Secure and robust**:
- Encryption at rest with **S/MIME** or **OpenPGP**.
- Automatic TLS certificate provisioning with [ACME](https://datatracker.ietf.org/doc/html/rfc8555) using `TLS-ALPN-01`, `DNS-01` or `HTTP-01` challenges.
- Automated blocking of IP addresses that attack, abuse or scan the server for exploits.
- Rate limiting.
- Security audited (read the [report](https://stalw.art/blog/security-audit)).
- Memory safe (thanks to Rust).
- **Scalable and fault-tolerant**:
- Designed to handle growth seamlessly, from small setups to large-scale deployments of thousands of nodes.
- Built with **fault tolerance** and **high availability** in mind, recovers from hardware or software failures with minimal operational impact.
- Peer-to-peer cluster coordination or with **Kafka**, **Redpanda**, **NATS** or **Redis**.
- **Kubernetes**, **Apache Mesos** and **Docker Swarm** support for automated scaling and container orchestration.
- Read replicas, sharded blob storage and in-memory data stores for high performance and low latency.
- **Authentication and Authorization**:
- **OpenID Connect** authentication.
- OAuth 2.0 authorization with [authorization code](https://www.rfc-editor.org/rfc/rfc8628) and [device authorization](https://www.rfc-editor.org/rfc/rfc8628) flows.
- **LDAP**, **OIDC**, **SQL** or built-in authentication backend support.
- Two-factor authentication with Time-based One-Time Passwords (`2FA-TOTP`)
- Application passwords (App Passwords).
- Roles and permissions.
- Access Control Lists (ACLs).
- **Observability**:
- Logging and tracing with **OpenTelemetry**, journald, log files and console support.
- Metrics with **OpenTelemetry** and **Prometheus** integration.
- Webhooks for event-driven automation.
- Alerts with email and webhook notifications.
- Live tracing and metrics.
- **Web-based administration**:
- Dashboard with real-time statistics and monitoring.
- Account, domain, group and mailing list management.
- SMTP queue management for messages and outbound DMARC and TLS reports.
- Report visualization interface for received DMARC, TLS-RPT and Failure (ARF) reports.
- Configuration of every aspect of the mail server.
- Log viewer with search and filtering capabilities.
- Self-service portal for password reset and encryption-at-rest key management.
## Screenshots
## Presentation
**Want a deeper dive?** Need to explain to your boss why Stalwart is the perfect fit? Whether you're evaluating options, making a case to your team, or simply curious about how it all works under the hood, these slides walk you through the key features, architecture, and benefits of Stalwart. Browse the [slides](https://stalw.art/slides) to see what makes it stand out.
## Get Started
Install Stalwart on your server by following the instructions for your platform:
- [Linux / MacOS](https://stalw.art/docs/install/platform/linux)
- [Windows](https://stalw.art/docs/install/platform/windows)
- [Docker](https://stalw.art/docs/install/platform/docker)
All documentation is available at [stalw.art/docs](https://stalw.art/docs/install/get-started).
## Support
If you are having problems running Stalwart, you found a bug or just have a question, do not hesitate to reach us on [GitHub Discussions](https://github.com/stalwartlabs/stalwart/discussions), [Reddit](https://www.reddit.com/r/stalwartlabs) or [Discord](https://discord.com/servers/stalwart-923615863037390889).
Additionally you may purchase an [Enterprise License](https://stalw.art/enterprise) to obtain priority support from Stalwart Labs LLC.
## Roadmap
Stalwart has reached an exciting point in its journey, it’s now **feature complete**. All the core functionality and open standard email and collaboration protocols that we set out to support are in place. In other words, Stalwart already does everything you’d expect from a modern, standards-compliant mail and collaboration platform.
The next major milestone is all about refinement: finalizing the database schema and focusing on performance optimizations to ensure everything runs as efficiently and reliably as possible. Once that’s done, we’ll be ready to roll out version **1.0**.
Of course, development doesn’t stop there. The community has contributed hundreds of great ideas for improvements and new features, everything from subtle usability tweaks to entirely new integrations. You can see the full list of proposals over on our [GitHub issues](https://github.com/stalwartlabs/stalwart/issues?q=is%3Aissue+is%3Aopen+sort%3Areactions-%2B1-desc+label%3Aenhancement). If there’s something you’d like to see prioritized, just give it a thumbs up as we plan to implement enhancements based on the community’s votes.
## Sponsorship
Your support is crucial in helping us continue to improve the project, add new features, and maintain the highest level of quality. By [becoming a sponsor](https://opencollective.com/stalwart), you help fund the development and future of Stalwart. As a thank-you, sponsors who contribute $5 per month or more will automatically receive a [Enterprise edition](https://stalw.art/enterprise/) license. And, sponsors who contribute $30 per month or more, also have access to [Premium Support](https://stalw.art/support) from Stalwart Labs.
## Funding
Part of the development of this project was funded through:
- [NGI0 Entrust Fund](https://nlnet.nl/entrust), a fund established by [NLnet](https://nlnet.nl/) with financial support from the European Commission's [Next Generation Internet](https://ngi.eu/) programme, under the aegis of DG Communications Networks, Content and Technology under grant agreement No 101069594.
- [NGI Zero Core](https://nlnet.nl/NGI0/), a fund established by [NLnet](https://nlnet.nl/) with financial support from the European Commission's programme, under the aegis of DG Communications Networks, Content and Technology under grant agreement No 101092990.
If you find the project useful you can help by [becoming a sponsor](https://opencollective.com/stalwart). Thank you!
## License
This project is dual-licensed under the **GNU Affero General Public License v3.0** (AGPL-3.0; as published by the Free Software Foundation) and the **Stalwart Enterprise License v1 (SELv1)**:
- The [GNU Affero General Public License v3.0](./LICENSES/AGPL-3.0-only.txt) is a free software license that ensures your freedom to use, modify, and distribute the software, with the condition that any modified versions of the software must also be distributed under the same license.
- The [Stalwart Enterprise License v1 (SELv1)](./LICENSES/LicenseRef-SEL.txt) is a proprietary license designed for commercial use. It offers additional features and greater flexibility for businesses that do not wish to comply with the AGPL-3.0 license requirements.
Each file in this project contains a license notice at the top, indicating the applicable license(s). The license notice follows the [REUSE guidelines](https://reuse.software/) to ensure clarity and consistency. The full text of each license is available in the [LICENSES](./LICENSES/) directory.
## Copyright
Copyright (C) 2020, Stalwart Labs LLC
================================================
FILE: SECURITY.md
================================================
# Security Policy for Stalwart
## Supported Versions
We provide security updates for the following versions of Stalwart:
| Version | Supported | End of Support |
| ------- | ------------------ | -------------- |
| 0.15.x | :white_check_mark: | TBD |
| 0.14.x | :white_check_mark: | 2026-06-08 |
| 0.13.x | :white_check_mark: | 2026-03-31 |
| < 0.13 | :x: | Ended |
**Note**: We typically support the current major version and one previous major version. Users are strongly encouraged to upgrade to the latest version for the best security posture.
## Reporting a Vulnerability
We take the security of Stalwart very seriously. If you believe you've found a security vulnerability, we encourage you to inform us responsibly through coordinated disclosure.
### How to Report
**Do not report security vulnerabilities through public GitHub issues, discussions, or social media.**
Instead, please use one of these secure channels:
1. **Email** (preferred): Send details to `security@stalw.art`
2. **GitHub Security Advisories**: Use the "Report a vulnerability" button in the Security tab
3. **Backup contact**: If no response within 48 hours, email `hello@stalw.art`
### What to Include
To help us understand and address the issue quickly, please include:
**Required Information:**
- Brief description of the vulnerability type
- Affected version(s) and components
- Steps to reproduce the issue
- Impact assessment (what could an attacker achieve?)
**Helpful Additional Details:**
- Full paths of affected source files
- Specific commit/branch where the issue exists
- Required configuration to reproduce
- Proof-of-concept code (if available)
- Suggested mitigation or fix (if you have ideas)
### Our Response Process
**Timeline Commitments:**
- **Initial acknowledgment**: Within 24 hours
- **Detailed response**: Within 72 hours
- **Status updates**: Every 7 days until resolved
- **Resolution target**: 90 days for most issues
**What We'll Do:**
1. Acknowledge your report and assign a tracking ID
2. Assess the vulnerability and determine severity
3. Develop and test a fix
4. Coordinate disclosure timeline with you
5. Release security update and publish advisory
6. Credit you in our security advisory (if desired)
## Disclosure Policy
We follow responsible disclosure principles:
- **Coordinated disclosure**: We'll work with you to determine appropriate disclosure timing
- **Typical timeline**: 90 days from report to public disclosure
- **Early disclosure**: May occur if issue is being actively exploited
- **Delayed disclosure**: May be necessary for complex issues requiring significant changes
## Scope
This security policy applies to:
**In Scope:**
- Stalwart (all supported versions)
- Official Docker images
- Documentation that could lead to insecure configurations
- Dependencies with security implications
**Out of Scope:**
- Third-party integrations or plugins
- Issues requiring physical access to the server
- Social engineering attacks
- Attacks requiring compromised credentials (unless the vulnerability enables credential compromise)
- Theoretical vulnerabilities without practical exploitation
## Security Measures
**Our Commitments:**
- Regular security audits of dependencies using `cargo audit`
- Automated security scanning in CI/CD pipeline
- Following Rust security best practices
- Prompt security updates for critical dependencies
- Security-focused code review process
**User Responsibilities:**
- Keep Stalwart updated to supported versions
- Follow security configuration guidelines
- Implement proper network security (firewalls, TLS, etc.)
- Regular security monitoring and logging
- Secure credential management
## Legal Safe Harbor
We support security research conducted in good faith. If you follow these guidelines:
**We will NOT:**
- Initiate legal action against you
- Contact law enforcement about your research
- Suspend or terminate your access to Stalwart services
**You must:**
- Only test against your own Stalwart installations
- Not access, modify, or delete user data
- Not perform testing that could degrade service availability
- Not publicly disclose the issue before coordinated disclosure
- Act in good faith and not for malicious purposes
## Recognition
We believe in recognizing security researchers who help keep Stalwart secure:
- **Security Advisory Credits**: We'll credit you in our GitHub Security Advisories (unless you prefer to remain anonymous)
- **Hall of Fame**: Significant contributors may be listed in our security acknowledgments
- **Swag**: We may send Stalwart merchandise for notable contributions
## Security Updates
**Stay Informed:**
- Subscribe to our [GitHub releases](https://github.com/stalwartlabs/stalwart/releases) for security updates
- Join our community channels for security announcements
- Enable GitHub notifications for security advisories
**Update Process:**
- Security updates are published as patch releases (e.g., 0.12.1 → 0.12.2)
- Critical vulnerabilities may receive out-of-band releases
- Docker images are updated simultaneously with releases
- Security advisories are published through GitHub Security Advisories
## Contact Information
- **Security reports**: security@stalw.art
- **General inquiries**: hello@stalw.art
- **PGP Key**: Available upon request for sensitive communications
## Additional Resources
- [Stalwart Security Incident Response Process](SECURITY_PROCESS.md)
- [Security Configuration Guide](https://stalw.art/docs/install/security)
- [Rust Security Advisory Database](https://rustsec.org/)
*This security policy is effective as of June 20, 2025 and may be updated periodically. Check back regularly for updates.*
================================================
FILE: SECURITY_PROCESS.md
================================================
# Stalwart Security Incident Response Checklist
## Phase 1 : Initial Assessment & Validation
### Updates
<< Use this section to detail the report received, initial assessment, and validation results >>
Example:
I've reviewed the security report and confirmed this vulnerability exists in Stalwart version X.Y.Z.
Assessment of exploitability:
- Attack complexity: [High/Medium/Low]
- Prerequisites: [Authentication required/Network access/Specific configuration/etc.]
- User interaction required: [Yes/No]
Potential impact:
- Email data confidentiality: [At risk/Not affected]
- Server integrity: [At risk/Not affected]
- Service availability: [At risk/Not affected]
- Estimated affected installations: [Number/Percentage]
### Resources
- [Stalwart Security Policy](https://github.com/stalwartlabs/stalwart/blob/main/SECURITY.md)
- [CVE Scoring Calculator](https://nvd.nist.gov/vuln-metrics/cvss/v3-calculator)
- [Rust Security Advisory Database](https://rustsec.org/)
### Tasks
- [ ] Reproduce the vulnerability in test environment
- [ ] Assess CVSS score and severity level
- [ ] Check if vulnerability affects current stable version
- [ ] Check if vulnerability affects LTS versions (if applicable)
- [ ] Determine if this requires immediate action or can wait for next release cycle
- [ ] Document technical details and root cause
### Assessment Summary
- **Severity Level**: `Critical|High|Medium|Low`
- **CVSS Score**: `X.X`
- **Affects versions**: `X.Y.Z to X.Y.Z`
- **Root cause**: Brief technical explanation
- **Introduced in commit/version**: `commit-hash` or `vX.Y.Z`
- **Attack vector**: `Network|Local|Physical`
- **Estimated timeline for fix**: `X days/weeks`
## Phase 2: Immediate Response & Mitigation
### Updates
<< Document immediate actions taken and mitigation strategies >>
Example:
Working on hotfix for version X.Y.Z. Temporary workaround available by disabling [feature] in configuration.
### Tasks
- [ ] Implement immediate workaround if possible
- [ ] Update security advisory draft
- [ ] Prepare patch/hotfix
- [ ] Test fix thoroughly in development environment
- [ ] Prepare updated Docker images and binaries
- [ ] Draft security advisory for GitHub Security Advisories
- [ ] Consider if coordinated disclosure timeline needs adjustment
### Mitigation Details
- **Workaround available**: `Yes|No` - If yes, describe briefly
- **Fix implemented on**: `YYYY-MM-DD`
- **Patch/hotfix version**: `vX.Y.Z`
- **GitHub Security Advisory ID**: `GHSA-XXXX-XXXX-XXXX`
## Phase 3: Impact Assessment & User Analysis
### Updates
<< Analysis of potential impact on the Stalwart deployments >>
Based on telemetry data and version statistics, approximately X installations may be affected.
### Tasks
- [ ] Analyze version adoption from update checks (if available)
- [ ] Estimate number of vulnerable installations
- [ ] Assess if default configurations are vulnerable
- [ ] Review if vulnerability has been exploited (check logs, reports)
- [ ] Determine if any user data may have been compromised
- [ ] Check for indicators of active exploitation in the wild
### Analysis Notes
_Document your impact assessment process and findings_
### Impact Summary
- **Estimated vulnerable installations**: `~X out of Y`
- **Default configuration vulnerable**: `Yes|No`
- **Evidence of exploitation**: `Found|Not found|Unknown`
- **User data potentially at risk**: `Email content|Credentials|Configuration|None`
- **Confidence in assessment**: `High|Medium|Low`
## Phase 4: Communication & Release
### Updates
<< Communication strategy and release timeline >>
Security release vX.Y.Z will be published on YYYY-MM-DD with coordinated disclosure.
### Tasks
**Pre-release preparation:**
- [ ] Finalize security patch
- [ ] Prepare release notes with security details
- [ ] Update documentation if needed
- [ ] Test automated update mechanisms
- [ ] Prepare GitHub Security Advisory
**Communication channels:**
- [ ] Draft announcement for Stalwart community forum/Discord
- [ ] Prepare release announcement for GitHub
- [ ] Draft security advisory content
- [ ] Consider notification to major distributors/packagers
**Release execution:**
- [ ] Publish patched version to GitHub releases
- [ ] Update Docker images on Docker Hub
- [ ] Publish GitHub Security Advisory
- [ ] Post to community channels (Discord/forum)
- [ ] Update project website/documentation
- [ ] Submit CVE request if warranted (CVSS ≥ 4.0)
**Post-release:**
- [ ] Monitor community channels for questions
- [ ] Track adoption of security update
- [ ] Follow up on any additional reports
- [ ] Document lessons learned
### Communication Record
- **Security release published**: `YYYY-MM-DD HH:MM UTC`
- **GitHub Security Advisory**: `GHSA-XXXX-XXXX-XXXX`
- **CVE ID** (if applicable): `CVE-YYYY-XXXXX`
- **Community announcement**: [Link to forum/Discord post]
- **Estimated time to 50% adoption**: `X days/weeks`
## Post-Incident Review
### What went well?
-
### What could be improved?
-
### Action items for future incidents:
- [ ]
- [ ]
- [ ]
### Process improvements:
- [ ]
- [ ]
## Emergency Contacts
- **Primary maintainer**: hello@stalw.art
================================================
FILE: SECURITY_TEMPLATE.md
================================================
# Stalwart Security Advisory
**CVE ID:** CVE-YYYY-NNNNN
**Publication Date:** YYYY-MM-DD
**Last Updated:** YYYY-MM-DD
## Summary
[Provide a brief, non-technical summary of the vulnerability in 1-2 sentences]
## Affected Products and Versions
**Product:** Stalwart Mail and Collaboration Server
**Affected Versions:**
- Version X.X.X through Y.Y.Y
- [List specific affected version ranges]
**Fixed Versions:**
- Version Z.Z.Z and later
- [List all versions that include the fix]
## Vulnerability Details
### Description
[Detailed technical description of the vulnerability, including how it can be exploited]
### Impact
[Describe the potential impact if this vulnerability is exploited]
### CVSS Score
**CVSS v3.1 Base Score:** X.X ([SEVERITY])
**Vector String:** CVSS:3.1/AV:X/AC:X/PR:X/UI:X/S:X/C:X/I:X/A:X
**Severity Breakdown:**
- **Attack Vector:** [Network/Adjacent/Local/Physical]
- **Attack Complexity:** [Low/High]
- **Privileges Required:** [None/Low/High]
- **User Interaction:** [None/Required]
- **Scope:** [Unchanged/Changed]
- **Confidentiality Impact:** [None/Low/High]
- **Integrity Impact:** [None/Low/High]
- **Availability Impact:** [None/Low/High]
### CWE Classification
**CWE-XXX:** [Weakness Name]
## Technical Details
### Root Cause
[Explain the underlying cause of the vulnerability]
### Attack Scenario
[Describe a realistic attack scenario or proof of concept, without providing exploit code]
### Prerequisites
[List any conditions that must be met for successful exploitation]
## Remediation
### Recommended Actions
1. **Immediate:** Upgrade to version Z.Z.Z or later
2. **Short-term:** [Any temporary mitigation measures]
3. **Long-term:** [Any additional security hardening recommendations]
### Upgrade Instructions
```bash
# Example upgrade commands
[Provide specific upgrade instructions for Stalwart]
```
### Workarounds
[If applicable, describe any temporary workarounds for systems that cannot be immediately upgraded]
**Note:** Workarounds are temporary measures and do not fully resolve the vulnerability. Upgrading is strongly recommended.
## Detection
### Indicators of Compromise
[List any logs, patterns, or indicators that may suggest exploitation attempts]
### Log Entries
```
[Example log entries that administrators should look for]
```
## Timeline
- **YYYY-MM-DD:** Vulnerability discovered [by researcher/team name]
- **YYYY-MM-DD:** Vendor notified
- **YYYY-MM-DD:** Vendor acknowledged issue
- **YYYY-MM-DD:** Fix developed and tested
- **YYYY-MM-DD:** Fixed version released
- **YYYY-MM-DD:** Public disclosure
## Credits
This vulnerability was discovered by [Researcher Name / Organization].
## References
- Stalwart Mail Server: https://stalw.art/
- CVE Entry: https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-YYYY-NNNNN
- GitHub Advisory: [Link to GitHub Security Advisory if applicable]
- Release Notes: [Link to release notes with fix]
## Contact Information
For questions or concerns regarding this advisory, please contact:
**Security Team:** hello@stalw.art
**Website:** https://stalw.art
To report security vulnerabilities in Stalwart, please follow our [responsible disclosure policy](https://github.com/stalwartlabs/stalwart/security/policy).
## Disclaimer
This advisory is provided "as is" without warranty of any kind. The information contained in this advisory is subject to change without notice.
---
**Document Version:** 1.0
**Classification:** Public
================================================
FILE: UPGRADING/v0_04.md
================================================
# Upgrading from `v0.4.0` to `v0.4.x`
- Replace the binary with the new version.
- Restart the service.
# Upgrading from `v0.3.x` to `v0.4.0`
## What's changed
- **Configuration File Split:** While the `config.toml` configuration file format hasn't changed much, the new version has divided it into multiple sub-files. These sub-files are now included from the new `config.toml`. This division was implemented because the config file had grown significantly, and splitting it improves organization.
- **Changes in the Sieve Interpreter Attribute Names:**
- The configuration key prefix `jmap.sieve` (JMAP Sieve Interpreter) has been renamed to `sieve.untrusted`.
- The configuration key prefix `sieve` (SMTP Sieve Interpreter) has been renamed to `sieve.trusted`.
## What's been added
- **SPAM Filter Module:** The most notable addition in this version is the SPAM filter module. It comprises:
- A TOML configuration file located at `etc/smtp/spamfilter.toml`.
- A set of Sieve scripts in `etc/spamfilter/scripts`.
- Lookup maps in `etc/spamfilter/maps`.
- **New Configuration Key:** A new key `resolver.public-suffix` has been added. This specifies the URL of the list of public suffixes.
## Migration Steps
1. **Backup:** Ensure you have a backup of your current `config.toml` file.
2. **Download Configuration Bundle:** Fetch the new configuration bundle from [this link](https://get.stalw.art/resources/config.zip). Unpack it under `BASE_DIR/etc` (for example `/opt/stalwart-mail/etc`).
3. **Update Configuration Files:** Modify the following files with your domain name, host name, certificate paths, DKIM signatures, and so on:
- `etc/config.toml`
- `etc/jmap/store.toml`
- `etc/jmap/oauth.toml`
- `etc/smtp/signature.toml`
- `etc/common/tls.toml`
4. **Adjust included files:** If you are using an LDAP directory for authentication, edit `etc/config.toml` and replace the `etc/directory/sql.toml` include with `etc/directory/ldap.toml`.
5. **Configure the SPAM Filter Database:** Set up and configure the SPAM filter database. More details can be found [here](https://stalw.art/docs/spamfilter/settings/database).
6. **Review All TOML Files:** Navigate to every TOML file under the `etc/` directory and make necessary changes.
7. **Update Binary:** Download and substitute the v0.4.0 binary suitable for your platform from [here](https://github.com/stalwartlabs/mail-server/releases/tag/v0.4.0).
8. **Restart Service:** Conclude by restarting the Stalwart service.
### Alternative Method:
1. **Separate Installation:** Install v0.4.0 in a distinct directory. This will auto-update all configuration files and establish the spam filter database in SQLite format.
2. **Move Configuration Files:** Transfer the configuration files from `etc/` and the SQLite spam filter database from `data/` to your current installation's directory.
3. **Replace Binary:** Move the binary from the `bin/` directory to your current installation's `data/` directory.
4. **Restart Service:** Finally, restart the Stalwart service.
We apologize for the lack of an automated migration tool for this upgrade. However, we are planning on introducing an automated migration tool in the near future. Thank you for your understanding and patience.
================================================
FILE: UPGRADING/v0_05.md
================================================
# Upgrading from `v0.5.2` to `v0.5.3`
- The following configuration attributes have been renamed, see [store.toml](https://github.com/stalwartlabs/mail-server/blob/main/resources/config/common/store.toml) for an example:
- `jmap.store.data` -> `storage.data`
- `jmap.store.fts` -> `storage.fts`
- `jmap.store.blob` -> `storage.blob`
- `jmap.encryption.*` -> `storage.encryption.*`
- `jmap.spam.header` -> `storage.spam.header`
- `jmap.fts.default-language` -> `storage.fts.default-language`
- `jmap.cluster.node-id` -> `storage.cluster.node-id`
- `management.directory` and `sieve.trusted.default.directory` -> `storage.directory`
- `sieve.trusted.default.store` -> `storage.lookup`
- Proxy networks are now configured under `server.proxy.trusted-networks` rather than `server.proxy-trusted-networks`. IP addresses/masks have to be defined within a set (`{}`) rather than a list (`[]`), see [server.toml](https://github.com/stalwartlabs/mail-server/blob/main/resources/config/common/server.toml) for an example.
# Upgrading from `v0.5.1` to `v0.5.2`
- Make sure that implicit TLS is enabled for the JMAP [listener](https://stalw.art/docs/server/listener) configured under `ets/jmap/listener.toml`:
```toml
[server.listener."jmap".tls]
implicit = true
```
- Optional: Enable automatic TLS with [ACME](https://stalw.art/docs/server/tls/acme).
- Replace the binary with the new version.
- Restart the service.
# Upgrading from `v0.5.0` to `v0.5.1`
- Replace the binary with the new version.
- Restart the service.
# Upgrading from `v0.4.x` to `v0.5.0`
## What's changed
- **Database Layout**: Version 0.5.0 utilizes a different database layout which is more efficient and allows multiple backends to be supported. For this reason, the database must be migrated to the new layout.
- **Configuration file changes**: The configuration file has been updated to support multiple stores, most configuration attributes starting with `store.*` and `directory.*` need to be reviewed.
- **SPAM filter**: Sieve scripts that interact with databases need to be updated. The functions `lookup` and `lookup_map` has been renamed to `key_exists` and `key_get`. It is recommended to replace all scripts with the new versions rather than updating them manually. Additionally, the SPAM database no longer requires an SQL server, it can now be stored in Redis or any of the supported databases.
- **Directory superusers**: Due to problems and confusion with the `superuser-group` attribute, the concept of a superuser group has been removed. Instead, a new attribute `type` has been added to external directories. The value of this attribute can be `individual`, `group` or `admin`. The `admin` type is equivalent to the old superuser group. The `type` attribute is required for all principals in the directory, it defaults to `individual` if not specified.
- **Purge schedules**: The attributes `jmap.purge.schedule.db` and `jmap.purge.schedule.blobs` have been removed. Instead, the purge frequency is now specified per store in `store..purge.frequency`. The attribute `jmap.purge.schedule.sessions` has been renamed to `jmap.purge.sessions.frequency`.
## What's been added
- **Multiple stores**: The server now supports multiple stores to be defined in the configuration file under `store.`. Which store to use is defined in the `jmap.store.data`, `jmap.store.fts` and `jmap.store.blob` settings.
- **More backend options**: It is now possible to use `RocksDB`, `PostgreSQL` and `MySQL` as data stores. It is also now possible to store blobs in any of the supported databases instead of being limited to the filesystem or an S3-compatible storage. Full-text indexing can now be done using `Elasticsearch` and the Spam database stored in `Redis`.
- **Internal Directory**: The server now has an internal directory that can be used to store user accounts, passwords and group membership. This directory can be used instead of an external directory such as LDAP or SQL.
- **New settings**: When running Stalwart in a cluster, `jmap.cluster.node-id` allows to specify a unique identifier for each node. Messages containing the SPAM headers defined in `jmap.spam.header` are moved automatically to the user's Junk Mail folder.
- **Default Sieve stores**: For Sieve scripts such as the Spam filter that require access to a directory and a lookup store, it is now possible to configure the default lookup store and directory using the `sieve.trusted.default.directory` and `sieve.trusted.default.store` settings.
## Migration Steps
Rather than manually updating the configuration file, it is recommended to start with a fresh configuration file and update it with the necessary settings:
- Install `v0.5.0` in a distinct directory. You now have the option to use an [internal directory](https://stalw.art/docs/directory/types/internal), which will allow you to manage users and groups directly from Stalwart server. Alternatively, you can continue to use an external directory such as LDAP or SQL.
- Update the configuration files with your previous settings. All configuration attributes are backward compatible, except those starting with `store.*`, `directory.*` and `jmap.purge.*`.
- Export each account following the procedure described in the [migration guide](https://stalw.art/docs/management/database/migrate).
- Stop the old `v0.4.x` server.
- If there are messages pending to be delivered in the SMTP queue, move the `queue` directory to the new installation.
- Start the new `v0.5.0` server.
- Import each account following the procedure described in the [migration guide](https://stalw.art/docs/management/database/migrate).
Once again, we apologize for the lack of an automated migration tool for this upgrade. However, we are planning on introducing an automated migration tool once the web-admin is released in Q1 2024. Thank you for your understanding and patience.
================================================
FILE: UPGRADING/v0_06.md
================================================
# Upgrading from `v0.5.3` to `v0.6.0`
- In order to support [expressions](https://stalw.art/docs/configuration/expressions/overview), version `0.6.0` introduces multiple breaking changes in the SMTP server configuration file. It is recommended to download the new SMTP configuration files from the [repository](https://github.com/stalwartlabs/mail-server/tree/main/resources/config/smtp), make any necessary changes and replace the old files under `INSTALL_DIR/etc/smtp` with the new ones.
- If you are using custom subaddressing of catch-all rules, you'll need to replace these rules with expressions. Check out the updated [syntax](https://stalw.art/docs/directory/addresses).
- Message queues are now distributed and stored in the backend specified by the `storage.data` and `storage.blob` settings. Make sure to flush your SMTP message queue before upgrading to `0.6.0` to avoid losing any outgoing messages pending delivery.
- Replace the binary with the new version.
- Restart the service.
================================================
FILE: UPGRADING/v0_07.md
================================================
# Upgrading from `v0.6.0` to `v0.7.0`
Version `0.7.0` of Stalwart introduces significant improvements and features that enhance performance and functionality. However, it also comes with multiple breaking changes in the configuration files and a revamped database layout optimized for accessing large mailboxes. Additionally, Stalwart now supports compression for binaries stored in the blob store, further increasing efficiency.
Due to these extensive changes, the recommended approach for upgrading is to perform a clean reinstallation of Stalwart and manually migrate your accounts to the new version.
## Pre-Upgrade Steps
- Download the `v0.7.0` mail-server and CLI binaries for your platform from the [releases page](https://github.com/stalwartlabs/mail-server/releases/latest/).
- Initialize the setup on a distinct directory using the command `sudo ./stalwart-mail --init /path/to/new-install`. This command will print the administrator password required to access the web-admin.
- Create the `bin` directory using `mkdir /path/to/new-install/bin`.
- Move the downloaded binaries to the `bin` directory using the command `mv stalwart-mail stalwart-cli /path/to/new-install/bin`.
- Open `/path/to/new-install/etc/config.toml` in a text editor and comment out all listeners except the HTTP listener for port `8080`.
- Start the new installation from the terminal using the command `sudo /path/to/new-install/bin/stalwart-mail --config /path/to/new-install/etc/config.toml`.
- Point your browser to the web-admin at `http://yourserver.org:8080` and login using the auto-generated administrator password.
- Configure the new installation with your domain, hostname, certificates, and other settings following the instructions at [stalw.art/docs/get-started](https://stalw.art/docs/get-started). Ignore the part about using the installation script, we are performing a manual installation.
- Add your user accounts.
- Configure Stalwart to run as the `stalwart-mail` user and `stalwart-mail` group from `Settings` > `Server` > `System`. This is not necessary if you are using Docker.
- Stop the new installation by pressing `Ctrl+C` in the terminal.
## Upgrade Steps
- On your `v0.6.0` installation, open in a text editor the `smtp/listener.toml`, `imap/listener.toml` files and comment out all listeners except the JMAP/HTTP listener (we are going to need it to export the user accounts) and then restart the service.
- If you are using an external store, backup the database using the appropriate method for your database system.
- Create the `~/exports` directory, here we will store the exported accounts.
- Using the existing CLI tool (not the one you just downloaded as it is not compatible), export each user account using the command `./stalwart-cli -u https://your-old-server.org -c export account ~/exports`.
- Stop the `v0.6.0` installation using the command `sudo systemctl stop stalwart-mail`.
- Move the old `v0.6.0` installation to a backup directory, for example `mv /opt/stalwart-mail /opt/stalwart-mail-backup`.
- Move the new `v0.7.0` installation to the old installation directory, for example `mv /path/to/new-install /opt/stalwart-mail`.
- Set the right permissions for the new installation using the command `sudo chown -R stalwart-mail:stalwart-mail /opt/stalwart-mail`.
- Start the new installation using the command `sudo systemctl start stalwart-mail`.
- Import the accounts using the new CLI tool with the command `./stalwart-cli -u http://yourserver.org:8080 -c import account ~/exports/`.
- Using the admin tool, reactivate all the necessary listener (SMTP, IMAP, etc.)
- Restart the service using the command `sudo systemctl restart stalwart-mail`.
We apologize for the complexity of the upgrade process associated with this version of Stalwart. We understand the challenges and inconveniences that the requirement for a clean reinstallation and manual account migration poses. Moving forward, an automated migration tool will be included in any future releases that necessitate changes to the database layout, aiming to streamline the upgrade process for you. Furthermore, as we approach the milestone of version 1.0.0, we anticipate that such foundational changes will become increasingly infrequent, leading to more straightforward updates. We appreciate your patience and commitment to Stalwart during this upgrade.
================================================
FILE: UPGRADING/v0_08.md
================================================
# Upgrading from `v0.7.3` to `v0.8.0`
Version `0.8.0` includes both performance and security enhancements that require your data to be migrated to a new database layout. Luckily version `0.7.3` includes a migration tool which should make this process much easier than previous upgrades. In addition to the new layout, you will have to change the systemd service file to use the `CAP_NET_BIND_SERVICE` capability.
## Preparation
- Upgrade to version `0.7.3` if you haven't already. If you are on a version previous to `0.7.0`, you will have to do a manual migration of your data using the Command-line Interface.
- Create a directory where your data will be exported to, for example `/opt/stalwart-mail/export`.
## Systemd service upgrade (Linux only)
- Stop the `v0.7.3` installation:
```bash
$ sudo systemctl stop stalwart-mail
```
- Update your systemd file to include the `CAP_NET_BIND_SERVICE` capability. Open the file `/etc/systemd/system/stalwart-mail.service` in a text editor and add the following lines under the `[Service]` section:
```
User=stalwart-mail
Group=stalwart-mail
AmbientCapabilities=CAP_NET_BIND_SERVICE
```
- Reload the daemon:
```bash
$ systemctl daemon-reload
```
- Do not start the service yet.
## Data migration
- Stop Stalwart and export your data:
```bash
$ sudo systemctl stop stalwart-mail
$ sudo /opt/stalwart-mail/bin/stalwart-mail --config /opt/stalwart-mail/etc/config.toml --export /opt/stalwart-mail/export
$ sudo chown -R stalwart-mail:stalwart-mail /opt/stalwart-mail/export
```
or, if you are using the Docker image:
```bash
$ docker stop stalwart-mail
$ docker run --rm -v :/opt/stalwart-mail -it stalwart-mail /opt/stalwart-mail/bin/stalwart-mail --config /opt/stalwart-mail/etc/config.toml --export /opt/stalwart-mail/export
```
- Backup your `v0.7.3` installation:
- If you are using RocksDB or SQLite, simply rename the `data` directory to `data-backup`, for example:
```bash
$ mv /opt/stalwart-mail/data /opt/stalwart-mail/data-backup
$ mkdir /opt/stalwart-mail/data
$ chown stalwart-mail:stalwart-mail /opt/stalwart-mail/data
```
- If you are using PostgreSQL, rename the database and create a blank database with the same name, for example:
```sql
ALTER DATABASE stalwart RENAME TO stalwart_old;
CREATE database stalwart;
```
- If you are using MySQL, rename the database and create a blank database with the same name, for example:
```sql
CREATE DATABASE stalwart_old;
RENAME TABLE stalwart.b TO stalwart_old.b;
RENAME TABLE stalwart.v TO stalwart_old.v;
RENAME TABLE stalwart.l TO stalwart_old.l;
RENAME TABLE stalwart.i TO stalwart_old.i;
RENAME TABLE stalwart.t TO stalwart_old.t;
RENAME TABLE stalwart.c TO stalwart_old.c;
DROP DATABASE stalwart;
CREATE database stalwart;
```
- If you are using FoundationDB, backup your database and clean the entire key range.
- Download the `v0.8.0` mail-server for your platform from the [releases page](https://github.com/stalwartlabs/mail-server/releases/latest/) and replace the binary in `/opt/stalwart-mail/bin`. If you are using the Docker image, pull the latest image.
- Import your data:
```bash
$ sudo -u stalwart-mail /opt/stalwart-mail/bin/stalwart-mail --config /opt/stalwart-mail/etc/config.toml --import /opt/stalwart-mail/export
```
or, if you are using the Docker image:
```bash
$ docker run --rm -v :/opt/stalwart-mail -it stalwart-mail /opt/stalwart-mail/bin/stalwart-mail --config /opt/stalwart-mail/etc/config.toml --import /opt/stalwart-mail/export
```
- Start the service:
```bash
$ sudo systemctl start stalwart-mail
```
Or, if you are using the Docker image:
```bash
$ docker start stalwart-mail
```
================================================
FILE: UPGRADING/v0_09.md
================================================
# Upgrading from `v0.8.x` to `v0.9.0`
Version `0.9.0` introduces significant internal improvements while maintaining compatibility with existing database layouts and configuration file formats from version `0.8.0`. As a result, no data or configuration migration is necessary. This release focuses on enhancing performance and functionality, particularly in logging and tracing capabilities.
To upgrade to Stalwart version `0.9.0` from `0.8.x`, begin by downloading the latest version of the `stalwart-mail` binary. Once downloaded, replace the existing binary with the new version. Additionally, it's important to update the WebAdmin interface to the latest version to ensure compatibility and to access new features introduced in this release.
In terms of breaking changes, this release brings significant updates to webhooks. All webhook event names have been modified, requiring a thorough review and adjustment of existing webhook configurations. Furthermore, the update introduces hundreds of new event types, enhancing the granularity and specificity of event handling capabilities. Users should familiarize themselves with these changes to effectively integrate them into their systems.
The reason for this release being classified as a major version, despite the absence of changes to the database or configuration formats, is the complete rewrite of the logging and tracing layer. This overhaul substantially improves the efficiency and speed of generating detailed tracing and logging events, making the system more robust and facilitating easier debugging and monitoring.
================================================
FILE: UPGRADING/v0_10.md
================================================
# Upgrading from `v0.9.x` to `v0.10.0`
## Important Notes
- In version `0.10.0` accounts are associated with roles and permissions, which define what resources they can access. The concept of administrator or super user accounts no longer exists, now there is a single account type (the `individual` principal) which can be assigned the `admin` role or custom permissions to have administrator access.
- Due to the changes in the database layout in order to support roles and permissions, the database must be migrated to the new layout. The migration is automatic and should not require any manual intervention.
- While the database migration is automatic, it's recommended to **back up your data** before upgrading.
- The webadmin must be upgraded **before** the mail server to maintain access post-upgrade. This is true even if you run Stalwart in Docker.
## Step-by-Step Upgrade Process
- Upgrade the webadmin by clicking on `Manage` > `Maintenance` > `Update Webadmin`.
- Stop Stalwart and backup your data:
```bash
$ sudo systemctl stop stalwart-mail
$ sudo /opt/stalwart-mail/bin/stalwart-mail --config /opt/stalwart-mail/etc/config.toml --export /opt/stalwart-mail/export
$ sudo chown -R stalwart-mail:stalwart-mail /opt/stalwart-mail/export
```
or, if you are using the Docker image:
```bash
$ docker stop stalwart-mail
$ docker run --rm -v :/opt/stalwart-mail -it stalwart-mail /usr/local/bin/stalwart-mail --config /opt/stalwart-mail/etc/config.toml --export /opt/stalwart-mail/export
```
- Download the `v0.10.0` mail-server for your platform from the [releases page](https://github.com/stalwartlabs/mail-server/releases/latest/) and replace the binary in `/opt/stalwart-mail/bin`. If you are using the Docker image, pull the latest image.
- Start the service:
```bash
$ sudo systemctl start stalwart-mail
```
Or, if you are using the Docker image:
```bash
$ docker start stalwart-mail
```
================================================
FILE: UPGRADING/v0_11.md
================================================
# Upgrading from `v0.10.x` to `v0.11.0`
Version `0.11.0` introduces breaking changes to the spam filter configuration. Although no data migration is required, if changes were made to the previous spam filter, the configuration of the new spam filter should be reviewed. In particular:
- `lookup.spam-*` settings are no longer used, these have been replaced by `spam-filter.*` settings. Review the [updated documentation](http://stalw.art/docs/spamfilter/overview).
- Previous `spam-filter` and `track-replies` Sieve scripts cannot be used with the new version. They have been replaced by a built-in spam filter written in Rust.
- Cache settings have changed, see the [documentation](https://stalw.art/docs/server/cache) for details.
- Support for Pipes was removed in favor of MTA hooks and Milter.
- `config.resource.spam-filter` is now `spam-filter.resource`.
- `config.resource.webadmin` is now `webadmin.resource`.
- `authentication.rate-limit` was removed as security is handled by fail2ban.
================================================
FILE: UPGRADING/v0_12.md
================================================
# Upgrading from `v0.11.x` to `v0.12.x`
## Important Notes
Version `0.12.x` introduces significant improvements such as zero-copy deserialization which make the new database layout incompatible with the previous version. As a result, the database must be migrated to the new layout. The migration is done automatically on startup and should not require any manual intervention. However, it is highly recommended to **back up your data** before upgrading since it is not possible to downgrade the database once it has been migrated. You may also want to run a mock migration before upgrading to ensure that everything works as expected.
In addition to the database layout changes, multiple settings were renamed:
- `server.http.*` to `http.*`.
- `jmap.folders.*` to `email.folders.*`.
- `jmap.account.purge.frequency` to `account.purge.frequency`.
- `jmap.email.auto-expunge` to `email.auto-expunge`.
- `jmap.protocol.changes.max-history` to `changes.max-history`.
- `storage.encryption.*` to `email.encryption.*`.
## Step-by-Step Upgrade Process
- Stop Stalwart in **every single node of your cluster**. If you are using the systemd service, you can do this with the following command:
```bash
$ sudo systemctl stop stalwart-mail
```
- Backup your data following your database system's instructions. For example, if you are using RocksDB or SQLite, you can simply copy the `data` directory to a backup location. If you are using PostgreSQL or MySQL, you can use the `pg_dump` or `mysqldump` commands to create a backup of your database. If your database does not support backups, you can use the [built-in migration utility](https://stalw.art/docs/management/migration) to export your data to a file. For example:
```bash
$ sudo /opt/stalwart-mail/bin/stalwart-mail --config /opt/stalwart-mail/etc/config.toml --export /opt/stalwart-mail/export
$ sudo chown -R stalwart-mail:stalwart-mail /opt/stalwart-mail/export
```
- Download the `v0.12.x` binary for your platform (which is now called `stalwart` rather than `mail-server`) from the [releases page](https://github.com/stalwartlabs/stalwart/releases/latest/) and replace the binary in `/opt/stalwart-mail/bin`. If you rename the binary from `stalwart` to `stalwart-mail`, you can keep the same systemd service file, otherwise you will need to update the service file to point to the new binary name.
- Start the service. In a cluster, you can speed up the migration process by starting all nodes at once.
```bash
$ sudo systemctl start stalwart-mail
```
- Upgrade the webadmin by clicking on `Manage` > `Maintenance` > `Update Webadmin`.
## Step-by-Step Upgrade Process (Docker)
- Stop the Stalwart container in **every single node of your cluster**. If you are using Docker, you can do this with the following command:
```bash
$ docker stop stalwart-mail
```
- Backup your data following your database system's instructions. For example, if you are using RocksDB or SQLite, you can simply copy the `data` directory to a backup location. If you are using PostgreSQL or MySQL, you can use the `pg_dump` or `mysqldump` commands to create a backup of your database. If your database does not support backups, you can use the `--export` command to export your data to a file. For example:
```bash
$ docker run --rm -v :/opt/stalwart-mail -it stalwart-mail /usr/local/bin/stalwart-mail --config /opt/stalwart-mail/etc/config.toml --export /opt/stalwart-mail/export
```
- The Docker image location has now changed to `stalwartlabs/stalwart` instead of `stalwartlabs/mail-server`. Pull the latest image and configure it to use your existing data directory:
```bash
$ docker run -d -ti -p 443:443 -p 8080:8080 \
-p 25:25 -p 587:587 -p 465:465 \
-p 143:143 -p 993:993 -p 4190:4190 \
-p 110:110 -p 995:995 \
-v :/opt/stalwart \
--name stalwart stalwartlabs/stalwart:latest
```
- Since the mount point has changed from `/opt/stalwart-mail` to `/opt/stalwart`, you will need to update your Stalwart's configuration file to reflect this change. Open the file `/opt/stalwart/etc/config.toml` and update the paths accordingly.
- Upgrade the webadmin by clicking on `Manage` > `Maintenance` > `Update Webadmin`.
================================================
FILE: UPGRADING/v0_13.md
================================================
# Upgrading from `v0.12.x` (and `v0.11.x`) to `v0.13.x`
## Important Notes
Version `0.13.x` introduces a significant redesign of the MTA’s delivery and queueing subsystem. This includes a transition to a new message queue serialization format and a move to a strategy-based configuration model for routing, scheduling, and delivery control. Upon first launch of version `0.13.0`, any messages currently in the outbound queue will be automatically migrated to the new format. This migration is handled internally and does not require manual intervention.
However, if your deployment includes custom routing rules or queueing logic, it is important to manually reconfigure those settings using the new strategy framework. The previous configuration format for routing is no longer compatible and will need to be updated. For systems that rely solely on the default configuration, no changes are required and the upgrade should proceed without issue.
Even if your system uses the default settings, it is strongly recommended to read the accompanying [blog announcement](https://stalw.art/blog/virtual-queues) and consult the [updated documentation](https://stalw.art/docs/mta/outbound/overview). These resources provide a full overview of the new delivery architecture and can help you determine whether any adjustments are needed for your environment.
Before applying the upgrade to a production system, take time to familiarize yourself with the new configuration structure and validate that your delivery behavior aligns with the new model.
## Step-by-Step Upgrade Process
- Stop Stalwart in **every single node of your cluster**. If you are using the systemd service, you can do this with the following command:
```bash
$ sudo systemctl stop stalwart
```
- Backup your data following your database system's instructions. For example, if you are using RocksDB or SQLite, you can simply copy the `data` directory to a backup location. If you are using PostgreSQL or MySQL, you can use the `pg_dump` or `mysqldump` commands to create a backup of your database. If your database does not support backups, you can use the [built-in migration utility](https://stalw.art/docs/management/migration) to export your data to a file. For example:
```bash
$ sudo /opt/stalwart/bin/stalwart --config /opt/stalwart/etc/config.toml --export /opt/stalwart/export
$ sudo chown -R stalwart:stalwart /opt/stalwart/export
```
- Download the `v0.13.x` binary for your platform from the [releases page](https://github.com/stalwartlabs/stalwart/releases/latest/) and replace the binary in `/opt/stalwart/bin`.
- Start the service. In a cluster, you can speed up the migration process by starting all nodes at once.
```bash
$ sudo systemctl start stalwart
```
- Upgrade the webadmin by clicking on `Manage` > `Maintenance` > `Update Webadmin`.
## Step-by-Step Upgrade Process (Docker)
- Stop the Stalwart container in **every single node of your cluster**. If you are using Docker, you can do this with the following command:
```bash
$ docker stop stalwart
```
- Backup your data following your database system's instructions. For example, if you are using RocksDB or SQLite, you can simply copy the `data` directory to a backup location. If you are using PostgreSQL or MySQL, you can use the `pg_dump` or `mysqldump` commands to create a backup of your database. If your database does not support backups, you can use the `--export` command to export your data to a file. For example:
```bash
$ docker run --rm -v :/opt/stalwart -it stalwart /usr/local/bin/stalwart --config /opt/stalwart/etc/config.toml --export /opt/stalwart/export
```
- Pull the latest image and restart the container:
```bash
$ docker pull stalwartlabs/stalwart:latest
$ docker start stalwart
```
- Upgrade the webadmin by clicking on `Manage` > `Maintenance` > `Update Webadmin`.
================================================
FILE: UPGRADING/v0_14.md
================================================
# Upgrading from `v0.13.x` to `v0.14.x`
## Binary installation
- Stop Stalwart in **every single node of your cluster**. If you are using the systemd service, you can do this with the following command:
```bash
$ sudo systemctl stop stalwart
```
- Backup your data following your database system's instructions. For example, if you are using RocksDB or SQLite, you can simply copy the `data` directory to a backup location. If you are using PostgreSQL or MySQL, you can use the `pg_dump` or `mysqldump` commands to create a backup of your database. If your database does not support backups, you can use the [built-in migration utility](https://stalw.art/docs/management/migration) to export your data to a file. For example:
```bash
$ sudo /opt/stalwart/bin/stalwart --config /opt/stalwart/etc/config.toml --export /opt/stalwart/export
$ sudo chown -R stalwart:stalwart /opt/stalwart/export
```
- Download the latest binary for your platform from the [releases page](https://github.com/stalwartlabs/stalwart/releases/latest/) and replace the binary in `/opt/stalwart/bin`.
- Start the service. In a cluster, you can speed up the migration process by starting all nodes at once.
```bash
$ sudo systemctl start stalwart
```
- Upgrade the webadmin by clicking on `Manage` > `Maintenance` > `Update Webadmin`.
## Containerized
- Stop the Stalwart container in **every single node of your cluster**. If you are using Docker, you can do this with the following command:
```bash
$ docker stop stalwart
```
- Backup your data following your database system's instructions. For example, if you are using RocksDB or SQLite, you can simply copy the `data` directory to a backup location. If you are using PostgreSQL or MySQL, you can use the `pg_dump` or `mysqldump` commands to create a backup of your database. If your database does not support backups, you can use the `--export` command to export your data to a file. For example:
```bash
$ docker run --rm -v :/opt/stalwart -it stalwart /usr/local/bin/stalwart --config /opt/stalwart/etc/config.toml --export /opt/stalwart/export
```
- Pull the latest image and restart the container:
```bash
$ docker pull stalwartlabs/stalwart:latest
$ docker start stalwart
```
- Upgrade the webadmin by clicking on `Manage` > `Maintenance` > `Update Webadmin`.
================================================
FILE: UPGRADING/v0_15.md
================================================
# Upgrading from `v0.14.x` to `v0.15.x`
Stalwart `v0.15.x` introduces **breaking changes** to both the **database schema** and some **configuration options**.
Upgrading to this version **requires a schema migration**, which is performed **automatically when Stalwart starts** for the first time on `v0.15.x`.
Because this migration modifies how data is stored and indexed, it is important to understand what will change, what will be migrated, and how the upgrade may impact your deployment—especially for larger installations.
## What's changed
Version `0.15.x` introduces significant internal improvements focused on performance, storage efficiency, and accuracy:
- **Optimized database schema**: The database schema has been redesigned to use less storage space and significantly reduce the number of read and write operations required for common tasks.
- **Rewritten search layer**: The search subsystem has been completely rewritten to use a more efficient and scalable indexing strategy.
- **Native full-text search for SQL backends**: When using **PostgreSQL** or **MySQL** as the backend, Stalwart now leverages the database’s **native full-text search capabilities**, replacing the previous custom full-text search implementation.
- **New spam classifier engine** : The spam classifier has been rewritten to use the **FTRL-Proximal** algorithm instead of the previous **Naive Bayes** implementation. This change improves classification accuracy, reduces memory usage, and reduces storage requirements for training data.
## What will be migrated
The migration process runs automatically at startup and will migrate the following data:
- **E-mail metadata**, including flags, folders, and parsed message representations. *(The raw e-mail content stored in the blob store is not migrated.)*
- **Encryption-at-rest settings**, which now also include a **spam training privacy option**
- **MTA message queue metadata** *(The actual message contents are not migrated.)*
- **Maintenance tasks**
- **Blob links** *(The underlying blobs themselves are not migrated.)*
- **Search indexes**, which will be **rebuilt** using the new indexing strategy
## Important considerations
- For deployments with **1,000 or more mailboxes**, the migration may take a **considerable amount of time**, depending on the volume of stored data.
- During migration, **Stalwart runs in read-only mode**:
- No new e-mail can be received
- No outbound e-mail can be sent
- It is **strongly recommended** to perform this upgrade during a **maintenance window**.
- By default, the migration process is **multithreaded** and uses two threads for each available CPUs. You can control the number of threads by setting the following environment variable ``NUM_THREADS=``
> **Note:** If you do **not** require any of the features introduced in `v0.15.x`, consider **waiting for the next major release**, which will introduce a proxy-based architecture allowing **zero-downtime upgrades**.
## Upgrading steps
### Binary installation
- Stop Stalwart in **every single node of your cluster**. If you are using the systemd service, you can do this with the following command:
```bash
$ sudo systemctl stop stalwart
```
- Backup your data following your database system's instructions. For example, if you are using RocksDB or SQLite, you can simply copy the `data` directory to a backup location. If you are using PostgreSQL or MySQL, you can use the `pg_dump` or `mysqldump` commands to create a backup of your database.
- Download the latest binary for your platform from the [releases page](https://github.com/stalwartlabs/stalwart/releases/latest/) and replace the binary in `/opt/stalwart/bin`.
- Start the service. In a cluster, you can speed up the migration process by starting all nodes at once.
```bash
$ sudo systemctl start stalwart
```
### Containerized
- Stop the Stalwart container in **every single node of your cluster**. If you are using Docker, you can do this with the following command:
```bash
$ docker stop stalwart
```
- Backup your data following your database system's instructions. For example, if you are using RocksDB or SQLite, you can simply copy the `data` directory to a backup location. If you are using PostgreSQL or MySQL, you can use the `pg_dump` or `mysqldump` commands to create a backup of your database.
- Pull the latest image and restart the container:
```bash
$ docker pull stalwartlabs/stalwart:latest
$ docker start stalwart
```
## Post-upgrade steps
After the upgrade and migration complete, several follow-up steps are required or recommended:
- **Upgrade the webadmin**: Upgrade the webadmin interface by navigating to ``Manage → Maintenance → Update Webadmin``
- **Update the spam rules**: Download and apply the latest spam rules from the webadmin ``Manage → Maintenance → Update Spam rules``
- **Update search settings**: Review the updated documentation for search settings, as some configuration options have changed. In particular, the Elasticsearch backend now uses **different authentication settings** than previous versions.
- **Rebuild search indexes**: All search indexes must be rebuilt to take advantage of the new indexing strategy. This can be done from the webadmin interface ``Manage → Maintenance``.
- **Recalculate disk quotas for all accounts**: This step is **not required immediately**, but it is recommended to perform it at some point after the upgrade. The new version includes additional metadata in quota calculations, so recalculating ensures accurate disk usage reporting.
```bash
$ curl -X DELETE https://myserver.org/api/store/quota/ -u : -k
```
- **Delete deprecated spam classifier keys**: Remove deprecated spam classifier keys from the memory store. These are the keys starting with the integer prefixes `12` to `16` and `17` to `18`:
- If you are using Redis:
```bash
$ for code in {12..18}; do
char=$(printf "\\x$(printf '%02x' $code)")
redis-cli --scan --pattern "${char}*" | xargs -r redis-cli DEL
done
```
- If you are using your database as the in-memory store:
```bash
$ /opt/stalwart/bin/stalwart --config /opt/stalwart/etc/config.toml --console
Stalwart Server v0.15.2 Data Store CLI
> delete y\x0c\x00 y\x12\xff
> delete m\x0c\x00 m\x12\xff
> exit
```
- If you are using your database as the in-memory store with Docker:
```bash
$ docker stop stalwart
$ docker run -it --rm \
-v :/opt/stalwart \
--entrypoint /usr/local/bin/stalwart \
stalwartlabs/stalwart:latest \
--config /opt/stalwart/etc/config.toml --console
Stalwart Server v0.15.2 Data Store CLI
> delete y\x0c\x00 y\x12\xff
> delete m\x0c\x00 m\x12\xff
> exit
$ docker start stalwart
```
## Troubleshooting
### Interrupted or stopped migration
If the migration process is interrupted or stopped, it can be **resumed automatically** by simply restarting Stalwart.
### `Data corruption detected` error
If you see an error message similar to: ``Data corruption detected``. This indicates that **another node wrote data using the old format while the migration was in progress**. This usually happens when the cluster was **not fully stopped** before starting the upgrade.
In order to resolve this issue, follow these steps:
1. Stop **all** Stalwart nodes.
2. Ensure **all nodes are upgraded** to `v0.15.x`.
3. Start the nodes again.
### Forcing a migration
If the migration does not resume because the node responsible for it already marked it as completed, you can force migration using environment variables:
- **Force re-migration of MTA queue metadata**: ``FORCE_MIGRATE_QUEUE=4``
- **Force re-migration of blob links**: ``FORCE_MIGRATE_BLOBS=4``
- **Force re-migration of a specific account**: ``FORCE_MIGRATE_ACCOUNT=``
- **Force re-migration of all data**: ``FORCE_MIGRATE=4``
Use these options with care and only when necessary.
================================================
FILE: api/v1/openapi.yml
================================================
# SPDX-FileCopyrightText: 2025 Stalwart Labs LLC
#
# SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
openapi: 3.0.0
info:
title: Stalwart API
version: 1.0.0
servers:
- url: https://mail.example.org/api
description: Sample server
paths:
/oauth:
post:
summary: Obtain OAuth token
responses:
"200":
description: OK
content:
application/json:
schema:
type: object
properties:
data:
type: object
properties:
code:
type: string
permissions:
type: array
items:
type: string
version:
type: string
isEnterprise:
type: boolean
example:
data:
code: 4YmRFLu9Df1t4JO7Iffnuney4B8tVLAxjimdRxEg
permissions:
- webadmin-update
- spam-filter-update
- dkim-signature-get
- dkim-signature-create
- undelete
- fts-reindex
- purge-account
- purge-in-memory-store
- purge-data-store
- purge-blob-store
version: 0.11.0
isEnterprise: true
"401":
description: Unauthorized
content:
application/json:
schema:
type: object
properties:
type:
type: string
status:
type: number
title:
type: string
detail:
type: string
example:
type: about:blank
status: 401
title: Unauthorized
detail: You have to authenticate first.
requestBody:
content:
application/json:
schema:
type: object
properties:
type:
type: string
client_id:
type: string
redirect_uri:
type: string
nonce:
type: string
example:
type: code
client_id: webadmin
redirect_uri: stalwart://auth
nonce: ttsaXca3qx
/telemetry/metrics:
get:
summary: Fetch Telemetry Metrics
responses:
"200":
description: OK
content:
application/json:
schema:
type: object
properties:
error:
type: string
details:
type: string
reason:
type: string
example:
error: other
details: No metrics store has been defined
reason:
You need to configure a metrics store in order to use this
feature.
parameters:
- name: after
in: query
required: false
schema:
type: string
/telemetry/live/metrics-token:
get:
summary: Obtain Metrics Telemetry token
responses:
"200":
description: OK
content:
application/json:
schema:
type: object
properties:
data:
type: string
example:
data: 2GO4RahIkSAms6S00R9BRsroo97ZdYTz4QVxFCOwGrGkr7zguP0AVyTMA/iha3Vz/////w8DhZi1+ALBmLX4AndlYg==
/telemetry/metrics/live:
get:
summary: Live Metrics
responses:
"200":
description: OK
content: {}
parameters:
- name: metrics
in: query
required: false
schema:
type: string
- name: interval
in: query
required: false
schema:
type: number
- name: token
in: query
required: false
schema:
type: string
/principal:
get:
summary: List Principals
responses:
"200":
description: OK
content:
application/json:
schema:
type: object
properties:
data:
type: object
properties:
items:
type: array
items: {}
total:
type: number
example:
data:
items: []
total: 0
parameters:
- name: page
in: query
required: false
schema:
type: number
- name: limit
in: query
required: false
schema:
type: number
- name: types
in: query
required: false
schema:
type: string
post:
summary: Create Principal
responses:
"200":
description: OK
content:
application/json:
schema:
type: object
properties:
data:
type: number
example:
data: 50
requestBody:
content:
application/json:
schema:
type: object
properties:
type:
type: string
quota:
type: number
name:
type: string
description:
type: string
secrets:
type: array
items: {}
emails:
type: array
items: {}
urls:
type: array
items: {}
memberOf:
type: array
items: {}
roles:
type: array
items: {}
lists:
type: array
items: {}
members:
type: array
items: {}
enabledPermissions:
type: array
items: {}
disabledPermissions:
type: array
items: {}
externalMembers:
type: array
items: {}
example:
type: domain
quota: 0
name: example.org
description: Example domain
secrets: []
emails: []
urls: []
memberOf: []
roles: []
lists: []
members: []
enabledPermissions: []
disabledPermissions: []
externalMembers: []
/dkim:
post:
summary: Create DKIM Signature
responses:
"200":
description: OK
content:
application/json:
schema:
type: object
properties:
data:
type: object
nullable: true
example:
data:
requestBody:
content:
application/json:
schema:
type: object
properties:
id:
type: object
nullable: true
algorithm:
type: string
domain:
type: string
selector:
type: object
nullable: true
example:
id:
algorithm: Ed25519
domain: example.org
selector:
/principal/{principal_id}:
get:
summary: Fetch Principal
responses:
"200":
description: OK
content:
application/json:
schema:
type: object
properties:
data:
type: object
properties:
id:
type: number
type:
type: string
secrets:
type: string
name:
type: string
quota:
type: number
description:
type: string
emails:
type: string
roles:
type: array
items:
type: string
lists:
type: array
items:
type: string
example:
data:
id: 90
type: individual
secrets: $6$ONjGT6nQtmPNaxw0$NNF5DXtPfOay2mfVnPJ0uQ77C.L3LNxXO/QMyphP/DzpODqbDBBGd4/gCnckYPQj3st6pqwY8/KeBsCJ.oe1Y1
name: jane
quota: 0
description: Jane Doe
emails: jane@example.org
roles:
- user
lists:
- all
parameters:
- name: principal_id
in: path
required: true
schema:
type: string
patch:
summary: Update Principal
responses:
"200":
description: OK
content:
application/json:
schema:
type: object
properties:
data:
type: object
nullable: true
example:
data:
parameters:
- name: principal_id
in: path
required: true
schema:
type: string
requestBody:
content:
application/json:
schema:
type: array
items:
type: object
properties:
action:
type: string
field:
type: string
value:
type: string
example:
- action: set
field: name
value: jane.doe
- action: set
field: description
value: Jane Mary Doe
- action: addItem
field: emails
value: jane-doe@example.org
- action: removeItem
field: emails
value: jane@example.org
delete:
summary: Delete Principal
responses:
"200":
description: OK
content:
application/json:
schema:
type: object
properties:
data:
type: object
nullable: true
example:
data:
parameters:
- name: principal_id
in: path
required: true
schema:
type: string
/queue/messages:
get:
summary: List Queued Messages
responses:
"200":
description: OK
content:
application/json:
schema:
type: object
properties:
data:
type: object
properties:
items:
type: array
items: {}
total:
type: number
status:
type: boolean
example:
data:
items: []
total: 0
status: true
parameters:
- name: page
in: query
required: false
schema:
type: number
- name: max-total
in: query
required: false
schema:
type: number
- name: limit
in: query
required: false
schema:
type: number
- name: values
in: query
required: false
schema:
type: number
patch:
summary: Reschedule Queued Messages
responses:
"200":
description: OK
content:
application/json:
schema:
type: object
properties:
data:
type: boolean
example:
data: true
parameters:
- name: filter
in: query
required: false
schema:
type: string
delete:
summary: Delete Queued Messages
responses:
"200":
description: OK
content:
application/json:
schema:
type: object
properties:
data:
type: boolean
example:
data: true
parameters:
- name: text
in: query
required: false
schema:
type: string
/queue/reports:
get:
summary: List Queued Reports
responses:
"200":
description: OK
content:
application/json:
schema:
type: object
properties:
data:
type: object
properties:
items:
type: array
items: {}
total:
type: number
example:
data:
items: []
total: 0
parameters:
- name: max-total
in: query
required: false
schema:
type: number
- name: limit
in: query
required: false
schema:
type: number
- name: page
in: query
required: false
schema:
type: number
/reports/dmarc:
get:
summary: List Incoming DMARC Reports
responses:
"200":
description: OK
content:
application/json:
schema:
type: object
properties:
data:
type: object
properties:
items:
type: array
items: {}
total:
type: number
example:
data:
items: []
total: 0
parameters:
- name: max-total
in: query
required: false
schema:
type: number
- name: limit
in: query
required: false
schema:
type: number
- name: page
in: query
required: false
schema:
type: number
/reports/tls:
get:
summary: List Incoming TLS Reports
responses:
"200":
description: OK
content:
application/json:
schema:
type: object
properties:
data:
type: object
properties:
items:
type: array
items: {}
total:
type: number
example:
data:
items: []
total: 0
parameters:
- name: limit
in: query
required: false
schema:
type: number
- name: max-total
in: query
required: false
schema:
type: number
- name: page
in: query
required: false
schema:
type: number
/reports/arf:
get:
summary: List Incoming ARF Reports
responses:
"200":
description: OK
content:
application/json:
schema:
type: object
properties:
data:
type: object
properties:
items:
type: array
items: {}
total:
type: number
example:
data:
items: []
total: 0
parameters:
- name: page
in: query
required: false
schema:
type: number
- name: limit
in: query
required: false
schema:
type: number
- name: max-total
in: query
required: false
schema:
type: number
/telemetry/traces:
get:
summary: List Stored Traces
responses:
"200":
description: OK
content:
application/json:
schema:
type: object
properties:
error:
type: string
details:
type: string
example:
error: unsupported
details: No tracing store has been configured
parameters:
- name: type
in: query
required: false
schema:
type: string
- name: page
in: query
required: false
schema:
type: number
- name: limit
in: query
required: false
schema:
type: number
- name: values
in: query
required: false
schema:
type: number
/logs:
get:
summary: Quere Log Files
responses:
"200":
description: OK
content:
application/json:
schema:
type: object
properties:
data:
type: object
properties:
items:
type: array
items:
type: object
properties:
timestamp:
type: string
level:
type: string
event:
type: string
event_id:
type: string
details:
type: string
total:
type: number
example:
data:
items:
- timestamp: "2025-01-05T14:06:29Z"
level: TRACE
event: HTTP request body
event_id: http.request-body
details:
listenerId = "http", localPort = 1443, remoteIp = ::1,
remotePort = 57223, contents = "", size = 0
- timestamp: "2025-01-05T14:06:29Z"
level: TRACE
event: Write batch operation
event_id: store.data-write
details: elapsed = 0ms, total = 2
- timestamp: "2025-01-05T14:06:29Z"
level: TRACE
event: Expression evaluation result
event_id: eval.result
details:
listenerId = "http", localPort = 1443, remoteIp = ::1,
remotePort = 57223, id = "server.http.allowed-endpoint", result
= "Integer(200)"
- timestamp: "2025-01-05T14:06:29Z"
level: DEBUG
event: HTTP request URL
event_id: http.request-url
details:
listenerId = "http", localPort = 1443, remoteIp = ::1,
remotePort = 57223, url = "/api/logs?page=1&limit=50&"
- timestamp: "2025-01-05T14:06:23Z"
level: TRACE
event: HTTP response body
event_id: http.response-body
details:
listenerId = "http", localPort = 1443, remoteIp = ::1,
remotePort = 57223, contents = "{"error":"unsupported","details":"No
tracing store has been configured"}", code = 200, size = 72
- timestamp: "2025-01-05T14:06:23Z"
level: DEBUG
event: Management operation not supported
event_id: manage.not-supported
details:
listenerId = "http", localPort = 1443, remoteIp = ::1,
remotePort = 57223, details = No tracing store has been configured
- timestamp: "2025-01-05T14:06:23Z"
level: TRACE
event: HTTP request body
event_id: http.request-body
details:
listenerId = "http", localPort = 1443, remoteIp = ::1,
remotePort = 57223, contents = "", size = 0
- timestamp: "2025-01-05T14:06:23Z"
level: TRACE
event: Write batch operation
event_id: store.data-write
details: elapsed = 0ms, total = 2
- timestamp: "2025-01-05T14:06:23Z"
level: TRACE
event: Expression evaluation result
event_id: eval.result
details:
listenerId = "http", localPort = 1443, remoteIp = ::1,
remotePort = 57223, id = "server.http.allowed-endpoint", result
= "Integer(200)"
- timestamp: "2025-01-05T14:06:23Z"
level: DEBUG
event: HTTP request URL
event_id: http.request-url
details:
listenerId = "http", localPort = 1443, remoteIp = ::1,
remotePort = 57223, url = "/api/telemetry/traces?page=1&type=delivery.attempt-start&limit=10&values=1&"
total: 100
parameters:
- name: page
in: query
required: false
schema:
type: number
- name: limit
in: query
required: false
schema:
type: number
/spam-filter/train/spam:
post:
summary: Train Spam Filter as Spam
responses:
"200":
description: OK
content:
application/json:
schema:
type: object
properties:
data:
type: object
nullable: true
example:
data:
requestBody:
content:
application/x-www-form-urlencoded:
schema:
type: object
properties:
? "From: john@example.org\nTo: list@example.org\nSubject: Testing,
please ignore\nContent-Type: text/plain; charset"
: type: string
example:
? "From: john@example.org\nTo: list@example.org\nSubject: Testing, please
ignore\nContent-Type: text/plain; charset"
: "\"utf-8\"\nContent-Transfer-Encoding: 8bit\n\nTesting 1, 2, 3\n"
/spam-filter/train/ham:
post:
summary: Train Spam Filter as Ham
responses:
"200":
description: OK
content:
application/json:
schema:
type: object
properties:
data:
type: object
nullable: true
example:
data:
requestBody:
content:
application/x-www-form-urlencoded:
schema:
type: object
properties:
? "From: john@example.org\nTo: list@example.org\nSubject: Testing,
please ignore\nContent-Type: text/plain; charset"
: type: string
example:
? "From: john@example.org\nTo: list@example.org\nSubject: Testing, please
ignore\nContent-Type: text/plain; charset"
: "\"utf-8\"\nContent-Transfer-Encoding: 8bit\n\nTesting 1, 2, 3\n"
/spam-filter/train/spam/{account_id}:
post:
summary: Train Account's Spam Filter as Spam
responses:
"200":
description: OK
content:
application/json:
schema:
type: object
properties:
data:
type: object
nullable: true
example:
data:
parameters:
- name: account_id
in: path
required: true
schema:
type: string
requestBody:
content:
application/x-www-form-urlencoded:
schema:
type: object
properties:
? "From: john@example.org\nTo: list@example.org\nSubject: Testing,
please ignore\nContent-Type: text/plain; charset"
: type: string
example:
? "From: john@example.org\nTo: list@example.org\nSubject: Testing, please
ignore\nContent-Type: text/plain; charset"
: "\"utf-8\"\nContent-Transfer-Encoding: 8bit\n\nTesting 1, 2, 3\n"
/spam-filter/train/ham/{account_id}:
post:
summary: Train Account's Spam Filter as Ham
responses:
"200":
description: OK
content:
application/json:
schema:
type: object
properties:
data:
type: object
nullable: true
example:
data:
parameters:
- name: account_id
in: path
required: true
schema:
type: string
requestBody:
content:
application/x-www-form-urlencoded:
schema:
type: object
properties:
? "From: john@example.org\nTo: list@example.org\nSubject: Testing,
please ignore\nContent-Type: text/plain; charset"
: type: string
example:
? "From: john@example.org\nTo: list@example.org\nSubject: Testing, please
ignore\nContent-Type: text/plain; charset"
: "\"utf-8\"\nContent-Transfer-Encoding: 8bit\n\nTesting 1, 2, 3\n"
/spam-filter/classify:
post:
summary: Test Spam Filter Classification
responses:
"200":
description: OK
content:
application/json:
schema:
type: object
properties:
data:
type: object
properties:
score:
type: number
tags:
type: object
properties:
FROM_NO_DN:
type: object
properties:
action:
type: string
value:
type: number
SOURCE_ASN_15169:
type: object
properties:
action:
type: string
value:
type: number
SOURCE_COUNTRY_US:
type: object
properties:
action:
type: string
value:
type: number
MISSING_DATE:
type: object
properties:
action:
type: string
value:
type: number
FROMHOST_NORES_A_OR_MX:
type: object
properties:
action:
type: string
value:
type: number
MISSING_MIME_VERSION:
type: object
properties:
action:
type: string
value:
type: number
FORGED_SENDER:
type: object
properties:
action:
type: string
value:
type: number
SPF_NA:
type: object
properties:
action:
type: string
value:
type: number
X_HDR_TO:
type: object
properties:
action:
type: string
value:
type: number
HELO_IPREV_MISMATCH:
type: object
properties:
action:
type: string
value:
type: number
X_HDR_CONTENT_TYPE:
type: object
properties:
action:
type: string
value:
type: number
AUTH_NA:
type: object
properties:
action:
type: string
value:
type: number
FORGED_RECIPIENTS:
type: object
properties:
action:
type: string
value:
type: number
RBL_SENDERSCORE_REPUT_BLOCKED:
type: object
properties:
action:
type: string
value:
type: number
RCVD_COUNT_ZERO:
type: object
properties:
action:
type: string
value:
type: number
X_HDR_SUBJECT:
type: object
properties:
action:
type: string
value:
type: number
X_HDR_FROM:
type: object
properties:
action:
type: string
value:
type: number
RCPT_COUNT_ONE:
type: object
properties:
action:
type: string
value:
type: number
MISSING_MID:
type: object
properties:
action:
type: string
value:
type: number
TO_DOM_EQ_FROM_DOM:
type: object
properties:
action:
type: string
value:
type: number
ARC_NA:
type: object
properties:
action:
type: string
value:
type: number
RCVD_TLS_LAST:
type: object
properties:
action:
type: string
value:
type: number
X_HDR_CONTENT_TRANSFER_ENCODING:
type: object
properties:
action:
type: string
value:
type: number
HELO_NORES_A_OR_MX:
type: object
properties:
action:
type: string
value:
type: number
TO_DN_NONE:
type: object
properties:
action:
type: string
value:
type: number
FROM_NEQ_ENV_FROM:
type: object
properties:
action:
type: string
value:
type: number
DMARC_NA:
type: object
properties:
action:
type: string
value:
type: number
SINGLE_SHORT_PART:
type: object
properties:
action:
type: string
value:
type: number
DKIM_NA:
type: object
properties:
action:
type: string
value:
type: number
disposition:
type: object
properties:
action:
type: string
value:
type: string
example:
data:
score: 12.7
tags:
FROM_NO_DN:
action: allow
value: 0.0
SOURCE_ASN_15169:
action: allow
value: 0.0
SOURCE_COUNTRY_US:
action: allow
value: 0.0
MISSING_DATE:
action: allow
value: 1.0
FROMHOST_NORES_A_OR_MX:
action: allow
value: 1.5
MISSING_MIME_VERSION:
action: allow
value: 2.0
FORGED_SENDER:
action: allow
value: 0.3
SPF_NA:
action: allow
value: 0.0
X_HDR_TO:
action: allow
value: 0.0
HELO_IPREV_MISMATCH:
action: allow
value: 1.0
X_HDR_CONTENT_TYPE:
action: allow
value: 0.0
AUTH_NA:
action: allow
value: 1.0
FORGED_RECIPIENTS:
action: allow
value: 2.0
RBL_SENDERSCORE_REPUT_BLOCKED:
action: allow
value: 0.0
RCVD_COUNT_ZERO:
action: allow
value: 0.1
X_HDR_SUBJECT:
action: allow
value: 0.0
X_HDR_FROM:
action: allow
value: 0.0
RCPT_COUNT_ONE:
action: allow
value: 0.0
MISSING_MID:
action: allow
value: 2.5
TO_DOM_EQ_FROM_DOM:
action: allow
value: 0.0
ARC_NA:
action: allow
value: 0.0
RCVD_TLS_LAST:
action: allow
value: 0.0
X_HDR_CONTENT_TRANSFER_ENCODING:
action: allow
value: 0.0
HELO_NORES_A_OR_MX:
action: allow
value: 0.3
TO_DN_NONE:
action: allow
value: 0.0
FROM_NEQ_ENV_FROM:
action: allow
value: 0.0
DMARC_NA:
action: allow
value: 1.0
SINGLE_SHORT_PART:
action: allow
value: 0.0
DKIM_NA:
action: allow
value: 0.0
disposition:
action: allow
value:
"X-Spam-Result: ARC_NA (0.00),\r\n\tDKIM_NA (0.00),\r\n
\tFROM_NEQ_ENV_FROM (0.00),\r\n\tFROM_NO_DN (0.00),\r\n\tRBL_SENDERSCORE_REPUT_BLOCKED
(0.00),\r\n\tRCPT_COUNT_ONE (0.00),\r\n\tRCVD_TLS_LAST (0.00),\r
\n\tSINGLE_SHORT_PART (0.00),\r\n\tSPF_NA (0.00),\r\n\tTO_DN_NONE
(0.00),\r\n\tTO_DOM_EQ_FROM_DOM (0.00),\r\n\tRCVD_COUNT_ZERO
(0.10),\r\n\tFORGED_SENDER (0.30),\r\n\tHELO_NORES_A_OR_MX (0.30),\r
\n\tAUTH_NA (1.00),\r\n\tDMARC_NA (1.00),\r\n\tHELO_IPREV_MISMATCH
(1.00),\r\n\tMISSING_DATE (1.00),\r\n\tFROMHOST_NORES_A_OR_MX
(1.50),\r\n\tFORGED_RECIPIENTS (2.00),\r\n\tMISSING_MIME_VERSION
(2.00),\r\n\tMISSING_MID (2.50)\r\nX-Spam-Status: Yes, score=12.70\r\
\n"
requestBody:
content:
application/json:
schema:
type: object
properties:
message:
type: string
remoteIp:
type: string
ehloDomain:
type: string
authenticatedAs:
type: object
nullable: true
isTls:
type: boolean
envFrom:
type: string
envFromFlags:
type: number
envRcptTo:
type: array
items:
type: string
example:
message:
"From: john@example.org\nTo: list@example.org\nSubject: Testing,
please ignore\nContent-Type: text/plain; charset=\"utf-8\"\nContent-Transfer-Encoding:
8bit\n\nTesting 1, 2, 3\n"
remoteIp: 8.8.8.8
ehloDomain: foo.org
authenticatedAs:
isTls: true
envFrom: bill@foo.org
envFromFlags: 0
envRcptTo:
- john@example.org
/troubleshoot/token:
get:
summary: Obtain a Troubleshooting Token
responses:
"200":
description: OK
content:
application/json:
schema:
type: object
properties:
data:
type: string
example:
data: +bS1rCUcrjoEtl9f7Vz1P6daqVs4nywxa56bHltPIASijRFrj1JrwvHxJCWphPKs/////w8E8p21+AKunrX4AndlYg==
/troubleshoot/delivery/{recipient}:
get:
summary: Run Delivery Troubleshooting
responses:
"200":
description: OK
content: {}
parameters:
- name: recipient
in: path
required: true
schema:
type: string
- name: token
in: query
required: false
schema:
type: string
/troubleshoot/dmarc:
post:
summary: Run DMARC Troubleshooting
responses:
"200":
description: OK
content:
application/json:
schema:
type: object
properties:
data:
type: object
properties:
spfEhloDomain:
type: string
spfEhloResult:
type: object
properties:
type:
type: string
spfMailFromDomain:
type: string
spfMailFromResult:
type: object
properties:
type:
type: string
details:
type: object
nullable: true
ipRevResult:
type: object
properties:
type:
type: string
ipRevPtr:
type: array
items:
type: string
dkimResults:
type: array
items: {}
dkimPass:
type: boolean
arcResult:
type: object
properties:
type:
type: string
dmarcResult:
type: object
properties:
type:
type: string
dmarcPass:
type: boolean
dmarcPolicy:
type: string
elapsed:
type: number
example:
data:
spfEhloDomain: mx.google.com
spfEhloResult:
type: none
spfMailFromDomain: google.com
spfMailFromResult:
type: softFail
details:
ipRevResult:
type: pass
ipRevPtr:
- dns.google.
dkimResults: []
dkimPass: false
arcResult:
type: none
dmarcResult:
type: none
dmarcPass: false
dmarcPolicy: reject
elapsed: 200
requestBody:
content:
application/json:
schema:
type: object
properties:
remoteIp:
type: string
ehloDomain:
type: string
mailFrom:
type: string
body:
type: string
example:
remoteIp: 8.8.8.8
ehloDomain: mx.google.com
mailFrom: john@google.com
body:
"From: john@example.org\nTo: list@example.org\nSubject: Testing,
please ignore\nContent-Type: text/plain; charset=\"utf-8\"\nContent-Transfer-Encoding:
8bit\n\nTesting 1, 2, 3\n"
/reload:
get:
summary: Reload Settings
responses:
"200":
description: OK
content:
application/json:
schema:
type: object
properties:
data:
type: object
properties:
warnings:
type: object
properties: {}
errors:
type: object
properties: {}
example:
data:
warnings: {}
errors: {}
parameters:
- name: dry-run
in: query
required: false
schema:
type: string
/update/spam-filter:
get:
summary: Update Spam Filter
responses:
"200":
description: OK
content:
application/json:
schema:
type: object
properties:
data:
type: object
nullable: true
example:
data:
/update/webadmin:
get:
summary: Update WebAdmin
responses:
"200":
description: OK
content:
application/json:
schema:
type: object
properties:
data:
type: object
nullable: true
example:
data:
/store/reindex:
get:
summary: Request FTS Reindex
responses:
"200":
description: OK
content:
application/json:
schema:
type: object
properties:
data:
type: object
nullable: true
example:
data:
/store/purge/in-memory/default/bayes-global:
get:
summary: Delete Global Bayes Model
responses:
"200":
description: OK
content:
application/json:
schema:
type: object
properties:
data:
type: object
nullable: true
example:
data:
/settings/keys:
get:
summary: List Settings by Key
responses:
"200":
description: OK
content:
application/json:
schema:
type: object
properties:
data:
type: object
properties:
lookup.default.hostname:
type: string
example:
data:
lookup.default.hostname: mx.fr.email
parameters:
- name: prefixes
in: query
required: false
schema:
type: string
- name: keys
in: query
required: false
schema:
type: string
/settings/group:
get:
summary: List Settings by Group
responses:
"200":
description: OK
content:
application/json:
schema:
type: object
properties:
data:
type: object
properties:
total:
type: number
items:
type: array
items:
type: object
properties:
_id:
type: string
bind:
type: string
protocol:
type: string
example:
data:
total: 11
items:
- _id: http
bind: "[::]:1443"
protocol: http
- bind: "[::]:443"
_id: https
protocol: http
tls.implicit: "true"
- protocol: imap
bind: "[::]:143"
_id: imap
- bind: "[::]:1143"
tls.implicit: "false"
_id: imapnotls
protocol: imap
proxy.override: "false"
tls.override: "false"
tls.enable: "false"
socket.override: "false"
- bind: "[::]:993"
tls.implicit: "true"
protocol: imap
_id: imaptls
- bind: "[::]:110"
protocol: pop3
_id: pop3
- tls.implicit: "true"
_id: pop3s
protocol: pop3
bind: "[::]:995"
- protocol: managesieve
_id: sieve
bind: "[::]:4190"
- bind: "[::]:25"
_id: smtp
protocol: smtp
- _id: submission
bind: "[::]:587"
protocol: smtp
parameters:
- name: limit
in: query
required: false
schema:
type: number
- name: page
in: query
required: false
schema:
type: number
- name: suffix
in: query
required: false
schema:
type: string
- name: prefix
in: query
required: false
schema:
type: string
/settings/list:
get:
summary: List Settings
responses:
"200":
description: OK
content:
application/json:
schema:
type: object
properties:
data:
type: object
properties:
total:
type: number
items:
type: object
properties:
enable:
type: string
format:
type: string
limits.entries:
type: string
limits.entry-size:
type: string
limits.size:
type: string
refresh:
type: string
retry:
type: string
timeout:
type: string
url:
type: string
example:
data:
total: 9
items:
enable: "true"
format: list
limits.entries: "100000"
limits.entry-size: "512"
limits.size: "104857600"
refresh: 12h
retry: 1h
timeout: 30s
url: https://openphish.com/feed.txt
parameters:
- name: prefix
in: query
required: false
schema:
type: string
/settings:
post:
summary: Update Settings
responses:
"200":
description: OK
content:
application/json:
schema:
type: object
properties:
data:
type: object
nullable: true
example:
data:
requestBody:
content:
application/json:
schema:
type: array
items:
type: object
properties:
type:
type: string
prefix:
type: string
example:
- type: clear
prefix: spam-filter.rule.stwt_arc_signed.
/account/crypto:
get:
summary: Obtain Encryption-at-Rest Settings
responses:
"200":
description: OK
content:
application/json:
schema:
type: object
properties:
data:
type: object
properties:
type:
type: string
example:
data:
type: disabled
post:
summary: Update Encryption-at-Rest Settings
responses:
"200":
description: OK
content:
application/json:
schema:
type: object
properties:
data:
type: number
example:
data: 1
requestBody:
content:
application/json:
schema:
type: object
properties:
type:
type: string
algo:
type: string
certs:
type: string
example:
type: pGP
algo: Aes256
certs:
"-----BEGIN PGP PUBLIC KEY BLOCK-----\n\nxsFNBGTGHwkBEADRB5EEtfsnUwgF2ZRg6h1fp2E8LNhv4lb9AWersI8KNFoWM6qx\n
Bk/MfEpgILSPdW3g7PWHOxPV/hxjtStFHfbU/Ye5VvfbkU49faIPiw1V3MQJJ171\n
cN6kgMnABfdixNiutDkHP4f34ABrEqexX2myOP+btxL24gI/N9UpOD5PiKTyKR7i\n
GwNpi+O022rs/KvjlWR7iSJ4vk7bGFfTNHvWI6dZworey1tZoTIZ0CgvgMeB/F1q\n
OOa0FvrJdNYR227RpHmICqFqTptNZ2EfdkJ6QUXW7bZ9dWgL36ds9QPJOGcG3c5i\n
JebeX5YdJnniBefiWjfZElcqh/N6SqVuEwoTLyMCnMZ6gjNMn6tddwPH24kavZhT\n
p6+vhTHmyq8XBqK/XEt9r+clSfg2hi5s7GO7hQV+W26xRjX7sQJY41PfzkgYJ0BM\n
6+w09X1ZO/iMjEp44t2rd3xSudwGYhlbazXbdB+OJaa3RtyjOAeFgY8OyNlODx3V\n
xXLtF+104HGSL7nkpBsu6LLighSgEEF2Vok43grr0omyb1NPhWoAZhM8sT5iv5gW\n
fKvB1O13c+hDc/iGTAvcrtdLLnF2Cs+6HD7r7zPPM4L6DrD1+oQt510H/oOEE5NZ\n
wIS9CmBf0txqwk7n1U5V95lonaCK9nfoKeQ1fKl/tu01dCeERRbMXG2nCQARAQAB\n
zRtKb2huIERvZSA8am9obkBleGFtcGxlLm9yZz7CwYcEEwEIADEWIQQWwx1eM+Aa\n
o8okGzL45grMTSggxQUCZMYfCQIbAwQLCQgHBRUICQoLBRYCAwEAAAoJEPjmCsxN\n
KCDFWP4QAI3eS5nPxmU0AC9/h8jeKNgjgpENroNQZKeWZQ8x4PfncDRkcbsJfT7Y\n
IVZl4zw6gFKY5EoB1s1KkYJxPgYsqicmKNiR7Tnzabb3mzomU48FKaIyVCBzFUnJ\n
YMroL/rm7QhoW2WWLvT+CPCPway/tA3By8Be/YOjhavJ8mf1W3rPzt87/4Vo6erf\n
yzL0lN+FQmmhKfT4j42jF4SMSyyC2yzvfC7PT49u+KUKQm/LpQsfKHpwXZ/VI6+X\n
GtZjTqsc+uglJYRo69oosImLzieA/ST1ltjmUutZQOSvlQFpDUEFrMej8XZ0qsrf\n
0gP2iwxyl0vkhV8c6wO6CacDHPivvQEHed9H1PNGn3DBfKb7Mq/jado2DapRtJg3\n
2OH0F0HTvQ0uNKl30xMUcwGQB0cKOlaFtksZT1LsosQPhtPLpFy1TuWaXOInpQLq\n
JmNVcTbydOsCKq0mb6bgGcvhElC1q39tclKP3rOEDOnJ8hE6wYNaMGrt6WSKr3Tt\n
h52M6KwTXOuMAecMvpDBSS3UFEVQ+T5puzInDTkjINxmj23ip+swA1x3HH2IgNrO\n
VJ7O20oEf0+qC47R5rTRUxrvh/U0U3DRE5xt2J2T3xetFDT2mnQv0jcyMg/UlXXv\n
GpGVfwNkvN0Cxmb1tFiBNLKCcPVizxq4MLrwx+MVfQBaRCwjJrUszsFNBGTGHwoB\n
EACr5lA+j5pH0Er6Q76btbS4q9JgNjDNrjKJwX9brdBY1oXIUeBqCW9ekoqDTFpn\n
xA5EFGJvPO++/0ZCa+zXE4IAcXS9+I9HVBouenPYBLETnXK0Phws+OCLoe0cAIvG\n
e9Xo9VrHcGXCs9tJruVSAW3NF04YejHmnHNfEuD8mbaUdxVn5zc23w/2gLaY/ABL\n
ZfNV8XZw0jBVBm3YXS3Ob3uIO+RvsNqBgnhGYN/C51QI9hdxXWUDlD1vdRacXmcI\n
LDCYC3w6u8caxL0ktXTS4zwN+hEu7jHxBNiKcovCeIF5VZ5NcPpp6+6Y+vNdmmXw\n
+lWNwAzj3ah6iu+y25LKSsz+7IkCh5liOwwYohO+YI7SjtTD+gL9HiHYAIO+PtBh\n
7GudmUwFoARu/q54hE4ThpzkeOzJzPqGkM/CzmwdKKM3u81ze+72ptJOqVKbFEsQ\n
3+RURrIAfyYyeJj4VVCfHNzrRRVpARZc9hJm1AXefxPnDN9dxbikjQgbg5UxrKaJ\n
cjVU+go5CH5lg2D1LRGfKqTJtfiWFPjtztNgMp/SeslkhhFXsyJ0RJDcU8VfRBrO\n
DBnZvPnZi4nLaWCL1LdHA8Y9EJgSwVOsfdRqL/Xk9qxqgl5R8m8lsNKZN2EYkfMN\n
4Vd+/8UBbmibHYoGIQi7UlNSPthc0XQcRzFen+3H4sg5kQARAQABwsF2BBgBCAAg\n
FiEEFsMdXjPgGqPKJBsy+OYKzE0oIMUFAmTGHwsCGwwACgkQ+OYKzE0oIMXn4hAA\n
lUWeF7tDdyENsOYyhsbtLIuLipYe6orHFY5m68NNOoLWwqEeTvutJgFeDT4WxYi0\n
PJaNQYFPyGVyg7N0hCx5cGwajdnwGpb5zpSNyvG2Yes9I1O/u7+FFrbSwOuo61t1\n
scGa8YlgTKoyGc9cwxl5U8krrlEwXTWQ/qF1Gq2wHG23wm1D2d2PXFDRvw3gPxJn\n
yWkrx5k26ru1kguM7XFVyRi7B+uG4vdvMlxMBXM3jpH1CJRr82VvzYPv7f05Z5To\n
C7XDqHpWKx3+AQvh/ZsSBpBhzK8qaixysMwnawe05rOPydWvsLlnMCGManKVnq9Y\n
Wek1P2dwYT9zuroBR5nmrECY+xVWk7vhsDasKsYlQ/LdDyzSL7qh0Vq3DjcoHxLI\n
uL7qQ3O0YRcKGfmQibpKdDzvIqA+48Nfh2nDnTxvfuwOxb41zdLTZQftaSXc0Xwd\n
HgquBAFbRDr5TyWlUUc8iACowKkk01pEPc8coxPCp6F/hz6kgmebRevzs7sxwrS7\n
aUWycSls783JC7WO267DRD30FNx+9S7SY4ECzhDGjLdne6wIoib1L9SFkk1AAKb3\n
m2+6BB/HxCXtMqi95pFeCjV99bp+PBqoifx9SlFYZq9qcGDr/jyrdG8V2Wf/HF4n\n
K8RIPxB+daAPMLTpj4WBhNquSE6mRQvABEf0GPi2eLA=\n=0TDv\n-----END PGP
PUBLIC KEY BLOCK-----\n\n\n"
/account/auth:
get:
summary: Obtain Account Authentication Settings
responses:
"200":
description: OK
content:
application/json:
schema:
type: object
properties:
data:
type: object
properties:
otpEnabled:
type: boolean
appPasswords:
type: array
items: {}
example:
data:
otpEnabled: false
appPasswords: []
post:
summary: Update Account Authentication Settings
responses:
"200":
description: OK
content:
application/json:
schema:
type: object
properties:
error:
type: string
details:
type: string
reason:
type: object
nullable: true
example:
error: other
details: Fallback administrator accounts do not support 2FA or AppPasswords
reason:
"401":
description: Unauthorized
content:
application/json:
schema:
type: object
properties:
type:
type: string
status:
type: number
title:
type: string
detail:
type: string
example:
type: about:blank
status: 401
title: Unauthorized
detail: You have to authenticate first.
requestBody:
content:
application/json:
schema:
type: array
items:
type: object
properties:
type:
type: string
name:
type: string
password:
type: string
example:
- type: addAppPassword
name: dGVzdCQyMDI1LTAxLTA1VDE0OjEyOjUxLjg0NyswMDowMA==
password: $6$4M/5LmG7b13r0cdE$6zb.i6wJ3pAQHA2MRHkKg0t8bgSYb2IeqiIU115t.NugwW6VXifE0VKI5n2BQUNwdeDMUzaX82TmhuVVgC0Gx1
/reload/:
get:
summary: Reload Settings
responses:
"200":
description: OK
content:
application/json:
schema:
type: object
properties:
data:
type: object
properties:
warnings:
type: object
properties: {}
errors:
type: object
properties: {}
example:
data:
warnings: {}
errors: {}
/queue/status/stop:
patch:
summary: Stop Queue Processing
responses:
"200":
description: OK
content:
application/json:
schema:
type: object
properties:
data:
type: boolean
example:
data: true
/queue/status/start:
patch:
summary: Resume Queue Processing
responses:
"200":
description: OK
content:
application/json:
schema:
type: object
properties:
data:
type: boolean
example:
data: false
/queue/messages/{message_id}:
get:
summary: Obtain Queued Message Details
responses:
"200":
description: OK
content:
application/json:
schema:
type: object
properties:
data:
type: object
properties:
id:
type: number
return_path:
type: string
domains:
type: array
items:
type: object
properties:
name:
type: string
status:
type: string
recipients:
type: array
items:
type: object
properties:
address:
type: string
status:
type: string
retry_num:
type: number
next_retry:
type: string
next_notify:
type: string
expires:
type: string
created:
type: string
size:
type: number
blob_hash:
type: string
example:
data:
id: 217700302698266624
return_path: pepe@pepe.com
domains:
- name: example.org
status: scheduled
recipients:
- address: john@example.org
status: scheduled
retry_num: 0
next_retry: "2025-01-05T14:33:15Z"
next_notify: "2025-01-06T14:33:15Z"
expires: "2025-01-10T14:33:15Z"
created: "2025-01-05T14:33:15Z"
size: 1451
blob_hash: ykrZ_KghvdG2AdjH4AZajkSvZvcsxP_oI2HEZvw-tS0
"404":
description: Not Found
content:
application/json:
schema:
type: object
properties:
type:
type: string
status:
type: number
title:
type: string
detail:
type: string
example:
type: about:blank
status: 404
title: Not Found
detail: The requested resource does not exist on this server.
parameters:
- name: message_id
in: path
required: true
schema:
type: string
patch:
summary: Reschedule Delivery of Queued Message
responses:
"200":
description: OK
content:
application/json:
schema:
type: object
properties:
data:
type: boolean
example:
data: true
parameters:
- name: message_id
in: path
required: true
schema:
type: string
delete:
summary: Cancel Delivery of Queued Message
responses:
"200":
description: OK
content:
application/json:
schema:
type: object
properties:
data:
type: boolean
example:
data: true
parameters:
- name: message_id
in: path
required: true
schema:
type: string
/store/blobs/{blob_id}:
get:
summary: Fetch Blob by ID
responses:
"200":
description: OK
content: {}
parameters:
- name: blob_id
in: path
required: true
schema:
type: string
- name: limit
in: query
required: false
schema:
type: number
/telemetry/trace/{trace_id}:
get:
summary: Obtain Trace Details
responses:
"200":
description: OK
content:
application/json:
schema:
type: object
properties:
data:
type: array
items:
type: object
properties:
text:
type: string
details:
type: string
createdAt:
type: string
type:
type: string
data:
type: object
properties:
listenerId:
type: string
localPort:
type: number
remoteIp:
type: string
remotePort:
type: number
example:
data:
- text: SMTP connection started
details: A new SMTP connection was started
createdAt: "2025-01-05T14:34:50Z"
type: smtp.connection-start
data:
listenerId: smtp
localPort: 25
remoteIp: ::1
remotePort: 57513
- text: SMTP EHLO command
details: The remote server sent an EHLO command
createdAt: "2025-01-05T14:34:50Z"
type: smtp.ehlo
data:
domain: test.eml
- text: SPF EHLO check failed
details: EHLO identity failed SPF check
createdAt: "2025-01-05T14:34:50Z"
type: smtp.spf-ehlo-fail
data:
domain: test.eml
result:
type: spf.none
text: No SPF record
details: No SPF record was found
data: {}
elapsed: 24
- text: IPREV check passed
details: Reverse IP check passed
createdAt: "2025-01-05T14:34:50Z"
type: smtp.iprev-pass
data:
domain: test.eml
result:
type: iprev.pass
text: IPREV check passed
details: The IPREV check has passed
data:
details:
- localhost.
elapsed: 0
- text: SPF From check failed
details: MAIL FROM identity failed SPF check
createdAt: "2025-01-05T14:34:50Z"
type: smtp.spf-from-fail
data:
domain: test.eml
from: pepe@pepe.com
result:
type: spf.none
text: No SPF record
details: No SPF record was found
data: {}
elapsed: 18
- text: SMTP MAIL FROM command
details: The remote client sent a MAIL FROM command
createdAt: "2025-01-05T14:34:50Z"
type: smtp.mail-from
data:
from: pepe@pepe.com
- text: SMTP RCPT TO command
details: The remote client sent an RCPT TO command
createdAt: "2025-01-05T14:34:50Z"
type: smtp.rcpt-to
data:
to: john@example.org
- text: DKIM verification failed
details: Failed to verify DKIM signature
createdAt: "2025-01-05T14:34:50Z"
type: smtp.dkim-fail
data:
strict: false
result: []
elapsed: 0
- text: ARC verification passed
details: Successful ARC verification
createdAt: "2025-01-05T14:34:50Z"
type: smtp.arc-pass
data:
strict: false
result:
type: dkim.none
text: No DKIM signature
details: No DKIM signature was found
data: {}
elapsed: 0
- text: DMARC check failed
details: Failed to verify DMARC policy
createdAt: "2025-01-05T14:34:50Z"
type: smtp.dmarc-fail
data:
strict: false
domain: example.org
policy: reject
result:
type: dmarc.none
text: No DMARC record
details: No DMARC record was found
data: {}
elapsed: 0
parameters:
- name: trace_id
in: path
required: true
schema:
type: string
/telemetry/live/tracing-token:
get:
summary: Request a Tracing Token
responses:
"200":
description: OK
content:
application/json:
schema:
type: object
properties:
data:
type: string
example:
data: VLxkixOwgDF8Frj0wi8kPhx3SpzKqtsDvbo25wgKw2tBIz/O8La0dwioQw9pN11c/////w8Ctau1+ALxq7X4AndlYg==
/telemetry/traces/live:
get:
summary: Start Live Tracing
responses:
"200":
description: OK
content: {}
parameters:
- name: filter
in: query
required: false
schema:
type: string
- name: token
in: query
required: false
schema:
type: string
/dns/records/{domain}:
get:
summary: Obtain DNS Records for Domain
responses:
"200":
description: OK
content:
application/json:
schema:
type: object
properties:
data:
type: array
items:
type: object
properties:
type:
type: string
name:
type: string
content:
type: string
example:
data:
- type: MX
name: example.org.
content: 10 mx.fr.email.
- type: CNAME
name: mail.example.org.
content: mx.fr.email.
- type: TXT
name: 202501e._domainkey.example.org.
content: v=DKIM1; k=ed25519; h=sha256; p=82LqzMGRHEBI2HGDogjojWGz+Crrv0TAi8pcaOBd1vw=
- type: TXT
name: 202501r._domainkey.example.org.
content: v=DKIM1; k=rsa; h=sha256;
p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAu1qtCbIlrZffIqm7gHqpihPUlxOq1zD6K3j1RO/enhkZRp5dEdCqcLbyFk5d+rqRsVIWwUZiU4HXHWqMTN1hlKojUlzmU1JYtlHRMwtM5vN4mzG4x1KA0i8ZHxkahE8ITsP+kPByDF9x0vAySHXpyErNXq3BeFyu/VW+6X+fmUW6x39PfWq7kQQTcwU0Ogo447oJfmAX9H4Z+/cD5WJVNiLgvLY6faVgoXm0mJJjRU5xoEStXoUcKwrwbl7G3K7JfxtmWsgEn97auV6v4he2LRRfTxbY9smkqUtcJs61E9iyyYroJv0iRda2pv71qg8e4wTb2sqBloZv/F2FZQhM+wIDAQAB
- type: TXT
name: example.org.
content: v=spf1 mx ra=postmaster -all
- type: SRV
name: _jmap._tcp.example.org.
content: 0 1 443 mx.fr.email.
- type: SRV
name: _imaps._tcp.example.org.
content: 0 1 993 mx.fr.email.
- type: SRV
name: _imap._tcp.example.org.
content: 0 1 143 mx.fr.email.
- type: SRV
name: _imap._tcp.example.org.
content: 0 1 1143 mx.fr.email.
- type: SRV
name: _pop3s._tcp.example.org.
content: 0 1 995 mx.fr.email.
parameters:
- name: domain
in: path
required: true
schema:
type: string
/store/purge/account/{account_id}:
get:
summary: Purge Account
responses:
"200":
description: OK
content:
application/json:
schema:
type: object
properties:
data:
type: object
nullable: true
example:
data:
parameters:
- name: account_id
in: path
required: true
schema:
type: string
/store/purge/in-memory/default/bayes-account/{account_id}:
get:
summary: Delete Bayes Model for Account
responses:
"200":
description: OK
content:
application/json:
schema:
type: object
properties:
data:
type: object
nullable: true
example:
data:
parameters:
- name: account_id
in: path
required: true
schema:
type: string
/store/undelete/{account_id}:
get:
summary: List Deleted Messages
responses:
"200":
description: OK
content:
application/json:
schema:
type: object
properties:
data:
type: object
properties:
items:
type: array
items: {}
total:
type: number
example:
data:
items: []
total: 0
parameters:
- name: account_id
in: path
required: true
schema:
type: string
- name: limit
in: query
required: false
schema:
type: number
- name: page
in: query
required: false
schema:
type: number
post:
summary: Undelete Messages
responses:
"200":
description: OK
content:
application/json:
schema:
type: object
properties:
data:
type: array
items:
type: object
properties:
type:
type: string
example:
data:
- type: success
parameters:
- name: account_id
in: path
required: true
schema:
type: string
requestBody:
content:
application/json:
schema:
type: array
items:
type: object
properties:
hash:
type: string
collection:
type: string
restoreTime:
type: string
cancelDeletion:
type: string
example:
- hash: 9pDYGrkDlLYuBNl062qhi0wStnDYyq4ZWalnj2vXbLY
collection: email
restoreTime: "2025-01-05T14:50:13Z"
cancelDeletion: "2025-02-04T14:50:13Z"
/queue/status:
get:
summary: Obtain Queue Status
responses:
"200":
description: OK
content:
application/json:
schema:
type: object
properties:
data:
type: boolean
example:
data: true
/store/purge/blob:
get:
summary: Purge Blob Store
responses:
"200":
description: OK
content:
application/json:
schema:
type: object
properties:
data:
type: object
nullable: true
example:
data:
/store/purge/data:
get:
summary: Purge Data Store
responses:
"200":
description: OK
content:
application/json:
schema:
type: object
properties:
data:
type: object
nullable: true
example:
data:
/store/purge/in-memory:
get:
summary: Purge In-Memory Store
responses:
"200":
description: OK
content:
application/json:
schema:
type: object
properties:
data:
type: object
nullable: true
example:
data:
/store/purge/account:
get:
summary: Purge All Accounts
responses:
"200":
description: OK
content:
application/json:
schema:
type: object
properties:
data:
type: object
nullable: true
example:
data:
/store/uids/{account_id}:
delete:
summary: Reset IMAP UIDs for Account
responses:
"200":
description: OK
content:
application/json:
schema:
type: object
properties:
data:
type: array
items:
type: number
example:
data:
- 0
- 0
parameters:
- name: account_id
in: path
required: true
schema:
type: string
================================================
FILE: crates/cli/Cargo.toml
================================================
[package]
name = "stalwart-cli"
description = "Stalwart Server CLI"
authors = ["Stalwart Labs LLC "]
license = "AGPL-3.0-only OR LicenseRef-SEL"
repository = "https://github.com/stalwartlabs/cli"
homepage = "https://github.com/stalwartlabs/cli"
version = "0.15.5"
edition = "2024"
readme = "README.md"
[dependencies]
jmap-client = { version = "0.3", features = ["async"] }
mail-parser = { version = "0.11", features = ["full_encoding", "serde"] }
reqwest = { version = "0.12", default-features = false, features = ["rustls-tls-webpki-roots", "http2"]}
tokio = { version = "1.47", features = ["full"] }
num_cpus = "1.13.1"
clap = { version = "4.1.6", features = ["derive"] }
prettytable-rs = "0.10.0"
rpassword = "7.0"
indicatif = "0.17.0"
console = { version = "0.15", default-features = false, features = ["ansi-parsing"] }
serde = { version = "1.0", features = ["derive"]}
serde_json = "1.0"
csv = "1.1"
form_urlencoded = "1.1.0"
human-size = "0.4.2"
futures = "0.3.28"
pwhash = "1.0.0"
rand = "0.9.0"
mail-auth = { version = "0.7.1" }
================================================
FILE: crates/cli/src/main.rs
================================================
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use std::{
collections::HashMap,
fmt::Display,
io::{BufRead, Write},
time::Duration,
};
use clap::Parser;
use console::style;
use jmap_client::client::Credentials;
use modules::{
UnwrapResult,
cli::{Cli, Client, Commands},
host, is_localhost,
};
use reqwest::{Method, StatusCode, header::AUTHORIZATION};
use serde::{Deserialize, Serialize, de::DeserializeOwned};
use crate::modules::OAuthResponse;
pub mod modules;
#[tokio::main]
async fn main() -> std::io::Result<()> {
let args = Cli::parse();
let url = args
.url
.or_else(|| std::env::var("URL").ok())
.map(|url| url.trim_end_matches('/').to_string())
.unwrap_or_else(|| {
eprintln!("No URL specified. Use --url or set the URL environment variable.");
std::process::exit(1);
});
let client = Client {
credentials: if let Some(credentials) = args.credentials {
parse_credentials(&credentials)
} else if let Ok(credentials) = std::env::var("CREDENTIALS") {
parse_credentials(&credentials)
} else if args.anonymous {
let credentials = "anonymous:".to_string();
parse_credentials(&credentials)
} else {
let credentials = rpassword::prompt_password(
"\nEnter administrator credentials or press [ENTER] to use OAuth: ",
)
.unwrap();
if !credentials.is_empty() {
parse_credentials(&credentials)
} else {
oauth(&url).await
}
},
timeout: args.timeout,
url,
};
match args.command {
Commands::Import(command) => {
command.exec(client).await;
}
Commands::Export(command) => {
command.exec(client).await;
}
Commands::Server(command) => command.exec(client).await,
/*Commands::Account(command) => command.exec(client).await,
Commands::Domain(command) => command.exec(client).await,
Commands::List(command) => command.exec(client).await,
Commands::Group(command) => command.exec(client).await,*/
Commands::Dkim(command) => command.exec(client).await,
Commands::Queue(command) => command.exec(client).await,
Commands::Report(command) => command.exec(client).await,
}
Ok(())
}
fn parse_credentials(credentials: &str) -> Credentials {
if let Some((account, secret)) = credentials.split_once(':') {
Credentials::basic(account, secret)
} else {
Credentials::basic("admin", credentials)
}
}
async fn oauth(url: &str) -> Credentials {
let metadata: HashMap = serde_json::from_slice(
&reqwest::Client::builder()
.danger_accept_invalid_certs(is_localhost(url))
.build()
.unwrap_or_default()
.get(format!("{}/.well-known/oauth-authorization-server", url))
.send()
.await
.unwrap_result("send OAuth GET request")
.bytes()
.await
.unwrap_result("fetch bytes"),
)
.unwrap_result("deserialize OAuth GET response");
let token_endpoint = metadata.property("token_endpoint");
let mut params: HashMap =
HashMap::from_iter([("client_id".to_string(), "Stalwart_CLI".to_string())]);
let response: HashMap = serde_json::from_slice(
&reqwest::Client::builder()
.danger_accept_invalid_certs(is_localhost(url))
.build()
.unwrap_or_default()
.post(metadata.property("device_authorization_endpoint"))
.form(¶ms)
.send()
.await
.unwrap_result("send OAuth POST request")
.bytes()
.await
.unwrap_result("fetch bytes"),
)
.unwrap_result("deserialize OAuth POST response");
params.insert(
"grant_type".to_string(),
"urn:ietf:params:oauth:grant-type:device_code".to_string(),
);
params.insert(
"device_code".to_string(),
response.property("device_code").to_string(),
);
print!(
"\nAuthenticate this request using code {} at {}. Please ENTER when done.",
style(response.property("user_code")).bold(),
style(response.property("verification_uri")).bold().dim()
);
std::io::stdout().flush().unwrap();
std::io::stdin().lock().lines().next();
let mut response: HashMap = serde_json::from_slice(
&reqwest::Client::builder()
.danger_accept_invalid_certs(is_localhost(url))
.build()
.unwrap_or_default()
.post(token_endpoint)
.form(¶ms)
.send()
.await
.unwrap_result("send OAuth POST request")
.bytes()
.await
.unwrap_result("fetch bytes"),
)
.unwrap_result("deserialize OAuth POST response");
if let Some(serde_json::Value::String(access_token)) = response.remove("access_token") {
Credentials::Bearer(access_token)
} else {
eprintln!(
"OAuth failed with code {}.",
response
.get("error")
.and_then(|s| s.as_str())
.unwrap_or("")
);
std::process::exit(1);
}
}
#[derive(Deserialize)]
#[serde(untagged)]
pub enum Response {
Error(ManagementApiError),
Data { data: T },
}
#[derive(Deserialize)]
#[serde(tag = "error")]
#[serde(rename_all = "camelCase")]
pub enum ManagementApiError {
FieldAlreadyExists { field: String, value: String },
FieldMissing { field: String },
NotFound { item: String },
Unsupported { details: String },
AssertFailed,
Other { details: String },
}
impl Client {
pub async fn into_jmap_client(self) -> jmap_client::client::Client {
jmap_client::client::Client::new()
.credentials(self.credentials)
.accept_invalid_certs(is_localhost(&self.url))
.follow_redirects([host(&self.url).expect("Invalid host").to_owned()])
.timeout(Duration::from_secs(self.timeout.unwrap_or(60)))
.connect(&self.url)
.await
.unwrap_or_else(|err| {
eprintln!("Failed to connect to JMAP server {}: {}.", &self.url, err);
std::process::exit(1);
})
}
pub async fn http_request(
&self,
method: Method,
url: &str,
body: Option,
) -> R {
self.try_http_request(method, url, body)
.await
.unwrap_or_else(|| {
eprintln!("Request failed: No data returned.");
std::process::exit(1);
})
}
pub async fn try_http_request(
&self,
method: Method,
url: &str,
body: Option,
) -> Option {
let url = format!(
"{}{}{}",
self.url,
if !self.url.ends_with('/') && !url.starts_with('/') {
"/"
} else {
""
},
url
);
let mut request = reqwest::Client::builder()
.danger_accept_invalid_certs(is_localhost(&url))
.timeout(Duration::from_secs(self.timeout.unwrap_or(60)))
.build()
.unwrap_or_default()
.request(method, url)
.header(
AUTHORIZATION,
match &self.credentials {
Credentials::Basic(s) => format!("Basic {s}"),
Credentials::Bearer(s) => format!("Bearer {s}"),
},
);
if let Some(body) = body {
request = request.body(serde_json::to_string(&body).unwrap_result("serialize body"));
}
let response = request.send().await.unwrap_result("send HTTP request");
match response.status() {
StatusCode::OK => (),
StatusCode::NOT_FOUND => {
return None;
}
StatusCode::UNAUTHORIZED => {
eprintln!(
"Authentication failed. Make sure the credentials are correct and that the account has administrator rights."
);
std::process::exit(1);
}
_ => {
eprintln!(
"Request failed: {}",
response.text().await.unwrap_result("fetch text")
);
std::process::exit(1);
}
}
let bytes = response.bytes().await.unwrap_result("fetch bytes");
match serde_json::from_slice::>(&bytes).unwrap_result(&format!(
"deserialize response {}",
String::from_utf8_lossy(bytes.as_ref())
)) {
Response::Data { data } => Some(data),
Response::Error(error) => {
eprintln!("Request failed: {error})");
std::process::exit(1);
}
}
}
}
impl Display for ManagementApiError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ManagementApiError::FieldAlreadyExists { field, value } => {
write!(f, "Field {} already exists with value {}.", field, value)
}
ManagementApiError::FieldMissing { field } => {
write!(f, "Field {} is missing.", field)
}
ManagementApiError::NotFound { item } => {
write!(f, "{} not found.", item)
}
ManagementApiError::Unsupported { details } => {
write!(f, "Unsupported: {}", details)
}
ManagementApiError::AssertFailed => {
write!(f, "Assertion failed.")
}
ManagementApiError::Other { details } => {
write!(f, "{}", details)
}
}
}
}
================================================
FILE: crates/cli/src/modules/account.rs
================================================
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use std::fmt::Display;
use prettytable::{Attr, Cell, Row, Table};
use pwhash::sha512_crypt;
use reqwest::Method;
use serde_json::Value;
use super::{
Principal, PrincipalField, PrincipalUpdate, PrincipalValue, Type,
cli::{AccountCommands, Client},
};
impl AccountCommands {
pub async fn exec(self, client: Client) {
match self {
AccountCommands::Create {
name,
password,
description,
quota,
is_admin,
addresses,
member_of,
} => {
let principal = Principal {
typ: if is_admin.unwrap_or_default() {
Type::Superuser
} else {
Type::Individual
}
.into(),
quota,
name: name.clone().into(),
secrets: vec![sha512_crypt::hash(password).unwrap()],
emails: addresses.unwrap_or_default(),
member_of: member_of.unwrap_or_default(),
description,
..Default::default()
};
let account_id = client
.http_request::(Method::POST, "/api/principal", Some(principal))
.await;
eprintln!("Successfully created account {name:?} with id {account_id}.");
}
AccountCommands::Update {
name,
new_name,
password,
description,
quota,
is_admin,
addresses,
member_of,
} => {
let mut changes = Vec::new();
if let Some(new_name) = new_name {
changes.push(PrincipalUpdate::set(
PrincipalField::Name,
PrincipalValue::String(new_name),
));
}
if let Some(password) = password {
changes.push(PrincipalUpdate::add_item(
PrincipalField::Secrets,
PrincipalValue::String(sha512_crypt::hash(password).unwrap()),
));
}
if let Some(description) = description {
changes.push(PrincipalUpdate::set(
PrincipalField::Description,
PrincipalValue::String(description),
));
}
if let Some(quota) = quota {
changes.push(PrincipalUpdate::set(
PrincipalField::Quota,
PrincipalValue::Integer(quota),
));
}
if let Some(is_admin) = is_admin {
changes.push(PrincipalUpdate::set(
PrincipalField::Type,
PrincipalValue::String(
if is_admin {
Type::Superuser
} else {
Type::Individual
}
.to_string()
.to_ascii_lowercase(),
),
));
}
if let Some(addresses) = addresses {
changes.push(PrincipalUpdate::set(
PrincipalField::Emails,
PrincipalValue::StringList(addresses),
));
}
if let Some(member_of) = member_of {
changes.push(PrincipalUpdate::set(
PrincipalField::MemberOf,
PrincipalValue::StringList(member_of),
));
}
if !changes.is_empty() {
client
.http_request::(
Method::PATCH,
&format!("/api/principal/{name}"),
Some(changes),
)
.await;
eprintln!("Successfully updated account {name:?}.");
} else {
eprintln!("No changes to apply.");
}
}
AccountCommands::AddEmail { name, addresses } => {
client
.http_request::(
Method::PATCH,
&format!("/api/principal/{name}"),
Some(
addresses
.into_iter()
.map(|address| {
PrincipalUpdate::add_item(
PrincipalField::Emails,
PrincipalValue::String(address),
)
})
.collect::>(),
),
)
.await;
eprintln!("Successfully updated account {name:?}.");
}
AccountCommands::RemoveEmail { name, addresses } => {
client
.http_request::(
Method::PATCH,
&format!("/api/principal/{name}"),
Some(
addresses
.into_iter()
.map(|address| {
PrincipalUpdate::remove_item(
PrincipalField::Emails,
PrincipalValue::String(address),
)
})
.collect::>(),
),
)
.await;
eprintln!("Successfully updated account {name:?}.");
}
AccountCommands::AddToGroup { name, member_of } => {
client
.http_request::(
Method::PATCH,
&format!("/api/principal/{name}"),
Some(
member_of
.into_iter()
.map(|group| {
PrincipalUpdate::add_item(
PrincipalField::MemberOf,
PrincipalValue::String(group),
)
})
.collect::>(),
),
)
.await;
eprintln!("Successfully updated account {name:?}.");
}
AccountCommands::RemoveFromGroup { name, member_of } => {
client
.http_request::(
Method::PATCH,
&format!("/api/principal/{name}"),
Some(
member_of
.into_iter()
.map(|group| {
PrincipalUpdate::remove_item(
PrincipalField::MemberOf,
PrincipalValue::String(group),
)
})
.collect::>(),
),
)
.await;
eprintln!("Successfully updated account {name:?}.");
}
AccountCommands::Delete { name } => {
client
.http_request::(
Method::DELETE,
&format!("/api/principal/{name}"),
None,
)
.await;
eprintln!("Successfully deleted account {name:?}.");
}
AccountCommands::Display { name } => {
client.display_principal(&name).await;
}
AccountCommands::List {
filter,
limit,
page,
} => {
client
.list_principals("individual", "Account", filter, page, limit)
.await;
}
}
}
}
impl Client {
pub async fn display_principal(&self, name: &str) {
let principal = self
.http_request::(Method::GET, &format!("/api/principal/{name}"), None)
.await;
let mut table = Table::new();
if let Some(name) = principal.name {
table.add_row(Row::new(vec![
Cell::new("Name").with_style(Attr::Bold),
Cell::new(&name),
]));
}
if let Some(typ) = principal.typ {
table.add_row(Row::new(vec![
Cell::new("Type").with_style(Attr::Bold),
Cell::new(&typ.to_string()),
]));
}
if let Some(description) = principal.description {
table.add_row(Row::new(vec![
Cell::new("Description").with_style(Attr::Bold),
Cell::new(&description),
]));
}
if matches!(
principal.typ,
Some(Type::Individual | Type::Superuser | Type::Group)
) {
if let Some(quota) = principal.quota {
table.add_row(Row::new(vec![
Cell::new("Quota").with_style(Attr::Bold),
if quota != 0 {
Cell::new("a.to_string())
} else {
Cell::new("Unlimited")
},
]));
}
if let Some(used_quota) = principal.used_quota {
table.add_row(Row::new(vec![
Cell::new("Used Quota").with_style(Attr::Bold),
Cell::new(&used_quota.to_string()),
]));
}
}
if !principal.members.is_empty() {
table.add_row(Row::new(vec![
Cell::new("Members").with_style(Attr::Bold),
Cell::new(&principal.members.join(", ")),
]));
}
if !principal.member_of.is_empty() {
table.add_row(Row::new(vec![
Cell::new("Member of").with_style(Attr::Bold),
Cell::new(&principal.member_of.join(", ")),
]));
}
if !principal.emails.is_empty() {
table.add_row(Row::new(vec![
Cell::new("E-mail address(es)").with_style(Attr::Bold),
Cell::new(&principal.emails.join(", ")),
]));
}
eprintln!();
table.printstd();
eprintln!();
}
pub async fn list_principals(
&self,
record_type: &str,
record_name: &str,
filter: Option,
page: Option,
limit: Option,
) {
let mut query = form_urlencoded::Serializer::new("/api/principal?".to_string());
query.append_pair("type", record_type);
if let Some(filter) = &filter {
query.append_pair("filter", filter);
}
if let Some(limit) = limit {
query.append_pair("limit", &limit.to_string());
}
if let Some(page) = page {
query.append_pair("page", &page.to_string());
}
let results = self
.http_request::(Method::GET, &query.finish(), None)
.await;
if !results.items.is_empty() {
let mut table = Table::new();
table.add_row(Row::new(vec![
Cell::new(&format!("{record_name} Name")).with_style(Attr::Bold),
]));
for item in &results.items {
table.add_row(Row::new(vec![Cell::new(item)]));
}
eprintln!();
table.printstd();
eprintln!();
}
eprintln!(
"\n\n{} {}{} found.\n",
results.total,
record_name.to_ascii_lowercase(),
if results.total == 1 { "" } else { "s" }
);
}
}
#[derive(Debug, serde::Deserialize)]
struct ListResponse {
pub total: usize,
pub items: Vec,
}
impl Display for Type {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Type::Superuser => write!(f, "Superuser"),
Type::Individual => write!(f, "Individual"),
Type::Group => write!(f, "Group"),
Type::List => write!(f, "List"),
Type::Resource => write!(f, "Resource"),
Type::Location => write!(f, "Location"),
Type::Other => write!(f, "Other"),
}
}
}
================================================
FILE: crates/cli/src/modules/cli.rs
================================================
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use super::dkim::Algorithm;
use clap::{Parser, Subcommand, ValueEnum};
use jmap_client::client::Credentials;
use mail_parser::DateTime;
use serde::Deserialize;
#[derive(Parser)]
#[clap(version, about, long_about = None)]
#[clap(name = "stalwart-cli")]
pub struct Cli {
#[clap(subcommand)]
pub command: Commands,
/// Server base URL
#[clap(short, long)]
pub url: Option,
/// Authentication credentials
#[clap(short, long)]
pub credentials: Option,
/// Connection timeout in seconds
#[clap(short, long)]
pub timeout: Option,
/// Do not ask for credentials
#[clap(short, long)]
pub anonymous: bool,
}
#[derive(Subcommand)]
pub enum Commands {
/// Manage user accounts
/* #[clap(subcommand)]
Account(AccountCommands),
/// Manage domains
#[clap(subcommand)]
Domain(DomainCommands),
/// Manage mailing lists
#[clap(subcommand)]
List(ListCommands),
/// Manage groups
#[clap(subcommand)]
Group(GroupCommands),
*/
/// Manage DKIM signatures
#[clap(subcommand)]
Dkim(DkimCommands),
/// Import JMAP accounts and Maildir/mbox mailboxes
#[clap(subcommand)]
Import(ImportCommands),
/// Export JMAP accounts
#[clap(subcommand)]
Export(ExportCommands),
/// Manage JMAP database
#[clap(subcommand)]
Server(ServerCommands),
/// Manage SMTP message queue
#[clap(subcommand)]
Queue(QueueCommands),
/// Manage SMTP DMARC/TLS report queue
#[clap(subcommand)]
Report(ReportCommands),
}
pub struct Client {
pub url: String,
pub credentials: Credentials,
pub timeout: Option,
}
#[derive(Subcommand)]
pub enum AccountCommands {
/// Create a new user account
Create {
/// Login Name
name: String,
/// Password
password: String,
/// Account description
#[clap(short, long)]
description: Option,
/// Quota in bytes
#[clap(short, long)]
quota: Option,
/// Whether the account is an administrator
#[clap(short, long)]
is_admin: Option,
/// E-mail addresses
#[clap(short, long)]
addresses: Option>,
/// Groups this account is a member of
#[clap(short, long)]
member_of: Option>,
},
/// Update an existing user account
Update {
/// Account login
name: String,
/// Rename account login
#[clap(short, long)]
new_name: Option,
/// Update password
#[clap(short, long)]
password: Option,
/// Update account description
#[clap(short, long)]
description: Option,
/// Update quota in bytes
#[clap(short, long)]
quota: Option,
/// Whether the account is an administrator
#[clap(short, long)]
is_admin: Option,
/// Update e-mail addresses
#[clap(short, long)]
addresses: Option>,
/// Update groups this account is a member of
#[clap(short, long)]
member_of: Option>,
},
/// Add e-mail aliases to a user account
AddEmail {
/// Account login
name: String,
/// E-mail aliases to add
#[clap(required = true)]
addresses: Vec,
},
/// Remove e-mail aliases to a user account
RemoveEmail {
/// Account login
name: String,
/// E-mail aliases to remove
#[clap(required = true)]
addresses: Vec,
},
/// Add a user account to groups
AddToGroup {
/// Account login
name: String,
/// Groups to add
#[clap(required = true)]
member_of: Vec,
},
/// Remove a user account from groups
RemoveFromGroup {
/// Account login
name: String,
/// Groups to remove
#[clap(required = true)]
member_of: Vec,
},
/// Delete an existing user account
Delete {
/// Account name to delete
name: String,
},
/// Display an existing user account
Display {
/// Account name to display
name: String,
},
/// List all user accounts
List {
/// Filter accounts by keywords
filter: Option,
/// Maximum number of accounts to list
limit: Option,
/// Page number
page: Option,
},
}
#[derive(Subcommand)]
pub enum ListCommands {
/// Create a new mailing list
Create {
/// List Name
name: String,
/// List email address
email: String,
/// Description
#[clap(short, long)]
description: Option,
/// Mailing list members
#[clap(short, long)]
members: Option>,
},
/// Update an existing mailing list
Update {
/// List Name
name: String,
/// Rename list
new_name: Option,
/// List email address
email: Option,
/// Description
#[clap(short, long)]
description: Option,
/// Mailing list members
#[clap(short, long)]
members: Option>,
},
/// Add members to a mailing list
AddMembers {
/// List Name
name: String,
/// Members to add
#[clap(required = true)]
members: Vec,
},
/// Remove members from a mailing list
RemoveMembers {
/// List Name
name: String,
/// Members to remove
#[clap(required = true)]
members: Vec,
},
/// Display an existing mailing list
Display {
/// Mailing list to display
name: String,
},
/// List all mailing lists
List {
/// Filter mailing lists by keywords
filter: Option,
/// Maximum number of mailing lists to list
limit: Option,
/// Page number
page: Option,
},
}
#[derive(Subcommand)]
pub enum GroupCommands {
/// Create a group
Create {
/// Group Name
name: String,
/// Group email address
email: Option,
/// Description
#[clap(short, long)]
description: Option,
/// Group members
#[clap(short, long)]
members: Option>,
},
/// Update an existing group
Update {
/// Group Name
name: String,
/// Rename group
new_name: Option,
/// Group email address
email: Option,
/// Description
#[clap(short, long)]
description: Option,
/// Update groups that this group is a member of
#[clap(short, long)]
members: Option>,
},
/// Add members to a group
AddMembers {
/// Group name
name: String,
/// Groups to add
#[clap(required = true)]
members: Vec,
},
/// Remove members from a group
RemoveMembers {
/// Group name
name: String,
/// Groups to remove
#[clap(required = true)]
members: Vec,
},
/// Display an existing group
Display {
/// Group name to display
name: String,
},
/// List all groups
List {
/// Filter groups by keywords
filter: Option,
/// Maximum number of groups to list
limit: Option,
/// Page number
page: Option,
},
}
#[derive(Subcommand)]
pub enum DomainCommands {
/// Create a new domain
Create {
/// Domain name to create
name: String,
},
/// Delete an existing domain
Delete {
/// Domain name to delete
name: String,
},
/// List DNS records for domain
DNSRecords {
/// Domain name to list DNS records for
name: String,
},
/// List all domains
List {
/// Starting point for listing domains
from: Option,
/// Maximum number of domains to list
limit: Option,
},
}
#[derive(Subcommand)]
pub enum DkimCommands {
/// Create DKIM signature
Create {
/// Algorithm to use
algorithm: Algorithm,
/// Domain name for which to create
domain: String,
/// Id
signature_id: Option,
/// Selector
selector: Option,
},
/// Get DKIM public key
GetPublicKey {
/// Signature id
signature_id: String,
},
}
#[derive(Subcommand)]
pub enum ImportCommands {
/// Import messages and folders
Messages {
#[clap(value_enum)]
#[clap(short, long)]
format: MailboxFormat,
/// Number of messages to import concurrently, defaults to the number of CPUs.
#[clap(short, long)]
num_concurrent: Option,
/// Account name or email to import messages into
account: String,
/// Path to the mailbox to import, or '-' for stdin (stdin only supported for mbox)
path: String,
},
/// Import a JMAP account
Account {
/// Number of concurrent requests, defaults to the number of CPUs.
#[clap(short, long)]
num_concurrent: Option,
/// Account name or email to import messages into
account: String,
/// Path to the exported account directory
path: String,
},
}
#[derive(Subcommand)]
pub enum ExportCommands {
/// Export a JMAP account
Account {
/// Number of concurrent blob downloads to perform, defaults to the number of CPUs.
#[clap(short, long)]
num_concurrent: Option,
/// Account name or email to import messages into
account: String,
/// Path to export the account to
path: String,
},
}
#[derive(Subcommand)]
pub enum ServerCommands {
/// Perform database maintenance
DatabaseMaintenance {},
/// Reload TLS certificates
ReloadCertificates {},
/// Reload configuration
ReloadConfig {},
/// Create a new configuration key
AddConfig {
/// Key to add
key: String,
/// Value to set
value: Option,
},
/// Delete a configuration key or prefix
DeleteConfig {
/// Configuration key or prefix to delete
key: String,
},
/// List all configuration entries
ListConfig {
/// Prefix to filter configuration entries by
prefix: Option,
},
/// Perform Healthcheck
Healthcheck {
/// Status `ready` (default) or `live` to check for
check: Option
},
}
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
pub enum MailboxFormat {
/// Mbox format
Mbox,
/// Maildir and Maildir++ formats
Maildir,
/// Maildir with hierarchical folders (i.e. Dovecot)
MaildirNested,
}
#[derive(Subcommand)]
pub enum QueueCommands {
/// Shows messages queued for delivery
List {
/// Filter by sender address
#[clap(short, long)]
sender: Option,
/// Filter by recipient
#[clap(short, long)]
rcpt: Option,
/// Filter messages due for delivery before a certain datetime
#[clap(short, long)]
#[arg(value_parser = parse_datetime)]
before: Option,
/// Filter messages due for delivery after a certain datetime
#[clap(short, long)]
#[arg(value_parser = parse_datetime)]
after: Option,
/// Number of items to show per page
#[clap(short, long)]
page_size: Option,
},
/// Displays details about a queued message
Status {
#[clap(required = true)]
ids: Vec,
},
/// Reschedule delivery
Retry {
/// Apply to messages matching a sender address
#[clap(short, long)]
sender: Option,
/// Apply to a specific domain
#[clap(short, long)]
domain: Option,
/// Apply to messages due before a certain datetime
#[clap(short, long)]
#[arg(value_parser = parse_datetime)]
before: Option,
/// Apply to messages due after a certain datetime
#[clap(short, long)]
#[arg(value_parser = parse_datetime)]
after: Option,
/// Schedule delivery at a specific time
#[clap(short, long)]
#[arg(value_parser = parse_datetime)]
time: Option,
// Reschedule one or multiple message ids
ids: Vec,
},
/// Cancel delivery
Cancel {
/// Apply to messages matching a sender address
#[clap(short, long)]
sender: Option,
/// Apply to specific recipients or domains
#[clap(short, long)]
rcpt: Option,
/// Apply to messages due before a certain datetime
#[clap(short, long)]
#[arg(value_parser = parse_datetime)]
before: Option,
/// Apply to messages due after a certain datetime
#[clap(short, long)]
#[arg(value_parser = parse_datetime)]
after: Option,
// Cancel one or multiple message ids
ids: Vec,
},
}
#[derive(Subcommand)]
pub enum ReportCommands {
/// Shows reports queued for delivery
List {
/// Filter by report domain
#[clap(short, long)]
domain: Option,
/// Filter by report type
#[clap(short, long)]
#[clap(value_enum)]
format: Option,
/// Number of items to show per page
#[clap(short, long)]
page_size: Option,
},
/// Displays details about a queued report
Status {
#[clap(required = true)]
ids: Vec,
},
/// Cancel report delivery
Cancel {
#[clap(required = true)]
ids: Vec,
},
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum, Deserialize)]
pub enum ReportFormat {
/// DMARC report
#[serde(rename = "dmarc")]
Dmarc,
/// TLS report
#[serde(rename = "tls")]
Tls,
}
fn parse_datetime(arg: &str) -> Result {
if arg.contains('T') {
DateTime::parse_rfc3339(arg).ok_or("Failed to parse RFC3339 datetime")
} else {
DateTime::parse_rfc3339(&format!("{arg}T00:00:00Z"))
.ok_or("Failed to parse RFC3339 datetime")
}
}
================================================
FILE: crates/cli/src/modules/database.rs
================================================
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use std::collections::HashMap;
use prettytable::{Attr, Cell, Row, Table};
use reqwest::{Method, StatusCode};
use serde_json::Value;
use crate::modules::{Response, UnwrapResult};
use super::cli::{Client, ServerCommands};
#[derive(Debug, serde::Serialize, serde::Deserialize)]
#[serde(tag = "type")]
#[serde(rename_all = "camelCase")]
pub enum UpdateSettings {
Delete {
keys: Vec,
},
Clear {
prefix: String,
#[serde(default)]
filter: Option,
},
Insert {
prefix: Option,
values: Vec<(String, String)>,
assert_empty: bool,
},
}
impl ServerCommands {
pub async fn exec(self, client: Client) {
match self {
ServerCommands::DatabaseMaintenance {} => {
client
.http_request::(Method::GET, "/api/store/maintenance", None)
.await;
eprintln!("Success.");
}
ServerCommands::ReloadCertificates {} => {
client
.http_request::(Method::GET, "/api/reload/certificate", None)
.await;
eprintln!("Success.");
}
ServerCommands::ReloadConfig {} => {
client
.http_request::(Method::GET, "/api/reload", None)
.await;
eprintln!("Success.");
}
ServerCommands::AddConfig { key, value } => {
client
.http_request::(
Method::POST,
"/api/settings",
Some(vec![UpdateSettings::Insert {
prefix: None,
values: vec![(key.clone(), value.unwrap_or_default())],
assert_empty: false,
}]),
)
.await;
eprintln!("Successfully added key {key}.");
}
ServerCommands::DeleteConfig { key } => {
client
.http_request::(
Method::POST,
"/api/settings",
Some(vec![UpdateSettings::Delete {
keys: vec![key.clone()],
}]),
)
.await;
eprintln!("Successfully deleted key {key}.");
}
ServerCommands::ListConfig { prefix } => {
let results = client
.http_request::>, String>(
Method::GET,
&format!("/api/settings/list?prefix={}", prefix.unwrap_or_default()),
None,
)
.await
.items;
if !results.is_empty() {
let mut table = Table::new();
table.add_row(Row::new(vec![
Cell::new("Key").with_style(Attr::Bold),
Cell::new("Value").with_style(Attr::Bold),
]));
for (key, value) in &results {
table.add_row(Row::new(vec![Cell::new(key), Cell::new(value)]));
}
eprintln!();
table.printstd();
eprintln!();
}
eprintln!(
"\n\n{} key{} found.\n",
results.len(),
if results.len() == 1 { "" } else { "s" }
);
}
ServerCommands::Healthcheck { check } => {
let response = reqwest::get(
format!("{}/healthz/{}",
client.url,
check.unwrap_or("ready".to_string()))
).await;
match response {
Ok(resp) => {
match resp.status() {
StatusCode::OK => {
eprintln!("Success")
},
_ => {
eprintln!(
"Request failed: {}",
resp.text().await.unwrap_result("fetch text")
);
std::process::exit(1);
}
}
}
Err(err) => {
eprintln!("Request failed: {}", err);
std::process::exit(1);
}
}
}
}
}
}
================================================
FILE: crates/cli/src/modules/dkim.rs
================================================
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use super::cli::{Client, DkimCommands};
use clap::ValueEnum;
use reqwest::Method;
use serde::{Deserialize, Serialize};
use serde_json::Value;
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, ValueEnum)]
pub enum Algorithm {
/// RSA
#[default]
Rsa,
/// ED25519
Ed25519,
}
#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize)]
struct DkimSignature {
#[serde(skip_serializing_if = "Option::is_none")]
pub id: Option,
pub algorithm: Algorithm,
pub domain: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub selector: Option,
}
impl DkimCommands {
pub async fn exec(self, client: Client) {
match self {
DkimCommands::Create {
signature_id,
algorithm,
domain,
selector,
} => {
let signature_req = DkimSignature {
id: signature_id,
algorithm,
domain: domain.clone(),
selector,
};
client
.http_request::(Method::POST, "/api/dkim", Some(signature_req))
.await;
eprintln!("Successfully created {algorithm:?} signature for domain {domain:?}");
}
DkimCommands::GetPublicKey { signature_id } => {
let response = client
.http_request::(
Method::GET,
&format!("/api/dkim/{signature_id}"),
None,
)
.await;
eprintln!();
eprintln!("Public DKIM key for signature {signature_id}: {response}");
eprintln!();
}
}
}
}
================================================
FILE: crates/cli/src/modules/domain.rs
================================================
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use std::borrow::Cow;
use prettytable::{Attr, Cell, Row, Table, format};
use reqwest::Method;
use serde_json::Value;
use crate::modules::List;
use super::cli::{Client, DomainCommands};
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize, Clone)]
struct DnsRecord {
#[serde(rename = "type")]
typ: String,
name: String,
content: String,
}
impl DomainCommands {
pub async fn exec(self, client: Client) {
match self {
DomainCommands::Create { name } => {
client
.http_request::(
Method::POST,
&format!("/api/domain/{name}"),
None,
)
.await;
eprintln!("Successfully created domain {name:?}");
}
DomainCommands::Delete { name } => {
client
.http_request::(
Method::DELETE,
&format!("/api/domain/{name}"),
None,
)
.await;
eprintln!("Successfully deleted domain {name:?}");
}
DomainCommands::DNSRecords { name } => {
let records = client
.http_request::, String>(
Method::GET,
&format!("/api/domain/{name}"),
None,
)
.await;
if !records.is_empty() {
let mut table = Table::new();
// no borderline separator separator, as long values will mess it up
table.set_format(*format::consts::FORMAT_NO_BORDER_LINE_SEPARATOR);
table.add_row(Row::new(vec![
Cell::new("Type").with_style(Attr::Bold),
Cell::new("Name").with_style(Attr::Bold),
Cell::new("Contents").with_style(Attr::Bold),
]));
for record in &records {
table.add_row(Row::new(vec![
Cell::new(&record.typ),
Cell::new(&record.name),
Cell::new(&record.content),
]));
}
eprintln!();
table.printstd();
eprintln!();
}
}
DomainCommands::List { from, limit } => {
let query = if from.is_none() && limit.is_none() {
Cow::Borrowed("/api/domain")
} else {
let mut query = "/api/domain?".to_string();
if let Some(from) = &from {
query.push_str(&format!("from={from}"));
}
if let Some(limit) = limit {
query.push_str(&format!(
"{}limit={limit}",
if from.is_some() { "&" } else { "" }
));
}
Cow::Owned(query)
};
let domains = client
.http_request::, String>(Method::GET, query.as_ref(), None)
.await;
if !domains.items.is_empty() {
let mut table = Table::new();
table.add_row(Row::new(vec![
Cell::new("Domain Name").with_style(Attr::Bold),
]));
for domain in &domains.items {
table.add_row(Row::new(vec![Cell::new(domain)]));
}
eprintln!();
table.printstd();
eprintln!();
}
eprintln!(
"\n\n{} domain{} found.\n",
domains.total,
if domains.total == 1 { "" } else { "s" }
);
}
}
}
}
================================================
FILE: crates/cli/src/modules/export.rs
================================================
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use std::{
path::{Path, PathBuf},
sync::Arc,
};
use futures::{StreamExt, stream::FuturesUnordered};
use jmap_client::{
email::{self, Email},
identity::{self, Identity},
mailbox::{self, Mailbox},
sieve::{self, SieveScript},
vacation_response::{self, VacationResponse},
};
use serde::Serialize;
use tokio::io::AsyncWriteExt;
use crate::modules::RETRY_ATTEMPTS;
use super::{
UnwrapResult,
cli::{Client, ExportCommands},
name_to_id,
};
impl ExportCommands {
pub async fn exec(self, client: Client) {
let mut client = client.into_jmap_client().await;
match self {
ExportCommands::Account {
num_concurrent,
account,
path,
} => {
client.set_default_account_id(name_to_id(&client, &account).await);
let max_objects_in_get = client
.session()
.core_capabilities()
.map(|c| c.max_objects_in_get())
.unwrap_or(500);
// Create directory
let mut path = PathBuf::from(path);
if !path.is_dir() {
eprintln!("Directory {} does not exist.", path.display());
std::process::exit(1);
}
path.push(&account);
if !path.is_dir() {
std::fs::create_dir(&path).unwrap_or_else(|_| {
eprintln!("Failed to create directory: {}", path.display());
std::process::exit(1);
});
}
// Export metadata
let mut blobs = Vec::new();
export_mailboxes(&client, max_objects_in_get, &path).await;
export_emails(&client, max_objects_in_get, &mut blobs, &path).await;
export_sieve_scripts(&client, max_objects_in_get, &mut blobs, &path).await;
export_identities(&client, &path).await;
export_vacation_responses(&client, &path).await;
// Export blobs
path.push("blobs");
if !path.exists() {
std::fs::create_dir(&path).unwrap_or_else(|_| {
eprintln!("Failed to create directory: {}", path.display());
std::process::exit(1);
});
}
let client = Arc::new(client);
let num_concurrent = num_concurrent.unwrap_or_else(num_cpus::get);
let mut futures = FuturesUnordered::new();
eprintln!("Exporting {} blobs...", blobs.len());
for blob_id in blobs {
let client = client.clone();
let mut blob_path = path.clone();
blob_path.push(&blob_id);
if tokio::fs::metadata(&blob_path).await.is_err() {
futures.push(async move {
let mut retry_count = 0;
let bytes = loop {
match client.download(&blob_id).await {
Ok(bytes) => break bytes,
Err(_) if retry_count < RETRY_ATTEMPTS => {
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
retry_count += 1;
}
result => {
result.unwrap_result("download blob");
return;
}
}
};
tokio::fs::OpenOptions::new()
.create(true)
.write(true)
.truncate(true)
.open(&blob_path)
.await
.unwrap_result(&format!("open {}", blob_path.display()))
.write_all(&bytes)
.await
.unwrap_result(&format!("write {}", blob_path.display()));
});
}
if futures.len() == num_concurrent {
futures.next().await.unwrap();
}
}
// Wait for remaining futures
while futures.next().await.is_some() {}
}
}
}
}
pub async fn fetch_mailboxes(
client: &jmap_client::client::Client,
max_objects_in_get: usize,
) -> Vec {
let mut position = 0;
let mut results = Vec::new();
loop {
let mut request = client.build();
let query_result = request
.query_mailbox()
.calculate_total(true)
.position(position)
.limit(max_objects_in_get)
.result_reference();
request.get_mailbox().ids_ref(query_result).properties([
mailbox::Property::Id,
mailbox::Property::Name,
mailbox::Property::IsSubscribed,
mailbox::Property::ParentId,
mailbox::Property::Role,
mailbox::Property::SortOrder,
mailbox::Property::ShareWith,
]);
let mut response = request
.send()
.await
.unwrap_result("send JMAP request")
.unwrap_method_responses();
if response.len() != 2 {
eprintln!("Invalid response while fetching mailboxes");
std::process::exit(1);
}
let mut get_response = response
.pop()
.unwrap()
.unwrap_get_mailbox()
.unwrap_result("fetch mailboxes");
let mailboxes_part = get_response.take_list();
let total_mailboxes = response
.pop()
.unwrap()
.unwrap_query_mailbox()
.unwrap_result("query mailboxes")
.total()
.unwrap_or(0);
let mailboxes_part_len = mailboxes_part.len();
if mailboxes_part_len > 0 {
results.extend(mailboxes_part);
if results.len() < total_mailboxes {
position += mailboxes_part_len as i32;
continue;
}
}
break;
}
results
}
async fn export_mailboxes(
client: &jmap_client::client::Client,
max_objects_in_get: usize,
path: &Path,
) {
eprintln!(
"Exported {} mailboxes.",
write_file(
path,
"mailboxes.json",
fetch_mailboxes(client, max_objects_in_get).await,
)
.await
);
}
pub async fn fetch_emails(
client: &jmap_client::client::Client,
max_objects_in_get: usize,
) -> Vec {
let mut position = 0;
let mut results = Vec::new();
loop {
let mut request = client.build();
let query_result = request
.query_email()
.calculate_total(true)
.position(position)
.limit(max_objects_in_get)
.result_reference();
request.get_email().ids_ref(query_result).properties([
email::Property::Id,
email::Property::MailboxIds,
email::Property::Keywords,
email::Property::ReceivedAt,
email::Property::BlobId,
email::Property::MessageId,
]);
let mut response = request
.send()
.await
.unwrap_result("send JMAP request")
.unwrap_method_responses();
if response.len() != 2 {
eprintln!("Invalid response while fetching emails");
std::process::exit(1);
}
let mut get_response = response
.pop()
.unwrap()
.unwrap_get_email()
.unwrap_result("fetch emails");
let emails_part = get_response.take_list();
let total_emails = response
.pop()
.unwrap()
.unwrap_query_email()
.unwrap_result("query emails")
.total()
.unwrap_or(0);
let emails_part_len = emails_part.len();
if emails_part_len > 0 {
results.extend(emails_part);
if results.len() < total_emails {
position += emails_part_len as i32;
continue;
}
}
break;
}
results
}
async fn export_emails(
client: &jmap_client::client::Client,
max_objects_in_get: usize,
blobs: &mut Vec,
path: &Path,
) {
let emails = fetch_emails(client, max_objects_in_get).await;
for email in &emails {
if let Some(blob_id) = email.blob_id() {
blobs.push(blob_id.to_string());
} else {
eprintln!(
"Warning: email {:?} has no blobId",
email.id().unwrap_or_default()
);
}
}
eprintln!(
"Exported {} emails.",
write_file(path, "emails.json", emails,).await
);
}
pub async fn fetch_sieve_scripts(
client: &jmap_client::client::Client,
max_objects_in_get: usize,
) -> Vec {
let mut position = 0;
let mut results = Vec::new();
loop {
let mut request = client.build();
let query_result = request
.query_sieve_script()
.calculate_total(true)
.position(position)
.limit(max_objects_in_get)
.result_reference();
request
.get_sieve_script()
.ids_ref(query_result)
.properties([
sieve::Property::Id,
sieve::Property::Name,
sieve::Property::BlobId,
sieve::Property::IsActive,
]);
let mut response = request
.send()
.await
.unwrap_result("send JMAP request")
.unwrap_method_responses();
if response.len() != 2 {
eprintln!("Invalid response while fetching sieve_scripts");
std::process::exit(1);
}
let mut get_response = response
.pop()
.unwrap()
.unwrap_get_sieve_script()
.unwrap_result("fetch sieve_scripts");
let sieve_scripts_part = get_response.take_list();
let total_sieve_scripts = response
.pop()
.unwrap()
.unwrap_query_sieve_script()
.unwrap_result("query sieve_scripts")
.total()
.unwrap_or(0);
let sieve_scripts_part_len = sieve_scripts_part.len();
if sieve_scripts_part_len > 0 {
results.extend(sieve_scripts_part);
if results.len() < total_sieve_scripts {
position += sieve_scripts_part_len as i32;
continue;
}
}
break;
}
results
}
async fn export_sieve_scripts(
client: &jmap_client::client::Client,
max_objects_in_get: usize,
blobs: &mut Vec,
path: &Path,
) {
let sieves = fetch_sieve_scripts(client, max_objects_in_get).await;
for sieve in &sieves {
if let Some(blob_id) = sieve.blob_id() {
blobs.push(blob_id.to_string());
} else {
eprintln!(
"Warning: sieve script {:?} has no blobId",
sieve.id().unwrap_or_default()
);
}
}
eprintln!(
"Exported {} sieve scripts.",
write_file(path, "sieve.json", sieves,).await
);
}
pub async fn fetch_identities(client: &jmap_client::client::Client) -> Vec {
let mut request = client.build();
request.get_identity().properties([
identity::Property::Id,
identity::Property::Name,
identity::Property::Email,
identity::Property::ReplyTo,
identity::Property::Bcc,
identity::Property::TextSignature,
identity::Property::HtmlSignature,
]);
request
.send_get_identity()
.await
.unwrap_result("send JMAP request")
.take_list()
}
async fn export_identities(client: &jmap_client::client::Client, path: &Path) {
eprintln!(
"Exported {} identities.",
write_file(path, "identities.json", fetch_identities(client).await).await
);
}
pub async fn fetch_vacation_responses(
client: &jmap_client::client::Client,
) -> Vec {
let mut request = client.build();
request.get_vacation_response().properties([
vacation_response::Property::Id,
vacation_response::Property::FromDate,
vacation_response::Property::ToDate,
vacation_response::Property::Subject,
vacation_response::Property::TextBody,
vacation_response::Property::HtmlBody,
vacation_response::Property::IsEnabled,
]);
request
.send_get_vacation_response()
.await
.unwrap_result("send JMAP request")
.take_list()
}
async fn export_vacation_responses(client: &jmap_client::client::Client, path: &Path) {
eprintln!(
"Exported {} vacation responses.",
write_file(
path,
"vacation.json",
fetch_vacation_responses(client).await
)
.await
);
}
async fn write_file(path: &Path, name: &str, contents: Vec) -> usize {
let mut path = PathBuf::from(path);
path.push(name);
let len = contents.len();
tokio::fs::OpenOptions::new()
.create(true)
.write(true)
.truncate(true)
.open(&path)
.await
.unwrap_result(&format!("open {}", path.display()))
.write_all(serde_json::to_string(&contents).unwrap().as_bytes())
.await
.unwrap_result(&format!("write to {}", path.display()));
len
}
================================================
FILE: crates/cli/src/modules/group.rs
================================================
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use std::vec;
use reqwest::Method;
use serde_json::Value;
use crate::modules::{Principal, Type};
use super::{
PrincipalField, PrincipalUpdate, PrincipalValue,
cli::{Client, GroupCommands},
};
impl GroupCommands {
pub async fn exec(self, client: Client) {
match self {
GroupCommands::Create {
name,
email,
description,
members,
} => {
let principal = Principal {
typ: Some(Type::Group),
name: name.clone().into(),
emails: email.map(|e| vec![e]).unwrap_or_default(),
description,
..Default::default()
};
let account_id = client
.http_request::(Method::POST, "/api/principal", Some(principal))
.await;
if let Some(members) = members {
client
.http_request::(
Method::PATCH,
&format!("/api/principal/{name}"),
Some(vec![PrincipalUpdate::set(
PrincipalField::Members,
PrincipalValue::StringList(members),
)]),
)
.await;
}
eprintln!("Successfully created group {name:?} with id {account_id}.");
}
GroupCommands::Update {
name,
new_name,
email,
description,
members,
} => {
let mut changes = Vec::new();
if let Some(new_name) = new_name {
changes.push(PrincipalUpdate::set(
PrincipalField::Name,
PrincipalValue::String(new_name),
));
}
if let Some(email) = email {
changes.push(PrincipalUpdate::set(
PrincipalField::Emails,
PrincipalValue::StringList(vec![email]),
));
}
if let Some(members) = members {
changes.push(PrincipalUpdate::set(
PrincipalField::Members,
PrincipalValue::StringList(members),
));
}
if let Some(description) = description {
changes.push(PrincipalUpdate::set(
PrincipalField::Description,
PrincipalValue::String(description),
));
}
if !changes.is_empty() {
client
.http_request::(
Method::PATCH,
&format!("/api/principal/{name}"),
Some(changes),
)
.await;
eprintln!("Successfully updated group {name:?}.");
} else {
eprintln!("No changes to apply.");
}
}
GroupCommands::AddMembers { name, members } => {
client
.http_request::(
Method::PATCH,
&format!("/api/principal/{name}"),
Some(
members
.into_iter()
.map(|group| {
PrincipalUpdate::add_item(
PrincipalField::Members,
PrincipalValue::String(group),
)
})
.collect::>(),
),
)
.await;
eprintln!("Successfully updated group {name:?}.");
}
GroupCommands::RemoveMembers { name, members } => {
client
.http_request::(
Method::PATCH,
&format!("/api/principal/{name}"),
Some(
members
.into_iter()
.map(|group| {
PrincipalUpdate::remove_item(
PrincipalField::Members,
PrincipalValue::String(group),
)
})
.collect::>(),
),
)
.await;
eprintln!("Successfully updated group {name:?}.");
}
GroupCommands::Display { name } => {
client.display_principal(&name).await;
}
GroupCommands::List {
filter,
limit,
page,
} => {
client
.list_principals("group", "Group", filter, page, limit)
.await;
}
}
}
}
================================================
FILE: crates/cli/src/modules/import.rs
================================================
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use std::{
collections::{HashMap, HashSet},
io::{self, Cursor},
path::{Path, PathBuf},
sync::{
Arc, Mutex,
atomic::{AtomicUsize, Ordering},
},
time::Duration,
};
use console::style;
use futures::{StreamExt, stream::FuturesUnordered};
use indicatif::{MultiProgress, ProgressBar, ProgressStyle};
use jmap_client::{
core::set::SetObject,
mailbox::{self, Role},
};
use mail_parser::mailbox::{
maildir,
mbox::{self, MessageIterator},
};
use rand::Rng;
use serde::de::DeserializeOwned;
use tokio::{fs::File, io::AsyncReadExt};
use crate::modules::{RETRY_ATTEMPTS, UnwrapResult, name_to_id};
use super::{
cli::{Client, ImportCommands, MailboxFormat},
export::{
fetch_emails, fetch_identities, fetch_mailboxes, fetch_sieve_scripts,
fetch_vacation_responses,
},
read_file,
};
enum Mailbox {
Mbox(mbox::MessageIterator>>),
Maildir(maildir::MessageIterator),
None,
}
#[derive(Debug)]
enum MailboxId<'x> {
ExistingId(&'x str),
CreateId(String),
None,
}
#[derive(Debug)]
struct Message {
identifier: String,
flags: Vec,
internal_date: u64,
contents: Vec,
}
impl ImportCommands {
pub async fn exec(self, client: Client) {
let mut client = client.into_jmap_client().await;
match self {
ImportCommands::Messages {
num_concurrent,
format,
account,
path,
} => {
client.set_default_account_id(name_to_id(&client, &account).await);
let mut create_mailboxes = Vec::new();
let mut create_mailbox_names = Vec::new();
let mut create_mailbox_ids = Vec::new();
eprintln!("{} Parsing mailbox...", style("[1/4]").bold().dim(),);
match format {
MailboxFormat::Mbox => {
create_mailbox_names.push(Vec::new());
create_mailboxes.push(Mailbox::Mbox(MessageIterator::new(Cursor::new(
read_file(&path),
))));
}
MailboxFormat::Maildir | MailboxFormat::MaildirNested => {
let (folder_sep, folder_split) = if format == MailboxFormat::Maildir {
(Some("."), ".")
} else {
(None, "/")
};
for folder in maildir::FolderIterator::new(path, folder_sep)
.unwrap_result("read Maildir folder")
{
let folder = folder.unwrap_result("read Maildir folder");
if let Some(folder_name) = folder.name() {
let mut folder_parts = Vec::new();
for folder_name in folder_name.split(folder_split) {
let mut folder_name = folder_name.trim();
if folder_name.is_empty() {
folder_name = ".";
}
folder_parts.push(folder_name.to_string());
if !create_mailbox_names.contains(&folder_parts) {
create_mailboxes.push(Mailbox::None);
create_mailbox_names.push(folder_parts.clone());
}
}
*create_mailboxes.last_mut().unwrap() = Mailbox::Maildir(folder);
} else {
create_mailboxes.push(Mailbox::Maildir(folder));
create_mailbox_names.push(Vec::new());
};
}
}
}
// Fetch all mailboxes for the account
eprintln!(
"{} Fetching existing mailboxes for account...",
style("[2/4]").bold().dim(),
);
let mut inbox_id = None;
let mut mailbox_ids = HashMap::new();
let mut children: HashMap