diff --git a/ansible/roles/podman/files/debyltechmail/lib/Mail/DebyltechEMailTemplate.php b/ansible/roles/podman/files/debyltechmail/lib/Mail/DebyltechEMailTemplate.php
index 97b3f13..5ba9b16 100644
--- a/ansible/roles/podman/files/debyltechmail/lib/Mail/DebyltechEMailTemplate.php
+++ b/ansible/roles/podman/files/debyltechmail/lib/Mail/DebyltechEMailTemplate.php
@@ -188,6 +188,21 @@ class DebyltechEMailTemplate extends EMailTemplate {
'font-size:26px;font-weight:300;letter-spacing:-0.02em',
$this->heading,
);
+
+ // Opt out of mail-client dark mode. Without this Apple Mail repaints
+ // the white ground charcoal on its own, and the black wordmark all but
+ // disappears. Swapping to a white logo under prefers-color-scheme is
+ // NOT a fix: with Apple Mail's "Use dark backgrounds for messages" off,
+ // the query still matches while the ground stays white, leaving a
+ // white-on-white logo. This mail is designed light; render it light.
+ $this->head = str_replace(
+ '',
+ ''
+ . ''
+ . ''
+ . '',
+ $this->head,
+ );
}
/**
diff --git a/ansible/roles/podman/tasks/containers/debyltech/cloud.yml b/ansible/roles/podman/tasks/containers/debyltech/cloud.yml
index aea8dfb..69f983b 100644
--- a/ansible/roles/podman/tasks/containers/debyltech/cloud.yml
+++ b/ansible/roles/podman/tasks/containers/debyltech/cloud.yml
@@ -370,6 +370,58 @@
register: debyltech_enum_fullmatch
changed_when: "'is now set to' in debyltech_enum_fullmatch.stdout"
+# Settings Skudak only ever had from clicks in the LibreSign admin page, never
+# in git -- a fresh instance without them cannot invite anyone by address:
+#
+# identify_methods The EMAIL identification method. Without it the signer
+# search only lists accounts, so an outside address
+# returns a bare "No signers." -- the whole point of this
+# instance. clickToSign (no emailed code) and
+# can_create_account=false, as on Skudak: the emailed link
+# is the identity check, and customers never get accounts.
+# signature_background_type=deleted
+# Drops the LibreSign logo watermark from behind the
+# stamp, so with GRAPHIC_ONLY the stamp is the drawn mark
+# and nothing else.
+# collect_metadata Records signer IP/user agent alongside each signature.
+- name: set libresign signer identification and stamp settings in debyltech-cloud
+ become: true
+ become_user: "{{ podman_user }}"
+ ansible.builtin.command: >
+ podman exec -u www-data debyltech-cloud
+ php occ config:app:set libresign {{ item.k }} --value={{ item.v | quote }}
+ register: debyltech_libresign_settings
+ changed_when: "'is now set to' in debyltech_libresign_settings.stdout"
+ loop:
+ - k: identify_methods
+ v: >-
+ {{ [{'name': 'email', 'friendly_name': 'Email', 'enabled': true,
+ 'mandatory': true,
+ 'signatureMethods': {
+ 'clickToSign': {'name': 'clickToSign', 'enabled': true},
+ 'emailToken': {'name': 'emailToken', 'enabled': false}},
+ 'can_create_account': false,
+ 'test_url': '/index.php/settings/admin/mailtest',
+ 'signatureMethodEnabled': 'clickToSign'}] | to_json }}
+ - {k: signature_background_type, v: deleted}
+ - {k: collect_metadata, v: "1"}
+ loop_control:
+ label: "{{ item.k }}"
+
+# The validation footer ("Digitally signed by ... Validate in ") is WANTED
+# -- only its QR code goes, below. FooterHandler defaults add_footer to true
+# when unset, but the 14.2 admin page renders unset as UNCHECKED, inviting
+# someone to "correct" it into actually turning the footer off. Stored
+# explicitly so the page shows what the code does.
+- name: keep libresign validation footer text in debyltech-cloud
+ become: true
+ become_user: "{{ podman_user }}"
+ ansible.builtin.command: >
+ podman exec -u www-data debyltech-cloud
+ php occ config:app:set libresign add_footer --value=1 --type=boolean
+ register: libresign_footer
+ changed_when: "'is now set to' in libresign_footer.stdout"
+
- name: drop libresign validation QR code from signed-PDF footer in debyltech-cloud
become: true
become_user: "{{ podman_user }}"
@@ -444,6 +496,23 @@
register: debyltechmail_enable
changed_when: "'already enabled' not in debyltechmail_enable.stdout"
+# Behind rootless podman's port forwarder, every request -- Caddy's included --
+# reaches Apache FROM THE CONTAINER'S OWN ADDRESS on `shared`, not from the
+# host. Unless exactly that address is a trusted proxy, Nextcloud ignores the
+# X-Forwarded-For header Caddy sends, so every client looks like one IP:
+# brute-force throttling then penalises everyone at once. Read per deploy
+# because the address is assigned at container creation, and deploys are the
+# only thing that recreate it.
+- name: read debyltech-cloud container address
+ become: true
+ become_user: "{{ podman_user }}"
+ ansible.builtin.command: >
+ podman inspect debyltech-cloud
+ --format "{{ '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' }}"
+ register: debyltech_cloud_ip
+ changed_when: false
+ failed_when: debyltech_cloud_ip.stdout is not match('^[0-9.]+$')
+
# System config, set only when it differs so a clean re-deploy reports no
# changes (Skudak's equivalents report changed on every run). Values are
# single-quoted into the shell, so the backslashes in mail_template_class pass
@@ -467,6 +536,10 @@
loop:
- {k: overwrite.cli.url, v: "https://{{ cloud_debyltech_server_name }}"}
- {k: overwriteprotocol, v: https}
+ - {k: trusted_proxies 0, v: "{{ debyltech_cloud_ip.stdout }}"}
+ # Hour in UTC: 05:00 UTC is 01:00/00:00 Eastern, ahead of the 04:15 backup.
+ - {k: maintenance_window_start, v: "5", t: integer}
+ - {k: default_phone_region, v: US}
- {k: loglevel, v: "2", t: integer}
- {k: log_rotate_size, v: "10485760", t: integer}
- {k: mail_template_class, v: "OCA\\Debyltechmail\\Mail\\DebyltechEMailTemplate"}
diff --git a/ansible/roles/podman/templates/nextcloud/debyltechmail-verify.php.j2 b/ansible/roles/podman/templates/nextcloud/debyltechmail-verify.php.j2
index cac2f3b..65720f8 100644
--- a/ansible/roles/podman/templates/nextcloud/debyltechmail-verify.php.j2
+++ b/ansible/roles/podman/templates/nextcloud/debyltechmail-verify.php.j2
@@ -72,6 +72,10 @@ if (!str_contains($text, 'official document-signing request')) {
if (!str_contains($html, 'debyltech.com/legal/privacy') || !str_contains($html, 'debyltech.com/legal/tos')) {
$failures[] = 'privacy/terms links missing from footer';
}
+if (!str_contains($html, 'content="light only"')) {
+ $failures[] = 'color-scheme "light only" meta missing -- dark-mode mail clients will repaint '
+ . 'the ground and bury the black wordmark (did upstream rename in $head?)';
+}
if (preg_match('/[»«]/u', $html)) {
$failures[] = 'German guillemets survived into the body';
}
@@ -131,6 +135,20 @@ if ($appConfig->getValueString('core', 'shareapi_restrict_user_enumeration_full_
. 'unset or "yes"; it is NOT the knob for the account-owned-email problem.';
}
+// Outside signers are invited by address; without an enabled email identify
+// method the signer search returns "No signers." for any non-account email.
+$methods = json_decode($appConfig->getValueString('libresign', 'identify_methods', '[]'), true) ?: [];
+$emailOn = false;
+foreach ($methods as $m) {
+ if (($m['name'] ?? '') === 'email' && !empty($m['enabled'])) {
+ $emailOn = true;
+ }
+}
+if (!$emailOn) {
+ $failures[] = 'libresign email identify method is not enabled -- outside addresses '
+ . 'cannot be added as signers ("No signers.")';
+}
+
$identDocs = $appConfig->getValueString('libresign', 'identification_documents', '');
if ($identDocs !== '0') {
$failures[] = 'libresign identification_documents is "' . $identDocs