envelope 2.2.0__tar.gz → 2.3.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {envelope-2.2.0 → envelope-2.3.0}/PKG-INFO +12 -2
- {envelope-2.2.0 → envelope-2.3.0}/README.md +11 -1
- {envelope-2.2.0 → envelope-2.3.0}/envelope/__main__.py +5 -1
- envelope-2.3.0/envelope/_auth.py +79 -0
- {envelope-2.2.0 → envelope-2.3.0}/envelope/envelope.py +52 -19
- {envelope-2.2.0 → envelope-2.3.0}/envelope/smtp_handler.py +5 -3
- {envelope-2.2.0 → envelope-2.3.0}/pyproject.toml +1 -1
- {envelope-2.2.0 → envelope-2.3.0}/LICENSE +0 -0
- {envelope-2.2.0 → envelope-2.3.0}/envelope/__init__.py +0 -0
- {envelope-2.2.0 → envelope-2.3.0}/envelope/address.py +0 -0
- {envelope-2.2.0 → envelope-2.3.0}/envelope/attachment.py +0 -0
- {envelope-2.2.0 → envelope-2.3.0}/envelope/constants.py +0 -0
- {envelope-2.2.0 → envelope-2.3.0}/envelope/message.py +0 -0
- {envelope-2.2.0 → envelope-2.3.0}/envelope/parser.py +0 -0
- {envelope-2.2.0 → envelope-2.3.0}/envelope/utils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: envelope
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.3.0
|
|
4
4
|
Summary: Insert a message and attachments and send e-mail / sign / encrypt contents by a single line.
|
|
5
5
|
License: LGPL-3.0-or-later
|
|
6
6
|
Author: Edvard Rejthar
|
|
@@ -23,7 +23,7 @@ Description-Content-Type: text/markdown
|
|
|
23
23
|
|
|
24
24
|
[](https://github.com/CZ-NIC/envelope/actions) [](https://pepy.tech/project/envelope)
|
|
25
25
|
|
|
26
|
-
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.
|
|
26
|
+
Quick layer over [python-gnupg](https://bitbucket.org/vinay.sajip/python-gnupg/src), [cryptography](https://github.com/pyca/cryptography), [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.
|
|
27
27
|
You insert a message, attachments and inline images and receive signed and/or encrypted output to the file or to your recipients' e-mail.
|
|
28
28
|
Just single line of code. With the great help of the examples below.
|
|
29
29
|
|
|
@@ -692,6 +692,16 @@ if xarf := Envelope.load(path="xarf.eml")._report():
|
|
|
692
692
|
print(xarf['SourceIp']) # '192.0.2.1'
|
|
693
693
|
```
|
|
694
694
|
|
|
695
|
+
* `_check_auth()`: To determine whether DMACR, SPF and DKIM are alright in a loaded message.
|
|
696
|
+
```python3
|
|
697
|
+
from envelope import Envelope
|
|
698
|
+
auth = Envelope.load(path="test.eml")._check_auth()
|
|
699
|
+
auth # <AuthResult spf='pass', dkim='pass', dmarc='pass', spf_received='pass', verdict='pass', failure_reason=None>
|
|
700
|
+
if auth: # Use the output object as bool. It is True only if all present checks are 'pass'.
|
|
701
|
+
...
|
|
702
|
+
```
|
|
703
|
+
|
|
704
|
+
|
|
695
705
|
## Envelope object
|
|
696
706
|
|
|
697
707
|
### Converting object to str or bool
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://github.com/CZ-NIC/envelope/actions) [](https://pepy.tech/project/envelope)
|
|
4
4
|
|
|
5
|
-
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.
|
|
5
|
+
Quick layer over [python-gnupg](https://bitbucket.org/vinay.sajip/python-gnupg/src), [cryptography](https://github.com/pyca/cryptography), [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.
|
|
6
6
|
You insert a message, attachments and inline images and receive signed and/or encrypted output to the file or to your recipients' e-mail.
|
|
7
7
|
Just single line of code. With the great help of the examples below.
|
|
8
8
|
|
|
@@ -671,6 +671,16 @@ if xarf := Envelope.load(path="xarf.eml")._report():
|
|
|
671
671
|
print(xarf['SourceIp']) # '192.0.2.1'
|
|
672
672
|
```
|
|
673
673
|
|
|
674
|
+
* `_check_auth()`: To determine whether DMACR, SPF and DKIM are alright in a loaded message.
|
|
675
|
+
```python3
|
|
676
|
+
from envelope import Envelope
|
|
677
|
+
auth = Envelope.load(path="test.eml")._check_auth()
|
|
678
|
+
auth # <AuthResult spf='pass', dkim='pass', dmarc='pass', spf_received='pass', verdict='pass', failure_reason=None>
|
|
679
|
+
if auth: # Use the output object as bool. It is True only if all present checks are 'pass'.
|
|
680
|
+
...
|
|
681
|
+
```
|
|
682
|
+
|
|
683
|
+
|
|
674
684
|
## Envelope object
|
|
675
685
|
|
|
676
686
|
### Converting object to str or bool
|
|
@@ -171,7 +171,11 @@ def main():
|
|
|
171
171
|
or args["subject"] is True \
|
|
172
172
|
or args["message"] is True \
|
|
173
173
|
or select.select([sys.stdin, ], [], [], 0.0)[0]:
|
|
174
|
-
|
|
174
|
+
stdin = sys.stdin.read()
|
|
175
|
+
if "\r" in stdin or "\n" in stdin:
|
|
176
|
+
instance = Envelope.load(stdin)
|
|
177
|
+
else:
|
|
178
|
+
instance = Envelope(message=stdin)
|
|
175
179
|
except select.error: # XX check if using `select` to detect STDIN does not mess up Windows
|
|
176
180
|
pass
|
|
177
181
|
del args["load"]
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from email.message import EmailMessage
|
|
3
|
+
import re
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass
|
|
8
|
+
class AuthResult:
|
|
9
|
+
verdict: str
|
|
10
|
+
spf: Optional[str] = None
|
|
11
|
+
dkim: Optional[str] = None
|
|
12
|
+
dmarc: Optional[str] = None
|
|
13
|
+
spf_received: Optional[str] = None
|
|
14
|
+
failure_reason: Optional[str] = None
|
|
15
|
+
|
|
16
|
+
def __bool__(self):
|
|
17
|
+
return self.verdict == "pass"
|
|
18
|
+
|
|
19
|
+
def __repr__(self):
|
|
20
|
+
return (f"<AuthResult spf={self.spf!r}, dkim={self.dkim!r}, dmarc={self.dmarc!r}, "
|
|
21
|
+
f"spf_received={self.spf_received!r}, verdict={self.verdict!r}, failure_reason={self.failure_reason!r}>")
|
|
22
|
+
|
|
23
|
+
@staticmethod
|
|
24
|
+
def _extract_results_from_text(text: str) -> dict:
|
|
25
|
+
def extract(key: str):
|
|
26
|
+
match = re.search(fr"{key}=([\w\-]+)", text, re.IGNORECASE)
|
|
27
|
+
return match.group(1).lower() if match else None
|
|
28
|
+
|
|
29
|
+
return extract("spf"), extract("dkim"), extract("dmarc")
|
|
30
|
+
|
|
31
|
+
@staticmethod
|
|
32
|
+
def from_headers(headers: EmailMessage) -> "AuthResult":
|
|
33
|
+
|
|
34
|
+
# Merge Authentication-Results and ARC-Authentication-Results hlavičky dohromady
|
|
35
|
+
auth_results_headers: list[str] = []
|
|
36
|
+
auth_results_headers += headers.get_all("Authentication-Results", [])
|
|
37
|
+
auth_results_headers += headers.get_all("ARC-Authentication-Results", [])
|
|
38
|
+
|
|
39
|
+
combined_auth_results = "; ".join(auth_results_headers)
|
|
40
|
+
|
|
41
|
+
# Extract from text
|
|
42
|
+
spf, dkim, dmarc = AuthResult._extract_results_from_text(combined_auth_results)
|
|
43
|
+
|
|
44
|
+
# SPF from Received-SPF
|
|
45
|
+
received_spf = headers.get("Received-SPF", "")
|
|
46
|
+
spf_received_match = re.search(r"([\w\-]+)", received_spf)
|
|
47
|
+
spf_received = spf_received_match.group(1).lower() if spf_received_match else None
|
|
48
|
+
|
|
49
|
+
# Determine the verdict
|
|
50
|
+
failure_reason = None
|
|
51
|
+
if spf and spf_received and spf != spf_received:
|
|
52
|
+
verdict = "fail"
|
|
53
|
+
failure_reason = f"SPF mismatch: Authentication-Results={spf}, Received-SPF={spf_received}"
|
|
54
|
+
|
|
55
|
+
elif dmarc == "pass":
|
|
56
|
+
verdict = "pass"
|
|
57
|
+
|
|
58
|
+
elif any(v == "fail" for v in [spf, dkim, dmarc, spf_received]):
|
|
59
|
+
verdict = "fail"
|
|
60
|
+
failure_reason = "At least one mechanism failed"
|
|
61
|
+
|
|
62
|
+
elif any(v == "softfail" for v in [spf, spf_received]):
|
|
63
|
+
verdict = "softfail"
|
|
64
|
+
failure_reason = "SPF softfail detected"
|
|
65
|
+
|
|
66
|
+
elif all(v == "pass" for v in [spf, dkim] if v and v != 'none'):
|
|
67
|
+
verdict = "pass"
|
|
68
|
+
else:
|
|
69
|
+
verdict = "none"
|
|
70
|
+
failure_reason = "No usable authentication results"
|
|
71
|
+
|
|
72
|
+
return AuthResult(
|
|
73
|
+
spf=spf,
|
|
74
|
+
dkim=dkim,
|
|
75
|
+
dmarc=dmarc,
|
|
76
|
+
spf_received=spf_received,
|
|
77
|
+
verdict=verdict,
|
|
78
|
+
failure_reason=failure_reason
|
|
79
|
+
)
|
|
@@ -24,6 +24,7 @@ from quopri import decodestring
|
|
|
24
24
|
from types import GeneratorType
|
|
25
25
|
from typing import Literal, Union, Optional, Any
|
|
26
26
|
|
|
27
|
+
from ._auth import AuthResult
|
|
27
28
|
from .address import Address, _getaddresses
|
|
28
29
|
from .attachment import Attachment
|
|
29
30
|
from .constants import ISSUE_LINK, smime_import_error, gnupg, CRLF, AUTO, PLAIN, HTML, SIMULATION, SAFE_LOCALE
|
|
@@ -205,13 +206,25 @@ class Envelope:
|
|
|
205
206
|
:param key: S/MIME key to decrypt with.
|
|
206
207
|
:param cert: S/MIME cert to decrypt with. (If not bundled with the key.)
|
|
207
208
|
:param gnupg_home: Path to the GNUPG_HOME or None if the environment default should be used.
|
|
209
|
+
|
|
210
|
+
:raise ValueError: If the message cannot be loaded.
|
|
208
211
|
"""
|
|
209
212
|
if path:
|
|
210
213
|
message = Path(path)
|
|
211
214
|
elif isinstance(message, Message):
|
|
212
215
|
message = str(message)
|
|
213
216
|
|
|
214
|
-
|
|
217
|
+
msg_ = assure_fetched(message, bytes)
|
|
218
|
+
o = message_from_bytes(msg_)
|
|
219
|
+
|
|
220
|
+
# NOTE this breaks up .load("hello text")
|
|
221
|
+
# quick check
|
|
222
|
+
for _ in o.keys():
|
|
223
|
+
break
|
|
224
|
+
else: # there are no headers
|
|
225
|
+
if not path and b"\n" not in msg_ and b"\r" not in msg_:
|
|
226
|
+
raise ValueError(f"Message cannot be loaded. Didn't you forgot to use the `Envelope.load(path=...)`?")
|
|
227
|
+
|
|
215
228
|
e = Envelope()
|
|
216
229
|
try:
|
|
217
230
|
return Parser(e, key=key, cert=cert, gnupg_home=gnupg_home or e._get_gnupg_home()) \
|
|
@@ -406,7 +419,8 @@ class Envelope:
|
|
|
406
419
|
split_addresses = []
|
|
407
420
|
for address in addresses:
|
|
408
421
|
split_addresses.extend(address.replace(';', ',').split(','))
|
|
409
|
-
|
|
422
|
+
# remove empty and whitespace-only strings
|
|
423
|
+
split_addresses = [x.strip() for x in split_addresses if x.strip()]
|
|
410
424
|
registry += (a for a in Address.parse(split_addresses) if a not in registry)
|
|
411
425
|
|
|
412
426
|
def to(self, email_or_more=None) -> Union["Envelope", list[Address]]:
|
|
@@ -607,7 +621,8 @@ class Envelope:
|
|
|
607
621
|
:param one_click: If True, rfc8058 List-Unsubscribe-Post header is added.
|
|
608
622
|
This says user can unsubscribe with a single click that is realized by a POST request
|
|
609
623
|
in order to prevent e-mail scanner to access the unsubscribe page by mistake. A 'https' url must be present.
|
|
610
|
-
:param web: URL. Ex: "
|
|
624
|
+
:param web: URL. Ex: "https://example.com/unsubscribe"
|
|
625
|
+
Without protocol, `https://` is prepended, "example.com/unsubscribe" -> "https://example.com/unsubscribe"
|
|
611
626
|
:param email: E-mail address. Ex: "me@example.com", "mailto:me@example.com"
|
|
612
627
|
:return: self
|
|
613
628
|
"""
|
|
@@ -1260,29 +1275,32 @@ class Envelope:
|
|
|
1260
1275
|
def _import_cryptoraphy_modules(self):
|
|
1261
1276
|
try:
|
|
1262
1277
|
from cryptography.hazmat.primitives.serialization import load_pem_private_key, pkcs7
|
|
1263
|
-
from cryptography.x509 import load_pem_x509_certificate
|
|
1278
|
+
from cryptography.x509 import load_pem_x509_certificates, load_pem_x509_certificate
|
|
1264
1279
|
from cryptography.hazmat.primitives import hashes, serialization
|
|
1265
|
-
return load_pem_private_key, pkcs7, load_pem_x509_certificate, hashes, serialization
|
|
1280
|
+
return load_pem_private_key, pkcs7, load_pem_x509_certificates, load_pem_x509_certificate, hashes, serialization
|
|
1266
1281
|
except ImportError:
|
|
1267
1282
|
raise ImportError(smime_import_error)
|
|
1268
|
-
|
|
1283
|
+
|
|
1269
1284
|
def smime_sign_only(self, email, sign):
|
|
1270
|
-
load_pem_private_key, pkcs7,
|
|
1285
|
+
load_pem_private_key, pkcs7, load_pem_x509_certificates, _, hashes, serialization = self._import_cryptoraphy_modules()
|
|
1271
1286
|
# get sender's cert
|
|
1272
1287
|
# cert and private key can be one file
|
|
1273
1288
|
|
|
1274
1289
|
# if certfile is not provided, sign has to have both private key and certificate
|
|
1275
1290
|
# cert is not in sign (--sign_path file), it is cert.pem
|
|
1276
1291
|
if self._cert:
|
|
1277
|
-
|
|
1292
|
+
certs = self._cert
|
|
1278
1293
|
else:
|
|
1279
1294
|
# certificate is in sign (--sign_path file), it is key-cert-together.pem
|
|
1280
|
-
|
|
1295
|
+
certs = sign
|
|
1281
1296
|
|
|
1282
1297
|
try:
|
|
1283
|
-
|
|
1298
|
+
certs = load_pem_x509_certificates(certs)
|
|
1284
1299
|
except ValueError as e:
|
|
1285
1300
|
raise ValueError(f"Certificate not found: {e}")
|
|
1301
|
+
# no certificate found
|
|
1302
|
+
if len(certs) == 0:
|
|
1303
|
+
raise ValueError("Certificate not found")
|
|
1286
1304
|
|
|
1287
1305
|
# get senders private key for signing
|
|
1288
1306
|
try:
|
|
@@ -1297,29 +1315,36 @@ class Envelope:
|
|
|
1297
1315
|
|
|
1298
1316
|
options = [pkcs7.PKCS7Options.DetachedSignature]
|
|
1299
1317
|
|
|
1300
|
-
|
|
1318
|
+
signature_builder = pkcs7.PKCS7SignatureBuilder().set_data(
|
|
1301
1319
|
email
|
|
1302
1320
|
).add_signer(
|
|
1303
|
-
|
|
1304
|
-
)
|
|
1321
|
+
certs[0], key, hashes.SHA256()
|
|
1322
|
+
)
|
|
1323
|
+
# add the whole certificate chain
|
|
1324
|
+
for cert in certs[1:]:
|
|
1325
|
+
signature_builder = signature_builder.add_certificate(cert)
|
|
1326
|
+
|
|
1327
|
+
signed_email = signature_builder.sign(
|
|
1305
1328
|
serialization.Encoding.SMIME, options
|
|
1306
1329
|
)
|
|
1307
1330
|
|
|
1308
1331
|
return signed_email
|
|
1309
1332
|
|
|
1310
1333
|
def smime_sign_encrypt(self, email, sign, encrypt):
|
|
1311
|
-
load_pem_private_key, pkcs7, load_pem_x509_certificate, hashes, serialization = self._import_cryptoraphy_modules()
|
|
1334
|
+
load_pem_private_key, pkcs7, load_pem_x509_certificates, load_pem_x509_certificate, hashes, serialization = self._import_cryptoraphy_modules()
|
|
1312
1335
|
|
|
1313
1336
|
if self._cert:
|
|
1314
|
-
|
|
1337
|
+
sender_certs = self._cert
|
|
1315
1338
|
else:
|
|
1316
1339
|
# certificate is in sign (--sign_path file), it is key-cert-together.pem
|
|
1317
|
-
|
|
1340
|
+
sender_certs = sign
|
|
1318
1341
|
|
|
1319
1342
|
try:
|
|
1320
|
-
|
|
1343
|
+
sender_certs = load_pem_x509_certificates(sender_certs)
|
|
1321
1344
|
except ValueError as e:
|
|
1322
1345
|
logger.warning(f"Certificate not found: {e}")
|
|
1346
|
+
if len(sender_certs) == 0:
|
|
1347
|
+
raise ValueError("Certificate not found")
|
|
1323
1348
|
|
|
1324
1349
|
# get senders private key for signing
|
|
1325
1350
|
try:
|
|
@@ -1333,7 +1358,10 @@ class Envelope:
|
|
|
1333
1358
|
|
|
1334
1359
|
# sign the message first
|
|
1335
1360
|
signature_builder = pkcs7.PKCS7SignatureBuilder().set_data(email)
|
|
1336
|
-
signature_builder = signature_builder.add_signer(
|
|
1361
|
+
signature_builder = signature_builder.add_signer(sender_certs[0], sender_key, hash_algorithm=hashes.SHA256())
|
|
1362
|
+
# add the whole certificate chain
|
|
1363
|
+
for cert in sender_certs[1:]:
|
|
1364
|
+
signature_builder = signature_builder.add_certificate(cert)
|
|
1337
1365
|
signed_email = signature_builder.sign(serialization.Encoding.SMIME, [pkcs7.PKCS7Options.DetachedSignature])
|
|
1338
1366
|
|
|
1339
1367
|
# get recipent's certificate for encryption
|
|
@@ -1370,7 +1398,7 @@ class Envelope:
|
|
|
1370
1398
|
|
|
1371
1399
|
def smime_encrypt_only(self, email, encrypt):
|
|
1372
1400
|
|
|
1373
|
-
_, pkcs7, load_pem_x509_certificate, _, serialization = self._import_cryptoraphy_modules()
|
|
1401
|
+
_, pkcs7, _, load_pem_x509_certificate, _, serialization = self._import_cryptoraphy_modules()
|
|
1374
1402
|
|
|
1375
1403
|
if self._cert:
|
|
1376
1404
|
certificates = [self._cert]
|
|
@@ -1716,6 +1744,11 @@ class Envelope:
|
|
|
1716
1744
|
passed *= bool(self._smtp.connect()) # check SMTP
|
|
1717
1745
|
return passed
|
|
1718
1746
|
|
|
1747
|
+
def _check_auth(self):
|
|
1748
|
+
""" Experimental. """
|
|
1749
|
+
# NOTE Do not know how to map in API. Because .check() seems to be for sending and this one for receiving.
|
|
1750
|
+
return AuthResult.from_headers(self._headers)
|
|
1751
|
+
|
|
1719
1752
|
def smtp_quit(self=None):
|
|
1720
1753
|
""" Explicitly closes cached SMTP connections. Either class or instance can be called.
|
|
1721
1754
|
Envelope.smtp_quit() → closing all
|
|
@@ -33,6 +33,8 @@ class SMTPHandler:
|
|
|
33
33
|
d = locals()
|
|
34
34
|
del d["self"]
|
|
35
35
|
self.key = repr(d)
|
|
36
|
+
d["password"] = "********"
|
|
37
|
+
self.obfuscated_key = repr(d)
|
|
36
38
|
|
|
37
39
|
def connect(self):
|
|
38
40
|
if self.instance: # we received this instance as is so we suppose it is already connected
|
|
@@ -55,13 +57,13 @@ class SMTPHandler:
|
|
|
55
57
|
smtp.login(self.user, self.password)
|
|
56
58
|
except SMTPAuthenticationError as e:
|
|
57
59
|
logger.error(
|
|
58
|
-
f"SMTP authentication failed: {self.
|
|
60
|
+
f"SMTP authentication failed: {self.obfuscated_key}.\n{e}")
|
|
59
61
|
return False
|
|
60
62
|
except SMTPException as e:
|
|
61
|
-
logger.error(f"SMTP connection failed: {self.
|
|
63
|
+
logger.error(f"SMTP connection failed: {self.obfuscated_key}.\n{e}")
|
|
62
64
|
return False
|
|
63
65
|
except (gaierror, ConnectionError):
|
|
64
|
-
logger.error(f"SMTP connection refused: {self.
|
|
66
|
+
logger.error(f"SMTP connection refused: {self.obfuscated_key}.")
|
|
65
67
|
return False
|
|
66
68
|
return smtp
|
|
67
69
|
|
|
@@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api"
|
|
|
4
4
|
|
|
5
5
|
[tool.poetry]
|
|
6
6
|
name = "envelope"
|
|
7
|
-
version = "2.
|
|
7
|
+
version = "2.3.0"
|
|
8
8
|
description = "Insert a message and attachments and send e-mail / sign / encrypt contents by a single line."
|
|
9
9
|
authors = ["Edvard Rejthar <edvard.rejthar@nic.cz>"]
|
|
10
10
|
license = "LGPL-3.0-or-later"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|