From 98fb4865fc108c812f3ae72eee67824666886e8d Mon Sep 17 00:00:00 2001 From: Chris Caron Date: Tue, 27 Aug 2024 18:58:56 -0400 Subject: [PATCH] Refactored base64 attachment handling (#1191) --- apprise/attachment/base.py | 4 ++-- apprise/attachment/memory.py | 19 +++++++++++++++ apprise/plugins/apprise_api.py | 40 ++++++++++++++++++------------- apprise/plugins/custom_form.py | 3 ++- apprise/plugins/custom_json.py | 37 ++++++++++++++-------------- apprise/plugins/custom_xml.py | 44 ++++++++++++++++++---------------- apprise/plugins/email.py | 8 +++++-- apprise/plugins/mailgun.py | 8 ++++++- apprise/plugins/pushbullet.py | 5 ++-- apprise/plugins/pushsafer.py | 41 ++++++++++++++++--------------- apprise/plugins/sendgrid.py | 12 ++++++++-- apprise/plugins/ses.py | 7 ++++-- apprise/plugins/signal_api.py | 23 +++++++++--------- apprise/plugins/slack.py | 5 ++-- apprise/plugins/smseagle.py | 32 +++++++++++++------------ apprise/plugins/smtp2go.py | 38 +++++++++++++++-------------- apprise/plugins/sparkpost.py | 42 ++++++++++++++++---------------- apprise/plugins/twitter.py | 10 +++++--- test/test_attach_http.py | 32 +++++++++++++++++++++++++ test/test_attach_memory.py | 10 ++++++++ test/test_plugin_sendgrid.py | 6 +++++ 21 files changed, 270 insertions(+), 156 deletions(-) diff --git a/apprise/attachment/base.py b/apprise/attachment/base.py index 11b7f8c4..a0f74f9a 100644 --- a/apprise/attachment/base.py +++ b/apprise/attachment/base.py @@ -291,7 +291,7 @@ class AttachBase(URLBase): return False if not retrieve_if_missing else self.download() - def base64(self, encoding='utf-8'): + def base64(self, encoding='ascii'): """ Returns the attachment object as a base64 string otherwise None is returned if an error occurs. @@ -306,7 +306,7 @@ class AttachBase(URLBase): raise exception.AppriseFileNotFound("Attachment Missing") try: - with open(self.path, 'rb') as f: + with self.open() as f: # Prepare our Attachment in Base64 return base64.b64encode(f.read()).decode(encoding) \ if encoding else base64.b64encode(f.read()) diff --git a/apprise/attachment/memory.py b/apprise/attachment/memory.py index 94645f26..c7d5dca2 100644 --- a/apprise/attachment/memory.py +++ b/apprise/attachment/memory.py @@ -29,7 +29,9 @@ import re import os import io +import base64 from .base import AttachBase +from .. import exception from ..common import ContentLocation from ..locale import gettext_lazy as _ import uuid @@ -145,6 +147,23 @@ class AttachMemory(AttachBase): return True + def base64(self, encoding='ascii'): + """ + We need to over-ride this since the base64 sub-library seems to close + our file descriptor making it no longer referencable. + """ + + 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") + self._data.seek(0, 0) + + return base64.b64encode(self._data.read()).decode(encoding) \ + if encoding else base64.b64encode(self._data.read()) + def invalidate(self): """ Removes data diff --git a/apprise/plugins/apprise_api.py b/apprise/plugins/apprise_api.py index fd71236b..d6156438 100644 --- a/apprise/plugins/apprise_api.py +++ b/apprise/plugins/apprise_api.py @@ -29,8 +29,8 @@ import re import requests from json import dumps -import base64 +from .. import exception from .base import NotifyBase from ..url import PrivacyMode from ..common import NotifyType @@ -261,39 +261,45 @@ class NotifyAppriseAPI(NotifyBase): if not attachment: # We could not access the attachment self.logger.error( - 'Could not access attachment {}.'.format( + 'Could not access Apprise API attachment {}.'.format( attachment.url(privacy=True))) return False try: + # Our Attachment filename + filename = attachment.name \ + if attachment.name else f'file{no:03}.dat' + if self.method == AppriseAPIMethod.JSON: - with open(attachment.path, 'rb') as f: - # Output must be in a DataURL format (that's what - # PushSafer calls it): - attachments.append({ - 'filename': attachment.name, - 'base64': base64.b64encode(f.read()) - .decode('utf-8'), - 'mimetype': attachment.mimetype, - }) + # Output must be in a DataURL format (that's what + # PushSafer calls it): + attachments.append({ + "filename": filename, + 'base64': attachment.base64(), + 'mimetype': attachment.mimetype, + }) else: # AppriseAPIMethod.FORM files.append(( 'file{:02d}'.format(no), ( - attachment.name, + filename, open(attachment.path, 'rb'), attachment.mimetype, ) )) - except (OSError, IOError) as e: - self.logger.warning( - 'An I/O error occurred while reading {}.'.format( - attachment.name if attachment else 'attachment')) - self.logger.debug('I/O Exception: %s' % str(e)) + except (TypeError, OSError, exception.AppriseException): + # We could not access the attachment + self.logger.error( + 'Could not access AppriseAPI attachment {}.'.format( + attachment.url(privacy=True))) return False + self.logger.debug( + 'Appending AppriseAPI attachment {}'.format( + attachment.url(privacy=True))) + # prepare Apprise API Object payload = { # Apprise API Payload diff --git a/apprise/plugins/custom_form.py b/apprise/plugins/custom_form.py index 05fe51d1..e9ffcbbb 100644 --- a/apprise/plugins/custom_form.py +++ b/apprise/plugins/custom_form.py @@ -302,7 +302,8 @@ class NotifyForm(NotifyBase): files.append(( self.attach_as.format(no) if self.attach_multi_support else self.attach_as, ( - attachment.name, + attachment.name + if attachment.name else f'file{no:03}.dat', open(attachment.path, 'rb'), attachment.mimetype) )) diff --git a/apprise/plugins/custom_json.py b/apprise/plugins/custom_json.py index 25b4467d..03585c9e 100644 --- a/apprise/plugins/custom_json.py +++ b/apprise/plugins/custom_json.py @@ -27,9 +27,9 @@ # POSSIBILITY OF SUCH DAMAGE. import requests -import base64 from json import dumps +from .. import exception from .base import NotifyBase from ..url import PrivacyMode from ..common import NotifyImageSize @@ -213,33 +213,34 @@ class NotifyJSON(NotifyBase): # Track our potential attachments attachments = [] if attach and self.attachment_support: - for attachment in attach: + for no, attachment in enumerate(attach, start=1): # Perform some simple error checking if not attachment: # We could not access the attachment self.logger.error( - 'Could not access attachment {}.'.format( + 'Could not access Custom JSON attachment {}.'.format( attachment.url(privacy=True))) return False try: - with open(attachment.path, 'rb') as f: - # Output must be in a DataURL format (that's what - # PushSafer calls it): - attachments.append({ - 'filename': attachment.name, - 'base64': base64.b64encode(f.read()) - .decode('utf-8'), - 'mimetype': attachment.mimetype, - }) - - except (OSError, IOError) as e: - self.logger.warning( - 'An I/O error occurred while reading {}.'.format( - attachment.name if attachment else 'attachment')) - self.logger.debug('I/O Exception: %s' % str(e)) + attachments.append({ + "filename": attachment.name + if attachment.name else f'file{no:03}.dat', + 'base64': attachment.base64(), + 'mimetype': attachment.mimetype, + }) + + except exception.AppriseException: + # We could not access the attachment + self.logger.error( + 'Could not access Custom JSON attachment {}.'.format( + attachment.url(privacy=True))) return False + self.logger.debug( + 'Appending Custom JSON attachment {}'.format( + attachment.url(privacy=True))) + # Prepare JSON Object payload = { JSONPayloadField.VERSION: self.json_version, diff --git a/apprise/plugins/custom_xml.py b/apprise/plugins/custom_xml.py index f72e9a1a..8bfff3ec 100644 --- a/apprise/plugins/custom_xml.py +++ b/apprise/plugins/custom_xml.py @@ -28,8 +28,8 @@ import re import requests -import base64 +from .. import exception from .base import NotifyBase from ..url import PrivacyMode from ..common import NotifyImageSize @@ -287,35 +287,39 @@ class NotifyXML(NotifyBase): attachments = [] if attach and self.attachment_support: - for attachment in attach: + for no, attachment in enumerate(attach, start=1): # Perform some simple error checking if not attachment: # We could not access the attachment self.logger.error( - 'Could not access attachment {}.'.format( + 'Could not access Custom XML attachment {}.'.format( attachment.url(privacy=True))) return False try: - with open(attachment.path, 'rb') as f: - # Prepare our Attachment in Base64 - entry = \ - ''.format( - NotifyXML.escape_html( - attachment.name, whitespace=False), - NotifyXML.escape_html( - attachment.mimetype, whitespace=False)) - entry += base64.b64encode(f.read()).decode('utf-8') - entry += '' - attachments.append(entry) - - except (OSError, IOError) as e: - self.logger.warning( - 'An I/O error occurred while reading {}.'.format( - attachment.name if attachment else 'attachment')) - self.logger.debug('I/O Exception: %s' % str(e)) + # Prepare our Attachment in Base64 + entry = \ + ''.format( + NotifyXML.escape_html( + attachment.name if attachment.name + else f'file{no:03}.dat', whitespace=False), + NotifyXML.escape_html( + attachment.mimetype, whitespace=False)) + entry += attachment.base64() + entry += '' + attachments.append(entry) + + except exception.AppriseException: + # We could not access the attachment + self.logger.error( + 'Could not access Custom XML attachment {}.'.format( + attachment.url(privacy=True))) return False + self.logger.debug( + 'Appending Custom XML attachment {}'.format( + attachment.url(privacy=True))) + # Update our xml_attachments record: xml_attachments = \ '' + \ diff --git a/apprise/plugins/email.py b/apprise/plugins/email.py index f720c426..2e423916 100644 --- a/apprise/plugins/email.py +++ b/apprise/plugins/email.py @@ -799,7 +799,7 @@ class NotifyEmail(NotifyBase): mixed = MIMEMultipart("mixed") mixed.attach(base) # Now store our attachments - for attachment in attach: + for no, attachment in enumerate(attach, start=1): if not attachment: # We could not load the attachment; take an early # exit since this isn't what the end user wanted @@ -819,10 +819,14 @@ class NotifyEmail(NotifyBase): app = MIMEApplication(abody.read()) app.set_type(attachment.mimetype) + # Prepare our attachment name + filename = attachment.name \ + if attachment.name else f'file{no:03}.dat' + app.add_header( 'Content-Disposition', 'attachment; filename="{}"'.format( - Header(attachment.name, 'utf-8')), + Header(filename, 'utf-8')), ) mixed.attach(app) base = mixed diff --git a/apprise/plugins/mailgun.py b/apprise/plugins/mailgun.py index 4b73957a..b6818395 100644 --- a/apprise/plugins/mailgun.py +++ b/apprise/plugins/mailgun.py @@ -383,9 +383,15 @@ class NotifyMailgun(NotifyBase): self.logger.debug( 'Preparing Mailgun attachment {}'.format( attachment.url(privacy=True))) + + # Prepare our filename + filename = attachment.name \ + if attachment.name \ + else 'file{no:03}.dat'.format(no=idx + 1) + try: files['attachment[{}]'.format(idx)] = \ - (attachment.name, open(attachment.path, 'rb')) + (filename, open(attachment.path, 'rb')) except (OSError, IOError) as e: self.logger.warning( diff --git a/apprise/plugins/pushbullet.py b/apprise/plugins/pushbullet.py index 9f2226f3..2b88bbed 100644 --- a/apprise/plugins/pushbullet.py +++ b/apprise/plugins/pushbullet.py @@ -152,7 +152,7 @@ class NotifyPushBullet(NotifyBase): if attach and self.attachment_support: # We need to upload our payload first so that we can source it # in remaining messages - for attachment in attach: + for no, attachment in enumerate(attach, start=1): # Perform some simple error checking if not attachment: @@ -168,7 +168,8 @@ class NotifyPushBullet(NotifyBase): # prepare payload payload = { - 'file_name': attachment.name, + 'file_name': attachment.name + if attachment.name else f'file{no:03}.dat', 'file_type': attachment.mimetype, } # First thing we need to do is make a request so that we can diff --git a/apprise/plugins/pushsafer.py b/apprise/plugins/pushsafer.py index 7d4052c0..dd5a6c82 100644 --- a/apprise/plugins/pushsafer.py +++ b/apprise/plugins/pushsafer.py @@ -26,11 +26,11 @@ # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. -import base64 import requests from json import loads from .base import NotifyBase +from .. import exception from ..common import NotifyType from ..utils import parse_list from ..utils import validate_regex @@ -548,12 +548,12 @@ class NotifyPushSafer(NotifyBase): if attach and self.attachment_support: # We need to upload our payload first so that we can source it # in remaining messages - for attachment in attach: + for no, attachment in enumerate(attach, start=1): # prepare payload if not attachment: # We could not access the attachment self.logger.error( - 'Could not access attachment {}.'.format( + 'Could not access PushSafer attachment {}.'.format( attachment.url(privacy=True))) return False @@ -569,24 +569,27 @@ class NotifyPushSafer(NotifyBase): attachment.url(privacy=True))) try: - with open(attachment.path, 'rb') as f: - # Output must be in a DataURL format (that's what - # PushSafer calls it): - attachment = ( - attachment.name, - 'data:{};base64,{}'.format( - attachment.mimetype, - base64.b64encode(f.read()))) - - except (OSError, IOError) as e: - self.logger.warning( - 'An I/O error occurred while reading {}.'.format( - attachment.name if attachment else 'attachment')) - self.logger.debug('I/O Exception: %s' % str(e)) + # Output must be in a DataURL format (that's what + # PushSafer calls it): + attachments.append(( + attachment.name + if attachment.name else f'file{no:03}.dat', + 'data:{};base64,{}'.format( + attachment.mimetype, + attachment.base64(), + ) + )) + + except exception.AppriseException: + # We could not access the attachment + self.logger.error( + 'Could not access PushSafer attachment {}.'.format( + attachment.url(privacy=True))) return False - # Save our pre-prepared payload for attachment posting - attachments.append(attachment) + self.logger.debug( + 'Appending PushSafer attachment {}'.format( + attachment.url(privacy=True))) # Create a copy of the targets list targets = list(self.targets) diff --git a/apprise/plugins/sendgrid.py b/apprise/plugins/sendgrid.py index 627815c7..b4f92e9f 100644 --- a/apprise/plugins/sendgrid.py +++ b/apprise/plugins/sendgrid.py @@ -342,11 +342,19 @@ class NotifySendGrid(NotifyBase): # Send our attachments for no, attachment in enumerate(attach, start=1): + # Perform some simple error checking + if not attachment: + # We could not access the attachment + self.logger.error( + 'Could not access SendGrid attachment {}.'.format( + attachment.url(privacy=True))) + return False + try: attachments.append({ "content": attachment.base64(), "filename": attachment.name - if attachment.name else f'attach{no:03}.dat', + if attachment.name else f'file{no:03}.dat', "type": "application/octet-stream", "disposition": "attachment" }) @@ -354,7 +362,7 @@ class NotifySendGrid(NotifyBase): except exception.AppriseException: # We could not access the attachment self.logger.error( - 'Could not access attachment {}.'.format( + 'Could not access SendGrid attachment {}.'.format( attachment.url(privacy=True))) return False diff --git a/apprise/plugins/ses.py b/apprise/plugins/ses.py index 5fe4a369..30e59702 100644 --- a/apprise/plugins/ses.py +++ b/apprise/plugins/ses.py @@ -448,7 +448,7 @@ class NotifySES(NotifyBase): base.attach(content) # Now store our attachments - for attachment in attach: + for no, attachment in enumerate(attach, start=1): if not attachment: # We could not load the attachment; take an early # exit since this isn't what the end user wanted @@ -468,10 +468,13 @@ class NotifySES(NotifyBase): app = MIMEApplication(abody.read()) app.set_type(attachment.mimetype) + filename = attachment.name \ + if attachment.name else f'file{no:03}.dat' + app.add_header( 'Content-Disposition', 'attachment; filename="{}"'.format( - Header(attachment.name, 'utf-8')), + Header(filename, 'utf-8')), ) base.attach(app) diff --git a/apprise/plugins/signal_api.py b/apprise/plugins/signal_api.py index 5795e0cf..9a019123 100644 --- a/apprise/plugins/signal_api.py +++ b/apprise/plugins/signal_api.py @@ -29,10 +29,10 @@ import re import requests from json import dumps -import base64 from .base import NotifyBase from ..common import NotifyType +from .. import exception from ..utils import is_phone_no from ..utils import parse_phone_no from ..utils import parse_bool @@ -239,23 +239,24 @@ class NotifySignalAPI(NotifyBase): if not attachment: # We could not access the attachment self.logger.error( - 'Could not access attachment {}.'.format( + 'Could not access Signal API attachment {}.'.format( attachment.url(privacy=True))) return False try: - with open(attachment.path, 'rb') as f: - # Prepare our Attachment in Base64 - attachments.append( - base64.b64encode(f.read()).decode('utf-8')) + attachments.append(attachment.base64()) - except (OSError, IOError) as e: - self.logger.warning( - 'An I/O error occurred while reading {}.'.format( - attachment.name if attachment else 'attachment')) - self.logger.debug('I/O Exception: %s' % str(e)) + except exception.AppriseException: + # We could not access the attachment + self.logger.error( + 'Could not access Signal API attachment {}.'.format( + attachment.url(privacy=True))) return False + self.logger.debug( + 'Appending Signal API attachment {}'.format( + attachment.url(privacy=True))) + # Prepare our headers headers = { 'User-Agent': self.app_id, diff --git a/apprise/plugins/slack.py b/apprise/plugins/slack.py index fb4a8b6e..11d78e4c 100644 --- a/apprise/plugins/slack.py +++ b/apprise/plugins/slack.py @@ -646,7 +646,7 @@ class NotifySlack(NotifyBase): if attach and self.attachment_support and \ self.mode is SlackMode.BOT and attach_channel_list: # Send our attachments (can only be done in bot mode) - for attachment in attach: + for no, attachment in enumerate(attach, start=1): # Perform some simple error checking if not attachment: @@ -663,7 +663,8 @@ class NotifySlack(NotifyBase): # Get the URL to which to upload the file. # https://api.slack.com/methods/files.getUploadURLExternal _params = { - 'filename': attachment.name, + 'filename': attachment.name + if attachment.name else f'file{no:03}.dat', 'length': len(attachment), } _url = self.api_url.format('files.getUploadURLExternal') diff --git a/apprise/plugins/smseagle.py b/apprise/plugins/smseagle.py index 81308345..15943310 100644 --- a/apprise/plugins/smseagle.py +++ b/apprise/plugins/smseagle.py @@ -29,11 +29,11 @@ import re import requests from json import dumps, loads -import base64 from itertools import chain from .base import NotifyBase from ..common import NotifyType +from .. import exception from ..utils import validate_regex from ..utils import is_phone_no from ..utils import parse_phone_no @@ -345,7 +345,7 @@ class NotifySMSEagle(NotifyBase): if not attachment: # We could not access the attachment self.logger.error( - 'Could not access attachment {}.'.format( + 'Could not access SMSEagle attachment {}.'.format( attachment.url(privacy=True))) return False @@ -357,21 +357,23 @@ class NotifySMSEagle(NotifyBase): continue try: - with open(attachment.path, 'rb') as f: - # Prepare our Attachment in Base64 - attachments.append({ - 'content_type': attachment.mimetype, - 'content': base64.b64encode( - f.read()).decode('utf-8'), - }) - - except (OSError, IOError) as e: - self.logger.warning( - 'An I/O error occurred while reading {}.'.format( - attachment.name if attachment else 'attachment')) - self.logger.debug('I/O Exception: %s' % str(e)) + # Prepare our Attachment in Base64 + attachments.append({ + 'content_type': attachment.mimetype, + 'content': attachment.base64(), + }) + + except exception.AppriseException: + # We could not access the attachment + self.logger.error( + 'Could not access SMSEagle attachment {}.'.format( + attachment.url(privacy=True))) return False + self.logger.debug( + 'Appending SMSEagle attachment {}'.format( + attachment.url(privacy=True))) + # Prepare our headers headers = { 'User-Agent': self.app_id, diff --git a/apprise/plugins/smtp2go.py b/apprise/plugins/smtp2go.py index cb8c71ff..a19e523f 100644 --- a/apprise/plugins/smtp2go.py +++ b/apprise/plugins/smtp2go.py @@ -45,11 +45,11 @@ # the email will be transmitted from. If no email address is specified # then it will also become the 'to' address as well. # -import base64 import requests from json import dumps from email.utils import formataddr from .base import NotifyBase +from .. import exception from ..common import NotifyType from ..common import NotifyFormat from ..utils import parse_emails @@ -294,33 +294,35 @@ class NotifySMTP2Go(NotifyBase): attachments = [] if attach and self.attachment_support: - for attachment in attach: + for no, attachment in enumerate(attach, start=1): # Perform some simple error checking if not attachment: # We could not access the attachment self.logger.error( - 'Could not access attachment {}.'.format( + 'Could not access SMTP2Go attachment {}.'.format( attachment.url(privacy=True))) return False try: - with open(attachment.path, 'rb') as f: - # Output must be in a DataURL format (that's what - # PushSafer calls it): - attachments.append({ - 'filename': attachment.name, - 'fileblob': base64.b64encode(f.read()) - .decode('utf-8'), - 'mimetype': attachment.mimetype, - }) - - except (OSError, IOError) as e: - self.logger.warning( - 'An I/O error occurred while reading {}.'.format( - attachment.name if attachment else 'attachment')) - self.logger.debug('I/O Exception: %s' % str(e)) + # Format our attachment + attachments.append({ + 'filename': attachment.name + if attachment.name else f'file{no:03}.dat', + 'fileblob': attachment.base64(), + 'mimetype': attachment.mimetype, + }) + + except exception.AppriseException: + # We could not access the attachment + self.logger.error( + 'Could not access SMTP2Go attachment {}.'.format( + attachment.url(privacy=True))) return False + self.logger.debug( + 'Appending SMTP2Go attachment {}'.format( + attachment.url(privacy=True))) + sender = formataddr( (self.from_name if self.from_name else False, self.from_addr), charset='utf-8') diff --git a/apprise/plugins/sparkpost.py b/apprise/plugins/sparkpost.py index b1fb7bca..4e4233ca 100644 --- a/apprise/plugins/sparkpost.py +++ b/apprise/plugins/sparkpost.py @@ -55,10 +55,10 @@ # API Documentation: https://developers.sparkpost.com/api/ # Specifically: https://developers.sparkpost.com/api/transmissions/ import requests -import base64 from json import loads from json import dumps from .base import NotifyBase +from .. import exception from ..common import NotifyType from ..common import NotifyFormat from ..utils import is_email @@ -500,7 +500,7 @@ class NotifySparkPost(NotifyBase): if not self.targets: # There is no one to email; we're done self.logger.warning( - 'There are no Email recipients to notify') + 'There are no SparkPost Email recipients to notify') return False # Initialize our has_error flag @@ -546,35 +546,35 @@ class NotifySparkPost(NotifyBase): # Prepare ourselves an attachment object payload['content']['attachments'] = [] - for attachment in attach: + for no, attachment in enumerate(attach, start=1): # Perform some simple error checking if not attachment: # We could not access the attachment self.logger.error( - 'Could not access attachment {}.'.format( + 'Could not access SparkPost attachment {}.'.format( attachment.url(privacy=True))) return False - self.logger.debug( - 'Preparing SparkPost attachment {}'.format( - attachment.url(privacy=True))) - try: - with open(attachment.path, 'rb') as fp: - # Prepare API Upload Payload - payload['content']['attachments'].append({ - 'name': attachment.name, - 'type': attachment.mimetype, - 'data': base64.b64encode(fp.read()).decode("ascii") - }) - - except (OSError, IOError) as e: - self.logger.warning( - 'An I/O error occurred while reading {}.'.format( - attachment.name if attachment else 'attachment')) - self.logger.debug('I/O Exception: %s' % str(e)) + # Prepare API Upload Payload + payload['content']['attachments'].append({ + 'name': attachment.name + if attachment.name else f'file{no:03}.dat', + 'type': attachment.mimetype, + 'data': attachment.base64(), + }) + + except exception.AppriseException: + # We could not access the attachment + self.logger.error( + 'Could not access SparkPost attachment {}.'.format( + attachment.url(privacy=True))) return False + self.logger.debug( + 'Appending SparkPost attachment {}'.format( + attachment.url(privacy=True))) + # Take a copy of our token dictionary tokens = self.tokens.copy() diff --git a/apprise/plugins/twitter.py b/apprise/plugins/twitter.py index 369aaac0..6d352ea6 100644 --- a/apprise/plugins/twitter.py +++ b/apprise/plugins/twitter.py @@ -287,7 +287,7 @@ class NotifyTwitter(NotifyBase): if attach and self.attachment_support: # We need to upload our payload first so that we can source it # in remaining messages - for attachment in attach: + for no, attachment in enumerate(attach, start=1): # Perform some simple error checking if not attachment: @@ -320,11 +320,15 @@ class NotifyTwitter(NotifyBase): # We can't post our attachment return False + # Prepare our filename + filename = attachment.name \ + if attachment.name else f'file{no:03}.dat' + if not (isinstance(response, dict) and response.get('media_id')): self.logger.debug( 'Could not attach the file to Twitter: %s (mime=%s)', - attachment.name, attachment.mimetype) + filename, attachment.mimetype) continue # If we get here, our output will look something like this: @@ -344,7 +348,7 @@ class NotifyTwitter(NotifyBase): response.update({ # Update our response to additionally include the # attachment details - 'file_name': attachment.name, + 'file_name': filename, 'file_mime': attachment.mimetype, 'file_path': attachment.path, }) diff --git a/test/test_attach_http.py b/test/test_attach_http.py index 36ecbad5..6caf8d62 100644 --- a/test/test_attach_http.py +++ b/test/test_attach_http.py @@ -29,6 +29,8 @@ import re from unittest import mock +import pytest +from apprise import exception import requests import mimetypes from os.path import join @@ -481,3 +483,33 @@ def test_attach_http(mock_get, mock_post): assert mock_post.call_count == 30 # We only fetched once and re-used the same fetch for all posts assert mock_get.call_count == 1 + + # + # We will test our base64 handling now + # + mock_get.reset_mock() + mock_post.reset_mock() + + AttachHTTP.max_file_size = getsize(path) + # Set ourselves a Content-Disposition (providing a filename) + dummy_response.headers['Content-Disposition'] = \ + 'attachment; filename="myimage.gif"' + results = AttachHTTP.parse_url('http://user@localhost/filename.gif') + assert isinstance(results, dict) + obj = AttachHTTP(**results) + + # now test our base64 output + assert isinstance(obj.base64(), str) + # No encoding if we choose + assert isinstance(obj.base64(encoding=None), bytes) + + # Error cases: + 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): + obj.base64() + + mock_file.side_effect = OSError + with pytest.raises(exception.AppriseDiskIOError): + obj.base64() diff --git a/test/test_attach_memory.py b/test/test_attach_memory.py index a4bec417..d5f4c52d 100644 --- a/test/test_attach_memory.py +++ b/test/test_attach_memory.py @@ -30,6 +30,7 @@ import re import urllib import pytest +from apprise import exception from apprise.attachment.base import AttachBase from apprise.attachment.memory import AttachMemory from apprise import AppriseAttachment @@ -203,3 +204,12 @@ def test_attach_memory(): # Test hosted configuration and that we can't add a valid memory file aa = AppriseAttachment(location=ContentLocation.HOSTED) assert aa.add(response) is False + + # now test our base64 output + assert isinstance(response.base64(), str) + # No encoding if we choose + assert isinstance(response.base64(encoding=None), bytes) + + response.invalidate() + with pytest.raises(exception.AppriseFileNotFound): + response.base64() diff --git a/test/test_plugin_sendgrid.py b/test/test_plugin_sendgrid.py index 4c775ca0..eb48befa 100644 --- a/test/test_plugin_sendgrid.py +++ b/test/test_plugin_sendgrid.py @@ -196,6 +196,12 @@ def test_plugin_sendgrid_attachments(mock_post, mock_get): mock_post.reset_mock() mock_get.reset_mock() + # Try again in a use case where we can't access the file + with mock.patch("os.path.isfile", return_value=False): + assert obj.notify( + body='body', title='title', notify_type=NotifyType.INFO, + attach=attach) is False + # Try again in a use case where we can't access the file with mock.patch("builtins.open", side_effect=OSError): assert obj.notify(