diff --git a/custom_components/o365/__init__.py b/custom_components/o365/__init__.py index e10b5f1..5391c9e 100644 --- a/custom_components/o365/__init__.py +++ b/custom_components/o365/__init__.py @@ -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 ( @@ -42,6 +43,7 @@ CONST_CONFIG_TYPE_DICT, CONST_CONFIG_TYPE_LIST, CONST_PRIMARY, + CONST_UTC_TIMEZONE, DEFAULT_CACHE_PATH, DEFAULT_NAME, DOMAIN, @@ -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) @@ -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, @@ -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 diff --git a/custom_components/o365/classes/mailsensor.py b/custom_components/o365/classes/mailsensor.py index 3190061..e754e3b 100644 --- a/custom_components/o365/classes/mailsensor.py +++ b/custom_components/o365/classes/mailsensor.py @@ -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, @@ -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): @@ -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 @@ -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: diff --git a/custom_components/o365/const.py b/custom_components/o365/const.py index 41dc46f..5c08b10 100644 --- a/custom_components/o365/const.py +++ b/custom_components/o365/const.py @@ -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" @@ -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 @@ -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" @@ -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], diff --git a/custom_components/o365/mailbox.py b/custom_components/o365/mailbox.py new file mode 100644 index 0000000..f19ccf9 --- /dev/null +++ b/custom_components/o365/mailbox.py @@ -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 diff --git a/custom_components/o365/schema.py b/custom_components/o365/schema.py index 52175d0..efc532b 100644 --- a/custom_components/o365/schema.py +++ b/custom_components/o365/schema.py @@ -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 @@ -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, @@ -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 = {} diff --git a/custom_components/o365/sensor.py b/custom_components/o365/sensor.py index d06d573..59b5d11 100644 --- a/custom_components/o365/sensor.py +++ b/custom_components/o365/sensor.py @@ -40,6 +40,7 @@ CONF_TRACK, CONF_TRACK_NEW, DOMAIN, + PERM_MINIMUM_MAILBOX_SETTINGS, PERM_MINIMUM_TASKS_WRITE, SENSOR_ENTITY_ID_FORMAT, SENSOR_MAIL, @@ -48,7 +49,12 @@ SENSOR_TODO, YAML_TASK_LISTS, ) -from .schema import NEW_TASK_SCHEMA, TASK_LIST_SCHEMA +from .schema import ( + AUTO_REPLY_DISABLE_SCHEMA, + AUTO_REPLY_ENABLE_SCHEMA, + NEW_TASK_SCHEMA, + TASK_LIST_SCHEMA, +) from .utils import ( build_config_file_path, build_token_filename, @@ -142,7 +148,7 @@ async def _async_email_sensors(self): hass=self.hass, ) emailsensor = O365EmailSensor( - self, sensor_conf, mail_folder, name, entity_id + self, self._config, sensor_conf, mail_folder, name, entity_id ) _LOGGER.debug( "Email sensor added: %s, %s", @@ -166,7 +172,7 @@ async def _async_query_sensors(self): hass=self.hass, ) querysensor = O365QuerySensor( - self, sensor_conf, mail_folder, name, entity_id + self, self._config, sensor_conf, mail_folder, name, entity_id ) entities.append(querysensor) return entities @@ -391,29 +397,63 @@ def _build_entity_id(hass, name, conf): ) -async def _async_setup_register_services(hass, conf): - todo_sensors = conf.get(CONF_TODO_SENSORS) +async def _async_setup_register_services(hass, config): + + await _async_setup_task_services(hass, config) + await _async_setup_mailbox_services(hass, config) + + +async def _async_setup_task_services(hass, config): + + if not config.get(CONF_ENABLE_UPDATE): + return + + todo_sensors = config.get(CONF_TODO_SENSORS) if not todo_sensors or not todo_sensors.get(CONF_ENABLED): return + sensor_services = SensorServices(hass) + hass.services.async_register( + DOMAIN, "scan_for_task_lists", sensor_services.async_scan_for_task_lists + ) + permissions = get_permissions( hass, - filename=build_token_filename(conf, conf.get(CONF_CONFIG_TYPE)), + filename=build_token_filename(config, config.get(CONF_CONFIG_TYPE)), ) - if conf.get(CONF_ENABLE_UPDATE) and validate_minimum_permission( - PERM_MINIMUM_TASKS_WRITE, permissions - ): - platform = entity_platform.async_get_current_platform() + platform = entity_platform.async_get_current_platform() + if validate_minimum_permission(PERM_MINIMUM_TASKS_WRITE, permissions): platform.async_register_entity_service( "new_task", NEW_TASK_SCHEMA, "new_task", ) - sensor_services = SensorServices(hass) - hass.services.async_register( - DOMAIN, "scan_for_task_lists", sensor_services.async_scan_for_task_lists + +async def _async_setup_mailbox_services(hass, config): + + if not config.get(CONF_ENABLE_UPDATE): + return + + if not config.get(CONF_EMAIL_SENSORS) and not config.get(CONF_QUERY_SENSORS): + return + + permissions = get_permissions( + hass, + filename=build_token_filename(config, config.get(CONF_CONFIG_TYPE)), ) + platform = entity_platform.async_get_current_platform() + if validate_minimum_permission(PERM_MINIMUM_MAILBOX_SETTINGS, permissions): + platform.async_register_entity_service( + "auto_reply_enable", + AUTO_REPLY_ENABLE_SCHEMA, + "auto_reply_enable", + ) + platform.async_register_entity_service( + "auto_reply_disable", + AUTO_REPLY_DISABLE_SCHEMA, + "auto_reply_disable", + ) class SensorServices: diff --git a/custom_components/o365/services.yaml b/custom_components/o365/services.yaml index 59a7e13..7b4527a 100644 --- a/custom_components/o365/services.yaml +++ b/custom_components/o365/services.yaml @@ -1,4 +1,13 @@ +scan_for_calendars: + name: Scan for new calendars + description: "Scan for newly available calendars" + +scan_for_task_lists: + name: Scan for new task lists + description: "Scan for newly available task lists" + respond_calendar_event: + name: Respond to an event description: "Respond to calendar event/invite" target: device: @@ -21,6 +30,7 @@ respond_calendar_event: example: True create_calendar_event: + name: Create a new event description: Create new calendar event target: device: @@ -59,6 +69,7 @@ create_calendar_event: description: "list of attendees formatted as email: example@example.com type: Required, Optional, or Resource (optional)" modify_calendar_event: + name: Modify an event description: Modify existing calendar event, all properties except event_id are optional. target: device: @@ -100,6 +111,7 @@ modify_calendar_event: description: "list of attendees formatted as email: example@example.com type: Required, Optional, or Resource" remove_calendar_event: + name: Delete an event description: Delete calendar event target: device: @@ -113,6 +125,7 @@ remove_calendar_event: example: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx new_task: + name: Create a task/ToDo description: Create a new task/ToDo target: device: @@ -133,3 +146,41 @@ new_task: reminder: description: When a reminder is needed example: "2023-01-01T12:00:00+0000" + + +auto_reply_enable: + name: Auto reply enable + description: Schedules auto reply + target: + device: + integration: o365 + entity: + integration: o365 + domain: sensor + fields: + start: + description: The start time of the schedule + example: "2023-01-01T12:00:00+0000" + required: true + end: + description: The end time of the schedule + example: "2023-01-02T12:30:00+0000" + required: true + external_reply: + description: The message to be send to external emails (or to all emails, if you don't have an organisation email) + example: I'm currently on holliday, please email Bob for answers + required: true + internal_reply: + description: The message to be send to internal emails + example: I'm currently on holliday + required: true + +auto_reply_disable: + name: Auto reply disable + description: Disables auto reply + target: + device: + integration: o365 + entity: + integration: o365 + domain: sensor \ No newline at end of file diff --git a/custom_components/o365/utils.py b/custom_components/o365/utils.py index ed29144..c1bd594 100644 --- a/custom_components/o365/utils.py +++ b/custom_components/o365/utils.py @@ -42,6 +42,7 @@ PERM_GROUP_READWRITE_ALL, PERM_MAIL_READ, PERM_MAIL_SEND, + PERM_MAILBOX_SETTINGS, PERM_MINIMUM_CALENDAR, PERM_MINIMUM_CHAT, PERM_MINIMUM_GROUP, @@ -124,6 +125,8 @@ def build_requested_permissions(config): scope.append(PERM_GROUP_READ_ALL) if len(email_sensors) > 0 or len(query_sensors) > 0: scope.append(PERM_MAIL_READ) + if enable_update: + scope.append(PERM_MAILBOX_SETTINGS) if len(status_sensors) > 0: scope.append(PERM_PRESENCE_READ) if len(chat_sensors) > 0: diff --git a/docs/prerequisites.md b/docs/prerequisites.md index 09df33e..96ffd8e 100644 --- a/docs/prerequisites.md +++ b/docs/prerequisites.md @@ -48,3 +48,6 @@ To allow authentication, you first need to register your application at Azure Ap If you intend to set [groups](./installation_and_configuration.md#configuration_variables) to True, (not supported in legacy installs), then the following permissions are also required: * Group.Read.All - *Read all groups* * Group.ReadWrite.All - *Read and write all groups* - To enable creation of events in group calendars if you have set [enable_update](./installation_and_configuration.md#configuration_variables). + + If you are enabling/disabling the auto reply you will need: + * MailboxSettings.ReadWrite - *Read and write user mailbox settings* diff --git a/docs/services.md b/docs/services.md index 3290b40..e3cce0e 100644 --- a/docs/services.md +++ b/docs/services.md @@ -54,6 +54,10 @@ Remove an event in the specified calendar - All paremeters are shown in the avai Respond to an event in the specified calendar - All paremeters are shown in the available parameter list on the Developer Tools/Services tab. Not possible for group calendars. ## o365.scan_for_calendars Scan for new calendars and add to o365_calendars.yaml - No parameters. Does not scan for group calendars. +## o365.set_auto_reply +Schedule the auto reply - All paremeters are shown in the available parameter list on the Developer Tools/Services tab. +## o365.disable_auto_reply +Disable the auto reply - All paremeters are shown in the available parameter list on the Developer Tools/Services tab. ### Example create event service call