88
99namespace Laminas \Mail \Protocol ;
1010
11+ use Generator ;
12+ use Laminas \Mail \Headers ;
13+
1114/**
1215 * SMTP implementation of Laminas\Mail\Protocol\AbstractProtocol
1316 *
@@ -18,6 +21,13 @@ class Smtp extends AbstractProtocol
1821{
1922 use ProtocolTrait;
2023
24+ /**
25+ * RFC 5322 section-2.2.3 specifies maximum of 998 bytes per line.
26+ * This may not be exceeded.
27+ * @see https://tools.ietf.org/html/rfc5322#section-2.2.3
28+ */
29+ public const SMTP_LINE_LIMIT = 998 ;
30+
2131 /**
2232 * The transport method for the socket
2333 *
@@ -170,6 +180,61 @@ public function setUseCompleteQuit($useCompleteQuit)
170180 return $ this ->useCompleteQuit = (bool ) $ useCompleteQuit ;
171181 }
172182
183+ /**
184+ * Read $data as lines terminated by "\n"
185+ *
186+ * @param string $data
187+ * @param int $chunkSize
188+ * @return Generator|string[]
189+ * @author Elan Ruusamäe <glen@pld-linux.org>
190+ */
191+ private static function chunkedReader (string $ data , int $ chunkSize = 4096 ): Generator
192+ {
193+ if (($ fp = fopen ("php://temp " , "r+ " )) === false ) {
194+ throw new Exception \RuntimeException ('cannot fopen ' );
195+ }
196+ if (fwrite ($ fp , $ data ) === false ) {
197+ throw new Exception \RuntimeException ('cannot fwrite ' );
198+ }
199+ rewind ($ fp );
200+
201+ $ line = null ;
202+ while (($ buffer = fgets ($ fp , $ chunkSize )) !== false ) {
203+ $ line .= $ buffer ;
204+
205+ // This is optimization to avoid calling length() in a loop.
206+ // We need to match a condition that is when:
207+ // 1. maximum was read from fgets, which is $chunkSize-1
208+ // 2. last byte of the buffer is not \n
209+ //
210+ // to access last byte of buffer, we can do
211+ // - $buffer[strlen($buffer)-1]
212+ // and when maximum is read from fgets, then:
213+ // - strlen($buffer) === $chunkSize-1
214+ // - strlen($buffer)-1 === $chunkSize-2
215+ // which means this is also true:
216+ // - $buffer[strlen($buffer)-1] === $buffer[$chunkSize-2]
217+ //
218+ // the null coalesce works, as string offset can never be null
219+ $ lastByte = $ buffer [$ chunkSize - 2 ] ?? null ;
220+
221+ // partial read, continue loop to read again to complete the line
222+ // compare \n first as that's usually false
223+ if ($ lastByte !== "\n" && $ lastByte !== null ) {
224+ continue ;
225+ }
226+
227+ yield $ line ;
228+ $ line = null ;
229+ }
230+
231+ if ($ line !== null ) {
232+ yield $ line ;
233+ }
234+
235+ fclose ($ fp );
236+ }
237+
173238 /**
174239 * Whether or not send QUIT command
175240 *
@@ -315,25 +380,25 @@ public function data($data)
315380 $ this ->_send ('DATA ' );
316381 $ this ->_expect (354 , 120 ); // Timeout set for 2 minutes as per RFC 2821 4.5.3.2
317382
318- if (($ fp = fopen ("php://temp " , "r+ " )) === false ) {
319- throw new Exception \RuntimeException ('cannot fopen ' );
320- }
321- if (fwrite ($ fp , $ data ) === false ) {
322- throw new Exception \RuntimeException ('cannot fwrite ' );
323- }
324- unset($ data );
325- rewind ($ fp );
326-
327- // max line length is 998 char + \r\n = 1000
328- while (($ line = stream_get_line ($ fp , 1000 , "\n" )) !== false ) {
329- $ line = rtrim ($ line , "\r" );
383+ $ reader = self ::chunkedReader ($ data );
384+ foreach ($ reader as $ line ) {
385+ $ line = rtrim ($ line , "\r\n" );
330386 if (isset ($ line [0 ]) && $ line [0 ] === '. ' ) {
331387 // Escape lines prefixed with a '.'
332388 $ line = '. ' . $ line ;
333389 }
390+
391+ if (strlen ($ line ) > self ::SMTP_LINE_LIMIT ) {
392+ // Long lines are "folded" by inserting "<CR><LF><SPACE>"
393+ // https://tools.ietf.org/html/rfc5322#section-2.2.3
394+ // Add "-1" to stay within limits,
395+ // because Headers::FOLDING includes a byte for space character after \r\n
396+ $ chunks = chunk_split ($ line , self ::SMTP_LINE_LIMIT - 1 , Headers::FOLDING );
397+ $ line = substr ($ chunks , 0 , -strlen (Headers::FOLDING ));
398+ }
399+
334400 $ this ->_send ($ line );
335401 }
336- fclose ($ fp );
337402
338403 $ this ->_send ('. ' );
339404 $ this ->_expect (250 , 600 ); // Timeout set for 10 minutes as per RFC 2821 4.5.3.2
0 commit comments