Compare commits

...

7 commits

Author SHA1 Message Date
the-djmaze
b5c1be34c2 Don't inflate http response by default 2026-10-23 16:45:02 +02:00
Maarten
0204ca48ee
Merge pull request #2088 from JPRauwolf/patch-1
remove opcache installation from docker build
2026-10-23 15:15:59 +02:00
Maarten
0358fdf971
Merge pull request #2087 from FathiBenNasr/fix/header-unfold-encoded-word
MailSo: keep the folding space before an encoded word that follows a plain word
2026-10-23 15:09:28 +02:00
the-djmaze
a0e093d527 Several security advisories for NPM packages break the JS compiler.
Don't run `npm audit fix`!!!
2026-10-23 12:58:16 +02:00
Jan Philipp Rauwolf
66725386b0
removed opcache installation from docker build
with php 8.5 opcache comes bundled, so the installation fails.
just removing the dependency fixed the build
2026-10-23 00:06:25 +02:00
Fathi Ben Nasr
7c432e392b MailSo: a folded plain-text line no longer leaves two spaces
Unfolding appended a newline before the continuation line, which kept its
own leading whitespace; Header turns the newline into a space, so every
fold became two spaces ("et  checklist"), carried into reply subjects.
The test now compares exactly.
2026-10-22 20:55:38 +00:00
Fathi Ben Nasr
37fcdbe534 MailSo: keep the folding space before an encoded word that follows a plain word
HeaderCollection::Parse() trimmed the folding whitespace in front of every
continuation line starting with an encoded word. RFC 2047 6.2 ignores only
whitespace BETWEEN two adjacent encoded words; after a plain word, RFC 5322
unfolding keeps it. Symfony Mailer folds subjects exactly there, so
"de vos\r\n =?utf-8?Q?=C3=A9quipements?=" was displayed "voséquipements".
The message list (IMAP ENVELOPE) was fine; the opened message was not.

Test: php test/header-unfold.php (fails before the fix).
2026-10-22 20:55:38 +00:00
5 changed files with 2689 additions and 2721 deletions

View file

@ -43,10 +43,11 @@
"dependencies": {
"@rollup/plugin-node-resolve": "^13.1.3",
"@rollup/plugin-replace": "^3.0.1",
"@rollup/plugin-terser": "^1.0.2",
"babel-eslint": "^10.1.0",
"del": "^6.0.0",
"eslint": "^7.32.0",
"gulp": "^5.0.0",
"gulp": "^5.0.1",
"gulp-append-prepend": "^1.0.9",
"gulp-cached": "^1.1.1",
"gulp-clean-css": "^4.3.0",
@ -64,7 +65,6 @@
"gulp-terser": "^2.1.0",
"rollup": "^2.56.3",
"rollup-plugin-external-globals": "^0.6.1",
"rollup-plugin-includepaths": "^0.2.4",
"rollup-plugin-terser": "^7.0.2"
"rollup-plugin-includepaths": "^0.2.4"
}
}

View file

@ -136,14 +136,12 @@ class HeaderCollection extends \MailSo\Base\Collection
$sHeadersValue = \rtrim($sHeadersValue);
}
if ('=?' === \substr(\ltrim($sHeadersValue), 0, 2)) {
$sHeadersValue = \ltrim($sHeadersValue);
}
if ('=?' === \substr($sHeadersValue, 0, 2)) {
$sValue .= $sHeadersValue;
// RFC 5322 unfolding removes the line break and keeps the folding
// whitespace; RFC 2047 6.2 drops it only between two encoded words.
if ('=?' === \substr(\ltrim($sHeadersValue), 0, 2) && '?=' === \substr($sValue, -2)) {
$sValue .= \ltrim($sHeadersValue);
} else {
$sValue .= "\n".$sHeadersValue;
$sValue .= ' '.\ltrim($sHeadersValue);
}
} else {
if (null !== $sName) {

View file

@ -39,16 +39,7 @@ class Response
$this->request_uri = $request_uri;
$this->final_uri = $request_uri;
$this->status = (int) $status;
if (\function_exists('gzinflate') && isset($this->headers['content-encoding'])
&& (false !== \stripos($this->headers['content-encoding'], 'gzip'))) {
$this->body = \gzinflate(\substr($body, 10, -4));
if (false === $this->body) {
$err = \error_get_last() ?: ['message' => 'gzinflate failed'];
throw new \RuntimeException("{$err['message']} for {$request_uri}");
}
} else {
$this->body = $body;
}
$this->body = $body;
}
function __get($k)
@ -56,6 +47,20 @@ class Response
return \property_exists($this, $k) ? $this->$k : null;
}
public function decoded()/*: string|false*/
{
$body = $this->body;
if (\function_exists('gzinflate') && isset($this->headers['content-encoding'])
&& (false !== \stripos($this->headers['content-encoding'], 'gzip'))) {
$body = \gzinflate(\substr($body, 10, -4));
if (false === $body) {
$err = \error_get_last() ?: ['message' => 'gzinflate failed'];
throw new \RuntimeException("{$err['message']} for {$request_uri}");
}
}
return $body;
}
/**
* returns string, array or null
*/

51
test/header-unfold.php Normal file
View file

@ -0,0 +1,51 @@
<?php
// Run with: php test/header-unfold.php
// RFC 5322 unfolding keeps the folding whitespace; RFC 2047 drops only the
// whitespace BETWEEN two encoded words. A plain word followed by a folded
// encoded word lost its space: "de vos\r\n =?utf-8?Q?=C3=A9quipements?="
// was shown as "voséquipements" (Symfony Mailer folds subjects this way).
define('APP_VERSION', 'test');
spl_autoload_register(static function (string $class): void {
if (str_starts_with($class, 'SnappyMail\\')) {
$file = dirname(__DIR__).'/snappymail/v/0.0.0/app/libraries/snappymail/'.strtolower(str_replace('\\', '/', substr($class, 11))).'.php';
} else {
$file = dirname(__DIR__).'/snappymail/v/0.0.0/app/libraries/'.str_replace('\\', '/', $class).'.php';
}
if (is_file($file)) {
require_once $file;
}
});
function check(bool $condition, string $message): void
{
if (!$condition) {
throw new RuntimeException($message);
}
}
function subject(string $raw): string
{
$headers = new \MailSo\Mime\HeaderCollection("Subject: {$raw}\r\nX-Next: 1\r\n");
return (string) $headers->ValueByName('Subject');
}
$cases = [
// plain word, then a folded encoded word: the space stays
"Traiter les =?utf-8?Q?vuln=C3=A9rabilit=C3=A9s?= de vos\r\n =?utf-8?Q?=C3=A9quipements?= : plan de =?utf-8?Q?rem=C3=A9diation?= et\r\n checklist =?utf-8?Q?sign=C3=A9e?="
=> 'Traiter les vulnérabilités de vos équipements : plan de remédiation et checklist signée',
// two encoded words split across lines: no space between them (RFC 2047 §6.2)
"=?utf-8?Q?r=C3=A9pon?=\r\n =?utf-8?Q?se?=" => 'réponse',
"=?utf-8?B?w6l0w6k=?=\r\n\t=?utf-8?B?IGZpbg==?=" => 'été fin',
// encoded word, then a folded plain word: the space stays
"=?utf-8?Q?r=C3=A9union?=\r\n demain" => 'réunion demain',
// no encoding at all
"Hello\r\n world" => 'Hello world',
];
foreach ($cases as $raw => $want) {
// exact: a fold is one space, never two ("et checklist" ended up in replies)
$got = subject($raw);
check($got === $want, "got [{$got}], want [{$want}]");
}
echo "header-unfold: ".count($cases)." checks OK\n";

5316
yarn.lock

File diff suppressed because it is too large Load diff