From 59be75bdaca25cecbf59f3d2acad81d07ce7bcd5 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Wed, 19 Aug 2026 11:52:59 +0200 Subject: [PATCH] fix(email): prevent Proton SMTP From header folding (#11400) --- app/Notifications/Channels/EmailChannel.php | 3 +- .../Channels/TransactionalEmailChannel.php | 4 +-- .../TransactionalEmails/ResetPassword.php | 1 + bootstrap/helpers/notifications.php | 35 +++++++++++++++---- tests/Unit/MailFromIdentityTest.php | 35 +++++++++++++++++++ 5 files changed, 67 insertions(+), 11 deletions(-) diff --git a/app/Notifications/Channels/EmailChannel.php b/app/Notifications/Channels/EmailChannel.php index 3f40b694b9..fd62a90720 100644 --- a/app/Notifications/Channels/EmailChannel.php +++ b/app/Notifications/Channels/EmailChannel.php @@ -88,8 +88,7 @@ class EmailChannel ); $mailer = new Mailer($transport); - $email = (new Email) - ->from(mail_from_address($settings)) + $email = mail_from_email(new Email, $settings) ->to(...$recipients) ->subject($mailMessage->subject) ->html((string) $mailMessage->render()); diff --git a/app/Notifications/Channels/TransactionalEmailChannel.php b/app/Notifications/Channels/TransactionalEmailChannel.php index 83c61f6d0f..f4e8b294a4 100644 --- a/app/Notifications/Channels/TransactionalEmailChannel.php +++ b/app/Notifications/Channels/TransactionalEmailChannel.php @@ -27,12 +27,10 @@ class TransactionalEmailChannel } $this->bootConfigs(); $mailMessage = $notification->toMail($notifiable); - $from = mail_from_identity($settings); Mail::send( [], [], - fn (Message $message) => $message - ->from($from['address'], $from['name']) + fn (Message $message) => mail_from_message($message, $settings) ->to($email) ->subject($mailMessage->subject) ->html((string) $mailMessage->render()) diff --git a/app/Notifications/TransactionalEmails/ResetPassword.php b/app/Notifications/TransactionalEmails/ResetPassword.php index 0e33bf495f..cb65391068 100644 --- a/app/Notifications/TransactionalEmails/ResetPassword.php +++ b/app/Notifications/TransactionalEmails/ResetPassword.php @@ -57,6 +57,7 @@ class ResetPassword extends Notification $from = mail_from_identity($this->settings); $mail = new MailMessage; $mail->from($from['address'], $from['name']); + $mail->withSymfonyMessage(fn ($message) => prevent_mail_from_header_folding($message, $this->settings)); $mail->subject('Coolify: Reset Password'); $mail->view('emails.reset-password', ['url' => $url, 'count' => config('auth.passwords.'.config('auth.defaults.passwords').'.expire')]); diff --git a/bootstrap/helpers/notifications.php b/bootstrap/helpers/notifications.php index c5b4388807..e76487383d 100644 --- a/bootstrap/helpers/notifications.php +++ b/bootstrap/helpers/notifications.php @@ -6,6 +6,7 @@ use Illuminate\Mail\Message; use Illuminate\Notifications\Messages\MailMessage; use Illuminate\Support\Facades\Mail; use Symfony\Component\Mime\Address; +use Symfony\Component\Mime\Email; function is_transactional_emails_enabled(): bool { @@ -45,6 +46,32 @@ function mail_from_formatted(object $settings): string return mail_from_address($settings)->toString(); } +function mail_from_email(Email $email, object $settings): Email +{ + $email->from(mail_from_address($settings)); + prevent_mail_from_header_folding($email, $settings); + + return $email; +} + +function mail_from_message(Message $message, object $settings): Message +{ + $identity = mail_from_identity($settings); + $message->from($identity['address'], $identity['name']); + prevent_mail_from_header_folding($message->getSymfonyMessage(), $settings); + + return $message; +} + +function prevent_mail_from_header_folding(Email $email, object $settings): void +{ + if (strtolower(trim((string) ($settings->smtp_host ?? ''))) !== 'smtp.protonmail.ch') { + return; + } + + $email->getHeaders()->get('From')?->setMaxLineLength(998); +} + function send_internal_notification(string $message): void { try { @@ -61,14 +88,11 @@ function send_user_an_email(MailMessage $mail, string $email, ?string $cc = null if (blank($type)) { throw new Exception('No email settings found.'); } - $from = mail_from_identity($settings); - if ($cc) { Mail::send( [], [], - fn (Message $message) => $message - ->from($from['address'], $from['name']) + fn (Message $message) => mail_from_message($message, $settings) ->to($email) ->replyTo($email) ->cc($cc) @@ -79,8 +103,7 @@ function send_user_an_email(MailMessage $mail, string $email, ?string $cc = null Mail::send( [], [], - fn (Message $message) => $message - ->from($from['address'], $from['name']) + fn (Message $message) => mail_from_message($message, $settings) ->to($email) ->subject($mail->subject) ->html((string) $mail->render()) diff --git a/tests/Unit/MailFromIdentityTest.php b/tests/Unit/MailFromIdentityTest.php index 4a317c91ca..d9587f9c7f 100644 --- a/tests/Unit/MailFromIdentityTest.php +++ b/tests/Unit/MailFromIdentityTest.php @@ -1,6 +1,8 @@ toBe('"Coolify" '); }); +it('keeps the smtp from name and address on the same header line', function () { + $email = mail_from_email(new Email, (object) [ + 'smtp_host' => 'smtp.protonmail.ch', + 'smtp_from_address' => 'contact@advanceddigitalmarketingltda.com', + 'smtp_from_name' => 'Advanced Digital Marketing LTDA', + ]); + + expect($email->getHeaders()->get('From')?->toString()) + ->toBe('From: Advanced Digital Marketing LTDA '); +}); + +it('keeps the Laravel mail from name and address on the same header line', function () { + $message = mail_from_message(new Message(new Email), (object) [ + 'smtp_host' => 'smtp.protonmail.ch', + 'smtp_from_address' => 'contact@advanceddigitalmarketingltda.com', + 'smtp_from_name' => 'Advanced Digital Marketing LTDA', + ]); + + expect($message->getSymfonyMessage()->getHeaders()->get('From')?->toString()) + ->toBe('From: Advanced Digital Marketing LTDA '); +}); + +it('keeps Symfony header folding for other smtp providers', function () { + $email = mail_from_email(new Email, (object) [ + 'smtp_host' => 'smtp.example.com', + 'smtp_from_address' => 'contact@advanceddigitalmarketingltda.com', + 'smtp_from_name' => 'Advanced Digital Marketing LTDA', + ]); + + expect($email->getHeaders()->get('From')?->toString()) + ->toBe("From: Advanced Digital Marketing LTDA\r\n "); +}); + it('treats a blank from name as missing instead of sending an unnamed address', function () { $identity = mail_from_identity((object) [ 'smtp_from_address' => 'admin@example.com',