Compare commits

..
16 Commits
Author SHA1 Message Date
jcoffey-dev d7bebd454d Merge pull request 'Release 2026.10.5' (#143) from release/2026.10.5-pr into main
ci / fork-checks (push) Skipped
ci / build (push) Skipped
publish / version (push) Skipped
publish / publish-amd64 (push) Skipped
publish / publish-arm64 (push) Skipped
publish / release (push) Skipped
publish / binaries (push) Skipped
github/ci (branch) GitHub Actions
ci / github (push) Successful in 41m48s
publish / github (push) Failing after 1h27m32s
publish / announce (push) Skipped
announce / announce (release) Successful in 21s
github/ci (tag) GitHub Actions
2026-10-05 05:25:14 +00:00
jcoffey-dev 282ad5fc13 Release 2026.10.5
ci / fork-checks (pull_request) Skipped
ci / build (pull_request) Skipped
github/ci (branch) GitHub Actions
ci / github (pull_request) Successful in 5m45s
2026-10-04 22:18:55 -07:00
jcoffey-dev 133d41df36 Merge pull request 'Check TLSA lookups for false bogus verdicts too' (#142) from fix/tlsa-false-bogus into main
ci / fork-checks (push) Skipped
ci / build (push) Skipped
github/ci (branch) GitHub Actions
ci / github (push) Canceled after 6m40s
2026-10-05 05:18:42 +00:00
jcoffey-dev c4a6e4d117 Check TLSA lookups for false bogus verdicts too
ci / fork-checks (pull_request) Skipped
ci / build (pull_request) Skipped
ci / github (pull_request) Successful in 6m46s
github/ci (branch) GitHub Actions
Mail to chuckmckinnon.com sat in the queue for days with "Error fetching
TLSA record: DNSSEC validation failed". Its MX, mail.usefulinsight.com,
is on Cloudflare, and behind Hetzner's resolvers
_25._tcp.mail.usefulinsight.com answers TLSA with a signed CNAME to the
zone apex, which has no TLSA record. That is the second hickory 0.26.3
bug #72 works around: it checks the denial against the name first asked
for, not the CNAME's target, and calls a valid answer bogus.

#72 put MX and address lookups through validated_lookup but left the
TLSA lookup calling hickory directly. It goes through validated_lookup
now: a signed CNAME is followed, the denial at the target validates, and
the result is "no TLSA record", so delivery goes ahead without DANE as
it should. A TLSA record that rechecks as insecure is treated as no
policy, since DANE needs a signed one.

Cloudflare's own resolver answers that name with a compact denial at the
name itself, which hickory already accepts, so the new ignored test
takes a resolver from INBUXA_TEST_DNS_TCP. Run against 185.12.64.2 over
an SSH bridge from host1, hickory alone fails with "DNSSEC validation
failed", as in production, and validated_lookup returns a non-bogus
denial. smtp lib tests pass; check --all-targets is clean.
2026-10-04 22:11:39 -07:00
jcoffey-dev f59a9de4dc Merge pull request 'Call the webmail inbuxa-webmail in docs and comments' (#141) from docs/inbuxa-webmail-name into main
ci / fork-checks (push) Skipped
ci / build (push) Skipped
github/ci (branch) GitHub Actions
ci / github (push) Successful in 1h2m11s
2026-10-05 04:02:07 +00:00
jcoffey-dev 083f22d6fb Call the webmail inbuxa-webmail in docs and comments
ci / fork-checks (pull_request) Skipped
ci / build (pull_request) Skipped
github/ci (branch) GitHub Actions
ci / github (pull_request) Successful in 25m30s
The webmail repository was renamed from ihasmail-inbuxa to inbuxa-webmail
on 2026-10-05. The OAuth client id stays ihasmail-inbuxa: that is what the
server registers, so the backticked and quoted ids are unchanged.
2026-10-04 20:36:03 -07:00
jcoffey-dev c43abef8ab Merge pull request 'Release 2026.9.30.2' (#140) from release/2026.9.30.2-pr into main
ci / fork-checks (push) Skipped
ci / build (push) Skipped
publish / version (push) Skipped
publish / publish-amd64 (push) Skipped
publish / publish-arm64 (push) Skipped
publish / release (push) Skipped
publish / binaries (push) Skipped
github/ci (branch) GitHub Actions
ci / github (push) Successful in 42m10s
publish / github (push) Successful in 1h9m32s
publish / announce (push) Failing after 22s
announce / announce (release) Successful in 21s
github/ci (tag) GitHub Actions
2026-10-01 02:09:11 +00:00
jcoffey-dev c9f8028502 Release 2026.9.30.2
ci / fork-checks (pull_request) Skipped
ci / build (pull_request) Skipped
github/ci (branch) GitHub Actions
ci / github (pull_request) Successful in 6m45s
2026-09-30 19:01:58 -07:00
jcoffey-dev cd7a0f4163 Merge pull request 'Metric history: only the calculating node stores cluster-wide gauges' (#139) from fix/cluster-gauges-one-node into main
ci / fork-checks (push) Skipped
github/ci (branch) GitHub Actions
ci / build (push) Skipped
ci / github (push) Canceled after 9m58s
2026-10-01 01:59:10 +00:00
jcoffey-dev 30d4cef0e7 Metric history: only the calculating node stores cluster-wide gauges
ci / build (pull_request) Skipped
ci / fork-checks (pull_request) Skipped
github/ci (branch) GitHub Actions
ci / github (pull_request) Successful in 7m5s
queue.count, user.count and domain.count count the whole cluster, and
only the node with the metrics-calculation role works them out. Every
node still stored them. On the others the queue gauge only moves with
local queue events, so it had drifted below zero (production: node 0 at
18,446,744,073,709,551,596, node 1 at ...613, i.e. -20 and -3), and
the account and domain counts stayed at 0. A reader taking the latest
reading got whichever node wrote last.

sample() now takes whether the node calculates them and leaves them out
otherwise. A unit test covers both cases.
2026-09-30 18:51:22 -07:00
jcoffey-dev 6c1eeea038 Merge pull request 'ci: retry release file uploads over HTTP/1.1' (#138) from ci/release-upload-retry into main
ci / fork-checks (push) Skipped
ci / build (push) Skipped
github/ci (branch) GitHub Actions
ci / github (push) Successful in 49m48s
2026-09-30 22:24:27 +00:00
jcoffey-dev ea9a6f0c58 ci: retry release file uploads over HTTP/1.1
ci / fork-checks (pull_request) Skipped
ci / build (pull_request) Skipped
github/ci (branch) GitHub Actions
ci / github (pull_request) Successful in 6m45s
The v2026.9.30.1 binaries job lost a 50 MB upload to Gitea's release
API on each of its two runs (curl 92, HTTP/2 PROTOCOL_ERROR; the origin
logged 400 with no body), arm64 the first time and amd64 the second.
The uploads cross Cloudflare. A failed run also left the release short
of the file it had just deleted.

Uploads now go over HTTP/1.1, and every API call retries 5 times.
2026-09-30 15:17:05 -07:00
jcoffey-dev 1c1838af05 Release 2026.9.30.1
github/ci (branch) GitHub Actions
publish / version (push) Skipped
publish / publish-amd64 (push) Skipped
publish / publish-arm64 (push) Skipped
publish / release (push) Skipped
publish / binaries (push) Skipped
ci / github (pull_request) Successful in 6m45s
announce / announce (release) Successful in 10s
publish / github (push) Failing after 1h16m13s
publish / announce (push) Skipped
github/ci (tag) GitHub Actions
ci / fork-checks (pull_request) Skipped
ci / build (pull_request) Skipped
2026-09-30 13:45:36 -07:00
jcoffey-dev 78c9490b1e Merge pull request 'ci: give the release link swap on GitHub runners' (#136) from ci/release-link-swap into main
github/ci (branch) GitHub Actions
ci / github (push) Successful in 41m9s
ci / build (push) Skipped
ci / fork-checks (push) Skipped
2026-09-30 20:45:24 +00:00
jcoffey-dev ce2742fc80 ci: give the release link swap on GitHub runners
ci / fork-checks (pull_request) Skipped
ci / build (pull_request) Skipped
github/ci (branch) GitHub Actions
ci / github (pull_request) Successful in 7m25s
The v2026.9.30 tag build's arm64 publish job was killed linking the
inbuxa binary (fat LTO, one codegen unit): cannot allocate memory on the
16 GB ubuntu-24.04-arm runner. index, ghcr, release, binaries and
announce were skipped. amd64 got through on the same size of runner.

Each publish job now adds a 16 GB swap file before the build; buildx's
container has no memory limit of its own, so the linker can use it.
2026-09-30 13:37:27 -07:00
jcoffey-dev 81deaa69c4 Merge pull request 'Release 2026.9.30' (#134) from release/2026.9.30-pr into main
ci / fork-checks (push) Skipped
ci / build (push) Skipped
github/ci (branch) GitHub Actions
ci / github (push) Canceled after 28m9s
2026-09-30 20:17:09 +00:00
20 changed files with 185 additions and 57 deletions

No files matched your search

+20 -4
View File
@@ -228,6 +228,17 @@ jobs:
- run: | - run: |
sudo rm -rf /usr/share/dotnet /usr/local/lib/android /opt/ghc /opt/hostedtoolcache/CodeQL sudo rm -rf /usr/share/dotnet /usr/local/lib/android /opt/ghc /opt/hostedtoolcache/CodeQL
echo "IMAGE=${{ vars.REGISTRY }}/${GITHUB_REPOSITORY,,}" >> "$GITHUB_ENV" echo "IMAGE=${{ vars.REGISTRY }}/${GITHUB_REPOSITORY,,}" >> "$GITHUB_ENV"
# The release link (fat LTO, one codegen unit) outgrows the runner's
# 16 GB: v2026.9.30's arm64 link was killed for memory. Swap gives it
# room; buildx's container has no memory limit of its own, so it
# reaches the host's swap.
- run: |
sudo fallocate -l 16G /swap.release
sudo chmod 600 /swap.release
sudo mkswap /swap.release >/dev/null
sudo swapon /swap.release
free -g
df -h /
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: docker/setup-buildx-action@594f3bf4285d9ea8dc53c9a0c9c4092420091003 # v4.4.0 - uses: docker/setup-buildx-action@594f3bf4285d9ea8dc53c9a0c9c4092420091003 # v4.4.0
- uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 - uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
@@ -346,18 +357,23 @@ jobs:
cat SHA256SUMS cat SHA256SUMS
# A re-run of a tag replaces its assets rather than leaving two files # A re-run of a tag replaces its assets rather than leaving two files
# with the same name and different contents. # with the same name and different contents.
#
# The uploads cross Cloudflare, which dropped 50 MB HTTP/2 uploads
# part-way for v2026.9.30.1 (curl 92, PROTOCOL_ERROR; origin logged
# 400), once on each of two runs. Uploads go over HTTP/1.1 and retry.
- name: attach them to the release - name: attach them to the release
run: | run: |
set -euo pipefail set -euo pipefail
api="$GITEA_URL/api/v1/repos/$GITHUB_REPOSITORY" api="$GITEA_URL/api/v1/repos/$GITHUB_REPOSITORY"
auth="Authorization: token $GITEA_TOKEN" auth="Authorization: token $GITEA_TOKEN"
rel="$(curl -fsS -H "$auth" "$api/releases/tags/$TAG" | jq -r .id)" retry=(--retry 5 --retry-all-errors --retry-delay 15)
assets="$(curl -fsS -H "$auth" "$api/releases/$rel/assets")" rel="$(curl -fsS "${retry[@]}" -H "$auth" "$api/releases/tags/$TAG" | jq -r .id)"
assets="$(curl -fsS "${retry[@]}" -H "$auth" "$api/releases/$rel/assets")"
for f in out/inbuxa-linux-amd64.tar.gz out/inbuxa-linux-arm64.tar.gz out/SHA256SUMS; do for f in out/inbuxa-linux-amd64.tar.gz out/inbuxa-linux-arm64.tar.gz out/SHA256SUMS; do
name="$(basename "$f")" name="$(basename "$f")"
old="$(jq -r --arg n "$name" '.[] | select(.name == $n) | .id' <<<"$assets")" old="$(jq -r --arg n "$name" '.[] | select(.name == $n) | .id' <<<"$assets")"
for id in $old; do curl -fsS -o /dev/null -X DELETE -H "$auth" "$api/releases/$rel/assets/$id"; done for id in $old; do curl -fsS "${retry[@]}" -o /dev/null -X DELETE -H "$auth" "$api/releases/$rel/assets/$id"; done
curl -fsS -o /dev/null -X POST -H "$auth" -F "attachment=@$f" "$api/releases/$rel/assets?name=$name" curl -fsS --http1.1 "${retry[@]}" -o /dev/null -X POST -H "$auth" -F "attachment=@$f" "$api/releases/$rel/assets?name=$name"
echo "attached $name" echo "attached $name"
done done
+1 -1
View File
@@ -36,7 +36,7 @@ to Stalwart Labs with credit to you, and you'll be told that has happened.
This repository is the mail server. The web front ends have their own: This repository is the mail server. The web front ends have their own:
- [inbuxa-admin](https://git.coffeylabs.org/inbuxa/inbuxa-admin) - [inbuxa-admin](https://git.coffeylabs.org/inbuxa/inbuxa-admin)
- [ihasmail-inbuxa](https://git.coffeylabs.org/inbuxa/ihasmail-inbuxa) - [inbuxa-webmail](https://git.coffeylabs.org/inbuxa/inbuxa-webmail)
Upstream's own security documents are kept in `.github-upstream/` for Upstream's own security documents are kept in `.github-upstream/` for
reference. They describe Stalwart Labs' process, not this project's. reference. They describe Stalwart Labs' process, not this project's.
+2 -2
View File
@@ -14,7 +14,7 @@
//! 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;
//! - ihasmail-inbuxa, as the confidential client `ihasmail-inbuxa`, when //! - inbuxa-webmail, as the confidential client `ihasmail-inbuxa`, when
//! `INBUXA_WEBMAIL_URL` and `INBUXA_WEBMAIL_CLIENT_SECRET` are set. //! `INBUXA_WEBMAIL_URL` and `INBUXA_WEBMAIL_CLIENT_SECRET` are set.
//! //!
//! inbuxa: the environment variables stand in for `x:FrontEnds` (C-4) until //! inbuxa: the environment variables stand in for `x:FrontEnds` (C-4) until
@@ -22,7 +22,7 @@
//! it instead. //! it instead.
//! //!
//! A missing client is created. An existing one gains any redirect URI it //! A missing client is created. An existing one gains any redirect URI it
//! lacks and, for ihasmail-inbuxa, the configured secret; nothing an operator //! lacks and, for inbuxa-webmail, the configured secret; nothing an operator
//! added is removed. //! added is removed.
use directory::core::secret::{hash_secret, verify_secret_hash}; use directory::core::secret::{hash_secret, verify_secret_hash};
+55 -3
View File
@@ -116,8 +116,19 @@ impl StoredMetric {
/// changes (MON-4). Per process: a restart counts from the start. /// changes (MON-4). Per process: a restart counts from the start.
static LAST: Mutex<Option<AHashMap<MetricType, (u64, u64)>>> = Mutex::new(None); static LAST: Mutex<Option<AHashMap<MetricType, (u64, u64)>>> = Mutex::new(None);
/// One tick's samples (MON-4 to MON-6). /// Gauges that count the whole cluster's data, not this node's. Only the node
pub fn sample() -> Vec<Metric> { /// that computes them (the metrics-calculation role) has a true reading; on
/// the others the queue gauge only moves with local queue events and drifts
/// below zero, and the account and domain counts stay at 0.
const CLUSTER_GAUGES: [MetricType; 3] = [
MetricType::QueueCount,
MetricType::UserCount,
MetricType::DomainCount,
];
/// One tick's samples (MON-4 to MON-6). `calculates` is whether this node
/// computes the cluster-wide gauges; a node that doesn't leaves them out.
pub fn sample(calculates: bool) -> Vec<Metric> {
let mut last_guard = LAST.lock().unwrap(); let mut last_guard = LAST.lock().unwrap();
let last = last_guard.get_or_insert_with(AHashMap::new); let last = last_guard.get_or_insert_with(AHashMap::new);
let mut samples = Vec::new(); let mut samples = Vec::new();
@@ -140,6 +151,9 @@ pub fn sample() -> Vec<Metric> {
// Gauges: the reading, always (MON-5) // Gauges: the reading, always (MON-5)
for gauge in Collector::collect_gauges() { for gauge in Collector::collect_gauges() {
if !calculates && CLUSTER_GAUGES.contains(&gauge.id()) {
continue;
}
samples.push(Metric::Gauge(MetricCount { samples.push(Metric::Gauge(MetricCount {
count: gauge.get(), count: gauge.get(),
metric: gauge.id(), metric: gauge.id(),
@@ -181,7 +195,7 @@ impl Server {
if store.is_none() { if store.is_none() {
return; return;
} }
let samples = sample(); let samples = sample(self.core.network.roles.metrics_calculate);
let count = samples.len(); let count = samples.len();
let started = std::time::Instant::now(); let started = std::time::Instant::now();
match store.write_metrics(samples, now()).await { match store.write_metrics(samples, now()).await {
@@ -271,3 +285,41 @@ impl Server {
} }
} }
} }
#[cfg(test)]
mod tests {
use super::*;
fn gauges(samples: &[Metric]) -> Vec<MetricType> {
samples
.iter()
.filter_map(|m| match m {
Metric::Gauge(g) => Some(g.metric),
_ => None,
})
.collect()
}
#[test]
fn only_the_calculating_node_stores_cluster_gauges() {
let all = gauges(&sample(true));
let local = gauges(&sample(false));
for metric in CLUSTER_GAUGES {
assert!(
all.contains(&metric),
"{metric:?} missing on the calculating node"
);
assert!(
!local.contains(&metric),
"{metric:?} stored by a node that doesn't compute it"
);
}
// Per-node gauges are stored either way
for metric in [MetricType::ServerMemory, MetricType::HttpActiveConnections] {
assert!(
all.contains(&metric) && local.contains(&metric),
"{metric:?}"
);
}
}
}
+69 -9
View File
@@ -176,16 +176,23 @@ impl TlsaLookup for Server {
return mail_auth::common::resolver::mock_resolve(key.as_ref()); return mail_auth::common::resolver::mock_resolve(key.as_ref());
} }
let tlsa_lookup = match self // Through `validated_lookup`, like the MX and address lookups: a TLSA
.core // name that is a signed CNAME to a name with no TLSA record (seen at
.smtp // `_25._tcp.mail.usefulinsight.com`, behind Hetzner's resolvers) is
.resolvers // otherwise called bogus, and the message waits on it until it
.dnssec // expires.
.resolver let tlsa_lookup = match validated_lookup(
.tlsa_lookup(Name::from_str_relaxed(key.as_ref())?) &self.core.smtp.resolvers.dnssec.resolver,
self.core.smtp.resolvers.dns.resolver(),
Name::from_str_relaxed(key.as_ref())?,
RecordType::TLSA,
)
.await .await
{ {
Ok(tlsa_lookup) => tlsa_lookup, // A TLSA record proved to sit in an unsigned zone is no DANE
// policy at all.
Ok(validated) if validated.insecure => return Ok(TlsaResult::Missing),
Ok(validated) => validated.lookup,
Err(err) => { Err(err) => {
if let Some(denial) = NegativeAnswer::from_error(&err) { if let Some(denial) = NegativeAnswer::from_error(&err) {
return Ok(if denial.dnssec_status == DnssecStatus::Bogus { return Ok(if denial.dnssec_status == DnssecStatus::Bogus {
@@ -436,7 +443,8 @@ impl TlsaLookup for Server {
// record in the DS reply, and public resolvers often send none. // record in the DS reply, and public resolvers often send none.
// - A signed CNAME to a signed name without the record type queried. Hickory // - A signed CNAME to a signed name without the record type queried. Hickory
// checks the denial of existence against the name first asked for, not the // checks the denial of existence against the name first asked for, not the
// target's, and rejects it. // target's, and rejects it. TLSA lookups hit this too: a TLSA name that is
// a CNAME to the zone apex, with no TLSA there, held mail to it for a week.
// //
// When hickory says bogus, check the answer again with lookups it gets right. // When hickory says bogus, check the answer again with lookups it gets right.
// A signed CNAME is followed and the lookup repeated at its target. Otherwise // A signed CNAME is followed and the lookup repeated at its target. Otherwise
@@ -869,4 +877,56 @@ mod tests {
assert!(validated.insecure); assert!(validated.insecure);
assert!(!validated.lookup.answers().is_empty()); assert!(!validated.lookup.answers().is_empty());
} }
// Needs the network: a TLSA name that is a signed CNAME to the zone apex,
// which has no TLSA record. Cloudflare's resolver answers with a compact
// denial at the name itself; Hetzner's (and others) follow the CNAME, and
// hickory then calls the answer bogus. Point the lookup at a resolver that
// follows it with INBUXA_TEST_DNS_TCP=<ip:port> (TCP), for instance over
// an SSH tunnel to 185.12.64.2:53 from a Hetzner host.
#[tokio::test]
#[ignore]
async fn validated_lookup_follows_signed_cname_for_tlsa() {
use mail_auth::hickory_resolver::{
config::{CLOUDFLARE, ConnectionConfig, NameServerConfig, ResolverConfig, ResolverOpts},
net::runtime::TokioRuntimeProvider,
};
let config = match std::env::var("INBUXA_TEST_DNS_TCP") {
Ok(addr) => {
let addr: std::net::SocketAddr = addr.parse().unwrap();
let mut ns = NameServerConfig::new(addr.ip(), true, vec![ConnectionConfig::tcp()]);
if let Some(c) = ns.connections.first_mut() {
c.port = addr.port();
} }
ResolverConfig::from_parts(None, vec![], vec![ns])
}
Err(_) => ResolverConfig::udp_and_tcp(&CLOUDFLARE),
};
let build = |validate: bool| {
let mut opts = ResolverOpts::default();
opts.validate = validate;
opts.num_concurrent_reqs = 1;
opts.cache_size = 0;
TokioResolver::builder_with_config(config.clone(), TokioRuntimeProvider::default())
.with_options(opts)
.build()
.unwrap()
};
let (dnssec, plain) = (build(true), build(false));
let query = name("_25._tcp.mail.usefulinsight.com.");
let direct = dnssec.lookup(query.clone(), RecordType::TLSA).await;
eprintln!("hickory alone: {:?}", direct.as_ref().err().map(|e| e.to_string()));
let err = match validated_lookup(&dnssec, &plain, query, RecordType::TLSA).await {
Ok(validated) => panic!("expected no TLSA record, got {:?}", validated.lookup.answers()),
Err(err) => err,
};
let denial = NegativeAnswer::from_error(&err).expect("a denial of existence");
assert_eq!(denial.response_code, ResponseCode::NoError);
assert_ne!(denial.dnssec_status, DnssecStatus::Bogus);
}
}
+1 -1
View File
@@ -81,7 +81,7 @@ fn legacy_setting(name: &str, is_set: impl Fn(&str) -> bool) -> Option<String> {
#[macro_export] #[macro_export]
macro_rules! brand_version { macro_rules! brand_version {
() => { () => {
"2026.9.30" "2026.10.5"
}; };
} }
+1 -1
View File
@@ -618,7 +618,7 @@ the tenant administrators' own view is not yet recorded
## 8. Open decisions ## 8. Open decisions
- The INBUXA fork of ihasmail is **ihasmail-inbuxa** (named 2026-09-18). Open: its repository, and how it tracks - The INBUXA fork of ihasmail is **inbuxa-webmail** (named ihasmail-inbuxa on 2026-09-18, renamed 2026-10-05). Open: how it tracks
public ihasmail (§5). public ihasmail (§5).
- Product name: whether the shipped product is called inbuxa-server or - Product name: whether the shipped product is called inbuxa-server or
something else inside the INBUXA brand. something else inside the INBUXA brand.
+19 -19
View File
@@ -7,11 +7,11 @@ Status: draft, 2026-09-18. Expands SPEC.md §5.2.
| Party | What it is | How it reaches the server | | Party | What it is | How it reaches the server |
|---|---|---| |---|---|---|
| **inbuxa-server** | The mail server | — | | **inbuxa-server** | The mail server | — |
| **ihasmail-inbuxa** | The INBUXA fork of ihasmail: a Node server and a web app. Public ihasmail stays Stalwart-facing and isn't party to this (SPEC.md §5) | Its **Node server** calls inbuxa-server, server to server. The browser only ever talks to ihasmail-inbuxa | | **inbuxa-webmail** | The INBUXA fork of ihasmail: a Node server and a web app. Public ihasmail stays Stalwart-facing and isn't party to this (SPEC.md §5) | Its **Node server** calls inbuxa-server, server to server. The browser only ever talks to inbuxa-webmail |
| **INBUXA Admin** (`inbuxa-admin`) | A static web app, a fork of Stalwart WebUI | The **browser** calls inbuxa-server directly, cross-origin | | **INBUXA Admin** (`inbuxa-admin`) | A static web app, a fork of Stalwart WebUI | The **browser** calls inbuxa-server directly, cross-origin |
That split decides most of what follows. Cross-origin rules matter only for That split decides most of what follows. Cross-origin rules matter only for
INBUXA Admin. Token custody matters most for ihasmail-inbuxa, which holds INBUXA Admin. Token custody matters most for inbuxa-webmail, which holds
tokens on its server for people who aren't there. tokens on its server for people who aren't there.
## What upstream does today ## What upstream does today
@@ -45,7 +45,7 @@ Observed in the source at `v0.16.22` and against a running inbuxa-server on
- **Endpoint gating:** `x:Http.allowedEndpoints` is an expression that can - **Endpoint gating:** `x:Http.allowedEndpoints` is an expression that can
refuse endpoints by path and client IP. JMAP administration shares `/jmap` refuse endpoints by path and client IP. JMAP administration shares `/jmap`
with everything else, so it can't separate admin calls on its own. with everything else, so it can't separate admin calls on its own.
- **ihasmail today** (public, and so the starting point for ihasmail-inbuxa) - **ihasmail today** (public, and so the starting point for inbuxa-webmail)
signs in with HTTP Basic auth and keeps the password sealed in its session signs in with HTTP Basic auth and keeps the password sealed in its session
store (`sealedCredentials: {username, password}`), sending it on every store (`sealedCredentials: {username, password}`), sending it on every
upstream call. It registers JMAP push subscriptions to its own URL, and reads upstream call. It registers JMAP push subscriptions to its own URL, and reads
@@ -76,7 +76,7 @@ Each has an ID, and tests name the IDs they check.
(LP-19). Added 2026-09-21. (LP-19). Added 2026-09-21.
- **C-2.** Each front end states the contract versions it supports and checks - **C-2.** Each front end states the contract versions it supports and checks
`contract` after signing in. Outside its range it stops, with a message `contract` after signing in. Outside its range it stops, with a message
naming both versions. For ihasmail-inbuxa this replaces public ihasmail's naming both versions. For inbuxa-webmail this replaces public ihasmail's
"Stalwart 0.16 or later" check. "Stalwart 0.16 or later" check.
- **C-3.** A breaking change to anything in this document bumps `contract`. - **C-3.** A breaking change to anything in this document bumps `contract`.
Adding optional fields doesn't. Adding optional fields doesn't.
@@ -117,7 +117,7 @@ Each has an ID, and tests name the IDs they check.
- **`inbuxa-admin`**: a public client (no secret), authorization code with - **`inbuxa-admin`**: a public client (no secret), authorization code with
PKCE S256, redirect URI `{adminUrl}/oauth/callback`. PKCE S256, redirect URI `{adminUrl}/oauth/callback`.
- **`ihasmail-inbuxa`**: a confidential client with a secret held by the - **`ihasmail-inbuxa`**: a confidential client with a secret held by the
ihasmail-inbuxa server, authorization code with PKCE S256, redirect URI inbuxa-webmail 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`.
Served by the server itself it uses the web interface's client, Served by the server itself it uses the web interface's client,
@@ -159,7 +159,7 @@ Each has an ID, and tests name the IDs they check.
- **C-8.** People sign in on **the server's own sign-in page** (`/login`, - **C-8.** People sign in on **the server's own sign-in page** (`/login`,
already INBUXA-branded), never on a front end's form. Two-factor happens already INBUXA-branded), never on a front end's form. Two-factor happens
there, on the page's existing one-time-code step. Front ends never see a there, on the page's existing one-time-code step. Front ends never see a
password. ihasmail-inbuxa's own sign-in form is retired in favor of a password. inbuxa-webmail's own sign-in form is retired in favor of a
redirect. redirect.
- **C-9.** **Consent for anything that isn't first-party.** When a client other - **C-9.** **Consent for anything that isn't first-party.** When a client other
than the two first-party ones asks to sign someone in, the sign-in page names than the two first-party ones asks to sign someone in, the sign-in page names
@@ -170,10 +170,10 @@ Each has an ID, and tests name the IDs they check.
### Tokens ### Tokens
- **C-10.** ihasmail-inbuxa holds tokens, never passwords. It keeps the access - **C-10.** inbuxa-webmail holds tokens, never passwords. It keeps the access
and refresh token for each session sealed in its session store, where it now and refresh token for each session sealed in its session store, where it now
keeps sealed credentials, and refreshes the access token before it expires. keeps sealed credentials, and refreshes the access token before it expires.
The browser still holds only ihasmail-inbuxa's own session cookie. Public The browser still holds only inbuxa-webmail's own session cookie. Public
ihasmail's "the browser never holds a credential" property is kept. ihasmail's "the browser never holds a credential" property is kept.
- **C-11.** INBUXA Admin holds its tokens in the browser, as upstream WebUI - **C-11.** INBUXA Admin holds its tokens in the browser, as upstream WebUI
does, since it has no server of its own. So admin tokens are short-lived does, since it has no server of its own. So admin tokens are short-lived
@@ -193,9 +193,9 @@ Each has an ID, and tests name the IDs they check.
A revoked token stops working on its next use, and never later than one A revoked token stops working on its next use, and never later than one
access-token lifetime. access-token lifetime.
- **C-13.** Grants are listed per account (client, device description, created, - **C-13.** Grants are listed per account (client, device description, created,
last used, IP), so ihasmail-inbuxa's "your sessions" screen shows server-side last used, IP), so inbuxa-webmail's "your sessions" screen shows server-side
truth. Lifetimes, all configurable: access tokens 1 hour and refresh 30 days truth. Lifetimes, all configurable: access tokens 1 hour and refresh 30 days
for ihasmail-inbuxa; access tokens 15 minutes and refresh 8 hours for for inbuxa-webmail; access tokens 15 minutes and refresh 8 hours for
`inbuxa-admin`. `inbuxa-admin`.
### Cross-origin ### Cross-origin
@@ -237,7 +237,7 @@ Each has an ID, and tests name the IDs they check.
scope `inbuxa:admin`, which only that client is ever granted. An admin scope `inbuxa:admin`, which only that client is ever granted. An admin
account signing in through a mail client can't administer the server with account signing in through a mail client can't administer the server with
that token, even though the account could. that token, even though the account could.
- **C-19.** ihasmail-inbuxa's own administration (accounts, domains, groups, - **C-19.** inbuxa-webmail's own administration (accounts, domains, groups,
lists, roles, tenants, the dashboard) uses the scope `inbuxa:account-admin`, lists, roles, tenants, the dashboard) uses the scope `inbuxa:account-admin`,
granted only to `ihasmail-inbuxa`, and limited to those object types, plus granted only to `ihasmail-inbuxa`, and limited to those object types, plus
`x:Metric` get and query for the dashboard's message cards (monitoring `x:Metric` get and query for the dashboard's message cards (monitoring
@@ -250,7 +250,7 @@ Each has an ID, and tests name the IDs they check.
### Push ### Push
- **C-22.** Unchanged from public ihasmail: ihasmail-inbuxa registers JMAP push - **C-22.** Unchanged from public ihasmail: inbuxa-webmail registers JMAP push
subscriptions to its own URL, with VAPID for browser notifications. The only subscriptions to its own URL, with VAPID for browser notifications. The only
difference is that it authenticates with its token rather than the password. difference is that it authenticates with its token rather than the password.
@@ -279,7 +279,7 @@ Each has an ID, and tests name the IDs they check.
`INBUXA_HTTP_BASIC_AUTH=all`; `dav`, the default, is this rule. Any other `INBUXA_HTTP_BASIC_AUTH=all`; `dav`, the default, is this rule. Any other
value logs a warning and keeps the default. The setting moves to the value logs a warning and keeps the default. The setting moves to the
registry with `x:FrontEnds` (C-4). registry with `x:FrontEnds` (C-4).
ihasmail-inbuxa confirms a typed password, which it does before creating inbuxa-webmail confirms a typed password, which it does before creating
an app password, on `/api/auth` as its own client, to its registered an app password, on `/api/auth` as its own client, to its registered
redirect URI, with a PKCE challenge whose verifier it discards. A redirect URI, with a PKCE challenge whose verifier it discards. A
"two-factor code needed" answer counts as confirmed, since the server gives "two-factor code needed" answer counts as confirmed, since the server gives
@@ -294,7 +294,7 @@ Each has an ID, and tests name the IDs they check.
didn't register refused. didn't register refused.
Observed before the change, in INBUXA's production logs from 2026-09-20 to 2026-09-29: Observed before the change, in INBUXA's production logs from 2026-09-20 to 2026-09-29:
every HTTPS password sign-in was the operator's own, apart from every HTTPS password sign-in was the operator's own, apart from
ihasmail-inbuxa's password sign-in on 2026-09-22, before it moved to OAuth. inbuxa-webmail's password sign-in on 2026-09-22, before it moved to OAuth.
The logs don't say whether a sign-in used a Basic header or the sign-in The logs don't say whether a sign-in used a Basic header or the sign-in
page. page.
@@ -304,8 +304,8 @@ Each has an ID, and tests name the IDs they check.
(SPEC.md §6.2). In bootstrap mode, CORS is permissive (C-16) and the (SPEC.md §6.2). In bootstrap mode, CORS is permissive (C-16) and the
recovery administrator applies. recovery administrator applies.
2. It sets `x:FrontEnds` (webmail and admin URLs, the public URL), which 2. It sets `x:FrontEnds` (webmail and admin URLs, the public URL), which
registers both first-party clients (C-6). For ihasmail-inbuxa it returns the registers both first-party clients (C-6). For inbuxa-webmail it returns the
client secret once, for the installer to write into ihasmail-inbuxa's client secret once, for the installer to write into inbuxa-webmail's
environment. environment.
3. After the restart out of bootstrap, CORS follows C-14, registration is 3. After the restart out of bootstrap, CORS follows C-14, registration is
required (C-5), and the recovery administrator is ignored (SPEC.md §6.2). required (C-5), and the recovery administrator is ignored (SPEC.md §6.2).
@@ -376,7 +376,7 @@ client and reload settings. This is the state C-5 and C-6 make the default.
5. The phishing flow in the security note fails at step 1, and a registered 5. The phishing flow in the security note fails at step 1, and a registered
third-party client with a non-first-party redirect shows the consent page third-party client with a non-first-party redirect shows the consent page
(C-9). (C-9).
6. ihasmail-inbuxa signs in without ever handling a password. Its session 6. inbuxa-webmail signs in without ever handling a password. Its session
store holds tokens only (C-8, C-10). store holds tokens only (C-8, C-10).
7. Revoking one grant stops that session within one access-token lifetime, 7. Revoking one grant stops that session within one access-token lifetime,
leaves others working, and "sign out other sessions" keeps the current one leaves others working, and "sign out other sessions" keeps the current one
@@ -388,7 +388,7 @@ client and reload settings. This is the state C-5 and C-6 make the default.
10. In bootstrap mode, INBUXA Admin reaches the server from any origin (C-16). 10. In bootstrap mode, INBUXA Admin reaches the server from any origin (C-16).
11. An admin account's token from a third-party mail client can't read 11. An admin account's token from a third-party mail client can't read
`x:NetworkListener`. The same account through `inbuxa-admin` can (C-18). `x:NetworkListener`. The same account through `inbuxa-admin` can (C-18).
12. ihasmail-inbuxa's token can manage accounts and tenants but not listeners 12. inbuxa-webmail's token can manage accounts and tenants but not listeners
or certificates (C-19). or certificates (C-19).
13. With `adminNetworks` set, an `inbuxa:admin` request from outside is refused 13. With `adminNetworks` set, an `inbuxa:admin` request from outside is refused
(C-20). (C-20).
@@ -402,4 +402,4 @@ client and reload settings. This is the state C-5 and C-6 make the default.
2. The consent page's wording and whether it remembers a decision per client. 2. The consent page's wording and whether it remembers a decision per client.
3. API keys and app passwords with explicit scopes (C-21): what upstream's 3. API keys and app passwords with explicit scopes (C-21): what upstream's
`x:ApiKey` already supports, to observe before specifying. `x:ApiKey` already supports, to observe before specifying.
4. Whether ihasmail-inbuxa's secret should rotate, and how. 4. Whether inbuxa-webmail's secret should rotate, and how.
+1 -1
View File
@@ -584,7 +584,7 @@ Three consequences:
name whose DNS points at the mail addresses. name whose DNS points at the mail addresses.
- Whether the front ends need anything at cutover, or follow separately - Whether the front ends need anything at cutover, or follow separately
(SPEC.md §5). The rehearsal does not start them. (SPEC.md §5). The rehearsal does not start them.
- Whether ihasmail-inbuxa and INBUXA Admin behave under real use, rather - Whether inbuxa-webmail and INBUXA Admin behave under real use, rather
than at first sign-in. Both were verified as far as signing in and, for the than at first sign-in. Both were verified as far as signing in and, for the
webmail, mail flowing. webmail, mail flowing.
- The checks only users can make: the second account, the mailbox comparison - The checks only users can make: the second account, the mailbox comparison
+1 -1
View File
@@ -353,7 +353,7 @@ adds at most 2.
## ihasmail changes ## ihasmail changes
These go in the INBUXA fork of ihasmail, ihasmail-inbuxa, never in public These go in the INBUXA fork of ihasmail, inbuxa-webmail, never in public
ihasmail, which stays Stalwart-facing (SPEC.md §5). ihasmail, which stays Stalwart-facing (SPEC.md §5).
- **Reading:** when a message has an `X-Spam-LLM` header, the message details - **Reading:** when a message has an `X-Spam-LLM` header, the message details
+2 -2
View File
@@ -275,7 +275,7 @@ Each requirement has an ID, and tests name the IDs they check.
## ihasmail changes ## ihasmail changes
These go in the INBUXA fork of ihasmail, ihasmail-inbuxa, never in public These go in the INBUXA fork of ihasmail, inbuxa-webmail, never in public
ihasmail, which stays Stalwart-facing (SPEC.md §5). ihasmail, which stays Stalwart-facing (SPEC.md §5).
- **Administration, Domains:** a logo field on each domain. Upload a PNG, JPEG - **Administration, Domains:** a logo field on each domain. Upload a PNG, JPEG
@@ -389,7 +389,7 @@ files carry hooks marked `inbuxa:`. Acceptance tests 1 to 17 pass as
`tests/src/system/branding.rs`. `tests/src/system/branding.rs`.
- **BT-1 to BT-26:** built. - **BT-1 to BT-26:** built.
- **ihasmail changes** belong to ihasmail-inbuxa and aren't part of this - **ihasmail changes** belong to inbuxa-webmail and aren't part of this
repository. repository.
- **Test 18 (compat)** is written as `branding_compat`, ignored, and unrun - **Test 18 (compat)** is written as `branding_compat`, ignored, and unrun
until a copy of INBUXA's data is provided. INBUXA holds no logos or until a copy of INBUXA's data is provided. INBUXA holds no logos or
@@ -14,7 +14,7 @@ Written for the record SPEC.md §3 rule 3 asks for. Sources, and nothing else:
|---|---|---| |---|---|---|
| This repository at `0502eb4` (2026-09-28): `crates/smtp/src/inbound/data.rs`, `crates/smtp/src/queue/`, `crates/jmap/src/submission/set.rs`, `crates/common/src/scripts/`, `vendor/sieve-rs`, `resources/schema/schema.json.gz` | AGPL-3.0-only | Where a check can run, what the queue stores, what a sender sees on a refusal | | This repository at `0502eb4` (2026-09-28): `crates/smtp/src/inbound/data.rs`, `crates/smtp/src/queue/`, `crates/jmap/src/submission/set.rs`, `crates/common/src/scripts/`, `vendor/sieve-rs`, `resources/schema/schema.json.gz` | AGPL-3.0-only | Where a check can run, what the queue stores, what a sender sees on a refusal |
| inbuxa-admin at `b82904c` | AGPL-3.0-only | Where the pages go | | inbuxa-admin at `b82904c` | AGPL-3.0-only | Where the pages go |
| ihasmail-inbuxa (the webmail) at `290bc63` | AGPL-3.0-or-later | How a refused send reaches the person sending | | inbuxa-webmail (the webmail) at `290bc63` | AGPL-3.0-or-later | How a refused send reaches the person sending |
| `inbuxa-drafts/queue/dlp.md`, `rule-builder.md` | Own | What John asked for and settled | | `inbuxa-drafts/queue/dlp.md`, `rule-builder.md` | Own | What John asked for and settled |
| The personal-data catalog spec and the audit-hold-lock spec | Own | Roles, the audit log, legal holds, the catalog check | | The personal-data catalog spec and the audit-hold-lock spec | Own | Roles, the audit log, legal holds, the catalog check |
| RFC 5321, RFC 3463 (enhanced status codes), RFC 8620/8621 (JMAP) | Public | Refusal codes and the submission error shape | | RFC 5321, RFC 3463 (enhanced status codes), RFC 8620/8621 (JMAP) | Public | Refusal codes and the submission error shape |
@@ -385,7 +385,7 @@ first). The inspection limit caps the worst case.
Every form previews the rule in words ("If a recipient is outside and the Every form previews the rule in words ("If a recipient is outside and the
message contains 5 or more card numbers, hold it for review"). message contains 5 or more card numbers, hold it for review").
## 4. Webmail (ihasmail-inbuxa) ## 4. Webmail (inbuxa-webmail)
- A warning dialog: the notice, a reason field, **Send anyway** and **Edit - A warning dialog: the notice, a reason field, **Send anyway** and **Edit
message**. message**.
+1 -1
View File
@@ -291,7 +291,7 @@ upstream files carry hooks marked `inbuxa:`. Acceptance tests 1 to 11 pass as
`createdBy`. The server-set name is **deferred** until sign-in goes through `createdBy`. The server-set name is **deferred** until sign-in goes through
OAuth (contract C-8): with Basic auth there's no client name, so a mask OAuth (contract C-8): with Basic auth there's no client name, so a mask
created through the Fastmail API has none. created through the Fastmail API has none.
- **ihasmail changes** belong to ihasmail-inbuxa and aren't part of this - **ihasmail changes** belong to inbuxa-webmail and aren't part of this
repository. repository.
- **Test 12 (compat)** is written as `masked_email_compat`, ignored, and unrun - **Test 12 (compat)** is written as `masked_email_compat`, ignored, and unrun
until a copy of INBUXA's data with masks made on it is provided. Its doc until a copy of INBUXA's data with masks made on it is provided. Its doc
+1 -1
View File
@@ -411,7 +411,7 @@ unchanged.
## ihasmail changes ## ihasmail changes
These go in ihasmail-inbuxa, not public ihasmail, which stays Stalwart-facing These go in inbuxa-webmail, not public ihasmail, which stays Stalwart-facing
(SPEC.md §5). (SPEC.md §5).
- The dashboard already reads `x:Metric` for received, sent and memory. Keep - The dashboard already reads `x:Metric` for received, sent and memory. Keep
+2 -2
View File
@@ -336,9 +336,9 @@ called from `system_tests` with no gate.
- **MT-1 to MT-18, MT-20 to MT-23:** built. - **MT-1 to MT-18, MT-20 to MT-23:** built.
- **MT-19, MT-19a:** built, except the submission-time warning, **deferred** - **MT-19, MT-19a:** built, except the submission-time warning, **deferred**
(see MT-19a) until the contract defines a warnings shape. (see MT-19a) until the contract defines a warnings shape.
- **ihasmail changes** (the section above) belong to ihasmail-inbuxa and - **ihasmail changes** (the section above) belong to inbuxa-webmail and
aren't part of this repository. Its branding and quota warnings wait for aren't part of this repository. Its branding and quota warnings wait for
ihasmail-inbuxa. inbuxa-webmail.
- **Test 15 (compat)** is written as `tenant_compat`, ignored, and unrun until - **Test 15 (compat)** is written as `tenant_compat`, ignored, and unrun until
a copy of INBUXA's data is provided. Its doc comment says how to run it. a copy of INBUXA's data is provided. Its doc comment says how to run it.
- **Known limits, not requirements of this spec:** - **Known limits, not requirements of this spec:**
+1 -1
View File
@@ -412,7 +412,7 @@ check it and the fork keeps it.
## ihasmail changes ## ihasmail changes
These go in ihasmail-inbuxa, the INBUXA fork of ihasmail, never in public These go in inbuxa-webmail, the INBUXA fork of ihasmail, never in public
ihasmail, which stays Stalwart-facing (SPEC.md §5). ihasmail, which stays Stalwart-facing (SPEC.md §5).
- **Domain editor:** a directory picker offering "server default" and the - **Domain editor:** a directory picker offering "server default" and the
+1 -1
View File
@@ -679,7 +679,7 @@ SCIM error documents (RFC 7644 §3.12): `schemas`
## ihasmail changes ## ihasmail changes
These go in ihasmail-inbuxa, not public ihasmail, which stays These go in inbuxa-webmail, not public ihasmail, which stays
Stalwart-facing (SPEC.md §5). Stalwart-facing (SPEC.md §5).
- **Domains:** an "Allow SCIM provisioning" switch on the domain form. Turning - **Domains:** an "Allow SCIM provisioning" switch on the domain form. Turning
+1 -1
View File
@@ -276,7 +276,7 @@ marked `inbuxa:`. Acceptance tests 1 to 15 pass as
`tests/src/system/undelete.rs`, with `/changes` and the `/query` filters. `tests/src/system/undelete.rs`, with `/changes` and the `/query` filters.
- **UD-1 to UD-17a:** built. - **UD-1 to UD-17a:** built.
- **ihasmail changes** belong to ihasmail-inbuxa and aren't part of this - **ihasmail changes** belong to inbuxa-webmail and aren't part of this
repository. repository.
- **Test 16 (compat)** is written as `undelete_compat`, ignored, and unrun - **Test 16 (compat)** is written as `undelete_compat`, ignored, and unrun
until a copy of INBUXA's data with archived items made on it is provided. until a copy of INBUXA's data with archived items made on it is provided.
+1 -1
View File
@@ -103,7 +103,7 @@ conflicts.
1. Every requirement MT-1 to MT-23 is implemented, or deliberately deferred 1. Every requirement MT-1 to MT-23 is implemented, or deliberately deferred
with a line in the spec saying so. The branding and quota warnings for with a line in the spec saying so. The branding and quota warnings for
ihasmail can wait for ihasmail-inbuxa. ihasmail can wait for inbuxa-webmail.
2. Acceptance tests 1 to 14 pass as integration tests 2. Acceptance tests 1 to 14 pass as integration tests
(`tests/src/system/tenant.rs`), with the `pending-rebuild` gate removed (`tests/src/system/tenant.rs`), with the `pending-rebuild` gate removed
from the tenant call. from the tenant call.
+3 -3
View File
@@ -12,7 +12,7 @@ Boots the debug binary and checks that:
- DAV still takes Basic, and its 401 still offers it; - DAV still takes Basic, and its 401 still offers it;
- a token from the sign-in endpoint (`/api/auth`, the password in the body) - a token from the sign-in endpoint (`/api/auth`, the password in the body)
and the token endpoint works on JMAP: the path the front ends use, and the and the token endpoint works on JMAP: the path the front ends use, and the
one ihasmail-inbuxa's password check relies on; one inbuxa-webmail's password check relies on;
- INBUXA_HTTP_BASIC_AUTH=all puts Basic back everywhere, an unknown value - INBUXA_HTTP_BASIC_AUTH=all puts Basic back everywhere, an unknown value
keeps the default with a warning, and recovery mode accepts Basic. keeps the default with a warning, and recovery mode accepts Basic.
@@ -64,7 +64,7 @@ def start(env=None):
"-p", f"127.0.0.1:{PORT}:8080", "-p", f"127.0.0.1:{PORT}:8080",
# A debug build's workers need more than the default stack. # A debug build's workers need more than the default stack.
"-e", "RUST_MIN_STACK=16777216", "-e", "RUST_MIN_STACK=16777216",
# Registers inbuxa-admin and ihasmail-inbuxa (C-6). # Registers inbuxa-admin and inbuxa-webmail (C-6).
"-e", f"INBUXA_ADMIN_URL={ADMIN_URL}", "-e", f"INBUXA_WEBMAIL_URL={WEBMAIL_URL}"] "-e", f"INBUXA_ADMIN_URL={ADMIN_URL}", "-e", f"INBUXA_WEBMAIL_URL={WEBMAIL_URL}"]
env_file = f"{DIR}/secrets/basic-env" env_file = f"{DIR}/secrets/basic-env"
with open(env_file, "w") as f: with open(env_file, "w") as f:
@@ -243,7 +243,7 @@ def main():
status, _, _ = request("/api/account", f"Bearer {access}") status, _, _ = request("/api/account", f"Bearer {access}")
check(status == 200, f"a token works on /api/account ({status})") check(status == 200, f"a token works on /api/account ({status})")
# ihasmail-inbuxa's password check before an app password: its own client, # inbuxa-webmail's password check before an app password: its own client,
# its registered redirect URI, a verifier it throws away. # its registered redirect URI, a verifier it throws away.
for password, want in ((user_pw, "authenticated"), ("not-the-password", "failure")): for password, want in ((user_pw, "authenticated"), ("not-the-password", "failure")):
got = sign_in(user, password, "ihasmail-inbuxa", WEBMAIL_REDIRECT, secrets.token_urlsafe(48)) got = sign_in(user, password, "ihasmail-inbuxa", WEBMAIL_REDIRECT, secrets.token_urlsafe(48))