package webui import ( "io" "net/http" "strings" "mailgoserver/internal/smime" ) // webmailCertsPage shows both of a mailbox owner's certificate/key types: S/MIME // certificates (generate/import/remove/download, used for signing, stored plain) // and PGP keys (used for encryption — see webmail_pgp.go — each protected by its // own passphrase), plus their collected contact certificates/keys for each // protocol. func (a *App) webmailCertsPage(w http.ResponseWriter, r *http.Request) { mbox := mailboxFromContext(r) token := sessionToken(r) identities, err := a.DB.ListSMIMEIdentities(mbox.ID) if err != nil { setFlash(w, "error", "Error loading your S/MIME certificates") } contacts, err := a.DB.ListSMIMEContacts(mbox.ID) if err != nil { setFlash(w, "error", "Error loading S/MIME contact certificates") } pgpIdentities, err := a.DB.ListPGPIdentities(mbox.ID) if err != nil { setFlash(w, "error", "Error loading your PGP keys") } pgpContacts, err := a.DB.ListPGPContacts(mbox.ID) if err != nil { setFlash(w, "error", "Error loading PGP contact keys") } pgpUnlocked := make(map[int64]bool, len(pgpIdentities)) for _, id := range pgpIdentities { if _, ok := a.pgpKeys.get(token, id.ID); ok { pgpUnlocked[id.ID] = true } } a.render(w, r, "webmail_certs.html", M{ "mailbox": mbox, "identities": identities, "contacts": contacts, "pgp_identities": pgpIdentities, "pgp_unlocked": pgpUnlocked, "pgp_contacts": pgpContacts, "flashes": popFlashes(w, r), }) } // storeIdentity adds a new identity with certPEM/keyPEM stored as-is — no // passphrase wrapping (see the schema comment on esrv_mailbox_smime_identities). func (a *App) storeIdentity(mailboxID int64, certPEM, keyPEM []byte) error { cert, err := smime.ParseCertPEM(certPEM) if err != nil { return err } _, err = a.DB.CreateSMIMEIdentity(mailboxID, string(certPEM), string(keyPEM), cert.NotAfter) return err } func (a *App) webmailSMIMEGenerate(w http.ResponseWriter, r *http.Request) { mbox := mailboxFromContext(r) certPEM, keyPEM, err := smime.GenerateSelfSigned(mbox.Email, smime.DefaultValidity) if err == nil { err = a.storeIdentity(mbox.ID, certPEM, keyPEM) } if err != nil { a.Logger.Error("smime generate for mailbox %d: %v", mbox.ID, err) setFlash(w, "error", "Error generating certificate") } else { setFlash(w, "success", "S/MIME certificate generated") } http.Redirect(w, r, MailboxPrefix+"/certs", http.StatusFound) } func (a *App) webmailSMIMEImport(w http.ResponseWriter, r *http.Request) { mbox := mailboxFromContext(r) if err := r.ParseMultipartForm(1 << 20); err != nil { setFlash(w, "error", "Error reading upload") http.Redirect(w, r, MailboxPrefix+"/certs", http.StatusFound) return } file, _, err := r.FormFile("p12_file") if err != nil { setFlash(w, "error", "Please choose a .p12/.pfx file") http.Redirect(w, r, MailboxPrefix+"/certs", http.StatusFound) return } defer file.Close() data, err := io.ReadAll(file) if err == nil { var certPEM, keyPEM []byte // p12_password unlocks the uploaded .p12 bundle itself, a one-time-use secret // distinct from anything stored afterward — the key is kept plain from here on. certPEM, keyPEM, err = smime.ImportPKCS12(data, r.FormValue("p12_password")) if err == nil { err = a.storeIdentity(mbox.ID, certPEM, keyPEM) } } if err != nil { setFlash(w, "error", "Error importing certificate: "+err.Error()) } else { setFlash(w, "success", "S/MIME certificate imported") } http.Redirect(w, r, MailboxPrefix+"/certs", http.StatusFound) } func (a *App) webmailSMIMERemoveIdentity(w http.ResponseWriter, r *http.Request) { mbox := mailboxFromContext(r) identityID := int64(atoi(r.PathValue("identity_id"))) if err := a.DB.DeleteSMIMEIdentity(mbox.ID, identityID); err != nil { setFlash(w, "error", "Error removing identity") } else { setFlash(w, "success", "S/MIME identity removed") } http.Redirect(w, r, MailboxPrefix+"/certs", http.StatusFound) } func (a *App) webmailSMIMEDownloadCert(w http.ResponseWriter, r *http.Request) { mbox := mailboxFromContext(r) identityID := int64(atoi(r.PathValue("identity_id"))) identity, err := a.DB.GetSMIMEIdentity(mbox.ID, identityID) if err != nil || identity == nil { http.NotFound(w, r) return } w.Header().Set("Content-Type", "application/x-x509-user-cert") w.Header().Set("Content-Disposition", `attachment; filename="`+mbox.Email+`.crt"`) w.Write([]byte(identity.CertPEM)) } func (a *App) webmailSMIMEAddContact(w http.ResponseWriter, r *http.Request) { mbox := mailboxFromContext(r) if err := r.ParseMultipartForm(1 << 20); err != nil { setFlash(w, "error", "Error reading upload") http.Redirect(w, r, MailboxPrefix+"/certs", http.StatusFound) return } email := strings.TrimSpace(r.FormValue("email")) file, _, err := r.FormFile("cert_file") if email == "" || err != nil { setFlash(w, "error", "Please provide an email and a certificate file") http.Redirect(w, r, MailboxPrefix+"/certs", http.StatusFound) return } defer file.Close() data, err := io.ReadAll(file) if err == nil { _, err = smime.ParseCertPEM(data) } if err != nil { setFlash(w, "error", "That doesn't look like a valid certificate file") http.Redirect(w, r, MailboxPrefix+"/certs", http.StatusFound) return } if err := a.DB.UpsertSMIMEContact(mbox.ID, email, string(data)); err != nil { setFlash(w, "error", "Error saving contact certificate") } else { setFlash(w, "success", "Contact certificate added") } http.Redirect(w, r, MailboxPrefix+"/certs", http.StatusFound) } func (a *App) webmailSMIMERemoveContact(w http.ResponseWriter, r *http.Request) { mbox := mailboxFromContext(r) contactID := int64(atoi(r.PathValue("contact_id"))) if err := a.DB.DeleteSMIMEContact(mbox.ID, contactID); err != nil { setFlash(w, "error", "Error removing contact") } else { setFlash(w, "success", "Contact removed") } http.Redirect(w, r, MailboxPrefix+"/certs", http.StatusFound) }