Skip to content

Commit 262aa33

Browse files
committed
fix: encode localized mail subjects as MIME headers
Signed-off-by: Vitor Mattos <[email protected]>
1 parent dd4460e commit 262aa33

2 files changed

Lines changed: 30 additions & 19 deletions

File tree

lib/Service/MailService.php

Lines changed: 23 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@
2121
class MailService {
2222
/** @var array */
2323
private $files = [];
24-
private const FALLBACK_MAIL_SUBJECT = 'LibreSign: Code to sign file';
2524

2625
public function __construct(
2726
private LoggerInterface $logger,
@@ -43,25 +42,32 @@ private function getFileById(int $fileId): File {
4342
return $this->files[$fileId];
4443
}
4544

46-
private function normalizeMailSubject(string $subject): string {
45+
private function encodeMailSubject(string $subject): string {
4746
if (preg_match('/^[\x20-\x7E]+$/', $subject) === 1) {
4847
return $subject;
4948
}
5049

51-
$normalized = $subject;
52-
if (class_exists(\Normalizer::class)) {
53-
$normalized = \Normalizer::normalize($normalized, \Normalizer::FORM_KD) ?: $normalized;
54-
$normalized = preg_replace('/\p{Mn}+/u', '', $normalized) ?: $normalized;
50+
if (function_exists('mb_encode_mimeheader')) {
51+
$encoded = mb_encode_mimeheader($subject, 'UTF-8', 'B', "\r\n");
52+
if (is_string($encoded) && $encoded !== '') {
53+
return str_replace(["\r", "\n"], '', $encoded);
54+
}
5555
}
5656

57-
$ascii = iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE', $normalized);
58-
if (!is_string($ascii) || trim($ascii) === '') {
59-
return self::FALLBACK_MAIL_SUBJECT;
57+
if (function_exists('iconv_mime_encode')) {
58+
$encoded = iconv_mime_encode('Subject', $subject, [
59+
'scheme' => 'B',
60+
'input-charset' => 'UTF-8',
61+
'output-charset' => 'UTF-8',
62+
'line-length' => 76,
63+
'line-break-chars' => "\r\n",
64+
]);
65+
if (is_string($encoded) && str_starts_with($encoded, 'Subject: ')) {
66+
return str_replace(["\r", "\n"], '', substr($encoded, 9));
67+
}
6068
}
6169

62-
$ascii = preg_replace('/[^\x20-\x7E]/', '', $ascii) ?: '';
63-
$ascii = trim(preg_replace('/\s+/', ' ', $ascii) ?: '');
64-
return $ascii !== '' ? $ascii : self::FALLBACK_MAIL_SUBJECT;
70+
return $subject;
6571
}
6672

6773
/**
@@ -70,7 +76,7 @@ private function normalizeMailSubject(string $subject): string {
7076
public function notifySignDataUpdated(SignRequest $data, string $email, ?string $description = null): void {
7177
$emailTemplate = $this->mailer->createEMailTemplate('settings.TestEmail');
7278
// TRANSLATORS The subject of the email that is sent after changes are made to the signature request that may affect something for the signer who will sign the document. Some possible reasons: URL for signature changed (when the URL expires), the person who requested the signature sent a notification
73-
$emailTemplate->setSubject($this->normalizeMailSubject($this->l10n->t('LibreSign: Changes into a file for you to sign')));
79+
$emailTemplate->setSubject($this->encodeMailSubject($this->l10n->t('LibreSign: Changes into a file for you to sign')));
7480
$emailTemplate->addHeader();
7581
$emailTemplate->addHeading($this->l10n->t('File to sign'), false);
7682

@@ -106,7 +112,7 @@ public function notifySignDataUpdated(SignRequest $data, string $email, ?string
106112
*/
107113
public function notifyUnsignedUser(SignRequest $data, string $email, ?string $description = null): void {
108114
$emailTemplate = $this->mailer->createEMailTemplate('settings.TestEmail');
109-
$emailTemplate->setSubject($this->normalizeMailSubject($this->l10n->t('LibreSign: There is a file for you to sign')));
115+
$emailTemplate->setSubject($this->encodeMailSubject($this->l10n->t('LibreSign: There is a file for you to sign')));
110116
$emailTemplate->addHeader();
111117
$emailTemplate->addHeading($this->l10n->t('File to sign'), false);
112118

@@ -140,7 +146,7 @@ public function notifyUnsignedUser(SignRequest $data, string $email, ?string $de
140146
public function notifySignedUser(SignRequest $signRequest, string $email, File $libreSignFile, string $displayName): void {
141147
$emailTemplate = $this->mailer->createEMailTemplate('settings.TestEmail');
142148
// TRANSLATORS The subject of the email that is sent after a document has been signed by a user. This email is sent to the person who requested the signature.
143-
$emailTemplate->setSubject($this->normalizeMailSubject($this->l10n->t('LibreSign: A file has been signed')));
149+
$emailTemplate->setSubject($this->encodeMailSubject($this->l10n->t('LibreSign: A file has been signed')));
144150
$emailTemplate->addHeader();
145151
$emailTemplate->addHeading($this->l10n->t('File signed'), false);
146152
// TRANSLATORS The text in the email that is sent after a document has been signed by a user. %s will be replaced with the name of the user who signed the document.
@@ -169,7 +175,7 @@ public function notifySignedUser(SignRequest $signRequest, string $email, File $
169175
public function notifyCanceledRequest(SignRequest $signRequest, string $email, File $libreSignFile): void {
170176
$emailTemplate = $this->mailer->createEMailTemplate('settings.TestEmail');
171177
// TRANSLATORS The subject of the email that is sent when a signature request has been canceled.
172-
$emailTemplate->setSubject($this->normalizeMailSubject($this->l10n->t('LibreSign: A signature request has been canceled')));
178+
$emailTemplate->setSubject($this->encodeMailSubject($this->l10n->t('LibreSign: A signature request has been canceled')));
173179
$emailTemplate->addHeader();
174180
$emailTemplate->addHeading($this->l10n->t('Signature request canceled'), false);
175181
// TRANSLATORS The text in the email that is sent when a signature request has been canceled. %s will be replaced with the name of the file.
@@ -191,7 +197,7 @@ public function notifyCanceledRequest(SignRequest $signRequest, string $email, F
191197

192198
public function sendCodeToSign(string $email, string $name, string $code): void {
193199
$emailTemplate = $this->mailer->createEMailTemplate('settings.TestEmail');
194-
$emailTemplate->setSubject($this->normalizeMailSubject($this->l10n->t('LibreSign: Code to sign file')));
200+
$emailTemplate->setSubject($this->encodeMailSubject($this->l10n->t('LibreSign: Code to sign file')));
195201
$emailTemplate->addHeader();
196202
$emailTemplate->addBodyText($this->l10n->t('Use this code to sign the document:'));
197203
$emailTemplate->addBodyText($code);

tests/php/Unit/Service/MailServiceTest.php

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ public function testFailToSendMailToUnsignedUser():void {
116116
$this->assertNull($actual);
117117
}
118118

119-
public function testSendCodeToSignNormalizesAccentedSubjectToAscii(): void {
119+
public function testSendCodeToSignEncodesAccentedSubjectAsMimeHeader(): void {
120120
$l10n = $this->createMock(IL10N::class);
121121
$l10n
122122
->method('t')
@@ -139,7 +139,12 @@ public function testSendCodeToSignNormalizesAccentedSubjectToAscii(): void {
139139
$emailTemplate
140140
->expects($this->once())
141141
->method('setSubject')
142-
->with('LibreSign : Code necessaire a la signature du fichier');
142+
->with($this->callback(static function (string $subject): bool {
143+
if (preg_match('/^[\x20-\x7E]+$/', $subject) !== 1) {
144+
return false;
145+
}
146+
return str_contains($subject, '=?UTF-8?B?') || str_contains($subject, '=?UTF-8?Q?');
147+
}));
143148
$emailTemplate
144149
->expects($this->once())
145150
->method('addHeader');

0 commit comments

Comments
 (0)