Skip to content

Commit

Permalink
Merge pull request #25 from zypp-io/use-msgrpah-sdk-for-mail
Browse files Browse the repository at this point in the history
Use msgraph SDK for mail
  • Loading branch information
TimvdHeijden authored Sep 13, 2024
2 parents 3fcfec3 + 32c4889 commit aaa89ed
Show file tree
Hide file tree
Showing 5 changed files with 91 additions and 50 deletions.
91 changes: 63 additions & 28 deletions notify/mail.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import asyncio
import base64
import logging
import os
Expand All @@ -6,6 +7,16 @@
import pandas as pd

from notify.msgraph import Graph

from msgraph.generated.models.body_type import BodyType
from msgraph.generated.models.message import Message
from msgraph.generated.models.email_address import EmailAddress
from msgraph.generated.models.item_body import ItemBody
from msgraph.generated.models.recipient import Recipient
from msgraph.generated.users.item.send_mail.send_mail_post_request_body import SendMailPostRequestBody
from msgraph.generated.models.o_data_errors.o_data_error import ODataError

from msgraph.generated.models.file_attachment import FileAttachment
from notify.utils import check_environment_variables, dataframe_to_html


Expand Down Expand Up @@ -72,24 +83,37 @@ def send_email(self):
-------
response: requests.Response
"""
endpoint = f"https://graph.microsoft.com/v1.0/users/{self.sender}/sendMail"

msg = {
"Message": {
"Subject": self.subject,
"Body": {"ContentType": "HTML", "Content": self.message},
"ToRecipients": [{"EmailAddress": {"Address": to.strip()}} for to in self.to.split(",")],
},
"SaveToSentItems": "true",
}

sender = EmailAddress(address=self.sender)
sender_recipient = Recipient(email_address=sender)
recipients = []
tos = self.to.split(",")
for to in tos:
to_email = EmailAddress(address=to)
to_recipient = Recipient(email_address=to_email)
recipients.append(to_recipient)
message = Message(
to_recipients=recipients, subject=self.subject, sender=sender_recipient, from_=sender_recipient
)
# CC if present
if self.cc:
msg["Message"]["CcRecipients"] = [{"EmailAddress": {"Address": cc.strip()}} for cc in self.cc.split(",")]
ccs = self.cc.split(",")
cc_recipients = []
for cc in ccs:
cc_email = EmailAddress(address=cc)
cc_recipient = Recipient(email_address=cc_email)
cc_recipients.append(cc_recipient)
message.cc_recipients = cc_recipients
# BCC if present
if self.bcc:
msg["Message"]["BccRecipients"] = [
{"EmailAddress": {"Address": bcc.strip()}} for bcc in self.bcc.split(",")
]

bccs = self.bcc.split(",")
bcc_recipients = []
for bcc in bccs:
bcc_email = EmailAddress(address=bcc)
bcc_recipient = Recipient(email_address=bcc_email)
bcc_recipients.append(bcc_recipient)
message.bcc_recipients = bcc_recipients
email_body = ItemBody(content=self.message, content_type=BodyType.Html)
# add html table (if table less than 30 records)
if self.df.shape[0] in range(1, 31):
html_table = dataframe_to_html(df=self.df)
Expand All @@ -99,22 +123,33 @@ def send_email(self):
else:
html_table = "" # no data in dataframe (0 records)

msg["Message"]["Body"]["Content"] += html_table

email_body.content += html_table
message.body = email_body
if self.files:
# There might be a more safe way to check if a string is an url, but for our purposes, this suffices.
attachments = list()
for name, path in self.files.items():
content = self.read_file_content(path)
attachments.append(
{
"@odata.type": "#microsoft.graph.fileAttachment",
"ContentBytes": content.decode("utf-8"),
"Name": name,
}
attachment = FileAttachment(
odata_type="#microsoft.graph.fileAttachment",
name=name,
content_bytes=base64.urlsafe_b64decode(content),
)

msg["Message"]["Attachments"] = attachments

response = self.graph.app_client.post(endpoint, json=msg)
return response
attachments.append(attachment)
message.attachments = attachments
request_body = SendMailPostRequestBody(message=message, save_to_sent_items=True)
success = asyncio.run(self.get_mail_response(request_body))
return success

async def get_mail_response(self, request_body):
try:
await self.graph.app_client.users.by_user_id(self.sender).send_mail.post(request_body)
except ODataError as e:
# Handle Microsoft Graph API errors
raise ODataError(f"Error sending email: {e.message}")

except Exception as e:
# Catch any other exceptions
raise Exception(f"An unexpected error occurred: {str(e)}")

return True
10 changes: 5 additions & 5 deletions notify/msgraph.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import os

from azure.identity import ClientSecretCredential
from msgraph.core import GraphClient
from msgraph import GraphServiceClient


class Graph:
user_client: GraphClient
user_client: GraphServiceClient
client_credential: ClientSecretCredential
app_client: GraphClient
app_client: GraphServiceClient

def ensure_graph_for_app_only_auth(self):
if not hasattr(self, "client_credential"):
Expand All @@ -18,6 +18,6 @@ def ensure_graph_for_app_only_auth(self):
self.client_credential = ClientSecretCredential(tenant_id, client_id, client_secret)

if not hasattr(self, "app_client"):
self.app_client = GraphClient(
credential=self.client_credential, scopes=["https://graph.microsoft.com/.default"]
self.app_client = GraphServiceClient(
credentials=self.client_credential, scopes=["https://graph.microsoft.com/.default"]
)
30 changes: 17 additions & 13 deletions notify/tests/test_email.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import os
import time

import pytest
from keyvault import secrets_to_environment

from msgraph.generated.models.o_data_errors.o_data_error import ODataError
from notify import NotifyMail, format_numbers
from notify.tests import PDF_STORAGE_LINK, import_sample_dfs

Expand All @@ -15,7 +14,7 @@ def test_send_single_email():
response = NotifyMail(
to=f"{os.environ.get('TEST_EMAIL_1')}", subject="Test Notify single email", message=message
).send_email()
assert response.status_code == 202
assert response
time.sleep(2)


Expand All @@ -29,7 +28,7 @@ def test_send_email_with_table():
message=message,
df=df,
).send_email()
assert response.status_code == 202
assert response
time.sleep(2)


Expand All @@ -43,20 +42,25 @@ def test_send_email_with_formatted_table():
message=message,
df=df,
).send_email()
assert response.status_code == 202
assert response
time.sleep(2)


@pytest.mark.parametrize("sep", [",", ";"])
def test_send_multiple_emails(sep: str):
message = "This is a test from notify"
to = sep.join([os.environ.get("TEST_EMAIL_1"), os.environ.get("TEST_EMAIL_2")])
to = sep.join(
[
os.environ.get("TEST_EMAIL_1"),
os.environ.get("TEST_EMAIL_2"),
]
)
response = NotifyMail(
to=to,
subject="Test Notify multiple emails",
message=message,
).send_email()
assert response.status_code == 202
assert response
time.sleep(2)


Expand All @@ -71,7 +75,7 @@ def test_send_file():
message=message,
files={file_name: file_path},
).send_email()
assert response.status_code == 202
assert response


def test_send_file_from_storage():
Expand All @@ -83,7 +87,7 @@ def test_send_file_from_storage():
message=message,
files={"testpdf.pdf": PDF_STORAGE_LINK},
).send_email()
assert response.status_code == 202
assert response


def test_send_cc():
Expand All @@ -94,7 +98,7 @@ def test_send_cc():
subject="Test Notify cc emails",
message=message,
).send_email()
assert response.status_code == 202
assert response
time.sleep(2)


Expand All @@ -106,12 +110,12 @@ def test_send_bcc():
subject="Test Notify bcc emails",
message=message,
).send_email()
assert response.status_code == 202
assert response
time.sleep(2)


def test_wrong_user():
mail = NotifyMail(to=os.environ.get("TEST_EMAIL_1"), subject="Test wrong sender", message="Test")
mail.sender = "[email protected]"
response = mail.send_email()
assert response.status_code == 404
with pytest.raises(ODataError):
mail.send_email()
5 changes: 3 additions & 2 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
azure-identity==1.7.1
azure-identity>=1.16.1
Babel>=2.14.0
msgraph-core==0.2.2
msgraph-core>=1.1.3
msgraph-sdk~=1.5.4
pandas>=2.2.2
tabulate>=0.8.10
5 changes: 3 additions & 2 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,9 @@ classifiers =
packages = notify
python_requires = >=3.9
install_requires =
azure-identity==1.7.1
azure-identity>=1.16.1
Babel>=2.14.0
msgraph-core==0.2.2
msgraph-core>=1.1.3
pandas>=2.2.2
tabulate>=0.8.10
msgraph-sdk~=1.5.4

0 comments on commit aaa89ed

Please sign in to comment.