diff --git a/README.md b/README.md
index 28dee93..3e71675 100644
--- a/README.md
+++ b/README.md
@@ -39,6 +39,15 @@ The eom.dev playbook maps them from the Vault names `listmonk_postgresql_passwor
1. In LDAP, create the posix account `news` with `mail: news@eom.dev` and add it to `cn=Postfix Users,ou=Postfix,ou=Services,dc=eom,dc=dev`. Its password is `listmonk_smtp_password`.
2. Add the `listmonk` client to Dex (`ericomeehan.dex/playbooks/deploy-eom.yml`) and run that play.
+## E-mail design
+
+Newsletters and listmonk's system e-mails (opt-in confirmation, password reset and so on) use the Catppuccin Mocha palette and the eom.dev logo, matching www.eom.dev.
+
+- `templates/email/campaign.html.j2` produces the **eom.dev** campaign template, which is the default for new campaigns, and an **eom.dev archive** variant without unsubscribe links. Both are kept in sync in the database on every run.
+- `templates/email/base.html.j2` overrides only the shared system e-mail header and footer. It is mounted from a ConfigMap through `--static-dir`.
+- The logo is a PNG (`www.eom.dev/static/email-logo.png`), because most mail clients don't show SVG.
+- Set `listmonk_email_postal_address` to add a mailing address to every footer. US anti-spam law (CAN-SPAM) requires one in commercial e-mail.
+
## API users
| User | Can do | Token |
diff --git a/defaults/main.yml b/defaults/main.yml
index 04ee5c0..5a96f9f 100644
--- a/defaults/main.yml
+++ b/defaults/main.yml
@@ -57,6 +57,16 @@ listmonk_oidc_admins:
email: eric@eom.dev
listmonk_site_name: eom.dev
+# E-mail design (templates/email/): Catppuccin Mocha like www.eom.dev.
+# The logo must be a PNG; most mail clients don't render SVG.
+listmonk_email_logo_url: https://www.eom.dev/static/email-logo.png
+listmonk_email_home_url: https://www.eom.dev/
+listmonk_email_members_url: https://www.eom.dev/members/
+# Postal address in every footer (CAN-SPAM wants one for commercial mail).
+listmonk_email_postal_address: ""
+# Campaign template names; the first becomes the default for new campaigns.
+listmonk_email_template_name: eom.dev
+listmonk_email_archive_template_name: eom.dev archive
listmonk_logo_url: https://dex.eom.dev/theme/logo.svg
listmonk_favicon_url: https://www.eom.dev/static/favicon.svg
listmonk_notify_emails:
diff --git a/tasks/main.yml b/tasks/main.yml
index abffff2..57580af 100644
--- a/tasks/main.yml
+++ b/tasks/main.yml
@@ -43,6 +43,14 @@
wait: true
wait_timeout: "{{ listmonk_wait_timeout }}"
+- name: Store the system e-mail templates
+ kubernetes.core.k8s:
+ kubeconfig: "{{ listmonk_kubeconfig | default(omit, true) }}"
+ context: "{{ listmonk_kube_context | default(omit, true) }}"
+ state: present
+ apply: true
+ definition: "{{ lookup('ansible.builtin.template', 'email-configmap.yml.j2') | from_yaml }}"
+
- name: Deploy listmonk
kubernetes.core.k8s:
kubeconfig: "{{ listmonk_kubeconfig | default(omit, true) }}"
diff --git a/templates/configure.sql.j2 b/templates/configure.sql.j2
index 2664b6d..960ae7b 100644
--- a/templates/configure.sql.j2
+++ b/templates/configure.sql.j2
@@ -168,6 +168,40 @@ WITH ur AS (
SELECT count(*) FROM up;
{% endfor %}
+{#- Campaign templates (templates/email/campaign.html.j2): created by name,
+ body kept in sync, and the main one is the default for new campaigns. #}
+{%- set email_templates = [
+ (listmonk_email_template_name, lookup('ansible.builtin.template', 'email/campaign.html.j2', template_vars={'archive': false})),
+ (listmonk_email_archive_template_name, lookup('ansible.builtin.template', 'email/campaign.html.j2', template_vars={'archive': true})),
+] %}
+{% for name, body in email_templates %}
+WITH ins AS (
+ INSERT INTO templates (name, type, subject, body)
+ SELECT {{ q(name) }}, 'campaign', '{{ '{{' }} .Campaign.Subject {{ '}}' }}', {{ q(body) }}
+ WHERE NOT EXISTS (SELECT 1 FROM templates WHERE name = {{ q(name) }} AND type = 'campaign')
+ RETURNING id
+)
+SELECT count(*) FROM ins;
+WITH upd AS (
+ UPDATE templates SET body = {{ q(body) }}, updated_at = NOW()
+ WHERE name = {{ q(name) }} AND type = 'campaign' AND body IS DISTINCT FROM {{ q(body) }}
+ RETURNING id
+)
+SELECT count(*) FROM upd;
+{% endfor %}
+WITH unset AS (
+ UPDATE templates SET is_default = false
+ WHERE is_default AND NOT (name = {{ q(listmonk_email_template_name) }} AND type = 'campaign')
+ RETURNING id
+)
+SELECT count(*) FROM unset;
+WITH setd AS (
+ UPDATE templates SET is_default = true
+ WHERE id = (SELECT min(id) FROM templates WHERE name = {{ q(listmonk_email_template_name) }} AND type = 'campaign') AND NOT is_default
+ RETURNING id
+)
+SELECT count(*) FROM setd;
+
{% if listmonk_remove_sample_data | bool %}
WITH del AS (
DELETE FROM campaigns
diff --git a/templates/email-configmap.yml.j2 b/templates/email-configmap.yml.j2
new file mode 100644
index 0000000..d985cb8
--- /dev/null
+++ b/templates/email-configmap.yml.j2
@@ -0,0 +1,11 @@
+apiVersion: v1
+kind: ConfigMap
+metadata:
+ name: {{ listmonk_name }}-email-templates
+ namespace: {{ listmonk_namespace }}
+ labels:
+ app.kubernetes.io/name: listmonk
+ app.kubernetes.io/instance: {{ listmonk_name }}
+data:
+ # Overrides only this file; every other system template stays built in.
+ base.html: {{ lookup('ansible.builtin.template', 'email/base.html.j2') | to_json }}
diff --git a/templates/email/base.html.j2 b/templates/email/base.html.j2
new file mode 100644
index 0000000..0fcac58
--- /dev/null
+++ b/templates/email/base.html.j2
@@ -0,0 +1,53 @@
+#jinja2:variable_start_string:'[[', variable_end_string:']]', block_start_string:'[%', block_end_string:'%]'
+{{ define "header" }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+{{ end }}
+
+{{ define "footer" }}
+
+
+
+
+
+{{ end }}
diff --git a/templates/email/campaign.html.j2 b/templates/email/campaign.html.j2
new file mode 100644
index 0000000..40f5d6a
--- /dev/null
+++ b/templates/email/campaign.html.j2
@@ -0,0 +1,89 @@
+#jinja2:variable_start_string:'[[', variable_end_string:']]', block_start_string:'[%', block_end_string:'%]'
+
+
+
+
+
+
+
+
+{{ .Campaign.Subject }}
+
+
+
+
+
+
+
+
+
+
+
+ |
+
+
+ |
+
+
+ |
+ {{ template "content" . }}
+ |
+
+
+
+[% if not archive %]
+ {{ TrackView }}
+[% endif %]
+ |
+
+
+
+
diff --git a/templates/listmonk.yml.j2 b/templates/listmonk.yml.j2
index 17f7f6f..4a126f6 100644
--- a/templates/listmonk.yml.j2
+++ b/templates/listmonk.yml.j2
@@ -39,6 +39,7 @@ spec:
app.kubernetes.io/component: app
annotations:
checksum/secret: {{ lookup('ansible.builtin.template', 'secret.yml.j2') | hash('sha256') }}
+ checksum/email: {{ lookup('ansible.builtin.template', 'email-configmap.yml.j2') | hash('sha256') }}
spec:
affinity: {{ listmonk_affinity | to_json }}
containers:
@@ -47,14 +48,15 @@ spec:
imagePullPolicy: {{ listmonk_image_pull_policy }}
# --config '' : configuration comes from the environment only.
# --install --idempotent only installs into an empty database;
- # --upgrade runs migrations after an image bump.
+ # --upgrade runs migrations after an image bump. --static-dir
+ # overlays the eom.dev system e-mail header/footer (base.html).
command:
- sh
- -c
- >-
./listmonk --install --idempotent --yes --config '' &&
./listmonk --upgrade --yes --config '' &&
- exec ./listmonk --config ''
+ exec ./listmonk --config '' --static-dir /listmonk/static-eom
env:
- name: TZ
value: America/New_York
@@ -97,7 +99,13 @@ spec:
volumeMounts:
- name: uploads
mountPath: /listmonk/uploads
+ - name: email-templates
+ mountPath: /listmonk/static-eom/email-templates
+ readOnly: true
volumes:
+ - name: email-templates
+ configMap:
+ name: {{ listmonk_name }}-email-templates
- name: uploads
persistentVolumeClaim:
claimName: {{ listmonk_name }}-uploads
diff --git a/tests/render.yml b/tests/render.yml
index 2be100b..2c4fd7b 100644
--- a/tests/render.yml
+++ b/tests/render.yml
@@ -29,9 +29,9 @@
src: "{{ role_path }}/templates/{{ item }}.j2"
dest: "{{ out }}/{{ item }}"
mode: "0600"
- loop: [secret.yml, postgres.yml, listmonk.yml, configure.sql]
+ loop: [secret.yml, postgres.yml, listmonk.yml, configure.sql, email-configmap.yml]
- name: Manifests parse as YAML
ansible.builtin.assert:
that: lookup('ansible.builtin.file', out ~ '/' ~ item) | from_yaml_all | list | length > 0
quiet: true
- loop: [secret.yml, postgres.yml, listmonk.yml]
+ loop: [secret.yml, postgres.yml, listmonk.yml, email-configmap.yml]