Skip to content

Commit

Permalink
refactored
Browse files Browse the repository at this point in the history
  • Loading branch information
caronc committed Dec 1, 2024
1 parent 5a32481 commit 2c6eb4a
Showing 1 changed file with 53 additions and 97 deletions.
150 changes: 53 additions & 97 deletions apprise/plugins/office365.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,11 @@
# Information on sending an email:
# https://docs.microsoft.com/en-us/graph/api/user-sendmail\
# ?view=graph-rest-1.0&tabs=http

#
# Note: One must set up Application Permissions (not Delegated Permissions)
# - Scopes required: Mail.Send
# - For Large Attachments: Mail.ReadWrite
#
import requests
from datetime import datetime
from datetime import timedelta
Expand All @@ -50,24 +54,6 @@
from ..locale import gettext_lazy as _


class Office365WebhookMode:
"""
Office 365 Webhook Mode
"""
# Send message as ourselves using the /me/ endpoint
SELF = 'self'

# Send message as ourselves using the /users/ endpoint
AS_USER = 'user'


# Define the modes in a list for validation purposes
OFFICE365_WEBHOOK_MODES = (
Office365WebhookMode.SELF,
Office365WebhookMode.AS_USER,
)


class NotifyOffice365(NotifyBase):
"""
A wrapper for Office 365 Notifications
Expand Down Expand Up @@ -117,11 +103,9 @@ class NotifyOffice365(NotifyBase):

# Define object templates
templates = (
# Send as user
'{schema}://{email}/{tenant}/{client_id}/{secret}',
'{schema}://{email}/{tenant}/{client_id}/{secret}/{targets}',
# Send from 'me'
'{schema}://{tenant}/{client_id}/{secret}/{targets}',
# Send as user (only supported method)
'{schema}://{source}/{tenant}/{client_id}/{secret}',
'{schema}://{source}/{tenant}/{client_id}/{secret}/{targets}',
)

# Define our template tokens
Expand All @@ -133,8 +117,8 @@ class NotifyOffice365(NotifyBase):
'private': True,
'regex': (r'^[a-z0-9-]+$', 'i'),
},
'email': {
'name': _('Account Email'),
'source': {
'name': _('Account Email or Object ID'),
'type': 'string',
'required': True,
},
Expand Down Expand Up @@ -181,33 +165,15 @@ class NotifyOffice365(NotifyBase):
'oauth_secret': {
'alias_of': 'secret',
},
'mode': {
'name': _('Webhook Mode'),
'type': 'choice:string',
'values': OFFICE365_WEBHOOK_MODES,
'default': Office365WebhookMode.SELF,
},
})

def __init__(self, tenant, client_id, secret, email=None,
mode=None, targets=None, cc=None, bcc=None, **kwargs):
def __init__(self, tenant, client_id, secret, source=None,
targets=None, cc=None, bcc=None, **kwargs):
"""
Initialize Office 365 Object
"""
super().__init__(**kwargs)

# Prepare our Mode
self.mode = self.template_args['mode']['default'] \
if not mode else next(
(f for f in OFFICE365_WEBHOOK_MODES
if f.startswith(
mode.lower())), None)
if mode and not self.mode:
msg = \
'The specified Webhook mode ({}) was not found '.format(mode)
self.logger.warning(msg)
raise TypeError(msg)

# Tenant identifier
self.tenant = validate_regex(
tenant, *self.template_tokens['tenant']['regex'])
Expand All @@ -217,23 +183,8 @@ def __init__(self, tenant, client_id, secret, email=None,
self.logger.warning(msg)
raise TypeError(msg)

self.email = None
if email is not None:
result = is_email(email)
if not result:
msg = 'An invalid Office 365 Email Account ID' \
'({}) was specified.'.format(email)
self.logger.warning(msg)
raise TypeError(msg)

# Otherwise store our the email address
self.email = result['full_email']

elif self.mode != Office365WebhookMode.SELF:
msg = 'An expected Office 365 Email was not specified ' \
'(mode={})'.format(self.mode)
self.logger.warning(msg)
raise TypeError(msg)
# Store our email/ObjectID Source
self.source = source

# Client Key (associated with generated OAuth2 Login)
self.client_id = validate_regex(
Expand Down Expand Up @@ -280,8 +231,14 @@ def __init__(self, tenant, client_id, secret, email=None,
.format(recipient))

else:
# If our target email list is empty we want to add ourselves to it
self.targets.append((False, self.email))
result = is_email(self.source)
if not result:
self.logger.warning('No Target Office 365 Email Detected')

else:
# If our target email list is empty we want to add ourselves to
# it
self.targets.append((False, self.source))

# Validate recipients (cc:) and drop bad ones:
for recipient in parse_emails(cc):
Expand Down Expand Up @@ -359,12 +316,9 @@ def send(self, body, title='', notify_type=NotifyType.INFO, attach=None,
emails = list(self.targets)

# Define our URL to post to
url = '{graph_url}/v1.0/me/sendMail'.format(
url = '{graph_url}/v1.0/users/{userid}/sendMail'.format(
userid=self.source,
graph_url=self.graph_url,
) if not self.email \
else '{graph_url}/v1.0/users/{userid}/sendMail'.format(
userid=self.email,
graph_url=self.graph_url,
)

attachments = []
Expand Down Expand Up @@ -478,9 +432,7 @@ def send(self, body, title='', notify_type=NotifyType.INFO, attach=None,
else '{}: '.format(self.names[e]), e) for e in bcc])))

# Perform upstream fetch
postokay, response = self._fetch(
url=url, payload=dumps(payload),
content_type='application/json')
postokay, response = self._fetch(url=url, payload=dumps(payload))

# Test if we were okay
if not postokay:
Expand Down Expand Up @@ -513,12 +465,12 @@ def authenticate(self):

# Prepare our payload
payload = {
'grant_type': 'client_credentials',
'client_id': self.client_id,
'client_secret': self.secret,
'scope': '{graph_url}/{scope}'.format(
graph_url=self.graph_url,
scope=self.scope),
'grant_type': 'client_credentials',
}

# Prepare our URL
Expand Down Expand Up @@ -573,8 +525,7 @@ def authenticate(self):
# We're authenticated
return True if self.token else False

def _fetch(self, url, payload,
content_type='application/x-www-form-urlencoded'):
def _fetch(self, url, payload, method='POST'):
"""
Wrapper to request object
Expand All @@ -583,7 +534,6 @@ def _fetch(self, url, payload,
# Prepare our headers:
headers = {
'User-Agent': self.app_id,
'Content-Type': content_type,
}

if self.token:
Expand All @@ -602,8 +552,9 @@ def _fetch(self, url, payload,
self.throttle()

# fetch function
req = requests.post if method == 'POST' else requests.get
try:
r = requests.post(
r = req(
url,
data=payload,
headers=headers,
Expand All @@ -625,6 +576,21 @@ def _fetch(self, url, payload,
', ' if status_str else '',
r.status_code))

# A Response could look like this if a Scope element was not
# found:
# {
# "error": {
# "code": "MissingClaimType",
# "message":"The token is missing the claim type \'oid\'.",
# "innerError": {
# "oAuthEventOperationId":" 7abe20-339f-4659-9381-38f52",
# "oAuthEventcV": "xsOSpAHSHVm3Tp4SNH5oIA.1.1",
# "errorUrl": "https://url",
# "requestId": "2328ea-ec9e-43a8-80f4-164c",
# "date":"2024-12-01T02:03:13"
# }}
# }

self.logger.debug(
'Response Details:\r\n{}'.format(r.content))

Expand Down Expand Up @@ -659,7 +625,7 @@ def url_identifier(self):
here.
"""
return (
self.secure_protocol[0], self.email, self.tenant, self.client_id,
self.secure_protocol[0], self.source, self.tenant, self.client_id,
self.secret,
)

Expand All @@ -668,13 +634,8 @@ def url(self, privacy=False, *args, **kwargs):
Returns the URL built dynamically based on specified arguments.
"""

# Define any URL parameters
params = {
'mode': self.mode,
}

# Extend our parameters
params.update(self.url_parameters(privacy=privacy, *args, **kwargs))
params = self.url_parameters(privacy=privacy, *args, **kwargs)

if self.cc:
# Handle our Carbon Copy Addresses
Expand All @@ -690,13 +651,13 @@ def url(self, privacy=False, *args, **kwargs):
'' if not self.names.get(e)
else '{}:'.format(self.names[e]), e) for e in self.bcc])

return '{schema}://{email}{tenant}/{client_id}/{secret}' \
return '{schema}://{source}/{tenant}/{client_id}/{secret}' \
'/{targets}/?{params}'.format(
schema=self.secure_protocol[0],
tenant=self.pprint(self.tenant, privacy, safe=''),
# email does not need to be escaped because it should
# already be a valid host and username at this point
email=self.email + '/' if self.email else '',
source=self.source,
client_id=self.pprint(self.client_id, privacy, safe=''),
secret=self.pprint(
self.secret, privacy, mode=PrivacyMode.Secret,
Expand Down Expand Up @@ -743,20 +704,19 @@ def parse_url(url):
if 'from' in results['qsd'] and \
len(results['qsd']['from']):
# Extract the sending account's information
results['email'] = \
results['source'] = \
NotifyOffice365.unquote(results['qsd']['from'])

# If tenant is occupied, then the user defined makes up our email
# If tenant is occupied, then the user defined makes up our source
elif results['user']:
results['email'] = '{}@{}'.format(
results['source'] = '{}@{}'.format(
NotifyOffice365.unquote(results['user']),
NotifyOffice365.unquote(results['host']),
)

else:
# Hostname is no longer part of `from` and possibly instead
# is the tenant id
entries.insert(0, NotifyOffice365.unquote(results['host']))
# Object ID instead of email
results['source'] = NotifyOffice365.unquote(results['host'])

# Tenant
if 'tenant' in results['qsd'] and len(results['qsd']['tenant']):
Expand Down Expand Up @@ -825,8 +785,4 @@ def parse_url(url):
if 'bcc' in results['qsd'] and len(results['qsd']['bcc']):
results['bcc'] = results['qsd']['bcc']

# Handle Mode
if 'mode' in results['qsd'] and len(results['qsd']['mode']):
results['mode'] = results['qsd']['mode']

return results

0 comments on commit 2c6eb4a

Please sign in to comment.