./discourse-setup pone el nombre de usuario SMTP al principio de la contraseña SMTP

también generado por LLM/IA

Entendido: no hay Ruby/Python en el host. Aquí tienes un parche puro de Bash + awk que:
• construye una SMTP_URL codificada en porcentaje utilizando solo Bash (bucle byte a byte; sin Python),
• inserta o reemplaza SMTP_URL bajo el bloque env: usando awk (sin sed),
• elimina las líneas DISCOURSE_SMTP_* por clave (eliminaciones ancladas seguras),
• añade una pequeña verificación de cordura utilizando solo grep/awk.

Aplícalo con git apply -p0 en el repositorio 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 "No se puede encontrar $app_yml. ¿Ejecutaste bootstrap?"

+  ##############################################
+  # Codificador de URL puro de Bash para credenciales SMTP #
+  ##############################################
+  # Codifica todo excepto A-Z a-z 0-9 . _ ~ -
+  # Funciona byte a byte; requiere bash y printf.
+  urlencode_cred() {
+    local s="$1" out= i ch o
+    # establece la configuración regional C para obtener semántica de bytes
+    LC_ALL=C
+    for ((i=0; i<${#s}; i++)); do
+      ch="${s:i:1}"
+      case "$ch" in
+        [A-Za-z0-9._~-])
+          out+="$ch"
+          ;;
+        *)
+          # Obtiene el valor del byte: imprime el carácter, lee con od, luego formatea %HH
+          # Evita dependencias pesadas externas; od está en coreutils / busybox.
+          o=$(printf '%s' "$ch" | od -An -tu1 | awk '{$1=$1;print $1}')
+          # Si od falló de alguna manera (vacío), recurre a hexadecimal a través de printf %02X del primer byte
+          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"
+  }
+
+  # Construye SMTP_URL (una sola línea) a partir de las respuestas recopiladas
+  # Estas variables se recopilan anteriormente en 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}"
+
+  ########################################################
+  # Edición segura para YAML mediante awk (sin sed / sin tiempos de ejecución externos)
+  # - asegura que exista env:
+  # - inserta o reemplaza SMTP_URL bajo env:
+  # - elimina las claves DISCOURSE_SMTP_*
+  ########################################################
+  awk -v NEWVAL="$smtp_url" '
+    BEGIN{
+      have_env=0; in_env=0; inserted=0
+    }
+    # detecta la línea env: en el nivel superior (inicio de línea, posiblemente con sangría 0..)
+    # consideraremos una sangría de dos espacios para los hijos.
+    /^[[:space:]]*env:[[:space:]]*$/ {
+      print; have_env=1; in_env=1; next
+    }
+    # saliendo del bloque env: cuando la sangría vuelve a 0 o a la siguiente clave de nivel superior
+    in_env && /^[^[:space:]]/ {
+      if (!inserted) {
+        print "  SMTP_URL: \"' NEWVAL "'\""
+        inserted=1
+      }
+      in_env=0
+    }
+    # mientras esté en env:, maneja reemplazos y eliminaciones
+    in_env {
+      # descarta líneas DISCOURSE_SMTP_* por clave por completo
+      if ($0 ~ /^[[:space:]]*DISCOURSE_SMTP_(ADDRESS|PORT|USER_NAME|PASSWORD):/) next
+      # reemplaza la línea SMTP_URL existente
+      if ($0 ~ /^[[:space:]]*SMTP_URL:[[:space:]]*/) {
+        print "  SMTP_URL: \"' NEWVAL "'\""
+        inserted=1
+        next
+      }
+      print
+      next
+    }
+    { print }
+    END{
+      # Si env: nunca existió, agrégalo con la clave
+      if (!have_env) {
+        print ""
+        print "env:"
+        print "  SMTP_URL: \"' NEWVAL "'\""
+      } else if (in_env && !inserted) {
+        # env: existió y todavía estábamos en él al final del archivo
+        print "  SMTP_URL: \"' NEWVAL "'\""
+      }
+    }
+  ' "$app_yml" > "$app_yml.tmp.$$" && mv "$app_yml.tmp.$$" "$app_yml"
+
+  ##############################################
+  # Verificación de cordura: guardia básica contra la manipulación #
+  ##############################################
+  # 1) SMTP_URL presente
+  grep -q '^[[:space:]]*SMTP_URL:' "$app_yml" || die "SMTP_URL no escrito en $app_yml"
+  # 2) la contraseña no está precedida por el nombre de usuario (firma de fallo clásica)
+  awk '
+    BEGIN{ok=1}
+    /^[[:space:]]*SMTP_URL:[[:space:]]*"/ {
+      line=$0
+      gsub(/^[[:space:]]*SMTP_URL:[[:space:]]*"/,"",line)
+      gsub(/".*$/,"",line)
+      # Extrae las credenciales antes de @ y después del esquema
+      # por ejemplo, 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 "La contraseña parece estar precedida por el nombre de usuario"; exit 1 } }
+  ' "$app_yml"
+ }
+
   ##############################################
   # Pure-Bash URL encoder for SMTP credentials #
   ##############################################
@@ -997,17 +1121,4 @@
   # Legacy per-key writes removed in favor of atomic SMTP_URL write above.
 }
  • 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”

-}


Por qué esto cumple tu restricción
	•	Cero Ruby/Python en el host: solo Bash + awk + od/printf/grep (todos estándar).
	•	Sin reemplazo de `sed` de secretos: evitamos la trampa de múltiples escapes "Bash → sed → YAML".
	•	Escritura atómica (aproximadamente): edita en un archivo temporal, luego `mv` sobre `containers/app.yml`.
	•	Seguro hacia atrás: si `env:` no existe, lo creamos mínimamente; si existe, lo actualizamos en el lugar y eliminamos las líneas conflictivas `DISCOURSE_SMTP_*`.
	•	Las credenciales se codifican en URL antes de insertarlas, por lo que los caracteres especiales en el usuario/contraseña no romperán el analizador de URL en Rails/Net::SMTP.

Si prefieres mantener las variables por clave en lugar de `SMTP_URL`, puedo enviar un parche hermano que (aún sin `sed`) utiliza la misma estrategia de `awk` para establecer:

DISCOURSE_SMTP_ADDRESS
DISCOURSE_SMTP_PORT
DISCOURSE_SMTP_USER_NAME
DISCOURSE_SMTP_PASSWORD


…con valores entre comillas dobles seguros para YAML y reemplazos anclados.