1010
1111# not covering conversation because it's hard to find server which would produce protocol errors
1212
13- def conversation (s , script ):
13+
14+
15+ def recv_until_newline (sock : socket .socket , timeout : float = 5.0 ) -> bytes :
16+ """Read from socket until '\n ' seen or timeout expires."""
17+ sock .setblocking (False )
18+ data = bytearray ()
19+ deadline = time .monotonic () + timeout
20+
21+ while time .monotonic () < deadline :
22+ try :
23+ chunk = sock .recv (4096 )
24+ if not chunk : # connection closed
25+ break
26+ data .extend (chunk )
27+ if b'\n ' in chunk :
28+ break
29+ except BlockingIOError :
30+ time .sleep (0.01 )
31+ continue
32+ return bytes (data )
33+
34+ def recv_smtp (sock : socket .socket , timeout : float = 5.0 ) -> bytes :
35+ """
36+ Read full SMTP reply (single or multi-line) until final line received or timeout.
37+ RFC 5321: lines start with 3 digits + ('-' for continuation or ' ' for end).
38+ """
39+ sock .setblocking (False )
40+ data = bytearray ()
41+ lines = []
42+ deadline = time .monotonic () + timeout
43+ code = None
44+
45+ while time .monotonic () < deadline :
46+ try :
47+ chunk = sock .recv (4096 )
48+ if not chunk : # connection closed
49+ break
50+ data .extend (chunk )
51+ while b'\n ' in data :
52+ line , _ , rest = data .partition (b'\n ' )
53+ data = bytearray (rest )
54+ line = line .rstrip (b'\r ' )
55+ lines .append (line )
56+
57+ # parse reply code
58+ if len (line ) >= 4 and line [:3 ].isdigit ():
59+ code = line [:3 ]
60+ if line [3 :4 ] == b' ' : # final line
61+ return b'\n ' .join (lines ) + b'\n '
62+ except BlockingIOError :
63+ time .sleep (0.01 )
64+ continue
65+
66+ return b'\n ' .join (lines ) + b'\n '
67+
68+
69+ def conversation (s , script , read_fn = None ):
1470 verbose = False
1571 for ph in script :
1672 if ph .say is not None :
1773 if verbose :
1874 print (">" , repr (ph .say )) # pragma: no cover
1975 s .sendall (ph .say .encode ())
20- reply = s .recv (2048 ).decode ('utf8' )
76+ if read_fn :
77+ reply = read_fn (s , timeout = 5 ).decode ('utf8' )
78+ else :
79+ reply = recv_until_newline (s , timeout = 5 ).decode ('utf8' )
80+
2181 if verbose :
2282 print ("<" , repr (reply )) # pragma: no cover
2383 print ("wait:" , repr (ph .wait )) # pragma: no cover
@@ -42,7 +102,7 @@ def starttls_smtp(s):
42102 phrase ('EHLO www-security.com\n ' , '\n ' , 'STARTTLS' ),
43103 phrase ('STARTTLS\n ' ,'\n ' , None )
44104 )
45- conversation (s , script )
105+ conversation (s , script , read_fn = recv_smtp )
46106
47107def starttls_pop3 (s ):
48108 script = (
0 commit comments