Skip to content

Commit

Permalink
SendGrid Attachment Support Added (#1190)
Browse files Browse the repository at this point in the history
  • Loading branch information
caronc committed Aug 27, 2024
1 parent 3cb270c commit ca50cb7
Show file tree
Hide file tree
Showing 4 changed files with 146 additions and 1 deletion.
33 changes: 33 additions & 0 deletions apprise/attachment/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -289,6 +291,37 @@ def exists(self, retrieve_if_missing=True):

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.
Expand Down
38 changes: 37 additions & 1 deletion apprise/plugins/sendgrid.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,13 +50,15 @@
from json import dumps

from .base import NotifyBase
from .. import exception
from ..common import NotifyFormat
from ..common import NotifyType
from ..utils import parse_list
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.',
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -297,7 +302,8 @@ def __len__(self):
"""
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
"""
Expand Down Expand Up @@ -331,6 +337,36 @@ def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
}],
}

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

Expand Down
36 changes: 36 additions & 0 deletions test/test_attach_file.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
import re
import time
import urllib
import pytest
from unittest import mock

from os.path import dirname
Expand All @@ -37,6 +38,7 @@
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
Expand Down Expand Up @@ -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()
40 changes: 40 additions & 0 deletions test/test_plugin_sendgrid.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,16 +28,23 @@

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

# Disable logging for a cleaner testing output
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'

Expand Down Expand Up @@ -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

0 comments on commit ca50cb7

Please sign in to comment.