mirror of
https://github.com/home-assistant/core.git
synced 2025-04-25 17:57:55 +00:00
Add header with parsed date to imap event data (#90422)
This commit is contained in:
parent
0ceee2b6c3
commit
93e1cd8dd8
@ -3,7 +3,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
from collections.abc import Mapping
|
from collections.abc import Mapping
|
||||||
from datetime import timedelta
|
from datetime import datetime, timedelta
|
||||||
import email
|
import email
|
||||||
import logging
|
import logging
|
||||||
from typing import Any
|
from typing import Any
|
||||||
@ -62,6 +62,19 @@ class ImapMessage:
|
|||||||
header_base[key] += header # type: ignore[assignment]
|
header_base[key] += header # type: ignore[assignment]
|
||||||
return header_base
|
return header_base
|
||||||
|
|
||||||
|
@property
|
||||||
|
def date(self) -> datetime | None:
|
||||||
|
"""Get the date the email was sent."""
|
||||||
|
# See https://www.rfc-editor.org/rfc/rfc2822#section-3.3
|
||||||
|
date_str: str | None
|
||||||
|
if (date_str := self.email_message["Date"]) is None:
|
||||||
|
return None
|
||||||
|
# In some cases a timezone or comment is added in parenthesis after the date
|
||||||
|
# We want to strip that part to avoid parsing errors
|
||||||
|
return datetime.strptime(
|
||||||
|
date_str.split("(")[0].strip(), "%a, %d %b %Y %H:%M:%S %z"
|
||||||
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def sender(self) -> str:
|
def sender(self) -> str:
|
||||||
"""Get the parsed message sender from the email."""
|
"""Get the parsed message sender from the email."""
|
||||||
@ -148,6 +161,7 @@ class ImapDataUpdateCoordinator(DataUpdateCoordinator[int | None]):
|
|||||||
"username": self.config_entry.data[CONF_USERNAME],
|
"username": self.config_entry.data[CONF_USERNAME],
|
||||||
"search": self.config_entry.data[CONF_SEARCH],
|
"search": self.config_entry.data[CONF_SEARCH],
|
||||||
"folder": self.config_entry.data[CONF_FOLDER],
|
"folder": self.config_entry.data[CONF_FOLDER],
|
||||||
|
"date": message.date,
|
||||||
"text": message.text,
|
"text": message.text,
|
||||||
"sender": message.sender,
|
"sender": message.sender,
|
||||||
"subject": message.subject,
|
"subject": message.subject,
|
||||||
|
@ -1,6 +1,11 @@
|
|||||||
"""Constants for tests imap integration."""
|
"""Constants for tests imap integration."""
|
||||||
|
|
||||||
TEST_MESSAGE = (
|
|
||||||
|
DATE_HEADER1 = b"Date: Fri, 24 Mar 2023 13:52:00 +0100\r\n"
|
||||||
|
DATE_HEADER2 = b"Date: Fri, 24 Mar 2023 13:52:00 +0100 (CET)\r\n"
|
||||||
|
DATE_HEADER_INVALID = b"2023-03-27T13:52:00 +0100\r\n"
|
||||||
|
|
||||||
|
TEST_MESSAGE_HEADERS1 = (
|
||||||
b"Return-Path: <john.doe@example.com>\r\nDelivered-To: notify@example.com\r\n"
|
b"Return-Path: <john.doe@example.com>\r\nDelivered-To: notify@example.com\r\n"
|
||||||
b"Received: from beta.example.com\r\n\tby beta with LMTP\r\n\t"
|
b"Received: from beta.example.com\r\n\tby beta with LMTP\r\n\t"
|
||||||
b"id eLp2M/GcHWQTLxQAho4UZQ\r\n\t(envelope-from <john.doe@example.com>)\r\n\t"
|
b"id eLp2M/GcHWQTLxQAho4UZQ\r\n\t(envelope-from <john.doe@example.com>)\r\n\t"
|
||||||
@ -8,13 +13,18 @@ TEST_MESSAGE = (
|
|||||||
b"Received: from localhost (localhost [127.0.0.1])\r\n\t"
|
b"Received: from localhost (localhost [127.0.0.1])\r\n\t"
|
||||||
b"by beta.example.com (Postfix) with ESMTP id D0FFA61425\r\n\t"
|
b"by beta.example.com (Postfix) with ESMTP id D0FFA61425\r\n\t"
|
||||||
b"for <notify@example.com>; Fri, 24 Mar 2023 13:52:01 +0100 (CET)\r\n"
|
b"for <notify@example.com>; Fri, 24 Mar 2023 13:52:01 +0100 (CET)\r\n"
|
||||||
b"Date: Fri, 24 Mar 2023 13:52:00 +0100\r\n"
|
)
|
||||||
|
TEST_MESSAGE_HEADERS2 = (
|
||||||
b"MIME-Version: 1.0\r\n"
|
b"MIME-Version: 1.0\r\n"
|
||||||
b"To: notify@example.com\r\n"
|
b"To: notify@example.com\r\n"
|
||||||
b"From: John Doe <john.doe@example.com>\r\n"
|
b"From: John Doe <john.doe@example.com>\r\n"
|
||||||
b"Subject: Test subject\r\n"
|
b"Subject: Test subject\r\n"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
TEST_MESSAGE = TEST_MESSAGE_HEADERS1 + DATE_HEADER1 + TEST_MESSAGE_HEADERS2
|
||||||
|
TEST_MESSAGE_ALT = TEST_MESSAGE_HEADERS1 + DATE_HEADER2 + TEST_MESSAGE_HEADERS2
|
||||||
|
TEST_INVALID_DATE = TEST_MESSAGE_HEADERS1 + DATE_HEADER_INVALID + TEST_MESSAGE_HEADERS2
|
||||||
|
|
||||||
TEST_CONTENT_TEXT_BARE = b"\r\n" b"Test body\r\n" b"\r\n"
|
TEST_CONTENT_TEXT_BARE = b"\r\n" b"Test body\r\n" b"\r\n"
|
||||||
|
|
||||||
TEST_CONTENT_BINARY = (
|
TEST_CONTENT_BINARY = (
|
||||||
@ -88,6 +98,31 @@ TEST_FETCH_RESPONSE_TEXT_PLAIN = (
|
|||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
TEST_FETCH_RESPONSE_TEXT_PLAIN_ALT = (
|
||||||
|
"OK",
|
||||||
|
[
|
||||||
|
b"1 FETCH (BODY[] {"
|
||||||
|
+ str(len(TEST_MESSAGE_ALT + TEST_CONTENT_TEXT_PLAIN)).encode("utf-8")
|
||||||
|
+ b"}",
|
||||||
|
bytearray(TEST_MESSAGE_ALT + TEST_CONTENT_TEXT_PLAIN),
|
||||||
|
b")",
|
||||||
|
b"Fetch completed (0.0001 + 0.000 secs).",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
TEST_FETCH_RESPONSE_INVALID_DATE = (
|
||||||
|
"OK",
|
||||||
|
[
|
||||||
|
b"1 FETCH (BODY[] {"
|
||||||
|
+ str(len(TEST_INVALID_DATE + TEST_CONTENT_TEXT_PLAIN)).encode("utf-8")
|
||||||
|
+ b"}",
|
||||||
|
bytearray(TEST_INVALID_DATE + TEST_CONTENT_TEXT_PLAIN),
|
||||||
|
b")",
|
||||||
|
b"Fetch completed (0.0001 + 0.000 secs).",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
TEST_FETCH_RESPONSE_TEXT_OTHER = (
|
TEST_FETCH_RESPONSE_TEXT_OTHER = (
|
||||||
"OK",
|
"OK",
|
||||||
[
|
[
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
"""Test the imap entry initialization."""
|
"""Test the imap entry initialization."""
|
||||||
import asyncio
|
import asyncio
|
||||||
from datetime import timedelta
|
from datetime import datetime, timedelta
|
||||||
from typing import Any
|
from typing import Any
|
||||||
from unittest.mock import AsyncMock, MagicMock, patch
|
from unittest.mock import AsyncMock, MagicMock, patch
|
||||||
|
|
||||||
@ -17,10 +17,12 @@ from .const import (
|
|||||||
BAD_RESPONSE,
|
BAD_RESPONSE,
|
||||||
TEST_FETCH_RESPONSE_BINARY,
|
TEST_FETCH_RESPONSE_BINARY,
|
||||||
TEST_FETCH_RESPONSE_HTML,
|
TEST_FETCH_RESPONSE_HTML,
|
||||||
|
TEST_FETCH_RESPONSE_INVALID_DATE,
|
||||||
TEST_FETCH_RESPONSE_MULTIPART,
|
TEST_FETCH_RESPONSE_MULTIPART,
|
||||||
TEST_FETCH_RESPONSE_TEXT_BARE,
|
TEST_FETCH_RESPONSE_TEXT_BARE,
|
||||||
TEST_FETCH_RESPONSE_TEXT_OTHER,
|
TEST_FETCH_RESPONSE_TEXT_OTHER,
|
||||||
TEST_FETCH_RESPONSE_TEXT_PLAIN,
|
TEST_FETCH_RESPONSE_TEXT_PLAIN,
|
||||||
|
TEST_FETCH_RESPONSE_TEXT_PLAIN_ALT,
|
||||||
TEST_SEARCH_RESPONSE,
|
TEST_SEARCH_RESPONSE,
|
||||||
)
|
)
|
||||||
from .test_config_flow import MOCK_CONFIG
|
from .test_config_flow import MOCK_CONFIG
|
||||||
@ -66,20 +68,31 @@ async def test_entry_startup_fails(
|
|||||||
|
|
||||||
@pytest.mark.parametrize("imap_search", [TEST_SEARCH_RESPONSE])
|
@pytest.mark.parametrize("imap_search", [TEST_SEARCH_RESPONSE])
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"imap_fetch",
|
("imap_fetch", "valid_date"),
|
||||||
[
|
[
|
||||||
TEST_FETCH_RESPONSE_TEXT_BARE,
|
(TEST_FETCH_RESPONSE_TEXT_BARE, True),
|
||||||
TEST_FETCH_RESPONSE_TEXT_PLAIN,
|
(TEST_FETCH_RESPONSE_TEXT_PLAIN, True),
|
||||||
TEST_FETCH_RESPONSE_TEXT_OTHER,
|
(TEST_FETCH_RESPONSE_TEXT_PLAIN_ALT, True),
|
||||||
TEST_FETCH_RESPONSE_HTML,
|
(TEST_FETCH_RESPONSE_INVALID_DATE, False),
|
||||||
TEST_FETCH_RESPONSE_MULTIPART,
|
(TEST_FETCH_RESPONSE_TEXT_OTHER, True),
|
||||||
TEST_FETCH_RESPONSE_BINARY,
|
(TEST_FETCH_RESPONSE_HTML, True),
|
||||||
|
(TEST_FETCH_RESPONSE_MULTIPART, True),
|
||||||
|
(TEST_FETCH_RESPONSE_BINARY, True),
|
||||||
|
],
|
||||||
|
ids=[
|
||||||
|
"bare",
|
||||||
|
"plain",
|
||||||
|
"plain_alt",
|
||||||
|
"invalid_date",
|
||||||
|
"other",
|
||||||
|
"html",
|
||||||
|
"multipart",
|
||||||
|
"binary",
|
||||||
],
|
],
|
||||||
ids=["bare", "plain", "other", "html", "multipart", "binary"],
|
|
||||||
)
|
)
|
||||||
@pytest.mark.parametrize("imap_has_capability", [True, False], ids=["push", "poll"])
|
@pytest.mark.parametrize("imap_has_capability", [True, False], ids=["push", "poll"])
|
||||||
async def test_receiving_message_successfully(
|
async def test_receiving_message_successfully(
|
||||||
hass: HomeAssistant, mock_imap_protocol: MagicMock
|
hass: HomeAssistant, mock_imap_protocol: MagicMock, valid_date: bool
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test receiving a message successfully."""
|
"""Test receiving a message successfully."""
|
||||||
event_called = async_capture_events(hass, "imap_content")
|
event_called = async_capture_events(hass, "imap_content")
|
||||||
@ -106,6 +119,12 @@ async def test_receiving_message_successfully(
|
|||||||
assert data["sender"] == "john.doe@example.com"
|
assert data["sender"] == "john.doe@example.com"
|
||||||
assert data["subject"] == "Test subject"
|
assert data["subject"] == "Test subject"
|
||||||
assert data["text"]
|
assert data["text"]
|
||||||
|
assert (
|
||||||
|
valid_date
|
||||||
|
and isinstance(data["date"], datetime)
|
||||||
|
or not valid_date
|
||||||
|
and data["date"] is None
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("imap_has_capability", [True, False], ids=["push", "poll"])
|
@pytest.mark.parametrize("imap_has_capability", [True, False], ids=["push", "poll"])
|
||||||
|
Loading…
x
Reference in New Issue
Block a user