Prepare platform sender migration and verify return-mail boundaries
Some checks failed
Use-case regression gate / usecase-regressions (push) Has been cancelled
Some checks failed
Use-case regression gate / usecase-regressions (push) Has been cancelled
Assistant: codex Assistant-Model: gpt-6-astra Assistant-Session: 01a18e2d-f513-7742-b8e7-12af0b744ff3
This commit is contained in:
parent
e56654933c
commit
d864333581
10 changed files with 1091 additions and 3 deletions
|
|
@ -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 |
|
||||
|
|
|
|||
178
docs/release/communication-mail-dns-2026-10-31.json
Normal file
178
docs/release/communication-mail-dns-2026-10-31.json
Normal file
|
|
@ -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
|
||||
}
|
||||
230
docs/release/communication-mail-native-2026-10-31.json
Normal file
230
docs/release/communication-mail-native-2026-10-31.json
Normal file
|
|
@ -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
|
||||
}
|
||||
37
docs/release/mail-return-boundary-rehearsal-2026-10-31.json
Normal file
37
docs/release/mail-return-boundary-rehearsal-2026-10-31.json
Normal file
|
|
@ -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
|
||||
}
|
||||
18
docs/release/platform-mail-ownership-2026-10-31.json
Normal file
18
docs/release/platform-mail-ownership-2026-10-31.json
Normal file
|
|
@ -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"
|
||||
}
|
||||
231
docs/release/platform-mail-sender-2026-10-31/candidate.json
Normal file
231
docs/release/platform-mail-sender-2026-10-31/candidate.json
Normal file
|
|
@ -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"
|
||||
}
|
||||
131
docs/release/platform-mail-sender-review-2026-10-31.md
Normal file
131
docs/release/platform-mail-sender-review-2026-10-31.md
Normal file
|
|
@ -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.
|
||||
66
scripts/prepare-platform-mail-sender.py
Normal file
66
scripts/prepare-platform-mail-sender.py
Normal file
|
|
@ -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))
|
||||
169
scripts/rehearse-mail-return-boundary.py
Normal file
169
scripts/rehearse-mail-return-boundary.py
Normal file
|
|
@ -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: <fixture-bounce@example.test>\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()
|
||||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue