mirror of
https://github.com/ChrispyBacon-dev/DockFlare.git
synced 2026-04-28 03:39:32 +00:00
285 lines
16 KiB
HTML
285 lines
16 KiB
HTML
{% extends "base.html" %}
|
|
|
|
{% block content %}
|
|
<div class="container mx-auto px-4 py-8">
|
|
|
|
<div class="flex flex-col sm:flex-row justify-between items-start sm:items-center mb-8 gap-4">
|
|
<div>
|
|
<h1 class="text-3xl font-bold">{{ t('email.title') }}</h1>
|
|
<p class="text-sm opacity-60 mt-1">{{ t('email.title_description') }}</p>
|
|
</div>
|
|
{% if email_enabled %}
|
|
<div class="flex gap-2 shrink-0">
|
|
<button class="btn btn-outline btn-sm" onclick="emailRedeployWorkers()">Redeploy Workers</button>
|
|
<button class="btn btn-primary btn-sm" onclick="emailOpenWebmail()">{{ t('email.webmail_link') }}</button>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
|
|
<div id="emailPermissionsBanner" class="alert alert-warning shadow-lg mb-8 hidden">
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" /></svg>
|
|
<div>
|
|
<h3 class="font-bold">{{ t('email.permissions_title') }}</h3>
|
|
<div class="text-sm mt-1">
|
|
<span id="permEmailRouting"></span> {{ t('email.permission_email_routing') }}<br>
|
|
<span id="permWorkers"></span> {{ t('email.permission_workers') }}<br>
|
|
<span id="permR2"></span> {{ t('email.permission_r2') }}
|
|
</div>
|
|
</div>
|
|
<button class="btn btn-sm btn-ghost" onclick="emailCheckPermissions()">{{ t('email.recheck_permissions') }}</button>
|
|
</div>
|
|
|
|
<section class="card bg-base-100 shadow-xl mb-8 sm:mb-12 transition-all duration-300 hover:shadow-2xl">
|
|
<div class="card-body overflow-visible">
|
|
<div class="flex flex-col sm:flex-row justify-between items-start sm:items-center border-b border-base-300 pb-4 mb-6 gap-4">
|
|
<div>
|
|
<h2 class="card-title text-2xl sm:text-3xl">{{ t('email.domain_setup') }}</h2>
|
|
<p class="text-sm opacity-60 mt-1">{{ t('email.domain_setup_description') }}</p>
|
|
</div>
|
|
<div class="flex items-center gap-2 w-full sm:w-auto">
|
|
<select id="emailZoneSelect" class="select select-bordered select-sm flex-1 sm:w-56">
|
|
<option disabled selected>{{ t('email.choose_domain') }}</option>
|
|
{% for zone in zones %}
|
|
<option value="{{ zone.id }}">{{ zone.name }}</option>
|
|
{% endfor %}
|
|
</select>
|
|
<button id="emailSetupBtn" class="btn btn-primary btn-sm shrink-0" onclick="emailSetupDomain(event)" disabled>{{ t('email.setup_email') }}</button>
|
|
</div>
|
|
</div>
|
|
<table class="table table-zebra w-full">
|
|
<colgroup>
|
|
<col>
|
|
<col class="w-36">
|
|
<col class="w-64">
|
|
</colgroup>
|
|
<thead>
|
|
<tr>
|
|
<th class="px-4 py-3">{{ t('email.domain') }}</th>
|
|
<th class="px-4 py-3">{{ t('email.status') }}</th>
|
|
<th class="px-4 py-3 text-right">{{ t('email.actions') }}</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% if email_config.domains %}
|
|
{% for domain, cfg in email_config.domains.items() %}
|
|
<tr>
|
|
<td class="px-4 py-3">{{ domain }}</td>
|
|
<td class="px-4 py-3"><div class="badge badge-success badge-sm">{{ t('email.setup_complete') }}</div></td>
|
|
<td class="px-4 py-3">
|
|
<div class="flex items-center justify-end gap-1">
|
|
<button class="btn btn-sm btn-ghost" onclick="emailVerifyDns('{{ domain }}', event)">{{ t('email.dns_verify') }}</button>
|
|
<button class="btn btn-sm btn-ghost" onclick="emailRepairDns('{{ domain }}', event)">Repair DNS</button>
|
|
<div class="dropdown dropdown-end">
|
|
<button tabindex="0" class="btn btn-sm btn-ghost px-2">⋮</button>
|
|
<ul tabindex="0" class="dropdown-content z-50 menu menu-sm p-2 shadow-lg bg-base-100 border border-base-200 rounded-box w-52">
|
|
<li><button onclick="emailUpdateR2('{{ domain }}', event)">R2 Credentials</button></li>
|
|
<li class="menu-title">Danger zone</li>
|
|
<li><button onclick="emailTeardownPartial('{{ domain }}', event)" class="text-warning">{{ t('email.teardown_partial') }}</button></li>
|
|
<li><button onclick="emailTeardownComplete('{{ domain }}', event)" class="text-error">{{ t('email.teardown_complete') }}</button></li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
{% endfor %}
|
|
{% else %}
|
|
<tr><td colspan="3" class="px-4 py-8 text-center opacity-50">{{ t('email.no_domains') }}</td></tr>
|
|
{% endif %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</section>
|
|
|
|
{% if email_enabled %}
|
|
|
|
<section class="card bg-base-100 shadow-xl mb-8 sm:mb-12 transition-all duration-300 hover:shadow-2xl">
|
|
<div class="card-body">
|
|
<div class="flex flex-col sm:flex-row justify-between items-start sm:items-center border-b border-base-300 pb-4 mb-6 gap-4">
|
|
<div>
|
|
<h2 class="card-title text-2xl sm:text-3xl">{{ t('email.mailbox_management') }}</h2>
|
|
<p class="text-sm opacity-60 mt-1">{{ t('email.mailbox_description') }}</p>
|
|
</div>
|
|
</div>
|
|
<div class="flex flex-wrap items-center gap-2 mb-6">
|
|
<div class="flex items-center gap-1 flex-1 min-w-48">
|
|
<input type="text" id="newMailboxAddress" placeholder="{{ t('email.address') }}" class="input input-bordered input-sm w-32 flex-1" />
|
|
<span class="text-sm opacity-40 select-none px-1">@</span>
|
|
<select id="newMailboxDomain" class="select select-bordered select-sm">
|
|
{% if email_config and email_config.domains %}
|
|
{% for domain in email_config.domains %}
|
|
<option value="{{ domain }}">{{ domain }}</option>
|
|
{% endfor %}
|
|
{% endif %}
|
|
</select>
|
|
</div>
|
|
<input type="text" id="newMailboxName" placeholder="{{ t('email.display_name') }}" class="input input-bordered input-sm flex-1 min-w-32" />
|
|
<button class="btn btn-primary btn-sm shrink-0" onclick="emailCreateMailbox(event)">{{ t('email.add_mailbox') }}</button>
|
|
</div>
|
|
<div class="overflow-x-auto">
|
|
<table class="table table-zebra w-full">
|
|
<colgroup>
|
|
<col>
|
|
<col class="w-36">
|
|
<col class="w-32">
|
|
<col class="w-40">
|
|
</colgroup>
|
|
<thead>
|
|
<tr>
|
|
<th class="px-4 py-3">{{ t('email.address') }}</th>
|
|
<th class="px-4 py-3">{{ t('email.display_name') }}</th>
|
|
<th class="px-4 py-3">{{ t('email.domain') }}</th>
|
|
<th class="px-4 py-3 text-right">{{ t('email.actions') }}</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% if email_config and email_config.domains %}
|
|
{% for domain, cfg in email_config.domains.items() %}
|
|
{% for addr, mb in cfg.mailboxes.items() %}
|
|
<tr>
|
|
<td class="px-4 py-3 font-mono text-sm">{{ addr }}</td>
|
|
<td class="px-4 py-3">{{ mb.display_name }}</td>
|
|
<td class="px-4 py-3 opacity-50 text-sm">{{ domain }}</td>
|
|
<td class="px-4 py-3">
|
|
<div class="flex items-center justify-end gap-1">
|
|
<button class="btn btn-sm btn-outline" onclick="emailSetPassword('{{ addr }}', '{{ domain }}')">Set Password</button>
|
|
<button class="btn btn-sm btn-error btn-outline" onclick="emailDeleteMailbox('{{ addr }}', '{{ domain }}', event)">{{ t('email.delete') }}</button>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
{% endfor %}
|
|
{% endfor %}
|
|
{% else %}
|
|
<tr><td colspan="4" class="px-4 py-8 text-center opacity-50">{{ t('email.no_mailboxes') }}</td></tr>
|
|
{% endif %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<section class="card bg-base-100 shadow-xl mb-8 sm:mb-12 transition-all duration-300 hover:shadow-2xl">
|
|
<div class="card-body">
|
|
<div class="border-b border-base-300 pb-4 mb-6">
|
|
<h2 class="card-title text-2xl sm:text-3xl">{{ t('email.statistics') }}</h2>
|
|
<p class="text-sm opacity-60 mt-1">{{ t('email.statistics_description') }}</p>
|
|
</div>
|
|
<div class="stats stats-horizontal shadow w-full">
|
|
<div class="stat">
|
|
<div class="stat-title">{{ t('email.stats_received') }}</div>
|
|
<div class="stat-value text-primary" id="statReceived">0</div>
|
|
</div>
|
|
<div class="stat">
|
|
<div class="stat-title">{{ t('email.stats_sent') }}</div>
|
|
<div class="stat-value text-secondary" id="statSent">0</div>
|
|
</div>
|
|
<div class="stat">
|
|
<div class="stat-title">{{ t('email.stats_storage') }}</div>
|
|
<div class="stat-value text-accent" id="statStorage">0</div>
|
|
</div>
|
|
<div class="stat">
|
|
<div class="stat-title">{{ t('email.stats_mailboxes') }}</div>
|
|
<div class="stat-value text-info" id="statMailboxes">0</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<section class="card bg-base-100 shadow-xl mb-8 sm:mb-12 transition-all duration-300 hover:shadow-2xl">
|
|
<div class="card-body">
|
|
<div class="border-b border-base-300 pb-4 mb-6">
|
|
<h2 class="card-title text-2xl sm:text-3xl">{{ t('email.backup_restore') }}</h2>
|
|
<p class="text-sm opacity-60 mt-1">{{ t('email.backup_restore_description') }}</p>
|
|
</div>
|
|
<div class="grid sm:grid-cols-2 gap-8">
|
|
<div>
|
|
<h3 class="font-semibold mb-1">{{ t('email.backup_title') }}</h3>
|
|
<p class="text-sm opacity-60 mb-3">{{ t('email.backup_description') }}</p>
|
|
<p class="text-xs text-warning font-medium mb-4">⚠ {{ t('email.backup_security_warning') }}</p>
|
|
<a href="{{ url_for('email.email_backup') }}" class="btn btn-primary btn-sm">{{ t('email.download_backup') }}</a>
|
|
</div>
|
|
<div>
|
|
<h3 class="font-semibold mb-1">{{ t('email.restore_title') }}</h3>
|
|
<p class="text-sm opacity-60 mb-3">{{ t('email.restore_description') }}</p>
|
|
<p class="text-xs text-error font-medium mb-4">⚠ {{ t('email.restore_warning') }}</p>
|
|
<div class="flex gap-2 items-center">
|
|
<label class="btn btn-outline btn-sm" for="emailRestoreFile">Choose file</label>
|
|
<span id="emailRestoreFileName" class="text-sm opacity-50 truncate flex-1">No file chosen</span>
|
|
<input type="file" id="emailRestoreFile" accept=".zip" class="hidden" onchange="document.getElementById('emailRestoreFileName').textContent = this.files[0]?.name || 'No file chosen'" />
|
|
<button class="btn btn-error btn-sm shrink-0" id="emailRestoreBtn" onclick="emailRestoreBackup()">{{ t('email.restore_backup') }}</button>
|
|
</div>
|
|
<p id="emailRestoreFeedback" class="text-sm mt-2 font-semibold hidden"></p>
|
|
</div>
|
|
</div>
|
|
<div id="emailOrphanedSection" class="hidden mt-6">
|
|
<div class="divider"></div>
|
|
<h3 class="font-semibold mb-1">{{ t('email.orphaned_data_title') }}</h3>
|
|
<p class="text-sm opacity-60 mb-3">{{ t('email.orphaned_data_description') }}</p>
|
|
<div id="emailOrphanedList"></div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<section class="card bg-base-100 shadow-xl border border-error mb-8 sm:mb-12 transition-all duration-300 hover:shadow-2xl">
|
|
<div class="card-body">
|
|
<div class="border-b border-error border-opacity-30 pb-4 mb-6">
|
|
<h2 class="card-title text-2xl sm:text-3xl text-error">{{ t('email.nuke_all_title') }}</h2>
|
|
<p class="text-sm opacity-60 mt-1">{{ t('email.nuke_all_description') }}</p>
|
|
</div>
|
|
<div class="flex gap-2">
|
|
<button class="btn btn-warning btn-sm" onclick="emailNukeAll(false, event)">{{ t('email.nuke_all_partial') }}</button>
|
|
<button class="btn btn-error btn-sm" onclick="emailNukeAll(true, event)">{{ t('email.nuke_all_complete') }}</button>
|
|
</div>
|
|
<p id="emailNukeFeedback" class="text-sm mt-2 font-semibold hidden"></p>
|
|
</div>
|
|
</section>
|
|
|
|
{% else %}
|
|
|
|
<section class="card bg-base-100 shadow-xl mb-8">
|
|
<div class="card-body">
|
|
<div class="alert alert-info">
|
|
<div>
|
|
<span>{{ t('email.container_stopped') }}</span><br>
|
|
<code class="text-sm mt-2 block">docker compose --profile email up -d</code>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
{% endif %}
|
|
|
|
</div>
|
|
|
|
<dialog id="emailSetPasswordModal" class="modal">
|
|
<div class="modal-box">
|
|
<h3 class="font-bold text-lg mb-1">Set Password</h3>
|
|
<p class="text-sm opacity-60 mb-4" id="emailSetPasswordTarget"></p>
|
|
<div class="space-y-3">
|
|
<input type="password" id="emailSetPasswordNew" placeholder="New password (min 8 characters)" class="input input-bordered w-full" autocomplete="new-password" />
|
|
<input type="password" id="emailSetPasswordConfirm" placeholder="Confirm password" class="input input-bordered w-full" autocomplete="new-password" />
|
|
<p id="emailSetPasswordError" class="text-error text-sm hidden"></p>
|
|
</div>
|
|
<div class="modal-action">
|
|
<form method="dialog"><button class="btn btn-ghost">Cancel</button></form>
|
|
<button class="btn btn-primary" id="emailSetPasswordSubmitBtn">Set Password</button>
|
|
</div>
|
|
</div>
|
|
<form method="dialog" class="modal-backdrop"><button>close</button></form>
|
|
</dialog>
|
|
|
|
<div id="emailProgressPanel" class="fixed bottom-4 right-4 w-80 z-50 transition-all duration-300 translate-y-4 opacity-0 pointer-events-none">
|
|
<div class="card bg-base-200 shadow-xl border border-base-300">
|
|
<div class="card-body p-4 gap-2">
|
|
<div class="flex items-center justify-between">
|
|
<h3 class="font-bold text-sm" id="emailProgressTitle"></h3>
|
|
<button class="btn btn-ghost btn-xs btn-circle" id="emailProgressClose">✕</button>
|
|
</div>
|
|
<ul id="emailProgressSteps" class="space-y-1 text-sm"></ul>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{% block scripts %}
|
|
<script>document.addEventListener('DOMContentLoaded', () => { emailCheckPermissions(); emailLoadOrphanedDomains(); });</script>
|
|
{% endblock %}
|
|
{% endblock %}
|