diff --git a/gateway/platforms/email.py b/gateway/platforms/email.py index c1c27c939..f3e153c4e 100644 --- a/gateway/platforms/email.py +++ b/gateway/platforms/email.py @@ -224,7 +224,7 @@ class EmailAdapter(BasePlatformAdapter): """Connect to the IMAP server and start polling for new messages.""" try: # Test IMAP connection - imap = imaplib.IMAP4_SSL(self._imap_host, self._imap_port) + imap = imaplib.IMAP4_SSL(self._imap_host, self._imap_port, timeout=30) imap.login(self._address, self._password) # Mark all existing messages as seen so we only process new ones imap.select("INBOX") @@ -240,7 +240,7 @@ class EmailAdapter(BasePlatformAdapter): try: # Test SMTP connection - smtp = smtplib.SMTP(self._smtp_host, self._smtp_port) + smtp = smtplib.SMTP(self._smtp_host, self._smtp_port, timeout=30) smtp.starttls(context=ssl.create_default_context()) smtp.login(self._address, self._password) smtp.quit() @@ -289,7 +289,7 @@ class EmailAdapter(BasePlatformAdapter): """Fetch new (unseen) messages from IMAP. Runs in executor thread.""" results = [] try: - imap = imaplib.IMAP4_SSL(self._imap_host, self._imap_port) + imap = imaplib.IMAP4_SSL(self._imap_host, self._imap_port, timeout=30) imap.login(self._address, self._password) imap.select("INBOX") @@ -442,7 +442,7 @@ class EmailAdapter(BasePlatformAdapter): msg.attach(MIMEText(body, "plain", "utf-8")) - smtp = smtplib.SMTP(self._smtp_host, self._smtp_port) + smtp = smtplib.SMTP(self._smtp_host, self._smtp_port, timeout=30) smtp.starttls(context=ssl.create_default_context()) smtp.login(self._address, self._password) smtp.send_message(msg) @@ -529,7 +529,7 @@ class EmailAdapter(BasePlatformAdapter): part.add_header("Content-Disposition", f"attachment; filename={fname}") msg.attach(part) - smtp = smtplib.SMTP(self._smtp_host, self._smtp_port) + smtp = smtplib.SMTP(self._smtp_host, self._smtp_port, timeout=30) smtp.starttls(context=ssl.create_default_context()) smtp.login(self._address, self._password) smtp.send_message(msg) diff --git a/gateway/platforms/homeassistant.py b/gateway/platforms/homeassistant.py index 7a40e1bde..746465594 100644 --- a/gateway/platforms/homeassistant.py +++ b/gateway/platforms/homeassistant.py @@ -114,7 +114,9 @@ class HomeAssistantAdapter(BasePlatformAdapter): return False # Dedicated REST session for send() calls - self._rest_session = aiohttp.ClientSession() + self._rest_session = aiohttp.ClientSession( + timeout=aiohttp.ClientTimeout(total=30) + ) # Warn if no event filters are configured if not self._watch_domains and not self._watch_entities and not self._watch_all: @@ -140,8 +142,10 @@ class HomeAssistantAdapter(BasePlatformAdapter): ws_url = self._hass_url.replace("http://", "ws://").replace("https://", "wss://") ws_url = f"{ws_url}/api/websocket" - self._session = aiohttp.ClientSession() - self._ws = await self._session.ws_connect(ws_url, heartbeat=30) + self._session = aiohttp.ClientSession( + timeout=aiohttp.ClientTimeout(total=30) + ) + self._ws = await self._session.ws_connect(ws_url, heartbeat=30, timeout=30) # Step 1: Receive auth_required msg = await self._ws.receive_json() diff --git a/gateway/platforms/mattermost.py b/gateway/platforms/mattermost.py index 8ef92f212..0f66577ff 100644 --- a/gateway/platforms/mattermost.py +++ b/gateway/platforms/mattermost.py @@ -116,7 +116,7 @@ class MattermostAdapter(BasePlatformAdapter): import aiohttp url = f"{self._base_url}/api/v4/{path.lstrip('/')}" try: - async with self._session.get(url, headers=self._headers()) as resp: + async with self._session.get(url, headers=self._headers(), timeout=aiohttp.ClientTimeout(total=30)) as resp: if resp.status >= 400: body = await resp.text() logger.error("MM API GET %s → %s: %s", path, resp.status, body[:200]) @@ -134,7 +134,8 @@ class MattermostAdapter(BasePlatformAdapter): url = f"{self._base_url}/api/v4/{path.lstrip('/')}" try: async with self._session.post( - url, headers=self._headers(), json=payload + url, headers=self._headers(), json=payload, + timeout=aiohttp.ClientTimeout(total=30) ) as resp: if resp.status >= 400: body = await resp.text() @@ -180,7 +181,7 @@ class MattermostAdapter(BasePlatformAdapter): content_type=content_type, ) headers = {"Authorization": f"Bearer {self._token}"} - async with self._session.post(url, headers=headers, data=form) as resp: + async with self._session.post(url, headers=headers, data=form, timeout=aiohttp.ClientTimeout(total=60)) as resp: if resp.status >= 400: body = await resp.text() logger.error("MM file upload → %s: %s", resp.status, body[:200]) @@ -201,7 +202,9 @@ class MattermostAdapter(BasePlatformAdapter): logger.error("Mattermost: URL or token not configured") return False - self._session = aiohttp.ClientSession() + self._session = aiohttp.ClientSession( + timeout=aiohttp.ClientTimeout(total=30) + ) self._closing = False # Verify credentials and fetch bot identity. diff --git a/gateway/platforms/sms.py b/gateway/platforms/sms.py index 2cf8fb080..750821a4c 100644 --- a/gateway/platforms/sms.py +++ b/gateway/platforms/sms.py @@ -106,7 +106,9 @@ class SmsAdapter(BasePlatformAdapter): await self._runner.setup() site = web.TCPSite(self._runner, "0.0.0.0", self._webhook_port) await site.start() - self._http_session = aiohttp.ClientSession() + self._http_session = aiohttp.ClientSession( + timeout=aiohttp.ClientTimeout(total=30), + ) self._running = True logger.info( @@ -144,7 +146,9 @@ class SmsAdapter(BasePlatformAdapter): "Authorization": self._basic_auth_header(), } - session = self._http_session or aiohttp.ClientSession() + session = self._http_session or aiohttp.ClientSession( + timeout=aiohttp.ClientTimeout(total=30), + ) try: for chunk in chunks: form_data = aiohttp.FormData()