Skip to content

Commit

Permalink
plugin updated to take in email id
Browse files Browse the repository at this point in the history
  • Loading branch information
caronc committed Apr 19, 2020
1 parent a9e3b29 commit 8043282
Show file tree
Hide file tree
Showing 3 changed files with 172 additions and 47 deletions.
143 changes: 116 additions & 27 deletions apprise/plugins/NotifyOffice365.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,32 @@
# https://docs.microsoft.com/en-us/graph/api/user-sendmail\
# ?view=graph-rest-1.0&tabs=http

# Steps to get your Microsoft Client ID, Client Secret, and Tenant ID:
# 1. You should have valid Microsoft personal account. Go to Azure Portal
# 2. Go to -> Microsoft Active Directory --> App Registrations
# 3. click new -> give any name (your choice) in Name field -> select
# personal Microsoft accounts only --> Register
# 4. Now you have client_id & Tenant id.
# 5. To create client_secret , go to active directory ->
# Certificate & Tokens -> New client secret
# **This is auto-generated string which may have '@' and '?'
# characters in it. You should encode these to prevent
# from having any issues.**
# 6. Now need to set permission Active directory -> API permissions ->
# Add permission (search mail) , add relevant permission.
# 7. Set the redirect uri (Web) to:
# https://login.microsoftonline.com/common/oauth2/nativeclient
#
# ...and click register.
#
# This needs to be inserted into the "Redirect URI" text box as simply
# checking the check box next to this link seems to be insufficient.
# This is the default redirect uri used by this library, but you can use
# any other if you want.
#
# 8. Now you're good to go

import requests
from itertools import chain
from datetime import datetime
from datetime import timedelta
from json import loads
Expand Down Expand Up @@ -66,7 +90,7 @@ class NotifyOffice365(NotifyBase):
request_rate_per_sec = 0.20

# A URL that takes you to the setup/help of the specific protocol
setup_url = 'https://github.com/caronc/apprise/wiki/Notify_o365'
setup_url = 'https://github.com/caronc/apprise/wiki/Notify_office365'

# URL to Microsoft Graph Server
graph_url = 'https://graph.microsoft.com'
Expand All @@ -86,7 +110,10 @@ class NotifyOffice365(NotifyBase):

# Define object templates
templates = (
'{schema}://{tenant}:{client_id}@{secret}/{targets}',
'{schema}://{email}/{client_id}/{secret}',
'{schema}://{tenant}:{email}/{client_id}/{secret}',
'{schema}://{email}/{client_id}/{secret}/{targets}',
'{schema}://{tenant}:{email}/{client_id}/{secret}/{targets}',
)

# Define our template tokens
Expand All @@ -96,6 +123,11 @@ class NotifyOffice365(NotifyBase):
'type': 'string',
'regex': (r'^[a-z0-9.-]+$', 'i'),
},
'email': {
'name': _('Account Email'),
'type': 'string',
'required': True,
},
'client_id': {
'name': _('Client ID'),
'type': 'string',
Expand Down Expand Up @@ -128,12 +160,24 @@ class NotifyOffice365(NotifyBase):
},
})

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

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

if not tenant:
# Default tenant to defined hostname found in email
tenant = email.split('@')[1]

# Office Tenant identifier
self.tenant = validate_regex(
tenant, *self.template_tokens['tenant']['regex'])
Expand Down Expand Up @@ -163,15 +207,20 @@ def __init__(self, tenant, client_id, secret, targets=None, **kwargs):
# Parse our targets
self.targets = list()

for target in parse_list(targets):
# Validate targets and drop bad ones:
if not is_email(target):
self.logger.warning(
'Dropped invalid email specified: {}'.format(target))
continue

# Add our email to our target list
self.targets.append(target)
targets = parse_list(targets)
if targets:
for target in targets:
# Validate targets and drop bad ones:
if not is_email(target):
self.logger.warning(
'Dropped invalid email specified: {}'.format(target))
continue

# Add our email to our target list
self.targets.append(target)
else:
# Default to adding ourselves
self.targets.append(self.email)

# Our token is acquired upon a successful login
self.token = None
Expand Down Expand Up @@ -215,7 +264,10 @@ def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
targets = list(self.targets)

# Define our URL to post to
url = '{graph_url}/v1.0/me/sendMail'.format(graph_url=self.graph_url)
url = '{graph_url}/v1.0/users/{email}/sendmail'.format(
email=self.email,
graph_url=self.graph_url,
)

while len(targets):
# Get our target to notify
Expand Down Expand Up @@ -260,7 +312,7 @@ def authenticate(self):
# Prepare our payload
payload = {
'client_id': self.client_id,
'client_secret': self.secret,
'secret': self.secret,
'scope': '{graph_url}/{scope}'.format(
graph_url=self.graph_url,
scope=self.scope),
Expand Down Expand Up @@ -334,7 +386,7 @@ def _fetch(self, url, payload,

if self.token:
# Are we authenticated?
headers['Authorization'] = 'Bearer ' + self.token,
headers['Authorization'] = 'Bearer ' + self.token

# Default content response object
content = {}
Expand Down Expand Up @@ -408,13 +460,20 @@ def url(self, privacy=False, *args, **kwargs):
'verify': 'yes' if self.verify_certificate else 'no',
}

return '{schema}://{tenant}:{client_id}@{secret}/{targets}/' \
'?{args}'.format(
tenant = '' if self.host == self.tenant else self.tenant

return '{schema}://{tenant}{email}/{client_id}/{secret}' \
'/{targets}/?{args}'.format(
schema=self.secure_protocol,
tenant=NotifyOffice365.quote(self.tenant, safe=''),
tenant='{}:'.format(NotifyOffice365.quote(tenant, safe=''))
if tenant else '',
# email does not need to be escaped because it should
# already be a valid host and username at this point
email=self.email,
client_id=self.pprint(self.client_id, privacy, safe=''),
secret=self.pprint(
self.secret, privacy, mode=PrivacyMode.Secret, safe=''),
self.secret, privacy, mode=PrivacyMode.Secret,
safe=''),
targets='/'.join(
[NotifyOffice365.quote(x, safe='') for x in self.targets]),
args=NotifyOffice365.urlencode(args))
Expand All @@ -439,6 +498,15 @@ def parse_url(url):
# of the secret key (since it can contain slashes in it)
entries = NotifyOffice365.split_path(results['fullpath'])

try:
# Get our client_id is the first entry on the path
results['client_id'] = NotifyOffice365.unquote(entries.pop(0))

except IndexError:
# no problem, we may get the client_id another way through
# arguments...
pass

# Prepare our target listing
results['targets'] = list()
while entries:
Expand All @@ -457,15 +525,29 @@ def parse_url(url):
# We're done
break

# Get our tenant from hostname
results['tenant'] = NotifyOffice365.unquote(results['host'])

# Assemble our secret key which is a combination of the host followed
# by all entries in the full path that follow up until the first email
results['secret'] = '/'.join(chain(
[NotifyOffice365.unquote(results['host'])],
[NotifyOffice365.unquote(x) for x in entries]))

# Get our tenant and client_id out of the user/pass
results['tenant'] = NotifyOffice365.unquote(results['user'])
results['client_id'] = NotifyOffice365.unquote(results['password'])
results['secret'] = '/'.join(
[NotifyOffice365.unquote(x) for x in entries])

# Assemble our client id from the user@hostname
if results['password']:
results['email'] = '{}@{}'.format(
NotifyOffice365.unquote(results['password']),
NotifyOffice365.unquote(results['host']),
)
# A tenant was specified in the user field, so over-ride
# the deffault host entry
results['tenant'] = NotifyOffice365.unquote(results['user'])

else:
results['email'] = '{}@{}'.format(
NotifyOffice365.unquote(results['user']),
NotifyOffice365.unquote(results['host']),
)

# OAuth2 ID
if 'oauth_id' in results['qsd'] and len(results['qsd']['oauth_id']):
Expand All @@ -480,6 +562,13 @@ def parse_url(url):
results['secret'] = \
NotifyOffice365.unquote(results['qsd']['oauth_secret'])

# Tenant
if 'from' in results['qsd'] and \
len(results['qsd']['from']):
# Extract the sending account's information
results['email'] = \
NotifyOffice365.unquote(results['qsd']['from'])

# Tenant
if 'tenant' in results['qsd'] and \
len(results['qsd']['tenant']):
Expand Down
40 changes: 34 additions & 6 deletions test/test_office365.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ def test_office365_general(mock_post):
plugins.NotifyBase.request_rate_per_sec = 0

# Initialize some generic (but valid) tokens
tenant = 'tenant.domain'
email_id = 'user@example.net'
client_id = 'aa-bb-cc-dd-ee'
secret = 'abcd/1234/abcd@ajd@/test'
targets = 'target@example.com'
Expand All @@ -67,9 +67,9 @@ def test_office365_general(mock_post):

# Instantiate our object
obj = Apprise.instantiate(
'o365://{tenant}:{client_id}@{secret}/{targets}'.format(
'o365://{email_id}/{client_id}/{secret}/{targets}'.format(
client_id=client_id,
tenant=tenant,
email_id=email_id,
secret=secret,
targets=targets))

Expand All @@ -81,20 +81,46 @@ def test_office365_general(mock_post):
with pytest.raises(TypeError):
# No secret
plugins.NotifyOffice365(
tenant='abc123',
email_id='abc123@example.com',
client_id='ab-cd-ef-gh',
secret=None,
targets=None,
)

with pytest.raises(TypeError):
# Invalid email_id
plugins.NotifyOffice365(
email_id=None,
client_id='ab-cd-ef-gh',
secret=secret,
targets=None,
)

with pytest.raises(TypeError):
# Invalid email_id
plugins.NotifyOffice365(
email_id='garbage',
client_id='ab-cd-ef-gh',
secret=secret,
targets=None,
)

# One of the targets are invalid
plugins.NotifyOffice365(
tenant='abc123',
email_id='abc123@example.com',
client_id='ab-cd-ef-gh',
secret='secret',
targets=('abc@gmail.com', 'garbage'),
)

# all of the targets are invalid
assert plugins.NotifyOffice365(
email_id='abc123@example.com',
client_id='ab-cd-ef-gh',
secret='secret',
targets=('invalid', 'garbage'),
).notify(body="test") is False


@mock.patch('requests.post')
def test_office365_authentication(mock_post):
Expand All @@ -107,6 +133,7 @@ def test_office365_authentication(mock_post):

# Initialize some generic (but valid) tokens
tenant = 'tenant.domain'
email_id = 'user@example.net'
client_id = 'aa-bb-cc-dd-ee'
secret = 'abcd/1234/abcd@ajd@/test'
targets = 'target@example.com'
Expand All @@ -132,9 +159,10 @@ def test_office365_authentication(mock_post):

# Instantiate our object
obj = Apprise.instantiate(
'o365://{tenant}:{client_id}@{secret}/{targets}'.format(
'o365://{tenant}:{email_id}/{client_id}/{secret}/{targets}'.format(
client_id=client_id,
tenant=tenant,
email_id=email_id,
secret=secret,
targets=targets))

Expand Down

0 comments on commit 8043282

Please sign in to comment.