mirror of https://github.com/caronc/apprise
Refactored Office 365 Plugin
parent
e9020e6f74
commit
bb5218afe6
|
@ -359,12 +359,27 @@ class AttachBase(URLBase):
|
||||||
|
|
||||||
def open(self, mode='rb'):
|
def open(self, mode='rb'):
|
||||||
"""
|
"""
|
||||||
return our file pointer and track it (we'll auto close later
|
return our file pointer and track it (we'll auto close later)
|
||||||
"""
|
"""
|
||||||
pointer = open(self.path, mode=mode)
|
pointer = open(self.path, mode=mode)
|
||||||
self.__pointers.add(pointer)
|
self.__pointers.add(pointer)
|
||||||
return pointer
|
return pointer
|
||||||
|
|
||||||
|
def chunk(self, size=5242880):
|
||||||
|
"""
|
||||||
|
A Generator that yield chunks of a file with the specified size.
|
||||||
|
|
||||||
|
By default the chunk size is set to 5MB (5242880 bytes)
|
||||||
|
"""
|
||||||
|
|
||||||
|
with self.open() as file:
|
||||||
|
while True:
|
||||||
|
chunk = file.read(size)
|
||||||
|
if not chunk:
|
||||||
|
break
|
||||||
|
|
||||||
|
yield chunk
|
||||||
|
|
||||||
def __enter__(self):
|
def __enter__(self):
|
||||||
"""
|
"""
|
||||||
support with keyword
|
support with keyword
|
||||||
|
@ -431,8 +446,16 @@ class AttachBase(URLBase):
|
||||||
Returns the filesize of the attachment.
|
Returns the filesize of the attachment.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
if not self:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
try:
|
||||||
return os.path.getsize(self.path) if self.path else 0
|
return os.path.getsize(self.path) if self.path else 0
|
||||||
|
|
||||||
|
except OSError:
|
||||||
|
# OSError can occur if the file is inaccessible
|
||||||
|
return 0
|
||||||
|
|
||||||
def __bool__(self):
|
def __bool__(self):
|
||||||
"""
|
"""
|
||||||
Allows the Apprise object to be wrapped in an based 'if statement'.
|
Allows the Apprise object to be wrapped in an based 'if statement'.
|
||||||
|
|
|
@ -34,37 +34,13 @@
|
||||||
# https://docs.microsoft.com/en-us/graph/api/user-sendmail\
|
# https://docs.microsoft.com/en-us/graph/api/user-sendmail\
|
||||||
# ?view=graph-rest-1.0&tabs=http
|
# ?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 your 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
|
import requests
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from json import loads
|
from json import loads
|
||||||
from json import dumps
|
from json import dumps
|
||||||
from .base import NotifyBase
|
from .base import NotifyBase
|
||||||
|
from .. import exception
|
||||||
from ..url import PrivacyMode
|
from ..url import PrivacyMode
|
||||||
from ..common import NotifyFormat
|
from ..common import NotifyFormat
|
||||||
from ..common import NotifyType
|
from ..common import NotifyType
|
||||||
|
@ -101,6 +77,16 @@ class NotifyOffice365(NotifyBase):
|
||||||
# Authentication URL
|
# Authentication URL
|
||||||
auth_url = 'https://login.microsoftonline.com/{tenant}/oauth2/v2.0/token'
|
auth_url = 'https://login.microsoftonline.com/{tenant}/oauth2/v2.0/token'
|
||||||
|
|
||||||
|
# Support attachments
|
||||||
|
attachment_support = True
|
||||||
|
|
||||||
|
# the maximum size an attachment can be for it to be allowed to be
|
||||||
|
# uploaded inline with the current email going out (one http post)
|
||||||
|
# Anything larger than this and a second PUT request is required to
|
||||||
|
# the outlook server to post the content through reference.
|
||||||
|
# Currently (as of 2024.10.06) this was documented to be 3MB
|
||||||
|
outlook_attachment_inline_max = 3145728
|
||||||
|
|
||||||
# Use all the direct application permissions you have configured for your
|
# Use all the direct application permissions you have configured for your
|
||||||
# app. The endpoint should issue a token for the ones associated with the
|
# app. The endpoint should issue a token for the ones associated with the
|
||||||
# resource you want to use.
|
# resource you want to use.
|
||||||
|
@ -113,8 +99,12 @@ class NotifyOffice365(NotifyBase):
|
||||||
|
|
||||||
# Define object templates
|
# Define object templates
|
||||||
templates = (
|
templates = (
|
||||||
'{schema}://{tenant}:{email}/{client_id}/{secret}',
|
# Send as user
|
||||||
'{schema}://{tenant}:{email}/{client_id}/{secret}/{targets}',
|
'{schema}://{email}/{tenant}/{client_id}/{secret}',
|
||||||
|
'{schema}://{email}/{tenant}/{client_id}/{secret}/{targets}',
|
||||||
|
# Send from 'me'
|
||||||
|
'{schema}://{tenant}/{client_id}/{secret}',
|
||||||
|
'{schema}://{tenant}/{client_id}/{secret}/{targets}',
|
||||||
)
|
)
|
||||||
|
|
||||||
# Define our template tokens
|
# Define our template tokens
|
||||||
|
@ -290,7 +280,8 @@ class NotifyOffice365(NotifyBase):
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
|
def send(self, body, title='', notify_type=NotifyType.INFO, attach=None,
|
||||||
|
**kwargs):
|
||||||
"""
|
"""
|
||||||
Perform Office 365 Notification
|
Perform Office 365 Notification
|
||||||
"""
|
"""
|
||||||
|
@ -310,25 +301,75 @@ class NotifyOffice365(NotifyBase):
|
||||||
|
|
||||||
# Prepare our payload
|
# Prepare our payload
|
||||||
payload = {
|
payload = {
|
||||||
'Message': {
|
'message': {
|
||||||
'Subject': title,
|
'subject': title,
|
||||||
'Body': {
|
'body': {
|
||||||
'ContentType': content_type,
|
'contentType': content_type,
|
||||||
'Content': body,
|
'content': body,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
'SaveToSentItems': 'false'
|
# Below takes a string (not bool) of either 'true' or 'false'
|
||||||
|
'saveToSentItems': 'true'
|
||||||
}
|
}
|
||||||
|
|
||||||
# Create a copy of the email list
|
# Create a copy of the email list
|
||||||
emails = list(self.targets)
|
emails = list(self.targets)
|
||||||
|
|
||||||
# Define our URL to post to
|
# Define our URL to post to
|
||||||
url = '{graph_url}/v1.0/users/{email}/sendmail'.format(
|
url = '{graph_url}/v1.0/me/sendMail'.format(
|
||||||
email=self.email,
|
graph_url=self.graph_url,
|
||||||
|
) if not self.self.email \
|
||||||
|
else '{graph_url}/v1.0/users/{userid}/sendMail'.format(
|
||||||
|
userid=self.email,
|
||||||
graph_url=self.graph_url,
|
graph_url=self.graph_url,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
attachments = []
|
||||||
|
too_large = []
|
||||||
|
if attach and self.attachment_support:
|
||||||
|
for no, attachment in enumerate(attach, start=1):
|
||||||
|
# Perform some simple error checking
|
||||||
|
if not attachment:
|
||||||
|
# We could not access the attachment
|
||||||
|
self.logger.error(
|
||||||
|
'Could not access Office 365 attachment {}.'.format(
|
||||||
|
attachment.url(privacy=True)))
|
||||||
|
return False
|
||||||
|
|
||||||
|
if len(attachment) > self.outlook_attachment_inline_max:
|
||||||
|
# Messages larger then xMB need to be uploaded after
|
||||||
|
too_large.append(attach)
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Prepare our Attachment in Base64
|
||||||
|
attachments.append({
|
||||||
|
"@odata.type": "#microsoft.graph.fileAttachment",
|
||||||
|
# Name of the attachment (as it should appear in email)
|
||||||
|
"name": attachment.name
|
||||||
|
if attachment.name else f'file{no:03}.dat',
|
||||||
|
# MIME type of the attachment
|
||||||
|
"contentType": "attachment.mimetype",
|
||||||
|
# Base64 Content
|
||||||
|
"contentBytes": attachment.base64(),
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
except exception.AppriseException:
|
||||||
|
# We could not access the attachment
|
||||||
|
self.logger.error(
|
||||||
|
'Could not access Office 365 attachment {}.'.format(
|
||||||
|
attachment.url(privacy=True)))
|
||||||
|
return False
|
||||||
|
|
||||||
|
self.logger.debug(
|
||||||
|
'Appending Office 365 attachment {}'.format(
|
||||||
|
attachment.url(privacy=True)))
|
||||||
|
|
||||||
|
if attachments:
|
||||||
|
# Store Attachments
|
||||||
|
payload['message']['attachments'] = attachments
|
||||||
|
|
||||||
while len(emails):
|
while len(emails):
|
||||||
# authenticate ourselves if we aren't already; but this function
|
# authenticate ourselves if we aren't already; but this function
|
||||||
# also tracks if our token we have is still valid and will
|
# also tracks if our token we have is still valid and will
|
||||||
|
@ -347,29 +388,29 @@ class NotifyOffice365(NotifyBase):
|
||||||
bcc = (self.bcc - set([to_addr]))
|
bcc = (self.bcc - set([to_addr]))
|
||||||
|
|
||||||
# Prepare our email
|
# Prepare our email
|
||||||
payload['Message']['ToRecipients'] = [{
|
payload['message']['toRecipients'] = [{
|
||||||
'EmailAddress': {
|
'emailAddress': {
|
||||||
'Address': to_addr
|
'Address': to_addr
|
||||||
}
|
}
|
||||||
}]
|
}]
|
||||||
if to_name:
|
if to_name:
|
||||||
# Apply our To Name
|
# Apply our To Name
|
||||||
payload['Message']['ToRecipients'][0]['EmailAddress']['Name'] \
|
payload['message']['toRecipients'][0]['emailAddress']['name'] \
|
||||||
= to_name
|
= to_name
|
||||||
|
|
||||||
self.logger.debug('Email To: {}'.format(to_addr))
|
self.logger.debug('Email To: {}'.format(to_addr))
|
||||||
|
|
||||||
if cc:
|
if cc:
|
||||||
# Prepare our CC list
|
# Prepare our CC list
|
||||||
payload['Message']['CcRecipients'] = []
|
payload['message']['ccRecipients'] = []
|
||||||
for addr in cc:
|
for addr in cc:
|
||||||
_payload = {'Address': addr}
|
_payload = {'Address': addr}
|
||||||
if self.names.get(addr):
|
if self.names.get(addr):
|
||||||
_payload['Name'] = self.names[addr]
|
_payload['Name'] = self.names[addr]
|
||||||
|
|
||||||
# Store our address in our payload
|
# Store our address in our payload
|
||||||
payload['Message']['CcRecipients']\
|
payload['message']['ccRecipients']\
|
||||||
.append({'EmailAddress': _payload})
|
.append({'emailAddress': _payload})
|
||||||
|
|
||||||
self.logger.debug('Email Cc: {}'.format(', '.join(
|
self.logger.debug('Email Cc: {}'.format(', '.join(
|
||||||
['{}{}'.format(
|
['{}{}'.format(
|
||||||
|
@ -378,15 +419,15 @@ class NotifyOffice365(NotifyBase):
|
||||||
|
|
||||||
if bcc:
|
if bcc:
|
||||||
# Prepare our CC list
|
# Prepare our CC list
|
||||||
payload['Message']['BccRecipients'] = []
|
payload['message']['bccRecipients'] = []
|
||||||
for addr in bcc:
|
for addr in bcc:
|
||||||
_payload = {'Address': addr}
|
_payload = {'address': addr}
|
||||||
if self.names.get(addr):
|
if self.names.get(addr):
|
||||||
_payload['Name'] = self.names[addr]
|
_payload['name'] = self.names[addr]
|
||||||
|
|
||||||
# Store our address in our payload
|
# Store our address in our payload
|
||||||
payload['Message']['BccRecipients']\
|
payload['message']['bccRecipients']\
|
||||||
.append({'EmailAddress': _payload})
|
.append({'emailAddress': _payload})
|
||||||
|
|
||||||
self.logger.debug('Email Bcc: {}'.format(', '.join(
|
self.logger.debug('Email Bcc: {}'.format(', '.join(
|
||||||
['{}{}'.format(
|
['{}{}'.format(
|
||||||
|
@ -402,6 +443,15 @@ class NotifyOffice365(NotifyBase):
|
||||||
if not postokay:
|
if not postokay:
|
||||||
has_error = True
|
has_error = True
|
||||||
|
|
||||||
|
elif too_large:
|
||||||
|
# We have large attachments now to upload and associate with
|
||||||
|
# our message. We need to prepare a draft message; acquire
|
||||||
|
# the message-id associated with it and then attach the file
|
||||||
|
# via this means.
|
||||||
|
|
||||||
|
# TODO
|
||||||
|
pass
|
||||||
|
|
||||||
return not has_error
|
return not has_error
|
||||||
|
|
||||||
def authenticate(self):
|
def authenticate(self):
|
||||||
|
@ -635,16 +685,44 @@ class NotifyOffice365(NotifyBase):
|
||||||
# of the secret key (since it can contain slashes in it)
|
# of the secret key (since it can contain slashes in it)
|
||||||
entries = NotifyOffice365.split_path(results['fullpath'])
|
entries = NotifyOffice365.split_path(results['fullpath'])
|
||||||
|
|
||||||
try:
|
# Initialize our tenant
|
||||||
# Get our client_id is the first entry on the path
|
results['tenant'] = None
|
||||||
results['client_id'] = NotifyOffice365.unquote(entries.pop(0))
|
# Initialize our email
|
||||||
|
results['email'] = None
|
||||||
|
|
||||||
except IndexError:
|
# From Email
|
||||||
# no problem, we may get the client_id another way through
|
if 'from' in results['qsd'] and \
|
||||||
# arguments...
|
len(results['qsd']['from']):
|
||||||
pass
|
# Extract the sending account's information
|
||||||
|
results['email'] = \
|
||||||
|
NotifyOffice365.unquote(results['qsd']['from'])
|
||||||
|
|
||||||
|
# Hostname is no longer part of `from` and possibly instead
|
||||||
|
# is the tenant id
|
||||||
|
entries.insert(0, NotifyOffice365.unquote(results['host']))
|
||||||
|
|
||||||
|
# Tenant
|
||||||
|
if 'tenant' in results['qsd'] and \
|
||||||
|
len(results['qsd']['tenant']):
|
||||||
|
# Extract the Tenant from the argument
|
||||||
|
results['tenant'] = \
|
||||||
|
NotifyOffice365.unquote(results['qsd']['tenant'])
|
||||||
|
|
||||||
|
# If tenant is occupied, then the user defined makes
|
||||||
|
# up our email
|
||||||
|
if not results['email'] and results['user']:
|
||||||
|
results['email'] = '{}@{}'.format(
|
||||||
|
NotifyOffice365.unquote(results['user']),
|
||||||
|
NotifyOffice365.unquote(results['host']),
|
||||||
|
)
|
||||||
|
|
||||||
|
elif not results['user']:
|
||||||
|
# Only tenant id specified (emails are sent 'from me')
|
||||||
|
results['tenant'] = NotifyOffice365.unquote(results['host'])
|
||||||
|
|
||||||
|
#
|
||||||
# Prepare our target listing
|
# Prepare our target listing
|
||||||
|
#
|
||||||
results['targets'] = list()
|
results['targets'] = list()
|
||||||
while entries:
|
while entries:
|
||||||
# Pop the last entry
|
# Pop the last entry
|
||||||
|
@ -662,36 +740,16 @@ class NotifyOffice365(NotifyBase):
|
||||||
# We're done
|
# We're done
|
||||||
break
|
break
|
||||||
|
|
||||||
# Initialize our tenant
|
|
||||||
results['tenant'] = None
|
|
||||||
|
|
||||||
# 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(
|
|
||||||
[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']),
|
|
||||||
)
|
|
||||||
# Update our tenant
|
|
||||||
results['tenant'] = NotifyOffice365.unquote(results['user'])
|
|
||||||
|
|
||||||
else:
|
|
||||||
# No tenant specified..
|
|
||||||
results['email'] = '{}@{}'.format(
|
|
||||||
NotifyOffice365.unquote(results['user']),
|
|
||||||
NotifyOffice365.unquote(results['host']),
|
|
||||||
)
|
|
||||||
|
|
||||||
# OAuth2 ID
|
# OAuth2 ID
|
||||||
if 'oauth_id' in results['qsd'] and len(results['qsd']['oauth_id']):
|
if 'oauth_id' in results['qsd'] and len(results['qsd']['oauth_id']):
|
||||||
# Extract the API Key from an argument
|
# Extract the API Key from an argument
|
||||||
results['client_id'] = \
|
results['client_id'] = \
|
||||||
NotifyOffice365.unquote(results['qsd']['oauth_id'])
|
NotifyOffice365.unquote(results['qsd']['oauth_id'])
|
||||||
|
|
||||||
|
elif entries:
|
||||||
|
# Get our client_id is the first entry on the path
|
||||||
|
results['client_id'] = NotifyOffice365.unquote(entries.pop(0))
|
||||||
|
|
||||||
# OAuth2 Secret
|
# OAuth2 Secret
|
||||||
if 'oauth_secret' in results['qsd'] and \
|
if 'oauth_secret' in results['qsd'] and \
|
||||||
len(results['qsd']['oauth_secret']):
|
len(results['qsd']['oauth_secret']):
|
||||||
|
@ -699,19 +757,12 @@ class NotifyOffice365(NotifyBase):
|
||||||
results['secret'] = \
|
results['secret'] = \
|
||||||
NotifyOffice365.unquote(results['qsd']['oauth_secret'])
|
NotifyOffice365.unquote(results['qsd']['oauth_secret'])
|
||||||
|
|
||||||
# Tenant
|
else:
|
||||||
if 'from' in results['qsd'] and \
|
# Assemble our secret key which is a combination of the host
|
||||||
len(results['qsd']['from']):
|
# followed by all entries in the full path that follow up until
|
||||||
# Extract the sending account's information
|
# the first email
|
||||||
results['email'] = \
|
results['secret'] = '/'.join(
|
||||||
NotifyOffice365.unquote(results['qsd']['from'])
|
[NotifyOffice365.unquote(x) for x in entries])
|
||||||
|
|
||||||
# Tenant
|
|
||||||
if 'tenant' in results['qsd'] and \
|
|
||||||
len(results['qsd']['tenant']):
|
|
||||||
# Extract the Tenant from the argument
|
|
||||||
results['tenant'] = \
|
|
||||||
NotifyOffice365.unquote(results['qsd']['tenant'])
|
|
||||||
|
|
||||||
# Support the 'to' variable so that we can support targets this way too
|
# Support the 'to' variable so that we can support targets this way too
|
||||||
# The 'to' makes it easier to use yaml configuration
|
# The 'to' makes it easier to use yaml configuration
|
||||||
|
|
Loading…
Reference in New Issue