mirror of
https://github.com/ChrispyBacon-dev/DockFlare.git
synced 2026-10-19 07:33:16 +00:00
domain teardown orphan cleanup and create mailbox refine, when mailbox is already created / cleanup orphan and github workflow update
This commit is contained in:
parent
674c2437c0
commit
dbff6f6449
6 changed files with 138 additions and 23 deletions
15
.github/workflows/docker-image.yml
vendored
15
.github/workflows/docker-image.yml
vendored
|
|
@ -13,6 +13,7 @@ on:
|
|||
- "v*.*.*-*"
|
||||
paths:
|
||||
- "dockflare/**"
|
||||
- ".github/workflows/docker-image.yml"
|
||||
pull_request:
|
||||
branches:
|
||||
- "stable"
|
||||
|
|
@ -21,10 +22,16 @@ on:
|
|||
- "dockflare-mail"
|
||||
paths:
|
||||
- "dockflare/**"
|
||||
- ".github/workflows/docker-image.yml"
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: docker-dockflare-${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
build_self_hosted:
|
||||
runs-on: self-hosted
|
||||
|
|
@ -44,7 +51,7 @@ jobs:
|
|||
uses: docker/setup-buildx-action@v4
|
||||
|
||||
- name: Log in to Docker Hub
|
||||
if: github.event_name == 'push'
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
|
|
@ -72,7 +79,7 @@ jobs:
|
|||
with:
|
||||
context: ./dockflare
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: ${{ github.event_name == 'push' }}
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
cache-from: type=gha,scope=dockflare
|
||||
|
|
@ -97,7 +104,7 @@ jobs:
|
|||
uses: docker/setup-buildx-action@v4
|
||||
|
||||
- name: Log in to Docker Hub
|
||||
if: github.event_name == 'push'
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
|
|
@ -125,7 +132,7 @@ jobs:
|
|||
with:
|
||||
context: ./dockflare
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: ${{ github.event_name == 'push' }}
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
cache-from: type=gha,scope=dockflare
|
||||
|
|
|
|||
15
.github/workflows/docker-mail-manager.yml
vendored
15
.github/workflows/docker-mail-manager.yml
vendored
|
|
@ -13,6 +13,7 @@ on:
|
|||
- "v*.*.*-*"
|
||||
paths:
|
||||
- "mail-manager/**"
|
||||
- ".github/workflows/docker-mail-manager.yml"
|
||||
pull_request:
|
||||
branches:
|
||||
- "stable"
|
||||
|
|
@ -21,10 +22,16 @@ on:
|
|||
- "dockflare-mail"
|
||||
paths:
|
||||
- "mail-manager/**"
|
||||
- ".github/workflows/docker-mail-manager.yml"
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: docker-mail-manager-${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
build_self_hosted:
|
||||
runs-on: self-hosted
|
||||
|
|
@ -44,7 +51,7 @@ jobs:
|
|||
uses: docker/setup-buildx-action@v4
|
||||
|
||||
- name: Log in to Docker Hub
|
||||
if: github.event_name == 'push'
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
|
|
@ -72,7 +79,7 @@ jobs:
|
|||
with:
|
||||
context: ./mail-manager
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: ${{ github.event_name == 'push' }}
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
cache-from: type=gha,scope=dockflare-mail-manager
|
||||
|
|
@ -97,7 +104,7 @@ jobs:
|
|||
uses: docker/setup-buildx-action@v4
|
||||
|
||||
- name: Log in to Docker Hub
|
||||
if: github.event_name == 'push'
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
|
|
@ -125,7 +132,7 @@ jobs:
|
|||
with:
|
||||
context: ./mail-manager
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: ${{ github.event_name == 'push' }}
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
cache-from: type=gha,scope=dockflare-mail-manager
|
||||
|
|
|
|||
15
.github/workflows/docker-webmail.yml
vendored
15
.github/workflows/docker-webmail.yml
vendored
|
|
@ -13,6 +13,7 @@ on:
|
|||
- "v*.*.*-*"
|
||||
paths:
|
||||
- "webmail/**"
|
||||
- ".github/workflows/docker-webmail.yml"
|
||||
pull_request:
|
||||
branches:
|
||||
- "stable"
|
||||
|
|
@ -21,10 +22,16 @@ on:
|
|||
- "dockflare-mail"
|
||||
paths:
|
||||
- "webmail/**"
|
||||
- ".github/workflows/docker-webmail.yml"
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: docker-webmail-${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
build_self_hosted:
|
||||
runs-on: self-hosted
|
||||
|
|
@ -44,7 +51,7 @@ jobs:
|
|||
uses: docker/setup-buildx-action@v4
|
||||
|
||||
- name: Log in to Docker Hub
|
||||
if: github.event_name == 'push'
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
|
|
@ -72,7 +79,7 @@ jobs:
|
|||
with:
|
||||
context: ./webmail
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: ${{ github.event_name == 'push' }}
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
cache-from: type=gha,scope=dockflare-webmail
|
||||
|
|
@ -97,7 +104,7 @@ jobs:
|
|||
uses: docker/setup-buildx-action@v4
|
||||
|
||||
- name: Log in to Docker Hub
|
||||
if: github.event_name == 'push'
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
|
|
@ -125,7 +132,7 @@ jobs:
|
|||
with:
|
||||
context: ./webmail
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: ${{ github.event_name == 'push' }}
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
cache-from: type=gha,scope=dockflare-webmail
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ All notable changes to this project will be documented in this file.
|
|||
- **Missing Access policies on fresh setups:** Fixed a regression from the system-policy work back in v3.0.x. On a fresh install the two built-in policies (`public-default-bypass` and `authenticated-default`) could fail to be created on Cloudflare because setup runs before the config is written, and the stale local-only reference then made DockFlare skip them on every later start - so label-created Access Apps came up with zero policies and every login was denied, with nothing useful in the logs. DockFlare now verifies both policies against Cloudflare on startup and creates or re-links them when they're missing. Also fixed a duplicate `public-default-bypass` being created each time the Access Policies page loaded, and a bad `dockflare.access.policy` value no longer silently strips an existing policy - it logs an error and leaves the app untouched. While in there, `cf_policy_id` and `cloudflare_policy_id` are now kept in sync. Thanks [@PauloJf](https://github.com/PauloJf) for the report and repro ([#399](https://github.com/ChrispyBacon-dev/DockFlare/issues/399)).
|
||||
- **Inbound email blocked when Webmail sits behind Cloudflare Access:** If your webmail hostname (e.g. `mail.example.com`) is protected by a Cloudflare Access application, the inbound email worker's webhook to `/api/v1/webhook/inbound` was being caught by the Access login redirect and never reached mail-manager. Outbound worked fine, but received mail just piled up in the R2 `temp_cache` with nothing in the logs, and the retry cron hit the same wall. DockFlare now creates a path-scoped `DockFlare Mail Webhook Bypass` Access application (bypass policy, exact path only) when you set up a domain, and on startup it detects existing installs where the webhook is behind Access and fixes them automatically. Only that one path is opened - the webhook is still authenticated by the per-domain HMAC signature, and your webmail UI stays protected. The auto-fix needs `Account > Access: Apps and Policies: Edit` on the API token (documented in the Email prerequisites).
|
||||
- **Webmail dependency refresh and build hygiene:** Upgraded the webmail frontend from Vite 5 to 8, Tiptap 2 to 3, `@vitejs/plugin-vue` 5 to 6, `vite-plugin-pwa` 0.21 to 1.3, and workbox 7.3 to 7.4. This clears all 35 `npm audit` findings (including one high-severity Vite advisory) — both webmail and DockFlare now audit clean. The rich-text composer was adjusted for Tiptap 3 (named `TextStyle` import; Link and Underline now ship inside StarterKit), the Vite config now uses `import.meta.dirname`, and the webmail build image moved to Node 22. Also fixed a long-standing build-hygiene issue: `vue-tsc` was emitting compiled `.js`/`.js.map` files next to the sources and they had been committed, so `noEmit` is now set, the 102 committed artifacts were removed, and the output is git-ignored.
|
||||
- **Email domain lifecycle fixes:** Three email issues surfaced while wiring up the webhook bypass. Tearing down a domain (or "teardown all") now removes the `DockFlare Mail Webhook Bypass` Access application it created instead of leaving it orphaned - and it skips the deletion if another configured domain still shares that webmail hostname. Creating a mailbox for an address that already has a routing rule (a leftover from a failed attempt, or one added manually) no longer fails with `409 Duplicated Zone rule`; DockFlare now finds the existing rule and updates it instead of blindly creating a new one. And quota KV namespace creation is idempotent again - it checks for an existing namespace before creating, so the `code 10014 "namespace already exists"` error no longer appears on every startup.
|
||||
|
||||
|
||||
## [v3.1.5] - 2026-09-01
|
||||
|
|
|
|||
|
|
@ -267,6 +267,24 @@ def set_worker_cron(script_name, cron_expressions):
|
|||
def delete_worker(script_name):
|
||||
return cf_api_request('DELETE', f'/accounts/{config.CF_ACCOUNT_ID}/workers/scripts/{script_name}')
|
||||
|
||||
def _find_email_routing_rule(zone_id, address):
|
||||
try:
|
||||
response = list_email_routing_rules(zone_id)
|
||||
rules = response.get('result') or []
|
||||
except Exception as e:
|
||||
logging.warning(f"Could not list email routing rules for zone {zone_id}: {e}")
|
||||
return None
|
||||
target = str(address or '').strip().lower()
|
||||
for rule in rules:
|
||||
for matcher in rule.get('matchers') or []:
|
||||
if (
|
||||
matcher.get('type') == 'literal'
|
||||
and matcher.get('field') == 'to'
|
||||
and str(matcher.get('value', '')).strip().lower() == target
|
||||
):
|
||||
return rule
|
||||
return None
|
||||
|
||||
def create_email_routing_rule(zone_id, address, worker_name):
|
||||
data = {
|
||||
"matchers": [{"type": "literal", "field": "to", "value": address}],
|
||||
|
|
@ -274,7 +292,21 @@ def create_email_routing_rule(zone_id, address, worker_name):
|
|||
"enabled": True,
|
||||
"name": f"DockFlare: {address}"
|
||||
}
|
||||
return cf_api_request('POST', f'/zones/{zone_id}/email/routing/rules', json_data=data)
|
||||
existing = _find_email_routing_rule(zone_id, address)
|
||||
if existing and existing.get('id'):
|
||||
logging.info(f"Email routing rule for {address} already exists; updating it instead of creating")
|
||||
return cf_api_request('PUT', f'/zones/{zone_id}/email/routing/rules/{existing["id"]}', json_data=data)
|
||||
try:
|
||||
return cf_api_request('POST', f'/zones/{zone_id}/email/routing/rules', json_data=data)
|
||||
except Exception as e:
|
||||
error_text = str(e).lower()
|
||||
if '409' not in error_text and 'duplicated' not in error_text and 'already exists' not in error_text:
|
||||
raise
|
||||
existing = _find_email_routing_rule(zone_id, address)
|
||||
if not existing or not existing.get('id'):
|
||||
raise
|
||||
logging.info(f"Email routing rule for {address} already exists (409); updating it")
|
||||
return cf_api_request('PUT', f'/zones/{zone_id}/email/routing/rules/{existing["id"]}', json_data=data)
|
||||
|
||||
def delete_email_routing_rule(zone_id, rule_id):
|
||||
return cf_api_request('DELETE', f'/zones/{zone_id}/email/routing/rules/{rule_id}')
|
||||
|
|
@ -356,19 +388,12 @@ def create_kv_namespace(title):
|
|||
return res.get('result', {}).get('id')
|
||||
|
||||
|
||||
def get_or_create_kv_namespace(title):
|
||||
try:
|
||||
ns_id = create_kv_namespace(title)
|
||||
if ns_id:
|
||||
return ns_id
|
||||
except Exception:
|
||||
pass
|
||||
def _list_kv_namespaces_tagged(title):
|
||||
page = 1
|
||||
while True:
|
||||
res = cf_api_request('GET', f'/accounts/{config.CF_ACCOUNT_ID}/storage/kv/namespaces',
|
||||
params={'per_page': 100, 'page': page})
|
||||
results = res.get('result') or []
|
||||
for ns in results:
|
||||
for ns in res.get('result') or []:
|
||||
if ns.get('title') == title:
|
||||
return ns.get('id')
|
||||
info = res.get('result_info', {})
|
||||
|
|
@ -377,6 +402,18 @@ def get_or_create_kv_namespace(title):
|
|||
page += 1
|
||||
return None
|
||||
|
||||
def get_or_create_kv_namespace(title):
|
||||
existing = _list_kv_namespaces_tagged(title)
|
||||
if existing:
|
||||
return existing
|
||||
try:
|
||||
ns_id = create_kv_namespace(title)
|
||||
if ns_id:
|
||||
return ns_id
|
||||
except Exception as e:
|
||||
logging.warning(f"Could not create KV namespace '{title}': {e}")
|
||||
return _list_kv_namespaces_tagged(title)
|
||||
|
||||
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"}
|
||||
|
|
@ -568,3 +605,35 @@ def ensure_webhook_access_bypass(webmail_hostname):
|
|||
)
|
||||
return None
|
||||
|
||||
|
||||
def delete_webhook_access_bypass(webmail_hostname):
|
||||
account_id = getattr(config, 'CF_ACCOUNT_ID', None)
|
||||
if not account_id or not webmail_hostname:
|
||||
return False
|
||||
|
||||
host = _normalize_access_destination(webmail_hostname)
|
||||
path = WEBHOOK_ACCESS_PATH
|
||||
|
||||
try:
|
||||
response = cf_api_request('GET', f'/accounts/{account_id}/access/apps', params={"per_page": 100})
|
||||
apps = response.get('result') or []
|
||||
except Exception as e:
|
||||
logging.warning(f"Could not list Access apps to remove webhook bypass for {host}: {e}")
|
||||
return False
|
||||
|
||||
existing = _find_webhook_bypass_app(apps, host, path)
|
||||
if not existing:
|
||||
return False
|
||||
|
||||
app_uuid = existing.get('id') or existing.get('uid')
|
||||
if not app_uuid:
|
||||
return False
|
||||
|
||||
try:
|
||||
cf_api_request('DELETE', f'/accounts/{account_id}/access/apps/{app_uuid}')
|
||||
logging.info(f"Deleted Cloudflare Access webhook bypass app for {host}{path}")
|
||||
return True
|
||||
except Exception as e:
|
||||
logging.warning(f"Could not delete Access webhook bypass app {app_uuid}: {e}")
|
||||
return False
|
||||
|
||||
|
|
|
|||
|
|
@ -142,7 +142,7 @@ def setup_email_domain():
|
|||
|
||||
quota_kv_ns_id = None
|
||||
try:
|
||||
quota_kv_ns_id = email_manager.create_kv_namespace(
|
||||
quota_kv_ns_id = email_manager.get_or_create_kv_namespace(
|
||||
f"dockflare-quota-{zone_name.replace('.', '-')}"
|
||||
)
|
||||
except Exception as e:
|
||||
|
|
@ -301,6 +301,18 @@ def teardown_domain():
|
|||
current_app.config['EMAIL_ENABLED'] = False
|
||||
save_email_config(email_cfg)
|
||||
|
||||
webmail_hostname = _get_webmail_hostname()
|
||||
teardown_host = webmail_hostname or f"mail.{zone_name}"
|
||||
remaining_hosts = {
|
||||
webmail_hostname or f"mail.{remaining_domain}"
|
||||
for remaining_domain in email_cfg.get('domains', {}).keys()
|
||||
}
|
||||
if teardown_host not in remaining_hosts:
|
||||
try:
|
||||
email_manager.delete_webhook_access_bypass(teardown_host)
|
||||
except Exception as e:
|
||||
errors.append(f"Webhook bypass: {e}")
|
||||
|
||||
_restart_mail_container()
|
||||
return jsonify({'success': True, 'errors': errors})
|
||||
|
||||
|
|
@ -313,6 +325,12 @@ def teardown_all():
|
|||
email_cfg = config.EMAIL_CONFIG.copy()
|
||||
errors = []
|
||||
|
||||
webmail_hostname = _get_webmail_hostname()
|
||||
webhook_hosts = {
|
||||
webmail_hostname or f"mail.{domain}"
|
||||
for domain in email_cfg.get('domains', {}).keys()
|
||||
}
|
||||
|
||||
for domain, domain_cfg in list(email_cfg.get('domains', {}).items()):
|
||||
errors.extend(_teardown_domain_remote(domain, domain_cfg))
|
||||
|
||||
|
|
@ -335,6 +353,12 @@ def teardown_all():
|
|||
current_app.config['EMAIL_ENABLED'] = False
|
||||
save_email_config(email_cfg)
|
||||
|
||||
for host in webhook_hosts:
|
||||
try:
|
||||
email_manager.delete_webhook_access_bypass(host)
|
||||
except Exception as e:
|
||||
errors.append(f"Webhook bypass ({host}): {e}")
|
||||
|
||||
_restart_mail_container()
|
||||
return jsonify({'success': True, 'errors': errors})
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue