mirror of
https://github.com/home-assistant/core.git
synced 2026-09-06 05:22:44 +01:00
Migrate SMTP integration to aiosmtplib (#180707)
Co-authored-by: Franck Nijhof <git@frenck.dev>
This commit is contained in:
@@ -1,8 +1,8 @@
|
||||
"""The smtp integration."""
|
||||
|
||||
import logging
|
||||
from smtplib import SMTPAuthenticationError
|
||||
from socket import gaierror
|
||||
|
||||
from aiosmtplib import SMTP, SMTPAuthenticationError, SMTPException
|
||||
|
||||
from homeassistant.components.notify import DOMAIN as NOTIFY_DOMAIN
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
@@ -11,7 +11,6 @@ from homeassistant.const import (
|
||||
CONF_PASSWORD,
|
||||
CONF_PORT,
|
||||
CONF_RECIPIENT,
|
||||
CONF_SENDER,
|
||||
CONF_TIMEOUT,
|
||||
CONF_USERNAME,
|
||||
CONF_VERIFY_SSL,
|
||||
@@ -25,23 +24,14 @@ from homeassistant.helpers import (
|
||||
entity_registry as er,
|
||||
)
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
from homeassistant.util.ssl import create_client_context
|
||||
from homeassistant.util.ssl import client_context, client_context_no_verify
|
||||
|
||||
from .const import (
|
||||
CONF_ENCRYPTION,
|
||||
CONF_ENTRY,
|
||||
CONF_OLD_RECIPIENT,
|
||||
CONF_SENDER_NAME,
|
||||
CONF_SERVER,
|
||||
DEFAULT_TIMEOUT,
|
||||
DOMAIN,
|
||||
)
|
||||
from .helpers import SmtpClient
|
||||
from .const import CONF_ENCRYPTION, CONF_ENTRY, CONF_OLD_RECIPIENT, CONF_SERVER, DOMAIN
|
||||
from .services import async_setup_services
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
type SmtpConfigEntry = ConfigEntry[SmtpClient]
|
||||
type SmtpConfigEntry = ConfigEntry[SMTP]
|
||||
|
||||
PLATFORMS: list[Platform] = [Platform.NOTIFY]
|
||||
|
||||
@@ -75,30 +65,30 @@ async def async_setup_entry(hass: HomeAssistant, entry: SmtpConfigEntry) -> bool
|
||||
{},
|
||||
)
|
||||
)
|
||||
client = SmtpClient(
|
||||
server=entry.data[CONF_SERVER],
|
||||
|
||||
client = SMTP(
|
||||
hostname=entry.data[CONF_SERVER],
|
||||
port=entry.data[CONF_PORT],
|
||||
timeout=entry.options.get(CONF_TIMEOUT, DEFAULT_TIMEOUT),
|
||||
sender=entry.data[CONF_SENDER],
|
||||
encryption=entry.data[CONF_ENCRYPTION],
|
||||
username=entry.data.get(CONF_USERNAME),
|
||||
password=entry.data.get(CONF_PASSWORD),
|
||||
sender_name=entry.data.get(CONF_SENDER_NAME),
|
||||
verify_ssl=entry.data[CONF_VERIFY_SSL],
|
||||
ssl_context=(
|
||||
await hass.async_add_executor_job(create_client_context)
|
||||
timeout=entry.options.get(CONF_TIMEOUT),
|
||||
use_tls=entry.data[CONF_ENCRYPTION] == "tls",
|
||||
start_tls=entry.data[CONF_ENCRYPTION] == "starttls",
|
||||
tls_context=(
|
||||
client_context()
|
||||
if entry.data[CONF_VERIFY_SSL]
|
||||
else None
|
||||
else client_context_no_verify()
|
||||
),
|
||||
)
|
||||
try:
|
||||
await hass.async_add_executor_job(lambda: client.connect().quit())
|
||||
async with client:
|
||||
pass
|
||||
except SMTPAuthenticationError as e:
|
||||
raise ConfigEntryAuthFailed(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="authentication_error",
|
||||
) from e
|
||||
except (gaierror, ConnectionRefusedError) as e:
|
||||
except SMTPException as e:
|
||||
_LOGGER.debug("Full exception:", exc_info=True)
|
||||
raise ConfigEntryNotReady(
|
||||
translation_domain=DOMAIN,
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
"""Config flow for the SMTP integration."""
|
||||
|
||||
from collections.abc import Mapping
|
||||
from contextlib import suppress
|
||||
import logging
|
||||
from smtplib import SMTP, SMTP_SSL, SMTPAuthenticationError, SMTPException
|
||||
import socket
|
||||
from ssl import SSLCertVerificationError
|
||||
from typing import Any, override
|
||||
|
||||
from aiosmtplib import SMTP, SMTPAuthenticationError, SMTPException, SMTPTimeoutError
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant import data_entry_flow
|
||||
@@ -33,7 +30,7 @@ from homeassistant.const import (
|
||||
CONF_VERIFY_SSL,
|
||||
UnitOfTime,
|
||||
)
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.selector import (
|
||||
NumberSelector,
|
||||
@@ -46,7 +43,7 @@ from homeassistant.helpers.selector import (
|
||||
TextSelectorConfig,
|
||||
TextSelectorType,
|
||||
)
|
||||
from homeassistant.util.ssl import create_client_context
|
||||
from homeassistant.util.ssl import client_context, client_context_no_verify
|
||||
|
||||
from . import SmtpConfigEntry
|
||||
from .const import (
|
||||
@@ -169,9 +166,7 @@ class MailConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
)
|
||||
entry_data = user_input.copy()
|
||||
options = entry_data.pop(SECTION_OPTIONS)
|
||||
errors = await self.hass.async_add_executor_job(
|
||||
validate_input, entry_data, options
|
||||
)
|
||||
errors = await validate_input(self.hass, entry_data, options)
|
||||
if not errors:
|
||||
return self.async_create_entry(
|
||||
title=entry_data.get(CONF_SENDER_NAME, entry_data[CONF_SENDER]),
|
||||
@@ -223,9 +218,7 @@ class MailConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
CONF_USERNAME: user_input.get(CONF_USERNAME),
|
||||
}
|
||||
)
|
||||
errors = await self.hass.async_add_executor_job(
|
||||
validate_input, user_input, dict(entry.options)
|
||||
)
|
||||
errors = await validate_input(self.hass, user_input, dict(entry.options))
|
||||
if not errors:
|
||||
return self.async_update_and_abort(
|
||||
entry,
|
||||
@@ -255,8 +248,8 @@ class MailConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
entry = self._get_reauth_entry()
|
||||
|
||||
if user_input is not None:
|
||||
errors = await self.hass.async_add_executor_job(
|
||||
validate_input, {**entry.data, **user_input}, dict(entry.options)
|
||||
errors = await validate_input(
|
||||
self.hass, {**entry.data, **user_input}, dict(entry.options)
|
||||
)
|
||||
if not errors:
|
||||
return self.async_update_and_abort(
|
||||
@@ -279,9 +272,8 @@ class MailConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
options = {CONF_TIMEOUT: import_info.pop(CONF_TIMEOUT, DEFAULT_TIMEOUT)}
|
||||
self._async_abort_entries_match(import_info)
|
||||
|
||||
errors = await self.hass.async_add_executor_job(
|
||||
validate_input, import_info, options
|
||||
)
|
||||
errors = await validate_input(self.hass, import_info, options)
|
||||
|
||||
if not errors:
|
||||
title = (
|
||||
import_info.get(CONF_NAME)
|
||||
@@ -306,49 +298,36 @@ class MailConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
return self.async_abort(reason=errors["base"])
|
||||
|
||||
|
||||
def validate_input(
|
||||
user_input: dict[str, Any], options: dict[str, Any]
|
||||
async def validate_input(
|
||||
hass: HomeAssistant, user_input: dict[str, Any], options: dict[str, Any]
|
||||
) -> dict[str, str]:
|
||||
"""Validate the user input allows us to connect."""
|
||||
errors: dict[str, str] = {}
|
||||
ssl_context = create_client_context() if user_input[CONF_VERIFY_SSL] else None
|
||||
mail: SMTP_SSL | SMTP | None = None
|
||||
try:
|
||||
if user_input[CONF_ENCRYPTION] == "tls":
|
||||
mail = SMTP_SSL(
|
||||
user_input[CONF_SERVER],
|
||||
user_input[CONF_PORT],
|
||||
timeout=options.get(CONF_TIMEOUT, DEFAULT_TIMEOUT),
|
||||
context=ssl_context,
|
||||
)
|
||||
else:
|
||||
mail = SMTP(
|
||||
user_input[CONF_SERVER],
|
||||
user_input[CONF_PORT],
|
||||
timeout=options.get(CONF_TIMEOUT, DEFAULT_TIMEOUT),
|
||||
)
|
||||
mail.ehlo_or_helo_if_needed()
|
||||
if user_input[CONF_ENCRYPTION] == "starttls":
|
||||
mail.starttls(context=ssl_context)
|
||||
mail.ehlo()
|
||||
if user_input.get(CONF_USERNAME) and user_input.get(CONF_PASSWORD):
|
||||
mail.login(user_input[CONF_USERNAME], user_input[CONF_PASSWORD])
|
||||
|
||||
except TimeoutError:
|
||||
async with SMTP(
|
||||
hostname=user_input[CONF_SERVER],
|
||||
port=user_input[CONF_PORT],
|
||||
username=user_input.get(CONF_USERNAME),
|
||||
password=user_input.get(CONF_PASSWORD),
|
||||
timeout=options.get(CONF_TIMEOUT),
|
||||
use_tls=user_input[CONF_ENCRYPTION] == "tls",
|
||||
start_tls=user_input[CONF_ENCRYPTION] == "starttls",
|
||||
tls_context=(
|
||||
client_context()
|
||||
if user_input[CONF_VERIFY_SSL]
|
||||
else client_context_no_verify()
|
||||
),
|
||||
):
|
||||
pass
|
||||
except SMTPTimeoutError:
|
||||
errors["base"] = "timeout_connect"
|
||||
except SMTPAuthenticationError:
|
||||
errors["base"] = "invalid_auth"
|
||||
except SSLCertVerificationError:
|
||||
errors["base"] = "invalid_cert"
|
||||
except socket.gaierror, ConnectionRefusedError, SMTPException:
|
||||
except SMTPException:
|
||||
errors["base"] = "cannot_connect"
|
||||
except Exception:
|
||||
_LOGGER.exception("Unexpected exception")
|
||||
errors["base"] = "unknown"
|
||||
finally:
|
||||
if mail is not None:
|
||||
with suppress(SMTPException):
|
||||
mail.quit()
|
||||
|
||||
return errors
|
||||
|
||||
@@ -362,11 +341,13 @@ class RecipientSubentryFlowHandler(ConfigSubentryFlow):
|
||||
"""User flow to add a new recipient."""
|
||||
|
||||
if user_input is not None:
|
||||
return self.async_create_entry(
|
||||
result = self.async_create_entry(
|
||||
title=user_input.get(CONF_NAME, user_input[CONF_RECIPIENT]),
|
||||
data={},
|
||||
unique_id=user_input[CONF_RECIPIENT],
|
||||
)
|
||||
self.hass.config_entries.async_schedule_reload(self._get_entry().entry_id)
|
||||
return result
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
data_schema=vol.Schema(
|
||||
|
||||
@@ -5,5 +5,6 @@
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/smtp",
|
||||
"integration_type": "service",
|
||||
"iot_class": "cloud_push"
|
||||
"iot_class": "cloud_push",
|
||||
"requirements": ["aiosmtplib==5.1.2"]
|
||||
}
|
||||
|
||||
@@ -7,17 +7,11 @@ from email.mime.multipart import MIMEMultipart
|
||||
from email.mime.text import MIMEText
|
||||
import email.utils
|
||||
import logging
|
||||
from smtplib import (
|
||||
SMTP,
|
||||
SMTP_SSL,
|
||||
SMTPAuthenticationError,
|
||||
SMTPException,
|
||||
SMTPServerDisconnected,
|
||||
)
|
||||
from socket import gaierror
|
||||
from smtplib import SMTPException, SMTPServerDisconnected
|
||||
from ssl import SSLContext
|
||||
from typing import TYPE_CHECKING, Any, override
|
||||
|
||||
import aiosmtplib
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.notify import (
|
||||
@@ -92,6 +86,8 @@ _LOGGER = logging.getLogger(__name__)
|
||||
|
||||
PARALLEL_UPDATES = 1
|
||||
|
||||
RETRIES = 2
|
||||
|
||||
PLATFORM_SCHEMA = NOTIFY_PLATFORM_SCHEMA.extend(
|
||||
{
|
||||
vol.Required(CONF_RECIPIENT): vol.All(cv.ensure_list, [vol.Email()]),
|
||||
@@ -198,7 +194,7 @@ class MailNotifyEntity(NotifyEntity):
|
||||
self,
|
||||
entry: SmtpConfigEntry,
|
||||
subentry: ConfigSubentry,
|
||||
client: SmtpClient,
|
||||
client: aiosmtplib.SMTP,
|
||||
) -> None:
|
||||
"""Initialize the notify entity."""
|
||||
|
||||
@@ -214,14 +210,14 @@ class MailNotifyEntity(NotifyEntity):
|
||||
self._attr_name = subentry.title
|
||||
|
||||
@override
|
||||
def send_message(self, message: str, title: str | None = None) -> None:
|
||||
async def async_send_message(self, message: str, title: str | None = None) -> None:
|
||||
"""Send an email message via notify.send_message action."""
|
||||
|
||||
msg = EmailMessage()
|
||||
msg.set_content(message)
|
||||
msg["Subject"] = title or ATTR_TITLE_DEFAULT
|
||||
|
||||
self._send_email(msg=msg)
|
||||
await self._send_email(msg=msg)
|
||||
|
||||
async def smtp_send_message(
|
||||
self,
|
||||
@@ -285,10 +281,10 @@ class MailNotifyEntity(NotifyEntity):
|
||||
filename=target_filename,
|
||||
)
|
||||
|
||||
await self.hass.async_add_executor_job(self._send_email, msg)
|
||||
await self._send_email(msg)
|
||||
self._async_record_notification()
|
||||
|
||||
def _send_email(self, msg: EmailMessage) -> None:
|
||||
async def _send_email(self, msg: EmailMessage) -> None:
|
||||
"""Send the message."""
|
||||
if TYPE_CHECKING:
|
||||
assert self._subentry.unique_id
|
||||
@@ -307,41 +303,25 @@ class MailNotifyEntity(NotifyEntity):
|
||||
msg.add_header("Date", email.utils.format_datetime(dt_util.now()))
|
||||
msg.add_header("Message-Id", email.utils.make_msgid())
|
||||
|
||||
client: SMTP_SSL | SMTP | None = None
|
||||
for attempt in range(self._client.tries):
|
||||
for attempt in range(RETRIES):
|
||||
try:
|
||||
client = self._client.connect()
|
||||
except SMTPAuthenticationError as e:
|
||||
async with self._client as client:
|
||||
await client.send_message(msg)
|
||||
break
|
||||
except aiosmtplib.SMTPAuthenticationError as e:
|
||||
raise ConfigEntryAuthFailed(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="authentication_error",
|
||||
) from e
|
||||
except (gaierror, ConnectionRefusedError, SMTPException) as e:
|
||||
_LOGGER.debug("Full exception:", exc_info=True)
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="send_mail_connection_error",
|
||||
) from e
|
||||
|
||||
try:
|
||||
client.sendmail(
|
||||
self._entry.data[CONF_SENDER],
|
||||
self._subentry.unique_id,
|
||||
msg.as_string(),
|
||||
)
|
||||
break
|
||||
except SMTPException as e:
|
||||
except aiosmtplib.SMTPException as e:
|
||||
_LOGGER.debug(
|
||||
"Error sending mail at attempt %s:", attempt + 1, exc_info=True
|
||||
)
|
||||
if attempt == self._client.tries - 1:
|
||||
if attempt == RETRIES - 1:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="send_mail_connection_error",
|
||||
) from e
|
||||
finally:
|
||||
with suppress(SMTPException):
|
||||
client.quit()
|
||||
|
||||
|
||||
class MailNotificationService(SmtpClient, BaseNotificationService):
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
"error": {
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
|
||||
"invalid_cert": "Invalid certificate",
|
||||
"timeout_connect": "[%key:common::config_flow::error::timeout_connect%]",
|
||||
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||
},
|
||||
|
||||
Generated
+3
@@ -434,6 +434,9 @@ aioskybell==22.7.0
|
||||
# homeassistant.components.slimproto
|
||||
aioslimproto==3.0.0
|
||||
|
||||
# homeassistant.components.smtp
|
||||
aiosmtplib==5.1.2
|
||||
|
||||
# homeassistant.components.solaredge
|
||||
aiosolaredge==1.0.2
|
||||
|
||||
|
||||
@@ -49,12 +49,23 @@ def mock_setup_entry() -> Generator[AsyncMock]:
|
||||
def mock_smtp() -> Generator[MagicMock]:
|
||||
"""Mock smtplib.SMTP."""
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.smtp.helpers.smtplib.SMTP", autospec=True
|
||||
) as mock_client:
|
||||
client = mock_client.return_value
|
||||
client.cls = mock_client
|
||||
yield client
|
||||
|
||||
|
||||
@pytest.fixture(name="aiosmtplib")
|
||||
def mock_aiosmtplib() -> Generator[AsyncMock]:
|
||||
"""Mock aiosmtplib."""
|
||||
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.smtp.config_flow.SMTP_SSL", autospec=True
|
||||
"homeassistant.components.smtp.config_flow.SMTP", autospec=True
|
||||
) as mock_client,
|
||||
patch("homeassistant.components.smtp.helpers.smtplib.SMTP", new=mock_client),
|
||||
patch("homeassistant.components.smtp.config_flow.SMTP", new=mock_client),
|
||||
patch("homeassistant.components.smtp.SMTP", new=mock_client),
|
||||
):
|
||||
client = mock_client.return_value
|
||||
client.cls = mock_client
|
||||
@@ -83,6 +94,22 @@ def mock_randrange() -> Generator[None]:
|
||||
yield
|
||||
|
||||
|
||||
@pytest.fixture(name="client_context")
|
||||
def mock_client_context() -> Generator[None]:
|
||||
"""Mock client_context."""
|
||||
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.smtp.config_flow.client_context"
|
||||
) as mock_client,
|
||||
patch(
|
||||
"homeassistant.components.smtp.client_context",
|
||||
new=mock_client,
|
||||
),
|
||||
):
|
||||
yield mock_client
|
||||
|
||||
|
||||
@pytest.fixture(name="config_entry")
|
||||
def mock_config_entry() -> MockConfigEntry:
|
||||
"""Mock smtp configuration entry."""
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
"""Test the SMTP config flow."""
|
||||
|
||||
from smtplib import SMTPAuthenticationError, SMTPServerDisconnected
|
||||
from socket import gaierror
|
||||
from ssl import SSLCertVerificationError
|
||||
from unittest.mock import AsyncMock, MagicMock
|
||||
|
||||
from aiosmtplib import SMTPAuthenticationError, SMTPException, SMTPTimeoutError
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.smtp.const import (
|
||||
CONF_ENCRYPTION,
|
||||
CONF_SENDER_NAME,
|
||||
DOMAIN,
|
||||
SECTION_OPTIONS,
|
||||
@@ -38,9 +35,12 @@ from .conftest import USER_INPUT
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
@pytest.mark.parametrize("encryption", ["tls", "starttls"])
|
||||
@pytest.mark.usefixtures("smtp")
|
||||
async def test_form(
|
||||
hass: HomeAssistant, mock_setup_entry: AsyncMock, encryption: str, smtp: MagicMock
|
||||
hass: HomeAssistant,
|
||||
mock_setup_entry: AsyncMock,
|
||||
aiosmtplib: AsyncMock,
|
||||
client_context: MagicMock,
|
||||
) -> None:
|
||||
"""Test we get the form."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
@@ -53,7 +53,6 @@ async def test_form(
|
||||
result["flow_id"],
|
||||
{
|
||||
**USER_INPUT,
|
||||
CONF_ENCRYPTION: encryption,
|
||||
SECTION_OPTIONS: {CONF_TIMEOUT: 60},
|
||||
},
|
||||
)
|
||||
@@ -61,10 +60,7 @@ async def test_form(
|
||||
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result["title"] == "Home Assistant"
|
||||
assert result["data"] == {
|
||||
**USER_INPUT,
|
||||
CONF_ENCRYPTION: encryption,
|
||||
}
|
||||
assert result["data"] == USER_INPUT
|
||||
assert result["options"] == {CONF_TIMEOUT: 60}
|
||||
assert len(mock_setup_entry.mock_calls) == 1
|
||||
|
||||
@@ -81,11 +77,19 @@ async def test_form(
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result["title"] == "Recipient"
|
||||
assert result["unique_id"] == "recipient@example.com"
|
||||
assert smtp.cls.call_args[0] == ("mail.example.com", 587)
|
||||
assert smtp.cls.call_args[1]["timeout"] == 60
|
||||
aiosmtplib.cls.assert_called_once_with(
|
||||
hostname="mail.example.com",
|
||||
port=587,
|
||||
username="test-username",
|
||||
password="test-password",
|
||||
timeout=60,
|
||||
use_tls=False,
|
||||
start_tls=True,
|
||||
tls_context=client_context(),
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("smtp")
|
||||
@pytest.mark.usefixtures("smtp", "aiosmtplib")
|
||||
async def test_form_already_configured(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
@@ -117,18 +121,16 @@ async def test_form_already_configured(
|
||||
("exception", "text_error"),
|
||||
[
|
||||
(SMTPAuthenticationError(0, ""), "invalid_auth"),
|
||||
(ConnectionRefusedError, "cannot_connect"),
|
||||
(TimeoutError, "timeout_connect"),
|
||||
(SMTPServerDisconnected, "cannot_connect"),
|
||||
(gaierror, "cannot_connect"),
|
||||
(SSLCertVerificationError, "invalid_cert"),
|
||||
(SMTPException(""), "cannot_connect"),
|
||||
(SMTPTimeoutError(""), "timeout_connect"),
|
||||
(ValueError, "unknown"),
|
||||
],
|
||||
)
|
||||
@pytest.mark.usefixtures("smtp")
|
||||
async def test_form_errors(
|
||||
hass: HomeAssistant,
|
||||
mock_setup_entry: AsyncMock,
|
||||
smtp: MagicMock,
|
||||
aiosmtplib: MagicMock,
|
||||
exception: Exception,
|
||||
text_error: str,
|
||||
) -> None:
|
||||
@@ -137,7 +139,7 @@ async def test_form_errors(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
|
||||
smtp.login.side_effect = exception
|
||||
aiosmtplib.cls.side_effect = exception
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
@@ -150,7 +152,7 @@ async def test_form_errors(
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["errors"] == {"base": text_error}
|
||||
|
||||
smtp.login.side_effect = None
|
||||
aiosmtplib.cls.side_effect = None
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
@@ -168,7 +170,7 @@ async def test_form_errors(
|
||||
assert len(mock_setup_entry.mock_calls) == 1
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("smtp")
|
||||
@pytest.mark.usefixtures("smtp", "aiosmtplib")
|
||||
async def test_form_recipient_already_configured(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
@@ -198,7 +200,7 @@ async def test_form_recipient_already_configured(
|
||||
assert result["reason"] == "already_configured"
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("smtp")
|
||||
@pytest.mark.usefixtures("smtp", "aiosmtplib")
|
||||
async def test_options_flow(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
@@ -229,8 +231,12 @@ async def test_options_flow(
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("smtp")
|
||||
async def test_form_reconfigure(
|
||||
hass: HomeAssistant, config_entry: MockConfigEntry, smtp: MagicMock
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
aiosmtplib: AsyncMock,
|
||||
client_context: MagicMock,
|
||||
) -> None:
|
||||
"""Test reconfigure flow."""
|
||||
|
||||
@@ -263,10 +269,19 @@ async def test_form_reconfigure(
|
||||
}
|
||||
|
||||
assert len(hass.config_entries.async_entries()) == 1
|
||||
smtp.cls.assert_called_with("mail.example.com", 587, timeout=1312)
|
||||
aiosmtplib.cls.assert_called_once_with(
|
||||
hostname="mail.example.com",
|
||||
port=587,
|
||||
username="new-username",
|
||||
password="new-password",
|
||||
timeout=1312,
|
||||
use_tls=False,
|
||||
start_tls=True,
|
||||
tls_context=client_context(),
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("smtp")
|
||||
@pytest.mark.usefixtures("smtp", "aiosmtplib")
|
||||
async def test_form_reconfigure_already_configured(
|
||||
hass: HomeAssistant, config_entry: MockConfigEntry
|
||||
) -> None:
|
||||
@@ -308,11 +323,8 @@ async def test_form_reconfigure_already_configured(
|
||||
("exception", "text_error"),
|
||||
[
|
||||
(SMTPAuthenticationError(0, ""), "invalid_auth"),
|
||||
(ConnectionRefusedError, "cannot_connect"),
|
||||
(SMTPServerDisconnected, "cannot_connect"),
|
||||
(TimeoutError, "timeout_connect"),
|
||||
(gaierror, "cannot_connect"),
|
||||
(SSLCertVerificationError, "invalid_cert"),
|
||||
(SMTPException(""), "cannot_connect"),
|
||||
(SMTPTimeoutError(""), "timeout_connect"),
|
||||
(ValueError, "unknown"),
|
||||
],
|
||||
)
|
||||
@@ -320,13 +332,13 @@ async def test_form_reconfigure_already_configured(
|
||||
async def test_form_reconfigure_errors(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
smtp: MagicMock,
|
||||
aiosmtplib: AsyncMock,
|
||||
exception: Exception,
|
||||
text_error: str,
|
||||
) -> None:
|
||||
"""Test reconfigure flow connection errors."""
|
||||
|
||||
smtp.login.side_effect = exception
|
||||
aiosmtplib.cls.side_effect = exception
|
||||
|
||||
config_entry.add_to_hass(hass)
|
||||
|
||||
@@ -348,7 +360,7 @@ async def test_form_reconfigure_errors(
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["errors"] == {"base": text_error}
|
||||
|
||||
smtp.login.side_effect = None
|
||||
aiosmtplib.cls.side_effect = None
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
@@ -372,8 +384,12 @@ async def test_form_reconfigure_errors(
|
||||
assert len(hass.config_entries.async_entries()) == 1
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("smtp")
|
||||
async def test_form_reauth(
|
||||
hass: HomeAssistant, config_entry: MockConfigEntry, smtp: MagicMock
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
aiosmtplib: AsyncMock,
|
||||
client_context: MagicMock,
|
||||
) -> None:
|
||||
"""Test reauth flow."""
|
||||
|
||||
@@ -403,16 +419,24 @@ async def test_form_reauth(
|
||||
}
|
||||
|
||||
assert len(hass.config_entries.async_entries()) == 1
|
||||
smtp.cls.assert_called_with("mail.example.com", 587, timeout=1312)
|
||||
aiosmtplib.cls.assert_called_once_with(
|
||||
hostname="mail.example.com",
|
||||
port=587,
|
||||
username="new-username",
|
||||
password="new-password",
|
||||
timeout=1312,
|
||||
use_tls=False,
|
||||
start_tls=True,
|
||||
tls_context=client_context(),
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("exception", "text_error"),
|
||||
[
|
||||
(SMTPAuthenticationError(0, ""), "invalid_auth"),
|
||||
(ConnectionRefusedError, "cannot_connect"),
|
||||
(gaierror, "cannot_connect"),
|
||||
(SSLCertVerificationError, "invalid_cert"),
|
||||
(SMTPException(""), "cannot_connect"),
|
||||
(SMTPTimeoutError(""), "timeout_connect"),
|
||||
(ValueError, "unknown"),
|
||||
],
|
||||
)
|
||||
@@ -420,13 +444,13 @@ async def test_form_reauth(
|
||||
async def test_form_reauth_errors(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
smtp: MagicMock,
|
||||
aiosmtplib: AsyncMock,
|
||||
exception: Exception,
|
||||
text_error: str,
|
||||
) -> None:
|
||||
"""Test reauth flow connection errors."""
|
||||
|
||||
smtp.login.side_effect = exception
|
||||
aiosmtplib.cls.side_effect = exception
|
||||
|
||||
config_entry.add_to_hass(hass)
|
||||
|
||||
@@ -446,7 +470,7 @@ async def test_form_reauth_errors(
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["errors"] == {"base": text_error}
|
||||
|
||||
smtp.login.side_effect = None
|
||||
aiosmtplib.cls.side_effect = None
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
@@ -467,7 +491,7 @@ async def test_form_reauth_errors(
|
||||
assert len(hass.config_entries.async_entries()) == 1
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("smtp")
|
||||
@pytest.mark.usefixtures("smtp", "aiosmtplib")
|
||||
async def test_form_subentry_reconfigure(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
@@ -508,7 +532,7 @@ async def test_form_subentry_reconfigure(
|
||||
assert entity.unique_id == "123456789_changed@example.com"
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("smtp")
|
||||
@pytest.mark.usefixtures("smtp", "aiosmtplib")
|
||||
async def test_form_subentry_reconfigure_already_configured(
|
||||
hass: HomeAssistant,
|
||||
) -> None:
|
||||
@@ -555,7 +579,7 @@ async def test_form_subentry_reconfigure_already_configured(
|
||||
assert result["reason"] == "already_configured"
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("smtp")
|
||||
@pytest.mark.usefixtures("smtp", "aiosmtplib")
|
||||
async def test_form_subentry_reconfigure_updates_title(
|
||||
hass: HomeAssistant,
|
||||
) -> None:
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
"""Tests for the SMTP integration."""
|
||||
|
||||
from smtplib import SMTPAuthenticationError
|
||||
from socket import gaierror
|
||||
from unittest.mock import AsyncMock, MagicMock
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
from aiosmtplib import SMTPAuthenticationError, SMTPException
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.notify import DOMAIN as NOTIFY_DOMAIN
|
||||
@@ -33,7 +32,7 @@ from homeassistant.setup import async_setup_component
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("smtp")
|
||||
@pytest.mark.usefixtures("smtp", "aiosmtplib")
|
||||
async def test_entry_setup_unload(
|
||||
hass: HomeAssistant, config_entry: MockConfigEntry
|
||||
) -> None:
|
||||
@@ -57,21 +56,21 @@ async def test_entry_setup_unload(
|
||||
@pytest.mark.parametrize(
|
||||
("exception", "state"),
|
||||
[
|
||||
(ConnectionRefusedError, ConfigEntryState.SETUP_RETRY),
|
||||
(gaierror, ConfigEntryState.SETUP_RETRY),
|
||||
(SMTPException(""), ConfigEntryState.SETUP_RETRY),
|
||||
(SMTPAuthenticationError(0, ""), ConfigEntryState.SETUP_ERROR),
|
||||
],
|
||||
)
|
||||
@pytest.mark.usefixtures("smtp")
|
||||
async def test_config_entry_not_ready(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
smtp: MagicMock,
|
||||
aiosmtplib: AsyncMock,
|
||||
exception: Exception,
|
||||
state: ConfigEntryState,
|
||||
) -> None:
|
||||
"""Test config entry not ready."""
|
||||
|
||||
smtp.login.side_effect = exception
|
||||
aiosmtplib.__aenter__.side_effect = exception
|
||||
|
||||
config_entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
@@ -80,7 +79,7 @@ async def test_config_entry_not_ready(
|
||||
assert config_entry.state is state
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("smtp")
|
||||
@pytest.mark.usefixtures("smtp", "aiosmtplib")
|
||||
async def test_import(
|
||||
hass: HomeAssistant,
|
||||
mock_setup_entry: AsyncMock,
|
||||
@@ -203,14 +202,15 @@ async def test_import_already_configured(
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("smtp")
|
||||
async def test_import_errors(
|
||||
hass: HomeAssistant,
|
||||
mock_setup_entry: AsyncMock,
|
||||
issue_registry: ir.IssueRegistry,
|
||||
smtp: MagicMock,
|
||||
aiosmtplib: AsyncMock,
|
||||
) -> None:
|
||||
"""Test yaml triggers import flow, aborts with errors, and creates error issue."""
|
||||
smtp.login.side_effect = ValueError
|
||||
aiosmtplib.__aenter__.side_effect = ValueError
|
||||
|
||||
await async_setup_component(
|
||||
hass,
|
||||
|
||||
@@ -2,16 +2,10 @@
|
||||
|
||||
from pathlib import Path
|
||||
import re
|
||||
from smtplib import (
|
||||
SMTPAuthenticationError,
|
||||
SMTPException,
|
||||
SMTPHeloError,
|
||||
SMTPSenderRefused,
|
||||
SMTPServerDisconnected,
|
||||
)
|
||||
from socket import gaierror
|
||||
from unittest.mock import MagicMock, patch
|
||||
from smtplib import SMTPException, SMTPServerDisconnected
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import aiosmtplib
|
||||
import pytest
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
|
||||
@@ -211,7 +205,7 @@ def test_send_target_message(target, hass: HomeAssistant, message) -> None:
|
||||
assert recipient == expected_recipient
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("smtp")
|
||||
@pytest.mark.usefixtures("smtp", "aiosmtplib")
|
||||
async def test_notify_platform(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
@@ -229,12 +223,12 @@ async def test_notify_platform(
|
||||
await snapshot_platform(hass, entity_registry, snapshot, config_entry.entry_id)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("make_msgid")
|
||||
@pytest.mark.usefixtures("make_msgid", "smtp")
|
||||
@pytest.mark.freeze_time("2026-05-03T03:09:37+00:00")
|
||||
async def test_notify_send_message(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
smtp: MagicMock,
|
||||
aiosmtplib: AsyncMock,
|
||||
snapshot: SnapshotAssertion,
|
||||
) -> None:
|
||||
"""Test sending an email message via notify.send_message action."""
|
||||
@@ -263,30 +257,26 @@ async def test_notify_send_message(
|
||||
assert state
|
||||
assert state.state == "2026-05-03T03:09:37+00:00"
|
||||
|
||||
assert smtp.sendmail.call_args[0][0] == "email@example.com"
|
||||
assert smtp.sendmail.call_args[0][1] == "recipient@example.com"
|
||||
assert smtp.sendmail.call_args[0][2] == snapshot
|
||||
msg = aiosmtplib.__aenter__.return_value.send_message.call_args[0][0]
|
||||
assert msg.as_string() == snapshot
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("call_method", "exception", "translation_key"),
|
||||
("exception", "translation_key", "call_count"),
|
||||
[
|
||||
("login", SMTPAuthenticationError(0, ""), "authentication_error"),
|
||||
("login", gaierror, "send_mail_connection_error"),
|
||||
("login", ConnectionRefusedError, "send_mail_connection_error"),
|
||||
("login", SMTPHeloError(0, ""), "send_mail_connection_error"),
|
||||
("sendmail", SMTPServerDisconnected, "send_mail_connection_error"),
|
||||
("sendmail", SMTPSenderRefused(0, b"", ""), "send_mail_connection_error"),
|
||||
(aiosmtplib.SMTPAuthenticationError(0, ""), "authentication_error", 1),
|
||||
(aiosmtplib.SMTPException(""), "send_mail_connection_error", 2),
|
||||
],
|
||||
)
|
||||
@pytest.mark.usefixtures("make_msgid", "smtp")
|
||||
@pytest.mark.freeze_time("2026-05-03T03:09:37+00:00")
|
||||
async def test_notify_send_message_exceptions(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
smtp: MagicMock,
|
||||
call_method: str,
|
||||
aiosmtplib: AsyncMock,
|
||||
exception: Exception,
|
||||
translation_key: str,
|
||||
call_count: int,
|
||||
) -> None:
|
||||
"""Test exceptions via notify.send_message action."""
|
||||
|
||||
@@ -296,7 +286,7 @@ async def test_notify_send_message_exceptions(
|
||||
|
||||
assert config_entry.state is ConfigEntryState.LOADED
|
||||
|
||||
getattr(smtp, call_method).side_effect = exception
|
||||
aiosmtplib.__aenter__.return_value.send_message.side_effect = exception
|
||||
|
||||
with pytest.raises(HomeAssistantError) as e:
|
||||
await hass.services.async_call(
|
||||
@@ -310,40 +300,11 @@ async def test_notify_send_message_exceptions(
|
||||
)
|
||||
|
||||
assert e.value.translation_key == translation_key
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("make_msgid")
|
||||
@pytest.mark.freeze_time("2026-05-03T03:09:37+00:00")
|
||||
async def test_notify_retry_on_disconnect_with_broken_quit(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
smtp: MagicMock,
|
||||
) -> None:
|
||||
"""Test retry succeeds when quit() raises on a dead connection."""
|
||||
|
||||
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
|
||||
|
||||
smtp.sendmail.side_effect = [SMTPServerDisconnected("gone"), None]
|
||||
smtp.quit.side_effect = SMTPServerDisconnected("please run connect() first")
|
||||
|
||||
await hass.services.async_call(
|
||||
NOTIFY_DOMAIN,
|
||||
SERVICE_SEND_MESSAGE,
|
||||
{
|
||||
ATTR_ENTITY_ID: "notify.home_assistant_recipient",
|
||||
ATTR_MESSAGE: "Hello World",
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
assert smtp.sendmail.call_count == 2
|
||||
assert aiosmtplib.__aenter__.return_value.send_message.call_count == call_count
|
||||
|
||||
|
||||
@pytest.mark.parametrize("exception", [SMTPServerDisconnected, SMTPException])
|
||||
@pytest.mark.usefixtures("aiosmtplib")
|
||||
async def test_legacy_notify_exception(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
@@ -375,12 +336,12 @@ async def test_legacy_notify_exception(
|
||||
assert smtp.sendmail.call_count == 2
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("make_msgid", "randrange")
|
||||
@pytest.mark.usefixtures("make_msgid", "smtp", "randrange")
|
||||
@pytest.mark.freeze_time("2026-05-03T03:09:37+00:00")
|
||||
async def test_smtp_send_message(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
smtp: MagicMock,
|
||||
aiosmtplib: AsyncMock,
|
||||
snapshot: SnapshotAssertion,
|
||||
) -> None:
|
||||
"""Test sending an email message via smtp.send_message action."""
|
||||
@@ -411,17 +372,16 @@ async def test_smtp_send_message(
|
||||
assert state
|
||||
assert state.state == "2026-05-03T03:09:37+00:00"
|
||||
|
||||
assert smtp.sendmail.call_args[0][0] == "email@example.com"
|
||||
assert smtp.sendmail.call_args[0][1] == "recipient@example.com"
|
||||
assert smtp.sendmail.call_args[0][2] == snapshot
|
||||
msg = aiosmtplib.__aenter__.return_value.send_message.call_args[0][0]
|
||||
assert msg.as_string() == snapshot
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("make_msgid", "randrange")
|
||||
@pytest.mark.usefixtures("make_msgid", "smtp", "randrange")
|
||||
@pytest.mark.freeze_time("2026-05-03T03:09:37+00:00")
|
||||
async def test_smtp_send_message_local_media_source(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
smtp: MagicMock,
|
||||
aiosmtplib: AsyncMock,
|
||||
snapshot: SnapshotAssertion,
|
||||
) -> None:
|
||||
"""Test sending an email message via smtp.send_message action with attachment from local media source."""
|
||||
@@ -453,17 +413,16 @@ async def test_smtp_send_message_local_media_source(
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
assert smtp.sendmail.call_args[0][0] == "email@example.com"
|
||||
assert smtp.sendmail.call_args[0][1] == "recipient@example.com"
|
||||
assert smtp.sendmail.call_args[0][2] == snapshot
|
||||
msg = aiosmtplib.__aenter__.return_value.send_message.call_args[0][0]
|
||||
assert msg.as_string() == snapshot
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("make_msgid", "randrange")
|
||||
@pytest.mark.usefixtures("make_msgid", "smtp", "randrange")
|
||||
@pytest.mark.freeze_time("2026-05-03T03:09:37+00:00")
|
||||
async def test_smtp_send_message_camera_source(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
smtp: MagicMock,
|
||||
aiosmtplib: AsyncMock,
|
||||
snapshot: SnapshotAssertion,
|
||||
) -> None:
|
||||
"""Test sending an email message via smtp.send_message action with attachment from camera source snapshot."""
|
||||
@@ -500,17 +459,16 @@ async def test_smtp_send_message_camera_source(
|
||||
blocking=True,
|
||||
)
|
||||
mock_get_image.assert_called_once_with(hass, "camera.demo_camera")
|
||||
assert smtp.sendmail.call_args[0][0] == "email@example.com"
|
||||
assert smtp.sendmail.call_args[0][1] == "recipient@example.com"
|
||||
assert smtp.sendmail.call_args[0][2] == snapshot
|
||||
msg = aiosmtplib.__aenter__.return_value.send_message.call_args[0][0]
|
||||
assert msg.as_string() == snapshot
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("make_msgid", "randrange")
|
||||
@pytest.mark.usefixtures("make_msgid", "smtp", "randrange")
|
||||
@pytest.mark.freeze_time("2026-05-03T03:09:37+00:00")
|
||||
async def test_smtp_send_message_image_source(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
smtp: MagicMock,
|
||||
aiosmtplib: AsyncMock,
|
||||
snapshot: SnapshotAssertion,
|
||||
) -> None:
|
||||
"""Test sending an email message via smtp.send_message action with attachment from image source."""
|
||||
@@ -552,17 +510,16 @@ async def test_smtp_send_message_image_source(
|
||||
blocking=True,
|
||||
)
|
||||
mock_get_image.assert_called_with(hass, "image.test")
|
||||
assert smtp.sendmail.call_args[0][0] == "email@example.com"
|
||||
assert smtp.sendmail.call_args[0][1] == "recipient@example.com"
|
||||
assert smtp.sendmail.call_args[0][2] == snapshot
|
||||
msg = aiosmtplib.__aenter__.return_value.send_message.call_args[0][0]
|
||||
assert msg.as_string() == snapshot
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("make_msgid", "randrange")
|
||||
@pytest.mark.usefixtures("make_msgid", "smtp", "randrange")
|
||||
@pytest.mark.freeze_time("2026-05-03T03:09:37+00:00")
|
||||
async def test_smtp_send_message_tts_source(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
smtp: MagicMock,
|
||||
aiosmtplib: AsyncMock,
|
||||
snapshot: SnapshotAssertion,
|
||||
) -> None:
|
||||
"""Test sending an email message via smtp.send_message action with audio attachment from tts source."""
|
||||
@@ -575,7 +532,7 @@ async def test_smtp_send_message_tts_source(
|
||||
with patch(
|
||||
"homeassistant.components.tts.async_get_media_source_audio",
|
||||
return_value=("mp3", b"Hello World!"),
|
||||
):
|
||||
) as mock_get_media_source_audio:
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
SERVICE_SEND_MESSAGE,
|
||||
@@ -596,12 +553,14 @@ async def test_smtp_send_message_tts_source(
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
assert smtp.sendmail.call_args[0][0] == "email@example.com"
|
||||
assert smtp.sendmail.call_args[0][1] == "recipient@example.com"
|
||||
assert smtp.sendmail.call_args[0][2] == snapshot
|
||||
mock_get_media_source_audio.assert_called_with(
|
||||
hass, "media-source://tts/demo?message=Hello+World%21&language=en"
|
||||
)
|
||||
msg = aiosmtplib.__aenter__.return_value.send_message.call_args[0][0]
|
||||
assert msg.as_string() == snapshot
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("smtp")
|
||||
@pytest.mark.usefixtures("smtp", "aiosmtplib")
|
||||
@pytest.mark.freeze_time("2026-05-03T03:09:37+00:00")
|
||||
async def test_smtp_send_message_media_source_not_supported(
|
||||
hass: HomeAssistant,
|
||||
@@ -650,7 +609,7 @@ async def test_smtp_send_message_media_source_not_supported(
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("smtp")
|
||||
@pytest.mark.usefixtures("smtp", "aiosmtplib")
|
||||
@pytest.mark.freeze_time("2026-05-03T03:09:37+00:00")
|
||||
async def test_smtp_send_message_media_source_missing_filename(
|
||||
hass: HomeAssistant,
|
||||
@@ -696,7 +655,7 @@ async def test_smtp_send_message_media_source_missing_filename(
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("smtp")
|
||||
@pytest.mark.usefixtures("smtp", "aiosmtplib")
|
||||
async def test_deprecated_legacy_notify_action(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
|
||||
Reference in New Issue
Block a user