Skip to content

Commit 0008e82

Browse files
authored
feat(mail): wire Tipimail SMTP on prod (#3237) (#3238)
1 parent 6407cbc commit 0008e82

7 files changed

Lines changed: 175 additions & 7 deletions

File tree

.kontinuous/env/preprod/templates/mail.configmap.yaml

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@ apiVersion: v1
33
metadata:
44
name: mail
55
data:
6-
MAIL_ENABLED: "false"
7-
SMTP_HOST: ""
6+
# Using MailDev as an in-cluster SMTP catcher until a Tipimail preprod
7+
# sealed-secret is available. The UI is exposed at
8+
# https://maildev-<global.host> via the maildev Ingress.
9+
MAIL_ENABLED: "true"
10+
SMTP_HOST: "maildev"
811
SMTP_PORT: "1025"
912
MAIL_FROM: "no-reply@egapro.preprod.fabrique.social.gouv.fr"
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
apiVersion: apps/v1
2+
kind: Deployment
3+
metadata:
4+
name: maildev
5+
namespace: {{ .Values.global.namespace }}
6+
labels:
7+
app: maildev
8+
spec:
9+
replicas: 1
10+
selector:
11+
matchLabels:
12+
app: maildev
13+
template:
14+
metadata:
15+
labels:
16+
app: maildev
17+
spec:
18+
securityContext:
19+
runAsNonRoot: true
20+
runAsUser: 1000
21+
containers:
22+
- name: maildev
23+
image: maildev/maildev:2.2.1
24+
securityContext:
25+
allowPrivilegeEscalation: false
26+
readOnlyRootFilesystem: true
27+
capabilities:
28+
drop: ["ALL"]
29+
ports:
30+
- name: smtp
31+
containerPort: 1025
32+
- name: web
33+
containerPort: 1080
34+
readinessProbe:
35+
httpGet:
36+
path: /
37+
port: web
38+
initialDelaySeconds: 5
39+
periodSeconds: 10
40+
resources:
41+
requests:
42+
cpu: 10m
43+
memory: 64Mi
44+
limits:
45+
cpu: 200m
46+
memory: 256Mi
47+
volumeMounts:
48+
- name: tmp
49+
mountPath: /tmp
50+
volumes:
51+
- name: tmp
52+
emptyDir: {}
53+
---
54+
apiVersion: v1
55+
kind: Service
56+
metadata:
57+
name: maildev
58+
namespace: {{ .Values.global.namespace }}
59+
spec:
60+
selector:
61+
app: maildev
62+
ports:
63+
- name: smtp
64+
port: 1025
65+
targetPort: smtp
66+
- name: web
67+
port: 1080
68+
targetPort: web
69+
---
70+
apiVersion: networking.k8s.io/v1
71+
kind: Ingress
72+
metadata:
73+
name: maildev
74+
namespace: {{ .Values.global.namespace }}
75+
annotations:
76+
cert-manager.io/cluster-issuer: letsencrypt-prod
77+
nginx.ingress.kubernetes.io/ssl-redirect: "true"
78+
spec:
79+
ingressClassName: nginx
80+
tls:
81+
- hosts:
82+
- maildev-{{ .Values.global.host }}
83+
secretName: maildev-tls
84+
rules:
85+
- host: maildev-{{ .Values.global.host }}
86+
http:
87+
paths:
88+
- path: /
89+
pathType: Prefix
90+
backend:
91+
service:
92+
name: maildev
93+
port:
94+
name: web

.kontinuous/env/prod/templates/mail.configmap.yaml

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,5 @@ apiVersion: v1
33
metadata:
44
name: mail
55
data:
6-
MAIL_ENABLED: "false"
7-
SMTP_HOST: ""
8-
SMTP_PORT: "1025"
9-
MAIL_FROM: "no-reply@egapro.travail.gouv.fr"
6+
MAIL_ENABLED: "true"
7+
MAIL_FROM: "EgaPro <index@travail.gouv.fr>"
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# Inherited as-is from the V1 prod setup (SocialGouv/egapro@master).
2+
# The MAILER_* keys are remapped to the V2 SMTP_* env var names in
3+
# .kontinuous/values.yaml. `MAILER_SEND_EMAILS` is a V1 legacy flag —
4+
# unused by the V2 app (sending is gated by MAIL_ENABLED in the `mail`
5+
# configmap). Kept in the sealed-secret for a no-op migration; can be
6+
# dropped once the prod credentials are re-sealed without it.
7+
apiVersion: bitnami.com/v1alpha1
8+
kind: SealedSecret
9+
metadata:
10+
annotations:
11+
sealedsecrets.bitnami.com/namespace-wide: 'true'
12+
name: smtp-app
13+
namespace: egapro
14+
spec:
15+
encryptedData:
16+
MAILER_SEND_EMAILS: AgCFRiBYt8dlFrNmu/9zB9bUiRWf589fyN438L2JmF9QfuwZyI8kdXPcYg5XCdo/WHA7dAPS3JaMzFFbxxOvd45qLRRcF7p9HzZjqIM7/3K2/pRbHxkxRSQBhi3X+aWq1OND4gLklPN2SDXNZjHIiaGkOiZJEQomDTTPpurJhLBmr91EDgqgspcPWDdFeDrA36pFkh6Q5hfeOka/Ew+vKc3O70t7++A/Qx9/HAFH+QWRhyahThMVr1hYGJUBSaW0azzot/fuFPkzvPntvRTmfS7W2Tcw531uu+j/irkXFTwws83Cfa/1Ykknd91KSbJBbjfl6XbHWFI7Rvja2YAj0NEV685xUDaDwkZBA+RydkJtKbSxnntVFw8tpiF06Pn7NNVKbjLMAVNoO80RLncj9S+FLvibZiSC5PCDa+QsmtBAokj0aUuVtIlHyax2Td+YOgX894eartj2Bo8oZH95pm7urK4yUnxbswMW5x064SrB9vNfBTDI+p3/QdNqBhIUiXEMt89ikiqSA7NaIcKXcn77PS2ySz7XhvA0qL4A3cs6jiWMSsCp0bTQ44vrwsbn+40UBTAplUrWHx4ZHrpBEd7KDbGcsJecmnZqZw3+TSaqyvY8lLz7JFDTACQ0UChtMxi5M4UARof/+s0Kc+75FwPCctoP2S4Nr6j6d7JVYiVJ7gy53Zjgd6xRIRy+RURL/lne
17+
MAILER_SMTP_HOST: AgCJUjNoGmqadk9PMkjJWhhUXel/d/QckP+KNnsmh+HeMy8UkHuPGytxneGaxYzPSxBkPexKpvqiyUTzhV7539kHJU9Vw02Xb3UGPvjIA+XDrK4q84bem+yE8VacbYPBI5jAhLgP/UmTRje3qJhQj+L7LIfgHEoEBX1dI23e/d2IlE35MRLIAKl73kna6/W4kayi4Rv61Emsqnocn02yYU/W5N7Oj7nEouW9e3Xkqu6PyCuHs5Y5EmUf73WG0Nl926xu257YWk39JqtRzMtxaQow1u3E4jguyZtL6JdJeRrXllp04TH4uRr2fzAXtuYyiDPV+l5WoR0PhZiFclfgToD4raCymkCP9EMvFO2H4dBQ1ZFOOzYrj7hJINqUzxn5AmxNsemtApfE8Qr7P7mvhuWKvlD9WJ4bdGx8M218gz8PKeszut1AfnhzWoYA6nnacO/H5iYK/3/ieZk1g4oIUZPFhYGWWpHWaI4pD31lvRoSRo+X4CtDbQALSWNA7aW1czM96Uxr5tHw29ozJ6h1LeL6mQSqWJJ54/TISHwUNawsAONrjrC1QM7H09MVYrZKQ4/w22+Zg59pa/YdmP85Jgz+tulM6gzX/jRjVbT4ncjo5vBLQKXFRjsSgT0+xMxHkpDMJx4ZieIRPiKbCOQC2jjsPAsrbtRuw2gXqRndU3hM8xKSoEyuUc2u82ro5Z++PQEXHUyuue6NoFS+/K+UqpZ8Qw==
18+
MAILER_SMTP_LOGIN: AgA1f6wwAt2o8Qa45WkfhB+7vWXYXZUqeYQufKaV+QN6sM3QbJoEabzzVOrtU8c7ZmqkeXvIaE397/1vB0tcOopbWDUtSaq+XSSrun3D9xXuRoJd0qOmm33WUh+8dpf/j925segebThXKj21/O3X21VTrGlLbbMVrZTEzKjFh4svelZStsf8C8/4cjOhoU9lVevhv4HbnQvb8BRqnVzLXbFYkO/HVnQp6CgqkwwXcyvZxQoHc6eWiIgcKgvIGxoxe9vsqZuqmXE715MFJD76j2KduU2DqXYV2FzeNB5LCVlg2RUkIQsqsGEhONf4P3xoWZUscD0u19x6ImEoG89qxeEfvgeHPwnyzXw7uEKBvSOekYT6ZkDIMjNFk3QgHr/Ow05zjpbXUaZmYyr2boE9ajiU9FwwQJDaAN4jrrBji/t/tHR1FV3TDFsmiVqrim/xo89ZmW05kiHKqfF2W1rxX1lrQINu3RmcSODeHKqqKQ0I6CM+MWE7cRyTr9lv1qy4aUXXXoCdo/gGqsv370IycrD0wL57OXIehcNm9/Oi/DxkT30mnb0yAHpWes5AEmOgybpPisej5N3Rlys2X3fNZ/Q7WRyumYCtsCZe2mgbno1hPUBL6uixPbbnFJOY6XQkRXnR9ki9Cnv1vLZ0Bu7BI9iqqwwUZXdUnbluvzgW+kh+cvlyHnNe2lkqblma6ZYjqpjGU+RlJiyJ3o5t7rJAJMpecLDTpq5yYoDljkJ5Y1LpeQ==
19+
MAILER_SMTP_PASSWORD: AgBIGjkxoeUK2UgIiyYYC1AZYjbDy8TkFg6p4ddDhhEX+eeh07stFygp0LNjRmoGco3WPElGvyoo5zt3jTyg7SxunsMRtYt0ewc5CmKfTenkOoAlcBW2o3d3WgIe3fERuwYFtjU3WUFJ9y1S1vRnxI/rGjunDEe+L7DHo6n4Rco+H3m22+h9HgxfXSG+TrMz8r/LJaLUOATpVptlSAlo1+nixvxIGE5EXeKYgn5D3nGeh6tzJ24LbGK3QSRWFSTw/qcsz+L2nFP1LXSboAiwqEWNfAndwYg3nwABO2J1TArLoqhzBBgIaHdkLXh9g9s5DLuYn0AxcQLi95CIdYvupa4XFrsrVI0J7OWCoMH9P8IdQJbRLBhbH/ULyuESLD4LfNwzcE3r9o9hDdM72/e2uROqFJ4ScCCkllTcqwTbSPu5xjKNUIdEVYp25BAi0dk84uc1Wm+lb61OWp6e6dLSYMuz8cOF9HZlVaVjm1WMujZQ0wE14wzipXAs54hXjz26BTe6P6Xn5JTURcg7ddMIQXngeoY8vrnleQbfgrsOzyvaZv/pjS9940MTR5isR9mAx0AC/iAVFlWBeXtljQ9eJ/vjb1rVfo+w0rDpywUP++Z2ubm6i70vdlk2Giea/ArKxTYVJC3FbRgK2pMPWGYezC87rZdL5Gl7W2s0WKWd7mB7ha8X/PcIu/xDqA40MAwnyKYjgouR2n3KcFvoxgNVsouoG9o6Rx0jNdLC6j06Yb7Uxg==
20+
MAILER_SMTP_PORT: AgBEz7m4f8/Q4stmQ8xbA0+dEUGaPqMuybL0OZDEJC7AOXbhbGqGUi/MGA8O1ZM7eg57R+BFbcjIj5QAJ0g4wvIR2IgKxw4UShp4/INYUPPXCu6Wnz29IsniyZQ8UZhqPV7IbxxmgdhNCEh3k5Hu0IYmA+NI/CiN55w3QWiUBgk3/u4hi+s0drMvYTr+ZlC5qiJ+vveDYJNcVIFOgbIBuaDjoqvnhelppAb/rfwGPoEOONcFLbWsKrSTw5E1Q7eVnJBNUazK9ev9j4M4au9MfWGfUcMUza6V8DJuPmgOfyEpk/81IbIuk36rPsikouwpqyA2uDl0K+MxTryiI//YMzJMJQYjEkuKdYSPtNzhKxrKURdTt4x7g5jjKEyudAbFBXEAtvxrmEikWbI6uCM2lqFA6Xhoykyt4PBbG6VjYZLf3mz+tPjr7FsAh2ELxqBq75PPwZLLyQcjqmvoq2YDQ7+e99YJ5S9VjOOrvU698c8cbxWMkcVr/cuaNynsgjoK6Mf1P5R/5GW0sk7fQmbzQDZFpIqH5f9TS8Q4trjEVyI81SaJd4yOHRtlTNivPdqpDppuZFyO3s5Y5l0V/00wkCdcOR6Xm6hsDbsijloMnY+xcswL+eZFJDRv5sKMp7WBjnUlw1KGcusv4SzvevYvxbYWArWEoOjIsjT5DWu46uittCg4+GT+Qc82rz3Xje439SR4LH4=
21+
MAILER_SMTP_SSL: AgBP4tbeiU7J4+exn1vup8r4wa5/6DXJN/sOgXIB6V5bVPlGCdNKBlILJAgR5ECRVLClB11Bqg+VPopkpX7VApd3ap5OwmXyyIodvsNoqTZ3p+OQlMsZhXoXIB29s4RdIwYcTil9jaEN3oKiYXRjWmcevO3rtuqUsy/maB74/tA1hAUarJ2brJDB8P/iqNIARk9EDLjWvU1pfiKiXG9FD1gUqeedvErcmCM8jdHndlk78wws7uwCT/w2fAItMEuWcbw97ULW+hAl1A92b6HwDIJdOpHBoC59msJvlAIc7k11siQ9gCpPljZyJRCKxi32+fkFvxV/kgCtrPKQbYhYjDdLNqrljUPXhXG9S9/z4EqCIn2Bx1Qu6jsBfRSfN0b1RID/SdTqaCyDhu/WSjb5xN5Fj4C9sRHSeO4TMzcKLBTAmYJ1J3Ajquu1zzjzkApEMon6nSoYvISP4Qs9alH0FyqTYttObQ02A8vV6dzOrhoaSfKvbeCRb4dXXZrD6BY8me5uaHfAimIjzGq8U5psYRUpZxiyfIJnmH97XMYPS3I7BHU5P7EpqpQmrpX6QlWzNcZ6fRKA4qM2fU1LagNWO+AZOEfGzCKzvJoMWDS4A+bT1IJvMOymF8BdFWhawLdLZK0gXnhhXozjKr/17cQeVzgV4GUNrv4rUFusTEWOdotLRbRC/QqToduND7PDJJgAYEZP
22+
template:
23+
metadata:
24+
annotations:
25+
sealedsecrets.bitnami.com/namespace-wide: 'true'
26+
name: smtp-app
27+
type: Opaque

.kontinuous/values.yaml

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,47 @@ app:
8181
secretKeyRef:
8282
name: "{{ .Values.global.pgSecretName }}"
8383
key: PGSSLMODE
84+
# SMTP (Tipimail in prod, MailDev on dev/preprod). The smtp-app
85+
# sealed-secret is kept as-is from the V1 prod setup, with MAILER_*
86+
# keys. We remap to the V2 SMTP_* names expected by src/env.js.
87+
#
88+
# Kubernetes precedence: `env` runs AFTER `envFrom`, so an `env` entry
89+
# would normally override a same-named envFrom value. But with
90+
# `optional: true` on a missing secret/key, the env entry is silently
91+
# dropped and the envFrom value is preserved. That is exactly the
92+
# fallback we rely on for dev/preprod review apps (no smtp-app
93+
# secret) where SMTP_HOST, SMTP_PORT, etc. come from the `mail`
94+
# configmap pointing at the in-cluster MailDev service.
95+
- name: SMTP_HOST
96+
valueFrom:
97+
secretKeyRef:
98+
name: smtp-app
99+
key: MAILER_SMTP_HOST
100+
optional: true
101+
- name: SMTP_PORT
102+
valueFrom:
103+
secretKeyRef:
104+
name: smtp-app
105+
key: MAILER_SMTP_PORT
106+
optional: true
107+
- name: SMTP_USER
108+
valueFrom:
109+
secretKeyRef:
110+
name: smtp-app
111+
key: MAILER_SMTP_LOGIN
112+
optional: true
113+
- name: SMTP_PASS
114+
valueFrom:
115+
secretKeyRef:
116+
name: smtp-app
117+
key: MAILER_SMTP_PASSWORD
118+
optional: true
119+
- name: SMTP_SECURE
120+
valueFrom:
121+
secretKeyRef:
122+
name: smtp-app
123+
key: MAILER_SMTP_SSL
124+
optional: true
84125
vars:
85126
NEXTAUTH_URL: "https://{{ .Values.global.host }}/api/auth"
86127
NEXT_PUBLIC_EGAPRO_ENV: "{{ .Values.global.env }}"

packages/app/src/env.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,10 @@ export const env = createEnv({
8888
SMTP_PORT: z.coerce.number().int().positive().default(1025),
8989
SMTP_USER: z.string().optional(),
9090
SMTP_PASS: z.string().optional(),
91+
SMTP_SECURE: z
92+
.string()
93+
.default("false")
94+
.transform((v) => v.toLowerCase() === "true"),
9195
MAIL_FROM: z.string().default("no-reply@egapro.local"),
9296
// Valkey cache URL — optional. When absent, the custom
9397
// cache handler (cache-handler.cjs) gracefully degrades to no-op.
@@ -150,6 +154,7 @@ export const env = createEnv({
150154
SMTP_PORT: process.env.SMTP_PORT,
151155
SMTP_USER: process.env.SMTP_USER,
152156
SMTP_PASS: process.env.SMTP_PASS,
157+
SMTP_SECURE: process.env.SMTP_SECURE,
153158
MAIL_FROM: process.env.MAIL_FROM,
154159
VALKEY_URL: process.env.VALKEY_URL,
155160
NEXT_PUBLIC_EGAPRO_ENV: process.env.NEXT_PUBLIC_EGAPRO_ENV,

packages/app/src/modules/mail/transporter.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ export function getTransporter(): Transporter {
1515
cachedTransporter = nodemailer.createTransport({
1616
host: env.SMTP_HOST,
1717
port: env.SMTP_PORT,
18-
secure: false,
18+
secure: env.SMTP_SECURE,
1919
auth,
2020
});
2121

0 commit comments

Comments
 (0)