SendGrid Attachment Support Added (#1190)

pull/622/merge
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 os
import time import time
import mimetypes import mimetypes
import base64
from .. import exception
from ..url import URLBase from ..url import URLBase
from ..utils import parse_bool from ..utils import parse_bool
from ..common import ContentLocation from ..common import ContentLocation
@ -289,6 +291,37 @@ class AttachBase(URLBase):
return False if not retrieve_if_missing else self.download() 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): def invalidate(self):
""" """
Release any temporary data that may be open by child classes. Release any temporary data that may be open by child classes.

View File

@ -50,6 +50,7 @@ import requests
from json import dumps from json import dumps
from .base import NotifyBase from .base import NotifyBase
from .. import exception
from ..common import NotifyFormat from ..common import NotifyFormat
from ..common import NotifyType from ..common import NotifyType
from ..utils import parse_list from ..utils import parse_list
@ -57,6 +58,7 @@ from ..utils import is_email
from ..utils import validate_regex from ..utils import validate_regex
from ..locale import gettext_lazy as _ from ..locale import gettext_lazy as _
# Extend HTTP Error Messages # Extend HTTP Error Messages
SENDGRID_HTTP_ERROR_MAP = { SENDGRID_HTTP_ERROR_MAP = {
401: 'Unauthorized - You do not have authorization to make the request.', 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 # The default Email API URL to use
notify_url = 'https://api.sendgrid.com/v3/mail/send' notify_url = 'https://api.sendgrid.com/v3/mail/send'
# Support attachments
attachment_support = True
# Allow 300 requests per minute. # Allow 300 requests per minute.
# 60/300 = 0.2 # 60/300 = 0.2
request_rate_per_sec = 0.2 request_rate_per_sec = 0.2
@ -297,7 +302,8 @@ class NotifySendGrid(NotifyBase):
""" """
return len(self.targets) 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 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: if self.template:
_payload['template_id'] = self.template _payload['template_id'] = self.template

View File

@ -29,6 +29,7 @@
import re import re
import time import time
import urllib import urllib
import pytest
from unittest import mock from unittest import mock
from os.path import dirname from os.path import dirname
@ -37,6 +38,7 @@ from apprise.attachment.base import AttachBase
from apprise.attachment.file import AttachFile from apprise.attachment.file import AttachFile
from apprise import AppriseAttachment from apprise import AppriseAttachment
from apprise.common import ContentLocation from apprise.common import ContentLocation
from apprise import exception
# Disable logging for a cleaner testing output # Disable logging for a cleaner testing output
import logging import logging
@ -210,3 +212,37 @@ def test_attach_file():
# Test hosted configuration and that we can't add a valid file # Test hosted configuration and that we can't add a valid file
aa = AppriseAttachment(location=ContentLocation.HOSTED) aa = AppriseAttachment(location=ContentLocation.HOSTED)
assert aa.add(path) is False 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 from unittest import mock
import os
import pytest import pytest
import requests import requests
from apprise import Apprise
from apprise import NotifyType
from apprise import AppriseAttachment
from apprise.plugins.sendgrid import NotifySendGrid from apprise.plugins.sendgrid import NotifySendGrid
from helpers import AppriseURLTester from helpers import AppriseURLTester
@ -38,6 +42,9 @@ from helpers import AppriseURLTester
import logging import logging
logging.disable(logging.CRITICAL) logging.disable(logging.CRITICAL)
# Attachment Directory
TEST_VAR_DIR = os.path.join(os.path.dirname(__file__), 'var')
# a test UUID we can use # a test UUID we can use
UUID4 = '8b799edf-6f98-4d3a-9be7-2862fb4e5752' 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', from_email='l2g@example.com',
bcc=('abc@def.com', '!invalid'), bcc=('abc@def.com', '!invalid'),
cc=('abc@test.org', '!invalid')), NotifySendGrid) 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