package webui import ( "net/http" "strconv" "strings" "github.com/pquerna/otp/totp" "mailgoserver/internal/db" ) // mailboxPendingMFACookieName mirrors pendingMFACookieName for the mailbox portal — // kept fully separate so an unfinished mailbox login can never be confused with (or // promoted into) an admin session, and vice versa. const mailboxPendingMFACookieName = "mailgoserver_mailbox_pending_mfa" func setMailboxPendingMFACookie(w http.ResponseWriter, mailboxID string) { http.SetCookie(w, &http.Cookie{ Name: mailboxPendingMFACookieName, Value: mailboxID, Path: "/", HttpOnly: true, SameSite: http.SameSiteLaxMode, MaxAge: 10 * 60, }) } func clearMailboxPendingMFACookie(w http.ResponseWriter) { http.SetCookie(w, &http.Cookie{Name: mailboxPendingMFACookieName, Value: "", Path: "/", MaxAge: -1}) } func pendingMailboxMFAID(r *http.Request) int64 { c, err := r.Cookie(mailboxPendingMFACookieName) if err != nil { return 0 } return int64(atoi(c.Value)) } func (a *App) webmailLoginForm(w http.ResponseWriter, r *http.Request) { if sess, mbox, _ := a.currentMailboxSession(r); sess != nil && mbox != nil { http.Redirect(w, r, MailboxPrefix+"/", http.StatusFound) return } a.render(w, r, "webmail_login.html", M{}) } // webmailLoginSubmit checks email+password against the mailbox's own portal // password (never an app password — that's for IMAP/SMTP clients only). func (a *App) webmailLoginSubmit(w http.ResponseWriter, r *http.Request) { email := strings.TrimSpace(r.FormValue("email")) password := r.FormValue("password") fail := func(msg string) { a.render(w, r, "webmail_login.html", M{"error": msg, "email": email}) } mbox, err := a.DB.GetMailboxByEmail(email) if err != nil { a.Logger.Error("webmail login lookup: %v", err) fail("Something went wrong. Try again.") return } if mbox == nil || !db.CheckPassword(password, mbox.PasswordHash) { _ = a.DB.LogAuthAttempt("webmail_login", email, requestIP(r), false, "Incorrect email or password") fail("Incorrect email or password.") return } needsMFA := mbox.TOTPEnabled if !needsMFA { if n, _ := a.DB.CountMailboxWebAuthnCredentials(mbox.ID); n > 0 { needsMFA = true } } if !needsMFA { token, err := a.DB.CreateMailboxSession(mbox.ID, true, sessionTTL) if err != nil { fail("Something went wrong. Try again.") return } _ = a.DB.LogAuthAttempt("webmail_login", email, requestIP(r), true, "Login successful") setMailboxSessionCookie(w, token, r.TLS != nil) http.Redirect(w, r, MailboxPrefix+"/", http.StatusFound) return } setMailboxPendingMFACookie(w, strconv.FormatInt(mbox.ID, 10)) http.Redirect(w, r, MailboxPrefix+"/login/mfa", http.StatusFound) } func (a *App) webmailMFAForm(w http.ResponseWriter, r *http.Request) { mailboxID := pendingMailboxMFAID(r) if mailboxID == 0 { http.Redirect(w, r, MailboxPrefix+"/login", http.StatusFound) return } mbox, _ := a.DB.GetMailboxByID(mailboxID) if mbox == nil { clearMailboxPendingMFACookie(w) http.Redirect(w, r, MailboxPrefix+"/login", http.StatusFound) return } hasPasskeys, _ := a.DB.CountMailboxWebAuthnCredentials(mailboxID) a.render(w, r, "webmail_login_mfa.html", M{"totp_enabled": mbox.TOTPEnabled, "has_passkeys": hasPasskeys > 0}) } func (a *App) webmailMFASubmit(w http.ResponseWriter, r *http.Request) { mailboxID := pendingMailboxMFAID(r) if mailboxID == 0 { http.Redirect(w, r, MailboxPrefix+"/login", http.StatusFound) return } mbox, err := a.DB.GetMailboxByID(mailboxID) if err != nil || mbox == nil { clearMailboxPendingMFACookie(w) http.Redirect(w, r, MailboxPrefix+"/login", http.StatusFound) return } code := strings.TrimSpace(r.FormValue("code")) if !mbox.TOTPEnabled || !totp.Validate(code, mbox.TOTPSecret) { _ = a.DB.LogAuthAttempt("webmail_login", mbox.Email, requestIP(r), false, "Invalid MFA code") hasPasskeys, _ := a.DB.CountMailboxWebAuthnCredentials(mailboxID) a.render(w, r, "webmail_login_mfa.html", M{"totp_enabled": mbox.TOTPEnabled, "has_passkeys": hasPasskeys > 0, "error": "Invalid code."}) return } token, err := a.DB.CreateMailboxSession(mbox.ID, true, sessionTTL) if err != nil { a.Logger.Error("create mailbox session: %v", err) http.Redirect(w, r, MailboxPrefix+"/login", http.StatusFound) return } _ = a.DB.LogAuthAttempt("webmail_login", mbox.Email, requestIP(r), true, "Login successful (authenticator app)") clearMailboxPendingMFACookie(w) setMailboxSessionCookie(w, token, r.TLS != nil) http.Redirect(w, r, MailboxPrefix+"/", http.StatusFound) } func (a *App) webmailLogout(w http.ResponseWriter, r *http.Request) { if c, err := r.Cookie(mailboxSessionCookieName); err == nil { _ = a.DB.DeleteMailboxSession(c.Value) } clearMailboxSessionCookie(w) http.Redirect(w, r, MailboxPrefix+"/login", http.StatusFound) }