Skip to content

Commit

Permalink
refact: organize the code with adapter
Browse files Browse the repository at this point in the history
  • Loading branch information
Kaspary committed May 18, 2023
1 parent 9e6e3da commit be32292
Show file tree
Hide file tree
Showing 8 changed files with 207 additions and 79 deletions.
126 changes: 126 additions & 0 deletions src/adapters.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
from email.mime.base import MIMEBase
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from email.utils import COMMASPACE, formatdate


class EmailMIMEAdapter(MIMEMultipart):
__body: str
__attatchments: list[tuple[bytes, str]]

def __init__(
self,
sender: str,
subject: str,
body: str,
sender_name: str = None,
to: list[str] = [],
cc: list[str] = [],
bcc: list[str] = [],
attatchments: list[tuple[bytes, str]] = [],
) -> None:
super().__init__()
self["Date"] = formatdate(localtime=True)
self.sender = sender
self.subject = subject
self.body = body
self.sender_name = sender_name
self.to = to
self.cc = cc
self.bcc = bcc
self.attatchments = attatchments

@property
def sender(self):
return self["From"]

@sender.setter
def sender(self, value: str | tuple[str, str]):
assert isinstance(
value, (str, tuple)
), f"sender expect string or tuple of strings type, but got {type(value)}"
if isinstance(value, str):
self["From"] = value
elif isinstance(value, tuple):
self["From"] = "{0} <{1}>".format(*value)

@property
def subject(self):
return self["Subject"]

@subject.setter
def subject(self, value: str):
assert isinstance(
value, str
), f"subject expect string type, but got {type(value)}"
self["Subject"] = value

@property
def to(self):
return self["To"].split(COMMASPACE)

@to.setter
def to(self, value: list[str]):
assert isinstance(
value, list
), f"to expect list of strings type, but got {type(value)}"
self["To"] = COMMASPACE.join(value)

@property
def cc(self):
return self["Cc"].split(COMMASPACE)

@cc.setter
def cc(self, value: list[str]):
assert isinstance(
value, list
), f"cc expect list of strings type, but got {type(value)}"
self["Cc"] = COMMASPACE.join(value)

@property
def bcc(self):
return self["Bcc"].split(COMMASPACE)

@bcc.setter
def bcc(self, value: list[str]):
assert isinstance(value, list), f"bcc expect a list type, but got {type(value)}"
self["Bcc"] = COMMASPACE.join(value)

@property
def recipients(self):
return self.to + self.cc + self.bcc

@property
def body(self):
return self.__body

@body.setter
def body(self, value: str):
assert isinstance(value, str), f"body expect string type, but got {type(value)}"
self.__body = value

@property
def attatchments(self):
return self.__attatchments

@attatchments.setter
def attatchments(self, value: list[tuple[bytes, str]]):
assert isinstance(
value, list
), f"attatchments expect a list type, but got {type(value)}"
self.__attatchments = value

def get_payload(self, *args, **kwargs):
self._payload = []
self.attach(MIMEText(self.body))
for attatchment in self.attatchments:
self.__set_attatchment(*attatchment)
return super().get_payload(*args, **kwargs)

def __set_attatchment(self, data: bytes, filename: str):
attatchment = MIMEBase("application", "octet-stream")
attatchment.set_payload(data)
attatchment.add_header(
"Content-Disposition", f"attachment; filename={filename}"
)
self.attach(attatchment)
6 changes: 3 additions & 3 deletions src/clients.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ def smtp(self):
return self

@abstractmethod
def send_mail(self, to_addrs: str | Sequence[str], msg: str, from_addr: str = None):
def send_email(self, to_addrs: str | Sequence[str], msg: str, from_addr: str = None):
pass


Expand All @@ -43,7 +43,7 @@ def smtp(self):
self._client.login(self._account, self._password)
return self

def send_mail(
def send_email(
self, to_addrs: str | Sequence[str], message: str, from_addr: str = None
):
from_addr = from_addr or self._account
Expand All @@ -64,7 +64,7 @@ def smtp(self):
self._client.login(self._account, self._password)
return self

def send_mail(
def send_email(
self, to_addrs: str | Sequence[str], message: str, from_addr: str = None
):
from_addr = from_addr or self._account
Expand Down
7 changes: 4 additions & 3 deletions src/enums.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
from enum import Enum


class ServerSMTP(Enum):
GMAIL = 'smtp.gmail.com'
OUTLOOK = 'smtp-mail.outlook.com'
GMAIL = "smtp.gmail.com"
OUTLOOK = "smtp-mail.outlook.com"


class PortSMTP(Enum):
GMAIL = 465
OUTLOOK = 587
OUTLOOK = 587
16 changes: 0 additions & 16 deletions src/models.py

This file was deleted.

30 changes: 3 additions & 27 deletions src/services.py
Original file line number Diff line number Diff line change
@@ -1,38 +1,14 @@
import logging
from email import encoders
from email.mime.base import MIMEBase
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from email.utils import COMMASPACE, formatdate
from src.adapters import EmailMIMEAdapter

from src.clients import BaseEmailClient
from src.models import EmailMime


class EmailService:
def __init__(self, client: BaseEmailClient):
self.__client = client

def _mount_email(self, email: EmailMime) -> MIMEMultipart:
mime = MIMEMultipart()
mime["From"] = f"No Reply <{email.sender}>"
mime["To"] = COMMASPACE.join(email.to)
mime["Cc"] = COMMASPACE.join(email.cc)
mime["Bcc"] = COMMASPACE.join(email.bcc)
mime["Date"] = formatdate(localtime=True)
mime["Subject"] = email.subject
mime.attach(MIMEText(email.body))

if email.attatchment:
part = MIMEBase("application", "octet-stream")
part.set_payload(email.attatchment)
encoders.encode_base64(part)
mime.attach(part)

return mime

def send_email(self, email: EmailMime) -> None:
def send_email(self, email: EmailMIMEAdapter) -> None:
logging.debug(f"Send e-mail to {email.recipients}")
mime = self._mount_email(email)
with self.__client.smtp() as smtp:
smtp.send_mail(email.recipients, mime.as_string())
smtp.send_email(email.recipients, email.as_string())
47 changes: 47 additions & 0 deletions tests/test_adapters.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
from src.adapters import EmailMIMEAdapter

SENDER_MOCK = "[email protected]"
SUBJECT_MOCK = "Email Title"
BODY_MOCK = "Email Body"
TO_MOCK = ["[email protected]"]
CC_MOCK = ["[email protected]"]
BCC_MOCK = ["[email protected]"]
ATTATCHMENTS_MOCK = [(b"", "filename.txt")]


class TestEmailMIMEAdapter:
def test_initialization(self):
adapter = EmailMIMEAdapter(
sender=SENDER_MOCK,
subject=SUBJECT_MOCK,
body=BODY_MOCK,
to=TO_MOCK,
cc=CC_MOCK,
bcc=BCC_MOCK,
attatchments=ATTATCHMENTS_MOCK,
)

assert adapter.sender == SENDER_MOCK
assert adapter.subject == SUBJECT_MOCK
assert adapter.body == BODY_MOCK
assert adapter.to == TO_MOCK
assert adapter.cc == CC_MOCK
assert adapter.bcc == BCC_MOCK
assert adapter.attatchments == ATTATCHMENTS_MOCK
assert isinstance(adapter.as_string(), str)
assert adapter.recipients == TO_MOCK + CC_MOCK + BCC_MOCK

def test_initialization_with_name_sender(self):
sender = ("Example", "[email protected]")
adapter = EmailMIMEAdapter(
sender=sender,
subject=SUBJECT_MOCK,
body=BODY_MOCK,
to=TO_MOCK,
cc=CC_MOCK,
bcc=BCC_MOCK,
attatchments=ATTATCHMENTS_MOCK,
)

assert sender[0] in adapter.sender
assert sender[1] in adapter.sender
27 changes: 16 additions & 11 deletions tests/test_clients.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@
PORT_MOCK = "8888"
ACCOUNT_MOCK = "[email protected]"
PASSWORD_MOCK = "password1234"
TO_MOCK = ['[email protected]']
MESSAGE_MOCK = 'EMAIL MESSAGE'
TO_MOCK = ["[email protected]"]
MESSAGE_MOCK = "EMAIL MESSAGE"


class TestBaseEmailClient:
def setup_method(self):
Expand All @@ -24,7 +25,7 @@ def test_initialization(self):
assert self.client._port == PORT_MOCK
assert self.client._account == ACCOUNT_MOCK
assert self.client._password == PASSWORD_MOCK
assert self.client._client == None
assert self.client._client is None

@mock.patch("src.clients.BaseEmailClient._client")
@mock.patch("src.clients.BaseEmailClient.__enter__")
Expand All @@ -48,7 +49,7 @@ def test_initialization(self):
assert self.client._port == PortSMTP.GMAIL.value
assert self.client._account == ACCOUNT_MOCK
assert self.client._password == PASSWORD_MOCK
assert self.client._client == None
assert self.client._client is None

@mock.patch("src.clients.smtplib.SMTP_SSL")
def test_smtp(self, smtp_mock):
Expand All @@ -62,13 +63,15 @@ def test_smtp(self, smtp_mock):
@mock.patch("src.clients.GmailClient._client")
def test_send_mail(self, client_mock):
client_mock.sendmail.return_value = None
result = self.client.send_mail(
result = self.client.send_email(
to_addrs=TO_MOCK,
message=MESSAGE_MOCK,
)

assert result == None
assert client_mock.sendmail.called_once_with(ACCOUNT_MOCK, TO_MOCK, MESSAGE_MOCK)
assert result is None
assert client_mock.sendmail.called_once_with(
ACCOUNT_MOCK, TO_MOCK, MESSAGE_MOCK
)


class TestOutlookClient:
Expand All @@ -80,7 +83,7 @@ def test_initialization(self):
assert self.client._port == PortSMTP.OUTLOOK.value
assert self.client._account == ACCOUNT_MOCK
assert self.client._password == PASSWORD_MOCK
assert self.client._client == None
assert self.client._client is None

@mock.patch("src.clients.smtplib.SMTP")
def test_smtp(self, smtp_mock):
Expand All @@ -96,10 +99,12 @@ def test_smtp(self, smtp_mock):
@mock.patch("src.clients.OutlookClient._client")
def test_send_mail(self, client_mock):
client_mock.sendmail.return_value = None
result = self.client.send_mail(
result = self.client.send_email(
to_addrs=TO_MOCK,
message=MESSAGE_MOCK,
)

assert result == None
assert client_mock.sendmail.called_once_with(ACCOUNT_MOCK, TO_MOCK, MESSAGE_MOCK)
assert result is None
assert client_mock.sendmail.called_once_with(
ACCOUNT_MOCK, TO_MOCK, MESSAGE_MOCK
)
27 changes: 8 additions & 19 deletions tests/test_services.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,15 @@
from unittest import mock
from src.models import EmailMime
from src.services import EmailService


class TestEmailService:

def setup_method(self):
pass


@mock.patch("src.services.EmailService._mount_email")
def test_send_email(self, mount_email):

def test_send_email(self):
mime_obj = mock.Mock()
mime_obj.as_string.return_value = 'EMAIL MESSAGE'
mount_email.return_value = mime_obj
mime_obj.as_string.return_value = "EMAIL MESSAGE"
mime_obj.recipients = ["[email protected]"]

client_obj = mock.MagicMock()
client_obj.__enter__.return_value = client_obj
Expand All @@ -24,18 +19,12 @@ def test_send_email(self, mount_email):

service = EmailService(client_obj)

email_mime = EmailMime(
sender='[email protected]',
subject='TESTE',
body='BODY',
to=['[email protected]'],
cc=['[email protected]'],
bcc=['[email protected]'],
)
result = service.send_email(email_mime)
result = service.send_email(mime_obj)

assert client_obj.__enter__.called_once
assert client_obj.__exit__.called_once
assert client_obj.smtp.called_once
assert client_obj.send_mail.called_once_with(email_mime.recipients, mime_obj.as_string.return_value)
assert result == None
assert client_obj.send_mail.called_once_with(
mime_obj.recipients, mime_obj.as_string.return_value
)
assert result is None

0 comments on commit be32292

Please sign in to comment.