From c25b18e739626b8d92385befe361b017abcfeb1c Mon Sep 17 00:00:00 2001 From: Matthew Wegner Date: Thu, 3 Jan 2019 10:24:31 -0700 Subject: [PATCH 1/4] 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. --- homeassistant/components/sensor/imap.py | 26 ++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/sensor/imap.py b/homeassistant/components/sensor/imap.py index 2ea1fd576e6748..5d0117242eabd7 100644 --- a/homeassistant/components/sensor/imap.py +++ b/homeassistant/components/sensor/imap.py @@ -24,6 +24,7 @@ CONF_SERVER = 'server' CONF_FOLDER = 'folder' +CONF_SEARCH = 'search' DEFAULT_PORT = 993 @@ -36,6 +37,7 @@ vol.Required(CONF_SERVER): cv.string, vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, vol.Optional(CONF_FOLDER, default='INBOX'): cv.string, + vol.Optional(CONF_SEARCH, default='UnSeen UnDeleted'): cv.string, }) @@ -49,7 +51,8 @@ async def async_setup_platform(hass, config.get(CONF_PASSWORD), config.get(CONF_SERVER), config.get(CONF_PORT), - config.get(CONF_FOLDER)) + config.get(CONF_FOLDER), + config.get(CONF_SEARCH)) if not await sensor.connection(): raise PlatformNotReady @@ -60,7 +63,7 @@ async def async_setup_platform(hass, class ImapSensor(Entity): """Representation of an IMAP sensor.""" - def __init__(self, name, user, password, server, port, folder): + def __init__(self, name, user, password, server, port, folder, search): """Initialize the sensor.""" self._name = name or user self._user = user @@ -68,7 +71,8 @@ def __init__(self, name, user, password, server, port, folder): self._server = server self._port = port self._folder = folder - self._unread_count = 0 + self._email_count = 0 + self._search = search self._connection = None self._does_push = None self._idle_loop_task = None @@ -90,8 +94,8 @@ def icon(self): @property def state(self): - """Return the number of unread emails.""" - return self._unread_count + """Return the number of emails found.""" + return self._email_count @property def available(self): @@ -127,7 +131,7 @@ async def idle_loop(self): while True: try: if await self.connection(): - await self.refresh_unread_count() + await self.refresh_email_count() await self.async_update_ha_state() idle = await self._connection.idle_start() @@ -146,16 +150,16 @@ async def async_update(self): try: if await self.connection(): - await self.refresh_unread_count() + await self.refresh_email_count() except (aioimaplib.AioImapException, asyncio.TimeoutError): self.disconnected() - async def refresh_unread_count(self): - """Check the number of unread emails.""" + async def refresh_email_count(self): + """Check the number of found emails.""" if self._connection: await self._connection.noop() - _, lines = await self._connection.search('UnSeen UnDeleted') - self._unread_count = len(lines[0].split()) + _, lines = await self._connection.search(self._search) + self._email_count = len(lines[0].split()) def disconnected(self): """Forget the connection after it was lost.""" From 8b939885e9e00240c0bbf173e7b85992757e1ebd Mon Sep 17 00:00:00 2001 From: Matthew Wegner Date: Thu, 3 Jan 2019 15:18:01 -0700 Subject: [PATCH 2/4] 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). --- homeassistant/components/sensor/imap.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sensor/imap.py b/homeassistant/components/sensor/imap.py index 5d0117242eabd7..8a81f663284334 100644 --- a/homeassistant/components/sensor/imap.py +++ b/homeassistant/components/sensor/imap.py @@ -158,8 +158,13 @@ async def refresh_email_count(self): """Check the number of found emails.""" if self._connection: await self._connection.noop() - _, lines = await self._connection.search(self._search) - self._email_count = len(lines[0].split()) + result, lines = await self._connection.search(self._search) + + if result == 'OK': + self._email_count = len(lines[0].split()) + else: + _LOGGER.warning("Can't parse IMAP server response to search '%s': %s / %s", self._search, result, lines[0]) + self._email_count = 0 def disconnected(self): """Forget the connection after it was lost.""" From fd956f18ca97d8500cfa6e25709654f656c980f2 Mon Sep 17 00:00:00 2001 From: Matthew Wegner Date: Wed, 16 Jan 2019 15:13:31 -0700 Subject: [PATCH 3/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. --- homeassistant/components/sensor/imap.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sensor/imap.py b/homeassistant/components/sensor/imap.py index 8a81f663284334..1f92c1afbec634 100644 --- a/homeassistant/components/sensor/imap.py +++ b/homeassistant/components/sensor/imap.py @@ -71,7 +71,7 @@ def __init__(self, name, user, password, server, port, folder, search): self._server = server self._port = port self._folder = folder - self._email_count = 0 + self._email_count = None self._search = search self._connection = None self._does_push = None @@ -163,8 +163,8 @@ async def refresh_email_count(self): if result == 'OK': self._email_count = len(lines[0].split()) else: - _LOGGER.warning("Can't parse IMAP server response to search '%s': %s / %s", self._search, result, lines[0]) - self._email_count = 0 + _LOGGER.error("Can't parse IMAP server response to search " + "'%s': %s / %s", self._search, result, lines[0]) def disconnected(self): """Forget the connection after it was lost.""" From 208828b2ab5eee527e46dd38db2cb1c917e23313 Mon Sep 17 00:00:00 2001 From: Matthew Wegner Date: Wed, 16 Jan 2019 15:19:28 -0700 Subject: [PATCH 4/4] 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) --- homeassistant/components/sensor/imap.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/sensor/imap.py b/homeassistant/components/sensor/imap.py index 1f92c1afbec634..b8d363417c2112 100644 --- a/homeassistant/components/sensor/imap.py +++ b/homeassistant/components/sensor/imap.py @@ -164,7 +164,8 @@ async def refresh_email_count(self): self._email_count = len(lines[0].split()) else: _LOGGER.error("Can't parse IMAP server response to search " - "'%s': %s / %s", self._search, result, lines[0]) + "'%s': %s / %s", + self._search, result, lines[0]) def disconnected(self): """Forget the connection after it was lost."""