Stalwart vérification serveur

Stalwart vérification serveur

Notes › _DIFFUSION › Stalwart

Vérification manuelle serveur messagerie

Linux, outils ligne de commande, liens web pour faire une vérification complète de mon serveur de messagerie “Stalwart web ui”

  • DOMAINE OVH yannig.net
  • MX yannig.net
  • IMAP SSL/TLS Port 993
  • SMTP SSL/TLS Port 465

Pour vérifier complètement votre serveur Stalwart associé à yannig.net, contrôlez séparément DNS, routage SMTP, certificats TLS, IMAP, authentification SPF/DKIM/DMARC, réputation et configuration Stalwart. Les ports 993/IMAPS et 465/SMTPS sont adaptés aux clients, mais le port 25 doit également être testé pour la réception et la livraison entre serveurs.

1. Vérification DNS depuis Linux

Effectuez ces commandes depuis une machine externe au serveur, si possible un VPS ou une connexion différente :

1
2
3
4
5
6
7
8
9
10
DOMAIN=yannig.net

dig "$DOMAIN" A +short
dig "$DOMAIN" AAAA +short
dig "$DOMAIN" MX +short
dig "$DOMAIN" NS +short
dig "$DOMAIN" CAA +short
dig "$DOMAIN" TXT +short
dig "_dmarc.$DOMAIN" TXT +short
dig "$DOMAIN" DNSKEY +dnssec

Vérifiez en particulier :

  • Le MX de yannig.net doit pointer vers un nom d’hôte stable, par exemple mail.yannig.net.
  • Le nom indiqué par le MX doit posséder un enregistrement A et éventuellement AAAA.
  • L’adresse IPv4 publique doit avoir un reverse DNS cohérent.
  • Il ne doit exister qu’un seul enregistrement SPF v=spf1.
  • Le domaine doit posséder un enregistrement DMARC.
  • Le sélecteur DKIM doit être récupéré dans l’interface Stalwart, puis testé ainsi :
1
dig TXT SELECTEUR._domainkey.yannig.net +short

Remplacez SELECTEUR par le sélecteur réellement affiché par Stalwart.

Pour connaître le reverse DNS de l’adresse IPv4 :

1
dig -x ADRESSE_IPV4 +short

Le résultat devrait être un nom tel que mail.yannig.net, et ce nom devrait ensuite résoudre vers la même adresse IP :

1
dig mail.yannig.net A +short

Stalwart documente les enregistrements nécessaires pour MX, SPF, DKIM, DMARC, TLSA et les autres mécanismes DNS de messagerie.

2. Vérification des ports réseau

Depuis une machine extérieure :

1
2
3
4
nc -vz -w5 yannig.net 25
nc -vz -w5 yannig.net 465
nc -vz -w5 yannig.net 587
nc -vz -w5 yannig.net 993

Résultat attendu :

1
succeeded

Le port 25 est important même si vous n’utilisez pas ce port dans Thunderbird ou K-9 Mail : les autres serveurs SMTP l’utilisent pour remettre les messages à votre serveur.

Vous pouvez obtenir davantage d’informations avec Nmap :

1
nmap -Pn -sV -p 25,465,587,993 yannig.net

Puis analyser les suites cryptographiques :

1
2
3
nmap -Pn -p 465,993 \
  --script ssl-enum-ciphers \
  yannig.net

Ne vous limitez pas à un test exécuté depuis le serveur lui-même : un pare-feu local, Docker, OVH ou un routeur peut autoriser les connexions locales tout en bloquant l’accès public.

3. Test des certificats TLS

SMTP implicite sur 465

Le port 465 démarre directement en TLS :

1
2
3
4
5
6
openssl s_client \
  -connect yannig.net:465 \
  -servername yannig.net \
  -showcerts \
  -verify_return_error \
  -brief </dev/null

IMAP implicite sur 993

1
2
3
4
5
6
openssl s_client \
  -connect yannig.net:993 \
  -servername yannig.net \
  -showcerts \
  -verify_return_error \
  -brief </dev/null

Contrôlez notamment :

  • Verification: OK.
  • Le certificat n’est pas expiré.
  • Le certificat contient yannig.net dans le SAN.
  • La chaîne complète est fournie.
  • Le certificat présenté sur 465 et 993 est bien celui attendu.
  • TLS 1.2 ou TLS 1.3 est disponible.
  • TLS 1.0 et TLS 1.1 sont désactivés.

Pour vérifier explicitement les protocoles :

1
2
3
4
5
6
7
8
for version in tls1 tls1_1 tls1_2 tls1_3; do
    echo "===== $version ====="
    openssl s_client \
      -connect yannig.net:465 \
      -servername yannig.net \
      -$version </dev/null 2>&1 | grep -E \
      'Protocol|Cipher|alert|handshake failure'
done

Pour le port SMTP 587, le mode correct est généralement STARTTLS :

1
2
3
4
5
6
openssl s_client \
  -connect yannig.net:587 \
  -servername yannig.net \
  -starttls smtp \
  -verify_return_error \
  -brief </dev/null

Le même test peut être effectué sur le port 25 :

1
2
3
4
5
6
openssl s_client \
  -connect yannig.net:25 \
  -servername yannig.net \
  -starttls smtp \
  -verify_return_error \
  -brief </dev/null

La commande openssl s_client -connect hôte:port convient aux connexions TLS implicites comme 465 et 993, tandis que -starttls smtp est nécessaire pour tester STARTTLS sur 25 ou 587.

4. Vérification du protocole IMAP

Pour vérifier que le serveur ne fait pas uniquement une négociation TLS mais répond bien en IMAP :

1
2
3
4
5
6
printf 'a001 CAPABILITY\r\na002 LOGOUT\r\n' |
  openssl s_client \
    -connect yannig.net:993 \
    -servername yannig.net \
    -quiet \
    -crlf

Vous devez obtenir une réponse contenant notamment une ligne similaire à :

1
* CAPABILITY IMAP4rev1 ...

Testez ensuite l’authentification uniquement avec un compte de test :

1
2
3
4
openssl s_client \
  -connect yannig.net:993 \
  -servername yannig.net \
  -crlf

Une fois connecté, utilisez les commandes IMAP suivantes :

1
2
3
4
a001 CAPABILITY
a002 LOGIN utilisateur@yannig.net MOT_DE_PASSE_DE_TEST
a003 LIST "" "*"
a004 LOGOUT

N’utilisez pas votre compte administrateur et ne laissez pas le mot de passe dans l’historique du shell.

5. Vérification SMTP authentifiée

Installez éventuellement swaks :

1
sudo apt install swaks

Pour tester l’envoi authentifié sur 465 :

1
2
3
4
5
6
7
8
swaks \
  --server yannig.net \
  --port 465 \
  --tls \
  --auth LOGIN \
  --auth-user utilisateur@yannig.net \
  --to adresse-externe@example.net \
  --from utilisateur@yannig.net

swaks demandera normalement le mot de passe de manière interactive selon votre version. Utilisez une boîte de test et une adresse externe que vous contrôlez.

Testez aussi la soumission sur 587 :

1
2
3
4
5
6
7
8
swaks \
  --server yannig.net \
  --port 587 \
  --tls \
  --auth LOGIN \
  --auth-user utilisateur@yannig.net \
  --to adresse-externe@example.net \
  --from utilisateur@yannig.net

À vérifier dans les résultats :

  • Le certificat TLS est valide.
  • L’authentification échoue avec un mauvais mot de passe.
  • L’envoi sans authentification depuis 465/587 est refusé.
  • Le serveur n’est pas un relais ouvert.
  • L’adresse d’expéditeur est correctement contrôlée.
  • Le message reçu contient Authentication-Results avec SPF, DKIM et DMARC.

Pour tester rapidement l’absence de relais ouvert, utilisez :

1
2
3
4
5
swaks \
  --server yannig.net \
  --port 25 \
  --from test-externe@example.net \
  --to autre-adresse-externe@example.net

Ce test ne doit pas aboutir sans authentification lorsqu’il s’agit d’un envoi non autorisé.

6. SPF, DKIM et DMARC

SPF

1
dig TXT yannig.net +short

Il doit y avoir une politique SPF, par exemple :

1
v=spf1 mx ip4:ADRESSE_IPV4 -all

Adaptez-la aux véritables serveurs autorisés à envoyer des messages. Évitez d’avoir plusieurs TXT SPF différents.

DKIM

Depuis l’interface Stalwart, récupérez le sélecteur et la clé publique. Puis :

1
dig TXT SELECTEUR._domainkey.yannig.net +short

Envoyez ensuite un message vers une boîte Gmail, Outlook ou une autre boîte de test, puis affichez les en-têtes complets. Vous devez trouver une ligne indiquant une signature DKIM valide, par exemple :

1
dkim=pass

Stalwart génère et signe les messages sortants avec une clé DKIM dont la clé publique doit être publiée sous SELECTEUR._domainkey.yannig.net.

DMARC

1
dig TXT _dmarc.yannig.net +short

Pour commencer, une politique de surveillance peut ressembler à :

1
v=DMARC1; p=none; rua=mailto:dmarc@yannig.net

Après observation des rapports et correction des erreurs SPF/DKIM, vous pourrez évoluer vers :

1
v=DMARC1; p=quarantine; rua=mailto:dmarc@yannig.net

Puis éventuellement :

1
v=DMARC1; p=reject; rua=mailto:dmarc@yannig.net

Ne passez pas directement à p=reject si vous n’avez pas vérifié tous les systèmes qui envoient des messages pour yannig.net.

7. Tests web recommandés

Utilisez les services suivants avec yannig.net :

  1. Internet.nl – test de messagerie Vérifie notamment IPv6, DNSSEC, SPF, DKIM, DMARC, STARTTLS, DANE, RPKI et la sécurité TLS.
  2. MXToolbox Testez successivement MX, SPF, DMARC, DKIM, les listes noires et le DNS. MXToolbox fournit des vérificateurs spécialisés pour ces enregistrements.
  3. Mail-Tester Générez une adresse temporaire, envoyez-lui un message depuis Stalwart et analysez le score ainsi que les résultats SPF, DKIM, DMARC, reverse DNS et réputation. Le test doit être effectué avec votre véritable serveur d’envoi.
  4. DNSRobot – test SMTP Utile pour contrôler la bannière SMTP, les commandes EHLO, STARTTLS et les ports 25, 465 et 587.
  5. Mail SSL Checker Vérifie directement les certificats et TLS sur 465, 993, ainsi que STARTTLS sur les ports compatibles.

Les résultats d’Internet.nl sont particulièrement utiles pour une vue globale, car le service produit un rapport avec un score général et le détail de chaque sous-test.[^4]

8. Contrôles spécifiques Stalwart

Dans l’interface WebAdmin de Stalwart, vérifiez :

  • Domaine yannig.net présent et activé.
  • Compte administrateur distinct des comptes de messagerie.
  • Certificat TLS associé au bon nom d’hôte.
  • Services IMAP, SMTP et éventuellement JMAP activés uniquement sur les interfaces nécessaires.
  • Authentification SMTP obligatoire pour 465 et 587.
  • Relaying sortant limité aux utilisateurs authentifiés.
  • DKIM activé pour les messages sortants.
  • Vérification SPF, DKIM et DMARC activée pour les messages entrants.
  • Limitation de débit et protection contre les abus.
  • Journaux d’authentification et de remise surveillés.
  • File d’attente SMTP vide ou sans erreurs répétées.
  • Aucun compte ou alias compromis.

Suivez les journaux pendant un test :

1
sudo journalctl -fu stalwart

Si Stalwart fonctionne dans Docker :

1
docker compose logs -f --tail=200 stalwart

ou :

1
docker logs -f --tail=200 NOM_DU_CONTENEUR_STALWART

Enfin, contrôlez les règles de pare-feu :

1
2
sudo ss -lntp | grep -E ':(25|465|587|993)\b'
sudo nft list ruleset

La configuration minimale attendue est donc : 25 accessible pour les échanges SMTP entrants, 465 en TLS implicite pour les clients, 587 en STARTTLS pour la soumission, et 993 en TLS implicite pour IMAP. Le certificat doit correspondre exactement au nom utilisé par le MX et par les clients, idéalement mail.yannig.net plutôt que le domaine nu si votre MX pointe vers ce nom.

Script vérification multi-domaines

Enregistrement MX directement vers yannig.net. est valide dans votre cas, puisque le domaine possède lui-même les enregistrements A et AAAA. Le certificat TLS présenté sur les ports 465 et 993 devra donc contenir yannig.net dans son SAN, et non obligatoirement mail.yannig.net.

Le script ci-dessous vérifie le DNS, le reverse DNS, les ports IPv4/IPv6, les certificats TLS, SMTP, IMAP, SPF, DMARC et DKIM si vous fournissez le sélecteur DKIM. Il reprend vos adresses 51.38.37.240 et 2001:41d0:305:2100::c1c.

Il faut maintenant distinguer le serveur de transport commun (yannig.net) des trois domaines signés. Le MX peut rester identique pour les trois domaines :

1
2
3
yannig.net.  MX  10 yannig.net.
cinay.eu.    MX  10 yannig.net.
yick.eu.     MX  10 yannig.net.

Les six sélecteurs DKIM doivent être testés indépendamment. Pour DKIM, ed25519 et rsa sont indiqués dans le champ k= ; le terme « SHA-256 » correspond à l’algorithme de signature, pas au type de clé DNS. Le type Ed25519 est officiellement identifié par k=ed25519.rfc-editor

Ce script vérifie :

  • les enregistrements A, AAAA et MX ;
  • les reverse DNS IPv4/IPv6 ;
  • SPF et DMARC pour les trois domaines ;
  • les deux sélecteurs DKIM de chaque domaine ;
  • le type de clé ed25519 ou rsa ;
  • les ports 25, 465, 587 et 993 ;
  • TLS implicite sur 465 et 993 ;
  • STARTTLS sur 25 et 587 ;
  • les capacités SMTP et IMAP ;
  • la correspondance du certificat avec yannig.net.

    Script complet

Enregistrez-le dans check-stalwart-multi-domain.sh :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
#!/usr/bin/env bash

set -u
set -o pipefail

readonly MAIL_HOST="yannig.net"
readonly EXPECTED_IPV4="51.38.37.240"
readonly EXPECTED_IPV6="2001:41d0:305:2100::c1c"

readonly SMTP_PORT=25
readonly SMTPS_PORT=465
readonly SUBMISSION_PORT=587
readonly IMAPS_PORT=993

readonly REPORT_DIR="./stalwart-report-$(date +%Y%m%d-%H%M%S)"
readonly REPORT_FILE="$REPORT_DIR/report.txt"

readonly DOMAINS=(
    "yannig.net"
    "cinay.eu"
    "yick.eu"
)

declare -A DKIM_SELECTORS=(
    ["yannig.net"]="v1-ed25519-20260720 v1-rsa-20260720"
    ["cinay.eu"]="v1-ed25519-20260723 v1-rsa-20260723"
    ["yick.eu"]="v1-ed25519-20260722 v1-rsa-20260722"
)

mkdir -p "$REPORT_DIR"
touch "$REPORT_FILE"

PASS=0
WARN=0
FAIL=0

GREEN=$'\033[32m'
YELLOW=$'\033[33m'
RED=$'\033[31m'
BLUE=$'\033[34m'
RESET=$'\033[0m'

log() {
    local level="$1"
    shift
    local message="$*"

    case "$level" in
        PASS)
            printf '%b[PASS]%b %s\n' "$GREEN" "$RESET" "$message"
            printf '[PASS] %s\n' "$message" >>"$REPORT_FILE"
            PASS=$((PASS + 1))
            ;;
        WARN)
            printf '%b[WARN]%b %s\n' "$YELLOW" "$RESET" "$message"
            printf '[WARN] %s\n' "$message" >>"$REPORT_FILE"
            WARN=$((WARN + 1))
            ;;
        FAIL)
            printf '%b[FAIL]%b %s\n' "$RED" "$RESET" "$message"
            printf '[FAIL] %s\n' "$message" >>"$REPORT_FILE"
            FAIL=$((FAIL + 1))
            ;;
        INFO)
            printf '%b[INFO]%b %s\n' "$BLUE" "$RESET" "$message"
            printf '[INFO] %s\n' "$message" >>"$REPORT_FILE"
            ;;
    esac
}

section() {
    printf '\n===== %s =====\n' "$1" | tee -a "$REPORT_FILE"
}

require_command() {
    if ! command -v "$1" >/dev/null 2>&1; then
        log FAIL "Commande manquante : $1"
        return 1
    fi
}

dns_short() {
    local type="$1"
    local name="$2"
    dig +short "$name" "$type" 2>/dev/null
}

check_dns_domain() {
    local domain="$1"
    local a aaaa mx txt spf dmarc

    section "DNS $domain"

    a="$(dns_short A "$domain")"
    aaaa="$(dns_short AAAA "$domain")"
    mx="$(dns_short MX "$domain")"
    txt="$(dns_short TXT "$domain")"
    spf="$(grep -i 'v=spf1' <<<"$txt" || true)"
    dmarc="$(dns_short TXT "_dmarc.$domain")"

    {
        echo "[$domain]"
        echo "A:"
        echo "$a"
        echo "AAAA:"
        echo "$aaaa"
        echo "MX:"
        echo "$mx"
        echo "TXT:"
        echo "$txt"
        echo "SPF:"
        echo "$spf"
        echo "DMARC:"
        echo "$dmarc"
    } >>"$REPORT_FILE"

    if [[ "$domain" == "$MAIL_HOST" ]]; then
        if grep -Fxq "$EXPECTED_IPV4" <<<"$a"; then
            log PASS "$domain : A = $EXPECTED_IPV4"
        else
            log FAIL "$domain : A différent de $EXPECTED_IPV4"
        fi

        if grep -Fxq "$EXPECTED_IPV6" <<<"$aaaa"; then
            log PASS "$domain : AAAA = $EXPECTED_IPV6"
        else
            log FAIL "$domain : AAAA différent de $EXPECTED_IPV6"
        fi
    fi

    if grep -Eq '(^|[[:space:]])10[[:space:]]+yannig\.net\.?$|yannig\.net\.$' <<<"$mx"; then
        log PASS "$domain : MX vers yannig.net"
    else
        log FAIL "$domain : MX inattendu : ${mx//$'\n'/ }"
    fi

    local spf_count
    spf_count="$(grep -ci 'v=spf1' <<<"$txt" || true)"

    if [[ "$spf_count" -eq 1 ]]; then
        log PASS "$domain : un unique SPF détecté"
    elif [[ "$spf_count" -eq 0 ]]; then
        log FAIL "$domain : SPF absent"
    else
        log FAIL "$domain : plusieurs SPF détectés ($spf_count)"
    fi

    if grep -q 'v=DMARC1' <<<"$dmarc"; then
        log PASS "$domain : DMARC détecté"
    else
        log FAIL "$domain : DMARC absent ou invalide"
    fi
}

check_dkim_domain() {
    local domain="$1"
    local selectors="${DKIM_SELECTORS[$domain]}"
    local selector record expected_type

    section "DKIM $domain"

    for selector in $selectors; do
        record="$(dns_short TXT "${selector}._domainkey.${domain}")"

        printf '[DKIM] %s._domainkey.%s\n%s\n' \
            "$selector" "$domain" "$record" >>"$REPORT_FILE"

        if [[ -z "$record" ]]; then
            log FAIL "$domain / $selector : enregistrement DKIM absent"
            continue
        fi

        if ! grep -q 'v=DKIM1' <<<"$record"; then
            log FAIL "$domain / $selector : v=DKIM1 absent"
        else
            log PASS "$domain / $selector : v=DKIM1 présent"
        fi

        if ! grep -q 'p=[A-Za-z0-9+/=]' <<<"$record"; then
            log FAIL "$domain / $selector : clé publique p= absente"
        else
            log PASS "$domain / $selector : clé publique présente"
        fi

        if [[ "$selector" == *ed25519* ]]; then
            expected_type="ed25519"
        elif [[ "$selector" == *rsa* ]]; then
            expected_type="rsa"
        else
            expected_type=""
        fi

        if [[ -n "$expected_type" ]]; then
            if grep -Eiq "k[[:space:]]*=[[:space:]]*$expected_type" <<<"$record"; then
                log PASS "$domain / $selector : type k=$expected_type confirmé"
            else
                log WARN "$domain / $selector : le sélecteur suggère k=$expected_type, mais le DNS ne le confirme pas"
            fi
        fi
    done
}

check_reverse_dns() {
    section "REVERSE DNS"

    local ptr4 ptr6

    ptr4="$(dig -x "$EXPECTED_IPV4" PTR +short | sed 's/\.$//' | head -n1)"
    ptr6="$(dig -x "$EXPECTED_IPV6" PTR +short | sed 's/\.$//' | head -n1)"

    if [[ -n "$ptr4" ]]; then
        log PASS "PTR IPv4 : $EXPECTED_IPV4 -> $ptr4"

        if dig +short A "$ptr4" | grep -Fxq "$EXPECTED_IPV4"; then
            log PASS "PTR IPv4 confirmé par résolution directe"
        else
            log WARN "Le PTR IPv4 ne revient pas vers $EXPECTED_IPV4"
        fi
    else
        log FAIL "PTR IPv4 absent pour $EXPECTED_IPV4"
    fi

    if [[ -n "$ptr6" ]]; then
        log PASS "PTR IPv6 : $EXPECTED_IPV6 -> $ptr6"

        if dig +short AAAA "$ptr6" | grep -Fxq "$EXPECTED_IPV6"; then
            log PASS "PTR IPv6 confirmé par résolution directe"
        else
            log WARN "Le PTR IPv6 ne revient pas vers $EXPECTED_IPV6"
        fi
    else
        log WARN "PTR IPv6 absent pour $EXPECTED_IPV6"
    fi
}

check_port() {
    local family="$1"
    local port="$2"
    local label="$3"

    if timeout 8 nc "$family" -zv "$MAIL_HOST" "$port" \
        >>"$REPORT_FILE" 2>&1; then
        log PASS "$label : $MAIL_HOST:$port accessible"
    else
        log FAIL "$label : $MAIL_HOST:$port inaccessible"
    fi
}

check_ports() {
    section "PORTS TCP"

    check_port -4 "$SMTP_PORT" "IPv4 SMTP"
    check_port -6 "$SMTP_PORT" "IPv6 SMTP"

    check_port -4 "$SMTPS_PORT" "IPv4 SMTPS"
    check_port -6 "$SMTPS_PORT" "IPv6 SMTPS"

    check_port -4 "$SUBMISSION_PORT" "IPv4 Submission"
    check_port -6 "$SUBMISSION_PORT" "IPv6 Submission"

    check_port -4 "$IMAPS_PORT" "IPv4 IMAPS"
    check_port -6 "$IMAPS_PORT" "IPv6 IMAPS"
}

check_tls() {
    local family="$1"
    local port="$2"
    local mode="$3"
    local label="$4"
    local output
    local family_option

    family_option="$family"

    if [[ "$mode" == "implicit" ]]; then
        output="$(
            timeout 15 openssl s_client \
                "$family_option" \
                -connect "$MAIL_HOST:$port" \
                -servername "$MAIL_HOST" \
                -verify_return_error \
                -brief </dev/null 2>&1
        )"
    else
        output="$(
            timeout 15 openssl s_client \
                "$family_option" \
                -connect "$MAIL_HOST:$port" \
                -servername "$MAIL_HOST" \
                -starttls smtp \
                -verify_return_error \
                -brief </dev/null 2>&1
        )"
    fi

    printf '\n[%s]\n%s\n' "$label" "$output" >>"$REPORT_FILE"

    if grep -q 'Verification: OK' <<<"$output"; then
        log PASS "$label : certificat validé"
    elif grep -qE 'CONNECTION ESTABLISHED|CONNECTED|New, TLS' <<<"$output"; then
        log WARN "$label : TLS actif, mais validation complète non confirmée"
    else
        log FAIL "$label : échec TLS"
    fi

    if grep -qiE 'TLSv1\.[23]' <<<"$output"; then
        log PASS "$label : TLS 1.2 ou TLS 1.3 négocié"
    fi
}

check_tls_all() {
    section "TLS"

    check_tls -4 "$SMTPS_PORT" implicit "IPv4 SMTPS 465"
    check_tls -6 "$SMTPS_PORT" implicit "IPv6 SMTPS 465"

    check_tls -4 "$IMAPS_PORT" implicit "IPv4 IMAPS 993"
    check_tls -6 "$IMAPS_PORT" implicit "IPv6 IMAPS 993"

    check_tls -4 "$SMTP_PORT" starttls "IPv4 SMTP STARTTLS 25"
    check_tls -6 "$SMTP_PORT" starttls "IPv6 SMTP STARTTLS 25"

    check_tls -4 "$SUBMISSION_PORT" starttls "IPv4 Submission STARTTLS 587"
    check_tls -6 "$SUBMISSION_PORT" starttls "IPv6 Submission STARTTLS 587"
}

check_smtp() {
    local family="$1"
    local port="$2"
    local label="$3"
    local output

    output="$(
        timeout 12 openssl s_client \
            "$family" \
            -connect "$MAIL_HOST:$port" \
            -servername "$MAIL_HOST" \
            -quiet \
            -crlf 2>/dev/null <<'EOF'
EHLO check.yannig.net
QUIT
EOF
    )"

    printf '\n[%s]\n%s\n' "$label" "$output" >>"$REPORT_FILE"

    if grep -qiE '220 |250[- ]' <<<"$output"; then
        log PASS "$label : réponse SMTP reçue"
    else
        log WARN "$label : réponse SMTP non détectée"
    fi
}

check_imap() {
    local family="$1"
    local label="$2"
    local output

    output="$(
        timeout 12 openssl s_client \
            "$family" \
            -connect "$MAIL_HOST:$IMAPS_PORT" \
            -servername "$MAIL_HOST" \
            -quiet \
            -crlf 2>/dev/null <<'EOF'
a001 CAPABILITY
a002 LOGOUT
EOF
    )"

    printf '\n[%s]\n%s\n' "$label" "$output" >>"$REPORT_FILE"

    if grep -qiE 'CAPABILITY|IMAP4' <<<"$output"; then
        log PASS "$label : réponse IMAP reçue"
    else
        log FAIL "$label : réponse IMAP absente"
    fi
}

check_protocols() {
    section "PROTOCOLES"

    check_smtp -4 "$SMTPS_PORT" "SMTP TLS IPv4 465"
    check_smtp -6 "$SMTPS_PORT" "SMTP TLS IPv6 465"

    check_imap -4 "IMAP TLS IPv4 993"
    check_imap -6 "IMAP TLS IPv6 993"
}

check_certificate_names() {
    section "CERTIFICAT"

    local output san

    output="$(
        timeout 15 openssl s_client \
            -connect "$MAIL_HOST:$SMTPS_PORT" \
            -servername "$MAIL_HOST" \
            -showcerts </dev/null 2>/dev/null |
        openssl x509 -noout -subject -issuer -dates -ext subjectAltName 2>/dev/null
    )"

    printf '%s\n' "$output" >>"$REPORT_FILE"

    san="$(grep -o 'DNS:[^, ]*' <<<"$output" || true)"

    if grep -q "DNS:$MAIL_HOST" <<<"$san"; then
        log PASS "Le certificat contient DNS:$MAIL_HOST"
    else
        log FAIL "Le certificat ne contient pas DNS:$MAIL_HOST"
    fi

    if grep -q 'notAfter' <<<"$output"; then
        log INFO "Dates du certificat enregistrées dans le rapport"
    fi
}

summary() {
    section "RÉSUMÉ"

    log INFO "Rapport : $REPORT_FILE"
    log INFO "PASS=$PASS WARN=$WARN FAIL=$FAIL"

    if [[ "$FAIL" -eq 0 ]]; then
        log PASS "Aucune erreur bloquante détectée"
        return 0
    fi

    log FAIL "$FAIL erreur(s) à corriger"
    return 1
}

main() {
    section "PARAMÈTRES"

    log INFO "Serveur MX : $MAIL_HOST"
    log INFO "IPv4 : $EXPECTED_IPV4"
    log INFO "IPv6 : $EXPECTED_IPV6"
    log INFO "Domaines : ${DOMAINS[*]}"
    log INFO "Date : $(date --iso-8601=seconds)"

    local command
    for command in dig openssl nc timeout; do
        require_command "$command" || exit 2
    done

    local domain
    for domain in "${DOMAINS[@]}"; do
        check_dns_domain "$domain"
        check_dkim_domain "$domain"
    done

    check_reverse_dns
    check_ports
    check_tls_all
    check_protocols
    check_certificate_names
    summary
}

main "$@"

Exécution

1
2
chmod +x check-stalwart-multi-domain.sh
./check-stalwart-multi-domain.sh

Le rapport sera généré dans un répertoire de ce type :

1
stalwart-report-20260813-063200/report.txt

Enregistrements DKIM testés

Le script interrogera automatiquement les noms DNS suivants :

1
2
3
4
5
6
7
8
v1-ed25519-20260720._domainkey.yannig.net
v1-rsa-20260720._domainkey.yannig.net

v1-ed25519-20260723._domainkey.cinay.eu
v1-rsa-20260723._domainkey.cinay.eu

v1-ed25519-20260722._domainkey.yick.eu
v1-rsa-20260722._domainkey.yick.eu

Les enregistrements attendus doivent ressembler à ceci :

v1-ed25519-20260720._domainkey.yannig.net. TXT "v=DKIM1; k=ed25519; p=..."
v1-rsa-20260720._domainkey.yannig.net.     TXT "v=DKIM1; k=rsa; p=..."

Les champs p= contiennent uniquement les clés publiques. Les clés privées doivent rester dans Stalwart et ne doivent jamais être publiées dans le DNS. Le format DNS d’un enregistrement DKIM utilise le sélecteur suivi de ._domainkey, avec v=DKIM1, k= et p=.[^2]

SPF recommandé

Chaque domaine envoyant directement via le serveur yannig.net devrait autoriser les deux adresses du serveur :

yannig.net. TXT "v=spf1 ip4:51.38.37.240 ip6:2001:41d0:305:2100::c1c mx -all"
cinay.eu.   TXT "v=spf1 ip4:51.38.37.240 ip6:2001:41d0:305:2100::c1c mx -all"
yick.eu.    TXT "v=spf1 ip4:51.38.37.240 ip6:2001:41d0:305:2100::c1c mx -all"

Ne publiez qu’un seul enregistrement v=spf1 par domaine.

Point à vérifier dans Stalwart

Lors de la rotation automatique des clés :

  • les anciennes clés doivent rester publiées pendant la durée de validité des messages déjà envoyés ;
  • la nouvelle clé privée doit correspondre exactement à la clé publique DNS ;
  • Stalwart doit signer avec le nouveau sélecteur ;
  • l’ancien sélecteur ne doit être retiré du DNS qu’après expiration des anciens messages ;
  • les en-têtes des messages doivent contenir le bon couple d= et s=.

Dans un message envoyé, vérifiez par exemple :

1
2
3
DKIM-Signature:
    d=yannig.net;
    s=v1-ed25519-20260720;

ou :

1
2
3
DKIM-Signature:
    d=cinay.eu;
    s=v1-rsa-20260723;

Le d= doit correspondre au domaine expéditeur et le s= à l’un des sélecteurs actuellement publiés.

Niveau de SPAM ?

un moyen de tester le niveau de spam accepté par le domaine

Il faut distinguer deux notions :

  1. La réputation de vos domaines/IP pour les messages sortants : risque d’arriver dans le spam chez Gmail, Outlook, etc.
  2. Le filtrage des messages entrants par Stalwart : quantité de spam acceptée, rejetée ou classée en quarantaine.

Il n’existe pas un score public unique indiquant « le niveau de spam accepté » par yannig.net, cinay.eu ou yick.eu.

Tester la réputation sortante

1. Mail-Tester

Utilisez Mail-Tester :

  1. Ouvrez le site.
  2. Copiez l’adresse temporaire fournie.
  3. Envoyez un message depuis chacun de vos domaines :
    • test@yannig.net
    • test@cinay.eu
    • test@yick.eu
  4. Analysez séparément les résultats.

Cela vérifie notamment :

  • SPF ;
  • DKIM ;
  • DMARC ;
  • reverse DNS ;
  • réputation de l’IP ;
  • présence éventuelle dans des listes noires ;
  • contenu du message ;
  • score SpamAssassin.

Le test doit être réalisé avec un véritable message envoyé par Stalwart, pas avec un simple message composé localement.

Avec swaks :

1
2
3
4
5
6
7
8
swaks \
  --server yannig.net \
  --port 465 \
  --tls \
  --auth LOGIN \
  --auth-user test@yannig.net \
  --to ADRESSE_MAIL_TESTER \
  --from test@yannig.net

Répétez avec :

1
--auth-user test@cinay.eu --from test@cinay.eu

puis :

1
--auth-user test@yick.eu --from test@yick.eu

N’utilisez pas votre compte administrateur.

2. Vérification des listes noires

Testez les adresses suivantes :

Testez au minimum :

1
2
3
4
yannig.net
cinay.eu
yick.eu
51.38.37.240

Spamhaus précise que la réputation d’un domaine dépend notamment de son historique d’utilisation, de son comportement d’envoi et des signalements associés.

Attention : une IP propre ne garantit pas que les messages arriveront en boîte principale. Gmail et Outlook utilisent également l’engagement des destinataires, les plaintes, le contenu, les volumes et l’historique.

Mesurer la réputation chez Gmail

Pour un domaine qui envoie régulièrement vers Gmail, utilisez Google Postmaster Tools.

Google y expose notamment :

  • le taux de messages signalés comme spam ;
  • les résultats SPF, DKIM et DMARC ;
  • les erreurs de livraison ;
  • la réputation de l’IP et du domaine, lorsque suffisamment de données sont disponibles.

Google indique que Postmaster Tools exploite les messages authentifiés par DKIM, ou SPF si DKIM n’est pas disponible, pour afficher les données de réputation.

Ajoutez et vérifiez les trois domaines :

1
2
3
yannig.net
cinay.eu
yick.eu

Le domaine doit normalement avoir un volume suffisant de messages vers des comptes Gmail personnels avant que les statistiques soient disponibles. Avec un faible trafic, l’absence de données ne signifie pas que la réputation est mauvaise.

Pour un envoi important vers Gmail, Google demande une authentification correcte et recommande de maintenir le taux de plaintes très bas ; les exigences spécifiques aux expéditeurs de gros volume sont détaillées dans ses directives officielles.

Tester concrètement le classement en spam

Créez une matrice de test avec plusieurs fournisseurs :

Fournisseur Adresse de destination
Gmail compte Gmail personnel
Outlook compte Outlook.com
Yahoo compte Yahoo
Proton Mail compte Proton Mail
Mail-Tester adresse temporaire
Votre propre domaine boîte locale de test

Pour chaque domaine, envoyez un message identique :

1
2
3
4
5
From: test@yannig.net
To: compte-de-test@gmail.com
Subject: Test délivrabilité yannig.net

Message de test de délivrabilité.

Répétez avec cinay.eu et yick.eu, puis contrôlez les en-têtes complets du message :

1
2
3
4
Authentication-Results:
    spf=pass
    dkim=pass
    dmarc=pass

À vérifier aussi :

1
2
3
DKIM-Signature:
    d=yannig.net;
    s=v1-ed25519-20260720;

ou :

1
2
3
DKIM-Signature:
    d=cinay.eu;
    s=v1-rsa-20260723;

Un message qui arrive en spam malgré spf=pass, dkim=pass et dmarc=pass peut simplement souffrir d’une mauvaise réputation, d’un contenu douteux, d’un volume inhabituel ou d’un manque d’historique.

Tester le filtrage entrant de Stalwart

Pour mesurer ce que votre serveur accepte comme spam entrant, utilisez une boîte de test, par exemple :

1
spam-test@yannig.net

Envoyez-lui des messages de test depuis une adresse externe. Le plus pratique est d’utiliser des messages de catégories différentes :

  • message normal ;
  • message avec URL ;
  • message contenant une pièce jointe ;
  • message avec expéditeur non authentifié ;
  • message avec SPF/DKIM/DMARC invalides ;
  • message de test GTUBE.

Test GTUBE

GTUBE est une chaîne standard utilisée pour vérifier qu’un moteur antispam détecte un message de test. Ne l’envoyez jamais à une vraie boîte utilisateur ou à une liste de diffusion.

Utilisez une boîte dédiée, puis envoyez le contenu suivant dans le corps du message :

1
XJS*C4JDBQADN1.NSBN3*2IDNEN*GTUBE-STANDARD-ANTI-UBE-TEST-EMAIL*C.34X

Exemple avec swaks depuis une machine externe :

1
2
3
4
5
6
7
8
9
10
swaks \
  --server yannig.net \
  --port 465 \
  --tls \
  --auth LOGIN \
  --auth-user test@yannig.net \
  --to spam-test@yannig.net \
  --from test@externe.example \
  --header "Subject: GTUBE test - ne pas supprimer" \
  --body 'XJS*C4JDBQADN1.NSBN3*2IDNEN*GTUBE-STANDARD-ANTI-UBE-TEST-EMAIL*C.34X'

Le résultat attendu dépend de votre configuration Stalwart :

  • rejet SMTP immédiat ;
  • acceptation avec classement spam ;
  • déplacement en quarantaine ;
  • ajout d’un en-tête indiquant le score antispam ;
  • acceptation normale si le filtre n’est pas activé.

Surveillez les journaux pendant le test :

1
sudo journalctl -fu stalwart

Avec Docker :

1
docker compose logs -f --tail=200 stalwart

Recherchez ensuite les éléments suivants :

1
2
docker compose logs stalwart |
  grep -Ei 'spam|score|quarantine|reject|gtube|dkim|spf|dmarc'

Le test GTUBE vérifie surtout que le moteur antispam réagit bien à un motif connu. Il ne mesure pas à lui seul la qualité globale du filtrage.

Contrôle rapide des en-têtes entrants

Pour voir si un message a été classé par Stalwart, utilisez :

1
2
3
grep -Ei \
  'Authentication-Results|X-Spam|X-Rspamd|X-Spam-Status|Subject|Received' \
  message.eml

Les noms exacts des en-têtes dépendent de votre configuration. Vous pouvez notamment rechercher :

1
2
3
4
X-Spam-Status
X-Spam-Score
X-Spam-Level
Authentication-Results

Test automatisé simple

Voici un petit script qui envoie un message de contrôle vers une boîte externe à l’aide de swaks. Il ne teste pas directement le classement spam, mais permet de suivre les résultats de livraison et les en-têtes d’authentification.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#!/usr/bin/env bash

set -u

SMTP_HOST="yannig.net"
SMTP_PORT=465
TEST_DESTINATION="${1:?Usage: $0 adresse-de-test@example.net}"

declare -A SENDERS=(
    ["yannig.net"]="test@yannig.net"
    ["cinay.eu"]="test@cinay.eu"
    ["yick.eu"]="test@yick.eu"
)

for domain in "${!SENDERS[@]}"; do
    sender="${SENDERS[$domain]}"

    echo
    echo "===== Test $domain ====="
    echo "Expéditeur : $sender"
    echo "Destination : $TEST_DESTINATION"

    swaks \
        --server "$SMTP_HOST" \
        --port "$SMTP_PORT" \
        --tls \
        --auth LOGIN \
        --auth-user "$sender" \
        --to "$TEST_DESTINATION" \
        --from "$sender" \
        --header "Subject: Test délivrabilité $domain" \
        --header "X-Mail-Test-Domain: $domain" \
        --body "Test de délivrabilité envoyé depuis $domain."
done

Exécution :

1
2
chmod +x test-deliverability.sh
./test-deliverability.sh votre-compte-gmail@gmail.com

Pour un résultat fiable, faites plusieurs essais espacés dans le temps et observez les mêmes messages dans Gmail, Outlook et Proton Mail. Le meilleur indicateur pratique pour vos trois domaines sera la combinaison suivante : SPF=pass, DKIM=pass, DMARC=pass, reverse DNS cohérent, aucune blacklist, puis placement réel en boîte principale chez plusieurs fournisseurs.