From 5b4a5b87f21b1a0324b2f7459a3d947b909d560c Mon Sep 17 00:00:00 2001
From: Janne Mareike Koschinski <mail@justjanne.de>
Date: Tue, 13 May 2025 20:18:49 +0200
Subject: [PATCH] feat: add stalwart

---
 stalwart/Chart.yaml                 |   6 ++
 stalwart/pipeline.yml               |  21 ++++++
 stalwart/templates/_helpers.tpl     |  56 +++++++++++++++
 stalwart/templates/configmap.yaml   | 106 ++++++++++++++++++++++++++++
 stalwart/templates/ingress.yaml     |  52 ++++++++++++++
 stalwart/templates/service.yaml     |  56 +++++++++++++++
 stalwart/templates/statefulset.yaml | 101 ++++++++++++++++++++++++++
 stalwart/values.yaml                |  75 ++++++++++++++++++++
 8 files changed, 473 insertions(+)
 create mode 100644 stalwart/Chart.yaml
 create mode 100644 stalwart/pipeline.yml
 create mode 100644 stalwart/templates/_helpers.tpl
 create mode 100644 stalwart/templates/configmap.yaml
 create mode 100644 stalwart/templates/ingress.yaml
 create mode 100644 stalwart/templates/service.yaml
 create mode 100644 stalwart/templates/statefulset.yaml
 create mode 100644 stalwart/values.yaml

diff --git a/stalwart/Chart.yaml b/stalwart/Chart.yaml
new file mode 100644
index 0000000..468d261
--- /dev/null
+++ b/stalwart/Chart.yaml
@@ -0,0 +1,6 @@
+apiVersion: v2
+name: stalwart
+description: Helm Chart for stalwart
+type: application
+version: 1.0.0
+appVersion: "latest"
diff --git a/stalwart/pipeline.yml b/stalwart/pipeline.yml
new file mode 100644
index 0000000..2582135
--- /dev/null
+++ b/stalwart/pipeline.yml
@@ -0,0 +1,21 @@
+lint-stalwart:
+  stage: lint
+  rules:
+    - changes:
+        - stalwart/**/*
+  script:
+    - helm lint stalwart
+
+release-stalwart:
+  stage: release
+  needs:
+    - lint-stalwart
+  rules:
+    - if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'
+      changes:
+        - stalwart/**/*
+  script:
+    - apk add --no-cache git
+    - helm plugin install https://github.com/chartmuseum/helm-push.git
+    - helm repo add --username gitlab-ci-token --password $CI_JOB_TOKEN repo ${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/packages/helm/stable
+    - helm cm-push stalwart repo
diff --git a/stalwart/templates/_helpers.tpl b/stalwart/templates/_helpers.tpl
new file mode 100644
index 0000000..c614d9a
--- /dev/null
+++ b/stalwart/templates/_helpers.tpl
@@ -0,0 +1,56 @@
+{{/*
+Expand the name of the chart.
+*/}}
+{{- define "stalwart-helm.name" -}}
+{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
+{{- end }}
+
+{{/*
+Create a default fully qualified app name.
+We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
+If release name contains chart name it will be used as a full name.
+*/}}
+{{- define "stalwart-helm.fullname" -}}
+{{- if .Values.fullnameOverride }}
+{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
+{{- else }}
+{{- $name := default .Chart.Name .Values.nameOverride }}
+{{- if contains $name .Release.Name }}
+{{- .Release.Name | trunc 63 | trimSuffix "-" }}
+{{- else }}
+{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
+{{- end }}
+{{- end }}
+{{- end }}
+
+{{/*
+Create chart name and version as used by the chart label.
+*/}}
+{{- define "stalwart-helm.chart" -}}
+{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
+{{- end }}
+
+{{/*
+Common labels
+*/}}
+{{- define "stalwart-helm.labels" -}}
+helm.sh/chart: {{ include "stalwart-helm.chart" . }}
+{{ include "stalwart-helm.selectorLabels" . }}
+{{- if .Chart.AppVersion }}
+app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
+{{- end }}
+app.kubernetes.io/managed-by: {{ .Release.Service }}
+{{- end }}
+
+{{/*
+Selector labels
+*/}}
+{{- define "stalwart-helm.selectorLabels" -}}
+app.kubernetes.io/name: {{ include "stalwart-helm.name" . }}
+app.kubernetes.io/instance: {{ .Release.Name }}
+{{- end }}
+
+
+{{- define "stalwart-helm.sslPath" -}}
+/certs
+{{- end }}
diff --git a/stalwart/templates/configmap.yaml b/stalwart/templates/configmap.yaml
new file mode 100644
index 0000000..21370e1
--- /dev/null
+++ b/stalwart/templates/configmap.yaml
@@ -0,0 +1,106 @@
+apiVersion: v1
+kind: ConfigMap
+metadata:
+  name: {{ include "stalwart-helm.fullname" . }}
+  labels:
+    {{- include "stalwart-helm.labels" . | nindent 4 }}
+data:
+  config.toml: |
+    [server]
+    hostname = {{ .Values.mail.hostname | toJson }}
+
+    listener.http.bind = "[::]:8080"
+    listener.http.protocol = "http"
+
+    listener.https.bind = "[::]:443"
+    listener.https.protocol = "http"
+    listener.https.tls.implicit = true
+
+    listener.imap.bind = "[::]:143"
+    listener.imap.protocol = "imap"
+    listener.imap.proxy.trusted-networks = {{ .Values.trustedNetworks | toJson }}
+
+    listener.imaptls.bind = "[::]:993"
+    listener.imaptls.protocol = "imap"
+    listener.imaptls.tls.implicit = true
+    listener.imaptls.proxy.trusted-networks = {{ .Values.trustedNetworks | toJson }}
+
+    listener.pop3.bind = "[::]:110"
+    listener.pop3.protocol = "pop3"
+    listener.pop3.proxy.trusted-networks = {{ .Values.trustedNetworks | toJson }}
+
+    listener.pop3s.bind = "[::]:995"
+    listener.pop3s.protocol = "pop3"
+    listener.pop3s.tls.implicit = true
+    listener.pop3s.proxy.trusted-networks = {{ .Values.trustedNetworks | toJson }}
+
+    listener.sieve.bind = "[::]:4190"
+    listener.sieve.protocol = "managesieve"
+    listener.sieve.proxy.trusted-networks = {{ .Values.trustedNetworks | toJson }}
+
+    listener.smtp.bind = "[::]:25"
+    listener.smtp.protocol = "smtp"
+    listener.smtp.proxy.trusted-networks = {{ .Values.trustedNetworks | toJson }}
+
+    listener.submission.bind = "[::]:587"
+    listener.submission.protocol = "smtp"
+    listener.submission.proxy.trusted-networks = {{ .Values.trustedNetworks | toJson }}
+
+    listener.submissions.bind = "[::]:465"
+    listener.submissions.protocol = "smtp"
+    listener.submissions.tls.implicit = true
+    listener.submissions.proxy.trusted-networks = {{ .Values.trustedNetworks | toJson }}
+
+    max-connections = 8192
+    socket.backlog = 1024
+    socket.nodelay = true
+    socket.reuse-addr = true
+    socket.reuse-port = true
+
+    http.url = "protocol + '://' + '{{ .Values.ingress.endpoint }}' + ':' + local_port"
+    http.use-x-forwarded = true
+
+    [certificate.default]
+    default = true
+    cert = "%{file:/etc/certs/{{.Values.tls.certificateFile}}}%"
+    private-key = "%{file:/etc/certs/{{.Values.tls.privateKeyFile}}}%"
+
+    [tracer]
+
+    [tracer.log]
+    ansi = false
+    enable = false
+    level = "info"
+    path = "/opt/stalwart-mail/logs"
+    prefix = "stalwart.log"
+    rotate = "daily"
+    type = "log"
+
+    [directory]
+    {{- range $id, $config := .Values.directory }}
+
+    [directory.{{ $id }}]
+    {{- range $key, $value := $config }}
+    {{ $key }} = {{ $value | toJson }}
+    {{- end }}
+    {{- end }}
+
+    [storage]
+    {{- range $key, $value := .Values.storage }}
+    {{ $key }} = {{ $value | toJson }}
+    {{- end }}
+
+    [store]
+    {{- range $id, $config := .Values.store }}
+
+    [store.{{ $id }}]
+    {{- range $key, $value := $config }}
+    {{ $key }} = {{ $value | toJson }}
+    {{- end }}
+    {{- end }}
+
+    [authentication.fallback-admin]
+    {{- range $key, $value := .Values.fallbackAdmin }}
+    {{ $key }} = {{ $value | toJson }}
+    {{- end }}
+
diff --git a/stalwart/templates/ingress.yaml b/stalwart/templates/ingress.yaml
new file mode 100644
index 0000000..775048f
--- /dev/null
+++ b/stalwart/templates/ingress.yaml
@@ -0,0 +1,52 @@
+apiVersion: networking.k8s.io/v1
+kind: Ingress
+metadata:
+  name: {{ include "stalwart-helm.fullname" . }}
+  labels:
+    {{- include "stalwart-helm.labels" . | nindent 4 }}
+  annotations:
+    {{- .Values.ingress.annotations | toYaml | nindent 4 }}
+spec:
+  ingressClassName: {{ .Values.ingress.className }}
+  rules:
+    - host: {{ .Values.ingress.endpoint }}
+      http:
+        paths:
+          - backend:
+              service:
+                name: {{ include "stalwart-helm.fullname" . }}
+                port:
+                  name: https
+            path: /
+            pathType: Prefix
+{{- range .Values.mail.domains }}
+    - host: mta-sts.{{ . }}
+      http:
+        paths:
+          - backend:
+              service:
+                name: {{ include "stalwart-helm.fullname" $ }}
+                port:
+                  name: https
+            path: /
+            pathType: Prefix
+    - host: {{ . }}
+      http:
+        paths:
+          - backend:
+              service:
+                name: {{ include "stalwart-helm.fullname" $ }}
+                port:
+                  name: https
+            path: /.well-known/autoconfig
+            pathType: ImplementationSpecific
+          - backend:
+              service:
+                name: {{ include "stalwart-helm.fullname" $ }}
+                port:
+                  name: https
+            path: /.well-known/mta-sts.txt
+            pathType: ImplementationSpecific
+{{- end }}
+  tls:
+    - secretName: {{ .Values.tls.existingSecret }}
\ No newline at end of file
diff --git a/stalwart/templates/service.yaml b/stalwart/templates/service.yaml
new file mode 100644
index 0000000..7a23318
--- /dev/null
+++ b/stalwart/templates/service.yaml
@@ -0,0 +1,56 @@
+apiVersion: v1
+kind: Service
+metadata:
+  name: {{ include "stalwart-helm.fullname" . }}
+  labels:
+    {{- include "stalwart-helm.labels" . | nindent 4 }}
+spec:
+  type: {{ .Values.service.type }}
+  ports:
+    # smtp
+    - name: submission
+      port: 25
+      targetPort: submission
+      protocol: TCP
+    - name: smtp
+      port: 587
+      targetPort: smtp
+      protocol: TCP
+    - name: smtps
+      port: 465
+      targetPort: smtps
+      protocol: TCP
+    # imap
+    - name: imap
+      port: 143
+      targetPort: imap
+      protocol: TCP
+    - name: imaps
+      port: 993
+      targetPort: imaps
+      protocol: TCP
+    # pop3
+    - name: pop3
+      port: 110
+      targetPort: pop3
+      protocol: TCP
+    - name: pop3s
+      port: 995
+      targetPort: pop3s
+      protocol: TCP
+    # sieve
+    - name: sieve
+      port: 4190
+      targetPort: sieve
+      protocol: TCP
+    # http
+    - name: https
+      port: 443
+      targetPort: https
+      protocol: TCP
+    - name: http
+      port: 8080
+      targetPort: http
+      protocol: TCP
+  selector:
+    {{- include "stalwart-helm.selectorLabels" . | nindent 4 }}
diff --git a/stalwart/templates/statefulset.yaml b/stalwart/templates/statefulset.yaml
new file mode 100644
index 0000000..a8ce6ae
--- /dev/null
+++ b/stalwart/templates/statefulset.yaml
@@ -0,0 +1,101 @@
+apiVersion: apps/v1
+kind: StatefulSet
+metadata:
+  name: {{ include "stalwart-helm.fullname" . }}
+  labels:
+    {{- include "stalwart-helm.labels" . | nindent 4 }}
+spec:
+  replicas: {{ .Values.replicaCount }}
+  serviceName: {{ include "stalwart-helm.fullname" . }}
+  selector:
+    matchLabels:
+      {{- include "stalwart-helm.selectorLabels" . | nindent 6 }}
+  template:
+    metadata:
+      {{- with .Values.podAnnotations }}
+      annotations:
+        {{- toYaml . | nindent 8 }}
+      {{- end }}
+      labels:
+        {{- include "stalwart-helm.selectorLabels" . | nindent 8 }}
+    spec:
+      {{- with .Values.imagePullSecrets }}
+      imagePullSecrets:
+        {{- toYaml . | nindent 8 }}
+      {{- end }}
+      securityContext:
+        {{- toYaml .Values.podSecurityContext | nindent 8 }}
+      volumes:
+        - name: config
+          configMap:
+            defaultMode: 0640
+            name: {{ include "stalwart-helm.fullname" . }}
+        - name: certs
+          secret:
+            defaultMode: 0640
+            secretName: {{ .Values.tls.existingSecret }}
+        - name: tmp
+          emptyDir: {}
+      containers:
+        - name: {{ .Chart.Name }}
+          image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
+          imagePullPolicy: {{ .Values.image.pullPolicy }}
+          ports:
+          # smtp
+          - containerPort: 25
+            protocol: TCP
+            name: submission
+          - containerPort: 587
+            protocol: TCP
+            name: smtp
+          - containerPort: 465
+            protocol: TCP
+            name: smtps
+          # imap
+          - containerPort: 143
+            protocol: TCP
+            name: imap
+          - containerPort: 993
+            protocol: TCP
+            name: imaps
+          # pop3
+          - containerPort: 110
+            protocol: TCP
+            name: pop3
+          - containerPort: 995
+            protocol: TCP
+            name: pop3s
+          # sieve
+          - containerPort: 4190
+            protocol: TCP
+            name: sieve
+          # http
+          - containerPort: 443
+            protocol: TCP
+            name: https
+          - containerPort: 8080
+            protocol: TCP
+            name: http
+          resources:
+            {{- toYaml .Values.resources | nindent 12 }}
+          securityContext:
+            {{- toYaml .Values.securityContext | nindent 12 }}
+          volumeMounts:
+            - mountPath: /opt/stalwart-mail/etc
+              name: config
+            - mountPath: /etc/certs
+              name: certs
+            - mountPath: /tmp
+              name: tmp
+      {{- with .Values.nodeSelector }}
+      nodeSelector:
+        {{- toYaml . | nindent 8 }}
+      {{- end }}
+      {{- with .Values.affinity }}
+      affinity:
+        {{- toYaml . | nindent 8 }}
+      {{- end }}
+      {{- with .Values.tolerations }}
+      tolerations:
+        {{- toYaml . | nindent 8 }}
+      {{- end }}
diff --git a/stalwart/values.yaml b/stalwart/values.yaml
new file mode 100644
index 0000000..175d742
--- /dev/null
+++ b/stalwart/values.yaml
@@ -0,0 +1,75 @@
+replicaCount: 1
+
+image:
+  repository: stalwartlabs/mail-server
+  pullPolicy: IfNotPresent
+  tag: ""
+
+imagePullSecrets: [ ]
+nameOverride: ""
+fullnameOverride: ""
+
+mail:
+  hostname: example.com
+  domains:
+    - example.com
+    - mail.example.com
+
+trustedNetworks: [ ]
+
+fallbackAdmin:
+  user: admin
+  secret: ""
+
+directory:
+  internal:
+    bind.dn: admin
+    bind.secret: ""
+    store: database
+    type: internal
+
+storage: {}
+store: {}
+
+ingress:
+  endpoint: mail.example.com
+  className: nginx
+  annotations: { }
+
+tls:
+  existingSecret: ""
+  certificateFile: "tls.crt"
+  privateKeyFile: "tls.key"
+
+podAnnotations: { }
+
+podSecurityContext:
+  fsGroup: 2000
+
+securityContext:
+  #capabilities:
+  #  drop:
+  #    - ALL
+  readOnlyRootFilesystem: true
+  #runAsNonRoot: true
+  #runAsUser: 1000
+
+service:
+  type: ClusterIP
+
+volume: |-
+  emptyDir: {}
+
+resources:
+  limits:
+    cpu: 500m
+    memory: 512Mi
+  requests:
+    cpu: 20m
+    memory: 64Mi
+
+nodeSelector: { }
+
+tolerations: [ ]
+
+affinity: { }
-- 
GitLab