mirror of
https://github.com/caronc/apprise.git
synced 2024-11-21 23:53:23 +01:00
Refactored base64 attachment handling (#1191)
This commit is contained in:
parent
ca50cb7820
commit
98fb4865fc
@ -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())
|
||||||
|
@ -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
|
||||||
|
@ -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,39 +261,45 @@ 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": filename,
|
||||||
'filename': attachment.name,
|
'base64': attachment.base64(),
|
||||||
'base64': base64.b64encode(f.read())
|
'mimetype': attachment.mimetype,
|
||||||
.decode('utf-8'),
|
})
|
||||||
'mimetype': attachment.mimetype,
|
|
||||||
})
|
|
||||||
|
|
||||||
else: # AppriseAPIMethod.FORM
|
else: # AppriseAPIMethod.FORM
|
||||||
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
|
||||||
|
@ -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)
|
||||||
))
|
))
|
||||||
|
@ -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:
|
attachments.append({
|
||||||
# Output must be in a DataURL format (that's what
|
"filename": attachment.name
|
||||||
# PushSafer calls it):
|
if attachment.name else f'file{no:03}.dat',
|
||||||
attachments.append({
|
'base64': attachment.base64(),
|
||||||
'filename': attachment.name,
|
'mimetype': attachment.mimetype,
|
||||||
'base64': base64.b64encode(f.read())
|
})
|
||||||
.decode('utf-8'),
|
|
||||||
'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,
|
||||||
|
@ -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 if attachment.name
|
||||||
attachment.name, whitespace=False),
|
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">' + \
|
||||||
|
@ -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
|
||||||
|
@ -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(
|
||||||
|
@ -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
|
||||||
|
@ -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):
|
attachments.append((
|
||||||
attachment = (
|
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)
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
@ -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,
|
||||||
|
@ -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')
|
||||||
|
@ -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': attachment.base64(),
|
||||||
'content': 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 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,
|
||||||
|
@ -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
|
attachments.append({
|
||||||
# PushSafer calls it):
|
'filename': attachment.name
|
||||||
attachments.append({
|
if attachment.name else f'file{no:03}.dat',
|
||||||
'filename': attachment.name,
|
'fileblob': attachment.base64(),
|
||||||
'fileblob': base64.b64encode(f.read())
|
'mimetype': attachment.mimetype,
|
||||||
.decode('utf-8'),
|
})
|
||||||
'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')
|
||||||
|
@ -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)))
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 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)))
|
attachment.url(privacy=True)))
|
||||||
return False
|
return False
|
||||||
|
|
||||||
self.logger.debug(
|
self.logger.debug(
|
||||||
'Preparing SparkPost attachment {}'.format(
|
'Appending SparkPost attachment {}'.format(
|
||||||
attachment.url(privacy=True)))
|
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))
|
|
||||||
return False
|
|
||||||
|
|
||||||
# Take a copy of our token dictionary
|
# Take a copy of our token dictionary
|
||||||
tokens = self.tokens.copy()
|
tokens = self.tokens.copy()
|
||||||
|
|
||||||
|
@ -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,
|
||||||
})
|
})
|
||||||
|
@ -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()
|
||||||
|
@ -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()
|
||||||
|
@ -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…
Reference in New Issue
Block a user