mirror of
https://github.com/caronc/apprise.git
synced 2024-11-21 23:53:23 +01:00
SendGrid Attachment Support Added (#1190)
This commit is contained in:
parent
3cb270cee8
commit
ca50cb7820
@ -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.
|
||||
|
@ -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
|
||||
|
||||
|
@ -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()
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user