diff --git a/homeassistant/components/imap/config_flow.py b/homeassistant/components/imap/config_flow.py index 414d5830bae..62ed4d42a07 100644 --- a/homeassistant/components/imap/config_flow.py +++ b/homeassistant/components/imap/config_flow.py @@ -33,6 +33,7 @@ from .const import ( CONF_CHARSET, CONF_CUSTOM_EVENT_DATA_TEMPLATE, CONF_ENABLE_PUSH, + CONF_EVENT_MESSAGE_DATA, CONF_FOLDER, CONF_MAX_MESSAGE_SIZE, CONF_SEARCH, @@ -42,6 +43,7 @@ from .const import ( DEFAULT_PORT, DOMAIN, MAX_MESSAGE_SIZE_LIMIT, + MESSAGE_DATA_OPTIONS, ) from .coordinator import connect_to_server from .errors import InvalidAuth, InvalidFolder @@ -55,6 +57,13 @@ CIPHER_SELECTOR = SelectSelector( ) ) TEMPLATE_SELECTOR = TemplateSelector(TemplateSelectorConfig()) +EVENT_MESSAGE_DATA_SELECTOR = SelectSelector( + SelectSelectorConfig( + options=MESSAGE_DATA_OPTIONS, + translation_key=CONF_EVENT_MESSAGE_DATA, + multiple=True, + ) +) CONFIG_SCHEMA = vol.Schema( { @@ -65,6 +74,8 @@ CONFIG_SCHEMA = vol.Schema( vol.Optional(CONF_CHARSET, default="utf-8"): str, vol.Optional(CONF_FOLDER, default="INBOX"): str, vol.Optional(CONF_SEARCH, default="UnSeen UnDeleted"): str, + # The default for new entries is to not include text and headers + vol.Optional(CONF_EVENT_MESSAGE_DATA, default=[]): cv.ensure_list, } ) CONFIG_SCHEMA_ADVANCED = { @@ -78,6 +89,10 @@ OPTIONS_SCHEMA = vol.Schema( { vol.Optional(CONF_FOLDER, default="INBOX"): str, vol.Optional(CONF_SEARCH, default="UnSeen UnDeleted"): str, + # The default for older entries is to include text and headers + vol.Optional( + CONF_EVENT_MESSAGE_DATA, default=MESSAGE_DATA_OPTIONS + ): EVENT_MESSAGE_DATA_SELECTOR, } ) diff --git a/homeassistant/components/imap/const.py b/homeassistant/components/imap/const.py index fd3da28971e..a341a2a55e7 100644 --- a/homeassistant/components/imap/const.py +++ b/homeassistant/components/imap/const.py @@ -8,7 +8,8 @@ CONF_SERVER: Final = "server" CONF_FOLDER: Final = "folder" CONF_SEARCH: Final = "search" CONF_CHARSET: Final = "charset" -CONF_MAX_MESSAGE_SIZE = "max_message_size" +CONF_EVENT_MESSAGE_DATA: Final = "event_message_data" +CONF_MAX_MESSAGE_SIZE: Final = "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" @@ -17,4 +18,6 @@ DEFAULT_PORT: Final = 993 DEFAULT_MAX_MESSAGE_SIZE = 2048 -MAX_MESSAGE_SIZE_LIMIT = 30000 +MESSAGE_DATA_OPTIONS: Final = ["text", "headers"] + +MAX_MESSAGE_SIZE_LIMIT: Final = 30000 diff --git a/homeassistant/components/imap/coordinator.py b/homeassistant/components/imap/coordinator.py index 59ceb2b3b3d..94699ae5dd4 100644 --- a/homeassistant/components/imap/coordinator.py +++ b/homeassistant/components/imap/coordinator.py @@ -41,6 +41,7 @@ from homeassistant.util.ssl import ( from .const import ( CONF_CHARSET, CONF_CUSTOM_EVENT_DATA_TEMPLATE, + CONF_EVENT_MESSAGE_DATA, CONF_FOLDER, CONF_MAX_MESSAGE_SIZE, CONF_SEARCH, @@ -48,6 +49,7 @@ from .const import ( CONF_SSL_CIPHER_LIST, DEFAULT_MAX_MESSAGE_SIZE, DOMAIN, + MESSAGE_DATA_OPTIONS, ) from .errors import InvalidAuth, InvalidFolder @@ -225,6 +227,12 @@ class ImapDataUpdateCoordinator(DataUpdateCoordinator[int | None]): self._last_message_id: str | None = None self.custom_event_template = None self._diagnostics_data: dict[str, Any] = {} + self._event_data_keys: list[str] = entry.data.get( + CONF_EVENT_MESSAGE_DATA, MESSAGE_DATA_OPTIONS + ) + self._max_event_size: int = entry.data.get( + CONF_MAX_MESSAGE_SIZE, DEFAULT_MAX_MESSAGE_SIZE + ) _custom_event_template = entry.data.get(CONF_CUSTOM_EVENT_DATA_TEMPLATE) if _custom_event_template is not None: self.custom_event_template = Template(_custom_event_template, hass=hass) @@ -261,12 +269,11 @@ class ImapDataUpdateCoordinator(DataUpdateCoordinator[int | None]): "folder": self.config_entry.data[CONF_FOLDER], "initial": initial, "date": message.date, - "text": message.text, "sender": message.sender, "subject": message.subject, - "headers": message.headers, "uid": last_message_uid, } + data.update({key: getattr(message, key) for key in self._event_data_keys}) if self.custom_event_template is not None: try: data["custom"] = self.custom_event_template.async_render( @@ -289,11 +296,8 @@ class ImapDataUpdateCoordinator(DataUpdateCoordinator[int | None]): last_message_uid, err, ) - data["text"] = message.text[ - : self.config_entry.data.get( - CONF_MAX_MESSAGE_SIZE, DEFAULT_MAX_MESSAGE_SIZE - ) - ] + if "text" in data: + data["text"] = message.text[: self._max_event_size] self._update_diagnostics(data) if (size := len(json_bytes(data))) > MAX_EVENT_DATA_BYTES: _LOGGER.warning( diff --git a/homeassistant/components/imap/strings.json b/homeassistant/components/imap/strings.json index a8413922036..115d46f3d0e 100644 --- a/homeassistant/components/imap/strings.json +++ b/homeassistant/components/imap/strings.json @@ -72,7 +72,8 @@ "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)", - "enable_push": "Enable Push-IMAP if the server supports it. Turn off if Push-IMAP updates are unreliable." + "enable_push": "Enable Push-IMAP if the server supports it. Turn off if Push-IMAP updates are unreliable.", + "event_message_data": "Message data to be included in the `imap_content` event data:" } } }, @@ -92,6 +93,12 @@ "modern": "Modern ciphers", "intermediate": "Intermediate ciphers" } + }, + "event_message_data": { + "options": { + "text": "Body text", + "headers": "Message headers" + } } }, "services": { diff --git a/tests/components/imap/test_config_flow.py b/tests/components/imap/test_config_flow.py index 2354c5fc9b9..459cecec4a6 100644 --- a/tests/components/imap/test_config_flow.py +++ b/tests/components/imap/test_config_flow.py @@ -29,11 +29,13 @@ MOCK_CONFIG = { "charset": "utf-8", "folder": "INBOX", "search": "UnSeen UnDeleted", + "event_message_data": ["text", "headers"], } MOCK_OPTIONS = { "folder": "INBOX", "search": "UnSeen UnDeleted", + "event_message_data": ["text", "headers"], } pytestmark = pytest.mark.usefixtures("mock_setup_entry") @@ -504,6 +506,38 @@ async def test_config_flow_with_cipherlist_and_ssl_verify( assert len(mock_setup_entry.mock_calls) == 1 +@pytest.mark.parametrize("event_message_data", [[], ["headers"], ["text", "headers"]]) +async def test_config_flow_with_event_message_data( + hass: HomeAssistant, mock_setup_entry: AsyncMock, event_message_data: list +) -> None: + """Test with different message data.""" + config = MOCK_CONFIG.copy() + config["event_message_data"] = event_message_data + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER, "show_advanced_options": False}, + ) + assert result["type"] is FlowResultType.FORM + assert result["errors"] is None + + with patch( + "homeassistant.components.imap.config_flow.connect_to_server" + ) as mock_client: + mock_client.return_value.search.return_value = ( + "OK", + [b""], + ) + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], config + ) + await hass.async_block_till_done() + + assert result2["type"] is FlowResultType.CREATE_ENTRY + assert result2["title"] == "email@email.com" + assert result2["data"] == config + assert len(mock_setup_entry.mock_calls) == 1 + + async def test_config_flow_from_with_advanced_settings( hass: HomeAssistant, mock_setup_entry: AsyncMock ) -> None: diff --git a/tests/components/imap/test_diagnostics.py b/tests/components/imap/test_diagnostics.py index 79d51b73401..721e09352f2 100644 --- a/tests/components/imap/test_diagnostics.py +++ b/tests/components/imap/test_diagnostics.py @@ -66,6 +66,10 @@ async def test_entry_diagnostics( "port": 993, "charset": "utf-8", "folder": "INBOX", + "event_message_data": [ + "text", + "headers", + ], "search": "UnSeen UnDeleted", "custom_event_data_template": "{{ 4 * 4 }}", } diff --git a/tests/components/imap/test_init.py b/tests/components/imap/test_init.py index 69c1aaabb2e..a8f51142d8d 100644 --- a/tests/components/imap/test_init.py +++ b/tests/components/imap/test_init.py @@ -674,6 +674,41 @@ async def test_message_is_truncated( assert len(event_data["text"]) == 3 +@pytest.mark.parametrize("imap_search", [TEST_SEARCH_RESPONSE]) +@pytest.mark.parametrize( + "imap_fetch", [(TEST_FETCH_RESPONSE_TEXT_PLAIN)], ids=["plain"] +) +@pytest.mark.parametrize("imap_has_capability", [True, False], ids=["push", "poll"]) +@pytest.mark.parametrize("event_message_data", [[], ["text"], ["text", "headers"]]) +async def test_message_data( + hass: HomeAssistant, + mock_imap_protocol: MagicMock, + caplog: pytest.LogCaptureFixture, + event_message_data: list, +) -> None: + """Test with different message data.""" + event_called = async_capture_events(hass, "imap_content") + + config = MOCK_CONFIG.copy() + # Mock different message data + config["event_message_data"] = event_message_data + 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 len(event_called) == 1 + + event_data = event_called[0].data + assert set(event_message_data).issubset(set(event_data)) + + @pytest.mark.parametrize( ("imap_search", "imap_fetch"), [(TEST_SEARCH_RESPONSE, TEST_FETCH_RESPONSE_TEXT_PLAIN)],