Refactored Office 365 Plugin

pull/1225/head
Chris Caron 2024-10-10 01:12:04 -04:00
parent e9020e6f74
commit bb5218afe6
2 changed files with 167 additions and 93 deletions

View File

@ -359,12 +359,27 @@ class AttachBase(URLBase):
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)
self.__pointers.add(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):
"""
support with keyword
@ -431,7 +446,15 @@ class AttachBase(URLBase):
Returns the filesize of the attachment.
"""
return os.path.getsize(self.path) if self.path else 0
if not self:
return 0
try:
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):
"""

View File

@ -34,37 +34,13 @@
# 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 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
from datetime import datetime
from datetime import timedelta
from json import loads
from json import dumps
from .base import NotifyBase
from .. import exception
from ..url import PrivacyMode
from ..common import NotifyFormat
from ..common import NotifyType
@ -101,6 +77,16 @@ class NotifyOffice365(NotifyBase):
# Authentication URL
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
# app. The endpoint should issue a token for the ones associated with the
# resource you want to use.
@ -113,8 +99,12 @@ class NotifyOffice365(NotifyBase):
# Define object templates
templates = (
'{schema}://{tenant}:{email}/{client_id}/{secret}',
'{schema}://{tenant}:{email}/{client_id}/{secret}/{targets}',
# Send as user
'{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
@ -290,7 +280,8 @@ class NotifyOffice365(NotifyBase):
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
"""
@ -310,25 +301,75 @@ class NotifyOffice365(NotifyBase):
# Prepare our payload
payload = {
'Message': {
'Subject': title,
'Body': {
'ContentType': content_type,
'Content': body,
'message': {
'subject': title,
'body': {
'contentType': content_type,
'content': body,
},
},
'SaveToSentItems': 'false'
# Below takes a string (not bool) of either 'true' or 'false'
'saveToSentItems': 'true'
}
# 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,
url = '{graph_url}/v1.0/me/sendMail'.format(
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,
)
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):
# authenticate ourselves if we aren't already; but this function
# 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]))
# Prepare our email
payload['Message']['ToRecipients'] = [{
'EmailAddress': {
payload['message']['toRecipients'] = [{
'emailAddress': {
'Address': to_addr
}
}]
if to_name:
# Apply our To Name
payload['Message']['ToRecipients'][0]['EmailAddress']['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'] = []
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})
payload['message']['ccRecipients']\
.append({'emailAddress': _payload})
self.logger.debug('Email Cc: {}'.format(', '.join(
['{}{}'.format(
@ -378,15 +419,15 @@ class NotifyOffice365(NotifyBase):
if bcc:
# Prepare our CC list
payload['Message']['BccRecipients'] = []
payload['message']['bccRecipients'] = []
for addr in bcc:
_payload = {'Address': addr}
_payload = {'address': addr}
if self.names.get(addr):
_payload['Name'] = self.names[addr]
_payload['name'] = self.names[addr]
# Store our address in our payload
payload['Message']['BccRecipients']\
.append({'EmailAddress': _payload})
payload['message']['bccRecipients']\
.append({'emailAddress': _payload})
self.logger.debug('Email Bcc: {}'.format(', '.join(
['{}{}'.format(
@ -402,6 +443,15 @@ class NotifyOffice365(NotifyBase):
if not postokay:
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
def authenticate(self):
@ -635,16 +685,44 @@ class NotifyOffice365(NotifyBase):
# 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))
# Initialize our tenant
results['tenant'] = None
# Initialize our email
results['email'] = None
except IndexError:
# no problem, we may get the client_id another way through
# arguments...
pass
# From Email
if 'from' in results['qsd'] and \
len(results['qsd']['from']):
# 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
#
results['targets'] = list()
while entries:
# Pop the last entry
@ -662,36 +740,16 @@ class NotifyOffice365(NotifyBase):
# We're done
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
if 'oauth_id' in results['qsd'] and len(results['qsd']['oauth_id']):
# Extract the API Key from an argument
results['client_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
if 'oauth_secret' in results['qsd'] and \
len(results['qsd']['oauth_secret']):
@ -699,19 +757,12 @@ class NotifyOffice365(NotifyBase):
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']):
# Extract the Tenant from the argument
results['tenant'] = \
NotifyOffice365.unquote(results['qsd']['tenant'])
else:
# 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])
# Support the 'to' variable so that we can support targets this way too
# The 'to' makes it easier to use yaml configuration