From f834516eda22ab8599e9afdac67e3d1dea8ac77a Mon Sep 17 00:00:00 2001 From: ChrispyBacon-dev Date: Tue, 14 Apr 2026 21:44:19 +0200 Subject: [PATCH] better quota logic - KV quota for CF worker not fully working - needs improvements - WIP but I take a break for today.. --- dockflare/app/core/email_manager.py | 19 +++ .../core/worker_templates/inbound_worker.js | 18 +++ dockflare/app/static/js/main.js | 24 +++- dockflare/app/templates/email.html | 3 +- dockflare/app/web/email_routes.py | 89 ++++++++++++++ mail-manager/app/api/routes.py | 7 +- mail-manager/app/api/webhook.py | 115 +++++++++++++++++- mail-manager/app/core/database.py | 3 + 8 files changed, 268 insertions(+), 10 deletions(-) diff --git a/dockflare/app/core/email_manager.py b/dockflare/app/core/email_manager.py index 74bffc6..7628d65 100644 --- a/dockflare/app/core/email_manager.py +++ b/dockflare/app/core/email_manager.py @@ -310,6 +310,25 @@ def scrub_email_dns_records(zone_id, zone_name): errors.append(f"DNS list CNAME: {e}") return errors +def create_kv_namespace(title): + res = cf_api_request('POST', f'/accounts/{config.CF_ACCOUNT_ID}/storage/kv/namespaces', + json_data={'title': title}) + return res.get('result', {}).get('id') + +def update_kv_entry(namespace_id, key, value_dict): + url = f"{config.CF_API_BASE_URL}/accounts/{config.CF_ACCOUNT_ID}/storage/kv/namespaces/{namespace_id}/values/{key}" + headers = {"Authorization": f"Bearer {config.CF_API_TOKEN}", "Content-Type": "text/plain"} + response = requests.put(url, data=json.dumps(value_dict), headers=headers, timeout=10) + response.raise_for_status() + return response.json() + +def delete_kv_entry(namespace_id, key): + try: + cf_api_request('DELETE', + f'/accounts/{config.CF_ACCOUNT_ID}/storage/kv/namespaces/{namespace_id}/values/{key}') + except Exception as e: + logging.warning(f"Could not delete KV entry {key} from {namespace_id}: {e}") + def setup_catchall_routing_rule(zone_id, worker_name): data = { "matchers": [{"type": "all"}], diff --git a/dockflare/app/core/worker_templates/inbound_worker.js b/dockflare/app/core/worker_templates/inbound_worker.js index 6ed6a9f..0d0107c 100644 --- a/dockflare/app/core/worker_templates/inbound_worker.js +++ b/dockflare/app/core/worker_templates/inbound_worker.js @@ -39,6 +39,24 @@ export default { return; } + // Check quota KV before accepting — reject at SMTP level so sender gets a bounce + if (typeof env.QUOTA_KV !== 'undefined') { + try { + const quota = await env.QUOTA_KV.get(message.to, "json"); + if (quota && quota.hard_limit_bytes > 0) { + const currentSize = quota.current_size_bytes || 0; + const msgSize = message.rawSize || 0; + if (currentSize + msgSize > quota.hard_limit_bytes) { + message.setReject("550 5.2.2 Mailbox full"); + return; + } + } + } catch (kvErr) { + // KV unavailable — fall through, webhook safety net handles enforcement + console.warn(`KV quota check failed for ${message.to}: ${kvErr.message}`); + } + } + const messageId = crypto.randomUUID(); const r2Key = `temp_cache/${messageId}.eml`; const receivedAt = new Date().toISOString(); diff --git a/dockflare/app/static/js/main.js b/dockflare/app/static/js/main.js index d14a31d..023a806 100644 --- a/dockflare/app/static/js/main.js +++ b/dockflare/app/static/js/main.js @@ -2331,9 +2331,27 @@ function _fmtBytes(b) { return b.toFixed(1) + '\u00a0' + u[i]; } -function emailUpdateQuotaLabel(labelId, stepIndex) { +function _calcGrace(quotaBytes) { + if (!quotaBytes || quotaBytes <= 0) return 0; + return Math.max(Math.round(quotaBytes * 0.15), 10 * 1024 * 1024); +} + +function emailUpdateQuotaLabel(labelId, stepIndex, graceInfoId) { const el = document.getElementById(labelId); - if (el) el.textContent = QUOTA_STEPS[parseInt(stepIndex)]?.label ?? '10 GB'; + const step = QUOTA_STEPS[parseInt(stepIndex)]; + if (el) el.textContent = step?.label ?? '10 GB'; + if (graceInfoId) { + const graceEl = document.getElementById(graceInfoId); + if (graceEl) { + const quota = step?.bytes ?? 0; + if (quota > 0) { + const grace = _calcGrace(quota); + graceEl.textContent = `Hard limit: ${_fmtBytes(quota + grace)} (includes ${_fmtBytes(grace)} grace buffer)`; + } else { + graceEl.textContent = ''; + } + } + } } function emailLoadStats() { @@ -2983,7 +3001,7 @@ function emailEditQuota(address, domain, currentQuotaBytes) { const stepIndex = QUOTA_STEPS.findIndex(s => s.bytes === currentQuotaBytes); const slider = document.getElementById('editMailboxQuota'); slider.value = stepIndex >= 0 ? stepIndex : 6; - emailUpdateQuotaLabel('editMailboxQuotaLabel', slider.value); + emailUpdateQuotaLabel('editMailboxQuotaLabel', slider.value, 'emailEditQuotaGraceInfo'); const submitBtn = document.getElementById('emailEditQuotaSubmitBtn'); const handler = async () => { submitBtn.removeEventListener('click', handler); diff --git a/dockflare/app/templates/email.html b/dockflare/app/templates/email.html index aa3d13f..a7e854e 100644 --- a/dockflare/app/templates/email.html +++ b/dockflare/app/templates/email.html @@ -460,12 +460,13 @@

- +
100 MB 10 GB Unlimited
+