attachment keyword in payload now supports Web Based URLs (#164)

This commit is contained in:
Chris Caron 2024-01-14 13:27:14 -05:00 committed by GitHub
parent 68fd580b84
commit 2b21ab71ef
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 580 additions and 37 deletions

View File

@ -147,16 +147,47 @@ curl -X POST \
-F attach2=@/my/path/to/Apprise.doc \ -F attach2=@/my/path/to/Apprise.doc \
http://localhost:8000/notify http://localhost:8000/notify
# This example shows how you can place the body among other parameters
# in the GET parameter and not the payload as another option.
curl -X POST -d 'urls=mailto://user:pass@gmail.com&body=test message' \ curl -X POST -d 'urls=mailto://user:pass@gmail.com&body=test message' \
-F @/path/to/your/attachment \ -F @/path/to/your/attachment \
http://localhost:8000/notify http://localhost:8000/notify
# The body is not required if an attachment is provided:
curl -X POST -d 'urls=mailto://user:pass@gmail.com' \
-F @/path/to/your/attachment \
http://localhost:8000/notify
# Send your notifications directly using JSON # Send your notifications directly using JSON
curl -X POST -d '{"urls": "mailto://user:pass@gmail.com", "body":"test message"}' \ curl -X POST -d '{"urls": "mailto://user:pass@gmail.com", "body":"test message"}' \
-H "Content-Type: application/json" \ -H "Content-Type: application/json" \
http://localhost:8000/notify http://localhost:8000/notify
``` ```
You can also send notifications that are URLs. Apprise will download the item so that it can send it along to all end points that should be notified about it.
```bash
# Use the 'attachment' parameter and send along a web request
curl -X POST \
-F 'urls=mailto://user:pass@gmail.com' \
-F attachment=https://i.redd.it/my2t4d2fx0u31.jpg \
http://localhost:8000/notify
# To send more then one URL, the following would work:
curl -X POST \
-F 'urls=mailto://user:pass@gmail.com' \
-F attachment=https://i.redd.it/my2t4d2fx0u31.jpg \
-F attachment=https://path/to/another/remote/file.pdf \
http://localhost:8000/notify
# Finally feel free to mix and match local files with external ones:
curl -X POST \
-F 'urls=mailto://user:pass@gmail.com' \
-F attachment=https://i.redd.it/my2t4d2fx0u31.jpg \
-F attachment=https://path/to/another/remote/file.pdf \
-F @/path/to/your/local/file/attachment \
http://localhost:8000/notify
```
### Persistent Storage Solution ### Persistent Storage Solution
You can pre-save all of your Apprise configuration and/or set of Apprise URLs and associate them with a `{KEY}` of your choosing. Once set, the configuration persists for retrieval by the `apprise` [CLI tool](https://github.com/caronc/apprise/wiki/CLI_Usage) or any other custom integration you've set up. The built in website with comes with a user interface that you can use to leverage these API calls as well. Those who wish to build their own application around this can use the following API end points: You can pre-save all of your Apprise configuration and/or set of Apprise URLs and associate them with a `{KEY}` of your choosing. Once set, the configuration persists for retrieval by the `apprise` [CLI tool](https://github.com/caronc/apprise/wiki/CLI_Usage) or any other custom integration you've set up. The built in website with comes with a user interface that you can use to leverage these API calls as well. Those who wish to build their own application around this can use the following API end points:

View File

@ -22,12 +22,21 @@
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE. # THE SOFTWARE.
import requests
from django.test import SimpleTestCase from django.test import SimpleTestCase
from unittest import mock from unittest import mock
from ..utils import Attachment from unittest.mock import mock_open
from ..utils import Attachment, HTTPAttachment
from ..utils import parse_attachments
from django.test.utils import override_settings from django.test.utils import override_settings
from tempfile import TemporaryDirectory from tempfile import TemporaryDirectory
from shutil import rmtree from shutil import rmtree
import base64
from unittest.mock import patch
from django.core.files.uploadedfile import SimpleUploadedFile
from os.path import dirname, join, getsize
SAMPLE_FILE = join(dirname(dirname(dirname(__file__))), 'static', 'logo.png')
class AttachmentTests(SimpleTestCase): class AttachmentTests(SimpleTestCase):
@ -54,10 +63,324 @@ class AttachmentTests(SimpleTestCase):
with mock.patch('os.makedirs', side_effect=OSError): with mock.patch('os.makedirs', side_effect=OSError):
with self.assertRaises(ValueError): with self.assertRaises(ValueError):
Attachment('file') Attachment('file')
with self.assertRaises(ValueError):
HTTPAttachment('web')
with mock.patch('tempfile.mkstemp', side_effect=FileNotFoundError): with mock.patch('tempfile.mkstemp', side_effect=FileNotFoundError):
with self.assertRaises(ValueError): with self.assertRaises(ValueError):
Attachment('file') Attachment('file')
with self.assertRaises(ValueError):
HTTPAttachment('web')
with mock.patch('os.remove', side_effect=FileNotFoundError):
a = Attachment('file')
# Force __del__ call to throw an exception which we gracefully
# handle
del a
a = HTTPAttachment('web')
a._path = 'abcd'
assert a.filename == 'web'
# Force __del__ call to throw an exception which we gracefully
# handle
del a
a = Attachment('file') a = Attachment('file')
assert a.filename assert a.filename
def test_form_file_attachment_parsing(self):
"""
Test the parsing of file attachments
"""
# Get ourselves a file to work with
files_request = {
'file1': SimpleUploadedFile(
"attach.txt", b"content here", content_type="text/plain")
}
result = parse_attachments(None, files_request)
assert isinstance(result, list)
assert len(result) == 1
# Test case where no filename was specified
files_request = {
'file1': SimpleUploadedFile(
" ", b"content here", content_type="text/plain")
}
result = parse_attachments(None, files_request)
assert isinstance(result, list)
assert len(result) == 1
# Test our case where we throw an error trying to open/read/write our
# attachment to disk
m = mock_open()
m.side_effect = OSError()
with patch('builtins.open', m):
with self.assertRaises(ValueError):
parse_attachments(None, files_request)
# Test a case where our attachment exceeds the maximum size we allow
# for
with override_settings(APPRISE_MAX_ATTACHMENT_SIZE=1):
files_request = {
'file1': SimpleUploadedFile(
"attach.txt",
b"some content more then 1 byte in length to pass along.",
content_type="text/plain")
}
with self.assertRaises(ValueError):
parse_attachments(None, files_request)
# Bad data provided in filename field
files_request = {
'file1': SimpleUploadedFile(
None, b"content here", content_type="text/plain")
}
with self.assertRaises(ValueError):
parse_attachments(None, files_request)
@patch('requests.get')
def test_direct_attachment_parsing(self, mock_get):
"""
Test the parsing of file attachments
"""
# Test the processing of file attachments
result = parse_attachments([], {})
assert isinstance(result, list)
assert len(result) == 0
# Response object
response = mock.Mock()
response.status_code = requests.codes.ok
response.raise_for_status.return_value = True
response.headers = {
'Content-Length': getsize(SAMPLE_FILE),
}
ref = {
'io': None,
}
def iter_content(chunk_size=1024, *args, **kwargs):
if not ref['io']:
ref['io'] = open(SAMPLE_FILE)
block = ref['io'].read(chunk_size)
if not block:
# Close for re-use
ref['io'].close()
ref['io'] = None
yield block
response.iter_content = iter_content
def test(*args, **kwargs):
return response
response.__enter__ = test
response.__exit__ = test
mock_get.return_value = response
# Support base64 encoding
attachment_payload = {
'base64': base64.b64encode(b'data to be encoded').decode('utf-8')
}
result = parse_attachments(attachment_payload, {})
assert isinstance(result, list)
assert len(result) == 1
# Support multi entries
attachment_payload = [
{
'base64': base64.b64encode(
b'data to be encoded 1').decode('utf-8'),
}, {
'base64': base64.b64encode(
b'data to be encoded 2').decode('utf-8'),
}, {
'base64': base64.b64encode(
b'data to be encoded 3').decode('utf-8'),
}
]
result = parse_attachments(attachment_payload, {})
assert isinstance(result, list)
assert len(result) == 3
# Support multi entries
attachment_payload = [
{
'url': 'http://localhost/my.attachment.3',
}, {
'url': 'http://localhost/my.attachment.2',
}, {
'url': 'http://localhost/my.attachment.1',
}
]
result = parse_attachments(attachment_payload, {})
assert isinstance(result, list)
assert len(result) == 3
# Garbage handling (integer, float, object, etc is invalid)
attachment_payload = 5
result = parse_attachments(attachment_payload, {})
assert isinstance(result, list)
assert len(result) == 0
attachment_payload = 5.5
result = parse_attachments(attachment_payload, {})
assert isinstance(result, list)
assert len(result) == 0
attachment_payload = object()
result = parse_attachments(attachment_payload, {})
assert isinstance(result, list)
assert len(result) == 0
# filename provided, but its empty (and/or contains whitespace)
attachment_payload = {
'base64': base64.b64encode(b'data to be encoded').decode('utf-8'),
'filename': ' '
}
result = parse_attachments(attachment_payload, {})
assert isinstance(result, list)
assert len(result) == 1
# filename too long
attachment_payload = {
'base64': base64.b64encode(b'data to be encoded').decode('utf-8'),
'filename': 'a' * 1000,
}
with self.assertRaises(ValueError):
parse_attachments(attachment_payload, {})
# filename invalid
attachment_payload = {
'base64': base64.b64encode(b'data to be encoded').decode('utf-8'),
'filename': 1,
}
with self.assertRaises(ValueError):
parse_attachments(attachment_payload, {})
attachment_payload = {
'base64': base64.b64encode(b'data to be encoded').decode('utf-8'),
'filename': None,
}
with self.assertRaises(ValueError):
parse_attachments(attachment_payload, {})
attachment_payload = {
'base64': base64.b64encode(b'data to be encoded').decode('utf-8'),
'filename': object(),
}
with self.assertRaises(ValueError):
parse_attachments(attachment_payload, {})
# List Entry with bad data
attachment_payload = [
None,
]
with self.assertRaises(ValueError):
parse_attachments(attachment_payload, {})
# We expect at least a 'base64' or something in our dict
attachment_payload = [
{},
]
with self.assertRaises(ValueError):
parse_attachments(attachment_payload, {})
# We can't parse entries that are not base64 but specified as
# though they are
attachment_payload = {
'base64': 'not-base-64',
}
with self.assertRaises(ValueError):
parse_attachments(attachment_payload, {})
# Support string; these become web requests
attachment_payload = \
"https://avatars.githubusercontent.com/u/850374?v=4"
result = parse_attachments(attachment_payload, {})
assert isinstance(result, list)
assert len(result) == 1
# Local files are not allowed
attachment_payload = "file:///etc/hosts"
with self.assertRaises(ValueError):
parse_attachments(attachment_payload, {})
attachment_payload = "/etc/hosts"
with self.assertRaises(ValueError):
parse_attachments(attachment_payload, {})
attachment_payload = "simply invalid"
with self.assertRaises(ValueError):
parse_attachments(attachment_payload, {})
# Test our case where we throw an error trying to write our attachment
# to disk
m = mock_open()
m.side_effect = OSError()
with patch('builtins.open', m):
with self.assertRaises(ValueError):
attachment_payload = b"some data to work with."
parse_attachments(attachment_payload, {})
# Test a case where our attachment exceeds the maximum size we allow
# for
with override_settings(APPRISE_MAX_ATTACHMENT_SIZE=1):
attachment_payload = \
b"some content more then 1 byte in length to pass along."
with self.assertRaises(ValueError):
parse_attachments(attachment_payload, {})
# Support byte data
attachment_payload = b"some content to pass along as an attachment."
result = parse_attachments(attachment_payload, {})
assert isinstance(result, list)
assert len(result) == 1
attachment_payload = [
# Request several images
"https://localhost/myotherfile.png",
"https://localhost/myfile.png"
]
result = parse_attachments(attachment_payload, {})
assert isinstance(result, list)
assert len(result) == 2
attachment_payload = [{
# Request several images
'url': "https://localhost/myotherfile.png",
}, {
'url': "https://localhost/myfile.png"
}]
result = parse_attachments(attachment_payload, {})
assert isinstance(result, list)
assert len(result) == 2
# Test pure binary payload (raw)
attachment_payload = [
b"some content to pass along as an attachment.",
b"some more content to pass along as an attachment.",
]
result = parse_attachments(attachment_payload, {})
assert isinstance(result, list)
assert len(result) == 2
def test_direct_attachment_parsing_nw(self):
"""
Test the parsing of file attachments with network availability
We test web requests that do not work or in accessible to access
this part of the test cases
"""
attachment_payload = [
# While we have a network in place, we're intentionally requesting
# URLs that do not exist (hopefully they don't anyway) as we want
# this test to fail.
"https://localhost/garbage/abcd1.png",
"https://localhost/garbage/abcd2.png",
]
with self.assertRaises(ValueError):
parse_attachments(attachment_payload, {})
# Support url encoding
attachment_payload = [{
'url': "https://localhost/garbage/abcd1.png",
}, {
'url': "https://localhost/garbage/abcd2.png",
}]
with self.assertRaises(ValueError):
parse_attachments(attachment_payload, {})

View File

@ -828,6 +828,25 @@ class NotifyTests(SimpleTestCase):
assert response.status_code == 424 assert response.status_code == 424
assert mock_notify.call_count == 2 assert mock_notify.call_count == 2
# One more test but we test our URL fetching
mock_notify.side_effect = True
# Reset our mock object
mock_notify.reset_mock()
# Preare our form data
form_data = {
'body': 'test notifiction',
'attachment': 'https://localhost/invalid/path/to/image.png',
}
# Send our notification
response = self.client.post(
'/notify/{}'.format(key), form_data)
# We fail because we couldn't retrieve our attachment
assert response.status_code == 400
assert mock_notify.call_count == 0
@mock.patch('apprise.Apprise.notify') @mock.patch('apprise.Apprise.notify')
def test_notify_by_loaded_urls_with_json(self, mock_notify): def test_notify_by_loaded_urls_with_json(self, mock_notify):
""" """

View File

@ -231,6 +231,25 @@ class StatelessNotifyTests(SimpleTestCase):
assert response.status_code == 424 assert response.status_code == 424
assert mock_notify.call_count == 2 assert mock_notify.call_count == 2
# Reset our mock object
mock_notify.reset_mock()
# Preare our form data
form_data = {
'body': 'test notifiction',
'urls': ', '.join([
'mailto://user:pass@hotmail.com',
'mailto://user:pass@gmail.com',
]),
'attachment': 'https://localhost/invalid/path/to/image.png',
}
# Send our notification
response = self.client.post('/notify', form_data)
# We fail because we couldn't retrieve our attachment
assert response.status_code == 400
assert mock_notify.call_count == 0
@override_settings(APPRISE_RECURSION_MAX=1) @override_settings(APPRISE_RECURSION_MAX=1)
@mock.patch('apprise.Apprise.notify') @mock.patch('apprise.Apprise.notify')
def test_stateless_notify_recursion(self, mock_notify): def test_stateless_notify_recursion(self, mock_notify):

View File

@ -60,6 +60,17 @@ class AppriseStoreMode(object):
DISABLED = 'disabled' DISABLED = 'disabled'
class AttachmentPayload(object):
"""
Defines the supported Attachment Payload Types
"""
# BASE64
BASE64 = 'base64'
# URL request
URL = 'url'
STORE_MODES = ( STORE_MODES = (
AppriseStoreMode.HASH, AppriseStoreMode.HASH,
AppriseStoreMode.SIMPLE, AppriseStoreMode.SIMPLE,
@ -79,7 +90,7 @@ class Attachment(A_MGR['file']):
Attachments Attachments
""" """
def __init__(self, filename, path=None, delete=True): def __init__(self, filename, path=None, delete=True, **kwargs):
""" """
Initialize our attachment Initialize our attachment
""" """
@ -108,7 +119,7 @@ class Attachment(A_MGR['file']):
self._path = path self._path = path
# Prepare our item # Prepare our item
super().__init__(path=self._path, name=filename) super().__init__(path=self._path, name=filename, **kwargs)
# Update our file size based on the settings value # Update our file size based on the settings value
self.max_file_size = settings.APPRISE_ATTACH_SIZE self.max_file_size = settings.APPRISE_ATTACH_SIZE
@ -136,6 +147,67 @@ class Attachment(A_MGR['file']):
pass pass
class HTTPAttachment(A_MGR['http']):
"""
A Light Weight Attachment Object for Auto-cleanup that wraps the Apprise
Web Attachments
"""
def __init__(self, filename, delete=True, **kwargs):
"""
Initialize our attachment
"""
self._filename = filename
self.delete = delete
self._path = None
try:
os.makedirs(settings.APPRISE_ATTACH_DIR, exist_ok=True)
except OSError:
# Permission error
raise ValueError('Could not create directory {}'.format(
settings.APPRISE_ATTACH_DIR))
try:
d, self._path = tempfile.mkstemp(dir=settings.APPRISE_ATTACH_DIR)
# Close our file descriptor
os.close(d)
except FileNotFoundError:
raise ValueError(
'Could not prepare {} attachment in {}'.format(
filename, settings.APPRISE_ATTACH_DIR))
# Prepare our item
super().__init__(name=filename, **kwargs)
# Update our file size based on the settings value
self.max_file_size = settings.APPRISE_ATTACH_SIZE
@property
def filename(self):
return self._filename
@property
def size(self):
"""
Return filesize
"""
return 0 if not self else os.stat(self._path).st_size
def __del__(self):
"""
De-Construtor is used to tidy up files during garbage collection
"""
if self.delete and self._path:
try:
os.remove(self._path)
except FileNotFoundError:
# no problem
pass
def parse_attachments(attachment_payload, files_request): def parse_attachments(attachment_payload, files_request):
""" """
Takes the payload provided in a `/notify` call and extracts the Takes the payload provided in a `/notify` call and extracts the
@ -148,11 +220,16 @@ def parse_attachments(attachment_payload, files_request):
# Attachment Count # Attachment Count
count = sum([ count = sum([
0 if not isinstance(attachment_payload, (tuple, list)) 0 if not isinstance(attachment_payload, (set, tuple, list))
else len(attachment_payload), else len(attachment_payload),
0 if not isinstance(files_request, dict) else len(files_request), 0 if not isinstance(files_request, dict) else len(files_request),
]) ])
if isinstance(attachment_payload, (dict, str, bytes)):
# Convert and adjust counter
attachment_payload = (attachment_payload, )
count += 1
if settings.APPRISE_MAX_ATTACHMENTS <= 0 or \ if settings.APPRISE_MAX_ATTACHMENTS <= 0 or \
(settings.APPRISE_MAX_ATTACHMENTS > 0 and (settings.APPRISE_MAX_ATTACHMENTS > 0 and
count > settings.APPRISE_MAX_ATTACHMENTS): count > settings.APPRISE_MAX_ATTACHMENTS):
@ -161,10 +238,9 @@ def parse_attachments(attachment_payload, files_request):
settings.APPRISE_MAX_ATTACHMENTS settings.APPRISE_MAX_ATTACHMENTS
if settings.APPRISE_MAX_ATTACHMENTS > 0 else 0) if settings.APPRISE_MAX_ATTACHMENTS > 0 else 0)
if isinstance(attachment_payload, (tuple, list)): if isinstance(attachment_payload, (tuple, list, set)):
for no, entry in enumerate(attachment_payload, start=1): for no, entry in enumerate(attachment_payload, start=1):
if isinstance(entry, (str, bytes)):
if isinstance(entry, str):
filename = "attachment.%.3d" % no filename = "attachment.%.3d" % no
elif isinstance(entry, dict): elif isinstance(entry, dict):
@ -180,7 +256,8 @@ def parse_attachments(attachment_payload, files_request):
elif not filename: elif not filename:
filename = "attachment.%.3d" % no filename = "attachment.%.3d" % no
except TypeError: except AttributeError:
# not a string that was provided
raise ValueError( raise ValueError(
"An invalid filename was provided for attachment %d" % "An invalid filename was provided for attachment %d" %
no) no)
@ -194,30 +271,70 @@ def parse_attachments(attachment_payload, files_request):
# #
# Prepare our Attachment # Prepare our Attachment
# #
attachment = Attachment(filename) if isinstance(entry, str):
if not re.match(r'^https?://.+', entry[:10], re.I):
# We failed to retrieve the product
raise ValueError(
"Failed to load attachment "
"%d (not web request): %s" % (no, entry))
try: attachment = HTTPAttachment(
with open(attachment.path, 'wb') as f: filename, **A_MGR['http'].parse_url(entry))
# Write our content to disk if not attachment:
f.write(base64.b64decode(entry["base64"])) # We failed to retrieve the attachment
raise ValueError(
"Failed to retrieve attachment %d: %s" % (no, entry))
except binascii.Error: else: # web, base64 or raw
# The file ws not base64 encoded attachment = Attachment(filename)
raise ValueError( try:
"Invalid filecontent was provided for attachment %s" % with open(attachment.path, 'wb') as f:
filename) # Write our content to disk
if isinstance(entry, dict) and \
AttachmentPayload.BASE64 in entry:
# BASE64
f.write(
base64.b64decode(
entry[AttachmentPayload.BASE64]))
except OSError: elif isinstance(entry, dict) and \
raise ValueError( AttachmentPayload.URL in entry:
"Could not write attachment %s to disk" % filename)
# attachment = HTTPAttachment(
# Some Validation filename, **A_MGR['http']
# .parse_url(entry[AttachmentPayload.URL]))
if settings.APPRISE_MAX_ATTACHMENT_SIZE > 0 and \ if not attachment:
attachment.size > settings.APPRISE_MAX_ATTACHMENT_SIZE: # We failed to retrieve the attachment
raise ValueError( raise ValueError(
"attachment %s's filesize is to large" % filename) "Failed to retrieve attachment "
"%d: %s" % (no, entry))
elif isinstance(entry, bytes):
# RAW
f.write(entry)
else:
raise ValueError(
"Invalid filetype was provided for "
"attachment %s" % filename)
except binascii.Error:
# The file ws not base64 encoded
raise ValueError(
"Invalid filecontent was provided for attachment %s" %
filename)
except OSError:
raise ValueError(
"Could not write attachment %s to disk" % filename)
#
# Some Validation
#
if settings.APPRISE_MAX_ATTACHMENT_SIZE > 0 and \
attachment.size > settings.APPRISE_MAX_ATTACHMENT_SIZE:
raise ValueError(
"attachment %s's filesize is to large" % filename)
# Add our attachment # Add our attachment
attachments.append(attachment) attachments.append(attachment)
@ -245,8 +362,7 @@ def parse_attachments(attachment_payload, files_request):
except (AttributeError, TypeError): except (AttributeError, TypeError):
raise ValueError( raise ValueError(
"An invalid filename was provided for attachment %d" % "An invalid filename was provided for attachment %d" % no)
no)
# #
# Prepare our Attachment # Prepare our Attachment

View File

@ -610,6 +610,10 @@ class NotifyView(View):
# Handle Attachments # Handle Attachments
attach = None attach = None
if not content.get('attachment') and 'attachment' in request.POST:
# Acquire attachments to work with them
content['attachment'] = request.POST.getlist('attachment')
if 'attachment' in content or request.FILES: if 'attachment' in content or request.FILES:
try: try:
attach = parse_attachments( attach = parse_attachments(
@ -974,8 +978,9 @@ class NotifyView(View):
) )
logger.info( logger.info(
'NOTIFY - %s - Proccessed%s KEY: %s', request.META['REMOTE_ADDR'], 'NOTIFY - %s - Delivered Notification(s) - %sKEY: %s',
'' if not tag else f' (Tags: {tag}),', key) request.META['REMOTE_ADDR'],
'' if not tag else f'Tags: {tag}, ', key)
# Return our retrieved content # Return our retrieved content
return HttpResponse( return HttpResponse(
@ -1011,14 +1016,22 @@ class StatelessNotifyView(View):
except (AttributeError, ValueError): except (AttributeError, ValueError):
# could not parse JSON response... # could not parse JSON response...
logger.warning(
'NOTIFY - %s - Invalid JSON Payload provided',
request.META['REMOTE_ADDR'])
return HttpResponse( return HttpResponse(
_('Invalid JSON specified.'), _('Invalid JSON specified.'),
status=ResponseCode.bad_request) status=ResponseCode.bad_request)
if not content: if not content:
# We could not handle the Content-Type # We could not handle the Content-Type
logger.warning(
'NOTIFY - %s - Invalid FORM Payload provided',
request.META['REMOTE_ADDR'])
return HttpResponse( return HttpResponse(
_('The message format is not supported.'), _('Bad FORM Payload provided.'),
status=ResponseCode.bad_request) status=ResponseCode.bad_request)
if not content.get('urls') and settings.APPRISE_STATELESS_URLS: if not content.get('urls') and settings.APPRISE_STATELESS_URLS:
@ -1031,15 +1044,22 @@ class StatelessNotifyView(View):
content.get('type', apprise.NotifyType.INFO) \ content.get('type', apprise.NotifyType.INFO) \
not in apprise.NOTIFY_TYPES: not in apprise.NOTIFY_TYPES:
logger.warning(
'NOTIFY - %s - Payload lacks minimum requirements',
request.META['REMOTE_ADDR'])
return HttpResponse( return HttpResponse(
_('An invalid payload was specified.'), _('Payload lacks minimum requirements.'),
status=ResponseCode.bad_request) status=ResponseCode.bad_request)
# Acquire our body format (if identified) # Acquire our body format (if identified)
body_format = content.get('format', apprise.NotifyFormat.TEXT) body_format = content.get('format', apprise.NotifyFormat.TEXT)
if body_format and body_format not in apprise.NOTIFY_FORMATS: if body_format and body_format not in apprise.NOTIFY_FORMATS:
logger.warning(
'NOTIFY - %s - Format parameter contains an unsupported '
'value (%s)', request.META['REMOTE_ADDR'], str(body_format))
return HttpResponse( return HttpResponse(
_('An invalid (body) format was specified.'), _('An invalid body input format was specified.'),
status=ResponseCode.bad_request) status=ResponseCode.bad_request)
# Prepare our keyword arguments (to be passed into an AppriseAsset # Prepare our keyword arguments (to be passed into an AppriseAsset
@ -1053,15 +1073,19 @@ class StatelessNotifyView(View):
kwargs['body_format'] = body_format kwargs['body_format'] = body_format
# Acquire our recursion count (if defined) # Acquire our recursion count (if defined)
recursion = request.headers.get('X-Apprise-Recursion-Count', 0)
try: try:
recursion = \ recursion = int(recursion)
int(request.headers.get('X-Apprise-Recursion-Count', 0))
if recursion < 0: if recursion < 0:
# We do not accept negative numbers # We do not accept negative numbers
raise TypeError("Invalid Recursion Value") raise TypeError("Invalid Recursion Value")
if recursion > settings.APPRISE_RECURSION_MAX: if recursion > settings.APPRISE_RECURSION_MAX:
logger.warning(
'NOTIFY - %s - Recursion limit reached (%d > %d)',
request.META['REMOTE_ADDR'], recursion,
settings.APPRISE_RECURSION_MAX)
return HttpResponse( return HttpResponse(
_('The recursion limit has been reached.'), _('The recursion limit has been reached.'),
status=ResponseCode.method_not_accepted) status=ResponseCode.method_not_accepted)
@ -1070,6 +1094,9 @@ class StatelessNotifyView(View):
kwargs['_recursion'] = recursion kwargs['_recursion'] = recursion
except (TypeError, ValueError): except (TypeError, ValueError):
logger.warning(
'NOTIFY - %s - Invalid recursion value (%s) provided',
request.META['REMOTE_ADDR'], str(recursion))
return HttpResponse( return HttpResponse(
_('An invalid recursion value was specified.'), _('An invalid recursion value was specified.'),
status=ResponseCode.bad_request) status=ResponseCode.bad_request)
@ -1100,6 +1127,10 @@ class StatelessNotifyView(View):
# Handle Attachments # Handle Attachments
attach = None attach = None
if not content.get('attachment') and 'attachment' in request.POST:
# Acquire attachments to work with them
content['attachment'] = request.POST.getlist('attachment')
if 'attachment' in content or request.FILES: if 'attachment' in content or request.FILES:
try: try:
attach = parse_attachments( attach = parse_attachments(
@ -1178,6 +1209,10 @@ class StatelessNotifyView(View):
_('One or more notification could not be sent.'), _('One or more notification could not be sent.'),
status=ResponseCode.failed_dependency) status=ResponseCode.failed_dependency)
logger.info(
'NOTIFY - %s - Delivered Stateless Notification(s)',
request.META['REMOTE_ADDR'])
# Return our retrieved content # Return our retrieved content
return HttpResponse( return HttpResponse(
_('Notification(s) sent.'), _('Notification(s) sent.'),