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.
@@ -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
+ [![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)
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
+
@@ -0,0 +1,44 @@
1
+ # Envelope
2
+
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)
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
- args["smtp"] = decode(" ".join(args["smtp"]))
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
- with mock.patch.object(Generator, '_handle_multipart_signed', Generator._handle_multipart):
1148
- # https://github.com/python/cpython/issues/99533 and #19
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))), add_headers=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
- del self._instances[self.key]
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 TextIOBase, BufferedIOBase
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, (TextIOBase, BufferedIOBase, str, bytes)):
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, (TextIOBase, BufferedIOBase)):
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.3.0"
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.project.optional-dependencies]
23
- smime = ["cryptography>=43"]
22
+ [tool.poetry.extras]
23
+ smime = ["cryptography"]
24
24
 
25
25
  [tool.poetry.scripts]
26
26
  envelope = "envelope.__main__:main"