-
-
Notifications
You must be signed in to change notification settings - Fork 35k
gh-49555: Support international mailbox names in imaplib #153391
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -525,7 +525,7 @@ def append(self, mailbox, flags, date_time, message, *, | |
| if translate_line_endings: | ||
| message = MapCRLF.sub(CRLF, message) | ||
| self.literal = message | ||
| return self._simple_command(name, self._astring(mailbox), flags, date_time) | ||
| return self._simple_command(name, self._mailbox(mailbox), flags, date_time) | ||
|
|
||
|
|
||
| def authenticate(self, mechanism, authobject): | ||
|
|
@@ -596,30 +596,30 @@ def copy(self, message_set, new_mailbox): | |
| (typ, [data]) = <instance>.copy(message_set, new_mailbox) | ||
| """ | ||
| return self._simple_command('COPY', self._sequence_set(message_set), | ||
| self._astring(new_mailbox)) | ||
| self._mailbox(new_mailbox)) | ||
|
|
||
|
|
||
| def create(self, mailbox): | ||
| """Create new mailbox. | ||
|
|
||
| (typ, [data]) = <instance>.create(mailbox) | ||
| """ | ||
| return self._simple_command('CREATE', self._astring(mailbox)) | ||
| return self._simple_command('CREATE', self._mailbox(mailbox)) | ||
|
|
||
|
|
||
| def delete(self, mailbox): | ||
| """Delete old mailbox. | ||
|
|
||
| (typ, [data]) = <instance>.delete(mailbox) | ||
| """ | ||
| return self._simple_command('DELETE', self._astring(mailbox)) | ||
| return self._simple_command('DELETE', self._mailbox(mailbox)) | ||
|
|
||
| def deleteacl(self, mailbox, who): | ||
| """Delete the ACLs (remove any rights) set for who on mailbox. | ||
|
|
||
| (typ, [data]) = <instance>.deleteacl(mailbox, who) | ||
| """ | ||
| return self._simple_command('DELETEACL', self._astring(mailbox), | ||
| return self._simple_command('DELETEACL', self._mailbox(mailbox), | ||
| self._astring(who)) | ||
|
|
||
| def enable(self, capability): | ||
|
|
@@ -669,15 +669,15 @@ def getacl(self, mailbox): | |
|
|
||
| (typ, [data]) = <instance>.getacl(mailbox) | ||
| """ | ||
| typ, dat = self._simple_command('GETACL', self._astring(mailbox)) | ||
| typ, dat = self._simple_command('GETACL', self._mailbox(mailbox)) | ||
| return self._untagged_response(typ, dat, 'ACL') | ||
|
|
||
|
|
||
| def getannotation(self, mailbox, entry, attribute): | ||
| """(typ, [data]) = <instance>.getannotation(mailbox, entry, attribute) | ||
| Retrieve ANNOTATIONs.""" | ||
|
|
||
| typ, dat = self._simple_command('GETANNOTATION', self._astring(mailbox), | ||
| typ, dat = self._simple_command('GETANNOTATION', self._mailbox(mailbox), | ||
| entry, attribute) | ||
| return self._untagged_response(typ, dat, 'ANNOTATION') | ||
|
|
||
|
|
@@ -689,7 +689,7 @@ def getquota(self, root): | |
|
|
||
| (typ, [data]) = <instance>.getquota(root) | ||
| """ | ||
| typ, dat = self._simple_command('GETQUOTA', self._astring(root)) | ||
| typ, dat = self._simple_command('GETQUOTA', self._mailbox(root)) | ||
| return self._untagged_response(typ, dat, 'QUOTA') | ||
|
|
||
|
|
||
|
|
@@ -698,7 +698,7 @@ def getquotaroot(self, mailbox): | |
|
|
||
| (typ, [[QUOTAROOT responses...], [QUOTA responses]]) = <instance>.getquotaroot(mailbox) | ||
| """ | ||
| typ, dat = self._simple_command('GETQUOTAROOT', self._astring(mailbox)) | ||
| typ, dat = self._simple_command('GETQUOTAROOT', self._mailbox(mailbox)) | ||
| typ, quota = self._untagged_response(typ, dat, 'QUOTA') | ||
| typ, quotaroot = self._untagged_response(typ, dat, 'QUOTAROOT') | ||
| return typ, [quotaroot, quota] | ||
|
|
@@ -747,7 +747,7 @@ def list(self, directory='', pattern='*'): | |
| 'data' is list of LIST responses. | ||
| """ | ||
| name = 'LIST' | ||
| typ, dat = self._simple_command(name, self._astring(directory), | ||
| typ, dat = self._simple_command(name, self._mailbox(directory), | ||
| self._list_mailbox(pattern)) | ||
| return self._untagged_response(typ, dat, name) | ||
|
|
||
|
|
@@ -838,7 +838,7 @@ def lsub(self, directory='', pattern='*'): | |
| 'data' are tuples of message part envelope and data. | ||
| """ | ||
| name = 'LSUB' | ||
| typ, dat = self._simple_command(name, self._astring(directory), | ||
| typ, dat = self._simple_command(name, self._mailbox(directory), | ||
| self._list_mailbox(pattern)) | ||
| return self._untagged_response(typ, dat, name) | ||
|
|
||
|
|
@@ -848,14 +848,14 @@ def move(self, message_set, new_mailbox): | |
| (typ, [data]) = <instance>.move(message_set, new_mailbox) | ||
| """ | ||
| return self._simple_command('MOVE', self._sequence_set(message_set), | ||
| self._astring(new_mailbox)) | ||
| self._mailbox(new_mailbox)) | ||
|
|
||
| def myrights(self, mailbox): | ||
| """Show my ACLs for a mailbox (i.e. the rights that I have on mailbox). | ||
|
|
||
| (typ, [data]) = <instance>.myrights(mailbox) | ||
| """ | ||
| typ,dat = self._simple_command('MYRIGHTS', self._astring(mailbox)) | ||
| typ,dat = self._simple_command('MYRIGHTS', self._mailbox(mailbox)) | ||
| return self._untagged_response(typ, dat, 'MYRIGHTS') | ||
|
|
||
| def namespace(self): | ||
|
|
@@ -909,8 +909,8 @@ def rename(self, oldmailbox, newmailbox): | |
|
|
||
| (typ, [data]) = <instance>.rename(oldmailbox, newmailbox) | ||
| """ | ||
| return self._simple_command('RENAME', self._astring(oldmailbox), | ||
| self._astring(newmailbox)) | ||
| return self._simple_command('RENAME', self._mailbox(oldmailbox), | ||
| self._mailbox(newmailbox)) | ||
|
|
||
|
|
||
| def search(self, charset, *criteria): | ||
|
|
@@ -950,7 +950,7 @@ def select(self, mailbox='INBOX', readonly=False): | |
| name = 'EXAMINE' | ||
| else: | ||
| name = 'SELECT' | ||
| typ, dat = self._simple_command(name, self._astring(mailbox)) | ||
| typ, dat = self._simple_command(name, self._mailbox(mailbox)) | ||
| if typ != 'OK': | ||
| self.state = 'AUTH' # Might have been 'SELECTED' | ||
| return typ, dat | ||
|
|
@@ -969,15 +969,15 @@ def setacl(self, mailbox, who, what): | |
|
|
||
| (typ, [data]) = <instance>.setacl(mailbox, who, what) | ||
| """ | ||
| return self._simple_command('SETACL', self._astring(mailbox), | ||
| return self._simple_command('SETACL', self._mailbox(mailbox), | ||
| self._astring(who), self._astring(what)) | ||
|
|
||
|
|
||
| def setannotation(self, mailbox, *args): | ||
| """(typ, [data]) = <instance>.setannotation(mailbox[, entry, attribute]+) | ||
| Set ANNOTATIONs.""" | ||
|
|
||
| typ, dat = self._simple_command('SETANNOTATION', self._astring(mailbox), *args) | ||
| typ, dat = self._simple_command('SETANNOTATION', self._mailbox(mailbox), *args) | ||
| return self._untagged_response(typ, dat, 'ANNOTATION') | ||
|
|
||
|
|
||
|
|
@@ -986,7 +986,7 @@ def setquota(self, root, limits): | |
|
|
||
| (typ, [data]) = <instance>.setquota(root, limits) | ||
| """ | ||
| typ, dat = self._simple_command('SETQUOTA', self._astring(root), | ||
| typ, dat = self._simple_command('SETQUOTA', self._mailbox(root), | ||
| self._set_quote(limits)) | ||
| return self._untagged_response(typ, dat, 'QUOTA') | ||
|
|
||
|
|
@@ -1038,7 +1038,7 @@ def status(self, mailbox, names): | |
| name = 'STATUS' | ||
| #if self.PROTOCOL_VERSION == 'IMAP4': # Let the server decide! | ||
| # raise self.error('%s unimplemented in IMAP4 (obtain IMAP4rev1 server, or re-code)' % name) | ||
| typ, dat = self._simple_command(name, self._astring(mailbox), | ||
| typ, dat = self._simple_command(name, self._mailbox(mailbox), | ||
| self._set_quote(names)) | ||
| return self._untagged_response(typ, dat, name) | ||
|
|
||
|
|
@@ -1059,7 +1059,7 @@ def subscribe(self, mailbox): | |
|
|
||
| (typ, [data]) = <instance>.subscribe(mailbox) | ||
| """ | ||
| return self._simple_command('SUBSCRIBE', self._astring(mailbox)) | ||
| return self._simple_command('SUBSCRIBE', self._mailbox(mailbox)) | ||
|
|
||
|
|
||
| def thread(self, threading_algorithm, charset, *search_criteria): | ||
|
|
@@ -1095,7 +1095,7 @@ def uid(self, command, *args): | |
| if command in ('COPY', 'MOVE'): | ||
| message_set, new_mailbox = args | ||
| args = (self._sequence_set(message_set), | ||
| self._astring(new_mailbox)) | ||
| self._mailbox(new_mailbox)) | ||
| elif command == 'FETCH': | ||
| message_set, message_parts = args | ||
| args = (self._sequence_set(message_set), | ||
|
|
@@ -1129,7 +1129,7 @@ def unsubscribe(self, mailbox): | |
|
|
||
| (typ, [data]) = <instance>.unsubscribe(mailbox) | ||
| """ | ||
| return self._simple_command('UNSUBSCRIBE', self._astring(mailbox)) | ||
| return self._simple_command('UNSUBSCRIBE', self._mailbox(mailbox)) | ||
|
|
||
|
|
||
| def unselect(self): | ||
|
|
@@ -1545,9 +1545,36 @@ def _astring(self, arg): | |
| return arg | ||
| return self._quote(arg) | ||
|
|
||
| def _encode_mailbox(self, arg, safe): | ||
| # Encode a mailbox name (or LIST pattern) for the wire. In the default | ||
| # (ASCII) mode a non-ASCII name uses modified UTF-7 (RFC 3501, 5.1.3). | ||
| # For backward compatibility a str that is already a valid modified | ||
| # UTF-7 token -- one that needs no quoting, or an explicitly quoted | ||
| # string -- is passed through unchanged, so that a name obtained as raw | ||
| # bytes from LIST (and decoded as ASCII) round-trips without being | ||
| # encoded again. Pass bytes to bypass encoding entirely. | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. "Pass bytes to bypass encoding entirely" doesn't seem to apply to this function itself? I think that sentence applies to _mailbox, and more importantly to API functions that expect mailbox names. So that should be clarified somehow if I'm correct (maybe just moving the slightly modified comment to _mailbox). If a later programmer tried to pass bytes to _encode_mailbox it wouldn't do what that sentence of the comment says, unless I'm misunderstanding. It also might be good to mention that _encoding can/should only ever be either ascii or utf-8, but that's optional ;) |
||
| if self._encoding != 'ascii': | ||
| return bytes(arg, self._encoding) | ||
| try: | ||
| raw = arg.encode('ascii') | ||
| except UnicodeEncodeError: | ||
| return arg.encode('utf-7-imap') | ||
| if _quoted.fullmatch(raw) or (raw and safe.search(raw) is None): | ||
| try: | ||
| raw.decode('utf-7-imap') | ||
| return raw | ||
| except UnicodeDecodeError: | ||
| pass | ||
| return arg.encode('utf-7-imap') | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this a slight change in behavior? If I understand what this is doing correctly, if the string can be encoded to ascii it gets encoded to utf-7-imap in order to encode any |
||
|
|
||
| def _mailbox(self, arg): | ||
| if isinstance(arg, str): | ||
| arg = self._encode_mailbox(arg, _non_astring_char) | ||
| return self._astring(arg) | ||
|
|
||
| def _list_mailbox(self, arg): | ||
| if isinstance(arg, str): | ||
| arg = bytes(arg, self._encoding) | ||
| arg = self._encode_mailbox(arg, _non_list_char) | ||
| if _quoted.fullmatch(arg): | ||
| return arg | ||
| if arg and _non_list_char.search(arg) is None: | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| :mod:`imaplib` now encodes non-ASCII mailbox names as modified UTF-7 | ||
| (:rfc:`3501`, section 5.1.3), so international mailbox names can be passed as | ||
| ordinary :class:`str`. A ``str`` that is already valid modified UTF-7, or a | ||
| :class:`bytes` object, is sent unchanged. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe add "even if UTF8=ACCEPT" has not been enabled? Or, if passing mailbox names as strings wasn't previously supported then this should be rephrased with that as the primary point and when utf-7 vs utf-8 is used explained after.