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'):
|
||||
"""
|
||||
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):
|
||||
"""
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue