Isso causa falha na entrega de e-mails, e o discourse-doctor confirma a senha corrompida.
Observações
• Colocar a senha entre aspas explicitamente no prompt não altera o resultado.
• A senha contém caracteres especiais (@, !), mas o problema não é apenas o escape YAML: a string do nome de usuário é literalmente concatenada no início da senha.
• Reproduzível em várias execuções de ./discourse-setup.
você pode testar se isso se relaciona particularmente a letras na senha, talvez @ / ! revele o bug?
Todo o sed/awk e os scripts bash elaborados em discourse-setup tornam a manutenção bastante complicada. Talvez @pfaffman tenha algumas ideias sobre isso?
Sim. Esse é o problema. Existem múltiplos níveis de escape que precisam acontecer, como quando o bash lê o valor, quando o bash entrega o valor para o sed, quando o sed o substitui e, então, talvez, quando o arquivo yml o recebe. É um problema conhecido:
Não consigo replicar. Qual sistema operacional você está usando?
root@bro:/var/discourse# ./discourse-setup --skip-connection-test --skip-rebuild
pulando teste de conexão
'samples/standalone.yml' -> 'containers/app.yml'
Encontrados 29GB de memória e 32 núcleos de CPU físicos
definindo db_shared_buffers = 4096MB
definindo UNICORN_WORKERS = 8
parâmetros de memória de containers/app.yml atualizados.
Nome do host para o seu Discourse? [discourse.example.com]: forum.phsics.site
Definindo EC para 2
Pulando verificação de porta.
Endereço de e-mail para contas de administrador? [me@example.com,you@example.com]: jay@literatecomputing.com
Endereço do servidor SMTP? [smtp.example.com]: smtp-relay.brevo.com
Porta SMTP? [587]: 2525
Nome de usuário SMTP? [user@example.com]: 83fca0012@smtp-brevo.com
Senha SMTP? []: 5AHQXrf4LDUmRB1J
Endereço de e-mail de notificação? [noreply@forum.phsics.site]:
Endereço de e-mail opcional para avisos do Let's Encrypt? (ENTER para pular) [me@example.com]:
ID de conta MaxMind opcional (ENTER para continuar sem o banco de dados de geolocalização GeoLite2 MAXMIND) [123456]:
Isso parece certo?
Nome do host : forum.phsics.site
E-mail : jay@literatecomputing.com
Endereço SMTP : smtp-relay.brevo.com
Porta SMTP : 2525
Nome de usuário SMTP : 83fca0012@smtp-brevo.com
Senha SMTP : 5AHQXrf4LDUmRB1J
E-mail de notificação: noreply@forum.phsics.site
ID da conta MaxMind: (não definido)
Chave de licença MaxMind: (não definida)
ENTER para continuar, 'n' para tentar novamente, Ctrl+C para sair:
letsencrypt.ssl.template.yml ativado
Arquivo de configuração em containers/app.yml atualizado com sucesso!
Atualizações bem-sucedidas. --skip-rebuild solicitado. Saindo.
root@bro:/var/discourse# grep SMTP containers/app.yml
## TODO: O servidor de e-mail SMTP usado para validar novas contas e enviar notificações
# O ENDEREÇO SMTP é obrigatório
# AVISO: A senha SMTP deve ser colocada entre aspas para evitar problemas
DISCOURSE_SMTP_ADDRESS: smtp-relay.brevo.com
DISCOURSE_SMTP_PORT: 2525
DISCOURSE_SMTP_USER_NAME: 83fca0012@smtp-brevo.com
DISCOURSE_SMTP_PASSWORD: "5AHQXrf4LDUmRB1J"
#DISCOURSE_SMTP_ENABLE_START_TLS: true # (opcional, padrão: true)
DISCOURSE_SMTP_DOMAIN: discourse.example.com # (obrigatório por alguns provedores)
#DISCOURSE_SMTP_OPENSSL_VERIFY_MODE: peer # (opcional, padrão: peer, valores válidos: none, peer, client_once, fail_if_no_peer_cert)
#DISCOURSE_SMTP_AUTHENTICATION: plain # (padrão: plain, valores válidos: plain, login, cram_md5)
\nabaixo está um patch conciso que:\n\n- para de usar sed\n- constrói um SMTP_URL com codificação percentual\n- edita containers/app.yml via Ruby’s YAML (Psych), então o escape/codificação YAML é tratado por um analisador real\n- exclui as variáveis SMTP por chave para evitar contradições\n\naplicar com git apply -p0 no repositório discourse_docker.\n\n- - -\n\nPatch 1 -\n\ndiscourse-setup\n\n(escreve SMTP usando Ruby YAML, não sed)\n\ndiff\n--- a/discourse-setup\n+++ b/discourse-setup\n@@ -867,6 +867,77 @@ write_smtp_settings() {\n local app_yml=\"containers/app.yml\"\n [[ -f \"$app_yml\" ]] || die \"Cannot find $app_yml. Did you run bootstrap?\"\n\n+ # Constrói um SMTP_URL codificado em URL usando a biblioteca padrão do Python (sem jogos de escape do shell)\n+ urlencode() {\n+ python3 - \u003c\u003c'PY'\n+import sys, urllib.parse\n+print(urllib.parse.quote(sys.stdin.read().strip(), safe='._~-'))\n+PY\n+ }\n+\n+ # IMPORTANTE: lê variáveis sem manipulação de barra invertida\n+ # (estas vêm de prompts anteriores; apenas garanta que -r seja usado no momento do prompt)\n+ local addr=\"$smtp_address\"\n+ local port=\"$smtp_port\"\n+ local user_enc pass_enc\n+ user_enc=\"$(printf '%s' \"$smtp_user\" | urlencode)\"\n+ pass_enc=\"$(printf '%s' \"$smtp_password\" | urlencode)\"\n+ local smtp_url=\"smtp://${user_enc}:${pass_enc}@${addr}:${port}\"\n+\n+ # Usa Ruby para carregar/modificar/descarregar YAML com segurança (elimina 3 camadas de escape)\n+ ruby - \u003c\u003c'RUBY' \"$app_yml\" \"$smtp_url\"\n+require \"yaml\"\n+require \"psych\"\n+path, url = ARGV\n+doc = YAML.safe_load(File.read(path), permitted_classes: [], aliases: true) || {}\n+\n+# Garante que a estrutura de nível superior seja um Hash e tenha env\n+unless doc.is_a?(Hash)\n+ abort \"containers/app.yml não analisa para um Hash\"\n+end\n+doc[\"env\"] ||= {}\n+env = doc[\"env\"]\n+\n+# Escreve SMTP_URL de linha única; remove variáveis por chave para evitar conflitos\n+env[\"SMTP_URL\"] = url\n+%w[DISCOURSE_SMTP_ADDRESS DISCOURSE_SMTP_PORT DISCOURSE_SMTP_USER_NAME DISCOURSE_SMTP_PASSWORD].each { |k| env.delete(k) }\n+\n+# Descarrega de volta. (Psych preserva strings com aspas seguras conforme necessário.)\n+File.write(path, Psych.dump(doc))\n+RUBY\n+\n+ # rápida verificação de sanidade para a falha clássica de \"senha prefixada pelo nome de usuário\"\n+ python3 - \u003c\u003c'PY'\n+import re, sys\n+y = open(\"containers/app.yml\",\"r\",encoding=\"utf-8\").read()\nm = re.search(r'^\\s*SMTP_URL:\\s*(?:\"|\\')?([^\\r\\n\"\\']+)', y, re.M)\n+assert m, \"SMTP_URL ausente após a escrita\"\n+creds = m.group(1).split('@',1)[0].split('://',1)[-1]\n+assert \":\" in creds, \"Credenciais SMTP_URL ausentes ':'\"\n+u, p = creds.split(':',1)\n+assert not p.startswith(u), \"Senha parece prefixada pelo nome de usuário\"\n+print(\"SMTP_URL parece razoável.\")\n+PY\n+}\n+\n- # Escreve entradas SMTP por chave (endereço/porta/nome de usuário/senha)\n- # (legado: realizado via substituições de sed)\n- # NOTA: historicamente frágil com caracteres especiais\n- update_setting_yaml \"DISCOURSE_SMTP_ADDRESS\" \"$smtp_address\"\n- update_setting_yaml \"DISCOURSE_SMTP_PORT\" \"$smtp_port\"\n- update_setting_yaml \"DISCOURSE_SMTP_USER_NAME\" \"$smtp_user\"\n- update_setting_yaml \"DISCOURSE_SMTP_PASSWORD\" \"$smtp_password\"\n-}\n+ # (escritas legadas por chave removidas em favor de SMTP_URL via YAML)\n+}\n\n\nentão Patch 2 -\n\ntemplates/web.template.yml\n\n(para documentar o caminho mais seguro)\n\ndiff\n--- a/templates/web.template.yml\n+++ b/templates/web.template.yml\n@@ -68,6 +68,14 @@ params:\n DISCOURSE_SMTP_ENABLE_START_TLS: true\n #DISCOURSE_NOTIFICATION_EMAIL: noreply@example.com\n\n+ ## Configuração SMTP preferida em linha única (definida por discourse-setup):\n+ ## Codifica em URL o nome de usuário e a senha; exemplo:\n+ ## SMTP_URL: \"smtp://user%40example.com:p%40ss%3Aword@smtp.example.com:587\"\n+ ##\n+ #SMTP_URL:\n+\n ## Se você não puder usar SMTP_URL, pode definir variáveis por chave em vez disso.\n ## Cuidado ao editar essas linhas com ferramentas de shell, pois os valores podem incluir\n ## caracteres como @, :, /, \", \\ ou novas linhas.\n\n\npor que isso funciona (e o que evita)\n\t•\tcamada bash: interpolamos apenas variáveis simples; segredos são passados para Python/Ruby via stdin/argv, não através de regexes sed ou avaliações de shell.\n\t•\tcamada sed: removida completamente.\n\t•\tcamada YAML: Ruby/Psych lida com aspas e escape corretamente; sem aspas manuais.\n\t•\tcredenciais SMTP: o escape % na SMTP_URL é o lugar certo para codificar caracteres especiais para autenticação.\n\nse você preferir manter variáveis por chave, posso fornecer um patch irmão que usa a mesma abordagem Ruby-YAML para definir DISCOURSE_SMTP_* diretamente (ainda sem sed), mas a rota SMTP_URL é a mais limpa porque é uma chave, uma escrita, uma etapa de codificação.\n
Infelizmente, não podemos depender da instalação do Ruby no sistema host em instalações do discourse - seria relativamente simples ter o script inteiro em Ruby se pudéssemos, mas não há garantia.
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:
hmmm eu me pergunto se podemos simplesmente agrupar a lógica no launcher2, então podemos usá-lo para executar a configuração.
A quantidade de saltos que o discourse-setup está dando é espetacular.
É possível compilar o launcher em um arquivo executável binário para lançamento? Dessa forma, o launcher (bash) poderá baixar apenas o arquivo executável (binário) correspondente com base no sistema.