./discourse-setup place le nom d'utilisateur SMTP au début du mot de passe SMTP

également généré par LLM/IA

Compris — pas de Ruby/Python sur l’hôte. Voici un patch purement Bash + awk qui :
• construit une URL SMTP_URL encodée en pourcentage en utilisant uniquement Bash (boucle octet par octet ; pas de Python),
• insère ou remplace SMTP_URL sous le bloc env: en utilisant awk (pas de sed),
• supprime les lignes DISCOURSE_SMTP_* par clé (suppressions ancrées sûres),
• ajoute une petite vérification de bon sens en utilisant uniquement grep/awk.

Appliquez avec git apply -p0 dans le dépôt discourse_docker.

--- a/discourse-setup
+++ b/discourse-setup
@@ -867,6 +867,130 @@ write_smtp_settings() {
   local app_yml="containers/app.yml"
   [[ -f "$app_yml" ]] || die "Impossible de trouver $app_yml. Avez-vous exécuté bootstrap ?"
 
+  ##############################################
+  # Encodeur d'URL Bash pur pour les identifiants SMTP #
+  ##############################################
+  # Encode tout sauf A-Z a-z 0-9 . _ ~ -
+  # Fonctionne octet par octet ; nécessite bash et printf.
+  urlencode_cred() {
+    local s="$1" out= i ch o
+    # définit la locale C pour obtenir la sémantique des octets
+    LC_ALL=C
+    for ((i=0; i<${#s}; i++)); do
+      ch="${s:i:1}"
+      case "$ch" in
+        [A-Za-z0-9._~-])
+          out+="$ch"
+          ;;
+        *)
+          # Obtient la valeur de l'octet : imprime le caractère, lit avec od, puis formate %HH
+          # Évite les dépendances externes lourdes ; od est dans coreutils / busybox.
+          o=$(printf '%s' "$ch" | od -An -tu1 | awk '{$1=$1;print $1}')
+          # Si od a échoué (vide), revenir au format hexadécimal via printf %02X du premier octet
+          if [ -z "$o" ]; then
+            o=$(printf '%s' "$ch" | head -c1 | od -An -tu1 | awk '{$1=$1;print $1}')
+          fi
+          printf -v o '%%%02X' "$o"
+          out+="$o"
+          ;;
+      esac
+    done
+    printf '%s' "$out"
+  }
+
+  # Construit SMTP_URL (une seule ligne) à partir des réponses collectées
+  # Ces variables sont collectées plus tôt dans discourse-setup :
+  #   $smtp_address  $smtp_port  $smtp_user  $smtp_password
+  local addr="$smtp_address"
+  local port="$smtp_port"
+  local user_enc pass_enc
+  user_enc="$(urlencode_cred "$smtp_user")"
+  pass_enc="$(urlencode_cred "$smtp_password")"
+  local smtp_url="smtp://${user_enc}:${pass_enc}@${addr}:${port}"
+
+  ########################################################
+  # Modification sûre pour YAML via awk (pas de sed / pas de runtimes externes)
+  # - s'assurer que env: existe
+  # - insérer ou remplacer SMTP_URL sous env:
+  # - supprimer les clés DISCOURSE_SMTP_*
+  ########################################################
+  awk -v NEWVAL="$smtp_url" '
+    BEGIN{
+      have_env=0; in_env=0; inserted=0
+    }
+    # détecte la ligne env: au niveau supérieur (début de ligne, éventuellement indentée de 0..)
+    # nous considérerons une indentation de deux espaces pour les enfants.
+    /^[[:space:]]*env:[[:space:]]*$/ {
+      print; have_env=1; in_env=1; next
+    }
+    # quitte le bloc env: lorsque l'indentation revient à 0 ou à la clé de niveau supérieur suivante
+    in_env && /^[^[:space:]]/ {
+      if (!inserted) {
+        print "  SMTP_URL: \"' NEWVAL '\""
+        inserted=1
+      }
+      in_env=0
+    }
+    # pendant que dans env:, gérer les remplacements et les suppressions
+    in_env {
+      # supprime complètement les lignes DISCOURSE_SMTP_* par clé
+      if ($0 ~ /^[[:space:]]*DISCOURSE_SMTP_(ADDRESS|PORT|USER_NAME|PASSWORD):/) next
+      # remplace la ligne SMTP_URL existante
+      if ($0 ~ /^[[:space:]]*SMTP_URL:[[:space:]]*/) {
+        print "  SMTP_URL: \"' NEWVAL '\""
+        inserted=1
+        next
+      }
+      print
+      next
+    }
+    { print }
+    END{
+      # Si env: n'a jamais existé, ajoutez-le avec la clé
+      if (!have_env) {
+        print ""
+        print "env:"
+        print "  SMTP_URL: \"' NEWVAL '\""
+      } else if (in_env && !inserted) {
+        # env: existait et nous étions toujours dedans à la fin du fichier
+        print "  SMTP_URL: \"' NEWVAL '\""
+      }
+    }
+  ' "$app_yml" > "$app_yml.tmp.$$" && mv "$app_yml.tmp.$$" "$app_yml"
+
+  ##############################################
+  # Vérification de bon sens : garde de base contre le déchirement #
+  ##############################################
+  # 1) SMTP_URL présent
+  grep -q '^[[:space:]]*SMTP_URL:' "$app_yml" || die "SMTP_URL non écrit dans $app_yml"
+  # 2) le mot de passe n'est pas préfixé par le nom d'utilisateur (signature d'échec classique)
+  awk '
+    BEGIN{ok=1}
+    /^[[:space:]]*SMTP_URL:[[:space:]]*"/ {
+      line=$0
+      gsub(/^[[:space:]]*SMTP_URL:[[:space:]]*"/,"",line)
+      gsub(/".*$/,"",line)
+      # Extrait les identifiants avant @ et après le schéma
+      # par exemple smtp://user:pass@host:port
+      sub(/^[a-z]+:\/\//,"",line)
+      at=index(line,"@")
+      if (at>0) {
+        creds=substr(line,1,at-1)
+        colon=index(creds,":")
+        if (colon>0) {
+          user=substr(creds,1,colon-1)
+          pass=substr(creds,colon+1)
+          if (index(pass,user)==1) { ok=0 }
+        }
+      }
+    }
+    END{ if (!ok) { print "Le mot de passe semble être préfixé par le nom d'utilisateur"; exit 1 } }
+  ' "$app_yml"
+ }
+
   ##############################################
   # Pure-Bash URL encoder for SMTP credentials #
   ##############################################
@@ -995,17 +1119,6 @@
   # NOTE: historically fragile with special chars
   update_setting_yaml "DISCOURSE_SMTP_ADDRESS"  "$smtp_address"
   update_setting_yaml "DISCOURSE_SMTP_PORT"     "$smtp_port"
-  update_setting_yaml "DISCOURSE_SMTP_USER_NAME" "$smtp_user"
-  update_setting_yaml "DISCOURSE_SMTP_PASSWORD" "$smtp_password"
-}
-
-
--  # Write per-key SMTP entries (address/port/username/password)
--  # (legacy: performed via sed substitutions)
--  # NOTE: historically fragile with special chars
--  update_setting_yaml "DISCOURSE_SMTP_ADDRESS"  "$smtp_address"
--  update_setting_yaml "DISCOURSE_SMTP_PORT"     "$smtp_port"
--  update_setting_yaml "DISCOURSE_SMTP_USER_NAME" "$smtp_user"
--  update_setting_yaml "DISCOURSE_SMTP_PASSWORD" "$smtp_password"
 -}
+  # Les écritures héritées par clé ont été supprimées en faveur de l'écriture atomique SMTP_URL ci-dessus.
+}

Pourquoi cela répond à votre contrainte
• Zéro Ruby/Python sur l’hôte : uniquement Bash + awk + od/printf/grep (tout est standard).
• Pas de remplacement de secrets par sed : nous évitons le piège multi-échappement “Bash → sed → YAML”.
• Écriture atomique (ou presque) : modification dans un fichier temporaire, puis déplacement sur containers/app.yml.
• Sûr pour la rétrocompatibilité : si env: n’existe pas, nous le créons de manière minimale ; s’il existe, nous le mettons à jour sur place et supprimons les lignes conflictuelles DISCOURSE_SMTP_*.
• Les identifiants sont encodés en URL avant l’insertion, de sorte que les caractères spéciaux dans l’utilisateur/mot de passe ne casseront pas l’analyseur d’URL dans Rails/Net::SMTP.

Si vous préférez conserver les variables par clé au lieu de SMTP_URL, je peux envoyer un patch frère qui (toujours sans sed) utilise la même stratégie awk pour définir :

DISCOURSE_SMTP_ADDRESS
DISCOURSE_SMTP_PORT
DISCOURSE_SMTP_USER_NAME
DISCOURSE_SMTP_PASSWORD

… avec des valeurs entre guillemets doubles sûres pour YAML et des remplacements ancrés.