132 lines
3.9 KiB
Python
132 lines
3.9 KiB
Python
from datetime import datetime
|
|
from typing import Any
|
|
from urllib.parse import urljoin
|
|
from zoneinfo import ZoneInfo
|
|
|
|
import frappe
|
|
from frappe import _
|
|
from frappe.frappeclient import FrappeClient, FrappeOAuth2Client
|
|
from frappe.utils import convert_utc_to_system_timezone, get_datetime, get_system_timezone
|
|
|
|
|
|
class FrappeMail:
|
|
"""Class to interact with the Frappe Mail API."""
|
|
|
|
def __init__(
|
|
self,
|
|
site: str,
|
|
mailbox: str,
|
|
api_key: str | None = None,
|
|
api_secret: str | None = None,
|
|
access_token: str | None = None,
|
|
) -> None:
|
|
self.site = site
|
|
self.mailbox = mailbox
|
|
self.api_key = api_key
|
|
self.api_secret = api_secret
|
|
self.access_token = access_token
|
|
self.client = self.get_client(
|
|
self.site, self.mailbox, self.api_key, self.api_secret, self.access_token
|
|
)
|
|
|
|
@staticmethod
|
|
def get_client(
|
|
site: str,
|
|
mailbox: str,
|
|
api_key: str | None = None,
|
|
api_secret: str | None = None,
|
|
access_token: str | None = None,
|
|
) -> FrappeClient | FrappeOAuth2Client:
|
|
"""Returns a FrappeClient or FrappeOAuth2Client instance."""
|
|
|
|
if hasattr(frappe.local, "frappe_mail_clients"):
|
|
if client := frappe.local.frappe_mail_clients.get(mailbox):
|
|
return client
|
|
else:
|
|
frappe.local.frappe_mail_clients = {}
|
|
|
|
client = (
|
|
FrappeOAuth2Client(url=site, access_token=access_token)
|
|
if access_token
|
|
else FrappeClient(url=site, api_key=api_key, api_secret=api_secret)
|
|
)
|
|
frappe.local.frappe_mail_clients[mailbox] = client
|
|
|
|
return client
|
|
|
|
def request(
|
|
self,
|
|
method: str,
|
|
endpoint: str,
|
|
params: dict | None = None,
|
|
data: dict | None = None,
|
|
json: dict | None = None,
|
|
files: dict | None = None,
|
|
headers: dict[str, str] | None = None,
|
|
timeout: int | tuple[int, int] = (60, 120),
|
|
) -> Any | None:
|
|
"""Makes a request to the Frappe Mail API."""
|
|
|
|
url = urljoin(self.client.url, endpoint)
|
|
|
|
headers = headers or {}
|
|
headers.update(self.client.headers)
|
|
|
|
if files:
|
|
headers.pop("content-type", None)
|
|
|
|
response = self.client.session.request(
|
|
method=method,
|
|
url=url,
|
|
params=params,
|
|
data=data,
|
|
json=json,
|
|
files=files,
|
|
headers=headers,
|
|
timeout=timeout,
|
|
)
|
|
|
|
return self.client.post_process(response)
|
|
|
|
def validate(self, for_outbound: bool = False, for_inbound: bool = False) -> None:
|
|
"""Validates the mailbox for inbound and outbound emails."""
|
|
|
|
endpoint = "/api/method/mail_client.api.auth.validate"
|
|
data = {"mailbox": self.mailbox, "for_outbound": for_outbound, "for_inbound": for_inbound}
|
|
self.request("POST", endpoint=endpoint, data=data)
|
|
|
|
def send_raw(
|
|
self, sender: str, recipients: str | list, message: str | bytes, is_newsletter: bool = False
|
|
) -> None:
|
|
"""Sends an email using the Frappe Mail API."""
|
|
|
|
endpoint = "/api/method/mail_client.api.outbound.send_raw"
|
|
data = {"from_": sender, "to": recipients, "is_newsletter": is_newsletter}
|
|
self.request("POST", endpoint=endpoint, data=data, files={"raw_message": message})
|
|
|
|
def pull_raw(self, limit: int = 50, last_synced_at: str | None = None) -> dict[str, str | list[str]]:
|
|
"""Pulls emails from the mailbox using the Frappe Mail API."""
|
|
|
|
endpoint = "/api/method/mail_client.api.inbound.pull_raw"
|
|
if last_synced_at:
|
|
last_synced_at = add_or_update_tzinfo(last_synced_at)
|
|
|
|
data = {"mailbox": self.mailbox, "limit": limit, "last_synced_at": last_synced_at}
|
|
headers = {"X-Site": frappe.utils.get_url()}
|
|
response = self.request("GET", endpoint=endpoint, data=data, headers=headers)
|
|
last_synced_at = convert_utc_to_system_timezone(get_datetime(response["last_synced_at"]))
|
|
|
|
return {"latest_messages": response["mails"], "last_synced_at": last_synced_at}
|
|
|
|
|
|
def add_or_update_tzinfo(date_time: datetime | str, timezone: str | None = None) -> str:
|
|
"""Adds or updates timezone to the datetime."""
|
|
date_time = get_datetime(date_time)
|
|
target_tz = ZoneInfo(timezone or get_system_timezone())
|
|
|
|
if date_time.tzinfo is None:
|
|
date_time = date_time.replace(tzinfo=target_tz)
|
|
else:
|
|
date_time = date_time.astimezone(target_tz)
|
|
|
|
return str(date_time)
|