However, the *password* argument to the ``LOGIN`` command is always quoted. If
you want to avoid having an argument string quoted (eg: the *flags* argument to
``STORE``) then enclose the string in parentheses (eg: ``r'(\Deleted)'``).
-In general, pass arguments unquoted and let the module quote them as needed.
-An argument that is already enclosed in double quotes is left unchanged,
-so that code which quotes arguments itself keeps working.
+Or you can quote the string yourself;
+an argument that is already enclosed in double quotes is left unchanged.
+In general, however, it is better to pass arguments unquoted
+and let the module quote them as needed.
Mailbox names are encoded as modified UTF-7 (:rfc:`3501`, section 5.1.3),
so a mailbox name containing non-ASCII characters can be passed as an
tuple. If a tuple, then the first part is the header of the response, and the
second part contains the data (ie: 'literal' value).
-The *message_set* options to commands below is a string specifying one or more
-messages to be acted upon. It may be a simple message number (``'1'``), a range
-of message numbers (``'2:4'``), or a group of non-contiguous ranges separated by
-commas (``'1:3,6:9'``). A range can contain an asterisk to indicate an infinite
-upper bound (``'3:*'``).
+The *message_set* options to the commands below can be a string specifying
+one or more messages to be acted upon.
+It may be a simple message number (``'1'``),
+a range of message numbers (``'2:4'``),
+or a group of non-contiguous ranges separated by commas (``'1:3,6:9'``).
+A range can contain an asterisk
+to indicate an infinite upper bound (``'3:*'``).
+
+Alternatively it can be specified using integers and :class:`range` objects.
+It may be a single message number or a sequence.
+The sequence items may be integers, ``(start, stop)`` tuples
+(where ``None`` or ``'*'`` stands for the last message),
+or :class:`range` objects.
+For example, ``[1, (3, 5), 8]`` and ``[range(1, 6), 8]``
+are both equivalent to ``'1,3:5,8'``.
+
+.. versionchanged:: next
+ Added support for the structured *message_set*.
+
+Command arguments that are parenthesized lists of atoms ---
+such as the *flag_list* argument of :meth:`~IMAP4.store` and the *flags*
+argument of :meth:`~IMAP4.append`,
+the *names* argument of :meth:`~IMAP4.status`,
+the *sort_criteria* argument of :meth:`~IMAP4.sort`,
+or the *message_parts* argument of :meth:`~IMAP4.fetch` ---
+can be passed as a sequence of strings instead of a single preformatted string.
+For example, ``[r'\Seen', r'\Answered']``
+is equivalent to ``(\Seen \Answered)``.
+
+.. versionchanged:: next
+ Added support for passing these arguments as a sequence.
+
+.. _imap4-params:
+
+The value-bearing arguments of the search and fetch commands
+can be quoted by hand, but this is error prone.
+Instead, they may contain ``?`` placeholders that are substituted, and quoted
+as required, from a *params* keyword argument,
+in the manner of :mod:`sqlite3` parameter substitution::
+
+ # SEARCH FROM me@example.com SUBJECT "trip report"
+ M.search(None, 'FROM ? SUBJECT ?', params=['me@example.com', 'trip report'])
+
+ # FETCH 1:5 (FLAGS BODY[HEADER.FIELDS (DATE FROM)])
+ M.fetch('1:5', 'FLAGS BODY[HEADER.FIELDS ?]', params=[['DATE', 'FROM']])
+
+The placeholders are:
+
+* ``?`` --- an ``astring``: a string (which will be quoted if necessary),
+ an integer, or a list of integers and/or strings
+ (which will be sent as a parenthesized list);
+* ``?f`` --- a flag or a list of flags, sent verbatim without quoting;
+* ``?s`` --- a *message_set* in the structured form described above.
+
+``??`` stands for a literal ``?``.
+
+Substitution is only performed when *params* is given;
+if no *params* are given, an argument containing a literal ``?`` is unchanged.
+The *params* keyword is accepted by :meth:`~IMAP4.search`,
+:meth:`~IMAP4.fetch`, :meth:`~IMAP4.sort`, :meth:`~IMAP4.thread` and
+:meth:`~IMAP4.uid`.
+
+.. versionadded:: next
+ The *params* keyword argument.
An :class:`IMAP4` instance has the following methods:
Added the *message_set* and *uid* parameters.
-.. method:: IMAP4.fetch(message_set, message_parts, *, uid=False)
+.. method:: IMAP4.fetch(message_set, message_parts, *, uid=False, params=None)
Fetch (parts of) messages. *message_parts* should be a string of message part
names enclosed within parentheses, eg: ``"(UID BODY[TEXT])"``. Returned data
If *uid* is true, *message_set* is a set of UIDs and the message numbers in
the response are UIDs (``UID FETCH``).
+ If *params* is given, ``?`` placeholders in *message_parts* are substituted
+ with the quoted parameters (see :ref:`the placeholders <imap4-params>`).
+
.. versionchanged:: next
- Added the *uid* parameter.
+ Added the *params* and *uid* parameters.
.. method:: IMAP4.getacl(mailbox)
code, instead of the usual type.
-.. method:: IMAP4.search(charset, criterion[, ...], *, uid=False)
+.. method:: IMAP4.search(charset, criterion[, ...], *, uid=False, params=None)
Search mailbox for matching messages. *charset* may be ``None``, in which case
no ``CHARSET`` will be specified in the request to the server. The IMAP
When *charset* is ``None`` (as it must be under ``UTF8=ACCEPT``),
the criterion is sent using the connection's encoding instead.
+ If *params* is given, ``?`` placeholders in the criteria are substituted
+ with the quoted parameters (see :ref:`the placeholders <imap4-params>`).
+
Example::
# M is a connected IMAP4 instance...
- typ, msgnums = M.search(None, 'FROM', '"LDJ"')
+ typ, msgnums = M.search(None, 'FROM', '"John Smith"')
# or:
- typ, msgnums = M.search(None, '(FROM "LDJ")')
+ typ, msgnums = M.search(None, '(FROM "John Smith")')
- .. versionchanged:: next
- Added the *uid* parameter.
+ # or, letting the module quote the value (this is recommended):
+ typ, msgnums = M.search(None, 'FROM ?', params=['John Smith'])
.. versionchanged:: next
+ Added the *params* and *uid* parameters.
``str`` search criteria are encoded to *charset*.
Returns socket instance used to connect to server.
-.. method:: IMAP4.sort(sort_criteria, charset, search_criterion[, ...], *, uid=False)
+.. method:: IMAP4.sort(sort_criteria, charset, search_criterion[, ...], *, uid=False, params=None)
The ``sort`` command is a variant of ``search`` with sorting semantics for the
results. Returned data contains a space separated list of matching message
a *search_criterion* passed as :class:`str` is encoded to *charset*;
pass :class:`bytes` to send one already encoded.
- This is an ``IMAP4rev1`` extension command.
+ If *params* is given, ``?`` placeholders in the search criteria are
+ substituted with the quoted parameters (see :ref:`the placeholders <imap4-params>`).
- .. versionchanged:: next
- Added the *uid* parameter.
+ This is an ``IMAP4rev1`` extension command.
.. versionchanged:: next
+ Added the *params* and *uid* parameters.
``str`` search criteria are encoded to *charset*.
typ, data = M.search(None, 'ALL')
for num in data[0].split():
- M.store(num, '+FLAGS', '\\Deleted')
+ M.store(num, '+FLAGS', r'\Deleted')
M.expunge()
.. note::
Subscribe to new mailbox.
-.. method:: IMAP4.thread(threading_algorithm, charset, search_criterion[, ...], *, uid=False)
+.. method:: IMAP4.thread(threading_algorithm, charset, search_criterion[, ...], *, uid=False, params=None)
The ``thread`` command is a variant of ``search`` with threading semantics for
the results. Returned data contains a space separated list of thread members.
a *search_criterion* passed as :class:`str` is encoded to *charset*;
pass :class:`bytes` to send one already encoded.
- This is an ``IMAP4rev1`` extension command.
+ If *params* is given, ``?`` placeholders in the search criteria are
+ substituted with the quoted parameters (see :ref:`the placeholders <imap4-params>`).
- .. versionchanged:: next
- Added the *uid* parameter.
+ This is an ``IMAP4rev1`` extension command.
.. versionchanged:: next
+ Added the *params* and *uid* parameters.
``str`` search criteria are encoded to *charset*.
-.. method:: IMAP4.uid(command, arg[, ...])
+.. method:: IMAP4.uid(command, arg[, ...], *, params=None)
Execute command args with messages identified by UID, rather than message
number. Returns response appropriate to command. At least one argument must be
supplied; if none are provided, the server will return an error and an exception
will be raised.
+ If *params* is given, ``?`` placeholders in the ``SEARCH``, ``SORT`` and
+ ``THREAD`` criteria or in the ``FETCH`` parts are substituted with the quoted
+ parameters (see :ref:`the placeholders <imap4-params>`).
+
+ .. versionchanged:: next
+ Added the *params* parameter.
+
.. method:: IMAP4.unsubscribe(mailbox)
the criteria are sent using the connection encoding instead.
(Contributed by Serhiy Storchaka in :gh:`153494`.)
+* Command methods now accept structured arguments,
+ so the module takes care of quoting instead of the caller.
+ A *message_set* and lists of flags or other atoms
+ can be passed as sequences instead of preformatted strings,
+ and the :meth:`~imaplib.IMAP4.search`, :meth:`~imaplib.IMAP4.fetch`,
+ :meth:`~imaplib.IMAP4.sort`, :meth:`~imaplib.IMAP4.thread` and
+ :meth:`~imaplib.IMAP4.uid` methods accept a *params* keyword argument
+ that substitutes and quotes ``?`` placeholders,
+ in the manner of :mod:`sqlite3` parameter substitution.
+ (Contributed by Serhiy Storchaka in :gh:`153521`.)
+
ipaddress
---------
# Only NUL, CR and LF are unsafe (they cannot be represented even in
# a quoted string); other control characters are sent quoted.
_control_chars = re.compile(b'[\x00\r\n]')
+_control_chars_str = re.compile('[\x00\r\n]')
_non_astring_char = re.compile(br'[(){ \x00-\x1f\x7f-\xff%*\\"]')
_non_list_char = re.compile(br'[(){ \x00-\x1f\x7f-\xff\\"]')
_quoted = re.compile(br'"(?:[^"\\]|\\.)*+"')
return depth + data.count(b'(') - data.count(b')')
+def _seq_number(n):
+ # A single message number; None or '*' is the last message.
+ if n is None or n == '*':
+ return '*'
+ if not isinstance(n, int):
+ raise TypeError('message number must be an integer, not %s'
+ % type(n).__name__)
+ return str(n)
+
+
+def _seq_range(item):
+ if isinstance(item, range):
+ if item.step == 1 and len(item):
+ item = (item.start, item[-1])
+ else:
+ return ','.join(map(str, item))
+ if isinstance(item, tuple):
+ start, stop = item
+ return '%s:%s' % (_seq_number(start), _seq_number(stop))
+ return _seq_number(item)
+
+
+def _format_sequence_set(arg):
+ if isinstance(arg, (int, str)):
+ return str(arg)
+ # A sequence of message numbers and ranges.
+ return ','.join(map(_seq_range, arg))
+
+
+# Characters that prevent a string from being sent as a bare atom.
+_astring_special = re.compile(r'[(){ %*\\"\x00-\x1f\x7f-\U0010ffff]')
+# A flag: an atom, optionally prefixed by a backslash.
+_flag = re.compile(r'\\?[^(){ %*"\\\]\x00-\x1f\x7f-\U0010ffff]+')
+# A placeholder in a format string: '?', '?f', '?s' or the escape '??'.
+_placeholder = re.compile(r'\?[?fs]?')
+
+
+def _format_astring(value):
+ if isinstance(value, (list, tuple)):
+ return '(' + ' '.join(map(_format_astring, value)) + ')'
+ if not isinstance(value, bool) and isinstance(value, int):
+ return str(value)
+ if isinstance(value, (bytes, bytearray)):
+ value = str(value, 'ascii')
+ elif not isinstance(value, str):
+ raise TypeError('expected a string, an integer or a list, not %s'
+ % type(value).__name__)
+ if value and _astring_special.search(value) is None:
+ return value # an atom, sent unquoted
+ if _control_chars_str.search(value):
+ raise ValueError('NUL, CR and LF cannot be represented inline: %r'
+ % value)
+ return '"' + value.replace('\\', r'\\').replace('"', r'\"') + '"'
+
+
+def _format_flags(value):
+ if isinstance(value, (list, tuple)):
+ # A nested sequence is not part of the API; it produces invalid
+ # syntax that is rejected by the server.
+ return '(' + ' '.join(map(_format_flags, value)) + ')'
+ if isinstance(value, (bytes, bytearray)):
+ value = str(value, 'ascii')
+ elif not isinstance(value, str):
+ raise TypeError('expected a flag string or a list, not %s'
+ % type(value).__name__)
+ if not _flag.fullmatch(value):
+ raise ValueError('invalid flag: %r' % value)
+ return value
+
+
+def _substitute(format, params):
+ params = iter(params)
+ def replace(match):
+ spec = match.group()
+ if spec == '??': # an escaped literal '?'
+ return '?'
+ try:
+ value = next(params)
+ except StopIteration:
+ raise TypeError('not enough parameters for the format string') \
+ from None
+ if spec == '?f': # a flag or a list of flags
+ return _format_flags(value)
+ if spec == '?s': # a message sequence set
+ return _format_sequence_set(value)
+ return _format_astring(value) # an astring or a list of astrings
+ result = _placeholder.sub(replace, format)
+ for value in params:
+ raise TypeError('too many parameters for the format string')
+ return result
+
+
class IMAP4:
r"""IMAP4 client class.
return self._untagged_response(typ, dat, name)
- def fetch(self, message_set, message_parts, *, uid=False):
+ def fetch(self, message_set, message_parts, *, uid=False, params=None):
"""Fetch (parts of) messages.
(typ, [data, ...]) = <instance>.fetch(message_set, message_parts)
'message_parts' should be a string of selected parts
enclosed in parentheses, eg: "(UID BODY[TEXT])".
+ If 'params' is given, '?' placeholders in 'message_parts' are
+ substituted with the quoted parameters.
+
'data' are tuples of message part envelope and data.
If 'uid' is true, 'message_set' is a set of UIDs and the message
numbers in the response are UIDs (UID FETCH).
"""
name = 'FETCH'
+ if params is not None:
+ message_parts = _substitute(message_parts, params)
args = (self._sequence_set(message_set),
self._fetch_parts(message_parts))
if uid:
self._mailbox(newmailbox))
- def search(self, charset, *criteria, uid=False):
+ def search(self, charset, *criteria, uid=False, params=None):
"""Search mailbox for matching messages.
(typ, [data]) = <instance>.search(charset, criterion, ...)
A 'criteria' passed as str is encoded to 'charset'; pass bytes to
send criteria that are already encoded.
+
+ If 'params' is given, '?' placeholders in the criteria are
+ substituted with the quoted parameters.
"""
name = 'SEARCH'
+ if params is not None:
+ criteria = (_substitute(' '.join(criteria), params),)
if charset is not None:
if self.utf8_enabled:
raise IMAP4.error("Non-None charset not valid in UTF8 mode")
return self._untagged_response(typ, dat, 'QUOTA')
- def sort(self, sort_criteria, charset, *search_criteria, uid=False):
+ def sort(self, sort_criteria, charset, *search_criteria, uid=False,
+ params=None):
"""IMAP4rev1 extension SORT command.
(typ, [data]) = <instance>.sort(sort_criteria, charset, search_criteria, ...)
If 'uid' is true, the message numbers in the response are UIDs
(UID SORT).
+
+ If 'params' is given, '?' placeholders in the search criteria are
+ substituted with the quoted parameters.
"""
name = 'SORT'
#if not name in self.capabilities: # Let the server decide!
# raise self.error('unimplemented extension command: %s' % name)
sort_criteria = self._set_quote(sort_criteria)
+ if params is not None:
+ search_criteria = (_substitute(' '.join(search_criteria), params),)
search_criteria = self._encode_criteria(charset, search_criteria)
if charset is not None:
charset = self._astring(charset)
return self._simple_command('SUBSCRIBE', self._mailbox(mailbox))
- def thread(self, threading_algorithm, charset, *search_criteria, uid=False):
+ def thread(self, threading_algorithm, charset, *search_criteria, uid=False,
+ params=None):
"""IMAPrev1 extension THREAD command.
(type, [data]) = <instance>.thread(threading_algorithm, charset, search_criteria, ...)
If 'uid' is true, the message numbers in the response are UIDs
(UID THREAD).
+
+ If 'params' is given, '?' placeholders in the search criteria are
+ substituted with the quoted parameters.
"""
name = 'THREAD'
+ if params is not None:
+ search_criteria = (_substitute(' '.join(search_criteria), params),)
search_criteria = self._encode_criteria(charset, search_criteria)
if charset is not None:
charset = self._astring(charset)
return self._untagged_response(typ, dat, name)
- def uid(self, command, *args):
+ def uid(self, command, *args, params=None):
"""Execute "command arg ..." with messages identified by UID,
rather than message number.
(typ, [data]) = <instance>.uid(command, arg1, arg2, ...)
Returns response appropriate to 'command'.
+
+ If 'params' is given, '?' placeholders in the SEARCH, SORT and
+ THREAD criteria or in the FETCH parts are substituted with the
+ quoted parameters.
"""
command = command.upper()
if not command in Commands:
self._mailbox(new_mailbox))
elif command == 'FETCH':
message_set, message_parts = args
+ if params is not None:
+ message_parts = _substitute(message_parts, params)
args = (self._sequence_set(message_set),
self._fetch_parts(message_parts))
elif command == 'STORE':
self._set_quote(flags))
elif command == 'SORT':
sort_criteria, charset, *search_criteria = args
+ if params is not None:
+ search_criteria = (_substitute(' '.join(search_criteria),
+ params),)
search_criteria = self._encode_criteria(charset, search_criteria)
if charset is not None:
charset = self._astring(charset)
*search_criteria)
elif command == 'THREAD':
threading_algorithm, charset, *search_criteria = args
+ if params is not None:
+ search_criteria = (_substitute(' '.join(search_criteria),
+ params),)
search_criteria = self._encode_criteria(charset, search_criteria)
if charset is not None:
charset = self._astring(charset)
args = (self._atom(threading_algorithm), charset,
*search_criteria)
+ elif command == 'SEARCH' and params is not None:
+ args = (_substitute(' '.join(args), params),)
typ, dat = self._simple_command(name, self._atom(command), *args)
if command in ('SEARCH', 'SORT', 'THREAD'):
name = command
return arg
def _sequence_set(self, arg):
- return arg
+ return _format_sequence_set(arg)
def _set_quote(self, arg):
+ if not isinstance(arg, str):
+ # A sequence of atoms (flags, criteria, item names, etc.);
+ # wrap them in parentheses as a single argument.
+ return '(' + ' '.join(arg) + ')'
if arg and arg[0] == '(' and arg[-1] == ')':
return arg
return '(' + arg + ')'
def _fetch_parts(self, arg):
# "ALL", "FULL" and "FAST" are macros, not data item names;
# they cannot be enclosed in parentheses.
- if arg.upper() in ('ALL', 'FULL', 'FAST'):
+ if isinstance(arg, str) and arg.upper() in ('ALL', 'FULL', 'FAST'):
return arg
return self._set_quote(arg)
self.assertEqual(m._astring(b'INBOX'), b'INBOX')
# Names with protocol-sensitive characters are quoted.
self.assertEqual(m._astring('New folder'), b'"New folder"')
- self.assertEqual(m._astring('a"b'), b'"a\\"b"')
- self.assertEqual(m._astring('a\\b'), b'"a\\\\b"')
+ self.assertEqual(m._astring('a"b'), rb'"a\"b"')
+ self.assertEqual(m._astring(r'a\b'), rb'"a\\b"')
self.assertEqual(m._astring(''), b'""')
self.assertEqual(m._astring('*'), b'"*"')
# A well-formed quoted string is passed through unchanged.
self.assertEqual(m._astring('""'), b'""')
# Including a lenient (non-RFC) backslash escape, which the server
# may accept.
- self.assertEqual(m._astring('"a\\b"'), b'"a\\b"')
+ self.assertEqual(m._astring(r'"a\b"'), rb'"a\b"')
# A string that only looks quoted but is not a single token is
# quoted as data, closing the argument injection vector.
self.assertEqual(m._astring('"a" SELECT evil "'),
- b'"\\"a\\" SELECT evil \\""')
- self.assertEqual(m._astring('"'), b'"\\""')
+ rb'"\"a\" SELECT evil \""')
+ self.assertEqual(m._astring('"'), rb'"\""')
# Non-ASCII names are only allowed in a quoted string or a
# literal, never in an atom (RFC 6855).
m._encoding = 'utf-8'
self.assertEqual(m._mailbox('Entwürfe'), '"Entwürfe"'.encode())
self.assertEqual(m._mailbox('Entw&APw-rfe'), b'Entw&APw-rfe')
+ def test_sequence_set(self):
+ m = imaplib.IMAP4.__new__(imaplib.IMAP4)
+ # A scalar is passed through as a string.
+ self.assertEqual(m._sequence_set(5), '5')
+ self.assertEqual(m._sequence_set('1:3,7'), '1:3,7')
+ # A sequence of numbers and ranges is formatted as a sequence set.
+ self.assertEqual(m._sequence_set([1, 2, 5]), '1,2,5')
+ self.assertEqual(m._sequence_set([1, (3, 5), (8, '*')]), '1,3:5,8:*')
+ self.assertEqual(m._sequence_set([(5, None)]), '5:*')
+ # A range is inclusive; a non-unit step falls back to explicit numbers.
+ self.assertEqual(m._sequence_set([range(1, 4), 7]), '1:3,7')
+ self.assertEqual(m._sequence_set([range(1, 10, 2)]), '1,3,5,7,9')
+ # Message numbers must be integers: a string is not coerced (the
+ # string form is the whole preformatted set), nor is a float.
+ self.assertRaises(TypeError, m._sequence_set, ['7'])
+ self.assertRaises(TypeError, m._sequence_set, [2.9])
+ self.assertRaises(TypeError, m._sequence_set, [(1, 2.9)])
+
+ def test_set_quote(self):
+ m = imaplib.IMAP4.__new__(imaplib.IMAP4)
+ # A string is parenthesized unless it already is.
+ self.assertEqual(m._set_quote(r'\Seen'), r'(\Seen)')
+ self.assertEqual(m._set_quote(r'(\Seen)'), r'(\Seen)')
+ # A sequence of atoms is joined and parenthesized.
+ self.assertEqual(m._set_quote([r'\Seen', r'\Answered']),
+ r'(\Seen \Answered)')
+ self.assertEqual(m._set_quote(['MESSAGES', 'UNSEEN']),
+ '(MESSAGES UNSEEN)')
+
+ def test_substitute(self):
+ sub = imaplib._substitute
+ # '?' quotes an astring; an atom-safe value is left unquoted.
+ self.assertEqual(sub('FROM ?', ['me@host']), 'FROM me@host')
+ self.assertEqual(sub('SUBJECT ?', ['hello world']),
+ 'SUBJECT "hello world"')
+ self.assertEqual(sub('SUBJECT ?', ['a"b']), r'SUBJECT "a\"b"')
+ # An integer becomes a number, a list a parenthesized list.
+ self.assertEqual(sub('LARGER ?', [1000]), 'LARGER 1000')
+ self.assertEqual(sub('HEADER.FIELDS ?', [['DATE', 'FROM']]),
+ 'HEADER.FIELDS (DATE FROM)')
+ # '?f' emits flags verbatim, never quoted.
+ self.assertEqual(sub('?f', [r'\Seen']), r'\Seen')
+ self.assertEqual(sub('?f', [[r'\Seen', r'\Answered']]),
+ r'(\Seen \Answered)')
+ # '?s' formats a message sequence set.
+ self.assertEqual(sub('?s', [[1, (3, 5), (8, '*')]]), '1,3:5,8:*')
+ # '??' is a literal '?'.
+ self.assertEqual(sub('a?? b', []), 'a? b')
+
+ def test_substitute_errors(self):
+ sub = imaplib._substitute
+ self.assertRaises(TypeError, sub, '? ?', ['x']) # too few parameters
+ self.assertRaises(TypeError, sub, '?', ['x', 'y']) # too many parameters
+ self.assertRaises(ValueError, sub, '?f', ['a b']) # not a valid flag
+ self.assertRaises(ValueError, sub, '?', ['a\r\nb']) # CR/LF not inline
+ self.assertRaises(TypeError, sub, '?', [True]) # bool is not a string
+ self.assertRaises(TypeError, sub, '?', [1.5]) # float is not a string
+ self.assertRaises(TypeError, sub, '?s', [['a']]) # not a message number
+ self.assertRaises(TypeError, sub, '?s', [[1.5]]) # not a message number
+ self.assertRaises(TypeError, sub, '?s', [[(1, 'a')]]) # not a message number
+ self.assertRaises(ValueError, sub, '?s', [[(1,)]]) # not a range pair
+ self.assertRaises(ValueError, sub, '?s', [[(1, 2, 3)]]) # not a range pair
+
if ssl:
class SecureTCPServer(socketserver.TCPServer):
self.assertEqual(data, [b'COPY completed'])
self.assertEqual(server.args, ['2:4', '"New folder"'])
+ # A structured message set is formatted into a sequence set.
+ typ, data = client.copy([2, (3, 5)], 'MEETING')
+ self.assertEqual(typ, 'OK')
+ self.assertEqual(server.args, ['2,3:5', 'MEETING'])
+
def test_uid_copy(self):
client, server = self._setup(make_simple_handler('UID',
completed='UID COPY completed'))
self.assertEqual(data, [b'UID COPY completed'])
self.assertEqual(server.args, ['COPY', '4827313:4828442', 'MEETING'])
+ # A structured message set is formatted into a sequence set.
+ typ, data = client.uid('copy', [1, (3, 5)], 'MEETING')
+ self.assertEqual(typ, 'OK')
+ self.assertEqual(server.args, ['COPY', '1,3:5', 'MEETING'])
+
def test_move(self):
client, server = self._setup(make_simple_handler('MOVE'))
client.login('user', 'pass')
self.assertEqual(data, [b'MOVE completed'])
self.assertEqual(server.args, ['2:4', '"New folder"'])
+ # A structured message set is formatted into a sequence set.
+ typ, data = client.move([2, (3, 5)], 'MEETING')
+ self.assertEqual(typ, 'OK')
+ self.assertEqual(server.args, ['2,3:5', 'MEETING'])
+
def test_uid_move(self):
client, server = self._setup(make_simple_handler('UID',
completed='UID MOVE completed'))
self.assertEqual(data, [b'UID MOVE completed'])
self.assertEqual(server.args, ['MOVE', '4827313:4828442', 'MEETING'])
+ # A structured message set is formatted into a sequence set.
+ typ, data = client.uid('move', [1, (3, 5)], 'MEETING')
+ self.assertEqual(typ, 'OK')
+ self.assertEqual(server.args, ['MOVE', '1,3:5', 'MEETING'])
+
def test_store(self):
client, server = self._setup(make_simple_handler('STORE', [
r'* 2 FETCH (FLAGS (\Deleted \Seen))',
self.assertEqual(typ, 'OK')
self.assertEqual(server.args, ['2:4', '+FLAGS', r'(\Deleted)'])
+ # The flags may be a sequence, and the message set may be structured.
+ typ, data = client.store([2, (3, 4)], '+FLAGS',
+ [r'\Deleted', r'\Seen'])
+ self.assertEqual(typ, 'OK')
+ self.assertEqual(server.args, ['2,3:4', '+FLAGS', r'(\Deleted \Seen)'])
+
def test_uid_store(self):
client, server = self._setup(make_simple_handler('UID', [
r'* 23 FETCH (FLAGS (\Deleted \Seen) UID 4827313)',
])
self.assertEqual(server.args, ['STORE', '4827313:4828442', '+FLAGS', r'(\Deleted)'])
+ # The flags may be a sequence.
+ typ, data = client.uid('store', '4827313:4828442', '+FLAGS',
+ [r'\Deleted', r'\Seen'])
+ self.assertEqual(typ, 'OK')
+ self.assertEqual(server.args,
+ ['STORE', '4827313:4828442', '+FLAGS', r'(\Deleted \Seen)'])
+
def test_fetch(self):
# The handler expands the requested sequence set and answers for
# exactly those messages, so the test exercises the round trip of
self.assertEqual(typ, 'OK')
self.assertEqual(server.args, ['2:4', 'fast'])
+ # A structured message set is formatted into a sequence set.
+ typ, data = client.fetch([2, (3, 4)], '(FLAGS)')
+ self.assertEqual(typ, 'OK')
+ self.assertEqual(server.args, ['2,3:4', '(FLAGS)'])
+
+ # message_parts may be a sequence of items.
+ typ, data = client.fetch('1', ['UID', 'FLAGS'])
+ self.assertEqual(typ, 'OK')
+ self.assertEqual(server.args, ['1', '(UID FLAGS)'])
+
+ # 'params' substitutes and quotes '?' placeholders.
+ typ, data = client.fetch('1', 'FLAGS BODY[HEADER.FIELDS ?]',
+ params=[['DATE', 'FROM']])
+ self.assertEqual(typ, 'OK')
+ self.assertEqual(server.args,
+ ['1', '(FLAGS BODY[HEADER.FIELDS (DATE FROM)])'])
+
def test_uid_fetch(self):
client, server = self._setup(make_simple_handler('UID', [
r'* 23 FETCH (FLAGS (\Seen) UID 4827313)',
])
self.assertEqual(server.args, ['FETCH', '4827313:4828442', '(FLAGS)'])
+ # message_parts may be a sequence, and 'params' substitutes '?'.
+ typ, data = client.uid('fetch', '4827313:4828442', ['UID', 'FLAGS'])
+ self.assertEqual(typ, 'OK')
+ self.assertEqual(server.args, ['FETCH', '4827313:4828442', '(UID FLAGS)'])
+
+ typ, data = client.uid('fetch', '4827313:4828442',
+ 'BODY[HEADER.FIELDS ?]', params=[['DATE', 'FROM']])
+ self.assertEqual(typ, 'OK')
+ self.assertEqual(server.args,
+ ['FETCH', '4827313:4828442',
+ '(BODY[HEADER.FIELDS (DATE FROM)])'])
+
def test_partial(self):
client, server = self._setup(make_simple_handler('PARTIAL',
['* 1 FETCH (RFC822.TEXT<0.10> "0123456789")']))
self.assertEqual(typ, 'OK')
self.assertEqual(server.args, ['CHARSET', '"NF_Z_62-010_(1973)"', 'TEXT', 'XXXXXX'])
+ # 'params' substitutes and quotes '?' placeholders.
+ response[:] = ['* SEARCH 1']
+ typ, data = client.search(None, 'FROM ? SUBJECT ?',
+ params=['me@host', 'trip report'])
+ self.assertEqual(typ, 'OK')
+ self.assertEqual(server.args,
+ ['FROM', 'me@host', 'SUBJECT', '"trip report"'])
+
+ # Without 'params', a literal '?' is sent unchanged.
+ typ, data = client.search(None, 'SUBJECT', '"what?"')
+ self.assertEqual(typ, 'OK')
+ self.assertEqual(server.args, ['SUBJECT', '"what?"'])
+
def test_uid_search(self):
response = []
client, server = self._setup(make_simple_handler('UID', response,
self.assertEqual(data, [b'43'])
self.assertEqual(server.args, ['SEARCH', 'CHARSET', 'UTF-8', 'TEXT', 'XXXXXX'])
+ # 'params' substitutes and quotes '?' placeholders.
+ response[:] = ['* SEARCH 1']
+ typ, data = client.uid('SEARCH', 'FROM ? SUBJECT ?',
+ params=['me@host', 'trip report'])
+ self.assertEqual(typ, 'OK')
+ self.assertEqual(server.args,
+ ['SEARCH', 'FROM', 'me@host', 'SUBJECT', '"trip report"'])
+
def test_sort(self):
response = []
client, server = self._setup(make_simple_handler('SORT', response))
self.assertEqual(typ, 'OK')
self.assertIn('"Київ"'.encode('koi8-u'), server.line)
+ # sort_criteria may be a sequence, and 'params' substitutes and
+ # quotes '?' (a value with a space becomes a quoted string).
+ response[:] = ['* SORT 1']
+ typ, data = client.sort(['REVERSE', 'DATE'], 'UTF-8', 'SUBJECT ?',
+ params=['trip report'])
+ self.assertEqual(typ, 'OK')
+ self.assertEqual(server.args,
+ ['(REVERSE DATE)', 'UTF-8', 'SUBJECT', '"trip report"'])
+
def test_uid_sort(self):
response = []
client, server = self._setup(make_simple_handler('UID', response,
self.assertEqual(data, [br'2 84 882'])
self.assertEqual(server.args, ['SORT', '(SUBJECT)', 'UTF-8', 'SINCE', '1-Feb-1994'])
+ # sort_criteria may be a sequence, and 'params' substitutes and
+ # quotes '?' (a value with a space becomes a quoted string).
+ response[:] = ['* SORT 1']
+ typ, data = client.uid('sort', ['REVERSE', 'DATE'], 'UTF-8', 'SUBJECT ?',
+ params=['trip report'])
+ self.assertEqual(typ, 'OK')
+ self.assertEqual(server.args,
+ ['SORT', '(REVERSE DATE)', 'UTF-8', 'SUBJECT', '"trip report"'])
+
def test_thread(self):
response = []
client, server = self._setup(make_simple_handler('THREAD', response))
self.assertEqual(typ, 'OK')
self.assertIn('"Київ"'.encode('koi8-u'), server.line)
+ # 'params' substitutes and quotes '?' (a value with a space becomes
+ # a quoted string).
+ response[:] = ['* THREAD (1)']
+ typ, data = client.thread('REFERENCES', 'UTF-8', 'SUBJECT ?',
+ params=['trip report'])
+ self.assertEqual(typ, 'OK')
+ self.assertEqual(server.args,
+ ['REFERENCES', 'UTF-8', 'SUBJECT', '"trip report"'])
+
def test_uid_thread(self):
response = []
client, server = self._setup(make_simple_handler('UID', response,
self.assertEqual(data, [b'(166)(167)(168)'])
self.assertEqual(server.args, ['THREAD', 'ORDEREDSUBJECT', 'UTF-8', 'SINCE', '5-MAR-2000'])
+ # 'params' substitutes and quotes '?' (a value with a space becomes
+ # a quoted string).
+ response[:] = ['* THREAD (1)']
+ typ, data = client.uid('THREAD', 'REFERENCES', 'UTF-8', 'SUBJECT ?',
+ params=['trip report'])
+ self.assertEqual(typ, 'OK')
+ self.assertEqual(server.args,
+ ['THREAD', 'REFERENCES', 'UTF-8', 'SUBJECT', '"trip report"'])
+
def test_delete(self):
client, server = self._setup(make_simple_handler('DELETE'))
client.login('user', 'pass')
self.assertEqual(typ, 'OK')
self.assertEqual(server.args, ['"New folder"', '(UIDNEXT MESSAGES)'])
+ # The names argument may be a sequence of item names.
+ typ, data = client.status('blurdybloop', ['UIDNEXT', 'MESSAGES'])
+ self.assertEqual(typ, 'OK')
+ self.assertEqual(server.args, ['blurdybloop', '(UIDNEXT MESSAGES)'])
+
def test_getacl(self):
client, server = self._setup(make_simple_handler('GETACL',
['* ACL INBOX Fred rwipslxetad']))
--- /dev/null
+Add support for structured arguments in :mod:`imaplib` command methods. A
+*message_set* and lists of flags or other atoms can now be passed as
+sequences instead of preformatted strings, and the
+:meth:`~imaplib.IMAP4.search`, :meth:`~imaplib.IMAP4.fetch`,
+:meth:`~imaplib.IMAP4.sort`, :meth:`~imaplib.IMAP4.thread` and
+:meth:`~imaplib.IMAP4.uid` methods accept a *params* keyword argument that
+substitutes and quotes ``?`` placeholders.