Skip to content

Commit

Permalink
feat: manage DKIM Key in Mail Agents
Browse files Browse the repository at this point in the history
  • Loading branch information
s-aga-r committed Jan 5, 2025
1 parent c8662e6 commit 5f8be4e
Show file tree
Hide file tree
Showing 4 changed files with 122 additions and 8 deletions.
12 changes: 10 additions & 2 deletions mail/mail/doctype/dkim_key/dkim_key.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@
from frappe.utils.caching import request_cache

from mail.mail.doctype.dns_record.dns_record import create_or_update_dns_record
from mail.mail.doctype.mail_agent_job.mail_agent_job import (
create_dkim_key_on_agents,
delete_dkim_key_from_agents,
)
from mail.utils import get_dkim_host


class DKIMKey(Document):
Expand All @@ -24,11 +29,14 @@ def validate(self) -> None:
def after_insert(self) -> None:
self.create_or_update_dns_record()
self.disable_existing_dkim_keys()
create_dkim_key_on_agents(self)

def on_trash(self) -> None:
if frappe.session.user != "Administrator":
frappe.throw(_("Only Administrator can delete DKIM Key."))

delete_dkim_key_from_agents(self)

def validate_domain_name(self) -> None:
"""Validates the Domain Name."""

Expand Down Expand Up @@ -56,7 +64,7 @@ def create_or_update_dns_record(self) -> None:

# RSA
create_or_update_dns_record(
host=f"{self.domain_name.replace('.', '-')}-r._domainkey",
host=f"{get_dkim_host(self.domain_name, 'rsa')}._domainkey",
type="TXT",
value=f"v=DKIM1; k=rsa; h=sha256; p={self.rsa_public_key}",
ttl=300,
Expand All @@ -67,7 +75,7 @@ def create_or_update_dns_record(self) -> None:

# Ed25519
create_or_update_dns_record(
host=f"{self.domain_name.replace('.', '-')}-e._domainkey",
host=f"{get_dkim_host(self.domain_name, 'ed25519')}._domainkey",
type="TXT",
value=f"v=DKIM1; k=ed25519; h=sha256; p={self.ed25519_public_key}",
ttl=300,
Expand Down
84 changes: 84 additions & 0 deletions mail/mail/doctype/mail_agent_job/mail_agent_job.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
# Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt

import json
from typing import TYPE_CHECKING
from urllib.parse import quote

import frappe
Expand All @@ -9,6 +11,10 @@
from frappe.utils import now, time_diff_in_seconds

from mail.agent import AgentAPI, Principal
from mail.utils import get_dkim_host, get_dkim_selector

if TYPE_CHECKING:
from mail.mail.doctype.dkim_key.dkim_key import DKIMKey


class MailAgentJob(Document):
Expand Down Expand Up @@ -103,3 +109,81 @@ def delete_domain_from_agents(domain_name: str, agents: list[str] | None = None)
agent_job.method = "DELETE"
agent_job.endpoint = f"/api/principal/{domain_name}"
agent_job.insert()


def create_dkim_key_on_agents(dkim_key: "DKIMKey", agents: list[str] | None = None) -> None:
"""Creates a DKIM Key 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

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/settings"
agent_job.request_data = json.dumps(
[
{
"type": "Insert",
"prefix": f"signature.{get_dkim_host(dkim_key.domain_name, 'rsa')}",
"values": [
["report", "true"],
["selector", get_dkim_selector("rsa")],
["canonicalization", "relaxed/relaxed"],
["private-key", dkim_key.rsa_private_key],
["algorithm", "rsa-sha256"],
["domain", dkim_key.domain_name],
],
"assert_empty": True,
},
{
"type": "Insert",
"prefix": f"signature.{get_dkim_host(dkim_key.domain_name, 'ed25519')}",
"values": [
["report", "true"],
["selector", get_dkim_selector("ed25519")],
["canonicalization", "relaxed/relaxed"],
["private-key", dkim_key.ed25519_private_key],
["algorithm", "ed25519-sha256"],
["domain", dkim_key.domain_name],
],
"assert_empty": True,
},
]
)
agent_job.insert()


def delete_dkim_key_from_agents(dkim_key: "DKIMKey", agents: list[str] | None = None) -> None:
"""Deletes a DKIM Key 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 = "POST"
agent_job.endpoint = "/api/settings"
agent_job.request_data = json.dumps(
[
{
"type": "Clear",
"prefix": f"signature.{get_dkim_host(dkim_key.domain_name, 'rsa')}",
},
{
"type": "Clear",
"prefix": f"signature.{get_dkim_host(dkim_key.domain_name, 'ed25519')}",
},
]
)
agent_job.insert()
10 changes: 5 additions & 5 deletions mail/mail/doctype/mail_domain/mail_domain.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

from mail.mail.doctype.dkim_key.dkim_key import create_dkim_key
from mail.mail.doctype.mail_agent_job.mail_agent_job import create_domain_on_agents, delete_domain_from_agents
from mail.utils import get_dmarc_address
from mail.utils import get_dkim_host, get_dkim_selector, get_dmarc_address
from mail.utils.dns import verify_dns_record


Expand Down Expand Up @@ -145,8 +145,8 @@ def get_dns_records(domain_name: str) -> list[dict]:
{
"category": "Sending Record",
"type": "CNAME",
"host": f"frappemail-r._domainkey.{domain_name}",
"value": f"{domain_name.replace('.', '-')}-r._domainkey.{mail_settings.root_domain_name}.",
"host": f"{get_dkim_selector('rsa')}._domainkey.{domain_name}",
"value": f"{get_dkim_host(domain_name, 'rsa')}._domainkey.{mail_settings.root_domain_name}.",
"ttl": mail_settings.default_ttl,
}
)
Expand All @@ -155,8 +155,8 @@ def get_dns_records(domain_name: str) -> list[dict]:
{
"category": "Sending Record",
"type": "CNAME",
"host": f"frappemail-e._domainkey.{domain_name}",
"value": f"{domain_name.replace('.', '-')}-e._domainkey.{mail_settings.root_domain_name}.",
"host": f"{get_dkim_selector('ed25519')}._domainkey.{domain_name}",
"value": f"{get_dkim_host(domain_name, 'ed25519')}._domainkey.{mail_settings.root_domain_name}.",
"ttl": mail_settings.default_ttl,
}
)
Expand Down
24 changes: 23 additions & 1 deletion mail/utils/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import zipfile
from collections.abc import Callable
from io import BytesIO
from typing import Literal

import frappe
from bs4 import BeautifulSoup
Expand Down Expand Up @@ -116,7 +117,28 @@ def check_deliverability(email: str) -> bool:
return validate_email_address(email, check_mx=True, verify=True, smtp_timeout=10)


def get_dkim_host(domain_name: str, type: Literal["rsa", "ed25519"]) -> str:
"""
Returns DKIM host.
e.g. example-com-r for RSA and example-com-e for Ed25519.
"""

return f"{domain_name.replace('.', '-')}-{type[0]}"


def get_dkim_selector(key_type: Literal["rsa", "ed25519"]) -> str:
"""
Returns DKIM selector.
e.g. frappemail-r for RSA and frappemail-e for Ed25519.
"""

return f"frappemail-{key_type[0]}"


def get_dmarc_address() -> str:
"""Returns DMARC address."""
"""
Returns DMARC address.
e.g. [email protected]
"""

return f"dmarc@{get_root_domain_name()}"

0 comments on commit 5f8be4e

Please sign in to comment.