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" }} + + + + + + + + + + + + +
 
+
+
+ eom.dev +
+{{ 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 }} + + + + + + + + +
+ + + + + + + + + + + + + + + +
+[% 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]