mirror of
git://f0xx.org/ac/ac-deploy
synced 2026-07-29 05:00:35 +03:00
cluster0: fix secrets MAIL_FROM parsing; normalize RFC5322 From in compose
load_secrets_lab quotes values safely; compose reads MAIL_FROM via read_secret. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -16,7 +16,8 @@ final class AuthMailer {
|
||||
array $attachments = [],
|
||||
?string $bodyHtml = null
|
||||
): bool {
|
||||
$from = (string) cfg('mail.from', 'Android Cast <noreply@apps.f0xx.org>');
|
||||
$smtpUser = (string) cfg('mail.smtp.username', '');
|
||||
$from = self::resolveFromAddress($smtpUser);
|
||||
$replyTo = (string) cfg('mail.reply_to', '');
|
||||
$transport = strtolower((string) cfg('mail.transport', 'smtp'));
|
||||
if ($transport === 'sendmail') {
|
||||
@@ -45,22 +46,37 @@ final class AuthMailer {
|
||||
}
|
||||
}
|
||||
$body = "Open this link to verify your email (valid 24h):\n\n"
|
||||
. $shortUrl . "\n\n";
|
||||
$bodyHtml = null;
|
||||
. $shortUrl . "\n";
|
||||
if ($qrImageUrl !== null) {
|
||||
$body .= "Or scan the QR code on your phone — open this image URL:\n"
|
||||
$body .= "\nOr scan the QR code on your phone — open this image URL:\n"
|
||||
. $qrImageUrl . "\n";
|
||||
$escUrl = htmlspecialchars($shortUrl, ENT_QUOTES | ENT_HTML5, 'UTF-8');
|
||||
$escQr = htmlspecialchars($qrImageUrl, ENT_QUOTES | ENT_HTML5, 'UTF-8');
|
||||
$bodyHtml = '<!DOCTYPE html><html><body style="font-family:sans-serif;line-height:1.5">'
|
||||
. '<p>Open this link to verify your email (valid 24h):</p>'
|
||||
. '<p><a href="' . $escUrl . '">' . $escUrl . '</a></p>'
|
||||
. '<p>Or scan this QR code on your phone:</p>'
|
||||
. '<p><img src="' . $escQr . '" alt="Verify email QR code" width="256" height="256"></p>'
|
||||
. '</body></html>';
|
||||
}
|
||||
// Hosted QR image (no PNG attachment) — Gmail blocks short-link + attachment as phishing.
|
||||
return self::send($to, $subject, $body, [], $bodyHtml);
|
||||
// Plain text only — Gmail relay flags multipart HTML + short links + remote QR images.
|
||||
return self::send($to, $subject, $body);
|
||||
}
|
||||
|
||||
private static function resolveFromAddress(string $smtpUser): string {
|
||||
$from = (string) cfg('mail.from', 'Android Cast <noreply@apps.f0xx.org>');
|
||||
$host = strtolower((string) cfg('mail.smtp.host', ''));
|
||||
if ($host === 'smtp.gmail.com' && $smtpUser !== '') {
|
||||
return $smtpUser;
|
||||
}
|
||||
return self::normalizeFrom($from, $smtpUser);
|
||||
}
|
||||
|
||||
private static function normalizeFrom(string $from, string $smtpUser = ''): string {
|
||||
if (preg_match('/<[^>]+>/', trim($from))) {
|
||||
return trim($from);
|
||||
}
|
||||
if (preg_match('/\S+@\S+/', $from, $m)) {
|
||||
$email = $m[0];
|
||||
$name = trim(str_replace($email, '', $from));
|
||||
return $name !== '' ? "{$name} <{$email}>" : $email;
|
||||
}
|
||||
if ($smtpUser !== '' && str_contains($smtpUser, '@')) {
|
||||
return $smtpUser;
|
||||
}
|
||||
return trim($from);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -119,7 +135,16 @@ final class AuthMailer {
|
||||
fclose($fp);
|
||||
return false;
|
||||
}
|
||||
$ehloHost = 'localhost';
|
||||
$ehloHost = (string) cfg('mail.smtp.ehlo', '');
|
||||
if ($ehloHost === '') {
|
||||
$ehloHost = php_uname('n') ?: 'localhost';
|
||||
if ($ehloHost === 'localhost' || !str_contains($ehloHost, '.')) {
|
||||
$origin = (string) cfg('public_origin', '');
|
||||
if ($origin !== '' && ($host = parse_url($origin, PHP_URL_HOST))) {
|
||||
$ehloHost = (string) $host;
|
||||
}
|
||||
}
|
||||
}
|
||||
fwrite($fp, "EHLO {$ehloHost}\r\n");
|
||||
if (!self::smtpExpect($fp, [250])) {
|
||||
fclose($fp);
|
||||
@@ -175,18 +200,7 @@ final class AuthMailer {
|
||||
return false;
|
||||
}
|
||||
$payload = self::buildMessageBody($body, $attachments, $bodyHtml);
|
||||
$msg = "From: {$from}\r\n";
|
||||
if ($replyTo !== '') {
|
||||
$msg .= "Reply-To: {$replyTo}\r\n";
|
||||
}
|
||||
$msg .= "To: {$to}\r\nSubject: {$subject}\r\n";
|
||||
if ($payload['mime'] === null) {
|
||||
$msg .= "Content-Type: text/plain; charset=UTF-8\r\n\r\n{$body}\r\n.\r\n";
|
||||
} else {
|
||||
$msg .= "MIME-Version: 1.0\r\n";
|
||||
$msg .= 'Content-Type: ' . $payload['mime'] . '; boundary="' . $payload['boundary'] . "\"\r\n\r\n";
|
||||
$msg .= $payload['body'] . "\r\n.\r\n";
|
||||
}
|
||||
$msg = self::buildSmtpHeaders($from, $to, $replyTo, $subject, $payload);
|
||||
fwrite($fp, $msg);
|
||||
if (!self::smtpExpect($fp, [250])) {
|
||||
fclose($fp);
|
||||
@@ -197,6 +211,43 @@ final class AuthMailer {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array{mime:?string,boundary:?string,body:string} $payload
|
||||
*/
|
||||
private static function buildSmtpHeaders(
|
||||
string $from,
|
||||
string $to,
|
||||
string $replyTo,
|
||||
string $subject,
|
||||
array $payload
|
||||
): string {
|
||||
$msg = 'Date: ' . gmdate('D, d M Y H:i:s') . " +0000\r\n";
|
||||
$msg .= 'Message-ID: <' . bin2hex(random_bytes(16)) . '@androidcast.local>' . "\r\n";
|
||||
$msg .= "From: {$from}\r\n";
|
||||
if ($replyTo !== '') {
|
||||
$msg .= "Reply-To: {$replyTo}\r\n";
|
||||
}
|
||||
$msg .= "To: {$to}\r\n";
|
||||
$msg .= 'Subject: ' . self::encodeHeader($subject) . "\r\n";
|
||||
$msg .= "MIME-Version: 1.0\r\n";
|
||||
if ($payload['mime'] === null) {
|
||||
$msg .= "Content-Type: text/plain; charset=UTF-8\r\n";
|
||||
$msg .= "Content-Transfer-Encoding: 8bit\r\n\r\n";
|
||||
$msg .= $payload['body'] . "\r\n.\r\n";
|
||||
return $msg;
|
||||
}
|
||||
$msg .= 'Content-Type: ' . $payload['mime'] . '; boundary="' . $payload['boundary'] . "\"\r\n\r\n";
|
||||
$msg .= $payload['body'] . "\r\n.\r\n";
|
||||
return $msg;
|
||||
}
|
||||
|
||||
private static function encodeHeader(string $value): string {
|
||||
if (preg_match('/^[\x20-\x7E]*$/', $value)) {
|
||||
return $value;
|
||||
}
|
||||
return '=?UTF-8?B?' . base64_encode($value) . '?=';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<array{name:string,data:string,type:string}> $attachments
|
||||
* @return array{mime:?string,boundary:?string,body:string}
|
||||
|
||||
@@ -118,7 +118,8 @@ AUTH_KEY="${AUTH_ENCRYPTION_KEY:-lab-cluster-dev-32-char-min-secret!!}"
|
||||
_MSMTP_PASS="$(read_secret MSMTP_APP_PASSWORD 2>/dev/null || true)"
|
||||
[ -n "$_MSMTP_PASS" ] || _MSMTP_PASS="${MSMTP_APP_PASSWORD:-}"
|
||||
MAIL_TRANSPORT="${MAIL_TRANSPORT:-sendmail}"
|
||||
MAIL_FROM="${MAIL_FROM:-Android Cast Lab <bestcastr@gmail.com>}"
|
||||
_mail_from_secret="$(read_secret MAIL_FROM 2>/dev/null || true)"
|
||||
[ -n "$_mail_from_secret" ] || _mail_from_secret="${MAIL_FROM:-}"
|
||||
MAIL_REPLY="${MAIL_REPLY_TO:-bestcastr@gmail.com}"
|
||||
SMTP_HOST="${SMTP_HOST:-127.0.0.1}"
|
||||
SMTP_PORT="${SMTP_PORT:-587}"
|
||||
@@ -130,9 +131,11 @@ if [ -n "$_MSMTP_PASS" ]; then
|
||||
SMTP_HOST=smtp.gmail.com
|
||||
SMTP_USER="${MSMTP_USER:-bestcastr@gmail.com}"
|
||||
SMTP_PASS="$_MSMTP_PASS"
|
||||
MAIL_FROM="${MAIL_FROM:-Android Cast <bestcastr@gmail.com>}"
|
||||
fi
|
||||
unset _MSMTP_PASS
|
||||
MAIL_FROM="$(normalize_mail_from "$_mail_from_secret" "$SMTP_USER")"
|
||||
[ -n "$MAIL_FROM" ] || MAIL_FROM="Android Cast Lab <${SMTP_USER:-bestcastr@gmail.com}>"
|
||||
unset _MSMTP_PASS _mail_from_secret
|
||||
MAIL_FROM_PHP="$(printf '%s' "$MAIL_FROM" | sed "s/'/\\\\'/g")"
|
||||
AUTH_BEARER_ID="$(read_secret URL_SHORTENER_AUTH_BEARER_ID 2>/dev/null || true)"
|
||||
[ -n "$AUTH_BEARER_ID" ] || AUTH_BEARER_ID="${URL_SHORTENER_AUTH_BEARER_ID:-0}"
|
||||
mkdir -p "${COMPOSED}/config"
|
||||
@@ -174,7 +177,7 @@ return [
|
||||
],
|
||||
'mail' => [
|
||||
'transport' => '${MAIL_TRANSPORT}',
|
||||
'from' => '${MAIL_FROM}',
|
||||
'from' => '${MAIL_FROM_PHP}',
|
||||
'reply_to' => '${MAIL_REPLY}',
|
||||
'smtp' => [
|
||||
'host' => '${SMTP_HOST}',
|
||||
|
||||
@@ -189,21 +189,55 @@ read_cred() {
|
||||
load_secrets_lab() {
|
||||
_file="${CAST_CLUSTER_ROOT}/secrets.lab.env"
|
||||
[ -f "$_file" ] || return 0
|
||||
# Export only simple KEY=value lines (no shell metacharacters in values via source).
|
||||
_tmp="/tmp/cast-secrets-load-$$"
|
||||
: > "$_tmp"
|
||||
while IFS= read -r _line || [ -n "$_line" ]; do
|
||||
case "$_line" in
|
||||
''|\#*) continue ;;
|
||||
*=*)
|
||||
_k="${_line%%=*}"
|
||||
_v="${_line#*=}"
|
||||
case "$_v" in
|
||||
\"*) _v="${_v#\"}"; _v="${_v%\"}" ;;
|
||||
esac
|
||||
case "$_k" in
|
||||
MAIL_*|MSMTP_*|SMTP_*|AUTH_*|GITEA_*|WG_*|RSSH_*|URL_SHORTENER_*)
|
||||
export "$_k=$_v"
|
||||
_v_escaped="$(printf '%s' "$_v" | sed "s/'/'\\\\''/g")"
|
||||
printf "%s='%s'\n" "$_k" "$_v_escaped" >> "$_tmp"
|
||||
;;
|
||||
esac
|
||||
;;
|
||||
esac
|
||||
done < "$_file"
|
||||
# shellcheck disable=SC1090
|
||||
. "$_tmp"
|
||||
rm -f "$_tmp"
|
||||
}
|
||||
|
||||
# RFC5322 From — secrets without angle brackets break Gmail SMTP (shell ate <email>).
|
||||
normalize_mail_from() {
|
||||
_in="$1"
|
||||
_smtp_user="${2:-}"
|
||||
case "$_in" in
|
||||
*'<'*'>'*) printf '%s' "$_in"; return ;;
|
||||
esac
|
||||
case "$_in" in
|
||||
*@*)
|
||||
_email="$(printf '%s' "$_in" | awk '{print $NF}')"
|
||||
_name="$(printf '%s' "$_in" | sed "s/ ${_email}\$//")"
|
||||
if [ -n "$_name" ]; then
|
||||
printf '%s <%s>' "$_name" "$_email"
|
||||
else
|
||||
printf '%s' "$_email"
|
||||
fi
|
||||
return
|
||||
;;
|
||||
esac
|
||||
if [ -n "$_smtp_user" ] && printf '%s' "$_smtp_user" | grep -q '@'; then
|
||||
printf 'Android Cast Lab <%s>' "$_smtp_user"
|
||||
return
|
||||
fi
|
||||
printf '%s' "$_in"
|
||||
}
|
||||
|
||||
read_secret() {
|
||||
@@ -213,6 +247,8 @@ read_secret() {
|
||||
awk -v k="$_key" '
|
||||
$0 ~ "^" k "=" {
|
||||
sub("^" k "=", "")
|
||||
sub(/^"/, "")
|
||||
sub(/"$/, "")
|
||||
print
|
||||
exit
|
||||
}
|
||||
|
||||
@@ -35,7 +35,8 @@ echo ($ok ? "mail_ok" : "mail_fail") . " short=" . $short;
|
||||
|
||||
log "$_out to=$TO"
|
||||
if echo "$_out" | grep -q mail_ok; then
|
||||
log "hint: expect s.f0xx.org short link + inline QR via s.f0xx.org/api/v1/qr/…png (encodes …&src=qr)"
|
||||
log "hint: expect s.f0xx.org short link + QR image URL in plain text (encodes …&src=qr)"
|
||||
log "hint: Gmail relay may still async-bounce verify mail — check spam + mailer-daemon"
|
||||
fi
|
||||
case "$_out" in
|
||||
mail_ok) exit 0 ;;
|
||||
|
||||
Reference in New Issue
Block a user