From 828a97d8a22f3a431378d948a3f1eca6679a35ec Mon Sep 17 00:00:00 2001 From: Christoph Wurst <1374172+ChristophWurst@users.noreply.github.com> Date: Tue, 30 Jun 2026 14:59:50 +0200 Subject: [PATCH 1/2] fix(sending): preserve Bcc header in Sent mailbox copy Horde_Mime_Mail::addHeader('Bcc', ...) silos the Bcc value into a separate $_bcc field instead of $_headers, so getRaw() never includes it. The raw message stored in the Sent mailbox therefore had no Bcc header, unlike Evolution, Thunderbird, and Horde IMP. Mirror Horde IMP's approach: build Horde_Mime_Headers directly with Bcc included, strip it from a clone for SMTP delivery (RFC 5321), and pass the full headers to $mimePart->toString() for the Sent mailbox copy. Assisted-by: Claude:claude-sonnet-4-6 Signed-off-by: Christoph Wurst <1374172+ChristophWurst@users.noreply.github.com> --- lib/Service/MailTransmission.php | 62 ++++++++++++++------- tests/Unit/Service/MailTransmissionTest.php | 2 +- 2 files changed, 44 insertions(+), 20 deletions(-) diff --git a/lib/Service/MailTransmission.php b/lib/Service/MailTransmission.php index ad5a4d2a1b..9c07738a4a 100644 --- a/lib/Service/MailTransmission.php +++ b/lib/Service/MailTransmission.php @@ -26,6 +26,7 @@ use Horde_Mime_Headers_Subject; use Horde_Mime_Mail; use Horde_Mime_Mdn; +use Horde_Mime_Part; use Horde_Smtp_Exception; use OCA\Mail\Account; use OCA\Mail\Address; @@ -108,29 +109,46 @@ public function sendMessage(Account $account, LocalMessage $localMessage): void } $transport = $this->smtpClientFactory->create($account); - // build mime body - $headers = $this->buildHeaders( - $from, - $to, - $cc, - $bcc, - $localMessage->getSubject() - ); + // Build full headers for the Sent-folder copy (FCC), including Bcc so the + // sender can see who was blind-copied when reviewing sent mail — the same + // approach used by Horde IMP and other clients (Evolution, Thunderbird). + $fccHeaders = new Horde_Mime_Headers(); + $fccHeaders->addHeaderOb(Horde_Mime_Headers_Date::create()); + $fccHeaders->addHeaderOb(Horde_Mime_Headers_MessageId::create()); + $fccHeaders->addHeader('From', $from->toHorde()); + $fccHeaders->addHeader('To', $to->toHorde()); + if (count($cc) > 0) { + $fccHeaders->addHeader('Cc', $cc->toHorde()); + } + if (count($bcc) > 0) { + $fccHeaders->addHeader('Bcc', $bcc->toHorde()); + } + if ($localMessage->getSubject() !== null) { + $fccHeaders->addHeader('Subject', $localMessage->getSubject()); + } // The table (oc_local_messages) currently only allows for a single reply to message id // but we already set the 'references' header for an email so we could support multiple references // Get the previous message and then concatenate all its "References" message ids with this one if (($inReplyTo = $localMessage->getInReplyToMessageId()) !== null) { - $headers['References'] = $inReplyTo; - $headers['In-Reply-To'] = $inReplyTo; + $fccHeaders->addHeader('References', $inReplyTo); + $fccHeaders->addHeader('In-Reply-To', $inReplyTo); } - if ($localMessage->getRequestMdn()) { - $headers[Horde_Mime_Mdn::MDN_HEADER] = $from->toHorde(); + $fccHeaders->addHeader(Horde_Mime_Mdn::MDN_HEADER, $from->toHorde()); } - $mail = new Horde_Mime_Mail(); - $mail->addHeaders($headers); + // For SMTP delivery: strip Bcc so it never appears in the transmitted + // message (RFC 5321). All three recipient lists are passed as SMTP + // envelope recipients so every addressee still receives the mail. + $sendHeaders = clone $fccHeaders; + $sendHeaders->removeHeader('Bcc'); + + $smtpRecipients = new Horde_Mail_Rfc822_List(); + $smtpRecipients->add($to->toHorde()); + $smtpRecipients->add($cc->toHorde()); + $smtpRecipients->add($bcc->toHorde()); + $smtpRecipients->unique(); $mimeMessage = new MimeMessage( new DataUriParser() @@ -151,12 +169,14 @@ public function sendMessage(Account $account, LocalMessage $localMessage): void return; } - $mail->setBasePart($mimePart); - // Send the message try { - $mail->send($transport, false, false); - $localMessage->setRaw($mail->getRaw(false)); + $mimePart->send($smtpRecipients->writeAddress(), $sendHeaders, $transport); + $localMessage->setRaw($mimePart->toString([ + 'encode' => Horde_Mime_Part::ENCODE_7BIT | Horde_Mime_Part::ENCODE_8BIT | Horde_Mime_Part::ENCODE_BINARY, + 'headers' => $fccHeaders, + 'stream' => false, + ])); $localMessage->setStatus(LocalMessage::STATUS_RAW); } catch (Horde_Mime_Exception $e) { if ($e->getPrevious() instanceof Horde_Smtp_Exception) { @@ -175,7 +195,11 @@ public function sendMessage(Account $account, LocalMessage $localMessage): void } try { - $localMessage->setRaw($mail->getRaw(false)); + $localMessage->setRaw($mimePart->toString([ + 'encode' => Horde_Mime_Part::ENCODE_7BIT | Horde_Mime_Part::ENCODE_8BIT | Horde_Mime_Part::ENCODE_BINARY, + 'headers' => $fccHeaders, + 'stream' => false, + ])); } catch (Throwable) { // Having the raw message is nice for troubleshooting, but should not fail hard. } diff --git a/tests/Unit/Service/MailTransmissionTest.php b/tests/Unit/Service/MailTransmissionTest.php index ad6be5162c..fd9a895904 100644 --- a/tests/Unit/Service/MailTransmissionTest.php +++ b/tests/Unit/Service/MailTransmissionTest.php @@ -470,7 +470,7 @@ public function testSendMessageCc() { $this->assertEquals(LocalMessage::STATUS_RAW, $localMessage->getStatus()); $this->assertStringContainsString('From: Bob ', $localMessage->getRaw()); $this->assertStringContainsString('Cc: Alice ', $localMessage->getRaw()); - $this->assertStringNotContainsString('Bcc:', $localMessage->getRaw()); + $this->assertStringContainsString('Bcc: Jane ', $localMessage->getRaw()); } public function testSendMessageOmitCc() { From cb78cf41e57fc201fcb95fc214b9d2812b55acac Mon Sep 17 00:00:00 2001 From: Christoph Wurst <1374172+ChristophWurst@users.noreply.github.com> Date: Tue, 30 Jun 2026 15:09:23 +0200 Subject: [PATCH 2/2] fixup! fix(sending): preserve Bcc header in Sent mailbox copy Signed-off-by: Christoph Wurst <1374172+ChristophWurst@users.noreply.github.com> --- lib/Service/MailTransmission.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/Service/MailTransmission.php b/lib/Service/MailTransmission.php index 9c07738a4a..e58a36d930 100644 --- a/lib/Service/MailTransmission.php +++ b/lib/Service/MailTransmission.php @@ -116,13 +116,13 @@ public function sendMessage(Account $account, LocalMessage $localMessage): void $fccHeaders = new Horde_Mime_Headers(); $fccHeaders->addHeaderOb(Horde_Mime_Headers_Date::create()); $fccHeaders->addHeaderOb(Horde_Mime_Headers_MessageId::create()); - $fccHeaders->addHeader('From', $from->toHorde()); - $fccHeaders->addHeader('To', $to->toHorde()); + $fccHeaders->addHeaderOb(new Horde_Mime_Headers_Addresses('From', $from->toHorde())); + $fccHeaders->addHeaderOb(new Horde_Mime_Headers_Addresses('To', $to->toHorde())); if (count($cc) > 0) { - $fccHeaders->addHeader('Cc', $cc->toHorde()); + $fccHeaders->addHeaderOb(new Horde_Mime_Headers_Addresses('Cc', $cc->toHorde())); } if (count($bcc) > 0) { - $fccHeaders->addHeader('Bcc', $bcc->toHorde()); + $fccHeaders->addHeaderOb(new Horde_Mime_Headers_Addresses('Bcc', $bcc->toHorde())); } if ($localMessage->getSubject() !== null) { $fccHeaders->addHeader('Subject', $localMessage->getSubject()); @@ -135,7 +135,7 @@ public function sendMessage(Account $account, LocalMessage $localMessage): void $fccHeaders->addHeader('In-Reply-To', $inReplyTo); } if ($localMessage->getRequestMdn()) { - $fccHeaders->addHeader(Horde_Mime_Mdn::MDN_HEADER, $from->toHorde()); + $fccHeaders->addHeaderOb(new Horde_Mime_Headers_Addresses(Horde_Mime_Mdn::MDN_HEADER, $from->toHorde())); } // For SMTP delivery: strip Bcc so it never appears in the transmitted