Skip to content

Commit

Permalink
refactor: generate DKIM keys (#31)
Browse files Browse the repository at this point in the history
  • Loading branch information
s-aga-r authored Nov 14, 2024
1 parent 4718743 commit ffad64c
Show file tree
Hide file tree
Showing 6 changed files with 130 additions and 52 deletions.
4 changes: 2 additions & 2 deletions mail_client/api/webhook.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@ def receive_email() -> None:
if not mail_domain.enabled:
frappe.throw(_("Mail Domain {0} is disabled").format(data["domain_name"]))

if data["inbound_token"] != mail_domain.get_password("inbound_token"):
frappe.throw(_("Invalid Inbound Token"))
if data["access_token"] != mail_domain.get_password("access_token"):
frappe.throw(_("Invalid Access Token"))

process_incoming_mail(
incoming_mail_log=data["incoming_mail_log"],
Expand Down
55 changes: 33 additions & 22 deletions mail_client/locale/main.pot
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ msgid ""
msgstr ""
"Project-Id-Version: Mail Client VERSION\n"
"Report-Msgid-Bugs-To: [email protected]\n"
"POT-Creation-Date: 2024-11-09 20:22+0553\n"
"PO-Revision-Date: 2024-11-09 20:22+0553\n"
"POT-Creation-Date: 2024-11-14 19:15+0553\n"
"PO-Revision-Date: 2024-11-14 19:15+0553\n"
"Last-Translator: [email protected]\n"
"Language-Team: [email protected]\n"
"MIME-Version: 1.0\n"
Expand Down Expand Up @@ -50,6 +50,11 @@ msgstr ""
msgid "Accepted"
msgstr ""

#. Label of the access_token (Password) field in DocType 'Mail Domain'
#: mail_client/mail_client/doctype/mail_domain/mail_domain.json
msgid "Access Token"
msgstr ""

#. Label of the action_after (Float) field in DocType 'Mail Recipient'
#: mail_client/mail_client/doctype/mail_recipient/mail_recipient.json
msgid "Action After (Seconds)"
Expand Down Expand Up @@ -253,9 +258,10 @@ msgstr ""
msgid "DKIM Description"
msgstr ""

#. Label of the dkim_private_key (Password) field in DocType 'Mail Domain'
#. Label of the section_break_i51k (Section Break) field in DocType 'Mail
#. Domain'
#: mail_client/mail_client/doctype/mail_domain/mail_domain.json
msgid "DKIM Private Key"
msgid "DKIM Keys"
msgstr ""

#. Label of the dmarc_pass (Check) field in DocType 'Incoming Mail'
Expand All @@ -273,7 +279,7 @@ msgstr ""
msgid "DNS Records"
msgstr ""

#: mail_client/mail_client/doctype/mail_domain/mail_domain.py:87
#: mail_client/mail_client/doctype/mail_domain/mail_domain.py:89
msgid "DNS Records verified successfully."
msgstr ""

Expand Down Expand Up @@ -469,13 +475,6 @@ msgstr ""
msgid "Future date is not allowed."
msgstr ""

#. Group in Mail Domain's connections
#. Group in Mailbox's connections
#: mail_client/mail_client/doctype/mail_domain/mail_domain.json
#: mail_client/mail_client/doctype/mailbox/mailbox.json
msgid "General"
msgstr ""

#. Label of the host (Data) field in DocType 'Mail Domain DNS Record'
#: mail_client/mail_client/doctype/mail_domain_dns_record/mail_domain_dns_record.json
msgid "Host"
Expand Down Expand Up @@ -525,11 +524,6 @@ msgstr ""
msgid "In Reply To Mail {0} - {1} does not exist."
msgstr ""

#. Label of the inbound_token (Password) field in DocType 'Mail Domain'
#: mail_client/mail_client/doctype/mail_domain/mail_domain.json
msgid "Inbound Token"
msgstr ""

#. Option for the 'Folder' (Select) field in DocType 'Incoming Mail'
#: mail_client/mail_client/doctype/incoming_mail/incoming_mail.json
msgid "Inbox"
Expand Down Expand Up @@ -564,7 +558,7 @@ msgid "Incoming Mail Log"
msgstr ""

#: mail_client/api/webhook.py:35
msgid "Invalid Inbound Token"
msgid "Invalid Access Token"
msgstr ""

#: mail_client/mail_client/report/outgoing_mail_summary/outgoing_mail_summary.py:202
Expand Down Expand Up @@ -829,11 +823,11 @@ msgstr ""
msgid "Newsletter Retention (Days)"
msgstr ""

#: mail_client/mail_client/doctype/mail_domain/mail_domain.py:34
#: mail_client/mail_client/doctype/mail_domain/mail_domain.py:37
msgid "Newsletter Retention must be greater than 0."
msgstr ""

#: mail_client/mail_client/doctype/mail_domain/mail_domain.py:41
#: mail_client/mail_client/doctype/mail_domain/mail_domain.py:44
msgid "Newsletter Retention must be less than or equal to {0}."
msgstr ""

Expand Down Expand Up @@ -932,6 +926,11 @@ msgstr ""
msgid "Priority"
msgstr ""

#. Label of the dkim_private_key (Password) field in DocType 'Mail Domain'
#: mail_client/mail_client/doctype/mail_domain/mail_domain.json
msgid "Private Key"
msgstr ""

#. Label of the processed_after (Float) field in DocType 'Incoming Mail'
#: mail_client/mail_client/doctype/incoming_mail/incoming_mail.json
msgid "Processed After (Seconds)"
Expand All @@ -948,6 +947,11 @@ msgstr ""
msgid "Processed At - Fetched At"
msgstr ""

#. Label of the dkim_public_key (Text) field in DocType 'Mail Domain'
#: mail_client/mail_client/doctype/mail_domain/mail_domain.json
msgid "Public Key"
msgstr ""

#. Option for the 'Status' (Select) field in DocType 'Outgoing Mail'
#: mail_client/mail_client/doctype/outgoing_mail/outgoing_mail.json
msgid "Queued"
Expand Down Expand Up @@ -1007,11 +1011,18 @@ msgstr ""
msgid "Recipients"
msgstr ""

#. Group in Mail Domain's connections
#. Group in Mailbox's connections
#: mail_client/mail_client/doctype/mail_domain/mail_domain.json
#: mail_client/mail_client/doctype/mailbox/mailbox.json
msgid "Reference"
msgstr ""

#: mail_client/mail_client/doctype/mail_domain/mail_domain.js:20
msgid "Refresh DNS Records"
msgstr ""

#: mail_client/mail_client/doctype/mail_domain/mail_domain.js:56
#: mail_client/mail_client/doctype/mail_domain/mail_domain.js:52
msgid "Refreshing DNS Records..."
msgstr ""

Expand Down Expand Up @@ -1385,7 +1396,7 @@ msgstr ""
msgid "Verify DNS Records"
msgstr ""

#: mail_client/mail_client/doctype/mail_domain/mail_domain.js:39
#: mail_client/mail_client/doctype/mail_domain/mail_domain.js:37
msgid "Verifying DNS Records..."
msgstr ""

Expand Down
8 changes: 2 additions & 6 deletions mail_client/mail_client/doctype/mail_domain/mail_domain.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,7 @@ frappe.ui.form.on("Mail Domain", {
frappe.call({
doc: frm.doc,
method: "verify_dns_records",
args: {
save: true,
},
args: {},
freeze: true,
freeze_message: __("Verifying DNS Records..."),
callback: (r) => {
Expand All @@ -49,9 +47,7 @@ frappe.ui.form.on("Mail Domain", {
frappe.call({
doc: frm.doc,
method: "refresh_dns_records",
args: {
save: true,
},
args: {},
freeze: true,
freeze_message: __("Refreshing DNS Records..."),
callback: (r) => {
Expand Down
30 changes: 22 additions & 8 deletions mail_client/mail_client/doctype/mail_domain/mail_domain.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,14 @@
"enabled",
"is_verified",
"column_break_lr3y",
"inbound_token",
"access_token",
"newsletter_retention",
"dns_records_section",
"dns_records",
"section_break_i51k",
"dkim_private_key"
"dkim_private_key",
"column_break_jqvh",
"dkim_public_key"
],
"fields": [
{
Expand Down Expand Up @@ -70,23 +72,35 @@
},
{
"fieldname": "section_break_i51k",
"fieldtype": "Section Break"
"fieldtype": "Section Break",
"hidden": 1,
"label": "DKIM Keys"
},
{
"fieldname": "dkim_private_key",
"fieldtype": "Password",
"hidden": 1,
"label": "DKIM Private Key",
"label": "Private Key",
"no_copy": 1,
"read_only": 1
},
{
"fieldname": "dkim_public_key",
"fieldtype": "Text",
"label": "Public Key",
"no_copy": 1,
"read_only": 1
},
{
"fieldname": "inbound_token",
"fieldname": "access_token",
"fieldtype": "Password",
"hidden": 1,
"label": "Inbound Token",
"label": "Access Token",
"no_copy": 1,
"read_only": 1
},
{
"fieldname": "column_break_jqvh",
"fieldtype": "Column Break"
}
],
"index_web_pages_for_search": 1,
Expand All @@ -112,7 +126,7 @@
"link_fieldname": "domain_name"
}
],
"modified": "2024-11-09 21:32:56.341864",
"modified": "2024-11-14 18:22:39.676626",
"modified_by": "Administrator",
"module": "Mail Client",
"name": "Mail Domain",
Expand Down
72 changes: 61 additions & 11 deletions mail_client/mail_client/doctype/mail_domain/mail_domain.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,10 @@ def validate(self) -> None:
self.validate_newsletter_retention()

if self.is_new():
self.access_token = generate_access_token()
self.dkim_private_key, self.dkim_public_key = generate_dkim_keys()
self.add_or_update_domain_in_mail_server()
self.refresh_dns_records(do_not_save=True)

if not self.enabled:
self.is_verified = 0
Expand Down Expand Up @@ -51,16 +54,15 @@ def add_or_update_domain_in_mail_server(self) -> None:
"""Adds or Updates the Domain in the Mail Server."""

domain_api = get_mail_server_domain_api()
response = domain_api.add_or_update_domain(self.domain_name, frappe.utils.get_url())

for record in response["dns_records"]:
self.append("dns_records", record)

self.inbound_token = response["inbound_token"]
self.dkim_private_key = response["dkim_private_key"]
domain_api.add_or_update_domain(
domain_name=self.domain_name,
access_token=self.access_token,
dkim_public_key=self.dkim_public_key,
mail_client_host=frappe.utils.get_url(),
)

@frappe.whitelist()
def refresh_dns_records(self) -> None:
def refresh_dns_records(self, do_not_save: bool = False) -> None:
"""Refreshes the DNS Records."""

self.is_verified = 0
Expand All @@ -72,10 +74,11 @@ def refresh_dns_records(self) -> None:
for record in dns_records:
self.append("dns_records", record)

self.save()
if not do_not_save:
self.save()

@frappe.whitelist()
def verify_dns_records(self, save: bool = True) -> None:
def verify_dns_records(self, do_not_save: bool = False) -> None:
"""Verifies the DNS Records."""

domain_api = get_mail_server_domain_api()
Expand All @@ -88,5 +91,52 @@ def verify_dns_records(self, save: bool = True) -> None:
self.is_verified = 0
frappe.msgprint(errors, title="DNS Verification Failed", indicator="red", as_list=True)

if save:
if not do_not_save:
self.save()


def generate_access_token() -> str:
"""Generates and returns the Access Token."""

return frappe.generate_hash(length=32)


def generate_dkim_keys(key_size: int = 2048) -> tuple[str, str]:
"""Generates and returns the DKIM Keys (Private and Public)."""

def get_filtered_dkim_key(key_pem: str) -> str:
"""Returns the filtered DKIM Key."""

key_pem = "".join(key_pem.split())
key_pem = (
key_pem.replace("-----BEGINPUBLICKEY-----", "")
.replace("-----ENDPUBLICKEY-----", "")
.replace("-----BEGINRSAPRIVATEKEY-----", "")
.replace("----ENDRSAPRIVATEKEY-----", "")
)

return key_pem

from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import rsa

private_key = rsa.generate_private_key(
public_exponent=65537, key_size=key_size, backend=default_backend()
)
public_key = private_key.public_key()

private_key_pem = private_key.private_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PrivateFormat.TraditionalOpenSSL,
encryption_algorithm=serialization.NoEncryption(),
).decode()
public_key_pem = public_key.public_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PublicFormat.SubjectPublicKeyInfo,
).decode()

private_key = private_key_pem
public_key = get_filtered_dkim_key(public_key_pem)

return private_key, public_key
13 changes: 10 additions & 3 deletions mail_client/mail_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,11 +91,18 @@ def validate(self) -> None:
class MailServerDomainAPI(MailServerAPI):
"""Class to manage domains in the Frappe Mail Server."""

def add_or_update_domain(self, domain_name: str, mail_client_host: str | None = None) -> dict:
"""Adds or updates a domain in the Frappe Mail Server."""
def add_or_update_domain(
self, domain_name: str, access_token: str, dkim_public_key: str, mail_client_host: str | None = None
) -> dict:
"""Adds or Updates a domain in the Frappe Mail Server."""

endpoint = "/api/method/mail_server.api.domain.add_or_update_domain"
data = {"domain_name": domain_name, "mail_client_host": mail_client_host}
data = {
"domain_name": domain_name,
"access_token": access_token,
"dkim_public_key": dkim_public_key,
"mail_client_host": mail_client_host,
}
return self.request("POST", endpoint=endpoint, data=data)

def get_dns_records(self, domain_name: str) -> list[dict] | None:
Expand Down

0 comments on commit ffad64c

Please sign in to comment.