@@ -205,6 +205,32 @@ 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 codec at all (not even via the iconv codec) cannot
229+ # encode str criteria; bytes criteria must be used with such
230+ # server-only charsets.
231+ self .assertRaises (LookupError , enc , 'no-such-charset' , ('TEXT' ,))
232+ self .assertEqual (enc ('no-such-charset' , (b'TEXT' ,)), (b'TEXT' ,))
233+
208234 def test_astring_idempotent (self ):
209235 # Quoting an already quoted argument should not change it, so that
210236 # quoting twice gives the same result as quoting once.
@@ -337,7 +363,11 @@ def handle(self):
337363 except StopIteration :
338364 self .continuation = None
339365 continue
340- splitline = splitargs (line .decode ().removesuffix ('\r \n ' ))
366+ self .server .line = line
367+ # surrogateescape so a criterion encoded in a non-UTF-8 charset
368+ # does not crash the handler; tests inspect server.line for bytes.
369+ splitline = splitargs (line .decode ('utf-8' , 'surrogateescape' )
370+ .removesuffix ('\r \n ' ))
341371 tag = splitline [0 ]
342372 cmd = splitline [1 ]
343373 args = splitline [2 :]
@@ -1546,7 +1576,17 @@ def test_search(self):
15461576 self .assertEqual (data , [b'43' ])
15471577 self .assertEqual (server .args , ['CHARSET' , 'UTF-8' , 'TEXT' , 'XXXXXX' ])
15481578
1549- typ , data = client .search ('NF_Z_62-010_(1973)' , 'TEXT' , 'XXXXXX' )
1579+ # A non-ASCII str criterion is encoded to the declared charset (KOI8-U
1580+ # here, which is not UTF-8, so check the encoded bytes on the wire).
1581+ response [:] = ['* SEARCH 43' ]
1582+ typ , data = client .search ('KOI8-U' , 'SUBJECT' , '"Київ"' )
1583+ self .assertEqual (typ , 'OK' )
1584+ self .assertIn (b'CHARSET KOI8-U ' , server .line )
1585+ self .assertIn ('"Київ"' .encode ('koi8-u' ), server .line )
1586+
1587+ # bytes criteria keep this focused on charset-name quoting (the
1588+ # parentheses force the name to be quoted) without criteria encoding.
1589+ typ , data = client .search ('NF_Z_62-010_(1973)' , b'TEXT' , b'XXXXXX' )
15501590 self .assertEqual (typ , 'OK' )
15511591 self .assertEqual (server .args , ['CHARSET' , '"NF_Z_62-010_(1973)"' , 'TEXT' , 'XXXXXX' ])
15521592
@@ -1616,10 +1656,16 @@ def test_sort(self):
16161656 self .assertEqual (data , [br'' ])
16171657 self .assertEqual (server .args , ['(SUBJECT)' , 'US-ASCII' , 'TEXT' , '"not in mailbox"' ])
16181658
1619- typ , data = client .sort ('SUBJECT' , 'NF_Z_62-010_(1973)' , 'TEXT' , '"not in mailbox"' )
1659+ typ , data = client .sort ('SUBJECT' , 'NF_Z_62-010_(1973)' , b 'TEXT' , b '"not in mailbox"' )
16201660 self .assertEqual (typ , 'OK' )
16211661 self .assertEqual (server .args , ['(SUBJECT)' , '"NF_Z_62-010_(1973)"' , 'TEXT' , '"not in mailbox"' ])
16221662
1663+ # A non-ASCII str criterion is encoded to the declared charset.
1664+ response [:] = ['* SORT' ]
1665+ typ , data = client .sort ('(SUBJECT)' , 'KOI8-U' , 'TEXT' , '"Київ"' )
1666+ self .assertEqual (typ , 'OK' )
1667+ self .assertIn ('"Київ"' .encode ('koi8-u' ), server .line )
1668+
16231669 def test_uid_sort (self ):
16241670 response = []
16251671 client , server = self ._setup (make_simple_handler ('UID' , response ,
@@ -1644,10 +1690,16 @@ def test_uid_sort(self):
16441690 self .assertEqual (data , [br'' ])
16451691 self .assertEqual (server .args , ['SORT' , '(SUBJECT)' , 'US-ASCII' , 'TEXT' , '"not in mailbox"' ])
16461692
1647- typ , data = client .uid ('sort' , 'SUBJECT' , 'NF_Z_62-010_(1973)' , 'TEXT' , '"not in mailbox"' )
1693+ typ , data = client .uid ('sort' , 'SUBJECT' , 'NF_Z_62-010_(1973)' , b 'TEXT' , b '"not in mailbox"' )
16481694 self .assertEqual (typ , 'OK' )
16491695 self .assertEqual (server .args , ['SORT' , '(SUBJECT)' , '"NF_Z_62-010_(1973)"' , 'TEXT' , '"not in mailbox"' ])
16501696
1697+ # A non-ASCII str criterion is encoded to the declared charset.
1698+ response [:] = ['* SORT' ]
1699+ typ , data = client .uid ('sort' , '(SUBJECT)' , 'KOI8-U' , 'TEXT' , '"Київ"' )
1700+ self .assertEqual (typ , 'OK' )
1701+ self .assertIn ('"Київ"' .encode ('koi8-u' ), server .line )
1702+
16511703 # The uid=True keyword is a shorthand for uid('SORT', ...).
16521704 response [:] = ['* SORT 2 84 882' ]
16531705 typ , data = client .sort ('(SUBJECT)' , 'UTF-8' , 'SINCE' , '1-Feb-1994' , uid = True )
@@ -1692,10 +1744,16 @@ def test_thread(self):
16921744 b'(199)(200 202)(201)(203)(204)(205 206 207)(208)' ])
16931745 self .assertEqual (server .args , ['ORDEREDSUBJECT' , 'US-ASCII' , 'TEXT' , '"gewp"' ])
16941746
1695- typ , data = client .thread ('ORDEREDSUBJECT' , 'NF_Z_62-010_(1973)' , 'TEXT' , '"gewp"' )
1747+ typ , data = client .thread ('ORDEREDSUBJECT' , 'NF_Z_62-010_(1973)' , b 'TEXT' , b '"gewp"' )
16961748 self .assertEqual (typ , 'OK' )
16971749 self .assertEqual (server .args , ['ORDEREDSUBJECT' , '"NF_Z_62-010_(1973)"' , 'TEXT' , '"gewp"' ])
16981750
1751+ # A non-ASCII str criterion is encoded to the declared charset.
1752+ response [:] = ['* THREAD (1)' ]
1753+ typ , data = client .thread ('ORDEREDSUBJECT' , 'KOI8-U' , 'TEXT' , '"Київ"' )
1754+ self .assertEqual (typ , 'OK' )
1755+ self .assertIn ('"Київ"' .encode ('koi8-u' ), server .line )
1756+
16991757 def test_uid_thread (self ):
17001758 response = []
17011759 client , server = self ._setup (make_simple_handler ('UID' , response ,
@@ -1734,10 +1792,16 @@ def test_uid_thread(self):
17341792 b'(199)(200 202)(201)(203)(204)(205 206 207)(208)' ])
17351793 self .assertEqual (server .args , ['THREAD' , 'ORDEREDSUBJECT' , 'US-ASCII' , 'TEXT' , '"gewp"' ])
17361794
1737- typ , data = client .uid ('THREAD' , 'ORDEREDSUBJECT' , 'NF_Z_62-010_(1973)' , 'TEXT' , '"gewp"' )
1795+ typ , data = client .uid ('THREAD' , 'ORDEREDSUBJECT' , 'NF_Z_62-010_(1973)' , b 'TEXT' , b '"gewp"' )
17381796 self .assertEqual (typ , 'OK' )
17391797 self .assertEqual (server .args , ['THREAD' , 'ORDEREDSUBJECT' , '"NF_Z_62-010_(1973)"' , 'TEXT' , '"gewp"' ])
17401798
1799+ # A non-ASCII str criterion is encoded to the declared charset.
1800+ response [:] = ['* THREAD (1)' ]
1801+ typ , data = client .uid ('THREAD' , 'ORDEREDSUBJECT' , 'KOI8-U' , 'TEXT' , '"Київ"' )
1802+ self .assertEqual (typ , 'OK' )
1803+ self .assertIn ('"Київ"' .encode ('koi8-u' ), server .line )
1804+
17411805 # The uid=True keyword is a shorthand for uid('THREAD', ...).
17421806 response [:] = ['* THREAD (166)(167)(168)' ]
17431807 typ , data = client .thread ('ORDEREDSUBJECT' , 'UTF-8' , 'SINCE' , '5-MAR-2000' ,
0 commit comments