feat: configurable Reply-To (#36774)
* feat: configurable `Reply-To` * chore: set `add_reply_to_header` to `1` * fix: Resolve conflicts --------- Co-authored-by: Suraj Shetty <13928957+surajshetty3416@users.noreply.github.com>
This commit is contained in:
parent
199f1f540f
commit
8bfb42deb4
7 changed files with 149 additions and 6 deletions
|
|
@ -67,6 +67,11 @@
|
|||
"send_unsubscribe_message",
|
||||
"add_x_original_from",
|
||||
"track_email_status",
|
||||
"headers_section",
|
||||
"column_break_mcbu",
|
||||
"add_x_original_from",
|
||||
"add_reply_to_header",
|
||||
"reply_to_addresses",
|
||||
"outgoing_mail_settings",
|
||||
"use_tls",
|
||||
"use_ssl_for_outgoing",
|
||||
|
|
@ -84,9 +89,11 @@
|
|||
"set_footer",
|
||||
"footer",
|
||||
"brand_logo",
|
||||
"section_break_jdoz",
|
||||
"uidvalidity",
|
||||
"uidnext",
|
||||
"no_failed"
|
||||
"no_failed",
|
||||
"column_break_wojv"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
|
|
@ -161,7 +168,8 @@
|
|||
"in_list_view": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Domain",
|
||||
"options": "Email Domain"
|
||||
"options": "Email Domain",
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:!doc.domain",
|
||||
|
|
@ -714,13 +722,46 @@
|
|||
"fieldname": "add_x_original_from",
|
||||
"fieldtype": "Check",
|
||||
"label": "Add X-Original-From header"
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
"fieldname": "add_reply_to_header",
|
||||
"fieldtype": "Check",
|
||||
"label": "Add Reply-To header"
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"fieldname": "headers_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Headers"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_jdoz",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_wojv",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_mcbu",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval: doc.add_reply_to_header",
|
||||
"description": "Addresses added here will be used as the Reply-To header for outgoing emails sent from this account.",
|
||||
"fieldname": "reply_to_addresses",
|
||||
"fieldtype": "Table",
|
||||
"ignore_xss_filter": 1,
|
||||
"label": "Reply-To Addresses",
|
||||
"options": "Reply To Address"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-inbox",
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"make_attachments_public": 1,
|
||||
"modified": "2026-02-04 15:50:27.898578",
|
||||
"modified": "2026-02-06 11:39:39.412130",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Email",
|
||||
"name": "Email Account",
|
||||
|
|
|
|||
|
|
@ -58,8 +58,10 @@ class EmailAccount(Document):
|
|||
|
||||
if TYPE_CHECKING:
|
||||
from frappe.email.doctype.imap_folder.imap_folder import IMAPFolder
|
||||
from frappe.email.doctype.reply_to_address.reply_to_address import ReplyToAddress
|
||||
from frappe.types import DF
|
||||
|
||||
add_reply_to_header: DF.Check
|
||||
add_signature: DF.Check
|
||||
add_x_original_from: DF.Check
|
||||
always_bcc: DF.Data | None
|
||||
|
|
@ -102,6 +104,7 @@ class EmailAccount(Document):
|
|||
no_smtp_authentication: DF.Check
|
||||
notify_if_unreplied: DF.Check
|
||||
password: DF.Password | None
|
||||
reply_to_addresses: DF.Table[ReplyToAddress]
|
||||
send_notification_to: DF.SmallText | None
|
||||
send_unsubscribe_message: DF.Check
|
||||
sent_folder_name: DF.Data | None
|
||||
|
|
@ -203,6 +206,9 @@ class EmailAccount(Document):
|
|||
if folder.append_to not in valid_doctypes:
|
||||
frappe.throw(_("Append To can be one of {0}").format(comma_or(valid_doctypes)))
|
||||
|
||||
if self.enable_outgoing:
|
||||
self.validate_reply_to_addresses()
|
||||
|
||||
@frappe.whitelist()
|
||||
def validate_frappe_mail_settings(self):
|
||||
if self.service == "Frappe Mail":
|
||||
|
|
@ -217,6 +223,12 @@ class EmailAccount(Document):
|
|||
self.get_smtp_server().session
|
||||
del self._smtp_server_instance
|
||||
|
||||
def validate_reply_to_addresses(self) -> None:
|
||||
for reply_to in self.reply_to_addresses:
|
||||
if not reply_to.email:
|
||||
frappe.throw(_("Reply To email is required"))
|
||||
validate_email_address(reply_to.email, True)
|
||||
|
||||
def before_save(self):
|
||||
messages = []
|
||||
as_list = 1
|
||||
|
|
|
|||
0
frappe/email/doctype/reply_to_address/__init__.py
Normal file
0
frappe/email/doctype/reply_to_address/__init__.py
Normal file
47
frappe/email/doctype/reply_to_address/reply_to_address.json
Normal file
47
frappe/email/doctype/reply_to_address/reply_to_address.json
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
{
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"creation": "2026-02-06 11:33:22.774848",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"_name",
|
||||
"column_break_xtxq",
|
||||
"email"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "_name",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Name"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_xtxq",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "email",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Email",
|
||||
"reqd": 1
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2026-02-06 11:35:05.181524",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Email",
|
||||
"name": "Reply To Address",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"row_format": "Dynamic",
|
||||
"rows_threshold_for_grid_search": 20,
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
24
frappe/email/doctype/reply_to_address/reply_to_address.py
Normal file
24
frappe/email/doctype/reply_to_address/reply_to_address.py
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
# Copyright (c) 2026, Frappe Technologies and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
# import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class ReplyToAddress(Document):
|
||||
# begin: auto-generated types
|
||||
# This code is auto-generated. Do not modify anything in this block.
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from frappe.types import DF
|
||||
|
||||
_name: DF.Data | None
|
||||
email: DF.Data
|
||||
parent: DF.Data
|
||||
parentfield: DF.Data
|
||||
parenttype: DF.Data
|
||||
# end: auto-generated types
|
||||
|
||||
pass
|
||||
|
|
@ -9,6 +9,7 @@ import re
|
|||
from email import policy
|
||||
from email.header import Header
|
||||
from email.mime.multipart import MIMEMultipart
|
||||
from email.utils import formataddr
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import frappe
|
||||
|
|
@ -25,6 +26,7 @@ from frappe.utils import (
|
|||
split_emails,
|
||||
strip,
|
||||
to_markdown,
|
||||
validate_email_address,
|
||||
)
|
||||
from frappe.utils.pdf import get_pdf
|
||||
|
||||
|
|
@ -268,13 +270,12 @@ class EMail:
|
|||
|
||||
def validate(self):
|
||||
"""validate the Email Addresses"""
|
||||
from frappe.utils import validate_email_address
|
||||
|
||||
if not self.sender:
|
||||
self.sender = self.email_account.default_sender
|
||||
|
||||
validate_email_address(strip(self.sender), True)
|
||||
self.reply_to = validate_email_address(strip(self.reply_to) or self.sender, True)
|
||||
self.validate_reply_to()
|
||||
|
||||
if self.email_account.add_x_original_from:
|
||||
self.set_header("X-Original-From", self.sender)
|
||||
|
|
@ -289,6 +290,23 @@ class EMail:
|
|||
for e in self.recipients + (self.cc or []) + (self.bcc or []):
|
||||
validate_email_address(e, True)
|
||||
|
||||
def validate_reply_to(self) -> None:
|
||||
if not self.email_account.add_reply_to_header:
|
||||
self.reply_to = None
|
||||
return
|
||||
|
||||
if self.email_account.reply_to_addresses:
|
||||
valid_addresses = [
|
||||
formataddr((reply_to._name, reply_to.email))
|
||||
for reply_to in self.email_account.reply_to_addresses
|
||||
if reply_to.email and validate_email_address(reply_to.email, True)
|
||||
]
|
||||
self.reply_to = ", ".join(valid_addresses) if valid_addresses else None
|
||||
return
|
||||
|
||||
fallback = strip(self.reply_to) or self.sender
|
||||
self.reply_to = validate_email_address(fallback, True)
|
||||
|
||||
def replace_sender(self):
|
||||
if cint(self.email_account.always_use_account_email_id_as_sender):
|
||||
sender_name, _ = parse_addr(self.sender)
|
||||
|
|
|
|||
|
|
@ -258,4 +258,5 @@ execute:frappe.delete_doc_if_exists("Workspace Sidebar", "Productivity")
|
|||
frappe.patches.v16_0.unset_standard_field_for_auto_generated_icons
|
||||
execute:from frappe.email.doctype.notification.notification import install_notification_templates; install_notification_templates()
|
||||
execute:frappe.db.set_value("Email Account", {}, "add_x_original_from", 1)
|
||||
frappe.patches.v16_0.fix_myanmar_language_name
|
||||
frappe.patches.v16_0.fix_myanmar_language_name
|
||||
execute:frappe.db.set_value("Email Account", {}, "add_reply_to_header", 1)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue