Référence CLI

Toutes les fonctions passent par le binaire unique difuzio. Les commandes qui agissent sur un objet suivent la forme difuzio <groupe> <action> [options], par exemple difuzio member add ou difuzio list set. Les daemons, les migrations et les commandes de diagnostic gardent une forme simple : difuzio serve-web, difuzio migrate up, difuzio doctor.

difuzio help                 # liste les groupes et les commandes
difuzio help <groupe>        # actions d'un groupe (ou : difuzio <groupe>)
difuzio version              # version du binaire et backend de base compilé

Les commandes daemon prennent --config <chemin> (et chargent les keyrings). Les commandes d'administration prennent soit --dsn <dsn>, soit --config <chemin> pour la connexion à la base ; --dsn l'emporte sur --config.

Le DSN (Data Source Name) est la chaîne de connexion à la base. Sa forme dépend du backend compilé dans le binaire : chaîne go-sql-driver pour MariaDB, chemin de fichier pour SQLite (voir Base de données). difuzio version indique le backend lié.

Toute commande qui lit --config charge aussi les trois keyrings. Si la configuration référence ${CREDENTIALS_DIRECTORY} (le cas par défaut, cf Configuration), poser la variable sur la commande, qui échoue sinon en nommant la variable manquante :

CREDENTIALS_DIRECTORY=/etc/difuzio/secrets difuzio <commande> --config /etc/difuzio/config.yaml

Avec --dsn, les keyrings ne sont pas chargés. Quatre commandes exigent donc --config et refusent --dsn seul, en l'expliquant : erase, send submit et archive backfill-attachments (le corps des messages vit hors de la base), et digest compile (la compilation met du courrier en file). member add sur une liste en politique confirm a besoin du keyring de tokens et le signale de la même façon.

Codes de sortie

  • 0 : succès.
  • 1 : échec d'exécution (base injoignable, objet introuvable, refus du service).
  • 2 : erreur d'invocation (option manquante, valeur inconnue, action inconnue).

difuzio <commande> --help sort en 0. Deux exceptions conservées : domain check-dns sort en 1 tant que le domaine n'est pas prêt pour l'envoi en masse (c'est un résultat, pas une erreur), et migrate status en 1 quand le schéma est marqué "dirty".

Daemons

serve-web

difuzio serve-web --config /etc/difuzio/config.yaml [--insecure]

Lance l'interface web et l'API REST. --insecure envoie les cookies sans le drapeau Secure (développement local uniquement, jamais derrière TLS public).

serve-lmtp

difuzio serve-lmtp --config /etc/difuzio/config.yaml

Lance le serveur de réception LMTP (posts, commandes par email, bounces, plaintes).

worker

difuzio worker --config /etc/difuzio/config.yaml

Lance le pipeline de traitement, l'envoi sortant, les tâches périodiques, et (si metrics.bind est défini) le serveur /metrics. On en lance plusieurs pour le débit. Rien ne part vers le relais tant qu'aucun worker ne tourne.

Configuration et diagnostic

config check

difuzio config check --config /etc/difuzio/config.yaml

Charge et valide la configuration (keyrings, invariants), puis quitte.

doctor

difuzio doctor [--dsn <dsn> | --config /etc/difuzio/config.yaml]

Vérifie que la base répond, que le schéma est à jour et propre, et, avec --config, recoupe la configuration avec la base : domaines déclarés, stockage des corps, boîtes d'ingestion.

stats show

difuzio stats show [--dsn <dsn> | --config <chemin>]

Compteurs de l'instance : domaines, listes, listes dont l'envoi est suspendu, listes sans VERP, éléments retenus en modération, copies en file.

queue stats

difuzio queue stats [--state queued|sending|sent|deferred|failed|cancelled]

Profondeur de la file outgoing_queue par état, avec le total. --state restreint le compte à un seul état.

queue requeue

difuzio queue requeue [--domain lists.example.org [--list announce]]

Ramène les copies deferred en queued avec une prochaine tentative immédiate, pour que le worker les reprenne sans attendre la fin du backoff. Les compteurs de tentatives restent intacts : la limite de tentatives continue de s'appliquer.

digest compile

difuzio digest compile --config /etc/difuzio/config.yaml

Compile une fois les digests en attente, sous le même verrou d'avis que la tâche périodique du worker. Si un worker compile au même moment, la commande ne fait rien et le dit (code 1) plutôt que d'envoyer un digest en double.

archive backfill-attachments

difuzio archive backfill-attachments --config /etc/difuzio/config.yaml

Recalcule l'indicateur "pièces jointes" (messages.has_attachments) des messages archivés avant que difuzio ne l'enregistre à la réception. La commande ne lit que les lignes qui valent encore "non" et dont le corps brut est toujours là : elle corrige un "non" en "oui", jamais l'inverse.

Ce rattrapage n'est pas fait par une migration : décider si un message porte une pièce jointe demande de parcourir son arborescence MIME, ce qu'aucun moteur SQL ne sait faire, et réécrire toute une archive pendant une migration bloquerait la mise à jour aussi longtemps que dure le parcours. C'est donc une passe de maintenance, à lancer quand l'instance est calme.

Elle est reprenable et interruptible : elle avance par lots validés au fur et à mesure, dans l'ordre des identifiants. Un Ctrl-C ne perd que le lot en cours, et relancer la commande reprend le travail. Un message dont le corps a été purgé par la rétention est compté comme illisible et laissé tel quel : il n'y a plus rien à inspecter. Sortie : nombre de messages examinés, corrigés, illisibles, et mal formés (arborescence MIME partiellement analysable, dont la raison est journalisée).

Migrations

Les migrations s'appliquent avec le compte DDL (Data Definition Language : le compte autorisé à faire des CREATE / ALTER / DROP), passé via --dsn, ou via --config en repli. Sous SQLite il n'y a pas de comptes : ce sont les droits POSIX sur le fichier qui font foi.

difuzio migrate up            [--dsn|--config]   # applique toutes les migrations
difuzio migrate down [N|all]  [--dsn|--config]   # annule 1, N, ou toutes (all)
difuzio migrate to <version>  [--dsn|--config]   # va à une version précise
difuzio migrate status        [--dsn|--config]   # version courante + drapeau dirty
difuzio migrate force <ver>   [--dsn|--config]   # force la version, efface dirty

migrate down all est destructeur (mot-clé all requis explicitement). En cas de schéma "dirty" après un échec, corriger puis migrate force <version>.

bootstrap-admin

difuzio bootstrap-admin --email vous@example.org --password-stdin [--force]

Crée le premier super_admin. Le mot de passe vient de --password ou, mieux, de --password-stdin (lu sur l'entrée standard). --force permet d'en créer un autre si un super_admin existe déjà.

Groupe domain

domain add

difuzio domain add --name lists.example.org --base-url https://lists.example.org \
    [--display-name "..."] [--robot difuzio] [--locale fr|en]

Provisionne un domaine (en état pending_mta). Voir Domaines. --locale fixe la langue des courriers transactionnels du domaine (fr par défaut) : voir Mails transactionnels.

domain ls

difuzio domain ls

Affiche les domaines : id, nom, statut, robot, URL publique.

domain set-status

difuzio domain set-status --domain lists.example.org --status active|suspended|pending_mta

Change l'état d'un domaine. Voir Domaines.

domain set-command-addresses

difuzio domain set-command-addresses --domain lists.example.org --routed=false
difuzio domain set-command-addresses --domain lists.example.org --routed=true

Déclare si les adresses de service des listes du domaine (liste-subscribe@, liste-unsubscribe@, liste-owner@, liste-request@) sont bien remises à difuzio. Ce n'est pas une préférence : cela décrit le serveur de messagerie placé devant.

--routed=true (défaut, cas historique) suppose un MTA qui route tout le domaine vers difuzio. --routed=false correspond au profil boîte externe, où seule la boîte de chaque liste est relevée : difuzio cesse alors d'annoncer ces adresses dans les en-têtes List-Help, List-Subscribe, List-Owner et List-Unsubscribe, et de les afficher sur les pages publiques. Sans ce réglage, un abonné qui suit l'un de ces en-têtes écrit à une adresse que l'hébergeur rejette. Voir Comptes de messagerie externes.

domain add-admin

difuzio domain add-admin --email admin@example.org --domain lists.example.org \
    [--display-name "..."] [--password-stdin]

Crée l'utilisateur s'il n'existe pas et lui octroie domain_admin sur le domaine. Sans mot de passe, le compte reste unverified : il ne peut ni se connecter, ni devenir propriétaire d'une liste.

domain check-dns

difuzio domain check-dns --domain lists.example.org [--dkim-selector default]

Vérifie SPF, _dmarc et, si un sélecteur est fourni, la clé DKIM publiée. Sort en 1 tant que le domaine n'est pas prêt pour l'envoi en masse.

La console expose la même vérification par POST /api/v1/domains/{domaine}/dns-check (voir API REST) : c'est le même code, donc les deux donnent toujours le même verdict. La commande ne touche pas la base ; l'appel API, lui, enregistre le résultat sur le domaine (dns_ok, dns_checked_at).

domain mta-config

difuzio domain mta-config --domain lists.example.org [--socket /run/difuzio/lmtp.sock]

Imprime les lignes Postfix qui routent le domaine vers difuzio.

Groupe list

list create

difuzio list create --domain lists.example.org --name announce \
    (--owner proprio@example.org | --owner-id 12) \
    [--display-name "..."] [--description "..."] [--type discussion|newsletter]

Crée une liste et octroie list_owner au propriétaire, qui doit être un compte actif existant. Exactement une des deux désignations de propriétaire est requise : en préférer une silencieusement confierait la liste à quelqu'un d'autre que celui qui a été nommé.

list ls

difuzio list ls [--domain lists.example.org]

Tableau des listes : id, nom, domaine, type, statut, suspension d'envoi éventuelle avec sa raison. Voir Listes.

list show

difuzio list show --domain lists.example.org --list announce

Affiche une liste en détail : identité, état d'envoi, et les 27 réglages avec leur valeur courante.

list set

difuzio list set --domain lists.example.org --list announce [<réglages>]

Met à jour les réglages fournis ; ceux qui ne sont pas passés restent inchangés. Une chaîne vide efface un réglage textuel (--subject-tag ""), et 0 rétablit "conserver indéfiniment" pour --archive-retention-days.

--subject-tag, --relay-pool et --consent-text-ver finissent tels quels dans un en-tête de message : un saut de ligne y est refusé, comme un dépassement de la longueur de colonne (64, 64 et 32 caractères).

Réglages disponibles :

--post-policy open|members|moderated|owner
--subscribe-policy open|confirm|moderated|closed
--archive-access public|members|owners
--who-can-see-members public|members|owners
--reply-to sender|list|both
--from-munging auto|always|never
--attachment-policy allow|strip|reject
--subject-tag "[annonces]"        --subject-tag-enabled=true|false
--footer-text "..."               --footer-html "..."
--footer-enabled=true|false       --echo-to-author=true|false
--max-message-size 10485760       --relay-pool bulk
--reject-empty-body=true|false    --require-dkim-or-spf=true|false
--verp-enabled=true|false         --moderation-ttl-hours 168
--moderate-first-post=true|false  --welcome-text "..."
--goodbye-text "..."              --privacy-notice "..."
--consent-text-ver v3             --archive-retention-days 365
--require-tls-in=true|false       --anonymize-sender=true|false

Voir Listes.

list set-policy

difuzio list set-policy --domain lists.example.org --list announce \
    [--post-policy ...] [--subscribe-policy ...] [--archive-access ...] \
    [--reply-to ...] [--from-munging ...]

Raccourci historique sur les cinq politiques principales. list set couvre tous les réglages.

list set-status

difuzio list set-status --domain lists.example.org --list announce \
    --status active|closed|suspended

Change le cycle de vie de la liste. Une liste non active refuse la réception, les posts et les envois en masse : c'est la suppression douce du produit. À ne pas confondre avec le kill-switch d'envoi ci-dessous, qui laisse la liste recevoir.

list suspend / list resume

difuzio list suspend --domain lists.example.org --list announce [--note "..."]
difuzio list resume  --domain lists.example.org --list announce

Suspend ou réactive l'émission d'une liste (kill-switch). Voir Modération.

Groupe member

member add

difuzio member add --domain lists.example.org --list announce --email abonne@example.org \
    [--name "..."] [--mode regular|nomail|digest] [--channel web|email|api] \
    [--force --consent-evidence "bulletin papier du 2026-08-10"] \
    [--consent-source web_form|email_confirm|admin_import|api] \
    [--show-confirm-token <fichier>|-]

Abonne une adresse. Le résultat dépend de la politique d'abonnement de la liste : activation immédiate (open), confirmation par mail (confirm, le défaut), attente de modération (moderated), ou refus (closed).

--force crée l'abonnement actif quelle que soit la politique : c'est le chemin d'import administratif, et il exige --consent-evidence, la preuve de consentement conservée au dossier RGPD. --show-confirm-token écrit le jeton de confirmation dans un fichier créé en mode 0600 (la commande refuse d'écraser un fichier existant), ou sur la sortie standard avec - ; il n'est jamais affiché sans avoir été demandé.

Quand l'abonnement n'est pas activé immédiatement (politiques confirm et moderated), --mode et --name ne sont pas conservés pendant l'attente : la demande en instance ne porte pas ces champs, et la confirmation active l'abonné en regular sans nom. La commande le signale. Pour poser un mode, utiliser member set-mode après confirmation, ou abonner directement avec --force.

member ls

difuzio member ls --domain lists.example.org --list announce \
    [--status pending|active|disabled|unsubscribed|removed] \
    [--mode regular|nomail|digest] [--limit 200] [--offset 0]

Tableau des abonnés : id, adresse, statut, mode, score de bounce, date d'abonnement, suivi du nombre affiché et du total filtré.

member rm

difuzio member rm --domain lists.example.org --list announce --email abonne@example.org \
    [--purge --confirm]

Retire un abonné (statut removed, historique de bounces conservé). --purge supprime la ligne définitivement et exige --confirm.

member unsubscribe

difuzio member unsubscribe --domain lists.example.org --list announce --email abonne@example.org

Enregistre que la personne a demandé à partir (statut unsubscribed). C'est un acte différent de member rm, qui est un retrait administratif.

member disable / enable

difuzio member disable --domain lists.example.org --list announce --email abonne@example.org
difuzio member enable  --domain lists.example.org --list announce --email abonne@example.org

Désactive ou réactive la livraison pour un abonné. La réactivation remet aussi le score de bounce à zéro et repose le plancher de la fenêtre de scoring.

member set-mode

difuzio member set-mode --domain lists.example.org --list announce \
    --email abonne@example.org --mode regular|nomail|digest

Change le mode de livraison d'un abonné.

member import

difuzio member import --domain lists.example.org --list announce \
    --file abonnes.csv --consent-evidence "formulaires signés, lot 12"

Importe des abonnés depuis un CSV (- lit l'entrée standard). La première ligne est un en-tête : la colonne email est obligatoire, display_name (ou name) et delivery_mode (ou mode) sont facultatives, l'ordre des colonnes est libre et les autres colonnes sont ignorées.

--consent-evidence est obligatoire : un import administratif affirme un consentement que personne n'a confirmé par mail. Le compte rendu donne les lignes traitées, ajoutées, ignorées et en erreur ; la commande sort en 1 si au moins une ligne a échoué, les autres restant importées.

member export

difuzio member export --domain lists.example.org --list announce \
    [--status ...] [--mode ...] [--file abonnes.csv]

Exporte les abonnés en CSV (-, le défaut, écrit sur la sortie standard). Les colonnes sont email,display_name,delivery_mode,status,subscribed_at : c'est l'aller-retour exact de member import. Un fichier est créé en mode 0600. Chaque export est tracé dans le journal d'audit.

Groupe send

send submit

difuzio send submit --config /etc/difuzio/config.yaml \
    --domain lists.example.org --list announce \
    --from redaction@lists.example.org [--from-name "La rédaction"] \
    --subject "Numéro 1" (--text-file corps.txt | --html-file corps.html | --raw-file message.eml)

Soumet un message à une liste : il suit exactement le même pipeline qu'un post reçu en LMTP. --raw-file porte un message RFC 5322 complet et exclut alors --subject, --text-file et --html-file. Un seul de ces fichiers peut valoir - (entrée standard). --config est obligatoire : le corps est écrit via le backend de stockage configuré.

Le message est mis en file ; rien ne part tant qu'un worker ne l'a pas traité. La commande imprime l'identifiant d'envoi et le nombre estimé de destinataires, et prévient si l'émission de la liste est suspendue.

send ls

difuzio send ls --domain lists.example.org --list announce [--limit 50]

Envois récents de la liste : id, date, expéditeur, distribué ou non, copies encore en file, sujet. C'est ainsi qu'on retrouve un identifiant d'envoi égaré. La colonne KEPT ne compte que les copies encore présentes dans la file : les copies remises sont purgées après leur fenêtre de rétention, donc un envoi ancien et entièrement distribué affiche légitimement 0.

send status

difuzio send status --domain lists.example.org --list announce --id 42

Compte des copies par état, dans l'ordre du cycle de vie et zéros compris.

send cancel

difuzio send cancel --domain lists.example.org --list announce --id 42

Annule les copies encore en queued. Les copies déjà réclamées par un worker (sending) ou différées par le relais (deferred) partiront quand même : pour tout arrêter, suspendre d'abord la liste avec list suspend.

Groupe moderation

difuzio moderation ls      --domain lists.example.org --list announce
difuzio moderation approve --domain lists.example.org --list announce --id <id> [--note "..."]
difuzio moderation reject  --domain lists.example.org --list announce --id <id> [--note "..."]

Liste, approuve ou rejette les éléments retenus (posts et abonnements). moderation list reste accepté comme synonyme de moderation ls. Voir Modération.

Groupe credential

Comptes de messagerie du fournisseur attachés à une liste (profil boîte externe, voir Listes). La saisie se fait dans la console : un mot de passe passé en argument finirait dans l'historique du shell. Ces commandes servent au diagnostic et à la reprise d'une configuration existante.

credential ls

difuzio credential ls --domain lists.example.org --list membres

Comptes enregistrés pour la liste : type, serveur, identifiant, activation, état. L'état distingue "jamais testé", "ok" avec la date du dernier succès, "failing" avec le message de l'hébergeur, et "unreadable" quand la clé de chiffrement qui protégeait le mot de passe n'est plus disponible. Le mot de passe n'est jamais affiché.

credential test

difuzio credential test --domain lists.example.org --list membres \
    --kind ingest --config /etc/difuzio/config.yaml

Ouvre une vraie session avec le compte enregistré : connexion, TLS, authentification, et ouverture du dossier pour l'IMAP. Aucun message n'est envoyé ni relevé. --kind vaut ingest (boîte IMAP) ou submission (compte SMTP). --config est requis : le mot de passe est déchiffré avec le keyring. Le résultat est enregistré sur la liste, donc visible ensuite dans la console.

credential rm

difuzio credential rm --domain lists.example.org --list membres --kind ingest

Supprime un compte enregistré. --kind vaut ingest (boîte IMAP) ou submission (compte SMTP). C'est la seule écriture disponible en ligne de commande : ajouter un compte demande un mot de passe, qui n'a rien à faire dans l'historique du shell, mais le retirer n'expose aucun secret.

Cette commande reste utilisable quand policy.external_mailboxes vaut deny. C'est même la sortie prévue dans ce cas : sous cette politique, serve-web et worker refusent de démarrer tant qu'un compte subsiste, en nommant les listes concernées (voir Configuration, section policy).

credential import

difuzio credential import --config /etc/difuzio/config.yaml
difuzio credential import --config /etc/difuzio/config.yaml --apply

Verse les entrées relay.auths et ingest.mailboxes du fichier de configuration dans la base, pour passer à des comptes gérés depuis la console sans ressaisir un seul mot de passe. Sans --apply, la commande affiche ce qu'elle ferait et n'écrit rien. Une liste qui a déjà un compte du même type est laissée telle quelle, sauf --overwrite. Le keyring security.credential_keyring est requis.

Une fois l'import fait et vérifié, retirer relay.auths et ingest.mailboxes du fichier : la base prime, et une entrée YAML devenue inutile n'apparaît plus que dans un avertissement du journal. relay.auth (identité de repli du robot) reste nécessaire.

Groupe webhook

webhook create

difuzio webhook create [--domain lists.example.org [--list announce]] \
    --url https://exemple.org/hook --events member.added,member.removed

Enregistre un point de terminaison. --events all souscrit aux dix événements : member.added, member.removed, member.bounced, subscription.pending, subscription.confirmed, send.submitted, send.completed, send.failed, message.held, list.suspended.

L'URL doit être en HTTPS et se résoudre vers une adresse publique : une cible en loopback, en RFC1918 ou sur une adresse de métadonnées est refusée. Le secret de signature n'est affiché qu'une seule fois, à la création.

webhook ls

difuzio webhook ls

Webhooks visibles : id, portée, actif, URL, événements. Le secret n'est jamais réaffiché.

webhook rm

difuzio webhook rm --id 3

Supprime un webhook.

webhook test

difuzio webhook test --id 3

Met en file un ping signé. La requête est émise par un worker en fonctionnement, et son résultat apparaît dans le journal du worker (component=webhook).

Groupe user

user create

difuzio user create --email u@example.org [--display-name "..."] [--locale fr_FR] \
    [--password-stdin]

Crée un compte, sans aucun rôle. Sans mot de passe, le compte reste unverified et ne peut pas être propriétaire d'une liste.

user ls

difuzio user ls [--email fragment] [--status unverified|active|disabled] \
    [--limit 200] [--offset 0]

--offset exige un --limit non nul (SQL n'a pas d'OFFSET seul). Comptes visibles : id, adresse, statut, super_admin, mot de passe posé ou non, dernière connexion.

user disable

difuzio user disable --email u@example.org [--enable]

Désactive un compte, ou le réactive avec --enable. Le service refuse de laisser un domaine sans administrateur.

user set-password

difuzio user set-password --email u@example.org --password-stdin

Pose un nouveau mot de passe (haché en argon2id) pour un compte existant.

user roles

difuzio user roles --email u@example.org

Rôles détenus par le compte, avec le domaine et la liste concernés.

Groupe role

difuzio role grant  --email u@example.org --role moderator --domain lists.example.org --list announce
difuzio role revoke --email u@example.org --role moderator --domain lists.example.org --list announce

Octroie ou révoque un rôle (super_admin, domain_admin, list_owner, moderator). --domain et --list selon la portée du rôle. Voir Utilisateurs et rôles.

Groupe apikey

apikey create

difuzio apikey create --email u@example.org --scope user|domain|list \
    [--domain ...] [--list ...] --perms read,members:write,send,moderate,manage [--name ...]

Frappe une clé d'API ; le secret n'est affiché qu'une fois. Voir API REST.

apikey ls

difuzio apikey ls [--all]

Clés administrables : id, propriétaire, nom, portée, permissions, état. Les clés révoquées ne sont affichées qu'avec --all. Le secret n'est jamais réaffiché.

apikey revoke

difuzio apikey revoke --id 7

Révoque une clé. L'opération est idempotente : une clé déjà révoquée est signalée comme telle, sans erreur.

Groupe audit

difuzio audit ls [--domain lists.example.org [--list announce]] \
    [--action member.export] [--result ok|denied|error] [--actor "difuzio erase"] \
    [--since 2026-08-01] [--until 2026-09-01] [--limit 100] [--offset 0] [--detail]

Lit le journal d'audit, du plus récent au plus ancien. --since et --until acceptent une date 2006-01-02 ou un horodatage RFC3339, en UTC. --detail imprime la charge JSON de chaque entrée.

erase

difuzio erase --email personne@example.org --confirm --config /etc/difuzio/config.yaml

Effacement RGPD d'une personne sur toutes les tables. --confirm est obligatoire. --config l'est aussi, et --dsn seul est refusé : le corps des messages vit hors de la base (storage.blob_dir) et seul le fichier de configuration en donne le chemin. Sans lui, l'effacement viderait les lignes en laissant les corps sur disque, en annonçant un succès. Le compte rendu indique le nombre d'objets déliés. Voir RGPD.

Anciennes orthographes

Les commandes plates d'avant le passage aux groupes restent acceptées et font exactement la même chose que leur forme groupée : les scripts existants n'ont pas besoin d'être réécrits.

Ancienne forme Forme groupée
add-domain domain add
set-domain-status domain set-status
add-admin domain add-admin
check-dns domain check-dns
mta-config domain mta-config
create-list list create
lists list ls
set-policy list set-policy
list-suspend list suspend
list-resume list resume
set-delivery-mode member set-mode
reset-password user set-password
grant-role role grant
revoke-role role revoke
create-api-key apikey create