Updated Apprise to 0.8.8

pull/1101/head
Louis Vézina 4 years ago
parent ae731bb78b
commit e6b8b1ad19

@ -33,7 +33,7 @@ from .common import NotifyFormat
from .common import MATCH_ALL_TAG
from .utils import is_exclusive_match
from .utils import parse_list
from .utils import split_urls
from .utils import parse_urls
from .logger import logger
from .AppriseAsset import AppriseAsset
@ -46,13 +46,19 @@ from .plugins.NotifyBase import NotifyBase
from . import plugins
from . import __version__
# Python v3+ support code made importable so it can remain backwards
# compatible with Python v2
from . import py3compat
ASYNCIO_SUPPORT = not six.PY2
class Apprise(object):
"""
Our Notification Manager
"""
def __init__(self, servers=None, asset=None):
def __init__(self, servers=None, asset=None, debug=False):
"""
Loads a set of server urls while applying the Asset() module to each
if specified.
@ -78,6 +84,9 @@ class Apprise(object):
# Initialize our locale object
self.locale = AppriseLocale()
# Set our debug flag
self.debug = debug
@staticmethod
def instantiate(url, asset=None, tag=None, suppress_exceptions=True):
"""
@ -111,14 +120,10 @@ class Apprise(object):
# Acquire our url tokens
results = plugins.url_to_dict(url)
if results is None:
# Failed to parse the server URL
logger.error('Unparseable URL {}.'.format(url))
# Failed to parse the server URL; detailed logging handled
# inside url_to_dict - nothing to report here.
return None
logger.trace('URL {} unpacked as:{}{}'.format(
url, os.linesep, os.linesep.join(
['{}="{}"'.format(k, v) for k, v in results.items()])))
elif isinstance(url, dict):
# We already have our result set
results = url
@ -154,11 +159,14 @@ class Apprise(object):
plugin = plugins.SCHEMA_MAP[results['schema']](**results)
# Create log entry of loaded URL
logger.debug('Loaded URL: {}'.format(plugin.url()))
logger.debug('Loaded {} URL: {}'.format(
plugins.SCHEMA_MAP[results['schema']].service_name,
plugin.url()))
except Exception:
# the arguments are invalid or can not be used.
logger.error('Could not load URL: %s' % url)
logger.error('Could not load {} URL: {}'.format(
plugins.SCHEMA_MAP[results['schema']].service_name, url))
return None
else:
@ -189,7 +197,7 @@ class Apprise(object):
if isinstance(servers, six.string_types):
# build our server list
servers = split_urls(servers)
servers = parse_urls(servers)
if len(servers) == 0:
return False
@ -226,7 +234,7 @@ class Apprise(object):
# returns None if it fails
instance = Apprise.instantiate(_server, asset=asset, tag=tag)
if not isinstance(instance, NotifyBase):
# No logging is requird as instantiate() handles failure
# No logging is required as instantiate() handles failure
# and/or success reasons for us
return_status = False
continue
@ -327,6 +335,10 @@ class Apprise(object):
body_format = self.asset.body_format \
if body_format is None else body_format
# for asyncio support; we track a list of our servers to notify
# sequentially
coroutines = []
# Iterate over our loaded plugins
for server in self.find(tag):
if status is None:
@ -384,6 +396,18 @@ class Apprise(object):
# Store entry directly
conversion_map[server.notify_format] = body
if ASYNCIO_SUPPORT and server.asset.async_mode:
# Build a list of servers requiring notification
# that will be triggered asynchronously afterwards
coroutines.append(server.async_notify(
body=conversion_map[server.notify_format],
title=title,
notify_type=notify_type,
attach=attach))
# We gather at this point and notify at the end
continue
try:
# Send notification
if not server.notify(
@ -405,6 +429,12 @@ class Apprise(object):
logger.exception("Notification Exception")
status = False
if coroutines:
# perform our async notification(s)
if not py3compat.asyncio.notify(coroutines, debug=self.debug):
# Toggle our status only if we had a failure
status = False
return status
def details(self, lang=None):

@ -99,6 +99,12 @@ class AppriseAsset(object):
# will be the default.
body_format = None
# Always attempt to send notifications asynchronous (as the same time
# if possible)
# This is a Python 3 supported option only. If set to False, then
# notifications are sent sequentially (one after another)
async_mode = True
def __init__(self, **kwargs):
"""
Asset Initialization

@ -27,6 +27,7 @@ import six
from . import config
from . import ConfigBase
from . import CONFIG_FORMATS
from . import URLBase
from .AppriseAsset import AppriseAsset
@ -46,7 +47,8 @@ class AppriseConfig(object):
"""
def __init__(self, paths=None, asset=None, cache=True, **kwargs):
def __init__(self, paths=None, asset=None, cache=True, recursion=0,
insecure_includes=False, **kwargs):
"""
Loads all of the paths specified (if any).
@ -69,6 +71,29 @@ class AppriseConfig(object):
It's also worth nothing that the cache value is only set to elements
that are not already of subclass ConfigBase()
recursion defines how deep we recursively handle entries that use the
`import` keyword. This keyword requires us to fetch more configuration
from another source and add it to our existing compilation. If the
file we remotely retrieve also has an `import` reference, we will only
advance through it if recursion is set to 2 deep. If set to zero
it is off. There is no limit to how high you set this value. It would
be recommended to keep it low if you do intend to use it.
insecure includes by default are disabled. When set to True, all
Apprise Config files marked to be in STRICT mode are treated as being
in ALWAYS mode.
Take a file:// based configuration for example, only a file:// based
configuration can import another file:// based one. because it is set
to STRICT mode. If an http:// based configuration file attempted to
import a file:// one it woul fail. However this import would be
possible if insecure_includes is set to True.
There are cases where a self hosting apprise developer may wish to load
configuration from memory (in a string format) that contains import
entries (even file:// based ones). In these circumstances if you want
these includes to be honored, this value must be set to True.
"""
# Initialize a server list of URLs
@ -81,13 +106,20 @@ class AppriseConfig(object):
# Set our cache flag
self.cache = cache
# Initialize our recursion value
self.recursion = recursion
# Initialize our insecure_includes flag
self.insecure_includes = insecure_includes
if paths is not None:
# Store our path(s)
self.add(paths)
return
def add(self, configs, asset=None, tag=None, cache=True):
def add(self, configs, asset=None, tag=None, cache=True, recursion=None,
insecure_includes=None):
"""
Adds one or more config URLs into our list.
@ -107,6 +139,12 @@ class AppriseConfig(object):
It's also worth nothing that the cache value is only set to elements
that are not already of subclass ConfigBase()
Optionally override the default recursion value.
Optionally override the insecure_includes flag.
if insecure_includes is set to True then all plugins that are
set to a STRICT mode will be a treated as ALWAYS.
"""
# Initialize our return status
@ -115,6 +153,14 @@ class AppriseConfig(object):
# Initialize our default cache value
cache = cache if cache is not None else self.cache
# Initialize our default recursion value
recursion = recursion if recursion is not None else self.recursion
# Initialize our default insecure_includes value
insecure_includes = \
insecure_includes if insecure_includes is not None \
else self.insecure_includes
if asset is None:
# prepare default asset
asset = self.asset
@ -154,7 +200,8 @@ class AppriseConfig(object):
# Instantiate ourselves an object, this function throws or
# returns None if it fails
instance = AppriseConfig.instantiate(
_config, asset=asset, tag=tag, cache=cache)
_config, asset=asset, tag=tag, cache=cache,
recursion=recursion, insecure_includes=insecure_includes)
if not isinstance(instance, ConfigBase):
return_status = False
continue
@ -165,7 +212,8 @@ class AppriseConfig(object):
# Return our status
return return_status
def add_config(self, content, asset=None, tag=None, format=None):
def add_config(self, content, asset=None, tag=None, format=None,
recursion=None, insecure_includes=None):
"""
Adds one configuration file in it's raw format. Content gets loaded as
a memory based object and only exists for the life of this
@ -174,8 +222,22 @@ class AppriseConfig(object):
If you know the format ('yaml' or 'text') you can specify
it for slightly less overhead during this call. Otherwise the
configuration is auto-detected.
Optionally override the default recursion value.
Optionally override the insecure_includes flag.
if insecure_includes is set to True then all plugins that are
set to a STRICT mode will be a treated as ALWAYS.
"""
# Initialize our default recursion value
recursion = recursion if recursion is not None else self.recursion
# Initialize our default insecure_includes value
insecure_includes = \
insecure_includes if insecure_includes is not None \
else self.insecure_includes
if asset is None:
# prepare default asset
asset = self.asset
@ -190,7 +252,13 @@ class AppriseConfig(object):
# Create ourselves a ConfigMemory Object to store our configuration
instance = config.ConfigMemory(
content=content, format=format, asset=asset, tag=tag)
content=content, format=format, asset=asset, tag=tag,
recursion=recursion, insecure_includes=insecure_includes)
if instance.config_format not in CONFIG_FORMATS:
logger.warning(
"The format of the configuration could not be deteced.")
return False
# Add our initialized plugin to our server listings
self.configs.append(instance)
@ -235,6 +303,7 @@ class AppriseConfig(object):
@staticmethod
def instantiate(url, asset=None, tag=None, cache=None,
recursion=0, insecure_includes=False,
suppress_exceptions=True):
"""
Returns the instance of a instantiated configuration plugin based on
@ -279,6 +348,12 @@ class AppriseConfig(object):
# Force an over-ride of the cache value to what we have specified
results['cache'] = cache
# Recursion can never be parsed from the URL
results['recursion'] = recursion
# Insecure includes flag can never be parsed from the URL
results['insecure_includes'] = insecure_includes
if suppress_exceptions:
try:
# Attempt to create an instance of our plugin using the parsed

@ -42,6 +42,7 @@ except ImportError:
from urllib.parse import quote as _quote
from urllib.parse import urlencode as _urlencode
from .AppriseLocale import gettext_lazy as _
from .AppriseAsset import AppriseAsset
from .utils import parse_url
from .utils import parse_bool
@ -98,6 +99,16 @@ class URLBase(object):
# Throttle
request_rate_per_sec = 0
# The connect timeout is the number of seconds Requests will wait for your
# client to establish a connection to a remote machine (corresponding to
# the connect()) call on the socket.
socket_connect_timeout = 4.0
# The read timeout is the number of seconds the client will wait for the
# server to send a response.
socket_read_timeout = 4.0
# Handle
# Maintain a set of tags to associate with this specific notification
tags = set()
@ -107,6 +118,78 @@ class URLBase(object):
# Logging
logger = logging.getLogger(__name__)
# Define a default set of template arguments used for dynamically building
# details about our individual plugins for developers.
# Define object templates
templates = ()
# Provides a mapping of tokens, certain entries are fixed and automatically
# configured if found (such as schema, host, user, pass, and port)
template_tokens = {}
# Here is where we define all of the arguments we accept on the url
# such as: schema://whatever/?cto=5.0&rto=15
# These act the same way as tokens except they are optional and/or
# have default values set if mandatory. This rule must be followed
template_args = {
'verify': {
'name': _('Verify SSL'),
# SSL Certificate Authority Verification
'type': 'bool',
# Provide a default
'default': verify_certificate,
# look up default using the following parent class value at
# runtime.
'_lookup_default': 'verify_certificate',
},
'rto': {
'name': _('Socket Read Timeout'),
'type': 'float',
# Provide a default
'default': socket_read_timeout,
# look up default using the following parent class value at
# runtime. The variable name identified here (in this case
# socket_read_timeout) is checked and it's result is placed
# over-top of the 'default'. This is done because once a parent
# class inherits this one, the overflow_mode already set as a
# default 'could' be potentially over-ridden and changed to a
# different value.
'_lookup_default': 'socket_read_timeout',
},
'cto': {
'name': _('Socket Connect Timeout'),
'type': 'float',
# Provide a default
'default': socket_connect_timeout,
# look up default using the following parent class value at
# runtime. The variable name identified here (in this case
# socket_connect_timeout) is checked and it's result is placed
# over-top of the 'default'. This is done because once a parent
# class inherits this one, the overflow_mode already set as a
# default 'could' be potentially over-ridden and changed to a
# different value.
'_lookup_default': 'socket_connect_timeout',
},
}
# kwargs are dynamically built because a prefix causes us to parse the
# content slightly differently. The prefix is required and can be either
# a (+ or -). Below would handle the +key=value:
# {
# 'headers': {
# 'name': _('HTTP Header'),
# 'prefix': '+',
# 'type': 'string',
# },
# },
#
# In a kwarg situation, the 'key' is always presumed to be treated as
# a string. When the 'type' is defined, it is being defined to respect
# the 'value'.
template_kwargs = {}
def __init__(self, asset=None, **kwargs):
"""
Initialize some general logging and common server arguments that will
@ -131,6 +214,9 @@ class URLBase(object):
self.port = int(self.port)
except (TypeError, ValueError):
self.logger.warning(
'Invalid port number specified {}'
.format(self.port))
self.port = None
self.user = kwargs.get('user')
@ -143,6 +229,26 @@ class URLBase(object):
# Always unquote the password if it exists
self.password = URLBase.unquote(self.password)
# Store our Timeout Variables
if 'socket_read_timeout' in kwargs:
try:
self.socket_read_timeout = \
float(kwargs.get('socket_read_timeout'))
except (TypeError, ValueError):
self.logger.warning(
'Invalid socket read timeout (rto) was specified {}'
.format(kwargs.get('socket_read_timeout')))
if 'socket_connect_timeout' in kwargs:
try:
self.socket_connect_timeout = \
float(kwargs.get('socket_connect_timeout'))
except (TypeError, ValueError):
self.logger.warning(
'Invalid socket connect timeout (cto) was specified {}'
.format(kwargs.get('socket_connect_timeout')))
if 'tag' in kwargs:
# We want to associate some tags with our notification service.
# the code below gets the 'tag' argument if defined, otherwise
@ -456,15 +562,41 @@ class URLBase(object):
@property
def app_id(self):
return self.asset.app_id
return self.asset.app_id if self.asset.app_id else ''
@property
def app_desc(self):
return self.asset.app_desc
return self.asset.app_desc if self.asset.app_desc else ''
@property
def app_url(self):
return self.asset.app_url
return self.asset.app_url if self.asset.app_url else ''
@property
def request_timeout(self):
"""This is primarily used to fullfill the `timeout` keyword argument
that is used by requests.get() and requests.put() calls.
"""
return (self.socket_connect_timeout, self.socket_read_timeout)
def url_parameters(self, *args, **kwargs):
"""
Provides a default set of args to work with. This can greatly
simplify URL construction in the acommpanied url() function.
The following property returns a dictionary (of strings) containing
all of the parameters that can be set on a URL and managed through
this class.
"""
return {
# The socket read timeout
'rto': str(self.socket_read_timeout),
# The request/socket connect timeout
'cto': str(self.socket_connect_timeout),
# Certificate verification
'verify': 'yes' if self.verify_certificate else 'no',
}
@staticmethod
def parse_url(url, verify_host=True):
@ -511,6 +643,14 @@ class URLBase(object):
if 'user' in results['qsd']:
results['user'] = results['qsd']['user']
# Store our socket read timeout if specified
if 'rto' in results['qsd']:
results['socket_read_timeout'] = results['qsd']['rto']
# Store our socket connect timeout if specified
if 'cto' in results['qsd']:
results['socket_connect_timeout'] = results['qsd']['cto']
return results
@staticmethod
@ -534,3 +674,24 @@ class URLBase(object):
response = ''
return response
def schemas(self):
"""A simple function that returns a set of all schemas associated
with this object based on the object.protocol and
object.secure_protocol
"""
schemas = set([])
for key in ('protocol', 'secure_protocol'):
schema = getattr(self, key, None)
if isinstance(schema, six.string_types):
schemas.add(schema)
elif isinstance(schema, (set, list, tuple)):
# Support iterables list types
for s in schema:
if isinstance(s, six.string_types):
schemas.add(s)
return schemas

@ -24,7 +24,7 @@
# THE SOFTWARE.
__title__ = 'apprise'
__version__ = '0.8.5'
__version__ = '0.8.8'
__author__ = 'Chris Caron'
__license__ = 'MIT'
__copywrite__ = 'Copyright (C) 2020 Chris Caron <lead2gold@gmail.com>'
@ -41,6 +41,8 @@ from .common import OverflowMode
from .common import OVERFLOW_MODES
from .common import ConfigFormat
from .common import CONFIG_FORMATS
from .common import ConfigIncludeMode
from .common import CONFIG_INCLUDE_MODES
from .URLBase import URLBase
from .URLBase import PrivacyMode
@ -66,5 +68,7 @@ __all__ = [
# Reference
'NotifyType', 'NotifyImageSize', 'NotifyFormat', 'OverflowMode',
'NOTIFY_TYPES', 'NOTIFY_IMAGE_SIZES', 'NOTIFY_FORMATS', 'OVERFLOW_MODES',
'ConfigFormat', 'CONFIG_FORMATS', 'PrivacyMode',
'ConfigFormat', 'CONFIG_FORMATS',
'ConfigIncludeMode', 'CONFIG_INCLUDE_MODES',
'PrivacyMode',
]

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 157 KiB

@ -57,20 +57,20 @@ class AttachFile(AttachBase):
Returns the URL built dynamically based on specified arguments.
"""
# Define any arguments set
args = {}
# Define any URL parameters
params = {}
if self._mimetype:
# A mime-type was enforced
args['mime'] = self._mimetype
params['mime'] = self._mimetype
if self._name:
# A name was enforced
args['name'] = self._name
params['name'] = self._name
return 'file://{path}{args}'.format(
return 'file://{path}{params}'.format(
path=self.quote(self.dirty_path),
args='?{}'.format(self.urlencode(args)) if args else '',
params='?{}'.format(self.urlencode(params)) if params else '',
)
def download(self, **kwargs):

@ -47,10 +47,6 @@ class AttachHTTP(AttachBase):
# The default secure protocol
secure_protocol = 'https'
# The maximum number of seconds to wait for a connection to be established
# before out-right just giving up
connection_timeout_sec = 5.0
# The number of bytes in memory to read from the remote source at a time
chunk_size = 8192
@ -129,7 +125,7 @@ class AttachHTTP(AttachBase):
auth=auth,
params=self.qsd,
verify=self.verify_certificate,
timeout=self.connection_timeout_sec,
timeout=self.request_timeout,
stream=True) as r:
# Handle Errors
@ -215,7 +211,7 @@ class AttachHTTP(AttachBase):
except requests.RequestException as e:
self.logger.error(
'A Connection error occured retrieving HTTP '
'A Connection error occurred retrieving HTTP '
'configuration from %s.' % self.host)
self.logger.debug('Socket Exception: %s' % str(e))
@ -258,10 +254,8 @@ class AttachHTTP(AttachBase):
Returns the URL built dynamically based on specified arguments.
"""
# Define any arguments set
args = {
'verify': 'yes' if self.verify_certificate else 'no',
}
# Our URL parameters
params = self.url_parameters(privacy=privacy, *args, **kwargs)
# Prepare our cache value
if self.cache is not None:
@ -271,21 +265,21 @@ class AttachHTTP(AttachBase):
cache = int(self.cache)
# Set our cache value
args['cache'] = cache
params['cache'] = cache
if self._mimetype:
# A format was enforced
args['mime'] = self._mimetype
params['mime'] = self._mimetype
if self._name:
# A name was enforced
args['name'] = self._name
params['name'] = self._name
# Append our headers into our args
args.update({'+{}'.format(k): v for k, v in self.headers.items()})
# Append our headers into our parameters
params.update({'+{}'.format(k): v for k, v in self.headers.items()})
# Apply any remaining entries to our URL
args.update(self.qsd)
params.update(self.qsd)
# Determine Authentication
auth = ''
@ -302,21 +296,21 @@ class AttachHTTP(AttachBase):
default_port = 443 if self.secure else 80
return '{schema}://{auth}{hostname}{port}{fullpath}?{args}'.format(
return '{schema}://{auth}{hostname}{port}{fullpath}?{params}'.format(
schema=self.secure_protocol if self.secure else self.protocol,
auth=auth,
hostname=self.quote(self.host, safe=''),
port='' if self.port is None or self.port == default_port
else ':{}'.format(self.port),
fullpath=self.quote(self.fullpath, safe='/'),
args=self.urlencode(args),
params=self.urlencode(params),
)
@staticmethod
def parse_url(url):
"""
Parses the URL and returns enough arguments that can allow
us to substantiate this object.
us to re-instantiate this object.
"""
results = AttachBase.parse_url(url)

@ -32,11 +32,13 @@ from os.path import expanduser
from os.path import expandvars
from . import NotifyType
from . import NotifyFormat
from . import Apprise
from . import AppriseAsset
from . import AppriseConfig
from .utils import parse_list
from .common import NOTIFY_TYPES
from .common import NOTIFY_FORMATS
from .logger import logger
from . import __title__
@ -44,6 +46,10 @@ from . import __version__
from . import __license__
from . import __copywrite__
# By default we allow looking 1 level down recursivly in Apprise configuration
# files.
DEFAULT_RECURSION_DEPTH = 1
# Defines our click context settings adding -h to the additional options that
# can be specified to get the help menu to come up
CONTEXT_SETTINGS = dict(help_option_names=['-h', '--help'])
@ -101,12 +107,19 @@ def print_version_msg():
help='Specify one or more configuration locations.')
@click.option('--attach', '-a', default=None, type=str, multiple=True,
metavar='ATTACHMENT_URL',
help='Specify one or more configuration locations.')
help='Specify one or more attachment.')
@click.option('--notification-type', '-n', default=NotifyType.INFO, type=str,
metavar='TYPE',
help='Specify the message type (default=info). Possible values'
' are "{}", and "{}".'.format(
'", "'.join(NOTIFY_TYPES[:-1]), NOTIFY_TYPES[-1]))
help='Specify the message type (default={}). '
'Possible values are "{}", and "{}".'.format(
NotifyType.INFO, '", "'.join(NOTIFY_TYPES[:-1]),
NOTIFY_TYPES[-1]))
@click.option('--input-format', '-i', default=NotifyFormat.TEXT, type=str,
metavar='FORMAT',
help='Specify the message input format (default={}). '
'Possible values are "{}", and "{}".'.format(
NotifyFormat.TEXT, '", "'.join(NOTIFY_FORMATS[:-1]),
NOTIFY_FORMATS[-1]))
@click.option('--theme', '-T', default='default', type=str, metavar='THEME',
help='Specify the default theme.')
@click.option('--tag', '-g', default=None, type=str, multiple=True,
@ -114,19 +127,28 @@ def print_version_msg():
'which services to notify. Use multiple --tag (-g) entries to '
'"OR" the tags together and comma separated to "AND" them. '
'If no tags are specified then all services are notified.')
@click.option('--disable-async', '-Da', is_flag=True,
help='Send all notifications sequentially')
@click.option('--dry-run', '-d', is_flag=True,
help='Perform a trial run but only prints the notification '
'services to-be triggered to stdout. Notifications are never '
'sent using this mode.')
@click.option('--recursion-depth', '-R', default=DEFAULT_RECURSION_DEPTH,
type=int,
help='The number of recursive import entries that can be '
'loaded from within Apprise configuration. By default '
'this is set to {}.'.format(DEFAULT_RECURSION_DEPTH))
@click.option('--verbose', '-v', count=True,
help='Makes the operation more talkative. Use multiple v to '
'increase the verbosity. I.e.: -vvvv')
@click.option('--debug', '-D', is_flag=True, help='Debug mode')
@click.option('--version', '-V', is_flag=True,
help='Display the apprise version and exit.')
@click.argument('urls', nargs=-1,
metavar='SERVER_URL [SERVER_URL2 [SERVER_URL3]]',)
def main(body, title, config, attach, urls, notification_type, theme, tag,
dry_run, verbose, version):
input_format, dry_run, recursion_depth, verbose, disable_async,
debug, version):
"""
Send a notification to all of the specified servers identified by their
URLs the content provided within the title, body and notification-type.
@ -138,6 +160,11 @@ def main(body, title, config, attach, urls, notification_type, theme, tag,
# want to return a specific error code, you must call sys.exit()
# as you will see below.
debug = True if debug else False
if debug:
# Verbosity must be a minimum of 3
verbose = 3 if verbose < 3 else verbose
# Logging
ch = logging.StreamHandler(sys.stdout)
if verbose > 3:
@ -166,21 +193,55 @@ def main(body, title, config, attach, urls, notification_type, theme, tag,
ch.setFormatter(formatter)
logger.addHandler(ch)
# Update our asyncio logger
asyncio_logger = logging.getLogger('asyncio')
for handler in logger.handlers:
asyncio_logger.addHandler(handler)
asyncio_logger.setLevel(logger.level)
if version:
print_version_msg()
sys.exit(0)
# Simple Error Checking
notification_type = notification_type.strip().lower()
if notification_type not in NOTIFY_TYPES:
logger.error(
'The --notification-type (-n) value of {} is not supported.'
.format(notification_type))
# 2 is the same exit code returned by Click if there is a parameter
# issue. For consistency, we also return a 2
sys.exit(2)
input_format = input_format.strip().lower()
if input_format not in NOTIFY_FORMATS:
logger.error(
'The --input-format (-i) value of {} is not supported.'
.format(input_format))
# 2 is the same exit code returned by Click if there is a parameter
# issue. For consistency, we also return a 2
sys.exit(2)
# Prepare our asset
asset = AppriseAsset(theme=theme)
asset = AppriseAsset(
body_format=input_format,
theme=theme,
# Async mode is only used for Python v3+ and allows a user to send
# all of their notifications asyncronously. This was made an option
# incase there are problems in the future where it's better that
# everything run sequentially/syncronously instead.
async_mode=disable_async is not True,
)
# Create our object
a = Apprise(asset=asset)
# Create our Apprise object
a = Apprise(asset=asset, debug=debug)
# Load our configuration if no URLs or specified configuration was
# identified on the command line
a.add(AppriseConfig(
paths=[f for f in DEFAULT_SEARCH_PATHS if isfile(expanduser(f))]
if not (config or urls) else config), asset=asset)
if not (config or urls) else config,
asset=asset, recursion=recursion_depth))
# Load our inventory up
for url in urls:
@ -234,7 +295,10 @@ def main(body, title, config, attach, urls, notification_type, theme, tag,
# There were no notifications set. This is a result of just having
# empty configuration files and/or being to restrictive when filtering
# by specific tag(s)
sys.exit(2)
# Exit code 3 is used since Click uses exit code 2 if there is an
# error with the parameters specified
sys.exit(3)
elif result is False:
# At least 1 notification service failed to send

@ -31,15 +31,15 @@ class NotifyType(object):
"""
INFO = 'info'
SUCCESS = 'success'
FAILURE = 'failure'
WARNING = 'warning'
FAILURE = 'failure'
NOTIFY_TYPES = (
NotifyType.INFO,
NotifyType.SUCCESS,
NotifyType.FAILURE,
NotifyType.WARNING,
NotifyType.FAILURE,
)
@ -129,6 +129,31 @@ CONFIG_FORMATS = (
ConfigFormat.YAML,
)
class ConfigIncludeMode(object):
"""
The different Cofiguration inclusion modes. All Configuration
plugins will have one of these associated with it.
"""
# - Configuration inclusion of same type only; hence a file:// can include
# a file://
# - Cross file inclusion is not allowed unless insecure_includes (a flag)
# is set to True. In these cases STRICT acts as type ALWAYS
STRICT = 'strict'
# This configuration type can never be included
NEVER = 'never'
# File configuration can always be included
ALWAYS = 'always'
CONFIG_INCLUDE_MODES = (
ConfigIncludeMode.STRICT,
ConfigIncludeMode.NEVER,
ConfigIncludeMode.ALWAYS,
)
# This is a reserved tag that is automatically assigned to every
# Notification Plugin
MATCH_ALL_TAG = 'all'

@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2019 Chris Caron <lead2gold@gmail.com>
# Copyright (C) 2020 Chris Caron <lead2gold@gmail.com>
# All rights reserved.
#
# This code is licensed under the MIT License.
@ -34,9 +34,12 @@ from ..AppriseAsset import AppriseAsset
from ..URLBase import URLBase
from ..common import ConfigFormat
from ..common import CONFIG_FORMATS
from ..common import ConfigIncludeMode
from ..utils import GET_SCHEMA_RE
from ..utils import parse_list
from ..utils import parse_bool
from ..utils import parse_urls
from . import SCHEMA_MAP
class ConfigBase(URLBase):
@ -60,7 +63,15 @@ class ConfigBase(URLBase):
# anything else. 128KB (131072B)
max_buffer_size = 131072
def __init__(self, cache=True, **kwargs):
# By default all configuration is not includable using the 'include'
# line found in configuration files.
allow_cross_includes = ConfigIncludeMode.NEVER
# the config path manages the handling of relative include
config_path = os.getcwd()
def __init__(self, cache=True, recursion=0, insecure_includes=False,
**kwargs):
"""
Initialize some general logging and common server arguments that will
keep things consistent when working with the configurations that
@ -76,6 +87,29 @@ class ConfigBase(URLBase):
You can alternatively set the cache value to an int identifying the
number of seconds the previously retrieved can exist for before it
should be considered expired.
recursion defines how deep we recursively handle entries that use the
`include` keyword. This keyword requires us to fetch more configuration
from another source and add it to our existing compilation. If the
file we remotely retrieve also has an `include` reference, we will only
advance through it if recursion is set to 2 deep. If set to zero
it is off. There is no limit to how high you set this value. It would
be recommended to keep it low if you do intend to use it.
insecure_include by default are disabled. When set to True, all
Apprise Config files marked to be in STRICT mode are treated as being
in ALWAYS mode.
Take a file:// based configuration for example, only a file:// based
configuration can include another file:// based one. because it is set
to STRICT mode. If an http:// based configuration file attempted to
include a file:// one it woul fail. However this include would be
possible if insecure_includes is set to True.
There are cases where a self hosting apprise developer may wish to load
configuration from memory (in a string format) that contains 'include'
entries (even file:// based ones). In these circumstances if you want
these 'include' entries to be honored, this value must be set to True.
"""
super(ConfigBase, self).__init__(**kwargs)
@ -88,6 +122,12 @@ class ConfigBase(URLBase):
# Tracks previously loaded content for speed
self._cached_servers = None
# Initialize our recursion value
self.recursion = recursion
# Initialize our insecure_includes flag
self.insecure_includes = insecure_includes
if 'encoding' in kwargs:
# Store the encoding
self.encoding = kwargs.get('encoding')
@ -154,15 +194,110 @@ class ConfigBase(URLBase):
# Dynamically load our parse_ function based on our config format
fn = getattr(ConfigBase, 'config_parse_{}'.format(config_format))
# Execute our config parse function which always returns a list
self._cached_servers.extend(fn(content=content, asset=asset))
# Initialize our asset object
asset = asset if isinstance(asset, AppriseAsset) else self.asset
# Execute our config parse function which always returns a tuple
# of our servers and our configuration
servers, configs = fn(content=content, asset=asset)
self._cached_servers.extend(servers)
# Configuration files were detected; recursively populate them
# If we have been configured to do so
for url in configs:
if self.recursion > 0:
# Attempt to acquire the schema at the very least to allow
# our configuration based urls.
schema = GET_SCHEMA_RE.match(url)
if schema is None:
# Plan B is to assume we're dealing with a file
schema = 'file'
if not os.path.isabs(url):
# We're dealing with a relative path; prepend
# our current config path
url = os.path.join(self.config_path, url)
url = '{}://{}'.format(schema, URLBase.quote(url))
else:
# Ensure our schema is always in lower case
schema = schema.group('schema').lower()
# Some basic validation
if schema not in SCHEMA_MAP:
ConfigBase.logger.warning(
'Unsupported include schema {}.'.format(schema))
continue
# Parse our url details of the server object as dictionary
# containing all of the information parsed from our URL
results = SCHEMA_MAP[schema].parse_url(url)
if not results:
# Failed to parse the server URL
self.logger.warning(
'Unparseable include URL {}'.format(url))
continue
# Handle cross inclusion based on allow_cross_includes rules
if (SCHEMA_MAP[schema].allow_cross_includes ==
ConfigIncludeMode.STRICT
and schema not in self.schemas()
and not self.insecure_includes) or \
SCHEMA_MAP[schema].allow_cross_includes == \
ConfigIncludeMode.NEVER:
# Prevent the loading if insecure base protocols
ConfigBase.logger.warning(
'Including {}:// based configuration is prohibited. '
'Ignoring URL {}'.format(schema, url))
continue
# Prepare our Asset Object
results['asset'] = asset
# No cache is required because we're just lumping this in
# and associating it with the cache value we've already
# declared (prior to our recursion)
results['cache'] = False
# Recursion can never be parsed from the URL; we decrement
# it one level
results['recursion'] = self.recursion - 1
# Insecure Includes flag can never be parsed from the URL
results['insecure_includes'] = self.insecure_includes
try:
# Attempt to create an instance of our plugin using the
# parsed URL information
cfg_plugin = SCHEMA_MAP[results['schema']](**results)
if len(self._cached_servers):
except Exception as e:
# the arguments are invalid or can not be used.
self.logger.warning(
'Could not load include URL: {}'.format(url))
self.logger.debug('Loading Exception: {}'.format(str(e)))
continue
# if we reach here, we can now add this servers found
# in this configuration file to our list
self._cached_servers.extend(
cfg_plugin.servers(asset=asset))
# We no longer need our configuration object
del cfg_plugin
else:
self.logger.debug(
'Recursion limit reached; ignoring Include URL: %s' % url)
if self._cached_servers:
self.logger.info('Loaded {} entries from {}'.format(
len(self._cached_servers), self.url()))
else:
self.logger.warning('Failed to load configuration from {}'.format(
self.url()))
self.logger.warning(
'Failed to load Apprise configuration from {}'.format(
self.url()))
# Set the time our content was cached at
self._cached_time = time.time()
@ -282,7 +417,8 @@ class ConfigBase(URLBase):
except TypeError:
# content was not expected string type
ConfigBase.logger.error('Invalid apprise config specified')
ConfigBase.logger.error(
'Invalid Apprise configuration specified.')
return None
# By default set our return value to None since we don't know
@ -297,7 +433,7 @@ class ConfigBase(URLBase):
if not result:
# Invalid syntax
ConfigBase.logger.error(
'Undetectable apprise configuration found '
'Undetectable Apprise configuration found '
'based on line {}.'.format(line))
# Take an early exit
return None
@ -338,14 +474,14 @@ class ConfigBase(URLBase):
if not config_format:
# We couldn't detect configuration
ConfigBase.logger.error('Could not detect configuration')
return list()
return (list(), list())
if config_format not in CONFIG_FORMATS:
# Invalid configuration type specified
ConfigBase.logger.error(
'An invalid configuration format ({}) was specified'.format(
config_format))
return list()
return (list(), list())
# Dynamically load our parse_ function based on our config format
fn = getattr(ConfigBase, 'config_parse_{}'.format(config_format))
@ -357,9 +493,14 @@ class ConfigBase(URLBase):
def config_parse_text(content, asset=None):
"""
Parse the specified content as though it were a simple text file only
containing a list of URLs. Return a list of loaded notification plugins
containing a list of URLs.
Optionally associate an asset with the notification.
Return a tuple that looks like (servers, configs) where:
- servers contains a list of loaded notification plugins
- configs contains a list of additional configuration files
referenced.
You may also optionally associate an asset with the notification.
The file syntax is:
@ -373,14 +514,25 @@ class ConfigBase(URLBase):
# Or you can use this format (no tags associated)
<URL>
# you can also use the keyword 'include' and identify a
# configuration location (like this file) which will be included
# as additional configuration entries when loaded.
include <ConfigURL>
"""
response = list()
# A list of loaded Notification Services
servers = list()
# A list of additional configuration files referenced using
# the include keyword
configs = list()
# Define what a valid line should look like
valid_line_re = re.compile(
r'^\s*(?P<line>([;#]+(?P<comment>.*))|'
r'(\s*(?P<tags>[^=]+)=|=)?\s*'
r'(?P<url>[a-z0-9]{2,9}://.*))?$', re.I)
r'(?P<url>[a-z0-9]{2,9}://.*)|'
r'include\s+(?P<config>.+))?\s*$', re.I)
try:
# split our content up to read line by line
@ -388,28 +540,35 @@ class ConfigBase(URLBase):
except TypeError:
# content was not expected string type
ConfigBase.logger.error('Invalid apprise text data specified')
return list()
ConfigBase.logger.error(
'Invalid Apprise TEXT based configuration specified.')
return (list(), list())
for line, entry in enumerate(content, start=1):
result = valid_line_re.match(entry)
if not result:
# Invalid syntax
ConfigBase.logger.error(
'Invalid apprise text format found '
'Invalid Apprise TEXT configuration format found '
'{} on line {}.'.format(entry, line))
# Assume this is a file we shouldn't be parsing. It's owner
# can read the error printed to screen and take action
# otherwise.
return list()
return (list(), list())
# Store our url read in
url = result.group('url')
if not url:
url, config = result.group('url'), result.group('config')
if not (url or config):
# Comment/empty line; do nothing
continue
if config:
ConfigBase.logger.debug('Include URL: {}'.format(config))
# Store our include line
configs.append(config.strip())
continue
# Acquire our url tokens
results = plugins.url_to_dict(url)
if results is None:
@ -422,11 +581,6 @@ class ConfigBase(URLBase):
# notifications if any were set
results['tag'] = set(parse_list(result.group('tags')))
ConfigBase.logger.trace(
'URL {} unpacked as:{}{}'.format(
url, os.linesep, os.linesep.join(
['{}="{}"'.format(k, v) for k, v in results.items()])))
# Prepare our Asset Object
results['asset'] = \
asset if isinstance(asset, AppriseAsset) else AppriseAsset()
@ -448,23 +602,32 @@ class ConfigBase(URLBase):
continue
# if we reach here, we successfully loaded our data
response.append(plugin)
servers.append(plugin)
# Return what was loaded
return response
return (servers, configs)
@staticmethod
def config_parse_yaml(content, asset=None):
"""
Parse the specified content as though it were a yaml file
specifically formatted for apprise. Return a list of loaded
notification plugins.
specifically formatted for Apprise.
Return a tuple that looks like (servers, configs) where:
- servers contains a list of loaded notification plugins
- configs contains a list of additional configuration files
referenced.
Optionally associate an asset with the notification.
You may optionally associate an asset with the notification.
"""
response = list()
# A list of loaded Notification Services
servers = list()
# A list of additional configuration files referenced using
# the include keyword
configs = list()
try:
# Load our data (safely)
@ -473,23 +636,24 @@ class ConfigBase(URLBase):
except (AttributeError, yaml.error.MarkedYAMLError) as e:
# Invalid content
ConfigBase.logger.error(
'Invalid apprise yaml data specified.')
'Invalid Apprise YAML data specified.')
ConfigBase.logger.debug(
'YAML Exception:{}{}'.format(os.linesep, e))
return list()
return (list(), list())
if not isinstance(result, dict):
# Invalid content
ConfigBase.logger.error('Invalid apprise yaml structure specified')
return list()
ConfigBase.logger.error(
'Invalid Apprise YAML based configuration specified.')
return (list(), list())
# YAML Version
version = result.get('version', 1)
if version != 1:
# Invalid syntax
ConfigBase.logger.error(
'Invalid apprise yaml version specified {}.'.format(version))
return list()
'Invalid Apprise YAML version specified {}.'.format(version))
return (list(), list())
#
# global asset object
@ -536,15 +700,38 @@ class ConfigBase(URLBase):
# Store any preset tags
global_tags = set(parse_list(tags))
#
# include root directive
#
includes = result.get('include', None)
if isinstance(includes, six.string_types):
# Support a single inline string or multiple ones separated by a
# comma and/or space
includes = parse_urls(includes)
elif not isinstance(includes, (list, tuple)):
# Not a problem; we simply have no includes
includes = list()
# Iterate over each config URL
for no, url in enumerate(includes):
if isinstance(url, six.string_types):
# Support a single inline string or multiple ones separated by
# a comma and/or space
configs.extend(parse_urls(url))
elif isinstance(url, dict):
# Store the url and ignore arguments associated
configs.extend(u for u in url.keys())
#
# urls root directive
#
urls = result.get('urls', None)
if not isinstance(urls, (list, tuple)):
# Unsupported
ConfigBase.logger.error(
'Missing "urls" directive in apprise yaml.')
return list()
# Not a problem; we simply have no urls
urls = list()
# Iterate over each URL
for no, url in enumerate(urls):
@ -656,7 +843,7 @@ class ConfigBase(URLBase):
else:
# Unsupported
ConfigBase.logger.warning(
'Unsupported apprise yaml entry #{}'.format(no + 1))
'Unsupported Apprise YAML entry #{}'.format(no + 1))
continue
# Track our entries
@ -669,7 +856,7 @@ class ConfigBase(URLBase):
# Grab our first item
_results = results.pop(0)
# tag is a special keyword that is managed by apprise object.
# tag is a special keyword that is managed by Apprise object.
# The below ensures our tags are set correctly
if 'tag' in _results:
# Tidy our list up
@ -698,17 +885,19 @@ class ConfigBase(URLBase):
ConfigBase.logger.debug(
'Loaded URL: {}'.format(plugin.url()))
except Exception:
except Exception as e:
# the arguments are invalid or can not be used.
ConfigBase.logger.warning(
'Could not load apprise yaml entry #{}, item #{}'
'Could not load Apprise YAML configuration '
'entry #{}, item #{}'
.format(no + 1, entry))
ConfigBase.logger.debug('Loading Exception: %s' % str(e))
continue
# if we reach here, we successfully loaded our data
response.append(plugin)
servers.append(plugin)
return response
return (servers, configs)
def pop(self, index=-1):
"""

@ -28,6 +28,7 @@ import io
import os
from .ConfigBase import ConfigBase
from ..common import ConfigFormat
from ..common import ConfigIncludeMode
from ..AppriseLocale import gettext_lazy as _
@ -42,6 +43,9 @@ class ConfigFile(ConfigBase):
# The default protocol
protocol = 'file'
# Configuration file inclusion can only be of the same type
allow_cross_includes = ConfigIncludeMode.STRICT
def __init__(self, path, **kwargs):
"""
Initialize File Object
@ -53,7 +57,10 @@ class ConfigFile(ConfigBase):
super(ConfigFile, self).__init__(**kwargs)
# Store our file path as it was set
self.path = os.path.expanduser(path)
self.path = os.path.abspath(os.path.expanduser(path))
# Update the config path to be relative to our file we just loaded
self.config_path = os.path.dirname(self.path)
return
@ -69,19 +76,19 @@ class ConfigFile(ConfigBase):
else:
cache = int(self.cache)
# Define any arguments set
args = {
# Define any URL parameters
params = {
'encoding': self.encoding,
'cache': cache,
}
if self.config_format:
# A format was enforced; make sure it's passed back with the url
args['format'] = self.config_format
params['format'] = self.config_format
return 'file://{path}{args}'.format(
return 'file://{path}{params}'.format(
path=self.quote(self.path),
args='?{}'.format(self.urlencode(args)) if args else '',
params='?{}'.format(self.urlencode(params)) if params else '',
)
def read(self, **kwargs):
@ -91,10 +98,9 @@ class ConfigFile(ConfigBase):
response = None
path = os.path.expanduser(self.path)
try:
if self.max_buffer_size > 0 and \
os.path.getsize(path) > self.max_buffer_size:
os.path.getsize(self.path) > self.max_buffer_size:
# Content exceeds maximum buffer size
self.logger.error(
@ -106,7 +112,7 @@ class ConfigFile(ConfigBase):
# getsize() can throw this acception if the file is missing
# and or simply isn't accessible
self.logger.error(
'File is not accessible: {}'.format(path))
'File is not accessible: {}'.format(self.path))
return None
# Always call throttle before any server i/o is made
@ -115,7 +121,7 @@ class ConfigFile(ConfigBase):
try:
# Python 3 just supports open(), however to remain compatible with
# Python 2, we use the io module
with io.open(path, "rt", encoding=self.encoding) as f:
with io.open(self.path, "rt", encoding=self.encoding) as f:
# Store our content for parsing
response = f.read()
@ -126,7 +132,7 @@ class ConfigFile(ConfigBase):
self.logger.error(
'File not using expected encoding ({}) : {}'.format(
self.encoding, path))
self.encoding, self.path))
return None
except (IOError, OSError):
@ -136,13 +142,13 @@ class ConfigFile(ConfigBase):
# Could not open and/or read the file; this is not a problem since
# we scan a lot of default paths.
self.logger.error(
'File can not be opened for read: {}'.format(path))
'File can not be opened for read: {}'.format(self.path))
return None
# Detect config format based on file extension if it isn't already
# enforced
if self.config_format is None and \
re.match(r'^.*\.ya?ml\s*$', path, re.I) is not None:
re.match(r'^.*\.ya?ml\s*$', self.path, re.I) is not None:
# YAML Filename Detected
self.default_config_format = ConfigFormat.YAML
@ -163,7 +169,7 @@ class ConfigFile(ConfigBase):
# We're done early; it's not a good URL
return results
match = re.match(r'file://(?P<path>[^?]+)(\?.*)?', url, re.I)
match = re.match(r'[a-z0-9]+://(?P<path>[^?]+)(\?.*)?', url, re.I)
if not match:
return None

@ -28,6 +28,7 @@ import six
import requests
from .ConfigBase import ConfigBase
from ..common import ConfigFormat
from ..common import ConfigIncludeMode
from ..URLBase import PrivacyMode
from ..AppriseLocale import gettext_lazy as _
@ -58,16 +59,15 @@ class ConfigHTTP(ConfigBase):
# The default secure protocol
secure_protocol = 'https'
# The maximum number of seconds to wait for a connection to be established
# before out-right just giving up
connection_timeout_sec = 5.0
# If an HTTP error occurs, define the number of characters you still want
# to read back. This is useful for debugging purposes, but nothing else.
# The idea behind enforcing this kind of restriction is to prevent abuse
# from queries to services that may be untrusted.
max_error_buffer_size = 2048
# Configuration file inclusion can always include this type
allow_cross_includes = ConfigIncludeMode.ALWAYS
def __init__(self, headers=None, **kwargs):
"""
Initialize HTTP Object
@ -104,18 +104,20 @@ class ConfigHTTP(ConfigBase):
cache = int(self.cache)
# Define any arguments set
args = {
'verify': 'yes' if self.verify_certificate else 'no',
params = {
'encoding': self.encoding,
'cache': cache,
}
# Extend our parameters
params.update(self.url_parameters(privacy=privacy, *args, **kwargs))
if self.config_format:
# A format was enforced; make sure it's passed back with the url
args['format'] = self.config_format
params['format'] = self.config_format
# Append our headers into our args
args.update({'+{}'.format(k): v for k, v in self.headers.items()})
params.update({'+{}'.format(k): v for k, v in self.headers.items()})
# Determine Authentication
auth = ''
@ -132,14 +134,14 @@ class ConfigHTTP(ConfigBase):
default_port = 443 if self.secure else 80
return '{schema}://{auth}{hostname}{port}{fullpath}/?{args}'.format(
return '{schema}://{auth}{hostname}{port}{fullpath}/?{params}'.format(
schema=self.secure_protocol if self.secure else self.protocol,
auth=auth,
hostname=self.quote(self.host, safe=''),
port='' if self.port is None or self.port == default_port
else ':{}'.format(self.port),
fullpath=self.quote(self.fullpath, safe='/'),
args=self.urlencode(args),
params=self.urlencode(params),
)
def read(self, **kwargs):
@ -185,7 +187,7 @@ class ConfigHTTP(ConfigBase):
headers=headers,
auth=auth,
verify=self.verify_certificate,
timeout=self.connection_timeout_sec,
timeout=self.request_timeout,
stream=True) as r:
# Handle Errors
@ -211,7 +213,7 @@ class ConfigHTTP(ConfigBase):
return None
# Store our result (but no more than our buffer length)
response = r.content[:self.max_buffer_size + 1]
response = r.text[:self.max_buffer_size + 1]
# Verify that our content did not exceed the buffer size:
if len(response) > self.max_buffer_size:
@ -240,7 +242,7 @@ class ConfigHTTP(ConfigBase):
except requests.RequestException as e:
self.logger.error(
'A Connection error occured retrieving HTTP '
'A Connection error occurred retrieving HTTP '
'configuration from %s.' % self.host)
self.logger.debug('Socket Exception: %s' % str(e))
@ -254,7 +256,7 @@ class ConfigHTTP(ConfigBase):
def parse_url(url):
"""
Parses the URL and returns enough arguments that can allow
us to substantiate this object.
us to re-instantiate this object.
"""
results = ConfigBase.parse_url(url)

@ -23,12 +23,12 @@
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
import six
import re
import six
from os import listdir
from os.path import dirname
from os.path import abspath
from ..logger import logger
# Maintains a mapping of all of the configuration services
SCHEMA_MAP = {}
@ -88,29 +88,39 @@ def __load_matrix(path=abspath(dirname(__file__)), name='apprise.config'):
# not the module:
globals()[plugin_name] = plugin
# Load protocol(s) if defined
proto = getattr(plugin, 'protocol', None)
if isinstance(proto, six.string_types):
if proto not in SCHEMA_MAP:
SCHEMA_MAP[proto] = plugin
elif isinstance(proto, (set, list, tuple)):
# Support iterables list types
for p in proto:
if p not in SCHEMA_MAP:
SCHEMA_MAP[p] = plugin
# Load secure protocol(s) if defined
protos = getattr(plugin, 'secure_protocol', None)
if isinstance(protos, six.string_types):
if protos not in SCHEMA_MAP:
SCHEMA_MAP[protos] = plugin
if isinstance(protos, (set, list, tuple)):
# Support iterables list types
for p in protos:
if p not in SCHEMA_MAP:
SCHEMA_MAP[p] = plugin
fn = getattr(plugin, 'schemas', None)
try:
schemas = set([]) if not callable(fn) else fn(plugin)
except TypeError:
# Python v2.x support where functions associated with classes
# were considered bound to them and could not be called prior
# to the classes initialization. This code can be dropped
# once Python v2.x support is dropped. The below code introduces
# replication as it already exists and is tested in
# URLBase.schemas()
schemas = set([])
for key in ('protocol', 'secure_protocol'):
schema = getattr(plugin, key, None)
if isinstance(schema, six.string_types):
schemas.add(schema)
elif isinstance(schema, (set, list, tuple)):
# Support iterables list types
for s in schema:
if isinstance(s, six.string_types):
schemas.add(s)
# map our schema to our plugin
for schema in schemas:
if schema in SCHEMA_MAP:
logger.error(
"Config schema ({}) mismatch detected - {} to {}"
.format(schema, SCHEMA_MAP[schema], plugin))
continue
# Assign plugin
SCHEMA_MAP[schema] = plugin
return SCHEMA_MAP

@ -6,16 +6,16 @@
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: apprise 0.8.5\n"
"Project-Id-Version: apprise 0.8.8\n"
"Report-Msgid-Bugs-To: lead2gold@gmail.com\n"
"POT-Creation-Date: 2020-03-30 16:00-0400\n"
"POT-Creation-Date: 2020-09-02 07:46-0400\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 2.8.0\n"
"Generated-By: Babel 2.7.0\n"
msgid "API Key"
msgstr ""
@ -35,6 +35,9 @@ msgstr ""
msgid "Access Token"
msgstr ""
msgid "Account Email"
msgstr ""
msgid "Account SID"
msgstr ""
@ -59,6 +62,9 @@ msgstr ""
msgid "Avatar Image"
msgstr ""
msgid "Avatar URL"
msgstr ""
msgid "Batch Mode"
msgstr ""
@ -83,6 +89,12 @@ msgstr ""
msgid "Channels"
msgstr ""
msgid "Client ID"
msgstr ""
msgid "Client Secret"
msgstr ""
msgid "Consumer Key"
msgstr ""
@ -92,9 +104,18 @@ msgstr ""
msgid "Country"
msgstr ""
msgid "Custom Icon"
msgstr ""
msgid "Cycles"
msgstr ""
msgid "Detect Bot Owner"
msgstr ""
msgid "Device API Key"
msgstr ""
msgid "Device ID"
msgstr ""
@ -161,6 +182,9 @@ msgstr ""
msgid "IRC Colors"
msgstr ""
msgid "Icon Type"
msgstr ""
msgid "Include Footer"
msgstr ""
@ -188,6 +212,9 @@ msgstr ""
msgid "Modal"
msgstr ""
msgid "Mode"
msgstr ""
msgid "Notify Format"
msgstr ""
@ -269,6 +296,12 @@ msgstr ""
msgid "Server Timeout"
msgstr ""
msgid "Socket Connect Timeout"
msgstr ""
msgid "Socket Read Timeout"
msgstr ""
msgid "Sound"
msgstr ""
@ -281,6 +314,12 @@ msgstr ""
msgid "Source Phone No"
msgstr ""
msgid "Sticky"
msgstr ""
msgid "Subtitle"
msgstr ""
msgid "Target Channel"
msgstr ""
@ -338,6 +377,9 @@ msgstr ""
msgid "Template Data"
msgstr ""
msgid "Tenant Domain"
msgstr ""
msgid "Text To Speech"
msgstr ""
@ -368,6 +410,9 @@ msgstr ""
msgid "Use Avatar"
msgstr ""
msgid "User ID"
msgstr ""
msgid "User Key"
msgstr ""

@ -24,6 +24,7 @@
# THE SOFTWARE.
import re
import six
from ..URLBase import URLBase
from ..common import NotifyType
@ -36,7 +37,17 @@ from ..AppriseLocale import gettext_lazy as _
from ..AppriseAttachment import AppriseAttachment
class NotifyBase(URLBase):
if six.PY3:
# Wrap our base with the asyncio wrapper
from ..py3compat.asyncio import AsyncNotifyBase
BASE_OBJECT = AsyncNotifyBase
else:
# Python v2.7 (backwards compatibility)
BASE_OBJECT = URLBase
class NotifyBase(BASE_OBJECT):
"""
This is the base class for all notification services
"""
@ -80,21 +91,11 @@ class NotifyBase(URLBase):
# use a <b> tag. The below causes the <b>title</b> to get generated:
default_html_tag_id = 'b'
# Define a default set of template arguments used for dynamically building
# details about our individual plugins for developers.
# Define object templates
templates = ()
# Provides a mapping of tokens, certain entries are fixed and automatically
# configured if found (such as schema, host, user, pass, and port)
template_tokens = {}
# Here is where we define all of the arguments we accept on the url
# such as: schema://whatever/?overflow=upstream&format=text
# These act the same way as tokens except they are optional and/or
# have default values set if mandatory. This rule must be followed
template_args = {
template_args = dict(URLBase.template_args, **{
'overflow': {
'name': _('Overflow Mode'),
'type': 'choice:string',
@ -119,34 +120,7 @@ class NotifyBase(URLBase):
# runtime.
'_lookup_default': 'notify_format',
},
'verify': {
'name': _('Verify SSL'),
# SSL Certificate Authority Verification
'type': 'bool',
# Provide a default
'default': URLBase.verify_certificate,
# look up default using the following parent class value at
# runtime.
'_lookup_default': 'verify_certificate',
},
}
# kwargs are dynamically built because a prefix causes us to parse the
# content slightly differently. The prefix is required and can be either
# a (+ or -). Below would handle the +key=value:
# {
# 'headers': {
# 'name': _('HTTP Header'),
# 'prefix': '+',
# 'type': 'string',
# },
# },
#
# In a kwarg situation, the 'key' is always presumed to be treated as
# a string. When the 'type' is defined, it is being defined to respect
# the 'value'.
template_kwargs = {}
})
def __init__(self, **kwargs):
"""
@ -161,7 +135,7 @@ class NotifyBase(URLBase):
# Store the specified format if specified
notify_format = kwargs.get('format', '')
if notify_format.lower() not in NOTIFY_FORMATS:
msg = 'Invalid notification format %s'.format(notify_format)
msg = 'Invalid notification format {}'.format(notify_format)
self.logger.error(msg)
raise TypeError(msg)
@ -368,6 +342,23 @@ class NotifyBase(URLBase):
raise NotImplementedError(
"send() is not implimented by the child class.")
def url_parameters(self, *args, **kwargs):
"""
Provides a default set of parameters to work with. This can greatly
simplify URL construction in the acommpanied url() function in all
defined plugin services.
"""
params = {
'format': self.notify_format,
'overflow': self.overflow_mode,
}
params.update(super(NotifyBase, self).url_parameters(*args, **kwargs))
# return default parameters
return params
@staticmethod
def parse_url(url, verify_host=True):
"""Parses the URL and returns it broken apart into a dictionary.

@ -279,6 +279,7 @@ class NotifyBoxcar(NotifyBase):
data=dumps(payload),
headers=headers,
verify=self.verify_certificate,
timeout=self.request_timeout,
)
# Boxcar returns 201 (Created) when successful
@ -304,7 +305,7 @@ class NotifyBoxcar(NotifyBase):
except requests.RequestException as e:
self.logger.warning(
'A Connection error occured sending Boxcar '
'A Connection error occurred sending Boxcar '
'notification to %s.' % (host))
self.logger.debug('Socket Exception: %s' % str(e))
@ -319,15 +320,15 @@ class NotifyBoxcar(NotifyBase):
Returns the URL built dynamically based on specified arguments.
"""
# Define any arguments set
args = {
'format': self.notify_format,
'overflow': self.overflow_mode,
# Define any URL parameters
params = {
'image': 'yes' if self.include_image else 'no',
'verify': 'yes' if self.verify_certificate else 'no',
}
return '{schema}://{access}/{secret}/{targets}?{args}'.format(
# Extend our parameters
params.update(self.url_parameters(privacy=privacy, *args, **kwargs))
return '{schema}://{access}/{secret}/{targets}?{params}'.format(
schema=self.secure_protocol,
access=self.pprint(self.access, privacy, safe=''),
secret=self.pprint(
@ -335,7 +336,7 @@ class NotifyBoxcar(NotifyBase):
targets='/'.join([
NotifyBoxcar.quote(x, safe='') for x in chain(
self.tags, self.device_tokens) if x != DEFAULT_TAG]),
args=NotifyBoxcar.urlencode(args),
params=NotifyBoxcar.urlencode(params),
)
@staticmethod
@ -345,7 +346,6 @@ class NotifyBoxcar(NotifyBase):
"""
results = NotifyBase.parse_url(url, verify_host=False)
if not results:
# We're done early
return None

@ -221,6 +221,7 @@ class NotifyClickSend(NotifyBase):
data=dumps(payload),
headers=headers,
verify=self.verify_certificate,
timeout=self.request_timeout,
)
if r.status_code != requests.codes.ok:
# We had a problem
@ -256,7 +257,7 @@ class NotifyClickSend(NotifyBase):
except requests.RequestException as e:
self.logger.warning(
'A Connection error occured sending {} ClickSend '
'A Connection error occurred sending {} ClickSend '
'notification(s).'.format(len(payload['messages'])))
self.logger.debug('Socket Exception: %s' % str(e))
@ -271,14 +272,14 @@ class NotifyClickSend(NotifyBase):
Returns the URL built dynamically based on specified arguments.
"""
# Define any arguments set
args = {
'format': self.notify_format,
'overflow': self.overflow_mode,
'verify': 'yes' if self.verify_certificate else 'no',
# Define any URL parameters
params = {
'batch': 'yes' if self.batch else 'no',
}
# Extend our parameters
params.update(self.url_parameters(privacy=privacy, *args, **kwargs))
# Setup Authentication
auth = '{user}:{password}@'.format(
user=NotifyClickSend.quote(self.user, safe=''),
@ -286,19 +287,19 @@ class NotifyClickSend(NotifyBase):
self.password, privacy, mode=PrivacyMode.Secret, safe=''),
)
return '{schema}://{auth}{targets}?{args}'.format(
return '{schema}://{auth}{targets}?{params}'.format(
schema=self.secure_protocol,
auth=auth,
targets='/'.join(
[NotifyClickSend.quote(x, safe='') for x in self.targets]),
args=NotifyClickSend.urlencode(args),
params=NotifyClickSend.urlencode(params),
)
@staticmethod
def parse_url(url):
"""
Parses the URL and returns enough arguments that can allow
us to substantiate this object.
us to re-instantiate this object.
"""
results = NotifyBase.parse_url(url, verify_host=False)

@ -304,6 +304,7 @@ class NotifyD7Networks(NotifyBase):
data=dumps(payload),
headers=headers,
verify=self.verify_certificate,
timeout=self.request_timeout,
)
if r.status_code not in (
@ -379,7 +380,7 @@ class NotifyD7Networks(NotifyBase):
except requests.RequestException as e:
self.logger.warning(
'A Connection error occured sending D7 Networks:%s ' % (
'A Connection error occurred sending D7 Networks:%s ' % (
', '.join(self.targets)) + 'notification.'
)
self.logger.debug('Socket Exception: %s' % str(e))
@ -394,38 +395,37 @@ class NotifyD7Networks(NotifyBase):
Returns the URL built dynamically based on specified arguments.
"""
# Define any arguments set
args = {
'format': self.notify_format,
'overflow': self.overflow_mode,
'verify': 'yes' if self.verify_certificate else 'no',
# Define any URL parameters
params = {
'batch': 'yes' if self.batch else 'no',
}
# Extend our parameters
params.update(self.url_parameters(privacy=privacy, *args, **kwargs))
if self.priority != self.template_args['priority']['default']:
args['priority'] = str(self.priority)
params['priority'] = str(self.priority)
if self.source:
args['from'] = self.source
params['from'] = self.source
return '{schema}://{user}:{password}@{targets}/?{args}'.format(
return '{schema}://{user}:{password}@{targets}/?{params}'.format(
schema=self.secure_protocol,
user=NotifyD7Networks.quote(self.user, safe=''),
password=self.pprint(
self.password, privacy, mode=PrivacyMode.Secret, safe=''),
targets='/'.join(
[NotifyD7Networks.quote(x, safe='') for x in self.targets]),
args=NotifyD7Networks.urlencode(args))
params=NotifyD7Networks.urlencode(params))
@staticmethod
def parse_url(url):
"""
Parses the URL and returns enough arguments that can allow
us to substantiate this object.
us to re-instantiate this object.
"""
results = NotifyBase.parse_url(url, verify_host=False)
if not results:
# We're done early as we couldn't load the results
return results

@ -29,7 +29,6 @@ from __future__ import print_function
from .NotifyBase import NotifyBase
from ..common import NotifyImageSize
from ..common import NotifyType
from ..utils import GET_SCHEMA_RE
from ..utils import parse_bool
from ..AppriseLocale import gettext_lazy as _
@ -141,7 +140,6 @@ class NotifyDBus(NotifyBase):
# object if we were to reference, we wouldn't be backwards compatible with
# Python v2. So converting the result set back into a list makes us
# compatible
protocol = list(MAINLOOP_MAP.keys())
# A URL that takes you to the setup/help of the specific protocol
@ -153,7 +151,7 @@ class NotifyDBus(NotifyBase):
# Allows the user to specify the NotifyImageSize object
image_size = NotifyImageSize.XY_128
# The number of seconds to keep the message present for
# The number of milliseconds to keep the message present for
message_timeout_ms = 13000
# Limit results to just the first 10 line otherwise there is just to much
@ -171,7 +169,7 @@ class NotifyDBus(NotifyBase):
# Define object templates
templates = (
'{schema}://_/',
'{schema}://',
)
# Define our template arguments
@ -355,27 +353,27 @@ class NotifyDBus(NotifyBase):
DBusUrgency.HIGH: 'high',
}
# Define any arguments set
args = {
'format': self.notify_format,
'overflow': self.overflow_mode,
# Define any URL parameters
params = {
'image': 'yes' if self.include_image else 'no',
'urgency': 'normal' if self.urgency not in _map
else _map[self.urgency],
'verify': 'yes' if self.verify_certificate else 'no',
}
# Extend our parameters
params.update(self.url_parameters(privacy=privacy, *args, **kwargs))
# x in (x,y) screen coordinates
if self.x_axis:
args['x'] = str(self.x_axis)
params['x'] = str(self.x_axis)
# y in (x,y) screen coordinates
if self.y_axis:
args['y'] = str(self.y_axis)
params['y'] = str(self.y_axis)
return '{schema}://_/?{args}'.format(
return '{schema}://_/?{params}'.format(
schema=self.schema,
args=NotifyDBus.urlencode(args),
params=NotifyDBus.urlencode(params),
)
@staticmethod
@ -386,24 +384,8 @@ class NotifyDBus(NotifyBase):
is in place.
"""
schema = GET_SCHEMA_RE.match(url)
if schema is None:
# Content is simply not parseable
return None
results = NotifyBase.parse_url(url)
if not results:
results = {
'schema': schema.group('schema').lower(),
'user': None,
'password': None,
'port': None,
'host': '_',
'fullpath': None,
'path': None,
'url': url,
'qsd': {},
}
results = NotifyBase.parse_url(url, verify_host=False)
# Include images with our message
results['include_image'] = \

@ -28,17 +28,17 @@
# here you'll be able to access the Webhooks menu and create a new one.
#
# When you've completed, you'll get a URL that looks a little like this:
# https://discordapp.com/api/webhooks/417429632418316298/\
# https://discord.com/api/webhooks/417429632418316298/\
# JHZ7lQml277CDHmQKMHI8qBe7bk2ZwO5UKjCiOAF7711o33MyqU344Qpgv7YTpadV_js
#
# Simplified, it looks like this:
# https://discordapp.com/api/webhooks/WEBHOOK_ID/WEBHOOK_TOKEN
# https://discord.com/api/webhooks/WEBHOOK_ID/WEBHOOK_TOKEN
#
# This plugin will simply work using the url of:
# discord://WEBHOOK_ID/WEBHOOK_TOKEN
#
# API Documentation on Webhooks:
# - https://discordapp.com/developers/docs/resources/webhook
# - https://discord.com/developers/docs/resources/webhook
#
import re
import requests
@ -63,7 +63,7 @@ class NotifyDiscord(NotifyBase):
service_name = 'Discord'
# The services URL
service_url = 'https://discordapp.com/'
service_url = 'https://discord.com/'
# The default secure protocol
secure_protocol = 'discord'
@ -72,7 +72,7 @@ class NotifyDiscord(NotifyBase):
setup_url = 'https://github.com/caronc/apprise/wiki/Notify_discord'
# Discord Webhook
notify_url = 'https://discordapp.com/api/webhooks'
notify_url = 'https://discord.com/api/webhooks'
# Allows the user to specify the NotifyImageSize object
image_size = NotifyImageSize.XY_256
@ -119,6 +119,10 @@ class NotifyDiscord(NotifyBase):
'type': 'bool',
'default': True,
},
'avatar_url': {
'name': _('Avatar URL'),
'type': 'string',
},
'footer': {
'name': _('Display Footer'),
'type': 'bool',
@ -139,7 +143,7 @@ class NotifyDiscord(NotifyBase):
def __init__(self, webhook_id, webhook_token, tts=False, avatar=True,
footer=False, footer_logo=True, include_image=False,
**kwargs):
avatar_url=None, **kwargs):
"""
Initialize Discord Object
@ -177,6 +181,11 @@ class NotifyDiscord(NotifyBase):
# Place a thumbnail image inline with the message body
self.include_image = include_image
# Avatar URL
# This allows a user to provide an over-ride to the otherwise
# dynamically generated avatar url images
self.avatar_url = avatar_url
return
def send(self, body, title='', notify_type=NotifyType.INFO, attach=None,
@ -247,8 +256,9 @@ class NotifyDiscord(NotifyBase):
payload['content'] = \
body if not title else "{}\r\n{}".format(title, body)
if self.avatar and image_url:
payload['avatar_url'] = image_url
if self.avatar and (image_url or self.avatar_url):
payload['avatar_url'] = \
self.avatar_url if self.avatar_url else image_url
if self.user:
# Optionally override the default username of the webhook
@ -343,6 +353,7 @@ class NotifyDiscord(NotifyBase):
headers=headers,
files=files,
verify=self.verify_certificate,
timeout=self.request_timeout,
)
if r.status_code not in (
requests.codes.ok, requests.codes.no_content):
@ -370,14 +381,14 @@ class NotifyDiscord(NotifyBase):
except requests.RequestException as e:
self.logger.warning(
'A Connection error occured posting {}to Discord.'.format(
'A Connection error occurred posting {}to Discord.'.format(
attach.name if attach else ''))
self.logger.debug('Socket Exception: %s' % str(e))
return False
except (OSError, IOError) as e:
self.logger.warning(
'An I/O error occured while reading {}.'.format(
'An I/O error occurred while reading {}.'.format(
attach.name if attach else 'attachment'))
self.logger.debug('I/O Exception: %s' % str(e))
return False
@ -395,37 +406,36 @@ class NotifyDiscord(NotifyBase):
Returns the URL built dynamically based on specified arguments.
"""
# Define any arguments set
args = {
'format': self.notify_format,
'overflow': self.overflow_mode,
# Define any URL parameters
params = {
'tts': 'yes' if self.tts else 'no',
'avatar': 'yes' if self.avatar else 'no',
'footer': 'yes' if self.footer else 'no',
'footer_logo': 'yes' if self.footer_logo else 'no',
'image': 'yes' if self.include_image else 'no',
'verify': 'yes' if self.verify_certificate else 'no',
}
return '{schema}://{webhook_id}/{webhook_token}/?{args}'.format(
# Extend our parameters
params.update(self.url_parameters(privacy=privacy, *args, **kwargs))
return '{schema}://{webhook_id}/{webhook_token}/?{params}'.format(
schema=self.secure_protocol,
webhook_id=self.pprint(self.webhook_id, privacy, safe=''),
webhook_token=self.pprint(self.webhook_token, privacy, safe=''),
args=NotifyDiscord.urlencode(args),
params=NotifyDiscord.urlencode(params),
)
@staticmethod
def parse_url(url):
"""
Parses the URL and returns enough arguments that can allow
us to substantiate this object.
us to re-instantiate this object.
Syntax:
discord://webhook_id/webhook_token
"""
results = NotifyBase.parse_url(url)
results = NotifyBase.parse_url(url, verify_host=False)
if not results:
# We're done early as we couldn't load the results
return results
@ -459,43 +469,39 @@ class NotifyDiscord(NotifyBase):
# Update Avatar Icon
results['avatar'] = parse_bool(results['qsd'].get('avatar', True))
# Use Thumbnail
if 'thumbnail' in results['qsd']:
# Deprication Notice issued for v0.7.5
NotifyDiscord.logger.deprecate(
'The Discord URL contains the parameter '
'"thumbnail=" which will be deprecated in an upcoming '
'release. Please use "image=" instead.'
)
# Boolean to include an image or not
results['include_image'] = parse_bool(results['qsd'].get(
'image', NotifyDiscord.template_args['image']['default']))
# use image= for consistency with the other plugins but we also
# support thumbnail= for backwards compatibility.
results['include_image'] = \
parse_bool(results['qsd'].get(
'image', results['qsd'].get('thumbnail', False)))
# Extract avatar url if it was specified
if 'avatar_url' in results['qsd']:
results['avatar_url'] = \
NotifyDiscord.unquote(results['qsd']['avatar_url'])
return results
@staticmethod
def parse_native_url(url):
"""
Support https://discordapp.com/api/webhooks/WEBHOOK_ID/WEBHOOK_TOKEN
Support https://discord.com/api/webhooks/WEBHOOK_ID/WEBHOOK_TOKEN
Support Legacy URL as well:
https://discordapp.com/api/webhooks/WEBHOOK_ID/WEBHOOK_TOKEN
"""
result = re.match(
r'^https?://discordapp\.com/api/webhooks/'
r'^https?://discord(app)?\.com/api/webhooks/'
r'(?P<webhook_id>[0-9]+)/'
r'(?P<webhook_token>[A-Z0-9_-]+)/?'
r'(?P<args>\?.+)?$', url, re.I)
r'(?P<params>\?.+)?$', url, re.I)
if result:
return NotifyDiscord.parse_url(
'{schema}://{webhook_id}/{webhook_token}/{args}'.format(
'{schema}://{webhook_id}/{webhook_token}/{params}'.format(
schema=NotifyDiscord.secure_protocol,
webhook_id=result.group('webhook_id'),
webhook_token=result.group('webhook_token'),
args='' if not result.group('args')
else result.group('args')))
params='' if not result.group('params')
else result.group('params')))
return None

@ -29,6 +29,9 @@ import smtplib
from email.mime.text import MIMEText
from email.mime.application import MIMEApplication
from email.mime.multipart import MIMEMultipart
from email.utils import formataddr
from email.header import Header
from email import charset
from socket import error as SocketError
from datetime import datetime
@ -38,10 +41,12 @@ from ..URLBase import PrivacyMode
from ..common import NotifyFormat
from ..common import NotifyType
from ..utils import is_email
from ..utils import parse_list
from ..utils import GET_EMAIL_RE
from ..utils import parse_emails
from ..AppriseLocale import gettext_lazy as _
# Globally Default encoding mode set to Quoted Printable.
charset.add_charset('utf-8', charset.QP, charset.QP, 'utf-8')
class WebBaseLogin(object):
"""
@ -116,6 +121,21 @@ EMAIL_TEMPLATES = (
},
),
# Microsoft Office 365 (Email Server)
# You must specify an authenticated sender address in the from= settings
# and a valid email in the to= to deliver your emails to
(
'Microsoft Office 365',
re.compile(
r'^[^@]+@(?P<domain>(smtp\.)?office365\.com)$', re.I),
{
'port': 587,
'smtp_host': 'smtp.office365.com',
'secure': True,
'secure_mode': SecureMailMode.STARTTLS,
},
),
# Yahoo Mail
(
'Yahoo Mail',
@ -380,8 +400,8 @@ class NotifyEmail(NotifyBase):
except (ValueError, TypeError):
self.timeout = self.connect_timeout
# Acquire targets
self.targets = parse_list(targets)
# Acquire Email 'To'
self.targets = list()
# Acquire Carbon Copies
self.cc = set()
@ -389,9 +409,11 @@ class NotifyEmail(NotifyBase):
# Acquire Blind Carbon Copies
self.bcc = set()
# For tracking our email -> name lookups
self.names = {}
# Now we want to construct the To and From email
# addresses from the URL provided
self.from_name = from_name
self.from_addr = from_addr
if self.user and not self.from_addr:
@ -401,15 +423,18 @@ class NotifyEmail(NotifyBase):
self.host,
)
if not is_email(self.from_addr):
result = is_email(self.from_addr)
if not result:
# Parse Source domain based on from_addr
msg = 'Invalid ~From~ email specified: {}'.format(self.from_addr)
self.logger.warning(msg)
raise TypeError(msg)
# If our target email list is empty we want to add ourselves to it
if len(self.targets) == 0:
self.targets.append(self.from_addr)
# Store our email address
self.from_addr = result['full_email']
# Set our from name
self.from_name = from_name if from_name else result['name']
# Now detect the SMTP Server
self.smtp_host = \
@ -425,11 +450,35 @@ class NotifyEmail(NotifyBase):
self.logger.warning(msg)
raise TypeError(msg)
# Validate recipients (cc:) and drop bad ones:
for recipient in parse_list(cc):
if targets:
# Validate recipients (to:) and drop bad ones:
for recipient in parse_emails(targets):
result = is_email(recipient)
if result:
self.targets.append(
(result['name'] if result['name'] else False,
result['full_email']))
continue
self.logger.warning(
'Dropped invalid To email '
'({}) specified.'.format(recipient),
)
if GET_EMAIL_RE.match(recipient):
self.cc.add(recipient)
else:
# If our target email list is empty we want to add ourselves to it
self.targets.append(
(self.from_name if self.from_name else False, self.from_addr))
# Validate recipients (cc:) and drop bad ones:
for recipient in parse_emails(cc):
email = is_email(recipient)
if email:
self.cc.add(email['full_email'])
# Index our name (if one exists)
self.names[email['full_email']] = \
email['name'] if email['name'] else False
continue
self.logger.warning(
@ -438,10 +487,14 @@ class NotifyEmail(NotifyBase):
)
# Validate recipients (bcc:) and drop bad ones:
for recipient in parse_list(bcc):
if GET_EMAIL_RE.match(recipient):
self.bcc.add(recipient)
for recipient in parse_emails(bcc):
email = is_email(recipient)
if email:
self.bcc.add(email['full_email'])
# Index our name (if one exists)
self.names[email['full_email']] = \
email['name'] if email['name'] else False
continue
self.logger.warning(
@ -529,36 +582,57 @@ class NotifyEmail(NotifyBase):
Perform Email Notification
"""
from_name = self.from_name
if not from_name:
from_name = self.app_desc
# Initialize our default from name
from_name = self.from_name if self.from_name else self.app_desc
# error tracking (used for function return)
has_error = False
if not self.targets:
# There is no one to email; we're done
self.logger.warning(
'There are no Email recipients to notify')
return False
# Create a copy of the targets list
emails = list(self.targets)
while len(emails):
# Get our email to notify
to_addr = emails.pop(0)
if not is_email(to_addr):
self.logger.warning(
'Invalid ~To~ email specified: {}'.format(to_addr))
has_error = True
continue
to_name, to_addr = emails.pop(0)
# Strip target out of cc list if in To or Bcc
cc = (self.cc - self.bcc - set([to_addr]))
# Strip target out of bcc list if in To
bcc = (self.bcc - set([to_addr]))
try:
# Format our cc addresses to support the Name field
cc = [formataddr(
(self.names.get(addr, False), addr), charset='utf-8')
for addr in cc]
# Format our bcc addresses to support the Name field
bcc = [formataddr(
(self.names.get(addr, False), addr), charset='utf-8')
for addr in bcc]
except TypeError:
# Python v2.x Support (no charset keyword)
# Format our cc addresses to support the Name field
cc = [formataddr(
(self.names.get(addr, False), addr)) for addr in cc]
# Format our bcc addresses to support the Name field
bcc = [formataddr(
(self.names.get(addr, False), addr)) for addr in bcc]
self.logger.debug(
'Email From: {} <{}>'.format(from_name, self.from_addr))
self.logger.debug('Email To: {}'.format(to_addr))
if len(cc):
if cc:
self.logger.debug('Email Cc: {}'.format(', '.join(cc)))
if len(bcc):
if bcc:
self.logger.debug('Email Bcc: {}'.format(', '.join(bcc)))
self.logger.debug('Login ID: {}'.format(self.user))
self.logger.debug(
@ -566,15 +640,25 @@ class NotifyEmail(NotifyBase):
# Prepare Email Message
if self.notify_format == NotifyFormat.HTML:
content = MIMEText(body, 'html')
content = MIMEText(body, 'html', 'utf-8')
else:
content = MIMEText(body, 'plain')
content = MIMEText(body, 'plain', 'utf-8')
base = MIMEMultipart() if attach else content
base['Subject'] = title
base['From'] = '{} <{}>'.format(from_name, self.from_addr)
base['To'] = to_addr
base['Subject'] = Header(title, 'utf-8')
try:
base['From'] = formataddr(
(from_name if from_name else False, self.from_addr),
charset='utf-8')
base['To'] = formataddr((to_name, to_addr), charset='utf-8')
except TypeError:
# Python v2.x Support (no charset keyword)
base['From'] = formataddr(
(from_name if from_name else False, self.from_addr))
base['To'] = formataddr((to_name, to_addr))
base['Cc'] = ','.join(cc)
base['Date'] = \
datetime.utcnow().strftime("%a, %d %b %Y %H:%M:%S +0000")
@ -608,7 +692,8 @@ class NotifyEmail(NotifyBase):
app.add_header(
'Content-Disposition',
'attachment; filename="{}"'.format(
attachment.name))
Header(attachment.name, 'utf-8')),
)
base.attach(app)
@ -653,7 +738,7 @@ class NotifyEmail(NotifyBase):
except (SocketError, smtplib.SMTPException, RuntimeError) as e:
self.logger.warning(
'A Connection error occured sending Email '
'A Connection error occurred sending Email '
'notification to {}.'.format(self.smtp_host))
self.logger.debug('Socket Exception: %s' % str(e))
@ -672,26 +757,34 @@ class NotifyEmail(NotifyBase):
Returns the URL built dynamically based on specified arguments.
"""
# Define any arguments set
args = {
'format': self.notify_format,
'overflow': self.overflow_mode,
# Define an URL parameters
params = {
'from': self.from_addr,
'name': self.from_name,
'mode': self.secure_mode,
'smtp': self.smtp_host,
'timeout': self.timeout,
'user': self.user,
'verify': 'yes' if self.verify_certificate else 'no',
}
# Extend our parameters
params.update(self.url_parameters(privacy=privacy, *args, **kwargs))
if self.from_name:
params['name'] = self.from_name
if len(self.cc) > 0:
# Handle our Carbon Copy Addresses
args['cc'] = ','.join(self.cc)
params['cc'] = ','.join(
['{}{}'.format(
'' if not e not in self.names
else '{}:'.format(self.names[e]), e) for e in self.cc])
if len(self.bcc) > 0:
# Handle our Blind Carbon Copy Addresses
args['bcc'] = ','.join(self.bcc)
params['bcc'] = ','.join(
['{}{}'.format(
'' if not e not in self.names
else '{}:'.format(self.names[e]), e) for e in self.bcc])
# pull email suffix from username (if present)
user = None if not self.user else self.user.split('@')[0]
@ -717,28 +810,31 @@ class NotifyEmail(NotifyBase):
# a simple boolean check as to whether we display our target emails
# or not
has_targets = \
not (len(self.targets) == 1 and self.targets[0] == self.from_addr)
not (len(self.targets) == 1
and self.targets[0][1] == self.from_addr)
return '{schema}://{auth}{hostname}{port}/{targets}?{args}'.format(
return '{schema}://{auth}{hostname}{port}/{targets}?{params}'.format(
schema=self.secure_protocol if self.secure else self.protocol,
auth=auth,
hostname=NotifyEmail.quote(self.host, safe=''),
# never encode hostname since we're expecting it to be a valid one
hostname=self.host,
port='' if self.port is None or self.port == default_port
else ':{}'.format(self.port),
targets='' if not has_targets else '/'.join(
[NotifyEmail.quote(x, safe='') for x in self.targets]),
args=NotifyEmail.urlencode(args),
[NotifyEmail.quote('{}{}'.format(
'' if not e[0] else '{}:'.format(e[0]), e[1]),
safe='') for e in self.targets]),
params=NotifyEmail.urlencode(params),
)
@staticmethod
def parse_url(url):
"""
Parses the URL and returns enough arguments that can allow
us to substantiate this object.
us to re-instantiate this object.
"""
results = NotifyBase.parse_url(url)
if not results:
# We're done early as we couldn't load the results
return results
@ -761,8 +857,7 @@ class NotifyEmail(NotifyBase):
# Attempt to detect 'to' email address
if 'to' in results['qsd'] and len(results['qsd']['to']):
results['targets'] += \
NotifyEmail.parse_list(results['qsd']['to'])
results['targets'].append(results['qsd']['to'])
if 'name' in results['qsd'] and len(results['qsd']['name']):
# Extract from name to associate with from address
@ -783,13 +878,11 @@ class NotifyEmail(NotifyBase):
# Handle Carbon Copy Addresses
if 'cc' in results['qsd'] and len(results['qsd']['cc']):
results['cc'] = \
NotifyEmail.parse_list(results['qsd']['cc'])
results['cc'] = results['qsd']['cc']
# Handle Blind Carbon Copy Addresses
if 'bcc' in results['qsd'] and len(results['qsd']['bcc']):
results['bcc'] = \
NotifyEmail.parse_list(results['qsd']['bcc'])
results['bcc'] = results['qsd']['bcc']
results['from_addr'] = from_addr
results['smtp_host'] = smtp_host

@ -61,9 +61,6 @@ class NotifyEmby(NotifyBase):
# A URL that takes you to the setup/help of the specific protocol
setup_url = 'https://github.com/caronc/apprise/wiki/Notify_emby'
# Emby uses the http protocol with JSON requests
emby_default_port = 8096
# By default Emby requires you to provide it a device id
# The following was just a random uuid4 generated one. There
# is no real reason to change this, but hey; that's what open
@ -94,6 +91,7 @@ class NotifyEmby(NotifyBase):
'type': 'int',
'min': 1,
'max': 65535,
'default': 8096
},
'user': {
'name': _('Username'),
@ -137,6 +135,10 @@ class NotifyEmby(NotifyBase):
# or a modal type box (requires an Okay acknowledgement)
self.modal = modal
if not self.port:
# Assign default port if one isn't otherwise specified:
self.port = self.template_tokens['port']['default']
if not self.user:
# User was not specified
msg = 'No Emby username was specified.'
@ -207,6 +209,7 @@ class NotifyEmby(NotifyBase):
headers=headers,
data=dumps(payload),
verify=self.verify_certificate,
timeout=self.request_timeout,
)
if r.status_code != requests.codes.ok:
@ -229,7 +232,7 @@ class NotifyEmby(NotifyBase):
except requests.RequestException as e:
self.logger.warning(
'A Connection error occured authenticating a user with Emby '
'A Connection error occurred authenticating a user with Emby '
'at %s.' % self.host)
self.logger.debug('Socket Exception: %s' % str(e))
@ -370,6 +373,7 @@ class NotifyEmby(NotifyBase):
url,
headers=headers,
verify=self.verify_certificate,
timeout=self.request_timeout,
)
if r.status_code != requests.codes.ok:
@ -392,7 +396,7 @@ class NotifyEmby(NotifyBase):
except requests.RequestException as e:
self.logger.warning(
'A Connection error occured querying Emby '
'A Connection error occurred querying Emby '
'for session information at %s.' % self.host)
self.logger.debug('Socket Exception: %s' % str(e))
@ -449,6 +453,7 @@ class NotifyEmby(NotifyBase):
url,
headers=headers,
verify=self.verify_certificate,
timeout=self.request_timeout,
)
if r.status_code not in (
@ -477,7 +482,7 @@ class NotifyEmby(NotifyBase):
except requests.RequestException as e:
self.logger.warning(
'A Connection error occured querying Emby '
'A Connection error occurred querying Emby '
'to logoff user %s at %s.' % (self.user, self.host))
self.logger.debug('Socket Exception: %s' % str(e))
@ -550,6 +555,7 @@ class NotifyEmby(NotifyBase):
data=dumps(payload),
headers=headers,
verify=self.verify_certificate,
timeout=self.request_timeout,
)
if r.status_code not in (
requests.codes.ok,
@ -577,7 +583,7 @@ class NotifyEmby(NotifyBase):
except requests.RequestException as e:
self.logger.warning(
'A Connection error occured sending Emby '
'A Connection error occurred sending Emby '
'notification to %s.' % self.host)
self.logger.debug('Socket Exception: %s' % str(e))
@ -592,14 +598,14 @@ class NotifyEmby(NotifyBase):
Returns the URL built dynamically based on specified arguments.
"""
# Define any arguments set
args = {
'format': self.notify_format,
'overflow': self.overflow_mode,
# Define any URL parameters
params = {
'modal': 'yes' if self.modal else 'no',
'verify': 'yes' if self.verify_certificate else 'no',
}
# Extend our parameters
params.update(self.url_parameters(privacy=privacy, *args, **kwargs))
# Determine Authentication
auth = ''
if self.user and self.password:
@ -613,13 +619,14 @@ class NotifyEmby(NotifyBase):
user=NotifyEmby.quote(self.user, safe=''),
)
return '{schema}://{auth}{hostname}{port}/?{args}'.format(
return '{schema}://{auth}{hostname}{port}/?{params}'.format(
schema=self.secure_protocol if self.secure else self.protocol,
auth=auth,
hostname=NotifyEmby.quote(self.host, safe=''),
port='' if self.port is None or self.port == self.emby_default_port
hostname=self.host,
port='' if self.port is None
or self.port == self.template_tokens['port']['default']
else ':{}'.format(self.port),
args=NotifyEmby.urlencode(args),
params=NotifyEmby.urlencode(params),
)
@property
@ -655,7 +662,7 @@ class NotifyEmby(NotifyBase):
def parse_url(url):
"""
Parses the URL and returns enough arguments that can allow
us to substantiate this object.
us to re-instantiate this object.
"""
results = NotifyBase.parse_url(url)
@ -663,10 +670,6 @@ class NotifyEmby(NotifyBase):
# We're done early
return results
# Assign Default Emby Port
if not results['port']:
results['port'] = NotifyEmby.emby_default_port
# Modal type popup (default False)
results['modal'] = parse_bool(results['qsd'].get('modal', False))
@ -679,7 +682,7 @@ class NotifyEmby(NotifyBase):
try:
self.logout()
except LookupError:
except LookupError: # pragma: no cover
# Python v3.5 call to requests can sometimes throw the exception
# "/usr/lib64/python3.7/socket.py", line 748, in getaddrinfo
# LookupError: unknown encoding: idna

@ -184,16 +184,16 @@ class NotifyEnigma2(NotifyBase):
Returns the URL built dynamically based on specified arguments.
"""
# Define any arguments set
args = {
'format': self.notify_format,
'overflow': self.overflow_mode,
'verify': 'yes' if self.verify_certificate else 'no',
# Define any URL parameters
params = {
'timeout': str(self.timeout),
}
# Append our headers into our args
args.update({'+{}'.format(k): v for k, v in self.headers.items()})
# Append our headers into our parameters
params.update({'+{}'.format(k): v for k, v in self.headers.items()})
# Extend our parameters
params.update(self.url_parameters(privacy=privacy, *args, **kwargs))
# Determine Authentication
auth = ''
@ -210,14 +210,15 @@ class NotifyEnigma2(NotifyBase):
default_port = 443 if self.secure else 80
return '{schema}://{auth}{hostname}{port}{fullpath}?{args}'.format(
return '{schema}://{auth}{hostname}{port}{fullpath}?{params}'.format(
schema=self.secure_protocol if self.secure else self.protocol,
auth=auth,
hostname=NotifyEnigma2.quote(self.host, safe=''),
# never encode hostname since we're expecting it to be a valid one
hostname=self.host,
port='' if self.port is None or self.port == default_port
else ':{}'.format(self.port),
fullpath=NotifyEnigma2.quote(self.fullpath, safe='/'),
args=NotifyEnigma2.urlencode(args),
params=NotifyEnigma2.urlencode(params),
)
def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
@ -269,6 +270,7 @@ class NotifyEnigma2(NotifyBase):
headers=headers,
auth=auth,
verify=self.verify_certificate,
timeout=self.request_timeout,
)
if r.status_code != requests.codes.ok:
@ -313,7 +315,7 @@ class NotifyEnigma2(NotifyBase):
except requests.RequestException as e:
self.logger.warning(
'A Connection error occured sending Enigma2 '
'A Connection error occurred sending Enigma2 '
'notification to %s.' % self.host)
self.logger.debug('Socket Exception: %s' % str(e))
@ -326,11 +328,10 @@ class NotifyEnigma2(NotifyBase):
def parse_url(url):
"""
Parses the URL and returns enough arguments that can allow
us to substantiate this object.
us to re-instantiate this object.
"""
results = NotifyBase.parse_url(url)
if not results:
# We're done early as we couldn't load the results
return results

@ -29,6 +29,7 @@ from ..common import NotifyImageSize
from ..common import NotifyType
from ..utils import parse_bool
from ..AppriseLocale import gettext_lazy as _
from ..utils import validate_regex
class NotifyFaast(NotifyBase):
@ -86,7 +87,12 @@ class NotifyFaast(NotifyBase):
super(NotifyFaast, self).__init__(**kwargs)
# Store the Authentication Token
self.authtoken = authtoken
self.authtoken = validate_regex(authtoken)
if not self.authtoken:
msg = 'An invalid Faast Authentication Token ' \
'({}) was specified.'.format(authtoken)
self.logger.warning(msg)
raise TypeError(msg)
# Associate an image with our post
self.include_image = include_image
@ -131,6 +137,7 @@ class NotifyFaast(NotifyBase):
data=payload,
headers=headers,
verify=self.verify_certificate,
timeout=self.request_timeout,
)
if r.status_code != requests.codes.ok:
# We had a problem
@ -154,7 +161,7 @@ class NotifyFaast(NotifyBase):
except requests.RequestException as e:
self.logger.warning(
'A Connection error occured sending Faast notification.',
'A Connection error occurred sending Faast notification.',
)
self.logger.debug('Socket Exception: %s' % str(e))
@ -168,29 +175,28 @@ class NotifyFaast(NotifyBase):
Returns the URL built dynamically based on specified arguments.
"""
# Define any arguments set
args = {
'format': self.notify_format,
'overflow': self.overflow_mode,
# Define any URL parameters
params = {
'image': 'yes' if self.include_image else 'no',
'verify': 'yes' if self.verify_certificate else 'no',
}
return '{schema}://{authtoken}/?{args}'.format(
# Extend our parameters
params.update(self.url_parameters(privacy=privacy, *args, **kwargs))
return '{schema}://{authtoken}/?{params}'.format(
schema=self.protocol,
authtoken=self.pprint(self.authtoken, privacy, safe=''),
args=NotifyFaast.urlencode(args),
params=NotifyFaast.urlencode(params),
)
@staticmethod
def parse_url(url):
"""
Parses the URL and returns enough arguments that can allow
us to substantiate this object.
us to re-instantiate this object.
"""
results = NotifyBase.parse_url(url)
results = NotifyBase.parse_url(url, verify_host=False)
if not results:
# We're done early as we couldn't load the results
return results

@ -100,7 +100,7 @@ class NotifyFlock(NotifyBase):
'token': {
'name': _('Access Key'),
'type': 'string',
'regex': (r'^[a-z0-9-]{24}$', 'i'),
'regex': (r'^[a-z0-9-]+$', 'i'),
'private': True,
'required': True,
},
@ -112,14 +112,14 @@ class NotifyFlock(NotifyBase):
'name': _('To User ID'),
'type': 'string',
'prefix': '@',
'regex': (r'^[A-Z0-9_]{12}$', 'i'),
'regex': (r'^[A-Z0-9_]+$', 'i'),
'map_to': 'targets',
},
'to_channel': {
'name': _('To Channel ID'),
'type': 'string',
'prefix': '#',
'regex': (r'^[A-Z0-9_]{12}$', 'i'),
'regex': (r'^[A-Z0-9_]+$', 'i'),
'map_to': 'targets',
},
'targets': {
@ -269,6 +269,7 @@ class NotifyFlock(NotifyBase):
data=dumps(payload),
headers=headers,
verify=self.verify_certificate,
timeout=self.request_timeout,
)
if r.status_code != requests.codes.ok:
# We had a problem
@ -294,7 +295,7 @@ class NotifyFlock(NotifyBase):
except requests.RequestException as e:
self.logger.warning(
'A Connection error occured sending Flock notification.'
'A Connection error occurred sending Flock notification.'
)
self.logger.debug('Socket Exception: %s' % str(e))
@ -308,31 +309,31 @@ class NotifyFlock(NotifyBase):
Returns the URL built dynamically based on specified arguments.
"""
args = {
'format': self.notify_format,
'overflow': self.overflow_mode,
# Define any URL parameters
params = {
'image': 'yes' if self.include_image else 'no',
'verify': 'yes' if self.verify_certificate else 'no',
}
return '{schema}://{token}/{targets}?{args}'\
# Extend our parameters
params.update(self.url_parameters(privacy=privacy, *args, **kwargs))
return '{schema}://{token}/{targets}?{params}'\
.format(
schema=self.secure_protocol,
token=self.pprint(self.token, privacy, safe=''),
targets='/'.join(
[NotifyFlock.quote(target, safe='')
for target in self.targets]),
args=NotifyFlock.urlencode(args),
params=NotifyFlock.urlencode(params),
)
@staticmethod
def parse_url(url):
"""
Parses the URL and returns enough arguments that can allow
us to substantiate this object.
us to re-instantiate this object.
"""
results = NotifyBase.parse_url(url)
results = NotifyBase.parse_url(url, verify_host=False)
if not results:
# We're done early as we couldn't load the results
return results
@ -363,14 +364,14 @@ class NotifyFlock(NotifyBase):
result = re.match(
r'^https?://api\.flock\.com/hooks/sendMessage/'
r'(?P<token>[a-z0-9-]{24})/?'
r'(?P<args>\?.+)?$', url, re.I)
r'(?P<params>\?.+)?$', url, re.I)
if result:
return NotifyFlock.parse_url(
'{schema}://{token}/{args}'.format(
'{schema}://{token}/{params}'.format(
schema=NotifyFlock.secure_protocol,
token=result.group('token'),
args='' if not result.group('args')
else result.group('args')))
params='' if not result.group('params')
else result.group('params')))
return None

@ -309,6 +309,7 @@ class NotifyGitter(NotifyBase):
data=payload,
headers=headers,
verify=self.verify_certificate,
timeout=self.request_timeout,
)
if r.status_code != requests.codes.ok:
@ -366,30 +367,29 @@ class NotifyGitter(NotifyBase):
Returns the URL built dynamically based on specified arguments.
"""
# Define any arguments set
args = {
'format': self.notify_format,
'overflow': self.overflow_mode,
# Define any URL parameters
params = {
'image': 'yes' if self.include_image else 'no',
'verify': 'yes' if self.verify_certificate else 'no',
}
return '{schema}://{token}/{targets}/?{args}'.format(
# Extend our parameters
params.update(self.url_parameters(privacy=privacy, *args, **kwargs))
return '{schema}://{token}/{targets}/?{params}'.format(
schema=self.secure_protocol,
token=self.pprint(self.token, privacy, safe=''),
targets='/'.join(
[NotifyGitter.quote(x, safe='') for x in self.targets]),
args=NotifyGitter.urlencode(args))
params=NotifyGitter.urlencode(params))
@staticmethod
def parse_url(url):
"""
Parses the URL and returns enough arguments that can allow
us to substantiate this object.
us to re-instantiate this object.
"""
results = NotifyBase.parse_url(url)
results = NotifyBase.parse_url(url, verify_host=False)
if not results:
# We're done early as we couldn't load the results
return results

@ -113,7 +113,7 @@ class NotifyGnome(NotifyBase):
# Define object templates
templates = (
'{schema}://_/',
'{schema}://',
)
# Define our template arguments
@ -141,7 +141,7 @@ class NotifyGnome(NotifyBase):
# The urgency of the message
if urgency not in GNOME_URGENCIES:
self.urgency = GnomeUrgency.NORMAL
self.urgency = self.template_args['urgency']['default']
else:
self.urgency = urgency
@ -214,19 +214,19 @@ class NotifyGnome(NotifyBase):
GnomeUrgency.HIGH: 'high',
}
# Define any arguments set
args = {
'format': self.notify_format,
'overflow': self.overflow_mode,
# Define any URL parameters
params = {
'image': 'yes' if self.include_image else 'no',
'urgency': 'normal' if self.urgency not in _map
else _map[self.urgency],
'verify': 'yes' if self.verify_certificate else 'no',
}
return '{schema}://_/?{args}'.format(
# Extend our parameters
params.update(self.url_parameters(privacy=privacy, *args, **kwargs))
return '{schema}://?{params}'.format(
schema=self.protocol,
args=NotifyGnome.urlencode(args),
params=NotifyGnome.urlencode(params),
)
@staticmethod
@ -238,19 +238,7 @@ class NotifyGnome(NotifyBase):
"""
results = NotifyBase.parse_url(url)
if not results:
results = {
'schema': NotifyGnome.protocol,
'user': None,
'password': None,
'port': None,
'host': '_',
'fullpath': None,
'path': None,
'url': url,
'qsd': {},
}
results = NotifyBase.parse_url(url, verify_host=False)
# Include images with our message
results['include_image'] = \

@ -77,10 +77,15 @@ class NotifyGotify(NotifyBase):
# A URL that takes you to the setup/help of the specific protocol
setup_url = 'https://github.com/caronc/apprise/wiki/Notify_gotify'
# Disable throttle rate
request_rate_per_sec = 0
# Define object templates
templates = (
'{schema}://{host}/{token}',
'{schema}://{host}:{port}/{token}',
'{schema}://{host}{path}{token}',
'{schema}://{host}:{port}{path}{token}',
)
# Define our template tokens
@ -96,6 +101,13 @@ class NotifyGotify(NotifyBase):
'type': 'string',
'required': True,
},
'path': {
'name': _('Path'),
'type': 'string',
'map_to': 'fullpath',
'default': '/',
'required': True,
},
'port': {
'name': _('Port'),
'type': 'int',
@ -129,6 +141,9 @@ class NotifyGotify(NotifyBase):
self.logger.warning(msg)
raise TypeError(msg)
# prepare our fullpath
self.fullpath = kwargs.get('fullpath', '/')
if priority not in GOTIFY_PRIORITIES:
self.priority = GotifyPriority.NORMAL
@ -153,7 +168,7 @@ class NotifyGotify(NotifyBase):
url += ':%d' % self.port
# Append our remaining path
url += '/message'
url += '{fullpath}message'.format(fullpath=self.fullpath)
# Define our parameteers
params = {
@ -188,6 +203,7 @@ class NotifyGotify(NotifyBase):
data=dumps(payload),
headers=headers,
verify=self.verify_certificate,
timeout=self.request_timeout,
)
if r.status_code != requests.codes.ok:
# We had a problem
@ -212,7 +228,7 @@ class NotifyGotify(NotifyBase):
except requests.RequestException as e:
self.logger.warning(
'A Connection error occured sending Gotify '
'A Connection error occurred sending Gotify '
'notification to %s.' % self.host)
self.logger.debug('Socket Exception: %s' % str(e))
@ -226,30 +242,33 @@ class NotifyGotify(NotifyBase):
Returns the URL built dynamically based on specified arguments.
"""
# Define any arguments set
args = {
'format': self.notify_format,
'overflow': self.overflow_mode,
# Define any URL parameters
params = {
'priority': self.priority,
'verify': 'yes' if self.verify_certificate else 'no',
}
# Extend our parameters
params.update(self.url_parameters(privacy=privacy, *args, **kwargs))
# Our default port
default_port = 443 if self.secure else 80
return '{schema}://{hostname}{port}/{token}/?{args}'.format(
return '{schema}://{hostname}{port}{fullpath}{token}/?{params}'.format(
schema=self.secure_protocol if self.secure else self.protocol,
hostname=NotifyGotify.quote(self.host, safe=''),
# never encode hostname since we're expecting it to be a valid one
hostname=self.host,
port='' if self.port is None or self.port == default_port
else ':{}'.format(self.port),
fullpath=NotifyGotify.quote(self.fullpath, safe='/'),
token=self.pprint(self.token, privacy, safe=''),
args=NotifyGotify.urlencode(args),
params=NotifyGotify.urlencode(params),
)
@staticmethod
def parse_url(url):
"""
Parses the URL and returns enough arguments that can allow
us to substantiate this object.
us to re-instantiate this object.
"""
results = NotifyBase.parse_url(url)
@ -262,13 +281,17 @@ class NotifyGotify(NotifyBase):
# optionally find the provider key
try:
# The first entry is our token
results['token'] = entries.pop(0)
# The last entry is our token
results['token'] = entries.pop()
except IndexError:
# No token was set
results['token'] = None
# Re-assemble our full path
results['fullpath'] = \
'/' if not entries else '/{}/'.format('/'.join(entries))
if 'priority' in results['qsd'] and len(results['qsd']['priority']):
_map = {
'l': GotifyPriority.LOW,

@ -1,374 +0,0 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2019 Chris Caron <lead2gold@gmail.com>
# All rights reserved.
#
# This code is licensed under the MIT License.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files(the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions :
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
from .gntp import notifier
from .gntp import errors
from ..NotifyBase import NotifyBase
from ...URLBase import PrivacyMode
from ...common import NotifyImageSize
from ...common import NotifyType
from ...utils import parse_bool
from ...AppriseLocale import gettext_lazy as _
# Priorities
class GrowlPriority(object):
LOW = -2
MODERATE = -1
NORMAL = 0
HIGH = 1
EMERGENCY = 2
GROWL_PRIORITIES = (
GrowlPriority.LOW,
GrowlPriority.MODERATE,
GrowlPriority.NORMAL,
GrowlPriority.HIGH,
GrowlPriority.EMERGENCY,
)
GROWL_NOTIFICATION_TYPE = "New Messages"
class NotifyGrowl(NotifyBase):
"""
A wrapper to Growl Notifications
"""
# The default descriptive name associated with the Notification
service_name = 'Growl'
# The services URL
service_url = 'http://growl.info/'
# The default protocol
protocol = 'growl'
# A URL that takes you to the setup/help of the specific protocol
setup_url = 'https://github.com/caronc/apprise/wiki/Notify_growl'
# Allows the user to specify the NotifyImageSize object
image_size = NotifyImageSize.XY_72
# Disable throttle rate for Growl requests since they are normally
# local anyway
request_rate_per_sec = 0
# A title can not be used for Growl Messages. Setting this to zero will
# cause any title (if defined) to get placed into the message body.
title_maxlen = 0
# Limit results to just the first 10 line otherwise there is just to much
# content to display
body_max_line_count = 2
# Default Growl Port
default_port = 23053
# Define object templates
# Define object templates
templates = (
'{schema}://{host}',
'{schema}://{host}:{port}',
'{schema}://{password}@{host}',
'{schema}://{password}@{host}:{port}',
)
# Define our template tokens
template_tokens = dict(NotifyBase.template_tokens, **{
'host': {
'name': _('Hostname'),
'type': 'string',
'required': True,
},
'port': {
'name': _('Port'),
'type': 'int',
'min': 1,
'max': 65535,
},
'password': {
'name': _('Password'),
'type': 'string',
'private': True,
},
})
# Define our template arguments
template_args = dict(NotifyBase.template_args, **{
'priority': {
'name': _('Priority'),
'type': 'choice:int',
'values': GROWL_PRIORITIES,
'default': GrowlPriority.NORMAL,
},
'version': {
'name': _('Version'),
'type': 'choice:int',
'values': (1, 2),
'default': 2,
},
'image': {
'name': _('Include Image'),
'type': 'bool',
'default': True,
'map_to': 'include_image',
},
})
def __init__(self, priority=None, version=2, include_image=True, **kwargs):
"""
Initialize Growl Object
"""
super(NotifyGrowl, self).__init__(**kwargs)
if not self.port:
self.port = self.default_port
# The Priority of the message
if priority not in GROWL_PRIORITIES:
self.priority = GrowlPriority.NORMAL
else:
self.priority = priority
# Always default the sticky flag to False
self.sticky = False
# Store Version
self.version = version
payload = {
'applicationName': self.app_id,
'notifications': [GROWL_NOTIFICATION_TYPE, ],
'defaultNotifications': [GROWL_NOTIFICATION_TYPE, ],
'hostname': self.host,
'port': self.port,
}
if self.password is not None:
payload['password'] = self.password
self.logger.debug('Growl Registration Payload: %s' % str(payload))
self.growl = notifier.GrowlNotifier(**payload)
try:
self.growl.register()
self.logger.debug(
'Growl server registration completed successfully.'
)
except errors.NetworkError:
msg = 'A network error occured sending Growl ' \
'notification to {}.'.format(self.host)
self.logger.warning(msg)
raise TypeError(msg)
except errors.AuthError:
msg = 'An authentication error occured sending Growl ' \
'notification to {}.'.format(self.host)
self.logger.warning(msg)
raise TypeError(msg)
except errors.UnsupportedError:
msg = 'An unsupported error occured sending Growl ' \
'notification to {}.'.format(self.host)
self.logger.warning(msg)
raise TypeError(msg)
# Track whether or not we want to send an image with our notification
# or not.
self.include_image = include_image
return
def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
"""
Perform Growl Notification
"""
icon = None
if self.version >= 2:
# URL Based
icon = None if not self.include_image \
else self.image_url(notify_type)
else:
# Raw
icon = None if not self.include_image \
else self.image_raw(notify_type)
payload = {
'noteType': GROWL_NOTIFICATION_TYPE,
'title': title,
'description': body,
'icon': icon is not None,
'sticky': False,
'priority': self.priority,
}
self.logger.debug('Growl Payload: %s' % str(payload))
# Update icon of payload to be raw data; this is intentionally done
# here after we spit the debug message above (so we don't try to
# print the binary contents of an image
payload['icon'] = icon
# Always call throttle before any remote server i/o is made
self.throttle()
try:
response = self.growl.notify(**payload)
if not isinstance(response, bool):
self.logger.warning(
'Growl notification failed to send with response: %s' %
str(response),
)
else:
self.logger.info('Sent Growl notification.')
except errors.BaseError as e:
# Since Growl servers listen for UDP broadcasts, it's possible
# that you will never get to this part of the code since there is
# no acknowledgement as to whether it accepted what was sent to it
# or not.
# However, if the host/server is unavailable, you will get to this
# point of the code.
self.logger.warning(
'A Connection error occured sending Growl '
'notification to %s.' % self.host)
self.logger.debug('Growl Exception: %s' % str(e))
# Return; we're done
return False
return True
def url(self, privacy=False, *args, **kwargs):
"""
Returns the URL built dynamically based on specified arguments.
"""
_map = {
GrowlPriority.LOW: 'low',
GrowlPriority.MODERATE: 'moderate',
GrowlPriority.NORMAL: 'normal',
GrowlPriority.HIGH: 'high',
GrowlPriority.EMERGENCY: 'emergency',
}
# Define any arguments set
args = {
'format': self.notify_format,
'overflow': self.overflow_mode,
'image': 'yes' if self.include_image else 'no',
'priority':
_map[GrowlPriority.NORMAL] if self.priority not in _map
else _map[self.priority],
'version': self.version,
'verify': 'yes' if self.verify_certificate else 'no',
}
auth = ''
if self.user:
# The growl password is stored in the user field
auth = '{password}@'.format(
password=self.pprint(
self.user, privacy, mode=PrivacyMode.Secret, safe=''),
)
return '{schema}://{auth}{hostname}{port}/?{args}'.format(
schema=self.secure_protocol if self.secure else self.protocol,
auth=auth,
hostname=NotifyGrowl.quote(self.host, safe=''),
port='' if self.port is None or self.port == self.default_port
else ':{}'.format(self.port),
args=NotifyGrowl.urlencode(args),
)
@staticmethod
def parse_url(url):
"""
Parses the URL and returns enough arguments that can allow
us to substantiate this object.
"""
results = NotifyBase.parse_url(url)
if not results:
# We're done early as we couldn't load the results
return results
version = None
if 'version' in results['qsd'] and len(results['qsd']['version']):
# Allow the user to specify the version of the protocol to use.
try:
version = int(
NotifyGrowl.unquote(
results['qsd']['version']).strip().split('.')[0])
except (AttributeError, IndexError, TypeError, ValueError):
NotifyGrowl.logger.warning(
'An invalid Growl version of "%s" was specified and will '
'be ignored.' % results['qsd']['version']
)
pass
if 'priority' in results['qsd'] and len(results['qsd']['priority']):
_map = {
'l': GrowlPriority.LOW,
'm': GrowlPriority.MODERATE,
'n': GrowlPriority.NORMAL,
'h': GrowlPriority.HIGH,
'e': GrowlPriority.EMERGENCY,
}
try:
results['priority'] = \
_map[results['qsd']['priority'][0].lower()]
except KeyError:
# No priority was set
pass
# Because of the URL formatting, the password is actually where the
# username field is. For this reason, we just preform this small hack
# to make it (the URL) conform correctly. The following strips out the
# existing password entry (if exists) so that it can be swapped with
# the new one we specify.
if results.get('password', None) is None:
results['password'] = results.get('user', None)
# Include images with our message
results['include_image'] = \
parse_bool(results['qsd'].get('image', True))
# Set our version
if version:
results['version'] = version
return results

@ -1,141 +0,0 @@
# Copyright: 2013 Paul Traylor
# These sources are released under the terms of the MIT license: see LICENSE
import logging
import os
import sys
from optparse import OptionParser, OptionGroup
from .notifier import GrowlNotifier
from .shim import RawConfigParser
from .version import __version__
DEFAULT_CONFIG = os.path.expanduser('~/.gntp')
config = RawConfigParser({
'hostname': 'localhost',
'password': None,
'port': 23053,
})
config.read([DEFAULT_CONFIG])
if not config.has_section('gntp'):
config.add_section('gntp')
class ClientParser(OptionParser):
def __init__(self):
OptionParser.__init__(self, version="%%prog %s" % __version__)
group = OptionGroup(self, "Network Options")
group.add_option("-H", "--host",
dest="host", default=config.get('gntp', 'hostname'),
help="Specify a hostname to which to send a remote notification. [%default]")
group.add_option("--port",
dest="port", default=config.getint('gntp', 'port'), type="int",
help="port to listen on [%default]")
group.add_option("-P", "--password",
dest='password', default=config.get('gntp', 'password'),
help="Network password")
self.add_option_group(group)
group = OptionGroup(self, "Notification Options")
group.add_option("-n", "--name",
dest="app", default='Python GNTP Test Client',
help="Set the name of the application [%default]")
group.add_option("-s", "--sticky",
dest='sticky', default=False, action="store_true",
help="Make the notification sticky [%default]")
group.add_option("--image",
dest="icon", default=None,
help="Icon for notification (URL or /path/to/file)")
group.add_option("-m", "--message",
dest="message", default=None,
help="Sets the message instead of using stdin")
group.add_option("-p", "--priority",
dest="priority", default=0, type="int",
help="-2 to 2 [%default]")
group.add_option("-d", "--identifier",
dest="identifier",
help="Identifier for coalescing")
group.add_option("-t", "--title",
dest="title", default=None,
help="Set the title of the notification [%default]")
group.add_option("-N", "--notification",
dest="name", default='Notification',
help="Set the notification name [%default]")
group.add_option("--callback",
dest="callback",
help="URL callback")
self.add_option_group(group)
# Extra Options
self.add_option('-v', '--verbose',
dest='verbose', default=0, action='count',
help="Verbosity levels")
def parse_args(self, args=None, values=None):
values, args = OptionParser.parse_args(self, args, values)
if values.message is None:
print('Enter a message followed by Ctrl-D')
try:
message = sys.stdin.read()
except KeyboardInterrupt:
exit()
else:
message = values.message
if values.title is None:
values.title = ' '.join(args)
# If we still have an empty title, use the
# first bit of the message as the title
if values.title == '':
values.title = message[:20]
values.verbose = logging.WARNING - values.verbose * 10
return values, message
def main():
(options, message) = ClientParser().parse_args()
logging.basicConfig(level=options.verbose)
if not os.path.exists(DEFAULT_CONFIG):
logging.info('No config read found at %s', DEFAULT_CONFIG)
growl = GrowlNotifier(
applicationName=options.app,
notifications=[options.name],
defaultNotifications=[options.name],
hostname=options.host,
password=options.password,
port=options.port,
)
result = growl.register()
if result is not True:
exit(result)
# This would likely be better placed within the growl notifier
# class but until I make _checkIcon smarter this is "easier"
if options.icon is not None and not options.icon.startswith('http'):
logging.info('Loading image %s', options.icon)
f = open(options.icon)
options.icon = f.read()
f.close()
result = growl.notify(
noteType=options.name,
title=options.title,
description=message,
icon=options.icon,
sticky=options.sticky,
priority=options.priority,
callback=options.callback,
identifier=options.identifier,
)
if result is not True:
exit(result)
if __name__ == "__main__":
main()

@ -1,77 +0,0 @@
# Copyright: 2013 Paul Traylor
# These sources are released under the terms of the MIT license: see LICENSE
"""
The gntp.config module is provided as an extended GrowlNotifier object that takes
advantage of the ConfigParser module to allow us to setup some default values
(such as hostname, password, and port) in a more global way to be shared among
programs using gntp
"""
import logging
import os
from .gntp import notifier
from .gntp import shim
__all__ = [
'mini',
'GrowlNotifier'
]
logger = logging.getLogger('gntp')
class GrowlNotifier(notifier.GrowlNotifier):
"""
ConfigParser enhanced GrowlNotifier object
For right now, we are only interested in letting users overide certain
values from ~/.gntp
::
[gntp]
hostname = ?
password = ?
port = ?
"""
def __init__(self, *args, **kwargs):
config = shim.RawConfigParser({
'hostname': kwargs.get('hostname', 'localhost'),
'password': kwargs.get('password'),
'port': kwargs.get('port', 23053),
})
config.read([os.path.expanduser('~/.gntp')])
# If the file does not exist, then there will be no gntp section defined
# and the config.get() lines below will get confused. Since we are not
# saving the config, it should be safe to just add it here so the
# code below doesn't complain
if not config.has_section('gntp'):
logger.info('Error reading ~/.gntp config file')
config.add_section('gntp')
kwargs['password'] = config.get('gntp', 'password')
kwargs['hostname'] = config.get('gntp', 'hostname')
kwargs['port'] = config.getint('gntp', 'port')
super(GrowlNotifier, self).__init__(*args, **kwargs)
def mini(description, **kwargs):
"""Single notification function
Simple notification function in one line. Has only one required parameter
and attempts to use reasonable defaults for everything else
:param string description: Notification message
"""
kwargs['notifierFactory'] = GrowlNotifier
notifier.mini(description, **kwargs)
if __name__ == '__main__':
# If we're running this module directly we're likely running it as a test
# so extra debugging is useful
logging.basicConfig(level=logging.INFO)
mini('Testing mini notification')

@ -1,511 +0,0 @@
# Copyright: 2013 Paul Traylor
# These sources are released under the terms of the MIT license: see LICENSE
import hashlib
import re
import time
from . import shim
from . import errors as errors
__all__ = [
'GNTPRegister',
'GNTPNotice',
'GNTPSubscribe',
'GNTPOK',
'GNTPError',
'parse_gntp',
]
#GNTP/<version> <messagetype> <encryptionAlgorithmID>[:<ivValue>][ <keyHashAlgorithmID>:<keyHash>.<salt>]
GNTP_INFO_LINE = re.compile(
r'GNTP/(?P<version>\d+\.\d+) (?P<messagetype>REGISTER|NOTIFY|SUBSCRIBE|\-OK|\-ERROR)' +
r' (?P<encryptionAlgorithmID>[A-Z0-9]+(:(?P<ivValue>[A-F0-9]+))?) ?' +
r'((?P<keyHashAlgorithmID>[A-Z0-9]+):(?P<keyHash>[A-F0-9]+).(?P<salt>[A-F0-9]+))?\r\n',
re.IGNORECASE
)
GNTP_INFO_LINE_SHORT = re.compile(
r'GNTP/(?P<version>\d+\.\d+) (?P<messagetype>REGISTER|NOTIFY|SUBSCRIBE|\-OK|\-ERROR)',
re.IGNORECASE
)
GNTP_HEADER = re.compile(r'([\w-]+):(.+)')
GNTP_EOL = shim.b('\r\n')
GNTP_SEP = shim.b(': ')
class _GNTPBuffer(shim.StringIO):
"""GNTP Buffer class"""
def writeln(self, value=None):
if value:
self.write(shim.b(value))
self.write(GNTP_EOL)
def writeheader(self, key, value):
if not isinstance(value, str):
value = str(value)
self.write(shim.b(key))
self.write(GNTP_SEP)
self.write(shim.b(value))
self.write(GNTP_EOL)
class _GNTPBase(object):
"""Base initilization
:param string messagetype: GNTP Message type
:param string version: GNTP Protocol version
:param string encription: Encryption protocol
"""
def __init__(self, messagetype=None, version='1.0', encryption=None):
self.info = {
'version': version,
'messagetype': messagetype,
'encryptionAlgorithmID': encryption
}
self.hash_algo = {
'MD5': hashlib.md5,
'SHA1': hashlib.sha1,
'SHA256': hashlib.sha256,
'SHA512': hashlib.sha512,
}
self.headers = {}
self.resources = {}
def __str__(self):
return self.encode()
def _parse_info(self, data):
"""Parse the first line of a GNTP message to get security and other info values
:param string data: GNTP Message
:return dict: Parsed GNTP Info line
"""
match = GNTP_INFO_LINE.match(data)
if not match:
raise errors.ParseError('ERROR_PARSING_INFO_LINE')
info = match.groupdict()
if info['encryptionAlgorithmID'] == 'NONE':
info['encryptionAlgorithmID'] = None
return info
def set_password(self, password, encryptAlgo='MD5'):
"""Set a password for a GNTP Message
:param string password: Null to clear password
:param string encryptAlgo: Supports MD5, SHA1, SHA256, SHA512
"""
if not password:
self.info['encryptionAlgorithmID'] = None
self.info['keyHashAlgorithm'] = None
return
self.password = shim.b(password)
self.encryptAlgo = encryptAlgo.upper()
if not self.encryptAlgo in self.hash_algo:
raise errors.UnsupportedError('INVALID HASH "%s"' % self.encryptAlgo)
hashfunction = self.hash_algo.get(self.encryptAlgo)
password = password.encode('utf8')
seed = time.ctime().encode('utf8')
salt = hashfunction(seed).hexdigest()
saltHash = hashfunction(seed).digest()
keyBasis = password + saltHash
key = hashfunction(keyBasis).digest()
keyHash = hashfunction(key).hexdigest()
self.info['keyHashAlgorithmID'] = self.encryptAlgo
self.info['keyHash'] = keyHash.upper()
self.info['salt'] = salt.upper()
def _decode_hex(self, value):
"""Helper function to decode hex string to `proper` hex string
:param string value: Human readable hex string
:return string: Hex string
"""
result = ''
for i in range(0, len(value), 2):
tmp = int(value[i:i + 2], 16)
result += chr(tmp)
return result
def _decode_binary(self, rawIdentifier, identifier):
rawIdentifier += '\r\n\r\n'
dataLength = int(identifier['Length'])
pointerStart = self.raw.find(rawIdentifier) + len(rawIdentifier)
pointerEnd = pointerStart + dataLength
data = self.raw[pointerStart:pointerEnd]
if not len(data) == dataLength:
raise errors.ParseError('INVALID_DATA_LENGTH Expected: %s Recieved %s' % (dataLength, len(data)))
return data
def _validate_password(self, password):
"""Validate GNTP Message against stored password"""
self.password = password
if password is None:
raise errors.AuthError('Missing password')
keyHash = self.info.get('keyHash', None)
if keyHash is None and self.password is None:
return True
if keyHash is None:
raise errors.AuthError('Invalid keyHash')
if self.password is None:
raise errors.AuthError('Missing password')
keyHashAlgorithmID = self.info.get('keyHashAlgorithmID','MD5')
password = self.password.encode('utf8')
saltHash = self._decode_hex(self.info['salt'])
keyBasis = password + saltHash
self.key = self.hash_algo[keyHashAlgorithmID](keyBasis).digest()
keyHash = self.hash_algo[keyHashAlgorithmID](self.key).hexdigest()
if not keyHash.upper() == self.info['keyHash'].upper():
raise errors.AuthError('Invalid Hash')
return True
def validate(self):
"""Verify required headers"""
for header in self._requiredHeaders:
if not self.headers.get(header, False):
raise errors.ParseError('Missing Notification Header: ' + header)
def _format_info(self):
"""Generate info line for GNTP Message
:return string:
"""
info = 'GNTP/%s %s' % (
self.info.get('version'),
self.info.get('messagetype'),
)
if self.info.get('encryptionAlgorithmID', None):
info += ' %s:%s' % (
self.info.get('encryptionAlgorithmID'),
self.info.get('ivValue'),
)
else:
info += ' NONE'
if self.info.get('keyHashAlgorithmID', None):
info += ' %s:%s.%s' % (
self.info.get('keyHashAlgorithmID'),
self.info.get('keyHash'),
self.info.get('salt')
)
return info
def _parse_dict(self, data):
"""Helper function to parse blocks of GNTP headers into a dictionary
:param string data:
:return dict: Dictionary of parsed GNTP Headers
"""
d = {}
for line in data.split('\r\n'):
match = GNTP_HEADER.match(line)
if not match:
continue
key = match.group(1).strip()
val = match.group(2).strip()
d[key] = val
return d
def add_header(self, key, value):
self.headers[key] = value
def add_resource(self, data):
"""Add binary resource
:param string data: Binary Data
"""
data = shim.b(data)
identifier = hashlib.md5(data).hexdigest()
self.resources[identifier] = data
return 'x-growl-resource://%s' % identifier
def decode(self, data, password=None):
"""Decode GNTP Message
:param string data:
"""
self.password = password
self.raw = shim.u(data)
parts = self.raw.split('\r\n\r\n')
self.info = self._parse_info(self.raw)
self.headers = self._parse_dict(parts[0])
def encode(self):
"""Encode a generic GNTP Message
:return string: GNTP Message ready to be sent. Returned as a byte string
"""
buff = _GNTPBuffer()
buff.writeln(self._format_info())
#Headers
for k, v in self.headers.items():
buff.writeheader(k, v)
buff.writeln()
#Resources
for resource, data in self.resources.items():
buff.writeheader('Identifier', resource)
buff.writeheader('Length', len(data))
buff.writeln()
buff.write(data)
buff.writeln()
buff.writeln()
return buff.getvalue()
class GNTPRegister(_GNTPBase):
"""Represents a GNTP Registration Command
:param string data: (Optional) See decode()
:param string password: (Optional) Password to use while encoding/decoding messages
"""
_requiredHeaders = [
'Application-Name',
'Notifications-Count'
]
_requiredNotificationHeaders = ['Notification-Name']
def __init__(self, data=None, password=None):
_GNTPBase.__init__(self, 'REGISTER')
self.notifications = []
if data:
self.decode(data, password)
else:
self.set_password(password)
self.add_header('Application-Name', 'pygntp')
self.add_header('Notifications-Count', 0)
def validate(self):
'''Validate required headers and validate notification headers'''
for header in self._requiredHeaders:
if not self.headers.get(header, False):
raise errors.ParseError('Missing Registration Header: ' + header)
for notice in self.notifications:
for header in self._requiredNotificationHeaders:
if not notice.get(header, False):
raise errors.ParseError('Missing Notification Header: ' + header)
def decode(self, data, password):
"""Decode existing GNTP Registration message
:param string data: Message to decode
"""
self.raw = shim.u(data)
parts = self.raw.split('\r\n\r\n')
self.info = self._parse_info(self.raw)
self._validate_password(password)
self.headers = self._parse_dict(parts[0])
for i, part in enumerate(parts):
if i == 0:
continue # Skip Header
if part.strip() == '':
continue
notice = self._parse_dict(part)
if notice.get('Notification-Name', False):
self.notifications.append(notice)
elif notice.get('Identifier', False):
notice['Data'] = self._decode_binary(part, notice)
#open('register.png','wblol').write(notice['Data'])
self.resources[notice.get('Identifier')] = notice
def add_notification(self, name, enabled=True):
"""Add new Notification to Registration message
:param string name: Notification Name
:param boolean enabled: Enable this notification by default
"""
notice = {}
notice['Notification-Name'] = name
notice['Notification-Enabled'] = enabled
self.notifications.append(notice)
self.add_header('Notifications-Count', len(self.notifications))
def encode(self):
"""Encode a GNTP Registration Message
:return string: Encoded GNTP Registration message. Returned as a byte string
"""
buff = _GNTPBuffer()
buff.writeln(self._format_info())
#Headers
for k, v in self.headers.items():
buff.writeheader(k, v)
buff.writeln()
#Notifications
if len(self.notifications) > 0:
for notice in self.notifications:
for k, v in notice.items():
buff.writeheader(k, v)
buff.writeln()
#Resources
for resource, data in self.resources.items():
buff.writeheader('Identifier', resource)
buff.writeheader('Length', len(data))
buff.writeln()
buff.write(data)
buff.writeln()
buff.writeln()
return buff.getvalue()
class GNTPNotice(_GNTPBase):
"""Represents a GNTP Notification Command
:param string data: (Optional) See decode()
:param string app: (Optional) Set Application-Name
:param string name: (Optional) Set Notification-Name
:param string title: (Optional) Set Notification Title
:param string password: (Optional) Password to use while encoding/decoding messages
"""
_requiredHeaders = [
'Application-Name',
'Notification-Name',
'Notification-Title'
]
def __init__(self, data=None, app=None, name=None, title=None, password=None):
_GNTPBase.__init__(self, 'NOTIFY')
if data:
self.decode(data, password)
else:
self.set_password(password)
if app:
self.add_header('Application-Name', app)
if name:
self.add_header('Notification-Name', name)
if title:
self.add_header('Notification-Title', title)
def decode(self, data, password):
"""Decode existing GNTP Notification message
:param string data: Message to decode.
"""
self.raw = shim.u(data)
parts = self.raw.split('\r\n\r\n')
self.info = self._parse_info(self.raw)
self._validate_password(password)
self.headers = self._parse_dict(parts[0])
for i, part in enumerate(parts):
if i == 0:
continue # Skip Header
if part.strip() == '':
continue
notice = self._parse_dict(part)
if notice.get('Identifier', False):
notice['Data'] = self._decode_binary(part, notice)
#open('notice.png','wblol').write(notice['Data'])
self.resources[notice.get('Identifier')] = notice
class GNTPSubscribe(_GNTPBase):
"""Represents a GNTP Subscribe Command
:param string data: (Optional) See decode()
:param string password: (Optional) Password to use while encoding/decoding messages
"""
_requiredHeaders = [
'Subscriber-ID',
'Subscriber-Name',
]
def __init__(self, data=None, password=None):
_GNTPBase.__init__(self, 'SUBSCRIBE')
if data:
self.decode(data, password)
else:
self.set_password(password)
class GNTPOK(_GNTPBase):
"""Represents a GNTP OK Response
:param string data: (Optional) See _GNTPResponse.decode()
:param string action: (Optional) Set type of action the OK Response is for
"""
_requiredHeaders = ['Response-Action']
def __init__(self, data=None, action=None):
_GNTPBase.__init__(self, '-OK')
if data:
self.decode(data)
if action:
self.add_header('Response-Action', action)
class GNTPError(_GNTPBase):
"""Represents a GNTP Error response
:param string data: (Optional) See _GNTPResponse.decode()
:param string errorcode: (Optional) Error code
:param string errordesc: (Optional) Error Description
"""
_requiredHeaders = ['Error-Code', 'Error-Description']
def __init__(self, data=None, errorcode=None, errordesc=None):
_GNTPBase.__init__(self, '-ERROR')
if data:
self.decode(data)
if errorcode:
self.add_header('Error-Code', errorcode)
self.add_header('Error-Description', errordesc)
def error(self):
return (self.headers.get('Error-Code', None),
self.headers.get('Error-Description', None))
def parse_gntp(data, password=None):
"""Attempt to parse a message as a GNTP message
:param string data: Message to be parsed
:param string password: Optional password to be used to verify the message
"""
data = shim.u(data)
match = GNTP_INFO_LINE_SHORT.match(data)
if not match:
raise errors.ParseError('INVALID_GNTP_INFO')
info = match.groupdict()
if info['messagetype'] == 'REGISTER':
return GNTPRegister(data, password=password)
elif info['messagetype'] == 'NOTIFY':
return GNTPNotice(data, password=password)
elif info['messagetype'] == 'SUBSCRIBE':
return GNTPSubscribe(data, password=password)
elif info['messagetype'] == '-OK':
return GNTPOK(data)
elif info['messagetype'] == '-ERROR':
return GNTPError(data)
raise errors.ParseError('INVALID_GNTP_MESSAGE')

@ -1,25 +0,0 @@
# Copyright: 2013 Paul Traylor
# These sources are released under the terms of the MIT license: see LICENSE
class BaseError(Exception):
pass
class ParseError(BaseError):
errorcode = 500
errordesc = 'Error parsing the message'
class AuthError(BaseError):
errorcode = 400
errordesc = 'Error with authorization'
class UnsupportedError(BaseError):
errorcode = 500
errordesc = 'Currently unsupported by gntp.py'
class NetworkError(BaseError):
errorcode = 500
errordesc = "Error connecting to growl server"

@ -1,265 +0,0 @@
# Copyright: 2013 Paul Traylor
# These sources are released under the terms of the MIT license: see LICENSE
"""
The gntp.notifier module is provided as a simple way to send notifications
using GNTP
.. note::
This class is intended to mostly mirror the older Python bindings such
that you should be able to replace instances of the old bindings with
this class.
`Original Python bindings <http://code.google.com/p/growl/source/browse/Bindings/python/Growl.py>`_
"""
import logging
import platform
import socket
import sys
from .version import __version__
from . import core
from . import errors as errors
from . import shim
__all__ = [
'mini',
'GrowlNotifier',
]
logger = logging.getLogger('gntp')
class GrowlNotifier(object):
"""Helper class to simplfy sending Growl messages
:param string applicationName: Sending application name
:param list notification: List of valid notifications
:param list defaultNotifications: List of notifications that should be enabled
by default
:param string applicationIcon: Icon URL
:param string hostname: Remote host
:param integer port: Remote port
"""
passwordHash = 'MD5'
socketTimeout = 3
def __init__(self, applicationName='Python GNTP', notifications=[],
defaultNotifications=None, applicationIcon=None, hostname='localhost',
password=None, port=23053):
self.applicationName = applicationName
self.notifications = list(notifications)
if defaultNotifications:
self.defaultNotifications = list(defaultNotifications)
else:
self.defaultNotifications = self.notifications
self.applicationIcon = applicationIcon
self.password = password
self.hostname = hostname
self.port = int(port)
def _checkIcon(self, data):
'''
Check the icon to see if it's valid
If it's a simple URL icon, then we return True. If it's a data icon
then we return False
'''
logger.info('Checking icon')
return shim.u(data).startswith('http')
def register(self):
"""Send GNTP Registration
.. warning::
Before sending notifications to Growl, you need to have
sent a registration message at least once
"""
logger.info('Sending registration to %s:%s', self.hostname, self.port)
register = core.GNTPRegister()
register.add_header('Application-Name', self.applicationName)
for notification in self.notifications:
enabled = notification in self.defaultNotifications
register.add_notification(notification, enabled)
if self.applicationIcon:
if self._checkIcon(self.applicationIcon):
register.add_header('Application-Icon', self.applicationIcon)
else:
resource = register.add_resource(self.applicationIcon)
register.add_header('Application-Icon', resource)
if self.password:
register.set_password(self.password, self.passwordHash)
self.add_origin_info(register)
self.register_hook(register)
return self._send('register', register)
def notify(self, noteType, title, description, icon=None, sticky=False,
priority=None, callback=None, identifier=None, custom={}):
"""Send a GNTP notifications
.. warning::
Must have registered with growl beforehand or messages will be ignored
:param string noteType: One of the notification names registered earlier
:param string title: Notification title (usually displayed on the notification)
:param string description: The main content of the notification
:param string icon: Icon URL path
:param boolean sticky: Sticky notification
:param integer priority: Message priority level from -2 to 2
:param string callback: URL callback
:param dict custom: Custom attributes. Key names should be prefixed with X-
according to the spec but this is not enforced by this class
.. warning::
For now, only URL callbacks are supported. In the future, the
callback argument will also support a function
"""
logger.info('Sending notification [%s] to %s:%s', noteType, self.hostname, self.port)
assert noteType in self.notifications
notice = core.GNTPNotice()
notice.add_header('Application-Name', self.applicationName)
notice.add_header('Notification-Name', noteType)
notice.add_header('Notification-Title', title)
if self.password:
notice.set_password(self.password, self.passwordHash)
if sticky:
notice.add_header('Notification-Sticky', sticky)
if priority:
notice.add_header('Notification-Priority', priority)
if icon:
if self._checkIcon(icon):
notice.add_header('Notification-Icon', icon)
else:
resource = notice.add_resource(icon)
notice.add_header('Notification-Icon', resource)
if description:
notice.add_header('Notification-Text', description)
if callback:
notice.add_header('Notification-Callback-Target', callback)
if identifier:
notice.add_header('Notification-Coalescing-ID', identifier)
for key in custom:
notice.add_header(key, custom[key])
self.add_origin_info(notice)
self.notify_hook(notice)
return self._send('notify', notice)
def subscribe(self, id, name, port):
"""Send a Subscribe request to a remote machine"""
sub = core.GNTPSubscribe()
sub.add_header('Subscriber-ID', id)
sub.add_header('Subscriber-Name', name)
sub.add_header('Subscriber-Port', port)
if self.password:
sub.set_password(self.password, self.passwordHash)
self.add_origin_info(sub)
self.subscribe_hook(sub)
return self._send('subscribe', sub)
def add_origin_info(self, packet):
"""Add optional Origin headers to message"""
packet.add_header('Origin-Machine-Name', platform.node())
packet.add_header('Origin-Software-Name', 'gntp.py')
packet.add_header('Origin-Software-Version', __version__)
packet.add_header('Origin-Platform-Name', platform.system())
packet.add_header('Origin-Platform-Version', platform.platform())
def register_hook(self, packet):
pass
def notify_hook(self, packet):
pass
def subscribe_hook(self, packet):
pass
def _send(self, messagetype, packet):
"""Send the GNTP Packet"""
packet.validate()
data = packet.encode()
logger.debug('To : %s:%s <%s>\n%s', self.hostname, self.port, packet.__class__, data)
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.settimeout(self.socketTimeout)
try:
s.connect((self.hostname, self.port))
s.send(data)
recv_data = s.recv(1024)
while not recv_data.endswith(shim.b("\r\n\r\n")):
recv_data += s.recv(1024)
except socket.error:
# Python2.5 and Python3 compatibile exception
exc = sys.exc_info()[1]
raise errors.NetworkError(exc)
response = core.parse_gntp(recv_data)
s.close()
logger.debug('From : %s:%s <%s>\n%s', self.hostname, self.port, response.__class__, response)
if type(response) == core.GNTPOK:
return True
logger.error('Invalid response: %s', response.error())
return response.error()
def mini(description, applicationName='PythonMini', noteType="Message",
title="Mini Message", applicationIcon=None, hostname='localhost',
password=None, port=23053, sticky=False, priority=None,
callback=None, notificationIcon=None, identifier=None,
notifierFactory=GrowlNotifier):
"""Single notification function
Simple notification function in one line. Has only one required parameter
and attempts to use reasonable defaults for everything else
:param string description: Notification message
.. warning::
For now, only URL callbacks are supported. In the future, the
callback argument will also support a function
"""
try:
growl = notifierFactory(
applicationName=applicationName,
notifications=[noteType],
defaultNotifications=[noteType],
applicationIcon=applicationIcon,
hostname=hostname,
password=password,
port=port,
)
result = growl.register()
if result is not True:
return result
return growl.notify(
noteType=noteType,
title=title,
description=description,
icon=notificationIcon,
sticky=sticky,
priority=priority,
callback=callback,
identifier=identifier,
)
except Exception:
# We want the "mini" function to be simple and swallow Exceptions
# in order to be less invasive
logger.exception("Growl error")
if __name__ == '__main__':
# If we're running this module directly we're likely running it as a test
# so extra debugging is useful
logging.basicConfig(level=logging.INFO)
mini('Testing mini notification')

@ -1,45 +0,0 @@
# Copyright: 2013 Paul Traylor
# These sources are released under the terms of the MIT license: see LICENSE
"""
Python2.5 and Python3.3 compatibility shim
Heavily inspirted by the "six" library.
https://pypi.python.org/pypi/six
"""
import sys
PY3 = sys.version_info[0] == 3
if PY3:
def b(s):
if isinstance(s, bytes):
return s
return s.encode('utf8', 'replace')
def u(s):
if isinstance(s, bytes):
return s.decode('utf8', 'replace')
return s
from io import BytesIO as StringIO
from configparser import RawConfigParser
else:
def b(s):
if isinstance(s, unicode): # noqa
return s.encode('utf8', 'replace')
return s
def u(s):
if isinstance(s, unicode): # noqa
return s
if isinstance(s, int):
s = str(s)
return unicode(s, "utf8", "replace") # noqa
from StringIO import StringIO
from ConfigParser import RawConfigParser
b.__doc__ = "Ensure we have a byte string"
u.__doc__ = "Ensure we have a unicode string"

@ -1,4 +0,0 @@
# Copyright: 2013 Paul Traylor
# These sources are released under the terms of the MIT license: see LICENSE
__version__ = '1.0.2'

@ -242,6 +242,7 @@ class NotifyIFTTT(NotifyBase):
data=dumps(payload),
headers=headers,
verify=self.verify_certificate,
timeout=self.request_timeout,
)
self.logger.debug(
u"IFTTT HTTP response headers: %r" % r.headers)
@ -274,7 +275,7 @@ class NotifyIFTTT(NotifyBase):
except requests.RequestException as e:
self.logger.warning(
'A Connection error occured sending IFTTT:%s ' % (
'A Connection error occurred sending IFTTT:%s ' % (
event) + 'notification.'
)
self.logger.debug('Socket Exception: %s' % str(e))
@ -290,34 +291,29 @@ class NotifyIFTTT(NotifyBase):
Returns the URL built dynamically based on specified arguments.
"""
# Define any arguments set
args = {
'format': self.notify_format,
'overflow': self.overflow_mode,
'verify': 'yes' if self.verify_certificate else 'no',
}
# Our URL parameters
params = self.url_parameters(privacy=privacy, *args, **kwargs)
# Store any new key/value pairs added to our list
args.update({'+{}'.format(k): v for k, v in self.add_tokens})
args.update({'-{}'.format(k): '' for k in self.del_tokens})
params.update({'+{}'.format(k): v for k, v in self.add_tokens})
params.update({'-{}'.format(k): '' for k in self.del_tokens})
return '{schema}://{webhook_id}@{events}/?{args}'.format(
return '{schema}://{webhook_id}@{events}/?{params}'.format(
schema=self.secure_protocol,
webhook_id=self.pprint(self.webhook_id, privacy, safe=''),
events='/'.join([NotifyIFTTT.quote(x, safe='')
for x in self.events]),
args=NotifyIFTTT.urlencode(args),
params=NotifyIFTTT.urlencode(params),
)
@staticmethod
def parse_url(url):
"""
Parses the URL and returns enough arguments that can allow
us to substantiate this object.
us to re-instantiate this object.
"""
results = NotifyBase.parse_url(url)
results = NotifyBase.parse_url(url, verify_host=False)
if not results:
# We're done early as we couldn't load the results
return results
@ -356,16 +352,16 @@ class NotifyIFTTT(NotifyBase):
r'^https?://maker\.ifttt\.com/use/'
r'(?P<webhook_id>[A-Z0-9_-]+)'
r'/?(?P<events>([A-Z0-9_-]+/?)+)?'
r'/?(?P<args>\?.+)?$', url, re.I)
r'/?(?P<params>\?.+)?$', url, re.I)
if result:
return NotifyIFTTT.parse_url(
'{schema}://{webhook_id}{events}{args}'.format(
'{schema}://{webhook_id}{events}{params}'.format(
schema=NotifyIFTTT.secure_protocol,
webhook_id=result.group('webhook_id'),
events='' if not result.group('events')
else '@{}'.format(result.group('events')),
args='' if not result.group('args')
else result.group('args')))
params='' if not result.group('params')
else result.group('params')))
return None

@ -128,15 +128,11 @@ class NotifyJSON(NotifyBase):
Returns the URL built dynamically based on specified arguments.
"""
# Define any arguments set
args = {
'format': self.notify_format,
'overflow': self.overflow_mode,
'verify': 'yes' if self.verify_certificate else 'no',
}
# Our URL parameters
params = self.url_parameters(privacy=privacy, *args, **kwargs)
# Append our headers into our args
args.update({'+{}'.format(k): v for k, v in self.headers.items()})
# Append our headers into our parameters
params.update({'+{}'.format(k): v for k, v in self.headers.items()})
# Determine Authentication
auth = ''
@ -153,14 +149,15 @@ class NotifyJSON(NotifyBase):
default_port = 443 if self.secure else 80
return '{schema}://{auth}{hostname}{port}{fullpath}/?{args}'.format(
return '{schema}://{auth}{hostname}{port}{fullpath}/?{params}'.format(
schema=self.secure_protocol if self.secure else self.protocol,
auth=auth,
hostname=NotifyJSON.quote(self.host, safe=''),
# never encode hostname since we're expecting it to be a valid one
hostname=self.host,
port='' if self.port is None or self.port == default_port
else ':{}'.format(self.port),
fullpath=NotifyJSON.quote(self.fullpath, safe='/'),
args=NotifyJSON.urlencode(args),
params=NotifyJSON.urlencode(params),
)
def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
@ -215,6 +212,7 @@ class NotifyJSON(NotifyBase):
headers=headers,
auth=auth,
verify=self.verify_certificate,
timeout=self.request_timeout,
)
if r.status_code != requests.codes.ok:
# We had a problem
@ -238,7 +236,7 @@ class NotifyJSON(NotifyBase):
except requests.RequestException as e:
self.logger.warning(
'A Connection error occured sending JSON '
'A Connection error occurred sending JSON '
'notification to %s.' % self.host)
self.logger.debug('Socket Exception: %s' % str(e))
@ -251,11 +249,10 @@ class NotifyJSON(NotifyBase):
def parse_url(url):
"""
Parses the URL and returns enough arguments that can allow
us to substantiate this object.
us to re-instantiate this object.
"""
results = NotifyBase.parse_url(url)
if not results:
# We're done early as we couldn't load the results
return results

@ -280,6 +280,7 @@ class NotifyJoin(NotifyBase):
data=payload,
headers=headers,
verify=self.verify_certificate,
timeout=self.request_timeout,
)
if r.status_code != requests.codes.ok:
@ -308,7 +309,7 @@ class NotifyJoin(NotifyBase):
except requests.RequestException as e:
self.logger.warning(
'A Connection error occured sending Join:%s '
'A Connection error occurred sending Join:%s '
'notification.' % target
)
self.logger.debug('Socket Exception: %s' % str(e))
@ -331,33 +332,32 @@ class NotifyJoin(NotifyBase):
JoinPriority.EMERGENCY: 'emergency',
}
# Define any arguments set
args = {
'format': self.notify_format,
'overflow': self.overflow_mode,
# Define any URL parameters
params = {
'priority':
_map[self.template_args['priority']['default']]
if self.priority not in _map else _map[self.priority],
'image': 'yes' if self.include_image else 'no',
'verify': 'yes' if self.verify_certificate else 'no',
}
return '{schema}://{apikey}/{targets}/?{args}'.format(
# Extend our parameters
params.update(self.url_parameters(privacy=privacy, *args, **kwargs))
return '{schema}://{apikey}/{targets}/?{params}'.format(
schema=self.secure_protocol,
apikey=self.pprint(self.apikey, privacy, safe=''),
targets='/'.join([NotifyJoin.quote(x, safe='')
for x in self.targets]),
args=NotifyJoin.urlencode(args))
params=NotifyJoin.urlencode(params))
@staticmethod
def parse_url(url):
"""
Parses the URL and returns enough arguments that can allow
us to substantiate this object.
us to re-instantiate this object.
"""
results = NotifyBase.parse_url(url)
results = NotifyBase.parse_url(url, verify_host=False)
if not results:
# We're done early as we couldn't load the results
return results

@ -263,6 +263,7 @@ class NotifyKavenegar(NotifyBase):
params=payload,
headers=headers,
verify=self.verify_certificate,
timeout=self.request_timeout,
)
if r.status_code not in (
@ -310,7 +311,7 @@ class NotifyKavenegar(NotifyBase):
except requests.RequestException as e:
self.logger.warning(
'A Connection error occured sending Kavenegar:%s ' % (
'A Connection error occurred sending Kavenegar:%s ' % (
', '.join(self.targets)) + 'notification.'
)
self.logger.debug('Socket Exception: %s' % str(e))
@ -325,30 +326,25 @@ class NotifyKavenegar(NotifyBase):
Returns the URL built dynamically based on specified arguments.
"""
# Define any arguments set
args = {
'format': self.notify_format,
'overflow': self.overflow_mode,
'verify': 'yes' if self.verify_certificate else 'no',
}
# Our URL parameters
params = self.url_parameters(privacy=privacy, *args, **kwargs)
return '{schema}://{source}{apikey}/{targets}?{args}'.format(
return '{schema}://{source}{apikey}/{targets}?{params}'.format(
schema=self.secure_protocol,
source='' if not self.source else '{}@'.format(self.source),
apikey=self.pprint(self.apikey, privacy, safe=''),
targets='/'.join(
[NotifyKavenegar.quote(x, safe='') for x in self.targets]),
args=NotifyKavenegar.urlencode(args))
params=NotifyKavenegar.urlencode(params))
@staticmethod
def parse_url(url):
"""
Parses the URL and returns enough arguments that can allow
us to substantiate this object.
us to re-instantiate this object.
"""
results = NotifyBase.parse_url(url, verify_host=False)
if not results:
# We're done early as we couldn't load the results
return results

@ -163,6 +163,7 @@ class NotifyKumulos(NotifyBase):
headers=headers,
auth=auth,
verify=self.verify_certificate,
timeout=self.request_timeout,
)
if r.status_code != requests.codes.ok:
# We had a problem
@ -187,7 +188,7 @@ class NotifyKumulos(NotifyBase):
except requests.RequestException as e:
self.logger.warning(
'A Connection error occured sending Kumulos '
'A Connection error occurred sending Kumulos '
'notification.')
self.logger.debug('Socket Exception: %s' % str(e))
@ -199,29 +200,24 @@ class NotifyKumulos(NotifyBase):
Returns the URL built dynamically based on specified arguments.
"""
# Define any arguments set
args = {
'format': self.notify_format,
'overflow': self.overflow_mode,
'verify': 'yes' if self.verify_certificate else 'no',
}
# Our URL parameters
params = self.url_parameters(privacy=privacy, *args, **kwargs)
return '{schema}://{apikey}/{serverkey}/?{args}'.format(
return '{schema}://{apikey}/{serverkey}/?{params}'.format(
schema=self.secure_protocol,
apikey=self.pprint(self.apikey, privacy, safe=''),
serverkey=self.pprint(self.serverkey, privacy, safe=''),
args=NotifyKumulos.urlencode(args),
params=NotifyKumulos.urlencode(params),
)
@staticmethod
def parse_url(url):
"""
Parses the URL and returns enough arguments that can allow
us to substantiate this object.
us to re-instantiate this object.
"""
results = NotifyBase.parse_url(url)
results = NotifyBase.parse_url(url, verify_host=False)
if not results:
# We're done early as we couldn't load the results
return results

@ -276,6 +276,7 @@ class NotifyMSG91(NotifyBase):
data=payload,
headers=headers,
verify=self.verify_certificate,
timeout=self.request_timeout,
)
if r.status_code != requests.codes.ok:
@ -302,7 +303,7 @@ class NotifyMSG91(NotifyBase):
except requests.RequestException as e:
self.logger.warning(
'A Connection error occured sending MSG91:%s '
'A Connection error occurred sending MSG91:%s '
'notification.' % ','.join(self.targets)
)
self.logger.debug('Socket Exception: %s' % str(e))
@ -316,34 +317,33 @@ class NotifyMSG91(NotifyBase):
Returns the URL built dynamically based on specified arguments.
"""
# Define any arguments set
args = {
'format': self.notify_format,
'overflow': self.overflow_mode,
'verify': 'yes' if self.verify_certificate else 'no',
# Define any URL parameters
params = {
'route': str(self.route),
}
# Extend our parameters
params.update(self.url_parameters(privacy=privacy, *args, **kwargs))
if self.country:
args['country'] = str(self.country)
params['country'] = str(self.country)
return '{schema}://{authkey}/{targets}/?{args}'.format(
return '{schema}://{authkey}/{targets}/?{params}'.format(
schema=self.secure_protocol,
authkey=self.pprint(self.authkey, privacy, safe=''),
targets='/'.join(
[NotifyMSG91.quote(x, safe='') for x in self.targets]),
args=NotifyMSG91.urlencode(args))
params=NotifyMSG91.urlencode(params))
@staticmethod
def parse_url(url):
"""
Parses the URL and returns enough arguments that can allow
us to substantiate this object.
us to re-instantiate this object.
"""
results = NotifyBase.parse_url(url)
results = NotifyBase.parse_url(url, verify_host=False)
if not results:
# We're done early as we couldn't load the results
return results

@ -240,6 +240,7 @@ class NotifyMSTeams(NotifyBase):
data=dumps(payload),
headers=headers,
verify=self.verify_certificate,
timeout=self.request_timeout,
)
if r.status_code != requests.codes.ok:
# We had a problem
@ -264,7 +265,7 @@ class NotifyMSTeams(NotifyBase):
except requests.RequestException as e:
self.logger.warning(
'A Connection error occured sending MSTeams notification.')
'A Connection error occurred sending MSTeams notification.')
self.logger.debug('Socket Exception: %s' % str(e))
# We failed
@ -277,32 +278,31 @@ class NotifyMSTeams(NotifyBase):
Returns the URL built dynamically based on specified arguments.
"""
# Define any arguments set
args = {
'format': self.notify_format,
'overflow': self.overflow_mode,
# Define any URL parameters
params = {
'image': 'yes' if self.include_image else 'no',
'verify': 'yes' if self.verify_certificate else 'no',
}
# Extend our parameters
params.update(self.url_parameters(privacy=privacy, *args, **kwargs))
return '{schema}://{token_a}/{token_b}/{token_c}/'\
'?{args}'.format(
'?{params}'.format(
schema=self.secure_protocol,
token_a=self.pprint(self.token_a, privacy, safe=''),
token_b=self.pprint(self.token_b, privacy, safe=''),
token_c=self.pprint(self.token_c, privacy, safe=''),
args=NotifyMSTeams.urlencode(args),
params=NotifyMSTeams.urlencode(params),
)
@staticmethod
def parse_url(url):
"""
Parses the URL and returns enough arguments that can allow
us to substantiate this object.
us to re-instantiate this object.
"""
results = NotifyBase.parse_url(url, verify_host=False)
if not results:
# We're done early as we couldn't load the results
return results
@ -359,16 +359,16 @@ class NotifyMSTeams(NotifyBase):
r'IncomingWebhook/'
r'(?P<token_b>[A-Z0-9]+)/'
r'(?P<token_c>[A-Z0-9-]+)/?'
r'(?P<args>\?.+)?$', url, re.I)
r'(?P<params>\?.+)?$', url, re.I)
if result:
return NotifyMSTeams.parse_url(
'{schema}://{token_a}/{token_b}/{token_c}/{args}'.format(
'{schema}://{token_a}/{token_b}/{token_c}/{params}'.format(
schema=NotifyMSTeams.secure_protocol,
token_a=result.group('token_a'),
token_b=result.group('token_b'),
token_c=result.group('token_c'),
args='' if not result.group('args')
else result.group('args')))
params='' if not result.group('params')
else result.group('params')))
return None

@ -269,6 +269,7 @@ class NotifyMailgun(NotifyBase):
data=payload,
headers=headers,
verify=self.verify_certificate,
timeout=self.request_timeout,
)
if r.status_code != requests.codes.ok:
@ -298,7 +299,7 @@ class NotifyMailgun(NotifyBase):
except requests.RequestException as e:
self.logger.warning(
'A Connection error occured sending Mailgun:%s ' % (
'A Connection error occurred sending Mailgun:%s ' % (
email) + 'notification.'
)
self.logger.debug('Socket Exception: %s' % str(e))
@ -314,36 +315,35 @@ class NotifyMailgun(NotifyBase):
Returns the URL built dynamically based on specified arguments.
"""
# Define any arguments set
args = {
'format': self.notify_format,
'overflow': self.overflow_mode,
'verify': 'yes' if self.verify_certificate else 'no',
# Define any URL parameters
params = {
'region': self.region_name,
}
# Extend our parameters
params.update(self.url_parameters(privacy=privacy, *args, **kwargs))
if self.from_name is not None:
# from_name specified; pass it back on the url
args['name'] = self.from_name
params['name'] = self.from_name
return '{schema}://{user}@{host}/{apikey}/{targets}/?{args}'.format(
return '{schema}://{user}@{host}/{apikey}/{targets}/?{params}'.format(
schema=self.secure_protocol,
host=self.host,
user=NotifyMailgun.quote(self.user, safe=''),
apikey=self.pprint(self.apikey, privacy, safe=''),
targets='/'.join(
[NotifyMailgun.quote(x, safe='') for x in self.targets]),
args=NotifyMailgun.urlencode(args))
params=NotifyMailgun.urlencode(params))
@staticmethod
def parse_url(url):
"""
Parses the URL and returns enough arguments that can allow
us to substantiate this object.
us to re-instantiate this object.
"""
results = NotifyBase.parse_url(url)
results = NotifyBase.parse_url(url, verify_host=False)
if not results:
# We're done early as we couldn't load the results
return results

@ -319,6 +319,7 @@ class NotifyMatrix(NotifyBase):
data=dumps(payload),
headers=headers,
verify=self.verify_certificate,
timeout=self.request_timeout,
)
if r.status_code != requests.codes.ok:
# We had a problem
@ -343,7 +344,7 @@ class NotifyMatrix(NotifyBase):
except requests.RequestException as e:
self.logger.warning(
'A Connection error occured sending Matrix notification.'
'A Connection error occurred sending Matrix notification.'
)
self.logger.debug('Socket Exception: %s' % str(e))
# Return; we're done
@ -927,6 +928,7 @@ class NotifyMatrix(NotifyBase):
params=params,
headers=headers,
verify=self.verify_certificate,
timeout=self.request_timeout,
)
response = loads(r.content)
@ -986,7 +988,7 @@ class NotifyMatrix(NotifyBase):
except requests.RequestException as e:
self.logger.warning(
'A Connection error occured while registering with Matrix'
'A Connection error occurred while registering with Matrix'
' server.')
self.logger.debug('Socket Exception: %s' % str(e))
# Return; we're done
@ -1009,15 +1011,15 @@ class NotifyMatrix(NotifyBase):
Returns the URL built dynamically based on specified arguments.
"""
# Define any arguments set
args = {
'format': self.notify_format,
'overflow': self.overflow_mode,
# Define any URL parameters
params = {
'image': 'yes' if self.include_image else 'no',
'verify': 'yes' if self.verify_certificate else 'no',
'mode': self.mode,
}
# Extend our parameters
params.update(self.url_parameters(privacy=privacy, *args, **kwargs))
# Determine Authentication
auth = ''
if self.user and self.password:
@ -1034,21 +1036,21 @@ class NotifyMatrix(NotifyBase):
default_port = 443 if self.secure else 80
return '{schema}://{auth}{hostname}{port}/{rooms}?{args}'.format(
return '{schema}://{auth}{hostname}{port}/{rooms}?{params}'.format(
schema=self.secure_protocol if self.secure else self.protocol,
auth=auth,
hostname=NotifyMatrix.quote(self.host, safe=''),
port='' if self.port is None
or self.port == default_port else ':{}'.format(self.port),
rooms=NotifyMatrix.quote('/'.join(self.rooms)),
args=NotifyMatrix.urlencode(args),
params=NotifyMatrix.urlencode(params),
)
@staticmethod
def parse_url(url):
"""
Parses the URL and returns enough arguments that can allow
us to substantiate this object.
us to re-instantiate this object.
"""
results = NotifyBase.parse_url(url, verify_host=False)
@ -1067,34 +1069,12 @@ class NotifyMatrix(NotifyBase):
if 'to' in results['qsd'] and len(results['qsd']['to']):
results['targets'] += NotifyMatrix.parse_list(results['qsd']['to'])
# Thumbnail (old way)
if 'thumbnail' in results['qsd']:
# Deprication Notice issued for v0.7.5
NotifyMatrix.logger.deprecate(
'The Matrix URL contains the parameter '
'"thumbnail=" which will be deprecated in an upcoming '
'release. Please use "image=" instead.'
)
# use image= for consistency with the other plugins but we also
# support thumbnail= for backwards compatibility.
results['include_image'] = \
parse_bool(results['qsd'].get(
'image', results['qsd'].get('thumbnail', False)))
# Webhook (old way)
if 'webhook' in results['qsd']:
# Deprication Notice issued for v0.7.5
NotifyMatrix.logger.deprecate(
'The Matrix URL contains the parameter '
'"webhook=" which will be deprecated in an upcoming '
'release. Please use "mode=" instead.'
)
# Boolean to include an image or not
results['include_image'] = parse_bool(results['qsd'].get(
'image', NotifyMatrix.template_args['image']['default']))
# use mode= for consistency with the other plugins but we also
# support webhook= for backwards compatibility.
results['mode'] = results['qsd'].get(
'mode', results['qsd'].get('webhook'))
# Get our mode
results['mode'] = results['qsd'].get('mode')
# t2bot detection... look for just a hostname, and/or just a user/host
# if we match this; we can go ahead and set the mode (but only if
@ -1117,16 +1097,16 @@ class NotifyMatrix(NotifyBase):
result = re.match(
r'^https?://webhooks\.t2bot\.io/api/v1/matrix/hook/'
r'(?P<webhook_token>[A-Z0-9_-]+)/?'
r'(?P<args>\?.+)?$', url, re.I)
r'(?P<params>\?.+)?$', url, re.I)
if result:
mode = 'mode={}'.format(MatrixWebhookMode.T2BOT)
return NotifyMatrix.parse_url(
'{schema}://{webhook_token}/{args}'.format(
'{schema}://{webhook_token}/{params}'.format(
schema=NotifyMatrix.secure_protocol,
webhook_token=result.group('webhook_token'),
args='?{}'.format(mode) if not result.group('args')
else '{}&{}'.format(result.group('args'), mode)))
params='?{}'.format(mode) if not result.group('params')
else '{}&{}'.format(result.group('params'), mode)))
return None

@ -227,6 +227,7 @@ class NotifyMatterMost(NotifyBase):
data=dumps(payload),
headers=headers,
verify=self.verify_certificate,
timeout=self.request_timeout,
)
if r.status_code != requests.codes.ok:
@ -259,7 +260,7 @@ class NotifyMatterMost(NotifyBase):
except requests.RequestException as e:
self.logger.warning(
'A Connection error occured sending MatterMost '
'A Connection error occurred sending MatterMost '
'notification{}.'.format(
'' if not channel
else ' to channel {}'.format(channel)))
@ -277,19 +278,19 @@ class NotifyMatterMost(NotifyBase):
Returns the URL built dynamically based on specified arguments.
"""
# Define any arguments set
args = {
'format': self.notify_format,
'overflow': self.overflow_mode,
# Define any URL parameters
params = {
'image': 'yes' if self.include_image else 'no',
'verify': 'yes' if self.verify_certificate else 'no',
}
# Extend our parameters
params.update(self.url_parameters(privacy=privacy, *args, **kwargs))
if self.channels:
# historically the value only accepted one channel and is
# therefore identified as 'channel'. Channels have always been
# optional, so that is why this setting is nested in an if block
args['channel'] = ','.join(self.channels)
params['channel'] = ','.join(self.channels)
default_port = 443 if self.secure else self.default_port
default_schema = self.secure_protocol if self.secure else self.protocol
@ -303,27 +304,28 @@ class NotifyMatterMost(NotifyBase):
return \
'{schema}://{botname}{hostname}{port}{fullpath}{authtoken}' \
'/?{args}'.format(
'/?{params}'.format(
schema=default_schema,
botname=botname,
hostname=NotifyMatterMost.quote(self.host, safe=''),
# never encode hostname since we're expecting it to be a valid
# one
hostname=self.host,
port='' if not self.port or self.port == default_port
else ':{}'.format(self.port),
fullpath='/' if not self.fullpath else '{}/'.format(
NotifyMatterMost.quote(self.fullpath, safe='/')),
authtoken=self.pprint(self.authtoken, privacy, safe=''),
args=NotifyMatterMost.urlencode(args),
params=NotifyMatterMost.urlencode(params),
)
@staticmethod
def parse_url(url):
"""
Parses the URL and returns enough arguments that can allow
us to substantiate this object.
us to re-instantiate this object.
"""
results = NotifyBase.parse_url(url)
if not results:
# We're done early as we couldn't load the results
return results

@ -234,6 +234,7 @@ class NotifyMessageBird(NotifyBase):
data=payload,
headers=headers,
verify=self.verify_certificate,
timeout=self.request_timeout,
)
# Sample output of a successful transmission
@ -297,7 +298,7 @@ class NotifyMessageBird(NotifyBase):
except requests.RequestException as e:
self.logger.warning(
'A Connection error occured sending MessageBird:%s ' % (
'A Connection error occurred sending MessageBird:%s ' % (
target) + 'notification.'
)
self.logger.debug('Socket Exception: %s' % str(e))
@ -313,31 +314,26 @@ class NotifyMessageBird(NotifyBase):
Returns the URL built dynamically based on specified arguments.
"""
# Define any arguments set
args = {
'format': self.notify_format,
'overflow': self.overflow_mode,
'verify': 'yes' if self.verify_certificate else 'no',
}
# Our URL parameters
params = self.url_parameters(privacy=privacy, *args, **kwargs)
return '{schema}://{apikey}/{source}/{targets}/?{args}'.format(
return '{schema}://{apikey}/{source}/{targets}/?{params}'.format(
schema=self.secure_protocol,
apikey=self.pprint(self.apikey, privacy, safe=''),
source=self.source,
targets='/'.join(
[NotifyMessageBird.quote(x, safe='') for x in self.targets]),
args=NotifyMessageBird.urlencode(args))
params=NotifyMessageBird.urlencode(params))
@staticmethod
def parse_url(url):
"""
Parses the URL and returns enough arguments that can allow
us to substantiate this object.
us to re-instantiate this object.
"""
results = NotifyBase.parse_url(url)
results = NotifyBase.parse_url(url, verify_host=False)
if not results:
# We're done early as we couldn't load the results
return results
@ -352,7 +348,7 @@ class NotifyMessageBird(NotifyBase):
except IndexError:
# No path specified... this URL is potentially un-parseable; we can
# hope for a from= entry
pass
results['source'] = None
# The hostname is our authentication key
results['apikey'] = NotifyMessageBird.unquote(results['host'])

@ -82,7 +82,7 @@ class NotifyNexmo(NotifyBase):
'name': _('API Key'),
'type': 'string',
'required': True,
'regex': (r'^AC[a-z0-9]{8}$', 'i'),
'regex': (r'^[a-z0-9]+$', 'i'),
'private': True,
},
'secret': {
@ -90,7 +90,7 @@ class NotifyNexmo(NotifyBase):
'type': 'string',
'private': True,
'required': True,
'regex': (r'^[a-z0-9]{16}$', 'i'),
'regex': (r'^[a-z0-9]+$', 'i'),
},
'from_phone': {
'name': _('From Phone No'),
@ -280,6 +280,7 @@ class NotifyNexmo(NotifyBase):
data=payload,
headers=headers,
verify=self.verify_certificate,
timeout=self.request_timeout,
)
if r.status_code != requests.codes.ok:
@ -308,7 +309,7 @@ class NotifyNexmo(NotifyBase):
except requests.RequestException as e:
self.logger.warning(
'A Connection error occured sending Nexmo:%s '
'A Connection error occurred sending Nexmo:%s '
'notification.' % target
)
self.logger.debug('Socket Exception: %s' % str(e))
@ -324,15 +325,15 @@ class NotifyNexmo(NotifyBase):
Returns the URL built dynamically based on specified arguments.
"""
# Define any arguments set
args = {
'format': self.notify_format,
'overflow': self.overflow_mode,
'verify': 'yes' if self.verify_certificate else 'no',
# Define any URL parameters
params = {
'ttl': str(self.ttl),
}
return '{schema}://{key}:{secret}@{source}/{targets}/?{args}'.format(
# Extend our parameters
params.update(self.url_parameters(privacy=privacy, *args, **kwargs))
return '{schema}://{key}:{secret}@{source}/{targets}/?{params}'.format(
schema=self.secure_protocol,
key=self.pprint(self.apikey, privacy, safe=''),
secret=self.pprint(
@ -340,17 +341,16 @@ class NotifyNexmo(NotifyBase):
source=NotifyNexmo.quote(self.source, safe=''),
targets='/'.join(
[NotifyNexmo.quote(x, safe='') for x in self.targets]),
args=NotifyNexmo.urlencode(args))
params=NotifyNexmo.urlencode(params))
@staticmethod
def parse_url(url):
"""
Parses the URL and returns enough arguments that can allow
us to substantiate this object.
us to re-instantiate this object.
"""
results = NotifyBase.parse_url(url, verify_host=False)
if not results:
# We're done early as we couldn't load the results
return results

@ -185,6 +185,7 @@ class NotifyNextcloud(NotifyBase):
headers=headers,
auth=auth,
verify=self.verify_certificate,
timeout=self.request_timeout,
)
if r.status_code != requests.codes.ok:
# We had a problem
@ -210,7 +211,7 @@ class NotifyNextcloud(NotifyBase):
except requests.RequestException as e:
self.logger.warning(
'A Connection error occured sending Nextcloud '
'A Connection error occurred sending Nextcloud '
'notification.',
)
self.logger.debug('Socket Exception: %s' % str(e))
@ -226,15 +227,11 @@ class NotifyNextcloud(NotifyBase):
Returns the URL built dynamically based on specified arguments.
"""
# Define any arguments set
args = {
'format': self.notify_format,
'overflow': self.overflow_mode,
'verify': 'yes' if self.verify_certificate else 'no',
}
# Create URL parameters from our headers
params = {'+{}'.format(k): v for k, v in self.headers.items()}
# Append our headers into our args
args.update({'+{}'.format(k): v for k, v in self.headers.items()})
# Our URL parameters
params = self.url_parameters(privacy=privacy, *args, **kwargs)
# Determine Authentication
auth = ''
@ -251,24 +248,26 @@ class NotifyNextcloud(NotifyBase):
default_port = 443 if self.secure else 80
return '{schema}://{auth}{hostname}{port}/{targets}?{args}' \
return '{schema}://{auth}{hostname}{port}/{targets}?{params}' \
.format(
schema=self.secure_protocol
if self.secure else self.protocol,
auth=auth,
hostname=NotifyNextcloud.quote(self.host, safe=''),
# never encode hostname since we're expecting it to be a
# valid one
hostname=self.host,
port='' if self.port is None or self.port == default_port
else ':{}'.format(self.port),
targets='/'.join([NotifyNextcloud.quote(x)
for x in self.targets]),
args=NotifyNextcloud.urlencode(args),
params=NotifyNextcloud.urlencode(params),
)
@staticmethod
def parse_url(url):
"""
Parses the URL and returns enough arguments that can allow
us to substantiate this object.
us to re-instantiate this object.
"""

@ -103,6 +103,14 @@ class NotifyNotica(NotifyBase):
'{schema}://{user}@{host}:{port}/{token}',
'{schema}://{user}:{password}@{host}/{token}',
'{schema}://{user}:{password}@{host}:{port}/{token}',
# Self-hosted notica servers (with custom path)
'{schema}://{host}{path}{token}',
'{schema}://{host}:{port}{path}{token}',
'{schema}://{user}@{host}{path}{token}',
'{schema}://{user}@{host}:{port}{path}{token}',
'{schema}://{user}:{password}@{host}{path}{token}',
'{schema}://{user}:{password}@{host}:{port}{path}{token}',
)
# Define our template tokens
@ -133,6 +141,12 @@ class NotifyNotica(NotifyBase):
'type': 'string',
'private': True,
},
'path': {
'name': _('Path'),
'type': 'string',
'map_to': 'fullpath',
'default': '/',
},
})
# Define any kwargs we're using
@ -228,6 +242,7 @@ class NotifyNotica(NotifyBase):
headers=headers,
auth=auth,
verify=self.verify_certificate,
timeout=self.request_timeout,
)
if r.status_code != requests.codes.ok:
# We had a problem
@ -251,7 +266,7 @@ class NotifyNotica(NotifyBase):
except requests.RequestException as e:
self.logger.warning(
'A Connection error occured sending Notica notification.',
'A Connection error occurred sending Notica notification.',
)
self.logger.debug('Socket Exception: %s' % str(e))
@ -265,25 +280,21 @@ class NotifyNotica(NotifyBase):
Returns the URL built dynamically based on specified arguments.
"""
# Define any arguments set
args = {
'format': self.notify_format,
'overflow': self.overflow_mode,
'verify': 'yes' if self.verify_certificate else 'no',
}
# Our URL parameters
params = self.url_parameters(privacy=privacy, *args, **kwargs)
if self.mode == NoticaMode.OFFICIAL:
# Official URLs are easy to assemble
return '{schema}://{token}/?{args}'.format(
return '{schema}://{token}/?{params}'.format(
schema=self.protocol,
token=self.pprint(self.token, privacy, safe=''),
args=NotifyNotica.urlencode(args),
params=NotifyNotica.urlencode(params),
)
# If we reach here then we are assembling a self hosted URL
# Append our headers into our args
args.update({'+{}'.format(k): v for k, v in self.headers.items()})
# Append URL parameters from our headers
params.update({'+{}'.format(k): v for k, v in self.headers.items()})
# Authorization can be used for self-hosted sollutions
auth = ''
@ -302,7 +313,7 @@ class NotifyNotica(NotifyBase):
default_port = 443 if self.secure else 80
return '{schema}://{auth}{hostname}{port}{fullpath}{token}/?{args}' \
return '{schema}://{auth}{hostname}{port}{fullpath}{token}/?{params}' \
.format(
schema=self.secure_protocol
if self.secure else self.protocol,
@ -313,14 +324,14 @@ class NotifyNotica(NotifyBase):
fullpath=NotifyNotica.quote(
self.fullpath, safe='/'),
token=self.pprint(self.token, privacy, safe=''),
args=NotifyNotica.urlencode(args),
params=NotifyNotica.urlencode(params),
)
@staticmethod
def parse_url(url):
"""
Parses the URL and returns enough arguments that can allow
us to substantiate this object.
us to re-instantiate this object.
"""
results = NotifyBase.parse_url(url, verify_host=False)
@ -367,14 +378,14 @@ class NotifyNotica(NotifyBase):
result = re.match(
r'^https?://notica\.us/?'
r'\??(?P<token>[^&]+)([&\s]*(?P<args>.+))?$', url, re.I)
r'\??(?P<token>[^&]+)([&\s]*(?P<params>.+))?$', url, re.I)
if result:
return NotifyNotica.parse_url(
'{schema}://{token}/{args}'.format(
'{schema}://{token}/{params}'.format(
schema=NotifyNotica.protocol,
token=result.group('token'),
args='' if not result.group('args')
else '?{}'.format(result.group('args'))))
params='' if not result.group('params')
else '?{}'.format(result.group('params'))))
return None

@ -199,20 +199,20 @@ class NotifyNotifico(NotifyBase):
Returns the URL built dynamically based on specified arguments.
"""
# Define any arguments set
args = {
'format': self.notify_format,
'overflow': self.overflow_mode,
'verify': 'yes' if self.verify_certificate else 'no',
# Define any URL parameters
params = {
'color': 'yes' if self.color else 'no',
'prefix': 'yes' if self.prefix else 'no',
}
return '{schema}://{proj}/{hook}/?{args}'.format(
# Extend our parameters
params.update(self.url_parameters(privacy=privacy, *args, **kwargs))
return '{schema}://{proj}/{hook}/?{params}'.format(
schema=self.secure_protocol,
proj=self.pprint(self.project_id, privacy, safe=''),
hook=self.pprint(self.msghook, privacy, safe=''),
args=NotifyNotifico.urlencode(args),
params=NotifyNotifico.urlencode(params),
)
def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
@ -288,6 +288,7 @@ class NotifyNotifico(NotifyBase):
params=payload,
headers=headers,
verify=self.verify_certificate,
timeout=self.request_timeout,
)
if r.status_code != requests.codes.ok:
# We had a problem
@ -311,7 +312,7 @@ class NotifyNotifico(NotifyBase):
except requests.RequestException as e:
self.logger.warning(
'A Connection error occured sending Notifico '
'A Connection error occurred sending Notifico '
'notification.')
self.logger.debug('Socket Exception: %s' % str(e))
@ -324,11 +325,11 @@ class NotifyNotifico(NotifyBase):
def parse_url(url):
"""
Parses the URL and returns enough arguments that can allow
us to substantiate this object.
us to re-instantiate this object.
"""
results = NotifyBase.parse_url(url)
results = NotifyBase.parse_url(url, verify_host=False)
if not results:
# We're done early as we couldn't load the results
return results
@ -364,15 +365,15 @@ class NotifyNotifico(NotifyBase):
r'^https?://n\.tkte\.ch/h/'
r'(?P<proj>[0-9]+)/'
r'(?P<hook>[A-Z0-9]+)/?'
r'(?P<args>\?.+)?$', url, re.I)
r'(?P<params>\?.+)?$', url, re.I)
if result:
return NotifyNotifico.parse_url(
'{schema}://{proj}/{hook}/{args}'.format(
'{schema}://{proj}/{hook}/{params}'.format(
schema=NotifyNotifico.secure_protocol,
proj=result.group('proj'),
hook=result.group('hook'),
args='' if not result.group('args')
else result.group('args')))
params='' if not result.group('params')
else result.group('params')))
return None

@ -191,6 +191,7 @@ class NotifyProwl(NotifyBase):
data=payload,
headers=headers,
verify=self.verify_certificate,
timeout=self.request_timeout,
)
if r.status_code != requests.codes.ok:
# We had a problem
@ -215,7 +216,7 @@ class NotifyProwl(NotifyBase):
except requests.RequestException as e:
self.logger.warning(
'A Connection error occured sending Prowl notification.')
'A Connection error occurred sending Prowl notification.')
self.logger.debug('Socket Exception: %s' % str(e))
# Return; we're done
@ -236,31 +237,30 @@ class NotifyProwl(NotifyBase):
ProwlPriority.EMERGENCY: 'emergency',
}
# Define any arguments set
args = {
'format': self.notify_format,
'overflow': self.overflow_mode,
# Define any URL parameters
params = {
'priority': 'normal' if self.priority not in _map
else _map[self.priority],
'verify': 'yes' if self.verify_certificate else 'no',
}
return '{schema}://{apikey}/{providerkey}/?{args}'.format(
# Extend our parameters
params.update(self.url_parameters(privacy=privacy, *args, **kwargs))
return '{schema}://{apikey}/{providerkey}/?{params}'.format(
schema=self.secure_protocol,
apikey=self.pprint(self.apikey, privacy, safe=''),
providerkey=self.pprint(self.providerkey, privacy, safe=''),
args=NotifyProwl.urlencode(args),
params=NotifyProwl.urlencode(params),
)
@staticmethod
def parse_url(url):
"""
Parses the URL and returns enough arguments that can allow
us to substantiate this object.
us to re-instantiate this object.
"""
results = NotifyBase.parse_url(url)
results = NotifyBase.parse_url(url, verify_host=False)
if not results:
# We're done early as we couldn't load the results
return results

@ -28,7 +28,7 @@ from json import dumps
from json import loads
from .NotifyBase import NotifyBase
from ..utils import GET_EMAIL_RE
from ..utils import is_email
from ..common import NotifyType
from ..utils import parse_list
from ..utils import validate_regex
@ -230,22 +230,29 @@ class NotifyPushBullet(NotifyBase):
'body': body,
}
if recipient is PUSHBULLET_SEND_TO_ALL:
# Check if an email was defined
match = is_email(recipient)
if match:
payload['email'] = match['full_email']
self.logger.debug(
"PushBullet recipient {} parsed as an email address"
.format(recipient))
elif recipient is PUSHBULLET_SEND_TO_ALL:
# Send to all
pass
elif GET_EMAIL_RE.match(recipient):
payload['email'] = recipient
self.logger.debug(
"Recipient '%s' is an email address" % recipient)
elif recipient[0] == '#':
payload['channel_tag'] = recipient[1:]
self.logger.debug("Recipient '%s' is a channel" % recipient)
self.logger.debug(
"PushBullet recipient {} parsed as a channel"
.format(recipient))
else:
payload['device_iden'] = recipient
self.logger.debug("Recipient '%s' is a device" % recipient)
self.logger.debug(
"PushBullet recipient {} parsed as a device"
.format(recipient))
okay, response = self._send(
self.notify_url.format('pushes'), payload)
@ -315,6 +322,7 @@ class NotifyPushBullet(NotifyBase):
files=files,
auth=auth,
verify=self.verify_certificate,
timeout=self.request_timeout,
)
try:
@ -352,14 +360,14 @@ class NotifyPushBullet(NotifyBase):
except requests.RequestException as e:
self.logger.warning(
'A Connection error occured communicating with PushBullet.')
'A Connection error occurred communicating with PushBullet.')
self.logger.debug('Socket Exception: %s' % str(e))
return False, response
except (OSError, IOError) as e:
self.logger.warning(
'An I/O error occured while reading {}.'.format(
'An I/O error occurred while reading {}.'.format(
payload.name if payload else 'attachment'))
self.logger.debug('I/O Exception: %s' % str(e))
return False, response
@ -375,12 +383,8 @@ class NotifyPushBullet(NotifyBase):
Returns the URL built dynamically based on specified arguments.
"""
# Define any arguments set
args = {
'format': self.notify_format,
'overflow': self.overflow_mode,
'verify': 'yes' if self.verify_certificate else 'no',
}
# Our URL parameters
params = self.url_parameters(privacy=privacy, *args, **kwargs)
targets = '/'.join([NotifyPushBullet.quote(x) for x in self.targets])
if targets == PUSHBULLET_SEND_TO_ALL:
@ -388,21 +392,20 @@ class NotifyPushBullet(NotifyBase):
# it from the recipients list
targets = ''
return '{schema}://{accesstoken}/{targets}/?{args}'.format(
return '{schema}://{accesstoken}/{targets}/?{params}'.format(
schema=self.secure_protocol,
accesstoken=self.pprint(self.accesstoken, privacy, safe=''),
targets=targets,
args=NotifyPushBullet.urlencode(args))
params=NotifyPushBullet.urlencode(params))
@staticmethod
def parse_url(url):
"""
Parses the URL and returns enough arguments that can allow
us to substantiate this object.
us to re-instantiate this object.
"""
results = NotifyBase.parse_url(url)
results = NotifyBase.parse_url(url, verify_host=False)
if not results:
# We're done early as we couldn't load the results
return results

@ -576,7 +576,7 @@ class NotifyPushSafer(NotifyBase):
except (OSError, IOError) as e:
self.logger.warning(
'An I/O error occured while reading {}.'.format(
'An I/O error occurred while reading {}.'.format(
attachment.name if attachment else 'attachment'))
self.logger.debug('I/O Exception: %s' % str(e))
return False
@ -693,6 +693,7 @@ class NotifyPushSafer(NotifyBase):
data=payload,
headers=headers,
verify=self.verify_certificate,
timeout=self.request_timeout,
)
try:
@ -746,7 +747,7 @@ class NotifyPushSafer(NotifyBase):
except requests.RequestException as e:
self.logger.warning(
'A Connection error occured communicating with PushSafer.')
'A Connection error occurred communicating with PushSafer.')
self.logger.debug('Socket Exception: %s' % str(e))
return False, response
@ -756,29 +757,25 @@ class NotifyPushSafer(NotifyBase):
Returns the URL built dynamically based on specified arguments.
"""
# Define any arguments set
args = {
'format': self.notify_format,
'overflow': self.overflow_mode,
'verify': 'yes' if self.verify_certificate else 'no',
}
# Our URL parameters
params = self.url_parameters(privacy=privacy, *args, **kwargs)
if self.priority is not None:
# Store our priority; but only if it was specified
args['priority'] = \
params['priority'] = \
next((key for key, value in PUSHSAFER_PRIORITY_MAP.items()
if value == self.priority),
DEFAULT_PRIORITY) # pragma: no cover
if self.sound is not None:
# Store our sound; but only if it was specified
args['sound'] = \
params['sound'] = \
next((key for key, value in PUSHSAFER_SOUND_MAP.items()
if value == self.sound), '') # pragma: no cover
if self.vibration is not None:
# Store our vibration; but only if it was specified
args['vibration'] = str(self.vibration)
params['vibration'] = str(self.vibration)
targets = '/'.join([NotifyPushSafer.quote(x) for x in self.targets])
if targets == PUSHSAFER_SEND_TO_ALL:
@ -786,20 +783,20 @@ class NotifyPushSafer(NotifyBase):
# it from the recipients list
targets = ''
return '{schema}://{privatekey}/{targets}?{args}'.format(
return '{schema}://{privatekey}/{targets}?{params}'.format(
schema=self.secure_protocol if self.secure else self.protocol,
privatekey=self.pprint(self.privatekey, privacy, safe=''),
targets=targets,
args=NotifyPushSafer.urlencode(args))
params=NotifyPushSafer.urlencode(params))
@staticmethod
def parse_url(url):
"""
Parses the URL and returns enough arguments that can allow
us to substantiate this object.
us to re-instantiate this object.
"""
results = NotifyBase.parse_url(url)
results = NotifyBase.parse_url(url, verify_host=False)
if not results:
# We're done early as we couldn't load the results
return results

@ -267,6 +267,7 @@ class NotifyPushed(NotifyBase):
data=dumps(payload),
headers=headers,
verify=self.verify_certificate,
timeout=self.request_timeout,
)
if r.status_code != requests.codes.ok:
@ -291,7 +292,7 @@ class NotifyPushed(NotifyBase):
except requests.RequestException as e:
self.logger.warning(
'A Connection error occured sending Pushed notification.')
'A Connection error occurred sending Pushed notification.')
self.logger.debug('Socket Exception: %s' % str(e))
# Return; we're done
@ -304,14 +305,10 @@ class NotifyPushed(NotifyBase):
Returns the URL built dynamically based on specified arguments.
"""
# Define any arguments set
args = {
'format': self.notify_format,
'overflow': self.overflow_mode,
'verify': 'yes' if self.verify_certificate else 'no',
}
# Our URL parameters
params = self.url_parameters(privacy=privacy, *args, **kwargs)
return '{schema}://{app_key}/{app_secret}/{targets}/?{args}'.format(
return '{schema}://{app_key}/{app_secret}/{targets}/?{params}'.format(
schema=self.secure_protocol,
app_key=self.pprint(self.app_key, privacy, safe=''),
app_secret=self.pprint(
@ -323,17 +320,16 @@ class NotifyPushed(NotifyBase):
# Users are prefixed with an @ symbol
['@{}'.format(x) for x in self.users],
)]),
args=NotifyPushed.urlencode(args))
params=NotifyPushed.urlencode(params))
@staticmethod
def parse_url(url):
"""
Parses the URL and returns enough arguments that can allow
us to substantiate this object.
us to re-instantiate this object.
"""
results = NotifyBase.parse_url(url)
results = NotifyBase.parse_url(url, verify_host=False)
if not results:
# We're done early as we couldn't load the results
return results

@ -60,10 +60,6 @@ class NotifyPushjet(NotifyBase):
'{schema}://{host}/{secret_key}',
'{schema}://{user}:{password}@{host}:{port}/{secret_key}',
'{schema}://{user}:{password}@{host}/{secret_key}',
# Kept for backwards compatibility; will be depricated eventually
'{schema}://{secret_key}@{host}',
'{schema}://{secret_key}@{host}:{port}',
)
# Define our tokens
@ -123,12 +119,8 @@ class NotifyPushjet(NotifyBase):
Returns the URL built dynamically based on specified arguments.
"""
# Define any arguments set
args = {
'format': self.notify_format,
'overflow': self.overflow_mode,
'verify': 'yes' if self.verify_certificate else 'no',
}
# Our URL parameters
params = self.url_parameters(privacy=privacy, *args, **kwargs)
default_port = 443 if self.secure else 80
@ -141,15 +133,16 @@ class NotifyPushjet(NotifyBase):
self.password, privacy, mode=PrivacyMode.Secret, safe=''),
)
return '{schema}://{auth}{hostname}{port}/{secret}/?{args}'.format(
return '{schema}://{auth}{hostname}{port}/{secret}/?{params}'.format(
schema=self.secure_protocol if self.secure else self.protocol,
auth=auth,
hostname=NotifyPushjet.quote(self.host, safe=''),
# never encode hostname since we're expecting it to be a valid one
hostname=self.host,
port='' if self.port is None or self.port == default_port
else ':{}'.format(self.port),
secret=self.pprint(
self.secret_key, privacy, mode=PrivacyMode.Secret, safe=''),
args=NotifyPushjet.urlencode(args),
params=NotifyPushjet.urlencode(params),
)
def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
@ -199,6 +192,7 @@ class NotifyPushjet(NotifyBase):
headers=headers,
auth=auth,
verify=self.verify_certificate,
timeout=self.request_timeout,
)
if r.status_code != requests.codes.ok:
# We had a problem
@ -222,7 +216,7 @@ class NotifyPushjet(NotifyBase):
except requests.RequestException as e:
self.logger.warning(
'A Connection error occured sending Pushjet '
'A Connection error occurred sending Pushjet '
'notification to %s.' % self.host)
self.logger.debug('Socket Exception: %s' % str(e))
@ -235,7 +229,7 @@ class NotifyPushjet(NotifyBase):
def parse_url(url):
"""
Parses the URL and returns enough arguments that can allow
us to substantiate this object.
us to re-instantiate this object.
Syntax:
pjet://hostname/secret_key
@ -246,16 +240,8 @@ class NotifyPushjet(NotifyBase):
pjets://hostname:port/secret_key
pjets://user:pass@hostname/secret_key
pjets://user:pass@hostname:port/secret_key
Legacy (Depricated) Syntax:
pjet://secret_key@hostname
pjet://secret_key@hostname:port
pjets://secret_key@hostname
pjets://secret_key@hostname:port
"""
results = NotifyBase.parse_url(url)
if not results:
# We're done early as we couldn't load the results
return results
@ -276,22 +262,4 @@ class NotifyPushjet(NotifyBase):
results['secret_key'] = \
NotifyPushjet.unquote(results['qsd']['secret'])
if results.get('secret_key') is None:
# Deprication Notice issued for v0.7.9
NotifyPushjet.logger.deprecate(
'The Pushjet URL contains secret_key in the user field'
' which will be deprecated in an upcoming '
'release. Please place this in the path of the URL instead.'
)
# Store it as it's value based on the user field
results['secret_key'] = \
NotifyPushjet.unquote(results.get('user'))
# there is no way http-auth is enabled, be sure to unset the
# current defined user (if present). This is done due to some
# logic that takes place in the send() since we support http-auth.
results['user'] = None
results['password'] = None
return results

@ -434,6 +434,7 @@ class NotifyPushover(NotifyBase):
files=files,
auth=auth,
verify=self.verify_certificate,
timeout=self.request_timeout,
)
if r.status_code != requests.codes.ok:
@ -461,7 +462,7 @@ class NotifyPushover(NotifyBase):
except requests.RequestException as e:
self.logger.warning(
'A Connection error occured sending Pushover:%s ' % (
'A Connection error occurred sending Pushover:%s ' % (
payload['device']) + 'notification.'
)
self.logger.debug('Socket Exception: %s' % str(e))
@ -470,7 +471,7 @@ class NotifyPushover(NotifyBase):
except (OSError, IOError) as e:
self.logger.warning(
'An I/O error occured while reading {}.'.format(
'An I/O error occurred while reading {}.'.format(
attach.name if attach else 'attachment'))
self.logger.debug('I/O Exception: %s' % str(e))
return False
@ -496,19 +497,20 @@ class NotifyPushover(NotifyBase):
PushoverPriority.EMERGENCY: 'emergency',
}
# Define any arguments set
args = {
'format': self.notify_format,
'overflow': self.overflow_mode,
# Define any URL parameters
params = {
'priority':
_map[self.template_args['priority']['default']]
if self.priority not in _map else _map[self.priority],
'verify': 'yes' if self.verify_certificate else 'no',
}
# Only add expire and retry for emergency messages,
# pushover ignores for all other priorities
if self.priority == PushoverPriority.EMERGENCY:
args.update({'expire': self.expire, 'retry': self.retry})
params.update({'expire': self.expire, 'retry': self.retry})
# Extend our parameters
params.update(self.url_parameters(privacy=privacy, *args, **kwargs))
# Escape our devices
devices = '/'.join([NotifyPushover.quote(x, safe='')
@ -519,22 +521,21 @@ class NotifyPushover(NotifyBase):
# it from the devices list
devices = ''
return '{schema}://{user_key}@{token}/{devices}/?{args}'.format(
return '{schema}://{user_key}@{token}/{devices}/?{params}'.format(
schema=self.secure_protocol,
user_key=self.pprint(self.user_key, privacy, safe=''),
token=self.pprint(self.token, privacy, safe=''),
devices=devices,
args=NotifyPushover.urlencode(args))
params=NotifyPushover.urlencode(params))
@staticmethod
def parse_url(url):
"""
Parses the URL and returns enough arguments that can allow
us to substantiate this object.
us to re-instantiate this object.
"""
results = NotifyBase.parse_url(url)
results = NotifyBase.parse_url(url, verify_host=False)
if not results:
# We're done early as we couldn't load the results
return results

@ -285,15 +285,15 @@ class NotifyRocketChat(NotifyBase):
Returns the URL built dynamically based on specified arguments.
"""
# Define any arguments set
args = {
'format': self.notify_format,
'overflow': self.overflow_mode,
'verify': 'yes' if self.verify_certificate else 'no',
# Define any URL parameters
params = {
'avatar': 'yes' if self.avatar else 'no',
'mode': self.mode,
}
# Extend our parameters
params.update(self.url_parameters(privacy=privacy, *args, **kwargs))
# Determine Authentication
if self.mode == RocketChatAuthMode.BASIC:
auth = '{user}:{password}@'.format(
@ -310,10 +310,11 @@ class NotifyRocketChat(NotifyBase):
default_port = 443 if self.secure else 80
return '{schema}://{auth}{hostname}{port}/{targets}/?{args}'.format(
return '{schema}://{auth}{hostname}{port}/{targets}/?{params}'.format(
schema=self.secure_protocol if self.secure else self.protocol,
auth=auth,
hostname=NotifyRocketChat.quote(self.host, safe=''),
# never encode hostname since we're expecting it to be a valid one
hostname=self.host,
port='' if self.port is None or self.port == default_port
else ':{}'.format(self.port),
targets='/'.join(
@ -325,7 +326,7 @@ class NotifyRocketChat(NotifyBase):
# Users
['@{}'.format(x) for x in self.users],
)]),
args=NotifyRocketChat.urlencode(args),
params=NotifyRocketChat.urlencode(params),
)
def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
@ -476,6 +477,7 @@ class NotifyRocketChat(NotifyBase):
data=payload,
headers=headers,
verify=self.verify_certificate,
timeout=self.request_timeout,
)
if r.status_code != requests.codes.ok:
# We had a problem
@ -502,7 +504,7 @@ class NotifyRocketChat(NotifyBase):
except requests.RequestException as e:
self.logger.warning(
'A Connection error occured sending Rocket.Chat '
'A Connection error occurred sending Rocket.Chat '
'{}:notification.'.format(self.mode))
self.logger.debug('Socket Exception: %s' % str(e))
@ -529,6 +531,7 @@ class NotifyRocketChat(NotifyBase):
api_url,
data=payload,
verify=self.verify_certificate,
timeout=self.request_timeout,
)
if r.status_code != requests.codes.ok:
# We had a problem
@ -570,13 +573,13 @@ class NotifyRocketChat(NotifyBase):
# - TypeError = r.content is None
# - AttributeError = r is None
self.logger.warning(
'A commuication error occured authenticating {} on '
'A commuication error occurred authenticating {} on '
'Rocket.Chat.'.format(self.user))
return False
except requests.RequestException as e:
self.logger.warning(
'A connection error occured authenticating {} on '
'A connection error occurred authenticating {} on '
'Rocket.Chat.'.format(self.user))
self.logger.debug('Socket Exception: %s' % str(e))
return False
@ -595,6 +598,7 @@ class NotifyRocketChat(NotifyBase):
api_url,
headers=self.headers,
verify=self.verify_certificate,
timeout=self.request_timeout,
)
if r.status_code != requests.codes.ok:
# We had a problem
@ -622,7 +626,7 @@ class NotifyRocketChat(NotifyBase):
except requests.RequestException as e:
self.logger.warning(
'A Connection error occured logging off the '
'A Connection error occurred logging off the '
'Rocket.Chat server')
self.logger.debug('Socket Exception: %s' % str(e))
return False
@ -633,7 +637,7 @@ class NotifyRocketChat(NotifyBase):
def parse_url(url):
"""
Parses the URL and returns enough arguments that can allow
us to substantiate this object.
us to re-instantiate this object.
"""
@ -665,7 +669,6 @@ class NotifyRocketChat(NotifyBase):
)
results = NotifyBase.parse_url(url)
if not results:
# We're done early as we couldn't load the results
return results

@ -236,6 +236,7 @@ class NotifyRyver(NotifyBase):
data=dumps(payload),
headers=headers,
verify=self.verify_certificate,
timeout=self.request_timeout,
)
if r.status_code != requests.codes.ok:
@ -260,7 +261,7 @@ class NotifyRyver(NotifyBase):
except requests.RequestException as e:
self.logger.warning(
'A Connection error occured sending Ryver:%s ' % (
'A Connection error occurred sending Ryver:%s ' % (
self.organization) + 'notification.'
)
self.logger.debug('Socket Exception: %s' % str(e))
@ -273,15 +274,15 @@ class NotifyRyver(NotifyBase):
Returns the URL built dynamically based on specified arguments.
"""
# Define any arguments set
args = {
'format': self.notify_format,
'overflow': self.overflow_mode,
# Define any URL parameters
params = {
'image': 'yes' if self.include_image else 'no',
'mode': self.mode,
'verify': 'yes' if self.verify_certificate else 'no',
}
# Extend our parameters
params.update(self.url_parameters(privacy=privacy, *args, **kwargs))
# Determine if there is a botname present
botname = ''
if self.user:
@ -289,24 +290,23 @@ class NotifyRyver(NotifyBase):
botname=NotifyRyver.quote(self.user, safe=''),
)
return '{schema}://{botname}{organization}/{token}/?{args}'.format(
return '{schema}://{botname}{organization}/{token}/?{params}'.format(
schema=self.secure_protocol,
botname=botname,
organization=NotifyRyver.quote(self.organization, safe=''),
token=self.pprint(self.token, privacy, safe=''),
args=NotifyRyver.urlencode(args),
params=NotifyRyver.urlencode(params),
)
@staticmethod
def parse_url(url):
"""
Parses the URL and returns enough arguments that can allow
us to substantiate this object.
us to re-instantiate this object.
"""
results = NotifyBase.parse_url(url)
results = NotifyBase.parse_url(url, verify_host=False)
if not results:
# We're done early as we couldn't load the results
return results
@ -323,19 +323,8 @@ class NotifyRyver(NotifyBase):
# no token
results['token'] = None
if 'webhook' in results['qsd']:
# Deprication Notice issued for v0.7.5
NotifyRyver.logger.deprecate(
'The Ryver URL contains the parameter '
'"webhook=" which will be deprecated in an upcoming '
'release. Please use "mode=" instead.'
)
# use mode= for consistency with the other plugins but we also
# support webhook= for backwards compatibility.
results['mode'] = results['qsd'].get(
'mode', results['qsd'].get(
'webhook', RyverWebhookMode.RYVER))
# Retrieve the mode
results['mode'] = results['qsd'].get('mode', RyverWebhookMode.RYVER)
# use image= for consistency with the other plugins
results['include_image'] = \
@ -352,15 +341,15 @@ class NotifyRyver(NotifyBase):
result = re.match(
r'^https?://(?P<org>[A-Z0-9_-]+)\.ryver\.com/application/webhook/'
r'(?P<webhook_token>[A-Z0-9]+)/?'
r'(?P<args>\?.+)?$', url, re.I)
r'(?P<params>\?.+)?$', url, re.I)
if result:
return NotifyRyver.parse_url(
'{schema}://{org}/{webhook_token}/{args}'.format(
'{schema}://{org}/{webhook_token}/{params}'.format(
schema=NotifyRyver.secure_protocol,
org=result.group('org'),
webhook_token=result.group('webhook_token'),
args='' if not result.group('args')
else result.group('args')))
params='' if not result.group('params')
else result.group('params')))
return None

@ -342,6 +342,7 @@ class NotifySNS(NotifyBase):
data=payload,
headers=headers,
verify=self.verify_certificate,
timeout=self.request_timeout,
)
if r.status_code != requests.codes.ok:
@ -368,7 +369,7 @@ class NotifySNS(NotifyBase):
except requests.RequestException as e:
self.logger.warning(
'A Connection error occured sending AWS '
'A Connection error occurred sending AWS '
'notification to "%s".' % (to),
)
self.logger.debug('Socket Exception: %s' % str(e))
@ -579,15 +580,11 @@ class NotifySNS(NotifyBase):
Returns the URL built dynamically based on specified arguments.
"""
# Define any arguments set
args = {
'format': self.notify_format,
'overflow': self.overflow_mode,
'verify': 'yes' if self.verify_certificate else 'no',
}
# Our URL parameters
params = self.url_parameters(privacy=privacy, *args, **kwargs)
return '{schema}://{key_id}/{key_secret}/{region}/{targets}/'\
'?{args}'.format(
'?{params}'.format(
schema=self.secure_protocol,
key_id=self.pprint(self.aws_access_key_id, privacy, safe=''),
key_secret=self.pprint(
@ -601,18 +598,17 @@ class NotifySNS(NotifyBase):
# Topics are prefixed with a pound/hashtag symbol
['#{}'.format(x) for x in self.topics],
)]),
args=NotifySNS.urlencode(args),
params=NotifySNS.urlencode(params),
)
@staticmethod
def parse_url(url):
"""
Parses the URL and returns enough arguments that can allow
us to substantiate this object.
us to re-instantiate this object.
"""
results = NotifyBase.parse_url(url)
results = NotifyBase.parse_url(url, verify_host=False)
if not results:
# We're done early as we couldn't load the results
return results

@ -50,7 +50,7 @@ from .NotifyBase import NotifyBase
from ..common import NotifyFormat
from ..common import NotifyType
from ..utils import parse_list
from ..utils import GET_EMAIL_RE
from ..utils import is_email
from ..utils import validate_regex
from ..AppriseLocale import gettext_lazy as _
@ -170,18 +170,15 @@ class NotifySendGrid(NotifyBase):
self.logger.warning(msg)
raise TypeError(msg)
self.from_email = from_email
try:
result = GET_EMAIL_RE.match(self.from_email)
if not result:
# let outer exception handle this
raise TypeError
except (TypeError, AttributeError):
msg = 'Invalid ~From~ email specified: {}'.format(self.from_email)
result = is_email(from_email)
if not result:
msg = 'Invalid ~From~ email specified: {}'.format(from_email)
self.logger.warning(msg)
raise TypeError(msg)
# Store email address
self.from_email = result['full_email']
# Acquire Targets (To Emails)
self.targets = list()
@ -201,8 +198,9 @@ class NotifySendGrid(NotifyBase):
# Validate recipients (to:) and drop bad ones:
for recipient in parse_list(targets):
if GET_EMAIL_RE.match(recipient):
self.targets.append(recipient)
result = is_email(recipient)
if result:
self.targets.append(result['full_email'])
continue
self.logger.warning(
@ -213,8 +211,9 @@ class NotifySendGrid(NotifyBase):
# Validate recipients (cc:) and drop bad ones:
for recipient in parse_list(cc):
if GET_EMAIL_RE.match(recipient):
self.cc.add(recipient)
result = is_email(recipient)
if result:
self.cc.add(result['full_email'])
continue
self.logger.warning(
@ -225,8 +224,9 @@ class NotifySendGrid(NotifyBase):
# Validate recipients (bcc:) and drop bad ones:
for recipient in parse_list(bcc):
if GET_EMAIL_RE.match(recipient):
self.bcc.add(recipient)
result = is_email(recipient)
if result:
self.bcc.add(result['full_email'])
continue
self.logger.warning(
@ -245,41 +245,38 @@ class NotifySendGrid(NotifyBase):
Returns the URL built dynamically based on specified arguments.
"""
# Define any arguments set
args = {
'format': self.notify_format,
'overflow': self.overflow_mode,
'verify': 'yes' if self.verify_certificate else 'no',
}
# Our URL parameters
params = self.url_parameters(privacy=privacy, *args, **kwargs)
if len(self.cc) > 0:
# Handle our Carbon Copy Addresses
args['cc'] = ','.join(self.cc)
params['cc'] = ','.join(self.cc)
if len(self.bcc) > 0:
# Handle our Blind Carbon Copy Addresses
args['bcc'] = ','.join(self.bcc)
params['bcc'] = ','.join(self.bcc)
if self.template:
# Handle our Template ID if if was specified
args['template'] = self.template
params['template'] = self.template
# Append our template_data into our args
args.update({'+{}'.format(k): v
for k, v in self.template_data.items()})
# Append our template_data into our parameter list
params.update(
{'+{}'.format(k): v for k, v in self.template_data.items()})
# a simple boolean check as to whether we display our target emails
# or not
has_targets = \
not (len(self.targets) == 1 and self.targets[0] == self.from_email)
return '{schema}://{apikey}:{from_email}/{targets}?{args}'.format(
return '{schema}://{apikey}:{from_email}/{targets}?{params}'.format(
schema=self.secure_protocol,
apikey=self.pprint(self.apikey, privacy, safe=''),
from_email=self.quote(self.from_email, safe='@'),
# never encode email since it plays a huge role in our hostname
from_email=self.from_email,
targets='' if not has_targets else '/'.join(
[NotifySendGrid.quote(x, safe='') for x in self.targets]),
args=NotifySendGrid.urlencode(args),
params=NotifySendGrid.urlencode(params),
)
def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
@ -361,6 +358,7 @@ class NotifySendGrid(NotifyBase):
data=dumps(payload),
headers=headers,
verify=self.verify_certificate,
timeout=self.request_timeout,
)
if r.status_code not in (
requests.codes.ok, requests.codes.accepted):
@ -390,7 +388,7 @@ class NotifySendGrid(NotifyBase):
except requests.RequestException as e:
self.logger.warning(
'A Connection error occured sending SendGrid '
'A Connection error occurred sending SendGrid '
'notification to {}.'.format(target))
self.logger.debug('Socket Exception: %s' % str(e))
@ -404,7 +402,7 @@ class NotifySendGrid(NotifyBase):
def parse_url(url):
"""
Parses the URL and returns enough arguments that can allow
us to substantiate this object.
us to re-instantiate this object.
"""

@ -142,14 +142,6 @@ class NotifySimplePush(NotifyBase):
# Default Event Name
self.event = None
# Encrypt Message (providing support is available)
if self.password and self.user and not CRYPTOGRAPHY_AVAILABLE:
# Provide the end user at least some notification that they're
# not getting what they asked for
self.logger.warning(
'SimplePush extended encryption is not supported by this '
'system.')
# Used/cached in _encrypt() function
self._iv = None
self._iv_hex = None
@ -189,6 +181,15 @@ class NotifySimplePush(NotifyBase):
Perform SimplePush Notification
"""
# Encrypt Message (providing support is available)
if self.password and self.user and not CRYPTOGRAPHY_AVAILABLE:
# Provide the end user at least some notification that they're
# not getting what they asked for
self.logger.warning(
"Authenticated SimplePush Notifications are not supported by "
"this system; `pip install cryptography`.")
return False
headers = {
'User-Agent': self.app_id,
'Content-type': "application/x-www-form-urlencoded",
@ -236,6 +237,7 @@ class NotifySimplePush(NotifyBase):
data=payload,
headers=headers,
verify=self.verify_certificate,
timeout=self.request_timeout,
)
# Get our SimplePush response (if it's possible)
@ -272,7 +274,7 @@ class NotifySimplePush(NotifyBase):
except requests.RequestException as e:
self.logger.warning(
'A Connection error occured sending SimplePush notification.')
'A Connection error occurred sending SimplePush notification.')
self.logger.debug('Socket Exception: %s' % str(e))
# Return; we're done
@ -285,15 +287,11 @@ class NotifySimplePush(NotifyBase):
Returns the URL built dynamically based on specified arguments.
"""
# Define any arguments set
args = {
'format': self.notify_format,
'overflow': self.overflow_mode,
'verify': 'yes' if self.verify_certificate else 'no',
}
# Our URL parameters
params = self.url_parameters(privacy=privacy, *args, **kwargs)
if self.event:
args['event'] = self.event
params['event'] = self.event
# Determine Authentication
auth = ''
@ -305,21 +303,21 @@ class NotifySimplePush(NotifyBase):
self.password, privacy, mode=PrivacyMode.Secret, safe=''),
)
return '{schema}://{auth}{apikey}/?{args}'.format(
return '{schema}://{auth}{apikey}/?{params}'.format(
schema=self.secure_protocol,
auth=auth,
apikey=self.pprint(self.apikey, privacy, safe=''),
args=NotifySimplePush.urlencode(args),
params=NotifySimplePush.urlencode(params),
)
@staticmethod
def parse_url(url):
"""
Parses the URL and returns enough arguments that can allow
us to substantiate this object.
us to re-instantiate this object.
"""
results = NotifyBase.parse_url(url)
results = NotifyBase.parse_url(url, verify_host=False)
if not results:
# We're done early as we couldn't load the results
return results

@ -322,6 +322,7 @@ class NotifySinch(NotifyBase):
data=json.dumps(payload),
headers=headers,
verify=self.verify_certificate,
timeout=self.request_timeout,
)
# The responsne might look like:
@ -383,7 +384,7 @@ class NotifySinch(NotifyBase):
except requests.RequestException as e:
self.logger.warning(
'A Connection error occured sending Sinch:%s ' % (
'A Connection error occurred sending Sinch:%s ' % (
target) + 'notification.'
)
self.logger.debug('Socket Exception: %s' % str(e))
@ -399,15 +400,15 @@ class NotifySinch(NotifyBase):
Returns the URL built dynamically based on specified arguments.
"""
# Define any arguments set
args = {
'format': self.notify_format,
'overflow': self.overflow_mode,
'verify': 'yes' if self.verify_certificate else 'no',
# Define any URL parameters
params = {
'region': self.region,
}
return '{schema}://{spi}:{token}@{source}/{targets}/?{args}'.format(
# Extend our parameters
params.update(self.url_parameters(privacy=privacy, *args, **kwargs))
return '{schema}://{spi}:{token}@{source}/{targets}/?{params}'.format(
schema=self.secure_protocol,
spi=self.pprint(
self.service_plan_id, privacy, mode=PrivacyMode.Tail, safe=''),
@ -415,13 +416,13 @@ class NotifySinch(NotifyBase):
source=NotifySinch.quote(self.source, safe=''),
targets='/'.join(
[NotifySinch.quote(x, safe='') for x in self.targets]),
args=NotifySinch.urlencode(args))
params=NotifySinch.urlencode(params))
@staticmethod
def parse_url(url):
"""
Parses the URL and returns enough arguments that can allow
us to substantiate this object.
us to re-instantiate this object.
"""
results = NotifyBase.parse_url(url, verify_host=False)

@ -505,6 +505,7 @@ class NotifySlack(NotifyBase):
headers=headers,
files=files,
verify=self.verify_certificate,
timeout=self.request_timeout,
)
if r.status_code != requests.codes.ok:
@ -622,14 +623,14 @@ class NotifySlack(NotifyBase):
# }
except requests.RequestException as e:
self.logger.warning(
'A Connection error occured posting {}to Slack.'.format(
'A Connection error occurred posting {}to Slack.'.format(
attach.name if attach else ''))
self.logger.debug('Socket Exception: %s' % str(e))
return False
except (OSError, IOError) as e:
self.logger.warning(
'An I/O error occured while reading {}.'.format(
'An I/O error occurred while reading {}.'.format(
attach.name if attach else 'attachment'))
self.logger.debug('I/O Exception: %s' % str(e))
return False
@ -648,15 +649,15 @@ class NotifySlack(NotifyBase):
Returns the URL built dynamically based on specified arguments.
"""
# Define any arguments set
args = {
'format': self.notify_format,
'overflow': self.overflow_mode,
# Define any URL parameters
params = {
'image': 'yes' if self.include_image else 'no',
'footer': 'yes' if self.include_footer else 'no',
'verify': 'yes' if self.verify_certificate else 'no',
}
# Extend our parameters
params.update(self.url_parameters(privacy=privacy, *args, **kwargs))
if self.mode == SlackMode.WEBHOOK:
# Determine if there is a botname present
botname = ''
@ -666,7 +667,7 @@ class NotifySlack(NotifyBase):
)
return '{schema}://{botname}{token_a}/{token_b}/{token_c}/'\
'{targets}/?{args}'.format(
'{targets}/?{params}'.format(
schema=self.secure_protocol,
botname=botname,
token_a=self.pprint(self.token_a, privacy, safe=''),
@ -675,23 +676,23 @@ class NotifySlack(NotifyBase):
targets='/'.join(
[NotifySlack.quote(x, safe='')
for x in self.channels]),
args=NotifySlack.urlencode(args),
params=NotifySlack.urlencode(params),
)
# else -> self.mode == SlackMode.BOT:
return '{schema}://{access_token}/{targets}/'\
'?{args}'.format(
'?{params}'.format(
schema=self.secure_protocol,
access_token=self.pprint(self.access_token, privacy, safe=''),
targets='/'.join(
[NotifySlack.quote(x, safe='') for x in self.channels]),
args=NotifySlack.urlencode(args),
params=NotifySlack.urlencode(params),
)
@staticmethod
def parse_url(url):
"""
Parses the URL and returns enough arguments that can allow
us to substantiate this object.
us to re-instantiate this object.
"""
results = NotifyBase.parse_url(url, verify_host=False)
@ -760,16 +761,16 @@ class NotifySlack(NotifyBase):
r'(?P<token_a>[A-Z0-9]+)/'
r'(?P<token_b>[A-Z0-9]+)/'
r'(?P<token_c>[A-Z0-9]+)/?'
r'(?P<args>\?.+)?$', url, re.I)
r'(?P<params>\?.+)?$', url, re.I)
if result:
return NotifySlack.parse_url(
'{schema}://{token_a}/{token_b}/{token_c}/{args}'.format(
'{schema}://{token_a}/{token_b}/{token_c}/{params}'.format(
schema=NotifySlack.secure_protocol,
token_a=result.group('token_a'),
token_b=result.group('token_b'),
token_c=result.group('token_c'),
args='' if not result.group('args')
else result.group('args')))
params='' if not result.group('params')
else result.group('params')))
return None

@ -233,32 +233,33 @@ class NotifySyslog(NotifyBase):
Returns the URL built dynamically based on specified arguments.
"""
# Define any arguments set
args = {
# Define any URL parameters
params = {
'logperror': 'yes' if self.log_perror else 'no',
'logpid': 'yes' if self.log_pid else 'no',
'format': self.notify_format,
'overflow': self.overflow_mode,
'verify': 'yes' if self.verify_certificate else 'no',
}
return '{schema}://{facility}/?{args}'.format(
# Extend our parameters
params.update(self.url_parameters(privacy=privacy, *args, **kwargs))
return '{schema}://{facility}/?{params}'.format(
facility=self.template_tokens['facility']['default']
if self.facility not in SYSLOG_FACILITY_RMAP
else SYSLOG_FACILITY_RMAP[self.facility],
schema=self.secure_protocol,
args=NotifySyslog.urlencode(args),
params=NotifySyslog.urlencode(params),
)
@staticmethod
def parse_url(url):
"""
Parses the URL and returns enough arguments that can allow
us to substantiate this object.
us to re-instantiate this object.
"""
results = NotifyBase.parse_url(url, verify_host=False)
if not results:
# We're done early as we couldn't load the results
return results
# if specified; save hostname into facility

@ -145,6 +145,7 @@ class NotifyTechulusPush(NotifyBase):
data=dumps(payload),
headers=headers,
verify=self.verify_certificate,
timeout=self.request_timeout,
)
if r.status_code not in (
requests.codes.ok, requests.codes.no_content):
@ -171,7 +172,7 @@ class NotifyTechulusPush(NotifyBase):
except requests.RequestException as e:
self.logger.warning(
'A Connection error occured sending Techulus Push '
'A Connection error occurred sending Techulus Push '
'notification.'
)
self.logger.debug('Socket Exception: %s' % str(e))
@ -185,28 +186,23 @@ class NotifyTechulusPush(NotifyBase):
Returns the URL built dynamically based on specified arguments.
"""
# Define any arguments set
args = {
'format': self.notify_format,
'overflow': self.overflow_mode,
'verify': 'yes' if self.verify_certificate else 'no',
}
# Our URL parameters
params = self.url_parameters(privacy=privacy, *args, **kwargs)
return '{schema}://{apikey}/?{args}'.format(
return '{schema}://{apikey}/?{params}'.format(
schema=self.secure_protocol,
apikey=self.pprint(self.apikey, privacy, safe=''),
args=NotifyTechulusPush.urlencode(args),
params=NotifyTechulusPush.urlencode(params),
)
@staticmethod
def parse_url(url):
"""
Parses the URL and returns enough arguments that can allow
us to substantiate this object.
us to re-instantiate this object.
"""
results = NotifyBase.parse_url(url, verify_host=False)
if not results:
# We're done early as we couldn't load the results
return results

@ -229,23 +229,19 @@ class NotifyTelegram(NotifyBase):
# Parse our list
self.targets = parse_list(targets)
# if detect_owner is set to True, we will attempt to determine who
# the bot owner is based on the first person who messaged it. This
# is not a fool proof way of doing things as over time Telegram removes
# the message history for the bot. So what appears (later on) to be
# the first message to it, maybe another user who sent it a message
# much later. Users who set this flag should update their Apprise
# URL later to directly include the user that we should message.
self.detect_owner = detect_owner
if self.user:
# Treat this as a channel too
self.targets.append(self.user)
if len(self.targets) == 0 and self.detect_owner:
_id = self.detect_bot_owner()
if _id:
# Store our id
self.targets.append(str(_id))
if len(self.targets) == 0:
err = 'No chat_id(s) were specified.'
self.logger.warning(err)
raise TypeError(err)
# Track whether or not we want to send an image with our notification
# or not.
self.include_image = include_image
@ -325,6 +321,7 @@ class NotifyTelegram(NotifyBase):
files=files,
data=payload,
verify=self.verify_certificate,
timeout=self.request_timeout,
)
if r.status_code != requests.codes.ok:
@ -349,7 +346,7 @@ class NotifyTelegram(NotifyBase):
except requests.RequestException as e:
self.logger.warning(
'A connection error occured posting Telegram '
'A connection error occurred posting Telegram '
'attachment.')
self.logger.debug('Socket Exception: %s' % str(e))
@ -393,6 +390,7 @@ class NotifyTelegram(NotifyBase):
url,
headers=headers,
verify=self.verify_certificate,
timeout=self.request_timeout,
)
if r.status_code != requests.codes.ok:
@ -436,12 +434,12 @@ class NotifyTelegram(NotifyBase):
# - TypeError = r.content is None
# - AttributeError = r is None
self.logger.warning(
'A communication error occured detecting the Telegram User.')
'A communication error occurred detecting the Telegram User.')
return 0
except requests.RequestException as e:
self.logger.warning(
'A connection error occured detecting the Telegram User.')
'A connection error occurred detecting the Telegram User.')
self.logger.debug('Socket Exception: %s' % str(e))
return 0
@ -472,7 +470,7 @@ class NotifyTelegram(NotifyBase):
entry = response['result'][0]
_id = entry['message']['from'].get('id', 0)
_user = entry['message']['from'].get('first_name')
self.logger.info('Detected telegram user %s (userid=%d)' % (
self.logger.info('Detected Telegram user %s (userid=%d)' % (
_user, _id))
# Return our detected userid
return _id
@ -488,6 +486,19 @@ class NotifyTelegram(NotifyBase):
Perform Telegram Notification
"""
if len(self.targets) == 0 and self.detect_owner:
_id = self.detect_bot_owner()
if _id:
# Permanently store our id in our target list for next time
self.targets.append(str(_id))
self.logger.info(
'Update your Telegram Apprise URL to read: '
'{}'.format(self.url(privacy=True)))
if len(self.targets) == 0:
self.logger.warning('There were not Telegram chat_ids to notify.')
return False
headers = {
'User-Agent': self.app_id,
'Content-Type': 'application/json',
@ -597,6 +608,7 @@ class NotifyTelegram(NotifyBase):
data=dumps(payload),
headers=headers,
verify=self.verify_certificate,
timeout=self.request_timeout,
)
if r.status_code != requests.codes.ok:
@ -631,7 +643,7 @@ class NotifyTelegram(NotifyBase):
except requests.RequestException as e:
self.logger.warning(
'A connection error occured sending Telegram:%s ' % (
'A connection error occurred sending Telegram:%s ' % (
payload['chat_id']) + 'notification.'
)
self.logger.debug('Socket Exception: %s' % str(e))
@ -663,29 +675,29 @@ class NotifyTelegram(NotifyBase):
Returns the URL built dynamically based on specified arguments.
"""
# Define any arguments set
args = {
'format': self.notify_format,
'overflow': self.overflow_mode,
# Define any URL parameters
params = {
'image': self.include_image,
'verify': 'yes' if self.verify_certificate else 'no',
'detect': 'yes' if self.detect_owner else 'no',
}
# Extend our parameters
params.update(self.url_parameters(privacy=privacy, *args, **kwargs))
# No need to check the user token because the user automatically gets
# appended into the list of chat ids
return '{schema}://{bot_token}/{targets}/?{args}'.format(
return '{schema}://{bot_token}/{targets}/?{params}'.format(
schema=self.secure_protocol,
bot_token=self.pprint(self.bot_token, privacy, safe=''),
targets='/'.join(
[NotifyTelegram.quote('@{}'.format(x)) for x in self.targets]),
args=NotifyTelegram.urlencode(args))
params=NotifyTelegram.urlencode(params))
@staticmethod
def parse_url(url):
"""
Parses the URL and returns enough arguments that can allow
us to substantiate this object.
us to re-instantiate this object.
"""
# This is a dirty hack; but it's the only work around to tgram://
@ -718,17 +730,14 @@ class NotifyTelegram(NotifyBase):
tgram.group('protocol'),
tgram.group('prefix'),
tgram.group('btoken_a'),
tgram.group('remaining')))
tgram.group('remaining')), verify_host=False)
else:
# Try again
results = NotifyBase.parse_url(
'%s%s/%s' % (
tgram.group('protocol'),
tgram.group('btoken_a'),
tgram.group('remaining'),
),
)
results = NotifyBase.parse_url('%s%s/%s' % (
tgram.group('protocol'),
tgram.group('btoken_a'),
tgram.group('remaining')), verify_host=False)
# The first token is stored in the hostname
bot_token_a = NotifyTelegram.unquote(results['host'])

@ -304,6 +304,7 @@ class NotifyTwilio(NotifyBase):
data=payload,
headers=headers,
verify=self.verify_certificate,
timeout=self.request_timeout,
)
if r.status_code not in (
@ -351,7 +352,7 @@ class NotifyTwilio(NotifyBase):
except requests.RequestException as e:
self.logger.warning(
'A Connection error occured sending Twilio:%s ' % (
'A Connection error occurred sending Twilio:%s ' % (
target) + 'notification.'
)
self.logger.debug('Socket Exception: %s' % str(e))
@ -367,14 +368,10 @@ class NotifyTwilio(NotifyBase):
Returns the URL built dynamically based on specified arguments.
"""
# Define any arguments set
args = {
'format': self.notify_format,
'overflow': self.overflow_mode,
'verify': 'yes' if self.verify_certificate else 'no',
}
# Our URL parameters
params = self.url_parameters(privacy=privacy, *args, **kwargs)
return '{schema}://{sid}:{token}@{source}/{targets}/?{args}'.format(
return '{schema}://{sid}:{token}@{source}/{targets}/?{params}'.format(
schema=self.secure_protocol,
sid=self.pprint(
self.account_sid, privacy, mode=PrivacyMode.Tail, safe=''),
@ -382,13 +379,13 @@ class NotifyTwilio(NotifyBase):
source=NotifyTwilio.quote(self.source, safe=''),
targets='/'.join(
[NotifyTwilio.quote(x, safe='') for x in self.targets]),
args=NotifyTwilio.urlencode(args))
params=NotifyTwilio.urlencode(params))
@staticmethod
def parse_url(url):
"""
Parses the URL and returns enough arguments that can allow
us to substantiate this object.
us to re-instantiate this object.
"""
results = NotifyBase.parse_url(url, verify_host=False)

@ -36,7 +36,7 @@ from ..URLBase import PrivacyMode
from ..common import NotifyFormat
from ..common import NotifyType
from ..utils import parse_list
from ..utils import GET_EMAIL_RE
from ..utils import is_email
from ..AppriseLocale import gettext_lazy as _
@ -140,12 +140,6 @@ class NotifyTwist(NotifyBase):
# <workspace_id>:<channel_id>
self.channel_ids = set()
# Initialize our Email Object
self.email = email if email else '{}@{}'.format(
self.user,
self.host,
)
# The token is None if we're not logged in and False if we
# failed to log in. Otherwise it is set to the actual token
self.token = None
@ -171,26 +165,31 @@ class NotifyTwist(NotifyBase):
# }
self._cached_channels = dict()
try:
result = GET_EMAIL_RE.match(self.email)
if not result:
# let outer exception handle this
raise TypeError
if email:
# Force user/host to be that of the defined email for
# consistency. This is very important for those initializing
# this object with the the email object would could potentially
# cause inconsistency to contents in the NotifyBase() object
self.user = result.group('fulluser')
self.host = result.group('domain')
except (TypeError, AttributeError):
# Initialize our Email Object
self.email = email if email else '{}@{}'.format(
self.user,
self.host,
)
# Check if it is valid
result = is_email(self.email)
if not result:
# let outer exception handle this
msg = 'The Twist Auth email specified ({}) is invalid.'\
.format(self.email)
self.logger.warning(msg)
raise TypeError(msg)
# Re-assign email based on what was parsed
self.email = result['full_email']
if email:
# Force user/host to be that of the defined email for
# consistency. This is very important for those initializing
# this object with the the email object would could potentially
# cause inconsistency to contents in the NotifyBase() object
self.user = result['user']
self.host = result['domain']
if not self.password:
msg = 'No Twist password was specified with account: {}'\
.format(self.email)
@ -229,28 +228,25 @@ class NotifyTwist(NotifyBase):
Returns the URL built dynamically based on specified arguments.
"""
# Define any arguments set
args = {
'format': self.notify_format,
'overflow': self.overflow_mode,
'verify': 'yes' if self.verify_certificate else 'no',
}
return '{schema}://{password}:{user}@{host}/{targets}/?{args}'.format(
schema=self.secure_protocol,
password=self.pprint(
self.password, privacy, mode=PrivacyMode.Secret, safe=''),
user=self.quote(self.user, safe=''),
host=self.host,
targets='/'.join(
[NotifyTwist.quote(x, safe='') for x in chain(
# Channels are prefixed with a pound/hashtag symbol
['#{}'.format(x) for x in self.channels],
# Channel IDs
self.channel_ids,
)]),
args=NotifyTwist.urlencode(args),
)
# Our URL parameters
params = self.url_parameters(privacy=privacy, *args, **kwargs)
return '{schema}://{password}:{user}@{host}/{targets}/' \
'?{params}'.format(
schema=self.secure_protocol,
password=self.pprint(
self.password, privacy, mode=PrivacyMode.Secret, safe=''),
user=self.quote(self.user, safe=''),
host=self.host,
targets='/'.join(
[NotifyTwist.quote(x, safe='') for x in chain(
# Channels are prefixed with a pound/hashtag symbol
['#{}'.format(x) for x in self.channels],
# Channel IDs
self.channel_ids,
)]),
params=NotifyTwist.urlencode(params),
)
def login(self):
"""
@ -640,7 +636,9 @@ class NotifyTwist(NotifyBase):
api_url,
data=payload,
headers=headers,
verify=self.verify_certificate)
verify=self.verify_certificate,
timeout=self.request_timeout,
)
# Get our JSON content if it's possible
try:
@ -679,7 +677,9 @@ class NotifyTwist(NotifyBase):
api_url,
data=payload,
headers=headers,
verify=self.verify_certificate)
verify=self.verify_certificate,
timeout=self.request_timeout
)
# Get our JSON content if it's possible
try:
@ -725,11 +725,10 @@ class NotifyTwist(NotifyBase):
def parse_url(url):
"""
Parses the URL and returns enough arguments that can allow
us to substantiate this object.
us to re-instantiate this object.
"""
results = NotifyBase.parse_url(url)
if not results:
# We're done early as we couldn't load the results
return results

@ -73,9 +73,8 @@ class NotifyTwitter(NotifyBase):
# The services URL
service_url = 'https://twitter.com/'
# The default secure protocol is twitter. 'tweet' is left behind
# for backwards compatibility of older apprise usage
secure_protocol = ('twitter', 'tweet')
# The default secure protocol is twitter.
secure_protocol = 'twitter'
# A URL that takes you to the setup/help of the specific protocol
setup_url = 'https://github.com/caronc/apprise/wiki/Notify_twitter'
@ -510,7 +509,9 @@ class NotifyTwitter(NotifyBase):
data=payload,
headers=headers,
auth=auth,
verify=self.verify_certificate)
verify=self.verify_certificate,
timeout=self.request_timeout,
)
if r.status_code != requests.codes.ok:
# We had a problem
@ -577,21 +578,21 @@ class NotifyTwitter(NotifyBase):
Returns the URL built dynamically based on specified arguments.
"""
# Define any arguments set
args = {
'format': self.notify_format,
'overflow': self.overflow_mode,
# Define any URL parameters
params = {
'mode': self.mode,
'verify': 'yes' if self.verify_certificate else 'no',
}
# Extend our parameters
params.update(self.url_parameters(privacy=privacy, *args, **kwargs))
if len(self.targets) > 0:
args['to'] = ','.join([NotifyTwitter.quote(x, safe='')
for x in self.targets])
params['to'] = ','.join(
[NotifyTwitter.quote(x, safe='') for x in self.targets])
return '{schema}://{ckey}/{csecret}/{akey}/{asecret}' \
'/{targets}/?{args}'.format(
schema=self.secure_protocol[0],
'/{targets}/?{params}'.format(
schema=self.secure_protocol,
ckey=self.pprint(self.ckey, privacy, safe=''),
csecret=self.pprint(
self.csecret, privacy, mode=PrivacyMode.Secret, safe=''),
@ -601,17 +602,16 @@ class NotifyTwitter(NotifyBase):
targets='/'.join(
[NotifyTwitter.quote('@{}'.format(target), safe='')
for target in self.targets]),
args=NotifyTwitter.urlencode(args))
params=NotifyTwitter.urlencode(params))
@staticmethod
def parse_url(url):
"""
Parses the URL and returns enough arguments that can allow
us to substantiate this object.
us to re-instantiate this object.
"""
results = NotifyBase.parse_url(url, verify_host=False)
if not results:
# We're done early as we couldn't load the results
return results
@ -662,9 +662,4 @@ class NotifyTwitter(NotifyBase):
results['targets'] += \
NotifyTwitter.parse_list(results['qsd']['to'])
if results.get('schema', 'twitter').lower() == 'tweet':
# Deprication Notice issued for v0.7.9
NotifyTwitter.logger.deprecate(
'tweet:// has been replaced by twitter://')
return results

@ -168,6 +168,7 @@ class NotifyWebexTeams(NotifyBase):
data=dumps(payload),
headers=headers,
verify=self.verify_certificate,
timeout=self.request_timeout,
)
if r.status_code not in (
requests.codes.ok, requests.codes.no_content):
@ -194,7 +195,7 @@ class NotifyWebexTeams(NotifyBase):
except requests.RequestException as e:
self.logger.warning(
'A Connection error occured sending Webex Teams '
'A Connection error occurred sending Webex Teams '
'notification.'
)
self.logger.debug('Socket Exception: %s' % str(e))
@ -208,28 +209,23 @@ class NotifyWebexTeams(NotifyBase):
Returns the URL built dynamically based on specified arguments.
"""
# Define any arguments set
args = {
'format': self.notify_format,
'overflow': self.overflow_mode,
'verify': 'yes' if self.verify_certificate else 'no',
}
# Our URL parameters
params = self.url_parameters(privacy=privacy, *args, **kwargs)
return '{schema}://{token}/?{args}'.format(
return '{schema}://{token}/?{params}'.format(
schema=self.secure_protocol,
token=self.pprint(self.token, privacy, safe=''),
args=NotifyWebexTeams.urlencode(args),
params=NotifyWebexTeams.urlencode(params),
)
@staticmethod
def parse_url(url):
"""
Parses the URL and returns enough arguments that can allow
us to substantiate this object.
us to re-instantiate this object.
"""
results = NotifyBase.parse_url(url, verify_host=False)
if not results:
# We're done early as we couldn't load the results
return results
@ -248,14 +244,14 @@ class NotifyWebexTeams(NotifyBase):
result = re.match(
r'^https?://api\.ciscospark\.com/v[1-9][0-9]*/webhooks/incoming/'
r'(?P<webhook_token>[A-Z0-9_-]+)/?'
r'(?P<args>\?.+)?$', url, re.I)
r'(?P<params>\?.+)?$', url, re.I)
if result:
return NotifyWebexTeams.parse_url(
'{schema}://{webhook_token}/{args}'.format(
'{schema}://{webhook_token}/{params}'.format(
schema=NotifyWebexTeams.secure_protocol,
webhook_token=result.group('webhook_token'),
args='' if not result.group('args')
else result.group('args')))
params='' if not result.group('params')
else result.group('params')))
return None

@ -48,7 +48,7 @@ try:
except ImportError:
# No problem; we just simply can't support this plugin because we're
# either using Linux, or simply do not have pypiwin32 installed.
# either using Linux, or simply do not have pywin32 installed.
pass
@ -91,7 +91,7 @@ class NotifyWindows(NotifyBase):
# Define object templates
templates = (
'{schema}://_/',
'{schema}://',
)
# Define our template arguments
@ -146,7 +146,8 @@ class NotifyWindows(NotifyBase):
if not self._enabled:
self.logger.warning(
"Windows Notifications are not supported by this system.")
"Windows Notifications are not supported by this system; "
"`pip install pywin32`.")
return False
# Always call throttle before any remote server i/o is made
@ -222,18 +223,18 @@ class NotifyWindows(NotifyBase):
Returns the URL built dynamically based on specified arguments.
"""
# Define any arguments set
args = {
'format': self.notify_format,
'overflow': self.overflow_mode,
# Define any URL parameters
params = {
'image': 'yes' if self.include_image else 'no',
'duration': str(self.duration),
'verify': 'yes' if self.verify_certificate else 'no',
}
return '{schema}://_/?{args}'.format(
# Extend our parameters
params.update(self.url_parameters(privacy=privacy, *args, **kwargs))
return '{schema}://?{params}'.format(
schema=self.protocol,
args=NotifyWindows.urlencode(args),
params=NotifyWindows.urlencode(params),
)
@staticmethod
@ -245,19 +246,7 @@ class NotifyWindows(NotifyBase):
"""
results = NotifyBase.parse_url(url)
if not results:
results = {
'schema': NotifyWindows.protocol,
'user': None,
'password': None,
'port': None,
'host': '_',
'fullpath': None,
'path': None,
'url': url,
'qsd': {},
}
results = NotifyBase.parse_url(url, verify_host=False)
# Include images with our message
results['include_image'] = \

@ -73,9 +73,6 @@ class NotifyXBMC(NotifyBase):
# Allows the user to specify the NotifyImageSize object
image_size = NotifyImageSize.XY_128
# The number of seconds to display the popup for
default_popup_duration_sec = 12
# XBMC default protocol version (v2)
xbmc_remote_protocol = 2
@ -137,8 +134,9 @@ class NotifyXBMC(NotifyBase):
super(NotifyXBMC, self).__init__(**kwargs)
# Number of seconds to display notification for
self.duration = self.default_popup_duration_sec \
if not (isinstance(duration, int) and duration > 0) else duration
self.duration = self.template_args['duration']['default'] \
if not (isinstance(duration, int) and
self.template_args['duration']['min'] > 0) else duration
# Build our schema
self.schema = 'https' if self.secure else 'http'
@ -264,6 +262,7 @@ class NotifyXBMC(NotifyBase):
headers=headers,
auth=auth,
verify=self.verify_certificate,
timeout=self.request_timeout,
)
if r.status_code != requests.codes.ok:
# We had a problem
@ -287,7 +286,7 @@ class NotifyXBMC(NotifyBase):
except requests.RequestException as e:
self.logger.warning(
'A Connection error occured sending XBMC/KODI '
'A Connection error occurred sending XBMC/KODI '
'notification.'
)
self.logger.debug('Socket Exception: %s' % str(e))
@ -302,15 +301,15 @@ class NotifyXBMC(NotifyBase):
Returns the URL built dynamically based on specified arguments.
"""
# Define any arguments set
args = {
'format': self.notify_format,
'overflow': self.overflow_mode,
# Define any URL parameters
params = {
'image': 'yes' if self.include_image else 'no',
'duration': str(self.duration),
'verify': 'yes' if self.verify_certificate else 'no',
}
# Extend our parameters
params.update(self.url_parameters(privacy=privacy, *args, **kwargs))
# Determine Authentication
auth = ''
if self.user and self.password:
@ -331,20 +330,21 @@ class NotifyXBMC(NotifyBase):
# Append 's' to schema
default_schema += 's'
return '{schema}://{auth}{hostname}{port}/?{args}'.format(
return '{schema}://{auth}{hostname}{port}/?{params}'.format(
schema=default_schema,
auth=auth,
hostname=NotifyXBMC.quote(self.host, safe=''),
# never encode hostname since we're expecting it to be a valid one
hostname=self.host,
port='' if not self.port or self.port == default_port
else ':{}'.format(self.port),
args=NotifyXBMC.urlencode(args),
params=NotifyXBMC.urlencode(params),
)
@staticmethod
def parse_url(url):
"""
Parses the URL and returns enough arguments that can allow
us to substantiate this object.
us to re-instantiate this object.
"""
results = NotifyBase.parse_url(url)

@ -143,15 +143,11 @@ class NotifyXML(NotifyBase):
Returns the URL built dynamically based on specified arguments.
"""
# Define any arguments set
args = {
'format': self.notify_format,
'overflow': self.overflow_mode,
'verify': 'yes' if self.verify_certificate else 'no',
}
# Store our defined headers into our URL parameters
params = {'+{}'.format(k): v for k, v in self.headers.items()}
# Append our headers into our args
args.update({'+{}'.format(k): v for k, v in self.headers.items()})
# Extend our parameters
params.update(self.url_parameters(privacy=privacy, *args, **kwargs))
# Determine Authentication
auth = ''
@ -168,14 +164,15 @@ class NotifyXML(NotifyBase):
default_port = 443 if self.secure else 80
return '{schema}://{auth}{hostname}{port}{fullpath}/?{args}'.format(
return '{schema}://{auth}{hostname}{port}{fullpath}/?{params}'.format(
schema=self.secure_protocol if self.secure else self.protocol,
auth=auth,
hostname=NotifyXML.quote(self.host, safe=''),
# never encode hostname since we're expecting it to be a valid one
hostname=self.host,
port='' if self.port is None or self.port == default_port
else ':{}'.format(self.port),
fullpath=NotifyXML.quote(self.fullpath, safe='/'),
args=NotifyXML.urlencode(args),
params=NotifyXML.urlencode(params),
)
def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
@ -234,6 +231,7 @@ class NotifyXML(NotifyBase):
headers=headers,
auth=auth,
verify=self.verify_certificate,
timeout=self.request_timeout,
)
if r.status_code != requests.codes.ok:
# We had a problem
@ -257,7 +255,7 @@ class NotifyXML(NotifyBase):
except requests.RequestException as e:
self.logger.warning(
'A Connection error occured sending XML '
'A Connection error occurred sending XML '
'notification to %s.' % self.host)
self.logger.debug('Socket Exception: %s' % str(e))
@ -270,11 +268,10 @@ class NotifyXML(NotifyBase):
def parse_url(url):
"""
Parses the URL and returns enough arguments that can allow
us to substantiate this object.
us to re-instantiate this object.
"""
results = NotifyBase.parse_url(url)
if not results:
# We're done early as we couldn't load the results
return results

@ -272,20 +272,16 @@ class NotifyXMPP(NotifyBase):
Returns the URL built dynamically based on specified arguments.
"""
# Define any arguments set
args = {
'format': self.notify_format,
'overflow': self.overflow_mode,
'verify': 'yes' if self.verify_certificate else 'no',
}
# Our URL parameters
params = self.url_parameters(privacy=privacy, *args, **kwargs)
if self.jid:
args['jid'] = self.jid
params['jid'] = self.jid
if self.xep:
# xep are integers, so we need to just iterate over a list and
# switch them to a string
args['xep'] = ','.join([str(xep) for xep in self.xep])
params['xep'] = ','.join([str(xep) for xep in self.xep])
# Target JID(s) can clash with our existing paths, so we just use comma
# and/or space as a delimiters - %20 = space
@ -307,25 +303,25 @@ class NotifyXMPP(NotifyBase):
self.password if self.password else self.user, privacy,
mode=PrivacyMode.Secret, safe='')
return '{schema}://{auth}@{hostname}{port}/{jids}?{args}'.format(
return '{schema}://{auth}@{hostname}{port}/{jids}?{params}'.format(
auth=auth,
schema=default_schema,
hostname=NotifyXMPP.quote(self.host, safe=''),
# never encode hostname since we're expecting it to be a valid one
hostname=self.host,
port='' if not self.port or self.port == default_port
else ':{}'.format(self.port),
jids=jids,
args=NotifyXMPP.urlencode(args),
params=NotifyXMPP.urlencode(params),
)
@staticmethod
def parse_url(url):
"""
Parses the URL and returns enough arguments that can allow
us to substantiate this object.
us to re-instantiate this object.
"""
results = NotifyBase.parse_url(url)
if not results:
# We're done early as we couldn't load the results
return results

@ -62,7 +62,7 @@ from .NotifyBase import NotifyBase
from ..common import NotifyType
from ..utils import parse_list
from ..utils import validate_regex
from ..utils import GET_EMAIL_RE
from ..utils import is_email
from ..AppriseLocale import gettext_lazy as _
# A Valid Bot Name
@ -260,7 +260,8 @@ class NotifyZulip(NotifyBase):
targets = list(self.targets)
while len(targets):
target = targets.pop(0)
if GET_EMAIL_RE.match(target):
result = is_email(target)
if result:
# Send a private message
payload['type'] = 'private'
else:
@ -268,7 +269,7 @@ class NotifyZulip(NotifyBase):
payload['type'] = 'stream'
# Set our target
payload['to'] = target
payload['to'] = target if not result else result['full_email']
self.logger.debug('Zulip POST URL: %s (cert_verify=%r)' % (
url, self.verify_certificate,
@ -284,6 +285,7 @@ class NotifyZulip(NotifyBase):
headers=headers,
auth=auth,
verify=self.verify_certificate,
timeout=self.request_timeout,
)
if r.status_code != requests.codes.ok:
# We had a problem
@ -312,7 +314,7 @@ class NotifyZulip(NotifyBase):
except requests.RequestException as e:
self.logger.warning(
'A Connection error occured sending Zulip '
'A Connection error occurred sending Zulip '
'notification to {}.'.format(target))
self.logger.debug('Socket Exception: %s' % str(e))
@ -327,12 +329,8 @@ class NotifyZulip(NotifyBase):
Returns the URL built dynamically based on specified arguments.
"""
# Define any arguments set
args = {
'format': self.notify_format,
'overflow': self.overflow_mode,
'verify': 'yes' if self.verify_certificate else 'no',
}
# Our URL parameters
params = self.url_parameters(privacy=privacy, *args, **kwargs)
# simplify our organization in our URL if we can
organization = '{}{}'.format(
@ -341,25 +339,24 @@ class NotifyZulip(NotifyBase):
if self.hostname != self.default_hostname else '')
return '{schema}://{botname}@{org}/{token}/' \
'{targets}?{args}'.format(
'{targets}?{params}'.format(
schema=self.secure_protocol,
botname=NotifyZulip.quote(self.botname, safe=''),
org=NotifyZulip.quote(organization, safe=''),
token=self.pprint(self.token, privacy, safe=''),
targets='/'.join(
[NotifyZulip.quote(x, safe='') for x in self.targets]),
args=NotifyZulip.urlencode(args),
params=NotifyZulip.urlencode(params),
)
@staticmethod
def parse_url(url):
"""
Parses the URL and returns enough arguments that can allow
us to substantiate this object.
us to re-instantiate this object.
"""
results = NotifyBase.parse_url(url)
results = NotifyBase.parse_url(url, verify_host=False)
if not results:
# We're done early as we couldn't load the results
return results

@ -23,17 +23,16 @@
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
import os
import six
import re
import copy
from os import listdir
from os.path import dirname
from os.path import abspath
# Used for testing
from . import NotifyEmail as NotifyEmailBase
from .NotifyGrowl import gntp
from .NotifyXMPP import SleekXmppAdapter
# NotifyBase object is passed in as a module not class
@ -45,6 +44,7 @@ from ..common import NotifyType
from ..common import NOTIFY_TYPES
from ..utils import parse_list
from ..utils import GET_SCHEMA_RE
from ..logger import logger
from ..AppriseLocale import gettext_lazy as _
from ..AppriseLocale import LazyTranslation
@ -62,9 +62,6 @@ __all__ = [
# Tokenizer
'url_to_dict',
# gntp (used for NotifyGrowl Testing)
'gntp',
# sleekxmpp access points (used for NotifyXMPP Testing)
'SleekXmppAdapter',
]
@ -85,7 +82,7 @@ def __load_matrix(path=abspath(dirname(__file__)), name='apprise.plugins'):
# The .py extension is optional as we support loading directories too
module_re = re.compile(r'^(?P<name>Notify[a-z0-9]+)(\.py)?$', re.I)
for f in listdir(path):
for f in os.listdir(path):
match = module_re.match(f)
if not match:
# keep going
@ -131,29 +128,39 @@ def __load_matrix(path=abspath(dirname(__file__)), name='apprise.plugins'):
# Load our module into memory so it's accessible to all
globals()[plugin_name] = plugin
# Load protocol(s) if defined
proto = getattr(plugin, 'protocol', None)
if isinstance(proto, six.string_types):
if proto not in SCHEMA_MAP:
SCHEMA_MAP[proto] = plugin
elif isinstance(proto, (set, list, tuple)):
# Support iterables list types
for p in proto:
if p not in SCHEMA_MAP:
SCHEMA_MAP[p] = plugin
# Load secure protocol(s) if defined
protos = getattr(plugin, 'secure_protocol', None)
if isinstance(protos, six.string_types):
if protos not in SCHEMA_MAP:
SCHEMA_MAP[protos] = plugin
if isinstance(protos, (set, list, tuple)):
# Support iterables list types
for p in protos:
if p not in SCHEMA_MAP:
SCHEMA_MAP[p] = plugin
fn = getattr(plugin, 'schemas', None)
try:
schemas = set([]) if not callable(fn) else fn(plugin)
except TypeError:
# Python v2.x support where functions associated with classes
# were considered bound to them and could not be called prior
# to the classes initialization. This code can be dropped
# once Python v2.x support is dropped. The below code introduces
# replication as it already exists and is tested in
# URLBase.schemas()
schemas = set([])
for key in ('protocol', 'secure_protocol'):
schema = getattr(plugin, key, None)
if isinstance(schema, six.string_types):
schemas.add(schema)
elif isinstance(schema, (set, list, tuple)):
# Support iterables list types
for s in schema:
if isinstance(s, six.string_types):
schemas.add(s)
# map our schema to our plugin
for schema in schemas:
if schema in SCHEMA_MAP:
logger.error(
"Notification schema ({}) mismatch detected - {} to {}"
.format(schema, SCHEMA_MAP[schema], plugin))
continue
# Assign plugin
SCHEMA_MAP[schema] = plugin
return SCHEMA_MAP
@ -452,6 +459,7 @@ def url_to_dict(url):
schema = GET_SCHEMA_RE.match(_url)
if schema is None:
# Not a valid URL; take an early exit
logger.error('Unsupported URL: {}'.format(url))
return None
# Ensure our schema is always in lower case
@ -466,10 +474,28 @@ def url_to_dict(url):
for r in MODULE_MAP.values()
if r['plugin'].parse_native_url(_url) is not None),
None)
if not results:
logger.error('Unparseable URL {}'.format(url))
return None
logger.trace('URL {} unpacked as:{}{}'.format(
url, os.linesep, os.linesep.join(
['{}="{}"'.format(k, v) for k, v in results.items()])))
else:
# Parse our url details of the server object as dictionary
# containing all of the information parsed from our URL
results = SCHEMA_MAP[schema].parse_url(_url)
if not results:
logger.error('Unparseable {} URL {}'.format(
SCHEMA_MAP[schema].service_name, url))
return None
logger.trace('{} URL {} unpacked as:{}{}'.format(
SCHEMA_MAP[schema].service_name, url,
os.linesep, os.linesep.join(
['{}="{}"'.format(k, v) for k, v in results.items()])))
# Return our results
return results

@ -104,40 +104,129 @@ GET_SCHEMA_RE = re.compile(r'\s*(?P<schema>[a-z0-9]{2,9})://.*$', re.I)
# Regular expression based and expanded from:
# http://www.regular-expressions.info/email.html
# Extended to support colon (:) delimiter for parsing names from the URL
# such as:
# - 'Optional Name':user@example.com
# - 'Optional Name' <user@example.com>
#
# The expression also parses the general email as well such as:
# - user@example.com
# - label+user@example.com
GET_EMAIL_RE = re.compile(
r"(?P<fulluser>((?P<label>[^+]+)\+)?"
r"(?P<userid>[a-z0-9$%=_~-]+"
r"(?:\.[a-z0-9$%+=_~-]+)"
r"*))@(?P<domain>(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+"
r"[a-z0-9](?:[a-z0-9-]*"
r"[a-z0-9]))?",
re.IGNORECASE,
)
r'((?P<name>[^:<]+)?[:<\s]+)?'
r'(?P<full_email>((?P<label>[^+]+)\+)?'
r'(?P<email>(?P<userid>[a-z0-9$%=_~-]+'
r'(?:\.[a-z0-9$%+=_~-]+)'
r'*)@(?P<domain>('
r'(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+'
r'[a-z0-9](?:[a-z0-9-]*[a-z0-9]))|'
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*$')
# Regular expression used to destinguish between multiple URLs
URL_DETECTION_RE = re.compile(
r'([a-z0-9]+?:\/\/.*?)[\s,]*(?=$|[a-z0-9]+?:\/\/)', re.I)
r'([a-z0-9]+?:\/\/.*?)(?=$|[\s,]+[a-z0-9]{2,9}?:\/\/)', re.I)
EMAIL_DETECTION_RE = re.compile(
r'[\s,]*([^@]+@.*?)(?=$|[\s,]+'
+ r'(?:[^:<]+?[:<\s]+?)?'
r'[^@\s,]+@[^\s,]+)',
re.IGNORECASE)
# validate_regex() utilizes this mapping to track and re-use pre-complied
# regular expressions
REGEX_VALIDATE_LOOKUP = {}
def is_hostname(hostname):
def is_ipaddr(addr, ipv4=True, ipv6=True):
"""
Validates against IPV4 and IPV6 IP Addresses
"""
if ipv4:
# Based on https://stackoverflow.com/questions/5284147/\
# validating-ipv4-addresses-with-regexp
re_ipv4 = re.compile(
r'^(?P<ip>((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}'
r'(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?))$'
)
match = re_ipv4.match(addr)
if match is not None:
# Return our matched IP
return match.group('ip')
if ipv6:
# Based on https://stackoverflow.com/questions/53497/\
# regular-expression-that-matches-valid-ipv6-addresses
#
# IPV6 URLs should be enclosed in square brackets when placed on a URL
# Source: https://tools.ietf.org/html/rfc2732
# - For this reason, they are additionally checked for existance
re_ipv6 = re.compile(
r'\[?(?P<ip>(([0-9a-f]{1,4}:){7,7}[0-9a-f]{1,4}|([0-9a-f]{1,4}:)'
r'{1,7}:|([0-9a-f]{1,4}:){1,6}:[0-9a-f]{1,4}|([0-9a-f]{1,4}:){1,5}'
r'(:[0-9a-f]{1,4}){1,2}|([0-9a-f]{1,4}:){1,4}'
r'(:[0-9a-f]{1,4}){1,3}|([0-9a-f]{1,4}:){1,3}'
r'(:[0-9a-f]{1,4}){1,4}|([0-9a-f]{1,4}:){1,2}'
r'(:[0-9a-f]{1,4}){1,5}|[0-9a-f]{1,4}:'
r'((:[0-9a-f]{1,4}){1,6})|:((:[0-9a-f]{1,4}){1,7}|:)|'
r'fe80:(:[0-9a-f]{0,4}){0,4}%[0-9a-z]{1,}|::'
r'(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]'
r'|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|'
r'1{0,1}[0-9]){0,1}[0-9])|([0-9a-f]{1,4}:){1,4}:((25[0-5]|'
r'(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|'
r'1{0,1}[0-9]){0,1}[0-9])))\]?', re.I,
)
match = re_ipv6.match(addr)
if match is not None:
# Return our matched IP between square brackets since that is
# required for URL formatting as per RFC 2732.
return '[{}]'.format(match.group('ip'))
# There was no match
return False
def is_hostname(hostname, ipv4=True, ipv6=True):
"""
Validate hostname
"""
if len(hostname) > 255 or len(hostname) == 0:
# The entire hostname, including the delimiting dots, has a maximum of 253
# ASCII characters.
if len(hostname) > 253 or len(hostname) == 0:
return False
# Strip trailling period on hostname (if one exists)
if hostname[-1] == ".":
hostname = hostname[:-1]
allowed = re.compile(r'(?!-)[A-Z\d_-]{1,63}(?<!-)$', re.IGNORECASE)
return all(allowed.match(x) for x in hostname.split("."))
# Split our hostname up
labels = hostname.split(".")
# ipv4 check
if len(labels) == 4 and re.match(r'[0-9.]+', hostname):
return is_ipaddr(hostname, ipv4=ipv4, ipv6=False)
# - RFC 1123 permits hostname labels to start with digits
# - digit must be followed by alpha/numeric so we don't end up
# processing IP addresses here
# - Hostnames can ony be comprised of alpha-numeric characters and the
# hyphen (-) character.
# - Hostnames can not start with the hyphen (-) character.
# - labels can not exceed 63 characters
allowed = re.compile(
r'(?!-)[a-z0-9][a-z0-9-]{1,62}(?<!-)$',
re.IGNORECASE,
)
if not all(allowed.match(x) for x in labels):
return is_ipaddr(hostname, ipv4=ipv4, ipv6=ipv6)
return hostname
def is_email(address):
@ -152,11 +241,33 @@ def is_email(address):
"""
try:
return GET_EMAIL_RE.match(address) is not None
match = GET_EMAIL_RE.match(address)
except TypeError:
# invalid syntax
# not parseable content
return False
if match:
return {
# The name parsed from the URL (if one exists)
'name': '' if match.group('name') is None
else match.group('name').strip(),
# The email address
'email': match.group('email'),
# The full email address (includes label if specified)
'full_email': match.group('full_email'),
# The label (if specified) e.g: label+user@example.com
'label': '' if match.group('label') is None
else match.group('label').strip(),
# The user (which does not include the label) from the email
# parsed.
'user': match.group('userid'),
# The domain associated with the email address
'domain': match.group('domain'),
}
return False
def tidy_path(path):
"""take a filename and or directory and attempts to tidy it up by removing
@ -384,30 +495,22 @@ def parse_url(url, default_schema='http', verify_host=True):
# and it's already assigned
pass
try:
(result['host'], result['port']) = \
re.split(r'[:]+', result['host'])[:2]
except ValueError:
# no problem then, user only exists
# and it's already assigned
pass
if result['port']:
try:
result['port'] = int(result['port'])
except (ValueError, TypeError):
# Invalid Port Specified
# Max port is 65535 so (1,5 digits)
match = re.search(
r'^(?P<host>.+):(?P<port>[1-9][0-9]{0,4})$', result['host'])
if match:
# Separate our port from our hostname (if port is detected)
result['host'] = match.group('host')
result['port'] = int(match.group('port'))
if verify_host:
# Verify and Validate our hostname
result['host'] = is_hostname(result['host'])
if not result['host']:
# Nothing more we can do without a hostname; give the user
# some indication as to what went wrong
return None
if result['port'] == 0:
result['port'] = None
if verify_host and not is_hostname(result['host']):
# Nothing more we can do without a hostname
return None
# Re-assemble cleaned up version of the url
result['url'] = '%s://' % result['schema']
if isinstance(result['user'], six.string_types):
@ -469,26 +572,76 @@ def parse_bool(arg, default=False):
return bool(arg)
def split_urls(urls):
def parse_emails(*args, **kwargs):
"""
Takes a string containing URLs separated by comma's and/or spaces and
returns a list.
"""
try:
results = URL_DETECTION_RE.findall(urls)
# 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)
except TypeError:
results = []
result = []
for arg in args:
if isinstance(arg, six.string_types) and arg:
_result = EMAIL_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))]
if len(results) > 0 and results[len(results) - 1][-1] != urls[-1]:
# we always want to save the end of url URL if we can; This handles
# cases where there is actually a comma (,) at the end of a single URL
# that would have otherwise got lost when our regex passed over it.
results[len(results) - 1] += \
re.match(r'.*?([\s,]+)?$', urls).group(1).rstrip()
elif isinstance(arg, (set, list, tuple)):
# Use recursion to handle the list of Emails
result += parse_emails(*arg, store_unparseable=store_unparseable)
return result
return results
def parse_urls(*args, **kwargs):
"""
Takes a string containing URLs 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_urls(*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 = URL_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 URLs
result += parse_urls(*arg, store_unparseable=store_unparseable)
return result
def parse_list(*args):

@ -1,4 +1,4 @@
apprise=0.8.5
apprise=0.8.8
apscheduler=3.5.1
babelfish=0.5.5
backports.functools-lru-cache=1.5

Loading…
Cancel
Save