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 <noreply@anthropic.com>
This commit is contained in:
346
assets/css/woocow.css
Normal file
346
assets/css/woocow.css
Normal file
@@ -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%; }
|
||||
}
|
||||
281
assets/js/woocow-account.js
Normal file
281
assets/js/woocow-account.js
Normal file
@@ -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(`<div class="woocommerce-${type === 'success' ? 'message' : 'error'}">${msg}</div>`);
|
||||
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('<p class="woocow-loading">Fetching mailboxes…</p>');
|
||||
|
||||
ajax('woocow_acct_mailboxes', { server_id: sid, domain }).done(res => {
|
||||
$(this).prop('disabled', false).text('Refresh');
|
||||
if (!res.success) {
|
||||
$list.html(`<p class="woocow-error">${esc(res.data)}</p>`);
|
||||
return;
|
||||
}
|
||||
|
||||
const boxes = res.data.mailboxes || [];
|
||||
const webmail = res.data.webmail_url;
|
||||
|
||||
if (!boxes.length) {
|
||||
$list.html('<p class="woocow-muted">No mailboxes yet. Create one below.</p>');
|
||||
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 += `<div class="woocow-mailbox-row" data-email="${esc(m.username)}">
|
||||
<div class="woocow-mbox-main">
|
||||
<div class="woocow-mbox-address">
|
||||
<span class="woocow-mbox-icon">✉</span>
|
||||
<strong>${esc(m.username)}</strong>
|
||||
${m.name ? `<span class="woocow-mbox-name">(${esc(m.name)})</span>` : ''}
|
||||
</div>
|
||||
<div class="woocow-quota-wrap">
|
||||
<div class="woocow-quota-bar-outer">
|
||||
<div class="woocow-quota-bar-inner" style="width:${pct}%;background:${col}"></div>
|
||||
</div>
|
||||
<span class="woocow-quota-text">${used} / ${max} (${pct}%)</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="woocow-mbox-actions">
|
||||
<button class="woocow-btn woocow-btn-sm wc-change-pw"
|
||||
data-server="${esc(sid)}" data-domain="${esc(domain)}" data-email="${esc(m.username)}">
|
||||
Change Password
|
||||
</button>
|
||||
<button class="woocow-btn woocow-btn-sm woocow-btn-outline wc-toggle-aliases"
|
||||
data-server="${esc(sid)}" data-domain="${esc(domain)}">
|
||||
Aliases
|
||||
</button>
|
||||
<a href="${esc(webmail)}" target="_blank" rel="noopener" class="woocow-btn woocow-btn-sm woocow-btn-ghost">
|
||||
Webmail ↗
|
||||
</a>
|
||||
</div>
|
||||
<div class="woocow-aliases-wrap" style="display:none">
|
||||
<div class="woocow-aliases-list"></div>
|
||||
<div class="woocow-alias-create-form" style="display:none">
|
||||
<div class="woocow-alias-fields">
|
||||
<input type="email" class="wc-alias-addr woocow-input" placeholder="alias@${esc(domain)}">
|
||||
<span class="woocow-arrow">→</span>
|
||||
<input type="email" class="wc-alias-goto woocow-input" placeholder="destination@example.com" value="${esc(m.username)}">
|
||||
<button class="woocow-btn woocow-btn-primary woocow-btn-sm wc-alias-save"
|
||||
data-server="${esc(sid)}" data-domain="${esc(domain)}">Add</button>
|
||||
<button class="woocow-btn woocow-btn-sm wc-alias-cancel">✕</button>
|
||||
</div>
|
||||
</div>
|
||||
<button class="woocow-btn woocow-btn-sm woocow-btn-outline wc-alias-add-btn">+ Add Alias</button>
|
||||
</div>
|
||||
</div>`;
|
||||
});
|
||||
|
||||
$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('<p class="woocow-muted">Loading aliases…</p>');
|
||||
$wrap.slideDown();
|
||||
|
||||
ajax('woocow_acct_aliases', { server_id: sid, domain }).done(res => {
|
||||
if (!res.success) {
|
||||
$list.html(`<p class="woocow-error">${esc(res.data)}</p>`);
|
||||
return;
|
||||
}
|
||||
const aliases = res.data;
|
||||
if (!aliases.length) {
|
||||
$list.html('<p class="woocow-muted">No aliases for this domain yet.</p>');
|
||||
return;
|
||||
}
|
||||
let html = '<ul class="woocow-alias-list">';
|
||||
aliases.forEach(a => {
|
||||
html += `<li>
|
||||
<span class="woocow-alias-addr">${esc(a.address)}</span>
|
||||
<span class="woocow-arrow">→</span>
|
||||
<span class="woocow-alias-goto">${esc(a.goto)}</span>
|
||||
<button class="woocow-btn woocow-btn-danger woocow-btn-xs wc-alias-del"
|
||||
data-id="${esc(a.id)}" data-server="${esc(sid)}" data-domain="${esc(domain)}">✕</button>
|
||||
</li>`;
|
||||
});
|
||||
html += '</ul>';
|
||||
$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('<span class="woocow-error">Username and password required.</span>'); return; }
|
||||
if (pass !== pass2) { $note.html('<span class="woocow-error">Passwords do not match.</span>'); 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('<span style="color:green">✓ Mailbox created!</span>');
|
||||
$form.slideUp();
|
||||
// Refresh mailbox list
|
||||
$panel.find('.woocow-load-mailboxes').trigger('click');
|
||||
} else {
|
||||
$note.html(`<span class="woocow-error">${esc(res.data)}</span>`);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// ── 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('<span class="woocow-error">Passwords do not match or are empty.</span>');
|
||||
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('<span style="color:green">✓ Password updated!</span>');
|
||||
setTimeout(() => $('#woocow-pw-modal').fadeOut(200), 1500);
|
||||
} else {
|
||||
$note.html(`<span class="woocow-error">${esc(res.data)}</span>`);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
})(jQuery);
|
||||
398
assets/js/woocow-admin.js
Normal file
398
assets/js/woocow-admin.js
Normal file
@@ -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(`<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</th><th>URL</th><th>Status</th><th>Added</th><th>Actions</th>
|
||||
</tr></thead><tbody>`;
|
||||
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)}</strong></td>
|
||||
<td><a href="${esc(s.url)}" target="_blank" rel="noopener">${esc(s.url)}</a></td>
|
||||
<td>${badge}</td>
|
||||
<td>${s.created_at.split(' ')[0]}</td>
|
||||
<td class="woocow-actions">
|
||||
<button class="button button-small wc-srv-edit" data-id="${s.id}">Edit</button>
|
||||
<button class="button button-small wc-srv-test" data-id="${s.id}">Test</button>
|
||||
<button class="button button-small wc-srv-del" data-id="${s.id}" style="color:#a00">Delete</button>
|
||||
</td>
|
||||
</tr>`;
|
||||
});
|
||||
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</th><th>Email</th><th>Domain</th><th>Server</th><th>Assigned</th><th>Actions</th>
|
||||
</tr></thead><tbody>`;
|
||||
rows.forEach(r => {
|
||||
html += `<tr>
|
||||
<td>${esc(r.display_name)}</td>
|
||||
<td>${esc(r.user_email)}</td>
|
||||
<td><strong>${esc(r.domain)}</strong></td>
|
||||
<td>${esc(r.server_name)}</td>
|
||||
<td>${r.created_at.split(' ')[0]}</td>
|
||||
<td><button class="button button-small wc-assign-del" data-id="${r.id}" style="color:#a00">Remove</button></td>
|
||||
</tr>`;
|
||||
});
|
||||
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</th><th>Name</th><th>Quota Used</th><th>Quota Max</th><th>Active</th><th>Actions</th>
|
||||
</tr></thead><tbody>`;
|
||||
boxes.forEach(m => {
|
||||
const pct = m.quota_used_in_percent || 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>`;
|
||||
html += `<tr>
|
||||
<td><a href="${esc(webmail)}" target="_blank">${esc(m.username)}</a></td>
|
||||
<td>${esc(m.name)}</td>
|
||||
<td>${used} ${bar}</td>
|
||||
<td>${max}</td>
|
||||
<td>${m.active == 1 ? '✓' : '–'}</td>
|
||||
<td class="woocow-actions">
|
||||
<button class="button button-small wc-mb-del" data-email="${esc(m.username)}" style="color:#a00">Delete</button>
|
||||
</td>
|
||||
</tr>`;
|
||||
});
|
||||
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);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// ── 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);
|
||||
458
includes/class-woocow-account.php
Normal file
458
includes/class-woocow-account.php
Normal file
@@ -0,0 +1,458 @@
|
||||
<?php
|
||||
/**
|
||||
* WooCow Account – WooCommerce My Account integration.
|
||||
*
|
||||
* Registers the "email-hosting" endpoint and handles all customer-facing AJAX.
|
||||
*/
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
class WooCow_Account {
|
||||
|
||||
const ENDPOINT = 'email-hosting';
|
||||
|
||||
public function __construct() {
|
||||
// Endpoint registration
|
||||
add_action( 'init', [ $this, 'register_endpoint' ] );
|
||||
add_filter( 'woocommerce_account_menu_items', [ $this, 'add_menu_item' ] );
|
||||
add_action( 'woocommerce_account_' . self::ENDPOINT . '_endpoint', [ $this, 'render_page' ] );
|
||||
|
||||
// Assets
|
||||
add_action( 'wp_enqueue_scripts', [ $this, 'enqueue_assets' ] );
|
||||
|
||||
// Password reset integration
|
||||
add_action( 'woocommerce_save_account_details', [ $this, 'maybe_sync_password' ], 20 );
|
||||
|
||||
// AJAX
|
||||
$actions = [
|
||||
'woocow_acct_domains',
|
||||
'woocow_acct_mailboxes',
|
||||
'woocow_acct_mailbox_create',
|
||||
'woocow_acct_mailbox_password',
|
||||
'woocow_acct_aliases',
|
||||
'woocow_acct_alias_create',
|
||||
'woocow_acct_alias_delete',
|
||||
];
|
||||
foreach ( $actions as $action ) {
|
||||
add_action( 'wp_ajax_' . $action, [ $this, 'ajax_' . $action ] );
|
||||
}
|
||||
}
|
||||
|
||||
public function register_endpoint(): void {
|
||||
add_rewrite_endpoint( self::ENDPOINT, EP_ROOT | EP_PAGES );
|
||||
}
|
||||
|
||||
public function add_menu_item( array $items ): array {
|
||||
// Insert before logout
|
||||
$logout = $items['customer-logout'] ?? null;
|
||||
unset( $items['customer-logout'] );
|
||||
$items[ self::ENDPOINT ] = __( 'Email Hosting', 'woocow' );
|
||||
if ( $logout ) {
|
||||
$items['customer-logout'] = $logout;
|
||||
}
|
||||
return $items;
|
||||
}
|
||||
|
||||
public function enqueue_assets(): void {
|
||||
if ( ! is_account_page() ) {
|
||||
return;
|
||||
}
|
||||
wp_enqueue_style( 'woocow-account', WOOCOW_PLUGIN_URL . 'assets/css/woocow.css', [], WOOCOW_VERSION );
|
||||
wp_enqueue_script( 'woocow-account', WOOCOW_PLUGIN_URL . 'assets/js/woocow-account.js', [ 'jquery' ], WOOCOW_VERSION, true );
|
||||
wp_localize_script( 'woocow-account', 'woocowAcct', [
|
||||
'ajax_url' => 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 '<p>' . esc_html__( 'Please log in to manage your email hosting.', 'woocow' ) . '</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
$customer_id = get_current_user_id();
|
||||
$assignments = $this->get_customer_assignments( $customer_id );
|
||||
|
||||
if ( empty( $assignments ) ) {
|
||||
echo '<div class="woocommerce-info">' . esc_html__( 'You have no email domains assigned yet. Please contact support.', 'woocow' ) . '</div>';
|
||||
return;
|
||||
}
|
||||
?>
|
||||
<div class="woocow-account" id="woocow-account">
|
||||
<div id="woocow-acct-notices"></div>
|
||||
|
||||
<?php foreach ( $assignments as $assignment ) : ?>
|
||||
<div class="woocow-domain-panel" data-assignment-id="<?php echo esc_attr( $assignment->id ); ?>"
|
||||
data-server-id="<?php echo esc_attr( $assignment->server_id ); ?>"
|
||||
data-domain="<?php echo esc_attr( $assignment->domain ); ?>">
|
||||
|
||||
<div class="woocow-domain-header">
|
||||
<span class="woocow-domain-name"><?php echo esc_html( $assignment->domain ); ?></span>
|
||||
<span class="woocow-domain-server"><?php echo esc_html( $assignment->server_name ); ?></span>
|
||||
<a href="<?php echo esc_url( $assignment->webmail_url ); ?>" target="_blank" rel="noopener" class="woocow-btn woocow-btn-sm">
|
||||
Open Webmail
|
||||
</a>
|
||||
<button class="woocow-btn woocow-btn-sm woocow-btn-outline woocow-load-mailboxes">Load Mailboxes</button>
|
||||
</div>
|
||||
|
||||
<div class="woocow-mailboxes-wrap" style="display:none">
|
||||
<div class="woocow-mailboxes-list"></div>
|
||||
|
||||
<!-- Create mailbox form -->
|
||||
<div class="woocow-create-mbox-form" style="display:none">
|
||||
<h4>Create Mailbox</h4>
|
||||
<div class="woocow-field-row">
|
||||
<div class="woocow-flex-inline">
|
||||
<input type="text" class="wc-mbox-local woocow-input" placeholder="username">
|
||||
<span class="woocow-at">@</span>
|
||||
<strong><?php echo esc_html( $assignment->domain ); ?></strong>
|
||||
</div>
|
||||
</div>
|
||||
<div class="woocow-field-row">
|
||||
<input type="text" class="wc-mbox-name woocow-input" placeholder="Full Name">
|
||||
</div>
|
||||
<div class="woocow-field-row">
|
||||
<input type="password" class="wc-mbox-pass woocow-input" placeholder="Password">
|
||||
</div>
|
||||
<div class="woocow-field-row">
|
||||
<input type="password" class="wc-mbox-pass2 woocow-input" placeholder="Confirm Password">
|
||||
</div>
|
||||
<div class="woocow-field-row">
|
||||
<label>Quota (MB): <input type="number" class="wc-mbox-quota woocow-input-sm" value="1024" min="1"></label>
|
||||
</div>
|
||||
<div class="woocow-form-actions">
|
||||
<button class="woocow-btn woocow-btn-primary wc-mbox-submit">Create Mailbox</button>
|
||||
<button class="woocow-btn woocow-btn-outline wc-mbox-cancel">Cancel</button>
|
||||
<span class="wc-mbox-notice"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button class="woocow-btn woocow-btn-outline woocow-create-mbox-btn">+ Add Mailbox</button>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
|
||||
<!-- Change Password Modal -->
|
||||
<div id="woocow-pw-modal" class="woocow-modal" style="display:none">
|
||||
<div class="woocow-modal-box">
|
||||
<h3>Change Mailbox Password</h3>
|
||||
<p class="woocow-modal-subtitle" id="woocow-pw-email"></p>
|
||||
<input type="hidden" id="woocow-pw-server-id">
|
||||
<input type="hidden" id="woocow-pw-mailbox">
|
||||
<div class="woocow-field-row">
|
||||
<input type="password" id="woocow-pw-new" class="woocow-input" placeholder="New Password">
|
||||
</div>
|
||||
<div class="woocow-field-row">
|
||||
<input type="password" id="woocow-pw-new2" class="woocow-input" placeholder="Confirm New Password">
|
||||
</div>
|
||||
<div class="woocow-modal-actions">
|
||||
<button class="woocow-btn woocow-btn-primary" id="woocow-pw-save">Update Password</button>
|
||||
<button class="woocow-btn woocow-btn-outline" id="woocow-pw-cancel">Cancel</button>
|
||||
<span id="woocow-pw-notice"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
|
||||
// ── Helpers ───────────────────────────────────────────────────────────────
|
||||
|
||||
private function get_customer_assignments( int $customer_id ): array {
|
||||
global $wpdb;
|
||||
$rows = $wpdb->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,
|
||||
] );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
545
includes/class-woocow-admin.php
Normal file
545
includes/class-woocow-admin.php
Normal file
@@ -0,0 +1,545 @@
|
||||
<?php
|
||||
/**
|
||||
* WooCow Admin – menu pages + AJAX handlers for the backend.
|
||||
*/
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
class WooCow_Admin {
|
||||
|
||||
public function __construct() {
|
||||
add_action( 'admin_menu', [ $this, 'register_menu' ] );
|
||||
add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_assets' ] );
|
||||
|
||||
$ajax_actions = [
|
||||
'woocow_servers_list',
|
||||
'woocow_server_save',
|
||||
'woocow_server_delete',
|
||||
'woocow_server_test',
|
||||
'woocow_server_domains',
|
||||
'woocow_assignments_list',
|
||||
'woocow_assignment_save',
|
||||
'woocow_assignment_delete',
|
||||
'woocow_customers_search',
|
||||
'woocow_admin_mailboxes',
|
||||
'woocow_admin_mailbox_create',
|
||||
'woocow_admin_mailbox_delete',
|
||||
];
|
||||
|
||||
foreach ( $ajax_actions as $action ) {
|
||||
add_action( 'wp_ajax_' . $action, [ $this, 'ajax_' . $action ] );
|
||||
}
|
||||
}
|
||||
|
||||
// ── Menu ─────────────────────────────────────────────────────────────────
|
||||
|
||||
public function register_menu(): void {
|
||||
add_menu_page(
|
||||
'WooCow',
|
||||
'WooCow',
|
||||
'manage_woocommerce',
|
||||
'woocow',
|
||||
[ $this, 'page_dashboard' ],
|
||||
'dashicons-email-alt2',
|
||||
56
|
||||
);
|
||||
add_submenu_page( 'woocow', 'Dashboard', 'Dashboard', 'manage_woocommerce', 'woocow', [ $this, 'page_dashboard' ] );
|
||||
add_submenu_page( 'woocow', 'Servers', 'Servers', 'manage_woocommerce', 'woocow-servers', [ $this, 'page_servers' ] );
|
||||
add_submenu_page( 'woocow', 'Assignments','Assignments', 'manage_woocommerce', 'woocow-assignments', [ $this, 'page_assignments' ] );
|
||||
add_submenu_page( 'woocow', 'Mailboxes', 'Mailboxes', 'manage_woocommerce', 'woocow-mailboxes', [ $this, 'page_mailboxes' ] );
|
||||
}
|
||||
|
||||
public function enqueue_assets( string $hook ): void {
|
||||
if ( strpos( $hook, 'woocow' ) === false ) {
|
||||
return;
|
||||
}
|
||||
wp_enqueue_style( 'woocow-admin', WOOCOW_PLUGIN_URL . 'assets/css/woocow.css', [], WOOCOW_VERSION );
|
||||
wp_enqueue_script( 'woocow-admin', WOOCOW_PLUGIN_URL . 'assets/js/woocow-admin.js', [ 'jquery' ], WOOCOW_VERSION, true );
|
||||
wp_localize_script( 'woocow-admin', 'woocow', [
|
||||
'ajax_url' => 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" );
|
||||
?>
|
||||
<div class="wrap woocow-wrap">
|
||||
<h1>WooCow <span class="woocow-version">v<?php echo esc_html( WOOCOW_VERSION ); ?></span></h1>
|
||||
<div class="woocow-dashboard-cards">
|
||||
<div class="woocow-card">
|
||||
<span class="woocow-card-number"><?php echo esc_html( $servers ); ?></span>
|
||||
<span class="woocow-card-label">Active Servers</span>
|
||||
<a href="<?php echo esc_url( admin_url( 'admin.php?page=woocow-servers' ) ); ?>" class="button button-secondary">Manage</a>
|
||||
</div>
|
||||
<div class="woocow-card">
|
||||
<span class="woocow-card-number"><?php echo esc_html( $customers ); ?></span>
|
||||
<span class="woocow-card-label">Customers with Email</span>
|
||||
<a href="<?php echo esc_url( admin_url( 'admin.php?page=woocow-assignments' ) ); ?>" class="button button-secondary">Manage</a>
|
||||
</div>
|
||||
<div class="woocow-card">
|
||||
<span class="woocow-card-number"><?php echo esc_html( $assignments ); ?></span>
|
||||
<span class="woocow-card-label">Domain Assignments</span>
|
||||
<a href="<?php echo esc_url( admin_url( 'admin.php?page=woocow-mailboxes' ) ); ?>" class="button button-secondary">View Mailboxes</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
|
||||
// ── Page: Servers ────────────────────────────────────────────────────────
|
||||
|
||||
public function page_servers(): void {
|
||||
?>
|
||||
<div class="wrap woocow-wrap">
|
||||
<h1>WooCow – Servers</h1>
|
||||
<p>Add your Mailcow server instances here. The API key must be a read-write key from <strong>Configuration → Access → Edit administrator details → API</strong>.</p>
|
||||
|
||||
<div class="woocow-toolbar">
|
||||
<button class="button button-primary" id="wc-add-server">+ Add Server</button>
|
||||
</div>
|
||||
<div id="wc-notices"></div>
|
||||
|
||||
<!-- Add / Edit form (hidden by default) -->
|
||||
<div id="wc-server-form" class="woocow-card woocow-form" style="display:none">
|
||||
<h3 id="wc-server-form-title">Add Server</h3>
|
||||
<input type="hidden" id="wc-server-id" value="">
|
||||
<table class="form-table">
|
||||
<tr>
|
||||
<th><label for="wc-server-name">Name</label></th>
|
||||
<td><input type="text" id="wc-server-name" class="regular-text" placeholder="My Mailcow Server"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><label for="wc-server-url">Server URL</label></th>
|
||||
<td><input type="url" id="wc-server-url" class="regular-text" placeholder="https://mail.example.com"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><label for="wc-server-key">API Key</label></th>
|
||||
<td><input type="text" id="wc-server-key" class="regular-text" placeholder="Your read-write API key"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><label for="wc-server-active">Active</label></th>
|
||||
<td><input type="checkbox" id="wc-server-active" checked></td>
|
||||
</tr>
|
||||
</table>
|
||||
<div class="woocow-form-actions">
|
||||
<button class="button button-primary" id="wc-server-save">Save Server</button>
|
||||
<button class="button" id="wc-server-test">Test Connection</button>
|
||||
<button class="button" id="wc-server-cancel">Cancel</button>
|
||||
<span id="wc-server-test-result"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Servers table -->
|
||||
<div id="wc-servers-table-wrap">
|
||||
<p id="wc-servers-loading">Loading servers…</p>
|
||||
</div>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
|
||||
// ── Page: Assignments ────────────────────────────────────────────────────
|
||||
|
||||
public function page_assignments(): void {
|
||||
?>
|
||||
<div class="wrap woocow-wrap">
|
||||
<h1>WooCow – Domain Assignments</h1>
|
||||
<p>Assign one or more Mailcow domains to a WooCommerce customer. The customer can then manage mailboxes for those domains from <em>My Account → Email Hosting</em>.</p>
|
||||
|
||||
<div class="woocow-card woocow-form" id="wc-assign-form">
|
||||
<h3>Assign Domain to Customer</h3>
|
||||
<table class="form-table">
|
||||
<tr>
|
||||
<th><label>Customer</label></th>
|
||||
<td>
|
||||
<input type="text" id="wc-cust-search" class="regular-text" placeholder="Search by name or email…" autocomplete="off">
|
||||
<div id="wc-cust-results" class="woocow-autocomplete"></div>
|
||||
<input type="hidden" id="wc-cust-id">
|
||||
<span id="wc-cust-selected" class="woocow-selected-badge"></span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><label for="wc-assign-server">Server</label></th>
|
||||
<td>
|
||||
<select id="wc-assign-server" class="regular-text">
|
||||
<option value="">— Select a server —</option>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
<tr id="wc-domain-row" style="display:none">
|
||||
<th><label for="wc-assign-domain">Domain</label></th>
|
||||
<td>
|
||||
<select id="wc-assign-domain" class="regular-text">
|
||||
<option value="">— Loading domains —</option>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<div class="woocow-form-actions">
|
||||
<button class="button button-primary" id="wc-assign-save">Assign Domain</button>
|
||||
</div>
|
||||
<div id="wc-assign-notice"></div>
|
||||
</div>
|
||||
|
||||
<h2>Current Assignments</h2>
|
||||
<div id="wc-assignments-loading">Loading…</div>
|
||||
<div id="wc-assignments-table-wrap"></div>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
|
||||
// ── Page: Mailboxes ──────────────────────────────────────────────────────
|
||||
|
||||
public function page_mailboxes(): void {
|
||||
global $wpdb;
|
||||
$servers = $wpdb->get_results( "SELECT id, name FROM {$wpdb->prefix}woocow_servers WHERE active=1 ORDER BY name" );
|
||||
?>
|
||||
<div class="wrap woocow-wrap">
|
||||
<h1>WooCow – Mailboxes</h1>
|
||||
|
||||
<div class="woocow-toolbar woocow-flex">
|
||||
<select id="wc-mb-server" class="regular-text">
|
||||
<option value="">— Select server —</option>
|
||||
<?php foreach ( $servers as $s ) : ?>
|
||||
<option value="<?php echo esc_attr( $s->id ); ?>"><?php echo esc_html( $s->name ); ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<select id="wc-mb-domain" class="regular-text" style="display:none">
|
||||
<option value="">— Select domain —</option>
|
||||
</select>
|
||||
<button class="button" id="wc-mb-load" disabled>Load Mailboxes</button>
|
||||
<button class="button button-primary" id="wc-mb-create" style="display:none">+ Create Mailbox</button>
|
||||
</div>
|
||||
|
||||
<div id="wc-mb-notices"></div>
|
||||
<div id="wc-mb-table-wrap"></div>
|
||||
|
||||
<!-- Create Mailbox Modal -->
|
||||
<div id="wc-mb-modal" class="woocow-modal" style="display:none">
|
||||
<div class="woocow-modal-box">
|
||||
<h3>Create Mailbox</h3>
|
||||
<table class="form-table">
|
||||
<tr>
|
||||
<th><label for="wc-mb-local">Local Part</label></th>
|
||||
<td>
|
||||
<div class="woocow-flex-inline">
|
||||
<input type="text" id="wc-mb-local" placeholder="user">
|
||||
<span class="woocow-at">@</span>
|
||||
<span id="wc-mb-domain-label" class="woocow-domain-label"></span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><label for="wc-mb-fullname">Full Name</label></th>
|
||||
<td><input type="text" id="wc-mb-fullname" class="regular-text" placeholder="Jane Doe"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><label for="wc-mb-pass">Password</label></th>
|
||||
<td><input type="password" id="wc-mb-pass" class="regular-text"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><label for="wc-mb-pass2">Confirm Password</label></th>
|
||||
<td><input type="password" id="wc-mb-pass2" class="regular-text"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><label for="wc-mb-quota">Quota (MB)</label></th>
|
||||
<td><input type="number" id="wc-mb-quota" value="1024" min="1"></td>
|
||||
</tr>
|
||||
</table>
|
||||
<div class="woocow-modal-actions">
|
||||
<button class="button button-primary" id="wc-mb-modal-save">Create</button>
|
||||
<button class="button" id="wc-mb-modal-cancel">Cancel</button>
|
||||
<span id="wc-mb-modal-notice"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
|
||||
// ── AJAX helpers ─────────────────────────────────────────────────────────
|
||||
|
||||
private function verify(): void {
|
||||
check_ajax_referer( 'woocow_admin', 'nonce' );
|
||||
if ( ! current_user_can( 'manage_woocommerce' ) ) {
|
||||
wp_send_json_error( 'Insufficient permissions.', 403 );
|
||||
}
|
||||
}
|
||||
|
||||
private function get_server( int $id ): ?object {
|
||||
global $wpdb;
|
||||
return $wpdb->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();
|
||||
}
|
||||
}
|
||||
165
includes/class-woocow-api.php
Normal file
165
includes/class-woocow-api.php
Normal file
@@ -0,0 +1,165 @@
|
||||
<?php
|
||||
/**
|
||||
* Mailcow API client.
|
||||
* Wraps all HTTP calls to a single Mailcow server instance.
|
||||
*/
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
class WooCow_API {
|
||||
|
||||
private string $base_url;
|
||||
private string $api_key;
|
||||
private int $timeout = 20;
|
||||
|
||||
public function __construct( string $url, string $api_key ) {
|
||||
$this->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 );
|
||||
}
|
||||
}
|
||||
39
includes/class-woocow-installer.php
Normal file
39
includes/class-woocow-installer.php
Normal file
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
class WooCow_Installer {
|
||||
|
||||
public static function install(): void {
|
||||
global $wpdb;
|
||||
|
||||
$charset = $wpdb->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 );
|
||||
}
|
||||
}
|
||||
51
includes/class-woocow-pw-sync.php
Normal file
51
includes/class-woocow-pw-sync.php
Normal file
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
/**
|
||||
* Adds an optional "sync password to mailboxes" checkbox on the
|
||||
* WooCommerce My Account > 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;
|
||||
}
|
||||
?>
|
||||
<fieldset class="woocow-pw-sync-field">
|
||||
<legend><?php esc_html_e( 'Email Hosting', 'woocow' ); ?></legend>
|
||||
<label for="woocow_sync_pw">
|
||||
<input type="checkbox" name="woocow_sync_pw" id="woocow_sync_pw" value="1">
|
||||
<?php esc_html_e( 'Also update password for all my email mailboxes (only applies when changing password above)', 'woocow' ); ?>
|
||||
</label>
|
||||
</fieldset>
|
||||
<style>
|
||||
.woocow-pw-sync-field {
|
||||
border: 1px dashed #ddd;
|
||||
border-radius: 6px;
|
||||
padding: 12px 16px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
.woocow-pw-sync-field legend {
|
||||
font-weight: 700;
|
||||
padding: 0 6px;
|
||||
color: #2271b1;
|
||||
}
|
||||
</style>
|
||||
<?php
|
||||
}
|
||||
}
|
||||
41
woocow.php
Normal file
41
woocow.php
Normal file
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
/**
|
||||
* Plugin Name: WooCow
|
||||
* Description: Mailcow email hosting management for WooCommerce customers.
|
||||
* Version: 1.0.0
|
||||
* Author: WooCow
|
||||
* Text Domain: woocow
|
||||
* Requires at least: 6.2
|
||||
* Requires PHP: 8.1
|
||||
* WC requires at least: 7.0
|
||||
*/
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
define( 'WOOCOW_VERSION', '1.0.0' );
|
||||
define( 'WOOCOW_PLUGIN_FILE', __FILE__ );
|
||||
define( 'WOOCOW_PLUGIN_DIR', plugin_dir_path( __FILE__ ) );
|
||||
define( 'WOOCOW_PLUGIN_URL', plugin_dir_url( __FILE__ ) );
|
||||
|
||||
require_once WOOCOW_PLUGIN_DIR . 'includes/class-woocow-installer.php';
|
||||
require_once WOOCOW_PLUGIN_DIR . 'includes/class-woocow-api.php';
|
||||
require_once WOOCOW_PLUGIN_DIR . 'includes/class-woocow-admin.php';
|
||||
require_once WOOCOW_PLUGIN_DIR . 'includes/class-woocow-account.php';
|
||||
require_once WOOCOW_PLUGIN_DIR . 'includes/class-woocow-pw-sync.php';
|
||||
|
||||
register_activation_hook( __FILE__, [ 'WooCow_Installer', 'install' ] );
|
||||
|
||||
add_action( 'plugins_loaded', function () {
|
||||
if ( ! class_exists( 'WooCommerce' ) ) {
|
||||
add_action( 'admin_notices', function () {
|
||||
printf(
|
||||
'<div class="notice notice-error"><p>%s</p></div>',
|
||||
esc_html__( 'WooCow requires WooCommerce to be installed and active.', 'woocow' )
|
||||
);
|
||||
} );
|
||||
return;
|
||||
}
|
||||
new WooCow_Admin();
|
||||
new WooCow_Account();
|
||||
new WooCow_PW_Sync();
|
||||
} );
|
||||
Reference in New Issue
Block a user