mirror of https://github.com/caronc/apprise
basic tests passing
parent
e5e7ab0784
commit
5e1e4857f1
|
@ -30,7 +30,7 @@
|
|||
# - https://sendpulse.com/integrations/api/smtp
|
||||
|
||||
import requests
|
||||
from json import dumps
|
||||
from json import dumps, loads
|
||||
import base64
|
||||
|
||||
from .base import NotifyBase
|
||||
|
@ -69,6 +69,9 @@ class NotifySendPulse(NotifyBase):
|
|||
# The default Email API URL to use
|
||||
notify_email_url = 'https://api.sendpulse.com/smtp/emails'
|
||||
|
||||
# Our OAuth Query
|
||||
notify_oauth_url = 'https://api.sendpulse.com/oauth/access_token'
|
||||
|
||||
# Support attachments
|
||||
attachment_support = True
|
||||
|
||||
|
@ -167,6 +170,15 @@ class NotifySendPulse(NotifyBase):
|
|||
"""
|
||||
super().__init__(**kwargs)
|
||||
|
||||
# Api Key is acquired upon a sucessful login
|
||||
self.access_token = None
|
||||
|
||||
result = is_email(from_email)
|
||||
if not result:
|
||||
msg = 'Invalid ~From~ email specified: {}'.format(from_email)
|
||||
self.logger.warning(msg)
|
||||
raise TypeError(msg)
|
||||
|
||||
# Client ID
|
||||
self.client_id = validate_regex(
|
||||
client_id, *self.template_tokens['client_id']['regex'])
|
||||
|
@ -185,12 +197,6 @@ class NotifySendPulse(NotifyBase):
|
|||
self.logger.warning(msg)
|
||||
raise TypeError(msg)
|
||||
|
||||
result = is_email(from_email)
|
||||
if not result:
|
||||
msg = 'Invalid ~From~ email specified: {}'.format(from_email)
|
||||
self.logger.warning(msg)
|
||||
raise TypeError(msg)
|
||||
|
||||
# Tracks emails to name lookups (if they exist)
|
||||
self.__email_map = {}
|
||||
|
||||
|
@ -208,6 +214,8 @@ class NotifySendPulse(NotifyBase):
|
|||
# Acquire Blind Carbon Copies
|
||||
self.bcc = set()
|
||||
|
||||
# No template
|
||||
self.template = None
|
||||
if template:
|
||||
try:
|
||||
# Store our template
|
||||
|
@ -229,9 +237,9 @@ class NotifySendPulse(NotifyBase):
|
|||
for recipient in parse_emails(targets):
|
||||
result = is_email(recipient)
|
||||
if result:
|
||||
self.targets.append(
|
||||
(result['name'] if result['name'] else False,
|
||||
result['full_email']))
|
||||
self.targets.append(result['full_email'])
|
||||
if result['name']:
|
||||
self.__email_map[result['full_email']] = result['name']
|
||||
continue
|
||||
|
||||
self.logger.warning(
|
||||
|
@ -250,7 +258,7 @@ class NotifySendPulse(NotifyBase):
|
|||
if result:
|
||||
self.cc.add(result['full_email'])
|
||||
if result['name']:
|
||||
self.__email_lookup[result['full_email']] = result['name']
|
||||
self.__email_map[result['full_email']] = result['name']
|
||||
continue
|
||||
|
||||
self.logger.warning(
|
||||
|
@ -265,7 +273,7 @@ class NotifySendPulse(NotifyBase):
|
|||
if result:
|
||||
self.bcc.add(result['full_email'])
|
||||
if result['name']:
|
||||
self.__email_lookup[result['full_email']] = result['name']
|
||||
self.__email_map[result['full_email']] = result['name']
|
||||
continue
|
||||
|
||||
self.logger.warning(
|
||||
|
@ -300,8 +308,8 @@ class NotifySendPulse(NotifyBase):
|
|||
# Handle our Carbon Copy Addresses
|
||||
params['cc'] = ','.join([
|
||||
formataddr(
|
||||
(self.__email_lookup[e]
|
||||
if e in self.__email_lookup else False, e),
|
||||
(self.__email_map[e]
|
||||
if e in self.__email_map else False, e),
|
||||
# Swap comma for it's escaped url code (if detected) since
|
||||
# we're using that as a delimiter
|
||||
charset='utf-8').replace(',', '%2C')
|
||||
|
@ -311,8 +319,8 @@ class NotifySendPulse(NotifyBase):
|
|||
# Handle our Blind Carbon Copy Addresses
|
||||
params['bcc'] = ','.join([
|
||||
formataddr(
|
||||
(self.__email_lookup[e]
|
||||
if e in self.__email_lookup else False, e),
|
||||
(self.__email_map[e]
|
||||
if e in self.__email_map else False, e),
|
||||
# Swap comma for it's escaped url code (if detected) since
|
||||
# we're using that as a delimiter
|
||||
charset='utf-8').replace(',', '%2C')
|
||||
|
@ -353,10 +361,24 @@ class NotifySendPulse(NotifyBase):
|
|||
Perform SendPulse Notification
|
||||
"""
|
||||
|
||||
headers = {
|
||||
'User-Agent': self.app_id,
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': 'Bearer {}'.format(self.apikey),
|
||||
if not self.access_token:
|
||||
# Attempt to acquire acquire a login
|
||||
_payload = {
|
||||
'grant_type': 'client_credentials',
|
||||
'client_id': self.client_id,
|
||||
'client_secret': self.client_secret,
|
||||
}
|
||||
|
||||
success, response = self._fetch(self.notify_oauth_url, _payload)
|
||||
if not success:
|
||||
return False
|
||||
|
||||
# If we get here, we're authenticated
|
||||
self.access_token = response.get('access_token')
|
||||
|
||||
# Store our bearer
|
||||
extended_headers = {
|
||||
'Authorization': 'Bearer {}'.format(self.access_token),
|
||||
}
|
||||
|
||||
# error tracking (used for function return)
|
||||
|
@ -451,8 +473,8 @@ class NotifySendPulse(NotifyBase):
|
|||
to = {
|
||||
'email': target
|
||||
}
|
||||
if target in self.__email_lookup:
|
||||
to['name'] = self.__email_lookup[target]
|
||||
if target in self.__email_map:
|
||||
to['name'] = self.__email_map[target]
|
||||
|
||||
# Set our target
|
||||
payload['email']['to'] = [to]
|
||||
|
@ -463,8 +485,8 @@ class NotifySendPulse(NotifyBase):
|
|||
item = {
|
||||
'email': email,
|
||||
}
|
||||
if email in self.__email_lookup:
|
||||
item['name'] = self.__email_lookup[email]
|
||||
if email in self.__email_map:
|
||||
item['name'] = self.__email_map[email]
|
||||
|
||||
payload['email']['cc'].append(item)
|
||||
|
||||
|
@ -474,33 +496,77 @@ class NotifySendPulse(NotifyBase):
|
|||
item = {
|
||||
'email': email,
|
||||
}
|
||||
if email in self.__email_lookup:
|
||||
item['name'] = self.__email_lookup[email]
|
||||
if email in self.__email_map:
|
||||
item['name'] = self.__email_map[email]
|
||||
|
||||
payload['email']['bcc'].append(item)
|
||||
|
||||
self.logger.debug('SendPulse POST URL: %s (cert_verify=%r)' % (
|
||||
self.notify_email_url, self.verify_certificate,
|
||||
))
|
||||
self.logger.debug('SendPulse Payload: %s' % str(payload))
|
||||
# Perform our post
|
||||
success, response = self._fetch(
|
||||
self.notify_email_url, payload, target, extended_headers)
|
||||
if not success:
|
||||
has_error = True
|
||||
continue
|
||||
|
||||
return not has_error
|
||||
|
||||
def _fetch(self, url, payload, target=None, extended_headers={}):
|
||||
"""
|
||||
Wrapper to request.post() to manage it's response better and make
|
||||
the send() function cleaner and easier to maintain.
|
||||
|
||||
This function returns True if the _post was successful and False
|
||||
if it wasn't.
|
||||
"""
|
||||
|
||||
headers = {
|
||||
'User-Agent': self.app_id,
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
|
||||
if extended_headers:
|
||||
headers.update(extended_headers)
|
||||
|
||||
self.logger.debug('SendPulse POST URL: %s (cert_verify=%r)' % (
|
||||
self.url, self.verify_certificate,
|
||||
))
|
||||
self.logger.debug('SendPulse Payload: %s' % str(payload))
|
||||
|
||||
# Prepare our default response
|
||||
response = {}
|
||||
|
||||
# Always call throttle before any remote server i/o is made
|
||||
self.throttle()
|
||||
try:
|
||||
r = requests.post(
|
||||
self.url,
|
||||
data=dumps(payload),
|
||||
headers=headers,
|
||||
verify=self.verify_certificate,
|
||||
timeout=self.request_timeout,
|
||||
)
|
||||
|
||||
# Always call throttle before any remote server i/o is made
|
||||
self.throttle()
|
||||
try:
|
||||
r = requests.post(
|
||||
self.notify_email_url,
|
||||
data=dumps(payload),
|
||||
headers=headers,
|
||||
verify=self.verify_certificate,
|
||||
timeout=self.request_timeout,
|
||||
)
|
||||
if r.status_code not in (
|
||||
requests.codes.ok, requests.codes.accepted):
|
||||
# We had a problem
|
||||
status_str = \
|
||||
NotifySendPulse.http_response_code_lookup(
|
||||
r.status_code)
|
||||
response = loads(r.content)
|
||||
|
||||
except (AttributeError, TypeError, ValueError):
|
||||
# This gets thrown if we can't parse our JSON Response
|
||||
# - ValueError = r.content is Unparsable
|
||||
# - TypeError = r.content is None
|
||||
# - AttributeError = r is None
|
||||
self.logger.warning('Invalid response from SendPulse server.')
|
||||
self.logger.debug(
|
||||
'Response Details:\r\n{}'.format(r.content))
|
||||
return (False, {})
|
||||
|
||||
if r.status_code not in (
|
||||
requests.codes.ok, requests.codes.accepted):
|
||||
# We had a problem
|
||||
status_str = \
|
||||
NotifySendPulse.http_response_code_lookup(
|
||||
r.status_code)
|
||||
|
||||
if target:
|
||||
self.logger.warning(
|
||||
'Failed to send SendPulse notification to {}: '
|
||||
'{}{}error={}.'.format(
|
||||
|
@ -508,29 +574,33 @@ class NotifySendPulse(NotifyBase):
|
|||
status_str,
|
||||
', ' if status_str else '',
|
||||
r.status_code))
|
||||
|
||||
self.logger.debug(
|
||||
'Response Details:\r\n{}'.format(r.content))
|
||||
|
||||
# Mark our failure
|
||||
has_error = True
|
||||
continue
|
||||
|
||||
else:
|
||||
self.logger.warning(
|
||||
'SendPulse Authentication Request failed: '
|
||||
'{}{}error={}.'.format(
|
||||
status_str,
|
||||
', ' if status_str else '',
|
||||
r.status_code))
|
||||
|
||||
self.logger.debug(
|
||||
'Response Details:\r\n{}'.format(r.content))
|
||||
|
||||
else:
|
||||
if target:
|
||||
self.logger.info(
|
||||
'Sent SendPulse notification to {}.'.format(target))
|
||||
else:
|
||||
self.logger.debug('SendPulse authentication successful')
|
||||
|
||||
except requests.RequestException as e:
|
||||
self.logger.warning(
|
||||
'A Connection error occurred sending SendPulse '
|
||||
'notification to {}.'.format(target))
|
||||
self.logger.debug('Socket Exception: %s' % str(e))
|
||||
return (True, response)
|
||||
|
||||
# Mark our failure
|
||||
has_error = True
|
||||
continue
|
||||
except requests.RequestException as e:
|
||||
self.logger.warning(
|
||||
'A Connection error occurred sending SendPulse '
|
||||
'notification to {}.'.format(target))
|
||||
self.logger.debug('Socket Exception: %s' % str(e))
|
||||
|
||||
return not has_error
|
||||
return (False, response)
|
||||
|
||||
@staticmethod
|
||||
def parse_url(url):
|
||||
|
@ -545,6 +615,12 @@ class NotifySendPulse(NotifyBase):
|
|||
# We're done early as we couldn't load the results
|
||||
return results
|
||||
|
||||
# Define our minimum requirements; defining them now saves us from
|
||||
# having to if/else all kinds of branches below...
|
||||
results['from_email'] = None
|
||||
results['client_id'] = None
|
||||
results['client_secret'] = None
|
||||
|
||||
# Our URL looks like this:
|
||||
# {schema}://{from_email}:{client_id}/{client_secret}/{targets}
|
||||
#
|
||||
|
@ -564,7 +640,7 @@ class NotifySendPulse(NotifyBase):
|
|||
results['client_id'] = \
|
||||
NotifySendPulse.unquote(results['qsd']['id'])
|
||||
|
||||
else:
|
||||
elif results['targets']:
|
||||
# Store our Client ID
|
||||
results['client_id'] = results['targets'].pop(0)
|
||||
|
||||
|
@ -573,7 +649,7 @@ class NotifySendPulse(NotifyBase):
|
|||
results['client_secret'] = \
|
||||
NotifySendPulse.unquote(results['qsd']['secret'])
|
||||
|
||||
else:
|
||||
elif results['targets']:
|
||||
# Store our Client Secret
|
||||
results['client_secret'] = results['targets'].pop(0)
|
||||
|
||||
|
|
|
@ -26,6 +26,7 @@
|
|||
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
# POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
from json import dumps
|
||||
from unittest import mock
|
||||
|
||||
import os
|
||||
|
@ -42,6 +43,12 @@ from helpers import AppriseURLTester
|
|||
import logging
|
||||
logging.disable(logging.CRITICAL)
|
||||
|
||||
SENDPULSE_GOOD_RESPONSE = dumps({
|
||||
"access_token": 'abc123'
|
||||
})
|
||||
|
||||
SENDPULSE_BAD_RESPONSE = '{'
|
||||
|
||||
# Attachment Directory
|
||||
TEST_VAR_DIR = os.path.join(os.path.dirname(__file__), 'var')
|
||||
|
||||
|
@ -54,61 +61,76 @@ apprise_url_tests = (
|
|||
'instance': None,
|
||||
}),
|
||||
('sendpulse://abcd', {
|
||||
# Just an broken email (no email, client_id or secret)
|
||||
'instance': None,
|
||||
# invalid from email
|
||||
'instance': TypeError,
|
||||
}),
|
||||
('sendpulse://abcd@host', {
|
||||
('sendpulse://abcd@host.com', {
|
||||
# Just an Email specified, no client_id or client_secret
|
||||
'instance': None,
|
||||
'instance': TypeError,
|
||||
}),
|
||||
('sendpulse://user@example.com/client_id/client_secret/', {
|
||||
('sendpulse://user@example.com/client_id/cs1/', {
|
||||
'instance': NotifySendPulse,
|
||||
'requests_response_text': SENDPULSE_GOOD_RESPONSE,
|
||||
}),
|
||||
('sendpulse://user@example.com/client_id/client_secret/'
|
||||
('sendpulse://user@example.com/client_id/cs1a/', {
|
||||
'instance': NotifySendPulse,
|
||||
'requests_response_text': SENDPULSE_BAD_RESPONSE,
|
||||
# Notify will fail because auth failed
|
||||
'response': False,
|
||||
}),
|
||||
('sendpulse://user@example.com/client_id/cs2/'
|
||||
'?bcc=l2g@nuxref.com', {
|
||||
# A good email with Blind Carbon Copy
|
||||
'instance': NotifySendPulse,
|
||||
'requests_response_text': SENDPULSE_GOOD_RESPONSE,
|
||||
}),
|
||||
('sendpulse://user@example.com/client_id/client_secret/'
|
||||
('sendpulse://user@example.com/client_id/cs3/'
|
||||
'?cc=l2g@nuxref.com', {
|
||||
# A good email with Carbon Copy
|
||||
'instance': NotifySendPulse,
|
||||
'requests_response_text': SENDPULSE_GOOD_RESPONSE,
|
||||
}),
|
||||
('sendpulse://user@example.com/client_id/client_secret/'
|
||||
('sendpulse://user@example.com/client_id/cs4/'
|
||||
'?to=l2g@nuxref.com', {
|
||||
# A good email with Carbon Copy
|
||||
'instance': NotifySendPulse,
|
||||
'requests_response_text': SENDPULSE_GOOD_RESPONSE,
|
||||
}),
|
||||
('sendpulse://user@example.com/client_id/client_secret/'
|
||||
('sendpulse://user@example.com/client_id/cs5/'
|
||||
'?template=1234', {
|
||||
# A good email with a template + no substitutions
|
||||
'instance': NotifySendPulse,
|
||||
'requests_response_text': SENDPULSE_GOOD_RESPONSE,
|
||||
}),
|
||||
('sendpulse://user@example.com/client_id/client_secret/'
|
||||
('sendpulse://user@example.com/client_id/cs6/'
|
||||
'?template=1234&+sub=value&+sub2=value2', {
|
||||
# A good email with a template + substitutions
|
||||
'instance': NotifySendPulse,
|
||||
'requests_response_text': SENDPULSE_GOOD_RESPONSE,
|
||||
|
||||
# Our expected url(privacy=True) startswith() response:
|
||||
'privacy_url': 'sendpulse://a...d:user@example.com/',
|
||||
'privacy_url': 'sendpulse://user@example.com/c...d/c...6/',
|
||||
}),
|
||||
('sendpulse://user@example.com/client_id/client_secret/', {
|
||||
('sendpulse://user@example.com/client_id/cs7/', {
|
||||
'instance': NotifySendPulse,
|
||||
# force a failure
|
||||
'response': False,
|
||||
'requests_response_code': requests.codes.internal_server_error,
|
||||
'requests_response_text': SENDPULSE_GOOD_RESPONSE,
|
||||
}),
|
||||
('sendpulse://user@example.com/client_id/client_secret/', {
|
||||
('sendpulse://user@example.com/client_id/cs8/', {
|
||||
'instance': NotifySendPulse,
|
||||
# throw a bizzare code forcing us to fail to look it up
|
||||
'response': False,
|
||||
'requests_response_code': 999,
|
||||
'requests_response_text': SENDPULSE_GOOD_RESPONSE,
|
||||
}),
|
||||
('sendpulse://user@example.com/client_id/client_secret/', {
|
||||
('sendpulse://user@example.com/client_id/cs9/', {
|
||||
'instance': NotifySendPulse,
|
||||
# Throws a series of connection and transfer exceptions when this flag
|
||||
# is set and tests that we gracfully handle them
|
||||
'test_requests_exceptions': True,
|
||||
'requests_response_text': SENDPULSE_GOOD_RESPONSE,
|
||||
}),
|
||||
)
|
||||
|
||||
|
@ -139,27 +161,27 @@ def test_plugin_sendpulse_edge_cases(mock_post, mock_get):
|
|||
|
||||
with pytest.raises(TypeError):
|
||||
NotifySendPulse(
|
||||
client_id=None, client_secret='abcd',
|
||||
client_id=None, client_secret='abcd123',
|
||||
from_email='user@example.com')
|
||||
|
||||
# invalid from email
|
||||
with pytest.raises(TypeError):
|
||||
NotifySendPulse(
|
||||
client_id='abcd', client_secret='abcd', from_email='!invalid')
|
||||
client_id='abcd', client_secret='abcd456', from_email='!invalid')
|
||||
|
||||
# no email
|
||||
with pytest.raises(TypeError):
|
||||
NotifySendPulse(
|
||||
client_id='abcd', client_secret='abcd', from_email=None)
|
||||
client_id='abcd', client_secret='abcd789', from_email=None)
|
||||
|
||||
# Invalid To email address
|
||||
NotifySendPulse(
|
||||
client_id='abcd', client_secret='abcd',
|
||||
client_id='abcd', client_secret='abcd321',
|
||||
from_email='user@example.com', targets="!invalid")
|
||||
|
||||
# Test invalid bcc/cc entries mixed with good ones
|
||||
assert isinstance(NotifySendPulse(
|
||||
client_id='abcd', client_secret='abcd',
|
||||
client_id='abcd', client_secret='abcd654',
|
||||
from_email='l2g@example.com',
|
||||
bcc=('abc@def.com', '!invalid'),
|
||||
cc=('abc@test.org', '!invalid')), NotifySendPulse)
|
||||
|
@ -175,6 +197,7 @@ def test_plugin_sendpulse_attachments(mock_post, mock_get):
|
|||
|
||||
request = mock.Mock()
|
||||
request.status_code = requests.codes.ok
|
||||
request.content = SENDPULSE_GOOD_RESPONSE
|
||||
|
||||
# Prepare Mock
|
||||
mock_post.return_value = request
|
||||
|
@ -182,7 +205,7 @@ def test_plugin_sendpulse_attachments(mock_post, mock_get):
|
|||
|
||||
path = os.path.join(TEST_VAR_DIR, 'apprise-test.gif')
|
||||
attach = AppriseAttachment(path)
|
||||
obj = Apprise.instantiate('sendpulse://user@example.com/abcd/abcd')
|
||||
obj = Apprise.instantiate('sendpulse://user@example.com/aaaa/bbbb')
|
||||
assert isinstance(obj, NotifySendPulse)
|
||||
assert obj.notify(
|
||||
body='body', title='title', notify_type=NotifyType.INFO,
|
||||
|
|
Loading…
Reference in New Issue