From 3b1698b4dbba80a68fb8f66e20bcff5860a6db01 Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Tue, 5 May 2026 13:42:30 +0530 Subject: [PATCH 01/55] refactor: generate `config.json` --- .../doctype/mail_cluster/mail_cluster.js | 155 ++-- .../doctype/mail_cluster/mail_cluster.json | 823 ++++++------------ .../doctype/mail_cluster/mail_cluster.py | 221 ++--- 3 files changed, 386 insertions(+), 813 deletions(-) diff --git a/mail/server/doctype/mail_cluster/mail_cluster.js b/mail/server/doctype/mail_cluster/mail_cluster.js index bf7b578bf..8e56abb54 100644 --- a/mail/server/doctype/mail_cluster/mail_cluster.js +++ b/mail/server/doctype/mail_cluster/mail_cluster.js @@ -1,88 +1,51 @@ // Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -const STORES_PRESET = { - RocksDB: { - store_id: 'rocksdb', - path: '/opt/stalwart/data', - compression: 'LZ4', - min_blob_size: 16834, - write_buffer_size: 128, - purge_frequency: '0 3 * * *', - }, - FoundationDB: { - store_id: 'foundationdb', - compression: 'LZ4', - purge_frequency: '0 3 * * *', - }, - PostgreSQL: { - store_id: 'postgresql', - port: 5432, - database: 'frappemail', - timeout: 15, - user: 'frappemail', - compression: 'LZ4', - purge_frequency: '0 3 * * *', - pool_max_connections: 10, - }, - mySQL: { - store_id: 'mysql', - port: 3306, - database: 'frappemail', - timeout: 15, - user: 'frappemail', - compression: 'LZ4', - purge_frequency: '0 3 * * *', - pool_max_connections: 10, - pool_min_connections: 5, - }, - SQLite: { - store_id: 'sqlite', - path: '/var/lib/data/index.sqlite3', - compression: 'LZ4', - purge_frequency: '0 3 * * *', - pool_max_connections: 10, - }, - 'S3-compatible': { - store_id: 's3', - timeout: 15, - bucket: 'frappemail', - key_prefix: '/', - compression: 'LZ4', - max_retries: 3, - purge_frequency: '0 3 * * *', - }, - 'Redis/Memcached': { - store_id: 'redis', - redis_type: 'Redis Single Node', - urls: 'redis://127.0.0.1', - timeout: 15, - user: 'frappemail', - read_from_replicas: 1, - }, - ElasticSearch: { - store_id: 'elasticsearch', - url: 'http://localhost:9200', - user: 'frappemail', - index_shards: 3, - index_replicas: 0, - }, - 'Azure Blob Storage': { - store_id: 'azure', - timeout: 15, - storage_account: 'frappe', - container: 'mail', - key_prefix: '/', - compression: 'LZ4', - max_retries: 3, - purge_frequency: '0 3 * * *', - }, - Filesystem: { - store_id: 'filesystem', - path: '/var/lib/data/blobs', - compression: 'LZ4', - purge_frequency: '0 3 * * *', - depth: 2, +const STORE_PRESET = { + RocksDb: { + store_path: '/var/lib/stalwart/rocksdb', + store_blob_size: 16834, + store_buffer_size: 134217728, + store_pool_workers: 0, + }, + Sqlite: { + store_path: '/var/lib/stalwart/sqlite', + store_pool_workers: 0, + store_pool_max_connections: 10, + }, + FoundationDb: { + store_cluster_file: null, + store_datacenter_id: null, + store_machine_id: null, + store_transaction_retry_delay: null, + store_transaction_retry_limit: null, + store_transaction_timeout: null, + }, + PostgreSql: { + store_timeout: '15s', + store_use_tls: 0, + store_allow_invalid_certs: 0, + store_pool_max_connections: 10, + store_pool_recycling_method: 'fast', + store_host: null, + store_port: 5432, + store_database: 'frappe', + store_auth_username: null, + store_auth_secret: null, + store_options: null, + }, + MySql: { + store_timeout: '15s', + store_max_allowed_packet: 0, + store_use_tls: 0, + store_allow_invalid_certs: 0, + store_pool_min_connections: 5, + store_pool_max_connections: 10, + store_host: null, + store_port: 3306, + store_database: 'frappe', + store_auth_username: null, + store_auth_secret: null, }, } @@ -123,6 +86,13 @@ frappe.ui.form.on('Mail Cluster', { frm.trigger('add_actions') }, + store_type(frm) { + const defaults = STORE_PRESET[frm.doc.store_type] + if (defaults) { + Object.entries(defaults).forEach(([key, value]) => frm.set_value(key, value)) + } + }, + initialize_defaults(frm) { if (!frm.doc.__islocal) return @@ -190,27 +160,6 @@ frappe.ui.form.on('Mail Cluster', { }, }) -frappe.ui.form.on('Mail Cluster Store', { - type(frm, cdt, cdn) { - const row = locals[cdt][cdn] - - if (row.type) { - const defaults = STORES_PRESET[row.type] - if (defaults) { - Object.entries(defaults).forEach(([key, value]) => - frappe.model.set_value(cdt, cdn, key, value), - ) - } - } - - refresh_field('stores') - }, - - redis_type() { - refresh_field('stores') - }, -}) - frappe.ui.form.on('Mail Cluster Trace', { type(frm, cdt, cdn) { const row = locals[cdt][cdn] diff --git a/mail/server/doctype/mail_cluster/mail_cluster.json b/mail/server/doctype/mail_cluster/mail_cluster.json index b4e3266e3..c8f0579a8 100644 --- a/mail/server/doctype/mail_cluster/mail_cluster.json +++ b/mail/server/doctype/mail_cluster/mail_cluster.json @@ -16,9 +16,6 @@ "column_break_9jti", "base_url", "api_key", - "proxy_section", - "server_proxy_trusted_networks", - "column_break_egzt", "networking_section", "ipv4_addresses", "column_break_uhc3", @@ -27,82 +24,38 @@ "ssh_key_section", "ssh_public_key", "ssh_private_key", - "section_break_r4np", - "stores_section", - "stores", - "directory_storage_section", - "storage_directory", - "data_storage_section", - "storage_data", - "column_break_iwwx", - "email_encryption_enable", - "email_encryption_append", - "blob_storage_section", - "storage_blob", - "column_break_xupm", - "storage_undelete_retention", - "search_store_section", - "storage_fts", - "storage_search_index_default_language", - "storage_search_index_batch_size", - "column_break_a9m0", - "storage_search_index_email_enable", - "storage_search_index_contacts_enable", - "storage_search_index_calendar_enable", - "storage_search_index_tracing_enable", - "in_memory_storage_section", - "storage_lookup", - "cleanup_section", - "account_purge_frequency", - "column_break_ld4b", - "changes_max_history", - "email_auto_expunge", - "listeners_tab", - "listeners", - "jmap_tab", - "push_subscription_section", - "jmap_push_throttle", - "jmap_push_retry_interval", - "column_break_0q0i", - "jmap_push_attempts_max", - "jmap_push_attempts_interval", - "push_timeouts_section", - "jmap_push_timeout_request", - "column_break_68lj", - "jmap_push_timeout_verify", - "request_limits_section", - "jmap_protocol_request_max_concurrent", - "column_break_rc7k", - "jmap_protocol_request_max_size", - "jmap_protocol_request_max_calls", - "max_objects_section", - "jmap_protocol_get_max_objects", - "column_break_rpmh", - "jmap_protocol_set_max_objects", - "max_results_section", - "jmap_protocol_query_max_results", - "column_break_ebem", - "jmap_protocol_changes_max_results", - "upload_limits_section", - "jmap_protocol_upload_max_size", - "jmap_protocol_upload_max_concurrent", - "jmap_protocol_upload_ttl", - "column_break_iqbf", - "jmap_protocol_upload_quota_files", - "jmap_protocol_upload_quota_size", - "mailbox_limits_section", - "jmap_mailbox_max_depth", - "column_break_dxmn", - "jmap_mailbox_max_name_length", - "email_limits_section", - "jmap_email_max_attachment_size", - "column_break_wuz8", - "jmap_email_max_size", - "parsing_limits_section", - "jmap_email_parse_max_items", - "column_break_cccs", - "jmap_calendar_parse_max_items", - "jmap_contact_parse_max_items", + "store_tab", + "store_type", + "column_break_yhyn", + "store_path", + "store_blob_size", + "store_buffer_size", + "section_break_plrk", + "store_timeout", + "store_max_allowed_packet", + "store_use_tls", + "store_allow_invalid_certs", + "column_break_djvr", + "store_pool_workers", + "store_pool_min_connections", + "store_pool_max_connections", + "store_pool_recycling_method", + "section_break_bpyx", + "store_cluster_file", + "store_datacenter_id", + "store_machine_id", + "column_break_dsjl", + "store_transaction_retry_delay", + "store_transaction_retry_limit", + "store_transaction_timeout", + "section_break_ntwb", + "store_host", + "store_port", + "store_database", + "column_break_grwu", + "store_auth_username", + "store_auth_secret", + "store_options", "log_metrics_tab", "section_break_rsr9", "traces", @@ -116,7 +69,9 @@ "metrics_prometheus_enable", "column_break_ozrd", "metrics_prometheus_auth_username", - "metrics_prometheus_auth_secret" + "metrics_prometheus_auth_secret", + "config_tab", + "config" ], "fields": [ { @@ -134,23 +89,6 @@ "fieldname": "column_break_fkha", "fieldtype": "Column Break" }, - { - "fieldname": "section_break_r4np", - "fieldtype": "Tab Break", - "label": "Storage" - }, - { - "fieldname": "column_break_iwwx", - "fieldtype": "Column Break" - }, - { - "fieldname": "column_break_a9m0", - "fieldtype": "Column Break" - }, - { - "fieldname": "column_break_ld4b", - "fieldtype": "Column Break" - }, { "fieldname": "details_tab", "fieldtype": "Tab Break", @@ -180,20 +118,6 @@ "no_copy": 1, "placeholder": "https://mail.frappemail.com/" }, - { - "fieldname": "stores_section", - "fieldtype": "Section Break", - "label": "Stores" - }, - { - "description": "Manage data, blob, full-text, and lookup stores.", - "fieldname": "stores", - "fieldtype": "Table", - "label": "Stores", - "no_copy": 1, - "options": "Mail Cluster Store", - "reqd": 1 - }, { "fieldname": "networking_section", "fieldtype": "Section Break", @@ -219,169 +143,10 @@ "no_copy": 1, "read_only": 1 }, - { - "description": "Manage SMTP, IMAP, HTTP, and other listeners.", - "fieldname": "listeners", - "fieldtype": "Table", - "label": "Listeners", - "options": "Mail Server Listener", - "reqd": 1 - }, - { - "fieldname": "listeners_tab", - "fieldtype": "Tab Break", - "label": "Listeners" - }, { "fieldname": "section_break_b7jt", "fieldtype": "Section Break" }, - { - "description": "Stores user accounts, passwords, email addresses, and settings. Used for authentication, email validation, and account management.", - "fieldname": "directory_storage_section", - "fieldtype": "Section Break", - "label": "Directory Storage" - }, - { - "description": "Core storage unit where email metadata, folders, and various settings are stored. Essentially, it contains all the data except for large binary objects (blobs).", - "fieldname": "data_storage_section", - "fieldtype": "Section Break", - "label": "Data Storage" - }, - { - "description": "Used for storing large binary objects such as emails, sieve scripts, and other files.", - "fieldname": "blob_storage_section", - "fieldtype": "Section Break", - "label": "Blob Storage" - }, - { - "description": "Key-value storage used primarily by the SMTP server and anti-spam components.", - "fieldname": "in_memory_storage_section", - "fieldtype": "Section Break", - "label": "In-Memory Storage" - }, - { - "fieldname": "proxy_section", - "fieldtype": "Section Break", - "label": "Proxy" - }, - { - "fieldname": "column_break_egzt", - "fieldtype": "Column Break" - }, - { - "fieldname": "jmap_tab", - "fieldtype": "Tab Break", - "label": "JMAP" - }, - { - "fieldname": "push_subscription_section", - "fieldtype": "Section Break", - "label": "Push Subscription" - }, - { - "fieldname": "push_timeouts_section", - "fieldtype": "Section Break", - "label": "Push Timeouts" - }, - { - "default": "1000", - "description": "Time to wait between retry attempts.", - "fieldname": "jmap_push_retry_interval", - "fieldtype": "Int", - "label": "Retry Interval (Milliseconds)", - "non_negative": 1, - "reqd": 1 - }, - { - "fieldname": "column_break_0q0i", - "fieldtype": "Column Break" - }, - { - "fieldname": "column_break_68lj", - "fieldtype": "Column Break" - }, - { - "default": "1000", - "description": "Time to wait before sending a new request to the push service.", - "fieldname": "jmap_push_throttle", - "fieldtype": "Int", - "label": "Throttle (Milliseconds)", - "non_negative": 1, - "reqd": 1 - }, - { - "default": "3", - "description": "Maximum number of push attempts before a notification is discarded.", - "fieldname": "jmap_push_attempts_max", - "fieldtype": "Int", - "label": "Max Attempts", - "non_negative": 1, - "reqd": 1 - }, - { - "default": "60000", - "description": "Time to wait between push attempts.", - "fieldname": "jmap_push_attempts_interval", - "fieldtype": "Int", - "label": "Attempt Interval (Milliseconds)", - "non_negative": 1, - "reqd": 1 - }, - { - "default": "10000", - "description": "Time before a connection with a push service URL times out.", - "fieldname": "jmap_push_timeout_request", - "fieldtype": "Int", - "label": "Request Timeout (Milliseconds)", - "non_negative": 1, - "reqd": 1 - }, - { - "default": "60000", - "description": "Time to wait for the push service to verify a subscription.", - "fieldname": "jmap_push_timeout_verify", - "fieldtype": "Int", - "label": "Verification Timeout (Milliseconds)", - "non_negative": 1, - "reqd": 1 - }, - { - "description": "Enable proxy protocol for connections from these networks.", - "fieldname": "server_proxy_trusted_networks", - "fieldtype": "Small Text", - "label": "Trusted Networks" - }, - { - "fieldname": "storage_directory", - "fieldtype": "Data", - "label": "Storage", - "reqd": 1 - }, - { - "fieldname": "storage_data", - "fieldtype": "Data", - "label": "Storage", - "reqd": 1 - }, - { - "fieldname": "storage_blob", - "fieldtype": "Data", - "label": "Storage", - "reqd": 1 - }, - { - "fieldname": "storage_fts", - "fieldtype": "Data", - "label": "Storage", - "reqd": 1 - }, - { - "fieldname": "storage_lookup", - "fieldtype": "Data", - "label": "Storage", - "reqd": 1 - }, { "default": "frappe", "description": "Username for administrative access to the cluster.", @@ -418,256 +183,6 @@ "set_only_once": 1, "unique": 1 }, - { - "fieldname": "request_limits_section", - "fieldtype": "Section Break", - "label": "Request Limits" - }, - { - "fieldname": "column_break_rc7k", - "fieldtype": "Column Break" - }, - { - "fieldname": "max_objects_section", - "fieldtype": "Section Break", - "label": "Max Objects" - }, - { - "fieldname": "column_break_rpmh", - "fieldtype": "Column Break" - }, - { - "fieldname": "max_results_section", - "fieldtype": "Section Break", - "label": "Max Results" - }, - { - "fieldname": "column_break_ebem", - "fieldtype": "Column Break" - }, - { - "fieldname": "upload_limits_section", - "fieldtype": "Section Break", - "label": "Upload Limits" - }, - { - "fieldname": "column_break_iqbf", - "fieldtype": "Column Break" - }, - { - "fieldname": "mailbox_limits_section", - "fieldtype": "Section Break", - "label": "Mailbox Limits" - }, - { - "fieldname": "column_break_dxmn", - "fieldtype": "Column Break" - }, - { - "fieldname": "email_limits_section", - "fieldtype": "Section Break", - "label": "Email Limits" - }, - { - "fieldname": "column_break_wuz8", - "fieldtype": "Column Break" - }, - { - "default": "40", - "description": "Restricts the number of concurrent requests a user can make to the JMAP server.", - "fieldname": "jmap_protocol_request_max_concurrent", - "fieldtype": "Int", - "label": "Concurrent", - "non_negative": 1, - "reqd": 1 - }, - { - "default": "10000000", - "description": "Defines the maximum size of a single request, in bytes, that the server will accept.", - "fieldname": "jmap_protocol_request_max_size", - "fieldtype": "Int", - "label": "Size (Bytes)", - "non_negative": 1, - "reqd": 1 - }, - { - "default": "16", - "description": "Limits the maximum number of method calls that can be included in a single request.", - "fieldname": "jmap_protocol_request_max_calls", - "fieldtype": "Int", - "label": "Method Calls", - "non_negative": 1, - "reqd": 1 - }, - { - "default": "500", - "description": "Determines the maximum number of objects that can be fetched in a single method call.", - "fieldname": "jmap_protocol_get_max_objects", - "fieldtype": "Int", - "label": "Get", - "non_negative": 1, - "reqd": 1 - }, - { - "default": "500", - "description": "Establishes the maximum number of objects that can be modified in a single method call.", - "fieldname": "jmap_protocol_set_max_objects", - "fieldtype": "Int", - "label": "Set", - "non_negative": 1, - "reqd": 1 - }, - { - "default": "5000", - "description": "Sets the maximum number of results that a Query method can return.", - "fieldname": "jmap_protocol_query_max_results", - "fieldtype": "Int", - "label": "Query", - "non_negative": 1, - "reqd": 1 - }, - { - "default": "5000", - "description": "Determines the maximum number of change objects that a Changes method can return.", - "fieldname": "jmap_protocol_changes_max_results", - "fieldtype": "Int", - "label": "Changes", - "non_negative": 1, - "reqd": 1 - }, - { - "default": "50000000", - "description": "Defines the maximum file size for file uploads to the server.", - "fieldname": "jmap_protocol_upload_max_size", - "fieldtype": "Int", - "label": "Max Size (Bytes)", - "non_negative": 1, - "reqd": 1 - }, - { - "default": "40", - "description": "Restricts the number of concurrent file uploads a user can perform.", - "fieldname": "jmap_protocol_upload_max_concurrent", - "fieldtype": "Int", - "label": "Max Concurrent", - "non_negative": 1, - "reqd": 1 - }, - { - "default": "1", - "description": "Specifies the Time-To-Live (TTL) for each uploaded file, after which the file is deleted from temporary storage.", - "fieldname": "jmap_protocol_upload_ttl", - "fieldtype": "Int", - "label": "Expire After (Hours)", - "non_negative": 1, - "reqd": 1 - }, - { - "default": "144000", - "description": "Specifies the maximum number of files that a user can upload within a certain period.", - "fieldname": "jmap_protocol_upload_quota_files", - "fieldtype": "Int", - "label": "Total Files", - "non_negative": 1, - "reqd": 1 - }, - { - "default": "5120", - "description": "Defines the total size of files that a user can upload within a certain period.", - "fieldname": "jmap_protocol_upload_quota_size", - "fieldtype": "Int", - "label": "Total Size (MB)", - "non_negative": 1, - "reqd": 1 - }, - { - "default": "10", - "description": "Restricts the maximum depth of nested mailboxes a user can create.", - "fieldname": "jmap_mailbox_max_depth", - "fieldtype": "Int", - "label": "Max Depth", - "non_negative": 1, - "reqd": 1 - }, - { - "default": "255", - "description": "Establishes the maximum length of a mailbox name.", - "fieldname": "jmap_mailbox_max_name_length", - "fieldtype": "Int", - "label": "Name Length", - "non_negative": 1, - "reqd": 1 - }, - { - "default": "50000000", - "description": "Specifies the maximum size for an email attachment.", - "fieldname": "jmap_email_max_attachment_size", - "fieldtype": "Int", - "label": "Attachment Size (Bytes)", - "non_negative": 1, - "reqd": 1 - }, - { - "default": "75000000", - "description": "Determines the maximum size for an email message.", - "fieldname": "jmap_email_max_size", - "fieldtype": "Int", - "label": "E-mail Size (Bytes)", - "non_negative": 1, - "reqd": 1 - }, - { - "default": "10", - "description": "Limits the maximum number of e-mail message that can be parsed in a single request.", - "fieldname": "jmap_email_parse_max_items", - "fieldtype": "Int", - "label": "Emails", - "non_negative": 1, - "reqd": 1 - }, - { - "default": "0 0 * * *", - "description": "Specifies how often tombstoned messages are deleted from the database.\n\n
\n
\n\n
*  *  *  *  *\n\u252c  \u252c  \u252c  \u252c  \u252c\n\u2502  \u2502  \u2502  \u2502  \u2502\n\u2502  \u2502  \u2502  \u2502  \u2514 day of week (0 - 6) (0 is Sunday)\n\u2502  \u2502  \u2502  \u2514\u2500\u2500\u2500\u2500\u2500 month (1 - 12)\n\u2502  \u2502  \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 day of month (1 - 31)\n\u2502  \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 hour (0 - 23)\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 minute (0 - 59)\n\n---\n\n* - Any value\n/ - Step values\n
\n", - "fieldname": "account_purge_frequency", - "fieldtype": "Data", - "label": "Frequency (Cron)", - "reqd": 1 - }, - { - "default": "30", - "description": "How long to keep messages in the Trash and Junk Mail folders before auto-expunging.", - "fieldname": "email_auto_expunge", - "fieldtype": "Int", - "label": "Trash Auto-Expunge (Days)", - "non_negative": 1 - }, - { - "default": "10000", - "description": "How many changes to keep in the history for each account. This is used to determine the changes that have occurred since the last time the client requested changes.", - "fieldname": "changes_max_history", - "fieldtype": "Int", - "label": "Changes History", - "non_negative": 1 - }, - { - "default": "1", - "description": "Allow users to configure encryption at rest for their data.", - "fieldname": "email_encryption_enable", - "fieldtype": "Check", - "label": "Enable encryption at rest" - }, - { - "default": "0", - "description": "Encrypt messages that are manually appended by the user using JMAP or IMAP.", - "fieldname": "email_encryption_append", - "fieldtype": "Check", - "label": "Encrypt on append" - }, - { - "fieldname": "cleanup_section", - "fieldtype": "Section Break", - "label": "Cleanup" - }, { "fieldname": "log_metrics_tab", "fieldtype": "Tab Break", @@ -789,95 +304,247 @@ "label": "SSH Key" }, { - "fieldname": "parsing_limits_section", - "fieldtype": "Section Break", - "label": "Parsing Limits" + "fieldname": "config_tab", + "fieldtype": "Tab Break", + "label": "Config" + }, + { + "depends_on": "eval: !doc.__islocal && doc.config && doc.config != \"{}\"", + "fieldname": "config", + "fieldtype": "JSON", + "is_virtual": 1, + "label": "Config", + "no_copy": 1, + "read_only": 1 }, { - "fieldname": "column_break_cccs", + "fieldname": "section_break_bpyx", + "fieldtype": "Section Break" + }, + { + "fieldname": "column_break_dsjl", "fieldtype": "Column Break" }, { - "default": "10", - "description": "Limits the maximum number of vCard items that can be parsed in a single request.", - "fieldname": "jmap_contact_parse_max_items", - "fieldtype": "Int", - "label": "Contacts", - "non_negative": 1, - "reqd": 1 + "fieldname": "section_break_plrk", + "fieldtype": "Section Break" }, { - "default": "10", - "description": "Limits the maximum number of iCalendar items that can be parsed in a single request.", - "fieldname": "jmap_calendar_parse_max_items", - "fieldtype": "Int", - "label": "Calendars", - "non_negative": 1, - "reqd": 1 + "fieldname": "column_break_djvr", + "fieldtype": "Column Break" }, { - "fieldname": "column_break_xupm", + "fieldname": "section_break_ntwb", + "fieldtype": "Section Break" + }, + { + "fieldname": "column_break_grwu", "fieldtype": "Column Break" }, { - "default": "0", - "description": "How long to keep deleted emails before they are permanently removed from the system.", - "fieldname": "storage_undelete_retention", + "fieldname": "column_break_yhyn", + "fieldtype": "Column Break" + }, + { + "depends_on": "eval: [\"PostgreSql\"].includes(doc.store_type)", + "description": "Additional connection options.", + "fieldname": "store_options", + "fieldtype": "Data", + "label": "Options" + }, + { + "depends_on": "eval: [\"PostgreSql\", \"MySql\"].includes(doc.store_type)", + "description": "Hostname of the database server.", + "fieldname": "store_host", + "fieldtype": "Data", + "label": "Host", + "mandatory_depends_on": "eval: [\"PostgreSql\", \"MySql\"].includes(doc.store_type)" + }, + { + "depends_on": "eval: [\"PostgreSql\", \"MySql\"].includes(doc.store_type)", + "description": "Port of the database server.", + "fieldname": "store_port", "fieldtype": "Int", - "label": "Un-delete Period (Days)", - "non_negative": 1, + "label": "Port", + "mandatory_depends_on": "eval: [\"PostgreSql\", \"MySql\"].includes(doc.store_type)" + }, + { + "default": "frappe", + "depends_on": "eval: [\"PostgreSql\", \"MySql\"].includes(doc.store_type)", + "description": "Name of the database.", + "fieldname": "store_database", + "fieldtype": "Data", + "label": "Database", + "mandatory_depends_on": "eval: [\"PostgreSql\", \"MySql\"].includes(doc.store_type)" + }, + { + "description": "Configures the primary data store backend.", + "fieldname": "store_type", + "fieldtype": "Select", + "label": "Type", + "options": "\nRocksDb\nSqlite\nFoundationDb\nPostgreSql\nMySql", "reqd": 1 }, { - "description": "Dedicated to indexing for full-text search, enhancing the speed and efficiency of text-based queries.", - "fieldname": "search_store_section", - "fieldtype": "Section Break", - "label": "Search Store" + "depends_on": "eval: [\"RocksDb\", \"Sqlite\"].includes(doc.store_type)", + "description": "Path to the data directory", + "fieldname": "store_path", + "fieldtype": "Data", + "label": "Path", + "mandatory_depends_on": "eval: [\"RocksDb\", \"Sqlite\"].includes(doc.store_type)" + }, + { + "default": "16834", + "depends_on": "eval: [\"RocksDb\"].includes(doc.store_type)", + "description": "Minimum size of a blob to store in the blob store, smaller blobs are stored in the metadata store.", + "fieldname": "store_blob_size", + "fieldtype": "Int", + "label": "Blob Size", + "mandatory_depends_on": "eval: [\"RocksDb\"].includes(doc.store_type)" }, { - "default": "en", - "description": "Default language to use when language detection is not possible.", - "fieldname": "storage_search_index_default_language", + "default": "134217728", + "depends_on": "eval: [\"RocksDb\"].includes(doc.store_type)", + "description": "Size of the write buffer in bytes, used to batch writes to the store.", + "fieldname": "store_buffer_size", + "fieldtype": "Int", + "label": "Buffer Size", + "mandatory_depends_on": "eval: [\"RocksDb\"].includes(doc.store_type)" + }, + { + "default": "15s", + "depends_on": "eval: [\"PostgreSql\", \"MySql\"].includes(doc.store_type)", + "description": "Connection timeout to the database.", + "fieldname": "store_timeout", "fieldtype": "Data", - "label": "Default Language", - "reqd": 1 + "label": "Timeout", + "mandatory_depends_on": "eval: [\"PostgreSql\", \"MySql\"].includes(doc.store_type)" }, { - "default": "1", - "description": "Enable full-text search indexing for email content and metadata.", - "fieldname": "storage_search_index_email_enable", - "fieldtype": "Check", - "label": "Enable Email Searching" + "depends_on": "eval: [\"MySql\"].includes(doc.store_type)", + "description": "Maximum size of a packet in bytes.", + "fieldname": "store_max_allowed_packet", + "fieldtype": "Int", + "label": "Max Allowed Packet", + "mandatory_depends_on": "eval: [\"MySql\"].includes(doc.store_type)" }, { - "default": "1", - "description": "Enable full-text search indexing for calendar data.", - "fieldname": "storage_search_index_contacts_enable", + "default": "0", + "depends_on": "eval: [\"PostgreSql\", \"MySql\"].includes(doc.store_type)", + "description": "Use TLS to connect to the store.", + "fieldname": "store_use_tls", "fieldtype": "Check", - "label": "Enable Calendar Searching" + "label": "Use TLS" }, { - "default": "1", - "description": "Enable full-text search indexing for contacts data.", - "fieldname": "storage_search_index_calendar_enable", + "default": "0", + "depends_on": "eval: [\"PostgreSql\", \"MySql\"].includes(doc.store_type)", + "description": "Allow invalid TLS certificates when connecting to the store.", + "fieldname": "store_allow_invalid_certs", "fieldtype": "Check", - "label": "Enable Contacts Searching" + "label": "Allow Invalid Certs" }, { - "default": "1", - "description": "Enable full-text search indexing for tracing data.", - "fieldname": "storage_search_index_tracing_enable", - "fieldtype": "Check", - "label": "Enable Tracing Searching" + "depends_on": "eval: [\"RocksDb\", \"Sqlite\"].includes(doc.store_type)", + "description": "Number of worker threads to use for the store, defaults to the number of cores.", + "fieldname": "store_pool_workers", + "fieldtype": "Int", + "label": "Pool Workers", + "mandatory_depends_on": "eval: [\"RocksDb\", \"Sqlite\"].includes(doc.store_type)" }, { - "default": "100", - "description": "Number of items to process in each batch during indexing operations.", - "fieldname": "storage_search_index_batch_size", + "default": "5", + "depends_on": "eval: [\"MySql\"].includes(doc.store_type)", + "description": "Minimum number of connections to the store.", + "fieldname": "store_pool_min_connections", "fieldtype": "Int", - "label": "Indexing Batch Size", - "non_negative": 1, - "reqd": 1 + "label": "Pool Min Connections", + "mandatory_depends_on": "eval: [\"MySql\"].includes(doc.store_type)" + }, + { + "default": "10", + "depends_on": "eval: [\"Sqlite\", \"PostgreSql\", \"MySql\"].includes(doc.store_type)", + "description": "Maximum number of connections to the store.", + "fieldname": "store_pool_max_connections", + "fieldtype": "Int", + "label": "Pool Max Connections", + "mandatory_depends_on": "eval: [\"Sqlite\", \"PostgreSql\", \"MySql\"].includes(doc.store_type)" + }, + { + "default": "fast", + "depends_on": "eval: [\"PostgreSql\"].includes(doc.store_type)", + "description": "Method to use when recycling connections in the pool.", + "fieldname": "store_pool_recycling_method", + "fieldtype": "Select", + "label": "Pool Recycling Method", + "mandatory_depends_on": "eval: [\"PostgreSql\"].includes(doc.store_type)", + "options": "fast\nverified\nclean" + }, + { + "depends_on": "eval: [\"FoundationDb\"].includes(doc.store_type)", + "description": "Path to the cluster file for the FoundationDB cluster.", + "fieldname": "store_cluster_file", + "fieldtype": "Data", + "label": "Cluster File", + "mandatory_depends_on": "eval: [\"FoundationDb\"].includes(doc.store_type)" + }, + { + "depends_on": "eval: [\"FoundationDb\"].includes(doc.store_type)", + "description": "Data center ID.", + "fieldname": "store_datacenter_id", + "fieldtype": "Data", + "label": "Datacenter ID" + }, + { + "depends_on": "eval: [\"FoundationDb\"].includes(doc.store_type)", + "description": "Machine ID in the FoundationDB cluster.", + "fieldname": "store_machine_id", + "fieldtype": "Data", + "label": "Machine ID" + }, + { + "depends_on": "eval: [\"FoundationDb\"].includes(doc.store_type)", + "description": "Transaction maximum retry delay.", + "fieldname": "store_transaction_retry_delay", + "fieldtype": "Data", + "label": "Transaction Retry Delay", + "mandatory_depends_on": "eval: [\"FoundationDb\"].includes(doc.store_type)" + }, + { + "depends_on": "eval: [\"FoundationDb\"].includes(doc.store_type)", + "description": "Transaction retry limit.", + "fieldname": "store_transaction_retry_limit", + "fieldtype": "Int", + "label": "Transaction Retry Limit", + "mandatory_depends_on": "eval: [\"FoundationDb\"].includes(doc.store_type)" + }, + { + "depends_on": "eval: [\"FoundationDb\"].includes(doc.store_type)", + "description": "Transaction timeout.", + "fieldname": "store_transaction_timeout", + "fieldtype": "Data", + "label": "Transaction Timeout", + "mandatory_depends_on": "eval: [\"FoundationDb\"].includes(doc.store_type)" + }, + { + "default": "frappe", + "depends_on": "eval: [\"PostgreSql\", \"MySql\"].includes(doc.store_type)", + "description": "Username to connect to the store.", + "fieldname": "store_auth_username", + "fieldtype": "Data", + "label": "Auth Username" + }, + { + "depends_on": "eval: [\"PostgreSql\", \"MySql\"].includes(doc.store_type)", + "description": "Password to connect to the store.", + "fieldname": "store_auth_secret", + "fieldtype": "Password", + "label": "Auth Secret" + }, + { + "fieldname": "store_tab", + "fieldtype": "Tab Break", + "label": "Store" } ], "grid_page_length": 50, @@ -889,7 +556,7 @@ "link_fieldname": "cluster" } ], - "modified": "2026-04-27 09:50:02.241468", + "modified": "2026-05-05 14:39:04.316477", "modified_by": "Administrator", "module": "Server", "name": "Mail Cluster", diff --git a/mail/server/doctype/mail_cluster/mail_cluster.py b/mail/server/doctype/mail_cluster/mail_cluster.py index f8c015241..891767473 100644 --- a/mail/server/doctype/mail_cluster/mail_cluster.py +++ b/mail/server/doctype/mail_cluster/mail_cluster.py @@ -3,57 +3,19 @@ import base64 import io +import json import frappe import paramiko from frappe import _ from frappe.model.document import Document -from frappe.utils import random_string +from frappe.utils import cint, random_string from mail.backend import MailBackendAPI, Principal from mail.jmap.connection import raise_for_status from mail.utils import generate_secret, hash_password from mail.utils.dns import get_dns_record -from mail.utils.validation import is_valid_cron_expression -DEFAULT_STORES = [ - { - "type": "RocksDB", - "store_id": "rocksdb", - "path": "/opt/stalwart/data", - "compression": "LZ4", - "min_blob_size": 16834, - "write_buffer_size": 128, - "purge_frequency": "0 3 * * *", - } -] -DEFAULT_LISTENERS = [ - {"protocol": "HTTP", "listener_id": "http", "bind": "[::]:8080", "tls_implicit": 0}, - {"protocol": "HTTP", "listener_id": "https", "bind": "[::]:443", "tls_implicit": 1}, - {"protocol": "IMAP4", "listener_id": "imap", "bind": "[::]:143", "tls_implicit": 0}, - {"protocol": "IMAP4", "listener_id": "imaptls", "bind": "[::]:993", "tls_implicit": 1}, - {"protocol": "POP3", "listener_id": "pop3", "bind": "[::]:110", "tls_implicit": 0}, - {"protocol": "POP3", "listener_id": "pop3s", "bind": "[::]:995", "tls_implicit": 1}, - { - "protocol": "ManageSieve", - "listener_id": "sieve", - "bind": "[::]:4190", - "tls_implicit": 0, - }, - {"protocol": "SMTP", "listener_id": "smtp", "bind": "[::]:25", "tls_implicit": 0}, - { - "protocol": "SMTP", - "listener_id": "submission", - "bind": "[::]:587\n[::]:8025", - "tls_implicit": 0, - }, - { - "protocol": "SMTP", - "listener_id": "submissions", - "bind": "[::]:465\n[::]:2525", - "tls_implicit": 1, - }, -] DEFAULT_TRACES = [ { "tracer_id": "log", @@ -64,25 +26,85 @@ "rotate": "Daily", } ] -STORAGE_OPTIONS = { - "storage_directory": ["RocksDB", "FoundationDB", "PostgreSQL", "mySQL", "SQLite"], - "storage_data": ["RocksDB", "FoundationDB", "PostgreSQL", "mySQL", "SQLite"], - "storage_blob": [ - "RocksDB", - "FoundationDB", - "PostgreSQL", - "mySQL", - "SQLite", - "S3-compatible", - "Azure Blob Storage", - "Filesystem", - ], - "storage_fts": ["RocksDB", "FoundationDB", "PostgreSQL", "mySQL", "SQLite", "ElasticSearch"], - "storage_lookup": ["RocksDB", "FoundationDB", "PostgreSQL", "mySQL", "SQLite", "Redis/Memcached"], -} class MailCluster(Document): + @property + def config(self) -> str: + """Returns the configuration for the cluster.""" + + if not self.store_type: + return "{}" + + config = {"@type": self.store_type} + + if self.store_type == "RocksDb": + config.update( + { + "path": self.store_path, + "blobSize": cint(self.store_blob_size), + "bufferSize": cint(self.store_buffer_size), + "poolWorkers": cint(self.store_pool_workers), + } + ) + + elif self.store_type == "Sqlite": + config.update( + { + "path": self.store_path, + "poolWorkers": cint(self.store_pool_workers), + "poolMaxConnections": cint(self.store_pool_max_connections), + } + ) + + elif self.store_type == "FoundationDb": + config.update( + { + "clusterFile": self.store_cluster_file, + "datacenterId": self.store_datacenter_id, + "machineId": self.store_machine_id, + "transactionRetryDelay": cint(self.store_transaction_retry_delay), + "transactionRetryLimit": cint(self.store_transaction_retry_limit), + "transactionTimeout": cint(self.store_transaction_timeout), + } + ) + + elif self.store_type == "PostgreSql": + config.update( + { + "timeout": cint(self.store_timeout), + "useTls": cint(self.store_use_tls), + "allowInvalidCerts": cint(self.store_allow_invalid_certs), + "poolMaxConnections": cint(self.store_pool_max_connections), + "poolRecyclingMethod": self.store_pool_recycling_method, + "host": self.store_host, + "port": cint(self.store_port), + "database": self.store_database, + "authUsername": self.store_auth_username, + "authSecret": self.get_password("store_auth_secret") if self.store_auth_secret else None, + "options": self.store_options, + } + ) + + elif self.store_type == "MySql": + config.update( + { + "timeout": cint(self.store_timeout), + "useTls": cint(self.store_use_tls), + "allowInvalidCerts": cint(self.store_allow_invalid_certs), + "maxAllowedPacket": cint(self.store_max_allowed_packet), + "poolMaxConnections": cint(self.store_pool_max_connections), + "poolMinConnections": cint(self.store_pool_min_connections), + "host": self.store_host, + "port": cint(self.store_port), + "database": self.store_database, + "authUsername": self.store_auth_username, + "authSecret": self.get_password("store_auth_secret") if self.store_auth_secret else None, + } + ) + + return json.dumps(config, indent=4) + def autoname(self) -> None: self.hostname = self.hostname.lower() self.name = self.hostname @@ -93,9 +115,6 @@ def validate(self) -> None: self.validate_fallback_admin_password() self.generate_fallback_admin_secret() self.validate_base_url() - self.validate_stores() - self.validate_storage() - self.validate_listeners() self.validate_traces() def before_insert(self) -> None: @@ -126,6 +145,8 @@ def validate_hostname(self) -> None: self.ipv6_addresses = "\n".join([r.address for r in get_dns_record(self.hostname, "AAAA") or []]) def validate_fallback_admin_password(self) -> None: + """Validates the fallback admin password.""" + if self.fallback_admin_password: if len(self.fallback_admin_password) < 16: frappe.throw(_("Password must be at least 16 characters long.")) @@ -144,58 +165,6 @@ def validate_base_url(self) -> None: if not self.base_url: self.base_url = f"https://{self.hostname}/" - def validate_stores(self) -> None: - """Validates the stores.""" - - store_ids = [] - for store in self.stores: - if store.store_id in store_ids: - frappe.throw( - _("Row #{0}: Store ID {1} is duplicated.").format(store.idx, frappe.bold(store.store_id)) - ) - - store_ids.append(store.store_id) - - if store.purge_frequency: - is_valid_cron_expression(store.purge_frequency, raise_exception=True) - - def validate_storage(self) -> None: - """Validates the selected stores against the stores.""" - - stores = {store.store_id: store for store in self.stores} - storage_labels = get_storage_labels() - - for key in STORAGE_OPTIONS.keys(): - selected_storage = getattr(self, key) - if selected_storage not in stores: - frappe.throw(_("Store with Store ID {0} not found.").format(frappe.bold(selected_storage))) - - store = stores[selected_storage] - if store.type not in STORAGE_OPTIONS[key]: - frappe.throw( - _("{0} has an invalid store type '{1}'. Allowed types are: {2}.").format( - frappe.bold(storage_labels[key]), - frappe.bold(store.type), - ", ".join(STORAGE_OPTIONS[key]), - ) - ) - - is_valid_cron_expression(self.account_purge_frequency, raise_exception=True) - - def validate_listeners(self) -> None: - """Validates the listeners.""" - - listener_ids = [] - for listener in self.listeners: - if listener.listener_id in listener_ids: - frappe.throw( - _("Row #{0}: Listener ID {1} is duplicated.").format( - listener.idx, frappe.bold(listener.listener_id) - ) - ) - - listener_ids.append(listener.listener_id) - def validate_traces(self) -> None: """Validates the traces.""" @@ -226,30 +195,18 @@ def generate_ssh_keypair(self, save: bool = False) -> None: def initialize_defaults(self) -> None: """Initializes the default values.""" - self.initialize_default_stores() - self.initialize_default_listeners() + self.initialize_store() self.initialize_default_traces() - def initialize_default_stores(self) -> None: - """Initializes the default stores.""" - - self.stores = [] - for store in DEFAULT_STORES: - self.append("stores", store) - - if len(self.stores) == 1: - primary_store = self.stores[0] - - for field in STORAGE_OPTIONS.keys(): - if primary_store.type in STORAGE_OPTIONS[field]: - setattr(self, field, primary_store.store_id) - - def initialize_default_listeners(self) -> None: - """Initializes the default listeners.""" + def initialize_store(self) -> None: + """Initializes the default store configuration.""" - self.listeners = [] - for listener in DEFAULT_LISTENERS: - self.append("listeners", listener) + if not self.store_type: + self.store_type = "RocksDb" + self.store_path = "/var/lib/stalwart/rocksdb" + self.store_blob_size = 16834 + self.store_buffer_size = 134217728 + self.store_pool_workers = 0 def initialize_default_traces(self) -> None: """Initializes the default traces.""" From c107ce99ce4a2487ebabf60077ab66bb2650869d Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Tue, 5 May 2026 16:19:57 +0530 Subject: [PATCH 02/55] refactor: remove `Mail Cluster Trace` --- .../doctype/mail_cluster/mail_cluster.js | 45 ---- .../doctype/mail_cluster/mail_cluster.json | 108 +--------- .../doctype/mail_cluster/mail_cluster.py | 34 --- .../doctype/mail_cluster_trace/__init__.py | 0 .../mail_cluster_trace.json | 201 ------------------ .../mail_cluster_trace/mail_cluster_trace.py | 9 - 6 files changed, 1 insertion(+), 396 deletions(-) delete mode 100644 mail/server/doctype/mail_cluster_trace/__init__.py delete mode 100644 mail/server/doctype/mail_cluster_trace/mail_cluster_trace.json delete mode 100644 mail/server/doctype/mail_cluster_trace/mail_cluster_trace.py diff --git a/mail/server/doctype/mail_cluster/mail_cluster.js b/mail/server/doctype/mail_cluster/mail_cluster.js index 8e56abb54..bd2bb5fe9 100644 --- a/mail/server/doctype/mail_cluster/mail_cluster.js +++ b/mail/server/doctype/mail_cluster/mail_cluster.js @@ -49,34 +49,6 @@ const STORE_PRESET = { }, } -const TRACES_PRESET = { - 'Log file': { - tracer_id: 'log', - level: 'Info', - path: '/opt/stalwart/logs', - prefix: 'stalwart.log', - rotate: 'Daily', - }, - Console: { - tracer_id: 'console', - level: 'Info', - buffer: true, - }, - 'Systemd Journal': { - tracer_id: 'journal', - level: 'Info', - }, - 'Open Telemetry': { - tracer_id: 'otel', - level: 'Info', - transport: 'HTTP', - timeout: 10, - throttle: 1000, - enable_log_exporter: true, - enable_span_exporter: true, - }, -} - frappe.ui.form.on('Mail Cluster', { setup(frm) { frm.trigger('initialize_defaults') @@ -159,20 +131,3 @@ frappe.ui.form.on('Mail Cluster', { }) }, }) - -frappe.ui.form.on('Mail Cluster Trace', { - type(frm, cdt, cdn) { - const row = locals[cdt][cdn] - - if (row.type) { - const defaults = TRACES_PRESET[row.type] - if (defaults) { - Object.entries(defaults).forEach(([key, value]) => - frappe.model.set_value(cdt, cdn, key, value), - ) - } - } - - refresh_field('traces') - }, -}) diff --git a/mail/server/doctype/mail_cluster/mail_cluster.json b/mail/server/doctype/mail_cluster/mail_cluster.json index c8f0579a8..245de2bfc 100644 --- a/mail/server/doctype/mail_cluster/mail_cluster.json +++ b/mail/server/doctype/mail_cluster/mail_cluster.json @@ -56,20 +56,6 @@ "store_auth_username", "store_auth_secret", "store_options", - "log_metrics_tab", - "section_break_rsr9", - "traces", - "opentelemetry_push_metrics_section", - "metrics_open_telemetry_transport", - "column_break_lkbk", - "metrics_open_telemetry_endpoint", - "metrics_open_telemetry_timeout", - "metrics_open_telemetry_interval", - "prometheus_pull_metrics_section", - "metrics_prometheus_enable", - "column_break_ozrd", - "metrics_prometheus_auth_username", - "metrics_prometheus_auth_secret", "config_tab", "config" ], @@ -183,98 +169,6 @@ "set_only_once": 1, "unique": 1 }, - { - "fieldname": "log_metrics_tab", - "fieldtype": "Tab Break", - "label": "Log & Metrics" - }, - { - "fieldname": "prometheus_pull_metrics_section", - "fieldtype": "Section Break", - "label": "Prometheus Pull Metrics" - }, - { - "default": "0", - "description": "Enable the Prometheus metrics endpoint.", - "fieldname": "metrics_prometheus_enable", - "fieldtype": "Check", - "label": "Enable" - }, - { - "fieldname": "column_break_ozrd", - "fieldtype": "Column Break" - }, - { - "default": "frappe", - "description": "The Prometheus endpoint's username for Basic authentication.", - "fieldname": "metrics_prometheus_auth_username", - "fieldtype": "Data", - "label": "Username", - "mandatory_depends_on": "eval: doc.metrics_prometheus_enable" - }, - { - "description": "The Prometheus endpoint's secret for Basic authentication.", - "fieldname": "metrics_prometheus_auth_secret", - "fieldtype": "Password", - "label": "Secret", - "mandatory_depends_on": "eval: doc.metrics_prometheus_enable" - }, - { - "fieldname": "opentelemetry_push_metrics_section", - "fieldtype": "Section Break", - "label": "OpenTelemetry Push Metrics" - }, - { - "default": "Disabled", - "description": "The transport protocol for Open Telemetry.", - "fieldname": "metrics_open_telemetry_transport", - "fieldtype": "Select", - "label": "Transport", - "options": "Disabled\nHTTP\ngRPC", - "reqd": 1 - }, - { - "fieldname": "column_break_lkbk", - "fieldtype": "Column Break" - }, - { - "depends_on": "eval: [\"HTTP\", \"gRPC\"].includes(doc.metrics_open_telemetry_transport)", - "description": "The endpoint for Open Telemetry.", - "fieldname": "metrics_open_telemetry_endpoint", - "fieldtype": "Data", - "label": "Endpoint", - "mandatory_depends_on": "eval: [\"HTTP\", \"gRPC\"].includes(doc.metrics_open_telemetry_transport)", - "placeholder": "https://tracing.example.com/v1/otel" - }, - { - "default": "10", - "depends_on": "eval: [\"HTTP\", \"gRPC\"].includes(doc.metrics_open_telemetry_transport)", - "description": "Maximum amount of time that Stalwart will wait for a response from the OpenTelemetry endpoint.", - "fieldname": "metrics_open_telemetry_timeout", - "fieldtype": "Int", - "label": "Timeout (Seconds)", - "mandatory_depends_on": "eval: [\"HTTP\", \"gRPC\"].includes(doc.metrics_open_telemetry_transport)" - }, - { - "default": "30", - "depends_on": "eval: [\"HTTP\", \"gRPC\"].includes(doc.metrics_open_telemetry_transport)", - "description": "The minimum amount of time that must pass between each push request to the OpenTelemetry endpoint.", - "fieldname": "metrics_open_telemetry_interval", - "fieldtype": "Int", - "label": "Push Interval (Seconds)", - "mandatory_depends_on": "eval: [\"HTTP\", \"gRPC\"].includes(doc.metrics_open_telemetry_transport)" - }, - { - "fieldname": "section_break_rsr9", - "fieldtype": "Section Break" - }, - { - "description": "Manage logging and tracing methods.", - "fieldname": "traces", - "fieldtype": "Table", - "label": "Traces", - "options": "Mail Cluster Trace" - }, { "fieldname": "ssh_tab", "fieldtype": "Tab Break", @@ -556,7 +450,7 @@ "link_fieldname": "cluster" } ], - "modified": "2026-05-05 14:39:04.316477", + "modified": "2026-05-05 15:23:51.010624", "modified_by": "Administrator", "module": "Server", "name": "Mail Cluster", diff --git a/mail/server/doctype/mail_cluster/mail_cluster.py b/mail/server/doctype/mail_cluster/mail_cluster.py index 891767473..a14389097 100644 --- a/mail/server/doctype/mail_cluster/mail_cluster.py +++ b/mail/server/doctype/mail_cluster/mail_cluster.py @@ -16,17 +16,6 @@ from mail.utils import generate_secret, hash_password from mail.utils.dns import get_dns_record -DEFAULT_TRACES = [ - { - "tracer_id": "log", - "type": "Log file", - "level": "Info", - "path": "/opt/stalwart/logs", - "prefix": "stalwart.log", - "rotate": "Daily", - } -] - class MailCluster(Document): @property @@ -115,7 +104,6 @@ def validate(self) -> None: self.validate_fallback_admin_password() self.generate_fallback_admin_secret() self.validate_base_url() - self.validate_traces() def before_insert(self) -> None: self.generate_ssh_keypair() @@ -165,20 +153,6 @@ def validate_base_url(self) -> None: if not self.base_url: self.base_url = f"https://{self.hostname}/" - def validate_traces(self) -> None: - """Validates the traces.""" - - tracer_ids = [] - for trace in self.traces: - if trace.tracer_id in tracer_ids: - frappe.throw( - _("Row #{0}: Tracer ID {1} is duplicated.").format( - trace.idx, frappe.bold(trace.tracer_id) - ) - ) - - tracer_ids.append(trace.tracer_id) - def generate_ssh_keypair(self, save: bool = False) -> None: """Generates an SSH key pair for the cluster.""" @@ -196,7 +170,6 @@ def initialize_defaults(self) -> None: """Initializes the default values.""" self.initialize_store() - self.initialize_default_traces() def initialize_store(self) -> None: """Initializes the default store configuration.""" @@ -208,13 +181,6 @@ def initialize_store(self) -> None: self.store_buffer_size = 134217728 self.store_pool_workers = 0 - def initialize_default_traces(self) -> None: - """Initializes the default traces.""" - - self.traces = [] - for trace in DEFAULT_TRACES: - self.append("traces", trace) - @frappe.whitelist() def get_fallback_admin_password(self) -> str: """Returns the admin password of the cluster.""" diff --git a/mail/server/doctype/mail_cluster_trace/__init__.py b/mail/server/doctype/mail_cluster_trace/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/mail/server/doctype/mail_cluster_trace/mail_cluster_trace.json b/mail/server/doctype/mail_cluster_trace/mail_cluster_trace.json deleted file mode 100644 index f66440433..000000000 --- a/mail/server/doctype/mail_cluster_trace/mail_cluster_trace.json +++ /dev/null @@ -1,201 +0,0 @@ -{ - "actions": [], - "allow_rename": 1, - "creation": "2025-08-06 20:26:07.025939", - "doctype": "DocType", - "editable_grid": 1, - "engine": "InnoDB", - "field_order": [ - "tracer_configuration_section", - "type", - "tracer_id", - "column_break_50om", - "level", - "options_section", - "path", - "prefix", - "rotate", - "transport", - "endpoint", - "timeout", - "throttle", - "column_break_mxza", - "ansi", - "multiline", - "buffer", - "enable_log_exporter", - "enable_span_exporter", - "lossy" - ], - "fields": [ - { - "fieldname": "tracer_configuration_section", - "fieldtype": "Section Break", - "label": "Tracer configuration" - }, - { - "description": "Unique identifier for the tracer.", - "fieldname": "tracer_id", - "fieldtype": "Data", - "in_list_view": 1, - "label": "Tracer ID", - "reqd": 1 - }, - { - "fieldname": "options_section", - "fieldtype": "Section Break", - "label": "Options" - }, - { - "depends_on": "eval: doc.type == \"Log file\"", - "description": "The path to the log file.", - "fieldname": "path", - "fieldtype": "Data", - "label": "Path", - "mandatory_depends_on": "eval: doc.type == \"Log file\"", - "placeholder": "/var/log" - }, - { - "depends_on": "eval: doc.type == \"Log file\"", - "description": "The prefix for the log file.", - "fieldname": "prefix", - "fieldtype": "Data", - "label": "Prefix", - "mandatory_depends_on": "eval: doc.type == \"Log file\"", - "placeholder": "stalwart.log" - }, - { - "description": "The type of tracer.", - "fieldname": "type", - "fieldtype": "Select", - "in_list_view": 1, - "label": "Method", - "options": "\nLog file\nConsole\nSystemd Journal\nOpen Telemetry", - "reqd": 1 - }, - { - "description": "The logging level for this tracer.", - "fieldname": "level", - "fieldtype": "Select", - "in_list_view": 1, - "label": "Logging Level", - "options": "\nError\nWarn\nInfo\nDebug\nTrace", - "reqd": 1 - }, - { - "fieldname": "column_break_50om", - "fieldtype": "Column Break" - }, - { - "fieldname": "column_break_mxza", - "fieldtype": "Column Break" - }, - { - "depends_on": "eval: doc.type == \"Open Telemetry\"", - "description": "The transport protocol for Open Telemetry.", - "fieldname": "transport", - "fieldtype": "Select", - "label": "Transport", - "mandatory_depends_on": "eval: doc.type == \"Open Telemetry\"", - "options": "\nHTTP\ngRPC" - }, - { - "depends_on": "eval: doc.type == \"Open Telemetry\"", - "description": "The endpoint for Open Telemetry.", - "fieldname": "endpoint", - "fieldtype": "Data", - "label": "Endpoint", - "mandatory_depends_on": "eval: doc.type == \"Open Telemetry\"", - "placeholder": "https://tracing.example.com/v1/otel" - }, - { - "depends_on": "eval: doc.type == \"Open Telemetry\"", - "description": "Maximum amount of time that Stalwart will wait for a response from the OpenTelemetry endpoint.", - "fieldname": "timeout", - "fieldtype": "Int", - "label": "Timeout (Seconds)", - "mandatory_depends_on": "eval: doc.type == \"Open Telemetry\"", - "non_negative": 1 - }, - { - "depends_on": "eval: doc.type == \"Open Telemetry\"", - "description": "The minimum amount of time that must pass between each request to the OpenTelemetry endpoint.", - "fieldname": "throttle", - "fieldtype": "Int", - "label": "Throttle (Milliseconds)", - "mandatory_depends_on": "eval: doc.type == \"Open Telemetry\"", - "non_negative": 1 - }, - { - "depends_on": "eval: doc.type == \"Log file\"", - "description": "The frequency to rotate the log file.", - "fieldname": "rotate", - "fieldtype": "Select", - "label": "Rotate Frequency", - "mandatory_depends_on": "eval: doc.type == \"Log file\"", - "options": "\nDaily\nHourly\nMinutely\nNever" - }, - { - "default": "0", - "depends_on": "eval: [\"Log file\", \"Console\"].includes(doc.type)", - "description": "Whether to use ANSI colors in logs.", - "fieldname": "ansi", - "fieldtype": "Check", - "label": "Use ANSI colors" - }, - { - "default": "0", - "depends_on": "eval: doc.type == \"Open Telemetry\"", - "description": "Whether to export logs to OpenTelemetry.", - "fieldname": "enable_log_exporter", - "fieldtype": "Check", - "label": "Export logs" - }, - { - "default": "0", - "depends_on": "eval: doc.type == \"Open Telemetry\"", - "description": "Whether to export spans to OpenTelemetry.", - "fieldname": "enable_span_exporter", - "fieldtype": "Check", - "label": "Export spans" - }, - { - "default": "0", - "depends_on": "eval: [\"Log file\", \"Console\", \"Systemd Journal\", \"Open Telemetry\"].includes(doc.type)", - "description": "Whether to drop log entries if there is backlog.", - "fieldname": "lossy", - "fieldtype": "Check", - "label": "Lossy mode" - }, - { - "default": "0", - "depends_on": "eval: [\"Log file\", \"Console\"].includes(doc.type)", - "description": "Whether to write log entries as a single line or multiline.", - "fieldname": "multiline", - "fieldtype": "Check", - "label": "Multiline entries" - }, - { - "default": "0", - "depends_on": "eval: doc.type == \"Console\"", - "description": "Whether to buffer log entries before writing to console.", - "fieldname": "buffer", - "fieldtype": "Check", - "label": "Buffered writes" - } - ], - "grid_page_length": 50, - "index_web_pages_for_search": 1, - "istable": 1, - "links": [], - "modified": "2025-08-07 12:12:19.659090", - "modified_by": "Administrator", - "module": "Server", - "name": "Mail Cluster Trace", - "owner": "Administrator", - "permissions": [], - "row_format": "Dynamic", - "sort_field": "creation", - "sort_order": "DESC", - "states": [] -} diff --git a/mail/server/doctype/mail_cluster_trace/mail_cluster_trace.py b/mail/server/doctype/mail_cluster_trace/mail_cluster_trace.py deleted file mode 100644 index b477afe1f..000000000 --- a/mail/server/doctype/mail_cluster_trace/mail_cluster_trace.py +++ /dev/null @@ -1,9 +0,0 @@ -# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors -# For license information, please see license.txt - -# import frappe -from frappe.model.document import Document - - -class MailClusterTrace(Document): - pass From c0337e6c254485ff03d3f78c28740014d09b196e Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Tue, 5 May 2026 16:22:10 +0530 Subject: [PATCH 03/55] refactor: remove `Mail Cluster Store` --- .../doctype/mail_cluster_store/__init__.py | 0 .../mail_cluster_store.json | 573 ------------------ .../mail_cluster_store/mail_cluster_store.py | 17 - 3 files changed, 590 deletions(-) delete mode 100644 mail/server/doctype/mail_cluster_store/__init__.py delete mode 100644 mail/server/doctype/mail_cluster_store/mail_cluster_store.json delete mode 100644 mail/server/doctype/mail_cluster_store/mail_cluster_store.py diff --git a/mail/server/doctype/mail_cluster_store/__init__.py b/mail/server/doctype/mail_cluster_store/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/mail/server/doctype/mail_cluster_store/mail_cluster_store.json b/mail/server/doctype/mail_cluster_store/mail_cluster_store.json deleted file mode 100644 index 341c036e4..000000000 --- a/mail/server/doctype/mail_cluster_store/mail_cluster_store.json +++ /dev/null @@ -1,573 +0,0 @@ -{ - "actions": [], - "autoname": "hash", - "creation": "2025-03-02 10:31:58.549698", - "doctype": "DocType", - "editable_grid": 1, - "engine": "InnoDB", - "field_order": [ - "section_break_oc18", - "type", - "region", - "endpoint", - "column_break_uo3q", - "store_id", - "path", - "profile", - "cluster_file", - "url", - "configuration_section", - "host", - "port", - "database", - "redis_type", - "urls", - "column_break_offi", - "max_allowed_packet", - "timeout", - "bucket_storage_account_section", - "bucket", - "storage_account", - "container", - "column_break_o8ml", - "key_prefix", - "authentication_section", - "user", - "access_key", - "secret_key", - "azure_access_key", - "column_break_r24o", - "password", - "security_token", - "sas_token", - "storage_settings_section", - "compression", - "min_blob_size", - "write_buffer_size", - "max_retries", - "depth", - "column_break_bsqd", - "purge_frequency", - "cluster_ids_section", - "machine", - "column_break_gg5z", - "datacenter", - "cluster_settings_section", - "retry_total", - "retry_max_wait", - "retry_min_wait", - "column_break_roov", - "read_from_replicas", - "transaction_settings_section", - "transaction_timeout", - "transaction_max_retry_delay", - "column_break_q4si", - "transaction_retry_limit", - "tls_section", - "tls_enable", - "tls_allow_invalid_certs", - "pools_section", - "workers", - "pool_max_connections", - "column_break_n71b", - "pool_min_connections", - "index_section", - "index_shards", - "column_break_5nye", - "index_replicas" - ], - "fields": [ - { - "fieldname": "section_break_oc18", - "fieldtype": "Section Break" - }, - { - "description": "Unique identifier for the store.", - "fieldname": "store_id", - "fieldtype": "Data", - "in_list_view": 1, - "label": "Store ID", - "reqd": 1 - }, - { - "description": "Storage backend type.", - "fieldname": "type", - "fieldtype": "Select", - "in_list_view": 1, - "label": "Type", - "options": "\nRocksDB\nFoundationDB\nPostgreSQL\nmySQL\nSQLite\nS3-compatible\nRedis/Memcached\nElasticSearch\nAzure Blob Storage\nFilesystem", - "reqd": 1 - }, - { - "fieldname": "storage_settings_section", - "fieldtype": "Section Break", - "label": "Storage Settings" - }, - { - "depends_on": "eval: [\"RocksDB\", \"FoundationDB\", \"PostgreSQL\", \"mySQL\", \"SQLite\", \"S3-compatible\", \"Azure Blob Storage\", \"Filesystem\"].includes(doc.type)", - "description": "Algorithm to use to compress large binary objects.", - "fieldname": "compression", - "fieldtype": "Select", - "label": "Compression", - "options": "\nLZ4" - }, - { - "fieldname": "pools_section", - "fieldtype": "Section Break", - "label": "Pools" - }, - { - "depends_on": "eval: [\"RocksDB\", \"SQLite\", \"Filesystem\"].includes(doc.type)", - "description": "Where to store the data in the server's filesystem.", - "fieldname": "path", - "fieldtype": "Data", - "label": "Path", - "mandatory_depends_on": "eval: [\"RocksDB\", \"SQLite\", \"Filesystem\"].includes(doc.type)" - }, - { - "fieldname": "column_break_uo3q", - "fieldtype": "Column Break" - }, - { - "fieldname": "column_break_bsqd", - "fieldtype": "Column Break" - }, - { - "depends_on": "eval: [\"PostgreSQL\", \"mySQL\"].includes(doc.type)", - "description": "Port of the database server.", - "fieldname": "port", - "fieldtype": "Int", - "label": "Port", - "mandatory_depends_on": "eval: [\"PostgreSQL\", \"mySQL\"].includes(doc.type)", - "non_negative": 1 - }, - { - "depends_on": "eval: [\"PostgreSQL\", \"mySQL\"].includes(doc.type)", - "description": "Name of the database.", - "fieldname": "database", - "fieldtype": "Data", - "label": "Database", - "mandatory_depends_on": "eval: [\"PostgreSQL\", \"mySQL\"].includes(doc.type)" - }, - { - "fieldname": "column_break_offi", - "fieldtype": "Column Break" - }, - { - "fieldname": "configuration_section", - "fieldtype": "Section Break", - "label": "Configuration" - }, - { - "fieldname": "authentication_section", - "fieldtype": "Section Break", - "label": "Authentication" - }, - { - "depends_on": "eval: [\"PostgreSQL\", \"mySQL\", \"ElasticSearch\"].includes(doc.type) || (doc.type == \"Redis/Memcached\" && doc.redis_type == \"Redis Cluster\")", - "description": "Password to connect to the database.", - "fieldname": "password", - "fieldtype": "Password", - "label": "Password" - }, - { - "fieldname": "tls_section", - "fieldtype": "Section Break", - "label": "TLS" - }, - { - "fieldname": "column_break_n71b", - "fieldtype": "Column Break" - }, - { - "fieldname": "column_break_r24o", - "fieldtype": "Column Break" - }, - { - "depends_on": "eval: doc.type == \"FoundationDB\"", - "description": "Path to the cluster file for the FoundationDB cluster.", - "fieldname": "cluster_file", - "fieldtype": "Data", - "label": "Cluster File", - "placeholder": "/etc/foundationdb/fdb.cluster" - }, - { - "fieldname": "cluster_ids_section", - "fieldtype": "Section Break", - "label": "Cluster IDs" - }, - { - "fieldname": "column_break_gg5z", - "fieldtype": "Column Break" - }, - { - "fieldname": "transaction_settings_section", - "fieldtype": "Section Break", - "label": "Transaction Settings" - }, - { - "depends_on": "eval: doc.type == \"FoundationDB\"", - "description": "Transaction retry limit.", - "fieldname": "transaction_retry_limit", - "fieldtype": "Int", - "label": "Retry Limit", - "non_negative": 1, - "placeholder": "10" - }, - { - "fieldname": "column_break_q4si", - "fieldtype": "Column Break" - }, - { - "fieldname": "cluster_settings_section", - "fieldtype": "Section Break", - "label": "Cluster Settings" - }, - { - "fieldname": "column_break_roov", - "fieldtype": "Column Break" - }, - { - "depends_on": "eval: doc.type == \"ElasticSearch\"", - "description": "URL of the store.", - "fieldname": "url", - "fieldtype": "Data", - "label": "URL", - "mandatory_depends_on": "eval: doc.type == \"ElasticSearch\"" - }, - { - "fieldname": "index_section", - "fieldtype": "Section Break", - "label": "Index" - }, - { - "fieldname": "column_break_5nye", - "fieldtype": "Column Break" - }, - { - "depends_on": "eval: doc.type == \"Azure Blob Storage\"", - "description": "The name of the container in the Storage Account.", - "fieldname": "container", - "fieldtype": "Data", - "label": "Container", - "mandatory_depends_on": "eval: doc.type == \"Azure Blob Storage\"" - }, - { - "depends_on": "eval: [\"S3-compatible\", \"Azure Blob Storage\"].includes(doc.type)", - "description": "A prefix that will be added to the keys of all objects stored in the blob store.", - "fieldname": "key_prefix", - "fieldtype": "Data", - "label": "Key Prefix" - }, - { - "fieldname": "column_break_o8ml", - "fieldtype": "Column Break" - }, - { - "depends_on": "eval: doc.type == \"S3-compatible\"", - "description": "The geographical region where the bucket resides.", - "fieldname": "region", - "fieldtype": "Data", - "label": "Region", - "mandatory_depends_on": "eval: doc.type == \"S3-compatible\"", - "placeholder": "us-east-1" - }, - { - "depends_on": "eval: doc.type == \"S3-compatible\"", - "description": "The network address (hostname and optionally a port) of the S3 service. For S3-compatible services, you will need to specify the endpoint explicitly.", - "fieldname": "endpoint", - "fieldtype": "Data", - "label": "Endpoint", - "mandatory_depends_on": "eval: doc.type == \"S3-compatible\"" - }, - { - "depends_on": "eval: doc.type == \"S3-compatible\"", - "description": "Used when retrieving credentials from a shared credentials file. If specified, the server will use the access key ID, secret access key, and session token (if available) associated with the given profile.", - "fieldname": "profile", - "fieldtype": "Data", - "label": "Profile" - }, - { - "fieldname": "bucket_storage_account_section", - "fieldtype": "Section Break", - "label": "Bucket / Storage Account" - }, - { - "depends_on": "eval: doc.type == \"Azure Blob Storage\"", - "description": "The access key for the Azure Storage Account.", - "fieldname": "azure_access_key", - "fieldtype": "Password", - "label": "Access Key" - }, - { - "depends_on": "eval: [\"PostgreSQL\", \"mySQL\"].includes(doc.type)", - "description": "Hostname of the database server.", - "fieldname": "host", - "fieldtype": "Data", - "label": "Hostname", - "mandatory_depends_on": "eval: [\"PostgreSQL\", \"mySQL\"].includes(doc.type)" - }, - { - "depends_on": "eval: doc.type == \"mySQL\"", - "description": "Maximum size of a packet in bytes.", - "fieldname": "max_allowed_packet", - "fieldtype": "Int", - "label": "Max Allowed Packet (Bytes)", - "non_negative": 1 - }, - { - "depends_on": "eval: [\"PostgreSQL\", \"mySQL\", \"S3-compatible\", \"Redis/Memcached\", \"Azure Blob Storage\"].includes(doc.type)", - "description": "Connection timeout to the database.", - "fieldname": "timeout", - "fieldtype": "Int", - "label": "Timeout (Seconds)", - "non_negative": 1 - }, - { - "depends_on": "eval: doc.type == \"Redis/Memcached\"", - "description": "Type of Redis server.", - "fieldname": "redis_type", - "fieldtype": "Select", - "label": "Server Type", - "mandatory_depends_on": "eval: doc.type == \"Redis/Memcached\"", - "options": "\nRedis Single Node\nRedis Cluster" - }, - { - "depends_on": "eval: doc.type == \"Redis/Memcached\"", - "description": "URL(s) of the Redis server(s).", - "fieldname": "urls", - "fieldtype": "Small Text", - "label": "Redis URL(s)", - "mandatory_depends_on": "eval: doc.type == \"Redis/Memcached\"" - }, - { - "depends_on": "eval: doc.type == \"S3-compatible\"", - "description": "The S3 bucket where blobs (e-mail messages, Sieve scripts, etc.) will be stored.", - "fieldname": "bucket", - "fieldtype": "Data", - "label": "Bucket", - "mandatory_depends_on": "eval: doc.type == \"S3-compatible\"" - }, - { - "depends_on": "eval: doc.type == \"Azure Blob Storage\"", - "description": "The Azure Storage Account where blobs (e-mail messages, Sieve scripts, etc.) will be stored.", - "fieldname": "storage_account", - "fieldtype": "Data", - "label": "Storage Account Name", - "mandatory_depends_on": "eval: doc.type == \"Azure Blob Storage\"" - }, - { - "depends_on": "eval: [\"PostgreSQL\", \"mySQL\", \"ElasticSearch\"].includes(doc.type) || (doc.type == \"Redis/Memcached\" && doc.redis_type == \"Redis Cluster\")", - "description": "Username to connect to the database.", - "fieldname": "user", - "fieldtype": "Data", - "label": "Username" - }, - { - "depends_on": "eval: doc.type == \"S3-compatible\"", - "description": "Identifies the S3 account.", - "fieldname": "access_key", - "fieldtype": "Password", - "label": "Access Key", - "mandatory_depends_on": "eval: doc.type == \"S3-compatible\"" - }, - { - "depends_on": "eval: doc.type == \"S3-compatible\"", - "description": "The security token for the S3 account.", - "fieldname": "security_token", - "fieldtype": "Password", - "label": "Security Token" - }, - { - "depends_on": "eval: doc.type == \"S3-compatible\"", - "description": "The secret key for the S3 account.", - "fieldname": "secret_key", - "fieldtype": "Password", - "label": "Secret Key", - "mandatory_depends_on": "eval: doc.type == \"S3-compatible\"" - }, - { - "depends_on": "eval: doc.type == \"Azure Blob Storage\"", - "description": "SAS Token, when not using access-key based authentication.", - "fieldname": "sas_token", - "fieldtype": "Password", - "label": "SAS Token" - }, - { - "depends_on": "eval: [\"RocksDB\", \"FoundationDB\", \"PostgreSQL\", \"mySQL\", \"SQLite\", \"S3-compatible\", \"Azure Blob Storage\", \"Filesystem\"].includes(doc.type)", - "description": "How often to purge the database. Expects a cron expression.\n\n
\n
\n\n
*  *  *  *  *\n\u252c  \u252c  \u252c  \u252c  \u252c\n\u2502  \u2502  \u2502  \u2502  \u2502\n\u2502  \u2502  \u2502  \u2502  \u2514 day of week (0 - 6) (0 is Sunday)\n\u2502  \u2502  \u2502  \u2514\u2500\u2500\u2500\u2500\u2500 month (1 - 12)\n\u2502  \u2502  \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 day of month (1 - 31)\n\u2502  \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 hour (0 - 23)\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 minute (0 - 59)\n\n---\n\n* - Any value\n/ - Step values\n
", - "fieldname": "purge_frequency", - "fieldtype": "Data", - "label": "Purge Frequency (Cron)", - "not_nullable": 1, - "placeholder": "0 3 * * *" - }, - { - "depends_on": "eval: doc.type == \"RocksDB\"", - "description": "Minimum size of a blob to store in the blob store, smaller blobs are stored in the metadata store.", - "fieldname": "min_blob_size", - "fieldtype": "Int", - "label": "Min Blob Size (Bytes)", - "non_negative": 1 - }, - { - "depends_on": "eval: doc.type == \"RocksDB\"", - "description": "Size of the write buffer in bytes, used to batch writes to the store.", - "fieldname": "write_buffer_size", - "fieldtype": "Int", - "label": "Write Buffer Size (MB)", - "non_negative": 1 - }, - { - "depends_on": "eval: [\"S3-compatible\", \"Azure Blob Storage\"].includes(doc.type)", - "description": "The maximum number of times to retry failed requests. Set to 0 to disable retries.", - "fieldname": "max_retries", - "fieldtype": "Int", - "label": "Retry Limit" - }, - { - "depends_on": "eval: doc.type == \"Filesystem\"", - "description": "Maximum depth of nested directories.", - "fieldname": "depth", - "fieldtype": "Int", - "label": "Nested Depth", - "non_negative": 1 - }, - { - "depends_on": "eval: doc.type == \"FoundationDB\"", - "description": "Machine ID in the FoundationDB cluster.", - "fieldname": "machine", - "fieldtype": "Data", - "label": "Machine ID" - }, - { - "depends_on": "eval: doc.type == \"FoundationDB\"", - "description": "Data center ID of the FoundationDB cluster.", - "fieldname": "datacenter", - "fieldtype": "Data", - "label": "Data Center ID" - }, - { - "depends_on": "eval: doc.type == \"Redis/Memcached\" && doc.redis_type == \"Redis Cluster\"", - "description": "Number of retries to connect to the Redis cluster.", - "fieldname": "retry_total", - "fieldtype": "Int", - "label": "Retries", - "non_negative": 1, - "placeholder": "3" - }, - { - "default": "0", - "depends_on": "eval: doc.type == \"Redis/Memcached\" && doc.redis_type == \"Redis Cluster\"", - "description": "Whether to read from replicas.", - "fieldname": "read_from_replicas", - "fieldtype": "Check", - "label": "Read From Replicas" - }, - { - "depends_on": "eval: doc.type == \"Redis/Memcached\" && doc.redis_type == \"Redis Cluster\"", - "description": "Maximum time to wait between retries.", - "fieldname": "retry_max_wait", - "fieldtype": "Int", - "label": "Max Wait (Milliseconds)", - "non_negative": 1 - }, - { - "depends_on": "eval: doc.type == \"Redis/Memcached\" && doc.redis_type == \"Redis Cluster\"", - "description": "Minimum time to wait between retries.", - "fieldname": "retry_min_wait", - "fieldtype": "Int", - "label": "Min Wait (Milliseconds)", - "non_negative": 1 - }, - { - "depends_on": "eval: doc.type == \"FoundationDB\"", - "description": "Transaction timeout.", - "fieldname": "transaction_timeout", - "fieldtype": "Int", - "label": "Timeout (Seconds)", - "non_negative": 1 - }, - { - "depends_on": "eval: doc.type == \"FoundationDB\"", - "description": "Transaction maximum retry delay.", - "fieldname": "transaction_max_retry_delay", - "fieldtype": "Int", - "label": "Max Retry Delay (Seconds)", - "non_negative": 1 - }, - { - "default": "0", - "depends_on": "eval: [\"PostgreSQL\", \"mySQL\"].includes(doc.type)", - "description": "Use TLS to connect to the store.", - "fieldname": "tls_enable", - "fieldtype": "Check", - "label": "Enable TLS" - }, - { - "default": "0", - "depends_on": "eval: [\"PostgreSQL\", \"mySQL\", \"ElasticSearch\"].includes(doc.type)", - "description": "Allow invalid TLS certificates when connecting to the store.", - "fieldname": "tls_allow_invalid_certs", - "fieldtype": "Check", - "label": "Allow Invalid Certs" - }, - { - "depends_on": "eval: [\"RocksDB\", \"SQLite\"].includes(doc.type)", - "description": "Number of worker threads to use for the store, defaults to the number of cores.", - "fieldname": "workers", - "fieldtype": "Int", - "label": "Thread Pool Size", - "placeholder": "8" - }, - { - "depends_on": "eval: doc.type == \"mySQL\"", - "description": "Minimum number of connections to the store.", - "fieldname": "pool_min_connections", - "fieldtype": "Int", - "label": "Min Connections", - "non_negative": 1 - }, - { - "depends_on": "eval: [\"PostgreSQL\", \"mySQL\", \"SQLite\"].includes(doc.type)", - "description": "Maximum number of connections to the store.", - "fieldname": "pool_max_connections", - "fieldtype": "Int", - "label": "Max Connections", - "non_negative": 1 - }, - { - "depends_on": "eval: doc.type == \"ElasticSearch\"", - "description": "Number of shards for the index.", - "fieldname": "index_shards", - "fieldtype": "Int", - "label": "Number of Shards", - "non_negative": 1 - }, - { - "depends_on": "eval: doc.type == \"ElasticSearch\"", - "description": "Number of replicas for the index.", - "fieldname": "index_replicas", - "fieldtype": "Int", - "label": "Number of Replicas", - "non_negative": 1 - } - ], - "grid_page_length": 50, - "index_web_pages_for_search": 1, - "istable": 1, - "links": [], - "modified": "2026-01-12 14:37:41.129244", - "modified_by": "Administrator", - "module": "Server", - "name": "Mail Cluster Store", - "naming_rule": "Random", - "owner": "Administrator", - "permissions": [], - "row_format": "Dynamic", - "show_title_field_in_link": 1, - "sort_field": "creation", - "sort_order": "DESC", - "states": [], - "title_field": "store_id" -} diff --git a/mail/server/doctype/mail_cluster_store/mail_cluster_store.py b/mail/server/doctype/mail_cluster_store/mail_cluster_store.py deleted file mode 100644 index 310ae86a2..000000000 --- a/mail/server/doctype/mail_cluster_store/mail_cluster_store.py +++ /dev/null @@ -1,17 +0,0 @@ -# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors -# For license information, please see license.txt - -import frappe -from frappe.model.document import Document - - -class MailClusterStore(Document): - pass - - -def on_doctype_update() -> None: - frappe.db.add_unique( - "Mail Cluster Store", - ["parenttype", "parent", "store_id"], - constraint_name="unique_parent_store_id", - ) From b1f2cbefb4155ee601561bad7bd61d7ed006566e Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Tue, 5 May 2026 16:39:03 +0530 Subject: [PATCH 04/55] refactor: Mail Server --- .../server/doctype/mail_server/mail_server.js | 41 ------ .../doctype/mail_server/mail_server.json | 98 ++----------- .../server/doctype/mail_server/mail_server.py | 107 -------------- .../mail_server_acme_provider/__init__.py | 0 .../mail_server_acme_provider.json | 137 ------------------ .../mail_server_acme_provider.py | 17 --- .../doctype/mail_server_listener/__init__.py | 0 .../mail_server_listener.json | 78 ---------- .../mail_server_listener.py | 17 --- .../mail_server_tls_certificate/__init__.py | 0 .../mail_server_tls_certificate.json | 108 -------------- .../mail_server_tls_certificate.py | 17 --- 12 files changed, 12 insertions(+), 608 deletions(-) delete mode 100644 mail/server/doctype/mail_server_acme_provider/__init__.py delete mode 100644 mail/server/doctype/mail_server_acme_provider/mail_server_acme_provider.json delete mode 100644 mail/server/doctype/mail_server_acme_provider/mail_server_acme_provider.py delete mode 100644 mail/server/doctype/mail_server_listener/__init__.py delete mode 100644 mail/server/doctype/mail_server_listener/mail_server_listener.json delete mode 100644 mail/server/doctype/mail_server_listener/mail_server_listener.py delete mode 100644 mail/server/doctype/mail_server_tls_certificate/__init__.py delete mode 100644 mail/server/doctype/mail_server_tls_certificate/mail_server_tls_certificate.json delete mode 100644 mail/server/doctype/mail_server_tls_certificate/mail_server_tls_certificate.py diff --git a/mail/server/doctype/mail_server/mail_server.js b/mail/server/doctype/mail_server/mail_server.js index 2d38a9373..f90286827 100644 --- a/mail/server/doctype/mail_server/mail_server.js +++ b/mail/server/doctype/mail_server/mail_server.js @@ -23,14 +23,6 @@ frappe.ui.form.on('Mail Server', { if (!frappe.user_roles.includes('System Manager')) return - frm.add_custom_button( - __('Generate Config'), - () => { - frm.trigger('generate_config') - }, - __('Actions'), - ) - frm.add_custom_button( __('Verify SSH Connection'), () => { @@ -68,20 +60,6 @@ frappe.ui.form.on('Mail Server', { } }, - generate_config(frm) { - frappe.call({ - doc: frm.doc, - method: 'generate_config', - freeze: true, - freeze_message: __('Generating Config...'), - callback: (r) => { - if (!r.exc) { - frm.refresh() - } - }, - }) - }, - verify_ssh_connection(frm) { frappe.call({ doc: frm.doc, @@ -133,22 +111,3 @@ frappe.ui.form.on('Mail Server', { }) }, }) - -frappe.ui.form.on('Mail Server ACME Provider', { - acme_providers_add(frm, cdt, cdn) { - const row = locals[cdt][cdn] - - if (frm.doc.hostname) { - row.directory_id = frm.doc.hostname.replaceAll('.', '-') - row.domains = frm.doc.hostname - row.contact = 'postmaster@' + frm.doc.hostname - } - if (frm.doc.acme_providers.length < 2) { - row.default = 1 - } - row.challenge = 'TLS-ALPN-01' - row.directory = 'https://acme-v02.api.letsencrypt.org/directory' - row.renew_before = 30 - refresh_field('acme_providers') - }, -}) diff --git a/mail/server/doctype/mail_server/mail_server.json b/mail/server/doctype/mail_server/mail_server.json index 6f602f756..105935417 100644 --- a/mail/server/doctype/mail_server/mail_server.json +++ b/mail/server/doctype/mail_server/mail_server.json @@ -8,18 +8,14 @@ "details_tab", "section_break_excr", "enabled", - "outbound_only", "column_break_0mtp", "cluster", "hostname", - "base_url", + "http_port", "network_settings_section", - "server_max_connections", - "column_break_3m4p", "ipv4_addresses", + "column_break_3m4p", "ipv6_addresses", - "cluster_settings_section", - "cluster_node_id", "ssh_tab", "section_break_rmju", "column_break_mwtr", @@ -28,12 +24,7 @@ "ssh_user", "ssh_port", "ssh_key_section", - "ssh_public_key", - "tls_tab", - "acme_providers", - "tls_certificates", - "listeners_tab", - "listeners" + "ssh_public_key" ], "fields": [ { @@ -49,29 +40,6 @@ "fieldtype": "Section Break", "label": "Network Settings" }, - { - "fieldname": "cluster_settings_section", - "fieldtype": "Section Break", - "label": "Cluster Settings" - }, - { - "default": "8192", - "description": "The maximum number of concurrent connections the server will accept.", - "fieldname": "server_max_connections", - "fieldtype": "Int", - "label": "Max Connections", - "reqd": 1 - }, - { - "description": "Unique identifier for this node in the cluster.", - "fieldname": "cluster_node_id", - "fieldtype": "Int", - "label": "Node ID", - "no_copy": 1, - "non_negative": 1, - "placeholder": "1", - "reqd": 1 - }, { "default": "1", "depends_on": "eval: !doc.__islocal", @@ -86,23 +54,11 @@ "fieldname": "column_break_3m4p", "fieldtype": "Column Break" }, - { - "fieldname": "listeners_tab", - "fieldtype": "Tab Break", - "label": "Listeners" - }, { "fieldname": "details_tab", "fieldtype": "Tab Break", "label": "Details" }, - { - "description": "Define custom listeners for this mail server. If not set, the cluster\u2019s listeners will be used.", - "fieldname": "listeners", - "fieldtype": "Table", - "label": "Listeners", - "options": "Mail Server Listener" - }, { "description": "The mail cluster this server belongs to.", "fieldname": "cluster", @@ -115,39 +71,6 @@ "search_index": 1, "set_only_once": 1 }, - { - "default": "0", - "description": "Disable email delivery to local domains and perform an MX lookup to route emails externally.", - "fieldname": "outbound_only", - "fieldtype": "Check", - "label": "Outbound Only", - "search_index": 1 - }, - { - "fieldname": "tls_tab", - "fieldtype": "Tab Break", - "label": "TLS" - }, - { - "description": "Manage TLS certificates.", - "fieldname": "tls_certificates", - "fieldtype": "Table", - "label": "TLS Certificates", - "options": "Mail Server TLS Certificate" - }, - { - "description": "Manage ACME TLS certificate providers.", - "fieldname": "acme_providers", - "fieldtype": "Table", - "label": "ACME Providers", - "options": "Mail Server ACME Provider" - }, - { - "description": "URL where HTTP requests are sent.", - "fieldname": "base_url", - "fieldtype": "Data", - "label": "Base URL" - }, { "description": "Hostname of the mail server.", "fieldname": "hostname", @@ -231,16 +154,19 @@ "label": "Public IPv6", "no_copy": 1, "read_only": 1 + }, + { + "default": "8080", + "fieldname": "http_port", + "fieldtype": "Int", + "label": "HTTP Port", + "non_negative": 1, + "reqd": 1 } ], "grid_page_length": 50, "index_web_pages_for_search": 1, "links": [ - { - "group": "Reference", - "link_doctype": "Server Config", - "link_fieldname": "server" - }, { "group": "Deployment", "link_doctype": "Server Job", @@ -257,7 +183,7 @@ "link_fieldname": "server" } ], - "modified": "2026-04-27 10:22:56.594485", + "modified": "2026-05-05 16:13:52.084056", "modified_by": "Administrator", "module": "Server", "name": "Mail Server", diff --git a/mail/server/doctype/mail_server/mail_server.py b/mail/server/doctype/mail_server/mail_server.py index 0abda16f7..b05128ce2 100644 --- a/mail/server/doctype/mail_server/mail_server.py +++ b/mail/server/doctype/mail_server/mail_server.py @@ -13,12 +13,8 @@ from frappe import _ from frappe.model.document import Document -from mail.server.doctype.server_config.server_config import create_server_config from mail.utils.dns import get_dns_record -if TYPE_CHECKING: - from mail.server.doctype.server_config.server_config import ServerConfig - class MailServer(Document): @property @@ -34,14 +30,6 @@ def autoname(self) -> None: def validate(self) -> None: self.validate_hostname() self.validate_cluster() - self.validate_base_url() - self.validate_cluster_node_id() - self.validate_acme_providers() - self.validate_tls_certificates() - self.validate_listeners() - - def after_insert(self) -> None: - self.generate_config() def on_trash(self) -> None: if frappe.session.user != "Administrator": @@ -62,101 +50,6 @@ def validate_cluster(self) -> None: if not frappe.db.get_value("Mail Cluster", self.cluster, "enabled"): frappe.throw(_("Mail Cluster {0} is disabled.").format(frappe.bold(self.cluster))) - def validate_base_url(self) -> None: - """Validates the base URL of the server.""" - - if not self.base_url: - self.base_url = f"https://{self.hostname}/" - - def validate_cluster_node_id(self) -> None: - """Validates the cluster node ID.""" - - if frappe.db.exists( - "Mail Server", - { - "cluster": self.cluster, - "cluster_node_id": self.cluster_node_id, - "name": ["!=", self.name], - }, - ): - frappe.throw( - _("Node ID {0} already assigned to another Mail Server.").format( - frappe.bold(self.cluster_node_id) - ) - ) - - def validate_acme_providers(self) -> None: - """Validates the ACME Providers.""" - - default_count = 0 - directory_ids = [] - for acme in self.acme_providers: - if acme.default: - default_count += 1 - - if acme.directory_id in directory_ids: - frappe.throw( - _("Row #{0}: Directory ID {1} is duplicated.").format( - acme.idx, frappe.bold(acme.directory_id) - ) - ) - directory_ids.append(acme.directory_id) - - if default_count > 1: - frappe.throw(_("Only one ACME Provider can be default.")) - - def validate_tls_certificates(self) -> None: - """Validates the TLS Certificates.""" - - default_count = 0 - certificate_ids = [] - for tls in self.tls_certificates: - if tls.default: - default_count += 1 - - if tls.certificate_id in certificate_ids: - frappe.throw( - _("Row #{0}: Certificate ID {1} is duplicated.").format( - tls.idx, frappe.bold(tls.certificate_id) - ) - ) - certificate_ids.append(tls.certificate_id) - - if not tls.cert and not tls.cert_path: - frappe.throw(_("Row #{0}: Certificate or Certificate Path is required.").format(tls.idx)) - if not tls.private_key and not tls.private_key_path: - frappe.throw(_("Row #{0}: Private Key or Private Key Path is required.").format(tls.idx)) - - if default_count > 1: - frappe.throw(_("Only one TLS Certificate can be default.")) - - def validate_listeners(self) -> None: - """Validates the listeners.""" - - listener_ids = [] - for listener in self.listeners: - if listener.listener_id in listener_ids: - frappe.throw( - _("Row #{0}: Listener ID {1} is duplicated.").format( - listener.idx, frappe.bold(listener.listener_id) - ) - ) - - listener_ids.append(listener.listener_id) - - @frappe.whitelist() - def generate_config(self) -> None: - """Generates the Server Config.""" - - frappe.only_for("System Manager") - self._generate_config() - frappe.msgprint(_("Server Config created."), indicator="green", alert=True) - - def _generate_config(self) -> "ServerConfig": - """Generates the Server Config.""" - - return create_server_config(self.name) - @frappe.whitelist() def verify_ssh_connection(self) -> None: """Verifies the SSH connection to the server.""" diff --git a/mail/server/doctype/mail_server_acme_provider/__init__.py b/mail/server/doctype/mail_server_acme_provider/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/mail/server/doctype/mail_server_acme_provider/mail_server_acme_provider.json b/mail/server/doctype/mail_server_acme_provider/mail_server_acme_provider.json deleted file mode 100644 index 96fb463be..000000000 --- a/mail/server/doctype/mail_server_acme_provider/mail_server_acme_provider.json +++ /dev/null @@ -1,137 +0,0 @@ -{ - "actions": [], - "allow_rename": 1, - "creation": "2025-03-18 17:41:49.398215", - "doctype": "DocType", - "editable_grid": 1, - "engine": "InnoDB", - "field_order": [ - "directory_id", - "column_break_5m6h", - "default", - "section_break_3ivg", - "challenge", - "directory", - "column_break_hxpf", - "renew_before", - "section_break_6dav", - "domains", - "column_break_lzmq", - "contact", - "external_account_binding_section", - "eab_kid", - "column_break_bgl5", - "eab_hmac_key" - ], - "fields": [ - { - "description": "Unique identifier for the ACME provider.", - "fieldname": "directory_id", - "fieldtype": "Data", - "in_list_view": 1, - "label": "Directory ID", - "reqd": 1 - }, - { - "fieldname": "column_break_5m6h", - "fieldtype": "Column Break" - }, - { - "fieldname": "section_break_6dav", - "fieldtype": "Section Break" - }, - { - "fieldname": "column_break_lzmq", - "fieldtype": "Column Break" - }, - { - "default": "0", - "description": "Whether the certificates generated by this provider should be the default when no SNI is provided.", - "fieldname": "default", - "fieldtype": "Check", - "in_list_view": 1, - "label": "Default" - }, - { - "fieldname": "section_break_3ivg", - "fieldtype": "Section Break" - }, - { - "fieldname": "column_break_hxpf", - "fieldtype": "Column Break" - }, - { - "fieldname": "external_account_binding_section", - "fieldtype": "Section Break", - "label": "External Account Binding" - }, - { - "fieldname": "column_break_bgl5", - "fieldtype": "Column Break" - }, - { - "description": "The ACME challenge type used to validate domain ownership.", - "fieldname": "challenge", - "fieldtype": "Select", - "in_list_view": 1, - "label": "Challenge Type", - "options": "TLS-ALPN-01", - "reqd": 1 - }, - { - "description": "The URL of the ACME directory endpoint.", - "fieldname": "directory", - "fieldtype": "Data", - "label": "Directory URL", - "reqd": 1 - }, - { - "description": "Determines how early before expiration the certificate should be renewed.", - "fieldname": "renew_before", - "fieldtype": "Int", - "in_list_view": 1, - "label": "Renew Before (Days)", - "reqd": 1 - }, - { - "description": "Hostnames covered by this ACME manager.", - "fieldname": "domains", - "fieldtype": "Small Text", - "label": "Subject Names", - "reqd": 1 - }, - { - "description": "The contact email address, which is used for important communications regarding your ACME account and certificates.", - "fieldname": "contact", - "fieldtype": "Small Text", - "label": "Contact Emails", - "reqd": 1 - }, - { - "description": "The External Account Binding (EAB) key ID.", - "fieldname": "eab_kid", - "fieldtype": "Data", - "label": "Key ID" - }, - { - "description": "The External Account Binding (EAB) HMAC key.", - "fieldname": "eab_hmac_key", - "fieldtype": "Password", - "label": "HMAC Key" - } - ], - "grid_page_length": 50, - "index_web_pages_for_search": 1, - "istable": 1, - "links": [], - "modified": "2025-04-30 00:15:42.325327", - "modified_by": "Administrator", - "module": "Server", - "name": "Mail Server ACME Provider", - "owner": "Administrator", - "permissions": [], - "row_format": "Dynamic", - "sort_field": "creation", - "sort_order": "DESC", - "states": [] -} diff --git a/mail/server/doctype/mail_server_acme_provider/mail_server_acme_provider.py b/mail/server/doctype/mail_server_acme_provider/mail_server_acme_provider.py deleted file mode 100644 index 1093e7f9f..000000000 --- a/mail/server/doctype/mail_server_acme_provider/mail_server_acme_provider.py +++ /dev/null @@ -1,17 +0,0 @@ -# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors -# For license information, please see license.txt - -import frappe -from frappe.model.document import Document - - -class MailServerACMEProvider(Document): - pass - - -def on_doctype_update() -> None: - frappe.db.add_unique( - "Mail Server ACME Provider", - ["parenttype", "parent", "directory_id"], - constraint_name="unique_parent_directory_id", - ) diff --git a/mail/server/doctype/mail_server_listener/__init__.py b/mail/server/doctype/mail_server_listener/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/mail/server/doctype/mail_server_listener/mail_server_listener.json b/mail/server/doctype/mail_server_listener/mail_server_listener.json deleted file mode 100644 index 865b8ccce..000000000 --- a/mail/server/doctype/mail_server_listener/mail_server_listener.json +++ /dev/null @@ -1,78 +0,0 @@ -{ - "actions": [], - "creation": "2025-03-05 12:18:08.380756", - "doctype": "DocType", - "editable_grid": 1, - "engine": "InnoDB", - "field_order": [ - "section_break_vibk", - "protocol", - "listener_id", - "column_break_eecc", - "bind", - "tls_options_section", - "tls_implicit" - ], - "fields": [ - { - "fieldname": "section_break_vibk", - "fieldtype": "Section Break" - }, - { - "description": "The protocol used by the listener.", - "fieldname": "protocol", - "fieldtype": "Select", - "in_list_view": 1, - "label": "Protocol", - "options": "\nSMTP\nLMTP\nHTTP\nIMAP4\nPOP3\nManageSieve", - "reqd": 1 - }, - { - "description": "Unique identifier for the listener.", - "fieldname": "listener_id", - "fieldtype": "Data", - "in_list_view": 1, - "label": "Listener ID", - "reqd": 1 - }, - { - "fieldname": "tls_options_section", - "fieldtype": "Section Break", - "label": "TLS Options" - }, - { - "fieldname": "column_break_eecc", - "fieldtype": "Column Break" - }, - { - "description": "The addresses the listener will bind to.", - "fieldname": "bind", - "fieldtype": "Small Text", - "in_list_view": 1, - "label": "Bind Addresses", - "reqd": 1 - }, - { - "default": "0", - "description": "Whether to use implicit TLS.", - "fieldname": "tls_implicit", - "fieldtype": "Check", - "in_list_view": 1, - "label": "Implicit TLS" - } - ], - "grid_page_length": 50, - "index_web_pages_for_search": 1, - "istable": 1, - "links": [], - "modified": "2025-04-29 23:49:22.381106", - "modified_by": "Administrator", - "module": "Server", - "name": "Mail Server Listener", - "owner": "Administrator", - "permissions": [], - "row_format": "Dynamic", - "sort_field": "creation", - "sort_order": "DESC", - "states": [] -} diff --git a/mail/server/doctype/mail_server_listener/mail_server_listener.py b/mail/server/doctype/mail_server_listener/mail_server_listener.py deleted file mode 100644 index 0ad914138..000000000 --- a/mail/server/doctype/mail_server_listener/mail_server_listener.py +++ /dev/null @@ -1,17 +0,0 @@ -# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors -# For license information, please see license.txt - -import frappe -from frappe.model.document import Document - - -class MailServerListener(Document): - pass - - -def on_doctype_update() -> None: - frappe.db.add_unique( - "Mail Server Listener", - ["parenttype", "parent", "listener_id"], - constraint_name="unique_parent_listener_id", - ) diff --git a/mail/server/doctype/mail_server_tls_certificate/__init__.py b/mail/server/doctype/mail_server_tls_certificate/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/mail/server/doctype/mail_server_tls_certificate/mail_server_tls_certificate.json b/mail/server/doctype/mail_server_tls_certificate/mail_server_tls_certificate.json deleted file mode 100644 index 47fd68e2c..000000000 --- a/mail/server/doctype/mail_server_tls_certificate/mail_server_tls_certificate.json +++ /dev/null @@ -1,108 +0,0 @@ -{ - "actions": [], - "allow_rename": 1, - "creation": "2025-03-18 14:27:40.333851", - "doctype": "DocType", - "editable_grid": 1, - "engine": "InnoDB", - "field_order": [ - "certificate_id", - "column_break_rea4", - "default", - "section_break_v9po", - "cert_path", - "cert", - "column_break_09nn", - "private_key_path", - "private_key", - "section_break_nyxe", - "subjects", - "column_break_zfsr" - ], - "fields": [ - { - "fieldname": "section_break_v9po", - "fieldtype": "Section Break" - }, - { - "description": "Unique identifier for the TLS certificate.", - "fieldname": "certificate_id", - "fieldtype": "Data", - "in_list_view": 1, - "label": "Certificate ID", - "reqd": 1 - }, - { - "description": "Private key in PEM format.", - "fieldname": "private_key", - "fieldtype": "Text", - "label": "Private Key" - }, - { - "default": "0", - "description": "Whether this certificate should be the default when no SNI is provided.", - "fieldname": "default", - "fieldtype": "Check", - "in_list_view": 1, - "label": "Default" - }, - { - "fieldname": "column_break_rea4", - "fieldtype": "Column Break" - }, - { - "description": "File path to the Private Key.", - "fieldname": "private_key_path", - "fieldtype": "Data", - "in_list_view": 1, - "label": "Private Key Path", - "placeholder": "/opt/stalwart/etc/key.pem" - }, - { - "fieldname": "section_break_nyxe", - "fieldtype": "Section Break" - }, - { - "fieldname": "column_break_zfsr", - "fieldtype": "Column Break" - }, - { - "fieldname": "column_break_09nn", - "fieldtype": "Column Break" - }, - { - "description": "Subject Alternative Names (SAN) for the certificate.", - "fieldname": "subjects", - "fieldtype": "Small Text", - "label": "Subject Alternative Names" - }, - { - "description": "File path to the TLS certificate.", - "fieldname": "cert_path", - "fieldtype": "Data", - "in_list_view": 1, - "label": "Certificate Path", - "placeholder": "/opt/stalwart/etc/cert.pem" - }, - { - "description": "TLS certificate in PEM format.", - "fieldname": "cert", - "fieldtype": "Text", - "label": "Certificate" - } - ], - "grid_page_length": 50, - "index_web_pages_for_search": 1, - "istable": 1, - "links": [], - "modified": "2025-05-28 12:11:05.286896", - "modified_by": "Administrator", - "module": "Server", - "name": "Mail Server TLS Certificate", - "owner": "Administrator", - "permissions": [], - "row_format": "Dynamic", - "sort_field": "creation", - "sort_order": "DESC", - "states": [] -} diff --git a/mail/server/doctype/mail_server_tls_certificate/mail_server_tls_certificate.py b/mail/server/doctype/mail_server_tls_certificate/mail_server_tls_certificate.py deleted file mode 100644 index 78a8a5545..000000000 --- a/mail/server/doctype/mail_server_tls_certificate/mail_server_tls_certificate.py +++ /dev/null @@ -1,17 +0,0 @@ -# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors -# For license information, please see license.txt - -import frappe -from frappe.model.document import Document - - -class MailServerTLSCertificate(Document): - pass - - -def on_doctype_update() -> None: - frappe.db.add_unique( - "Mail Server TLS Certificate", - ["parenttype", "parent", "certificate_id"], - constraint_name="unique_parent_certificate_id", - ) From 320f778d4046a4cd04698388d7e2987e23b946c4 Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Tue, 5 May 2026 16:44:35 +0530 Subject: [PATCH 05/55] refactor: remove `Server Config` --- mail/server/doctype/server_config/__init__.py | 0 .../doctype/server_config/server_config.js | 61 -- .../doctype/server_config/server_config.json | 87 --- .../doctype/server_config/server_config.py | 612 ------------------ .../server_config/test_server_config.py | 29 - .../server_deployment/server_deployment.json | 27 +- mail/workspace_sidebar/server.json | 12 - 7 files changed, 6 insertions(+), 822 deletions(-) delete mode 100644 mail/server/doctype/server_config/__init__.py delete mode 100644 mail/server/doctype/server_config/server_config.js delete mode 100644 mail/server/doctype/server_config/server_config.json delete mode 100644 mail/server/doctype/server_config/server_config.py delete mode 100644 mail/server/doctype/server_config/test_server_config.py diff --git a/mail/server/doctype/server_config/__init__.py b/mail/server/doctype/server_config/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/mail/server/doctype/server_config/server_config.js b/mail/server/doctype/server_config/server_config.js deleted file mode 100644 index 55cac9a0b..000000000 --- a/mail/server/doctype/server_config/server_config.js +++ /dev/null @@ -1,61 +0,0 @@ -// Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors -// For license information, please see license.txt - -frappe.ui.form.on('Server Config', { - refresh(frm) { - frm.trigger('add_actions') - }, - - add_actions(frm) { - if (frm.doc.__islocal) return - - if (frappe.user_roles.includes('System Manager')) { - frm.add_custom_button( - __('Deploy'), - () => { - frappe.confirm(__('Are you sure you want to proceed?'), () => - frm.trigger('deploy'), - ) - }, - __('Actions'), - ) - frm.add_custom_button( - __('Update config.toml'), - () => { - frappe.confirm(__('Are you sure you want to proceed?'), () => - frm.trigger('update_config_on_server'), - ) - }, - __('Actions'), - ) - } - }, - - deploy(frm) { - frappe.call({ - doc: frm.doc, - method: 'deploy', - freeze: true, - freeze_message: __('Deploying Configuration...'), - callback: (r) => { - if (!r.exc) { - frm.refresh() - } - }, - }) - }, - - update_config_on_server(frm) { - frappe.call({ - doc: frm.doc, - method: 'update_config_on_server', - freeze: true, - freeze_message: __('Updating Configuration...'), - callback: (r) => { - if (!r.exc) { - frm.refresh() - } - }, - }) - }, -}) diff --git a/mail/server/doctype/server_config/server_config.json b/mail/server/doctype/server_config/server_config.json deleted file mode 100644 index 06fc67f65..000000000 --- a/mail/server/doctype/server_config/server_config.json +++ /dev/null @@ -1,87 +0,0 @@ -{ - "actions": [], - "autoname": "hash", - "creation": "2025-03-06 16:33:11.735196", - "doctype": "DocType", - "engine": "InnoDB", - "field_order": [ - "section_break_ulrw", - "server", - "section_break_ffqq", - "config_toml", - "config" - ], - "fields": [ - { - "fieldname": "section_break_ulrw", - "fieldtype": "Section Break" - }, - { - "fieldname": "server", - "fieldtype": "Link", - "in_list_view": 1, - "in_standard_filter": 1, - "label": "Server", - "no_copy": 1, - "options": "Mail Server", - "read_only": 1, - "reqd": 1, - "search_index": 1, - "set_only_once": 1 - }, - { - "fieldname": "section_break_ffqq", - "fieldtype": "Section Break" - }, - { - "fieldname": "config_toml", - "fieldtype": "Password", - "hidden": 1, - "label": "Config", - "no_copy": 1, - "read_only": 1, - "set_only_once": 1 - }, - { - "fieldname": "config", - "fieldtype": "Code", - "is_virtual": 1, - "label": "Config", - "read_only": 1 - } - ], - "grid_page_length": 50, - "in_create": 1, - "index_web_pages_for_search": 1, - "links": [ - { - "group": "Reference", - "link_doctype": "Server Deployment", - "link_fieldname": "config" - } - ], - "modified": "2025-09-27 11:33:26.552955", - "modified_by": "Administrator", - "module": "Server", - "name": "Server Config", - "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 - } - ], - "row_format": "Dynamic", - "sort_field": "creation", - "sort_order": "DESC", - "states": [] -} \ No newline at end of file diff --git a/mail/server/doctype/server_config/server_config.py b/mail/server/doctype/server_config/server_config.py deleted file mode 100644 index 67b8c6d91..000000000 --- a/mail/server/doctype/server_config/server_config.py +++ /dev/null @@ -1,612 +0,0 @@ -# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors -# For license information, please see license.txt - -import json -from uuid import uuid7 - -import frappe -from frappe import _ -from frappe.model.document import Document - -from mail.backend import get_mail_backend_api -from mail.jmap.connection import raise_for_status -from mail.utils import flatten_dict, password_or_none - -LOCAL_KEYS = [ - "acme.*", - "store.*", - "directory.*", - "tracer.*", - "metrics.*", - "!server.blocked-ip.*", - "!server.allowed-ip.*", - "server.*", - "authentication.fallback-admin.*", - "cluster.*", - "config.local-keys.*", - "storage.data", - "storage.blob", - "storage.lookup", - "storage.fts", - "storage.directory", - "storage.search-index.*", - "storage.undelete.retention", - "certificate.*", - "account.purge.frequency", - "jmap.push.*", - "jmap.email.*", - "jmap.mailbox.*", - "jmap.contact.*", - "jmap.calendar.*", - "jmap.protocol.*", - "email.encryption.*", - "email.auto-expunge", - "changes.max-history", - "enterprise.license-key", -] -STORE_TYPE_MAP = { - "RocksDB": "rocksdb", - "FoundationDB": "foundationdb", - "PostgreSQL": "postgresql", - "mySQL": "mysql", - "SQLite": "sqlite", - "S3-compatible": "s3", - "Redis/Memcached": "redis", - "ElasticSearch": "elasticsearch", - "Azure Blob Storage": "azure", - "Filesystem": "fs", - "SQL with Replicas": "sql-read-replica", - "Sharded Blob Store": "sharded-blob", - "Sharded In-Memory Store": "sharded-in-memory", -} -PROTOCOL_MAP = { - "SMTP": "smtp", - "LMTP": "lmtp", - "HTTP": "http", - "IMAP4": "imap", - "POP3": "pop3", - "ManageSieve": "managesieve", -} - - -class ServerConfig(Document): - @property - def config(self) -> str | None: - """Returns the TOML configuration for the Mail Server.""" - - if self.flags.in_delete: - return - - frappe.only_for("System Manager") - - if self.config_toml: - return self.get_password("config_toml") - - def autoname(self) -> None: - self.name = str(uuid7()) - - def before_insert(self) -> None: - self.generate_config_toml() - - def generate_config_toml(self) -> None: - """Generates the TOML configuration for the Mail Server.""" - - self.config_toml = get_config_toml(self.server) - - @frappe.whitelist() - def deploy(self) -> None: - """Deploys Stalwart with the current configuration.""" - - frappe.only_for("System Manager") - - server = frappe.get_doc("Mail Server", self.server) - server._install_stalwart(config=self.name) - frappe.msgprint(_("Deploy of Stalwart initiated."), indicator="green", alert=True) - - @frappe.whitelist() - def update_config_on_server(self) -> None: - """Updates the configuration on the Mail Server.""" - - frappe.only_for("System Manager") - - values = [] - for cfg in self.get_password("config_toml").split("\n"): - stripped = cfg.strip() - if not stripped or stripped.startswith("#"): - continue - - key, value = cfg.split("=", 1) - key = key.strip() - value = value.strip().strip('"') - values.append([key, value]) - - data = [ - { - "type": "insert", - "prefix": None, - "values": values, - "assert_empty": False, - } - ] - backend_api = get_mail_backend_api("Mail Server", self.server) - response = backend_api.request("POST", "/api/settings", data=json.dumps(data)) - raise_for_status(response) - - if response_json := response.json(): - if response_json.get("error"): - frappe.throw( - title=_("Failed to update configuration"), msg=json.dumps(response_json, indent=4) - ) - elif response_json.get("data") is None: - frappe.msgprint( - _("Configuration updated successfully."), - alert=True, - indicator="green", - ) - else: - frappe.msgprint( - _( - "Configuration updated successfully, but the response from the server was unexpected: {response}" - ).format(response=json.dumps(response_json)) - ) - - -def create_server_config(server: str) -> "ServerConfig": - """Creates a new Server Config.""" - - config = frappe.new_doc("Server Config") - config.server = server - config.insert() - - return config - - -def get_server_config(server: str) -> ServerConfig | None: - """Returns the Server Config.""" - - if config := frappe.db.exists("Server Config", {"server": server}): - return frappe.get_doc("Server Config", config) - - -def get_config_toml(server: str) -> str | None: - """Returns the TOML configuration for the Mail Server.""" - - def _format_value_or_zero(value: int, postfix: str) -> str | int: - return f"{value}{postfix}" if value else 0 - - def _wrap_in_triple_quotes(value: str) -> str: - return f"'''{value}'''" - - def _split_lines(value: str) -> list: - return value.split("\n") - - def _split_lines_or_empty(value: str) -> list: - return _split_lines(value) if value else [] - - def _format_keys(keys: list) -> dict: - width = len(str(len(keys) - 1)) - return {str(i).zfill(width): key for i, key in enumerate(keys)} - - def _get_acme_config(acme) -> dict: - config = { - "default": bool(acme.default), - "directory": acme.directory, - "challenge": acme.challenge.lower(), - "contact": _split_lines_or_empty(acme.contact), - "domains": _split_lines_or_empty(acme.domains), - "cache": "%{BASE_PATH}%/etc/acme", - "renew-before": _format_value_or_zero(acme.renew_before, "d"), - "eab": {"kid": acme.eab_kid, "hmac-key": password_or_none(acme, "eab_hmac_key")}, - } - - return {acme.directory_id: config} - - def _get_acme_providers(acme_providers: list) -> dict: - return {k: v for acme in acme_providers for k, v in _get_acme_config(acme).items()} - - def _get_tls_config(tls) -> dict: - cert = _wrap_in_triple_quotes(tls.cert) if tls.cert else f"%{{file:{tls.cert_path}}}%" - private_key = ( - _wrap_in_triple_quotes(tls.private_key) - if tls.private_key - else f"%{{file:{tls.private_key_path}}}%" - ) - config = { - "default": bool(tls.default), - "cert": cert, - "private-key": private_key, - "subjects": _split_lines_or_empty(tls.subjects), - } - - return {tls.certificate_id: config} - - def _get_tls_certificates(tls_certificates: list) -> dict: - return {k: v for tls in tls_certificates for k, v in _get_tls_config(tls).items()} - - def _get_listeners(listeners: list) -> dict: - result = {} - - for listener in listeners: - binds = _split_lines(listener.bind) - result[listener.listener_id] = { - "bind": _format_keys(binds) if len(binds) > 1 else binds[0], - "protocol": PROTOCOL_MAP[listener.protocol], - "tls": {"implicit": bool(listener.tls_implicit)}, - } - - return result - - def _get_local_keys(outbound_only: bool = False) -> dict: - local_keys = LOCAL_KEYS + ( - ["session.rcpt.directory", "queue.strategy.route"] if outbound_only else [] - ) - return _format_keys(local_keys) - - def _get_store_config(store) -> dict: - config = {"type": STORE_TYPE_MAP[store.type]} - - if store.type in ["SQLite", "PostgreSQL", "mySQL"]: - config.update({"pool": {"max-connections": store.pool_max_connections}}) - - if store.type in ["S3-compatible", "Azure Blob Storage"]: - config.update({"key-prefix": store.key_prefix, "max-retries": store.max_retries}) - - if store.type in ["PostgreSQL", "mySQL", "Redis/Memcached"]: - if not (store.type == "Redis/Memcached" and store.redis_type == "Redis Single Node"): - config.update({"user": store.user, "password": password_or_none(store, "password")}) - - if store.type in ["PostgreSQL", "mySQL", "S3-compatible", "Redis/Memcached", "Azure Blob Storage"]: - config["timeout"] = _format_value_or_zero(store.timeout, "s") - - if store.type in [ - "RocksDB", - "SQLite", - "Filesystem", - "FoundationDB", - "PostgreSQL", - "mySQL", - "S3-compatible", - "Azure Blob Storage", - ]: - config.update( - { - "compression": store.compression.lower(), - "purge": {"frequency": store.purge_frequency}, - } - ) - - match store.type: - case "RocksDB" | "SQLite" | "Filesystem": - config["path"] = store.path - - if store.type in ["RocksDB", "SQLite"]: - config["workers"] = store.workers - - if store.type == "RocksDB": - config.update( - { - "min-blob-size": store.min_blob_size, - "write-buffer-size": store.write_buffer_size, - } - ) - elif store.type == "Filesystem": - config["depth"] = store.depth - - case "FoundationDB": - config.update( - { - "cluster-file": store.cluster_file, - "transaction": { - "timeout": _format_value_or_zero(store.transaction_timeout, "s"), - "retry-limit": store.transaction_retry_limit, - "max-retry-delay": _format_value_or_zero(store.transaction_max_retry_delay, "s"), - }, - "ids": { - "machine": store.machine, - "datacenter": store.datacenter, - }, - } - ) - - case "PostgreSQL" | "mySQL": - config.update( - { - "host": store.host, - "port": store.port, - "database": store.database, - "tls": { - "enable": bool(store.tls_enable), - "allow-invalid-certs": bool(store.tls_allow_invalid_certs), - }, - } - ) - - if store.type == "mySQL": - config["max-allowed-packet"] = store.max_allowed_packet - config["pool"]["min-connections"] = store.pool_min_connections - - case "S3-compatible": - config.update( - { - "region": store.region, - "endpoint": store.endpoint, - "profile": store.profile, - "bucket": store.bucket, - "access-key": password_or_none(store, "access_key"), - "secret-key": password_or_none(store, "secret_key"), - "security-token": password_or_none(store, "security_token"), - } - ) - - case "Redis/Memcached": - redis_type = "single" if store.redis_type == "Redis Single Node" else "cluster" - config.update( - { - "redis-type": redis_type, - "urls": _split_lines_or_empty(store.urls), - } - ) - - if redis_type == "cluster": - config.update( - { - "read-from-replicas": bool(store.read_from_replicas), - "retry": { - "total": store.retry_total, - "max-wait": _format_value_or_zero(store.retry_max_wait, "ms"), - "min-wait": _format_value_or_zero(store.retry_min_wait, "ms"), - }, - } - ) - - case "ElasticSearch": - config.update( - { - "url": store.url, - "auth": { - "username": store.user, - "secret": password_or_none(store, "password"), - }, - "tls": {"allow-invalid-certs": bool(store.tls_allow_invalid_certs)}, - "index": { - "shards": store.index_shards, - "replicas": store.index_replicas, - }, - } - ) - - case "Azure Blob Storage": - config.update( - { - "storage-account": store.storage_account, - "container": store.container, - "azure-access-key": password_or_none(store, "azure_access_key"), - "sas-token": password_or_none(store, "sas_token"), - } - ) - - return {store.store_id: config} - - def _get_stores(stores: list) -> dict: - return {k: v for store in stores for k, v in _get_store_config(store).items()} - - def _get_traces(traces: list) -> dict: - result = {} - trace_types = { - "Log file": "log", - "Console": "console", - "Systemd Journal": "journal", - "Open Telemetry": "open-telemetry", - } - - for trace in traces: - result[trace.tracer_id] = { - "enable": True, - "type": trace_types[trace.type], - "level": trace.level.lower(), - "lossy": bool(trace.lossy), - } - - if trace.type == "Log file": - result[trace.tracer_id].update( - { - "path": trace.path, - "prefix": trace.prefix, - "rotate": trace.rotate.lower(), - "ansi": bool(trace.ansi), - "multiline": bool(trace.multiline), - } - ) - elif trace.type == "Console": - result[trace.tracer_id].update( - { - "ansi": bool(trace.ansi), - "multiline": bool(trace.multiline), - "buffer": bool(trace.buffer), - } - ) - elif trace.type == "Open Telemetry": - result[trace.tracer_id].update( - { - "transport": trace.transport.lower(), - "endpoint": trace.endpoint, - "timeout": _format_value_or_zero(trace.timeout, "s"), - "throttle": _format_value_or_zero(trace.throttle, "ms"), - "enable": { - "log-exporter": bool(trace.enable_log_exporter), - "span-exporter": bool(trace.enable_span_exporter), - }, - } - ) - - return result - - server = frappe.get_doc("Mail Server", server) - cluster = frappe.get_doc("Mail Cluster", server.cluster) - - config = { - "authentication": { - "fallback-admin": { - "user": cluster.fallback_admin_user, - "secret": cluster.fallback_admin_secret, - } - }, - "acme": _get_acme_providers(server.acme_providers), - "certificate": _get_tls_certificates(server.tls_certificates), - "server": { - "hostname": server.hostname, - "proxy": {"trusted-networks": _split_lines_or_empty(cluster.server_proxy_trusted_networks)}, - "max-connections": server.server_max_connections, - "listener": _get_listeners(server.listeners or cluster.listeners), - "socket": { - "backlog": 1024, - "nodelay": True, - "reuse-addr": True, - "reuse-port": True, - }, - }, - "cluster": { - "node-id": server.cluster_node_id, - }, - "config": { - "local-keys": _get_local_keys(bool(server.outbound_only)), - }, - "directory": { - f"{cluster.storage_directory}": { - "type": "internal", - "store": f"{cluster.storage_directory}", - "cache": { - "size": 1048576, - "ttl": { - "negative": "10m", - "positive": "1h", - }, - }, - } - }, - "storage": { - "directory": cluster.storage_directory, - "data": cluster.storage_data, - "blob": cluster.storage_blob, - "fts": cluster.storage_fts, - "search-index": { - "batch-size": cluster.storage_search_index_batch_size, - "default-language": cluster.storage_search_index_default_language, - "email": {"enable": bool(cluster.storage_search_index_email_enable)}, - "contacts": {"enable": bool(cluster.storage_search_index_contacts_enable)}, - "calendar": {"enable": bool(cluster.storage_search_index_calendar_enable)}, - "tracing": {"enable": bool(cluster.storage_search_index_tracing_enable)}, - }, - "lookup": cluster.storage_lookup, - "undelete": { - "retention": _format_value_or_zero(cluster.storage_undelete_retention, "d") - if cluster.storage_undelete_retention - else False - }, - }, - "account": {"purge": {"frequency": cluster.account_purge_frequency}}, - "email": { - "encryption": { - "enable": bool(cluster.email_encryption_enable), - "append": bool(cluster.email_encryption_append), - }, - "auto-expunge": _format_value_or_zero(cluster.email_auto_expunge, "d"), - }, - "changes": {"max-history": cluster.changes_max_history}, - "jmap": { - "email": { - "max-attachment-size": cluster.jmap_email_max_attachment_size, - "max-size": cluster.jmap_email_max_size, - "parse": {"max-items": cluster.jmap_email_parse_max_items}, - }, - "protocol": { - "changes": { - "max-results": cluster.jmap_protocol_changes_max_results, - }, - "request": { - "max-concurrent": cluster.jmap_protocol_request_max_concurrent, - "max-size": cluster.jmap_protocol_request_max_size, - "max-calls": cluster.jmap_protocol_request_max_calls, - }, - "get": {"max-objects": cluster.jmap_protocol_get_max_objects}, - "set": {"max-objects": cluster.jmap_protocol_set_max_objects}, - "query": {"max-results": cluster.jmap_protocol_query_max_results}, - "upload": { - "max-size": cluster.jmap_protocol_upload_max_size, - "max-concurrent": cluster.jmap_protocol_upload_max_concurrent, - "ttl": _format_value_or_zero(cluster.jmap_protocol_upload_ttl, "h"), - "quota": { - "files": cluster.jmap_protocol_upload_quota_files, - "size": cluster.jmap_protocol_upload_quota_size * 1024 * 1024, - }, - }, - }, - "mailbox": { - "max-depth": cluster.jmap_mailbox_max_depth, - "max-name-length": cluster.jmap_mailbox_max_name_length, - }, - "push": { - "throttle": _format_value_or_zero(cluster.jmap_push_throttle, "ms"), - "attempts": { - "interval": _format_value_or_zero(cluster.jmap_push_attempts_interval, "ms"), - "max": cluster.jmap_push_attempts_max, - }, - "retry": { - "interval": _format_value_or_zero(cluster.jmap_push_retry_interval, "ms"), - }, - "timeout": { - "request": _format_value_or_zero(cluster.jmap_push_timeout_request, "ms"), - "verify": _format_value_or_zero(cluster.jmap_push_timeout_verify, "ms"), - }, - }, - "calendar": { - "parse": { - "max-items": cluster.jmap_calendar_parse_max_items, - } - }, - "contact": {"parse": {"max-items": cluster.jmap_contact_parse_max_items}}, - }, - "store": _get_stores(cluster.stores), - "metrics": { - "open-telemetry": { - "transport": cluster.metrics_open_telemetry_transport.lower(), - "endpoint": cluster.metrics_open_telemetry_endpoint, - "timeout": _format_value_or_zero(cluster.metrics_open_telemetry_timeout, "s"), - "interval": _format_value_or_zero(cluster.metrics_open_telemetry_interval, "s"), - }, - "prometheus": { - "enable": bool(cluster.metrics_prometheus_enable), - "auth": { - "username": cluster.metrics_prometheus_auth_username, - "secret": password_or_none(cluster, "metrics_prometheus_auth_secret"), - }, - }, - }, - "tracer": _get_traces(cluster.traces), - } - - if server.outbound_only: - config.setdefault("session", {}).setdefault("rcpt", {})["directory"] = False - config.setdefault("queue", {}).setdefault("strategy", {})["route"] = "'mx'" - - toml_lines = [] - for key, value in sorted(flatten_dict(config).items()): - if value or isinstance(value, bool): - if isinstance(value, str): - if (value.startswith("[") and "{ if = " in value and "}" in value) or ( - key.startswith("certificate.") and value.startswith("'''") and value.endswith("'''") - ): - formatted_value = value - else: - formatted_value = f'"{value}"' - elif isinstance(value, bool): - formatted_value = str(value).lower() - elif isinstance(value, list): - formatted_list = ", ".join(f'"{v}"' if isinstance(v, str) else str(v) for v in value) - formatted_value = f"[{formatted_list}]" - else: - formatted_value = str(value) - - toml_lines.append(f"{key} = {formatted_value}") - - return "\n".join(toml_lines) + "\n" diff --git a/mail/server/doctype/server_config/test_server_config.py b/mail/server/doctype/server_config/test_server_config.py deleted file mode 100644 index d3dfcaf8a..000000000 --- a/mail/server/doctype/server_config/test_server_config.py +++ /dev/null @@ -1,29 +0,0 @@ -# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and Contributors -# See license.txt - -# import frappe -from frappe.tests import IntegrationTestCase, UnitTestCase - -# On IntegrationTestCase, the doctype test records and all -# link-field test record dependencies are recursively loaded -# Use these module variables to add/remove to/from that list -EXTRA_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"] -IGNORE_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"] - - -class UnitTestServerConfig(UnitTestCase): - """ - Unit tests for ServerConfig. - Use this class for testing individual functions and methods. - """ - - pass - - -class IntegrationTestServerConfig(IntegrationTestCase): - """ - Integration tests for ServerConfig. - Use this class for testing interactions between multiple components. - """ - - pass diff --git a/mail/server/doctype/server_deployment/server_deployment.json b/mail/server/doctype/server_deployment/server_deployment.json index ad8003610..b3603c5ad 100644 --- a/mail/server/doctype/server_deployment/server_deployment.json +++ b/mail/server/doctype/server_deployment/server_deployment.json @@ -19,8 +19,7 @@ "started_after", "duration", "section_break_gicu", - "config", - "config_checksum", + "column_break_slvv", "column_break_qjvy", "install_redis", "section_break_mndp", @@ -114,28 +113,10 @@ "fieldname": "section_break_gicu", "fieldtype": "Section Break" }, - { - "fieldname": "config", - "fieldtype": "Link", - "in_standard_filter": 1, - "label": "Config", - "options": "Server Config", - "reqd": 1, - "search_index": 1, - "set_only_once": 1 - }, { "fieldname": "column_break_qjvy", "fieldtype": "Column Break" }, - { - "fieldname": "config_checksum", - "fieldtype": "Small Text", - "hidden": 1, - "label": "Config Checksum", - "no_copy": 1, - "read_only": 1 - }, { "collapsible": 1, "collapsible_depends_on": "eval: false", @@ -201,6 +182,10 @@ "label": "Services", "options": "Server Deployment Service", "set_only_once": 1 + }, + { + "fieldname": "column_break_slvv", + "fieldtype": "Column Break" } ], "grid_page_length": 50, @@ -212,7 +197,7 @@ "link_fieldname": "deployment" } ], - "modified": "2025-11-17 11:29:27.009137", + "modified": "2026-05-05 16:41:28.623245", "modified_by": "Administrator", "module": "Server", "name": "Server Deployment", diff --git a/mail/workspace_sidebar/server.json b/mail/workspace_sidebar/server.json index 7fa81ccb7..1bdbc140a 100644 --- a/mail/workspace_sidebar/server.json +++ b/mail/workspace_sidebar/server.json @@ -99,18 +99,6 @@ "show_arrow": 0, "type": "Link" }, - { - "child": 1, - "collapsible": 1, - "icon": "book-text", - "indent": 0, - "keep_closed": 0, - "label": "Server Config", - "link_to": "Server Config", - "link_type": "DocType", - "show_arrow": 0, - "type": "Link" - }, { "child": 1, "collapsible": 1, From 0e2c12e9c6cae2ed9f0f0918d66955deb989e049 Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Wed, 6 May 2026 11:31:32 +0530 Subject: [PATCH 06/55] chore: install latest version of stalwart-cli --- mail/install.py | 28 +++++++++++++++++++--------- mail/utils/__init__.py | 9 ++++++++- 2 files changed, 27 insertions(+), 10 deletions(-) diff --git a/mail/install.py b/mail/install.py index 29b62e7de..e8abe6f1e 100644 --- a/mail/install.py +++ b/mail/install.py @@ -7,7 +7,7 @@ from frappe.core.api.file import create_new_folder from mail.mail.doctype.rate_limit.rate_limit import create_rate_limit -from mail.utils import get_mail_app_path, get_stalwart_cli_path, get_stalwart_version +from mail.utils import get_mail_app_path, get_stalwart_cli_path, get_stalwart_cli_version def after_install() -> None: @@ -75,20 +75,30 @@ def install_stalwart_cli() -> str: urllib.request.urlretrieve(url, tar_path) if frappe.conf.developer_mode: - print(f"\tExtracting {filename}...") + print(f"\tExtracting stalwart-cli from {filename}...") - with tarfile.open(tar_path, "r:gz") as tar: - tar.extractall(path=install_dir) + with tarfile.open(tar_path, "r:xz") as tar: + member = next( + (m for m in tar.getmembers() if os.path.basename(m.name) == "stalwart-cli"), + None, + ) + + if not member: + raise FileNotFoundError("stalwart-cli not found in archive") + + member.name = "stalwart-cli" + tar.extract(member, path=install_dir) cli_path = get_stalwart_cli_path() os.chmod(cli_path, 0o755) if frappe.conf.developer_mode: print(f"\tRemoving {tar_path}...") + os.remove(tar_path) if frappe.conf.developer_mode: - print(f"\tStalwart CLI installed to: {cli_path}") + print(f"\tStalwart CLI installed at: {cli_path}") return cli_path @@ -96,11 +106,11 @@ def install_stalwart_cli() -> str: def _get_stalwart_cli_download_url() -> str: """Returns the download URL and filename for the Stalwart CLI tool.""" - version = get_stalwart_version() + version = get_stalwart_cli_version() github_release_base = ( - "https://github.com/stalwartlabs/stalwart/releases/latest/download" + "https://github.com/stalwartlabs/cli/releases/latest/download" if version == "latest" - else f"https://github.com/stalwartlabs/stalwart/releases/download/{version}" + else f"https://github.com/stalwartlabs/cli/releases/download/{version}" ) system = platform.system().lower() @@ -120,5 +130,5 @@ def _get_stalwart_cli_download_url() -> str: else: raise Exception(f"Unsupported operating system: {system}") - filename = f"stalwart-cli-{arch}-{os_id}.tar.gz" + filename = f"stalwart-cli-{arch}-{os_id}.tar.xz" return f"{github_release_base}/{filename}", filename diff --git a/mail/utils/__init__.py b/mail/utils/__init__.py index 0b20c93d0..bd78bd5f7 100644 --- a/mail/utils/__init__.py +++ b/mail/utils/__init__.py @@ -105,7 +105,8 @@ def get_mail_config(key: str | None = None) -> dict[str, Any] | Any: "server_deployment_timeout": 1500, "server_job_timeout": 1500, "stalwart_cli_command_timeout": 3600, - "stalwart_version": "v0.15.4", + "stalwart_cli_version": "latest", + "stalwart_version": "v0.16.4", } config = frappe.conf.mail or {} @@ -864,3 +865,9 @@ def get_stalwart_version() -> str: """Returns the Stalwart version from configuration or default.""" return get_mail_config("stalwart_version") + + +def get_stalwart_cli_version() -> str: + """Returns the Stalwart CLI version from configuration or default.""" + + return get_mail_config("stalwart_cli_version") From f44caeaa200d018b8c0773c3e01abffaa29d83c4 Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Wed, 6 May 2026 23:59:34 +0530 Subject: [PATCH 07/55] refactor: remove `Message Queue` --- mail/server/doctype/message_queue/__init__.py | 0 .../doctype/message_queue/message_queue.js | 155 ----------- .../doctype/message_queue/message_queue.json | 137 ---------- .../doctype/message_queue/message_queue.py | 254 ------------------ .../message_queue/message_queue_list.js | 81 ------ .../message_queue/test_message_queue.py | 29 -- .../message_queue_recipient/__init__.py | 0 .../message_queue_recipient.json | 131 --------- .../message_queue_recipient.py | 31 --- 9 files changed, 818 deletions(-) delete mode 100644 mail/server/doctype/message_queue/__init__.py delete mode 100644 mail/server/doctype/message_queue/message_queue.js delete mode 100644 mail/server/doctype/message_queue/message_queue.json delete mode 100644 mail/server/doctype/message_queue/message_queue.py delete mode 100644 mail/server/doctype/message_queue/message_queue_list.js delete mode 100644 mail/server/doctype/message_queue/test_message_queue.py delete mode 100644 mail/server/doctype/message_queue_recipient/__init__.py delete mode 100644 mail/server/doctype/message_queue_recipient/message_queue_recipient.json delete mode 100644 mail/server/doctype/message_queue_recipient/message_queue_recipient.py diff --git a/mail/server/doctype/message_queue/__init__.py b/mail/server/doctype/message_queue/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/mail/server/doctype/message_queue/message_queue.js b/mail/server/doctype/message_queue/message_queue.js deleted file mode 100644 index dad29e3d9..000000000 --- a/mail/server/doctype/message_queue/message_queue.js +++ /dev/null @@ -1,155 +0,0 @@ -// Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors -// For license information, please see license.txt - -frappe.ui.form.on('Message Queue', { - refresh(frm) { - frm.trigger('add_actions') - }, - - add_actions(frm) { - if (!frappe.user_roles.includes('System Manager')) return - - frm.page.set_primary_action('Retry', () => frm.trigger('retry_delivery')) - frm.page.set_secondary_action('Cancel', () => frm.trigger('cancel_delivery')) - }, - - retry_delivery(frm) { - frappe.call({ - doc: frm.doc, - method: 'retry_delivery', - freeze: true, - freeze_message: __('Retrying Delivery...'), - callback: () => { - frm.reload_doc() - }, - }) - }, - - cancel_delivery(frm) { - const dialog = new frappe.ui.Dialog({ - title: __('Cancel Delivery'), - size: 'large', - fields: [ - { - fieldname: 'recipients', - fieldtype: 'Table', - label: __('Recipients'), - allow_bulk_edit: false, - cannot_add_rows: true, - cannot_delete_rows: true, - data: [], - fields: [ - { fieldtype: 'Section Break' }, - { - fieldname: 'email', - fieldtype: 'Data', - label: __('Email'), - in_list_view: 1, - read_only: 1, - }, - { - fieldname: 'domain_name', - fieldtype: 'Data', - label: __('Domain Name'), - read_only: 1, - }, - { fieldtype: 'Column Break' }, - { - fieldname: 'status', - fieldtype: 'Data', - label: __('Status'), - in_list_view: 1, - read_only: 1, - }, - - { - fieldname: 'retry_num', - fieldtype: 'Int', - label: __('Retry Num'), - in_list_view: 1, - read_only: 1, - }, - { fieldtype: 'Section Break' }, - { - fieldname: 'next_retry', - fieldtype: 'Datetime', - label: __('Next Retry'), - in_list_view: 1, - read_only: 1, - }, - { - fieldname: 'next_notify', - fieldtype: 'Datetime', - label: __('Next Notify'), - read_only: 1, - }, - { fieldtype: 'Column Break' }, - { - fieldname: 'expires', - fieldtype: 'Datetime', - label: __('Expires'), - read_only: 1, - }, - { fieldtype: 'Section Break' }, - { - fieldname: 'server_response', - fieldtype: 'JSON', - label: __('Server Response'), - read_only: 1, - }, - ], - }, - ], - primary_action_label: __('Cancel Delivery'), - primary_action: () => { - const data = { - recipients: dialog.fields_dict.recipients.grid.get_selected_children(), - } - - if (data.recipients && data.recipients.length > 0) { - frappe.call({ - doc: frm.doc, - method: 'cancel_delivery', - args: { - recipients: data.recipients.map((recipient) => recipient.email), - }, - freeze: true, - freeze_message: __('Cancelling Delivery...'), - callback: () => { - if ( - data.recipients.length == - dialog.fields_dict.recipients.df.data.length - ) { - frappe.set_route('List', 'Message Queue') - } else { - frm.reload_doc() - } - }, - }) - - dialog.hide() - } else { - frappe.msgprint(__('Please select recipients to cancel delivery.')) - } - }, - }) - - frm.doc.recipients.forEach((recipient) => { - if (recipient.status !== 'Permanent Failure') { - dialog.fields_dict.recipients.df.data.push({ - email: recipient.email, - domain_name: recipient.domain_name, - status: recipient.status, - retry_num: recipient.retry_num, - next_retry: recipient.next_retry, - next_notify: recipient.next_notify, - expires: recipient.expires, - server_response: recipient.server_response, - }) - } - }) - - dialog.fields_dict.recipients.grid.refresh() - dialog.show() - }, -}) diff --git a/mail/server/doctype/message_queue/message_queue.json b/mail/server/doctype/message_queue/message_queue.json deleted file mode 100644 index 8eab2c320..000000000 --- a/mail/server/doctype/message_queue/message_queue.json +++ /dev/null @@ -1,137 +0,0 @@ -{ - "actions": [], - "creation": "2025-03-26 19:23:11.587142", - "doctype": "DocType", - "engine": "InnoDB", - "field_order": [ - "section_break_yoky", - "env_id", - "created_at", - "return_path", - "message_size", - "column_break_0til", - "blob_hash", - "section_break_nxzs", - "text", - "domains", - "section_break_x3li", - "recipients", - "section_break_l6ts", - "message" - ], - "fields": [ - { - "fieldname": "section_break_yoky", - "fieldtype": "Section Break" - }, - { - "fieldname": "return_path", - "fieldtype": "Data", - "in_list_view": 1, - "label": "Return Path", - "read_only": 1 - }, - { - "fieldname": "created_at", - "fieldtype": "Datetime", - "in_list_view": 1, - "label": "Created At", - "read_only": 1 - }, - { - "fieldname": "message_size", - "fieldtype": "Int", - "in_list_view": 1, - "label": "Message Size", - "non_negative": 1, - "options": "File Size", - "read_only": 1 - }, - { - "fieldname": "blob_hash", - "fieldtype": "Data", - "label": "Blob Hash", - "read_only": 1 - }, - { - "fieldname": "recipients", - "fieldtype": "Table", - "label": "Recipients", - "options": "Message Queue Recipient", - "read_only": 1 - }, - { - "fieldname": "column_break_0til", - "fieldtype": "Column Break" - }, - { - "fieldname": "domains", - "fieldtype": "JSON", - "hidden": 1, - "label": "Domains", - "read_only": 1 - }, - { - "fieldname": "section_break_x3li", - "fieldtype": "Section Break" - }, - { - "fieldname": "section_break_l6ts", - "fieldtype": "Section Break" - }, - { - "depends_on": "eval: doc.env_id", - "fieldname": "env_id", - "fieldtype": "Data", - "label": "Env ID", - "read_only": 1 - }, - { - "fieldname": "text", - "fieldtype": "Data", - "hidden": 1, - "in_standard_filter": 1, - "label": "Text", - "read_only": 1 - }, - { - "collapsible": 1, - "fieldname": "section_break_nxzs", - "fieldtype": "Section Break" - }, - { - "fieldname": "message", - "fieldtype": "Code", - "label": "Message", - "read_only": 1 - } - ], - "grid_page_length": 50, - "in_create": 1, - "index_web_pages_for_search": 1, - "is_virtual": 1, - "links": [], - "modified": "2026-04-23 14:38:38.553369", - "modified_by": "Administrator", - "module": "Server", - "name": "Message Queue", - "owner": "Administrator", - "permissions": [ - { - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "print": 1, - "read": 1, - "report": 1, - "role": "System Manager", - "share": 1, - "write": 1 - } - ], - "row_format": "Dynamic", - "sort_field": "creation", - "sort_order": "DESC", - "states": [] -} diff --git a/mail/server/doctype/message_queue/message_queue.py b/mail/server/doctype/message_queue/message_queue.py deleted file mode 100644 index a78bc16b7..000000000 --- a/mail/server/doctype/message_queue/message_queue.py +++ /dev/null @@ -1,254 +0,0 @@ -# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors -# For license information, please see license.txt - -import json - -import frappe -from frappe import _ -from frappe.model.document import Document - -from mail.backend import get_mail_backend_api -from mail.utils import extract_filter_values, rename_keys - - -class MessageQueue(Document): - def db_insert(self, *args, **kwargs) -> None: - raise self._create() - - def load_from_db(self) -> "MessageQueue": - message = self._get() - return super(Document, self).__init__(message) - - def db_update(self) -> None: - MessageQueue._update(self.name) - - def delete(self) -> None: - self._delete() - if not frappe.flags.in_bulk_delete: - frappe.msgprint(_("Message deleted successfully."), alert=True) - - @staticmethod - def get_list(filters=None, page_length=20, **kwargs) -> list: - filters = filters or [] - text = extract_filter_values(filters, [{"text": "like"}])[0] - - messages = MessageQueue._get_all(limit=page_length, text=text) - if not messages: - frappe.msgprint(_("No messages found."), alert=True) - - return messages - - @staticmethod - def get_count(filters=None, **kwargs) -> int: - filters = filters or [] - text = extract_filter_values(filters, [{"text": "like"}])[0] - - return frappe.cache.get_value(get_total_cache_key(text)) if text else 0 - - @staticmethod - def get_stats(**kwargs) -> dict: - return {} - - @frappe.whitelist() - def retry_delivery(self) -> None: - """Retries delivery of a message to all recipients.""" - - frappe.only_for("System Manager") - MessageQueue._update(self.name) - frappe.msgprint(_("Delivery retried successfully."), alert=True) - - @frappe.whitelist() - def cancel_delivery(self, recipients: list[str]) -> None: - """Cancels delivery of a message to specific recipients.""" - - frappe.only_for("System Manager") - - recipients = list(set(recipients)) - for recipient in recipients: - self._delete(recipient) - - frappe.msgprint(_("Delivery cancelled successfully."), alert=True) - - def _create(self) -> None: - raise NotImplementedError - - def _get(self) -> dict: - """Returns the message details from the server.""" - - backend_api = get_mail_backend_api() - response = backend_api.request(method="GET", endpoint=f"/api/queue/messages/{self.name}") - - message = response.json()["data"] - message = MessageQueue._format(message, extract_recipients=True) - message["message"] = MessageQueue._get_blob(message["blob_hash"]) - - return message - - @staticmethod - def _get_blob(blob_id: str) -> str: - """Returns the raw message blob from the server.""" - - backend_api = get_mail_backend_api() - response = backend_api.request(method="GET", endpoint=f"/api/store/blobs/{blob_id}") - return response.text.strip() - - @staticmethod - def _get_all(page: int = 1, limit: int = 10, text: str | None = None) -> list: - """Returns all messages from the server.""" - - backend_api = get_mail_backend_api() - response = backend_api.request( - method="GET", - endpoint="/api/queue/messages", - params={"page": page, "limit": limit, "values": 1, "text": text}, - ) - - data = response.json()["data"] - frappe.cache.set_value(get_status_cache_key(), data["status"], expires_in_sec=600) - frappe.cache.set_value(get_total_cache_key(text), data["total"], expires_in_sec=600) - - return [MessageQueue._format(item) for item in data["items"]] - - @staticmethod - def _update(name: str) -> None: - """Retries delivery of a message to all recipients.""" - - backend_api = get_mail_backend_api() - backend_api.request(method="PATCH", endpoint=f"/api/queue/messages/{name}") - - def _delete(self, recipient: str | None = None) -> None: - """Deletes a message or cancels delivery to a specific recipient.""" - - backend_api = get_mail_backend_api() - backend_api.request( - method="DELETE", endpoint=f"/api/queue/messages/{self.name}", params={"filter": recipient} - ) - - @staticmethod - def _pause() -> None: - """Pauses queue processing on the server.""" - - backend_api = get_mail_backend_api() - backend_api.request( - method="PATCH", - endpoint="/api/queue/status/stop", - ) - - @staticmethod - def _resume() -> None: - """Resumes queue processing on the server.""" - - backend_api = get_mail_backend_api() - backend_api.request( - method="PATCH", - endpoint="/api/queue/status/start", - ) - - @staticmethod - def _format(message: dict, extract_recipients: bool = False) -> dict: - """Formats the message details.""" - - if extract_recipients: - recipients = [] - for recipient in message["recipients"]: - status = "Scheduled" - - server_response = None - if recipient["status"] != status.lower(): - rcpt_status = recipient["status"] - if rcpt_status.get("temp_fail", False): - status = "Temporary Failure" - elif rcpt_status.get("perm_fail", False): - status = "Permanent Failure" - - server_response = json.dumps(rcpt_status, indent=4) - - recipients.append( - { - "email": recipient["address"], - "domain_name": recipient["address"].split("@")[-1], - "original_rcpt": recipient.get("orcpt"), - "status": status, - "queue": recipient["queue"], - "retry_num": recipient["retry_num"], - "next_retry": recipient["next_retry"], - "next_notify": recipient["next_notify"], - "expires": recipient.get("expires"), - "server_response": server_response, - } - ) - - message["recipients"] = recipients - - message = rename_keys( - message, - { - "size": "message_size", - "created": "created_at", - }, - ) - - message.update( - { - "name": str(message.pop("id")), - "creation": message["created_at"], - "modified": message["created_at"], - "domains": json.dumps(message.get("domains", []), indent=4), - } - ) - - return message - - -@frappe.whitelist() -def get_queue_status() -> bool: - """Returns the status of the message queue.""" - - frappe.only_for("System Manager") - return bool(frappe.cache.get_value(get_status_cache_key())) - - -@frappe.whitelist() -def pause_queue() -> None: - """Pauses queue processing on the mail server.""" - - frappe.only_for("System Manager") - MessageQueue._pause() - frappe.msgprint(_("Queue paused successfully."), alert=True) - - -@frappe.whitelist() -def resume_queue() -> None: - """Resumes queue processing on the mail server.""" - - frappe.only_for("System Manager") - MessageQueue._resume() - frappe.msgprint(_("Queue resumed successfully."), alert=True) - - -@frappe.whitelist() -def bulk_retry_delivery(names: str | list[str]) -> None: - """Retries delivery of messages to all recipients.""" - - frappe.only_for("System Manager") - - if isinstance(names, str): - names = json.loads(names) - - for name in names: - MessageQueue._update(name) - - frappe.msgprint(_("Delivery retried successfully."), alert=True) - - -def get_status_cache_key() -> str: - """Returns a cache key for message status.""" - - return "message-queue:status" - - -def get_total_cache_key(text: str | None = None) -> str: - """Returns a cache key for total message count.""" - - text = text or "" - return f"message-queue:{text}:total" diff --git a/mail/server/doctype/message_queue/message_queue_list.js b/mail/server/doctype/message_queue/message_queue_list.js deleted file mode 100644 index a74394b22..000000000 --- a/mail/server/doctype/message_queue/message_queue_list.js +++ /dev/null @@ -1,81 +0,0 @@ -// Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors -// For license information, please see license.txt - -frappe.listview_settings['Message Queue'] = { - refresh: (listview) => { - add_pause_resume_buttons(listview) - add_bulk_retry_button_to_actions(listview) - }, -} - -function add_pause_resume_buttons(listview) { - if (!frappe.user_roles.includes('System Manager')) return - if (!listview.filters) return - - frappe.call({ - method: 'mail.server.doctype.message_queue.message_queue.get_queue_status', - freeze: true, - freeze_message: __('Getting Queue Status...'), - callback: (r) => { - if (!r.exc) { - update_pause_resume_buttons(listview, r.message) - } - }, - }) -} - -function update_pause_resume_buttons(listview, status) { - if (status) { - listview.page.remove_inner_button('Resume') - listview.page.add_inner_button(__('Pause'), () => { - pause_queue(listview) - }) - } else { - listview.page.remove_inner_button('Pause') - listview.page.add_inner_button(__('Resume'), () => { - resume_queue(listview) - }) - } -} - -function pause_queue(listview) { - frappe.call({ - method: 'mail.server.doctype.message_queue.message_queue.pause_queue', - freeze: true, - freeze_message: __('Pausing Queue...'), - callback: (r) => { - if (!r.exc) { - update_pause_resume_buttons(listview, false) - } - }, - }) -} - -function resume_queue(listview) { - frappe.call({ - method: 'mail.server.doctype.message_queue.message_queue.resume_queue', - freeze: true, - freeze_message: __('Resuming Queue...'), - callback: (r) => { - if (!r.exc) { - update_pause_resume_buttons(listview, true) - } - }, - }) -} - -function add_bulk_retry_button_to_actions(listview) { - listview.page.add_actions_menu_item(__('Retry'), () => { - frappe.call({ - method: 'mail.server.doctype.message_queue.message_queue.bulk_retry_delivery', - args: { - names: listview.get_checked_items(true), - }, - callback: (r) => { - if (!r.exc) { - listview.refresh() - } - }, - }) - }) -} diff --git a/mail/server/doctype/message_queue/test_message_queue.py b/mail/server/doctype/message_queue/test_message_queue.py deleted file mode 100644 index 09a6a53a2..000000000 --- a/mail/server/doctype/message_queue/test_message_queue.py +++ /dev/null @@ -1,29 +0,0 @@ -# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and Contributors -# See license.txt - -# import frappe -from frappe.tests import IntegrationTestCase, UnitTestCase - -# On IntegrationTestCase, the doctype test records and all -# link-field test record dependencies are recursively loaded -# Use these module variables to add/remove to/from that list -EXTRA_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"] -IGNORE_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"] - - -class UnitTestMessageQueue(UnitTestCase): - """ - Unit tests for MessageQueue. - Use this class for testing individual functions and methods. - """ - - pass - - -class IntegrationTestMessageQueue(IntegrationTestCase): - """ - Integration tests for MessageQueue. - Use this class for testing interactions between multiple components. - """ - - pass diff --git a/mail/server/doctype/message_queue_recipient/__init__.py b/mail/server/doctype/message_queue_recipient/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/mail/server/doctype/message_queue_recipient/message_queue_recipient.json b/mail/server/doctype/message_queue_recipient/message_queue_recipient.json deleted file mode 100644 index 771792619..000000000 --- a/mail/server/doctype/message_queue_recipient/message_queue_recipient.json +++ /dev/null @@ -1,131 +0,0 @@ -{ - "actions": [], - "allow_rename": 1, - "creation": "2025-03-26 21:47:15.026714", - "doctype": "DocType", - "editable_grid": 1, - "engine": "InnoDB", - "field_order": [ - "section_break_1jom", - "email", - "domain_name", - "original_rcpt", - "column_break_dffn", - "status", - "queue", - "retry_num", - "section_break_2qkq", - "next_retry", - "next_notify", - "column_break_vbsz", - "expires", - "section_break_5xmz", - "server_response" - ], - "fields": [ - { - "fieldname": "section_break_1jom", - "fieldtype": "Section Break" - }, - { - "fieldname": "domain_name", - "fieldtype": "Data", - "label": "Domain Name", - "read_only": 1 - }, - { - "fieldname": "status", - "fieldtype": "Data", - "in_list_view": 1, - "label": "Status", - "read_only": 1 - }, - { - "fieldname": "retry_num", - "fieldtype": "Int", - "in_list_view": 1, - "label": "Retry Num", - "non_negative": 1, - "read_only": 1 - }, - { - "fieldname": "next_retry", - "fieldtype": "Datetime", - "in_list_view": 1, - "label": "Next Retry", - "read_only": 1 - }, - { - "fieldname": "next_notify", - "fieldtype": "Datetime", - "label": "Next Notify", - "read_only": 1 - }, - { - "fieldname": "expires", - "fieldtype": "Datetime", - "label": "Expires", - "read_only": 1 - }, - { - "fieldname": "email", - "fieldtype": "Data", - "in_list_view": 1, - "label": "Email", - "length": 255, - "read_only": 1 - }, - { - "fieldname": "column_break_dffn", - "fieldtype": "Column Break" - }, - { - "fieldname": "section_break_2qkq", - "fieldtype": "Section Break" - }, - { - "fieldname": "column_break_vbsz", - "fieldtype": "Column Break" - }, - { - "fieldname": "section_break_5xmz", - "fieldtype": "Section Break" - }, - { - "fieldname": "server_response", - "fieldtype": "JSON", - "label": "Server Response", - "read_only": 1 - }, - { - "fieldname": "queue", - "fieldtype": "Data", - "in_list_view": 1, - "label": "Queue", - "read_only": 1 - }, - { - "depends_on": "eval: doc.email != doc.original_rcpt", - "fieldname": "original_rcpt", - "fieldtype": "Data", - "label": "Original RCPT", - "length": 255, - "read_only": 1 - } - ], - "grid_page_length": 50, - "index_web_pages_for_search": 1, - "is_virtual": 1, - "istable": 1, - "links": [], - "modified": "2025-08-13 17:42:34.698050", - "modified_by": "Administrator", - "module": "Server", - "name": "Message Queue Recipient", - "owner": "Administrator", - "permissions": [], - "row_format": "Dynamic", - "sort_field": "creation", - "sort_order": "DESC", - "states": [] -} diff --git a/mail/server/doctype/message_queue_recipient/message_queue_recipient.py b/mail/server/doctype/message_queue_recipient/message_queue_recipient.py deleted file mode 100644 index bec348be7..000000000 --- a/mail/server/doctype/message_queue_recipient/message_queue_recipient.py +++ /dev/null @@ -1,31 +0,0 @@ -# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors -# For license information, please see license.txt - -# import frappe -from frappe.model.document import Document - - -class MessageQueueRecipient(Document): - def db_insert(self, *args, **kwargs): - raise NotImplementedError - - def load_from_db(self): - raise NotImplementedError - - def db_update(self): - raise NotImplementedError - - def delete(self): - raise NotImplementedError - - @staticmethod - def get_list(filters=None, page_length=20, **kwargs): - pass - - @staticmethod - def get_count(filters=None, **kwargs): - pass - - @staticmethod - def get_stats(**kwargs): - pass From 01c978361c71c8f5539b8299547799feef1cba34 Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Thu, 7 May 2026 00:00:05 +0530 Subject: [PATCH 08/55] refactor: remove `Allowed IP` and `Blocked IP` --- mail/server/doctype/allowed_ip/__init__.py | 0 mail/server/doctype/allowed_ip/allowed_ip.js | 8 - .../server/doctype/allowed_ip/allowed_ip.json | 65 -------- mail/server/doctype/allowed_ip/allowed_ip.py | 149 ------------------ .../doctype/allowed_ip/test_allowed_ip.py | 29 ---- mail/server/doctype/blocked_ip/__init__.py | 0 mail/server/doctype/blocked_ip/blocked_ip.js | 8 - .../server/doctype/blocked_ip/blocked_ip.json | 65 -------- mail/server/doctype/blocked_ip/blocked_ip.py | 149 ------------------ .../doctype/blocked_ip/test_blocked_ip.py | 29 ---- 10 files changed, 502 deletions(-) delete mode 100644 mail/server/doctype/allowed_ip/__init__.py delete mode 100644 mail/server/doctype/allowed_ip/allowed_ip.js delete mode 100644 mail/server/doctype/allowed_ip/allowed_ip.json delete mode 100644 mail/server/doctype/allowed_ip/allowed_ip.py delete mode 100644 mail/server/doctype/allowed_ip/test_allowed_ip.py delete mode 100644 mail/server/doctype/blocked_ip/__init__.py delete mode 100644 mail/server/doctype/blocked_ip/blocked_ip.js delete mode 100644 mail/server/doctype/blocked_ip/blocked_ip.json delete mode 100644 mail/server/doctype/blocked_ip/blocked_ip.py delete mode 100644 mail/server/doctype/blocked_ip/test_blocked_ip.py diff --git a/mail/server/doctype/allowed_ip/__init__.py b/mail/server/doctype/allowed_ip/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/mail/server/doctype/allowed_ip/allowed_ip.js b/mail/server/doctype/allowed_ip/allowed_ip.js deleted file mode 100644 index 576a3944e..000000000 --- a/mail/server/doctype/allowed_ip/allowed_ip.js +++ /dev/null @@ -1,8 +0,0 @@ -// Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors -// For license information, please see license.txt - -// frappe.ui.form.on("Allowed IP", { -// refresh(frm) { - -// }, -// }); diff --git a/mail/server/doctype/allowed_ip/allowed_ip.json b/mail/server/doctype/allowed_ip/allowed_ip.json deleted file mode 100644 index 33f35c227..000000000 --- a/mail/server/doctype/allowed_ip/allowed_ip.json +++ /dev/null @@ -1,65 +0,0 @@ -{ - "actions": [], - "creation": "2025-04-07 11:30:16.487687", - "doctype": "DocType", - "engine": "InnoDB", - "field_order": [ - "section_break_f82v", - "ip_address", - "column_break_3xet", - "host" - ], - "fields": [ - { - "fieldname": "section_break_f82v", - "fieldtype": "Section Break" - }, - { - "fieldname": "column_break_3xet", - "fieldtype": "Column Break" - }, - { - "fieldname": "ip_address", - "fieldtype": "Data", - "in_list_view": 1, - "in_standard_filter": 1, - "label": "IP Address", - "reqd": 1, - "set_only_once": 1 - }, - { - "fieldname": "host", - "fieldtype": "Data", - "is_virtual": 1, - "label": "Host", - "read_only": 1 - } - ], - "grid_page_length": 50, - "index_web_pages_for_search": 1, - "is_virtual": 1, - "links": [], - "modified": "2026-04-22 16:50:12.860057", - "modified_by": "Administrator", - "module": "Server", - "name": "Allowed IP", - "owner": "Administrator", - "permissions": [ - { - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "print": 1, - "read": 1, - "report": 1, - "role": "System Manager", - "share": 1, - "write": 1 - } - ], - "row_format": "Dynamic", - "sort_field": "creation", - "sort_order": "DESC", - "states": [] -} diff --git a/mail/server/doctype/allowed_ip/allowed_ip.py b/mail/server/doctype/allowed_ip/allowed_ip.py deleted file mode 100644 index 1e2545e5a..000000000 --- a/mail/server/doctype/allowed_ip/allowed_ip.py +++ /dev/null @@ -1,149 +0,0 @@ -# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors -# For license information, please see license.txt - -import json - -import frappe -from frappe import _ -from frappe.model.document import Document -from frappe.utils import today - -from mail.backend import get_mail_backend_api -from mail.utils import extract_filter_values -from mail.utils.dns import get_host_by_ip - - -class AllowedIP(Document): - @property - def host(self) -> str | None: - """Returns the host name of the IP address.""" - - if self.ip_address: - return get_host_by_ip(self.ip_address) - - def autoname(self) -> None: - self.name = self.ip_address - - def db_insert(self, *args, **kwargs) -> None: - self._create() - - def load_from_db(self) -> "AllowedIP": - allowed_ip = self._get() - return super(Document, self).__init__(allowed_ip) - - def db_update(self) -> None: - self._update() - - def delete(self) -> None: - self._delete() - if not frappe.flags.in_bulk_delete: - frappe.msgprint(_("Allowed IP removed successfully."), alert=True) - - @staticmethod - def get_list(filters=None, page_length=20, **kwargs) -> list: - filters = filters or [] - ip_address = extract_filter_values(filters, [{"ip_address": "like"}])[0] - - allowed_ips = AllowedIP._get_all(limit=page_length, text=ip_address) - if not allowed_ips: - frappe.msgprint(_("No allowed IPs found."), alert=True) - - return allowed_ips - - @staticmethod - def get_count(filters=None, **kwargs) -> int: - filters = filters or [] - ip_address = extract_filter_values(filters, [{"ip_address": "like"}])[0] - - return frappe.cache.get_value(get_total_cache_key(ip_address)) if ip_address else 0 - - @staticmethod - def get_stats(**kwargs) -> dict: - return {} - - def _create(self) -> None: - """Creates the allowed IP in the backend.""" - - ip_addresses = [self.ip_address] - request_data = [] - for ip in ip_addresses: - request_data.append( - { - "type": "insert", - "prefix": None, - "values": [[f"server.allowed-ip.{ip}", ""]], - "assert_empty": True, - } - ) - - backend_api = get_mail_backend_api() - backend_api.request( - method="POST", - endpoint="/api/settings", - data=json.dumps(request_data), - ) - - def _get(self) -> None: - """Returns the allowed IP from the backend.""" - - ip_address = self.name - backend_api = get_mail_backend_api() - response = backend_api.request( - method="GET", - endpoint="api/settings/group", - params={"prefix": "server.allowed-ip", "limit": 1, "filter": ip_address}, - ) - - allowed_ip = response.json()["data"]["items"][0] - return AllowedIP._format(allowed_ip) - - @staticmethod - def _get_all(page: int = 1, limit: int = 10, text: str | None = None) -> list: - """Returns all allowed IPs.""" - backend_api = get_mail_backend_api() - response = backend_api.request( - method="GET", - endpoint="api/settings/group", - params={"page": page, "prefix": "server.allowed-ip", "limit": limit, "filter": text}, - ) - - data = response.json()["data"] - frappe.cache.set_value(get_total_cache_key(text), data["total"], expires_in_sec=600) - - return [AllowedIP._format(item) for item in data["items"]] - - def _update(self) -> None: - raise NotImplementedError - - def _delete(self) -> None: - """Deletes the allowed IP from the backend.""" - - ip_addresses = [self.ip_address] - request_data = [] - for ip in ip_addresses: - request_data.append({"type": "delete", "keys": [f"server.allowed-ip.{ip}"]}) - - backend_api = get_mail_backend_api() - backend_api.request( - method="POST", - endpoint="/api/settings", - data=json.dumps(request_data), - ) - - @staticmethod - def _format(allowed_ip: dict) -> dict: - """Formats the allowed IP data from the backend.""" - - return { - "ip_address": allowed_ip["_id"], - "name": allowed_ip["_id"], - "creation": today(), - "modified": today(), - } - - -def get_total_cache_key(text: str | None = None) -> str: - """Returns a cache key for total allowed IP count.""" - - text = text or "" - return f"allowed-ip:{text}:total" diff --git a/mail/server/doctype/allowed_ip/test_allowed_ip.py b/mail/server/doctype/allowed_ip/test_allowed_ip.py deleted file mode 100644 index 6712bb2f9..000000000 --- a/mail/server/doctype/allowed_ip/test_allowed_ip.py +++ /dev/null @@ -1,29 +0,0 @@ -# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and Contributors -# See license.txt - -# import frappe -from frappe.tests import IntegrationTestCase, UnitTestCase - -# On IntegrationTestCase, the doctype test records and all -# link-field test record dependencies are recursively loaded -# Use these module variables to add/remove to/from that list -EXTRA_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"] -IGNORE_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"] - - -class UnitTestAllowedIP(UnitTestCase): - """ - Unit tests for AllowedIP. - Use this class for testing individual functions and methods. - """ - - pass - - -class IntegrationTestAllowedIP(IntegrationTestCase): - """ - Integration tests for AllowedIP. - Use this class for testing interactions between multiple components. - """ - - pass diff --git a/mail/server/doctype/blocked_ip/__init__.py b/mail/server/doctype/blocked_ip/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/mail/server/doctype/blocked_ip/blocked_ip.js b/mail/server/doctype/blocked_ip/blocked_ip.js deleted file mode 100644 index bebbe546f..000000000 --- a/mail/server/doctype/blocked_ip/blocked_ip.js +++ /dev/null @@ -1,8 +0,0 @@ -// Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors -// For license information, please see license.txt - -// frappe.ui.form.on("Blocked IP", { -// refresh(frm) { - -// }, -// }); diff --git a/mail/server/doctype/blocked_ip/blocked_ip.json b/mail/server/doctype/blocked_ip/blocked_ip.json deleted file mode 100644 index 046a26a03..000000000 --- a/mail/server/doctype/blocked_ip/blocked_ip.json +++ /dev/null @@ -1,65 +0,0 @@ -{ - "actions": [], - "creation": "2025-04-06 15:03:32.401911", - "doctype": "DocType", - "engine": "InnoDB", - "field_order": [ - "section_break_nafl", - "ip_address", - "column_break_zda5", - "host" - ], - "fields": [ - { - "fieldname": "section_break_nafl", - "fieldtype": "Section Break" - }, - { - "fieldname": "ip_address", - "fieldtype": "Data", - "in_list_view": 1, - "in_standard_filter": 1, - "label": "IP Address", - "reqd": 1, - "set_only_once": 1 - }, - { - "fieldname": "column_break_zda5", - "fieldtype": "Column Break" - }, - { - "fieldname": "host", - "fieldtype": "Data", - "is_virtual": 1, - "label": "Host", - "read_only": 1 - } - ], - "grid_page_length": 50, - "index_web_pages_for_search": 1, - "is_virtual": 1, - "links": [], - "modified": "2026-04-23 12:04:38.190171", - "modified_by": "Administrator", - "module": "Server", - "name": "Blocked IP", - "owner": "Administrator", - "permissions": [ - { - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "print": 1, - "read": 1, - "report": 1, - "role": "System Manager", - "share": 1, - "write": 1 - } - ], - "row_format": "Dynamic", - "sort_field": "creation", - "sort_order": "DESC", - "states": [] -} diff --git a/mail/server/doctype/blocked_ip/blocked_ip.py b/mail/server/doctype/blocked_ip/blocked_ip.py deleted file mode 100644 index c2675f773..000000000 --- a/mail/server/doctype/blocked_ip/blocked_ip.py +++ /dev/null @@ -1,149 +0,0 @@ -# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors -# For license information, please see license.txt - -import json - -import frappe -from frappe import _ -from frappe.model.document import Document -from frappe.utils import today - -from mail.backend import get_mail_backend_api -from mail.utils import extract_filter_values -from mail.utils.dns import get_host_by_ip - - -class BlockedIP(Document): - @property - def host(self) -> str | None: - """Returns the host name of the IP address.""" - - if self.ip_address: - return get_host_by_ip(self.ip_address) - - def autoname(self) -> None: - self.name = self.ip_address - - def db_insert(self, *args, **kwargs) -> None: - self._create() - - def load_from_db(self) -> "BlockedIP": - blocked_ip = self._get() - return super(Document, self).__init__(blocked_ip) - - def db_update(self) -> None: - self._update() - - def delete(self) -> None: - self._delete() - if not frappe.flags.in_bulk_delete: - frappe.msgprint(_("Blocked IP removed successfully."), alert=True) - - @staticmethod - def get_list(filters=None, page_length=20, **kwargs) -> list: - filters = filters or [] - ip_address = extract_filter_values(filters, [{"ip_address": "like"}])[0] - - blocked_ips = BlockedIP._get_all(limit=page_length, text=ip_address) - if not blocked_ips: - frappe.msgprint(_("No blocked IPs found."), alert=True) - - return blocked_ips - - @staticmethod - def get_count(filters=None, **kwargs) -> int: - filters = filters or [] - ip_address = extract_filter_values(filters, [{"ip_address": "like"}])[0] - - return frappe.cache.get_value(get_total_cache_key(ip_address)) if ip_address else 0 - - @staticmethod - def get_stats(**kwargs) -> dict: - return {} - - def _create(self) -> None: - """Creates the blocked IP in the backend.""" - - ip_addresses = [self.ip_address] - request_data = [] - for ip in ip_addresses: - request_data.append( - { - "type": "insert", - "prefix": None, - "values": [[f"server.blocked-ip.{ip}", ""]], - "assert_empty": True, - } - ) - - backend_api = get_mail_backend_api() - backend_api.request( - method="POST", - endpoint="/api/settings", - data=json.dumps(request_data), - ) - - def _get(self) -> None: - """Returns the blocked IP from the backend.""" - - ip_address = self.name - backend_api = get_mail_backend_api() - response = backend_api.request( - method="GET", - endpoint="api/settings/group", - params={"prefix": "server.blocked-ip", "limit": 1, "filter": ip_address}, - ) - - blocked_ip = response.json()["data"]["items"][0] - return BlockedIP._format(blocked_ip) - - @staticmethod - def _get_all(page: int = 1, limit: int = 10, text: str | None = None) -> list: - """Returns all blocked IPs.""" - backend_api = get_mail_backend_api() - response = backend_api.request( - method="GET", - endpoint="api/settings/group", - params={"page": page, "prefix": "server.blocked-ip", "limit": limit, "filter": text}, - ) - - data = response.json()["data"] - frappe.cache.set_value(get_total_cache_key(text), data["total"], expires_in_sec=600) - - return [BlockedIP._format(item) for item in data["items"]] - - def _update(self) -> None: - raise NotImplementedError - - def _delete(self) -> None: - """Deletes the blocked IP from the backend.""" - - ip_addresses = [self.ip_address] - request_data = [] - for ip in ip_addresses: - request_data.append({"type": "delete", "keys": [f"server.blocked-ip.{ip}"]}) - - backend_api = get_mail_backend_api() - backend_api.request( - method="POST", - endpoint="/api/settings", - data=json.dumps(request_data), - ) - - @staticmethod - def _format(blocked_ip: dict) -> dict: - """Formats the blocked IP data from the backend.""" - - return { - "ip_address": blocked_ip["_id"], - "name": blocked_ip["_id"], - "creation": today(), - "modified": today(), - } - - -def get_total_cache_key(text: str | None = None) -> str: - """Returns a cache key for total blocked IP count.""" - - text = text or "" - return f"blocked-ip:{text}:total" diff --git a/mail/server/doctype/blocked_ip/test_blocked_ip.py b/mail/server/doctype/blocked_ip/test_blocked_ip.py deleted file mode 100644 index 32f5d41c9..000000000 --- a/mail/server/doctype/blocked_ip/test_blocked_ip.py +++ /dev/null @@ -1,29 +0,0 @@ -# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and Contributors -# See license.txt - -# import frappe -from frappe.tests import IntegrationTestCase, UnitTestCase - -# On IntegrationTestCase, the doctype test records and all -# link-field test record dependencies are recursively loaded -# Use these module variables to add/remove to/from that list -EXTRA_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"] -IGNORE_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"] - - -class UnitTestBlockedIP(UnitTestCase): - """ - Unit tests for BlockedIP. - Use this class for testing individual functions and methods. - """ - - pass - - -class IntegrationTestBlockedIP(IntegrationTestCase): - """ - Integration tests for BlockedIP. - Use this class for testing interactions between multiple components. - """ - - pass From cd801747e2ba51c9b4fabd29bec277506934fa68 Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Thu, 7 May 2026 00:01:10 +0530 Subject: [PATCH 09/55] refactor: remove `DMARC Report` --- mail/server/doctype/dmarc_report/__init__.py | 0 .../doctype/dmarc_report/dmarc_report.js | 8 - .../doctype/dmarc_report/dmarc_report.json | 188 ------------------ .../doctype/dmarc_report/dmarc_report.py | 183 ----------------- .../doctype/dmarc_report/test_dmarc_report.py | 29 --- .../doctype/dmarc_report_detail/__init__.py | 0 .../dmarc_report_detail.json | 129 ------------ .../dmarc_report_detail.py | 31 --- .../report/dmarc_report_viewer/__init__.py | 0 .../dmarc_report_viewer.js | 21 -- .../dmarc_report_viewer.json | 28 --- .../dmarc_report_viewer.py | 94 --------- 12 files changed, 711 deletions(-) delete mode 100644 mail/server/doctype/dmarc_report/__init__.py delete mode 100644 mail/server/doctype/dmarc_report/dmarc_report.js delete mode 100644 mail/server/doctype/dmarc_report/dmarc_report.json delete mode 100644 mail/server/doctype/dmarc_report/dmarc_report.py delete mode 100644 mail/server/doctype/dmarc_report/test_dmarc_report.py delete mode 100644 mail/server/doctype/dmarc_report_detail/__init__.py delete mode 100644 mail/server/doctype/dmarc_report_detail/dmarc_report_detail.json delete mode 100644 mail/server/doctype/dmarc_report_detail/dmarc_report_detail.py delete mode 100644 mail/server/report/dmarc_report_viewer/__init__.py delete mode 100644 mail/server/report/dmarc_report_viewer/dmarc_report_viewer.js delete mode 100644 mail/server/report/dmarc_report_viewer/dmarc_report_viewer.json delete mode 100644 mail/server/report/dmarc_report_viewer/dmarc_report_viewer.py diff --git a/mail/server/doctype/dmarc_report/__init__.py b/mail/server/doctype/dmarc_report/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/mail/server/doctype/dmarc_report/dmarc_report.js b/mail/server/doctype/dmarc_report/dmarc_report.js deleted file mode 100644 index 60094f371..000000000 --- a/mail/server/doctype/dmarc_report/dmarc_report.js +++ /dev/null @@ -1,8 +0,0 @@ -// Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors -// For license information, please see license.txt - -// frappe.ui.form.on("DMARC Report", { -// refresh(frm) { - -// }, -// }); diff --git a/mail/server/doctype/dmarc_report/dmarc_report.json b/mail/server/doctype/dmarc_report/dmarc_report.json deleted file mode 100644 index a3842c1e1..000000000 --- a/mail/server/doctype/dmarc_report/dmarc_report.json +++ /dev/null @@ -1,188 +0,0 @@ -{ - "actions": [], - "creation": "2025-06-12 14:43:35.996965", - "doctype": "DocType", - "engine": "InnoDB", - "field_order": [ - "section_break_xmmn", - "id", - "domain_name", - "column_break_bgdm", - "report_begin", - "report_end", - "report_details_section", - "report_id", - "organization_name", - "email", - "received_from", - "extra_contact_info", - "column_break_bgdr", - "subject", - "published_policy_section", - "dkim_alignment", - "spf_alignment", - "column_break_bfle", - "domain_policy", - "subdomain_policy", - "records_section", - "records" - ], - "fields": [ - { - "fieldname": "section_break_xmmn", - "fieldtype": "Section Break" - }, - { - "fieldname": "subject", - "fieldtype": "Small Text", - "label": "Subject", - "read_only": 1 - }, - { - "fieldname": "report_id", - "fieldtype": "Data", - "label": "Report ID", - "read_only": 1 - }, - { - "fieldname": "domain_name", - "fieldtype": "Data", - "in_list_view": 1, - "label": "Domain Name", - "read_only": 1 - }, - { - "fieldname": "extra_contact_info", - "fieldtype": "Small Text", - "label": "Extra Contact Info", - "read_only": 1 - }, - { - "fieldname": "email", - "fieldtype": "Data", - "label": "E-mail", - "read_only": 1 - }, - { - "fieldname": "organization_name", - "fieldtype": "Data", - "in_list_view": 1, - "label": "Organization Name", - "read_only": 1 - }, - { - "fieldname": "report_end", - "fieldtype": "Datetime", - "in_list_view": 1, - "label": "Report End", - "read_only": 1 - }, - { - "fieldname": "received_from", - "fieldtype": "Data", - "label": "Received From", - "read_only": 1 - }, - { - "fieldname": "column_break_bgdm", - "fieldtype": "Column Break" - }, - { - "fieldname": "dkim_alignment", - "fieldtype": "Data", - "label": "DKIM Alignment", - "read_only": 1 - }, - { - "fieldname": "spf_alignment", - "fieldtype": "Data", - "label": "SPF Alignment", - "read_only": 1 - }, - { - "fieldname": "domain_policy", - "fieldtype": "Data", - "label": "Domain Policy", - "read_only": 1 - }, - { - "fieldname": "subdomain_policy", - "fieldtype": "Data", - "label": "Subdomain Policy", - "read_only": 1 - }, - { - "fieldname": "records", - "fieldtype": "Table", - "label": "Records", - "options": "DMARC Report Detail", - "read_only": 1 - }, - { - "fieldname": "report_begin", - "fieldtype": "Datetime", - "in_list_view": 1, - "label": "Report Begin", - "read_only": 1 - }, - { - "fieldname": "published_policy_section", - "fieldtype": "Section Break", - "label": "Published Policy" - }, - { - "collapsible": 1, - "fieldname": "records_section", - "fieldtype": "Section Break", - "label": "Records" - }, - { - "fieldname": "report_details_section", - "fieldtype": "Section Break", - "label": "Report Details" - }, - { - "fieldname": "column_break_bgdr", - "fieldtype": "Column Break" - }, - { - "fieldname": "column_break_bfle", - "fieldtype": "Column Break" - }, - { - "fieldname": "id", - "fieldtype": "Data", - "label": "ID", - "read_only": 1 - } - ], - "grid_page_length": 50, - "in_create": 1, - "index_web_pages_for_search": 1, - "is_virtual": 1, - "links": [], - "modified": "2026-04-23 12:08:19.068219", - "modified_by": "Administrator", - "module": "Server", - "name": "DMARC Report", - "owner": "Administrator", - "permissions": [ - { - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "print": 1, - "read": 1, - "report": 1, - "role": "System Manager", - "share": 1, - "write": 1 - } - ], - "row_format": "Dynamic", - "sort_field": "creation", - "sort_order": "DESC", - "states": [], - "title_field": "id" -} diff --git a/mail/server/doctype/dmarc_report/dmarc_report.py b/mail/server/doctype/dmarc_report/dmarc_report.py deleted file mode 100644 index b75416b5a..000000000 --- a/mail/server/doctype/dmarc_report/dmarc_report.py +++ /dev/null @@ -1,183 +0,0 @@ -# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors -# For license information, please see license.txt - - -import json -from datetime import UTC, datetime, timezone - -import frappe -from frappe import _ -from frappe.model.document import Document -from frappe.utils import cint, convert_utc_to_system_timezone, get_datetime_str, now - -from mail.backend import get_mail_backend_api -from mail.utils import extract_filter_values - - -class DMARCReport(Document): - def autoname(self) -> None: - self.name = self.id - - def db_insert(self, *args, **kwargs) -> None: - self._create() - - def load_from_db(self) -> "DMARCReport": - report = self._get() - return super(Document, self).__init__(report) - - def db_update(self) -> None: - self._update() - - def delete(self) -> None: - self._delete(self) - if not frappe.flags.in_bulk_delete: - frappe.msgprint(_("DMARC report removed successfully."), alert=True) - - @staticmethod - def get_list(filters=None, page_length=20, **kwargs) -> list: - filters = filters or [] - text = None - - if isinstance(filters, dict): - text = filters.get("text") - elif isinstance(filters, list): - text = extract_filter_values(filters, [{"text": "like"}])[0] - - reports = DMARCReport._get_all(limit=page_length, text=text) - if not reports: - frappe.msgprint(_("No DMARC reports found."), alert=True) - - return reports - - @staticmethod - def get_count(filters=None, **kwargs) -> int: - filters = filters or [] - text = extract_filter_values(filters, [{"text": "like"}])[0] - - return frappe.cache.get_value(get_total_cache_key(text)) if text else 0 - - @staticmethod - def get_stats(**kwargs) -> dict: - return {} - - def _create(self) -> None: - raise NotImplementedError - - def _get(self) -> None: - """Returns DMARC report details from cache or backend.""" - - if report := frappe.cache.hget("dmarc_reports", self.name): - return report - - backend_api = get_mail_backend_api() - response = backend_api.request(method="GET", endpoint=f"/api/reports/dmarc/{self.name}") - - report = response.json()["data"] - report["id"] = self.name - report = DMARCReport._format(report) - frappe.cache.hset("dmarc_reports", self.name, report) - - return report - - @staticmethod - def _get_all(page: int = 1, limit: int = 10, text: str | None = None) -> list: - """Returns list of DMARC reports from backend.""" - - backend_api = get_mail_backend_api() - response = backend_api.request( - method="GET", - endpoint="api/reports/dmarc", - params={"page": page, "limit": limit, "filter": text}, - ) - - data = response.json()["data"] - frappe.cache.set_value(get_total_cache_key(text), data["total"], expires_in_sec=600) - - reports = [] - for idx in range(min(len(data["items"]), limit)): - report_id = data["items"][idx] - if report := frappe.cache.hget("dmarc_reports", report_id): - reports.append(report) - continue - - response = backend_api.request(method="GET", endpoint=f"/api/reports/dmarc/{report_id}") - report = response.json()["data"] - report["id"] = report_id - report = DMARCReport._format(report) - frappe.cache.hset("dmarc_reports", report_id, report) - reports.append(report) - - return reports - - def _update(self) -> None: - raise NotImplementedError - - def _delete(self) -> None: - """Deletes DMARC report from backend and cache.""" - - backend_api = get_mail_backend_api() - backend_api.request(method="DELETE", endpoint=f"/api/reports/dmarc/{self.name}") - - @staticmethod - def _format(report: dict) -> dict: - """Formats DMARC report data.""" - - report_begin = get_datetime_str( - convert_utc_to_system_timezone( - datetime.fromtimestamp( - int(report["report"]["report_metadata"]["date_range"]["begin"]), tz=UTC - ) - ) - ) - report_end = get_datetime_str( - convert_utc_to_system_timezone( - datetime.fromtimestamp(int(report["report"]["report_metadata"]["date_range"]["end"]), tz=UTC) - ) - ) - - formatted_report = { - "id": report["id"], - "name": report["id"], - "report_id": report["report"]["report_metadata"]["report_id"], - "organization_name": report["report"]["report_metadata"]["org_name"], - "email": report["report"]["report_metadata"]["email"], - "extra_contact_info": report["report"]["report_metadata"]["extra_contact_info"], - "received_from": report["from"], - "report_begin": report_begin, - "report_end": report_end, - "subject": report["subject"], - "domain_name": report["report"]["policy_published"]["domain"], - "dkim_alignment": report["report"]["policy_published"]["adkim"], - "spf_alignment": report["report"]["policy_published"]["aspf"], - "domain_policy": report["report"]["policy_published"]["p"], - "subdomain_policy": report["report"]["policy_published"]["sp"], - "records": [], - "creation": now(), - "modified": now(), - } - - for record in report["report"]["record"]: - formatted_report["records"].append( - { - "source_ip": record["row"]["source_ip"], - "count": cint(record["row"]["count"]), - "disposition": record["row"]["policy_evaluated"]["disposition"], - "dkim_result": record["row"]["policy_evaluated"]["dkim"], - "spf_result": record["row"]["policy_evaluated"]["spf"], - "reason": json.dumps(record["row"]["policy_evaluated"]["reason"], indent=4), - "envelope_to": record["identifiers"]["envelope_to"], - "envelope_from": record["identifiers"]["envelope_from"], - "header_from": record["identifiers"]["header_from"], - "dkim_results": json.dumps(record["auth_results"]["dkim"], indent=4), - "spf_results": json.dumps(record["auth_results"]["spf"], indent=4), - } - ) - - return formatted_report - - -def get_total_cache_key(text: str | None = None) -> str: - """Returns a cache key for total reports count.""" - - text = text or "" - return f"dmarc_reports:{text}:total" diff --git a/mail/server/doctype/dmarc_report/test_dmarc_report.py b/mail/server/doctype/dmarc_report/test_dmarc_report.py deleted file mode 100644 index 76c3fb8cd..000000000 --- a/mail/server/doctype/dmarc_report/test_dmarc_report.py +++ /dev/null @@ -1,29 +0,0 @@ -# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and Contributors -# See license.txt - -# import frappe -from frappe.tests import IntegrationTestCase, UnitTestCase - -# On IntegrationTestCase, the doctype test records and all -# link-field test record dependencies are recursively loaded -# Use these module variables to add/remove to/from that list -EXTRA_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"] -IGNORE_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"] - - -class UnitTestDMARCReport(UnitTestCase): - """ - Unit tests for DMARCReport. - Use this class for testing individual functions and methods. - """ - - pass - - -class IntegrationTestDMARCReport(IntegrationTestCase): - """ - Integration tests for DMARCReport. - Use this class for testing interactions between multiple components. - """ - - pass diff --git a/mail/server/doctype/dmarc_report_detail/__init__.py b/mail/server/doctype/dmarc_report_detail/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/mail/server/doctype/dmarc_report_detail/dmarc_report_detail.json b/mail/server/doctype/dmarc_report_detail/dmarc_report_detail.json deleted file mode 100644 index ea4c7101c..000000000 --- a/mail/server/doctype/dmarc_report_detail/dmarc_report_detail.json +++ /dev/null @@ -1,129 +0,0 @@ -{ - "actions": [], - "creation": "2025-06-12 19:41:37.374066", - "doctype": "DocType", - "editable_grid": 1, - "engine": "InnoDB", - "field_order": [ - "section_break_syrx", - "header_from", - "envelope_from", - "envelope_to", - "source_ip", - "count", - "column_break_gk3f", - "disposition", - "dkim_result", - "spf_result", - "auth_results_section", - "reason", - "dkim_results", - "spf_results" - ], - "fields": [ - { - "fieldname": "section_break_syrx", - "fieldtype": "Section Break" - }, - { - "fieldname": "source_ip", - "fieldtype": "Data", - "in_list_view": 1, - "label": "Source IP", - "read_only": 1 - }, - { - "fieldname": "disposition", - "fieldtype": "Data", - "in_list_view": 1, - "label": "Disposition", - "read_only": 1 - }, - { - "depends_on": "eval: doc.envelope_from", - "fieldname": "envelope_from", - "fieldtype": "Data", - "in_list_view": 1, - "label": "From", - "read_only": 1 - }, - { - "depends_on": "eval: doc.envelope_to", - "fieldname": "envelope_to", - "fieldtype": "Data", - "in_list_view": 1, - "label": "To", - "read_only": 1 - }, - { - "depends_on": "eval: doc.header_from", - "fieldname": "header_from", - "fieldtype": "Data", - "in_list_view": 1, - "label": "Header From", - "read_only": 1 - }, - { - "fieldname": "count", - "fieldtype": "Int", - "in_list_view": 1, - "label": "Count", - "read_only": 1 - }, - { - "fieldname": "reason", - "fieldtype": "JSON", - "label": "Reason", - "read_only": 1 - }, - { - "fieldname": "dkim_results", - "fieldtype": "JSON", - "label": "DKIM Results", - "read_only": 1 - }, - { - "fieldname": "spf_results", - "fieldtype": "JSON", - "label": "SPF Results", - "read_only": 1 - }, - { - "collapsible": 1, - "fieldname": "auth_results_section", - "fieldtype": "Section Break", - "label": "Auth Results" - }, - { - "fieldname": "column_break_gk3f", - "fieldtype": "Column Break" - }, - { - "fieldname": "dkim_result", - "fieldtype": "Data", - "label": "DKIM Result", - "read_only": 1 - }, - { - "fieldname": "spf_result", - "fieldtype": "Data", - "label": "SPF Result", - "read_only": 1 - } - ], - "grid_page_length": 50, - "index_web_pages_for_search": 1, - "is_virtual": 1, - "istable": 1, - "links": [], - "modified": "2025-06-13 16:50:45.025031", - "modified_by": "Administrator", - "module": "Server", - "name": "DMARC Report Detail", - "owner": "Administrator", - "permissions": [], - "row_format": "Dynamic", - "sort_field": "creation", - "sort_order": "DESC", - "states": [] -} diff --git a/mail/server/doctype/dmarc_report_detail/dmarc_report_detail.py b/mail/server/doctype/dmarc_report_detail/dmarc_report_detail.py deleted file mode 100644 index e0edd8e03..000000000 --- a/mail/server/doctype/dmarc_report_detail/dmarc_report_detail.py +++ /dev/null @@ -1,31 +0,0 @@ -# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors -# For license information, please see license.txt - -# import frappe -from frappe.model.document import Document - - -class DMARCReportDetail(Document): - def db_insert(self, *args, **kwargs): - raise NotImplementedError - - def load_from_db(self): - raise NotImplementedError - - def db_update(self): - raise NotImplementedError - - def delete(self): - raise NotImplementedError - - @staticmethod - def get_list(filters=None, page_length=20, **kwargs): - pass - - @staticmethod - def get_count(filters=None, **kwargs): - pass - - @staticmethod - def get_stats(**kwargs): - pass diff --git a/mail/server/report/dmarc_report_viewer/__init__.py b/mail/server/report/dmarc_report_viewer/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/mail/server/report/dmarc_report_viewer/dmarc_report_viewer.js b/mail/server/report/dmarc_report_viewer/dmarc_report_viewer.js deleted file mode 100644 index 52956f9e4..000000000 --- a/mail/server/report/dmarc_report_viewer/dmarc_report_viewer.js +++ /dev/null @@ -1,21 +0,0 @@ -// Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors -// For license information, please see license.txt - -frappe.query_reports['DMARC Report Viewer'] = { - formatter(value, row, column, data, default_formatter) { - value = default_formatter(value, row, column, data) - - if (['spf_result', 'dkim_result', 'result'].includes(column.fieldname)) { - value = - data[column.fieldname] === 'PASS' - ? `${value}` - : `${value}` - } else if (column.fieldname === 'source_ip' && data[column.fieldname]) { - value = data['is_local_ip'] - ? `${value}` - : `${value}` - } - - return value - }, -} diff --git a/mail/server/report/dmarc_report_viewer/dmarc_report_viewer.json b/mail/server/report/dmarc_report_viewer/dmarc_report_viewer.json deleted file mode 100644 index 4be0ea272..000000000 --- a/mail/server/report/dmarc_report_viewer/dmarc_report_viewer.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "add_total_row": 0, - "add_translate_data": 0, - "columns": [], - "creation": "2024-11-21 17:31:42.807210", - "disabled": 0, - "docstatus": 0, - "doctype": "Report", - "filters": [], - "idx": 0, - "is_standard": "Yes", - "letterhead": null, - "modified": "2025-06-13 17:07:13.913224", - "modified_by": "Administrator", - "module": "Server", - "name": "DMARC Report Viewer", - "owner": "Administrator", - "prepared_report": 0, - "ref_doctype": "DMARC Report", - "report_name": "DMARC Report Viewer", - "report_type": "Script Report", - "roles": [ - { - "role": "System Manager" - } - ], - "timeout": 0 -} diff --git a/mail/server/report/dmarc_report_viewer/dmarc_report_viewer.py b/mail/server/report/dmarc_report_viewer/dmarc_report_viewer.py deleted file mode 100644 index c9d07b00b..000000000 --- a/mail/server/report/dmarc_report_viewer/dmarc_report_viewer.py +++ /dev/null @@ -1,94 +0,0 @@ -# Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors -# For license information, please see license.txt - -import json - -import frappe -from frappe import _ - -from mail.server.doctype.dmarc_report.dmarc_report import DMARCReport - - -def execute(filters: dict | None = None) -> tuple: - columns = get_columns() - data = get_data(filters) - - return columns, data - - -def get_columns() -> list[dict]: - return [ - { - "label": _("Name"), - "fieldname": "name", - "fieldtype": "Link", - "options": "DMARC Report", - "width": 200, - }, - {"label": _("Report Begin"), "fieldname": "report_begin", "fieldtype": "Datetime", "width": 180}, - {"label": _("report_end"), "fieldname": "report_end", "fieldtype": "Datetime", "width": 180}, - { - "label": _("Domain Name"), - "fieldname": "domain_name", - "fieldtype": "Data", - "width": 150, - }, - {"label": _("Organization"), "fieldname": "organization_name", "fieldtype": "Data", "width": 150}, - {"label": _("Report ID"), "fieldname": "report_id", "fieldtype": "Data", "width": 150}, - {"label": _("Source IP"), "fieldname": "source_ip", "fieldtype": "Data", "width": 150}, - {"label": _("Count"), "fieldname": "count", "fieldtype": "Int", "width": 70}, - {"label": _("Disposition"), "fieldname": "disposition", "fieldtype": "Data", "width": 150}, - {"label": _("Header From"), "fieldname": "header_from", "fieldtype": "Data", "width": 150}, - {"label": _("Envelope From"), "fieldname": "envelope_from", "fieldtype": "Data", "width": 150}, - {"label": _("Envelope To"), "fieldname": "envelope_to", "fieldtype": "Data", "width": 150}, - {"label": _("DKIM Result"), "fieldname": "dkim_result", "fieldtype": "Data", "width": 150}, - {"label": _("SPF Result"), "fieldname": "spf_result", "fieldtype": "Data", "width": 150}, - {"label": _("Auth Type"), "fieldname": "auth_type", "fieldtype": "Data", "width": 150}, - {"label": _("Selector"), "fieldname": "selector", "fieldtype": "Data", "width": 150}, - {"label": _("Scope"), "fieldname": "scope", "fieldtype": "Data", "width": 150}, - {"label": _("Domain"), "fieldname": "domain", "fieldtype": "Data", "width": 150}, - {"label": _("Result"), "fieldname": "result", "fieldtype": "Data", "width": 150}, - ] - - -def get_data(filters: dict | None = None) -> list[list]: - filters = filters or {} - local_ips = get_local_ip_addresses() - dmarc_reports = DMARCReport.get_list(filters) - - data = [] - for dmarc_report in dmarc_reports: - records = dmarc_report.pop("records", []) - - if not records: - continue - - dmarc_report["indent"] = 0 - data.append(dmarc_report) - - for record in records: - record["indent"] = 1 - record["is_local_ip"] = record["source_ip"] in local_ips - record["dkim_result"] = record["dkim_result"].upper() - record["spf_result"] = record["spf_result"].upper() - data.append(record) - - for field in ["dkim_results", "spf_results"]: - for result in json.loads(record[field]): - result["indent"] = 2 - result["auth_type"] = field.replace("_results", "").upper() - result["result"] = result["result"].upper() - data.append(result) - - return data - - -def get_local_ip_addresses() -> list[str]: - """Returns list of local IPs (Mail Servers IPs).""" - - ip_addresses = [] - for addresses in frappe.db.get_all("Mail Server", {}, ["ipv4_addresses", "ipv6_addresses"]): - for field in ["ipv4_addresses", "ipv6_addresses"]: - ip_addresses.extend(addresses[field].split("\n") if addresses[field] else []) - - return ip_addresses From f871bd7bbf0475639b360d6d75fa2c3443d8052c Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Tue, 5 May 2026 23:19:04 +0530 Subject: [PATCH 10/55] refactor: server deployment --- .../doctype/mail_cluster/mail_cluster.js | 12 +- .../doctype/mail_cluster/mail_cluster.json | 55 ++++---- .../doctype/mail_cluster/mail_cluster.py | 40 +++--- .../server/doctype/mail_server/mail_server.js | 9 ++ .../doctype/mail_server/mail_server.json | 28 +++- .../server/doctype/mail_server/mail_server.py | 103 +++++++++++--- .../server_deployment/server_deployment.json | 27 +--- .../server_deployment/server_deployment.py | 85 ++---------- .../server_deployment_service.json | 6 +- .../ansible/playbooks/deploy-mail-server.yml | 127 +++++++++++++----- .../ansible/playbooks/install-docker.yml | 19 ++- mail/utils/fc/filebeat_stream_setup.sh | 3 +- mail/utils/fc/post_deploy_ssl_setup.sh | 2 +- 13 files changed, 298 insertions(+), 218 deletions(-) diff --git a/mail/server/doctype/mail_cluster/mail_cluster.js b/mail/server/doctype/mail_cluster/mail_cluster.js index bd2bb5fe9..b9c14bcaa 100644 --- a/mail/server/doctype/mail_cluster/mail_cluster.js +++ b/mail/server/doctype/mail_cluster/mail_cluster.js @@ -3,14 +3,14 @@ const STORE_PRESET = { RocksDb: { - store_path: '/var/lib/stalwart/rocksdb', + store_path: '/etc/stalwart/data', store_blob_size: 16834, store_buffer_size: 134217728, - store_pool_workers: 0, + store_pool_workers: 1, }, Sqlite: { - store_path: '/var/lib/stalwart/sqlite', - store_pool_workers: 0, + store_path: '/etc/stalwart/data', + store_pool_workers: 1, store_pool_max_connections: 10, }, FoundationDb: { @@ -86,7 +86,7 @@ frappe.ui.form.on('Mail Cluster', { if (!frappe.user_roles.includes('System Manager')) return - if (frm.doc.fallback_admin_password) { + if (frm.doc.recovery_admin_password) { frm.add_custom_button(__('Show Password'), () => { frm.trigger('show_password') }) @@ -106,7 +106,7 @@ frappe.ui.form.on('Mail Cluster', { show_password(frm) { frappe.call({ doc: frm.doc, - method: 'get_fallback_admin_password', + method: 'get_recovery_admin_password', freeze: true, freeze_message: __('Getting Password...'), callback: (r) => { diff --git a/mail/server/doctype/mail_cluster/mail_cluster.json b/mail/server/doctype/mail_cluster/mail_cluster.json index 245de2bfc..5c42181fb 100644 --- a/mail/server/doctype/mail_cluster/mail_cluster.json +++ b/mail/server/doctype/mail_cluster/mail_cluster.json @@ -9,10 +9,10 @@ "enabled", "column_break_fkha", "hostname", + "default_domain", "authentication_section", - "fallback_admin_user", - "fallback_admin_password", - "fallback_admin_secret", + "recovery_admin_user", + "recovery_admin_password", "column_break_9jti", "base_url", "api_key", @@ -133,29 +133,6 @@ "fieldname": "section_break_b7jt", "fieldtype": "Section Break" }, - { - "default": "frappe", - "description": "Username for administrative access to the cluster.", - "fieldname": "fallback_admin_user", - "fieldtype": "Data", - "label": "Username", - "reqd": 1 - }, - { - "description": "Password for administrative access to the cluster.", - "fieldname": "fallback_admin_password", - "fieldtype": "Password", - "label": "Password", - "no_copy": 1 - }, - { - "fieldname": "fallback_admin_secret", - "fieldtype": "Small Text", - "hidden": 1, - "label": "Password Secret", - "no_copy": 1, - "read_only": 1 - }, { "description": "FQDN of the proxy or load balancer forwarding requests to mail servers.", "fieldname": "hostname", @@ -439,6 +416,30 @@ "fieldname": "store_tab", "fieldtype": "Tab Break", "label": "Store" + }, + { + "default": "frappe", + "description": "Username for administrative access to the cluster.", + "fieldname": "recovery_admin_user", + "fieldtype": "Data", + "label": "Username", + "reqd": 1 + }, + { + "description": "Password for administrative access to the cluster.", + "fieldname": "recovery_admin_password", + "fieldtype": "Password", + "label": "Password", + "no_copy": 1 + }, + { + "fieldname": "default_domain", + "fieldtype": "Data", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Default Domain", + "search_index": 1, + "set_only_once": 1 } ], "grid_page_length": 50, @@ -450,7 +451,7 @@ "link_fieldname": "cluster" } ], - "modified": "2026-05-05 15:23:51.010624", + "modified": "2026-05-07 14:53:24.555320", "modified_by": "Administrator", "module": "Server", "name": "Mail Cluster", diff --git a/mail/server/doctype/mail_cluster/mail_cluster.py b/mail/server/doctype/mail_cluster/mail_cluster.py index a14389097..d2812d3b4 100644 --- a/mail/server/doctype/mail_cluster/mail_cluster.py +++ b/mail/server/doctype/mail_cluster/mail_cluster.py @@ -13,7 +13,7 @@ from mail.backend import MailBackendAPI, Principal from mail.jmap.connection import raise_for_status -from mail.utils import generate_secret, hash_password +from mail.utils import generate_secret from mail.utils.dns import get_dns_record @@ -101,8 +101,8 @@ def autoname(self) -> None: def validate(self) -> None: self.validate_enabled() self.validate_hostname() - self.validate_fallback_admin_password() - self.generate_fallback_admin_secret() + self.validate_default_domain() + self.validate_recovery_admin_password() self.validate_base_url() def before_insert(self) -> None: @@ -132,20 +132,20 @@ def validate_hostname(self) -> None: self.ipv4_addresses = "\n".join([r.address for r in get_dns_record(self.hostname, "A") or []]) self.ipv6_addresses = "\n".join([r.address for r in get_dns_record(self.hostname, "AAAA") or []]) - def validate_fallback_admin_password(self) -> None: - """Validates the fallback admin password.""" + def validate_default_domain(self) -> None: + """Validates the default domain of the cluster.""" - if self.fallback_admin_password: - if len(self.fallback_admin_password) < 16: - frappe.throw(_("Password must be at least 16 characters long.")) - else: - self.fallback_admin_password = random_string(length=20) + if not self.default_domain: + self.default_domain = self.hostname - def generate_fallback_admin_secret(self) -> None: - """Generates the fallback admin secret.""" + def validate_recovery_admin_password(self) -> None: + """Validates the recovery admin password.""" - if self.has_value_changed("fallback_admin_password"): - self.fallback_admin_secret = hash_password(self.get_password("fallback_admin_password")) + if self.recovery_admin_password: + if len(self.recovery_admin_password) < 16: + frappe.throw(_("Password must be at least 16 characters long.")) + else: + self.recovery_admin_password = random_string(length=20) def validate_base_url(self) -> None: """Validates the base URL of the cluster.""" @@ -176,17 +176,17 @@ def initialize_store(self) -> None: if not self.store_type: self.store_type = "RocksDb" - self.store_path = "/var/lib/stalwart/rocksdb" + self.store_path = "/etc/stalwart/data/rocksdb" self.store_blob_size = 16834 self.store_buffer_size = 134217728 - self.store_pool_workers = 0 + self.store_pool_workers = 1 @frappe.whitelist() - def get_fallback_admin_password(self) -> str: + def get_recovery_admin_password(self) -> str: """Returns the admin password of the cluster.""" frappe.only_for("System Manager") - return self.get_password("fallback_admin_password") + return self.get_password("recovery_admin_password") @frappe.whitelist() def generate_api_key(self) -> None: @@ -209,8 +209,8 @@ def _generate_api_key(self) -> str: ) backend_api = MailBackendAPI( self.base_url, - username=self.fallback_admin_user, - password=self.get_password("fallback_admin_password"), + username=self.recovery_admin_user, + password=self.get_password("recovery_admin_password"), ) response = backend_api.request(method="POST", endpoint="/api/principal", json=principal.__dict__) raise_for_status(response) diff --git a/mail/server/doctype/mail_server/mail_server.js b/mail/server/doctype/mail_server/mail_server.js index f90286827..472afd64f 100644 --- a/mail/server/doctype/mail_server/mail_server.js +++ b/mail/server/doctype/mail_server/mail_server.js @@ -10,6 +10,15 @@ frappe.ui.form.on('Mail Server', { frm.trigger('add_actions') }, + regenerate_bootstrap_ndjson(frm) { + frappe.call({ + doc: frm.doc, + method: 'regenerate_bootstrap_ndjson', + freeze: true, + freeze_message: __('Regenerating bootstrap.ndjson...'), + }) + }, + set_queries(frm) { frm.set_query('cluster', () => ({ filters: { diff --git a/mail/server/doctype/mail_server/mail_server.json b/mail/server/doctype/mail_server/mail_server.json index 105935417..591b665aa 100644 --- a/mail/server/doctype/mail_server/mail_server.json +++ b/mail/server/doctype/mail_server/mail_server.json @@ -11,7 +11,7 @@ "column_break_0mtp", "cluster", "hostname", - "http_port", + "recovery_http_port", "network_settings_section", "ipv4_addresses", "column_break_3m4p", @@ -24,7 +24,10 @@ "ssh_user", "ssh_port", "ssh_key_section", - "ssh_public_key" + "ssh_public_key", + "bootstrap_tab", + "bootstrap_ndjson", + "regenerate_bootstrap_ndjson" ], "fields": [ { @@ -155,11 +158,26 @@ "no_copy": 1, "read_only": 1 }, + { + "fieldname": "bootstrap_tab", + "fieldtype": "Tab Break", + "label": "Bootstrap" + }, + { + "fieldname": "bootstrap_ndjson", + "fieldtype": "Code", + "label": "bootstrap.ndjson" + }, + { + "fieldname": "regenerate_bootstrap_ndjson", + "fieldtype": "Button", + "label": "Regenerate" + }, { "default": "8080", - "fieldname": "http_port", + "fieldname": "recovery_http_port", "fieldtype": "Int", - "label": "HTTP Port", + "label": "Recovery HTTP Port", "non_negative": 1, "reqd": 1 } @@ -183,7 +201,7 @@ "link_fieldname": "server" } ], - "modified": "2026-05-05 16:13:52.084056", + "modified": "2026-05-07 14:39:23.127998", "modified_by": "Administrator", "module": "Server", "name": "Mail Server", diff --git a/mail/server/doctype/mail_server/mail_server.py b/mail/server/doctype/mail_server/mail_server.py index b05128ce2..d5a4c4e59 100644 --- a/mail/server/doctype/mail_server/mail_server.py +++ b/mail/server/doctype/mail_server/mail_server.py @@ -6,12 +6,12 @@ import json import os import socket -from typing import TYPE_CHECKING import frappe import paramiko from frappe import _ from frappe.model.document import Document +from frappe.utils import cint from mail.utils.dns import get_dns_record @@ -30,6 +30,7 @@ def autoname(self) -> None: def validate(self) -> None: self.validate_hostname() self.validate_cluster() + self.set_bootstrap_ndjson() def on_trash(self) -> None: if frappe.session.user != "Administrator": @@ -50,6 +51,60 @@ def validate_cluster(self) -> None: if not frappe.db.get_value("Mail Cluster", self.cluster, "enabled"): frappe.throw(_("Mail Cluster {0} is disabled.").format(frappe.bold(self.cluster))) + def set_bootstrap_ndjson(self) -> None: + """Sets the bootstrap NDJSON for the server.""" + + if not self.bootstrap_ndjson: + self.bootstrap_ndjson = self._generate_bootstrap_ndjson() + + @frappe.whitelist() + def regenerate_bootstrap_ndjson(self) -> None: + """Regenerates the bootstrap NDJSON for the server.""" + + frappe.only_for("System Manager") + self.bootstrap_ndjson = self._generate_bootstrap_ndjson() + self._db_set(bootstrap_ndjson=self.bootstrap_ndjson, notify=True) + frappe.msgprint(_("Bootstrap NDJSON regenerated."), indicator="green", alert=True) + + def _generate_bootstrap_ndjson(self) -> str: + """Generates the bootstrap NDJSON for the server.""" + + cluster = frappe.get_doc("Mail Cluster", self.cluster) + + operations = [ + { + "@type": "update", + "object": "Bootstrap", + "id": "singleton", + "value": { + # required + "serverHostname": self.hostname, + "defaultDomain": cluster.default_domain, + # optional + "requestTlsCertificate": True, + "generateDkimKeys": True, + "dataStore": json.loads(cluster.config), + "blobStore": {"@type": "Default"}, + "searchStore": {"@type": "Default"}, + "inMemoryStore": {"@type": "Default"}, + "directory": {"@type": "Internal"}, + "tracer": { + "@type": "Log", + "ansi": True, + "enable": True, + "eventsPolicy": "exclude", + "level": "info", + "prefix": "stalwart", + "rotate": "daily", + "path": "/etc/stalwart/logs", + }, + "dnsServer": {"@type": "Manual"}, + }, + } + ] + + return "\n".join([json.dumps(op) for op in operations]) + @frappe.whitelist() def verify_ssh_connection(self) -> None: """Verifies the SSH connection to the server.""" @@ -151,27 +206,45 @@ def install_stalwart(self) -> None: self._install_stalwart() frappe.msgprint(_("Install of Stalwart initiated."), indicator="green", alert=True) - def _install_stalwart(self, config: str | None = None) -> None: + def _install_stalwart(self) -> None: """Installs Stalwart on the Mail Server.""" - config = config or frappe.db.get_value("Server Config", {"server": self.name}) - if not config: - frappe.throw(_("Please generate the Server Config before installing Stalwart.")) - - install_redis = 0 - cluster = frappe.get_doc("Mail Cluster", self.cluster) - for store in cluster.stores: - if store.type == "Redis/Memcached" and "redis://redis:6379" in store.urls: - install_redis = 1 - break - deployment = frappe.new_doc("Server Deployment") deployment.status = "Pending" deployment.server = self.name - deployment.config = config - deployment.install_redis = install_redis + deployment.max_retries = 0 deployment.insert(ignore_permissions=True) + def _get_stalwart_env( + self, + recovery_mode: bool = False, + role: str | None = None, + push_shard: str | None = None, + as_dict: bool = False, + ) -> str | dict: + """Returns the environment variables for Stalwart.""" + + cluster = frappe.get_doc("Mail Cluster", self.cluster) + env = { + "STALWART_HOSTNAME": self.hostname, + "STALWART_RECOVERY_MODE": cint(recovery_mode), + "STALWART_RECOVERY_MODE_PORT": cint(self.recovery_http_port), + "STALWART_RECOVERY_MODE_LOG_LEVEL": "debug", + "STALWART_RECOVERY_ADMIN": f"{cluster.recovery_admin_user}:{cluster.get_password('recovery_admin_password')}", + } + + if cluster.base_url: + env["STALWART_PUBLIC_URL"] = cluster.base_url + if role: + env["STALWART_ROLE"] = role + if push_shard: + env["STALWART_PUSH_SHARD"] = push_shard + + if as_dict: + return env + + return "\n".join([f"{key}={value}" for key, value in env.items()]) + def _db_set( self, update_modified: bool = True, diff --git a/mail/server/doctype/server_deployment/server_deployment.json b/mail/server/doctype/server_deployment/server_deployment.json index b3603c5ad..3ce8ac049 100644 --- a/mail/server/doctype/server_deployment/server_deployment.json +++ b/mail/server/doctype/server_deployment/server_deployment.json @@ -18,10 +18,6 @@ "column_break_vkxq", "started_after", "duration", - "section_break_gicu", - "column_break_slvv", - "column_break_qjvy", - "install_redis", "section_break_mndp", "services", "error_log_section", @@ -109,14 +105,6 @@ "read_only": 1, "search_index": 1 }, - { - "fieldname": "section_break_gicu", - "fieldtype": "Section Break" - }, - { - "fieldname": "column_break_qjvy", - "fieldtype": "Column Break" - }, { "collapsible": 1, "collapsible_depends_on": "eval: false", @@ -163,15 +151,6 @@ "search_index": 1, "set_only_once": 1 }, - { - "default": "0", - "depends_on": "eval: doc.__islocal || doc.install_redis", - "fieldname": "install_redis", - "fieldtype": "Check", - "label": "Install Redis", - "no_copy": 1, - "set_only_once": 1 - }, { "fieldname": "section_break_mndp", "fieldtype": "Section Break" @@ -182,10 +161,6 @@ "label": "Services", "options": "Server Deployment Service", "set_only_once": 1 - }, - { - "fieldname": "column_break_slvv", - "fieldtype": "Column Break" } ], "grid_page_length": 50, @@ -197,7 +172,7 @@ "link_fieldname": "deployment" } ], - "modified": "2026-05-05 16:41:28.623245", + "modified": "2026-05-05 22:24:48.231315", "modified_by": "Administrator", "module": "Server", "name": "Server Deployment", diff --git a/mail/server/doctype/server_deployment/server_deployment.py b/mail/server/doctype/server_deployment/server_deployment.py index 38568af28..dc4988b8b 100644 --- a/mail/server/doctype/server_deployment/server_deployment.py +++ b/mail/server/doctype/server_deployment/server_deployment.py @@ -1,10 +1,8 @@ # Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors # For license information, please see license.txt -import hashlib import json import os -import urllib.parse from uuid import uuid7 import frappe @@ -17,13 +15,6 @@ class ServerDeployment(Document): - @property - def config_toml(self) -> str | None: - """Returns the config.toml content.""" - - if self.config: - return frappe.get_doc("Server Config", self.config).config - @property def docker_compose(self) -> str | None: """Returns the docker-compose.yml content.""" @@ -37,6 +28,10 @@ def docker_compose(self) -> str | None: docker_compose += f" restart: {service.restart}\n" docker_compose += f" network_mode: {service.network_mode}\n" + if service.service == "stalwart": + docker_compose += " env_file:\n" + docker_compose += " - ./stalwart.env\n" + depends_on = json.loads(service.depends_on) if service.depends_on else [] if depends_on: docker_compose += " depends_on:\n" @@ -57,42 +52,12 @@ def docker_compose(self) -> str | None: return docker_compose - @property - def http_port(self) -> int: - """Returns the HTTP port from the server or cluster listeners.""" - - server = frappe.get_doc("Mail Server", self.server) - cluster = frappe.get_doc("Mail Cluster", server.cluster) - for listener in server.listeners or cluster.listeners: - if listener.protocol == "HTTP" and not listener.tls_implicit: - parsed = urllib.parse.urlparse(f"//{listener.bind}") - if port := parsed.port: - return port - - return 8080 - - @property - def https_port(self) -> int: - """Returns the HTTPS port from the server or cluster listeners.""" - - server = frappe.get_doc("Mail Server", self.server) - cluster = frappe.get_doc("Mail Cluster", server.cluster) - for listener in server.listeners or cluster.listeners: - if listener.protocol == "HTTP" and listener.tls_implicit: - parsed = urllib.parse.urlparse(f"//{listener.bind}") - if port := parsed.port: - return port - - return 443 - def autoname(self) -> None: self.name = str(uuid7()) def validate(self) -> None: self.validate_status() self.validate_server() - self.validate_config() - self.validate_config_checksum() self.validate_services() def after_insert(self) -> None: @@ -122,22 +87,6 @@ def validate_server(self) -> None: elif not frappe.db.get_value("Mail Server", self.server, "ssh_verified"): frappe.throw(_("Please verify SSH connection for Mail Server {0}").format(self.server)) - def validate_config(self) -> None: - """Validates if the config belongs to the selected server.""" - - if not self.config: - frappe.throw(_("Config is required.")) - - config_server = frappe.db.get_value("Server Config", self.config, "server") - if config_server != self.server: - frappe.throw(_("Config does not belong to the selected server.")) - - def validate_config_checksum(self) -> None: - """Sets the config checksum if not set.""" - - if not self.config_checksum: - self.config_checksum = hashlib.sha256(self.config_toml.encode("utf-8")).hexdigest() - def validate_services(self) -> None: """Validates and prepares the services for deployment.""" @@ -148,22 +97,11 @@ def validate_services(self) -> None: "container": f"stalwart_{server_hostname}", "restart": "unless-stopped", "network_mode": "host", - "depends_on": json.dumps(["redis"] if self.install_redis else [], indent=4), - "ports": json.dumps([], indent=4), - "volumes": json.dumps(["{{ stalwart_root }}:/opt/stalwart"], indent=4), - } - } - - if self.install_redis: - services["redis"] = { - "image": "redis:7-alpine", - "container": f"redis_{server_hostname}", - "restart": "unless-stopped", - "network_mode": "host", "depends_on": json.dumps([], indent=4), "ports": json.dumps([], indent=4), - "volumes": json.dumps(["{{ stalwart_root }}/redis:/opt/stalwart/redis"], indent=4), + "volumes": json.dumps(["{{ stalwart_root }}:/etc/stalwart"], indent=4), } + } for service in self.services: services[service.service] = { @@ -199,13 +137,16 @@ def execute(self) -> None: try: self.validate_server() + server = frappe.get_doc("Mail Server", self.server) + cluster = frappe.get_doc("Mail Cluster", server.cluster) variables = { - "server_hostname": frappe.db.get_value("Mail Server", self.server, "hostname"), - "config_toml": self.config_toml, + "server_hostname": server.hostname, + "stalwart_env": server._get_stalwart_env(recovery_mode=False, as_dict=False), + "stalwart_bootstrap_plan": server.bootstrap_ndjson, + "recovery_admin_user": cluster.recovery_admin_user, + "recovery_admin_password": cluster.get_password("recovery_admin_password"), "docker_compose": self.docker_compose, - "install_redis": cint(self.install_redis), - "http_port": self.http_port, } play = frappe.new_doc("Server Ansible Play") play.status = "Pending" diff --git a/mail/server/doctype/server_deployment_service/server_deployment_service.json b/mail/server/doctype/server_deployment_service/server_deployment_service.json index b8fcecf0b..cc4a662e3 100644 --- a/mail/server/doctype/server_deployment_service/server_deployment_service.json +++ b/mail/server/doctype/server_deployment_service/server_deployment_service.json @@ -63,7 +63,7 @@ { "default": "[]", "depends_on": "eval: doc.__islocal || (doc.volumes && doc.volumes != \"[]\")", - "description": "List of volume mappings, e.g. [\"/data:/opt/data\"].", + "description": "List of volume mappings, e.g. [\"/data:/etc/data\"].", "fieldname": "volumes", "fieldtype": "JSON", "label": "Volumes" @@ -99,7 +99,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2025-11-07 12:38:48.566967", + "modified": "2026-05-05 23:22:00.493653", "modified_by": "Administrator", "module": "Server", "name": "Server Deployment Service", @@ -109,4 +109,4 @@ "sort_field": "creation", "sort_order": "DESC", "states": [] -} \ No newline at end of file +} diff --git a/mail/utils/ansible/playbooks/deploy-mail-server.yml b/mail/utils/ansible/playbooks/deploy-mail-server.yml index eab0ae0af..04223fbd5 100644 --- a/mail/utils/ansible/playbooks/deploy-mail-server.yml +++ b/mail/utils/ansible/playbooks/deploy-mail-server.yml @@ -3,30 +3,44 @@ hosts: all become: true vars: - stalwart_root: "/opt/stalwart/{{ server_hostname }}" - stalwart_config_dir: "{{ stalwart_root }}/etc" - stalwart_config_content: "{{ config_toml }}" + stalwart_root: "/etc/stalwart/{{ server_hostname }}" + stalwart_env_content: "{{ stalwart_env }}" + stalwart_bootstrap_plan_content: "{{ stalwart_bootstrap_plan }}" + recovery_admin_user_value: "{{ recovery_admin_user }}" + recovery_admin_password_value: "{{ recovery_admin_password }}" docker_compose_content: "{{ docker_compose }}" tasks: # ------------------------------------------------------------------------- # Directories and Configuration # ------------------------------------------------------------------------- - - name: Ensure Stalwart root directories exist + - name: Ensure Stalwart configuration & application directories exist file: path: "{{ item }}" state: directory - mode: "0755" + owner: "2000" + group: "2000" + mode: "0750" loop: - "{{ stalwart_root }}" - - "{{ stalwart_config_dir }}" + - "{{ stalwart_root }}/data" - "{{ stalwart_root }}/logs" - - name: Copy `config.toml` + - name: Copy `stalwart.env` copy: - content: "{{ stalwart_config_content }}" - dest: "{{ stalwart_config_dir }}/config.toml" - mode: "0644" + content: "{{ stalwart_env_content }}" + dest: "{{ stalwart_root }}/stalwart.env" + owner: "2000" + group: "2000" + mode: "0640" + + - name: Copy `bootstrap.ndjson` + copy: + content: "{{ stalwart_bootstrap_plan_content }}" + dest: "{{ stalwart_root }}/bootstrap.ndjson" + owner: "2000" + group: "2000" + mode: "0640" - name: Copy `docker-compose.yml` copy: @@ -34,53 +48,97 @@ dest: "{{ stalwart_root }}/docker-compose.yml" mode: "0644" + - name: Validate `docker-compose.yml` + command: docker compose config -q + args: + chdir: "{{ stalwart_root }}" + # ------------------------------------------------------------------------- - # Docker Image and Container Management + # Initial Startup in Bootstrap Mode # ------------------------------------------------------------------------- + - name: Pull images (optional) command: docker compose pull args: chdir: "{{ stalwart_root }}" register: pull_result - ignore_errors: yes + ignore_errors: true - - name: Stop any existing Stalwart container - command: docker compose down + - name: Start stack in bootstrap mode + command: docker compose up -d --remove-orphans args: chdir: "{{ stalwart_root }}" - ignore_errors: yes - - - name: Remove Redis container if not required - command: docker rm -f redis_{{ server_hostname }} - ignore_errors: yes - when: not install_redis + register: compose_up_bootstrap # ------------------------------------------------------------------------- - # Start stack + # Health Checks # ------------------------------------------------------------------------- - - name: Start docker stack - command: docker compose up -d + - name: Wait for HTTP port to be ready + wait_for: + host: 127.0.0.1 + port: 8080 + timeout: 180 + register: wait_result + failed_when: wait_result is failed + + - name: Copy local `stalwart-cli` binary to remote host + copy: + src: "{{ playbook_dir }}/../../../../stalwart-cli" + dest: "{{ stalwart_root }}/stalwart-cli" + mode: "0755" + + - name: Check if bootstrap has already been completed + stat: + path: "{{ stalwart_root }}/config.json" + register: bootstrap_config + + - name: Apply bootstrap plan through `stalwart-cli` + command: > + {{ stalwart_root }}/stalwart-cli apply --file {{ stalwart_root }}/bootstrap.ndjson --json + environment: + STALWART_URL: "http://127.0.0.1:8080" + STALWART_USER: "{{ recovery_admin_user_value }}" + STALWART_PASSWORD: "{{ recovery_admin_password_value }}" + register: apply_result + changed_when: apply_result.rc == 0 and ('"created"' in apply_result.stdout or '"updated"' in apply_result.stdout) + failed_when: false + when: not bootstrap_config.stat.exists + + - name: Fail deployment if bootstrap apply failed for reasons other than already-bootstrapped server + fail: + msg: "{{ apply_result.stdout | default(apply_result.stderr, true) }}" + when: + - not bootstrap_config.stat.exists + - apply_result.rc != 0 + - "'This operation is only allowed bootstrap mode' not in (apply_result.stdout | default(''))" + - "'This operation is only allowed bootstrap mode' not in (apply_result.stderr | default(''))" + + - name: Restart the Stalwart + command: docker compose restart stalwart + args: + chdir: "{{ stalwart_root }}" + register: restart_stalwart + + - name: Ensure full stack is up + command: docker compose up -d --remove-orphans args: chdir: "{{ stalwart_root }}" register: compose_up - # ------------------------------------------------------------------------- - # Health Checks - # ------------------------------------------------------------------------- - - name: Wait for HTTP port to be ready + - name: Wait for HTTP port to be ready after normal restart wait_for: host: 127.0.0.1 - port: "{{ http_port | int }}" - timeout: 90 - register: wait_result - ignore_errors: yes + port: 8080 + timeout: 180 + register: wait_result_normal + failed_when: wait_result_normal is failed - name: Check container health via `docker compose ps` command: docker compose ps args: chdir: "{{ stalwart_root }}" register: ps_result - ignore_errors: yes + ignore_errors: true # ------------------------------------------------------------------------- # Debug Info @@ -88,3 +146,8 @@ - name: Display container status debug: var: ps_result.stdout_lines + + - name: Display bootstrap apply summary + debug: + var: apply_result.stdout_lines + when: apply_result is defined diff --git a/mail/utils/ansible/playbooks/install-docker.yml b/mail/utils/ansible/playbooks/install-docker.yml index feb314e13..e4a7f58e3 100644 --- a/mail/utils/ansible/playbooks/install-docker.yml +++ b/mail/utils/ansible/playbooks/install-docker.yml @@ -2,7 +2,7 @@ - name: Install Docker hosts: all become: true - gather_facts: yes + gather_facts: true vars: docker_arch_map: @@ -45,6 +45,7 @@ - docker-ce - docker-ce-cli - containerd.io + - docker-compose-plugin state: present update_cache: true when: ansible_os_family == "Debian" @@ -76,6 +77,7 @@ - docker-ce - docker-ce-cli - containerd.io + - docker-compose-plugin state: present when: ansible_os_family == "RedHat" @@ -103,6 +105,7 @@ - docker-ce - docker-ce-cli - containerd.io + - docker-compose-plugin state: present when: ansible_distribution == "Fedora" @@ -120,7 +123,9 @@ - name: Install Docker (Arch) pacman: - name: docker + name: + - docker + - docker-compose state: present when: ansible_os_family == "Archlinux" @@ -133,16 +138,10 @@ enabled: true state: started - - name: Install Docker Compose - get_url: - url: "https://github.com/docker/compose/releases/latest/download/docker-compose-{{ ansible_system | lower }}-{{ ansible_architecture }}" - dest: /usr/local/bin/docker-compose - mode: "0755" - - name: Verify Docker installation command: docker --version changed_when: false - - name: Verify `docker-compose` installation - command: docker-compose --version + - name: Verify `docker compose` installation + command: docker compose version changed_when: false diff --git a/mail/utils/fc/filebeat_stream_setup.sh b/mail/utils/fc/filebeat_stream_setup.sh index 8359af2d9..dba97b7c8 100644 --- a/mail/utils/fc/filebeat_stream_setup.sh +++ b/mail/utils/fc/filebeat_stream_setup.sh @@ -12,7 +12,8 @@ sudo bash -c "cat > $FILEBEAT_INPUT_PATH" < Date: Fri, 8 May 2026 12:31:54 +0530 Subject: [PATCH 11/55] feat: Mail Cluster Store --- .../doctype/mail_cluster/mail_cluster.js | 75 ---- .../doctype/mail_cluster/mail_cluster.json | 353 +++++------------- .../doctype/mail_cluster/mail_cluster.py | 167 +++++---- .../doctype/mail_cluster_store/__init__.py | 0 .../mail_cluster_store/mail_cluster_store.js | 59 +++ .../mail_cluster_store.json | 313 ++++++++++++++++ .../mail_cluster_store/mail_cluster_store.py | 94 +++++ .../test_mail_cluster_store.py | 20 + .../server/doctype/mail_server/mail_server.py | 33 +- 9 files changed, 660 insertions(+), 454 deletions(-) create mode 100644 mail/server/doctype/mail_cluster_store/__init__.py create mode 100644 mail/server/doctype/mail_cluster_store/mail_cluster_store.js create mode 100644 mail/server/doctype/mail_cluster_store/mail_cluster_store.json create mode 100644 mail/server/doctype/mail_cluster_store/mail_cluster_store.py create mode 100644 mail/server/doctype/mail_cluster_store/test_mail_cluster_store.py diff --git a/mail/server/doctype/mail_cluster/mail_cluster.js b/mail/server/doctype/mail_cluster/mail_cluster.js index b9c14bcaa..1372c7ad0 100644 --- a/mail/server/doctype/mail_cluster/mail_cluster.js +++ b/mail/server/doctype/mail_cluster/mail_cluster.js @@ -1,86 +1,11 @@ // Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -const STORE_PRESET = { - RocksDb: { - store_path: '/etc/stalwart/data', - store_blob_size: 16834, - store_buffer_size: 134217728, - store_pool_workers: 1, - }, - Sqlite: { - store_path: '/etc/stalwart/data', - store_pool_workers: 1, - store_pool_max_connections: 10, - }, - FoundationDb: { - store_cluster_file: null, - store_datacenter_id: null, - store_machine_id: null, - store_transaction_retry_delay: null, - store_transaction_retry_limit: null, - store_transaction_timeout: null, - }, - PostgreSql: { - store_timeout: '15s', - store_use_tls: 0, - store_allow_invalid_certs: 0, - store_pool_max_connections: 10, - store_pool_recycling_method: 'fast', - store_host: null, - store_port: 5432, - store_database: 'frappe', - store_auth_username: null, - store_auth_secret: null, - store_options: null, - }, - MySql: { - store_timeout: '15s', - store_max_allowed_packet: 0, - store_use_tls: 0, - store_allow_invalid_certs: 0, - store_pool_min_connections: 5, - store_pool_max_connections: 10, - store_host: null, - store_port: 3306, - store_database: 'frappe', - store_auth_username: null, - store_auth_secret: null, - }, -} - frappe.ui.form.on('Mail Cluster', { - setup(frm) { - frm.trigger('initialize_defaults') - }, - refresh(frm) { frm.trigger('add_actions') }, - store_type(frm) { - const defaults = STORE_PRESET[frm.doc.store_type] - if (defaults) { - Object.entries(defaults).forEach(([key, value]) => frm.set_value(key, value)) - } - }, - - initialize_defaults(frm) { - if (!frm.doc.__islocal) return - - frappe.call({ - doc: frm.doc, - method: 'initialize_defaults', - freeze: true, - freeze_message: __('Initializing Defaults...'), - callback: (r) => { - if (!r.exc) { - frm.refresh() - } - }, - }) - }, - add_actions(frm) { if (frm.doc.__islocal) return diff --git a/mail/server/doctype/mail_cluster/mail_cluster.json b/mail/server/doctype/mail_cluster/mail_cluster.json index 5c42181fb..2fe8a59e7 100644 --- a/mail/server/doctype/mail_cluster/mail_cluster.json +++ b/mail/server/doctype/mail_cluster/mail_cluster.json @@ -24,40 +24,18 @@ "ssh_key_section", "ssh_public_key", "ssh_private_key", - "store_tab", - "store_type", - "column_break_yhyn", - "store_path", - "store_blob_size", - "store_buffer_size", - "section_break_plrk", - "store_timeout", - "store_max_allowed_packet", - "store_use_tls", - "store_allow_invalid_certs", - "column_break_djvr", - "store_pool_workers", - "store_pool_min_connections", - "store_pool_max_connections", - "store_pool_recycling_method", - "section_break_bpyx", - "store_cluster_file", - "store_datacenter_id", - "store_machine_id", - "column_break_dsjl", - "store_transaction_retry_delay", - "store_transaction_retry_limit", - "store_transaction_timeout", - "section_break_ntwb", - "store_host", - "store_port", - "store_database", - "column_break_grwu", - "store_auth_username", - "store_auth_secret", - "store_options", - "config_tab", - "config" + "storage_tab", + "data_store", + "blob_store", + "column_break_vvzf", + "search_store", + "in_memory_store", + "configs_tab", + "data_store_config", + "blob_store_config", + "column_break_rxvx", + "search_store_config", + "in_memory_store_config" ], "fields": [ { @@ -174,272 +152,111 @@ "fieldtype": "Section Break", "label": "SSH Key" }, - { - "fieldname": "config_tab", - "fieldtype": "Tab Break", - "label": "Config" - }, - { - "depends_on": "eval: !doc.__islocal && doc.config && doc.config != \"{}\"", - "fieldname": "config", - "fieldtype": "JSON", - "is_virtual": 1, - "label": "Config", - "no_copy": 1, - "read_only": 1 - }, - { - "fieldname": "section_break_bpyx", - "fieldtype": "Section Break" - }, - { - "fieldname": "column_break_dsjl", - "fieldtype": "Column Break" - }, - { - "fieldname": "section_break_plrk", - "fieldtype": "Section Break" - }, - { - "fieldname": "column_break_djvr", - "fieldtype": "Column Break" - }, - { - "fieldname": "section_break_ntwb", - "fieldtype": "Section Break" - }, - { - "fieldname": "column_break_grwu", - "fieldtype": "Column Break" - }, - { - "fieldname": "column_break_yhyn", - "fieldtype": "Column Break" - }, - { - "depends_on": "eval: [\"PostgreSql\"].includes(doc.store_type)", - "description": "Additional connection options.", - "fieldname": "store_options", - "fieldtype": "Data", - "label": "Options" - }, - { - "depends_on": "eval: [\"PostgreSql\", \"MySql\"].includes(doc.store_type)", - "description": "Hostname of the database server.", - "fieldname": "store_host", - "fieldtype": "Data", - "label": "Host", - "mandatory_depends_on": "eval: [\"PostgreSql\", \"MySql\"].includes(doc.store_type)" - }, - { - "depends_on": "eval: [\"PostgreSql\", \"MySql\"].includes(doc.store_type)", - "description": "Port of the database server.", - "fieldname": "store_port", - "fieldtype": "Int", - "label": "Port", - "mandatory_depends_on": "eval: [\"PostgreSql\", \"MySql\"].includes(doc.store_type)" - }, { "default": "frappe", - "depends_on": "eval: [\"PostgreSql\", \"MySql\"].includes(doc.store_type)", - "description": "Name of the database.", - "fieldname": "store_database", + "description": "Username for administrative access to the cluster.", + "fieldname": "recovery_admin_user", "fieldtype": "Data", - "label": "Database", - "mandatory_depends_on": "eval: [\"PostgreSql\", \"MySql\"].includes(doc.store_type)" - }, - { - "description": "Configures the primary data store backend.", - "fieldname": "store_type", - "fieldtype": "Select", - "label": "Type", - "options": "\nRocksDb\nSqlite\nFoundationDb\nPostgreSql\nMySql", + "label": "Username", "reqd": 1 }, { - "depends_on": "eval: [\"RocksDb\", \"Sqlite\"].includes(doc.store_type)", - "description": "Path to the data directory", - "fieldname": "store_path", - "fieldtype": "Data", - "label": "Path", - "mandatory_depends_on": "eval: [\"RocksDb\", \"Sqlite\"].includes(doc.store_type)" - }, - { - "default": "16834", - "depends_on": "eval: [\"RocksDb\"].includes(doc.store_type)", - "description": "Minimum size of a blob to store in the blob store, smaller blobs are stored in the metadata store.", - "fieldname": "store_blob_size", - "fieldtype": "Int", - "label": "Blob Size", - "mandatory_depends_on": "eval: [\"RocksDb\"].includes(doc.store_type)" - }, - { - "default": "134217728", - "depends_on": "eval: [\"RocksDb\"].includes(doc.store_type)", - "description": "Size of the write buffer in bytes, used to batch writes to the store.", - "fieldname": "store_buffer_size", - "fieldtype": "Int", - "label": "Buffer Size", - "mandatory_depends_on": "eval: [\"RocksDb\"].includes(doc.store_type)" + "description": "Password for administrative access to the cluster.", + "fieldname": "recovery_admin_password", + "fieldtype": "Password", + "label": "Password", + "no_copy": 1 }, { - "default": "15s", - "depends_on": "eval: [\"PostgreSql\", \"MySql\"].includes(doc.store_type)", - "description": "Connection timeout to the database.", - "fieldname": "store_timeout", + "fieldname": "default_domain", "fieldtype": "Data", - "label": "Timeout", - "mandatory_depends_on": "eval: [\"PostgreSql\", \"MySql\"].includes(doc.store_type)" - }, - { - "depends_on": "eval: [\"MySql\"].includes(doc.store_type)", - "description": "Maximum size of a packet in bytes.", - "fieldname": "store_max_allowed_packet", - "fieldtype": "Int", - "label": "Max Allowed Packet", - "mandatory_depends_on": "eval: [\"MySql\"].includes(doc.store_type)" - }, - { - "default": "0", - "depends_on": "eval: [\"PostgreSql\", \"MySql\"].includes(doc.store_type)", - "description": "Use TLS to connect to the store.", - "fieldname": "store_use_tls", - "fieldtype": "Check", - "label": "Use TLS" - }, - { - "default": "0", - "depends_on": "eval: [\"PostgreSql\", \"MySql\"].includes(doc.store_type)", - "description": "Allow invalid TLS certificates when connecting to the store.", - "fieldname": "store_allow_invalid_certs", - "fieldtype": "Check", - "label": "Allow Invalid Certs" - }, - { - "depends_on": "eval: [\"RocksDb\", \"Sqlite\"].includes(doc.store_type)", - "description": "Number of worker threads to use for the store, defaults to the number of cores.", - "fieldname": "store_pool_workers", - "fieldtype": "Int", - "label": "Pool Workers", - "mandatory_depends_on": "eval: [\"RocksDb\", \"Sqlite\"].includes(doc.store_type)" - }, - { - "default": "5", - "depends_on": "eval: [\"MySql\"].includes(doc.store_type)", - "description": "Minimum number of connections to the store.", - "fieldname": "store_pool_min_connections", - "fieldtype": "Int", - "label": "Pool Min Connections", - "mandatory_depends_on": "eval: [\"MySql\"].includes(doc.store_type)" - }, - { - "default": "10", - "depends_on": "eval: [\"Sqlite\", \"PostgreSql\", \"MySql\"].includes(doc.store_type)", - "description": "Maximum number of connections to the store.", - "fieldname": "store_pool_max_connections", - "fieldtype": "Int", - "label": "Pool Max Connections", - "mandatory_depends_on": "eval: [\"Sqlite\", \"PostgreSql\", \"MySql\"].includes(doc.store_type)" - }, - { - "default": "fast", - "depends_on": "eval: [\"PostgreSql\"].includes(doc.store_type)", - "description": "Method to use when recycling connections in the pool.", - "fieldname": "store_pool_recycling_method", - "fieldtype": "Select", - "label": "Pool Recycling Method", - "mandatory_depends_on": "eval: [\"PostgreSql\"].includes(doc.store_type)", - "options": "fast\nverified\nclean" + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Default Domain", + "search_index": 1, + "set_only_once": 1 }, { - "depends_on": "eval: [\"FoundationDb\"].includes(doc.store_type)", - "description": "Path to the cluster file for the FoundationDB cluster.", - "fieldname": "store_cluster_file", - "fieldtype": "Data", - "label": "Cluster File", - "mandatory_depends_on": "eval: [\"FoundationDb\"].includes(doc.store_type)" + "fieldname": "storage_tab", + "fieldtype": "Tab Break", + "label": "Storage" }, { - "depends_on": "eval: [\"FoundationDb\"].includes(doc.store_type)", - "description": "Data center ID.", - "fieldname": "store_datacenter_id", - "fieldtype": "Data", - "label": "Datacenter ID" + "description": "Configures the primary data store backend.", + "fieldname": "data_store", + "fieldtype": "Link", + "label": "Data Store", + "options": "Mail Cluster Store" }, { - "depends_on": "eval: [\"FoundationDb\"].includes(doc.store_type)", - "description": "Machine ID in the FoundationDB cluster.", - "fieldname": "store_machine_id", - "fieldtype": "Data", - "label": "Machine ID" + "description": "Configures the blob store backend.", + "fieldname": "blob_store", + "fieldtype": "Link", + "label": "Blob Store", + "options": "Mail Cluster Store" }, { - "depends_on": "eval: [\"FoundationDb\"].includes(doc.store_type)", - "description": "Transaction maximum retry delay.", - "fieldname": "store_transaction_retry_delay", - "fieldtype": "Data", - "label": "Transaction Retry Delay", - "mandatory_depends_on": "eval: [\"FoundationDb\"].includes(doc.store_type)" + "fieldname": "column_break_vvzf", + "fieldtype": "Column Break" }, { - "depends_on": "eval: [\"FoundationDb\"].includes(doc.store_type)", - "description": "Transaction retry limit.", - "fieldname": "store_transaction_retry_limit", - "fieldtype": "Int", - "label": "Transaction Retry Limit", - "mandatory_depends_on": "eval: [\"FoundationDb\"].includes(doc.store_type)" + "description": "Configures the search store backend.", + "fieldname": "search_store", + "fieldtype": "Link", + "label": "Search Store", + "options": "Mail Cluster Store" }, { - "depends_on": "eval: [\"FoundationDb\"].includes(doc.store_type)", - "description": "Transaction timeout.", - "fieldname": "store_transaction_timeout", - "fieldtype": "Data", - "label": "Transaction Timeout", - "mandatory_depends_on": "eval: [\"FoundationDb\"].includes(doc.store_type)" + "description": "Configures the in-memory store backend.", + "fieldname": "in_memory_store", + "fieldtype": "Link", + "label": "In-Memory Store", + "options": "Mail Cluster Store" }, { - "default": "frappe", - "depends_on": "eval: [\"PostgreSql\", \"MySql\"].includes(doc.store_type)", - "description": "Username to connect to the store.", - "fieldname": "store_auth_username", - "fieldtype": "Data", - "label": "Auth Username" + "depends_on": "eval: !doc.__islocal && doc.data_store_config", + "fieldname": "data_store_config", + "fieldtype": "JSON", + "is_virtual": 1, + "label": "Data Config", + "no_copy": 1, + "read_only": 1 }, { - "depends_on": "eval: [\"PostgreSql\", \"MySql\"].includes(doc.store_type)", - "description": "Password to connect to the store.", - "fieldname": "store_auth_secret", - "fieldtype": "Password", - "label": "Auth Secret" + "fieldname": "configs_tab", + "fieldtype": "Tab Break", + "label": "Configs" }, { - "fieldname": "store_tab", - "fieldtype": "Tab Break", - "label": "Store" + "depends_on": "eval: !doc.__islocal && doc.blob_store_config", + "fieldname": "blob_store_config", + "fieldtype": "JSON", + "is_virtual": 1, + "label": "Blob Config", + "no_copy": 1, + "read_only": 1 }, { - "default": "frappe", - "description": "Username for administrative access to the cluster.", - "fieldname": "recovery_admin_user", - "fieldtype": "Data", - "label": "Username", - "reqd": 1 + "depends_on": "eval: !doc.__islocal && doc.search_store_config", + "fieldname": "search_store_config", + "fieldtype": "JSON", + "is_virtual": 1, + "label": "Search Config", + "no_copy": 1, + "read_only": 1 }, { - "description": "Password for administrative access to the cluster.", - "fieldname": "recovery_admin_password", - "fieldtype": "Password", - "label": "Password", - "no_copy": 1 + "depends_on": "eval: !doc.__islocal && doc.in_memory_store_config", + "fieldname": "in_memory_store_config", + "fieldtype": "JSON", + "is_virtual": 1, + "label": "In-Memory Config", + "no_copy": 1, + "read_only": 1 }, { - "fieldname": "default_domain", - "fieldtype": "Data", - "in_list_view": 1, - "in_standard_filter": 1, - "label": "Default Domain", - "search_index": 1, - "set_only_once": 1 + "fieldname": "column_break_rxvx", + "fieldtype": "Column Break" } ], "grid_page_length": 50, @@ -451,7 +268,7 @@ "link_fieldname": "cluster" } ], - "modified": "2026-05-07 14:53:24.555320", + "modified": "2026-05-08 12:30:30.414245", "modified_by": "Administrator", "module": "Server", "name": "Mail Cluster", diff --git a/mail/server/doctype/mail_cluster/mail_cluster.py b/mail/server/doctype/mail_cluster/mail_cluster.py index d2812d3b4..ec919483b 100644 --- a/mail/server/doctype/mail_cluster/mail_cluster.py +++ b/mail/server/doctype/mail_cluster/mail_cluster.py @@ -9,7 +9,7 @@ import paramiko from frappe import _ from frappe.model.document import Document -from frappe.utils import cint, random_string +from frappe.utils import random_string from mail.backend import MailBackendAPI, Principal from mail.jmap.connection import raise_for_status @@ -19,94 +19,52 @@ class MailCluster(Document): @property - def config(self) -> str: - """Returns the configuration for the cluster.""" - - if not self.store_type: + def data_store_config(self) -> str: + if not self.data_store: return "{}" - config = {"@type": self.store_type} + store = frappe.get_doc("Mail Cluster Store", self.data_store) + return json.dumps(store.config, indent=4) - if self.store_type == "RocksDb": - config.update( - { - "path": self.store_path, - "blobSize": cint(self.store_blob_size), - "bufferSize": cint(self.store_buffer_size), - "poolWorkers": cint(self.store_pool_workers), - } - ) + @property + def blob_store_config(self) -> str: + if not self.blob_store: + return '{"@type": "Default"}' - elif self.store_type == "Sqlite": - config.update( - { - "path": self.store_path, - "poolWorkers": cint(self.store_pool_workers), - "poolMaxConnections": cint(self.store_pool_max_connections), - } - ) + store = frappe.get_doc("Mail Cluster Store", self.blob_store) + return json.dumps(store.config, indent=4) - elif self.store_type == "FoundationDb": - config.update( - { - "clusterFile": self.store_cluster_file, - "datacenterId": self.store_datacenter_id, - "machineId": self.store_machine_id, - "transactionRetryDelay": cint(self.store_transaction_retry_delay), - "transactionRetryLimit": cint(self.store_transaction_retry_limit), - "transactionTimeout": cint(self.store_transaction_timeout), - } - ) + @property + def search_store_config(self) -> str: + if not self.search_store: + return '{"@type": "Default"}' - elif self.store_type == "PostgreSql": - config.update( - { - "timeout": cint(self.store_timeout), - "useTls": cint(self.store_use_tls), - "allowInvalidCerts": cint(self.store_allow_invalid_certs), - "poolMaxConnections": cint(self.store_pool_max_connections), - "poolRecyclingMethod": self.store_pool_recycling_method, - "host": self.store_host, - "port": cint(self.store_port), - "database": self.store_database, - "authUsername": self.store_auth_username, - "authSecret": self.get_password("store_auth_secret") if self.store_auth_secret else None, - "options": self.store_options, - } - ) + store = frappe.get_doc("Mail Cluster Store", self.search_store) + return json.dumps(store.config, indent=4) - elif self.store_type == "MySql": - config.update( - { - "timeout": cint(self.store_timeout), - "useTls": cint(self.store_use_tls), - "allowInvalidCerts": cint(self.store_allow_invalid_certs), - "maxAllowedPacket": cint(self.store_max_allowed_packet), - "poolMaxConnections": cint(self.store_pool_max_connections), - "poolMinConnections": cint(self.store_pool_min_connections), - "host": self.store_host, - "port": cint(self.store_port), - "database": self.store_database, - "authUsername": self.store_auth_username, - "authSecret": self.get_password("store_auth_secret") if self.store_auth_secret else None, - } - ) + @property + def in_memory_store_config(self) -> str: + if not self.in_memory_store: + return '{"@type": "Default"}' - return json.dumps(config, indent=4) + store = frappe.get_doc("Mail Cluster Store", self.in_memory_store) + return json.dumps(store.config, indent=4) def autoname(self) -> None: self.hostname = self.hostname.lower() self.name = self.hostname + def before_insert(self) -> None: + self.generate_ssh_keypair() + self.initialize_defaults() + def validate(self) -> None: self.validate_enabled() self.validate_hostname() self.validate_default_domain() self.validate_recovery_admin_password() self.validate_base_url() - - def before_insert(self) -> None: - self.generate_ssh_keypair() + self.validate_stores() def on_trash(self) -> None: if frappe.session.user != "Administrator": @@ -153,6 +111,16 @@ def validate_base_url(self) -> None: if not self.base_url: self.base_url = f"https://{self.hostname}/" + def validate_stores(self) -> None: + """Validates the data stores of the cluster.""" + + if not self.data_store: + frappe.throw(_("Data Store is required.")) + + for store_field in ["blob_store", "search_store", "in_memory_store"]: + if self.get(store_field) == self.data_store: + setattr(self, store_field, None) + def generate_ssh_keypair(self, save: bool = False) -> None: """Generates an SSH key pair for the cluster.""" @@ -169,17 +137,21 @@ def generate_ssh_keypair(self, save: bool = False) -> None: def initialize_defaults(self) -> None: """Initializes the default values.""" - self.initialize_store() + self.initialize_data_store() - def initialize_store(self) -> None: - """Initializes the default store configuration.""" + def initialize_data_store(self) -> None: + """Initializes the data store for the cluster.""" - if not self.store_type: - self.store_type = "RocksDb" - self.store_path = "/etc/stalwart/data/rocksdb" - self.store_blob_size = 16834 - self.store_buffer_size = 134217728 - self.store_pool_workers = 1 + if not self.data_store: + store = frappe.new_doc("Mail Cluster Store") + store.type = "RocksDb" + store.path = "/etc/stalwart/data" + store.blob_size = 16834 + store.buffer_size = 134217728 + store.pool_workers = 1 + store.insert() + + self.data_store = store.name @frappe.whitelist() def get_recovery_admin_password(self) -> str: @@ -221,6 +193,43 @@ def _generate_api_key(self) -> str: return f"api_{base64.b64encode(f'{name}:{secret}'.encode()).decode()}" + def get_bootstrap_operations(self, hostname: str = "{{ hostname }}") -> list[dict]: + """Returns the bootstrap operations for the cluster.""" + + operations = [ + { + "@type": "update", + "object": "Bootstrap", + "id": "singleton", + "value": { + # required + "serverHostname": hostname, + "defaultDomain": self.default_domain, + # optional + "requestTlsCertificate": True, + "generateDkimKeys": True, + "dataStore": json.loads(self.data_store_config), + "blobStore": json.loads(self.blob_store_config), + "searchStore": json.loads(self.search_store_config), + "inMemoryStore": json.loads(self.in_memory_store_config), + "directory": {"@type": "Internal"}, + "tracer": { + "@type": "Log", + "ansi": True, + "enable": True, + "eventsPolicy": "exclude", + "level": "info", + "prefix": "stalwart", + "rotate": "daily", + "path": "/etc/stalwart/logs", + }, + "dnsServer": {"@type": "Manual"}, + }, + } + ] + + return operations + def get_storage_labels() -> dict: """Returns the storage labels.""" diff --git a/mail/server/doctype/mail_cluster_store/__init__.py b/mail/server/doctype/mail_cluster_store/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/mail/server/doctype/mail_cluster_store/mail_cluster_store.js b/mail/server/doctype/mail_cluster_store/mail_cluster_store.js new file mode 100644 index 000000000..c9131f45f --- /dev/null +++ b/mail/server/doctype/mail_cluster_store/mail_cluster_store.js @@ -0,0 +1,59 @@ +// Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +const STORE_PRESET = { + RocksDb: { + path: '/etc/stalwart/data', + blob_size: 16834, + buffer_size: 134217728, + pool_workers: 1, + }, + Sqlite: { + path: '/etc/stalwart/data', + pool_workers: 1, + pool_max_connections: 10, + }, + FoundationDb: { + cluster_file: null, + datacenter_id: null, + machine_id: null, + transaction_retry_delay: null, + transaction_retry_limit: null, + transaction_timeout: null, + }, + PostgreSql: { + timeout: '15s', + use_tls: 0, + allow_invalid_certs: 0, + pool_max_connections: 10, + pool_recycling_method: 'fast', + host: null, + port: 5432, + database: 'frappe', + auth_username: null, + auth_secret: null, + options: null, + }, + MySql: { + timeout: '15s', + max_allowed_packet: 0, + use_tls: 0, + allow_invalid_certs: 0, + pool_min_connections: 5, + pool_max_connections: 10, + host: null, + port: 3306, + database: 'frappe', + auth_username: null, + auth_secret: null, + }, +} + +frappe.ui.form.on('Mail Cluster Store', { + type(frm) { + const defaults = STORE_PRESET[frm.doc.type] + if (defaults) { + Object.entries(defaults).forEach(([key, value]) => frm.set_value(key, value)) + } + }, +}) diff --git a/mail/server/doctype/mail_cluster_store/mail_cluster_store.json b/mail/server/doctype/mail_cluster_store/mail_cluster_store.json new file mode 100644 index 000000000..abae57ea9 --- /dev/null +++ b/mail/server/doctype/mail_cluster_store/mail_cluster_store.json @@ -0,0 +1,313 @@ +{ + "actions": [], + "creation": "2026-05-08 11:05:47.343917", + "default_view": "List", + "doctype": "DocType", + "engine": "InnoDB", + "field_order": [ + "store_tab", + "type", + "description", + "column_break_yhyn", + "path", + "blob_size", + "buffer_size", + "section_break_plrk", + "timeout", + "max_allowed_packet", + "use_tls", + "allow_invalid_certs", + "column_break_djvr", + "pool_workers", + "pool_min_connections", + "pool_max_connections", + "pool_recycling_method", + "section_break_bpyx", + "cluster_file", + "datacenter_id", + "machine_id", + "column_break_dsjl", + "transaction_retry_delay", + "transaction_retry_limit", + "transaction_timeout", + "section_break_ntwb", + "host", + "port", + "database", + "column_break_grwu", + "auth_username", + "auth_secret", + "options" + ], + "fields": [ + { + "fieldname": "store_tab", + "fieldtype": "Tab Break", + "label": "Store" + }, + { + "fieldname": "column_break_yhyn", + "fieldtype": "Column Break" + }, + { + "fieldname": "section_break_plrk", + "fieldtype": "Section Break" + }, + { + "fieldname": "column_break_djvr", + "fieldtype": "Column Break" + }, + { + "fieldname": "section_break_bpyx", + "fieldtype": "Section Break" + }, + { + "fieldname": "column_break_dsjl", + "fieldtype": "Column Break" + }, + { + "fieldname": "section_break_ntwb", + "fieldtype": "Section Break" + }, + { + "fieldname": "column_break_grwu", + "fieldtype": "Column Break" + }, + { + "description": "Description for the backend store.", + "fieldname": "description", + "fieldtype": "Small Text", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Description" + }, + { + "description": "Type of the store backend.", + "fieldname": "type", + "fieldtype": "Select", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Type", + "options": "\nRocksDb\nSqlite\nFoundationDb\nPostgreSql\nMySql", + "reqd": 1 + }, + { + "depends_on": "eval: [\"RocksDb\", \"Sqlite\"].includes(doc.type)", + "description": "Path to the data directory", + "fieldname": "path", + "fieldtype": "Data", + "label": "Path", + "mandatory_depends_on": "eval: [\"RocksDb\", \"Sqlite\"].includes(doc.type)" + }, + { + "default": "16834", + "depends_on": "eval: [\"RocksDb\"].includes(doc.type)", + "description": "Minimum size of a blob to store in the blob store, smaller blobs are stored in the metadata store.", + "fieldname": "blob_size", + "fieldtype": "Int", + "label": "Blob Size", + "mandatory_depends_on": "eval: [\"RocksDb\"].includes(doc.type)" + }, + { + "default": "134217728", + "depends_on": "eval: [\"RocksDb\"].includes(doc.type)", + "description": "Size of the write buffer in bytes, used to batch writes to the store.", + "fieldname": "buffer_size", + "fieldtype": "Int", + "label": "Buffer Size", + "mandatory_depends_on": "eval: [\"RocksDb\"].includes(doc.type)" + }, + { + "default": "15s", + "depends_on": "eval: [\"PostgreSql\", \"MySql\"].includes(doc.type)", + "description": "Connection timeout to the database.", + "fieldname": "timeout", + "fieldtype": "Data", + "label": "Timeout", + "mandatory_depends_on": "eval: [\"PostgreSql\", \"MySql\"].includes(doc.type)" + }, + { + "depends_on": "eval: [\"MySql\"].includes(doc.type)", + "description": "Maximum size of a packet in bytes.", + "fieldname": "max_allowed_packet", + "fieldtype": "Int", + "label": "Max Allowed Packet", + "mandatory_depends_on": "eval: [\"MySql\"].includes(doc.type)" + }, + { + "default": "0", + "depends_on": "eval: [\"PostgreSql\", \"MySql\"].includes(doc.type)", + "description": "Use TLS to connect to the store.", + "fieldname": "use_tls", + "fieldtype": "Check", + "label": "Use TLS" + }, + { + "default": "0", + "depends_on": "eval: [\"PostgreSql\", \"MySql\"].includes(doc.type)", + "description": "Allow invalid TLS certificates when connecting to the store.", + "fieldname": "allow_invalid_certs", + "fieldtype": "Check", + "label": "Allow Invalid Certs" + }, + { + "depends_on": "eval: [\"RocksDb\", \"Sqlite\"].includes(doc.type)", + "description": "Number of worker threads to use for the store, defaults to the number of cores.", + "fieldname": "pool_workers", + "fieldtype": "Int", + "label": "Pool Workers", + "mandatory_depends_on": "eval: [\"RocksDb\", \"Sqlite\"].includes(doc.type)" + }, + { + "default": "5", + "depends_on": "eval: [\"MySql\"].includes(doc.type)", + "description": "Minimum number of connections to the store.", + "fieldname": "pool_min_connections", + "fieldtype": "Int", + "label": "Pool Min Connections", + "mandatory_depends_on": "eval: [\"MySql\"].includes(doc.type)" + }, + { + "default": "10", + "depends_on": "eval: [\"Sqlite\", \"PostgreSql\", \"MySql\"].includes(doc.type)", + "description": "Maximum number of connections to the store.", + "fieldname": "pool_max_connections", + "fieldtype": "Int", + "label": "Pool Max Connections", + "mandatory_depends_on": "eval: [\"Sqlite\", \"PostgreSql\", \"MySql\"].includes(doc.type)" + }, + { + "default": "fast", + "depends_on": "eval: [\"PostgreSql\"].includes(doc.type)", + "description": "Method to use when recycling connections in the pool.", + "fieldname": "pool_recycling_method", + "fieldtype": "Select", + "label": "Pool Recycling Method", + "mandatory_depends_on": "eval: [\"PostgreSql\"].includes(doc.type)", + "options": "fast\nverified\nclean" + }, + { + "depends_on": "eval: [\"FoundationDb\"].includes(doc.type)", + "description": "Path to the cluster file for the FoundationDB cluster.", + "fieldname": "cluster_file", + "fieldtype": "Data", + "label": "Cluster File", + "mandatory_depends_on": "eval: [\"FoundationDb\"].includes(doc.type)" + }, + { + "depends_on": "eval: [\"FoundationDb\"].includes(doc.type)", + "description": "Data center ID.", + "fieldname": "datacenter_id", + "fieldtype": "Data", + "label": "Datacenter ID" + }, + { + "depends_on": "eval: [\"FoundationDb\"].includes(doc.type)", + "description": "Machine ID in the FoundationDB cluster.", + "fieldname": "machine_id", + "fieldtype": "Data", + "label": "Machine ID" + }, + { + "depends_on": "eval: [\"FoundationDb\"].includes(doc.type)", + "description": "Transaction maximum retry delay.", + "fieldname": "transaction_retry_delay", + "fieldtype": "Data", + "label": "Transaction Retry Delay", + "mandatory_depends_on": "eval: [\"FoundationDb\"].includes(doc.type)" + }, + { + "depends_on": "eval: [\"FoundationDb\"].includes(doc.type)", + "description": "Transaction retry limit.", + "fieldname": "transaction_retry_limit", + "fieldtype": "Int", + "label": "Transaction Retry Limit", + "mandatory_depends_on": "eval: [\"FoundationDb\"].includes(doc.type)" + }, + { + "depends_on": "eval: [\"FoundationDb\"].includes(doc.type)", + "description": "Transaction timeout.", + "fieldname": "transaction_timeout", + "fieldtype": "Data", + "label": "Transaction Timeout", + "mandatory_depends_on": "eval: [\"FoundationDb\"].includes(doc.type)" + }, + { + "depends_on": "eval: [\"PostgreSql\", \"MySql\"].includes(doc.type)", + "description": "Hostname of the database server.", + "fieldname": "host", + "fieldtype": "Data", + "label": "Host", + "mandatory_depends_on": "eval: [\"PostgreSql\", \"MySql\"].includes(doc.type)" + }, + { + "depends_on": "eval: [\"PostgreSql\", \"MySql\"].includes(doc.type)", + "description": "Port of the database server.", + "fieldname": "port", + "fieldtype": "Int", + "label": "Port", + "mandatory_depends_on": "eval: [\"PostgreSql\", \"MySql\"].includes(doc.type)" + }, + { + "default": "frappe", + "depends_on": "eval: [\"PostgreSql\", \"MySql\"].includes(doc.type)", + "description": "Name of the database.", + "fieldname": "database", + "fieldtype": "Data", + "label": "Database", + "mandatory_depends_on": "eval: [\"PostgreSql\", \"MySql\"].includes(doc.type)" + }, + { + "default": "frappe", + "depends_on": "eval: [\"PostgreSql\", \"MySql\"].includes(doc.type)", + "description": "Username to connect to the store.", + "fieldname": "auth_username", + "fieldtype": "Data", + "label": "Auth Username" + }, + { + "depends_on": "eval: [\"PostgreSql\", \"MySql\"].includes(doc.type)", + "description": "Password to connect to the store.", + "fieldname": "auth_secret", + "fieldtype": "Password", + "label": "Auth Secret" + }, + { + "depends_on": "eval: [\"PostgreSql\"].includes(doc.type)", + "description": "Additional connection options.", + "fieldname": "options", + "fieldtype": "Data", + "label": "Options" + } + ], + "grid_page_length": 50, + "index_web_pages_for_search": 1, + "links": [], + "modified": "2026-05-08 12:20:05.216537", + "modified_by": "Administrator", + "module": "Server", + "name": "Mail Cluster Store", + "naming_rule": "Set by user", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "row_format": "Dynamic", + "show_title_field_in_link": 1, + "sort_field": "creation", + "sort_order": "DESC", + "states": [], + "title_field": "description", + "track_changes": 1 +} diff --git a/mail/server/doctype/mail_cluster_store/mail_cluster_store.py b/mail/server/doctype/mail_cluster_store/mail_cluster_store.py new file mode 100644 index 000000000..a19d712f0 --- /dev/null +++ b/mail/server/doctype/mail_cluster_store/mail_cluster_store.py @@ -0,0 +1,94 @@ +# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from uuid import uuid7 + +from frappe.model.document import Document +from frappe.utils import cint + + +class MailClusterStore(Document): + def autoname(self) -> None: + self.name = str(uuid7()) + + @property + def config(self) -> dict: + """Returns the configuration for the cluster store.""" + + if not self.type: + return {} + + config = {"@type": self.type} + + if self.type == "RocksDb": + config.update( + { + "path": self.path, + "blobSize": cint(self.blob_size), + "bufferSize": cint(self.buffer_size), + "poolWorkers": cint(self.pool_workers), + } + ) + + elif self.type == "Sqlite": + config.update( + { + "path": self.path, + "poolWorkers": cint(self.pool_workers), + "poolMaxConnections": cint(self.pool_max_connections), + } + ) + + elif self.type == "FoundationDb": + config.update( + { + "clusterFile": self.cluster_file, + "datacenterId": self.datacenter_id, + "machineId": self.machine_id, + "transactionRetryDelay": cint(self.transaction_retry_delay), + "transactionRetryLimit": cint(self.transaction_retry_limit), + "transactionTimeout": cint(self.transaction_timeout), + } + ) + + elif self.type == "PostgreSql": + config.update( + { + "timeout": cint(self.timeout), + "useTls": cint(self.use_tls), + "allowInvalidCerts": cint(self.allow_invalid_certs), + "poolMaxConnections": cint(self.pool_max_connections), + "poolRecyclingMethod": self.pool_recycling_method, + "host": self.host, + "port": cint(self.port), + "database": self.database, + "authUsername": self.auth_username, + "authSecret": self.get_password("auth_secret") if self.auth_secret else None, + "options": self.options, + } + ) + + elif self.type == "MySql": + config.update( + { + "timeout": cint(self.timeout), + "useTls": cint(self.use_tls), + "allowInvalidCerts": cint(self.allow_invalid_certs), + "maxAllowedPacket": cint(self.max_allowed_packet), + "poolMaxConnections": cint(self.pool_max_connections), + "poolMinConnections": cint(self.pool_min_connections), + "host": self.host, + "port": cint(self.port), + "database": self.database, + "authUsername": self.auth_username, + "authSecret": self.get_password("auth_secret") if self.auth_secret else None, + } + ) + + return config + + def validate(self) -> None: + """Validates the cluster store configuration.""" + + if not self.description: + self.description = self.type diff --git a/mail/server/doctype/mail_cluster_store/test_mail_cluster_store.py b/mail/server/doctype/mail_cluster_store/test_mail_cluster_store.py new file mode 100644 index 000000000..2518a1170 --- /dev/null +++ b/mail/server/doctype/mail_cluster_store/test_mail_cluster_store.py @@ -0,0 +1,20 @@ +# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +# import frappe +from frappe.tests import IntegrationTestCase + +# On IntegrationTestCase, the doctype test records and all +# link-field test record dependencies are recursively loaded +# Use these module variables to add/remove to/from that list +EXTRA_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"] +IGNORE_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"] + + +class IntegrationTestMailClusterStore(IntegrationTestCase): + """ + Integration tests for MailClusterStore. + Use this class for testing interactions between multiple components. + """ + + pass diff --git a/mail/server/doctype/mail_server/mail_server.py b/mail/server/doctype/mail_server/mail_server.py index d5a4c4e59..df16b5b54 100644 --- a/mail/server/doctype/mail_server/mail_server.py +++ b/mail/server/doctype/mail_server/mail_server.py @@ -70,38 +70,7 @@ def _generate_bootstrap_ndjson(self) -> str: """Generates the bootstrap NDJSON for the server.""" cluster = frappe.get_doc("Mail Cluster", self.cluster) - - operations = [ - { - "@type": "update", - "object": "Bootstrap", - "id": "singleton", - "value": { - # required - "serverHostname": self.hostname, - "defaultDomain": cluster.default_domain, - # optional - "requestTlsCertificate": True, - "generateDkimKeys": True, - "dataStore": json.loads(cluster.config), - "blobStore": {"@type": "Default"}, - "searchStore": {"@type": "Default"}, - "inMemoryStore": {"@type": "Default"}, - "directory": {"@type": "Internal"}, - "tracer": { - "@type": "Log", - "ansi": True, - "enable": True, - "eventsPolicy": "exclude", - "level": "info", - "prefix": "stalwart", - "rotate": "daily", - "path": "/etc/stalwart/logs", - }, - "dnsServer": {"@type": "Manual"}, - }, - } - ] + operations = cluster.get_bootstrap_operations(self.hostname) return "\n".join([json.dumps(op) for op in operations]) From 941641adf71b0cd512cef0ad6c21b050f36774d7 Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Mon, 11 May 2026 10:26:42 +0530 Subject: [PATCH 12/55] fix: validate stores --- .../doctype/mail_cluster/mail_cluster.js | 30 ++++++++++++++ .../doctype/mail_cluster/mail_cluster.py | 39 ++++++++++++++++--- 2 files changed, 64 insertions(+), 5 deletions(-) diff --git a/mail/server/doctype/mail_cluster/mail_cluster.js b/mail/server/doctype/mail_cluster/mail_cluster.js index 1372c7ad0..93cf76c84 100644 --- a/mail/server/doctype/mail_cluster/mail_cluster.js +++ b/mail/server/doctype/mail_cluster/mail_cluster.js @@ -2,10 +2,40 @@ // For license information, please see license.txt frappe.ui.form.on('Mail Cluster', { + setup(frm) { + frm.trigger('set_queries') + }, + refresh(frm) { frm.trigger('add_actions') }, + set_queries(frm) { + frm.set_query( + 'data_store', + () => ({ + filters: { + type: ['in', ['RocksDb', 'Sqlite', 'FoundationDb', 'PostgreSql', 'MySql']], + }, + }), + frm.set_query('blob_store', () => ({ + filters: { + type: ['in', ['RocksDb']], + }, + })), + frm.set_query('search_store', () => ({ + filters: { + type: ['in', ['RocksDb']], + }, + })), + frm.set_query('in_memory_store', () => ({ + filters: { + type: ['in', ['RocksDb']], + }, + })), + ) + }, + add_actions(frm) { if (frm.doc.__islocal) return diff --git a/mail/server/doctype/mail_cluster/mail_cluster.py b/mail/server/doctype/mail_cluster/mail_cluster.py index ec919483b..40bdefa1f 100644 --- a/mail/server/doctype/mail_cluster/mail_cluster.py +++ b/mail/server/doctype/mail_cluster/mail_cluster.py @@ -16,6 +16,13 @@ from mail.utils import generate_secret from mail.utils.dns import get_dns_record +ALLOWED_STORE_TYPES = { + "data_store": ["RocksDb", "Sqlite", "FoundationDb", "PostgreSql", "MySql"], + "blob_store": ["RocksDb"], + "search_store": ["RocksDb"], + "in_memory_store": ["RocksDb"], +} + class MailCluster(Document): @property @@ -114,12 +121,34 @@ def validate_base_url(self) -> None: def validate_stores(self) -> None: """Validates the data stores of the cluster.""" - if not self.data_store: - frappe.throw(_("Data Store is required.")) + def validate_store(store_field: str, required: bool = False) -> None: + store = self.get(store_field) + + if required and not store: + frappe.throw(_("{0} is required.").format(self.meta.get_field(store_field).label)) + + if not store: + return + + if store_field != "data_store" and store == self.data_store: + self.set(store_field, None) + return + + store_type = frappe.db.get_value("Mail Cluster Store", store, "type") + allowed_types = ALLOWED_STORE_TYPES[store_field] + + if store_type not in allowed_types: + frappe.throw( + _("{0} type must be one of {1}.").format( + self.meta.get_field(store_field).label, + ", ".join(allowed_types), + ) + ) + + validate_store("data_store", required=True) - for store_field in ["blob_store", "search_store", "in_memory_store"]: - if self.get(store_field) == self.data_store: - setattr(self, store_field, None) + for field in ["blob_store", "search_store", "in_memory_store"]: + validate_store(field) def generate_ssh_keypair(self, save: bool = False) -> None: """Generates an SSH key pair for the cluster.""" From e377557cb8b8459a42dd2b159775087bb813e5d9 Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Mon, 11 May 2026 10:51:45 +0530 Subject: [PATCH 13/55] chore: add support for Default and Sharded stores --- .../doctype/mail_cluster/mail_cluster.js | 6 ++--- .../doctype/mail_cluster/mail_cluster.py | 26 +++++++++++-------- .../mail_cluster_store.json | 4 +-- 3 files changed, 20 insertions(+), 16 deletions(-) diff --git a/mail/server/doctype/mail_cluster/mail_cluster.js b/mail/server/doctype/mail_cluster/mail_cluster.js index 93cf76c84..dc60132b6 100644 --- a/mail/server/doctype/mail_cluster/mail_cluster.js +++ b/mail/server/doctype/mail_cluster/mail_cluster.js @@ -20,17 +20,17 @@ frappe.ui.form.on('Mail Cluster', { }), frm.set_query('blob_store', () => ({ filters: { - type: ['in', ['RocksDb']], + type: ['in', ['RocksDb', 'Default', 'Sharded']], }, })), frm.set_query('search_store', () => ({ filters: { - type: ['in', ['RocksDb']], + type: ['in', ['RocksDb', 'Default']], }, })), frm.set_query('in_memory_store', () => ({ filters: { - type: ['in', ['RocksDb']], + type: ['in', ['RocksDb', 'Default']], }, })), ) diff --git a/mail/server/doctype/mail_cluster/mail_cluster.py b/mail/server/doctype/mail_cluster/mail_cluster.py index 40bdefa1f..d72a328d2 100644 --- a/mail/server/doctype/mail_cluster/mail_cluster.py +++ b/mail/server/doctype/mail_cluster/mail_cluster.py @@ -18,9 +18,9 @@ ALLOWED_STORE_TYPES = { "data_store": ["RocksDb", "Sqlite", "FoundationDb", "PostgreSql", "MySql"], - "blob_store": ["RocksDb"], - "search_store": ["RocksDb"], - "in_memory_store": ["RocksDb"], + "blob_store": ["RocksDb", "Default", "Sharded"], + "search_store": ["RocksDb", "Default"], + "in_memory_store": ["RocksDb", "Default"], } @@ -121,17 +121,21 @@ def validate_base_url(self) -> None: def validate_stores(self) -> None: """Validates the data stores of the cluster.""" - def validate_store(store_field: str, required: bool = False) -> None: + def validate_store(store_field: str) -> None: store = self.get(store_field) - if required and not store: - frappe.throw(_("{0} is required.").format(self.meta.get_field(store_field).label)) + if store_field == "data_store" and not store: + frappe.throw(_("Data Store is required.")) - if not store: - return + if not store or (store_field != "data_store" and store == self.data_store): + default_store = frappe.db.get_value("Mail Cluster Store", {"type": "Default"}, "name") + if not default_store: + doc = frappe.new_doc("Mail Cluster Store") + doc.type = "Default" + doc.insert() + default_store = doc.name - if store_field != "data_store" and store == self.data_store: - self.set(store_field, None) + self.set(store_field, default_store) return store_type = frappe.db.get_value("Mail Cluster Store", store, "type") @@ -145,7 +149,7 @@ def validate_store(store_field: str, required: bool = False) -> None: ) ) - validate_store("data_store", required=True) + validate_store("data_store") for field in ["blob_store", "search_store", "in_memory_store"]: validate_store(field) diff --git a/mail/server/doctype/mail_cluster_store/mail_cluster_store.json b/mail/server/doctype/mail_cluster_store/mail_cluster_store.json index abae57ea9..dc42d0ca2 100644 --- a/mail/server/doctype/mail_cluster_store/mail_cluster_store.json +++ b/mail/server/doctype/mail_cluster_store/mail_cluster_store.json @@ -88,7 +88,7 @@ "in_list_view": 1, "in_standard_filter": 1, "label": "Type", - "options": "\nRocksDb\nSqlite\nFoundationDb\nPostgreSql\nMySql", + "options": "\nRocksDb\nSqlite\nFoundationDb\nPostgreSql\nMySql\nDefault\nSharded", "reqd": 1 }, { @@ -283,7 +283,7 @@ "grid_page_length": 50, "index_web_pages_for_search": 1, "links": [], - "modified": "2026-05-08 12:20:05.216537", + "modified": "2026-05-11 10:45:44.453116", "modified_by": "Administrator", "module": "Server", "name": "Mail Cluster Store", From c9c0e37cbe0234b1f28f992aeef32c2a0b6ce60c Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Mon, 11 May 2026 10:58:30 +0530 Subject: [PATCH 14/55] feat: template for Blob stores --- .../doctype/mail_cluster/mail_cluster.js | 6 +- .../doctype/mail_cluster/mail_cluster.py | 6 +- .../mail_cluster_store/mail_cluster_store.js | 34 ++-- .../mail_cluster_store.json | 154 +++++++++++++++++- .../mail_cluster_store/mail_cluster_store.py | 62 ++++++- 5 files changed, 221 insertions(+), 41 deletions(-) diff --git a/mail/server/doctype/mail_cluster/mail_cluster.js b/mail/server/doctype/mail_cluster/mail_cluster.js index dc60132b6..df878deac 100644 --- a/mail/server/doctype/mail_cluster/mail_cluster.js +++ b/mail/server/doctype/mail_cluster/mail_cluster.js @@ -20,17 +20,17 @@ frappe.ui.form.on('Mail Cluster', { }), frm.set_query('blob_store', () => ({ filters: { - type: ['in', ['RocksDb', 'Default', 'Sharded']], + type: ['in', ['Default', 'RocksDb', 'S3', 'Azure', 'FileSystem']], }, })), frm.set_query('search_store', () => ({ filters: { - type: ['in', ['RocksDb', 'Default']], + type: ['in', ['Default', 'RocksDb']], }, })), frm.set_query('in_memory_store', () => ({ filters: { - type: ['in', ['RocksDb', 'Default']], + type: ['in', ['Default', 'RocksDb']], }, })), ) diff --git a/mail/server/doctype/mail_cluster/mail_cluster.py b/mail/server/doctype/mail_cluster/mail_cluster.py index d72a328d2..6348d3d01 100644 --- a/mail/server/doctype/mail_cluster/mail_cluster.py +++ b/mail/server/doctype/mail_cluster/mail_cluster.py @@ -18,9 +18,9 @@ ALLOWED_STORE_TYPES = { "data_store": ["RocksDb", "Sqlite", "FoundationDb", "PostgreSql", "MySql"], - "blob_store": ["RocksDb", "Default", "Sharded"], - "search_store": ["RocksDb", "Default"], - "in_memory_store": ["RocksDb", "Default"], + "blob_store": ["Default", "RocksDb", "S3", "Azure", "FileSystem"], + "search_store": ["Default", "RocksDb"], + "in_memory_store": ["Default", "RocksDb"], } diff --git a/mail/server/doctype/mail_cluster_store/mail_cluster_store.js b/mail/server/doctype/mail_cluster_store/mail_cluster_store.js index c9131f45f..4a2c3c56b 100644 --- a/mail/server/doctype/mail_cluster_store/mail_cluster_store.js +++ b/mail/server/doctype/mail_cluster_store/mail_cluster_store.js @@ -13,39 +13,33 @@ const STORE_PRESET = { pool_workers: 1, pool_max_connections: 10, }, - FoundationDb: { - cluster_file: null, - datacenter_id: null, - machine_id: null, - transaction_retry_delay: null, - transaction_retry_limit: null, - transaction_timeout: null, - }, + FoundationDb: {}, PostgreSql: { timeout: '15s', - use_tls: 0, - allow_invalid_certs: 0, pool_max_connections: 10, pool_recycling_method: 'fast', - host: null, port: 5432, database: 'frappe', - auth_username: null, - auth_secret: null, - options: null, }, MySql: { timeout: '15s', - max_allowed_packet: 0, - use_tls: 0, - allow_invalid_certs: 0, pool_min_connections: 5, pool_max_connections: 10, - host: null, port: 3306, database: 'frappe', - auth_username: null, - auth_secret: null, + }, + S3: { + timeout: '30s', + max_retries: 3, + verify_after_write: 1, + }, + Azure: { + timeout: '30s', + max_retries: 3, + }, + FileSystem: { + path: '/etc/stalwart/blob', + depth: 2, }, } diff --git a/mail/server/doctype/mail_cluster_store/mail_cluster_store.json b/mail/server/doctype/mail_cluster_store/mail_cluster_store.json index dc42d0ca2..dd15ee46e 100644 --- a/mail/server/doctype/mail_cluster_store/mail_cluster_store.json +++ b/mail/server/doctype/mail_cluster_store/mail_cluster_store.json @@ -10,6 +10,7 @@ "description", "column_break_yhyn", "path", + "depth", "blob_size", "buffer_size", "section_break_plrk", @@ -17,6 +18,7 @@ "max_allowed_packet", "use_tls", "allow_invalid_certs", + "verify_after_write", "column_break_djvr", "pool_workers", "pool_min_connections", @@ -37,7 +39,21 @@ "column_break_grwu", "auth_username", "auth_secret", - "options" + "options", + "section_break_uxnr", + "region", + "bucket", + "profile", + "max_retries", + "key_prefix", + "storage_account", + "container", + "column_break_ncyb", + "access_key", + "secret_key", + "security_token", + "session_token", + "sas_token" ], "fields": [ { @@ -88,16 +104,16 @@ "in_list_view": 1, "in_standard_filter": 1, "label": "Type", - "options": "\nRocksDb\nSqlite\nFoundationDb\nPostgreSql\nMySql\nDefault\nSharded", + "options": "\nRocksDb\nSqlite\nFoundationDb\nPostgreSql\nMySql\nDefault\nS3\nAzure\nFileSystem", "reqd": 1 }, { - "depends_on": "eval: [\"RocksDb\", \"Sqlite\"].includes(doc.type)", + "depends_on": "eval: [\"RocksDb\", \"Sqlite\", \"FileSystem\"].includes(doc.type)", "description": "Path to the data directory", "fieldname": "path", "fieldtype": "Data", "label": "Path", - "mandatory_depends_on": "eval: [\"RocksDb\", \"Sqlite\"].includes(doc.type)" + "mandatory_depends_on": "eval: [\"RocksDb\", \"Sqlite\", \"FileSystem\"].includes(doc.type)" }, { "default": "16834", @@ -119,12 +135,12 @@ }, { "default": "15s", - "depends_on": "eval: [\"PostgreSql\", \"MySql\"].includes(doc.type)", - "description": "Connection timeout to the database.", + "depends_on": "eval: [\"PostgreSql\", \"MySql\", \"S3\", \"Azure\"].includes(doc.type)", + "description": "Connection timeout to the store.", "fieldname": "timeout", "fieldtype": "Data", "label": "Timeout", - "mandatory_depends_on": "eval: [\"PostgreSql\", \"MySql\"].includes(doc.type)" + "mandatory_depends_on": "eval: [\"PostgreSql\", \"MySql\", \"S3\", \"Azure\"].includes(doc.type)" }, { "depends_on": "eval: [\"MySql\"].includes(doc.type)", @@ -144,7 +160,7 @@ }, { "default": "0", - "depends_on": "eval: [\"PostgreSql\", \"MySql\"].includes(doc.type)", + "depends_on": "eval: [\"PostgreSql\", \"MySql\", \"S3\"].includes(doc.type)", "description": "Allow invalid TLS certificates when connecting to the store.", "fieldname": "allow_invalid_certs", "fieldtype": "Check", @@ -278,12 +294,132 @@ "fieldname": "options", "fieldtype": "Data", "label": "Options" + }, + { + "fieldname": "section_break_uxnr", + "fieldtype": "Section Break" + }, + { + "depends_on": "eval: [\"S3\"].includes(doc.type)", + "description": "The S3 region where the bucket resides.", + "fieldname": "region", + "fieldtype": "Data", + "label": "Region", + "mandatory_depends_on": "eval: [\"S3\"].includes(doc.type)" + }, + { + "fieldname": "column_break_ncyb", + "fieldtype": "Column Break" + }, + { + "default": "1", + "depends_on": "eval: [\"S3\"].includes(doc.type)", + "description": "After each successful write, verify the object is readable on the backend. Defends against the rare case where an S3-compatible backend returns success but does not actually persist the data. Adds one extra request per write.", + "fieldname": "verify_after_write", + "fieldtype": "Check", + "label": "Verify After Write" + }, + { + "depends_on": "eval: [\"S3\"].includes(doc.type)", + "description": "The S3 bucket where blobs (e-mail messages, Sieve scripts, etc.) will be stored.", + "fieldname": "bucket", + "fieldtype": "Data", + "label": "Bucket", + "mandatory_depends_on": "eval: [\"S3\"].includes(doc.type)" + }, + { + "depends_on": "eval: [\"S3\"].includes(doc.type)", + "description": "Used when retrieving credentials from a shared credentials file. If specified, the server will use the access key ID, secret access key, and session token (if available) associated with the given profile.", + "fieldname": "profile", + "fieldtype": "Data", + "label": "Profile" + }, + { + "default": "3", + "depends_on": "eval: [\"S3\", \"Azure\"].includes(doc.type)", + "description": "The maximum number of times to retry failed requests. Set to 0 to disable retries.", + "fieldname": "max_retries", + "fieldtype": "Int", + "label": "Max Retries", + "mandatory_depends_on": "eval: [\"S3\", \"Azure\"].includes(doc.type)" + }, + { + "depends_on": "eval: [\"S3\", \"Azure\"].includes(doc.type)", + "description": "A prefix that will be added to the keys of all objects stored in the blob store.", + "fieldname": "key_prefix", + "fieldtype": "Data", + "label": "Key Prefix" + }, + { + "depends_on": "eval: [\"S3\", \"Azure\"].includes(doc.type)", + "description": "Identifies the store account.", + "fieldname": "access_key", + "fieldtype": "Data", + "label": "Access Key", + "mandatory_depends_on": "eval: [\"S3\", \"Azure\"].includes(doc.type)" + }, + { + "depends_on": "eval: [\"S3\"].includes(doc.type)", + "description": "The secret key for the S3 account.", + "fieldname": "secret_key", + "fieldtype": "Password", + "label": "Secret Key", + "mandatory_depends_on": "eval: [\"S3\"].includes(doc.type)" + }, + { + "depends_on": "eval: [\"S3\"].includes(doc.type)", + "description": "Security token for temporary credentials.", + "fieldname": "security_token", + "fieldtype": "Password", + "label": "Security Token", + "mandatory_depends_on": "eval: [\"S3\"].includes(doc.type)" + }, + { + "depends_on": "eval: [\"S3\"].includes(doc.type)", + "description": "Temporary session token for the S3 account.", + "fieldname": "session_token", + "fieldtype": "Password", + "label": "Session Token", + "mandatory_depends_on": "eval: [\"S3\"].includes(doc.type)" + }, + { + "depends_on": "eval: [\"Azure\"].includes(doc.type)", + "description": "SAS Token, when not using accessKey based authentication.", + "fieldname": "sas_token", + "fieldtype": "Password", + "label": "SAS Token", + "mandatory_depends_on": "eval: [\"Azure\"].includes(doc.type)" + }, + { + "depends_on": "eval: [\"Azure\"].includes(doc.type)", + "description": "The Azure Storage Account where blobs (e-mail messages, Sieve scripts, etc.) will be stored.", + "fieldname": "storage_account", + "fieldtype": "Data", + "label": "Storage Account", + "mandatory_depends_on": "eval: [\"Azure\"].includes(doc.type)" + }, + { + "depends_on": "eval: [\"Azure\"].includes(doc.type)", + "description": "The name of the container in the Storage Account.", + "fieldname": "container", + "fieldtype": "Data", + "label": "Container", + "mandatory_depends_on": "eval: [\"Azure\"].includes(doc.type)" + }, + { + "default": "2", + "depends_on": "eval: [\"FileSystem\"].includes(doc.type)", + "description": "Maximum depth of nested directories.", + "fieldname": "depth", + "fieldtype": "Int", + "label": "Depth", + "mandatory_depends_on": "eval: [\"FileSystem\"].includes(doc.type)" } ], "grid_page_length": 50, "index_web_pages_for_search": 1, "links": [], - "modified": "2026-05-11 10:45:44.453116", + "modified": "2026-05-11 11:54:05.920080", "modified_by": "Administrator", "module": "Server", "name": "Mail Cluster Store", diff --git a/mail/server/doctype/mail_cluster_store/mail_cluster_store.py b/mail/server/doctype/mail_cluster_store/mail_cluster_store.py index a19d712f0..38b227767 100644 --- a/mail/server/doctype/mail_cluster_store/mail_cluster_store.py +++ b/mail/server/doctype/mail_cluster_store/mail_cluster_store.py @@ -3,6 +3,8 @@ from uuid import uuid7 +import frappe +from frappe import _ from frappe.model.document import Document from frappe.utils import cint @@ -54,9 +56,9 @@ def config(self) -> dict: elif self.type == "PostgreSql": config.update( { - "timeout": cint(self.timeout), - "useTls": cint(self.use_tls), - "allowInvalidCerts": cint(self.allow_invalid_certs), + "timeout": self.timeout, + "useTls": bool(self.use_tls), + "allowInvalidCerts": bool(self.allow_invalid_certs), "poolMaxConnections": cint(self.pool_max_connections), "poolRecyclingMethod": self.pool_recycling_method, "host": self.host, @@ -71,9 +73,9 @@ def config(self) -> dict: elif self.type == "MySql": config.update( { - "timeout": cint(self.timeout), - "useTls": cint(self.use_tls), - "allowInvalidCerts": cint(self.allow_invalid_certs), + "timeout": self.timeout, + "useTls": bool(self.use_tls), + "allowInvalidCerts": bool(self.allow_invalid_certs), "maxAllowedPacket": cint(self.max_allowed_packet), "poolMaxConnections": cint(self.pool_max_connections), "poolMinConnections": cint(self.pool_min_connections), @@ -85,6 +87,45 @@ def config(self) -> dict: } ) + elif self.type == "S3": + config.update( + { + "region": self.region, + "bucket": self.bucket, + "accessKey": self.access_key, + "secretKey": self.get_password("secret_key") if self.secret_key else None, + "securityToken": self.get_password("security_token") if self.security_token else None, + "sessionToken": self.get_password("session_token") if self.session_token else None, + "profile": self.profile, + "timeout": self.timeout, + "maxRetries": cint(self.max_retries), + "keyPrefix": self.key_prefix, + "allowInvalidCerts": bool(self.allow_invalid_certs), + "verifyAfterWrite": bool(self.verify_after_write), + } + ) + + elif self.type == "Azure": + config.update( + { + "storageAccount": self.storage_account, + "container": self.container, + "accessKey": self.get_password("access_key") if self.access_key else None, + "sasToken": self.get_password("sas_token") if self.sas_token else None, + "timeout": self.timeout, + "maxRetries": cint(self.max_retries), + "keyPrefix": self.key_prefix, + } + ) + + elif self.type == "FileSystem": + config.update( + { + "path": self.path, + "depth": cint(self.depth), + } + ) + return config def validate(self) -> None: @@ -92,3 +133,12 @@ def validate(self) -> None: if not self.description: self.description = self.type + + self.validate_singleton_default() + + def validate_singleton_default(self) -> None: + """Validates that only one Default store exists.""" + + if self.type in ["Default"]: + if frappe.db.exists("Mail Cluster Store", {"type": self.type, "name": ["!=", self.name]}): + frappe.throw(_("Only one {0} store is allowed.").format(self.type)) From 99151635632090fd2d89c68d2206fdbae39c1ca6 Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Mon, 11 May 2026 12:46:47 +0530 Subject: [PATCH 15/55] feat: template for Search stores --- .../doctype/mail_cluster/mail_cluster.js | 2 +- .../doctype/mail_cluster/mail_cluster.py | 2 +- .../mail_cluster_store/mail_cluster_store.js | 13 +++ .../mail_cluster_store.json | 105 ++++++++++++++++-- .../mail_cluster_store/mail_cluster_store.py | 33 +++++- .../mail_cluster_store_http_auth/__init__.py | 0 .../mail_cluster_store_http_auth.js | 8 ++ .../mail_cluster_store_http_auth.json | 99 +++++++++++++++++ .../mail_cluster_store_http_auth.py | 34 ++++++ .../test_mail_cluster_store_http_auth.py | 20 ++++ 10 files changed, 303 insertions(+), 13 deletions(-) create mode 100644 mail/server/doctype/mail_cluster_store_http_auth/__init__.py create mode 100644 mail/server/doctype/mail_cluster_store_http_auth/mail_cluster_store_http_auth.js create mode 100644 mail/server/doctype/mail_cluster_store_http_auth/mail_cluster_store_http_auth.json create mode 100644 mail/server/doctype/mail_cluster_store_http_auth/mail_cluster_store_http_auth.py create mode 100644 mail/server/doctype/mail_cluster_store_http_auth/test_mail_cluster_store_http_auth.py diff --git a/mail/server/doctype/mail_cluster/mail_cluster.js b/mail/server/doctype/mail_cluster/mail_cluster.js index df878deac..ff4f823ed 100644 --- a/mail/server/doctype/mail_cluster/mail_cluster.js +++ b/mail/server/doctype/mail_cluster/mail_cluster.js @@ -25,7 +25,7 @@ frappe.ui.form.on('Mail Cluster', { })), frm.set_query('search_store', () => ({ filters: { - type: ['in', ['Default', 'RocksDb']], + type: ['in', ['Default', 'RocksDb', 'ElasticSearch', 'Meilisearch']], }, })), frm.set_query('in_memory_store', () => ({ diff --git a/mail/server/doctype/mail_cluster/mail_cluster.py b/mail/server/doctype/mail_cluster/mail_cluster.py index 6348d3d01..60e2283c9 100644 --- a/mail/server/doctype/mail_cluster/mail_cluster.py +++ b/mail/server/doctype/mail_cluster/mail_cluster.py @@ -19,7 +19,7 @@ ALLOWED_STORE_TYPES = { "data_store": ["RocksDb", "Sqlite", "FoundationDb", "PostgreSql", "MySql"], "blob_store": ["Default", "RocksDb", "S3", "Azure", "FileSystem"], - "search_store": ["Default", "RocksDb"], + "search_store": ["Default", "RocksDb", "ElasticSearch", "Meilisearch"], "in_memory_store": ["Default", "RocksDb"], } diff --git a/mail/server/doctype/mail_cluster_store/mail_cluster_store.js b/mail/server/doctype/mail_cluster_store/mail_cluster_store.js index 4a2c3c56b..4952e099a 100644 --- a/mail/server/doctype/mail_cluster_store/mail_cluster_store.js +++ b/mail/server/doctype/mail_cluster_store/mail_cluster_store.js @@ -41,6 +41,19 @@ const STORE_PRESET = { path: '/etc/stalwart/blob', depth: 2, }, + ElasticSearch: { + timeout: '30s', + num_replicas: 0, + num_shards: 3, + http_headers: '{}', + }, + Meilisearch: { + timeout: '30s', + poll_interval: '500ms', + max_retries: 120, + fail_on_timeout: 1, + http_headers: '{}', + }, } frappe.ui.form.on('Mail Cluster Store', { diff --git a/mail/server/doctype/mail_cluster_store/mail_cluster_store.json b/mail/server/doctype/mail_cluster_store/mail_cluster_store.json index dd15ee46e..08b96614c 100644 --- a/mail/server/doctype/mail_cluster_store/mail_cluster_store.json +++ b/mail/server/doctype/mail_cluster_store/mail_cluster_store.json @@ -19,6 +19,7 @@ "use_tls", "allow_invalid_certs", "verify_after_write", + "fail_on_timeout", "column_break_djvr", "pool_workers", "pool_min_connections", @@ -53,7 +54,16 @@ "secret_key", "security_token", "session_token", - "sas_token" + "sas_token", + "section_break_wmqb", + "url", + "poll_interval", + "num_replicas", + "num_shards", + "include_source", + "column_break_sefr", + "http_auth", + "http_headers" ], "fields": [ { @@ -104,8 +114,10 @@ "in_list_view": 1, "in_standard_filter": 1, "label": "Type", - "options": "\nRocksDb\nSqlite\nFoundationDb\nPostgreSql\nMySql\nDefault\nS3\nAzure\nFileSystem", - "reqd": 1 + "options": "\nRocksDb\nSqlite\nFoundationDb\nPostgreSql\nMySql\nDefault\nS3\nAzure\nFileSystem\nElasticSearch\nMeilisearch", + "reqd": 1, + "search_index": 1, + "set_only_once": 1 }, { "depends_on": "eval: [\"RocksDb\", \"Sqlite\", \"FileSystem\"].includes(doc.type)", @@ -135,12 +147,12 @@ }, { "default": "15s", - "depends_on": "eval: [\"PostgreSql\", \"MySql\", \"S3\", \"Azure\"].includes(doc.type)", + "depends_on": "eval: [\"PostgreSql\", \"MySql\", \"S3\", \"Azure\", \"ElasticSearch\", \"Meilisearch\"].includes(doc.type)", "description": "Connection timeout to the store.", "fieldname": "timeout", "fieldtype": "Data", "label": "Timeout", - "mandatory_depends_on": "eval: [\"PostgreSql\", \"MySql\", \"S3\", \"Azure\"].includes(doc.type)" + "mandatory_depends_on": "eval: [\"PostgreSql\", \"MySql\", \"S3\", \"Azure\", \"ElasticSearch\", \"Meilisearch\"].includes(doc.type)" }, { "depends_on": "eval: [\"MySql\"].includes(doc.type)", @@ -160,7 +172,7 @@ }, { "default": "0", - "depends_on": "eval: [\"PostgreSql\", \"MySql\", \"S3\"].includes(doc.type)", + "depends_on": "eval: [\"PostgreSql\", \"MySql\", \"S3\", \"ElasticSearch\", \"Meilisearch\"].includes(doc.type)", "description": "Allow invalid TLS certificates when connecting to the store.", "fieldname": "allow_invalid_certs", "fieldtype": "Check", @@ -336,12 +348,12 @@ }, { "default": "3", - "depends_on": "eval: [\"S3\", \"Azure\"].includes(doc.type)", + "depends_on": "eval: [\"S3\", \"Azure\", \"Meilisearch\"].includes(doc.type)", "description": "The maximum number of times to retry failed requests. Set to 0 to disable retries.", "fieldname": "max_retries", "fieldtype": "Int", "label": "Max Retries", - "mandatory_depends_on": "eval: [\"S3\", \"Azure\"].includes(doc.type)" + "mandatory_depends_on": "eval: [\"S3\", \"Azure\", \"Meilisearch\"].includes(doc.type)" }, { "depends_on": "eval: [\"S3\", \"Azure\"].includes(doc.type)", @@ -414,12 +426,87 @@ "fieldtype": "Int", "label": "Depth", "mandatory_depends_on": "eval: [\"FileSystem\"].includes(doc.type)" + }, + { + "fieldname": "section_break_wmqb", + "fieldtype": "Section Break" + }, + { + "depends_on": "eval: [\"ElasticSearch\", \"Meilisearch\"].includes(doc.type)", + "description": "URL of the store.", + "fieldname": "url", + "fieldtype": "Data", + "label": "URL", + "mandatory_depends_on": "eval: [\"ElasticSearch\", \"Meilisearch\"].includes(doc.type)" + }, + { + "default": "0", + "depends_on": "eval: [\"ElasticSearch\"].includes(doc.type)", + "description": "Whether to index the full source document.", + "fieldname": "include_source", + "fieldtype": "Check", + "label": "Include Source" + }, + { + "fieldname": "column_break_sefr", + "fieldtype": "Column Break" + }, + { + "depends_on": "eval: [\"ElasticSearch\", \"Meilisearch\"].includes(doc.type)", + "description": "The HTTP authentication to use.", + "fieldname": "http_auth", + "fieldtype": "Link", + "label": "HTTP Auth", + "mandatory_depends_on": "eval: [\"ElasticSearch\", \"Meilisearch\"].includes(doc.type)", + "options": "Mail Cluster Store HTTP Auth" + }, + { + "default": "{}", + "depends_on": "eval: [\"ElasticSearch\", \"Meilisearch\"].includes(doc.type)", + "description": "Additional headers to include in HTTP requests.", + "fieldname": "http_headers", + "fieldtype": "JSON", + "label": "HTTP Headers" + }, + { + "default": "0", + "depends_on": "eval: [\"ElasticSearch\"].includes(doc.type)", + "description": "Number of replicas for the index", + "fieldname": "num_replicas", + "fieldtype": "Int", + "label": "No. of Replicas", + "mandatory_depends_on": "eval: [\"ElasticSearch\"].includes(doc.type)" + }, + { + "default": "3", + "depends_on": "eval: [\"ElasticSearch\"].includes(doc.type)", + "description": "Number of shards for the index.", + "fieldname": "num_shards", + "fieldtype": "Int", + "label": "No. of Shards", + "mandatory_depends_on": "eval: [\"ElasticSearch\"].includes(doc.type)" + }, + { + "depends_on": "eval: [\"Meilisearch\"].includes(doc.type)", + "description": "Interval between polling for task status.", + "fieldname": "poll_interval", + "fieldtype": "Data", + "label": "Poll Interval", + "mandatory_depends_on": "eval: [\"Meilisearch\"].includes(doc.type)" + }, + { + "default": "1", + "depends_on": "eval: [\"Meilisearch\"].includes(doc.type)", + "description": "Whether to fail the operation if the task does not complete within the polling retries.", + "fieldname": "fail_on_timeout", + "fieldtype": "Check", + "label": "Fail On Timeout" } ], "grid_page_length": 50, "index_web_pages_for_search": 1, "links": [], - "modified": "2026-05-11 11:54:05.920080", + "modified": "2026-05-11 12:42:04.254981", "modified_by": "Administrator", "module": "Server", "name": "Mail Cluster Store", diff --git a/mail/server/doctype/mail_cluster_store/mail_cluster_store.py b/mail/server/doctype/mail_cluster_store/mail_cluster_store.py index 38b227767..2608892a4 100644 --- a/mail/server/doctype/mail_cluster_store/mail_cluster_store.py +++ b/mail/server/doctype/mail_cluster_store/mail_cluster_store.py @@ -1,6 +1,7 @@ # Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and contributors # For license information, please see license.txt +import json from uuid import uuid7 import frappe @@ -126,11 +127,39 @@ def config(self) -> dict: } ) + elif self.type == "ElasticSearch": + http_auth = frappe.get_doc("Mail Cluster Store HTTP Auth", self.http_auth) + config.update( + { + "url": self.url, + "numReplicas": cint(self.num_replicas), + "numShards": cint(self.num_shards), + "includeSource": bool(self.include_source), + "timeout": cint(self.timeout), + "allowInvalidCerts": bool(self.allow_invalid_certs), + "httpAuth": http_auth.config, + "httpHeaders": json.loads(self.http_headers) if self.http_headers else {}, + } + ) + + elif self.type == "Meilisearch": + http_auth = frappe.get_doc("Mail Cluster Store HTTP Auth", self.http_auth) + config.update( + { + "url": self.url, + "pollInterval": self.pool_interval, + "maxRetries": cint(self.max_retries), + "failOnTimeout": bool(self.fail_on_timeout), + "timeout": cint(self.timeout), + "allowInvalidCerts": bool(self.allow_invalid_certs), + "httpAuth": http_auth.config, + "httpHeaders": json.loads(self.http_headers) if self.http_headers else {}, + } + ) + return config def validate(self) -> None: - """Validates the cluster store configuration.""" - if not self.description: self.description = self.type diff --git a/mail/server/doctype/mail_cluster_store_http_auth/__init__.py b/mail/server/doctype/mail_cluster_store_http_auth/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/mail/server/doctype/mail_cluster_store_http_auth/mail_cluster_store_http_auth.js b/mail/server/doctype/mail_cluster_store_http_auth/mail_cluster_store_http_auth.js new file mode 100644 index 000000000..304363cb4 --- /dev/null +++ b/mail/server/doctype/mail_cluster_store_http_auth/mail_cluster_store_http_auth.js @@ -0,0 +1,8 @@ +// Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +// frappe.ui.form.on("Mail Cluster Store HTTP Auth", { +// refresh(frm) { + +// }, +// }); diff --git a/mail/server/doctype/mail_cluster_store_http_auth/mail_cluster_store_http_auth.json b/mail/server/doctype/mail_cluster_store_http_auth/mail_cluster_store_http_auth.json new file mode 100644 index 000000000..9c777551f --- /dev/null +++ b/mail/server/doctype/mail_cluster_store_http_auth/mail_cluster_store_http_auth.json @@ -0,0 +1,99 @@ +{ + "actions": [], + "allow_bulk_edit": 1, + "creation": "2026-05-11 12:10:41.078576", + "doctype": "DocType", + "engine": "InnoDB", + "field_order": [ + "section_break_rkcx", + "type", + "username", + "secret", + "bearer_token", + "column_break_okoa", + "description" + ], + "fields": [ + { + "fieldname": "section_break_rkcx", + "fieldtype": "Section Break" + }, + { + "description": "Description for the HTTP auth.", + "fieldname": "description", + "fieldtype": "Small Text", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Description" + }, + { + "description": "Type of the HTTP authentication method.", + "fieldname": "type", + "fieldtype": "Select", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Type", + "options": "\nUnauthenticated\nBasic\nBearer", + "reqd": 1, + "search_index": 1 + }, + { + "fieldname": "column_break_okoa", + "fieldtype": "Column Break" + }, + { + "depends_on": "eval: [\"Basic\"].includes(doc.type)", + "description": "Username for HTTP Basic Authentication.", + "fieldname": "username", + "fieldtype": "Data", + "label": "Username", + "mandatory_depends_on": "eval: [\"Basic\"].includes(doc.type)" + }, + { + "depends_on": "eval: [\"Basic\"].includes(doc.type)", + "description": "Password or secret value.", + "fieldname": "secret", + "fieldtype": "Password", + "label": "Secret", + "mandatory_depends_on": "eval: [\"Basic\"].includes(doc.type)" + }, + { + "depends_on": "eval: [\"Bearer\"].includes(doc.type)", + "description": "Bearer token for HTTP Bearer Authentication.", + "fieldname": "bearer_token", + "fieldtype": "Password", + "label": "Bearer Token", + "mandatory_depends_on": "eval: [\"Bearer\"].includes(doc.type)" + } + ], + "grid_page_length": 50, + "index_web_pages_for_search": 1, + "links": [], + "modified": "2026-05-11 12:26:45.666253", + "modified_by": "Administrator", + "module": "Server", + "name": "Mail Cluster Store HTTP Auth", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "row_format": "Dynamic", + "rows_threshold_for_grid_search": 20, + "show_title_field_in_link": 1, + "sort_field": "creation", + "sort_order": "DESC", + "states": [], + "title_field": "description", + "track_changes": 1 +} diff --git a/mail/server/doctype/mail_cluster_store_http_auth/mail_cluster_store_http_auth.py b/mail/server/doctype/mail_cluster_store_http_auth/mail_cluster_store_http_auth.py new file mode 100644 index 000000000..96f970f63 --- /dev/null +++ b/mail/server/doctype/mail_cluster_store_http_auth/mail_cluster_store_http_auth.py @@ -0,0 +1,34 @@ +# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from uuid import uuid7 + +from frappe.model.document import Document + + +class MailClusterStoreHTTPAuth(Document): + def autoname(self) -> None: + self.name = str(uuid7()) + + @property + def config(self) -> dict: + """Returns the configuration for the HTTP Auth cluster store.""" + + config = {} + + if self.type == "Basic": + config.update( + { + "username": self.username, + "secret": self.get_password("secret") if self.secret else None, + } + ) + + elif self.type == "Bearer": + config["bearerToken"] = self.get_password("bearer_token") if self.bearer_token else None + + return config + + def validate(self) -> None: + if not self.description: + self.description = self.type diff --git a/mail/server/doctype/mail_cluster_store_http_auth/test_mail_cluster_store_http_auth.py b/mail/server/doctype/mail_cluster_store_http_auth/test_mail_cluster_store_http_auth.py new file mode 100644 index 000000000..418494303 --- /dev/null +++ b/mail/server/doctype/mail_cluster_store_http_auth/test_mail_cluster_store_http_auth.py @@ -0,0 +1,20 @@ +# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +# import frappe +from frappe.tests import IntegrationTestCase + +# On IntegrationTestCase, the doctype test records and all +# link-field test record dependencies are recursively loaded +# Use these module variables to add/remove to/from that list +EXTRA_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"] +IGNORE_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"] + + +class IntegrationTestMailClusterStoreHTTPAuth(IntegrationTestCase): + """ + Integration tests for MailClusterStoreHTTPAuth. + Use this class for testing interactions between multiple components. + """ + + pass From 0afe28d733139a3e6649e57218f65d5ede324976 Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Mon, 11 May 2026 13:22:15 +0530 Subject: [PATCH 16/55] feat: template for In-Memory stores --- .../doctype/mail_cluster/mail_cluster.js | 2 +- .../doctype/mail_cluster/mail_cluster.py | 2 +- .../mail_cluster_store/mail_cluster_store.js | 16 ++ .../mail_cluster_store.json | 143 +++++++++++++----- .../mail_cluster_store/mail_cluster_store.py | 31 ++++ 5 files changed, 158 insertions(+), 36 deletions(-) diff --git a/mail/server/doctype/mail_cluster/mail_cluster.js b/mail/server/doctype/mail_cluster/mail_cluster.js index ff4f823ed..ad804635b 100644 --- a/mail/server/doctype/mail_cluster/mail_cluster.js +++ b/mail/server/doctype/mail_cluster/mail_cluster.js @@ -30,7 +30,7 @@ frappe.ui.form.on('Mail Cluster', { })), frm.set_query('in_memory_store', () => ({ filters: { - type: ['in', ['Default', 'RocksDb']], + type: ['in', ['Default', 'RocksDb', 'Redis', 'RedisCluster']], }, })), ) diff --git a/mail/server/doctype/mail_cluster/mail_cluster.py b/mail/server/doctype/mail_cluster/mail_cluster.py index 60e2283c9..0c71b7052 100644 --- a/mail/server/doctype/mail_cluster/mail_cluster.py +++ b/mail/server/doctype/mail_cluster/mail_cluster.py @@ -20,7 +20,7 @@ "data_store": ["RocksDb", "Sqlite", "FoundationDb", "PostgreSql", "MySql"], "blob_store": ["Default", "RocksDb", "S3", "Azure", "FileSystem"], "search_store": ["Default", "RocksDb", "ElasticSearch", "Meilisearch"], - "in_memory_store": ["Default", "RocksDb"], + "in_memory_store": ["Default", "RocksDb", "Redis", "RedisCluster"], } diff --git a/mail/server/doctype/mail_cluster_store/mail_cluster_store.js b/mail/server/doctype/mail_cluster_store/mail_cluster_store.js index 4952e099a..d4a7561a6 100644 --- a/mail/server/doctype/mail_cluster_store/mail_cluster_store.js +++ b/mail/server/doctype/mail_cluster_store/mail_cluster_store.js @@ -54,6 +54,22 @@ const STORE_PRESET = { fail_on_timeout: 1, http_headers: '{}', }, + Redis: { + timeout: '10s', + pool_max_connections: 10, + pool_timeout_create: '30s', + pool_timeout_wait: '30s', + pool_timeout_recycle: '30s', + }, + RedisCluster: { + timeout: '10s', + urls: '["redis://127.0.0.1"]', + read_from_replicas: 1, + pool_max_connections: 10, + pool_timeout_create: '30s', + pool_timeout_wait: '30s', + pool_timeout_recycle: '30s', + }, } frappe.ui.form.on('Mail Cluster Store', { diff --git a/mail/server/doctype/mail_cluster_store/mail_cluster_store.json b/mail/server/doctype/mail_cluster_store/mail_cluster_store.json index 08b96614c..c71a386ab 100644 --- a/mail/server/doctype/mail_cluster_store/mail_cluster_store.json +++ b/mail/server/doctype/mail_cluster_store/mail_cluster_store.json @@ -7,25 +7,28 @@ "field_order": [ "store_tab", "type", + "column_break_ovdu", "description", - "column_break_yhyn", + "section_break_plrk", "path", "depth", + "column_break_yhyn", "blob_size", "buffer_size", - "section_break_plrk", + "section_break_bpyx", "timeout", - "max_allowed_packet", "use_tls", "allow_invalid_certs", - "verify_after_write", - "fail_on_timeout", - "column_break_djvr", + "max_allowed_packet", "pool_workers", + "column_break_djvr", "pool_min_connections", "pool_max_connections", "pool_recycling_method", - "section_break_bpyx", + "pool_timeout_create", + "pool_timeout_wait", + "pool_timeout_recycle", + "section_break_ntwb", "cluster_file", "datacenter_id", "machine_id", @@ -33,23 +36,22 @@ "transaction_retry_delay", "transaction_retry_limit", "transaction_timeout", - "section_break_ntwb", + "section_break_uxnr", "host", "port", "database", - "column_break_grwu", - "auth_username", - "auth_secret", - "options", - "section_break_uxnr", "region", "bucket", "profile", - "max_retries", "key_prefix", + "max_retries", + "verify_after_write", + "column_break_grwu", + "auth_username", + "auth_secret", + "options", "storage_account", "container", - "column_break_ncyb", "access_key", "secret_key", "security_token", @@ -57,13 +59,19 @@ "sas_token", "section_break_wmqb", "url", - "poll_interval", + "urls", + "protocol_version", "num_replicas", "num_shards", "include_source", - "column_break_sefr", + "read_from_replicas", + "column_break_ncyb", + "min_retry_wait", + "max_retry_wait", "http_auth", - "http_headers" + "http_headers", + "poll_interval", + "fail_on_timeout" ], "fields": [ { @@ -114,7 +122,7 @@ "in_list_view": 1, "in_standard_filter": 1, "label": "Type", - "options": "\nRocksDb\nSqlite\nFoundationDb\nPostgreSql\nMySql\nDefault\nS3\nAzure\nFileSystem\nElasticSearch\nMeilisearch", + "options": "\nRocksDb\nSqlite\nFoundationDb\nPostgreSql\nMySql\nDefault\nS3\nAzure\nFileSystem\nElasticSearch\nMeilisearch\nRedis\nRedisCluster", "reqd": 1, "search_index": 1, "set_only_once": 1 @@ -147,12 +155,12 @@ }, { "default": "15s", - "depends_on": "eval: [\"PostgreSql\", \"MySql\", \"S3\", \"Azure\", \"ElasticSearch\", \"Meilisearch\"].includes(doc.type)", + "depends_on": "eval: [\"PostgreSql\", \"MySql\", \"S3\", \"Azure\", \"ElasticSearch\", \"Meilisearch\", \"Redis\", \"RedisCluster\"].includes(doc.type)", "description": "Connection timeout to the store.", "fieldname": "timeout", "fieldtype": "Data", "label": "Timeout", - "mandatory_depends_on": "eval: [\"PostgreSql\", \"MySql\", \"S3\", \"Azure\", \"ElasticSearch\", \"Meilisearch\"].includes(doc.type)" + "mandatory_depends_on": "eval: [\"PostgreSql\", \"MySql\", \"S3\", \"Azure\", \"ElasticSearch\", \"Meilisearch\", \"Redis\", \"RedisCluster\"].includes(doc.type)" }, { "depends_on": "eval: [\"MySql\"].includes(doc.type)", @@ -197,12 +205,12 @@ }, { "default": "10", - "depends_on": "eval: [\"Sqlite\", \"PostgreSql\", \"MySql\"].includes(doc.type)", + "depends_on": "eval: [\"Sqlite\", \"PostgreSql\", \"MySql\", \"Redis\", \"RedisCluster\"].includes(doc.type)", "description": "Maximum number of connections to the store.", "fieldname": "pool_max_connections", "fieldtype": "Int", "label": "Pool Max Connections", - "mandatory_depends_on": "eval: [\"Sqlite\", \"PostgreSql\", \"MySql\"].includes(doc.type)" + "mandatory_depends_on": "eval: [\"Sqlite\", \"PostgreSql\", \"MySql\", \"Redis\", \"RedisCluster\"].includes(doc.type)" }, { "default": "fast", @@ -287,14 +295,14 @@ }, { "default": "frappe", - "depends_on": "eval: [\"PostgreSql\", \"MySql\"].includes(doc.type)", + "depends_on": "eval: [\"PostgreSql\", \"MySql\", \"RedisCluster\"].includes(doc.type)", "description": "Username to connect to the store.", "fieldname": "auth_username", "fieldtype": "Data", "label": "Auth Username" }, { - "depends_on": "eval: [\"PostgreSql\", \"MySql\"].includes(doc.type)", + "depends_on": "eval: [\"PostgreSql\", \"MySql\", \"RedisCluster\"].includes(doc.type)", "description": "Password to connect to the store.", "fieldname": "auth_secret", "fieldtype": "Password", @@ -348,12 +356,12 @@ }, { "default": "3", - "depends_on": "eval: [\"S3\", \"Azure\", \"Meilisearch\"].includes(doc.type)", + "depends_on": "eval: [\"S3\", \"Azure\", \"Meilisearch\", \"RedisCluster\"].includes(doc.type)", "description": "The maximum number of times to retry failed requests. Set to 0 to disable retries.", "fieldname": "max_retries", "fieldtype": "Int", "label": "Max Retries", - "mandatory_depends_on": "eval: [\"S3\", \"Azure\", \"Meilisearch\"].includes(doc.type)" + "mandatory_depends_on": "eval: [\"S3\", \"Azure\", \"Meilisearch\", \"RedisCluster\"].includes(doc.type)" }, { "depends_on": "eval: [\"S3\", \"Azure\"].includes(doc.type)", @@ -432,12 +440,12 @@ "fieldtype": "Section Break" }, { - "depends_on": "eval: [\"ElasticSearch\", \"Meilisearch\"].includes(doc.type)", + "depends_on": "eval: [\"ElasticSearch\", \"Meilisearch\", \"Redis\"].includes(doc.type)", "description": "URL of the store.", "fieldname": "url", "fieldtype": "Data", "label": "URL", - "mandatory_depends_on": "eval: [\"ElasticSearch\", \"Meilisearch\"].includes(doc.type)" + "mandatory_depends_on": "eval: [\"ElasticSearch\", \"Meilisearch\", \"Redis\"].includes(doc.type)" }, { "default": "0", @@ -447,10 +455,6 @@ "fieldtype": "Check", "label": "Include Source" }, - { - "fieldname": "column_break_sefr", - "fieldtype": "Column Break" - }, { "depends_on": "eval: [\"ElasticSearch\", \"Meilisearch\"].includes(doc.type)", "description": "The HTTP authentication to use.", @@ -501,12 +505,83 @@ "fieldname": "fail_on_timeout", "fieldtype": "Check", "label": "Fail On Timeout" + }, + { + "depends_on": "eval: [\"Redis\", \"RedisCluster\"].includes(doc.type)", + "description": "Timeout for creating a new connection.", + "fieldname": "pool_timeout_create", + "fieldtype": "Data", + "label": "Pool Timeout Create", + "mandatory_depends_on": "eval: [\"Redis\", \"RedisCluster\"].includes(doc.type)" + }, + { + "depends_on": "eval: [\"Redis\", \"RedisCluster\"].includes(doc.type)", + "description": "Timeout for waiting for a connection from the pool.", + "fieldname": "pool_timeout_wait", + "fieldtype": "Data", + "label": "Pool Timeout Wait", + "mandatory_depends_on": "eval: [\"Redis\", \"RedisCluster\"].includes(doc.type)" + }, + { + "depends_on": "eval: [\"Redis\", \"RedisCluster\"].includes(doc.type)", + "description": "Timeout for recycling a connection.", + "fieldname": "pool_timeout_recycle", + "fieldtype": "Data", + "label": "Pool Timeout Recycle", + "mandatory_depends_on": "eval: [\"Redis\", \"RedisCluster\"].includes(doc.type)" + }, + { + "default": "[]", + "depends_on": "eval: [\"RedisCluster\"].includes(doc.type)", + "description": "URL(s) of the Redis server(s)", + "fieldname": "urls", + "fieldtype": "JSON", + "label": "URLs", + "mandatory_depends_on": "eval: [\"RedisCluster\"].includes(doc.type)" + }, + { + "depends_on": "eval: [\"RedisCluster\"].includes(doc.type)", + "description": "Maximum time to wait between retries.", + "fieldname": "max_retry_wait", + "fieldtype": "Data", + "label": "Max Retry Wait", + "mandatory_depends_on": "eval: [\"RedisCluster\"].includes(doc.type)" + }, + { + "depends_on": "eval: [\"RedisCluster\"].includes(doc.type)", + "description": "Minimum time to wait between retries.", + "fieldname": "min_retry_wait", + "fieldtype": "Data", + "label": "Min Retry Wait", + "mandatory_depends_on": "eval: [\"RedisCluster\"].includes(doc.type)" + }, + { + "default": "1", + "depends_on": "eval: [\"RedisCluster\"].includes(doc.type)", + "description": "Whether to read from replicas.", + "fieldname": "read_from_replicas", + "fieldtype": "Check", + "label": "Read From Replicas" + }, + { + "default": "resp2", + "depends_on": "eval: [\"RedisCluster\"].includes(doc.type)", + "description": "Redis protocol version.", + "fieldname": "protocol_version", + "fieldtype": "Select", + "label": "Protocol Version", + "mandatory_depends_on": "eval: [\"RedisCluster\"].includes(doc.type)", + "options": "\nresp2\nresp3" + }, + { + "fieldname": "column_break_ovdu", + "fieldtype": "Column Break" } ], "grid_page_length": 50, "index_web_pages_for_search": 1, "links": [], - "modified": "2026-05-11 12:42:04.254981", + "modified": "2026-05-11 14:36:51.430174", "modified_by": "Administrator", "module": "Server", "name": "Mail Cluster Store", diff --git a/mail/server/doctype/mail_cluster_store/mail_cluster_store.py b/mail/server/doctype/mail_cluster_store/mail_cluster_store.py index 2608892a4..900e607e9 100644 --- a/mail/server/doctype/mail_cluster_store/mail_cluster_store.py +++ b/mail/server/doctype/mail_cluster_store/mail_cluster_store.py @@ -157,6 +157,37 @@ def config(self) -> dict: } ) + elif self.type == "Redis": + config.update( + { + "url": self.url, + "timeout": cint(self.timeout), + "poolMaxConnections": cint(self.pool_max_connections), + "poolTimeoutCreate": self.pool_timeout_create, + "poolTimeoutWait": self.pool_timeout_wait, + "poolTimeoutRecycle": self.pool_timeout_recycle, + } + ) + + elif self.type == "RedisCluster": + config.update( + { + "urls": json.loads(self.urls) if self.urls else [], + "timeout": cint(self.timeout), + "authUsername": self.auth_username, + "authSecret": self.get_password("auth_secret") if self.auth_secret else None, + "maxRetryWait": self.max_retry_wait, + "minRetryWait": self.min_retry_wait, + "maxRetries": cint(self.max_retries), + "readFromReplicas": bool(self.read_from_replicas), + "protocolVersion": self.protocol_version, + "poolMaxConnections": cint(self.pool_max_connections), + "poolTimeoutCreate": self.pool_timeout_create, + "poolTimeoutWait": self.pool_timeout_wait, + "poolTimeoutRecycle": self.pool_timeout_recycle, + } + ) + return config def validate(self) -> None: From b1f5f6a03461ae8c72e523bacb511615f44fbc4a Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Tue, 12 May 2026 12:14:41 +0530 Subject: [PATCH 17/55] chore: `ignore_links_on_delete` --- mail/hooks.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/mail/hooks.py b/mail/hooks.py index 9c5603871..8b99393a6 100644 --- a/mail/hooks.py +++ b/mail/hooks.py @@ -290,6 +290,8 @@ "Mail Account Request", "Mail Data Exchange", "Mail Domain Request", + "Server Ansible Play", + "Server Deployment", # Client "Mail Exchange", "Mail Queue", From f74f3e38e8f2bc71844c55f6ef324eea4ac7e8f1 Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Tue, 12 May 2026 13:39:46 +0530 Subject: [PATCH 18/55] refactor: remove API Key from Mail Cluster --- .../doctype/mail_cluster/mail_cluster.js | 24 ------------ .../doctype/mail_cluster/mail_cluster.json | 15 +++----- .../doctype/mail_cluster/mail_cluster.py | 37 ------------------- 3 files changed, 6 insertions(+), 70 deletions(-) diff --git a/mail/server/doctype/mail_cluster/mail_cluster.js b/mail/server/doctype/mail_cluster/mail_cluster.js index ad804635b..b7ae1bd86 100644 --- a/mail/server/doctype/mail_cluster/mail_cluster.js +++ b/mail/server/doctype/mail_cluster/mail_cluster.js @@ -46,16 +46,6 @@ frappe.ui.form.on('Mail Cluster', { frm.trigger('show_password') }) } - - if (frm.doc.base_url) { - frm.add_custom_button( - __('Generate API Key'), - () => { - frm.trigger('generate_api_key') - }, - __('Actions'), - ) - } }, show_password(frm) { @@ -71,18 +61,4 @@ frappe.ui.form.on('Mail Cluster', { }, }) }, - - generate_api_key(frm) { - frappe.call({ - doc: frm.doc, - method: 'generate_api_key', - freeze: true, - freeze_message: __('Generating API Key...'), - callback: (r) => { - if (!r.exc) { - frm.refresh() - } - }, - }) - }, }) diff --git a/mail/server/doctype/mail_cluster/mail_cluster.json b/mail/server/doctype/mail_cluster/mail_cluster.json index 2fe8a59e7..bd531ed7d 100644 --- a/mail/server/doctype/mail_cluster/mail_cluster.json +++ b/mail/server/doctype/mail_cluster/mail_cluster.json @@ -11,11 +11,11 @@ "hostname", "default_domain", "authentication_section", + "column_break_eyum", "recovery_admin_user", "recovery_admin_password", "column_break_9jti", "base_url", - "api_key", "networking_section", "ipv4_addresses", "column_break_uhc3", @@ -63,13 +63,6 @@ "fieldtype": "Section Break", "label": "Authentication" }, - { - "description": "Key for authenticating HTTP requests. Generate one via Actions > Generate API Key.", - "fieldname": "api_key", - "fieldtype": "Password", - "label": "API Key", - "no_copy": 1 - }, { "fieldname": "column_break_9jti", "fieldtype": "Column Break" @@ -257,6 +250,10 @@ { "fieldname": "column_break_rxvx", "fieldtype": "Column Break" + }, + { + "fieldname": "column_break_eyum", + "fieldtype": "Column Break" } ], "grid_page_length": 50, @@ -268,7 +265,7 @@ "link_fieldname": "cluster" } ], - "modified": "2026-05-08 12:30:30.414245", + "modified": "2026-05-12 13:37:24.340189", "modified_by": "Administrator", "module": "Server", "name": "Mail Cluster", diff --git a/mail/server/doctype/mail_cluster/mail_cluster.py b/mail/server/doctype/mail_cluster/mail_cluster.py index 0c71b7052..ab57bc652 100644 --- a/mail/server/doctype/mail_cluster/mail_cluster.py +++ b/mail/server/doctype/mail_cluster/mail_cluster.py @@ -1,7 +1,6 @@ # Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors # For license information, please see license.txt -import base64 import io import json @@ -11,9 +10,6 @@ from frappe.model.document import Document from frappe.utils import random_string -from mail.backend import MailBackendAPI, Principal -from mail.jmap.connection import raise_for_status -from mail.utils import generate_secret from mail.utils.dns import get_dns_record ALLOWED_STORE_TYPES = { @@ -193,39 +189,6 @@ def get_recovery_admin_password(self) -> str: frappe.only_for("System Manager") return self.get_password("recovery_admin_password") - @frappe.whitelist() - def generate_api_key(self) -> None: - """Generates an API key for the cluster.""" - - frappe.only_for("System Manager") - self.api_key = self._generate_api_key() - self.save() - - def _generate_api_key(self) -> str: - """Generates an API key for the cluster.""" - - if not self.base_url: - frappe.throw(_("Base URL is required.")) - - name = f"{random_string(10)}-{self.hostname}".lower() - secret = generate_secret() - principal = Principal( - name=name, type="apiKey", secrets=secret, roles=["admin"], enabledPermissions=["authenticate"] - ) - backend_api = MailBackendAPI( - self.base_url, - username=self.recovery_admin_user, - password=self.get_password("recovery_admin_password"), - ) - response = backend_api.request(method="POST", endpoint="/api/principal", json=principal.__dict__) - raise_for_status(response) - response_json = response.json() - - if error := response_json.get("error"): - frappe.throw(error) - - return f"api_{base64.b64encode(f'{name}:{secret}'.encode()).decode()}" - def get_bootstrap_operations(self, hostname: str = "{{ hostname }}") -> list[dict]: """Returns the bootstrap operations for the cluster.""" From 64f4b8454713395739cf5608bbf3f8c410ad08bd Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Tue, 12 May 2026 13:43:15 +0530 Subject: [PATCH 19/55] feat: `StalwartCLI` --- mail/install.py | 83 +--------------------------- mail/stalwart.py | 140 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 142 insertions(+), 81 deletions(-) create mode 100644 mail/stalwart.py diff --git a/mail/install.py b/mail/install.py index e8abe6f1e..bec5ea417 100644 --- a/mail/install.py +++ b/mail/install.py @@ -1,13 +1,8 @@ -import os -import platform -import tarfile -import urllib.request - import frappe from frappe.core.api.file import create_new_folder from mail.mail.doctype.rate_limit.rate_limit import create_rate_limit -from mail.utils import get_mail_app_path, get_stalwart_cli_path, get_stalwart_cli_version +from mail.stalwart import StalwartCLI def after_install() -> None: @@ -17,7 +12,7 @@ def after_install() -> None: def after_migrate() -> None: - install_stalwart_cli() + StalwartCLI()._install() def add_rate_limits() -> None: @@ -58,77 +53,3 @@ def generate_jmap_push_keys() -> None: settings = frappe.get_single("Mail Settings") if not settings.jmap_push_p256dh or not settings.jmap_push_private_key or not settings.jmap_push_auth: settings._generate_jmap_push_keys() - - -def install_stalwart_cli() -> str: - """Download and install the Stalwart CLI tool.""" - - print("Installing Stalwart CLI...") - - url, filename = _get_stalwart_cli_download_url() - install_dir = get_mail_app_path() - tar_path = os.path.join(install_dir, filename) - - if frappe.conf.developer_mode: - print(f"\tDownloading {url}...") - - urllib.request.urlretrieve(url, tar_path) - - if frappe.conf.developer_mode: - print(f"\tExtracting stalwart-cli from {filename}...") - - with tarfile.open(tar_path, "r:xz") as tar: - member = next( - (m for m in tar.getmembers() if os.path.basename(m.name) == "stalwart-cli"), - None, - ) - - if not member: - raise FileNotFoundError("stalwart-cli not found in archive") - - member.name = "stalwart-cli" - tar.extract(member, path=install_dir) - - cli_path = get_stalwart_cli_path() - os.chmod(cli_path, 0o755) - - if frappe.conf.developer_mode: - print(f"\tRemoving {tar_path}...") - - os.remove(tar_path) - - if frappe.conf.developer_mode: - print(f"\tStalwart CLI installed at: {cli_path}") - - return cli_path - - -def _get_stalwart_cli_download_url() -> str: - """Returns the download URL and filename for the Stalwart CLI tool.""" - - version = get_stalwart_cli_version() - github_release_base = ( - "https://github.com/stalwartlabs/cli/releases/latest/download" - if version == "latest" - else f"https://github.com/stalwartlabs/cli/releases/download/{version}" - ) - - system = platform.system().lower() - arch = platform.machine().lower() - - if arch in ["x86_64", "amd64"]: - arch = "x86_64" - elif arch in ["arm64", "aarch64"]: - arch = "aarch64" - else: - frappe.throw(f"Unsupported architecture: {arch}") - - if system == "linux": - os_id = "unknown-linux-gnu" - elif system == "darwin": - os_id = "apple-darwin" - else: - raise Exception(f"Unsupported operating system: {system}") - - filename = f"stalwart-cli-{arch}-{os_id}.tar.xz" - return f"{github_release_base}/{filename}", filename diff --git a/mail/stalwart.py b/mail/stalwart.py new file mode 100644 index 000000000..6664a77f0 --- /dev/null +++ b/mail/stalwart.py @@ -0,0 +1,140 @@ +import os +import platform +import subprocess +import tarfile +import urllib.request +from typing import TYPE_CHECKING + +import frappe + +from mail.utils import get_mail_app_path, get_mail_config, get_stalwart_cli_path, get_stalwart_cli_version + +if TYPE_CHECKING: + from subprocess import CompletedProcess + + +class StalwartCLI: + def __init__(self, credentials: dict[str, str] | None = None) -> None: + if credentials: + self._validate_credentials(credentials) + self._credentials = credentials + + else: + config = get_mail_config() + credentials = { + "server_url": config.get("server_url"), + "username": config.get("username"), + "password": config.get("password"), + } + self._validate_credentials(credentials) + + self._credentials = credentials + self.cli_path = get_stalwart_cli_path() + + if not os.path.exists(self.cli_path): + self._install() + + def _url(self) -> tuple[str, str]: + """Returns the download URL and filename for the appropriate Stalwart CLI release based on the current platform and architecture.""" + + version = get_stalwart_cli_version() + github_release_base = ( + "https://github.com/stalwartlabs/cli/releases/latest/download" + if version == "latest" + else f"https://github.com/stalwartlabs/cli/releases/download/{version}" + ) + + system = platform.system().lower() + arch = platform.machine().lower() + + if arch in ["x86_64", "amd64"]: + arch = "x86_64" + elif arch in ["arm64", "aarch64"]: + arch = "aarch64" + else: + frappe.throw(f"Unsupported architecture: {arch}") + + if system == "linux": + os_id = "unknown-linux-gnu" + elif system == "darwin": + os_id = "apple-darwin" + else: + raise Exception(f"Unsupported operating system: {system}") + + filename = f"stalwart-cli-{arch}-{os_id}.tar.xz" + return f"{github_release_base}/{filename}", filename + + def _install(self) -> None: + """Downloads and installs the Stalwart CLI to the mail app directory.""" + + print("Installing Stalwart CLI...") + + url, filename = self._url() + install_dir = get_mail_app_path() + tar_path = os.path.join(install_dir, filename) + + if frappe.conf.developer_mode: + print(f"\tDownloading {url}...") + + urllib.request.urlretrieve(url, tar_path) + + if frappe.conf.developer_mode: + print(f"\tExtracting stalwart-cli from {filename}...") + + with tarfile.open(tar_path, "r:xz") as tar: + member = next( + (m for m in tar.getmembers() if os.path.basename(m.name) == "stalwart-cli"), + None, + ) + + if not member: + raise FileNotFoundError("stalwart-cli not found in archive") + + member.name = "stalwart-cli" + tar.extract(member, path=install_dir) + + os.chmod(self.cli_path, 0o755) + + if frappe.conf.developer_mode: + print(f"\tRemoving {tar_path}...") + + os.remove(tar_path) + + if frappe.conf.developer_mode: + print(f"\tStalwart CLI installed at: {self.cli_path}") + + def _validate_credentials(self, credentials: dict) -> None: + """Validates that the provided credentials contain all mandatory fields.""" + + if frappe.flags.in_migrate: + return + + mandatory_fields = ["server_url", "username", "password"] + for field in mandatory_fields: + if field not in credentials or not credentials[field]: + frappe.throw(f"Missing mandatory credential field: {field}") + + def _parse_process_result(self, result: "CompletedProcess") -> dict: + """Parses the result of a subprocess execution and returns a dictionary with success status and output or error message.""" + + if result.returncode == 0: + return {"success": True, "output": result.stdout.strip()} + else: + return {"success": False, "error": result.stderr.strip()} + + def run(self, args: list[str]) -> dict: + """Runs a Stalwart CLI command with the provided arguments and returns the result.""" + + cmd = [ + self.cli_path, + "--url", + self._credentials["server_url"], + "--user", + self._credentials["username"], + "--password", + self._credentials["password"], + *args, + ] + + result = subprocess.run(cmd, capture_output=True, text=True) + return self._parse_process_result(result) From 398079f9d925362e1f4fe6de40875db4e50d1ad2 Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Tue, 12 May 2026 17:59:39 +0530 Subject: [PATCH 20/55] feat: Domain and Account services --- mail/install.py | 2 +- mail/stalwart/__init__.py | 0 mail/stalwart/account.py | 404 ++++++++++++++++++++++++++ mail/{stalwart.py => stalwart/cli.py} | 7 +- mail/stalwart/domain.py | 67 +++++ 5 files changed, 476 insertions(+), 4 deletions(-) create mode 100644 mail/stalwart/__init__.py create mode 100644 mail/stalwart/account.py rename mail/{stalwart.py => stalwart/cli.py} (94%) create mode 100644 mail/stalwart/domain.py diff --git a/mail/install.py b/mail/install.py index bec5ea417..801c5efbd 100644 --- a/mail/install.py +++ b/mail/install.py @@ -2,7 +2,7 @@ from frappe.core.api.file import create_new_folder from mail.mail.doctype.rate_limit.rate_limit import create_rate_limit -from mail.stalwart import StalwartCLI +from mail.stalwart.cli import StalwartCLI def after_install() -> None: diff --git a/mail/stalwart/__init__.py b/mail/stalwart/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/mail/stalwart/account.py b/mail/stalwart/account.py new file mode 100644 index 000000000..d149a9243 --- /dev/null +++ b/mail/stalwart/account.py @@ -0,0 +1,404 @@ +import json +from dataclasses import dataclass +from enum import Enum + +import frappe +from frappe import _ +from frappe.utils import random_string + +from mail.stalwart.cli import StalwartCLI +from mail.stalwart.domain import DomainService + + +class CredentialType(Enum): + PASSWORD = "Password" + + +@dataclass +class PasswordCredential: + secret: str + + +@dataclass +class Credential: + type: CredentialType + password: PasswordCredential + + def to_dict(self) -> dict: + if self.type == CredentialType.PASSWORD: + return { + "@type": self.type.value, + "secret": self.password.secret, + } + else: + raise ValueError(f"Unsupported credential type: {self.type}") + + +class RoleType(Enum): + USER = "User" + ADMIN = "Admin" + CUSTOM = "Custom" + + +@dataclass +class UserRoles: + type: RoleType + role_ids: list[str] | None = None + + def __post_init__(self) -> None: + if self.type == RoleType.CUSTOM and not self.role_ids: + raise ValueError("Custom role type requires role_ids to be provided.") + + if self.type != RoleType.CUSTOM and self.role_ids: + raise ValueError("Only custom role type can have role_ids defined.") + + def to_dict(self) -> dict: + if self.type == RoleType.CUSTOM: + return {"@type": self.type.value, "roleIds": {role_id: True for role_id in self.role_ids}} + else: + return {"@type": self.type.value} + + +class PermissionType(Enum): + INHERIT = "Inherit" + MERGE = "Merge" + REPLACE = "Replace" + + +@dataclass +class Permissions: + type: PermissionType + enabled_permissions: list[str] | None = None + disabled_permissions: list[str] | None = None + + def __post_init__(self) -> None: + if self.type == PermissionType.INHERIT and (self.enabled_permissions or self.disabled_permissions): + raise ValueError( + "Inherit permission type should not have enabled or disabled permissions defined." + ) + + if self.type != PermissionType.INHERIT and not ( + self.enabled_permissions or self.disabled_permissions + ): + raise ValueError( + "Merge or Replace permission type requires at least one of enabled or disabled permissions to be defined." + ) + + def to_dict(self) -> dict: + if self.type == PermissionType.INHERIT: + return {"@type": self.type.value} + else: + return { + "@type": self.type.value, + "enabledPermissions": {perm: True for perm in self.enabled_permissions} + if self.enabled_permissions + else {}, + "disabledPermissions": {perm: True for perm in self.disabled_permissions} + if self.disabled_permissions + else {}, + } + + +@dataclass +class StorageQuota: + max_emails: int | None = None + max_mailboxes: int | None = None + max_email_submissions: int | None = None + max_email_identities: int | None = None + max_participant_identities: int | None = None + max_sieve_scripts: int | None = None + max_push_subscriptions: int | None = None + max_calendars: int | None = None + max_calendar_events: int | None = None + max_calendar_event_notifications: int | None = None + max_address_books: int | None = None + max_contact_cards: int | None = None + max_files: int | None = None + max_folders: int | None = None + max_masked_addresses: int | None = None + max_app_passwords: int | None = None + max_api_keys: int | None = None + max_public_keys: int | None = None + max_disk_quota: int | None = None + + def __post_init__(self) -> None: + for field_name, value in self.__dict__.items(): + if value is not None and value < 0: + raise ValueError(f"{field_name} cannot be negative") + + def to_dict(self) -> dict: + quotas = {} + for field_name, value in self.__dict__.items(): + if value: + quotas[field_name] = value + + return quotas + + +@dataclass +class EmailAlias: + name: str + domain_id: str + enabled: bool = True + description: str | None = None + + def to_dict(self) -> dict: + return { + "name": self.name, + "domainId": self.domain_id, + "enabled": self.enabled, + "description": self.description, + } + + +class EncryptionType(Enum): + DISABLED = "Disabled" + AES128 = "Aes128" + AES256 = "Aes256" + + +@dataclass +class EncryptionSettings: + public_key_id: str + encrypt_on_append: bool = False + allow_spam_training: bool = False + + def to_dict(self) -> dict: + return { + "publicKey": self.public_key_id, + "encryptOnAppend": self.encrypt_on_append, + "allowSpamTraining": self.allow_spam_training, + } + + +@dataclass +class EncryptionAtRest: + type: EncryptionType + settings: EncryptionSettings | None = None + + def __post_init__(self) -> None: + if self.type != EncryptionType.DISABLED and not self.settings: + raise ValueError("Encryption settings must be provided when encryption is enabled.") + + if self.type == EncryptionType.DISABLED and self.settings: + raise ValueError("Encryption settings should not be provided when encryption is disabled.") + + def to_dict(self) -> dict: + if self.type == EncryptionType.DISABLED: + return {"@type": self.type.value} + else: + return {"@type": self.type.value, "settings": self.settings.to_dict()} + + +@dataclass +class Account: + name: str + domain_id: str + credentials: list[Credential] | None = None + member_group_ids: list[str] | None = None + roles: UserRoles | None = None + permissions: Permissions | None = None + quotas: StorageQuota | None = None + aliases: list[EmailAlias] | None = None + description: str | None = None + locale: str = "en_US" + timezone: str | None = None + encryption_at_rest: EncryptionAtRest | None = None + + def __post_init__(self) -> None: + self.encryption_at_rest = self.encryption_at_rest or EncryptionAtRest(type=EncryptionType.DISABLED) + + def to_dict(self) -> dict: + return { + "@type": "User", + "name": self.name, + "domainId": self.domain_id, + "credentials": {f"{idx}": credential.to_dict() for idx, credential in enumerate(self.credentials)} + if self.credentials + else {}, + "memberGroupIds": {group_id: True for group_id in self.member_group_ids} + if self.member_group_ids + else {}, + "roles": self.roles.to_dict() if self.roles else {}, + "permissions": self.permissions.to_dict() if self.permissions else {}, + "quotas": self.quotas.to_dict() if self.quotas else {}, + "aliases": {f"{idx}": alias.to_dict() for idx, alias in enumerate(self.aliases)} + if self.aliases + else {}, + "description": self.description, + "locale": self.locale, + "timeZone": self.timezone, + "encryptionAtRest": self.encryption_at_rest.to_dict(), + } + + +class AccountService(StalwartCLI): + def get(self, id: str, fields: list[str] | None = None) -> dict: + """Fetches an account by ID from the Stalwart server, selecting specific fields if provided.""" + + if not isinstance(fields, list): + fields = [ + "@type", + "id", + "name", + "description", + "emailAddress", + "aliases", + "domainId", + "memberGroupIds", + "quotas", + "roles", + "timeZone", + "usedDiskQuota", + ] + + commands = commands = ["get", "account", id] + + if fields: + commands.extend(["--fields", ",".join(fields)]) + + commands.append("--json") + response = self.run(commands) + + if response["success"]: + if response["output"]: + return json.loads(response["output"]) + else: + frappe.throw(_("Account with ID {0} not found.").format(id)) + else: + frappe.throw(_("Failed to fetch account: {0}").format(response["error"])) + + def get_all(self, filters: dict[str, str] | None = None, fields: list[str] | None = None) -> list[dict]: + """Fetches all accounts from the Stalwart server, applying optional filters and selecting specific fields.""" + + filters = filters or {} + + if not isinstance(fields, list): + fields = [ + "@type", + "id", + "name", + "description", + "emailAddress", + "aliases", + "domainId", + "memberGroupIds", + "quotas", + "roles", + "timeZone", + "usedDiskQuota", + ] + + commands = commands = ["query", "account"] + + if filters: + allowed_filter_keys = {"text", "name", "domainId", "memberGroupIds"} + for key, value in filters.items(): + if key in allowed_filter_keys: + commands.extend(["--where", f"{key}={value}"]) + else: + frappe.throw( + _("Invalid filter key: {0}. Allowed keys are: {1}").format( + key, ", ".join(allowed_filter_keys) + ) + ) + + if fields: + commands.extend(["--fields", ",".join(fields)]) + + commands.append("--json") + response = self.run(commands) + + if response["success"]: + if response["output"]: + accounts = response["output"].splitlines() + return [json.loads(account) for account in accounts] + + return [] + else: + frappe.throw(_("Failed to fetch accounts: {0}").format(response["error"])) + + def create(self, account: "Account") -> dict: + account_data = account.to_dict() + account_json = json.dumps(account_data) + response = self.run(["create", "account", "--json", account_json]) + + if not response["success"]: + frappe.throw(_("Failed to create account: {0}").format(response["error"])) + + return response + + +def create_account( + name: str, + domain: str, + password: str | None = None, + description: str | None = None, + aliases: list[str] | None = None, + groups: list[str] | None = None, + is_admin: bool = False, + quota: int | None = None, + timezone: str | None = None, +) -> None: + domain_manager = DomainService() + account_manager = AccountService() + + domain_id = None + if domains := domain_manager.get_all({"name": domain}, fields=["id"]): + domain_id = domains[0]["id"] + + if not domain_id: + frappe.throw(_("Domain {0} not found.").format(domain)) + + email_aliases = [] + domain_ids_map = {domain: domain_id} + for alias in aliases or []: + if not alias: + continue + + alias = alias.strip() + + if "@" in alias: + alias_name, alias_domain = alias.split("@", 1) + + alias_domain_id = domain_ids_map.get(alias_domain) + if not alias_domain_id: + if domains := domain_manager.get_all({"name": alias_domain}, fields=["id"]): + alias_domain_id = domains[0]["id"] + + if not alias_domain_id: + frappe.throw(_("Domain {0} not found for alias {1}.").format(alias_domain, alias)) + + domain_ids_map[alias_domain] = alias_domain_id + + email_aliases.append(EmailAlias(name=alias_name, domain_id=alias_domain_id)) + + member_group_ids = [] + for group in groups or []: + if groups := account_manager.get_all({"name": group}, fields=["id"]): + group_id = groups[0]["id"] + member_group_ids.append(group_id) + else: + frappe.throw(_("Group {0} not found.").format(group)) + + password = password or random_string(12) + + roles = UserRoles( + type=RoleType.CUSTOM if is_admin else RoleType.USER, role_ids=["d"] if is_admin else None + ) + quotas = StorageQuota(max_disk_quota=quota) if quota else None + + account = Account( + name=name, + domain_id=domain_id, + credentials=[Credential(type=CredentialType.PASSWORD, password=PasswordCredential(secret=password))], + member_group_ids=member_group_ids, + roles=roles, + permissions=Permissions(type=PermissionType.INHERIT), + quotas=quotas, + aliases=email_aliases, + description=description, + timezone=timezone, + ) + + account_manager.create(account) diff --git a/mail/stalwart.py b/mail/stalwart/cli.py similarity index 94% rename from mail/stalwart.py rename to mail/stalwart/cli.py index 6664a77f0..380da9891 100644 --- a/mail/stalwart.py +++ b/mail/stalwart/cli.py @@ -6,6 +6,7 @@ from typing import TYPE_CHECKING import frappe +from frappe import _ from mail.utils import get_mail_app_path, get_mail_config, get_stalwart_cli_path, get_stalwart_cli_version @@ -52,14 +53,14 @@ def _url(self) -> tuple[str, str]: elif arch in ["arm64", "aarch64"]: arch = "aarch64" else: - frappe.throw(f"Unsupported architecture: {arch}") + frappe.throw(_("Unsupported architecture: {0}").format(arch)) if system == "linux": os_id = "unknown-linux-gnu" elif system == "darwin": os_id = "apple-darwin" else: - raise Exception(f"Unsupported operating system: {system}") + frappe.throw(_("Unsupported operating system: {0}").format(system)) filename = f"stalwart-cli-{arch}-{os_id}.tar.xz" return f"{github_release_base}/{filename}", filename @@ -112,7 +113,7 @@ def _validate_credentials(self, credentials: dict) -> None: mandatory_fields = ["server_url", "username", "password"] for field in mandatory_fields: if field not in credentials or not credentials[field]: - frappe.throw(f"Missing mandatory credential field: {field}") + frappe.throw(_("Missing mandatory credential field: {0}").format(field)) def _parse_process_result(self, result: "CompletedProcess") -> dict: """Parses the result of a subprocess execution and returns a dictionary with success status and output or error message.""" diff --git a/mail/stalwart/domain.py b/mail/stalwart/domain.py new file mode 100644 index 000000000..37ef6065c --- /dev/null +++ b/mail/stalwart/domain.py @@ -0,0 +1,67 @@ +import json + +import frappe +from frappe import _ + +from mail.stalwart.cli import StalwartCLI + + +class DomainService(StalwartCLI): + def get(self, id: str, fields: list[str] | None = None) -> dict: + """Fetches a domain by ID from the Stalwart server, selecting specific fields if provided.""" + + if not isinstance(fields, list): + fields = ["id", "isEnabled", "name", "description"] + + commands = ["get", "domain", id] + + if fields: + commands.extend(["--fields", ",".join(fields)]) + + commands.append("--json") + response = self.run(commands) + + if response["success"]: + if response["output"]: + return json.loads(response["output"]) + else: + frappe.throw(_("Domain with ID {0} not found.").format(id)) + else: + frappe.throw(_("Failed to fetch domain: {0}").format(response["error"])) + + def get_all(self, filters: dict[str, str] | None = None, fields: list[str] | None = None) -> list[dict]: + """Fetches all domains from the Stalwart server, applying optional filters and selecting specific fields.""" + + filters = filters or {} + + if not isinstance(fields, list): + fields = ["id", "isEnabled", "name", "description"] + + commands = ["query", "domain"] + + if filters: + allowed_filter_keys = {"text", "name"} + for key, value in filters.items(): + if key in allowed_filter_keys: + commands.extend(["--where", f"{key}={value}"]) + else: + frappe.throw( + _("Invalid filter key: {0}. Allowed keys are: {1}").format( + key, ", ".join(allowed_filter_keys) + ) + ) + + if fields: + commands.extend(["--fields", ",".join(fields)]) + + commands.append("--json") + response = self.run(commands) + + if response["success"]: + if response["output"]: + domains = response["output"].splitlines() + return [json.loads(domain) for domain in domains] + + return [] + else: + frappe.throw(_("Failed to fetch domains: {0}").format(response["error"])) From dbfb38f6c27b0136ddfac52f6b22a033b64c7b53 Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Wed, 13 May 2026 11:12:28 +0530 Subject: [PATCH 21/55] fix: remove composite index from Mail Server --- mail/locale/main.pot | 4620 ++++++----------- .../server/doctype/mail_server/mail_server.py | 6 - 2 files changed, 1654 insertions(+), 2972 deletions(-) diff --git a/mail/locale/main.pot b/mail/locale/main.pot index 25bb37bba..67fa76498 100644 --- a/mail/locale/main.pot +++ b/mail/locale/main.pot @@ -7,8 +7,8 @@ msgid "" msgstr "" "Project-Id-Version: Mail VERSION\n" "Report-Msgid-Bugs-To: developers@frappe.io\n" -"POT-Creation-Date: 2026-04-27 10:36+0553\n" -"PO-Revision-Date: 2026-04-27 10:36+0553\n" +"POT-Creation-Date: 2026-05-13 11:11+0553\n" +"PO-Revision-Date: 2026-05-13 11:11+0553\n" "Last-Translator: developers@frappe.io\n" "Language-Team: developers@frappe.io\n" "MIME-Version: 1.0\n" @@ -21,11 +21,11 @@ msgstr "" msgid " Save as Draft" msgstr "" -#: mail/api/mail.py:422 +#: mail/api/mail.py:417 msgid "'Fail'" msgstr "" -#: mail/api/mail.py:422 +#: mail/api/mail.py:417 msgid "'Pass'" msgstr "" @@ -50,7 +50,7 @@ msgstr "" msgid ".zip" msgstr "" -#: frontend/src/pages/MailboxView.vue:1175 +#: frontend/src/pages/MailboxView.vue:1207 msgid "1 item selected" msgstr "" @@ -101,7 +101,7 @@ msgstr "" msgid "A color to be used when displaying events associated with the calendar." msgstr "" -#: mail/client/doctype/contact_card/contact_card.py:187 +#: mail/client/doctype/contact_card/contact_card.py:192 msgid "A contact card must belong to at least one address book." msgstr "" @@ -193,11 +193,6 @@ msgstr "" msgid "ACCEPTED" msgstr "" -#. Label of the acme_providers (Table) field in DocType 'Mail Server' -#: mail/server/doctype/mail_server/mail_server.json -msgid "ACME Providers" -msgstr "" - #. Label of the via_api (Check) field in DocType 'Mail Queue' #: mail/client/doctype/mail_queue/mail_queue.json msgid "API" @@ -208,19 +203,17 @@ msgstr "" msgid "API Access" msgstr "" -#. Label of the api_key (Password) field in DocType 'Mail Cluster' #. Option for the 'Type' (Select) field in DocType 'Principal' #. Option for the 'Principal Type' (Select) field in DocType 'Principal #. Settings' #: frontend/src/components/Settings/AdvancedSettings.vue:3 #: frontend/src/components/Settings/AdvancedSettings.vue:18 -#: mail/server/doctype/mail_cluster/mail_cluster.json #: mail/server/doctype/principal/principal.json #: mail/server/doctype/principal_settings/principal_settings.json msgid "API Key" msgstr "" -#: mail/backend.py:54 +#: mail/backend.py:53 msgid "API Key or Username and Password is required." msgstr "" @@ -238,9 +231,7 @@ msgid "Accepted" msgstr "" #. Label of the dns_provider_access_key (Data) field in DocType 'Mail Settings' -#. Label of the azure_access_key (Password) field in DocType 'Mail Cluster -#. Store' -#. Label of the access_key (Password) field in DocType 'Mail Cluster Store' +#. Label of the access_key (Data) field in DocType 'Mail Cluster Store' #: mail/mail/doctype/mail_settings/mail_settings.json #: mail/server/doctype/mail_cluster_store/mail_cluster_store.json msgid "Access Key" @@ -256,7 +247,7 @@ msgstr "" msgid "Access denied: Your IP address ({0}) is blocked due to explicit IP restrictions." msgstr "" -#: mail/auth.py:82 mail/auth.py:92 +#: mail/auth.py:76 mail/auth.py:86 msgid "Access not allowed for this URL" msgstr "" @@ -264,19 +255,82 @@ msgstr "" msgid "Access to GoDaddy’s Domain Management and DNS APIs is restricted to accounts with 10 or more domains or an active Pro Discount Domain Club membership. Please verify that your account meets these requirements before proceeding." msgstr "" -#. Label of the account (Link) field in DocType 'Mail Signature' +#. Label of the account (Select) field in DocType 'Account Settings' +#. Label of the account (Select) field in DocType 'Address Book' +#. Label of the account (Select) field in DocType 'Blocked Email Address' +#. Label of the account (Select) field in DocType 'Calendar' +#. Label of the account (Select) field in DocType 'Calendar Event' +#. Label of the account (Select) field in DocType 'Contact Card' +#. Label of the account (Select) field in DocType 'Event Notification' +#. Label of the account (Select) field in DocType 'Identity' +#. Label of the account (Select) field in DocType 'Mail Exchange' +#. Label of the account (Select) field in DocType 'Mail Message' +#. Label of the account (Select) field in DocType 'Mail Queue' +#. Label of the account (Select) field in DocType 'Mail Sync History' +#. Label of the account (Select) field in DocType 'Mailbox' +#. Label of the account (Select) field in DocType 'Mailbox Settings' +#. Label of the account (Select) field in DocType 'Participant Identity' +#. Label of the account (Select) field in DocType 'Quota' +#. Label of the account (Select) field in DocType 'Sieve Script' +#. Label of the account (Select) field in DocType 'Vacation Response' #. Label of the account (Data) field in DocType 'Mail Account Request' -#: frontend/src/components/Modals/SettingsModal.vue:81 -#: mail/client/doctype/mail_signature/mail_signature.json +#: frontend/src/components/Modals/SettingsModal.vue:85 +#: mail/client/doctype/account_settings/account_settings.json +#: mail/client/doctype/address_book/address_book.json +#: mail/client/doctype/blocked_email_address/blocked_email_address.json +#: mail/client/doctype/calendar/calendar.json +#: mail/client/doctype/calendar_event/calendar_event.json +#: mail/client/doctype/contact_card/contact_card.json +#: mail/client/doctype/event_notification/event_notification.json +#: mail/client/doctype/identity/identity.json +#: mail/client/doctype/mail_exchange/mail_exchange.json +#: mail/client/doctype/mail_message/mail_message.json +#: mail/client/doctype/mail_queue/mail_queue.json +#: mail/client/doctype/mail_sync_history/mail_sync_history.json +#: mail/client/doctype/mailbox/mailbox.json +#: mail/client/doctype/mailbox_settings/mailbox_settings.json +#: mail/client/doctype/participant_identity/participant_identity.json +#: mail/client/doctype/quota/quota.json +#: mail/client/doctype/sieve_script/sieve_script.json +#: mail/client/doctype/vacation_response/vacation_response.json #: mail/server/doctype/mail_account_request/mail_account_request.json msgid "Account" msgstr "" +#. Label of the id (Data) field in DocType 'User Account' +#: mail/client/doctype/user_account/user_account.json +msgid "Account ID" +msgstr "" + #. Label of a Workspace Sidebar Item #: mail/workspace_sidebar/server.json msgid "Account Request" msgstr "" +#. Name of a DocType +#: mail/client/doctype/account_settings/account_settings.json +msgid "Account Settings" +msgstr "" + +#: mail/client/doctype/mail_message/mail_message.py:761 +#: mail/client/doctype/mail_message/mail_message.py:830 +#: mail/client/doctype/mail_message/mail_message.py:865 +#: mail/client/doctype/mail_message/mail_message.py:900 +msgid "Account and Mail IDs are required." +msgstr "" + +#: mail/client/doctype/mail_message/mail_message.py:781 +msgid "Account and Mailbox ID are required." +msgstr "" + +#: mail/client/doctype/mail_message/mail_message.py:733 +msgid "Account and Thread IDs are required." +msgstr "" + +#: mail/client/doctype/mail_message/mail_message.py:673 +msgid "Account and filter are required." +msgstr "" + #: mail/server/doctype/mail_account_request/mail_account_request.py:114 msgid "Account domain {0} does not match with domain {1}." msgstr "" @@ -285,6 +339,10 @@ msgstr "" msgid "Account is already verified and created." msgstr "" +#: mail/client/doctype/mail_message/mail_message.py:938 +msgid "Account is required." +msgstr "" + #: frontend/src/components/Settings/AccountSettings.vue:73 #: frontend/src/pages/dashboard/MemberView.vue:263 msgid "Account updated." @@ -295,48 +353,59 @@ msgstr "" msgid "Account username for the DNS provider." msgstr "" +#: mail/client/doctype/user_account/user_account.py:86 +msgid "Account with ID '{0}' not found for user '{1}'." +msgstr "" + +#: mail/stalwart/account.py:267 +msgid "Account with ID {0} not found." +msgstr "" + #: mail/server/doctype/mail_account_request/mail_account_request.py:191 msgid "Account {0} is already created." msgstr "" +#: frontend/src/components/AppSidebar.vue:218 +msgid "Accounts" +msgstr "" + +#: mail/client/doctype/mail_message/mail_message.py:809 +msgid "Accounts, Mail IDs, and Mailbox ID are required." +msgstr "" + #. Label of the action (Select) field in DocType 'Event Alert' #: mail/client/doctype/event_alert/event_alert.json msgid "Action" msgstr "" -#: frontend/src/components/MailActions.vue:272 -#: frontend/src/pages/MailboxView.vue:1082 +#: frontend/src/components/MailActions.vue:279 +#: frontend/src/pages/MailboxView.vue:1114 msgid "Action failed. Please try again in some time." msgstr "" #: frontend/src/components/Modals/ShortcutsModal.vue:66 -#: mail/client/doctype/mail_exchange/mail_exchange.js:16 -#: mail/client/doctype/mail_message/mail_message.js:163 -#: mail/client/doctype/mail_message/mail_message.js:171 -#: mail/client/doctype/mail_queue/mail_queue.js:27 -#: mail/client/doctype/mail_queue/mail_queue.js:38 +#: mail/client/doctype/mail_exchange/mail_exchange.js:21 +#: mail/client/doctype/mail_message/mail_message.js:183 +#: mail/client/doctype/mail_message/mail_message.js:191 +#: mail/client/doctype/mail_queue/mail_queue.js:32 +#: mail/client/doctype/mail_queue/mail_queue.js:43 #: mail/client/doctype/push_subscription/push_subscription.js:17 #: mail/mail/doctype/mail_settings/mail_settings.js:51 #: mail/server/doctype/dns_record/dns_record.js:16 #: mail/server/doctype/dns_record/dns_record.js:25 #: mail/server/doctype/mail_account_request/mail_account_request.js:19 #: mail/server/doctype/mail_account_request/mail_account_request.js:27 -#: mail/server/doctype/mail_cluster/mail_cluster.js:159 #: mail/server/doctype/mail_data_exchange/mail_data_exchange.js:16 #: mail/server/doctype/mail_domain_request/mail_domain_request.js:16 #: mail/server/doctype/mail_domain_request/mail_domain_request.js:23 -#: mail/server/doctype/mail_server/mail_server.js:31 -#: mail/server/doctype/mail_server/mail_server.js:39 -#: mail/server/doctype/mail_server/mail_server.js:48 -#: mail/server/doctype/mail_server/mail_server.js:56 -#: mail/server/doctype/mail_server/mail_server.js:66 +#: mail/server/doctype/mail_server/mail_server.js:40 +#: mail/server/doctype/mail_server/mail_server.js:49 +#: mail/server/doctype/mail_server/mail_server.js:57 +#: mail/server/doctype/mail_server/mail_server.js:67 #: mail/server/doctype/principal/principal.js:18 #: mail/server/doctype/principal/principal.js:31 #: mail/server/doctype/principal/principal.js:44 -#: mail/server/doctype/principal/principal.js:52 #: mail/server/doctype/server_ansible_play/server_ansible_play.js:30 -#: mail/server/doctype/server_config/server_config.js:20 -#: mail/server/doctype/server_config/server_config.js:29 #: mail/server/doctype/server_deployment/server_deployment.js:31 #: mail/server/doctype/server_deployment/server_deployment.js:37 #: mail/server/doctype/server_deployment/server_deployment.js:42 @@ -376,7 +445,7 @@ msgid "Active Script {0} Detected" msgstr "" #: frontend/src/components/DashboardCard.vue:16 -#: frontend/src/components/Modals/AddAddressBookContactsModal.vue:49 +#: frontend/src/components/Modals/AddAddressBookContactsModal.vue:52 #: frontend/src/components/Modals/AddEmailModal.vue:8 #: frontend/src/components/Modals/AddMailingListExternalMemberModal.vue:8 #: frontend/src/components/Modals/AddMailingListInternalMembersModal.vue:8 @@ -431,7 +500,7 @@ msgstr "" msgid "Add Reply To" msgstr "" -#: frontend/src/components/Modals/FolderModal.vue:129 +#: frontend/src/components/Modals/FolderModal.vue:130 msgid "Add Star" msgstr "" @@ -447,15 +516,26 @@ msgstr "" msgid "Add to Mailing List" msgstr "" +#. Description of the 'Options' (Data) field in DocType 'Mail Cluster Store' +#: mail/server/doctype/mail_cluster_store/mail_cluster_store.json +msgid "Additional connection options." +msgstr "" + #. Description of the 'Description' (Small Text) field in DocType 'Calendar #. Event' #: mail/client/doctype/calendar_event/calendar_event.json msgid "Additional free-text details about the event." msgstr "" +#. Description of the 'HTTP Headers' (JSON) field in DocType 'Mail Cluster +#. Store' +#: mail/server/doctype/mail_cluster_store/mail_cluster_store.json +msgid "Additional headers to include in HTTP requests." +msgstr "" + #. Label of the address (Data) field in DocType 'Contact Card Email' #: frontend/src/components/Modals/AddContactEmailModal.vue:8 -#: frontend/src/pages/ContactView.vue:389 +#: frontend/src/pages/ContactView.vue:392 #: mail/client/doctype/contact_card_email/contact_card_email.json msgid "Address" msgstr "" @@ -471,20 +551,22 @@ msgstr "" msgid "Address Book" msgstr "" -#: mail/client/doctype/address_book/address_book.py:145 +#: mail/client/doctype/address_book/address_book.py:146 msgid "Address Book Creation Error" msgstr "" -#: mail/client/doctype/address_book/address_book.py:220 +#: mail/client/doctype/address_book/address_book.py:223 msgid "Address Book Deletion Error" msgstr "" -#: mail/client/doctype/address_book/address_book.py:219 +#: mail/client/doctype/address_book/address_book.py:222 msgid "Address Book Deletion Error(s):
{0}" msgstr "" +#. Label of the id (Data) field in DocType 'Address Book' #. Label of the address_book_id (Data) field in DocType 'Contact Card Address #. Book' +#: mail/client/doctype/address_book/address_book.json #: mail/client/doctype/contact_card_address_book/contact_card_address_book.json msgid "Address Book ID" msgstr "" @@ -495,26 +577,26 @@ msgstr "" msgid "Address Book Name" msgstr "" -#: mail/client/doctype/address_book/address_book.py:167 +#: mail/client/doctype/address_book/address_book.py:170 msgid "Address Book Not Found" msgstr "" -#: mail/client/doctype/address_book/address_book.py:197 +#: mail/client/doctype/address_book/address_book.py:200 msgid "Address Book Update Error" msgstr "" -#: mail/client/doctype/address_book/address_book.py:92 -msgid "Address Book name must be in the format 'user|id'." +#: mail/client/doctype/address_book/address_book.py:93 +msgid "Address Book name must be in the format 'account|id'." msgstr "" -#: mail/client/doctype/address_book/address_book.py:166 -msgid "Address Book with ID {0} not found in user {1}." +#: mail/client/doctype/address_book/address_book.py:167 +msgid "Address Book with ID {0} not found in account {1}." msgstr "" #. Label of the address_books (Table) field in DocType 'Contact Card' -#: frontend/src/components/AppSidebar.vue:264 +#: frontend/src/components/AppSidebar.vue:342 #: frontend/src/components/Modals/AddContactModal.vue:34 -#: frontend/src/pages/AddressBookView.vue:198 +#: frontend/src/pages/AddressBookView.vue:201 #: frontend/src/pages/AddressBooksView.vue:4 #: frontend/src/pages/AddressBooksView.vue:65 #: frontend/src/pages/ContactView.vue:29 @@ -522,19 +604,19 @@ msgstr "" msgid "Address Books" msgstr "" -#: mail/client/doctype/address_book/address_book.py:116 +#: mail/client/doctype/address_book/address_book.py:117 msgid "Address Books deleted successfully." msgstr "" -#: frontend/src/components/Modals/AddAddressBookModal.vue:66 +#: frontend/src/components/Modals/AddAddressBookModal.vue:65 msgid "Address book created." msgstr "" -#: frontend/src/pages/AddressBookView.vue:207 +#: frontend/src/pages/AddressBookView.vue:210 msgid "Address book deleted." msgstr "" -#: frontend/src/pages/AddressBookView.vue:138 +#: frontend/src/pages/AddressBookView.vue:140 msgid "Address book updated." msgstr "" @@ -549,15 +631,15 @@ msgstr "" msgid "Admin" msgstr "" -#: frontend/src/components/AppSidebar.vue:160 +#: frontend/src/components/AppSidebar.vue:188 msgid "Admin Dashboard" msgstr "" -#: mail/utils/validation.py:363 +#: mail/utils/validation.py:321 msgid "Admin credentials are not set in Mail Configuration." msgstr "" -#: frontend/src/components/Modals/SettingsModal.vue:134 +#: frontend/src/components/Modals/SettingsModal.vue:146 msgid "Advanced" msgstr "" @@ -568,18 +650,18 @@ msgstr "" msgid "After" msgstr "" +#. Description of the 'Verify After Write' (Check) field in DocType 'Mail +#. Cluster Store' +#: mail/server/doctype/mail_cluster_store/mail_cluster_store.json +msgid "After each successful write, verify the object is readable on the backend. Defends against the rare case where an S3-compatible backend returns success but does not actually persist the data. Adds one extra request per write." +msgstr "" + #. Label of the alerts (Table) field in DocType 'Calendar Event' #. Label of the alerts_tab (Tab Break) field in DocType 'Calendar Event' #: mail/client/doctype/calendar_event/calendar_event.json msgid "Alerts" msgstr "" -#. Description of the 'Compression' (Select) field in DocType 'Mail Cluster -#. Store' -#: mail/server/doctype/mail_cluster_store/mail_cluster_store.json -msgid "Algorithm to use to compress large binary objects." -msgstr "" - #. Option for the 'Category' (Select) field in DocType 'Principal DNS Record' #: mail/server/doctype/principal_dns_record/principal_dns_record.json msgid "Alias" @@ -588,7 +670,7 @@ msgstr "" #. Name of a role #. Option for the 'Include In Availability' (Select) field in DocType #. 'Calendar' -#: frontend/src/pages/MailboxView.vue:1112 +#: frontend/src/pages/MailboxView.vue:1144 #: mail/client/doctype/address_book/address_book.json #: mail/client/doctype/blocked_email_address/blocked_email_address.json #: mail/client/doctype/calendar/calendar.json @@ -606,6 +688,7 @@ msgstr "" #: mail/client/doctype/push_subscription/push_subscription.json #: mail/client/doctype/quota/quota.json #: mail/client/doctype/sieve_script/sieve_script.json +#: mail/client/doctype/user_account/user_account.json #: mail/client/doctype/user_settings/user_settings.json #: mail/client/doctype/vacation_response/vacation_response.json #: mail/server/doctype/mail_data_exchange/mail_data_exchange.json @@ -616,11 +699,11 @@ msgstr "" msgid "All Contacts" msgstr "" -#: frontend/src/pages/MailboxView.vue:1186 +#: frontend/src/pages/MailboxView.vue:1218 msgid "All Mails" msgstr "" -#: frontend/src/components/Modals/SearchModal.vue:214 +#: frontend/src/components/Modals/SearchModal.vue:217 msgid "All folders" msgstr "" @@ -637,7 +720,7 @@ msgstr "" msgid "All rules and filters associated with this script will take effect immediately." msgstr "" -#. Label of the tls_allow_invalid_certs (Check) field in DocType 'Mail Cluster +#. Label of the allow_invalid_certs (Check) field in DocType 'Mail Cluster #. Store' #: mail/server/doctype/mail_cluster_store/mail_cluster_store.json msgid "Allow Invalid Certs" @@ -654,23 +737,11 @@ msgstr "" msgid "Allow invalid TLS certificates when connecting to the store." msgstr "" -#. Description of the 'Enable encryption at rest' (Check) field in DocType -#. 'Mail Cluster' -#: mail/server/doctype/mail_cluster/mail_cluster.json -msgid "Allow users to configure encryption at rest for their data." -msgstr "" - -#. Name of a DocType #. Label of a Workspace Sidebar Item -#: mail/server/doctype/allowed_ip/allowed_ip.json #: mail/workspace_sidebar/server.json msgid "Allowed IP" msgstr "" -#: mail/server/doctype/allowed_ip/allowed_ip.py:40 -msgid "Allowed IP removed successfully." -msgstr "" - #. Label of the allowed_ips (Small Text) field in DocType 'Rate Limit' #: mail/mail/doctype/rate_limit/rate_limit.json msgid "Allowed IPs" @@ -692,7 +763,7 @@ msgid "AmazonRoute53" msgstr "" #. Option for the 'Color' (Select) field in DocType 'Mailbox Settings' -#: frontend/src/components/Modals/FolderModal.vue:295 +#: frontend/src/components/Modals/FolderModal.vue:299 #: mail/client/doctype/mailbox_settings/mailbox_settings.json msgid "Amber" msgstr "" @@ -772,7 +843,7 @@ msgstr "" #. Label of the appearance_section (Section Break) field in DocType 'User #. Settings' -#: frontend/src/components/Modals/SettingsModal.vue:93 +#: frontend/src/components/Modals/SettingsModal.vue:99 #: frontend/src/components/Settings/AppearanceSettings.vue:2 #: mail/client/doctype/user_settings/user_settings.json msgid "Appearance" @@ -783,10 +854,10 @@ msgid "Appearance updated." msgstr "" #: frontend/src/components/Settings/ExportSettings.vue:20 -msgid "Apply filters to select specific emails for export" +msgid "Apply filters to select specific emails for export." msgstr "" -#: frontend/src/components/AppSidebar.vue:132 +#: frontend/src/components/AppSidebar.vue:149 msgid "Apps" msgstr "" @@ -800,16 +871,16 @@ msgstr "" msgid "Archive Type" msgstr "" -#: mail/client/doctype/mail_exchange/mail_exchange.py:592 +#: mail/client/doctype/mail_exchange/mail_exchange.py:594 #: mail/server/doctype/mail_data_exchange/mail_data_exchange.py:98 msgid "Archive Type is required." msgstr "" -#: frontend/src/pages/ContactView.vue:286 +#: frontend/src/pages/ContactView.vue:289 msgid "Are you sure you want to delete the contact for {0}?" msgstr "" -#: frontend/src/pages/ContactsView.vue:133 +#: frontend/src/pages/ContactsView.vue:148 msgid "Are you sure you want to delete the selected contacts?" msgstr "" @@ -837,21 +908,19 @@ msgstr "" msgid "Are you sure you want to delete this member? This action cannot be undone." msgstr "" -#: frontend/src/pages/AddressBookView.vue:252 +#: frontend/src/pages/AddressBookView.vue:255 msgid "Are you sure you want to delete {0}?" msgstr "" -#: frontend/src/pages/MailboxView.vue:929 +#: frontend/src/pages/MailboxView.vue:961 msgid "Are you sure you want to mark the selected {0} as junk?" msgstr "" -#: frontend/src/pages/MailboxView.vue:930 +#: frontend/src/pages/MailboxView.vue:962 msgid "Are you sure you want to permanently delete the selected {0}?" msgstr "" -#: mail/server/doctype/mail_server/mail_server.js:62 -#: mail/server/doctype/server_config/server_config.js:16 -#: mail/server/doctype/server_config/server_config.js:25 +#: mail/server/doctype/mail_server/mail_server.js:63 msgid "Are you sure you want to proceed?" msgstr "" @@ -859,23 +928,23 @@ msgstr "" msgid "Are you sure you want to refresh the DNS records? If there are any changes, you'll need to update the DNS settings with your DNS provider accordingly." msgstr "" -#: frontend/src/pages/ContactView.vue:357 +#: frontend/src/pages/ContactView.vue:360 msgid "Are you sure you want to remove the selected addresses?" msgstr "" -#: frontend/src/pages/AddressBookView.vue:259 +#: frontend/src/pages/AddressBookView.vue:262 msgid "Are you sure you want to remove the selected contacts?" msgstr "" -#: frontend/src/pages/ContactView.vue:315 +#: frontend/src/pages/ContactView.vue:318 msgid "Are you sure you want to remove the selected emails?" msgstr "" -#: frontend/src/pages/ContactView.vue:336 +#: frontend/src/pages/ContactView.vue:339 msgid "Are you sure you want to remove the selected phones?" msgstr "" -#: frontend/src/pages/ContactView.vue:294 +#: frontend/src/pages/ContactView.vue:297 msgid "Are you sure you want to remove this contact from the selected address books?" msgstr "" @@ -883,7 +952,7 @@ msgstr "" msgid "Are you sure you want to rotate the DKIM keys? This will generate new keys for email signing and you'll need to update the DNS settings with your DNS provider accordingly. This may take some time to propagate across DNS servers. Emails sent during this period may fail DKIM verification." msgstr "" -#: frontend/src/components/Settings/BlockListSettings.vue:92 +#: frontend/src/components/Settings/BlockListSettings.vue:96 msgid "Are you sure you want to unblock the selected email addresses?" msgstr "" @@ -892,13 +961,7 @@ msgstr "" msgid "Assigned Email" msgstr "" -#. Label of the jmap_email_max_attachment_size (Int) field in DocType 'Mail -#. Cluster' -#: mail/server/doctype/mail_cluster/mail_cluster.json -msgid "Attachment Size (Bytes)" -msgstr "" - -#: mail/api/mail.py:372 +#: mail/api/mail.py:367 msgid "Attachment with cid {0} not found in the current draft." msgstr "" @@ -908,19 +971,13 @@ msgstr "" #. Label of the attachments_section (Section Break) field in DocType 'Mail #. Queue' #. Label of the attachments (JSON) field in DocType 'Mail Queue' -#: frontend/src/components/Modals/SearchModal.vue:75 +#: frontend/src/components/Modals/SearchModal.vue:78 #: frontend/src/components/Settings/ExportSettings.vue:47 #: mail/client/doctype/mail_message/mail_message.json #: mail/client/doctype/mail_queue/mail_queue.json msgid "Attachments" msgstr "" -#. Label of the jmap_push_attempts_interval (Int) field in DocType 'Mail -#. Cluster' -#: mail/server/doctype/mail_cluster/mail_cluster.json -msgid "Attempt Interval (Milliseconds)" -msgstr "" - #. Option for the 'Include In Availability' (Select) field in DocType #. 'Calendar' #: mail/client/doctype/calendar/calendar.json @@ -940,23 +997,20 @@ msgstr "" msgid "Auth" msgstr "" -#. Label of the auth_results_section (Section Break) field in DocType 'DMARC -#. Report Detail' -#: mail/server/doctype/dmarc_report_detail/dmarc_report_detail.json -msgid "Auth Results" +#. Label of the auth_secret (Password) field in DocType 'Mail Cluster Store' +#: mail/server/doctype/mail_cluster_store/mail_cluster_store.json +msgid "Auth Secret" msgstr "" -#: mail/server/report/dmarc_report_viewer/dmarc_report_viewer.py:46 -msgid "Auth Type" +#. Label of the auth_username (Data) field in DocType 'Mail Cluster Store' +#: mail/server/doctype/mail_cluster_store/mail_cluster_store.json +msgid "Auth Username" msgstr "" #. Label of the authentication_section (Section Break) field in DocType 'Mail #. Cluster' -#. Label of the authentication_section (Section Break) field in DocType 'Mail -#. Cluster Store' #. Label of the authentication_tab (Tab Break) field in DocType 'Principal' #: mail/server/doctype/mail_cluster/mail_cluster.json -#: mail/server/doctype/mail_cluster_store/mail_cluster_store.json #: mail/server/doctype/principal/principal.json msgid "Authentication" msgstr "" @@ -1008,16 +1062,16 @@ msgstr "" msgid "Automatically deletes the newsletter after it is sent." msgstr "" -#: frontend/src/components/Modals/FolderModal.vue:121 +#: frontend/src/components/Modals/FolderModal.vue:122 msgid "Automatically mark emails as read when they are moved to this folder." msgstr "" -#: frontend/src/components/Modals/FolderModal.vue:132 +#: frontend/src/components/Modals/FolderModal.vue:133 msgid "Automatically star emails when they are moved to this folder." msgstr "" -#: frontend/src/components/Modals/FolderModal.vue:182 -#: frontend/src/components/Modals/SettingsModal.vue:110 +#: frontend/src/components/Modals/FolderModal.vue:184 +#: frontend/src/components/Modals/SettingsModal.vue:122 msgid "Automation" msgstr "" @@ -1025,16 +1079,16 @@ msgstr "" msgid "Available" msgstr "" -#: mail/api/mail.py:614 +#: mail/api/mail.py:615 msgid "Avatar not found." msgstr "" #. Option for the 'Type' (Select) field in DocType 'Mail Cluster Store' #: mail/server/doctype/mail_cluster_store/mail_cluster_store.json -msgid "Azure Blob Storage" +msgid "Azure" msgstr "" -#: mail/api/mail.py:438 +#: mail/api/mail.py:433 msgid "BCC" msgstr "" @@ -1042,15 +1096,15 @@ msgstr "" msgid "Back" msgstr "" -#: mail/backend.py:99 +#: mail/backend.py:98 msgid "Backend Request Failed" msgstr "" -#: mail/backend.py:100 +#: mail/backend.py:99 msgid "Backend request failed with status code {0}. Check Error Log for details." msgstr "" -#: mail/backend.py:105 +#: mail/backend.py:104 msgid "Backend request failed. Check Error Log for details." msgstr "" @@ -1070,19 +1124,19 @@ msgstr "" msgid "Backup Email is required." msgstr "" -#: mail/utils/validation.py:176 +#: mail/utils/validation.py:132 msgid "Bad Cron Expression" msgstr "" #. Label of the base_url (Data) field in DocType 'Mail Cluster' -#. Label of the base_url (Data) field in DocType 'Mail Server' #: mail/server/doctype/mail_cluster/mail_cluster.json -#: mail/server/doctype/mail_server/mail_server.json msgid "Base URL" msgstr "" -#: mail/server/doctype/mail_cluster/mail_cluster.py:280 -msgid "Base URL is required." +#. Option for the 'Type' (Select) field in DocType 'Mail Cluster Store HTTP +#. Auth' +#: mail/server/doctype/mail_cluster_store_http_auth/mail_cluster_store_http_auth.json +msgid "Basic" msgstr "" #. Option for the 'Delivery Mode' (Select) field in DocType 'Mail Queue' @@ -1094,7 +1148,7 @@ msgstr "" #. Label of the _bcc (Data) field in DocType 'Mail Message' #. Option for the 'Type' (Select) field in DocType 'Mail Message Recipient' #: frontend/src/components/ComposeMailEditor.vue:116 -#: frontend/src/components/Modals/SearchModal.vue:56 +#: frontend/src/components/Modals/SearchModal.vue:59 #: frontend/src/components/Settings/IdentitySettings.vue:44 #: mail/client/doctype/identity/identity.json #: mail/client/doctype/mail_message/mail_message.json @@ -1106,6 +1160,24 @@ msgstr "" msgid "Bcc: " msgstr "" +#. Option for the 'Type' (Select) field in DocType 'Mail Cluster Store HTTP +#. Auth' +#: mail/server/doctype/mail_cluster_store_http_auth/mail_cluster_store_http_auth.json +msgid "Bearer" +msgstr "" + +#. Label of the bearer_token (Password) field in DocType 'Mail Cluster Store +#. HTTP Auth' +#: mail/server/doctype/mail_cluster_store_http_auth/mail_cluster_store_http_auth.json +msgid "Bearer Token" +msgstr "" + +#. Description of the 'Bearer Token' (Password) field in DocType 'Mail Cluster +#. Store HTTP Auth' +#: mail/server/doctype/mail_cluster_store_http_auth/mail_cluster_store_http_auth.json +msgid "Bearer token for HTTP Bearer Authentication." +msgstr "" + #. Label of the before (Datetime) field in DocType 'Calendar Event' #. Label of the before (Datetime) field in DocType 'Mail Message' #: mail/client/doctype/calendar_event/calendar_event.json @@ -1113,14 +1185,9 @@ msgstr "" msgid "Before" msgstr "" -#. Label of the bind (Small Text) field in DocType 'Mail Server Listener' -#: mail/server/doctype/mail_server_listener/mail_server_listener.json -msgid "Bind Addresses" -msgstr "" - -#. Label of the blob_hash (Data) field in DocType 'Message Queue' -#: mail/server/doctype/message_queue/message_queue.json -msgid "Blob Hash" +#. Label of the blob_store_config (JSON) field in DocType 'Mail Cluster' +#: mail/server/doctype/mail_cluster/mail_cluster.json +msgid "Blob Config" msgstr "" #. Label of the blob_id (Data) field in DocType 'Mail Message' @@ -1134,18 +1201,25 @@ msgstr "" msgid "Blob ID" msgstr "" -#: mail/api/mail.py:195 +#: mail/api/mail.py:188 msgid "Blob ID is required." msgstr "" -#. Label of the blob_storage_section (Section Break) field in DocType 'Mail -#. Cluster' -#: mail/server/doctype/mail_cluster/mail_cluster.json -#: mail/server/doctype/mail_cluster/mail_cluster.py:308 +#. Label of the blob_size (Int) field in DocType 'Mail Cluster Store' +#: mail/server/doctype/mail_cluster_store/mail_cluster_store.json +msgid "Blob Size" +msgstr "" + +#: mail/server/doctype/mail_cluster/mail_cluster.py:236 msgid "Blob Storage" msgstr "" -#: mail/client/doctype/user_settings/user_settings.js:39 +#. Label of the blob_store (Link) field in DocType 'Mail Cluster' +#: mail/server/doctype/mail_cluster/mail_cluster.json +msgid "Blob Store" +msgstr "" + +#: mail/client/doctype/account_settings/account_settings.js:32 msgid "Blobs" msgstr "" @@ -1153,16 +1227,16 @@ msgstr "" msgid "Block" msgstr "" -#: frontend/src/components/Modals/SettingsModal.vue:116 +#: frontend/src/components/Modals/SettingsModal.vue:128 #: frontend/src/components/Settings/BlockListSettings.vue:2 msgid "Block List" msgstr "" -#: frontend/src/components/MailActions.vue:183 +#: frontend/src/components/MailActions.vue:190 msgid "Block Sender" msgstr "" -#: frontend/src/components/Settings/BlockListSettings.vue:105 +#: frontend/src/components/Settings/BlockListSettings.vue:109 msgid "Block specific addresses to prevent their messages from appearing in your inbox." msgstr "" @@ -1171,28 +1245,22 @@ msgstr "" msgid "Blocked Email Address" msgstr "" -#. Name of a DocType #. Label of a Workspace Sidebar Item -#: mail/server/doctype/blocked_ip/blocked_ip.json #: mail/workspace_sidebar/server.json msgid "Blocked IP" msgstr "" -#: mail/server/doctype/blocked_ip/blocked_ip.py:40 -msgid "Blocked IP removed successfully." -msgstr "" - #. Label of the blocked_ips (Small Text) field in DocType 'Rate Limit' #: mail/mail/doctype/rate_limit/rate_limit.json msgid "Blocked IPs" msgstr "" -#: frontend/src/components/MailActions.vue:307 +#: frontend/src/components/MailActions.vue:341 msgid "Blocking email address..." msgstr "" #. Option for the 'Color' (Select) field in DocType 'Mailbox Settings' -#: frontend/src/components/Modals/FolderModal.vue:293 +#: frontend/src/components/Modals/FolderModal.vue:297 #: mail/client/doctype/mailbox_settings/mailbox_settings.json msgid "Blue" msgstr "" @@ -1211,7 +1279,16 @@ msgstr "" msgid "Body Parts" msgstr "" -#: frontend/src/components/Modals/FolderModal.vue:107 +#. Label of the bootstrap_tab (Tab Break) field in DocType 'Mail Server' +#: mail/server/doctype/mail_server/mail_server.json +msgid "Bootstrap" +msgstr "" + +#: mail/server/doctype/mail_server/mail_server.py:67 +msgid "Bootstrap NDJSON regenerated." +msgstr "" + +#: frontend/src/components/Modals/FolderModal.vue:108 msgid "Both conditions are met" msgstr "" @@ -1220,28 +1297,22 @@ msgstr "" msgid "Bucket" msgstr "" -#. Label of the bucket_storage_account_section (Section Break) field in DocType -#. 'Mail Cluster Store' +#. Label of the buffer_size (Int) field in DocType 'Mail Cluster Store' #: mail/server/doctype/mail_cluster_store/mail_cluster_store.json -msgid "Bucket / Storage Account" -msgstr "" - -#. Label of the buffer (Check) field in DocType 'Mail Cluster Trace' -#: mail/server/doctype/mail_cluster_trace/mail_cluster_trace.json -msgid "Buffered writes" -msgstr "" - -#: mail/client/doctype/address_book/address_book_list.js:11 -#: mail/client/doctype/calendar/calendar_list.js:11 -#: mail/client/doctype/calendar_event/calendar_event_list.js:20 -#: mail/client/doctype/contact_card/contact_card_list.js:11 -#: mail/client/doctype/event_notification/event_notification_list.js:11 -#: mail/client/doctype/identity/identity_list.js:11 -#: mail/client/doctype/mail_message/mail_message_list.js:11 -#: mail/client/doctype/mailbox/mailbox_list.js:11 -#: mail/client/doctype/participant_identity/participant_identity_list.js:11 +msgid "Buffer Size" +msgstr "" + +#: mail/client/doctype/address_book/address_book_list.js:12 +#: mail/client/doctype/calendar/calendar_list.js:12 +#: mail/client/doctype/calendar_event/calendar_event_list.js:21 +#: mail/client/doctype/contact_card/contact_card_list.js:12 +#: mail/client/doctype/event_notification/event_notification_list.js:12 +#: mail/client/doctype/identity/identity_list.js:12 +#: mail/client/doctype/mail_message/mail_message_list.js:12 +#: mail/client/doctype/mailbox/mailbox_list.js:12 +#: mail/client/doctype/participant_identity/participant_identity_list.js:12 #: mail/client/doctype/push_subscription/push_subscription_list.js:11 -#: mail/client/doctype/sieve_script/sieve_script_list.js:11 +#: mail/client/doctype/sieve_script/sieve_script_list.js:12 msgid "Bulk Delete" msgstr "" @@ -1250,7 +1321,7 @@ msgstr "" msgid "Busy" msgstr "" -#: mail/api/mail.py:437 +#: mail/api/mail.py:432 msgid "CC" msgstr "" @@ -1261,8 +1332,9 @@ msgstr "" msgid "CNAME" msgstr "" -#. Label of the cache_section (Section Break) field in DocType 'User Settings' -#: mail/client/doctype/user_settings/user_settings.json +#. Label of the cache_section (Section Break) field in DocType 'Account +#. Settings' +#: mail/client/doctype/account_settings/account_settings.json msgid "Cache" msgstr "" @@ -1273,15 +1345,15 @@ msgstr "" msgid "Calendar" msgstr "" -#: mail/client/doctype/calendar/calendar.py:156 +#: mail/client/doctype/calendar/calendar.py:157 msgid "Calendar Creation Error" msgstr "" -#: mail/client/doctype/calendar/calendar.py:238 +#: mail/client/doctype/calendar/calendar.py:239 msgid "Calendar Deletion Error" msgstr "" -#: mail/client/doctype/calendar/calendar.py:237 +#: mail/client/doctype/calendar/calendar.py:238 msgid "Calendar Deletion Error(s):
{0}" msgstr "" @@ -1292,49 +1364,53 @@ msgstr "" msgid "Calendar Event" msgstr "" -#: mail/client/doctype/calendar_event/calendar_event.py:328 +#: mail/client/doctype/calendar_event/calendar_event.py:332 msgid "Calendar Event Creation Error" msgstr "" -#: mail/client/doctype/calendar_event/calendar_event.py:504 #: mail/client/doctype/calendar_event/calendar_event.py:508 +#: mail/client/doctype/calendar_event/calendar_event.py:512 msgid "Calendar Event Deletion Error" msgstr "" -#: mail/client/doctype/calendar_event/calendar_event.py:503 +#: mail/client/doctype/calendar_event/calendar_event.py:507 msgid "Calendar Event Deletion Error(s):
{0}" msgstr "" +#. Label of the id (Data) field in DocType 'Calendar Event' #. Label of the calendar_event_id (Data) field in DocType 'Event Notification' +#: mail/client/doctype/calendar_event/calendar_event.json #: mail/client/doctype/event_notification/event_notification.json msgid "Calendar Event ID" msgstr "" -#: mail/client/doctype/calendar_event/calendar_event.py:520 +#: mail/client/doctype/calendar_event/calendar_event.py:524 msgid "Calendar Event Instance Deletion Error" msgstr "" -#: mail/client/doctype/calendar_event/calendar_event.py:481 +#: mail/client/doctype/calendar_event/calendar_event.py:485 msgid "Calendar Event Instance Update Error" msgstr "" -#: mail/client/doctype/calendar_event/calendar_event.py:128 +#: mail/client/doctype/calendar_event/calendar_event.py:130 msgid "Calendar Event Not Found" msgstr "" -#: mail/client/doctype/calendar_event/calendar_event.py:456 +#: mail/client/doctype/calendar_event/calendar_event.py:460 msgid "Calendar Event Update Error" msgstr "" #: mail/client/doctype/calendar_event/calendar_event.py:127 -msgid "Calendar Event with ID {0} not found in user {1}." +msgid "Calendar Event with ID {0} not found in account {1}." msgstr "" -#: mail/client/doctype/calendar_event/calendar_event.py:270 +#: mail/client/doctype/calendar_event/calendar_event.py:274 msgid "Calendar Events deleted successfully." msgstr "" +#. Label of the id (Data) field in DocType 'Calendar' #. Label of the calendar_id (Data) field in DocType 'Event Calendar' +#: mail/client/doctype/calendar/calendar.json #: mail/client/doctype/event_calendar/event_calendar.json msgid "Calendar ID" msgstr "" @@ -1344,7 +1420,7 @@ msgstr "" msgid "Calendar Name" msgstr "" -#: mail/client/doctype/calendar/calendar.py:177 +#: mail/client/doctype/calendar/calendar.py:178 msgid "Calendar Not Found" msgstr "" @@ -1353,27 +1429,24 @@ msgstr "" msgid "Calendar Rights" msgstr "" -#: mail/client/doctype/calendar/calendar.py:215 +#: mail/client/doctype/calendar/calendar.py:216 msgid "Calendar Update Error" msgstr "" -#: mail/client/doctype/calendar/calendar.py:101 -msgid "Calendar name must be in the format 'user|id'." +#: mail/client/doctype/calendar/calendar.py:102 +msgid "Calendar name must be in the format 'account|id'." msgstr "" -#: mail/client/doctype/calendar/calendar.py:176 -msgid "Calendar with ID {0} not found for user {1}" +#: mail/client/doctype/calendar/calendar.py:177 +msgid "Calendar with ID {0} not found for account {1}" msgstr "" #. Label of the calendars (Table) field in DocType 'Calendar Event' -#. Label of the jmap_calendar_parse_max_items (Int) field in DocType 'Mail -#. Cluster' #: mail/client/doctype/calendar_event/calendar_event.json -#: mail/server/doctype/mail_cluster/mail_cluster.json msgid "Calendars" msgstr "" -#: mail/client/doctype/calendar/calendar.py:119 +#: mail/client/doctype/calendar/calendar.py:120 msgid "Calendars deleted successfully." msgstr "" @@ -1381,11 +1454,6 @@ msgstr "" msgid "Cancel" msgstr "" -#: mail/server/doctype/message_queue/message_queue.js:30 -#: mail/server/doctype/message_queue/message_queue.js:103 -msgid "Cancel Delivery" -msgstr "" - #. Option for the 'Status' (Select) field in DocType 'Calendar Event' #. Option for the 'Status' (Select) field in DocType 'Mail Exchange' #. Option for the 'Status' (Select) field in DocType 'Mail Data Exchange' @@ -1396,31 +1464,34 @@ msgstr "" msgid "Cancelled" msgstr "" -#: mail/server/doctype/message_queue/message_queue.js:117 -msgid "Cancelling Delivery..." -msgstr "" - -#: mail/client/doctype/sieve_script/sieve_script.py:40 +#: mail/client/doctype/sieve_script/sieve_script.py:42 msgid "Cannot delete an active sieve script. Please deactivate it first." msgstr "" -#: mail/server/doctype/principal/principal.py:636 +#: mail/server/doctype/principal/principal.py:584 msgid "Cannot delete domain {0} as there are addresses associated with it." msgstr "" -#: mail/api/inbound.py:84 +#: mail/api/inbound.py:88 msgid "Cannot fetch more than {0} emails at a time." msgstr "" -#: mail/client/doctype/calendar_event/calendar_event.py:226 +#: mail/client/doctype/calendar_event/calendar_event.py:230 msgid "Cannot mark an existing event as draft." msgstr "" -#: mail/client/doctype/mail_queue/mail_queue.py:569 +#: mail/client/doctype/mail_queue/mail_queue.py:572 msgid "Cannot retry a mail with status {0}" msgstr "" -#: mail/server/doctype/principal/principal.py:197 +#. Label of the capabilities_section (Section Break) field in DocType 'User +#. Account' +#. Label of the capabilities (JSON) field in DocType 'User Account' +#: mail/client/doctype/user_account/user_account.json +msgid "Capabilities" +msgstr "" + +#: mail/server/doctype/principal/principal.py:195 msgid "Catch-all email addresses are not allowed for principals." msgstr "" @@ -1434,7 +1505,7 @@ msgstr "" #. Label of the _cc (Data) field in DocType 'Mail Message' #. Option for the 'Type' (Select) field in DocType 'Mail Message Recipient' #: frontend/src/components/ComposeMailEditor.vue:96 -#: frontend/src/components/Modals/SearchModal.vue:55 +#: frontend/src/components/Modals/SearchModal.vue:58 #: mail/client/doctype/mail_message/mail_message.json #: mail/client/doctype/mail_message_recipient/mail_message_recipient.json msgid "Cc" @@ -1444,27 +1515,6 @@ msgstr "" msgid "Cc: " msgstr "" -#. Label of the cert (Text) field in DocType 'Mail Server TLS Certificate' -#: mail/server/doctype/mail_server_tls_certificate/mail_server_tls_certificate.json -msgid "Certificate" -msgstr "" - -#. Label of the certificate_id (Data) field in DocType 'Mail Server TLS -#. Certificate' -#: mail/server/doctype/mail_server_tls_certificate/mail_server_tls_certificate.json -msgid "Certificate ID" -msgstr "" - -#. Label of the cert_path (Data) field in DocType 'Mail Server TLS Certificate' -#: mail/server/doctype/mail_server_tls_certificate/mail_server_tls_certificate.json -msgid "Certificate Path" -msgstr "" - -#. Label of the challenge (Select) field in DocType 'Mail Server ACME Provider' -#: mail/server/doctype/mail_server_acme_provider/mail_server_acme_provider.json -msgid "Challenge Type" -msgstr "" - #: frontend/src/components/Modals/ChangePasswordModal.vue:51 #: frontend/src/components/Settings/ProfileSettings.vue:30 msgid "Change Password" @@ -1481,23 +1531,12 @@ msgstr "" msgid "Changed By" msgstr "" -#. Label of the jmap_protocol_changes_max_results (Int) field in DocType 'Mail -#. Cluster' -#: mail/server/doctype/mail_cluster/mail_cluster.json -msgid "Changes" -msgstr "" - -#. Label of the changes_max_history (Int) field in DocType 'Mail Cluster' -#: mail/server/doctype/mail_cluster/mail_cluster.json -msgid "Changes History" -msgstr "" - #. Label of the charset (Data) field in DocType 'Mail Message Part' #: mail/client/doctype/mail_message_part/mail_message_part.json msgid "Charset" msgstr "" -#: frontend/src/components/Modals/FolderModal.vue:49 +#: frontend/src/components/Modals/FolderModal.vue:50 msgid "Check to disable push notifications for this folder." msgstr "" @@ -1528,12 +1567,7 @@ msgstr "" msgid "Choose your preferred interface theme." msgstr "" -#. Label of the cleanup_section (Section Break) field in DocType 'Mail Cluster' -#: mail/server/doctype/mail_cluster/mail_cluster.json -msgid "Cleanup" -msgstr "" - -#: frontend/src/pages/MailboxView.vue:70 +#: frontend/src/pages/MailboxView.vue:69 msgid "Clear All (Esc)" msgstr "" @@ -1541,38 +1575,42 @@ msgstr "" msgid "Clear All Mails" msgstr "" -#: mail/client/doctype/user_settings/user_settings.js:63 +#: mail/client/doctype/account_settings/account_settings.js:56 msgid "Clear Cache" msgstr "" -#: frontend/src/components/Modals/SearchModal.vue:90 +#: frontend/src/components/Modals/SearchModal.vue:93 msgid "Clear Filters" msgstr "" -#: mail/client/doctype/user_settings/user_settings.js:41 -msgid "Clearing Cached Blobs..." +#: mail/client/doctype/user_settings/user_settings.js:13 +msgid "Clear JMAP Session" msgstr "" -#: mail/client/doctype/user_settings/user_settings.js:53 -msgid "Clearing Cached Contact Cards..." +#: mail/client/doctype/account_settings/account_settings.js:34 +msgid "Clearing Cached Blobs..." msgstr "" -#: mail/client/doctype/user_settings/user_settings.js:23 -msgid "Clearing Cached JMAP Connection..." +#: mail/client/doctype/account_settings/account_settings.js:46 +msgid "Clearing Cached Contact Cards..." msgstr "" -#: mail/client/doctype/user_settings/user_settings.js:29 +#: mail/client/doctype/account_settings/account_settings.js:22 msgid "Clearing Cached JMAP Identities..." msgstr "" -#: mail/client/doctype/user_settings/user_settings.js:35 +#: mail/client/doctype/account_settings/account_settings.js:28 msgid "Clearing Cached JMAP Mailboxes..." msgstr "" -#: mail/client/doctype/user_settings/user_settings.js:47 +#: mail/client/doctype/account_settings/account_settings.js:40 msgid "Clearing Cached Mail Messages..." msgstr "" +#: mail/client/doctype/user_settings/user_settings.js:30 +msgid "Clearing JMAP Session..." +msgstr "" + #: frontend/src/components/DNSRecords.vue:22 msgid "Click to copy" msgstr "" @@ -1604,28 +1642,13 @@ msgstr "" msgid "Cluster File" msgstr "" -#. Label of the cluster_ids_section (Section Break) field in DocType 'Mail -#. Cluster Store' -#: mail/server/doctype/mail_cluster_store/mail_cluster_store.json -msgid "Cluster IDs" -msgstr "" - -#. Label of the cluster_settings_section (Section Break) field in DocType 'Mail -#. Cluster Store' -#. Label of the cluster_settings_section (Section Break) field in DocType 'Mail -#. Server' -#: mail/server/doctype/mail_cluster_store/mail_cluster_store.json -#: mail/server/doctype/mail_server/mail_server.json -msgid "Cluster Settings" -msgstr "" - #: frontend/src/components/CopyCode.vue:25 msgid "Code copied to clipboard" msgstr "" #. Label of the color (Color) field in DocType 'Calendar' #. Label of the color (Select) field in DocType 'Mailbox Settings' -#: frontend/src/components/Modals/FolderModal.vue:33 +#: frontend/src/components/Modals/FolderModal.vue:34 #: mail/client/doctype/calendar/calendar.json #: mail/client/doctype/mailbox_settings/mailbox_settings.json msgid "Color" @@ -1642,7 +1665,7 @@ msgstr "" msgid "Color assigned to the mailbox for easy identification and UI customization." msgstr "" -#: frontend/src/pages/MailboxView.vue:571 +#: frontend/src/pages/MailboxView.vue:575 msgid "Color scheme updated to {0}." msgstr "" @@ -1714,72 +1737,51 @@ msgstr "" msgid "Compose New Mail" msgstr "" -#. Label of the compression (Select) field in DocType 'Mail Cluster Store' -#: mail/server/doctype/mail_cluster_store/mail_cluster_store.json -msgid "Compression" -msgstr "" - -#. Label of the jmap_protocol_request_max_concurrent (Int) field in DocType -#. 'Mail Cluster' +#. Label of the configs_tab (Tab Break) field in DocType 'Mail Cluster' #: mail/server/doctype/mail_cluster/mail_cluster.json -msgid "Concurrent" -msgstr "" - -#. Label of the config_toml (Password) field in DocType 'Server Config' -#. Label of the config (Code) field in DocType 'Server Config' -#. Label of the config (Link) field in DocType 'Server Deployment' -#: mail/server/doctype/server_config/server_config.json -#: mail/server/doctype/server_deployment/server_deployment.json -msgid "Config" -msgstr "" - -#. Label of the config_checksum (Small Text) field in DocType 'Server -#. Deployment' -#: mail/server/doctype/server_deployment/server_deployment.json -msgid "Config Checksum" -msgstr "" - -#: mail/server/doctype/server_deployment/server_deployment.py:133 -msgid "Config does not belong to the selected server." +msgid "Configs" msgstr "" -#: mail/server/doctype/server_deployment/server_deployment.py:129 -msgid "Config is required." +#: frontend/src/components/AppSidebar.vue:293 +#: frontend/src/components/Settings/FolderSettings.vue:79 +msgid "Configure" msgstr "" -#. Label of the configuration_section (Section Break) field in DocType 'Mail -#. Cluster Store' -#: mail/server/doctype/mail_cluster_store/mail_cluster_store.json -msgid "Configuration" +#. Description of the 'Blob Store' (Link) field in DocType 'Mail Cluster' +#: mail/server/doctype/mail_cluster/mail_cluster.json +msgid "Configures the blob store backend." msgstr "" -#: mail/server/doctype/server_config/server_config.py:148 -msgid "Configuration updated successfully, but the response from the server was unexpected: {response}" +#. Description of the 'In-Memory Store' (Link) field in DocType 'Mail Cluster' +#: mail/server/doctype/mail_cluster/mail_cluster.json +msgid "Configures the in-memory store backend." msgstr "" -#: mail/server/doctype/server_config/server_config.py:142 -msgid "Configuration updated successfully." +#. Description of the 'Data Store' (Link) field in DocType 'Mail Cluster' +#: mail/server/doctype/mail_cluster/mail_cluster.json +msgid "Configures the primary data store backend." msgstr "" -#: frontend/src/components/AppSidebar.vue:215 -msgid "Configure" +#. Description of the 'Search Store' (Link) field in DocType 'Mail Cluster' +#: mail/server/doctype/mail_cluster/mail_cluster.json +msgid "Configures the search store backend." msgstr "" #: frontend/src/components/Modals/ChangePasswordModal.vue:54 #: frontend/src/components/Modals/DeleteFolderModal.vue:11 #: frontend/src/components/Modals/DeleteSieveScriptModal.vue:9 #: frontend/src/components/Modals/SetSieveScriptStateModal.vue:32 -#: frontend/src/components/Settings/BlockListSettings.vue:95 -#: frontend/src/pages/AddressBookView.vue:254 -#: frontend/src/pages/AddressBookView.vue:261 -#: frontend/src/pages/ContactView.vue:288 -#: frontend/src/pages/ContactView.vue:298 -#: frontend/src/pages/ContactView.vue:319 -#: frontend/src/pages/ContactView.vue:340 -#: frontend/src/pages/ContactView.vue:361 -#: frontend/src/pages/ContactsView.vue:135 -#: frontend/src/pages/MailboxView.vue:944 -#: frontend/src/pages/MailboxView.vue:972 +#: frontend/src/components/Settings/BlockListSettings.vue:99 +#: frontend/src/pages/AddressBookView.vue:257 +#: frontend/src/pages/AddressBookView.vue:264 +#: frontend/src/pages/ContactView.vue:291 +#: frontend/src/pages/ContactView.vue:301 +#: frontend/src/pages/ContactView.vue:322 +#: frontend/src/pages/ContactView.vue:343 +#: frontend/src/pages/ContactView.vue:364 +#: frontend/src/pages/ContactsView.vue:150 +#: frontend/src/pages/MailboxView.vue:976 +#: frontend/src/pages/MailboxView.vue:1004 #: frontend/src/pages/ResetPasswordView.vue:23 #: frontend/src/pages/dashboard/DomainView.vue:219 #: frontend/src/pages/dashboard/InvitesView.vue:186 @@ -1799,15 +1801,9 @@ msgstr "" msgid "Confirmed" msgstr "" -#. Description of the 'Timeout (Seconds)' (Int) field in DocType 'Mail Cluster -#. Store' +#. Description of the 'Timeout' (Data) field in DocType 'Mail Cluster Store' #: mail/server/doctype/mail_cluster_store/mail_cluster_store.json -msgid "Connection timeout to the database." -msgstr "" - -#. Option for the 'Method' (Select) field in DocType 'Mail Cluster Trace' -#: mail/server/doctype/mail_cluster_trace/mail_cluster_trace.json -msgid "Console" +msgid "Connection timeout to the store." msgstr "" #. Name of a DocType @@ -1827,8 +1823,8 @@ msgstr "" msgid "Contact Card Address Book" msgstr "" -#: mail/client/doctype/contact_card/contact_card.py:241 -#: mail/client/doctype/contact_card/contact_card.py:264 +#: mail/client/doctype/contact_card/contact_card.py:246 +#: mail/client/doctype/contact_card/contact_card.py:269 msgid "Contact Card Creation Error" msgstr "" @@ -1837,7 +1833,12 @@ msgstr "" msgid "Contact Card Email" msgstr "" -#: mail/client/doctype/contact_card/contact_card.py:107 +#. Label of the id (Data) field in DocType 'Contact Card' +#: mail/client/doctype/contact_card/contact_card.json +msgid "Contact Card ID" +msgstr "" + +#: mail/client/doctype/contact_card/contact_card.py:111 msgid "Contact Card Not Found" msgstr "" @@ -1846,20 +1847,20 @@ msgstr "" msgid "Contact Card Phone" msgstr "" -#: mail/client/doctype/contact_card/contact_card.py:352 -#: mail/client/doctype/contact_card/contact_card.py:386 +#: mail/client/doctype/contact_card/contact_card.py:361 +#: mail/client/doctype/contact_card/contact_card.py:395 msgid "Contact Card Update Error" msgstr "" -#: mail/client/doctype/contact_card/contact_card.py:106 -msgid "Contact Card with ID {0} not found in user {1}." +#: mail/client/doctype/contact_card/contact_card.py:108 +msgid "Contact Card with ID {0} not found in account {1}." msgstr "" -#: mail/client/doctype/user_settings/user_settings.js:51 +#: mail/client/doctype/account_settings/account_settings.js:44 msgid "Contact Cards" msgstr "" -#: mail/client/doctype/contact_card/contact_card.py:210 +#: mail/client/doctype/contact_card/contact_card.py:215 msgid "Contact Cards deleted successfully." msgstr "" @@ -1867,46 +1868,37 @@ msgstr "" msgid "Contact Email" msgstr "" -#. Label of the contact (Small Text) field in DocType 'Mail Server ACME -#. Provider' -#: mail/server/doctype/mail_server_acme_provider/mail_server_acme_provider.json -msgid "Contact Emails" -msgstr "" - -#: frontend/src/components/Modals/AddContactModal.vue:76 +#: frontend/src/components/Modals/AddContactModal.vue:78 msgid "Contact created." msgstr "" -#: frontend/src/pages/ContactView.vue:275 +#: frontend/src/pages/ContactView.vue:278 msgid "Contact deleted." msgstr "" -#: frontend/src/pages/ContactView.vue:262 +#: frontend/src/pages/ContactView.vue:265 msgid "Contact updated." msgstr "" #. Label of the contacts_section (Section Break) field in DocType 'User #. Settings' -#. Label of the jmap_contact_parse_max_items (Int) field in DocType 'Mail -#. Cluster' -#: frontend/src/components/AppSidebar.vue:270 +#: frontend/src/components/AppSidebar.vue:348 #: frontend/src/pages/AddressBookView.vue:30 -#: frontend/src/pages/ContactView.vue:382 frontend/src/pages/ContactsView.vue:3 -#: frontend/src/pages/ContactsView.vue:62 +#: frontend/src/pages/ContactView.vue:385 frontend/src/pages/ContactsView.vue:3 +#: frontend/src/pages/ContactsView.vue:65 #: mail/client/doctype/user_settings/user_settings.json -#: mail/server/doctype/mail_cluster/mail_cluster.json msgid "Contacts" msgstr "" -#: frontend/src/pages/AddressBookView.vue:223 +#: frontend/src/pages/AddressBookView.vue:226 msgid "Contacts added." msgstr "" -#: frontend/src/pages/ContactsView.vue:110 +#: frontend/src/pages/ContactsView.vue:122 msgid "Contacts deleted." msgstr "" -#: frontend/src/pages/AddressBookView.vue:241 +#: frontend/src/pages/AddressBookView.vue:244 msgid "Contacts removed." msgstr "" @@ -1960,33 +1952,21 @@ msgstr "" msgid "Copy to Clipboard" msgstr "" -#. Description of the 'Data Storage' (Section Break) field in DocType 'Mail -#. Cluster' -#: mail/server/doctype/mail_cluster/mail_cluster.json -msgid "Core storage unit where email metadata, folders, and various settings are stored. Essentially, it contains all the data except for large binary objects (blobs)." -msgstr "" - #: mail/server/doctype/spam_check_log/spam_check_log.py:134 msgid "Could not connect to SpamAssassin (spamd). Please ensure it's running on {0}:{1}" msgstr "" -#: mail/server/doctype/mail_server/mail_server.py:182 +#: mail/server/doctype/mail_server/mail_server.py:99 msgid "Could not connect to {0}:{1}. Error: {2}" msgstr "" -#: mail/server/doctype/dns_record/dns_record.py:114 +#: mail/server/doctype/dns_record/dns_record.py:116 msgid "Could not verify {0}:{1} record." msgstr "" -#. Label of the count (Int) field in DocType 'DMARC Report Detail' -#: mail/server/doctype/dmarc_report_detail/dmarc_report_detail.json -#: mail/server/report/dmarc_report_viewer/dmarc_report_viewer.py:39 -msgid "Count" -msgstr "" - #. Label of the country (Data) field in DocType 'Contact Card Address' #: frontend/src/components/Modals/AddContactAddressModal.vue:16 -#: frontend/src/pages/ContactView.vue:406 +#: frontend/src/pages/ContactView.vue:409 #: mail/client/doctype/contact_card_address/contact_card_address.json msgid "Country" msgstr "" @@ -2030,9 +2010,7 @@ msgid "Created (UTC)" msgstr "" #. Label of the created_at (Data) field in DocType 'Contact Card' -#. Label of the created_at (Datetime) field in DocType 'Message Queue' #: mail/client/doctype/contact_card/contact_card.json -#: mail/server/doctype/message_queue/message_queue.json msgid "Created At" msgstr "" @@ -2041,7 +2019,7 @@ msgstr "" msgid "Created On" msgstr "" -#: mail/api/mail.py:428 +#: mail/api/mail.py:423 msgid "Created at" msgstr "" @@ -2068,12 +2046,12 @@ msgstr "" msgid "Current Password" msgstr "" -#. Label of the email_current_state (Data) field in DocType 'User Settings' -#: mail/client/doctype/user_settings/user_settings.json +#. Label of the email_current_state (Data) field in DocType 'Account Settings' +#: mail/client/doctype/account_settings/account_settings.json msgid "Current State (Email)" msgstr "" -#: frontend/src/components/AppSidebar.vue:279 +#: frontend/src/components/AppSidebar.vue:357 msgid "Custom" msgstr "" @@ -2092,51 +2070,35 @@ msgstr "" #. Label of the dkim_pass (Check) field in DocType 'Mail Message' #. Option for the 'Category' (Select) field in DocType 'Principal DNS Record' -#: mail/api/mail.py:447 mail/client/doctype/mail_message/mail_message.json +#: mail/api/mail.py:442 mail/client/doctype/mail_message/mail_message.json #: mail/server/doctype/principal_dns_record/principal_dns_record.json msgid "DKIM" msgstr "" -#. Label of the dkim_alignment (Data) field in DocType 'DMARC Report' -#: mail/server/doctype/dmarc_report/dmarc_report.json -msgid "DKIM Alignment" -msgstr "" - #. Label of the dkim_description (Small Text) field in DocType 'Mail Message' #: mail/client/doctype/mail_message/mail_message.json msgid "DKIM Description" msgstr "" -#: mail/server/doctype/principal/principal.py:275 +#: mail/server/doctype/principal/principal.py:273 msgid "DKIM Keys can only be rotated for Domain principals." msgstr "" -#: mail/server/doctype/principal/principal.py:292 +#: mail/server/doctype/principal/principal.py:290 msgid "DKIM Keys rotated successfully." msgstr "" -#. Label of the dkim_result (Data) field in DocType 'DMARC Report Detail' -#: mail/server/doctype/dmarc_report_detail/dmarc_report_detail.json -#: mail/server/report/dmarc_report_viewer/dmarc_report_viewer.py:44 -msgid "DKIM Result" -msgstr "" - -#. Label of the dkim_results (JSON) field in DocType 'DMARC Report Detail' -#: mail/server/doctype/dmarc_report_detail/dmarc_report_detail.json -msgid "DKIM Results" -msgstr "" - -#: mail/server/doctype/principal/principal.py:381 +#: mail/server/doctype/principal/principal.py:372 msgid "DKIM signature can only be created for Domain principals." msgstr "" -#: mail/server/doctype/principal/principal.py:673 +#: mail/server/doctype/principal/principal.py:622 msgid "DKIM signature can only be deleted for Domain principals." msgstr "" #. Label of the dmarc_pass (Check) field in DocType 'Mail Message' #. Option for the 'Category' (Select) field in DocType 'Principal DNS Record' -#: mail/api/mail.py:449 mail/client/doctype/mail_message/mail_message.json +#: mail/api/mail.py:444 mail/client/doctype/mail_message/mail_message.json #: mail/server/doctype/principal_dns_record/principal_dns_record.json msgid "DMARC" msgstr "" @@ -2146,27 +2108,11 @@ msgstr "" msgid "DMARC Description" msgstr "" -#. Name of a DocType #. Label of a Workspace Sidebar Item -#: mail/server/doctype/dmarc_report/dmarc_report.json #: mail/workspace_sidebar/server.json msgid "DMARC Report" msgstr "" -#. Name of a DocType -#: mail/server/doctype/dmarc_report_detail/dmarc_report_detail.json -msgid "DMARC Report Detail" -msgstr "" - -#. Name of a report -#: mail/server/report/dmarc_report_viewer/dmarc_report_viewer.json -msgid "DMARC Report Viewer" -msgstr "" - -#: mail/server/doctype/dmarc_report/dmarc_report.py:34 -msgid "DMARC report removed successfully." -msgstr "" - #. Label of the dns_provider (Select) field in DocType 'Mail Settings' #. Label of the dns_provider_section (Section Break) field in DocType 'Mail #. Settings' @@ -2181,7 +2127,7 @@ msgstr "" msgid "DNS Record" msgstr "" -#: mail/server/doctype/dns_record/dns_record.py:57 +#: mail/server/doctype/dns_record/dns_record.py:59 msgid "DNS Record with the same host and type already exists." msgstr "" @@ -2193,23 +2139,23 @@ msgstr "" msgid "DNS Records" msgstr "" -#: mail/server/doctype/principal/principal.py:262 +#: mail/server/doctype/principal/principal.py:260 msgid "DNS Records can only be refreshed for Domain principals." msgstr "" -#: mail/server/doctype/principal/principal.py:229 +#: mail/server/doctype/principal/principal.py:227 msgid "DNS Records can only be verified for Domain principals." msgstr "" -#: mail/server/doctype/principal/principal.py:469 +#: mail/server/doctype/principal/principal.py:426 msgid "DNS Records for principal {0} not found in backend." msgstr "" -#: mail/server/doctype/principal/principal.py:266 +#: mail/server/doctype/principal/principal.py:264 msgid "DNS Records refreshed successfully." msgstr "" -#: mail/server/doctype/principal/principal.py:247 +#: mail/server/doctype/principal/principal.py:245 msgid "DNS Records verified successfully." msgstr "" @@ -2221,7 +2167,7 @@ msgstr "" msgid "DNS provider or token not configured. Please manually add the {0} to the DNS provider for the domain {1}." msgstr "" -#: mail/server/doctype/principal/principal.js:62 +#: mail/server/doctype/principal/principal.js:54 msgid "DNS records for the domain {0} are not verified. Please ensure that the DNS records are correctly configured with your DNS provider." msgstr "" @@ -2233,21 +2179,15 @@ msgstr "" msgid "DNS verification failed." msgstr "" -#. Option for the 'Rotate Frequency' (Select) field in DocType 'Mail Cluster -#. Trace' -#: mail/server/doctype/mail_cluster_trace/mail_cluster_trace.json -msgid "Daily" -msgstr "" - #. Option for the 'Color Scheme' (Select) field in DocType 'User Settings' #: frontend/src/components/Settings/AppearanceSettings.vue:76 #: mail/client/doctype/user_settings/user_settings.json msgid "Dark Mode" msgstr "" -#. Label of the datacenter (Data) field in DocType 'Mail Cluster Store' -#: mail/server/doctype/mail_cluster_store/mail_cluster_store.json -msgid "Data Center ID" +#. Label of the data_store_config (JSON) field in DocType 'Mail Cluster' +#: mail/server/doctype/mail_cluster/mail_cluster.json +msgid "Data Config" msgstr "" #. Label of a Workspace Sidebar Item @@ -2255,17 +2195,23 @@ msgstr "" msgid "Data Exchange" msgstr "" -#. Label of the data_storage_section (Section Break) field in DocType 'Mail -#. Cluster' -#: mail/server/doctype/mail_cluster/mail_cluster.json -#: mail/server/doctype/mail_cluster/mail_cluster.py:307 +#: mail/server/doctype/mail_cluster/mail_cluster.py:235 msgid "Data Storage" msgstr "" -#. Description of the 'Data Center ID' (Data) field in DocType 'Mail Cluster +#. Label of the data_store (Link) field in DocType 'Mail Cluster' +#: mail/server/doctype/mail_cluster/mail_cluster.json +msgid "Data Store" +msgstr "" + +#: mail/server/doctype/mail_cluster/mail_cluster.py:124 +msgid "Data Store is required." +msgstr "" + +#. Description of the 'Datacenter ID' (Data) field in DocType 'Mail Cluster #. Store' #: mail/server/doctype/mail_cluster_store/mail_cluster_store.json -msgid "Data center ID of the FoundationDB cluster." +msgid "Data center ID." msgstr "" #. Label of the database (Data) field in DocType 'Mail Cluster Store' @@ -2273,8 +2219,13 @@ msgstr "" msgid "Database" msgstr "" -#: frontend/src/components/MailDetails.vue:52 -msgid "Date: " +#. Label of the datacenter_id (Data) field in DocType 'Mail Cluster Store' +#: mail/server/doctype/mail_cluster_store/mail_cluster_store.json +msgid "Datacenter ID" +msgstr "" + +#: frontend/src/components/MailDetails.vue:52 +msgid "Date: " msgstr "" #. Option for the 'Group Messages By' (Select) field in DocType 'User Settings' @@ -2288,12 +2239,6 @@ msgstr "" msgid "Deactivate" msgstr "" -#. Option for the 'Logging Level' (Select) field in DocType 'Mail Cluster -#. Trace' -#: mail/server/doctype/mail_cluster_trace/mail_cluster_trace.json -msgid "Debug" -msgstr "" - #: mail/client/doctype/push_subscription/push_subscription.py:429 msgid "Decrypted payload exceeds maximum allowed size." msgstr "" @@ -2302,12 +2247,6 @@ msgstr "" msgid "Decrypted push payload is not valid JSON." msgstr "" -#. Description of the 'Search Store' (Section Break) field in DocType 'Mail -#. Cluster' -#: mail/server/doctype/mail_cluster/mail_cluster.json -msgid "Dedicated to indexing for full-text search, enhancing the speed and efficiency of text-based queries." -msgstr "" - #. Label of the deduplicate_export (Check) field in DocType 'Mail Exchange' #: mail/client/doctype/mail_exchange/mail_exchange.json msgid "Deduplicate by Message-ID" @@ -2316,37 +2255,34 @@ msgstr "" #. Label of the default (Check) field in DocType 'Address Book' #. Label of the default (Check) field in DocType 'Calendar' #. Label of the default (Check) field in DocType 'Participant Identity' -#. Label of the default (Check) field in DocType 'Mail Server ACME Provider' -#. Label of the default (Check) field in DocType 'Mail Server TLS Certificate' +#. Option for the 'Type' (Select) field in DocType 'Mail Cluster Store' #. Option for the 'Value' (Select) field in DocType 'Principal Permission' -#: frontend/src/components/AppSidebar.vue:278 +#: frontend/src/components/AppSidebar.vue:356 #: frontend/src/pages/AddressBookView.vue:5 #: frontend/src/pages/AddressBooksView.vue:35 #: mail/client/doctype/address_book/address_book.json #: mail/client/doctype/calendar/calendar.json #: mail/client/doctype/participant_identity/participant_identity.json -#: mail/server/doctype/mail_server_acme_provider/mail_server_acme_provider.json -#: mail/server/doctype/mail_server_tls_certificate/mail_server_tls_certificate.json +#: mail/server/doctype/mail_cluster_store/mail_cluster_store.json #: mail/server/doctype/principal_permission/principal_permission.json msgid "Default" msgstr "" +#. Label of the default_domain (Data) field in DocType 'Mail Cluster' +#: mail/server/doctype/mail_cluster/mail_cluster.json +msgid "Default Domain" +msgstr "" + #. Label of the default_outgoing_email (Data) field in DocType 'User Settings' #: mail/client/doctype/user_settings/user_settings.json msgid "Default Email" msgstr "" -#. Label of the storage_search_index_default_language (Data) field in DocType -#. 'Mail Cluster' -#: mail/server/doctype/mail_cluster/mail_cluster.json -msgid "Default Language" -msgstr "" - #: frontend/src/components/Settings/AccountSettings.vue:7 msgid "Default Outgoing Email" msgstr "" -#: mail/client/doctype/user_settings/user_settings.py:125 +#: mail/client/doctype/user_settings/user_settings.py:103 msgid "Default Outgoing Email {0} is not found in the identities of the JMAP account." msgstr "" @@ -2359,16 +2295,6 @@ msgstr "" msgid "Default email address used as the sender when composing new messages." msgstr "" -#. Description of the 'Default Language' (Data) field in DocType 'Mail Cluster' -#: mail/server/doctype/mail_cluster/mail_cluster.json -msgid "Default language to use when language detection is not possible." -msgstr "" - -#. Description of the 'Listeners' (Table) field in DocType 'Mail Server' -#: mail/server/doctype/mail_server/mail_server.json -msgid "Define custom listeners for this mail server. If not set, the cluster’s listeners will be used." -msgstr "" - #. Description of the 'Type' (Select) field in DocType 'Event Alert' #: mail/client/doctype/event_alert/event_alert.json msgid "Defines how the alert time is calculated." @@ -2515,16 +2441,6 @@ msgid "" "" msgstr "" -#. Description of the 'Max Size (Bytes)' (Int) field in DocType 'Mail Cluster' -#: mail/server/doctype/mail_cluster/mail_cluster.json -msgid "Defines the maximum file size for file uploads to the server." -msgstr "" - -#. Description of the 'Size (Bytes)' (Int) field in DocType 'Mail Cluster' -#: mail/server/doctype/mail_cluster/mail_cluster.json -msgid "Defines the maximum size of a single request, in bytes, that the server will accept." -msgstr "" - #. Description of the 'Roles' (JSON) field in DocType 'Event Participant' #: mail/client/doctype/event_participant/event_participant.json msgid "" @@ -2555,16 +2471,12 @@ msgstr "" msgid "Defines the sort order of calendars when presented in the client's UI." msgstr "" -#. Description of the 'Total Size (MB)' (Int) field in DocType 'Mail Cluster' -#: mail/server/doctype/mail_cluster/mail_cluster.json -msgid "Defines the total size of files that a user can upload within a certain period." -msgstr "" - -#: frontend/src/components/AppSidebar.vue:223 +#: frontend/src/components/AppSidebar.vue:301 #: frontend/src/components/Settings/AutomationSettings.vue:109 +#: frontend/src/components/Settings/FolderSettings.vue:84 #: frontend/src/components/Settings/SignatureSettings.vue:88 -#: frontend/src/pages/AddressBookView.vue:275 -#: frontend/src/pages/ContactView.vue:411 +#: frontend/src/pages/AddressBookView.vue:278 +#: frontend/src/pages/ContactView.vue:414 #: frontend/src/pages/ContactsView.vue:29 #: frontend/src/pages/dashboard/InvitesView.vue:70 #: frontend/src/pages/dashboard/MailingListView.vue:7 @@ -2574,15 +2486,15 @@ msgstr "" msgid "Delete" msgstr "" -#: frontend/src/pages/AddressBookView.vue:251 +#: frontend/src/pages/AddressBookView.vue:254 msgid "Delete Address Book" msgstr "" -#: frontend/src/pages/ContactView.vue:285 +#: frontend/src/pages/ContactView.vue:288 msgid "Delete Contact" msgstr "" -#: frontend/src/pages/ContactsView.vue:132 +#: frontend/src/pages/ContactsView.vue:147 msgid "Delete Contacts" msgstr "" @@ -2619,7 +2531,7 @@ msgstr "" msgid "Delete Members" msgstr "" -#: frontend/src/components/MailActions.vue:177 +#: frontend/src/components/MailActions.vue:178 msgid "Delete Message" msgstr "" @@ -2627,7 +2539,7 @@ msgstr "" msgid "Delete Newsletter After Sending" msgstr "" -#: frontend/src/pages/MailboxView.vue:33 +#: frontend/src/pages/MailboxView.vue:38 msgid "Delete Now" msgstr "" @@ -2635,38 +2547,38 @@ msgstr "" msgid "Delete Sieve Script" msgstr "" -#: frontend/src/components/MailListItem.vue:288 +#: frontend/src/components/MailListItem.vue:292 msgid "Delete Thread" msgstr "" -#: frontend/src/components/MailThread.vue:497 +#: frontend/src/components/MailThread.vue:584 msgid "Delete Thread (Shift+Delete)" msgstr "" -#: frontend/src/pages/MailboxView.vue:686 +#: frontend/src/pages/MailboxView.vue:690 msgid "Delete Threads (Shift+Delete)" msgstr "" -#: frontend/src/pages/MailboxView.vue:922 +#: frontend/src/pages/MailboxView.vue:954 msgid "Delete {0} {1}" msgstr "" -#: mail/client/doctype/address_book/address_book_list.js:15 -#: mail/client/doctype/calendar/calendar_list.js:15 -#: mail/client/doctype/calendar_event/calendar_event_list.js:24 -#: mail/client/doctype/contact_card/contact_card_list.js:15 -#: mail/client/doctype/event_notification/event_notification_list.js:15 -#: mail/client/doctype/identity/identity_list.js:15 -#: mail/client/doctype/mail_message/mail_message_list.js:15 -#: mail/client/doctype/mailbox/mailbox_list.js:15 -#: mail/client/doctype/participant_identity/participant_identity_list.js:15 +#: mail/client/doctype/address_book/address_book_list.js:16 +#: mail/client/doctype/calendar/calendar_list.js:16 +#: mail/client/doctype/calendar_event/calendar_event_list.js:25 +#: mail/client/doctype/contact_card/contact_card_list.js:16 +#: mail/client/doctype/event_notification/event_notification_list.js:16 +#: mail/client/doctype/identity/identity_list.js:16 +#: mail/client/doctype/mail_message/mail_message_list.js:16 +#: mail/client/doctype/mailbox/mailbox_list.js:16 +#: mail/client/doctype/participant_identity/participant_identity_list.js:16 #: mail/client/doctype/push_subscription/push_subscription_list.js:15 -#: mail/client/doctype/sieve_script/sieve_script_list.js:15 +#: mail/client/doctype/sieve_script/sieve_script_list.js:16 msgid "Delete {0} {1} permanently?" msgstr "" -#: frontend/src/components/MailActions.vue:270 -#: frontend/src/pages/MailboxView.vue:1080 +#: frontend/src/components/MailActions.vue:277 +#: frontend/src/pages/MailboxView.vue:1112 msgid "Deleting..." msgstr "" @@ -2675,32 +2587,11 @@ msgstr "" msgid "Delivery Mode" msgstr "" -#: mail/server/doctype/message_queue/message_queue.py:70 -msgid "Delivery cancelled successfully." -msgstr "" - -#: mail/server/doctype/message_queue/message_queue.py:58 -#: mail/server/doctype/message_queue/message_queue.py:241 -msgid "Delivery retried successfully." -msgstr "" - #. Label of the depends_on (JSON) field in DocType 'Server Deployment Service' #: mail/server/doctype/server_deployment_service/server_deployment_service.json msgid "Depends On" msgstr "" -#: mail/server/doctype/server_config/server_config.js:14 -msgid "Deploy" -msgstr "" - -#: mail/server/doctype/server_config/server_config.py:104 -msgid "Deploy of Stalwart initiated." -msgstr "" - -#: mail/server/doctype/server_config/server_config.js:39 -msgid "Deploying Configuration..." -msgstr "" - #. Group in Mail Server's connections #. Label of the deployment (Link) field in DocType 'Server Ansible Play' #: mail/server/doctype/mail_server/mail_server.json @@ -2708,6 +2599,11 @@ msgstr "" msgid "Deployment" msgstr "" +#. Label of the depth (Int) field in DocType 'Mail Cluster Store' +#: mail/server/doctype/mail_cluster_store/mail_cluster_store.json +msgid "Depth" +msgstr "" + #. Description of the 'Type' (Select) field in DocType 'Event Notification' #: mail/client/doctype/event_notification/event_notification.json msgid "Describes the nature of the event change." @@ -2718,6 +2614,9 @@ msgstr "" #. Label of the description (Small Text) field in DocType 'Calendar Event' #. Label of the description (Small Text) field in DocType 'Event Participant' #. Label of the description (Small Text) field in DocType 'Quota' +#. Label of the description (Small Text) field in DocType 'Mail Cluster Store' +#. Label of the description (Small Text) field in DocType 'Mail Cluster Store +#. HTTP Auth' #. Label of the description (Small Text) field in DocType 'Principal' #. Label of the description (Data) field in DocType 'Principal Permission' #: frontend/src/components/Modals/AddAddressBookModal.vue:26 @@ -2733,11 +2632,25 @@ msgstr "" #: mail/client/doctype/calendar_event/calendar_event.json #: mail/client/doctype/event_participant/event_participant.json #: mail/client/doctype/quota/quota.json +#: mail/server/doctype/mail_cluster_store/mail_cluster_store.json +#: mail/server/doctype/mail_cluster_store_http_auth/mail_cluster_store_http_auth.json #: mail/server/doctype/principal/principal.json #: mail/server/doctype/principal_permission/principal_permission.json msgid "Description" msgstr "" +#. Description of the 'Description' (Small Text) field in DocType 'Mail Cluster +#. Store HTTP Auth' +#: mail/server/doctype/mail_cluster_store_http_auth/mail_cluster_store_http_auth.json +msgid "Description for the HTTP auth." +msgstr "" + +#. Description of the 'Description' (Small Text) field in DocType 'Mail Cluster +#. Store' +#: mail/server/doctype/mail_cluster_store/mail_cluster_store.json +msgid "Description for the backend store." +msgstr "" + #. Description of the 'Description' (Small Text) field in DocType 'Quota' #: mail/client/doctype/quota/quota.json msgid "Description of this quota." @@ -2772,28 +2685,6 @@ msgstr "" msgid "Details" msgstr "" -#. Description of the 'Renew Before (Days)' (Int) field in DocType 'Mail Server -#. ACME Provider' -#: mail/server/doctype/mail_server_acme_provider/mail_server_acme_provider.json -msgid "Determines how early before expiration the certificate should be renewed." -msgstr "" - -#. Description of the 'Changes' (Int) field in DocType 'Mail Cluster' -#: mail/server/doctype/mail_cluster/mail_cluster.json -msgid "Determines the maximum number of change objects that a Changes method can return." -msgstr "" - -#. Description of the 'Get' (Int) field in DocType 'Mail Cluster' -#: mail/server/doctype/mail_cluster/mail_cluster.json -msgid "Determines the maximum number of objects that can be fetched in a single method call." -msgstr "" - -#. Description of the 'E-mail Size (Bytes)' (Int) field in DocType 'Mail -#. Cluster' -#: mail/server/doctype/mail_cluster/mail_cluster.json -msgid "Determines the maximum size for an email message." -msgstr "" - #. Label of the device_client_id (Data) field in DocType 'Push Subscription' #: mail/client/doctype/push_subscription/push_subscription.json msgid "Device Client ID" @@ -2804,24 +2695,10 @@ msgstr "" msgid "DigitalOcean" msgstr "" -#. Label of the directory_id (Data) field in DocType 'Mail Server ACME -#. Provider' -#: mail/server/doctype/mail_server_acme_provider/mail_server_acme_provider.json -msgid "Directory ID" -msgstr "" - -#. Label of the directory_storage_section (Section Break) field in DocType -#. 'Mail Cluster' -#: mail/server/doctype/mail_cluster/mail_cluster.json -#: mail/server/doctype/mail_cluster/mail_cluster.py:306 +#: mail/server/doctype/mail_cluster/mail_cluster.py:234 msgid "Directory Storage" msgstr "" -#. Label of the directory (Data) field in DocType 'Mail Server ACME Provider' -#: mail/server/doctype/mail_server_acme_provider/mail_server_acme_provider.json -msgid "Directory URL" -msgstr "" - #: frontend/src/components/Modals/SetSieveScriptStateModal.vue:47 msgid "Disable" msgstr "" @@ -2832,15 +2709,10 @@ msgstr "" msgid "Disable Push Notification" msgstr "" -#: frontend/src/components/Modals/FolderModal.vue:46 +#: frontend/src/components/Modals/FolderModal.vue:47 msgid "Disable Push Notifications" msgstr "" -#. Description of the 'Outbound Only' (Check) field in DocType 'Mail Server' -#: mail/server/doctype/mail_server/mail_server.json -msgid "Disable email delivery to local domains and perform an MX lookup to route emails externally." -msgstr "" - #. Description of the 'Enabled' (Check) field in DocType 'Mail Cluster' #: mail/server/doctype/mail_cluster/mail_cluster.json msgid "Disable to stop sending and receiving emails form this cluster." @@ -2851,9 +2723,7 @@ msgstr "" msgid "Disable to stop this mail server from sending or receiving emails." msgstr "" -#. Option for the 'Transport' (Select) field in DocType 'Mail Cluster' #. Label of the disabled_permissions (JSON) field in DocType 'Principal' -#: mail/server/doctype/mail_cluster/mail_cluster.json #: mail/server/doctype/principal/principal.json msgid "Disabled" msgstr "" @@ -2896,10 +2766,7 @@ msgid "Display name of the Sieve script." msgstr "" #. Label of the disposition (Select) field in DocType 'Mail Message Part' -#. Label of the disposition (Data) field in DocType 'DMARC Report Detail' #: mail/client/doctype/mail_message_part/mail_message_part.json -#: mail/server/doctype/dmarc_report_detail/dmarc_report_detail.json -#: mail/server/report/dmarc_report_viewer/dmarc_report_viewer.py:40 msgid "Disposition" msgstr "" @@ -2922,28 +2789,16 @@ msgstr "" #: mail/server/doctype/mail_account_request/mail_account_request.json #: mail/server/doctype/principal/principal.json #: mail/server/doctype/principal_settings/principal_settings.json -#: mail/server/report/dmarc_report_viewer/dmarc_report_viewer.py:49 msgid "Domain" msgstr "" -#. Label of the domain_name (Data) field in DocType 'DMARC Report' #. Label of the domain_name (Data) field in DocType 'Mail Domain Request' -#. Label of the domain_name (Data) field in DocType 'Message Queue Recipient' #: frontend/src/components/Modals/AddDomainModal.vue:26 #: frontend/src/pages/SignupView.vue:20 -#: mail/server/doctype/dmarc_report/dmarc_report.json #: mail/server/doctype/mail_domain_request/mail_domain_request.json -#: mail/server/doctype/message_queue/message_queue.js:53 -#: mail/server/doctype/message_queue_recipient/message_queue_recipient.json -#: mail/server/report/dmarc_report_viewer/dmarc_report_viewer.py:31 msgid "Domain Name" msgstr "" -#. Label of the domain_policy (Data) field in DocType 'DMARC Report' -#: mail/server/doctype/dmarc_report/dmarc_report.json -msgid "Domain Policy" -msgstr "" - #. Label of a Workspace Sidebar Item #: mail/workspace_sidebar/server.json msgid "Domain Request" @@ -2969,6 +2824,10 @@ msgstr "" msgid "Domain settings updated." msgstr "" +#: mail/stalwart/domain.py:28 +msgid "Domain with ID {0} not found." +msgstr "" + #: mail/server/doctype/mail_domain_request/mail_domain_request.py:50 #: mail/server/doctype/mail_domain_request/mail_domain_request.py:90 msgid "Domain {0} already registered." @@ -2983,20 +2842,22 @@ msgid "Domain {0} is not allowed for signup." msgstr "" #: mail/mail/doctype/mail_settings/mail_settings.py:92 -#: mail/utils/validation.py:101 +#: mail/utils/validation.py:57 msgid "Domain {0} is not verified." msgstr "" -#: mail/utils/validation.py:109 +#: mail/stalwart/account.py:370 +msgid "Domain {0} not found for alias {1}." +msgstr "" + +#: mail/stalwart/account.py:351 mail/utils/validation.py:65 msgid "Domain {0} not found." msgstr "" -#. Label of the domains (JSON) field in DocType 'Message Queue' -#: frontend/src/components/AppSidebar.vue:184 +#: frontend/src/components/AppSidebar.vue:254 #: frontend/src/pages/dashboard/DomainView.vue:177 #: frontend/src/pages/dashboard/DomainsView.vue:3 #: frontend/src/pages/dashboard/DomainsView.vue:75 -#: mail/server/doctype/message_queue/message_queue.json msgid "Domains" msgstr "" @@ -3015,8 +2876,8 @@ msgstr "" #. Option for the 'Status' (Select) field in DocType 'Mail Exchange' #. Label of the draft (Check) field in DocType 'Mail Message' #. Option for the 'Status' (Select) field in DocType 'Mail Data Exchange' -#: frontend/src/components/MailListItem.vue:160 -#: frontend/src/components/MailThread.vue:120 +#: frontend/src/components/MailListItem.vue:164 +#: frontend/src/components/MailThread.vue:148 #: frontend/src/pages/MailExchangesView.vue:93 #: mail/client/doctype/calendar_event/calendar_event.json #: mail/client/doctype/event_notification/event_notification.json @@ -3026,11 +2887,11 @@ msgstr "" msgid "Draft" msgstr "" -#: frontend/src/components/ComposeMailEditor.vue:473 +#: frontend/src/components/ComposeMailEditor.vue:486 msgid "Draft discarded." msgstr "" -#: frontend/src/components/ComposeMailEditor.vue:438 +#: frontend/src/components/ComposeMailEditor.vue:449 msgid "Draft saved." msgstr "" @@ -3059,11 +2920,11 @@ msgstr "" msgid "Drop files to upload" msgstr "" -#: mail/client/doctype/mailbox_settings/mailbox_settings.py:37 +#: mail/client/doctype/mailbox_settings/mailbox_settings.py:42 msgid "Duplicate Mailbox Settings" msgstr "" -#: mail/server/doctype/dns_record/dns_record.py:58 +#: mail/server/doctype/dns_record/dns_record.py:60 msgid "Duplicate Record" msgstr "" @@ -3091,20 +2952,10 @@ msgstr "" msgid "Duration (Seconds)" msgstr "" -#: mail/client/doctype/calendar_event/calendar_event.py:232 +#: mail/client/doctype/calendar_event/calendar_event.py:236 msgid "Duration is required for non-draft events." msgstr "" -#. Label of the email (Data) field in DocType 'DMARC Report' -#: mail/server/doctype/dmarc_report/dmarc_report.json -msgid "E-mail" -msgstr "" - -#. Label of the jmap_email_max_size (Int) field in DocType 'Mail Cluster' -#: mail/server/doctype/mail_cluster/mail_cluster.json -msgid "E-mail Size (Bytes)" -msgstr "" - #. Description of the 'JMAP Push Private Key' (Password) field in DocType 'Mail #. Settings' #: mail/mail/doctype/mail_settings/mail_settings.json @@ -3125,7 +2976,7 @@ msgstr "" msgid "Edit" msgstr "" -#: frontend/src/components/MailActions.vue:93 +#: frontend/src/components/MailActions.vue:94 msgid "Edit Draft" msgstr "" @@ -3157,15 +3008,15 @@ msgstr "" msgid "Edit Signature" msgstr "" -#: mail/client/doctype/mail_queue/mail_queue.py:510 +#: mail/client/doctype/mail_queue/mail_queue.py:513 msgid "Either blob_id or file_url is required for attachments." msgstr "" -#: frontend/src/components/Modals/FolderModal.vue:106 +#: frontend/src/components/Modals/FolderModal.vue:107 msgid "Either condition is met" msgstr "" -#: mail/utils/__init__.py:443 +#: mail/utils/__init__.py:457 msgid "Either file path or file data is required." msgstr "" @@ -3191,17 +3042,16 @@ msgstr "" #. Label of the email (Data) field in DocType 'Identity' #. Label of the email (Data) field in DocType 'Mail Message Recipient' #. Label of the email (Data) field in DocType 'Participant Identity' -#. Label of the email (Data) field in DocType 'Message Queue Recipient' #. Label of the email (Data) field in DocType 'Principal Email' #: frontend/src/components/AddMailingListMemberInput.vue:5 #: frontend/src/components/IdentitySettingsListView.vue:55 -#: frontend/src/components/Modals/AddAddressBookContactsModal.vue:96 +#: frontend/src/components/Modals/AddAddressBookContactsModal.vue:103 #: frontend/src/components/Modals/AddMailingListExternalMemberModal.vue:20 #: frontend/src/components/Modals/ContactsModal.vue:127 #: frontend/src/components/Settings/IdentitySettings.vue:10 #: frontend/src/components/Settings/IdentitySettings.vue:124 -#: frontend/src/pages/AddressBookView.vue:284 -#: frontend/src/pages/ContactsView.vue:128 +#: frontend/src/pages/AddressBookView.vue:287 +#: frontend/src/pages/ContactsView.vue:143 #: frontend/src/pages/ForgotPasswordView.vue:18 #: frontend/src/pages/InviteSetupView.vue:5 frontend/src/pages/LoginView.vue:5 #: frontend/src/pages/ResetPasswordView.vue:4 @@ -3214,14 +3064,12 @@ msgstr "" #: mail/client/doctype/identity/identity.json #: mail/client/doctype/mail_message_recipient/mail_message_recipient.json #: mail/client/doctype/participant_identity/participant_identity.json -#: mail/server/doctype/message_queue/message_queue.js:46 -#: mail/server/doctype/message_queue_recipient/message_queue_recipient.json #: mail/server/doctype/principal_email/principal_email.json msgid "Email" msgstr "" #. Name of a DocType -#: frontend/src/components/Settings/BlockListSettings.vue:103 +#: frontend/src/components/Settings/BlockListSettings.vue:107 #: frontend/src/pages/dashboard/MailingListView.vue:40 #: frontend/src/pages/dashboard/MemberView.vue:60 #: mail/client/doctype/email_address/email_address.json @@ -3237,36 +3085,34 @@ msgstr "" msgid "Email Deliverability" msgstr "" -#. Label of the email_limits_section (Section Break) field in DocType 'Mail -#. Cluster' -#: mail/server/doctype/mail_cluster/mail_cluster.json -msgid "Email Limits" -msgstr "" - #: frontend/src/pages/dashboard/DomainView.vue:75 msgid "Email Transport Security Records" msgstr "" -#: frontend/src/components/MailActions.vue:300 -#: frontend/src/components/Settings/BlockListSettings.vue:73 +#: mail/api/auth.py:47 +msgid "Email address '{0}' is not associated with any account of the user '{1}'." +msgstr "" + +#: frontend/src/components/MailActions.vue:334 +#: frontend/src/components/Settings/BlockListSettings.vue:74 msgid "Email address blocked." msgstr "" +#: mail/api/auth.py:40 +msgid "Email address is required." +msgstr "" + #. Description of the 'Username' (Data) field in DocType 'User Settings' #: mail/client/doctype/user_settings/user_settings.json msgid "Email address or username used to sign in to your JMAP account." msgstr "" -#: frontend/src/components/MailActions.vue:300 -#: frontend/src/components/MailThread.vue:514 +#: frontend/src/components/MailActions.vue:334 +#: frontend/src/components/MailThread.vue:601 msgid "Email address unblocked." msgstr "" -#: mail/api/auth.py:41 -msgid "Email address {0} is not associated with the user {1}." -msgstr "" - -#: frontend/src/components/Settings/BlockListSettings.vue:83 +#: frontend/src/components/Settings/BlockListSettings.vue:87 msgid "Email addresses unblocked." msgstr "" @@ -3274,15 +3120,15 @@ msgstr "" msgid "Email authentication records that verify your domain and protect outgoing mail from spoofing." msgstr "" -#: mail/client/doctype/mail_queue/mail_queue.py:578 +#: mail/client/doctype/mail_queue/mail_queue.py:581 msgid "Email does not have a blob ID." msgstr "" -#: mail/utils/validation.py:87 +#: mail/utils/validation.py:43 msgid "Email domain {0} does not match with domain {1}." msgstr "" -#: mail/api/mail.py:587 +#: mail/api/mail.py:588 msgid "Email is required to fetch avatar." msgstr "" @@ -3290,69 +3136,45 @@ msgstr "" msgid "Email is required to send invite" msgstr "" -#: mail/client/doctype/mail_exchange/mail_exchange.py:990 +#: mail/client/doctype/mail_exchange/mail_exchange.py:992 msgid "Email must have a valid non-future Received or Date header." msgstr "" -#: mail/client/doctype/mail_queue/mail_queue.py:806 +#: mail/client/doctype/mail_queue/mail_queue.py:809 msgid "Email processing aborted: {retries} out of {total_count} emails failed ({failure_rate:.2%} failure rate). Please investigate the issue before retrying." msgstr "" #. Label of the emails (Table) field in DocType 'Contact Card' -#. Label of the jmap_email_parse_max_items (Int) field in DocType 'Mail -#. Cluster' #. Label of the emails (Table) field in DocType 'Principal' #: frontend/src/pages/ContactView.vue:60 #: frontend/src/pages/dashboard/MailingListsView.vue:111 #: frontend/src/pages/dashboard/UsersView.vue:150 #: mail/client/doctype/contact_card/contact_card.json -#: mail/server/doctype/mail_cluster/mail_cluster.json #: mail/server/doctype/principal/principal.json msgid "Emails" msgstr "" -#: frontend/src/components/Modals/FolderModal.vue:80 +#: frontend/src/components/Modals/FolderModal.vue:81 msgid "Emails From" msgstr "" -#: frontend/src/components/Modals/FolderModal.vue:83 +#: frontend/src/components/Modals/FolderModal.vue:84 msgid "Emails from these addresses will be automatically moved to this folder." msgstr "" -#: frontend/src/components/Modals/FolderModal.vue:93 +#: frontend/src/components/Modals/FolderModal.vue:94 msgid "Emails with these keywords in the subject will be automatically moved to this folder." msgstr "" -#: frontend/src/pages/MailboxView.vue:967 +#: frontend/src/pages/MailboxView.vue:999 msgid "Empty {0}" msgstr "" -#. Label of the metrics_prometheus_enable (Check) field in DocType 'Mail -#. Cluster' -#: frontend/src/components/Modals/FolderModal.vue:72 +#: frontend/src/components/Modals/FolderModal.vue:73 #: frontend/src/components/Modals/SetSieveScriptStateModal.vue:52 -#: mail/server/doctype/mail_cluster/mail_cluster.json msgid "Enable" msgstr "" -#. Label of the storage_search_index_contacts_enable (Check) field in DocType -#. 'Mail Cluster' -#: mail/server/doctype/mail_cluster/mail_cluster.json -msgid "Enable Calendar Searching" -msgstr "" - -#. Label of the storage_search_index_calendar_enable (Check) field in DocType -#. 'Mail Cluster' -#: mail/server/doctype/mail_cluster/mail_cluster.json -msgid "Enable Contacts Searching" -msgstr "" - -#. Label of the storage_search_index_email_enable (Check) field in DocType -#. 'Mail Cluster' -#: mail/server/doctype/mail_cluster/mail_cluster.json -msgid "Enable Email Searching" -msgstr "" - #: frontend/src/components/PWASettings.vue:16 msgid "Enable Push Notifications" msgstr "" @@ -3362,57 +3184,6 @@ msgstr "" msgid "Enable Spam Detection" msgstr "" -#. Label of the tls_enable (Check) field in DocType 'Mail Cluster Store' -#: mail/server/doctype/mail_cluster_store/mail_cluster_store.json -msgid "Enable TLS" -msgstr "" - -#. Label of the storage_search_index_tracing_enable (Check) field in DocType -#. 'Mail Cluster' -#: mail/server/doctype/mail_cluster/mail_cluster.json -msgid "Enable Tracing Searching" -msgstr "" - -#. Label of the email_encryption_enable (Check) field in DocType 'Mail Cluster' -#: mail/server/doctype/mail_cluster/mail_cluster.json -msgid "Enable encryption at rest" -msgstr "" - -#. Description of the 'Enable Calendar Searching' (Check) field in DocType -#. 'Mail Cluster' -#: mail/server/doctype/mail_cluster/mail_cluster.json -msgid "Enable full-text search indexing for calendar data." -msgstr "" - -#. Description of the 'Enable Contacts Searching' (Check) field in DocType -#. 'Mail Cluster' -#: mail/server/doctype/mail_cluster/mail_cluster.json -msgid "Enable full-text search indexing for contacts data." -msgstr "" - -#. Description of the 'Enable Email Searching' (Check) field in DocType 'Mail -#. Cluster' -#: mail/server/doctype/mail_cluster/mail_cluster.json -msgid "Enable full-text search indexing for email content and metadata." -msgstr "" - -#. Description of the 'Enable Tracing Searching' (Check) field in DocType 'Mail -#. Cluster' -#: mail/server/doctype/mail_cluster/mail_cluster.json -msgid "Enable full-text search indexing for tracing data." -msgstr "" - -#. Description of the 'Trusted Networks' (Small Text) field in DocType 'Mail -#. Cluster' -#: mail/server/doctype/mail_cluster/mail_cluster.json -msgid "Enable proxy protocol for connections from these networks." -msgstr "" - -#. Description of the 'Enable' (Check) field in DocType 'Mail Cluster' -#: mail/server/doctype/mail_cluster/mail_cluster.json -msgid "Enable the Prometheus metrics endpoint." -msgstr "" - #. Label of the enabled (Check) field in DocType 'Vacation Response' #. Label of the enabled (Check) field in DocType 'Rate Limit' #. Label of the enabled (Check) field in DocType 'Mail Cluster' @@ -3435,17 +3206,6 @@ msgstr "" msgid "Enabling Push Notifications..." msgstr "" -#. Description of the 'Encrypt on append' (Check) field in DocType 'Mail -#. Cluster' -#: mail/server/doctype/mail_cluster/mail_cluster.json -msgid "Encrypt messages that are manually appended by the user using JMAP or IMAP." -msgstr "" - -#. Label of the email_encryption_append (Check) field in DocType 'Mail Cluster' -#: mail/server/doctype/mail_cluster/mail_cluster.json -msgid "Encrypt on append" -msgstr "" - #: mail/client/doctype/push_subscription/push_subscription.py:348 msgid "Encrypted payload missing ciphertext data." msgstr "" @@ -3487,16 +3247,6 @@ msgstr "" msgid "Ended At - Started At" msgstr "" -#. Label of the metrics_open_telemetry_endpoint (Data) field in DocType 'Mail -#. Cluster' -#. Label of the endpoint (Data) field in DocType 'Mail Cluster Store' -#. Label of the endpoint (Data) field in DocType 'Mail Cluster Trace' -#: mail/server/doctype/mail_cluster/mail_cluster.json -#: mail/server/doctype/mail_cluster_store/mail_cluster_store.json -#: mail/server/doctype/mail_cluster_trace/mail_cluster_trace.json -msgid "Endpoint" -msgstr "" - #. Option for the 'Delivery Mode' (Select) field in DocType 'Mail Queue' #: mail/client/doctype/mail_queue/mail_queue.json msgid "Enqueue" @@ -3506,27 +3256,11 @@ msgstr "" msgid "Enter email address" msgstr "" -#. Label of the env_id (Data) field in DocType 'Message Queue' -#: mail/server/doctype/message_queue/message_queue.json -msgid "Env ID" -msgstr "" - -#: mail/server/report/dmarc_report_viewer/dmarc_report_viewer.py:42 -msgid "Envelope From" -msgstr "" - -#: mail/server/report/dmarc_report_viewer/dmarc_report_viewer.py:43 -msgid "Envelope To" -msgstr "" - #. Label of the error_section (Section Break) field in DocType 'Mail Queue' -#. Option for the 'Logging Level' (Select) field in DocType 'Mail Cluster -#. Trace' #. Label of the stderr (Code) field in DocType 'Server Ansible Play Task' #. Label of the stderr (Code) field in DocType 'Server Job Command' #: frontend/src/pages/MimeMessageView.vue:30 #: mail/client/doctype/mail_queue/mail_queue.json -#: mail/server/doctype/mail_cluster_trace/mail_cluster_trace.json #: mail/server/doctype/server_ansible_play_task/server_ansible_play_task.json #: mail/server/doctype/server_job_command/server_job_command.json msgid "Error" @@ -3556,20 +3290,10 @@ msgstr "" msgid "Error parsing playbook {0}: {1}" msgstr "" -#: mail/api/outbound.py:51 +#: mail/api/outbound.py:47 msgid "Error uploading attachment" msgstr "" -#. Description of the 'Name Length' (Int) field in DocType 'Mail Cluster' -#: mail/server/doctype/mail_cluster/mail_cluster.json -msgid "Establishes the maximum length of a mailbox name." -msgstr "" - -#. Description of the 'Set' (Int) field in DocType 'Mail Cluster' -#: mail/server/doctype/mail_cluster/mail_cluster.json -msgid "Establishes the maximum number of objects that can be modified in a single method call." -msgstr "" - #. Label of the section_break_cafk (Section Break) field in DocType 'Event #. Notification' #. Label of the event (JSON) field in DocType 'Event Notification' @@ -3602,14 +3326,19 @@ msgstr "" msgid "Event Notification" msgstr "" -#: mail/client/doctype/event_notification/event_notification.py:155 +#: mail/client/doctype/event_notification/event_notification.py:156 msgid "Event Notification Deletion Error" msgstr "" -#: mail/client/doctype/event_notification/event_notification.py:154 +#: mail/client/doctype/event_notification/event_notification.py:155 msgid "Event Notification Deletion Error(s):
{0}" msgstr "" +#. Label of the id (Data) field in DocType 'Event Notification' +#: mail/client/doctype/event_notification/event_notification.json +msgid "Event Notification ID" +msgstr "" + #: mail/client/doctype/event_notification/event_notification.py:31 msgid "Event Notification Not Found" msgstr "" @@ -3623,10 +3352,10 @@ msgid "Event Notification cannot be updated by the user." msgstr "" #: mail/client/doctype/event_notification/event_notification.py:28 -msgid "Event Notification with ID {0} not found in user {1}." +msgid "Event Notification with ID {0} not found in account {1}." msgstr "" -#: mail/client/doctype/event_notification/event_notification.py:96 +#: mail/client/doctype/event_notification/event_notification.py:97 msgid "Event Notifications deleted successfully." msgstr "" @@ -3665,20 +3394,12 @@ msgstr "" msgid "Expect Reply" msgstr "" -#. Label of the jmap_protocol_upload_ttl (Int) field in DocType 'Mail Cluster' -#: mail/server/doctype/mail_cluster/mail_cluster.json -msgid "Expire After (Hours)" -msgstr "" - #: frontend/src/pages/dashboard/InvitesView.vue:211 msgid "Expired" msgstr "" #. Label of the expires (Datetime) field in DocType 'Push Subscription' -#. Label of the expires (Datetime) field in DocType 'Message Queue Recipient' #: mail/client/doctype/push_subscription/push_subscription.json -#: mail/server/doctype/message_queue/message_queue.js:90 -#: mail/server/doctype/message_queue_recipient/message_queue_recipient.json msgid "Expires" msgstr "" @@ -3691,18 +3412,18 @@ msgstr "" #. Option for the 'Operation' (Select) field in DocType 'Mail Exchange' #. Option for the 'Operation' (Select) field in DocType 'Mail Data Exchange' -#: frontend/src/components/Modals/SettingsModal.vue:128 +#: frontend/src/components/Modals/SettingsModal.vue:140 #: frontend/src/pages/MailExchangesView.vue:148 #: mail/client/doctype/mail_exchange/mail_exchange.json #: mail/server/doctype/mail_data_exchange/mail_data_exchange.json msgid "Export" msgstr "" -#: mail/client/doctype/mail_exchange/mail_exchange.py:602 +#: mail/client/doctype/mail_exchange/mail_exchange.py:604 msgid "Export Limit cannot exceed {0}." msgstr "" -#: mail/client/doctype/mail_exchange/mail_exchange.py:600 +#: mail/client/doctype/mail_exchange/mail_exchange.py:602 msgid "Export Limit must be greater than zero." msgstr "" @@ -3710,11 +3431,11 @@ msgstr "" msgid "Export Mail" msgstr "" -#: mail/client/doctype/mail_exchange/mail_exchange.py:757 +#: mail/client/doctype/mail_exchange/mail_exchange.py:759 msgid "Export completed" msgstr "" -#: mail/client/doctype/mail_exchange/mail_exchange.py:589 +#: mail/client/doctype/mail_exchange/mail_exchange.py:591 msgid "Export filter must be valid JSON." msgstr "" @@ -3722,49 +3443,31 @@ msgstr "" msgid "Export in progress. We'll email you when it's ready." msgstr "" -#: mail/client/doctype/mail_exchange/mail_exchange.py:731 +#: mail/client/doctype/mail_exchange/mail_exchange.py:733 msgid "Export limit exceeded." msgstr "" -#. Label of the enable_log_exporter (Check) field in DocType 'Mail Cluster -#. Trace' -#: mail/server/doctype/mail_cluster_trace/mail_cluster_trace.json -msgid "Export logs" -msgstr "" - -#. Label of the enable_span_exporter (Check) field in DocType 'Mail Cluster -#. Trace' -#: mail/server/doctype/mail_cluster_trace/mail_cluster_trace.json -msgid "Export spans" -msgstr "" - #: frontend/src/pages/dashboard/MailingListView.vue:62 #: frontend/src/pages/dashboard/MailingListView.vue:255 msgid "External" msgstr "" -#. Label of the external_account_binding_section (Section Break) field in -#. DocType 'Mail Server ACME Provider' -#: mail/server/doctype/mail_server_acme_provider/mail_server_acme_provider.json -msgid "External Account Binding" -msgstr "" - #. Label of the external_members (Table) field in DocType 'Principal' #: frontend/src/pages/dashboard/MailingListsView.vue:113 #: mail/server/doctype/principal/principal.json msgid "External Members" msgstr "" -#. Label of the extra_contact_info (Small Text) field in DocType 'DMARC Report' -#: mail/server/doctype/dmarc_report/dmarc_report.json -msgid "Extra Contact Info" -msgstr "" - #. Description of the 'Hostname' (Data) field in DocType 'Mail Cluster' #: mail/server/doctype/mail_cluster/mail_cluster.json msgid "FQDN of the proxy or load balancer forwarding requests to mail servers." msgstr "" +#. Label of the fail_on_timeout (Check) field in DocType 'Mail Cluster Store' +#: mail/server/doctype/mail_cluster_store/mail_cluster_store.json +msgid "Fail On Timeout" +msgstr "" + #. Option for the 'Status' (Select) field in DocType 'Mail Exchange' #. Option for the 'Status' (Select) field in DocType 'Mail Queue' #. Option for the 'Status' (Select) field in DocType 'Mail Data Exchange' @@ -3796,7 +3499,7 @@ msgstr "" msgid "Failed to Submit" msgstr "" -#: mail/server/doctype/principal/principal.py:354 +#: mail/server/doctype/principal/principal.py:345 msgid "Failed to add principal {0}: {1}" msgstr "" @@ -3812,52 +3515,72 @@ msgstr "" msgid "Failed to copy code" msgstr "" -#: mail/server/doctype/principal/principal.py:406 +#: mail/server/doctype/principal/principal.py:397 msgid "Failed to create DKIM signature for domain {0}" msgstr "" +#: mail/stalwart/account.py:327 +msgid "Failed to create account: {0}" +msgstr "" + #: mail/client/doctype/push_subscription/push_subscription.py:413 msgid "Failed to decrypt push payload (authentication failed)." msgstr "" -#: mail/server/doctype/principal/principal.py:684 +#: mail/server/doctype/principal/principal.py:633 msgid "Failed to delete DKIM signature for domain {0}" msgstr "" -#: mail/client/doctype/mail_message/mail_message.py:762 +#: mail/client/doctype/mail_message/mail_message.py:771 msgid "Failed to delete mail(s)" msgstr "" -#: mail/client/doctype/mail_message/mail_message.py:765 +#: mail/client/doctype/mail_message/mail_message.py:774 msgid "Failed to delete mail(s)." msgstr "" -#: mail/client/doctype/mail_message/mail_message.py:790 +#: mail/client/doctype/mail_message/mail_message.py:799 msgid "Failed to empty mailbox" msgstr "" -#: mail/client/doctype/mail_message/mail_message.py:793 +#: mail/client/doctype/mail_message/mail_message.py:802 msgid "Failed to empty mailbox." msgstr "" -#: mail/client/doctype/mail_queue/mail_queue.py:560 +#: mail/client/doctype/mail_queue/mail_queue.py:563 msgid "Failed to fetch In Reply To ID" msgstr "" -#: mail/client/doctype/mail_message/mail_message.py:947 +#: mail/stalwart/account.py:269 +msgid "Failed to fetch account: {0}" +msgstr "" + +#: mail/stalwart/account.py:319 +msgid "Failed to fetch accounts: {0}" +msgstr "" + +#: mail/client/doctype/mail_message/mail_message.py:973 msgid "Failed to fetch blob(s)" msgstr "" -#: mail/client/doctype/mail_message/mail_message.py:950 +#: mail/client/doctype/mail_message/mail_message.py:976 msgid "Failed to fetch blob(s)." msgstr "" -#: mail/client/doctype/mail_message/mail_message.py:1284 +#: mail/client/doctype/mail_message/mail_message.py:1276 msgid "Failed to fetch changes" msgstr "" -#: mail/client/doctype/mail_message/mail_message.py:744 -#: mail/client/doctype/mail_message/mail_message.py:745 +#: mail/stalwart/domain.py:30 +msgid "Failed to fetch domain: {0}" +msgstr "" + +#: mail/stalwart/domain.py:67 +msgid "Failed to fetch domains: {0}" +msgstr "" + +#: mail/client/doctype/mail_message/mail_message.py:753 +#: mail/client/doctype/mail_message/mail_message.py:754 msgid "Failed to fetch message IDs." msgstr "" @@ -3865,7 +3588,7 @@ msgstr "" msgid "Failed to generate SSH keypair" msgstr "" -#: mail/api/jmap.py:145 +#: mail/api/jmap.py:146 msgid "Failed to handle JMAP Push Notification." msgstr "" @@ -3873,59 +3596,55 @@ msgstr "" msgid "Failed to load attachment" msgstr "" -#: mail/utils/__init__.py:463 +#: mail/utils/__init__.py:477 msgid "Failed to load content from the compressed file." msgstr "" -#: mail/client/doctype/mail_message/mail_message.py:811 +#: mail/client/doctype/mail_message/mail_message.py:820 msgid "Failed to move mail(s) to mailbox" msgstr "" -#: mail/client/doctype/mail_message/mail_message.py:814 +#: mail/client/doctype/mail_message/mail_message.py:823 msgid "Failed to move mail(s) to mailbox." msgstr "" -#: mail/client/doctype/mail_message/mail_message.py:871 +#: mail/client/doctype/mail_message/mail_message.py:890 msgid "Failed to set flagged status for mail(s)" msgstr "" -#: mail/client/doctype/mail_message/mail_message.py:874 +#: mail/client/doctype/mail_message/mail_message.py:893 msgid "Failed to set flagged status for mail(s)." msgstr "" -#: mail/client/doctype/mail_message/mail_message.py:841 +#: mail/client/doctype/mail_message/mail_message.py:855 msgid "Failed to set seen status for mail(s)" msgstr "" -#: mail/client/doctype/mail_message/mail_message.py:844 +#: mail/client/doctype/mail_message/mail_message.py:858 msgid "Failed to set seen status for mail(s)." msgstr "" -#: mail/client/doctype/mail_message/mail_message.py:902 +#: mail/client/doctype/mail_message/mail_message.py:922 msgid "Failed to set spam status for mail(s)" msgstr "" -#: mail/client/doctype/mail_message/mail_message.py:905 +#: mail/client/doctype/mail_message/mail_message.py:925 msgid "Failed to set spam status for mail(s)." msgstr "" -#: mail/server/doctype/server_config/server_config.py:138 -msgid "Failed to update configuration" -msgstr "" - #: mail/events.py:64 msgid "Failed to update password for Principal {0}. Response: {1}" msgstr "" -#: mail/server/doctype/principal/principal.py:610 +#: mail/server/doctype/principal/principal.py:567 msgid "Failed to update principal {0}: {1}" msgstr "" -#: mail/client/doctype/calendar_event/calendar_event.py:538 +#: mail/client/doctype/calendar_event/calendar_event.py:542 msgid "Failed to upload ICS data." msgstr "" -#: frontend/src/components/ComposeMailEditor.vue:679 +#: frontend/src/components/ComposeMailEditor.vue:692 msgid "Failed to upload {0}" msgstr "" @@ -3959,41 +3678,29 @@ msgstr "" msgid "File URL" msgstr "" -#: mail/utils/__init__.py:495 +#: mail/utils/__init__.py:509 msgid "File not found: {0}" msgstr "" -#. Description of the 'Private Key Path' (Data) field in DocType 'Mail Server -#. TLS Certificate' -#: mail/server/doctype/mail_server_tls_certificate/mail_server_tls_certificate.json -msgid "File path to the Private Key." -msgstr "" - -#. Description of the 'Certificate Path' (Data) field in DocType 'Mail Server -#. TLS Certificate' -#: mail/server/doctype/mail_server_tls_certificate/mail_server_tls_certificate.json -msgid "File path to the TLS certificate." -msgstr "" - #: frontend/src/components/Settings/ImportSettings.vue:91 msgid "File uploaded: {0}" msgstr "" +#. Option for the 'Type' (Select) field in DocType 'Mail Cluster Store' +#: mail/server/doctype/mail_cluster_store/mail_cluster_store.json +msgid "FileSystem" +msgstr "" + #: mail/server/doctype/server_deployment/server_deployment.js:29 msgid "Filebeat Stream Setup (FC)" msgstr "" -#: mail/server/doctype/server_deployment/server_deployment.py:273 +#: mail/server/doctype/server_deployment/server_deployment.py:214 msgid "Filebeat stream setup job has been created." msgstr "" -#. Option for the 'Type' (Select) field in DocType 'Mail Cluster Store' -#: mail/server/doctype/mail_cluster_store/mail_cluster_store.json -msgid "Filesystem" -msgstr "" - #. Label of the export_filter (JSON) field in DocType 'Mail Exchange' -#: frontend/src/pages/MailboxView.vue:87 +#: frontend/src/pages/MailboxView.vue:86 #: mail/client/doctype/mail_exchange/mail_exchange.json msgid "Filter" msgstr "" @@ -4022,11 +3729,11 @@ msgstr "" msgid "Folder" msgstr "" -#: frontend/src/components/Modals/FolderModal.vue:59 +#: frontend/src/components/Modals/FolderModal.vue:60 msgid "Folder Automation Disabled" msgstr "" -#: frontend/src/components/Modals/FolderModal.vue:258 +#: frontend/src/components/Modals/FolderModal.vue:262 msgid "Folder Automation enabled." msgstr "" @@ -4034,7 +3741,7 @@ msgstr "" msgid "Folder Settings" msgstr "" -#: frontend/src/components/Modals/FolderModal.vue:230 +#: frontend/src/components/Modals/FolderModal.vue:233 msgid "Folder created." msgstr "" @@ -4042,10 +3749,20 @@ msgstr "" msgid "Folder deleted." msgstr "" -#: frontend/src/components/Modals/FolderModal.vue:246 +#: frontend/src/components/Modals/FolderModal.vue:250 +#: frontend/src/components/Settings/FolderSettings.vue:103 msgid "Folder updated." msgstr "" +#: frontend/src/components/Modals/SettingsModal.vue:104 +#: frontend/src/components/Settings/FolderSettings.vue:3 +msgid "Folders" +msgstr "" + +#: frontend/src/components/Settings/FolderSettings.vue:38 +msgid "Folders let you organize your emails into different categories." +msgstr "" + #: mail/server/doctype/mail_account_request/mail_account_request.js:23 msgid "Force Verify and Create Account" msgstr "" @@ -4070,14 +3787,14 @@ msgstr "" msgid "Format" msgstr "" -#: frontend/src/components/ComposeMailEditor.vue:493 -#: frontend/src/components/MailActions.vue:135 -#: frontend/src/components/MailThread.vue:546 -#: mail/client/doctype/mail_message/mail_message.js:91 +#: frontend/src/components/ComposeMailEditor.vue:506 +#: frontend/src/components/MailActions.vue:136 +#: frontend/src/components/MailThread.vue:630 +#: mail/client/doctype/mail_message/mail_message.js:111 msgid "Forward" msgstr "" -#: frontend/src/components/MailThread.vue:547 +#: frontend/src/components/MailThread.vue:631 msgid "Forward (F)" msgstr "" @@ -4097,7 +3814,7 @@ msgstr "" #. Option for the 'Type' (Select) field in DocType 'Mail Cluster Store' #: mail/server/doctype/mail_cluster_store/mail_cluster_store.json -msgid "FoundationDB" +msgid "FoundationDb" msgstr "" #. Option for the 'Free Busy Status' (Select) field in DocType 'Calendar Event' @@ -4110,22 +3827,15 @@ msgstr "" msgid "Free Busy Status" msgstr "" -#. Label of the account_purge_frequency (Data) field in DocType 'Mail Cluster' -#: mail/server/doctype/mail_cluster/mail_cluster.json -msgid "Frequency (Cron)" -msgstr "" - #. Label of the _from (Data) field in DocType 'Mail Message' -#. Label of the envelope_from (Data) field in DocType 'DMARC Report Detail' #: frontend/src/components/ComposeMailEditor.vue:26 -#: frontend/src/components/Modals/SearchModal.vue:52 mail/api/mail.py:435 +#: frontend/src/components/Modals/SearchModal.vue:55 mail/api/mail.py:430 #: mail/client/doctype/mail_message/mail_message.json -#: mail/server/doctype/dmarc_report_detail/dmarc_report_detail.json msgid "From" msgstr "" #. Label of the from_date (Datetime) field in DocType 'Vacation Response' -#: frontend/src/components/Modals/SearchModal.vue:62 +#: frontend/src/components/Modals/SearchModal.vue:65 #: frontend/src/components/Settings/ExportSettings.vue:35 #: frontend/src/components/Settings/VacationResponseSettings.vue:13 #: mail/client/doctype/vacation_response/vacation_response.json @@ -4139,7 +3849,7 @@ msgstr "" msgid "From Email" msgstr "" -#: mail/client/doctype/mail_queue/mail_queue.py:344 +#: mail/client/doctype/mail_queue/mail_queue.py:347 msgid "From Email is required." msgstr "" @@ -4171,11 +3881,11 @@ msgstr "" msgid "Full Name" msgstr "" -#: mail/server/doctype/mail_cluster/mail_cluster.py:309 +#: mail/server/doctype/mail_cluster/mail_cluster.py:237 msgid "Full Text Index Storage" msgstr "" -#: frontend/src/components/Modals/FolderModal.vue:181 +#: frontend/src/components/Modals/FolderModal.vue:183 msgid "General" msgstr "" @@ -4186,14 +3896,6 @@ msgstr "" msgid "General Information" msgstr "" -#: mail/server/doctype/mail_cluster/mail_cluster.js:155 -msgid "Generate API Key" -msgstr "" - -#: mail/server/doctype/mail_server/mail_server.js:27 -msgid "Generate Config" -msgstr "" - #: mail/mail/doctype/mail_settings/mail_settings.js:49 msgid "Generate JMAP Push Keys" msgstr "" @@ -4202,24 +3904,10 @@ msgstr "" msgid "Generate Keys" msgstr "" -#: mail/server/doctype/mail_cluster/mail_cluster.js:183 -msgid "Generating API Key..." -msgstr "" - -#: mail/server/doctype/mail_server/mail_server.js:76 -msgid "Generating Config..." -msgstr "" - #: mail/mail/doctype/mail_settings/mail_settings.js:65 msgid "Generating keys…" msgstr "" -#. Label of the jmap_protocol_get_max_objects (Int) field in DocType 'Mail -#. Cluster' -#: mail/server/doctype/mail_cluster/mail_cluster.json -msgid "Get" -msgstr "" - #: frontend/src/components/InstallPrompt.vue:7 msgid "Get the app on your device for easy access & a better experience!" msgstr "" @@ -4228,15 +3916,11 @@ msgstr "" msgid "Get the app on your iPhone for easy access & a better experience" msgstr "" -#: mail/client/doctype/user_settings/user_settings.js:73 -#: mail/server/doctype/mail_cluster/mail_cluster.js:169 +#: mail/client/doctype/user_settings/user_settings.js:51 +#: mail/server/doctype/mail_cluster/mail_cluster.js:56 msgid "Getting Password..." msgstr "" -#: mail/server/doctype/message_queue/message_queue_list.js:18 -msgid "Getting Queue Status..." -msgstr "" - #: frontend/src/components/Modals/ShortcutsModal.vue:87 msgid "Go to First Mail" msgstr "" @@ -4271,13 +3955,13 @@ msgid "GoDaddy" msgstr "" #. Option for the 'Color' (Select) field in DocType 'Mailbox Settings' -#: frontend/src/components/Modals/FolderModal.vue:292 +#: frontend/src/components/Modals/FolderModal.vue:296 #: mail/client/doctype/mailbox_settings/mailbox_settings.json msgid "Gray" msgstr "" #. Option for the 'Color' (Select) field in DocType 'Mailbox Settings' -#: frontend/src/components/Modals/FolderModal.vue:294 +#: frontend/src/components/Modals/FolderModal.vue:298 #: mail/client/doctype/mailbox_settings/mailbox_settings.json msgid "Green" msgstr "" @@ -4286,7 +3970,7 @@ msgstr "" #. Option for the 'Type' (Select) field in DocType 'Principal' #. Option for the 'Principal Type' (Select) field in DocType 'Principal #. Settings' -#: frontend/src/components/Modals/AddContactModal.vue:89 +#: frontend/src/components/Modals/AddContactModal.vue:91 #: frontend/src/components/Modals/EditContactModal.vue:46 #: mail/client/doctype/event_participant/event_participant.json #: mail/server/doctype/principal/principal.json @@ -4306,14 +3990,12 @@ msgstr "" msgid "Group email messages in the list by time period (e.g., day or month)." msgstr "" -#: frontend/src/pages/dashboard/UsersView.vue:151 -msgid "Groups" +#: mail/stalwart/account.py:382 +msgid "Group {0} not found." msgstr "" -#. Label of the eab_hmac_key (Password) field in DocType 'Mail Server ACME -#. Provider' -#: mail/server/doctype/mail_server_acme_provider/mail_server_acme_provider.json -msgid "HMAC Key" +#: frontend/src/pages/dashboard/UsersView.vue:151 +msgid "Groups" msgstr "" #. Label of the html_signature (HTML Editor) field in DocType 'Identity' @@ -4334,13 +4016,14 @@ msgstr "" msgid "HTML Body" msgstr "" -#. Option for the 'Transport' (Select) field in DocType 'Mail Cluster' -#. Option for the 'Transport' (Select) field in DocType 'Mail Cluster Trace' -#. Option for the 'Protocol' (Select) field in DocType 'Mail Server Listener' -#: mail/server/doctype/mail_cluster/mail_cluster.json -#: mail/server/doctype/mail_cluster_trace/mail_cluster_trace.json -#: mail/server/doctype/mail_server_listener/mail_server_listener.json -msgid "HTTP" +#. Label of the http_auth (Link) field in DocType 'Mail Cluster Store' +#: mail/server/doctype/mail_cluster_store/mail_cluster_store.json +msgid "HTTP Auth" +msgstr "" + +#. Label of the http_headers (JSON) field in DocType 'Mail Cluster Store' +#: mail/server/doctype/mail_cluster_store/mail_cluster_store.json +msgid "HTTP Headers" msgstr "" #. Label of the hard_limit (Int) field in DocType 'Quota' @@ -4353,21 +4036,15 @@ msgstr "" msgid "Has Attachment" msgstr "" -#. Label of the has_cached_jmap_connection (Check) field in DocType 'User +#. Label of the has_cached_jmap_identities (Check) field in DocType 'Account #. Settings' -#: mail/client/doctype/user_settings/user_settings.json -msgid "Has Cached JMAP Connection" -msgstr "" - -#. Label of the has_cached_jmap_identities (Check) field in DocType 'User -#. Settings' -#: mail/client/doctype/user_settings/user_settings.json +#: mail/client/doctype/account_settings/account_settings.json msgid "Has Cached JMAP Identities" msgstr "" -#. Label of the has_cached_jmap_mailboxes (Check) field in DocType 'User +#. Label of the has_cached_jmap_mailboxes (Check) field in DocType 'Account #. Settings' -#: mail/client/doctype/user_settings/user_settings.json +#: mail/client/doctype/account_settings/account_settings.json msgid "Has Cached JMAP Mailboxes" msgstr "" @@ -4376,7 +4053,7 @@ msgstr "" msgid "Has Keyword" msgstr "" -#: frontend/src/pages/MailboxView.vue:1128 +#: frontend/src/pages/MailboxView.vue:1160 msgid "Has attachments" msgstr "" @@ -4385,12 +4062,6 @@ msgstr "" msgid "Has the user indicated they wish to see this Mailbox in their client?" msgstr "" -#. Label of the header_from (Data) field in DocType 'DMARC Report Detail' -#: mail/server/doctype/dmarc_report_detail/dmarc_report_detail.json -#: mail/server/report/dmarc_report_viewer/dmarc_report_viewer.py:41 -msgid "Header From" -msgstr "" - #. Label of the headers_section (Section Break) field in DocType 'Mail Queue' #. Label of the headers (JSON) field in DocType 'Mail Queue' #: mail/client/doctype/mail_queue/mail_queue.json @@ -4406,6 +4077,10 @@ msgstr "" msgid "Hetzner" msgstr "" +#: frontend/src/components/Settings/FolderSettings.vue:73 +msgid "Hide" +msgstr "" + #. Label of the hide_attendees (Check) field in DocType 'Calendar Event' #: mail/client/doctype/calendar_event/calendar_event.json msgid "Hide Attendees" @@ -4417,29 +4092,25 @@ msgid "High" msgstr "" #. Label of the spamd_host (Data) field in DocType 'Mail Settings' -#. Label of the host (Data) field in DocType 'Allowed IP' -#. Label of the host (Data) field in DocType 'Blocked IP' #. Label of the host (Data) field in DocType 'DNS Record' +#. Label of the host (Data) field in DocType 'Mail Cluster Store' #. Label of the host (Data) field in DocType 'Principal DNS Record' #: frontend/src/components/DNSRecords.vue:50 #: mail/mail/doctype/mail_settings/mail_settings.json -#: mail/server/doctype/allowed_ip/allowed_ip.json -#: mail/server/doctype/blocked_ip/blocked_ip.json #: mail/server/doctype/dns_record/dns_record.json +#: mail/server/doctype/mail_cluster_store/mail_cluster_store.json #: mail/server/doctype/principal_dns_record/principal_dns_record.json msgid "Host" msgstr "" #. Label of the hostname (Data) field in DocType 'Mail Cluster' -#. Label of the host (Data) field in DocType 'Mail Cluster Store' #. Label of the hostname (Data) field in DocType 'Mail Server' #: mail/server/doctype/mail_cluster/mail_cluster.json -#: mail/server/doctype/mail_cluster_store/mail_cluster_store.json #: mail/server/doctype/mail_server/mail_server.json msgid "Hostname" msgstr "" -#. Description of the 'Hostname' (Data) field in DocType 'Mail Cluster Store' +#. Description of the 'Host' (Data) field in DocType 'Mail Cluster Store' #: mail/server/doctype/mail_cluster_store/mail_cluster_store.json msgid "Hostname of the database server." msgstr "" @@ -4454,60 +4125,6 @@ msgstr "" msgid "Hostname or IP of the SpamAssassin server." msgstr "" -#. Description of the 'Subject Names' (Small Text) field in DocType 'Mail -#. Server ACME Provider' -#: mail/server/doctype/mail_server_acme_provider/mail_server_acme_provider.json -msgid "Hostnames covered by this ACME manager." -msgstr "" - -#. Option for the 'Rotate Frequency' (Select) field in DocType 'Mail Cluster -#. Trace' -#: mail/server/doctype/mail_cluster_trace/mail_cluster_trace.json -msgid "Hourly" -msgstr "" - -#. Description of the 'Un-delete Period (Days)' (Int) field in DocType 'Mail -#. Cluster' -#: mail/server/doctype/mail_cluster/mail_cluster.json -msgid "How long to keep deleted emails before they are permanently removed from the system." -msgstr "" - -#. Description of the 'Trash Auto-Expunge (Days)' (Int) field in DocType 'Mail -#. Cluster' -#: mail/server/doctype/mail_cluster/mail_cluster.json -msgid "How long to keep messages in the Trash and Junk Mail folders before auto-expunging." -msgstr "" - -#. Description of the 'Changes History' (Int) field in DocType 'Mail Cluster' -#: mail/server/doctype/mail_cluster/mail_cluster.json -msgid "How many changes to keep in the history for each account. This is used to determine the changes that have occurred since the last time the client requested changes." -msgstr "" - -#. Description of the 'Purge Frequency (Cron)' (Data) field in DocType 'Mail -#. Cluster Store' -#: mail/server/doctype/mail_cluster_store/mail_cluster_store.json -msgid "" -"How often to purge the database. Expects a cron expression.\n" -"\n" -"
\n" -"
\n" -"\n" -"
*  *  *  *  *\n"
-"┬  ┬  ┬  ┬  ┬\n"
-"│  │  │  │  │\n"
-"│  │  │  │  └ day of week (0 - 6) (0 is Sunday)\n"
-"│  │  │  └───── month (1 - 12)\n"
-"│  │  └────────── day of month (1 - 31)\n"
-"│  └─────────────── hour (0 - 23)\n"
-"└──────────────────── minute (0 - 59)\n"
-"\n"
-"---\n"
-"\n"
-"* - Any value\n"
-"/ - Step values\n"
-"
" -msgstr "" - #. Option for the 'Scanning Mode' (Select) field in DocType 'Mail Settings' #: mail/mail/doctype/mail_settings/mail_settings.json msgid "Hybrid Approach" @@ -4522,53 +4139,22 @@ msgstr "" msgid "Hybrid Scanning Threshold" msgstr "" -#: mail/client/doctype/calendar_event/calendar_event.py:542 +#: mail/client/doctype/calendar_event/calendar_event.py:546 msgid "ICS Parsing Error" msgstr "" -#: mail/client/doctype/calendar_event/calendar_event.py:538 +#: mail/client/doctype/calendar_event/calendar_event.py:542 msgid "ICS Upload Error" msgstr "" -#. Label of the id (Data) field in DocType 'Address Book' -#. Label of the id (Data) field in DocType 'Calendar' -#. Label of the id (Data) field in DocType 'Calendar Event' -#. Label of the id (Data) field in DocType 'Contact Card' -#. Label of the id (Data) field in DocType 'Event Notification' -#. Label of the id (Data) field in DocType 'Identity' -#. Label of the id (Data) field in DocType 'Mailbox' #. Label of the id (Data) field in DocType 'Participant Identity' -#. Label of the id (Data) field in DocType 'Push Subscription' -#. Label of the id (Data) field in DocType 'Quota' -#. Label of the id (Data) field in DocType 'Sieve Script' -#. Label of the id (Data) field in DocType 'DMARC Report' #. Label of the id (Data) field in DocType 'Principal' -#: mail/client/doctype/address_book/address_book.json -#: mail/client/doctype/calendar/calendar.json -#: mail/client/doctype/calendar_event/calendar_event.json -#: mail/client/doctype/contact_card/contact_card.json -#: mail/client/doctype/event_notification/event_notification.json -#: mail/client/doctype/identity/identity.json -#: mail/client/doctype/mailbox/mailbox.json #: mail/client/doctype/participant_identity/participant_identity.json -#: mail/client/doctype/push_subscription/push_subscription.json -#: mail/client/doctype/quota/quota.json -#: mail/client/doctype/sieve_script/sieve_script.json -#: mail/server/doctype/dmarc_report/dmarc_report.json #: mail/server/doctype/principal/principal.json msgid "ID" msgstr "" -#. Option for the 'Protocol' (Select) field in DocType 'Mail Server Listener' -#: mail/server/doctype/mail_server_listener/mail_server_listener.json -msgid "IMAP4" -msgstr "" - -#. Label of the ip_address (Data) field in DocType 'Allowed IP' -#. Label of the ip_address (Data) field in DocType 'Blocked IP' #. Label of the ip_address (Data) field in DocType 'Mail Account Request' -#: mail/server/doctype/allowed_ip/allowed_ip.json -#: mail/server/doctype/blocked_ip/blocked_ip.json #: mail/server/doctype/mail_account_request/mail_account_request.json msgid "IP Address" msgstr "" @@ -4594,7 +4180,7 @@ msgid "IPv6 Addresses" msgstr "" #. Label of the icon (Icon) field in DocType 'Mailbox Settings' -#: frontend/src/components/Modals/FolderModal.vue:26 +#: frontend/src/components/Modals/FolderModal.vue:27 #: mail/client/doctype/mailbox_settings/mailbox_settings.json msgid "Icon" msgstr "" @@ -4614,54 +4200,53 @@ msgstr "" msgid "Identifies Mailboxes that have a particular common purpose (e.g., the \"inbox\"), regardless of the name property (which may be localised)." msgstr "" -#. Description of the 'Access Key' (Password) field in DocType 'Mail Cluster -#. Store' -#: mail/server/doctype/mail_cluster_store/mail_cluster_store.json -msgid "Identifies the S3 account." -msgstr "" - #. Description of the 'Organizer' (Data) field in DocType 'Calendar Event' #: mail/client/doctype/calendar_event/calendar_event.json msgid "Identifies the principal responsible for the event." msgstr "" -#: mail/client/doctype/identity/identity.py:197 +#. Description of the 'Access Key' (Data) field in DocType 'Mail Cluster Store' +#: mail/server/doctype/mail_cluster_store/mail_cluster_store.json +msgid "Identifies the store account." +msgstr "" + +#: mail/client/doctype/identity/identity.py:140 msgid "Identities deleted successfully." msgstr "" #. Name of a DocType #. Label of a Workspace Sidebar Item #: frontend/src/components/Modals/SetDefaultSignatureModal.vue:7 -#: frontend/src/components/Modals/SettingsModal.vue:87 +#: frontend/src/components/Modals/SettingsModal.vue:93 #: frontend/src/components/Settings/IdentitySettings.vue:5 #: mail/client/doctype/identity/identity.json #: mail/workspace_sidebar/client.json msgid "Identity" msgstr "" -#: mail/client/doctype/identity/identity.py:159 -#: mail/client/doctype/identity/identity.py:228 +#: mail/client/doctype/identity/identity.py:171 msgid "Identity Creation Error" msgstr "" -#: mail/client/doctype/identity/identity.py:303 +#: mail/client/doctype/identity/identity.py:246 msgid "Identity Deletion Error" msgstr "" -#: mail/client/doctype/identity/identity.py:302 +#: mail/client/doctype/identity/identity.py:245 msgid "Identity Deletion Error(s):
{0}" msgstr "" -#: mail/client/doctype/identity/identity.py:250 -msgid "Identity Not Found" +#. Label of the id (Data) field in DocType 'Identity' +#: mail/client/doctype/identity/identity.json +msgid "Identity ID" msgstr "" -#: mail/client/doctype/identity/identity.py:281 -msgid "Identity Update Error" +#: mail/client/doctype/identity/identity.py:193 +msgid "Identity Not Found" msgstr "" -#: mail/client/doctype/identity/identity.py:168 -msgid "Identity creation request failed." +#: mail/client/doctype/identity/identity.py:224 +msgid "Identity Update Error" msgstr "" #: frontend/src/components/Modals/SetDefaultSignatureModal.vue:69 @@ -4669,8 +4254,8 @@ msgstr "" msgid "Identity updated." msgstr "" -#: mail/client/doctype/identity/identity.py:249 -msgid "Identity with ID {0} not found in user {1}." +#: mail/client/doctype/identity/identity.py:192 +msgid "Identity with ID {0} not found in account {1}." msgstr "" #. Description of the 'Destroy After Submit' (Check) field in DocType 'Mail @@ -4704,26 +4289,21 @@ msgstr "" msgid "Immediate" msgstr "" -#. Label of the tls_implicit (Check) field in DocType 'Mail Server Listener' -#: mail/server/doctype/mail_server_listener/mail_server_listener.json -msgid "Implicit TLS" -msgstr "" - #. Option for the 'Operation' (Select) field in DocType 'Mail Exchange' #. Option for the 'Operation' (Select) field in DocType 'Mail Data Exchange' -#: frontend/src/components/Modals/SettingsModal.vue:122 +#: frontend/src/components/Modals/SettingsModal.vue:134 #: frontend/src/pages/MailExchangesView.vue:147 #: mail/client/doctype/mail_exchange/mail_exchange.json #: mail/server/doctype/mail_data_exchange/mail_data_exchange.json msgid "Import" msgstr "" -#: mail/client/doctype/mail_exchange/mail_exchange.py:565 +#: mail/client/doctype/mail_exchange/mail_exchange.py:567 #: mail/server/doctype/mail_data_exchange/mail_data_exchange.py:84 msgid "Import File is required." msgstr "" -#: mail/client/doctype/mail_exchange/mail_exchange.py:563 +#: mail/client/doctype/mail_exchange/mail_exchange.py:565 #: mail/server/doctype/mail_data_exchange/mail_data_exchange.py:82 msgid "Import Format is required." msgstr "" @@ -4732,7 +4312,7 @@ msgstr "" msgid "Import Mail" msgstr "" -#: mail/client/doctype/mail_exchange/mail_exchange.py:703 +#: mail/client/doctype/mail_exchange/mail_exchange.py:705 msgid "Import completed" msgstr "" @@ -4740,7 +4320,7 @@ msgstr "" msgid "Import in progress. We'll email you when it's ready." msgstr "" -#: mail/client/doctype/mail_exchange/mail_exchange.py:698 +#: mail/client/doctype/mail_exchange/mail_exchange.py:700 msgid "Import limit exceeded." msgstr "" @@ -4769,13 +4349,20 @@ msgstr "" msgid "In Reply To (Message ID)" msgstr "" -#. Label of the in_memory_storage_section (Section Break) field in DocType -#. 'Mail Cluster' +#. Label of the in_memory_store_config (JSON) field in DocType 'Mail Cluster' #: mail/server/doctype/mail_cluster/mail_cluster.json -#: mail/server/doctype/mail_cluster/mail_cluster.py:310 +msgid "In-Memory Config" +msgstr "" + +#: mail/server/doctype/mail_cluster/mail_cluster.py:238 msgid "In-Memory Storage" msgstr "" +#. Label of the in_memory_store (Link) field in DocType 'Mail Cluster' +#: mail/server/doctype/mail_cluster/mail_cluster.json +msgid "In-Memory Store" +msgstr "" + #. Label of the inbound_section (Section Break) field in DocType 'User #. Settings' #: mail/client/doctype/user_settings/user_settings.json @@ -4796,16 +4383,9 @@ msgstr "" msgid "Include In Availability" msgstr "" -#. Label of the index_section (Section Break) field in DocType 'Mail Cluster -#. Store' +#. Label of the include_source (Check) field in DocType 'Mail Cluster Store' #: mail/server/doctype/mail_cluster_store/mail_cluster_store.json -msgid "Index" -msgstr "" - -#. Label of the storage_search_index_batch_size (Int) field in DocType 'Mail -#. Cluster' -#: mail/server/doctype/mail_cluster/mail_cluster.json -msgid "Indexing Batch Size" +msgid "Include Source" msgstr "" #. Description of the 'Free Busy Status' (Select) field in DocType 'Calendar @@ -4880,7 +4460,7 @@ msgstr "" #. Option for the 'Type' (Select) field in DocType 'Principal' #. Option for the 'Principal Type' (Select) field in DocType 'Principal #. Settings' -#: frontend/src/components/Modals/AddContactModal.vue:88 +#: frontend/src/components/Modals/AddContactModal.vue:90 #: frontend/src/components/Modals/EditContactModal.vue:45 #: mail/client/doctype/event_participant/event_participant.json #: mail/server/doctype/principal/principal.json @@ -4888,16 +4468,6 @@ msgstr "" msgid "Individual" msgstr "" -#. Option for the 'Logging Level' (Select) field in DocType 'Mail Cluster -#. Trace' -#: mail/server/doctype/mail_cluster_trace/mail_cluster_trace.json -msgid "Info" -msgstr "" - -#: mail/server/doctype/mail_cluster/mail_cluster.js:133 -msgid "Initializing Defaults..." -msgstr "" - #: frontend/src/components/Modals/ContactsModal.vue:61 msgid "Insert" msgstr "" @@ -4906,11 +4476,11 @@ msgstr "" msgid "Install" msgstr "" -#: mail/server/doctype/mail_server/mail_server.js:44 +#: mail/server/doctype/mail_server/mail_server.js:45 msgid "Install Ansible (Job)" msgstr "" -#: mail/server/doctype/mail_server/mail_server.js:52 +#: mail/server/doctype/mail_server/mail_server.js:53 msgid "Install Docker (Ansible Play)" msgstr "" @@ -4919,36 +4489,31 @@ msgstr "" msgid "Install Frappe Mail" msgstr "" -#. Label of the install_redis (Check) field in DocType 'Server Deployment' -#: mail/server/doctype/server_deployment/server_deployment.json -msgid "Install Redis" -msgstr "" - -#: mail/server/doctype/mail_server/mail_server.js:60 +#: mail/server/doctype/mail_server/mail_server.js:61 msgid "Install Stalwart (Deployment)" msgstr "" -#: mail/server/doctype/mail_server/mail_server.py:211 +#: mail/server/doctype/mail_server/mail_server.py:128 msgid "Install of Ansible initiated." msgstr "" -#: mail/server/doctype/mail_server/mail_server.py:237 +#: mail/server/doctype/mail_server/mail_server.py:154 msgid "Install of Docker initiated." msgstr "" -#: mail/server/doctype/mail_server/mail_server.py:259 +#: mail/server/doctype/mail_server/mail_server.py:176 msgid "Install of Stalwart initiated." msgstr "" -#: mail/server/doctype/mail_server/mail_server.js:99 +#: mail/server/doctype/mail_server/mail_server.js:86 msgid "Installing Ansible..." msgstr "" -#: mail/server/doctype/mail_server/mail_server.js:113 +#: mail/server/doctype/mail_server/mail_server.js:100 msgid "Installing Docker..." msgstr "" -#: mail/server/doctype/mail_server/mail_server.js:127 +#: mail/server/doctype/mail_server/mail_server.js:114 msgid "Installing Stalwart..." msgstr "" @@ -4957,6 +4522,12 @@ msgstr "" msgid "Internal" msgstr "" +#. Description of the 'Poll Interval' (Data) field in DocType 'Mail Cluster +#. Store' +#: mail/server/doctype/mail_cluster_store/mail_cluster_store.json +msgid "Interval between polling for task status." +msgstr "" + #: mail/api/jmap.py:57 msgid "Invalid Content-Encoding. Expected 'aes128gcm'." msgstr "" @@ -4965,7 +4536,7 @@ msgstr "" msgid "Invalid JMAP Push Subscription keys: {0}" msgstr "" -#: mail/utils/validation.py:211 +#: mail/utils/validation.py:167 msgid "Invalid JMAP Structure" msgstr "" @@ -4973,16 +4544,16 @@ msgstr "" msgid "Invalid JMAP push private key length (must be 32 bytes)." msgstr "" -#: mail/utils/validation.py:242 mail/utils/validation.py:271 -#: mail/utils/validation.py:329 +#: mail/utils/validation.py:198 mail/utils/validation.py:227 +#: mail/utils/validation.py:285 msgid "Invalid Maildir" msgstr "" -#: mail/utils/validation.py:268 mail/utils/validation.py:326 +#: mail/utils/validation.py:224 mail/utils/validation.py:282 msgid "Invalid Maildir format in the following directories: {0}" msgstr "" -#: mail/utils/validation.py:239 +#: mail/utils/validation.py:195 msgid "Invalid Maildir format: at least one of {0} must exist and contain files." msgstr "" @@ -4990,7 +4561,7 @@ msgstr "" msgid "Invalid OTP. Please try again." msgstr "" -#: mail/api/jmap.py:140 +#: mail/api/jmap.py:141 msgid "Invalid Push Notification @type = {0}" msgstr "" @@ -5002,7 +4573,7 @@ msgstr "" msgid "Invalid date format: {0}" msgstr "" -#: mail/client/doctype/mail_queue/mail_queue.py:383 +#: mail/client/doctype/mail_queue/mail_queue.py:386 msgid "Invalid delivery mode: {0}" msgstr "" @@ -5010,18 +4581,22 @@ msgstr "" msgid "Invalid domain name" msgstr "" -#: mail/server/doctype/principal/principal.py:193 +#: mail/server/doctype/principal/principal.py:191 msgid "Invalid domain name provided for principal." msgstr "" -#: mail/client/doctype/mail_queue/mail_queue.py:495 +#: mail/client/doctype/mail_queue/mail_queue.py:498 msgid "Invalid file URL: {0}. File URLs must start with '/files/' or '/private/files/'." msgstr "" -#: mail/api/outbound.py:32 +#: mail/api/outbound.py:28 msgid "Invalid file type." msgstr "" +#: mail/stalwart/account.py:301 mail/stalwart/domain.py:49 +msgid "Invalid filter key: {0}. Allowed keys are: {1}" +msgstr "" + #: mail/client/doctype/push_subscription/push_subscription.py:389 msgid "Invalid nonce base length derived." msgstr "" @@ -5091,34 +4666,21 @@ msgstr "" msgid "It seems that the DNS provider or token is not configured in the {0}. Please manually add this DNS record to your provider for the root domain." msgstr "" -#: frontend/src/pages/MailboxView.vue:31 +#: frontend/src/pages/MailboxView.vue:36 msgid "Items in this mailbox will be automatically deleted after 30 days." msgstr "" -#. Label of the jmap_tab (Tab Break) field in DocType 'Mail Cluster' -#: mail/server/doctype/mail_cluster/mail_cluster.json -msgid "JMAP" -msgstr "" - -#: mail/client/doctype/user_settings/user_settings.js:21 -msgid "JMAP Connection" -msgstr "" - #. Label of the jmap_credentials_section (Section Break) field in DocType 'User #. Settings' #: mail/client/doctype/user_settings/user_settings.json msgid "JMAP Credentials" msgstr "" -#: mail/client/doctype/user_settings/user_settings.js:27 +#: mail/client/doctype/account_settings/account_settings.js:20 msgid "JMAP Identities" msgstr "" -#: mail/server/doctype/principal/principal.py:418 -msgid "JMAP Identities can only be synced for Individual principals." -msgstr "" - -#: mail/client/doctype/user_settings/user_settings.js:33 +#: mail/client/doctype/account_settings/account_settings.js:26 msgid "JMAP Mailboxes" msgstr "" @@ -5156,18 +4718,18 @@ msgstr "" msgid "JMAP Push keys generated successfully." msgstr "" -#. Label of the jmap_state_section (Section Break) field in DocType 'User -#. Settings' -#: mail/client/doctype/user_settings/user_settings.json -msgid "JMAP State" +#: mail/client/doctype/user_settings/user_settings.js:36 +msgid "JMAP Session cleared successfully" msgstr "" -#: mail/client/doctype/user_settings/user_settings.py:137 -msgid "JMAP Username must be the same as the User name." +#. Label of the jmap_states_section (Section Break) field in DocType 'Account +#. Settings' +#: mail/client/doctype/account_settings/account_settings.json +msgid "JMAP States" msgstr "" -#: mail/jmap/__init__.py:45 -msgid "JMAP username for local user {0} must be the same as the system username." +#: mail/client/doctype/user_settings/user_settings.py:117 +msgid "JMAP Username must be the same as the User name." msgstr "" #. Label of the job (Data) field in DocType 'Server Job' @@ -5188,7 +4750,7 @@ msgstr "" msgid "Junk" msgstr "" -#: frontend/src/pages/MailboxView.vue:1063 +#: frontend/src/pages/MailboxView.vue:1095 msgid "Junk status restored." msgstr "" @@ -5201,11 +4763,6 @@ msgstr "" msgid "Key" msgstr "" -#. Label of the eab_kid (Data) field in DocType 'Mail Server ACME Provider' -#: mail/server/doctype/mail_server_acme_provider/mail_server_acme_provider.json -msgid "Key ID" -msgstr "" - #. Label of the key_prefix (Data) field in DocType 'Mail Cluster Store' #: mail/server/doctype/mail_cluster_store/mail_cluster_store.json msgid "Key Prefix" @@ -5215,22 +4772,11 @@ msgstr "" msgid "Key derivation failed." msgstr "" -#. Description of the 'API Key' (Password) field in DocType 'Mail Cluster' -#: mail/server/doctype/mail_cluster/mail_cluster.json -msgid "Key for authenticating HTTP requests. Generate one via Actions > Generate API Key." -msgstr "" - #. Description of the 'Key' (Data) field in DocType 'Mail Settings' #: mail/mail/doctype/mail_settings/mail_settings.json msgid "Key used for authorization with the DNS provider." msgstr "" -#. Description of the 'In-Memory Storage' (Section Break) field in DocType -#. 'Mail Cluster' -#: mail/server/doctype/mail_cluster/mail_cluster.json -msgid "Key-value storage used primarily by the SMTP server and anti-spam components." -msgstr "" - #. Label of the keywords (JSON) field in DocType 'Mail Message' #: mail/client/doctype/mail_message/mail_message.json msgid "Keywords" @@ -5240,30 +4786,20 @@ msgstr "" #. Label of the kind (Select) field in DocType 'Event Participant' #: frontend/src/components/Modals/AddContactModal.vue:30 #: frontend/src/components/Modals/EditContactModal.vue:9 -#: frontend/src/pages/AddressBookView.vue:283 +#: frontend/src/pages/AddressBookView.vue:286 #: frontend/src/pages/ContactView.vue:17 -#: frontend/src/pages/ContactsView.vue:127 +#: frontend/src/pages/ContactsView.vue:142 #: mail/client/doctype/contact_card/contact_card.json #: mail/client/doctype/event_participant/event_participant.json msgid "Kind" msgstr "" -#. Option for the 'Protocol' (Select) field in DocType 'Mail Server Listener' -#: mail/server/doctype/mail_server_listener/mail_server_listener.json -msgid "LMTP" -msgstr "" - -#. Option for the 'Compression' (Select) field in DocType 'Mail Cluster Store' -#: mail/server/doctype/mail_cluster_store/mail_cluster_store.json -msgid "LZ4" -msgstr "" - #. Label of the label (Data) field in DocType 'Contact Card Email' #. Label of the label (Data) field in DocType 'Contact Card Phone' #: frontend/src/components/Modals/AddContactEmailModal.vue:18 #: frontend/src/components/Modals/AddContactPhoneModal.vue:13 -#: frontend/src/pages/ContactView.vue:391 -#: frontend/src/pages/ContactView.vue:397 +#: frontend/src/pages/ContactView.vue:394 +#: frontend/src/pages/ContactView.vue:400 #: mail/client/doctype/contact_card_email/contact_card_email.json #: mail/client/doctype/contact_card_phone/contact_card_phone.json msgid "Label" @@ -5279,9 +4815,9 @@ msgstr "" msgid "Last Active" msgstr "" -#. Label of the last_active_sieve_script_id (Data) field in DocType 'User +#. Label of the last_active_sieve_script_id (Data) field in DocType 'Account #. Settings' -#: mail/client/doctype/user_settings/user_settings.json +#: mail/client/doctype/account_settings/account_settings.json msgid "Last Active Sieve Script ID" msgstr "" @@ -5309,12 +4845,6 @@ msgstr "" msgid "Last Received Mail" msgstr "" -#. Description of the 'Current State (Email)' (Data) field in DocType 'User -#. Settings' -#: mail/client/doctype/user_settings/user_settings.json -msgid "Latest email state retrieved from the server for synchronization." -msgstr "" - #: frontend/src/components/Settings/AutomationSettings.vue:41 msgid "Learn more." msgstr "" @@ -5336,26 +4866,6 @@ msgstr "" msgid "Limited Access to GoDaddy DNS APIs" msgstr "" -#. Description of the 'Emails' (Int) field in DocType 'Mail Cluster' -#: mail/server/doctype/mail_cluster/mail_cluster.json -msgid "Limits the maximum number of e-mail message that can be parsed in a single request." -msgstr "" - -#. Description of the 'Calendars' (Int) field in DocType 'Mail Cluster' -#: mail/server/doctype/mail_cluster/mail_cluster.json -msgid "Limits the maximum number of iCalendar items that can be parsed in a single request." -msgstr "" - -#. Description of the 'Method Calls' (Int) field in DocType 'Mail Cluster' -#: mail/server/doctype/mail_cluster/mail_cluster.json -msgid "Limits the maximum number of method calls that can be included in a single request." -msgstr "" - -#. Description of the 'Contacts' (Int) field in DocType 'Mail Cluster' -#: mail/server/doctype/mail_cluster/mail_cluster.json -msgid "Limits the maximum number of vCard items that can be parsed in a single request." -msgstr "" - #. Label of the href (Data) field in DocType 'Event Link' #: mail/client/doctype/event_link/event_link.json msgid "Link" @@ -5396,62 +4906,48 @@ msgstr "" #. Description of the 'Volumes' (JSON) field in DocType 'Server Deployment #. Service' #: mail/server/doctype/server_deployment_service/server_deployment_service.json -msgid "List of volume mappings, e.g. [\"/data:/opt/data\"]." -msgstr "" - -#. Label of the listener_id (Data) field in DocType 'Mail Server Listener' -#: mail/server/doctype/mail_server_listener/mail_server_listener.json -msgid "Listener ID" -msgstr "" - -#. Label of the listeners (Table) field in DocType 'Mail Cluster' -#. Label of the listeners_tab (Tab Break) field in DocType 'Mail Cluster' -#. Label of the listeners_tab (Tab Break) field in DocType 'Mail Server' -#. Label of the listeners (Table) field in DocType 'Mail Server' -#: mail/server/doctype/mail_cluster/mail_cluster.json -#: mail/server/doctype/mail_server/mail_server.json -msgid "Listeners" +msgid "List of volume mappings, e.g. [\"/data:/etc/data\"]." msgstr "" #: frontend/src/pages/dashboard/UsersView.vue:152 msgid "Lists" msgstr "" -#: mail/client/doctype/mail_message/mail_message.js:161 +#: mail/client/doctype/mail_message/mail_message.js:181 msgid "Load Attachments" msgstr "" -#: mail/client/doctype/mail_message/mail_message.js:169 -#: mail/client/doctype/mail_queue/mail_queue.js:36 +#: mail/client/doctype/mail_message/mail_message.js:189 +#: mail/client/doctype/mail_queue/mail_queue.js:41 msgid "Load MIME Message" msgstr "" -#: mail/client/doctype/mail_message/mail_message.js:202 +#: mail/client/doctype/mail_message/mail_message.js:222 msgid "Loading Attachments..." msgstr "" -#: mail/client/doctype/mail_message/mail_message.js:216 -#: mail/client/doctype/mail_queue/mail_queue.js:62 +#: mail/client/doctype/mail_message/mail_message.js:236 +#: mail/client/doctype/mail_queue/mail_queue.js:87 msgid "Loading MIME Message..." msgstr "" -#: mail/client/doctype/mail_message/mail_message.js:111 +#: mail/client/doctype/mail_message/mail_message.js:131 msgid "Loading Mailboxes..." msgstr "" -#: mail/client/doctype/vacation_response/vacation_response.js:15 +#: mail/client/doctype/vacation_response/vacation_response.js:35 msgid "Loading Vacation Response..." msgstr "" -#: frontend/src/pages/MailboxView.vue:211 +#: frontend/src/pages/MailboxView.vue:210 msgid "Loading more mails..." msgstr "" -#: frontend/src/pages/ContactsView.vue:121 -#: frontend/src/pages/MailboxView.vue:54 -#: mail/client/doctype/mail_message/mail_message.js:71 -#: mail/client/doctype/mail_message/mail_message.js:84 -#: mail/client/doctype/mail_message/mail_message.js:97 +#: frontend/src/pages/ContactsView.vue:133 +#: frontend/src/pages/MailboxView.vue:53 +#: mail/client/doctype/mail_message/mail_message.js:91 +#: mail/client/doctype/mail_message/mail_message.js:104 +#: mail/client/doctype/mail_message/mail_message.js:117 msgid "Loading..." msgstr "" @@ -5462,7 +4958,7 @@ msgstr "" #. Label of the locality (Data) field in DocType 'Contact Card Address' #: frontend/src/components/Modals/AddContactAddressModal.vue:13 -#: frontend/src/pages/ContactView.vue:403 +#: frontend/src/pages/ContactView.vue:406 #: mail/client/doctype/contact_card_address/contact_card_address.json msgid "Locality" msgstr "" @@ -5484,38 +4980,18 @@ msgstr "" msgid "Lock timeout must be greater than 0 seconds." msgstr "" -#. Label of the log_metrics_tab (Tab Break) field in DocType 'Mail Cluster' -#: mail/server/doctype/mail_cluster/mail_cluster.json -msgid "Log & Metrics" -msgstr "" - #: frontend/src/pages/LoginView.vue:25 msgid "Log In" msgstr "" -#: frontend/src/components/AppSidebar.vue:175 +#: frontend/src/components/AppSidebar.vue:243 msgid "Log Out" msgstr "" -#. Option for the 'Method' (Select) field in DocType 'Mail Cluster Trace' -#: mail/server/doctype/mail_cluster_trace/mail_cluster_trace.json -msgid "Log file" -msgstr "" - -#. Label of the level (Select) field in DocType 'Mail Cluster Trace' -#: mail/server/doctype/mail_cluster_trace/mail_cluster_trace.json -msgid "Logging Level" -msgstr "" - -#: frontend/src/components/Modals/SearchModal.vue:48 +#: frontend/src/components/Modals/SearchModal.vue:51 msgid "Look In" msgstr "" -#. Label of the lossy (Check) field in DocType 'Mail Cluster Trace' -#: mail/server/doctype/mail_cluster_trace/mail_cluster_trace.json -msgid "Lossy mode" -msgstr "" - #. Option for the 'Priority' (Select) field in DocType 'Mail Queue' #: mail/client/doctype/mail_queue/mail_queue.json msgid "Low" @@ -5532,7 +5008,7 @@ msgstr "" msgid "MX" msgstr "" -#. Label of the machine (Data) field in DocType 'Mail Cluster Store' +#. Label of the machine_id (Data) field in DocType 'Mail Cluster Store' #: mail/server/doctype/mail_cluster_store/mail_cluster_store.json msgid "Machine ID" msgstr "" @@ -5562,7 +5038,7 @@ msgstr "" msgid "Mail Admin" msgstr "" -#: mail/backend.py:94 +#: mail/backend.py:93 msgid "Mail Backend Request Failed" msgstr "" @@ -5577,15 +5053,15 @@ msgid "Mail Cluster Store" msgstr "" #. Name of a DocType -#: mail/server/doctype/mail_cluster_trace/mail_cluster_trace.json -msgid "Mail Cluster Trace" +#: mail/server/doctype/mail_cluster_store_http_auth/mail_cluster_store_http_auth.json +msgid "Mail Cluster Store HTTP Auth" msgstr "" -#: mail/server/doctype/mail_cluster/mail_cluster.py:123 +#: mail/server/doctype/mail_cluster/mail_cluster.py:91 msgid "Mail Cluster {0} already exists." msgstr "" -#: mail/server/doctype/mail_server/mail_server.py:63 +#: mail/server/doctype/mail_server/mail_server.py:52 msgid "Mail Cluster {0} is disabled." msgstr "" @@ -5594,7 +5070,7 @@ msgstr "" msgid "Mail Data Exchange" msgstr "" -#: mail/client/doctype/mail_exchange/mail_exchange.py:908 +#: mail/client/doctype/mail_exchange/mail_exchange.py:910 #: mail/server/doctype/mail_data_exchange/mail_data_exchange.py:309 msgid "Mail Data {0} {1}" msgstr "" @@ -5641,23 +5117,23 @@ msgstr "" msgid "Mail Message Recipient" msgstr "" -#: mail/client/doctype/mail_message/mail_message.py:463 +#: mail/client/doctype/mail_message/mail_message.py:466 msgid "Mail Message does not have a blob ID." msgstr "" -#: mail/client/doctype/mail_message/mail_message.py:284 +#: mail/client/doctype/mail_message/mail_message.py:287 msgid "Mail Message {0} is a draft. Please send it before performing this action." msgstr "" -#: mail/client/doctype/mail_message/mail_message.py:505 +#: mail/client/doctype/mail_message/mail_message.py:508 msgid "Mail Message {0} is not a draft." msgstr "" -#: mail/client/doctype/user_settings/user_settings.js:45 +#: mail/client/doctype/account_settings/account_settings.js:38 msgid "Mail Messages" msgstr "" -#: mail/client/doctype/mail_message/mail_message.py:588 +#: mail/client/doctype/mail_message/mail_message.py:591 msgid "Mail Messages deleted successfully." msgstr "" @@ -5673,32 +5149,17 @@ msgstr "" msgid "Mail Server" msgstr "" -#. Name of a DocType -#: mail/server/doctype/mail_server_acme_provider/mail_server_acme_provider.json -msgid "Mail Server ACME Provider" -msgstr "" - -#. Name of a DocType -#: mail/server/doctype/mail_server_listener/mail_server_listener.json -msgid "Mail Server Listener" -msgstr "" - -#. Name of a DocType -#: mail/server/doctype/mail_server_tls_certificate/mail_server_tls_certificate.json -msgid "Mail Server TLS Certificate" -msgstr "" - -#: mail/server/doctype/mail_server/mail_server.py:54 +#: mail/server/doctype/mail_server/mail_server.py:43 msgid "Mail Server {0} already exists." msgstr "" #: mail/server/doctype/server_ansible_play/server_ansible_play.py:49 -#: mail/server/doctype/server_deployment/server_deployment.py:121 +#: mail/server/doctype/server_deployment/server_deployment.py:86 #: mail/server/doctype/server_job/server_job.py:49 msgid "Mail Server {0} is disabled" msgstr "" -#: mail/server/doctype/mail_cluster/mail_cluster.py:116 +#: mail/server/doctype/mail_cluster/mail_cluster.py:84 msgid "Mail Server {0} is enabled. Please disable it first." msgstr "" @@ -5720,43 +5181,43 @@ msgstr "" msgid "Mail Sync History" msgstr "" -#: mail/client/doctype/mail_sync_history/mail_sync_history.py:20 -msgid "Mail Sync History already exists for this source and user." +#: mail/client/doctype/mail_sync_history/mail_sync_history.py:23 +msgid "Mail Sync History already exists for this account and source." msgstr "" -#: mail/utils/__init__.py:117 +#: mail/utils/__init__.py:116 msgid "Mail config key '{0}' has invalid type. Expected {1}." msgstr "" -#: mail/utils/__init__.py:128 +#: mail/utils/__init__.py:127 msgid "Mail config key '{0}' is not set" msgstr "" -#: mail/utils/__init__.py:124 +#: mail/utils/__init__.py:123 msgid "Mail config key '{0}' not found" msgstr "" -#: mail/utils/validation.py:352 +#: mail/utils/validation.py:310 msgid "Mail configuration is not set." msgstr "" -#: frontend/src/components/MailActions.vue:271 +#: frontend/src/components/MailActions.vue:278 msgid "Mail deleted." msgstr "" -#: frontend/src/components/MailActions.vue:224 +#: frontend/src/components/MailActions.vue:231 msgid "Mail marked as Junk." msgstr "" -#: frontend/src/components/MailActions.vue:224 +#: frontend/src/components/MailActions.vue:231 msgid "Mail marked as Not Junk." msgstr "" -#: frontend/src/components/MailActions.vue:250 +#: frontend/src/components/MailActions.vue:257 msgid "Mail moved back to {0}." msgstr "" -#: frontend/src/components/MailActions.vue:257 +#: frontend/src/components/MailActions.vue:264 msgid "Mail moved to {0}." msgstr "" @@ -5764,7 +5225,7 @@ msgstr "" msgid "Mail routing records that ensure messages sent to your domain are delivered to the correct mail server." msgstr "" -#: mail/utils/validation.py:355 +#: mail/utils/validation.py:313 msgid "Mail server URL is not set in Mail Configuration." msgstr "" @@ -5775,49 +5236,45 @@ msgstr "" #. Label of the mailbox (Link) field in DocType 'Mail Message Mailbox' #. Name of a DocType #. Label of a Workspace Sidebar Item -#: frontend/src/components/AppSidebar.vue:153 +#: frontend/src/components/AppSidebar.vue:167 #: mail/client/doctype/mail_message_mailbox/mail_message_mailbox.json #: mail/client/doctype/mailbox/mailbox.json mail/workspace_sidebar/client.json msgid "Mailbox" msgstr "" -#: mail/client/doctype/mailbox/mailbox.py:132 +#: mail/client/doctype/mailbox/mailbox.py:133 msgid "Mailbox Creation Error" msgstr "" -#: mail/client/doctype/mailbox/mailbox.py:213 +#: mail/client/doctype/mailbox/mailbox.py:214 msgid "Mailbox Deletion Error" msgstr "" -#: mail/client/doctype/mailbox/mailbox.py:212 +#: mail/client/doctype/mailbox/mailbox.py:213 msgid "Mailbox Deletion Error(s):
{0}" msgstr "" #. Label of the mailbox_id (Data) field in DocType 'Mail Message Mailbox' #. Label of the mailbox_id (Data) field in DocType 'Mail Queue' +#. Label of the id (Data) field in DocType 'Mailbox' #. Label of the mailbox_id (Data) field in DocType 'Mailbox Settings' #: mail/client/doctype/mail_message_mailbox/mail_message_mailbox.json #: mail/client/doctype/mail_queue/mail_queue.json +#: mail/client/doctype/mailbox/mailbox.json #: mail/client/doctype/mailbox_settings/mailbox_settings.json msgid "Mailbox ID" msgstr "" -#. Label of the mailbox_limits_section (Section Break) field in DocType 'Mail -#. Cluster' -#: mail/server/doctype/mail_cluster/mail_cluster.json -msgid "Mailbox Limits" -msgstr "" - #. Label of the mailbox_name (Data) field in DocType 'Mail Message Mailbox' #: mail/client/doctype/mail_message_mailbox/mail_message_mailbox.json msgid "Mailbox Name" msgstr "" -#: mail/client/doctype/mailbox/mailbox.py:155 +#: mail/client/doctype/mailbox/mailbox.py:156 msgid "Mailbox Not Found" msgstr "" -#: mail/client/doctype/mailbox/mailbox.py:343 +#: mail/client/doctype/mailbox/mailbox.py:346 msgid "Mailbox Position Update Error" msgstr "" @@ -5826,28 +5283,32 @@ msgstr "" msgid "Mailbox Settings" msgstr "" -#: mail/client/doctype/mailbox_settings/mailbox_settings.py:34 -msgid "Mailbox Settings for user {0} with mailbox ID {1} already exists." +#: mail/client/doctype/mailbox_settings/mailbox_settings.py:39 +msgid "Mailbox Settings for account {0} with mailbox ID {1} already exists." msgstr "" -#: mail/client/doctype/mailbox_settings/mailbox_settings.py:60 -msgid "Mailbox Settings for user {0} with mailbox ID {1} not found." +#: mail/client/doctype/mailbox_settings/mailbox_settings.py:67 +msgid "Mailbox Settings for account {0} with mailbox ID {1} not found." msgstr "" -#: mail/client/doctype/mailbox/mailbox.py:173 +#: mail/client/doctype/mailbox/mailbox.py:174 msgid "Mailbox Update Error" msgstr "" -#: mail/client/doctype/mailbox/mailbox.py:175 +#: mail/client/doctype/mailbox/mailbox.py:176 msgid "Mailbox cannot be a parent of itself." msgstr "" -#: mail/client/doctype/mail_exchange/mail_exchange.py:334 +#: mail/client/doctype/mail_exchange/mail_exchange.py:333 msgid "Mailbox not found for folder {0}" msgstr "" -#: mail/client/doctype/mailbox/mailbox.py:154 -msgid "Mailbox with ID {0} not found in user {1}." +#: mail/client/doctype/mailbox/mailbox.py:155 +msgid "Mailbox with ID {0} not found in account {1}." +msgstr "" + +#: mail/api/sieve.py:212 +msgid "Mailbox with name '{0}' not found." msgstr "" #. Label of the mailboxes_section (Section Break) field in DocType 'Mail @@ -5856,7 +5317,7 @@ msgstr "" msgid "Mailboxes" msgstr "" -#: mail/client/doctype/mailbox/mailbox.py:103 +#: mail/client/doctype/mailbox/mailbox.py:104 msgid "Mailboxes deleted successfully." msgstr "" @@ -5871,7 +5332,7 @@ msgid "Mailing List created." msgstr "" #. Label of the lists (Table) field in DocType 'Principal' -#: frontend/src/components/AppSidebar.vue:196 +#: frontend/src/components/AppSidebar.vue:266 #: frontend/src/pages/dashboard/MailingListView.vue:262 #: frontend/src/pages/dashboard/MailingListsView.vue:3 #: frontend/src/pages/dashboard/MailingListsView.vue:63 @@ -5892,7 +5353,11 @@ msgstr "" msgid "Mailing lists deleted." msgstr "" -#: frontend/src/pages/MailboxView.vue:1184 +#: frontend/src/components/MailActions.vue:301 +msgid "Mails" +msgstr "" + +#: frontend/src/pages/MailboxView.vue:1216 msgid "Mails With Attachments" msgstr "" @@ -5900,212 +5365,130 @@ msgstr "" msgid "Malformed encrypted payload (invalid key length)." msgstr "" -#. Description of the 'ACME Providers' (Table) field in DocType 'Mail Server' -#: mail/server/doctype/mail_server/mail_server.json -msgid "Manage ACME TLS certificate providers." -msgstr "" - -#. Description of the 'Listeners' (Table) field in DocType 'Mail Cluster' -#: mail/server/doctype/mail_cluster/mail_cluster.json -msgid "Manage SMTP, IMAP, HTTP, and other listeners." -msgstr "" - -#. Description of the 'TLS Certificates' (Table) field in DocType 'Mail Server' -#: mail/server/doctype/mail_server/mail_server.json -msgid "Manage TLS certificates." -msgstr "" - -#. Description of the 'Stores' (Table) field in DocType 'Mail Cluster' -#: mail/server/doctype/mail_cluster/mail_cluster.json -msgid "Manage data, blob, full-text, and lookup stores." -msgstr "" - -#. Description of the 'Traces' (Table) field in DocType 'Mail Cluster' -#: mail/server/doctype/mail_cluster/mail_cluster.json -msgid "Manage logging and tracing methods." -msgstr "" - -#. Option for the 'Protocol' (Select) field in DocType 'Mail Server Listener' -#: mail/server/doctype/mail_server_listener/mail_server_listener.json -msgid "ManageSieve" -msgstr "" - #. Label of the mandatory (Check) field in DocType 'Principal DNS Record' #: mail/server/doctype/principal_dns_record/principal_dns_record.json msgid "Mandatory" msgstr "" -#: frontend/src/components/MailActions.vue:159 +#: frontend/src/components/MailActions.vue:184 +msgid "Mark Unread from Here" +msgstr "" + +#: frontend/src/components/MailActions.vue:160 #: frontend/src/components/Modals/ShortcutsModal.vue:72 msgid "Mark as Junk" msgstr "" -#: frontend/src/components/MailThread.vue:477 -#: frontend/src/pages/MailboxView.vue:655 +#: frontend/src/components/MailThread.vue:564 +#: frontend/src/pages/MailboxView.vue:659 msgid "Mark as Junk (!)" msgstr "" -#: frontend/src/components/MailActions.vue:165 -#: frontend/src/components/MailThread.vue:485 -#: frontend/src/pages/MailboxView.vue:668 +#: frontend/src/components/MailActions.vue:166 +#: frontend/src/components/MailThread.vue:572 +#: frontend/src/pages/MailboxView.vue:672 msgid "Mark as Not Junk" msgstr "" -#: frontend/src/components/MailListItem.vue:276 -#: frontend/src/components/Modals/FolderModal.vue:118 +#: frontend/src/components/MailListItem.vue:280 +#: frontend/src/components/Modals/FolderModal.vue:119 #: frontend/src/components/Modals/ShortcutsModal.vue:74 #: frontend/src/components/Settings/ImportSettings.vue:21 msgid "Mark as Read" msgstr "" -#: frontend/src/pages/MailboxView.vue:692 +#: frontend/src/pages/MailboxView.vue:696 msgid "Mark as Read (Shift+U)" msgstr "" -#: frontend/src/components/MailListItem.vue:270 +#: frontend/src/components/MailActions.vue:184 +#: frontend/src/components/MailListItem.vue:274 #: frontend/src/components/Modals/ShortcutsModal.vue:73 msgid "Mark as Unread" msgstr "" -#: frontend/src/components/MailThread.vue:503 -#: frontend/src/pages/MailboxView.vue:704 +#: frontend/src/components/MailThread.vue:590 +#: frontend/src/pages/MailboxView.vue:708 msgid "Mark as Unread (U)" msgstr "" -#: frontend/src/pages/MailboxView.vue:921 +#: frontend/src/pages/MailboxView.vue:953 msgid "Mark {0} {1} as Junk" msgstr "" -#: frontend/src/components/MailActions.vue:231 -#: frontend/src/pages/MailboxView.vue:1067 +#: frontend/src/components/MailActions.vue:238 +#: frontend/src/pages/MailboxView.vue:1099 msgid "Marking as Junk..." msgstr "" -#: frontend/src/components/MailActions.vue:231 -#: frontend/src/pages/MailboxView.vue:1067 +#: frontend/src/components/MailActions.vue:238 +#: frontend/src/pages/MailboxView.vue:1099 msgid "Marking as Not Junk..." msgstr "" -#: frontend/src/pages/MailboxView.vue:1010 +#: frontend/src/pages/MailboxView.vue:1042 msgid "Marking as read..." msgstr "" -#: frontend/src/pages/MailboxView.vue:1010 +#: frontend/src/pages/MailboxView.vue:1042 msgid "Marking as unread..." msgstr "" -#: frontend/src/components/Modals/FolderModal.vue:103 +#: frontend/src/components/Modals/FolderModal.vue:104 msgid "Match If" msgstr "" #. Label of the max_allowed_packet (Int) field in DocType 'Mail Cluster Store' #: mail/server/doctype/mail_cluster_store/mail_cluster_store.json -msgid "Max Allowed Packet (Bytes)" -msgstr "" - -#. Label of the jmap_push_attempts_max (Int) field in DocType 'Mail Cluster' -#: mail/server/doctype/mail_cluster/mail_cluster.json -msgid "Max Attempts" -msgstr "" - -#. Label of the jmap_protocol_upload_max_concurrent (Int) field in DocType -#. 'Mail Cluster' -#: mail/server/doctype/mail_cluster/mail_cluster.json -msgid "Max Concurrent" -msgstr "" - -#. Label of the pool_max_connections (Int) field in DocType 'Mail Cluster -#. Store' -#. Label of the server_max_connections (Int) field in DocType 'Mail Server' -#: mail/server/doctype/mail_cluster_store/mail_cluster_store.json -#: mail/server/doctype/mail_server/mail_server.json -msgid "Max Connections" -msgstr "" - -#. Label of the jmap_mailbox_max_depth (Int) field in DocType 'Mail Cluster' -#: mail/server/doctype/mail_cluster/mail_cluster.json -msgid "Max Depth" +msgid "Max Allowed Packet" msgstr "" #: frontend/src/components/Settings/ExportSettings.vue:59 msgid "Max Number of Emails" msgstr "" -#. Label of the max_objects_section (Section Break) field in DocType 'Mail -#. Cluster' -#: mail/server/doctype/mail_cluster/mail_cluster.json -msgid "Max Objects" -msgstr "" - -#. Label of the max_results_section (Section Break) field in DocType 'Mail -#. Cluster' -#: mail/server/doctype/mail_cluster/mail_cluster.json -msgid "Max Results" -msgstr "" - #. Label of the max_retries (Int) field in DocType 'Mail Queue' +#. Label of the max_retries (Int) field in DocType 'Mail Cluster Store' #. Label of the max_retries (Int) field in DocType 'Server Ansible Play' #. Label of the max_retries (Int) field in DocType 'Server Deployment' #. Label of the max_retries (Int) field in DocType 'Server Job' #: mail/client/doctype/mail_queue/mail_queue.json +#: mail/server/doctype/mail_cluster_store/mail_cluster_store.json #: mail/server/doctype/server_ansible_play/server_ansible_play.json #: mail/server/doctype/server_deployment/server_deployment.json #: mail/server/doctype/server_job/server_job.json msgid "Max Retries" msgstr "" -#. Label of the transaction_max_retry_delay (Int) field in DocType 'Mail -#. Cluster Store' +#. Label of the max_retry_wait (Data) field in DocType 'Mail Cluster Store' #: mail/server/doctype/mail_cluster_store/mail_cluster_store.json -msgid "Max Retry Delay (Seconds)" +msgid "Max Retry Wait" msgstr "" #. Label of the max_size (Int) field in DocType 'Mail Message' -#. Label of the jmap_protocol_upload_max_size (Int) field in DocType 'Mail -#. Cluster' #: mail/client/doctype/mail_message/mail_message.json -#: mail/server/doctype/mail_cluster/mail_cluster.json msgid "Max Size (Bytes)" msgstr "" -#. Label of the retry_max_wait (Int) field in DocType 'Mail Cluster Store' -#: mail/server/doctype/mail_cluster_store/mail_cluster_store.json -msgid "Max Wait (Milliseconds)" -msgstr "" - -#. Description of the 'Timeout (Seconds)' (Int) field in DocType 'Mail Cluster' -#. Description of the 'Timeout (Seconds)' (Int) field in DocType 'Mail Cluster -#. Trace' -#: mail/server/doctype/mail_cluster/mail_cluster.json -#: mail/server/doctype/mail_cluster_trace/mail_cluster_trace.json -msgid "Maximum amount of time that Stalwart will wait for a response from the OpenTelemetry endpoint." -msgstr "" - -#. Description of the 'Nested Depth' (Int) field in DocType 'Mail Cluster -#. Store' +#. Description of the 'Depth' (Int) field in DocType 'Mail Cluster Store' #: mail/server/doctype/mail_cluster_store/mail_cluster_store.json msgid "Maximum depth of nested directories." msgstr "" -#. Description of the 'Max Connections' (Int) field in DocType 'Mail Cluster -#. Store' +#. Description of the 'Pool Max Connections' (Int) field in DocType 'Mail +#. Cluster Store' #: mail/server/doctype/mail_cluster_store/mail_cluster_store.json msgid "Maximum number of connections to the store." msgstr "" -#. Description of the 'Max Attempts' (Int) field in DocType 'Mail Cluster' -#: mail/server/doctype/mail_cluster/mail_cluster.json -msgid "Maximum number of push attempts before a notification is discarded." -msgstr "" - -#. Description of the 'Max Allowed Packet (Bytes)' (Int) field in DocType 'Mail -#. Cluster Store' +#. Description of the 'Max Allowed Packet' (Int) field in DocType 'Mail Cluster +#. Store' #: mail/server/doctype/mail_cluster_store/mail_cluster_store.json msgid "Maximum size of a packet in bytes." msgstr "" -#. Description of the 'Max Wait (Milliseconds)' (Int) field in DocType 'Mail -#. Cluster Store' +#. Description of the 'Max Retry Wait' (Data) field in DocType 'Mail Cluster +#. Store' #: mail/server/doctype/mail_cluster_store/mail_cluster_store.json msgid "Maximum time to wait between retries." msgstr "" @@ -6231,6 +5614,11 @@ msgstr "" msgid "May Write Own" msgstr "" +#. Option for the 'Type' (Select) field in DocType 'Mail Cluster Store' +#: mail/server/doctype/mail_cluster_store/mail_cluster_store.json +msgid "Meilisearch" +msgstr "" + #. Label of the member (Data) field in DocType 'Principal Member' #: mail/server/doctype/principal_member/principal_member.json msgid "Member" @@ -6257,7 +5645,7 @@ msgstr "" #. Label of the members (Table) field in DocType 'Principal' #. Label of the total_members (Int) field in DocType 'Principal' -#: frontend/src/components/AppSidebar.vue:190 +#: frontend/src/components/AppSidebar.vue:260 #: frontend/src/pages/dashboard/MailingListView.vue:52 #: frontend/src/pages/dashboard/MailingListsView.vue:112 #: frontend/src/pages/dashboard/MemberView.vue:287 @@ -6280,7 +5668,6 @@ msgstr "" #. Label of the message (Code) field in DocType 'Mail Message' #. Label of the message_section (Section Break) field in DocType 'Mail Queue' #. Label of the message (Code) field in DocType 'Mail Queue' -#. Label of the message (Code) field in DocType 'Message Queue' #. Label of the message (Code) field in DocType 'Spam Check Log' #. Label of the message_section (Section Break) field in DocType 'Spam Check #. Log' @@ -6288,7 +5675,6 @@ msgstr "" #: frontend/src/components/Settings/VacationResponseSettings.vue:29 #: mail/client/doctype/mail_message/mail_message.json #: mail/client/doctype/mail_queue/mail_queue.json -#: mail/server/doctype/message_queue/message_queue.json #: mail/server/doctype/spam_check_log/spam_check_log.json #: mail/workspace_sidebar/client.json msgid "Message" @@ -6296,7 +5682,7 @@ msgstr "" #. Label of the message_id (Data) field in DocType 'Mail Message' #. Label of the message_id (Data) field in DocType 'Mail Queue' -#: mail/api/mail.py:426 mail/client/doctype/mail_message/mail_message.json +#: mail/api/mail.py:421 mail/client/doctype/mail_message/mail_message.json #: mail/client/doctype/mail_queue/mail_queue.json msgid "Message ID" msgstr "" @@ -6305,32 +5691,16 @@ msgstr "" msgid "Message Information" msgstr "" -#. Name of a DocType #. Label of a Workspace Sidebar Item -#: mail/server/doctype/message_queue/message_queue.json -#: mail/workspace_sidebar/server.json -msgid "Message Queue" -msgstr "" - -#. Name of a DocType -#: mail/server/doctype/message_queue_recipient/message_queue_recipient.json -msgid "Message Queue Recipient" -msgstr "" - -#. Label of the message_size (Int) field in DocType 'Message Queue' -#: mail/server/doctype/message_queue/message_queue.json -msgid "Message Size" -msgstr "" - -#: mail/server/doctype/message_queue/message_queue.py:28 -msgid "Message deleted successfully." +#: mail/workspace_sidebar/server.json +msgid "Message Queue" msgstr "" -#: mail/client/doctype/mail_message/mail_message.py:185 +#: mail/client/doctype/mail_message/mail_message.py:187 msgid "Message not found or you do not have permission to view it." msgstr "" -#: frontend/src/components/ComposeMailEditor.vue:439 +#: frontend/src/components/ComposeMailEditor.vue:450 msgid "Message sent." msgstr "" @@ -6355,36 +5725,25 @@ msgstr "" msgid "Metadata" msgstr "" -#. Label of the type (Select) field in DocType 'Mail Cluster Trace' -#: mail/server/doctype/mail_cluster_trace/mail_cluster_trace.json -msgid "Method" -msgstr "" - -#. Label of the jmap_protocol_request_max_calls (Int) field in DocType 'Mail -#. Cluster' -#: mail/server/doctype/mail_cluster/mail_cluster.json -msgid "Method Calls" -msgstr "" - #. Label of the method_path (Data) field in DocType 'Rate Limit' #: mail/mail/doctype/rate_limit/rate_limit.json msgid "Method Path" msgstr "" +#. Description of the 'Pool Recycling Method' (Select) field in DocType 'Mail +#. Cluster Store' +#: mail/server/doctype/mail_cluster_store/mail_cluster_store.json +msgid "Method to use when recycling connections in the pool." +msgstr "" + #. Label of the methods (Small Text) field in DocType 'Rate Limit' #: mail/mail/doctype/rate_limit/rate_limit.json msgid "Methods" msgstr "" -#. Label of the min_blob_size (Int) field in DocType 'Mail Cluster Store' -#: mail/server/doctype/mail_cluster_store/mail_cluster_store.json -msgid "Min Blob Size (Bytes)" -msgstr "" - -#. Label of the pool_min_connections (Int) field in DocType 'Mail Cluster -#. Store' +#. Label of the min_retry_wait (Data) field in DocType 'Mail Cluster Store' #: mail/server/doctype/mail_cluster_store/mail_cluster_store.json -msgid "Min Connections" +msgid "Min Retry Wait" msgstr "" #. Label of the min_size (Int) field in DocType 'Mail Message' @@ -6392,36 +5751,28 @@ msgstr "" msgid "Min Size (Bytes)" msgstr "" -#. Label of the retry_min_wait (Int) field in DocType 'Mail Cluster Store' -#: mail/server/doctype/mail_cluster_store/mail_cluster_store.json -msgid "Min Wait (Milliseconds)" -msgstr "" - -#. Description of the 'Min Connections' (Int) field in DocType 'Mail Cluster -#. Store' +#. Description of the 'Pool Min Connections' (Int) field in DocType 'Mail +#. Cluster Store' #: mail/server/doctype/mail_cluster_store/mail_cluster_store.json msgid "Minimum number of connections to the store." msgstr "" -#. Description of the 'Min Blob Size (Bytes)' (Int) field in DocType 'Mail -#. Cluster Store' +#. Description of the 'Blob Size' (Int) field in DocType 'Mail Cluster Store' #: mail/server/doctype/mail_cluster_store/mail_cluster_store.json msgid "Minimum size of a blob to store in the blob store, smaller blobs are stored in the metadata store." msgstr "" -#. Description of the 'Min Wait (Milliseconds)' (Int) field in DocType 'Mail -#. Cluster Store' +#. Description of the 'Min Retry Wait' (Data) field in DocType 'Mail Cluster +#. Store' #: mail/server/doctype/mail_cluster_store/mail_cluster_store.json msgid "Minimum time to wait between retries." msgstr "" -#. Option for the 'Rotate Frequency' (Select) field in DocType 'Mail Cluster -#. Trace' -#: mail/server/doctype/mail_cluster_trace/mail_cluster_trace.json -msgid "Minutely" +#: mail/stalwart/cli.py:116 +msgid "Missing mandatory credential field: {0}" msgstr "" -#: mail/utils/validation.py:208 +#: mail/utils/validation.py:164 msgid "Missing required files/directories for JMAP format: {0}" msgstr "" @@ -6446,48 +5797,48 @@ msgstr "" msgid "More Info" msgstr "" -#: mail/client/doctype/mail_message/mail_message.js:129 +#: mail/client/doctype/mail_message/mail_message.js:149 msgid "Move" msgstr "" #: frontend/src/components/MailThread.vue:36 -#: frontend/src/pages/MailboxView.vue:110 +#: frontend/src/pages/MailboxView.vue:109 msgid "Move To" msgstr "" -#: mail/client/doctype/mail_message/mail_message.js:127 +#: mail/client/doctype/mail_message/mail_message.js:147 msgid "Move to " msgstr "" -#: frontend/src/components/MailActions.vue:171 -#: frontend/src/components/MailListItem.vue:282 +#: frontend/src/components/MailActions.vue:172 +#: frontend/src/components/MailListItem.vue:286 #: frontend/src/components/Modals/ShortcutsModal.vue:75 msgid "Move to Trash" msgstr "" -#: frontend/src/components/MailThread.vue:491 -#: frontend/src/pages/MailboxView.vue:680 +#: frontend/src/components/MailThread.vue:578 +#: frontend/src/pages/MailboxView.vue:684 msgid "Move to Trash (Delete)" msgstr "" -#: mail/client/doctype/mail_message/mail_message.js:181 +#: mail/client/doctype/mail_message/mail_message.js:201 msgid "Moving to Mailbox..." msgstr "" -#: frontend/src/components/MailActions.vue:256 -#: frontend/src/pages/MailboxView.vue:1048 +#: frontend/src/components/MailActions.vue:263 +#: frontend/src/pages/MailboxView.vue:1080 msgid "Moving to {0}..." msgstr "" -#. Label of the multiline (Check) field in DocType 'Mail Cluster Trace' -#: mail/server/doctype/mail_cluster_trace/mail_cluster_trace.json -msgid "Multiline entries" -msgstr "" - #: mail/server/doctype/mail_data_exchange/mail_data_exchange.py:193 msgid "Multiple {0} files found. Please provide only one." msgstr "" +#. Option for the 'Type' (Select) field in DocType 'Mail Cluster Store' +#: mail/server/doctype/mail_cluster_store/mail_cluster_store.json +msgid "MySql" +msgstr "" + #. Option for the 'Status' (Select) field in DocType 'Event Participant' #: mail/client/doctype/event_participant/event_participant.json msgid "NEEDS-ACTION" @@ -6503,8 +5854,9 @@ msgstr "" #. Label of the _name (Data) field in DocType 'Participant Identity' #. Label of the _name (Data) field in DocType 'Quota' #. Label of the _name (Data) field in DocType 'Sieve Script' +#. Label of the _name (Data) field in DocType 'User Account' #. Label of the _name (Data) field in DocType 'Principal' -#: frontend/src/components/Modals/AddAddressBookContactsModal.vue:95 +#: frontend/src/components/Modals/AddAddressBookContactsModal.vue:102 #: frontend/src/components/Modals/AddAddressBookModal.vue:20 #: frontend/src/components/Modals/AddContactModal.vue:24 #: frontend/src/components/Modals/AddEmailModal.vue:34 @@ -6512,12 +5864,12 @@ msgstr "" #: frontend/src/components/Modals/ContactsModal.vue:126 #: frontend/src/components/Modals/EditAddressBookModal.vue:7 #: frontend/src/components/Modals/EditContactModal.vue:5 -#: frontend/src/components/Modals/FolderModal.vue:23 +#: frontend/src/components/Modals/FolderModal.vue:24 #: frontend/src/pages/AddressBookView.vue:19 -#: frontend/src/pages/AddressBookView.vue:282 -#: frontend/src/pages/AddressBooksView.vue:77 -#: frontend/src/pages/ContactView.vue:16 frontend/src/pages/ContactView.vue:386 -#: frontend/src/pages/ContactsView.vue:126 +#: frontend/src/pages/AddressBookView.vue:285 +#: frontend/src/pages/AddressBooksView.vue:79 +#: frontend/src/pages/ContactView.vue:16 frontend/src/pages/ContactView.vue:389 +#: frontend/src/pages/ContactsView.vue:141 #: frontend/src/pages/dashboard/MailingListView.vue:246 #: frontend/src/pages/dashboard/MailingListsView.vue:110 #: mail/client/doctype/address_book/address_book.json @@ -6530,8 +5882,8 @@ msgstr "" #: mail/client/doctype/participant_identity/participant_identity.json #: mail/client/doctype/quota/quota.json #: mail/client/doctype/sieve_script/sieve_script.json +#: mail/client/doctype/user_account/user_account.json #: mail/server/doctype/principal/principal.json -#: mail/server/report/dmarc_report_viewer/dmarc_report_viewer.py:22 msgid "Name" msgstr "" @@ -6540,12 +5892,6 @@ msgstr "" msgid "Name Breakup" msgstr "" -#. Label of the jmap_mailbox_max_name_length (Int) field in DocType 'Mail -#. Cluster' -#: mail/server/doctype/mail_cluster/mail_cluster.json -msgid "Name Length" -msgstr "" - #. Description of the 'Container' (Data) field in DocType 'Server Deployment #. Service' #: mail/server/doctype/server_deployment_service/server_deployment_service.json @@ -6572,11 +5918,6 @@ msgstr "" msgid "Navigation" msgstr "" -#. Label of the depth (Int) field in DocType 'Mail Cluster Store' -#: mail/server/doctype/mail_cluster_store/mail_cluster_store.json -msgid "Nested Depth" -msgstr "" - #. Label of the network_mode (Select) field in DocType 'Server Deployment #. Service' #: mail/server/doctype/server_deployment_service/server_deployment_service.json @@ -6595,14 +5936,12 @@ msgstr "" msgid "Networking" msgstr "" -#. Option for the 'Rotate Frequency' (Select) field in DocType 'Mail Cluster -#. Trace' #: frontend/src/pages/dashboard/UsersView.vue:82 -#: mail/server/doctype/mail_cluster_trace/mail_cluster_trace.json msgid "Never" msgstr "" #: frontend/src/components/Settings/AutomationSettings.vue:4 +#: frontend/src/components/Settings/FolderSettings.vue:4 #: frontend/src/components/Settings/SignatureSettings.vue:4 msgid "New" msgstr "" @@ -6623,7 +5962,7 @@ msgstr "" msgid "New External Member" msgstr "" -#: frontend/src/components/AppSidebar.vue:253 +#: frontend/src/components/AppSidebar.vue:331 #: frontend/src/components/Modals/FolderModal.vue:5 msgid "New Folder" msgstr "" @@ -6670,20 +6009,6 @@ msgstr "" msgid "Next" msgstr "" -#. Label of the next_notify (Datetime) field in DocType 'Message Queue -#. Recipient' -#: mail/server/doctype/message_queue/message_queue.js:83 -#: mail/server/doctype/message_queue_recipient/message_queue_recipient.json -msgid "Next Notify" -msgstr "" - -#. Label of the next_retry (Datetime) field in DocType 'Message Queue -#. Recipient' -#: mail/server/doctype/message_queue/message_queue.js:76 -#: mail/server/doctype/message_queue_recipient/message_queue_recipient.json -msgid "Next Retry" -msgstr "" - #. Label of the next_retry_after (Datetime) field in DocType 'Mail Queue' #: mail/client/doctype/mail_queue/mail_queue.json msgid "Next Retry After" @@ -6697,11 +6022,11 @@ msgstr "" msgid "No" msgstr "" -#: mail/client/doctype/mail_exchange/mail_exchange.py:142 +#: mail/client/doctype/mail_exchange/mail_exchange.py:141 msgid "No .eml files found" msgstr "" -#: mail/client/doctype/mail_exchange/mail_exchange.py:192 +#: mail/client/doctype/mail_exchange/mail_exchange.py:191 msgid "No .mbox files found" msgstr "" @@ -6709,19 +6034,19 @@ msgstr "" msgid "No Bcc addresses added." msgstr "" -#: mail/server/doctype/dmarc_report/dmarc_report.py:48 -msgid "No DMARC reports found." -msgstr "" - #: frontend/src/components/Settings/IdentitySettings.vue:32 msgid "No Reply To addresses added." msgstr "" +#: mail/client/doctype/user_account/user_account.py:49 +msgid "No accounts found." +msgstr "" + #: mail/client/doctype/address_book/address_book.py:68 msgid "No address book found." msgstr "" -#: frontend/src/pages/AddressBooksView.vue:84 +#: frontend/src/pages/AddressBooksView.vue:86 msgid "No address books found." msgstr "" @@ -6733,27 +6058,19 @@ msgstr "" msgid "No addresses." msgstr "" -#: mail/server/doctype/allowed_ip/allowed_ip.py:49 -msgid "No allowed IPs found." -msgstr "" - #: mail/utils/dns.py:27 msgid "No answer for {0}." msgstr "" -#: mail/server/doctype/blocked_ip/blocked_ip.py:49 -msgid "No blocked IPs found." -msgstr "" - #: frontend/src/components/Settings/BlockListSettings.vue:38 msgid "No blocked email addresses." msgstr "" -#: mail/client/doctype/calendar_event/calendar_event.py:546 +#: mail/client/doctype/calendar_event/calendar_event.py:550 msgid "No calendar event parsed from the provided ICS data." msgstr "" -#: mail/client/doctype/calendar_event/calendar_event.py:198 +#: mail/client/doctype/calendar_event/calendar_event.py:201 msgid "No calendar events found." msgstr "" @@ -6761,16 +6078,16 @@ msgstr "" msgid "No calendars found." msgstr "" -#: mail/client/doctype/contact_card/contact_card.py:162 +#: mail/client/doctype/contact_card/contact_card.py:166 msgid "No contact card found." msgstr "" -#: frontend/src/pages/AddressBookView.vue:289 -#: frontend/src/pages/ContactsView.vue:121 +#: frontend/src/pages/AddressBookView.vue:292 +#: frontend/src/pages/ContactsView.vue:133 msgid "No contacts found." msgstr "" -#: frontend/src/components/Modals/AddAddressBookContactsModal.vue:99 +#: frontend/src/components/Modals/AddAddressBookContactsModal.vue:106 msgid "No contacts to add." msgstr "" @@ -6782,11 +6099,11 @@ msgstr "" msgid "No domains found." msgstr "" -#: mail/client/doctype/mail_exchange/mail_exchange.py:735 +#: mail/client/doctype/mail_exchange/mail_exchange.py:737 msgid "No emails found for export." msgstr "" -#: mail/client/doctype/mail_exchange/mail_exchange.py:696 +#: mail/client/doctype/mail_exchange/mail_exchange.py:698 msgid "No emails found for import." msgstr "" @@ -6802,15 +6119,19 @@ msgstr "" msgid "No exports in progress." msgstr "" -#: mail/api/outbound.py:54 +#: mail/api/outbound.py:50 msgid "No file found in the request." msgstr "" -#: mail/api/outbound.py:174 +#: mail/api/outbound.py:172 msgid "No file part named 'raw_message' found." msgstr "" -#: mail/client/doctype/identity/identity.py:89 +#: frontend/src/components/Settings/FolderSettings.vue:35 +msgid "No folders found." +msgstr "" + +#: mail/client/doctype/identity/identity.py:88 msgid "No identities found." msgstr "" @@ -6834,7 +6155,7 @@ msgstr "" msgid "No mailing lists found." msgstr "" -#: frontend/src/pages/MailboxView.vue:217 +#: frontend/src/pages/MailboxView.vue:216 msgid "No mails found for the selected filter." msgstr "" @@ -6843,8 +6164,7 @@ msgstr "" msgid "No members found." msgstr "" -#: mail/client/doctype/mail_message/mail_message.py:261 -#: mail/server/doctype/message_queue/message_queue.py:37 +#: mail/client/doctype/mail_message/mail_message.py:263 msgid "No messages found." msgstr "" @@ -6852,6 +6172,10 @@ msgstr "" msgid "No participant identities found." msgstr "" +#: mail/client/doctype/user_settings/user_settings.py:97 +msgid "No personal account found for the user on the JMAP server." +msgstr "" + #: frontend/src/pages/ContactView.vue:98 msgid "No phones." msgstr "" @@ -6864,8 +6188,8 @@ msgstr "" msgid "No quotas found." msgstr "" -#: frontend/src/components/Modals/SearchModal.vue:149 -#: frontend/src/pages/MailboxView.vue:272 +#: frontend/src/components/Modals/SearchModal.vue:152 +#: frontend/src/pages/MailboxView.vue:271 msgid "No results found for the given query." msgstr "" @@ -6874,7 +6198,7 @@ msgid "No rows." msgstr "" #: frontend/src/components/Settings/AutomationSettings.vue:29 -#: mail/client/doctype/sieve_script/sieve_script.py:71 +#: mail/client/doctype/sieve_script/sieve_script.py:73 msgid "No sieve scripts found." msgstr "" @@ -6886,13 +6210,14 @@ msgstr "" msgid "No {0} file found in the archive." msgstr "" -#. Label of the cluster_node_id (Int) field in DocType 'Mail Server' -#: mail/server/doctype/mail_server/mail_server.json -msgid "Node ID" +#. Label of the num_replicas (Int) field in DocType 'Mail Cluster Store' +#: mail/server/doctype/mail_cluster_store/mail_cluster_store.json +msgid "No. of Replicas" msgstr "" -#: mail/server/doctype/mail_server/mail_server.py:83 -msgid "Node ID {0} already assigned to another Mail Server." +#. Label of the num_shards (Int) field in DocType 'Mail Cluster Store' +#: mail/server/doctype/mail_cluster_store/mail_cluster_store.json +msgid "No. of Shards" msgstr "" #. Option for the 'Include In Availability' (Select) field in DocType @@ -6910,8 +6235,8 @@ msgstr "" msgid "Normal" msgstr "" -#: frontend/src/pages/MailboxView.vue:1070 -#: frontend/src/pages/MailboxView.vue:1071 +#: frontend/src/pages/MailboxView.vue:1102 +#: frontend/src/pages/MailboxView.vue:1103 msgid "Not Junk" msgstr "" @@ -6926,11 +6251,11 @@ msgstr "" msgid "Not Verified" msgstr "" -#: mail/client/doctype/sieve_script/sieve_script.py:105 +#: mail/client/doctype/sieve_script/sieve_script.py:108 msgid "Not allowed to create automation script." msgstr "" -#: mail/utils/user.py:194 +#: mail/utils/user.py:228 msgid "Not permitted" msgstr "" @@ -6941,45 +6266,24 @@ msgstr "" #. Label of the number (Data) field in DocType 'Contact Card Phone' #: frontend/src/components/Modals/AddContactPhoneModal.vue:5 -#: frontend/src/pages/ContactView.vue:395 +#: frontend/src/pages/ContactView.vue:398 #: mail/client/doctype/contact_card_phone/contact_card_phone.json msgid "Number" msgstr "" -#. Label of the index_replicas (Int) field in DocType 'Mail Cluster Store' -#: mail/server/doctype/mail_cluster_store/mail_cluster_store.json -msgid "Number of Replicas" -msgstr "" - -#. Label of the index_shards (Int) field in DocType 'Mail Cluster Store' -#: mail/server/doctype/mail_cluster_store/mail_cluster_store.json -msgid "Number of Shards" -msgstr "" - -#. Description of the 'Indexing Batch Size' (Int) field in DocType 'Mail -#. Cluster' -#: mail/server/doctype/mail_cluster/mail_cluster.json -msgid "Number of items to process in each batch during indexing operations." -msgstr "" - -#. Description of the 'Number of Replicas' (Int) field in DocType 'Mail Cluster +#. Description of the 'No. of Replicas' (Int) field in DocType 'Mail Cluster #. Store' #: mail/server/doctype/mail_cluster_store/mail_cluster_store.json -msgid "Number of replicas for the index." -msgstr "" - -#. Description of the 'Retries' (Int) field in DocType 'Mail Cluster Store' -#: mail/server/doctype/mail_cluster_store/mail_cluster_store.json -msgid "Number of retries to connect to the Redis cluster." +msgid "Number of replicas for the index" msgstr "" -#. Description of the 'Number of Shards' (Int) field in DocType 'Mail Cluster +#. Description of the 'No. of Shards' (Int) field in DocType 'Mail Cluster #. Store' #: mail/server/doctype/mail_cluster_store/mail_cluster_store.json msgid "Number of shards for the index." msgstr "" -#. Description of the 'Thread Pool Size' (Int) field in DocType 'Mail Cluster +#. Description of the 'Pool Workers' (Int) field in DocType 'Mail Cluster #. Store' #: mail/server/doctype/mail_cluster_store/mail_cluster_store.json msgid "Number of worker threads to use for the store, defaults to the number of cores." @@ -7022,15 +6326,15 @@ msgstr "" msgid "On" msgstr "" -#: mail/client/doctype/contact_card/contact_card.py:267 +#: mail/client/doctype/contact_card/contact_card.py:272 msgid "One or more contact cards failed to create" msgstr "" -#: mail/server/doctype/mail_cluster/mail_cluster.py:106 +#: mail/server/doctype/mail_cluster/mail_cluster.py:74 msgid "Only Administrator can delete Mail Cluster." msgstr "" -#: mail/server/doctype/mail_server/mail_server.py:48 +#: mail/server/doctype/mail_server/mail_server.py:37 msgid "Only Administrator can delete Mail Server." msgstr "" @@ -7042,7 +6346,7 @@ msgstr "" msgid "Only failed ansible plays can be retried." msgstr "" -#: mail/server/doctype/server_deployment/server_deployment.py:317 +#: mail/server/doctype/server_deployment/server_deployment.py:258 msgid "Only failed deployments can be retried." msgstr "" @@ -7050,27 +6354,23 @@ msgstr "" msgid "Only failed jobs can be retried." msgstr "" -#: mail/client/doctype/mail_exchange/mail_exchange.py:645 +#: mail/client/doctype/mail_exchange/mail_exchange.py:647 msgid "Only mail exchange with status 'Queued', 'In Progress', or 'Failed' can be retried." msgstr "" -#: mail/server/doctype/mail_server/mail_server.py:106 -msgid "Only one ACME Provider can be default." -msgstr "" - -#: mail/server/doctype/mail_server/mail_server.py:131 -msgid "Only one TLS Certificate can be default." +#: mail/server/doctype/mail_cluster_store/mail_cluster_store.py:204 +msgid "Only one {0} store is allowed." msgstr "" #: mail/server/doctype/mail_data_exchange/mail_data_exchange.py:135 msgid "Only submitted data exchange can be retried." msgstr "" -#: mail/client/doctype/mail_exchange/mail_exchange.py:642 +#: mail/client/doctype/mail_exchange/mail_exchange.py:644 msgid "Only submitted mail exchange can be retried." msgstr "" -#: mail/client/doctype/mail_exchange/mail_exchange.py:570 +#: mail/client/doctype/mail_exchange/mail_exchange.py:572 #: mail/server/doctype/mail_data_exchange/mail_data_exchange.py:89 msgid "Only {0} files are supported for import." msgstr "" @@ -7083,17 +6383,6 @@ msgstr "" msgid "Open Settings" msgstr "" -#. Option for the 'Method' (Select) field in DocType 'Mail Cluster Trace' -#: mail/server/doctype/mail_cluster_trace/mail_cluster_trace.json -msgid "Open Telemetry" -msgstr "" - -#. Label of the opentelemetry_push_metrics_section (Section Break) field in -#. DocType 'Mail Cluster' -#: mail/server/doctype/mail_cluster/mail_cluster.json -msgid "OpenTelemetry Push Metrics" -msgstr "" - #. Label of the operation (Select) field in DocType 'Mail Exchange' #. Label of the operation (Select) field in DocType 'Mail Data Exchange' #: mail/client/doctype/mail_exchange/mail_exchange.json @@ -7107,21 +6396,11 @@ msgstr "" msgid "Optional additional information about the participant." msgstr "" -#. Label of the options_section (Section Break) field in DocType 'Mail Cluster -#. Trace' -#: mail/server/doctype/mail_cluster_trace/mail_cluster_trace.json +#. Label of the options (Data) field in DocType 'Mail Cluster Store' +#: mail/server/doctype/mail_cluster_store/mail_cluster_store.json msgid "Options" msgstr "" -#: mail/server/report/dmarc_report_viewer/dmarc_report_viewer.py:36 -msgid "Organization" -msgstr "" - -#. Label of the organization_name (Data) field in DocType 'DMARC Report' -#: mail/server/doctype/dmarc_report/dmarc_report.json -msgid "Organization Name" -msgstr "" - #. Label of the organizer (Data) field in DocType 'Calendar Event' #: mail/client/doctype/calendar_event/calendar_event.json msgid "Organizer" @@ -7132,11 +6411,6 @@ msgstr "" msgid "Origin" msgstr "" -#. Label of the original_rcpt (Data) field in DocType 'Message Queue Recipient' -#: mail/server/doctype/message_queue_recipient/message_queue_recipient.json -msgid "Original RCPT" -msgstr "" - #. Option for the 'Category' (Select) field in DocType 'Principal DNS Record' #: frontend/src/components/Modals/AddContactAddressModal.vue:62 #: frontend/src/components/Modals/AddContactEmailModal.vue:57 @@ -7152,11 +6426,6 @@ msgstr "" msgid "Other services this one depends on." msgstr "" -#. Label of the outbound_only (Check) field in DocType 'Mail Server' -#: mail/server/doctype/mail_server/mail_server.json -msgid "Outbound Only" -msgstr "" - #. Label of the outgoing_section (Section Break) field in DocType 'User #. Settings' #: frontend/src/components/Settings/AccountSettings.vue:3 @@ -7185,11 +6454,6 @@ msgstr "" msgid "P256DH" msgstr "" -#. Option for the 'Protocol' (Select) field in DocType 'Mail Server Listener' -#: mail/server/doctype/mail_server_listener/mail_server_listener.json -msgid "POP3" -msgstr "" - #. Label of the _parent (Link) field in DocType 'Mailbox' #: mail/client/doctype/mailbox/mailbox.json msgid "Parent" @@ -7200,10 +6464,8 @@ msgstr "" msgid "Parent ID" msgstr "" -#. Label of the parsing_limits_section (Section Break) field in DocType 'Mail -#. Cluster' -#: mail/server/doctype/mail_cluster/mail_cluster.json -msgid "Parsing Limits" +#: mail/api/sieve.py:228 +msgid "Parent mailbox with name '{0}' not found." msgstr "" #. Label of the part_id (Data) field in DocType 'Mail Message Part' @@ -7211,7 +6473,7 @@ msgstr "" msgid "Part ID" msgstr "" -#: mail/client/doctype/participant_identity/participant_identity.py:104 +#: mail/client/doctype/participant_identity/participant_identity.py:105 msgid "Participant Identities deleted successfully." msgstr "" @@ -7220,28 +6482,28 @@ msgstr "" msgid "Participant Identity" msgstr "" -#: mail/client/doctype/participant_identity/participant_identity.py:124 +#: mail/client/doctype/participant_identity/participant_identity.py:125 msgid "Participant Identity Creation Error" msgstr "" -#: mail/client/doctype/participant_identity/participant_identity.py:190 +#: mail/client/doctype/participant_identity/participant_identity.py:191 msgid "Participant Identity Deletion Error" msgstr "" -#: mail/client/doctype/participant_identity/participant_identity.py:189 +#: mail/client/doctype/participant_identity/participant_identity.py:190 msgid "Participant Identity Deletion Error(s):
{0}" msgstr "" -#: mail/client/doctype/participant_identity/participant_identity.py:147 +#: mail/client/doctype/participant_identity/participant_identity.py:148 msgid "Participant Identity Not Found" msgstr "" -#: mail/client/doctype/participant_identity/participant_identity.py:168 +#: mail/client/doctype/participant_identity/participant_identity.py:169 msgid "Participant Identity Update Error" msgstr "" -#: mail/client/doctype/participant_identity/participant_identity.py:144 -msgid "Participant Identity with ID {0} not found in user {1}." +#: mail/client/doctype/participant_identity/participant_identity.py:145 +msgid "Participant Identity with ID {0} not found in account {1}." msgstr "" #. Label of the participants (Table) field in DocType 'Calendar Event' @@ -7250,9 +6512,8 @@ msgstr "" msgid "Participants" msgstr "" -#. Label of the fallback_admin_password (Password) field in DocType 'Mail +#. Label of the recovery_admin_password (Password) field in DocType 'Mail #. Cluster' -#. Label of the password (Password) field in DocType 'Mail Cluster Store' #. Label of the password (Data) field in DocType 'Principal' #. Label of the password (Data) field in DocType 'Principal Password' #: frontend/src/components/Modals/AddMemberModal.vue:75 @@ -7260,18 +6521,11 @@ msgstr "" #: frontend/src/pages/LoginView.vue:12 frontend/src/pages/SignupView.vue:42 #: mail/server/doctype/mail_account_request/mail_account_request.js:64 #: mail/server/doctype/mail_cluster/mail_cluster.json -#: mail/server/doctype/mail_cluster_store/mail_cluster_store.json #: mail/server/doctype/principal/principal.json #: mail/server/doctype/principal_password/principal_password.json msgid "Password" msgstr "" -#. Label of the fallback_admin_secret (Small Text) field in DocType 'Mail -#. Cluster' -#: mail/server/doctype/mail_cluster/mail_cluster.json -msgid "Password Secret" -msgstr "" - #. Description of the 'Password' (Password) field in DocType 'Mail Cluster' #: mail/server/doctype/mail_cluster/mail_cluster.json msgid "Password for administrative access to the cluster." @@ -7281,14 +6535,20 @@ msgstr "" msgid "Password is required to create account." msgstr "" -#: mail/server/doctype/mail_cluster/mail_cluster.py:131 +#: mail/server/doctype/mail_cluster/mail_cluster.py:107 msgid "Password must be at least 16 characters long." msgstr "" -#. Description of the 'Password' (Password) field in DocType 'Mail Cluster +#. Description of the 'Secret' (Password) field in DocType 'Mail Cluster Store +#. HTTP Auth' +#: mail/server/doctype/mail_cluster_store_http_auth/mail_cluster_store_http_auth.json +msgid "Password or secret value." +msgstr "" + +#. Description of the 'Auth Secret' (Password) field in DocType 'Mail Cluster #. Store' #: mail/server/doctype/mail_cluster_store/mail_cluster_store.json -msgid "Password to connect to the database." +msgid "Password to connect to the store." msgstr "" #: frontend/src/components/Modals/ChangePasswordModal.vue:69 @@ -7300,9 +6560,7 @@ msgid "Passwords do not match" msgstr "" #. Label of the path (Data) field in DocType 'Mail Cluster Store' -#. Label of the path (Data) field in DocType 'Mail Cluster Trace' #: mail/server/doctype/mail_cluster_store/mail_cluster_store.json -#: mail/server/doctype/mail_cluster_trace/mail_cluster_trace.json msgid "Path" msgstr "" @@ -7312,12 +6570,9 @@ msgstr "" msgid "Path to the cluster file for the FoundationDB cluster." msgstr "" -#: mail/server/doctype/message_queue/message_queue_list.js:30 -msgid "Pause" -msgstr "" - -#: mail/server/doctype/message_queue/message_queue_list.js:45 -msgid "Pausing Queue..." +#. Description of the 'Path' (Data) field in DocType 'Mail Cluster Store' +#: mail/server/doctype/mail_cluster_store/mail_cluster_store.json +msgid "Path to the data directory" msgstr "" #. Option for the 'Status' (Select) field in DocType 'Mail Queue' @@ -7336,7 +6591,7 @@ msgstr "" msgid "Pending" msgstr "" -#: frontend/src/components/AppSidebar.vue:280 +#: frontend/src/components/AppSidebar.vue:358 msgid "People" msgstr "" @@ -7350,9 +6605,11 @@ msgstr "" msgid "Permissions" msgstr "" +#. Label of the is_personal (Check) field in DocType 'User Account' #: frontend/src/components/Modals/AddContactAddressModal.vue:60 #: frontend/src/components/Modals/AddContactEmailModal.vue:55 #: frontend/src/components/Modals/AddContactPhoneModal.vue:50 +#: mail/client/doctype/user_account/user_account.json msgid "Personal" msgstr "" @@ -7404,8 +6661,8 @@ msgstr "" msgid "Please add at least one command to execute." msgstr "" -#: frontend/src/components/ComposeMailEditor.vue:389 -#: mail/client/doctype/mail_queue/mail_queue.py:455 +#: frontend/src/components/ComposeMailEditor.vue:400 +#: mail/client/doctype/mail_queue/mail_queue.py:458 msgid "Please add at least one recipient." msgstr "" @@ -7417,64 +6674,60 @@ msgstr "" msgid "Please enter a contact email" msgstr "" -#: mail/server/doctype/mail_server/mail_server.py:266 -msgid "Please generate the Server Config before installing Stalwart." +#: mail/client/doctype/user_account/user_account.py:38 +msgid "Please select a user to view accounts." +msgstr "" + +#: mail/client/doctype/push_subscription/push_subscription.py:60 +msgid "Please select a user to view push subscriptions." msgstr "" #: mail/client/doctype/address_book/address_book.py:57 -msgid "Please select a user to view address books." +msgid "Please select an account to view address books." msgstr "" -#: mail/client/doctype/calendar_event/calendar_event.py:180 -msgid "Please select a user to view calendar events." +#: mail/client/doctype/calendar_event/calendar_event.py:176 +msgid "Please select an account to view calendar events." msgstr "" #: mail/client/doctype/calendar/calendar.py:65 -msgid "Please select a user to view calendars." +msgid "Please select an account to view calendars." msgstr "" -#: mail/client/doctype/contact_card/contact_card.py:135 -msgid "Please select a user to view contact cards." +#: mail/client/doctype/contact_card/contact_card.py:139 +msgid "Please select an account to view contact cards." msgstr "" #: mail/client/doctype/event_notification/event_notification.py:47 -msgid "Please select a user to view event notifications." +msgid "Please select an account to view event notifications." msgstr "" -#: mail/client/doctype/identity/identity.py:78 -msgid "Please select a user to view identities." +#: mail/client/doctype/identity/identity.py:77 +msgid "Please select an account to view identities." msgstr "" #: mail/client/doctype/mailbox/mailbox.py:52 -msgid "Please select a user to view mailboxes." +msgid "Please select an account to view mailboxes." msgstr "" -#: mail/client/doctype/mail_message/mail_message.py:202 -msgid "Please select a user to view messages." +#: mail/client/doctype/mail_message/mail_message.py:204 +msgid "Please select an account to view messages." msgstr "" #: mail/client/doctype/participant_identity/participant_identity.py:58 -msgid "Please select a user to view participant identities." -msgstr "" - -#: mail/client/doctype/push_subscription/push_subscription.py:60 -msgid "Please select a user to view push subscriptions." +msgid "Please select an account to view participant identities." msgstr "" #: mail/client/doctype/quota/quota.py:38 -msgid "Please select a user to view quotas." +msgid "Please select an account to view quotas." msgstr "" -#: mail/client/doctype/sieve_script/sieve_script.py:51 -msgid "Please select a user to view the Sieve Scripts." +#: mail/client/doctype/sieve_script/sieve_script.py:53 +msgid "Please select an account to view the Sieve Scripts." msgstr "" -#: mail/client/doctype/vacation_response/vacation_response.py:33 -msgid "Please select a user to view vacation response details." -msgstr "" - -#: mail/server/doctype/message_queue/message_queue.js:132 -msgid "Please select recipients to cancel delivery." +#: mail/client/doctype/vacation_response/vacation_response.py:32 +msgid "Please select an account to view vacation response details." msgstr "" #: mail/mail/doctype/mail_settings/mail_settings.py:45 @@ -7497,25 +6750,25 @@ msgstr "" msgid "Please set the Root Domain Name before configuring the DNS Provider." msgstr "" -#: mail/server/doctype/dns_record/dns_record.py:21 +#: mail/server/doctype/dns_record/dns_record.py:23 msgid "Please set the Root Domain Name in Mail Settings." msgstr "" #: mail/server/doctype/server_ansible_play/server_ansible_play.py:51 -#: mail/server/doctype/server_deployment/server_deployment.py:123 +#: mail/server/doctype/server_deployment/server_deployment.py:88 #: mail/server/doctype/server_job/server_job.py:51 msgid "Please verify SSH connection for Mail Server {0}" msgstr "" -#: mail/server/doctype/mail_server/mail_server.py:208 +#: mail/server/doctype/mail_server/mail_server.py:125 msgid "Please verify the SSH connection before installing Ansible." msgstr "" -#: mail/server/doctype/mail_server/mail_server.py:234 +#: mail/server/doctype/mail_server/mail_server.py:151 msgid "Please verify the SSH connection before installing Docker." msgstr "" -#: mail/server/doctype/mail_server/mail_server.py:256 +#: mail/server/doctype/mail_server/mail_server.py:173 msgid "Please verify the SSH connection before installing Stalwart." msgstr "" @@ -7527,13 +6780,52 @@ msgstr "" msgid "Please verify the {0} for the new {1}." msgstr "" -#. Label of the pools_section (Section Break) field in DocType 'Mail Cluster +#. Label of the poll_interval (Data) field in DocType 'Mail Cluster Store' +#: mail/server/doctype/mail_cluster_store/mail_cluster_store.json +msgid "Poll Interval" +msgstr "" + +#. Label of the pool_max_connections (Int) field in DocType 'Mail Cluster +#. Store' +#: mail/server/doctype/mail_cluster_store/mail_cluster_store.json +msgid "Pool Max Connections" +msgstr "" + +#. Label of the pool_min_connections (Int) field in DocType 'Mail Cluster +#. Store' +#: mail/server/doctype/mail_cluster_store/mail_cluster_store.json +msgid "Pool Min Connections" +msgstr "" + +#. Label of the pool_recycling_method (Select) field in DocType 'Mail Cluster +#. Store' +#: mail/server/doctype/mail_cluster_store/mail_cluster_store.json +msgid "Pool Recycling Method" +msgstr "" + +#. Label of the pool_timeout_create (Data) field in DocType 'Mail Cluster +#. Store' +#: mail/server/doctype/mail_cluster_store/mail_cluster_store.json +msgid "Pool Timeout Create" +msgstr "" + +#. Label of the pool_timeout_recycle (Data) field in DocType 'Mail Cluster #. Store' #: mail/server/doctype/mail_cluster_store/mail_cluster_store.json -msgid "Pools" +msgid "Pool Timeout Recycle" +msgstr "" + +#. Label of the pool_timeout_wait (Data) field in DocType 'Mail Cluster Store' +#: mail/server/doctype/mail_cluster_store/mail_cluster_store.json +msgid "Pool Timeout Wait" +msgstr "" + +#. Label of the pool_workers (Int) field in DocType 'Mail Cluster Store' +#: mail/server/doctype/mail_cluster_store/mail_cluster_store.json +msgid "Pool Workers" msgstr "" -#: frontend/src/components/ComposeMailEditor.vue:503 +#: frontend/src/components/ComposeMailEditor.vue:516 msgid "Pop Out" msgstr "" @@ -7571,19 +6863,14 @@ msgstr "" #. Label of the postcode (Data) field in DocType 'Contact Card Address' #: frontend/src/components/Modals/AddContactAddressModal.vue:15 -#: frontend/src/pages/ContactView.vue:405 +#: frontend/src/pages/ContactView.vue:408 #: mail/client/doctype/contact_card_address/contact_card_address.json msgid "Postcode" msgstr "" #. Option for the 'Type' (Select) field in DocType 'Mail Cluster Store' #: mail/server/doctype/mail_cluster_store/mail_cluster_store.json -msgid "PostgreSQL" -msgstr "" - -#. Label of the prefix (Data) field in DocType 'Mail Cluster Trace' -#: mail/server/doctype/mail_cluster_trace/mail_cluster_trace.json -msgid "Prefix" +msgid "PostgreSql" msgstr "" #. Description of the 'Read Only' (Check) field in DocType 'Sieve Script' @@ -7607,8 +6894,8 @@ msgstr "" msgid "Preview not available for this file type" msgstr "" -#. Label of the email_previous_state (Data) field in DocType 'User Settings' -#: mail/client/doctype/user_settings/user_settings.json +#. Label of the email_previous_state (Data) field in DocType 'Account Settings' +#: mail/client/doctype/account_settings/account_settings.json msgid "Previous State (Email)" msgstr "" @@ -7616,12 +6903,6 @@ msgstr "" msgid "Previous Thread (↑/K)" msgstr "" -#. Description of the 'Previous State (Email)' (Data) field in DocType 'User -#. Settings' -#: mail/client/doctype/user_settings/user_settings.json -msgid "Previously synced email state used to detect changes from the server." -msgstr "" - #. Name of a DocType #. Label of a Workspace Sidebar Item #: mail/server/doctype/principal/principal.json @@ -7689,7 +6970,7 @@ msgstr "" msgid "Principal Settings" msgstr "" -#: mail/client/doctype/user_settings/user_settings.py:145 +#: mail/client/doctype/user_settings/user_settings.py:125 msgid "Principal Settings for {0} does not exist. Please create Principal Settings with the principal name same as the JMAP username." msgstr "" @@ -7703,19 +6984,19 @@ msgstr "" msgid "Principal URL" msgstr "" -#: mail/server/doctype/principal/principal.py:538 +#: mail/server/doctype/principal/principal.py:495 msgid "Principal name or type cannot be changed directly. Please create a new principal." msgstr "" -#: mail/server/doctype/principal/principal.py:184 +#: mail/server/doctype/principal/principal.py:182 msgid "Principal type {0} is not supported yet." msgstr "" -#: mail/server/doctype/principal/principal.py:457 +#: mail/server/doctype/principal/principal.py:414 msgid "Principal {0} not found in backend." msgstr "" -#: mail/client/doctype/mailbox/mailbox.py:263 +#: mail/client/doctype/mailbox/mailbox.py:266 msgid "Prior mailbox ID {0} not found." msgstr "" @@ -7740,39 +7021,24 @@ msgid "Private" msgstr "" #. Label of the ssh_private_key (Password) field in DocType 'Mail Cluster' -#. Label of the private_key (Text) field in DocType 'Mail Server TLS -#. Certificate' #: mail/mail/doctype/mail_settings/mail_settings.py:116 #: mail/server/doctype/mail_cluster/mail_cluster.json -#: mail/server/doctype/mail_server_tls_certificate/mail_server_tls_certificate.json msgid "Private Key" msgstr "" -#. Label of the private_key_path (Data) field in DocType 'Mail Server TLS -#. Certificate' -#: mail/server/doctype/mail_server_tls_certificate/mail_server_tls_certificate.json -msgid "Private Key Path" -msgstr "" - #. Label of the dns_provider_private_zone (Check) field in DocType 'Mail #. Settings' #: mail/mail/doctype/mail_settings/mail_settings.json msgid "Private Zone" msgstr "" -#. Description of the 'Private Key' (Text) field in DocType 'Mail Server TLS -#. Certificate' -#: mail/server/doctype/mail_server_tls_certificate/mail_server_tls_certificate.json -msgid "Private key in PEM format." -msgstr "" - #. Label of the processed (Int) field in DocType 'Server Ansible Play' #: mail/server/doctype/server_ansible_play/server_ansible_play.json msgid "Processed" msgstr "" #. Label of the profile (Data) field in DocType 'Mail Cluster Store' -#: frontend/src/components/Modals/SettingsModal.vue:76 +#: frontend/src/components/Modals/SettingsModal.vue:80 #: frontend/src/components/Settings/ProfileSettings.vue:2 #: mail/server/doctype/mail_cluster_store/mail_cluster_store.json msgid "Profile" @@ -7786,15 +7052,9 @@ msgstr "" msgid "Profile updated." msgstr "" -#. Label of the prometheus_pull_metrics_section (Section Break) field in -#. DocType 'Mail Cluster' -#: mail/server/doctype/mail_cluster/mail_cluster.json -msgid "Prometheus Pull Metrics" -msgstr "" - -#. Label of the protocol (Select) field in DocType 'Mail Server Listener' -#: mail/server/doctype/mail_server_listener/mail_server_listener.json -msgid "Protocol" +#. Label of the protocol_version (Select) field in DocType 'Mail Cluster Store' +#: mail/server/doctype/mail_cluster_store/mail_cluster_store.json +msgid "Protocol Version" msgstr "" #. Description of the 'Reply To' (JSON) field in DocType 'Mail Queue' @@ -7826,11 +7086,6 @@ msgid "" "Example: {\"X-Custom-Header\": \"Value\"}" msgstr "" -#. Label of the proxy_section (Section Break) field in DocType 'Mail Cluster' -#: mail/server/doctype/mail_cluster/mail_cluster.json -msgid "Proxy" -msgstr "" - #. Option for the 'Privacy' (Select) field in DocType 'Calendar Event' #: mail/client/doctype/calendar_event/calendar_event.json msgid "Public" @@ -7880,39 +7135,19 @@ msgstr "" msgid "Public key used to authenticate API requests." msgstr "" -#. Label of the published_policy_section (Section Break) field in DocType -#. 'DMARC Report' -#: mail/server/doctype/dmarc_report/dmarc_report.json -msgid "Published Policy" -msgstr "" - -#. Label of the purge_frequency (Data) field in DocType 'Mail Cluster Store' -#: mail/server/doctype/mail_cluster_store/mail_cluster_store.json -msgid "Purge Frequency (Cron)" -msgstr "" - #. Option for the 'Color' (Select) field in DocType 'Mailbox Settings' -#: frontend/src/components/Modals/FolderModal.vue:297 +#: frontend/src/components/Modals/FolderModal.vue:301 #: mail/client/doctype/mailbox_settings/mailbox_settings.json msgid "Purple" msgstr "" -#. Label of the metrics_open_telemetry_interval (Int) field in DocType 'Mail -#. Cluster' -#: mail/server/doctype/mail_cluster/mail_cluster.json -msgid "Push Interval (Seconds)" -msgstr "" - #: frontend/src/components/PWASettings.vue:82 msgid "Push Notification permission denied" msgstr "" #. Name of a DocType -#. Label of the push_subscription_section (Section Break) field in DocType -#. 'Mail Cluster' #. Label of a Workspace Sidebar Item #: mail/client/doctype/push_subscription/push_subscription.json -#: mail/server/doctype/mail_cluster/mail_cluster.json #: mail/workspace_sidebar/client.json msgid "Push Subscription" msgstr "" @@ -7929,6 +7164,11 @@ msgstr "" msgid "Push Subscription Deletion Error(s):
{0}" msgstr "" +#. Label of the id (Data) field in DocType 'Push Subscription' +#: mail/client/doctype/push_subscription/push_subscription.json +msgid "Push Subscription ID" +msgstr "" + #: mail/client/doctype/push_subscription/push_subscription.py:184 msgid "Push Subscription Not Found" msgstr "" @@ -7939,19 +7179,13 @@ msgid "Push Subscription Renewal Error" msgstr "" #: mail/client/doctype/push_subscription/push_subscription.py:181 -msgid "Push Subscription with ID {0} not found in user {1}." +msgid "Push Subscription with ID {0} not found for user {1}." msgstr "" #: mail/client/doctype/push_subscription/push_subscription.py:126 msgid "Push Subscriptions deleted successfully." msgstr "" -#. Label of the push_timeouts_section (Section Break) field in DocType 'Mail -#. Cluster' -#: mail/server/doctype/mail_cluster/mail_cluster.json -msgid "Push Timeouts" -msgstr "" - #: mail/api/jmap.py:44 msgid "Push notifications are currently frozen for this user." msgstr "" @@ -7964,25 +7198,6 @@ msgstr "" msgid "Push notifications have been disabled on your site" msgstr "" -#. Label of the jmap_protocol_query_max_results (Int) field in DocType 'Mail -#. Cluster' -#: mail/server/doctype/mail_cluster/mail_cluster.json -msgid "Query" -msgstr "" - -#. Label of the queue (Data) field in DocType 'Message Queue Recipient' -#: mail/server/doctype/message_queue_recipient/message_queue_recipient.json -msgid "Queue" -msgstr "" - -#: mail/server/doctype/message_queue/message_queue.py:217 -msgid "Queue paused successfully." -msgstr "" - -#: mail/server/doctype/message_queue/message_queue.py:226 -msgid "Queue resumed successfully." -msgstr "" - #. Option for the 'Status' (Select) field in DocType 'Mail Exchange' #. Option for the 'Status' (Select) field in DocType 'Mail Queue' #. Option for the 'Status' (Select) field in DocType 'Mail Data Exchange' @@ -8031,7 +7246,12 @@ msgstr "" msgid "Quota (in GB)" msgstr "" -#: mail/client/doctype/quota/quota.py:88 +#. Label of the id (Data) field in DocType 'Quota' +#: mail/client/doctype/quota/quota.json +msgid "Quota ID" +msgstr "" + +#: mail/client/doctype/quota/quota.py:89 msgid "Quota Not Found" msgstr "" @@ -8039,8 +7259,8 @@ msgstr "" msgid "Quota Usage" msgstr "" -#: mail/client/doctype/quota/quota.py:87 -msgid "Quota with ID {0} not found in user {1}." +#: mail/client/doctype/quota/quota.py:88 +msgid "Quota with ID {0} not found in account {1}." msgstr "" #. Name of a DocType @@ -8064,22 +7284,22 @@ msgstr "" msgid "Read Only" msgstr "" -#: frontend/src/components/Modals/SearchModal.vue:81 +#: frontend/src/components/Modals/SearchModal.vue:84 #: frontend/src/components/Settings/ExportSettings.vue:54 msgid "Read Status" msgstr "" -#: frontend/src/pages/MailboxView.vue:1006 +#: frontend/src/pages/MailboxView.vue:1038 msgid "Read status restored." msgstr "" -#: mail/client/doctype/sieve_script/sieve_script.py:250 +#: mail/client/doctype/sieve_script/sieve_script.py:253 msgid "Read-Only Sieve Script" msgstr "" -#. Label of the reason (JSON) field in DocType 'DMARC Report Detail' -#: mail/server/doctype/dmarc_report_detail/dmarc_report_detail.json -msgid "Reason" +#. Label of the is_read_only (Check) field in DocType 'User Account' +#: mail/client/doctype/user_account/user_account.json +msgid "Readonly" msgstr "" #. Label of the received_after (Float) field in DocType 'Mail Message' @@ -8110,11 +7330,6 @@ msgstr "" msgid "Received At - Sent At" msgstr "" -#. Label of the received_from (Data) field in DocType 'DMARC Report' -#: mail/server/doctype/dmarc_report/dmarc_report.json -msgid "Received From" -msgstr "" - #. Option for the 'Category' (Select) field in DocType 'Principal DNS Record' #: mail/server/doctype/principal_dns_record/principal_dns_record.json msgid "Receiving" @@ -8130,11 +7345,8 @@ msgstr "" #. Label of the recipients_section (Section Break) field in DocType 'Mail #. Queue' #. Label of the recipients (JSON) field in DocType 'Mail Queue' -#. Label of the recipients (Table) field in DocType 'Message Queue' #: mail/client/doctype/mail_message/mail_message.json #: mail/client/doctype/mail_queue/mail_queue.json -#: mail/server/doctype/message_queue/message_queue.js:36 -#: mail/server/doctype/message_queue/message_queue.json msgid "Recipients" msgstr "" @@ -8142,12 +7354,6 @@ msgstr "" msgid "Recommended" msgstr "" -#. Label of the records (Table) field in DocType 'DMARC Report' -#. Label of the records_section (Section Break) field in DocType 'DMARC Report' -#: mail/server/doctype/dmarc_report/dmarc_report.json -msgid "Records" -msgstr "" - #: frontend/src/pages/dashboard/DomainView.vue:68 msgid "Records that allow mail and sync apps to automatically locate and connect to your domain’s email, calendar, and contacts services." msgstr "" @@ -8159,6 +7365,11 @@ msgstr "" msgid "Recovery" msgstr "" +#. Label of the recovery_http_port (Int) field in DocType 'Mail Server' +#: mail/server/doctype/mail_server/mail_server.json +msgid "Recovery HTTP Port" +msgstr "" + #. Label of the recurrence_tab (Tab Break) field in DocType 'Calendar Event' #: mail/client/doctype/calendar_event/calendar_event.json msgid "Recurrence" @@ -8181,7 +7392,7 @@ msgid "Recurrence Rule" msgstr "" #. Option for the 'Color' (Select) field in DocType 'Mailbox Settings' -#: frontend/src/components/Modals/FolderModal.vue:296 +#: frontend/src/components/Modals/FolderModal.vue:300 #: mail/client/doctype/mailbox_settings/mailbox_settings.json msgid "Red" msgstr "" @@ -8190,42 +7401,34 @@ msgstr "" msgid "Redirecting..." msgstr "" -#. Option for the 'Server Type' (Select) field in DocType 'Mail Cluster Store' -#: mail/server/doctype/mail_cluster_store/mail_cluster_store.json -msgid "Redis Cluster" -msgstr "" - -#. Option for the 'Server Type' (Select) field in DocType 'Mail Cluster Store' +#. Option for the 'Type' (Select) field in DocType 'Mail Cluster Store' #: mail/server/doctype/mail_cluster_store/mail_cluster_store.json -msgid "Redis Single Node" +msgid "Redis" msgstr "" -#. Label of the urls (Small Text) field in DocType 'Mail Cluster Store' +#. Description of the 'Protocol Version' (Select) field in DocType 'Mail +#. Cluster Store' #: mail/server/doctype/mail_cluster_store/mail_cluster_store.json -msgid "Redis URL(s)" +msgid "Redis protocol version." msgstr "" #. Option for the 'Type' (Select) field in DocType 'Mail Cluster Store' #: mail/server/doctype/mail_cluster_store/mail_cluster_store.json -msgid "Redis/Memcached" +msgid "RedisCluster" msgstr "" #. Group in Mail Cluster's connections -#. Group in Mail Server's connections #. Group in Principal's connections #. Group in Server Ansible Play's connections -#. Group in Server Config's connections #. Group in Server Deployment's connections #: mail/server/doctype/mail_cluster/mail_cluster.json -#: mail/server/doctype/mail_server/mail_server.json #: mail/server/doctype/principal/principal.json #: mail/server/doctype/server_ansible_play/server_ansible_play.json -#: mail/server/doctype/server_config/server_config.json #: mail/server/doctype/server_deployment/server_deployment.json msgid "Reference" msgstr "" -#: frontend/src/pages/MailboxView.vue:716 +#: frontend/src/pages/MailboxView.vue:720 msgid "Refresh" msgstr "" @@ -8235,18 +7438,28 @@ msgstr "" msgid "Refresh DNS Records" msgstr "" -#: mail/server/doctype/principal/principal.js:92 +#: mail/server/doctype/principal/principal.js:84 msgid "Refreshing DNS Records..." msgstr "" +#. Label of the regenerate_bootstrap_ndjson (Button) field in DocType 'Mail +#. Server' +#: mail/server/doctype/mail_server/mail_server.json +msgid "Regenerate" +msgstr "" + #: frontend/src/components/Settings/AdvancedSettings.vue:9 msgid "Regenerate Secret" msgstr "" +#: mail/server/doctype/mail_server/mail_server.js:18 +msgid "Regenerating bootstrap.ndjson..." +msgstr "" + #. Label of the region (Data) field in DocType 'Contact Card Address' #. Label of the region (Data) field in DocType 'Mail Cluster Store' #: frontend/src/components/Modals/AddContactAddressModal.vue:14 -#: frontend/src/pages/ContactView.vue:404 +#: frontend/src/pages/ContactView.vue:407 #: mail/client/doctype/contact_card_address/contact_card_address.json #: mail/server/doctype/mail_cluster_store/mail_cluster_store.json msgid "Region" @@ -8275,11 +7488,11 @@ msgstr "" msgid "Remove" msgstr "" -#: frontend/src/pages/ContactView.vue:356 +#: frontend/src/pages/ContactView.vue:359 msgid "Remove Addresses" msgstr "" -#: frontend/src/pages/AddressBookView.vue:258 +#: frontend/src/pages/AddressBookView.vue:261 msgid "Remove Contacts" msgstr "" @@ -8287,15 +7500,15 @@ msgstr "" msgid "Remove Current Photo" msgstr "" -#: frontend/src/pages/ContactView.vue:314 +#: frontend/src/pages/ContactView.vue:317 msgid "Remove Emails" msgstr "" -#: frontend/src/pages/ContactView.vue:335 +#: frontend/src/pages/ContactView.vue:338 msgid "Remove Phones" msgstr "" -#: frontend/src/pages/ContactView.vue:293 +#: frontend/src/pages/ContactView.vue:296 msgid "Remove from Address Books" msgstr "" @@ -8303,41 +7516,36 @@ msgstr "" msgid "Renew" msgstr "" -#. Label of the renew_before (Int) field in DocType 'Mail Server ACME Provider' -#: mail/server/doctype/mail_server_acme_provider/mail_server_acme_provider.json -msgid "Renew Before (Days)" -msgstr "" - #: mail/client/doctype/push_subscription/push_subscription.js:26 msgid "Renewing..." msgstr "" -#: frontend/src/components/ComposeMailEditor.vue:486 -#: frontend/src/components/MailActions.vue:99 -#: frontend/src/components/MailActions.vue:123 -#: frontend/src/components/MailThread.vue:533 -#: mail/client/doctype/mail_message/mail_message.js:65 +#: frontend/src/components/ComposeMailEditor.vue:499 +#: frontend/src/components/MailActions.vue:100 +#: frontend/src/components/MailActions.vue:124 +#: frontend/src/components/MailThread.vue:617 +#: mail/client/doctype/mail_message/mail_message.js:85 msgid "Reply" msgstr "" -#: frontend/src/components/MailThread.vue:534 +#: frontend/src/components/MailThread.vue:618 msgid "Reply (R)" msgstr "" -#: mail/client/doctype/mail_message/mail_message.js:74 -#: mail/client/doctype/mail_message/mail_message.js:87 -#: mail/client/doctype/mail_message/mail_message.js:100 +#: mail/client/doctype/mail_message/mail_message.js:94 +#: mail/client/doctype/mail_message/mail_message.js:107 +#: mail/client/doctype/mail_message/mail_message.js:120 msgid "Reply / Forward" msgstr "" -#: frontend/src/components/ComposeMailEditor.vue:488 -#: frontend/src/components/MailActions.vue:129 -#: frontend/src/components/MailThread.vue:539 -#: mail/client/doctype/mail_message/mail_message.js:78 +#: frontend/src/components/ComposeMailEditor.vue:501 +#: frontend/src/components/MailActions.vue:130 +#: frontend/src/components/MailThread.vue:623 +#: mail/client/doctype/mail_message/mail_message.js:98 msgid "Reply All" msgstr "" -#: frontend/src/components/MailThread.vue:540 +#: frontend/src/components/MailThread.vue:624 msgid "Reply All (Shift+R)" msgstr "" @@ -8365,45 +7573,11 @@ msgstr "" msgid "Reply to Mail" msgstr "" -#. Label of the report_begin (Datetime) field in DocType 'DMARC Report' -#: mail/server/doctype/dmarc_report/dmarc_report.json -#: mail/server/report/dmarc_report_viewer/dmarc_report_viewer.py:28 -msgid "Report Begin" -msgstr "" - -#. Label of the report_details_section (Section Break) field in DocType 'DMARC -#. Report' -#: mail/server/doctype/dmarc_report/dmarc_report.json -msgid "Report Details" -msgstr "" - -#. Label of the report_end (Datetime) field in DocType 'DMARC Report' -#: mail/server/doctype/dmarc_report/dmarc_report.json -msgid "Report End" -msgstr "" - -#. Label of the report_id (Data) field in DocType 'DMARC Report' -#: mail/server/doctype/dmarc_report/dmarc_report.json -#: mail/server/report/dmarc_report_viewer/dmarc_report_viewer.py:37 -msgid "Report ID" -msgstr "" - #. Label of the request_key (Data) field in DocType 'Mail Account Request' #: mail/server/doctype/mail_account_request/mail_account_request.json msgid "Request Key" msgstr "" -#. Label of the request_limits_section (Section Break) field in DocType 'Mail -#. Cluster' -#: mail/server/doctype/mail_cluster/mail_cluster.json -msgid "Request Limits" -msgstr "" - -#. Label of the jmap_push_timeout_request (Int) field in DocType 'Mail Cluster' -#: mail/server/doctype/mail_cluster/mail_cluster.json -msgid "Request Timeout (Milliseconds)" -msgstr "" - #: frontend/src/pages/dashboard/DomainView.vue:42 msgid "Required" msgstr "" @@ -8413,7 +7587,7 @@ msgstr "" msgid "Rescued" msgstr "" -#: frontend/src/components/LoginLayout.vue:48 mail/api/account.py:205 +#: frontend/src/components/LoginLayout.vue:48 mail/api/account.py:206 msgid "Reset Password" msgstr "" @@ -8437,49 +7611,23 @@ msgid "Response" msgstr "" #. Label of the restart (Select) field in DocType 'Server Deployment Service' -#: mail/server/doctype/server_deployment_service/server_deployment_service.json -msgid "Restart" -msgstr "" - -#. Description of the 'Max Depth' (Int) field in DocType 'Mail Cluster' -#: mail/server/doctype/mail_cluster/mail_cluster.json -msgid "Restricts the maximum depth of nested mailboxes a user can create." -msgstr "" - -#. Description of the 'Max Concurrent' (Int) field in DocType 'Mail Cluster' -#: mail/server/doctype/mail_cluster/mail_cluster.json -msgid "Restricts the number of concurrent file uploads a user can perform." -msgstr "" - -#. Description of the 'Concurrent' (Int) field in DocType 'Mail Cluster' -#: mail/server/doctype/mail_cluster/mail_cluster.json -msgid "Restricts the number of concurrent requests a user can make to the JMAP server." +#: mail/server/doctype/server_deployment_service/server_deployment_service.json +msgid "Restart" msgstr "" #. Label of the result (JSON) field in DocType 'Server Ansible Play Task' #: mail/server/doctype/server_ansible_play_task/server_ansible_play_task.json -#: mail/server/report/dmarc_report_viewer/dmarc_report_viewer.py:50 msgid "Result" msgstr "" -#: mail/server/doctype/message_queue/message_queue_list.js:35 -msgid "Resume" -msgstr "" - -#: mail/server/doctype/message_queue/message_queue_list.js:58 -msgid "Resuming Queue..." -msgstr "" - #. Label of the retries (Int) field in DocType 'Mail Exchange' #. Label of the retries (Int) field in DocType 'Mail Queue' -#. Label of the retry_total (Int) field in DocType 'Mail Cluster Store' #. Label of the retries (Int) field in DocType 'Mail Data Exchange' #. Label of the retries (Int) field in DocType 'Server Ansible Play' #. Label of the retries (Int) field in DocType 'Server Deployment' #. Label of the retries (Int) field in DocType 'Server Job' #: mail/client/doctype/mail_exchange/mail_exchange.json #: mail/client/doctype/mail_queue/mail_queue.json -#: mail/server/doctype/mail_cluster_store/mail_cluster_store.json #: mail/server/doctype/mail_data_exchange/mail_data_exchange.json #: mail/server/doctype/server_ansible_play/server_ansible_play.json #: mail/server/doctype/server_deployment/server_deployment.json @@ -8487,41 +7635,18 @@ msgstr "" msgid "Retries" msgstr "" -#: mail/client/doctype/mail_exchange/mail_exchange.js:16 -#: mail/client/doctype/mail_queue/mail_queue.js:27 -#: mail/client/doctype/mail_queue/mail_queue_list.js:26 +#: mail/client/doctype/mail_exchange/mail_exchange.js:21 +#: mail/client/doctype/mail_queue/mail_queue.js:32 +#: mail/client/doctype/mail_queue/mail_queue_list.js:27 #: mail/server/doctype/mail_data_exchange/mail_data_exchange.js:16 -#: mail/server/doctype/message_queue/message_queue_list.js:68 #: mail/server/doctype/server_ansible_play/server_ansible_play.js:30 #: mail/server/doctype/server_deployment/server_deployment.js:42 #: mail/server/doctype/server_job/server_job.js:16 msgid "Retry" msgstr "" -#. Label of the jmap_push_retry_interval (Int) field in DocType 'Mail Cluster' -#: mail/server/doctype/mail_cluster/mail_cluster.json -msgid "Retry Interval (Milliseconds)" -msgstr "" - -#. Label of the transaction_retry_limit (Int) field in DocType 'Mail Cluster -#. Store' -#. Label of the max_retries (Int) field in DocType 'Mail Cluster Store' -#: mail/server/doctype/mail_cluster_store/mail_cluster_store.json -msgid "Retry Limit" -msgstr "" - -#. Label of the retry_num (Int) field in DocType 'Message Queue Recipient' -#: mail/server/doctype/message_queue/message_queue.js:68 -#: mail/server/doctype/message_queue_recipient/message_queue_recipient.json -msgid "Retry Num" -msgstr "" - -#: mail/server/doctype/message_queue/message_queue.js:21 -msgid "Retrying Delivery..." -msgstr "" - -#: mail/client/doctype/mail_exchange/mail_exchange.js:25 -#: mail/client/doctype/mail_queue/mail_queue.js:48 +#: mail/client/doctype/mail_exchange/mail_exchange.js:50 +#: mail/client/doctype/mail_queue/mail_queue.js:73 #: mail/server/doctype/mail_data_exchange/mail_data_exchange.js:25 #: mail/server/doctype/server_ansible_play/server_ansible_play.js:39 #: mail/server/doctype/server_deployment/server_deployment.js:106 @@ -8529,11 +7654,6 @@ msgstr "" msgid "Retrying..." msgstr "" -#. Label of the return_path (Data) field in DocType 'Message Queue' -#: mail/server/doctype/message_queue/message_queue.json -msgid "Return Path" -msgstr "" - #: frontend/src/pages/MimeMessageView.vue:32 msgid "Return to Home" msgstr "" @@ -8549,7 +7669,7 @@ msgstr "" #. Option for the 'Type' (Select) field in DocType 'Mail Cluster Store' #: mail/server/doctype/mail_cluster_store/mail_cluster_store.json -msgid "RocksDB" +msgid "RocksDb" msgstr "" #. Label of the role (Select) field in DocType 'Mailbox' @@ -8589,16 +7709,11 @@ msgstr "" msgid "Rotate DKIM Keys" msgstr "" -#. Label of the rotate (Select) field in DocType 'Mail Cluster Trace' -#: mail/server/doctype/mail_cluster_trace/mail_cluster_trace.json -msgid "Rotate Frequency" -msgstr "" - -#: mail/server/doctype/principal/principal.js:107 +#: mail/server/doctype/principal/principal.js:99 msgid "Rotating DKIM Keys..." msgstr "" -#: mail/client/doctype/contact_card/contact_card.py:28 +#: mail/client/doctype/contact_card/contact_card.py:30 msgid "Row #{0}: Address Book ID is required." msgstr "" @@ -8606,39 +7721,10 @@ msgstr "" msgid "Row #{0}: Calendar ID is required." msgstr "" -#: mail/server/doctype/mail_server/mail_server.py:119 -msgid "Row #{0}: Certificate ID {1} is duplicated." -msgstr "" - -#: mail/server/doctype/mail_server/mail_server.py:126 -msgid "Row #{0}: Certificate or Certificate Path is required." -msgstr "" - -#: mail/server/doctype/mail_server/mail_server.py:99 -msgid "Row #{0}: Directory ID {1} is duplicated." -msgstr "" - -#: mail/server/doctype/principal/principal.py:237 +#: mail/server/doctype/principal/principal.py:235 msgid "Row #{0}: Failed to verify {1} : {2}" msgstr "" -#: mail/server/doctype/mail_cluster/mail_cluster.py:192 -#: mail/server/doctype/mail_server/mail_server.py:140 -msgid "Row #{0}: Listener ID {1} is duplicated." -msgstr "" - -#: mail/server/doctype/mail_server/mail_server.py:128 -msgid "Row #{0}: Private Key or Private Key Path is required." -msgstr "" - -#: mail/server/doctype/mail_cluster/mail_cluster.py:154 -msgid "Row #{0}: Store ID {1} is duplicated." -msgstr "" - -#: mail/server/doctype/mail_cluster/mail_cluster.py:206 -msgid "Row #{0}: Tracer ID {1} is duplicated." -msgstr "" - #. Option for the 'Status' (Select) field in DocType 'Server Ansible Play' #. Option for the 'Status' (Select) field in DocType 'Server Ansible Play Task' #. Option for the 'Status' (Select) field in DocType 'Server Deployment' @@ -8654,7 +7740,7 @@ msgstr "" #. Option for the 'Type' (Select) field in DocType 'Mail Cluster Store' #: mail/server/doctype/mail_cluster_store/mail_cluster_store.json -msgid "S3-compatible" +msgid "S3" msgstr "" #. Label of the sas_token (Password) field in DocType 'Mail Cluster Store' @@ -8665,45 +7751,19 @@ msgstr "" #. Description of the 'SAS Token' (Password) field in DocType 'Mail Cluster #. Store' #: mail/server/doctype/mail_cluster_store/mail_cluster_store.json -msgid "SAS Token, when not using access-key based authentication." -msgstr "" - -#. Option for the 'Protocol' (Select) field in DocType 'Mail Server Listener' -#: mail/server/doctype/mail_server_listener/mail_server_listener.json -msgid "SMTP" +msgid "SAS Token, when not using accessKey based authentication." msgstr "" #. Label of the spf_pass (Check) field in DocType 'Mail Message' -#: mail/api/mail.py:443 mail/client/doctype/mail_message/mail_message.json +#: mail/api/mail.py:438 mail/client/doctype/mail_message/mail_message.json msgid "SPF" msgstr "" -#. Label of the spf_alignment (Data) field in DocType 'DMARC Report' -#: mail/server/doctype/dmarc_report/dmarc_report.json -msgid "SPF Alignment" -msgstr "" - #. Label of the spf_description (Small Text) field in DocType 'Mail Message' #: mail/client/doctype/mail_message/mail_message.json msgid "SPF Description" msgstr "" -#. Label of the spf_result (Data) field in DocType 'DMARC Report Detail' -#: mail/server/doctype/dmarc_report_detail/dmarc_report_detail.json -#: mail/server/report/dmarc_report_viewer/dmarc_report_viewer.py:45 -msgid "SPF Result" -msgstr "" - -#. Label of the spf_results (JSON) field in DocType 'DMARC Report Detail' -#: mail/server/doctype/dmarc_report_detail/dmarc_report_detail.json -msgid "SPF Results" -msgstr "" - -#. Option for the 'Type' (Select) field in DocType 'Mail Cluster Store' -#: mail/server/doctype/mail_cluster_store/mail_cluster_store.json -msgid "SQLite" -msgstr "" - #. Option for the 'Type' (Select) field in DocType 'Principal DNS Record' #: mail/server/doctype/principal_dns_record/principal_dns_record.json msgid "SRV" @@ -8733,15 +7793,15 @@ msgstr "" msgid "SSH User" msgstr "" -#: mail/server/doctype/mail_server/mail_server.py:199 +#: mail/server/doctype/mail_server/mail_server.py:116 msgid "SSH connection failed. Error: {0}" msgstr "" -#: mail/server/doctype/mail_server/mail_server.py:197 +#: mail/server/doctype/mail_server/mail_server.py:114 msgid "SSH connection successful." msgstr "" -#: mail/server/doctype/server_deployment/server_deployment.py:308 +#: mail/server/doctype/server_deployment/server_deployment.py:249 msgid "SSL setup job has been created." msgstr "" @@ -8757,7 +7817,7 @@ msgstr "" #: frontend/src/components/Modals/EditContactModal.vue:33 #: frontend/src/components/Modals/EditInviteModal.vue:9 #: frontend/src/components/Modals/EditSignatureModal.vue:66 -#: frontend/src/components/Modals/FolderModal.vue:9 +#: frontend/src/components/Modals/FolderModal.vue:10 #: frontend/src/components/Modals/SetDefaultSignatureModal.vue:54 #: frontend/src/components/Modals/SieveScriptModal.vue:9 #: frontend/src/components/Settings/AccountSettings.vue:44 @@ -8795,7 +7855,6 @@ msgstr "" #. Label of the scope (Data) field in DocType 'Quota' #: mail/client/doctype/quota/quota.json -#: mail/server/report/dmarc_report_viewer/dmarc_report_viewer.py:48 msgid "Scope" msgstr "" @@ -8807,11 +7866,11 @@ msgstr "" msgid "Script Name" msgstr "" -#: frontend/src/components/Modals/SearchModal.vue:95 +#: frontend/src/components/Modals/SearchModal.vue:98 #: frontend/src/pages/AddressBookView.vue:32 #: frontend/src/pages/AddressBooksView.vue:8 #: frontend/src/pages/ContactsView.vue:7 -#: frontend/src/pages/MailboxView.vue:1149 +#: frontend/src/pages/MailboxView.vue:1181 #: frontend/src/pages/dashboard/DomainsView.vue:8 #: frontend/src/pages/dashboard/InvitesView.vue:3 #: frontend/src/pages/dashboard/MailingListView.vue:68 @@ -8824,12 +7883,16 @@ msgstr "" msgid "Search ({0}+K)" msgstr "" +#. Label of the search_store_config (JSON) field in DocType 'Mail Cluster' +#: mail/server/doctype/mail_cluster/mail_cluster.json +msgid "Search Config" +msgstr "" + #: frontend/src/components/Modals/ShortcutsModal.vue:90 msgid "Search Mail" msgstr "" -#. Label of the search_store_section (Section Break) field in DocType 'Mail -#. Cluster' +#. Label of the search_store (Link) field in DocType 'Mail Cluster' #: mail/server/doctype/mail_cluster/mail_cluster.json msgid "Search Store" msgstr "" @@ -8854,11 +7917,11 @@ msgid "Seconds" msgstr "" #. Label of the dns_provider_secret (Password) field in DocType 'Mail Settings' -#. Label of the metrics_prometheus_auth_secret (Password) field in DocType -#. 'Mail Cluster' +#. Label of the secret (Password) field in DocType 'Mail Cluster Store HTTP +#. Auth' #. Label of the secret (Small Text) field in DocType 'Principal Password' #: mail/mail/doctype/mail_settings/mail_settings.json -#: mail/server/doctype/mail_cluster/mail_cluster.json +#: mail/server/doctype/mail_cluster_store_http_auth/mail_cluster_store_http_auth.json #: mail/server/doctype/principal_password/principal_password.json msgid "Secret" msgstr "" @@ -8884,7 +7947,13 @@ msgstr "" msgid "Security Token" msgstr "" -#: frontend/src/components/MailActions.vue:202 +#. Description of the 'Security Token' (Password) field in DocType 'Mail +#. Cluster Store' +#: mail/server/doctype/mail_cluster_store/mail_cluster_store.json +msgid "Security token for temporary credentials." +msgstr "" + +#: frontend/src/components/MailActions.vue:209 msgid "See MIME Message" msgstr "" @@ -8893,7 +7962,7 @@ msgstr "" msgid "Seen" msgstr "" -#: frontend/src/pages/MailboxView.vue:71 +#: frontend/src/pages/MailboxView.vue:70 msgid "Select All ({0}+A)" msgstr "" @@ -8901,7 +7970,7 @@ msgstr "" msgid "Select All Mails" msgstr "" -#: frontend/src/components/Modals/AddAddressBookContactsModal.vue:46 +#: frontend/src/components/Modals/AddAddressBookContactsModal.vue:49 #: frontend/src/components/Modals/ContactsModal.vue:58 msgid "Select Contacts" msgstr "" @@ -8910,7 +7979,7 @@ msgstr "" msgid "Select From" msgstr "" -#: frontend/src/components/MailThread.vue:317 +#: frontend/src/components/MailThread.vue:358 msgid "Select an email to view the thread." msgstr "" @@ -8920,10 +7989,6 @@ msgstr "" msgid "Select from contacts" msgstr "" -#: mail/server/report/dmarc_report_viewer/dmarc_report_viewer.py:47 -msgid "Selector" -msgstr "" - #: frontend/src/components/ComposeMailToolbar.vue:48 msgid "Send" msgstr "" @@ -8995,14 +8060,12 @@ msgstr "" #. Option for the 'Category' (Select) field in DocType 'Principal DNS Record' #. Label of the server (Link) field in DocType 'Server Ansible Play' -#. Label of the server (Link) field in DocType 'Server Config' #. Label of the server (Link) field in DocType 'Server Deployment' #. Label of the server (Link) field in DocType 'Server Job' #. Title of a Workspace Sidebar #. Label of a Workspace Sidebar Item #: mail/server/doctype/principal_dns_record/principal_dns_record.json #: mail/server/doctype/server_ansible_play/server_ansible_play.json -#: mail/server/doctype/server_config/server_config.json #: mail/server/doctype/server_deployment/server_deployment.json #: mail/server/doctype/server_job/server_job.json #: mail/workspace_sidebar/server.json @@ -9024,17 +8087,6 @@ msgstr "" msgid "Server Ansible Play Variable" msgstr "" -#. Name of a DocType -#. Label of a Workspace Sidebar Item -#: mail/server/doctype/server_config/server_config.json -#: mail/workspace_sidebar/server.json -msgid "Server Config" -msgstr "" - -#: mail/server/doctype/mail_server/mail_server.py:153 -msgid "Server Config created." -msgstr "" - #. Name of a DocType #. Label of a Workspace Sidebar Item #: mail/server/doctype/server_deployment/server_deployment.json @@ -9064,28 +8116,16 @@ msgstr "" msgid "Server Record" msgstr "" -#. Label of the server_response (JSON) field in DocType 'Message Queue -#. Recipient' -#: mail/server/doctype/message_queue/message_queue.js:97 -#: mail/server/doctype/message_queue_recipient/message_queue_recipient.json -msgid "Server Response" -msgstr "" - -#. Label of the redis_type (Select) field in DocType 'Mail Cluster Store' -#: mail/server/doctype/mail_cluster_store/mail_cluster_store.json -msgid "Server Type" -msgstr "" - #. Label of the server_url (Data) field in DocType 'User Settings' #: mail/client/doctype/user_settings/user_settings.json msgid "Server URL" msgstr "" -#: mail/client/doctype/user_settings/user_settings.py:101 +#: mail/client/doctype/user_settings/user_settings.py:70 msgid "Server URL and App Password are required to validate JMAP settings." msgstr "" -#: mail/jmap/__init__.py:54 +#: mail/jmap/__init__.py:60 msgid "Server URL must be set in either the user's settings or the site configuration." msgstr "" @@ -9111,10 +8151,25 @@ msgstr "" msgid "Services" msgstr "" -#. Label of the jmap_protocol_set_max_objects (Int) field in DocType 'Mail -#. Cluster' -#: mail/server/doctype/mail_cluster/mail_cluster.json -msgid "Set" +#. Label of the session_section (Section Break) field in DocType 'User +#. Settings' +#: mail/client/doctype/user_settings/user_settings.json +msgid "Session" +msgstr "" + +#. Label of the session_last_update (Datetime) field in DocType 'User Settings' +#: mail/client/doctype/user_settings/user_settings.json +msgid "Session Last Update" +msgstr "" + +#. Label of the session_state (Data) field in DocType 'User Settings' +#: mail/client/doctype/user_settings/user_settings.json +msgid "Session State" +msgstr "" + +#. Label of the session_token (Password) field in DocType 'Mail Cluster Store' +#: mail/server/doctype/mail_cluster_store/mail_cluster_store.json +msgid "Session Token" msgstr "" #: frontend/src/components/Settings/SignatureSettings.vue:74 @@ -9131,7 +8186,7 @@ msgstr "" #: frontend/src/components/Modals/AddAddressBookModal.vue:32 #: frontend/src/components/Modals/EditAddressBookModal.vue:19 -#: frontend/src/pages/AddressBookView.vue:266 +#: frontend/src/pages/AddressBookView.vue:269 msgid "Set as Default" msgstr "" @@ -9144,11 +8199,6 @@ msgstr "" msgid "Set up organization" msgstr "" -#. Description of the 'Query' (Int) field in DocType 'Mail Cluster' -#: mail/server/doctype/mail_cluster/mail_cluster.json -msgid "Sets the maximum number of results that a Query method can return." -msgstr "" - #: mail/server/doctype/server_deployment/server_deployment.js:51 msgid "Setting up Filebeat Stream..." msgstr "" @@ -9157,7 +8207,7 @@ msgstr "" msgid "Setting up SSL..." msgstr "" -#: frontend/src/components/AppSidebar.vue:170 +#: frontend/src/components/AppSidebar.vue:203 #: frontend/src/components/Modals/SettingsModal.vue:2 #: frontend/src/components/Modals/SettingsModal.vue:6 #: frontend/src/components/PWASettings.vue:10 @@ -9173,6 +8223,7 @@ msgstr "" msgid "Share With" msgstr "" +#: frontend/src/components/AppSidebar.vue:208 #: frontend/src/components/Modals/ShortcutsModal.vue:2 msgid "Shortcuts" msgstr "" @@ -9187,11 +8238,15 @@ msgstr "" msgid "Should the calendar's events be displayed to the user at the moment?" msgstr "" -#: mail/client/doctype/user_settings/user_settings.js:13 +#: frontend/src/components/Settings/FolderSettings.vue:73 +msgid "Show" +msgstr "" + +#: mail/client/doctype/user_settings/user_settings.js:19 msgid "Show App Password" msgstr "" -#: mail/server/doctype/mail_cluster/mail_cluster.js:148 +#: mail/server/doctype/mail_cluster/mail_cluster.js:45 msgid "Show Password" msgstr "" @@ -9212,12 +8267,13 @@ msgstr "" msgid "Show a preview pane beside the email list for quick reading." msgstr "" -#: frontend/src/components/Modals/SearchModal.vue:139 +#: frontend/src/components/Modals/SearchModal.vue:142 msgid "Showing top 5 results. " msgstr "" -#. Label of the sieve_section (Section Break) field in DocType 'User Settings' -#: mail/client/doctype/user_settings/user_settings.json +#. Label of the sieve_section (Section Break) field in DocType 'Account +#. Settings' +#: mail/client/doctype/account_settings/account_settings.json msgid "Sieve" msgstr "" @@ -9226,36 +8282,41 @@ msgstr "" msgid "Sieve Script" msgstr "" -#: mail/client/doctype/sieve_script/sieve_script.py:117 +#: mail/client/doctype/sieve_script/sieve_script.py:120 msgid "Sieve Script Creation Error" msgstr "" -#: mail/client/doctype/sieve_script/sieve_script.py:243 +#: mail/client/doctype/sieve_script/sieve_script.py:246 msgid "Sieve Script Deletion Error" msgstr "" -#: mail/client/doctype/sieve_script/sieve_script.py:242 +#: mail/client/doctype/sieve_script/sieve_script.py:245 msgid "Sieve Script Deletion Error(s):
{0}" msgstr "" -#: mail/client/doctype/sieve_script/sieve_script.py:29 -#: mail/client/doctype/sieve_script/sieve_script.py:213 +#. Label of the id (Data) field in DocType 'Sieve Script' +#: mail/client/doctype/sieve_script/sieve_script.json +msgid "Sieve Script ID" +msgstr "" + +#: mail/client/doctype/sieve_script/sieve_script.py:31 +#: mail/client/doctype/sieve_script/sieve_script.py:216 msgid "Sieve Script Not Found" msgstr "" -#: mail/client/doctype/sieve_script/sieve_script.py:221 +#: mail/client/doctype/sieve_script/sieve_script.py:224 msgid "Sieve Script Update Error" msgstr "" -#: mail/client/doctype/sieve_script/sieve_script.py:188 +#: mail/client/doctype/sieve_script/sieve_script.py:191 msgid "Sieve Script Validation Error" msgstr "" #: mail/client/doctype/sieve_script/sieve_script.py:28 -msgid "Sieve Script with ID {0} not found in user {1}." +msgid "Sieve Script with ID {0} not found in account {1}." msgstr "" -#: mail/client/doctype/sieve_script/sieve_script.py:212 +#: mail/client/doctype/sieve_script/sieve_script.py:215 msgid "Sieve Script with ID {0} not found." msgstr "" @@ -9263,13 +8324,13 @@ msgstr "" msgid "Sieve Scripts" msgstr "" -#: mail/client/doctype/sieve_script/sieve_script.py:286 +#: mail/client/doctype/sieve_script/sieve_script.py:288 msgid "Sieve Scripts deleted successfully." msgstr "" -#: mail/client/doctype/sieve_script/sieve_script.py:100 -#: mail/client/doctype/sieve_script/sieve_script.py:178 -#: mail/client/doctype/sieve_script/sieve_script.py:203 +#: mail/client/doctype/sieve_script/sieve_script.py:103 +#: mail/client/doctype/sieve_script/sieve_script.py:181 +#: mail/client/doctype/sieve_script/sieve_script.py:206 msgid "Sieve script content cannot be empty." msgstr "" @@ -9281,7 +8342,7 @@ msgstr "" msgid "Sieve script deleted." msgstr "" -#: mail/client/doctype/sieve_script/sieve_script.py:262 +#: mail/client/doctype/sieve_script/sieve_script.py:264 msgid "Sieve script is valid." msgstr "" @@ -9303,7 +8364,6 @@ msgstr "" #. Label of the signature_section (Section Break) field in DocType 'Identity' #: frontend/src/components/Modals/SetDefaultSignatureModal.vue:19 -#: frontend/src/components/Modals/SettingsModal.vue:98 #: mail/client/doctype/identity/identity.json msgid "Signature" msgstr "" @@ -9328,6 +8388,7 @@ msgstr "" msgid "Signature updated." msgstr "" +#: frontend/src/components/Modals/SettingsModal.vue:110 #: frontend/src/components/Settings/SignatureSettings.vue:3 msgid "Signatures" msgstr "" @@ -9360,14 +8421,7 @@ msgstr "" msgid "Size" msgstr "" -#. Label of the jmap_protocol_request_max_size (Int) field in DocType 'Mail -#. Cluster' -#: mail/server/doctype/mail_cluster/mail_cluster.json -msgid "Size (Bytes)" -msgstr "" - -#. Description of the 'Write Buffer Size (MB)' (Int) field in DocType 'Mail -#. Cluster Store' +#. Description of the 'Buffer Size' (Int) field in DocType 'Mail Cluster Store' #: mail/server/doctype/mail_cluster_store/mail_cluster_store.json msgid "Size of the write buffer in bytes, used to batch writes to the store." msgstr "" @@ -9420,12 +8474,6 @@ msgstr "" msgid "Source Host" msgstr "" -#. Label of the source_ip (Data) field in DocType 'DMARC Report Detail' -#: mail/server/doctype/dmarc_report_detail/dmarc_report_detail.json -#: mail/server/report/dmarc_report_viewer/dmarc_report_viewer.py:38 -msgid "Source IP" -msgstr "" - #. Label of the source_ip_address (Data) field in DocType 'Spam Check Log' #: mail/server/doctype/spam_check_log/spam_check_log.json msgid "Source IP Address" @@ -9469,77 +8517,41 @@ msgstr "" msgid "SpamAssassin did not return the expected response. This may indicate a permission issue or an unauthorized connection. Please check the following: {0}" msgstr "" -#. Description of the 'Frequency (Cron)' (Data) field in DocType 'Mail Cluster' -#: mail/server/doctype/mail_cluster/mail_cluster.json -msgid "" -"Specifies how often tombstoned messages are deleted from the database.\n" -"\n" -"
\n" -"
\n" -"\n" -"
*  *  *  *  *\n"
-"┬  ┬  ┬  ┬  ┬\n"
-"│  │  │  │  │\n"
-"│  │  │  │  └ day of week (0 - 6) (0 is Sunday)\n"
-"│  │  │  └───── month (1 - 12)\n"
-"│  │  └────────── day of month (1 - 31)\n"
-"│  └─────────────── hour (0 - 23)\n"
-"└──────────────────── minute (0 - 59)\n"
-"\n"
-"---\n"
-"\n"
-"* - Any value\n"
-"/ - Step values\n"
-"
\n" -msgstr "" - #. Description of the 'Action' (Select) field in DocType 'Event Alert' #: mail/client/doctype/event_alert/event_alert.json msgid "Specifies how the alert is delivered to the user." msgstr "" -#. Description of the 'Expire After (Hours)' (Int) field in DocType 'Mail -#. Cluster' -#: mail/server/doctype/mail_cluster/mail_cluster.json -msgid "Specifies the Time-To-Live (TTL) for each uploaded file, after which the file is deleted from temporary storage." -msgstr "" - #. Description of the 'Relative To' (Select) field in DocType 'Event Alert' #: mail/client/doctype/event_alert/event_alert.json msgid "Specifies the event reference point used for offset-based alerts." msgstr "" -#. Description of the 'Total Files' (Int) field in DocType 'Mail Cluster' -#: mail/server/doctype/mail_cluster/mail_cluster.json -msgid "Specifies the maximum number of files that a user can upload within a certain period." -msgstr "" - -#. Description of the 'Attachment Size (Bytes)' (Int) field in DocType 'Mail -#. Cluster' -#: mail/server/doctype/mail_cluster/mail_cluster.json -msgid "Specifies the maximum size for an email attachment." +#. Option for the 'Type' (Select) field in DocType 'Mail Cluster Store' +#: mail/server/doctype/mail_cluster_store/mail_cluster_store.json +msgid "Sqlite" msgstr "" -#: mail/utils/__init__.py:834 +#: mail/utils/__init__.py:848 msgid "Stalwart CLI not found at {0}." msgstr "" -#: mail/server/doctype/server_deployment/server_deployment.py:288 +#: mail/server/doctype/server_deployment/server_deployment.py:229 msgid "Stalwart service not found in deployment {0}" msgstr "" -#: frontend/src/components/MailActions.vue:87 -#: frontend/src/components/MailActions.vue:153 +#: frontend/src/components/MailActions.vue:88 +#: frontend/src/components/MailActions.vue:154 msgid "Star" msgstr "" -#: frontend/src/components/AppSidebar.vue:242 -#: frontend/src/pages/MailboxView.vue:1122 -#: frontend/src/pages/MailboxView.vue:1147 +#: frontend/src/components/AppSidebar.vue:320 +#: frontend/src/pages/MailboxView.vue:1154 +#: frontend/src/pages/MailboxView.vue:1179 msgid "Starred" msgstr "" -#: frontend/src/pages/MailboxView.vue:1182 +#: frontend/src/pages/MailboxView.vue:1214 msgid "Starred Mails" msgstr "" @@ -9554,7 +8566,7 @@ msgstr "" msgid "Start From" msgstr "" -#: mail/client/doctype/calendar_event/calendar_event.py:230 +#: mail/client/doctype/calendar_event/calendar_event.py:234 msgid "Start time is required for non-draft events." msgstr "" @@ -9616,9 +8628,9 @@ msgstr "" msgid "Started At - Queued At" msgstr "" -#. Label of the email_state_last_update (Datetime) field in DocType 'User +#. Label of the email_state_last_update (Data) field in DocType 'Account #. Settings' -#: mail/client/doctype/user_settings/user_settings.json +#: mail/client/doctype/account_settings/account_settings.json msgid "State Last Update (Email)" msgstr "" @@ -9637,7 +8649,6 @@ msgstr "" #. Label of the status (Select) field in DocType 'Mail Exchange' #. Label of the status (Select) field in DocType 'Mail Queue' #. Label of the status (Select) field in DocType 'Mail Data Exchange' -#. Label of the status (Data) field in DocType 'Message Queue Recipient' #. Label of the status (Select) field in DocType 'Server Ansible Play' #. Label of the status (Select) field in DocType 'Server Ansible Play Task' #. Label of the status (Select) field in DocType 'Server Deployment' @@ -9652,8 +8663,6 @@ msgstr "" #: mail/client/doctype/mail_exchange/mail_exchange.json #: mail/client/doctype/mail_queue/mail_queue.json #: mail/server/doctype/mail_data_exchange/mail_data_exchange.json -#: mail/server/doctype/message_queue/message_queue.js:60 -#: mail/server/doctype/message_queue_recipient/message_queue_recipient.json #: mail/server/doctype/server_ansible_play/server_ansible_play.json #: mail/server/doctype/server_ansible_play_task/server_ansible_play_task.json #: mail/server/doctype/server_deployment/server_deployment.json @@ -9662,12 +8671,7 @@ msgstr "" msgid "Status" msgstr "" -#. Label of the section_break_r4np (Tab Break) field in DocType 'Mail Cluster' -#. Label of the storage_directory (Data) field in DocType 'Mail Cluster' -#. Label of the storage_data (Data) field in DocType 'Mail Cluster' -#. Label of the storage_blob (Data) field in DocType 'Mail Cluster' -#. Label of the storage_fts (Data) field in DocType 'Mail Cluster' -#. Label of the storage_lookup (Data) field in DocType 'Mail Cluster' +#. Label of the storage_tab (Tab Break) field in DocType 'Mail Cluster' #: frontend/src/components/QuotaBar.vue:6 #: mail/server/doctype/mail_cluster/mail_cluster.json msgid "Storage" @@ -9675,13 +8679,7 @@ msgstr "" #. Label of the storage_account (Data) field in DocType 'Mail Cluster Store' #: mail/server/doctype/mail_cluster_store/mail_cluster_store.json -msgid "Storage Account Name" -msgstr "" - -#. Label of the storage_settings_section (Section Break) field in DocType 'Mail -#. Cluster Store' -#: mail/server/doctype/mail_cluster_store/mail_cluster_store.json -msgid "Storage Settings" +msgid "Storage Account" msgstr "" #: frontend/src/pages/dashboard/MemberView.vue:152 @@ -9692,84 +8690,38 @@ msgstr "" msgid "Storage Used (in GB)" msgstr "" -#. Description of the 'Type' (Select) field in DocType 'Mail Cluster Store' -#: mail/server/doctype/mail_cluster_store/mail_cluster_store.json -msgid "Storage backend type." -msgstr "" - -#. Label of the store_id (Data) field in DocType 'Mail Cluster Store' +#. Label of the store_tab (Tab Break) field in DocType 'Mail Cluster Store' #: mail/server/doctype/mail_cluster_store/mail_cluster_store.json -msgid "Store ID" -msgstr "" - -#: mail/server/doctype/mail_cluster/mail_cluster.py:171 -msgid "Store with Store ID {0} not found." -msgstr "" - -#. Label of the stores_section (Section Break) field in DocType 'Mail Cluster' -#. Label of the stores (Table) field in DocType 'Mail Cluster' -#: mail/server/doctype/mail_cluster/mail_cluster.json -msgid "Stores" -msgstr "" - -#. Description of the 'Directory Storage' (Section Break) field in DocType -#. 'Mail Cluster' -#: mail/server/doctype/mail_cluster/mail_cluster.json -msgid "Stores user accounts, passwords, email addresses, and settings. Used for authentication, email validation, and account management." +msgid "Store" msgstr "" #. Label of the street (Small Text) field in DocType 'Contact Card Address' #: frontend/src/components/Modals/AddContactAddressModal.vue:12 -#: frontend/src/pages/ContactView.vue:402 +#: frontend/src/pages/ContactView.vue:405 #: mail/client/doctype/contact_card_address/contact_card_address.json msgid "Street" msgstr "" -#: mail/utils/validation.py:62 +#: mail/utils/validation.py:18 msgid "Subaddressing is not allowed in the email address." msgstr "" -#. Label of the subdomain_policy (Data) field in DocType 'DMARC Report' -#: mail/server/doctype/dmarc_report/dmarc_report.json -msgid "Subdomain Policy" -msgstr "" - #. Label of the subject (Small Text) field in DocType 'Mail Message' #. Label of the subject (Small Text) field in DocType 'Mail Queue' #. Label of the subject (Data) field in DocType 'Vacation Response' -#. Label of the subject (Small Text) field in DocType 'DMARC Report' #: frontend/src/components/ComposeMailEditor.vue:133 -#: frontend/src/components/Modals/SearchModal.vue:51 +#: frontend/src/components/Modals/SearchModal.vue:54 #: frontend/src/components/Settings/VacationResponseSettings.vue:24 -#: mail/api/mail.py:434 mail/client/doctype/mail_message/mail_message.json +#: mail/api/mail.py:429 mail/client/doctype/mail_message/mail_message.json #: mail/client/doctype/mail_queue/mail_queue.json #: mail/client/doctype/vacation_response/vacation_response.json -#: mail/server/doctype/dmarc_report/dmarc_report.json msgid "Subject" msgstr "" -#. Label of the subjects (Small Text) field in DocType 'Mail Server TLS -#. Certificate' -#: mail/server/doctype/mail_server_tls_certificate/mail_server_tls_certificate.json -msgid "Subject Alternative Names" -msgstr "" - -#. Description of the 'Subject Alternative Names' (Small Text) field in DocType -#. 'Mail Server TLS Certificate' -#: mail/server/doctype/mail_server_tls_certificate/mail_server_tls_certificate.json -msgid "Subject Alternative Names (SAN) for the certificate." -msgstr "" - -#: frontend/src/components/Modals/FolderModal.vue:90 +#: frontend/src/components/Modals/FolderModal.vue:91 msgid "Subject Contains" msgstr "" -#. Label of the domains (Small Text) field in DocType 'Mail Server ACME -#. Provider' -#: mail/server/doctype/mail_server_acme_provider/mail_server_acme_provider.json -msgid "Subject Names" -msgstr "" - #: frontend/src/components/MailDetails.vue:57 msgid "Subject: " msgstr "" @@ -9818,7 +8770,7 @@ msgstr "" msgid "Success" msgstr "" -#: mail/client/doctype/mail_queue/mail_queue.py:766 +#: mail/client/doctype/mail_queue/mail_queue.py:769 msgid "Successfully retried {0} emails." msgstr "" @@ -9844,18 +8796,10 @@ msgstr "" msgid "Sync DNS Record" msgstr "" -#: mail/server/doctype/principal/principal.js:48 -msgid "Sync JMAP Identities" -msgstr "" - #: mail/server/doctype/dns_record/dns_record.js:64 msgid "Syncing DNS Record..." msgstr "" -#: mail/server/doctype/principal/principal.js:122 -msgid "Syncing JMAP Identities..." -msgstr "" - #. Option for the 'Color Scheme' (Select) field in DocType 'User Settings' #: frontend/src/components/Settings/AppearanceSettings.vue:74 #: mail/client/doctype/user_settings/user_settings.json @@ -9863,6 +8807,7 @@ msgid "System Default" msgstr "" #. Name of a role +#: mail/client/doctype/account_settings/account_settings.json #: mail/client/doctype/address_book/address_book.json #: mail/client/doctype/blocked_email_address/blocked_email_address.json #: mail/client/doctype/calendar/calendar.json @@ -9881,77 +8826,39 @@ msgstr "" #: mail/client/doctype/push_subscription/push_subscription.json #: mail/client/doctype/quota/quota.json #: mail/client/doctype/sieve_script/sieve_script.json +#: mail/client/doctype/user_account/user_account.json #: mail/client/doctype/user_settings/user_settings.json #: mail/client/doctype/vacation_response/vacation_response.json #: mail/mail/doctype/mail_settings/mail_settings.json #: mail/mail/doctype/rate_limit/rate_limit.json -#: mail/server/doctype/allowed_ip/allowed_ip.json -#: mail/server/doctype/blocked_ip/blocked_ip.json -#: mail/server/doctype/dmarc_report/dmarc_report.json #: mail/server/doctype/dns_record/dns_record.json #: mail/server/doctype/mail_account_request/mail_account_request.json #: mail/server/doctype/mail_cluster/mail_cluster.json +#: mail/server/doctype/mail_cluster_store/mail_cluster_store.json +#: mail/server/doctype/mail_cluster_store_http_auth/mail_cluster_store_http_auth.json #: mail/server/doctype/mail_data_exchange/mail_data_exchange.json #: mail/server/doctype/mail_domain_request/mail_domain_request.json #: mail/server/doctype/mail_server/mail_server.json -#: mail/server/doctype/message_queue/message_queue.json #: mail/server/doctype/principal/principal.json #: mail/server/doctype/principal_settings/principal_settings.json #: mail/server/doctype/server_ansible_play/server_ansible_play.json #: mail/server/doctype/server_ansible_play_task/server_ansible_play_task.json -#: mail/server/doctype/server_config/server_config.json #: mail/server/doctype/server_deployment/server_deployment.json #: mail/server/doctype/server_job/server_job.json #: mail/server/doctype/spam_check_log/spam_check_log.json msgid "System Manager" msgstr "" -#. Option for the 'Method' (Select) field in DocType 'Mail Cluster Trace' -#: mail/server/doctype/mail_cluster_trace/mail_cluster_trace.json -msgid "Systemd Journal" -msgstr "" - #. Option for the 'Status' (Select) field in DocType 'Event Participant' #: mail/client/doctype/event_participant/event_participant.json msgid "TENTATIVE" msgstr "" -#. Label of the tls_section (Section Break) field in DocType 'Mail Cluster -#. Store' -#. Label of the tls_tab (Tab Break) field in DocType 'Mail Server' -#: mail/server/doctype/mail_cluster_store/mail_cluster_store.json -#: mail/server/doctype/mail_server/mail_server.json -msgid "TLS" -msgstr "" - -#. Label of the tls_certificates (Table) field in DocType 'Mail Server' -#: mail/server/doctype/mail_server/mail_server.json -msgid "TLS Certificates" -msgstr "" - -#. Label of the tls_options_section (Section Break) field in DocType 'Mail -#. Server Listener' -#: mail/server/doctype/mail_server_listener/mail_server_listener.json -msgid "TLS Options" -msgstr "" - #. Option for the 'Category' (Select) field in DocType 'Principal DNS Record' #: mail/server/doctype/principal_dns_record/principal_dns_record.json msgid "TLS Reporting" msgstr "" -#. Description of the 'Certificate' (Text) field in DocType 'Mail Server TLS -#. Certificate' -#: mail/server/doctype/mail_server_tls_certificate/mail_server_tls_certificate.json -msgid "TLS certificate in PEM format." -msgstr "" - -#. Option for the 'Challenge Type' (Select) field in DocType 'Mail Server ACME -#. Provider' -#: mail/server/doctype/mail_server_acme_provider/mail_server_acme_provider.json -msgid "TLS-ALPN-01" -msgstr "" - #. Label of the ttl (Int) field in DocType 'DNS Record' #: mail/server/doctype/dns_record/dns_record.json msgid "TTL" @@ -9975,7 +8882,7 @@ msgstr "" msgid "TXT records that enforce encrypted mail delivery and provide reporting on failed or insecure SMTP connections." msgstr "" -#: mail/client/doctype/mailbox/mailbox.py:249 +#: mail/client/doctype/mailbox/mailbox.py:252 msgid "Target mailbox ID {0} not found." msgstr "" @@ -9996,6 +8903,12 @@ msgstr "" msgid "Team Updates" msgstr "" +#. Description of the 'Session Token' (Password) field in DocType 'Mail Cluster +#. Store' +#: mail/server/doctype/mail_cluster_store/mail_cluster_store.json +msgid "Temporary session token for the S3 account." +msgstr "" + #. Option for the 'Status' (Select) field in DocType 'Calendar Event' #: mail/client/doctype/calendar_event/calendar_event.json msgid "Tentative" @@ -10006,12 +8919,10 @@ msgstr "" #. Label of the text (Data) field in DocType 'Mail Message' #. Label of the text_body (Code) field in DocType 'Mail Queue' #. Label of the text_body (Code) field in DocType 'Vacation Response' -#. Label of the text (Data) field in DocType 'Message Queue' #: mail/client/doctype/identity/identity.json #: mail/client/doctype/mail_message/mail_message.json #: mail/client/doctype/mail_queue/mail_queue.json #: mail/client/doctype/vacation_response/vacation_response.json -#: mail/server/doctype/message_queue/message_queue.json msgid "Text" msgstr "" @@ -10035,18 +8946,12 @@ msgstr "" msgid "The \"Scope\" of this quota as defined in `#scope`." msgstr "" -#: mail/client/doctype/sieve_script/sieve_script.py:249 +#: mail/client/doctype/sieve_script/sieve_script.py:252 msgid "The '{0}' sieve script cannot be modified." msgstr "" -#. Description of the 'Challenge Type' (Select) field in DocType 'Mail Server -#. ACME Provider' -#: mail/server/doctype/mail_server_acme_provider/mail_server_acme_provider.json -msgid "The ACME challenge type used to validate domain ownership." -msgstr "" - -#. Description of the 'Storage Account Name' (Data) field in DocType 'Mail -#. Cluster Store' +#. Description of the 'Storage Account' (Data) field in DocType 'Mail Cluster +#. Store' #: mail/server/doctype/mail_cluster_store/mail_cluster_store.json msgid "The Azure Storage Account where blobs (e-mail messages, Sieve scripts, etc.) will be stored." msgstr "" @@ -10056,16 +8961,8 @@ msgstr "" msgid "The Bcc value the client should set when creating a new Email from this Identity." msgstr "" -#. Description of the 'HMAC Key' (Password) field in DocType 'Mail Server ACME -#. Provider' -#: mail/server/doctype/mail_server_acme_provider/mail_server_acme_provider.json -msgid "The External Account Binding (EAB) HMAC key." -msgstr "" - -#. Description of the 'Key ID' (Data) field in DocType 'Mail Server ACME -#. Provider' -#: mail/server/doctype/mail_server_acme_provider/mail_server_acme_provider.json -msgid "The External Account Binding (EAB) key ID." +#: mail/client/doctype/mail_queue/mail_queue.py:342 +msgid "The From Email {0} is not a valid identity. Please add it as an identity or use a different email address." msgstr "" #. Description of the 'HTML' (Text Editor) field in DocType 'Vacation Response' @@ -10073,7 +8970,12 @@ msgstr "" msgid "The HTML body to send in response to messages when the vacation response is enabled." msgstr "" -#. Description of the 'ID' (Data) field in DocType 'Address Book' +#. Description of the 'HTTP Auth' (Link) field in DocType 'Mail Cluster Store' +#: mail/server/doctype/mail_cluster_store/mail_cluster_store.json +msgid "The HTTP authentication to use." +msgstr "" + +#. Description of the 'Address Book ID' (Data) field in DocType 'Address Book' #: mail/client/doctype/address_book/address_book.json msgid "The ID of the Address Book." msgstr "" @@ -10084,7 +8986,7 @@ msgstr "" msgid "The ID of the CalendarEvent that this notification is about." msgstr "" -#. Description of the 'ID' (Data) field in DocType 'Contact Card' +#. Description of the 'Contact Card ID' (Data) field in DocType 'Contact Card' #: mail/client/doctype/contact_card/contact_card.json msgid "The ID of the Contact Card." msgstr "" @@ -10100,17 +9002,18 @@ msgstr "" msgid "The ID of the blob containing the raw octets of the script." msgstr "" -#. Description of the 'ID' (Data) field in DocType 'Calendar' +#. Description of the 'Calendar ID' (Data) field in DocType 'Calendar' #: mail/client/doctype/calendar/calendar.json msgid "The ID of the calendar." msgstr "" -#. Description of the 'ID' (Data) field in DocType 'Push Subscription' +#. Description of the 'Push Subscription ID' (Data) field in DocType 'Push +#. Subscription' #: mail/client/doctype/push_subscription/push_subscription.json msgid "The ID of the push subscription." msgstr "" -#. Description of the 'ID' (Data) field in DocType 'Sieve Script' +#. Description of the 'Sieve Script ID' (Data) field in DocType 'Sieve Script' #: mail/client/doctype/sieve_script/sieve_script.json msgid "The ID of the script." msgstr "" @@ -10138,16 +9041,6 @@ msgstr "" msgid "The Mailbox link for the parent of this Mailbox." msgstr "" -#. Description of the 'Secret' (Password) field in DocType 'Mail Cluster' -#: mail/server/doctype/mail_cluster/mail_cluster.json -msgid "The Prometheus endpoint's secret for Basic authentication." -msgstr "" - -#. Description of the 'Username' (Data) field in DocType 'Mail Cluster' -#: mail/server/doctype/mail_cluster/mail_cluster.json -msgid "The Prometheus endpoint's username for Basic authentication." -msgstr "" - #. Description of the 'Reply To' (Table) field in DocType 'Identity' #: mail/client/doctype/identity/identity.json msgid "The Reply-To value the client should set when creating a new Email from this Identity." @@ -10158,6 +9051,11 @@ msgstr "" msgid "The S3 bucket where blobs (e-mail messages, Sieve scripts, etc.) will be stored." msgstr "" +#. Description of the 'Region' (Data) field in DocType 'Mail Cluster Store' +#: mail/server/doctype/mail_cluster_store/mail_cluster_store.json +msgid "The S3 region where the bucket resides." +msgstr "" + #. Description of the 'Content' (Code) field in DocType 'Sieve Script' #: mail/client/doctype/sieve_script/sieve_script.json msgid "The Sieve script code used for mail filtering rules." @@ -10172,12 +9070,6 @@ msgstr "" msgid "The URL must start with 'https://'." msgstr "" -#. Description of the 'Directory URL' (Data) field in DocType 'Mail Server ACME -#. Provider' -#: mail/server/doctype/mail_server_acme_provider/mail_server_acme_provider.json -msgid "The URL of the ACME directory endpoint." -msgstr "" - #. Description of the 'Updated (UTC)' (Data) field in DocType 'Calendar Event' #: mail/client/doctype/calendar_event/calendar_event.json msgid "The UTC timestamp of the most recent modification to the event." @@ -10188,27 +9080,67 @@ msgstr "" msgid "The UTC timestamp when the event was created." msgstr "" -#. Description of the 'Access Key' (Password) field in DocType 'Mail Cluster -#. Store' -#: mail/server/doctype/mail_cluster_store/mail_cluster_store.json -msgid "The access key for the Azure Storage Account." +#. Description of the 'Account' (Select) field in DocType 'Address Book' +#: mail/client/doctype/address_book/address_book.json +msgid "The account this address book belongs to." msgstr "" -#. Description of the 'Bind Addresses' (Small Text) field in DocType 'Mail -#. Server Listener' -#: mail/server/doctype/mail_server_listener/mail_server_listener.json -msgid "The addresses the listener will bind to." +#. Description of the 'Account' (Select) field in DocType 'Calendar' +#: mail/client/doctype/calendar/calendar.json +msgid "The account this calendar belongs to." msgstr "" -#. Description of the 'Calendars' (Table) field in DocType 'Calendar Event' +#. Description of the 'Account' (Select) field in DocType 'Contact Card' +#: mail/client/doctype/contact_card/contact_card.json +msgid "The account this contact card belongs to." +msgstr "" + +#. Description of the 'Account' (Select) field in DocType 'Calendar Event' #: mail/client/doctype/calendar_event/calendar_event.json -msgid "The calendars to which this event belongs." +msgid "The account this event belongs to." +msgstr "" + +#. Description of the 'Account' (Select) field in DocType 'Identity' +#. Description of the 'Account' (Select) field in DocType 'Participant +#. Identity' +#: mail/client/doctype/identity/identity.json +#: mail/client/doctype/participant_identity/participant_identity.json +msgid "The account this identity belongs to." +msgstr "" + +#. Description of the 'Account' (Select) field in DocType 'Mailbox' +#: mail/client/doctype/mailbox/mailbox.json +msgid "The account this mailbox belongs to." +msgstr "" + +#. Description of the 'Account' (Select) field in DocType 'Event Notification' +#: mail/client/doctype/event_notification/event_notification.json +msgid "The account this notification belongs to." +msgstr "" + +#. Description of the 'Account' (Select) field in DocType 'Quota' +#: mail/client/doctype/quota/quota.json +msgid "The account this quota belongs to." +msgstr "" + +#. Description of the 'Account' (Select) field in DocType 'Sieve Script' +#: mail/client/doctype/sieve_script/sieve_script.json +msgid "The account this script belongs to." +msgstr "" + +#. Description of the 'Account' (Select) field in DocType 'Vacation Response' +#: mail/client/doctype/vacation_response/vacation_response.json +msgid "The account this vacation response belongs to." +msgstr "" + +#. Description of the 'Account' (Select) field in DocType 'Mailbox Settings' +#: mail/client/doctype/mailbox_settings/mailbox_settings.json +msgid "The account to whom these mailbox settings apply." msgstr "" -#. Description of the 'Contact Emails' (Small Text) field in DocType 'Mail -#. Server ACME Provider' -#: mail/server/doctype/mail_server_acme_provider/mail_server_acme_provider.json -msgid "The contact email address, which is used for important communications regarding your ACME account and certificates." +#. Description of the 'Calendars' (Table) field in DocType 'Calendar Event' +#: mail/client/doctype/calendar_event/calendar_event.json +msgid "The calendars to which this event belongs." msgstr "" #. Description of the 'Used' (Int) field in DocType 'Quota' @@ -10242,7 +9174,7 @@ msgstr "" msgid "The domain name used to create DNS records." msgstr "" -#: mail/client/doctype/mail_queue/mail_queue.py:361 +#: mail/client/doctype/mail_queue/mail_queue.py:364 msgid "The domain {0} is not verified. Please verify the domain or use an email address with a verified domain." msgstr "" @@ -10251,12 +9183,12 @@ msgstr "" msgid "The email address associated with the participant." msgstr "" -#: mail/utils/validation.py:73 +#: mail/utils/validation.py:29 msgid "The email address {0} is already assigned." msgstr "" -#: mail/client/doctype/blocked_email_address/blocked_email_address.py:30 -msgid "The email address {0} is already blocked for user {1}." +#: mail/client/doctype/blocked_email_address/blocked_email_address.py:40 +msgid "The email address {0} is already blocked for account {1}." msgstr "" #. Description of the 'Email' (Data) field in DocType 'Event Notification' @@ -10264,35 +9196,17 @@ msgstr "" msgid "The email of the person who made the change." msgstr "" -#. Description of the 'Endpoint' (Data) field in DocType 'Mail Cluster' -#. Description of the 'Endpoint' (Data) field in DocType 'Mail Cluster Trace' -#: mail/server/doctype/mail_cluster/mail_cluster.json -#: mail/server/doctype/mail_cluster_trace/mail_cluster_trace.json -msgid "The endpoint for Open Telemetry." -msgstr "" - #. Description of the 'When' (Data) field in DocType 'Event Alert' #: mail/client/doctype/event_alert/event_alert.json msgid "The exact date and time when the alert should trigger." msgstr "" -#. Description of the 'Rotate Frequency' (Select) field in DocType 'Mail -#. Cluster Trace' -#: mail/server/doctype/mail_cluster_trace/mail_cluster_trace.json -msgid "The frequency to rotate the log file." -msgstr "" - -#. Description of the 'Region' (Data) field in DocType 'Mail Cluster Store' -#: mail/server/doctype/mail_cluster_store/mail_cluster_store.json -msgid "The geographical region where the bucket resides." -msgstr "" - #. Description of the 'Hard Limit' (Int) field in DocType 'Quota' #: mail/client/doctype/quota/quota.json msgid "The hard limit set by this quota." msgstr "" -#: mail/client/doctype/mail_queue/mail_queue.py:427 +#: mail/client/doctype/mail_queue/mail_queue.py:430 msgid "The header {0} is a standard email header and cannot be overridden. Please use custom headers prefixed with X-." msgstr "" @@ -10301,17 +9215,18 @@ msgstr "" msgid "The human-readable name of the calendar." msgstr "" -#. Description of the 'ID' (Data) field in DocType 'Event Notification' +#. Description of the 'Event Notification ID' (Data) field in DocType 'Event +#. Notification' #: mail/client/doctype/event_notification/event_notification.json msgid "The id of the Event Notification." msgstr "" -#. Description of the 'ID' (Data) field in DocType 'Identity' +#. Description of the 'Identity ID' (Data) field in DocType 'Identity' #: mail/client/doctype/identity/identity.json msgid "The id of the Identity." msgstr "" -#. Description of the 'ID' (Data) field in DocType 'Mailbox' +#. Description of the 'Mailbox ID' (Data) field in DocType 'Mailbox' #: mail/client/doctype/mailbox/mailbox.json msgid "The id of the Mailbox." msgstr "" @@ -10332,23 +9247,12 @@ msgstr "" msgid "The link to the CalendarEvent that this notification is about." msgstr "" -#. Description of the 'Logging Level' (Select) field in DocType 'Mail Cluster -#. Trace' -#: mail/server/doctype/mail_cluster_trace/mail_cluster_trace.json -msgid "The logging level for this tracer." -msgstr "" - #. Description of the 'Cluster' (Link) field in DocType 'Mail Server' #: mail/server/doctype/mail_server/mail_server.json msgid "The mail cluster this server belongs to." msgstr "" -#. Description of the 'Max Connections' (Int) field in DocType 'Mail Server' -#: mail/server/doctype/mail_server/mail_server.json -msgid "The maximum number of concurrent connections the server will accept." -msgstr "" - -#. Description of the 'Retry Limit' (Int) field in DocType 'Mail Cluster Store' +#. Description of the 'Max Retries' (Int) field in DocType 'Mail Cluster Store' #: mail/server/doctype/mail_cluster_store/mail_cluster_store.json msgid "The maximum number of times to retry failed requests. Set to 0 to disable retries." msgstr "" @@ -10366,18 +9270,6 @@ msgstr "" msgid "The method {method} is missing the required {decorator} decorator. Please add it to enforce dynamic rate limiting." msgstr "" -#. Description of the 'Push Interval (Seconds)' (Int) field in DocType 'Mail -#. Cluster' -#: mail/server/doctype/mail_cluster/mail_cluster.json -msgid "The minimum amount of time that must pass between each push request to the OpenTelemetry endpoint." -msgstr "" - -#. Description of the 'Throttle (Milliseconds)' (Int) field in DocType 'Mail -#. Cluster Trace' -#: mail/server/doctype/mail_cluster_trace/mail_cluster_trace.json -msgid "The minimum amount of time that must pass between each request to the OpenTelemetry endpoint." -msgstr "" - #. Description of the 'Container' (Data) field in DocType 'Mail Cluster Store' #: mail/server/doctype/mail_cluster_store/mail_cluster_store.json msgid "The name of the container in the Storage Account." @@ -10393,11 +9285,6 @@ msgstr "" msgid "The name of the quota object." msgstr "" -#. Description of the 'Endpoint' (Data) field in DocType 'Mail Cluster Store' -#: mail/server/doctype/mail_cluster_store/mail_cluster_store.json -msgid "The network address (hostname and optionally a port) of the S3 service. For S3-compatible services, you will need to specify the endpoint explicitly." -msgstr "" - #. Description of the 'Unread Emails' (Int) field in DocType 'Mailbox' #: mail/client/doctype/mailbox/mailbox.json msgid "The number of Emails in this Mailbox that have neither the `$seen` keyword nor the `$draft` keyword." @@ -10423,11 +9310,6 @@ msgstr "" msgid "The participant’s current participation status." msgstr "" -#. Description of the 'Path' (Data) field in DocType 'Mail Cluster Trace' -#: mail/server/doctype/mail_cluster_trace/mail_cluster_trace.json -msgid "The path to the log file." -msgstr "" - #. Description of the 'Participants' (Table) field in DocType 'Calendar Event' #: mail/client/doctype/calendar_event/calendar_event.json msgid "The people or entities involved in the event." @@ -10443,11 +9325,6 @@ msgstr "" msgid "The plaintext body to send in response to messages when the vacation response is enabled." msgstr "" -#. Description of the 'Prefix' (Data) field in DocType 'Mail Cluster Trace' -#: mail/server/doctype/mail_cluster_trace/mail_cluster_trace.json -msgid "The prefix for the log file." -msgstr "" - #. Description of the 'May Write All' (Check) field in DocType 'Calendar #. Rights' #: mail/client/doctype/calendar_rights/calendar_rights.json @@ -10498,17 +9375,11 @@ msgstr "" msgid "The principals this calendar is shared with, and the access rights granted to each." msgstr "" -#. Description of the 'Protocol' (Select) field in DocType 'Mail Server -#. Listener' -#: mail/server/doctype/mail_server_listener/mail_server_listener.json -msgid "The protocol used by the listener." -msgstr "" - -#: mail/api/outbound.py:125 mail/api/outbound.py:190 +#: mail/api/outbound.py:123 mail/api/outbound.py:188 msgid "The raw message exceeds the maximum allowed size of {0} MB." msgstr "" -#: mail/api/outbound.py:120 mail/api/outbound.py:236 +#: mail/api/outbound.py:118 mail/api/outbound.py:234 msgid "The raw message is required." msgstr "" @@ -10534,13 +9405,8 @@ msgstr "" msgid "The secret key for the S3 account." msgstr "" -#. Description of the 'Security Token' (Password) field in DocType 'Mail -#. Cluster Store' -#: mail/server/doctype/mail_cluster_store/mail_cluster_store.json -msgid "The security token for the S3 account." -msgstr "" - -#. Description of the 'ID' (Data) field in DocType 'Calendar Event' +#. Description of the 'Calendar Event ID' (Data) field in DocType 'Calendar +#. Event' #: mail/client/doctype/calendar_event/calendar_event.json msgid "The server-assigned unique identifier of the event." msgstr "" @@ -10602,25 +9468,12 @@ msgstr "" msgid "The token used to authenticate with your DNS provider." msgstr "" -#. Description of the 'Transport' (Select) field in DocType 'Mail Cluster' -#. Description of the 'Transport' (Select) field in DocType 'Mail Cluster -#. Trace' -#: mail/server/doctype/mail_cluster/mail_cluster.json -#: mail/server/doctype/mail_cluster_trace/mail_cluster_trace.json -msgid "The transport protocol for Open Telemetry." -msgstr "" - #. Description of the 'Kind' (Select) field in DocType 'Event Participant' #: mail/client/doctype/event_participant/event_participant.json msgid "The type of participant." msgstr "" -#. Description of the 'Method' (Select) field in DocType 'Mail Cluster Trace' -#: mail/server/doctype/mail_cluster_trace/mail_cluster_trace.json -msgid "The type of tracer." -msgstr "" - -#. Description of the 'ID' (Data) field in DocType 'Quota' +#. Description of the 'Quota ID' (Data) field in DocType 'Quota' #: mail/client/doctype/quota/quota.json msgid "The unique identifier for this quota." msgstr "" @@ -10825,7 +9678,7 @@ msgstr "" msgid "This request has expired. Please create a new one." msgstr "" -#: frontend/src/components/MailThread.vue:224 +#: frontend/src/components/MailThread.vue:258 msgid "This sender is blocked" msgstr "" @@ -10843,7 +9696,7 @@ msgstr "" msgid "This will replace any existing JMAP Push keys. Existing push subscriptions must be recreated after generating new keys. Continue?" msgstr "" -#: frontend/src/pages/MailboxView.vue:918 +#: frontend/src/pages/MailboxView.vue:950 msgid "Thread" msgstr "" @@ -10854,46 +9707,41 @@ msgstr "" msgid "Thread ID" msgstr "" -#. Label of the workers (Int) field in DocType 'Mail Cluster Store' -#: mail/server/doctype/mail_cluster_store/mail_cluster_store.json -msgid "Thread Pool Size" -msgstr "" - -#: frontend/src/pages/MailboxView.vue:1081 +#: frontend/src/pages/MailboxView.vue:1113 msgid "Thread deleted." msgstr "" -#: frontend/src/pages/MailboxView.vue:1013 -#: frontend/src/pages/MailboxView.vue:1070 +#: frontend/src/pages/MailboxView.vue:1045 +#: frontend/src/pages/MailboxView.vue:1102 msgid "Thread marked as {0}." msgstr "" -#: frontend/src/pages/MailboxView.vue:1042 +#: frontend/src/pages/MailboxView.vue:1074 msgid "Thread moved back." msgstr "" -#: frontend/src/pages/MailboxView.vue:1051 +#: frontend/src/pages/MailboxView.vue:1083 msgid "Thread moved to {0}." msgstr "" -#: frontend/src/pages/MailboxView.vue:918 +#: frontend/src/pages/MailboxView.vue:950 msgid "Threads" msgstr "" -#: frontend/src/pages/MailboxView.vue:1081 +#: frontend/src/pages/MailboxView.vue:1113 msgid "Threads deleted." msgstr "" -#: frontend/src/pages/MailboxView.vue:1014 -#: frontend/src/pages/MailboxView.vue:1071 +#: frontend/src/pages/MailboxView.vue:1046 +#: frontend/src/pages/MailboxView.vue:1103 msgid "Threads marked as {0}." msgstr "" -#: frontend/src/pages/MailboxView.vue:1042 +#: frontend/src/pages/MailboxView.vue:1074 msgid "Threads moved back." msgstr "" -#: frontend/src/pages/MailboxView.vue:1052 +#: frontend/src/pages/MailboxView.vue:1084 msgid "Threads moved to {0}." msgstr "" @@ -10903,13 +9751,6 @@ msgstr "" msgid "Threshold for when to switch from scanning only the email body to scanning attachments selectively." msgstr "" -#. Label of the jmap_push_throttle (Int) field in DocType 'Mail Cluster' -#. Label of the throttle (Int) field in DocType 'Mail Cluster Trace' -#: mail/server/doctype/mail_cluster/mail_cluster.json -#: mail/server/doctype/mail_cluster_trace/mail_cluster_trace.json -msgid "Throttle (Milliseconds)" -msgstr "" - #. Label of the time_zone (Autocomplete) field in DocType 'Calendar' #. Label of the time_zone (Autocomplete) field in DocType 'Calendar Event' #. Label of the time_zone (Autocomplete) field in DocType 'Contact Card @@ -10920,55 +9761,31 @@ msgstr "" msgid "Time Zone" msgstr "" -#. Description of the 'Request Timeout (Milliseconds)' (Int) field in DocType -#. 'Mail Cluster' -#: mail/server/doctype/mail_cluster/mail_cluster.json -msgid "Time before a connection with a push service URL times out." -msgstr "" - -#. Description of the 'Throttle (Milliseconds)' (Int) field in DocType 'Mail -#. Cluster' -#: mail/server/doctype/mail_cluster/mail_cluster.json -msgid "Time to wait before sending a new request to the push service." -msgstr "" - -#. Description of the 'Attempt Interval (Milliseconds)' (Int) field in DocType -#. 'Mail Cluster' -#: mail/server/doctype/mail_cluster/mail_cluster.json -msgid "Time to wait between push attempts." -msgstr "" - -#. Description of the 'Retry Interval (Milliseconds)' (Int) field in DocType -#. 'Mail Cluster' -#: mail/server/doctype/mail_cluster/mail_cluster.json -msgid "Time to wait between retry attempts." +#: mail/server/doctype/spam_check_log/spam_check_log.py:128 +msgid "Timed out waiting for response from SpamAssassin." msgstr "" -#. Description of the 'Verification Timeout (Milliseconds)' (Int) field in -#. DocType 'Mail Cluster' -#: mail/server/doctype/mail_cluster/mail_cluster.json -msgid "Time to wait for the push service to verify a subscription." +#. Label of the timeout (Data) field in DocType 'Mail Cluster Store' +#: mail/server/doctype/mail_cluster_store/mail_cluster_store.json +msgid "Timeout" msgstr "" -#: mail/server/doctype/spam_check_log/spam_check_log.py:128 -msgid "Timed out waiting for response from SpamAssassin." +#. Description of the 'Pool Timeout Create' (Data) field in DocType 'Mail +#. Cluster Store' +#: mail/server/doctype/mail_cluster_store/mail_cluster_store.json +msgid "Timeout for creating a new connection." msgstr "" -#. Label of the metrics_open_telemetry_timeout (Int) field in DocType 'Mail -#. Cluster' -#. Label of the timeout (Int) field in DocType 'Mail Cluster Store' -#. Label of the transaction_timeout (Int) field in DocType 'Mail Cluster Store' -#. Label of the timeout (Int) field in DocType 'Mail Cluster Trace' -#: mail/server/doctype/mail_cluster/mail_cluster.json +#. Description of the 'Pool Timeout Recycle' (Data) field in DocType 'Mail +#. Cluster Store' #: mail/server/doctype/mail_cluster_store/mail_cluster_store.json -#: mail/server/doctype/mail_cluster_trace/mail_cluster_trace.json -msgid "Timeout (Seconds)" +msgid "Timeout for recycling a connection." msgstr "" -#. Description of the 'State Last Update (Email)' (Datetime) field in DocType -#. 'User Settings' -#: mail/client/doctype/user_settings/user_settings.json -msgid "Timestamp of the last successful email state synchronization." +#. Description of the 'Pool Timeout Wait' (Data) field in DocType 'Mail Cluster +#. Store' +#: mail/server/doctype/mail_cluster_store/mail_cluster_store.json +msgid "Timeout for waiting for a connection from the pool." msgstr "" #. Label of the title (Data) field in DocType 'Calendar Event' @@ -10978,28 +9795,26 @@ msgstr "" #. Label of the _to (Data) field in DocType 'Mail Message' #. Option for the 'Type' (Select) field in DocType 'Mail Message Recipient' -#. Label of the envelope_to (Data) field in DocType 'DMARC Report Detail' #: frontend/src/components/ComposeMailEditor.vue:61 -#: frontend/src/components/Modals/SearchModal.vue:53 mail/api/mail.py:436 +#: frontend/src/components/Modals/SearchModal.vue:56 mail/api/mail.py:431 #: mail/client/doctype/mail_message/mail_message.json #: mail/client/doctype/mail_message_recipient/mail_message_recipient.json -#: mail/server/doctype/dmarc_report_detail/dmarc_report_detail.json msgid "To" msgstr "" #. Label of the to_date (Datetime) field in DocType 'Vacation Response' -#: frontend/src/components/Modals/SearchModal.vue:68 +#: frontend/src/components/Modals/SearchModal.vue:71 #: frontend/src/components/Settings/ExportSettings.vue:41 #: frontend/src/components/Settings/VacationResponseSettings.vue:19 #: mail/client/doctype/vacation_response/vacation_response.json msgid "To Date" msgstr "" -#: mail/client/doctype/vacation_response/vacation_response.py:100 +#: mail/client/doctype/vacation_response/vacation_response.py:99 msgid "To Date must be after From Date." msgstr "" -#: frontend/src/components/MailListItem.vue:251 +#: frontend/src/components/MailListItem.vue:255 msgid "To:" msgstr "" @@ -11024,20 +9839,20 @@ msgstr "" msgid "Token" msgstr "" -#. Label of the total_cached_blobs (Int) field in DocType 'User Settings' -#: mail/client/doctype/user_settings/user_settings.json +#. Label of the total_cached_blobs (Int) field in DocType 'Account Settings' +#: mail/client/doctype/account_settings/account_settings.json msgid "Total Cached Blobs" msgstr "" -#. Label of the total_cached_contact_cards (Int) field in DocType 'User +#. Label of the total_cached_contact_cards (Int) field in DocType 'Account #. Settings' -#: mail/client/doctype/user_settings/user_settings.json +#: mail/client/doctype/account_settings/account_settings.json msgid "Total Cached Contact Cards" msgstr "" -#. Label of the total_cached_mail_messages (Int) field in DocType 'User +#. Label of the total_cached_mail_messages (Int) field in DocType 'Account #. Settings' -#: mail/client/doctype/user_settings/user_settings.json +#: mail/client/doctype/account_settings/account_settings.json msgid "Total Cached Mail Messages" msgstr "" @@ -11050,12 +9865,6 @@ msgstr "" msgid "Total Emails" msgstr "" -#. Label of the jmap_protocol_upload_quota_files (Int) field in DocType 'Mail -#. Cluster' -#: mail/server/doctype/mail_cluster/mail_cluster.json -msgid "Total Files" -msgstr "" - #: frontend/src/pages/dashboard/MailingListView.vue:27 msgid "Total Members" msgstr "" @@ -11064,39 +9873,11 @@ msgstr "" msgid "Total Quota" msgstr "" -#. Label of the jmap_protocol_upload_quota_size (Int) field in DocType 'Mail -#. Cluster' -#: mail/server/doctype/mail_cluster/mail_cluster.json -msgid "Total Size (MB)" -msgstr "" - #. Label of the total_threads (Int) field in DocType 'Mailbox' #: mail/client/doctype/mailbox/mailbox.json msgid "Total Threads" msgstr "" -#. Option for the 'Logging Level' (Select) field in DocType 'Mail Cluster -#. Trace' -#: mail/server/doctype/mail_cluster_trace/mail_cluster_trace.json -msgid "Trace" -msgstr "" - -#. Label of the tracer_id (Data) field in DocType 'Mail Cluster Trace' -#: mail/server/doctype/mail_cluster_trace/mail_cluster_trace.json -msgid "Tracer ID" -msgstr "" - -#. Label of the tracer_configuration_section (Section Break) field in DocType -#. 'Mail Cluster Trace' -#: mail/server/doctype/mail_cluster_trace/mail_cluster_trace.json -msgid "Tracer configuration" -msgstr "" - -#. Label of the traces (Table) field in DocType 'Mail Cluster' -#: mail/server/doctype/mail_cluster/mail_cluster.json -msgid "Traces" -msgstr "" - #: frontend/src/components/Settings/ExportSettings.vue:177 #: frontend/src/components/Settings/ImportSettings.vue:143 msgid "Track status" @@ -11107,46 +9888,40 @@ msgstr "" msgid "Tracking Record" msgstr "" -#. Label of the transaction_settings_section (Section Break) field in DocType -#. 'Mail Cluster Store' -#: mail/server/doctype/mail_cluster_store/mail_cluster_store.json -msgid "Transaction Settings" -msgstr "" - -#. Description of the 'Max Retry Delay (Seconds)' (Int) field in DocType 'Mail -#. Cluster Store' +#. Label of the transaction_retry_delay (Data) field in DocType 'Mail Cluster +#. Store' #: mail/server/doctype/mail_cluster_store/mail_cluster_store.json -msgid "Transaction maximum retry delay." +msgid "Transaction Retry Delay" msgstr "" -#. Description of the 'Retry Limit' (Int) field in DocType 'Mail Cluster Store' +#. Label of the transaction_retry_limit (Int) field in DocType 'Mail Cluster +#. Store' #: mail/server/doctype/mail_cluster_store/mail_cluster_store.json -msgid "Transaction retry limit." +msgid "Transaction Retry Limit" msgstr "" -#. Description of the 'Timeout (Seconds)' (Int) field in DocType 'Mail Cluster +#. Label of the transaction_timeout (Data) field in DocType 'Mail Cluster #. Store' #: mail/server/doctype/mail_cluster_store/mail_cluster_store.json -msgid "Transaction timeout." +msgid "Transaction Timeout" msgstr "" -#. Label of the metrics_open_telemetry_transport (Select) field in DocType -#. 'Mail Cluster' -#. Label of the transport (Select) field in DocType 'Mail Cluster Trace' -#: mail/server/doctype/mail_cluster/mail_cluster.json -#: mail/server/doctype/mail_cluster_trace/mail_cluster_trace.json -msgid "Transport" +#. Description of the 'Transaction Retry Delay' (Data) field in DocType 'Mail +#. Cluster Store' +#: mail/server/doctype/mail_cluster_store/mail_cluster_store.json +msgid "Transaction maximum retry delay." msgstr "" -#. Label of the email_auto_expunge (Int) field in DocType 'Mail Cluster' -#: mail/server/doctype/mail_cluster/mail_cluster.json -msgid "Trash Auto-Expunge (Days)" +#. Description of the 'Transaction Retry Limit' (Int) field in DocType 'Mail +#. Cluster Store' +#: mail/server/doctype/mail_cluster_store/mail_cluster_store.json +msgid "Transaction retry limit." msgstr "" -#. Label of the server_proxy_trusted_networks (Small Text) field in DocType -#. 'Mail Cluster' -#: mail/server/doctype/mail_cluster/mail_cluster.json -msgid "Trusted Networks" +#. Description of the 'Transaction Timeout' (Data) field in DocType 'Mail +#. Cluster Store' +#: mail/server/doctype/mail_cluster_store/mail_cluster_store.json +msgid "Transaction timeout." msgstr "" #. Label of the type (Data) field in DocType 'Contact Card Address' @@ -11158,6 +9933,7 @@ msgstr "" #. Label of the type (Select) field in DocType 'Mail Message Recipient' #. Label of the type (Select) field in DocType 'DNS Record' #. Label of the type (Select) field in DocType 'Mail Cluster Store' +#. Label of the type (Select) field in DocType 'Mail Cluster Store HTTP Auth' #. Label of the type (Select) field in DocType 'Principal' #. Label of the type (Select) field in DocType 'Principal DNS Record' #. Label of the type (Data) field in DocType 'Principal Permission' @@ -11165,9 +9941,9 @@ msgstr "" #: frontend/src/components/Modals/AddContactAddressModal.vue:8 #: frontend/src/components/Modals/AddContactEmailModal.vue:14 #: frontend/src/components/Modals/AddContactPhoneModal.vue:9 -#: frontend/src/pages/ContactView.vue:390 -#: frontend/src/pages/ContactView.vue:396 -#: frontend/src/pages/ContactView.vue:401 +#: frontend/src/pages/ContactView.vue:393 +#: frontend/src/pages/ContactView.vue:399 +#: frontend/src/pages/ContactView.vue:404 #: mail/client/doctype/contact_card_address/contact_card_address.json #: mail/client/doctype/contact_card_email/contact_card_email.json #: mail/client/doctype/contact_card_phone/contact_card_phone.json @@ -11177,16 +9953,22 @@ msgstr "" #: mail/client/doctype/mail_message_recipient/mail_message_recipient.json #: mail/server/doctype/dns_record/dns_record.json #: mail/server/doctype/mail_cluster_store/mail_cluster_store.json +#: mail/server/doctype/mail_cluster_store_http_auth/mail_cluster_store_http_auth.json #: mail/server/doctype/principal/principal.json #: mail/server/doctype/principal_dns_record/principal_dns_record.json #: mail/server/doctype/principal_permission/principal_permission.json msgid "Type" msgstr "" -#. Description of the 'Server Type' (Select) field in DocType 'Mail Cluster -#. Store' +#. Description of the 'Type' (Select) field in DocType 'Mail Cluster Store HTTP +#. Auth' +#: mail/server/doctype/mail_cluster_store_http_auth/mail_cluster_store_http_auth.json +msgid "Type of the HTTP authentication method." +msgstr "" + +#. Description of the 'Type' (Select) field in DocType 'Mail Cluster Store' #: mail/server/doctype/mail_cluster_store/mail_cluster_store.json -msgid "Type of Redis server." +msgid "Type of the store backend." msgstr "" #: frontend/src/components/Settings/VacationResponseSettings.vue:32 @@ -11246,33 +10028,23 @@ msgstr "" msgid "URL where HTTP requests are sent before being forwarded to mail servers." msgstr "" -#. Description of the 'Base URL' (Data) field in DocType 'Mail Server' -#: mail/server/doctype/mail_server/mail_server.json -msgid "URL where HTTP requests are sent." -msgstr "" - -#. Description of the 'Redis URL(s)' (Small Text) field in DocType 'Mail -#. Cluster Store' +#. Description of the 'URLs' (JSON) field in DocType 'Mail Cluster Store' #: mail/server/doctype/mail_cluster_store/mail_cluster_store.json -msgid "URL(s) of the Redis server(s)." +msgid "URL(s) of the Redis server(s)" msgstr "" +#. Label of the urls (JSON) field in DocType 'Mail Cluster Store' #. Label of the urls_tab (Tab Break) field in DocType 'Principal' +#: mail/server/doctype/mail_cluster_store/mail_cluster_store.json #: mail/server/doctype/principal/principal.json msgid "URLs" msgstr "" -#. Label of the storage_undelete_retention (Int) field in DocType 'Mail -#. Cluster' -#: mail/server/doctype/mail_cluster/mail_cluster.json -msgid "Un-delete Period (Days)" -msgstr "" - -#: mail/client/doctype/user_settings/user_settings.py:115 +#: mail/client/doctype/user_settings/user_settings.py:85 msgid "Unable to connect to the JMAP server. Please check the server URL and your network connection." msgstr "" -#: mail/client/doctype/user_settings/user_settings.py:112 +#: mail/client/doctype/user_settings/user_settings.py:82 msgid "Unable to connect to the JMAP server. Please check your credentials." msgstr "" @@ -11280,20 +10052,26 @@ msgstr "" msgid "Unable to save appearance settings." msgstr "" -#: frontend/src/components/MailThread.vue:237 +#. Option for the 'Type' (Select) field in DocType 'Mail Cluster Store HTTP +#. Auth' +#: mail/server/doctype/mail_cluster_store_http_auth/mail_cluster_store_http_auth.json +msgid "Unauthenticated" +msgstr "" + +#: frontend/src/components/MailThread.vue:271 #: frontend/src/components/Settings/BlockListSettings.vue:33 msgid "Unblock" msgstr "" -#: frontend/src/components/Settings/BlockListSettings.vue:91 +#: frontend/src/components/Settings/BlockListSettings.vue:95 msgid "Unblock Email Addresses" msgstr "" -#: frontend/src/components/MailActions.vue:191 +#: frontend/src/components/MailActions.vue:198 msgid "Unblock Sender" msgstr "" -#: frontend/src/components/MailActions.vue:307 +#: frontend/src/components/MailActions.vue:341 msgid "Unblocking email address..." msgstr "" @@ -11301,48 +10079,15 @@ msgstr "" msgid "Undo Last Action" msgstr "" -#: frontend/src/components/MailActions.vue:226 -#: frontend/src/components/MailActions.vue:249 -#: frontend/src/components/MailActions.vue:302 -#: frontend/src/pages/MailboxView.vue:1006 -#: frontend/src/pages/MailboxView.vue:1043 -#: frontend/src/pages/MailboxView.vue:1063 +#: frontend/src/components/MailActions.vue:233 +#: frontend/src/components/MailActions.vue:256 +#: frontend/src/components/MailActions.vue:336 +#: frontend/src/pages/MailboxView.vue:1038 +#: frontend/src/pages/MailboxView.vue:1075 +#: frontend/src/pages/MailboxView.vue:1095 msgid "Undoing..." msgstr "" -#. Description of the 'Directory ID' (Data) field in DocType 'Mail Server ACME -#. Provider' -#: mail/server/doctype/mail_server_acme_provider/mail_server_acme_provider.json -msgid "Unique identifier for the ACME provider." -msgstr "" - -#. Description of the 'Certificate ID' (Data) field in DocType 'Mail Server TLS -#. Certificate' -#: mail/server/doctype/mail_server_tls_certificate/mail_server_tls_certificate.json -msgid "Unique identifier for the TLS certificate." -msgstr "" - -#. Description of the 'Listener ID' (Data) field in DocType 'Mail Server -#. Listener' -#: mail/server/doctype/mail_server_listener/mail_server_listener.json -msgid "Unique identifier for the listener." -msgstr "" - -#. Description of the 'Store ID' (Data) field in DocType 'Mail Cluster Store' -#: mail/server/doctype/mail_cluster_store/mail_cluster_store.json -msgid "Unique identifier for the store." -msgstr "" - -#. Description of the 'Tracer ID' (Data) field in DocType 'Mail Cluster Trace' -#: mail/server/doctype/mail_cluster_trace/mail_cluster_trace.json -msgid "Unique identifier for the tracer." -msgstr "" - -#. Description of the 'Node ID' (Int) field in DocType 'Mail Server' -#: mail/server/doctype/mail_server/mail_server.json -msgid "Unique identifier for this node in the cluster." -msgstr "" - #. Description of the 'Zone ID' (Data) field in DocType 'Mail Settings' #: mail/mail/doctype/mail_settings/mail_settings.json msgid "Unique identifier for your DNS zone." @@ -11350,14 +10095,14 @@ msgstr "" #. Description of the 'Mailbox ID' (Data) field in DocType 'Mailbox Settings' #: mail/client/doctype/mailbox_settings/mailbox_settings.json -msgid "Unique identifier of the mailbox associated with the user." +msgid "Unique identifier of the mailbox associated with the account." msgstr "" #: frontend/src/components/QuotaProgressBar.vue:33 msgid "Unlimited" msgstr "" -#: frontend/src/components/QuotaBar.vue:45 +#: frontend/src/components/QuotaBar.vue:52 msgid "Unlimited ({0} used)" msgstr "" @@ -11368,7 +10113,7 @@ msgstr "" msgid "Unreachable" msgstr "" -#: frontend/src/pages/MailboxView.vue:1117 +#: frontend/src/pages/MailboxView.vue:1149 msgid "Unread" msgstr "" @@ -11377,7 +10122,7 @@ msgstr "" msgid "Unread Emails" msgstr "" -#: frontend/src/pages/MailboxView.vue:1180 +#: frontend/src/pages/MailboxView.vue:1212 msgid "Unread Mails" msgstr "" @@ -11386,37 +10131,41 @@ msgstr "" msgid "Unread Threads" msgstr "" -#: mail/utils/__init__.py:482 mail/utils/__init__.py:491 +#: mail/utils/__init__.py:496 mail/utils/__init__.py:505 msgid "Unsafe file path detected: {0}" msgstr "" -#: frontend/src/components/MailActions.vue:81 -#: frontend/src/components/MailActions.vue:146 +#: frontend/src/components/MailActions.vue:82 +#: frontend/src/components/MailActions.vue:147 msgid "Unstar" msgstr "" -#: mail/utils/__init__.py:286 +#: mail/utils/__init__.py:300 msgid "Unsupported algorithm. Use 'rsa-sha256' or 'ed25519-sha256'." msgstr "" -#: mail/client/doctype/mail_exchange/mail_exchange.py:380 +#: mail/stalwart/cli.py:56 +msgid "Unsupported architecture: {0}" +msgstr "" + +#: mail/client/doctype/mail_exchange/mail_exchange.py:379 msgid "Unsupported export format: {0}" msgstr "" -#: mail/utils/__init__.py:509 +#: mail/utils/__init__.py:523 msgid "Unsupported file format: {0}" msgstr "" -#: mail/client/doctype/mail_exchange/mail_exchange.py:112 +#: mail/client/doctype/mail_exchange/mail_exchange.py:111 msgid "Unsupported import format: {0}" msgstr "" -#: mail/utils/__init__.py:540 -msgid "Unsupported output file format. Supported formats are .zip, .tgz, and .tar.gz." +#: mail/stalwart/cli.py:63 +msgid "Unsupported operating system: {0}" msgstr "" -#: mail/server/doctype/server_config/server_config.js:23 -msgid "Update config.toml" +#: mail/utils/__init__.py:554 +msgid "Unsupported output file format. Supported formats are .zip, .tgz, and .tar.gz." msgstr "" #. Option for the 'Type' (Select) field in DocType 'Event Notification' @@ -11438,20 +10187,10 @@ msgstr "" msgid "Updated On" msgstr "" -#: mail/server/doctype/server_config/server_config.js:53 -msgid "Updating Configuration..." -msgstr "" - #: frontend/src/components/Settings/ImportSettings.vue:35 msgid "Upload File" msgstr "" -#. Label of the upload_limits_section (Section Break) field in DocType 'Mail -#. Cluster' -#: mail/server/doctype/mail_cluster/mail_cluster.json -msgid "Upload Limits" -msgstr "" - #: frontend/src/components/Modals/EditPhotoModal.vue:23 msgid "Upload New Photo" msgstr "" @@ -11460,11 +10199,6 @@ msgstr "" msgid "Uploading ({0}%)" msgstr "" -#. Label of the ansi (Check) field in DocType 'Mail Cluster Trace' -#: mail/server/doctype/mail_cluster_trace/mail_cluster_trace.json -msgid "Use ANSI colors" -msgstr "" - #. Label of the use_default_alerts (Check) field in DocType 'Calendar Event' #: mail/client/doctype/calendar_event/calendar_event.json msgid "Use Default Alerts" @@ -11474,8 +10208,12 @@ msgstr "" msgid "Use Saved Signature" msgstr "" -#. Description of the 'Enable TLS' (Check) field in DocType 'Mail Cluster -#. Store' +#. Label of the use_tls (Check) field in DocType 'Mail Cluster Store' +#: mail/server/doctype/mail_cluster_store/mail_cluster_store.json +msgid "Use TLS" +msgstr "" + +#. Description of the 'Use TLS' (Check) field in DocType 'Mail Cluster Store' #: mail/server/doctype/mail_cluster_store/mail_cluster_store.json msgid "Use TLS to connect to the store." msgstr "" @@ -11496,17 +10234,12 @@ msgstr "" msgid "Used Quota" msgstr "" -#. Description of the 'Blob Storage' (Section Break) field in DocType 'Mail -#. Cluster' -#: mail/server/doctype/mail_cluster/mail_cluster.json -msgid "Used for storing large binary objects such as emails, sieve scripts, and other files." -msgstr "" - #. Description of the 'Profile' (Data) field in DocType 'Mail Cluster Store' #: mail/server/doctype/mail_cluster_store/mail_cluster_store.json msgid "Used when retrieving credentials from a shared credentials file. If specified, the server will use the access key ID, secret access key, and session token (if available) associated with the given profile." msgstr "" +#. Label of the user (Link) field in DocType 'Account Settings' #. Label of the user (Link) field in DocType 'Address Book' #. Label of the user (Link) field in DocType 'Blocked Email Address' #. Label of the user (Link) field in DocType 'Calendar' @@ -11517,6 +10250,7 @@ msgstr "" #. Label of the user (Link) field in DocType 'Mail Exchange' #. Label of the user (Link) field in DocType 'Mail Message' #. Label of the user (Link) field in DocType 'Mail Queue' +#. Label of the user (Link) field in DocType 'Mail Signature' #. Label of the user (Link) field in DocType 'Mail Sync History' #. Label of the user (Link) field in DocType 'Mailbox' #. Label of the user (Link) field in DocType 'Mailbox Settings' @@ -11524,11 +10258,13 @@ msgstr "" #. Label of the user (Link) field in DocType 'Push Subscription' #. Label of the user (Link) field in DocType 'Quota' #. Label of the user (Link) field in DocType 'Sieve Script' +#. Label of the user (Link) field in DocType 'User Account' #. Label of the user (Link) field in DocType 'User Settings' #. Label of the user (Link) field in DocType 'Vacation Response' #. Label of the user (Link) field in DocType 'Mail Data Exchange' #. Label of the user (Link) field in DocType 'Mail Domain Request' #: frontend/src/pages/dashboard/UsersView.vue:148 +#: mail/client/doctype/account_settings/account_settings.json #: mail/client/doctype/address_book/address_book.json #: mail/client/doctype/blocked_email_address/blocked_email_address.json #: mail/client/doctype/calendar/calendar.json @@ -11539,6 +10275,7 @@ msgstr "" #: mail/client/doctype/mail_exchange/mail_exchange.json #: mail/client/doctype/mail_message/mail_message.json #: mail/client/doctype/mail_queue/mail_queue.json +#: mail/client/doctype/mail_signature/mail_signature.json #: mail/client/doctype/mail_sync_history/mail_sync_history.json #: mail/client/doctype/mailbox/mailbox.json #: mail/client/doctype/mailbox_settings/mailbox_settings.json @@ -11546,6 +10283,7 @@ msgstr "" #: mail/client/doctype/push_subscription/push_subscription.json #: mail/client/doctype/quota/quota.json #: mail/client/doctype/sieve_script/sieve_script.json +#: mail/client/doctype/user_account/user_account.json #: mail/client/doctype/user_settings/user_settings.json #: mail/client/doctype/vacation_response/vacation_response.json #: mail/server/doctype/mail_data_exchange/mail_data_exchange.json @@ -11553,6 +10291,11 @@ msgstr "" msgid "User" msgstr "" +#. Name of a DocType +#: mail/client/doctype/user_account/user_account.json +msgid "User Account" +msgstr "" + #. Label of the user_details_section (Section Break) field in DocType 'Mail #. Account Request' #: mail/server/doctype/mail_account_request/mail_account_request.json @@ -11564,25 +10307,6 @@ msgstr "" msgid "User Settings" msgstr "" -#: mail/client/doctype/mail_message/mail_message.py:752 -#: mail/client/doctype/mail_message/mail_message.py:821 -#: mail/client/doctype/mail_message/mail_message.py:851 -#: mail/client/doctype/mail_message/mail_message.py:881 -msgid "User and Mail IDs are required." -msgstr "" - -#: mail/client/doctype/mail_message/mail_message.py:772 -msgid "User and Mailbox ID are required." -msgstr "" - -#: mail/client/doctype/mail_message/mail_message.py:724 -msgid "User and Thread IDs are required." -msgstr "" - -#: mail/client/doctype/mail_message/mail_message.py:670 -msgid "User and filter are required." -msgstr "" - #. Description of the 'Email' (Data) field in DocType 'Participant Identity' #: mail/client/doctype/participant_identity/participant_identity.json msgid "User associated email address to use when adding this participant to an event." @@ -11596,10 +10320,6 @@ msgstr "" msgid "User is required" msgstr "" -#: mail/api/mail.py:192 mail/client/doctype/mail_message/mail_message.py:918 -msgid "User is required." -msgstr "" - #: mail/server/doctype/mail_account_request/mail_account_request.py:197 msgid "User with email {0} already exists." msgstr "" @@ -11608,13 +10328,12 @@ msgstr "" msgid "User {0} do not have permission to access the file {1}." msgstr "" -#: mail/api/account.py:171 +#: mail/api/account.py:172 msgid "User {0} does not exist." msgstr "" -#: mail/api/auth.py:30 mail/api/outbound.py:26 -#: mail/client/doctype/mail_exchange/mail_exchange.py:555 -#: mail/jmap/__init__.py:38 mail/utils/user.py:67 +#: mail/client/doctype/mail_exchange/mail_exchange.py:557 +#: mail/jmap/__init__.py:53 mail/utils/user.py:69 msgid "User {0} does not have JMAP settings configured." msgstr "" @@ -11623,20 +10342,20 @@ msgstr "" msgid "User {0} does not have Mail Admin role." msgstr "" -#: mail/utils/user.py:55 +#: mail/utils/user.py:57 msgid "User {0} does not have User Settings configured." msgstr "" -#: mail/client/doctype/identity/identity.py:127 -msgid "User {0} does not have permission to create identity for user {1}." +#: mail/client/doctype/user_account/user_account.py:119 mail/utils/user.py:113 +msgid "User {0} does not have a personal account configured." msgstr "" #: mail/server/doctype/mail_account_request/mail_account_request.py:93 msgid "User {0} does not have permission to invite." msgstr "" -#: mail/client/doctype/identity/identity.py:176 -#: mail/client/doctype/identity/identity.py:314 +#: mail/client/doctype/identity/identity.py:119 +#: mail/client/doctype/identity/identity.py:259 msgid "User {0} does not have permission to view identities for user {1}." msgstr "" @@ -11644,7 +10363,7 @@ msgstr "" msgid "User {0} is already registered." msgstr "" -#: mail/jmap/__init__.py:73 +#: mail/jmap/__init__.py:49 msgid "User {0} is disabled." msgstr "" @@ -11652,14 +10371,10 @@ msgstr "" msgid "User {0} is not a Mail Admin." msgstr "" -#: mail/utils/validation.py:370 +#: mail/utils/validation.py:330 msgid "User {0} is not a local user." msgstr "" -#: mail/client/doctype/mail_message/mail_message.py:800 -msgid "User, Mail IDs, and Mailbox ID are required." -msgstr "" - #. Description of the 'Name' (Data) field in DocType 'Mailbox' #: mail/client/doctype/mailbox/mailbox.json msgid "User-visible name for the Mailbox, e.g., \"Inbox\"." @@ -11667,16 +10382,14 @@ msgstr "" #. Label of the username (Data) field in DocType 'User Settings' #. Label of the dns_provider_username (Data) field in DocType 'Mail Settings' -#. Label of the fallback_admin_user (Data) field in DocType 'Mail Cluster' -#. Label of the metrics_prometheus_auth_username (Data) field in DocType 'Mail -#. Cluster' -#. Label of the user (Data) field in DocType 'Mail Cluster Store' +#. Label of the recovery_admin_user (Data) field in DocType 'Mail Cluster' +#. Label of the username (Data) field in DocType 'Mail Cluster Store HTTP Auth' #: frontend/src/components/Modals/AddMemberModal.vue:20 #: frontend/src/pages/SignupView.vue:6 #: mail/client/doctype/user_settings/user_settings.json #: mail/mail/doctype/mail_settings/mail_settings.json #: mail/server/doctype/mail_cluster/mail_cluster.json -#: mail/server/doctype/mail_cluster_store/mail_cluster_store.json +#: mail/server/doctype/mail_cluster_store_http_auth/mail_cluster_store_http_auth.json msgid "Username" msgstr "" @@ -11684,14 +10397,21 @@ msgstr "" msgid "Username already taken." msgstr "" +#. Description of the 'Username' (Data) field in DocType 'Mail Cluster Store +#. HTTP Auth' +#: mail/server/doctype/mail_cluster_store_http_auth/mail_cluster_store_http_auth.json +msgid "Username for HTTP Basic Authentication." +msgstr "" + #. Description of the 'Username' (Data) field in DocType 'Mail Cluster' #: mail/server/doctype/mail_cluster/mail_cluster.json msgid "Username for administrative access to the cluster." msgstr "" -#. Description of the 'Username' (Data) field in DocType 'Mail Cluster Store' +#. Description of the 'Auth Username' (Data) field in DocType 'Mail Cluster +#. Store' #: mail/server/doctype/mail_cluster_store/mail_cluster_store.json -msgid "Username to connect to the database." +msgid "Username to connect to the store." msgstr "" #: frontend/src/pages/dashboard/MembersView.vue:11 @@ -11699,13 +10419,13 @@ msgid "Users" msgstr "" #. Name of a DocType -#: frontend/src/components/Modals/SettingsModal.vue:104 +#: frontend/src/components/Modals/SettingsModal.vue:116 #: frontend/src/components/Settings/VacationResponseSettings.vue:3 #: mail/client/doctype/vacation_response/vacation_response.json msgid "Vacation Response" msgstr "" -#: frontend/src/components/Settings/VacationResponseSettings.vue:120 +#: frontend/src/components/Settings/VacationResponseSettings.vue:113 msgid "Vacation response updated." msgstr "" @@ -11714,7 +10434,7 @@ msgstr "" msgid "Validate Script" msgstr "" -#: mail/client/doctype/sieve_script/sieve_script.js:10 +#: mail/client/doctype/sieve_script/sieve_script.js:30 msgid "Validating Script..." msgstr "" @@ -11752,11 +10472,6 @@ msgstr "" msgid "Verification Key" msgstr "" -#. Label of the jmap_push_timeout_verify (Int) field in DocType 'Mail Cluster' -#: mail/server/doctype/mail_cluster/mail_cluster.json -msgid "Verification Timeout (Milliseconds)" -msgstr "" - #: mail/server/doctype/mail_account_request/mail_account_request.py:166 msgid "Verification email sent successfully." msgstr "" @@ -11775,7 +10490,7 @@ msgstr "" msgid "Verified" msgstr "" -#: mail/server/doctype/dns_record/dns_record.py:108 +#: mail/server/doctype/dns_record/dns_record.py:110 msgid "Verified {0}:{1} record." msgstr "" @@ -11792,6 +10507,12 @@ msgstr "" msgid "Verify Account" msgstr "" +#. Label of the verify_after_write (Check) field in DocType 'Mail Cluster +#. Store' +#: mail/server/doctype/mail_cluster_store/mail_cluster_store.json +msgid "Verify After Write" +msgstr "" + #: frontend/src/components/Modals/AddDomainModal.vue:8 msgid "Verify DNS" msgstr "" @@ -11804,7 +10525,7 @@ msgstr "" msgid "Verify DNS Records" msgstr "" -#: mail/server/doctype/mail_server/mail_server.js:35 +#: mail/server/doctype/mail_server/mail_server.js:36 msgid "Verify SSH Connection" msgstr "" @@ -11820,11 +10541,11 @@ msgstr "" msgid "Verifying DNS Record..." msgstr "" -#: mail/server/doctype/principal/principal.js:77 +#: mail/server/doctype/principal/principal.js:69 msgid "Verifying DNS Records..." msgstr "" -#: mail/server/doctype/mail_server/mail_server.js:90 +#: mail/server/doctype/mail_server/mail_server.js:77 msgid "Verifying SSH Connection..." msgstr "" @@ -11836,7 +10557,7 @@ msgstr "" msgid "View Shortcuts" msgstr "" -#: mail/client/doctype/mail_exchange/mail_exchange.py:920 +#: mail/client/doctype/mail_exchange/mail_exchange.py:922 #: mail/server/doctype/mail_data_exchange/mail_data_exchange.py:316 msgid "View details for this exchange." msgstr "" @@ -11850,21 +10571,21 @@ msgstr "" msgid "View in Bytes" msgstr "" -#: frontend/src/components/MailActions.vue:208 +#: frontend/src/components/MailActions.vue:215 #: frontend/src/pages/MailExchangeView.vue:112 #: frontend/src/pages/dashboard/DomainView.vue:244 msgid "View in Desk" msgstr "" -#: frontend/src/components/Modals/SearchModal.vue:141 +#: frontend/src/components/Modals/SearchModal.vue:144 msgid "View more." msgstr "" -#: frontend/src/components/MailListItem.vue:107 +#: frontend/src/components/MailListItem.vue:111 msgid "View remaining attachments" msgstr "" -#: mail/client/doctype/mail_exchange/mail_exchange.py:921 +#: mail/client/doctype/mail_exchange/mail_exchange.py:923 #: mail/server/doctype/mail_data_exchange/mail_data_exchange.py:317 msgid "View {0}" msgstr "" @@ -11879,12 +10600,6 @@ msgstr "" msgid "Volumes" msgstr "" -#. Option for the 'Logging Level' (Select) field in DocType 'Mail Cluster -#. Trace' -#: mail/server/doctype/mail_cluster_trace/mail_cluster_trace.json -msgid "Warn" -msgstr "" - #. Label of the warn_limit (Int) field in DocType 'Quota' #: mail/client/doctype/quota/quota.json msgid "Warn Limit" @@ -11910,45 +10625,16 @@ msgstr "" msgid "When checked, the email will be saved as a draft instead of being sent." msgstr "" -#. Description of the 'Path' (Data) field in DocType 'Mail Cluster Store' +#. Description of the 'Fail On Timeout' (Check) field in DocType 'Mail Cluster +#. Store' #: mail/server/doctype/mail_cluster_store/mail_cluster_store.json -msgid "Where to store the data in the server's filesystem." -msgstr "" - -#. Description of the 'Default' (Check) field in DocType 'Mail Server ACME -#. Provider' -#: mail/server/doctype/mail_server_acme_provider/mail_server_acme_provider.json -msgid "Whether the certificates generated by this provider should be the default when no SNI is provided." -msgstr "" - -#. Description of the 'Default' (Check) field in DocType 'Mail Server TLS -#. Certificate' -#: mail/server/doctype/mail_server_tls_certificate/mail_server_tls_certificate.json -msgid "Whether this certificate should be the default when no SNI is provided." -msgstr "" - -#. Description of the 'Buffered writes' (Check) field in DocType 'Mail Cluster -#. Trace' -#: mail/server/doctype/mail_cluster_trace/mail_cluster_trace.json -msgid "Whether to buffer log entries before writing to console." -msgstr "" - -#. Description of the 'Lossy mode' (Check) field in DocType 'Mail Cluster -#. Trace' -#: mail/server/doctype/mail_cluster_trace/mail_cluster_trace.json -msgid "Whether to drop log entries if there is backlog." -msgstr "" - -#. Description of the 'Export logs' (Check) field in DocType 'Mail Cluster -#. Trace' -#: mail/server/doctype/mail_cluster_trace/mail_cluster_trace.json -msgid "Whether to export logs to OpenTelemetry." +msgid "Whether to fail the operation if the task does not complete within the polling retries." msgstr "" -#. Description of the 'Export spans' (Check) field in DocType 'Mail Cluster -#. Trace' -#: mail/server/doctype/mail_cluster_trace/mail_cluster_trace.json -msgid "Whether to export spans to OpenTelemetry." +#. Description of the 'Include Source' (Check) field in DocType 'Mail Cluster +#. Store' +#: mail/server/doctype/mail_cluster_store/mail_cluster_store.json +msgid "Whether to index the full source document." msgstr "" #. Description of the 'Read From Replicas' (Check) field in DocType 'Mail @@ -11957,25 +10643,7 @@ msgstr "" msgid "Whether to read from replicas." msgstr "" -#. Description of the 'Use ANSI colors' (Check) field in DocType 'Mail Cluster -#. Trace' -#: mail/server/doctype/mail_cluster_trace/mail_cluster_trace.json -msgid "Whether to use ANSI colors in logs." -msgstr "" - -#. Description of the 'Implicit TLS' (Check) field in DocType 'Mail Server -#. Listener' -#: mail/server/doctype/mail_server_listener/mail_server_listener.json -msgid "Whether to use implicit TLS." -msgstr "" - -#. Description of the 'Multiline entries' (Check) field in DocType 'Mail -#. Cluster Trace' -#: mail/server/doctype/mail_cluster_trace/mail_cluster_trace.json -msgid "Whether to write log entries as a single line or multiline." -msgstr "" - -#: mail/utils/validation.py:388 +#: mail/utils/validation.py:350 msgid "Wildcard characters ({0}) are not allowed in email addresses." msgstr "" @@ -11995,11 +10663,6 @@ msgstr "" msgid "Work signature" msgstr "" -#. Label of the write_buffer_size (Int) field in DocType 'Mail Cluster Store' -#: mail/server/doctype/mail_cluster_store/mail_cluster_store.json -msgid "Write Buffer Size (MB)" -msgstr "" - #: frontend/src/components/Modals/AddSignatureModal.vue:17 #: frontend/src/components/Modals/EditSignatureModal.vue:17 #: frontend/src/components/Modals/SetDefaultSignatureModal.vue:23 @@ -12019,24 +10682,20 @@ msgstr "" msgid "You are not authorized to perform this action." msgstr "" -#: mail/client/doctype/mail_queue/mail_queue.py:339 -msgid "You cannot send email from {0} using user {1}. Please use the email address associated with the user." -msgstr "" - -#: mail/jmap/__init__.py:66 +#: mail/jmap/__init__.py:42 msgid "You do not have permission to access the JMAPConnection for user {0}." msgstr "" -#: mail/utils/validation.py:378 +#: mail/utils/validation.py:340 msgid "You do not have permission to access the mail backend." msgstr "" -#: mail/utils/validation.py:342 +#: mail/utils/validation.py:300 msgid "You do not have permission to access this resource." msgstr "" -#: mail/client/doctype/mail_message/mail_message.py:206 -msgid "You do not have permission to view messages for this user." +#: mail/client/doctype/mail_message/mail_message.py:208 +msgid "You do not have permission to view messages for this account." msgstr "" #: mail/server/doctype/mail_account_request/mail_account_request.py:150 @@ -12047,23 +10706,23 @@ msgstr "" msgid "You have been invited by {0} to join Frappe Mail." msgstr "" -#: frontend/src/pages/MailboxView.vue:273 +#: frontend/src/pages/MailboxView.vue:272 msgid "You have no mails in this folder." msgstr "" -#: mail/utils/validation.py:149 +#: mail/utils/validation.py:105 msgid "You have reached the maximum limit of {0} accounts." msgstr "" -#: mail/utils/validation.py:122 +#: mail/utils/validation.py:78 msgid "You have reached the maximum limit of {0} domains." msgstr "" -#: mail/utils/validation.py:135 +#: mail/utils/validation.py:91 msgid "You have reached the maximum limit of {0} groups." msgstr "" -#: mail/utils/validation.py:163 +#: mail/utils/validation.py:119 msgid "You have reached the maximum limit of {0} lists." msgstr "" @@ -12072,15 +10731,15 @@ msgstr "" msgid "Zone ID" msgstr "" -#: frontend/src/pages/MailboxView.vue:1168 +#: frontend/src/pages/MailboxView.vue:1200 msgid "[No Subject]" msgstr "" -#: frontend/src/components/MailListItem.vue:78 +#: frontend/src/components/MailListItem.vue:82 #: frontend/src/components/MailThread.vue:19 #: frontend/src/components/MailThread.vue:72 -#: frontend/src/components/Modals/SearchModal.vue:111 -#: mail/client/doctype/mail_message/mail_message.py:1248 +#: frontend/src/components/Modals/SearchModal.vue:114 +#: mail/client/doctype/mail_message/mail_message.py:1240 msgid "[No subject]" msgstr "" @@ -12108,40 +10767,50 @@ msgstr "" msgid "attachment" msgstr "" +#. Label of the bootstrap_ndjson (Code) field in DocType 'Mail Server' +#: mail/server/doctype/mail_server/mail_server.json +msgid "bootstrap.ndjson" +msgstr "" + #. Option for the 'Network Mode' (Select) field in DocType 'Server Deployment #. Service' #: mail/server/doctype/server_deployment_service/server_deployment_service.json msgid "bridge" msgstr "" +#. Option for the 'Pool Recycling Method' (Select) field in DocType 'Mail +#. Cluster Store' +#: mail/server/doctype/mail_cluster_store/mail_cluster_store.json +msgid "clean" +msgstr "" + #: frontend/src/components/Modals/SetSieveScriptStateModal.vue:71 msgid "deactivate" msgstr "" -#: mail/client/doctype/address_book/address_book.py:151 -#: mail/client/doctype/address_book/address_book.py:202 -#: mail/client/doctype/calendar/calendar.py:162 -#: mail/client/doctype/calendar/calendar.py:220 -#: mail/client/doctype/calendar_event/calendar_event.py:334 -#: mail/client/doctype/calendar_event/calendar_event.py:461 -#: mail/client/doctype/calendar_event/calendar_event.py:486 -#: mail/client/doctype/calendar_event/calendar_event.py:525 -#: mail/client/doctype/contact_card/contact_card.py:247 -#: mail/client/doctype/contact_card/contact_card.py:357 -#: mail/client/doctype/contact_card/contact_card.py:391 -#: mail/client/doctype/identity/identity.py:166 -#: mail/client/doctype/identity/identity.py:234 -#: mail/client/doctype/identity/identity.py:285 -#: mail/client/doctype/mailbox/mailbox.py:139 -#: mail/client/doctype/mailbox/mailbox.py:193 -#: mail/client/doctype/mailbox/mailbox.py:345 -#: mail/client/doctype/participant_identity/participant_identity.py:130 -#: mail/client/doctype/participant_identity/participant_identity.py:172 +#: mail/client/doctype/address_book/address_book.py:152 +#: mail/client/doctype/address_book/address_book.py:205 +#: mail/client/doctype/calendar/calendar.py:163 +#: mail/client/doctype/calendar/calendar.py:221 +#: mail/client/doctype/calendar_event/calendar_event.py:338 +#: mail/client/doctype/calendar_event/calendar_event.py:465 +#: mail/client/doctype/calendar_event/calendar_event.py:490 +#: mail/client/doctype/calendar_event/calendar_event.py:529 +#: mail/client/doctype/contact_card/contact_card.py:252 +#: mail/client/doctype/contact_card/contact_card.py:366 +#: mail/client/doctype/contact_card/contact_card.py:400 +#: mail/client/doctype/identity/identity.py:177 +#: mail/client/doctype/identity/identity.py:228 +#: mail/client/doctype/mailbox/mailbox.py:140 +#: mail/client/doctype/mailbox/mailbox.py:194 +#: mail/client/doctype/mailbox/mailbox.py:348 +#: mail/client/doctype/participant_identity/participant_identity.py:131 +#: mail/client/doctype/participant_identity/participant_identity.py:173 #: mail/client/doctype/push_subscription/push_subscription.py:166 #: mail/client/doctype/push_subscription/push_subscription.py:206 #: mail/client/doctype/push_subscription/push_subscription.py:223 -#: mail/client/doctype/sieve_script/sieve_script.py:123 -#: mail/client/doctype/sieve_script/sieve_script.py:226 +#: mail/client/doctype/sieve_script/sieve_script.py:126 +#: mail/client/doctype/sieve_script/sieve_script.py:229 msgid "description" msgstr "" @@ -12154,7 +10823,7 @@ msgstr "" msgid "drafts" msgstr "" -#: mail/client/doctype/mail_exchange/mail_exchange.py:122 +#: mail/client/doctype/mail_exchange/mail_exchange.py:121 msgid "emails.json not found in the import directory." msgstr "" @@ -12171,11 +10840,10 @@ msgstr "" msgid "enabled" msgstr "" -#. Option for the 'Transport' (Select) field in DocType 'Mail Cluster' -#. Option for the 'Transport' (Select) field in DocType 'Mail Cluster Trace' -#: mail/server/doctype/mail_cluster/mail_cluster.json -#: mail/server/doctype/mail_cluster_trace/mail_cluster_trace.json -msgid "gRPC" +#. Option for the 'Pool Recycling Method' (Select) field in DocType 'Mail +#. Cluster Store' +#: mail/server/doctype/mail_cluster_store/mail_cluster_store.json +msgid "fast" msgstr "" #. Option for the 'Network Mode' (Select) field in DocType 'Server Deployment @@ -12211,19 +10879,23 @@ msgstr "" msgid "junk" msgstr "" -#: mail/client/doctype/mail_exchange/mail_exchange.py:133 +#: frontend/src/components/MailThread.vue:102 +msgid "mail" +msgstr "" + +#: mail/client/doctype/mail_exchange/mail_exchange.py:132 msgid "mailboxIds are required in Metadata for EML format." msgstr "" -#: mail/client/doctype/mail_exchange/mail_exchange.py:578 +#: mail/client/doctype/mail_exchange/mail_exchange.py:580 msgid "mailboxIds are required in Metadata for EML, MBOX, and Maildir formats." msgstr "" -#: mail/client/doctype/mail_exchange/mail_exchange.py:196 +#: mail/client/doctype/mail_exchange/mail_exchange.py:195 msgid "mailboxIds are required in Metadata for MBOX format." msgstr "" -#: mail/client/doctype/mail_exchange/mail_exchange.py:261 +#: mail/client/doctype/mail_exchange/mail_exchange.py:260 msgid "mailboxIds are required in Metadata for Maildir format." msgstr "" @@ -12241,6 +10913,10 @@ msgstr "" msgid "maildir-nested" msgstr "" +#: frontend/src/components/MailThread.vue:102 +msgid "mails" +msgstr "" + #. Option for the 'Format' (Select) field in DocType 'Mail Exchange' #. Option for the 'Format' (Select) field in DocType 'Mail Data Exchange' #: mail/client/doctype/mail_exchange/mail_exchange.json @@ -12248,11 +10924,6 @@ msgstr "" msgid "mbox" msgstr "" -#. Option for the 'Type' (Select) field in DocType 'Mail Cluster Store' -#: mail/server/doctype/mail_cluster_store/mail_cluster_store.json -msgid "mySQL" -msgstr "" - #. Option for the 'Restart' (Select) field in DocType 'Server Deployment #. Service' #: mail/server/doctype/server_deployment_service/server_deployment_service.json @@ -12265,44 +10936,43 @@ msgstr "" msgid "none" msgstr "" -#: mail/client/doctype/address_book/address_book.py:149 -#: mail/client/doctype/calendar/calendar.py:160 -#: mail/client/doctype/calendar_event/calendar_event.py:332 -#: mail/client/doctype/contact_card/contact_card.py:245 -#: mail/client/doctype/identity/identity.py:164 -#: mail/client/doctype/identity/identity.py:232 -#: mail/client/doctype/mailbox/mailbox.py:137 -#: mail/client/doctype/participant_identity/participant_identity.py:128 +#: mail/client/doctype/address_book/address_book.py:150 +#: mail/client/doctype/calendar/calendar.py:161 +#: mail/client/doctype/calendar_event/calendar_event.py:336 +#: mail/client/doctype/contact_card/contact_card.py:250 +#: mail/client/doctype/identity/identity.py:175 +#: mail/client/doctype/mailbox/mailbox.py:138 +#: mail/client/doctype/participant_identity/participant_identity.py:129 #: mail/client/doctype/push_subscription/push_subscription.py:164 -#: mail/client/doctype/sieve_script/sieve_script.py:121 +#: mail/client/doctype/sieve_script/sieve_script.py:124 msgid "notCreateddescription" msgstr "" -#: mail/client/doctype/calendar_event/calendar_event.py:508 +#: mail/client/doctype/calendar_event/calendar_event.py:512 msgid "notDestroyeddescription" msgstr "" -#: mail/client/doctype/calendar_event/calendar_event.py:553 +#: mail/client/doctype/calendar_event/calendar_event.py:557 msgid "notFounddescription" msgstr "" -#: mail/client/doctype/calendar_event/calendar_event.py:551 +#: mail/client/doctype/calendar_event/calendar_event.py:555 msgid "notParsabledescription" msgstr "" -#: mail/client/doctype/address_book/address_book.py:200 -#: mail/client/doctype/calendar/calendar.py:218 -#: mail/client/doctype/calendar_event/calendar_event.py:459 -#: mail/client/doctype/calendar_event/calendar_event.py:484 -#: mail/client/doctype/calendar_event/calendar_event.py:523 -#: mail/client/doctype/contact_card/contact_card.py:355 -#: mail/client/doctype/contact_card/contact_card.py:389 -#: mail/client/doctype/identity/identity.py:283 -#: mail/client/doctype/mailbox/mailbox.py:191 -#: mail/client/doctype/participant_identity/participant_identity.py:170 +#: mail/client/doctype/address_book/address_book.py:203 +#: mail/client/doctype/calendar/calendar.py:219 +#: mail/client/doctype/calendar_event/calendar_event.py:463 +#: mail/client/doctype/calendar_event/calendar_event.py:488 +#: mail/client/doctype/calendar_event/calendar_event.py:527 +#: mail/client/doctype/contact_card/contact_card.py:364 +#: mail/client/doctype/contact_card/contact_card.py:398 +#: mail/client/doctype/identity/identity.py:226 +#: mail/client/doctype/mailbox/mailbox.py:192 +#: mail/client/doctype/participant_identity/participant_identity.py:171 #: mail/client/doctype/push_subscription/push_subscription.py:204 #: mail/client/doctype/push_subscription/push_subscription.py:221 -#: mail/client/doctype/sieve_script/sieve_script.py:224 +#: mail/client/doctype/sieve_script/sieve_script.py:227 msgid "notUpdateddescription" msgstr "" @@ -12320,15 +10990,23 @@ msgstr "" msgid "or" msgstr "" -#: mail/server/report/dmarc_report_viewer/dmarc_report_viewer.py:29 -msgid "report_end" +#. Option for the 'Protocol Version' (Select) field in DocType 'Mail Cluster +#. Store' +#: mail/server/doctype/mail_cluster_store/mail_cluster_store.json +msgid "resp2" +msgstr "" + +#. Option for the 'Protocol Version' (Select) field in DocType 'Mail Cluster +#. Store' +#: mail/server/doctype/mail_cluster_store/mail_cluster_store.json +msgid "resp3" msgstr "" -#: frontend/src/pages/MailboxView.vue:1156 +#: frontend/src/pages/MailboxView.vue:1188 msgid "result" msgstr "" -#: frontend/src/pages/MailboxView.vue:1156 +#: frontend/src/pages/MailboxView.vue:1188 msgid "results" msgstr "" @@ -12352,13 +11030,13 @@ msgstr "" msgid "then" msgstr "" -#: frontend/src/pages/MailboxView.vue:926 -#: frontend/src/pages/MailboxView.vue:1157 +#: frontend/src/pages/MailboxView.vue:958 +#: frontend/src/pages/MailboxView.vue:1189 msgid "thread" msgstr "" -#: frontend/src/pages/MailboxView.vue:926 -#: frontend/src/pages/MailboxView.vue:1157 +#: frontend/src/pages/MailboxView.vue:958 +#: frontend/src/pages/MailboxView.vue:1189 msgid "threads" msgstr "" @@ -12367,7 +11045,7 @@ msgstr "" msgid "trash" msgstr "" -#: mail/client/doctype/mail_queue/mail_queue.py:476 +#: mail/client/doctype/mail_queue/mail_queue.py:479 msgid "type is required for blob attachments." msgstr "" @@ -12377,8 +11055,8 @@ msgstr "" msgid "unless-stopped" msgstr "" -#: frontend/src/pages/MailboxView.vue:1013 -#: frontend/src/pages/MailboxView.vue:1014 +#: frontend/src/pages/MailboxView.vue:1045 +#: frontend/src/pages/MailboxView.vue:1046 msgid "unread" msgstr "" @@ -12386,13 +11064,19 @@ msgstr "" msgid "user" msgstr "" -#: mail/api/mail.py:429 +#. Option for the 'Pool Recycling Method' (Select) field in DocType 'Mail +#. Cluster Store' +#: mail/server/doctype/mail_cluster_store/mail_cluster_store.json +msgid "verified" +msgstr "" + +#: mail/api/mail.py:424 msgid "{0} (Delivered after {1} seconds)" msgstr "" -#: frontend/src/components/Modals/AddAddressBookContactsModal.vue:74 -#: frontend/src/pages/AddressBookView.vue:165 -#: frontend/src/pages/ContactsView.vue:84 +#: frontend/src/components/Modals/AddAddressBookContactsModal.vue:81 +#: frontend/src/pages/AddressBookView.vue:168 +#: frontend/src/pages/ContactsView.vue:91 msgid "{0} + {1} more" msgstr "" @@ -12412,19 +11096,15 @@ msgstr "" msgid "{0} does not exist." msgstr "" -#: frontend/src/pages/MailboxView.vue:960 +#: frontend/src/pages/MailboxView.vue:992 msgid "{0} emptied." msgstr "" -#: mail/server/doctype/mail_cluster/mail_cluster.py:176 -msgid "{0} has an invalid store type '{1}'. Allowed types are: {2}." -msgstr "" - -#: mail/utils/validation.py:175 +#: mail/utils/validation.py:131 msgid "{0} is not a valid Cron expression." msgstr "" -#: frontend/src/pages/MailboxView.vue:1176 +#: frontend/src/pages/MailboxView.vue:1208 msgid "{0} items selected" msgstr "" @@ -12432,15 +11112,23 @@ msgstr "" msgid "{0} job has been created." msgstr "" -#: mail/api/mail.py:444 +#: frontend/src/components/MailActions.vue:301 +msgid "{0} marked as unread." +msgstr "" + +#: mail/server/doctype/mail_cluster/mail_cluster.py:142 +msgid "{0} type must be one of {1}." +msgstr "" + +#: mail/api/mail.py:439 msgid "{0} with IP {1}" msgstr "" -#: mail/client/doctype/sieve_script/sieve_script.py:187 +#: mail/client/doctype/sieve_script/sieve_script.py:190 msgid "{0}: {1}" msgstr "" -#: frontend/src/components/MailListItem.vue:84 +#: frontend/src/components/MailListItem.vue:88 msgid "— No message body —" msgstr "" diff --git a/mail/server/doctype/mail_server/mail_server.py b/mail/server/doctype/mail_server/mail_server.py index df16b5b54..83422c95f 100644 --- a/mail/server/doctype/mail_server/mail_server.py +++ b/mail/server/doctype/mail_server/mail_server.py @@ -224,9 +224,3 @@ def _db_set( """Updates the document with the given key-value pairs.""" self.db_set(kwargs, update_modified=update_modified, notify=notify, commit=commit) - - -def on_doctype_update() -> None: - frappe.db.add_unique( - "Mail Server", ["cluster", "cluster_node_id"], constraint_name="unique_cluster_node_id" - ) From c7097fb5a0c99751ffc1524df9b7eba4ff5b1aa4 Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Wed, 13 May 2026 11:23:27 +0530 Subject: [PATCH 22/55] fix: auto-create User Settings after User insert --- mail/events.py | 9 +++++++++ mail/hooks.py | 3 +++ .../doctype/mail_account_request/mail_account_request.py | 5 ++--- 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/mail/events.py b/mail/events.py index fa1d1591e..79164dddc 100644 --- a/mail/events.py +++ b/mail/events.py @@ -15,6 +15,15 @@ from mail.utils.validation import validate_mail_config +def create_user_settings(doc: Document, method: str | None = None) -> None: + """Create User Settings for the new user if not already present.""" + + if not frappe.db.exists("User Settings", {"user": doc.name}): + settings = frappe.new_doc("User Settings") + settings.user = doc.name + settings.insert(ignore_permissions=True, ignore_mandatory=True) + + def update_account_password(doc: Document, method: str | None = None) -> None: """Update the password in the Principal when the User's password is changed, but ONLY if the hash is different from the backend stored hash.""" diff --git a/mail/hooks.py b/mail/hooks.py index 5ce812272..ab884fdc5 100644 --- a/mail/hooks.py +++ b/mail/hooks.py @@ -217,6 +217,9 @@ doc_events = { "User": { + "after_insert": [ + "mail.events.create_user_settings", + ], "on_update": [ "mail.events.update_account_password", ], diff --git a/mail/server/doctype/mail_account_request/mail_account_request.py b/mail/server/doctype/mail_account_request/mail_account_request.py index 4fde1e6d0..3e49cfc8c 100644 --- a/mail/server/doctype/mail_account_request/mail_account_request.py +++ b/mail/server/doctype/mail_account_request/mail_account_request.py @@ -219,9 +219,8 @@ def create_account(self, first_name: str, last_name: str, password: str) -> None principal.insert(ignore_permissions=True) - # Create User Settings - user_settings = frappe.new_doc("User Settings") - user_settings.user = user + # Update User Settings + user_settings = frappe.get_doc("User Settings", {"user": user}) user_settings.username = self.account user_settings.app_password = app_password user_settings.backup_email = self.backup_email From 22d692546dc687653786f6ddf6149cd534844a96 Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Wed, 13 May 2026 11:56:09 +0530 Subject: [PATCH 23/55] refactor: move spamd settings to mail config --- frontend/src/types/doctypes.ts | 10 -- .../doctype/user_settings/user_settings.py | 5 +- .../doctype/mail_settings/mail_settings.js | 13 --- .../doctype/mail_settings/mail_settings.json | 100 ++++-------------- .../doctype/spam_check_log/spam_check_log.py | 14 +-- 5 files changed, 27 insertions(+), 115 deletions(-) diff --git a/frontend/src/types/doctypes.ts b/frontend/src/types/doctypes.ts index 031f55126..ec2cefb02 100644 --- a/frontend/src/types/doctypes.ts +++ b/frontend/src/types/doctypes.ts @@ -129,16 +129,6 @@ export interface MailSettings extends DocType { jmap_push_private_key?: string; /** JMAP Push Auth Secret: Password */ jmap_push_auth?: string; - /** Host: Data */ - spamd_host?: string; - /** Port: Int */ - spamd_port?: number; - /** Scanning Mode: Select */ - spamd_scanning_mode?: 'Exclude Attachments' | 'Include Attachments' | 'Hybrid Approach'; - /** Hybrid Scanning Threshold: Float */ - spamd_hybrid_scanning_threshold?: number; - /** Enable Spam Detection: Check */ - enable_spamd: 0 | 1; /** Allow Signup: Check */ allow_signup: 0 | 1; /** Signup Domains: Table MultiSelect (Personal Signup Domain) */ diff --git a/mail/client/doctype/user_settings/user_settings.py b/mail/client/doctype/user_settings/user_settings.py index 9a8e22d37..fa5821839 100644 --- a/mail/client/doctype/user_settings/user_settings.py +++ b/mail/client/doctype/user_settings/user_settings.py @@ -58,10 +58,7 @@ def validate(self) -> None: def validate_jmap_settings(self) -> None: """Validate the JMAP settings by connecting to the JMAP server and verifying the default outgoing email.""" - if self.flags.skip_jmap_validation: - return - - if not self.username: + if not self.username or self.flags.skip_jmap_validation: return server_url = self.server_url or get_mail_config("server_url") diff --git a/mail/mail/doctype/mail_settings/mail_settings.js b/mail/mail/doctype/mail_settings/mail_settings.js index d99d757fc..699eb6598 100644 --- a/mail/mail/doctype/mail_settings/mail_settings.js +++ b/mail/mail/doctype/mail_settings/mail_settings.js @@ -7,7 +7,6 @@ frappe.ui.form.on('Mail Settings', { }, refresh(frm) { - frm.trigger('add_comments') frm.trigger('add_actions') }, @@ -32,18 +31,6 @@ frappe.ui.form.on('Mail Settings', { } }, - add_comments(frm) { - if (frm.doc.root_domain_name && (!frm.doc.dns_provider || !frm.doc.dns_provider_token)) { - const bold_root_domain_name = `${frm.doc.root_domain_name}` - const dns_record_list_link = `${__('DNS Records')}` - const msg = __( - 'DNS provider or token not configured. Please manually add the {0} to the DNS provider for the domain {1}.', - [dns_record_list_link, bold_root_domain_name], - ) - frm.dashboard.add_comment(msg, 'yellow', true) - } - }, - add_actions(frm) { frm.add_custom_button( __('Generate JMAP Push Keys'), diff --git a/mail/mail/doctype/mail_settings/mail_settings.json b/mail/mail/doctype/mail_settings/mail_settings.json index 96c312178..75775429d 100644 --- a/mail/mail/doctype/mail_settings/mail_settings.json +++ b/mail/mail/doctype/mail_settings/mail_settings.json @@ -4,6 +4,11 @@ "doctype": "DocType", "engine": "InnoDB", "field_order": [ + "signup_tab", + "allow_signup", + "column_break_daqm", + "signup_domains", + "dns_tab", "section_break_hrww", "root_domain_name", "column_break_4ccc", @@ -19,24 +24,12 @@ "dns_provider_client_ip", "dns_provider_token", "dns_provider_private_zone", + "jmap_push_tab", "section_break_qvpk", "jmap_push_p256dh", "jmap_push_private_key", "column_break_wpvf", - "jmap_push_auth", - "signup_tab", - "allow_signup", - "column_break_daqm", - "signup_domains", - "spamassassin_tab", - "section_break_hgqa", - "enable_spamd", - "section_break_r530", - "spamd_host", - "spamd_port", - "column_break_c43x", - "spamd_scanning_mode", - "spamd_hybrid_scanning_threshold" + "jmap_push_auth" ], "fields": [ { @@ -73,8 +66,7 @@ }, { "fieldname": "section_break_qvpk", - "fieldtype": "Section Break", - "label": "JMAP Push" + "fieldtype": "Section Break" }, { "description": "P-256 ECDH public key for JMAP PushSubscription encryption.", @@ -101,70 +93,6 @@ "label": "JMAP Push Auth Secret", "read_only": 1 }, - { - "fieldname": "spamassassin_tab", - "fieldtype": "Tab Break", - "label": "SpamAssassin", - "mandatory_depends_on": "eval: doc.enable_spam_detection", - "read_only_depends_on": "eval: !doc.enable_spam_detection" - }, - { - "fieldname": "section_break_hgqa", - "fieldtype": "Section Break" - }, - { - "description": "Hostname or IP of the SpamAssassin server.", - "fieldname": "spamd_host", - "fieldtype": "Data", - "label": "Host", - "mandatory_depends_on": "eval: doc.enable_spamd", - "placeholder": "spamd.frappemail.com", - "read_only_depends_on": "eval: !doc.enable_spamd" - }, - { - "default": "783", - "description": "Port for connecting to the SpamAssassin server.", - "fieldname": "spamd_port", - "fieldtype": "Int", - "label": "Port", - "mandatory_depends_on": "eval: doc.enable_spamd", - "non_negative": 1, - "read_only_depends_on": "eval: !doc.enable_spamd" - }, - { - "fieldname": "section_break_r530", - "fieldtype": "Section Break" - }, - { - "default": "Hybrid Approach", - "fieldname": "spamd_scanning_mode", - "fieldtype": "Select", - "label": "Scanning Mode", - "mandatory_depends_on": "eval: doc.enable_spamd", - "options": "Exclude Attachments\nInclude Attachments\nHybrid Approach", - "read_only_depends_on": "eval: !doc.enable_spamd" - }, - { - "default": "2", - "description": "Threshold for when to switch from scanning only the email body to scanning attachments selectively.", - "fieldname": "spamd_hybrid_scanning_threshold", - "fieldtype": "Float", - "label": "Hybrid Scanning Threshold", - "mandatory_depends_on": "eval: doc.spamd_scanning_mode == \"Hybrid Approach\"", - "precision": "1", - "read_only_depends_on": "eval: !doc.enable_spamd || doc.spamd_scanning_mode != \"Hybrid Approach\"" - }, - { - "fieldname": "column_break_c43x", - "fieldtype": "Column Break" - }, - { - "default": "0", - "description": "Check to enable spam detection.", - "fieldname": "enable_spamd", - "fieldtype": "Check", - "label": "Enable Spam Detection" - }, { "default": "0", "description": "Allows users to sign up and select a preferred username.", @@ -262,12 +190,22 @@ "fieldname": "signup_tab", "fieldtype": "Tab Break", "label": "Signup" + }, + { + "fieldname": "dns_tab", + "fieldtype": "Tab Break", + "label": "DNS" + }, + { + "fieldname": "jmap_push_tab", + "fieldtype": "Tab Break", + "label": "JMAP Push" } ], "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2026-04-27 09:25:58.236007", + "modified": "2026-05-13 11:55:08.939903", "modified_by": "Administrator", "module": "Mail", "name": "Mail Settings", diff --git a/mail/server/doctype/spam_check_log/spam_check_log.py b/mail/server/doctype/spam_check_log/spam_check_log.py index db55f3a99..6de65c13c 100644 --- a/mail/server/doctype/spam_check_log/spam_check_log.py +++ b/mail/server/doctype/spam_check_log/spam_check_log.py @@ -44,15 +44,15 @@ def set_source_host(self) -> None: def scan_message(self) -> None: """Scans the message for spam""" - mail_settings = frappe.get_cached_doc("Mail Settings") + config = get_mail_config() + spamd_host = config.get("spamd_host") + spamd_port = config.get("spamd_port") - if not mail_settings.enable_spamd: - frappe.throw(_("Spam Detection is disabled")) + if not all([spamd_host, spamd_port]): + frappe.throw(_("Configure SpamAssassin (spamd) host and port to enable spam detection.")) - spamd_host = mail_settings.spamd_host - spamd_port = mail_settings.spamd_port - scanning_mode = mail_settings.spamd_scanning_mode - hybrid_scanning_threshold = mail_settings.spamd_hybrid_scanning_threshold + scanning_mode = config.get("spamd_scanning_mode", "Hybrid Approach") + hybrid_scanning_threshold = config.get("spamd_hybrid_scanning_threshold", 2.0) response = None self.started_at = now() From 93aed179eb23645c4dbd9b05adfe06ad100dd7f8 Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Wed, 13 May 2026 14:05:45 +0530 Subject: [PATCH 24/55] chore: prefer Mail Settings over global conf --- mail/api/inbound.py | 4 +- mail/api/mail.py | 4 +- mail/api/outbound.py | 4 +- mail/backend.py | 4 +- .../doctype/mail_exchange/mail_exchange.py | 12 +- .../doctype/mail_message/mail_message.py | 6 +- mail/client/doctype/mail_queue/mail_queue.py | 8 +- .../doctype/user_settings/user_settings.py | 4 +- mail/jmap/__init__.py | 4 +- .../doctype/mail_settings/mail_settings.json | 420 +++++++++++++++++- mail/server/doctype/dns_record/dns_record.py | 4 +- .../mail_account_request.py | 2 +- .../mail_data_exchange/mail_data_exchange.py | 10 +- mail/server/doctype/principal/principal.py | 14 +- .../server_ansible_play.py | 6 +- .../server_deployment/server_deployment.py | 6 +- mail/server/doctype/server_job/server_job.py | 6 +- .../doctype/spam_check_log/spam_check_log.py | 6 +- mail/stalwart/cli.py | 4 +- mail/storage/__init__.py | 6 +- mail/utils/__init__.py | 132 +++--- mail/utils/lock.py | 6 +- mail/utils/validation.py | 12 +- 23 files changed, 552 insertions(+), 132 deletions(-) diff --git a/mail/api/inbound.py b/mail/api/inbound.py index 29320c722..74e91de9e 100644 --- a/mail/api/inbound.py +++ b/mail/api/inbound.py @@ -10,7 +10,7 @@ from mail.client.doctype.mail_message.mail_message import fetch_blobs, fetch_messages from mail.client.doctype.mail_sync_history.mail_sync_history import get_mail_sync_history from mail.jmap import get_mailbox_id_by_role -from mail.utils import get_mail_config +from mail.utils import get_config from mail.utils.dt import convert_to_utc from mail.utils.rate_limiter import dynamic_rate_limit from mail.utils.user import get_user_personal_account @@ -82,7 +82,7 @@ def pull_raw( def validate_max_sync_limit(limit: int) -> None: """Validates if the limit is within the maximum limit.""" - max_sync = cint(get_mail_config("max_email_sync")) + max_sync = cint(get_config("max_email_sync")) if limit > max_sync: frappe.throw(_("Cannot fetch more than {0} emails at a time.").format(max_sync)) diff --git a/mail/api/mail.py b/mail/api/mail.py index 7b9a5ec51..767a96f90 100644 --- a/mail/api/mail.py +++ b/mail/api/mail.py @@ -27,7 +27,7 @@ from mail.client.doctype.mailbox.mailbox import add_mailbox, delete_mailboxes from mail.client.doctype.mailbox_settings.mailbox_settings import set_mailbox_settings from mail.jmap import get_email_service, get_mailbox_id_by_role -from mail.utils import convert_html_to_text, get_mail_config +from mail.utils import convert_html_to_text, get_config from mail.utils.user import get_account_emails, is_jmap_configured from mail.utils.validation import has_permission_for_user @@ -597,7 +597,7 @@ def get_avatar(email: str, size: int = 128, strict: bool = False) -> None: if not avatar: # 2. Try Gravatar - default = get_mail_config("gravatar_default_avatar") + default = get_config("default_gravatar") try: res = requests.get( f"https://secure.gravatar.com/avatar/{email_hash}", diff --git a/mail/api/outbound.py b/mail/api/outbound.py index 481003691..1fbc8e73d 100644 --- a/mail/api/outbound.py +++ b/mail/api/outbound.py @@ -10,7 +10,7 @@ from mail.api.auth import validate_user from mail.client.doctype.mail_queue.mail_queue import MailQueue -from mail.utils import get_mail_config, get_messages_directory +from mail.utils import get_config, get_messages_directory from mail.utils.rate_limiter import dynamic_rate_limit from mail.utils.user import get_user_personal_account @@ -223,7 +223,7 @@ def _normalize_recipients( def _get_max_message_payload_size() -> int: """Returns the maximum message payload size from configuration.""" - return cint(get_mail_config("max_message_payload_size")) + return cint(get_config("max_message_payload_size_mb")) * 1024 * 1024 def _enqueue_mail(from_: str, to: str | list[str], raw_message: str, is_newsletter: bool = False) -> str: diff --git a/mail/backend.py b/mail/backend.py index 0ab0069ce..db1e15826 100644 --- a/mail/backend.py +++ b/mail/backend.py @@ -7,7 +7,7 @@ from frappe import _ from mail.jmap.connection import raise_for_status -from mail.utils import get_mail_config +from mail.utils import get_config from mail.utils.validation import validate_mail_config @@ -108,7 +108,7 @@ def get_mail_backend_api() -> MailBackendAPI: """Returns an authenticated BackendAPI instance.""" validate_mail_config() - config = get_mail_config() + config = get_config() return MailBackendAPI( config["server_url"], diff --git a/mail/client/doctype/mail_exchange/mail_exchange.py b/mail/client/doctype/mail_exchange/mail_exchange.py index deddb1748..add26bb78 100644 --- a/mail/client/doctype/mail_exchange/mail_exchange.py +++ b/mail/client/doctype/mail_exchange/mail_exchange.py @@ -39,7 +39,7 @@ from mail.utils import ( compress_directory, extract_compressed_file, - get_mail_config, + get_config, get_mail_export_directory, get_mail_import_directory, get_mbox_files, @@ -481,13 +481,13 @@ class MailExchange(Document): def max_import(self) -> int: """Returns the maximum number of emails allowed for import.""" - return cint(get_mail_config("exchange_max_import")) + return cint(get_config("exchange_max_import")) @property def max_export(self) -> int: """Returns the maximum number of emails allowed for export.""" - return cint(get_mail_config("exchange_max_export")) + return cint(get_config("exchange_max_export")) @property def export_filter_dict(self) -> dict: @@ -616,7 +616,7 @@ def process(self) -> None: self.name, "_import", queue="long", - timeout=cint(get_mail_config("exchange_import_timeout")), + timeout=cint(get_config("exchange_import_timeout")), job_id=job_id, deduplicate=True, enqueue_after_commit=True, @@ -628,7 +628,7 @@ def process(self) -> None: self.name, "_export", queue="long", - timeout=cint(get_mail_config("exchange_export_timeout")), + timeout=cint(get_config("exchange_export_timeout")), job_id=job_id, deduplicate=True, enqueue_after_commit=True, @@ -826,7 +826,7 @@ def _export_batches( if self.export_format == "jmap": ExportWriter.write_meta(emails, out_dir) - batch_size = cint(get_mail_config("exchange_export_batch_size")) + batch_size = cint(get_config("exchange_export_batch_size")) for batch in create_batch(emails, batch_size): blobs = [(e["blobId"], None) for e in batch if e.get("blobId")] data = service.download_blobs_concurrently(blobs) diff --git a/mail/client/doctype/mail_message/mail_message.py b/mail/client/doctype/mail_message/mail_message.py index 235ff0dc6..cd6daeedd 100644 --- a/mail/client/doctype/mail_message/mail_message.py +++ b/mail/client/doctype/mail_message/mail_message.py @@ -32,7 +32,7 @@ from mail.storage.data_store import Entity from mail.utils import ( enqueue_job, - get_mail_config, + get_config, get_push_logger, parse_filters, user_context, @@ -1222,7 +1222,7 @@ def fetch_changes(account: str, email_state: str | None = None, ctx: dict | None logger.debug({**ctx, "notify_candidates_count": len(notify_candidates)}) - max_push_notifications = cint(get_mail_config("max_push_notifications")) + max_push_notifications = cint(get_config("max_push_notifications")) recent_messages = notify_candidates[:max_push_notifications] logger.debug({**ctx, "recent_notify_candidates_count": len(recent_messages)}) @@ -1305,7 +1305,7 @@ def enqueue_fetch_changes(account: str, email_state: str | None = None, ctx: dic logger.info({**ctx, "event": "enqueueing-fetch-changes"}) lockname = f"fetch_changes:{account}" - fetch_lock_timeout = cint(get_mail_config("fetch_lock_timeout")) + fetch_lock_timeout = cint(get_config("fetch_lock_timeout")) identifier = acquire_lock(lockname, acquire_timeout=0, lock_timeout=fetch_lock_timeout) if not identifier: diff --git a/mail/client/doctype/mail_queue/mail_queue.py b/mail/client/doctype/mail_queue/mail_queue.py index a6502ea18..aa725555e 100644 --- a/mail/client/doctype/mail_queue/mail_queue.py +++ b/mail/client/doctype/mail_queue/mail_queue.py @@ -33,7 +33,7 @@ from mail.jmap.models import EmailAddress, EmailAttachment, EmailCreateModel, EmailHeader, EmailRecipient from mail.jmap.services.mail.email import EmailService from mail.jmap.services.mail.mailbox import MailboxService -from mail.utils import get_mail_config +from mail.utils import get_config from mail.utils.dt import parsedate_to_datetime from mail.utils.user import is_administrator, is_local_user from mail.utils.validation import has_permission_for_user @@ -816,8 +816,8 @@ def process_pending_emails(mails: list[str]) -> None: def enqueue_process_pending_emails(batch_size: int | None = None, max_batch_size: int | None = None) -> None: """Enqueue process pending emails.""" - batch_size = batch_size or cint(get_mail_config("process_pending_emails_batch_size")) - max_batch_size = max_batch_size or cint(get_mail_config("process_pending_emails_max_batch_size")) + batch_size = batch_size or cint(get_config("process_pending_emails_batch_size")) + max_batch_size = max_batch_size or cint(get_config("process_pending_emails_max_batch_size")) if batch_size > max_batch_size: batch_size = max_batch_size @@ -862,7 +862,7 @@ def enqueue_process_pending_emails(batch_size: int | None = None, max_batch_size frappe.enqueue( process_pending_emails, queue="long", - timeout=cint(get_mail_config("process_pending_emails_timeout")), + timeout=cint(get_config("process_pending_emails_timeout")), job_name=f"process_pending_emails_{i}_{len(batch)}", enqueue_after_commit=False, mails=batch, diff --git a/mail/client/doctype/user_settings/user_settings.py b/mail/client/doctype/user_settings/user_settings.py index fa5821839..8b461d103 100644 --- a/mail/client/doctype/user_settings/user_settings.py +++ b/mail/client/doctype/user_settings/user_settings.py @@ -13,7 +13,7 @@ from mail.jmap import get_jmap_session_manager from mail.jmap.connection import JMAPConnection, JMAPConnectionInfo from mail.jmap.services.mail.identity import IdentityService -from mail.utils import get_mail_config +from mail.utils import get_config from mail.utils.dt import timestamp_to_datetime from mail.utils.user import is_local_user, is_system_manager @@ -61,7 +61,7 @@ def validate_jmap_settings(self) -> None: if not self.username or self.flags.skip_jmap_validation: return - server_url = self.server_url or get_mail_config("server_url") + server_url = self.server_url or get_config("server_url") if not server_url or not self.get_password("app_password"): frappe.throw(_("Server URL and App Password are required to validate JMAP settings.")) diff --git a/mail/jmap/__init__.py b/mail/jmap/__init__.py index 3940e609e..b35b64b2a 100644 --- a/mail/jmap/__init__.py +++ b/mail/jmap/__init__.py @@ -26,7 +26,7 @@ from mail.jmap.services.websocket.websocket import WebSocketService from mail.storage import get_data_store from mail.storage.data_store import Entity -from mail.utils import get_mail_config +from mail.utils import get_config from mail.utils.validation import has_permission_for_user @@ -54,7 +54,7 @@ def get_jmap_connection( user_settings = frappe.get_cached_doc("User Settings", settings) - server_url = user_settings.server_url or get_mail_config("server_url") + server_url = user_settings.server_url or get_config("server_url") if not server_url: frappe.throw( _("Server URL must be set in either the user's settings or the site configuration."), diff --git a/mail/mail/doctype/mail_settings/mail_settings.json b/mail/mail/doctype/mail_settings/mail_settings.json index 75775429d..499ffd141 100644 --- a/mail/mail/doctype/mail_settings/mail_settings.json +++ b/mail/mail/doctype/mail_settings/mail_settings.json @@ -4,6 +4,63 @@ "doctype": "DocType", "engine": "InnoDB", "field_order": [ + "config_tab", + "jmap_section", + "server_url", + "column_break_xvlo", + "api_key", + "column_break_auqm", + "username", + "password", + "spamassassin_section", + "spamd_host", + "spamd_port", + "column_break_njvq", + "spamd_scanning_mode", + "spamd_hybrid_scanning_threshold", + "defaults_section", + "default_dns_ttl", + "default_disk_quota_gb", + "default_gravatar", + "column_break_cuxv", + "stalwart_version", + "stalwart_cli_version", + "storage_shard_count", + "logs_section", + "push_log_file_count", + "push_log_level", + "push_log_max_file_size", + "column_break_ztzv", + "storage_log_file_count", + "storage_log_level", + "storage_log_max_file_size", + "limits_section", + "exchange_max_export", + "exchange_max_import", + "exchange_export_batch_size", + "column_break_rvxs", + "max_email_sync", + "max_message_payload_size_mb", + "max_push_notifications", + "column_break_cazi", + "process_pending_emails_batch_size", + "process_pending_emails_max_batch_size", + "timeouts_seconds_section", + "column_break_rtlq", + "ansible_play_timeout", + "server_job_timeout", + "server_deployment_timeout", + "column_break_ahml", + "scan_message_timeout", + "process_pending_emails_timeout", + "stalwart_cli_command_timeout", + "column_break_wvdy", + "exchange_export_timeout", + "exchange_import_timeout", + "column_break_jotg", + "fetch_lock_timeout", + "lock_acquire_timeout", + "lock_timeout", "signup_tab", "allow_signup", "column_break_daqm", @@ -200,12 +257,373 @@ "fieldname": "jmap_push_tab", "fieldtype": "Tab Break", "label": "JMAP Push" + }, + { + "fieldname": "server_url", + "fieldtype": "Data", + "label": "Server URL", + "options": "URL", + "placeholder": "https://mail.frappemail.com" + }, + { + "fieldname": "config_tab", + "fieldtype": "Tab Break", + "label": "Config" + }, + { + "fieldname": "jmap_section", + "fieldtype": "Section Break", + "label": "JMAP" + }, + { + "fieldname": "column_break_xvlo", + "fieldtype": "Column Break" + }, + { + "fieldname": "username", + "fieldtype": "Data", + "label": "Username", + "mandatory_depends_on": "eval: doc.password" + }, + { + "fieldname": "password", + "fieldtype": "Password", + "label": "Password", + "mandatory_depends_on": "eval: doc.username" + }, + { + "fieldname": "column_break_auqm", + "fieldtype": "Column Break" + }, + { + "fieldname": "api_key", + "fieldtype": "Password", + "label": "API Key" + }, + { + "default": "1500", + "fieldname": "ansible_play_timeout", + "fieldtype": "Int", + "label": "Ansible Play Timeout", + "reqd": 1 + }, + { + "default": "3600", + "fieldname": "exchange_export_timeout", + "fieldtype": "Int", + "label": "Mail Exchange Export Timeout", + "reqd": 1 + }, + { + "default": "3600", + "fieldname": "exchange_import_timeout", + "fieldtype": "Int", + "label": "Mail Exchange Import Timeout", + "reqd": 1 + }, + { + "fieldname": "column_break_jotg", + "fieldtype": "Column Break" + }, + { + "default": "300", + "fieldname": "fetch_lock_timeout", + "fieldtype": "Int", + "label": "Fetch Lock Timeout", + "reqd": 1 + }, + { + "default": "0", + "fieldname": "lock_acquire_timeout", + "fieldtype": "Int", + "label": "Lock Acquire Timeout", + "reqd": 1 + }, + { + "fieldname": "column_break_rtlq", + "fieldtype": "Column Break" + }, + { + "default": "10", + "fieldname": "lock_timeout", + "fieldtype": "Int", + "label": "Lock Timeout", + "reqd": 1 + }, + { + "default": "120", + "fieldname": "scan_message_timeout", + "fieldtype": "Int", + "label": "Scan Message Timeout", + "reqd": 1 + }, + { + "default": "1500", + "fieldname": "server_deployment_timeout", + "fieldtype": "Int", + "label": "Server Deployment Timeout", + "reqd": 1 + }, + { + "default": "1500", + "fieldname": "process_pending_emails_timeout", + "fieldtype": "Int", + "label": "Process Pending Emails Timeout", + "reqd": 1 + }, + { + "default": "1500", + "fieldname": "server_job_timeout", + "fieldtype": "Int", + "label": "Server Job Timeout", + "reqd": 1 + }, + { + "default": "3600", + "fieldname": "stalwart_cli_command_timeout", + "fieldtype": "Int", + "label": "Stalwart CLI Command Timeout", + "reqd": 1 + }, + { + "fieldname": "column_break_wvdy", + "fieldtype": "Column Break" + }, + { + "fieldname": "column_break_ahml", + "fieldtype": "Column Break" + }, + { + "default": "3600", + "fieldname": "default_dns_ttl", + "fieldtype": "Int", + "label": "Default DNS TTL", + "reqd": 1 + }, + { + "fieldname": "column_break_cuxv", + "fieldtype": "Column Break" + }, + { + "default": "1", + "fieldname": "default_disk_quota_gb", + "fieldtype": "Int", + "label": "Default Disk Quota (GB)", + "reqd": 1 + }, + { + "default": "404", + "fieldname": "default_gravatar", + "fieldtype": "Select", + "label": "Default Gravatar", + "options": "404", + "reqd": 1 + }, + { + "fieldname": "stalwart_cli_version", + "fieldtype": "Data", + "label": "Stalwart CLI Version", + "reqd": 1 + }, + { + "default": "v0.16.4", + "fieldname": "stalwart_version", + "fieldtype": "Data", + "label": "Stalwart Version", + "reqd": 1 + }, + { + "default": "8", + "fieldname": "storage_shard_count", + "fieldtype": "Int", + "label": "Storage Shard Count", + "reqd": 1 + }, + { + "default": "10", + "fieldname": "push_log_file_count", + "fieldtype": "Int", + "label": "Push Log File Count", + "reqd": 1 + }, + { + "fieldname": "column_break_ztzv", + "fieldtype": "Column Break" + }, + { + "default": "INFO", + "fieldname": "push_log_level", + "fieldtype": "Select", + "label": "Push Log Level", + "options": "ERROR\nWARNING\nINFO\nDEBUG", + "reqd": 1 + }, + { + "default": "5000000", + "fieldname": "push_log_max_file_size", + "fieldtype": "Int", + "label": "Push Log Max File Size", + "reqd": 1 + }, + { + "default": "10", + "fieldname": "storage_log_file_count", + "fieldtype": "Int", + "label": "Storage Log File Count", + "reqd": 1 + }, + { + "default": "INFO", + "fieldname": "storage_log_level", + "fieldtype": "Select", + "label": "Storage Log Level", + "options": "ERROR\nWARNING\nINFO\nDEBUG", + "reqd": 1 + }, + { + "default": "5000000", + "fieldname": "storage_log_max_file_size", + "fieldtype": "Int", + "label": "Storage Log Max File Size", + "reqd": 1 + }, + { + "default": "1000", + "fieldname": "exchange_max_export", + "fieldtype": "Int", + "label": "Exchange Max Export", + "reqd": 1 + }, + { + "default": "1000", + "fieldname": "exchange_max_import", + "fieldtype": "Int", + "label": "Exchange Max Import", + "reqd": 1 + }, + { + "default": "500", + "fieldname": "exchange_export_batch_size", + "fieldtype": "Int", + "label": "Exchange Export Batch Size", + "reqd": 1 + }, + { + "fieldname": "column_break_rvxs", + "fieldtype": "Column Break" + }, + { + "default": "100", + "fieldname": "max_email_sync", + "fieldtype": "Int", + "label": "Max Email Sync", + "reqd": 1 + }, + { + "default": "25", + "fieldname": "max_message_payload_size_mb", + "fieldtype": "Int", + "label": "Max Message Payload Size (MB)", + "reqd": 1 + }, + { + "default": "5", + "fieldname": "max_push_notifications", + "fieldtype": "Int", + "label": "Max Push Notifications", + "reqd": 1 + }, + { + "fieldname": "column_break_cazi", + "fieldtype": "Column Break" + }, + { + "default": "2500", + "fieldname": "process_pending_emails_batch_size", + "fieldtype": "Int", + "label": "Process Pending Emails Batch Size", + "reqd": 1 + }, + { + "default": "25000", + "fieldname": "process_pending_emails_max_batch_size", + "fieldtype": "Int", + "label": "Process Pending Emails Max Batch Size", + "reqd": 1 + }, + { + "collapsible": 1, + "collapsible_depends_on": "eval: true", + "fieldname": "spamassassin_section", + "fieldtype": "Section Break", + "label": "SpamAssassin" + }, + { + "fieldname": "spamd_host", + "fieldtype": "Data", + "label": "Host", + "placeholder": "spamd.example.com" + }, + { + "default": "783", + "fieldname": "spamd_port", + "fieldtype": "Int", + "label": "Port" + }, + { + "fieldname": "column_break_njvq", + "fieldtype": "Column Break" + }, + { + "default": "Hybrid Approach", + "fieldname": "spamd_scanning_mode", + "fieldtype": "Select", + "label": "Scanning Mode", + "options": "Exclude Attachments\nInclude Attachments\nHybrid Approach", + "reqd": 1 + }, + { + "default": "2", + "fieldname": "spamd_hybrid_scanning_threshold", + "fieldtype": "Float", + "label": "Hybrid Scanning Threshold", + "mandatory_depends_on": "eval: doc.spamd_scanning_mode == \"Hybrid Approach\"", + "precision": "1", + "read_only_depends_on": "eval: doc.spamd_scanning_mode != \"Hybrid Approach\"" + }, + { + "collapsible": 1, + "collapsible_depends_on": "eval: true", + "fieldname": "defaults_section", + "fieldtype": "Section Break", + "label": "Defaults" + }, + { + "collapsible": 1, + "collapsible_depends_on": "eval: true", + "fieldname": "logs_section", + "fieldtype": "Section Break", + "label": "Logs" + }, + { + "collapsible": 1, + "collapsible_depends_on": "eval: true", + "fieldname": "timeouts_seconds_section", + "fieldtype": "Section Break", + "label": "Timeouts (Seconds)" + }, + { + "collapsible": 1, + "collapsible_depends_on": "eval: true", + "fieldname": "limits_section", + "fieldtype": "Section Break", + "label": "Limits" } ], "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2026-05-13 11:55:08.939903", + "modified": "2026-05-13 13:31:41.108765", "modified_by": "Administrator", "module": "Mail", "name": "Mail Settings", diff --git a/mail/server/doctype/dns_record/dns_record.py b/mail/server/doctype/dns_record/dns_record.py index 55711f617..84f35ca29 100644 --- a/mail/server/doctype/dns_record/dns_record.py +++ b/mail/server/doctype/dns_record/dns_record.py @@ -9,7 +9,7 @@ from frappe.utils import cint, now from mail.server.doctype.dns_record.dns_provider import DNSProvider -from mail.utils import enqueue_job, get_mail_config, password_or_none, user_context +from mail.utils import enqueue_job, get_config, password_or_none, user_context from mail.utils.dns import verify_dns_record @@ -63,7 +63,7 @@ def validate_duplicate_record(self) -> None: def validate_ttl(self) -> None: """Validates the TTL value""" - self.ttl = self.ttl or cint(get_mail_config("default_dns_ttl")) + self.ttl = self.ttl or cint(get_config("default_dns_ttl")) @frappe.whitelist() def sync_dns_record(self) -> None: diff --git a/mail/server/doctype/mail_account_request/mail_account_request.py b/mail/server/doctype/mail_account_request/mail_account_request.py index 3e49cfc8c..5c133161d 100644 --- a/mail/server/doctype/mail_account_request/mail_account_request.py +++ b/mail/server/doctype/mail_account_request/mail_account_request.py @@ -17,7 +17,7 @@ validate_email_address, ) -from mail.utils import generate_random_phrase, get_mail_config +from mail.utils import generate_random_phrase, get_config from mail.utils.user import is_mail_admin, is_system_manager from mail.utils.validation import ( is_email_assigned, diff --git a/mail/server/doctype/mail_data_exchange/mail_data_exchange.py b/mail/server/doctype/mail_data_exchange/mail_data_exchange.py index fc428884a..30a6bd4cd 100644 --- a/mail/server/doctype/mail_data_exchange/mail_data_exchange.py +++ b/mail/server/doctype/mail_data_exchange/mail_data_exchange.py @@ -22,9 +22,9 @@ from mail.utils import ( compress_directory, extract_compressed_file, + get_config, get_data_export_directory, get_data_import_directory, - get_mail_config, get_mbox_files, get_stalwart_cli_path, reconnect_on_failure, @@ -107,7 +107,7 @@ def process(self) -> None: self.name, "_import", queue="long", - timeout=cint(get_mail_config("data_exchange_import_timeout")), + timeout=cint(get_config("data_exchange_import_timeout")), job_id=job_id, deduplicate=True, enqueue_after_commit=True, @@ -119,7 +119,7 @@ def process(self) -> None: self.name, "_export", queue="long", - timeout=cint(get_mail_config("data_exchange_export_timeout")), + timeout=cint(get_config("data_exchange_export_timeout")), job_id=job_id, deduplicate=True, enqueue_after_commit=True, @@ -262,7 +262,7 @@ def _get_host_and_credentials(self) -> tuple[str, str]: """Returns the host and credentials for the user's cluster.""" validate_mail_config() - config = get_mail_config() + config = get_config() return (config["server_url"], f"{config['username']}:{config['password']}") @@ -354,7 +354,7 @@ def _run_stalwart_cli_command(command: str | list[str], _credentials: str, timeo if isinstance(command, list): command = " ".join(shlex.quote(arg) for arg in command) - timeout = timeout or cint(get_mail_config("stalwart_cli_command_timeout")) + timeout = timeout or cint(get_config("stalwart_cli_command_timeout")) child = pexpect.spawn(command, encoding="utf-8", timeout=timeout) child.expect("Enter administrator credentials or press \\[ENTER\\] to use OAuth:") child.sendline(_credentials) diff --git a/mail/server/doctype/principal/principal.py b/mail/server/doctype/principal/principal.py index 0cf7b3445..d7956ac99 100644 --- a/mail/server/doctype/principal/principal.py +++ b/mail/server/doctype/principal/principal.py @@ -18,8 +18,8 @@ from mail.utils import ( generate_app_password, generate_dkim_keys, + get_config, get_dkim_selector, - get_mail_config, hash_password, is_catch_all_address, is_probable_hash, @@ -277,7 +277,7 @@ def rotate_dkim_keys(self) -> None: self._delete_dkim_signature(backend, "rsa-sha256", raise_exception=False) self._create_dkim_signature(backend, "rsa-sha256", raise_exception=False) - if bool(get_mail_config("enable_ed25519_dkim")): + if bool(get_config("enable_ed25519_dkim")): self._delete_dkim_signature(backend, "ed25519-sha256", raise_exception=False) self._create_dkim_signature(backend, "ed25519-sha256", raise_exception=False) @@ -325,7 +325,7 @@ def _create(self) -> None: payload = { "name": self.name, "type": _get_principal_type(self.type), - "quota": cint(self.quota) or cint(get_mail_config("default_mail_quota")), + "quota": cint(self.quota) or (cint(get_config("default_disk_quota_gb")) * 1024**3), "description": self.description or "", "secrets": _secrets, "emails": _emails, @@ -350,7 +350,7 @@ def _create(self) -> None: try: self._create_dkim_signature(backend, "rsa-sha256", raise_exception=False) - if bool(get_mail_config("enable_ed25519_dkim")): + if bool(get_config("enable_ed25519_dkim")): self._create_dkim_signature(backend, "ed25519-sha256", raise_exception=False) backend.request("GET", RELOAD_ENDPOINT) @@ -373,7 +373,7 @@ def _create_dkim_signature( key_type = algorithm.split("-")[0] selector = get_dkim_selector(key_type) - rsa_key_size = cint(get_mail_config("rsa_key_size")) + rsa_key_size = cint(get_config("rsa_key_size")) private_key, _public_key = generate_dkim_keys(algorithm, rsa_key_size) payload = [ @@ -603,7 +603,7 @@ def _delete(self) -> None: elif principal.type == "Domain": self._delete_dkim_signature(backend, "rsa-sha256", raise_exception=False) - if bool(get_mail_config("enable_ed25519_dkim")): + if bool(get_config("enable_ed25519_dkim")): self._delete_dkim_signature(backend, "ed25519-sha256", raise_exception=False) backend.request("GET", RELOAD_ENDPOINT) @@ -802,7 +802,7 @@ def is_mandatory(record: dict) -> bool: "priority": parse_priority(record), "value": record["content"], "mandatory": cint(is_mandatory(record)), - "ttl": cint(get_mail_config("default_dns_ttl")), + "ttl": cint(get_config("default_dns_ttl")), } formatted_records.append(entry) diff --git a/mail/server/doctype/server_ansible_play/server_ansible_play.py b/mail/server/doctype/server_ansible_play/server_ansible_play.py index 14d33d8b5..613b598e1 100644 --- a/mail/server/doctype/server_ansible_play/server_ansible_play.py +++ b/mail/server/doctype/server_ansible_play/server_ansible_play.py @@ -11,7 +11,7 @@ from frappe.utils import cint, now, time_diff_in_seconds from mail.ansible import Ansible -from mail.utils import get_mail_config +from mail.utils import get_config class ServerAnsiblePlay(Document): @@ -32,7 +32,7 @@ def after_insert(self) -> None: self.name, "execute", queue="long", - timeout=cint(get_mail_config("ansible_play_timeout")), + timeout=cint(get_config("ansible_play_timeout")), enqueue_after_commit=True, ) @@ -139,7 +139,7 @@ def retry(self) -> None: self.name, "execute", queue="long", - timeout=cint(get_mail_config("ansible_play_timeout")), + timeout=cint(get_config("ansible_play_timeout")), enqueue_after_commit=True, ) diff --git a/mail/server/doctype/server_deployment/server_deployment.py b/mail/server/doctype/server_deployment/server_deployment.py index dc4988b8b..828317663 100644 --- a/mail/server/doctype/server_deployment/server_deployment.py +++ b/mail/server/doctype/server_deployment/server_deployment.py @@ -11,7 +11,7 @@ from frappe.query_builder import Order from frappe.utils import cint, now, time_diff_in_seconds -from mail.utils import get_mail_config, get_stalwart_version +from mail.utils import get_config, get_stalwart_version class ServerDeployment(Document): @@ -69,7 +69,7 @@ def after_insert(self) -> None: self.name, "execute", queue="long", - timeout=cint(get_mail_config("server_deployment_timeout")), + timeout=cint(get_config("server_deployment_timeout")), enqueue_after_commit=True, ) @@ -264,7 +264,7 @@ def retry(self) -> None: self.name, "execute", queue="long", - timeout=cint(get_mail_config("server_deployment_timeout")), + timeout=cint(get_config("server_deployment_timeout")), enqueue_after_commit=True, ) diff --git a/mail/server/doctype/server_job/server_job.py b/mail/server/doctype/server_job/server_job.py index 1cc51f4c8..0e64bdd7f 100644 --- a/mail/server/doctype/server_job/server_job.py +++ b/mail/server/doctype/server_job/server_job.py @@ -11,7 +11,7 @@ from frappe.query_builder import Order from frappe.utils import cint, now, time_diff_in_seconds -from mail.utils import get_mail_config +from mail.utils import get_config class ServerJob(Document): @@ -32,7 +32,7 @@ def after_insert(self) -> None: self.name, "execute", queue="long", - timeout=cint(get_mail_config("server_job_timeout")), + timeout=cint(get_config("server_job_timeout")), enqueue_after_commit=True, ) @@ -168,7 +168,7 @@ def retry(self) -> None: self.name, "execute", queue="long", - timeout=cint(get_mail_config("server_job_timeout")), + timeout=cint(get_config("server_job_timeout")), enqueue_after_commit=True, ) diff --git a/mail/server/doctype/spam_check_log/spam_check_log.py b/mail/server/doctype/spam_check_log/spam_check_log.py index 6de65c13c..0ab588b96 100644 --- a/mail/server/doctype/spam_check_log/spam_check_log.py +++ b/mail/server/doctype/spam_check_log/spam_check_log.py @@ -12,7 +12,7 @@ from frappe.model.document import Document from frappe.utils import add_to_date, get_datetime, now, time_diff_in_seconds -from mail.utils import get_mail_config +from mail.utils import get_config from mail.utils.dns import get_host_by_ip @@ -44,7 +44,7 @@ def set_source_host(self) -> None: def scan_message(self) -> None: """Scans the message for spam""" - config = get_mail_config() + config = get_config() spamd_host = config.get("spamd_host") spamd_port = config.get("spamd_port") @@ -110,7 +110,7 @@ def scan_message(host: str, port: int, message: str) -> str: try: with socket.create_connection((host, port), timeout=60) as sock: - sock.settimeout(get_mail_config("scan_message_timeout")) + sock.settimeout(get_config("scan_message_timeout")) command = "SYMBOLS SPAMC/1.5\r\n\r\n" sock.sendall(command.encode("utf-8")) sock.sendall(message.encode("utf-8")) diff --git a/mail/stalwart/cli.py b/mail/stalwart/cli.py index 380da9891..c4476d05c 100644 --- a/mail/stalwart/cli.py +++ b/mail/stalwart/cli.py @@ -8,7 +8,7 @@ import frappe from frappe import _ -from mail.utils import get_mail_app_path, get_mail_config, get_stalwart_cli_path, get_stalwart_cli_version +from mail.utils import get_config, get_mail_app_path, get_stalwart_cli_path, get_stalwart_cli_version if TYPE_CHECKING: from subprocess import CompletedProcess @@ -21,7 +21,7 @@ def __init__(self, credentials: dict[str, str] | None = None) -> None: self._credentials = credentials else: - config = get_mail_config() + config = get_config() credentials = { "server_url": config.get("server_url"), "username": config.get("username"), diff --git a/mail/storage/__init__.py b/mail/storage/__init__.py index 8210ec721..d1c5199c7 100644 --- a/mail/storage/__init__.py +++ b/mail/storage/__init__.py @@ -5,7 +5,7 @@ from mail.storage.blob_store import BlobStore from mail.storage.data_store import DataStore -from mail.utils import get_mail_config +from mail.utils import get_config def get_data_store(user: str, account_id: str | None = None) -> DataStore: @@ -13,7 +13,7 @@ def get_data_store(user: str, account_id: str | None = None) -> DataStore: base_path = os.path.join(get_bench_path(), "sites", frappe.local.site, "private", "files", "data-store") key = f"{user}{DataStore.SEPARATOR}{account_id}" if account_id else user - shard_count = get_mail_config("storage_shard_count") + shard_count = get_config("storage_shard_count") return DataStore( base_path=base_path, @@ -31,7 +31,7 @@ def get_blob_store(user: str, account_id: str | None = None) -> "BlobStore": base_path = os.path.join(get_bench_path(), "sites", frappe.local.site, "private", "files", "blob-store") key = f"{user}{BlobStore.SEPARATOR}{account_id}" if account_id else user - shard_count = get_mail_config("storage_shard_count") + shard_count = get_config("storage_shard_count") return BlobStore( base_path=base_path, diff --git a/mail/utils/__init__.py b/mail/utils/__init__.py index 6a40b6be6..c4560c7fc 100644 --- a/mail/utils/__init__.py +++ b/mail/utils/__init__.py @@ -39,6 +39,55 @@ ) +CONFIG_KEYS = [ + # JMAP + "server_url", + "api_key", + "username", + "password", + # SpamAssassin + "spamd_host", + "spamd_port", + "spamd_scanning_mode", + "spamd_hybrid_scanning_threshold", + # Defaults + "default_dns_ttl", + "default_disk_quota_gb", + "default_gravatar", + "stalwart_version", + "stalwart_cli_version", + "storage_shard_count", + # Logs + "push_log_file_count", + "push_log_level", + "push_log_max_file_size", + "storage_log_file_count", + "storage_log_level", + "storage_log_max_file_size", + # Limits + "exchange_max_export", + "exchange_max_import", + "exchange_export_batch_size", + "max_email_sync", + "max_message_payload_size_mb", + "max_push_notifications", + "process_pending_emails_batch_size", + "process_pending_emails_max_batch_size", + # Timeouts + "ansible_play_timeout", + "server_job_timeout", + "server_deployment_timeout", + "scan_message_timeout", + "process_pending_emails_timeout", + "stalwart_cli_command_timeout", + "exchange_export_timeout", + "exchange_import_timeout", + "fetch_lock_timeout", + "lock_acquire_timeout", + "lock_timeout", +] + + def reconnect_on_failure(max_retries: int = 3) -> callable: """Decorator to reconnect to the database if a connection error occurs.""" @@ -62,70 +111,23 @@ def wrapper(wrapped, instance, args, kwargs): return wrapper -def get_mail_config(key: str | None = None) -> dict[str, Any] | Any: - """Returns the mail configuration from frappe.conf, or an empty dict if not set.""" - - default_config = { - "ansible_play_timeout": 1500, - "data_exchange_export_timeout": 3600, - "data_exchange_import_timeout": 3600, - "default_dns_ttl": 3600, - "default_mail_quota": 1024**3, # 1 GB - "enable_ed25519_dkim": False, - "exchange_export_batch_size": 500, - "exchange_export_timeout": 3600, - "exchange_import_timeout": 3600, - "exchange_max_export": 1_000, - "exchange_max_import": 1_000, - "fetch_lock_timeout": 300, - "gravatar_default_avatar": "404", - "lock_acquire_timeout": 0, - "lock_timeout": 10, - "max_accounts": 0, - "max_domains": 0, - "max_email_sync": 100, - "max_groups": 0, - "max_lists": 0, - "max_message_payload_size": 25 * 1024 * 1024, # 25 MB - "max_push_notifications": 5, - "process_pending_emails_batch_size": 2_500, - "process_pending_emails_max_batch_size": 25_000, - "process_pending_emails_timeout": 1500, - "push_log_file_count": 10, - "push_log_level": "INFO", - "push_log_max_size": 5_000_000, - "rsa_key_size": 2048, - "scan_message_timeout": 2 * 60, # 2 minutes - "server_deployment_timeout": 1500, - "server_job_timeout": 1500, - "stalwart_cli_command_timeout": 3600, - "stalwart_cli_version": "latest", - "storage_log_file_count": 10, - "storage_log_level": "INFO", - "storage_log_max_size": 5_000_000, - "stalwart_version": "v0.16.4", - "storage_shard_count": 8, - } +def get_config(key: str | None = None) -> dict[str, Any] | Any: + """Fetches configuration values, prioritizing Mail Settings over global config.""" - config = frappe.conf.mail or {} - config = {**default_config, **config} + mail_conf = frappe.conf.mail or {} + settings = frappe.get_cached_doc("Mail Settings") - for k, v in config.items(): - if k in default_config and not isinstance(v, type(default_config[k])): - frappe.throw( - _("Mail config key '{0}' has invalid type. Expected {1}.").format( - k, type(default_config[k]).__name__ - ) - ) + config = {} + for field in CONFIG_KEYS: + if field in ["api_key", "password"]: + config[field] = password_or_none(settings, field) or mail_conf.get(field) + else: + config[field] = settings.get(field) or mail_conf.get(field) if key: if key not in config: frappe.throw(_("Mail config key '{0}' not found").format(key)) - value = config[key] - if not value and type(value) not in (int, float, bool): - frappe.throw(_("Mail config key '{0}' is not set").format(key)) - return config[key] return config @@ -134,13 +136,13 @@ def get_mail_config(key: str | None = None) -> dict[str, Any] | Any: def get_storage_logger() -> "Logger": """Returns a logger instance for mail storage operations.""" - config = get_mail_config() + config = get_config() - max_size = cint(config["push_log_max_size"]) - file_count = cint(config["push_log_file_count"]) + max_size = cint(config["storage_log_max_file_size"]) + file_count = cint(config["storage_log_file_count"]) logger = frappe.logger("mail.storage", allow_site=True, max_size=max_size, file_count=file_count) - log_level = config["push_log_level"].upper() + log_level = config["storage_log_level"].upper() logger.setLevel(log_level) return logger @@ -149,9 +151,9 @@ def get_storage_logger() -> "Logger": def get_push_logger() -> "Logger": """Returns a logger instance for mail push notifications.""" - config = get_mail_config() + config = get_config() - max_size = cint(config["push_log_max_size"]) + max_size = cint(config["push_log_max_file_size"]) file_count = cint(config["push_log_file_count"]) logger = frappe.logger("mail.push", allow_site=True, max_size=max_size, file_count=file_count) @@ -877,10 +879,10 @@ def is_catch_all_address(address: str) -> bool: def get_stalwart_version() -> str: """Returns the Stalwart version from configuration or default.""" - return get_mail_config("stalwart_version") + return get_config("stalwart_version") def get_stalwart_cli_version() -> str: """Returns the Stalwart CLI version from configuration or default.""" - return get_mail_config("stalwart_cli_version") + return get_config("stalwart_cli_version") diff --git a/mail/utils/lock.py b/mail/utils/lock.py index 3786c967d..b419e127e 100644 --- a/mail/utils/lock.py +++ b/mail/utils/lock.py @@ -6,7 +6,7 @@ from frappe.utils import cint from redis.exceptions import WatchError -from mail.utils import get_mail_config +from mail.utils import get_config def acquire_lock( @@ -21,8 +21,8 @@ def acquire_lock( :param lock_timeout: TTL for lock in seconds, default: 10 """ - acquire_timeout = acquire_timeout or cint(get_mail_config("lock_acquire_timeout")) - lock_timeout = lock_timeout or cint(get_mail_config("lock_timeout")) + acquire_timeout = acquire_timeout or cint(get_config("lock_acquire_timeout")) + lock_timeout = lock_timeout or cint(get_config("lock_timeout")) if lock_timeout <= 0: frappe.throw(_("Lock timeout must be greater than 0 seconds.")) diff --git a/mail/utils/validation.py b/mail/utils/validation.py index e283f5183..1d10be54b 100644 --- a/mail/utils/validation.py +++ b/mail/utils/validation.py @@ -7,7 +7,7 @@ from frappe.utils import cint from frappe.utils.caching import request_cache -from mail.utils import get_mail_config +from mail.utils import get_config def is_subaddressed_email(email: str, raise_exception: bool = False) -> bool: @@ -68,7 +68,7 @@ def validate_local_domain(domain_name: str) -> None: def validate_max_domains() -> None: """Validates if the maximum number of domains has been reached.""" - max_domains = cint(get_mail_config("max_domains")) + max_domains = cint(get_config("max_domains")) if max_domains <= 0: return @@ -81,7 +81,7 @@ def validate_max_domains() -> None: def validate_max_groups() -> None: """Validates if the maximum number of groups has been reached.""" - max_groups = cint(get_mail_config("max_groups")) + max_groups = cint(get_config("max_groups")) if max_groups <= 0: return @@ -94,7 +94,7 @@ def validate_max_groups() -> None: def validate_max_accounts() -> None: """Validates if the maximum number of accounts has been reached.""" - max_accounts = cint(get_mail_config("max_accounts")) + max_accounts = cint(get_config("max_accounts")) if max_accounts <= 0: return @@ -109,7 +109,7 @@ def validate_max_accounts() -> None: def validate_max_lists() -> None: """Validates if the maximum number of lists has been reached.""" - max_lists = cint(get_mail_config("max_lists")) + max_lists = cint(get_config("max_lists")) if max_lists <= 0: return @@ -305,7 +305,7 @@ def has_permission_for_user(user: str, raise_exception: bool = True) -> bool: def validate_mail_config() -> None: """Validates the mail configuration. Checks if the server URL is set and if the fallback admin credentials are set.""" - config = get_mail_config() + config = get_config() if not config: frappe.throw(_("Mail configuration is not set.")) From f08f7542431099052f772900c31cbf2691170bb5 Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Wed, 13 May 2026 14:25:29 +0530 Subject: [PATCH 25/55] chore: option to destroy data & blob store from Mail Settings --- .../doctype/mail_settings/mail_settings.js | 50 +++++++++++++++++++ mail/storage/__init__.py | 41 ++++++++++++++- 2 files changed, 89 insertions(+), 2 deletions(-) diff --git a/mail/mail/doctype/mail_settings/mail_settings.js b/mail/mail/doctype/mail_settings/mail_settings.js index 699eb6598..ad9bef9d5 100644 --- a/mail/mail/doctype/mail_settings/mail_settings.js +++ b/mail/mail/doctype/mail_settings/mail_settings.js @@ -37,6 +37,20 @@ frappe.ui.form.on('Mail Settings', { () => frm.trigger('generate_jmap_push_keys'), __('Actions'), ) + + if (frappe.user.has_role('System Manager')) { + frm.add_custom_button( + __('Destroy Data Store'), + () => frm.trigger('destroy_data_store'), + __('Actions'), + ) + + frm.add_custom_button( + __('Destroy Blob Store'), + () => frm.trigger('destroy_blob_store'), + __('Actions'), + ) + } }, generate_jmap_push_keys(frm) { @@ -57,4 +71,40 @@ frappe.ui.form.on('Mail Settings', { }, ) }, + + destroy_data_store() { + frappe.confirm( + __( + 'This will permanently delete all data in the Data Store. This action cannot be undone. Do you want to continue?', + ), + () => { + frappe.call({ + method: 'mail.storage.destroy_data_store', + freeze: true, + freeze_message: __('Destroying Data Store…'), + callback: (r) => { + if (!r.exc) frappe.msgprint(__('Data Store destroyed successfully.')) + }, + }) + }, + ) + }, + + destroy_blob_store() { + frappe.confirm( + __( + 'This will permanently delete all data in the Blob Store. This action cannot be undone. Do you want to continue?', + ), + () => { + frappe.call({ + method: 'mail.storage.destroy_blob_store', + freeze: true, + freeze_message: __('Destroying Blob Store…'), + callback: (r) => { + if (!r.exc) frappe.msgprint(__('Blob Store destroyed successfully.')) + }, + }) + }, + ) + }, }) diff --git a/mail/storage/__init__.py b/mail/storage/__init__.py index d1c5199c7..48102653b 100644 --- a/mail/storage/__init__.py +++ b/mail/storage/__init__.py @@ -1,4 +1,5 @@ import os +import shutil import frappe from frappe.utils import get_bench_path @@ -8,10 +9,22 @@ from mail.utils import get_config +def _get_data_base_path() -> str: + """Helper function to get the base path for data storage.""" + + return os.path.join(get_bench_path(), "sites", frappe.local.site, "private", "files", "data-store") + + +def _get_blob_base_path() -> str: + """Helper function to get the base path for blob storage.""" + + return os.path.join(get_bench_path(), "sites", frappe.local.site, "private", "files", "blob-store") + + def get_data_store(user: str, account_id: str | None = None) -> DataStore: """Factory function to create a DataStore instance for the given user and account ID.""" - base_path = os.path.join(get_bench_path(), "sites", frappe.local.site, "private", "files", "data-store") + base_path = _get_data_base_path() key = f"{user}{DataStore.SEPARATOR}{account_id}" if account_id else user shard_count = get_config("storage_shard_count") @@ -26,10 +39,22 @@ def get_data_store(user: str, account_id: str | None = None) -> DataStore: ) +@frappe.whitelist() +def destroy_data_store() -> None: + """Utility function to destroy the data store.""" + + from mail.utils.user import is_system_manager + + if is_system_manager(frappe.session.user): + base_path = _get_data_base_path() + if os.path.exists(base_path): + shutil.rmtree(base_path) + + def get_blob_store(user: str, account_id: str | None = None) -> "BlobStore": """Factory function to create a BlobStore instance for the given user and account ID.""" - base_path = os.path.join(get_bench_path(), "sites", frappe.local.site, "private", "files", "blob-store") + base_path = _get_blob_base_path() key = f"{user}{BlobStore.SEPARATOR}{account_id}" if account_id else user shard_count = get_config("storage_shard_count") @@ -38,3 +63,15 @@ def get_blob_store(user: str, account_id: str | None = None) -> "BlobStore": key=key, shard_count=shard_count, ) + + +@frappe.whitelist() +def destroy_blob_store() -> None: + """Utility function to destroy the blob store.""" + + from mail.utils.user import is_system_manager + + if is_system_manager(frappe.session.user): + base_path = _get_blob_base_path() + if os.path.exists(base_path): + shutil.rmtree(base_path) From fbb93b3ed04943c52a88c85380f3794407956938 Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Wed, 13 May 2026 14:36:13 +0530 Subject: [PATCH 26/55] fix: manually invalidate Mail Settings document cache --- mail/mail/doctype/mail_settings/mail_settings.py | 1 + 1 file changed, 1 insertion(+) diff --git a/mail/mail/doctype/mail_settings/mail_settings.py b/mail/mail/doctype/mail_settings/mail_settings.py index 806a2f601..0b8a6bd25 100644 --- a/mail/mail/doctype/mail_settings/mail_settings.py +++ b/mail/mail/doctype/mail_settings/mail_settings.py @@ -20,6 +20,7 @@ def validate(self) -> None: def on_update(self) -> None: self.clear_cache() + frappe.clear_document_cache(self.doctype) if self.has_value_changed("root_domain_name"): self.handle_root_domain_change() From e70c69f1bd83d01e70b10b4ac3122f04b75fd307 Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Wed, 13 May 2026 14:43:59 +0530 Subject: [PATCH 27/55] chore: auth stalwart-cli with api-key --- mail/stalwart/cli.py | 48 ++++++++++++++++++++++++++++---------------- 1 file changed, 31 insertions(+), 17 deletions(-) diff --git a/mail/stalwart/cli.py b/mail/stalwart/cli.py index c4476d05c..f887cac2b 100644 --- a/mail/stalwart/cli.py +++ b/mail/stalwart/cli.py @@ -17,18 +17,23 @@ class StalwartCLI: def __init__(self, credentials: dict[str, str] | None = None) -> None: if credentials: - self._validate_credentials(credentials) - self._credentials = credentials + credentials = { + "server_url": credentials.get("server_url"), + "api_key": credentials.get("api_key"), + "username": credentials.get("username"), + "password": credentials.get("password"), + } else: config = get_config() credentials = { "server_url": config.get("server_url"), + "api_key": config.get("api_key"), "username": config.get("username"), "password": config.get("password"), } - self._validate_credentials(credentials) + self._validate_credentials(credentials) self._credentials = credentials self.cli_path = get_stalwart_cli_path() @@ -110,10 +115,14 @@ def _validate_credentials(self, credentials: dict) -> None: if frappe.flags.in_migrate: return - mandatory_fields = ["server_url", "username", "password"] - for field in mandatory_fields: - if field not in credentials or not credentials[field]: - frappe.throw(_("Missing mandatory credential field: {0}").format(field)) + if not credentials.get("server_url"): + frappe.throw(_("Server URL is required for Stalwart CLI operations.")) + if not credentials.get("api_key") and ( + not credentials.get("username") or not credentials.get("password") + ): + frappe.throw( + _("Either API key or username and password are required for Stalwart CLI operations.") + ) def _parse_process_result(self, result: "CompletedProcess") -> dict: """Parses the result of a subprocess execution and returns a dictionary with success status and output or error message.""" @@ -126,16 +135,21 @@ def _parse_process_result(self, result: "CompletedProcess") -> dict: def run(self, args: list[str]) -> dict: """Runs a Stalwart CLI command with the provided arguments and returns the result.""" - cmd = [ - self.cli_path, - "--url", - self._credentials["server_url"], - "--user", - self._credentials["username"], - "--password", - self._credentials["password"], - *args, - ] + cmd = [self.cli_path, "--url", self._credentials["server_url"]] + + if self._credentials.get("api_key"): + cmd.extend(["--api-key", self._credentials["api_key"]]) + else: + cmd.extend( + [ + "--user", + self._credentials["username"], + "--password", + self._credentials["password"], + ] + ) + + cmd.extend(args) result = subprocess.run(cmd, capture_output=True, text=True) return self._parse_process_result(result) From efb6b1c47176156825a598d68812e17e48631e12 Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Wed, 13 May 2026 14:53:55 +0530 Subject: [PATCH 28/55] refactor: remove Mail Data Exchange --- README.md | 2 +- .../{data-exchange.png => mail-exchange.png} | Bin frontend/src/pages/MailExchangesView.vue | 15 +- mail/hooks.py | 5 - mail/locale/main.pot | 558 +++++++++++------- .../doctype/mail_data_exchange/__init__.py | 0 .../mail_data_exchange/mail_data_exchange.js | 33 -- .../mail_data_exchange.json | 247 -------- .../mail_data_exchange/mail_data_exchange.py | 427 -------------- .../mail_data_exchange_list.js | 16 - .../test_mail_data_exchange.py | 29 - mail/utils/__init__.py | 16 - mail/workspace_sidebar/server.json | 12 - 13 files changed, 342 insertions(+), 1018 deletions(-) rename docs/screenshots/ui/{data-exchange.png => mail-exchange.png} (100%) delete mode 100644 mail/server/doctype/mail_data_exchange/__init__.py delete mode 100644 mail/server/doctype/mail_data_exchange/mail_data_exchange.js delete mode 100644 mail/server/doctype/mail_data_exchange/mail_data_exchange.json delete mode 100644 mail/server/doctype/mail_data_exchange/mail_data_exchange.py delete mode 100644 mail/server/doctype/mail_data_exchange/mail_data_exchange_list.js delete mode 100644 mail/server/doctype/mail_data_exchange/test_mail_data_exchange.py diff --git a/README.md b/README.md index c9a45691b..19584dec5 100644 --- a/README.md +++ b/README.md @@ -531,7 +531,7 @@ Some of its features include: #### 8. Importing or exporting pre-existing mail data -![Data exchange](docs/screenshots/ui/data-exchange.png) +![Mail exchange](docs/screenshots/ui/mail-exchange.png) #### 9. Progressive Web App (PWA) support for a fast, app-like experience diff --git a/docs/screenshots/ui/data-exchange.png b/docs/screenshots/ui/mail-exchange.png similarity index 100% rename from docs/screenshots/ui/data-exchange.png rename to docs/screenshots/ui/mail-exchange.png diff --git a/frontend/src/pages/MailExchangesView.vue b/frontend/src/pages/MailExchangesView.vue index bd3daf98c..a1009d674 100644 --- a/frontend/src/pages/MailExchangesView.vue +++ b/frontend/src/pages/MailExchangesView.vue @@ -22,18 +22,18 @@ /> - + -