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_url
from .utils import parse_bool from .utils import parse_bool
from .utils import parse_list from .utils import parse_list
from .utils import parse_phone_no
# Used to break a path list into parts # Used to break a path list into parts
PATHSPLIT_LIST_DELIM = re.compile(r'[ \t\r\n,\\/]+') PATHSPLIT_LIST_DELIM = re.compile(r'[ \t\r\n,\\/]+')
@ -560,6 +561,39 @@ class URLBase(object):
return content 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 @property
def app_id(self): def app_id(self):
return self.asset.app_id if self.asset.app_id else '' 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: # The API reference used to build this plugin was documented here:
# https://developers.clicksend.com/docs/rest/v3/ # https://developers.clicksend.com/docs/rest/v3/
# #
import re
import requests import requests
from json import dumps from json import dumps
from base64 import b64encode from base64 import b64encode
@ -44,7 +43,8 @@ from base64 import b64encode
from .NotifyBase import NotifyBase from .NotifyBase import NotifyBase
from ..URLBase import PrivacyMode from ..URLBase import PrivacyMode
from ..common import NotifyType 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 ..utils import parse_bool
from ..AppriseLocale import gettext_lazy as _ from ..AppriseLocale import gettext_lazy as _
@ -53,12 +53,6 @@ CLICKSEND_HTTP_ERROR_MAP = {
401: 'Unauthorized - Invalid Token.', 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): class NotifyClickSend(NotifyBase):
""" """
@ -151,13 +145,10 @@ class NotifyClickSend(NotifyBase):
self.logger.warning(msg) self.logger.warning(msg)
raise TypeError(msg) raise TypeError(msg)
for target in parse_list(targets): for target in parse_phone_no(targets):
# Validate targets and drop bad ones: # Validate targets and drop bad ones:
result = IS_PHONE_NO.match(target) result = is_phone_no(target)
if result: if not 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( self.logger.warning(
'Dropped invalid phone # ' 'Dropped invalid phone # '
'({}) specified.'.format(target), '({}) specified.'.format(target),
@ -165,12 +156,7 @@ class NotifyClickSend(NotifyBase):
continue continue
# store valid phone number # store valid phone number
self.targets.append(result) self.targets.append(result['full'])
continue
self.logger.warning(
'Dropped invalid phone # '
'({}) specified.'.format(target))
def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs): 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 # Support the 'to' variable so that we can support rooms this way too
# The 'to' makes it easier to use yaml configuration # The 'to' makes it easier to use yaml configuration
if 'to' in results['qsd'] and len(results['qsd']['to']): if 'to' in results['qsd'] and len(results['qsd']['to']):
results['targets'] += [x for x in filter( results['targets'] += \
bool, TARGET_LIST_DELIM.split( NotifyClickSend.parse_phone_no(results['qsd']['to'])
NotifyClickSend.unquote(results['qsd']['to'])))]
return results return results

View File

@ -30,7 +30,6 @@
# (both user and password) from the API Details section from within your # (both user and password) from the API Details section from within your
# account profile area: https://d7networks.com/accounts/profile/ # account profile area: https://d7networks.com/accounts/profile/
import re
import six import six
import requests import requests
import base64 import base64
@ -40,7 +39,8 @@ from json import loads
from .NotifyBase import NotifyBase from .NotifyBase import NotifyBase
from ..URLBase import PrivacyMode from ..URLBase import PrivacyMode
from ..common import NotifyType 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 ..utils import parse_bool
from ..AppriseLocale import gettext_lazy as _ from ..AppriseLocale import gettext_lazy as _
@ -52,9 +52,6 @@ D7NETWORKS_HTTP_ERROR_MAP = {
500: 'A Serverside Error Occured Handling the Request.', 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 # Priorities
class D7SMSPriority(object): class D7SMSPriority(object):
@ -197,18 +194,17 @@ class NotifyD7Networks(NotifyBase):
self.source = None \ self.source = None \
if not isinstance(source, six.string_types) else source.strip() if not isinstance(source, six.string_types) else source.strip()
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 # Parse our targets
self.targets = list() self.targets = list()
for target in parse_phone_no(targets):
for target in parse_list(targets):
# Validate targets and drop bad ones: # Validate targets and drop bad ones:
result = IS_PHONE_NO.match(target) result = result = is_phone_no(target)
if result: if not 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( self.logger.warning(
'Dropped invalid phone # ' 'Dropped invalid phone # '
'({}) specified.'.format(target), '({}) specified.'.format(target),
@ -216,16 +212,7 @@ class NotifyD7Networks(NotifyBase):
continue continue
# store valid phone number # store valid phone number
self.targets.append(result) self.targets.append(result['full'])
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)
return return
@ -235,6 +222,11 @@ class NotifyD7Networks(NotifyBase):
redirects to the appropriate handling 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) # error tracking (used for function return)
has_error = False has_error = False
@ -479,6 +471,6 @@ class NotifyD7Networks(NotifyBase):
# The 'to' makes it easier to use yaml configuration # The 'to' makes it easier to use yaml configuration
if 'to' in results['qsd'] and len(results['qsd']['to']): if 'to' in results['qsd'] and len(results['qsd']['to']):
results['targets'] += \ results['targets'] += \
NotifyD7Networks.parse_list(results['qsd']['to']) NotifyD7Networks.parse_phone_no(results['qsd']['to'])
return results return results

View File

@ -32,13 +32,13 @@
# This provider does not accept +1 (for example) as a country code. You need # This provider does not accept +1 (for example) as a country code. You need
# to specify 001 instead. # to specify 001 instead.
# #
import re
import requests import requests
from json import loads from json import loads
from .NotifyBase import NotifyBase from .NotifyBase import NotifyBase
from ..common import NotifyType 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 ..utils import validate_regex
from ..AppriseLocale import gettext_lazy as _ 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', 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): class NotifyKavenegar(NotifyBase):
""" """
@ -165,36 +162,23 @@ class NotifyKavenegar(NotifyBase):
self.source = None self.source = None
if source is not None: if source is not None:
result = IS_PHONE_NO.match(source) result = is_phone_no(source)
if not result: if not result:
msg = 'The Kavenegar source specified ({}) is invalid.'\ msg = 'The Kavenegar source specified ({}) is invalid.'\
.format(source) .format(source)
self.logger.warning(msg) self.logger.warning(msg)
raise TypeError(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 # Store our source
self.source = result self.source = result['full']
# Parse our targets # Parse our targets
self.targets = list() self.targets = list()
for target in parse_list(targets): for target in parse_phone_no(targets):
# Validate targets and drop bad ones: # Validate targets and drop bad ones:
result = IS_PHONE_NO.match(target) result = is_phone_no(target)
if result: if not 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( self.logger.warning(
'Dropped invalid phone # ' 'Dropped invalid phone # '
'({}) specified.'.format(target), '({}) specified.'.format(target),
@ -202,16 +186,7 @@ class NotifyKavenegar(NotifyBase):
continue continue
# store valid phone number # store valid phone number
self.targets.append(result) self.targets.append(result['full'])
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)
return return
@ -220,6 +195,11 @@ class NotifyKavenegar(NotifyBase):
Sends SMS Message 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) # error tracking (used for function return)
has_error = False has_error = False
@ -364,7 +344,7 @@ class NotifyKavenegar(NotifyBase):
# The 'to' makes it easier to use yaml configuration # The 'to' makes it easier to use yaml configuration
if 'to' in results['qsd'] and len(results['qsd']['to']): if 'to' in results['qsd'] and len(results['qsd']['to']):
results['targets'] += \ 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']): if 'from' in results['qsd'] and len(results['qsd']['from']):
results['source'] = \ results['source'] = \

View File

@ -31,18 +31,15 @@
# Get details on the API used in this plugin here: # Get details on the API used in this plugin here:
# - https://world.msg91.com/apidoc/textsms/send-sms.php # - https://world.msg91.com/apidoc/textsms/send-sms.php
import re
import requests import requests
from .NotifyBase import NotifyBase from .NotifyBase import NotifyBase
from ..common import NotifyType 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 ..utils import validate_regex
from ..AppriseLocale import gettext_lazy as _ 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): class MSG91Route(object):
""" """
@ -207,13 +204,10 @@ class NotifyMSG91(NotifyBase):
# Parse our targets # Parse our targets
self.targets = list() self.targets = list()
for target in parse_list(targets): for target in parse_phone_no(targets):
# Validate targets and drop bad ones: # Validate targets and drop bad ones:
result = IS_PHONE_NO.match(target) result = is_phone_no(target)
if result: if not 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( self.logger.warning(
'Dropped invalid phone # ' 'Dropped invalid phone # '
'({}) specified.'.format(target), '({}) specified.'.format(target),
@ -221,19 +215,7 @@ class NotifyMSG91(NotifyBase):
continue continue
# store valid phone number # store valid phone number
self.targets.append(result) self.targets.append(result['full'])
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)
return return
@ -242,6 +224,11 @@ class NotifyMSG91(NotifyBase):
Perform MSG91 Notification 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 # Prepare our headers
headers = { headers = {
'User-Agent': self.app_id, 'User-Agent': self.app_id,
@ -365,6 +352,6 @@ class NotifyMSG91(NotifyBase):
# The 'to' makes it easier to use yaml configuration # The 'to' makes it easier to use yaml configuration
if 'to' in results['qsd'] and len(results['qsd']['to']): if 'to' in results['qsd'] and len(results['qsd']['to']):
results['targets'] += \ results['targets'] += \
NotifyMSG91.parse_list(results['qsd']['to']) NotifyMSG91.parse_phone_no(results['qsd']['to'])
return results return results

View File

@ -29,18 +29,15 @@
# - https://dashboard.messagebird.com/en/user/index # - https://dashboard.messagebird.com/en/user/index
# #
import re
import requests import requests
from .NotifyBase import NotifyBase from .NotifyBase import NotifyBase
from ..common import NotifyType 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 ..utils import validate_regex
from ..AppriseLocale import gettext_lazy as _ 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): class NotifyMessageBird(NotifyBase):
""" """
@ -129,28 +126,20 @@ class NotifyMessageBird(NotifyBase):
self.logger.warning(msg) self.logger.warning(msg)
raise TypeError(msg) raise TypeError(msg)
result = IS_PHONE_NO.match(source) result = is_phone_no(source)
if not result: if not result:
msg = 'The MessageBird source specified ({}) is invalid.'\ msg = 'The MessageBird source specified ({}) is invalid.'\
.format(source) .format(source)
self.logger.warning(msg) self.logger.warning(msg)
raise TypeError(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 # Store our source
self.source = result self.source = result['full']
# Parse our targets # Parse our targets
self.targets = list() self.targets = list()
targets = parse_list(targets) targets = parse_phone_no(targets)
if not targets: if not targets:
# No sources specified, use our own phone no # No sources specified, use our own phone no
self.targets.append(self.source) self.targets.append(self.source)
@ -159,11 +148,8 @@ class NotifyMessageBird(NotifyBase):
# otherwise, store all of our target numbers # otherwise, store all of our target numbers
for target in targets: for target in targets:
# Validate targets and drop bad ones: # Validate targets and drop bad ones:
result = IS_PHONE_NO.match(target) result = is_phone_no(target)
if result: if not 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( self.logger.warning(
'Dropped invalid phone # ' 'Dropped invalid phone # '
'({}) specified.'.format(target), '({}) specified.'.format(target),
@ -171,19 +157,7 @@ class NotifyMessageBird(NotifyBase):
continue continue
# store valid phone number # store valid phone number
self.targets.append(result) self.targets.append(result['full'])
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)
return return
@ -192,6 +166,11 @@ class NotifyMessageBird(NotifyBase):
Perform MessageBird Notification 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) # error tracking (used for function return)
has_error = False has_error = False
@ -345,6 +324,7 @@ class NotifyMessageBird(NotifyBase):
try: try:
# The first path entry is the source/originator # The first path entry is the source/originator
results['source'] = results['targets'].pop(0) results['source'] = results['targets'].pop(0)
except IndexError: except IndexError:
# No path specified... this URL is potentially un-parseable; we can # No path specified... this URL is potentially un-parseable; we can
# hope for a from= entry # hope for a from= entry
@ -357,7 +337,7 @@ class NotifyMessageBird(NotifyBase):
# The 'to' makes it easier to use yaml configuration # The 'to' makes it easier to use yaml configuration
if 'to' in results['qsd'] and len(results['qsd']['to']): if 'to' in results['qsd'] and len(results['qsd']['to']):
results['targets'] += \ 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']): if 'from' in results['qsd'] and len(results['qsd']['from']):
results['source'] = \ results['source'] = \

View File

@ -28,20 +28,16 @@
# Get your (api) key and secret here: # Get your (api) key and secret here:
# - https://dashboard.nexmo.com/getting-started-guide # - https://dashboard.nexmo.com/getting-started-guide
# #
import re
import requests import requests
from .NotifyBase import NotifyBase from .NotifyBase import NotifyBase
from ..URLBase import PrivacyMode from ..URLBase import PrivacyMode
from ..common import NotifyType 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 ..utils import validate_regex
from ..AppriseLocale import gettext_lazy as _ 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): class NotifyNexmo(NotifyBase):
""" """
@ -185,30 +181,23 @@ class NotifyNexmo(NotifyBase):
# The Source Phone # # The Source Phone #
self.source = source 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 ' \ msg = 'The Account (From) Phone # specified ' \
'({}) is invalid.'.format(source) '({}) is invalid.'.format(source)
self.logger.warning(msg) self.logger.warning(msg)
raise TypeError(msg) raise TypeError(msg)
# Tidy source # Store our parsed value
self.source = re.sub(r'[^\d]+', '', self.source) self.source = result['full']
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)
# Parse our targets # Parse our targets
self.targets = list() self.targets = list()
for target in parse_list(targets): for target in parse_phone_no(targets):
# Validate targets and drop bad ones: # Validate targets and drop bad ones:
result = IS_PHONE_NO.match(target) result = is_phone_no(target)
if result: if not 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( self.logger.warning(
'Dropped invalid phone # ' 'Dropped invalid phone # '
'({}) specified.'.format(target), '({}) specified.'.format(target),
@ -216,13 +205,7 @@ class NotifyNexmo(NotifyBase):
continue continue
# store valid phone number # store valid phone number
self.targets.append(result) self.targets.append(result['full'])
continue
self.logger.warning(
'Dropped invalid phone # '
'({}) specified.'.format(target),
)
return return
@ -393,10 +376,10 @@ class NotifyNexmo(NotifyBase):
results['ttl'] = \ results['ttl'] = \
NotifyNexmo.unquote(results['qsd']['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 # The 'to' makes it easier to use yaml configuration
if 'to' in results['qsd'] and len(results['qsd']['to']): if 'to' in results['qsd'] and len(results['qsd']['to']):
results['targets'] += \ results['targets'] += \
NotifyNexmo.parse_list(results['qsd']['to']) NotifyNexmo.parse_phone_no(results['qsd']['to'])
return results return results

View File

@ -23,20 +23,17 @@
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE. # THE SOFTWARE.
import re
import requests import requests
from .NotifyBase import NotifyBase from .NotifyBase import NotifyBase
from ..common import NotifyType from ..common import NotifyType
from ..utils import is_email from ..utils import is_email
from ..utils import is_phone_no
from ..utils import parse_list from ..utils import parse_list
from ..utils import parse_bool from ..utils import parse_bool
from ..utils import validate_regex from ..utils import validate_regex
from ..AppriseLocale import gettext_lazy as _ 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): class NotifyPopcornNotify(NotifyBase):
""" """
@ -127,19 +124,10 @@ class NotifyPopcornNotify(NotifyBase):
for target in parse_list(targets): for target in parse_list(targets):
# Validate targets and drop bad ones: # Validate targets and drop bad ones:
result = IS_PHONE_NO.match(target) result = is_phone_no(target)
if result: 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 # store valid phone number
self.targets.append(result) self.targets.append(result['full'])
continue continue
result = is_email(target) result = is_email(target)

View File

@ -35,13 +35,11 @@ from itertools import chain
from .NotifyBase import NotifyBase from .NotifyBase import NotifyBase
from ..URLBase import PrivacyMode from ..URLBase import PrivacyMode
from ..common import NotifyType from ..common import NotifyType
from ..utils import is_phone_no
from ..utils import parse_list from ..utils import parse_list
from ..utils import validate_regex from ..utils import validate_regex
from ..AppriseLocale import gettext_lazy as _ from ..AppriseLocale import gettext_lazy as _
# Some Phone Number Detection
IS_PHONE_NO = re.compile(r'^\+?(?P<phone>[0-9\s)(+-]+)\s*$')
# Topic Detection # Topic Detection
# Summary: 256 Characters max, only alpha/numeric plus underscore (_) and # Summary: 256 Characters max, only alpha/numeric plus underscore (_) and
# dash (-) additionally allowed. # dash (-) additionally allowed.
@ -198,24 +196,10 @@ class NotifySNS(NotifyBase):
self.aws_auth_algorithm = 'AWS4-HMAC-SHA256' self.aws_auth_algorithm = 'AWS4-HMAC-SHA256'
self.aws_auth_request = 'aws4_request' self.aws_auth_request = 'aws4_request'
# Get our targets
targets = parse_list(targets)
# Validate targets and drop bad ones: # Validate targets and drop bad ones:
for target in targets: for target in parse_list(targets):
result = IS_PHONE_NO.match(target) result = is_phone_no(target)
if result: 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 # store valid phone number
self.phone.append('+{}'.format(result)) self.phone.append('+{}'.format(result))
continue continue
@ -231,12 +215,6 @@ class NotifySNS(NotifyBase):
'(%s) specified.' % target, '(%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 return
def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs): 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 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 # Initiaize our error tracking
error_count = 0 error_count = 0

View File

@ -33,7 +33,6 @@
# from). Activated phone numbers can be found on your dashboard here: # from). Activated phone numbers can be found on your dashboard here:
# - https://dashboard.sinch.com/numbers/your-numbers/numbers # - https://dashboard.sinch.com/numbers/your-numbers/numbers
# #
import re
import six import six
import requests import requests
import json import json
@ -41,15 +40,12 @@ import json
from .NotifyBase import NotifyBase from .NotifyBase import NotifyBase
from ..URLBase import PrivacyMode from ..URLBase import PrivacyMode
from ..common import NotifyType 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 ..utils import validate_regex
from ..AppriseLocale import gettext_lazy as _ 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): class SinchRegion(object):
""" """
Defines the Sinch Server Regions Defines the Sinch Server Regions
@ -194,15 +190,6 @@ class NotifySinch(NotifyBase):
self.logger.warning(msg) self.logger.warning(msg)
raise TypeError(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 # Setup our region
self.region = self.template_args['region']['default'] \ self.region = self.template_args['region']['default'] \
if not isinstance(region, six.string_types) else region.lower() if not isinstance(region, six.string_types) else region.lower()
@ -211,8 +198,16 @@ class NotifySinch(NotifyBase):
self.logger.warning(msg) self.logger.warning(msg)
raise TypeError(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 # Tidy source
self.source = re.sub(r'[^\d]+', '', self.source) self.source = result['full']
if len(self.source) < 11 or len(self.source) > 14: if len(self.source) < 11 or len(self.source) > 14:
# A short code is a special 5 or 6 digit telephone number # A short code is a special 5 or 6 digit telephone number
@ -233,15 +228,10 @@ class NotifySinch(NotifyBase):
# Parse our targets # Parse our targets
self.targets = list() self.targets = list()
for target in parse_list(targets): for target in parse_phone_no(targets):
# Validate targets and drop bad ones: # Parse each phone number we found
result = IS_PHONE_NO.match(target) result = is_phone_no(target)
if result: if not 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( self.logger.warning(
'Dropped invalid phone # ' 'Dropped invalid phone # '
'({}) specified.'.format(target), '({}) specified.'.format(target),
@ -249,21 +239,7 @@ class NotifySinch(NotifyBase):
continue continue
# store valid phone number # store valid phone number
self.targets.append('+{}'.format(result)) self.targets.append('+{}'.format(result['full']))
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)
return return
@ -272,6 +248,14 @@ class NotifySinch(NotifyBase):
Perform Sinch Notification 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) # error tracking (used for function return)
has_error = False has_error = False
@ -459,6 +443,7 @@ class NotifySinch(NotifyBase):
if 'from' in results['qsd'] and len(results['qsd']['from']): if 'from' in results['qsd'] and len(results['qsd']['from']):
results['source'] = \ results['source'] = \
NotifySinch.unquote(results['qsd']['from']) NotifySinch.unquote(results['qsd']['from'])
if 'source' in results['qsd'] and len(results['qsd']['source']): if 'source' in results['qsd'] and len(results['qsd']['source']):
results['source'] = \ results['source'] = \
NotifySinch.unquote(results['qsd']['source']) NotifySinch.unquote(results['qsd']['source'])
@ -472,6 +457,6 @@ class NotifySinch(NotifyBase):
# The 'to' makes it easier to use yaml configuration # The 'to' makes it easier to use yaml configuration
if 'to' in results['qsd'] and len(results['qsd']['to']): if 'to' in results['qsd'] and len(results['qsd']['to']):
results['targets'] += \ results['targets'] += \
NotifySinch.parse_list(results['qsd']['to']) NotifySinch.parse_phone_no(results['qsd']['to'])
return results return results

View File

@ -40,22 +40,18 @@
# or consider purchasing a short-code from here: # or consider purchasing a short-code from here:
# https://www.twilio.com/docs/glossary/what-is-a-short-code # https://www.twilio.com/docs/glossary/what-is-a-short-code
# #
import re
import requests import requests
from json import loads from json import loads
from .NotifyBase import NotifyBase from .NotifyBase import NotifyBase
from ..URLBase import PrivacyMode from ..URLBase import PrivacyMode
from ..common import NotifyType 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 ..utils import validate_regex
from ..AppriseLocale import gettext_lazy as _ 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): class NotifyTwilio(NotifyBase):
""" """
A wrapper for Twilio Notifications A wrapper for Twilio Notifications
@ -181,17 +177,15 @@ class NotifyTwilio(NotifyBase):
self.logger.warning(msg) self.logger.warning(msg)
raise TypeError(msg) raise TypeError(msg)
# The Source Phone # and/or short-code result = is_phone_no(source, min_len=5)
self.source = source if not result:
if not IS_PHONE_NO.match(self.source):
msg = 'The Account (From) Phone # or Short-code specified ' \ msg = 'The Account (From) Phone # or Short-code specified ' \
'({}) is invalid.'.format(source) '({}) is invalid.'.format(source)
self.logger.warning(msg) self.logger.warning(msg)
raise TypeError(msg) raise TypeError(msg)
# Tidy source # Store The Source Phone # and/or short-code
self.source = re.sub(r'[^\d]+', '', self.source) self.source = result['full']
if len(self.source) < 11 or len(self.source) > 14: if len(self.source) < 11 or len(self.source) > 14:
# https://www.twilio.com/docs/glossary/what-is-a-short-code # https://www.twilio.com/docs/glossary/what-is-a-short-code
@ -213,15 +207,10 @@ class NotifyTwilio(NotifyBase):
# Parse our targets # Parse our targets
self.targets = list() self.targets = list()
for target in parse_list(targets): for target in parse_phone_no(targets):
# Validate targets and drop bad ones: # Validate targets and drop bad ones:
result = IS_PHONE_NO.match(target) result = is_phone_no(target)
if result: if not 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( self.logger.warning(
'Dropped invalid phone # ' 'Dropped invalid phone # '
'({}) specified.'.format(target), '({}) specified.'.format(target),
@ -230,20 +219,6 @@ class NotifyTwilio(NotifyBase):
# store valid phone number # store valid phone number
self.targets.append('+{}'.format(result)) self.targets.append('+{}'.format(result))
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)
return return
@ -252,6 +227,14 @@ class NotifyTwilio(NotifyBase):
Perform Twilio Notification 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) # error tracking (used for function return)
has_error = False has_error = False
@ -431,6 +414,6 @@ class NotifyTwilio(NotifyBase):
# The 'to' makes it easier to use yaml configuration # The 'to' makes it easier to use yaml configuration
if 'to' in results['qsd'] and len(results['qsd']['to']): if 'to' in results['qsd'] and len(results['qsd']['to']):
results['targets'] += \ results['targets'] += \
NotifyTwilio.parse_list(results['qsd']['to']) NotifyTwilio.parse_phone_no(results['qsd']['to'])
return results 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 # - user@example.com
# - label+user@example.com # - label+user@example.com
GET_EMAIL_RE = re.compile( GET_EMAIL_RE = re.compile(
r'((?P<name>[^:<]+)?[:<\s]+)?' r'(([\s"\']+)?(?P<name>[^:<"\']+)?[:<\s"\']+)?'
r'(?P<full_email>((?P<label>[^+]+)\+)?' r'(?P<full_email>((?P<label>[^+]+)\+)?'
r'(?P<email>(?P<userid>[a-z0-9$%=_~-]+' r'(?P<email>(?P<userid>[a-z0-9$%=_~-]+'
r'(?:\.[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'[a-z0-9][a-z0-9_-]{5,})))'
r'\s*>?', re.IGNORECASE) r'\s*>?', re.IGNORECASE)
# Regular expression used to extract a phone number # A simple verification check to make sure the content specified
GET_PHONE_NO_RE = re.compile(r'^\+?(?P<phone>[0-9\s)(+-]+)\s*$') # 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 # Regular expression used to destinguish between multiple URLs
URL_DETECTION_RE = re.compile( URL_DETECTION_RE = re.compile(
@ -273,6 +278,98 @@ def is_uuid(uuid):
return True if match else False 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): def is_email(address):
"""Determine if the specified entry is an email address """Determine if the specified entry is an email address
@ -633,9 +730,46 @@ def parse_bool(arg, default=False):
return bool(arg) 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): 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. returns a list.
""" """

View File

@ -207,6 +207,7 @@ def test_notify_base():
# Test invalid data # Test invalid data
assert NotifyBase.parse_list(None) == [] assert NotifyBase.parse_list(None) == []
assert NotifyBase.parse_list(object()) == []
assert NotifyBase.parse_list(42) == [] assert NotifyBase.parse_list(42) == []
result = NotifyBase.parse_list( result = NotifyBase.parse_list(
@ -234,6 +235,26 @@ def test_notify_base():
assert '//' in result assert '//' in result
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 # Give nothing, get nothing
assert NotifyBase.escape_html("") == "" assert NotifyBase.escape_html("") == ""
assert NotifyBase.escape_html(None) == "" assert NotifyBase.escape_html(None) == ""

View File

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

View File

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

View File

@ -694,6 +694,15 @@ def test_is_email():
assert 'spichai' == results['user'] assert 'spichai' == results['user']
assert 'ceo' == results['label'] 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 # An email without name, but contains delimiters
results = utils.is_email(' <spichai@gmail.com>') results = utils.is_email(' <spichai@gmail.com>')
assert '' == results['name'] assert '' == results['name']
@ -731,6 +740,198 @@ def test_is_email():
assert utils.is_email("Name <bademail>") is False 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(): def test_parse_emails():
"""utils: parse_emails() testing """ """utils: parse_emails() testing """
# A simple single array entry (As str) # A simple single array entry (As str)
@ -764,7 +965,7 @@ def test_parse_emails():
assert isinstance(results, list) assert isinstance(results, list)
assert len(results) == 0 assert len(results) == 0
# Now test valid URLs # Now test valid emails
results = utils.parse_emails('user@example.com') results = utils.parse_emails('user@example.com')
assert isinstance(results, list) assert isinstance(results, list)
assert len(results) == 1 assert len(results) == 1