Skip to content

Commit

Permalink
feat: Mail Server Sync History
Browse files Browse the repository at this point in the history
  • Loading branch information
s-aga-r committed Oct 28, 2024
1 parent 6fe7cd3 commit e7bb349
Show file tree
Hide file tree
Showing 9 changed files with 320 additions and 18 deletions.
23 changes: 6 additions & 17 deletions mail_server/api/domain.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import frappe
from frappe import _

from mail_server.utils.cache import get_root_domain_name, get_user_owned_domains
from mail_server.utils.user import has_role, is_system_manager
from mail_server.utils.validation import is_domain_registry_exists
from mail_server.utils.cache import get_root_domain_name
from mail_server.utils.validation import (
is_domain_registry_exists,
validate_user_has_domain_owner_role,
validate_user_is_domain_owner,
)


@frappe.whitelist(methods=["POST"])
Expand Down Expand Up @@ -62,17 +65,3 @@ def verify_dns_records(domain_name: str) -> list[str] | None:
doc = frappe.get_doc("Mail Domain Registry", domain_name)
doc.verify_dns_records()
return doc.verification_errors.split("\n") if doc.verification_errors else None


def validate_user_has_domain_owner_role(user: str) -> None:
"""Validate if the user has Domain Owner role or System Manager role."""

if not has_role(user, "Domain Owner") and not is_system_manager(user):
frappe.throw(_("You are not authorized to perform this action."), frappe.PermissionError)


def validate_user_is_domain_owner(user: str, domain_name: str) -> None:
"""Validate if the user is the owner of the given domain."""

if domain_name not in get_user_owned_domains(user) and not is_system_manager(user):
frappe.throw(_("You are not authorized to perform this action."), frappe.PermissionError)
114 changes: 114 additions & 0 deletions mail_server/api/inbound.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
from datetime import datetime
from typing import TYPE_CHECKING

import frappe
import pytz
from frappe import _
from frappe.utils import convert_utc_to_system_timezone, now

from mail_server.mail_server.doctype.mail_server_sync_history.mail_server_sync_history import (
get_mail_server_sync_history,
)
from mail_server.utils.validation import (
is_domain_registry_exists,
validate_user_has_domain_owner_role,
validate_user_is_domain_owner,
)

if TYPE_CHECKING:
from mail_server.mail_server.doctype.mail_server_sync_history.mail_server_sync_history import (
MailServerSyncHistory,
)


from email.utils import formataddr

from mail_server.utils import convert_to_utc


@frappe.whitelist(methods=["GET"])
def fetch(
domain_name: str, limit: int = 100, last_synced_at: str | None = None
) -> dict[str, list[dict] | str]:
"""Returns the incoming mails for the given domain."""

user = frappe.session.user
validate_user_has_domain_owner_role(user)
is_domain_registry_exists(domain_name, raise_exception=True)
validate_user_is_domain_owner(user, domain_name)

source = get_source()
last_synced_at = convert_to_system_timezone(last_synced_at)
sync_history = get_mail_server_sync_history(source, frappe.session.user, domain_name)
result = get_incoming_mails(domain_name, limit, last_synced_at or sync_history.last_synced_at)
update_mail_server_sync_history(sync_history, result["last_synced_at"], result["last_synced_mail"])
result["last_synced_at"] = convert_to_utc(result["last_synced_at"])

return result


def get_source() -> str:
"""Returns the source of the request."""

return frappe.request.headers.get("X-Frappe-Mail-Site") or frappe.local.request_ip


def convert_to_system_timezone(last_synced_at: str) -> datetime | None:
"""Converts the last_synced_at to system timezone."""

if last_synced_at:
dt = datetime.fromisoformat(last_synced_at)
dt_utc = dt.astimezone(pytz.utc)
return convert_utc_to_system_timezone(dt_utc)


def get_incoming_mails(
domain_name: str,
limit: int,
last_synced_at: str | None = None,
) -> dict[str, list[dict] | str]:
"""Returns the incoming mails for the given domain."""

IML = frappe.qb.DocType("Incoming Mail Log")
query = (
frappe.qb.from_(IML)
.select(
IML.name.as_("id"),
IML.processed_at,
IML.is_spam,
IML.message,
)
.where((IML.is_rejected == 0) & (IML.status == "Accepted") & (IML.domain_name == domain_name))
.orderby(IML.processed_at)
.limit(limit)
)

if last_synced_at:
query = query.where(IML.processed_at > last_synced_at)

mails = query.run(as_dict=True)
last_synced_at = mails[-1].processed_at if mails else now()
last_synced_mail = mails[-1].id if mails else None

return {
"mails": mails,
"last_synced_at": last_synced_at,
"last_synced_mail": last_synced_mail,
}


def update_mail_server_sync_history(
sync_history: "MailServerSyncHistory",
last_synced_at: str,
last_synced_mail: str | None = None,
) -> None:
"""Update the last_synced_at in the Mail Server Sync History."""

kwargs = {
"last_synced_at": last_synced_at or now(),
}

if last_synced_mail:
kwargs["last_synced_mail"] = last_synced_mail

sync_history._db_set(**kwargs, commit=True)
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ function fetch_emails_from_queue(listview) {
freeze_message: __("Creating Job..."),
callback: () => {
frappe.show_alert({
message: __("{0} job has been created.", [__("Fetch Mails").bold()]),
message: __("{0} job has been created.", [__("Fetch Emails").bold()]),
indicator: "green",
});
},
Expand Down
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt

// frappe.ui.form.on("Mail Server Sync History", {
// refresh(frm) {

// },
// });
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
{
"actions": [],
"autoname": "hash",
"creation": "2024-10-28 12:25:01.227429",
"doctype": "DocType",
"engine": "InnoDB",
"field_order": [
"section_break_gdn1",
"user",
"source",
"column_break_xk0e",
"domain_name",
"last_synced_at",
"last_synced_mail"
],
"fields": [
{
"fieldname": "section_break_gdn1",
"fieldtype": "Section Break"
},
{
"fieldname": "user",
"fieldtype": "Link",
"in_list_view": 1,
"label": "User",
"options": "User",
"reqd": 1,
"search_index": 1
},
{
"fieldname": "source",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Source",
"reqd": 1,
"search_index": 1
},
{
"fieldname": "column_break_xk0e",
"fieldtype": "Column Break"
},
{
"fieldname": "domain_name",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Domain Name",
"options": "Mail Domain Registry",
"reqd": 1,
"search_index": 1
},
{
"fieldname": "last_synced_at",
"fieldtype": "Datetime",
"label": "Last Synced At",
"search_index": 1
},
{
"fieldname": "last_synced_mail",
"fieldtype": "Data",
"label": "Last Synced Mail"
}
],
"in_create": 1,
"index_web_pages_for_search": 1,
"links": [],
"modified": "2024-10-28 12:28:26.008255",
"modified_by": "Administrator",
"module": "Mail Server",
"name": "Mail Server Sync History",
"naming_rule": "Random",
"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
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt

import frappe
from frappe import _
from frappe.model.document import Document


class MailServerSyncHistory(Document):
def before_insert(self) -> None:
self.validate_duplicate()

def validate_duplicate(self) -> None:
"""Validate if the Mail Server Sync History already exists."""

if frappe.db.exists(
"Mail Server Sync History",
{"source": self.source, "user": self.user, "domain_name": self.domain_name},
):
frappe.throw(_("Mail Server Sync History already exists for this source, user and domain."))

def _db_set(
self,
update_modified: bool = True,
commit: bool = False,
notify_update: bool = False,
**kwargs,
) -> None:
"""Updates the document with the given key-value pairs."""

self.db_set(kwargs, update_modified=update_modified, commit=commit)

if notify_update:
self.notify_update()


def create_mail_server_sync_history(
source: str,
user: str,
domain_name: str,
last_synced_at: str | None = None,
commit: bool = False,
) -> "MailServerSyncHistory":
"""Create a Mail Server Sync History."""

doc = frappe.new_doc("Mail Server Sync History")
doc.source = source
doc.user = user
doc.domain_name = domain_name
doc.last_synced_at = last_synced_at
doc.insert(ignore_permissions=True)

if commit:
frappe.db.commit()

return doc


def get_mail_server_sync_history(source: str, user: str, domain_name: str) -> "MailServerSyncHistory":
"""Returns the Mail Server Sync History for the given source, user and domain."""

if name := frappe.db.exists(
"Mail Server Sync History", {"source": source, "user": user, "domain_name": domain_name}
):
return frappe.get_doc("Mail Server Sync History", name)

return create_mail_server_sync_history(source, user, domain_name, commit=True)


def on_doctype_update():
frappe.db.add_unique(
"Mail Server Sync History",
["source", "user", "domain_name"],
constraint_name="unique_source_user_domain",
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt

# import frappe
from frappe.tests.utils import FrappeTestCase


class TestMailServerSyncHistory(FrappeTestCase):
pass
17 changes: 17 additions & 0 deletions mail_server/utils/validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
import frappe
from frappe import _

from mail_server.utils.cache import get_user_owned_domains
from mail_server.utils.user import has_role, is_system_manager


def is_valid_host(host: str) -> bool:
"""Returns True if the host is a valid hostname else False."""
Expand Down Expand Up @@ -37,3 +40,17 @@ def is_domain_registry_exists(
)

return False


def validate_user_has_domain_owner_role(user: str) -> None:
"""Validate if the user has Domain Owner role or System Manager role."""

if not has_role(user, "Domain Owner") and not is_system_manager(user):
frappe.throw(_("You are not authorized to perform this action."), frappe.PermissionError)


def validate_user_is_domain_owner(user: str, domain_name: str) -> None:
"""Validate if the user is the owner of the given domain."""

if domain_name not in get_user_owned_domains(user) and not is_system_manager(user):
frappe.throw(_("You are not authorized to perform this action."), frappe.PermissionError)

0 comments on commit e7bb349

Please sign in to comment.