Browse Source

Refactored base64 attachment handling (#1191)

pull/622/merge
Chris Caron 3 months ago committed by GitHub
parent
commit
98fb4865fc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 4
      apprise/attachment/base.py
  2. 19
      apprise/attachment/memory.py
  3. 30
      apprise/plugins/apprise_api.py
  4. 3
      apprise/plugins/custom_form.py
  5. 29
      apprise/plugins/custom_json.py
  6. 26
      apprise/plugins/custom_xml.py
  7. 8
      apprise/plugins/email.py
  8. 8
      apprise/plugins/mailgun.py
  9. 5
      apprise/plugins/pushbullet.py
  10. 31
      apprise/plugins/pushsafer.py
  11. 12
      apprise/plugins/sendgrid.py
  12. 7
      apprise/plugins/ses.py
  13. 23
      apprise/plugins/signal_api.py
  14. 5
      apprise/plugins/slack.py
  15. 22
      apprise/plugins/smseagle.py
  16. 30
      apprise/plugins/smtp2go.py
  17. 32
      apprise/plugins/sparkpost.py
  18. 10
      apprise/plugins/twitter.py
  19. 32
      test/test_attach_http.py
  20. 10
      test/test_attach_memory.py
  21. 6
      test/test_plugin_sendgrid.py

4
apprise/attachment/base.py

@ -291,7 +291,7 @@ 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'): def base64(self, encoding='ascii'):
""" """
Returns the attachment object as a base64 string otherwise Returns the attachment object as a base64 string otherwise
None is returned if an error occurs. None is returned if an error occurs.
@ -306,7 +306,7 @@ class AttachBase(URLBase):
raise exception.AppriseFileNotFound("Attachment Missing") raise exception.AppriseFileNotFound("Attachment Missing")
try: try:
with open(self.path, 'rb') as f: with self.open() as f:
# Prepare our Attachment in Base64 # Prepare our Attachment in Base64
return base64.b64encode(f.read()).decode(encoding) \ return base64.b64encode(f.read()).decode(encoding) \
if encoding else base64.b64encode(f.read()) if encoding else base64.b64encode(f.read())

19
apprise/attachment/memory.py

@ -29,7 +29,9 @@
import re import re
import os import os
import io import io
import base64
from .base import AttachBase from .base import AttachBase
from .. import exception
from ..common import ContentLocation from ..common import ContentLocation
from ..locale import gettext_lazy as _ from ..locale import gettext_lazy as _
import uuid import uuid
@ -145,6 +147,23 @@ class AttachMemory(AttachBase):
return True 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): def invalidate(self):
""" """
Removes data Removes data

30
apprise/plugins/apprise_api.py

@ -29,8 +29,8 @@
import re import re
import requests import requests
from json import dumps from json import dumps
import base64
from .. import exception
from .base import NotifyBase from .base import NotifyBase
from ..url import PrivacyMode from ..url import PrivacyMode
from ..common import NotifyType from ..common import NotifyType
@ -261,19 +261,21 @@ class NotifyAppriseAPI(NotifyBase):
if not attachment: if not attachment:
# We could not access the attachment # We could not access the attachment
self.logger.error( self.logger.error(
'Could not access attachment {}.'.format( 'Could not access Apprise API attachment {}.'.format(
attachment.url(privacy=True))) attachment.url(privacy=True)))
return False return False
try: try:
# Our Attachment filename
filename = attachment.name \
if attachment.name else f'file{no:03}.dat'
if self.method == AppriseAPIMethod.JSON: if self.method == AppriseAPIMethod.JSON:
with open(attachment.path, 'rb') as f:
# Output must be in a DataURL format (that's what # Output must be in a DataURL format (that's what
# PushSafer calls it): # PushSafer calls it):
attachments.append({ attachments.append({
'filename': attachment.name, "filename": filename,
'base64': base64.b64encode(f.read()) 'base64': attachment.base64(),
.decode('utf-8'),
'mimetype': attachment.mimetype, 'mimetype': attachment.mimetype,
}) })
@ -281,19 +283,23 @@ class NotifyAppriseAPI(NotifyBase):
files.append(( files.append((
'file{:02d}'.format(no), 'file{:02d}'.format(no),
( (
attachment.name, filename,
open(attachment.path, 'rb'), open(attachment.path, 'rb'),
attachment.mimetype, attachment.mimetype,
) )
)) ))
except (OSError, IOError) as e: except (TypeError, OSError, exception.AppriseException):
self.logger.warning( # We could not access the attachment
'An I/O error occurred while reading {}.'.format( self.logger.error(
attachment.name if attachment else 'attachment')) 'Could not access AppriseAPI attachment {}.'.format(
self.logger.debug('I/O Exception: %s' % str(e)) attachment.url(privacy=True)))
return False return False
self.logger.debug(
'Appending AppriseAPI attachment {}'.format(
attachment.url(privacy=True)))
# prepare Apprise API Object # prepare Apprise API Object
payload = { payload = {
# Apprise API Payload # Apprise API Payload

3
apprise/plugins/custom_form.py

@ -302,7 +302,8 @@ class NotifyForm(NotifyBase):
files.append(( files.append((
self.attach_as.format(no) self.attach_as.format(no)
if self.attach_multi_support else self.attach_as, ( 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'), open(attachment.path, 'rb'),
attachment.mimetype) attachment.mimetype)
)) ))

29
apprise/plugins/custom_json.py

@ -27,9 +27,9 @@
# POSSIBILITY OF SUCH DAMAGE. # POSSIBILITY OF SUCH DAMAGE.
import requests import requests
import base64
from json import dumps from json import dumps
from .. import exception
from .base import NotifyBase from .base import NotifyBase
from ..url import PrivacyMode from ..url import PrivacyMode
from ..common import NotifyImageSize from ..common import NotifyImageSize
@ -213,33 +213,34 @@ class NotifyJSON(NotifyBase):
# Track our potential attachments # Track our potential attachments
attachments = [] attachments = []
if attach and self.attachment_support: if attach and self.attachment_support:
for attachment in attach: for no, attachment in enumerate(attach, start=1):
# Perform some simple error checking # Perform some simple error checking
if not attachment: if not attachment:
# We could not access the attachment # We could not access the attachment
self.logger.error( self.logger.error(
'Could not access attachment {}.'.format( 'Could not access Custom JSON attachment {}.'.format(
attachment.url(privacy=True))) attachment.url(privacy=True)))
return False return False
try: try:
with open(attachment.path, 'rb') as f:
# Output must be in a DataURL format (that's what
# PushSafer calls it):
attachments.append({ attachments.append({
'filename': attachment.name, "filename": attachment.name
'base64': base64.b64encode(f.read()) if attachment.name else f'file{no:03}.dat',
.decode('utf-8'), 'base64': attachment.base64(),
'mimetype': attachment.mimetype, 'mimetype': attachment.mimetype,
}) })
except (OSError, IOError) as e: except exception.AppriseException:
self.logger.warning( # We could not access the attachment
'An I/O error occurred while reading {}.'.format( self.logger.error(
attachment.name if attachment else 'attachment')) 'Could not access Custom JSON attachment {}.'.format(
self.logger.debug('I/O Exception: %s' % str(e)) attachment.url(privacy=True)))
return False return False
self.logger.debug(
'Appending Custom JSON attachment {}'.format(
attachment.url(privacy=True)))
# Prepare JSON Object # Prepare JSON Object
payload = { payload = {
JSONPayloadField.VERSION: self.json_version, JSONPayloadField.VERSION: self.json_version,

26
apprise/plugins/custom_xml.py

@ -28,8 +28,8 @@
import re import re
import requests import requests
import base64
from .. import exception
from .base import NotifyBase from .base import NotifyBase
from ..url import PrivacyMode from ..url import PrivacyMode
from ..common import NotifyImageSize from ..common import NotifyImageSize
@ -287,35 +287,39 @@ class NotifyXML(NotifyBase):
attachments = [] attachments = []
if attach and self.attachment_support: if attach and self.attachment_support:
for attachment in attach: for no, attachment in enumerate(attach, start=1):
# Perform some simple error checking # Perform some simple error checking
if not attachment: if not attachment:
# We could not access the attachment # We could not access the attachment
self.logger.error( self.logger.error(
'Could not access attachment {}.'.format( 'Could not access Custom XML attachment {}.'.format(
attachment.url(privacy=True))) attachment.url(privacy=True)))
return False return False
try: try:
with open(attachment.path, 'rb') as f:
# Prepare our Attachment in Base64 # Prepare our Attachment in Base64
entry = \ entry = \
'<Attachment filename="{}" mimetype="{}">'.format( '<Attachment filename="{}" mimetype="{}">'.format(
NotifyXML.escape_html( NotifyXML.escape_html(
attachment.name, whitespace=False), attachment.name if attachment.name
else f'file{no:03}.dat', whitespace=False),
NotifyXML.escape_html( NotifyXML.escape_html(
attachment.mimetype, whitespace=False)) attachment.mimetype, whitespace=False))
entry += base64.b64encode(f.read()).decode('utf-8') entry += attachment.base64()
entry += '</Attachment>' entry += '</Attachment>'
attachments.append(entry) attachments.append(entry)
except (OSError, IOError) as e: except exception.AppriseException:
self.logger.warning( # We could not access the attachment
'An I/O error occurred while reading {}.'.format( self.logger.error(
attachment.name if attachment else 'attachment')) 'Could not access Custom XML attachment {}.'.format(
self.logger.debug('I/O Exception: %s' % str(e)) attachment.url(privacy=True)))
return False return False
self.logger.debug(
'Appending Custom XML attachment {}'.format(
attachment.url(privacy=True)))
# Update our xml_attachments record: # Update our xml_attachments record:
xml_attachments = \ xml_attachments = \
'<Attachments format="base64">' + \ '<Attachments format="base64">' + \

8
apprise/plugins/email.py

@ -799,7 +799,7 @@ class NotifyEmail(NotifyBase):
mixed = MIMEMultipart("mixed") mixed = MIMEMultipart("mixed")
mixed.attach(base) mixed.attach(base)
# Now store our attachments # Now store our attachments
for attachment in attach: for no, attachment in enumerate(attach, start=1):
if not attachment: if not attachment:
# We could not load the attachment; take an early # We could not load the attachment; take an early
# exit since this isn't what the end user wanted # exit since this isn't what the end user wanted
@ -819,10 +819,14 @@ class NotifyEmail(NotifyBase):
app = MIMEApplication(abody.read()) app = MIMEApplication(abody.read())
app.set_type(attachment.mimetype) app.set_type(attachment.mimetype)
# Prepare our attachment name
filename = attachment.name \
if attachment.name else f'file{no:03}.dat'
app.add_header( app.add_header(
'Content-Disposition', 'Content-Disposition',
'attachment; filename="{}"'.format( 'attachment; filename="{}"'.format(
Header(attachment.name, 'utf-8')), Header(filename, 'utf-8')),
) )
mixed.attach(app) mixed.attach(app)
base = mixed base = mixed

8
apprise/plugins/mailgun.py

@ -383,9 +383,15 @@ class NotifyMailgun(NotifyBase):
self.logger.debug( self.logger.debug(
'Preparing Mailgun attachment {}'.format( 'Preparing Mailgun attachment {}'.format(
attachment.url(privacy=True))) attachment.url(privacy=True)))
# Prepare our filename
filename = attachment.name \
if attachment.name \
else 'file{no:03}.dat'.format(no=idx + 1)
try: try:
files['attachment[{}]'.format(idx)] = \ files['attachment[{}]'.format(idx)] = \
(attachment.name, open(attachment.path, 'rb')) (filename, open(attachment.path, 'rb'))
except (OSError, IOError) as e: except (OSError, IOError) as e:
self.logger.warning( self.logger.warning(

5
apprise/plugins/pushbullet.py

@ -152,7 +152,7 @@ class NotifyPushBullet(NotifyBase):
if attach and self.attachment_support: if attach and self.attachment_support:
# We need to upload our payload first so that we can source it # We need to upload our payload first so that we can source it
# in remaining messages # in remaining messages
for attachment in attach: for no, attachment in enumerate(attach, start=1):
# Perform some simple error checking # Perform some simple error checking
if not attachment: if not attachment:
@ -168,7 +168,8 @@ class NotifyPushBullet(NotifyBase):
# prepare payload # prepare payload
payload = { payload = {
'file_name': attachment.name, 'file_name': attachment.name
if attachment.name else f'file{no:03}.dat',
'file_type': attachment.mimetype, 'file_type': attachment.mimetype,
} }
# First thing we need to do is make a request so that we can # First thing we need to do is make a request so that we can

31
apprise/plugins/pushsafer.py

@ -26,11 +26,11 @@
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE. # POSSIBILITY OF SUCH DAMAGE.
import base64
import requests import requests
from json import loads from json import loads
from .base import NotifyBase from .base import NotifyBase
from .. import exception
from ..common import NotifyType from ..common import NotifyType
from ..utils import parse_list from ..utils import parse_list
from ..utils import validate_regex from ..utils import validate_regex
@ -548,12 +548,12 @@ class NotifyPushSafer(NotifyBase):
if attach and self.attachment_support: if attach and self.attachment_support:
# We need to upload our payload first so that we can source it # We need to upload our payload first so that we can source it
# in remaining messages # in remaining messages
for attachment in attach: for no, attachment in enumerate(attach, start=1):
# prepare payload # prepare payload
if not attachment: if not attachment:
# We could not access the attachment # We could not access the attachment
self.logger.error( self.logger.error(
'Could not access attachment {}.'.format( 'Could not access PushSafer attachment {}.'.format(
attachment.url(privacy=True))) attachment.url(privacy=True)))
return False return False
@ -569,24 +569,27 @@ class NotifyPushSafer(NotifyBase):
attachment.url(privacy=True))) attachment.url(privacy=True)))
try: try:
with open(attachment.path, 'rb') as f:
# Output must be in a DataURL format (that's what # Output must be in a DataURL format (that's what
# PushSafer calls it): # PushSafer calls it):
attachment = ( attachments.append((
attachment.name, attachment.name
if attachment.name else f'file{no:03}.dat',
'data:{};base64,{}'.format( 'data:{};base64,{}'.format(
attachment.mimetype, attachment.mimetype,
base64.b64encode(f.read()))) attachment.base64(),
)
))
except (OSError, IOError) as e: except exception.AppriseException:
self.logger.warning( # We could not access the attachment
'An I/O error occurred while reading {}.'.format( self.logger.error(
attachment.name if attachment else 'attachment')) 'Could not access PushSafer attachment {}.'.format(
self.logger.debug('I/O Exception: %s' % str(e)) attachment.url(privacy=True)))
return False return False
# Save our pre-prepared payload for attachment posting self.logger.debug(
attachments.append(attachment) 'Appending PushSafer attachment {}'.format(
attachment.url(privacy=True)))
# Create a copy of the targets list # Create a copy of the targets list
targets = list(self.targets) targets = list(self.targets)

12
apprise/plugins/sendgrid.py

@ -342,11 +342,19 @@ class NotifySendGrid(NotifyBase):
# Send our attachments # Send our attachments
for no, attachment in enumerate(attach, start=1): 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: try:
attachments.append({ attachments.append({
"content": attachment.base64(), "content": attachment.base64(),
"filename": attachment.name "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", "type": "application/octet-stream",
"disposition": "attachment" "disposition": "attachment"
}) })
@ -354,7 +362,7 @@ class NotifySendGrid(NotifyBase):
except exception.AppriseException: except exception.AppriseException:
# We could not access the attachment # We could not access the attachment
self.logger.error( self.logger.error(
'Could not access attachment {}.'.format( 'Could not access SendGrid attachment {}.'.format(
attachment.url(privacy=True))) attachment.url(privacy=True)))
return False return False

7
apprise/plugins/ses.py

@ -448,7 +448,7 @@ class NotifySES(NotifyBase):
base.attach(content) base.attach(content)
# Now store our attachments # Now store our attachments
for attachment in attach: for no, attachment in enumerate(attach, start=1):
if not attachment: if not attachment:
# We could not load the attachment; take an early # We could not load the attachment; take an early
# exit since this isn't what the end user wanted # exit since this isn't what the end user wanted
@ -468,10 +468,13 @@ class NotifySES(NotifyBase):
app = MIMEApplication(abody.read()) app = MIMEApplication(abody.read())
app.set_type(attachment.mimetype) app.set_type(attachment.mimetype)
filename = attachment.name \
if attachment.name else f'file{no:03}.dat'
app.add_header( app.add_header(
'Content-Disposition', 'Content-Disposition',
'attachment; filename="{}"'.format( 'attachment; filename="{}"'.format(
Header(attachment.name, 'utf-8')), Header(filename, 'utf-8')),
) )
base.attach(app) base.attach(app)

23
apprise/plugins/signal_api.py

@ -29,10 +29,10 @@
import re import re
import requests import requests
from json import dumps from json import dumps
import base64
from .base import NotifyBase from .base import NotifyBase
from ..common import NotifyType from ..common import NotifyType
from .. import exception
from ..utils import is_phone_no from ..utils import is_phone_no
from ..utils import parse_phone_no from ..utils import parse_phone_no
from ..utils import parse_bool from ..utils import parse_bool
@ -239,23 +239,24 @@ class NotifySignalAPI(NotifyBase):
if not attachment: if not attachment:
# We could not access the attachment # We could not access the attachment
self.logger.error( self.logger.error(
'Could not access attachment {}.'.format( 'Could not access Signal API attachment {}.'.format(
attachment.url(privacy=True))) attachment.url(privacy=True)))
return False return False
try: try:
with open(attachment.path, 'rb') as f: attachments.append(attachment.base64())
# Prepare our Attachment in Base64
attachments.append(
base64.b64encode(f.read()).decode('utf-8'))
except (OSError, IOError) as e: except exception.AppriseException:
self.logger.warning( # We could not access the attachment
'An I/O error occurred while reading {}.'.format( self.logger.error(
attachment.name if attachment else 'attachment')) 'Could not access Signal API attachment {}.'.format(
self.logger.debug('I/O Exception: %s' % str(e)) attachment.url(privacy=True)))
return False return False
self.logger.debug(
'Appending Signal API attachment {}'.format(
attachment.url(privacy=True)))
# Prepare our headers # Prepare our headers
headers = { headers = {
'User-Agent': self.app_id, 'User-Agent': self.app_id,

5
apprise/plugins/slack.py

@ -646,7 +646,7 @@ class NotifySlack(NotifyBase):
if attach and self.attachment_support and \ if attach and self.attachment_support and \
self.mode is SlackMode.BOT and attach_channel_list: self.mode is SlackMode.BOT and attach_channel_list:
# Send our attachments (can only be done in bot mode) # 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 # Perform some simple error checking
if not attachment: if not attachment:
@ -663,7 +663,8 @@ class NotifySlack(NotifyBase):
# Get the URL to which to upload the file. # Get the URL to which to upload the file.
# https://api.slack.com/methods/files.getUploadURLExternal # https://api.slack.com/methods/files.getUploadURLExternal
_params = { _params = {
'filename': attachment.name, 'filename': attachment.name
if attachment.name else f'file{no:03}.dat',
'length': len(attachment), 'length': len(attachment),
} }
_url = self.api_url.format('files.getUploadURLExternal') _url = self.api_url.format('files.getUploadURLExternal')

22
apprise/plugins/smseagle.py

@ -29,11 +29,11 @@
import re import re
import requests import requests
from json import dumps, loads from json import dumps, loads
import base64
from itertools import chain from itertools import chain
from .base import NotifyBase from .base import NotifyBase
from ..common import NotifyType from ..common import NotifyType
from .. import exception
from ..utils import validate_regex from ..utils import validate_regex
from ..utils import is_phone_no from ..utils import is_phone_no
from ..utils import parse_phone_no from ..utils import parse_phone_no
@ -345,7 +345,7 @@ class NotifySMSEagle(NotifyBase):
if not attachment: if not attachment:
# We could not access the attachment # We could not access the attachment
self.logger.error( self.logger.error(
'Could not access attachment {}.'.format( 'Could not access SMSEagle attachment {}.'.format(
attachment.url(privacy=True))) attachment.url(privacy=True)))
return False return False
@ -357,21 +357,23 @@ class NotifySMSEagle(NotifyBase):
continue continue
try: try:
with open(attachment.path, 'rb') as f:
# Prepare our Attachment in Base64 # Prepare our Attachment in Base64
attachments.append({ attachments.append({
'content_type': attachment.mimetype, 'content_type': attachment.mimetype,
'content': base64.b64encode( 'content': attachment.base64(),
f.read()).decode('utf-8'),
}) })
except (OSError, IOError) as e: except exception.AppriseException:
self.logger.warning( # We could not access the attachment
'An I/O error occurred while reading {}.'.format( self.logger.error(
attachment.name if attachment else 'attachment')) 'Could not access SMSEagle attachment {}.'.format(
self.logger.debug('I/O Exception: %s' % str(e)) attachment.url(privacy=True)))
return False return False
self.logger.debug(
'Appending SMSEagle attachment {}'.format(
attachment.url(privacy=True)))
# Prepare our headers # Prepare our headers
headers = { headers = {
'User-Agent': self.app_id, 'User-Agent': self.app_id,

30
apprise/plugins/smtp2go.py

@ -45,11 +45,11 @@
# the email will be transmitted from. If no email address is specified # the email will be transmitted from. If no email address is specified
# then it will also become the 'to' address as well. # then it will also become the 'to' address as well.
# #
import base64
import requests import requests
from json import dumps from json import dumps
from email.utils import formataddr from email.utils import formataddr
from .base import NotifyBase from .base import NotifyBase
from .. import exception
from ..common import NotifyType from ..common import NotifyType
from ..common import NotifyFormat from ..common import NotifyFormat
from ..utils import parse_emails from ..utils import parse_emails
@ -294,33 +294,35 @@ class NotifySMTP2Go(NotifyBase):
attachments = [] attachments = []
if attach and self.attachment_support: if attach and self.attachment_support:
for attachment in attach: for no, attachment in enumerate(attach, start=1):
# Perform some simple error checking # Perform some simple error checking
if not attachment: if not attachment:
# We could not access the attachment # We could not access the attachment
self.logger.error( self.logger.error(
'Could not access attachment {}.'.format( 'Could not access SMTP2Go attachment {}.'.format(
attachment.url(privacy=True))) attachment.url(privacy=True)))
return False return False
try: try:
with open(attachment.path, 'rb') as f: # Format our attachment
# Output must be in a DataURL format (that's what
# PushSafer calls it):
attachments.append({ attachments.append({
'filename': attachment.name, 'filename': attachment.name
'fileblob': base64.b64encode(f.read()) if attachment.name else f'file{no:03}.dat',
.decode('utf-8'), 'fileblob': attachment.base64(),
'mimetype': attachment.mimetype, 'mimetype': attachment.mimetype,
}) })
except (OSError, IOError) as e: except exception.AppriseException:
self.logger.warning( # We could not access the attachment
'An I/O error occurred while reading {}.'.format( self.logger.error(
attachment.name if attachment else 'attachment')) 'Could not access SMTP2Go attachment {}.'.format(
self.logger.debug('I/O Exception: %s' % str(e)) attachment.url(privacy=True)))
return False return False
self.logger.debug(
'Appending SMTP2Go attachment {}'.format(
attachment.url(privacy=True)))
sender = formataddr( sender = formataddr(
(self.from_name if self.from_name else False, (self.from_name if self.from_name else False,
self.from_addr), charset='utf-8') self.from_addr), charset='utf-8')

32
apprise/plugins/sparkpost.py

@ -55,10 +55,10 @@
# API Documentation: https://developers.sparkpost.com/api/ # API Documentation: https://developers.sparkpost.com/api/
# Specifically: https://developers.sparkpost.com/api/transmissions/ # Specifically: https://developers.sparkpost.com/api/transmissions/
import requests import requests
import base64
from json import loads from json import loads
from json import dumps from json import dumps
from .base import NotifyBase from .base import NotifyBase
from .. import exception
from ..common import NotifyType from ..common import NotifyType
from ..common import NotifyFormat from ..common import NotifyFormat
from ..utils import is_email from ..utils import is_email
@ -500,7 +500,7 @@ class NotifySparkPost(NotifyBase):
if not self.targets: if not self.targets:
# There is no one to email; we're done # There is no one to email; we're done
self.logger.warning( self.logger.warning(
'There are no Email recipients to notify') 'There are no SparkPost Email recipients to notify')
return False return False
# Initialize our has_error flag # Initialize our has_error flag
@ -546,35 +546,35 @@ class NotifySparkPost(NotifyBase):
# Prepare ourselves an attachment object # Prepare ourselves an attachment object
payload['content']['attachments'] = [] payload['content']['attachments'] = []
for attachment in attach: for no, attachment in enumerate(attach, start=1):
# Perform some simple error checking # Perform some simple error checking
if not attachment: if not attachment:
# We could not access the attachment # We could not access the attachment
self.logger.error( self.logger.error(
'Could not access attachment {}.'.format( 'Could not access SparkPost attachment {}.'.format(
attachment.url(privacy=True))) attachment.url(privacy=True)))
return False return False
self.logger.debug(
'Preparing SparkPost attachment {}'.format(
attachment.url(privacy=True)))
try: try:
with open(attachment.path, 'rb') as fp:
# Prepare API Upload Payload # Prepare API Upload Payload
payload['content']['attachments'].append({ payload['content']['attachments'].append({
'name': attachment.name, 'name': attachment.name
if attachment.name else f'file{no:03}.dat',
'type': attachment.mimetype, 'type': attachment.mimetype,
'data': base64.b64encode(fp.read()).decode("ascii") 'data': attachment.base64(),
}) })
except (OSError, IOError) as e: except exception.AppriseException:
self.logger.warning( # We could not access the attachment
'An I/O error occurred while reading {}.'.format( self.logger.error(
attachment.name if attachment else 'attachment')) 'Could not access SparkPost attachment {}.'.format(
self.logger.debug('I/O Exception: %s' % str(e)) attachment.url(privacy=True)))
return False return False
self.logger.debug(
'Appending SparkPost attachment {}'.format(
attachment.url(privacy=True)))
# Take a copy of our token dictionary # Take a copy of our token dictionary
tokens = self.tokens.copy() tokens = self.tokens.copy()

10
apprise/plugins/twitter.py

@ -287,7 +287,7 @@ class NotifyTwitter(NotifyBase):
if attach and self.attachment_support: if attach and self.attachment_support:
# We need to upload our payload first so that we can source it # We need to upload our payload first so that we can source it
# in remaining messages # in remaining messages
for attachment in attach: for no, attachment in enumerate(attach, start=1):
# Perform some simple error checking # Perform some simple error checking
if not attachment: if not attachment:
@ -320,11 +320,15 @@ class NotifyTwitter(NotifyBase):
# We can't post our attachment # We can't post our attachment
return False return False
# Prepare our filename
filename = attachment.name \
if attachment.name else f'file{no:03}.dat'
if not (isinstance(response, dict) if not (isinstance(response, dict)
and response.get('media_id')): and response.get('media_id')):
self.logger.debug( self.logger.debug(
'Could not attach the file to Twitter: %s (mime=%s)', 'Could not attach the file to Twitter: %s (mime=%s)',
attachment.name, attachment.mimetype) filename, attachment.mimetype)
continue continue
# If we get here, our output will look something like this: # If we get here, our output will look something like this:
@ -344,7 +348,7 @@ class NotifyTwitter(NotifyBase):
response.update({ response.update({
# Update our response to additionally include the # Update our response to additionally include the
# attachment details # attachment details
'file_name': attachment.name, 'file_name': filename,
'file_mime': attachment.mimetype, 'file_mime': attachment.mimetype,
'file_path': attachment.path, 'file_path': attachment.path,
}) })

32
test/test_attach_http.py

@ -29,6 +29,8 @@
import re import re
from unittest import mock from unittest import mock
import pytest
from apprise import exception
import requests import requests
import mimetypes import mimetypes
from os.path import join from os.path import join
@ -481,3 +483,33 @@ def test_attach_http(mock_get, mock_post):
assert mock_post.call_count == 30 assert mock_post.call_count == 30
# We only fetched once and re-used the same fetch for all posts # We only fetched once and re-used the same fetch for all posts
assert mock_get.call_count == 1 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()

10
test/test_attach_memory.py

@ -30,6 +30,7 @@ import re
import urllib import urllib
import pytest import pytest
from apprise import exception
from apprise.attachment.base import AttachBase from apprise.attachment.base import AttachBase
from apprise.attachment.memory import AttachMemory from apprise.attachment.memory import AttachMemory
from apprise import AppriseAttachment 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 # Test hosted configuration and that we can't add a valid memory file
aa = AppriseAttachment(location=ContentLocation.HOSTED) aa = AppriseAttachment(location=ContentLocation.HOSTED)
assert aa.add(response) is False 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()

6
test/test_plugin_sendgrid.py

@ -196,6 +196,12 @@ def test_plugin_sendgrid_attachments(mock_post, mock_get):
mock_post.reset_mock() mock_post.reset_mock()
mock_get.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 # Try again in a use case where we can't access the file
with mock.patch("builtins.open", side_effect=OSError): with mock.patch("builtins.open", side_effect=OSError):
assert obj.notify( assert obj.notify(

Loading…
Cancel
Save