Compare commits
16
Commits
v2026.9.30
...
v2026.10.5
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d7bebd454d | ||
|
|
282ad5fc13 | ||
|
|
133d41df36 | ||
|
|
c4a6e4d117 | ||
|
|
f59a9de4dc | ||
|
|
083f22d6fb | ||
|
|
c43abef8ab | ||
|
|
c9f8028502 | ||
|
|
cd7a0f4163 | ||
|
|
30d4cef0e7 | ||
|
|
6c1eeea038 | ||
|
|
ea9a6f0c58 | ||
|
|
1c1838af05 | ||
|
|
78c9490b1e | ||
|
|
ce2742fc80 | ||
|
|
81deaa69c4 |
No files matched your search
@@ -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
@@ -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.
|
||||||
@@ -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};
|
||||||
|
|||||||
@@ -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:?}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
.await
|
self.core.smtp.resolvers.dns.resolver(),
|
||||||
|
Name::from_str_relaxed(key.as_ref())?,
|
||||||
|
RecordType::TLSA,
|
||||||
|
)
|
||||||
|
.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);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -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
@@ -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
@@ -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.
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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**.
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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:**
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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))
|
||||||
|
|||||||
Reference in new issue
Block a user