From 87a5711cae603d29abd0963cfb5aed4839e26282 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zden=C4=9Bk=20Frydr=C3=BDn?= <51875432+adidas-official@users.noreply.github.com> Date: Fri, 17 Jan 2025 16:07:50 +0100 Subject: [PATCH] exotic email addresses Test exotic email addresses for v3.11+ Added method to import cryptography library. Keep current address parsing behaviour for Python3.12 Properly encrypt and sign to multiple recipients - will fix #41 (#42) Refactor envelope.py to support multiple recipient certificates and to use binary PKCS7 options for encryption --------- Co-authored-by: G22147 Co-authored-by: zfydryn --- .github/workflows/run-unittest.yml | 6 ++---- envelope/address.py | 10 +++++++++ envelope/constants.py | 2 +- envelope/envelope.py | 33 ++++++++++++++++++------------ test_.py | 11 ++++++++-- 5 files changed, 42 insertions(+), 20 deletions(-) diff --git a/.github/workflows/run-unittest.yml b/.github/workflows/run-unittest.yml index 38b0006..a0698b9 100644 --- a/.github/workflows/run-unittest.yml +++ b/.github/workflows/run-unittest.yml @@ -5,9 +5,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - # python-version: ["3.10", 3.11, 3.12, 3.13] - # TODO - python-version: [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 }} @@ -37,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/envelope/address.py b/envelope/address.py index e7cba04..25b5e96 100644 --- a/envelope/address.py +++ b/envelope/address.py @@ -57,10 +57,12 @@ def __new__(cls, displayed_email=None, name=None, address=None): if 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 @@ -171,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 @@ -180,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 aeb1394..0b4f56d 100644 --- a/envelope/envelope.py +++ b/envelope/envelope.py @@ -402,7 +402,12 @@ 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) + # 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). @@ -1252,10 +1257,17 @@ def _get_decipherers(self) -> set[str]: """ 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 @@ -1296,9 +1308,7 @@ 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 @@ -1309,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: @@ -1342,7 +1352,6 @@ def smime_sign_encrypt(self, email, sign, encrypt): raise ValueError("failed to load certificate from file") recipient_certs.append(c) - try: pubkey = load_pem_x509_certificate(pubkey) except ValueError as e: @@ -1361,9 +1370,7 @@ def smime_sign_encrypt(self, email, sign, encrypt): 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] @@ -1379,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: diff --git a/test_.py b/test_.py index cf63952..74dc70d 100644 --- a/test_.py +++ b/test_.py @@ -538,7 +538,7 @@ def test_smime_decrypt_attachments(self): 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() + "==" + d = data_temp.split('\n\n')[1].strip() + "==" attachment_content = b64decode(d).decode('utf-8') with open(self.text_attachment, 'r') as f: @@ -1242,6 +1242,10 @@ 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 + + if sys.version_info < (3, 11): + return + disguise_addr = "first@example.cz " same = "person@example.com " self.assertEqual(('', 'first@example.cz'), _parseaddr(disguise_addr)) @@ -1526,6 +1530,10 @@ 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. """ + + if sys.version_info < (3, 11): + return + 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)) @@ -1533,7 +1541,6 @@ def test_invalid_email_addresses(self): 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