This causes mail delivery to fail, and discourse-doctor confirms the mangled password.
Notes
• Quoting the password explicitly at the prompt does not change the outcome.
• Password contains special characters (@, !), but the problem is not just YAML quoting: the username string is literally concatenated at the start of the password.
• Reproducible across multiple runs of ./discourse-setup.
@Ethsim2 als je het wachtwoord verandert in iets met alleen cijfers en letters en niet hetzelfde als een deel van de gebruikersnaam, heb je dan dit probleem?
Yeah. That’s the issue. There are multiple levels of escaping that need to happen, like when bash reads the value, when bash hands the value to sed, when sed replaces it, and then, maybe, when the yml file gets it. It’s a known issue:
\nhieronder staat een strakke patch die:\n\n- stopt met het gebruik van sed\n- construeert een percent-gecodeerde SMTP_URL\n- bewerkt containers/app.yml via Ruby’s YAML (Psych), zodat YAML-quoting/escaping wordt afgehandeld door een echte parser\n- verwijdert de per-sleutel SMTP-variabelen om tegenstrijdigheden te voorkomen\n\n toepassen met git apply -p0 in de discourse_docker repo.\n\n- - -\n\nPatch 1 -\n\ndiscourse-setup\n\n(schrijf SMTP met Ruby YAML, niet met 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+ # Bouw een URL-gecodeerde SMTP_URL met Python stdlib (geen shell escaping games)\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+ # BELANGRIJK: lees variabelen zonder backslash-vervorming\n+ # (deze komen van eerdere prompts; zorg ervoor dat -r wordt gebruikt bij het prompten)\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+ # Gebruik Ruby om veilig YAML te laden/bewerken/dumpen (doodt 3 lagen van escaping)\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+# Zorg ervoor dat de top-level structuur een Hash is en env heeft\n+unless doc.is_a?(Hash)\n+ abort \"containers/app.yml parseert niet naar een Hash\"\n+end\n+doc[\"env\"] ||= {}\n+env = doc[\"env\"]\n+\n+# Schrijf enkele regel SMTP_URL; verwijder per-sleutel variabelen om conflicten te voorkomen\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+# Dump terug. (Psych behoudt strings veilig geciteerd zoals nodig.)\n+File.write(path, Psych.dump(doc))\n+RUBY\n+\n+ # snelle sanity check voor de klassieke "wachtwoord voorafgegaan door gebruikersnaam" mislukking\n+ python3 - \u003c\u003c'PY'\n+import re, sys\n+y = open(\"containers/app.yml\",\"r\",encoding=\"utf-8\").read()\n+m = re.search(r'^\\s*SMTP_URL:\\s*(?:\"|\\')?([^\\r\\n\"\\']+)', y, re.M)\n+assert m, \"SMTP_URL ontbreekt na schrijven\"\n+creds = m.group(1).split('@',1)[0].split('://',1)[-1]\n+assert \":\" in creds, \"SMTP_URL creds missen ':'\"\n+u, p = creds.split(':',1)\n+assert not p.startswith(u), \"Wachtwoord lijkt voorafgegaan door gebruikersnaam\"\n+print(\"SMTP_URL ziet er gezond uit.\")\n+PY\n+}\n+\n- # Schrijf per-sleutel SMTP-invoer (adres/poort/gebruikersnaam/wachtwoord)\n- # (legacy: uitgevoerd via sed-substituties)\n- # OPMERKING: historisch fragiel met speciale tekens\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+ # (legacy per-sleutel schrijfacties verwijderd ten gunste van SMTP_URL via YAML)\n+}\n\n\ndan Patch 2 -\n\ntemplates/web.template.yml\n\n(om het veiligere pad te documenteren)\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+ ## Voorkeurs SMTP-configuratie in één regel (ingesteld door discourse-setup):\n+ ## URL-codeer gebruikersnaam en wachtwoord; voorbeeld:\n+ ## SMTP_URL: \"smtp://user%40example.com:p%40ss%3Aword@smtp.example.com:587\"\n+ ##\n+ #SMTP_URL:\n+\n ## Als u SMTP_URL niet kunt gebruiken, kunt u in plaats daarvan per-sleutel variabelen instellen.\n ## Pas op dat het bewerken van die regels met shell-tools fragiel kan zijn als waarden\n ## tekens bevatten zoals @, :, /, \", \\, of nieuwe regels.\n\n\nwaarom dit werkt (en wat het vermijdt)\n\t•\tbash-laag: we interpoleren alleen eenvoudige variabelen; geheimen worden via stdin/argv aan Python/Ruby doorgegeven, niet via sed-regexes of shell-evaluaties.\n\t•\tsed-laag: volledig verwijderd.\n\t•\tYAML-laag: Ruby/Psych behandelt quoting en escaping correct; geen handmatig gemaakte quoting.\n\t•\tSMTP-credentials: %-encoding in SMTP_URL is de juiste plaats om speciale tekens voor authenticatie te coderen.\n\nals u de voorkeur geeft aan het behouden van per-sleutel variabelen, kan ik u een zusterpatch geven die dezelfde Ruby-YAML-aanpak gebruikt om DISCOURSE_SMTP_* direct in te stellen (nog steeds geen sed), maar de SMTP_URL-route is de schoonste omdat het één sleutel, één schrijfactie, één coderingstap is.\n
We kunnen er helaas niet op vertrouwen dat Ruby is geïnstalleerd op het hostsysteem in discourse-installaties - het zou relatief eenvoudig zijn om het hele script een ruby-script te laten zijn als we dat konden, maar er is geen garantie.
Got it — no Ruby/Python on the host. Here’s a pure Bash + awk patch that:
• builds a percent-encoded SMTP_URL using only Bash (byte-wise loop; no Python),
• inserts or replaces SMTP_URL under the env: block using awk (no sed),
• removes the per-key DISCOURSE_SMTP_* lines (safe anchored deletes),
• adds a tiny sanity check using grep/awk only.
Apply with git apply -p0 in the discourse_docker repo.
--- 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"
}
- # 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.
+}
Why this meets your constraint
• Zero Ruby/Python on the host: only Bash + awk + od/printf/grep (all standard).
• No sed replacement of secrets: we avoid the “Bash → sed → YAML” multi-escape trap.
• Atomic-ish write: edit into a temp file, then mv over containers/app.yml.
• Backwards-safe: if env: doesn’t exist, we create it minimally; if it does, we update in place and remove conflicting DISCOURSE_SMTP_* lines.
• Credentials are URL-encoded before insertion, so special chars in user/pass won’t break the URL parser in Rails/Net::SMTP.
If you’d prefer to keep the per-key variables instead of SMTP_URL, I can send a sibling patch that (still without sed) uses the same awk strategy to set:
hmmm ik vraag me af of we de logica gewoon in launcher2 kunnen bundelen, dan kunnen we die gebruiken om de installatie uit te voeren.
De hoeveelheid horden waar discourse-setup overheen springt is spectaculair.
Is het mogelijk om de launcher te compileren tot een uitvoerbaar binair bestand voor release? Op deze manier kan de launcher (bash) alleen het bijbehorende uitvoerbare bestand (binair) downloaden op basis van het systeem.