mirror of
https://github.com/mailcow/mailcow-dockerized.git
synced 2026-11-07 01:09:21 +00:00
[Web] Archive settings, domain and mailbox switches, archive log and API
This commit is contained in:
25 files changed
+834
-12
No files matched your search
@@ -60,6 +60,7 @@ $_SESSION['acl']['sogo_access'] = "1";
|
||||
$_SESSION['acl']['protocol_access'] = "1";
|
||||
$_SESSION['acl']['mailbox_relayhost'] = "1";
|
||||
$_SESSION['acl']['unlimited_quota'] = "1";
|
||||
$_SESSION['acl']['archive'] = "1";
|
||||
|
||||
// One run at a time, released when the process ends
|
||||
$lock = fopen('/tmp/identity-sync.lock', 'c');
|
||||
|
||||
@@ -46,6 +46,7 @@ $template_data = [
|
||||
'lang_edit' => json_encode($lang['edit']),
|
||||
'lang_datatables' => json_encode($lang['datatables']),
|
||||
'imapsync_redirect_uri' => \Mailcow\Imapsync\ImapsyncModule::fromGlobals()->oauth->redirectUri(),
|
||||
'archive_enabled' => \Mailcow\Archive\ArchiveModule::fromGlobals()->settings->enabled(),
|
||||
];
|
||||
|
||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/footer.inc.php';
|
||||
@@ -13,6 +13,7 @@ $js_minifier->add('/web/js/site/admin.js');
|
||||
$js_minifier->add('/web/js/presets/rspamd.js');
|
||||
$js_minifier->add('/web/js/site/pwgen.js');
|
||||
$js_minifier->add('/web/js/site/identity.js');
|
||||
$js_minifier->add('/web/js/site/archive.js');
|
||||
|
||||
// all domains
|
||||
$domains = mailbox('get', 'domains');
|
||||
@@ -116,6 +117,7 @@ $template_data = [
|
||||
'ui_theme' => customize('get', 'ui_theme'),
|
||||
'password_complexity' => password_complexity('get'),
|
||||
'imapsync_settings' => \Mailcow\Imapsync\ImapsyncModule::fromGlobals()->api->getSettings(),
|
||||
'archive_settings' => \Mailcow\Archive\ArchiveModule::fromGlobals()->api->getSettings(),
|
||||
'show_rspamd_global_filters' => @$_SESSION['show_rspamd_global_filters'],
|
||||
'cors_settings' => $cors_settings,
|
||||
'is_https' => isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on',
|
||||
|
||||
@@ -685,6 +685,11 @@ paths:
|
||||
is domain global address list active or not, it enables
|
||||
shared contacts accross domain in SOGo webmail
|
||||
type: boolean
|
||||
archive:
|
||||
description: >-
|
||||
archive mails of the mailboxes of this domain by default (mailboxes set to `inherit`),
|
||||
see GET /api/v1/get/archive/mailboxes
|
||||
type: boolean
|
||||
mailboxes:
|
||||
description: limit count of mailboxes associated with this domain
|
||||
type: number
|
||||
@@ -1254,6 +1259,12 @@ paths:
|
||||
force_tfa:
|
||||
description: force 2FA enrollment at login
|
||||
type: boolean
|
||||
archive:
|
||||
description: >-
|
||||
archiving of the mailbox: `inherit` follows the domain, `1` always, `0` never (admins, domain
|
||||
admins with the archive permission; otherwise the template decides)
|
||||
type: string
|
||||
enum: [inherit, "1", "0"]
|
||||
tls_enforce_in:
|
||||
description: force inbound email tls encryption
|
||||
type: boolean
|
||||
@@ -3918,6 +3929,11 @@ paths:
|
||||
is domain global address list active or not, it enables
|
||||
shared contacts accross domain in SOGo webmail
|
||||
type: boolean
|
||||
archive:
|
||||
description: >-
|
||||
archive mails of the mailboxes of this domain by default (admins, domain admins with the
|
||||
archive permission)
|
||||
type: boolean
|
||||
mailboxes:
|
||||
description: limit count of mailboxes associated with this domain
|
||||
type: number
|
||||
@@ -4222,6 +4238,12 @@ paths:
|
||||
force_tfa:
|
||||
description: force 2FA enrollment at login
|
||||
type: boolean
|
||||
archive:
|
||||
description: >-
|
||||
archiving of the mailbox: `inherit` follows the domain, `1` always, `0` never (admins, domain
|
||||
admins with the archive permission)
|
||||
type: string
|
||||
enum: [inherit, "1", "0"]
|
||||
name:
|
||||
description: Full name of the mailbox user
|
||||
type: string
|
||||
@@ -4546,6 +4568,104 @@ paths:
|
||||
default: run
|
||||
type: object
|
||||
summary: Request identity sync run
|
||||
/api/v1/edit/archive:
|
||||
post:
|
||||
responses:
|
||||
"401":
|
||||
$ref: "#/components/responses/Unauthorized"
|
||||
"200":
|
||||
content:
|
||||
application/json:
|
||||
examples:
|
||||
response:
|
||||
value:
|
||||
- log:
|
||||
- archive
|
||||
- edit
|
||||
- settings
|
||||
- enabled: "1"
|
||||
address: journal@archive.example.org
|
||||
signing_secret: "*"
|
||||
msg: archive_settings_saved
|
||||
type: success
|
||||
description: OK
|
||||
headers: {}
|
||||
tags:
|
||||
- Archive
|
||||
description: >-
|
||||
Archive target, admins with a read-write key only. mailcow sends a copy of every accepted mail of archived
|
||||
mailboxes by SMTP to `address`, through the Postfix queue (envelope sender `archive-bounce@localhost`, notices
|
||||
to it are dropped: no bounce or delay warning reaches a sender). Attributes missing in the request keep their
|
||||
value, an empty `password` or `signing_secret` keeps the stored one. Switching on needs the address, and the
|
||||
signing key for the profile archivecow. With `action: test` the stored target is tested (connection, TLS,
|
||||
AUTH, whether the address is accepted; no mail is sent), with `action: testmail` a test copy is sent through
|
||||
the mail queue.
|
||||
operationId: Edit archive settings
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
example:
|
||||
items:
|
||||
- archive
|
||||
attr:
|
||||
enabled: "1"
|
||||
address: journal@archive.example.org
|
||||
host: archive.example.org
|
||||
port: 25
|
||||
tls: encrypt
|
||||
profile: archivecow
|
||||
signing_secret: secret-key
|
||||
properties:
|
||||
items:
|
||||
type: array
|
||||
default: ["archive"]
|
||||
attr:
|
||||
type: object
|
||||
properties:
|
||||
enabled:
|
||||
description: copies of accepted mails go to the archive; copies already queued are delivered either way
|
||||
type: boolean
|
||||
address:
|
||||
description: address the archive receives copies at
|
||||
type: string
|
||||
host:
|
||||
description: SMTP server of the archive, empty for the MX of the address domain
|
||||
type: string
|
||||
port:
|
||||
type: integer
|
||||
default: 25
|
||||
tls:
|
||||
description: "`encrypt` (TLS required), `verify` (TLS required, certificate verified), `may`"
|
||||
type: string
|
||||
enum: [encrypt, verify, may]
|
||||
username:
|
||||
description: SMTP AUTH user at the archive, empty for none
|
||||
type: string
|
||||
password:
|
||||
description: SMTP AUTH password, never returned
|
||||
type: string
|
||||
profile:
|
||||
description: >-
|
||||
headers in front of each copy: `archivecow` (X-Archive-Owner, -Rcpt, -From, -Queue-ID,
|
||||
-Received, -Direction, -Source, -SHA256 and X-Archive-Signature), `envelope` (sender and
|
||||
recipients under `header_from` / `header_rcpt`), `none`
|
||||
type: string
|
||||
enum: [archivecow, envelope, none]
|
||||
signing_secret:
|
||||
description: key of X-Archive-Signature (HMAC-SHA256), never returned
|
||||
type: string
|
||||
header_from:
|
||||
type: string
|
||||
default: X-Envelope-From
|
||||
header_rcpt:
|
||||
type: string
|
||||
default: X-Envelope-To
|
||||
action:
|
||||
type: string
|
||||
enum: [test, testmail]
|
||||
type: object
|
||||
summary: Edit archive settings
|
||||
/api/v1/edit/mailq:
|
||||
post:
|
||||
responses:
|
||||
@@ -5758,6 +5878,7 @@ paths:
|
||||
description: Some description
|
||||
domain_name: domain.tld
|
||||
gal: "0"
|
||||
archive: 1
|
||||
max_new_mailbox_quota: 10737418240
|
||||
max_num_aliases_for_domain: 400
|
||||
max_num_mboxes_for_domain: 10
|
||||
@@ -6312,7 +6433,9 @@ paths:
|
||||
response:
|
||||
value:
|
||||
- active: "1"
|
||||
archive: 1
|
||||
attributes:
|
||||
archive: inherit
|
||||
force_pw_update: "0"
|
||||
force_tfa: "0"
|
||||
mailbox_format: "maildir:"
|
||||
@@ -6507,6 +6630,154 @@ paths:
|
||||
nothing was changed).
|
||||
operationId: Get identity sync status
|
||||
summary: Get identity sync status
|
||||
/api/v1/get/archive/mailboxes:
|
||||
get:
|
||||
responses:
|
||||
"401":
|
||||
$ref: "#/components/responses/Unauthorized"
|
||||
"200":
|
||||
content:
|
||||
application/json:
|
||||
examples:
|
||||
response:
|
||||
value:
|
||||
enabled: 1
|
||||
mailboxes:
|
||||
- username: anna@example.org
|
||||
domain: example.org
|
||||
name: Anna Example
|
||||
active: 1
|
||||
source: domain
|
||||
- username: board@example.net
|
||||
domain: example.net
|
||||
name: Board
|
||||
active: 2
|
||||
source: mailbox
|
||||
description: OK
|
||||
headers: {}
|
||||
tags:
|
||||
- Archive
|
||||
description: >-
|
||||
Mailboxes whose mails go to the archive now (admins, read-only keys included). A mailbox is archived when
|
||||
archiving is switched on (`enabled`) and its setting is `1`, or `inherit` while its domain archives by default
|
||||
(`source` says which). Only mailboxes that receive mail are listed: `active` 1, or 2 (no login, mail is
|
||||
delivered). `username` is the value of the X-Archive-Owner header. Mailboxes that drop out of the list
|
||||
(archiving switched off, deleted, renamed) keep their mails in the archive.
|
||||
operationId: Get archived mailboxes
|
||||
summary: Get archived mailboxes
|
||||
/api/v1/get/archive/status:
|
||||
get:
|
||||
responses:
|
||||
"401":
|
||||
$ref: "#/components/responses/Unauthorized"
|
||||
"200":
|
||||
content:
|
||||
application/json:
|
||||
examples:
|
||||
response:
|
||||
value:
|
||||
enabled: 1
|
||||
configured: 1
|
||||
queued: 2
|
||||
queued_bytes: 182044
|
||||
oldest_queued: 1762425600
|
||||
delay_reasons:
|
||||
- "connect to archive.example.org[192.0.2.10]:25: Connection refused"
|
||||
last_delivery: 1762425580
|
||||
dropped: 3
|
||||
dropped_24h: 1
|
||||
last_dropped:
|
||||
time: 1762425500
|
||||
reason: "owner lookup failed (nil, 502)"
|
||||
description: OK
|
||||
headers: {}
|
||||
tags:
|
||||
- Archive
|
||||
description: >-
|
||||
Summary of the archive state (admins, read-only keys included): copies waiting in the Postfix queue with the
|
||||
reasons Postfix gives, the time of the last delivery to the archive, and mails that were not archived because
|
||||
their copy could not be handed to Postfix (`dropped` since ever, `dropped_24h` and `last_dropped` for the last
|
||||
24 hours).
|
||||
operationId: Get archive status
|
||||
summary: Get archive status
|
||||
"/api/v1/get/logs/archive/{count}":
|
||||
get:
|
||||
parameters:
|
||||
- description: Number of logs to return
|
||||
in: path
|
||||
name: count
|
||||
required: true
|
||||
schema:
|
||||
type: number
|
||||
- description: e.g. api-key-string
|
||||
example: api-key-string
|
||||
in: header
|
||||
name: X-API-Key
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
"401":
|
||||
$ref: "#/components/responses/Unauthorized"
|
||||
"200":
|
||||
content:
|
||||
application/json:
|
||||
examples:
|
||||
response:
|
||||
value:
|
||||
- time: 1762425580
|
||||
status: sent
|
||||
qid: 4XyZ1abc
|
||||
message_id: "c3d1e0f2@example.org"
|
||||
text: "250 2.0.0 Ok: queued as 9F2C4"
|
||||
- time: 1762425500
|
||||
status: dropped
|
||||
qid: 3AbC2def
|
||||
message_id: "a1b2c3@example.org"
|
||||
text: "owner lookup failed (nil, 502)"
|
||||
description: OK
|
||||
headers: {}
|
||||
tags:
|
||||
- Logs
|
||||
description: >-
|
||||
Archive log, newest first (admins, read-only keys included): deliveries of copies from the Postfix log
|
||||
(`status` sent, deferred, softbounce when the archive refused the copy and Postfix keeps it, or bounced),
|
||||
copies discarded after the queue lifetime (expired) and mails that were not archived (dropped, `text` is the
|
||||
reason). `message_id` is the Message-ID of the mail, `qid` the queue ID of the copy (of the mail for dropped).
|
||||
|
||||
Tip: You can limit how many logs you want to get by using `/<count>` at
|
||||
the end of the api url.
|
||||
operationId: Get archive log
|
||||
summary: Get archive log
|
||||
/api/v1/get/archive/settings:
|
||||
get:
|
||||
responses:
|
||||
"401":
|
||||
$ref: "#/components/responses/Unauthorized"
|
||||
"200":
|
||||
content:
|
||||
application/json:
|
||||
examples:
|
||||
response:
|
||||
value:
|
||||
enabled: 1
|
||||
address: journal@archive.example.org
|
||||
host: archive.example.org
|
||||
port: 25
|
||||
tls: encrypt
|
||||
username: ""
|
||||
has_password: 0
|
||||
profile: archivecow
|
||||
has_signing_secret: 1
|
||||
header_from: X-Envelope-From
|
||||
header_rcpt: X-Envelope-To
|
||||
description: OK
|
||||
headers: {}
|
||||
tags:
|
||||
- Archive
|
||||
description: Archive target (admins, read-only keys included); password and signing key are never returned.
|
||||
operationId: Get archive settings
|
||||
summary: Get archive settings
|
||||
"/api/v1/get/identity-link/{username}":
|
||||
get:
|
||||
parameters:
|
||||
@@ -7597,7 +7868,9 @@ paths:
|
||||
response:
|
||||
value:
|
||||
- active: "1"
|
||||
archive: 1
|
||||
attributes:
|
||||
archive: inherit
|
||||
force_pw_update: "0"
|
||||
force_tfa: "0"
|
||||
mailbox_format: "maildir:"
|
||||
@@ -8025,3 +8298,7 @@ tags:
|
||||
description: Manage Cross-Origin Resource Sharing (CORS) settings
|
||||
- name: Identity Provider
|
||||
description: Manage external Identity Provider settings
|
||||
- name: Archive
|
||||
description: >-
|
||||
Archiving (SMTP journaling): a copy of every accepted incoming, outgoing and internal mail of archived
|
||||
mailboxes goes by SMTP to the archive, through the Postfix queue.
|
||||
@@ -45,6 +45,7 @@ $template_data = [
|
||||
'lang_edit' => json_encode($lang['edit']),
|
||||
'lang_datatables' => json_encode($lang['datatables']),
|
||||
'imapsync_redirect_uri' => \Mailcow\Imapsync\ImapsyncModule::fromGlobals()->oauth->redirectUri(),
|
||||
'archive_enabled' => \Mailcow\Archive\ArchiveModule::fromGlobals()->settings->enabled(),
|
||||
];
|
||||
|
||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/footer.inc.php';
|
||||
+5
-1
@@ -65,7 +65,8 @@ if (isset($_SESSION['mailcow_cc_role'])) {
|
||||
'mta_sts' => $mta_sts,
|
||||
'mailboxes' => mailbox('get', 'mailboxes', $_GET["domain"]),
|
||||
'aliases' => mailbox('get', 'aliases', $_GET["domain"], 'address'),
|
||||
'alias_domains' => mailbox('get', 'alias_domains', $_GET["domain"])
|
||||
'alias_domains' => mailbox('get', 'alias_domains', $_GET["domain"]),
|
||||
'archive_enabled' => \Mailcow\Archive\ArchiveModule::fromGlobals()->settings->enabled()
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -75,6 +76,7 @@ if (isset($_SESSION['mailcow_cc_role'])) {
|
||||
$template_data = [
|
||||
'template' => $domain_template,
|
||||
'rl' => ['frame' => $domain_template['attributes']['rl_frame']],
|
||||
'archive_enabled' => \Mailcow\Archive\ArchiveModule::fromGlobals()->settings->enabled(),
|
||||
];
|
||||
$template = 'edit/domain-templates.twig';
|
||||
$result = true;
|
||||
@@ -85,6 +87,7 @@ if (isset($_SESSION['mailcow_cc_role'])) {
|
||||
$template_data = [
|
||||
'template' => $mailbox_template,
|
||||
'rl' => ['frame' => $mailbox_template['attributes']['rl_frame']],
|
||||
'archive_enabled' => \Mailcow\Archive\ArchiveModule::fromGlobals()->settings->enabled(),
|
||||
];
|
||||
$template = 'edit/mailbox-templates.twig';
|
||||
$result = true;
|
||||
@@ -162,6 +165,7 @@ if (isset($_SESSION['mailcow_cc_role'])) {
|
||||
'user_acls' => acl('get', 'user', $mailbox),
|
||||
'mailbox_details' => $result,
|
||||
'iam_settings' => $iam_settings,
|
||||
'archive_enabled' => \Mailcow\Archive\ArchiveModule::fromGlobals()->settings->enabled(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -539,6 +539,7 @@ function sys_mail($_data) {
|
||||
$mail->Body = $mass_text;
|
||||
}
|
||||
$mail->XMailer = 'MooMassMail';
|
||||
$archive = \Mailcow\Archive\ArchiveModule::fromGlobals();
|
||||
foreach ($rcpts as $rcpt) {
|
||||
$mail->AddAddress($rcpt);
|
||||
if (!$mail->send()) {
|
||||
@@ -548,6 +549,10 @@ function sys_mail($_data) {
|
||||
'msg' => 'Mailer error (RCPT "' . htmlspecialchars($rcpt) . '"): ' . str_replace('https://github.com/PHPMailer/PHPMailer/wiki/Troubleshooting', '', $mail->ErrorInfo)
|
||||
);
|
||||
}
|
||||
else {
|
||||
// Delivered by LMTP without rspamd, so the archive gets it here
|
||||
$archive->journal->submitDelivered($mail->getSentMIMEMessage(), $mass_from, array($rcpt), 'massmail');
|
||||
}
|
||||
$mail->ClearAllRecipients();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -366,6 +366,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
$relay_unknown_only = (isset($_data['relay_unknown_only'])) ? intval($_data['relay_unknown_only']) : $DOMAIN_DEFAULT_ATTRIBUTES['relay_unknown_only'];
|
||||
$backupmx = (isset($_data['backupmx'])) ? intval($_data['backupmx']) : $DOMAIN_DEFAULT_ATTRIBUTES['backupmx'];
|
||||
$gal = (isset($_data['gal'])) ? intval($_data['gal']) : $DOMAIN_DEFAULT_ATTRIBUTES['gal'];
|
||||
$archive = (intval((isset($_data['archive'])) ? $_data['archive'] : ($DOMAIN_DEFAULT_ATTRIBUTES['archive'] ?? 0)) == 1) ? 1 : 0;
|
||||
if ($relay_all_recipients == 1) {
|
||||
$backupmx = '1';
|
||||
}
|
||||
@@ -421,8 +422,8 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
':domain' => '%@' . $domain
|
||||
));
|
||||
// save domain
|
||||
$stmt = $pdo->prepare("INSERT INTO `domain` (`domain`, `description`, `aliases`, `mailboxes`, `defquota`, `maxquota`, `quota`, `backupmx`, `gal`, `active`, `relay_unknown_only`, `relay_all_recipients`)
|
||||
VALUES (:domain, :description, :aliases, :mailboxes, :defquota, :maxquota, :quota, :backupmx, :gal, :active, :relay_unknown_only, :relay_all_recipients)");
|
||||
$stmt = $pdo->prepare("INSERT INTO `domain` (`domain`, `description`, `aliases`, `mailboxes`, `defquota`, `maxquota`, `quota`, `backupmx`, `gal`, `active`, `relay_unknown_only`, `relay_all_recipients`, `archive`)
|
||||
VALUES (:domain, :description, :aliases, :mailboxes, :defquota, :maxquota, :quota, :backupmx, :gal, :active, :relay_unknown_only, :relay_all_recipients, :archive)");
|
||||
$stmt->execute(array(
|
||||
':domain' => $domain,
|
||||
':description' => $description,
|
||||
@@ -435,7 +436,8 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
':gal' => $gal,
|
||||
':active' => $active,
|
||||
':relay_unknown_only' => $relay_unknown_only,
|
||||
':relay_all_recipients' => $relay_all_recipients
|
||||
':relay_all_recipients' => $relay_all_recipients,
|
||||
':archive' => $archive
|
||||
));
|
||||
// save tags
|
||||
foreach($tags as $index => $tag){
|
||||
@@ -940,6 +942,8 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
$relayhost = (isset($_data['relayhost'])) ? intval($_data['relayhost']) : 0;
|
||||
// Shared mailbox: no login of its own, members open it through group shares
|
||||
$shared = (isset($_data['shared'])) ? intval($_data['shared']) : 0;
|
||||
// Archiving comes from the template unless the caller may set it
|
||||
$archive = \Mailcow\Archive\Model\Mailbox::setting((isset($_data['archive']) && hasACLAccess("archive")) ? $_data['archive'] : ($MAILBOX_DEFAULT_ATTRIBUTES['archive'] ?? 'inherit'));
|
||||
$quarantine_notification = (isset($_data['quarantine_notification'])) ? strval($_data['quarantine_notification']) : strval($MAILBOX_DEFAULT_ATTRIBUTES['quarantine_notification']);
|
||||
$quarantine_category = (isset($_data['quarantine_category'])) ? strval($_data['quarantine_category']) : strval($MAILBOX_DEFAULT_ATTRIBUTES['quarantine_category']);
|
||||
// Validate quarantine_category
|
||||
@@ -974,6 +978,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
'dav_access' => strval($dav_access),
|
||||
'relayhost' => strval($relayhost),
|
||||
'shared' => strval($shared),
|
||||
'archive' => $archive,
|
||||
'passwd_update' => time(),
|
||||
'mailbox_format' => strval($MAILBOX_DEFAULT_ATTRIBUTES['mailbox_format']),
|
||||
'quarantine_notification' => strval($quarantine_notification),
|
||||
@@ -1525,6 +1530,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
$attr['backupmx'] = (isset($_data['backupmx'])) ? intval($_data['backupmx']) : 0;
|
||||
$attr['relay_all_recipients'] = (isset($_data['relay_all_recipients'])) ? intval($_data['relay_all_recipients']) : 0;
|
||||
$attr['relay_unknown_only'] = (isset($_data['relay_unknown_only'])) ? intval($_data['relay_unknown_only']) : 0;
|
||||
$attr['archive'] = (isset($_data['archive']) && intval($_data['archive']) == 1) ? 1 : 0;
|
||||
$attr['dkim_selector'] = (isset($_data['dkim_selector'])) ? $_data['dkim_selector'] : "dkim";
|
||||
$attr['key_size'] = isset($_data['key_size']) ? intval($_data['key_size']) : 2048;
|
||||
|
||||
@@ -1600,6 +1606,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
$attr["rl_value"] = (!empty($_data['rl_value'])) ? $_data['rl_value'] : "";
|
||||
$attr["force_pw_update"] = isset($_data['force_pw_update']) ? intval($_data['force_pw_update']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['force_pw_update']);
|
||||
$attr["force_tfa"] = isset($_data['force_tfa']) ? intval($_data['force_tfa']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['force_tfa']);
|
||||
$attr["archive"] = \Mailcow\Archive\Model\Mailbox::setting($_data['archive'] ?? $MAILBOX_DEFAULT_ATTRIBUTES['archive']);
|
||||
$attr["sogo_access"] = isset($_data['sogo_access']) ? intval($_data['sogo_access']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['sogo_access']);
|
||||
$attr["active"] = isset($_data['active']) ? intval($_data['active']) : 1;
|
||||
$attr["tls_enforce_in"] = isset($_data['tls_enforce_in']) ? intval($_data['tls_enforce_in']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['tls_enforce_in']);
|
||||
@@ -2488,6 +2495,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
$gal = (isset($_data['gal'])) ? intval($_data['gal']) : $is_now['gal'];
|
||||
$description = (!empty($_data['description']) && isset($_SESSION['acl']['domain_desc']) && $_SESSION['acl']['domain_desc'] == "1") ? $_data['description'] : $is_now['description'];
|
||||
(int)$relayhost = (isset($_data['relayhost']) && isset($_SESSION['acl']['domain_relayhost']) && $_SESSION['acl']['domain_relayhost'] == "1") ? intval($_data['relayhost']) : intval($is_now['relayhost']);
|
||||
$archive = (isset($_data['archive']) && hasACLAccess("archive")) ? ((intval($_data['archive']) == 1) ? 1 : 0) : intval($is_now['archive']);
|
||||
$tags = (is_array($_data['tags']) ? $_data['tags'] : array());
|
||||
}
|
||||
else {
|
||||
@@ -2501,11 +2509,13 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
|
||||
$stmt = $pdo->prepare("UPDATE `domain` SET
|
||||
`description` = :description,
|
||||
`gal` = :gal
|
||||
`gal` = :gal,
|
||||
`archive` = :archive
|
||||
WHERE `domain` = :domain");
|
||||
$stmt->execute(array(
|
||||
':description' => $description,
|
||||
':gal' => $gal,
|
||||
':archive' => $archive,
|
||||
':domain' => $domain
|
||||
));
|
||||
// save tags
|
||||
@@ -2538,6 +2548,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
$active = (isset($_data['active'])) ? intval($_data['active']) : $is_now['active'];
|
||||
$backupmx = (isset($_data['backupmx'])) ? intval($_data['backupmx']) : $is_now['backupmx'];
|
||||
$gal = (isset($_data['gal'])) ? intval($_data['gal']) : $is_now['gal'];
|
||||
$archive = (isset($_data['archive'])) ? ((intval($_data['archive']) == 1) ? 1 : 0) : intval($is_now['archive']);
|
||||
$relay_all_recipients = (isset($_data['relay_all_recipients'])) ? intval($_data['relay_all_recipients']) : $is_now['relay_all_recipients'];
|
||||
$relay_unknown_only = (isset($_data['relay_unknown_only'])) ? intval($_data['relay_unknown_only']) : $is_now['relay_unknown_only'];
|
||||
$relayhost = (isset($_data['relayhost'])) ? intval($_data['relayhost']) : $is_now['relayhost'];
|
||||
@@ -2652,6 +2663,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
`relay_unknown_only` = :relay_unknown_only,
|
||||
`backupmx` = :backupmx,
|
||||
`gal` = :gal,
|
||||
`archive` = :archive,
|
||||
`active` = :active,
|
||||
`quota` = :quota,
|
||||
`defquota` = :defquota,
|
||||
@@ -2666,6 +2678,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
':relay_unknown_only' => $relay_unknown_only,
|
||||
':backupmx' => $backupmx,
|
||||
':gal' => $gal,
|
||||
':archive' => $archive,
|
||||
':active' => $active,
|
||||
':quota' => $quota,
|
||||
':defquota' => $defquota,
|
||||
@@ -2753,6 +2766,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
$attr['backupmx'] = (isset($_data['backupmx'])) ? intval($_data['backupmx']) : 0;
|
||||
$attr['relay_all_recipients'] = (isset($_data['relay_all_recipients'])) ? intval($_data['relay_all_recipients']) : 0;
|
||||
$attr['relay_unknown_only'] = (isset($_data['relay_unknown_only'])) ? intval($_data['relay_unknown_only']) : 0;
|
||||
$attr['archive'] = (isset($_data['archive'])) ? ((intval($_data['archive']) == 1) ? 1 : 0) : intval($is_now['attributes']['archive'] ?? 0);
|
||||
$attr['dkim_selector'] = (isset($_data['dkim_selector'])) ? $_data['dkim_selector'] : "dkim";
|
||||
$attr['key_size'] = isset($_data['key_size']) ? intval($_data['key_size']) : 2048;
|
||||
|
||||
@@ -2814,6 +2828,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
(int)$eas_access = (isset($_data['eas_access']) && hasACLAccess("protocol_access")) ? intval($_data['eas_access']) : intval($is_now['attributes']['eas_access']);
|
||||
(int)$dav_access = (isset($_data['dav_access']) && hasACLAccess("protocol_access")) ? intval($_data['dav_access']) : intval($is_now['attributes']['dav_access']);
|
||||
(int)$relayhost = (isset($_data['relayhost']) && hasACLAccess("mailbox_relayhost")) ? intval($_data['relayhost']) : intval($is_now['attributes']['relayhost']);
|
||||
$archive = \Mailcow\Archive\Model\Mailbox::setting((isset($_data['archive']) && hasACLAccess("archive")) ? $_data['archive'] : ($is_now['attributes']['archive'] ?? null));
|
||||
(int)$shared = (isset($_data['shared'])) ? intval($_data['shared']) : intval($is_now['attributes']['shared'] ?? 0);
|
||||
(int)$quota_m = (isset_has_content($_data['quota'])) ? intval($_data['quota']) : ($is_now['quota'] / 1048576);
|
||||
$name = (!empty($_data['name'])) ? ltrim(rtrim($_data['name'], '>'), '<') : $is_now['name'];
|
||||
@@ -3114,6 +3129,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
`attributes` = JSON_SET(`attributes`, '$.smtp_access', :smtp_access),
|
||||
`attributes` = JSON_SET(`attributes`, '$.eas_access', :eas_access),
|
||||
`attributes` = JSON_SET(`attributes`, '$.dav_access', :dav_access),
|
||||
`attributes` = JSON_SET(`attributes`, '$.archive', :archive),
|
||||
`attributes` = JSON_SET(`attributes`, '$.recovery_email', :recovery_email),
|
||||
`attributes` = JSON_SET(`attributes`, '$.attribute_hash', :attribute_hash)
|
||||
WHERE `username` = :username");
|
||||
@@ -3131,6 +3147,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
':smtp_access' => $smtp_access,
|
||||
':eas_access' => $eas_access,
|
||||
':dav_access' => $dav_access,
|
||||
':archive' => $archive,
|
||||
':recovery_email' => $pw_recovery_email,
|
||||
':relayhost' => $relayhost,
|
||||
':shared' => strval($shared),
|
||||
@@ -3534,6 +3551,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
$attr["rl_value"] = (!empty($_data['rl_value'])) ? $_data['rl_value'] : $is_now['rl_value'];
|
||||
$attr["force_pw_update"] = isset($_data['force_pw_update']) ? intval($_data['force_pw_update']) : $is_now['force_pw_update'];
|
||||
$attr["force_tfa"] = isset($_data['force_tfa']) ? intval($_data['force_tfa']) : $is_now['force_tfa'];
|
||||
$attr["archive"] = \Mailcow\Archive\Model\Mailbox::setting($_data['archive'] ?? ($is_now['attributes']['archive'] ?? null));
|
||||
$attr["sogo_access"] = isset($_data['sogo_access']) ? intval($_data['sogo_access']) : $is_now['sogo_access'];
|
||||
$attr["active"] = isset($_data['active']) ? intval($_data['active']) : $is_now['active'];
|
||||
$attr["tls_enforce_in"] = isset($_data['tls_enforce_in']) ? intval($_data['tls_enforce_in']) : $is_now['tls_enforce_in'];
|
||||
@@ -4682,6 +4700,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
`relay_unknown_only`,
|
||||
`backupmx`,
|
||||
`gal`,
|
||||
`archive`,
|
||||
`active`
|
||||
FROM `domain` WHERE `domain`= :domain");
|
||||
$stmt->execute(array(
|
||||
@@ -4742,6 +4761,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
$domaindata['backupmx_int'] = $row['backupmx'];
|
||||
$domaindata['gal'] = $row['gal'];
|
||||
$domaindata['gal_int'] = $row['gal'];
|
||||
$domaindata['archive'] = $row['archive'];
|
||||
$domaindata['rl'] = $rl;
|
||||
$domaindata['active'] = $row['active'];
|
||||
$domaindata['active_int'] = $row['active'];
|
||||
@@ -4829,6 +4849,8 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
if (preg_match('/y|yes/i', getenv('MASTER'))) {
|
||||
$stmt = $pdo->prepare("SELECT
|
||||
`domain`.`backupmx`,
|
||||
`domain`.`active` AS `domain_active`,
|
||||
`domain`.`archive` AS `domain_archive`,
|
||||
`mailbox`.`username`,
|
||||
`mailbox`.`name`,
|
||||
`mailbox`.`active`,
|
||||
@@ -4851,6 +4873,8 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
else {
|
||||
$stmt = $pdo->prepare("SELECT
|
||||
`domain`.`backupmx`,
|
||||
`domain`.`active` AS `domain_active`,
|
||||
`domain`.`archive` AS `domain_archive`,
|
||||
`mailbox`.`username`,
|
||||
`mailbox`.`name`,
|
||||
`mailbox`.`active`,
|
||||
@@ -4890,6 +4914,18 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
$mailboxdata['created'] = $row['created'];
|
||||
$mailboxdata['modified'] = $row['modified'];
|
||||
$mailboxdata['authsource'] = ($row['authsource']) ? $row['authsource'] : 'mailcow';
|
||||
// Mails of the mailbox go to the archive: global switch, mailbox setting ("inherit" follows the domain), receives mail
|
||||
static $archive_enabled = null;
|
||||
if ($archive_enabled === null) {
|
||||
try {
|
||||
$archive_enabled = \Mailcow\Archive\ArchiveModule::fromGlobals()->settings->enabled();
|
||||
}
|
||||
catch (PDOException $e) {
|
||||
$archive_enabled = false;
|
||||
}
|
||||
}
|
||||
$mailboxdata['archive'] = ($archive_enabled && $row['active'] != 0 && $row['domain_active'] == 1 && $row['backupmx'] == 0
|
||||
&& \Mailcow\Archive\Model\Mailbox::effective($mailboxdata['attributes']['archive'] ?? null, $row['domain_archive'])) ? 1 : 0;
|
||||
|
||||
if ($mailboxdata['percent_in_use'] === '- ') {
|
||||
$mailboxdata['percent_class'] = "info";
|
||||
|
||||
@@ -154,6 +154,7 @@ function quarantine($_action, $_data = null) {
|
||||
$mail->addAttachment($msg_tmpf, $detail_row['qid'] . '.eml');
|
||||
$mail->Body = sprintf($lang['quarantine']['release_body']);
|
||||
$mail->send();
|
||||
$released_msg = $mail->getSentMIMEMessage();
|
||||
unlink($msg_tmpf);
|
||||
}
|
||||
catch (phpmailerException $e) {
|
||||
@@ -170,6 +171,7 @@ function quarantine($_action, $_data = null) {
|
||||
}
|
||||
elseif ($release_format == 'raw') {
|
||||
$detail_row['msg'] = preg_replace('/^X-Spam-Flag: (.*)/m', 'X-Pre-Release-Spam-Flag: $1', $detail_row['msg']);
|
||||
$released_msg = $detail_row['msg'];
|
||||
// dot-stuffing per RFC5321 4.5.2: escape leading dots
|
||||
$detail_row['msg'] = preg_replace('~^\.~m', '..', $detail_row['msg']);
|
||||
$postfix_talk = array(
|
||||
@@ -213,6 +215,10 @@ function quarantine($_action, $_data = null) {
|
||||
}
|
||||
fclose($smtp_connection);
|
||||
}
|
||||
// Port 590 bypasses rspamd, so the archive gets released mails here
|
||||
if (isset($released_msg)) {
|
||||
\Mailcow\Archive\ArchiveModule::fromGlobals()->journal->submitDelivered($released_msg, $sender, array($detail_row['rcpt']), 'quarantine');
|
||||
}
|
||||
$stmt = $pdo->prepare("DELETE FROM `quarantine` WHERE id = :id");
|
||||
$stmt->execute(array(
|
||||
':id' => $row['id']
|
||||
@@ -453,6 +459,7 @@ function quarantine($_action, $_data = null) {
|
||||
$mail->addAttachment($msg_tmpf, $row['qid'] . '.eml');
|
||||
$mail->Body = sprintf($lang['quarantine']['release_body']);
|
||||
$mail->send();
|
||||
$released_msg = $mail->getSentMIMEMessage();
|
||||
unlink($msg_tmpf);
|
||||
}
|
||||
catch (phpmailerException $e) {
|
||||
@@ -467,6 +474,7 @@ function quarantine($_action, $_data = null) {
|
||||
}
|
||||
elseif ($release_format == 'raw') {
|
||||
$row['msg'] = preg_replace('/^X-Spam-Flag: (.*)/m', 'X-Pre-Release-Spam-Flag: $1', $row['msg']);
|
||||
$released_msg = $row['msg'];
|
||||
// dot-stuffing per RFC5321 4.5.2: escape leading dots
|
||||
$row['msg'] = preg_replace('~^\.~m', '..', $row['msg']);
|
||||
$postfix_talk = array(
|
||||
@@ -506,6 +514,11 @@ function quarantine($_action, $_data = null) {
|
||||
}
|
||||
fclose($smtp_connection);
|
||||
}
|
||||
// Port 590 bypasses rspamd, so the archive gets released mails here
|
||||
if (isset($released_msg)) {
|
||||
\Mailcow\Archive\ArchiveModule::fromGlobals()->journal->submitDelivered($released_msg, $sender, array($row['rcpt']), 'quarantine');
|
||||
unset($released_msg);
|
||||
}
|
||||
$stmt = $pdo->prepare("DELETE FROM `quarantine` WHERE `id` = :id");
|
||||
$stmt->execute(array(
|
||||
':id' => $id
|
||||
|
||||
@@ -37,10 +37,10 @@ class ArchiveApiController
|
||||
return $this->read(fn(): array => $this->status->status());
|
||||
}
|
||||
|
||||
/** GET archive/log */
|
||||
public function getLog(): array
|
||||
/** GET logs/archive[/count] */
|
||||
public function getLog(int $count = 0): array
|
||||
{
|
||||
return $this->read(fn(): array => $this->status->log());
|
||||
return $this->read(fn(): array => $this->status->log($count));
|
||||
}
|
||||
|
||||
/** GET archive/mailboxes */
|
||||
|
||||
@@ -72,13 +72,14 @@ class StatusService
|
||||
}
|
||||
|
||||
/**
|
||||
* GET archive/log: deliveries, expired copies and mails not archived, newest first (admins, read-only keys included).
|
||||
* GET logs/archive: deliveries, expired copies and mails not archived, newest first (admins, read-only keys included);
|
||||
* $count limits the entries, 0 keeps the default.
|
||||
*
|
||||
* @throws AccessDenied
|
||||
*/
|
||||
public function log(): array
|
||||
public function log(int $count = 0): array
|
||||
{
|
||||
$this->context->requireAdmin(false);
|
||||
return $this->state->events();
|
||||
return ($count > 0) ? $this->state->events($count) : $this->state->events();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
// Archive tab of the admin system page: header profile fields and, while archiving is enabled, the state of the archive
|
||||
// under the switch (one line per problem; the details are in the archive log of the dashboard and in the queue manager).
|
||||
var mailcowArchive = (function($) {
|
||||
'use strict';
|
||||
|
||||
function L(key) {
|
||||
return (lang[key] !== undefined) ? lang[key] : key;
|
||||
}
|
||||
function esc(s) {
|
||||
return escapeHtml(s === null || s === undefined ? '' : String(s));
|
||||
}
|
||||
function time(ts) {
|
||||
return ts ? new Date(ts * 1000).toLocaleString(LOCALE, DATETIME_FORMAT) : '-';
|
||||
}
|
||||
// lang string with %s placeholders, values escaped
|
||||
function text(key, values) {
|
||||
var i = 0;
|
||||
return esc(L(key)).replace(/%s/g, function() { return esc(values[i++]); });
|
||||
}
|
||||
function notice(cls, html) {
|
||||
return '<div class="alert alert-' + cls + ' py-2 mb-2">' + html + '</div>';
|
||||
}
|
||||
var loaded = 0;
|
||||
|
||||
function renderStatus(s) {
|
||||
var html = '';
|
||||
if (s.enabled && !s.configured) {
|
||||
html += notice('warning', esc(L('archive_state_unconfigured')));
|
||||
}
|
||||
if (s.dropped_24h) {
|
||||
html += notice('danger', text('archive_state_dropped', [s.dropped_24h, time(s.last_dropped.time), s.last_dropped.reason]));
|
||||
}
|
||||
if (s.queued) {
|
||||
var reasons = $.map(s.delay_reasons || [], function(r) { return '<li class="text-break">' + esc(r) + '</li>'; });
|
||||
html += notice('warning', text('archive_state_queued', [s.queued, time(s.oldest_queued)]) +
|
||||
(reasons.length ? '<ul class="mb-0 mt-1 small">' + reasons.join('') + '</ul>' : ''));
|
||||
}
|
||||
if (!s.enabled) {
|
||||
html += notice('secondary', esc(L('archive_state_off')));
|
||||
} else if (!html) {
|
||||
html += notice('success', s.last_delivery ? text('archive_state_ok', [time(s.last_delivery)]) : esc(L('archive_state_ok_none')));
|
||||
}
|
||||
$('#archive_state').html(html);
|
||||
}
|
||||
|
||||
function load() {
|
||||
loaded = Date.now();
|
||||
$.get('/api/v1/get/archive/status', function(s) { renderStatus(s || {}); }, 'json').fail(function() {
|
||||
$('#archive_state').html(notice('danger', esc(lang_danger.generic_server_error)));
|
||||
});
|
||||
}
|
||||
|
||||
// fields of the selected header profile only; hidden fields are not posted, so their stored values are kept
|
||||
function showProfile() {
|
||||
var profile = $('#archive_profile').val();
|
||||
$('.archive-profile').each(function() {
|
||||
var on = $(this).hasClass('archive-profile-' + profile);
|
||||
$(this).toggleClass('d-none', !on).find(':input').prop('disabled', !on);
|
||||
});
|
||||
$('.archive-profile-info').each(function() {
|
||||
$(this).toggleClass('d-none', !$(this).hasClass('archive-profile-info-' + profile));
|
||||
});
|
||||
}
|
||||
|
||||
$(document).ready(function() {
|
||||
$('#archive_profile').on('change', showProfile);
|
||||
// the tests use the saved settings: not with unsaved changes in the form
|
||||
$('form[data-id="archive"]').on('input change', ':input', function() {
|
||||
$('.archive-test').prop('disabled', true);
|
||||
$('#archive_test_info').text(L('archive_save_first')).removeClass('text-muted').addClass('text-warning');
|
||||
});
|
||||
// the state line is only shown while archiving is enabled
|
||||
var tab = document.getElementById('collapse-tab-archive');
|
||||
if (!tab || !document.getElementById('archive_state')) return;
|
||||
// the tab body is at the top: visible as soon as the tab (or on mobile its card) is opened; load again when shown later
|
||||
new IntersectionObserver(function(entries) {
|
||||
if (entries.some(function(e) { return e.intersectionRatio > 0; }) && Date.now() - loaded > 10000) load();
|
||||
}).observe(tab);
|
||||
$('#archive_refresh').on('click', function(e) {
|
||||
e.preventDefault();
|
||||
load();
|
||||
});
|
||||
// open the dashboard with the archive log (the dashboard restores the last tab)
|
||||
$('#archive_log_link').on('click', function() {
|
||||
try {
|
||||
localStorage.setItem('lastTag', 'tab-archive-logs');
|
||||
} catch (e) {}
|
||||
});
|
||||
});
|
||||
|
||||
return {load: load};
|
||||
})(jQuery);
|
||||
@@ -875,6 +875,81 @@ jQuery(function($){
|
||||
hideTableExpandCollapseBtn('#tab-cron-logs', '#cron_log');
|
||||
});
|
||||
}
|
||||
// archive copies from the Postfix log and mails not archived; the Message-ID finds a mail
|
||||
function draw_archive_logs() {
|
||||
// just recalc width if instance already exists
|
||||
if ($.fn.DataTable.isDataTable('#archive_log') ) {
|
||||
$('#archive_log').DataTable().columns.adjust().responsive.recalc();
|
||||
return;
|
||||
}
|
||||
// softbounce: the archive refused the copy, Postfix keeps it and tries again
|
||||
var status_class = {sent: 'bg-success', deferred: 'bg-warning text-dark', softbounce: 'bg-danger', bounced: 'bg-danger', expired: 'bg-danger', dropped: 'bg-danger'};
|
||||
|
||||
$('#archive_log').DataTable({
|
||||
responsive: true,
|
||||
processing: true,
|
||||
serverSide: false,
|
||||
stateSave: true,
|
||||
pageLength: log_pagination_size,
|
||||
dom: "<'row'<'col-sm-12 col-md-6'f><'col-sm-12 col-md-6'l>>" +
|
||||
"tr" +
|
||||
"<'row'<'col-sm-12 col-md-5'i><'col-sm-12 col-md-7'p>>",
|
||||
language: lang_datatables,
|
||||
order: [[0, 'desc']],
|
||||
ajax: {
|
||||
type: "GET",
|
||||
url: "/api/v1/get/logs/archive",
|
||||
dataSrc: function(data){
|
||||
return Array.isArray(data) ? data : [];
|
||||
}
|
||||
},
|
||||
columns: [
|
||||
{
|
||||
title: lang.time,
|
||||
data: 'time',
|
||||
defaultContent: '',
|
||||
createdCell: function(td, cellData) {
|
||||
createSortableDate(td, cellData)
|
||||
}
|
||||
},
|
||||
{
|
||||
title: lang.archive_delivery,
|
||||
data: 'status',
|
||||
defaultContent: '',
|
||||
render: function(data) {
|
||||
var label = lang['archive_delivery_' + data] || data;
|
||||
return '<span class="badge ' + (status_class[data] || 'bg-secondary') + '">' + escapeHtml(label) + '</span>';
|
||||
}
|
||||
},
|
||||
{
|
||||
title: lang.archive_message_id,
|
||||
data: 'message_id',
|
||||
defaultContent: '',
|
||||
className: 'text-break',
|
||||
render: function(data) {
|
||||
return escapeHtml(data);
|
||||
}
|
||||
},
|
||||
{
|
||||
title: lang.archive_queue_id,
|
||||
data: 'qid',
|
||||
defaultContent: '',
|
||||
render: function(data) {
|
||||
return escapeHtml(data);
|
||||
}
|
||||
},
|
||||
{
|
||||
title: lang.archive_details,
|
||||
data: 'text',
|
||||
defaultContent: '',
|
||||
className: 'dtr-col-md text-break',
|
||||
render: function(data) {
|
||||
return escapeHtml(data);
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
}
|
||||
function rspamd_pie_graph() {
|
||||
$.ajax({
|
||||
url: '/api/v1/get/rspamd/actions',
|
||||
@@ -1295,6 +1370,7 @@ jQuery(function($){
|
||||
onVisible("[id^=sasl_logs]", () => draw_sasl_logs());
|
||||
onVisible("[id^=netfilter_log]", () => draw_netfilter_logs());
|
||||
onVisible("[id^=cron_log]", () => draw_cron_logs());
|
||||
onVisible("[id^=archive_log]", () => draw_archive_logs());
|
||||
onVisible("[id^=rspamd_history]", () => draw_rspamd_history());
|
||||
onVisible("[id^=rspamd_donut]", () => rspamd_pie_graph());
|
||||
|
||||
|
||||
@@ -232,6 +232,7 @@ $(document).ready(function() {
|
||||
} else {
|
||||
$('#addDomain_gal').prop('checked', false);
|
||||
}
|
||||
$('#addDomain_archive').prop('checked', template.archive == 1);
|
||||
|
||||
if (template.active == 1){
|
||||
$('#addDomain_active').prop('checked', true);
|
||||
@@ -433,6 +434,7 @@ $(document).ready(function() {
|
||||
} else {
|
||||
$('#force_tfa').prop('checked', false);
|
||||
}
|
||||
$('#addMailbox_archive').selectpicker('val', ['0', '1'].includes(String(template.archive)) ? String(template.archive) : 'inherit');
|
||||
if (template.sogo_access == 1){
|
||||
$('#sogo_access').prop('checked', true);
|
||||
} else {
|
||||
@@ -1187,6 +1189,18 @@ jQuery(function($){
|
||||
return 1==data?'<i class="bi bi-check-lg"><span class="sorting-value">1</span></i>':(0==data?'<i class="bi bi-x-lg"><span class="sorting-value">0</span></i>':2==data&&'—');
|
||||
}
|
||||
},
|
||||
{
|
||||
title: lang.archive,
|
||||
data: 'archive',
|
||||
searchable: false,
|
||||
defaultContent: '',
|
||||
responsivePriority: 5,
|
||||
// not shown while archiving is switched off
|
||||
className: archive_enabled ? '' : 'never',
|
||||
render: function (data, type) {
|
||||
return 1==data?'<i class="bi bi-check-lg"><span class="sorting-value">1</span></i>':'<i class="bi bi-x-lg"><span class="sorting-value">0</span></i>';
|
||||
}
|
||||
},
|
||||
{
|
||||
title: lang.action,
|
||||
data: 'action',
|
||||
|
||||
@@ -858,6 +858,26 @@ if (isset($_GET['query'])) {
|
||||
}
|
||||
break;
|
||||
|
||||
case "archive":
|
||||
switch ($object) {
|
||||
case "settings":
|
||||
process_get_return(\Mailcow\Archive\ArchiveModule::fromGlobals()->api->getSettings());
|
||||
break;
|
||||
|
||||
case "status":
|
||||
process_get_return(\Mailcow\Archive\ArchiveModule::fromGlobals()->api->getStatus());
|
||||
break;
|
||||
|
||||
case "mailboxes":
|
||||
process_get_return(\Mailcow\Archive\ArchiveModule::fromGlobals()->api->getMailboxes());
|
||||
break;
|
||||
|
||||
default:
|
||||
process_get_return(null);
|
||||
break;
|
||||
}
|
||||
break;
|
||||
|
||||
case "logs":
|
||||
switch ($object) {
|
||||
case "dovecot":
|
||||
@@ -904,6 +924,11 @@ if (isset($_GET['query'])) {
|
||||
}
|
||||
echo (isset($logs) && !empty($logs)) ? json_encode($logs, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT) : '{}';
|
||||
break;
|
||||
case "archive":
|
||||
// deliveries of archive copies and mails not archived, newest first
|
||||
$logs = \Mailcow\Archive\ArchiveModule::fromGlobals()->api->getLog(isset($extra) ? intval(preg_replace('/[^\d]/', '', $extra)) : 0);
|
||||
echo (!empty($logs)) ? json_encode($logs, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT) : '{}';
|
||||
break;
|
||||
case "postfix":
|
||||
// 0 is first record, so empty is fine
|
||||
if (isset($extra)) {
|
||||
@@ -1682,6 +1707,8 @@ if (isset($_GET['query'])) {
|
||||
['db' => 'messages', 'dt' => 20, 'dummy' => true, 'order_subquery' => "SELECT SUM(messages) FROM `quota2` WHERE `quota2`.`username` = `m`.`username`"],
|
||||
['db' => 'tags', 'dt' => 23, 'dummy' => true, 'search' => ['join' => 'LEFT JOIN `tags_mailbox` AS `tm` ON `tm`.`username` = `m`.`username`', 'where_column' => '`tm`.`tag_name`']],
|
||||
['db' => 'active', 'dt' => 24],
|
||||
// archived by the mailbox setting or ('inherit') the domain default
|
||||
['db' => 'archive', 'dt' => 25, 'dummy' => true, 'order_subquery' => "SELECT IF(JSON_VALUE(`m`.`attributes`, '$.archive') = '1' OR (COALESCE(JSON_VALUE(`m`.`attributes`, '$.archive'), '') NOT IN ('0', '1') AND `domain`.`archive` = '1'), 1, 0) FROM `domain` WHERE `domain`.`domain` = `m`.`domain`"],
|
||||
];
|
||||
|
||||
if($_SESSION['mailcow_cc_role'] === 'admin') {
|
||||
@@ -1953,6 +1980,9 @@ if (isset($_GET['query'])) {
|
||||
case "mailbox-group":
|
||||
process_edit_return(\Mailcow\MailboxGroup\MailboxGroupModule::fromGlobals()->api->editGroups($items, $attr));
|
||||
break;
|
||||
case "archive":
|
||||
process_edit_return(\Mailcow\Archive\ArchiveModule::fromGlobals()->api->edit($attr));
|
||||
break;
|
||||
case "recipient_map":
|
||||
process_edit_return(recipient_map('edit', array_merge(array('id' => $items), $attr)));
|
||||
break;
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
</ul>
|
||||
</li>
|
||||
<li role="presentation" class="nav-item"><button class="nav-link" data-bs-target="#tab-routing" aria-selected="false" aria-controls="tab-routing" role="tab" data-bs-toggle="tab">{{ lang.admin.routing }}</button></li>
|
||||
<li role="presentation" class="nav-item"><button class="nav-link" data-bs-target="#tab-archive" aria-selected="false" aria-controls="tab-archive" role="tab" data-bs-toggle="tab">{{ lang.admin.archive }}</button></li>
|
||||
<li role="presentation" class="nav-item"><button class="nav-link" data-bs-target="#tab-sys-mails" aria-selected="false" aria-controls="tab-sys-mails" role="tab" data-bs-toggle="tab">{{ lang.admin.sys_mails }}</button></li>
|
||||
<li class="nav-item dropdown">
|
||||
<a class="nav-link dropdown-toggle" data-bs-toggle="dropdown" href="#" role="button" aria-expanded="false">{{ lang.admin.rspamd_global_filters }}</a>
|
||||
@@ -47,6 +48,7 @@
|
||||
{% include 'admin/tab-config-oauth2.twig' %}
|
||||
{% include 'admin/tab-config-rspamd.twig' %}
|
||||
{% include 'admin/tab-routing.twig' %}
|
||||
{% include 'admin/tab-archive.twig' %}
|
||||
{% include 'admin/tab-config-dkim.twig' %}
|
||||
{% include 'admin/tab-config-fwdhosts.twig' %}
|
||||
{% include 'admin/tab-config-f2b.twig' %}
|
||||
|
||||
@@ -0,0 +1,168 @@
|
||||
<div class="tab-pane fade" id="tab-archive" role="tabpanel" aria-labelledby="tab-archive">
|
||||
<div class="card mb-4">
|
||||
<div class="card-header d-flex fs-5">
|
||||
<button class="btn d-md-none flex-grow-1 text-start" data-bs-target="#collapse-tab-archive" data-bs-toggle="collapse" aria-controls="collapse-tab-archive">
|
||||
{{ lang.admin.archive }}
|
||||
</button>
|
||||
<span class="d-none d-md-block">{{ lang.admin.archive }}</span>
|
||||
</div>
|
||||
<div id="collapse-tab-archive" class="card-body collapse" data-bs-parent="#admin-content">
|
||||
<div class="row">
|
||||
<p class="offset-md-3 col-12 col-md-9 mb-4">{{ lang.admin.archive_info }}</p>
|
||||
</div>
|
||||
<form class="form-horizontal" role="form" data-id="archive" method="post" autocomplete="off">
|
||||
<div class="row mb-2">
|
||||
<div class="col-md-3 d-flex align-items-center justify-content-md-end">
|
||||
<label class="control-label" for="archive_enabled">{{ lang.admin.archive_enabled }}:</label>
|
||||
</div>
|
||||
<div class="col-12 col-md-9 d-flex align-items-center">
|
||||
<input type="hidden" name="enabled" value="0">
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" role="switch" id="archive_enabled" name="enabled" value="1"{% if archive_settings.enabled == 1 %} checked{% endif %}>
|
||||
</div>
|
||||
</div>
|
||||
<div class="offset-md-3 col-12 col-md-9">
|
||||
<small class="text-muted">{{ lang.admin.archive_enabled_info }}</small>
|
||||
</div>
|
||||
</div>
|
||||
{% if archive_settings.enabled == 1 %}
|
||||
{# Filled by js/site/archive.js: one line per problem; details in the archive log and the queue manager #}
|
||||
<div class="row mb-2" id="archive_status">
|
||||
<div class="offset-md-3 col-12 col-md-9 col-lg-7">
|
||||
<div id="archive_state"><p class="text-muted mb-2">{{ lang.datatables.loadingRecords }}</p></div>
|
||||
<small class="d-block">
|
||||
<a href="/admin/dashboard" id="archive_log_link"><i class="bi bi-journal-text"></i> {{ lang.admin.archive_log }}</a>
|
||||
<a href="/admin/queue" class="ms-3"><i class="bi bi-inboxes"></i> {{ lang.queue.queue_manager }}</a>
|
||||
<a href="#" class="ms-3" id="archive_refresh"><i class="bi bi-arrow-clockwise"></i> {{ lang.admin.refresh }}</a>
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<legend class="fs-5 mt-4"><i class="bi bi-plug"></i> {{ lang.admin.archive_section_connection }}</legend><hr>
|
||||
<div class="row mb-2">
|
||||
<div class="col-md-3 d-flex align-items-center justify-content-md-end">
|
||||
<label class="control-label" for="archive_address">{{ lang.admin.archive_address }}:</label>
|
||||
</div>
|
||||
<div class="col-12 col-md-9 col-lg-5">
|
||||
<input type="email" class="form-control" id="archive_address" name="address" placeholder="journal@archive.example.com" value="{{ archive_settings.address }}">
|
||||
</div>
|
||||
<div class="offset-md-3 col-12 col-md-9">
|
||||
<small class="text-muted">{{ lang.admin.archive_address_info }}</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-2">
|
||||
<div class="col-md-3 d-flex align-items-center justify-content-md-end">
|
||||
<label class="control-label" for="archive_host">{{ lang.admin.archive_host }}:</label>
|
||||
</div>
|
||||
<div class="col-12 col-md-9 col-lg-5">
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control" id="archive_host" name="host" placeholder="archive.example.com" value="{{ archive_settings.host }}">
|
||||
<span class="input-group-text">{{ lang.admin.archive_port }}</span>
|
||||
<input type="number" class="form-control" id="archive_port" name="port" min="1" max="65535" style="max-width: 7rem" aria-label="{{ lang.admin.archive_port }}" value="{{ archive_settings.port }}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="offset-md-3 col-12 col-md-9">
|
||||
<small class="text-muted">{{ lang.admin.archive_host_info }}</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-2">
|
||||
<div class="col-md-3 d-flex align-items-center justify-content-md-end">
|
||||
<label class="control-label" for="archive_tls">{{ lang.admin.archive_tls }}:</label>
|
||||
</div>
|
||||
<div class="col-12 col-md-9 col-lg-5">
|
||||
<select class="full-width-select form-control" id="archive_tls" name="tls">
|
||||
{% for mode in ['encrypt', 'verify', 'may'] %}
|
||||
<option value="{{ mode }}"{% if archive_settings.tls == mode %} selected{% endif %}>{{ attribute(lang.admin, 'archive_tls_' ~ mode) }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-2">
|
||||
<div class="col-md-3 d-flex align-items-center justify-content-md-end">
|
||||
<label class="control-label" for="archive_username">{{ lang.admin.archive_username }}:</label>
|
||||
</div>
|
||||
<div class="col-12 col-md-9 col-lg-5">
|
||||
<input type="text" class="form-control" id="archive_username" name="username" autocomplete="off" value="{{ archive_settings.username }}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-2">
|
||||
<div class="col-md-3 d-flex align-items-center justify-content-md-end">
|
||||
<label class="control-label" for="archive_password">{{ lang.admin.archive_password }}:</label>
|
||||
</div>
|
||||
<div class="col-12 col-md-9 col-lg-5">
|
||||
<input type="password" class="form-control" id="archive_password" name="password" autocomplete="new-password"
|
||||
placeholder="{% if archive_settings.has_password == 1 %}{{ lang.edit.unchanged_if_empty }}{% endif %}">
|
||||
{% if archive_settings.has_password == 1 %}
|
||||
<div class="form-check mt-1">
|
||||
<input class="form-check-input" type="checkbox" id="archive_password_remove" name="password_remove" value="1">
|
||||
<label class="form-check-label" for="archive_password_remove">{{ lang.admin.archive_password_remove }}</label>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<legend class="fs-5 mt-4"><i class="bi bi-file-earmark-text"></i> {{ lang.admin.archive_section_headers }}</legend><hr>
|
||||
<div class="row mb-2">
|
||||
<div class="col-md-3 d-flex align-items-center justify-content-md-end">
|
||||
<label class="control-label" for="archive_profile">{{ lang.admin.archive_profile }}:</label>
|
||||
</div>
|
||||
<div class="col-12 col-md-9 col-lg-5">
|
||||
<select class="full-width-select form-control" id="archive_profile" name="profile">
|
||||
{% for profile in ['archivecow', 'envelope', 'none'] %}
|
||||
<option value="{{ profile }}"{% if archive_settings.profile == profile %} selected{% endif %}>{{ attribute(lang.admin, 'archive_profile_' ~ profile) }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="offset-md-3 col-12 col-md-9">
|
||||
{% for profile in ['archivecow', 'envelope', 'none'] %}
|
||||
<small class="text-muted archive-profile-info archive-profile-info-{{ profile }}{% if archive_settings.profile != profile %} d-none{% endif %}">{{ attribute(lang.admin, 'archive_profile_' ~ profile ~ '_info') }}</small>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{# Fields of the other profiles are hidden and disabled, so their stored values are kept #}
|
||||
{% set archivecow = archive_settings.profile == 'archivecow' %}
|
||||
{% set envelope = archive_settings.profile == 'envelope' %}
|
||||
<div class="row mb-2 archive-profile archive-profile-archivecow{% if not archivecow %} d-none{% endif %}">
|
||||
<div class="col-md-3 d-flex align-items-center justify-content-md-end">
|
||||
<label class="control-label" for="archive_signing_secret">{{ lang.admin.archive_signing_secret }}:</label>
|
||||
</div>
|
||||
<div class="col-12 col-md-9 col-lg-5">
|
||||
<input type="password" class="form-control" id="archive_signing_secret" name="signing_secret" autocomplete="new-password"
|
||||
placeholder="{% if archive_settings.has_signing_secret == 1 %}{{ lang.edit.unchanged_if_empty }}{% endif %}"{% if not archivecow %} disabled{% endif %}>
|
||||
</div>
|
||||
<div class="offset-md-3 col-12 col-md-9">
|
||||
<small class="text-muted">{{ lang.admin.archive_signing_secret_info }}</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-2 archive-profile archive-profile-envelope{% if not envelope %} d-none{% endif %}">
|
||||
<div class="col-md-3 d-flex align-items-center justify-content-md-end">
|
||||
<label class="control-label" for="archive_header_from">{{ lang.admin.archive_header_from }}:</label>
|
||||
</div>
|
||||
<div class="col-12 col-md-9 col-lg-5">
|
||||
<input type="text" class="form-control" id="archive_header_from" name="header_from" value="{{ archive_settings.header_from }}"{% if not envelope %} disabled{% endif %}>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-2 archive-profile archive-profile-envelope{% if not envelope %} d-none{% endif %}">
|
||||
<div class="col-md-3 d-flex align-items-center justify-content-md-end">
|
||||
<label class="control-label" for="archive_header_rcpt">{{ lang.admin.archive_header_rcpt }}:</label>
|
||||
</div>
|
||||
<div class="col-12 col-md-9 col-lg-5">
|
||||
<input type="text" class="form-control" id="archive_header_rcpt" name="header_rcpt" value="{{ archive_settings.header_rcpt }}"{% if not envelope %} disabled{% endif %}>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class="mt-4">
|
||||
<div class="row mb-2">
|
||||
<div class="offset-md-3 col-12 col-md-9">
|
||||
{% set archive_ready = archive_settings.address and (archive_settings.profile != 'archivecow' or archive_settings.has_signing_secret == 1) %}
|
||||
<button type="button" class="btn btn-sm d-block d-sm-inline btn-success mb-1" data-action="edit_selected" data-item="archive" data-id="archive" data-api-url='edit/archive' data-api-attr='{}'><i class="bi bi-check-lg"></i> {{ lang.admin.save }}</button>
|
||||
<button type="button" class="btn btn-sm d-block d-sm-inline btn-secondary mb-1 archive-test" data-action="edit_selected" data-item="archive" data-id="archive_test" data-api-url='edit/archive' data-api-attr='{"action":"test"}'{% if not archive_ready %} disabled{% endif %}><i class="bi bi-plug"></i> {{ lang.admin.archive_test }}</button>
|
||||
<button type="button" class="btn btn-sm d-block d-sm-inline btn-secondary mb-1 archive-test" data-action="edit_selected" data-item="archive" data-id="archive_test" data-api-url='edit/archive' data-api-attr='{"action":"testmail"}'{% if not archive_ready %} disabled{% endif %}><i class="bi bi-envelope"></i> {{ lang.admin.archive_testmail }}</button>
|
||||
<small class="text-muted d-block" id="archive_test_info">{{ lang.admin.archive_test_info }}</small>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -17,6 +17,7 @@
|
||||
<li role="presentation"><button class="dropdown-item" data-bs-target="#tab-api-logs" aria-selected="false" aria-controls="tab-api-logs" role="tab" data-bs-toggle="tab">API</button></li>
|
||||
<li role="presentation"><button class="dropdown-item" data-bs-target="#tab-api-rl" aria-selected="false" aria-controls="tab-api-rl" role="tab" data-bs-toggle="tab">Ratelimits</button></li>
|
||||
<li role="presentation"><button class="dropdown-item" data-bs-target="#tab-cron-logs" aria-selected="false" aria-controls="tab-api-rl" role="tab" data-bs-toggle="tab">Crontasks</button></li>
|
||||
<li role="presentation"><button class="dropdown-item" data-bs-target="#tab-archive-logs" aria-selected="false" aria-controls="tab-archive-logs" role="tab" data-bs-toggle="tab">{{ lang.admin.archive }}</button></li>
|
||||
<li role="presentation"><span class="dropdown-header fs-6">{{ lang.debug.external_logs }}</span></li>
|
||||
<li role="presentation"><button class="dropdown-item" data-bs-target="#tab-rspamd-history" aria-selected="false" aria-controls="tab-rspamd-history" role="tab" data-bs-toggle="tab">Rspamd</button></li>
|
||||
<li role="presentation"><span class="dropdown-header fs-6">{{ lang.debug.static_logs }}</span></li>
|
||||
@@ -547,6 +548,21 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div role="tabpanel" class="tab-pane" id="tab-archive-logs">
|
||||
<div class="debug-log-info">{{ lang.admin.archive_log_info }}</div>
|
||||
<div class="card">
|
||||
<div class="card-header d-flex align-items-center fs-5">
|
||||
<span class="mt-2 ms-2">{{ lang.admin.archive }}</span>
|
||||
<div class="btn-group ms-auto">
|
||||
<button class="btn btn-sm btn-secondary refresh_table" data-table="archive_log">{{ lang.admin.refresh }}</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<table id="archive_log" class="table table-striped dt-responsive w-100"></table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div> <!-- /tab-content -->
|
||||
</div> <!-- /col-md-12 -->
|
||||
</div> <!-- /row -->
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
<input type="hidden" value="0" name="active">
|
||||
<input type="hidden" value="0" name="backupmx">
|
||||
<input type="hidden" value="0" name="gal">
|
||||
<input type="hidden" value="0" name="archive">
|
||||
<input type="hidden" value="0" name="relay_all_recipients">
|
||||
<input type="hidden" value="0" name="relay_unknown_only">
|
||||
|
||||
@@ -70,6 +71,14 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row{% if not archive_enabled %} d-none{% endif %}">
|
||||
<div class="offset-sm-2 col-sm-10">
|
||||
<div class="form-check">
|
||||
<label><input type="checkbox" class="form-check-input" value="1" name="archive"{% if template.attributes.archive == '1' %} checked{% endif %}> {{ lang.edit.archive_domain }}</label>
|
||||
<small class="text-muted">{{ lang.edit.archive_domain_info }}</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-2">
|
||||
<div class="offset-sm-2 col-sm-10">
|
||||
<div class="form-check">
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
<input type="hidden" value="0" name="active">
|
||||
<input type="hidden" value="0" name="backupmx">
|
||||
<input type="hidden" value="0" name="gal">
|
||||
<input type="hidden" value="0" name="archive">
|
||||
<input type="hidden" value="0" name="relay_all_recipients">
|
||||
<input type="hidden" value="0" name="relay_unknown_only">
|
||||
<div class="row mb-4">
|
||||
@@ -133,6 +134,14 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row{% if not archive_enabled %} d-none{% endif %}" data-acl="{{ acl.archive }}">
|
||||
<div class="offset-sm-2 col-sm-10">
|
||||
<div class="form-check">
|
||||
<label><input type="checkbox" class="form-check-input" value="1" name="archive"{% if result.archive == '1' %} checked{% endif %}{% if acl.archive != 1 %} disabled{% endif %}> {{ lang.edit.archive_domain }}</label>
|
||||
<small class="text-muted">{{ lang.edit.archive_domain_info }}</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<hr>
|
||||
<div class="row mb-2">
|
||||
<div class="offset-sm-2 col-sm-10">
|
||||
|
||||
@@ -114,6 +114,17 @@
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-2{% if not archive_enabled %} d-none{% endif %}">
|
||||
<label class="control-label col-sm-2" for="archive">{{ lang.edit.archive }}</label>
|
||||
<div class="col-sm-10">
|
||||
<select class="form-control" name="archive" id="archive">
|
||||
<option value="inherit"{% if template.attributes.archive not in ['0', '1'] %} selected{% endif %}>{{ lang.edit.archive_inherit }}</option>
|
||||
<option value="1"{% if template.attributes.archive == '1' %} selected{% endif %}>{{ lang.edit.archive_on }}</option>
|
||||
<option value="0"{% if template.attributes.archive == '0' %} selected{% endif %}>{{ lang.edit.archive_off }}</option>
|
||||
</select>
|
||||
<small class="text-muted d-block">{{ lang.edit.archive_info }}</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-4">
|
||||
<label class="control-label col-sm-2">ACL</label>
|
||||
<div class="col-sm-10">
|
||||
|
||||
@@ -331,6 +331,19 @@
|
||||
<small class="text-muted">{{ lang.edit.allow_from_smtp_info }}</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mt-2{% if not archive_enabled %} d-none{% endif %}" data-acl="{{ acl.archive }}">
|
||||
<label class="control-label col-sm-2" for="archive">{{ lang.edit.archive }}
|
||||
<br><span class="badge {% if result.archive == 1 %}bg-success{% else %}bg-secondary{% endif %}">{% if result.archive == 1 %}{{ lang.edit.archive_active }}{% else %}{{ lang.edit.archive_inactive }}{% endif %}</span>
|
||||
</label>
|
||||
<div class="col-sm-10">
|
||||
<select class="form-control" name="archive" id="archive"{% if acl.archive != 1 %} disabled{% endif %}>
|
||||
<option value="inherit"{% if result.attributes.archive not in ['0', '1'] %} selected{% endif %}>{{ lang.edit.archive_inherit }}</option>
|
||||
<option value="1"{% if result.attributes.archive == '1' %} selected{% endif %}>{{ lang.edit.archive_on }}</option>
|
||||
<option value="0"{% if result.attributes.archive == '0' %} selected{% endif %}>{{ lang.edit.archive_off }}</option>
|
||||
</select>
|
||||
<small class="text-muted d-block">{{ lang.edit.archive_info }}</small>
|
||||
</div>
|
||||
</div>
|
||||
<hr>
|
||||
<div class="row">
|
||||
<div class="offset-sm-2 col-sm-10">
|
||||
|
||||
@@ -81,6 +81,7 @@
|
||||
var csrf_token = '{{ csrf_token }}';
|
||||
var pagination_size = Math.trunc('{{ pagination_size }}');
|
||||
var role = '{{ role }}';
|
||||
var archive_enabled = {{ archive_enabled ? 1 : 0 }};
|
||||
var is_dual = {{ is_dual }};
|
||||
var ALLOW_ADMIN_EMAIL_LOGIN = {{ allow_admin_email_login }};
|
||||
</script>
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
<div class="row mb-4">
|
||||
<label class="control-label col-sm-2 text-sm-end text-sm-end" for="description">{{ lang.mailbox.template }}</label>
|
||||
<div class="col-sm-10">
|
||||
<select data-live-search="true" id="mailbox_templates" class="form-control" title="{{ lang.mailbox.template }}">
|
||||
<select data-live-search="true" id="mailbox_templates" name="template" class="form-control" title="{{ lang.mailbox.template }}">
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
@@ -154,6 +154,17 @@
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-2{% if not archive_enabled %} d-none{% endif %}" data-acl="{{ acl.archive }}">
|
||||
<label class="control-label col-sm-2 text-sm-end" for="addMailbox_archive">{{ lang.edit.archive }}</label>
|
||||
<div class="col-sm-10">
|
||||
<select class="form-control" name="archive" id="addMailbox_archive"{% if acl.archive != 1 %} disabled{% endif %}>
|
||||
<option value="inherit" selected>{{ lang.edit.archive_inherit }}</option>
|
||||
<option value="1">{{ lang.edit.archive_on }}</option>
|
||||
<option value="0">{{ lang.edit.archive_off }}</option>
|
||||
</select>
|
||||
<small class="text-muted d-block">{{ lang.edit.archive_info }}</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-4">
|
||||
<label class="control-label col-sm-2 text-sm-end text-sm-end">ACL</label>
|
||||
<div class="col-sm-10">
|
||||
@@ -360,6 +371,17 @@
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-2{% if not archive_enabled %} d-none{% endif %}">
|
||||
<label class="control-label col-sm-2 text-sm-end" for="addMailboxTemplate_archive">{{ lang.edit.archive }}</label>
|
||||
<div class="col-sm-10">
|
||||
<select class="form-control" name="archive" id="addMailboxTemplate_archive">
|
||||
<option value="inherit" selected>{{ lang.edit.archive_inherit }}</option>
|
||||
<option value="1">{{ lang.edit.archive_on }}</option>
|
||||
<option value="0">{{ lang.edit.archive_off }}</option>
|
||||
</select>
|
||||
<small class="text-muted d-block">{{ lang.edit.archive_info }}</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-4">
|
||||
<label class="control-label col-sm-2 text-sm-end text-sm-end">ACL</label>
|
||||
<div class="col-sm-10">
|
||||
@@ -451,6 +473,7 @@
|
||||
<div class="modal-body">
|
||||
<form class="form-horizontal" data-cached-form="true" data-id="add_domain" role="form">
|
||||
<input type="hidden" value="0" name="gal">
|
||||
<input type="hidden" value="0" name="archive">
|
||||
<input type="hidden" value="0" name="active">
|
||||
<input type="hidden" value="0" name="backupmx">
|
||||
<input type="hidden" value="0" name="relay_all_recipients">
|
||||
@@ -525,6 +548,14 @@
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="row mb-2{% if not archive_enabled %} d-none{% endif %}" data-acl="{{ acl.archive }}">
|
||||
<div class="offset-sm-2 col-sm-10">
|
||||
<div class="form-check">
|
||||
<label><input type="checkbox" class="form-check-input" value="1" name="archive" id="addDomain_archive"> {{ lang.edit.archive_domain }}</label>
|
||||
<small class="text-muted">{{ lang.edit.archive_domain_info }}</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-4">
|
||||
<div class="offset-sm-2 col-sm-10">
|
||||
<div class="form-check">
|
||||
@@ -664,6 +695,14 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row{% if not archive_enabled %} d-none{% endif %}">
|
||||
<div class="offset-sm-2 col-sm-10">
|
||||
<div class="form-check">
|
||||
<label><input type="checkbox" class="form-check-input" value="1" name="archive"> {{ lang.edit.archive_domain }}</label>
|
||||
<small class="text-muted">{{ lang.edit.archive_domain_info }}</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-2">
|
||||
<div class="offset-sm-2 col-sm-10">
|
||||
<div class="form-check">
|
||||
|
||||
Reference in new issue
Block a user