diff --git a/.github/workflows/run-unittest.yml b/.github/workflows/run-unittest.yml index 064cca9..a0698b9 100644 --- a/.github/workflows/run-unittest.yml +++ b/.github/workflows/run-unittest.yml @@ -5,7 +5,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ["3.10", 3.11, 3.12] + python-version: ["3.10", 3.11, 3.12, 3.13] steps: - uses: actions/checkout@v2 - name: Set up Python ${{ matrix.python-version }} @@ -15,7 +15,8 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - pip install M2Crypto + pip install M2Crypto # TODO remove when tests are updated + pip install cryptography pip install -e . - name: Run tests run: python3 test_.py @@ -26,7 +27,7 @@ jobs: run: pip uninstall python-magic -y - name: Test libmagic missing id: should_fail - run: python3 tests.py TestMime.test_libmagic + run: python3 test_.py TestMime.test_libmagic continue-on-error: true - name: Check on failures if: steps.should_fail.outcome != 'failure' @@ -34,4 +35,4 @@ jobs: - name: Install file-magic run: pip install file-magic - name: Test file-magic - run: python3 test_.py TestMime.test_libmagic + run: python3 test_.py TestMime.test_libmagic \ No newline at end of file diff --git a/README.md b/README.md index a649ae4..53ac971 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Envelope -[![Build Status](https://github.com/CZ-NIC/envelope/actions/workflows/run-unittest.yml/badge.svg)](https://github.com/CZ-NIC/envelope/actions) [![Downloads](https://pepy.tech/badge/envelope)](https://pepy.tech/project/envelope) +[![Build Status](https://github.com/CZ-NIC/envelope/actions/workflows/run-unittest.yml/badge.svg)](https://github.com/CZ-NIC/envelope/actions) [![Downloads](https://static.pepy.tech/badge/envelope)](https://pepy.tech/project/envelope) Quick layer over [python-gnupg](https://bitbucket.org/vinay.sajip/python-gnupg/src), [M2Crypto](https://m2crypto.readthedocs.io/), [smtplib](https://docs.python.org/3/library/smtplib.html), [magic](https://pypi.org/project/python-magic/) and [email](https://docs.python.org/3/library/email.html?highlight=email#module-email) handling packages. Their common use cases merged into a single function. Want to sign a text and tired of forgetting how to do it right? You do not need to know everything about GPG or S/MIME, you do not have to bother with importing keys. Do not hassle with reconnecting to an SMTP server. Do not study various headers meanings to let your users unsubscribe via a URL. You insert a message, attachments and inline images and receive signed and/or encrypted output to the file or to your recipients' e-mail. @@ -77,7 +77,7 @@ Envelope.load(path="message.eml").attachments() ``` * Or just download the project and launch `python3 -m envelope` * If planning to sign/encrypt with GPG, assure you have it on the system with `sudo apt install gpg` and possibly see [Configure your GPG](#configure-your-gpg) tutorial. -* If planning to use S/MIME, you should ensure some prerequisites: `sudo apt install swig build-essential python3-dev libssl-dev && pip3 install M2Crypto` +* If planning to use S/MIME, you might be required to ensure some [prerequisites](https://cryptography.io/en/latest/installation/), ex: `sudo apt install build-essential libssl-dev libffi-dev python3-dev cargo pkg-config` * If planning to send e-mails, prepare SMTP credentials or visit [Configure your SMTP](#configure-your-smtp) tutorial. * If your e-mails are to be received outside your local domain, visit [DMARC](#dmarc) section. * Package [python-magic](https://pypi.org/project/python-magic/) is used as a dependency. Due to a [well-known](https://github.com/ahupp/python-magic/blob/master/COMPAT.md) name clash with the [file-magic](https://pypi.org/project/file-magic/) package, in case you need to use the latter, don't worry to run `pip uninstall python-magic && pip install file-magic` after installing envelope which is fully compatible with both projects. Both use `libmagic` under the hood which is probably already installed. However, if it is not, [install](https://github.com/ahupp/python-magic?tab=readme-ov-file#installation) `sudo apt install libmagic1`. diff --git a/envelope/address.py b/envelope/address.py index ecae05e..25b5e96 100644 --- a/envelope/address.py +++ b/envelope/address.py @@ -2,6 +2,7 @@ import logging from os import environ import re +import sys from .utils import assure_list environ['PY3VE_IGNORE_UPDATER'] = '1' @@ -11,6 +12,23 @@ logger = logging.getLogger(__name__) +def _getaddresses(*args): + # NOTE Python finally changed the old way of parsing wrong addresses. + # We might start using strict=True (default) in the future. + if sys.version_info <= (3, 11): + return getaddresses(*args) + return getaddresses(*args, strict=False) + + +def _parseaddr(*args): + # NOTE Python finally changed the old way of parsing wrong addresses. + # We might start using strict=True (default) in the future. + # README should reflect that. + if sys.version_info <= (3, 11): + return parseaddr(*args) + return parseaddr(*args, strict=False) + + class Address(str): """ You can safely access the `self.name` property to access the real name and `self.address` to access the e-mail address. @@ -37,12 +55,14 @@ class Address(str): def __new__(cls, displayed_email=None, name=None, address=None): if displayed_email: - v = parseaddr(cls.remedy(displayed_email)) + v = _parseaddr(cls.remedy(displayed_email)) name, address = v[0] or name, v[1] or address + if name: displayed_email = f"{name} <{address}>" else: displayed_email = address + instance = super().__new__(cls, displayed_email or "") instance._name, instance._address = name or "", address or "" return instance @@ -143,7 +163,7 @@ def parse(cls, email_or_list, single=False, allow_false=False): if allow_false and email_or_list is False: return False - addrs = getaddresses(cls.remedy(x) for x in assure_list(email_or_list)) + addrs = _getaddresses(cls.remedy(x) for x in assure_list(email_or_list)) addresses = [Address(name=real_name, address=address) for real_name, address in addrs if not (real_name == address == "")] if single: @@ -153,6 +173,8 @@ def parse(cls, email_or_list, single=False, allow_false=False): return addresses[0] # if len(addresses) == 0: # raise ValueError(f"E-mail address cannot be parsed: {email_or_list}") + # if len(addresses) == 0: + # return email_or_list return addresses @classmethod @@ -162,6 +184,12 @@ def remedy(s): parsed as two distinguish addresses with getaddresses. Rename the at-sign in the display name to "person--AT--example.com " so that the result of getaddresses is less wrong. """ + + """ + What happens when the string have more addresses? + It also needs to get the address from string like "person@example.com, " so we need to + take care of the comma and semicolon as well. + """ if s.group(1).strip() == s.group(2).strip(): # Display name is the same as the e-mail in angle brackets # Ex: "person@example.com " diff --git a/envelope/constants.py b/envelope/constants.py index 1374ec3..07cacdf 100644 --- a/envelope/constants.py +++ b/envelope/constants.py @@ -5,7 +5,7 @@ except ImportError: gnupg = None -smime_import_error = "Cannot import M2Crypto. Run: `sudo apt install swig && pip3 install M2Crypto`" +smime_import_error = "Cannot import cryptography. Run: `sudo apt install cryptography`" CRLF = '\r\n' AUTO = "auto" PLAIN = "plain" # XX allow text/plain too? diff --git a/envelope/envelope.py b/envelope/envelope.py index b9be86e..0b4f56d 100644 --- a/envelope/envelope.py +++ b/envelope/envelope.py @@ -8,8 +8,7 @@ import sys from tempfile import NamedTemporaryFile from unittest import mock -import warnings -from base64 import b64decode, b64encode +from base64 import b64decode from configparser import ConfigParser from copy import deepcopy from email import message_from_bytes @@ -17,16 +16,15 @@ from email.generator import Generator from email.message import EmailMessage, Message from email.parser import BytesParser -from email.utils import make_msgid, formatdate, getaddresses -from getpass import getpass +from email.utils import make_msgid, formatdate from itertools import chain -from os import environ, urandom +from os import environ from pathlib import Path from quopri import decodestring from types import GeneratorType -from typing import Literal, Union, List, Set, Optional, Any +from typing import Literal, Union, Optional, Any -from .address import Address +from .address import Address, _getaddresses from .attachment import Attachment from .constants import ISSUE_LINK, smime_import_error, gnupg, CRLF, AUTO, PLAIN, HTML, SIMULATION, SAFE_LOCALE from .message import _Message @@ -35,7 +33,6 @@ from .utils import AutoSubmittedHeader, Fetched, is_gpg_importable_key, assure_list, assure_fetched, get_mimetype - __doc__ = """Quick layer over python-gnupg, M2Crypto, smtplib, magic and email handling packages. Their common use cases merged into a single function. Want to sign a text and tired of forgetting how to do it right? You do not need to know everything about GPG or S/MIME, you do not have to bother with importing keys. @@ -320,14 +317,14 @@ def __init__(self, message=None, from_=None, to=None, subject=None, headers=None # that explicitly states we have no from header self._from: Union[Address, False, None] = None # e-mail From header self._from_addr: Optional[Address] = None # SMTP envelope MAIL FROM address - self._to: List[Address] = [] - self._cc: List[Address] = [] - self._bcc: List[Address] = [] - self._reply_to: List[Address] = [] + self._to: list[Address] = [] + self._cc: list[Address] = [] + self._bcc: list[Address] = [] + self._reply_to: list[Address] = [] self._subject: Union[str, None] = None self._subject_encrypted: Union[str, bool] = True self._smtp = None - self._attachments: List[Attachment] = [] + self._attachments: list[Attachment] = [] self._mime = AUTO self._nl2br = AUTO self._headers = EmailMessage() # object for storing headers the most standard way possible @@ -336,7 +333,7 @@ def __init__(self, message=None, from_=None, to=None, subject=None, headers=None # variables defined while processing self._status: bool = False # whether we successfully encrypted/signed/send self._processed: bool = False # prevent the user from mistakenly call .sign().send() instead of .signature().send() - self._result: List[Union[str, EmailMessage, Message]] = [] # text output for str() conversion + self._result: list[Union[str, EmailMessage, Message]] = [] # text output for str() conversion self._result_cache: Optional[str] = None self._result_cache_hash: Optional[int] = None self._smtp = SMTPHandler() @@ -405,34 +402,39 @@ def _parse_addresses(registry, email_or_more): registry.clear() addresses = [x for x in addresses if x] # filter out possible "" or False if addresses: - registry += (a for a in Address.parse(addresses) if a not in registry) - - def to(self, email_or_more=None) -> Union["Envelope", List[Address]]: + # Split addresses by both commas and semicolons + split_addresses = [] + for address in addresses: + split_addresses.extend(address.replace(';', ',').split(',')) + split_addresses = [x.strip() for x in split_addresses if x.strip()] # remove empty and whitespace-only strings + registry += (a for a in Address.parse(split_addresses) if a not in registry) + + def to(self, email_or_more=None) -> Union["Envelope", list[Address]]: """ Multiple addresses may be given in a string, delimited by comma (or semicolon). (The same is valid for `to`, `cc`, `bcc` and `reply-to`.) - :param email_or_more: str|Tuple[str]|List[str]|Generator[str]|Set[str]|Frozenset[str] + :param email_or_more: str|Tuple[str]|list[str]|Generator[str]|Set[str]|Frozenset[str] Set e-mail address/es. If None, we are reading. - return: Envelope if `email_or_more` set or List[Address] if not set + return: Envelope if `email_or_more` set or list[Address] if not set """ if email_or_more is None: return self._to self._parse_addresses(self._to, email_or_more) return self - def cc(self, email_or_more=None) -> Union["Envelope", List[Address]]: + def cc(self, email_or_more=None) -> Union["Envelope", list[Address]]: if email_or_more is None: return self._cc self._parse_addresses(self._cc, email_or_more) return self - def bcc(self, email_or_more=None) -> Union["Envelope", List[Address]]: + def bcc(self, email_or_more=None) -> Union["Envelope", list[Address]]: if email_or_more is None: return self._bcc self._parse_addresses(self._bcc, email_or_more) return self - def reply_to(self, email_or_more=None) -> Union["Envelope", List[Address]]: + def reply_to(self, email_or_more=None) -> Union["Envelope", list[Address]]: if email_or_more is None: return self._reply_to self._parse_addresses(self._reply_to, email_or_more) @@ -586,7 +588,7 @@ def subject(self, subject=None, encrypted: Union[str, bool] = None) -> Union["En self._subject_encrypted = encrypted return self - def mime(self, subtype=AUTO, nl2br: Literal["auto"] | bool=AUTO): + def mime(self, subtype=AUTO, nl2br: Literal["auto"] | bool = AUTO): """ Ignored if `Content-Type` header put to the message. @type subtype: str Set contents mime subtype: "auto" (default), "html" or "plain" for plain text. @@ -1084,7 +1086,6 @@ def _gpg_import_or_fail(self, key): else: raise ValueError(f"Could not import key starting: {key[:80]}...") - def _get_gnupg_home(self, for_help=False): s = self._gpg if type(self._gpg) is str else None if for_help: @@ -1127,14 +1128,16 @@ def _send_now(self, email, encrypt, encrypted_subject, send): email["Message-ID"] = make_msgid() if send and send != SIMULATION: + recipients = list(map(str, set(self._to + self._cc + self._bcc))) with mock.patch.object(Generator, '_handle_multipart_signed', Generator._handle_multipart): # https://github.com/python/cpython/issues/99533 and #19 failures = self._smtp.send_message(email, from_addr=self._from_addr, - to_addrs=list(map(str, set(self._to + self._cc + self._bcc)))) + to_addrs=recipients) if failures: logger.warning(f"Unable to send to all recipients: {repr(failures)}.") elif failures is False: + # TODO add here and test, logger.warning(f"Sending {recipients}, Message-ID: {email["Message-ID"]}") return False else: if send != SIMULATION: @@ -1234,7 +1237,7 @@ def _encrypt_gpg_now(self, message, encrypt, sign_fingerprint): return False def _gpg_list_keys(self, secret=False): - return ((key, address) for key in self._gnupg.list_keys(secret) for _, address in getaddresses(key["uids"])) + return ((key, address) for key in self._gnupg.list_keys(secret) for _, address in _getaddresses(key["uids"])) def _gpg_verify(self, signature: bytes, data: bytes): """ Allows verifying detached GPG signature. @@ -1248,16 +1251,23 @@ def _gpg_verify(self, signature: bytes, data: bytes): fp.seek(0) return bool(self._gnupg.verify_data(fp.name, data)) - def _get_decipherers(self) -> Set[str]: + def _get_decipherers(self) -> set[str]: """ :return: Set of e-mail addresses """ return set(x.address for x in self._to + self._cc + self._bcc + [x for x in [self._from] if x]) + def _import_cryptoraphy_modules(self): + try: + from cryptography.hazmat.primitives.serialization import load_pem_private_key, pkcs7 + from cryptography.x509 import load_pem_x509_certificate + from cryptography.hazmat.primitives import hashes, serialization + return load_pem_private_key, pkcs7, load_pem_x509_certificate, hashes, serialization + except ImportError: + raise ImportError(smime_import_error) + def smime_sign_only(self, email, sign): - from cryptography.hazmat.primitives.serialization import load_pem_private_key, pkcs7 - from cryptography.x509 import load_pem_x509_certificate - from cryptography.hazmat.primitives import hashes, serialization + load_pem_private_key, pkcs7, load_pem_x509_certificate, hashes, serialization = self._import_cryptoraphy_modules() # get sender's cert # cert and private key can be one file @@ -1297,11 +1307,8 @@ def smime_sign_only(self, email, sign): return signed_email - def smime_sign_encrypt(self, email, sign, encrypt): - from cryptography.hazmat.primitives.serialization import load_pem_private_key, pkcs7 - from cryptography.x509 import load_pem_x509_certificate - from cryptography.hazmat.primitives import hashes, serialization + load_pem_private_key, pkcs7, load_pem_x509_certificate, hashes, serialization = self._import_cryptoraphy_modules() if self._cert: sender_cert = self._cert @@ -1312,7 +1319,7 @@ def smime_sign_encrypt(self, email, sign, encrypt): try: sender_cert = load_pem_x509_certificate(sender_cert) except ValueError as e: - print(f"Certificate not found: {e}") + logger.warning(f"Certificate not found: {e}") # get senders private key for signing try: @@ -1335,6 +1342,16 @@ def smime_sign_encrypt(self, email, sign, encrypt): else: pubkey = encrypt + certificates = encrypt + + recipient_certs = [] + for cert in certificates: + try: + c = load_pem_x509_certificate(cert) + except ValueError as e: + raise ValueError("failed to load certificate from file") + + recipient_certs.append(c) try: pubkey = load_pem_x509_certificate(pubkey) except ValueError as e: @@ -1344,16 +1361,16 @@ def smime_sign_encrypt(self, email, sign, encrypt): envelope_builder = pkcs7.PKCS7EnvelopeBuilder().set_data(signed_email) envelope_builder = envelope_builder.add_recipient(pubkey) - options = [pkcs7.PKCS7Options.Text] + for recip in recipient_certs: + envelope_builder = envelope_builder.add_recipient(recip) + + options = [pkcs7.PKCS7Options.Binary] encrypted_email = envelope_builder.encrypt(serialization.Encoding.SMIME, options) return encrypted_email def smime_encrypt_only(self, email, encrypt): - from cryptography.hazmat.primitives.serialization import pkcs7 - from cryptography.x509 import load_pem_x509_certificate - from cryptography.hazmat.primitives import serialization - + _, pkcs7, load_pem_x509_certificate, _, serialization = self._import_cryptoraphy_modules() if self._cert: certificates = [self._cert] @@ -1369,7 +1386,7 @@ def smime_encrypt_only(self, email, encrypt): recipient_certs.append(c) - options = [pkcs7.PKCS7Options.Text] + options = [pkcs7.PKCS7Options.Binary] encrypted_email = pkcs7.PKCS7EnvelopeBuilder().set_data(email) for recip in recipient_certs: @@ -1379,8 +1396,7 @@ def smime_encrypt_only(self, email, encrypt): return encrypted_email - - def _encrypt_smime_now(self, email, sign, encrypt: Union[None, bool, bytes, List[bytes]]): + def _encrypt_smime_now(self, email, sign, encrypt: Union[None, bool, bytes, list[bytes]]): """ :type encrypt: Can be None, False, bytes or list[bytes] @@ -1388,7 +1404,7 @@ def _encrypt_smime_now(self, email, sign, encrypt: Union[None, bool, bytes, List # passphrase has to be bytes if (self._passphrase is not None): - self._passphrase = self._passphrase.encode('utf-8') + self._passphrase = self._passphrase.encode('utf-8') if sign is not None and type(sign) != bool: sign = assure_fetched(sign, bytes) @@ -1397,15 +1413,13 @@ def _encrypt_smime_now(self, email, sign, encrypt: Union[None, bool, bytes, List output = self.smime_sign_only(email, sign) elif sign and encrypt: - output = self.smime_sign_encrypt(email, sign, encrypt[0]) + output = self.smime_sign_encrypt(email, sign, encrypt) elif not sign and encrypt: output = self.smime_encrypt_only(email, encrypt) return output - - def _compose_gpg_signed(self, email, text, micalg=None): msg_payload = email email = EmailMessage() @@ -1583,7 +1597,7 @@ def _prepare_email(self, plain: bytes, html: bytes, encrypt_gpg: bool, sign_gpg: email["Subject"] = self._subject return email - def recipients(self, *, clear=False) -> Union[Set[Address], 'Envelope']: + def recipients(self, *, clear=False) -> Union[set[Address], 'Envelope']: """ Return set of all recipients – To, Cc, Bcc :param: clear If true, all To, Cc and Bcc recipients are removed and the object is returned. @@ -1615,7 +1629,7 @@ def _report(self) -> Union[dict, None]: raise NotImplemented("Current multipart/report has not been impemented." f"Please post current message as a new issue at {ISSUE_LINK}") - def attachments(self, name=None, inline=None) -> Union[Attachment, List[Attachment], bool]: + def attachments(self, name=None, inline=None) -> Union[Attachment, list[Attachment], bool]: """ Access the attachments. XX make available from CLI too --attachments(-inline)(-enclosed) [name] diff --git a/requirements.txt b/requirements.txt index 3ccd454..d66dfe5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,4 @@ jsonpickle python-magic python-gnupg>=0.5 -py3-validate-email -cryptography>=43 \ No newline at end of file +py3-validate-email \ No newline at end of file diff --git a/setup.py b/setup.py index e5141ce..2773541 100644 --- a/setup.py +++ b/setup.py @@ -32,8 +32,7 @@ long_description_content_type="text/markdown", install_requires=install_requires, extras_require={ - "smime": "M2Crypto", # need to have: `sudo apt install swig` - "cryptography": "cryptography>=43" # need to have `pip install cryptography` + "smime": ["cryptography>=43"] }, entry_points={ 'console_scripts': [ diff --git a/test_.py b/test_.py index 86adf38..c9493a6 100644 --- a/test_.py +++ b/test_.py @@ -1,4 +1,3 @@ -from email.utils import getaddresses, parseaddr import logging import re import sys @@ -14,7 +13,7 @@ from unittest import main, TestCase, mock from envelope import Envelope -from envelope.address import Address +from envelope.address import Address, _parseaddr, _getaddresses from envelope.constants import AUTO, PLAIN, HTML from envelope.parser import Parser from envelope.smtp_handler import SMTPHandler @@ -74,7 +73,8 @@ def check_lines(self, o, lines: Union[str, Tuple[str, ...]] = (), longer: Union[ try: index = output_tmp.index(search) except ValueError: - message = f"is in the wrong order (above the line '{last_search}' )" if search in output else "not found" + message = f"is in the wrong order (above the line '{last_search}' )" \ + if search in output else "not found" self.fail(f"Line '{search}' {message} in the output:\n{o}") output_tmp = output_tmp[index + 1:] last_search = search @@ -377,7 +377,6 @@ def test_smime_key_cert_together_passphrase(self): .sign(), ('Content-Disposition: attachment; filename="smime.p7s"', "MIIEUwYJKoZIhvcNAQcCoIIERDCCBEACAQExDzANBglghkgBZQMEAgEFADALBgkq"), 10) - def test_smime_encrypt(self): # Message will look that way: @@ -414,8 +413,8 @@ def test_smime_detection(self): .encryption(key=Path("tests/gpg_keys/envelope-example-identity-2@example.com.key")), result=True) self.check_lines(Envelope(MESSAGE).from_(IDENTITY_2).to(IDENTITY_2) - .signature(key=Path("tests/gpg_keys/envelope-example-identity-2@example.com.key"), passphrase=GPG_PASSPHRASE), - result=True) + .signature(key=Path("tests/gpg_keys/envelope-example-identity-2@example.com.key"), passphrase=GPG_PASSPHRASE), + result=True) # Implicit S/MIME self.check_lines(Envelope(MESSAGE) @@ -443,7 +442,7 @@ def test_smime_detection(self): def test_multiple_recipients(self): # output is generated using pyca cryptography from M2Crypto import SMIME - msg=MESSAGE + msg = MESSAGE def decrypt(key, cert, text): try: @@ -452,47 +451,49 @@ def decrypt(key, cert, text): return False # encrypt for both keys - output=(Envelope(msg) + output = (Envelope(msg) .smime() .reply_to("test-reply@example.com") .subject("my message") .encrypt([Path(self.smime_cert), Path("tests/smime/smime-identity@example.com-cert.pem")])) - + # First key - decrypted_message = decrypt('tests/smime/smime-identity@example.com-key.pem', 'tests/smime/smime-identity@example.com-cert.pem', output).decode('utf-8') + decrypted_message = decrypt('tests/smime/smime-identity@example.com-key.pem', + 'tests/smime/smime-identity@example.com-cert.pem', output).decode('utf-8') result = re.search(msg, decrypted_message) self.assertTrue(result) # Second key - decrypted_message = decrypt(self.smime_key,self.smime_cert, output).decode('utf-8') + decrypted_message = decrypt(self.smime_key, self.smime_cert, output).decode('utf-8') result = re.search(msg, decrypted_message) self.assertTrue(result) # encrypt for single key only - output=(Envelope(msg) + output = (Envelope(msg) .smime() .reply_to("test-reply@example.com") .subject("my message") .encrypt([Path(self.smime_cert)])) # Should be false, no search required - decrypted_message = decrypt('tests/smime/smime-identity@example.com-key.pem', 'tests/smime/smime-identity@example.com-cert.pem', output) + decrypted_message = decrypt('tests/smime/smime-identity@example.com-key.pem', + 'tests/smime/smime-identity@example.com-cert.pem', output) self.assertFalse(decrypted_message) - decrypted_message = decrypt(self.smime_key,self.smime_cert, output).decode('utf-8') + decrypted_message = decrypt(self.smime_key, self.smime_cert, output).decode('utf-8') result = re.search(msg, decrypted_message) self.assertTrue(result) def test_smime_decrypt(self): - e=Envelope.load(path="tests/eml/smime_encrypt.eml", key=self.smime_key, cert=self.smime_cert) + e = Envelope.load(path="tests/eml/smime_encrypt.eml", key=self.smime_key, cert=self.smime_cert) self.assertEqual(MESSAGE, e.message()) def test_smime_decrypt_attachments(self): from M2Crypto import BIO, SMIME import re from base64 import b64encode, b64decode - body="an encrypted message with the attachments" # note that the inline image is not referenced in the text - encrypted_envelope=(Envelope(body) + body = "an encrypted message with the attachments" # note that the inline image is not referenced in the text + encrypted_envelope = (Envelope(body) .smime() .reply_to("test-reply@example.com") .subject("my message") @@ -528,21 +529,21 @@ def test_smime_decrypt_attachments(self): pos = decrypted_data.index(cd_string) # get only gif data - data_temp = decrypted_data[pos + len(cd_string):].strip().replace('\n','').replace('\r', '') - data_temp = data_temp[:data_temp.index("==") +2] + data_temp = decrypted_data[pos + len(cd_string):].strip().replace('\n', '').replace('\r', '') + data_temp = data_temp[:data_temp.index("==") + 2] base64_content = b64encode(self.image_file.read_bytes()).decode('ascii') self.assertEqual(base64_content, data_temp) # find generic.txt attachment cd_string = 'Content-Disposition: attachment; filename="generic.txt"' - pos = decrypted_data.index(cd_string) - data_temp = decrypted_data[pos:] - d = data_temp.split('\r\n\r\n')[1].strip() + "==" + pos = decrypted_data.index(cd_string) + data_temp = decrypted_data[pos:] + d = data_temp.split('\n\n')[1].strip() + "==" attachment_content = b64decode(d).decode('utf-8') with open(self.text_attachment, 'r') as f: file_content = f.read() - + self.assertEqual(attachment_content, file_content) # XX smime_sign.eml is not used right now. @@ -553,7 +554,7 @@ def test_smime_decrypt_attachments(self): def test_smime_key_cert_together(self): # XX verify signature - e=Envelope.load(path="tests/eml/smime_key_cert_together.eml", key=self.key_cert_together) + e = Envelope.load(path="tests/eml/smime_key_cert_together.eml", key=self.key_cert_together) self.assertEqual(MESSAGE, e.message()) @@ -600,14 +601,14 @@ def test_gpg_auto_sign(self): '-----END PGP SIGNATURE-----',), 10) # mail from "envelope-example-identity-not-stated-in-ring@example.com" should not be signed - output=str(Envelope(MESSAGE) + output = str(Envelope(MESSAGE) .gpg(GNUPG_HOME) .from_("envelope-example-identity-not-stated-in-ring@example.com") .sign("auto")).splitlines() self.assertNotIn('-----BEGIN PGP SIGNATURE-----', output) # force-signing without specifying a key nor sending address should produce a message signed with a first-found key - output=str(Envelope(MESSAGE) + output = str(Envelope(MESSAGE) .gpg(GNUPG_HOME) .sign(True)).splitlines() self.assertIn('-----BEGIN PGP SIGNATURE-----', output) @@ -629,7 +630,7 @@ def test_gpg_encrypt_message(self): # =rK+/ # -----END PGP MESSAGE----- - message=(Envelope(MESSAGE) + message = (Envelope(MESSAGE) .gpg(GNUPG_HOME) .from_(IDENTITY_1) .to(IDENTITY_2) @@ -669,7 +670,7 @@ def test_gpg_encrypt(self): # # --===============1001129828818615570==-- - e=str(Envelope(MESSAGE) + e = str(Envelope(MESSAGE) .to(IDENTITY_2) .gpg(GNUPG_HOME) .from_(IDENTITY_1) @@ -685,8 +686,8 @@ def test_gpg_encrypt(self): "To: envelope-example-identity-2@example.com", ), 10, not_in='Subject: dumb subject') - lines=e.splitlines() - message="\n".join(lines[lines.index(PGP_MESSAGE):]) + lines = e.splitlines() + message = "\n".join(lines[lines.index(PGP_MESSAGE):]) self.check_lines(self.bash("gpg", "--decrypt", piped=message, envelope=False), ('Content-Type: multipart/mixed; protected-headers="v1";', 'Subject: dumb subject', @@ -696,23 +697,23 @@ def test_gpg_encrypt(self): def test_arbitrary_encrypt(self): """ Keys to be encrypted with explicitly chosen """ - temp=[TemporaryDirectory() for _ in range(4)] # must exist in the scope to preserve the dirs - rings=[t.name for t in temp] - message=MESSAGE - key1_raw=Path("tests/gpg_keys/envelope-example-identity@example.com.bytes.key").read_bytes() - key1_armored=Path("tests/gpg_keys/envelope-example-identity@example.com.key").read_text() - _importer=Envelope("just importer") + temp = [TemporaryDirectory() for _ in range(4)] # must exist in the scope to preserve the dirs + rings = [t.name for t in temp] + message = MESSAGE + key1_raw = Path("tests/gpg_keys/envelope-example-identity@example.com.bytes.key").read_bytes() + key1_armored = Path("tests/gpg_keys/envelope-example-identity@example.com.key").read_text() + _importer = Envelope("just importer") # helper methods def decrypt(s, ring, equal=True): - m=self.assertEqual if equal else self.assertNotEqual + m = self.assertEqual if equal else self.assertNotEqual m(message, Envelope.load(s, gnupg_home=rings[ring]).message()) def importer(ring, key, passphrase=None): _importer.gpg(rings[ring]).sign(Path("tests/gpg_keys/" + key), passphrase=passphrase) # Message encrypted for envelope-example-identity@example.com only, not for the sender - e1=str(Envelope(message) + e1 = str(Envelope(message) .to(IDENTITY_1) .gpg(GNUPG_HOME) .from_(IDENTITY_2) @@ -730,7 +731,7 @@ def importer(ring, key, passphrase=None): decrypt(e1, 0) # message encrypted for multiple recipients - e2=str(Envelope(message) + e2 = str(Envelope(message) .to(IDENTITY_1) .gpg(GNUPG_HOME) .from_(IDENTITY_3) @@ -744,7 +745,7 @@ def importer(ring, key, passphrase=None): decrypt(e2, 2) # message not encrypted for a recipient but for a sender only (for some unknown reason) - e3=str(Envelope(message) + e3 = str(Envelope(message) .to(IDENTITY_2) .gpg(GNUPG_HOME) .from_(IDENTITY_1) @@ -755,7 +756,7 @@ def importer(ring, key, passphrase=None): decrypt(e3, 2) # ring 2 has "envelope-example-identity@example.com" # message encrypted for combination of fingerprints and e-mails - e3=str(Envelope(message) + e3 = str(Envelope(message) .to("envelope-example-identity-3@example.com, envelope-example-identity@example.com") .gpg(GNUPG_HOME) .from_(IDENTITY_2) @@ -784,14 +785,14 @@ def importer(ring, key, passphrase=None): # import raw unarmored key in a list ("envelope-example-identity@example.com" into ring 1) # (note that we pass a set to the .encryption to test if it takes other iterables than a list) - e4=Envelope(message).encryption({IDENTITY_2, key1_raw}).to(IDENTITY_3).from_(IDENTITY_2).gpg( + e4 = Envelope(message).encryption({IDENTITY_2, key1_raw}).to(IDENTITY_3).from_(IDENTITY_2).gpg( rings[1]).as_message() decrypt(e4, 1) decrypt(e4, 2) # multiple encryption keys in bash def bash(ring, from_, to, encrypt, valid=True): - contains=PGP_MESSAGE if valid else "Signing/encrypting failed." + contains = PGP_MESSAGE if valid else "Signing/encrypting failed." self.assertIn(contains, self.bash("--from", from_, "--to", *to, "--encrypt", *encrypt, piped=message, env={"GNUPGHOME": rings[ring]})) @@ -806,13 +807,13 @@ def bash(ring, from_, to, encrypt, valid=True): bash(3, IDENTITY_2, (IDENTITY_1,), ("--no-from",)) # --no-sender supress the need for ID=2 def test_arbitrary_encrypt_with_signing(self): - model=(Envelope(MESSAGE) + model = (Envelope(MESSAGE) .to(f"{IDENTITY_3}, {IDENTITY_1}") .from_(IDENTITY_2) .gpg(GNUPG_HOME)) def logged(signature, encryption, warning=False): - e=(model.copy().signature(signature).encryption(encryption)) + e = (model.copy().signature(signature).encryption(encryption)) if warning: with self.assertLogs('envelope', level='WARNING') as cm: self.assertEqual("", str(e)) @@ -887,7 +888,7 @@ def test_gpg_sign_passphrase(self): ("-----BEGIN PGP SIGNATURE-----",), 10) def test_auto_import(self): - temp=TemporaryDirectory() + temp = TemporaryDirectory() # no signature - empty ring self.check_lines(Envelope(MESSAGE) @@ -960,52 +961,52 @@ def test_auto_import(self): def test_signed_gpg(self): # XX we should test signature verification with e._gpg_verify(), # however .load does not load application/pgp-signature content at the moment - e=Envelope.load(path="tests/eml/test_signed_gpg.eml") + e = Envelope.load(path="tests/eml/test_signed_gpg.eml") self.assertEqual(MESSAGE, e.message()) def test_encrypted_gpg(self): - e=Envelope.load(path="tests/eml/test_encrypted_gpg.eml") + e = Envelope.load(path="tests/eml/test_encrypted_gpg.eml") self.assertEqual("dumb encrypted message", e.message()) def test_encrypted_signed_gpg(self): - e=Envelope.load(path="tests/eml/test_encrypted_signed_gpg.eml") + e = Envelope.load(path="tests/eml/test_encrypted_signed_gpg.eml") self.assertEqual("dumb encrypted and signed message", e.message()) def test_encrypted_gpg_subject(self): - body="just a body text" - subject="This is an encrypted subject" - encrypted_subject="Encrypted message" - ref=(Envelope(body) + body = "just a body text" + subject = "This is an encrypted subject" + encrypted_subject = "Encrypted message" + ref = (Envelope(body) .gpg(GNUPG_HOME) .to(IDENTITY_2) .from_(IDENTITY_1) .encryption()) - encrypted_eml=ref.subject(subject).as_message().as_string() + encrypted_eml = ref.subject(subject).as_message().as_string() # subject has been encrypted self.assertIn("Subject: " + encrypted_subject, encrypted_eml) self.assertNotIn(subject, encrypted_eml) # subject has been decrypted - e=Envelope.load(encrypted_eml) + e = Envelope.load(encrypted_eml) self.assertEqual(body, e.message()) self.assertEqual(subject, e.subject()) # further meddling with the encrypt parameter def check_decryption(reference, other_subject=encrypted_subject): - encrypted=reference.as_message().as_string() + encrypted = reference.as_message().as_string() self.assertIn(other_subject, encrypted) self.assertNotIn(subject, encrypted) - decrypted=Envelope.load(encrypted).as_message().as_string() + decrypted = Envelope.load(encrypted).as_message().as_string() self.assertIn(subject, decrypted) self.assertNotIn(other_subject, decrypted) - front_text="Front text" + front_text = "Front text" check_decryption(ref.subject(subject, encrypted=True)) # the default behaviour check_decryption(ref.subject(subject, front_text), front_text) # choose another placeholder text - always_visible=ref.subject(subject, encrypted=False).as_message().as_string() # do not encrypt the subject + always_visible = ref.subject(subject, encrypted=False).as_message().as_string() # do not encrypt the subject self.assertIn(subject, always_visible) self.assertIn(subject, Envelope.load(always_visible).as_message().as_string()) @@ -1029,7 +1030,7 @@ def test_long_attachment_filename(self): However, if the user uses Envelope.as_message(), its gets the underlying Message without the correction with GPG void. See #19 and https://github.com/python/cpython/issues/99533 """ - e=(Envelope(MESSAGE) + e = (Envelope(MESSAGE) .to(IDENTITY_2) .gpg(GNUPG_HOME) .from_(IDENTITY_1) @@ -1037,14 +1038,14 @@ def test_long_attachment_filename(self): .attach("some data", name="A"*35)) def verify_inline_message(txt: str): - boundary=re.search(r'boundary="(.*)"', txt).group(1) - reg=fr'{boundary}.*{boundary}\n(.*)\n--{boundary}.*(-----BEGIN PGP SIGNATURE-----.*-----END PGP SIGNATURE-----)' - m=re.search(reg, txt, re.DOTALL) + boundary = re.search(r'boundary="(.*)"', txt).group(1) + reg = (fr'{boundary}.*{boundary}\n(.*)\n--{boundary}.*(-----BEGIN PGP SIGNATURE-----.*-----END PGP SIGNATURE-----)') + m = re.search(reg, txt, re.DOTALL) return e._gpg_verify(m[2].encode(), m[1].encode()) # accessing via standard email package with get_payload called on different parts keeps signature - sig=e.as_message().get_payload()[1].get_payload().encode() - data=e.as_message().get_payload()[0].as_bytes() + sig = e.as_message().get_payload()[1].get_payload().encode() + data = e.as_message().get_payload()[0].as_bytes() self.assertTrue(e._gpg_verify(sig, data)) # accessing via standard email package on the whole message does not keep signature @@ -1065,39 +1066,39 @@ def check_sending(o, email, **_): class TestMime(TestAbstract): - plain="""First + plain = """First Second Third """ - html="""First
+ html = """First
Second Third """ - html_without_line_break="""First + html_without_line_break = """First Second Third """ - mime_plain='Content-Type: text/plain; charset="utf-8"' - mime_html='Content-Type: text/html; charset="utf-8"' + mime_plain = 'Content-Type: text/plain; charset="utf-8"' + mime_html = 'Content-Type: text/html; charset="utf-8"' def test_plain(self): - pl=self.mime_plain + pl = self.mime_plain self.check_lines(Envelope().message(self.plain).mime("plain", "auto"), pl) self.check_lines(Envelope().message(self.plain), pl) self.check_lines(Envelope().message(self.html).mime("plain"), pl) def test_html(self): - m=self.mime_html + m = self.mime_html self.check_lines(Envelope().message(self.plain).mime("html", "auto"), m) self.check_lines(Envelope().message(self.html), m) self.check_lines(Envelope().message(self.html_without_line_break), m) def test_nl2br(self): - nobr="Second" - br="Second
" + nobr = "Second" + br = "Second
" self.check_lines(Envelope().message(self.html), nobr) # there already is a
tag so nl2br "auto" should not convert it self.check_lines(Envelope().message(self.html).mime(nl2br=True), br) @@ -1108,11 +1109,11 @@ def test_nl2br(self): self.check_lines(Envelope().message(self.html_without_line_break).mime(nl2br=False), nobr) def test_alternative(self): - boundary="=====envelope-test====" + boundary = "=====envelope-test====" # alternative="auto" can become both "html" and "plain" - e1=Envelope().message("Hello").message("Hello", alternative="plain", boundary=boundary).date(False) - e2=Envelope().message("Hello", alternative="html").message("Hello", boundary=boundary).date(False) + e1 = Envelope().message("Hello").message("Hello", alternative="plain", boundary=boundary).date(False) + e2 = Envelope().message("Hello", alternative="html").message("Hello", boundary=boundary).date(False) self.assertEqual(e1, e2) # HTML variant is always the last even if defined before plain variant @@ -1122,7 +1123,7 @@ def test_alternative(self): "Hello")) def test_only_2_alternatives_allowed(self): - e1=Envelope().message("Hello").message("Hello", alternative="plain") + e1 = Envelope().message("Hello").message("Hello", alternative="plain") # we can replace alternative e1.copy().message("Test").message("Test", alternative="plain") @@ -1136,7 +1137,7 @@ def test_libmagic(self): self.assertEqual("image/gif", get_mimetype(path=self.image_file)) # test get_mimetype in the action while dealing attachments - e=(Envelope() + e = (Envelope() .attach("hello", "text/plain") .attach(b"hello bytes") .attach(Path("tests/gpg_ring/trustdb.gpg")) @@ -1150,8 +1151,8 @@ def test_libmagic(self): class TestRecipients(TestAbstract): def test_from(self): - id1="identity-1@example.com" - id2="identity-2@example.com" + id1 = "identity-1@example.com" + id2 = "identity-2@example.com" self.check_lines(Envelope(MESSAGE).header("sender", id1), f"sender: {id1}", not_in=f"From: {id1}") self.check_lines(Envelope(MESSAGE, headers=[("sender", id1)]), @@ -1172,23 +1173,23 @@ def test_from(self): (f"From: {id1}", f"Sender: {id2}")) def test_from_addr(self): - mail1="envelope-from@example.com" - mail2="header-from@example.com" - e=Envelope(MESSAGE).from_addr(mail1).from_(mail2) + mail1 = "envelope-from@example.com" + mail2 = "header-from@example.com" + e = Envelope(MESSAGE).from_addr(mail1).from_(mail2) self.assertEqual(mail1, e.from_addr()) self.assertEqual(mail2, e.from_()) self.assertIn("Have not been sent from " + mail1, str(e.send(False))) - e=Envelope(MESSAGE).from_(mail2) + e = Envelope(MESSAGE).from_(mail2) self.assertIn("Have not been sent from " + mail2, str(e.send(False))) - e=Envelope(MESSAGE, from_addr=mail1).from_(mail2) + e = Envelope(MESSAGE, from_addr=mail1).from_(mail2) self.assertIn("Have not been sent from " + mail1, str(e.send(False))) self.assertIn("Have not been sent from " + mail1, self.bash("--from-addr", mail1, "--send", "0", file=self.eml)) def test_addresses(self): - e=Envelope.load(path=self.eml) + e = Envelope.load(path=self.eml) self.assertEqual(1, len(e.to())) - contact=e.to()[0] - full="Person " + contact = e.to()[0] + full = "Person " self.assertEqual(full, contact) self.assertEqual("person@example.com", contact) self.assertEqual("person@example.com", contact.address) @@ -1207,7 +1208,7 @@ def test_addresses(self): self.assertNotEqual("PERSON", contact.user) # Address is correctly typed, empty properties returns string - empty=Address() + empty = Address() self.assertEqual(Address(""), empty) self.assertEqual("", str(empty.user)) self.assertEqual("", str(empty.host)) @@ -1221,7 +1222,7 @@ def test_addresses(self): self.assertEqual(f"{full}, {full}", ", ".join((contact, contact))) # casefold method - c=contact.casefold() + c = contact.casefold() self.assertEqual(contact, c) self.assertIsNot(contact, c) self.assertEqual(c.name, "person") @@ -1241,13 +1242,13 @@ def test_disguised_addresses(self): # If any of these tests fails, it's a good message the underlying Python libraries are better # and we may stop remedying. # https://github.com/python/cpython/issues/40889#issuecomment-1094001067 - disguise_addr="first@example.cz " - same="person@example.com " - self.assertEqual(('', 'first@example.cz'), parseaddr(disguise_addr)) + disguise_addr = "first@example.cz " + same = "person@example.com " + self.assertEqual(('', 'first@example.cz'), _parseaddr(disguise_addr)) self.assertEqual([('', 'first@example.cz'), ('', 'second@example.com')], - getaddresses([disguise_addr])) + _getaddresses([disguise_addr])) self.assertEqual([('', 'person@example.com'), ('', 'person@example.com')], - getaddresses([same])) + _getaddresses([same])) # For the same input, Envelope receives better results. self.assertEqual(Address(name='first@example.cz', address='second@example.com'), Address(disguise_addr)) @@ -1260,7 +1261,7 @@ def test_disguised_addresses(self): self.assertEqual(Address(address='person@example.com'), Address.parse(same, single=True)) # Try various disguised addresses - examples=["person@example.com ", # the same + examples = ["person@example.com ", # the same "person@example.com ", # differs, the name hiding the address "pers'one'@'ample.com ", # single address "pers'one'@'ample.com, ", # two addresses @@ -1273,7 +1274,7 @@ def test_disguised_addresses(self): # three of them are disguised 'ug@ly3@example.com ,ugly2@example.com , ugly@example.com '] - expected_parseaddr=[("", "person@example.com"), + expected_parseaddr = [("", "person@example.com"), ("person--AT--example.com", "person@example2.com"), ("pers'one'--AT--'ample.com", "a@example.com"), ("", "pers'one'@'ample.com"), @@ -1283,7 +1284,7 @@ def test_disguised_addresses(self): ("User (nested comment)", "foo@bar.com"), ("ug--AT--ly3--AT--example.com", "another3@example.com")] - expected_getaddresses=[ + expected_getaddresses = [ [("", "person@example.com")], [("person--AT--example.com", "person@example2.com")], [("pers'one'--AT--'ample.com", "a@example.com")], @@ -1305,11 +1306,11 @@ def test_disguised_addresses(self): ("ugly--AT--example.com", "another@example.com")]] for e, r in zip(expected_parseaddr, (Address(e) for e in examples)): - name, addr=e + name, addr = e self.assertEqual(Address(name=name, address=addr), r) for e, r in zip(expected_getaddresses, (Address.parse(e) for e in examples)): - expected=[Address(name=name, address=addr) for name, addr in e] + expected = [Address(name=name, address=addr) for name, addr in e] self.assertEqual(expected, r) # As we want to be slightly better than the standard library @@ -1320,8 +1321,8 @@ def test_disguised_addresses(self): def check(addresses, models): """ Parsing addresses is exactly the same as in the standard email.utils library. """ - compared=[Address(name=v[0], address=v[1]) for v in models if v[0] or v[1]] - parsed=Address.parse(addresses) + compared = [Address(name=v[0], address=v[1]) for v in models if v[0] or v[1]] + parsed = Address.parse(addresses) self.assertEqual([(a.name, a.address) for a in parsed], [(a.name, a.address) for a in compared]) check(['aperson@dom.ain (Al Person)', @@ -1344,7 +1345,7 @@ def check(addresses, models): check(['Al Person '], [('Al Person', 'aperson@dom.ain')]) def test_removing_contact(self): - contact="Person2 " + contact = "Person2 " def e(): return Envelope.load(path=self.eml).cc(contact) @@ -1364,7 +1365,7 @@ def e(): self.assertEqual([contact], e().to("").cc()) # Works from bash too - header_row=f"To: Person " + header_row = f"To: Person " self.assertIn(header_row, self.bash(file=self.eml)) self.assertNotIn(header_row, self.bash("--to", "", file=self.eml)) self.assertNotIn(f"To: {contact}", self.bash("--to", "", "contact", file=self.eml)) @@ -1384,7 +1385,7 @@ def test_reading_contact(self): def test_empty_contact(self): """ Be sure to receive an address even if the header misses. """ - e1=Envelope.load("Empty message") + e1 = Envelope.load("Empty message") self.assertTrue(isinstance(e1.from_(), Address)) self.assertTrue(isinstance(e1.to(), list)) self.assertTrue(isinstance(e1.cc(), list)) @@ -1395,7 +1396,7 @@ def test_empty_contact(self): self.assertEqual("", e1.from_().address) - e2=Envelope.load("From: test@example.com\n\nEmpty message") + e2 = Envelope.load("From: test@example.com\n\nEmpty message") self.assertTrue(isinstance(e2.from_(), Address)) self.assertTrue(e2.from_()) self.assertTrue(e2.header("from")) @@ -1403,12 +1404,12 @@ def test_empty_contact(self): self.assertFalse(e2.header("sender")) self.assertEqual("", e2.from_().name) - e3=Envelope.load("From: Person \n\nEmpty message") + e3 = Envelope.load("From: Person \n\nEmpty message") self.assertTrue(e3.from_()) self.assertEqual("Person", e3.from_().name) self.assertTrue(e3.from_().is_valid()) - e4=Envelope.load("From: Invalid\n\nEmpty message") + e4 = Envelope.load("From: Invalid\n\nEmpty message") self.assertTrue(e4.from_()) self.assertEqual("Invalid", e4.from_().address) self.assertEqual("", e4.from_().name) @@ -1416,9 +1417,9 @@ def test_empty_contact(self): def test_multiple_recipients_format(self): """ You can use iterables like tuple, list, generator, set, frozenset for specifying multiple values """ - one=[IDENTITY_1] - two=[IDENTITY_1, IDENTITY_2] - three=[IDENTITY_1, IDENTITY_2, IDENTITY_3] + one = [IDENTITY_1] + two = [IDENTITY_1, IDENTITY_2] + three = [IDENTITY_1, IDENTITY_2, IDENTITY_3] # try single value, tuple and list self.assertEqual(one, Envelope(MESSAGE).to(IDENTITY_1).to()) @@ -1440,9 +1441,9 @@ def test_multiple_recipients_format(self): class TestSubject(TestAbstract): def test_cache_recreation(self): - s1="Test" - s2="Another" - e=Envelope(MESSAGE).subject(s1) + s1 = "Test" + s2 = "Another" + e = Envelope(MESSAGE).subject(s1) self.check_lines(e, f"Subject: {s1}") e.subject(s2) @@ -1452,7 +1453,7 @@ def test_cache_recreation(self): class TestHeaders(TestAbstract): def test_generic_header_manipulation(self): # Add a custom header and delete it - e=Envelope(MESSAGE).subject("my subject").header("custom", "1") + e = Envelope(MESSAGE).subject("my subject").header("custom", "1") self.assertEqual(e.header("custom"), "1") self.assertIs(e.header("custom", replace=True), e) @@ -1474,11 +1475,11 @@ def test_specific_header_manipulation(self): Ex: It is useful to have Cc as a special header since it can hold the list of receivers. """ # Add a specific header like and delete it - s="my subject" - id1="person@example.com" - id2="person2@example.com" - id3="person3@example.com" - e=Envelope(MESSAGE) \ + s = "my subject" + id1 = "person@example.com" + id2 = "person2@example.com" + id3 = "person3@example.com" + e = Envelope(MESSAGE) \ .subject(s) \ .header("custom", "1") \ .cc(id1) # set headers via their specific methods @@ -1502,7 +1503,7 @@ def test_date(self): self.assertNotIn(f"Date: ", str(Envelope(MESSAGE).date(False))) def test_email_addresses(self): - e=(Envelope() + e = (Envelope() .cc("person1@example.com") .to("person2@example.com") # add as string .to(["person3@example.com", "person4@example.com"]) # add as list @@ -1525,26 +1526,25 @@ def test_email_addresses(self): def test_invalid_email_addresses(self): """ If we discard silently every invalid e-mail address received, the user would not know their recipients are not valid. """ - e=(Envelope().to('person1@example.com, [invalid!email], person2@example.com')) + e = (Envelope().to('person1@example.com, [invalid!email], person2@example.com')) self.assertEqual(3, len(e.to())) self.assertFalse(e.check(check_mx=False, check_smtp=False)) - e=(Envelope().to('person1@example.com, person2@example.com')) + e = (Envelope().to('person1@example.com, person2@example.com')) self.assertTrue(e.check(check_mx=False, check_smtp=False)) - class TestSupportive(TestAbstract): def test_copy(self): - factory=Envelope().cc("original@example.com").copy - e1=factory().to("independent-1@example.com") - e2=factory().to("independent-2@example.com").cc("additional@example.com") + factory = Envelope().cc("original@example.com").copy + e1 = factory().to("independent-1@example.com") + e2 = factory().to("independent-2@example.com").cc("additional@example.com") self.assertEqual(e1.recipients(), {'independent-1@example.com', 'original@example.com'}) self.assertEqual(e2.recipients(), {'independent-2@example.com', 'original@example.com', 'additional@example.com'}) def test_message(self): - e=Envelope("hello").as_message() + e = Envelope("hello").as_message() self.assertEqual(type(e), EmailMessage) self.assertEqual(e.get_payload(), "hello\n") @@ -1559,7 +1559,7 @@ def test_smtp_quit(self): class DummySMTPConnection: def __init__(self, name): - self.name=name + self.name = name def quit(self): print(self.name) @@ -1568,12 +1568,12 @@ def key(name): return "{'host': '" + name + "', 'port': 25, 'user': None, 'password': None," \ " 'security': None, 'timeout': 3, 'attempts': 3, 'delay': 3, 'local_hostname': None}" - SMTPHandler._instances={key(name): DummySMTPConnection(name) for name in (f"dummy{i}" for i in range(4))} + SMTPHandler._instances = {key(name): DummySMTPConnection(name) for name in (f"dummy{i}" for i in range(4))} - e1=Envelope().smtp("dummy1").smtp("dummy2") # this object uses dummy2 only - e2=Envelope().smtp("dummy3") # this object uses dummy3 + e1 = Envelope().smtp("dummy1").smtp("dummy2") # this object uses dummy2 only + e2 = Envelope().smtp("dummy3") # this object uses dummy3 - stdout=StringIO() + stdout = StringIO() with redirect_stdout(stdout): e2.smtp_quit() Envelope.smtp_quit() @@ -1589,19 +1589,19 @@ def test_bcc(self): self.assertNotIn("person@example.com", self.bash("--bcc", "person@example.com", "--send", "off")) def test_attachment(self): - preview_text=f"Attachment generic.txt (text/plain): Small sample text at..." + preview_text = f"Attachment generic.txt (text/plain): Small sample text at..." self.assertIn(preview_text, self.bash("--attach", self.text_attachment, "--preview")) - o=self.bash("--attach", self.text_attachment, "--send", "0") + o = self.bash("--attach", self.text_attachment, "--send", "0") self.assertNotIn(preview_text, o) self.assertIn('Content-Disposition: attachment; filename="generic.txt"', o) def test_subject(self): - subject1="Hello world" - subject2="Good bye sun" - default_placeholder="Encrypted message" # default text used by the library + subject1 = "Hello world" + subject2 = "Good bye sun" + default_placeholder = "Encrypted message" # default text used by the library def get_encrypted(subject, subject_encrypted): - ref=self.bash("--attach", self.text_attachment, "--send", "0", + ref = self.bash("--attach", self.text_attachment, "--send", "0", "--gpg", GNUPG_HOME, "--to", IDENTITY_2, "--from", IDENTITY_1, @@ -1609,22 +1609,22 @@ def get_encrypted(subject, subject_encrypted): "--subject", subject, "--subject-encrypted", subject_encrypted, piped="text") # remove text "Have not been sent ... Encrypted subject: ..." prepended by ._send_now - ref=ref[ref.index("\n\n") + 2:] + ref = ref[ref.index("\n\n") + 2:] return ref, Envelope.load(ref).as_message().as_string() - encrypted, decrypted=get_encrypted(subject1, subject2) + encrypted, decrypted = get_encrypted(subject1, subject2) self.assertIn(f"Subject: {subject2}", encrypted) self.assertNotIn(subject1, encrypted) self.assertIn(f"Subject: {subject1}", decrypted) self.assertNotIn(subject2, decrypted) for x in ("False", "FALSE", "0", "oFF"): - encrypted, decrypted=get_encrypted(subject1, x) + encrypted, decrypted = get_encrypted(subject1, x) self.assertIn(f"Subject: {subject1}", encrypted) self.assertIn(f"Subject: {subject1}", decrypted) for x in ("True", "TRUE", "1", "oN"): - encrypted, decrypted=get_encrypted(subject1, x) + encrypted, decrypted = get_encrypted(subject1, x) self.assertIn(f"Subject: {default_placeholder}", encrypted) self.assertIn(f"Subject: {subject1}", decrypted) @@ -1632,7 +1632,7 @@ def get_encrypted(subject, subject_encrypted): class TestAttachment(TestAbstract): def test_casting(self): - e=Envelope() \ + e = Envelope() \ .attach("hello", "text/plain") \ .attach(b"hello bytes") @@ -1647,48 +1647,48 @@ def test_casting(self): self.assertEqual("hello bytes", str(e.attachments()[1])) def test_different_order(self): - path=Path(self.text_attachment) - e=Envelope() \ + path = Path(self.text_attachment) + e = Envelope() \ .attach(path, "text/csv", "foo") \ .attach(mimetype="text/csv", name="foo", path=self.text_attachment) \ .attach(path, "foo", "text/csv") \ .attach([(path, "text/csv", "foo",)]) \ .attach(((path, "text/csv", "foo",),)) - model=repr(e.attachments()[0]) + model = repr(e.attachments()[0]) # a tuple with a single attachment (and its details) - e2=Envelope(attachments=(path, "text/csv", "foo")) + e2 = Envelope(attachments=(path, "text/csv", "foo")) # a list that contains multiple attachments - e3=Envelope(attachments=[(path, "text/csv", "foo"), (path, "text/csv", "foo")]) + e3 = Envelope(attachments=[(path, "text/csv", "foo"), (path, "text/csv", "foo")]) [self.assertEqual(model, repr(a)) for a in e.attachments() + e2.attachments() + e3.attachments()] def test_inline(self): def e(): return Envelope().subject("Inline image message") - image=self.image_file - image_path=image.absolute() - name=image.name + image = self.image_file + image_path = image.absolute() + name = image.name # Specified the only HTML alternative, no plain text - e1=e().message(f"Hi ", alternative=HTML).attach(image, inline=True) - single_alternative=("Content-Type: multipart/related;", + e1 = e().message(f"Hi ", alternative=HTML).attach(image, inline=True) + single_alternative = ("Content-Type: multipart/related;", "Subject: Inline image message", 'Content-Type: text/html; charset="utf-8"') - img_msg="Content-Disposition: inline", "R0lGODlhAwADAKEDAAIJAvz9/v///wAAACH+EUNyZWF0ZWQgd2l0aCBHSU1QACwAAAAAAwADAAAC" - image_gif="Hi ", "Content-Type: image/gif", "Content-ID: ", *img_msg - multiple_alternatives=('Content-Type: text/plain; charset="utf-8"', + img_msg = "Content-Disposition: inline", "R0lGODlhAwADAKEDAAIJAvz9/v///wAAACH+EUNyZWF0ZWQgd2l0aCBHSU1QACwAAAAAAwADAAAC" + image_gif = "Hi ", "Content-Type: image/gif", "Content-ID: ", *img_msg + multiple_alternatives = ('Content-Type: text/plain; charset="utf-8"', "Plain alternative", "Content-Type: multipart/related;", 'Content-Type: text/html; charset="utf-8"') - compare_lines=*single_alternative, *image_gif + compare_lines = *single_alternative, *image_gif self.check_lines(e1, compare_lines) # Not specifying the only HTML alternative - e2=e().message(f"Hi ").attach(path=image_path, inline=True) + e2 = e().message(f"Hi ").attach(path=image_path, inline=True) self.check_lines(e2, compare_lines) # Two message alternatives, the plain is specified - e3=e().message(f"Hi ").message("Plain alternative", alternative=PLAIN, + e3 = e().message(f"Hi ").message("Plain alternative", alternative=PLAIN, boundary="bound") \ .attach(image, inline=True) self.check_lines(e3, ( @@ -1699,7 +1699,7 @@ def e(): *image_gif)) # Two message alternatives, the HTML is specified - e4=e().message(f"Hi ", alternative=HTML).message("Plain alternative") \ + e4 = e().message(f"Hi ", alternative=HTML).message("Plain alternative") \ .attach(path=image.absolute(), inline=True) self.check_lines(e4, ("Content-Type: multipart/alternative;", "Subject: Inline image message", @@ -1707,8 +1707,8 @@ def e(): *image_gif)) # Setting a name of an inline image - custom_cid="custom-name.jpg" - e5=e().message(f"Hi ").attach(path=image_path, inline=custom_cid) + custom_cid = "custom-name.jpg" + e5 = e().message(f"Hi ").attach(path=image_path, inline=custom_cid) self.check_lines(e5, (*single_alternative, "Hi ", @@ -1717,8 +1717,8 @@ def e(): *img_msg)) # Getting a name from the file name when contents is given - custom_filename="filename.gif" - e6=e().message(f"Hi ") \ + custom_filename = "filename.gif" + e6 = e().message(f"Hi ") \ .attach(image.read_bytes(), name=custom_filename, inline=True) self.check_lines(e6, (*single_alternative, @@ -1729,8 +1729,8 @@ def e(): # Getting a name from the file name when contents is given # Setting a name of an inline image - custom_filename="filename.jpg" - e7=e().message(f"Hi ") \ + custom_filename = "filename.jpg" + e7 = e().message(f"Hi ") \ .attach(image.read_bytes(), name=custom_filename, inline=custom_cid) self.check_lines(e7, (*single_alternative, @@ -1741,13 +1741,13 @@ def e(): class TestLoad(TestBash): - inline_image="tests/eml/inline_image.eml" + inline_image = "tests/eml/inline_image.eml" def test_load(self): self.assertEqual(Envelope.load("Subject: testing message").subject(), "testing message") def test_load_file(self): - e=Envelope.load(self.eml.read_text()) + e = Envelope.load(self.eml.read_text()) self.assertEqual(e.subject(), "Hello world subject") # multiple headers returned as list and in the same order @@ -1755,7 +1755,7 @@ def test_load_file(self): self.assertEqual(e.header("Received")[1][:26], "from receiver2.example.com") def test_encoded_headers(self): - e=Envelope.load(path=str(self.utf_header)) + e = Envelope.load(path=str(self.utf_header)) self.assertEqual(e.subject(), "Re: text") self.assertEqual("Jiří ", e.from_()) @@ -1772,12 +1772,12 @@ def test_encoded_headers(self): # When longer than certain number of characters, the method Parser.parse header.Header.encode() # returned chunks that were problematic to parse with policy.header_store_parse. # This will be treated as 'unknown-8bit' header. - address=Envelope.load("To: Novák Honza Name longer than 75 chars ").to()[0] + address = Envelope.load("To: Novák Honza Name longer than 75 chars ").to()[0] self.assertEqual("honza.novak@example.com", address.address) self.assertEqual("Novák Honza Name longer than 75 chars", address.name) # other than UTF-8 headers - iso_2="Subject: =?iso-8859-2?Q?=BE=E1dost_o_blokaci_dom=E9ny?=" + iso_2 = "Subject: =?iso-8859-2?Q?=BE=E1dost_o_blokaci_dom=E9ny?=" self.assertEqual("žádost o blokaci domény", Envelope.load(iso_2).subject()) def test_load_bash(self): @@ -1792,7 +1792,7 @@ def test_multiline_folded_header(self): self.bash("--subject", file=self.quopri)) def test_alternative_and_related(self): - e=Envelope.load(path=self.inline_image) + e = Envelope.load(path=self.inline_image) self.assertEqual("Hi ", e.message()) self.assertEqual("Inline image message", e.subject()) self.assertEqual("Plain alternative", e.message(alternative=PLAIN)) @@ -1822,7 +1822,7 @@ def test_group_recipient(self): # with self.assertLogs('envelope', level='WARNING') as cm: # e = Envelope.load(self.group_recipient) # self.assertEqual(cm.output, [msg]) - e=Envelope.load(self.group_recipient) + e = Envelope.load(self.group_recipient) self.assertEqual([], e.to()) self.assertEqual("From Alice Smith", e.subject()) @@ -1830,19 +1830,19 @@ def test_group_recipient(self): self.assertEqual({"hi", "hi2"}, Envelope.load("To: group: hi; group b: hi2;").recipients()) def test_invalid_characters(self): - msg="WARNING:envelope.parser:Replacing some invalid characters in text/plain:" \ - " 'utf-8' codec can't decode byte 0xe1 in position 1: invalid continuation byte" + msg = "WARNING:envelope.parser:Replacing some invalid characters in text/plain:" \ + " 'utf-8' codec can't decode byte 0xe1 in position 1: invalid continuation byte" with self.assertLogs('envelope', level='WARNING') as cm: - e=Envelope.load(self.invalid_characters) + e = Envelope.load(self.invalid_characters) self.assertEqual(cm.output, [msg]) - text='V�\x17Een� z�kazn�ku!\n Va\x161e z�silka bude' + text = 'V�\x17Een� z�kazn�ku!\n Va\x161e z�silka bude' self.assertEqual(text, e.message(alternative="plain")[:len(text)]) - html='

Vážený' + html = '

Vážený' self.assertEqual(html, e.message()[:len(html)]) # test subject decoded from base64 - subject="Vaše zásilka ceká na dorucení" + subject = "Vaše zásilka ceká na dorucení" self.assertEqual(subject, e.subject()) # test header internationalized @@ -1852,12 +1852,12 @@ def test_invalid_characters(self): def test_invalid_headers(self): """ Following file has some invalid headers whose parsing would normally fail. """ - msg=['WARNING:envelope.envelope:Header List-Unsubscribe could not be successfully ' + msg = ['WARNING:envelope.envelope:Header List-Unsubscribe could not be successfully ' "loaded with : 'Header' object is not subscriptable", 'WARNING:envelope.parser:Replacing some invalid characters in text/html: ' 'unknown encoding: "utf-8message-id: <123456@example.com>'] with self.assertLogs('envelope', level='WARNING') as cm: - e=Envelope.load(self.invalid_headers) + e = Envelope.load(self.invalid_headers) if (3, 6) == (sys.version_info.major, sys.version_info.minor): # XX drop with Python3.6 support self.assertIn("support indexing", cm.output[0]) else: @@ -1868,20 +1868,20 @@ def test_invalid_headers(self): class TestTransfer(TestBash): - long_text="J'interdis aux marchands de vanter trop leurs marchandises." \ - " Car ils se font vite pédagogues et t'enseignent comme but ce qui n'est par essence qu'un moyen," \ - " et te trompant ainsi sur la route à suivre les voilà bientôt qui te dégradent," \ + long_text = "J'interdis aux marchands de vanter trop leurs marchandises." \ + " Car ils se font vite pédagogues et t'enseignent comme but ce qui n'est par essence qu'un moyen," \ + " et te trompant ainsi sur la route à suivre les voilà bientôt qui te dégradent," \ " car si leur musique est vulgaire ils te fabriquent pour te la vendre une âme vulgaire." - quoted="J'interdis aux marchands de vanter trop leurs marchandises. Car ils se font v=" \ - "\nite p=C3=A9dagogues et t'enseignent comme but ce qui n'est par essence qu'un =" \ - "\nmoyen, et te trompant ainsi sur la route =C3=A0 suivre les voil=C3=A0 bient=" \ + quoted = "J'interdis aux marchands de vanter trop leurs marchandises. Car ils se font v=" \ + "\nite p=C3=A9dagogues et t'enseignent comme but ce qui n'est par essence qu'un =" \ + "\nmoyen, et te trompant ainsi sur la route =C3=A0 suivre les voil=C3=A0 bient=" \ "\n=C3=B4t qui te d=C3=A9gradent, car si leur musique est vulgaire ils te fabriq=" \ "\nuent pour te la vendre une =C3=A2me vulgaire." def _quoted_message(self, e: Envelope): self.assertEqual(self.long_text, e.message()) self.assertIn(self.long_text, e.preview()) # when using preview, we receive original text - output=str(e.send(False)) # but when sending, quoted text is got instead + output = str(e.send(False)) # but when sending, quoted text is got instead self.assertNotIn(self.long_text, output) self.assertIn(self.quoted, output) @@ -1900,22 +1900,22 @@ def test_quoted_printable_bash(self): self.assertEqual(self.long_text, self.bash("--message", file=self.quopri)) def test_base64(self): - hello="aGVsbG8gd29ybGQ=" + hello = "aGVsbG8gd29ybGQ=" self.assertEqual(Envelope.load(f"\n{hello}").message(), hello) self.assertEqual(Envelope.load(f"Content-Transfer-Encoding: base64\n\n{hello}").message(), "hello world") def test_implanted_transfer(self): - e=(Envelope().header("Content-Transfer-Encoding", "quoted-printable").message(self.quoted)) + e = (Envelope().header("Content-Transfer-Encoding", "quoted-printable").message(self.quoted)) self.assertEqual(self.long_text, e.message()) # we replace Content-Transfer-Encoding and change the message - original="hello world" - hello="aGVsbG8gd29ybGQ=" - e=(Envelope().header("Content-Transfer-Encoding", "base64").message(hello)) + original = "hello world" + hello = "aGVsbG8gd29ybGQ=" + e = (Envelope().header("Content-Transfer-Encoding", "base64").message(hello)) self.assertEqual(original, e.message()) # the user specified Content-Transfer-Encoding but left the message unencoded - e2=(Envelope().header("Content-Transfer-Encoding", "base64").message(original)) + e2 = (Envelope().header("Content-Transfer-Encoding", "base64").message(original)) self.assertEqual(original, e2.message()) @@ -1931,32 +1931,32 @@ def test_smtp_parameters(self): class TestReport(TestAbstract): - xarf=Path("tests/eml/multipart-report-xarf.eml") + xarf = Path("tests/eml/multipart-report-xarf.eml") def test_loading_xarf(self): # no report in an empty object self.assertFalse(Envelope()._report()) # expected XARF report - e=Envelope.load(self.xarf) - report=e._report() + e = Envelope.load(self.xarf) + report = e._report() self.assertSubset(report["ReporterInfo"], {"ReporterOrg": 'Example'}) self.assertSubset(report["Report"], {'SourceIp': '192.0.2.1'}) def test_unsupported_message(self): # only `Content-Type: message/feedback-report` is implemented within `multipart/report`` - t=self.xarf.read_text().replace("Content-Type: message/feedback-report", + t = self.xarf.read_text().replace("Content-Type: message/feedback-report", "Content-Type: message/UNSUPPORTED") - msg="WARNING:envelope.envelope:Message might not have been loaded correctly. " \ + msg = "WARNING:envelope.envelope:Message might not have been loaded correctly. " \ "Parsing multipart/report / message/unsupported not implemented." with self.assertLogs('envelope', level='WARNING') as cm: Envelope.load(t) self.assertEqual(cm.output, [msg]) # `Content-Type: message` is not implemented within `multipart/mixed` - msg="WARNING:envelope.envelope:Message might not have been loaded correctly. "\ + msg = "WARNING:envelope.envelope:Message might not have been loaded correctly. "\ "Parsing multipart/mixed / message/feedback-report failed or not implemented." - t=self.xarf.read_text().replace("Content-Type: multipart/report", + t = self.xarf.read_text().replace("Content-Type: multipart/report", "Content-Type: multipart/mixed") with self.assertLogs('envelope', level='WARNING') as cm: Envelope.load(t)