Skip to content

Commit

Permalink
Merge pull request frappe#46 from s-aga-r/mono-repo
Browse files Browse the repository at this point in the history
refactor!: Mail
  • Loading branch information
s-aga-r authored Jan 20, 2025
2 parents ca5dc9a + 35642e1 commit ae24819
Show file tree
Hide file tree
Showing 171 changed files with 10,360 additions and 3,152 deletions.
531 changes: 330 additions & 201 deletions README.md

Large diffs are not rendered by default.

Binary file added docs/screenshots/hetzner-rdns.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file removed docs/screenshots/incoming-mail-accepted.png
Binary file not shown.
Binary file removed docs/screenshots/incoming-mail-auth-checks.png
Binary file not shown.
Binary file removed docs/screenshots/incoming-mail-more-info.png
Binary file not shown.
Binary file added docs/screenshots/mail-account.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/screenshots/mail-agent-group.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/screenshots/mail-agent.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file removed docs/screenshots/mail-alias.png
Binary file not shown.
Binary file removed docs/screenshots/mail-domain-dns-records.png
Binary file not shown.
Binary file removed docs/screenshots/mail-domain-new.png
Binary file not shown.
Binary file removed docs/screenshots/mail-domain-verify-dns-records.png
Binary file not shown.
Binary file added docs/screenshots/mail-domain.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/screenshots/mail-settings-details.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/screenshots/mail-settings-inbound-limits.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file removed docs/screenshots/mail-settings-incoming.png
Binary file not shown.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file removed docs/screenshots/mailbox.png
Binary file not shown.
Binary file added docs/screenshots/outgoing-mail-desk.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file removed docs/screenshots/outgoing-mail-sent.png
Binary file not shown.
Binary file removed docs/screenshots/outgoing-mail-smtp-response.png
Binary file not shown.
Binary file removed docs/screenshots/outgoing-mail-tracking.png
Binary file not shown.
Binary file removed docs/screenshots/outgoing-mail-transfer-now.png
Binary file not shown.
Binary file added docs/screenshots/outgoing-mail-ui.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file removed docs/screenshots/report-mail-tracker.png
Binary file not shown.
Binary file removed docs/screenshots/report-outbound-delay.png
Binary file not shown.
Binary file removed docs/screenshots/report-outgoing-mail-summary.png
Diff not rendered.
Binary file added docs/screenshots/spf-dns-record.png
Binary file added docs/screenshots/stalwart-install.png
Binary file added docs/screenshots/stalwart-network-settings.png
Binary file added docs/screenshots/stalwart-tls-ACME-provider.png
4 changes: 2 additions & 2 deletions frontend/src/components/Modals/SendMail.vue
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
<span class="text-xs text-gray-500">{{ __('From') }}:</span>
<Link
v-model="mail.from"
doctype="Mailbox"
doctype="Mail Account"
:filters="{ user: user.data.name }"
/>
</div>
Expand Down Expand Up @@ -309,7 +309,7 @@ const createDraftMail = createResource({
method: 'POST',
makeParams() {
return {
// TODO: use mailbox display_name
// TODO: use mail account display_name
from_: `${user.data?.full_name} <${mail.from}>`,
do_not_submit: !isSend.value,
...mail,
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/components/Modals/Settings.vue
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
<script setup>
import { markRaw, ref } from 'vue'
import UserSettings from '@/components/Settings/UserSettings.vue'
import MailboxSettings from '@/components/Settings/MailboxSettings.vue'
import MailAccountSettings from '@/components/Settings/MailAccountSettings.vue'
import { Dialog, Button } from 'frappe-ui'
import { User, Mailbox } from 'lucide-vue-next'
Expand All @@ -51,7 +51,7 @@ const tabs = [
{
label: 'Mailbox',
icon: Mailbox,
component: markRaw(MailboxSettings),
component: markRaw(MailAccountSettings),
},
]
const activeTab = ref(tabs[0])
Expand Down
88 changes: 88 additions & 0 deletions frontend/src/components/Settings/MailAccountSettings.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
<template>
<h1 class="font-semibold mb-8">Account</h1>
<div class="flex items-center mb-3">
<span class="font-medium leading-normal text-gray-800 text-base">Email Address</span>
<Link
v-model="email"
doctype="Mail Account"
:filters="{ user: userResource.data?.name }"
class="ml-auto"
/>
</div>
<div v-if="account.doc" class="space-y-1.5">
<Switch
label="Enabled"
v-model="account.doc.enabled"
@update:modelValue="account.setValue.submit({ enabled: account.doc.enabled })"
/>
<Switch
label="Default Outgoing"
v-model="account.doc.is_default"
@update:modelValue="account.setValue.submit({ is_default: account.doc.is_default })"
/>
<Switch
label="Track Outgoing Mail"
v-model="account.doc.track_outgoing_mail"
@update:modelValue="
account.setValue.submit({ track_outgoing_mail: account.doc.track_outgoing_mail })
"
/>
<Switch
label="Create Mail Contact"
v-model="account.doc.create_mail_contact"
@update:modelValue="
account.setValue.submit({ create_mail_contact: account.doc.create_mail_contact })
"
/>
<div class="mx-2.5 space-y-2.5 pt-0.5">
<div class="flex items-center justify-between">
<span class="font-medium leading-normal text-gray-800 text-base">
Display Name
</span>
<TextInput
v-model="account.doc.display_name"
@input="
account.setValueDebounced.submit({
display_name: account.doc.display_name,
})
"
/>
</div>
<div class="flex items-center justify-between">
<span class="font-medium leading-normal text-gray-800 text-base">Reply To</span>
<TextInput
v-model="account.doc.reply_to"
@input="account.setValueDebounced.submit({ reply_to: account.doc.reply_to })"
/>
</div>
</div>
</div>
</template>
<script setup>
import { ref, watch, onMounted } from 'vue'
import { Switch, TextInput, createDocumentResource } from 'frappe-ui'
import Link from '@/components/Controls/Link.vue'
import { userStore } from '@/stores/user'
const { userResource, defaultOutgoing } = userStore()
const email = ref(defaultOutgoing.data)
const fetchMailAccount = () => {
account.name = email.value
account.reload()
}
onMounted(fetchMailAccount)
watch(email, fetchMailAccount)
const account = createDocumentResource({
doctype: 'Mail Account',
name: email.value,
auto: false,
transform(data) {
for (const d of ['enabled', 'is_default', 'track_outgoing_mail', 'create_mail_contact']) {
data[d] = !!data[d]
}
},
})
</script>
107 changes: 0 additions & 107 deletions frontend/src/components/Settings/MailboxSettings.vue

This file was deleted.

91 changes: 91 additions & 0 deletions mail/agent.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
from dataclasses import dataclass, field
from typing import Any, Literal
from urllib.parse import urljoin

import frappe
import requests
from frappe import _


@dataclass
class Principal:
"""Dataclass to represent a principal."""

name: str
type: Literal["domain", "apiKey", "individual"]
id: int = 0
quota: int = 0
description: str = ""
secrets: str | list[str] = field(default_factory=list)
emails: list[str] = field(default_factory=list)
urls: list[str] = field(default_factory=list)
memberOf: list[str] = field(default_factory=list)
roles: list[str] = field(default_factory=list)
lists: list[str] = field(default_factory=list)
members: list[str] = field(default_factory=list)
enabledPermissions: list[str] = field(default_factory=list)
disabledPermissions: list[str] = field(default_factory=list)
externalMembers: list[str] = field(default_factory=list)


class AgentAPI:
"""Class to interact with the Agent."""

def __init__(
self,
base_url: str,
api_key: str | None = None,
username: str | None = None,
password: str | None = None,
) -> None:
self.base_url = base_url
self.__api_key = api_key
self.__username = username
self.__password = password
self.__session = requests.Session()

self.__auth = None
self.__headers = {}
if self.__api_key:
self.__headers.update({"Authorization": f"Bearer {self.__api_key}"})
else:
if not self.__username or not self.__password:
frappe.throw(_("API Key or Username and Password is required."))

self.__auth = (self.__username, self.__password)

def request(
self,
method: str,
endpoint: str,
params: dict | None = None,
data: dict | None = None,
json: dict | None = None,
files: dict | None = None,
headers: dict[str, str] | None = None,
timeout: int | tuple[int, int] = (60, 120),
) -> Any | None:
"""Makes an HTTP request to the Agent."""

url = urljoin(self.base_url, endpoint)

headers = headers or {}
headers.update(self.__headers)

if files:
headers.pop("content-type", None)

response = self.__session.request(
method=method,
url=url,
params=params,
data=data,
headers=headers,
files=files,
auth=self.__auth,
timeout=timeout,
json=json,
)
response.raise_for_status()

return response.json()
36 changes: 14 additions & 22 deletions mail/api/auth.py
Original file line number Diff line number Diff line change
@@ -1,43 +1,35 @@
import frappe
from frappe import _

from mail.utils.user import has_role, is_mailbox_owner
from mail.utils.validation import (
validate_mailbox_for_incoming,
validate_mailbox_for_outgoing,
)
from mail.utils.user import has_role, is_mail_account_owner


@frappe.whitelist(methods=["POST"])
def validate(mailbox: str | None = None, for_inbound: bool = False, for_outbound: bool = False) -> None:
"""Validates the mailbox for inbound and outbound emails."""
def validate(account: str | None = None) -> None:
"""Validates the account for inbound and outbound emails."""

if mailbox:
if account:
validate_user()
validate_mailbox(mailbox)

if for_inbound:
validate_mailbox_for_incoming(mailbox)

if for_outbound:
validate_mailbox_for_outgoing(mailbox)
validate_account(account)


def validate_user() -> None:
"""Validates if the user has the required role to access mailboxes."""
"""Validates if the user has the required role to access mail accounts."""

user = frappe.session.user

if not has_role(user, "Mailbox User"):
frappe.throw(_("User {0} is not allowed to access mailboxes.").format(frappe.bold(user)))
if not has_role(user, "Mail User"):
frappe.throw(_("User {0} is not allowed to access mail accounts.").format(frappe.bold(user)))


def validate_mailbox(mailbox: str) -> None:
"""Validates if the mailbox is associated with the user."""
def validate_account(account: str) -> None:
"""Validates if the mail account is associated with the user."""

user = frappe.session.user

if not is_mailbox_owner(mailbox, user):
if not is_mail_account_owner(account, user):
frappe.throw(
_("Mailbox {0} is not associated with user {1}").format(frappe.bold(mailbox), frappe.bold(user))
_("Mail Account {0} is not associated with user {1}").format(
frappe.bold(account), frappe.bold(user)
)
)
14 changes: 14 additions & 0 deletions mail/api/blacklist.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import frappe
from frappe import _

from mail.mail.doctype.ip_blacklist.ip_blacklist import get_blacklist_for_ip_address


@frappe.whitelist(methods=["GET"], allow_guest=True)
def get(ip_address: str) -> dict:
"""Returns the blacklist for the given IP address."""

if not ip_address:
frappe.throw(_("IP address is required."), frappe.MandatoryError)

return get_blacklist_for_ip_address(ip_address, create_if_not_exists=True, commit=True) or {}
Loading

0 comments on commit ae24819

Please sign in to comment.