Skip to content

Bug: _normalise_search_criteria() does not propagate charset in recursive calls #645

Description

@uppersafe

Description

When using IMAPClient.search() with nested search criteria and a non-ASCII charset such as UTF-8, _normalise_search_criteria() does not propagate the charset argument when recursively processing nested lists or tuples.

As a result, nested criteria fall back to the default "us-ascii" charset instead of using the charset provided by the caller.

The relevant code currently looks like this:

elif isinstance(item, (list, tuple)):
    # Process nested criteria list and wrap in parens.
    inner = _normalise_search_criteria(item)

I believe the recursive call should pass the current charset:

elif isinstance(item, (list, tuple)):
    # Process nested criteria list and wrap in parens.
    inner = _normalise_search_criteria(item, charset)

Example

For example:

criteria = [
    "OR",
    ["TEXT", "test"],
    [
        "OR",
        ["TEXT", "signé"],
        ["TEXT", "école"],
    ],
]

client.search(criteria, charset="UTF-8")

The top-level call correctly uses "UTF-8", but when _normalise_search_criteria() processes a nested list, it calls itself without passing charset.

Since the function defaults to "us-ascii" when no charset is provided, nested search criteria are therefore normalized using the wrong charset.

Current behavior

A recursive call such as:

_normalise_search_criteria(item)

causes the nested invocation to fall back to:

charset = "us-ascii"

This can lead to incorrect handling of non-ASCII search terms such as:

signé
école

Resulting in the following exception:

Traceback (most recent call last):
  File "/usr/local/lib/python3.11/threading.py", line 1002, in _bootstrap
    self._bootstrap_inner()
  File "/usr/local/lib/python3.11/threading.py", line 1045, in _bootstrap_inner
    self.run()
  File "/usr/local/lib/python3.11/threading.py", line 982, in run
    self._target(*self._args, **self._kwargs)
  File "/usr/local/lib/python3.11/concurrent/futures/thread.py", line 83, in _worker
    work_item.run()
  File "/usr/local/lib/python3.11/concurrent/futures/thread.py", line 58, in run
    result = self.fn(*self.args, **self.kwargs)
  File "<string>", line 351, in _browse_imap
> File "<string>", line 127, in search
  File "/usr/local/lib/python3.11/site-packages/imapclient/imapclient.py", line 1122, in search
    return self._search(criteria, charset)
  File "/usr/local/lib/python3.11/site-packages/imapclient/imapclient.py", line 1145, in _search
    args.extend(_normalise_search_criteria(criteria, charset))
  File "/usr/local/lib/python3.11/site-packages/imapclient/imapclient.py", line 1847, in _normalise_search_criteria
    inner = _normalise_search_criteria(item)
  File "/usr/local/lib/python3.11/site-packages/imapclient/imapclient.py", line 1847, in _normalise_search_criteria
    inner = _normalise_search_criteria(item)
  File "/usr/local/lib/python3.11/site-packages/imapclient/imapclient.py", line 1852, in _normalise_search_criteria
    out.append(_quoted.maybe(to_bytes(item, charset)))
  File "/usr/local/lib/python3.11/site-packages/imapclient/util.py", line 29, in to_bytes
    return s.encode(charset)
UnicodeEncodeError: 'ascii' codec can't encode character '\xe9' in position 4: ordinal not in range(128)

Also, even by encoding the criteria before passing it to the search function, I encountered another exception:

criteria = [
    "OR",
    ["TEXT", "test"],
    [
        "OR",
        ["TEXT", "signé".encode("utf-8")],
        ["TEXT", "école".encode("utf-8")],
    ],
]

client.search(criteria, charset="UTF-8")

Resulting in an invalid SEARCH / UID SEARCH command being sent to the IMAP server.

Traceback (most recent call last):
  File "/usr/local/lib/python3.11/threading.py", line 1002, in _bootstrap
    self._bootstrap_inner()
  File "/usr/local/lib/python3.11/threading.py", line 1045, in _bootstrap_inner
    self.run()
  File "/usr/local/lib/python3.11/threading.py", line 982, in run
    self._target(*self._args, **self._kwargs)
  File "/usr/local/lib/python3.11/concurrent/futures/thread.py", line 83, in _worker
    work_item.run()
  File "/usr/local/lib/python3.11/concurrent/futures/thread.py", line 58, in run
    result = self.fn(*self.args, **self.kwargs)
  File "<string>", line 352, in _browse_imap
> File "<string>", line 127, in search
  File "/usr/local/lib/python3.11/site-packages/imapclient/imapclient.py", line 1122, in search
    return self._search(criteria, charset)
  File "/usr/local/lib/python3.11/site-packages/imapclient/imapclient.py", line 1153, in _search
    raise exceptions.InvalidCriteriaError(
imapclient.exceptions.InvalidCriteriaError: b"Error in IMAP command UID SEARCH: Missing ')' (0.001 + 0.000 secs)."

This error may have been caused by a syntax error in the criteria: ['OR', ['TEXT', 'test'], ['OR', ['TEXT', b'sign\xc3\xa9'], ['TEXT', b'\xc3\xa9cole']]]
Please refer to the documentation for more information about search criteria syntax..
https://imapclient.readthedocs.io/en/master/#imapclient.IMAPClient.search

Expected behavior

The charset passed to search() should be preserved while recursively normalizing all nested search criteria.

Suggested fix

elif isinstance(item, (list, tuple)):
    # Process nested criteria list and wrap in parens.
-   inner = _normalise_search_criteria(item)
+   inner = _normalise_search_criteria(item, charset)

A regression test using nested TEXT criteria containing UTF-8 characters would probably also be useful.

Environment

  • Python: 3.11

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions