@@ -302,4 +302,111 @@ public function testGetHostnameFallsBackToGethostnameFunction(): void
302302
303303 $ this ->assertSame (gethostname (), $ getHostname ());
304304 }
305+
306+ #[DataProvider('providePrepQuotedPrintableWithLfCrlf ' )]
307+ public function testPrepQuotedPrintableWithLfCrlf (string $ input , string $ expected ): void
308+ {
309+ $ email = new Email ();
310+ $ email ->CRLF = "\n" ;
311+ $ prepQP = self ::getPrivateMethodInvoker ($ email , 'prepQuotedPrintable ' );
312+
313+ $ this ->assertSame ($ expected , $ prepQP ($ input ));
314+ }
315+
316+ /**
317+ * @return iterable<string, array{string, string}>
318+ */
319+ public static function providePrepQuotedPrintableWithLfCrlf (): iterable
320+ {
321+ return [
322+ 'empty string ' => ['' , '' ],
323+ 'safe ascii only ' => ['hello world ' , 'hello world ' ],
324+ 'safe chars only ' => ['abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789(),-./:? ' , 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789(),-./:? ' ],
325+ 'unsafe char encoded ' => ["a \x01b " , 'a=01b ' ],
326+ 'trailing space encoded ' => ["hello \nworld " , "hello=20 \nworld " ],
327+ 'trailing tab encoded ' => ["hello \t\nworld " , "hello=09 \nworld " ],
328+ 'equals sign encoded as =3D ' => ['a=b ' , 'a=3Db ' ],
329+ 'multiple spaces reduced ' => ['a b ' , 'a b ' ],
330+ 'null bytes removed ' => ["a \x00b " , 'ab ' ],
331+ 'unwrap tags removed ' => ['{unwrap}secret{/unwrap} ' , 'secret ' ],
332+ 'single line ' => ['test ' , 'test ' ],
333+ 'two lines ' => ["line1 \nline2 " , "line1 \nline2 " ],
334+ 'three lines trailing empty ' => ["line1 \nline2 \n" , "line1 \nline2 \n" ],
335+ ];
336+ }
337+
338+ public function testPrepQuotedPrintableWithCrlfNative (): void
339+ {
340+ $ email = new Email ();
341+ $ email ->CRLF = "\r\n" ;
342+ $ prepQP = self ::getPrivateMethodInvoker ($ email , 'prepQuotedPrintable ' );
343+
344+ $ result = $ prepQP ('test ' );
345+
346+ $ this ->assertSame (quoted_printable_encode ('test ' ), $ result );
347+ }
348+
349+ public function testPrepQuotedPrintableSoftLineBreak (): void
350+ {
351+ $ email = new Email ();
352+ $ email ->CRLF = "\n" ;
353+ $ prepQP = self ::getPrivateMethodInvoker ($ email , 'prepQuotedPrintable ' );
354+
355+ // 76 'a' chars fit in one line; add 2 more 'b' chars and they soft-wrap
356+ // After reduction: no trailing spaces, just safe chars
357+ $ input = str_repeat ('a ' , 76 ) . 'bb ' ;
358+ $ result = $ prepQP ($ input );
359+
360+ $ this ->assertStringContainsString ("= \n" , $ result , 'Soft line break must be present ' );
361+ $ this ->assertStringNotContainsString ("\r\n" , $ result , 'Custom CRLF must not contain \\r ' );
362+ }
363+
364+ public function testPrepQuotedPrintableSoftBreakAfterEncodedChar (): void
365+ {
366+ $ email = new Email ();
367+ $ email ->CRLF = "\n" ;
368+ $ prepQP = self ::getPrivateMethodInvoker ($ email , 'prepQuotedPrintable ' );
369+
370+ // 74 safe chars + 1 encoded (=3D = 3 bytes) = 77 → must break before encoded
371+ $ input = str_repeat ('a ' , 74 ) . '= ' ;
372+ $ result = $ prepQP ($ input );
373+
374+ $ this ->assertSame (str_repeat ('a ' , 74 ) . "= \n=3D " , $ result );
375+ }
376+
377+ public function testPrepQuotedPrintableHardLineBreakNoInternalSpaceReduction (): void
378+ {
379+ $ email = new Email ();
380+ $ email ->CRLF = "\n" ;
381+ $ prepQP = self ::getPrivateMethodInvoker ($ email , 'prepQuotedPrintable ' );
382+
383+ // Spaces not at end of line must be left as-is
384+ $ this ->assertSame ('a b ' , $ prepQP ('a b ' ));
385+ }
386+
387+ public function testPrepQuotedPrintableMixedContent (): void
388+ {
389+ $ email = new Email ();
390+ $ email ->CRLF = "\n" ;
391+ $ prepQP = self ::getPrivateMethodInvoker ($ email , 'prepQuotedPrintable ' );
392+
393+ $ input = "Hello, World! \nline ends with tab \t\n=special chars: \x01\x02" ;
394+ $ result = $ prepQP ($ input );
395+
396+ $ this ->assertStringContainsString ('Hello, World=21 ' , $ result );
397+ $ this ->assertStringContainsString ('=09 ' , $ result );
398+ $ this ->assertStringContainsString ('=3D ' , $ result );
399+ $ this ->assertStringContainsString ('=01 ' , $ result );
400+ $ this ->assertStringContainsString ('=02 ' , $ result );
401+ }
402+
403+ public function testPrepQuotedPrintableUnwrapRemovesTagsOnly (): void
404+ {
405+ $ email = new Email ();
406+ $ email ->CRLF = "\n" ;
407+ $ prepQP = self ::getPrivateMethodInvoker ($ email , 'prepQuotedPrintable ' );
408+
409+ $ this ->assertSame ('keep =7Bbraces=7D ' , $ prepQP ('keep {braces} ' ));
410+ $ this ->assertSame ('keep (parentheses) ' , $ prepQP ('keep (parentheses) ' ));
411+ }
305412}
0 commit comments