diff --git a/README.md b/README.md index 1016408..457a4f2 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ a Python venv + separate services. drafts, search, keyboard shortcuts, conversation grouping, recipient autocomplete, filter-rule management, and PGP (OpenPGP encrypt/decrypt/sign/verify) and S/MIME (sign/verify) support per mailbox. -- **Admin dashboard** (`/pymta-manager`) — manage domains, senders, mailboxes, DKIM +- **Admin dashboard** (`/smtp-server`) — manage domains, senders, mailboxes, DKIM keys, IP whitelisting, TLS (self-signed/custom, or up to two simultaneous Let's Encrypt certificates — DNS-01 via Cloudflare/Route53/DigitalOcean/Google Cloud DNS, and HTTP-01 for domains you don't manage DNS for, optionally covering the server's own IP @@ -75,11 +75,11 @@ directory, it generates: on every subsequent start — back these up like any other secret (losing the mailstore master key makes all stored mail unrecoverable, even for admins). -From there: log into `/pymta-manager`, add a domain (and complete its DNS ownership +From there: log into `/smtp-server`, add a domain (and complete its DNS ownership verification), add a mailbox or sender, and you're sending/receiving. The bare `/` root redirects to the webmail login (`/webmail/login`) by default, since most visitors are mailbox owners, not admins — there's a "Login as Admin" link from there to -`/pymta-manager`. +`/smtp-server`. Default ports (all configurable in `settings.ini`): SMTP `25`, direct-TLS SMTP `465`, IMAP `143`, direct-TLS IMAP `993` — the real standard mail ports, so binding them diff --git a/graphify-out/cache/semantic/pd5fd89c46bb5/c9fdca732d5d23867a176f849a94a2b3ee22e4fe7de81b5e85902c26ddbf941a.json b/graphify-out/cache/semantic/pd5fd89c46bb5/c9fdca732d5d23867a176f849a94a2b3ee22e4fe7de81b5e85902c26ddbf941a.json index 316d2b3..b4aa984 100644 --- a/graphify-out/cache/semantic/pd5fd89c46bb5/c9fdca732d5d23867a176f849a94a2b3ee22e4fe7de81b5e85902c26ddbf941a.json +++ b/graphify-out/cache/semantic/pd5fd89c46bb5/c9fdca732d5d23867a176f849a94a2b3ee22e4fe7de81b5e85902c26ddbf941a.json @@ -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"}]} \ No newline at end of file +{"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"}]} \ No newline at end of file diff --git a/graphify-out/graph.json b/graphify-out/graph.json index 4d376ce..92df710 100644 --- a/graphify-out/graph.json +++ b/graphify-out/graph.json @@ -10719,7 +10719,7 @@ "captured_at": null, "author": null, "contributor": null, - "rationale": "Uses the browser navigator.credentials.create() WebAuthn API directly against /pymta-manager/account/passkey/begin and /finish, base64url-encoding challenge/credential buffers by hand since no client library is loaded.", + "rationale": "Uses the browser navigator.credentials.create() WebAuthn API directly against /smtp-server/account/passkey/begin and /finish, base64url-encoding challenge/credential buffers by hand since no client library is loaded.", "id": "internal_webui_templates_account_passkey_management", "community": 16, "community_name": "Admin Dashboard & Deployment Docs", diff --git a/internal/webui/mailbox_apppasswords_test.go b/internal/webui/mailbox_apppasswords_test.go index e565a54..479889c 100644 --- a/internal/webui/mailbox_apppasswords_test.go +++ b/internal/webui/mailbox_apppasswords_test.go @@ -98,7 +98,7 @@ func TestAddAppPasswordSetsRevealCookieNotFlash(t *testing.T) { mboxID := mailboxes[0].ID form := url.Values{"label": {"laptop"}, "expires_preset": {"never"}} - req := httptest.NewRequest(http.MethodPost, "/pymta-manager/mailboxes/"+itoa(mboxID)+"/apppasswords/add", strings.NewReader(form.Encode())) + req := httptest.NewRequest(http.MethodPost, "/smtp-server/mailboxes/"+itoa(mboxID)+"/apppasswords/add", strings.NewReader(form.Encode())) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.AddCookie(cookie) rec := httptest.NewRecorder() @@ -125,7 +125,7 @@ func TestAddAppPasswordSetsRevealCookieNotFlash(t *testing.T) { // Following the redirect (as the browser would) should render the reveal modal // with the secret, and clear the one-time cookie. - req2 := httptest.NewRequest(http.MethodGet, "/pymta-manager/mailboxes/"+itoa(mboxID)+"/apppasswords", nil) + req2 := httptest.NewRequest(http.MethodGet, "/smtp-server/mailboxes/"+itoa(mboxID)+"/apppasswords", nil) req2.AddCookie(cookie) req2.AddCookie(revealCookie) rec2 := httptest.NewRecorder() diff --git a/internal/webui/mfa_enforcement_test.go b/internal/webui/mfa_enforcement_test.go index 43fc816..c15400b 100644 --- a/internal/webui/mfa_enforcement_test.go +++ b/internal/webui/mfa_enforcement_test.go @@ -56,7 +56,7 @@ func TestAdminMFAEnforcementForcesIsolatedSetupThenReleases(t *testing.T) { if !strings.Contains(body, "Two-factor authentication required") { t.Error("expected the MFA-required heading on /mfa-setup") } - if strings.Contains(body, "sidebar") || strings.Contains(body, `href="/pymta-manager/domains"`) { + if strings.Contains(body, "sidebar") || strings.Contains(body, `href="/smtp-server/domains"`) { t.Error("expected no sidebar/navigation on the isolated setup page") } diff --git a/internal/webui/settings_scope_test.go b/internal/webui/settings_scope_test.go index 370ed69..7227467 100644 --- a/internal/webui/settings_scope_test.go +++ b/internal/webui/settings_scope_test.go @@ -24,12 +24,12 @@ func TestScopedAdminCannotAccessServerSettingsOrLetsEncrypt(t *testing.T) { routes := []struct { method, path string }{ - {http.MethodGet, "/pymta-manager/settings"}, - {http.MethodPost, "/pymta-manager/settings_update"}, - {http.MethodGet, "/pymta-manager/letsencrypt"}, - {http.MethodPost, "/pymta-manager/letsencrypt/save"}, - {http.MethodPost, "/pymta-manager/letsencrypt/obtain"}, - {http.MethodGet, "/pymta-manager/api/settings/get_public_ip"}, + {http.MethodGet, "/smtp-server/settings"}, + {http.MethodPost, "/smtp-server/settings_update"}, + {http.MethodGet, "/smtp-server/letsencrypt"}, + {http.MethodPost, "/smtp-server/letsencrypt/save"}, + {http.MethodPost, "/smtp-server/letsencrypt/obtain"}, + {http.MethodGet, "/smtp-server/api/settings/get_public_ip"}, } for _, rt := range routes { req := httptest.NewRequest(rt.method, rt.path, nil) @@ -43,7 +43,7 @@ func TestScopedAdminCannotAccessServerSettingsOrLetsEncrypt(t *testing.T) { // A global admin must still reach these routes normally. globalCookie := loginSession(t, app) - req := httptest.NewRequest(http.MethodGet, "/pymta-manager/settings", nil) + req := httptest.NewRequest(http.MethodGet, "/smtp-server/settings", nil) req.AddCookie(globalCookie) rec := httptest.NewRecorder() mux.ServeHTTP(rec, req) @@ -61,7 +61,7 @@ func TestScopedAdminSidebarHidesServerWideLinks(t *testing.T) { domains, _ := app.DB.ListDomains() cookie := scopedLogin(t, app, "scoped-sidebar-admin", []int64{domains[0].ID}) - req := httptest.NewRequest(http.MethodGet, "/pymta-manager/", nil) + req := httptest.NewRequest(http.MethodGet, "/smtp-server/", nil) req.AddCookie(cookie) rec := httptest.NewRecorder() mux.ServeHTTP(rec, req) @@ -69,10 +69,10 @@ func TestScopedAdminSidebarHidesServerWideLinks(t *testing.T) { t.Fatalf("dashboard: status=%d body=%s", rec.Code, rec.Body.String()) } body := rec.Body.String() - if strings.Contains(body, `href="/pymta-manager/settings"`) { + if strings.Contains(body, `href="/smtp-server/settings"`) { t.Error("scoped admin's sidebar should not link to Server Settings") } - if strings.Contains(body, `href="/pymta-manager/letsencrypt"`) { + if strings.Contains(body, `href="/smtp-server/letsencrypt"`) { t.Error("scoped admin's sidebar should not link to Let's Encrypt") } } diff --git a/internal/webui/settings_test.go b/internal/webui/settings_test.go index e755eee..953e433 100644 --- a/internal/webui/settings_test.go +++ b/internal/webui/settings_test.go @@ -35,7 +35,7 @@ func TestSettingsUpdateSavesUppercaseDefinedKeys(t *testing.T) { cookie := loginSession(t, app) form := url.Values{"Server.smtp_port": {"2525"}} - req := httptest.NewRequest(http.MethodPost, "/pymta-manager/settings_update", strings.NewReader(form.Encode())) + req := httptest.NewRequest(http.MethodPost, "/smtp-server/settings_update", strings.NewReader(form.Encode())) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.AddCookie(cookie) rec := httptest.NewRecorder() diff --git a/internal/webui/templates/account.html b/internal/webui/templates/account.html index a656472..3ec45ee 100644 --- a/internal/webui/templates/account.html +++ b/internal/webui/templates/account.html @@ -10,7 +10,7 @@
Username: {{.user.Username}}
Add an admin and assign them the domains they should manage
- Add Your First Admin + Add Your First Admin