./discourse-setup が SMTP ユーザー名を SMTP パスワードの先頭に配置する

「LLM/AI生成の推奨事項」

以下は、以下の機能を持つタイトなパッチです。

  • sedの使用を停止します
  • パーセントエンコードされたSMTP_URLを構築します
  • RubyのYAML(Psych)を使用してcontainers/app.ymlを編集します。これにより、YAMLの引用/エスケープは実際のパーサーによって処理されます。
  • 一貫性を避けるために、キーごとのSMTP変数を削除します。

discourse_dockerリポジトリでgit apply -p0を使用して適用してください。


パッチ1 -

discourse-setup

(sedではなく、Ruby YAMLを使用してSMTPを書き込みます)

--- a/discourse-setup
+++ b/discourse-setup
@@ -867,6 +867,77 @@ write_smtp_settings() {
   local app_yml="containers/app.yml"
   [[ -f "$app_yml" ]] || die "Cannot find $app_yml. Did you run bootstrap?"

+  # Python標準ライブラリを使用してURLエンコードされたSMTP_URLを構築します(シェルエスケープゲームなし)
+  urlencode() {
+    python3 - <<'PY'
+import sys, urllib.parse
+print(urllib.parse.quote(sys.stdin.read().strip(), safe='._~-'))
+PY
+  }
+
+  # IMPORTANT: バックスラッシュの誤変換なしで変数を読み取ります
+  # (これらは以前のプロンプトから来ています。プロンプト時に-rを使用するだけで十分です)
+  local addr="$smtp_address"
+  local port="$smtp_port"
+  local user_enc pass_enc
+  user_enc="$(printf '%s' "$smtp_user"     | urlencode)"
+  pass_enc="$(printf '%s' "$smtp_password" | urlencode)"
+  local smtp_url="smtp://${user_enc}:${pass_enc}@${addr}:${port}"
+
+  # 安全にYAMLをロード/変更/ダンプするためにRubyを使用します(3層のエスケープを削除します)
+  ruby - <<'RUBY' "$app_yml" "$smtp_url"
+require "yaml"
+require "psych"
+path, url = ARGV
+doc = YAML.safe_load(File.read(path), permitted_classes: [], aliases: true) || {}
+
+# トップレベル構造がHashであり、envを持つことを確認します
+unless doc.is_a?(Hash)
+  abort "containers/app.yml does not parse to a Hash"
+end
+doc["env"] ||= {}
+env = doc["env"]
+
+# 単一行のSMTP_URLを書き込みます。競合を避けるためにキーごとの変数を削除します。
+env["SMTP_URL"] = url
+%w[DISCOURSE_SMTP_ADDRESS DISCOURSE_SMTP_PORT DISCOURSE_SMTP_USER_NAME DISCOURSE_SMTP_PASSWORD].each { |k| env.delete(k) }
+
+# 書き戻します。(Psychは必要に応じて安全に引用符で囲まれた文字列を保持します。)
+File.write(path, Psych.dump(doc))
+RUBY
+
+  # 古典的な「ユーザー名の前にパスワードがプレフィックスされている」失敗のための簡単な健全性チェック
+  python3 - <<'PY'
+import re, sys
+y = open("containers/app.yml","r",encoding="utf-8").read()
+m = re.search(r'^\\s*SMTP_URL:\\s*(?:\"|\\')?([^\\r\\n\"\\']+)', y, re.M)
+assert m, "SMTP_URL missing after write"
+creds = m.group(1).split('@',1)[0].split('://',1)[-1]
+assert ":" in creds, "SMTP_URL creds missing ':'"
+u, p = creds.split(':',1)
+assert not p.startswith(u), "Password appears prefixed by username"
+print("SMTP_URL looks sane.")
+PY
+}
+
-  # 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"
-}
+  # (レガシーなキーごとの書き込みは、YAML経由のSMTP_URLに置き換えられました)
+}

次にパッチ2 -

templates/web.template.yml

(より安全なパスを文書化するため)

--- a/templates/web.template.yml
+++ b/templates/web.template.yml
@@ -68,6 +68,14 @@ params:
   DISCOURSE_SMTP_ENABLE_START_TLS: true
   #DISCOURSE_NOTIFICATION_EMAIL: noreply@example.com
 
+  ## 推奨される単一行SMTP設定(discourse-setupによって設定されます):
+  ## ユーザー名とパスワードをURLエンコードします。例:
+  ##   SMTP_URL: "smtp://user%40example.com:p%40ss%3Aword@smtp.example.com:587"
+  ##
+  #SMTP_URL:
+
   ## SMTP_URLを使用できない場合は、代わりにキーごとの変数を使用できます。
   ## 値に@、:、/、"、\、または改行などの文字が含まれている場合、
   ## これらの行をシェルツールで編集するのは脆弱になる可能性があることに注意してください。

これが機能する理由(および回避されること)

  • bashレイヤー:単純な変数をのみ補間します。秘密はsed正規表現やシェルevalを介してではなく、stdin/argvを介してPython/Rubyに渡されます。
  • sedレイヤー:完全に削除されました。
  • YAMLレイヤー:Ruby/Psychが引用符とエスケープを正しく処理します。手作業での引用はありません。
  • SMTP認証情報:SMTP_URLでの%エンコーディングは、認証のための特殊文字をエンコードするのに適した場所です。

キーごとの変数を維持したい場合は、DISCOURSE_SMTP_ *を直接設定するために同じRuby-YAMLアプローチを使用する姉妹パッチを提供できます(ただし、sedは使用しません)。ただし、SMTP_URLルートは、キーが1つ、書き込みが1つ、エンコードステップが1つであるため、最もクリーンです。