change admin menu url to smtp-server

This commit is contained in:
2026-08-15 21:51:43 +01:00
parent f60dbcc6b0
commit ca0ad630d2
45 changed files with 188 additions and 188 deletions
+3 -3
View File
@@ -22,7 +22,7 @@ a Python venv + separate services.
drafts, search, keyboard shortcuts, conversation grouping, recipient autocomplete,
filter-rule management, and PGP (OpenPGP encrypt/decrypt/sign/verify) and S/MIME
(sign/verify) support per mailbox.
- **Admin dashboard** (`/pymta-manager`) — manage domains, senders, mailboxes, DKIM
- **Admin dashboard** (`/smtp-server`) — manage domains, senders, mailboxes, DKIM
keys, IP whitelisting, TLS (self-signed/custom, or up to two simultaneous Let's
Encrypt certificates — DNS-01 via Cloudflare/Route53/DigitalOcean/Google Cloud DNS, and
HTTP-01 for domains you don't manage DNS for, optionally covering the server's own IP
@@ -75,11 +75,11 @@ directory, it generates:
on every subsequent start — back these up like any other secret (losing the mailstore
master key makes all stored mail unrecoverable, even for admins).
From there: log into `/pymta-manager`, add a domain (and complete its DNS ownership
From there: log into `/smtp-server`, add a domain (and complete its DNS ownership
verification), add a mailbox or sender, and you're sending/receiving. The bare `/` root
redirects to the webmail login (`/webmail/login`) by default, since most visitors are
mailbox owners, not admins — there's a "Login as Admin" link from there to
`/pymta-manager`.
`/smtp-server`.
Default ports (all configurable in `settings.ini`): SMTP `25`, direct-TLS SMTP `465`,
IMAP `143`, direct-TLS IMAP `993` — the real standard mail ports, so binding them
@@ -1 +1 @@
{"nodes": [{"id": "internal_webui_templates_account_account_settings_page", "label": "Account Settings Page", "file_type": "document", "source_file": "internal/webui/templates/account.html", "source_location": null, "source_url": null, "captured_at": null, "author": null, "contributor": null}, {"id": "internal_webui_templates_account_totp_mfa_section", "label": "TOTP MFA Section", "file_type": "document", "source_file": "internal/webui/templates/account.html", "source_location": null, "source_url": null, "captured_at": null, "author": null, "contributor": null}, {"id": "internal_webui_templates_account_passkey_management", "label": "Passkey / WebAuthn Management", "file_type": "document", "source_file": "internal/webui/templates/account.html", "source_location": null, "source_url": null, "captured_at": null, "author": null, "contributor": null, "rationale": "Uses the browser navigator.credentials.create() WebAuthn API directly against /pymta-manager/account/passkey/begin and /finish, base64url-encoding challenge/credential buffers by hand since no client library is loaded."}], "edges": [{"source": "internal_webui_templates_account_account_settings_page", "target": "internal_webui_templates_base_base_layout", "relation": "references", "confidence": "EXTRACTED", "confidence_score": 1.0, "source_file": "internal/webui/templates/account.html", "source_location": null, "weight": 1.0}, {"source": "internal_webui_templates_account_account_settings_page", "target": "internal_webui_templates_account_totp_mfa_section", "relation": "references", "confidence": "EXTRACTED", "confidence_score": 1.0, "source_file": "internal/webui/templates/account.html", "source_location": null, "weight": 1.0}, {"source": "internal_webui_templates_account_account_settings_page", "target": "internal_webui_templates_account_passkey_management", "relation": "references", "confidence": "EXTRACTED", "confidence_score": 1.0, "source_file": "internal/webui/templates/account.html", "source_location": null, "weight": 1.0}, {"source": "internal_webui_templates_account_totp_mfa_section", "target": "readme_totp_mfa", "relation": "conceptually_related_to", "confidence": "INFERRED", "confidence_score": 0.95, "source_file": "internal/webui/templates/account.html", "source_location": null, "weight": 1.0}, {"source": "internal_webui_templates_account_passkey_management", "target": "readme_webauthn_passkeys", "relation": "conceptually_related_to", "confidence": "INFERRED", "confidence_score": 0.95, "source_file": "internal/webui/templates/account.html", "source_location": null, "weight": 1.0}], "hyperedges": [{"id": "mfa_second_factor_flow", "label": "Optional MFA / Second-Factor Setup Flow", "nodes": ["readme_admin_dashboard_login", "internal_webui_templates_account_account_settings_page", "internal_webui_templates_account_totp_mfa_section", "internal_webui_templates_account_passkey_management"], "relation": "participate_in", "confidence": "INFERRED", "confidence_score": 0.85, "source_file": "internal/webui/templates/account.html"}]}
{"nodes": [{"id": "internal_webui_templates_account_account_settings_page", "label": "Account Settings Page", "file_type": "document", "source_file": "internal/webui/templates/account.html", "source_location": null, "source_url": null, "captured_at": null, "author": null, "contributor": null}, {"id": "internal_webui_templates_account_totp_mfa_section", "label": "TOTP MFA Section", "file_type": "document", "source_file": "internal/webui/templates/account.html", "source_location": null, "source_url": null, "captured_at": null, "author": null, "contributor": null}, {"id": "internal_webui_templates_account_passkey_management", "label": "Passkey / WebAuthn Management", "file_type": "document", "source_file": "internal/webui/templates/account.html", "source_location": null, "source_url": null, "captured_at": null, "author": null, "contributor": null, "rationale": "Uses the browser navigator.credentials.create() WebAuthn API directly against /smtp-server/account/passkey/begin and /finish, base64url-encoding challenge/credential buffers by hand since no client library is loaded."}], "edges": [{"source": "internal_webui_templates_account_account_settings_page", "target": "internal_webui_templates_base_base_layout", "relation": "references", "confidence": "EXTRACTED", "confidence_score": 1.0, "source_file": "internal/webui/templates/account.html", "source_location": null, "weight": 1.0}, {"source": "internal_webui_templates_account_account_settings_page", "target": "internal_webui_templates_account_totp_mfa_section", "relation": "references", "confidence": "EXTRACTED", "confidence_score": 1.0, "source_file": "internal/webui/templates/account.html", "source_location": null, "weight": 1.0}, {"source": "internal_webui_templates_account_account_settings_page", "target": "internal_webui_templates_account_passkey_management", "relation": "references", "confidence": "EXTRACTED", "confidence_score": 1.0, "source_file": "internal/webui/templates/account.html", "source_location": null, "weight": 1.0}, {"source": "internal_webui_templates_account_totp_mfa_section", "target": "readme_totp_mfa", "relation": "conceptually_related_to", "confidence": "INFERRED", "confidence_score": 0.95, "source_file": "internal/webui/templates/account.html", "source_location": null, "weight": 1.0}, {"source": "internal_webui_templates_account_passkey_management", "target": "readme_webauthn_passkeys", "relation": "conceptually_related_to", "confidence": "INFERRED", "confidence_score": 0.95, "source_file": "internal/webui/templates/account.html", "source_location": null, "weight": 1.0}], "hyperedges": [{"id": "mfa_second_factor_flow", "label": "Optional MFA / Second-Factor Setup Flow", "nodes": ["readme_admin_dashboard_login", "internal_webui_templates_account_account_settings_page", "internal_webui_templates_account_totp_mfa_section", "internal_webui_templates_account_passkey_management"], "relation": "participate_in", "confidence": "INFERRED", "confidence_score": 0.85, "source_file": "internal/webui/templates/account.html"}]}
+1 -1
View File
@@ -10719,7 +10719,7 @@
"captured_at": null,
"author": null,
"contributor": null,
"rationale": "Uses the browser navigator.credentials.create() WebAuthn API directly against /pymta-manager/account/passkey/begin and /finish, base64url-encoding challenge/credential buffers by hand since no client library is loaded.",
"rationale": "Uses the browser navigator.credentials.create() WebAuthn API directly against /smtp-server/account/passkey/begin and /finish, base64url-encoding challenge/credential buffers by hand since no client library is loaded.",
"id": "internal_webui_templates_account_passkey_management",
"community": 16,
"community_name": "Admin Dashboard & Deployment Docs",
+2 -2
View File
@@ -98,7 +98,7 @@ func TestAddAppPasswordSetsRevealCookieNotFlash(t *testing.T) {
mboxID := mailboxes[0].ID
form := url.Values{"label": {"laptop"}, "expires_preset": {"never"}}
req := httptest.NewRequest(http.MethodPost, "/pymta-manager/mailboxes/"+itoa(mboxID)+"/apppasswords/add", strings.NewReader(form.Encode()))
req := httptest.NewRequest(http.MethodPost, "/smtp-server/mailboxes/"+itoa(mboxID)+"/apppasswords/add", strings.NewReader(form.Encode()))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
req.AddCookie(cookie)
rec := httptest.NewRecorder()
@@ -125,7 +125,7 @@ func TestAddAppPasswordSetsRevealCookieNotFlash(t *testing.T) {
// Following the redirect (as the browser would) should render the reveal modal
// with the secret, and clear the one-time cookie.
req2 := httptest.NewRequest(http.MethodGet, "/pymta-manager/mailboxes/"+itoa(mboxID)+"/apppasswords", nil)
req2 := httptest.NewRequest(http.MethodGet, "/smtp-server/mailboxes/"+itoa(mboxID)+"/apppasswords", nil)
req2.AddCookie(cookie)
req2.AddCookie(revealCookie)
rec2 := httptest.NewRecorder()
+1 -1
View File
@@ -56,7 +56,7 @@ func TestAdminMFAEnforcementForcesIsolatedSetupThenReleases(t *testing.T) {
if !strings.Contains(body, "Two-factor authentication required") {
t.Error("expected the MFA-required heading on /mfa-setup")
}
if strings.Contains(body, "sidebar") || strings.Contains(body, `href="/pymta-manager/domains"`) {
if strings.Contains(body, "sidebar") || strings.Contains(body, `href="/smtp-server/domains"`) {
t.Error("expected no sidebar/navigation on the isolated setup page")
}
+10 -10
View File
@@ -24,12 +24,12 @@ func TestScopedAdminCannotAccessServerSettingsOrLetsEncrypt(t *testing.T) {
routes := []struct {
method, path string
}{
{http.MethodGet, "/pymta-manager/settings"},
{http.MethodPost, "/pymta-manager/settings_update"},
{http.MethodGet, "/pymta-manager/letsencrypt"},
{http.MethodPost, "/pymta-manager/letsencrypt/save"},
{http.MethodPost, "/pymta-manager/letsencrypt/obtain"},
{http.MethodGet, "/pymta-manager/api/settings/get_public_ip"},
{http.MethodGet, "/smtp-server/settings"},
{http.MethodPost, "/smtp-server/settings_update"},
{http.MethodGet, "/smtp-server/letsencrypt"},
{http.MethodPost, "/smtp-server/letsencrypt/save"},
{http.MethodPost, "/smtp-server/letsencrypt/obtain"},
{http.MethodGet, "/smtp-server/api/settings/get_public_ip"},
}
for _, rt := range routes {
req := httptest.NewRequest(rt.method, rt.path, nil)
@@ -43,7 +43,7 @@ func TestScopedAdminCannotAccessServerSettingsOrLetsEncrypt(t *testing.T) {
// A global admin must still reach these routes normally.
globalCookie := loginSession(t, app)
req := httptest.NewRequest(http.MethodGet, "/pymta-manager/settings", nil)
req := httptest.NewRequest(http.MethodGet, "/smtp-server/settings", nil)
req.AddCookie(globalCookie)
rec := httptest.NewRecorder()
mux.ServeHTTP(rec, req)
@@ -61,7 +61,7 @@ func TestScopedAdminSidebarHidesServerWideLinks(t *testing.T) {
domains, _ := app.DB.ListDomains()
cookie := scopedLogin(t, app, "scoped-sidebar-admin", []int64{domains[0].ID})
req := httptest.NewRequest(http.MethodGet, "/pymta-manager/", nil)
req := httptest.NewRequest(http.MethodGet, "/smtp-server/", nil)
req.AddCookie(cookie)
rec := httptest.NewRecorder()
mux.ServeHTTP(rec, req)
@@ -69,10 +69,10 @@ func TestScopedAdminSidebarHidesServerWideLinks(t *testing.T) {
t.Fatalf("dashboard: status=%d body=%s", rec.Code, rec.Body.String())
}
body := rec.Body.String()
if strings.Contains(body, `href="/pymta-manager/settings"`) {
if strings.Contains(body, `href="/smtp-server/settings"`) {
t.Error("scoped admin's sidebar should not link to Server Settings")
}
if strings.Contains(body, `href="/pymta-manager/letsencrypt"`) {
if strings.Contains(body, `href="/smtp-server/letsencrypt"`) {
t.Error("scoped admin's sidebar should not link to Let's Encrypt")
}
}
+1 -1
View File
@@ -35,7 +35,7 @@ func TestSettingsUpdateSavesUppercaseDefinedKeys(t *testing.T) {
cookie := loginSession(t, app)
form := url.Values{"Server.smtp_port": {"2525"}}
req := httptest.NewRequest(http.MethodPost, "/pymta-manager/settings_update", strings.NewReader(form.Encode()))
req := httptest.NewRequest(http.MethodPost, "/smtp-server/settings_update", strings.NewReader(form.Encode()))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
req.AddCookie(cookie)
rec := httptest.NewRecorder()
+6 -6
View File
@@ -10,7 +10,7 @@
<p><strong>Username:</strong> {{.user.Username}}</p>
<hr>
<h6>Change password</h6>
<form method="POST" action="/pymta-manager/account/password">
<form method="POST" action="/smtp-server/account/password">
<div class="mb-3">
<label class="form-label">Current password</label>
<input type="password" class="form-control" name="current_password" required>
@@ -36,12 +36,12 @@
<div class="card-body">
{{if .user.TOTPEnabled}}
<p class="text-success"><i class="bi bi-check-circle me-1"></i>Enabled</p>
<form method="POST" action="/pymta-manager/account/totp/disable">
<form method="POST" action="/smtp-server/account/totp/disable">
<button type="submit" class="btn btn-outline-danger btn-sm" data-confirm="Disable authenticator app MFA?">Disable</button>
</form>
{{else}}
<p class="text-muted">Not enabled. Add an authenticator app (Google Authenticator, 1Password, etc.) as an optional second factor.</p>
<form method="POST" action="/pymta-manager/account/totp/setup">
<form method="POST" action="/smtp-server/account/totp/setup">
<button type="submit" class="btn btn-outline-primary btn-sm"><i class="bi bi-qr-code me-1"></i>Set up</button>
</form>
{{end}}
@@ -56,7 +56,7 @@
{{range .passkeys}}
<li class="list-group-item d-flex justify-content-between align-items-center">
<span><i class="bi bi-key me-2"></i>{{.Name}} <small class="text-muted">added {{strftime "%Y-%m-%d" .CreatedAt}}</small></span>
<form method="POST" action="/pymta-manager/account/passkey/{{.ID}}/remove">
<form method="POST" action="/smtp-server/account/passkey/{{.ID}}/remove">
<button type="submit" class="btn btn-sm btn-outline-danger" data-confirm="Remove this passkey?">Remove</button>
</form>
</li>
@@ -96,7 +96,7 @@
try {
const name = prompt('Name this passkey (e.g. "YubiKey", "MacBook Touch ID"):', 'Passkey') || 'Passkey';
const beginResp = await fetch('/pymta-manager/account/passkey/begin', { method: 'POST' });
const beginResp = await fetch('/smtp-server/account/passkey/begin', { method: 'POST' });
if (!beginResp.ok) throw new Error((await beginResp.json()).error || 'Could not start passkey registration');
const options = await beginResp.json();
@@ -119,7 +119,7 @@
},
};
const finishResp = await fetch('/pymta-manager/account/passkey/finish?name=' + encodeURIComponent(name), {
const finishResp = await fetch('/smtp-server/account/passkey/finish?name=' + encodeURIComponent(name), {
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body),
});
if (!finishResp.ok) throw new Error((await finishResp.json()).error || 'Could not save passkey');
+1 -1
View File
@@ -45,7 +45,7 @@
</div>
<div class="d-flex justify-content-between">
<a href="/pymta-manager/admins" class="btn btn-secondary"><i class="bi bi-arrow-left me-2"></i>Back</a>
<a href="/smtp-server/admins" class="btn btn-secondary"><i class="bi bi-arrow-left me-2"></i>Back</a>
<button type="submit" class="btn btn-primary"><i class="bi bi-person-plus me-2"></i>Create Admin</button>
</div>
</form>
+1 -1
View File
@@ -24,7 +24,7 @@
</ul>
</div>
<div class="d-flex justify-content-between">
<a href="/pymta-manager/domains" class="btn btn-secondary"><i class="bi bi-arrow-left me-2"></i>Back to Domains</a>
<a href="/smtp-server/domains" class="btn btn-secondary"><i class="bi bi-arrow-left me-2"></i>Back to Domains</a>
<button type="submit" class="btn btn-primary"><i class="bi bi-plus-circle me-2"></i>Add Domain</button>
</div>
</form>
+1 -1
View File
@@ -51,7 +51,7 @@
</ul>
</div>
<div class="d-flex justify-content-between">
<a href="/pymta-manager/ips" class="btn btn-secondary"><i class="bi bi-arrow-left me-2"></i>Back to IP List</a>
<a href="/smtp-server/ips" class="btn btn-secondary"><i class="bi bi-arrow-left me-2"></i>Back to IP List</a>
<button type="submit" class="btn btn-success"><i class="bi bi-shield-plus me-2"></i>Add to Whitelist</button>
</div>
</form>
+1 -1
View File
@@ -32,7 +32,7 @@
<div class="form-text">Leave blank to use the domain's default quota.</div>
</div>
<div class="d-flex justify-content-between">
<a href="/pymta-manager/mailboxes" class="btn btn-secondary"><i class="bi bi-arrow-left me-2"></i>Back to Mailboxes</a>
<a href="/smtp-server/mailboxes" class="btn btn-secondary"><i class="bi bi-arrow-left me-2"></i>Back to Mailboxes</a>
<button type="submit" class="btn btn-success"><i class="bi bi-mailbox me-2"></i>Add Mailbox</button>
</div>
</form>
+1 -1
View File
@@ -48,7 +48,7 @@
</ul>
</div>
<div class="d-flex justify-content-between">
<a href="/pymta-manager/senders" class="btn btn-secondary"><i class="bi bi-arrow-left me-2"></i>Back to Senders</a>
<a href="/smtp-server/senders" class="btn btn-secondary"><i class="bi bi-arrow-left me-2"></i>Back to Senders</a>
<button type="submit" class="btn btn-success"><i class="bi bi-person-plus me-2"></i>Add Sender</button>
</div>
</form>
+5 -5
View File
@@ -4,7 +4,7 @@
{{define "content"}}
<div class="d-flex justify-content-between align-items-center mb-4">
<h2><i class="bi bi-people-fill me-2"></i>Admins</h2>
<a href="/pymta-manager/admins/add" class="btn btn-primary"><i class="bi bi-person-plus me-2"></i>Add Admin</a>
<a href="/smtp-server/admins/add" class="btn btn-primary"><i class="bi bi-person-plus me-2"></i>Add Admin</a>
</div>
<div class="alert alert-info">
@@ -40,12 +40,12 @@
{{else}}
<div class="btn-group btn-group-sm" role="group">
{{if not $u.IsGlobalAdmin}}
<a href="/pymta-manager/admins/{{$u.ID}}/edit" class="btn btn-outline-primary" title="Edit Domain Access"><i class="bi bi-pencil"></i></a>
<a href="/smtp-server/admins/{{$u.ID}}/edit" class="btn btn-outline-primary" title="Edit Domain Access"><i class="bi bi-pencil"></i></a>
{{end}}
<form method="post" action="/pymta-manager/admins/{{$u.ID}}/reset_mfa" class="d-inline">
<form method="post" action="/smtp-server/admins/{{$u.ID}}/reset_mfa" class="d-inline">
<button type="submit" class="btn btn-outline-warning" data-confirm="Reset MFA for {{$u.Username}}? They will need to re-enroll an authenticator app or passkey." title="Reset MFA"><i class="bi bi-shield-x"></i></button>
</form>
<form method="post" action="/pymta-manager/admins/{{$u.ID}}/remove" class="d-inline">
<form method="post" action="/smtp-server/admins/{{$u.ID}}/remove" class="d-inline">
<button type="submit" class="btn btn-outline-danger" data-confirm="Permanently remove admin {{$u.Username}}? This cannot be undone." title="Remove Admin"><i class="bi bi-trash"></i></button>
</form>
</div>
@@ -61,7 +61,7 @@
<i class="bi bi-people text-muted" style="font-size: 4rem;"></i>
<h4 class="text-muted mt-3">No delegated admins yet</h4>
<p class="text-muted">Add an admin and assign them the domains they should manage</p>
<a href="/pymta-manager/admins/add" class="btn btn-primary"><i class="bi bi-person-plus me-2"></i>Add Your First Admin</a>
<a href="/smtp-server/admins/add" class="btn btn-primary"><i class="bi bi-person-plus me-2"></i>Add Your First Admin</a>
</div>
{{end}}
</div>
+5 -5
View File
@@ -13,8 +13,8 @@
}
</script>
<link href="/pymta-manager/static/vendor/bootstrap/css/bootstrap.min.css" rel="stylesheet">
<link href="/pymta-manager/static/vendor/bootstrap-icons/font/bootstrap-icons.css" rel="stylesheet">
<link href="/smtp-server/static/vendor/bootstrap/css/bootstrap.min.css" rel="stylesheet">
<link href="/smtp-server/static/vendor/bootstrap-icons/font/bootstrap-icons.css" rel="stylesheet">
<style>
:root { --sidebar-width: 280px; }
@@ -54,7 +54,7 @@
::-webkit-scrollbar-thumb:hover { background: #6c757d; }
</style>
<link href="/pymta-manager/static/css/smtp-management.css" rel="stylesheet">
<link href="/smtp-server/static/css/smtp-management.css" rel="stylesheet">
<style>
.tooltip-inner { color: #fff !important; background-color: #222 !important; font-size: 1rem; text-align: left; }
@@ -129,7 +129,7 @@
</div>
</div>
<script src="/pymta-manager/static/vendor/bootstrap/js/bootstrap.bundle.min.js"></script>
<script src="/smtp-server/static/vendor/bootstrap/js/bootstrap.bundle.min.js"></script>
<script>
function updateTime() {
@@ -265,7 +265,7 @@
});
</script>
<script src="/pymta-manager/static/js/smtp-management.js"></script>
<script src="/smtp-server/static/js/smtp-management.js"></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
+6 -6
View File
@@ -24,10 +24,10 @@
<td><small class="text-muted">{{strftime "%Y-%m-%d %H:%M" .ExpiresAt}}</small></td>
<td>
<div class="btn-group" role="group">
<form method="post" action="/pymta-manager/blacklist/{{.ID}}/whitelist" class="d-inline">
<form method="post" action="/smtp-server/blacklist/{{.ID}}/whitelist" class="d-inline">
<button type="submit" class="btn btn-outline-success btn-sm" title="Whitelist this IP" data-confirm="Remove {{.IPAddress}} from the blacklist and exempt it from abuse detection?"><i class="bi bi-shield-check"></i></button>
</form>
<form method="post" action="/pymta-manager/blacklist/{{.ID}}/remove" class="d-inline">
<form method="post" action="/smtp-server/blacklist/{{.ID}}/remove" class="d-inline">
<button type="submit" class="btn btn-outline-danger btn-sm" title="Remove entry" data-confirm="Remove the blacklist entry for {{.IPAddress}}?"><i class="bi bi-trash"></i></button>
</form>
</div>
@@ -49,7 +49,7 @@
<div class="card mb-4">
<div class="card-header"><h6 class="mb-0"><i class="bi bi-plus-circle me-2"></i>Manually Blacklist an IP</h6></div>
<div class="card-body">
<form method="post" action="/pymta-manager/blacklist/add" class="row g-2 align-items-end">
<form method="post" action="/smtp-server/blacklist/add" class="row g-2 align-items-end">
<div class="col-md-4">
<label class="form-label small">IP Address</label>
<input type="text" name="ip_address" class="form-control font-monospace" placeholder="203.0.113.7" required>
@@ -74,7 +74,7 @@
<div class="card-body">
<div class="alert alert-info small">
IPs here are exempt from automatic blacklisting for failed SMTP/IMAP auth. This is separate
from the <a href="/pymta-manager/ips">relay whitelist</a>, which authorizes unauthenticated sending for a domain.
from the <a href="/smtp-server/ips">relay whitelist</a>, which authorizes unauthenticated sending for a domain.
</div>
{{if .whitelist}}
<div class="table-responsive mb-3">
@@ -87,7 +87,7 @@
<td><small class="text-muted">{{.Note}}</small></td>
<td><small class="text-muted">{{strftime "%Y-%m-%d %H:%M" .CreatedAt}}</small></td>
<td>
<form method="post" action="/pymta-manager/abuse-whitelist/{{.ID}}/remove" class="d-inline">
<form method="post" action="/smtp-server/abuse-whitelist/{{.ID}}/remove" class="d-inline">
<button type="submit" class="btn btn-outline-danger btn-sm" title="Remove" data-confirm="Remove {{.IPAddress}} from the abuse-detection whitelist?"><i class="bi bi-trash"></i></button>
</form>
</td>
@@ -97,7 +97,7 @@
</table>
</div>
{{end}}
<form method="post" action="/pymta-manager/abuse-whitelist/add" class="row g-2 align-items-end">
<form method="post" action="/smtp-server/abuse-whitelist/add" class="row g-2 align-items-end">
<div class="col-md-4">
<label class="form-label small">IP Address</label>
<input type="text" name="ip_address" class="form-control font-monospace" placeholder="203.0.113.7" required>
+12 -12
View File
@@ -4,7 +4,7 @@
{{define "content"}}
<div class="row">
<div class="col-lg-3 col-md-6 mb-4">
<a href="/pymta-manager/domains" class="text-decoration-none">
<a href="/smtp-server/domains" class="text-decoration-none">
<div class="card border-primary">
<div class="card-body">
<div class="d-flex align-items-center">
@@ -21,7 +21,7 @@
</div>
<div class="col-lg-3 col-md-6 mb-4">
<a href="/pymta-manager/senders" class="text-decoration-none">
<a href="/smtp-server/senders" class="text-decoration-none">
<div class="card border-success">
<div class="card-body">
<div class="d-flex align-items-center">
@@ -38,7 +38,7 @@
</div>
<div class="col-lg-3 col-md-6 mb-4">
<a href="/pymta-manager/dkim" class="text-decoration-none">
<a href="/smtp-server/dkim" class="text-decoration-none">
<div class="card border-warning">
<div class="card-body">
<div class="d-flex align-items-center">
@@ -55,7 +55,7 @@
</div>
<div class="col-lg-3 col-md-6 mb-4">
<a href="/pymta-manager/mailboxes" class="text-decoration-none">
<a href="/smtp-server/mailboxes" class="text-decoration-none">
<div class="card {{if gt .mailboxes_near_quota 0}}border-danger{{else}}border-primary{{end}}">
<div class="card-body">
<div class="d-flex align-items-center">
@@ -106,7 +106,7 @@
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="mb-0"><i class="bi bi-envelope me-2"></i>Recent Email Activity</h5>
<a href="/pymta-manager/logs?type=emails" class="btn btn-outline-light btn-sm">View All</a>
<a href="/smtp-server/logs?type=emails" class="btn btn-outline-light btn-sm">View All</a>
</div>
<div class="card-body p-0">
{{if .recent_emails}}
@@ -154,7 +154,7 @@
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="mb-0"><i class="bi bi-shield-lock me-2"></i>Recent Auth Activity</h5>
<a href="/pymta-manager/logs?type=auth" class="btn btn-outline-light btn-sm">View All</a>
<a href="/smtp-server/logs?type=auth" class="btn btn-outline-light btn-sm">View All</a>
</div>
<div class="card-body p-0">
{{if .recent_auths}}
@@ -187,7 +187,7 @@
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="mb-0"><i class="bi bi-shield-x me-2"></i>Attack Activity</h5>
<a href="/pymta-manager/blacklist" class="btn btn-outline-light btn-sm">View Blacklist</a>
<a href="/smtp-server/blacklist" class="btn btn-outline-light btn-sm">View Blacklist</a>
</div>
<div class="card-body">
<div class="row text-center">
@@ -224,13 +224,13 @@
<div class="card-header"><h5 class="mb-0"><i class="bi bi-lightning me-2"></i>Quick Actions</h5></div>
<div class="card-body">
<div class="row">
<div class="col-md-3 mb-3"><div class="d-grid"><a href="/pymta-manager/domains/add" class="btn btn-outline-primary"><i class="bi bi-plus-circle me-2"></i>Add Domain</a></div></div>
<div class="col-md-3 mb-3"><div class="d-grid"><a href="/pymta-manager/senders/add" class="btn btn-outline-success"><i class="bi bi-person-plus me-2"></i>Add Sender</a></div></div>
<div class="col-md-3 mb-3"><div class="d-grid"><a href="/pymta-manager/ips/add" class="btn btn-outline-warning"><i class="bi bi-shield-plus me-2"></i>Whitelist IP</a></div></div>
<div class="col-md-3 mb-3"><div class="d-grid"><a href="/smtp-server/domains/add" class="btn btn-outline-primary"><i class="bi bi-plus-circle me-2"></i>Add Domain</a></div></div>
<div class="col-md-3 mb-3"><div class="d-grid"><a href="/smtp-server/senders/add" class="btn btn-outline-success"><i class="bi bi-person-plus me-2"></i>Add Sender</a></div></div>
<div class="col-md-3 mb-3"><div class="d-grid"><a href="/smtp-server/ips/add" class="btn btn-outline-warning"><i class="bi bi-shield-plus me-2"></i>Whitelist IP</a></div></div>
{{if dget . "is_global_admin"}}
<div class="col-md-3 mb-3"><div class="d-grid"><a href="/pymta-manager/settings" class="btn btn-outline-info"><i class="bi bi-gear me-2"></i>Settings</a></div></div>
<div class="col-md-3 mb-3"><div class="d-grid"><a href="/smtp-server/settings" class="btn btn-outline-info"><i class="bi bi-gear me-2"></i>Settings</a></div></div>
{{else}}
<div class="col-md-3 mb-3"><div class="d-grid"><a href="/pymta-manager/mailboxes/add" class="btn btn-outline-info"><i class="bi bi-mailbox me-2"></i>Add Mailbox</a></div></div>
<div class="col-md-3 mb-3"><div class="d-grid"><a href="/smtp-server/mailboxes/add" class="btn btn-outline-info"><i class="bi bi-mailbox me-2"></i>Add Mailbox</a></div></div>
{{end}}
</div>
</div>
+10 -10
View File
@@ -23,7 +23,7 @@
<div class="modal fade" id="createDKIMModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<form id="createDKIMForm" method="post" action="/pymta-manager/dkim/create">
<form id="createDKIMForm" method="post" action="/smtp-server/dkim/create">
<div class="modal-header">
<h5 class="modal-title">Create New DKIM Key</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
@@ -67,21 +67,21 @@
<i class="bi bi-search me-1"></i>Check DNS
</button>
<div class="btn-group btn-group-sm" role="group">
<a href="/pymta-manager/dkim/{{$key.ID}}/edit" class="btn btn-outline-info" onclick="event.stopPropagation();"><i class="bi bi-pencil me-1"></i>Edit</a>
<form method="post" action="/pymta-manager/dkim/{{$key.ID}}/toggle" class="d-inline">
<a href="/smtp-server/dkim/{{$key.ID}}/edit" class="btn btn-outline-info" onclick="event.stopPropagation();"><i class="bi bi-pencil me-1"></i>Edit</a>
<form method="post" action="/smtp-server/dkim/{{$key.ID}}/toggle" class="d-inline">
{{if $key.IsActive}}
<button type="submit" class="btn btn-outline-warning" onclick="event.stopPropagation();" title="Disable DKIM"><i class="bi bi-pause-circle me-1"></i>Disable</button>
{{else}}
<button type="submit" class="btn btn-outline-success" onclick="event.stopPropagation();" title="Enable DKIM"><i class="bi bi-play-circle me-1"></i>Enable</button>
{{end}}
</form>
<form method="post" action="/pymta-manager/dkim/{{$key.ID}}/remove" class="d-inline">
<form method="post" action="/smtp-server/dkim/{{$key.ID}}/remove" class="d-inline">
<button type="submit" class="btn btn-outline-danger" onclick="event.stopPropagation();" data-confirm="Permanently remove the DKIM key for {{$domain.domain_name}}? You will lose the ability to sign emails until you regenerate a new key.">
<i class="bi bi-trash me-1"></i>Remove
</button>
</form>
</div>
<form method="post" action="/pymta-manager/dkim/{{$domain.id}}/regenerate" class="d-inline" onsubmit="event.stopPropagation();">
<form method="post" action="/smtp-server/dkim/{{$domain.id}}/regenerate" class="d-inline" onsubmit="event.stopPropagation();">
<button type="submit" class="btn btn-outline-warning" onclick="event.stopPropagation();"><i class="bi bi-arrow-clockwise me-1"></i>Regenerate</button>
</form>
</div>
@@ -146,10 +146,10 @@
<small class="text-muted">Selector: <code>{{$key.Selector}}</code> | Created: {{strftime "%Y-%m-%d %H:%M" $key.CreatedAt}}</small>
</div>
<div class="btn-group btn-group-sm">
<form method="post" action="/pymta-manager/dkim/{{$key.ID}}/toggle" class="d-inline">
<form method="post" action="/smtp-server/dkim/{{$key.ID}}/toggle" class="d-inline">
<button type="submit" class="btn btn-outline-success btn-sm"><i class="bi bi-play-circle me-1"></i>Reactivate</button>
</form>
<form method="post" action="/pymta-manager/dkim/{{$key.ID}}/remove" class="d-inline">
<form method="post" action="/smtp-server/dkim/{{$key.ID}}/remove" class="d-inline">
<button type="submit" class="btn btn-outline-danger btn-sm" data-confirm="Permanently remove this old DKIM key? This action cannot be undone."><i class="bi bi-trash me-1"></i>Remove</button>
</form>
</div>
@@ -167,7 +167,7 @@
<i class="bi bi-shield-x text-muted" style="font-size: 4rem;"></i>
<h4 class="text-muted mt-3">No DKIM Keys Found</h4>
<p class="text-muted">Add domains first to automatically generate DKIM keys</p>
<a href="/pymta-manager/domains/add" class="btn btn-primary"><i class="bi bi-plus-circle me-2"></i>Add Domain</a>
<a href="/smtp-server/domains/add" class="btn btn-primary"><i class="bi bi-plus-circle me-2"></i>Add Domain</a>
</div>
</div>
{{end}}
@@ -196,9 +196,9 @@
dkimStatus.innerHTML = '<span class="status-indicator status-warning"></span><small class="text-muted">Checking...</small>';
spfStatus.innerHTML = '<span class="status-indicator status-warning"></span><small class="text-muted">Checking...</small>';
try {
const dkimResponse = await fetch('/pymta-manager/dkim/check_dns', { method: 'POST', headers: {'Content-Type': 'application/x-www-form-urlencoded'}, body: new URLSearchParams({domain, selector}) });
const dkimResponse = await fetch('/smtp-server/dkim/check_dns', { method: 'POST', headers: {'Content-Type': 'application/x-www-form-urlencoded'}, body: new URLSearchParams({domain, selector}) });
const dkimResult = await dkimResponse.json();
const spfResponse = await fetch('/pymta-manager/dkim/check_spf', { method: 'POST', headers: {'Content-Type': 'application/x-www-form-urlencoded'}, body: new URLSearchParams({domain}) });
const spfResponse = await fetch('/smtp-server/dkim/check_spf', { method: 'POST', headers: {'Content-Type': 'application/x-www-form-urlencoded'}, body: new URLSearchParams({domain}) });
const spfResult = await spfResponse.json();
const domainCard = document.getElementById(`domain-${domain.replace('.', '-')}`);
const isActive = domainCard && domainCard.dataset.isActive === 'true';
+6 -6
View File
@@ -4,7 +4,7 @@
{{define "content"}}
<div class="d-flex justify-content-between align-items-center mb-4">
<h2><i class="bi bi-globe me-2"></i>Domains</h2>
<a href="/pymta-manager/domains/add" class="btn btn-primary"><i class="bi bi-plus-circle me-2"></i>Add Domain</a>
<a href="/smtp-server/domains/add" class="btn btn-primary"><i class="bi bi-plus-circle me-2"></i>Add Domain</a>
</div>
<div class="card">
@@ -52,15 +52,15 @@
</td>
<td>
<div class="btn-group btn-group-sm" role="group">
<a href="/pymta-manager/domains/{{$domain.ID}}/edit" class="btn btn-outline-primary" title="Edit Domain"><i class="bi bi-pencil"></i></a>
<form method="post" action="/pymta-manager/domains/{{$domain.ID}}/toggle" class="d-inline">
<a href="/smtp-server/domains/{{$domain.ID}}/edit" class="btn btn-outline-primary" title="Edit Domain"><i class="bi bi-pencil"></i></a>
<form method="post" action="/smtp-server/domains/{{$domain.ID}}/toggle" class="d-inline">
{{if $domain.IsActive}}
<button type="submit" class="btn btn-outline-warning" data-confirm="Are you sure you want to disable domain {{$domain.DomainName}}?" title="Disable Domain"><i class="bi bi-pause-circle"></i></button>
{{else}}
<button type="submit" class="btn btn-outline-success" data-confirm="Are you sure you want to enable domain {{$domain.DomainName}}?" title="Enable Domain"><i class="bi bi-play-circle"></i></button>
{{end}}
</form>
<form method="post" action="/pymta-manager/domains/{{$domain.ID}}/remove" class="d-inline">
<form method="post" action="/smtp-server/domains/{{$domain.ID}}/remove" class="d-inline">
<button type="submit" class="btn btn-outline-danger" data-confirm="WARNING: This will permanently delete domain {{$domain.DomainName}} and ALL associated data. This action cannot be undone. Continue?" title="Permanently Remove Domain"><i class="bi bi-trash"></i></button>
</form>
</div>
@@ -75,7 +75,7 @@
<i class="bi bi-globe text-muted" style="font-size: 4rem;"></i>
<h4 class="text-muted mt-3">No domains configured</h4>
<p class="text-muted">Get started by adding your first domain</p>
<a href="/pymta-manager/domains/add" class="btn btn-primary"><i class="bi bi-plus-circle me-2"></i>Add Your First Domain</a>
<a href="/smtp-server/domains/add" class="btn btn-primary"><i class="bi bi-plus-circle me-2"></i>Add Your First Domain</a>
</div>
{{end}}
</div>
@@ -135,7 +135,7 @@ document.addEventListener('DOMContentLoaded', function() {
btn.disabled = true;
btn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span>Checking...';
try {
const response = await fetch(`/pymta-manager/domains/${currentDomainID}/verify_check`, { method: 'POST' });
const response = await fetch(`/smtp-server/domains/${currentDomainID}/verify_check`, { method: 'POST' });
const result = await response.json();
const resultEl = document.getElementById('verifyResult');
resultEl.innerHTML = `<div class="alert ${result.success ? 'alert-success' : 'alert-warning'} mb-0 mt-2">${result.message}</div>`;
+1 -1
View File
@@ -21,7 +21,7 @@
<p class="text-muted">You don't manage any domains to assign.</p>
{{end}}
<div class="d-flex justify-content-between">
<a href="/pymta-manager/admins" class="btn btn-secondary"><i class="bi bi-arrow-left me-2"></i>Back</a>
<a href="/smtp-server/admins" class="btn btn-secondary"><i class="bi bi-arrow-left me-2"></i>Back</a>
<button type="submit" class="btn btn-primary"><i class="bi bi-check-lg me-2"></i>Save</button>
</div>
</form>
+2 -2
View File
@@ -7,7 +7,7 @@
<div class="card">
<div class="card-header bg-primary text-white d-flex justify-content-between align-items-center">
<h4 class="mb-0"><i class="bi bi-pencil me-2"></i>Edit DKIM Selector</h4>
<a href="/pymta-manager/dkim" class="btn btn-light btn-sm"><i class="bi bi-arrow-left me-1"></i>Back to DKIM Keys</a>
<a href="/smtp-server/dkim" class="btn btn-light btn-sm"><i class="bi bi-arrow-left me-1"></i>Back to DKIM Keys</a>
</div>
<div class="card-body">
<form method="POST" class="needs-validation" novalidate>
@@ -35,7 +35,7 @@
</div>
</div>
<div class="d-flex justify-content-between">
<a href="/pymta-manager/dkim" class="btn btn-secondary"><i class="bi bi-x me-1"></i>Cancel</a>
<a href="/smtp-server/dkim" class="btn btn-secondary"><i class="bi bi-x me-1"></i>Cancel</a>
<button type="submit" class="btn btn-primary"><i class="bi bi-save me-1"></i>Update Selector</button>
</div>
</form>
+2 -2
View File
@@ -7,7 +7,7 @@
<div class="card">
<div class="card-header bg-primary text-white d-flex justify-content-between align-items-center">
<h4 class="mb-0"><i class="bi bi-pencil me-2"></i>Edit Domain</h4>
<a href="/pymta-manager/domains" class="btn btn-light btn-sm"><i class="bi bi-arrow-left me-1"></i>Back to Domains</a>
<a href="/smtp-server/domains" class="btn btn-light btn-sm"><i class="bi bi-arrow-left me-1"></i>Back to Domains</a>
</div>
<div class="card-body">
<form method="POST" class="needs-validation" novalidate>
@@ -44,7 +44,7 @@
</div>
<div class="d-flex justify-content-between">
<a href="/pymta-manager/domains" class="btn btn-secondary"><i class="bi bi-x me-1"></i>Cancel</a>
<a href="/smtp-server/domains" class="btn btn-secondary"><i class="bi bi-x me-1"></i>Cancel</a>
<button type="submit" class="btn btn-primary"><i class="bi bi-save me-1"></i>Update Domain</button>
</div>
</form>
+1 -1
View File
@@ -29,7 +29,7 @@
</div>
<div class="d-flex gap-2">
<button type="submit" class="btn btn-primary"><i class="bi bi-check-lg me-1"></i>Update IP Whitelist</button>
<a href="/pymta-manager/ips" class="btn btn-secondary"><i class="bi bi-x-lg me-1"></i>Cancel</a>
<a href="/smtp-server/ips" class="btn btn-secondary"><i class="bi bi-x-lg me-1"></i>Cancel</a>
</div>
</form>
</div>
+5 -5
View File
@@ -28,7 +28,7 @@
</div>
<div class="d-flex gap-2">
<button type="submit" class="btn btn-primary"><i class="bi bi-check-lg me-1"></i>Update Mailbox</button>
<a href="/pymta-manager/mailboxes" class="btn btn-secondary"><i class="bi bi-x-lg me-1"></i>Cancel</a>
<a href="/smtp-server/mailboxes" class="btn btn-secondary"><i class="bi bi-x-lg me-1"></i>Cancel</a>
</div>
</form>
</div>
@@ -49,10 +49,10 @@
<dd class="col-sm-7"><small class="text-muted">{{filesize .mailbox.UsedBytes}} / {{filesize .mailbox.QuotaBytes}}</small></dd>
<dt class="col-sm-5">Created:</dt><dd class="col-sm-7"><small class="text-muted">{{strftime "%Y-%m-%d %H:%M" .mailbox.CreatedAt}}</small></dd>
</dl>
<a href="/pymta-manager/mailboxes/{{.mailbox.ID}}/apppasswords" class="btn btn-outline-secondary btn-sm w-100 mt-2"><i class="bi bi-key me-1"></i>Manage App Passwords</a>
<a href="/pymta-manager/mailboxes/{{.mailbox.ID}}/aliases" class="btn btn-outline-secondary btn-sm w-100 mt-2"><i class="bi bi-signpost-split me-1"></i>Manage Aliases</a>
<a href="/pymta-manager/mailboxes/{{.mailbox.ID}}/lists" class="btn btn-outline-secondary btn-sm w-100 mt-2"><i class="bi bi-shield-exclamation me-1"></i>Allow/Block List</a>
<a href="/pymta-manager/mailboxes/{{.mailbox.ID}}/rules" class="btn btn-outline-secondary btn-sm w-100 mt-2"><i class="bi bi-funnel me-1"></i>Filter Rules</a>
<a href="/smtp-server/mailboxes/{{.mailbox.ID}}/apppasswords" class="btn btn-outline-secondary btn-sm w-100 mt-2"><i class="bi bi-key me-1"></i>Manage App Passwords</a>
<a href="/smtp-server/mailboxes/{{.mailbox.ID}}/aliases" class="btn btn-outline-secondary btn-sm w-100 mt-2"><i class="bi bi-signpost-split me-1"></i>Manage Aliases</a>
<a href="/smtp-server/mailboxes/{{.mailbox.ID}}/lists" class="btn btn-outline-secondary btn-sm w-100 mt-2"><i class="bi bi-shield-exclamation me-1"></i>Allow/Block List</a>
<a href="/smtp-server/mailboxes/{{.mailbox.ID}}/rules" class="btn btn-outline-secondary btn-sm w-100 mt-2"><i class="bi bi-funnel me-1"></i>Filter Rules</a>
</div>
</div>
</div>
+1 -1
View File
@@ -41,7 +41,7 @@
</div>
<div class="d-flex gap-2">
<button type="submit" class="btn btn-primary"><i class="bi bi-check-lg me-1"></i>Update Sender</button>
<a href="/pymta-manager/senders" class="btn btn-secondary"><i class="bi bi-x-lg me-1"></i>Cancel</a>
<a href="/smtp-server/senders" class="btn btn-secondary"><i class="bi bi-x-lg me-1"></i>Cancel</a>
</div>
</form>
</div>
+1 -1
View File
@@ -49,7 +49,7 @@
<div class="card-footer">
<div class="d-flex justify-content-between align-items-center">
<div>
<a href="/pymta-manager/" class="btn btn-primary">
<a href="/smtp-server/" class="btn btn-primary">
<i class="fas fa-home me-1"></i>
Return to Dashboard
</a>
+1 -1
View File
@@ -19,7 +19,7 @@
<div class="card-header"><h5 class="mb-0"><i class="bi bi-person-gear me-2"></i>Choose your {{if .must_change_username}}credentials{{else}}password{{end}}</h5></div>
<div class="card-body">
{{if .error}}<div class="alert alert-danger">{{.error}}</div>{{end}}
<form method="POST" action="/pymta-manager/first-login">
<form method="POST" action="/smtp-server/first-login">
{{if .must_change_username}}
<div class="mb-3">
<label for="username" class="form-label">New username</label>
+7 -7
View File
@@ -4,7 +4,7 @@
<div class="container-fluid">
<div class="d-flex justify-content-between align-items-center mb-4">
<h2><i class="bi bi-router me-2"></i>Whitelisted IP Addresses</h2>
<a href="/pymta-manager/ips/add" class="btn btn-success"><i class="bi bi-plus-circle me-2"></i>Add IP Address</a>
<a href="/smtp-server/ips/add" class="btn btn-success"><i class="bi bi-plus-circle me-2"></i>Add IP Address</a>
</div>
<div class="card mb-4">
@@ -25,17 +25,17 @@
<td><small class="text-muted">{{strftime "%Y-%m-%d %H:%M" $ip.CreatedAt}}</small></td>
<td>
<div class="btn-group" role="group">
<a href="/pymta-manager/ips/{{$ip.ID}}/edit" class="btn btn-outline-primary btn-sm" title="Edit IP"><i class="bi bi-pencil"></i></a>
<a href="/smtp-server/ips/{{$ip.ID}}/edit" class="btn btn-outline-primary btn-sm" title="Edit IP"><i class="bi bi-pencil"></i></a>
{{if $ip.IsActive}}
<form method="post" action="/pymta-manager/ips/{{$ip.ID}}/delete" class="d-inline">
<form method="post" action="/smtp-server/ips/{{$ip.ID}}/delete" class="d-inline">
<button type="submit" class="btn btn-outline-warning btn-sm" title="Disable IP" data-confirm="Disable {{$ip.IPAddress}}?"><i class="bi bi-pause-circle"></i></button>
</form>
{{else}}
<form method="post" action="/pymta-manager/ips/{{$ip.ID}}/enable" class="d-inline">
<form method="post" action="/smtp-server/ips/{{$ip.ID}}/enable" class="d-inline">
<button type="submit" class="btn btn-outline-success btn-sm" title="Enable IP" data-confirm="Enable {{$ip.IPAddress}}?"><i class="bi bi-play-circle"></i></button>
</form>
{{end}}
<form method="post" action="/pymta-manager/ips/{{$ip.ID}}/remove" class="d-inline">
<form method="post" action="/smtp-server/ips/{{$ip.ID}}/remove" class="d-inline">
<button type="submit" class="btn btn-outline-danger btn-sm" title="Permanently Remove IP" data-confirm="Permanently remove {{$ip.IPAddress}}? This cannot be undone!"><i class="bi bi-trash"></i></button>
</form>
</div>
@@ -50,7 +50,7 @@
<i class="bi bi-router text-muted" style="font-size: 4rem;"></i>
<h4 class="text-muted mt-3">No IP Addresses Whitelisted</h4>
<p class="text-muted">Add IP addresses to allow authentication without username/password</p>
<a href="/pymta-manager/ips/add" class="btn btn-primary"><i class="bi bi-plus-circle me-2"></i>Add First IP Address</a>
<a href="/smtp-server/ips/add" class="btn btn-primary"><i class="bi bi-plus-circle me-2"></i>Add First IP Address</a>
</div>
{{end}}
</div>
@@ -104,7 +104,7 @@
const currentIPElement = document.getElementById('current-ip');
const ip = currentIPElement.textContent.trim();
if (ip && ip !== 'Detecting...' && ip !== 'Unable to detect') {
const url = new URL('/pymta-manager/ips/add', window.location.origin);
const url = new URL('/smtp-server/ips/add', window.location.origin);
url.searchParams.set('ip', ip);
window.location.href = url.toString();
} else {
+7 -7
View File
@@ -10,7 +10,7 @@
Two independent certificates can be obtained here — DNS-01 (needs a supported DNS provider) and
HTTP-01 (needs nothing but port 80 reachable from the internet). Enable either, both, or
neither. Which listener (SMTP-TLS, IMAP-TLS, or the admin/webmail HTTPS) actually uses which
certificate is chosen on the <a href="/pymta-manager/settings">Settings</a> page's TLS/SSL
certificate is chosen on the <a href="/smtp-server/settings">Settings</a> page's TLS/SSL
section — e.g. run the HTTP-01 cert on mail while the dashboard keeps a DNS-01 or custom cert.
</p>
@@ -43,12 +43,12 @@
{{end}}
</dd>
</dl>
<form method="post" action="/pymta-manager/letsencrypt/obtain">
<form method="post" action="/smtp-server/letsencrypt/obtain">
<button type="submit" class="btn btn-primary" data-confirm="Obtain or renew the DNS-01 certificate now using the saved configuration?"><i class="bi bi-arrow-repeat me-1"></i>Obtain / Renew Now</button>
</form>
<hr>
<form method="POST" action="/pymta-manager/letsencrypt/save">
<form method="POST" action="/smtp-server/letsencrypt/save">
<div class="mb-3">
<label class="form-label">Enable DNS-01</label>
<select class="form-select" name="enabled">
@@ -176,12 +176,12 @@
{{end}}
</dd>
</dl>
<form method="post" action="/pymta-manager/letsencrypt/http/obtain">
<form method="post" action="/smtp-server/letsencrypt/http/obtain">
<button type="submit" class="btn btn-primary" data-confirm="Obtain or renew the HTTP-01 certificate now using the saved configuration?"><i class="bi bi-arrow-repeat me-1"></i>Obtain / Renew Now</button>
</form>
<hr>
<form method="POST" action="/pymta-manager/letsencrypt/http/save">
<form method="POST" action="/smtp-server/letsencrypt/http/save">
<div class="mb-3">
<label class="form-label">Enable HTTP-01</label>
<select class="form-select" name="enabled">
@@ -240,7 +240,7 @@
updateProviderFields();
document.getElementById('le_detect_ip_btn').addEventListener('click', function() {
fetch('/pymta-manager/api/letsencrypt/detect_ip')
fetch('/smtp-server/api/letsencrypt/detect_ip')
.then(r => r.json())
.then(data => {
if (data.status === 'success') { document.getElementById('le_ip_override').value = data.ip; showToast('Detected WAN IP: ' + data.ip, 'success'); }
@@ -253,7 +253,7 @@
if (!file) return;
const formData = new FormData();
formData.append('gcloud_key_file', file);
fetch('/pymta-manager/api/letsencrypt/upload_gcloud_key', { method: 'POST', body: formData })
fetch('/smtp-server/api/letsencrypt/upload_gcloud_key', { method: 'POST', body: formData })
.then(r => r.json())
.then(data => {
if (data.status === 'success') { document.getElementById('gcloud_sa_path').value = data.filepath; showToast('Service account key uploaded', 'success'); }
+3 -3
View File
@@ -5,8 +5,8 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Sign in - mailgoserver</title>
<link href="/pymta-manager/static/vendor/bootstrap/css/bootstrap.min.css" rel="stylesheet">
<link href="/pymta-manager/static/vendor/bootstrap-icons/font/bootstrap-icons.css" rel="stylesheet">
<link href="/smtp-server/static/vendor/bootstrap/css/bootstrap.min.css" rel="stylesheet">
<link href="/smtp-server/static/vendor/bootstrap-icons/font/bootstrap-icons.css" rel="stylesheet">
<style>
body { background-color: #1a1a1a; color: #e0e0e0; min-height: 100vh; display: flex; align-items: center; }
.login-card { max-width: 420px; margin: 0 auto; width: 100%; }
@@ -24,7 +24,7 @@
<div class="card">
<div class="card-body p-4">
{{if .error}}<div class="alert alert-danger">{{.error}}</div>{{end}}
<form method="POST" action="/pymta-manager/login">
<form method="POST" action="/smtp-server/login">
<input type="hidden" name="next" value="{{.next}}">
<div class="mb-3">
<label for="username" class="form-label">Username</label>
+6 -6
View File
@@ -5,8 +5,8 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Verify it's you - mailgoserver</title>
<link href="/pymta-manager/static/vendor/bootstrap/css/bootstrap.min.css" rel="stylesheet">
<link href="/pymta-manager/static/vendor/bootstrap-icons/font/bootstrap-icons.css" rel="stylesheet">
<link href="/smtp-server/static/vendor/bootstrap/css/bootstrap.min.css" rel="stylesheet">
<link href="/smtp-server/static/vendor/bootstrap-icons/font/bootstrap-icons.css" rel="stylesheet">
<style>
body { background-color: #1a1a1a; color: #e0e0e0; min-height: 100vh; display: flex; align-items: center; }
.login-card { max-width: 420px; margin: 0 auto; width: 100%; }
@@ -40,7 +40,7 @@
{{end}}
{{if .totp_enabled}}
<form method="POST" action="/pymta-manager/login/mfa" id="totp-form" {{if .has_passkeys}}class="d-none"{{end}}>
<form method="POST" action="/smtp-server/login/mfa" id="totp-form" {{if .has_passkeys}}class="d-none"{{end}}>
<input type="hidden" name="next" value="{{.next}}">
<div class="mb-3">
<label for="code" class="form-label">6-digit authenticator code</label>
@@ -87,7 +87,7 @@
const errEl = document.getElementById('passkey-error');
errEl.classList.add('d-none');
try {
const beginResp = await fetch('/pymta-manager/login/passkey/begin');
const beginResp = await fetch('/smtp-server/login/passkey/begin');
if (!beginResp.ok) throw new Error((await beginResp.json()).error || 'Could not start passkey login');
const options = await beginResp.json();
@@ -111,12 +111,12 @@
},
};
const finishResp = await fetch('/pymta-manager/login/passkey/finish', {
const finishResp = await fetch('/smtp-server/login/passkey/finish', {
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body),
});
if (!finishResp.ok) throw new Error((await finishResp.json()).error || 'Passkey verification failed');
window.location.href = {{if .next}}'{{.next}}'{{else}}'/pymta-manager/'{{end}};
window.location.href = {{if .next}}'{{.next}}'{{else}}'/smtp-server/'{{end}};
} catch (e) {
errEl.textContent = e.message || 'Passkey login failed';
errEl.classList.remove('d-none');
+10 -10
View File
@@ -16,19 +16,19 @@
<div class="d-flex justify-content-between align-items-center mb-4">
<h2><i class="bi bi-journal-text me-2"></i>Emails Log</h2>
<div class="btn-group">
<a href="/pymta-manager/logs?type=all" class="btn {{if eq .filter_type "all"}}btn-primary{{else}}btn-outline-primary{{end}}"><i class="bi bi-list-ul me-1"></i>All Logs</a>
<a href="/pymta-manager/logs?type=emails" class="btn {{if eq .filter_type "emails"}}btn-primary{{else}}btn-outline-primary{{end}}"><i class="bi bi-envelope me-1"></i>Email Logs</a>
<a href="/pymta-manager/logs?type=auth" class="btn {{if eq .filter_type "auth"}}btn-primary{{else}}btn-outline-primary{{end}}"><i class="bi bi-shield-lock me-1"></i>Auth Logs</a>
<a href="/smtp-server/logs?type=all" class="btn {{if eq .filter_type "all"}}btn-primary{{else}}btn-outline-primary{{end}}"><i class="bi bi-list-ul me-1"></i>All Logs</a>
<a href="/smtp-server/logs?type=emails" class="btn {{if eq .filter_type "emails"}}btn-primary{{else}}btn-outline-primary{{end}}"><i class="bi bi-envelope me-1"></i>Email Logs</a>
<a href="/smtp-server/logs?type=auth" class="btn {{if eq .filter_type "auth"}}btn-primary{{else}}btn-outline-primary{{end}}"><i class="bi bi-shield-lock me-1"></i>Auth Logs</a>
</div>
</div>
{{if eq .filter_type "auth"}}
<div class="d-flex justify-content-end mb-3">
<div class="btn-group btn-group-sm">
<a href="/pymta-manager/logs?type=auth" class="btn {{if or (eq .auth_category "") (eq .auth_category "all")}}btn-secondary{{else}}btn-outline-secondary{{end}}">All</a>
<a href="/pymta-manager/logs?type=auth&auth_category=admin" class="btn {{if eq .auth_category "admin"}}btn-secondary{{else}}btn-outline-secondary{{end}}">Admin</a>
<a href="/pymta-manager/logs?type=auth&auth_category=webmail" class="btn {{if eq .auth_category "webmail"}}btn-secondary{{else}}btn-outline-secondary{{end}}">Webmail</a>
<a href="/pymta-manager/logs?type=auth&auth_category=mailserver" class="btn {{if eq .auth_category "mailserver"}}btn-secondary{{else}}btn-outline-secondary{{end}}">Mail Server</a>
<a href="/smtp-server/logs?type=auth" class="btn {{if or (eq .auth_category "") (eq .auth_category "all")}}btn-secondary{{else}}btn-outline-secondary{{end}}">All</a>
<a href="/smtp-server/logs?type=auth&auth_category=admin" class="btn {{if eq .auth_category "admin"}}btn-secondary{{else}}btn-outline-secondary{{end}}">Admin</a>
<a href="/smtp-server/logs?type=auth&auth_category=webmail" class="btn {{if eq .auth_category "webmail"}}btn-secondary{{else}}btn-outline-secondary{{end}}">Webmail</a>
<a href="/smtp-server/logs?type=auth&auth_category=mailserver" class="btn {{if eq .auth_category "mailserver"}}btn-secondary{{else}}btn-outline-secondary{{end}}">Mail Server</a>
</div>
</div>
{{end}}
@@ -145,9 +145,9 @@
{{if or .has_prev .has_next}}
<nav aria-label="Log pagination" class="mt-4">
<ul class="pagination justify-content-center">
{{if .has_prev}}<li class="page-item"><a class="page-link" href="/pymta-manager/logs?type={{.filter_type}}&auth_category={{.auth_category}}&page={{sub .page 1}}"><i class="bi bi-chevron-left"></i> Previous</a></li>{{end}}
{{if .has_prev}}<li class="page-item"><a class="page-link" href="/smtp-server/logs?type={{.filter_type}}&auth_category={{.auth_category}}&page={{sub .page 1}}"><i class="bi bi-chevron-left"></i> Previous</a></li>{{end}}
<li class="page-item active"><span class="page-link">Page {{.page}}</span></li>
{{if .has_next}}<li class="page-item"><a class="page-link" href="/pymta-manager/logs?type={{.filter_type}}&auth_category={{.auth_category}}&page={{add .page 1}}">Next <i class="bi bi-chevron-right"></i></a></li>{{end}}
{{if .has_next}}<li class="page-item"><a class="page-link" href="/smtp-server/logs?type={{.filter_type}}&auth_category={{.auth_category}}&page={{add .page 1}}">Next <i class="bi bi-chevron-right"></i></a></li>{{end}}
</ul>
</nav>
{{end}}
@@ -184,7 +184,7 @@
const body = document.getElementById('messageContentModalBody');
body.innerHTML = '<div class="text-center text-muted py-5"><div class="spinner-border" role="status"></div></div>';
bootstrap.Modal.getOrCreateInstance(modalEl).show();
fetch('/pymta-manager/msg/content/' + id)
fetch('/smtp-server/msg/content/' + id)
.then(function(r) { if (!r.ok) throw new Error(r.status); return r.text(); })
.then(function(html) { body.innerHTML = html; })
.catch(function() { body.innerHTML = '<p class="text-danger">Failed to load the message.</p>'; });
@@ -4,7 +4,7 @@
{{define "content"}}
<div class="d-flex justify-content-between align-items-center mb-4">
<h2><i class="bi bi-signpost-split me-2"></i>Aliases <small class="text-muted fs-6">{{.mailbox.Email}}</small></h2>
<a href="/pymta-manager/mailboxes" class="btn btn-secondary"><i class="bi bi-arrow-left me-2"></i>Back to Mailboxes</a>
<a href="/smtp-server/mailboxes" class="btn btn-secondary"><i class="bi bi-arrow-left me-2"></i>Back to Mailboxes</a>
</div>
<div class="alert alert-info">
@@ -14,7 +14,7 @@
<div class="card mb-4">
<div class="card-header"><h5 class="mb-0"><i class="bi bi-plus-circle me-2"></i>Add Alias</h5></div>
<div class="card-body">
<form method="POST" action="/pymta-manager/mailboxes/{{.mailbox.ID}}/aliases/add">
<form method="POST" action="/smtp-server/mailboxes/{{.mailbox.ID}}/aliases/add">
<div class="mb-3">
<div class="input-group">
<input type="text" class="form-control" id="local_part" name="local_part" required placeholder="alias"
@@ -55,7 +55,7 @@
</td>
<td><small class="text-muted">{{strftime "%Y-%m-%d %H:%M" .CreatedAt}}</small></td>
<td>
<form method="post" action="/pymta-manager/mailboxes/{{$.mailbox.ID}}/aliases/{{.ID}}/remove" class="d-inline">
<form method="post" action="/smtp-server/mailboxes/{{$.mailbox.ID}}/aliases/{{.ID}}/remove" class="d-inline">
<button type="submit" class="btn btn-outline-danger btn-sm" title="Remove" data-confirm="Remove alias {{.Email}}?"><i class="bi bi-trash"></i></button>
</form>
</td>
@@ -5,8 +5,8 @@
<div class="d-flex justify-content-between align-items-center mb-4">
<h2><i class="bi bi-key me-2"></i>App Passwords <small class="text-muted fs-6">{{.mailbox.Email}}</small></h2>
<div class="d-flex gap-2">
<a href="/pymta-manager/mailboxes/{{.mailbox.ID}}/edit" class="btn btn-outline-warning"><i class="bi bi-shield-lock me-2"></i>Reset Mailbox Password</a>
<a href="/pymta-manager/mailboxes" class="btn btn-secondary"><i class="bi bi-arrow-left me-2"></i>Back to Mailboxes</a>
<a href="/smtp-server/mailboxes/{{.mailbox.ID}}/edit" class="btn btn-outline-warning"><i class="bi bi-shield-lock me-2"></i>Reset Mailbox Password</a>
<a href="/smtp-server/mailboxes" class="btn btn-secondary"><i class="bi bi-arrow-left me-2"></i>Back to Mailboxes</a>
</div>
</div>
@@ -17,7 +17,7 @@
<div class="card mb-4">
<div class="card-header"><h5 class="mb-0"><i class="bi bi-plus-circle me-2"></i>Create App Password</h5></div>
<div class="card-body">
<form method="POST" action="/pymta-manager/mailboxes/{{.mailbox.ID}}/apppasswords/add" class="row g-2 align-items-end">
<form method="POST" action="/smtp-server/mailboxes/{{.mailbox.ID}}/apppasswords/add" class="row g-2 align-items-end">
<div class="col-auto">
<label for="label" class="form-label">Label</label>
<input type="text" class="form-control" id="label" name="label" placeholder="e.g. Thunderbird laptop">
@@ -66,7 +66,7 @@
</td>
<td>{{if .IsActive}}<span class="badge bg-success">Active</span>{{else}}<span class="badge bg-danger">Revoked</span>{{end}}</td>
<td>
<form method="post" action="/pymta-manager/mailboxes/{{$.mailbox.ID}}/apppasswords/{{.ID}}/revoke" class="d-inline">
<form method="post" action="/smtp-server/mailboxes/{{$.mailbox.ID}}/apppasswords/{{.ID}}/revoke" class="d-inline">
<button type="submit" class="btn btn-outline-danger btn-sm" title="Revoke" data-confirm="Revoke app password &quot;{{.Label}}&quot;? Any client using it will stop working."><i class="bi bi-trash"></i></button>
</form>
</td>
+5 -5
View File
@@ -4,7 +4,7 @@
{{define "content"}}
<div class="d-flex justify-content-between align-items-center mb-4">
<h2><i class="bi bi-shield-exclamation me-2"></i>Allow/Block List <small class="text-muted fs-6">{{.mailbox.Email}}</small></h2>
<a href="/pymta-manager/mailboxes" class="btn btn-secondary"><i class="bi bi-arrow-left me-2"></i>Back to Mailboxes</a>
<a href="/smtp-server/mailboxes" class="btn btn-secondary"><i class="bi bi-arrow-left me-2"></i>Back to Mailboxes</a>
</div>
<div class="alert alert-info">
@@ -16,7 +16,7 @@
<div class="card">
<div class="card-header"><h5 class="mb-0 text-success"><i class="bi bi-check-circle me-2"></i>Allow List</h5></div>
<div class="card-body">
<form method="POST" action="/pymta-manager/mailboxes/{{.mailbox.ID}}/lists/add" class="d-flex gap-2 mb-3">
<form method="POST" action="/smtp-server/mailboxes/{{.mailbox.ID}}/lists/add" class="d-flex gap-2 mb-3">
<input type="hidden" name="list_type" value="allow">
<input type="text" class="form-control" name="pattern" placeholder="friend@example.com or @example.com" required>
<button type="submit" class="btn btn-success"><i class="bi bi-plus-lg"></i></button>
@@ -26,7 +26,7 @@
{{range .allow}}
<li class="list-group-item list-group-item-dark d-flex justify-content-between align-items-center">
<code>{{.Pattern}}</code>
<form method="post" action="/pymta-manager/mailboxes/{{$.mailbox.ID}}/lists/{{.ID}}/remove" class="d-inline">
<form method="post" action="/smtp-server/mailboxes/{{$.mailbox.ID}}/lists/{{.ID}}/remove" class="d-inline">
<button type="submit" class="btn btn-outline-danger btn-sm" data-confirm="Remove {{.Pattern}} from the allow list?"><i class="bi bi-trash"></i></button>
</form>
</li>
@@ -43,7 +43,7 @@
<div class="card">
<div class="card-header"><h5 class="mb-0 text-danger"><i class="bi bi-x-circle me-2"></i>Block List</h5></div>
<div class="card-body">
<form method="POST" action="/pymta-manager/mailboxes/{{.mailbox.ID}}/lists/add" class="d-flex gap-2 mb-3">
<form method="POST" action="/smtp-server/mailboxes/{{.mailbox.ID}}/lists/add" class="d-flex gap-2 mb-3">
<input type="hidden" name="list_type" value="block">
<input type="text" class="form-control" name="pattern" placeholder="spam@evil.com or @evil.com" required>
<button type="submit" class="btn btn-danger"><i class="bi bi-plus-lg"></i></button>
@@ -53,7 +53,7 @@
{{range .block}}
<li class="list-group-item list-group-item-dark d-flex justify-content-between align-items-center">
<code>{{.Pattern}}</code>
<form method="post" action="/pymta-manager/mailboxes/{{$.mailbox.ID}}/lists/{{.ID}}/remove" class="d-inline">
<form method="post" action="/smtp-server/mailboxes/{{$.mailbox.ID}}/lists/{{.ID}}/remove" class="d-inline">
<button type="submit" class="btn btn-outline-danger btn-sm" data-confirm="Remove {{.Pattern}} from the block list?"><i class="bi bi-trash"></i></button>
</form>
</li>
+3 -3
View File
@@ -4,7 +4,7 @@
{{define "content"}}
<div class="d-flex justify-content-between align-items-center mb-4">
<h2><i class="bi bi-funnel me-2"></i>Filter Rules <small class="text-muted fs-6">{{.mailbox.Email}}</small></h2>
<a href="/pymta-manager/mailboxes" class="btn btn-secondary"><i class="bi bi-arrow-left me-2"></i>Back to Mailboxes</a>
<a href="/smtp-server/mailboxes" class="btn btn-secondary"><i class="bi bi-arrow-left me-2"></i>Back to Mailboxes</a>
</div>
<div class="alert alert-info">
@@ -14,7 +14,7 @@
<div class="card mb-4">
<div class="card-header"><h5 class="mb-0"><i class="bi bi-plus-circle me-2"></i>Add Rule</h5></div>
<div class="card-body">
<form method="POST" action="/pymta-manager/mailboxes/{{.mailbox.ID}}/rules/add">
<form method="POST" action="/smtp-server/mailboxes/{{.mailbox.ID}}/rules/add">
<div class="row g-2 align-items-end mb-3">
<div class="col-auto">
<label class="form-label">Priority</label>
@@ -99,7 +99,7 @@
</td>
<td>{{if .IsActive}}<span class="badge bg-success">Active</span>{{else}}<span class="badge bg-secondary">Inactive</span>{{end}}</td>
<td>
<form method="post" action="/pymta-manager/mailboxes/{{$.mailbox.ID}}/rules/{{.ID}}/remove" class="d-inline">
<form method="post" action="/smtp-server/mailboxes/{{$.mailbox.ID}}/rules/{{.ID}}/remove" class="d-inline">
<button type="submit" class="btn btn-outline-danger btn-sm" title="Remove" data-confirm="Remove this rule?"><i class="bi bi-trash"></i></button>
</form>
</td>
+9 -9
View File
@@ -4,7 +4,7 @@
{{define "content"}}
<div class="d-flex justify-content-between align-items-center mb-4">
<h2><i class="bi bi-inbox me-2"></i>Mailboxes</h2>
<a href="/pymta-manager/mailboxes/add" class="btn btn-primary"><i class="bi bi-mailbox me-2"></i>Add Mailbox</a>
<a href="/smtp-server/mailboxes/add" class="btn btn-primary"><i class="bi bi-mailbox me-2"></i>Add Mailbox</a>
</div>
<div class="alert alert-info">
@@ -40,22 +40,22 @@
<td><small class="text-muted">{{strftime "%Y-%m-%d %H:%M" $mailbox.CreatedAt}}</small></td>
<td>
<div class="btn-group" role="group">
<a href="/pymta-manager/mailboxes/{{$mailbox.ID}}/apppasswords" class="btn btn-outline-secondary btn-sm" title="App Passwords"><i class="bi bi-key"></i></a>
<a href="/pymta-manager/mailboxes/{{$mailbox.ID}}/aliases" class="btn btn-outline-secondary btn-sm" title="Aliases"><i class="bi bi-signpost-split"></i></a>
<a href="/pymta-manager/mailboxes/{{$mailbox.ID}}/edit" class="btn btn-outline-primary btn-sm" title="Edit Mailbox"><i class="bi bi-pencil"></i></a>
<form method="post" action="/pymta-manager/mailboxes/{{$mailbox.ID}}/reset_mfa" class="d-inline">
<a href="/smtp-server/mailboxes/{{$mailbox.ID}}/apppasswords" class="btn btn-outline-secondary btn-sm" title="App Passwords"><i class="bi bi-key"></i></a>
<a href="/smtp-server/mailboxes/{{$mailbox.ID}}/aliases" class="btn btn-outline-secondary btn-sm" title="Aliases"><i class="bi bi-signpost-split"></i></a>
<a href="/smtp-server/mailboxes/{{$mailbox.ID}}/edit" class="btn btn-outline-primary btn-sm" title="Edit Mailbox"><i class="bi bi-pencil"></i></a>
<form method="post" action="/smtp-server/mailboxes/{{$mailbox.ID}}/reset_mfa" class="d-inline">
<button type="submit" class="btn btn-outline-warning btn-sm" data-confirm="Reset MFA for {{$mailbox.Email}}? They will need to re-enroll an authenticator app or passkey." title="Reset MFA"><i class="bi bi-shield-x"></i></button>
</form>
{{if $mailbox.IsActive}}
<form method="post" action="/pymta-manager/mailboxes/{{$mailbox.ID}}/delete" class="d-inline">
<form method="post" action="/smtp-server/mailboxes/{{$mailbox.ID}}/delete" class="d-inline">
<button type="submit" class="btn btn-outline-warning btn-sm" title="Disable Mailbox" data-confirm="Disable mailbox {{$mailbox.Email}}?"><i class="bi bi-pause-circle"></i></button>
</form>
{{else}}
<form method="post" action="/pymta-manager/mailboxes/{{$mailbox.ID}}/enable" class="d-inline">
<form method="post" action="/smtp-server/mailboxes/{{$mailbox.ID}}/enable" class="d-inline">
<button type="submit" class="btn btn-outline-success btn-sm" title="Enable Mailbox" data-confirm="Enable mailbox {{$mailbox.Email}}?"><i class="bi bi-play-circle"></i></button>
</form>
{{end}}
<form method="post" action="/pymta-manager/mailboxes/{{$mailbox.ID}}/remove" class="d-inline">
<form method="post" action="/smtp-server/mailboxes/{{$mailbox.ID}}/remove" class="d-inline">
<button type="submit" class="btn btn-outline-danger btn-sm" title="Permanently Remove Mailbox" data-confirm="Permanently remove mailbox {{$mailbox.Email}} and all its stored mail? This cannot be undone!"><i class="bi bi-trash"></i></button>
</form>
</div>
@@ -70,7 +70,7 @@
<i class="bi bi-inbox text-muted" style="font-size: 4rem;"></i>
<h4 class="text-muted mt-3">No mailboxes configured</h4>
<p class="text-muted">Add a mailbox to let a client app (Thunderbird, etc.) receive mail via IMAP</p>
<a href="/pymta-manager/mailboxes/add" class="btn btn-primary"><i class="bi bi-mailbox me-2"></i>Add Your First Mailbox</a>
<a href="/smtp-server/mailboxes/add" class="btn btn-primary"><i class="bi bi-mailbox me-2"></i>Add Your First Mailbox</a>
</div>
{{end}}
</div>
@@ -5,8 +5,8 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Set up two-factor authentication - mailgoserver</title>
<link href="/pymta-manager/static/vendor/bootstrap/css/bootstrap.min.css" rel="stylesheet">
<link href="/pymta-manager/static/vendor/bootstrap-icons/font/bootstrap-icons.css" rel="stylesheet">
<link href="/smtp-server/static/vendor/bootstrap/css/bootstrap.min.css" rel="stylesheet">
<link href="/smtp-server/static/vendor/bootstrap-icons/font/bootstrap-icons.css" rel="stylesheet">
<style>
body { background-color: #1a1a1a; color: #e0e0e0; min-height: 100vh; display: flex; align-items: center; }
.setup-card { max-width: 480px; margin: 0 auto; width: 100%; }
@@ -49,14 +49,14 @@
<div class="card-header"><h5 class="mb-0"><i class="bi bi-qr-code me-2"></i>Authenticator App</h5></div>
<div class="card-body">
<p class="text-muted">Use Google Authenticator, 1Password, or any TOTP app.</p>
<form method="post" action="/pymta-manager/account/totp/setup">
<form method="post" action="/smtp-server/account/totp/setup">
<button type="submit" class="btn btn-outline-primary"><i class="bi bi-qr-code me-1"></i>Set up Authenticator App</button>
</form>
</div>
</div>
</div>
<script src="/pymta-manager/static/vendor/bootstrap/js/bootstrap.bundle.min.js"></script>
<script src="/smtp-server/static/vendor/bootstrap/js/bootstrap.bundle.min.js"></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
document.querySelectorAll('.toast').forEach(function(el) { new bootstrap.Toast(el, {delay: 6000}).show(); });
@@ -79,7 +79,7 @@
const errEl = document.getElementById('passkey-error');
errEl.classList.add('d-none');
try {
const beginResp = await fetch('/pymta-manager/account/passkey/begin', { method: 'POST' });
const beginResp = await fetch('/smtp-server/account/passkey/begin', { method: 'POST' });
if (!beginResp.ok) throw new Error((await beginResp.json()).error || 'Could not start passkey registration');
const options = await beginResp.json();
const publicKey = options.publicKey;
@@ -96,11 +96,11 @@
clientDataJSON: bufToB64url(cred.response.clientDataJSON),
},
};
const finishResp = await fetch('/pymta-manager/account/passkey/finish', {
const finishResp = await fetch('/smtp-server/account/passkey/finish', {
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body),
});
if (!finishResp.ok) throw new Error((await finishResp.json()).error || 'Could not save passkey');
window.location.href = '/pymta-manager/';
window.location.href = '/smtp-server/';
} catch (e) {
errEl.textContent = e.message || 'Passkey registration failed';
errEl.classList.remove('d-none');
+6 -6
View File
@@ -4,7 +4,7 @@
{{define "content"}}
<div class="d-flex justify-content-between align-items-center mb-4">
<h2><i class="bi bi-people me-2"></i>Senders</h2>
<a href="/pymta-manager/senders/add" class="btn btn-primary"><i class="bi bi-person-plus me-2"></i>Add Sender</a>
<a href="/smtp-server/senders/add" class="btn btn-primary"><i class="bi bi-person-plus me-2"></i>Add Sender</a>
</div>
<div class="card">
@@ -40,17 +40,17 @@
<td><small class="text-muted">{{strftime "%Y-%m-%d %H:%M" $sender.CreatedAt}}</small></td>
<td>
<div class="btn-group" role="group">
<a href="/pymta-manager/senders/{{$sender.ID}}/edit" class="btn btn-outline-primary btn-sm" title="Edit Sender"><i class="bi bi-pencil"></i></a>
<a href="/smtp-server/senders/{{$sender.ID}}/edit" class="btn btn-outline-primary btn-sm" title="Edit Sender"><i class="bi bi-pencil"></i></a>
{{if $sender.IsActive}}
<form method="post" action="/pymta-manager/senders/{{$sender.ID}}/delete" class="d-inline">
<form method="post" action="/smtp-server/senders/{{$sender.ID}}/delete" class="d-inline">
<button type="submit" class="btn btn-outline-warning btn-sm" title="Disable Sender" data-confirm="Disable user {{$sender.Email}}?"><i class="bi bi-pause-circle"></i></button>
</form>
{{else}}
<form method="post" action="/pymta-manager/senders/{{$sender.ID}}/enable" class="d-inline">
<form method="post" action="/smtp-server/senders/{{$sender.ID}}/enable" class="d-inline">
<button type="submit" class="btn btn-outline-success btn-sm" title="Enable Sender" data-confirm="Enable user {{$sender.Email}}?"><i class="bi bi-play-circle"></i></button>
</form>
{{end}}
<form method="post" action="/pymta-manager/senders/{{$sender.ID}}/remove" class="d-inline">
<form method="post" action="/smtp-server/senders/{{$sender.ID}}/remove" class="d-inline">
<button type="submit" class="btn btn-outline-danger btn-sm" title="Permanently Remove Sender" data-confirm="Permanently remove user {{$sender.Email}}? This cannot be undone!"><i class="bi bi-trash"></i></button>
</form>
</div>
@@ -65,7 +65,7 @@
<i class="bi bi-people text-muted" style="font-size: 4rem;"></i>
<h4 class="text-muted mt-3">No senders configured</h4>
<p class="text-muted">Add sender to enable username/password authentication</p>
<a href="/pymta-manager/senders/add" class="btn btn-primary"><i class="bi bi-person-plus me-2"></i>Add Your First Sender</a>
<a href="/smtp-server/senders/add" class="btn btn-primary"><i class="bi bi-person-plus me-2"></i>Add Your First Sender</a>
</div>
{{end}}
</div>
+7 -7
View File
@@ -16,7 +16,7 @@
</div>
</div>
<form method="POST" action="/pymta-manager/settings_update" id="settingsForm">
<form method="POST" action="/smtp-server/settings_update" id="settingsForm">
<div class="card mb-4">
<div class="card-header"><h5 class="mb-0"><i class="bi bi-server me-2"></i>Server Configuration</h5></div>
<div class="card-body">
@@ -206,7 +206,7 @@
<div class="form-text mb-3">The "custom" certificate: self-signed on first run, or your own uploaded cert/key above.</div>
<hr>
<p class="mb-2">Which certificate each TLS listener uses — <code>custom</code> (above), or one of the two Let's
Encrypt certificates managed on the <a href="/pymta-manager/letsencrypt">Let's Encrypt</a> page. Independent
Encrypt certificates managed on the <a href="/smtp-server/letsencrypt">Let's Encrypt</a> page. Independent
per listener, e.g. an HTTP-01 cert for mail while the dashboard keeps a DNS-01 or custom cert.</p>
<div class="row">
<div class="col-md-4"><div class="mb-3"><label class="form-label">SMTP (direct-TLS, 465)</label>
@@ -327,7 +327,7 @@
if (!file) return;
const formData = new FormData();
formData.append('cert_file', file);
fetch('/pymta-manager/api/settings/upload_cert', { method: 'POST', body: formData })
fetch('/smtp-server/api/settings/upload_cert', { method: 'POST', body: formData })
.then(r => r.json())
.then(data => {
if (data.status === 'success') { document.querySelector('[name="TLS.tls_cert_file"]').value = data.filepath; showToast('Certificate uploaded', 'success'); }
@@ -340,7 +340,7 @@
if (!file) return;
const formData = new FormData();
formData.append('key_file', file);
fetch('/pymta-manager/api/settings/upload_key', { method: 'POST', body: formData })
fetch('/smtp-server/api/settings/upload_key', { method: 'POST', body: formData })
.then(r => r.json())
.then(data => {
if (data.status === 'success') { document.querySelector('[name="TLS.tls_key_file"]').value = data.filepath; showToast('Key uploaded', 'success'); }
@@ -350,14 +350,14 @@
function testDatabaseConnection() {
const url = document.getElementById('databaseUrl').value;
fetch('/pymta-manager/api/settings/test_database', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({url}) })
fetch('/smtp-server/api/settings/test_database', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({url}) })
.then(r => r.json())
.then(data => showToast(data.message || (data.status === 'success' ? 'Connection successful!' : 'Failed to connect'), data.status === 'success' ? 'success' : 'danger'))
.catch(() => showToast('Failed to test database connection', 'danger'));
}
function getPublicIP() {
fetch('/pymta-manager/api/settings/get_public_ip')
fetch('/smtp-server/api/settings/get_public_ip')
.then(r => r.json())
.then(data => {
if (data.ip) { document.querySelector('[name="DKIM.spf_server_ip"]').value = data.ip; showToast('Public IP fetched', 'success'); }
@@ -369,7 +369,7 @@
const path = document.querySelector('input[name="Attachments.attachments_path"]').value;
const feedback = document.getElementById('attachments-path-feedback');
if (!feedback) return;
fetch('/pymta-manager/test_attachments_path', { method: 'POST', headers: {'Content-Type': 'application/x-www-form-urlencoded'}, body: `path=${encodeURIComponent(path)}` })
fetch('/smtp-server/test_attachments_path', { method: 'POST', headers: {'Content-Type': 'application/x-www-form-urlencoded'}, body: `path=${encodeURIComponent(path)}` })
.then(r => r.json())
.then(data => {
feedback.innerHTML = data.message + (data.success ? `<br><small class="text-muted">Absolute path: ${data.absolute_path}</small>` : '');
+13 -13
View File
@@ -17,7 +17,7 @@
<div class="flex-grow-1 overflow-auto">
<ul class="nav nav-pills flex-column p-3">
<li class="nav-item mb-2">
<a href="/pymta-manager/" class="nav-link text-white {{if eq (dget . "active") "dashboard"}}active{{end}}">
<a href="/smtp-server/" class="nav-link text-white {{if eq (dget . "active") "dashboard"}}active{{end}}">
<i class="bi bi-speedometer2 me-2"></i>
Dashboard
</a>
@@ -31,7 +31,7 @@
</li>
<li class="nav-item mb-1">
<a href="/pymta-manager/domains" class="nav-link text-white {{if eq (dget . "active") "domains"}}active{{end}}">
<a href="/smtp-server/domains" class="nav-link text-white {{if eq (dget . "active") "domains"}}active{{end}}">
<i class="bi bi-list-ul me-2"></i>
Domains
<span class="badge bg-secondary ms-auto">{{dget . "domain_count"}}</span>
@@ -39,7 +39,7 @@
</li>
<li class="nav-item mb-1">
<a href="/pymta-manager/senders" class="nav-link text-white {{if eq (dget . "active") "senders"}}active{{end}}">
<a href="/smtp-server/senders" class="nav-link text-white {{if eq (dget . "active") "senders"}}active{{end}}">
<i class="bi bi-people me-2"></i>
Allowed Senders
<span class="badge bg-secondary ms-auto">{{dget . "sender_count"}}</span>
@@ -47,7 +47,7 @@
</li>
<li class="nav-item mb-1">
<a href="/pymta-manager/mailboxes" class="nav-link text-white {{if eq (dget . "active") "mailboxes"}}active{{end}}">
<a href="/smtp-server/mailboxes" class="nav-link text-white {{if eq (dget . "active") "mailboxes"}}active{{end}}">
<i class="bi bi-inbox me-2"></i>
Mailboxes
<span class="badge bg-secondary ms-auto">{{dget . "mailbox_count"}}</span>
@@ -55,7 +55,7 @@
</li>
<li class="nav-item mb-1">
<a href="/pymta-manager/ips" class="nav-link text-white {{if eq (dget . "active") "ips"}}active{{end}}">
<a href="/smtp-server/ips" class="nav-link text-white {{if eq (dget . "active") "ips"}}active{{end}}">
<i class="bi bi-router me-2"></i>
Whitelisted IPs
<span class="badge bg-secondary ms-auto">{{dget . "ip_count"}}</span>
@@ -63,7 +63,7 @@
</li>
<li class="nav-item mb-1">
<a href="/pymta-manager/dkim" class="nav-link text-white {{if eq (dget . "active") "dkim"}}active{{end}}">
<a href="/smtp-server/dkim" class="nav-link text-white {{if eq (dget . "active") "dkim"}}active{{end}}">
<i class="bi bi-shield-check me-2"></i>
DKIM Keys
<span class="badge bg-secondary ms-auto">{{dget . "dkim_count"}}</span>
@@ -72,7 +72,7 @@
{{if dget . "is_global_admin"}}
<li class="nav-item mb-1">
<a href="/pymta-manager/letsencrypt" class="nav-link text-white {{if eq (dget . "active") "letsencrypt"}}active{{end}}">
<a href="/smtp-server/letsencrypt" class="nav-link text-white {{if eq (dget . "active") "letsencrypt"}}active{{end}}">
<i class="bi bi-patch-check me-2"></i>
Let's Encrypt
</a>
@@ -80,7 +80,7 @@
{{end}}
<li class="nav-item mb-1">
<a href="/pymta-manager/logs" class="nav-link text-white {{if eq (dget . "active") "logs"}}active{{end}}">
<a href="/smtp-server/logs" class="nav-link text-white {{if eq (dget . "active") "logs"}}active{{end}}">
<i class="bi bi-journal-text me-2"></i>
Emails Log
</a>
@@ -94,7 +94,7 @@
</h6>
</li>
<li class="nav-item mb-1">
<a href="/pymta-manager/blacklist" class="nav-link text-white {{if eq (dget . "active") "blacklist"}}active{{end}}">
<a href="/smtp-server/blacklist" class="nav-link text-white {{if eq (dget . "active") "blacklist"}}active{{end}}">
<i class="bi bi-shield-x me-2"></i>
Blacklist
<span class="badge bg-secondary ms-auto">{{dget . "blacklist_count"}}</span>
@@ -111,7 +111,7 @@
{{if dget . "is_global_admin"}}
<li class="nav-item mb-1">
<a href="/pymta-manager/settings" class="nav-link text-white {{if eq (dget . "active") "settings"}}active{{end}}">
<a href="/smtp-server/settings" class="nav-link text-white {{if eq (dget . "active") "settings"}}active{{end}}">
<i class="bi bi-sliders me-2"></i>
Server Settings
</a>
@@ -119,14 +119,14 @@
{{end}}
<li class="nav-item mb-1">
<a href="/pymta-manager/admins" class="nav-link text-white {{if eq (dget . "active") "admins"}}active{{end}}">
<a href="/smtp-server/admins" class="nav-link text-white {{if eq (dget . "active") "admins"}}active{{end}}">
<i class="bi bi-people-fill me-2"></i>
Admins
</a>
</li>
<li class="nav-item mb-1">
<a href="/pymta-manager/account" class="nav-link text-white {{if eq (dget . "active") "account"}}active{{end}}">
<a href="/smtp-server/account" class="nav-link text-white {{if eq (dget . "active") "account"}}active{{end}}">
<i class="bi bi-person-circle me-2"></i>
Account
</a>
@@ -141,7 +141,7 @@
</li>
<li class="nav-item mb-1">
<form method="post" action="/pymta-manager/logout">
<form method="post" action="/smtp-server/logout">
<button type="submit" class="nav-link text-white w-100 text-start border-0 bg-transparent">
<i class="bi bi-box-arrow-right me-2"></i>
Sign out
+4 -4
View File
@@ -5,8 +5,8 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Set up authenticator app - mailgoserver</title>
<link href="/pymta-manager/static/vendor/bootstrap/css/bootstrap.min.css" rel="stylesheet">
<link href="/pymta-manager/static/vendor/bootstrap-icons/font/bootstrap-icons.css" rel="stylesheet">
<link href="/smtp-server/static/vendor/bootstrap/css/bootstrap.min.css" rel="stylesheet">
<link href="/smtp-server/static/vendor/bootstrap-icons/font/bootstrap-icons.css" rel="stylesheet">
<style>
body { background-color: #1a1a1a; color: #e0e0e0; }
.card { background-color: #2d2d2d; border: 1px solid #404040; }
@@ -26,13 +26,13 @@
<p class="text-muted">Can't scan? Enter this key manually:</p>
<code class="d-block mb-4" style="word-break: break-all;">{{.secret}}</code>
<form method="POST" action="/pymta-manager/account/totp/confirm" class="text-start">
<form method="POST" action="/smtp-server/account/totp/confirm" class="text-start">
<div class="mb-3">
<label for="code" class="form-label">Enter the 6-digit code from your app to confirm</label>
<input type="text" class="form-control" id="code" name="code" inputmode="numeric" pattern="[0-9]{6}" maxlength="6" required autofocus>
</div>
<div class="d-flex justify-content-between">
<a href="/pymta-manager/account" class="btn btn-secondary">Cancel</a>
<a href="/smtp-server/account" class="btn btn-secondary">Cancel</a>
<button type="submit" class="btn btn-primary"><i class="bi bi-check-lg me-1"></i>Confirm and enable</button>
</div>
</form>
@@ -34,9 +34,9 @@
<li class="list-group-item d-flex justify-content-between align-items-center">
<div><i class="bi bi-paperclip me-1"></i>{{.Filename}} <small class="text-muted">({{filesize .Size}})</small></div>
<div class="btn-group" role="group">
<a href="/pymta-manager/msg/attachment/{{.ID}}/download" class="btn btn-sm btn-outline-primary" target="_blank" rel="noopener" title="Open in new tab"><i class="bi bi-box-arrow-up-right"></i> View</a>
<a href="/pymta-manager/msg/attachment/{{.ID}}/download?download=true" class="btn btn-sm btn-outline-secondary" title="Download file"><i class="bi bi-download"></i> Download</a>
<form method="POST" action="/pymta-manager/msg/attachment/{{.ID}}/delete" style="display: inline;" onsubmit="return confirm('Are you sure you want to delete this attachment?');">
<a href="/smtp-server/msg/attachment/{{.ID}}/download" class="btn btn-sm btn-outline-primary" target="_blank" rel="noopener" title="Open in new tab"><i class="bi bi-box-arrow-up-right"></i> View</a>
<a href="/smtp-server/msg/attachment/{{.ID}}/download?download=true" class="btn btn-sm btn-outline-secondary" title="Download file"><i class="bi bi-download"></i> Download</a>
<form method="POST" action="/smtp-server/msg/attachment/{{.ID}}/delete" style="display: inline;" onsubmit="return confirm('Are you sure you want to delete this attachment?');">
<input type="hidden" name="csrf_token" value="{{$.csrf_token}}">
<button type="submit" class="btn btn-sm btn-outline-danger" title="Delete attachment"><i class="bi bi-trash"></i> Delete</button>
</form>
+1 -1
View File
@@ -40,7 +40,7 @@
</form>
<hr>
<div class="d-grid">
<a href="/pymta-manager/login" class="btn btn-outline-secondary btn-sm"><i class="bi bi-shield-lock me-1"></i>Login as Admin</a>
<a href="/smtp-server/login" class="btn btn-outline-secondary btn-sm"><i class="bi bi-shield-lock me-1"></i>Login as Admin</a>
</div>
</div>
</div>
+2 -2
View File
@@ -1,5 +1,5 @@
// Package webui is the admin web interface, mirroring email_server/server_web_ui/.
// It's mounted at /pymta-manager, matching the Flask blueprint's url_prefix exactly.
// It's mounted at /smtp-server, matching the Flask blueprint's url_prefix exactly.
package webui
import (
@@ -18,7 +18,7 @@ import (
"mailgoserver/internal/toolbox"
)
const Prefix = "/pymta-manager"
const Prefix = "/smtp-server"
// App holds every dependency the web routes need, mirroring the module-level
// singletons (Session, DKIMManager, settings) that server_web_ui/*.py imports.