envelope 2.3.0__tar.gz → 2.4.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.4.0/PKG-INFO +69 -0
- envelope-2.4.0/README.md +44 -0
- {envelope-2.3.0 → envelope-2.4.0}/envelope/__main__.py +19 -4
- {envelope-2.3.0 → envelope-2.4.0}/envelope/constants.py +2 -1
- {envelope-2.3.0 → envelope-2.4.0}/envelope/envelope.py +91 -15
- {envelope-2.3.0 → envelope-2.4.0}/envelope/parser.py +11 -6
- {envelope-2.3.0 → envelope-2.4.0}/envelope/smtp_handler.py +2 -1
- {envelope-2.3.0 → envelope-2.4.0}/envelope/utils.py +13 -3
- {envelope-2.3.0 → envelope-2.4.0}/pyproject.toml +4 -4
- envelope-2.3.0/PKG-INFO +0 -967
- envelope-2.3.0/README.md +0 -945
- {envelope-2.3.0 → envelope-2.4.0}/LICENSE +0 -0
- {envelope-2.3.0 → envelope-2.4.0}/envelope/__init__.py +0 -0
- {envelope-2.3.0 → envelope-2.4.0}/envelope/_auth.py +0 -0
- {envelope-2.3.0 → envelope-2.4.0}/envelope/address.py +0 -0
- {envelope-2.3.0 → envelope-2.4.0}/envelope/attachment.py +0 -0
- {envelope-2.3.0 → envelope-2.4.0}/envelope/message.py +0 -0
envelope-2.4.0/PKG-INFO
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: envelope
|
|
3
|
+
Version: 2.4.0
|
|
4
|
+
Summary: Insert a message and attachments and send e-mail / sign / encrypt contents by a single line.
|
|
5
|
+
License: LGPL-3.0-or-later
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Author: Edvard Rejthar
|
|
8
|
+
Author-email: edvard.rejthar@nic.cz
|
|
9
|
+
Requires-Python: >=3.10
|
|
10
|
+
Classifier: License :: OSI Approved :: GNU Lesser General Public License v3 or later (LGPLv3+)
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
17
|
+
Provides-Extra: smime
|
|
18
|
+
Requires-Dist: cryptography (>=43) ; extra == "smime"
|
|
19
|
+
Requires-Dist: py3-validate-email
|
|
20
|
+
Requires-Dist: python-gnupg (>=0.5)
|
|
21
|
+
Requires-Dist: python-magic
|
|
22
|
+
Project-URL: Homepage, https://github.com/CZ-NIC/envelope
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
|
|
25
|
+
# Envelope
|
|
26
|
+
|
|
27
|
+
[](https://github.com/CZ-NIC/envelope/actions) [](https://pepy.tech/project/envelope)
|
|
28
|
+
|
|
29
|
+
A thin, fluent 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) — their common use cases merged into a single object. Tired of forgetting how to sign a message correctly? You do not need to know everything about GPG or S/MIME, you do not have to bother with importing keys, you do not have to hassle with reconnecting to an SMTP server, and you do not have to study what every header means just to let your users unsubscribe via a URL.
|
|
30
|
+
You insert a message, attachments and inline images and get back signed and/or encrypted output — written to a file or sent straight to your recipients' e-mail.
|
|
31
|
+
Just a single line of code, as the examples below show.
|
|
32
|
+
|
|
33
|
+
```python3
|
|
34
|
+
Envelope("my message")
|
|
35
|
+
.subject("hello world")
|
|
36
|
+
.to("example@example.com")
|
|
37
|
+
.attach(file_contents, name="attached-file.txt")
|
|
38
|
+
.smtp("localhost", 587, "user", "pass", "starttls")
|
|
39
|
+
.signature()
|
|
40
|
+
.send()
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
```python3
|
|
44
|
+
# Inline image
|
|
45
|
+
Envelope("My inline image: <img src='cid:image.jpg' />")
|
|
46
|
+
.attach(path="image.jpg", inline=True)
|
|
47
|
+
|
|
48
|
+
# Load a message and read its attachments
|
|
49
|
+
Envelope.load(path="message.eml").attachments()
|
|
50
|
+
# in bash: envelope --load message.eml --attachments
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Documentation
|
|
54
|
+
|
|
55
|
+
- [Installation](https://cz-nic.github.io/envelope/installation/) — PyPI/GitHub install, GPG/S-MIME prerequisites, bash completion.
|
|
56
|
+
- [Usage](https://cz-nic.github.io/envelope/usage/) — the CLI, the fluent interface and the one-liner module call.
|
|
57
|
+
- Reference — every setter, in all three interfaces:
|
|
58
|
+
* [Overview](https://cz-nic.github.io/envelope/reference/overview/) — notation used below, "any attainable contents".
|
|
59
|
+
* [Input / Output](https://cz-nic.github.io/envelope/reference/input-output/) — message, output.
|
|
60
|
+
* [Recipients](https://cz-nic.github.io/envelope/reference/recipients/) — from, to, cc, bcc, reply-to, from_addr.
|
|
61
|
+
* [Sending](https://cz-nic.github.io/envelope/reference/sending/) — send, subject, date, smtp, attachments, mime, headers.
|
|
62
|
+
* [Ciphering](https://cz-nic.github.io/envelope/reference/ciphering/) — choosing GPG/S-MIME, signing, encrypting.
|
|
63
|
+
* [Supportive](https://cz-nic.github.io/envelope/reference/supportive/) — recipients(), copy(), preview, check, load, Address.
|
|
64
|
+
* [Experimental](https://cz-nic.github.io/envelope/reference/experimental/) — `_report()`, `_check_auth()`.
|
|
65
|
+
* [Envelope object](https://cz-nic.github.io/envelope/reference/envelope-object/) — converting to str/bool, object equality.
|
|
66
|
+
- [Examples](https://cz-nic.github.io/envelope/examples/) — signing/encrypting, sending, attachments, inline images, a complex example.
|
|
67
|
+
- [Related affairs](https://cz-nic.github.io/envelope/related-affairs/) — configuring an SMTP server, GPG, S/MIME, and SPF/DKIM/DMARC.
|
|
68
|
+
- [Changelog](https://cz-nic.github.io/envelope/changelog/)
|
|
69
|
+
|
envelope-2.4.0/README.md
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# Envelope
|
|
2
|
+
|
|
3
|
+
[](https://github.com/CZ-NIC/envelope/actions) [](https://pepy.tech/project/envelope)
|
|
4
|
+
|
|
5
|
+
A thin, fluent 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) — their common use cases merged into a single object. Tired of forgetting how to sign a message correctly? You do not need to know everything about GPG or S/MIME, you do not have to bother with importing keys, you do not have to hassle with reconnecting to an SMTP server, and you do not have to study what every header means just to let your users unsubscribe via a URL.
|
|
6
|
+
You insert a message, attachments and inline images and get back signed and/or encrypted output — written to a file or sent straight to your recipients' e-mail.
|
|
7
|
+
Just a single line of code, as the examples below show.
|
|
8
|
+
|
|
9
|
+
```python3
|
|
10
|
+
Envelope("my message")
|
|
11
|
+
.subject("hello world")
|
|
12
|
+
.to("example@example.com")
|
|
13
|
+
.attach(file_contents, name="attached-file.txt")
|
|
14
|
+
.smtp("localhost", 587, "user", "pass", "starttls")
|
|
15
|
+
.signature()
|
|
16
|
+
.send()
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
```python3
|
|
20
|
+
# Inline image
|
|
21
|
+
Envelope("My inline image: <img src='cid:image.jpg' />")
|
|
22
|
+
.attach(path="image.jpg", inline=True)
|
|
23
|
+
|
|
24
|
+
# Load a message and read its attachments
|
|
25
|
+
Envelope.load(path="message.eml").attachments()
|
|
26
|
+
# in bash: envelope --load message.eml --attachments
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Documentation
|
|
30
|
+
|
|
31
|
+
- [Installation](https://cz-nic.github.io/envelope/installation/) — PyPI/GitHub install, GPG/S-MIME prerequisites, bash completion.
|
|
32
|
+
- [Usage](https://cz-nic.github.io/envelope/usage/) — the CLI, the fluent interface and the one-liner module call.
|
|
33
|
+
- Reference — every setter, in all three interfaces:
|
|
34
|
+
* [Overview](https://cz-nic.github.io/envelope/reference/overview/) — notation used below, "any attainable contents".
|
|
35
|
+
* [Input / Output](https://cz-nic.github.io/envelope/reference/input-output/) — message, output.
|
|
36
|
+
* [Recipients](https://cz-nic.github.io/envelope/reference/recipients/) — from, to, cc, bcc, reply-to, from_addr.
|
|
37
|
+
* [Sending](https://cz-nic.github.io/envelope/reference/sending/) — send, subject, date, smtp, attachments, mime, headers.
|
|
38
|
+
* [Ciphering](https://cz-nic.github.io/envelope/reference/ciphering/) — choosing GPG/S-MIME, signing, encrypting.
|
|
39
|
+
* [Supportive](https://cz-nic.github.io/envelope/reference/supportive/) — recipients(), copy(), preview, check, load, Address.
|
|
40
|
+
* [Experimental](https://cz-nic.github.io/envelope/reference/experimental/) — `_report()`, `_check_auth()`.
|
|
41
|
+
* [Envelope object](https://cz-nic.github.io/envelope/reference/envelope-object/) — converting to str/bool, object equality.
|
|
42
|
+
- [Examples](https://cz-nic.github.io/envelope/examples/) — signing/encrypting, sending, attachments, inline images, a complex example.
|
|
43
|
+
- [Related affairs](https://cz-nic.github.io/envelope/related-affairs/) — configuring an SMTP server, GPG, S/MIME, and SPF/DKIM/DMARC.
|
|
44
|
+
- [Changelog](https://cz-nic.github.io/envelope/changelog/)
|
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
import argparse
|
|
2
|
+
import json
|
|
2
3
|
import select
|
|
3
4
|
import sys
|
|
4
5
|
from pathlib import Path
|
|
5
6
|
|
|
6
|
-
from jsonpickle import decode
|
|
7
|
-
|
|
8
7
|
from .envelope import Envelope, __doc__ as doc
|
|
9
8
|
from .attachment import Attachment
|
|
10
|
-
|
|
9
|
+
from .constants import SENDMAIL_PATH
|
|
11
10
|
|
|
12
11
|
class SmartFormatter(argparse.RawDescriptionHelpFormatter):
|
|
13
12
|
|
|
@@ -141,6 +140,9 @@ def main():
|
|
|
141
140
|
" Security may be explicitly set to 'starttls', 'tls'"
|
|
142
141
|
" or automatically determined by port.",
|
|
143
142
|
nargs="*", action=BlankTrue, metavar=("HOST", "PORT"))
|
|
143
|
+
group_send.add_argument('--sendmail', help="Path to sendmail binary. Disables SMTP."
|
|
144
|
+
f" Leave blank for default {SENDMAIL_PATH}.", nargs="?", action=BlankTrue,
|
|
145
|
+
metavar="PATH")
|
|
144
146
|
group_send.add_argument('--send', help="Send e-mail. Blank to send now.", nargs="?", action=BlankTrue)
|
|
145
147
|
|
|
146
148
|
group_supp = parser.add_argument_group("Supportive")
|
|
@@ -230,7 +232,20 @@ def main():
|
|
|
230
232
|
|
|
231
233
|
# smtp can be a dict
|
|
232
234
|
if args["smtp"] and args["smtp"][0].startswith("{"):
|
|
233
|
-
|
|
235
|
+
smtp_allowed_keys = {"host", "port", "user", "password", "security", "timeout",
|
|
236
|
+
"attempts", "delay", "local_hostname"}
|
|
237
|
+
try:
|
|
238
|
+
smtp_dict = json.loads(" ".join(args["smtp"]))
|
|
239
|
+
except json.JSONDecodeError as e:
|
|
240
|
+
raise RuntimeError(f"Invalid JSON in --smtp: {e}")
|
|
241
|
+
if not isinstance(smtp_dict, dict):
|
|
242
|
+
raise RuntimeError("--smtp JSON must be an object/dict.")
|
|
243
|
+
unknown_keys = smtp_dict.keys() - smtp_allowed_keys
|
|
244
|
+
if unknown_keys:
|
|
245
|
+
raise RuntimeError(f"Unknown --smtp key(s): {', '.join(unknown_keys)}")
|
|
246
|
+
args["smtp"] = smtp_dict
|
|
247
|
+
elif args["smtp"] and args["smtp"].lower() in ["0", "false", "no"]:
|
|
248
|
+
args["smtp"] = False
|
|
234
249
|
|
|
235
250
|
# send = False turns on debugging
|
|
236
251
|
if args["send"] and type(args["send"]) is not bool:
|
|
@@ -19,4 +19,5 @@ SIMULATION = "simulation"
|
|
|
19
19
|
# We cannot set as constant whole os.environ dict as it might be changed in the tests.py (settings GNUPG_HOME).
|
|
20
20
|
SAFE_LOCALE = "C" if (sys.version_info.major, sys.version_info.minor) > (3, 6) else "C.UTF-8"
|
|
21
21
|
|
|
22
|
-
ISSUE_LINK = "https://github.com/CZ-NIC/envelope/issues/new"
|
|
22
|
+
ISSUE_LINK = "https://github.com/CZ-NIC/envelope/issues/new"
|
|
23
|
+
SENDMAIL_PATH = "/usr/sbin/sendmail"
|
|
@@ -27,11 +27,12 @@ from typing import Literal, Union, Optional, Any
|
|
|
27
27
|
from ._auth import AuthResult
|
|
28
28
|
from .address import Address, _getaddresses
|
|
29
29
|
from .attachment import Attachment
|
|
30
|
-
from .constants import ISSUE_LINK, smime_import_error, gnupg, CRLF, AUTO, PLAIN, HTML, SIMULATION, SAFE_LOCALE
|
|
30
|
+
from .constants import ISSUE_LINK, smime_import_error, gnupg, CRLF, AUTO, PLAIN, HTML, SIMULATION, SAFE_LOCALE, SENDMAIL_PATH
|
|
31
31
|
from .message import _Message
|
|
32
32
|
from .parser import Parser
|
|
33
33
|
from .smtp_handler import SMTPHandler
|
|
34
|
-
from .utils import AutoSubmittedHeader, Fetched, is_gpg_importable_key, assure_list, assure_fetched, get_mimetype
|
|
34
|
+
from .utils import AutoSubmittedHeader, Fetched, is_gpg_importable_key, assure_list, assure_fetched, get_mimetype, \
|
|
35
|
+
is_safe_argv_value
|
|
35
36
|
|
|
36
37
|
|
|
37
38
|
__doc__ = """Quick layer over python-gnupg, M2Crypto, smtplib, magic and email handling packages.
|
|
@@ -268,7 +269,7 @@ class Envelope:
|
|
|
268
269
|
gpg=None, smime=None,
|
|
269
270
|
encrypt=None, sign=None, passphrase=None, attach_key=None, cert=None, subject_encrypted=None,
|
|
270
271
|
sender=None, cc=None, bcc=None, reply_to=None, mime=None, attachments=None,
|
|
271
|
-
smtp=None, output=None, send=None):
|
|
272
|
+
smtp=None, sendmail=None, output=None, send=None):
|
|
272
273
|
"""
|
|
273
274
|
:rtype: object If output not set, return output bytes, else True/False if output file was correctly written to.
|
|
274
275
|
|
|
@@ -302,6 +303,10 @@ class Envelope:
|
|
|
302
303
|
:param mime: Set contents mime subtype: "html" (default) or "plain" for plain text
|
|
303
304
|
:param smtp: tuple or dict of these optional parameters: host, port, username, password, security ("tlsstart").
|
|
304
305
|
Or link to existing INI file with the SMTP section.
|
|
306
|
+
Or False to not use SMTP.
|
|
307
|
+
:param sendmail: True or path to ubiquitious sendmail binary.
|
|
308
|
+
Otherwise the standard "/usr/sbin/sendmail" is used.
|
|
309
|
+
Sendmail is only used if this method OR smtp(False) is called
|
|
305
310
|
:param send: True for sending the mail. False will just print the output.
|
|
306
311
|
:param cc: E-mail or more in an iterable.
|
|
307
312
|
:param bcc: E-mail or more in an iterable.
|
|
@@ -336,7 +341,6 @@ class Envelope:
|
|
|
336
341
|
self._reply_to: list[Address] = []
|
|
337
342
|
self._subject: Union[str, None] = None
|
|
338
343
|
self._subject_encrypted: Union[str, bool] = True
|
|
339
|
-
self._smtp = None
|
|
340
344
|
self._attachments: list[Attachment] = []
|
|
341
345
|
self._mime = AUTO
|
|
342
346
|
self._nl2br = AUTO
|
|
@@ -349,7 +353,8 @@ class Envelope:
|
|
|
349
353
|
self._result: list[Union[str, EmailMessage, Message]] = [] # text output for str() conversion
|
|
350
354
|
self._result_cache: Optional[str] = None
|
|
351
355
|
self._result_cache_hash: Optional[int] = None
|
|
352
|
-
self._smtp = SMTPHandler()
|
|
356
|
+
self._smtp: Union[SMTPHandler, bool] = SMTPHandler()
|
|
357
|
+
self._sendmail: Union[str, bool, None, Path] = False
|
|
353
358
|
self.auto_submitted = AutoSubmittedHeader(self) # allows fluent interface to set header
|
|
354
359
|
|
|
355
360
|
self._multipart_report_message: Optional[Message] = None
|
|
@@ -729,7 +734,7 @@ class Envelope:
|
|
|
729
734
|
Obtain SMTP server connection.
|
|
730
735
|
Note that you may safely call this in a loop,
|
|
731
736
|
envelope will remember the settings and connect only once (without reconnecting every iteration).
|
|
732
|
-
:param host: hostname, smtplib.SMTP, INI file path, or a list or dict with the parameters (see README.md)
|
|
737
|
+
:param host: hostname, smtplib.SMTP, INI file path, False, or a list or dict with the parameters (see README.md)
|
|
733
738
|
:param port:
|
|
734
739
|
:param user:
|
|
735
740
|
:param password:
|
|
@@ -771,11 +776,24 @@ class Envelope:
|
|
|
771
776
|
self._smtp = SMTPHandler(*host)
|
|
772
777
|
elif isinstance(host, smtplib.SMTP):
|
|
773
778
|
self._smtp = SMTPHandler(host)
|
|
779
|
+
elif host == False:
|
|
780
|
+
self._smtp = False
|
|
781
|
+
self._sendmail = True
|
|
774
782
|
else:
|
|
775
783
|
self._smtp = SMTPHandler(host, port, user, password, security, timeout=timeout, attempts=attempts,
|
|
776
784
|
delay=delay, local_hostname=local_hostname)
|
|
777
785
|
return self
|
|
778
786
|
|
|
787
|
+
def sendmail(self, sendmail=True):
|
|
788
|
+
"""
|
|
789
|
+
:param sendmail: String for Sendmail binary or True.
|
|
790
|
+
Disables SMTP when enabled.
|
|
791
|
+
"""
|
|
792
|
+
self._sendmail = sendmail
|
|
793
|
+
if bool(self._sendmail):
|
|
794
|
+
self._smtp = False
|
|
795
|
+
return self
|
|
796
|
+
|
|
779
797
|
def attach(self, attachment=None, mimetype=None, name=None, inline=None, *, path=None):
|
|
780
798
|
"""
|
|
781
799
|
|
|
@@ -1144,15 +1162,8 @@ class Envelope:
|
|
|
1144
1162
|
|
|
1145
1163
|
if send and send != SIMULATION:
|
|
1146
1164
|
recipients = list(map(str, set(self._to + self._cc + self._bcc)))
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
failures = self._smtp.send_message(email,
|
|
1150
|
-
from_addr=self._from_addr,
|
|
1151
|
-
to_addrs=recipients)
|
|
1152
|
-
if failures:
|
|
1153
|
-
logger.warning(f"Unable to send to all recipients: {repr(failures)}.")
|
|
1154
|
-
elif failures is False:
|
|
1155
|
-
# TODO add here and test, logger.warning(f"Sending {recipients}, Message-ID: {email["Message-ID"]}")
|
|
1165
|
+
success = self._deliver_now(email, recipients)
|
|
1166
|
+
if not success:
|
|
1156
1167
|
return False
|
|
1157
1168
|
else:
|
|
1158
1169
|
if send != SIMULATION:
|
|
@@ -1167,6 +1178,68 @@ class Envelope:
|
|
|
1167
1178
|
|
|
1168
1179
|
return email
|
|
1169
1180
|
|
|
1181
|
+
def _deliver_now(self, email, recipients):
|
|
1182
|
+
# _smtp XOR _sendmail
|
|
1183
|
+
if (_smtp := bool(self._smtp)) == (_sendmail := bool(self._sendmail)):
|
|
1184
|
+
logger.error(f"Need either SMTP (is {('off', 'on')[_smtp]}) or local delivery/sendmail (is {('off', 'on')[_sendmail]}) to send")
|
|
1185
|
+
return False
|
|
1186
|
+
if self._smtp:
|
|
1187
|
+
success_or_failures = self._deliver_smtp(email, self._from_addr, recipients)
|
|
1188
|
+
elif self._sendmail:
|
|
1189
|
+
success_or_failures = self._deliver_sendmail(email, self._from_addr or self._from, recipients)
|
|
1190
|
+
else:
|
|
1191
|
+
assert False, "Should not reach, either SMTP or Sendmail should work"
|
|
1192
|
+
|
|
1193
|
+
if success_or_failures:
|
|
1194
|
+
logger.warning(f"Unable to send to all recipients: {repr(success_or_failures)}.")
|
|
1195
|
+
# elif success_or_failures is False:
|
|
1196
|
+
# # TODO add here and test, logger.warning(f"Sending {recipients}, Message-ID: {email["Message-ID"]}")
|
|
1197
|
+
# return False
|
|
1198
|
+
return success_or_failures
|
|
1199
|
+
|
|
1200
|
+
def _deliver_smtp(self, email, from_addr, recipients):
|
|
1201
|
+
"""
|
|
1202
|
+
Use configured SMTP server to deliver email.
|
|
1203
|
+
:param email: String contents of whole email to send
|
|
1204
|
+
:param from_addr: Email address of sender
|
|
1205
|
+
:param recipients: List of recipient email addresses
|
|
1206
|
+
:returns: List of recipients to whom delivery failed.
|
|
1207
|
+
Or False for an unspecified error condition
|
|
1208
|
+
:rtype: list or False
|
|
1209
|
+
"""
|
|
1210
|
+
if not isinstance(self._smtp, SMTPHandler):
|
|
1211
|
+
self._smtp = SMTPHandler()
|
|
1212
|
+
with mock.patch.object(Generator, '_handle_multipart_signed', Generator._handle_multipart):
|
|
1213
|
+
# https://github.com/python/cpython/issues/99533 and #19
|
|
1214
|
+
return self._smtp.send_message(email,
|
|
1215
|
+
from_addr=self._from_addr,
|
|
1216
|
+
to_addrs=recipients)
|
|
1217
|
+
|
|
1218
|
+
def _deliver_sendmail(self, email, from_addr, to_addrs):
|
|
1219
|
+
"""
|
|
1220
|
+
Use a local MTA with sendmail interface to send mail via queue"
|
|
1221
|
+
|
|
1222
|
+
:param email: String contents of whole email to send
|
|
1223
|
+
:param from_addr: Email address of sender
|
|
1224
|
+
:param recipients: (unused, for signature compatibility with _deliver_smtp,
|
|
1225
|
+
Originally: List of recipient email addresses)
|
|
1226
|
+
:returns: Empty list on success or False for an unspecified error condition
|
|
1227
|
+
(For return compatibilty with _deliver_smtp)
|
|
1228
|
+
:rtype: list or False
|
|
1229
|
+
"""
|
|
1230
|
+
self._sendmail = SENDMAIL_PATH if self._sendmail == True else str(self._sendmail)
|
|
1231
|
+
_unused = to_addrs
|
|
1232
|
+
from_addr = str(from_addr)
|
|
1233
|
+
if not is_safe_argv_value(from_addr):
|
|
1234
|
+
logger.error(f"Refusing to invoke sendmail with a suspicious From address: {from_addr!r}")
|
|
1235
|
+
return False
|
|
1236
|
+
args = [self._sendmail, "-t", "-oi", "-f", from_addr]
|
|
1237
|
+
try:
|
|
1238
|
+
subprocess.run(args, input=str(email), text=True, check=True)
|
|
1239
|
+
return []
|
|
1240
|
+
except subprocess.CalledProcessError:
|
|
1241
|
+
return False
|
|
1242
|
+
|
|
1170
1243
|
def _param_hash(self):
|
|
1171
1244
|
""" Check if headers changed from last _start call."""
|
|
1172
1245
|
return (hash(frozenset(self._headers.items())) + hash("".join(self.recipients()))
|
|
@@ -1697,6 +1770,9 @@ class Envelope:
|
|
|
1697
1770
|
if type(query_or_list) is not list:
|
|
1698
1771
|
query_or_list = [query_or_list]
|
|
1699
1772
|
for query in query_or_list:
|
|
1773
|
+
if not is_safe_argv_value(query):
|
|
1774
|
+
logger.warning(f"Refusing to run dig with a suspicious query: {query!r}")
|
|
1775
|
+
return []
|
|
1700
1776
|
try:
|
|
1701
1777
|
text = subprocess.check_output(["dig", "-t", rr, query],
|
|
1702
1778
|
env=dict(environ, LC_ALL=SAFE_LOCALE)).decode("utf-8")
|
|
@@ -11,6 +11,8 @@ if TYPE_CHECKING:
|
|
|
11
11
|
|
|
12
12
|
logger = logging.getLogger(__name__)
|
|
13
13
|
|
|
14
|
+
MAX_PARSE_DEPTH = 20
|
|
15
|
+
|
|
14
16
|
|
|
15
17
|
class Parser:
|
|
16
18
|
|
|
@@ -20,7 +22,9 @@ class Parser:
|
|
|
20
22
|
self.cert = cert
|
|
21
23
|
self.gnupg_home = gnupg_home
|
|
22
24
|
|
|
23
|
-
def parse(self, o: Message, add_headers=False):
|
|
25
|
+
def parse(self, o: Message, add_headers=False, depth=0):
|
|
26
|
+
if depth > MAX_PARSE_DEPTH:
|
|
27
|
+
raise ValueError(f"MIME structure nested too deeply (> {MAX_PARSE_DEPTH} levels)")
|
|
24
28
|
if add_headers:
|
|
25
29
|
for k, val in o.items():
|
|
26
30
|
# We skip "Content-Type" and "Content-Transfer-Encoding" since we decode text payload before importing.
|
|
@@ -45,12 +49,12 @@ class Parser:
|
|
|
45
49
|
if o.is_multipart():
|
|
46
50
|
payload: List[Message] = o.get_payload()
|
|
47
51
|
if subtype == "alternative":
|
|
48
|
-
[self.parse(x) for x in payload]
|
|
52
|
+
[self.parse(x, depth=depth + 1) for x in payload]
|
|
49
53
|
elif subtype in ("related", "mixed", "report"):
|
|
50
54
|
for p in payload:
|
|
51
55
|
if p.get_content_maintype() in ["text", "multipart"] \
|
|
52
56
|
and p.get_content_disposition() != "attachment":
|
|
53
|
-
self.parse(p)
|
|
57
|
+
self.parse(p, depth=depth + 1)
|
|
54
58
|
elif subtype == "report" and p.get_content_maintype() == "message":
|
|
55
59
|
if p.get_content_type() != FEEDBACK_REPORT: # only XARF implemented
|
|
56
60
|
raise ValueError(f"Parsing {maintype}/{subtype} / {p.get_content_type()} not implemented.")
|
|
@@ -69,13 +73,14 @@ class Parser:
|
|
|
69
73
|
if p.get_content_type() == o.get_param("protocol"): # ex: application/x-pkcs7-signature
|
|
70
74
|
continue # XX we might verify signature
|
|
71
75
|
else:
|
|
72
|
-
self.parse(p)
|
|
76
|
+
self.parse(p, depth=depth + 1)
|
|
73
77
|
elif subtype == "encrypted":
|
|
74
78
|
for p in payload:
|
|
75
79
|
if p.get_content_type() == o.get_param("protocol"): # ex: application/pgp-encrypted
|
|
76
80
|
continue
|
|
77
81
|
elif p.get_content_type() == "application/octet-stream":
|
|
78
|
-
self.parse(message_from_string(self.gpg_decrypt(p.get_payload(decode=True))),
|
|
82
|
+
self.parse(message_from_string(self.gpg_decrypt(p.get_payload(decode=True))),
|
|
83
|
+
add_headers=True, depth=depth + 1)
|
|
79
84
|
else:
|
|
80
85
|
raise ValueError(f"Cannot decrypt.")
|
|
81
86
|
else:
|
|
@@ -96,7 +101,7 @@ class Parser:
|
|
|
96
101
|
else:
|
|
97
102
|
raise ValueError(f"Unknown subtype: {subtype}")
|
|
98
103
|
elif maintype == "application" and subtype == "x-pkcs7-mime": # decrypting S/MIME
|
|
99
|
-
self.parse(message_from_bytes(self.smime_decrypt(o.as_bytes())), add_headers=True)
|
|
104
|
+
self.parse(message_from_bytes(self.smime_decrypt(o.as_bytes())), add_headers=True, depth=depth + 1)
|
|
100
105
|
else:
|
|
101
106
|
raise ValueError(f"Unknown maintype: {maintype}")
|
|
102
107
|
return self.e
|
|
@@ -80,7 +80,8 @@ class SMTPHandler:
|
|
|
80
80
|
return smtp.send_message(email, from_addr=from_addr, to_addrs=to_addrs)
|
|
81
81
|
except (timeout_exc, SMTPException) as e:
|
|
82
82
|
# this connection is gone, reconnect next time
|
|
83
|
-
|
|
83
|
+
if self.key in self._instances:
|
|
84
|
+
del self._instances[self.key]
|
|
84
85
|
if isinstance(e, SMTPAuthenticationError):
|
|
85
86
|
logger.warning(
|
|
86
87
|
f"SMTP authentication error, will not re-try. {e}")
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import logging
|
|
2
|
-
from io import
|
|
2
|
+
from io import IOBase
|
|
3
3
|
from pathlib import Path
|
|
4
4
|
from typing import TYPE_CHECKING, Iterable
|
|
5
5
|
|
|
@@ -65,6 +65,16 @@ def get_mimetype(data: bytes = None, path: Path = None):
|
|
|
65
65
|
return magic.detect_from_filename(str(path)).mime_type
|
|
66
66
|
|
|
67
67
|
|
|
68
|
+
def is_safe_argv_value(s: str) -> bool:
|
|
69
|
+
""" Check a string is safe to pass as a single subprocess argv value derived from
|
|
70
|
+
untrusted (e.g. parsed e-mail) content: not empty, does not start with "-" (which some
|
|
71
|
+
CLI parsers could otherwise mistake for a new option) and contains no whitespace or
|
|
72
|
+
control characters. """
|
|
73
|
+
if not s or s[0] == "-":
|
|
74
|
+
return False
|
|
75
|
+
return all(c.isprintable() and not c.isspace() for c in s)
|
|
76
|
+
|
|
77
|
+
|
|
68
78
|
def assure_list(v):
|
|
69
79
|
""" Accepts object and returns list.
|
|
70
80
|
If object is tuple, generator, set, frozenset, it's converted to a list.
|
|
@@ -76,7 +86,7 @@ def assure_list(v):
|
|
|
76
86
|
"""
|
|
77
87
|
if v is None:
|
|
78
88
|
return []
|
|
79
|
-
if isinstance(v, Iterable) and not isinstance(v, (
|
|
89
|
+
if isinstance(v, Iterable) and not isinstance(v, (IOBase, str, bytes)):
|
|
80
90
|
return list(v)
|
|
81
91
|
return [v]
|
|
82
92
|
|
|
@@ -96,7 +106,7 @@ def assure_fetched(message, retyped=None) -> Fetched:
|
|
|
96
106
|
return None
|
|
97
107
|
elif isinstance(message, Path):
|
|
98
108
|
message = message.read_bytes()
|
|
99
|
-
elif isinstance(message,
|
|
109
|
+
elif isinstance(message, IOBase):
|
|
100
110
|
message = message.read()
|
|
101
111
|
elif not isinstance(message, (str, bytes, bool)):
|
|
102
112
|
raise ValueError(
|
|
@@ -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.4.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"
|
|
@@ -14,13 +14,13 @@ classifiers = ["Programming Language :: Python :: 3"]
|
|
|
14
14
|
|
|
15
15
|
[tool.poetry.dependencies]
|
|
16
16
|
python = ">=3.10"
|
|
17
|
-
jsonpickle = "*"
|
|
18
17
|
python-magic = "*"
|
|
19
18
|
python-gnupg = ">=0.5"
|
|
20
19
|
py3-validate-email = "*"
|
|
20
|
+
cryptography = { version = ">=43", optional = true }
|
|
21
21
|
|
|
22
|
-
[tool.poetry.
|
|
23
|
-
smime = ["cryptography
|
|
22
|
+
[tool.poetry.extras]
|
|
23
|
+
smime = ["cryptography"]
|
|
24
24
|
|
|
25
25
|
[tool.poetry.scripts]
|
|
26
26
|
envelope = "envelope.__main__:main"
|