diff --git a/homeassistant/components/imap/__init__.py b/homeassistant/components/imap/__init__.py index 04069d42d7d..3914e0c52c1 100644 --- a/homeassistant/components/imap/__init__.py +++ b/homeassistant/components/imap/__init__.py @@ -14,7 +14,7 @@ from homeassistant.exceptions import ( ConfigEntryNotReady, ) -from .const import DOMAIN +from .const import CONF_ENABLE_PUSH, DOMAIN from .coordinator import ( ImapPollingDataUpdateCoordinator, ImapPushDataUpdateCoordinator, @@ -39,7 +39,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: coordinator_class: type[ ImapPushDataUpdateCoordinator | ImapPollingDataUpdateCoordinator ] - if imap_client.has_capability("IDLE"): + enable_push: bool = entry.data.get(CONF_ENABLE_PUSH, True) + if enable_push and imap_client.has_capability("IDLE"): coordinator_class = ImapPushDataUpdateCoordinator else: coordinator_class = ImapPollingDataUpdateCoordinator diff --git a/homeassistant/components/imap/config_flow.py b/homeassistant/components/imap/config_flow.py index 00be545fb67..4c4a2e2a35c 100644 --- a/homeassistant/components/imap/config_flow.py +++ b/homeassistant/components/imap/config_flow.py @@ -33,6 +33,7 @@ from homeassistant.util.ssl import SSLCipherList from .const import ( CONF_CHARSET, CONF_CUSTOM_EVENT_DATA_TEMPLATE, + CONF_ENABLE_PUSH, CONF_FOLDER, CONF_MAX_MESSAGE_SIZE, CONF_SEARCH, @@ -87,6 +88,7 @@ OPTIONS_SCHEMA_ADVANCED = { cv.positive_int, vol.Range(min=DEFAULT_MAX_MESSAGE_SIZE, max=MAX_MESSAGE_SIZE_LIMIT), ), + vol.Optional(CONF_ENABLE_PUSH, default=True): BOOLEAN_SELECTOR, } diff --git a/homeassistant/components/imap/const.py b/homeassistant/components/imap/const.py index 2e36dd41e16..fd3da28971e 100644 --- a/homeassistant/components/imap/const.py +++ b/homeassistant/components/imap/const.py @@ -11,6 +11,7 @@ CONF_CHARSET: Final = "charset" CONF_MAX_MESSAGE_SIZE = "max_message_size" CONF_CUSTOM_EVENT_DATA_TEMPLATE: Final = "custom_event_data_template" CONF_SSL_CIPHER_LIST: Final = "ssl_cipher_list" +CONF_ENABLE_PUSH: Final = "enable_push" DEFAULT_PORT: Final = 993 diff --git a/homeassistant/components/imap/strings.json b/homeassistant/components/imap/strings.json index 6fad8895931..62579d61f5a 100644 --- a/homeassistant/components/imap/strings.json +++ b/homeassistant/components/imap/strings.json @@ -42,7 +42,8 @@ "folder": "[%key:component::imap::config::step::user::data::folder%]", "search": "[%key:component::imap::config::step::user::data::search%]", "custom_event_data_template": "Template to create custom event data", - "max_message_size": "Max message size (2048 < size < 30000)" + "max_message_size": "Max message size (2048 < size < 30000)", + "enable_push": "Enable Push-IMAP if the server supports it. Turn off if Push-IMAP updates are unreliable" } } }, diff --git a/tests/components/imap/test_config_flow.py b/tests/components/imap/test_config_flow.py index fb4347b08a7..efb505cda77 100644 --- a/tests/components/imap/test_config_flow.py +++ b/tests/components/imap/test_config_flow.py @@ -401,9 +401,9 @@ async def test_key_options_in_options_form(hass: HomeAssistant) -> None: @pytest.mark.parametrize( ("advanced_options", "assert_result"), [ - ({"max_message_size": "8192"}, data_entry_flow.FlowResultType.CREATE_ENTRY), - ({"max_message_size": "1024"}, data_entry_flow.FlowResultType.FORM), - ({"max_message_size": "65536"}, data_entry_flow.FlowResultType.FORM), + ({"max_message_size": 8192}, data_entry_flow.FlowResultType.CREATE_ENTRY), + ({"max_message_size": 1024}, data_entry_flow.FlowResultType.FORM), + ({"max_message_size": 65536}, data_entry_flow.FlowResultType.FORM), ( {"custom_event_data_template": "{{ subject }}"}, data_entry_flow.FlowResultType.CREATE_ENTRY, @@ -412,6 +412,8 @@ async def test_key_options_in_options_form(hass: HomeAssistant) -> None: {"custom_event_data_template": "{{ invalid_syntax"}, data_entry_flow.FlowResultType.FORM, ), + ({"enable_push": True}, data_entry_flow.FlowResultType.CREATE_ENTRY), + ({"enable_push": False}, data_entry_flow.FlowResultType.CREATE_ENTRY), ], ids=[ "valid_message_size", @@ -419,6 +421,8 @@ async def test_key_options_in_options_form(hass: HomeAssistant) -> None: "invalid_message_size_high", "valid_template", "invalid_template", + "enable_push_true", + "enable_push_false", ], ) async def test_advanced_options_form( @@ -459,7 +463,7 @@ async def test_advanced_options_form( else: # Check if entry was updated for key, value in new_config.items(): - assert str(entry.data[key]) == value + assert entry.data[key] == value except vol.MultipleInvalid: # Check if form was expected with these options assert assert_result == data_entry_flow.FlowResultType.FORM diff --git a/tests/components/imap/test_init.py b/tests/components/imap/test_init.py index ff949423614..31b42b50781 100644 --- a/tests/components/imap/test_init.py +++ b/tests/components/imap/test_init.py @@ -2,7 +2,7 @@ import asyncio from datetime import datetime, timedelta, timezone from typing import Any -from unittest.mock import AsyncMock, MagicMock, patch +from unittest.mock import AsyncMock, MagicMock, call, patch from aioimaplib import AUTH, NONAUTH, SELECTED, AioImapException, Response import pytest @@ -36,13 +36,17 @@ from tests.common import MockConfigEntry, async_capture_events, async_fire_time_ @pytest.mark.parametrize( - ("cipher_list", "verify_ssl"), + ("cipher_list", "verify_ssl", "enable_push"), [ - (None, None), - ("python_default", True), - ("python_default", False), - ("modern", True), - ("intermediate", True), + (None, None, None), + ("python_default", True, None), + ("python_default", False, None), + ("modern", True, None), + ("intermediate", True, None), + (None, None, False), + (None, None, True), + ("python_default", True, False), + ("python_default", False, True), ], ) @pytest.mark.parametrize("imap_has_capability", [True, False], ids=["push", "poll"]) @@ -51,6 +55,7 @@ async def test_entry_startup_and_unload( mock_imap_protocol: MagicMock, cipher_list: str | None, verify_ssl: bool | None, + enable_push: bool | None, ) -> None: """Test imap entry startup and unload with push and polling coordinator and alternate ciphers.""" config = MOCK_CONFIG.copy() @@ -58,6 +63,8 @@ async def test_entry_startup_and_unload( config["ssl_cipher_list"] = cipher_list if verify_ssl is not None: config["verify_ssl"] = verify_ssl + if enable_push is not None: + config["enable_push"] = enable_push config_entry = MockConfigEntry(domain=DOMAIN, data=config) config_entry.add_to_hass(hass) @@ -618,3 +625,58 @@ async def test_custom_template( assert data["text"] assert data["custom"] == result assert error in caplog.text if error is not None else True + + +@pytest.mark.parametrize( + ("imap_search", "imap_fetch"), + [(TEST_SEARCH_RESPONSE, TEST_FETCH_RESPONSE_TEXT_PLAIN)], +) +@pytest.mark.parametrize( + ("imap_has_capability", "enable_push", "should_poll"), + [ + (True, False, True), + (False, False, True), + (True, True, False), + (False, True, True), + ], + ids=["enforce_poll", "poll", "auto_push", "auto_poll"], +) +async def test_enforce_polling( + hass: HomeAssistant, + mock_imap_protocol: MagicMock, + enable_push: bool, + should_poll: True, +) -> None: + """Test enforce polling.""" + event_called = async_capture_events(hass, "imap_content") + config = MOCK_CONFIG.copy() + config["enable_push"] = enable_push + + config_entry = MockConfigEntry(domain=DOMAIN, data=config) + config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + # Make sure we have had one update (when polling) + async_fire_time_changed(hass, utcnow() + timedelta(seconds=5)) + await hass.async_block_till_done() + state = hass.states.get("sensor.imap_email_email_com") + # we should have received one message + assert state is not None + assert state.state == "1" + assert state.attributes["state_class"] == SensorStateClass.MEASUREMENT + + # we should have received one event + assert len(event_called) == 1 + data: dict[str, Any] = event_called[0].data + assert data["server"] == "imap.server.com" + assert data["username"] == "email@email.com" + assert data["search"] == "UnSeen UnDeleted" + assert data["folder"] == "INBOX" + assert data["sender"] == "john.doe@example.com" + assert data["subject"] == "Test subject" + assert data["text"] + + if should_poll: + mock_imap_protocol.wait_server_push.assert_not_called() + else: + mock_imap_protocol.assert_has_calls([call.wait_server_push])