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-" 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) }