parent
1cace9a609
commit
8b5177e989
5 changed files with 121 additions and 25 deletions
|
@ -11,7 +11,13 @@ import voluptuous as vol
|
|||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import EVENT_HOMEASSISTANT_STOP, Platform
|
||||
from homeassistant.core import HomeAssistant, ServiceCall, callback
|
||||
from homeassistant.core import (
|
||||
HomeAssistant,
|
||||
ServiceCall,
|
||||
ServiceResponse,
|
||||
SupportsResponse,
|
||||
callback,
|
||||
)
|
||||
from homeassistant.exceptions import (
|
||||
ConfigEntryAuthFailed,
|
||||
ConfigEntryError,
|
||||
|
@ -23,6 +29,7 @@ from homeassistant.helpers.typing import ConfigType
|
|||
|
||||
from .const import CONF_ENABLE_PUSH, DOMAIN
|
||||
from .coordinator import (
|
||||
ImapMessage,
|
||||
ImapPollingDataUpdateCoordinator,
|
||||
ImapPushDataUpdateCoordinator,
|
||||
connect_to_server,
|
||||
|
@ -56,6 +63,7 @@ SERVICE_MOVE_SCHEMA = _SERVICE_UID_SCHEMA.extend(
|
|||
}
|
||||
)
|
||||
SERVICE_DELETE_SCHEMA = _SERVICE_UID_SCHEMA
|
||||
SERVICE_FETCH_TEXT_SCHEMA = _SERVICE_UID_SCHEMA
|
||||
|
||||
|
||||
async def async_get_imap_client(hass: HomeAssistant, entry_id: str) -> IMAP4_SSL:
|
||||
|
@ -188,6 +196,42 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
|||
|
||||
hass.services.async_register(DOMAIN, "delete", async_delete, SERVICE_DELETE_SCHEMA)
|
||||
|
||||
async def async_fetch(call: ServiceCall) -> ServiceResponse:
|
||||
"""Process fetch email service and return content."""
|
||||
entry_id: str = call.data[CONF_ENTRY]
|
||||
uid: str = call.data[CONF_UID]
|
||||
_LOGGER.debug(
|
||||
"Fetch text for message %s. Entry: %s",
|
||||
uid,
|
||||
entry_id,
|
||||
)
|
||||
client = await async_get_imap_client(hass, entry_id)
|
||||
try:
|
||||
response = await client.fetch(uid, "BODY.PEEK[]")
|
||||
except (TimeoutError, AioImapException) as exc:
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="imap_server_fail",
|
||||
translation_placeholders={"error": str(exc)},
|
||||
) from exc
|
||||
raise_on_error(response, "fetch_failed")
|
||||
message = ImapMessage(response.lines[1])
|
||||
await client.close()
|
||||
return {
|
||||
"text": message.text,
|
||||
"sender": message.sender,
|
||||
"subject": message.subject,
|
||||
"uid": uid,
|
||||
}
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN,
|
||||
"fetch",
|
||||
async_fetch,
|
||||
SERVICE_FETCH_TEXT_SCHEMA,
|
||||
supports_response=SupportsResponse.ONLY,
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
|
|
|
@ -12,6 +12,7 @@
|
|||
"services": {
|
||||
"seen": "mdi:email-open-outline",
|
||||
"move": "mdi:email-arrow-right-outline",
|
||||
"delete": "mdi:trash-can-outline"
|
||||
"delete": "mdi:trash-can-outline",
|
||||
"fetch": "mdi:email-sync-outline"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -43,3 +43,16 @@ delete:
|
|||
required: true
|
||||
selector:
|
||||
text:
|
||||
|
||||
fetch:
|
||||
fields:
|
||||
entry:
|
||||
required: true
|
||||
selector:
|
||||
config_entry:
|
||||
integration: "imap"
|
||||
uid:
|
||||
required: true
|
||||
example: "12"
|
||||
selector:
|
||||
text:
|
||||
|
|
|
@ -45,6 +45,9 @@
|
|||
"expunge_failed": {
|
||||
"message": "Expunging the message failed with \"{error}\"."
|
||||
},
|
||||
"fetch_failed": {
|
||||
"message": "Fetching the message text failed with \"{error}\"."
|
||||
},
|
||||
"invalid_entry": {
|
||||
"message": "No valid IMAP entry was found."
|
||||
},
|
||||
|
@ -92,6 +95,20 @@
|
|||
}
|
||||
},
|
||||
"services": {
|
||||
"fetch": {
|
||||
"name": "Fetch message",
|
||||
"description": "Fetch the email message from the server.",
|
||||
"fields": {
|
||||
"entry": {
|
||||
"name": "Entry",
|
||||
"description": "The IMAP config entry."
|
||||
},
|
||||
"uid": {
|
||||
"name": "UID",
|
||||
"description": "The email identifier (UID)."
|
||||
}
|
||||
}
|
||||
},
|
||||
"seen": {
|
||||
"name": "Mark message as seen",
|
||||
"description": "Mark an email as seen.",
|
||||
|
|
|
@ -847,6 +847,17 @@ async def test_services(hass: HomeAssistant, mock_imap_protocol: MagicMock) -> N
|
|||
mock_imap_protocol.store.assert_called_with("1", "+FLAGS (\\Deleted)")
|
||||
mock_imap_protocol.protocol.expunge.assert_called_once()
|
||||
|
||||
# Test fetch service
|
||||
data = {"entry": config_entry.entry_id, "uid": "1"}
|
||||
response = await hass.services.async_call(
|
||||
DOMAIN, "fetch", data, blocking=True, return_response=True
|
||||
)
|
||||
mock_imap_protocol.fetch.assert_called_with("1", "BODY.PEEK[]")
|
||||
assert response["text"] == "Test body\r\n"
|
||||
assert response["sender"] == "john.doe@example.com"
|
||||
assert response["subject"] == "Test subject"
|
||||
assert response["uid"] == "1"
|
||||
|
||||
# Test with invalid entry_id
|
||||
data = {"entry": "invalid", "uid": "1"}
|
||||
with pytest.raises(ServiceValidationError) as exc:
|
||||
|
@ -877,43 +888,53 @@ async def test_services(hass: HomeAssistant, mock_imap_protocol: MagicMock) -> N
|
|||
)
|
||||
|
||||
# Test unexpected errors with storing a flag during a service call
|
||||
service_calls = {
|
||||
"seen": {"entry": config_entry.entry_id, "uid": "1"},
|
||||
"move": {
|
||||
"entry": config_entry.entry_id,
|
||||
"uid": "1",
|
||||
"seen": False,
|
||||
"target_folder": "Trash",
|
||||
},
|
||||
"delete": {"entry": config_entry.entry_id, "uid": "1"},
|
||||
service_calls_response = {
|
||||
"seen": ({"entry": config_entry.entry_id, "uid": "1"}, False),
|
||||
"move": (
|
||||
{
|
||||
"entry": config_entry.entry_id,
|
||||
"uid": "1",
|
||||
"seen": False,
|
||||
"target_folder": "Trash",
|
||||
},
|
||||
False,
|
||||
),
|
||||
"delete": ({"entry": config_entry.entry_id, "uid": "1"}, False),
|
||||
"fetch": ({"entry": config_entry.entry_id, "uid": "1"}, True),
|
||||
}
|
||||
store_error_translation_key = {
|
||||
"seen": "seen_failed",
|
||||
"move": "copy_failed",
|
||||
"delete": "delete_failed",
|
||||
patch_error_translation_key = {
|
||||
"seen": ("store", "seen_failed"),
|
||||
"move": ("copy", "copy_failed"),
|
||||
"delete": ("store", "delete_failed"),
|
||||
"fetch": ("fetch", "fetch_failed"),
|
||||
}
|
||||
for service, data in service_calls.items():
|
||||
for service, (data, response) in service_calls_response.items():
|
||||
with (
|
||||
pytest.raises(ServiceValidationError) as exc,
|
||||
patch.object(
|
||||
mock_imap_protocol, "store", side_effect=AioImapException("Bla")
|
||||
mock_imap_protocol,
|
||||
patch_error_translation_key[service][0],
|
||||
side_effect=AioImapException("Bla"),
|
||||
),
|
||||
):
|
||||
await hass.services.async_call(DOMAIN, service, data, blocking=True)
|
||||
await hass.services.async_call(
|
||||
DOMAIN, service, data, blocking=True, return_response=response
|
||||
)
|
||||
assert exc.value.translation_domain == DOMAIN
|
||||
assert exc.value.translation_key == "imap_server_fail"
|
||||
assert exc.value.translation_placeholders == {"error": "Bla"}
|
||||
# Test with bad responses on store command
|
||||
# Test with bad responses
|
||||
with (
|
||||
pytest.raises(ServiceValidationError) as exc,
|
||||
patch.object(
|
||||
mock_imap_protocol, "store", return_value=Response("BAD", [b"Bla"])
|
||||
),
|
||||
patch.object(
|
||||
mock_imap_protocol, "copy", return_value=Response("BAD", [b"Bla"])
|
||||
mock_imap_protocol,
|
||||
patch_error_translation_key[service][0],
|
||||
return_value=Response("BAD", [b"Bla"]),
|
||||
),
|
||||
):
|
||||
await hass.services.async_call(DOMAIN, service, data, blocking=True)
|
||||
await hass.services.async_call(
|
||||
DOMAIN, service, data, blocking=True, return_response=response
|
||||
)
|
||||
assert exc.value.translation_domain == DOMAIN
|
||||
assert exc.value.translation_key == store_error_translation_key[service]
|
||||
assert exc.value.translation_key == patch_error_translation_key[service][1]
|
||||
assert exc.value.translation_placeholders == {"error": "Bla"}
|
||||
|
|
Loading…
Add table
Reference in a new issue