diff --git a/notifiers/providers/email.py b/notifiers/providers/email.py index dbdbb9fd..ef449828 100644 --- a/notifiers/providers/email.py +++ b/notifiers/providers/email.py @@ -1,11 +1,12 @@ import getpass +import mimetypes import smtplib import socket from email.message import EmailMessage -from email.mime.application import MIMEApplication from email.utils import formatdate from pathlib import Path from smtplib import SMTPAuthenticationError, SMTPServerDisconnected, SMTPSenderRefused +from typing import Tuple, List from ..core import Provider, Response from ..utils.schema.helpers import one_or_more, list_to_commas @@ -82,6 +83,17 @@ class SMTP(Provider): "additionalProperties": False, } + @staticmethod + def _get_mimetype(attachment: Path) -> Tuple[str, str]: + """Taken from https://docs.python.org/3/library/email.examples.html""" + ctype, encoding = mimetypes.guess_type(str(attachment)) + if ctype is None or encoding is not None: + # No guess could be made, or the file is encoded (compressed), so + # use a generic bag-of-bits type. + ctype = 'application/octet-stream' + maintype, subtype = ctype.split('/', 1) + return maintype, subtype + def __init__(self): super().__init__() self.smtp_server = None @@ -119,14 +131,11 @@ def _build_email(data: dict) -> EmailMessage: email.add_alternative(data["message"], subtype=content_type) return email - @staticmethod - def _add_attachments(data: dict, email: EmailMessage) -> EmailMessage: - for attachment in data["attachments"]: - file = Path(attachment).read_bytes() - part = MIMEApplication(file) - part.add_header("Content-Disposition", "attachment", filename=attachment) - email.attach(part) - return email + def _add_attachments(self, attachments: List[str], email: EmailMessage): + for attachment in attachments: + attachment = Path(attachment) + maintype, subtype = self._get_mimetype(attachment) + email.add_attachment(attachment.read_bytes(), maintype=maintype, subtype=subtype, filename=attachment.name) def _connect_to_server(self, data: dict): self.smtp_server = smtplib.SMTP_SSL if data["ssl"] else smtplib.SMTP @@ -155,7 +164,7 @@ def _send_notification(self, data: dict) -> Response: self._connect_to_server(data) email = self._build_email(data) if data.get("attachments"): - email = self._add_attachments(data, email) + self._add_attachments(data['attachments'], email) self.smtp_server.send_message(email) except ( SMTPServerDisconnected, diff --git a/source/changelog.rst b/source/changelog.rst index d8dd2750..902123e8 100644 --- a/source/changelog.rst +++ b/source/changelog.rst @@ -7,6 +7,9 @@ Changelog ------------------ - Added ability to cancel login to SMTP/GMAIL if credentials are used (`#210 `_, `#266 `_) +- Loosened dependencies (`#209 `_, `#266 `_) +- Added mimetype guessing for email (`#239 `_, `#266 `_) + 1.0.4 ------ diff --git a/tests/providers/test_smtp.py b/tests/providers/test_smtp.py index 4eb12140..595684e6 100644 --- a/tests/providers/test_smtp.py +++ b/tests/providers/test_smtp.py @@ -1,3 +1,5 @@ +from email.message import EmailMessage + import pytest from notifiers.exceptions import BadArguments, NotificationError @@ -64,6 +66,22 @@ def test_attachment(self, provider, tmpdir): ) assert rsp.data["attachments"] == attachments + def test_attachment_mimetypes(self, provider, tmpdir): + dir_ = tmpdir.mkdir("sub") + file_1 = dir_.join("foo.txt") + file_1.write("foo") + file_2 = dir_.join("bar.jpg") + file_2.write("foo") + file_3 = dir_.join("baz.pdf") + file_3.write("foo") + attachments = [str(file_1), str(file_2), str(file_3)] + email = EmailMessage() + provider._add_attachments(attachments=attachments, email=email) + attach1, attach2, attach3 = email.iter_attachments() + assert attach1.get_content_type() == 'text/plain' + assert attach2.get_content_type() == 'image/jpeg' + assert attach3.get_content_type() == 'application/pdf' + @pytest.mark.online def test_smtp_sanity(self, provider, test_message): """using Gmail SMTP"""