diff --git a/mail/mail/doctype/mail_agent_job/mail_agent_job.py b/mail/mail/doctype/mail_agent_job/mail_agent_job.py index b06711bc..51b83e7c 100644 --- a/mail/mail/doctype/mail_agent_job/mail_agent_job.py +++ b/mail/mail/doctype/mail_agent_job/mail_agent_job.py @@ -69,44 +69,6 @@ def execute(self) -> None: self.db_update() -def create_domain_on_agents(domain_name: str, agents: list[str] | None = None) -> None: - """Creates a domain on all primary agents.""" - - primary_agents = agents or frappe.db.get_all( - "Mail Agent", filters={"enabled": 1, "is_primary": 1}, pluck="name" - ) - - if not primary_agents: - return - - principal = Principal(name=domain_name, type="domain").__dict__ - for agent in primary_agents: - agent_job = frappe.new_doc("Mail Agent Job") - agent_job.agent = agent - agent_job.method = "POST" - agent_job.endpoint = "/api/principal" - agent_job.request_json = principal - agent_job.insert() - - -def delete_domain_from_agents(domain_name: str, agents: list[str] | None = None) -> None: - """Deletes a domain from all primary agents.""" - - primary_agents = agents or frappe.db.get_all( - "Mail Agent", filters={"enabled": 1, "is_primary": 1}, pluck="name" - ) - - if not primary_agents: - return - - for agent in primary_agents: - agent_job = frappe.new_doc("Mail Agent Job") - agent_job.agent = agent - agent_job.method = "DELETE" - agent_job.endpoint = f"/api/principal/{domain_name}" - agent_job.insert() - - def create_dkim_key_on_agents( domain_name: str, rsa_private_key: str, ed25519_private_key: str, agents: list[str] | None = None ) -> None: @@ -187,6 +149,44 @@ def delete_dkim_key_from_agents(domain_name: str, agents: list[str] | None = Non agent_job.insert() +def create_domain_on_agents(domain_name: str, agents: list[str] | None = None) -> None: + """Creates a domain on all primary agents.""" + + primary_agents = agents or frappe.db.get_all( + "Mail Agent", filters={"enabled": 1, "is_primary": 1}, pluck="name" + ) + + if not primary_agents: + return + + principal = Principal(name=domain_name, type="domain").__dict__ + for agent in primary_agents: + agent_job = frappe.new_doc("Mail Agent Job") + agent_job.agent = agent + agent_job.method = "POST" + agent_job.endpoint = "/api/principal" + agent_job.request_json = principal + agent_job.insert() + + +def delete_domain_from_agents(domain_name: str, agents: list[str] | None = None) -> None: + """Deletes a domain from all primary agents.""" + + primary_agents = agents or frappe.db.get_all( + "Mail Agent", filters={"enabled": 1, "is_primary": 1}, pluck="name" + ) + + if not primary_agents: + return + + for agent in primary_agents: + agent_job = frappe.new_doc("Mail Agent Job") + agent_job.agent = agent + agent_job.method = "DELETE" + agent_job.endpoint = f"/api/principal/{domain_name}" + agent_job.insert() + + def create_account_on_agents( email: str, display_name: str, secret: str, agents: list[str] | None = None ) -> None: @@ -274,6 +274,66 @@ def delete_account_from_agents(email: str, agents: list[str] | None = None) -> N agent_job.insert() +def create_group_on_agents(email: str, display_name: str, agents: list[str] | None = None) -> None: + """Creates a group on all primary agents.""" + + primary_agents = agents or frappe.db.get_all( + "Mail Agent", filters={"enabled": 1, "is_primary": 1}, pluck="name" + ) + + if not primary_agents: + return + + principal = Principal( + name=email, + type="group", + description=display_name, + emails=[email], + enabledPermissions=["email-send", "email-receive"], + ).__dict__ + for agent in primary_agents: + agent_job = frappe.new_doc("Mail Agent Job") + agent_job.agent = agent + agent_job.method = "POST" + agent_job.endpoint = "/api/principal" + agent_job.request_json = principal + agent_job.insert() + + +def patch_group_on_agents(email: str, display_name: str, agents: list[str] | None = None) -> None: + """Patches a group on all primary agents.""" + + primary_agents = agents or frappe.db.get_all( + "Mail Agent", filters={"enabled": 1, "is_primary": 1}, pluck="name" + ) + + if not primary_agents: + return + + request_data = json.dumps( + [ + { + "action": "set", + "field": "description", + "value": display_name, + } + ] + ) + for agent in primary_agents: + agent_job = frappe.new_doc("Mail Agent Job") + agent_job.agent = agent + agent_job.method = "PATCH" + agent_job.endpoint = f"/api/principal/{email}" + agent_job.request_data = request_data + agent_job.insert() + + +def delete_group_from_agents(email: str, agents: list[str] | None = None) -> None: + """Deletes a group from all primary agents.""" + + delete_account_from_agents(email, agents) + + def create_alias_on_agents(account: str, alias: str, agents: list[str] | None = None) -> None: """Creates an alias on all primary agents.""" diff --git a/mail/mail/doctype/mail_alias/mail_alias.json b/mail/mail/doctype/mail_alias/mail_alias.json index 09f6960f..aee0bc5b 100644 --- a/mail/mail/doctype/mail_alias/mail_alias.json +++ b/mail/mail/doctype/mail_alias/mail_alias.json @@ -59,7 +59,7 @@ "fieldname": "alias_for_type", "fieldtype": "Select", "label": "Alias For (Type)", - "options": "\nMail Account", + "options": "\nMail Account\nMail Group", "reqd": 1 }, { @@ -76,7 +76,7 @@ ], "index_web_pages_for_search": 1, "links": [], - "modified": "2025-01-06 12:54:35.804872", + "modified": "2025-01-06 14:29:12.825375", "modified_by": "Administrator", "module": "Mail", "name": "Mail Alias", diff --git a/mail/mail/doctype/mail_domain/mail_domain.json b/mail/mail/doctype/mail_domain/mail_domain.json index fd09d73d..1a329128 100644 --- a/mail/mail/doctype/mail_domain/mail_domain.json +++ b/mail/mail/doctype/mail_domain/mail_domain.json @@ -92,7 +92,12 @@ "links": [ { "group": "Reference", - "link_doctype": "Mailbox", + "link_doctype": "Mail Account", + "link_fieldname": "domain_name" + }, + { + "group": "Reference", + "link_doctype": "Mail Group", "link_fieldname": "domain_name" }, { @@ -106,17 +111,22 @@ "link_fieldname": "domain_name" }, { - "group": "Reference", + "group": "Mail", "link_doctype": "Incoming Mail", "link_fieldname": "domain_name" }, { - "group": "Reference", + "group": "Mail", "link_doctype": "Outgoing Mail", "link_fieldname": "domain_name" + }, + { + "group": "Report", + "link_doctype": "DMARC Report", + "link_fieldname": "domain_name" } ], - "modified": "2025-01-05 12:07:54.486802", + "modified": "2025-01-06 15:35:35.811831", "modified_by": "Administrator", "module": "Mail", "name": "Mail Domain", diff --git a/mail/mail/doctype/mail_group/__init__.py b/mail/mail/doctype/mail_group/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/mail/mail/doctype/mail_group/mail_group.js b/mail/mail/doctype/mail_group/mail_group.js new file mode 100644 index 00000000..3aef5e70 --- /dev/null +++ b/mail/mail/doctype/mail_group/mail_group.js @@ -0,0 +1,8 @@ +// Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +// frappe.ui.form.on("Mail Group", { +// refresh(frm) { + +// }, +// }); diff --git a/mail/mail/doctype/mail_group/mail_group.json b/mail/mail/doctype/mail_group/mail_group.json new file mode 100644 index 00000000..f8559fb1 --- /dev/null +++ b/mail/mail/doctype/mail_group/mail_group.json @@ -0,0 +1,94 @@ +{ + "actions": [], + "creation": "2025-01-06 14:23:03.332903", + "doctype": "DocType", + "engine": "InnoDB", + "field_order": [ + "section_break_uwhi", + "enabled", + "column_break_zfbl", + "domain_name", + "email", + "display_name" + ], + "fields": [ + { + "fieldname": "section_break_uwhi", + "fieldtype": "Section Break" + }, + { + "default": "1", + "depends_on": "eval: !doc.__islocal", + "fieldname": "enabled", + "fieldtype": "Check", + "in_list_view": 1, + "label": "Enabled", + "search_index": 1 + }, + { + "fieldname": "domain_name", + "fieldtype": "Link", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Domain Name", + "no_copy": 1, + "options": "Mail Domain", + "reqd": 1, + "search_index": 1, + "set_only_once": 1 + }, + { + "fieldname": "email", + "fieldtype": "Data", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Email", + "no_copy": 1, + "options": "Email", + "reqd": 1, + "set_only_once": 1, + "unique": 1 + }, + { + "fieldname": "column_break_zfbl", + "fieldtype": "Column Break" + }, + { + "fieldname": "display_name", + "fieldtype": "Data", + "label": "Display Name" + } + ], + "index_web_pages_for_search": 1, + "links": [ + { + "group": "Reference", + "link_doctype": "Mail Alias", + "link_fieldname": "alias_for_name" + } + ], + "modified": "2025-01-06 15:29:16.465956", + "modified_by": "Administrator", + "module": "Mail", + "name": "Mail Group", + "naming_rule": "Set by user", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "sort_field": "creation", + "sort_order": "DESC", + "states": [], + "track_changes": 1 +} \ No newline at end of file diff --git a/mail/mail/doctype/mail_group/mail_group.py b/mail/mail/doctype/mail_group/mail_group.py new file mode 100644 index 00000000..4c7089b8 --- /dev/null +++ b/mail/mail/doctype/mail_group/mail_group.py @@ -0,0 +1,54 @@ +# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +import frappe +from frappe.model.document import Document + +from mail.mail.doctype.mail_agent_job.mail_agent_job import ( + create_group_on_agents, + delete_group_from_agents, + patch_group_on_agents, +) +from mail.utils.validation import ( + is_valid_email_for_domain, + validate_domain_is_enabled_and_verified, +) + + +class MailGroup(Document): + def autoname(self) -> None: + self.email = self.email.strip().lower() + self.name = self.email + + def validate(self) -> None: + self.validate_domain() + self.validate_email() + self.validate_display_name() + + def on_update(self) -> None: + if self.has_value_changed("email"): + create_group_on_agents(self.email, self.display_name) + return + + has_value_changed = self.has_value_changed("display_name") + if has_value_changed: + patch_group_on_agents(self.email, self.display_name) + + def on_trash(self) -> None: + delete_group_from_agents(self.email) + + def validate_domain(self) -> None: + """Validates the domain.""" + + validate_domain_is_enabled_and_verified(self.domain_name) + + def validate_email(self) -> None: + """Validates the email address.""" + + is_valid_email_for_domain(self.email, self.domain_name, raise_exception=True) + + def validate_display_name(self) -> None: + """Validates the display name.""" + + if self.is_new() and not self.display_name: + self.display_name = frappe.db.get_value("User", self.user, "full_name") diff --git a/mail/mail/doctype/mail_group/test_mail_group.py b/mail/mail/doctype/mail_group/test_mail_group.py new file mode 100644 index 00000000..67fa4d8a --- /dev/null +++ b/mail/mail/doctype/mail_group/test_mail_group.py @@ -0,0 +1,29 @@ +# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +# import frappe +from frappe.tests import IntegrationTestCase, UnitTestCase + +# On IntegrationTestCase, the doctype test records and all +# link-field test record dependencies are recursively loaded +# Use these module variables to add/remove to/from that list +EXTRA_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"] +IGNORE_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"] + + +class UnitTestMailGroup(UnitTestCase): + """ + Unit tests for MailGroup. + Use this class for testing individual functions and methods. + """ + + pass + + +class IntegrationTestMailGroup(IntegrationTestCase): + """ + Integration tests for MailGroup. + Use this class for testing interactions between multiple components. + """ + + pass