Refactored the way phone numbers are managed (#408)

This commit is contained in:
Chris Caron 2021-07-28 10:32:10 -04:00 committed by GitHub
parent 93c7aef433
commit 8a455695ba
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 652 additions and 389 deletions

View File

@ -47,6 +47,7 @@ from .AppriseAsset import AppriseAsset
from .utils import parse_url
from .utils import parse_bool
from .utils import parse_list
from .utils import parse_phone_no
# Used to break a path list into parts
PATHSPLIT_LIST_DELIM = re.compile(r'[ \t\r\n,\\/]+')
@ -560,6 +561,39 @@ class URLBase(object):
return content
@staticmethod
def parse_phone_no(content, unquote=True):
"""A wrapper to utils.parse_phone_no() with unquoting support
Parses a specified set of data and breaks it into a list.
Args:
content (str): The path to split up into a list. If a list is
provided, then it's individual entries are processed.
unquote (:obj:`bool`, optional): call unquote on each element
added to the returned list.
Returns:
list: A unique list containing all of the elements in the path
"""
if unquote:
try:
content = URLBase.unquote(content)
except TypeError:
# Nothing further to do
return []
except AttributeError:
# This exception ONLY gets thrown under Python v2.7 if an
# object() is passed in place of the content
return []
content = parse_phone_no(content)
return content
@property
def app_id(self):
return self.asset.app_id if self.asset.app_id else ''

View File

@ -36,7 +36,6 @@
# The API reference used to build this plugin was documented here:
# https://developers.clicksend.com/docs/rest/v3/
#
import re
import requests
from json import dumps
from base64 import b64encode
@ -44,7 +43,8 @@ from base64 import b64encode
from .NotifyBase import NotifyBase
from ..URLBase import PrivacyMode
from ..common import NotifyType
from ..utils import parse_list
from ..utils import is_phone_no
from ..utils import parse_phone_no
from ..utils import parse_bool
from ..AppriseLocale import gettext_lazy as _
@ -53,12 +53,6 @@ CLICKSEND_HTTP_ERROR_MAP = {
401: 'Unauthorized - Invalid Token.',
}
# Some Phone Number Detection
IS_PHONE_NO = re.compile(r'^\+?(?P<phone>[0-9\s)(+-]+)\s*$')
# Used to break path apart into list of channels
TARGET_LIST_DELIM = re.compile(r'[ \t\r\n,#\\/]+')
class NotifyClickSend(NotifyBase):
"""
@ -151,26 +145,18 @@ class NotifyClickSend(NotifyBase):
self.logger.warning(msg)
raise TypeError(msg)
for target in parse_list(targets):
for target in parse_phone_no(targets):
# Validate targets and drop bad ones:
result = IS_PHONE_NO.match(target)
if result:
# Further check our phone # for it's digit count
result = ''.join(re.findall(r'\d+', result.group('phone')))
if len(result) < 11 or len(result) > 14:
self.logger.warning(
'Dropped invalid phone # '
'({}) specified.'.format(target),
)
continue
# store valid phone number
self.targets.append(result)
result = is_phone_no(target)
if not result:
self.logger.warning(
'Dropped invalid phone # '
'({}) specified.'.format(target),
)
continue
self.logger.warning(
'Dropped invalid phone # '
'({}) specified.'.format(target))
# store valid phone number
self.targets.append(result['full'])
def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
"""
@ -321,8 +307,7 @@ class NotifyClickSend(NotifyBase):
# Support the 'to' variable so that we can support rooms this way too
# The 'to' makes it easier to use yaml configuration
if 'to' in results['qsd'] and len(results['qsd']['to']):
results['targets'] += [x for x in filter(
bool, TARGET_LIST_DELIM.split(
NotifyClickSend.unquote(results['qsd']['to'])))]
results['targets'] += \
NotifyClickSend.parse_phone_no(results['qsd']['to'])
return results

View File

@ -30,7 +30,6 @@
# (both user and password) from the API Details section from within your
# account profile area: https://d7networks.com/accounts/profile/
import re
import six
import requests
import base64
@ -40,7 +39,8 @@ from json import loads
from .NotifyBase import NotifyBase
from ..URLBase import PrivacyMode
from ..common import NotifyType
from ..utils import parse_list
from ..utils import is_phone_no
from ..utils import parse_phone_no
from ..utils import parse_bool
from ..AppriseLocale import gettext_lazy as _
@ -52,9 +52,6 @@ D7NETWORKS_HTTP_ERROR_MAP = {
500: 'A Serverside Error Occured Handling the Request.',
}
# Some Phone Number Detection
IS_PHONE_NO = re.compile(r'^\+?(?P<phone>[0-9\s)(+-]+)\s*$')
# Priorities
class D7SMSPriority(object):
@ -197,36 +194,26 @@ class NotifyD7Networks(NotifyBase):
self.source = None \
if not isinstance(source, six.string_types) else source.strip()
# Parse our targets
self.targets = list()
for target in parse_list(targets):
# Validate targets and drop bad ones:
result = IS_PHONE_NO.match(target)
if result:
# Further check our phone # for it's digit count
# if it's less than 10, then we can assume it's
# a poorly specified phone no and spit a warning
result = ''.join(re.findall(r'\d+', result.group('phone')))
if len(result) < 11 or len(result) > 14:
self.logger.warning(
'Dropped invalid phone # '
'({}) specified.'.format(target),
)
continue
# store valid phone number
self.targets.append(result)
continue
self.logger.warning(
'Dropped invalid phone # ({}) specified.'.format(target))
if len(self.targets) == 0:
msg = 'There are no valid targets identified to notify.'
if not (self.user and self.password):
msg = 'A D7 Networks user/pass was not provided.'
self.logger.warning(msg)
raise TypeError(msg)
# Parse our targets
self.targets = list()
for target in parse_phone_no(targets):
# Validate targets and drop bad ones:
result = result = is_phone_no(target)
if not result:
self.logger.warning(
'Dropped invalid phone # '
'({}) specified.'.format(target),
)
continue
# store valid phone number
self.targets.append(result['full'])
return
def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
@ -235,6 +222,11 @@ class NotifyD7Networks(NotifyBase):
redirects to the appropriate handling
"""
if len(self.targets) == 0:
# There were no services to notify
self.logger.warning('There were no D7 Networks targets to notify.')
return False
# error tracking (used for function return)
has_error = False
@ -479,6 +471,6 @@ class NotifyD7Networks(NotifyBase):
# The 'to' makes it easier to use yaml configuration
if 'to' in results['qsd'] and len(results['qsd']['to']):
results['targets'] += \
NotifyD7Networks.parse_list(results['qsd']['to'])
NotifyD7Networks.parse_phone_no(results['qsd']['to'])
return results

View File

@ -32,13 +32,13 @@
# This provider does not accept +1 (for example) as a country code. You need
# to specify 001 instead.
#
import re
import requests
from json import loads
from .NotifyBase import NotifyBase
from ..common import NotifyType
from ..utils import parse_list
from ..utils import is_phone_no
from ..utils import parse_phone_no
from ..utils import validate_regex
from ..AppriseLocale import gettext_lazy as _
@ -68,9 +68,6 @@ KAVENEGAR_HTTP_ERROR_MAP = {
501: 'SMS can only be sent to the account holder number',
}
# Some Phone Number Detection
IS_PHONE_NO = re.compile(r'^\+?(?P<phone>[0-9\s)(+-]+)\s*$')
class NotifyKavenegar(NotifyBase):
"""
@ -165,53 +162,31 @@ class NotifyKavenegar(NotifyBase):
self.source = None
if source is not None:
result = IS_PHONE_NO.match(source)
result = is_phone_no(source)
if not result:
msg = 'The Kavenegar source specified ({}) is invalid.'\
.format(source)
self.logger.warning(msg)
raise TypeError(msg)
# Further check our phone # for it's digit count
result = ''.join(re.findall(r'\d+', result.group('phone')))
if len(result) < 11 or len(result) > 14:
msg = 'The MessageBird source # specified ({}) is invalid.'\
.format(source)
self.logger.warning(msg)
raise TypeError(msg)
# Store our source
self.source = result
self.source = result['full']
# Parse our targets
self.targets = list()
for target in parse_list(targets):
for target in parse_phone_no(targets):
# Validate targets and drop bad ones:
result = IS_PHONE_NO.match(target)
if result:
# Further check our phone # for it's digit count
# if it's less than 10, then we can assume it's
# a poorly specified phone no and spit a warning
result = ''.join(re.findall(r'\d+', result.group('phone')))
if len(result) < 11 or len(result) > 14:
self.logger.warning(
'Dropped invalid phone # '
'({}) specified.'.format(target),
)
continue
# store valid phone number
self.targets.append(result)
result = is_phone_no(target)
if not result:
self.logger.warning(
'Dropped invalid phone # '
'({}) specified.'.format(target),
)
continue
self.logger.warning(
'Dropped invalid phone # ({}) specified.'.format(target))
if len(self.targets) == 0:
msg = 'There are no valid targets identified to notify.'
self.logger.warning(msg)
raise TypeError(msg)
# store valid phone number
self.targets.append(result['full'])
return
@ -220,6 +195,11 @@ class NotifyKavenegar(NotifyBase):
Sends SMS Message
"""
if len(self.targets) == 0:
# There were no services to notify
self.logger.warning('There were no Kavenegar targets to notify.')
return False
# error tracking (used for function return)
has_error = False
@ -364,7 +344,7 @@ class NotifyKavenegar(NotifyBase):
# The 'to' makes it easier to use yaml configuration
if 'to' in results['qsd'] and len(results['qsd']['to']):
results['targets'] += \
NotifyKavenegar.parse_list(results['qsd']['to'])
NotifyKavenegar.parse_phone_no(results['qsd']['to'])
if 'from' in results['qsd'] and len(results['qsd']['from']):
results['source'] = \

View File

@ -31,18 +31,15 @@
# Get details on the API used in this plugin here:
# - https://world.msg91.com/apidoc/textsms/send-sms.php
import re
import requests
from .NotifyBase import NotifyBase
from ..common import NotifyType
from ..utils import parse_list
from ..utils import is_phone_no
from ..utils import parse_phone_no
from ..utils import validate_regex
from ..AppriseLocale import gettext_lazy as _
# Some Phone Number Detection
IS_PHONE_NO = re.compile(r'^\+?(?P<phone>[0-9\s)(+-]+)\s*$')
class MSG91Route(object):
"""
@ -207,33 +204,18 @@ class NotifyMSG91(NotifyBase):
# Parse our targets
self.targets = list()
for target in parse_list(targets):
for target in parse_phone_no(targets):
# Validate targets and drop bad ones:
result = IS_PHONE_NO.match(target)
if result:
# Further check our phone # for it's digit count
result = ''.join(re.findall(r'\d+', result.group('phone')))
if len(result) < 11 or len(result) > 14:
self.logger.warning(
'Dropped invalid phone # '
'({}) specified.'.format(target),
)
continue
# store valid phone number
self.targets.append(result)
result = is_phone_no(target)
if not result:
self.logger.warning(
'Dropped invalid phone # '
'({}) specified.'.format(target),
)
continue
self.logger.warning(
'Dropped invalid phone # '
'({}) specified.'.format(target),
)
if not self.targets:
# We have a bot token and no target(s) to message
msg = 'No MSG91 targets to notify.'
self.logger.warning(msg)
raise TypeError(msg)
# store valid phone number
self.targets.append(result['full'])
return
@ -242,6 +224,11 @@ class NotifyMSG91(NotifyBase):
Perform MSG91 Notification
"""
if len(self.targets) == 0:
# There were no services to notify
self.logger.warning('There were no MSG91 targets to notify.')
return False
# Prepare our headers
headers = {
'User-Agent': self.app_id,
@ -365,6 +352,6 @@ class NotifyMSG91(NotifyBase):
# The 'to' makes it easier to use yaml configuration
if 'to' in results['qsd'] and len(results['qsd']['to']):
results['targets'] += \
NotifyMSG91.parse_list(results['qsd']['to'])
NotifyMSG91.parse_phone_no(results['qsd']['to'])
return results

View File

@ -29,18 +29,15 @@
# - https://dashboard.messagebird.com/en/user/index
#
import re
import requests
from .NotifyBase import NotifyBase
from ..common import NotifyType
from ..utils import parse_list
from ..utils import is_phone_no
from ..utils import parse_phone_no
from ..utils import validate_regex
from ..AppriseLocale import gettext_lazy as _
# Some Phone Number Detection
IS_PHONE_NO = re.compile(r'^\+?(?P<phone>[0-9\s)(+-]+)\s*$')
class NotifyMessageBird(NotifyBase):
"""
@ -129,28 +126,20 @@ class NotifyMessageBird(NotifyBase):
self.logger.warning(msg)
raise TypeError(msg)
result = IS_PHONE_NO.match(source)
result = is_phone_no(source)
if not result:
msg = 'The MessageBird source specified ({}) is invalid.'\
.format(source)
self.logger.warning(msg)
raise TypeError(msg)
# Further check our phone # for it's digit count
result = ''.join(re.findall(r'\d+', result.group('phone')))
if len(result) < 11 or len(result) > 14:
msg = 'The MessageBird source # specified ({}) is invalid.'\
.format(source)
self.logger.warning(msg)
raise TypeError(msg)
# Store our source
self.source = result
self.source = result['full']
# Parse our targets
self.targets = list()
targets = parse_list(targets)
targets = parse_phone_no(targets)
if not targets:
# No sources specified, use our own phone no
self.targets.append(self.source)
@ -159,31 +148,16 @@ class NotifyMessageBird(NotifyBase):
# otherwise, store all of our target numbers
for target in targets:
# Validate targets and drop bad ones:
result = IS_PHONE_NO.match(target)
if result:
# Further check our phone # for it's digit count
result = ''.join(re.findall(r'\d+', result.group('phone')))
if len(result) < 11 or len(result) > 14:
self.logger.warning(
'Dropped invalid phone # '
'({}) specified.'.format(target),
)
continue
# store valid phone number
self.targets.append(result)
result = is_phone_no(target)
if not result:
self.logger.warning(
'Dropped invalid phone # '
'({}) specified.'.format(target),
)
continue
self.logger.warning(
'Dropped invalid phone # '
'({}) specified.'.format(target),
)
if not self.targets:
# We have a bot token and no target(s) to message
msg = 'No MessageBird targets to notify.'
self.logger.warning(msg)
raise TypeError(msg)
# store valid phone number
self.targets.append(result['full'])
return
@ -192,6 +166,11 @@ class NotifyMessageBird(NotifyBase):
Perform MessageBird Notification
"""
if len(self.targets) == 0:
# There were no services to notify
self.logger.warning('There were no MessageBird targets to notify.')
return False
# error tracking (used for function return)
has_error = False
@ -345,6 +324,7 @@ class NotifyMessageBird(NotifyBase):
try:
# The first path entry is the source/originator
results['source'] = results['targets'].pop(0)
except IndexError:
# No path specified... this URL is potentially un-parseable; we can
# hope for a from= entry
@ -357,7 +337,7 @@ class NotifyMessageBird(NotifyBase):
# The 'to' makes it easier to use yaml configuration
if 'to' in results['qsd'] and len(results['qsd']['to']):
results['targets'] += \
NotifyMessageBird.parse_list(results['qsd']['to'])
NotifyMessageBird.parse_phone_no(results['qsd']['to'])
if 'from' in results['qsd'] and len(results['qsd']['from']):
results['source'] = \

View File

@ -28,20 +28,16 @@
# Get your (api) key and secret here:
# - https://dashboard.nexmo.com/getting-started-guide
#
import re
import requests
from .NotifyBase import NotifyBase
from ..URLBase import PrivacyMode
from ..common import NotifyType
from ..utils import parse_list
from ..utils import is_phone_no
from ..utils import parse_phone_no
from ..utils import validate_regex
from ..AppriseLocale import gettext_lazy as _
# Some Phone Number Detection
IS_PHONE_NO = re.compile(r'^\+?(?P<phone>[0-9\s)(+-]+)\s*$')
class NotifyNexmo(NotifyBase):
"""
@ -185,44 +181,31 @@ class NotifyNexmo(NotifyBase):
# The Source Phone #
self.source = source
if not IS_PHONE_NO.match(self.source):
result = is_phone_no(source)
if not result:
msg = 'The Account (From) Phone # specified ' \
'({}) is invalid.'.format(source)
self.logger.warning(msg)
raise TypeError(msg)
# Tidy source
self.source = re.sub(r'[^\d]+', '', self.source)
if len(self.source) < 11 or len(self.source) > 14:
msg = 'The Account (From) Phone # specified ' \
'({}) contains an invalid digit count.'.format(source)
self.logger.warning(msg)
raise TypeError(msg)
# Store our parsed value
self.source = result['full']
# Parse our targets
self.targets = list()
for target in parse_list(targets):
for target in parse_phone_no(targets):
# Validate targets and drop bad ones:
result = IS_PHONE_NO.match(target)
if result:
# Further check our phone # for it's digit count
result = ''.join(re.findall(r'\d+', result.group('phone')))
if len(result) < 11 or len(result) > 14:
self.logger.warning(
'Dropped invalid phone # '
'({}) specified.'.format(target),
)
continue
# store valid phone number
self.targets.append(result)
result = is_phone_no(target)
if not result:
self.logger.warning(
'Dropped invalid phone # '
'({}) specified.'.format(target),
)
continue
self.logger.warning(
'Dropped invalid phone # '
'({}) specified.'.format(target),
)
# store valid phone number
self.targets.append(result['full'])
return
@ -393,10 +376,10 @@ class NotifyNexmo(NotifyBase):
results['ttl'] = \
NotifyNexmo.unquote(results['qsd']['ttl'])
# Support the 'to' variable so that we can support targets this way too
# Support the 'to' variable so that we can support rooms this way too
# The 'to' makes it easier to use yaml configuration
if 'to' in results['qsd'] and len(results['qsd']['to']):
results['targets'] += \
NotifyNexmo.parse_list(results['qsd']['to'])
NotifyNexmo.parse_phone_no(results['qsd']['to'])
return results

View File

@ -23,20 +23,17 @@
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
import re
import requests
from .NotifyBase import NotifyBase
from ..common import NotifyType
from ..utils import is_email
from ..utils import is_phone_no
from ..utils import parse_list
from ..utils import parse_bool
from ..utils import validate_regex
from ..AppriseLocale import gettext_lazy as _
# Some Phone Number Detection
IS_PHONE_NO = re.compile(r'^\+?(?P<phone>[0-9\s)(+-]+)\s*$')
class NotifyPopcornNotify(NotifyBase):
"""
@ -127,19 +124,10 @@ class NotifyPopcornNotify(NotifyBase):
for target in parse_list(targets):
# Validate targets and drop bad ones:
result = IS_PHONE_NO.match(target)
result = is_phone_no(target)
if result:
# Further check our phone # for it's digit count
result = ''.join(re.findall(r'\d+', result.group('phone')))
if len(result) < 11 or len(result) > 14:
self.logger.warning(
'Dropped invalid phone # '
'({}) specified.'.format(target),
)
continue
# store valid phone number
self.targets.append(result)
self.targets.append(result['full'])
continue
result = is_email(target)

View File

@ -35,13 +35,11 @@ from itertools import chain
from .NotifyBase import NotifyBase
from ..URLBase import PrivacyMode
from ..common import NotifyType
from ..utils import is_phone_no
from ..utils import parse_list
from ..utils import validate_regex
from ..AppriseLocale import gettext_lazy as _
# Some Phone Number Detection
IS_PHONE_NO = re.compile(r'^\+?(?P<phone>[0-9\s)(+-]+)\s*$')
# Topic Detection
# Summary: 256 Characters max, only alpha/numeric plus underscore (_) and
# dash (-) additionally allowed.
@ -198,24 +196,10 @@ class NotifySNS(NotifyBase):
self.aws_auth_algorithm = 'AWS4-HMAC-SHA256'
self.aws_auth_request = 'aws4_request'
# Get our targets
targets = parse_list(targets)
# Validate targets and drop bad ones:
for target in targets:
result = IS_PHONE_NO.match(target)
for target in parse_list(targets):
result = is_phone_no(target)
if result:
# Further check our phone # for it's digit count
# if it's less than 10, then we can assume it's
# a poorly specified phone no and spit a warning
result = ''.join(re.findall(r'\d+', result.group('phone')))
if len(result) < 11 or len(result) > 14:
self.logger.warning(
'Dropped invalid phone # '
'(%s) specified.' % target,
)
continue
# store valid phone number
self.phone.append('+{}'.format(result))
continue
@ -231,12 +215,6 @@ class NotifySNS(NotifyBase):
'(%s) specified.' % target,
)
if len(self.phone) == 0 and len(self.topics) == 0:
# We have a bot token and no target(s) to message
msg = 'No AWS targets to notify.'
self.logger.warning(msg)
raise TypeError(msg)
return
def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
@ -244,6 +222,11 @@ class NotifySNS(NotifyBase):
wrapper to send_notification since we can alert more then one channel
"""
if len(self.phone) == 0 and len(self.topics) == 0:
# We have a bot token and no target(s) to message
self.logger.warning('No AWS targets to notify.')
return False
# Initiaize our error tracking
error_count = 0

View File

@ -33,7 +33,6 @@
# from). Activated phone numbers can be found on your dashboard here:
# - https://dashboard.sinch.com/numbers/your-numbers/numbers
#
import re
import six
import requests
import json
@ -41,15 +40,12 @@ import json
from .NotifyBase import NotifyBase
from ..URLBase import PrivacyMode
from ..common import NotifyType
from ..utils import parse_list
from ..utils import is_phone_no
from ..utils import parse_phone_no
from ..utils import validate_regex
from ..AppriseLocale import gettext_lazy as _
# Some Phone Number Detection
IS_PHONE_NO = re.compile(r'^\+?(?P<phone>[0-9\s)(+-]+)\s*$')
class SinchRegion(object):
"""
Defines the Sinch Server Regions
@ -194,15 +190,6 @@ class NotifySinch(NotifyBase):
self.logger.warning(msg)
raise TypeError(msg)
# The Source Phone # and/or short-code
self.source = source
if not IS_PHONE_NO.match(self.source):
msg = 'The Account (From) Phone # or Short-code specified ' \
'({}) is invalid.'.format(source)
self.logger.warning(msg)
raise TypeError(msg)
# Setup our region
self.region = self.template_args['region']['default'] \
if not isinstance(region, six.string_types) else region.lower()
@ -211,8 +198,16 @@ class NotifySinch(NotifyBase):
self.logger.warning(msg)
raise TypeError(msg)
# The Source Phone # and/or short-code
result = is_phone_no(source, min_len=5)
if not result:
msg = 'The Account (From) Phone # or Short-code specified ' \
'({}) is invalid.'.format(source)
self.logger.warning(msg)
raise TypeError(msg)
# Tidy source
self.source = re.sub(r'[^\d]+', '', self.source)
self.source = result['full']
if len(self.source) < 11 or len(self.source) > 14:
# A short code is a special 5 or 6 digit telephone number
@ -233,37 +228,18 @@ class NotifySinch(NotifyBase):
# Parse our targets
self.targets = list()
for target in parse_list(targets):
# Validate targets and drop bad ones:
result = IS_PHONE_NO.match(target)
if result:
# Further check our phone # for it's digit count
# if it's less than 10, then we can assume it's
# a poorly specified phone no and spit a warning
result = ''.join(re.findall(r'\d+', result.group('phone')))
if len(result) < 11 or len(result) > 14:
self.logger.warning(
'Dropped invalid phone # '
'({}) specified.'.format(target),
)
continue
# store valid phone number
self.targets.append('+{}'.format(result))
for target in parse_phone_no(targets):
# Parse each phone number we found
result = is_phone_no(target)
if not result:
self.logger.warning(
'Dropped invalid phone # '
'({}) specified.'.format(target),
)
continue
self.logger.warning(
'Dropped invalid phone # '
'({}) specified.'.format(target),
)
if not self.targets:
if len(self.source) in (5, 6):
# raise a warning since we're a short-code. We need
# a number to message
msg = 'There are no valid Sinch targets to notify.'
self.logger.warning(msg)
raise TypeError(msg)
# store valid phone number
self.targets.append('+{}'.format(result['full']))
return
@ -272,6 +248,14 @@ class NotifySinch(NotifyBase):
Perform Sinch Notification
"""
if not self.targets:
if len(self.source) in (5, 6):
# Generate a warning since we're a short-code. We need
# a number to message at minimum
self.logger.warning(
'There are no valid Sinch targets to notify.')
return False
# error tracking (used for function return)
has_error = False
@ -459,6 +443,7 @@ class NotifySinch(NotifyBase):
if 'from' in results['qsd'] and len(results['qsd']['from']):
results['source'] = \
NotifySinch.unquote(results['qsd']['from'])
if 'source' in results['qsd'] and len(results['qsd']['source']):
results['source'] = \
NotifySinch.unquote(results['qsd']['source'])
@ -472,6 +457,6 @@ class NotifySinch(NotifyBase):
# The 'to' makes it easier to use yaml configuration
if 'to' in results['qsd'] and len(results['qsd']['to']):
results['targets'] += \
NotifySinch.parse_list(results['qsd']['to'])
NotifySinch.parse_phone_no(results['qsd']['to'])
return results

View File

@ -40,22 +40,18 @@
# or consider purchasing a short-code from here:
# https://www.twilio.com/docs/glossary/what-is-a-short-code
#
import re
import requests
from json import loads
from .NotifyBase import NotifyBase
from ..URLBase import PrivacyMode
from ..common import NotifyType
from ..utils import parse_list
from ..utils import is_phone_no
from ..utils import parse_phone_no
from ..utils import validate_regex
from ..AppriseLocale import gettext_lazy as _
# Some Phone Number Detection
IS_PHONE_NO = re.compile(r'^\+?(?P<phone>[0-9\s)(+-]+)\s*$')
class NotifyTwilio(NotifyBase):
"""
A wrapper for Twilio Notifications
@ -181,17 +177,15 @@ class NotifyTwilio(NotifyBase):
self.logger.warning(msg)
raise TypeError(msg)
# The Source Phone # and/or short-code
self.source = source
if not IS_PHONE_NO.match(self.source):
result = is_phone_no(source, min_len=5)
if not result:
msg = 'The Account (From) Phone # or Short-code specified ' \
'({}) is invalid.'.format(source)
self.logger.warning(msg)
raise TypeError(msg)
# Tidy source
self.source = re.sub(r'[^\d]+', '', self.source)
# Store The Source Phone # and/or short-code
self.source = result['full']
if len(self.source) < 11 or len(self.source) > 14:
# https://www.twilio.com/docs/glossary/what-is-a-short-code
@ -213,37 +207,18 @@ class NotifyTwilio(NotifyBase):
# Parse our targets
self.targets = list()
for target in parse_list(targets):
for target in parse_phone_no(targets):
# Validate targets and drop bad ones:
result = IS_PHONE_NO.match(target)
if result:
# Further check our phone # for it's digit count
# if it's less than 10, then we can assume it's
# a poorly specified phone no and spit a warning
result = ''.join(re.findall(r'\d+', result.group('phone')))
if len(result) < 11 or len(result) > 14:
self.logger.warning(
'Dropped invalid phone # '
'({}) specified.'.format(target),
)
continue
# store valid phone number
self.targets.append('+{}'.format(result))
result = is_phone_no(target)
if not result:
self.logger.warning(
'Dropped invalid phone # '
'({}) specified.'.format(target),
)
continue
self.logger.warning(
'Dropped invalid phone # '
'({}) specified.'.format(target),
)
if not self.targets:
if len(self.source) in (5, 6):
# raise a warning since we're a short-code. We need
# a number to message
msg = 'There are no valid Twilio targets to notify.'
self.logger.warning(msg)
raise TypeError(msg)
# store valid phone number
self.targets.append('+{}'.format(result))
return
@ -252,6 +227,14 @@ class NotifyTwilio(NotifyBase):
Perform Twilio Notification
"""
if not self.targets:
if len(self.source) in (5, 6):
# Generate a warning since we're a short-code. We need
# a number to message at minimum
self.logger.warning(
'There are no valid Twilio targets to notify.')
return False
# error tracking (used for function return)
has_error = False
@ -431,6 +414,6 @@ class NotifyTwilio(NotifyBase):
# The 'to' makes it easier to use yaml configuration
if 'to' in results['qsd'] and len(results['qsd']['to']):
results['targets'] += \
NotifyTwilio.parse_list(results['qsd']['to'])
NotifyTwilio.parse_phone_no(results['qsd']['to'])
return results

View File

@ -115,7 +115,7 @@ GET_SCHEMA_RE = re.compile(r'\s*(?P<schema>[a-z0-9]{2,9})://.*$', re.I)
# - user@example.com
# - label+user@example.com
GET_EMAIL_RE = re.compile(
r'((?P<name>[^:<]+)?[:<\s]+)?'
r'(([\s"\']+)?(?P<name>[^:<"\']+)?[:<\s"\']+)?'
r'(?P<full_email>((?P<label>[^+]+)\+)?'
r'(?P<email>(?P<userid>[a-z0-9$%=_~-]+'
r'(?:\.[a-z0-9$%+=_~-]+)'
@ -125,8 +125,13 @@ GET_EMAIL_RE = re.compile(
r'[a-z0-9][a-z0-9_-]{5,})))'
r'\s*>?', re.IGNORECASE)
# Regular expression used to extract a phone number
GET_PHONE_NO_RE = re.compile(r'^\+?(?P<phone>[0-9\s)(+-]+)\s*$')
# A simple verification check to make sure the content specified
# rougly conforms to a phone number before we parse it further
IS_PHONE_NO = re.compile(r'^\+?(?P<phone>[0-9\s)(+-]+)\s*$')
# Regular expression used to destinguish between multiple phone numbers
PHONE_NO_DETECTION_RE = re.compile(
r'\s*([+(\s]*[0-9][0-9()\s-]+[0-9])(?=$|[\s,+(]+[0-9])', re.I)
# Regular expression used to destinguish between multiple URLs
URL_DETECTION_RE = re.compile(
@ -273,6 +278,98 @@ def is_uuid(uuid):
return True if match else False
def is_phone_no(phone, min_len=11):
"""Determine if the specified entry is a phone number
Args:
phone (str): The string you want to check.
min_len (int): Defines the smallest expected length of the phone
before it's to be considered invalid. By default
the phone number can't be any larger then 14
Returns:
bool: Returns False if the address specified is not a phone number
and a dictionary of the parsed email if it is as:
{
'country': '1',
'area': '800',
'line': '1234567',
'full': '18001234567',
'pretty': '+1 800-123-4567',
}
Non conventional numbers such as 411 would look like provided that
`min_len` is set to at least a 3:
{
'country': '',
'area': '',
'line': '411',
'full': '411',
'pretty': '411',
}
"""
try:
if not IS_PHONE_NO.match(phone):
# not parseable content as it does not even conform closely to a
# phone number)
return False
except TypeError:
return False
# Tidy phone number up first
phone = re.sub(r'[^\d]+', '', phone)
if len(phone) > 14 or len(phone) < min_len:
# Invalid phone number
return False
# Full phone number without any markup is as is now
full = phone
# Break apart our phone number
line = phone[-7:]
phone = phone[:len(phone) - 7] if len(phone) > 7 else ''
# the area code (if present)
area = phone[-3:] if phone else ''
# The country code is the leftovers
country = phone[:len(phone) - 3] if len(phone) > 3 else ''
# Prepare a nicely (consistently) formatted phone number
pretty = ''
if country:
# The leftover is the country code
pretty += '+{} '.format(country)
if area:
pretty += '{}-'.format(area)
if len(line) >= 7:
pretty += '{}-{}'.format(line[:3], line[3:])
else:
pretty += line
return {
# The line code (last 7 digits)
'line': line,
# Area code
'area': area,
# The country code (if identified)
'country': country,
# A nicely formatted phone no
'pretty': pretty,
# All digits in-line
'full': full,
}
def is_email(address):
"""Determine if the specified entry is an email address
@ -633,9 +730,46 @@ def parse_bool(arg, default=False):
return bool(arg)
def parse_phone_no(*args, **kwargs):
"""
Takes a string containing phone numbers separated by comma's and/or spaces
and returns a list.
"""
# for Python 2.7 support, store_unparsable is not in the url above
# as just parse_emails(*args, store_unparseable=True) since it is
# an invalid syntax. This is the workaround to be backards compatible:
store_unparseable = kwargs.get('store_unparseable', True)
result = []
for arg in args:
if isinstance(arg, six.string_types) and arg:
_result = PHONE_NO_DETECTION_RE.findall(arg)
if _result:
result += _result
elif not _result and store_unparseable:
# we had content passed into us that was lost because it was
# so poorly formatted that it didn't even come close to
# meeting the regular expression we defined. We intentially
# keep it as part of our result set so that parsing done
# at a higher level can at least report this to the end user
# and hopefully give them some indication as to what they
# may have done wrong.
result += \
[x for x in filter(bool, re.split(STRING_DELIMITERS, arg))]
elif isinstance(arg, (set, list, tuple)):
# Use recursion to handle the list of phone numbers
result += parse_phone_no(
*arg, store_unparseable=store_unparseable)
return result
def parse_emails(*args, **kwargs):
"""
Takes a string containing URLs separated by comma's and/or spaces and
Takes a string containing emails separated by comma's and/or spaces and
returns a list.
"""

View File

@ -207,6 +207,7 @@ def test_notify_base():
# Test invalid data
assert NotifyBase.parse_list(None) == []
assert NotifyBase.parse_list(object()) == []
assert NotifyBase.parse_list(42) == []
result = NotifyBase.parse_list(
@ -234,6 +235,26 @@ def test_notify_base():
assert '//' in result
assert '///' in result
# Phone number parsing
assert NotifyBase.parse_phone_no(None) == []
assert NotifyBase.parse_phone_no(object()) == []
assert NotifyBase.parse_phone_no(42) == []
result = NotifyBase.parse_phone_no(
'+1-800-123-1234,(800) 123-4567', unquote=False)
assert isinstance(result, list) is True
assert len(result) == 2
assert '+1-800-123-1234' in result
assert '(800) 123-4567' in result
# %2B == +
result = NotifyBase.parse_phone_no(
'%2B1-800-123-1234,%2B1%20800%20123%204567', unquote=True)
assert isinstance(result, list) is True
assert len(result) == 2
assert '+1-800-123-1234' in result
assert '+1 800 123 4567' in result
# Give nothing, get nothing
assert NotifyBase.escape_html("") == ""
assert NotifyBase.escape_html(None) == ""

View File

@ -196,7 +196,10 @@ TEST_URLS = (
}),
('d7sms://user:pass@{}/{}/{}'.format('1' * 10, '2' * 15, 'a' * 13), {
# No valid targets to notify
'instance': TypeError,
'instance': plugins.NotifyD7Networks,
# Since there are no targets specified we expect a False return on
# send()
'notify_response': False,
}),
('d7sms://user:pass@{}?batch=yes'.format('3' * 14), {
# valid number
@ -1297,8 +1300,11 @@ TEST_URLS = (
'instance': TypeError,
}),
('kavenegar://{}/{}/{}'.format('1' * 10, '2' * 15, 'a' * 13), {
# No valid targets to notify
'instance': TypeError,
# valid api key and valid authentication
'instance': plugins.NotifyKavenegar,
# Since there are no targets specified we expect a False return on
# send()
'notify_response': False,
}),
('kavenegar://{}/{}'.format('a' * 24, '3' * 14), {
# valid api key and valid number
@ -4034,7 +4040,9 @@ TEST_URLS = (
('sinch://{}:{}@{}'.format('a' * 32, 'b' * 32, '3' * 5), {
# using short-code (5 characters) without a target
# We can still instantiate ourselves with a valid short code
'instance': TypeError,
'instance': plugins.NotifySinch,
# Expected notify() response because we have no one to notify
'notify_response': False,
}),
('sinch://{}:{}@{}'.format('a' * 32, 'b' * 32, '3' * 9), {
# spi and token provided and from but invalid from no
@ -4942,7 +4950,10 @@ TEST_URLS = (
('twilio://AC{}:{}@{}'.format('a' * 32, 'b' * 32, '3' * 5), {
# using short-code (5 characters) without a target
# We can still instantiate ourselves with a valid short code
'instance': TypeError,
'instance': plugins.NotifyTwilio,
# Since there are no targets specified we expect a False return on
# send()
'notify_response': False,
}),
('twilio://AC{}:{}@{}'.format('a' * 32, 'b' * 32, '3' * 9), {
# sid and token provided and from but invalid from no
@ -5211,16 +5222,25 @@ TEST_URLS = (
'instance': TypeError,
}),
('msg91://{}'.format('a' * 23), {
# No number specified
'instance': TypeError,
# valid AuthKey
'instance': plugins.NotifyMSG91,
# Since there are no targets specified we expect a False return on
# send()
'notify_response': False,
}),
('msg91://{}/123'.format('a' * 23), {
# invalid phone number
'instance': TypeError,
'instance': plugins.NotifyMSG91,
# Since there are no targets specified we expect a False return on
# send()
'notify_response': False,
}),
('msg91://{}/abcd'.format('a' * 23), {
# No number to notify
'instance': TypeError,
'instance': plugins.NotifyMSG91,
# Since there are no targets specified we expect a False return on
# send()
'notify_response': False,
}),
('msg91://{}/15551232000/?country=invalid'.format('a' * 23), {
# invalid country
@ -5293,12 +5313,18 @@ TEST_URLS = (
'privacy_url': 'msgbird://a...a/15551232000',
}),
('msgbird://{}/15551232000/abcd'.format('a' * 25), {
# invalid target phone number; we have no one to notify
'instance': TypeError,
# valid credentials
'instance': plugins.NotifyMessageBird,
# Since there are no targets specified we expect a False return on
# send()
'notify_response': False,
}),
('msgbird://{}/15551232000/123'.format('a' * 25), {
# invalid target phone number
'instance': TypeError,
# valid credentials
'instance': plugins.NotifyMessageBird,
# Since there are no targets specified we expect a False return on
# send()
'notify_response': False,
}),
('msgbird://{}/?from=15551233000&to=15551232000'.format('a' * 25), {
# reference to to= and from=

View File

@ -39,7 +39,10 @@ TEST_ACCESS_KEY_SECRET = 'bu1dHSdO22pfaaVy/wmNsdljF4C07D3bndi9PQJ9'
TEST_REGION = 'us-east-2'
def test_object_initialization():
# We initialize a post object just incase a test fails below
# we don't want it sending any notifications upstream
@mock.patch('requests.post')
def test_object_initialization(mock_post):
"""
API: NotifySNS Plugin() initialization
@ -73,43 +76,40 @@ def test_object_initialization():
targets='+1800555999',
)
with pytest.raises(TypeError):
# No recipients
plugins.NotifySNS(
access_key_id=TEST_ACCESS_KEY_ID,
secret_access_key=TEST_ACCESS_KEY_SECRET,
region_name=TEST_REGION,
targets=None,
)
# No recipients
obj = plugins.NotifySNS(
access_key_id=TEST_ACCESS_KEY_ID,
secret_access_key=TEST_ACCESS_KEY_SECRET,
region_name=TEST_REGION,
targets=None,
)
with pytest.raises(TypeError):
# No recipients - garbage recipients object
plugins.NotifySNS(
access_key_id=TEST_ACCESS_KEY_ID,
secret_access_key=TEST_ACCESS_KEY_SECRET,
region_name=TEST_REGION,
targets=object(),
)
# The object initializes properly but would not be able to send anything
assert obj.notify(body='test', title='test') is False
with pytest.raises(TypeError):
# The phone number is invalid, and without it, there is nothing
# to notify
plugins.NotifySNS(
access_key_id=TEST_ACCESS_KEY_ID,
secret_access_key=TEST_ACCESS_KEY_SECRET,
region_name=TEST_REGION,
targets='+1809',
)
# The phone number is invalid, and without it, there is nothing
# to notify
obj = plugins.NotifySNS(
access_key_id=TEST_ACCESS_KEY_ID,
secret_access_key=TEST_ACCESS_KEY_SECRET,
region_name=TEST_REGION,
targets='+1809',
)
with pytest.raises(TypeError):
# The phone number is invalid, and without it, there is nothing
# to notify; we
plugins.NotifySNS(
access_key_id=TEST_ACCESS_KEY_ID,
secret_access_key=TEST_ACCESS_KEY_SECRET,
region_name=TEST_REGION,
targets='#(invalid-topic-because-of-the-brackets)',
)
# The object initializes properly but would not be able to send anything
assert obj.notify(body='test', title='test') is False
# The phone number is invalid, and without it, there is nothing
# to notify; we
obj = plugins.NotifySNS(
access_key_id=TEST_ACCESS_KEY_ID,
secret_access_key=TEST_ACCESS_KEY_SECRET,
region_name=TEST_REGION,
targets='#(invalid-topic-because-of-the-brackets)',
)
# The object initializes properly but would not be able to send anything
assert obj.notify(body='test', title='test') is False
def test_url_parsing():
@ -170,16 +170,17 @@ def test_object_parsing():
assert a.add('sns://nosecret') is False
assert a.add('sns://nosecret/noregion/') is False
# This is valid but without valid recipients, the URL is actually useless
assert a.add('sns://norecipient/norecipient/us-west-2') is False
assert len(a) == 0
# This is valid but without valid recipients; while it's still a valid URL
# it won't do much when the user goes to send a notification
assert a.add('sns://norecipient/norecipient/us-west-2') is True
assert len(a) == 1
# Parse a good one
assert a.add('sns://oh/yeah/us-west-2/abcdtopic/+12223334444') is True
assert len(a) == 1
assert len(a) == 2
assert a.add('sns://oh/yeah/us-west-2/12223334444') is True
assert len(a) == 2
assert len(a) == 3
def test_aws_response_handling():

View File

@ -694,6 +694,15 @@ def test_is_email():
assert 'spichai' == results['user']
assert 'ceo' == results['label']
# Support Quotes
results = utils.is_email('"Chris Hemsworth" <ch@test.com>')
assert 'Chris Hemsworth' == results['name']
assert 'ch@test.com' == results['email']
assert 'ch@test.com' == results['full_email']
assert 'test.com' == results['domain']
assert 'ch' == results['user']
assert '' == results['label']
# An email without name, but contains delimiters
results = utils.is_email(' <spichai@gmail.com>')
assert '' == results['name']
@ -731,6 +740,198 @@ def test_is_email():
assert utils.is_email("Name <bademail>") is False
def test_is_phone_no():
"""
API: is_phone_no() function
"""
# Invalid numbers
assert utils.is_phone_no(None) is False
assert utils.is_phone_no(42) is False
assert utils.is_phone_no(object) is False
assert utils.is_phone_no('') is False
assert utils.is_phone_no('1') is False
assert utils.is_phone_no('12') is False
assert utils.is_phone_no('abc') is False
assert utils.is_phone_no('+()') is False
assert utils.is_phone_no('+') is False
assert utils.is_phone_no(None) is False
assert utils.is_phone_no(42) is False
assert utils.is_phone_no(object, min_len=0) is False
assert utils.is_phone_no('', min_len=1) is False
assert utils.is_phone_no('abc', min_len=0) is False
assert utils.is_phone_no('', min_len=0) is False
# Ambigious, but will document it here in this test as such
results = utils.is_phone_no('+((()))--+', min_len=0)
assert '' == results['country']
assert '' == results['area']
assert '' == results['line']
assert '' == results['pretty']
assert '' == results['full']
# Valid phone numbers
assert utils.is_phone_no('+(0)') is False
results = utils.is_phone_no('+(0)', min_len=1)
assert '' == results['country']
assert '' == results['area']
assert '0' == results['line']
assert '0' == results['pretty']
assert '0' == results['full']
assert utils.is_phone_no('1') is False
results = utils.is_phone_no('1', min_len=1)
assert '' == results['country']
assert '' == results['area']
assert '1' == results['line']
assert '1' == results['pretty']
assert '1' == results['full']
assert utils.is_phone_no('12') is False
results = utils.is_phone_no('12', min_len=2)
assert '' == results['country']
assert '' == results['area']
assert '12' == results['line']
assert '12' == results['pretty']
assert '12' == results['full']
assert utils.is_phone_no('911') is False
results = utils.is_phone_no('911', min_len=3)
assert isinstance(results, dict)
assert '' == results['country']
assert '' == results['area']
assert '911' == results['line']
assert '911' == results['pretty']
assert '911' == results['full']
assert utils.is_phone_no('1234') is False
results = utils.is_phone_no('1234', min_len=4)
assert isinstance(results, dict)
assert '' == results['country']
assert '' == results['area']
assert '1234' == results['line']
assert '1234' == results['pretty']
assert '1234' == results['full']
assert utils.is_phone_no('12345') is False
results = utils.is_phone_no('12345', min_len=5)
assert isinstance(results, dict)
assert '' == results['country']
assert '' == results['area']
assert '12345' == results['line']
assert '12345' == results['pretty']
assert '12345' == results['full']
assert utils.is_phone_no('123456') is False
results = utils.is_phone_no('123456', min_len=6)
assert isinstance(results, dict)
assert '' == results['country']
assert '' == results['area']
assert '123456' == results['line']
assert '123456' == results['pretty']
assert '123456' == results['full']
# at 7 digits, the format hyphenates in the `pretty` section
assert utils.is_phone_no('1234567') is False
results = utils.is_phone_no('1234567', min_len=7)
assert isinstance(results, dict)
assert '' == results['country']
assert '' == results['area']
assert '1234567' == results['line']
assert '123-4567' == results['pretty']
assert '1234567' == results['full']
results = utils.is_phone_no('1(800) 123-4567')
assert isinstance(results, dict)
assert '1' == results['country']
assert '800' == results['area']
assert '1234567' == results['line']
assert '+1 800-123-4567' == results['pretty']
assert '18001234567' == results['full']
def test_parse_phone_no():
"""utils: parse_phone_no() testing """
# A simple single array entry (As str)
results = utils.parse_phone_no('')
assert isinstance(results, list)
assert len(results) == 0
# just delimeters
results = utils.parse_phone_no(', ,, , ,,, ')
assert isinstance(results, list)
assert len(results) == 0
results = utils.parse_phone_no(',')
assert isinstance(results, list)
assert len(results) == 0
results = utils.parse_phone_no(None)
assert isinstance(results, list)
assert len(results) == 0
results = utils.parse_phone_no(42)
assert isinstance(results, list)
assert len(results) == 0
results = utils.parse_phone_no('this is not a parseable phoneno at all')
assert isinstance(results, list)
assert len(results) == 8
# Now we do it again with the store_unparsable flag set to False
results = utils.parse_phone_no(
'this is not a parseable email at all', store_unparseable=False)
assert isinstance(results, list)
assert len(results) == 0
results = utils.parse_phone_no('+', store_unparseable=False)
assert isinstance(results, list)
assert len(results) == 0
results = utils.parse_phone_no('(', store_unparseable=False)
assert isinstance(results, list)
assert len(results) == 0
# Number is too short
results = utils.parse_phone_no('0', store_unparseable=False)
assert isinstance(results, list)
assert len(results) == 0
results = utils.parse_phone_no('12', store_unparseable=False)
assert isinstance(results, list)
assert len(results) == 0
# Now test valid phone numbers
results = utils.parse_phone_no('+1 (124) 245 2345')
assert isinstance(results, list)
assert len(results) == 1
assert '+1 (124) 245 2345' in results
results = utils.parse_phone_no('911', store_unparseable=False)
assert isinstance(results, list)
assert len(results) == 1
assert '911' in results
results = utils.parse_phone_no(
'911, 123-123-1234', store_unparseable=False)
assert isinstance(results, list)
assert len(results) == 2
assert '911' in results
assert '123-123-1234' in results
# Space variations
results = utils.parse_phone_no(' 911 , +1 (123) 123-1234')
assert isinstance(results, list)
assert len(results) == 2
assert '911' in results
assert '+1 (123) 123-1234' in results
results = utils.parse_phone_no(' 911 , + 1 ( 123 ) 123-1234')
assert isinstance(results, list)
assert len(results) == 2
assert '911' in results
assert '+ 1 ( 123 ) 123-1234' in results
def test_parse_emails():
"""utils: parse_emails() testing """
# A simple single array entry (As str)
@ -764,7 +965,7 @@ def test_parse_emails():
assert isinstance(results, list)
assert len(results) == 0
# Now test valid URLs
# Now test valid emails
results = utils.parse_emails('user@example.com')
assert isinstance(results, list)
assert len(results) == 1