diff --git a/frappe/email/doctype/email_account/email_account.json b/frappe/email/doctype/email_account/email_account.json index 08d661314d..19fa3985d7 100644 --- a/frappe/email/doctype/email_account/email_account.json +++ b/frappe/email/doctype/email_account/email_account.json @@ -78,6 +78,7 @@ "smtp_port", "column_break_38", "no_smtp_authentication", + "dsn_notify_type", "always_bcc", "signature_section", "add_signature", @@ -754,13 +755,20 @@ "ignore_xss_filter": 1, "label": "Reply-To Addresses", "options": "Reply To Address" + }, + { + "description": "Select which delivery events should trigger a delivery status notification (DSN) from the SMTP server.", + "fieldname": "dsn_notify_type", + "fieldtype": "Select", + "label": "Delivery Status Notification Type", + "options": "\nSUCCESS\nFAILURE\nDELAY\nSUCCESS,FAILURE\nSUCCESS,FAILURE,DELAY\nNEVER" } ], "icon": "fa fa-inbox", "index_web_pages_for_search": 1, "links": [], "make_attachments_public": 1, - "modified": "2026-02-06 11:39:39.412130", + "modified": "2026-02-11 16:18:15.572240", "modified_by": "Administrator", "module": "Email", "name": "Email Account", diff --git a/frappe/email/doctype/email_account/email_account.py b/frappe/email/doctype/email_account/email_account.py index 73ca59b640..f56a7d2cd3 100755 --- a/frappe/email/doctype/email_account/email_account.py +++ b/frappe/email/doctype/email_account/email_account.py @@ -84,6 +84,9 @@ class EmailAccount(Document): default_incoming: DF.Check default_outgoing: DF.Check domain: DF.Link | None + dsn_notify_type: DF.Literal[ + "SUCCESS", "FAILURE", "DELAY", "SUCCESS,FAILURE", "SUCCESS,FAILURE,DELAY", "NEVER" + ] email_account_name: DF.Data | None email_id: DF.Data email_server: DF.Data | None @@ -220,7 +223,8 @@ class EmailAccount(Document): frappe.throw(_("SMTP Server is required")) self.flags.validate_smtp_connection = True - self.get_smtp_server().session + session = self.get_smtp_server().session + self.validate_dsn(session) del self._smtp_server_instance def validate_reply_to_addresses(self) -> None: @@ -229,6 +233,18 @@ class EmailAccount(Document): frappe.throw(_("Reply To email is required")) validate_email_address(reply_to.email, True) + def validate_dsn(self, smtp_session) -> None: + """Validate if the configured SMTP server supports DSN (Delivery Status Notification).""" + + if not self.dsn_notify_type: + return + + if not smtp_session.has_extn("DSN"): + self.dsn_notify_type = None + frappe.msgprint( + _("The configured SMTP server does not support DSN (Delivery Status Notification).") + ) + def before_save(self): messages = [] as_list = 1 diff --git a/frappe/email/doctype/email_queue/email_queue.py b/frappe/email/doctype/email_queue/email_queue.py index 41983bd5ed..0f021cf055 100644 --- a/frappe/email/doctype/email_queue/email_queue.py +++ b/frappe/email/doctype/email_queue/email_queue.py @@ -196,10 +196,20 @@ class EmailQueue(Document): is_newsletter=is_newsletter, ) else: + mail_options = [] + rcpt_options = [] + + if ctx.smtp_server.session.has_extn("DSN"): + if dsn_notify_type := ctx.email_account_doc.dsn_notify_type: + mail_options = ["RET=FULL", f"ENVID={self.name}"] + rcpt_options = [f"NOTIFY={dsn_notify_type}"] + ctx.smtp_server.session.sendmail( from_addr=self.sender, to_addrs=recipient.recipient, msg=message.decode("utf-8").encode(), + mail_options=mail_options, + rcpt_options=rcpt_options, ) ctx.update_recipient_status_to_sent(recipient) diff --git a/frappe/email/smtp.py b/frappe/email/smtp.py index 1c782979e2..997cb87758 100644 --- a/frappe/email/smtp.py +++ b/frappe/email/smtp.py @@ -92,6 +92,9 @@ class SMTPServer: if res[0] != 235: frappe.msgprint(res[1], raise_exception=frappe.OutgoingEmailError) + # Re-issue EHLO after AUTH to refresh server capabilities + _session.ehlo() + self._session = _session self._enqueue_connection_closure() return self._session