Skip to content

Commit

Permalink
Office 365 Email Full Name support (plus bcc and cc added)
Browse files Browse the repository at this point in the history
  • Loading branch information
caronc committed Aug 18, 2020
1 parent 8af15a9 commit 353864b
Show file tree
Hide file tree
Showing 3 changed files with 195 additions and 40 deletions.
6 changes: 3 additions & 3 deletions apprise/plugins/NotifyEmail.py
Original file line number Diff line number Diff line change
Expand Up @@ -409,7 +409,7 @@ def __init__(self, timeout=15, smtp_host=None, from_name=None,
# Acquire Blind Carbon Copies
self.bcc = set()

# For tracking our name lookups
# For tracking our email -> name lookups
self.names = {}

# Now we want to construct the To and From email
Expand Down Expand Up @@ -629,9 +629,9 @@ def send(self, body, title='', notify_type=NotifyType.INFO, attach=None,
self.logger.debug(
'Email From: {} <{}>'.format(from_name, self.from_addr))
self.logger.debug('Email To: {}'.format(to_addr))
if len(cc):
if cc:
self.logger.debug('Email Cc: {}'.format(', '.join(cc)))
if len(bcc):
if bcc:
self.logger.debug('Email Bcc: {}'.format(', '.join(bcc)))
self.logger.debug('Login ID: {}'.format(self.user))
self.logger.debug(
Expand Down
192 changes: 162 additions & 30 deletions apprise/plugins/NotifyOffice365.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@
from ..common import NotifyFormat
from ..common import NotifyType
from ..utils import is_email
from ..utils import parse_list
from ..utils import parse_emails
from ..utils import validate_regex
from ..AppriseLocale import gettext_lazy as _

Expand Down Expand Up @@ -152,6 +152,14 @@ class NotifyOffice365(NotifyBase):
'to': {
'alias_of': 'targets',
},
'cc': {
'name': _('Carbon Copy'),
'type': 'list:string',
},
'bcc': {
'name': _('Blind Carbon Copy'),
'type': 'list:string',
},
'oauth_id': {
'alias_of': 'client_id',
},
Expand All @@ -161,7 +169,7 @@ class NotifyOffice365(NotifyBase):
})

def __init__(self, tenant, email, client_id, secret,
targets=None, **kwargs):
targets=None, cc=None, bcc=None, **kwargs):
"""
Initialize Office 365 Object
"""
Expand All @@ -176,13 +184,15 @@ def __init__(self, tenant, email, client_id, secret,
self.logger.warning(msg)
raise TypeError(msg)

match = is_email(email)
if not match:
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)
self.email = match['email']

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

# Client Key (associated with generated OAuth2 Login)
self.client_id = validate_regex(
Expand All @@ -201,24 +211,73 @@ def __init__(self, tenant, email, client_id, secret,
self.logger.warning(msg)
raise TypeError(msg)

# For tracking our email -> name lookups
self.names = {}

# Acquire Carbon Copies
self.cc = set()

# Acquire Blind Carbon Copies
self.bcc = set()

# Parse our targets
self.targets = list()

targets = parse_list(targets)
if targets:
for target in targets:
# Validate targets and drop bad ones:
match = is_email(target)
if not match:
self.logger.warning(
'Dropped invalid email specified: {}'.format(target))
for recipient in parse_emails(targets):
# Validate recipients (to:) and drop bad ones:
result = is_email(recipient)
if result:
# Add our email to our target list
self.targets.append(
(result['name'] if result['name'] else False,
result['full_email']))
continue

# Add our email to our target list
self.targets.append(match['email'])
self.logger.warning(
'Dropped invalid To email ({}) specified.'
.format(recipient))

if not self.targets:
msg = 'There were no valid target emails to send to.'
self.logger.warning(msg)
raise TypeError(msg)

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

# Validate recipients (cc:) and drop bad ones:
for recipient in parse_emails(cc):
email = is_email(recipient)
if email:
self.cc.add(email['full_email'])

# Index our name (if one exists)
self.names[email['full_email']] = \
email['name'] if email['name'] else False
continue

self.logger.warning(
'Dropped invalid Carbon Copy email '
'({}) specified.'.format(recipient),
)

# Validate recipients (bcc:) and drop bad ones:
for recipient in parse_emails(bcc):
email = is_email(recipient)
if email:
self.bcc.add(email['full_email'])

# Index our name (if one exists)
self.names[email['full_email']] = \
email['name'] if email['name'] else False
continue

self.logger.warning(
'Dropped invalid Blind Carbon Copy email '
'({}) specified.'.format(recipient),
)

# Our token is acquired upon a successful login
self.token = None
Expand Down Expand Up @@ -258,36 +317,85 @@ def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
'SaveToSentItems': 'false'
}

# Create a copy of the targets list
targets = list(self.targets)
# Create a copy of the email list
emails = list(self.targets)

# Define our URL to post to
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
target = targets.pop(0)

# Prepare our email
payload['Message']['ToRecipients'] = [{
'EmailAddress': {
'Address': target
}
}]

while len(emails):
# authenticate ourselves if we aren't already; but this function
# also tracks if our token we have is still valid and will
# re-authenticate ourselves if nessisary.
if not self.authenticate():
# We could not authenticate ourselves; we're done
return False

# Get our email to notify
to_name, to_addr = emails.pop(0)

# Strip target out of cc list if in To or Bcc
cc = (self.cc - self.bcc - set([to_addr]))

# Strip target out of bcc list if in To
bcc = (self.bcc - set([to_addr]))

# Prepare our email
payload['Message']['ToRecipients'] = [{
'EmailAddress': {
'Address': to_addr
}
}]
if to_name:
# Apply our To Name
payload['Message']['ToRecipients'][0]['EmailAddress']['Name'] \
= to_name

self.logger.debug('Email To: {}'.format(to_addr))

if cc:
# Prepare our CC list
payload['Message']['CcRecipients'] = []
for addr in cc:
_payload = {'Address': addr}
if self.names.get(addr):
_payload['Name'] = self.names[addr]

# Store our address in our payload
payload['Message']['CcRecipients']\
.append({'EmailAddress': _payload})

self.logger.debug('Email Cc: {}'.format(', '.join(
['{}{}'.format(
'' if self.names.get(e)
else '{}: '.format(self.names[e]), e) for e in cc])))

if bcc:
# Prepare our CC list
payload['Message']['BccRecipients'] = []
for addr in bcc:
_payload = {'Address': addr}
if self.names.get(addr):
_payload['Name'] = self.names[addr]

# Store our address in our payload
payload['Message']['BccRecipients']\
.append({'EmailAddress': _payload})

self.logger.debug('Email Bcc: {}'.format(', '.join(
['{}{}'.format(
'' if self.names.get(e)
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')

# Test if we were okay
if not postokay:
has_error = True

Expand Down Expand Up @@ -455,6 +563,20 @@ def url(self, privacy=False, *args, **kwargs):
# Our URL parameters
params = self.url_parameters(privacy=privacy, *args, **kwargs)

if self.cc:
# Handle our Carbon Copy Addresses
params['cc'] = ','.join(
['{}{}'.format(
'' if not self.names.get(e)
else '{}:'.format(self.names[e]), e) for e in self.cc])

if self.bcc:
# Handle our Blind Carbon Copy Addresses
params['bcc'] = ','.join(
['{}{}'.format(
'' if not self.names.get(e)
else '{}:'.format(self.names[e]), e) for e in self.bcc])

return '{schema}://{tenant}:{email}/{client_id}/{secret}' \
'/{targets}/?{params}'.format(
schema=self.secure_protocol,
Expand All @@ -467,7 +589,9 @@ def url(self, privacy=False, *args, **kwargs):
self.secret, privacy, mode=PrivacyMode.Secret,
safe=''),
targets='/'.join(
[NotifyOffice365.quote(x, safe='') for x in self.targets]),
[NotifyOffice365.quote('{}{}'.format(
'' if not e[0] else '{}:'.format(e[0]), e[1]),
safe='') for e in self.targets]),
params=NotifyOffice365.urlencode(params))

@staticmethod
Expand Down Expand Up @@ -574,4 +698,12 @@ def parse_url(url):
results['targets'] += \
NotifyOffice365.parse_list(results['qsd']['to'])

# Handle Carbon Copy Addresses
if 'cc' in results['qsd'] and len(results['qsd']['cc']):
results['cc'] = results['qsd']['cc']

# Handle Blind Carbon Copy Addresses
if 'bcc' in results['qsd'] and len(results['qsd']['bcc']):
results['bcc'] = results['qsd']['bcc']

return results
37 changes: 30 additions & 7 deletions test/test_office365.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
# THE SOFTWARE.

import os
import six
import mock
import pytest
import requests
Expand Down Expand Up @@ -79,6 +80,27 @@ def test_office365_general(mock_post):
# Test our notification
assert obj.notify(title='title', body='test') is True

# Instantiate our object
obj = Apprise.instantiate(
'o365://{tenant}:{email}/{tenant}/{secret}/{targets}'
'?bcc={bcc}&cc={cc}'.format(
tenant=tenant,
email=email,
secret=secret,
targets=targets,
# Test the cc and bcc list (use good and bad email)
cc='Chuck Norris cnorris@yahoo.ca, invalid@!',
bcc='Bruce Willis bwillis@hotmail.com, invalid@!',
))

assert isinstance(obj, plugins.NotifyOffice365)

# Test our URL generation
assert isinstance(obj.url(), six.string_types)

# Test our notification
assert obj.notify(title='title', body='test') is True

with pytest.raises(TypeError):
# No secret
plugins.NotifyOffice365(
Expand Down Expand Up @@ -119,13 +141,14 @@ def test_office365_general(mock_post):
)

# all of the targets are invalid
assert plugins.NotifyOffice365(
email=email,
client_id=client_id,
tenant=tenant,
secret=secret,
targets=('invalid', 'garbage'),
).notify(body="test") is False
with pytest.raises(TypeError):
plugins.NotifyOffice365(
email=email,
client_id=client_id,
tenant=tenant,
secret=secret,
targets=('invalid', 'garbage'),
)


@mock.patch('requests.post')
Expand Down

0 comments on commit 353864b

Please sign in to comment.