18 Commits
Author SHA1 Message Date
jcoffey-dev 0d8caaa514 Merge pull request 'Compile the scim crate in release, and check that profile in CI' (#26) from fix/scim-recursion-limit into main
ci / fork-checks (push) Successful in 1m11s
publish / version (push) Successful in 58s
publish / publish (push) Failing after 34s
publish / release (push) Skipped
publish / binaries (push) Skipped
ci / build (push) Canceled after 25m24s
2026-09-23 04:23:07 +00:00
jcoffey-dev ce6882fe93 Merge pull request 'queue_retry test: measure retries from when each attempt started' (#25) from fix/queue-retry-test into main
ci / fork-checks (push) Canceled after 1m30s
ci / build (push) Canceled after 1m30s
Reviewed-on: #25
2026-09-23 04:21:37 +00:00
jcoffey-dev 3df042e7d4 Compile the scim crate in release, and check that profile in CI
ci / name-check (pull_request) Successful in 3m31s
ci / build (pull_request) Successful in 7m28s
v2026.9.24 was tagged on a commit CI had passed, and its release build could
not compile crates/scim at all:

  error: queries overflow the depth limit!
    = note: query depth increased by 130 when computing layout of
      {async fn body of context::<impl ...>::writable_domain()}

The crate is ours, and the failure is profile-dependent: the release profile
computes those async fn layouts in one go and goes past rustc's default query
depth, while the dev profile never gets that far. CI builds dev, so CI was
green on a commit that could not be released. The tag produced no image and
no release, which is the one merciful part.

Two changes:

- #![recursion_limit = "256"] on the crate, which is what rustc itself
  suggests, with a note saying why it only shows up in release. Proved by
  building -p scim in release locally: it now finishes.

- CI builds the release profile too, on pushes to main. Pull requests stay
  on dev, where the wait is worth less. A few minutes per merge is cheaper
  than learning this from a tag, which throws away a multi-architecture
  build and leaves a version half-cut.
2026-09-22 21:13:50 -07:00
jcoffey-dev 64385007c1 Merge pull request 'Fix the antispam test: pin the rules it scores against, stop live Pyzor' (#24) from fix/antispam-test into main
ci / fork-checks (push) Successful in 2m4s
ci / build (push) Successful in 6m26s
Reviewed-on: #24
2026-09-23 04:12:40 +00:00
jcoffey-dev 4d794c6a65 Merge pull request 'Fork/brand lowercase' (#23) from fork/brand-lowercase into main
ci / fork-checks (push) Successful in 15s
ci / build (push) Canceled after 31s
Reviewed-on: #23
2026-09-23 04:12:06 +00:00
jcoffey-dev a404ca89f0 Merge pull request 'Fork/rename upstream identifiers' (#22) from fork/rename-upstream-identifiers into main
ci / fork-checks (push) Successful in 17s
ci / build (push) Canceled after 27s
Reviewed-on: #22
2026-09-23 04:11:34 +00:00
jcoffey-dev 79f54add2f Merge pull request 'Fork tooling: a build check and a rename pass in the strip, a notice check in CI' (#21) from fork/strip-build-check-and-notices into main
ci / fork-checks (push) Successful in 48s
ci / build (push) Canceled after 1m2s
Reviewed-on: #21
2026-09-23 04:10:32 +00:00
jcoffey-dev 86bf2432a2 queue_retry test: measure retries from when each attempt started
ci / name-check (pull_request) Successful in 2m30s
ci / build (pull_request) Successful in 7m33s
The server sets a deferred recipient's next retry from its clock when the
attempt defers, in whole seconds. The test subtracted its own clock taken
when the loop next saw the message, after saving and reporting, so
whenever that lag crossed a second boundary the 2 s retry measured 1 s
and the test failed. Under load, after the other SMTP tests, that was
most runs.

It now measures from when the test started the attempt, which the
server's deferral can only follow, by under a second: each retry is its
interval or one more. Each position is still checked against its own
interval, so a wrong schedule still fails.
2026-09-22 20:55:42 -07:00
jcoffey-dev 5be578ba3c antispam test: run it serially, like the tests sharing its port
ci / name-check (pull_request) Successful in 17s
ci / build (pull_request) Successful in 7m11s
It listens on HTTP 19048, as the dkim2 DSN and report tests do. Those are
marked serial, this wasn't, so when the SMTP tests ran together (as
upstream's CI runs them) it could start beside them and requests reached
whichever server had the port: missing JMAP creates here, and 'You must
authenticate first' in dkim2_dsn_is_signed.
2026-09-22 20:55:42 -07:00
jcoffey-dev f32992ca36 Fix the antispam test: pin the rules it scores against, stop live Pyzor
ci / name-check (pull_request) Successful in 17s
ci / build (pull_request) Successful in 7m12s
It failed everywhere but upstream's machines, for two reasons:

- The spam rules, which carry every score, came from a path on an
  upstream developer's own disk. Without SPAM_RULES_URL none loaded, every
  score was 0.00 and the combined case came out ham instead of spam at
  13.70. The published rules of spam-filter v3.0.2 are now pinned beside
  the test cases (Apache-2.0 or MIT, taken as MIT; in THIRD-PARTY.md).
  SPAM_RULES_URL still overrides.
- The first combined case expects a Pyzor hit, and its digest (that of an
  empty body) wasn't among the three the test mode answers, so it went to
  a public Pyzor server: it failed offline and would drift with that
  server's counts. Test mode now answers every digest from a fixed table,
  with the empty body's added, and never reaches the network.

The test passes online and offline, alone and with the rest of the SMTP
tests. queue_retry, unrelated, still fails when it runs after the others
in one process, though it passes alone every time.
2026-09-22 20:22:29 -07:00
jcoffey-dev a993f9ab01 Write the brand in lowercase where people see it
ci / fork-checks (pull_request) Successful in 47s
ci / build (pull_request) Successful in 5m4s
The name is inbuxa, lowercase, like the wordmark; INBUXA reads as an
acronym. The admin and webmail already changed. Here that's everything
the server shows people: the brand macro behind the protocol greetings,
the HTTP and SCIM realms, the startup banner and the calendar and contact
PRODID; the first-party OAuth client descriptions; the legacy-protocol
refusals; the default calendar and address book names and the SMTP
greeting default, in the code and the schema served to the admin
(checksum regenerated); startup and shutdown events; the User-Agent;
the sign-in and RSVP pages; the service units; the OpenAPI realm; the
crate descriptions and the README, where it's set in bold.

Identifiers that are uppercase for their own reasons stay: INBUXA_*
settings, SUBSPACE_INBUXA. So do code comments and the AGPL 5(a) notice
lines.

Tests follow: the IMAP ID name, the default collection names, the PRODID
in the iTIP fixtures and the CalDAV free-busy expectations, and the e2e
legacy-protocol refusals. The webdav, imap and jmap suites pass, so do
the unit tests of every crate touched, and 73 of 75 SMTP tests; of the
other two, antispam fails on main too, and queue_retry is a timing flake
that passes on its own.
2026-09-22 20:08:10 -07:00
jcoffey-dev 99096cdc9b Merge pull request 'Release 2026.9.24' (#20) from release/2026.9.24 into main
ci / name-check (push) Successful in 1m6s
publish / version (push) Successful in 1m3s
ci / build (push) Successful in 4m38s
publish / publish (push) Failing after 24m24s
publish / release (push) Skipped
publish / binaries (push) Skipped
2026-09-23 02:53:03 +00:00
jcoffey-dev 96ac70ad28 Release 2026.9.24
ci / name-check (pull_request) Successful in 15s
ci / build (pull_request) Successful in 7m10s
Carries upstream 0.16.23 -- the DSN, POP3, Sieve, DMARC-report, ACME and
DNSSEC-resolver fixes in its own change log -- with the files it changed
marked under AGPL section 5(a), and one upstream test dropped that the fork's
routing makes meaningless.

It is also the first release whose tag attaches binaries: a host install can
now fetch inbuxa-linux-amd64.tar.gz or inbuxa-linux-arm64.tar.gz instead of
pulling the image and copying the file out of it.
2026-09-22 19:45:19 -07:00
jcoffey-dev 835b278e66 Merge pull request 'Attach binaries to a release, for installs that are not containers' (#19) from release/binaries into main
ci / name-check (push) Successful in 44s
ci / build (push) Successful in 4m34s
2026-09-23 02:36:53 +00:00
jcoffey-dev cc6f1eb298 Rename the identifiers that carried the upstream name
ci / fork-checks (pull_request) Successful in 16s
ci / build (pull_request) Successful in 7m53s
Everything clients, users and operators meet now carries the fork's name,
with no aliases (SPEC.md §2.4, changed here from "protocol identifiers
stay"):

- JMAP: upstream's registry capability is urn:inbuxa:jmap:registry, beside
  the fork's own urn:inbuxa:jmap.
- WebDAV lock and sync tokens are urn:inbuxa:dav*; clients resync once.
- Sieve: vnd.inbuxa.while and vnd.inbuxa.expressions. sieve-rs spells these
  into its compiler, so it's vendored (vendor/sieve-rs, 0.7.3) and patched in;
  a unit test fails if Cargo.lock ever moves past the vendored copy. The
  trusted runtime now names itself too, rather than answering sieve-rs's
  default.
- The web interface's OAuth client is inbuxa-webui. On every start the old
  stalwart-webui client is removed and any application naming it is moved
  over.
- The spam filter's blobs are INBUXA_SPAM_*; every start moves any left
  under the old keys, so a trained model survives.
- SQL stores and log files default to inbuxa, in the code and in the
  schema served to the admin (checksum regenerated).
- Settings are INBUXA_* only. A STALWART_* variable that's set where its
  INBUXA_* one isn't stops the server at startup, naming it.
- The version-upgrade messages link docs.inbuxa.org's migration page, and
  the OpenAPI description, smtp crate metadata and web-push test fixtures
  lose the name.

Kept on purpose, allowlisted with reasons: the OAuth key-derivation
contexts (renaming them would end every session and invalidate every
sealed client id) and the hashed application prefix.

Also fixes a latent start-up failure: ensure_client updated an existing
first-party client with a revision of 0, which the registry's assertion
never matches, so adding a redirect URI or changing the webmail secret
failed start-up. And the principal session test now expects
legacyProtocols (C-1, added 2026-09-21), which it had missed.

Tested: the server builds without warnings; common's 106 unit tests,
including the vendoring check; a new integration test for the two
start-up migrations; and the webdav, jmap, imap and SMTP Sieve suites.
2026-09-22 19:33:02 -07:00
jcoffey-dev 674ae5d037 Attach binaries to a release, for installs that are not containers
ci / name-check (pull_request) Successful in 43s
ci / build (pull_request) Successful in 5m25s
A release published an image and nothing else, so there was nothing for a
host install to download -- the only way to get the binary was to pull the
image and copy it out, which makes "install without Docker" depend on
Docker.

Each release now carries inbuxa-linux-amd64.tar.gz, inbuxa-linux-arm64.tar.gz
and SHA256SUMS, named as stalwart-migrator's are.

They are taken out of the image this pipeline just pushed rather than
compiled again. A second Rust build per architecture is the slowest thing
here, and it would leave two artifacts that are meant to be the same build
and only probably are. Extracting makes that identity a fact: the binary in
the tarball is the file the image runs. `docker create` starts nothing, so
copying a file out of an arm64 image on an amd64 runner needs no emulation.

One thing the extraction cannot carry: the image grants the binary
cap_net_bind_service, and a tar archive does not keep that xattr. The
release body says so, and says what to do instead -- setcap, or
AmbientCapabilities in the unit -- because a server that cannot bind 25 and
does not say why is a bad first hour.

Checked by hand against v2026.9.23 before this landed: both architectures
extract to the right ELF, and the amd64 binary runs on a bare Debian 13 with
every library resolved and reports its own version.
2026-09-22 19:31:05 -07:00
jcoffey-dev 4799d191a0 Fork tooling: a build check and a rename pass in the strip, a notice check in CI
ci / fork-checks (pull_request) Successful in 18s
ci / build (pull_request) Successful in 7m11s
strip.py compiles the stripped tree, so a dual-licensed file that only
serves an Enterprise feature fails the import instead of the merge, as
v0.16.23's tests/src/directory/issuer.rs does. Upstream's tests of the
features the fork rebuilt are expected not to compile there and are listed
in build-check-known.txt; an error anywhere else fails the run. Checked
against both imports: v0.16.22 passes with its 16 expected errors, v0.16.23
fails on issuer.rs alone. Imports the strip leaves unused are reported.

It also renames the upstream name where clients, users or operators meet
it as an identifier, from tools/fork/renames.py: wire-protocol names, the
web interface's client id, store keys, configuration defaults and the
served schema. main is renamed with the same module, so a re-import
arrives purged and those lines don't conflict.

notice-check.py fails CI when an upstream file the fork changed, measured
against the upstream branch, lacks its AGPL 5(a) notice; --fix adds it.
It runs beside the name check in a renamed fork-checks job.

Also commits v0.16.23's strip report under docs/fork/strip-reports/, which
the import in #18 left out.
2026-09-22 19:02:34 -07:00
jcoffey-dev c5bf67f1bf Merge pull request 'Merge/upstream v0.16.23' (#18) from merge/upstream-v0.16.23 into main
ci / name-check (push) Successful in 19s
ci / build (push) Successful in 37m41s
Reviewed-on: #18
2026-09-23 00:48:23 +00:00
177 changed files with 21934 additions and 338 deletions
+19 -3
View File
@@ -20,15 +20,21 @@ concurrency:
cancel-in-progress: true cancel-in-progress: true
jobs: jobs:
# The upstream name in a new string literal, typically brought in by an # What an upstream merge can bring in or leave behind without a conflict:
# upstream merge. Seconds, and needs no toolchain. tools/fork/name-check.py. # the upstream name in a new string literal, and a changed upstream file
name-check: # without the AGPL 5(a) notice. Seconds, and needs no toolchain. The notice
# check diffs against the upstream snapshot branch, hence the full fetch.
fork-checks:
runs-on: light runs-on: light
container: container:
image: python:3.13-slim@sha256:8d9d0b8bcf6506481eae4907c18f5e3e7902e629f5f6d684f9e7c32e85e3ddf0 # 3.13-slim image: python:3.13-slim@sha256:8d9d0b8bcf6506481eae4907c18f5e3e7902e629f5f6d684f9e7c32e85e3ddf0 # 3.13-slim
steps: steps:
- uses: coffey-labs/actions/checkout@fab0c4d45e0162963965f1555df27b7bed5e20ec - uses: coffey-labs/actions/checkout@fab0c4d45e0162963965f1555df27b7bed5e20ec
with:
fetch-depth: 0
- run: python3 tools/fork/name-check.py - run: python3 tools/fork/name-check.py
- if: always()
run: python3 tools/fork/notice-check.py
build: build:
# Either runner (host1 or host2): the build needs no docker socket. # Either runner (host1 or host2): the build needs no docker socket.
@@ -61,6 +67,16 @@ jobs:
# --no-run: the workflow compiled every test target without running them, # --no-run: the workflow compiled every test target without running them,
# which catches a test that no longer builds without paying for the suite. # which catches a test that no longer builds without paying for the suite.
- run: cargo test --workspace --locked --no-run - run: cargo test --workspace --locked --no-run
# The release profile, on main only. It is the profile the image is
# built with, and it fails in ways the dev profile does not: v2026.9.24
# was tagged on a commit whose CI was green and whose release build
# could not compile the scim crate at all. A few minutes per merge is
# cheaper than finding that out from a tag, which throws away a
# multi-architecture build and leaves a version half-cut.
#
# Pull requests stay on the dev profile, where the wait is worth less.
- if: github.event_name == 'push'
run: cargo build -p inbuxa --locked --release
# Keep the cache from growing without bound: past 60 GB the target dir # Keep the cache from growing without bound: past 60 GB the target dir
# is dropped and the next build starts cold. The download cache stays. # is dropped and the next build starts cold. The download cache stays.
# Two builds (dev + test profiles) already fill ~22 GB, so the limit # Two builds (dev + test profiles) already fill ~22 GB, so the limit
+88 -1
View File
@@ -131,8 +131,95 @@ jobs:
except urllib.error.HTTPError as e: except urllib.error.HTTPError as e:
if e.code != 404: raise if e.code != 404: raise
image = f"{os.environ['REGISTRY']}/{os.environ['REPO']}:{version}" image = f"{os.environ['REGISTRY']}/{os.environ['REPO']}:{version}"
body = f"Container image: `{image}` (linux/amd64, linux/arm64); also `:latest`." body = (f"Container image: `{image}` (linux/amd64, linux/arm64); also `:latest`.\n\n"
"Binaries for a host install are attached: `inbuxa-linux-amd64.tar.gz` and "
"`inbuxa-linux-arm64.tar.gz`, with `SHA256SUMS`. Each is the binary out of this "
"release's image for that architecture, so it is the same build. The image "
"grants it `cap_net_bind_service`; a host install has to grant that itself "
"(`setcap`, or `AmbientCapabilities` in the unit) to bind port 25.")
data = json.dumps({"tag_name": tag, "name": f"INBUXA {version}", "body": body}).encode() data = json.dumps({"tag_name": tag, "name": f"INBUXA {version}", "body": body}).encode()
r = json.load(urllib.request.urlopen(urllib.request.Request(f"{api}/releases", data=data, headers=h))) r = json.load(urllib.request.urlopen(urllib.request.Request(f"{api}/releases", data=data, headers=h)))
print(f"created release {r['tag_name']}") print(f"created release {r['tag_name']}")
PY PY
# The binaries for a host install, taken out of the image that was just
# pushed rather than compiled again.
#
# Building them separately would mean a second Rust build per architecture
# -- the slowest thing this pipeline does -- and would leave two artifacts
# that are supposed to be the same build but only probably are. Extracting
# them makes that identity a fact: the binary in the tarball is the file
# the image runs.
#
# `docker create` does not start anything, so pulling an arm64 image on an
# amd64 runner and copying a file out of it needs no emulation.
binaries:
needs: [version, publish, release]
runs-on: docker
container:
image: docker:28-cli@sha256:625d9431a9f54c5a2bc90f24f0e1c3d55b1349fd857dd85035f98c2c9acbdd4d # 28-cli
volumes:
- /var/run/docker.sock:/var/run/docker.sock
env:
REGISTRY: ${{ vars.REGISTRY }}
IMAGE: ${{ vars.REGISTRY }}/${{ github.repository }}
VERSION: ${{ needs.version.outputs.version }}
TAG: ${{ github.ref_name }}
REPO: ${{ github.repository }}
PACKAGE_TOKEN: ${{ secrets.PACKAGE_TOKEN }}
TOKEN: ${{ secrets.GITHUB_TOKEN }}
steps:
- name: take the binaries out of the image
run: |
set -euo pipefail
echo "$PACKAGE_TOKEN" | docker login -u jcoffey-dev --password-stdin "$REGISTRY"
mkdir -p /out && cd /out
for arch in amd64 arm64; do
docker pull -q --platform "linux/$arch" "$IMAGE:$VERSION"
id="$(docker create --platform "linux/$arch" "$IMAGE:$VERSION")"
docker cp "$id:/usr/local/bin/inbuxa" "inbuxa"
docker rm -f "$id" >/dev/null
chmod 0755 inbuxa
tar -czf "inbuxa-linux-$arch.tar.gz" inbuxa
rm inbuxa
done
sha256sum inbuxa-linux-*.tar.gz > SHA256SUMS
cat SHA256SUMS
- name: attach them to the release
run: |
set -euo pipefail
apk add --no-cache -q python3
python3 - <<'PY'
import json, os, urllib.request, urllib.error, uuid, pathlib
api = f"{os.environ['CI_SERVER_INTERNAL']}/api/v1/repos/{os.environ['REPO']}"
tok = {"Authorization": f"token {os.environ['TOKEN']}"}
tag = os.environ["TAG"]
def get(path):
return json.load(urllib.request.urlopen(urllib.request.Request(api + path, headers=tok)))
rel = get(f"/releases/tags/{tag}")
assets = {a["name"]: a["id"] for a in get(f"/releases/{rel['id']}/assets")}
for path in ["/out/inbuxa-linux-amd64.tar.gz", "/out/inbuxa-linux-arm64.tar.gz", "/out/SHA256SUMS"]:
name = os.path.basename(path)
# A re-run of a tag replaces its assets rather than leaving two
# files with the same name and different contents.
if name in assets:
urllib.request.urlopen(urllib.request.Request(
f"{api}/releases/{rel['id']}/assets/{assets[name]}", headers=tok, method="DELETE"))
boundary = uuid.uuid4().hex
body = b"".join([
f"--{boundary}\r\nContent-Disposition: form-data; name=\"attachment\"; filename=\"{name}\"\r\n".encode(),
b"Content-Type: application/octet-stream\r\n\r\n",
pathlib.Path(path).read_bytes(),
f"\r\n--{boundary}--\r\n".encode(),
])
req = urllib.request.Request(
f"{api}/releases/{rel['id']}/assets?name={name}", data=body, method="POST",
headers={**tok, "Content-Type": f"multipart/form-data; boundary={boundary}"})
urllib.request.urlopen(req)
print("attached", name)
PY
- if: always()
run: docker logout "$REGISTRY" || true
Generated
+1 -2
View File
@@ -7958,8 +7958,6 @@ checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
[[package]] [[package]]
name = "sieve-rs" name = "sieve-rs"
version = "0.7.3" version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bd00a548fde57bd0c8e7c13ae65fc5fe30bd923ff8655c81e010f3f02a90997b"
dependencies = [ dependencies = [
"ahash", "ahash",
"arc-swap", "arc-swap",
@@ -8590,6 +8588,7 @@ dependencies = [
"mail-builder 1.0.0", "mail-builder 1.0.0",
"mail-parser", "mail-parser",
"managesieve", "managesieve",
"migration",
"nlp", "nlp",
"pop3", "pop3",
"quick-xml 0.41.0", "quick-xml 0.41.0",
+9
View File
@@ -1,5 +1,7 @@
[workspace] [workspace]
resolver = "2" resolver = "2"
# Vendored crates are patched in below, not built as members.
exclude = ["vendor"]
members = [ members = [
"crates/main", "crates/main",
"crates/types", "crates/types",
@@ -78,3 +80,10 @@ incremental = false
debug-assertions = false debug-assertions = false
overflow-checks = false overflow-checks = false
rpath = false rpath = false
# inbuxa: sieve-rs spells upstream's name into its Sieve extension names
# (vnd.stalwart.*), which scripts `require` and ManageSieve advertises.
# vendor/sieve-rs is the published 0.7.3 with those renamed; see its
# VENDORED.md. Re-vendor when the version in Cargo.lock moves.
[patch.crates-io]
sieve-rs = { path = "vendor/sieve-rs" }
+8 -7
View File
@@ -8,13 +8,13 @@
--- ---
**INBUXA** is a mail and collaboration server: JMAP, IMAP, POP3, SMTP, **inbuxa** is a mail and collaboration server: JMAP, IMAP, POP3, SMTP,
CalDAV, CardDAV and WebDAV, in one Rust binary, with ihasmail as its web front CalDAV, CardDAV and WebDAV, in one Rust binary, with ihasmail as its web front
end. It is a fork of [Stalwart](https://github.com/stalwartlabs/stalwart). end. It is a fork of [Stalwart](https://github.com/stalwartlabs/stalwart).
Project site: [inbuxa.org](https://inbuxa.org). Documentation: [docs.inbuxa.org](https://docs.inbuxa.org). Project site: [inbuxa.org](https://inbuxa.org). Documentation: [docs.inbuxa.org](https://docs.inbuxa.org).
Stalwart ships some features only in a paid Enterprise Edition: multi-tenancy, Stalwart ships some features only in a paid Enterprise Edition: multi-tenancy,
masked email, undelete and others. INBUXA ships everything to everybody under masked email, undelete and others. **inbuxa** ships everything to everybody under
the AGPL-3.0, rebuilding those features independently and without using any the AGPL-3.0, rebuilding those features independently and without using any
of Stalwart's Enterprise code. of Stalwart's Enterprise code.
@@ -46,25 +46,26 @@ docker build -t inbuxa . # or the container image
``` ```
Settings are read from `INBUXA_*` environment variables. An existing Stalwart Settings are read from `INBUXA_*` environment variables. An existing Stalwart
install's `STALWART_*` variables still work, with a warning to rename them. install's `STALWART_*` variables aren't read: the server stops at startup and
names each one to rename.
New installs keep their data in `/var/lib/inbuxa` and logs in New installs keep their data in `/var/lib/inbuxa` and logs in
`/var/log/inbuxa`. Existing installs keep the paths their configuration `/var/log/inbuxa`. Existing installs keep the paths their configuration
already names, so none of their data moves. already names, so none of their data moves.
## License and credits ## License and credits
INBUXA is free software under the [GNU Affero General Public License, **inbuxa** is free software under the [GNU Affero General Public License,
version 3](./LICENSES/AGPL-3.0-only.txt). version 3](./LICENSES/AGPL-3.0-only.txt).
It is a fork of Stalwart, copyright © Stalwart Labs LLC, **modified by It is a fork of Stalwart, copyright © Stalwart Labs LLC, **modified by
Coffey Labs in 2026**. Upstream's copyright notices are kept on every file Coffey Labs in 2026**. Upstream's copyright notices are kept on every file
they cover, and every upstream file this fork changed says so in its header, they cover, and every upstream file this fork changed says so in its header,
under the notice it came with. Stalwart's files are dual-licensed under the notice it came with. Stalwart's files are dual-licensed
AGPL-3.0-only or Stalwart's Enterprise License, and INBUXA takes them under AGPL-3.0-only or Stalwart's Enterprise License, and **inbuxa** takes them under
the AGPL-3.0 only. A few of those files also carry code from other projects the AGPL-3.0 only. A few of those files also carry code from other projects
under MIT or BSD licenses, which stays under those licenses; under MIT or BSD licenses, which stays under those licenses;
[THIRD-PARTY.md](./THIRD-PARTY.md) lists it with its notices. "Stalwart" is [THIRD-PARTY.md](./THIRD-PARTY.md) lists it with its notices. "Stalwart" is
Stalwart Labs' name. INBUXA isn't affiliated with or endorsed by Stalwart Stalwart Labs' name. **inbuxa** isn't affiliated with or endorsed by Stalwart
Labs. Labs.
The INBUXA mark reuses ihasmail's cat-and-envelope artwork. The **inbuxa** mark reuses ihasmail's cat-and-envelope artwork.
+1
View File
@@ -24,6 +24,7 @@ carry their own license files.
| `crates/common/src/network/acme/directory.rs`, `crates/common/src/network/acme/jose.rs`, `crates/common/src/network/acme/order.rs` | [rustls-acme](https://github.com/FlorianUekermann/rustls-acme) (MIT or Apache-2.0) | Copyright (c) Florian Uekermann | | `crates/common/src/network/acme/directory.rs`, `crates/common/src/network/acme/jose.rs`, `crates/common/src/network/acme/order.rs` | [rustls-acme](https://github.com/FlorianUekermann/rustls-acme) (MIT or Apache-2.0) | Copyright (c) Florian Uekermann |
| `crates/types/src/id.rs` | [crockford](https://github.com/archer884/crockford) (MIT or Apache-2.0) | Copyright (c) 2017 J/A <archer884@gmail.com> | | `crates/types/src/id.rs` | [crockford](https://github.com/archer884/crockford) (MIT or Apache-2.0) | Copyright (c) 2017 J/A <archer884@gmail.com> |
| `crates/nlp/src/tokenizers/types.rs` | test cases from [linkify](https://github.com/robinst/linkify) (MIT or Apache-2.0) | Copyright (c) 2017 Robin Stocker | | `crates/nlp/src/tokenizers/types.rs` | test cases from [linkify](https://github.com/robinst/linkify) (MIT or Apache-2.0) | Copyright (c) 2017 Robin Stocker |
| `tests/resources/smtp/antispam/spam-filter-rules.json.gz` | the published rules of [spam-filter](https://github.com/stalwartlabs/spam-filter) v3.0.2, unmodified, for the spam filter's tests (MIT or Apache-2.0) | Copyright (C) 2024, Stalwart Labs LLC |
Each notice above applies with this permission notice: Each notice above applies with this permission notice:
+7 -7
View File
@@ -1,8 +1,8 @@
openapi: 3.0.3 openapi: 3.0.3
info: info:
title: Stalwart Management API title: inbuxa Management API
description: | description: |
REST Management API for Stalwart server. These endpoints are helpers REST Management API for the inbuxa server. These endpoints are helpers
that complement the JMAP API — most of the server's configuration and data that complement the JMAP API — most of the server's configuration and data
is managed via JMAP (see `POST /jmap/`). The endpoints documented here cover is managed via JMAP (see `POST /jmap/`). The endpoints documented here cover
interactive login, account introspection, configuration schema retrieval and interactive login, account introspection, configuration schema retrieval and
@@ -12,11 +12,11 @@ info:
name: AGPL-3.0-only OR LicenseRef-SEL name: AGPL-3.0-only OR LicenseRef-SEL
servers: servers:
- url: https://{host} - url: https://{host}
description: Stalwart server description: inbuxa server
variables: variables:
host: host:
default: mail.example.com default: mail.example.com
description: The hostname of Stalwart server description: The hostname of the inbuxa server
security: security:
- bearerAuth: [] - bearerAuth: []
- basicAuth: [] - basicAuth: []
@@ -154,7 +154,7 @@ paths:
operationId: getSchema operationId: getSchema
summary: Return the configuration schema at a specific hash summary: Return the configuration schema at a specific hash
description: | description: |
Returns the JSON Schema describing the full Stalwart configuration tree. Returns the JSON Schema describing the full inbuxa configuration tree.
The response is always gzip-encoded (`Content-Encoding: gzip`) and served The response is always gzip-encoded (`Content-Encoding: gzip`) and served
with an immutable cache policy — the schema for a given hash never with an immutable cache policy — the schema for a given hash never
changes. If the hash does not match the server's current schema, the changes. If the hash does not match the server's current schema, the
@@ -183,7 +183,7 @@ paths:
application/json: application/json:
schema: schema:
type: object type: object
description: JSON Schema document describing Stalwart config description: JSON Schema document describing inbuxa config
additionalProperties: true additionalProperties: true
'302': '302':
description: Redirect to the current schema URL when the hash is stale description: Redirect to the current schema URL when the hash is stale
@@ -395,7 +395,7 @@ components:
WWW-Authenticate: WWW-Authenticate:
schema: schema:
type: string type: string
example: Bearer realm="Stalwart Server" example: Bearer realm="inbuxa Server"
content: content:
application/problem+json: application/problem+json:
schema: schema:
+23 -1
View File
@@ -143,7 +143,9 @@ impl Scripting {
.with_cpu_limit(trusted.max_cpu_cycles as usize) .with_cpu_limit(trusted.max_cpu_cycles as usize)
.with_max_nested_includes(trusted.max_nested_includes as usize) .with_max_nested_includes(trusted.max_nested_includes as usize)
.with_max_received_headers(trusted.max_received_headers as usize) .with_max_received_headers(trusted.max_received_headers as usize)
.with_default_duplicate_expiry(trusted.duplicate_expiry.into_inner().as_secs()); .with_default_duplicate_expiry(trusted.duplicate_expiry.into_inner().as_secs())
// inbuxa: without it, `environment "name"` answers sieve-rs's default
.with_env_variable("name", types::brand_server!());
trusted_runtime.set_local_hostname(local_hostname.clone()); trusted_runtime.set_local_hostname(local_hostname.clone());
untrusted_runtime.set_local_hostname(local_hostname); untrusted_runtime.set_local_hostname(local_hostname);
@@ -279,3 +281,23 @@ impl Clone for Scripting {
} }
} }
} }
#[cfg(test)]
mod tests {
use sieve::compiler::grammar::Capability;
// inbuxa: sieve-rs is vendored (vendor/sieve-rs) to carry the fork's
// name in its Sieve extensions. If Cargo.lock moves sieve-rs past the
// vendored version, Cargo drops the patch with only a warning and
// upstream's spelling comes back; this fails instead.
#[test]
fn sieve_extensions_carry_the_fork_name() {
for (capability, name) in [
(Capability::While, "vnd.inbuxa.while"),
(Capability::Expressions, "vnd.inbuxa.expressions"),
] {
assert_eq!(capability.to_string(), name);
assert_eq!(Capability::parse(name), capability);
}
}
}
+3 -3
View File
@@ -514,12 +514,12 @@ mod tests {
#[test] #[test]
fn index_is_rewritten_with_the_prefix_and_client_id() { fn index_is_rewritten_with_the_prefix_and_client_id() {
let meta = oauth_client_id_meta("stalwart-webui"); let meta = oauth_client_id_meta("inbuxa-webui");
let html = String::from_utf8(rewrite_index(INDEX, "admin", Some(&meta))).unwrap(); let html = String::from_utf8(rewrite_index(INDEX, "admin", Some(&meta))).unwrap();
assert!(html.contains("<base href=\"/admin/\" />"), "{html}"); assert!(html.contains("<base href=\"/admin/\" />"), "{html}");
assert!( assert!(
html.contains("<meta name=\"oauth-client-id\" content=\"stalwart-webui\" />"), html.contains("<meta name=\"oauth-client-id\" content=\"inbuxa-webui\" />"),
"{html}" "{html}"
); );
assert!(html.contains("<title>Portal</title>"), "{html}"); assert!(html.contains("<title>Portal</title>"), "{html}");
@@ -541,7 +541,7 @@ mod tests {
#[test] #[test]
fn index_without_a_placeholder_is_left_alone() { fn index_without_a_placeholder_is_left_alone() {
let bundle = "<head>\n <base href=\"/\" />\n</head>"; let bundle = "<head>\n <base href=\"/\" />\n</head>";
let meta = oauth_client_id_meta("stalwart-webui"); let meta = oauth_client_id_meta("inbuxa-webui");
let html = String::from_utf8(rewrite_index(bundle, "admin", Some(&meta))).unwrap(); let html = String::from_utf8(rewrite_index(bundle, "admin", Some(&meta))).unwrap();
assert_eq!(html, "<head>\n <base href=\"/admin/\" />\n</head>"); assert_eq!(html, "<head>\n <base href=\"/admin/\" />\n</head>");
+68 -10
View File
@@ -10,7 +10,7 @@
//! that ship with it are registered for it, on every start: //! that ship with it are registered for it, on every start:
//! //!
//! - the web interface the server serves itself (`Application`, `/admin` and //! - the web interface the server serves itself (`Application`, `/admin` and
//! `/account`), as its OAuth client id, `stalwart-webui` unless the //! `/account`), as its OAuth client id, `inbuxa-webui` unless the
//! application names another; //! application names another;
//! - INBUXA Admin hosted elsewhere, as `inbuxa-admin`, when `INBUXA_ADMIN_URL` //! - INBUXA Admin hosted elsewhere, as `inbuxa-admin`, when `INBUXA_ADMIN_URL`
//! is set; //! is set;
@@ -29,7 +29,7 @@ use directory::core::secret::{hash_secret, verify_secret_hash};
use registry::{ use registry::{
schema::{ schema::{
enums::{PasswordHashAlgorithm, ServiceProtocol}, enums::{PasswordHashAlgorithm, ServiceProtocol},
prelude::{ObjectType, Property, UTCDateTime}, prelude::{Object, ObjectInner, ObjectType, Property, UTCDateTime},
structs::{Application, OAuthClient, SystemSettings}, structs::{Application, OAuthClient, SystemSettings},
}, },
types::map::Map, types::map::Map,
@@ -40,9 +40,12 @@ use store::registry::{
}; };
/// The client id the upstream web interface uses when its application names none. /// The client id the upstream web interface uses when its application names none.
pub const WEB_INTERFACE_CLIENT_ID: &str = "stalwart-webui"; pub const WEB_INTERFACE_CLIENT_ID: &str = "inbuxa-webui";
pub const ADMIN_CLIENT_ID: &str = "inbuxa-admin"; pub const ADMIN_CLIENT_ID: &str = "inbuxa-admin";
pub const WEBMAIL_CLIENT_ID: &str = "ihasmail-inbuxa"; pub const WEBMAIL_CLIENT_ID: &str = "ihasmail-inbuxa";
/// The web interface's client id before the fork renamed it (SPEC §2.4).
/// Only ever read to retire it.
const LEGACY_WEB_INTERFACE_CLIENT_ID: &str = "stalwart-webui";
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
pub struct FirstPartyClient { pub struct FirstPartyClient {
@@ -102,7 +105,7 @@ pub fn first_party_clients(
if let Some(url) = admin_url.map(|url| url.trim().trim_end_matches('/')).filter(|url| !url.is_empty()) { if let Some(url) = admin_url.map(|url| url.trim().trim_end_matches('/')).filter(|url| !url.is_empty()) {
clients.push(FirstPartyClient { clients.push(FirstPartyClient {
client_id: ADMIN_CLIENT_ID.to_string(), client_id: ADMIN_CLIENT_ID.to_string(),
description: "INBUXA Admin".to_string(), description: "inbuxa Admin".to_string(),
redirect_uris: vec![format!("{url}/oauth/callback")], redirect_uris: vec![format!("{url}/oauth/callback")],
secret: None, secret: None,
}); });
@@ -187,6 +190,7 @@ fn env(name: &str) -> Option<String> {
} }
pub(crate) async fn ensure_first_party_clients(bp: &mut Bootstrap) -> trc::Result<()> { pub(crate) async fn ensure_first_party_clients(bp: &mut Bootstrap) -> trc::Result<()> {
retire_legacy_web_interface_client(bp).await?;
let system = bp.setting_infallible::<SystemSettings>().await; let system = bp.setting_infallible::<SystemSettings>().await;
let base_url = base_url(bp, &system); let base_url = base_url(bp, &system);
let applications = bp let applications = bp
@@ -213,6 +217,56 @@ pub(crate) async fn ensure_first_party_clients(bp: &mut Bootstrap) -> trc::Resul
Ok(()) Ok(())
} }
/// An install from before the rename, upstream's or this fork's, has the web
/// interface registered as `stalwart-webui`, and may
/// have an application naming it. The application is moved to the current id
/// and the old client removed, so the old id stops working rather than
/// living on as an alias; anyone signed in to the web interface signs in
/// again. Runs on every start and does nothing once both are gone.
async fn retire_legacy_web_interface_client(bp: &mut Bootstrap) -> trc::Result<()> {
for app in bp.list_infallible::<Application>().await {
if app.object.oauth_client_id.as_deref() != Some(LEGACY_WEB_INTERFACE_CLIENT_ID) {
continue;
}
let mut updated = app.object.clone();
updated.oauth_client_id = Some(WEB_INTERFACE_CLIENT_ID.to_string());
// The old object carries its revision: the write asserts on it.
let current = Object::with_revision(ObjectInner::from(app.object), app.revision);
let result = bp
.registry
.write(RegistryWrite::update(app.id.id(), &updated.into(), &current))
.await?;
if !matches!(result, RegistryWriteResult::Success(_)) {
return Err(trc::StoreEvent::UnexpectedError
.into_err()
.details("Failed to move an application to the renamed web interface client.")
.reason(result.to_string())
.caused_by(trc::location!()));
}
}
if let Some(object_id) = bp
.registry
.primary_key(
ObjectType::OAuthClient.into(),
Property::ClientId,
LEGACY_WEB_INTERFACE_CLIENT_ID.as_bytes().to_vec(),
)
.await?
{
let result = bp.registry.write(RegistryWrite::delete(object_id)).await?;
if !matches!(result, RegistryWriteResult::Success(_)) {
return Err(trc::StoreEvent::UnexpectedError
.into_err()
.details("Failed to remove the web interface's pre-rename OAuth client.")
.reason(result.to_string())
.caused_by(trc::location!()));
}
}
Ok(())
}
async fn ensure_client(bp: &mut Bootstrap, client: FirstPartyClient) -> trc::Result<()> { async fn ensure_client(bp: &mut Bootstrap, client: FirstPartyClient) -> trc::Result<()> {
let existing = match bp let existing = match bp
.registry .registry
@@ -223,15 +277,18 @@ async fn ensure_client(bp: &mut Bootstrap, client: FirstPartyClient) -> trc::Res
) )
.await? .await?
{ {
// inbuxa: read as an Object, keeping the revision the update below
// asserts on (a bare OAuthClient converts back with revision 0, which
// never matches, so any update failed start-up).
Some(object_id) => bp Some(object_id) => bp
.registry .registry
.object::<OAuthClient>(object_id.id()) .get(object_id)
.await? .await?
.map(|object| (object_id.id(), object)), .map(|object| (object_id.id(), object.revision, OAuthClient::from(object))),
None => None, None => None,
}; };
let result = if let Some((id, current)) = existing { let result = if let Some((id, revision, current)) = existing {
let mut updated = current.clone(); let mut updated = current.clone();
for uri in &client.redirect_uris { for uri in &client.redirect_uris {
if !updated.redirect_uris.contains(uri) { if !updated.redirect_uris.contains(uri) {
@@ -255,8 +312,9 @@ async fn ensure_client(bp: &mut Bootstrap, client: FirstPartyClient) -> trc::Res
if updated == current { if updated == current {
return Ok(()); return Ok(());
} }
let current = Object::with_revision(ObjectInner::from(current), revision);
bp.registry bp.registry
.write(RegistryWrite::update(id, &updated.into(), &current.into())) .write(RegistryWrite::update(id, &updated.into(), &current))
.await? .await?
} else { } else {
let secret = match &client.secret { let secret = match &client.secret {
@@ -298,7 +356,7 @@ mod tests {
fn web_interface() -> Application { fn web_interface() -> Application {
Application { Application {
description: "INBUXA Web Interface".to_string(), description: "inbuxa Web Interface".to_string(),
enabled: true, enabled: true,
url_prefix: Map::new(vec!["/admin".into(), "/account".into()]), url_prefix: Map::new(vec!["/admin".into(), "/account".into()]),
..Default::default() ..Default::default()
@@ -312,7 +370,7 @@ mod tests {
clients, clients,
vec![FirstPartyClient { vec![FirstPartyClient {
client_id: WEB_INTERFACE_CLIENT_ID.to_string(), client_id: WEB_INTERFACE_CLIENT_ID.to_string(),
description: "INBUXA Web Interface (served by this server)".to_string(), description: "inbuxa Web Interface (served by this server)".to_string(),
redirect_uris: vec![ redirect_uris: vec![
"https://mail.example.org/admin/oauth/callback".to_string(), "https://mail.example.org/admin/oauth/callback".to_string(),
"https://mail.example.org/account/oauth/callback".to_string(), "https://mail.example.org/account/oauth/callback".to_string(),
+2 -2
View File
@@ -23,8 +23,8 @@ pub mod defaults;
pub mod first_party; pub mod first_party;
pub mod restore; pub mod restore;
pub const SPAM_TRAINER_KEY: &[u8] = "STALWART_SPAM_TRAIN_DATA.lz4".as_bytes(); pub const SPAM_TRAINER_KEY: &[u8] = "INBUXA_SPAM_TRAIN_DATA.lz4".as_bytes();
pub const SPAM_CLASSIFIER_KEY: &[u8] = "STALWART_SPAM_CLASSIFIER_MODEL.lz4".as_bytes(); pub const SPAM_CLASSIFIER_KEY: &[u8] = "INBUXA_SPAM_CLASSIFIER_MODEL.lz4".as_bytes();
pub async fn fetch_resource( pub async fn fetch_resource(
url: &str, url: &str,
+15 -15
View File
@@ -299,28 +299,28 @@ impl LegacyProtocol {
pub fn refusal(&self, scope: RefusalScope) -> &'static str { pub fn refusal(&self, scope: RefusalScope) -> &'static str {
match (scope, self) { match (scope, self) {
(RefusalScope::Server, LegacyProtocol::Imap) => { (RefusalScope::Server, LegacyProtocol::Imap) => {
"This server allows only INBUXA webmail and JMAP apps. This mail app can't sign in." "This server allows only inbuxa webmail and JMAP apps. This mail app can't sign in."
} }
(RefusalScope::Server, LegacyProtocol::Pop3) => { (RefusalScope::Server, LegacyProtocol::Pop3) => {
"[AUTH] This server allows only INBUXA webmail and JMAP apps. This mail app can't sign in." "[AUTH] This server allows only inbuxa webmail and JMAP apps. This mail app can't sign in."
} }
(RefusalScope::Server, LegacyProtocol::ManageSieve) => { (RefusalScope::Server, LegacyProtocol::ManageSieve) => {
"This server allows only INBUXA webmail and JMAP apps." "This server allows only inbuxa webmail and JMAP apps."
} }
(RefusalScope::Server, LegacyProtocol::Submission) => { (RefusalScope::Server, LegacyProtocol::Submission) => {
"535 5.7.0 This server allows only INBUXA webmail and JMAP apps. This mail app can't send.\r\n" "535 5.7.0 This server allows only inbuxa webmail and JMAP apps. This mail app can't send.\r\n"
} }
(RefusalScope::Tenant(_), LegacyProtocol::Imap) => { (RefusalScope::Tenant(_), LegacyProtocol::Imap) => {
"Your organization allows only INBUXA webmail and JMAP apps. This mail app can't sign in." "Your organization allows only inbuxa webmail and JMAP apps. This mail app can't sign in."
} }
(RefusalScope::Tenant(_), LegacyProtocol::Pop3) => { (RefusalScope::Tenant(_), LegacyProtocol::Pop3) => {
"[AUTH] Your organization allows only INBUXA webmail and JMAP apps. This mail app can't sign in." "[AUTH] Your organization allows only inbuxa webmail and JMAP apps. This mail app can't sign in."
} }
(RefusalScope::Tenant(_), LegacyProtocol::ManageSieve) => { (RefusalScope::Tenant(_), LegacyProtocol::ManageSieve) => {
"Your organization allows only INBUXA webmail and JMAP apps." "Your organization allows only inbuxa webmail and JMAP apps."
} }
(RefusalScope::Tenant(_), LegacyProtocol::Submission) => { (RefusalScope::Tenant(_), LegacyProtocol::Submission) => {
"535 5.7.0 Your organization allows only INBUXA webmail and JMAP apps. This mail app can't send.\r\n" "535 5.7.0 Your organization allows only inbuxa webmail and JMAP apps. This mail app can't send.\r\n"
} }
} }
} }
@@ -541,7 +541,7 @@ mod tests {
let server = RefusalScope::Server; let server = RefusalScope::Server;
assert_eq!( assert_eq!(
LegacyProtocol::Imap.refusal(server), LegacyProtocol::Imap.refusal(server),
"This server allows only INBUXA webmail and JMAP apps. This mail app can't sign in." "This server allows only inbuxa webmail and JMAP apps. This mail app can't sign in."
); );
assert!( assert!(
LegacyProtocol::Pop3 LegacyProtocol::Pop3
@@ -550,11 +550,11 @@ mod tests {
); );
assert_eq!( assert_eq!(
LegacyProtocol::ManageSieve.refusal(server), LegacyProtocol::ManageSieve.refusal(server),
"This server allows only INBUXA webmail and JMAP apps." "This server allows only inbuxa webmail and JMAP apps."
); );
assert_eq!( assert_eq!(
LegacyProtocol::Submission.refusal(server), LegacyProtocol::Submission.refusal(server),
"535 5.7.0 This server allows only INBUXA webmail and JMAP apps. This mail app can't send.\r\n" "535 5.7.0 This server allows only inbuxa webmail and JMAP apps. This mail app can't send.\r\n"
); );
} }
@@ -564,19 +564,19 @@ mod tests {
let tenant = RefusalScope::Tenant(7); let tenant = RefusalScope::Tenant(7);
assert_eq!( assert_eq!(
LegacyProtocol::Imap.refusal(tenant), LegacyProtocol::Imap.refusal(tenant),
"Your organization allows only INBUXA webmail and JMAP apps. This mail app can't sign in." "Your organization allows only inbuxa webmail and JMAP apps. This mail app can't sign in."
); );
assert_eq!( assert_eq!(
LegacyProtocol::Pop3.refusal(tenant), LegacyProtocol::Pop3.refusal(tenant),
"[AUTH] Your organization allows only INBUXA webmail and JMAP apps. This mail app can't sign in." "[AUTH] Your organization allows only inbuxa webmail and JMAP apps. This mail app can't sign in."
); );
assert_eq!( assert_eq!(
LegacyProtocol::ManageSieve.refusal(tenant), LegacyProtocol::ManageSieve.refusal(tenant),
"Your organization allows only INBUXA webmail and JMAP apps." "Your organization allows only inbuxa webmail and JMAP apps."
); );
assert_eq!( assert_eq!(
LegacyProtocol::Submission.refusal(tenant), LegacyProtocol::Submission.refusal(tenant),
"535 5.7.0 Your organization allows only INBUXA webmail and JMAP apps. This mail app can't send.\r\n" "535 5.7.0 Your organization allows only inbuxa webmail and JMAP apps. This mail app can't send.\r\n"
); );
let err = LegacyProtocol::Imap.refused(tenant, Some("example.org".into())); let err = LegacyProtocol::Imap.refused(tenant, Some("example.org".into()));
assert_eq!(err.value_as_str(trc::Key::Policy), Some("tenant")); assert_eq!(err.value_as_str(trc::Key::Policy), Some("tenant"));
+10 -8
View File
@@ -2,6 +2,8 @@
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]> * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
* *
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*
* Modified by Coffey Labs in 2026 for INBUXA.
*/ */
use base64::{Engine, engine::general_purpose::URL_SAFE_NO_PAD}; use base64::{Engine, engine::general_purpose::URL_SAFE_NO_PAD};
@@ -313,16 +315,16 @@ B4yDfR2rGOd2H6Kv3fQNHPj9Nu5Tks8QYMLzrX8ONCNoFnNUQl9S0r0QS6phVqD0
#[test] #[test]
fn contact_is_normalized_to_a_uri() { fn contact_is_normalized_to_a_uri() {
for (input, expected) in [ for (input, expected) in [
("hello@stalw.art", Some("mailto:hello@stalw.art")), ("hello@example.org", Some("mailto:hello@example.org")),
(" hello@stalw.art ", Some("mailto:hello@stalw.art")), (" hello@example.org ", Some("mailto:hello@example.org")),
("mailto:hello@stalw.art", Some("mailto:hello@stalw.art")), ("mailto:hello@example.org", Some("mailto:hello@example.org")),
("MAILTO:hello@stalw.art", Some("MAILTO:hello@stalw.art")), ("MAILTO:hello@example.org", Some("MAILTO:hello@example.org")),
( (
"https://stalw.art/contact", "https://example.org/contact",
Some("https://stalw.art/contact"), Some("https://example.org/contact"),
), ),
("stalw.art", None), ("example.org", None),
("http://stalw.art", None), ("http://example.org", None),
("tel:+123456789", None), ("tel:+123456789", None),
("", None), ("", None),
] { ] {
+3 -1
View File
@@ -2,6 +2,8 @@
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]> * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
* *
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*
* Modified by Coffey Labs in 2026 for INBUXA.
*/ */
use super::ETag; use super::ETag;
@@ -490,7 +492,7 @@ impl LockRequestHandler for Server {
for cond in &if_.list { for cond in &if_.list {
match cond { match cond {
Condition::StateToken { token, .. } => { Condition::StateToken { token, .. } => {
if token.starts_with("urn:stalwart:davsync:") { if token.starts_with("urn:inbuxa:davsync:") {
needs_sync_token = true; needs_sync_token = true;
} else { } else {
needs_lock_token = true; needs_lock_token = true;
+7 -5
View File
@@ -2,6 +2,8 @@
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]> * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
* *
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*
* Modified by Coffey Labs in 2026 for INBUXA.
*/ */
use crate::{DavError, DavResourceName}; use crate::{DavError, DavResourceName};
@@ -181,12 +183,12 @@ impl OwnedUri<'_> {
impl Urn { impl Urn {
pub fn try_extract_sync_id(token: &str) -> Option<&str> { pub fn try_extract_sync_id(token: &str) -> Option<&str> {
token token
.strip_prefix("urn:stalwart:davsync:") .strip_prefix("urn:inbuxa:davsync:")
.map(|x| x.split_once(':').map(|(x, _)| x).unwrap_or(x)) .map(|x| x.split_once(':').map(|(x, _)| x).unwrap_or(x))
} }
pub fn parse(input: &str) -> Option<Self> { pub fn parse(input: &str) -> Option<Self> {
let inbox = input.strip_prefix("urn:stalwart:")?; let inbox = input.strip_prefix("urn:inbuxa:")?;
let (kind, id) = inbox.split_once(':')?; let (kind, id) = inbox.split_once(':')?;
match kind { match kind {
"davlock" => u64::from_str_radix(id, 16).ok().map(Urn::Lock), "davlock" => u64::from_str_radix(id, 16).ok().map(Urn::Lock),
@@ -223,12 +225,12 @@ impl Urn {
impl Display for Urn { impl Display for Urn {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self { match self {
Urn::Lock(id) => write!(f, "urn:stalwart:davlock:{id:x}",), Urn::Lock(id) => write!(f, "urn:inbuxa:davlock:{id:x}",),
Urn::Sync { id, seq } => { Urn::Sync { id, seq } => {
if *seq == 0 { if *seq == 0 {
write!(f, "urn:stalwart:davsync:{id:x}") write!(f, "urn:inbuxa:davsync:{id:x}")
} else { } else {
write!(f, "urn:stalwart:davsync:{id:x}:{seq:x}") write!(f, "urn:inbuxa:davsync:{id:x}:{seq:x}")
} }
} }
} }
+1 -1
View File
@@ -40,7 +40,7 @@ impl OpenIdDirectory {
pub async fn new(config: OidcConfig) -> Result<Self, OidcError> { pub async fn new(config: OidcConfig) -> Result<Self, OidcError> {
let http = utils::http::http_client_builder(false) let http = utils::http::http_client_builder(false)
.user_agent("INBUXA/1.0") // types::brand!(); this crate does not depend on types .user_agent("inbuxa/1.0") // types::brand!(); this crate does not depend on types
.timeout(Duration::from_secs(30)) .timeout(Duration::from_secs(30))
.build() .build()
.map_err(|e| OidcError::Network(format!("HTTP client build failed: {e}")))?; .map_err(|e| OidcError::Network(format!("HTTP client build failed: {e}")))?;
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "inbuxa-features" name = "inbuxa-features"
description = "INBUXA's rebuilt features: behavior Stalwart ships only in its Enterprise Edition, rebuilt clean-room" description = "inbuxa's rebuilt features: behavior Stalwart ships only in its Enterprise Edition, rebuilt clean-room"
license = "AGPL-3.0-only" license = "AGPL-3.0-only"
version = "0.16.22" version = "0.16.22"
edition = "2024" edition = "2024"
+3 -3
View File
@@ -91,7 +91,7 @@ pub enum Capability {
FileNode = 1 << 15, FileNode = 1 << 15,
#[serde(rename(serialize = "urn:ietf:params:jmap:mail:share"))] #[serde(rename(serialize = "urn:ietf:params:jmap:mail:share"))]
MailShare = 1 << 16, MailShare = 1 << 16,
#[serde(rename(serialize = "urn:stalwart:jmap"))] #[serde(rename(serialize = "urn:inbuxa:jmap:registry"))]
Stalwart = 1 << 17, Stalwart = 1 << 17,
#[serde(rename(serialize = "urn:ietf:params:jmap:webpush-vapid"))] #[serde(rename(serialize = "urn:ietf:params:jmap:webpush-vapid"))]
WebPushVapid = 1 << 18, WebPushVapid = 1 << 18,
@@ -353,7 +353,7 @@ impl Capability {
Capability::PrincipalsAvailability => "urn:ietf:params:jmap:principals:availability", Capability::PrincipalsAvailability => "urn:ietf:params:jmap:principals:availability",
Capability::FileNode => "urn:ietf:params:jmap:filenode", Capability::FileNode => "urn:ietf:params:jmap:filenode",
Capability::MailShare => "urn:ietf:params:jmap:mail:share", Capability::MailShare => "urn:ietf:params:jmap:mail:share",
Capability::Stalwart => "urn:stalwart:jmap", Capability::Stalwart => "urn:inbuxa:jmap:registry",
Capability::WebPushVapid => "urn:ietf:params:jmap:webpush-vapid", Capability::WebPushVapid => "urn:ietf:params:jmap:webpush-vapid",
Capability::EmailPush => "urn:ietf:params:jmap:emailpush", Capability::EmailPush => "urn:ietf:params:jmap:emailpush",
Capability::Inbuxa => "urn:inbuxa:jmap", Capability::Inbuxa => "urn:inbuxa:jmap",
@@ -501,7 +501,7 @@ impl Capability {
"urn:ietf:params:jmap:contacts:parse" => Capability::ContactsParse, "urn:ietf:params:jmap:contacts:parse" => Capability::ContactsParse,
"urn:ietf:params:jmap:calendars:parse" => Capability::CalendarsParse, "urn:ietf:params:jmap:calendars:parse" => Capability::CalendarsParse,
"urn:ietf:params:jmap:mail:share" => Capability::MailShare, "urn:ietf:params:jmap:mail:share" => Capability::MailShare,
"urn:stalwart:jmap" => Capability::Stalwart, "urn:inbuxa:jmap:registry" => Capability::Stalwart,
"urn:ietf:params:jmap:webpush-vapid" => Capability::WebPushVapid, "urn:ietf:params:jmap:webpush-vapid" => Capability::WebPushVapid,
"urn:ietf:params:jmap:emailpush" => Capability::EmailPush, "urn:ietf:params:jmap:emailpush" => Capability::EmailPush,
"urn:inbuxa:jmap" => Capability::Inbuxa, "urn:inbuxa:jmap" => Capability::Inbuxa,
@@ -208,7 +208,7 @@ pub(crate) async fn bootstrap_set(
.with_description(concat!( .with_description(concat!(
"The selected data store contains information from an older version. ", "The selected data store contains information from an older version. ",
"Please follow the upgrade instructions at ", "Please follow the upgrade instructions at ",
"https://github.com/stalwartlabs/stalwart/blob/main/UPGRADING/v0_16.md" "https://docs.inbuxa.org/install/migrating/"
)), )),
); );
break; break;
@@ -679,7 +679,7 @@ fn build_default_bootstrap(server: &Server) -> Bootstrap {
directory: DirectoryBootstrap::Internal, directory: DirectoryBootstrap::Internal,
tracer: Tracer::Log(TracerLog { tracer: Tracer::Log(TracerLog {
path: "/var/log/inbuxa/".to_string(), path: "/var/log/inbuxa/".to_string(),
prefix: "stalwart".to_string(), prefix: "inbuxa".to_string(),
ansi: true, ansi: true,
enable: true, enable: true,
..Default::default() ..Default::default()
+2 -2
View File
@@ -1,11 +1,11 @@
[package] [package]
name = "inbuxa" name = "inbuxa"
description = "INBUXA Mail and Collaboration Server, a fork of Stalwart" description = "inbuxa mail and collaboration server, a fork of Stalwart"
authors = [ "Stalwart Labs LLC <[email protected]>"] authors = [ "Stalwart Labs LLC <[email protected]>"]
homepage = "https://inbuxa.org" homepage = "https://inbuxa.org"
keywords = ["imap", "jmap", "smtp", "email", "mail", "webdav", "server"] keywords = ["imap", "jmap", "smtp", "email", "mail", "webdav", "server"]
categories = ["email"] categories = ["email"]
# Upstream offers AGPL-3.0-only OR LicenseRef-SEL; INBUXA takes the AGPL only. # Upstream offers AGPL-3.0-only OR LicenseRef-SEL; inbuxa takes the AGPL only.
license = "AGPL-3.0-only" license = "AGPL-3.0-only"
version = "0.16.23" version = "0.16.23"
edition = "2024" edition = "2024"
+53 -3
View File
@@ -2,6 +2,8 @@
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]> * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
* *
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*
* Modified by Coffey Labs in 2026 for INBUXA.
*/ */
#![warn(clippy::large_futures)] #![warn(clippy::large_futures)]
@@ -19,6 +21,10 @@ pub mod destroy;
pub mod v016; pub mod v016;
pub async fn try_migrate(server: &Server) -> trc::Result<()> { pub async fn try_migrate(server: &Server) -> trc::Result<()> {
// inbuxa: before the version check, which returns early on a current
// store, and before migrate_v0_16, which reads the renamed key.
rename_spam_blobs(server).await?;
match server match server
.store() .store()
.get_value::<u32>(AnyKey { .get_value::<u32>(AnyKey {
@@ -36,14 +42,14 @@ pub async fn try_migrate(server: &Server) -> trc::Result<()> {
Some(0..=4) => { Some(0..=4) => {
abort(concat!( abort(concat!(
"You must first upgrade to version 0.15, please read ", "You must first upgrade to version 0.15, please read ",
"https://github.com/stalwartlabs/stalwart/blob/main/UPGRADING/v0_16.md" "https://docs.inbuxa.org/install/migrating/"
)); ));
} }
Some(5) => { Some(5) => {
if !server.registry().is_recovery_mode() { if !server.registry().is_recovery_mode() {
abort(concat!( abort(concat!(
"Upgrading to version 0.16 is a multi-step process, please read ", "Upgrading to version 0.16 is a multi-step process, please read ",
"https://github.com/stalwartlabs/stalwart/blob/main/UPGRADING/v0_16.md" "https://docs.inbuxa.org/install/migrating/"
)); ));
} }
} }
@@ -61,7 +67,7 @@ pub async fn try_migrate(server: &Server) -> trc::Result<()> {
} else { } else {
abort(concat!( abort(concat!(
"You must first upgrade to version 0.15, please read ", "You must first upgrade to version 0.15, please read ",
"https://github.com/stalwartlabs/stalwart/blob/main/UPGRADING/v0_16.md" "https://docs.inbuxa.org/install/migrating/"
)); ));
} }
} }
@@ -134,3 +140,47 @@ async fn is_new_install(server: &Server) -> trc::Result<bool> {
Ok(true) Ok(true)
} }
/// inbuxa: the spam filter's trainer and model blobs, under the names they
/// had before the fork renamed them (SPEC §2.4), paired with the current ones.
const RENAMED_SPAM_BLOBS: [(&[u8], &[u8]); 2] = [
(b"STALWART_SPAM_TRAIN_DATA.lz4", common::manager::SPAM_TRAINER_KEY),
(
b"STALWART_SPAM_CLASSIFIER_MODEL.lz4",
common::manager::SPAM_CLASSIFIER_KEY,
),
];
/// Moves each spam blob from its pre-rename key to the current one, so a
/// trained model survives the rename. A blob already under the current key
/// wins and the old one is just removed; with neither, nothing happens.
async fn rename_spam_blobs(server: &Server) -> trc::Result<()> {
let blobs = server.blob_store();
for (old, new) in RENAMED_SPAM_BLOBS {
let Some(data) = blobs
.get_blob(old, 0..usize::MAX)
.await
.caused_by(trc::location!())?
else {
continue;
};
if blobs
.get_blob(new, 0..usize::MAX)
.await
.caused_by(trc::location!())?
.is_none()
{
blobs
.put_blob(new, &data, server.core.email.compression)
.await
.caused_by(trc::location!())?;
}
blobs.delete_blob(old).await.caused_by(trc::location!())?;
trc::event!(
Server(trc::ServerEvent::Startup),
Details = "Moved a spam filter blob to its renamed key",
Key = new,
);
}
Ok(())
}
+6 -4
View File
@@ -2,6 +2,8 @@
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art> * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
* *
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*
* Modified by Coffey Labs in 2026 for INBUXA.
*/ */
// This file is auto-generated. Do not edit directly. // This file is auto-generated. Do not edit directly.
@@ -10372,8 +10374,8 @@ impl EnumImpl for SieveCapability {
b"spamtest" => SieveCapability::Spamtest, b"spamtest" => SieveCapability::Spamtest,
b"spamtestplus" => SieveCapability::Spamtestplus, b"spamtestplus" => SieveCapability::Spamtestplus,
b"virustest" => SieveCapability::Virustest, b"virustest" => SieveCapability::Virustest,
b"vnd.stalwart.while" => SieveCapability::VndStalwartWhile, b"vnd.inbuxa.while" => SieveCapability::VndStalwartWhile,
b"vnd.stalwart.expressions" => SieveCapability::VndStalwartExpressions, b"vnd.inbuxa.expressions" => SieveCapability::VndStalwartExpressions,
} }
} }
@@ -10426,8 +10428,8 @@ impl EnumImpl for SieveCapability {
SieveCapability::Spamtest => "spamtest", SieveCapability::Spamtest => "spamtest",
SieveCapability::Spamtestplus => "spamtestplus", SieveCapability::Spamtestplus => "spamtestplus",
SieveCapability::Virustest => "virustest", SieveCapability::Virustest => "virustest",
SieveCapability::VndStalwartWhile => "vnd.stalwart.while", SieveCapability::VndStalwartWhile => "vnd.inbuxa.while",
SieveCapability::VndStalwartExpressions => "vnd.stalwart.expressions", SieveCapability::VndStalwartExpressions => "vnd.inbuxa.expressions",
} }
} }
+17 -17
View File
@@ -732,7 +732,7 @@ impl Pickle for AddressBook {
impl Default for AddressBook { impl Default for AddressBook {
fn default() -> Self { fn default() -> Self {
Self { Self {
default_display_name: Some("INBUXA Address Book".to_string()), default_display_name: Some("inbuxa Address Book".to_string()),
default_href_name: Some("default".to_string()), default_href_name: Some("default".to_string()),
max_v_card_size: 524288u64, max_v_card_size: 524288u64,
max_address_books: Some(250u64), max_address_books: Some(250u64),
@@ -4597,7 +4597,7 @@ impl Pickle for Calendar {
impl Default for Calendar { impl Default for Calendar {
fn default() -> Self { fn default() -> Self {
Self { Self {
default_display_name: Some("INBUXA Calendar".to_string()), default_display_name: Some("inbuxa Calendar".to_string()),
default_href_name: Some("default".to_string()), default_href_name: Some("default".to_string()),
max_attendees: 20u64, max_attendees: 20u64,
max_recurrence_expansions: 3000u64, max_recurrence_expansions: 3000u64,
@@ -4734,7 +4734,7 @@ impl Default for CalendarAlarm {
allow_external_rcpts: false, allow_external_rcpts: false,
enable: true, enable: true,
from_email: Default::default(), from_email: Default::default(),
from_name: "INBUXA Calendar".to_string(), from_name: "inbuxa Calendar".to_string(),
min_trigger_interval: Duration::from_millis(3600000), min_trigger_interval: Duration::from_millis(3600000),
template: Default::default(), template: Default::default(),
} }
@@ -28310,7 +28310,7 @@ impl MtaStageConnect {
ExpressionContext { ExpressionContext {
expr: &self.smtp_greeting, expr: &self.smtp_greeting,
default: Some(Expression { default: Some(Expression {
else_: "system('hostname') + ' INBUXA ESMTP at your service'".to_string(), else_: "system('hostname') + ' inbuxa ESMTP at your service'".to_string(),
..Default::default() ..Default::default()
}), }),
property: Property::SmtpGreeting, property: Property::SmtpGreeting,
@@ -28374,7 +28374,7 @@ impl Default for MtaStageConnect {
fn default() -> Self { fn default() -> Self {
Self { Self {
smtp_greeting: Expression { smtp_greeting: Expression {
else_: "system('hostname') + ' INBUXA ESMTP at your service'".to_string(), else_: "system('hostname') + ' inbuxa ESMTP at your service'".to_string(),
..Default::default() ..Default::default()
}, },
hostname: Expression { hostname: Expression {
@@ -29622,8 +29622,8 @@ impl Default for MySqlSettings {
Self { Self {
host: Default::default(), host: Default::default(),
port: 3306u64, port: 3306u64,
database: "stalwart".to_string(), database: "inbuxa".to_string(),
auth_username: Some("stalwart".to_string()), auth_username: Some("inbuxa".to_string()),
auth_secret: Default::default(), auth_secret: Default::default(),
} }
} }
@@ -29780,8 +29780,8 @@ impl Default for MySqlStore {
read_replicas: Default::default(), read_replicas: Default::default(),
host: Default::default(), host: Default::default(),
port: 3306u64, port: 3306u64,
database: "stalwart".to_string(), database: "inbuxa".to_string(),
auth_username: Some("stalwart".to_string()), auth_username: Some("inbuxa".to_string()),
auth_secret: Default::default(), auth_secret: Default::default(),
} }
} }
@@ -29942,7 +29942,7 @@ impl Default for NatsCoordinator {
no_echo: true, no_echo: true,
use_tls: false, use_tls: false,
auth_secret: Default::default(), auth_secret: Default::default(),
auth_username: Some("stalwart".to_string()), auth_username: Some("inbuxa".to_string()),
credentials: Default::default(), credentials: Default::default(),
} }
} }
@@ -31125,8 +31125,8 @@ impl Default for PostgreSqlSettings {
Self { Self {
host: Default::default(), host: Default::default(),
port: 5432u64, port: 5432u64,
database: "stalwart".to_string(), database: "inbuxa".to_string(),
auth_username: Some("stalwart".to_string()), auth_username: Some("inbuxa".to_string()),
auth_secret: Default::default(), auth_secret: Default::default(),
options: Default::default(), options: Default::default(),
} }
@@ -31270,8 +31270,8 @@ impl Default for PostgreSqlStore {
read_replicas: Default::default(), read_replicas: Default::default(),
host: Default::default(), host: Default::default(),
port: 5432u64, port: 5432u64,
database: "stalwart".to_string(), database: "inbuxa".to_string(),
auth_username: Some("stalwart".to_string()), auth_username: Some("inbuxa".to_string()),
auth_secret: Default::default(), auth_secret: Default::default(),
options: Default::default(), options: Default::default(),
} }
@@ -32576,7 +32576,7 @@ impl Default for RedisClusterStore {
Self { Self {
urls: Map::new(vec!["redis://127.0.0.1".to_string()]), urls: Map::new(vec!["redis://127.0.0.1".to_string()]),
timeout: Duration::from_millis(10000), timeout: Duration::from_millis(10000),
auth_username: Some("stalwart".to_string()), auth_username: Some("inbuxa".to_string()),
auth_secret: Default::default(), auth_secret: Default::default(),
max_retry_wait: Default::default(), max_retry_wait: Default::default(),
min_retry_wait: Default::default(), min_retry_wait: Default::default(),
@@ -32743,7 +32743,7 @@ impl Default for RedisSentinelStore {
urls: Map::new(vec!["redis://127.0.0.1:26379".to_string()]), urls: Map::new(vec!["redis://127.0.0.1:26379".to_string()]),
service_name: "mymaster".to_string(), service_name: "mymaster".to_string(),
timeout: Duration::from_millis(10000), timeout: Duration::from_millis(10000),
auth_username: Some("stalwart".to_string()), auth_username: Some("inbuxa".to_string()),
auth_secret: Default::default(), auth_secret: Default::default(),
sentinel_username: Default::default(), sentinel_username: Default::default(),
sentinel_secret: Default::default(), sentinel_secret: Default::default(),
@@ -46308,7 +46308,7 @@ impl Default for TracerLog {
fn default() -> Self { fn default() -> Self {
Self { Self {
path: Default::default(), path: Default::default(),
prefix: "stalwart".to_string(), prefix: "inbuxa".to_string(),
rotate: LogRotateFrequency::Daily, rotate: LogRotateFrequency::Daily,
ansi: true, ansi: true,
multiline: false, multiline: false,
+9 -1
View File
@@ -4,6 +4,14 @@
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
// The release profile computes the layout of this crate's async fn bodies in
// one go, and the deepest of them -- writable_domain, which awaits through
// the directory, the store and the JMAP registry -- takes rustc past its
// default query depth. The dev profile does not get that far, so the failure
// only appears in a release build: CI was green and the tag that started a
// release was not.
#![recursion_limit = "256"]
//! SCIM 2.0 provisioning (`docs/spec/features/scim.md`). inbuxa-server is the //! SCIM 2.0 provisioning (`docs/spec/features/scim.md`). inbuxa-server is the
//! service provider: an identity provider pushes users and groups to //! service provider: an identity provider pushes users and groups to
//! `/scim/v2`, and each request becomes the same `x:Account` reads and //! `/scim/v2`, and each request becomes the same `x:Account` reads and
@@ -205,7 +213,7 @@ impl ScimResponse {
if error.status == 401 { if error.status == 401 {
response.headers.push(( response.headers.push((
"WWW-Authenticate", "WWW-Authenticate",
"Bearer realm=\"INBUXA SCIM\"".to_string(), "Bearer realm=\"inbuxa SCIM\"".to_string(),
)); ));
} }
response response
+2 -3
View File
@@ -1,9 +1,8 @@
[package] [package]
name = "smtp" name = "smtp"
description = "Stalwart SMTP Server" description = "inbuxa SMTP server"
authors = [ "Stalwart Labs LLC <[email protected]>"] authors = [ "Stalwart Labs LLC <[email protected]>"]
repository = "https://github.com/stalwartlabs/smtp-server" homepage = "https://inbuxa.org"
homepage = "https://stalw.art/smtp"
keywords = ["smtp", "email", "mail", "server"] keywords = ["smtp", "email", "mail", "server"]
categories = ["email"] categories = ["email"]
license = "AGPL-3.0-only OR LicenseRef-SEL" license = "AGPL-3.0-only OR LicenseRef-SEL"
+34 -27
View File
@@ -2,6 +2,8 @@
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]> * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
* *
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*
* Modified by Coffey Labs in 2026 for INBUXA.
*/ */
use common::config::mailstore::spamfilter::PyzorConfig; use common::config::mailstore::spamfilter::PyzorConfig;
@@ -43,35 +45,14 @@ pub(crate) async fn pyzor_check(
// Hash message // Hash message
let request = message.pyzor_check_message(); let request = message.pyzor_check_message();
// Send message to address. inbuxa: in tests, a fixed table answers
// instead of a public server (test_response).
#[cfg(not(feature = "test_mode"))]
let response = pyzor_send_message(config.address, config.timeout, &request).await;
#[cfg(feature = "test_mode")] #[cfg(feature = "test_mode")]
{ let response = std::io::Result::Ok(test_response(&request));
if request.contains("b5b476f0b5ba6e1c038361d3ded5818dd39c90a2") {
return Ok(PyzorResponse {
code: 200,
count: 1000,
wl_count: 0,
}
.into());
} else if request.contains("d67d4b8bfc3860449e3418bb6017e2612f3e2a99") {
return Ok(PyzorResponse {
code: 200,
count: 60,
wl_count: 10,
}
.into());
} else if request.contains("81763547012b75e57a20d18ce0b93014208cdfdb") {
return Ok(PyzorResponse {
code: 200,
count: 50,
wl_count: 20,
}
.into());
}
}
// Send message to address response
pyzor_send_message(config.address, config.timeout, &request)
.await
.map(Into::into) .map(Into::into)
.map_err(|err| { .map_err(|err| {
trc::SpamEvent::PyzorError trc::SpamEvent::PyzorError
@@ -82,6 +63,32 @@ pub(crate) async fn pyzor_check(
}) })
} }
/// inbuxa: the answers tests get, by digest, instead of a public server's,
/// whose counts change and which a test may not be able to reach. Upstream
/// answered the first three here and sent every other digest to the network.
#[cfg(feature = "test_mode")]
fn test_response(request: &str) -> PyzorResponse {
let (count, wl_count) = if request.contains("b5b476f0b5ba6e1c038361d3ded5818dd39c90a2")
// The digest of an empty body, as an HTML-only message with no text
// to hash produces; public servers report it widely.
|| request.contains("da39a3ee5e6b4b0d3255bfef95601890afd80709")
{
(1000, 0)
} else if request.contains("d67d4b8bfc3860449e3418bb6017e2612f3e2a99") {
(60, 10)
} else if request.contains("81763547012b75e57a20d18ce0b93014208cdfdb") {
(50, 20)
} else {
(0, 0)
};
PyzorResponse {
code: 200,
count,
wl_count,
}
}
#[cfg_attr(feature = "test_mode", allow(dead_code))]
async fn pyzor_send_message( async fn pyzor_send_message(
addr: SocketAddr, addr: SocketAddr,
timeout: Duration, timeout: Duration,
+2 -2
View File
@@ -27,7 +27,7 @@ impl RegistryStore {
// variable, so it's ignored there, and loudly. // variable, so it's ignored there, and loudly.
if !inner.env_recovery_mode && inner.env_recovery_admin.take().is_some() { if !inner.env_recovery_mode && inner.env_recovery_admin.take().is_some() {
eprintln!(); eprintln!();
eprintln!("⚠️ INBUXA_RECOVERY_ADMIN (or STALWART_RECOVERY_ADMIN) is set, but the"); eprintln!("⚠️ INBUXA_RECOVERY_ADMIN is set, but the");
eprintln!(" server is configured and not in recovery mode, so it is ignored."); eprintln!(" server is configured and not in recovery mode, so it is ignored.");
eprintln!(" Remove it from the environment. To use it for recovery, also set"); eprintln!(" Remove it from the environment. To use it for recovery, also set");
eprintln!(" INBUXA_RECOVERY_MODE=1."); eprintln!(" INBUXA_RECOVERY_MODE=1.");
@@ -47,7 +47,7 @@ impl RegistryStore {
.collect::<String>(); .collect::<String>();
eprintln!(); eprintln!();
eprintln!("════════════════════════════════════════════════════════════"); eprintln!("════════════════════════════════════════════════════════════");
eprintln!("🔑 INBUXA bootstrap mode - temporary administrator account"); eprintln!("🔑 inbuxa bootstrap mode - temporary administrator account");
eprintln!(); eprintln!();
eprintln!(" username: admin"); eprintln!(" username: admin");
eprintln!(" password: {password}"); eprintln!(" password: {password}");
+3 -3
View File
@@ -3729,8 +3729,8 @@ impl EventType {
EventType::Security(SecurityEvent::LegacyProtocolsChanged) => { EventType::Security(SecurityEvent::LegacyProtocolsChanged) => {
"Legacy mail protocols switch changed" "Legacy mail protocols switch changed"
} }
EventType::Server(ServerEvent::Startup) => "Starting INBUXA Server", EventType::Server(ServerEvent::Startup) => "Starting inbuxa Server",
EventType::Server(ServerEvent::Shutdown) => "Shutting down INBUXA Server", EventType::Server(ServerEvent::Shutdown) => "Shutting down inbuxa Server",
EventType::Server(ServerEvent::StartupError) => "Server startup error", EventType::Server(ServerEvent::StartupError) => "Server startup error",
EventType::Server(ServerEvent::ThreadError) => "Server thread error", EventType::Server(ServerEvent::ThreadError) => "Server thread error",
EventType::Server(ServerEvent::Licensing) => "Server licensing event", EventType::Server(ServerEvent::Licensing) => "Server licensing event",
@@ -3983,7 +3983,7 @@ impl EventType {
EventType::Auth(AuthEvent::ClientRegistration) => "Authentication error", EventType::Auth(AuthEvent::ClientRegistration) => "Authentication error",
// inbuxa: legacy-protocols LP-6 // inbuxa: legacy-protocols LP-6
EventType::Auth(AuthEvent::LegacyProtocolRefused) => { EventType::Auth(AuthEvent::LegacyProtocolRefused) => {
"This server allows only INBUXA webmail and JMAP apps" "This server allows only inbuxa webmail and JMAP apps"
} }
EventType::Auth(AuthEvent::Error) => "Authentication error", EventType::Auth(AuthEvent::Error) => "Authentication error",
EventType::Auth(AuthEvent::CredentialExpired) => "Credential expired", EventType::Auth(AuthEvent::CredentialExpired) => "Credential expired",
+38 -15
View File
@@ -18,7 +18,7 @@
#[macro_export] #[macro_export]
macro_rules! brand { macro_rules! brand {
() => { () => {
"INBUXA" "inbuxa"
}; };
} }
@@ -47,22 +47,32 @@ macro_rules! brand_url {
} }
/// Reads one of the server's environment variables by its unprefixed name, /// Reads one of the server's environment variables by its unprefixed name,
/// such as `RECOVERY_ADMIN`. /// such as `RECOVERY_ADMIN`, from `INBUXA_<name>`.
/// ///
/// `INBUXA_<name>` wins. `STALWART_<name>` is still read when the new name /// The upstream prefix isn't read (SPEC §2.4). An install moved over from
/// isn't set, so an existing Stalwart install moves over without editing its /// upstream that still sets it stops here with the variable to rename, rather
/// environment, and a warning says which variable to rename. /// than starting on defaults the operator didn't choose.
pub fn env_var(name: &str) -> Result<String, std::env::VarError> { pub fn env_var(name: &str) -> Result<String, std::env::VarError> {
match std::env::var(format!("INBUXA_{name}")) { let found = std::env::var(format!("INBUXA_{name}"));
Err(std::env::VarError::NotPresent) => { if matches!(found, Err(std::env::VarError::NotPresent))
let legacy = std::env::var(format!("STALWART_{name}")); && let Some(legacy) = legacy_setting(name, |var| std::env::var_os(var).is_some())
if legacy.is_ok() { {
eprintln!("Warning: STALWART_{name} is deprecated; set INBUXA_{name} instead."); eprintln!(
} "Error: {legacy} is set, but inbuxa reads INBUXA_{name}. Rename it and start again \
legacy (https://docs.inbuxa.org/install/migrating/)."
} );
found => found, std::process::exit(1);
} }
found
}
/// The environment prefix upstream reads. Only ever used to refuse it.
const LEGACY_ENV_PREFIX: &str = "STALWART";
/// The upstream-prefixed variable for `name`, if it's set.
fn legacy_setting(name: &str, is_set: impl Fn(&str) -> bool) -> Option<String> {
let legacy = format!("{LEGACY_ENV_PREFIX}_{name}");
is_set(&legacy).then_some(legacy)
} }
/// INBUXA's own version, dated like the rest of its family: `YYYY.M.D`, with /// INBUXA's own version, dated like the rest of its family: `YYYY.M.D`, with
@@ -71,7 +81,7 @@ pub fn env_var(name: &str) -> Result<String, std::env::VarError> {
#[macro_export] #[macro_export]
macro_rules! brand_version { macro_rules! brand_version {
() => { () => {
"2026.9.23" "2026.9.24"
}; };
} }
@@ -90,3 +100,16 @@ macro_rules! brand_version_full {
concat!($crate::brand_version!(), " (upstream ", env!("CARGO_PKG_VERSION"), ")") concat!($crate::brand_version!(), " (upstream ", env!("CARGO_PKG_VERSION"), ")")
}; };
} }
#[cfg(test)]
mod tests {
use super::{LEGACY_ENV_PREFIX, legacy_setting};
#[test]
fn an_upstream_setting_is_named_for_renaming() {
let old = format!("{LEGACY_ENV_PREFIX}_RECOVERY_ADMIN");
let set = |var: &str| var == old;
assert_eq!(legacy_setting("RECOVERY_ADMIN", set), Some(old.clone()));
assert_eq!(legacy_setting("HOSTNAME", set), None);
}
}
+1 -1
View File
@@ -147,7 +147,7 @@ pub fn build_http_client(
allow_invalid_certs: bool, allow_invalid_certs: bool,
) -> Result<Client, String> { ) -> Result<Client, String> {
let mut headers = build_http_headers(raw_headers, username, password, token, content_type)?; let mut headers = build_http_headers(raw_headers, username, password, token, content_type)?;
headers.insert(USER_AGENT, "INBUXA/1.0.0".parse().unwrap()); // types::brand!(); utils does not depend on types headers.insert(USER_AGENT, "inbuxa/1.0.0".parse().unwrap()); // types::brand!(); utils does not depend on types
match http_client_builder(allow_invalid_certs) match http_client_builder(allow_invalid_certs)
.connect_timeout(timeout) .connect_timeout(timeout)
+500
View File
@@ -0,0 +1,500 @@
{
"ref": "v0.16.23",
"commit": "9d1c75ab68435e4417337f768291e5f947686203",
"removed_files": [
"crates/common/src/enterprise/alerts.rs",
"crates/common/src/enterprise/config.rs",
"crates/common/src/enterprise/license.rs",
"crates/common/src/enterprise/llm.rs",
"crates/common/src/enterprise/masked.rs",
"crates/common/src/enterprise/mod.rs",
"crates/common/src/telemetry/metrics/store.rs",
"crates/common/src/telemetry/metrics/test_data.rs",
"crates/common/src/telemetry/tracers/store.rs",
"crates/http/src/api/telemetry.rs",
"crates/jmap/src/registry/mapping/archived_item.rs",
"crates/jmap/src/registry/mapping/masked_email.rs",
"crates/jmap/src/registry/mapping/telemetry.rs",
"crates/scim-proto/src/attributes.rs",
"crates/scim-proto/src/etag.rs",
"crates/scim-proto/src/filter.rs",
"crates/scim-proto/src/json.rs",
"crates/scim-proto/src/lib.rs",
"crates/scim-proto/src/message/bulk.rs",
"crates/scim-proto/src/message/error.rs",
"crates/scim-proto/src/message/list.rs",
"crates/scim-proto/src/message/mod.rs",
"crates/scim-proto/src/message/patch.rs",
"crates/scim-proto/src/message/search.rs",
"crates/scim-proto/src/path.rs",
"crates/scim-proto/src/schema/group.rs",
"crates/scim-proto/src/schema/mod.rs",
"crates/scim-proto/src/schema/spc.rs",
"crates/scim-proto/src/schema/user.rs",
"crates/scim/src/auth.rs",
"crates/scim/src/bulk.rs",
"crates/scim/src/context.rs",
"crates/scim/src/discovery.rs",
"crates/scim/src/error.rs",
"crates/scim/src/groups/get.rs",
"crates/scim/src/groups/mod.rs",
"crates/scim/src/groups/patch.rs",
"crates/scim/src/groups/set.rs",
"crates/scim/src/lib.rs",
"crates/scim/src/query/cursor.rs",
"crates/scim/src/query/mod.rs",
"crates/scim/src/request.rs",
"crates/scim/src/users/get.rs",
"crates/scim/src/users/mod.rs",
"crates/scim/src/users/patch.rs",
"crates/scim/src/users/set.rs",
"crates/spam-filter/src/analysis/llm.rs",
"crates/store/src/backend/composite/mod.rs",
"crates/store/src/backend/composite/read_replica.rs",
"crates/store/src/backend/composite/sharded_blob.rs",
"crates/store/src/backend/composite/sharded_lookup.rs",
"crates/trc/src/serializers/binary.rs",
"tests/src/directory/oidc.rs",
"tests/src/scim/auth.rs",
"tests/src/scim/bulk.rs",
"tests/src/scim/discovery.rs",
"tests/src/scim/groups.rs",
"tests/src/scim/limits.rs",
"tests/src/scim/mod.rs",
"tests/src/scim/query.rs",
"tests/src/scim/users.rs",
"tests/src/system/archiving.rs",
"tests/src/system/tenant.rs"
],
"removed_snippets": {
"crates/common/src/auth/authentication.rs": 3,
"crates/common/src/auth/mod.rs": 2,
"crates/common/src/auth/permissions.rs": 1,
"crates/common/src/cache/directory.rs": 6,
"crates/common/src/cache/invalidate.rs": 1,
"crates/common/src/cache/principals.rs": 2,
"crates/common/src/cache/reload.rs": 1,
"crates/common/src/config/mod.rs": 2,
"crates/common/src/config/telemetry.rs": 4,
"crates/common/src/lib.rs": 2,
"crates/common/src/manager/boot.rs": 1,
"crates/common/src/network/mta.rs": 1,
"crates/common/src/scripts/plugins/llm_prompt.rs": 1,
"crates/common/src/storage/quota.rs": 2,
"crates/common/src/telemetry/metrics/mod.rs": 1,
"crates/common/src/telemetry/metrics/prometheus.rs": 1,
"crates/common/src/telemetry/mod.rs": 1,
"crates/common/src/telemetry/tracers/mod.rs": 1,
"crates/http/src/api/mod.rs": 4,
"crates/http/src/auth/permissions.rs": 1,
"crates/http/src/request.rs": 4,
"crates/jmap/src/registry/get.rs": 1,
"crates/jmap/src/registry/mapping/mod.rs": 1,
"crates/jmap/src/registry/mapping/principal.rs": 3,
"crates/jmap/src/registry/mapping/queued_message.rs": 1,
"crates/jmap/src/registry/mapping/task.rs": 1,
"crates/jmap/src/registry/mod.rs": 1,
"crates/jmap/src/registry/query.rs": 1,
"crates/jmap/src/registry/set.rs": 2,
"crates/main/src/main.rs": 1,
"crates/services/src/task_manager/alarm.rs": 1,
"crates/services/src/task_manager/imip.rs": 1,
"crates/services/src/task_manager/index.rs": 2,
"crates/services/src/task_manager/maintenance.rs": 3,
"crates/services/src/task_manager/scheduler.rs": 8,
"crates/spam-filter/src/analysis/mod.rs": 1,
"crates/spam-filter/src/analysis/score.rs": 2,
"crates/store/src/backend/mod.rs": 1,
"crates/store/src/backend/mysql/main.rs": 1,
"crates/store/src/backend/postgres/main.rs": 1,
"crates/store/src/build/blob.rs": 2,
"crates/store/src/build/lookup.rs": 1,
"crates/store/src/build/memory.rs": 2,
"crates/store/src/dispatch/blob.rs": 6,
"crates/store/src/dispatch/lookup.rs": 10,
"crates/store/src/dispatch/mod.rs": 1,
"crates/store/src/dispatch/search.rs": 6,
"crates/store/src/dispatch/store.rs": 8,
"crates/store/src/lib.rs": 6,
"crates/trc/src/serializers/mod.rs": 1
},
"cargo_edits": [
{
"file": "crates/main/Cargo.toml",
"line": 50,
"before": "default = [\"rocks\", \"enterprise\"]",
"after": "default = [\"rocks\"]"
},
{
"file": "tests/Cargo.toml",
"line": 22,
"before": "store = { path = \"../crates/store\", features = [\"test_mode\", \"enterprise\"] }",
"after": "store = { path = \"../crates/store\", features = [\"test_mode\"] }"
},
{
"file": "tests/Cargo.toml",
"line": 24,
"before": "directory = { path = \"../crates/directory\", features = [\"test_mode\", \"enterprise\"] }",
"after": "directory = { path = \"../crates/directory\", features = [\"test_mode\"] }"
},
{
"file": "tests/Cargo.toml",
"line": 25,
"before": "coordinator = { path = \"../crates/coordinator\", features = [\"test_mode\", \"enterprise\"] }",
"after": "coordinator = { path = \"../crates/coordinator\", features = [\"test_mode\"] }"
},
{
"file": "tests/Cargo.toml",
"line": 26,
"before": "jmap = { path = \"../crates/jmap\", features = [\"test_mode\", \"enterprise\"] }",
"after": "jmap = { path = \"../crates/jmap\", features = [\"test_mode\"] }"
},
{
"file": "tests/Cargo.toml",
"line": 35,
"before": "http = { path = \"../crates/http\", features = [\"test_mode\", \"enterprise\"] }",
"after": "http = { path = \"../crates/http\", features = [\"test_mode\"] }"
},
{
"file": "tests/Cargo.toml",
"line": 37,
"before": "scim = { path = \"../crates/scim\", features = [\"test_mode\", \"enterprise\"] }",
"after": "scim = { path = \"../crates/scim\", features = [\"test_mode\"] }"
},
{
"file": "tests/Cargo.toml",
"line": 39,
"before": "services = { path = \"../crates/services\", features = [\"test_mode\", \"enterprise\"] }",
"after": "services = { path = \"../crates/services\", features = [\"test_mode\"] }"
},
{
"file": "tests/Cargo.toml",
"line": 41,
"before": "smtp = { path = \"../crates/smtp\", features = [\"test_mode\", \"enterprise\"] }",
"after": "smtp = { path = \"../crates/smtp\", features = [\"test_mode\"] }"
},
{
"file": "tests/Cargo.toml",
"line": 42,
"before": "common = { path = \"../crates/common\", features = [\"test_mode\", \"enterprise\"] }",
"after": "common = { path = \"../crates/common\", features = [\"test_mode\"] }"
},
{
"file": "tests/Cargo.toml",
"line": 44,
"before": "email = { path = \"../crates/email\", features = [\"test_mode\", \"enterprise\"] }",
"after": "email = { path = \"../crates/email\", features = [\"test_mode\"] }"
},
{
"file": "tests/Cargo.toml",
"line": 45,
"before": "spam-filter = { path = \"../crates/spam-filter\", features = [\"test_mode\", \"enterprise\"] }",
"after": "spam-filter = { path = \"../crates/spam-filter\", features = [\"test_mode\"] }"
},
{
"file": "tests/Cargo.toml",
"line": 46,
"before": "trc = { path = \"../crates/trc\", features = [\"enterprise\"] }",
"after": "trc = { path = \"../crates/trc\", features = [] }"
},
{
"file": "tests/Cargo.toml",
"line": 47,
"before": "managesieve = { path = \"../crates/managesieve\", features = [\"test_mode\", \"enterprise\"] }",
"after": "managesieve = { path = \"../crates/managesieve\", features = [\"test_mode\"] }"
},
{
"file": ".github/workflows/ci.yml",
"line": 341,
"before": "cargo build --release --target ${{matrix.target}} -p stalwart --no-default-features --features \"sqlite postgres mysql rocks s3 redis azure nats enterprise\"",
"after": "cargo build --release --target ${{matrix.target}} -p stalwart --no-default-features --features \"sqlite postgres mysql rocks s3 redis azure nats\""
},
{
"file": ".github/workflows/ci.yml",
"line": 379,
"before": "# cargo build --release --target ${{matrix.target}} -p stalwart --no-default-features --features \"foundationdb s3 redis nats enterprise\"",
"after": "# cargo build --release --target ${{matrix.target}} -p stalwart --no-default-features --features \"foundationdb s3 redis nats\""
},
{
"file": ".github/workflows/ci.yml",
"line": 386,
"before": "cargo build --release --target ${{matrix.target}} -p stalwart --no-default-features --features \"sqlite postgres mysql rocks s3 redis azure nats enterprise\"",
"after": "cargo build --release --target ${{matrix.target}} -p stalwart --no-default-features --features \"sqlite postgres mysql rocks s3 redis azure nats\""
},
{
"file": ".github/workflows/ci.yml",
"line": 442,
"before": "cargo build --release -p stalwart --no-default-features --features \"sqlite postgres mysql rocks s3 redis azure nats enterprise\"",
"after": "cargo build --release -p stalwart --no-default-features --features \"sqlite postgres mysql rocks s3 redis azure nats\""
},
{
"file": "Dockerfile",
"line": 22,
"before": "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",
"after": "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\" --recipe-path /recipe.json"
},
{
"file": "Dockerfile",
"line": 24,
"before": "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\"",
"after": "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\""
},
{
"file": "Dockerfile.build",
"line": 111,
"before": "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\"; \\",
"after": "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\"; \\"
},
{
"file": "Dockerfile.build",
"line": 119,
"before": "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\"",
"after": "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\""
},
{
"file": "Dockerfile.build",
"line": 132,
"before": "RUSTFLAGS=\"-L /usr/lib\" cargo zigbuild --release --target ${TARGET} -p stalwart --no-default-features --features \"foundationdb s3 redis nats enterprise\" && \\",
"after": "RUSTFLAGS=\"-L /usr/lib\" cargo zigbuild --release --target ${TARGET} -p stalwart --no-default-features --features \"foundationdb s3 redis nats\" && \\"
},
{
"file": "Dockerfile.build",
"line": 142,
"before": "cargo zigbuild --release --target ${TARGET} -p stalwart --no-default-features --features \"sqlite postgres mysql rocks s3 redis azure nats enterprise\" && \\",
"after": "cargo zigbuild --release --target ${TARGET} -p stalwart --no-default-features --features \"sqlite postgres mysql rocks s3 redis azure nats\" && \\"
},
{
"file": "Dockerfile.fdb",
"line": 56,
"before": "RUN cargo build -p stalwart --no-default-features --features \"foundationdb s3 redis azure nats enterprise\" --release",
"after": "RUN cargo build -p stalwart --no-default-features --features \"foundationdb s3 redis azure nats\" --release"
}
],
"dangling_mods": [
{
"file": "crates/common/src/telemetry/metrics/mod.rs",
"line": 12,
"module": "test_data",
"lines": [
"#[cfg(any(feature = \"dev_mode\", feature = \"test_mode\"))]",
"pub mod test_data;"
]
},
{
"file": "tests/src/directory/mod.rs",
"line": 11,
"module": "oidc",
"lines": [
"pub mod oidc;"
]
},
{
"file": "tests/src/lib.rs",
"line": 27,
"module": "scim",
"lines": [
"#[cfg(test)]",
"pub mod scim;"
]
},
{
"file": "tests/src/system/mod.rs",
"line": 8,
"module": "archiving",
"lines": [
"pub mod archiving;"
]
},
{
"file": "tests/src/system/mod.rs",
"line": 19,
"module": "tenant",
"lines": [
"pub mod tenant;"
]
}
],
"problems": [],
"feature_gates": {
"crates/common/src/cache/reload.rs": 1,
"crates/common/src/manager/boot.rs": 1,
"crates/common/src/storage/mod.rs": 1,
"crates/common/src/storage/quota.rs": 1,
"crates/common/src/telemetry/metrics/prometheus.rs": 1,
"crates/http/src/api/mod.rs": 1,
"crates/http/src/auth/permissions.rs": 1,
"crates/jmap/src/registry/get.rs": 1,
"crates/jmap/src/registry/mapping/principal.rs": 2,
"crates/jmap/src/registry/mapping/queued_message.rs": 1,
"crates/jmap/src/registry/mapping/task.rs": 1,
"crates/jmap/src/registry/set.rs": 1,
"crates/services/src/task_manager/alarm.rs": 1,
"crates/services/src/task_manager/imip.rs": 1,
"crates/services/src/task_manager/index.rs": 1,
"crates/services/src/task_manager/maintenance.rs": 1,
"crates/services/src/task_manager/scheduler.rs": 1,
"crates/store/src/lib.rs": 1
},
"edition_checks": {},
"schema": {
"objects": [
"x:AiModel",
"x:Alert",
"x:ArchivedItem",
"x:MaskedEmail",
"x:MetricsStore",
"x:SpamLlm",
"x:Tenant",
"x:Trace",
"x:TracingStore"
],
"fields": [
"x:AcmeProvider.memberTenantId",
"x:ArfExternalReport.memberTenantId",
"x:Authentication.defaultTenantRoleIds",
"x:CalendarAlarm.template",
"x:CalendarScheduling.emailTemplate",
"x:CalendarScheduling.httpRsvpTemplate",
"x:DataRetention.archiveDeletedAccountsFor",
"x:DataRetention.archiveDeletedItemsFor",
"x:DataRetention.holdMetricsFor",
"x:DataRetention.holdTracesFor",
"x:DataRetention.metricsCollectionInterval",
"x:Dkim1Signature.memberTenantId",
"x:Dkim2Signature.memberTenantId",
"x:DmarcExternalReport.memberTenantId",
"x:Domain.allowScimProvisioning",
"x:Domain.directoryId",
"x:Domain.logo",
"x:Domain.memberTenantId",
"x:Email.maxMaskedAddresses",
"x:Enterprise.logoUrl",
"x:GroupAccount.externalId",
"x:LdapDirectory.memberTenantId",
"x:MySqlStore.readReplicas",
"x:OidcDirectory.memberTenantId",
"x:PostgreSqlStore.readReplicas",
"x:Search.indexTelemetry",
"x:Search.indexTracingFields",
"x:SqlDirectory.memberTenantId",
"x:TlsExternalReport.memberTenantId",
"x:UserAccount.externalId"
]
},
"third_party": {
"crates/common/src/network/acme/directory.rs": [
{
"line": 7,
"text": "Adapted from rustls-acme (https://github.com/FlorianUekermann/rustls-acme), licensed under MIT/Apache-2.0."
}
],
"crates/common/src/network/acme/jose.rs": [
{
"line": 7,
"text": "Adapted from rustls-acme (https://github.com/FlorianUekermann/rustls-acme), licensed under MIT/Apache-2.0."
}
],
"crates/common/src/network/acme/order.rs": [
{
"line": 7,
"text": "Adapted from rustls-acme (https://github.com/FlorianUekermann/rustls-acme), licensed under MIT/Apache-2.0."
}
],
"crates/common/src/scripts/functions/text.rs": [
{
"line": 239,
"text": "MIT licensed."
},
{
"line": 241,
"text": "Copyright (c) 2016 Titus Wormer <[email protected]>"
}
],
"crates/common/src/telemetry/tracers/journald.rs": [
{
"line": 70,
"text": "SPDX-FileCopyrightText: 2018 Benjamin Saunders <[email protected]>"
},
{
"line": 71,
"text": "SPDX-License-Identifier: MIT"
}
],
"crates/imap-proto/src/utf7.rs": [
{
"line": 7,
"text": "Ported from https://github.com/jstedfast/MailKit/blob/master/MailKit/Net/Imap/ImapEncoding.cs"
}
],
"crates/jmap/src/registry/mapping/log.rs": [
{
"line": 341,
"text": "SPDX-FileCopyrightText: 2017 Michael Coyne <[email protected]>"
},
{
"line": 343,
"text": "SPDX-License-Identifier: MIT"
},
{
"line": 346,
"text": "Adapted from https://github.com/mjc-gh/rev_lines/blob/main/src/lib.rs"
}
],
"crates/jmap-proto/src/types/date.rs": [
{
"line": 121,
"text": "Ported from http://howardhinnant.github.io/date_algorithms.html#civil_from_days"
},
{
"line": 158,
"text": "Ported from https://github.com/protocolbuffers/upb/blob/22182e6e/upb/json_decode.c#L982-L992"
}
],
"crates/nlp/src/tokenizers/japanese.rs": [
{
"line": 73,
"text": "Ported from https://github.com/woxtu/rust-tinysegmenter, MIT license"
}
],
"crates/nlp/src/tokenizers/types.rs": [
{
"line": 738,
"text": "Credits: test suite from linkify crate"
}
],
"crates/registry/src/types/datetime.rs": [
{
"line": 150,
"text": "Ported from http://howardhinnant.github.io/date_algorithms.html#civil_from_days"
},
{
"line": 188,
"text": "Ported from https://github.com/protocolbuffers/upb/blob/22182e6e/upb/json_decode.c#L982-L992"
}
],
"crates/store/src/backend/postgres/tls.rs": [
{
"line": 7,
"text": "Credits: https://github.com/jbg/tokio-postgres-rustls"
}
],
"crates/types/src/id.rs": [
{
"line": 69,
"text": "From https://github.com/archer884/crockford by J/A <[email protected]>"
},
{
"line": 70,
"text": "License: MIT/Apache 2.0"
}
],
"crates/utils/src/glob.rs": [
{
"line": 107,
"text": "Credits: Algorithm ported from https://research.swtch.com/glob"
}
]
},
"third_party_unlisted": [],
"ossify_log": "$ ossify.py crates\nProcessing Rust files in: /tmp/claude-1000/-run-media-john-PROJECTS/b60a056e-7e44-433c-bbb6-7e1b1fece570/scratchpad/strip-v0.16.23/tree/crates\n\nFound 957 Rust files\n\n\nSummary:\n- 50 files were completely removed\n- 2 crate roots were emptied\n- 118 proprietary snippets were removed from 50 files\n\n$ ossify.py tests\nProcessing Rust files in: /tmp/claude-1000/-run-media-john-PROJECTS/b60a056e-7e44-433c-bbb6-7e1b1fece570/scratchpad/strip-v0.16.23/tree/tests\n\nFound 211 Rust files\n\n\nSummary:\n- 11 files were completely removed\n- 0 crate roots were emptied\n- 0 proprietary snippets were removed from 0 files\n"
}
+211
View File
@@ -0,0 +1,211 @@
# Strip report: upstream v0.16.23 (9d1c75ab6843)
- Enterprise-only files removed or emptied: **63**
- Enterprise-only snippets removed: **118** in 50 files
- Cargo edits turning `enterprise` off: **25**
- Dangling module declarations removed: **5**
- Verification: **clean**
- Left for the rebuilt features to replace: 19 `enterprise` feature gates in 18 files; 0 `is_enterprise_edition()` checks in 0 files
- Third-party code: 14 files, **0** not in THIRD-PARTY.md
- Upstream schema flags 9 objects and 30 fields as Enterprise
## Removed files
- `crates/common/src/enterprise/alerts.rs`
- `crates/common/src/enterprise/config.rs`
- `crates/common/src/enterprise/license.rs`
- `crates/common/src/enterprise/llm.rs`
- `crates/common/src/enterprise/masked.rs`
- `crates/common/src/enterprise/mod.rs`
- `crates/common/src/telemetry/metrics/store.rs`
- `crates/common/src/telemetry/metrics/test_data.rs`
- `crates/common/src/telemetry/tracers/store.rs`
- `crates/http/src/api/telemetry.rs`
- `crates/jmap/src/registry/mapping/archived_item.rs`
- `crates/jmap/src/registry/mapping/masked_email.rs`
- `crates/jmap/src/registry/mapping/telemetry.rs`
- `crates/scim-proto/src/attributes.rs`
- `crates/scim-proto/src/etag.rs`
- `crates/scim-proto/src/filter.rs`
- `crates/scim-proto/src/json.rs`
- `crates/scim-proto/src/lib.rs`
- `crates/scim-proto/src/message/bulk.rs`
- `crates/scim-proto/src/message/error.rs`
- `crates/scim-proto/src/message/list.rs`
- `crates/scim-proto/src/message/mod.rs`
- `crates/scim-proto/src/message/patch.rs`
- `crates/scim-proto/src/message/search.rs`
- `crates/scim-proto/src/path.rs`
- `crates/scim-proto/src/schema/group.rs`
- `crates/scim-proto/src/schema/mod.rs`
- `crates/scim-proto/src/schema/spc.rs`
- `crates/scim-proto/src/schema/user.rs`
- `crates/scim/src/auth.rs`
- `crates/scim/src/bulk.rs`
- `crates/scim/src/context.rs`
- `crates/scim/src/discovery.rs`
- `crates/scim/src/error.rs`
- `crates/scim/src/groups/get.rs`
- `crates/scim/src/groups/mod.rs`
- `crates/scim/src/groups/patch.rs`
- `crates/scim/src/groups/set.rs`
- `crates/scim/src/lib.rs`
- `crates/scim/src/query/cursor.rs`
- `crates/scim/src/query/mod.rs`
- `crates/scim/src/request.rs`
- `crates/scim/src/users/get.rs`
- `crates/scim/src/users/mod.rs`
- `crates/scim/src/users/patch.rs`
- `crates/scim/src/users/set.rs`
- `crates/spam-filter/src/analysis/llm.rs`
- `crates/store/src/backend/composite/mod.rs`
- `crates/store/src/backend/composite/read_replica.rs`
- `crates/store/src/backend/composite/sharded_blob.rs`
- `crates/store/src/backend/composite/sharded_lookup.rs`
- `crates/trc/src/serializers/binary.rs`
- `tests/src/directory/oidc.rs`
- `tests/src/scim/auth.rs`
- `tests/src/scim/bulk.rs`
- `tests/src/scim/discovery.rs`
- `tests/src/scim/groups.rs`
- `tests/src/scim/limits.rs`
- `tests/src/scim/mod.rs`
- `tests/src/scim/query.rs`
- `tests/src/scim/users.rs`
- `tests/src/system/archiving.rs`
- `tests/src/system/tenant.rs`
## Removed snippets
- `crates/common/src/auth/authentication.rs`: 3
- `crates/common/src/auth/mod.rs`: 2
- `crates/common/src/auth/permissions.rs`: 1
- `crates/common/src/cache/directory.rs`: 6
- `crates/common/src/cache/invalidate.rs`: 1
- `crates/common/src/cache/principals.rs`: 2
- `crates/common/src/cache/reload.rs`: 1
- `crates/common/src/config/mod.rs`: 2
- `crates/common/src/config/telemetry.rs`: 4
- `crates/common/src/lib.rs`: 2
- `crates/common/src/manager/boot.rs`: 1
- `crates/common/src/network/mta.rs`: 1
- `crates/common/src/scripts/plugins/llm_prompt.rs`: 1
- `crates/common/src/storage/quota.rs`: 2
- `crates/common/src/telemetry/metrics/mod.rs`: 1
- `crates/common/src/telemetry/metrics/prometheus.rs`: 1
- `crates/common/src/telemetry/mod.rs`: 1
- `crates/common/src/telemetry/tracers/mod.rs`: 1
- `crates/http/src/api/mod.rs`: 4
- `crates/http/src/auth/permissions.rs`: 1
- `crates/http/src/request.rs`: 4
- `crates/jmap/src/registry/get.rs`: 1
- `crates/jmap/src/registry/mapping/mod.rs`: 1
- `crates/jmap/src/registry/mapping/principal.rs`: 3
- `crates/jmap/src/registry/mapping/queued_message.rs`: 1
- `crates/jmap/src/registry/mapping/task.rs`: 1
- `crates/jmap/src/registry/mod.rs`: 1
- `crates/jmap/src/registry/query.rs`: 1
- `crates/jmap/src/registry/set.rs`: 2
- `crates/main/src/main.rs`: 1
- `crates/services/src/task_manager/alarm.rs`: 1
- `crates/services/src/task_manager/imip.rs`: 1
- `crates/services/src/task_manager/index.rs`: 2
- `crates/services/src/task_manager/maintenance.rs`: 3
- `crates/services/src/task_manager/scheduler.rs`: 8
- `crates/spam-filter/src/analysis/mod.rs`: 1
- `crates/spam-filter/src/analysis/score.rs`: 2
- `crates/store/src/backend/mod.rs`: 1
- `crates/store/src/backend/mysql/main.rs`: 1
- `crates/store/src/backend/postgres/main.rs`: 1
- `crates/store/src/build/blob.rs`: 2
- `crates/store/src/build/lookup.rs`: 1
- `crates/store/src/build/memory.rs`: 2
- `crates/store/src/dispatch/blob.rs`: 6
- `crates/store/src/dispatch/lookup.rs`: 10
- `crates/store/src/dispatch/mod.rs`: 1
- `crates/store/src/dispatch/search.rs`: 6
- `crates/store/src/dispatch/store.rs`: 8
- `crates/store/src/lib.rs`: 6
- `crates/trc/src/serializers/mod.rs`: 1
## Dangling module declarations removed
- `crates/common/src/telemetry/metrics/mod.rs:12`: `mod test_data` (#[cfg(any(feature = "dev_mode", feature = "test_mode"))] / pub mod test_data;)
- `tests/src/directory/mod.rs:11`: `mod oidc` (pub mod oidc;)
- `tests/src/lib.rs:27`: `mod scim` (#[cfg(test)] / pub mod scim;)
- `tests/src/system/mod.rs:8`: `mod archiving` (pub mod archiving;)
- `tests/src/system/mod.rs:19`: `mod tenant` (pub mod tenant;)
## Cargo edits
- `crates/main/Cargo.toml:50`: `default = ["rocks", "enterprise"]``default = ["rocks"]`
- `tests/Cargo.toml:22`: `store = { path = "../crates/store", features = ["test_mode", "enterprise"] }``store = { path = "../crates/store", features = ["test_mode"] }`
- `tests/Cargo.toml:24`: `directory = { path = "../crates/directory", features = ["test_mode", "enterprise"] }``directory = { path = "../crates/directory", features = ["test_mode"] }`
- `tests/Cargo.toml:25`: `coordinator = { path = "../crates/coordinator", features = ["test_mode", "enterprise"] }``coordinator = { path = "../crates/coordinator", features = ["test_mode"] }`
- `tests/Cargo.toml:26`: `jmap = { path = "../crates/jmap", features = ["test_mode", "enterprise"] }``jmap = { path = "../crates/jmap", features = ["test_mode"] }`
- `tests/Cargo.toml:35`: `http = { path = "../crates/http", features = ["test_mode", "enterprise"] }``http = { path = "../crates/http", features = ["test_mode"] }`
- `tests/Cargo.toml:37`: `scim = { path = "../crates/scim", features = ["test_mode", "enterprise"] }``scim = { path = "../crates/scim", features = ["test_mode"] }`
- `tests/Cargo.toml:39`: `services = { path = "../crates/services", features = ["test_mode", "enterprise"] }``services = { path = "../crates/services", features = ["test_mode"] }`
- `tests/Cargo.toml:41`: `smtp = { path = "../crates/smtp", features = ["test_mode", "enterprise"] }``smtp = { path = "../crates/smtp", features = ["test_mode"] }`
- `tests/Cargo.toml:42`: `common = { path = "../crates/common", features = ["test_mode", "enterprise"] }``common = { path = "../crates/common", features = ["test_mode"] }`
- `tests/Cargo.toml:44`: `email = { path = "../crates/email", features = ["test_mode", "enterprise"] }``email = { path = "../crates/email", features = ["test_mode"] }`
- `tests/Cargo.toml:45`: `spam-filter = { path = "../crates/spam-filter", features = ["test_mode", "enterprise"] }``spam-filter = { path = "../crates/spam-filter", features = ["test_mode"] }`
- `tests/Cargo.toml:46`: `trc = { path = "../crates/trc", features = ["enterprise"] }``trc = { path = "../crates/trc", features = [] }`
- `tests/Cargo.toml:47`: `managesieve = { path = "../crates/managesieve", features = ["test_mode", "enterprise"] }``managesieve = { path = "../crates/managesieve", features = ["test_mode"] }`
- `.github/workflows/ci.yml:341`: `cargo build --release --target ${{matrix.target}} -p stalwart --no-default-features --features "sqlite postgres mysql rocks s3 redis azure nats enterprise"``cargo build --release --target ${{matrix.target}} -p stalwart --no-default-features --features "sqlite postgres mysql rocks s3 redis azure nats"`
- `.github/workflows/ci.yml:379`: `# cargo build --release --target ${{matrix.target}} -p stalwart --no-default-features --features "foundationdb s3 redis nats enterprise"``# cargo build --release --target ${{matrix.target}} -p stalwart --no-default-features --features "foundationdb s3 redis nats"`
- `.github/workflows/ci.yml:386`: `cargo build --release --target ${{matrix.target}} -p stalwart --no-default-features --features "sqlite postgres mysql rocks s3 redis azure nats enterprise"``cargo build --release --target ${{matrix.target}} -p stalwart --no-default-features --features "sqlite postgres mysql rocks s3 redis azure nats"`
- `.github/workflows/ci.yml:442`: `cargo build --release -p stalwart --no-default-features --features "sqlite postgres mysql rocks s3 redis azure nats enterprise"``cargo build --release -p stalwart --no-default-features --features "sqlite postgres mysql rocks s3 redis azure nats"`
- `Dockerfile:22`: `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``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" --recipe-path /recipe.json`
- `Dockerfile:24`: `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 --no-default-features --features "sqlite postgres mysql rocks s3 redis azure nats"`
- `Dockerfile.build:111`: `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"; \``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"; \`
- `Dockerfile.build:119`: `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 --no-default-features --features "sqlite postgres mysql rocks s3 redis azure nats"`
- `Dockerfile.build:132`: `RUSTFLAGS="-L /usr/lib" cargo zigbuild --release --target ${TARGET} -p stalwart --no-default-features --features "foundationdb s3 redis nats enterprise" && \``RUSTFLAGS="-L /usr/lib" cargo zigbuild --release --target ${TARGET} -p stalwart --no-default-features --features "foundationdb s3 redis nats" && \`
- `Dockerfile.build:142`: `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 --no-default-features --features "sqlite postgres mysql rocks s3 redis azure nats" && \`
- `Dockerfile.fdb:56`: `RUN cargo build -p stalwart --no-default-features --features "foundationdb s3 redis azure nats enterprise" --release``RUN cargo build -p stalwart --no-default-features --features "foundationdb s3 redis azure nats" --release`
## Flagged Enterprise in upstream's schema
**Objects:** `x:AiModel`, `x:Alert`, `x:ArchivedItem`, `x:MaskedEmail`, `x:MetricsStore`, `x:SpamLlm`, `x:Tenant`, `x:Trace`, `x:TracingStore`
**Fields:** `x:AcmeProvider.memberTenantId`, `x:ArfExternalReport.memberTenantId`, `x:Authentication.defaultTenantRoleIds`, `x:CalendarAlarm.template`, `x:CalendarScheduling.emailTemplate`, `x:CalendarScheduling.httpRsvpTemplate`, `x:DataRetention.archiveDeletedAccountsFor`, `x:DataRetention.archiveDeletedItemsFor`, `x:DataRetention.holdMetricsFor`, `x:DataRetention.holdTracesFor`, `x:DataRetention.metricsCollectionInterval`, `x:Dkim1Signature.memberTenantId`, `x:Dkim2Signature.memberTenantId`, `x:DmarcExternalReport.memberTenantId`, `x:Domain.allowScimProvisioning`, `x:Domain.directoryId`, `x:Domain.logo`, `x:Domain.memberTenantId`, `x:Email.maxMaskedAddresses`, `x:Enterprise.logoUrl`, `x:GroupAccount.externalId`, `x:LdapDirectory.memberTenantId`, `x:MySqlStore.readReplicas`, `x:OidcDirectory.memberTenantId`, `x:PostgreSqlStore.readReplicas`, `x:Search.indexTelemetry`, `x:Search.indexTracingFields`, `x:SqlDirectory.memberTenantId`, `x:TlsExternalReport.memberTenantId`, `x:UserAccount.externalId`
## Third-party code
Comments in the stripped tree that name another copyright holder, another license, or a source the code came from. Files marked **new** aren't in THIRD-PARTY.md yet.
- `crates/common/src/network/acme/directory.rs`
- 7: Adapted from rustls-acme (https://github.com/FlorianUekermann/rustls-acme), licensed under MIT/Apache-2.0.
- `crates/common/src/network/acme/jose.rs`
- 7: Adapted from rustls-acme (https://github.com/FlorianUekermann/rustls-acme), licensed under MIT/Apache-2.0.
- `crates/common/src/network/acme/order.rs`
- 7: Adapted from rustls-acme (https://github.com/FlorianUekermann/rustls-acme), licensed under MIT/Apache-2.0.
- `crates/common/src/scripts/functions/text.rs`
- 239: MIT licensed.
- 241: Copyright (c) 2016 Titus Wormer <tituswormer@gmail.com>
- `crates/common/src/telemetry/tracers/journald.rs`
- 70: SPDX-FileCopyrightText: 2018 Benjamin Saunders <ben.e.saunders@gmail.com>
- 71: SPDX-License-Identifier: MIT
- `crates/imap-proto/src/utf7.rs`
- 7: Ported from https://github.com/jstedfast/MailKit/blob/master/MailKit/Net/Imap/ImapEncoding.cs
- `crates/jmap/src/registry/mapping/log.rs`
- 341: SPDX-FileCopyrightText: 2017 Michael Coyne <mjc@hey.com>
- 343: SPDX-License-Identifier: MIT
- 346: Adapted from https://github.com/mjc-gh/rev_lines/blob/main/src/lib.rs
- `crates/jmap-proto/src/types/date.rs`
- 121: Ported from http://howardhinnant.github.io/date_algorithms.html#civil_from_days
- 158: Ported from https://github.com/protocolbuffers/upb/blob/22182e6e/upb/json_decode.c#L982-L992
- `crates/nlp/src/tokenizers/japanese.rs`
- 73: Ported from https://github.com/woxtu/rust-tinysegmenter, MIT license
- `crates/nlp/src/tokenizers/types.rs`
- 738: Credits: test suite from linkify crate
- `crates/registry/src/types/datetime.rs`
- 150: Ported from http://howardhinnant.github.io/date_algorithms.html#civil_from_days
- 188: Ported from https://github.com/protocolbuffers/upb/blob/22182e6e/upb/json_decode.c#L982-L992
- `crates/store/src/backend/postgres/tls.rs`
- 7: Credits: https://github.com/jbg/tokio-postgres-rustls
- `crates/types/src/id.rs`
- 69: From https://github.com/archer884/crockford by J/A <archer884@gmail.com>
- 70: License: MIT/Apache 2.0
- `crates/utils/src/glob.rs`
- 107: Credits: Algorithm ported from https://research.swtch.com/glob
+53 -20
View File
@@ -91,7 +91,22 @@ The wrapper is `tools/fork/strip.py`. Beyond `ossify.py` it:
was ported or adapted from elsewhere. Any file `THIRD-PARTY.md` doesn't was ported or adapted from elsewhere. Any file `THIRD-PARTY.md` doesn't
cover yet is flagged as new. It's reported, not a failure: the notice goes cover yet is flagged as new. It's reported, not a failure: the notice goes
into `THIRD-PARTY.md` in the merge that brings the release in, since the into `THIRD-PARTY.md` in the merge that brings the release in, since the
fork redistributes that code and its license requires the notice. fork redistributes that code and its license requires the notice;
- renames the upstream name where it's an identifier clients, users or
operators meet (wire-protocol names, the web interface's client id, store
keys; §2.4), with the same substitutions `main`
carries, so those lines arrive purged and never conflict (added
2026-09-22);
- compiles the stripped tree. A dual-licensed file that only serves an
Enterprise feature survives the strip but can't build without it:
v0.16.23's `tests/src/directory/issuer.rs` was the first. The build check
fails the run on it, and the merge into `main` drops or reworks it (added
2026-09-22).
Two checks in CI cover what a merge can bring in without a conflict:
`tools/fork/name-check.py` (the upstream name in a new string literal) and
`tools/fork/notice-check.py` (a changed upstream file without its AGPL 5(a)
notice).
### 2.2a Snapshots, not a git fork ### 2.2a Snapshots, not a git fork
@@ -197,14 +212,29 @@ depends on `store` and can't be called from it (`features/scale-out-storage.md`)
- Factual statements are allowed and required: "a fork of Stalwart", - Factual statements are allowed and required: "a fork of Stalwart",
"compatible with Stalwart 0.16 data". Upstream copyright notices stay on "compatible with Stalwart 0.16 data". Upstream copyright notices stay on
every file they cover. every file they cover.
- Protocol identifiers stay as upstream has them, for example the JMAP - **Identifiers people meet carry the fork's name** (changed 2026-09-22;
capability `urn:stalwart:jmap` and the `x:` object names. They're this bullet used to keep upstream's). Upstream's JMAP capability for the
interoperability, not branding, and renaming them breaks every existing registry (`x:`) objects is `urn:inbuxa:jmap:registry`, beside the fork's
client. Anything the fork adds uses its own namespace (open: which one). own `urn:inbuxa:jmap` (contract C-1); WebDAV lock and sync tokens are
- **Version and build metadata are exempt from the first bullet** (added `urn:inbuxa:dav*`, the Sieve extensions are `vnd.inbuxa.while` and `vnd.inbuxa.expressions`, the
2026-09-19). `inbuxa --version`, the startup banner and events, web interface's OAuth client is `inbuxa-webui`, the spam filter's blobs are
OpenTelemetry's `service.version`, the JMAP `implementation` string, release `INBUXA_SPAM_*`, and the SQL stores and log files default to `inbuxa`.
notes and the strip report all name the Stalwart base, as §2.6 requires. There are no aliases: the old names stop working, so front ends move with
the server, Sieve scripts that `require` the old extensions are edited,
DAV clients resync once, and anyone signed in to the web interface signs in
again. Pre-rename data is carried over where it would otherwise be lost
(the spam model, the web interface's client). The `x:` object names are
unchanged, having no name in them. `tools/fork/renames.py` holds the list,
and the strip applies it to every import (§2.2).
- **Identifiers nobody sees keep upstream's spelling:** the OAuth
key-derivation contexts, whose renaming would invalidate every issued token
and client id, and the application blob prefix, which is hashed before use.
`tools/fork/name-allowlist.txt` lists them with their reasons.
- **Version and build metadata give the base without the name** (added
2026-09-19, narrowed 2026-09-22). `inbuxa --version`, the startup banner and
events, OpenTelemetry's `service.version` and the JMAP `implementation`
string say `2026.9.23 (upstream 0.16.23)`, as §2.6 requires; release notes
and the strip report may name the Stalwart base.
That is a factual statement about what was compiled, not a name the product That is a factual statement about what was compiled, not a name the product
calls itself, and the two bullets don't conflict: the first governs calls itself, and the two bullets don't conflict: the first governs
identity, this one governs provenance. A reader who takes "no Stalwart in identity, this one governs provenance. A reader who takes "no Stalwart in
@@ -228,15 +258,18 @@ Done 2026-09-18:
- The package and binary are `inbuxa` (`cargo build -p inbuxa`). The binary's - The package and binary are `inbuxa` (`cargo build -p inbuxa`). The binary's
help, banner and every protocol greeting say INBUXA (the branding module, help, banner and every protocol greeting say INBUXA (the branding module,
`types::brand!()`). `types::brand!()`).
- Settings come from `INBUXA_*` environment variables. Each still falls back - Settings come from `INBUXA_*` environment variables
to its `STALWART_*` name, with a startup warning to rename it (`types::branding::env_var`): `HOSTNAME`, `RECOVERY_MODE`, `RECOVERY_ADMIN`,
(`types::branding::env_var`). That covers all nine the server reads: `RECOVERY_MODE_PORT`, `RECOVERY_MODE_LOG_LEVEL`, `ROLE`, `PUSH_SHARD`,
`HOSTNAME`, `RECOVERY_MODE`, `RECOVERY_ADMIN`, `RECOVERY_MODE_PORT`, `PUBLIC_URL`, `HTTPS_PORT`, and the front-end variables of contract C-6.
`RECOVERY_MODE_LOG_LEVEL`, `ROLE`, `PUSH_SHARD`, `PUBLIC_URL`, `HTTPS_PORT`. Until 2026-09-22 each fell back to its `STALWART_*` name with a warning.
- **Not renamed, on purpose:** `STALWART_APP_` and the two `STALWART_SPAM_...` Now a `STALWART_*` name that's set where its `INBUXA_*` one isn't stops the
names. They look like environment variables, but they're keys inside the server at startup, naming the variable to rename, so an install moved over
data store, so renaming them would orphan existing installed apps and from upstream never runs on settings it silently dropped.
spam-classifier models. - The spam filter's blobs moved from `STALWART_SPAM_*` to `INBUXA_SPAM_*` on
2026-09-22; every start moves any left under the old keys
(`migration::try_migrate`), so no trained model is orphaned.
`STALWART_APP_` stays: it's hashed into a blob key and never seen.
- New installs default to `/var/lib/inbuxa` for data and `/var/log/inbuxa` for - New installs default to `/var/lib/inbuxa` for data and `/var/log/inbuxa` for
logs. Existing installs keep the paths their configuration names, so no data logs. Existing installs keep the paths their configuration names, so no data
moves. moves.
@@ -406,8 +439,8 @@ Versioned, and advertised in the JMAP session so either side can check it.
address or network, so an admin credential is useless from anywhere else. address or network, so an admin credential is useless from anywhere else.
- **Push.** Unchanged: JMAP push with VAPID, as ihasmail uses today. - **Push.** Unchanged: JMAP push with VAPID, as ihasmail uses today.
- **INBUXA Admin's client.** INBUXA Admin signs in by OAuth (authorization - **INBUXA Admin's client.** INBUXA Admin signs in by OAuth (authorization
code with PKCE) as upstream's `webui` does, as client `stalwart-webui` for code with PKCE) as upstream's `webui` does, as client `inbuxa-webui`
now. The fork registers a first-party `inbuxa-admin` client with the (upstream's `stalwart-webui` until 2026-09-22) when the server serves it. The fork registers a first-party `inbuxa-admin` client with the
admin's own redirect URIs, and the admin switches to it (its admin's own redirect URIs, and the admin switches to it (its
`<meta name="oauth-client-id">`). `<meta name="oauth-client-id">`).
- **Cross-origin access.** Verified 2026-09-18 against a separate - **Cross-origin access.** Verified 2026-09-18 against a separate
+4 -3
View File
@@ -106,8 +106,9 @@ Each has an ID, and tests name the IDs they check.
ihasmail-inbuxa server, authorization code with PKCE S256, redirect URI ihasmail-inbuxa server, authorization code with PKCE S256, redirect URI
`{webmailUrl}/api/auth/callback`. `{webmailUrl}/api/auth/callback`.
INBUXA Admin's `<meta name="oauth-client-id">` is set to `inbuxa-admin`. INBUXA Admin's `<meta name="oauth-client-id">` is set to `inbuxa-admin`.
Until then it keeps upstream's `stalwart-webui`, which only works while Served by the server itself it uses the web interface's client,
registration isn't required. `inbuxa-webui` (upstream's `stalwart-webui` until 2026-09-22, retired on
start since).
**Built (interim), 2026-09-18.** C-5's defaults are in the server, and **Built (interim), 2026-09-18.** C-5's defaults are in the server, and
`crates/common/src/manager/first_party.rs` registers the clients on every `crates/common/src/manager/first_party.rs` registers the clients on every
@@ -116,7 +117,7 @@ Each has an ID, and tests name the IDs they check.
`INBUXA_WEBMAIL_CLIENT_SECRET` (the webmail client is registered only when `INBUXA_WEBMAIL_CLIENT_SECRET` (the webmail client is registered only when
both of its variables are set). A web interface the server serves itself both of its variables are set). A web interface the server serves itself
(none on a new install since SPEC.md §5.3; possible on one upgraded from (none on a new install since SPEC.md §5.3; possible on one upgraded from
Stalwart) is registered too, as its application's OAuth client id or `stalwart-webui`, at the Stalwart) is registered too, as its application's OAuth client id or `inbuxa-webui`, at the
server's public URL. A missing client is created. An existing one gains any server's public URL. A missing client is created. An existing one gains any
redirect URI it lacks, and the webmail client gets the configured secret. redirect URI it lacks, and the webmail client gets the configured secret.
Nothing an operator added is removed. Bootstrap and recovery mode skip this: Nothing an operator added is removed. Bootstrap and recovery mode skip this:
+1 -1
View File
@@ -291,7 +291,7 @@ adds at most 2.
### The Sieve function `llm_prompt` ### The Sieve function `llm_prompt`
- **AI-20.** `llm_prompt(model, prompt, temperature)`, available with - **AI-20.** `llm_prompt(model, prompt, temperature)`, available with
`require "vnd.stalwart.expressions"`. `model` names an `x:AiModel` by its `require "vnd.inbuxa.expressions"`. `model` names an `x:AiModel` by its
`name`, or failing that by its id. `prompt` is sent as is. `temperature` is `name`, or failing that by its id. `prompt` is sent as is. `temperature` is
clamped to 0.01.0. A value that isn't a number uses the model's own clamped to 0.01.0. A value that isn't a number uses the model's own
`temperature`. **Decision** on name first, see open question 4. `temperature`. **Decision** on name first, see open question 4.
+1 -1
View File
@@ -210,7 +210,7 @@ accepted, which is the fact `enabled` reports.
### Upstream's, unchanged ### Upstream's, unchanged
`x:MaskedEmail/get`, `/query`, `/set` under `urn:stalwart:jmap`, standard RFC `x:MaskedEmail/get`, `/query`, `/set` under `urn:inbuxa:jmap:registry`, standard RFC
8620 shapes, the record above. Upstream's `/query` accepts only an 8620 shapes, the record above. Upstream's `/query` accepts only an
`accountId` filter, and it has no `/changes` (observed 6). `accountId` filter, and it has no `/changes` (observed 6).
+1 -1
View File
@@ -374,7 +374,7 @@ unchanged.
## Interfaces ## Interfaces
- **JMAP, existing names, unchanged.** Over `urn:stalwart:jmap`: - **JMAP, existing names, unchanged.** Over `urn:inbuxa:jmap:registry`:
- `x:Alert/get`, `/query`, `/set`, and the `x:TracingStore`, - `x:Alert/get`, `/query`, `/set`, and the `x:TracingStore`,
`x:MetricsStore`, `x:DataRetention`, `x:Search` singletons. `x:MetricsStore`, `x:DataRetention`, `x:Search` singletons.
- `x:Trace/get` and `/query`. Filters: `text`, `timestamp` (comparison names - `x:Trace/get` and `/query`. Filters: `text`, `timestamp` (comparison names
File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 35 KiB

File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
+1 -1
View File
@@ -1 +1 @@
rLRbZKj15KvcPMVmnisfCDsXXZEKks6BXpMkMpS0mlI rWqJwNJkqgKsbC1eqcEmmIAMtJPmnlDlThR2ZtW_N1c
+1 -1
View File
@@ -6,7 +6,7 @@
<key>Label</key> <key>Label</key>
<string>inbuxa.mail</string> <string>inbuxa.mail</string>
<key>ServiceDescription</key> <key>ServiceDescription</key>
<string>INBUXA</string> <string>inbuxa</string>
<key>ProgramArguments</key> <key>ProgramArguments</key>
<array> <array>
<string>__PATH__/bin/inbuxa</string> <string>__PATH__/bin/inbuxa</string>
+1 -1
View File
@@ -1,5 +1,5 @@
[Unit] [Unit]
Description=INBUXA Server Description=inbuxa Server
Conflicts=postfix.service sendmail.service exim4.service Conflicts=postfix.service sendmail.service exim4.service
ConditionPathExists=__PATH__/etc/config.json ConditionPathExists=__PATH__/etc/config.json
After=network-online.target After=network-online.target
+1
View File
@@ -49,6 +49,7 @@ email = { path = "../crates/email", features = ["test_mode"] }
spam-filter = { path = "../crates/spam-filter", features = ["test_mode"] } spam-filter = { path = "../crates/spam-filter", features = ["test_mode"] }
trc = { path = "../crates/trc", features = [] } trc = { path = "../crates/trc", features = [] }
managesieve = { path = "../crates/managesieve", features = ["test_mode"] } managesieve = { path = "../crates/managesieve", features = ["test_mode"] }
migration = { path = "../crates/migration" }
smtp-proto = { version = "0.2" } smtp-proto = { version = "0.2" }
mail-auth = { version = "0.13", features = ["test"] } mail-auth = { version = "0.13", features = ["test"] }
mail-parser = { version = "0.11", features = ["full_encoding", "rkyv"] } mail-parser = { version = "0.11", features = ["full_encoding", "rkyv"] }
+5 -5
View File
@@ -49,7 +49,7 @@ HTTP = "http://127.0.0.1:18080"
# made to speak. # made to speak.
PORTS = {"imap": 18993, "pop3": 18995, "submissions": 18465, "smtp": 18025} PORTS = {"imap": 18993, "pop3": 18995, "submissions": 18465, "smtp": 18025}
TLS_PORTS = {18993, 18995, 18465} TLS_PORTS = {18993, 18995, 18465}
SMTP_REFUSAL = ("535 5.7.0 This server allows only INBUXA webmail and JMAP apps. " SMTP_REFUSAL = ("535 5.7.0 This server allows only inbuxa webmail and JMAP apps. "
"This mail app can't send.") "This mail app can't send.")
INBUXA = "urn:inbuxa:jmap" INBUXA = "urn:inbuxa:jmap"
failures = [] failures = []
@@ -105,7 +105,7 @@ def stop():
docker("rm", "-f", NAME, check_rc=False) docker("rm", "-f", NAME, check_rc=False)
def jmap(user, password, calls, using=("urn:ietf:params:jmap:core", "urn:stalwart:jmap", INBUXA)): def jmap(user, password, calls, using=("urn:ietf:params:jmap:core", "urn:inbuxa:jmap:registry", INBUXA)):
body = json.dumps({"using": list(using), "methodCalls": calls}).encode() body = json.dumps({"using": list(using), "methodCalls": calls}).encode()
req = urllib.request.Request(f"{HTTP}/jmap/", data=body, method="POST") req = urllib.request.Request(f"{HTTP}/jmap/", data=body, method="POST")
req.add_header("Content-Type", "application/json") req.add_header("Content-Type", "application/json")
@@ -333,7 +333,7 @@ def tenant_checks(admin, admin_pw, account):
"and still enabled for an account outside the tenant (test 13)") "and still enabled for an account outside the tenant (test 13)")
# Refused on the tenant's domain, every way in the same words (tests 6-8). # Refused on the tenant's domain, every way in the same words (tests 6-8).
imap_no = ("NO [ALERT] Your organization allows only INBUXA webmail and JMAP apps. " imap_no = ("NO [ALERT] Your organization allows only inbuxa webmail and JMAP apps. "
"This mail app can't sign in.") "This mail app can't sign in.")
check(imap_login(PORTS["imap"], tu, user_pw) == imap_no, check(imap_login(PORTS["imap"], tu, user_pw) == imap_no,
"the tenant's user is refused over IMAP with the right password (test 6)") "the tenant's user is refused over IMAP with the right password (test 6)")
@@ -341,10 +341,10 @@ def tenant_checks(admin, admin_pw, account):
check(imap_login(PORTS["imap"], "[email protected]", "x") == imap_no, check(imap_login(PORTS["imap"], "[email protected]", "x") == imap_no,
"and a made-up address on the domain gets the same (test 7)") "and a made-up address on the domain gets the same (test 7)")
check(pop3_login(PORTS["pop3"], tu, user_pw) == check(pop3_login(PORTS["pop3"], tu, user_pw) ==
"-ERR [AUTH] Your organization allows only INBUXA webmail and JMAP apps. " "-ERR [AUTH] Your organization allows only inbuxa webmail and JMAP apps. "
"This mail app can't sign in.", "POP3 refuses in its own form (test 8)") "This mail app can't sign in.", "POP3 refuses in its own form (test 8)")
check(smtp_auths(PORTS["submissions"], tu, [user_pw])[0] == check(smtp_auths(PORTS["submissions"], tu, [user_pw])[0] ==
"535 5.7.0 Your organization allows only INBUXA webmail and JMAP apps. " "535 5.7.0 Your organization allows only inbuxa webmail and JMAP apps. "
"This mail app can't send.", "submission refuses in its own form (test 8)") "This mail app can't send.", "submission refuses in its own form (test 8)")
check(imap_login(PORTS["imap"], admin, admin_pw).startswith("OK"), check(imap_login(PORTS["imap"], admin, admin_pw).startswith("OK"),
"an account on another domain signs in over IMAP normally (test 6)") "an account on another domain signs in over IMAP normally (test 6)")
+3 -3
View File
@@ -61,7 +61,7 @@ summary.rrule: Rrule(ICalendarRecurrenceRule { freq: Weekly, until: None, count:
summary.summary: Text("Meet me maybe") summary.summary: Text("Meet me maybe")
BEGIN:VCALENDAR BEGIN:VCALENDAR
METHOD:REQUEST METHOD:REQUEST
PRODID:-//INBUXA//INBUXA Server//EN PRODID:-//inbuxa//inbuxa Server//EN
VERSION:2.0 VERSION:2.0
BEGIN:VEVENT BEGIN:VEVENT
X-MICROSOFT-CDO-OWNERAPPTID:299828133 X-MICROSOFT-CDO-OWNERAPPTID:299828133
@@ -176,7 +176,7 @@ summary.summary: Text("Meet me maybe")
~summary.description: Text("This is the event description") ~summary.description: Text("This is the event description")
BEGIN:VCALENDAR BEGIN:VCALENDAR
METHOD:REQUEST METHOD:REQUEST
PRODID:-//INBUXA//INBUXA Server//EN PRODID:-//inbuxa//inbuxa Server//EN
VERSION:2.0 VERSION:2.0
BEGIN:VEVENT BEGIN:VEVENT
X-MICROSOFT-CDO-OWNERAPPTID:299828133 X-MICROSOFT-CDO-OWNERAPPTID:299828133
@@ -224,7 +224,7 @@ END:VCALENDAR
# Make sure the original alarms are preserved # Make sure the original alarms are preserved
> get [email protected] [email protected] > get [email protected] [email protected]
BEGIN:VCALENDAR BEGIN:VCALENDAR
PRODID:-//INBUXA//INBUXA Server//EN PRODID:-//inbuxa//inbuxa Server//EN
VERSION:2.0 VERSION:2.0
BEGIN:VEVENT BEGIN:VEVENT
X-MICROSOFT-CDO-OWNERAPPTID:299828133 X-MICROSOFT-CDO-OWNERAPPTID:299828133
+2 -2
View File
@@ -64,7 +64,7 @@ summary.location: Text("Conference Room A")
summary.summary: Text("Review Accounts") summary.summary: Text("Review Accounts")
BEGIN:VCALENDAR BEGIN:VCALENDAR
METHOD:REQUEST METHOD:REQUEST
PRODID:-//INBUXA//INBUXA Server//EN PRODID:-//inbuxa//inbuxa Server//EN
VERSION:2.0 VERSION:2.0
BEGIN:VEVENT BEGIN:VEVENT
LOCATION:Conference Room A LOCATION:Conference Room A
@@ -114,7 +114,7 @@ summary.location: Text("Conference Room A")
summary.summary: Text("Review Accounts") summary.summary: Text("Review Accounts")
BEGIN:VCALENDAR BEGIN:VCALENDAR
METHOD:REQUEST METHOD:REQUEST
PRODID:-//INBUXA//INBUXA Server//EN PRODID:-//inbuxa//inbuxa Server//EN
VERSION:2.0 VERSION:2.0
BEGIN:VEVENT BEGIN:VEVENT
LOCATION:Conference Room A LOCATION:Conference Room A
+3 -3
View File
@@ -146,7 +146,7 @@ summary.dtstart: Time(ItipTime { start: 867787200, tz_id: 32768 })
summary.summary: Text("Conference") summary.summary: Text("Conference")
BEGIN:VCALENDAR BEGIN:VCALENDAR
METHOD:REQUEST METHOD:REQUEST
PRODID:-//INBUXA//INBUXA Server//EN PRODID:-//inbuxa//inbuxa Server//EN
VERSION:2.0 VERSION:2.0
BEGIN:VEVENT BEGIN:VEVENT
STATUS:CONFIRMED STATUS:CONFIRMED
@@ -402,7 +402,7 @@ summary.dtstart: Time(ItipTime { start: 867787200, tz_id: 32768 })
summary.summary: Text("Conference") summary.summary: Text("Conference")
BEGIN:VCALENDAR BEGIN:VCALENDAR
METHOD:REQUEST METHOD:REQUEST
PRODID:-//INBUXA//INBUXA Server//EN PRODID:-//inbuxa//inbuxa Server//EN
VERSION:2.0 VERSION:2.0
BEGIN:VEVENT BEGIN:VEVENT
STATUS:CONFIRMED STATUS:CONFIRMED
@@ -450,7 +450,7 @@ summary.dtstart: Time(ItipTime { start: 867787200, tz_id: 32768 })
summary.summary: Text("Conference") summary.summary: Text("Conference")
BEGIN:VCALENDAR BEGIN:VCALENDAR
METHOD:REQUEST METHOD:REQUEST
PRODID:-//INBUXA//INBUXA Server//EN PRODID:-//inbuxa//inbuxa Server//EN
VERSION:2.0 VERSION:2.0
BEGIN:VEVENT BEGIN:VEVENT
STATUS:CONFIRMED STATUS:CONFIRMED
@@ -37,7 +37,7 @@ summary.rrule: Rrule(ICalendarRecurrenceRule { freq: Monthly, until: Some(Partia
summary.summary: Text("IETF Calendaring Working Group Meeting") summary.summary: Text("IETF Calendaring Working Group Meeting")
BEGIN:VCALENDAR BEGIN:VCALENDAR
METHOD:REQUEST METHOD:REQUEST
PRODID:-//INBUXA//INBUXA Server//EN PRODID:-//inbuxa//inbuxa Server//EN
VERSION:2.0 VERSION:2.0
BEGIN:VEVENT BEGIN:VEVENT
CLASS:PUBLIC CLASS:PUBLIC
@@ -117,7 +117,7 @@ summary.rrule: Rrule(ICalendarRecurrenceRule { freq: Monthly, until: Some(Partia
summary.summary: Text("IETF Calendaring Working Group Meeting") summary.summary: Text("IETF Calendaring Working Group Meeting")
BEGIN:VCALENDAR BEGIN:VCALENDAR
METHOD:REQUEST METHOD:REQUEST
PRODID:-//INBUXA//INBUXA Server//EN PRODID:-//inbuxa//inbuxa Server//EN
VERSION:2.0 VERSION:2.0
BEGIN:VEVENT BEGIN:VEVENT
CLASS:PUBLIC CLASS:PUBLIC
@@ -207,7 +207,7 @@ summary.rrule: Rrule(ICalendarRecurrenceRule { freq: Monthly, until: Some(Partia
summary.summary: Text("IETF Calendaring Working Group Meeting") summary.summary: Text("IETF Calendaring Working Group Meeting")
BEGIN:VCALENDAR BEGIN:VCALENDAR
METHOD:CANCEL METHOD:CANCEL
PRODID:-//INBUXA//INBUXA Server//EN PRODID:-//inbuxa//inbuxa Server//EN
VERSION:2.0 VERSION:2.0
BEGIN:VEVENT BEGIN:VEVENT
STATUS:CANCELLED STATUS:CANCELLED
@@ -226,7 +226,7 @@ END:VCALENDAR
# Make sure B has the cancelled event # Make sure B has the cancelled event
> get [email protected] [email protected] > get [email protected] [email protected]
BEGIN:VCALENDAR BEGIN:VCALENDAR
PRODID:-//INBUXA//INBUXA Server//EN PRODID:-//inbuxa//inbuxa Server//EN
VERSION:2.0 VERSION:2.0
BEGIN:VEVENT BEGIN:VEVENT
CLASS:PUBLIC CLASS:PUBLIC
@@ -358,7 +358,7 @@ summary.rrule: Rrule(ICalendarRecurrenceRule { freq: Monthly, until: Some(Partia
summary.summary: Text("IETF Calendaring Working Group Meeting") summary.summary: Text("IETF Calendaring Working Group Meeting")
BEGIN:VCALENDAR BEGIN:VCALENDAR
METHOD:REQUEST METHOD:REQUEST
PRODID:-//INBUXA//INBUXA Server//EN PRODID:-//inbuxa//inbuxa Server//EN
VERSION:2.0 VERSION:2.0
BEGIN:VEVENT BEGIN:VEVENT
CLASS:PUBLIC CLASS:PUBLIC
@@ -386,7 +386,7 @@ END:VCALENDAR
# Make sure B has the complete event including all updates # Make sure B has the complete event including all updates
> get [email protected] [email protected] > get [email protected] [email protected]
BEGIN:VCALENDAR BEGIN:VCALENDAR
PRODID:-//INBUXA//INBUXA Server//EN PRODID:-//inbuxa//inbuxa Server//EN
VERSION:2.0 VERSION:2.0
BEGIN:VEVENT BEGIN:VEVENT
CLASS:PUBLIC CLASS:PUBLIC
@@ -538,7 +538,7 @@ summary.rrule: Rrule(ICalendarRecurrenceRule { freq: Monthly, until: Some(Partia
summary.summary: Text("IETF Calendaring Working Group Meeting") summary.summary: Text("IETF Calendaring Working Group Meeting")
BEGIN:VCALENDAR BEGIN:VCALENDAR
METHOD:CANCEL METHOD:CANCEL
PRODID:-//INBUXA//INBUXA Server//EN PRODID:-//inbuxa//inbuxa Server//EN
VERSION:2.0 VERSION:2.0
BEGIN:VEVENT BEGIN:VEVENT
DESCRIPTION:IETF-C&S Conference Call DESCRIPTION:IETF-C&S Conference Call
@@ -564,7 +564,7 @@ END:VCALENDAR
# Make sure all instances in B were deleted # Make sure all instances in B were deleted
> get [email protected] [email protected] > get [email protected] [email protected]
BEGIN:VCALENDAR BEGIN:VCALENDAR
PRODID:-//INBUXA//INBUXA Server//EN PRODID:-//inbuxa//inbuxa Server//EN
VERSION:2.0 VERSION:2.0
BEGIN:VEVENT BEGIN:VEVENT
CLASS:PUBLIC CLASS:PUBLIC
@@ -677,7 +677,7 @@ summary.rrule: Rrule(ICalendarRecurrenceRule { freq: Weekly, until: None, count:
summary.summary: Text("Review Accounts") summary.summary: Text("Review Accounts")
BEGIN:VCALENDAR BEGIN:VCALENDAR
METHOD:REQUEST METHOD:REQUEST
PRODID:-//INBUXA//INBUXA Server//EN PRODID:-//inbuxa//inbuxa Server//EN
VERSION:2.0 VERSION:2.0
BEGIN:VEVENT BEGIN:VEVENT
LOCATION:The White Room LOCATION:The White Room
@@ -742,7 +742,7 @@ summary.summary: Text("Review Accounts")
~summary.rrule: Rrule(ICalendarRecurrenceRule { freq: Weekly, until: None, count: None, interval: None, bysecond: [], byminute: [], byhour: [], byday: [ICalendarDay { ordwk: None, weekday: Tuesday }], bymonthday: [], byyearday: [], byweekno: [], bymonth: [], bysetpos: [], wkst: Some(Sunday), rscale: None, skip: None }) ~summary.rrule: Rrule(ICalendarRecurrenceRule { freq: Weekly, until: None, count: None, interval: None, bysecond: [], byminute: [], byhour: [], byday: [ICalendarDay { ordwk: None, weekday: Tuesday }], bymonthday: [], byyearday: [], byweekno: [], bymonth: [], bysetpos: [], wkst: Some(Sunday), rscale: None, skip: None })
BEGIN:VCALENDAR BEGIN:VCALENDAR
METHOD:REQUEST METHOD:REQUEST
PRODID:-//INBUXA//INBUXA Server//EN PRODID:-//inbuxa//inbuxa Server//EN
VERSION:2.0 VERSION:2.0
BEGIN:VEVENT BEGIN:VEVENT
LOCATION:The White Room LOCATION:The White Room
@@ -770,7 +770,7 @@ summary.rrule: Rrule(ICalendarRecurrenceRule { freq: Weekly, until: None, count:
summary.summary: Text("Review Accounts") summary.summary: Text("Review Accounts")
BEGIN:VCALENDAR BEGIN:VCALENDAR
METHOD:CANCEL METHOD:CANCEL
PRODID:-//INBUXA//INBUXA Server//EN PRODID:-//inbuxa//inbuxa Server//EN
VERSION:2.0 VERSION:2.0
BEGIN:VEVENT BEGIN:VEVENT
LOCATION:The Red Room LOCATION:The Red Room
@@ -791,7 +791,7 @@ END:VCALENDAR
# Make sure B has the updated event # Make sure B has the updated event
> get [email protected] [email protected] > get [email protected] [email protected]
BEGIN:VCALENDAR BEGIN:VCALENDAR
PRODID:-//INBUXA//INBUXA Server//EN PRODID:-//inbuxa//inbuxa Server//EN
VERSION:2.0 VERSION:2.0
BEGIN:VEVENT BEGIN:VEVENT
LOCATION:The White Room LOCATION:The White Room
@@ -812,7 +812,7 @@ END:VCALENDAR
# Make sure C has the updated event # Make sure C has the updated event
> get [email protected] [email protected] > get [email protected] [email protected]
BEGIN:VCALENDAR BEGIN:VCALENDAR
PRODID:-//INBUXA//INBUXA Server//EN PRODID:-//inbuxa//inbuxa Server//EN
VERSION:2.0 VERSION:2.0
BEGIN:VEVENT BEGIN:VEVENT
LOCATION:The Red Room LOCATION:The Red Room
@@ -853,7 +853,7 @@ summary.rrule: Rrule(ICalendarRecurrenceRule { freq: Weekly, until: None, count:
summary.summary: Text("Review Accounts") summary.summary: Text("Review Accounts")
BEGIN:VCALENDAR BEGIN:VCALENDAR
METHOD:REPLY METHOD:REPLY
PRODID:-//INBUXA//INBUXA Server//EN PRODID:-//inbuxa//inbuxa Server//EN
VERSION:2.0 VERSION:2.0
BEGIN:VEVENT BEGIN:VEVENT
SUMMARY:Review Accounts SUMMARY:Review Accounts
@@ -919,7 +919,7 @@ summary.rrule: Rrule(ICalendarRecurrenceRule { freq: Weekly, until: None, count:
summary.summary: Text("Weekly Sync") summary.summary: Text("Weekly Sync")
BEGIN:VCALENDAR BEGIN:VCALENDAR
METHOD:REQUEST METHOD:REQUEST
PRODID:-//INBUXA//INBUXA Server//EN PRODID:-//inbuxa//inbuxa Server//EN
VERSION:2.0 VERSION:2.0
BEGIN:VEVENT BEGIN:VEVENT
SUMMARY:Weekly Sync SUMMARY:Weekly Sync
@@ -967,7 +967,7 @@ summary.rrule: Rrule(ICalendarRecurrenceRule { freq: Weekly, until: None, count:
summary.summary: Text("Weekly Sync") summary.summary: Text("Weekly Sync")
BEGIN:VCALENDAR BEGIN:VCALENDAR
METHOD:REPLY METHOD:REPLY
PRODID:-//INBUXA//INBUXA Server//EN PRODID:-//inbuxa//inbuxa Server//EN
VERSION:2.0 VERSION:2.0
BEGIN:VEVENT BEGIN:VEVENT
SUMMARY:Weekly Sync SUMMARY:Weekly Sync
+20 -20
View File
@@ -32,7 +32,7 @@ summary.dtstart: Time(ItipTime { start: 867787200, tz_id: 32768 })
summary.summary: Text("Conference") summary.summary: Text("Conference")
BEGIN:VCALENDAR BEGIN:VCALENDAR
METHOD:REQUEST METHOD:REQUEST
PRODID:-//INBUXA//INBUXA Server//EN PRODID:-//inbuxa//inbuxa Server//EN
VERSION:2.0 VERSION:2.0
BEGIN:VEVENT BEGIN:VEVENT
STATUS:CONFIRMED STATUS:CONFIRMED
@@ -61,7 +61,7 @@ END:VCALENDAR
# Make sure B receives the request # Make sure B receives the request
> get [email protected] [email protected] > get [email protected] [email protected]
BEGIN:VCALENDAR BEGIN:VCALENDAR
PRODID:-//INBUXA//INBUXA Server//EN PRODID:-//inbuxa//inbuxa Server//EN
VERSION:2.0 VERSION:2.0
BEGIN:VEVENT BEGIN:VEVENT
STATUS:CONFIRMED STATUS:CONFIRMED
@@ -88,7 +88,7 @@ END:VCALENDAR
> put [email protected] [email protected] > put [email protected] [email protected]
BEGIN:VCALENDAR BEGIN:VCALENDAR
VERSION:2.0 VERSION:2.0
PRODID:-//INBUXA//INBUXA Server//EN PRODID:-//inbuxa//inbuxa Server//EN
BEGIN:VEVENT BEGIN:VEVENT
DTSTAMP:19970701T200000Z DTSTAMP:19970701T200000Z
SEQUENCE:1 SEQUENCE:1
@@ -119,7 +119,7 @@ summary.dtstart: Time(ItipTime { start: 867787200, tz_id: 32768 })
summary.summary: Text("Conference") summary.summary: Text("Conference")
BEGIN:VCALENDAR BEGIN:VCALENDAR
METHOD:REPLY METHOD:REPLY
PRODID:-//INBUXA//INBUXA Server//EN PRODID:-//inbuxa//inbuxa Server//EN
VERSION:2.0 VERSION:2.0
BEGIN:VEVENT BEGIN:VEVENT
SUMMARY:Conference SUMMARY:Conference
@@ -198,7 +198,7 @@ summary.summary: Text("Phone Conference")
~summary.summary: Text("Conference") ~summary.summary: Text("Conference")
BEGIN:VCALENDAR BEGIN:VCALENDAR
METHOD:REQUEST METHOD:REQUEST
PRODID:-//INBUXA//INBUXA Server//EN PRODID:-//inbuxa//inbuxa Server//EN
VERSION:2.0 VERSION:2.0
BEGIN:VEVENT BEGIN:VEVENT
STATUS:CONFIRMED STATUS:CONFIRMED
@@ -228,7 +228,7 @@ END:VCALENDAR
# Make sure C receives the request # Make sure C receives the request
> get [email protected] [email protected] > get [email protected] [email protected]
BEGIN:VCALENDAR BEGIN:VCALENDAR
PRODID:-//INBUXA//INBUXA Server//EN PRODID:-//inbuxa//inbuxa Server//EN
VERSION:2.0 VERSION:2.0
BEGIN:VEVENT BEGIN:VEVENT
STATUS:CONFIRMED STATUS:CONFIRMED
@@ -256,7 +256,7 @@ END:VCALENDAR
> put [email protected] [email protected] > put [email protected] [email protected]
BEGIN:VCALENDAR BEGIN:VCALENDAR
VERSION:2.0 VERSION:2.0
PRODID:-//INBUXA//INBUXA Server//EN PRODID:-//inbuxa//inbuxa Server//EN
BEGIN:VEVENT BEGIN:VEVENT
DTSTAMP:19970701T180000Z DTSTAMP:19970701T180000Z
UID:[email protected] UID:[email protected]
@@ -287,7 +287,7 @@ summary.dtstart: Time(ItipTime { start: 867780000, tz_id: 32768 })
summary.summary: Text("Phone Conference") summary.summary: Text("Phone Conference")
BEGIN:VCALENDAR BEGIN:VCALENDAR
METHOD:REPLY METHOD:REPLY
PRODID:-//INBUXA//INBUXA Server//EN PRODID:-//inbuxa//inbuxa Server//EN
VERSION:2.0 VERSION:2.0
BEGIN:VEVENT BEGIN:VEVENT
SUMMARY:Phone Conference SUMMARY:Phone Conference
@@ -313,7 +313,7 @@ summary.dtstart: Time(ItipTime { start: 867780000, tz_id: 32768 })
summary.summary: Text("Phone Conference") summary.summary: Text("Phone Conference")
BEGIN:VCALENDAR BEGIN:VCALENDAR
METHOD:REQUEST METHOD:REQUEST
PRODID:-//INBUXA//INBUXA Server//EN PRODID:-//inbuxa//inbuxa Server//EN
VERSION:2.0 VERSION:2.0
BEGIN:VEVENT BEGIN:VEVENT
STATUS:CONFIRMED STATUS:CONFIRMED
@@ -343,7 +343,7 @@ END:VCALENDAR
# Make sure E receives the request # Make sure E receives the request
> get [email protected] [email protected] > get [email protected] [email protected]
BEGIN:VCALENDAR BEGIN:VCALENDAR
PRODID:-//INBUXA//INBUXA Server//EN PRODID:-//inbuxa//inbuxa Server//EN
VERSION:2.0 VERSION:2.0
BEGIN:VEVENT BEGIN:VEVENT
STATUS:CONFIRMED STATUS:CONFIRMED
@@ -398,7 +398,7 @@ END:VCALENDAR
> put [email protected] [email protected] > put [email protected] [email protected]
BEGIN:VCALENDAR BEGIN:VCALENDAR
VERSION:2.0 VERSION:2.0
PRODID:-//INBUXA//INBUXA Server//EN PRODID:-//inbuxa//inbuxa Server//EN
BEGIN:VEVENT BEGIN:VEVENT
DTSTAMP:19970701T180000Z DTSTAMP:19970701T180000Z
SEQUENCE:2 SEQUENCE:2
@@ -430,7 +430,7 @@ summary.dtstart: Time(ItipTime { start: 867780000, tz_id: 32768 })
summary.summary: Text("Phone Conference") summary.summary: Text("Phone Conference")
BEGIN:VCALENDAR BEGIN:VCALENDAR
METHOD:REPLY METHOD:REPLY
PRODID:-//INBUXA//INBUXA Server//EN PRODID:-//inbuxa//inbuxa Server//EN
VERSION:2.0 VERSION:2.0
BEGIN:VEVENT BEGIN:VEVENT
SUMMARY:Phone Conference SUMMARY:Phone Conference
@@ -479,7 +479,7 @@ END:VCALENDAR
# Make sure C receives the reply # Make sure C receives the reply
> get [email protected] [email protected] > get [email protected] [email protected]
BEGIN:VCALENDAR BEGIN:VCALENDAR
PRODID:-//INBUXA//INBUXA Server//EN PRODID:-//inbuxa//inbuxa Server//EN
VERSION:2.0 VERSION:2.0
BEGIN:VEVENT BEGIN:VEVENT
STATUS:CONFIRMED STATUS:CONFIRMED
@@ -508,7 +508,7 @@ END:VCALENDAR
> put [email protected] [email protected] > put [email protected] [email protected]
BEGIN:VCALENDAR BEGIN:VCALENDAR
VERSION:2.0 VERSION:2.0
PRODID:-//INBUXA//INBUXA Server//EN PRODID:-//inbuxa//inbuxa Server//EN
BEGIN:VEVENT BEGIN:VEVENT
DTSTAMP:19970701T180000Z DTSTAMP:19970701T180000Z
SEQUENCE:2 SEQUENCE:2
@@ -540,7 +540,7 @@ summary.dtstart: Time(ItipTime { start: 867780000, tz_id: 32768 })
summary.summary: Text("Phone Conference") summary.summary: Text("Phone Conference")
BEGIN:VCALENDAR BEGIN:VCALENDAR
METHOD:REPLY METHOD:REPLY
PRODID:-//INBUXA//INBUXA Server//EN PRODID:-//inbuxa//inbuxa Server//EN
VERSION:2.0 VERSION:2.0
BEGIN:VEVENT BEGIN:VEVENT
SUMMARY:Phone Conference SUMMARY:Phone Conference
@@ -564,7 +564,7 @@ END:VCALENDAR
# Make sure C receives the reply # Make sure C receives the reply
> get [email protected] [email protected] > get [email protected] [email protected]
BEGIN:VCALENDAR BEGIN:VCALENDAR
PRODID:-//INBUXA//INBUXA Server//EN PRODID:-//inbuxa//inbuxa Server//EN
VERSION:2.0 VERSION:2.0
BEGIN:VEVENT BEGIN:VEVENT
STATUS:CONFIRMED STATUS:CONFIRMED
@@ -650,7 +650,7 @@ summary.summary: Text("Phone Conference")
~summary.attendee: Participants([ItipParticipant { email: "[email protected]", name: None, is_organizer: true }, ItipParticipant { email: "[email protected]", name: None, is_organizer: false }, ItipParticipant { email: "[email protected]", name: None, is_organizer: false }, ItipParticipant { email: "[email protected]", name: None, is_organizer: false }, ItipParticipant { email: "[email protected]", name: Some("Hal"), is_organizer: false }, ItipParticipant { email: "[email protected]", name: None, is_organizer: false }]) ~summary.attendee: Participants([ItipParticipant { email: "[email protected]", name: None, is_organizer: true }, ItipParticipant { email: "[email protected]", name: None, is_organizer: false }, ItipParticipant { email: "[email protected]", name: None, is_organizer: false }, ItipParticipant { email: "[email protected]", name: None, is_organizer: false }, ItipParticipant { email: "[email protected]", name: Some("Hal"), is_organizer: false }, ItipParticipant { email: "[email protected]", name: None, is_organizer: false }])
BEGIN:VCALENDAR BEGIN:VCALENDAR
METHOD:REQUEST METHOD:REQUEST
PRODID:-//INBUXA//INBUXA Server//EN PRODID:-//inbuxa//inbuxa Server//EN
VERSION:2.0 VERSION:2.0
BEGIN:VEVENT BEGIN:VEVENT
STATUS:CONFIRMED STATUS:CONFIRMED
@@ -681,7 +681,7 @@ summary.dtstart: Time(ItipTime { start: 867780000, tz_id: 32768 })
summary.summary: Text("Phone Conference") summary.summary: Text("Phone Conference")
BEGIN:VCALENDAR BEGIN:VCALENDAR
METHOD:CANCEL METHOD:CANCEL
PRODID:-//INBUXA//INBUXA Server//EN PRODID:-//inbuxa//inbuxa Server//EN
VERSION:2.0 VERSION:2.0
BEGIN:VEVENT BEGIN:VEVENT
STATUS:CANCELLED STATUS:CANCELLED
@@ -702,7 +702,7 @@ END:VCALENDAR
# Make sure B receives the cancelation # Make sure B receives the cancelation
> get [email protected] [email protected] > get [email protected] [email protected]
BEGIN:VCALENDAR BEGIN:VCALENDAR
PRODID:-//INBUXA//INBUXA Server//EN PRODID:-//inbuxa//inbuxa Server//EN
VERSION:2.0 VERSION:2.0
BEGIN:VEVENT BEGIN:VEVENT
STATUS:CANCELLED STATUS:CANCELLED
@@ -727,7 +727,7 @@ summary.dtstart: Time(ItipTime { start: 867780000, tz_id: 32768 })
summary.summary: Text("Phone Conference") summary.summary: Text("Phone Conference")
BEGIN:VCALENDAR BEGIN:VCALENDAR
METHOD:CANCEL METHOD:CANCEL
PRODID:-//INBUXA//INBUXA Server//EN PRODID:-//inbuxa//inbuxa Server//EN
VERSION:2.0 VERSION:2.0
BEGIN:VEVENT BEGIN:VEVENT
STATUS:CANCELLED STATUS:CANCELLED
+9 -9
View File
@@ -32,7 +32,7 @@ summary.dtstart: Time(ItipTime { start: 867776400, tz_id: 32768 })
summary.summary: Text("Create the requirements document") summary.summary: Text("Create the requirements document")
BEGIN:VCALENDAR BEGIN:VCALENDAR
METHOD:REQUEST METHOD:REQUEST
PRODID:-//INBUXA//INBUXA Server//EN PRODID:-//inbuxa//inbuxa Server//EN
VERSION:2.0 VERSION:2.0
BEGIN:VTODO BEGIN:VTODO
PRIORITY:1 PRIORITY:1
@@ -57,7 +57,7 @@ END:VCALENDAR
# Make sure B received the todo # Make sure B received the todo
> get [email protected] [email protected] > get [email protected] [email protected]
BEGIN:VCALENDAR BEGIN:VCALENDAR
PRODID:-//INBUXA//INBUXA Server//EN PRODID:-//inbuxa//inbuxa Server//EN
VERSION:2.0 VERSION:2.0
BEGIN:VTODO BEGIN:VTODO
PRIORITY:1 PRIORITY:1
@@ -103,7 +103,7 @@ summary.attendee: Participants([ItipParticipant { email: "[email protected]", name:
summary.dtstart: Time(ItipTime { start: 867776400, tz_id: 32768 }) summary.dtstart: Time(ItipTime { start: 867776400, tz_id: 32768 })
BEGIN:VCALENDAR BEGIN:VCALENDAR
METHOD:REPLY METHOD:REPLY
PRODID:-//INBUXA//INBUXA Server//EN PRODID:-//inbuxa//inbuxa Server//EN
VERSION:2.0 VERSION:2.0
BEGIN:VTODO BEGIN:VTODO
STATUS:IN-PROCESS STATUS:IN-PROCESS
@@ -172,7 +172,7 @@ summary.attendee: Participants([ItipParticipant { email: "[email protected]", name:
summary.dtstart: Time(ItipTime { start: 867776400, tz_id: 32768 }) summary.dtstart: Time(ItipTime { start: 867776400, tz_id: 32768 })
BEGIN:VCALENDAR BEGIN:VCALENDAR
METHOD:REPLY METHOD:REPLY
PRODID:-//INBUXA//INBUXA Server//EN PRODID:-//inbuxa//inbuxa Server//EN
VERSION:2.0 VERSION:2.0
BEGIN:VTODO BEGIN:VTODO
PERCENT-COMPLETE:75 PERCENT-COMPLETE:75
@@ -241,7 +241,7 @@ summary.attendee: Participants([ItipParticipant { email: "[email protected]", name:
summary.dtstart: Time(ItipTime { start: 867776400, tz_id: 32768 }) summary.dtstart: Time(ItipTime { start: 867776400, tz_id: 32768 })
BEGIN:VCALENDAR BEGIN:VCALENDAR
METHOD:REPLY METHOD:REPLY
PRODID:-//INBUXA//INBUXA Server//EN PRODID:-//inbuxa//inbuxa Server//EN
VERSION:2.0 VERSION:2.0
BEGIN:VTODO BEGIN:VTODO
DUE:19970722T170000Z DUE:19970722T170000Z
@@ -317,7 +317,7 @@ summary.summary: Text("Send Status Reports to Area Managers")
~summary.summary: Text("Create the requirements document") ~summary.summary: Text("Create the requirements document")
BEGIN:VCALENDAR BEGIN:VCALENDAR
METHOD:REQUEST METHOD:REQUEST
PRODID:-//INBUXA//INBUXA Server//EN PRODID:-//inbuxa//inbuxa Server//EN
VERSION:2.0 VERSION:2.0
BEGIN:VTODO BEGIN:VTODO
PRIORITY:1 PRIORITY:1
@@ -347,7 +347,7 @@ summary.rrule: Rrule(ICalendarRecurrenceRule { freq: Monthly, until: None, count
summary.summary: Text("Create the requirements document") summary.summary: Text("Create the requirements document")
BEGIN:VCALENDAR BEGIN:VCALENDAR
METHOD:CANCEL METHOD:CANCEL
PRODID:-//INBUXA//INBUXA Server//EN PRODID:-//inbuxa//inbuxa Server//EN
VERSION:2.0 VERSION:2.0
BEGIN:VTODO BEGIN:VTODO
STATUS:CANCELLED STATUS:CANCELLED
@@ -367,7 +367,7 @@ END:VCALENDAR
# Make sure "C" received the cancel request # Make sure "C" received the cancel request
> get [email protected] [email protected] > get [email protected] [email protected]
BEGIN:VCALENDAR BEGIN:VCALENDAR
PRODID:-//INBUXA//INBUXA Server//EN PRODID:-//inbuxa//inbuxa Server//EN
VERSION:2.0 VERSION:2.0
BEGIN:VTODO BEGIN:VTODO
PRIORITY:1 PRIORITY:1
@@ -451,7 +451,7 @@ summary.rrule: Rrule(ICalendarRecurrenceRule { freq: Monthly, until: None, count
summary.summary: Text("Send Status Reports to Area Managers") summary.summary: Text("Send Status Reports to Area Managers")
BEGIN:VCALENDAR BEGIN:VCALENDAR
METHOD:REPLY METHOD:REPLY
PRODID:-//INBUXA//INBUXA Server//EN PRODID:-//inbuxa//inbuxa Server//EN
VERSION:2.0 VERSION:2.0
BEGIN:VTODO BEGIN:VTODO
PERCENT-COMPLETE:75 PERCENT-COMPLETE:75
+4 -4
View File
@@ -47,7 +47,7 @@ summary.rrule: Rrule(ICalendarRecurrenceRule { freq: Daily, until: None, count:
summary.summary: Text("Review Internet-Draft") summary.summary: Text("Review Internet-Draft")
BEGIN:VCALENDAR BEGIN:VCALENDAR
METHOD:REQUEST METHOD:REQUEST
PRODID:-//INBUXA//INBUXA Server//EN PRODID:-//inbuxa//inbuxa Server//EN
VERSION:2.0 VERSION:2.0
BEGIN:VEVENT BEGIN:VEVENT
SUMMARY:Review Internet-Draft SUMMARY:Review Internet-Draft
@@ -89,7 +89,7 @@ END:VCALENDAR
# Make sure the participant receives the event # Make sure the participant receives the event
> get [email protected] 9263504FD3AD > get [email protected] 9263504FD3AD
BEGIN:VCALENDAR BEGIN:VCALENDAR
PRODID:-//INBUXA//INBUXA Server//EN PRODID:-//inbuxa//inbuxa Server//EN
VERSION:2.0 VERSION:2.0
BEGIN:VEVENT BEGIN:VEVENT
SUMMARY:Review Internet-Draft SUMMARY:Review Internet-Draft
@@ -185,7 +185,7 @@ summary.rrule: Rrule(ICalendarRecurrenceRule { freq: Daily, until: None, count:
summary.summary: Text("Review Internet-Draft") summary.summary: Text("Review Internet-Draft")
BEGIN:VCALENDAR BEGIN:VCALENDAR
METHOD:REPLY METHOD:REPLY
PRODID:-//INBUXA//INBUXA Server//EN PRODID:-//inbuxa//inbuxa Server//EN
VERSION:2.0 VERSION:2.0
BEGIN:VEVENT BEGIN:VEVENT
SUMMARY:Review Internet-Draft SUMMARY:Review Internet-Draft
@@ -343,7 +343,7 @@ summary.rrule: Rrule(ICalendarRecurrenceRule { freq: Daily, until: None, count:
summary.summary: Text("Review Internet-Draft") summary.summary: Text("Review Internet-Draft")
BEGIN:VCALENDAR BEGIN:VCALENDAR
METHOD:REPLY METHOD:REPLY
PRODID:-//INBUXA//INBUXA Server//EN PRODID:-//inbuxa//inbuxa Server//EN
VERSION:2.0 VERSION:2.0
BEGIN:VEVENT BEGIN:VEVENT
SUMMARY:Review Internet-Draft SUMMARY:Review Internet-Draft
+3 -3
View File
@@ -30,7 +30,7 @@ summary.dtstart: Time(ItipTime { start: 1243958400, tz_id: 32768 })
summary.summary: Text("Lunch") summary.summary: Text("Lunch")
BEGIN:VCALENDAR BEGIN:VCALENDAR
METHOD:REQUEST METHOD:REQUEST
PRODID:-//INBUXA//INBUXA Server//EN PRODID:-//inbuxa//inbuxa Server//EN
VERSION:2.0 VERSION:2.0
BEGIN:VEVENT BEGIN:VEVENT
SUMMARY:Lunch SUMMARY:Lunch
@@ -83,7 +83,7 @@ END:VCALENDAR
# Make sure the message was received by the attendees # Make sure the message was received by the attendees
> get [email protected] 9263504FD3AD > get [email protected] 9263504FD3AD
BEGIN:VCALENDAR BEGIN:VCALENDAR
PRODID:-//INBUXA//INBUXA Server//EN PRODID:-//inbuxa//inbuxa Server//EN
VERSION:2.0 VERSION:2.0
BEGIN:VEVENT BEGIN:VEVENT
SUMMARY:Lunch SUMMARY:Lunch
@@ -170,7 +170,7 @@ summary.dtstart: Time(ItipTime { start: 1243958400, tz_id: 32768 })
summary.summary: Text("Lunch") summary.summary: Text("Lunch")
BEGIN:VCALENDAR BEGIN:VCALENDAR
METHOD:REPLY METHOD:REPLY
PRODID:-//INBUXA//INBUXA Server//EN PRODID:-//inbuxa//inbuxa Server//EN
VERSION:2.0 VERSION:2.0
BEGIN:VEVENT BEGIN:VEVENT
SUMMARY:Lunch SUMMARY:Lunch
+4 -4
View File
@@ -26,7 +26,7 @@ summary.dtstart: Time(ItipTime { start: 1794322800, tz_id: 433 })
summary.summary: Text("Berlin meeting") summary.summary: Text("Berlin meeting")
BEGIN:VCALENDAR BEGIN:VCALENDAR
METHOD:REQUEST METHOD:REQUEST
PRODID:-//INBUXA//INBUXA Server//EN PRODID:-//inbuxa//inbuxa Server//EN
VERSION:2.0 VERSION:2.0
BEGIN:VEVENT BEGIN:VEVENT
SUMMARY:Berlin meeting SUMMARY:Berlin meeting
@@ -92,7 +92,7 @@ summary.dtstart: Time(ItipTime { start: 1794322800, tz_id: 433 })
summary.summary: Text("Berlin meeting") summary.summary: Text("Berlin meeting")
BEGIN:VCALENDAR BEGIN:VCALENDAR
METHOD:REPLY METHOD:REPLY
PRODID:-//INBUXA//INBUXA Server//EN PRODID:-//inbuxa//inbuxa Server//EN
VERSION:2.0 VERSION:2.0
BEGIN:VEVENT BEGIN:VEVENT
SUMMARY:Berlin meeting SUMMARY:Berlin meeting
@@ -144,7 +144,7 @@ summary.dtstart: Time(ItipTime { start: 1794322800, tz_id: 433 })
summary.summary: Text("Berlin meeting") summary.summary: Text("Berlin meeting")
BEGIN:VCALENDAR BEGIN:VCALENDAR
METHOD:CANCEL METHOD:CANCEL
PRODID:-//INBUXA//INBUXA Server//EN PRODID:-//inbuxa//inbuxa Server//EN
VERSION:2.0 VERSION:2.0
BEGIN:VEVENT BEGIN:VEVENT
STATUS:CANCELLED STATUS:CANCELLED
@@ -228,7 +228,7 @@ summary.dtstart: Time(ItipTime { start: 1794322800, tz_id: 433 })
summary.summary: Text("Berlin meeting") summary.summary: Text("Berlin meeting")
BEGIN:VCALENDAR BEGIN:VCALENDAR
METHOD:REQUEST METHOD:REQUEST
PRODID:-//INBUXA//INBUXA Server//EN PRODID:-//inbuxa//inbuxa Server//EN
VERSION:2.0 VERSION:2.0
BEGIN:VEVENT BEGIN:VEVENT
SUMMARY:Berlin meeting SUMMARY:Berlin meeting
@@ -1,4 +1,4 @@
require ["fileinto", "mailbox", "mailboxid", "special-use", "ihave", "imap4flags", "vnd.stalwart.expressions"]; require ["fileinto", "mailbox", "mailboxid", "special-use", "ihave", "imap4flags", "vnd.inbuxa.expressions"];
# SpecialUse extension tests # SpecialUse extension tests
if not specialuse_exists ["inbox", "trash"] { if not specialuse_exists ["inbox", "trash"] {
@@ -8,7 +8,7 @@ import urllib.error
import urllib.request import urllib.request
CORE = "urn:ietf:params:jmap:core" CORE = "urn:ietf:params:jmap:core"
STALWART = "urn:stalwart:jmap" STALWART = "urn:inbuxa:jmap:registry"
USING = [CORE, STALWART] USING = [CORE, STALWART]
DEFAULT_BASE_URL = "https://127.0.0.1" DEFAULT_BASE_URL = "https://127.0.0.1"
+1 -1
View File
@@ -1,4 +1,4 @@
require ["variables", "include", "vnd.stalwart.expressions", "reject"]; require ["variables", "include", "vnd.inbuxa.expressions", "reject"];
global "score"; global "score";
+1 -1
View File
@@ -1,4 +1,4 @@
require ["variables", "include", "vnd.stalwart.expressions", "reject"]; require ["variables", "include", "vnd.inbuxa.expressions", "reject"];
global "score"; global "score";
set "awl_factor" "0.5"; set "awl_factor" "0.5";
+1 -1
View File
@@ -1,4 +1,4 @@
require ["variables", "envelope", "reject", "vnd.stalwart.expressions"]; require ["variables", "envelope", "reject", "vnd.inbuxa.expressions"];
if envelope :localpart :is "from" "spammer" { if envelope :localpart :is "from" "spammer" {
reject "450 4.1.1 Invalid address"; reject "450 4.1.1 Invalid address";
+1 -1
View File
@@ -1,4 +1,4 @@
require ["variables", "envelope", "reject", "vnd.stalwart.expressions"]; require ["variables", "envelope", "reject", "vnd.inbuxa.expressions"];
if envelope :domain :is "to" "foobar.org" { if envelope :domain :is "to" "foobar.org" {
eval "query('sql', 'CREATE TABLE IF NOT EXISTS greylist (addr TEXT PRIMARY KEY)', [])"; eval "query('sql', 'CREATE TABLE IF NOT EXISTS greylist (addr TEXT PRIMARY KEY)', [])";
+1 -1
View File
@@ -47,7 +47,7 @@ pub async fn test(imap: &mut ImapConnection, _imap_check: &mut ImapConnection) {
imap.send("ID").await; imap.send("ID").await;
imap.assert_read(Type::Tagged, ResponseType::Ok) imap.assert_read(Type::Tagged, ResponseType::Ok)
.await .await
.assert_contains("* ID (\"name\" \"INBUXA\" \"version\" "); .assert_contains("* ID (\"name\" \"inbuxa\" \"version\" ");
// Login should be disabled // Login should be disabled
imap.send("LOGIN [email protected] secret").await; imap.send("LOGIN [email protected] secret").await;
+2 -2
View File
@@ -45,7 +45,7 @@ pub async fn test(test: &TestServer) {
list[0], list[0],
json!({ json!({
"id": default_calendar_id, "id": default_calendar_id,
"name": "INBUXA Calendar ([email protected])", "name": "inbuxa Calendar ([email protected])",
"description": null, "description": null,
"sortOrder": 0, "sortOrder": 0,
"isSubscribed": true, "isSubscribed": true,
@@ -310,7 +310,7 @@ pub async fn test(test: &TestServer) {
})); }));
response.list()[1].assert_is_equal(json!({ response.list()[1].assert_is_equal(json!({
"id": default_calendar_id, "id": default_calendar_id,
"name": "INBUXA Calendar ([email protected])", "name": "inbuxa Calendar ([email protected])",
"description": (), "description": (),
"sortOrder": 0, "sortOrder": 0,
"isSubscribed": true, "isSubscribed": true,
+2 -2
View File
@@ -38,7 +38,7 @@ pub async fn test(test: &TestServer) {
assert_eq!( assert_eq!(
list[0], list[0],
json!({ json!({
"name": "INBUXA Address Book ([email protected])", "name": "inbuxa Address Book ([email protected])",
"description": (), "description": (),
"sortOrder": 0, "sortOrder": 0,
"isSubscribed": true, "isSubscribed": true,
@@ -148,7 +148,7 @@ pub async fn test(test: &TestServer) {
"id": addressbook_id, "id": addressbook_id,
}), }),
json!({ json!({
"name": "INBUXA Address Book ([email protected])", "name": "inbuxa Address Book ([email protected])",
"description": (), "description": (),
"sortOrder": 0, "sortOrder": 0,
"isSubscribed": true, "isSubscribed": true,
+5 -4
View File
@@ -250,9 +250,10 @@ pub async fn test(test: &TestServer) {
"webWriteUrlTemplate": null "webWriteUrlTemplate": null
}, },
"urn:ietf:params:jmap:mail:share": {}, "urn:ietf:params:jmap:mail:share": {},
"urn:stalwart:jmap": {}, "urn:inbuxa:jmap:registry": {},
// inbuxa: MT-22, the logo that applies to the account // inbuxa: MT-22, the logo that applies to the account, and
"urn:inbuxa:jmap": { "logo": null }, // LP-19, whether the legacy protocols are open to it
"urn:inbuxa:jmap": { "logo": null, "legacyProtocols": "enabled" },
"https://www.fastmail.com/dev/maskedemail": {} "https://www.fastmail.com/dev/maskedemail": {}
} }
} }
@@ -274,7 +275,7 @@ pub async fn test(test: &TestServer) {
"urn:ietf:params:jmap:principals:availability": john_id, "urn:ietf:params:jmap:principals:availability": john_id,
"urn:ietf:params:jmap:filenode": john_id, "urn:ietf:params:jmap:filenode": john_id,
"urn:ietf:params:jmap:mail:share": john_id, "urn:ietf:params:jmap:mail:share": john_id,
"urn:stalwart:jmap": john_id, "urn:inbuxa:jmap:registry": john_id,
"https://www.fastmail.com/dev/maskedemail": john_id "https://www.fastmail.com/dev/maskedemail": john_id
}, },
"username": "[email protected]", "username": "[email protected]",
+2
View File
@@ -30,6 +30,8 @@ pub mod imap;
#[cfg(test)] #[cfg(test)]
pub mod jmap; pub mod jmap;
#[cfg(test)] #[cfg(test)]
pub mod renamed_identifiers; // inbuxa: SPEC.md §2.4
#[cfg(test)]
pub mod scim; pub mod scim;
#[cfg(test)] #[cfg(test)]
pub mod smtp; pub mod smtp;
+112
View File
@@ -0,0 +1,112 @@
/*
* SPDX-FileCopyrightText: 2026 Coffey Labs
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
//! inbuxa: what an install from before the rename carries over (SPEC.md
//! §2.4). The web interface's OAuth client is retired rather than kept as an
//! alias, and a trained spam filter moves to its new keys.
use crate::utils::server::TestServerBuilder;
use common::manager::{SPAM_CLASSIFIER_KEY, SPAM_TRAINER_KEY, first_party::WEB_INTERFACE_CLIENT_ID};
use registry::{
schema::{
enums::CompressionAlgo,
prelude::{ObjectType, Property},
structs::{Application, OAuthClient},
},
types::map::Map,
};
const OLD_CLIENT_ID: &str = "stalwart-webui";
const OLD_TRAINER_KEY: &[u8] = b"STALWART_SPAM_TRAIN_DATA.lz4";
const OLD_CLASSIFIER_KEY: &[u8] = b"STALWART_SPAM_CLASSIFIER_MODEL.lz4";
#[tokio::test(flavor = "multi_thread")]
async fn renamed_identifiers() {
// The web interface registered under upstream's client id, and an
// application naming it. Disabled, so nothing is fetched for it.
let test = TestServerBuilder::new("renamed_identifiers")
.await
.with_object(OAuthClient {
client_id: OLD_CLIENT_ID.to_string(),
redirect_uris: Map::new(vec!["https://127.0.0.1/admin/oauth/callback".to_string()]),
..Default::default()
})
.await
.with_object(Application {
enabled: false,
description: "Web interface".to_string(),
resource_url: "https://127.0.0.1/webui.zip".to_string(),
url_prefix: Map::new(vec!["/admin".to_string()]),
oauth_client_id: Some(OLD_CLIENT_ID.to_string()),
..Default::default()
})
.await
.disable_services()
.build()
.await;
println!("Running renamed identifier tests...");
let registry = test.server.registry();
assert_eq!(
registry
.primary_key(
ObjectType::OAuthClient.into(),
Property::ClientId,
OLD_CLIENT_ID.as_bytes().to_vec(),
)
.await
.unwrap(),
None,
"the pre-rename web interface client is retired on start"
);
let applications = registry.list::<Application>().await.unwrap();
assert_eq!(applications.len(), 1);
assert_eq!(
applications[0].object.oauth_client_id.as_deref(),
Some(WEB_INTERFACE_CLIENT_ID),
"an application naming the old client moves to the new one"
);
// A trained model under the pre-rename keys moves to the new ones.
let blobs = test.server.blob_store();
for (key, data) in [
(OLD_TRAINER_KEY, &b"trainer"[..]),
(OLD_CLASSIFIER_KEY, &b"classifier"[..]),
] {
blobs.put_blob(key, data, CompressionAlgo::None).await.unwrap();
}
migration::try_migrate(&test.server).await.unwrap();
for (old, new, data) in [
(OLD_TRAINER_KEY, SPAM_TRAINER_KEY, &b"trainer"[..]),
(OLD_CLASSIFIER_KEY, SPAM_CLASSIFIER_KEY, &b"classifier"[..]),
] {
assert_eq!(blobs.get_blob(new, 0..usize::MAX).await.unwrap().as_deref(), Some(data));
assert_eq!(blobs.get_blob(old, 0..usize::MAX).await.unwrap(), None);
}
// A blob already under the new key wins over a stale old one.
blobs
.put_blob(OLD_CLASSIFIER_KEY, b"stale", CompressionAlgo::None)
.await
.unwrap();
migration::try_migrate(&test.server).await.unwrap();
assert_eq!(
blobs
.get_blob(SPAM_CLASSIFIER_KEY, 0..usize::MAX)
.await
.unwrap()
.as_deref(),
Some(&b"classifier"[..])
);
assert_eq!(blobs.get_blob(OLD_CLASSIFIER_KEY, 0..usize::MAX).await.unwrap(), None);
// With nothing left to move, starting again changes nothing.
migration::try_migrate(&test.server).await.unwrap();
assert_eq!(
blobs.get_blob(SPAM_TRAINER_KEY, 0..usize::MAX).await.unwrap().as_deref(),
Some(&b"trainer"[..])
);
}
+1 -1
View File
@@ -106,7 +106,7 @@ async fn discovery(scim: &ScimTest) {
); );
assert!( assert!(
!config.body.contains("stalw"), !config.body.contains("stalw"),
"SCIM-4: documentation is INBUXA's own" "SCIM-4: documentation is inbuxa's own"
); );
let types = anonymous.get("/ResourceTypes").await; let types = anonymous.get("/ResourceTypes").await;
+15 -2
View File
@@ -68,6 +68,10 @@ use std::{
time::{Duration, Instant}, time::{Duration, Instant},
}; };
// inbuxa: its HTTP listener (19048) is shared with the dkim2 and report
// tests, which are serial; without this it ran beside them and each got the
// other's server.
#[serial_test::serial]
#[tokio::test(flavor = "multi_thread")] #[tokio::test(flavor = "multi_thread")]
async fn antispam() { async fn antispam() {
let mut test = TestServerBuilder::new("smtp_antispam_test") let mut test = TestServerBuilder::new("smtp_antispam_test")
@@ -81,9 +85,18 @@ async fn antispam() {
admin admin
.registry_create_object(SpamSettings { .registry_create_object(SpamSettings {
score_spam: Float::new(5.0), score_spam: Float::new(5.0),
// inbuxa: the rules carry the scores the expectations are written
// against, so they're pinned (spam-filter v3.0.2, beside the test
// cases) rather than read from a developer's own checkout, which
// left every score at zero. SPAM_RULES_URL still overrides.
spam_filter_rules_url: std::env::var("SPAM_RULES_URL") spam_filter_rules_url: std::env::var("SPAM_RULES_URL")
.unwrap_or_else(|_| { .unwrap_or_else(|_| {
"file:///Users/me/code/spam-filter/spam-filter-rules.json.gz".to_string() concat!(
"file://",
env!("CARGO_MANIFEST_DIR"),
"/resources/smtp/antispam/spam-filter-rules.json.gz"
)
.to_string()
}) })
.into(), .into(),
..Default::default() ..Default::default()
@@ -156,7 +169,7 @@ async fn antispam() {
})) }))
.await; .await;
// inbuxa: a rules file that can't be read is retried later; don't wait // inbuxa: a rules file that can't be read is retried later; don't wait
// for that retry (the path above is a developer's own checkout) // for that retry (SPAM_RULES_URL may name one that isn't there)
test.wait_for_tasks_skip_not_due().await; test.wait_for_tasks_skip_not_due().await;
admin.reload_settings().await; admin.reload_settings().await;
admin.reload_lookup_stores().await; admin.reload_lookup_stores().await;
+20 -3
View File
@@ -2,6 +2,8 @@
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art> * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
* *
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*
* Modified by Coffey Labs in 2026 for INBUXA.
*/ */
use crate::{ use crate::{
@@ -11,7 +13,7 @@ use crate::{
}, },
utils::server::TestServerBuilder, utils::server::TestServerBuilder,
}; };
use ahash::AHashSet; use ahash::{AHashMap, AHashSet};
use common::{ use common::{
config::smtp::queue::QueueName, config::smtp::queue::QueueName,
ipc::{QueueEvent, QueueEventStatus}, ipc::{QueueEvent, QueueEventStatus},
@@ -180,7 +182,15 @@ async fn queue_retry() {
let attempt = local.expect_message_for_queue_then_deliver("default").await; let attempt = local.expect_message_for_queue_then_deliver("default").await;
let mut dsn = Vec::new(); let mut dsn = Vec::new();
let mut retries = Vec::new(); let mut retries = Vec::new();
// inbuxa: when each attempt started, by test clock. The server sets the
// next retry from its clock when the attempt defers, which is at or after
// this and under a second later, so due - started is the interval or one
// more. Measuring from when the loop next sees the message instead made
// the result shrink by however long saving and reporting took, and it
// failed whenever that crossed a second boundary (under load, often).
let mut started = AHashMap::new();
in_fight.insert(attempt.queue_id); in_fight.insert(attempt.queue_id);
started.insert(attempt.queue_id, now());
attempt.try_deliver(local.server.clone()); attempt.try_deliver(local.server.clone());
loop { loop {
@@ -226,15 +236,22 @@ async fn queue_retry() {
.await; .await;
dsn.push(message); dsn.push(message);
} else { } else {
retries.push(event.due.saturating_sub(now)); retries.push(event.due.saturating_sub(started[&event.queue_id]));
in_fight.insert(event.queue_id); in_fight.insert(event.queue_id);
started.insert(event.queue_id, store::write::now());
event.try_deliver(local.server.clone()); event.try_deliver(local.server.clone());
tokio::time::sleep(Duration::from_millis(100)).await; tokio::time::sleep(Duration::from_millis(100)).await;
} }
} }
} }
local.assert_queue_is_empty().await; local.assert_queue_is_empty().await;
assert_eq!(retries, vec![1, 2, 3]); assert_eq!(retries.len(), 3, "retries: {retries:?}");
for (retry, interval) in retries.iter().zip([1, 2, 3]) {
assert!(
(interval..=interval + 1).contains(retry),
"retry after {retry}s where the schedule says {interval}s: {retries:?}"
);
}
assert_eq!(dsn.len(), 4); assert_eq!(dsn.len(), 4);
let mut dsn = dsn.into_iter(); let mut dsn = dsn.into_iter();
+1 -1
View File
@@ -479,7 +479,7 @@ pub async fn test(test: &mut TestServer) {
.await; .await;
stub.set(Mode::Echo); stub.set(Mode::Echo);
user.activate_script(concat!( user.activate_script(concat!(
"require [\"vnd.stalwart.expressions\", \"editheader\", \"variables\"];\n", "require [\"vnd.inbuxa.expressions\", \"editheader\", \"variables\"];\n",
"let \"a\" \"llm_prompt('echo-test', 'hello world', 0.5)\";\n", "let \"a\" \"llm_prompt('echo-test', 'hello world', 0.5)\";\n",
"let \"b\" \"llm_prompt('no-such-model', 'x', 0.5)\";\n", "let \"b\" \"llm_prompt('no-such-model', 'x', 0.5)\";\n",
"addheader \"X-Llm-Echo\" \"${a}\";\n", "addheader \"X-Llm-Echo\" \"${a}\";\n",
+1 -1
View File
@@ -503,7 +503,7 @@ pub async fn branding_compat() {
let admin = std::env::var("INBUXA_COMPAT_ADMIN").expect("INBUXA_COMPAT_ADMIN"); let admin = std::env::var("INBUXA_COMPAT_ADMIN").expect("INBUXA_COMPAT_ADMIN");
assert!( assert!(
std::env::var("NO_INSERT").is_ok(), std::env::var("NO_INSERT").is_ok(),
"NO_INSERT must be set, or the copy of INBUXA's data is wiped" "NO_INSERT must be set, or the copy of inbuxa's data is wiped"
); );
let test = TestServerBuilder::new("branding_compat") let test = TestServerBuilder::new("branding_compat")
.await .await
+1 -1
View File
@@ -431,7 +431,7 @@ pub async fn masked_email_compat() {
.expect("masks JSON"); .expect("masks JSON");
assert!( assert!(
std::env::var("NO_INSERT").is_ok(), std::env::var("NO_INSERT").is_ok(),
"NO_INSERT must be set, or the copy of INBUXA's data is wiped" "NO_INSERT must be set, or the copy of inbuxa's data is wiped"
); );
let test = TestServerBuilder::new("masked_email_compat") let test = TestServerBuilder::new("masked_email_compat")
+1 -1
View File
@@ -908,7 +908,7 @@ pub async fn tenant_compat() {
.expect("expected JSON"); .expect("expected JSON");
assert!( assert!(
std::env::var("NO_INSERT").is_ok(), std::env::var("NO_INSERT").is_ok(),
"NO_INSERT must be set, or the copy of INBUXA's data is wiped" "NO_INSERT must be set, or the copy of inbuxa's data is wiped"
); );
let test = TestServerBuilder::new("tenant_compat") let test = TestServerBuilder::new("tenant_compat")
+1 -1
View File
@@ -618,7 +618,7 @@ pub async fn undelete_compat() {
.expect("archived items JSON"); .expect("archived items JSON");
assert!( assert!(
std::env::var("NO_INSERT").is_ok(), std::env::var("NO_INSERT").is_ok(),
"NO_INSERT must be set, or the copy of INBUXA's data is wiped" "NO_INSERT must be set, or the copy of inbuxa's data is wiped"
); );
let test = TestServerBuilder::new("undelete_compat") let test = TestServerBuilder::new("undelete_compat")
+3 -1
View File
@@ -2,6 +2,8 @@
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art> * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
* *
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*
* Modified by Coffey Labs in 2026 for INBUXA.
*/ */
use crate::utils::account::Account; use crate::utils::account::Account;
@@ -408,7 +410,7 @@ impl Account {
"urn:ietf:params:jmap:principals:availability", "urn:ietf:params:jmap:principals:availability",
"urn:ietf:params:jmap:filenode", "urn:ietf:params:jmap:filenode",
"urn:ietf:params:jmap:mail:share", "urn:ietf:params:jmap:mail:share",
"urn:stalwart:jmap" "urn:inbuxa:jmap:registry"
], ],
"methodCalls": calls "methodCalls": calls
}); });
+2 -2
View File
@@ -907,7 +907,7 @@ const REPORT_10: &str = r#"<?xml version="1.0" encoding="utf-8" ?>
const REPORT_10_RESPONSE: &str = r#"BEGIN:VCALENDAR const REPORT_10_RESPONSE: &str = r#"BEGIN:VCALENDAR
VERSION:2.0 VERSION:2.0
PRODID:-//INBUXA//INBUXA Server//EN PRODID:-//inbuxa//inbuxa Server//EN
BEGIN:VFREEBUSY BEGIN:VFREEBUSY
DTSTART:20060104T140000Z DTSTART:20060104T140000Z
DTEND:20060105T220000Z DTEND:20060105T220000Z
@@ -928,7 +928,7 @@ const REPORT_11: &str = r#"<?xml version="1.0" encoding="utf-8" ?>
const REPORT_11_RESPONSE: &str = r#"BEGIN:VCALENDAR const REPORT_11_RESPONSE: &str = r#"BEGIN:VCALENDAR
VERSION:2.0 VERSION:2.0
PRODID:-//INBUXA//INBUXA Server//EN PRODID:-//inbuxa//inbuxa Server//EN
BEGIN:VFREEBUSY BEGIN:VFREEBUSY
DTSTART:20060101T000000Z DTSTART:20060101T000000Z
DTEND:20060104T140000Z DTEND:20060104T140000Z
+4 -2
View File
@@ -2,6 +2,8 @@
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art> * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
* *
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*
* Modified by Coffey Labs in 2026 for INBUXA.
*/ */
use crate::utils::{server::TestServer, webdav::GenerateTestDavResource}; use crate::utils::{server::TestServer, webdav::GenerateTestDavResource};
@@ -45,7 +47,7 @@ pub async fn test(test: &TestServer) {
// Test 2: Refreshing a lock token with an invalid a lock token should fail // Test 2: Refreshing a lock token with an invalid a lock token should fail
client client
.lock_refresh(&path, "urn:stalwart:davlock:1234", "infinity", "Second-456") .lock_refresh(&path, "urn:inbuxa:davlock:1234", "infinity", "Second-456")
.await .await
.with_status(StatusCode::PRECONDITION_FAILED); .with_status(StatusCode::PRECONDITION_FAILED);
@@ -148,7 +150,7 @@ pub async fn test(test: &TestServer) {
// Test 10: Unlock with and without a lock token // Test 10: Unlock with and without a lock token
client client
.unlock(&path, "urn:stalwart:davlock:1234") .unlock(&path, "urn:inbuxa:davlock:1234")
.await .await
.with_status(StatusCode::CONFLICT) .with_status(StatusCode::CONFLICT)
.with_value("D:error.D:lock-token-matches-request-uri", ""); .with_value("D:error.D:lock-token-matches-request-uri", "");
+32 -1
View File
@@ -19,7 +19,23 @@ The report's Third-party code section lists upstream code under other
licenses. Files marked **new** need their notice added to `THIRD-PARTY.md` licenses. Files marked **new** need their notice added to `THIRD-PARTY.md`
at the repository root before the import is merged. at the repository root before the import is merged.
It needs Python 3.12+ (for `tarfile`'s `data` filter) and git. It needs Python 3.12+ (for `tarfile`'s `data` filter), git and cargo.
Two passes run after the strip:
- **Renames.** The upstream name is replaced where it's an identifier
clients, users or operators meet: wire-protocol names, the web interface's
client id, store keys, configuration defaults and the served schema, as
`renames.py` lists them. `main` was renamed with the same module. A
re-import arrives purged, so those lines never conflict. Copyright notices
and prose are left alone. The report lists every substitution by file.
- **Build check.** The stripped tree is compiled (`cargo check --workspace
--all-targets`) into `target/strip-check`, which stays warm between
imports. A file that survived the strip but calls code that didn't fails
the run; the report names it. Handle it in the merge into `main`, never on
`upstream`: `upstream` holds the strip's output and nothing else. Imports
the strip left unused are listed without failing. `--no-build-check`
skips the pass.
## name-check.py ## name-check.py
@@ -37,6 +53,21 @@ Rename what it reports. If a string has to stay, such as a key-derivation
context or a wire-protocol identifier, add its `--list` line to the allowlist context or a wire-protocol identifier, add its `--list` line to the allowlist
under the reason it stays. under the reason it stays.
## notice-check.py
Fails when an upstream file the fork changed doesn't carry the AGPL 5(a)
notice, `Modified by Coffey Labs in <year> for INBUXA.`, under upstream's
license line. "Changed" means it differs from the `upstream` branch, so the
list comes from the diff, not from memory. CI runs it beside the name check.
```bash
tools/fork/notice-check.py # exit 1 on a missing notice
tools/fork/notice-check.py --fix # add it where it's missing
```
Run `--fix` after resolving an upstream merge: a conflict resolved by taking
upstream's side can drop a notice the file had.
## record-compat.py ## record-compat.py
Records what the `*_compat` tests compare against, from the Enterprise Records what the `*_compat` tests compare against, from the Enterprise
+18
View File
@@ -0,0 +1,18 @@
# Files in a stripped upstream tree that are expected not to compile, read by
# tools/fork/strip.py's build check. Each is upstream's shared test of a
# feature upstream builds only in Enterprise and the fork rebuilt clean-room
# on `main` (docs/spec/SPEC.md §2.2b, §4), so the stripped tree alone lacks
# what they call. Errors here are reported as expected; an error in any other
# file fails the strip. One path per line.
# OIDC directories: `main` has its own tests/src/directory/oidc.rs.
tests/src/directory/mod.rs
# Tenants and archiving.
tests/src/system/mod.rs
# The LLM spam-filter classifier.
tests/src/smtp/inbound/antispam.rs
# Telemetry: alerts, stored metrics and traces, webhooks.
tests/src/telemetry/alerts.rs
tests/src/telemetry/metrics.rs
tests/src/telemetry/tracing.rs
tests/src/telemetry/webhooks.rs
+15 -46
View File
@@ -2,58 +2,27 @@
# Read by tools/fork/name-check.py. One per line: path<TAB>literal as a JSON # Read by tools/fork/name-check.py. One per line: path<TAB>literal as a JSON
# string, as `name-check.py --list` prints it. Each group says why it stays; # string, as `name-check.py --list` prints it. Each group says why it stays;
# add a line only under a reason, or with a new one. # add a line only under a reason, or with a new one.
#
# Everything clients, users and operators meet was renamed on 2026-09-22
# (docs/spec/SPEC.md §2.4, tools/fork/renames.py). What's left is invisible,
# or names the old spelling in order to retire it.
# Key-derivation contexts. Renaming them invalidates every sealed OAuth token # Key-derivation contexts. Nobody sees them, and renaming them would
# and client id already issued. # invalidate every sealed OAuth token and client id already issued.
crates/common/src/auth/oauth/client_id.rs "stalwart-oauth-client-id-sw1" crates/common/src/auth/oauth/client_id.rs "stalwart-oauth-client-id-sw1"
crates/common/src/auth/oauth/token.rs "stalwart-oauth-token-sw1" crates/common/src/auth/oauth/token.rs "stalwart-oauth-token-sw1"
# Keys and prefixes of data already in the store. # Hashed before it's used as a blob key, so it never appears anywhere.
crates/common/src/manager/application.rs "STALWART_APP_" crates/common/src/manager/application.rs "STALWART_APP_"
crates/common/src/manager/mod.rs "STALWART_SPAM_CLASSIFIER_MODEL.lz4"
crates/common/src/manager/mod.rs "STALWART_SPAM_TRAIN_DATA.lz4"
# The web interface's OAuth client id, which existing installs and the admin # Pre-rename names, read only to retire them: the web interface's OAuth
# front end already use. The id itself, and the tests that check it. # client (removed on start), the spam filter's blobs (moved to their new
# keys), and the environment prefix (refused with the variable to rename).
crates/common/src/manager/first_party.rs "stalwart-webui" crates/common/src/manager/first_party.rs "stalwart-webui"
crates/common/src/manager/application.rs "stalwart-webui" crates/migration/src/lib.rs "STALWART_SPAM_CLASSIFIER_MODEL.lz4"
crates/common/src/manager/application.rs "<meta name=\\\"oauth-client-id\\\" content=\\\"stalwart-webui\\\" />" crates/migration/src/lib.rs "STALWART_SPAM_TRAIN_DATA.lz4"
crates/types/src/branding.rs "STALWART"
# Wire-protocol identifiers clients already hold or negotiate: WebDAV lock and # OPEN, not yet decided (2026-09-22): upstream's published spam-filter rules,
# sync tokens, the JMAP capability, Sieve extensions. # which the server downloads at runtime from this address.
crates/dav/src/common/lock.rs "urn:stalwart:davsync:"
crates/dav/src/common/uri.rs "urn:stalwart:"
crates/dav/src/common/uri.rs "urn:stalwart:davlock:{id:x}"
crates/dav/src/common/uri.rs "urn:stalwart:davsync:"
crates/dav/src/common/uri.rs "urn:stalwart:davsync:{id:x}"
crates/dav/src/common/uri.rs "urn:stalwart:davsync:{id:x}:{seq:x}"
crates/jmap-proto/src/request/capability.rs "urn:stalwart:jmap"
crates/registry/src/schema/enums_impl.rs "vnd.stalwart.expressions"
crates/registry/src/schema/enums_impl.rs "vnd.stalwart.while"
# Moving an existing upstream installation over: its environment variables,
# and upstream's upgrade guide for the store conversion.
crates/types/src/branding.rs "STALWART_{name}"
crates/types/src/branding.rs "Warning: STALWART_{name} is deprecated; set INBUXA_{name} instead."
crates/store/src/build/registry.rs "⚠️ INBUXA_RECOVERY_ADMIN (or STALWART_RECOVERY_ADMIN) is set, but the"
crates/jmap/src/registry/mapping/bootstrap.rs "https://github.com/stalwartlabs/stalwart/blob/main/UPGRADING/v0_16.md"
crates/migration/src/lib.rs "https://github.com/stalwartlabs/stalwart/blob/main/UPGRADING/v0_16.md"
# Upstream's published spam-filter rules, fetched at runtime.
crates/registry/src/schema/structs_impl.rs "https://github.com/stalwartlabs/spam-filter/releases/latest/download/spam-filter-rules.json.gz" crates/registry/src/schema/structs_impl.rs "https://github.com/stalwartlabs/spam-filter/releases/latest/download/spam-filter-rules.json.gz"
# Test fixtures: web-push contact address parsing.
crates/common/src/network/webpush.rs " [email protected] "
crates/common/src/network/webpush.rs "MAILTO:[email protected]"
crates/common/src/network/webpush.rs "[email protected]"
crates/common/src/network/webpush.rs "http://stalw.art"
crates/common/src/network/webpush.rs "https://stalw.art/contact"
crates/common/src/network/webpush.rs "mailto:[email protected]"
crates/common/src/network/webpush.rs "stalw.art"
# OPEN, not yet decided (2026-09-22): operator-visible defaults. The log file
# prefix (TracerLog, and the bootstrap's tracer) names files stalwart.* in
# /var/log/inbuxa/, and the SQL stores default their database and user to
# "stalwart". Changing the SQL defaults would break an install relying on them.
crates/jmap/src/registry/mapping/bootstrap.rs "stalwart"
crates/registry/src/schema/structs_impl.rs "stalwart"
+10 -3
View File
@@ -13,7 +13,8 @@ names, descriptions -- carries INBUXA's. Merging an upstream release brings
new strings in with the name, and the merge itself can't tell, so this runs new strings in with the name, and the merge itself can't tell, so this runs
in CI on every push and pull request. in CI on every push and pull request.
Scope: string literals in `crates/**/*.rs`, test directories excluded. Scope: string literals in `crates/**/*.rs` and `vendor/**/*.rs`, test
directories excluded.
Comments are skipped, so copyright headers and doc comments never match. Comments are skipped, so copyright headers and doc comments never match.
Some literals have to keep the name -- key-derivation contexts, wire-protocol Some literals have to keep the name -- key-derivation contexts, wire-protocol
identifiers, defaults that read an upstream installation -- and those are identifiers, defaults that read an upstream installation -- and those are
@@ -79,8 +80,14 @@ def literals(src):
def findings(): def findings():
found = set() found = set()
crates = os.path.join(ROOT, 'crates') for top in ('crates', 'vendor'):
for dirpath, dirnames, filenames in os.walk(crates): found |= findings_under(os.path.join(ROOT, top))
return found
def findings_under(top):
found = set()
for dirpath, dirnames, filenames in os.walk(top):
dirnames[:] = sorted(d for d in dirnames if d not in SKIP_DIRS) dirnames[:] = sorted(d for d in dirnames if d not in SKIP_DIRS)
for f in sorted(filenames): for f in sorted(filenames):
if not f.endswith('.rs'): if not f.endswith('.rs'):
+104
View File
@@ -0,0 +1,104 @@
#!/usr/bin/env python3
# SPDX-FileCopyrightText: 2026 Coffey Labs
# SPDX-License-Identifier: AGPL-3.0-only
"""
Fail when an upstream file the fork changed doesn't say so (AGPL section 5(a)).
tools/fork/notice-check.py # check; exit 1 on a missing notice
tools/fork/notice-check.py --fix # add the notice where it's missing
The AGPL asks a modified work to carry a prominent notice that it was
modified, with a date. Every upstream file this fork changes carries one
beneath upstream's own notice:
* Modified by Coffey Labs in 2026 for INBUXA.
"Changed" is measured against the `upstream` branch, which holds the stripped
upstream release `main` was last merged with (docs/spec/SPEC.md §2.2a), so
the list is what actually differs rather than a guess. A file counts as
upstream's when its header names Stalwart Labs as a copyright holder; files
the fork wrote carry their own copyright and need nothing. Files with no
comment header at all (README, manifests) are covered by the README's prose.
"""
import argparse
import datetime
import re
import subprocess
import sys
HEADER_LINES = 15
UPSTREAM_HOLDER = re.compile(r'SPDX-FileCopyrightText:.*Stalwart Labs')
NOTICE = re.compile(r'Modified by Coffey Labs in \d{4}')
LICENSE_LINE = re.compile(r'^(\s*(?:\*|//|#)\s*)SPDX-License-Identifier:.*$')
def git(*args):
return subprocess.run(['git', *args], check=True, capture_output=True, text=True).stdout
def snapshot_ref():
for ref in ('origin/upstream', 'upstream'):
if subprocess.run(['git', 'rev-parse', '--verify', '--quiet', f'{ref}^{{commit}}'],
capture_output=True).returncode == 0:
return ref
sys.exit('notice-check: no upstream snapshot branch (origin/upstream or upstream); fetch it first')
def changed_upstream_files(ref):
# Against the working tree, not HEAD, so it also checks work not yet
# committed; in CI the two are the same.
for path in git('diff', '--name-only', '--diff-filter=M', ref).splitlines():
try:
head = open(path, encoding='utf-8').read().split('\n')[:HEADER_LINES]
except (OSError, UnicodeDecodeError):
continue
if any(UPSTREAM_HOLDER.search(line) for line in head):
yield path, head
def add_notice(path):
"""Put the notice under the license line, in that comment's own style."""
lines = open(path, encoding='utf-8').read().split('\n')
for n, line in enumerate(lines[:HEADER_LINES]):
m = LICENSE_LINE.match(line)
if m:
prefix = m.group(1)
blank = prefix.rstrip()
year = datetime.date.today().year
lines[n + 1:n + 1] = [blank, f'{prefix}Modified by Coffey Labs in {year} for INBUXA.']
open(path, 'w', encoding='utf-8').write('\n'.join(lines))
return True
return False
def main():
ap = argparse.ArgumentParser(description=__doc__.split('\n\n')[0].strip())
ap.add_argument('--fix', action='store_true', help='add the notice to every file missing it')
args = ap.parse_args()
ref = snapshot_ref()
checked, missing = 0, []
for path, head in changed_upstream_files(ref):
checked += 1
if not any(NOTICE.search(line) for line in head):
missing.append(path)
if args.fix:
unfixable = [p for p in missing if not add_notice(p)]
for p in sorted(set(missing) - set(unfixable)):
print(f'added: {p}')
missing = unfixable
if missing:
print(f'{len(missing)} upstream file(s) changed against {ref} without the modification notice:\n')
for p in missing:
print(f' {p}')
print('\nAdd "Modified by Coffey Labs in <year> for INBUXA." under the license line, '
'or run tools/fork/notice-check.py --fix.')
return 1
print(f'notice check: clean ({checked} changed upstream file(s), all marked; against {ref}).')
return 0
if __name__ == '__main__':
sys.exit(main())
+141
View File
@@ -0,0 +1,141 @@
#!/usr/bin/env python3
# SPDX-FileCopyrightText: 2026 Coffey Labs
# SPDX-License-Identifier: AGPL-3.0-only
"""
The upstream name where it's an identifier, and its replacement.
tools/fork/renames.py DIR # apply to a tree; prints what changed
Clients, users and operators meet the upstream name in a few places that
aren't notices: wire-protocol names (the JMAP registry capability, WebDAV
state tokens, Sieve extensions), the web interface's OAuth client id, store keys an
operator sees as blob names, and configuration defaults (SQL database and
user, the log file prefix). docs/spec/SPEC.md §2.4 renames them all.
strip.py applies this to every upstream import, so each release arrives
already renamed and those lines never conflict in the merge. `main` was
renamed with it once, on 2026-09-22. Don't run it on `main` again: code
written since that names the old spelling on purpose (the migrations that
retire it) would be renamed too. Those strings are listed in
name-allowlist.txt instead.
Names nobody sees keep upstream's spelling: the OAuth key-derivation contexts
and the hashed application prefix. Renaming them would only destroy state.
Copyright notices and prose (`.md`, `.txt`) are never touched.
"""
import gzip
import hashlib
import base64
import re
import sys
from pathlib import Path
# Plain substrings, in code and data: (old, new, the roots it applies under).
TEXT_RENAMES = [
# Upstream's JMAP capability for its registry (`x:`) objects. Not plain
# `urn:inbuxa:jmap`, which is the fork's own capability (contract C-1);
# listed before the general prefix below so it wins.
('urn:stalwart:jmap', 'urn:inbuxa:jmap:registry', None),
('urn:stalwart:', 'urn:inbuxa:', None),
('vnd.stalwart.', 'vnd.inbuxa.', None),
('(vnd.stalwart)', '(vnd.inbuxa)', None),
('stalwart-webui', 'inbuxa-webui', None),
('STALWART_SPAM_', 'INBUXA_SPAM_', None),
# Configuration defaults, as upstream's generated registry code spells
# them. Server code only: the tests use the same spelling for fixtures
# that must match their containers and identity provider (database users,
# passwords, an OIDC audience), and name their databases explicitly.
('"stalwart".to_string()', '"inbuxa".to_string()', ('crates',)),
]
ROOTS = ('crates', 'tests', 'resources')
SKIP_SUFFIXES = {'.md', '.txt'}
TEXT_SUFFIXES = {'.rs', '.toml', '.py', '.sh', '.json', '.yml', '.yaml', '.js', '.ts', '.html', '.sieve', '.sql'}
COPYRIGHT = re.compile(r'(?i)(?:SPDX-FileCopyrightText:|\bcopyright\b|©)')
# The JSON Schema the server serves to INBUXA Admin, and its checksum.
SCHEMA = Path('resources/schema/schema.json.gz')
SCHEMA_HASH = Path('resources/schema/schema.json.sha256')
SCHEMA_RENAMES = [
('"stalwart"', '"inbuxa"'),
('vnd.stalwart', 'vnd.inbuxa'),
]
def rename_line(line, root):
"""Returns (new line, [(old, new, count)]) for a line of a file under `root`."""
if COPYRIGHT.search(line):
return line, []
done = []
for old, new, roots in TEXT_RENAMES:
if roots is not None and root not in roots:
continue
if old in line:
done.append((old, new, line.count(old)))
line = line.replace(old, new)
return line, done
def schema_bytes(gz):
"""The renamed schema, gzipped deterministically, and its checksum."""
text = gzip.decompress(gz).decode('utf-8')
for old, new in SCHEMA_RENAMES:
text = text.replace(old, new)
out = gzip.compress(text.encode('utf-8'), compresslevel=9, mtime=0)
digest = base64.urlsafe_b64encode(hashlib.sha256(out).digest()).decode().rstrip('=')
return out, digest
def apply(tree):
"""Apply every rename under `tree`; returns {"old → new": {file: count}}."""
tree = Path(tree)
done = {}
def note(old, new, rel, count):
done.setdefault(f'{old}{new}', {})
done[f'{old}{new}'][rel] = done[f'{old}{new}'].get(rel, 0) + count
for root in ROOTS:
base = tree / root
if not base.is_dir():
continue
for path in sorted(base.rglob('*')):
if not path.is_file() or path.suffix in SKIP_SUFFIXES:
continue
if path.suffix not in TEXT_SUFFIXES and not path.name.startswith('Dockerfile'):
continue
try:
lines = path.read_text(encoding='utf-8').split('\n')
except UnicodeDecodeError:
continue
changed = False
rel = str(path.relative_to(tree))
for n, line in enumerate(lines):
new_line, subs = rename_line(line, root)
for old, new, count in subs:
note(old, new, rel, count)
if subs:
lines[n] = new_line
changed = True
if changed:
path.write_text('\n'.join(lines), encoding='utf-8')
schema = tree / SCHEMA
if schema.is_file():
before = schema.read_bytes()
text = gzip.decompress(before).decode('utf-8')
counts = {old: text.count(old) for old, _ in SCHEMA_RENAMES}
out, digest = schema_bytes(before)
if any(counts.values()):
schema.write_bytes(out)
(tree / SCHEMA_HASH).write_text(digest, encoding='utf-8')
for old, new in SCHEMA_RENAMES:
if counts[old]:
note(old, new, str(SCHEMA), counts[old])
return {k: dict(sorted(v.items())) for k, v in sorted(done.items())}
if __name__ == '__main__':
if len(sys.argv) != 2:
sys.exit(__doc__.split('\n\n')[1])
for sub, files in apply(sys.argv[1]).items():
print(f'{sub}: ' + ', '.join(f'{f} ({n})' for f, n in files.items()))
+93 -2
View File
@@ -33,6 +33,15 @@ What it does, in order (docs/spec/SPEC.md §2.2):
comments mark it (another copyright holder or license, or "ported from" comments mark it (another copyright holder or license, or "ported from"
and the like), and names any file THIRD-PARTY.md doesn't cover yet. That's and the like), and names any file THIRD-PARTY.md doesn't cover yet. That's
a report, not a failure: the notice goes in THIRD-PARTY.md with the merge. a report, not a failure: the notice goes in THIRD-PARTY.md with the merge.
8. Renames the upstream name where it's an identifier clients, users or
operators meet (renames.py beside this script), so a re-import arrives
purged and merges without conflicts on those lines. Copyright notices
and prose are never touched.
9. Compiles the result (`cargo check --workspace --all-targets`). A file
that survived the strip but calls code that didn't -- a dual-licensed test
of an Enterprise feature, say -- fails here, on the `upstream` branch,
instead of in the merge. Imports the strip left unused are reported, not
failed. `--no-build-check` skips it.
Only license markers and Cargo manifests are read for meaning. The code inside Only license markers and Cargo manifests are read for meaning. The code inside
an Enterprise file or snippet is never printed, reported or kept, which is what an Enterprise file or snippet is never printed, reported or kept, which is what
@@ -52,6 +61,9 @@ import tarfile
import tempfile import tempfile
from pathlib import Path from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent))
from renames import apply as rename_upstream_names # noqa: E402
SEL = 'LicenseRef-SEL' SEL = 'LicenseRef-SEL'
AGPL = 'AGPL-3.0-only' AGPL = 'AGPL-3.0-only'
# A license identifier as it appears in a comment header, in any comment style. # A license identifier as it appears in a comment header, in any comment style.
@@ -375,6 +387,50 @@ def remaining_hooks(tree):
return dict(sorted(gates.items())), dict(sorted(checks.items())) return dict(sorted(gates.items())), dict(sorted(checks.items()))
BUILD_KNOWN = Path(__file__).resolve().parent / 'build-check-known.txt'
def known_build_failures():
"""Files expected not to compile in a stripped tree (build-check-known.txt)."""
if not BUILD_KNOWN.is_file():
return set()
return {l.strip() for l in BUILD_KNOWN.read_text(encoding='utf-8').split('\n')
if l.strip() and not l.lstrip().startswith('#')}
def build_check(tree, target_dir):
"""
`cargo check` the stripped tree. Returns (errors, unused): each error is
{file, line, message} from a compiler diagnostic, grouped by the file it
points at; unused lists the imports reported unused.
Only compiler diagnostics are read, and they point at the shared code
that failed, never at the removed code.
"""
cmd = ['cargo', 'check', '--workspace', '--all-targets', '--locked', '--message-format=json',
'--target-dir', str(target_dir)]
r = subprocess.run(cmd, cwd=tree, capture_output=True, text=True)
errors, unused = [], []
for line in r.stdout.splitlines():
try:
msg = json.loads(line)
except ValueError:
continue
if msg.get('reason') != 'compiler-message':
continue
d = msg['message']
span = next((s for s in d.get('spans', []) if s.get('is_primary')), None)
where = {'file': span['file_name'], 'line': span['line_start']} if span else {'file': '?', 'line': 0}
if d.get('level') == 'error':
errors.append({**where, 'message': d.get('message', '')})
elif (d.get('code') or {}).get('code') == 'unused_imports':
unused.append({**where, 'message': d.get('message', '')})
if r.returncode != 0 and not errors:
errors.append({'file': '?', 'line': 0, 'message': (r.stderr.strip().splitlines() or ['cargo check failed'])[-1]})
dedup = lambda items: [dict(t) for t in sorted({tuple(sorted(i.items())) for i in items}, key=lambda t: (dict(t)['file'], dict(t)['line']))]
return dedup(errors), dedup(unused)
def write_report(out_dir, report): def write_report(out_dir, report):
(out_dir / 'STRIP-REPORT.json').write_text(json.dumps(report, indent=2) + '\n', encoding='utf-8') (out_dir / 'STRIP-REPORT.json').write_text(json.dumps(report, indent=2) + '\n', encoding='utf-8')
r = report r = report
@@ -390,6 +446,13 @@ def write_report(out_dir, report):
f'in {len(r["feature_gates"])} files; {sum(r["edition_checks"].values())} `is_enterprise_edition()` checks ' f'in {len(r["feature_gates"])} files; {sum(r["edition_checks"].values())} `is_enterprise_edition()` checks '
f'in {len(r["edition_checks"])} files', f'in {len(r["edition_checks"])} files',
f'- Third-party code: {len(r["third_party"])} files, **{len(r["third_party_unlisted"])}** not in THIRD-PARTY.md', f'- Third-party code: {len(r["third_party"])} files, **{len(r["third_party_unlisted"])}** not in THIRD-PARTY.md',
f'- Renamed identifiers: {sum(sum(f.values()) for f in r["renames"].values())} in '
f'{len({p for f in r["renames"].values() for p in f})} files',
'- Build check: ' + ('skipped' if r['build'] is None else
f'**{"clean" if not r["build"]["errors"] else f"{len(r["build"]["errors"])} errors"}**, '
f'{len(r["build"]["expected"])} expected errors in '
f'{len({e["file"] for e in r["build"]["expected"]})} rebuilt-feature tests, '
f'{len(r["build"]["unused"])} imports left unused'),
] ]
if r['schema']: if r['schema']:
md.append(f'- Upstream schema flags {len(r["schema"]["objects"])} objects and {len(r["schema"]["fields"])} fields as Enterprise') md.append(f'- Upstream schema flags {len(r["schema"]["objects"])} objects and {len(r["schema"]["fields"])} fields as Enterprise')
@@ -406,6 +469,19 @@ def write_report(out_dir, report):
for f, found in r['third_party'].items(): for f, found in r['third_party'].items():
md.append(f'- `{f}`{" **new**" if f in r["third_party_unlisted"] else ""}') md.append(f'- `{f}`{" **new**" if f in r["third_party_unlisted"] else ""}')
md += [f' - {h["line"]}: {h["text"]}' for h in found] md += [f' - {h["line"]}: {h["text"]}' for h in found]
md += ['', '## Renamed identifiers', '']
for sub, files in r['renames'].items():
md.append(f'- `{sub}`: ' + ', '.join(f'`{f}` ({n})' for f, n in files.items()))
if r['build'] is not None:
md += ['', '## Build check', '',
'Errors mean shared code calls something the strip removed: usually a dual-licensed file that only '
'serves an Enterprise feature. Drop or rework it in the merge into `main`, never on `upstream`.', '']
md += [f'- error `{e["file"]}:{e["line"]}`: {e["message"]}' for e in r['build']['errors']]
md += [f'- unused `{u["file"]}:{u["line"]}`: {u["message"]}' for u in r['build']['unused']]
md += ['', 'Expected: upstream\'s tests of features the fork rebuilt on `main` '
'(tools/fork/build-check-known.txt).', '']
md += [f'- `{e["file"]}:{e["line"]}`: {e["message"]}' for e in r['build']['expected']]
md += [f'- `{f}` now compiles: take it off the known list' for f in r['build']['known_clean']]
if r['problems']: if r['problems']:
md += ['', '## Problems', ''] + [f'- {p}' for p in r['problems']] md += ['', '## Problems', ''] + [f'- {p}' for p in r['problems']]
(out_dir / 'STRIP-REPORT.md').write_text('\n'.join(md) + '\n', encoding='utf-8') (out_dir / 'STRIP-REPORT.md').write_text('\n'.join(md) + '\n', encoding='utf-8')
@@ -416,6 +492,10 @@ def main():
ap.add_argument('--upstream', required=True, type=Path, help='a git clone of upstream Stalwart') ap.add_argument('--upstream', required=True, type=Path, help='a git clone of upstream Stalwart')
ap.add_argument('--ref', required=True, help='tag, branch or commit to snapshot, e.g. v0.16.22') ap.add_argument('--ref', required=True, help='tag, branch or commit to snapshot, e.g. v0.16.22')
ap.add_argument('--out', required=True, type=Path, help='new directory; the tree goes in OUT/tree') ap.add_argument('--out', required=True, type=Path, help='new directory; the tree goes in OUT/tree')
ap.add_argument('--target-dir', type=Path, default=Path(__file__).resolve().parents[2] / 'target' / 'strip-check',
help="cargo's target dir for the build check (default: this repo's target/strip-check, "
'which keeps the dependency build warm between imports)')
ap.add_argument('--no-build-check', action='store_true', help='skip compiling the stripped tree')
args = ap.parse_args() args = ap.parse_args()
if args.out.exists(): if args.out.exists():
@@ -428,7 +508,7 @@ def main():
write_report(args.out, {'ref': args.ref, 'commit': commit, 'removed_files': [], 'removed_snippets': {}, write_report(args.out, {'ref': args.ref, 'commit': commit, 'removed_files': [], 'removed_snippets': {},
'cargo_edits': [], 'dangling_mods': [], 'problems': malformed, 'feature_gates': {}, 'edition_checks': {}, 'cargo_edits': [], 'dangling_mods': [], 'problems': malformed, 'feature_gates': {}, 'edition_checks': {},
'schema': None, 'third_party': {}, 'third_party_unlisted': [], 'schema': None, 'third_party': {}, 'third_party_unlisted': [],
'ossify_log': ''}) 'renames': {}, 'build': None, 'ossify_log': ''})
print('\n'.join(malformed), file=sys.stderr) print('\n'.join(malformed), file=sys.stderr)
fail('malformed snippet markers; nothing stripped', code=1) fail('malformed snippet markers; nothing stripped', code=1)
@@ -436,10 +516,21 @@ def main():
log = run_ossify(tree, rust_roots(tree)) log = run_ossify(tree, rust_roots(tree))
edits = deactivate_enterprise(tree) + deactivate_enterprise_in_scripts(tree) edits = deactivate_enterprise(tree) + deactivate_enterprise_in_scripts(tree)
dangling = remove_dangling_mods(tree) dangling = remove_dangling_mods(tree)
renames = rename_upstream_names(tree)
problems = verify(tree) problems = verify(tree)
gates, checks = remaining_hooks(tree) gates, checks = remaining_hooks(tree)
others = third_party(tree) others = third_party(tree)
new_others = unlisted(others) new_others = unlisted(others)
build = None
if not args.no_build_check and not problems:
print('strip: compiling the stripped tree (cargo check)...', file=sys.stderr)
errors, unused = build_check(tree, args.target_dir)
known = known_build_failures()
expected = [e for e in errors if e['file'] in known]
errors = [e for e in errors if e['file'] not in known]
build = {'errors': errors, 'expected': expected, 'unused': unused,
'known_clean': sorted(known - {e['file'] for e in expected})}
problems += [f'{e["file"]}:{e["line"]}: does not compile: {e["message"]}' for e in errors]
report = { report = {
'ref': args.ref, 'commit': commit, 'ref': args.ref, 'commit': commit,
@@ -447,7 +538,7 @@ def main():
'cargo_edits': edits, 'dangling_mods': dangling, 'problems': problems, 'cargo_edits': edits, 'dangling_mods': dangling, 'problems': problems,
'feature_gates': gates, 'edition_checks': checks, 'feature_gates': gates, 'edition_checks': checks,
'schema': schema_flags(tree), 'third_party': others, 'third_party_unlisted': new_others, 'schema': schema_flags(tree), 'third_party': others, 'third_party_unlisted': new_others,
'ossify_log': log, 'renames': renames, 'build': build, 'ossify_log': log,
} }
write_report(args.out, report) write_report(args.out, report)
print(f'{args.ref} ({commit[:12]}): removed {len(removed_files)} files and ' print(f'{args.ref} ({commit[:12]}): removed {len(removed_files)} files and '
+22
View File
@@ -0,0 +1,22 @@
name: "CI"
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
env:
CARGO_TERM_COLOR: always
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Build
run: cargo build --verbose
- name: Run tests
run: cargo test --verbose
+12
View File
@@ -0,0 +1,12 @@
# Generated by Cargo
# will have compiled files and executables
/target/
# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
Cargo.lock
# These are backup files generated by rustfmt
**/*.rs.bk
*.failed
.DS_Store
+62
View File
@@ -0,0 +1,62 @@
sieve-rs 0.7.3
================================
- Bump `mail-builder` dependency to 0.5.
sieve-rs 0.7.2
================================
- Fix: `replace` action adds additional `From` header.
sieve-rs 0.7.1
================================
- Bump `fancy-regex` dependency to 0.17.0.
- Fixed `rkyv` serialization lifetimes for rustc 1.93.0.
sieve-rs 0.7.0
================================
- Added `rkyv` support.
- Bump `mail-parser` dependency to 0.11.0.
- Fix: Allow redirect to sender (#11).
sieve-rs 0.6.0
================================
- Replaced `phf` with `hashify`.
- Bump `mail-parser` dependency to 0.10.0.
sieve-rs 0.5.3
================================
- Fixed `register_match_var` function.
sieve-rs 0.5.2
================================
- Fixed `set_global_variable` function.
sieve-rs 0.5.1
================================
- Case insensitive envelope tests (#6).
- Set envelope variables internally (#10).
sieve-rs 0.5.0
================================
- Removed context.
sieve-rs 0.4.0
================================
- Support for expressions.
sieve-rs 0.3.1
================================
- Bump `mail-builder` dependency to 0.3.0.
sieve-rs 0.3.0
================================
- Updated ``execute`` grammar.
- Upgraded to latest mail-parser.
- Envelope accessible from environment variables.
sieve-rs 0.2.0
================================
- Improved event loop.
sieve-rs 0.1.0
================================
- Initial release.
+106
View File
@@ -0,0 +1,106 @@
# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO
#
# When uploading crates to the registry Cargo will automatically
# "normalize" Cargo.toml files for maximal compatibility
# with all versions of Cargo and also rewrite `path` dependencies
# to registry (e.g., crates.io) dependencies.
#
# If you are reading this file be aware that the original Cargo.toml
# will likely look very different (and much more reasonable).
# See Cargo.toml.orig for the original contents.
[package]
edition = "2024"
name = "sieve-rs"
version = "0.7.3"
authors = ["Stalwart Labs <[email protected]>"]
build = false
autolib = false
autobins = false
autoexamples = false
autotests = false
autobenches = false
description = "Sieve filter interpreter for Rust"
homepage = "https://github.com/stalwartlabs/sieve"
readme = "README.md"
keywords = [
"sieve",
"interpreter",
"compiler",
"email",
"mail",
]
categories = [
"email",
"compilers",
]
license = "AGPL-3.0-only"
repository = "https://github.com/stalwartlabs/sieve"
[features]
default = []
rkyv = [
"dep:rkyv",
"mail-parser/rkyv",
]
serde = [
"dep:serde",
"mail-parser/serde",
]
[lib]
name = "sieve"
path = "src/lib.rs"
doctest = false
[dependencies.ahash]
version = "0.8.0"
[dependencies.arc-swap]
version = "1.7.1"
[dependencies.fancy-regex]
version = "0.19.0"
[dependencies.hashify]
version = "0.2"
[dependencies.mail-builder]
version = "0.5"
[dependencies.mail-parser]
version = "0.11"
features = ["full_encoding"]
[dependencies.rkyv]
version = "0.8"
optional = true
[dependencies.serde]
version = "1.0"
features = [
"derive",
"rc",
]
optional = true
[dev-dependencies.evalexpr]
version = "13.1.0"
[dev-dependencies.mail-parser]
version = "0.11"
features = [
"full_encoding",
"serde",
"rkyv",
]
[dev-dependencies.serde]
version = "1.0"
features = [
"derive",
"rc",
]
[dev-dependencies.serde_json]
version = "1.0"
+235
View File
@@ -0,0 +1,235 @@
GNU AFFERO GENERAL PUBLIC LICENSE
Version 3, 19 November 2007
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
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.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
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 <http://www.gnu.org/licenses/>.
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 <http://www.gnu.org/licenses/>.
+101
View File
@@ -0,0 +1,101 @@
Stalwart Enterprise License 1.0 (SELv1) Agreement
=================================================
Last Update: July 8, 2024
PLEASE CAREFULLY READ THIS STALWART ENTERPRISE LICENSE AGREEMENT ("AGREEMENT"). THIS AGREEMENT CONSTITUTES A LEGALLY BINDING AGREEMENT BETWEEN YOU AND STALWART LABS LTD 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 Ltd and you, or the legal entity on behalf of whom you are acting.
1. DEFINITIONS
1.1. "Software" refers to the Stalwart Mail 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 Ltd, 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
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.
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 partys 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 Ltd. at:
Stalwart Labs Ltd.
128 City Road
London, United Kingdom
[email protected]
+242
View File
@@ -0,0 +1,242 @@
# sieve
[![crates.io](https://img.shields.io/crates/v/sieve-rs)](https://crates.io/crates/sieve-rs)
[![build](https://github.com/stalwartlabs/sieve/actions/workflows/rust.yml/badge.svg)](https://github.com/stalwartlabs/sieve/actions/workflows/rust.yml)
[![docs.rs](https://img.shields.io/docsrs/sieve-rs)](https://docs.rs/sieve-rs)
[![License: AGPL v3](https://img.shields.io/badge/License-AGPL_v3-blue.svg)](https://www.gnu.org/licenses/agpl-3.0)
_sieve_ is a fast and secure Sieve filter interpreter for Rust that supports all [registered Sieve extensions](https://www.iana.org/assignments/sieve-extensions/sieve-extensions.xhtml).
## Usage Example
```rust
use sieve::{runtime::RuntimeError, Action, Compiler, Event, Input, Runtime};
// Sieve script to execute
let text_script = br#"
require ["fileinto", "body", "imap4flags"];
if body :contains "tps" {
setflag "$tps_reports";
}
if header :matches "List-ID" "*<*@*" {
fileinto "INBOX.lists.${2}"; stop;
}
"#;
// Message to filter
let raw_message = r#"From: Sales Mailing List <list-sales@example.org>
To: John Doe <jdoe@example.org>
List-ID: <sales@example.org>
Subject: TPS Reports
We're putting new coversheets on all the TPS reports before they go out now.
So if you could go ahead and try to remember to do that from now on, that'd be great. All right!
"#;
// Compile
let compiler = Compiler::new();
let script = compiler.compile(text_script).unwrap();
// Build runtime
let runtime = Runtime::new();
// Create filter instance
let mut instance = runtime.filter(raw_message.as_bytes());
let mut input = Input::script("my-script", script);
let mut messages: Vec<String> = Vec::new();
// Start event loop
while let Some(result) = instance.run(input) {
match result {
Ok(event) => match event {
Event::IncludeScript { name, optional } => {
// NOTE: Just for demonstration purposes, script name needs to be validated first.
if let Ok(bytes) = std::fs::read(name.as_str()) {
let script = compiler.compile(&bytes).unwrap();
input = Input::script(name, script);
} else if optional {
input = Input::False;
} else {
panic!("Script {} not found.", name);
}
}
Event::MailboxExists { .. } => {
// Set to true if the mailbox exists
input = false.into();
}
Event::ListContains { .. } => {
// Set to true if the list(s) contains an entry
input = false.into();
}
Event::DuplicateId { .. } => {
// Set to true if the ID is duplicate
input = false.into();
}
Event::Execute { command, arguments } => {
println!(
"Script executed command {:?} with parameters {:?}",
command, arguments
);
// Set to true if the script succeeded
input = false.into();
}
Event::Keep { flags, message_id } => {
println!(
"Keep message '{}' with flags {:?}.",
if message_id > 0 {
messages[message_id - 1].as_str()
} else {
raw_message
},
flags
);
input = true.into();
}
Event::Discard => {
println!("Discard message.");
input = true.into();
}
Event::Reject { reason, .. } => {
println!("Reject message with reason {:?}.", reason);
input = true.into();
}
Event::FileInto {
folder,
flags,
message_id,
..
} => {
println!(
"File message '{}' in folder {:?} with flags {:?}.",
if message_id > 0 {
messages[message_id - 1].as_str()
} else {
raw_message
},
folder,
flags
);
input = true.into();
}
Event::SendMessage {
recipient,
message_id,
..
} => {
println!(
"Send message '{}' to {:?}.",
if message_id > 0 {
messages[message_id - 1].as_str()
} else {
raw_message
},
recipient
);
input = true.into();
}
Event::Notify {
message, method, ..
} => {
println!("Notify URI {:?} with message {:?}", method, message);
input = true.into();
}
Event::CreatedMessage { message, .. } => {
messages.push(String::from_utf8(message).unwrap());
input = true.into();
}
#[cfg(test)]
_ => unreachable!(),
},
Err(error) => {
match error {
RuntimeError::TooManyIncludes => {
eprintln!("Too many included scripts.");
}
RuntimeError::InvalidInstruction(instruction) => {
eprintln!(
"Invalid instruction {:?} found at {}:{}.",
instruction.name(),
instruction.line_num(),
instruction.line_pos()
);
}
RuntimeError::ScriptErrorMessage(message) => {
eprintln!("Script called the 'error' function with {:?}", message);
}
RuntimeError::CapabilityNotAllowed(capability) => {
eprintln!(
"Capability {:?} has been disabled by the administrator.",
capability
);
}
RuntimeError::CapabilityNotSupported(capability) => {
eprintln!("Capability {:?} not supported.", capability);
}
RuntimeError::CPULimitReached => {
eprintln!("Script exceeded the configured CPU limit.");
}
}
input = true.into();
}
}
}
```
## Testing & Fuzzing
To run the testsuite:
```bash
$ cargo test --all-features
```
To fuzz the library with `cargo-fuzz`:
```bash
$ cargo +nightly fuzz run sieve
```
## Conformed RFCs
- [RFC 5228 - Sieve: An Email Filtering Language](https://datatracker.ietf.org/doc/html/rfc5228)
- [RFC 3894 - Copying Without Side Effects](https://datatracker.ietf.org/doc/html/rfc3894)
- [RFC 5173 - Body Extension](https://datatracker.ietf.org/doc/html/rfc5173)
- [RFC 5183 - Environment Extension](https://datatracker.ietf.org/doc/html/rfc5183)
- [RFC 5229 - Variables Extension](https://datatracker.ietf.org/doc/html/rfc5229)
- [RFC 5230 - Vacation Extension](https://datatracker.ietf.org/doc/html/rfc5230)
- [RFC 5231 - Relational Extension](https://datatracker.ietf.org/doc/html/rfc5231)
- [RFC 5232 - Imap4flags Extension](https://datatracker.ietf.org/doc/html/rfc5232)
- [RFC 5233 - Subaddress Extension](https://datatracker.ietf.org/doc/html/rfc5233)
- [RFC 5235 - Spamtest and Virustest Extensions](https://datatracker.ietf.org/doc/html/rfc5235)
- [RFC 5260 - Date and Index Extensions](https://datatracker.ietf.org/doc/html/rfc5260)
- [RFC 5293 - Editheader Extension](https://datatracker.ietf.org/doc/html/rfc5293)
- [RFC 5429 - Reject and Extended Reject Extensions](https://datatracker.ietf.org/doc/html/rfc5429)
- [RFC 5435 - Extension for Notifications](https://datatracker.ietf.org/doc/html/rfc5435)
- [RFC 5463 - Ihave Extension](https://datatracker.ietf.org/doc/html/rfc5463)
- [RFC 5490 - Extensions for Checking Mailbox Status and Accessing Mailbox Metadata](https://datatracker.ietf.org/doc/html/rfc5490)
- [RFC 5703 - MIME Part Tests, Iteration, Extraction, Replacement, and Enclosure](https://datatracker.ietf.org/doc/html/rfc5703)
- [RFC 6009 - Delivery Status Notifications and Deliver-By Extensions](https://datatracker.ietf.org/doc/html/rfc6009)
- [RFC 6131 - Sieve Vacation Extension: "Seconds" Parameter](https://datatracker.ietf.org/doc/html/rfc6131)
- [RFC 6134 - Externally Stored Lists](https://datatracker.ietf.org/doc/html/rfc6134)
- [RFC 6558 - Converting Messages before Delivery](https://datatracker.ietf.org/doc/html/rfc6558)
- [RFC 6609 - Include Extension](https://datatracker.ietf.org/doc/html/rfc6609)
- [RFC 7352 - Detecting Duplicate Deliveries](https://datatracker.ietf.org/doc/html/rfc7352)
- [RFC 8579 - Delivering to Special-Use Mailboxes](https://datatracker.ietf.org/doc/html/rfc8579)
- [RFC 8580 - File Carbon Copy (FCC)](https://datatracker.ietf.org/doc/html/rfc8580)
- [RFC 9042 - Delivery by MAILBOXID](https://datatracker.ietf.org/doc/html/rfc9042)
- [REGEX-01 - Regular Expression Extension (draft-ietf-sieve-regex-01)](https://www.ietf.org/archive/id/draft-ietf-sieve-regex-01.html)
## License
Licensed under the terms of the [GNU Affero General Public License](https://www.gnu.org/licenses/agpl-3.0.en.html) as published by
the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
You can be released from the requirements of the AGPLv3 license by purchasing a commercial license. Please contact licensing@stalw.art for more details.
## Copyright
Copyright (C) 2020, Stalwart Labs LLC
+33
View File
@@ -0,0 +1,33 @@
# sieve-rs, vendored
The published [sieve-rs](https://crates.io/crates/sieve-rs) **0.7.3**, taken
from crates.io under AGPL-3.0-only, with the upstream project's name taken
out of the identifiers it spells into the Sieve language (docs/spec/SPEC.md
§2.4). The root `Cargo.toml` patches it in with `[patch.crates-io]`.
## Changes
Every changed file carries the AGPL 5(a) notice in its header.
- `src/compiler/grammar/mod.rs`: the extensions scripts `require` and
ManageSieve advertises are `vnd.inbuxa.while` and
`vnd.inbuxa.expressions`.
- `src/runtime/mod.rs`: the default `environment "name"` is `inbuxa Sieve`
(the server sets its own name on both of its runtimes anyway), and the
test-only capability is `vnd.inbuxa.testsuite`.
- `src/lib.rs`: the test-suite environment variables are `vnd.inbuxa.*`.
- `Cargo.toml`: the example target is dropped, with the top-level
`examples/` and `tests/`, which the server never builds. (`src/` keeps its
own `tests` modules: the crate declares them.)
## Updating
When the server's `Cargo.lock` moves sieve-rs to a new version, Cargo stops
using this copy and only warns that the patch went unused. The unit test
`sieve_extensions_carry_the_fork_name` in `crates/common` fails when that
happens. Re-vendor the new version from `~/.cargo/registry/src/*/sieve-rs-*`,
dropping the top-level `tests/` and `examples/` (only those: `rsync
--exclude /tests --exclude /examples`), `Cargo.lock` and `Cargo.toml.orig`, repeat
the changes above, and update the version here and in the root `Cargo.toml`
comment. `tools/fork/name-check.py` covers `vendor/` too, so a rename missed
in a new version fails CI.
@@ -0,0 +1,51 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <hello@stalw.art>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::compiler::{
CompileError, Value,
grammar::{
instruction::{CompilerState, Instruction},
test::Test,
},
};
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(
any(test, feature = "serde"),
derive(serde::Serialize, serde::Deserialize)
)]
#[cfg_attr(
feature = "rkyv",
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
)]
pub(crate) struct Convert {
pub from_media_type: Value,
pub to_media_type: Value,
pub transcoding_params: Vec<Value>,
pub is_not: bool,
}
impl CompilerState<'_> {
pub(crate) fn parse_test_convert(&mut self) -> Result<Test, CompileError> {
Ok(Test::Convert(Convert {
from_media_type: self.parse_string()?,
to_media_type: self.parse_string()?,
transcoding_params: self.parse_strings(false)?,
is_not: false,
}))
}
pub(crate) fn parse_convert(&mut self) -> Result<(), CompileError> {
let cmd = Instruction::Convert(Convert {
from_media_type: self.parse_string()?,
to_media_type: self.parse_string()?,
transcoding_params: self.parse_strings(false)?,
is_not: false,
});
self.instructions.push(cmd);
Ok(())
}
}
@@ -0,0 +1,212 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <hello@stalw.art>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use mail_parser::HeaderName;
use crate::compiler::{
CompileError, ErrorType, Value,
grammar::{
Capability, Comparator,
instruction::{CompilerState, Instruction},
},
lexer::{Token, word::Word},
};
use crate::compiler::grammar::MatchType;
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(
any(test, feature = "serde"),
derive(serde::Serialize, serde::Deserialize)
)]
#[cfg_attr(
feature = "rkyv",
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
)]
pub(crate) struct AddHeader {
pub last: bool,
pub field_name: Value,
pub value: Value,
}
/*
Usage: "deleteheader" [":index" <fieldno: number> [":last"]]
[COMPARATOR] [MATCH-TYPE]
<field-name: string>
[<value-patterns: string-list>]
*/
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(
any(test, feature = "serde"),
derive(serde::Serialize, serde::Deserialize)
)]
#[cfg_attr(
feature = "rkyv",
derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
)]
pub(crate) struct DeleteHeader {
pub index: Option<i32>,
pub comparator: Comparator,
pub match_type: MatchType,
pub field_name: Value,
pub value_patterns: Vec<Value>,
pub mime_anychild: bool,
}
impl CompilerState<'_> {
pub(crate) fn parse_addheader(&mut self) -> Result<(), CompileError> {
let mut field_name = None;
let value;
let mut last = false;
loop {
let token_info = self.tokens.unwrap_next()?;
match token_info.token {
Token::Tag(Word::Last) => {
self.validate_argument(1, None, token_info.line_num, token_info.line_pos)?;
last = true;
}
_ => {
let string = self.parse_string_token(token_info)?;
if field_name.is_none() {
if let Value::Text(header_name) = &string
&& HeaderName::parse(header_name.as_ref()).is_none()
{
return Err(self
.tokens
.unwrap_next()?
.custom(ErrorType::InvalidHeaderName));
}
field_name = string.into();
} else {
if matches!(
&string,
Value::Text(value) if value.len() > self.compiler.max_header_size
) {
return Err(self
.tokens
.unwrap_next()?
.custom(ErrorType::HeaderTooLong));
}
value = string;
break;
}
}
}
}
self.instructions.push(Instruction::AddHeader(AddHeader {
last,
field_name: field_name.unwrap(),
value,
}));
Ok(())
}
pub(crate) fn parse_deleteheader(&mut self) -> Result<(), CompileError> {
let field_name: Value;
let mut match_type = MatchType::Is;
let mut comparator = Comparator::AsciiCaseMap;
let mut index = None;
let mut index_last = false;
let mut mime = false;
let mut mime_anychild = false;
loop {
let token_info = self.tokens.unwrap_next()?;
match token_info.token {
Token::Tag(
word @ (Word::Is
| Word::Contains
| Word::Matches
| Word::Value
| Word::Count
| Word::Regex),
) => {
self.validate_argument(
1,
match word {
Word::Value | Word::Count => Capability::Relational.into(),
Word::Regex => Capability::Regex.into(),
Word::List => Capability::ExtLists.into(),
_ => None,
},
token_info.line_num,
token_info.line_pos,
)?;
match_type = self.parse_match_type(word)?;
}
Token::Tag(Word::Comparator) => {
self.validate_argument(2, None, token_info.line_num, token_info.line_pos)?;
comparator = self.parse_comparator()?;
}
Token::Tag(Word::Index) => {
self.validate_argument(3, None, token_info.line_num, token_info.line_pos)?;
index = (self.tokens.expect_number(u16::MAX as usize)? as i32).into();
}
Token::Tag(Word::Last) => {
self.validate_argument(4, None, token_info.line_num, token_info.line_pos)?;
index_last = true;
}
Token::Tag(Word::Mime) => {
self.validate_argument(
5,
Capability::Mime.into(),
token_info.line_num,
token_info.line_pos,
)?;
mime = true;
}
Token::Tag(Word::AnyChild) => {
self.validate_argument(
6,
Capability::Mime.into(),
token_info.line_num,
token_info.line_pos,
)?;
mime_anychild = true;
}
_ => {
field_name = self.parse_string_token(token_info)?;
if let Value::Text(header_name) = &field_name
&& HeaderName::parse(header_name.as_ref()).is_none()
{
return Err(self
.tokens
.unwrap_next()?
.custom(ErrorType::InvalidHeaderName));
}
break;
}
}
}
if !mime && mime_anychild {
return Err(self.tokens.unwrap_next()?.missing_tag(":mime"));
}
let cmd = Instruction::DeleteHeader(DeleteHeader {
index: if index_last { index.map(|i| -i) } else { index },
comparator,
match_type,
field_name,
value_patterns: if let Some(Ok(
Token::StringConstant(_) | Token::StringVariable(_) | Token::BracketOpen,
)) = self.tokens.peek().map(|r| r.map(|t| &t.token))
{
let mut key_list = self.parse_strings(false)?;
self.validate_match(&match_type, &mut key_list)?;
key_list
} else {
Vec::new()
},
mime_anychild,
});
self.instructions.push(cmd);
Ok(())
}
}

Some files were not shown because too many files have changed in this diff Show More