feat: Send Mails via Frappe Mail

This commit is contained in:
s-aga-r 2024-06-11 17:54:50 +05:30
parent c01eb68b83
commit 527ac29c2e
3 changed files with 130 additions and 19 deletions

View file

@ -2,7 +2,7 @@
"actions": [],
"allow_rename": 1,
"autoname": "field:email_account_name",
"creation": "2014-09-11 12:04:34.163728",
"creation": "2024-06-11 16:39:01.323289",
"doctype": "DocType",
"document_type": "Setup",
"engine": "InnoDB",
@ -15,6 +15,11 @@
"column_break_3",
"domain",
"service",
"frappe_mail_site",
"authentication_section",
"api_key",
"column_break_ghqa",
"api_secret",
"authentication_column",
"auth_method",
"authorize_api_access",
@ -50,19 +55,21 @@
"notify_if_unreplied",
"unreplied_for_mins",
"send_notification_to",
"outgoing_smtp_tab",
"outgoing_mail_settings",
"column_break_bidn",
"use_tls",
"use_ssl_for_outgoing",
"smtp_server",
"smtp_port",
"column_break_38",
"outgoing_tab",
"section_break_nesl",
"column_break_y6hx",
"column_break_h5pd",
"default_outgoing",
"always_use_account_email_id_as_sender",
"always_use_account_name_as_sender_name",
"send_unsubscribe_message",
"track_email_status",
"outgoing_mail_settings",
"use_tls",
"use_ssl_for_outgoing",
"smtp_server",
"smtp_port",
"column_break_38",
"no_smtp_authentication",
"signature_section",
"add_signature",
@ -289,6 +296,7 @@
"mandatory_depends_on": "notify_if_unreplied"
},
{
"depends_on": "eval: doc.service != \"Frappe Mail\"",
"fieldname": "outgoing_mail_settings",
"fieldtype": "Section Break",
"hide_days": 1,
@ -533,6 +541,7 @@
"label": "Brand Logo"
},
{
"depends_on": "eval: doc.service != \"Frappe Mail\"",
"fieldname": "authentication_column",
"fieldtype": "Section Break",
"label": "Authentication"
@ -624,20 +633,58 @@
"label": "Incoming (POP/IMAP)"
},
{
"depends_on": "enable_outgoing",
"fieldname": "outgoing_smtp_tab",
"fieldtype": "Tab Break",
"label": "Outgoing (SMTP)"
"fieldname": "api_key",
"fieldtype": "Data",
"label": "API Key",
"mandatory_depends_on": "eval: doc.service == \"Frappe Mail\""
},
{
"fieldname": "column_break_bidn",
"fieldname": "api_secret",
"fieldtype": "Password",
"label": "API Secret",
"mandatory_depends_on": "eval: doc.service == \"Frappe Mail\""
},
{
"depends_on": "eval: doc.service == \"Frappe Mail\"",
"fieldname": "authentication_section",
"fieldtype": "Section Break",
"label": "Authentication"
},
{
"fieldname": "column_break_ghqa",
"fieldtype": "Column Break"
},
{
"default": "https://frappemail.com",
"depends_on": "eval: doc.service == \"Frappe Mail\"",
"fieldname": "frappe_mail_site",
"fieldtype": "Data",
"label": "Frappe Mail Site",
"mandatory_depends_on": "eval: doc.service == \"Frappe Mail\""
},
{
"depends_on": "enable_outgoing",
"fieldname": "outgoing_tab",
"fieldtype": "Tab Break",
"label": "Outgoing"
},
{
"fieldname": "section_break_nesl",
"fieldtype": "Section Break"
},
{
"fieldname": "column_break_y6hx",
"fieldtype": "Column Break"
},
{
"fieldname": "column_break_h5pd",
"fieldtype": "Column Break"
}
],
"icon": "fa fa-inbox",
"index_web_pages_for_search": 1,
"links": [],
"modified": "2024-06-10 18:18:08.403133",
"modified": "2024-06-11 16:54:22.255325",
"modified_by": "Administrator",
"module": "Email",
"name": "Email Account",

View file

@ -61,6 +61,8 @@ class EmailAccount(Document):
add_signature: DF.Check
always_use_account_email_id_as_sender: DF.Check
always_use_account_name_as_sender_name: DF.Check
api_key: DF.Data | None
api_secret: DF.Password | None
append_emails_to_sent_folder: DF.Check
append_to: DF.Link | None
ascii_encode_password: DF.Check
@ -84,6 +86,7 @@ class EmailAccount(Document):
enable_incoming: DF.Check
enable_outgoing: DF.Check
footer: DF.TextEditor | None
frappe_mail_site: DF.Data | None
imap_folder: DF.Table[IMAPFolder]
incoming_port: DF.Data | None
initial_sync_count: DF.Literal["100", "250", "500"]
@ -152,7 +155,11 @@ class EmailAccount(Document):
self.awaiting_password = 0
self.password = None
if not frappe.local.flags.in_install and not self.awaiting_password:
if (
not frappe.local.flags.in_install
and not self.awaiting_password
and not self.service == "Frappe Mail"
):
if validate_oauth or self.password or self.smtp_server in ("127.0.0.1", "localhost"):
if self.enable_incoming:
self.get_incoming_server()

View file

@ -7,6 +7,7 @@ import traceback
from contextlib import suppress
from email.parser import Parser
from email.policy import SMTP
from typing import TYPE_CHECKING
import frappe
from frappe import _, safe_encode, task
@ -16,6 +17,7 @@ from frappe.email.doctype.email_account.email_account import EmailAccount
from frappe.email.email_body import add_attachment, get_email, get_formatted_html
from frappe.email.queue import get_unsubcribed_url, get_unsubscribe_message
from frappe.email.smtp import SMTPServer
from frappe.frappeclient import FrappeClient
from frappe.model.document import Document
from frappe.query_builder import DocType, Interval
from frappe.query_builder.functions import Now
@ -34,6 +36,9 @@ from frappe.utils import (
from frappe.utils.deprecations import deprecated
from frappe.utils.verified_command import get_signed_params
if TYPE_CHECKING:
from requests import Response
class EmailQueue(Document):
# begin: auto-generated types
@ -168,8 +173,14 @@ class EmailQueue(Document):
message = ctx.build_message(recipient.recipient)
if method := get_hook_method("override_email_send"):
method(self, self.sender, recipient.recipient, message)
else:
if not frappe.flags.in_test or frappe.flags.testing_email:
elif not frappe.flags.in_test or frappe.flags.testing_email:
if ctx.email_account_doc.service == "Frappe Mail":
ctx.frappe_mail.send(
sender=self.sender,
recipients=recipient.recipient,
message=message.decode("utf-8"),
)
else:
ctx.smtp_server.session.sendmail(
from_addr=self.sender,
to_addrs=recipient.recipient,
@ -240,7 +251,10 @@ class SendMailContext:
def fetch_smtp_server(self):
self.email_account_doc = self.queue_doc.get_email_account(raise_error=True)
if not self.smtp_server:
if self.email_account_doc.service == "Frappe Mail":
self.frappe_mail = FrappeMail(self.email_account_doc)
elif not self.smtp_server:
self.smtp_server = self.email_account_doc.get_smtp_server()
def __enter__(self):
@ -802,3 +816,46 @@ class QueueBuilder:
d["recipients"] = self.final_recipients()
return d
class FrappeMail:
def __init__(self, email_account: "EmailAccount") -> None:
self.client = self.get_client(email_account)
self.frappe_mail_site = email_account.frappe_mail_site
@staticmethod
def get_client(email_account: "EmailAccount") -> FrappeClient:
"""Returns FrappeClient object for the given email account."""
if hasattr(frappe.local, "frappe_mail_clients"):
if client := frappe.local.frappe_mail_clients.get(email_account.name):
return client
else:
frappe.local.frappe_mail_clients = {}
url = email_account.frappe_mail_site
api_key = email_account.api_key
api_secret = email_account.get_password("api_secret")
client = FrappeClient(url, api_key=api_key, api_secret=api_secret)
frappe.local.frappe_mail_clients[email_account.name] = client
return client
def request(
self,
method: str,
endpoint: str,
data: dict | None = None,
timeout: int | tuple[int, int] = (60, 120),
) -> "Response":
url = f"{self.frappe_mail_site}/{endpoint}"
response = self.client.session.request(
method=method, url=url, headers=self.client.headers, json=data, timeout=timeout
)
return response
def send(self, sender: str, recipients: str, message: str):
endpoint = "outbound/send-raw"
data = {"from": sender, "to": recipients, "raw_message": message}
self.request("POST", endpoint, data)