diff --git a/WORK-RECORDS.md b/WORK-RECORDS.md index 7164483..31b902e 100644 --- a/WORK-RECORDS.md +++ b/WORK-RECORDS.md @@ -111,7 +111,7 @@ | task | CSOC-WP-0014-T06 | progress | — | workplans/CSOC-WP-0014-cohort-transfer-bubble-offramp.md | | task | CSOC-WP-0014-T07 | todo | — | workplans/CSOC-WP-0014-cohort-transfer-bubble-offramp.md | | task | CSOC-WP-0015-T01 | done | — | workplans/CSOC-WP-0015-communication-runtime-acceptance.md | -| task | CSOC-WP-0015-T02 | todo | — | workplans/CSOC-WP-0015-communication-runtime-acceptance.md | +| task | CSOC-WP-0015-T02 | progress | — | workplans/CSOC-WP-0015-communication-runtime-acceptance.md | | task | CSOC-WP-0015-T03 | done | — | workplans/CSOC-WP-0015-communication-runtime-acceptance.md | | task | CSOC-WP-0015-T04 | done | — | workplans/CSOC-WP-0015-communication-runtime-acceptance.md | | task | CSOC-WP-0015-T05 | todo | — | workplans/CSOC-WP-0015-communication-runtime-acceptance.md | diff --git a/docs/release/communication-mail-dns-2026-10-31.json b/docs/release/communication-mail-dns-2026-10-31.json new file mode 100644 index 0000000..bc4b5ea --- /dev/null +++ b/docs/release/communication-mail-dns-2026-10-31.json @@ -0,0 +1,178 @@ +{ + "schema": "coulomb.communication-mail-dns-observation.v1", + "observed_at": "2026-10-31T10:22:14.208342+00:00", + "selector_source": "https://www.ionos.com/help/domains/configuring-mail-servers-and-other-related-records/email-authentication-with-dkim/", + "queries": [ + { + "server": "ns1082.ui-dns.com", + "name": "coulomb.social", + "type": "MX", + "returncode": 0, + "response": ";; Got answer:\n;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 9022\n;; flags: qr aa; QUERY: 1, ANSWER: 2, AUTHORITY: 0, ADDITIONAL: 1\n\n;; OPT PSEUDOSECTION:\n; EDNS: version: 0, flags:; udp: 1220\n;; ANSWER SECTION:\ncoulomb.social.\t\t3600\tIN\tMX\t10 mx01.ionos.de.\ncoulomb.social.\t\t3600\tIN\tMX\t10 mx00.ionos.de.\n\n" + }, + { + "server": "ns1082.ui-dns.com", + "name": "coulomb.social", + "type": "TXT", + "returncode": 0, + "response": ";; Got answer:\n;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 59826\n;; flags: qr aa; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1\n\n;; OPT PSEUDOSECTION:\n; EDNS: version: 0, flags:; udp: 1220\n;; ANSWER SECTION:\ncoulomb.social.\t\t3600\tIN\tTXT\t\"v=spf1 include:_spf-eu.ionos.com ~all\"\n\n" + }, + { + "server": "ns1082.ui-dns.com", + "name": "_dmarc.coulomb.social", + "type": "TXT", + "returncode": 0, + "response": ";; Got answer:\n;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 30510\n;; flags: qr aa; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1\n\n;; OPT PSEUDOSECTION:\n; EDNS: version: 0, flags:; udp: 1220\n;; ANSWER SECTION:\n_dmarc.coulomb.social.\t3600\tIN\tTXT\t\"v=DMARC1; p=none\"\n\n" + }, + { + "server": "ns1082.ui-dns.com", + "name": "s1-ionos._domainkey.coulomb.social", + "type": "CNAME", + "returncode": 0, + "response": ";; Got answer:\n;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 38847\n;; flags: qr aa; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1\n\n;; OPT PSEUDOSECTION:\n; EDNS: version: 0, flags:; udp: 1220\n;; ANSWER SECTION:\ns1-ionos._domainkey.coulomb.social. 3600 IN CNAME s1.dkim.ionos.com.\n\n" + }, + { + "server": "ns1082.ui-dns.com", + "name": "s2-ionos._domainkey.coulomb.social", + "type": "CNAME", + "returncode": 0, + "response": ";; Got answer:\n;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 38467\n;; flags: qr aa; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1\n\n;; OPT PSEUDOSECTION:\n; EDNS: version: 0, flags:; udp: 1220\n;; ANSWER SECTION:\ns2-ionos._domainkey.coulomb.social. 3600 IN CNAME s2.dkim.ionos.com.\n\n" + }, + { + "server": "ns1082.ui-dns.com", + "name": "s42582890._domainkey.coulomb.social", + "type": "CNAME", + "returncode": 0, + "response": ";; Got answer:\n;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 38456\n;; flags: qr aa; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1\n\n;; OPT PSEUDOSECTION:\n; EDNS: version: 0, flags:; udp: 1220\n;; ANSWER SECTION:\ns42582890._domainkey.coulomb.social. 3600 IN CNAME s42582890.dkim.ionos.com.\n\n" + }, + { + "server": "ns1121.ui-dns.de", + "name": "coulomb.social", + "type": "MX", + "returncode": 0, + "response": ";; Got answer:\n;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 23805\n;; flags: qr aa; QUERY: 1, ANSWER: 2, AUTHORITY: 0, ADDITIONAL: 1\n\n;; OPT PSEUDOSECTION:\n; EDNS: version: 0, flags:; udp: 1220\n;; ANSWER SECTION:\ncoulomb.social.\t\t3600\tIN\tMX\t10 mx00.ionos.de.\ncoulomb.social.\t\t3600\tIN\tMX\t10 mx01.ionos.de.\n\n" + }, + { + "server": "ns1121.ui-dns.de", + "name": "coulomb.social", + "type": "TXT", + "returncode": 0, + "response": ";; Got answer:\n;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 29488\n;; flags: qr aa; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1\n\n;; OPT PSEUDOSECTION:\n; EDNS: version: 0, flags:; udp: 1220\n;; ANSWER SECTION:\ncoulomb.social.\t\t3600\tIN\tTXT\t\"v=spf1 include:_spf-eu.ionos.com ~all\"\n\n" + }, + { + "server": "ns1121.ui-dns.de", + "name": "_dmarc.coulomb.social", + "type": "TXT", + "returncode": 0, + "response": ";; Got answer:\n;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 38938\n;; flags: qr aa; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1\n\n;; OPT PSEUDOSECTION:\n; EDNS: version: 0, flags:; udp: 1220\n;; ANSWER SECTION:\n_dmarc.coulomb.social.\t3600\tIN\tTXT\t\"v=DMARC1; p=none\"\n\n" + }, + { + "server": "ns1121.ui-dns.de", + "name": "s1-ionos._domainkey.coulomb.social", + "type": "CNAME", + "returncode": 0, + "response": ";; Got answer:\n;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 61796\n;; flags: qr aa; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1\n\n;; OPT PSEUDOSECTION:\n; EDNS: version: 0, flags:; udp: 1220\n;; ANSWER SECTION:\ns1-ionos._domainkey.coulomb.social. 3600 IN CNAME s1.dkim.ionos.com.\n\n" + }, + { + "server": "ns1121.ui-dns.de", + "name": "s2-ionos._domainkey.coulomb.social", + "type": "CNAME", + "returncode": 0, + "response": ";; Got answer:\n;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 29455\n;; flags: qr aa; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1\n\n;; OPT PSEUDOSECTION:\n; EDNS: version: 0, flags:; udp: 1220\n;; ANSWER SECTION:\ns2-ionos._domainkey.coulomb.social. 3600 IN CNAME s2.dkim.ionos.com.\n\n" + }, + { + "server": "ns1121.ui-dns.de", + "name": "s42582890._domainkey.coulomb.social", + "type": "CNAME", + "returncode": 0, + "response": ";; Got answer:\n;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 43275\n;; flags: qr aa; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1\n\n;; OPT PSEUDOSECTION:\n; EDNS: version: 0, flags:; udp: 1220\n;; ANSWER SECTION:\ns42582890._domainkey.coulomb.social. 3600 IN CNAME s42582890.dkim.ionos.com.\n\n" + }, + { + "server": "ns1047.ui-dns.biz", + "name": "coulomb.social", + "type": "MX", + "returncode": 0, + "response": ";; Got answer:\n;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 51444\n;; flags: qr aa; QUERY: 1, ANSWER: 2, AUTHORITY: 0, ADDITIONAL: 1\n\n;; OPT PSEUDOSECTION:\n; EDNS: version: 0, flags:; udp: 1220\n;; ANSWER SECTION:\ncoulomb.social.\t\t3600\tIN\tMX\t10 mx01.ionos.de.\ncoulomb.social.\t\t3600\tIN\tMX\t10 mx00.ionos.de.\n\n" + }, + { + "server": "ns1047.ui-dns.biz", + "name": "coulomb.social", + "type": "TXT", + "returncode": 0, + "response": ";; Got answer:\n;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 63384\n;; flags: qr aa; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1\n\n;; OPT PSEUDOSECTION:\n; EDNS: version: 0, flags:; udp: 1220\n;; ANSWER SECTION:\ncoulomb.social.\t\t3600\tIN\tTXT\t\"v=spf1 include:_spf-eu.ionos.com ~all\"\n\n" + }, + { + "server": "ns1047.ui-dns.biz", + "name": "_dmarc.coulomb.social", + "type": "TXT", + "returncode": 0, + "response": ";; Got answer:\n;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 42564\n;; flags: qr aa; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1\n\n;; OPT PSEUDOSECTION:\n; EDNS: version: 0, flags:; udp: 1220\n;; ANSWER SECTION:\n_dmarc.coulomb.social.\t3600\tIN\tTXT\t\"v=DMARC1; p=none\"\n\n" + }, + { + "server": "ns1047.ui-dns.biz", + "name": "s1-ionos._domainkey.coulomb.social", + "type": "CNAME", + "returncode": 0, + "response": ";; Got answer:\n;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 29324\n;; flags: qr aa; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1\n\n;; OPT PSEUDOSECTION:\n; EDNS: version: 0, flags:; udp: 1220\n;; ANSWER SECTION:\ns1-ionos._domainkey.coulomb.social. 3600 IN CNAME s1.dkim.ionos.com.\n\n" + }, + { + "server": "ns1047.ui-dns.biz", + "name": "s2-ionos._domainkey.coulomb.social", + "type": "CNAME", + "returncode": 0, + "response": ";; Got answer:\n;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 28199\n;; flags: qr aa; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1\n\n;; OPT PSEUDOSECTION:\n; EDNS: version: 0, flags:; udp: 1220\n;; ANSWER SECTION:\ns2-ionos._domainkey.coulomb.social. 3600 IN CNAME s2.dkim.ionos.com.\n\n" + }, + { + "server": "ns1047.ui-dns.biz", + "name": "s42582890._domainkey.coulomb.social", + "type": "CNAME", + "returncode": 0, + "response": ";; Got answer:\n;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 5757\n;; flags: qr aa; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1\n\n;; OPT PSEUDOSECTION:\n; EDNS: version: 0, flags:; udp: 1220\n;; ANSWER SECTION:\ns42582890._domainkey.coulomb.social. 3600 IN CNAME s42582890.dkim.ionos.com.\n\n" + }, + { + "server": "ns1045.ui-dns.org", + "name": "coulomb.social", + "type": "MX", + "returncode": 0, + "response": ";; Got answer:\n;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 60470\n;; flags: qr aa; QUERY: 1, ANSWER: 2, AUTHORITY: 0, ADDITIONAL: 1\n\n;; OPT PSEUDOSECTION:\n; EDNS: version: 0, flags:; udp: 1220\n;; ANSWER SECTION:\ncoulomb.social.\t\t3600\tIN\tMX\t10 mx01.ionos.de.\ncoulomb.social.\t\t3600\tIN\tMX\t10 mx00.ionos.de.\n\n" + }, + { + "server": "ns1045.ui-dns.org", + "name": "coulomb.social", + "type": "TXT", + "returncode": 0, + "response": ";; Got answer:\n;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 60103\n;; flags: qr aa; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1\n\n;; OPT PSEUDOSECTION:\n; EDNS: version: 0, flags:; udp: 1220\n;; ANSWER SECTION:\ncoulomb.social.\t\t3600\tIN\tTXT\t\"v=spf1 include:_spf-eu.ionos.com ~all\"\n\n" + }, + { + "server": "ns1045.ui-dns.org", + "name": "_dmarc.coulomb.social", + "type": "TXT", + "returncode": 0, + "response": ";; Got answer:\n;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 6244\n;; flags: qr aa; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1\n\n;; OPT PSEUDOSECTION:\n; EDNS: version: 0, flags:; udp: 1220\n;; ANSWER SECTION:\n_dmarc.coulomb.social.\t3600\tIN\tTXT\t\"v=DMARC1; p=none\"\n\n" + }, + { + "server": "ns1045.ui-dns.org", + "name": "s1-ionos._domainkey.coulomb.social", + "type": "CNAME", + "returncode": 0, + "response": ";; Got answer:\n;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 19973\n;; flags: qr aa; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1\n\n;; OPT PSEUDOSECTION:\n; EDNS: version: 0, flags:; udp: 1220\n;; ANSWER SECTION:\ns1-ionos._domainkey.coulomb.social. 3600 IN CNAME s1.dkim.ionos.com.\n\n" + }, + { + "server": "ns1045.ui-dns.org", + "name": "s2-ionos._domainkey.coulomb.social", + "type": "CNAME", + "returncode": 0, + "response": ";; Got answer:\n;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 13871\n;; flags: qr aa; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1\n\n;; OPT PSEUDOSECTION:\n; EDNS: version: 0, flags:; udp: 1220\n;; ANSWER SECTION:\ns2-ionos._domainkey.coulomb.social. 3600 IN CNAME s2.dkim.ionos.com.\n\n" + }, + { + "server": "ns1045.ui-dns.org", + "name": "s42582890._domainkey.coulomb.social", + "type": "CNAME", + "returncode": 0, + "response": ";; Got answer:\n;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 5332\n;; flags: qr aa; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1\n\n;; OPT PSEUDOSECTION:\n; EDNS: version: 0, flags:; udp: 1220\n;; ANSWER SECTION:\ns42582890._domainkey.coulomb.social. 3600 IN CNAME s42582890.dkim.ionos.com.\n\n" + } + ], + "mail_sent": false, + "message_authentication_verified": false, + "mailbox_accessed": false +} diff --git a/docs/release/communication-mail-native-2026-10-31.json b/docs/release/communication-mail-native-2026-10-31.json new file mode 100644 index 0000000..84fd99f --- /dev/null +++ b/docs/release/communication-mail-native-2026-10-31.json @@ -0,0 +1,230 @@ +{ + "schema": "coulomb.communication-mail-native-observation.v1", + "observed_at": "2026-10-31T10:21:51.260753+00:00", + "namespace": { + "name": "email-connect", + "namespace": null, + "uid": "5678c5d7-e4f5-4a62-8f24-1c6e1d906319", + "generation": null + }, + "deployment": { + "name": "email-connect", + "namespace": "email-connect", + "uid": "2e98e52a-a2d1-4c52-9ac8-6efbc0a18e72", + "generation": 74, + "desired_replicas": 1, + "ready_replicas": 1, + "observed_generation": 74, + "containers": [ + { + "name": "email-connect", + "image": "forgejo.coulomb.social/coulomb/email-connect@sha256:5d79fd1fec15d3a279383820f170d263de3a55f5308df01a0a34732ebaea6d3d", + "envFrom": [ + { + "configMapRef": { + "name": "email-connect-config" + } + }, + { + "secretRef": { + "name": "email-connect-runtime", + "optional": false + } + } + ], + "public_env_overrides": {} + } + ] + }, + "configmap": { + "name": "email-connect-config", + "namespace": "email-connect", + "uid": "558d3338-c4c2-490c-a5e5-af206c39eb51", + "generation": null, + "public_settings": { + "EMAIL_CONNECT_SENDER": "forgejo@coulomb.social", + "EMAIL_CONNECT_SMTP_HOST": "smtp.ionos.de", + "EMAIL_CONNECT_SMTP_PORT": "587", + "EMAIL_CONNECT_SMTP_SECURITY": null, + "EMAIL_CONNECT_PORTAL_URL": "https://users.coulomb.social" + } + }, + "smtp_projection": { + "name": "email-connect-runtime", + "namespace": "email-connect", + "uid": "fb6ab8d5-9f26-4f41-b1d9-b2ea596c7534", + "generation": 2, + "store": { + "kind": "ClusterSecretStore", + "name": "openbao-email-connect" + }, + "remote_keys": [ + { + "key": "platform/workloads/email-connect/transactional", + "property": "EMAIL_CONNECT_INGEST_TOKEN", + "secretKey": "EMAIL_CONNECT_INGEST_TOKEN" + }, + { + "key": "platform/workloads/email-connect/transactional", + "property": "EMAIL_CONNECT_SMTP_USERNAME", + "secretKey": "EMAIL_CONNECT_SMTP_USERNAME" + }, + { + "key": "platform/workloads/email-connect/transactional", + "property": "EMAIL_CONNECT_SMTP_PASSWORD", + "secretKey": "EMAIL_CONNECT_SMTP_PASSWORD" + } + ], + "conditions": [ + { + "type": "Ready", + "status": "True", + "reason": "SecretSynced" + } + ] + }, + "pods": [ + { + "name": "email-connect-66cf8b84dd-24qkr", + "namespace": "email-connect", + "uid": "8f6a2a7b-ed0b-4ec0-9ffc-9f5bbaed6f0b", + "generation": 1, + "phase": "Running", + "containers": [ + { + "name": "email-connect", + "ready": true, + "imageID": "forgejo.coulomb.social/coulomb/email-connect@sha256:5d79fd1fec15d3a279383820f170d263de3a55f5308df01a0a34732ebaea6d3d", + "restartCount": 0 + } + ] + } + ], + "network_policies": [ + { + "name": "email-connect-default-deny", + "spec": { + "podSelector": {}, + "policyTypes": [ + "Ingress", + "Egress" + ] + } + }, + { + "name": "email-connect-egress", + "spec": { + "egress": [ + { + "ports": [ + { + "port": 53, + "protocol": "UDP" + }, + { + "port": 53, + "protocol": "TCP" + } + ], + "to": [ + { + "namespaceSelector": { + "matchLabels": { + "kubernetes.io/metadata.name": "kube-system" + } + } + } + ] + }, + { + "ports": [ + { + "port": 587, + "protocol": "TCP" + } + ], + "to": [ + { + "ipBlock": { + "cidr": "0.0.0.0/0", + "except": [ + "10.0.0.0/8", + "172.16.0.0/12", + "192.168.0.0/16" + ] + } + } + ] + } + ], + "podSelector": { + "matchLabels": { + "app.kubernetes.io/name": "email-connect" + } + }, + "policyTypes": [ + "Egress" + ] + } + }, + { + "name": "email-connect-user-engine-ingress", + "spec": { + "ingress": [ + { + "from": [ + { + "namespaceSelector": { + "matchLabels": { + "kubernetes.io/metadata.name": "user-engine" + } + }, + "podSelector": { + "matchLabels": { + "app.kubernetes.io/name": "user-engine" + } + } + } + ], + "ports": [ + { + "port": 8080, + "protocol": "TCP" + } + ] + } + ], + "podSelector": { + "matchLabels": { + "app.kubernetes.io/name": "email-connect" + } + }, + "policyTypes": [ + "Ingress" + ] + } + } + ], + "cronjobs": [], + "secrets_read": false, + "mail_sent": false, + "mailbox_accessed": false, + "production_data_read": false, + "running_process_configuration": { + "public_environment": { + "EMAIL_CONNECT_SENDER": "forgejo@coulomb.social", + "EMAIL_CONNECT_SMTP_HOST": "smtp.ionos.de", + "EMAIL_CONNECT_SMTP_PORT": "587", + "EMAIL_CONNECT_SMTP_SECURITY": null, + "EMAIL_CONNECT_PORTAL_URL": "https://users.coulomb.social" + }, + "transactional_source_sha256": "36fdfd8cb58f10148cd491a7e34d7aea798e4289fce760af70a48776bede204d" + }, + "additional_packaged_sources_sha256": { + "callers": "06fe961b9030dba2c0b26b92f8097e8fbd24e205d5fa79642aa9af60382886dc", + "scanner": "6266cbad2dbc610c1f6df1c051ded2ee55b43d7ff30af6eab38721924931c1ed", + "storage": "57fbd6d3b2e7541c0bf52d87a4f7361a4ff4a03511620d2a1c3789632b73afeb", + "parser": "786b291256ddfac5a8622c743622508cdd9f45f06156d0f5487ce399fad1287c" + }, + "additional_packaged_sources_match_checkout": true +} diff --git a/docs/release/mail-return-boundary-rehearsal-2026-10-31.json b/docs/release/mail-return-boundary-rehearsal-2026-10-31.json new file mode 100644 index 0000000..2f19176 --- /dev/null +++ b/docs/release/mail-return-boundary-rehearsal-2026-10-31.json @@ -0,0 +1,37 @@ +{ + "schema": "coulomb.mail-return-boundary-rehearsal.v1", + "completed_at": "2026-10-31T10:25:09.945402+00:00", + "source_sha256": { + "src/email_connect/__init__.py": "0f1ae5e9251f80b0bd5220b1d7bf91735d6ea273e07aa26cb7852a7950be7087", + "src/email_connect/adapter_contract.py": "5059b2eaaac2408f4e247bb468a97960eb3f823e63c3eeb151a1c2250e1f3bbf", + "src/email_connect/callers.py": "06fe961b9030dba2c0b26b92f8097e8fbd24e205d5fa79642aa9af60382886dc", + "src/email_connect/cli.py": "6ac6ae8acd091ba2d2102589be3b75fe3a48392309e40e61f8383deb3329ad42", + "src/email_connect/config.py": "6ca89fb7c3e1f35bc4faedfba8c3313d4366b2cb7830ef88ed3f7d2313f7867e", + "src/email_connect/evidence.py": "42cca03234a79dc78033ec1fbaf0b1785e6089ef3f3128e2e9d666ed239a3582", + "src/email_connect/mailbox.py": "7f0fe7e19ce1f814269609c4236152042f0f49bf3b517114b3b3f6f482471362", + "src/email_connect/models.py": "7f8136646b663a097280286dccce76a90abb7190ea0712fff7da32d47526521c", + "src/email_connect/parser.py": "786b291256ddfac5a8622c743622508cdd9f45f06156d0f5487ce399fad1287c", + "src/email_connect/recipients.py": "68792f44f0d2ba204649542aa8bd6b0f209d89c675baa4fc0767f1250a29045f", + "src/email_connect/reporting.py": "8f8ce5f98e4347b778893d08a9e58f2bab3b44b80c159fcf52d0b5772429737c", + "src/email_connect/scanner.py": "6266cbad2dbc610c1f6df1c051ded2ee55b43d7ff30af6eab38721924931c1ed", + "src/email_connect/storage.py": "57fbd6d3b2e7541c0bf52d87a4f7361a4ff4a03511620d2a1c3789632b73afeb", + "src/email_connect/transactional.py": "36fdfd8cb58f10148cd491a7e34d7aea798e4289fce760af70a48776bede204d" + }, + "probe_sha256": "90dec8a764015cf7fb077f45744b27d39fcb77990a5a16185a2ec3581c54fb0c", + "transactional_source_matches_native": true, + "smtp_transport": "in-process synthetic replacement; no SMTP connection", + "message_id_correlates_with_ledger_and_synthetic_bounce": true, + "explicit_reply_to_or_return_path_headers": false, + "scanner_deduplicates_evidence": true, + "scanner_updates_transactional_suppression": false, + "ordinary_sender_suppress_and_unsuppress_denied": true, + "synthetic_operator_suppression_blocks_send_and_survives_restart": true, + "suppression_scope": "recipient-global across callers and tenants", + "explicit_synthetic_operator_unsuppression_verified": true, + "native_operator_admission_proved": false, + "received_mail_authenticity_or_correlation_authority_proved": false, + "python_network_operations_blocked": true, + "temporary_ledgers_removed": true, + "mail_sent": false, + "production_mailbox_accessed": false +} diff --git a/docs/release/platform-mail-ownership-2026-10-31.json b/docs/release/platform-mail-ownership-2026-10-31.json new file mode 100644 index 0000000..96cd263 --- /dev/null +++ b/docs/release/platform-mail-ownership-2026-10-31.json @@ -0,0 +1,18 @@ +{ + "workplan_id": "38208f72-c285-56fa-be44-de9b59a4e031", + "id": "9392ef52-3bfe-42fd-b6d5-ec0730656775", + "topic_id": null, + "title": "Platform-root owns platform mailbox replies and bounces", + "description": "For platform@coulomb.social human replies and bounces, including registration and invitation mail, the user selected platform-root with Bernd Worsch as the human operator. This settles accountable support/return-mail ownership under CSOC-WP-0015-T02.", + "decision_type": "pending", + "status": "resolved", + "rationale": "Explicit user response in this session: platform-root, with me as the human operator (recommended). Operational mailbox access, email-connect platform SMTP admission, correlated handling, retention and delivery proof remain separate unfinished implementation/acceptance requirements. No forwarding rule or automatic mailbox scan was requested.", + "decided_by": "Bernd Worsch (explicit user response)", + "decided_at": "2026-10-31T10:24:03.783410Z", + "deadline": null, + "escalation_note": null, + "superseded_by": null, + "created_at": "2026-10-31T10:24:06.946680Z", + "updated_at": "2026-10-31T10:24:06.946680Z", + "workstream_id": "38208f72-c285-56fa-be44-de9b59a4e031" +} diff --git a/docs/release/platform-mail-sender-2026-10-31/candidate.json b/docs/release/platform-mail-sender-2026-10-31/candidate.json new file mode 100644 index 0000000..ae38e58 --- /dev/null +++ b/docs/release/platform-mail-sender-2026-10-31/candidate.json @@ -0,0 +1,231 @@ +{ + "schema": "coulomb.platform-mail-sender-candidate.v1", + "observed_at": "2026-10-31T10:28:30.447584+00:00", + "deployment_uid": "2e98e52a-a2d1-4c52-9ac8-6efbc0a18e72", + "deployment_generation": 74, + "image": "forgejo.coulomb.social/coulomb/email-connect@sha256:5d79fd1fec15d3a279383820f170d263de3a55f5308df01a0a34732ebaea6d3d", + "new_resources": [ + { + "apiVersion": "v1", + "kind": "ServiceAccount", + "metadata": { + "name": "email-connect-platform-smtp-eso", + "namespace": "email-connect" + }, + "automountServiceAccountToken": false + }, + { + "apiVersion": "external-secrets.io/v1", + "kind": "SecretStore", + "metadata": { + "name": "openbao-email-connect-platform-smtp", + "namespace": "email-connect" + }, + "spec": { + "provider": { + "vault": { + "server": "http://openbao.openbao.svc:8200", + "path": "platform", + "version": "v2", + "auth": { + "kubernetes": { + "mountPath": "kubernetes", + "role": "email-connect-platform-smtp-eso", + "serviceAccountRef": { + "name": "email-connect-platform-smtp-eso", + "audiences": [ + "openbao" + ] + } + } + } + } + } + } + }, + { + "apiVersion": "external-secrets.io/v1", + "kind": "ExternalSecret", + "metadata": { + "name": "email-connect-platform-smtp", + "namespace": "email-connect" + }, + "spec": { + "refreshInterval": "1h", + "secretStoreRef": { + "kind": "SecretStore", + "name": "openbao-email-connect-platform-smtp" + }, + "target": { + "name": "email-connect-platform-smtp", + "creationPolicy": "Owner", + "deletionPolicy": "Retain", + "template": { + "type": "Opaque" + } + }, + "data": [ + { + "secretKey": "password", + "remoteRef": { + "key": "workloads/railiance-telemetry/smtp", + "property": "SMTP_PASSWORD" + } + } + ] + } + } + ], + "deployment_json_patch": [ + { + "op": "test", + "path": "/metadata/uid", + "value": "2e98e52a-a2d1-4c52-9ac8-6efbc0a18e72" + }, + { + "op": "test", + "path": "/metadata/resourceVersion", + "value": "83014220" + }, + { + "op": "test", + "path": "/spec/template/spec/containers/0/image", + "value": "forgejo.coulomb.social/coulomb/email-connect@sha256:5d79fd1fec15d3a279383820f170d263de3a55f5308df01a0a34732ebaea6d3d" + }, + { + "op": "test", + "path": "/spec/template/spec/containers/0/envFrom", + "value": [ + { + "configMapRef": { + "name": "email-connect-config" + } + }, + { + "secretRef": { + "name": "email-connect-runtime", + "optional": false + } + } + ] + }, + { + "op": "test", + "path": "/spec/template/spec/containers/0/env", + "value": [ + { + "name": "EMAIL_CONNECT_CALLERS_FILE", + "value": "/etc/email-connect-callers/callers.json" + } + ] + }, + { + "op": "replace", + "path": "/spec/template/spec/containers/0/envFrom", + "value": [ + { + "configMapRef": { + "name": "email-connect-config" + } + } + ] + }, + { + "op": "replace", + "path": "/spec/template/spec/containers/0/env", + "value": [ + { + "name": "EMAIL_CONNECT_CALLERS_FILE", + "value": "/etc/email-connect-callers/callers.json" + }, + { + "name": "EMAIL_CONNECT_SMTP_USERNAME", + "value": "platform@coulomb.social" + }, + { + "name": "EMAIL_CONNECT_SMTP_PASSWORD", + "valueFrom": { + "secretKeyRef": { + "name": "email-connect-platform-smtp", + "key": "password", + "optional": false + } + } + } + ] + } + ], + "configmap_json_patch": [ + { + "op": "test", + "path": "/metadata/uid", + "value": "558d3338-c4c2-490c-a5e5-af206c39eb51" + }, + { + "op": "test", + "path": "/data/EMAIL_CONNECT_SENDER", + "value": "forgejo@coulomb.social" + }, + { + "op": "replace", + "path": "/data/EMAIL_CONNECT_SENDER", + "value": "platform@coulomb.social" + } + ], + "native_server_dry_run": [ + { + "kind": "ServiceAccount", + "name": "email-connect-platform-smtp-eso", + "accepted": true + }, + { + "kind": "SecretStore", + "name": "openbao-email-connect-platform-smtp", + "accepted": true + }, + { + "kind": "ExternalSecret", + "name": "email-connect-platform-smtp", + "accepted": true + }, + { + "kind": "Deployment", + "name": "email-connect", + "accepted": true, + "only_envFrom_and_env_changed": true + }, + { + "kind": "ConfigMap", + "name": "email-connect-config", + "accepted": true, + "only_sender_changed": true + } + ], + "live_caller_policy": { + "name": "email-connect-callers-user-engine-276257849bf2", + "sha256": "276257849bf260d844e8e0767deda7edd2f3d807f12794b869f871b75b255a07", + "callers": [ + { + "caller_id": "user-engine", + "operations": [ + "send", + "delivery-status", + "status", + "verify", + "consume", + "cancel" + ], + "templates": [ + "family_member.invited", + "family_invitation.resent" + ] + } + ] + }, + "platform_mailbox_admission_executed": false, + "resources_installed": false, + "mail_sent": false, + "secrets_read": false, + "execution_ready": false, + "preparer_sha256": "c7d8bbda1b2807b50d46ee04703cb1d0b465c514f578185a041d7b34e59a95d5" +} diff --git a/docs/release/platform-mail-sender-review-2026-10-31.md b/docs/release/platform-mail-sender-review-2026-10-31.md new file mode 100644 index 0000000..6ab3b7d --- /dev/null +++ b/docs/release/platform-mail-sender-review-2026-10-31.md @@ -0,0 +1,131 @@ +# Platform sender and return-mail operations + +WP-0015-T02 is in progress. **Ownership is settled; sender migration and monitored +return-mail operation are not yet deployed or accepted.** Decision +9392ef52-3bfe-42fd-b6d5-ec0730656775 records the user's choice: platform-root is +accountable for replies and bounces at platform@coulomb.social, with Bernd Worsch +as the human operator, including registration and invitation mail. + +This decision does not create a forwarding rule to Gmail. The existing Gmail +address remains the separately accepted backup-alert destination. No mailbox +credentials, message bodies or live recipient records were read during this work. + +## Current evidence + +| Requirement | Verified evidence | Remaining return | +| --- | --- | --- | +| Sender | Native generation74, one Ready email-connect replica, image5d79fd1f; ConfigMap and container environment both use forgejo@coulomb.social | Admit and deploy the dedicated platform sender below | +| Credential custody | Existing email-connect runtime projection is Ready; platform mailbox is separately admitted for telemetry and Authelia | Admit email-connect as an additional scoped consumer through railiance-platform; existing consumers' grants are not transferable | +| Support/return owner | User explicitly selected platform-root and Bernd; decision9392ef52 | Prove that this operator can access the intended inbox and handle selected returns | +| Public DNS | All four authoritative servers agree on two IONOS MX records, IONOS SPF include, DMARC p=none, and three documented DKIM CNAME records | Inspect recipient authentication results and actual envelope/signature alignment for a specifically authorized new-sender message; DNS presence alone does not prove message authentication | +| Return correlation | Synthetic accepted Message-ID is retained in the delivery ledger and recovered from a synthetic hard bounce by the actual scanner | Native selected-message correlation, authoritative recipient/caller binding and operator handling | +| Suppression | Existing scoped API denies ordinary senders; synthetic operator suppression blocks later sends and survives restart | No operator caller is currently admitted. Suppression is global by recipient across callers/tenants; that effect needs explicit operator-policy admission | +| Retention | Source has no automatic evidence-retention/purge operation connecting the scanner and transactional ledger | Agree mailbox/evidence retention and controlled expiry while preserving pending sends, challenges and suppression decisions | + +Native observation: `communication-mail-native-2026-10-31.json`. The packaged +transactional, caller, scanner, parser and storage source hashes match the owner +checkout. The separately captured live policy grants user-engine send/status/ +verification/consume/cancel only; it grants no suppress/unsuppress operation. +No CronJob exists in the email-connect namespace; this observation does not rule +out a manual or external operator elsewhere. + +DNS evidence: `communication-mail-dns-2026-10-31.json`, 24 authoritative queries. +The DKIM selector names and CNAME targets were checked against +[IONOS's own documentation](https://www.ionos.com/help/domains/configuring-mail-servers-and-other-related-records/email-authentication-with-dkim/). +The current records are `s1-ionos`, `s2-ionos` and `s42582890`, pointing to their +corresponding `dkim.ionos.com` names. The observed DMARC record is exactly +`v=DMARC1; p=none`; it contains no reporting destination. No DNS change is proposed +by this packet, and no signed message or destination DKIM public-key TXT was +verified here. + +## Concrete sender candidate + +`platform-mail-sender-2026-10-31/candidate.json` contains the exact native +preconditions, three new resources and two guarded JSON patches. All five +operations passed native server dry-run; nothing was installed. + +- Dedicated ServiceAccount `email-connect/email-connect-platform-smtp-eso`, with + automatic token mounting disabled. +- Namespace SecretStore `openbao-email-connect-platform-smtp`, using that exact + service account, the OpenBao audience and dedicated Kubernetes-auth role. +- ExternalSecret `email-connect-platform-smtp`, projecting only `SMTP_PASSWORD` + from existing `platform/workloads/railiance-telemetry/smtp` into key `password`. + No mailbox credential copy or rotation is proposed. +- Change the existing ConfigMap sender to `platform@coulomb.social`. +- Remove the old runtime Secret from Deployment `envFrom`. Set the public SMTP + username to platform@coulomb.social and take the password from the new + projection explicitly. Preserve the current caller-policy and caller-credential + mounts; no legacy shared ingest fallback is reintroduced. + +The whole Deployment spec comparison permits only those environment changes; +image, probes, resources, PVC, service account and volume mounts remain identical. +The ConfigMap comparison permits only the sender change. Existing legacy SMTP +custody is retained outside the pod for controlled rollback; it is not deleted. +Existing SMTP egress and fixed invitation/verification templates remain unchanged. + +The owner must admit the exact role/policy with a read-only mailbox path, +OpenBao audience, maximum15-minute token and no default policy, with positive +consumer/negative other-consumer and coding-agent boundaries. Reuse the fixed +owner lane demonstrated in RPF-WP-0052 and +`railiance-platform/scripts/authelia_smtp_delivery.py`; its historical approval +applies to Authelia, not this additional consumer. SMTP AUTH proof must not submit +mail. The application sender migration needs a separate supervised configuration +executor and independent postflight after the scoped projection is Ready. + +This candidate is **not execution-ready**: no owner admission helper/receipt or +supervised rollout packet exists for this new consumer yet. Resource-version and +identity tests must be refreshed and re-reviewed at execution time. Do not apply +these individual patches by hand as a substitute for that packet. + +Rollback must reconcile any in-flight sends, restore the prior sender and exact +credential references, and verify Ready without replaying uncertain submissions. +Keep both prior and new custody evidence until that outcome is established. No +ledger restore, mailbox rotation or deletion of the existing projections belongs +to this sender change. + +Reproduce preparation with a new output path: + +```sh +python3 scripts/prepare-platform-mail-sender.py --output /tmp/new-platform-mail-candidate.json +``` + +This script performs reads and `--dry-run=server` operations only, refuses unknown +environment fields and preserves existing output. Its resulting packet records +the preparer hash. Dry-run does not prove that the new OpenBao role exists or that +the projected password is usable. + +## Return-mail boundary rehearsal and operating design + +Run the retained offline probe: + +```sh +python3 scripts/rehearse-mail-return-boundary.py \ + --owner-repo /home/worsch/email-connect \ + --native-evidence docs/release/communication-mail-native-2026-10-31.json +``` + +It uses temporary ledgers, synthetic identities and an in-process SMTP replacement; +Python connection/bind/DNS/sendto operations are denied. It exercises real owner +sources and removes its fixtures afterward. Proof: +`mail-return-boundary-rehearsal-2026-10-31.json`. + +The scanner classifies and deduplicates a hard bounce, preserving Original-Message-ID, +but leaves transactional suppression unchanged. A synthetic operator policy can +suppress and explicitly unsuppress; an ordinary sender receives403. Suppression +also blocks a different synthetic caller/tenant using the same recipient. This +is a capability/boundary proof, not a native operator grant or an automatic return +processor. The SMTP source emits Message-ID and From but no explicit Reply-To or +Return-Path header; actual return delivery remains provider/recipient evidence. + +For the initial operating path, use an attended, explicitly selected return rather +than a blanket mailbox scan. Correlate the accepted Message-ID and authoritative +outbound recipient/caller record before considering suppression. A parsed body or +matching text can be forged and is not sufficient authority. Unknown/unmatched +returns, temporary deferrals and automatic replies remain review items; they must +not silently acquire permanent suppression. No automatic replies are introduced. + +The next implementation must provide a bounded, auditable operator action, define +its global-recipient effect and release policy, and agree retention. It must retain +proof of why suppression changed and refuse unrelated/uncertain records. Productive +inbox access, selected-message authentication/correlation and the operator response +remain acceptance gates alongside the sender rollout. diff --git a/scripts/prepare-platform-mail-sender.py b/scripts/prepare-platform-mail-sender.py new file mode 100644 index 0000000..e634f93 --- /dev/null +++ b/scripts/prepare-platform-mail-sender.py @@ -0,0 +1,66 @@ +#!/usr/bin/env python3 +"""Prepare a fixed email-connect sender candidate using reads and server dry-runs. + +No create/apply/patch is executed without --dry-run=server. This does not admit +OpenBao access, install a resource, scan a mailbox or send an email. +""" +import argparse, os, subprocess,shlex,json,hashlib +from pathlib import Path +from datetime import datetime,timezone +parser = argparse.ArgumentParser(description=__doc__) +parser.add_argument('--output', type=Path, required=True) +args = parser.parse_args() +if not __debug__: + raise SystemExit('Assertions must be enabled') +if args.output.exists(): + raise SystemExit('Choose a new candidate output; existing evidence is retained') +code=r''' +import subprocess,json,hashlib +from datetime import datetime,timezone +K=['k3s','kubectl'] +def get(kind,name): + return json.loads(subprocess.check_output(K+['get',kind,name,'-n','email-connect','-o','json'],timeout=20)) +d=get('deployment','email-connect');cm=get('configmap','email-connect-config') +assert d['metadata']['uid']=='2e98e52a-a2d1-4c52-9ac8-6efbc0a18e72' +assert d['status']['observedGeneration']==d['metadata']['generation'] and d['status']['readyReplicas']==1 +c=d['spec']['template']['spec']['containers'][0] +assert c['name']=='email-connect' and len(d['spec']['template']['spec']['containers'])==1 +assert c['image']=='forgejo.coulomb.social/coulomb/email-connect@sha256:5d79fd1fec15d3a279383820f170d263de3a55f5308df01a0a34732ebaea6d3d' +assert c['envFrom']==[{'configMapRef':{'name':'email-connect-config'}},{'secretRef':{'name':'email-connect-runtime','optional':False}}] +assert cm['data']['EMAIL_CONNECT_SENDER']=='forgejo@coulomb.social' +assert all(e['name'] not in {'EMAIL_CONNECT_SMTP_USERNAME','EMAIL_CONNECT_SMTP_PASSWORD','EMAIL_CONNECT_SENDER'} for e in c['env']) +assert c['env']==[{'name':'EMAIL_CONNECT_CALLERS_FILE','value':'/etc/email-connect-callers/callers.json'}], 'unexpected environment; refuse to copy unknown values' +role='email-connect-platform-smtp-eso';store='openbao-email-connect-platform-smtp';secret='email-connect-platform-smtp' +def obj(api,kind,name,**extra):return {'apiVersion':api,'kind':kind,'metadata':{'name':name,'namespace':'email-connect'},**extra} +resources=[obj('v1','ServiceAccount',role,automountServiceAccountToken=False),obj('external-secrets.io/v1','SecretStore',store,spec={'provider':{'vault':{'server':'http://openbao.openbao.svc:8200','path':'platform','version':'v2','auth':{'kubernetes':{'mountPath':'kubernetes','role':role,'serviceAccountRef':{'name':role,'audiences':['openbao']}}}}}}),obj('external-secrets.io/v1','ExternalSecret',secret,spec={'refreshInterval':'1h','secretStoreRef':{'kind':'SecretStore','name':store},'target':{'name':secret,'creationPolicy':'Owner','deletionPolicy':'Retain','template':{'type':'Opaque'}},'data':[{'secretKey':'password','remoteRef':{'key':'workloads/railiance-telemetry/smtp','property':'SMTP_PASSWORD'}}]})] +for x in resources: + out=subprocess.check_output(K+['get',x['kind'],x['metadata']['name'],'-n','email-connect','--ignore-not-found','-o','name'],timeout=20) + assert not out.strip(),'candidate resource already exists' +base='/spec/template/spec/containers/0' +dpatch=[{'op':'test','path':'/metadata/uid','value':d['metadata']['uid']},{'op':'test','path':'/metadata/resourceVersion','value':d['metadata']['resourceVersion']},{'op':'test','path':base+'/image','value':c['image']},{'op':'test','path':base+'/envFrom','value':c['envFrom']},{'op':'test','path':base+'/env','value':c['env']},{'op':'replace','path':base+'/envFrom','value':[{'configMapRef':{'name':'email-connect-config'}}]},{'op':'replace','path':base+'/env','value':c['env']+[{'name':'EMAIL_CONNECT_SMTP_USERNAME','value':'platform@coulomb.social'},{'name':'EMAIL_CONNECT_SMTP_PASSWORD','valueFrom':{'secretKeyRef':{'name':secret,'key':'password','optional':False}}}]}] +cmpatch=[{'op':'test','path':'/metadata/uid','value':cm['metadata']['uid']},{'op':'test','path':'/data/EMAIL_CONNECT_SENDER','value':'forgejo@coulomb.social'},{'op':'replace','path':'/data/EMAIL_CONNECT_SENDER','value':'platform@coulomb.social'}] +def patchdry(kind,name,patch): + return json.loads(subprocess.check_output(K+['patch',kind,name,'-n','email-connect','--type=json','--dry-run=server','-p',json.dumps(patch),'-o','json'],timeout=30)) +checks=[] +for x in resources: + p=subprocess.run(K+['create','--dry-run=server','-f','-','-o','json'],input=json.dumps(x).encode(),capture_output=True,timeout=30) + if p.returncode:raise RuntimeError('resource dry-run failed: '+x['kind']) + y=json.loads(p.stdout);checks.append({'kind':y['kind'],'name':y['metadata']['name'],'accepted':True}) +da=patchdry('deployment','email-connect',dpatch);ca=patchdry('configmap','email-connect-config',cmpatch) +expected=json.loads(json.dumps(d['spec']));expected['template']['spec']['containers'][0]['envFrom']=dpatch[-2]['value'];expected['template']['spec']['containers'][0]['env']=dpatch[-1]['value'] +assert da['spec']==expected,'unexpected deployment spec change' +expectedcm=dict(cm['data'],EMAIL_CONNECT_SENDER='platform@coulomb.social');assert ca['data']==expectedcm +checks.extend([{'kind':'Deployment','name':'email-connect','accepted':True,'only_envFrom_and_env_changed':True},{'kind':'ConfigMap','name':'email-connect-config','accepted':True,'only_sender_changed':True}]) +volumes=d['spec']['template']['spec']['volumes']; policy_name=next(v['configMap']['name'] for v in volumes if v['name']=='caller-policy') +pol=get('configmap',policy_name)['data']['callers.json'];parsed=json.loads(pol) +publiccallers=[{k:x.get(k) for k in ('caller_id','operations','templates')} for x in parsed['callers']] +print(json.dumps({'schema':'coulomb.platform-mail-sender-candidate.v1','observed_at':datetime.now(timezone.utc).isoformat(),'deployment_uid':d['metadata']['uid'],'deployment_generation':d['metadata']['generation'],'image':c['image'],'new_resources':resources,'deployment_json_patch':dpatch,'configmap_json_patch':cmpatch,'native_server_dry_run':checks,'live_caller_policy':{'name':policy_name,'sha256':hashlib.sha256(pol.encode()).hexdigest(),'callers':publiccallers},'platform_mailbox_admission_executed':False,'resources_installed':False,'mail_sent':False,'secrets_read':False,'execution_ready':False})) +''' +r=subprocess.run(['ssh','-o','BatchMode=yes','-o','ConnectTimeout=10','railiance01','sudo -n python3 -c '+shlex.quote(code)],capture_output=True,text=True,timeout=180) +if r.returncode:raise SystemExit('read/dry-run preparation failed: '+r.stderr[-1500:]) +j=json.loads(r.stdout) +j['preparer_sha256']=hashlib.sha256(Path(__file__).read_bytes()).hexdigest() +fd=os.open(args.output,os.O_WRONLY|os.O_CREAT|os.O_EXCL,0o600) +with os.fdopen(fd,'w') as target: + json.dump(j,target,indent=2);target.write('\n');target.flush();os.fsync(target.fileno()) +print(json.dumps({k:j[k] for k in ('observed_at','deployment_generation','native_server_dry_run','live_caller_policy','execution_ready')},indent=2)) diff --git a/scripts/rehearse-mail-return-boundary.py b/scripts/rehearse-mail-return-boundary.py new file mode 100644 index 0000000..6506930 --- /dev/null +++ b/scripts/rehearse-mail-return-boundary.py @@ -0,0 +1,169 @@ +#!/usr/bin/env python3 +"""Exercise owner mail sources using temporary ledgers and synthetic mail only. + +This probe documents present capabilities and missing integration. It is not an +inbound worker, sender deployment, credential admission or suppression operator. +""" +from __future__ import annotations + +import argparse +import hashlib +import io +import json +import sys +import tempfile +from datetime import datetime, timezone +from pathlib import Path +from unittest.mock import patch + + +def main(): + if not __debug__: + raise SystemExit('Assertions must be enabled') + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--owner-repo', type=Path, required=True) + parser.add_argument('--native-evidence', type=Path, required=True) + args = parser.parse_args() + source = args.owner_repo.resolve() / 'src' + native = json.loads(args.native_evidence.read_text()) + module = source / 'email_connect/transactional.py' + assert hashlib.sha256(module.read_bytes()).hexdigest() == native[ + 'running_process_configuration']['transactional_source_sha256'] + # Fail any attempted Python network connection/bind, even if a future source + # change accidentally substitutes a real transport for these fixtures. + def no_network(event, _args): + if event in {'socket.connect', 'socket.bind', 'socket.getaddrinfo', 'socket.sendto'}: + raise RuntimeError('Offline rehearsal forbids network access') + sys.addaudithook(no_network) + sys.path.insert(0, str(source)) + from email_connect.callers import CallerPolicy, ScopedTransactionalApplication + from email_connect.config import (AppConfig, MailboxConfig, ReportsConfig, + ScanConfig, SourceConfig, StorageConfig) + from email_connect.scanner import scan_mailbox + from email_connect.storage import StateStore + from email_connect.transactional import SMTPProvider, SQLiteDeliveryStore + + captured = [] + class SyntheticSMTP: + def __init__(self, *args, **kwargs): + self.tls = False + def __enter__(self): + return self + def __exit__(self, *args): + pass + def starttls(self, context): + assert context.check_hostname + self.tls = True + def login(self, username, password): + assert self.tls and username == 'fixture' and password == 'fixture-only' + def send_message(self, message): + assert message['To'] == 'recipient@example.test' + captured.append(message) + + def policy(name, operations, tenant='tenant:fixture'): + return CallerPolicy(caller_id=name, token='fixture-credential-' + name, + source=name, bindings=frozenset({(tenant, 'client:fixture')}), + operations=frozenset(operations), + templates=frozenset({'family_member.invited'}), + rate_per_minute=60, burst=30, max_payload_bytes=8192) + sender = policy('fixture-sender', {'send', 'delivery-status'}) + operator = policy('fixture-operator', {'suppress', 'unsuppress'}) + other = policy('fixture-other', {'send'}, 'tenant:other') + def event(name, caller=sender, tenant='tenant:fixture'): + return {'id': name, 'source': caller.source, 'type': 'family_member.invited', + 'tenant': tenant, 'client_id': 'client:fixture', + 'data': {'primary_email': 'recipient@example.test', 'invitation_id': name}} + def invoke(app, payload, path='/v1/send', caller=sender): + raw = json.dumps(payload).encode(); result = {} + env = {'PATH_INFO': path, 'REQUEST_METHOD': 'POST', 'CONTENT_LENGTH': str(len(raw)), + 'wsgi.input': io.BytesIO(raw), 'HTTP_AUTHORIZATION': 'Bearer ' + caller.token, + 'HTTP_IDEMPOTENCY_KEY': payload.get('id', '')} + body = b''.join(app(env, lambda status, headers: result.update(status=int(status[:3])))) + return result['status'], json.loads(body) + + with tempfile.TemporaryDirectory(prefix='csoc-mail-return-') as tmp, patch( + 'email_connect.transactional.smtplib.SMTP', SyntheticSMTP): + root = Path(tmp) + ledger = SQLiteDeliveryStore(str(root / 'transactional.sqlite')) + provider = SMTPProvider('smtp.example.test', 587, 'fixture', 'fixture-only', + 'platform@example.test') + app = ScopedTransactionalApplication(ledger, provider, 'https://issuer.example.test', + [sender, operator, other]) + assert invoke(app, event('invitation-1'))[0] == 202 + assert len(captured) == 1 + message = captured[0] + reference = str(message['Message-ID']) + assert message['From'] == 'platform@example.test' + assert message['Reply-To'] is None and message['Return-Path'] is None + assert ledger.db.execute('SELECT count(*) FROM deliveries WHERE provider_ref=?', + (reference,)).fetchone()[0] == 1 + fixture = root / 'mailbox'; fixture.mkdir() + (fixture / 'bounce.eml').write_text( + 'From: mailer-daemon@example.test\nTo: platform@example.test\n' + 'Subject: Delivery Status Notification (Failure)\n' + 'Date: Sat, 31 Oct 2026 10:00:00 +0000\n' + 'Message-ID: \n' + 'Content-Type: text/plain; charset=utf-8\n\n' + 'Delivery failure.\nFinal-Recipient: rfc822; recipient@example.test\n' + 'Action: failed\nStatus: 5.1.1\nDiagnostic-Code: smtp; 550 User unknown\n' + 'Original-Message-ID: ' + reference + '\n') + config = AppConfig(mailbox=MailboxConfig(id='fixture-mailbox', protocol='fixture'), + scan=ScanConfig(), storage=StorageConfig(str(root / 'scanner.sqlite')), + reports=ReportsConfig(str(root / 'reports')), + source=SourceConfig(fixture_dir=str(fixture))) + scan = scan_mailbox(config) + evidence = StateStore(config.storage.path) + rows = evidence.evidence_rows() + assert scan.scan.messages_seen == 1 and len(rows) == 1 + assert rows[0]['original_message_id'] == reference + assert rows[0]['affected_email_address'] == 'recipient@example.test' + assert rows[0]['assessment_subclass'] == 'fail.hard_bounce' + assert not ledger.is_suppressed('recipient@example.test') + repeat = scan_mailbox(config) + assert repeat.scan.messages_new == 0 and repeat.scan.evidence_events_created == 0 + payload = {'recipient': 'recipient@example.test', 'reason': 'hard_bounce'} + assert invoke(app, payload, '/v1/suppressions')[0] == 403 + assert not ledger.is_suppressed('recipient@example.test') + assert invoke(app, payload, '/v1/suppressions', operator)[0] == 200 + assert invoke(app, event('invitation-2')) == (400, {'error': 'recipient_suppressed'}) + assert invoke(app, event('invitation-3', other, 'tenant:other'), caller=other) == ( + 400, {'error': 'recipient_suppressed'}) + assert len(captured) == 1 + ledger.db.close() + ledger = SQLiteDeliveryStore(str(root / 'transactional.sqlite')) + assert ledger.is_suppressed('recipient@example.test') + app = ScopedTransactionalApplication(ledger, provider, 'https://issuer.example.test', + [sender, operator, other]) + assert invoke(app, payload, '/v1/suppressions/remove')[0] == 403 + assert invoke(app, payload, '/v1/suppressions/remove', operator)[0] == 200 + assert not ledger.is_suppressed('recipient@example.test') + assert len(captured) == 1 + ledger.db.close(); evidence.close() + assert not root.exists() + print(json.dumps({ + 'schema': 'coulomb.mail-return-boundary-rehearsal.v1', + 'completed_at': datetime.now(timezone.utc).isoformat(), + 'source_sha256': {str(p.relative_to(args.owner_repo.resolve())): + hashlib.sha256(p.read_bytes()).hexdigest() + for p in sorted((source / 'email_connect').glob('*.py'))}, + 'probe_sha256': hashlib.sha256(Path(__file__).read_bytes()).hexdigest(), + 'transactional_source_matches_native': True, + 'smtp_transport': 'in-process synthetic replacement; no SMTP connection', + 'message_id_correlates_with_ledger_and_synthetic_bounce': True, + 'explicit_reply_to_or_return_path_headers': False, + 'scanner_deduplicates_evidence': True, + 'scanner_updates_transactional_suppression': False, + 'ordinary_sender_suppress_and_unsuppress_denied': True, + 'synthetic_operator_suppression_blocks_send_and_survives_restart': True, + 'suppression_scope': 'recipient-global across callers and tenants', + 'explicit_synthetic_operator_unsuppression_verified': True, + 'native_operator_admission_proved': False, + 'received_mail_authenticity_or_correlation_authority_proved': False, + 'python_network_operations_blocked': True, + 'temporary_ledgers_removed': True, + 'mail_sent': False, 'production_mailbox_accessed': False, + }, indent=2)) + + +if __name__ == '__main__': + main() diff --git a/workplans/CSOC-WP-0015-communication-runtime-acceptance.md b/workplans/CSOC-WP-0015-communication-runtime-acceptance.md index 34679c8..8f6f3e7 100644 --- a/workplans/CSOC-WP-0015-communication-runtime-acceptance.md +++ b/workplans/CSOC-WP-0015-communication-runtime-acceptance.md @@ -18,7 +18,14 @@ Residual acceptance from CSOC-WP-0010, split at the user's request. Existing imp ## Current execution map — 2026-10-31 -T01 caller policy, T03 independent ledger recovery and the narrowed T04 implementation/native rehearsal/application release are complete. T02 still owns the general platform sender, reply/inbox/support, DNS and bounce operation; backup-specific Gmail receipt does not close it. T05 exposes productive flow verdicts and T06 the remaining native reset-authority owner return. +T01 caller policy, T03 independent ledger recovery and the narrowed T04 +implementation/native rehearsal/application release are complete. T02 owns the +platform sender and return-mail operation. Platform-root with Bernd as human +operator is now explicitly accepted; see decision 9392ef52 below. Sender admission, +inbox access and return-message correlation remain unverified. Suppression +handling and retention also remain open. Backup-specific +Gmail receipt does not close those gates. T05 exposes productive flow verdicts +and T06 the remaining native reset-authority owner return. The [four-workplan assessment](../docs/release/goal-progress-audit-2026-10-31.md) records the scope split and effort estimate. Task counts are not effort percentages. @@ -46,13 +53,34 @@ flow verdicts remain T04. See docs/release/acceptance-audit-2026-10-27.md. ```task id: CSOC-WP-0015-T02 -status: todo +status: progress priority: high state_hub_task_id: "0604cb15-72c7-5d15-bb42-0335d4ec80df" ``` Review replacing the receiver forgejo sender with platform identity through its credential owner, then name reply/inbox/support owner and prove authorized inbound access, DNS MX/SPF/DKIM/DMARC, correlated bounce/suppression and retention. No unsolicited scans or auto responses. Existing native personal reset is accepted and stays separately evidenced. +2026-10-31 concrete follow-through: T02 advanced from todo to progress. User +selected platform-root with Bernd Worsch as human operator for platform-mail +replies/bounces, including registration/invitation; decision9392ef52-3bfe-42fd-b6d5-ec0730656775 +is resolved. Native generation74/source hashes confirm the receiver still uses +forgejo@coulomb.social and has no admitted suppression caller. Four authoritative +nameservers agree on MX/SPF/DMARC and three documented DKIM CNAMEs. This is DNS +publication evidence, not delivered-message authentication. + +Prepared three scoped SMTP projection resources and exact sender/environment +patches; all five native server dry-runs pass with unchanged image/PVC/caller +policy and unrelated spec. New consumer custody and supervised rollout remain +unimplemented, so execution_ready is false. Offline actual-owner-source proof +shows accepted Message-ID/bounce correlation and scanner deduplication, but no +scanner-to-transactional suppression bridge; the existing operator API works only +under synthetic admission and affects a recipient across callers/tenants. No +production send, mailbox scan, credential read or runtime change occurred. +Concrete packet and remaining sequence: +docs/release/platform-mail-sender-review-2026-10-31.md. Owner identity need not be +asked again. Next: fixed owner SMTP admission and supervised sender rollout; +bounded operator return handling plus retention and productive access proof. + ## T03 — Complete independent ledger recovery and copy custody ```task