mirror of
https://github.com/home-assistant/core.git
synced 2026-09-25 07:51:46 -05:00
Fix SMTP attaching valid images as files when content sniffing fails (#176774)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -137,26 +137,36 @@ def _attach_file(
|
||||
_LOGGER.warning("Attachment %s not found. Skipping", atch_name)
|
||||
return None
|
||||
|
||||
attachment: MIMEImage | MIMEApplication
|
||||
attachment: MIMEImage | MIMEApplication | None = None
|
||||
try:
|
||||
attachment = MIMEImage(file_bytes)
|
||||
except TypeError:
|
||||
# Not all valid images are recognized from their content, e.g. JPEGs
|
||||
# written by ffmpeg for camera.snapshot start with a comment marker
|
||||
# instead of JFIF/Exif, so fall back to guessing from the filename.
|
||||
# Compressed files (e.g. .svgz) must not be labeled as plain images.
|
||||
mime_type, encoding = mimetypes.guess_type(atch_name)
|
||||
maintype, _, subtype = (mime_type or "").partition("/")
|
||||
if encoding is None and maintype == "image":
|
||||
attachment = MIMEImage(file_bytes, _subtype=subtype)
|
||||
|
||||
if attachment is None:
|
||||
_LOGGER.warning(
|
||||
"Attachment %s has an unknown MIME type. Falling back to file",
|
||||
"Could not determine an image type for attachment %s from its"
|
||||
" content or filename. Falling back to file",
|
||||
atch_name,
|
||||
)
|
||||
attachment = MIMEApplication(file_bytes, Name=os.path.basename(atch_name))
|
||||
attachment["Content-Disposition"] = (
|
||||
f'attachment; filename="{os.path.basename(atch_name)}"'
|
||||
)
|
||||
elif content_id:
|
||||
attachment.add_header("Content-ID", f"<{content_id}>")
|
||||
else:
|
||||
if content_id:
|
||||
attachment.add_header("Content-ID", f"<{content_id}>")
|
||||
else:
|
||||
attachment.add_header(
|
||||
"Content-Disposition",
|
||||
f"attachment; filename={os.path.basename(atch_name)}",
|
||||
)
|
||||
attachment.add_header(
|
||||
"Content-Disposition",
|
||||
f"attachment; filename={os.path.basename(atch_name)}",
|
||||
)
|
||||
|
||||
return attachment
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"""The tests for the notify smtp platform."""
|
||||
|
||||
import gzip
|
||||
from pathlib import Path
|
||||
import re
|
||||
from smtplib import (
|
||||
@@ -17,6 +18,7 @@ from syrupy.assertion import SnapshotAssertion
|
||||
|
||||
from homeassistant.components import camera, image, media_source
|
||||
from homeassistant.components.notify import (
|
||||
ATTR_DATA,
|
||||
ATTR_MESSAGE,
|
||||
ATTR_TARGET,
|
||||
DOMAIN as NOTIFY_DOMAIN,
|
||||
@@ -721,3 +723,69 @@ async def test_deprecated_legacy_notify_action(
|
||||
assert issue_registry.async_get_issue(
|
||||
domain=DOMAIN, issue_id="deprecated_notify_action_home_assistant"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("file_name", "file_bytes", "expected", "not_expected"),
|
||||
[
|
||||
(
|
||||
"doorphone.jpg",
|
||||
bytes.fromhex("ffd8fffe0010")
|
||||
+ b"Lavc62.28.102\x00"
|
||||
+ bytes.fromhex("ffdb"),
|
||||
"Content-Type: image/jpeg",
|
||||
"application/octet-stream",
|
||||
),
|
||||
(
|
||||
"diagram.svgz",
|
||||
gzip.compress(b"<svg xmlns='http://www.w3.org/2000/svg'/>"),
|
||||
"application/octet-stream",
|
||||
"Content-Type: image/",
|
||||
),
|
||||
],
|
||||
ids=[
|
||||
"Verify a JPEG the stdlib cannot sniff is attached as an image.",
|
||||
"Verify a compressed image is attached as a file.",
|
||||
],
|
||||
)
|
||||
@pytest.mark.usefixtures("aiosmtplib")
|
||||
async def test_legacy_notify_image_attachment(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
smtp: MagicMock,
|
||||
tmp_path: Path,
|
||||
file_name: str,
|
||||
file_bytes: bytes,
|
||||
expected: str,
|
||||
not_expected: str,
|
||||
) -> None:
|
||||
"""Test the MIME type images are attached with.
|
||||
|
||||
JPEGs written by ffmpeg for camera.snapshot start with an SOI + COM marker
|
||||
instead of JFIF/Exif, which MIMEImage does not recognize, so the file name
|
||||
decides the type. Compressed images stay on the file attachment path.
|
||||
"""
|
||||
|
||||
image_file = tmp_path / file_name
|
||||
image_file.write_bytes(file_bytes)
|
||||
hass.config.allowlist_external_dirs.add(tmp_path)
|
||||
|
||||
config_entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert config_entry.state is ConfigEntryState.LOADED
|
||||
|
||||
await hass.services.async_call(
|
||||
NOTIFY_DOMAIN,
|
||||
"home_assistant",
|
||||
{
|
||||
ATTR_MESSAGE: "Test msg",
|
||||
ATTR_DATA: {"images": [str(image_file)]},
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
sent_message = smtp.sendmail.call_args[0][2]
|
||||
assert expected in sent_message
|
||||
assert not_expected not in sent_message
|
||||
|
||||
Reference in New Issue
Block a user