Added Mailgun support; refs #101

This commit is contained in:
Chris Caron 2019-04-27 17:41:20 -04:00 committed by GitHub
parent 22735babea
commit 25f5066e27
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 422 additions and 3 deletions

View File

@ -46,6 +46,7 @@ The table below identifies the services this tool supports and some example serv
| [IFTTT](https://github.com/caronc/apprise/wiki/Notify_ifttt) | ifttt:// | (TCP) 443 | ifttt://webhooksID/Event<br />ifttt://webhooksID/Event1/Event2/EventN<br/>ifttt://webhooksID/Event1/?+Key=Value<br/>ifttt://webhooksID/Event1/?-Key=value1
| [Join](https://github.com/caronc/apprise/wiki/Notify_join) | join:// | (TCP) 443 | join://apikey/device<br />join://apikey/device1/device2/deviceN/<br />join://apikey/group<br />join://apikey/groupA/groupB/groupN<br />join://apikey/DeviceA/groupA/groupN/DeviceN/
| [KODI](https://github.com/caronc/apprise/wiki/Notify_kodi) | kodi:// or kodis:// | (TCP) 8080 or 443 | kodi://hostname<br />kodi://user@hostname<br />kodi://user:password@hostname:port
| [Mailgun](https://github.com/caronc/apprise/wiki/Notify_mailgun) | mailgun:// | (TCP) 443 | mailgun://user@hostname/apikey<br />mailgun://user@hostname/apikey/email<br />mailgun://user@hostname/apikey/email1/email2/emailN<br />mailgun://user@hostname/apikey/?name="From%20User"
| [Matrix](https://github.com/caronc/apprise/wiki/Notify_matrix) | matrix:// or matrixs:// | (TCP) 80 or 443 | matrix://hostname<br />matrix://user@hostname<br />matrixs://user:pass@hostname:port/#room_alias<br />matrixs://user:pass@hostname:port/!room_id<br />matrixs://user:pass@hostname:port/#room_alias/!room_id/#room2<br />matrixs://token@hostname:port/?webhook=matrix<br />matrix://user:token@hostname/?webhook=slack&format=markdown
| [Mattermost](https://github.com/caronc/apprise/wiki/Notify_mattermost) | mmost:// | (TCP) 8065 | mmost://hostname/authkey<br />mmost://hostname:80/authkey<br />mmost://user@hostname:80/authkey<br />mmost://hostname/authkey?channel=channel<br />mmosts://hostname/authkey<br />mmosts://user@hostname/authkey<br />
| [Microsoft Teams](https://github.com/caronc/apprise/wiki/Notify_msteams) | msteams:// | (TCP) 443 | msteams://TokenA/TokenB/TokenC/

View File

@ -0,0 +1,341 @@
# -*- 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.
# Signup @ https://www.mailgun.com/
#
# Each domain will have an API key associated with it. If you sign up you'll
# get a sandbox domain to use. Or if you set up your own, they'll have
# api keys associated with them too. Find your API key out by visiting
# https://app.mailgun.com/app/domains
#
# From here you can click on the domain you're interested in. You can acquire
# the API Key from here which will look something like:
# 4b4f2918c6c21ba0a26ad2af73c07f4d-dk5f51da-8f91a0df
#
# You'll also need to know the domain that is associated with your API key.
# This will be obvious with a paid account because it will be the domain name
# you've registered with them. But if you're using a test account, it will
# be name of the sandbox you've set up such as:
# sandbox74bda3414c06kb5acb946.mailgun.org
#
# Knowing this, you can buid your mailgun url as follows:
# mailgun://{user}@{domain}/{apikey}
# mailgun://{user}@{domain}/{apikey}/{email}
#
# You can email as many addresses as you want as:
# mailgun://{user}@{domain}/{apikey}/{email1}/{email2}/{emailN}
#
# The {user}@{domain} effectively assembles the 'from' email address
# the email will be transmitted from. If no email address is specified
# then it will also become the 'to' address as well.
#
import re
import requests
from .NotifyBase import NotifyBase
from ..common import NotifyType
from ..utils import parse_list
from ..utils import is_email
# Used to validate your personal access apikey
VALIDATE_API_KEY = re.compile(r'^[a-z0-9]{32}-[a-z0-9]{8}-[a-z0-9]{8}$', re.I)
# Provide some known codes Mailgun uses and what they translate to:
# Based on https://documentation.mailgun.com/en/latest/api-intro.html#errors
MAILGUN_HTTP_ERROR_MAP = {
400: 'A bad request was made to the server.',
401: 'The provided API Key was not valid.',
402: 'The request failed for a reason out of your control.',
404: 'The requested API query is not valid.',
413: 'Provided attachment is to big.',
}
# Priorities
class MailgunRegion(object):
US = 'us'
EU = 'eu'
# Mailgun APIs
MAILGUN_API_LOOKUP = {
MailgunRegion.US: 'https://api.mailgun.net/v3/',
MailgunRegion.EU: 'https://api.eu.mailgun.net/v3/',
}
# A List of our regions we can use for verification
MAILGUN_REGIONS = (
MailgunRegion.US,
MailgunRegion.EU,
)
class NotifyMailgun(NotifyBase):
"""
A wrapper for Mailgun Notifications
"""
# The default descriptive name associated with the Notification
service_name = 'Mailgun'
# The services URL
service_url = 'https://www.mailgun.com/'
# All pushover requests are secure
secure_protocol = 'mailgun'
# Mailgun advertises they allow 300 requests per minute.
# 60/300 = 0.2
request_rate_per_sec = 0.20
# A URL that takes you to the setup/help of the specific protocol
setup_url = 'https://github.com/caronc/apprise/wiki/Notify_mailgun'
# The default region to use if one isn't otherwise specified
mailgun_default_region = MailgunRegion.US
def __init__(self, apikey, targets, from_name=None, region_name=None,
**kwargs):
"""
Initialize Mailgun Object
"""
super(NotifyMailgun, self).__init__(**kwargs)
try:
# The personal access apikey associated with the account
self.apikey = apikey.strip()
except AttributeError:
# Token was None
msg = 'No API Key was specified.'
self.logger.warning(msg)
raise TypeError(msg)
if not VALIDATE_API_KEY.match(self.apikey):
msg = 'The API Key specified ({}) is invalid.' \
.format(apikey)
self.logger.warning(msg)
raise TypeError(msg)
# Validate our username
if not self.user:
msg = 'No username was specified.'
self.logger.warning(msg)
raise TypeError(msg)
# Parse our targets
self.targets = parse_list(targets)
# Store our region
try:
self.region_name = self.mailgun_default_region \
if region_name is None else region_name.lower()
if self.region_name not in MAILGUN_REGIONS:
# allow the outer except to handle this common response
raise
except:
# Invalid region specified
msg = 'The region specified ({}) is invalid.' \
.format(region_name)
self.logger.warning(msg)
raise TypeError(msg)
# Get our From username (if specified)
self.from_name = from_name
# Get our from email address
self.from_addr = '{user}@{host}'.format(user=self.user, host=self.host)
if not is_email(self.from_addr):
# Parse Source domain based on from_addr
msg = 'Invalid ~From~ email format: {}'.format(self.from_addr)
self.logger.warning(msg)
raise TypeError(msg)
def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
"""
Perform Mailgun Notification
"""
# error tracking (used for function return)
has_error = False
# Prepare our headers
headers = {
'User-Agent': self.app_id,
'Accept': 'application/json',
}
# Prepare our payload
payload = {
'from': '{name} <{addr}>'.format(
name=self.app_id if not self.from_name else self.from_name,
addr=self.from_addr),
'subject': title,
'text': body,
}
# Prepare our URL as it's based on our hostname
url = '{}{}/messages'.format(
MAILGUN_API_LOOKUP[self.region_name], self.host)
# Create a copy of the targets list
emails = list(self.targets)
if len(emails) == 0:
# No email specified; use the from
emails.append(self.from_addr)
while len(emails):
# Get our email to notify
email = emails.pop(0)
# Prepare our user
payload['to'] = '{} <{}>'.format(email, email)
# Some Debug Logging
self.logger.debug('Mailgun POST URL: {} (cert_verify={})'.format(
url, self.verify_certificate))
self.logger.debug('Mailgun Payload: {}' .format(payload))
# Always call throttle before any remote server i/o is made
self.throttle()
try:
r = requests.post(
url,
auth=("api", self.apikey),
data=payload,
headers=headers,
verify=self.verify_certificate,
)
if r.status_code != requests.codes.ok:
# We had a problem
status_str = \
NotifyBase.http_response_code_lookup(
r.status_code, MAILGUN_API_LOOKUP)
self.logger.warning(
'Failed to send Mailgun notification to {}: '
'{}{}error={}.'.format(
email,
status_str,
', ' if status_str else '',
r.status_code))
self.logger.debug(
'Response Details:\r\n{}'.format(r.content))
# Mark our failure
has_error = True
continue
else:
self.logger.info(
'Sent Mailgun notification to {}.'.format(email))
except requests.RequestException as e:
self.logger.warning(
'A Connection error occured sending Mailgun:%s ' % (
email) + 'notification.'
)
self.logger.debug('Socket Exception: %s' % str(e))
# Mark our failure
has_error = True
continue
return not has_error
def url(self):
"""
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',
'region': self.region_name,
}
if self.from_name is not None:
# from_name specified; pass it back on the url
args['name'] = self.from_name
return '{schema}://{user}@{host}/{apikey}/{targets}/?{args}'.format(
schema=self.secure_protocol,
host=self.host,
user=NotifyMailgun.quote(self.user, safe=''),
apikey=NotifyMailgun.quote(self.apikey, safe=''),
targets='/'.join(
[NotifyMailgun.quote(x, safe='') for x in self.targets]),
args=NotifyMailgun.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
# Get our entries; split_path() looks after unquoting content for us
# by default
results['targets'] = NotifyMailgun.split_path(results['fullpath'])
# Our very first entry is reserved for our api key
try:
results['apikey'] = results['targets'].pop(0)
except IndexError:
# We're done - no API Key found
results['apikey'] = None
if 'name' in results['qsd'] and len(results['qsd']['name']):
# Extract from name to associate with from address
results['from_name'] = \
NotifyMailgun.unquote(results['qsd']['name'])
if 'region' in results['qsd'] and len(results['qsd']['region']):
# Extract from name to associate with from address
results['region_name'] = \
NotifyMailgun.unquote(results['qsd']['region'])
# Support the 'to' variable so that we can support targets this way too
# The 'to' makes it easier to use yaml configuration
if 'to' in results['qsd'] and len(results['qsd']['to']):
results['targets'] += \
NotifyMailgun.parse_list(results['qsd']['to'])
return results

View File

@ -48,7 +48,7 @@ notification services that are out there. Apprise opens the door and makes
it easy to access:
Boxcar, Discord, E-Mail, Emby, Faast, Flock, Gitter, Gotify, Growl, IFTTT,
Join, KODI, MatterMost, Matrix, Microsoft Windows Notifications,
Join, KODI, Mailgun, MatterMost, Matrix, Microsoft Windows Notifications,
Microsoft Teams, Notify My Android, Prowl, Pushalot, PushBullet, Pushjet,
Pushover, Rocket.Chat, Slack, Super Toasty, Stride, Telegram, Twitter, XBMC,
XMPP, Webex Teams}

View File

@ -57,8 +57,8 @@ setup(
long_description_content_type='text/markdown',
url='https://github.com/caronc/apprise',
keywords='Push Notifications Alerts Email AWS SNS Boxcar Discord Dbus '
'Emby Faast Flock Gitter Gnome Gotify Growl IFTTT Join KODI Matrix '
'Mattermost Matrix Prowl PushBullet Pushjet Pushed Pushover '
'Emby Faast Flock Gitter Gnome Gotify Growl IFTTT Join KODI Mailgun '
'Matrix Mattermost Prowl PushBullet Pushjet Pushed Pushover '
'Rocket.Chat Ryver Slack Stride Telegram Twitter XBMC Microsoft '
'Windows Webex CLI API',
author='Chris Caron',

View File

@ -757,6 +757,83 @@ TEST_URLS = (
'test_requests_exceptions': True,
}),
##################################
# NotifyMailgun
##################################
('mailgun://', {
'instance': None,
}),
('mailgun://:@/', {
'instance': None,
}),
# No Token specified
('mailgun://user@host', {
'instance': TypeError,
}),
# Token specified but it's invalid
('mailgun://user@host/{}'.format('a' * 12), {
'instance': TypeError,
}),
# Token is valid, but no user name specified
('mailgun://host/{}-{}-{}'.format('a' * 32, 'b' * 8, 'c' * 8), {
'instance': TypeError,
}),
# Invalid from email address
('mailgun://!@host/{}-{}-{}'.format('a' * 32, 'b' * 8, 'c' * 8), {
'instance': TypeError,
}),
# No To email address, but everything else is valid
('mailgun://user@host/{}-{}-{}'.format('a' * 32, 'b' * 8, 'c' * 8), {
'instance': plugins.NotifyMailgun,
}),
# valid url with region specified (case insensitve)
('mailgun://user@host/{}-{}-{}?region=uS'.format(
'a' * 32, 'b' * 8, 'c' * 8), {
'instance': plugins.NotifyMailgun,
}),
# valid url with region specified (case insensitve)
('mailgun://user@host/{}-{}-{}?region=EU'.format(
'a' * 32, 'b' * 8, 'c' * 8), {
'instance': plugins.NotifyMailgun,
}),
# invalid url with region specified (case insensitve)
('mailgun://user@host/{}-{}-{}?region=invalid'.format(
'a' * 32, 'b' * 8, 'c' * 8), {
'instance': TypeError,
}),
# One To Email address
('mailgun://user@host/{}-{}-{}/test@example.com'.format(
'a' * 32, 'b' * 8, 'c' * 8), {
'instance': plugins.NotifyMailgun,
}),
('mailgun://user@host/{}-{}-{}?to=test@example.com'.format(
'a' * 32, 'b' * 8, 'c' * 8), {
'instance': plugins.NotifyMailgun,
}),
# One To Email address, a from name specified too
('mailgun://user@host/{}-{}-{}/test@example.com?name="Frodo"'.format(
'a' * 32, 'b' * 8, 'c' * 8), {
'instance': plugins.NotifyMailgun,
}),
('mailgun://user@host/{}-{}-{}'.format('a' * 32, 'b' * 8, 'c' * 8), {
'instance': plugins.NotifyMailgun,
# force a failure
'response': False,
'requests_response_code': requests.codes.internal_server_error,
}),
('mailgun://user@host/{}-{}-{}'.format('a' * 32, 'b' * 8, 'c' * 8), {
'instance': plugins.NotifyMailgun,
# throw a bizzare code forcing us to fail to look it up
'response': False,
'requests_response_code': 999,
}),
('mailgun://user@host/{}-{}-{}'.format('a' * 32, 'b' * 8, 'c' * 8), {
'instance': plugins.NotifyMailgun,
# Throws a series of connection and transfer exceptions when this flag
# is set and tests that we gracfully handle them
'test_requests_exceptions': True,
}),
##################################
# NotifyMatrix
##################################