Skip to content

Commit 4c0d169

Browse files
authored
Fix imap parsing non rfc compliant date crash (home-assistant#93630)
* Fix imap parsing non rfc compliant date crash * Use parsedate_to_datetime from mail.utils
1 parent 202c907 commit 4c0d169

File tree

3 files changed

+64
-20
lines changed

3 files changed

+64
-20
lines changed

homeassistant/components/imap/coordinator.py

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
from collections.abc import Mapping
66
from datetime import datetime, timedelta
77
import email
8+
from email.header import decode_header, make_header
9+
from email.utils import parseaddr, parsedate_to_datetime
810
import logging
911
from typing import Any
1012

@@ -82,9 +84,9 @@ def headers(self) -> dict[str, tuple[str,]]:
8284
"""Get the email headers."""
8385
header_base: dict[str, tuple[str,]] = {}
8486
for key, value in self.email_message.items():
85-
header: tuple[str,] = (str(value),)
86-
if header_base.setdefault(key, header) != header:
87-
header_base[key] += header # type: ignore[assignment]
87+
header_instances: tuple[str,] = (str(value),)
88+
if header_base.setdefault(key, header_instances) != header_instances:
89+
header_base[key] += header_instances # type: ignore[assignment]
8890
return header_base
8991

9092
@property
@@ -94,23 +96,26 @@ def date(self) -> datetime | None:
9496
date_str: str | None
9597
if (date_str := self.email_message["Date"]) is None:
9698
return None
97-
# In some cases a timezone or comment is added in parenthesis after the date
98-
# We want to strip that part to avoid parsing errors
99-
return datetime.strptime(
100-
date_str.split("(")[0].strip(), "%a, %d %b %Y %H:%M:%S %z"
101-
)
99+
try:
100+
mail_dt_tm = parsedate_to_datetime(date_str)
101+
except ValueError:
102+
_LOGGER.debug(
103+
"Parsed date %s is not compliant with rfc2822#section-3.3", date_str
104+
)
105+
return None
106+
return mail_dt_tm
102107

103108
@property
104109
def sender(self) -> str:
105110
"""Get the parsed message sender from the email."""
106-
return str(email.utils.parseaddr(self.email_message["From"])[1])
111+
return str(parseaddr(self.email_message["From"])[1])
107112

108113
@property
109114
def subject(self) -> str:
110115
"""Decode the message subject."""
111-
decoded_header = email.header.decode_header(self.email_message["Subject"])
112-
header = email.header.make_header(decoded_header)
113-
return str(header)
116+
decoded_header = decode_header(self.email_message["Subject"])
117+
subject_header = make_header(decoded_header)
118+
return str(subject_header)
114119

115120
@property
116121
def text(self) -> str:

tests/components/imap/const.py

Lines changed: 38 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@
33

44
DATE_HEADER1 = b"Date: Fri, 24 Mar 2023 13:52:00 +0100\r\n"
55
DATE_HEADER2 = b"Date: Fri, 24 Mar 2023 13:52:00 +0100 (CET)\r\n"
6-
DATE_HEADER_INVALID = b"2023-03-27T13:52:00 +0100\r\n"
6+
DATE_HEADER3 = b"Date: 24 Mar 2023 13:52:00 +0100\r\n"
7+
DATE_HEADER_INVALID1 = b"2023-03-27T13:52:00 +0100\r\n"
8+
DATE_HEADER_INVALID2 = b"Date: 2023-03-27T13:52:00 +0100\r\n"
9+
DATE_HEADER_INVALID3 = b"Date: Fri, 2023-03-27T13:52:00 +0100\r\n"
710

811
TEST_MESSAGE_HEADERS1 = (
912
b"Return-Path: <john.doe@example.com>\r\nDelivered-To: notify@example.com\r\n"
@@ -23,7 +26,15 @@
2326

2427
TEST_MESSAGE = TEST_MESSAGE_HEADERS1 + DATE_HEADER1 + TEST_MESSAGE_HEADERS2
2528
TEST_MESSAGE_ALT = TEST_MESSAGE_HEADERS1 + DATE_HEADER2 + TEST_MESSAGE_HEADERS2
26-
TEST_INVALID_DATE = TEST_MESSAGE_HEADERS1 + DATE_HEADER_INVALID + TEST_MESSAGE_HEADERS2
29+
TEST_INVALID_DATE1 = (
30+
TEST_MESSAGE_HEADERS1 + DATE_HEADER_INVALID1 + TEST_MESSAGE_HEADERS2
31+
)
32+
TEST_INVALID_DATE2 = (
33+
TEST_MESSAGE_HEADERS1 + DATE_HEADER_INVALID2 + TEST_MESSAGE_HEADERS2
34+
)
35+
TEST_INVALID_DATE3 = (
36+
TEST_MESSAGE_HEADERS1 + DATE_HEADER_INVALID3 + TEST_MESSAGE_HEADERS2
37+
)
2738

2839
TEST_CONTENT_TEXT_BARE = b"\r\n" b"Test body\r\n" b"\r\n"
2940

@@ -110,13 +121,35 @@
110121
],
111122
)
112123

113-
TEST_FETCH_RESPONSE_INVALID_DATE = (
124+
TEST_FETCH_RESPONSE_INVALID_DATE1 = (
125+
"OK",
126+
[
127+
b"1 FETCH (BODY[] {"
128+
+ str(len(TEST_INVALID_DATE1 + TEST_CONTENT_TEXT_PLAIN)).encode("utf-8")
129+
+ b"}",
130+
bytearray(TEST_INVALID_DATE1 + TEST_CONTENT_TEXT_PLAIN),
131+
b")",
132+
b"Fetch completed (0.0001 + 0.000 secs).",
133+
],
134+
)
135+
TEST_FETCH_RESPONSE_INVALID_DATE2 = (
136+
"OK",
137+
[
138+
b"1 FETCH (BODY[] {"
139+
+ str(len(TEST_INVALID_DATE2 + TEST_CONTENT_TEXT_PLAIN)).encode("utf-8")
140+
+ b"}",
141+
bytearray(TEST_INVALID_DATE2 + TEST_CONTENT_TEXT_PLAIN),
142+
b")",
143+
b"Fetch completed (0.0001 + 0.000 secs).",
144+
],
145+
)
146+
TEST_FETCH_RESPONSE_INVALID_DATE3 = (
114147
"OK",
115148
[
116149
b"1 FETCH (BODY[] {"
117-
+ str(len(TEST_INVALID_DATE + TEST_CONTENT_TEXT_PLAIN)).encode("utf-8")
150+
+ str(len(TEST_INVALID_DATE3 + TEST_CONTENT_TEXT_PLAIN)).encode("utf-8")
118151
+ b"}",
119-
bytearray(TEST_INVALID_DATE + TEST_CONTENT_TEXT_PLAIN),
152+
bytearray(TEST_INVALID_DATE3 + TEST_CONTENT_TEXT_PLAIN),
120153
b")",
121154
b"Fetch completed (0.0001 + 0.000 secs).",
122155
],

tests/components/imap/test_init.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,9 @@
1818
EMPTY_SEARCH_RESPONSE,
1919
TEST_FETCH_RESPONSE_BINARY,
2020
TEST_FETCH_RESPONSE_HTML,
21-
TEST_FETCH_RESPONSE_INVALID_DATE,
21+
TEST_FETCH_RESPONSE_INVALID_DATE1,
22+
TEST_FETCH_RESPONSE_INVALID_DATE2,
23+
TEST_FETCH_RESPONSE_INVALID_DATE3,
2224
TEST_FETCH_RESPONSE_MULTIPART,
2325
TEST_FETCH_RESPONSE_TEXT_BARE,
2426
TEST_FETCH_RESPONSE_TEXT_OTHER,
@@ -81,7 +83,9 @@ async def test_entry_startup_fails(
8183
(TEST_FETCH_RESPONSE_TEXT_BARE, True),
8284
(TEST_FETCH_RESPONSE_TEXT_PLAIN, True),
8385
(TEST_FETCH_RESPONSE_TEXT_PLAIN_ALT, True),
84-
(TEST_FETCH_RESPONSE_INVALID_DATE, False),
86+
(TEST_FETCH_RESPONSE_INVALID_DATE1, False),
87+
(TEST_FETCH_RESPONSE_INVALID_DATE2, False),
88+
(TEST_FETCH_RESPONSE_INVALID_DATE3, False),
8589
(TEST_FETCH_RESPONSE_TEXT_OTHER, True),
8690
(TEST_FETCH_RESPONSE_HTML, True),
8791
(TEST_FETCH_RESPONSE_MULTIPART, True),
@@ -91,7 +95,9 @@ async def test_entry_startup_fails(
9195
"bare",
9296
"plain",
9397
"plain_alt",
94-
"invalid_date",
98+
"invalid_date1",
99+
"invalid_date2",
100+
"invalid_date3",
95101
"other",
96102
"html",
97103
"multipart",

0 commit comments

Comments
 (0)