8
8
import sys
9
9
from tempfile import NamedTemporaryFile
10
10
from unittest import mock
11
- import warnings
12
- from base64 import b64decode , b64encode
11
+ from base64 import b64decode
13
12
from configparser import ConfigParser
14
13
from copy import deepcopy
15
14
from email import message_from_bytes
16
15
from email .header import decode_header
17
16
from email .generator import Generator
18
17
from email .message import EmailMessage , Message
19
18
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
22
20
from itertools import chain
23
- from os import environ , urandom
21
+ from os import environ
24
22
from pathlib import Path
25
23
from quopri import decodestring
26
24
from types import GeneratorType
27
- from typing import Literal , Union , List , Set , Optional , Any
25
+ from typing import Literal , Union , Optional , Any
28
26
29
- from .address import Address
27
+ from .address import Address , _getaddresses
30
28
from .attachment import Attachment
31
29
from .constants import ISSUE_LINK , smime_import_error , gnupg , CRLF , AUTO , PLAIN , HTML , SIMULATION , SAFE_LOCALE
32
30
from .message import _Message
35
33
from .utils import AutoSubmittedHeader , Fetched , is_gpg_importable_key , assure_list , assure_fetched , get_mimetype
36
34
37
35
38
-
39
36
__doc__ = """Quick layer over python-gnupg, M2Crypto, smtplib, magic and email handling packages.
40
37
Their common use cases merged into a single function. Want to sign a text and tired of forgetting how to do it right?
41
38
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
320
317
# that explicitly states we have no from header
321
318
self ._from : Union [Address , False , None ] = None # e-mail From header
322
319
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 ] = []
327
324
self ._subject : Union [str , None ] = None
328
325
self ._subject_encrypted : Union [str , bool ] = True
329
326
self ._smtp = None
330
- self ._attachments : List [Attachment ] = []
327
+ self ._attachments : list [Attachment ] = []
331
328
self ._mime = AUTO
332
329
self ._nl2br = AUTO
333
330
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
336
333
# variables defined while processing
337
334
self ._status : bool = False # whether we successfully encrypted/signed/send
338
335
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
340
337
self ._result_cache : Optional [str ] = None
341
338
self ._result_cache_hash : Optional [int ] = None
342
339
self ._smtp = SMTPHandler ()
@@ -407,32 +404,32 @@ def _parse_addresses(registry, email_or_more):
407
404
if addresses :
408
405
registry += (a for a in Address .parse (addresses ) if a not in registry )
409
406
410
- def to (self , email_or_more = None ) -> Union ["Envelope" , List [Address ]]:
407
+ def to (self , email_or_more = None ) -> Union ["Envelope" , list [Address ]]:
411
408
""" Multiple addresses may be given in a string, delimited by comma (or semicolon).
412
409
(The same is valid for `to`, `cc`, `bcc` and `reply-to`.)
413
410
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]
415
412
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
417
414
"""
418
415
if email_or_more is None :
419
416
return self ._to
420
417
self ._parse_addresses (self ._to , email_or_more )
421
418
return self
422
419
423
- def cc (self , email_or_more = None ) -> Union ["Envelope" , List [Address ]]:
420
+ def cc (self , email_or_more = None ) -> Union ["Envelope" , list [Address ]]:
424
421
if email_or_more is None :
425
422
return self ._cc
426
423
self ._parse_addresses (self ._cc , email_or_more )
427
424
return self
428
425
429
- def bcc (self , email_or_more = None ) -> Union ["Envelope" , List [Address ]]:
426
+ def bcc (self , email_or_more = None ) -> Union ["Envelope" , list [Address ]]:
430
427
if email_or_more is None :
431
428
return self ._bcc
432
429
self ._parse_addresses (self ._bcc , email_or_more )
433
430
return self
434
431
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 ]]:
436
433
if email_or_more is None :
437
434
return self ._reply_to
438
435
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
586
583
self ._subject_encrypted = encrypted
587
584
return self
588
585
589
- def mime (self , subtype = AUTO , nl2br : Literal ["auto" ] | bool = AUTO ):
586
+ def mime (self , subtype = AUTO , nl2br : Literal ["auto" ] | bool = AUTO ):
590
587
"""
591
588
Ignored if `Content-Type` header put to the message.
592
589
@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):
1084
1081
else :
1085
1082
raise ValueError (f"Could not import key starting: { key [:80 ]} ..." )
1086
1083
1087
-
1088
1084
def _get_gnupg_home (self , for_help = False ):
1089
1085
s = self ._gpg if type (self ._gpg ) is str else None
1090
1086
if for_help :
@@ -1127,14 +1123,16 @@ def _send_now(self, email, encrypt, encrypted_subject, send):
1127
1123
email ["Message-ID" ] = make_msgid ()
1128
1124
1129
1125
if send and send != SIMULATION :
1126
+ recipients = list (map (str , set (self ._to + self ._cc + self ._bcc )))
1130
1127
with mock .patch .object (Generator , '_handle_multipart_signed' , Generator ._handle_multipart ):
1131
1128
# https://github.com/python/cpython/issues/99533 and #19
1132
1129
failures = self ._smtp .send_message (email ,
1133
1130
from_addr = self ._from_addr ,
1134
- to_addrs = list ( map ( str , set ( self . _to + self . _cc + self . _bcc ))) )
1131
+ to_addrs = recipients )
1135
1132
if failures :
1136
1133
logger .warning (f"Unable to send to all recipients: { repr (failures )} ." )
1137
1134
elif failures is False :
1135
+ # TODO add here and test, logger.warning(f"Sending {recipients}, Message-ID: {email["Message-ID"]}")
1138
1136
return False
1139
1137
else :
1140
1138
if send != SIMULATION :
@@ -1234,7 +1232,7 @@ def _encrypt_gpg_now(self, message, encrypt, sign_fingerprint):
1234
1232
return False
1235
1233
1236
1234
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" ]))
1238
1236
1239
1237
def _gpg_verify (self , signature : bytes , data : bytes ):
1240
1238
""" Allows verifying detached GPG signature.
@@ -1248,7 +1246,7 @@ def _gpg_verify(self, signature: bytes, data: bytes):
1248
1246
fp .seek (0 )
1249
1247
return bool (self ._gnupg .verify_data (fp .name , data ))
1250
1248
1251
- def _get_decipherers (self ) -> Set [str ]:
1249
+ def _get_decipherers (self ) -> set [str ]:
1252
1250
"""
1253
1251
:return: Set of e-mail addresses
1254
1252
"""
@@ -1297,7 +1295,6 @@ def smime_sign_only(self, email, sign):
1297
1295
1298
1296
return signed_email
1299
1297
1300
-
1301
1298
def smime_sign_encrypt (self , email , sign , encrypt ):
1302
1299
from cryptography .hazmat .primitives .serialization import load_pem_private_key , pkcs7
1303
1300
from cryptography .x509 import load_pem_x509_certificate
@@ -1345,7 +1342,7 @@ def smime_sign_encrypt(self, email, sign, encrypt):
1345
1342
raise ValueError ("failed to load certificate from file" )
1346
1343
1347
1344
recipient_certs .append (c )
1348
-
1345
+
1349
1346
try :
1350
1347
pubkey = load_pem_x509_certificate (pubkey )
1351
1348
except ValueError as e :
@@ -1358,7 +1355,6 @@ def smime_sign_encrypt(self, email, sign, encrypt):
1358
1355
for recip in recipient_certs :
1359
1356
envelope_builder = envelope_builder .add_recipient (recip )
1360
1357
1361
-
1362
1358
options = [pkcs7 .PKCS7Options .Binary ]
1363
1359
encrypted_email = envelope_builder .encrypt (serialization .Encoding .SMIME , options )
1364
1360
return encrypted_email
@@ -1369,7 +1365,6 @@ def smime_encrypt_only(self, email, encrypt):
1369
1365
from cryptography .x509 import load_pem_x509_certificate
1370
1366
from cryptography .hazmat .primitives import serialization
1371
1367
1372
-
1373
1368
if self ._cert :
1374
1369
certificates = [self ._cert ]
1375
1370
else :
@@ -1394,16 +1389,15 @@ def smime_encrypt_only(self, email, encrypt):
1394
1389
1395
1390
return encrypted_email
1396
1391
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 ]]):
1399
1393
"""
1400
1394
1401
1395
:type encrypt: Can be None, False, bytes or list[bytes]
1402
1396
"""
1403
1397
1404
1398
# passphrase has to be bytes
1405
1399
if (self ._passphrase is not None ):
1406
- self ._passphrase = self ._passphrase .encode ('utf-8' )
1400
+ self ._passphrase = self ._passphrase .encode ('utf-8' )
1407
1401
1408
1402
if sign is not None and type (sign ) != bool :
1409
1403
sign = assure_fetched (sign , bytes )
@@ -1419,8 +1413,6 @@ def _encrypt_smime_now(self, email, sign, encrypt: Union[None, bool, bytes, List
1419
1413
1420
1414
return output
1421
1415
1422
-
1423
-
1424
1416
def _compose_gpg_signed (self , email , text , micalg = None ):
1425
1417
msg_payload = email
1426
1418
email = EmailMessage ()
@@ -1598,7 +1590,7 @@ def _prepare_email(self, plain: bytes, html: bytes, encrypt_gpg: bool, sign_gpg:
1598
1590
email ["Subject" ] = self ._subject
1599
1591
return email
1600
1592
1601
- def recipients (self , * , clear = False ) -> Union [Set [Address ], 'Envelope' ]:
1593
+ def recipients (self , * , clear = False ) -> Union [set [Address ], 'Envelope' ]:
1602
1594
""" Return set of all recipients – To, Cc, Bcc
1603
1595
:param: clear If true, all To, Cc and Bcc recipients are removed and the object is returned.
1604
1596
@@ -1630,7 +1622,7 @@ def _report(self) -> Union[dict, None]:
1630
1622
raise NotImplemented ("Current multipart/report has not been impemented."
1631
1623
f"Please post current message as a new issue at { ISSUE_LINK } " )
1632
1624
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 ]:
1634
1626
""" Access the attachments.
1635
1627
XX make available from CLI too
1636
1628
--attachments(-inline)(-enclosed) [name]
0 commit comments