From 6e1b8a0bd64b162442ff8b78b20034da504efa19 Mon Sep 17 00:00:00 2001 From: Chris Caron Date: Tue, 18 Aug 2020 14:05:29 -0400 Subject: [PATCH] Advanced email parsing added; eg: Full Name email@domain.com (#276) --- apprise/Apprise.py | 4 +- apprise/plugins/NotifyEmail.py | 138 ++++++++++---- apprise/plugins/NotifyOffice365.py | 187 ++++++++++++++++--- apprise/plugins/NotifyPopcornNotify.py | 6 +- apprise/plugins/NotifyPushBullet.py | 25 ++- apprise/plugins/NotifySendGrid.py | 32 ++-- apprise/plugins/NotifyTwist.py | 41 ++-- apprise/plugins/NotifyZulip.py | 7 +- apprise/plugins/__init__.py | 2 +- apprise/utils.py | 127 +++++++++++-- test/test_api.py | 6 + test/test_email_plugin.py | 23 ++- test/test_office365.py | 38 +++- test/test_utils.py | 247 +++++++++++++++++++++++-- 14 files changed, 718 insertions(+), 165 deletions(-) diff --git a/apprise/Apprise.py b/apprise/Apprise.py index 06a0b9d9..b95da22a 100644 --- a/apprise/Apprise.py +++ b/apprise/Apprise.py @@ -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 @@ -197,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 diff --git a/apprise/plugins/NotifyEmail.py b/apprise/plugins/NotifyEmail.py index 2b5c6c18..604fc5b5 100644 --- a/apprise/plugins/NotifyEmail.py +++ b/apprise/plugins/NotifyEmail.py @@ -41,8 +41,7 @@ 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. @@ -401,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() @@ -410,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: @@ -422,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 = \ @@ -446,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 - if GET_EMAIL_RE.match(recipient): - self.cc.add(recipient) + self.logger.warning( + 'Dropped invalid To email ' + '({}) specified.'.format(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( @@ -459,10 +487,14 @@ class NotifyEmail(NotifyBase): ) # Validate recipients (bcc:) and drop bad ones: - for recipient in parse_list(bcc): + for recipient in parse_emails(bcc): + email = is_email(recipient) + if email: + self.bcc.add(email['full_email']) - if GET_EMAIL_RE.match(recipient): - self.bcc.add(recipient) + # Index our name (if one exists) + self.names[email['full_email']] = \ + email['name'] if email['name'] else False continue self.logger.warning( @@ -556,29 +588,51 @@ class NotifyEmail(NotifyBase): # 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( @@ -597,13 +651,13 @@ class NotifyEmail(NotifyBase): base['From'] = formataddr( (from_name if from_name else False, self.from_addr), charset='utf-8') - base['To'] = formataddr((False, to_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((False, to_addr)) + base['To'] = formataddr((to_name, to_addr)) base['Cc'] = ','.join(cc) base['Date'] = \ @@ -706,7 +760,6 @@ class NotifyEmail(NotifyBase): # Define an URL parameters params = { 'from': self.from_addr, - 'name': self.from_name, 'mode': self.secure_mode, 'smtp': self.smtp_host, 'timeout': self.timeout, @@ -716,13 +769,22 @@ class NotifyEmail(NotifyBase): # 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 - params['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 - params['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] @@ -748,7 +810,8 @@ 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}?{params}'.format( schema=self.secure_protocol if self.secure else self.protocol, @@ -758,7 +821,9 @@ class NotifyEmail(NotifyBase): 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]), + [NotifyEmail.quote('{}{}'.format( + '' if not e[0] else '{}:'.format(e[0]), e[1]), + safe='') for e in self.targets]), params=NotifyEmail.urlencode(params), ) @@ -792,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 @@ -814,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 diff --git a/apprise/plugins/NotifyOffice365.py b/apprise/plugins/NotifyOffice365.py index b7cef03a..5c8ea934 100644 --- a/apprise/plugins/NotifyOffice365.py +++ b/apprise/plugins/NotifyOffice365.py @@ -66,7 +66,7 @@ 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 parse_emails from ..utils import validate_regex from ..AppriseLocale import gettext_lazy as _ @@ -152,6 +152,14 @@ class NotifyOffice365(NotifyBase): 'to': { 'alias_of': 'targets', }, + 'cc': { + 'name': _('Carbon Copy'), + 'type': 'list:string', + }, + 'bcc': { + 'name': _('Blind Carbon Copy'), + 'type': 'list:string', + }, 'oauth_id': { 'alias_of': 'client_id', }, @@ -161,7 +169,7 @@ class NotifyOffice365(NotifyBase): }) def __init__(self, tenant, email, client_id, secret, - targets=None, **kwargs): + targets=None, cc=None, bcc=None, **kwargs): """ Initialize Office 365 Object """ @@ -176,12 +184,15 @@ class NotifyOffice365(NotifyBase): self.logger.warning(msg) raise TypeError(msg) - if not is_email(email): + result = is_email(email) + if not result: msg = 'An invalid Office 365 Email Account ID' \ '({}) was specified.'.format(email) self.logger.warning(msg) raise TypeError(msg) - self.email = email + + # Otherwise store our the email address + self.email = result['full_email'] # Client Key (associated with generated OAuth2 Login) self.client_id = validate_regex( @@ -200,23 +211,68 @@ class NotifyOffice365(NotifyBase): self.logger.warning(msg) raise TypeError(msg) + # For tracking our email -> name lookups + self.names = {} + + # Acquire Carbon Copies + self.cc = set() + + # Acquire Blind Carbon Copies + self.bcc = set() + # Parse our targets self.targets = list() - targets = parse_list(targets) if targets: - for target in targets: - # Validate targets and drop bad ones: - if not is_email(target): - self.logger.warning( - 'Dropped invalid email specified: {}'.format(target)) + for recipient in parse_emails(targets): + # Validate recipients (to:) and drop bad ones: + result = is_email(recipient) + if result: + # Add our email to our target list + self.targets.append( + (result['name'] if result['name'] else False, + result['full_email'])) continue - # Add our email to our target list - self.targets.append(target) + self.logger.warning( + 'Dropped invalid To email ({}) specified.' + .format(recipient)) + else: - # Default to adding ourselves - self.targets.append(self.email) + # If our target email list is empty we want to add ourselves to it + self.targets.append((False, self.email)) + + # 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( + 'Dropped invalid Carbon Copy email ' + '({}) specified.'.format(recipient), + ) + + # Validate recipients (bcc:) and drop bad ones: + 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( + 'Dropped invalid Blind Carbon Copy email ' + '({}) specified.'.format(recipient), + ) # Our token is acquired upon a successful login self.token = None @@ -237,7 +293,7 @@ class NotifyOffice365(NotifyBase): if not self.targets: # There is no one to email; we're done self.logger.warning( - 'There are no Office 365 recipients to notify') + 'There are no Email recipients to notify') return False # Setup our Content Type @@ -256,8 +312,8 @@ class NotifyOffice365(NotifyBase): 'SaveToSentItems': 'false' } - # Create a copy of the targets list - targets = list(self.targets) + # Create a copy of the email list + emails = list(self.targets) # Define our URL to post to url = '{graph_url}/v1.0/users/{email}/sendmail'.format( @@ -265,17 +321,7 @@ class NotifyOffice365(NotifyBase): graph_url=self.graph_url, ) - while len(targets): - # Get our target to notify - target = targets.pop(0) - - # Prepare our email - payload['Message']['ToRecipients'] = [{ - 'EmailAddress': { - 'Address': target - } - }] - + while len(emails): # authenticate ourselves if we aren't already; but this function # also tracks if our token we have is still valid and will # re-authenticate ourselves if nessisary. @@ -283,9 +329,68 @@ class NotifyOffice365(NotifyBase): # We could not authenticate ourselves; we're done return False + # Get our email to notify + 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])) + + # Prepare our email + payload['Message']['ToRecipients'] = [{ + 'EmailAddress': { + 'Address': to_addr + } + }] + if to_name: + # Apply our To Name + payload['Message']['ToRecipients'][0]['EmailAddress']['Name'] \ + = to_name + + self.logger.debug('Email To: {}'.format(to_addr)) + + if cc: + # Prepare our CC list + payload['Message']['CcRecipients'] = [] + for addr in cc: + _payload = {'Address': addr} + if self.names.get(addr): + _payload['Name'] = self.names[addr] + + # Store our address in our payload + payload['Message']['CcRecipients']\ + .append({'EmailAddress': _payload}) + + self.logger.debug('Email Cc: {}'.format(', '.join( + ['{}{}'.format( + '' if self.names.get(e) + else '{}: '.format(self.names[e]), e) for e in cc]))) + + if bcc: + # Prepare our CC list + payload['Message']['BccRecipients'] = [] + for addr in bcc: + _payload = {'Address': addr} + if self.names.get(addr): + _payload['Name'] = self.names[addr] + + # Store our address in our payload + payload['Message']['BccRecipients']\ + .append({'EmailAddress': _payload}) + + self.logger.debug('Email Bcc: {}'.format(', '.join( + ['{}{}'.format( + '' if self.names.get(e) + else '{}: '.format(self.names[e]), e) for e in bcc]))) + + # Perform upstream fetch postokay, response = self._fetch( url=url, payload=dumps(payload), content_type='application/json') + + # Test if we were okay if not postokay: has_error = True @@ -453,6 +558,20 @@ class NotifyOffice365(NotifyBase): # Our URL parameters params = self.url_parameters(privacy=privacy, *args, **kwargs) + if self.cc: + # Handle our Carbon Copy Addresses + params['cc'] = ','.join( + ['{}{}'.format( + '' if not self.names.get(e) + else '{}:'.format(self.names[e]), e) for e in self.cc]) + + if self.bcc: + # Handle our Blind Carbon Copy Addresses + params['bcc'] = ','.join( + ['{}{}'.format( + '' if not self.names.get(e) + else '{}:'.format(self.names[e]), e) for e in self.bcc]) + return '{schema}://{tenant}:{email}/{client_id}/{secret}' \ '/{targets}/?{params}'.format( schema=self.secure_protocol, @@ -465,7 +584,9 @@ class NotifyOffice365(NotifyBase): self.secret, privacy, mode=PrivacyMode.Secret, safe=''), targets='/'.join( - [NotifyOffice365.quote(x, safe='') for x in self.targets]), + [NotifyOffice365.quote('{}{}'.format( + '' if not e[0] else '{}:'.format(e[0]), e[1]), + safe='') for e in self.targets]), params=NotifyOffice365.urlencode(params)) @staticmethod @@ -572,4 +693,12 @@ class NotifyOffice365(NotifyBase): results['targets'] += \ NotifyOffice365.parse_list(results['qsd']['to']) + # Handle Carbon Copy Addresses + if 'cc' in results['qsd'] and len(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'] = results['qsd']['bcc'] + return results diff --git a/apprise/plugins/NotifyPopcornNotify.py b/apprise/plugins/NotifyPopcornNotify.py index 164078be..81791518 100644 --- a/apprise/plugins/NotifyPopcornNotify.py +++ b/apprise/plugins/NotifyPopcornNotify.py @@ -28,7 +28,7 @@ import requests from .NotifyBase import NotifyBase from ..common import NotifyType -from ..utils import GET_EMAIL_RE +from ..utils import is_email from ..utils import parse_list from ..utils import parse_bool from ..utils import validate_regex @@ -142,10 +142,10 @@ class NotifyPopcornNotify(NotifyBase): self.targets.append(result) continue - result = GET_EMAIL_RE.match(target) + result = is_email(target) if result: # store valid email - self.targets.append(target) + self.targets.append(result['full_email']) continue self.logger.warning( diff --git a/apprise/plugins/NotifyPushBullet.py b/apprise/plugins/NotifyPushBullet.py index 78e9168c..9bae32f9 100644 --- a/apprise/plugins/NotifyPushBullet.py +++ b/apprise/plugins/NotifyPushBullet.py @@ -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) diff --git a/apprise/plugins/NotifySendGrid.py b/apprise/plugins/NotifySendGrid.py index 20234295..12f829fb 100644 --- a/apprise/plugins/NotifySendGrid.py +++ b/apprise/plugins/NotifySendGrid.py @@ -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( diff --git a/apprise/plugins/NotifyTwist.py b/apprise/plugins/NotifyTwist.py index c7c19684..39bec5ea 100644 --- a/apprise/plugins/NotifyTwist.py +++ b/apprise/plugins/NotifyTwist.py @@ -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): # : 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 + # Initialize our Email Object + self.email = email if email else '{}@{}'.format( + self.user, + self.host, + ) - 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): + # 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) diff --git a/apprise/plugins/NotifyZulip.py b/apprise/plugins/NotifyZulip.py index 016c44b6..2290efb0 100644 --- a/apprise/plugins/NotifyZulip.py +++ b/apprise/plugins/NotifyZulip.py @@ -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, diff --git a/apprise/plugins/__init__.py b/apprise/plugins/__init__.py index cdf8836a..ceefba95 100644 --- a/apprise/plugins/__init__.py +++ b/apprise/plugins/__init__.py @@ -449,7 +449,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)) + logger.error('Unsupported URL: {}'.format(url)) return None # Ensure our schema is always in lower case diff --git a/apprise/utils.py b/apprise/utils.py index 1e482747..8d092007 100644 --- a/apprise/utils.py +++ b/apprise/utils.py @@ -104,16 +104,24 @@ GET_SCHEMA_RE = re.compile(r'\s*(?P[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' +# +# 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((?P