Aller au contenu
  1. Posts/

QFieldCloud auto-hébergé [A03] : brancher un vrai relais de courriel

Sommaire
QFieldCloud auto-hébergé - Cet article fait partie d'une série.
Partie 8: Cet article

À qui s’adresse cette annexe
#

L’instance de base embarque un piège à courriels : rien ne part. Cette annexe dit exactement ce que ce choix interdit, pourquoi il est plus lourd de conséquences qu’il n’y paraît, et comment brancher un vrai relais.

Les fichiers qu’on touche
#

Deux, et la bascule tient dans le premier. Chaque bloc de code plus bas rappelle en première ligne le fichier auquel il appartient.

Fichier Ce qu’on y met
.env les cinq variables EMAIL_* et DEFAULT_FROM_EMAIL
docker-compose.override.yml le profil never sur smtp4dev, ou ses trois ports dissociés tant qu’on le garde

L’effet de bord qu’on n’avait pas vu venir
#

À la fin de l’épisode 4, l’instance est passée sur AccountAdapterSignUpClosed, un adaptateur pour allauth — la bibliothèque tierce qui gère l’inscription et la connexion dans QFieldCloud. Résultat : plus personne ne peut créer son compte depuis la page d’inscription. Bonne décision de sécurité — sur une instance publiée, le défaut upstream laisse n’importe qui s’enregistrer.

Elle a une conséquence qu’il faut regarder en face : le seul chemin d’entrée en libre-service devient l’invitation par courriel. Le courriel cesse d’être un confort et devient le mécanisme d’inscription.

Info : les commandes docker compose se lancent depuis src/, le répertoire du sous-module — c’est de là que COMPOSE_FILE est résolu — et avec --env-file ../.env. Chaque bloc le rappelle par son cd src, à ne faire qu’une fois par session. Le $ en début de ligne est le symbole du terminal, il ne se copie pas.

Une autre voie subsiste, et elle mérite d’être connue avant de se croire bloqué : l’administrateur peut créer le compte lui-même, depuis l’admin Django (/admin/, modèle Person) ou en ligne de commande.

$ cd src && docker compose --env-file ../.env exec app python manage.py createuser \
    --username=dupont --email=dupont@exemple.fr --password='…'
User dupont has been successfully created

Le compte obtenu est complet : User.save() crée le UserAccount et l’abonnement courant dans la même transaction. Et comme ACCOUNT_EMAIL_VERIFICATION vaut optional, l’adresse non vérifiée n’empêche pas la connexion.

Deux réserves, cependant. La docstring de createuser annonce elle-même un usage de test, et le mot de passe passe en clair par la ligne de commande — donc par l’historique du shell : préférez l’admin Django, ou changez le mot de passe juste après. Surtout, cette voie ne remplace pas le courriel, elle le déplace : il faut transmettre l’identifiant et le mot de passe par un autre canal, et l’utilisateur qui oubliera ce dernier n’aura toujours aucun moyen de le réinitialiser seul. Elle dépanne pour trois comptes ; elle ne tient pas pour trente.

Or voici ce que fait l’instance quand on lui demande d’envoyer :

$ cd src && docker compose --env-file ../.env exec app python manage.py sendtestemail vous@exemple.fr
$ curl -s 'http://127.0.0.1:8012/api/Messages?pageSize=5'
messages piégés: 1
 - 2026-09-04T14:08:19 | webmaster@localhost -> ['vous@exemple.fr']
   | Test email from 7bd08766e2a9 on 2026-09-04 14:08:19

Le message n’est pas parti : il est dans le piège, consultable sur http://127.0.0.1:8012. Et son expéditeur est webmaster@localhost, valeur par défaut de DEFAULT_FROM_EMAIL — une adresse sans domaine pleinement qualifié, que le premier relais venu refuserait.

Ce qui est bloqué, concrètement
#

Fonctionnalité Ce qui se passe
Invitation d’un utilisateur Le message part dans le piège. Il faut y chercher le lien et le transmettre à la main
Réinitialisation de mot de passe Idem — l’utilisateur attend un courriel qui n’arrivera jamais
Vérification d’adresse (ACCOUNT_EMAIL_VERIFICATION=optional) Sans effet visible, puisqu’optionnelle
Notifications de fin de job Perdues silencieusement
Attention : ne passez pas ACCOUNT_EMAIL_VERIFICATION à mandatory en croyant durcir l’instance. Avec un piège à courriels, plus personne ne peut activer son compte — vous compris.

Pour une instance à poignée d’utilisateurs, tous connus, le compromis se défend : on relève les invitations dans l’interface de smtp4dev et on transmet le lien par le canal qu’on veut. À condition de le savoir, et de ne pas découvrir le jour J que l’invitation envoyée à un collègue n’est jamais partie.

La bascule, en cinq variables
#

# .env
EMAIL_HOST="smtp.exemple.fr"       # au lieu de smtp4dev
EMAIL_PORT=587                     # au lieu de 25
EMAIL_USE_TLS=True                 # STARTTLS ; EMAIL_USE_SSL pour du 465 implicite
EMAIL_HOST_USER="…"
EMAIL_HOST_PASSWORD="…"
DEFAULT_FROM_EMAIL="qfieldcloud@votre-domaine.fr"   # LA variable qu'on oublie

Aucune modification du sous-module. Deux confusions classiques :

EMAIL_PORT n’a rien à voir avec SMTP4DEV_SMTP_PORT. Le premier est le port que l’application compose sur le réseau Docker ; le second, le port publié sur l’hôte — déplacé sur 2525 à l’épisode 4 pour éviter le conflit avec l’agent de transport local. Changer l’un en croyant agir sur l’autre est le classique du genre.

DEFAULT_FROM_EMAIL doit appartenir à un domaine dont les enregistrements SPF autorisent le relais choisi. Faute de quoi les messages partent bien — et arrivent en indésirables, ce qui est plus difficile à diagnostiquer qu’un échec franc.

Une fois le relais en place, smtp4dev se retire exactement comme certbot :

# docker-compose.override.yml
  smtp4dev:
    profiles: ["never"]

Avec la même réserve qu’en A01 : le profil exclut le service de la configuration, il ne supprime pas le conteneur qui tourne déjà. up -d --remove-orphans le laissera en place, invisible dans docker compose config et bien présent dans docker ps. Il faut le nommer, en réactivant le profil le temps de la commande :

$ cd src && docker compose --env-file ../.env --profile never rm -sf smtp4dev

Y accéder tant qu’il est là
#

L’interface web écoute sur le port SMTP4DEV_WEB_PORT (8012 ici), et l’adresse sur laquelle elle est publiée décide de tout le reste.

Par défaut, 127.0.0.1 : elle n’est joignable que depuis la machine elle-même, donc par un tunnel SSH depuis votre poste.

$ ssh -N -L 8012:127.0.0.1:8012 utilisateur@10.0.0.10
# puis http://127.0.0.1:8012 dans le navigateur

C’est fastidieux pour une consultation quotidienne, et la tentation est grande de publier l’interface sur le LAN. Elle est légitime — à une condition près, qu’il faut avoir en tête.

Attention : smtp4dev n’a aucune authentification. Son interface affiche les invitations et les liens de réinitialisation de mot de passe de tous les comptes. La publier sur un réseau, c’est y offrir la prise de contrôle de l’instance à qui s’y trouve. Sur un LAN de confiance, le compromis se défend ; sur un réseau partagé, non.

Si vous la publiez, ne publiez qu’elle. Une seule variable pilote les trois ports dans le fichier upstream — web, SMTP et IMAP partent ensemble —, alors que l’IMAP donne accès aux mêmes messages que l’interface et que le SMTP accepterait n’importe quel envoi. Les dissocier dans l’override coûte trois lignes :

# docker-compose.override.yml
  smtp4dev:
    ports: !override
      - "${SMTP4DEV_WEB_BIND_IP}:${SMTP4DEV_WEB_PORT}:80"   # seule à sortir
      - "127.0.0.1:${SMTP4DEV_SMTP_PORT}:25"
      - "127.0.0.1:${SMTP4DEV_IMAP_PORT}:143"

Une porte au lieu de trois, pour le même service rendu. Et un contrôle qui ne coûte rien, une fois le conteneur recréé :

$ docker port qfieldcloud-smtp4dev-1
25/tcp -> 127.0.0.1:2525
80/tcp -> 10.0.0.10:8012
143/tcp -> 127.0.0.1:143

Notez qu’après cette bascule, http://127.0.0.1:8012 ne répond plus depuis la machine elle-même : le port n’y est plus publié. C’est la même URL pour tout le monde, ce qui est plutôt une bonne chose.

Vérifier que ça marche vraiment
#

$ cd src && docker compose --env-file ../.env exec app python manage.py sendtestemail vous@votre-domaine.fr

Puis regardez la boîte de réception, pas les journaux : Django considère l’envoi réussi dès que le relais a accepté le message. Le reste — SPF, DKIM, réputation — se joue après, et hors de son champ de vision.

Ce qu’il faut retenir
#

  • Fermer les inscriptions fait du courriel le seul chemin d’entrée en libre-service. L’administrateur garde la création à la main (/admin/ ou manage.py createuser), mais elle ne dispense pas d’un relais : la réinitialisation de mot de passe, elle, n’a pas d’équivalent manuel.
  • DEFAULT_FROM_EMAIL par défaut vaut webmaster@localhost, refusé par n’importe quel relais.
  • EMAIL_PORT et SMTP4DEV_SMTP_PORT désignent deux choses différentes.
  • ACCOUNT_EMAIL_VERIFICATION=mandatory avec un piège à courriels verrouille l’instance entière.

Image de couverture : Scott Blake sur Unsplash.

QFieldCloud auto-hébergé - Cet article fait partie d'une série.
Partie 8: Cet article

Articles connexes