1
mirror of https://github.com/home-assistant/core synced 2024-09-12 15:16:21 +02:00

Add feature to turn off using IMAP-Push on an IMAP server (#96436)

* Add feature to enforce polling an IMAP server

* Add test

* Remove not needed string tweak

* Rename enforce_polling to enable_push

* Push enabled by default
This commit is contained in:
Jan Bouwhuis 2023-07-14 21:26:35 +02:00 committed by GitHub
parent bbc3d0d287
commit 72458b6672
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 85 additions and 14 deletions

View File

@ -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

View File

@ -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,
}

View File

@ -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

View File

@ -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"
}
}
},

View File

@ -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

View File

@ -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])