@@ -205,6 +205,31 @@ def test_astring(self):
205205 self .assertEqual (m ._astring ('Entwürfe' ), '"Entwürfe"' .encode ())
206206 self .assertEqual (m ._astring (b'Entw\xc3 \xbc rfe' ), b'"Entw\xc3 \xbc rfe"' )
207207
208+ def test_encode_criteria (self ):
209+ m = imaplib .IMAP4 .__new__ (imaplib .IMAP4 )
210+ enc = m ._encode_criteria
211+ # No charset: criteria are returned unchanged.
212+ self .assertEqual (enc (None , ('TEXT' , 'x' )), ('TEXT' , 'x' ))
213+ # str criteria are encoded to the charset; ASCII is charset-independent.
214+ self .assertEqual (enc ('UTF-8' , ('TEXT' , 'XXXXXX' )), (b'TEXT' , b'XXXXXX' ))
215+ # Non-ASCII text is encoded to the declared charset, including charsets
216+ # other than ASCII, Latin-1 and UTF-8.
217+ self .assertEqual (enc ('UTF-8' , ('"café"' ,)), ('"café"' .encode ('utf-8' ),))
218+ self .assertEqual (enc ('ISO-8859-1' , ('"café"' ,)),
219+ ('"café"' .encode ('latin-1' ),))
220+ self .assertEqual (enc ('KOI8-U' , ('"Київ"' ,)),
221+ ('"Київ"' .encode ('koi8-u' ),))
222+ self .assertEqual (enc ('SHIFT_JIS' , ('"日本"' ,)),
223+ ('"日本"' .encode ('shift_jis' ),))
224+ # bytes criteria are already encoded and pass through unchanged.
225+ self .assertEqual (enc ('SHIFT_JIS' , (b'"already"' ,)), (b'"already"' ,))
226+ # The charset name may itself be bytes.
227+ self .assertEqual (enc (b'UTF-8' , ('"café"' ,)), ('"café"' .encode ('utf-8' ),))
228+ # A charset with no Python codec cannot encode str criteria; bytes
229+ # criteria must be used with such server-only charsets.
230+ self .assertRaises (LookupError , enc , 'NF_Z_62-010_(1973)' , ('TEXT' ,))
231+ self .assertEqual (enc ('NF_Z_62-010_(1973)' , (b'TEXT' ,)), (b'TEXT' ,))
232+
208233 def test_astring_idempotent (self ):
209234 # Quoting an already quoted argument should not change it, so that
210235 # quoting twice gives the same result as quoting once.
@@ -337,7 +362,11 @@ def handle(self):
337362 except StopIteration :
338363 self .continuation = None
339364 continue
340- splitline = splitargs (line .decode ().removesuffix ('\r \n ' ))
365+ self .server .line = line
366+ # surrogateescape so a criterion encoded in a non-UTF-8 charset
367+ # does not crash the handler; tests inspect server.line for bytes.
368+ splitline = splitargs (line .decode ('utf-8' , 'surrogateescape' )
369+ .removesuffix ('\r \n ' ))
341370 tag = splitline [0 ]
342371 cmd = splitline [1 ]
343372 args = splitline [2 :]
@@ -1494,7 +1523,17 @@ def test_search(self):
14941523 self .assertEqual (data , [b'43' ])
14951524 self .assertEqual (server .args , ['CHARSET' , 'UTF-8' , 'TEXT' , 'XXXXXX' ])
14961525
1497- typ , data = client .search ('NF_Z_62-010_(1973)' , 'TEXT' , 'XXXXXX' )
1526+ # A non-ASCII str criterion is encoded to the declared charset (KOI8-U
1527+ # here, which is not UTF-8, so check the encoded bytes on the wire).
1528+ response [:] = ['* SEARCH 43' ]
1529+ typ , data = client .search ('KOI8-U' , 'SUBJECT' , '"Київ"' )
1530+ self .assertEqual (typ , 'OK' )
1531+ self .assertIn (b'CHARSET KOI8-U ' , server .line )
1532+ self .assertIn ('"Київ"' .encode ('koi8-u' ), server .line )
1533+
1534+ # bytes criteria: NF_Z_62-010 has no Python codec, so this exercises
1535+ # charset-name quoting without criteria encoding.
1536+ typ , data = client .search ('NF_Z_62-010_(1973)' , b'TEXT' , b'XXXXXX' )
14981537 self .assertEqual (typ , 'OK' )
14991538 self .assertEqual (server .args , ['CHARSET' , '"NF_Z_62-010_(1973)"' , 'TEXT' , 'XXXXXX' ])
15001539
@@ -1549,10 +1588,16 @@ def test_sort(self):
15491588 self .assertEqual (data , [br'' ])
15501589 self .assertEqual (server .args , ['(SUBJECT)' , 'US-ASCII' , 'TEXT' , '"not in mailbox"' ])
15511590
1552- typ , data = client .sort ('SUBJECT' , 'NF_Z_62-010_(1973)' , 'TEXT' , '"not in mailbox"' )
1591+ typ , data = client .sort ('SUBJECT' , 'NF_Z_62-010_(1973)' , b 'TEXT' , b '"not in mailbox"' )
15531592 self .assertEqual (typ , 'OK' )
15541593 self .assertEqual (server .args , ['(SUBJECT)' , '"NF_Z_62-010_(1973)"' , 'TEXT' , '"not in mailbox"' ])
15551594
1595+ # A non-ASCII str criterion is encoded to the declared charset.
1596+ response [:] = ['* SORT' ]
1597+ typ , data = client .sort ('(SUBJECT)' , 'KOI8-U' , 'TEXT' , '"Київ"' )
1598+ self .assertEqual (typ , 'OK' )
1599+ self .assertIn ('"Київ"' .encode ('koi8-u' ), server .line )
1600+
15561601 def test_uid_sort (self ):
15571602 response = []
15581603 client , server = self ._setup (make_simple_handler ('UID' , response ,
@@ -1577,10 +1622,16 @@ def test_uid_sort(self):
15771622 self .assertEqual (data , [br'' ])
15781623 self .assertEqual (server .args , ['SORT' , '(SUBJECT)' , 'US-ASCII' , 'TEXT' , '"not in mailbox"' ])
15791624
1580- typ , data = client .uid ('sort' , 'SUBJECT' , 'NF_Z_62-010_(1973)' , 'TEXT' , '"not in mailbox"' )
1625+ typ , data = client .uid ('sort' , 'SUBJECT' , 'NF_Z_62-010_(1973)' , b 'TEXT' , b '"not in mailbox"' )
15811626 self .assertEqual (typ , 'OK' )
15821627 self .assertEqual (server .args , ['SORT' , '(SUBJECT)' , '"NF_Z_62-010_(1973)"' , 'TEXT' , '"not in mailbox"' ])
15831628
1629+ # A non-ASCII str criterion is encoded to the declared charset.
1630+ response [:] = ['* SORT' ]
1631+ typ , data = client .uid ('sort' , '(SUBJECT)' , 'KOI8-U' , 'TEXT' , '"Київ"' )
1632+ self .assertEqual (typ , 'OK' )
1633+ self .assertIn ('"Київ"' .encode ('koi8-u' ), server .line )
1634+
15841635 def test_thread (self ):
15851636 response = []
15861637 client , server = self ._setup (make_simple_handler ('THREAD' , response ))
@@ -1618,10 +1669,16 @@ def test_thread(self):
16181669 b'(199)(200 202)(201)(203)(204)(205 206 207)(208)' ])
16191670 self .assertEqual (server .args , ['ORDEREDSUBJECT' , 'US-ASCII' , 'TEXT' , '"gewp"' ])
16201671
1621- typ , data = client .thread ('ORDEREDSUBJECT' , 'NF_Z_62-010_(1973)' , 'TEXT' , '"gewp"' )
1672+ typ , data = client .thread ('ORDEREDSUBJECT' , 'NF_Z_62-010_(1973)' , b 'TEXT' , b '"gewp"' )
16221673 self .assertEqual (typ , 'OK' )
16231674 self .assertEqual (server .args , ['ORDEREDSUBJECT' , '"NF_Z_62-010_(1973)"' , 'TEXT' , '"gewp"' ])
16241675
1676+ # A non-ASCII str criterion is encoded to the declared charset.
1677+ response [:] = ['* THREAD (1)' ]
1678+ typ , data = client .thread ('ORDEREDSUBJECT' , 'KOI8-U' , 'TEXT' , '"Київ"' )
1679+ self .assertEqual (typ , 'OK' )
1680+ self .assertIn ('"Київ"' .encode ('koi8-u' ), server .line )
1681+
16251682 def test_uid_thread (self ):
16261683 response = []
16271684 client , server = self ._setup (make_simple_handler ('UID' , response ,
@@ -1660,10 +1717,16 @@ def test_uid_thread(self):
16601717 b'(199)(200 202)(201)(203)(204)(205 206 207)(208)' ])
16611718 self .assertEqual (server .args , ['THREAD' , 'ORDEREDSUBJECT' , 'US-ASCII' , 'TEXT' , '"gewp"' ])
16621719
1663- typ , data = client .uid ('THREAD' , 'ORDEREDSUBJECT' , 'NF_Z_62-010_(1973)' , 'TEXT' , '"gewp"' )
1720+ typ , data = client .uid ('THREAD' , 'ORDEREDSUBJECT' , 'NF_Z_62-010_(1973)' , b 'TEXT' , b '"gewp"' )
16641721 self .assertEqual (typ , 'OK' )
16651722 self .assertEqual (server .args , ['THREAD' , 'ORDEREDSUBJECT' , '"NF_Z_62-010_(1973)"' , 'TEXT' , '"gewp"' ])
16661723
1724+ # A non-ASCII str criterion is encoded to the declared charset.
1725+ response [:] = ['* THREAD (1)' ]
1726+ typ , data = client .uid ('THREAD' , 'ORDEREDSUBJECT' , 'KOI8-U' , 'TEXT' , '"Київ"' )
1727+ self .assertEqual (typ , 'OK' )
1728+ self .assertIn ('"Київ"' .encode ('koi8-u' ), server .line )
1729+
16671730 def test_delete (self ):
16681731 client , server = self ._setup (make_simple_handler ('DELETE' ))
16691732 client .login ('user' , 'pass' )
0 commit comments