6666 'CREATE' : ('AUTH' , 'SELECTED' ),
6767 'DELETE' : ('AUTH' , 'SELECTED' ),
6868 'DELETEACL' : ('AUTH' , 'SELECTED' ),
69+ 'ENABLE' : ('AUTH' , ),
6970 'EXAMINE' : ('AUTH' , 'SELECTED' ),
7071 'EXPUNGE' : ('SELECTED' ,),
7172 'FETCH' : ('SELECTED' ,),
107108 br' (?P<hour>[0-9][0-9]):(?P<min>[0-9][0-9]):(?P<sec>[0-9][0-9])'
108109 br' (?P<zonen>[-+])(?P<zoneh>[0-9][0-9])(?P<zonem>[0-9][0-9])'
109110 br'"' )
111+ # Literal is no longer used; kept for backward compatibility.
110112Literal = re .compile (br'.*{(?P<size>\d+)}$' , re .ASCII )
111113MapCRLF = re .compile (br'\r\n|\r|\n' )
112114Response_code = re .compile (br'\[(?P<type>[A-Z-]+)( (?P<data>[^\]]*))?\]' )
113115Untagged_response = re .compile (br'\* (?P<type>[A-Z-]+)( (?P<data>.*))?' )
116+ # Untagged_status is no longer used; kept for backward compatibility
114117Untagged_status = re .compile (
115118 br'\* (?P<data>\d+) (?P<type>[A-Z-]+)( (?P<data2>.*))?' , re .ASCII )
119+ # We compile these in _mode_xxx.
120+ _Literal = br'.*{(?P<size>\d+)}$'
121+ _Untagged_status = br'\* (?P<data>\d+) (?P<type>[A-Z-]+)( (?P<data2>.*))?'
116122
117123
118124
@@ -166,7 +172,7 @@ class error(Exception): pass # Logical errors - debug required
166172 class abort (error ): pass # Service errors - close and retry
167173 class readonly (abort ): pass # Mailbox status changed to READ-ONLY
168174
169- def __init__ (self , host = '' , port = IMAP4_PORT ):
175+ def __init__ (self , host = '' , port = IMAP4_PORT ):
170176 self .debug = Debug
171177 self .state = 'LOGOUT'
172178 self .literal = None # A literal argument to a command
@@ -176,6 +182,7 @@ def __init__(self, host = '', port = IMAP4_PORT):
176182 self .is_readonly = False # READ-ONLY desired state
177183 self .tagnum = 0
178184 self ._tls_established = False
185+ self ._mode_ascii ()
179186
180187 # Open socket to server.
181188
@@ -190,6 +197,19 @@ def __init__(self, host = '', port = IMAP4_PORT):
190197 pass
191198 raise
192199
200+ def _mode_ascii (self ):
201+ self .utf8_enabled = False
202+ self ._encoding = 'ascii'
203+ self .Literal = re .compile (_Literal , re .ASCII )
204+ self .Untagged_status = re .compile (_Untagged_status , re .ASCII )
205+
206+
207+ def _mode_utf8 (self ):
208+ self .utf8_enabled = True
209+ self ._encoding = 'utf-8'
210+ self .Literal = re .compile (_Literal )
211+ self .Untagged_status = re .compile (_Untagged_status )
212+
193213
194214 def _connect (self ):
195215 # Create unique tag for this session,
@@ -360,7 +380,10 @@ def append(self, mailbox, flags, date_time, message):
360380 date_time = Time2Internaldate (date_time )
361381 else :
362382 date_time = None
363- self .literal = MapCRLF .sub (CRLF , message )
383+ literal = MapCRLF .sub (CRLF , message )
384+ if self .utf8_enabled :
385+ literal = b'UTF8 (' + literal + b')'
386+ self .literal = literal
364387 return self ._simple_command (name , mailbox , flags , date_time )
365388
366389
@@ -455,6 +478,18 @@ def deleteacl(self, mailbox, who):
455478 """
456479 return self ._simple_command ('DELETEACL' , mailbox , who )
457480
481+ def enable (self , capability ):
482+ """Send an RFC5161 enable string to the server.
483+
484+ (typ, [data]) = <intance>.enable(capability)
485+ """
486+ if 'ENABLE' not in self .capabilities :
487+ raise IMAP4 .error ("Server does not support ENABLE" )
488+ typ , data = self ._simple_command ('ENABLE' , capability )
489+ if typ == 'OK' and 'UTF8=ACCEPT' in capability .upper ():
490+ self ._mode_utf8 ()
491+ return typ , data
492+
458493 def expunge (self ):
459494 """Permanently remove deleted items from selected mailbox.
460495
@@ -561,7 +596,7 @@ def login_cram_md5(self, user, password):
561596 def _CRAM_MD5_AUTH (self , challenge ):
562597 """ Authobject to use with CRAM-MD5 authentication. """
563598 import hmac
564- pwd = (self .password .encode ('ASCII ' ) if isinstance (self .password , str )
599+ pwd = (self .password .encode ('utf-8 ' ) if isinstance (self .password , str )
565600 else self .password )
566601 return self .user + " " + hmac .HMAC (pwd , challenge , 'md5' ).hexdigest ()
567602
@@ -661,9 +696,12 @@ def search(self, charset, *criteria):
661696 (typ, [data]) = <instance>.search(charset, criterion, ...)
662697
663698 'data' is space separated list of matching message numbers.
699+ If UTF8 is enabled, charset MUST be None.
664700 """
665701 name = 'SEARCH'
666702 if charset :
703+ if self .utf8_enabled :
704+ raise IMAP4 .error ("Non-None charset not valid in UTF8 mode" )
667705 typ , dat = self ._simple_command (name , 'CHARSET' , charset , * criteria )
668706 else :
669707 typ , dat = self ._simple_command (name , * criteria )
@@ -877,7 +915,7 @@ def _append_untagged(self, typ, dat):
877915 def _check_bye (self ):
878916 bye = self .untagged_responses .get ('BYE' )
879917 if bye :
880- raise self .abort (bye [- 1 ].decode ('ascii' , 'replace' ))
918+ raise self .abort (bye [- 1 ].decode (self . _encoding , 'replace' ))
881919
882920
883921 def _command (self , name , * args ):
@@ -898,12 +936,12 @@ def _command(self, name, *args):
898936 raise self .readonly ('mailbox status changed to READ-ONLY' )
899937
900938 tag = self ._new_tag ()
901- name = bytes (name , 'ASCII' )
939+ name = bytes (name , self . _encoding )
902940 data = tag + b' ' + name
903941 for arg in args :
904942 if arg is None : continue
905943 if isinstance (arg , str ):
906- arg = bytes (arg , "ASCII" )
944+ arg = bytes (arg , self . _encoding )
907945 data = data + b' ' + arg
908946
909947 literal = self .literal
@@ -913,7 +951,7 @@ def _command(self, name, *args):
913951 literator = literal
914952 else :
915953 literator = None
916- data = data + bytes (' {%s}' % len (literal ), 'ASCII' )
954+ data = data + bytes (' {%s}' % len (literal ), self . _encoding )
917955
918956 if __debug__ :
919957 if self .debug >= 4 :
@@ -978,7 +1016,7 @@ def _get_capabilities(self):
9781016 typ , dat = self .capability ()
9791017 if dat == [None ]:
9801018 raise self .error ('no CAPABILITY response from server' )
981- dat = str (dat [- 1 ], "ASCII" )
1019+ dat = str (dat [- 1 ], self . _encoding )
9821020 dat = dat .upper ()
9831021 self .capabilities = tuple (dat .split ())
9841022
@@ -997,10 +1035,10 @@ def _get_response(self):
9971035 if self ._match (self .tagre , resp ):
9981036 tag = self .mo .group ('tag' )
9991037 if not tag in self .tagged_commands :
1000- raise self .abort ('unexpected tagged response: %s ' % resp )
1038+ raise self .abort ('unexpected tagged response: %r ' % resp )
10011039
10021040 typ = self .mo .group ('type' )
1003- typ = str (typ , 'ASCII' )
1041+ typ = str (typ , self . _encoding )
10041042 dat = self .mo .group ('data' )
10051043 self .tagged_commands [tag ] = (typ , [dat ])
10061044 else :
@@ -1009,7 +1047,7 @@ def _get_response(self):
10091047 # '*' (untagged) responses?
10101048
10111049 if not self ._match (Untagged_response , resp ):
1012- if self ._match (Untagged_status , resp ):
1050+ if self ._match (self . Untagged_status , resp ):
10131051 dat2 = self .mo .group ('data2' )
10141052
10151053 if self .mo is None :
@@ -1019,17 +1057,17 @@ def _get_response(self):
10191057 self .continuation_response = self .mo .group ('data' )
10201058 return None # NB: indicates continuation
10211059
1022- raise self .abort ("unexpected response: '%s' " % resp )
1060+ raise self .abort ("unexpected response: %r " % resp )
10231061
10241062 typ = self .mo .group ('type' )
1025- typ = str (typ , 'ascii' )
1063+ typ = str (typ , self . _encoding )
10261064 dat = self .mo .group ('data' )
10271065 if dat is None : dat = b'' # Null untagged response
10281066 if dat2 : dat = dat + b' ' + dat2
10291067
10301068 # Is there a literal to come?
10311069
1032- while self ._match (Literal , dat ):
1070+ while self ._match (self . Literal , dat ):
10331071
10341072 # Read literal direct from connection.
10351073
@@ -1053,7 +1091,7 @@ def _get_response(self):
10531091
10541092 if typ in ('OK' , 'NO' , 'BAD' ) and self ._match (Response_code , dat ):
10551093 typ = self .mo .group ('type' )
1056- typ = str (typ , "ASCII" )
1094+ typ = str (typ , self . _encoding )
10571095 self ._append_untagged (typ , self .mo .group ('data' ))
10581096
10591097 if __debug__ :
@@ -1123,7 +1161,7 @@ def _match(self, cre, s):
11231161
11241162 def _new_tag (self ):
11251163
1126- tag = self .tagpre + bytes (str (self .tagnum ), 'ASCII' )
1164+ tag = self .tagpre + bytes (str (self .tagnum ), self . _encoding )
11271165 self .tagnum = self .tagnum + 1
11281166 self .tagged_commands [tag ] = None
11291167 return tag
@@ -1213,7 +1251,8 @@ class IMAP4_SSL(IMAP4):
12131251 """
12141252
12151253
1216- def __init__ (self , host = '' , port = IMAP4_SSL_PORT , keyfile = None , certfile = None , ssl_context = None ):
1254+ def __init__ (self , host = '' , port = IMAP4_SSL_PORT , keyfile = None ,
1255+ certfile = None , ssl_context = None ):
12171256 if ssl_context is not None and keyfile is not None :
12181257 raise ValueError ("ssl_context and keyfile arguments are mutually "
12191258 "exclusive" )
@@ -1251,7 +1290,7 @@ class IMAP4_stream(IMAP4):
12511290
12521291 Instantiate with: IMAP4_stream(command)
12531292
1254- where "command" is a string that can be passed to subprocess.Popen()
1293+ "command" - a string that can be passed to subprocess.Popen()
12551294
12561295 for more documentation see the docstring of the parent class IMAP4.
12571296 """
@@ -1328,7 +1367,7 @@ def encode(self, inp):
13281367 #
13291368 oup = b''
13301369 if isinstance (inp , str ):
1331- inp = inp .encode ('ASCII ' )
1370+ inp = inp .encode ('utf-8 ' )
13321371 while inp :
13331372 if len (inp ) > 48 :
13341373 t = inp [:48 ]
0 commit comments