Add strict type checking to SMTP integration (#143698)

This commit is contained in:
Michael
2025-04-29 12:56:29 +02:00
committed by GitHub
parent 7493b340ca
commit 493ca261dc
3 changed files with 52 additions and 25 deletions

View File

@ -463,6 +463,7 @@ homeassistant.components.slack.*
homeassistant.components.sleepiq.*
homeassistant.components.smhi.*
homeassistant.components.smlight.*
homeassistant.components.smtp.*
homeassistant.components.snooz.*
homeassistant.components.solarlog.*
homeassistant.components.sonarr.*

View File

@ -11,6 +11,9 @@ import logging
import os
from pathlib import Path
import smtplib
import socket
import ssl
from typing import Any
import voluptuous as vol
@ -113,19 +116,19 @@ class MailNotificationService(BaseNotificationService):
def __init__(
self,
server,
port,
timeout,
sender,
encryption,
username,
password,
recipients,
sender_name,
debug,
verify_ssl,
ssl_context,
):
server: str,
port: int,
timeout: int,
sender: str,
encryption: str,
username: str | None,
password: str | None,
recipients: list[str],
sender_name: str | None,
debug: bool,
verify_ssl: bool,
ssl_context: ssl.SSLContext | None,
) -> None:
"""Initialize the SMTP service."""
self._server = server
self._port = port
@ -141,8 +144,9 @@ class MailNotificationService(BaseNotificationService):
self.tries = 2
self._ssl_context = ssl_context
def connect(self):
def connect(self) -> smtplib.SMTP_SSL | smtplib.SMTP:
"""Connect/authenticate to SMTP Server."""
mail: smtplib.SMTP_SSL | smtplib.SMTP
if self.encryption == "tls":
mail = smtplib.SMTP_SSL(
self._server,
@ -161,12 +165,12 @@ class MailNotificationService(BaseNotificationService):
mail.login(self.username, self.password)
return mail
def connection_is_valid(self):
def connection_is_valid(self) -> bool:
"""Check for valid config, verify connectivity."""
server = None
try:
server = self.connect()
except (smtplib.socket.gaierror, ConnectionRefusedError):
except (socket.gaierror, ConnectionRefusedError):
_LOGGER.exception(
(
"SMTP server not found or refused connection (%s:%s). Please check"
@ -188,7 +192,7 @@ class MailNotificationService(BaseNotificationService):
return True
def send_message(self, message="", **kwargs):
def send_message(self, message: str, **kwargs: Any) -> None:
"""Build and send a message to a user.
Will send plain text normally, with pictures as attachments if images config is
@ -196,6 +200,7 @@ class MailNotificationService(BaseNotificationService):
"""
subject = kwargs.get(ATTR_TITLE, ATTR_TITLE_DEFAULT)
msg: MIMEMultipart | MIMEText
if data := kwargs.get(ATTR_DATA):
if ATTR_HTML in data:
msg = _build_html_msg(
@ -213,20 +218,24 @@ class MailNotificationService(BaseNotificationService):
msg["Subject"] = subject
if not (recipients := kwargs.get(ATTR_TARGET)):
if targets := kwargs.get(ATTR_TARGET):
recipients: list[str] = targets # ensured by NOTIFY_SERVICE_SCHEMA
else:
recipients = self.recipients
msg["To"] = recipients if isinstance(recipients, str) else ",".join(recipients)
msg["To"] = ",".join(recipients)
if self._sender_name:
msg["From"] = f"{self._sender_name} <{self._sender}>"
else:
msg["From"] = self._sender
msg["X-Mailer"] = "Home Assistant"
msg["Date"] = email.utils.format_datetime(dt_util.now())
msg["Message-Id"] = email.utils.make_msgid()
return self._send_email(msg, recipients)
def _send_email(self, msg, recipients):
def _send_email(self, msg: MIMEMultipart | MIMEText, recipients: list[str]) -> None:
"""Send the message."""
mail = self.connect()
for _ in range(self.tries):
@ -246,13 +255,15 @@ class MailNotificationService(BaseNotificationService):
mail.quit()
def _build_text_msg(message):
def _build_text_msg(message: str) -> MIMEText:
"""Build plaintext email."""
_LOGGER.debug("Building plain text email")
return MIMEText(message)
def _attach_file(hass, atch_name, content_id=""):
def _attach_file(
hass: HomeAssistant, atch_name: str, content_id: str | None = None
) -> MIMEImage | MIMEApplication | None:
"""Create a message attachment.
If MIMEImage is successful and content_id is passed (HTML), add images in-line.
@ -271,7 +282,7 @@ def _attach_file(hass, atch_name, content_id=""):
translation_key="remote_path_not_allowed",
translation_placeholders={
"allow_list": allow_list,
"file_path": file_path,
"file_path": str(file_path),
"file_name": file_name,
"url": url,
},
@ -282,6 +293,7 @@ def _attach_file(hass, atch_name, content_id=""):
_LOGGER.warning("Attachment %s not found. Skipping", atch_name)
return None
attachment: MIMEImage | MIMEApplication
try:
attachment = MIMEImage(file_bytes)
except TypeError:
@ -305,7 +317,9 @@ def _attach_file(hass, atch_name, content_id=""):
return attachment
def _build_multipart_msg(hass, message, images):
def _build_multipart_msg(
hass: HomeAssistant, message: str, images: list[str]
) -> MIMEMultipart:
"""Build Multipart message with images as attachments."""
_LOGGER.debug("Building multipart email with image attachme_build_html_msgnt(s)")
msg = MIMEMultipart()
@ -320,7 +334,9 @@ def _build_multipart_msg(hass, message, images):
return msg
def _build_html_msg(hass, text, html, images):
def _build_html_msg(
hass: HomeAssistant, text: str, html: str, images: list[str]
) -> MIMEMultipart:
"""Build Multipart message with in-line images and rich HTML (UTF-8)."""
_LOGGER.debug("Building HTML rich email")
msg = MIMEMultipart("related")

10
mypy.ini generated
View File

@ -4386,6 +4386,16 @@ disallow_untyped_defs = true
warn_return_any = true
warn_unreachable = true
[mypy-homeassistant.components.smtp.*]
check_untyped_defs = true
disallow_incomplete_defs = true
disallow_subclassing_any = true
disallow_untyped_calls = true
disallow_untyped_decorators = true
disallow_untyped_defs = true
warn_return_any = true
warn_unreachable = true
[mypy-homeassistant.components.snooz.*]
check_untyped_defs = true
disallow_incomplete_defs = true