Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,22 @@ mail_platform_brevo_sender_email: operator@example.com
mail_platform_brevo_sender_name: Florin Mail Bridge
mail_platform_reply_to_email: "{{ mail_platform_mailbox_address }}"
mail_platform_gateway_bind_port: "{{ mail_platform_gateway_port }}"
mail_platform_gateway_force_brevo_fallback: true
mail_platform_gateway_attempt_local_first: false
mail_platform_gateway_force_brevo_fallback: false
mail_platform_gateway_attempt_local_first: true
mail_platform_dkim_enabled: true
mail_platform_dkim_selector: mail
mail_platform_dkim_private_key_local_file: "{{ repo_shared_local_root }}/mail-platform/dkim-private.pem"
mail_platform_dkim_private_key_host_path: "{{ mail_platform_stalwart_dir }}/etc/dkim-private.pem"
mail_platform_dkim_private_key_container_path: /opt/stalwart/etc/dkim-private.pem
# Public key (base64, no PEM headers) — read from .local at runtime.
# lookup() returns '' when the file is absent so a missing key becomes a visible empty record.
mail_platform_dkim_public_key: >-
{{ lookup('ansible.builtin.file', repo_shared_local_root ~ '/mail-platform/dkim-public-key.txt', errors='ignore') | default('', true) | trim }}
# DNS TXT record value split into ≤255-char chunks (DNS limit per string).
# Produces: "v=DKIM1; k=rsa; p=<chunk1>" "<chunk2>"
mail_platform_dkim_dns_value: >-
{%- set full = 'v=DKIM1; k=rsa; p=' ~ mail_platform_dkim_public_key -%}
"{{ full[:255] }}" "{{ full[255:] }}"
mail_platform_gateway_uvicorn_workers: 1
mail_platform_gateway_api_user: mail-platform
mail_platform_notification_profiles:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ def load_notification_profiles() -> dict[str, dict[str, Any]]:
api_key = str(item.get("gateway_api_key", "")).strip()
mailbox_address = str(item.get("mailbox_address", "")).strip()
mailbox_localpart = str(item.get("mailbox_localpart", "")).strip()
mailbox_password = str(item.get("mailbox_password", "")).strip()
description = str(item.get("description", "")).strip()
owner = str(item.get("owner", "")).strip()
credential_scope = str(item.get("credential_scope", "")).strip()
Expand Down Expand Up @@ -106,6 +107,7 @@ def load_notification_profiles() -> dict[str, dict[str, Any]]:
"id": profile_id,
"mailbox_localpart": mailbox_localpart,
"mailbox_address": mailbox_address,
"mailbox_password": mailbox_password,
"sender_email": sender_email,
"sender_name": sender_name,
"reply_to": reply_to,
Expand Down Expand Up @@ -373,10 +375,14 @@ def send_via_local_smtp(payload: SendRequest, profile: dict[str, Any]) -> None:
message.set_content(payload.text or "")

context = ssl.create_default_context()
context.check_hostname = False
context.verify_mode = ssl.CERT_NONE # local Stalwart uses self-signed cert
with smtplib.SMTP(LOCAL_SMTP_HOST, LOCAL_SMTP_PORT, timeout=20) as client:
client.starttls(context=context)
if LOCAL_SMTP_USERNAME and LOCAL_SMTP_PASSWORD:
client.login(LOCAL_SMTP_USERNAME, LOCAL_SMTP_PASSWORD)
smtp_username = profile.get("mailbox_localpart") or LOCAL_SMTP_USERNAME
smtp_password = profile.get("mailbox_password") or LOCAL_SMTP_PASSWORD
if smtp_username and smtp_password:
client.login(smtp_username, smtp_password)
client.send_message(message)


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -383,6 +383,16 @@
become: false
no_log: true

- name: Deploy the DKIM private key for Stalwart outbound signing
ansible.builtin.copy:
src: "{{ mail_platform_dkim_private_key_local_file }}"
dest: "{{ mail_platform_dkim_private_key_host_path }}"
owner: root
group: root
mode: "0644"
when: mail_platform_dkim_enabled | default(false)
no_log: true

- name: Render the Stalwart configuration file
ansible.builtin.template:
src: stalwart-config.toml.j2
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,3 +80,17 @@ events = {{ mail_platform_webhook_events | to_json }}
timeout = "10s"
throttle = "1s"
lossy = false
{% if mail_platform_dkim_enabled | default(false) %}

[signature."rsa-{{ mail_platform_dkim_selector }}"]
private-key = "file://{{ mail_platform_dkim_private_key_container_path }}"
algorithm = "rsa-sha256"
domain = "{{ mail_platform_domain }}"
selector = "{{ mail_platform_dkim_selector }}"
headers.sign = ["From", "To", "Date", "Message-ID", "Subject", "MIME-Version", "Content-Type"]
headers.oversign = ["From"]
expiration = "7d"

[queue.outbound.signing]
sign = [{ else = ["rsa-{{ mail_platform_dkim_selector }}"] }]
{% endif %}
13 changes: 12 additions & 1 deletion inventory/host_vars/proxmox-host.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1973,28 +1973,39 @@ mail_platform_dns_records:
ttl: 300
- name: "@"
type: TXT
value: '"v=spf1 mx include:spf.brevo.com -all"'
value: '"v=spf1 mx ~all"'
value_match_regex: '^"v=spf1 '
ttl: 300
- name: _dmarc
type: TXT
value: "\"v=DMARC1; p=quarantine; rua=mailto:{{ platform_operator_email }}\""
ttl: 300
- name: "mail._domainkey"
type: TXT
# DKIM key exceeds 255 chars per DNS string; split into two quoted chunks.
# mail_platform_dkim_dns_value is computed in role defaults from dkim-public-key.txt.
value: "{{ mail_platform_dkim_dns_value }}"
value_match_regex: '^"v=DKIM1; k=rsa; p='
ttl: 300
# Retire Brevo-specific records — mail is now delivered via local Stalwart SMTP
- name: brevo1._domainkey
type: CNAME
value: b1.lv3-org.dkim.brevo.com.
value_match_regex: '^b1\\.lv3-org\\.dkim\\.brevo\\.com\\.?$'
ttl: 300
state: absent
- name: brevo2._domainkey
type: CNAME
value: b2.lv3-org.dkim.brevo.com.
value_match_regex: '^b2\\.lv3-org\\.dkim\\.brevo\\.com\\.?$'
ttl: 300
state: absent
- name: "@"
type: TXT
value: '"brevo-code:b5e6a2e60000b8da96b912f8ea4c2e9a"'
value_match_regex: '^"brevo-code:'
ttl: 300
state: absent

route_dns_assertion_extra_records:
- name: "@"
Expand Down
9 changes: 5 additions & 4 deletions versions/stack.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
schema_version: 1.0.0
repo_version: 0.179.46
platform_version: 0.179.46
repo_version: 0.179.47
platform_version: 0.179.47

live_apply_evidence:
receipt_dir: receipts/live-applies
Expand Down Expand Up @@ -50,7 +50,7 @@ live_apply_evidence:
vaultwarden: 2026-04-05-ws-0331-runtime-pool-mainline-live-apply
certificate_lifecycle: 2026-03-27-adr-0101-certificate-lifecycle-main-live-apply
windmill: 2026-04-05-ws-0331-runtime-pool-mainline-live-apply
woodpecker: 2026-04-12-ws-0025-compose-stack-lifecycle-mainline-live-apply
woodpecker: 2026-05-21-woodpecker-ci-deployed-live-apply
self_correcting_automation_loops: 2026-03-28-adr-0204-self-correcting-automation-loops-live-apply
budgeted_workflow_scheduler: 2026-03-27-adr-0119-budgeted-workflow-scheduler-mainline-live-apply
observation_to_action_closure_loop: 2026-03-26-adr-0126-observation-to-action-closure-loop-live-apply
Expand Down Expand Up @@ -131,7 +131,7 @@ live_apply_evidence:
nomad_scheduler: 2026-04-05-ws-0331-runtime-pool-mainline-live-apply
serverclaw: 2026-04-01-adr-0294-one-api-mainline-live-apply
serverclaw_memory: 2026-03-29-adr-0263-serverclaw-memory-substrate-mainline-live-apply
route_dns_assertion_ledger: 2026-03-29-adr-0273-public-endpoint-admission-control-mainline-live-apply
route_dns_assertion_ledger: 2026-05-26-route-dns-assertion-ledger-live-apply
session_logout_authority: 2026-03-29-adr-0248-session-logout-authority-mainline-live-apply
restore_verification: 2026-03-29-adr-0272-restore-readiness-mainline-live-apply
public_endpoint_admission_control: 2026-03-29-adr-0273-public-endpoint-admission-control-mainline-live-apply
Expand Down Expand Up @@ -192,6 +192,7 @@ live_apply_evidence:
repowise: 2026-04-14-adr-0371-parameterized-service-verification-tasks-mainline-live-apply
repo_intake: 2026-04-21-adr-0373-service-registry-and-derived-defaults-mainline-live-apply
platform_ops: 2026-04-21-adr-0391-cpu-only-operational-automation-live-apply
platform_service_watchdog: 2026-05-26-platform-service-watchdog-live-apply
desired_state:
host_id: proxmox-host
provider: hetzner-dedicated
Expand Down
Loading