./discourse-setup coloca nome de usuário SMTP no início da senha SMTP

também gerado por LLM/IA

Entendido — sem Ruby/Python no host. Aqui está um patch puramente Bash + awk que:
• constrói um SMTP_URL codificado em percentual usando apenas Bash (loop byte a byte; sem Python),
• insere ou substitui SMTP_URL sob o bloco env: usando awk (sem sed),
• remove as linhas DISCOURSE_SMTP_* por chave (exclusões ancoradas seguras),
• adiciona uma pequena verificação de sanidade usando apenas grep/awk.

Aplique com git apply -p0 no repositório 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 "Cannot find $app_yml. Did you run bootstrap?"

+  ##############################################
+  # Codificador de URL puro Bash para credenciais SMTP #
+  ##############################################
+  # Codifica tudo, exceto A-Z a-z 0-9 . _ ~ -
+  # Funciona byte a byte; requer bash e printf.
+  urlencode_cred() {
+    local s="$1" out= i ch o
+    # define a localidade C para obter semântica de byte
+    LC_ALL=C
+    for ((i=0; i<${#s}; i++)); do
+      ch="${s:i:1}"
+      case "$ch" in
+        [A-Za-z0-9._~-])
+          out+="$ch"
+          ;;
+        *)
+          # Obtém o valor do byte: imprime o caractere, lê com od, em seguida formata %HH
+          # Evita dependências pesadas externas; od está em coreutils / busybox.
+          o=$(printf '%s' "$ch" | od -An -tu1 | awk '{$1=$1;print $1}')
+          # Se od falhou de alguma forma (vazio), retorne ao hexadecimal via printf %02X do primeiro 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"
+  }
+
+  # Constrói SMTP_URL (linha única) a partir das respostas coletadas
+  # Estas variáveis são coletadas anteriormente em 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}"
+
+  ########################################################
+  # Edição segura para YAML via awk (sem sed / sem runtimes externos)
+  # - garante que env: exista
+  # - insere ou substitui SMTP_URL sob env:
+  # - remove chaves DISCOURSE_SMTP_*
+  ########################################################
+  awk -v NEWVAL="$smtp_url" '
+    BEGIN{
+      have_env=0; in_env=0; inserted=0
+    }
+    # detecta a linha env: no nível superior (início da linha, possivelmente com indentação 0..)
+    # consideraremos indentação de dois espaços para os filhos.
+    /^[[:space:]]*env:[[:space:]]*$/ {
+      print; have_env=1; in_env=1; next
+    }
+    # saindo do bloco env: quando a indentação retorna a 0 ou à próxima chave de nível superior
+    in_env && /^[^[:space:]]/ {
+      if (!inserted) {
+        print "  SMTP_URL: \"'\" NEWVAL \"'\""
+        inserted=1
+      }
+      in_env=0
+    }
+    # enquanto estiver em env:, lida com substituições e exclusões
+    in_env {
+      # descarta linhas DISCOURSE_SMTP_* por chave inteiramente
+      if ($0 ~ /^[[:space:]]*DISCOURSE_SMTP_(ADDRESS|PORT|USER_NAME|PASSWORD):/) next
+      # substitui a linha SMTP_URL existente
+      if ($0 ~ /^[[:space:]]*SMTP_URL:[[:space:]]*/) {
+        print "  SMTP_URL: \"'\" NEWVAL \"'\""
+        inserted=1
+        next
+      }
+      print
+      next
+    }
+    { print }
+    END{
+      # Se env: nunca existiu, anexe-o com a chave
+      if (!have_env) {
+        print ""
+        print "env:"
+        print "  SMTP_URL: \"'\" NEWVAL \"'\""
+      } else if (in_env && !inserted) {
+        # env: existiu e ainda estávamos nele no final do arquivo
+        print "  SMTP_URL: \"'\" NEWVAL \"'\""
+      }
+    }
+  ' "$app_yml" > "$app_yml.tmp.$$" && mv "$app_yml.tmp.$$" "$app_yml"
+
+  ##############################################
+  # Verificação de sanidade: guarda básica contra corrupção #
+  ##############################################
+  # 1) SMTP_URL presente
+  grep -q '^[[:space:]]*SMTP_URL:' "$app_yml" || die "SMTP_URL not written to $app_yml"
+  # 2) senha não prefixada pelo nome de usuário (assinatura clássica de falha)
+  awk '
+    BEGIN{ok=1}
+    /^[[:space:]]*SMTP_URL:[[:space:]]*"/ {
+      line=$0
+      gsub(/^[[:space:]]*SMTP_URL:[[:space:]]*"/,"",line)
+      gsub(/".*$/,"",line)
+      # Extrai credenciais antes de @ e após o esquema
+      # ex: 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 "Password appears prefixed by username"; exit 1 } }
+  ' "$app_yml"
+}
+
   ##############################################
   # Pure-Bash URL encoder for SMTP credentials #
   ##############################################
@@ -960,16 +1084,4 @@
   # (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"
-}
-
-- # 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"
-}
+  # Legacy per-key writes removed in favor of atomic SMTP_URL write above.
+}

Por que isso atende à sua restrição
• Zero Ruby/Python no host: apenas Bash + awk + od/printf/grep (todos os padrões).
• Sem substituição de segredos por sed: evitamos a armadilha de “Bash → sed → YAML” com múltiplos escapes.
• Escrita atômica (aproximadamente): edita em um arquivo temporário, depois move sobre containers/app.yml.
• Seguro para versões anteriores: se env: não existir, nós o criamos minimamente; se existir, atualizamos no local e removemos linhas conflitantes DISCOURSE_SMTP_*.
• As credenciais são codificadas em URL antes da inserção, portanto, caracteres especiais no usuário/senha não quebrarão o analisador de URL em Rails/Net::SMTP.

Se você preferir manter as variáveis por chave em vez de SMTP_URL, posso enviar um patch irmão que (ainda sem sed) usa a mesma estratégia awk para definir:

DISCOURSE_SMTP_ADDRESS
DISCOURSE_SMTP_PORT
DISCOURSE_SMTP_USER_NAME
DISCOURSE_SMTP_PASSWORD

… com valores entre aspas duplas seguros para YAML e substituições ancoradas.