forked from laminas/laminas-mail
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request laminas#140 from glensc/long_header_fields-v2
[2.14]: Fix: Fold long lines during SMTP communication
- Loading branch information
Showing
2 changed files
with
123 additions
and
13 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -8,6 +8,9 @@ | |
|
||
namespace Laminas\Mail\Protocol; | ||
|
||
use Generator; | ||
use Laminas\Mail\Headers; | ||
|
||
/** | ||
* SMTP implementation of Laminas\Mail\Protocol\AbstractProtocol | ||
* | ||
|
@@ -18,6 +21,13 @@ class Smtp extends AbstractProtocol | |
{ | ||
use ProtocolTrait; | ||
|
||
/** | ||
* RFC 5322 section-2.2.3 specifies maximum of 998 bytes per line. | ||
* This may not be exceeded. | ||
* @see https://tools.ietf.org/html/rfc5322#section-2.2.3 | ||
*/ | ||
public const SMTP_LINE_LIMIT = 998; | ||
|
||
/** | ||
* The transport method for the socket | ||
* | ||
|
@@ -170,6 +180,61 @@ public function setUseCompleteQuit($useCompleteQuit) | |
return $this->useCompleteQuit = (bool) $useCompleteQuit; | ||
} | ||
|
||
/** | ||
* Read $data as lines terminated by "\n" | ||
* | ||
* @param string $data | ||
* @param int $chunkSize | ||
* @return Generator|string[] | ||
* @author Elan Ruusamäe <[email protected]> | ||
*/ | ||
private static function chunkedReader(string $data, int $chunkSize = 4096): Generator | ||
{ | ||
if (($fp = fopen("php://temp", "r+")) === false) { | ||
throw new Exception\RuntimeException('cannot fopen'); | ||
} | ||
if (fwrite($fp, $data) === false) { | ||
throw new Exception\RuntimeException('cannot fwrite'); | ||
} | ||
rewind($fp); | ||
|
||
$line = null; | ||
while (($buffer = fgets($fp, $chunkSize)) !== false) { | ||
$line .= $buffer; | ||
|
||
// This is optimization to avoid calling length() in a loop. | ||
// We need to match a condition that is when: | ||
// 1. maximum was read from fgets, which is $chunkSize-1 | ||
// 2. last byte of the buffer is not \n | ||
// | ||
// to access last byte of buffer, we can do | ||
// - $buffer[strlen($buffer)-1] | ||
// and when maximum is read from fgets, then: | ||
// - strlen($buffer) === $chunkSize-1 | ||
// - strlen($buffer)-1 === $chunkSize-2 | ||
// which means this is also true: | ||
// - $buffer[strlen($buffer)-1] === $buffer[$chunkSize-2] | ||
// | ||
// the null coalesce works, as string offset can never be null | ||
$lastByte = $buffer[$chunkSize - 2] ?? null; | ||
|
||
// partial read, continue loop to read again to complete the line | ||
// compare \n first as that's usually false | ||
if ($lastByte !== "\n" && $lastByte !== null) { | ||
continue; | ||
} | ||
|
||
yield $line; | ||
$line = null; | ||
} | ||
|
||
if ($line !== null) { | ||
yield $line; | ||
} | ||
|
||
fclose($fp); | ||
} | ||
|
||
/** | ||
* Whether or not send QUIT command | ||
* | ||
|
@@ -315,25 +380,25 @@ public function data($data) | |
$this->_send('DATA'); | ||
$this->_expect(354, 120); // Timeout set for 2 minutes as per RFC 2821 4.5.3.2 | ||
|
||
if (($fp = fopen("php://temp", "r+")) === false) { | ||
throw new Exception\RuntimeException('cannot fopen'); | ||
} | ||
if (fwrite($fp, $data) === false) { | ||
throw new Exception\RuntimeException('cannot fwrite'); | ||
} | ||
unset($data); | ||
rewind($fp); | ||
|
||
// max line length is 998 char + \r\n = 1000 | ||
while (($line = stream_get_line($fp, 1000, "\n")) !== false) { | ||
$line = rtrim($line, "\r"); | ||
$reader = self::chunkedReader($data); | ||
foreach ($reader as $line) { | ||
$line = rtrim($line, "\r\n"); | ||
if (isset($line[0]) && $line[0] === '.') { | ||
// Escape lines prefixed with a '.' | ||
$line = '.' . $line; | ||
} | ||
|
||
if (strlen($line) > self::SMTP_LINE_LIMIT) { | ||
// Long lines are "folded" by inserting "<CR><LF><SPACE>" | ||
// https://tools.ietf.org/html/rfc5322#section-2.2.3 | ||
// Add "-1" to stay within limits, | ||
// because Headers::FOLDING includes a byte for space character after \r\n | ||
$chunks = chunk_split($line, self::SMTP_LINE_LIMIT - 1, Headers::FOLDING); | ||
$line = substr($chunks, 0, -strlen(Headers::FOLDING)); | ||
} | ||
|
||
$this->_send($line); | ||
} | ||
fclose($fp); | ||
|
||
$this->_send('.'); | ||
$this->_expect(250, 600); // Timeout set for 10 minutes as per RFC 2821 4.5.3.2 | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters