2026-02-27 08:06:22 +01:00
/ * *
* WooCow – Admin JavaScript
* /
( function ( $ ) {
'use strict' ;
const ajax = ( action , data ) =>
$ . post ( woocow . ajax _url , { action , nonce : woocow . nonce , ... data } ) ;
const notice = ( $el , type , msg , autohide = true ) => {
$el . html ( ` <div class="notice notice- ${ type } is-dismissible"><p> ${ msg } </p></div> ` ) ;
if ( autohide ) setTimeout ( ( ) => $el . find ( '.notice' ) . fadeOut ( ) , 4000 ) ;
} ;
// ── Servers Page ──────────────────────────────────────────────────────────
if ( $ ( '#wc-servers-table-wrap' ) . length ) {
let editId = 0 ;
const loadServers = ( ) => {
ajax ( 'woocow_servers_list' ) . done ( res => {
if ( ! res . success ) return ;
const rows = res . data ;
if ( ! rows . length ) {
$ ( '#wc-servers-table-wrap' ) . html ( '<p>No servers yet. Add one above.</p>' ) ;
return ;
}
let html = ` <table class="wp-list-table widefat fixed striped woocow-table">
< thead > < tr >
< th > Name < / t h > < t h > U R L < / t h > < t h > S t a t u s < / t h > < t h > A d d e d < / t h > < t h > A c t i o n s < / t h >
< / t r > < / t h e a d > < t b o d y > ` ;
rows . forEach ( s => {
const badge = s . active == 1
? '<span class="woocow-badge woocow-badge-green">Active</span>'
: '<span class="woocow-badge woocow-badge-grey">Inactive</span>' ;
html += ` <tr data-id=" ${ s . id } " data-name=" ${ esc ( s . name ) } " data-url=" ${ esc ( s . url ) } ">
< td > < strong > $ { esc ( s . name ) } < / s t r o n g > < / t d >
< td > < a href = "${esc(s.url)}" target = "_blank" rel = "noopener" > $ { esc ( s . url ) } < / a > < / t d >
< td > $ { badge } < / t d >
< td > $ { s . created _at . split ( ' ' ) [ 0 ] } < / t d >
< td class = "woocow-actions" >
< button class = "button button-small wc-srv-edit" data - id = "${s.id}" > Edit < / b u t t o n >
< button class = "button button-small wc-srv-test" data - id = "${s.id}" > Test < / b u t t o n >
< button class = "button button-small wc-srv-del" data - id = "${s.id}" style = "color:#a00" > Delete < / b u t t o n >
< / t d >
< / t r > ` ;
} ) ;
html += '</tbody></table>' ;
$ ( '#wc-servers-table-wrap' ) . html ( html ) ;
$ ( '#wc-servers-loading' ) . hide ( ) ;
} ) ;
} ;
loadServers ( ) ;
// Show add form
$ ( '#wc-add-server' ) . on ( 'click' , ( ) => {
editId = 0 ;
$ ( '#wc-server-id' ) . val ( '' ) ;
$ ( '#wc-server-name, #wc-server-url, #wc-server-key' ) . val ( '' ) ;
$ ( '#wc-server-active' ) . prop ( 'checked' , true ) ;
$ ( '#wc-server-form-title' ) . text ( 'Add Server' ) ;
$ ( '#wc-server-form' ) . slideDown ( ) ;
} ) ;
// Edit row
$ ( document ) . on ( 'click' , '.wc-srv-edit' , function ( ) {
editId = $ ( this ) . data ( 'id' ) ;
const $row = $ ( this ) . closest ( 'tr' ) ;
$ ( '#wc-server-id' ) . val ( editId ) ;
$ ( '#wc-server-name' ) . val ( $row . data ( 'name' ) ) ;
$ ( '#wc-server-url' ) . val ( $row . data ( 'url' ) ) ;
$ ( '#wc-server-key' ) . val ( '' ) ;
$ ( '#wc-server-active' ) . prop ( 'checked' , true ) ;
$ ( '#wc-server-form-title' ) . text ( 'Edit Server' ) ;
$ ( '#wc-server-form' ) . slideDown ( ) ;
$ ( 'html, body' ) . animate ( { scrollTop : 0 } , 300 ) ;
} ) ;
// Save
$ ( '#wc-server-save' ) . on ( 'click' , ( ) => {
const data = {
id : $ ( '#wc-server-id' ) . val ( ) ,
name : $ ( '#wc-server-name' ) . val ( ) . trim ( ) ,
url : $ ( '#wc-server-url' ) . val ( ) . trim ( ) ,
api _key : $ ( '#wc-server-key' ) . val ( ) . trim ( ) ,
active : $ ( '#wc-server-active' ) . is ( ':checked' ) ? 1 : 0 ,
} ;
if ( ! data . name || ! data . url || ( ! data . api _key && ! editId ) ) {
notice ( $ ( '#wc-notices' ) , 'error' , 'Please fill in all required fields.' ) ;
return ;
}
ajax ( 'woocow_server_save' , data ) . done ( res => {
if ( res . success ) {
notice ( $ ( '#wc-notices' ) , 'success' , 'Server saved.' ) ;
$ ( '#wc-server-form' ) . slideUp ( ) ;
loadServers ( ) ;
} else {
notice ( $ ( '#wc-notices' ) , 'error' , res . data || 'Save failed.' ) ;
}
} ) ;
} ) ;
// Test connection
$ ( '#wc-server-test' ) . on ( 'click' , ( ) => {
const $result = $ ( '#wc-server-test-result' ) . text ( 'Testing…' ) ;
ajax ( 'woocow_server_test' , {
id : $ ( '#wc-server-id' ) . val ( ) ,
url : $ ( '#wc-server-url' ) . val ( ) . trim ( ) ,
api _key : $ ( '#wc-server-key' ) . val ( ) . trim ( ) ,
} ) . done ( res => {
if ( res . success ) {
$result . html ( ` <span style="color:green">✓ Connected – Mailcow ${ esc ( res . data . version ) } </span> ` ) ;
} else {
$result . html ( ` <span style="color:red">✗ ${ esc ( res . data ) } </span> ` ) ;
}
} ) ;
} ) ;
// Test from row
$ ( document ) . on ( 'click' , '.wc-srv-test' , function ( ) {
const id = $ ( this ) . data ( 'id' ) ;
const $td = $ ( this ) . closest ( 'td' ) ;
$td . append ( '<span class="wc-inline-test"> Testing…</span>' ) ;
ajax ( 'woocow_server_test' , { id } ) . done ( res => {
$td . find ( '.wc-inline-test' ) . html (
res . success
? ` <span style="color:green"> ✓ v ${ esc ( res . data . version ) } </span> `
: ` <span style="color:red"> ✗ ${ esc ( res . data ) } </span> `
) ;
} ) ;
} ) ;
// Delete
$ ( document ) . on ( 'click' , '.wc-srv-del' , function ( ) {
if ( ! confirm ( 'Delete this server? All domain assignments for it will also be removed.' ) ) return ;
ajax ( 'woocow_server_delete' , { id : $ ( this ) . data ( 'id' ) } ) . done ( res => {
if ( res . success ) loadServers ( ) ;
else notice ( $ ( '#wc-notices' ) , 'error' , res . data ) ;
} ) ;
} ) ;
$ ( '#wc-server-cancel' ) . on ( 'click' , ( ) => $ ( '#wc-server-form' ) . slideUp ( ) ) ;
}
// ── Assignments Page ──────────────────────────────────────────────────────
if ( $ ( '#wc-assignments-table-wrap' ) . length ) {
const loadAssignments = ( ) => {
ajax ( 'woocow_assignments_list' ) . done ( res => {
if ( ! res . success ) return ;
const rows = res . data ;
if ( ! rows . length ) {
$ ( '#wc-assignments-table-wrap' ) . html ( '<p>No assignments yet.</p>' ) ;
$ ( '#wc-assignments-loading' ) . hide ( ) ;
return ;
}
let html = ` <table class="wp-list-table widefat fixed striped woocow-table">
< thead > < tr >
< th > Customer < / t h > < t h > E m a i l < / t h > < t h > D o m a i n < / t h > < t h > S e r v e r < / t h > < t h > A s s i g n e d < / t h > < t h > A c t i o n s < / t h >
< / t r > < / t h e a d > < t b o d y > ` ;
rows . forEach ( r => {
html += ` <tr>
< td > $ { esc ( r . display _name ) } < / t d >
< td > $ { esc ( r . user _email ) } < / t d >
< td > < strong > $ { esc ( r . domain ) } < / s t r o n g > < / t d >
< td > $ { esc ( r . server _name ) } < / t d >
< td > $ { r . created _at . split ( ' ' ) [ 0 ] } < / t d >
< td > < button class = "button button-small wc-assign-del" data - id = "${r.id}" style = "color:#a00" > Remove < / b u t t o n > < / t d >
< / t r > ` ;
} ) ;
html += '</tbody></table>' ;
$ ( '#wc-assignments-table-wrap' ) . html ( html ) ;
$ ( '#wc-assignments-loading' ) . hide ( ) ;
} ) ;
} ;
// Load servers into select
ajax ( 'woocow_servers_list' ) . done ( res => {
if ( ! res . success ) return ;
res . data . filter ( s => s . active == 1 ) . forEach ( s => {
$ ( '#wc-assign-server' ) . append ( ` <option value=" ${ s . id } "> ${ esc ( s . name ) } </option> ` ) ;
} ) ;
} ) ;
// Server → load domains
$ ( '#wc-assign-server' ) . on ( 'change' , function ( ) {
const sid = $ ( this ) . val ( ) ;
$ ( '#wc-assign-domain' ) . html ( '<option value="">— Loading —</option>' ) ;
if ( ! sid ) { $ ( '#wc-domain-row' ) . hide ( ) ; return ; }
ajax ( 'woocow_server_domains' , { server _id : sid } ) . done ( res => {
if ( ! res . success ) {
alert ( 'Could not load domains: ' + res . data ) ;
return ;
}
$ ( '#wc-assign-domain' ) . html ( '<option value="">— Select domain —</option>' ) ;
res . data . forEach ( d => {
$ ( '#wc-assign-domain' ) . append ( ` <option value=" ${ esc ( d . domain ) } "> ${ esc ( d . domain ) } </option> ` ) ;
} ) ;
$ ( '#wc-domain-row' ) . show ( ) ;
} ) ;
} ) ;
// Customer autocomplete
let searchTimer ;
$ ( '#wc-cust-search' ) . on ( 'input' , function ( ) {
clearTimeout ( searchTimer ) ;
const term = $ ( this ) . val ( ) . trim ( ) ;
if ( term . length < 2 ) { $ ( '#wc-cust-results' ) . hide ( ) ; return ; }
searchTimer = setTimeout ( ( ) => {
ajax ( 'woocow_customers_search' , { term } ) . done ( res => {
if ( ! res . success || ! res . data . length ) { $ ( '#wc-cust-results' ) . hide ( ) ; return ; }
let html = '' ;
res . data . forEach ( c => {
html += ` <div class="woocow-ac-item" data-id=" ${ c . id } " data-label=" ${ esc ( c . label ) } "> ${ esc ( c . label ) } </div> ` ;
} ) ;
$ ( '#wc-cust-results' ) . html ( html ) . show ( ) ;
} ) ;
} , 250 ) ;
} ) ;
$ ( document ) . on ( 'click' , '.woocow-ac-item' , function ( ) {
$ ( '#wc-cust-id' ) . val ( $ ( this ) . data ( 'id' ) ) ;
$ ( '#wc-cust-search' ) . val ( '' ) ;
$ ( '#wc-cust-selected' ) . text ( $ ( this ) . data ( 'label' ) ) ;
$ ( '#wc-cust-results' ) . hide ( ) ;
} ) ;
$ ( document ) . on ( 'click' , function ( e ) {
if ( ! $ ( e . target ) . closest ( '#wc-cust-results, #wc-cust-search' ) . length ) {
$ ( '#wc-cust-results' ) . hide ( ) ;
}
} ) ;
// Save assignment
$ ( '#wc-assign-save' ) . on ( 'click' , ( ) => {
const customer _id = $ ( '#wc-cust-id' ) . val ( ) ;
const server _id = $ ( '#wc-assign-server' ) . val ( ) ;
const domain = $ ( '#wc-assign-domain' ) . val ( ) ;
if ( ! customer _id || ! server _id || ! domain ) {
notice ( $ ( '#wc-assign-notice' ) , 'error' , 'Please select a customer, server, and domain.' ) ;
return ;
}
ajax ( 'woocow_assignment_save' , { customer _id , server _id , domain } ) . done ( res => {
if ( res . success ) {
notice ( $ ( '#wc-assign-notice' ) , 'success' , ` Domain <strong> ${ esc ( domain ) } </strong> assigned. ` ) ;
$ ( '#wc-cust-id' ) . val ( '' ) ;
$ ( '#wc-cust-selected' ) . text ( '' ) ;
loadAssignments ( ) ;
} else {
notice ( $ ( '#wc-assign-notice' ) , 'error' , res . data ) ;
}
} ) ;
} ) ;
// Delete assignment
$ ( document ) . on ( 'click' , '.wc-assign-del' , function ( ) {
if ( ! confirm ( 'Remove this domain assignment?' ) ) return ;
ajax ( 'woocow_assignment_delete' , { id : $ ( this ) . data ( 'id' ) } ) . done ( res => {
if ( res . success ) loadAssignments ( ) ;
} ) ;
} ) ;
loadAssignments ( ) ;
}
// ── Mailboxes Page ────────────────────────────────────────────────────────
if ( $ ( '#wc-mb-table-wrap' ) . length ) {
let currentServerId = null ;
let currentDomain = null ;
// Server → load domains
$ ( '#wc-mb-server' ) . on ( 'change' , function ( ) {
const sid = $ ( this ) . val ( ) ;
$ ( '#wc-mb-domain' ) . hide ( ) . html ( '<option value="">— Select domain —</option>' ) ;
$ ( '#wc-mb-load' ) . prop ( 'disabled' , true ) ;
if ( ! sid ) return ;
ajax ( 'woocow_server_domains' , { server _id : sid } ) . done ( res => {
if ( ! res . success ) return ;
res . data . forEach ( d => {
$ ( '#wc-mb-domain' ) . append ( ` <option value=" ${ esc ( d . domain ) } "> ${ esc ( d . domain ) } </option> ` ) ;
} ) ;
$ ( '#wc-mb-domain' ) . show ( ) ;
} ) ;
} ) ;
$ ( '#wc-mb-domain' ) . on ( 'change' , function ( ) {
$ ( '#wc-mb-load' ) . prop ( 'disabled' , ! $ ( this ) . val ( ) ) ;
} ) ;
const loadMailboxes = ( ) => {
currentServerId = $ ( '#wc-mb-server' ) . val ( ) ;
currentDomain = $ ( '#wc-mb-domain' ) . val ( ) ;
if ( ! currentServerId || ! currentDomain ) return ;
$ ( '#wc-mb-table-wrap' ) . html ( '<p>Loading…</p>' ) ;
ajax ( 'woocow_admin_mailboxes' , { server _id : currentServerId , domain : currentDomain } ) . done ( res => {
if ( ! res . success ) {
$ ( '#wc-mb-table-wrap' ) . html ( ` <div class="notice notice-error"><p> ${ esc ( res . data ) } </p></div> ` ) ;
return ;
}
const boxes = res . data . mailboxes || [ ] ;
const webmail = res . data . webmail _url ;
if ( ! boxes . length ) {
$ ( '#wc-mb-table-wrap' ) . html ( '<p>No mailboxes found for this domain.</p>' ) ;
} else {
let html = ` <table class="wp-list-table widefat fixed striped woocow-table">
< thead > < tr >
< th > Email < / t h > < t h > N a m e < / t h > < t h > Q u o t a U s e d < / t h > < t h > Q u o t a M a x < / t h > < t h > A c t i v e < / t h > < t h > A c t i o n s < / t h >
< / t r > < / t h e a d > < t b o d y > ` ;
boxes . forEach ( m => {
2026-02-27 08:21:46 +01:00
const pct = m . percent _in _use || 0 ;
const used = formatMB ( m . quota _used ) ;
const max = formatMB ( m . quota ) ;
const bar = ` <div class="woocow-quota-bar"><div style="width: ${ pct } %"></div></div> ` ;
const quotaMB = Math . round ( ( m . quota || 0 ) / 1024 / 1024 ) ;
2026-02-27 08:06:22 +01:00
html += ` <tr>
< td > < a href = "${esc(webmail)}" target = "_blank" > $ { esc ( m . username ) } < / a > < / t d >
< td > $ { esc ( m . name ) } < / t d >
< td > $ { used } $ { bar } < / t d >
< td > $ { max } < / t d >
< td > $ { m . active == 1 ? '✓' : '– ' } < / t d >
< td class = "woocow-actions" >
feat: domains, transports, logs, quarantine, spam filter, i18n + UX fixes
Features added:
- Admin > Domains: add domains to Mailcow servers, auto-generate DKIM,
display full DNS record set (MX, SPF, DMARC, DKIM, autoconfig CNAMEs)
with one-click copy per record
- Admin > Transports: manage sender-dependent relay hosts (add/delete)
- Admin > Logs: view Postfix, Dovecot, Rspamd, Ratelimit, API and other
server logs in a dark scrollable panel
- My Account: per-domain Quarantine panel — view score, sender, subject,
date; permanently delete quarantined messages
- My Account: per-mailbox Spam Filter slider (1–15 threshold) saved via API
- My Account: Aliases & Forwarders (alias creation doubles as forwarder
to any external address)
UX fixes:
- Quota 0 now displays ∞ (unlimited) in both admin and account views
- Admin mailbox action buttons replaced with Dashicon icon buttons
(lock, chart-bar, trash) with title tooltips
i18n:
- load_plugin_textdomain registered on init hook
- All user-facing PHP strings wrapped in __() / esc_html__()
- Translated strings array passed to account JS via wp_localize_script
- woocow-es_ES.po/.mo — Spanish translation
- woocow-ro_RO.po/.mo — Romanian translation (with correct plural forms)
- English remains the fallback
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-27 08:38:52 +01:00
< button class = "button button-small woocow-icon-btn wc-mb-reset-pw"
title = "Reset Password" data - email = "${esc(m.username)}" >
< span class = "dashicons dashicons-lock" > < / s p a n >
< / b u t t o n >
< button class = "button button-small woocow-icon-btn wc-mb-set-quota"
title = "Set Quota" data - email = "${esc(m.username)}" data - quota = "${quotaMB}" >
< span class = "dashicons dashicons-chart-bar" > < / s p a n >
< / b u t t o n >
< button class = "button button-small woocow-icon-btn wc-mb-del"
title = "Delete" data - email = "${esc(m.username)}" style = "color:#a00" >
< span class = "dashicons dashicons-trash" > < / s p a n >
< / b u t t o n >
2026-02-27 08:06:22 +01:00
< / t d >
< / t r > ` ;
} ) ;
html += '</tbody></table>' ;
$ ( '#wc-mb-table-wrap' ) . html ( html ) ;
}
$ ( '#wc-mb-domain-label' ) . text ( currentDomain ) ;
$ ( '#wc-mb-create' ) . show ( ) ;
} ) ;
} ;
$ ( '#wc-mb-load' ) . on ( 'click' , loadMailboxes ) ;
// Create mailbox modal
$ ( '#wc-mb-create' ) . on ( 'click' , ( ) => {
$ ( '#wc-mb-local, #wc-mb-fullname, #wc-mb-pass, #wc-mb-pass2' ) . val ( '' ) ;
$ ( '#wc-mb-quota' ) . val ( 1024 ) ;
$ ( '#wc-mb-modal-notice' ) . text ( '' ) ;
$ ( '#wc-mb-modal' ) . show ( ) ;
} ) ;
$ ( '#wc-mb-modal-cancel' ) . on ( 'click' , ( ) => $ ( '#wc-mb-modal' ) . hide ( ) ) ;
$ ( document ) . on ( 'keydown' , e => { if ( e . key === 'Escape' ) $ ( '#wc-mb-modal' ) . hide ( ) ; } ) ;
$ ( '#wc-mb-modal-save' ) . on ( 'click' , ( ) => {
const data = {
server _id : currentServerId ,
domain : currentDomain ,
local _part : $ ( '#wc-mb-local' ) . val ( ) . trim ( ) ,
name : $ ( '#wc-mb-fullname' ) . val ( ) . trim ( ) ,
password : $ ( '#wc-mb-pass' ) . val ( ) ,
password2 : $ ( '#wc-mb-pass2' ) . val ( ) ,
quota : $ ( '#wc-mb-quota' ) . val ( ) ,
} ;
$ ( '#wc-mb-modal-notice' ) . text ( 'Creating…' ) ;
ajax ( 'woocow_admin_mailbox_create' , data ) . done ( res => {
if ( res . success ) {
$ ( '#wc-mb-modal' ) . hide ( ) ;
notice ( $ ( '#wc-mb-notices' ) , 'success' , ` Mailbox <strong> ${ esc ( res . data . email ) } </strong> created. ` ) ;
loadMailboxes ( ) ;
} else {
$ ( '#wc-mb-modal-notice' ) . html ( ` <span style="color:red"> ${ esc ( res . data ) } </span> ` ) ;
}
} ) ;
} ) ;
// Delete mailbox
$ ( document ) . on ( 'click' , '.wc-mb-del' , function ( ) {
const email = $ ( this ) . data ( 'email' ) ;
if ( ! confirm ( ` Delete mailbox ${ email } ? This cannot be undone. ` ) ) return ;
ajax ( 'woocow_admin_mailbox_delete' , { server _id : currentServerId , email } ) . done ( res => {
if ( res . success ) loadMailboxes ( ) ;
else notice ( $ ( '#wc-mb-notices' ) , 'error' , res . data ) ;
} ) ;
} ) ;
2026-02-27 08:21:46 +01:00
// ── Edit modal: Reset PW ──────────────────────────────────────────────
let editEmail = '' ;
let editType = '' ;
const openEditModal = ( email , type , currentQuota ) => {
editEmail = email ;
editType = type ;
$ ( '#wc-mb-edit-subtitle' ) . text ( email ) ;
$ ( '#wc-mb-edit-notice' ) . text ( '' ) ;
$ ( '#wc-mb-edit-pw-section, #wc-mb-edit-quota-section' ) . hide ( ) ;
if ( type === 'password' ) {
$ ( '#wc-mb-edit-title' ) . text ( 'Reset Password' ) ;
$ ( '#wc-mb-edit-pass, #wc-mb-edit-pass2' ) . val ( '' ) ;
$ ( '#wc-mb-edit-pw-section' ) . show ( ) ;
setTimeout ( ( ) => $ ( '#wc-mb-edit-pass' ) . trigger ( 'focus' ) , 100 ) ;
} else {
$ ( '#wc-mb-edit-title' ) . text ( 'Set Quota' ) ;
$ ( '#wc-mb-edit-quota' ) . val ( currentQuota || 1024 ) ;
$ ( '#wc-mb-edit-quota-section' ) . show ( ) ;
setTimeout ( ( ) => $ ( '#wc-mb-edit-quota' ) . trigger ( 'focus' ) , 100 ) ;
}
$ ( '#wc-mb-edit-modal' ) . show ( ) ;
} ;
$ ( document ) . on ( 'click' , '.wc-mb-reset-pw' , function ( ) {
openEditModal ( $ ( this ) . data ( 'email' ) , 'password' , null ) ;
} ) ;
$ ( document ) . on ( 'click' , '.wc-mb-set-quota' , function ( ) {
openEditModal ( $ ( this ) . data ( 'email' ) , 'quota' , $ ( this ) . data ( 'quota' ) ) ;
} ) ;
$ ( '#wc-mb-edit-cancel' ) . on ( 'click' , ( ) => $ ( '#wc-mb-edit-modal' ) . hide ( ) ) ;
$ ( document ) . on ( 'keydown' , e => { if ( e . key === 'Escape' ) $ ( '#wc-mb-edit-modal' ) . hide ( ) ; } ) ;
$ ( '#wc-mb-edit-save' ) . on ( 'click' , ( ) => {
const $note = $ ( '#wc-mb-edit-notice' ) . text ( 'Saving…' ) ;
const data = { server _id : currentServerId , email : editEmail , type : editType } ;
if ( editType === 'password' ) {
data . password = $ ( '#wc-mb-edit-pass' ) . val ( ) ;
data . password2 = $ ( '#wc-mb-edit-pass2' ) . val ( ) ;
if ( ! data . password ) { $note . html ( '<span style="color:red">Password cannot be empty.</span>' ) ; return ; }
if ( data . password !== data . password2 ) { $note . html ( '<span style="color:red">Passwords do not match.</span>' ) ; return ; }
} else {
data . quota = $ ( '#wc-mb-edit-quota' ) . val ( ) ;
if ( ! data . quota || data . quota < 1 ) { $note . html ( '<span style="color:red">Enter a valid quota.</span>' ) ; return ; }
}
ajax ( 'woocow_admin_mailbox_edit' , data ) . done ( res => {
if ( res . success ) {
$ ( '#wc-mb-edit-modal' ) . hide ( ) ;
const msg = editType === 'password' ? 'Password updated.' : 'Quota updated.' ;
notice ( $ ( '#wc-mb-notices' ) , 'success' , ` <strong> ${ esc ( editEmail ) } </strong> — ${ msg } ` ) ;
if ( editType === 'quota' ) loadMailboxes ( ) ; // refresh to show new quota
} else {
$note . html ( ` <span style="color:red"> ${ esc ( res . data ) } </span> ` ) ;
}
} ) ;
} ) ;
2026-02-27 08:06:22 +01:00
}
// ── Utilities ─────────────────────────────────────────────────────────────
function esc ( str ) {
return String ( str ) . replace ( /[&<>"']/g , m => ( {
'&' : '&' , '<' : '<' , '>' : '>' , '"' : '"' , "'" : '''
} ) [ m ] ) ;
}
function formatMB ( bytes ) {
feat: domains, transports, logs, quarantine, spam filter, i18n + UX fixes
Features added:
- Admin > Domains: add domains to Mailcow servers, auto-generate DKIM,
display full DNS record set (MX, SPF, DMARC, DKIM, autoconfig CNAMEs)
with one-click copy per record
- Admin > Transports: manage sender-dependent relay hosts (add/delete)
- Admin > Logs: view Postfix, Dovecot, Rspamd, Ratelimit, API and other
server logs in a dark scrollable panel
- My Account: per-domain Quarantine panel — view score, sender, subject,
date; permanently delete quarantined messages
- My Account: per-mailbox Spam Filter slider (1–15 threshold) saved via API
- My Account: Aliases & Forwarders (alias creation doubles as forwarder
to any external address)
UX fixes:
- Quota 0 now displays ∞ (unlimited) in both admin and account views
- Admin mailbox action buttons replaced with Dashicon icon buttons
(lock, chart-bar, trash) with title tooltips
i18n:
- load_plugin_textdomain registered on init hook
- All user-facing PHP strings wrapped in __() / esc_html__()
- Translated strings array passed to account JS via wp_localize_script
- woocow-es_ES.po/.mo — Spanish translation
- woocow-ro_RO.po/.mo — Romanian translation (with correct plural forms)
- English remains the fallback
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-27 08:38:52 +01:00
if ( bytes === 0 || bytes === '0' ) return '∞' ;
2026-02-27 08:06:22 +01:00
if ( ! bytes ) return '0 MB' ;
const mb = bytes / 1024 / 1024 ;
return mb >= 1024 ? ( mb / 1024 ) . toFixed ( 1 ) + ' GB' : mb . toFixed ( 0 ) + ' MB' ;
}
feat: domains, transports, logs, quarantine, spam filter, i18n + UX fixes
Features added:
- Admin > Domains: add domains to Mailcow servers, auto-generate DKIM,
display full DNS record set (MX, SPF, DMARC, DKIM, autoconfig CNAMEs)
with one-click copy per record
- Admin > Transports: manage sender-dependent relay hosts (add/delete)
- Admin > Logs: view Postfix, Dovecot, Rspamd, Ratelimit, API and other
server logs in a dark scrollable panel
- My Account: per-domain Quarantine panel — view score, sender, subject,
date; permanently delete quarantined messages
- My Account: per-mailbox Spam Filter slider (1–15 threshold) saved via API
- My Account: Aliases & Forwarders (alias creation doubles as forwarder
to any external address)
UX fixes:
- Quota 0 now displays ∞ (unlimited) in both admin and account views
- Admin mailbox action buttons replaced with Dashicon icon buttons
(lock, chart-bar, trash) with title tooltips
i18n:
- load_plugin_textdomain registered on init hook
- All user-facing PHP strings wrapped in __() / esc_html__()
- Translated strings array passed to account JS via wp_localize_script
- woocow-es_ES.po/.mo — Spanish translation
- woocow-ro_RO.po/.mo — Romanian translation (with correct plural forms)
- English remains the fallback
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-27 08:38:52 +01:00
// ── Domains Page ──────────────────────────────────────────────────────────
if ( $ ( '#wc-dom-server' ) . length ) {
let domServerId = null ;
let domServerUrl = null ;
2026-02-27 08:53:11 +01:00
let domainsData = { } ; // domain name → full data object
feat: domains, transports, logs, quarantine, spam filter, i18n + UX fixes
Features added:
- Admin > Domains: add domains to Mailcow servers, auto-generate DKIM,
display full DNS record set (MX, SPF, DMARC, DKIM, autoconfig CNAMEs)
with one-click copy per record
- Admin > Transports: manage sender-dependent relay hosts (add/delete)
- Admin > Logs: view Postfix, Dovecot, Rspamd, Ratelimit, API and other
server logs in a dark scrollable panel
- My Account: per-domain Quarantine panel — view score, sender, subject,
date; permanently delete quarantined messages
- My Account: per-mailbox Spam Filter slider (1–15 threshold) saved via API
- My Account: Aliases & Forwarders (alias creation doubles as forwarder
to any external address)
UX fixes:
- Quota 0 now displays ∞ (unlimited) in both admin and account views
- Admin mailbox action buttons replaced with Dashicon icon buttons
(lock, chart-bar, trash) with title tooltips
i18n:
- load_plugin_textdomain registered on init hook
- All user-facing PHP strings wrapped in __() / esc_html__()
- Translated strings array passed to account JS via wp_localize_script
- woocow-es_ES.po/.mo — Spanish translation
- woocow-ro_RO.po/.mo — Romanian translation (with correct plural forms)
- English remains the fallback
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-27 08:38:52 +01:00
$ ( '#wc-dom-server' ) . on ( 'change' , function ( ) {
domServerId = $ ( this ) . val ( ) ;
domServerUrl = $ ( this ) . find ( 'option:selected' ) . data ( 'url' ) ;
$ ( '#wc-dom-load' ) . prop ( 'disabled' , ! domServerId ) ;
$ ( '#wc-dom-add-btn, #wc-dom-table-wrap' ) . hide ( ) ;
2026-02-27 08:53:11 +01:00
domainsData = { } ;
feat: domains, transports, logs, quarantine, spam filter, i18n + UX fixes
Features added:
- Admin > Domains: add domains to Mailcow servers, auto-generate DKIM,
display full DNS record set (MX, SPF, DMARC, DKIM, autoconfig CNAMEs)
with one-click copy per record
- Admin > Transports: manage sender-dependent relay hosts (add/delete)
- Admin > Logs: view Postfix, Dovecot, Rspamd, Ratelimit, API and other
server logs in a dark scrollable panel
- My Account: per-domain Quarantine panel — view score, sender, subject,
date; permanently delete quarantined messages
- My Account: per-mailbox Spam Filter slider (1–15 threshold) saved via API
- My Account: Aliases & Forwarders (alias creation doubles as forwarder
to any external address)
UX fixes:
- Quota 0 now displays ∞ (unlimited) in both admin and account views
- Admin mailbox action buttons replaced with Dashicon icon buttons
(lock, chart-bar, trash) with title tooltips
i18n:
- load_plugin_textdomain registered on init hook
- All user-facing PHP strings wrapped in __() / esc_html__()
- Translated strings array passed to account JS via wp_localize_script
- woocow-es_ES.po/.mo — Spanish translation
- woocow-ro_RO.po/.mo — Romanian translation (with correct plural forms)
- English remains the fallback
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-27 08:38:52 +01:00
} ) ;
const loadDomains = ( ) => {
ajax ( 'woocow_server_domains' , { server _id : domServerId } ) . done ( res => {
if ( ! res . success ) {
notice ( $ ( '#wc-dom-notices' ) , 'error' , res . data ) ;
return ;
}
const domains = res . data ;
2026-02-27 08:53:11 +01:00
domainsData = { } ;
domains . forEach ( d => { domainsData [ d . domain ] = d ; } ) ;
feat: domains, transports, logs, quarantine, spam filter, i18n + UX fixes
Features added:
- Admin > Domains: add domains to Mailcow servers, auto-generate DKIM,
display full DNS record set (MX, SPF, DMARC, DKIM, autoconfig CNAMEs)
with one-click copy per record
- Admin > Transports: manage sender-dependent relay hosts (add/delete)
- Admin > Logs: view Postfix, Dovecot, Rspamd, Ratelimit, API and other
server logs in a dark scrollable panel
- My Account: per-domain Quarantine panel — view score, sender, subject,
date; permanently delete quarantined messages
- My Account: per-mailbox Spam Filter slider (1–15 threshold) saved via API
- My Account: Aliases & Forwarders (alias creation doubles as forwarder
to any external address)
UX fixes:
- Quota 0 now displays ∞ (unlimited) in both admin and account views
- Admin mailbox action buttons replaced with Dashicon icon buttons
(lock, chart-bar, trash) with title tooltips
i18n:
- load_plugin_textdomain registered on init hook
- All user-facing PHP strings wrapped in __() / esc_html__()
- Translated strings array passed to account JS via wp_localize_script
- woocow-es_ES.po/.mo — Spanish translation
- woocow-ro_RO.po/.mo — Romanian translation (with correct plural forms)
- English remains the fallback
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-27 08:38:52 +01:00
if ( ! domains . length ) {
$ ( '#wc-dom-table-wrap' ) . html ( '<p>No domains on this server yet.</p>' ) . show ( ) ;
} else {
let html = ` <table class="wp-list-table widefat fixed striped woocow-table">
2026-02-27 08:53:11 +01:00
< thead > < tr >
< th > Domain < / t h >
< th > Mailboxes < / t h >
< th > Quota Used < / t h >
< th > Active < / t h >
< th > Actions < / t h >
< / t r > < / t h e a d > < t b o d y > ` ;
feat: domains, transports, logs, quarantine, spam filter, i18n + UX fixes
Features added:
- Admin > Domains: add domains to Mailcow servers, auto-generate DKIM,
display full DNS record set (MX, SPF, DMARC, DKIM, autoconfig CNAMEs)
with one-click copy per record
- Admin > Transports: manage sender-dependent relay hosts (add/delete)
- Admin > Logs: view Postfix, Dovecot, Rspamd, Ratelimit, API and other
server logs in a dark scrollable panel
- My Account: per-domain Quarantine panel — view score, sender, subject,
date; permanently delete quarantined messages
- My Account: per-mailbox Spam Filter slider (1–15 threshold) saved via API
- My Account: Aliases & Forwarders (alias creation doubles as forwarder
to any external address)
UX fixes:
- Quota 0 now displays ∞ (unlimited) in both admin and account views
- Admin mailbox action buttons replaced with Dashicon icon buttons
(lock, chart-bar, trash) with title tooltips
i18n:
- load_plugin_textdomain registered on init hook
- All user-facing PHP strings wrapped in __() / esc_html__()
- Translated strings array passed to account JS via wp_localize_script
- woocow-es_ES.po/.mo — Spanish translation
- woocow-ro_RO.po/.mo — Romanian translation (with correct plural forms)
- English remains the fallback
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-27 08:38:52 +01:00
domains . forEach ( d => {
2026-02-27 08:53:11 +01:00
const mboxes = ` ${ d . mboxes _in } / ${ d . mailboxes || '∞' } ` ;
const qUsed = formatMB ( d . quota _used * 1024 * 1024 ) ;
const qTotal = d . quota ? formatMB ( d . quota * 1024 * 1024 ) : '∞' ;
const active = d . active == 1
? '<span class="woocow-badge woocow-badge-green">Active</span>'
: '<span class="woocow-badge woocow-badge-grey">Inactive</span>' ;
feat: domains, transports, logs, quarantine, spam filter, i18n + UX fixes
Features added:
- Admin > Domains: add domains to Mailcow servers, auto-generate DKIM,
display full DNS record set (MX, SPF, DMARC, DKIM, autoconfig CNAMEs)
with one-click copy per record
- Admin > Transports: manage sender-dependent relay hosts (add/delete)
- Admin > Logs: view Postfix, Dovecot, Rspamd, Ratelimit, API and other
server logs in a dark scrollable panel
- My Account: per-domain Quarantine panel — view score, sender, subject,
date; permanently delete quarantined messages
- My Account: per-mailbox Spam Filter slider (1–15 threshold) saved via API
- My Account: Aliases & Forwarders (alias creation doubles as forwarder
to any external address)
UX fixes:
- Quota 0 now displays ∞ (unlimited) in both admin and account views
- Admin mailbox action buttons replaced with Dashicon icon buttons
(lock, chart-bar, trash) with title tooltips
i18n:
- load_plugin_textdomain registered on init hook
- All user-facing PHP strings wrapped in __() / esc_html__()
- Translated strings array passed to account JS via wp_localize_script
- woocow-es_ES.po/.mo — Spanish translation
- woocow-ro_RO.po/.mo — Romanian translation (with correct plural forms)
- English remains the fallback
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-27 08:38:52 +01:00
html += ` <tr>
2026-02-27 08:53:11 +01:00
< td > < strong > $ { esc ( d . domain ) } < / s t r o n g > $ { d . d e s c r i p t i o n ? ` < b r > < s m a l l c l a s s = " d e s c r i p t i o n " > $ { e s c ( d . d e s c r i p t i o n ) } < / s m a l l > ` : ' ' } < / t d >
< td > $ { mboxes } < / t d >
< td > $ { qUsed } / $ { qTotal } < / t d >
< td > $ { active } < / t d >
< td class = "woocow-actions" >
< button class = "button button-small woocow-icon-btn wc-dom-dns"
title = "DNS Records" data - domain = "${esc(d.domain)}" >
< span class = "dashicons dashicons-admin-site" > < / s p a n >
< / b u t t o n >
< button class = "button button-small woocow-icon-btn wc-dom-edit"
title = "Edit" data - domain = "${esc(d.domain)}" >
< span class = "dashicons dashicons-edit" > < / s p a n >
< / b u t t o n >
< button class = "button button-small woocow-icon-btn wc-dom-del"
title = "Delete" data - domain = "${esc(d.domain)}" style = "color:#a00" >
< span class = "dashicons dashicons-trash" > < / s p a n >
feat: domains, transports, logs, quarantine, spam filter, i18n + UX fixes
Features added:
- Admin > Domains: add domains to Mailcow servers, auto-generate DKIM,
display full DNS record set (MX, SPF, DMARC, DKIM, autoconfig CNAMEs)
with one-click copy per record
- Admin > Transports: manage sender-dependent relay hosts (add/delete)
- Admin > Logs: view Postfix, Dovecot, Rspamd, Ratelimit, API and other
server logs in a dark scrollable panel
- My Account: per-domain Quarantine panel — view score, sender, subject,
date; permanently delete quarantined messages
- My Account: per-mailbox Spam Filter slider (1–15 threshold) saved via API
- My Account: Aliases & Forwarders (alias creation doubles as forwarder
to any external address)
UX fixes:
- Quota 0 now displays ∞ (unlimited) in both admin and account views
- Admin mailbox action buttons replaced with Dashicon icon buttons
(lock, chart-bar, trash) with title tooltips
i18n:
- load_plugin_textdomain registered on init hook
- All user-facing PHP strings wrapped in __() / esc_html__()
- Translated strings array passed to account JS via wp_localize_script
- woocow-es_ES.po/.mo — Spanish translation
- woocow-ro_RO.po/.mo — Romanian translation (with correct plural forms)
- English remains the fallback
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-27 08:38:52 +01:00
< / b u t t o n >
< / t d >
< / t r > ` ;
} ) ;
html += '</tbody></table>' ;
$ ( '#wc-dom-table-wrap' ) . html ( html ) . show ( ) ;
}
$ ( '#wc-dom-add-btn' ) . show ( ) ;
} ) ;
} ;
$ ( '#wc-dom-load' ) . on ( 'click' , loadDomains ) ;
$ ( '#wc-dom-add-btn' ) . on ( 'click' , ( ) => {
$ ( '#wc-dom-name, #wc-dom-desc' ) . val ( '' ) ;
$ ( '#wc-dom-form-notice' ) . text ( '' ) ;
$ ( '#wc-dom-form' ) . slideDown ( ) ;
} ) ;
$ ( '#wc-dom-cancel' ) . on ( 'click' , ( ) => $ ( '#wc-dom-form' ) . slideUp ( ) ) ;
$ ( '#wc-dom-save' ) . on ( 'click' , ( ) => {
const $note = $ ( '#wc-dom-form-notice' ) . text ( 'Adding domain…' ) ;
const data = {
server _id : domServerId ,
domain : $ ( '#wc-dom-name' ) . val ( ) . trim ( ) ,
description : $ ( '#wc-dom-desc' ) . val ( ) . trim ( ) ,
mailboxes : $ ( '#wc-dom-mailboxes' ) . val ( ) ,
aliases : $ ( '#wc-dom-aliases' ) . val ( ) ,
quota : $ ( '#wc-dom-quota' ) . val ( ) ,
defquota : $ ( '#wc-dom-defquota' ) . val ( ) ,
dkim _size : $ ( '#wc-dom-dkim-size' ) . val ( ) ,
} ;
if ( ! data . domain ) { $note . html ( '<span style="color:red">Domain name required.</span>' ) ; return ; }
ajax ( 'woocow_admin_domain_add' , data ) . done ( res => {
if ( res . success ) {
$note . html ( '<span style="color:green">✓ Domain added with DKIM generated!</span>' ) ;
$ ( '#wc-dom-form' ) . slideUp ( ) ;
loadDomains ( ) ;
} else {
$note . html ( ` <span style="color:red"> ${ esc ( res . data ) } </span> ` ) ;
}
} ) ;
} ) ;
2026-02-27 08:53:11 +01:00
// ── Edit Domain modal ─────────────────────────────────────────────────
$ ( document ) . on ( 'click' , '.wc-dom-edit' , function ( ) {
const domain = $ ( this ) . data ( 'domain' ) ;
const d = domainsData [ domain ] ;
if ( ! d ) return ;
$ ( '#wc-dom-edit-domain' ) . val ( domain ) ;
$ ( '#wc-dom-edit-name' ) . text ( domain ) ;
$ ( '#wc-dom-edit-desc' ) . val ( d . description || '' ) ;
$ ( '#wc-dom-edit-mboxes' ) . val ( d . mailboxes || 10 ) ;
$ ( '#wc-dom-edit-aliases' ) . val ( d . aliases || 400 ) ;
$ ( '#wc-dom-edit-quota' ) . val ( d . quota || 10240 ) ;
$ ( '#wc-dom-edit-defquota' ) . val ( d . defquota || 3072 ) ;
$ ( '#wc-dom-edit-rl-value' ) . val ( d . rl _value || 0 ) ;
$ ( '#wc-dom-edit-rl-frame' ) . val ( d . rl _frame || 's' ) ;
$ ( '#wc-dom-edit-active' ) . prop ( 'checked' , d . active == 1 ) ;
$ ( '#wc-dom-edit-notice' ) . text ( '' ) ;
// Load relayhosts into transport dropdown
const $sel = $ ( '#wc-dom-edit-relayhost' ) . html ( '<option value="0">— Direct delivery (no relay) —</option>' ) ;
ajax ( 'woocow_admin_relayhosts_list' , { server _id : domServerId } ) . done ( rh => {
if ( rh . success && Array . isArray ( rh . data ) ) {
rh . data . forEach ( r => {
$sel . append ( ` <option value=" ${ r . id } "> ${ esc ( r . hostname ) } </option> ` ) ;
} ) ;
$sel . val ( d . relayhost || '0' ) ;
}
} ) ;
$ ( '#wc-dom-edit-modal' ) . show ( ) ;
} ) ;
$ ( '#wc-dom-edit-cancel' ) . on ( 'click' , ( ) => $ ( '#wc-dom-edit-modal' ) . hide ( ) ) ;
$ ( '#wc-dom-edit-save' ) . on ( 'click' , ( ) => {
const $note = $ ( '#wc-dom-edit-notice' ) . text ( 'Saving…' ) ;
const domain = $ ( '#wc-dom-edit-domain' ) . val ( ) ;
ajax ( 'woocow_admin_domain_edit' , {
server _id : domServerId ,
domain ,
description : $ ( '#wc-dom-edit-desc' ) . val ( ) ,
mailboxes : $ ( '#wc-dom-edit-mboxes' ) . val ( ) ,
aliases : $ ( '#wc-dom-edit-aliases' ) . val ( ) ,
quota : $ ( '#wc-dom-edit-quota' ) . val ( ) ,
defquota : $ ( '#wc-dom-edit-defquota' ) . val ( ) ,
rl _value : $ ( '#wc-dom-edit-rl-value' ) . val ( ) ,
rl _frame : $ ( '#wc-dom-edit-rl-frame' ) . val ( ) ,
relayhost : $ ( '#wc-dom-edit-relayhost' ) . val ( ) ,
active : $ ( '#wc-dom-edit-active' ) . is ( ':checked' ) ? 1 : 0 ,
} ) . done ( res => {
if ( res . success ) {
$ ( '#wc-dom-edit-modal' ) . hide ( ) ;
notice ( $ ( '#wc-dom-notices' ) , 'success' , ` Domain <strong> ${ esc ( domain ) } </strong> updated. ` ) ;
loadDomains ( ) ;
} else {
$note . html ( ` <span style="color:red"> ${ esc ( res . data ) } </span> ` ) ;
}
} ) ;
} ) ;
// ── Delete Domain ─────────────────────────────────────────────────────
$ ( document ) . on ( 'click' , '.wc-dom-del' , function ( ) {
const domain = $ ( this ) . data ( 'domain' ) ;
if ( ! confirm ( ` Delete domain ${ domain } ? This will also delete all mailboxes, aliases, and data on the Mailcow server. This cannot be undone! ` ) ) return ;
ajax ( 'woocow_admin_domain_delete' , { server _id : domServerId , domain } ) . done ( res => {
if ( res . success ) {
notice ( $ ( '#wc-dom-notices' ) , 'success' , ` Domain <strong> ${ esc ( domain ) } </strong> deleted. ` ) ;
loadDomains ( ) ;
} else {
notice ( $ ( '#wc-dom-notices' ) , 'error' , res . data ) ;
}
} ) ;
} ) ;
feat: domains, transports, logs, quarantine, spam filter, i18n + UX fixes
Features added:
- Admin > Domains: add domains to Mailcow servers, auto-generate DKIM,
display full DNS record set (MX, SPF, DMARC, DKIM, autoconfig CNAMEs)
with one-click copy per record
- Admin > Transports: manage sender-dependent relay hosts (add/delete)
- Admin > Logs: view Postfix, Dovecot, Rspamd, Ratelimit, API and other
server logs in a dark scrollable panel
- My Account: per-domain Quarantine panel — view score, sender, subject,
date; permanently delete quarantined messages
- My Account: per-mailbox Spam Filter slider (1–15 threshold) saved via API
- My Account: Aliases & Forwarders (alias creation doubles as forwarder
to any external address)
UX fixes:
- Quota 0 now displays ∞ (unlimited) in both admin and account views
- Admin mailbox action buttons replaced with Dashicon icon buttons
(lock, chart-bar, trash) with title tooltips
i18n:
- load_plugin_textdomain registered on init hook
- All user-facing PHP strings wrapped in __() / esc_html__()
- Translated strings array passed to account JS via wp_localize_script
- woocow-es_ES.po/.mo — Spanish translation
- woocow-ro_RO.po/.mo — Romanian translation (with correct plural forms)
- English remains the fallback
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-27 08:38:52 +01:00
// DNS Records panel
$ ( document ) . on ( 'click' , '.wc-dom-dns' , function ( ) {
const domain = $ ( this ) . data ( 'domain' ) ;
$ ( '#wc-dom-dns-domain' ) . text ( domain ) ;
$ ( '#wc-dom-dns-content' ) . html ( '<p>Loading…</p>' ) ;
$ ( '#wc-dom-dns-panel' ) . show ( ) ;
$ ( 'html,body' ) . animate ( { scrollTop : $ ( '#wc-dom-dns-panel' ) . offset ( ) . top - 40 } , 300 ) ;
ajax ( 'woocow_admin_domain_dns' , { server _id : domServerId , domain } ) . done ( res => {
if ( ! res . success ) {
$ ( '#wc-dom-dns-content' ) . html ( ` <p style="color:red"> ${ esc ( res . data ) } </p> ` ) ;
return ;
}
const d = res . data ;
let html = ` <table class="wp-list-table widefat fixed striped woocow-table woocow-dns-table">
< thead > < tr > < th > Type < /th><th>Host / Name < / t h > < t h > V a l u e < / t h > < t h > P r i o r i t y < / t h > < t h > T T L < / t h > < t h > N o t e < / t h > < t h > < / t h > < / t r > < / t h e a d > < t b o d y > ` ;
d . records . forEach ( r => {
html += ` <tr>
< td > < code > $ { esc ( r . type ) } < / c o d e > < / t d >
< td class = "woocow-dns-host" > < code > $ { esc ( r . host ) } < / c o d e > < / t d >
< td class = "woocow-dns-val" > < code class = "woocow-dns-value" > $ { esc ( r . value ) } < / c o d e > < / t d >
< td > $ { esc ( r . prio ) } < / t d >
< td > $ { esc ( r . ttl ) } < / t d >
< td > < em > $ { esc ( r . note || '' ) } < / e m > < / t d >
< td > < button class = "button button-small wc-copy-dns" data - val = "${esc(r.value)}" > Copy < / b u t t o n > < / t d >
< / t r > ` ;
} ) ;
html += '</tbody></table>' ;
if ( ! d . dkim _txt ) {
html += ` <p class="description" style="color:orange">⚠ DKIM key not yet generated for this domain. Add the domain first, then view DNS records again.</p> ` ;
}
$ ( '#wc-dom-dns-content' ) . html ( html ) ;
} ) ;
} ) ;
$ ( document ) . on ( 'click' , '.wc-copy-dns' , function ( ) {
const val = $ ( this ) . data ( 'val' ) ;
navigator . clipboard . writeText ( val ) . then ( ( ) => {
$ ( this ) . text ( 'Copied!' ) ;
setTimeout ( ( ) => $ ( this ) . text ( 'Copy' ) , 1500 ) ;
} ) ;
} ) ;
$ ( '#wc-dom-dns-close' ) . on ( 'click' , ( ) => $ ( '#wc-dom-dns-panel' ) . hide ( ) ) ;
}
// ── Transports Page ───────────────────────────────────────────────────────
if ( $ ( '#wc-tr-server' ) . length ) {
let trServerId = null ;
$ ( '#wc-tr-server' ) . on ( 'change' , function ( ) {
trServerId = $ ( this ) . val ( ) ;
$ ( '#wc-tr-load' ) . prop ( 'disabled' , ! trServerId ) ;
} ) ;
const loadTransports = ( ) => {
ajax ( 'woocow_admin_relayhosts_list' , { server _id : trServerId } ) . done ( res => {
if ( ! res . success ) { notice ( $ ( '#wc-tr-notices' ) , 'error' , res . data ) ; return ; }
const rows = res . data ;
if ( ! Array . isArray ( rows ) || ! rows . length ) {
$ ( '#wc-tr-table-wrap' ) . html ( '<p>No transports configured on this server.</p>' ) ;
} else {
let html = ` <table class="wp-list-table widefat fixed striped woocow-table">
< thead > < tr > < th > Hostname : Port < / t h > < t h > U s e r n a m e < / t h > < t h > U s e d b y D o m a i n s < / t h > < t h > A c t i v e < / t h > < t h > A c t i o n s < / t h > < / t r > < / t h e a d > < t b o d y > ` ;
rows . forEach ( r => {
html += ` <tr>
< td > < code > $ { esc ( r . hostname ) } < / c o d e > < / t d >
< td > $ { esc ( r . username ) } < / t d >
< td > $ { esc ( r . used _by _domains || '—' ) } < / t d >
< td > $ { r . active == 1 ? '✓' : '– ' } < / t d >
< td > < button class = "button button-small woocow-icon-btn wc-tr-del" title = "Delete" data - id = "${r.id}" style = "color:#a00" >
< span class = "dashicons dashicons-trash" > < / s p a n >
< / b u t t o n > < / t d >
< / t r > ` ;
} ) ;
html += '</tbody></table>' ;
$ ( '#wc-tr-table-wrap' ) . html ( html ) ;
}
$ ( '#wc-tr-add-btn' ) . show ( ) ;
} ) ;
} ;
$ ( '#wc-tr-load' ) . on ( 'click' , loadTransports ) ;
$ ( '#wc-tr-add-btn' ) . on ( 'click' , ( ) => { $ ( '#wc-tr-hostname,#wc-tr-user,#wc-tr-pass' ) . val ( '' ) ; $ ( '#wc-tr-form' ) . slideDown ( ) ; } ) ;
$ ( '#wc-tr-cancel' ) . on ( 'click' , ( ) => $ ( '#wc-tr-form' ) . slideUp ( ) ) ;
$ ( '#wc-tr-save' ) . on ( 'click' , ( ) => {
const $note = $ ( '#wc-tr-form-notice' ) . text ( 'Saving…' ) ;
ajax ( 'woocow_admin_relayhost_save' , {
server _id : trServerId ,
hostname : $ ( '#wc-tr-hostname' ) . val ( ) . trim ( ) ,
username : $ ( '#wc-tr-user' ) . val ( ) . trim ( ) ,
password : $ ( '#wc-tr-pass' ) . val ( ) ,
active : $ ( '#wc-tr-active' ) . is ( ':checked' ) ? 1 : 0 ,
} ) . done ( res => {
if ( res . success ) { $note . html ( '<span style="color:green">✓ Transport added.</span>' ) ; $ ( '#wc-tr-form' ) . slideUp ( ) ; loadTransports ( ) ; }
else $note . html ( ` <span style="color:red"> ${ esc ( res . data ) } </span> ` ) ;
} ) ;
} ) ;
$ ( document ) . on ( 'click' , '.wc-tr-del' , function ( ) {
if ( ! confirm ( 'Delete this transport?' ) ) return ;
ajax ( 'woocow_admin_relayhost_delete' , { server _id : trServerId , id : $ ( this ) . data ( 'id' ) } ) . done ( res => {
if ( res . success ) loadTransports ( ) ;
else notice ( $ ( '#wc-tr-notices' ) , 'error' , res . data ) ;
} ) ;
} ) ;
}
// ── Logs Page ─────────────────────────────────────────────────────────────
if ( $ ( '#wc-log-server' ) . length ) {
$ ( '#wc-log-server' ) . on ( 'change' , function ( ) {
$ ( '#wc-log-load' ) . prop ( 'disabled' , ! $ ( this ) . val ( ) ) ;
} ) ;
feat: rich Rspamd history viewer with score badges and symbol details
- Rspamd log type renders a structured table instead of raw JSON
- Each row shows: timestamp, sender, recipient, subject, colour-coded
score (green/amber/red relative to required threshold), action badge
- Expand button reveals a detail panel per message with:
- Meta grid: Sender SMTP/MIME, IP, size, process time, Message-ID
- Threshold strip showing all configured score boundaries
- Symbol table sorted by impact, with dashicons (warning/check/minus),
+/- score in colour, description, and option values
- Zero-score informational symbols shown dimmed at the bottom
- Other log types (postfix, dovecot, etc.) still render as before
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-27 08:57:00 +01:00
// ── Rspamd history renderer ───────────────────────────────────────────
const rspamdActionBadge = ( action ) => {
const map = {
'no action' : [ 'woocow-badge-green' , 'No Action' ] ,
'add header' : [ 'woocow-badge-blue' , 'Add Header' ] ,
'rewrite subject' : [ 'woocow-badge-orange' , 'Rewrite Subject' ] ,
'greylist' : [ 'woocow-badge-orange' , 'Greylist' ] ,
'soft reject' : [ 'woocow-badge-orange' , 'Soft Reject' ] ,
'reject' : [ 'woocow-badge-red' , 'Reject' ] ,
} ;
const [ cls , label ] = map [ ( action || '' ) . toLowerCase ( ) ] || [ 'woocow-badge-grey' , esc ( action || '—' ) ] ;
return ` <span class="woocow-badge ${ cls } "> ${ label } </span> ` ;
} ;
const rspamdScoreClass = ( score , req ) => {
const pct = ( score || 0 ) / ( req || 25 ) ;
if ( pct < 0.3 ) return 'woocow-score-clean' ;
if ( pct < 0.7 ) return 'woocow-score-low' ;
return 'woocow-score-high' ;
} ;
const rspamdDetail = ( m ) => {
let html = '<div class="woocow-rspamd-symbols">' ;
// Meta grid
const size = m . size ? ( ( m . size / 1024 ) . toFixed ( 1 ) + ' KB' ) : '—' ;
const proc = m . time _real ? ( m . time _real . toFixed ( 3 ) + 's' ) : '—' ;
html += ` <div class="woocow-rspamd-meta">
< div class = "woocow-rspamd-meta-item" > < label > Sender SMTP < / l a b e l > $ { e s c ( m . s e n d e r _ s m t p | | ' — ' ) } < / d i v >
< div class = "woocow-rspamd-meta-item" > < label > Sender MIME < / l a b e l > $ { e s c ( m . s e n d e r _ m i m e | | ' — ' ) } < / d i v >
< div class = "woocow-rspamd-meta-item" > < label > IP < / l a b e l > $ { e s c ( m . i p | | ' — ' ) } < / d i v >
< div class = "woocow-rspamd-meta-item" > < label > Size < / l a b e l > $ { s i z e } < / d i v >
< div class = "woocow-rspamd-meta-item" > < label > Process Time < / l a b e l > $ { p r o c } < / d i v >
< div class = "woocow-rspamd-meta-item" > < label > Message - ID < / l a b e l > < s p a n s t y l e = " w o r d - b r e a k : b r e a k - a l l " > $ { e s c ( m [ ' m e s s a g e - i d ' ] | | ' — ' ) } < / s p a n > < / d i v >
< / d i v > ` ;
// Thresholds
if ( m . thresholds ) {
const parts = Object . entries ( m . thresholds )
. sort ( ( a , b ) => a [ 1 ] - b [ 1 ] )
. map ( ( [ k , v ] ) => ` <strong> ${ esc ( k ) } :</strong> ${ v } ` )
. join ( ' · ' ) ;
html += ` <p class="woocow-rspamd-thresholds">Thresholds: ${ parts } </p> ` ;
}
// Symbols table
if ( m . symbols ) {
const syms = Object . values ( m . symbols ) ;
const triggered = syms . filter ( s => ( s . metric _score || 0 ) !== 0 )
. sort ( ( a , b ) => Math . abs ( b . metric _score ) - Math . abs ( a . metric _score ) ) ;
const info = syms . filter ( s => ( s . metric _score || 0 ) === 0 )
. sort ( ( a , b ) => ( a . name || '' ) . localeCompare ( b . name || '' ) ) ;
html += ` <table class="woocow-rspamd-sym-table">
< thead > < tr >
< th style = "width:24px" > < / t h >
< th style = "width:30%" > Symbol < / t h >
< th style = "width:64px" > Score < / t h >
< th > Description < / t h >
< th > Options < / t h >
< / t r > < / t h e a d > < t b o d y > ` ;
[ ... triggered , ... info ] . forEach ( s => {
const ms = s . metric _score || 0 ;
const icon = ms > 0
? '<span class="dashicons dashicons-warning" style="font-size:14px;color:#c0392b"></span>'
: ms < 0
? '<span class="dashicons dashicons-yes-alt" style="font-size:14px;color:#27ae60"></span>'
: '<span class="dashicons dashicons-minus" style="font-size:14px;color:#bbb"></span>' ;
const cls = ms > 0 ? 'woocow-sym-score-pos' : ms < 0 ? 'woocow-sym-score-neg' : 'woocow-sym-score-zero' ;
const sign = ms > 0 ? '+' : '' ;
const opts = ( s . options || [ ] ) . join ( ', ' ) ;
const zero = ms === 0 ? ' sym-zero' : '' ;
html += ` <tr class=" ${ zero } ">
< td > $ { icon } < / t d >
< td class = "woocow-sym-name" > < code > $ { esc ( s . name ) } < / c o d e > < / t d >
< td > < span class = "${cls}" > $ { sign } $ { ms . toFixed ( 3 ) } < / s p a n > < / t d >
< td > $ { esc ( s . description || '' ) } < / t d >
< td style = "color:#888" > $ { esc ( opts ) } < / t d >
< / t r > ` ;
} ) ;
html += '</tbody></table>' ;
}
html += '</div>' ;
return html ;
} ;
const renderRspamdLog = ( entries ) => {
let html = ` <div class="woocow-log-toolbar">
< strong > Rspamd History < / s t r o n g >
< span style = "color:#666;font-size:12px" > $ { entries . length } messages ( click row for details ) < / s p a n >
< / d i v >
< table class = "wp-list-table widefat fixed striped woocow-rspamd-table woocow-table" >
< thead > < tr >
< th style = "width:130px" > Time < / t h >
< th > From < / t h >
< th > To < / t h >
< th > Subject < / t h >
< th style = "width:90px" > Score < / t h >
< th style = "width:110px" > Action < / t h >
< th style = "width:34px" > < / t h >
< / t r > < / t h e a d > < t b o d y > ` ;
entries . forEach ( ( m , i ) => {
const dt = m . unix _time ? new Date ( m . unix _time * 1000 ) . toLocaleString ( ) : '—' ;
const from = m . sender _mime || m . sender _smtp || '—' ;
const to = ( m . rcpt _mime || m . rcpt _smtp || [ ] ) . join ( ', ' ) || '—' ;
const subj = m . subject || '—' ;
const req = m . required _score || 25 ;
const scr = ( m . score || 0 ) . toFixed ( 2 ) ;
const cls = rspamdScoreClass ( m . score , req ) ;
html += ` <tr>
< td style = "font-size:11px;white-space:nowrap" > $ { esc ( dt ) } < / t d >
< td style = "font-size:11px" > $ { esc ( from ) } < / t d >
< td style = "font-size:11px" > $ { esc ( to ) } < / t d >
< td style = "font-size:12px" > $ { esc ( subj ) } < / t d >
< td > < span class = "woocow-score ${cls}" > $ { scr } < span style = "opacity:.6;font-weight:400" > / $ { r e q } < / s p a n > < / s p a n > < / t d >
< td > $ { rspamdActionBadge ( m . action ) } < / t d >
< td > < button class = "button button-small woocow-icon-btn wc-rspamd-toggle" data - idx = "${i}" title = "Details" >
< span class = "dashicons dashicons-arrow-down-alt2" > < / s p a n >
< / b u t t o n > < / t d >
< / t r >
< tr class = "woocow-rspamd-detail" id = "woocow-rspamd-${i}" style = "display:none" >
< td colspan = "7" > $ { rspamdDetail ( m ) } < / t d >
< / t r > ` ;
} ) ;
html += '</tbody></table>' ;
$ ( '#wc-log-wrap' ) . html ( html ) ;
} ;
$ ( document ) . on ( 'click' , '.wc-rspamd-toggle' , function ( ) {
const i = $ ( this ) . data ( 'idx' ) ;
const $det = $ ( ` #woocow-rspamd- ${ i } ` ) ;
const $ico = $ ( this ) . find ( '.dashicons' ) ;
if ( $det . is ( ':visible' ) ) {
$det . hide ( ) ;
$ico . removeClass ( 'dashicons-arrow-up-alt2' ) . addClass ( 'dashicons-arrow-down-alt2' ) ;
} else {
$det . show ( ) ;
$ico . removeClass ( 'dashicons-arrow-down-alt2' ) . addClass ( 'dashicons-arrow-up-alt2' ) ;
}
} ) ;
2026-02-27 09:47:49 +01:00
// ── Ratelimited log renderer ──────────────────────────────────────────
const renderRatelimitLog = ( entries ) => {
let html = ` <div class="woocow-log-toolbar">
< strong > Rate - Limited Messages < / s t r o n g >
< span style = "color:#666;font-size:12px" > $ { entries . length } entries < / s p a n >
< / d i v >
< table class = "wp-list-table widefat fixed striped woocow-table" >
< thead > < tr >
< th style = "width:130px" > Time < / t h >
< th > Sender < / t h >
< th > Recipient < / t h >
< th > Subject < / t h >
< th style = "width:110px" > IP < / t h >
< th style = "width:120px" > Rate Limit Rule < / t h >
< th style = "width:110px" > Queue ID < / t h >
< / t r > < / t h e a d > < t b o d y > ` ;
entries . forEach ( e => {
const dt = e . time ? new Date ( e . time * 1000 ) . toLocaleString ( ) : '—' ;
const sender = e . header _from || e . from || '—' ;
const subject = e . header _subject || '—' ;
html += ` <tr>
< td style = "font-size:11px;white-space:nowrap" > $ { esc ( dt ) } < / t d >
< td style = "font-size:11px" > $ { esc ( sender ) } < / t d >
< td style = "font-size:11px" > $ { esc ( e . rcpt || '—' ) } < / t d >
< td style = "font-size:12px" > $ { esc ( subject ) } < / t d >
< td > < code style = "font-size:11px" > $ { esc ( e . ip || '—' ) } < / c o d e > < / t d >
< td >
< span class = "woocow-badge woocow-badge-orange" title = "${esc(e.rl_info || '')}" > $ { esc ( e . rl _name || '—' ) } < / s p a n >
< / t d >
< td > < code style = "font-size:11px" > $ { esc ( e . qid || '—' ) } < / c o d e > < / t d >
< / t r > ` ;
} ) ;
html += '</tbody></table>' ;
$ ( '#wc-log-wrap' ) . html ( html ) ;
} ;
feat: rich log viewers for all remaining log types
- Syslog-style table (postfix, dovecot, sogo, netfilter, acme):
Time | Priority badge (colour-coded debug→info→notice→warn→error→crit)
| Process | Message — columns hidden when unused by that log type
- API access log: Time | Method badge (GET=green, POST=blue,
PUT=orange, DELETE=red) | Endpoint | Remote IP | Data
- Autodiscover: Time | User | Service badge (ActiveSync, CalDAV,
CardDAV, IMAP, SMTP) | User Agent
- Watchdog: Time | Service | Health badge (Healthy/Degraded/Critical
based on lvl) | Processes (now/total) | Change (+/-/±0 coloured)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-27 09:52:17 +01:00
// ── Syslog-style renderer (postfix, dovecot, sogo, netfilter, acme) ──
const priorityBadge = ( priority ) => {
const p = ( priority || 'info' ) . toLowerCase ( ) ;
const styles = {
debug : 'background:#f0f0f0;color:#888' ,
info : 'background:#e9ecef;color:#495057' ,
notice : 'background:#cce5ff;color:#004085' ,
warn : 'background:#fff3cd;color:#856404' ,
warning : 'background:#fff3cd;color:#856404' ,
err : 'background:#f8d7da;color:#721c24' ,
error : 'background:#f8d7da;color:#721c24' ,
crit : 'background:#721c24;color:#fff' ,
critical : 'background:#721c24;color:#fff' ,
alert : 'background:#721c24;color:#fff' ,
emerg : 'background:#1a1a1a;color:#fff' ,
} ;
const style = styles [ p ] || 'background:#e9ecef;color:#495057' ;
const label = p . charAt ( 0 ) . toUpperCase ( ) + p . slice ( 1 ) ;
return ` <span class="woocow-badge" style=" ${ style } "> ${ label } </span> ` ;
} ;
const renderSyslogTable = ( entries , label ) => {
const hasProgram = entries . some ( e => e . program ) ;
const hasPriority = entries . some ( e => e . priority ) ;
let html = ` <div class="woocow-log-toolbar">
< strong > $ { esc ( label ) } < / s t r o n g >
< span style = "color:#666;font-size:12px" > $ { entries . length } entries < / s p a n >
< / d i v >
< table class = "wp-list-table widefat fixed striped woocow-table" >
< thead > < tr >
< th style = "width:140px" > Time < / t h >
$ { hasPriority ? '<th style="width:80px">Priority</th>' : '' }
$ { hasProgram ? '<th style="width:150px">Process</th>' : '' }
< th > Message < / t h >
< / t r > < / t h e a d > < t b o d y > ` ;
entries . forEach ( e => {
const dt = e . time ? new Date ( parseInt ( e . time ) * 1000 ) . toLocaleString ( ) : '—' ;
html += ` <tr>
< td style = "font-size:11px;white-space:nowrap" > $ { esc ( dt ) } < / t d >
$ { hasPriority ? ` <td> ${ priorityBadge ( e . priority ) } </td> ` : '' }
$ { hasProgram ? ` <td><code style="font-size:11px"> ${ esc ( e . program || '—' ) } </code></td> ` : '' }
< td style = "font-size:12px;word-break:break-word" > $ { esc ( e . message || '—' ) } < / t d >
< / t r > ` ;
} ) ;
html += '</tbody></table>' ;
$ ( '#wc-log-wrap' ) . html ( html ) ;
} ;
// ── API access log renderer ───────────────────────────────────────────
const renderApiLog = ( entries ) => {
const methodBadge = ( method ) => {
const colors = { GET : '#27ae60' , POST : '#2271b1' , PUT : '#e67e22' , DELETE : '#c0392b' , PATCH : '#8e44ad' } ;
const bg = colors [ ( method || '' ) . toUpperCase ( ) ] || '#555' ;
return ` <span style="display:inline-block;padding:1px 7px;border-radius:3px;background: ${ bg } ;color:#fff;font-size:11px;font-weight:700;font-family:monospace"> ${ esc ( method || '?' ) } </span> ` ;
} ;
let html = ` <div class="woocow-log-toolbar">
< strong > API Access Log < / s t r o n g >
< span style = "color:#666;font-size:12px" > $ { entries . length } requests < / s p a n >
< / d i v >
< table class = "wp-list-table widefat fixed striped woocow-table" >
< thead > < tr >
< th style = "width:140px" > Time < / t h >
< th style = "width:72px" > Method < / t h >
< th > Endpoint < / t h >
< th style = "width:110px" > Remote IP < / t h >
< th style = "width:160px" > Data < / t h >
< / t r > < / t h e a d > < t b o d y > ` ;
entries . forEach ( e => {
const dt = e . time ? new Date ( e . time * 1000 ) . toLocaleString ( ) : '—' ;
html += ` <tr>
< td style = "font-size:11px;white-space:nowrap" > $ { esc ( dt ) } < / t d >
< td > $ { methodBadge ( e . method ) } < / t d >
< td > < code style = "font-size:11px" > $ { esc ( e . uri || '—' ) } < / c o d e > < / t d >
< td > < code style = "font-size:11px" > $ { esc ( e . remote || '—' ) } < / c o d e > < / t d >
< td style = "font-size:11px;color:#888;word-break:break-all" > $ { esc ( e . data || '—' ) } < / t d >
< / t r > ` ;
} ) ;
html += '</tbody></table>' ;
$ ( '#wc-log-wrap' ) . html ( html ) ;
} ;
// ── Autodiscover log renderer ─────────────────────────────────────────
const renderAutodiscoverLog = ( entries ) => {
const svcBadge = ( svc ) => {
const map = {
activesync : [ 'woocow-badge-blue' , 'ActiveSync' ] ,
caldav : [ 'woocow-badge-green' , 'CalDAV' ] ,
carddav : [ 'woocow-badge-green' , 'CardDAV' ] ,
imap : [ 'woocow-badge-grey' , 'IMAP' ] ,
smtp : [ 'woocow-badge-grey' , 'SMTP' ] ,
} ;
const [ cls , label ] = map [ ( svc || '' ) . toLowerCase ( ) ] || [ 'woocow-badge-grey' , esc ( svc || '—' ) ] ;
return ` <span class="woocow-badge ${ cls } "> ${ label } </span> ` ;
} ;
let html = ` <div class="woocow-log-toolbar">
< strong > Autodiscover Log < / s t r o n g >
< span style = "color:#666;font-size:12px" > $ { entries . length } requests < / s p a n >
< / d i v >
< table class = "wp-list-table widefat fixed striped woocow-table" >
< thead > < tr >
< th style = "width:140px" > Time < / t h >
< th > User < / t h >
< th style = "width:120px" > Service < / t h >
< th > User Agent < / t h >
< / t r > < / t h e a d > < t b o d y > ` ;
entries . forEach ( e => {
const dt = e . time ? new Date ( e . time * 1000 ) . toLocaleString ( ) : '—' ;
html += ` <tr>
< td style = "font-size:11px;white-space:nowrap" > $ { esc ( dt ) } < / t d >
< td style = "font-size:12px" > $ { esc ( e . user || '—' ) } < / t d >
< td > $ { svcBadge ( e . service ) } < / t d >
< td style = "font-size:11px;color:#666" > $ { esc ( e . ua || '—' ) } < / t d >
< / t r > ` ;
} ) ;
html += '</tbody></table>' ;
$ ( '#wc-log-wrap' ) . html ( html ) ;
} ;
// ── Watchdog log renderer ─────────────────────────────────────────────
const renderWatchdogLog = ( entries ) => {
const healthBadge = ( lvl ) => {
const n = parseInt ( lvl ) || 0 ;
if ( n >= 100 ) return '<span class="woocow-badge woocow-badge-green">Healthy</span>' ;
if ( n >= 50 ) return '<span class="woocow-badge woocow-badge-orange">Degraded</span>' ;
return '<span class="woocow-badge woocow-badge-red">Critical</span>' ;
} ;
let html = ` <div class="woocow-log-toolbar">
< strong > Watchdog Log < / s t r o n g >
< span style = "color:#666;font-size:12px" > $ { entries . length } entries < / s p a n >
< / d i v >
< table class = "wp-list-table widefat fixed striped woocow-table" >
< thead > < tr >
< th style = "width:140px" > Time < / t h >
< th style = "width:140px" > Service < / t h >
< th style = "width:90px" > Status < / t h >
< th style = "width:100px" > Processes < / t h >
< th style = "width:70px" > Change < / t h >
< / t r > < / t h e a d > < t b o d y > ` ;
entries . forEach ( e => {
const dt = e . time ? new Date ( parseInt ( e . time ) * 1000 ) . toLocaleString ( ) : '—' ;
const diff = parseInt ( e . hpdiff ) || 0 ;
const diffHtml = diff > 0
? ` <span style="color:#27ae60;font-weight:700">+ ${ diff } </span> `
: diff < 0
? ` <span style="color:#c0392b;font-weight:700"> ${ diff } </span> `
: ` <span style="color:#bbb">±0</span> ` ;
html += ` <tr>
< td style = "font-size:11px;white-space:nowrap" > $ { esc ( dt ) } < / t d >
< td > < strong > $ { esc ( e . service || '—' ) } < / s t r o n g > < / t d >
< td > $ { healthBadge ( e . lvl ) } < / t d >
< td style = "font-family:monospace" > $ { esc ( e . hpnow || '0' ) } / $ { esc ( e . hptotal || '0' ) } < / t d >
< td > $ { diffHtml } < / t d >
< / t r > ` ;
} ) ;
html += '</tbody></table>' ;
$ ( '#wc-log-wrap' ) . html ( html ) ;
} ;
feat: rich Rspamd history viewer with score badges and symbol details
- Rspamd log type renders a structured table instead of raw JSON
- Each row shows: timestamp, sender, recipient, subject, colour-coded
score (green/amber/red relative to required threshold), action badge
- Expand button reveals a detail panel per message with:
- Meta grid: Sender SMTP/MIME, IP, size, process time, Message-ID
- Threshold strip showing all configured score boundaries
- Symbol table sorted by impact, with dashicons (warning/check/minus),
+/- score in colour, description, and option values
- Zero-score informational symbols shown dimmed at the bottom
- Other log types (postfix, dovecot, etc.) still render as before
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-27 08:57:00 +01:00
// ── Log load ──────────────────────────────────────────────────────────
feat: domains, transports, logs, quarantine, spam filter, i18n + UX fixes
Features added:
- Admin > Domains: add domains to Mailcow servers, auto-generate DKIM,
display full DNS record set (MX, SPF, DMARC, DKIM, autoconfig CNAMEs)
with one-click copy per record
- Admin > Transports: manage sender-dependent relay hosts (add/delete)
- Admin > Logs: view Postfix, Dovecot, Rspamd, Ratelimit, API and other
server logs in a dark scrollable panel
- My Account: per-domain Quarantine panel — view score, sender, subject,
date; permanently delete quarantined messages
- My Account: per-mailbox Spam Filter slider (1–15 threshold) saved via API
- My Account: Aliases & Forwarders (alias creation doubles as forwarder
to any external address)
UX fixes:
- Quota 0 now displays ∞ (unlimited) in both admin and account views
- Admin mailbox action buttons replaced with Dashicon icon buttons
(lock, chart-bar, trash) with title tooltips
i18n:
- load_plugin_textdomain registered on init hook
- All user-facing PHP strings wrapped in __() / esc_html__()
- Translated strings array passed to account JS via wp_localize_script
- woocow-es_ES.po/.mo — Spanish translation
- woocow-ro_RO.po/.mo — Romanian translation (with correct plural forms)
- English remains the fallback
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-27 08:38:52 +01:00
$ ( '#wc-log-load' ) . on ( 'click' , ( ) => {
const sid = $ ( '#wc-log-server' ) . val ( ) ;
const type = $ ( '#wc-log-type' ) . val ( ) ;
$ ( '#wc-log-wrap' ) . html ( '<p>Loading…</p>' ) ;
ajax ( 'woocow_admin_logs' , { server _id : sid , log _type : type } ) . done ( res => {
if ( ! res . success ) {
$ ( '#wc-log-wrap' ) . html ( ` <p style="color:red"> ${ esc ( res . data ) } </p> ` ) ;
return ;
}
const entries = Array . isArray ( res . data ) ? res . data : Object . values ( res . data ) ;
if ( ! entries . length ) {
$ ( '#wc-log-wrap' ) . html ( '<p>No log entries.</p>' ) ;
return ;
}
feat: rich log viewers for all remaining log types
- Syslog-style table (postfix, dovecot, sogo, netfilter, acme):
Time | Priority badge (colour-coded debug→info→notice→warn→error→crit)
| Process | Message — columns hidden when unused by that log type
- API access log: Time | Method badge (GET=green, POST=blue,
PUT=orange, DELETE=red) | Endpoint | Remote IP | Data
- Autodiscover: Time | User | Service badge (ActiveSync, CalDAV,
CardDAV, IMAP, SMTP) | User Agent
- Watchdog: Time | Service | Health badge (Healthy/Degraded/Critical
based on lvl) | Processes (now/total) | Change (+/-/±0 coloured)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-27 09:52:17 +01:00
const typeLabel = $ ( '#wc-log-type option:selected' ) . text ( ) ;
2026-02-27 09:47:49 +01:00
if ( type === 'rspamd-history' ) { renderRspamdLog ( entries ) ; return ; }
if ( type === 'ratelimited' ) { renderRatelimitLog ( entries ) ; return ; }
feat: rich log viewers for all remaining log types
- Syslog-style table (postfix, dovecot, sogo, netfilter, acme):
Time | Priority badge (colour-coded debug→info→notice→warn→error→crit)
| Process | Message — columns hidden when unused by that log type
- API access log: Time | Method badge (GET=green, POST=blue,
PUT=orange, DELETE=red) | Endpoint | Remote IP | Data
- Autodiscover: Time | User | Service badge (ActiveSync, CalDAV,
CardDAV, IMAP, SMTP) | User Agent
- Watchdog: Time | Service | Health badge (Healthy/Degraded/Critical
based on lvl) | Processes (now/total) | Change (+/-/±0 coloured)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-27 09:52:17 +01:00
if ( type === 'api' ) { renderApiLog ( entries ) ; return ; }
if ( type === 'autodiscover' ) { renderAutodiscoverLog ( entries ) ; return ; }
if ( type === 'watchdog' ) { renderWatchdogLog ( entries ) ; return ; }
// postfix, dovecot, sogo, netfilter, acme → syslog table
renderSyslogTable ( entries , typeLabel + ' Log' ) ;
feat: domains, transports, logs, quarantine, spam filter, i18n + UX fixes
Features added:
- Admin > Domains: add domains to Mailcow servers, auto-generate DKIM,
display full DNS record set (MX, SPF, DMARC, DKIM, autoconfig CNAMEs)
with one-click copy per record
- Admin > Transports: manage sender-dependent relay hosts (add/delete)
- Admin > Logs: view Postfix, Dovecot, Rspamd, Ratelimit, API and other
server logs in a dark scrollable panel
- My Account: per-domain Quarantine panel — view score, sender, subject,
date; permanently delete quarantined messages
- My Account: per-mailbox Spam Filter slider (1–15 threshold) saved via API
- My Account: Aliases & Forwarders (alias creation doubles as forwarder
to any external address)
UX fixes:
- Quota 0 now displays ∞ (unlimited) in both admin and account views
- Admin mailbox action buttons replaced with Dashicon icon buttons
(lock, chart-bar, trash) with title tooltips
i18n:
- load_plugin_textdomain registered on init hook
- All user-facing PHP strings wrapped in __() / esc_html__()
- Translated strings array passed to account JS via wp_localize_script
- woocow-es_ES.po/.mo — Spanish translation
- woocow-ro_RO.po/.mo — Romanian translation (with correct plural forms)
- English remains the fallback
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-27 08:38:52 +01:00
} ) ;
} ) ;
}
2026-02-27 08:53:11 +01:00
// ── Admin Quarantine Page ─────────────────────────────────────────────────
if ( $ ( '#wc-quar-server' ) . length ) {
let quarServerId = null ;
$ ( '#wc-quar-server' ) . on ( 'change' , function ( ) {
quarServerId = $ ( this ) . val ( ) ;
$ ( '#wc-quar-load' ) . prop ( 'disabled' , ! quarServerId ) ;
} ) ;
const loadQuarantine = ( ) => {
$ ( '#wc-quar-wrap' ) . html ( '<p>Loading…</p>' ) ;
ajax ( 'woocow_admin_quarantine' , { server _id : quarServerId } ) . done ( res => {
if ( ! res . success ) {
$ ( '#wc-quar-wrap' ) . html ( ` <div class="notice notice-error"><p> ${ esc ( res . data ) } </p></div> ` ) ;
return ;
}
const msgs = Array . isArray ( res . data ) ? res . data : [ ] ;
if ( ! msgs . length ) {
$ ( '#wc-quar-wrap' ) . html ( '<p>No quarantined messages.</p>' ) ;
return ;
}
let html = ` <table class="wp-list-table widefat fixed striped woocow-quarantine-table">
< thead > < tr >
< th > Date < / t h > < t h > S e n d e r < / t h > < t h > R e c i p i e n t < / t h > < t h > S u b j e c t < / t h > < t h > S c o r e < / t h > < t h > A c t i o n s < / t h >
< / t r > < / t h e a d > < t b o d y > ` ;
msgs . forEach ( m => {
const date = m . created ? new Date ( m . created * 1000 ) . toLocaleString ( ) : '—' ;
const domain = ( m . rcpt || '' ) . split ( '@' ) [ 1 ] || '' ;
html += ` <tr>
< td > $ { esc ( date ) } < / t d >
< td > < code > $ { esc ( m . sender ) } < / c o d e > < / t d >
< td > $ { esc ( m . rcpt ) } < / t d >
< td > $ { esc ( m . subject ) } < / t d >
< td > $ { esc ( m . score ) } < / t d >
< td class = "woocow-actions" >
< button class = "button button-small woocow-icon-btn wc-quar-del"
title = "Delete" data - qid = "${m.id}" style = "color:#a00" >
< span class = "dashicons dashicons-trash" > < / s p a n >
< / b u t t o n >
< button class = "button button-small woocow-icon-btn wc-quar-block"
title = "Blacklist sender" data - sender = "${esc(m.sender)}" data - domain = "${esc(domain)}" >
< span class = "dashicons dashicons-shield-alt" > < / s p a n >
< / b u t t o n >
< / t d >
< / t r > ` ;
} ) ;
html += '</tbody></table>' ;
html += ` <p class="description"> ${ msgs . length } quarantined message(s). <em>Note: To release a message to inbox, use the link in the quarantine notification email or Webmail.</em></p> ` ;
$ ( '#wc-quar-wrap' ) . html ( html ) ;
} ) ;
} ;
$ ( '#wc-quar-load' ) . on ( 'click' , loadQuarantine ) ;
$ ( document ) . on ( 'click' , '.wc-quar-del' , function ( ) {
if ( ! confirm ( 'Permanently delete this quarantined message?' ) ) return ;
const qid = $ ( this ) . data ( 'qid' ) ;
ajax ( 'woocow_admin_quarantine_delete' , { server _id : quarServerId , qid } ) . done ( res => {
if ( res . success ) loadQuarantine ( ) ;
else notice ( $ ( '#wc-quar-notices' ) , 'error' , res . data ) ;
} ) ;
} ) ;
$ ( document ) . on ( 'click' , '.wc-quar-block' , function ( ) {
const sender = $ ( this ) . data ( 'sender' ) ;
const domain = $ ( this ) . data ( 'domain' ) ;
if ( ! domain ) { notice ( $ ( '#wc-quar-notices' ) , 'error' , 'Could not determine recipient domain.' ) ; return ; }
if ( ! confirm ( ` Add ${ sender } to the blacklist for domain ${ domain } ? ` ) ) return ;
ajax ( 'woocow_admin_quarantine_block' , {
server _id : quarServerId ,
domain ,
object _from : sender ,
} ) . done ( res => {
if ( res . success ) notice ( $ ( '#wc-quar-notices' ) , 'success' , ` Sender <strong> ${ esc ( sender ) } </strong> blacklisted for <strong> ${ esc ( domain ) } </strong>. ` ) ;
else notice ( $ ( '#wc-quar-notices' ) , 'error' , res . data ) ;
} ) ;
} ) ;
}
2026-02-27 08:06:22 +01:00
} ) ( jQuery ) ;