# ./discourse-setup 将 SMTP 用户名放在 SMTP 密码的开头

**URL:** <https://meta.discourse.org/t/discourse-setup-puts-smtp-username-at-start-of-smtp-password/381710>\
**Category:** Support\
**Tags:** install\
**Created:** [2025年九月5日 12:37 UTC](https://meta.discourse.org/t/discourse-setup-puts-smtp-username-at-start-of-smtp-password/381710 "2025-09-05T12:37:46Z")\
**Posts on this page:** 1\
**Showing post:** 17

<div class="post-metadata">

**Author:** ![Ethsim2](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/ethsim2/32/522255_2.png) [@Ethsim2](https://meta.discourse.org/u/Ethsim2)\
**Post date:** [2025年九月15日 19:50 UTC](https://meta.discourse.org/t/discourse-setup-puts-smtp-username-at-start-of-smtp-password/381710/17 "2025-09-15T19:50:18Z")

</div>

> **LLM/AI 生成**
>
> 明白了——主机上没有 Ruby/Python。这是一个纯 Bash + awk 的补丁，它：  
> • 仅使用 Bash（逐字节循环；无 Python）构建一个百分比编码的 SMTP\_URL，  
> • 使用 awk（无 sed）在 env: 块下插入或替换 SMTP\_URL，  
> • 删除每个键的 DISCOURSE\_SMTP\_\* 行（安全的锚定删除），  
> • 仅使用 grep/awk 添加一个简单的健全性检查。
> 
> 在 discourse\_docker 仓库中使用 `git apply -p0` 应用。
> 
> ```diff
> --- 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?"
>  
> + ##############################################
> + # Pure-Bash URL encoder for SMTP credentials #
> + ##############################################
> + # Encodes everything except A-Z a-z 0-9 . _ ~ -
> + # Works byte-by-byte; requires bash and printf.
> + urlencode_cred() {
> + local s="$1" out= i ch o
> + # set C locale to get byte semantics
> + LC_ALL=C
> + for ((i=0; i<${#s}; i++)); do
> + ch="${s:i:1}"
> + case "$ch" in
> + [A-Za-z0-9._~-])
> + out+="$ch"
> + ;;
> + *)
> + # Get byte value: print char, read with od, then format %HH
> + # Avoid external heavy deps; od is in coreutils / busybox.
> + o=$(printf '%s' "$ch" | od -An -tu1 | awk '{$1=$1;print $1}')
> + # If od somehow failed (empty), fall back to hex via printf %02X of first 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"
> + }
> +
> + # Build SMTP_URL (single line) from collected answers
> + # These vars are gathered earlier in 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}"
> +
> + ########################################################
> + # YAML-safe edit via awk (no sed / no external runtimes)
> + # - ensure env: exists
> + # - insert or replace SMTP_URL under env:
> + # - remove DISCOURSE_SMTP_* keys
> + ########################################################
> + awk -v NEWVAL="$smtp_url" '
> + BEGIN{
> + have_env=0; in_env=0; inserted=0
> + }
> + # detect env: line at top level (start of line, possibly indented 0..)
> + # we’ll consider two-space indentation for children.
> + /^[[:space:]]*env:[[:space:]]*$/ {
> + print; have_env=1; in_env=1; next
> + }
> + # leaving env: block when indentation returns to 0 or next top-level key
> + in_env && /^[^[:space:]]/ {
> + if (!inserted) {
> + print " SMTP_URL: \"' NEWVAL "\""
> + inserted=1
> + }
> + in_env=0
> + }
> + # while in env:, handle replacements and deletions
> + in_env {
> + # drop per-key DISCOURSE_SMTP_* lines entirely
> + if ($0 ~ /^[[:space:]]*DISCOURSE_SMTP_(ADDRESS|PORT|USER_NAME|PASSWORD):/) next
> + # replace existing SMTP_URL line
> + if ($0 ~ /^[[:space:]]*SMTP_URL:[[:space:]]*/) {
> + print " SMTP_URL: \"' NEWVAL "\""
> + inserted=1
> + next
> + }
> + print
> + next
> + }
> + { print }
> + END{
> + # If env: never existed, append it with the key
> + if (!have_env) {
> + print ""
> + print "env:"
> + print " SMTP_URL: \"' NEWVAL "\""
> + } else if (in_env && !inserted) {
> + # env: existed and we were still in it at EOF
> + print " SMTP_URL: \"' NEWVAL "\""
> + }
> + }
> + ' "$app_yml" > "$app_yml.tmp.$$" && mv "$app_yml.tmp.$$" "$app_yml"
> +
> + ##############################################
> + # Sanity check: basic guard against mangling #
> + ##############################################
> + # 1) SMTP_URL present
> + grep -q '^[[:space:]]*SMTP_URL:' "$app_yml" || die "SMTP_URL not written to $app_yml"
> + # 2) password not prefixed by username (classic failure signature)
> + awk '
> + BEGIN{ok=1}
> + /^[[:space:]]*SMTP_URL:[[:space:]]*"/ {
> + line=$0
> + gsub(/^[[:space:]]*SMTP_URL:[[:space:]]*"/,"",line)
> + gsub(/".*$/,"",line)
> + # Extract creds before @ and after scheme
> + # e.g. 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 #
> ##############################################
> @@ -996,16 +1120,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"
> -}
> + # Legacy per-key writes removed in favor of atomic SMTP_URL write above.
> +}
> +
> 
> ```
> 
> 为什么这符合您的要求  
> • 主机上没有 Ruby/Python：只有 Bash + awk + od/printf/grep（全部是标准的）。  
> • 没有 sed 替换敏感信息：我们避免了“Bash → sed → YAML”的多重转义陷阱。  
> • 原子性写入：编辑到临时文件，然后 `mv` 到 containers/app.yml。  
> • 向后兼容：如果 env: 不存在，我们会最小化地创建它；如果存在，我们会就地更新并删除冲突的 DISCOURSE\_SMTP\_\* 行。  
> • 凭据在插入前会进行 URL 编码，因此用户/密码中的特殊字符不会破坏 Rails/Net::SMTP 中的 URL 解析器。
> 
> 如果您希望保留每个键的变量而不是 SMTP\_URL，我可以发送一个同级补丁，该补丁（仍然没有 sed）使用相同的 awk 策略来设置：
> 
> ```plaintext
> DISCOURSE_SMTP_ADDRESS
> DISCOURSE_SMTP_PORT
> DISCOURSE_SMTP_USER_NAME
> DISCOURSE_SMTP_PASSWORD
> 
> ```
> 
> …使用 YAML 安全的双引号值和锚定替换。

---

_[View the full topic](https://meta.discourse.org/t/discourse-setup-puts-smtp-username-at-start-of-smtp-password/381710)._
