diff --git a/notify/mail.py b/notify/mail.py index fad2921..cb88116 100644 --- a/notify/mail.py +++ b/notify/mail.py @@ -1,3 +1,4 @@ +import asyncio import base64 import logging import os @@ -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 @@ -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) @@ -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 diff --git a/notify/msgraph.py b/notify/msgraph.py index 5f0547e..5c14f39 100644 --- a/notify/msgraph.py +++ b/notify/msgraph.py @@ -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"): @@ -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"] ) diff --git a/notify/tests/test_email.py b/notify/tests/test_email.py index 933359c..be48308 100644 --- a/notify/tests/test_email.py +++ b/notify/tests/test_email.py @@ -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 @@ -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) @@ -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) @@ -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) @@ -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(): @@ -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(): @@ -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) @@ -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 = "wrong@zypp.io" - response = mail.send_email() - assert response.status_code == 404 + with pytest.raises(ODataError): + mail.send_email() diff --git a/requirements.txt b/requirements.txt index 995f751..af27be6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -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 diff --git a/setup.cfg b/setup.cfg index d62afac..63c74b6 100644 --- a/setup.cfg +++ b/setup.cfg @@ -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