Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: generate DKIM keys #31

Merged
merged 1 commit into from
Nov 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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: developers@frappe.io\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: developers@frappe.io\n"
"Language-Team: developers@frappe.io\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 @@ -86,11 +86,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
Loading