Skip to content

Commit

Permalink
adds send_async() to the email module
Browse files Browse the repository at this point in the history
  • Loading branch information
trp07 committed Oct 11, 2021
1 parent cf57527 commit c62876c
Show file tree
Hide file tree
Showing 5 changed files with 134 additions and 24 deletions.
42 changes: 36 additions & 6 deletions messages/email_.py
Expand Up @@ -2,19 +2,22 @@
Module designed to make creating and sending emails easy.
1. Email
- Uses the Python 3 standard library MIMEMultipart email
object to construct the email.
- Uses the Python 3 standard library email.message.EmailMessage
class to construct the email.
"""

import reprlib
import smtplib
import ssl
from smtplib import SMTPResponseException
from collections.abc import MutableSequence
from email.message import EmailMessage
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from email.mime.application import MIMEApplication

import aiosmtplib

from ._exceptions import MessageSendError
from ._interface import Message
from ._utils import credential_property
Expand Down Expand Up @@ -72,7 +75,7 @@ class Email(Message):
Usage:
Create an email object with required Args above.
Send email with self.send() method.
Send email with self.send() or self.send_async() methods.
Note:
Some email servers may require you to modify security setting, such as
Expand Down Expand Up @@ -169,7 +172,7 @@ def list_to_string(recipient):
return ", ".join(recipient)
return recipient

def _generate_email(self):
def _construct_message(self):
"""Put the parts of the email together."""
self.message = MIMEMultipart()
self._add_header()
Expand Down Expand Up @@ -238,12 +241,12 @@ def _get_tls(self):

def send(self):
"""
Send the message.
Send the message synchronously.
First, a message is constructed, then a session with the email
servers is created, finally the message is sent and the session
is stopped.
"""
self._generate_email()
self._construct_message()

if self.verbose:
print(
Expand Down Expand Up @@ -278,3 +281,30 @@ def send(self):
)

print("Message sent.")


async def send_async(self):
"""
Send the message synchronously.
"""
self._construct_message()

if self.port in (465, "465"):
await aiosmtplib.send(
message=self.message,
hostname=self.server,
port=self.port,
username=self.from_,
password=self._auth,
use_tls=True,
)
elif self.port in (587, "587"):
await aiosmtplib.send(
message=self.message,
hostname=self.server,
port=self.port,
username=self.from_,
password=self._auth,
start_tls=True,
)

18 changes: 17 additions & 1 deletion poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions pyproject.toml
Expand Up @@ -30,6 +30,7 @@ include = ["CHANGES.md"]
python = "^3.7"
validus = "^0.3.0"
httpx = "^0.19.0"
aiosmtplib = "^1.1.6"

[tool.poetry.dev-dependencies]
pytest-cov = "^2.6"
Expand Down
11 changes: 11 additions & 0 deletions tests/conftest.py
@@ -1,6 +1,8 @@
"""reusable testing fixtures."""

import os
from unittest.mock import MagicMock

import pytest


Expand All @@ -18,3 +20,12 @@
skip_if_not_on_travisCI = pytest.mark.skipif("TRAVIS" not in os.environ,
reason='skipping test if not on travis-ci')


##############################################################################
# AsyncIO fixtures
##############################################################################

class AsyncMock(MagicMock):
"""Generic mock class to be used for async testing."""
async def __call__(self, *args, **kwargs):
return super().__call__(*args, **kwargs)
86 changes: 69 additions & 17 deletions tests/test_email.py
@@ -1,6 +1,6 @@
"""messages.email_ tests."""

import getpass
import asyncio
import pathlib
import smtplib
from smtplib import SMTPResponseException
Expand All @@ -12,6 +12,7 @@
from messages.email_ import Email
from messages._exceptions import MessageSendError

from conftest import AsyncMock
from conftest import skip_if_on_travisCI
from conftest import skip_if_not_on_travisCI

Expand Down Expand Up @@ -140,20 +141,20 @@ def test_list_to_string(get_email):


##############################################################################
# TESTS: Email._generate_email
# TESTS: Email._construct_message
##############################################################################

def test_generate_email(get_email, mocker):
def test_construct_message(get_email, mocker):
"""
GIVEN a valid Email object
WHEN Email.generate_email() is called
WHEN Email._construct_message() is called
THEN assert the email structure is created
"""
header_mock = mocker.patch.object(Email, '_add_header')
body_mock = mocker.patch.object(Email, '_add_body')
attach_mock = mocker.patch.object(Email, '_add_attachments')
e = get_email
e._generate_email()
e._construct_message()
assert isinstance(e.message, MIMEMultipart)
assert header_mock.call_count == 1
assert body_mock.call_count == 1
Expand All @@ -166,14 +167,14 @@ def test_generate_email(get_email, mocker):

def test_add_header(get_email, mocker):
"""
GIVEN a valid Email object, where Email.generate_email() has been called
GIVEN a valid Email object, where Email._construct_message() has been called
WHEN Email.add_header() is called
THEN assert correct parameters are set
"""
body_mock = mocker.patch.object(Email, '_add_body')
attach_mock = mocker.patch.object(Email, '_add_attachments')
e = get_email
e._generate_email()
e._construct_message()
assert e.message['From'] == 'me@here.com'
assert e.message['Subject'] == 'subject'

Expand All @@ -184,15 +185,15 @@ def test_add_header(get_email, mocker):

def test_add_body(get_email, mocker):
"""
GIVEN a valid Email object, where Email.generate_email() has been called
GIVEN a valid Email object, where Email._construct_message() has been called
WHEN Email.add_body() is called
THEN assert body_text is attached
"""
attach_mock = mocker.patch.object(Email, '_add_attachments')
header_mock = mocker.patch.object(Email, '_add_header')
mime_attach_mock = mocker.patch.object(MIMEMultipart, 'attach')
e = get_email
e._generate_email()
e._construct_message()
assert mime_attach_mock.call_count == 1


Expand All @@ -203,7 +204,7 @@ def test_add_body(get_email, mocker):
@skip_if_on_travisCI
def test_add_attachments_list_local(get_email, mocker):
"""
GIVEN a valid Email object, where Email.generate_email() has been called
GIVEN a valid Email object, where Email._construct_message() has been called
and Email.attachments is a list
WHEN Email.add_attachments() is called
THEN assert correct attachments are attached
Expand All @@ -214,14 +215,14 @@ def test_add_attachments_list_local(get_email, mocker):
e = get_email
e.attachments = [str(TESTDIR.joinpath('file1.txt')), str(TESTDIR.joinpath('file2.png')),
str(TESTDIR.joinpath('file3.pdf')), str(TESTDIR.joinpath('file4.xlsx'))]
e._generate_email()
e._construct_message()
assert mime_attach_mock.call_count == 4


@skip_if_not_on_travisCI
def test_add_attachments_list_travis(get_email, mocker):
"""
GIVEN a valid Email object, where Email.generate_email() has been called
GIVEN a valid Email object, where Email._construct_message() has been called
and Email.attachments is a list
WHEN Email.add_attachments() is called
THEN assert correct attachments are attached
Expand All @@ -233,14 +234,14 @@ def test_add_attachments_list_travis(get_email, mocker):
PATH = '/home/travis/build/HomeMadePy/messages/tests/data/'
e.attachments = [PATH + 'file1.txt', PATH + 'file2.png',
PATH + 'file3.pdf', PATH + 'file4.xlsx']
e._generate_email()
e._construct_message()
assert mime_attach_mock.call_count == 4


@skip_if_on_travisCI
def test_add_attachments_str_local(get_email, mocker):
"""
GIVEN a valid Email object, where Email.generate_email() has been called
GIVEN a valid Email object, where Email._construct_message() has been called
and Email.attachments is a str
WHEN Email.add_attachments() is called
THEN assert correct attachments are attached
Expand All @@ -250,14 +251,14 @@ def test_add_attachments_str_local(get_email, mocker):
mime_attach_mock = mocker.patch.object(MIMEMultipart, 'attach')
e = get_email
e.attachments = str(TESTDIR.joinpath('file1.txt'))
e._generate_email()
e._construct_message()
assert mime_attach_mock.call_count == 1


@skip_if_not_on_travisCI
def test_add_attachments_str_travis(get_email, mocker):
"""
GIVEN a valid Email object, where Email.generate_email() has been called
GIVEN a valid Email object, where Email._construct_message() has been called
and Email.attachments is a str
WHEN Email.add_attachments() is called
THEN assert correct attachments are attached
Expand All @@ -268,7 +269,7 @@ def test_add_attachments_str_travis(get_email, mocker):
e = get_email
PATH = '/home/travis/build/HomeMadePy/messages/tests/data/'
e.attachments = PATH + 'file1.txt'
e._generate_email()
e._construct_message()
assert mime_attach_mock.call_count == 1


Expand Down Expand Up @@ -465,3 +466,54 @@ def test_send_verbose_false(get_email, capsys, mocker):
assert ' * Server: smtp.gmail.com:465' not in out
assert ' * From: me@here.com' not in out
assert err == ''


##############################################################################
# TESTS: Email.send_async
##############################################################################

@pytest.mark.asyncio
async def test_send_async_ssl(get_email, mocker):
"""
GIVEN a valid Email object with port=465
WHEN Email.send_async() is called
THEN assert calls aiosmtplib.send with ssl
AsyncMock found in conftest.py
"""
async_mock = mocker.patch("aiosmtplib.send", new_callable=AsyncMock)
e = get_email
e.attachments = None
await e.send_async()
async_mock.assert_called_with(
message=e.message,
hostname=e.server,
port=e.port,
username=e.from_,
password=e._auth,
use_tls=True,
)


@pytest.mark.asyncio
async def test_send_async_tls(get_email, mocker):
"""
GIVEN a valid Email object with port=587
WHEN Email.send_async() is called
THEN assert calls aiosmtplib.send with TLS
AsyncMock found in conftest.py
"""
async_mock = mocker.patch("aiosmtplib.send", new_callable=AsyncMock)
e = get_email
e.attachments = None
e.port = 587
await e.send_async()
async_mock.assert_called_with(
message=e.message,
hostname=e.server,
port=e.port,
username=e.from_,
password=e._auth,
start_tls=True,
)

0 comments on commit c62876c

Please sign in to comment.