SMTP Extension Bug Fixes and Enhancements

- Resolves Pull Request 669
- Resolves Issue 667
- Resolves Issue 668
This commit is contained in:
BJ Dierkes 2024-02-26 20:13:02 -06:00
parent 1232b8f317
commit 739c5cdcfc
9 changed files with 488 additions and 105 deletions

View File

@ -3,23 +3,47 @@ sudo: false
script: ./scripts/travis.sh
os:
- linux
# env's are redundant, but at global scope additional jobs are created for
# each env var which I'm sure has a purpose but don't like
matrix:
include:
- python: "3.8"
dist: "xenial"
dist: "focal"
sudo: true
env:
- DOCKER_COMPOSE_VERSION=v2.17.3
- SMTP_HOST=localhost
- SMTP_PORT=1025
- python: "3.9"
dist: "xenial"
dist: "focal"
sudo: true
env:
- DOCKER_COMPOSE_VERSION=v2.17.3
- SMTP_HOST=localhost
- SMTP_PORT=1025
- python: "3.10"
dist: "focal"
sudo: true
env:
- DOCKER_COMPOSE_VERSION=v2.17.3
- SMTP_HOST=localhost
- SMTP_PORT=1025
- python: "3.11"
dist: "focal"
sudo: true
env:
- DOCKER_COMPOSE_VERSION=v2.17.3
- SMTP_HOST=localhost
- SMTP_PORT=1025
- python: "3.12"
dist: "jammy"
sudo: true
env:
- DOCKER_COMPOSE_VERSION=v2.17.3
- SMTP_HOST=localhost
- SMTP_PORT=1025
services:
- memcached
- redis-server
- docker

View File

@ -8,18 +8,27 @@ Bugs:
- [Issue #310](https://github.com/datafolklabs/cement/issues/310)
- `[core.foundation]` Quiet mode file is never closed
- [Issue #653](https://github.com/datafolklabs/cement/issues/653)
- `[ext.smtp]` Ability to Enable TLS without SSL
- [Issue #667](https://github.com/datafolklabs/cement/issues/667)
- `[ext.smtp]` Empty (wrong) addresses sent when CC/BCC is `None`
- [Issue #668](https://github.com/datafolklabs/cement/issues/668)
Features:
- `[utils.fs]` Add Timestamp Support to fs.backup
- [Issue #611](https://github.com/datafolklabs/cement/issues/611)
- `[ext.smtp]` Support for sending file attachements.
- [PR #669](https://github.com/datafolklabs/cement/pull/669)
- `[ext.smtp]` Support for sending both Plain Text and HTML
- [PR #669](https://github.com/datafolklabs/cement/pull/669)
Refactoring:
- `[core.plugin]` Deprecate the use of `imp` in favor of `importlib`
- [Issue #386](https://github.com/datafolklabs/cement/issues/386)
- `[ext.smtp]` Actually test SMTP against a real server (replace mocks)
Misc:
@ -34,7 +43,7 @@ Misc:
- `[dev]` Add `comply-typing` to make helpers, start working toward typing.
- [Issue #599](https://github.com/datafolklabs/cement/issues/661)
- [PR #628](https://github.com/datafolklabs/cement/pull/628)
- `[dev]` Add `mailpit` service to docker-compose development config.
Deprecations:

View File

@ -2,7 +2,6 @@
Cement smtp extension module.
"""
import os
import smtplib
from email.header import Header
from email.mime.multipart import MIMEMultipart
@ -10,8 +9,10 @@ from email.mime.base import MIMEBase
from email.mime.text import MIMEText
from email import encoders
from ..core import mail
from ..utils import fs
from ..utils.misc import minimal_logger, is_true
LOG = minimal_logger(__name__)
@ -74,7 +75,10 @@ class SMTPMailHandler(mail.MailHandler):
configuration defaults (cc, bcc, etc).
Args:
body: The message body to send
body (list): The message body to send. List is treated as:
``[<text>, <html>]``. If a single string is passed it will be
converted to ``[<text>]``. At minimum, a text version is
required.
Keyword Args:
to (list): List of recipients (generally email addresses)
@ -82,6 +86,9 @@ class SMTPMailHandler(mail.MailHandler):
cc (list): List of CC Recipients
bcc (list): List of BCC Recipients
subject (str): Message subject line
subject_prefix (str): Prefix for message subject line (useful to
override if you want to remove/change the default prefix).
files (list): List of file paths to attach to the message.
Returns:
bool:``True`` if message is sent successfully, ``False`` otherwise
@ -107,7 +114,7 @@ class SMTPMailHandler(mail.MailHandler):
if is_true(params['ssl']):
server = smtplib.SMTP_SSL(params['host'], params['port'],
params['timeout'])
LOG.debug("%s : initiating ssl" % self._meta.label)
LOG.debug("%s : initiating smtp over ssl" % self._meta.label)
else:
server = smtplib.SMTP(params['host'], params['port'],
@ -143,45 +150,61 @@ class SMTPMailHandler(mail.MailHandler):
else:
subject = params['subject']
msg['Subject'] = Header(subject)
# add body as text and or or as html
partText = None
partHtml = None
if isinstance(body, str):
partText = MIMEText(body)
elif isinstance(body, list):
# handle plain text
if len(body) >= 1:
partText = MIMEText(body[0], 'plain')
# handle html
if len(body) >= 2:
partHtml = MIMEText(body[1], 'html')
elif isinstance(body, dict):
if 'text' in body:
partText = MIMEText(body['text'], 'plain')
if 'html' in body:
partHtml = MIMEText(body['html'], 'html')
if partText:
msg.attach(partText)
if partHtml:
msg.attach(partHtml)
# loop files
# attach files
if params['files']:
for path in params['files']:
for in_path in params['files']:
part = MIMEBase('application', 'octet-stream')
# test filename for a seperate attachement disposition name
# support for alternative file name if its tuple
# like (filename.ext=attname.ext)
filename = os.path.basename(path)
# test for divider in filename
i = filename.find('=')
# split attname from filename
if i < 0:
attname = filename
if isinstance(in_path, tuple):
attname = in_path[0]
path = in_path[1]
else:
attname = filename[i+1:]
filename = filename[0:i]
# update the filename to read from
path = os.path.dirname(path) + '/' + filename
attname = in_path
path = in_path
path = fs.abspath(path)
# filename = os.path.basename(path)
# # test for divider in filename
# i = filename.find('=')
# # split attname from filename
# if i < 0:
# attname = filename
# else:
# attname = filename[i+1:]
# filename = filename[0:i]
# # update the filename to read from
# path = fs.join(os.path.dirname(path), filename)
# add attachment
with open(path, 'rb') as file:
part.set_payload(file.read())
# encode and name
encoders.encode_base64(part)
part.add_header(

View File

@ -1,80 +1,104 @@
version: "3"
volumes:
mailpit-data:
services:
cement: &DEFAULTS
image: "cement:dev"
build:
context: .
dockerfile: docker/Dockerfile.dev
hostname: cement
stdin_open: true
tty: true
volumes:
- '.:/src'
working_dir: '/src'
links:
- redis:redis
- memcached:memcached
environment:
REDIS_HOST: redis
MEMCACHED_HOST: memcached
cement: &DEFAULTS
image: "cement:dev"
build:
context: .
dockerfile: docker/Dockerfile.dev
hostname: cement
stdin_open: true
tty: true
volumes:
- '.:/src'
working_dir: '/src'
links:
- redis:redis
- memcached:memcached
environment:
REDIS_HOST: redis
MEMCACHED_HOST: memcached
SMTP_HOST: mailpit
SMTP_PORT: 1025
cement-py35:
<<: *DEFAULTS
image: "cement:dev-py35"
build:
context: .
dockerfile: docker/Dockerfile.dev-py35
profiles:
- donotstart
cement-py35:
<<: *DEFAULTS
image: "cement:dev-py35"
build:
context: .
dockerfile: docker/Dockerfile.dev-py35
profiles:
- donotstart
cement-py36:
<<: *DEFAULTS
image: "cement:dev-py36"
build:
context: .
dockerfile: docker/Dockerfile.dev-py36
profiles:
- donotstart
cement-py36:
<<: *DEFAULTS
image: "cement:dev-py36"
build:
context: .
dockerfile: docker/Dockerfile.dev-py36
profiles:
- donotstart
cement-py37:
<<: *DEFAULTS
image: "cement:dev-py37"
build:
context: .
dockerfile: docker/Dockerfile.dev-py37
cement-py38:
<<: *DEFAULTS
image: "cement:dev-py38"
build:
context: .
dockerfile: docker/Dockerfile.dev-py38
cement-py39:
<<: *DEFAULTS
image: "cement:dev-py39"
build:
context: .
dockerfile: docker/Dockerfile.dev-py39
cement-py310:
<<: *DEFAULTS
image: "cement:dev-py310"
build:
context: .
dockerfile: docker/Dockerfile.dev-py310
cement-py37:
<<: *DEFAULTS
image: "cement:dev-py37"
build:
context: .
dockerfile: docker/Dockerfile.dev-py37
cement-py38:
<<: *DEFAULTS
image: "cement:dev-py38"
build:
context: .
dockerfile: docker/Dockerfile.dev-py38
cement-py39:
<<: *DEFAULTS
image: "cement:dev-py39"
build:
context: .
dockerfile: docker/Dockerfile.dev-py39
cement-py310:
<<: *DEFAULTS
image: "cement:dev-py310"
build:
context: .
dockerfile: docker/Dockerfile.dev-py310
cement-py311:
<<: *DEFAULTS
image: "cement:dev-py311"
build:
context: .
dockerfile: docker/Dockerfile.dev-py311
cement-py311:
<<: *DEFAULTS
image: "cement:dev-py311"
build:
context: .
dockerfile: docker/Dockerfile.dev-py311
redis:
image: redis:latest
hostname: redis
redis:
image: redis:latest
hostname: redis
memcached:
image: memcached:latest
hostname: memcached
memcached:
image: memcached:latest
hostname: memcached
mailpit:
image: axllent/mailpit
container_name: mailpit
restart: always
volumes:
- mailpit-data:/data
- ./docker/mailpit:/certificates
ports:
- 8025:8025
- 1025:1025
environment:
MP_MAX_MESSAGES: 5000
MP_DATA_FILE: /data/mailpit.db
MP_SMTP_AUTH_ACCEPT_ANY: 1
MP_SMTP_AUTH_ALLOW_INSECURE: 1
MP_SMTP_TLS_CERT: /certificates/dev-cert.pem
MP_SMTP_TLS_KEY: /certificates/dev-key.pem

View File

@ -0,0 +1,35 @@
-----BEGIN CERTIFICATE-----
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
mjErtgV+K4j0gVg4kaci6kXx1g==
-----END CERTIFICATE-----

View File

@ -0,0 +1,52 @@
-----BEGIN PRIVATE KEY-----
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
mDpNhf5uD/yZ9rYY6wjJ4nQYBTTJpQ==
-----END PRIVATE KEY-----

View File

@ -6,6 +6,10 @@ set -e
# https://travis-ci.community/t/build-error-for-python-3-7-on-two-different-projects/12895/3
# pip install -U importlib_metadata
docker-compose up -d mailpit 2>&1 >/dev/null
sleep 10
rm -f .coverage
pip install -r requirements-dev.txt
make test

View File

@ -23,4 +23,4 @@ def key(request):
@pytest.fixture(scope="function")
def rando(request):
yield _rando()
yield _rando()[:12]

View File

@ -1,8 +1,26 @@
import os
import mock
import requests
import json
from time import sleep
from cement.utils.test import TestApp
from cement.utils.misc import init_defaults
if 'SMTP_HOST' in os.environ.keys():
smtp_host = os.environ['SMTP_HOST']
else:
smtp_host = 'mailpit'
mailpit_api = f'http://{smtp_host}:8025/api/v1'
defaults = init_defaults('mail.smtp')
defaults['mail.smtp']['host'] = smtp_host
defaults['mail.smtp']['port'] = 1025
defaults['mail.smtp']['to'] = 'noreply@localhost'
defaults['mail.smtp']['from_addr'] = 'nobody@localhost'
defaults['mail.smtp']['subject_prefix'] = 'UNIT TEST >'
class SMTPApp(TestApp):
class Meta:
@ -10,14 +28,167 @@ class SMTPApp(TestApp):
mail_handler = 'smtp'
def test_smtp_defaults():
def delete_msg(message_id):
payload = {
"ids": [
message_id
]
}
payload = json.dumps(payload)
headers = {
'accept': 'application/json',
'content-type': 'application/json',
}
requests.delete(f"{mailpit_api}/messages", data=payload, headers=headers)
def test_smtp_send(rando):
defaults['mail.smtp']['subject'] = rando
with SMTPApp(config_defaults=defaults) as app:
app.run()
app.mail.send(f"{rando}",
to=[f'to-{rando}@localhost'],
from_addr=f'from-{rando}@localhost')
res = requests.get(f"{mailpit_api}/search?query={rando}")
data = res.json()
assert len(data['messages']) == 1
msg = data['messages'][0]
assert msg['From']['Address'] == f'from-{rando}@localhost'
assert msg['To'][0]['Address'] == f'to-{rando}@localhost'
assert msg['Subject'] == f'UNIT TEST > {rando}'
assert msg['Attachments'] == 0
assert msg['Cc'] == []
assert msg['Bcc'] == []
delete_msg(msg['ID'])
def test_smtp_html(rando):
defaults['mail.smtp']['subject'] = rando
with SMTPApp(config_defaults=defaults) as app:
app.run()
app.mail.send([f"{rando}", f"<body>{rando}</body>"])
sleep(3)
res = requests.get(f"{mailpit_api}/search?query={rando}")
data = res.json()
assert len(data['messages']) == 1
msg = data['messages'][0]
text_url = f"http://{smtp_host}:8025/view/{msg['ID']}.txt"
res_text = requests.get(text_url)
assert res_text.content.decode('utf-8') == rando
html_url = f"http://{smtp_host}:8025/view/{msg['ID']}.html"
res_html = requests.get(html_url)
assert res_html.content.decode('utf-8') == f"<body>{rando}</body>"
delete_msg(msg['ID'])
def test_smtp_cc_bcc(rando):
defaults['mail.smtp']['subject'] = rando
with SMTPApp(config_defaults=defaults) as app:
app.run()
cc = [f"cc1-{rando}@localhost", f"cc2-{rando}@localhost"]
bcc = [f"bcc1-{rando}@localhost", f"bcc2-{rando}@localhost"]
app.mail.send(f"{rando}",
cc=cc,
bcc=bcc)
res = requests.get(f"{mailpit_api}/search?query={rando}")
data = res.json()
assert len(data['messages']) == 1
msg = data['messages'][0]
cc_verify = [x['Address'] for x in msg['Cc']]
bcc_verify = [x['Address'] for x in msg['Bcc']]
assert f"cc1-{rando}@localhost" in cc_verify
assert f"cc2-{rando}@localhost" in cc_verify
assert f"bcc1-{rando}@localhost" in bcc_verify
assert f"bcc2-{rando}@localhost" in bcc_verify
delete_msg(msg['ID'])
def test_smtp_files(rando, tmp):
defaults['mail.smtp']['subject'] = rando
with SMTPApp(config_defaults=defaults) as app:
app.run()
files = []
for iter in [1, 2, 3]:
_file = f"{tmp.file}-{iter}"
with open(_file, 'w') as _open_file:
_open_file.write(f"{rando}-{iter}")
files.append(_file)
app.mail.send(f"{rando}", files=files)
sleep(3)
res = requests.get(f"{mailpit_api}/search?query={rando}")
data = res.json()
assert len(data['messages']) == 1
msg = data['messages'][0]
assert msg['Attachments'] == 3
delete_msg(msg['ID'])
def test_smtp_files_alt_name(rando, tmp):
defaults['mail.smtp']['subject'] = rando
with SMTPApp(config_defaults=defaults) as app:
app.run()
files = [(f"alt-filename-{rando}", tmp.file)]
app.mail.send(f"{rando}", files=files)
sleep(3)
res = requests.get(f"{mailpit_api}/search?query={rando}")
data = res.json()
assert len(data['messages']) == 1
msg = data['messages'][0]
assert msg['Attachments'] == 1
res_full = requests.get(f"{mailpit_api}/message/{msg['ID']}")
data = res_full.json()
assert data['Attachments'][0]['FileName'] == f"alt-filename-{rando}"
delete_msg(msg['ID'])
def test_smtp_tls(rando):
defaults['mail.smtp']['subject'] = rando
defaults['mail.smtp']['tls'] = True
with SMTPApp(config_defaults=defaults, debug=True) as app:
app.run()
app.mail.send(rando)
res = requests.get(f"{mailpit_api}/search?query={rando}")
data = res.json()
assert len(data['messages']) == 1
msg = data['messages'][0]
delete_msg(msg['ID'])
# FIXME: need to replace old mocks with mailpit tests
def test_mock_smtp_defaults():
defaults = init_defaults('mail.smtp')
defaults['mail.smtp']['to'] = 'nobody@localhost'
defaults['mail.smtp']['to'] = 'test_smtp_defaults@localhost'
defaults['mail.smtp']['from_addr'] = 'nobody@localhost'
defaults['mail.smtp']['cc'] = 'nobody@localhost'
defaults['mail.smtp']['bcc'] = 'nobody@localhost'
defaults['mail.smtp']['subject'] = 'Test Email To nobody@localhost'
defaults['mail.smtp']['subject_prefix'] = 'PREFIX >'
defaults['mail.smtp']['subject'] = 'test_smtp_defaults'
defaults['mail.smtp']['subject_prefix'] = 'UNIT TEST >'
with mock.patch('smtplib.SMTP') as mock_smtp:
with SMTPApp(config_defaults=defaults) as app:
@ -28,17 +199,38 @@ def test_smtp_defaults():
assert instance.send_message.call_count == 1
def test_smtp_ssl_tls():
def test_mock_smtp_ssl():
defaults = init_defaults('mail.smtp')
defaults['mail.smtp']['ssl'] = True
defaults['mail.smtp']['tls'] = True
defaults['mail.smtp']['port'] = 25
defaults['mail.smtp']['subject'] = 'test_smtp_ssl'
defaults['mail.smtp']['subject_prefix'] = 'UNIT TEST >'
with mock.patch('smtplib.SMTP_SSL') as mock_smtp:
with SMTPApp(config_defaults=defaults, debug=True) as app:
app.run()
app.mail.send('TEST MESSAGE',
to=['me@localhost'],
to=['test_smtp_ssl@localhost'],
from_addr='noreply@localhost')
instance = mock_smtp.return_value
assert instance.send_message.call_count == 1
def test_mock_smtp_tls_no_ssl():
defaults = init_defaults('mail.smtp')
defaults['mail.smtp']['ssl'] = False
defaults['mail.smtp']['tls'] = True
defaults['mail.smtp']['port'] = 25
defaults['mail.smtp']['subject'] = 'test_smtp_tls_no_ssl'
defaults['mail.smtp']['subject_prefix'] = 'UNIT TEST >'
with mock.patch('smtplib.SMTP') as mock_smtp:
with SMTPApp(config_defaults=defaults, debug=True) as app:
app.run()
app.mail.send('TEST MESSAGE',
to=['test_smtp_tls_no_ssl@localhost'],
from_addr='noreply@localhost')
instance = mock_smtp.return_value
@ -46,7 +238,27 @@ def test_smtp_ssl_tls():
assert instance.starttls.call_count == 1
def test_smtp_auth(rando):
def test_mock_smtp_tls_over_ssl():
defaults = init_defaults('mail.smtp')
defaults['mail.smtp']['ssl'] = True
defaults['mail.smtp']['tls'] = True
defaults['mail.smtp']['port'] = 25
defaults['mail.smtp']['subject'] = 'test_smtp_tls_over_ssl'
defaults['mail.smtp']['subject_prefix'] = 'UNIT TEST >'
with mock.patch('smtplib.SMTP_SSL') as mock_smtp:
with SMTPApp(config_defaults=defaults, debug=True) as app:
app.run()
app.mail.send('TEST MESSAGE',
to=['test_smtp_tls_no_ssl@localhost'],
from_addr='noreply@localhost')
instance = mock_smtp.return_value
assert instance.send_message.call_count == 1
assert instance.starttls.call_count == 1
def test_mock_smtp_auth(rando):
defaults = init_defaults(rando, 'mail.smtp')
defaults[rando]['debug'] = True
defaults['mail.smtp']['auth'] = True