Skip to content

Commit

Permalink
exotic email addresses
Browse files Browse the repository at this point in the history
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 <[email protected]>
Co-authored-by: zfydryn <[email protected]>
  • Loading branch information
adidas-official authored and e3rd committed Jan 17, 2025
1 parent a8946be commit 87a5711
Show file tree
Hide file tree
Showing 5 changed files with 42 additions and 20 deletions.
6 changes: 2 additions & 4 deletions .github/workflows/run-unittest.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}
Expand Down Expand Up @@ -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
10 changes: 10 additions & 0 deletions envelope/address.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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 <[email protected]>" 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 "[email protected], <[email protected]>" 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: "[email protected] <[email protected]>"
Expand Down
2 changes: 1 addition & 1 deletion envelope/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -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?
Expand Down
33 changes: 20 additions & 13 deletions envelope/envelope.py
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand All @@ -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:
Expand Down Expand Up @@ -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:
Expand All @@ -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]
Expand All @@ -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:
Expand Down
11 changes: 9 additions & 2 deletions test_.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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 = "[email protected] <[email protected]>"
same = "[email protected] <[email protected]>"
self.assertEqual(('', '[email protected]'), _parseaddr(disguise_addr))
Expand Down Expand Up @@ -1526,14 +1530,17 @@ 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('[email protected], [invalid!email], [email protected]'))
self.assertEqual(3, len(e.to()))
self.assertFalse(e.check(check_mx=False, check_smtp=False))

e = (Envelope().to('[email protected], [email protected]'))
self.assertTrue(e.check(check_mx=False, check_smtp=False))


class TestSupportive(TestAbstract):
def test_copy(self):
factory = Envelope().cc("[email protected]").copy
Expand Down

0 comments on commit 87a5711

Please sign in to comment.