diff --git a/apprise/plugins/email/pgp.py b/apprise/plugins/email/pgp.py deleted file mode 100644 index fa446664..00000000 --- a/apprise/plugins/email/pgp.py +++ /dev/null @@ -1,354 +0,0 @@ -# -*- coding: utf-8 -*- -# BSD 2-Clause License -# -# Apprise - Push Notification Library. -# Copyright (c) 2024, Chris Caron -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are met: -# -# 1. Redistributions of source code must retain the above copyright notice, -# this list of conditions and the following disclaimer. -# -# 2. Redistributions in binary form must reproduce the above copyright notice, -# this list of conditions and the following disclaimer in the documentation -# and/or other materials provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE -# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. - -import os -import hashlib -from datetime import datetime -from datetime import timedelta -from datetime import timezone - -from ...asset import AppriseAsset -from ...apprise_attachment import AppriseAttachment -from ...logger import logger -from ...exception import ApprisePluginException - -try: - import pgpy - # Pretty Good Privacy (PGP) Support enabled - PGP_SUPPORT = True - -except ImportError: - # Pretty Good Privacy (PGP) Support disabled - PGP_SUPPORT = False - - -class ApprisePGPException(ApprisePluginException): - """ - Thrown when there is an error with the Pretty Good Privacy Controller - """ - def __init__(self, message, error_code=602): - super().__init__(message, error_code=error_code) - - -class ApprisePGPController: - """ - Pretty Good Privacy Controller Tool for the Apprise Library - """ - - # There is no reason a PGP Public Key should exceed 8K in size - # If it is more than this, then it is not accepted - max_pgp_public_key_size = 8000 - - def __init__(self, path, pub_keyfile=None, email=None, asset=None, - **kwargs): - """ - Path should be the directory keys can be written and read from such as - .store.path - - Optionally additionally specify a keyfile to explicitly open - """ - - # PGP hash - self.__key_lookup = {} - - # Directory we can work with - self.path = path - - # Our email - self.email = email - - # Prepare our Asset Object - self.asset = \ - asset if isinstance(asset, AppriseAsset) else AppriseAsset() - - if pub_keyfile: - # Create ourselves an Attachment to work with; this grants us the - # ability to pull this key from a remote site or anything else - # supported by the Attachment object - self._pub_keyfile = AppriseAttachment(asset=self.asset) - - # Add our definition to our pgp_key reference - self._pub_keyfile.add(pub_keyfile) - - # Enforce maximum file size - self._pub_keyfile[0].max_file_size = self.max_pgp_public_key_size - - else: - self._pub_keyfile = None - - def keygen(self, email=None, name=None, force=False): - """ - Generates a set of keys based on email configured. - """ - - try: - # Create a new RSA key pair with 2048-bit strength - key = pgpy.PGPKey.new( - pgpy.constants.PubKeyAlgorithm.RSAEncryptOrSign, 2048) - - except NameError: - # PGPy not installed - logger.debug('PGPy not installed; keygen disabled') - return False - - if self._pub_keyfile is not None or not self.path: - logger.trace( - 'PGP keygen disabled, reason=%s', - 'keyfile-defined' if self._pub_keyfile is not None - else 'no-write-path') - return False - - if not name: - name = self.asset.app_id - - if not email: - email = self.email - - # Prepare our UID - uid = pgpy.PGPUID.new(name, email=email) - - # Filenames - file_prefix = email.split('@')[0].lower() - - pub_path = os.path.join(self.path, f'{file_prefix}-pub.asc') - prv_path = os.path.join(self.path, f'{file_prefix}-prv.asc') - - if os.path.isfile(pub_path) and not force: - logger.debug( - 'PGP generation skipped; Public Key already exists: %s', - pub_path) - return True - - # Persistent Storage Key - lookup_key = hashlib.sha1( - os.path.abspath(pub_path).encode('utf-8')).hexdigest() - if lookup_key in self.__key_lookup: - # Ensure our key no longer exists - del self.__key_lookup[lookup_key] - - # Add the user ID to the key - key.add_uid(uid, usage={ - pgpy.constants.KeyFlags.Sign, - pgpy.constants.KeyFlags.EncryptCommunications}, - hashes=[pgpy.constants.HashAlgorithm.SHA256], - ciphers=[pgpy.constants.SymmetricKeyAlgorithm.AES256], - compression=[pgpy.constants.CompressionAlgorithm.ZLIB]) - - try: - # Write our keys to disk - with open(pub_path, 'w') as f: - f.write(str(key.pubkey)) - - except OSError as e: - logger.warning('Error writing PGP file %s', pub_path) - logger.debug(f'I/O Exception: {e}') - - # Cleanup - try: - os.unlink(pub_path) - logger.trace('Removed %s', pub_path) - - except OSError: - pass - - try: - with open(prv_path, 'w') as f: - f.write(str(key)) - - except OSError as e: - logger.warning('Error writing PGP file %s', prv_path) - logger.debug(f'I/O Exception: {e}') - - try: - os.unlink(pub_path) - logger.trace('Removed %s', pub_path) - - except OSError: - pass - - try: - os.unlink(prv_path) - logger.trace('Removed %s', prv_path) - - except OSError: - pass - - return False - - logger.info( - 'Wrote PGP Keys for %s/%s', - os.path.dirname(pub_path), - os.path.basename(pub_path)) - return True - - def public_keyfile(self, *emails): - """ - Returns the first match of a useable public key based emails provided - """ - - if not PGP_SUPPORT: - msg = 'PGP Support unavailable; install PGPy library' - logger.warning(msg) - raise ApprisePGPException(msg) - - if self._pub_keyfile is not None: - # If our code reaches here, then we fetch our public key - pgp_key = self._pub_keyfile[0] - if not pgp_key: - # We could not access the attachment - logger.error( - 'Could not access PGP Public Key {}.'.format( - pgp_key.url(privacy=True))) - return False - - return pgp_key.path - - elif not self.path: - # No path - return None - - fnames = [ - 'pgp-public.asc', - 'pgp-pub.asc', - 'public.asc', - 'pub.asc', - ] - - if self.email: - # Include our email in the list - emails = [self.email] + [*emails] - - for email in emails: - _entry = email.split('@')[0].lower() - fnames.insert(0, f'{_entry}-pub.asc') - - # Lowercase email (Highest Priority) - _entry = email.lower() - fnames.insert(0, f'{_entry}-pub.asc') - - return next( - (os.path.join(self.path, fname) - for fname in fnames - if os.path.isfile(os.path.join(self.path, fname))), - None) - - def public_key(self, *emails, autogen=None): - """ - Opens a spcified pgp public file and returns the key from it which - is used to encrypt the message - """ - path = self.public_keyfile(*emails) - if not path: - if (autogen if autogen is not None else self.asset.pgp_autogen) \ - and self.keygen(*emails): - path = self.public_keyfile(*emails) - if path: - # We should get a hit now - return self.public_key(*emails) - - logger.warning('No PGP Public Key could be loaded') - return None - - # Persistent Storage Key - key = hashlib.sha1( - os.path.abspath(path).encode('utf-8')).hexdigest() - if key in self.__key_lookup: - # Take an early exit - return self.__key_lookup[key]['public_key'] - - try: - with open(path, 'r') as key_file: - public_key, _ = pgpy.PGPKey.from_blob(key_file.read()) - - except NameError: - # PGPy not installed - logger.debug( - 'PGPy not installed; skipping PGP support: %s', path) - return None - - except FileNotFoundError: - # Generate keys - logger.debug('PGP Public Key file not found: %s', path) - return None - - except OSError as e: - logger.warning('Error accessing PGP Public Key file %s', path) - logger.debug(f'I/O Exception: {e}') - return None - - self.__key_lookup[key] = { - 'public_key': public_key, - 'expires': - datetime.now(timezone.utc) + timedelta(seconds=86400) - } - return public_key - - # Encrypt message using the recipient's public key - def encrypt(self, message, *emails): - """ - If provided a path to a pgp-key, content is encrypted - """ - - # Acquire our key - public_key = self.public_key(*emails) - if not public_key: - # Encryption not possible - return False - - try: - message_object = pgpy.PGPMessage.new(message) - encrypted_message = public_key.encrypt(message_object) - return str(encrypted_message) - - except pgpy.errors.PGPError: - # Encryption not Possible - logger.debug( - 'PGP Public Key Corruption; encryption not possible') - - except NameError: - # PGPy not installed - logger.debug('PGPy not installed; Skipping PGP encryption') - - return None - - def prune(self): - """ - Prunes old entries from the public_key index - """ - self.__key_lookup = { - key: value for key, value in self.__key_lookup.items() - if value['expires'] > datetime.now(timezone.utc)} - - @property - def pub_keyfile(self): - """ - Returns the Public Keyfile Path if set otherwise it returns None - This property returns False if a keyfile was provided, but was invalid - """ - return None if self._pub_keyfile is None else ( - False if not self._pub_keyfile[0] else self._pub_keyfile[0].path) diff --git a/apprise/plugins/gmail.py b/apprise/plugins/gmail.py index 2f540537..f87d9d8d 100644 --- a/apprise/plugins/gmail.py +++ b/apprise/plugins/gmail.py @@ -37,9 +37,7 @@ from email.utils import formataddr from ..url import PrivacyMode from ..common import NotifyFormat from ..common import NotifyType -from ..utils import is_email -from ..utils import parse_emails -from ..utils import validate_regex +from ..utils.parse import is_email, parse_emails, validate_regex from ..locale import gettext_lazy as _ from ..common import PersistentStoreMode from . import email as _email