2019-04-03 15:40:03 +00:00
|
|
|
"""IMAP sensor support."""
|
2017-09-26 07:26:26 +00:00
|
|
|
import asyncio
|
2018-01-21 06:35:38 +00:00
|
|
|
import logging
|
2016-08-20 22:40:16 +00:00
|
|
|
|
2019-10-17 13:01:50 +00:00
|
|
|
from aioimaplib import IMAP4_SSL, AioImapException
|
2018-01-21 06:35:38 +00:00
|
|
|
import async_timeout
|
2016-07-10 20:21:53 +00:00
|
|
|
import voluptuous as vol
|
|
|
|
|
2016-08-20 22:40:16 +00:00
|
|
|
from homeassistant.components.sensor import PLATFORM_SCHEMA
|
|
|
|
from homeassistant.const import (
|
2019-07-31 19:25:30 +00:00
|
|
|
CONF_NAME,
|
|
|
|
CONF_PASSWORD,
|
|
|
|
CONF_PORT,
|
|
|
|
CONF_USERNAME,
|
|
|
|
EVENT_HOMEASSISTANT_STOP,
|
|
|
|
)
|
2017-09-26 07:26:26 +00:00
|
|
|
from homeassistant.exceptions import PlatformNotReady
|
2016-07-10 20:21:53 +00:00
|
|
|
import homeassistant.helpers.config_validation as cv
|
2018-01-21 06:35:38 +00:00
|
|
|
from homeassistant.helpers.entity import Entity
|
2016-07-10 20:21:53 +00:00
|
|
|
|
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
CONF_SERVER = "server"
|
|
|
|
CONF_FOLDER = "folder"
|
|
|
|
CONF_SEARCH = "search"
|
2019-10-27 12:07:44 +00:00
|
|
|
CONF_CHARSET = "charset"
|
2016-07-10 20:21:53 +00:00
|
|
|
|
|
|
|
DEFAULT_PORT = 993
|
2017-09-26 07:26:26 +00:00
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
ICON = "mdi:email-outline"
|
2016-07-10 20:21:53 +00:00
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
|
|
|
{
|
|
|
|
vol.Optional(CONF_NAME): cv.string,
|
|
|
|
vol.Required(CONF_USERNAME): cv.string,
|
|
|
|
vol.Required(CONF_PASSWORD): cv.string,
|
|
|
|
vol.Required(CONF_SERVER): cv.string,
|
|
|
|
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
|
2019-10-27 12:07:44 +00:00
|
|
|
vol.Optional(CONF_CHARSET, default="utf-8"): cv.string,
|
2019-07-31 19:25:30 +00:00
|
|
|
vol.Optional(CONF_FOLDER, default="INBOX"): cv.string,
|
|
|
|
vol.Optional(CONF_SEARCH, default="UnSeen UnDeleted"): cv.string,
|
|
|
|
}
|
|
|
|
)
|
2016-07-10 20:21:53 +00:00
|
|
|
|
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
|
2018-01-21 06:35:38 +00:00
|
|
|
"""Set up the IMAP platform."""
|
2019-07-31 19:25:30 +00:00
|
|
|
sensor = ImapSensor(
|
|
|
|
config.get(CONF_NAME),
|
|
|
|
config.get(CONF_USERNAME),
|
|
|
|
config.get(CONF_PASSWORD),
|
|
|
|
config.get(CONF_SERVER),
|
|
|
|
config.get(CONF_PORT),
|
2019-10-27 12:07:44 +00:00
|
|
|
config.get(CONF_CHARSET),
|
2019-07-31 19:25:30 +00:00
|
|
|
config.get(CONF_FOLDER),
|
|
|
|
config.get(CONF_SEARCH),
|
|
|
|
)
|
2018-03-08 20:30:50 +00:00
|
|
|
if not await sensor.connection():
|
2017-09-26 07:26:26 +00:00
|
|
|
raise PlatformNotReady
|
2016-07-10 20:21:53 +00:00
|
|
|
|
2020-10-19 21:25:33 +00:00
|
|
|
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, sensor.shutdown)
|
2018-08-24 14:37:30 +00:00
|
|
|
async_add_entities([sensor], True)
|
2016-07-10 20:21:53 +00:00
|
|
|
|
|
|
|
|
|
|
|
class ImapSensor(Entity):
|
2016-07-12 14:46:29 +00:00
|
|
|
"""Representation of an IMAP sensor."""
|
2016-07-10 20:21:53 +00:00
|
|
|
|
2019-10-27 12:07:44 +00:00
|
|
|
def __init__(self, name, user, password, server, port, charset, folder, search):
|
2016-07-10 20:21:53 +00:00
|
|
|
"""Initialize the sensor."""
|
|
|
|
self._name = name or user
|
|
|
|
self._user = user
|
|
|
|
self._password = password
|
|
|
|
self._server = server
|
|
|
|
self._port = port
|
2019-10-27 12:07:44 +00:00
|
|
|
self._charset = charset
|
2017-10-25 09:36:00 +00:00
|
|
|
self._folder = folder
|
Added Search Configuration to IMAP Sensor (#19749)
* Added Search Configuration to IMAP Sensor
The IMAP sensor currently only counts unread emails in a folder. By exposing the IMAP search parameter, the sensor can be used to count other results:
- All emails in an inbox
- Emails sent from an address
- Emails matching a subject
- Other advanced searches, especially with vendor-specific extensions. Gmail in particular supports X-GM-RAW, which lets you use any Gmail search directly ("emails with X label older than 14 days with", etc)
For my use case, I just wanted total emails in a folder, to show an "X/Y" counter for total/unread. I started work on a one-off script to throw the data in, but figured I'd try to extend Home Assistant more directly, especially since this IMAP sensor correctly handles servers that push data. This is my first Home Assistant contribution, so apologies in advance if something is out of place! It's a pretty minimal modification.
* Added Server Response Checking
Looks like no library exception is thrown, so check for response text before parsing out results (previous code just counts spaces, so an error actually returns a state value of 4).
* IMAP Warning -> Error, Count Initializes to None
IMAP search response parsing throws an error instead of a warning.
Email count initializes as None instead 0.
Email count is untouched in case of failure to parse response (i.e. if server is temporarily down or throwing errors, or maybe due to user updating their authentication/login/etc).
Fixed line length on error so it fits under 80 characters.
* Fixed Indent on Logger Error
Sorry about the churn! Python is pretty far from my daily-use language. (I did run this one through pep8, at least)
2019-01-19 20:37:02 +00:00
|
|
|
self._email_count = None
|
|
|
|
self._search = search
|
2017-09-26 07:26:26 +00:00
|
|
|
self._connection = None
|
|
|
|
self._does_push = None
|
|
|
|
self._idle_loop_task = None
|
2016-07-10 20:21:53 +00:00
|
|
|
|
2018-03-08 20:30:50 +00:00
|
|
|
async def async_added_to_hass(self):
|
2017-09-26 07:26:26 +00:00
|
|
|
"""Handle when an entity is about to be added to Home Assistant."""
|
|
|
|
if not self.should_poll:
|
|
|
|
self._idle_loop_task = self.hass.loop.create_task(self.idle_loop())
|
2016-07-10 20:21:53 +00:00
|
|
|
|
|
|
|
@property
|
|
|
|
def name(self):
|
|
|
|
"""Return the name of the sensor."""
|
|
|
|
return self._name
|
|
|
|
|
2017-09-26 07:26:26 +00:00
|
|
|
@property
|
|
|
|
def icon(self):
|
|
|
|
"""Return the icon to use in the frontend."""
|
|
|
|
return ICON
|
|
|
|
|
2016-07-10 20:21:53 +00:00
|
|
|
@property
|
|
|
|
def state(self):
|
Added Search Configuration to IMAP Sensor (#19749)
* Added Search Configuration to IMAP Sensor
The IMAP sensor currently only counts unread emails in a folder. By exposing the IMAP search parameter, the sensor can be used to count other results:
- All emails in an inbox
- Emails sent from an address
- Emails matching a subject
- Other advanced searches, especially with vendor-specific extensions. Gmail in particular supports X-GM-RAW, which lets you use any Gmail search directly ("emails with X label older than 14 days with", etc)
For my use case, I just wanted total emails in a folder, to show an "X/Y" counter for total/unread. I started work on a one-off script to throw the data in, but figured I'd try to extend Home Assistant more directly, especially since this IMAP sensor correctly handles servers that push data. This is my first Home Assistant contribution, so apologies in advance if something is out of place! It's a pretty minimal modification.
* Added Server Response Checking
Looks like no library exception is thrown, so check for response text before parsing out results (previous code just counts spaces, so an error actually returns a state value of 4).
* IMAP Warning -> Error, Count Initializes to None
IMAP search response parsing throws an error instead of a warning.
Email count initializes as None instead 0.
Email count is untouched in case of failure to parse response (i.e. if server is temporarily down or throwing errors, or maybe due to user updating their authentication/login/etc).
Fixed line length on error so it fits under 80 characters.
* Fixed Indent on Logger Error
Sorry about the churn! Python is pretty far from my daily-use language. (I did run this one through pep8, at least)
2019-01-19 20:37:02 +00:00
|
|
|
"""Return the number of emails found."""
|
|
|
|
return self._email_count
|
2016-07-10 20:21:53 +00:00
|
|
|
|
2017-09-26 07:26:26 +00:00
|
|
|
@property
|
|
|
|
def available(self):
|
|
|
|
"""Return the availability of the device."""
|
|
|
|
return self._connection is not None
|
2016-07-10 20:21:53 +00:00
|
|
|
|
|
|
|
@property
|
2017-09-26 07:26:26 +00:00
|
|
|
def should_poll(self):
|
|
|
|
"""Return if polling is needed."""
|
|
|
|
return not self._does_push
|
|
|
|
|
2018-03-08 20:30:50 +00:00
|
|
|
async def connection(self):
|
2017-09-26 07:26:26 +00:00
|
|
|
"""Return a connection to the server, establishing it if necessary."""
|
|
|
|
if self._connection is None:
|
|
|
|
try:
|
2019-10-17 13:01:50 +00:00
|
|
|
self._connection = IMAP4_SSL(self._server, self._port)
|
2018-03-08 20:30:50 +00:00
|
|
|
await self._connection.wait_hello_from_server()
|
|
|
|
await self._connection.login(self._user, self._password)
|
|
|
|
await self._connection.select(self._folder)
|
2019-07-31 19:25:30 +00:00
|
|
|
self._does_push = self._connection.has_capability("IDLE")
|
2019-10-17 13:01:50 +00:00
|
|
|
except (AioImapException, asyncio.TimeoutError):
|
2017-09-26 07:26:26 +00:00
|
|
|
self._connection = None
|
|
|
|
|
|
|
|
return self._connection
|
|
|
|
|
2018-03-08 20:30:50 +00:00
|
|
|
async def idle_loop(self):
|
2017-09-26 07:26:26 +00:00
|
|
|
"""Wait for data pushed from server."""
|
|
|
|
while True:
|
|
|
|
try:
|
2018-03-08 20:30:50 +00:00
|
|
|
if await self.connection():
|
Added Search Configuration to IMAP Sensor (#19749)
* Added Search Configuration to IMAP Sensor
The IMAP sensor currently only counts unread emails in a folder. By exposing the IMAP search parameter, the sensor can be used to count other results:
- All emails in an inbox
- Emails sent from an address
- Emails matching a subject
- Other advanced searches, especially with vendor-specific extensions. Gmail in particular supports X-GM-RAW, which lets you use any Gmail search directly ("emails with X label older than 14 days with", etc)
For my use case, I just wanted total emails in a folder, to show an "X/Y" counter for total/unread. I started work on a one-off script to throw the data in, but figured I'd try to extend Home Assistant more directly, especially since this IMAP sensor correctly handles servers that push data. This is my first Home Assistant contribution, so apologies in advance if something is out of place! It's a pretty minimal modification.
* Added Server Response Checking
Looks like no library exception is thrown, so check for response text before parsing out results (previous code just counts spaces, so an error actually returns a state value of 4).
* IMAP Warning -> Error, Count Initializes to None
IMAP search response parsing throws an error instead of a warning.
Email count initializes as None instead 0.
Email count is untouched in case of failure to parse response (i.e. if server is temporarily down or throwing errors, or maybe due to user updating their authentication/login/etc).
Fixed line length on error so it fits under 80 characters.
* Fixed Indent on Logger Error
Sorry about the churn! Python is pretty far from my daily-use language. (I did run this one through pep8, at least)
2019-01-19 20:37:02 +00:00
|
|
|
await self.refresh_email_count()
|
2020-04-03 07:34:50 +00:00
|
|
|
self.async_write_ha_state()
|
2017-09-26 07:26:26 +00:00
|
|
|
|
2018-03-08 20:30:50 +00:00
|
|
|
idle = await self._connection.idle_start()
|
|
|
|
await self._connection.wait_server_push()
|
2017-09-26 07:26:26 +00:00
|
|
|
self._connection.idle_done()
|
|
|
|
with async_timeout.timeout(10):
|
2018-03-08 20:30:50 +00:00
|
|
|
await idle
|
2017-09-26 07:26:26 +00:00
|
|
|
else:
|
2020-04-03 07:34:50 +00:00
|
|
|
self.async_write_ha_state()
|
2019-10-17 13:01:50 +00:00
|
|
|
except (AioImapException, asyncio.TimeoutError):
|
2017-09-26 07:26:26 +00:00
|
|
|
self.disconnected()
|
|
|
|
|
2018-03-08 20:30:50 +00:00
|
|
|
async def async_update(self):
|
2017-09-26 07:26:26 +00:00
|
|
|
"""Periodic polling of state."""
|
|
|
|
try:
|
2018-03-08 20:30:50 +00:00
|
|
|
if await self.connection():
|
Added Search Configuration to IMAP Sensor (#19749)
* Added Search Configuration to IMAP Sensor
The IMAP sensor currently only counts unread emails in a folder. By exposing the IMAP search parameter, the sensor can be used to count other results:
- All emails in an inbox
- Emails sent from an address
- Emails matching a subject
- Other advanced searches, especially with vendor-specific extensions. Gmail in particular supports X-GM-RAW, which lets you use any Gmail search directly ("emails with X label older than 14 days with", etc)
For my use case, I just wanted total emails in a folder, to show an "X/Y" counter for total/unread. I started work on a one-off script to throw the data in, but figured I'd try to extend Home Assistant more directly, especially since this IMAP sensor correctly handles servers that push data. This is my first Home Assistant contribution, so apologies in advance if something is out of place! It's a pretty minimal modification.
* Added Server Response Checking
Looks like no library exception is thrown, so check for response text before parsing out results (previous code just counts spaces, so an error actually returns a state value of 4).
* IMAP Warning -> Error, Count Initializes to None
IMAP search response parsing throws an error instead of a warning.
Email count initializes as None instead 0.
Email count is untouched in case of failure to parse response (i.e. if server is temporarily down or throwing errors, or maybe due to user updating their authentication/login/etc).
Fixed line length on error so it fits under 80 characters.
* Fixed Indent on Logger Error
Sorry about the churn! Python is pretty far from my daily-use language. (I did run this one through pep8, at least)
2019-01-19 20:37:02 +00:00
|
|
|
await self.refresh_email_count()
|
2019-10-17 13:01:50 +00:00
|
|
|
except (AioImapException, asyncio.TimeoutError):
|
2017-09-26 07:26:26 +00:00
|
|
|
self.disconnected()
|
|
|
|
|
Added Search Configuration to IMAP Sensor (#19749)
* Added Search Configuration to IMAP Sensor
The IMAP sensor currently only counts unread emails in a folder. By exposing the IMAP search parameter, the sensor can be used to count other results:
- All emails in an inbox
- Emails sent from an address
- Emails matching a subject
- Other advanced searches, especially with vendor-specific extensions. Gmail in particular supports X-GM-RAW, which lets you use any Gmail search directly ("emails with X label older than 14 days with", etc)
For my use case, I just wanted total emails in a folder, to show an "X/Y" counter for total/unread. I started work on a one-off script to throw the data in, but figured I'd try to extend Home Assistant more directly, especially since this IMAP sensor correctly handles servers that push data. This is my first Home Assistant contribution, so apologies in advance if something is out of place! It's a pretty minimal modification.
* Added Server Response Checking
Looks like no library exception is thrown, so check for response text before parsing out results (previous code just counts spaces, so an error actually returns a state value of 4).
* IMAP Warning -> Error, Count Initializes to None
IMAP search response parsing throws an error instead of a warning.
Email count initializes as None instead 0.
Email count is untouched in case of failure to parse response (i.e. if server is temporarily down or throwing errors, or maybe due to user updating their authentication/login/etc).
Fixed line length on error so it fits under 80 characters.
* Fixed Indent on Logger Error
Sorry about the churn! Python is pretty far from my daily-use language. (I did run this one through pep8, at least)
2019-01-19 20:37:02 +00:00
|
|
|
async def refresh_email_count(self):
|
|
|
|
"""Check the number of found emails."""
|
2017-09-26 07:26:26 +00:00
|
|
|
if self._connection:
|
2018-03-08 20:30:50 +00:00
|
|
|
await self._connection.noop()
|
2019-10-27 12:07:44 +00:00
|
|
|
result, lines = await self._connection.search(
|
|
|
|
self._search, charset=self._charset
|
|
|
|
)
|
Added Search Configuration to IMAP Sensor (#19749)
* Added Search Configuration to IMAP Sensor
The IMAP sensor currently only counts unread emails in a folder. By exposing the IMAP search parameter, the sensor can be used to count other results:
- All emails in an inbox
- Emails sent from an address
- Emails matching a subject
- Other advanced searches, especially with vendor-specific extensions. Gmail in particular supports X-GM-RAW, which lets you use any Gmail search directly ("emails with X label older than 14 days with", etc)
For my use case, I just wanted total emails in a folder, to show an "X/Y" counter for total/unread. I started work on a one-off script to throw the data in, but figured I'd try to extend Home Assistant more directly, especially since this IMAP sensor correctly handles servers that push data. This is my first Home Assistant contribution, so apologies in advance if something is out of place! It's a pretty minimal modification.
* Added Server Response Checking
Looks like no library exception is thrown, so check for response text before parsing out results (previous code just counts spaces, so an error actually returns a state value of 4).
* IMAP Warning -> Error, Count Initializes to None
IMAP search response parsing throws an error instead of a warning.
Email count initializes as None instead 0.
Email count is untouched in case of failure to parse response (i.e. if server is temporarily down or throwing errors, or maybe due to user updating their authentication/login/etc).
Fixed line length on error so it fits under 80 characters.
* Fixed Indent on Logger Error
Sorry about the churn! Python is pretty far from my daily-use language. (I did run this one through pep8, at least)
2019-01-19 20:37:02 +00:00
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
if result == "OK":
|
Added Search Configuration to IMAP Sensor (#19749)
* Added Search Configuration to IMAP Sensor
The IMAP sensor currently only counts unread emails in a folder. By exposing the IMAP search parameter, the sensor can be used to count other results:
- All emails in an inbox
- Emails sent from an address
- Emails matching a subject
- Other advanced searches, especially with vendor-specific extensions. Gmail in particular supports X-GM-RAW, which lets you use any Gmail search directly ("emails with X label older than 14 days with", etc)
For my use case, I just wanted total emails in a folder, to show an "X/Y" counter for total/unread. I started work on a one-off script to throw the data in, but figured I'd try to extend Home Assistant more directly, especially since this IMAP sensor correctly handles servers that push data. This is my first Home Assistant contribution, so apologies in advance if something is out of place! It's a pretty minimal modification.
* Added Server Response Checking
Looks like no library exception is thrown, so check for response text before parsing out results (previous code just counts spaces, so an error actually returns a state value of 4).
* IMAP Warning -> Error, Count Initializes to None
IMAP search response parsing throws an error instead of a warning.
Email count initializes as None instead 0.
Email count is untouched in case of failure to parse response (i.e. if server is temporarily down or throwing errors, or maybe due to user updating their authentication/login/etc).
Fixed line length on error so it fits under 80 characters.
* Fixed Indent on Logger Error
Sorry about the churn! Python is pretty far from my daily-use language. (I did run this one through pep8, at least)
2019-01-19 20:37:02 +00:00
|
|
|
self._email_count = len(lines[0].split())
|
|
|
|
else:
|
2019-07-31 19:25:30 +00:00
|
|
|
_LOGGER.error(
|
2020-01-02 19:17:10 +00:00
|
|
|
"Can't parse IMAP server response to search '%s': %s / %s",
|
2019-07-31 19:25:30 +00:00
|
|
|
self._search,
|
|
|
|
result,
|
|
|
|
lines[0],
|
|
|
|
)
|
2017-09-26 07:26:26 +00:00
|
|
|
|
|
|
|
def disconnected(self):
|
|
|
|
"""Forget the connection after it was lost."""
|
|
|
|
_LOGGER.warning("Lost %s (will attempt to reconnect)", self._server)
|
|
|
|
self._connection = None
|
|
|
|
|
2020-10-19 21:25:33 +00:00
|
|
|
async def shutdown(self, *_):
|
2017-09-26 07:26:26 +00:00
|
|
|
"""Close resources."""
|
|
|
|
if self._connection:
|
|
|
|
if self._connection.has_pending_idle():
|
|
|
|
self._connection.idle_done()
|
2018-03-08 20:30:50 +00:00
|
|
|
await self._connection.logout()
|
2017-09-26 07:26:26 +00:00
|
|
|
if self._idle_loop_task:
|
|
|
|
self._idle_loop_task.cancel()
|