179 lines
5.9 KiB
Go
179 lines
5.9 KiB
Go
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)
|
||
|
|
}
|