| Name | Description | |------------------------------|--------------------------------------------------------------------------------| | Microsoft.CreateDraftEmail | Compose a new draft email in Outlook | | Microsoft.UpdateDraftEmail | Update an existing draft email in Outlook | | Microsoft.CreateAndSendEmail | Create and immediately send a new email in Outlook to the specified recipients | | Microsoft.SendDraftEmail | Send an existing draft email in Outlook | | Microsoft.ReplyToEmail | Reply only to the sender of an existing email in Outlook | | Microsoft.ReplyAllToEmail | Reply to all recipients of an existing email in Outlook | | Microsoft.ListEmails | List emails in the user's mailbox across all folders | | Microsoft.ListEmailsInFolder | List the user's emails in the specified folder |
218 lines
8.8 KiB
Python
218 lines
8.8 KiB
Python
import re
|
|
from dataclasses import dataclass, field
|
|
from typing import Any
|
|
|
|
from bs4 import BeautifulSoup
|
|
from msgraph.generated.models.body_type import BodyType
|
|
from msgraph.generated.models.email_address import EmailAddress
|
|
from msgraph.generated.models.item_body import ItemBody
|
|
from msgraph.generated.models.message import Message as GraphMessage
|
|
from msgraph.generated.models.recipient import Recipient as GraphRecipient
|
|
|
|
|
|
@dataclass
|
|
class Recipient:
|
|
"""A recipient of an email message."""
|
|
|
|
email_address: str = ""
|
|
name: str = ""
|
|
|
|
@classmethod
|
|
def from_sdk(cls, recipient: GraphRecipient) -> "Recipient":
|
|
"""Convert a Microsoft Graph SDK Recipient object to a Recipient dataclass."""
|
|
address = (
|
|
recipient.email_address.address
|
|
if recipient and recipient.email_address and recipient.email_address.address
|
|
else ""
|
|
)
|
|
name = (
|
|
recipient.email_address.name
|
|
if recipient and recipient.email_address and recipient.email_address.name
|
|
else ""
|
|
)
|
|
return cls(email_address=address, name=name)
|
|
|
|
def to_dict(self) -> dict[str, str]:
|
|
return {"email_address": self.email_address, "name": self.name}
|
|
|
|
def to_sdk(self) -> GraphRecipient:
|
|
"""Converts the Recipient dataclass to a Microsoft Graph SDK Recipient object."""
|
|
recipient = GraphRecipient()
|
|
email_address = EmailAddress()
|
|
email_address.address = self.email_address
|
|
email_address.name = self.name
|
|
recipient.email_address = email_address
|
|
return recipient
|
|
|
|
|
|
@dataclass
|
|
class Message:
|
|
"""An email message in Outlook."""
|
|
|
|
bcc_recipients: list[Recipient] = field(default_factory=list)
|
|
cc_recipients: list[Recipient] = field(default_factory=list)
|
|
reply_to: list[Recipient] = field(default_factory=list)
|
|
to_recipients: list[Recipient] = field(default_factory=list)
|
|
from_: Recipient = field(default_factory=Recipient)
|
|
subject: str = ""
|
|
body: str = ""
|
|
conversation_id: str = ""
|
|
conversation_index: str = ""
|
|
flag: dict[str, str] = field(default_factory=dict)
|
|
has_attachments: bool = False
|
|
importance: str = ""
|
|
is_read: bool = False
|
|
received_date_time: str = ""
|
|
web_link: str = ""
|
|
is_draft: bool = True
|
|
message_id: str = "" # The unique identifier of the email message. Read-only.
|
|
|
|
@staticmethod
|
|
def _safe_str(value: Any) -> str:
|
|
if not value:
|
|
return ""
|
|
if isinstance(value, bytes | bytearray):
|
|
return value.decode("utf-8", errors="ignore")
|
|
return str(value)
|
|
|
|
@staticmethod
|
|
def _safe_bool(value: Any) -> bool:
|
|
return bool(value)
|
|
|
|
@staticmethod
|
|
def _parse_body(mime: str) -> str:
|
|
if not mime:
|
|
return ""
|
|
soup = BeautifulSoup(mime, "html.parser")
|
|
text = soup.get_text(separator=" ")
|
|
# Replace multiple newlines with a single newline
|
|
text = re.sub(r"\n+", "\n", text)
|
|
# Replace multiple spaces with a single space
|
|
text = re.sub(r"\s+", " ", text)
|
|
# Remove leading/trailing whitespace from each line
|
|
text = "\n".join(line.strip() for line in text.split("\n"))
|
|
|
|
return text
|
|
|
|
@staticmethod
|
|
def _parse_importance(value: Any) -> str:
|
|
return value.value if getattr(value, "value", None) else ""
|
|
|
|
@staticmethod
|
|
def _parse_flag(flag: Any) -> dict[str, str]:
|
|
if not flag:
|
|
return {"flag_status": "", "due_date_time": ""}
|
|
status = flag.flag_status.value if getattr(flag, "flag_status", None) else ""
|
|
due = ""
|
|
if getattr(flag, "due_date_time", None) and getattr(flag.due_date_time, "date_time", None):
|
|
due = Message._safe_str(flag.due_date_time.date_time)
|
|
return {"flag_status": status, "due_date_time": due}
|
|
|
|
@classmethod
|
|
def from_sdk(cls, msg: GraphMessage) -> "Message":
|
|
"""Convert a Microsoft Graph SDK Message object to a Message dataclass."""
|
|
text = cls._parse_body(msg.body.content if msg.body and msg.body.content else "")
|
|
return cls(
|
|
bcc_recipients=[
|
|
Recipient.from_sdk(recipient) for recipient in msg.bcc_recipients or []
|
|
],
|
|
cc_recipients=[Recipient.from_sdk(recipient) for recipient in msg.cc_recipients or []],
|
|
reply_to=[Recipient.from_sdk(recipient) for recipient in msg.reply_to or []],
|
|
to_recipients=[Recipient.from_sdk(recipient) for recipient in msg.to_recipients or []],
|
|
from_=Recipient.from_sdk(msg.from_) if msg.from_ else Recipient(),
|
|
subject=cls._safe_str(msg.subject),
|
|
body=text,
|
|
conversation_id=cls._safe_str(msg.conversation_id),
|
|
conversation_index=(
|
|
msg.conversation_index.decode("utf-8", errors="ignore")
|
|
if isinstance(msg.conversation_index, bytes | bytearray)
|
|
else cls._safe_str(msg.conversation_index)
|
|
),
|
|
flag=cls._parse_flag(msg.flag),
|
|
has_attachments=cls._safe_bool(msg.has_attachments),
|
|
importance=cls._parse_importance(msg.importance),
|
|
is_read=cls._safe_bool(msg.is_read),
|
|
received_date_time=(
|
|
msg.received_date_time.isoformat() if msg.received_date_time else ""
|
|
),
|
|
web_link=cls._safe_str(msg.web_link),
|
|
is_draft=cls._safe_bool(msg.is_draft),
|
|
message_id=cls._safe_str(msg.id),
|
|
)
|
|
|
|
def to_sdk(self) -> GraphMessage:
|
|
"""Converts the Message dataclass to a Microsoft Graph SDK Message object."""
|
|
sdk_msg = GraphMessage()
|
|
sdk_msg.subject = self.subject
|
|
body_obj = ItemBody()
|
|
body_obj.content = self.body
|
|
body_obj.content_type = BodyType.Text
|
|
sdk_msg.body = body_obj
|
|
sdk_msg.is_draft = self.is_draft
|
|
sdk_msg.to_recipients = [r.to_sdk() for r in self.to_recipients]
|
|
sdk_msg.cc_recipients = [r.to_sdk() for r in self.cc_recipients]
|
|
sdk_msg.bcc_recipients = [r.to_sdk() for r in self.bcc_recipients]
|
|
sdk_msg.reply_to = [r.to_sdk() for r in self.reply_to]
|
|
|
|
return sdk_msg
|
|
|
|
def to_dict(self) -> dict[str, Any]:
|
|
"""Converts the Message dataclass to a dictionary."""
|
|
return {
|
|
"bcc_recipients": [recipient.to_dict() for recipient in self.bcc_recipients],
|
|
"cc_recipients": [recipient.to_dict() for recipient in self.cc_recipients],
|
|
"reply_to": [recipient.to_dict() for recipient in self.reply_to],
|
|
"to_recipients": [recipient.to_dict() for recipient in self.to_recipients],
|
|
"from": self.from_.to_dict(),
|
|
"subject": self.subject,
|
|
"body": self.body,
|
|
"conversation_id": self.conversation_id,
|
|
"conversation_index": self.conversation_index,
|
|
"flag": self.flag,
|
|
"has_attachments": self.has_attachments,
|
|
"importance": self.importance,
|
|
"is_read": self.is_read,
|
|
"received_date_time": self.received_date_time,
|
|
"web_link": self.web_link,
|
|
"is_draft": self.is_draft,
|
|
"message_id": self.message_id,
|
|
}
|
|
|
|
def update_recipient_lists(
|
|
self,
|
|
to_add: list[str] | None = None,
|
|
to_remove: list[str] | None = None,
|
|
cc_add: list[str] | None = None,
|
|
cc_remove: list[str] | None = None,
|
|
bcc_add: list[str] | None = None,
|
|
bcc_remove: list[str] | None = None,
|
|
) -> None:
|
|
"""Update each recipient list of the message.
|
|
|
|
This function updates the recipient lists of the message by first adding new recipients
|
|
and then removing existing recipients. Therefore, if an email address is both
|
|
added and removed, then it will not be included in the returned list.
|
|
"""
|
|
for attr, add_emails_input, remove_emails_input in (
|
|
("to_recipients", to_add, to_remove),
|
|
("cc_recipients", cc_add, cc_remove),
|
|
("bcc_recipients", bcc_add, bcc_remove),
|
|
):
|
|
current_recipients = getattr(self, attr) or []
|
|
# Add recipients
|
|
existing_emails = {r.email_address.lower() for r in current_recipients}
|
|
new_additions = [
|
|
Recipient(email_address=email)
|
|
for email in (add_emails_input or [])
|
|
if email.lower() not in existing_emails
|
|
]
|
|
# Remove recipients
|
|
updated_list = current_recipients + new_additions
|
|
remove_emails = {email.lower() for email in (remove_emails_input or [])}
|
|
updated_list = [
|
|
recipient
|
|
for recipient in updated_list
|
|
if recipient.email_address.lower() not in remove_emails
|
|
]
|
|
# Update the message's attribute with the new list
|
|
setattr(self, attr, updated_list)
|