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
from collections.abc import Mapping
from datetime import timedelta
from datetime import datetime, timedelta
import email
import logging
from typing import Any
@ -62,6 +62,19 @@ class ImapMessage:
header_base[key] += header # type: ignore[assignment]
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
def sender(self) -> str:
"""Get the parsed message sender from the email."""
@ -148,6 +161,7 @@ class ImapDataUpdateCoordinator(DataUpdateCoordinator[int | None]):
"username": self.config_entry.data[CONF_USERNAME],
"search": self.config_entry.data[CONF_SEARCH],
"folder": self.config_entry.data[CONF_FOLDER],
"date": message.date,
"text": message.text,
"sender": message.sender,
"subject": message.subject,

View File

@ -1,6 +1,11 @@
"""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"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"
@ -8,13 +13,18 @@ TEST_MESSAGE = (
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"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"To: notify@example.com\r\n"
b"From: John Doe <john.doe@example.com>\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_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 = (
"OK",
[

View File

@ -1,6 +1,6 @@
"""Test the imap entry initialization."""
import asyncio
from datetime import timedelta
from datetime import datetime, timedelta
from typing import Any
from unittest.mock import AsyncMock, MagicMock, patch
@ -17,10 +17,12 @@ from .const import (
BAD_RESPONSE,
TEST_FETCH_RESPONSE_BINARY,
TEST_FETCH_RESPONSE_HTML,
TEST_FETCH_RESPONSE_INVALID_DATE,
TEST_FETCH_RESPONSE_MULTIPART,
TEST_FETCH_RESPONSE_TEXT_BARE,
TEST_FETCH_RESPONSE_TEXT_OTHER,
TEST_FETCH_RESPONSE_TEXT_PLAIN,
TEST_FETCH_RESPONSE_TEXT_PLAIN_ALT,
TEST_SEARCH_RESPONSE,
)
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_fetch",
("imap_fetch", "valid_date"),
[
TEST_FETCH_RESPONSE_TEXT_BARE,
TEST_FETCH_RESPONSE_TEXT_PLAIN,
TEST_FETCH_RESPONSE_TEXT_OTHER,
TEST_FETCH_RESPONSE_HTML,
TEST_FETCH_RESPONSE_MULTIPART,
TEST_FETCH_RESPONSE_BINARY,
(TEST_FETCH_RESPONSE_TEXT_BARE, True),
(TEST_FETCH_RESPONSE_TEXT_PLAIN, True),
(TEST_FETCH_RESPONSE_TEXT_PLAIN_ALT, True),
(TEST_FETCH_RESPONSE_INVALID_DATE, False),
(TEST_FETCH_RESPONSE_TEXT_OTHER, True),
(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"])
async def test_receiving_message_successfully(
hass: HomeAssistant, mock_imap_protocol: MagicMock
hass: HomeAssistant, mock_imap_protocol: MagicMock, valid_date: bool
) -> None:
"""Test receiving a message successfully."""
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["subject"] == "Test subject"
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"])