Files
mailgoserver/internal/jmap/identity.go
T
2026-08-22 06:45:05 +01:00

191 lines
6.4 KiB
Go

package jmap
import (
"encoding/json"
"strconv"
"mailgoserver/internal/db"
)
// jmapIdentity is RFC 8621 §6.1's Identity object.
type jmapIdentity struct {
ID string `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
TextSignature string `json:"textSignature"`
HTMLSignature string `json:"htmlSignature"`
MayDelete bool `json:"mayDelete"`
}
const primaryIdentityID = "primary"
// identityRow pairs an Identity id with the address it represents — "primary" for the
// mailbox's own address, "alias-<aliasID>" for each active send-as alias
// (esrv_mailbox_aliases.can_send_as) — see identityGet's doc comment for why
// aliases (not S/MIME/PGP identities) are the source here.
type identityRow struct {
id, email string
}
func identityRows(b *Backend, mbox *db.Mailbox) ([]identityRow, error) {
out := []identityRow{{id: primaryIdentityID, email: mbox.Email}}
aliases, err := b.DB.ListAliasesForMailbox(mbox.ID)
if err != nil {
return nil, err
}
for _, a := range aliases {
if a.IsActive && a.CanSendAs {
out = append(out, identityRow{id: "alias-" + strconv.FormatInt(a.ID, 10), email: a.Email})
}
}
return out, nil
}
func buildIdentity(b *Backend, mbox *db.Mailbox, row identityRow) jmapIdentity {
id := jmapIdentity{ID: row.id, Name: mbox.Email, Email: row.email, MayDelete: false}
sig, err := b.DB.GetDefaultSignature(mbox.ID, false, row.email)
if err == nil && sig != nil {
id.HTMLSignature = sig.ContentHTML
}
return id
}
type identityGetArgs struct {
IDs *[]string `json:"ids"`
}
type identityGetResult struct {
AccountID string `json:"accountId"`
State string `json:"state"`
List []jmapIdentity `json:"list"`
NotFound []string `json:"notFound"`
}
// identityGet is Identity/get (RFC 8621 §6.2). Identities are synthesized from this
// mailbox's own address plus its active send-as aliases (esrv_mailbox_aliases) — not
// from the S/MIME/PGP identity tables, which are signing/encryption key material, a
// different concept from JMAP's "an address + display name + signature you can send
// from" Identity object. State is just the mailbox's own aliases/signatures being
// static within a session in practice; a fixed "1" is sufficient since neither
// changes via any method this server implements.
func identityGet(b *Backend, mbox *db.Mailbox, rawArgs json.RawMessage) (any, *methodError) {
var args identityGetArgs
if err := json.Unmarshal(rawArgs, &args); err != nil {
return nil, &methodError{Type: "invalidArguments", Description: err.Error()}
}
rows, err := identityRows(b, mbox)
if err != nil {
return nil, &methodError{Type: "serverFail", Description: err.Error()}
}
result := identityGetResult{AccountID: strconv.FormatInt(mbox.ID, 10), State: "1", List: []jmapIdentity{}, NotFound: []string{}}
if args.IDs == nil {
for _, row := range rows {
result.List = append(result.List, buildIdentity(b, mbox, row))
}
return result, nil
}
byID := make(map[string]identityRow, len(rows))
for _, row := range rows {
byID[row.id] = row
}
for _, id := range *args.IDs {
row, ok := byID[id]
if !ok {
result.NotFound = append(result.NotFound, id)
continue
}
result.List = append(result.List, buildIdentity(b, mbox, row))
}
return result, nil
}
type identitySetArgs struct {
Update map[string]json.RawMessage `json:"update"`
}
type identitySetResult struct {
AccountID string `json:"accountId"`
OldState string `json:"oldState"`
NewState string `json:"newState"`
Updated map[string]any `json:"updated"`
NotCreated map[string]*methodError `json:"notCreated"`
NotUpdated map[string]*methodError `json:"notUpdated"`
}
// identitySet is Identity/set (RFC 8621 §6.3) — limited to updating htmlSignature on
// an existing identity (routed to db.SetDefaultSignature/db.SetSignatureAliasDefault,
// creating a signature row via db.CreateSignature if none exists yet). No create/
// destroy: the underlying send-as grant stays admin-controlled via
// esrv_mailbox_aliases.can_send_as, unchanged by this method — an Identity here isn't
// an independent object, it's a view onto that grant plus a signature.
func identitySet(b *Backend, mbox *db.Mailbox, rawArgs json.RawMessage) (any, *methodError) {
var args identitySetArgs
if err := json.Unmarshal(rawArgs, &args); err != nil {
return nil, &methodError{Type: "invalidArguments", Description: err.Error()}
}
rows, err := identityRows(b, mbox)
if err != nil {
return nil, &methodError{Type: "serverFail", Description: err.Error()}
}
byID := make(map[string]identityRow, len(rows))
for _, row := range rows {
byID[row.id] = row
}
result := identitySetResult{
AccountID: strconv.FormatInt(mbox.ID, 10), OldState: "1", NewState: "1",
Updated: map[string]any{}, NotCreated: map[string]*methodError{}, NotUpdated: map[string]*methodError{},
}
for idStr, rawPatch := range args.Update {
row, ok := byID[idStr]
if !ok {
result.NotUpdated[idStr] = &methodError{Type: "notFound"}
continue
}
var patch map[string]json.RawMessage
if err := json.Unmarshal(rawPatch, &patch); err != nil {
result.NotUpdated[idStr] = &methodError{Type: "invalidPatch", Description: err.Error()}
continue
}
htmlRaw, ok := patch["htmlSignature"]
if !ok {
if len(patch) == 0 {
result.Updated[idStr] = nil
continue
}
result.NotUpdated[idStr] = &methodError{Type: "invalidProperties", Description: "only htmlSignature updates are supported"}
continue
}
var html string
if err := json.Unmarshal(htmlRaw, &html); err != nil {
result.NotUpdated[idStr] = &methodError{Type: "invalidPatch", Description: err.Error()}
continue
}
if err := setIdentitySignature(b, mbox, row, html); err != nil {
result.NotUpdated[idStr] = &methodError{Type: "serverFail", Description: err.Error()}
continue
}
result.Updated[idStr] = nil
}
return result, nil
}
func setIdentitySignature(b *Backend, mbox *db.Mailbox, row identityRow, html string) error {
sig, err := b.DB.GetDefaultSignature(mbox.ID, false, row.email)
if err != nil {
return err
}
if sig != nil {
return b.DB.UpdateSignature(mbox.ID, sig.ID, sig.Name, html)
}
name := "JMAP signature (" + row.email + ")"
newID, err := b.DB.CreateSignature(mbox.ID, name, html)
if err != nil {
return err
}
if row.id == primaryIdentityID {
return b.DB.SetDefaultSignature(mbox.ID, newID, false)
}
return b.DB.SetSignatureAliasDefault(mbox.ID, newID, row.email, false)
}