Add header with parsed date to imap event data (#90422)

This commit is contained in:
Jan Bouwhuis 2023-03-28 22:50:25 +02:00 committed by GitHub
parent 0ceee2b6c3
commit 93e1cd8dd8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 81 additions and 13 deletions

View File

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

View File

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

View File

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