Skip to content

Commit a8946be

Browse files
committed
keep current address parsing behaviour for Python3.12
1 parent 05d7743 commit a8946be

File tree

6 files changed

+258
-245
lines changed

6 files changed

+258
-245
lines changed

.github/workflows/run-unittest.yml

+5-2
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@ jobs:
55
runs-on: ubuntu-latest
66
strategy:
77
matrix:
8-
python-version: ["3.10", 3.11, 3.12, 3.13]
8+
# python-version: ["3.10", 3.11, 3.12, 3.13]
9+
# TODO
10+
python-version: [3.11, 3.12]
911
steps:
1012
- uses: actions/checkout@v2
1113
- name: Set up Python ${{ matrix.python-version }}
@@ -15,7 +17,8 @@ jobs:
1517
- name: Install dependencies
1618
run: |
1719
python -m pip install --upgrade pip
18-
pip install M2Crypto
20+
pip install M2Crypto # TODO remove when tests are updated
21+
pip install cryptography
1922
pip install -e .
2023
- name: Run tests
2124
run: python3 test_.py

README.md

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Envelope
22

3-
[![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)
3+
[![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)
44

55
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.
66
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()
7777
```
7878
* Or just download the project and launch `python3 -m envelope`
7979
* 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.
80-
* If planning to use S/MIME, you should ensure some prerequisites: `sudo apt install swig build-essential python3-dev libssl-dev && pip3 install M2Crypto`
80+
* 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`
8181
* If planning to send e-mails, prepare SMTP credentials or visit [Configure your SMTP](#configure-your-smtp) tutorial.
8282
* If your e-mails are to be received outside your local domain, visit [DMARC](#dmarc) section.
8383
* 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`.

envelope/address.py

+20-2
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import logging
33
from os import environ
44
import re
5+
import sys
56
from .utils import assure_list
67

78
environ['PY3VE_IGNORE_UPDATER'] = '1'
@@ -11,6 +12,23 @@
1112
logger = logging.getLogger(__name__)
1213

1314

15+
def _getaddresses(*args):
16+
# NOTE Python finally changed the old way of parsing wrong addresses.
17+
# We might start using strict=True (default) in the future.
18+
if sys.version_info <= (3, 11):
19+
return getaddresses(*args)
20+
return getaddresses(*args, strict=False)
21+
22+
23+
def _parseaddr(*args):
24+
# NOTE Python finally changed the old way of parsing wrong addresses.
25+
# We might start using strict=True (default) in the future.
26+
# README should reflect that.
27+
if sys.version_info <= (3, 11):
28+
return parseaddr(*args)
29+
return parseaddr(*args, strict=False)
30+
31+
1432
class Address(str):
1533
"""
1634
You can safely access the `self.name` property to access the real name and `self.address` to access the e-mail address.
@@ -37,7 +55,7 @@ class Address(str):
3755

3856
def __new__(cls, displayed_email=None, name=None, address=None):
3957
if displayed_email:
40-
v = parseaddr(cls.remedy(displayed_email))
58+
v = _parseaddr(cls.remedy(displayed_email))
4159
name, address = v[0] or name, v[1] or address
4260
if name:
4361
displayed_email = f"{name} <{address}>"
@@ -143,7 +161,7 @@ def parse(cls, email_or_list, single=False, allow_false=False):
143161
if allow_false and email_or_list is False:
144162
return False
145163

146-
addrs = getaddresses(cls.remedy(x) for x in assure_list(email_or_list))
164+
addrs = _getaddresses(cls.remedy(x) for x in assure_list(email_or_list))
147165
addresses = [Address(name=real_name, address=address)
148166
for real_name, address in addrs if not (real_name == address == "")]
149167
if single:

envelope/envelope.py

+28-36
Original file line numberDiff line numberDiff line change
@@ -8,25 +8,23 @@
88
import sys
99
from tempfile import NamedTemporaryFile
1010
from unittest import mock
11-
import warnings
12-
from base64 import b64decode, b64encode
11+
from base64 import b64decode
1312
from configparser import ConfigParser
1413
from copy import deepcopy
1514
from email import message_from_bytes
1615
from email.header import decode_header
1716
from email.generator import Generator
1817
from email.message import EmailMessage, Message
1918
from email.parser import BytesParser
20-
from email.utils import make_msgid, formatdate, getaddresses
21-
from getpass import getpass
19+
from email.utils import make_msgid, formatdate
2220
from itertools import chain
23-
from os import environ, urandom
21+
from os import environ
2422
from pathlib import Path
2523
from quopri import decodestring
2624
from types import GeneratorType
27-
from typing import Literal, Union, List, Set, Optional, Any
25+
from typing import Literal, Union, Optional, Any
2826

29-
from .address import Address
27+
from .address import Address, _getaddresses
3028
from .attachment import Attachment
3129
from .constants import ISSUE_LINK, smime_import_error, gnupg, CRLF, AUTO, PLAIN, HTML, SIMULATION, SAFE_LOCALE
3230
from .message import _Message
@@ -35,7 +33,6 @@
3533
from .utils import AutoSubmittedHeader, Fetched, is_gpg_importable_key, assure_list, assure_fetched, get_mimetype
3634

3735

38-
3936
__doc__ = """Quick layer over python-gnupg, M2Crypto, smtplib, magic and email handling packages.
4037
Their common use cases merged into a single function. Want to sign a text and tired of forgetting how to do it right?
4138
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
320317
# that explicitly states we have no from header
321318
self._from: Union[Address, False, None] = None # e-mail From header
322319
self._from_addr: Optional[Address] = None # SMTP envelope MAIL FROM address
323-
self._to: List[Address] = []
324-
self._cc: List[Address] = []
325-
self._bcc: List[Address] = []
326-
self._reply_to: List[Address] = []
320+
self._to: list[Address] = []
321+
self._cc: list[Address] = []
322+
self._bcc: list[Address] = []
323+
self._reply_to: list[Address] = []
327324
self._subject: Union[str, None] = None
328325
self._subject_encrypted: Union[str, bool] = True
329326
self._smtp = None
330-
self._attachments: List[Attachment] = []
327+
self._attachments: list[Attachment] = []
331328
self._mime = AUTO
332329
self._nl2br = AUTO
333330
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
336333
# variables defined while processing
337334
self._status: bool = False # whether we successfully encrypted/signed/send
338335
self._processed: bool = False # prevent the user from mistakenly call .sign().send() instead of .signature().send()
339-
self._result: List[Union[str, EmailMessage, Message]] = [] # text output for str() conversion
336+
self._result: list[Union[str, EmailMessage, Message]] = [] # text output for str() conversion
340337
self._result_cache: Optional[str] = None
341338
self._result_cache_hash: Optional[int] = None
342339
self._smtp = SMTPHandler()
@@ -407,32 +404,32 @@ def _parse_addresses(registry, email_or_more):
407404
if addresses:
408405
registry += (a for a in Address.parse(addresses) if a not in registry)
409406

410-
def to(self, email_or_more=None) -> Union["Envelope", List[Address]]:
407+
def to(self, email_or_more=None) -> Union["Envelope", list[Address]]:
411408
""" Multiple addresses may be given in a string, delimited by comma (or semicolon).
412409
(The same is valid for `to`, `cc`, `bcc` and `reply-to`.)
413410
414-
:param email_or_more: str|Tuple[str]|List[str]|Generator[str]|Set[str]|Frozenset[str]
411+
:param email_or_more: str|Tuple[str]|list[str]|Generator[str]|Set[str]|Frozenset[str]
415412
Set e-mail address/es. If None, we are reading.
416-
return: Envelope if `email_or_more` set or List[Address] if not set
413+
return: Envelope if `email_or_more` set or list[Address] if not set
417414
"""
418415
if email_or_more is None:
419416
return self._to
420417
self._parse_addresses(self._to, email_or_more)
421418
return self
422419

423-
def cc(self, email_or_more=None) -> Union["Envelope", List[Address]]:
420+
def cc(self, email_or_more=None) -> Union["Envelope", list[Address]]:
424421
if email_or_more is None:
425422
return self._cc
426423
self._parse_addresses(self._cc, email_or_more)
427424
return self
428425

429-
def bcc(self, email_or_more=None) -> Union["Envelope", List[Address]]:
426+
def bcc(self, email_or_more=None) -> Union["Envelope", list[Address]]:
430427
if email_or_more is None:
431428
return self._bcc
432429
self._parse_addresses(self._bcc, email_or_more)
433430
return self
434431

435-
def reply_to(self, email_or_more=None) -> Union["Envelope", List[Address]]:
432+
def reply_to(self, email_or_more=None) -> Union["Envelope", list[Address]]:
436433
if email_or_more is None:
437434
return self._reply_to
438435
self._parse_addresses(self._reply_to, email_or_more)
@@ -586,7 +583,7 @@ def subject(self, subject=None, encrypted: Union[str, bool] = None) -> Union["En
586583
self._subject_encrypted = encrypted
587584
return self
588585

589-
def mime(self, subtype=AUTO, nl2br: Literal["auto"] | bool=AUTO):
586+
def mime(self, subtype=AUTO, nl2br: Literal["auto"] | bool = AUTO):
590587
"""
591588
Ignored if `Content-Type` header put to the message.
592589
@type subtype: str Set contents mime subtype: "auto" (default), "html" or "plain" for plain text.
@@ -1084,7 +1081,6 @@ def _gpg_import_or_fail(self, key):
10841081
else:
10851082
raise ValueError(f"Could not import key starting: {key[:80]}...")
10861083

1087-
10881084
def _get_gnupg_home(self, for_help=False):
10891085
s = self._gpg if type(self._gpg) is str else None
10901086
if for_help:
@@ -1127,14 +1123,16 @@ def _send_now(self, email, encrypt, encrypted_subject, send):
11271123
email["Message-ID"] = make_msgid()
11281124

11291125
if send and send != SIMULATION:
1126+
recipients = list(map(str, set(self._to + self._cc + self._bcc)))
11301127
with mock.patch.object(Generator, '_handle_multipart_signed', Generator._handle_multipart):
11311128
# https://github.com/python/cpython/issues/99533 and #19
11321129
failures = self._smtp.send_message(email,
11331130
from_addr=self._from_addr,
1134-
to_addrs=list(map(str, set(self._to + self._cc + self._bcc))))
1131+
to_addrs=recipients)
11351132
if failures:
11361133
logger.warning(f"Unable to send to all recipients: {repr(failures)}.")
11371134
elif failures is False:
1135+
# TODO add here and test, logger.warning(f"Sending {recipients}, Message-ID: {email["Message-ID"]}")
11381136
return False
11391137
else:
11401138
if send != SIMULATION:
@@ -1234,7 +1232,7 @@ def _encrypt_gpg_now(self, message, encrypt, sign_fingerprint):
12341232
return False
12351233

12361234
def _gpg_list_keys(self, secret=False):
1237-
return ((key, address) for key in self._gnupg.list_keys(secret) for _, address in getaddresses(key["uids"]))
1235+
return ((key, address) for key in self._gnupg.list_keys(secret) for _, address in _getaddresses(key["uids"]))
12381236

12391237
def _gpg_verify(self, signature: bytes, data: bytes):
12401238
""" Allows verifying detached GPG signature.
@@ -1248,7 +1246,7 @@ def _gpg_verify(self, signature: bytes, data: bytes):
12481246
fp.seek(0)
12491247
return bool(self._gnupg.verify_data(fp.name, data))
12501248

1251-
def _get_decipherers(self) -> Set[str]:
1249+
def _get_decipherers(self) -> set[str]:
12521250
"""
12531251
:return: Set of e-mail addresses
12541252
"""
@@ -1297,7 +1295,6 @@ def smime_sign_only(self, email, sign):
12971295

12981296
return signed_email
12991297

1300-
13011298
def smime_sign_encrypt(self, email, sign, encrypt):
13021299
from cryptography.hazmat.primitives.serialization import load_pem_private_key, pkcs7
13031300
from cryptography.x509 import load_pem_x509_certificate
@@ -1345,7 +1342,7 @@ def smime_sign_encrypt(self, email, sign, encrypt):
13451342
raise ValueError("failed to load certificate from file")
13461343

13471344
recipient_certs.append(c)
1348-
1345+
13491346
try:
13501347
pubkey = load_pem_x509_certificate(pubkey)
13511348
except ValueError as e:
@@ -1358,7 +1355,6 @@ def smime_sign_encrypt(self, email, sign, encrypt):
13581355
for recip in recipient_certs:
13591356
envelope_builder = envelope_builder.add_recipient(recip)
13601357

1361-
13621358
options = [pkcs7.PKCS7Options.Binary]
13631359
encrypted_email = envelope_builder.encrypt(serialization.Encoding.SMIME, options)
13641360
return encrypted_email
@@ -1369,7 +1365,6 @@ def smime_encrypt_only(self, email, encrypt):
13691365
from cryptography.x509 import load_pem_x509_certificate
13701366
from cryptography.hazmat.primitives import serialization
13711367

1372-
13731368
if self._cert:
13741369
certificates = [self._cert]
13751370
else:
@@ -1394,16 +1389,15 @@ def smime_encrypt_only(self, email, encrypt):
13941389

13951390
return encrypted_email
13961391

1397-
1398-
def _encrypt_smime_now(self, email, sign, encrypt: Union[None, bool, bytes, List[bytes]]):
1392+
def _encrypt_smime_now(self, email, sign, encrypt: Union[None, bool, bytes, list[bytes]]):
13991393
"""
14001394
14011395
:type encrypt: Can be None, False, bytes or list[bytes]
14021396
"""
14031397

14041398
# passphrase has to be bytes
14051399
if (self._passphrase is not None):
1406-
self._passphrase = self._passphrase.encode('utf-8')
1400+
self._passphrase = self._passphrase.encode('utf-8')
14071401

14081402
if sign is not None and type(sign) != bool:
14091403
sign = assure_fetched(sign, bytes)
@@ -1419,8 +1413,6 @@ def _encrypt_smime_now(self, email, sign, encrypt: Union[None, bool, bytes, List
14191413

14201414
return output
14211415

1422-
1423-
14241416
def _compose_gpg_signed(self, email, text, micalg=None):
14251417
msg_payload = email
14261418
email = EmailMessage()
@@ -1598,7 +1590,7 @@ def _prepare_email(self, plain: bytes, html: bytes, encrypt_gpg: bool, sign_gpg:
15981590
email["Subject"] = self._subject
15991591
return email
16001592

1601-
def recipients(self, *, clear=False) -> Union[Set[Address], 'Envelope']:
1593+
def recipients(self, *, clear=False) -> Union[set[Address], 'Envelope']:
16021594
""" Return set of all recipients – To, Cc, Bcc
16031595
:param: clear If true, all To, Cc and Bcc recipients are removed and the object is returned.
16041596
@@ -1630,7 +1622,7 @@ def _report(self) -> Union[dict, None]:
16301622
raise NotImplemented("Current multipart/report has not been impemented."
16311623
f"Please post current message as a new issue at {ISSUE_LINK}")
16321624

1633-
def attachments(self, name=None, inline=None) -> Union[Attachment, List[Attachment], bool]:
1625+
def attachments(self, name=None, inline=None) -> Union[Attachment, list[Attachment], bool]:
16341626
""" Access the attachments.
16351627
XX make available from CLI too
16361628
--attachments(-inline)(-enclosed) [name]

setup.py

+1-2
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,7 @@
3232
long_description_content_type="text/markdown",
3333
install_requires=install_requires,
3434
extras_require={
35-
"smime": ["M2Crypto", # need to have: `sudo apt install swig`
36-
"cryptography>=43"]
35+
"smime": ["cryptography>=43"]
3736
},
3837
entry_points={
3938
'console_scripts': [

0 commit comments

Comments
 (0)