SendGrid Attachment Support Added (#1190)

This commit is contained in:
Chris Caron 2024-08-27 16:32:52 -04:00 committed by GitHub
parent 3cb270cee8
commit ca50cb7820
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 146 additions and 1 deletions

View File

@ -29,6 +29,8 @@
import os
import time
import mimetypes
import base64
from .. import exception
from ..url import URLBase
from ..utils import parse_bool
from ..common import ContentLocation
@ -289,6 +291,37 @@ class AttachBase(URLBase):
return False if not retrieve_if_missing else self.download()
def base64(self, encoding='utf-8'):
"""
Returns the attachment object as a base64 string otherwise
None is returned if an error occurs.
If encoding is set to None, then it is not encoded when returned
"""
if not self:
# We could not access the attachment
self.logger.error(
'Could not access attachment {}.'.format(
self.url(privacy=True)))
raise exception.AppriseFileNotFound("Attachment Missing")
try:
with open(self.path, 'rb') as f:
# Prepare our Attachment in Base64
return base64.b64encode(f.read()).decode(encoding) \
if encoding else base64.b64encode(f.read())
except (TypeError, FileNotFoundError):
# We no longer have a path to open
raise exception.AppriseFileNotFound("Attachment Missing")
except (TypeError, OSError, IOError) as e:
self.logger.warning(
'An I/O error occurred while reading {}.'.format(
self.name if self else 'attachment'))
self.logger.debug('I/O Exception: %s' % str(e))
raise exception.AppriseDiskIOError("Attachment Access Error")
def invalidate(self):
"""
Release any temporary data that may be open by child classes.

View File

@ -50,6 +50,7 @@ import requests
from json import dumps
from .base import NotifyBase
from .. import exception
from ..common import NotifyFormat
from ..common import NotifyType
from ..utils import parse_list
@ -57,6 +58,7 @@ from ..utils import is_email
from ..utils import validate_regex
from ..locale import gettext_lazy as _
# Extend HTTP Error Messages
SENDGRID_HTTP_ERROR_MAP = {
401: 'Unauthorized - You do not have authorization to make the request.',
@ -90,6 +92,9 @@ class NotifySendGrid(NotifyBase):
# The default Email API URL to use
notify_url = 'https://api.sendgrid.com/v3/mail/send'
# Support attachments
attachment_support = True
# Allow 300 requests per minute.
# 60/300 = 0.2
request_rate_per_sec = 0.2
@ -297,7 +302,8 @@ class NotifySendGrid(NotifyBase):
"""
return len(self.targets)
def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
def send(self, body, title='', notify_type=NotifyType.INFO, attach=None,
**kwargs):
"""
Perform SendGrid Notification
"""
@ -331,6 +337,36 @@ class NotifySendGrid(NotifyBase):
}],
}
if attach and self.attachment_support:
attachments = []
# Send our attachments
for no, attachment in enumerate(attach, start=1):
try:
attachments.append({
"content": attachment.base64(),
"filename": attachment.name
if attachment.name else f'attach{no:03}.dat',
"type": "application/octet-stream",
"disposition": "attachment"
})
except exception.AppriseException:
# We could not access the attachment
self.logger.error(
'Could not access attachment {}.'.format(
attachment.url(privacy=True)))
return False
self.logger.debug(
'Appending SendGrid attachment {}'.format(
attachment.url(privacy=True)))
# Append our attachments to the payload
_payload.update({
'attachments': attachments,
})
if self.template:
_payload['template_id'] = self.template

View File

@ -29,6 +29,7 @@
import re
import time
import urllib
import pytest
from unittest import mock
from os.path import dirname
@ -37,6 +38,7 @@ from apprise.attachment.base import AttachBase
from apprise.attachment.file import AttachFile
from apprise import AppriseAttachment
from apprise.common import ContentLocation
from apprise import exception
# Disable logging for a cleaner testing output
import logging
@ -210,3 +212,37 @@ def test_attach_file():
# Test hosted configuration and that we can't add a valid file
aa = AppriseAttachment(location=ContentLocation.HOSTED)
assert aa.add(path) is False
def test_attach_file_base64():
"""
API: AttachFile() with base64 encoding
"""
# Simple gif test
path = join(TEST_VAR_DIR, 'apprise-test.gif')
response = AppriseAttachment.instantiate(path)
assert isinstance(response, AttachFile)
assert response.name == 'apprise-test.gif'
assert response.mimetype == 'image/gif'
# now test our base64 output
assert isinstance(response.base64(), str)
# No encoding if we choose
assert isinstance(response.base64(encoding=None), bytes)
# Error cases:
with mock.patch('os.path.isfile', return_value=False):
with pytest.raises(exception.AppriseFileNotFound):
response.base64()
with mock.patch("builtins.open", new_callable=mock.mock_open,
read_data="mocked file content") as mock_file:
mock_file.side_effect = FileNotFoundError
with pytest.raises(exception.AppriseFileNotFound):
response.base64()
mock_file.side_effect = OSError
with pytest.raises(exception.AppriseDiskIOError):
response.base64()

View File

@ -28,9 +28,13 @@
from unittest import mock
import os
import pytest
import requests
from apprise import Apprise
from apprise import NotifyType
from apprise import AppriseAttachment
from apprise.plugins.sendgrid import NotifySendGrid
from helpers import AppriseURLTester
@ -38,6 +42,9 @@ from helpers import AppriseURLTester
import logging
logging.disable(logging.CRITICAL)
# Attachment Directory
TEST_VAR_DIR = os.path.join(os.path.dirname(__file__), 'var')
# a test UUID we can use
UUID4 = '8b799edf-6f98-4d3a-9be7-2862fb4e5752'
@ -161,3 +168,36 @@ def test_plugin_sendgrid_edge_cases(mock_post, mock_get):
from_email='l2g@example.com',
bcc=('abc@def.com', '!invalid'),
cc=('abc@test.org', '!invalid')), NotifySendGrid)
@mock.patch('requests.get')
@mock.patch('requests.post')
def test_plugin_sendgrid_attachments(mock_post, mock_get):
"""
NotifySendGrid() Attachments
"""
request = mock.Mock()
request.status_code = requests.codes.ok
# Prepare Mock
mock_post.return_value = request
mock_get.return_value = request
path = os.path.join(TEST_VAR_DIR, 'apprise-test.gif')
attach = AppriseAttachment(path)
obj = Apprise.instantiate('sendgrid://abcd:user@example.com')
assert isinstance(obj, NotifySendGrid)
assert obj.notify(
body='body', title='title', notify_type=NotifyType.INFO,
attach=attach) is True
mock_post.reset_mock()
mock_get.reset_mock()
# Try again in a use case where we can't access the file
with mock.patch("builtins.open", side_effect=OSError):
assert obj.notify(
body='body', title='title', notify_type=NotifyType.INFO,
attach=attach) is False