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, drafts, search, keyboard shortcuts, conversation grouping, recipient autocomplete,
filter-rule management, and PGP (OpenPGP encrypt/decrypt/sign/verify) and S/MIME filter-rule management, and PGP (OpenPGP encrypt/decrypt/sign/verify) and S/MIME
(sign/verify) support per mailbox. (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 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 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 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 on every subsequent start — back these up like any other secret (losing the mailstore
master key makes all stored mail unrecoverable, even for admins). 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 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 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 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`, 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 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, "captured_at": null,
"author": null, "author": null,
"contributor": 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", "id": "internal_webui_templates_account_passkey_management",
"community": 16, "community": 16,
"community_name": "Admin Dashboard & Deployment Docs", "community_name": "Admin Dashboard & Deployment Docs",
+2 -2
View File
@@ -98,7 +98,7 @@ func TestAddAppPasswordSetsRevealCookieNotFlash(t *testing.T) {
mboxID := mailboxes[0].ID mboxID := mailboxes[0].ID
form := url.Values{"label": {"laptop"}, "expires_preset": {"never"}} 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.Header.Set("Content-Type", "application/x-www-form-urlencoded")
req.AddCookie(cookie) req.AddCookie(cookie)
rec := httptest.NewRecorder() rec := httptest.NewRecorder()
@@ -125,7 +125,7 @@ func TestAddAppPasswordSetsRevealCookieNotFlash(t *testing.T) {
// Following the redirect (as the browser would) should render the reveal modal // Following the redirect (as the browser would) should render the reveal modal
// with the secret, and clear the one-time cookie. // 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(cookie)
req2.AddCookie(revealCookie) req2.AddCookie(revealCookie)
rec2 := httptest.NewRecorder() rec2 := httptest.NewRecorder()
+1 -1
View File
@@ -56,7 +56,7 @@ func TestAdminMFAEnforcementForcesIsolatedSetupThenReleases(t *testing.T) {
if !strings.Contains(body, "Two-factor authentication required") { if !strings.Contains(body, "Two-factor authentication required") {
t.Error("expected the MFA-required heading on /mfa-setup") 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") 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 { routes := []struct {
method, path string method, path string
}{ }{
{http.MethodGet, "/pymta-manager/settings"}, {http.MethodGet, "/smtp-server/settings"},
{http.MethodPost, "/pymta-manager/settings_update"}, {http.MethodPost, "/smtp-server/settings_update"},
{http.MethodGet, "/pymta-manager/letsencrypt"}, {http.MethodGet, "/smtp-server/letsencrypt"},
{http.MethodPost, "/pymta-manager/letsencrypt/save"}, {http.MethodPost, "/smtp-server/letsencrypt/save"},
{http.MethodPost, "/pymta-manager/letsencrypt/obtain"}, {http.MethodPost, "/smtp-server/letsencrypt/obtain"},
{http.MethodGet, "/pymta-manager/api/settings/get_public_ip"}, {http.MethodGet, "/smtp-server/api/settings/get_public_ip"},
} }
for _, rt := range routes { for _, rt := range routes {
req := httptest.NewRequest(rt.method, rt.path, nil) 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. // A global admin must still reach these routes normally.
globalCookie := loginSession(t, app) 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) req.AddCookie(globalCookie)
rec := httptest.NewRecorder() rec := httptest.NewRecorder()
mux.ServeHTTP(rec, req) mux.ServeHTTP(rec, req)
@@ -61,7 +61,7 @@ func TestScopedAdminSidebarHidesServerWideLinks(t *testing.T) {
domains, _ := app.DB.ListDomains() domains, _ := app.DB.ListDomains()
cookie := scopedLogin(t, app, "scoped-sidebar-admin", []int64{domains[0].ID}) 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) req.AddCookie(cookie)
rec := httptest.NewRecorder() rec := httptest.NewRecorder()
mux.ServeHTTP(rec, req) 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()) t.Fatalf("dashboard: status=%d body=%s", rec.Code, rec.Body.String())
} }
body := 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") 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") 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) cookie := loginSession(t, app)
form := url.Values{"Server.smtp_port": {"2525"}} 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.Header.Set("Content-Type", "application/x-www-form-urlencoded")
req.AddCookie(cookie) req.AddCookie(cookie)
rec := httptest.NewRecorder() rec := httptest.NewRecorder()
+6 -6
View File
@@ -10,7 +10,7 @@
<p><strong>Username:</strong> {{.user.Username}}</p> <p><strong>Username:</strong> {{.user.Username}}</p>
<hr> <hr>
<h6>Change password</h6> <h6>Change password</h6>
<form method="POST" action="/pymta-manager/account/password"> <form method="POST" action="/smtp-server/account/password">
<div class="mb-3"> <div class="mb-3">
<label class="form-label">Current password</label> <label class="form-label">Current password</label>
<input type="password" class="form-control" name="current_password" required> <input type="password" class="form-control" name="current_password" required>
@@ -36,12 +36,12 @@
<div class="card-body"> <div class="card-body">
{{if .user.TOTPEnabled}} {{if .user.TOTPEnabled}}
<p class="text-success"><i class="bi bi-check-circle me-1"></i>Enabled</p> <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> <button type="submit" class="btn btn-outline-danger btn-sm" data-confirm="Disable authenticator app MFA?">Disable</button>
</form> </form>
{{else}} {{else}}
<p class="text-muted">Not enabled. Add an authenticator app (Google Authenticator, 1Password, etc.) as an optional second factor.</p> <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> <button type="submit" class="btn btn-outline-primary btn-sm"><i class="bi bi-qr-code me-1"></i>Set up</button>
</form> </form>
{{end}} {{end}}
@@ -56,7 +56,7 @@
{{range .passkeys}} {{range .passkeys}}
<li class="list-group-item d-flex justify-content-between align-items-center"> <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> <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> <button type="submit" class="btn btn-sm btn-outline-danger" data-confirm="Remove this passkey?">Remove</button>
</form> </form>
</li> </li>
@@ -96,7 +96,7 @@
try { try {
const name = prompt('Name this passkey (e.g. "YubiKey", "MacBook Touch ID"):', 'Passkey') || 'Passkey'; 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'); if (!beginResp.ok) throw new Error((await beginResp.json()).error || 'Could not start passkey registration');
const options = await beginResp.json(); 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), 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'); if (!finishResp.ok) throw new Error((await finishResp.json()).error || 'Could not save passkey');
+1 -1
View File
@@ -45,7 +45,7 @@
</div> </div>
<div class="d-flex justify-content-between"> <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> <button type="submit" class="btn btn-primary"><i class="bi bi-person-plus me-2"></i>Create Admin</button>
</div> </div>
</form> </form>
+1 -1
View File
@@ -24,7 +24,7 @@
</ul> </ul>
</div> </div>
<div class="d-flex justify-content-between"> <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> <button type="submit" class="btn btn-primary"><i class="bi bi-plus-circle me-2"></i>Add Domain</button>
</div> </div>
</form> </form>
+1 -1
View File
@@ -51,7 +51,7 @@
</ul> </ul>
</div> </div>
<div class="d-flex justify-content-between"> <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> <button type="submit" class="btn btn-success"><i class="bi bi-shield-plus me-2"></i>Add to Whitelist</button>
</div> </div>
</form> </form>
+1 -1
View File
@@ -32,7 +32,7 @@
<div class="form-text">Leave blank to use the domain's default quota.</div> <div class="form-text">Leave blank to use the domain's default quota.</div>
</div> </div>
<div class="d-flex justify-content-between"> <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> <button type="submit" class="btn btn-success"><i class="bi bi-mailbox me-2"></i>Add Mailbox</button>
</div> </div>
</form> </form>
+1 -1
View File
@@ -48,7 +48,7 @@
</ul> </ul>
</div> </div>
<div class="d-flex justify-content-between"> <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> <button type="submit" class="btn btn-success"><i class="bi bi-person-plus me-2"></i>Add Sender</button>
</div> </div>
</form> </form>
+5 -5
View File
@@ -4,7 +4,7 @@
{{define "content"}} {{define "content"}}
<div class="d-flex justify-content-between align-items-center mb-4"> <div class="d-flex justify-content-between align-items-center mb-4">
<h2><i class="bi bi-people-fill me-2"></i>Admins</h2> <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>
<div class="alert alert-info"> <div class="alert alert-info">
@@ -40,12 +40,12 @@
{{else}} {{else}}
<div class="btn-group btn-group-sm" role="group"> <div class="btn-group btn-group-sm" role="group">
{{if not $u.IsGlobalAdmin}} {{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}} {{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> <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>
<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> <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> </form>
</div> </div>
@@ -61,7 +61,7 @@
<i class="bi bi-people text-muted" style="font-size: 4rem;"></i> <i class="bi bi-people text-muted" style="font-size: 4rem;"></i>
<h4 class="text-muted mt-3">No delegated admins yet</h4> <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> <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> </div>
{{end}} {{end}}
</div> </div>
+5 -5
View File
@@ -13,8 +13,8 @@
} }
</script> </script>
<link href="/pymta-manager/static/vendor/bootstrap/css/bootstrap.min.css" rel="stylesheet"> <link href="/smtp-server/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-icons/font/bootstrap-icons.css" rel="stylesheet">
<style> <style>
:root { --sidebar-width: 280px; } :root { --sidebar-width: 280px; }
@@ -54,7 +54,7 @@
::-webkit-scrollbar-thumb:hover { background: #6c757d; } ::-webkit-scrollbar-thumb:hover { background: #6c757d; }
</style> </style>
<link href="/pymta-manager/static/css/smtp-management.css" rel="stylesheet"> <link href="/smtp-server/static/css/smtp-management.css" rel="stylesheet">
<style> <style>
.tooltip-inner { color: #fff !important; background-color: #222 !important; font-size: 1rem; text-align: left; } .tooltip-inner { color: #fff !important; background-color: #222 !important; font-size: 1rem; text-align: left; }
@@ -129,7 +129,7 @@
</div> </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> <script>
function updateTime() { function updateTime() {
@@ -265,7 +265,7 @@
}); });
</script> </script>
<script src="/pymta-manager/static/js/smtp-management.js"></script> <script src="/smtp-server/static/js/smtp-management.js"></script>
<script> <script>
document.addEventListener('DOMContentLoaded', function() { 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><small class="text-muted">{{strftime "%Y-%m-%d %H:%M" .ExpiresAt}}</small></td>
<td> <td>
<div class="btn-group" role="group"> <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> <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>
<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> <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> </form>
</div> </div>
@@ -49,7 +49,7 @@
<div class="card mb-4"> <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-header"><h6 class="mb-0"><i class="bi bi-plus-circle me-2"></i>Manually Blacklist an IP</h6></div>
<div class="card-body"> <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"> <div class="col-md-4">
<label class="form-label small">IP Address</label> <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> <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="card-body">
<div class="alert alert-info small"> <div class="alert alert-info small">
IPs here are exempt from automatic blacklisting for failed SMTP/IMAP auth. This is separate 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> </div>
{{if .whitelist}} {{if .whitelist}}
<div class="table-responsive mb-3"> <div class="table-responsive mb-3">
@@ -87,7 +87,7 @@
<td><small class="text-muted">{{.Note}}</small></td> <td><small class="text-muted">{{.Note}}</small></td>
<td><small class="text-muted">{{strftime "%Y-%m-%d %H:%M" .CreatedAt}}</small></td> <td><small class="text-muted">{{strftime "%Y-%m-%d %H:%M" .CreatedAt}}</small></td>
<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> <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> </form>
</td> </td>
@@ -97,7 +97,7 @@
</table> </table>
</div> </div>
{{end}} {{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"> <div class="col-md-4">
<label class="form-label small">IP Address</label> <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> <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"}} {{define "content"}}
<div class="row"> <div class="row">
<div class="col-lg-3 col-md-6 mb-4"> <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 border-primary">
<div class="card-body"> <div class="card-body">
<div class="d-flex align-items-center"> <div class="d-flex align-items-center">
@@ -21,7 +21,7 @@
</div> </div>
<div class="col-lg-3 col-md-6 mb-4"> <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 border-success">
<div class="card-body"> <div class="card-body">
<div class="d-flex align-items-center"> <div class="d-flex align-items-center">
@@ -38,7 +38,7 @@
</div> </div>
<div class="col-lg-3 col-md-6 mb-4"> <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 border-warning">
<div class="card-body"> <div class="card-body">
<div class="d-flex align-items-center"> <div class="d-flex align-items-center">
@@ -55,7 +55,7 @@
</div> </div>
<div class="col-lg-3 col-md-6 mb-4"> <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 {{if gt .mailboxes_near_quota 0}}border-danger{{else}}border-primary{{end}}">
<div class="card-body"> <div class="card-body">
<div class="d-flex align-items-center"> <div class="d-flex align-items-center">
@@ -106,7 +106,7 @@
<div class="card"> <div class="card">
<div class="card-header d-flex justify-content-between align-items-center"> <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> <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>
<div class="card-body p-0"> <div class="card-body p-0">
{{if .recent_emails}} {{if .recent_emails}}
@@ -154,7 +154,7 @@
<div class="card"> <div class="card">
<div class="card-header d-flex justify-content-between align-items-center"> <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> <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>
<div class="card-body p-0"> <div class="card-body p-0">
{{if .recent_auths}} {{if .recent_auths}}
@@ -187,7 +187,7 @@
<div class="card"> <div class="card">
<div class="card-header d-flex justify-content-between align-items-center"> <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> <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>
<div class="card-body"> <div class="card-body">
<div class="row text-center"> <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-header"><h5 class="mb-0"><i class="bi bi-lightning me-2"></i>Quick Actions</h5></div>
<div class="card-body"> <div class="card-body">
<div class="row"> <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="/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="/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="/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="/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/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"}} {{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}} {{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}} {{end}}
</div> </div>
</div> </div>
+10 -10
View File
@@ -23,7 +23,7 @@
<div class="modal fade" id="createDKIMModal" tabindex="-1" aria-hidden="true"> <div class="modal fade" id="createDKIMModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog"> <div class="modal-dialog">
<div class="modal-content"> <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"> <div class="modal-header">
<h5 class="modal-title">Create New DKIM Key</h5> <h5 class="modal-title">Create New DKIM Key</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> <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 <i class="bi bi-search me-1"></i>Check DNS
</button> </button>
<div class="btn-group btn-group-sm" role="group"> <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> <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="/pymta-manager/dkim/{{$key.ID}}/toggle" class="d-inline"> <form method="post" action="/smtp-server/dkim/{{$key.ID}}/toggle" class="d-inline">
{{if $key.IsActive}} {{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> <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}} {{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> <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}} {{end}}
</form> </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."> <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 <i class="bi bi-trash me-1"></i>Remove
</button> </button>
</form> </form>
</div> </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> <button type="submit" class="btn btn-outline-warning" onclick="event.stopPropagation();"><i class="bi bi-arrow-clockwise me-1"></i>Regenerate</button>
</form> </form>
</div> </div>
@@ -146,10 +146,10 @@
<small class="text-muted">Selector: <code>{{$key.Selector}}</code> | Created: {{strftime "%Y-%m-%d %H:%M" $key.CreatedAt}}</small> <small class="text-muted">Selector: <code>{{$key.Selector}}</code> | Created: {{strftime "%Y-%m-%d %H:%M" $key.CreatedAt}}</small>
</div> </div>
<div class="btn-group btn-group-sm"> <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> <button type="submit" class="btn btn-outline-success btn-sm"><i class="bi bi-play-circle me-1"></i>Reactivate</button>
</form> </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> <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> </form>
</div> </div>
@@ -167,7 +167,7 @@
<i class="bi bi-shield-x text-muted" style="font-size: 4rem;"></i> <i class="bi bi-shield-x text-muted" style="font-size: 4rem;"></i>
<h4 class="text-muted mt-3">No DKIM Keys Found</h4> <h4 class="text-muted mt-3">No DKIM Keys Found</h4>
<p class="text-muted">Add domains first to automatically generate DKIM keys</p> <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>
</div> </div>
{{end}} {{end}}
@@ -196,9 +196,9 @@
dkimStatus.innerHTML = '<span class="status-indicator status-warning"></span><small class="text-muted">Checking...</small>'; 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>'; spfStatus.innerHTML = '<span class="status-indicator status-warning"></span><small class="text-muted">Checking...</small>';
try { 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 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 spfResult = await spfResponse.json();
const domainCard = document.getElementById(`domain-${domain.replace('.', '-')}`); const domainCard = document.getElementById(`domain-${domain.replace('.', '-')}`);
const isActive = domainCard && domainCard.dataset.isActive === 'true'; const isActive = domainCard && domainCard.dataset.isActive === 'true';
+6 -6
View File
@@ -4,7 +4,7 @@
{{define "content"}} {{define "content"}}
<div class="d-flex justify-content-between align-items-center mb-4"> <div class="d-flex justify-content-between align-items-center mb-4">
<h2><i class="bi bi-globe me-2"></i>Domains</h2> <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>
<div class="card"> <div class="card">
@@ -52,15 +52,15 @@
</td> </td>
<td> <td>
<div class="btn-group btn-group-sm" role="group"> <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> <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="/pymta-manager/domains/{{$domain.ID}}/toggle" class="d-inline"> <form method="post" action="/smtp-server/domains/{{$domain.ID}}/toggle" class="d-inline">
{{if $domain.IsActive}} {{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> <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}} {{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> <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}} {{end}}
</form> </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> <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> </form>
</div> </div>
@@ -75,7 +75,7 @@
<i class="bi bi-globe text-muted" style="font-size: 4rem;"></i> <i class="bi bi-globe text-muted" style="font-size: 4rem;"></i>
<h4 class="text-muted mt-3">No domains configured</h4> <h4 class="text-muted mt-3">No domains configured</h4>
<p class="text-muted">Get started by adding your first domain</p> <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> </div>
{{end}} {{end}}
</div> </div>
@@ -135,7 +135,7 @@ document.addEventListener('DOMContentLoaded', function() {
btn.disabled = true; btn.disabled = true;
btn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span>Checking...'; btn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span>Checking...';
try { 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 result = await response.json();
const resultEl = document.getElementById('verifyResult'); const resultEl = document.getElementById('verifyResult');
resultEl.innerHTML = `<div class="alert ${result.success ? 'alert-success' : 'alert-warning'} mb-0 mt-2">${result.message}</div>`; 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> <p class="text-muted">You don't manage any domains to assign.</p>
{{end}} {{end}}
<div class="d-flex justify-content-between"> <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> <button type="submit" class="btn btn-primary"><i class="bi bi-check-lg me-2"></i>Save</button>
</div> </div>
</form> </form>
+2 -2
View File
@@ -7,7 +7,7 @@
<div class="card"> <div class="card">
<div class="card-header bg-primary text-white d-flex justify-content-between align-items-center"> <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> <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>
<div class="card-body"> <div class="card-body">
<form method="POST" class="needs-validation" novalidate> <form method="POST" class="needs-validation" novalidate>
@@ -35,7 +35,7 @@
</div> </div>
</div> </div>
<div class="d-flex justify-content-between"> <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> <button type="submit" class="btn btn-primary"><i class="bi bi-save me-1"></i>Update Selector</button>
</div> </div>
</form> </form>
+2 -2
View File
@@ -7,7 +7,7 @@
<div class="card"> <div class="card">
<div class="card-header bg-primary text-white d-flex justify-content-between align-items-center"> <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> <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>
<div class="card-body"> <div class="card-body">
<form method="POST" class="needs-validation" novalidate> <form method="POST" class="needs-validation" novalidate>
@@ -44,7 +44,7 @@
</div> </div>
<div class="d-flex justify-content-between"> <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> <button type="submit" class="btn btn-primary"><i class="bi bi-save me-1"></i>Update Domain</button>
</div> </div>
</form> </form>
+1 -1
View File
@@ -29,7 +29,7 @@
</div> </div>
<div class="d-flex gap-2"> <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> <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> </div>
</form> </form>
</div> </div>
+5 -5
View File
@@ -28,7 +28,7 @@
</div> </div>
<div class="d-flex gap-2"> <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> <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> </div>
</form> </form>
</div> </div>
@@ -49,10 +49,10 @@
<dd class="col-sm-7"><small class="text-muted">{{filesize .mailbox.UsedBytes}} / {{filesize .mailbox.QuotaBytes}}</small></dd> <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> <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> </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="/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="/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="/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="/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="/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="/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}}/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> </div>
</div> </div>
+1 -1
View File
@@ -41,7 +41,7 @@
</div> </div>
<div class="d-flex gap-2"> <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> <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> </div>
</form> </form>
</div> </div>
+1 -1
View File
@@ -49,7 +49,7 @@
<div class="card-footer"> <div class="card-footer">
<div class="d-flex justify-content-between align-items-center"> <div class="d-flex justify-content-between align-items-center">
<div> <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> <i class="fas fa-home me-1"></i>
Return to Dashboard Return to Dashboard
</a> </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-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"> <div class="card-body">
{{if .error}}<div class="alert alert-danger">{{.error}}</div>{{end}} {{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}} {{if .must_change_username}}
<div class="mb-3"> <div class="mb-3">
<label for="username" class="form-label">New username</label> <label for="username" class="form-label">New username</label>
+7 -7
View File
@@ -4,7 +4,7 @@
<div class="container-fluid"> <div class="container-fluid">
<div class="d-flex justify-content-between align-items-center mb-4"> <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> <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>
<div class="card mb-4"> <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><small class="text-muted">{{strftime "%Y-%m-%d %H:%M" $ip.CreatedAt}}</small></td>
<td> <td>
<div class="btn-group" role="group"> <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}} {{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> <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> </form>
{{else}} {{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> <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> </form>
{{end}} {{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> <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> </form>
</div> </div>
@@ -50,7 +50,7 @@
<i class="bi bi-router text-muted" style="font-size: 4rem;"></i> <i class="bi bi-router text-muted" style="font-size: 4rem;"></i>
<h4 class="text-muted mt-3">No IP Addresses Whitelisted</h4> <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> <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> </div>
{{end}} {{end}}
</div> </div>
@@ -104,7 +104,7 @@
const currentIPElement = document.getElementById('current-ip'); const currentIPElement = document.getElementById('current-ip');
const ip = currentIPElement.textContent.trim(); const ip = currentIPElement.textContent.trim();
if (ip && ip !== 'Detecting...' && ip !== 'Unable to detect') { 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); url.searchParams.set('ip', ip);
window.location.href = url.toString(); window.location.href = url.toString();
} else { } else {
+7 -7
View File
@@ -10,7 +10,7 @@
Two independent certificates can be obtained here — DNS-01 (needs a supported DNS provider) and 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 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 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. section — e.g. run the HTTP-01 cert on mail while the dashboard keeps a DNS-01 or custom cert.
</p> </p>
@@ -43,12 +43,12 @@
{{end}} {{end}}
</dd> </dd>
</dl> </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> <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> </form>
<hr> <hr>
<form method="POST" action="/pymta-manager/letsencrypt/save"> <form method="POST" action="/smtp-server/letsencrypt/save">
<div class="mb-3"> <div class="mb-3">
<label class="form-label">Enable DNS-01</label> <label class="form-label">Enable DNS-01</label>
<select class="form-select" name="enabled"> <select class="form-select" name="enabled">
@@ -176,12 +176,12 @@
{{end}} {{end}}
</dd> </dd>
</dl> </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> <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> </form>
<hr> <hr>
<form method="POST" action="/pymta-manager/letsencrypt/http/save"> <form method="POST" action="/smtp-server/letsencrypt/http/save">
<div class="mb-3"> <div class="mb-3">
<label class="form-label">Enable HTTP-01</label> <label class="form-label">Enable HTTP-01</label>
<select class="form-select" name="enabled"> <select class="form-select" name="enabled">
@@ -240,7 +240,7 @@
updateProviderFields(); updateProviderFields();
document.getElementById('le_detect_ip_btn').addEventListener('click', function() { 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(r => r.json())
.then(data => { .then(data => {
if (data.status === 'success') { document.getElementById('le_ip_override').value = data.ip; showToast('Detected WAN IP: ' + data.ip, 'success'); } 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; if (!file) return;
const formData = new FormData(); const formData = new FormData();
formData.append('gcloud_key_file', file); 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(r => r.json())
.then(data => { .then(data => {
if (data.status === 'success') { document.getElementById('gcloud_sa_path').value = data.filepath; showToast('Service account key uploaded', 'success'); } 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 charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Sign in - mailgoserver</title> <title>Sign in - mailgoserver</title>
<link href="/pymta-manager/static/vendor/bootstrap/css/bootstrap.min.css" rel="stylesheet"> <link href="/smtp-server/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-icons/font/bootstrap-icons.css" rel="stylesheet">
<style> <style>
body { background-color: #1a1a1a; color: #e0e0e0; min-height: 100vh; display: flex; align-items: center; } body { background-color: #1a1a1a; color: #e0e0e0; min-height: 100vh; display: flex; align-items: center; }
.login-card { max-width: 420px; margin: 0 auto; width: 100%; } .login-card { max-width: 420px; margin: 0 auto; width: 100%; }
@@ -24,7 +24,7 @@
<div class="card"> <div class="card">
<div class="card-body p-4"> <div class="card-body p-4">
{{if .error}}<div class="alert alert-danger">{{.error}}</div>{{end}} {{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}}"> <input type="hidden" name="next" value="{{.next}}">
<div class="mb-3"> <div class="mb-3">
<label for="username" class="form-label">Username</label> <label for="username" class="form-label">Username</label>
+6 -6
View File
@@ -5,8 +5,8 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Verify it's you - mailgoserver</title> <title>Verify it's you - mailgoserver</title>
<link href="/pymta-manager/static/vendor/bootstrap/css/bootstrap.min.css" rel="stylesheet"> <link href="/smtp-server/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-icons/font/bootstrap-icons.css" rel="stylesheet">
<style> <style>
body { background-color: #1a1a1a; color: #e0e0e0; min-height: 100vh; display: flex; align-items: center; } body { background-color: #1a1a1a; color: #e0e0e0; min-height: 100vh; display: flex; align-items: center; }
.login-card { max-width: 420px; margin: 0 auto; width: 100%; } .login-card { max-width: 420px; margin: 0 auto; width: 100%; }
@@ -40,7 +40,7 @@
{{end}} {{end}}
{{if .totp_enabled}} {{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}}"> <input type="hidden" name="next" value="{{.next}}">
<div class="mb-3"> <div class="mb-3">
<label for="code" class="form-label">6-digit authenticator code</label> <label for="code" class="form-label">6-digit authenticator code</label>
@@ -87,7 +87,7 @@
const errEl = document.getElementById('passkey-error'); const errEl = document.getElementById('passkey-error');
errEl.classList.add('d-none'); errEl.classList.add('d-none');
try { 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'); if (!beginResp.ok) throw new Error((await beginResp.json()).error || 'Could not start passkey login');
const options = await beginResp.json(); 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), method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body),
}); });
if (!finishResp.ok) throw new Error((await finishResp.json()).error || 'Passkey verification failed'); 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) { } catch (e) {
errEl.textContent = e.message || 'Passkey login failed'; errEl.textContent = e.message || 'Passkey login failed';
errEl.classList.remove('d-none'); 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"> <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> <h2><i class="bi bi-journal-text me-2"></i>Emails Log</h2>
<div class="btn-group"> <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="/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="/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="/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="/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=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>
</div> </div>
{{if eq .filter_type "auth"}} {{if eq .filter_type "auth"}}
<div class="d-flex justify-content-end mb-3"> <div class="d-flex justify-content-end mb-3">
<div class="btn-group btn-group-sm"> <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="/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="/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="/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="/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="/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="/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&auth_category=mailserver" class="btn {{if eq .auth_category "mailserver"}}btn-secondary{{else}}btn-outline-secondary{{end}}">Mail Server</a>
</div> </div>
</div> </div>
{{end}} {{end}}
@@ -145,9 +145,9 @@
{{if or .has_prev .has_next}} {{if or .has_prev .has_next}}
<nav aria-label="Log pagination" class="mt-4"> <nav aria-label="Log pagination" class="mt-4">
<ul class="pagination justify-content-center"> <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> <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> </ul>
</nav> </nav>
{{end}} {{end}}
@@ -184,7 +184,7 @@
const body = document.getElementById('messageContentModalBody'); const body = document.getElementById('messageContentModalBody');
body.innerHTML = '<div class="text-center text-muted py-5"><div class="spinner-border" role="status"></div></div>'; body.innerHTML = '<div class="text-center text-muted py-5"><div class="spinner-border" role="status"></div></div>';
bootstrap.Modal.getOrCreateInstance(modalEl).show(); 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(r) { if (!r.ok) throw new Error(r.status); return r.text(); })
.then(function(html) { body.innerHTML = html; }) .then(function(html) { body.innerHTML = html; })
.catch(function() { body.innerHTML = '<p class="text-danger">Failed to load the message.</p>'; }); .catch(function() { body.innerHTML = '<p class="text-danger">Failed to load the message.</p>'; });
@@ -4,7 +4,7 @@
{{define "content"}} {{define "content"}}
<div class="d-flex justify-content-between align-items-center mb-4"> <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> <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>
<div class="alert alert-info"> <div class="alert alert-info">
@@ -14,7 +14,7 @@
<div class="card mb-4"> <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-header"><h5 class="mb-0"><i class="bi bi-plus-circle me-2"></i>Add Alias</h5></div>
<div class="card-body"> <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="mb-3">
<div class="input-group"> <div class="input-group">
<input type="text" class="form-control" id="local_part" name="local_part" required placeholder="alias" <input type="text" class="form-control" id="local_part" name="local_part" required placeholder="alias"
@@ -55,7 +55,7 @@
</td> </td>
<td><small class="text-muted">{{strftime "%Y-%m-%d %H:%M" .CreatedAt}}</small></td> <td><small class="text-muted">{{strftime "%Y-%m-%d %H:%M" .CreatedAt}}</small></td>
<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> <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> </form>
</td> </td>
@@ -5,8 +5,8 @@
<div class="d-flex justify-content-between align-items-center mb-4"> <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> <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"> <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="/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="/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>
</div> </div>
@@ -17,7 +17,7 @@
<div class="card mb-4"> <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-header"><h5 class="mb-0"><i class="bi bi-plus-circle me-2"></i>Create App Password</h5></div>
<div class="card-body"> <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"> <div class="col-auto">
<label for="label" class="form-label">Label</label> <label for="label" class="form-label">Label</label>
<input type="text" class="form-control" id="label" name="label" placeholder="e.g. Thunderbird laptop"> <input type="text" class="form-control" id="label" name="label" placeholder="e.g. Thunderbird laptop">
@@ -66,7 +66,7 @@
</td> </td>
<td>{{if .IsActive}}<span class="badge bg-success">Active</span>{{else}}<span class="badge bg-danger">Revoked</span>{{end}}</td> <td>{{if .IsActive}}<span class="badge bg-success">Active</span>{{else}}<span class="badge bg-danger">Revoked</span>{{end}}</td>
<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> <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> </form>
</td> </td>
+5 -5
View File
@@ -4,7 +4,7 @@
{{define "content"}} {{define "content"}}
<div class="d-flex justify-content-between align-items-center mb-4"> <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> <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>
<div class="alert alert-info"> <div class="alert alert-info">
@@ -16,7 +16,7 @@
<div class="card"> <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-header"><h5 class="mb-0 text-success"><i class="bi bi-check-circle me-2"></i>Allow List</h5></div>
<div class="card-body"> <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="hidden" name="list_type" value="allow">
<input type="text" class="form-control" name="pattern" placeholder="friend@example.com or @example.com" required> <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> <button type="submit" class="btn btn-success"><i class="bi bi-plus-lg"></i></button>
@@ -26,7 +26,7 @@
{{range .allow}} {{range .allow}}
<li class="list-group-item list-group-item-dark d-flex justify-content-between align-items-center"> <li class="list-group-item list-group-item-dark d-flex justify-content-between align-items-center">
<code>{{.Pattern}}</code> <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> <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> </form>
</li> </li>
@@ -43,7 +43,7 @@
<div class="card"> <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-header"><h5 class="mb-0 text-danger"><i class="bi bi-x-circle me-2"></i>Block List</h5></div>
<div class="card-body"> <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="hidden" name="list_type" value="block">
<input type="text" class="form-control" name="pattern" placeholder="spam@evil.com or @evil.com" required> <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> <button type="submit" class="btn btn-danger"><i class="bi bi-plus-lg"></i></button>
@@ -53,7 +53,7 @@
{{range .block}} {{range .block}}
<li class="list-group-item list-group-item-dark d-flex justify-content-between align-items-center"> <li class="list-group-item list-group-item-dark d-flex justify-content-between align-items-center">
<code>{{.Pattern}}</code> <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> <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> </form>
</li> </li>
+3 -3
View File
@@ -4,7 +4,7 @@
{{define "content"}} {{define "content"}}
<div class="d-flex justify-content-between align-items-center mb-4"> <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> <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>
<div class="alert alert-info"> <div class="alert alert-info">
@@ -14,7 +14,7 @@
<div class="card mb-4"> <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-header"><h5 class="mb-0"><i class="bi bi-plus-circle me-2"></i>Add Rule</h5></div>
<div class="card-body"> <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="row g-2 align-items-end mb-3">
<div class="col-auto"> <div class="col-auto">
<label class="form-label">Priority</label> <label class="form-label">Priority</label>
@@ -99,7 +99,7 @@
</td> </td>
<td>{{if .IsActive}}<span class="badge bg-success">Active</span>{{else}}<span class="badge bg-secondary">Inactive</span>{{end}}</td> <td>{{if .IsActive}}<span class="badge bg-success">Active</span>{{else}}<span class="badge bg-secondary">Inactive</span>{{end}}</td>
<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> <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> </form>
</td> </td>
+9 -9
View File
@@ -4,7 +4,7 @@
{{define "content"}} {{define "content"}}
<div class="d-flex justify-content-between align-items-center mb-4"> <div class="d-flex justify-content-between align-items-center mb-4">
<h2><i class="bi bi-inbox me-2"></i>Mailboxes</h2> <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>
<div class="alert alert-info"> <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><small class="text-muted">{{strftime "%Y-%m-%d %H:%M" $mailbox.CreatedAt}}</small></td>
<td> <td>
<div class="btn-group" role="group"> <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="/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="/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="/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="/pymta-manager/mailboxes/{{$mailbox.ID}}/edit" class="btn btn-outline-primary btn-sm" title="Edit Mailbox"><i class="bi bi-pencil"></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="/pymta-manager/mailboxes/{{$mailbox.ID}}/reset_mfa" class="d-inline"> <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> <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> </form>
{{if $mailbox.IsActive}} {{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> <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> </form>
{{else}} {{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> <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> </form>
{{end}} {{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> <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> </form>
</div> </div>
@@ -70,7 +70,7 @@
<i class="bi bi-inbox text-muted" style="font-size: 4rem;"></i> <i class="bi bi-inbox text-muted" style="font-size: 4rem;"></i>
<h4 class="text-muted mt-3">No mailboxes configured</h4> <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> <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> </div>
{{end}} {{end}}
</div> </div>
@@ -5,8 +5,8 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Set up two-factor authentication - mailgoserver</title> <title>Set up two-factor authentication - mailgoserver</title>
<link href="/pymta-manager/static/vendor/bootstrap/css/bootstrap.min.css" rel="stylesheet"> <link href="/smtp-server/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-icons/font/bootstrap-icons.css" rel="stylesheet">
<style> <style>
body { background-color: #1a1a1a; color: #e0e0e0; min-height: 100vh; display: flex; align-items: center; } body { background-color: #1a1a1a; color: #e0e0e0; min-height: 100vh; display: flex; align-items: center; }
.setup-card { max-width: 480px; margin: 0 auto; width: 100%; } .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-header"><h5 class="mb-0"><i class="bi bi-qr-code me-2"></i>Authenticator App</h5></div>
<div class="card-body"> <div class="card-body">
<p class="text-muted">Use Google Authenticator, 1Password, or any TOTP app.</p> <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> <button type="submit" class="btn btn-outline-primary"><i class="bi bi-qr-code me-1"></i>Set up Authenticator App</button>
</form> </form>
</div> </div>
</div> </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> <script>
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
document.querySelectorAll('.toast').forEach(function(el) { new bootstrap.Toast(el, {delay: 6000}).show(); }); document.querySelectorAll('.toast').forEach(function(el) { new bootstrap.Toast(el, {delay: 6000}).show(); });
@@ -79,7 +79,7 @@
const errEl = document.getElementById('passkey-error'); const errEl = document.getElementById('passkey-error');
errEl.classList.add('d-none'); errEl.classList.add('d-none');
try { 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'); if (!beginResp.ok) throw new Error((await beginResp.json()).error || 'Could not start passkey registration');
const options = await beginResp.json(); const options = await beginResp.json();
const publicKey = options.publicKey; const publicKey = options.publicKey;
@@ -96,11 +96,11 @@
clientDataJSON: bufToB64url(cred.response.clientDataJSON), 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), 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'); 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) { } catch (e) {
errEl.textContent = e.message || 'Passkey registration failed'; errEl.textContent = e.message || 'Passkey registration failed';
errEl.classList.remove('d-none'); errEl.classList.remove('d-none');
+6 -6
View File
@@ -4,7 +4,7 @@
{{define "content"}} {{define "content"}}
<div class="d-flex justify-content-between align-items-center mb-4"> <div class="d-flex justify-content-between align-items-center mb-4">
<h2><i class="bi bi-people me-2"></i>Senders</h2> <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>
<div class="card"> <div class="card">
@@ -40,17 +40,17 @@
<td><small class="text-muted">{{strftime "%Y-%m-%d %H:%M" $sender.CreatedAt}}</small></td> <td><small class="text-muted">{{strftime "%Y-%m-%d %H:%M" $sender.CreatedAt}}</small></td>
<td> <td>
<div class="btn-group" role="group"> <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}} {{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> <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> </form>
{{else}} {{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> <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> </form>
{{end}} {{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> <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> </form>
</div> </div>
@@ -65,7 +65,7 @@
<i class="bi bi-people text-muted" style="font-size: 4rem;"></i> <i class="bi bi-people text-muted" style="font-size: 4rem;"></i>
<h4 class="text-muted mt-3">No senders configured</h4> <h4 class="text-muted mt-3">No senders configured</h4>
<p class="text-muted">Add sender to enable username/password authentication</p> <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> </div>
{{end}} {{end}}
</div> </div>
+7 -7
View File
@@ -16,7 +16,7 @@
</div> </div>
</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 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-header"><h5 class="mb-0"><i class="bi bi-server me-2"></i>Server Configuration</h5></div>
<div class="card-body"> <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> <div class="form-text mb-3">The "custom" certificate: self-signed on first run, or your own uploaded cert/key above.</div>
<hr> <hr>
<p class="mb-2">Which certificate each TLS listener uses — <code>custom</code> (above), or one of the two Let's <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> 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="row">
<div class="col-md-4"><div class="mb-3"><label class="form-label">SMTP (direct-TLS, 465)</label> <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; if (!file) return;
const formData = new FormData(); const formData = new FormData();
formData.append('cert_file', file); 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(r => r.json())
.then(data => { .then(data => {
if (data.status === 'success') { document.querySelector('[name="TLS.tls_cert_file"]').value = data.filepath; showToast('Certificate uploaded', 'success'); } 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; if (!file) return;
const formData = new FormData(); const formData = new FormData();
formData.append('key_file', file); 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(r => r.json())
.then(data => { .then(data => {
if (data.status === 'success') { document.querySelector('[name="TLS.tls_key_file"]').value = data.filepath; showToast('Key uploaded', 'success'); } if (data.status === 'success') { document.querySelector('[name="TLS.tls_key_file"]').value = data.filepath; showToast('Key uploaded', 'success'); }
@@ -350,14 +350,14 @@
function testDatabaseConnection() { function testDatabaseConnection() {
const url = document.getElementById('databaseUrl').value; 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(r => r.json())
.then(data => showToast(data.message || (data.status === 'success' ? 'Connection successful!' : 'Failed to connect'), data.status === 'success' ? 'success' : 'danger')) .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')); .catch(() => showToast('Failed to test database connection', 'danger'));
} }
function getPublicIP() { function getPublicIP() {
fetch('/pymta-manager/api/settings/get_public_ip') fetch('/smtp-server/api/settings/get_public_ip')
.then(r => r.json()) .then(r => r.json())
.then(data => { .then(data => {
if (data.ip) { document.querySelector('[name="DKIM.spf_server_ip"]').value = data.ip; showToast('Public IP fetched', 'success'); } 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 path = document.querySelector('input[name="Attachments.attachments_path"]').value;
const feedback = document.getElementById('attachments-path-feedback'); const feedback = document.getElementById('attachments-path-feedback');
if (!feedback) return; 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(r => r.json())
.then(data => { .then(data => {
feedback.innerHTML = data.message + (data.success ? `<br><small class="text-muted">Absolute path: ${data.absolute_path}</small>` : ''); 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"> <div class="flex-grow-1 overflow-auto">
<ul class="nav nav-pills flex-column p-3"> <ul class="nav nav-pills flex-column p-3">
<li class="nav-item mb-2"> <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> <i class="bi bi-speedometer2 me-2"></i>
Dashboard Dashboard
</a> </a>
@@ -31,7 +31,7 @@
</li> </li>
<li class="nav-item mb-1"> <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> <i class="bi bi-list-ul me-2"></i>
Domains Domains
<span class="badge bg-secondary ms-auto">{{dget . "domain_count"}}</span> <span class="badge bg-secondary ms-auto">{{dget . "domain_count"}}</span>
@@ -39,7 +39,7 @@
</li> </li>
<li class="nav-item mb-1"> <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> <i class="bi bi-people me-2"></i>
Allowed Senders Allowed Senders
<span class="badge bg-secondary ms-auto">{{dget . "sender_count"}}</span> <span class="badge bg-secondary ms-auto">{{dget . "sender_count"}}</span>
@@ -47,7 +47,7 @@
</li> </li>
<li class="nav-item mb-1"> <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> <i class="bi bi-inbox me-2"></i>
Mailboxes Mailboxes
<span class="badge bg-secondary ms-auto">{{dget . "mailbox_count"}}</span> <span class="badge bg-secondary ms-auto">{{dget . "mailbox_count"}}</span>
@@ -55,7 +55,7 @@
</li> </li>
<li class="nav-item mb-1"> <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> <i class="bi bi-router me-2"></i>
Whitelisted IPs Whitelisted IPs
<span class="badge bg-secondary ms-auto">{{dget . "ip_count"}}</span> <span class="badge bg-secondary ms-auto">{{dget . "ip_count"}}</span>
@@ -63,7 +63,7 @@
</li> </li>
<li class="nav-item mb-1"> <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> <i class="bi bi-shield-check me-2"></i>
DKIM Keys DKIM Keys
<span class="badge bg-secondary ms-auto">{{dget . "dkim_count"}}</span> <span class="badge bg-secondary ms-auto">{{dget . "dkim_count"}}</span>
@@ -72,7 +72,7 @@
{{if dget . "is_global_admin"}} {{if dget . "is_global_admin"}}
<li class="nav-item mb-1"> <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> <i class="bi bi-patch-check me-2"></i>
Let's Encrypt Let's Encrypt
</a> </a>
@@ -80,7 +80,7 @@
{{end}} {{end}}
<li class="nav-item mb-1"> <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> <i class="bi bi-journal-text me-2"></i>
Emails Log Emails Log
</a> </a>
@@ -94,7 +94,7 @@
</h6> </h6>
</li> </li>
<li class="nav-item mb-1"> <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> <i class="bi bi-shield-x me-2"></i>
Blacklist Blacklist
<span class="badge bg-secondary ms-auto">{{dget . "blacklist_count"}}</span> <span class="badge bg-secondary ms-auto">{{dget . "blacklist_count"}}</span>
@@ -111,7 +111,7 @@
{{if dget . "is_global_admin"}} {{if dget . "is_global_admin"}}
<li class="nav-item mb-1"> <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> <i class="bi bi-sliders me-2"></i>
Server Settings Server Settings
</a> </a>
@@ -119,14 +119,14 @@
{{end}} {{end}}
<li class="nav-item mb-1"> <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> <i class="bi bi-people-fill me-2"></i>
Admins Admins
</a> </a>
</li> </li>
<li class="nav-item mb-1"> <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> <i class="bi bi-person-circle me-2"></i>
Account Account
</a> </a>
@@ -141,7 +141,7 @@
</li> </li>
<li class="nav-item mb-1"> <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"> <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> <i class="bi bi-box-arrow-right me-2"></i>
Sign out Sign out
+4 -4
View File
@@ -5,8 +5,8 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Set up authenticator app - mailgoserver</title> <title>Set up authenticator app - mailgoserver</title>
<link href="/pymta-manager/static/vendor/bootstrap/css/bootstrap.min.css" rel="stylesheet"> <link href="/smtp-server/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-icons/font/bootstrap-icons.css" rel="stylesheet">
<style> <style>
body { background-color: #1a1a1a; color: #e0e0e0; } body { background-color: #1a1a1a; color: #e0e0e0; }
.card { background-color: #2d2d2d; border: 1px solid #404040; } .card { background-color: #2d2d2d; border: 1px solid #404040; }
@@ -26,13 +26,13 @@
<p class="text-muted">Can't scan? Enter this key manually:</p> <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> <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"> <div class="mb-3">
<label for="code" class="form-label">Enter the 6-digit code from your app to confirm</label> <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> <input type="text" class="form-control" id="code" name="code" inputmode="numeric" pattern="[0-9]{6}" maxlength="6" required autofocus>
</div> </div>
<div class="d-flex justify-content-between"> <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> <button type="submit" class="btn btn-primary"><i class="bi bi-check-lg me-1"></i>Confirm and enable</button>
</div> </div>
</form> </form>
@@ -34,9 +34,9 @@
<li class="list-group-item d-flex justify-content-between align-items-center"> <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><i class="bi bi-paperclip me-1"></i>{{.Filename}} <small class="text-muted">({{filesize .Size}})</small></div>
<div class="btn-group" role="group"> <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="/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="/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> <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="/pymta-manager/msg/attachment/{{.ID}}/delete" style="display: inline;" onsubmit="return confirm('Are you sure you want to delete this attachment?');"> <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}}"> <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> <button type="submit" class="btn btn-sm btn-outline-danger" title="Delete attachment"><i class="bi bi-trash"></i> Delete</button>
</form> </form>
+1 -1
View File
@@ -40,7 +40,7 @@
</form> </form>
<hr> <hr>
<div class="d-grid"> <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> </div>
</div> </div>
+2 -2
View File
@@ -1,5 +1,5 @@
// Package webui is the admin web interface, mirroring email_server/server_web_ui/. // 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 package webui
import ( import (
@@ -18,7 +18,7 @@ import (
"mailgoserver/internal/toolbox" "mailgoserver/internal/toolbox"
) )
const Prefix = "/pymta-manager" const Prefix = "/smtp-server"
// App holds every dependency the web routes need, mirroring the module-level // App holds every dependency the web routes need, mirroring the module-level
// singletons (Session, DKIMManager, settings) that server_web_ui/*.py imports. // singletons (Session, DKIMManager, settings) that server_web_ui/*.py imports.