commit 2ee81efacf8f81969dc97004d58791802517361e Author: Malin Date: Fri Feb 27 08:06:22 2026 +0100 feat: initial WooCow plugin — Mailcow/WooCommerce integration - Mailcow API client wrapping domains, mailboxes, aliases endpoints - Admin backend: server management, customer-domain assignments, mailbox overview - WooCommerce My Account: email hosting tab with mailbox/alias management - Per-mailbox password change (independent of WP account password) - Optional WP account password sync to all customer mailboxes - Installer creates wp_woocow_servers and wp_woocow_assignments DB tables - Full nonce + capability + ownership verification on all AJAX endpoints Co-Authored-By: Claude Sonnet 4.6 diff --git a/assets/css/woocow.css b/assets/css/woocow.css new file mode 100644 index 0000000..208e0fd --- /dev/null +++ b/assets/css/woocow.css @@ -0,0 +1,346 @@ +/* ========================================================= + WooCow – Shared styles (admin + account) + ========================================================= */ + +/* ── Admin wrap ──────────────────────────────────────────── */ +.woocow-wrap h1 { margin-bottom: 8px; } +.woocow-version { font-size: 13px; font-weight: 400; color: #777; vertical-align: middle; } + +.woocow-toolbar { + display: flex; + gap: 10px; + align-items: center; + margin: 16px 0; +} +.woocow-flex { flex-wrap: wrap; } + +/* ── Cards ───────────────────────────────────────────────── */ +.woocow-card { + background: #fff; + border: 1px solid #ddd; + border-radius: 6px; + padding: 20px 24px; + margin-bottom: 20px; +} + +.woocow-form table.form-table th { width: 160px; } +.woocow-form-actions { + display: flex; + align-items: center; + gap: 10px; + margin-top: 16px; + padding-top: 16px; + border-top: 1px solid #f0f0f0; +} + +/* ── Dashboard cards ─────────────────────────────────────── */ +.woocow-dashboard-cards { + display: flex; + gap: 20px; + flex-wrap: wrap; + margin-top: 20px; +} +.woocow-dashboard-cards .woocow-card { + flex: 1; + min-width: 180px; + text-align: center; + padding: 30px 20px; +} +.woocow-card-number { + display: block; + font-size: 48px; + font-weight: 700; + color: #2c3e50; + line-height: 1; + margin-bottom: 8px; +} +.woocow-card-label { + display: block; + font-size: 14px; + color: #666; + margin-bottom: 16px; +} + +/* ── Tables ──────────────────────────────────────────────── */ +.woocow-table { margin-top: 12px; } +.woocow-table td.woocow-actions { white-space: nowrap; } +.woocow-table td.woocow-actions button { margin-right: 4px; } + +/* ── Badges ──────────────────────────────────────────────── */ +.woocow-badge { + display: inline-block; + padding: 2px 8px; + border-radius: 10px; + font-size: 12px; + font-weight: 600; +} +.woocow-badge-green { background: #d4edda; color: #155724; } +.woocow-badge-grey { background: #e9ecef; color: #495057; } + +/* ── Quota bar (admin) ───────────────────────────────────── */ +.woocow-quota-bar { + width: 80px; + height: 6px; + background: #eee; + border-radius: 3px; + overflow: hidden; + display: inline-block; + vertical-align: middle; + margin-left: 6px; +} +.woocow-quota-bar div { + height: 100%; + background: #27ae60; + border-radius: 3px; +} + +/* ── Customer autocomplete ───────────────────────────────── */ +.woocow-autocomplete { + position: absolute; + background: #fff; + border: 1px solid #ccc; + border-radius: 4px; + max-height: 220px; + overflow-y: auto; + z-index: 9999; + min-width: 320px; + box-shadow: 0 4px 12px rgba(0,0,0,.1); +} +.woocow-ac-item { + padding: 8px 12px; + cursor: pointer; + font-size: 13px; +} +.woocow-ac-item:hover { background: #f0f7ff; } +.woocow-selected-badge { + display: inline-block; + margin-left: 10px; + padding: 3px 10px; + background: #e8f4fd; + border-radius: 12px; + font-size: 12px; + color: #0073aa; + font-weight: 600; +} + +/* ── Modal (admin) ───────────────────────────────────────── */ +.woocow-modal { + position: fixed; + inset: 0; + background: rgba(0,0,0,.5); + z-index: 100000; + display: flex; + align-items: center; + justify-content: center; +} +.woocow-modal-box { + background: #fff; + border-radius: 8px; + padding: 28px 32px; + min-width: 420px; + max-width: 560px; + width: 90%; + box-shadow: 0 8px 30px rgba(0,0,0,.2); +} +.woocow-modal-box h3 { margin-top: 0; } +.woocow-modal-actions { + display: flex; + gap: 10px; + align-items: center; + margin-top: 20px; +} + +/* ── Flex inline ─────────────────────────────────────────── */ +.woocow-flex-inline { display: flex; align-items: center; gap: 6px; } +.woocow-at, .woocow-arrow { font-weight: 700; color: #555; } +.woocow-domain-label { font-weight: 600; color: #333; } + +/* ── Inline test result ──────────────────────────────────── */ +.wc-inline-test { font-size: 12px; } + +/* ============================================================= + My Account – Email Hosting + ============================================================= */ + +.woocow-account { max-width: 900px; } + +/* Domain panel */ +.woocow-domain-panel { + border: 1px solid #e0e0e0; + border-radius: 8px; + margin-bottom: 24px; + overflow: hidden; + background: #fff; +} + +.woocow-domain-header { + display: flex; + align-items: center; + gap: 12px; + padding: 14px 20px; + background: #f7f8fa; + border-bottom: 1px solid #e0e0e0; + flex-wrap: wrap; +} +.woocow-domain-name { + font-size: 17px; + font-weight: 700; + color: #1a1a2e; + flex: 1; + min-width: 140px; +} +.woocow-domain-server { + font-size: 12px; + color: #888; + margin-right: auto; +} + +.woocow-mailboxes-wrap { padding: 16px 20px; } + +/* Mailbox row */ +.woocow-mailbox-row { + border: 1px solid #ececec; + border-radius: 6px; + padding: 14px 16px; + margin-bottom: 12px; + background: #fafafa; + transition: box-shadow .2s; +} +.woocow-mailbox-row:hover { box-shadow: 0 2px 8px rgba(0,0,0,.07); } + +.woocow-mbox-main { + display: flex; + align-items: center; + gap: 16px; + flex-wrap: wrap; + margin-bottom: 10px; +} +.woocow-mbox-address { display: flex; align-items: center; gap: 8px; } +.woocow-mbox-icon { font-size: 18px; } +.woocow-mbox-name { font-size: 13px; color: #777; } + +.woocow-quota-wrap { display: flex; align-items: center; gap: 8px; } +.woocow-quota-bar-outer { + width: 120px; + height: 8px; + background: #e8e8e8; + border-radius: 4px; + overflow: hidden; +} +.woocow-quota-bar-inner { height: 100%; border-radius: 4px; transition: width .4s; } +.woocow-quota-text { font-size: 12px; color: #666; white-space: nowrap; } + +.woocow-mbox-actions { + display: flex; + gap: 8px; + flex-wrap: wrap; +} + +/* Alias section */ +.woocow-aliases-wrap { + margin-top: 12px; + padding-top: 12px; + border-top: 1px dashed #e0e0e0; +} +.woocow-alias-list { + list-style: none; + margin: 0 0 10px; + padding: 0; +} +.woocow-alias-list li { + display: flex; + align-items: center; + gap: 8px; + padding: 6px 0; + border-bottom: 1px solid #f0f0f0; + font-size: 13px; +} +.woocow-alias-list li:last-child { border-bottom: none; } +.woocow-alias-addr { font-weight: 600; } +.woocow-alias-goto { color: #666; } + +.woocow-alias-fields { + display: flex; + align-items: center; + gap: 8px; + flex-wrap: wrap; + margin-bottom: 8px; + padding: 12px; + background: #f0f7ff; + border-radius: 6px; +} + +/* Create mailbox form */ +.woocow-create-mbox-form { + margin-top: 16px; + padding: 20px; + background: #f7f9fc; + border: 1px dashed #bbd; + border-radius: 8px; +} +.woocow-create-mbox-form h4 { margin: 0 0 14px; } +.woocow-field-row { margin-bottom: 10px; } + +/* Buttons */ +.woocow-btn { + display: inline-flex; + align-items: center; + gap: 4px; + padding: 7px 14px; + border-radius: 5px; + border: none; + font-size: 13px; + font-weight: 600; + cursor: pointer; + text-decoration: none; + transition: opacity .15s, background .15s; + white-space: nowrap; + line-height: 1; +} +.woocow-btn:hover { opacity: .88; } +.woocow-btn-primary { background: #2271b1; color: #fff; } +.woocow-btn-outline { background: transparent; border: 1.5px solid #2271b1; color: #2271b1; } +.woocow-btn-ghost { background: transparent; border: 1.5px solid #bbb; color: #555; } +.woocow-btn-danger { background: #c0392b; color: #fff; } +.woocow-btn-sm { padding: 5px 10px; font-size: 12px; } +.woocow-btn-xs { padding: 3px 7px; font-size: 11px; } + +/* Inputs */ +.woocow-input { padding: 7px 10px; border: 1px solid #ccc; border-radius: 4px; font-size: 14px; width: 100%; max-width: 280px; box-sizing: border-box; } +.woocow-input-sm { width: 80px; padding: 5px 8px; border: 1px solid #ccc; border-radius: 4px; font-size: 13px; } + +/* Modal (account) */ +#woocow-pw-modal { + position: fixed; + inset: 0; + background: rgba(0,0,0,.55); + z-index: 99999; + display: flex; + align-items: center; + justify-content: center; +} +#woocow-pw-modal .woocow-modal-box { + background: #fff; + border-radius: 10px; + padding: 32px; + min-width: 340px; + max-width: 460px; + width: 92%; + box-shadow: 0 8px 40px rgba(0,0,0,.22); +} +#woocow-pw-modal h3 { margin: 0 0 4px; } +.woocow-modal-subtitle { color: #555; font-size: 14px; margin: 0 0 20px; } + +/* Misc */ +.woocow-loading { color: #888; font-style: italic; } +.woocow-muted { color: #aaa; font-size: 13px; } +.woocow-error { color: #c0392b; } + +@media (max-width: 600px) { + .woocow-domain-header, + .woocow-mbox-main, + .woocow-mbox-actions { flex-direction: column; align-items: flex-start; } + .woocow-quota-bar-outer { width: 80px; } + .woocow-alias-fields { flex-direction: column; } + .woocow-input { max-width: 100%; } +} diff --git a/assets/js/woocow-account.js b/assets/js/woocow-account.js new file mode 100644 index 0000000..bfb9b61 --- /dev/null +++ b/assets/js/woocow-account.js @@ -0,0 +1,281 @@ +/** + * WooCow – My Account frontend JavaScript + */ +(function ($) { + 'use strict'; + + if (!$('#woocow-account').length) return; + + const ajax = (action, data) => + $.post(woocowAcct.ajax_url, { action, nonce: woocowAcct.nonce, ...data }); + + const notice = (msg, type = 'success') => { + const $n = $('#woocow-acct-notices'); + $n.html(`
${msg}
`); + setTimeout(() => $n.find('> div').fadeOut(400, function () { $(this).remove(); }), 5000); + }; + + function esc(str) { + return String(str).replace(/[&<>"']/g, m => ({ + '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' + })[m]); + } + + function formatMB(bytes) { + if (!bytes) return '0 MB'; + const mb = bytes / 1024 / 1024; + return mb >= 1024 ? (mb / 1024).toFixed(1) + ' GB' : mb.toFixed(0) + ' MB'; + } + + // ── Load Mailboxes ──────────────────────────────────────────────────────── + + $(document).on('click', '.woocow-load-mailboxes', function () { + const $panel = $(this).closest('.woocow-domain-panel'); + const sid = $panel.data('server-id'); + const domain = $panel.data('domain'); + const $wrap = $panel.find('.woocow-mailboxes-wrap'); + const $list = $panel.find('.woocow-mailboxes-list'); + + $(this).prop('disabled', true).text('Loading…'); + $wrap.show(); + $list.html('

Fetching mailboxes…

'); + + ajax('woocow_acct_mailboxes', { server_id: sid, domain }).done(res => { + $(this).prop('disabled', false).text('Refresh'); + if (!res.success) { + $list.html(`

${esc(res.data)}

`); + return; + } + + const boxes = res.data.mailboxes || []; + const webmail = res.data.webmail_url; + + if (!boxes.length) { + $list.html('

No mailboxes yet. Create one below.

'); + return; + } + + let html = ''; + boxes.forEach(m => { + const pct = parseFloat(m.quota_used_in_percent || 0); + const used = formatMB(m.quota_used); + const max = formatMB(m.quota); + const col = pct > 85 ? '#e74c3c' : pct > 60 ? '#f39c12' : '#27ae60'; + + html += `
+
+
+ + ${esc(m.username)} + ${m.name ? `(${esc(m.name)})` : ''} +
+
+
+
+
+ ${used} / ${max} (${pct}%) +
+
+
+ + + + Webmail ↗ + +
+ +
`; + }); + + $list.html(html); + }); + }); + + // ── Aliases ─────────────────────────────────────────────────────────────── + + $(document).on('click', '.wc-toggle-aliases', function () { + const $row = $(this).closest('.woocow-mailbox-row'); + const $wrap = $row.find('.woocow-aliases-wrap'); + const sid = $(this).data('server'); + const domain = $(this).data('domain'); + + if ($wrap.is(':visible')) { + $wrap.slideUp(); + return; + } + + const $list = $row.find('.woocow-aliases-list').html('

Loading aliases…

'); + $wrap.slideDown(); + + ajax('woocow_acct_aliases', { server_id: sid, domain }).done(res => { + if (!res.success) { + $list.html(`

${esc(res.data)}

`); + return; + } + const aliases = res.data; + if (!aliases.length) { + $list.html('

No aliases for this domain yet.

'); + return; + } + let html = ''; + $list.html(html); + }); + }); + + $(document).on('click', '.wc-alias-add-btn', function () { + $(this).closest('.woocow-aliases-wrap').find('.woocow-alias-create-form').slideToggle(); + }); + + $(document).on('click', '.wc-alias-cancel', function () { + $(this).closest('.woocow-alias-create-form').slideUp(); + }); + + $(document).on('click', '.wc-alias-save', function () { + const $form = $(this).closest('.woocow-alias-create-form'); + const sid = $(this).data('server'); + const domain = $(this).data('domain'); + const addr = $form.find('.wc-alias-addr').val().trim(); + const goto_ = $form.find('.wc-alias-goto').val().trim(); + + if (!addr || !goto_) { alert('Both alias and destination are required.'); return; } + + ajax('woocow_acct_alias_create', { server_id: sid, domain, address: addr, goto: goto_ }).done(res => { + if (res.success) { + // Refresh alias list + $(this).closest('.woocow-mailbox-row').find('.wc-toggle-aliases').trigger('click'); + setTimeout(() => { $(this).closest('.woocow-mailbox-row').find('.wc-toggle-aliases').trigger('click'); }, 300); + $form.slideUp(); + } else { + alert('Error: ' + res.data); + } + }); + }); + + $(document).on('click', '.wc-alias-del', function () { + if (!confirm('Delete this alias?')) return; + const $li = $(this).closest('li'); + const sid = $(this).data('server'); + const domain = $(this).data('domain'); + const id = $(this).data('id'); + ajax('woocow_acct_alias_delete', { server_id: sid, domain, alias_id: id }).done(res => { + if (res.success) $li.fadeOut(300, function () { $(this).remove(); }); + else alert('Delete failed: ' + res.data); + }); + }); + + // ── Create Mailbox ──────────────────────────────────────────────────────── + + $(document).on('click', '.woocow-create-mbox-btn', function () { + const $panel = $(this).closest('.woocow-domain-panel'); + $panel.find('.woocow-create-mbox-form').slideToggle(); + }); + + $(document).on('click', '.wc-mbox-cancel', function () { + $(this).closest('.woocow-create-mbox-form').slideUp(); + }); + + $(document).on('click', '.wc-mbox-submit', function () { + const $panel = $(this).closest('.woocow-domain-panel'); + const sid = $panel.data('server-id'); + const domain = $panel.data('domain'); + const $form = $(this).closest('.woocow-create-mbox-form'); + const $note = $form.find('.wc-mbox-notice'); + + const local = $form.find('.wc-mbox-local').val().trim(); + const name = $form.find('.wc-mbox-name').val().trim(); + const pass = $form.find('.wc-mbox-pass').val(); + const pass2 = $form.find('.wc-mbox-pass2').val(); + const quota = $form.find('.wc-mbox-quota').val(); + + if (!local || !pass) { $note.html('Username and password required.'); return; } + if (pass !== pass2) { $note.html('Passwords do not match.'); return; } + + $note.text('Creating…'); + ajax('woocow_acct_mailbox_create', { server_id: sid, domain, local_part: local, name, password: pass, password2: pass2, quota }).done(res => { + if (res.success) { + $note.html('✓ Mailbox created!'); + $form.slideUp(); + // Refresh mailbox list + $panel.find('.woocow-load-mailboxes').trigger('click'); + } else { + $note.html(`${esc(res.data)}`); + } + }); + }); + + // ── Change Password Modal ───────────────────────────────────────────────── + + $(document).on('click', '.wc-change-pw', function () { + const sid = $(this).data('server'); + const domain = $(this).data('domain'); + const email = $(this).data('email'); + $('#woocow-pw-server-id').val(sid); + $('#woocow-pw-mailbox').val(email); + $('#woocow-pw-email').text(email); + $('#woocow-pw-new, #woocow-pw-new2').val(''); + $('#woocow-pw-notice').text(''); + $('#woocow-pw-modal').fadeIn(200); + // Store domain on modal for verification + $('#woocow-pw-modal').data('domain', domain); + }); + + $('#woocow-pw-cancel').on('click', () => $('#woocow-pw-modal').fadeOut(200)); + + $(document).on('click', '#woocow-pw-modal', function (e) { + if ($(e.target).is('#woocow-pw-modal')) $(this).fadeOut(200); + }); + + $('#woocow-pw-save').on('click', function () { + const sid = $('#woocow-pw-server-id').val(); + const email = $('#woocow-pw-mailbox').val(); + const domain = $('#woocow-pw-modal').data('domain'); + const pass = $('#woocow-pw-new').val(); + const pass2 = $('#woocow-pw-new2').val(); + const $note = $('#woocow-pw-notice'); + + if (!pass || pass !== pass2) { + $note.html('Passwords do not match or are empty.'); + return; + } + + $note.text('Updating…'); + ajax('woocow_acct_mailbox_password', { server_id: sid, domain, email, password: pass, password2: pass2 }).done(res => { + if (res.success) { + $note.html('✓ Password updated!'); + setTimeout(() => $('#woocow-pw-modal').fadeOut(200), 1500); + } else { + $note.html(`${esc(res.data)}`); + } + }); + }); + +})(jQuery); diff --git a/assets/js/woocow-admin.js b/assets/js/woocow-admin.js new file mode 100644 index 0000000..e59ed47 --- /dev/null +++ b/assets/js/woocow-admin.js @@ -0,0 +1,398 @@ +/** + * 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(`

${msg}

`); + 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('

No servers yet. Add one above.

'); + return; + } + let html = ` + + + `; + rows.forEach(s => { + const badge = s.active == 1 + ? 'Active' + : 'Inactive'; + html += ` + + + + + + `; + }); + html += '
NameURLStatusAddedActions
${esc(s.name)}${esc(s.url)}${badge}${s.created_at.split(' ')[0]} + + + +
'; + $('#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(`✓ Connected – Mailcow ${esc(res.data.version)}`); + } else { + $result.html(`✗ ${esc(res.data)}`); + } + }); + }); + + // Test from row + $(document).on('click', '.wc-srv-test', function () { + const id = $(this).data('id'); + const $td = $(this).closest('td'); + $td.append(' Testing…'); + ajax('woocow_server_test', { id }).done(res => { + $td.find('.wc-inline-test').html( + res.success + ? ` ✓ v${esc(res.data.version)}` + : ` ✗ ${esc(res.data)}` + ); + }); + }); + + // 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('

No assignments yet.

'); + $('#wc-assignments-loading').hide(); + return; + } + let html = ` + + + `; + rows.forEach(r => { + html += ` + + + + + + + `; + }); + html += '
CustomerEmailDomainServerAssignedActions
${esc(r.display_name)}${esc(r.user_email)}${esc(r.domain)}${esc(r.server_name)}${r.created_at.split(' ')[0]}
'; + $('#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(``); + }); + }); + + // Server → load domains + $('#wc-assign-server').on('change', function () { + const sid = $(this).val(); + $('#wc-assign-domain').html(''); + 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(''); + res.data.forEach(d => { + $('#wc-assign-domain').append(``); + }); + $('#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 += `
${esc(c.label)}
`; + }); + $('#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 ${esc(domain)} 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(''); + $('#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(``); + }); + $('#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('

Loading…

'); + ajax('woocow_admin_mailboxes', { server_id: currentServerId, domain: currentDomain }).done(res => { + if (!res.success) { + $('#wc-mb-table-wrap').html(`

${esc(res.data)}

`); + return; + } + const boxes = res.data.mailboxes || []; + const webmail = res.data.webmail_url; + + if (!boxes.length) { + $('#wc-mb-table-wrap').html('

No mailboxes found for this domain.

'); + } else { + let html = ` + + + `; + boxes.forEach(m => { + const pct = m.quota_used_in_percent || 0; + const used = formatMB(m.quota_used); + const max = formatMB(m.quota); + const bar = `
`; + html += ` + + + + + + + `; + }); + html += '
EmailNameQuota UsedQuota MaxActiveActions
${esc(m.username)}${esc(m.name)}${used} ${bar}${max}${m.active == 1 ? '✓' : '–'} + +
'; + $('#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 ${esc(res.data.email)} created.`); + loadMailboxes(); + } else { + $('#wc-mb-modal-notice').html(`${esc(res.data)}`); + } + }); + }); + + // 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); + }); + }); + } + + // ── Utilities ───────────────────────────────────────────────────────────── + + function esc(str) { + return String(str).replace(/[&<>"']/g, m => ({ + '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' + })[m]); + } + + function formatMB(bytes) { + if (!bytes) return '0 MB'; + const mb = bytes / 1024 / 1024; + return mb >= 1024 ? (mb / 1024).toFixed(1) + ' GB' : mb.toFixed(0) + ' MB'; + } + +})(jQuery); diff --git a/includes/class-woocow-account.php b/includes/class-woocow-account.php new file mode 100644 index 0000000..0d34e73 --- /dev/null +++ b/includes/class-woocow-account.php @@ -0,0 +1,458 @@ + admin_url( 'admin-ajax.php' ), + 'nonce' => wp_create_nonce( 'woocow_account' ), + ] ); + } + + // ── My Account page render ──────────────────────────────────────────────── + + public function render_page(): void { + if ( ! is_user_logged_in() ) { + echo '

' . esc_html__( 'Please log in to manage your email hosting.', 'woocow' ) . '

'; + return; + } + + $customer_id = get_current_user_id(); + $assignments = $this->get_customer_assignments( $customer_id ); + + if ( empty( $assignments ) ) { + echo '
' . esc_html__( 'You have no email domains assigned yet. Please contact support.', 'woocow' ) . '
'; + return; + } + ?> + + + + + get_results( $wpdb->prepare( " + SELECT a.id, a.server_id, a.domain, s.name AS server_name, s.url AS server_url + FROM {$wpdb->prefix}woocow_assignments a + JOIN {$wpdb->prefix}woocow_servers s ON s.id = a.server_id + WHERE a.customer_id = %d AND s.active = 1 + ORDER BY a.domain + ", $customer_id ) ); + + foreach ( $rows as $row ) { + $row->webmail_url = rtrim( $row->server_url, '/' ) . '/SOGo'; + } + return $rows; + } + + private function get_server( int $id ): ?object { + global $wpdb; + return $wpdb->get_row( $wpdb->prepare( + "SELECT * FROM {$wpdb->prefix}woocow_servers WHERE id = %d AND active = 1", + $id + ) ); + } + + /** Verify that the current user owns the given assignment. */ + private function verify_ownership( int $server_id, string $domain ): bool { + global $wpdb; + $found = $wpdb->get_var( $wpdb->prepare( " + SELECT id FROM {$wpdb->prefix}woocow_assignments + WHERE customer_id = %d AND server_id = %d AND domain = %s + ", get_current_user_id(), $server_id, $domain ) ); + return (bool) $found; + } + + private function account_verify(): void { + check_ajax_referer( 'woocow_account', 'nonce' ); + if ( ! is_user_logged_in() ) { + wp_send_json_error( 'Not logged in.', 401 ); + } + } + + // ── AJAX: Account ───────────────────────────────────────────────────────── + + public function ajax_woocow_acct_domains(): void { + $this->account_verify(); + $assignments = $this->get_customer_assignments( get_current_user_id() ); + wp_send_json_success( $assignments ); + } + + public function ajax_woocow_acct_mailboxes(): void { + $this->account_verify(); + + $server_id = absint( $_POST['server_id'] ?? 0 ); + $domain = sanitize_text_field( $_POST['domain'] ?? '' ); + + if ( ! $this->verify_ownership( $server_id, $domain ) ) { + wp_send_json_error( 'Access denied.', 403 ); + } + + $server = $this->get_server( $server_id ); + if ( ! $server ) { + wp_send_json_error( 'Server unavailable.' ); + } + + $api = WooCow_API::from_server( $server ); + $result = $api->get_domain_mailboxes( $domain ); + + if ( ! $result['success'] ) { + wp_send_json_error( $result['error'] ?? 'Could not load mailboxes.' ); + } + + wp_send_json_success( [ + 'mailboxes' => $result['data'] ?? [], + 'webmail_url' => $api->get_webmail_url(), + ] ); + } + + public function ajax_woocow_acct_mailbox_create(): void { + $this->account_verify(); + + $server_id = absint( $_POST['server_id'] ?? 0 ); + $domain = sanitize_text_field( $_POST['domain'] ?? '' ); + $local_part = sanitize_text_field( $_POST['local_part'] ?? '' ); + $name = sanitize_text_field( $_POST['name'] ?? '' ); + $password = $_POST['password'] ?? ''; + $password2 = $_POST['password2'] ?? ''; + $quota = absint( $_POST['quota'] ?? 1024 ); + + if ( ! $this->verify_ownership( $server_id, $domain ) ) { + wp_send_json_error( 'Access denied.', 403 ); + } + if ( ! $local_part || ! $password ) { + wp_send_json_error( 'Username and password are required.' ); + } + if ( $password !== $password2 ) { + wp_send_json_error( 'Passwords do not match.' ); + } + + $server = $this->get_server( $server_id ); + if ( ! $server ) { + wp_send_json_error( 'Server unavailable.' ); + } + + $api = WooCow_API::from_server( $server ); + $result = $api->create_mailbox( [ + 'local_part' => $local_part, + 'domain' => $domain, + 'name' => $name ?: $local_part, + 'password' => $password, + 'password2' => $password2, + 'quota' => $quota, + 'active' => 1, + 'force_pw_update' => 0, + 'tls_enforce_in' => 0, + 'tls_enforce_out' => 0, + ] ); + + if ( ! $result['success'] ) { + wp_send_json_error( $result['error'] ?? 'Failed to create mailbox.' ); + } + + wp_send_json_success( [ 'email' => $local_part . '@' . $domain ] ); + } + + public function ajax_woocow_acct_mailbox_password(): void { + $this->account_verify(); + + $server_id = absint( $_POST['server_id'] ?? 0 ); + $domain = sanitize_text_field( $_POST['domain'] ?? '' ); + $email = sanitize_email( $_POST['email'] ?? '' ); + $password = $_POST['password'] ?? ''; + $password2 = $_POST['password2'] ?? ''; + + if ( ! $this->verify_ownership( $server_id, $domain ) ) { + wp_send_json_error( 'Access denied.', 403 ); + } + if ( ! $password || $password !== $password2 ) { + wp_send_json_error( 'Passwords do not match or are empty.' ); + } + + // Validate that email belongs to the customer's domain. + if ( ! str_ends_with( $email, '@' . $domain ) ) { + wp_send_json_error( 'Email does not belong to your domain.' ); + } + + $server = $this->get_server( $server_id ); + if ( ! $server ) { + wp_send_json_error( 'Server unavailable.' ); + } + + $api = WooCow_API::from_server( $server ); + $result = $api->edit_mailbox( [ $email ], [ + 'password' => $password, + 'password2' => $password2, + ] ); + + if ( ! $result['success'] ) { + wp_send_json_error( $result['error'] ?? 'Failed to update password.' ); + } + + wp_send_json_success(); + } + + public function ajax_woocow_acct_aliases(): void { + $this->account_verify(); + + $server_id = absint( $_POST['server_id'] ?? 0 ); + $domain = sanitize_text_field( $_POST['domain'] ?? '' ); + + if ( ! $this->verify_ownership( $server_id, $domain ) ) { + wp_send_json_error( 'Access denied.', 403 ); + } + + $server = $this->get_server( $server_id ); + if ( ! $server ) { + wp_send_json_error( 'Server unavailable.' ); + } + + $api = WooCow_API::from_server( $server ); + $result = $api->get_all_aliases(); + + if ( ! $result['success'] ) { + wp_send_json_error( $result['error'] ?? 'Could not load aliases.' ); + } + + // Filter to this domain only. + $aliases = array_filter( (array) $result['data'], function ( $a ) use ( $domain ) { + return isset( $a['address'] ) && str_ends_with( $a['address'], '@' . $domain ); + } ); + + wp_send_json_success( array_values( $aliases ) ); + } + + public function ajax_woocow_acct_alias_create(): void { + $this->account_verify(); + + $server_id = absint( $_POST['server_id'] ?? 0 ); + $domain = sanitize_text_field( $_POST['domain'] ?? '' ); + $address = sanitize_email( $_POST['address'] ?? '' ); + $goto = sanitize_email( $_POST['goto'] ?? '' ); + + if ( ! $this->verify_ownership( $server_id, $domain ) ) { + wp_send_json_error( 'Access denied.', 403 ); + } + if ( ! $address || ! $goto ) { + wp_send_json_error( 'Alias address and destination are required.' ); + } + if ( ! str_ends_with( $address, '@' . $domain ) ) { + wp_send_json_error( 'Alias address must belong to your domain.' ); + } + + $server = $this->get_server( $server_id ); + if ( ! $server ) { + wp_send_json_error( 'Server unavailable.' ); + } + + $api = WooCow_API::from_server( $server ); + $result = $api->create_alias( [ + 'address' => $address, + 'goto' => $goto, + 'active' => 1, + ] ); + + if ( ! $result['success'] ) { + wp_send_json_error( $result['error'] ?? 'Failed to create alias.' ); + } + + wp_send_json_success(); + } + + public function ajax_woocow_acct_alias_delete(): void { + $this->account_verify(); + + $server_id = absint( $_POST['server_id'] ?? 0 ); + $domain = sanitize_text_field( $_POST['domain'] ?? '' ); + $alias_id = absint( $_POST['alias_id'] ?? 0 ); + + if ( ! $this->verify_ownership( $server_id, $domain ) ) { + wp_send_json_error( 'Access denied.', 403 ); + } + + $server = $this->get_server( $server_id ); + if ( ! $server ) { + wp_send_json_error( 'Server unavailable.' ); + } + + $api = WooCow_API::from_server( $server ); + $result = $api->delete_alias( $alias_id ); + + if ( ! $result['success'] ) { + wp_send_json_error( $result['error'] ?? 'Failed to delete alias.' ); + } + + wp_send_json_success(); + } + + // ── WP password change sync ─────────────────────────────────────────────── + + /** + * When a customer saves their WooCommerce account details with a new password, + * offer them the option to sync it to all their mailboxes. + * + * NOTE: This hook fires after the WP password has been updated. + * We only sync if the customer explicitly checked the option. + */ + public function maybe_sync_password( int $user_id ): void { + // Only proceed if the "sync to mailcow" checkbox was checked and a new password given. + if ( empty( $_POST['woocow_sync_pw'] ) || empty( $_POST['password_1'] ) ) { + return; + } + + $password = $_POST['password_1']; + global $wpdb; + + $assignments = $wpdb->get_results( $wpdb->prepare( " + SELECT a.server_id, a.domain, s.url, s.api_key + FROM {$wpdb->prefix}woocow_assignments a + JOIN {$wpdb->prefix}woocow_servers s ON s.id = a.server_id + WHERE a.customer_id = %d AND s.active = 1 + ", $user_id ) ); + + foreach ( $assignments as $assignment ) { + $api = new WooCow_API( $assignment->url, $assignment->api_key ); + $mboxes = $api->get_domain_mailboxes( $assignment->domain ); + if ( ! $mboxes['success'] ) { + continue; + } + foreach ( (array) ( $mboxes['data'] ?? [] ) as $mbox ) { + $api->edit_mailbox( [ $mbox['username'] ], [ + 'password' => $password, + 'password2' => $password, + ] ); + } + } + } +} diff --git a/includes/class-woocow-admin.php b/includes/class-woocow-admin.php new file mode 100644 index 0000000..a3d6aaa --- /dev/null +++ b/includes/class-woocow-admin.php @@ -0,0 +1,545 @@ + admin_url( 'admin-ajax.php' ), + 'nonce' => wp_create_nonce( 'woocow_admin' ), + ] ); + } + + // ── Page: Dashboard ────────────────────────────────────────────────────── + + public function page_dashboard(): void { + global $wpdb; + $servers = (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$wpdb->prefix}woocow_servers WHERE active=1" ); + $assignments = (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$wpdb->prefix}woocow_assignments" ); + $customers = (int) $wpdb->get_var( "SELECT COUNT(DISTINCT customer_id) FROM {$wpdb->prefix}woocow_assignments" ); + ?> +
+

WooCow v

+
+
+ + Active Servers + Manage +
+
+ + Customers with Email + Manage +
+
+ + Domain Assignments + View Mailboxes +
+
+
+ +
+

WooCow – Servers

+

Add your Mailcow server instances here. The API key must be a read-write key from Configuration → Access → Edit administrator details → API.

+ +
+ +
+
+ + + + + +
+

Loading servers…

+
+
+ +
+

WooCow – Domain Assignments

+

Assign one or more Mailcow domains to a WooCommerce customer. The customer can then manage mailboxes for those domains from My Account → Email Hosting.

+ +
+

Assign Domain to Customer

+ + + + + + + + + + + + + +
+ +
+ + +
+ +
+
+ +
+
+
+ +

Current Assignments

+
Loading…
+
+
+ get_results( "SELECT id, name FROM {$wpdb->prefix}woocow_servers WHERE active=1 ORDER BY name" ); + ?> +
+

WooCow – Mailboxes

+ +
+ + + + +
+ +
+
+ + + +
+ get_row( $wpdb->prepare( + "SELECT * FROM {$wpdb->prefix}woocow_servers WHERE id = %d", + $id + ) ); + } + + private function json_ok( $data = null ): void { + wp_send_json_success( $data ); + } + + private function json_err( string $msg ): void { + wp_send_json_error( $msg ); + } + + // ── AJAX: Servers ───────────────────────────────────────────────────────── + + public function ajax_woocow_servers_list(): void { + $this->verify(); + global $wpdb; + $rows = $wpdb->get_results( "SELECT id, name, url, active, created_at FROM {$wpdb->prefix}woocow_servers ORDER BY name" ); + $this->json_ok( $rows ); + } + + public function ajax_woocow_server_save(): void { + $this->verify(); + global $wpdb; + + $id = absint( $_POST['id'] ?? 0 ); + $name = sanitize_text_field( $_POST['name'] ?? '' ); + $url = esc_url_raw( $_POST['url'] ?? '' ); + $key = sanitize_text_field( $_POST['api_key'] ?? '' ); + $active = absint( $_POST['active'] ?? 1 ); + + if ( ! $name || ! $url || ! $key ) { + $this->json_err( 'Name, URL, and API key are required.' ); + } + + $data = compact( 'name', 'url', 'active' ) + [ 'api_key' => $key ]; + + if ( $id ) { + $wpdb->update( "{$wpdb->prefix}woocow_servers", $data, [ 'id' => $id ] ); + $this->json_ok( [ 'id' => $id ] ); + } else { + $wpdb->insert( "{$wpdb->prefix}woocow_servers", $data ); + $this->json_ok( [ 'id' => $wpdb->insert_id ] ); + } + } + + public function ajax_woocow_server_delete(): void { + $this->verify(); + global $wpdb; + $id = absint( $_POST['id'] ?? 0 ); + $wpdb->delete( "{$wpdb->prefix}woocow_servers", [ 'id' => $id ] ); + $wpdb->delete( "{$wpdb->prefix}woocow_assignments", [ 'server_id' => $id ] ); + $this->json_ok(); + } + + public function ajax_woocow_server_test(): void { + $this->verify(); + + $id = absint( $_POST['id'] ?? 0 ); + + // Allow testing before saving (pass url+key directly) + if ( $id ) { + $server = $this->get_server( $id ); + if ( ! $server ) { + $this->json_err( 'Server not found.' ); + } + $api = WooCow_API::from_server( $server ); + } else { + $url = esc_url_raw( $_POST['url'] ?? '' ); + $key = sanitize_text_field( $_POST['api_key'] ?? '' ); + if ( ! $url || ! $key ) { + $this->json_err( 'URL and API key required.' ); + } + $api = new WooCow_API( $url, $key ); + } + + $result = $api->test_connection(); + if ( $result['success'] ) { + $version = $result['data']['version'] ?? $result['data'][0]['version'] ?? 'unknown'; + $this->json_ok( [ 'version' => $version ] ); + } else { + $this->json_err( $result['error'] ?? 'Connection failed.' ); + } + } + + public function ajax_woocow_server_domains(): void { + $this->verify(); + $id = absint( $_POST['server_id'] ?? 0 ); + $server = $this->get_server( $id ); + if ( ! $server ) { + $this->json_err( 'Server not found.' ); + } + $api = WooCow_API::from_server( $server ); + $result = $api->get_domains(); + if ( ! $result['success'] ) { + $this->json_err( $result['error'] ?? 'Failed to fetch domains.' ); + } + $domains = array_map( fn( $d ) => [ 'domain' => $d['domain'], 'active' => $d['active'] ], (array) $result['data'] ); + $this->json_ok( $domains ); + } + + // ── AJAX: Assignments ───────────────────────────────────────────────────── + + public function ajax_woocow_assignments_list(): void { + $this->verify(); + global $wpdb; + $rows = $wpdb->get_results( " + SELECT a.id, a.customer_id, a.domain, a.created_at, + s.name AS server_name, s.url AS server_url, + u.display_name, u.user_email + FROM {$wpdb->prefix}woocow_assignments a + JOIN {$wpdb->prefix}woocow_servers s ON s.id = a.server_id + JOIN {$wpdb->users} u ON u.ID = a.customer_id + ORDER BY u.display_name, a.domain + " ); + $this->json_ok( $rows ); + } + + public function ajax_woocow_assignment_save(): void { + $this->verify(); + global $wpdb; + + $customer_id = absint( $_POST['customer_id'] ?? 0 ); + $server_id = absint( $_POST['server_id'] ?? 0 ); + $domain = sanitize_text_field( $_POST['domain'] ?? '' ); + + if ( ! $customer_id || ! $server_id || ! $domain ) { + $this->json_err( 'Customer, server, and domain are all required.' ); + } + + $existing = $wpdb->get_var( $wpdb->prepare( + "SELECT id FROM {$wpdb->prefix}woocow_assignments WHERE customer_id=%d AND domain=%s", + $customer_id, $domain + ) ); + if ( $existing ) { + $this->json_err( 'This domain is already assigned to this customer.' ); + } + + $wpdb->insert( "{$wpdb->prefix}woocow_assignments", compact( 'customer_id', 'server_id', 'domain' ) ); + $this->json_ok( [ 'id' => $wpdb->insert_id ] ); + } + + public function ajax_woocow_assignment_delete(): void { + $this->verify(); + global $wpdb; + $id = absint( $_POST['id'] ?? 0 ); + $wpdb->delete( "{$wpdb->prefix}woocow_assignments", [ 'id' => $id ] ); + $this->json_ok(); + } + + // ── AJAX: Customer search ───────────────────────────────────────────────── + + public function ajax_woocow_customers_search(): void { + $this->verify(); + + $term = sanitize_text_field( $_POST['term'] ?? '' ); + if ( strlen( $term ) < 2 ) { + $this->json_ok( [] ); + } + + $users = get_users( [ + 'search' => '*' . $term . '*', + 'search_columns' => [ 'user_login', 'user_email', 'display_name' ], + 'role__in' => [ 'customer', 'subscriber', 'administrator', 'shop_manager' ], + 'number' => 15, + ] ); + + $out = array_map( fn( $u ) => [ + 'id' => $u->ID, + 'label' => sprintf( '%s (%s)', $u->display_name, $u->user_email ), + ], $users ); + + $this->json_ok( $out ); + } + + // ── AJAX: Admin Mailboxes ───────────────────────────────────────────────── + + public function ajax_woocow_admin_mailboxes(): void { + $this->verify(); + + $server_id = absint( $_POST['server_id'] ?? 0 ); + $domain = sanitize_text_field( $_POST['domain'] ?? '' ); + + $server = $this->get_server( $server_id ); + if ( ! $server ) { + $this->json_err( 'Server not found.' ); + } + + $api = WooCow_API::from_server( $server ); + + $result = $domain + ? $api->get_domain_mailboxes( $domain ) + : $api->get_all_mailboxes(); + + if ( ! $result['success'] ) { + $this->json_err( $result['error'] ?? 'Failed to fetch mailboxes.' ); + } + + $this->json_ok( [ + 'mailboxes' => $result['data'] ?? [], + 'webmail_url' => $api->get_webmail_url(), + ] ); + } + + public function ajax_woocow_admin_mailbox_create(): void { + $this->verify(); + + $server_id = absint( $_POST['server_id'] ?? 0 ); + $domain = sanitize_text_field( $_POST['domain'] ?? '' ); + $local_part = sanitize_text_field( $_POST['local_part'] ?? '' ); + $name = sanitize_text_field( $_POST['name'] ?? '' ); + $password = $_POST['password'] ?? ''; + $password2 = $_POST['password2'] ?? ''; + $quota = absint( $_POST['quota'] ?? 1024 ); + + if ( ! $domain || ! $local_part || ! $password ) { + $this->json_err( 'Domain, local part, and password are required.' ); + } + if ( $password !== $password2 ) { + $this->json_err( 'Passwords do not match.' ); + } + + $server = $this->get_server( $server_id ); + if ( ! $server ) { + $this->json_err( 'Server not found.' ); + } + + $api = WooCow_API::from_server( $server ); + $result = $api->create_mailbox( [ + 'local_part' => $local_part, + 'domain' => $domain, + 'name' => $name ?: $local_part, + 'password' => $password, + 'password2' => $password2, + 'quota' => $quota, + 'active' => 1, + 'force_pw_update' => 0, + 'tls_enforce_in' => 0, + 'tls_enforce_out' => 0, + ] ); + + if ( ! $result['success'] ) { + $this->json_err( $result['error'] ?? 'Failed to create mailbox.' ); + } + + $this->json_ok( [ 'email' => $local_part . '@' . $domain ] ); + } + + public function ajax_woocow_admin_mailbox_delete(): void { + $this->verify(); + + $server_id = absint( $_POST['server_id'] ?? 0 ); + $email = sanitize_email( $_POST['email'] ?? '' ); + + $server = $this->get_server( $server_id ); + if ( ! $server ) { + $this->json_err( 'Server not found.' ); + } + + $api = WooCow_API::from_server( $server ); + $result = $api->delete_mailbox( $email ); + + if ( ! $result['success'] ) { + $this->json_err( $result['error'] ?? 'Failed to delete mailbox.' ); + } + + $this->json_ok(); + } +} diff --git a/includes/class-woocow-api.php b/includes/class-woocow-api.php new file mode 100644 index 0000000..30f433d --- /dev/null +++ b/includes/class-woocow-api.php @@ -0,0 +1,165 @@ +base_url = rtrim( $url, '/' ); + $this->api_key = $api_key; + } + + // ── Core HTTP ──────────────────────────────────────────────────────────── + + private function request( string $method, string $endpoint, array $body = [] ): array { + $args = [ + 'method' => strtoupper( $method ), + 'timeout' => $this->timeout, + 'sslverify' => apply_filters( 'woocow_sslverify', true ), + 'headers' => [ + 'X-API-Key' => $this->api_key, + 'Content-Type' => 'application/json', + 'Accept' => 'application/json', + ], + ]; + + if ( ! empty( $body ) ) { + $args['body'] = wp_json_encode( $body ); + } + + $response = wp_remote_request( $this->base_url . $endpoint, $args ); + + if ( is_wp_error( $response ) ) { + return [ 'success' => false, 'error' => $response->get_error_message() ]; + } + + $code = wp_remote_retrieve_response_code( $response ); + $raw = wp_remote_retrieve_body( $response ); + $data = json_decode( $raw, true ); + + if ( $code === 401 ) { + return [ 'success' => false, 'error' => 'Authentication failed – check your API key.' ]; + } + + return [ + 'success' => ( $code >= 200 && $code < 300 ), + 'data' => $data, + 'code' => $code, + ]; + } + + // ── Health / Version ───────────────────────────────────────────────────── + + public function test_connection(): array { + return $this->request( 'GET', '/api/v1/get/status/version' ); + } + + // ── Domains ────────────────────────────────────────────────────────────── + + public function get_domains(): array { + return $this->request( 'GET', '/api/v1/get/domain/all' ); + } + + public function get_domain( string $domain ): array { + return $this->request( 'GET', '/api/v1/get/domain/' . rawurlencode( $domain ) ); + } + + public function create_domain( array $data ): array { + return $this->request( 'POST', '/api/v1/add/domain', $data ); + } + + public function edit_domain( array $items, array $attr ): array { + return $this->request( 'POST', '/api/v1/edit/domain', [ 'items' => $items, 'attr' => $attr ] ); + } + + public function delete_domain( string $domain ): array { + return $this->request( 'POST', '/api/v1/delete/domain', [ 'items' => [ $domain ] ] ); + } + + // ── Mailboxes ──────────────────────────────────────────────────────────── + + public function get_all_mailboxes(): array { + return $this->request( 'GET', '/api/v1/get/mailbox/all' ); + } + + public function get_mailbox( string $email ): array { + return $this->request( 'GET', '/api/v1/get/mailbox/' . rawurlencode( $email ) ); + } + + /** Mailcow uses POST for domain-filtered mailbox listing */ + public function get_domain_mailboxes( string $domain ): array { + return $this->request( 'POST', '/api/v1/get/mailboxes/' . rawurlencode( $domain ) ); + } + + public function create_mailbox( array $data ): array { + return $this->request( 'POST', '/api/v1/add/mailbox', $data ); + } + + public function edit_mailbox( array $items, array $attr ): array { + return $this->request( 'POST', '/api/v1/edit/mailbox', [ 'items' => $items, 'attr' => $attr ] ); + } + + public function delete_mailbox( string $email ): array { + return $this->request( 'POST', '/api/v1/delete/mailbox', [ 'items' => [ $email ] ] ); + } + + // ── Aliases ────────────────────────────────────────────────────────────── + + public function get_all_aliases(): array { + return $this->request( 'GET', '/api/v1/get/alias/all' ); + } + + public function get_alias( int $id ): array { + return $this->request( 'GET', '/api/v1/get/alias/' . $id ); + } + + public function create_alias( array $data ): array { + return $this->request( 'POST', '/api/v1/add/alias', $data ); + } + + public function edit_alias( array $items, array $attr ): array { + return $this->request( 'POST', '/api/v1/edit/alias', [ 'items' => $items, 'attr' => $attr ] ); + } + + public function delete_alias( int $id ): array { + return $this->request( 'POST', '/api/v1/delete/alias', [ 'items' => [ $id ] ] ); + } + + // ── Domain Admins ──────────────────────────────────────────────────────── + + public function get_domain_admins(): array { + return $this->request( 'GET', '/api/v1/get/domain-admin/all' ); + } + + public function create_domain_admin( array $data ): array { + return $this->request( 'POST', '/api/v1/add/domain-admin', $data ); + } + + public function delete_domain_admin( string $username ): array { + return $this->request( 'POST', '/api/v1/delete/domain-admin', [ 'items' => [ $username ] ] ); + } + + // ── Helpers ────────────────────────────────────────────────────────────── + + public function get_webmail_url(): string { + return $this->base_url . '/SOGo'; + } + + public function get_base_url(): string { + return $this->base_url; + } + + // ── Static factory ─────────────────────────────────────────────────────── + + /** Build API instance from a server DB row. */ + public static function from_server( object $server ): self { + return new self( $server->url, $server->api_key ); + } +} diff --git a/includes/class-woocow-installer.php b/includes/class-woocow-installer.php new file mode 100644 index 0000000..a151984 --- /dev/null +++ b/includes/class-woocow-installer.php @@ -0,0 +1,39 @@ +get_charset_collate(); + + $servers_sql = "CREATE TABLE {$wpdb->prefix}woocow_servers ( + id INT NOT NULL AUTO_INCREMENT, + name VARCHAR(100) NOT NULL, + url VARCHAR(500) NOT NULL, + api_key VARCHAR(500) NOT NULL, + active TINYINT(1) NOT NULL DEFAULT 1, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (id) + ) $charset;"; + + $assignments_sql = "CREATE TABLE {$wpdb->prefix}woocow_assignments ( + id INT NOT NULL AUTO_INCREMENT, + customer_id BIGINT NOT NULL, + server_id INT NOT NULL, + domain VARCHAR(255) NOT NULL, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (id), + KEY idx_customer (customer_id), + KEY idx_server (server_id), + UNIQUE KEY uniq_cust_domain (customer_id, domain) + ) $charset;"; + + require_once ABSPATH . 'wp-admin/includes/upgrade.php'; + dbDelta( $servers_sql ); + dbDelta( $assignments_sql ); + + update_option( 'woocow_db_version', WOOCOW_VERSION ); + } +} diff --git a/includes/class-woocow-pw-sync.php b/includes/class-woocow-pw-sync.php new file mode 100644 index 0000000..04078ed --- /dev/null +++ b/includes/class-woocow-pw-sync.php @@ -0,0 +1,51 @@ + Account Details page. + * + * Loaded automatically from woocow.php when WooCommerce is active. + */ +defined( 'ABSPATH' ) || exit; + +class WooCow_PW_Sync { + + public function __construct() { + // Render the checkbox below the password fields + add_action( 'woocommerce_edit_account_form', [ $this, 'render_checkbox' ] ); + } + + public function render_checkbox(): void { + // Only show if the current customer has mailcow assignments + global $wpdb; + $count = (int) $wpdb->get_var( $wpdb->prepare( + "SELECT COUNT(*) FROM {$wpdb->prefix}woocow_assignments WHERE customer_id = %d", + get_current_user_id() + ) ); + + if ( ! $count ) { + return; + } + ?> +
+ + +
+ +

%s

', + esc_html__( 'WooCow requires WooCommerce to be installed and active.', 'woocow' ) + ); + } ); + return; + } + new WooCow_Admin(); + new WooCow_Account(); + new WooCow_PW_Sync(); +} );