Skip to content

Commit

Permalink
better S/MIME detection, HELO FQDN
Browse files Browse the repository at this point in the history
  • Loading branch information
e3rd committed Jan 2, 2024
1 parent 7e0673e commit eec443a
Show file tree
Hide file tree
Showing 9 changed files with 39 additions and 21 deletions.
6 changes: 3 additions & 3 deletions .github/workflows/run-unittest.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: [3.7, 3.8, 3.9, "3.10", 3.11]
python-version: ["3.10", 3.11, 3.12]
steps:
- uses: actions/checkout@v2
- name: Set up Python ${{ matrix.python-version }}
Expand All @@ -17,7 +17,7 @@ jobs:
python -m pip install --upgrade pip
pip install M2Crypto
pip install -e .
- name: Run tests
- name: Run tests
run: python3 tests.py
# As we support both python-magic and file-magic, try them one by one.
- name: Test python-magic
Expand All @@ -26,7 +26,7 @@ jobs:
run: pip uninstall python-magic -y
- name: Test libmagic missing
id: should_fail
run: python3 tests.py TestMime.test_libmagic
run: python3 tests.py TestMime.test_libmagic
continue-on-error: true
- name: Check on failures
if: steps.should_fail.outcome != 'failure'
Expand Down
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

## 2.0.3
- fix: loading headers not encoded with utf-8
- fix: better S/MIME detection #29
- drop Python 3.9 support
- SMTP HELO FQDN

## 2.0.2 (2022-11-25)
- experimental [XARF](http://xarf.org/) reports reading
Expand Down
9 changes: 5 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -307,18 +307,19 @@ Envelope().attach(path="file.jpg")
* **.date(date)** `str|False` Specify Date header (otherwise Date is added automatically). If False, the Date header will not be added automatically.
* **smtp**: SMTP server
* **--smtp**
* **.smtp(host="localhost", port=25, user=, password=, security=, timeout=3, attempts=3, delay=3)**
* **.smtp(host="localhost", port=25, user=, password=, security=, timeout=3, attempts=3, delay=3, local_hostname=None)**
* **Envelope(smtp=)**
* Parameters:
* `host` May include hostname or any of the following input formats (ex: path to an INI file or a `dict`)
* `security` If not set, automatically set to `starttls` for port *587* and to `tls` for port *465*
* `timeout` How many seconds should SMTP wait before timing out.
* `attempts` How many times we try to send the message to an SMTP server.
* `delay` How many seconds to sleep before re-trying a timed out connection.
* `local_hostname` FQDN of the local host in the HELO/EHLO command.
* Input format may be in the following form:
* `None` default localhost server used
* `smtplib.SMTP` object
* `list` or `tuple` having `host, [port, [username, password, [security, [timeout, [attempts, [delay]]]]]]` parameters
* standard [`smtplib.SMTP`](https://docs.python.org/3/library/smtplib.html) object
* `list` or `tuple` having `host, [port, [username, password, [security, [timeout, [attempts, [delay, [local_hostname]]]]]]]` parameters
* ex: `envelope --smtp localhost 125 [email protected]` will set up host, port and username parameters
* `dict` specifying {"host": ..., "port": ...}
* ex: `envelope --smtp '{"host": "localhost"}'` will set up host parameter
Expand All @@ -330,7 +331,7 @@ Envelope().attach(path="file.jpg")
```
* Do not fear to pass the `smtp` in a loop, we make just a single connection to the server. If timed out, we attempt to reconnect once.
```python3
smtp = localhost, 25
smtp = "localhost", 25
for mail in mails:
Envelope(...).smtp(smtp).send()
```
Expand Down
2 changes: 1 addition & 1 deletion envelope/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ def main():
metavar="SUBTYPE")
group_send.add_argument('--smtp',
help="SMTP server. List `host, [port, [username, password,"
" [security, [timeout, [attempts, [delay]]]]]]` or dict.\n"
" [security, [timeout, [attempts, [delay, [local_hostname]]]]]]]` or dict.\n"
"Ex: '--smtp {\"host\": \"localhost\", \"port\": 25}'."
" Security may be explicitly set to 'starttls', 'tls'"
" or automatically determined by port.",
Expand Down
26 changes: 19 additions & 7 deletions envelope/envelope.py
100755 → 100644
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
from .message import _Message
from .parser import Parser
from .smtp_handler import SMTPHandler
from .utils import AutoSubmittedHeader, is_gpg_importable_key, assure_list, assure_fetched, get_mimetype
from .utils import AutoSubmittedHeader, Fetched, is_gpg_importable_key, assure_list, assure_fetched, get_mimetype

__doc__ = """Quick layer over python-gnupg, M2Crypto, smtplib, magic and 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?
Expand Down Expand Up @@ -705,7 +705,7 @@ def header(self, key, val=None, replace=False) -> Union["Envelope", list, str, N
return self

def smtp(self, host: Any = "localhost", port=25, user=None, password=None, security=None, timeout=3, attempts=3,
delay=3):
delay=3, local_hostname=None):
"""
Obtain SMTP server connection.
Note that you may safely call this in a loop,
Expand All @@ -718,6 +718,7 @@ def smtp(self, host: Any = "localhost", port=25, user=None, password=None, secur
:param timeout: How many seconds should SMTP wait before timing out.
:param attempts: How many times we try to send the message to an SMTP server.
:param delay: How many seconds to sleep before re-trying a timed out connection.
:param local_hostname: FQDN of the local host in the HELO/EHLO command.
:return:
"""
# CLI interface returns always a list or dict, ex: host=["localhost"] or host=["ini file"] or host={}
Expand Down Expand Up @@ -753,7 +754,7 @@ def smtp(self, host: Any = "localhost", port=25, user=None, password=None, secur
self._smtp = SMTPHandler(host)
else:
self._smtp = SMTPHandler(host, port, user, password, security, timeout=timeout, attempts=attempts,
delay=delay)
delay=delay, local_hostname=local_hostname)
return self

def attach(self, attachment=None, mimetype=None, name=None, inline=None, *, path=None):
Expand Down Expand Up @@ -1001,7 +1002,7 @@ def _start(self, sign=None, encrypt=None, send=None):

return email

def _determine_gpg(self, encrypt, sign):
def _determine_gpg(self, encrypt: Fetched | list[Fetched], sign: Fetched):
""" determine if we are using gpg or smime"""
gpg_on = None
if encrypt or sign:
Expand Down Expand Up @@ -1047,8 +1048,7 @@ def _determine_gpg(self, encrypt, sign):
raise RuntimeError("No GPG sign key found")
elif is_gpg_importable_key(sign):
# sign is Path or key contents, import it and get its fingerprint
result = self._gnupg.import_keys(assure_fetched(sign, bytes))
sign = result.fingerprints[0]
sign = self._gpg_import_or_fail(sign)[0]

if encrypt:
if encrypt == AUTO:
Expand All @@ -1064,13 +1064,25 @@ def _determine_gpg(self, encrypt, sign):
decipherers = []
for item in assure_list(encrypt):
if is_gpg_importable_key(item):
decipherers.extend(self._gnupg.import_keys(assure_fetched(item, bytes)).fingerprints)
decipherers.extend(self._gpg_import_or_fail(item))
else:
decipherers.append(item)
encrypt = decipherers

return encrypt, sign, gpg_on

def _gpg_import_or_fail(self, key):
"""
:param key: Any attainable contents
:raises ValueError: If import failed
:return: Fingerprints
"""
if imported := self._gnupg.import_keys(assure_fetched(key, bytes)):
return imported.fingerprints
else:
raise ValueError(f"Could not import key starting: {key[:80]}...")


def _get_gnupg_home(self, for_help=False):
s = self._gpg if type(self._gpg) is str else None
if for_help:
Expand Down
7 changes: 4 additions & 3 deletions envelope/smtp_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ class SMTPHandler:
_instances: Dict[str, SMTP] = {}

def __init__(self, host="localhost", port=25, user=None, password=None, security=None, timeout=3, attempts=3,
delay=3):
delay=3, local_hostname=None):
self.attempts = attempts
# If sending timeouts, delay N seconds before another attempt.
self.delay = delay
Expand All @@ -29,6 +29,7 @@ def __init__(self, host="localhost", port=25, user=None, password=None, security
self.password = password
self.security = security
self.timeout = timeout
self.local_hostname = local_hostname
d = locals()
del d["self"]
self.key = repr(d)
Expand All @@ -43,10 +44,10 @@ def connect(self):

context = ssl.create_default_context()
if self.security == "tls":
smtp = SMTP_SSL(self.host, self.port,
smtp = SMTP_SSL(self.host, self.port, self.local_hostname,
timeout=self.timeout, context=context)
else:
smtp = SMTP(self.host, self.port, timeout=self.timeout)
smtp = SMTP(self.host, self.port, self.local_hostname, timeout=self.timeout)
if self.security == "starttls":
smtp.starttls(context=context)
if self.user:
Expand Down
3 changes: 2 additions & 1 deletion envelope/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from .envelope import Envelope

logger = logging.getLogger(__name__)
Fetched = str | bytes | None


class AutoSubmittedHeader:
Expand Down Expand Up @@ -76,7 +77,7 @@ def assure_list(v):
return [v]


def assure_fetched(message, retyped=None):
def assure_fetched(message, retyped=None) -> Fetched:
""" Accepts object, returns its string or bytes.
If object is
* str or bytes, we consider this is the file contents
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,5 +42,5 @@
classifiers=[
'Programming Language :: Python :: 3'
],
python_requires='>=3.7',
python_requires='>=3.10',
)
2 changes: 1 addition & 1 deletion tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -1522,7 +1522,7 @@ def quit(self):

def key(name):
return "{'host': '" + name + "', 'port': 25, 'user': None, 'password': None," \
" 'security': None, 'timeout': 3, 'attempts': 3, 'delay': 3}"
" 'security': None, 'timeout': 3, 'attempts': 3, 'delay': 3, 'local_hostname': None}"

SMTPHandler._instances={key(name): DummySMTPConnection(name) for name in (f"dummy{i}" for i in range(4))}

Expand Down

0 comments on commit eec443a

Please sign in to comment.