add JMAP functionality
This commit is contained in:
@@ -0,0 +1,190 @@
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user