191 lines
6.4 KiB
Go
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)
|
|
}
|