From ab5165fdfa31a683ca22b73b1e2ad06f64e99188 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Tue, 3 Dec 2024 13:06:54 +0100 Subject: [PATCH] Fix imap sensor in case of alternative empty search response (#132081) --- homeassistant/components/imap/coordinator.py | 12 +++++++++++- tests/components/imap/const.py | 2 ++ tests/components/imap/test_init.py | 13 +++++++++++-- 3 files changed, 24 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/imap/coordinator.py b/homeassistant/components/imap/coordinator.py index a9d0fdfbd48..2726b47a679 100644 --- a/homeassistant/components/imap/coordinator.py +++ b/homeassistant/components/imap/coordinator.py @@ -332,7 +332,17 @@ class ImapDataUpdateCoordinator(DataUpdateCoordinator[int | None]): raise UpdateFailed( f"Invalid response for search '{self.config_entry.data[CONF_SEARCH]}': {result} / {lines[0]}" ) - if not (count := len(message_ids := lines[0].split())): + # Check we do have returned items. + # + # In rare cases, when no UID's are returned, + # only the status line is returned, and not an empty line. + # See: https://github.com/home-assistant/core/issues/132042 + # + # Strictly the RfC notes that 0 or more numbers should be returned + # delimited by a space. + # + # See: https://datatracker.ietf.org/doc/html/rfc3501#section-7.2.5 + if len(lines) == 1 or not (count := len(message_ids := lines[0].split())): self._last_message_uid = None return 0 last_message_uid = ( diff --git a/tests/components/imap/const.py b/tests/components/imap/const.py index 037960c9e5d..8f6761bd795 100644 --- a/tests/components/imap/const.py +++ b/tests/components/imap/const.py @@ -141,6 +141,8 @@ TEST_CONTENT_MULTIPART_BASE64_INVALID = ( ) EMPTY_SEARCH_RESPONSE = ("OK", [b"", b"Search completed (0.0001 + 0.000 secs)."]) +EMPTY_SEARCH_RESPONSE_ALT = ("OK", [b"Search completed (0.0001 + 0.000 secs)."]) + BAD_RESPONSE = ("BAD", [b"", b"Unexpected error"]) TEST_SEARCH_RESPONSE = ("OK", [b"1", b"Search completed (0.0001 + 0.000 secs)."]) diff --git a/tests/components/imap/test_init.py b/tests/components/imap/test_init.py index 7bdfc44571a..d4281b9e513 100644 --- a/tests/components/imap/test_init.py +++ b/tests/components/imap/test_init.py @@ -20,6 +20,7 @@ from homeassistant.util.dt import utcnow from .const import ( BAD_RESPONSE, EMPTY_SEARCH_RESPONSE, + EMPTY_SEARCH_RESPONSE_ALT, TEST_BADLY_ENCODED_CONTENT, TEST_FETCH_RESPONSE_BINARY, TEST_FETCH_RESPONSE_HTML, @@ -517,6 +518,11 @@ async def test_fetch_number_of_messages( assert state.state == STATE_UNAVAILABLE +@pytest.mark.parametrize( + "empty_search_reponse", + [EMPTY_SEARCH_RESPONSE, EMPTY_SEARCH_RESPONSE_ALT], + ids=["regular_empty_search_response", "alt_empty_search_response"], +) @pytest.mark.parametrize("imap_search", [TEST_SEARCH_RESPONSE]) @pytest.mark.parametrize( ("imap_fetch", "valid_date"), @@ -525,7 +531,10 @@ async def test_fetch_number_of_messages( ) @pytest.mark.parametrize("imap_has_capability", [True, False], ids=["push", "poll"]) async def test_reset_last_message( - hass: HomeAssistant, mock_imap_protocol: MagicMock, valid_date: bool + hass: HomeAssistant, + mock_imap_protocol: MagicMock, + valid_date: bool, + empty_search_reponse: tuple[str, list[bytes]], ) -> None: """Test receiving a message successfully.""" event = asyncio.Event() # needed for pushed coordinator to make a new loop @@ -580,7 +589,7 @@ async def test_reset_last_message( ) # Simulate an update where no messages are found (needed for pushed coordinator) - mock_imap_protocol.search.return_value = Response(*EMPTY_SEARCH_RESPONSE) + mock_imap_protocol.search.return_value = Response(*empty_search_reponse) # Make sure we have an update async_fire_time_changed(hass, utcnow() + timedelta(seconds=30))