Skip to content

Commit

Permalink
Merge pull request #675 from datafolklabs/pr-669
Browse files Browse the repository at this point in the history
Alternative to PR 669
  • Loading branch information
derks authored Feb 29, 2024
2 parents 497701b + 0b8d39e commit 9243c30
Show file tree
Hide file tree
Showing 9 changed files with 580 additions and 103 deletions.
28 changes: 26 additions & 2 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
13 changes: 11 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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:

Expand Down
107 changes: 88 additions & 19 deletions cement/ext/ext_smtp.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,18 @@
Cement smtp extension module.
"""

import os
import smtplib
from email.header import Header
from email.mime.multipart import MIMEMultipart
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__)


Expand Down Expand Up @@ -44,13 +49,15 @@ class Meta:
'auth': False,
'username': None,
'password': None,
'files': None,
}

def _get_params(self, **kw):
params = dict()

# some keyword args override configuration defaults
for item in ['to', 'from_addr', 'cc', 'bcc', 'subject']:
for item in ['to', 'from_addr', 'cc', 'bcc', 'subject',
'subject_prefix', 'files']:
config_item = self.app.config.get(self._meta.config_section, item)
params[item] = kw.get(item, config_item)

Expand All @@ -61,12 +68,6 @@ def _get_params(self, **kw):
params[item] = self.app.config.get(self._meta.config_section,
item)

# also grab the subject_prefix
params['subject_prefix'] = self.app.config.get(
self._meta.config_section,
'subject_prefix'
)

return params

def send(self, body, **kw):
Expand All @@ -75,14 +76,23 @@ def send(self, body, **kw):
configuration defaults (cc, bcc, etc).
Args:
body: The message body to send
body (tuple): The message body to send. Tuple 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)
from_addr (str): Address (generally email) of the sender
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. Can be
``[ '/path/to/file.ext', ... ]`` or alternative filename can
be defined by passing a list of tuples in the form of
``[ ('alt-name.ext', '/path/to/file.ext'), ...]``
Returns:
bool:``True`` if message is sent successfully, ``False`` otherwise
Expand All @@ -108,21 +118,23 @@ def send(self, body, **kw):
if is_true(params['ssl']):
server = smtplib.SMTP_SSL(params['host'], params['port'],
params['timeout'])
LOG.debug("%s : initiating ssl" % self._meta.label)
if is_true(params['tls']):
LOG.debug("%s : initiating tls" % self._meta.label)
server.starttls()
LOG.debug("%s : initiating smtp over ssl" % self._meta.label)

else:
server = smtplib.SMTP(params['host'], params['port'],
params['timeout'])

if is_true(params['auth']):
server.login(params['username'], params['password'])
LOG.debug("%s : initiating smtp" % self._meta.label)

if self.app.debug is True:
server.set_debuglevel(9)

if is_true(params['tls']):
LOG.debug("%s : initiating tls" % self._meta.label)
server.starttls()

if is_true(params['auth']):
server.login(params['username'], params['password'])

self._send_message(server, body, **params)
server.quit()

Expand All @@ -132,17 +144,74 @@ def _send_message(self, server, body, **params):

msg['From'] = params['from_addr']
msg['To'] = ', '.join(params['to'])
msg['Cc'] = ', '.join(params['cc'])
msg['Bcc'] = ', '.join(params['bcc'])
if params['cc']:
msg['Cc'] = ', '.join(params['cc'])
if params['bcc']:
msg['Bcc'] = ', '.join(params['bcc'])
if params['subject_prefix'] not in [None, '']:
subject = '%s %s' % (params['subject_prefix'],
params['subject'])
else:
subject = params['subject']
msg['Subject'] = Header(subject)

part = MIMEText(body)
msg.attach(part)
# add body as text and/or as html
partText = None
partHtml = None

if type(body) not in [str, tuple]:
error_msg = "Message body must be string or tuple " \
"('<text>', '<html>')"
raise TypeError(error_msg)

if isinstance(body, str):
partText = MIMEText(body)
elif isinstance(body, tuple):
# handle plain text
if len(body) >= 1:
partText = MIMEText(body[0], 'plain')

# handle html
if len(body) >= 2:
partHtml = MIMEText(body[1], 'html')

if partText:
msg.attach(partText)
if partHtml:
msg.attach(partHtml)

# attach files
if params['files']:
for in_path in params['files']:
part = MIMEBase('application', 'octet-stream')

# support for alternative file name if its tuple
# like ('alt-name.ext', '/path/to/file.ext')
if isinstance(in_path, tuple):
if in_path[0] == in_path[1]:
# protect against the full path being passed in
alt_name = os.path.basename(in_path[0])
else:
alt_name = in_path[0]
path = in_path[1]
else:
alt_name = os.path.basename(in_path)
path = in_path

path = fs.abspath(path)

# add attachment
with open(path, 'rb') as file:
part.set_payload(file.read())

# encode and name
encoders.encode_base64(part)
part.add_header(
'Content-Disposition',
f'attachment; filename={alt_name}',
)
msg.attach(part)

server.send_message(msg)


Expand Down
Loading

0 comments on commit 9243c30

Please sign in to comment.