Skip to content

Commit

Permalink
Merge pull request #94 from RogerSelwyn/sourcery/pull-89
Browse files Browse the repository at this point in the history
Added mailbox service to enable auto reply (Sourcery refactored)
  • Loading branch information
RogerSelwyn authored Dec 11, 2022
2 parents b1f7233 + e630bbc commit 87d8774
Show file tree
Hide file tree
Showing 10 changed files with 295 additions and 34 deletions.
15 changes: 11 additions & 4 deletions custom_components/o365/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from homeassistant.helpers import discovery
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
from homeassistant.helpers.network import get_url

from O365 import Account, FileSystemTokenBackend

from .const import (
Expand Down Expand Up @@ -42,6 +43,7 @@
CONST_CONFIG_TYPE_DICT,
CONST_CONFIG_TYPE_LIST,
CONST_PRIMARY,
CONST_UTC_TIMEZONE,
DEFAULT_CACHE_PATH,
DEFAULT_NAME,
DOMAIN,
Expand Down Expand Up @@ -155,7 +157,12 @@ async def _async_setup_account(hass, account_conf, conf_type):
)

account = await hass.async_add_executor_job(
ft.partial(Account, credentials, token_backend=token_backend, timezone="UTC")
ft.partial(
Account,
credentials,
token_backend=token_backend,
timezone=CONST_UTC_TIMEZONE,
)
)
is_authenticated = account.is_authenticated
minimum_permissions = build_minimum_permissions(hass, account_conf, conf_type)
Expand Down Expand Up @@ -284,7 +291,7 @@ def _create_request_content_default(hass, url, callback_view, account_name):
return o365configurator.async_request_config(
hass,
view_name,
callback_view.default_callback,
callback=callback_view.default_callback,
link_name=CONFIGURATOR_LINK_NAME,
link_url=url,
fields=CONFIGURATOR_FIELDS,
Expand Down Expand Up @@ -389,11 +396,11 @@ def default_callback(self, data):
)
return

account_data = self._hass.data[DOMAIN][self._account_name]
request_id = self._hass.data[DOMAIN][self._account_name]
do_setup(
self._hass, self._config, self._account, self._account_name, self._conf_type
)
self.configurator.async_request_done(self._hass, account_data)
self.configurator.async_request_done(self._hass, request_id)

self._log_authenticated(self._account_name)
return
Expand Down
82 changes: 65 additions & 17 deletions custom_components/o365/classes/mailsensor.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
"""O365 mail sensors."""
import datetime

import voluptuous as vol
from homeassistant.helpers.entity import Entity
from homeassistant.util.dt import as_utc

from ..const import (
ATTR_ATTRIBUTES,
CONF_ACCOUNT,
CONF_BODY_CONTAINS,
CONF_CONFIG_TYPE,
CONF_DOWNLOAD_ATTACHMENTS,
CONF_HAS_ATTACHMENT,
CONF_IMPORTANCE,
Expand All @@ -14,21 +18,26 @@
CONF_MAX_ITEMS,
CONF_SUBJECT_CONTAINS,
CONF_SUBJECT_IS,
CONST_UTC_TIMEZONE,
PERM_MAILBOX_SETTINGS,
PERM_MINIMUM_MAILBOX_SETTINGS,
SENSOR_MAIL,
)
from ..utils import build_token_filename, get_permissions, validate_minimum_permission
from .sensorentity import O365Sensor


class O365MailSensor(O365Sensor):
"""O365 generic Mail Sensor class."""

def __init__(self, coordinator, conf, mail_folder, name, entity_id):
def __init__(self, coordinator, config, sensor_conf, mail_folder, name, entity_id):
"""Initialise the O365 Sensor."""
super().__init__(coordinator, name, entity_id, SENSOR_MAIL)
self.mail_folder = mail_folder
self.download_attachments = conf.get(CONF_DOWNLOAD_ATTACHMENTS, True)
self.max_items = conf.get(CONF_MAX_ITEMS, 5)
self.download_attachments = sensor_conf.get(CONF_DOWNLOAD_ATTACHMENTS, True)
self.max_items = sensor_conf.get(CONF_MAX_ITEMS, 5)
self.query = None
self._config = config

@property
def icon(self):
Expand All @@ -40,27 +49,66 @@ def extra_state_attributes(self):
"""Device state attributes."""
return self.coordinator.data[self.entity_id][ATTR_ATTRIBUTES]

def auto_reply_enable(self, start, end, external_reply, internal_reply):
"""Enable out of office autoreply."""
if not self._validate_permissions():
return

start = as_utc(start)
end = as_utc(end)
starttime = start.strftime("%Y-%m-%dT%H:%M:%S")
endtime = end.strftime("%Y-%m-%dT%H:%M:%S")
account = self._config[CONF_ACCOUNT]
mailbox = account.mailbox()
mailbox.set_automatic_reply(
internal_reply, external_reply, starttime, endtime, CONST_UTC_TIMEZONE
)

def auto_reply_disable(self):
"""Disable out of office autoreply."""
if not self._validate_permissions():
return

account = self._config[CONF_ACCOUNT]
mailbox = account.mailbox()
mailbox.set_disable_reply()

def _validate_permissions(self):
permissions = get_permissions(
self.hass,
filename=build_token_filename(
self._config, self._config.get(CONF_CONFIG_TYPE)
),
)
if not validate_minimum_permission(PERM_MINIMUM_MAILBOX_SETTINGS, permissions):
raise vol.Invalid(
"Not authorisied to update auto reply - requires permission: "
+ f"{PERM_MAILBOX_SETTINGS}"
)

return True


class O365QuerySensor(O365MailSensor, Entity):
"""O365 Query sensor processing."""

def __init__(self, coordinator, conf, mail_folder, name, entity_id):
def __init__(self, coordinator, config, sensor_conf, mail_folder, name, entity_id):
"""Initialise the O365 Query."""
super().__init__(coordinator, conf, mail_folder, name, entity_id)
super().__init__(coordinator, config, sensor_conf, mail_folder, name, entity_id)

self.query = self.mail_folder.new_query()
self.query.order_by("receivedDateTime", ascending=False)

self._build_query(conf)
self._build_query(sensor_conf)

def _build_query(self, conf):
body_contains = conf.get(CONF_BODY_CONTAINS)
subject_contains = conf.get(CONF_SUBJECT_CONTAINS)
subject_is = conf.get(CONF_SUBJECT_IS)
has_attachment = conf.get(CONF_HAS_ATTACHMENT)
importance = conf.get(CONF_IMPORTANCE)
email_from = conf.get(CONF_MAIL_FROM)
is_unread = conf.get(CONF_IS_UNREAD)
def _build_query(self, sensor_conf):
body_contains = sensor_conf.get(CONF_BODY_CONTAINS)
subject_contains = sensor_conf.get(CONF_SUBJECT_CONTAINS)
subject_is = sensor_conf.get(CONF_SUBJECT_IS)
has_attachment = sensor_conf.get(CONF_HAS_ATTACHMENT)
importance = sensor_conf.get(CONF_IMPORTANCE)
email_from = sensor_conf.get(CONF_MAIL_FROM)
is_unread = sensor_conf.get(CONF_IS_UNREAD)
if (
body_contains is not None
or subject_contains is not None
Expand Down Expand Up @@ -98,11 +146,11 @@ def _add_to_query(self, qtype, attribute_name, attribute_value, check_value=True
class O365EmailSensor(O365MailSensor, Entity):
"""O365 Email sensor processing."""

def __init__(self, coordinator, conf, mail_folder, name, entity_id):
def __init__(self, coordinator, config, sensor_conf, mail_folder, name, entity_id):
"""Initialise the O365 Email sensor."""
super().__init__(coordinator, conf, mail_folder, name, entity_id)
super().__init__(coordinator, config, sensor_conf, mail_folder, name, entity_id)

is_unread = conf.get(CONF_IS_UNREAD)
is_unread = sensor_conf.get(CONF_IS_UNREAD)

self.query = None
if is_unread is not None:
Expand Down
5 changes: 5 additions & 0 deletions custom_components/o365/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,12 @@ class EventResponse(Enum):
ATTR_ENTITY_ID = "entity_id"
ATTR_ERROR = "error"
ATTR_EVENT_ID = "event_id"
ATTR_EXTERNALREPLY = "external_reply"
ATTR_FROM_DISPLAY_NAME = "from_display_name"
ATTR_GROUP = "group"
ATTR_IS_ALL_DAY = "is_all_day"
ATTR_IMPORTANCE = "importance"
ATTR_INTERNALREPLY = "internal_reply"
ATTR_LOCATION = "location"
ATTR_MESSAGE_IS_HTML = "message_is_html"
ATTR_OVERDUE_TASKS = "overdue_tasks"
Expand Down Expand Up @@ -111,6 +113,7 @@ class EventResponse(Enum):
CONST_CONFIG_TYPE_LIST = "list"
CONST_GROUP = "group:"
CONST_PRIMARY = "$o365-primary$"
CONST_UTC_TIMEZONE = "UTC"
DATETIME_FORMAT = "%Y-%m-%dT%H:%M:%S%z"
DEFAULT_CACHE_PATH = ".O365-token-cache"
DEFAULT_HOURS_BACKWARD_TO_GET = 0
Expand All @@ -129,6 +132,7 @@ class EventResponse(Enum):
PERM_CHAT_READ = "Chat.Read"
PERM_GROUP_READ_ALL = "Group.Read.All"
PERM_GROUP_READWRITE_ALL = "Group.ReadWrite.All"
PERM_MAILBOX_SETTINGS = "MailboxSettings.ReadWrite"
PERM_MAIL_READ = "Mail.Read"
PERM_MAIL_READ_SHARED = "Mail.Read.Shared"
PERM_MAIL_READWRITE = "Mail.ReadWrite"
Expand All @@ -146,6 +150,7 @@ class EventResponse(Enum):
PERM_MINIMUM_TASKS = [PERM_TASKS_READ, [PERM_TASKS_READWRITE]]
PERM_MINIMUM_TASKS_WRITE = [PERM_TASKS_READWRITE, []]
PERM_MINIMUM_USER = [PERM_USER_READ, []]
PERM_MINIMUM_MAILBOX_SETTINGS = [PERM_MAILBOX_SETTINGS, []]
PERM_MINIMUM_MAIL = [
PERM_MAIL_READ,
[PERM_MAIL_READ_SHARED, PERM_MAIL_READWRITE, PERM_MAIL_READWRITE_SHARED],
Expand Down
88 changes: 88 additions & 0 deletions custom_components/o365/mailbox.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
"""Main mailbox processing."""

from homeassistant.core import HomeAssistant

import voluptuous as vol
from .const import (
CONF_ACCOUNT,
CONF_ACCOUNT_NAME,
CONF_CONFIG_TYPE,
DOMAIN,
PERM_MAILBOX_SETTINGS,
PERM_MINIMUM_MAILBOX,
)

from .utils import (
build_token_filename,
get_permissions,
validate_minimum_permission,
)

from .schema import (
ATTR_ACCOUNT,
ATTR_INTERNALREPLY,
ATTR_EXTERNALREPLY,
ATTR_START,
ATTR_END,
ATTR_TIMEZONE,
)


async def async_setup_mailbox(
hass: HomeAssistant, discovery_info=None
): # pylint: disable=unused-argument
"""Set up the O365 mailbox."""
if discovery_info is None:
return None

account_name = discovery_info[CONF_ACCOUNT_NAME]
conf = hass.data[DOMAIN][account_name]
account = conf[CONF_ACCOUNT]
if not account.is_authenticated:
return False

def _validate_permissions(error_message, config):
permissions = get_permissions(
hass,
filename=build_token_filename(config, config.get(CONF_CONFIG_TYPE)),
)
if not validate_minimum_permission(PERM_MINIMUM_MAILBOX, permissions):
raise vol.Invalid(
f"Not authorisied to {PERM_MAILBOX_SETTINGS} calendar event "
+ f"- requires permission: {error_message}"
)
return True

def set_auto_reply(call):
"""Schedule the auto reply."""
account_name = call.data.get(ATTR_ACCOUNT)
if account_name not in hass.data[DOMAIN]:
return
conf = hass.data[DOMAIN][account_name]
account = conf[CONF_ACCOUNT]
if not _validate_permissions("MailboxSettings.ReadWrite", conf):
return
internalReply = call.data.get(ATTR_INTERNALREPLY)
externalReply = call.data.get(ATTR_EXTERNALREPLY)
start = call.data.get(ATTR_START)
end = call.data.get(ATTR_END)
timezone = call.data.get(ATTR_TIMEZONE)
mailbox = account.mailbox()
mailbox.set_automatic_reply(internalReply, externalReply, start, end, timezone)

def disable_auto_reply(call):
"""Schedule the auto reply."""
account_name = call.data.get(ATTR_ACCOUNT)
if account_name not in hass.data[DOMAIN]:
return
conf = hass.data[DOMAIN][account_name]
account = conf[CONF_ACCOUNT]
if not _validate_permissions("MailboxSettings.ReadWrite", conf):
return

mailbox = account.mailbox()
mailbox.set_disable_reply()

hass.services.async_register(DOMAIN, "set_auto_reply", set_auto_reply)
hass.services.async_register(DOMAIN, "disable_auto_reply", disable_auto_reply)
return True
12 changes: 12 additions & 0 deletions custom_components/o365/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
ATTR_TITLE,
)
from homeassistant.const import CONF_ENABLED, CONF_NAME

from O365.calendar import AttendeeType # pylint: disable=no-name-in-module
from O365.calendar import EventSensitivity # pylint: disable=no-name-in-module
from O365.calendar import EventShowAs # pylint: disable=no-name-in-module
Expand All @@ -24,6 +25,8 @@
ATTR_END,
ATTR_ENTITY_ID,
ATTR_EVENT_ID,
ATTR_EXTERNALREPLY,
ATTR_INTERNALREPLY,
ATTR_IS_ALL_DAY,
ATTR_LOCATION,
ATTR_MESSAGE_IS_HTML,
Expand Down Expand Up @@ -257,3 +260,12 @@
vol.Optional(ATTR_DUE): cv.string,
vol.Optional(ATTR_REMINDER): cv.datetime,
}

AUTO_REPLY_ENABLE_SCHEMA = {
vol.Required(ATTR_START): cv.datetime,
vol.Required(ATTR_END): cv.datetime,
vol.Required(ATTR_EXTERNALREPLY): cv.string,
vol.Required(ATTR_INTERNALREPLY): cv.string,
}

AUTO_REPLY_DISABLE_SCHEMA = {}
Loading

0 comments on commit 87d8774

Please sign in to comment.