jmap: fix Email/set

This commit is contained in:
Pascal Bleser
2025-10-09 15:08:12 +02:00
parent a3c6cea180
commit 933688db83
4 changed files with 74 additions and 27 deletions

View File

@@ -522,8 +522,8 @@ func (j *Client) ImportEmail(accountId string, session *Session, ctx context.Con
}
type CreatedEmail struct {
Email Email `json:"email"`
State State `json:"state"`
Email *Email `json:"email"`
State State `json:"state"`
}
func (j *Client) CreateEmail(accountId string, email EmailCreate, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string) (CreatedEmail, SessionState, Language, Error) {
@@ -572,8 +572,8 @@ func (j *Client) CreateEmail(accountId string, email EmailCreate, session *Sessi
}
type UpdatedEmails struct {
Updated map[string]Email `json:"email"`
State State `json:"state"`
Updated map[string]*Email `json:"email"`
State State `json:"state"`
}
// The Email/set method encompasses:
@@ -613,7 +613,8 @@ func (j *Client) UpdateEmails(accountId string, updates map[string]EmailUpdate,
}
type DeletedEmails struct {
State State `json:"state"`
State State `json:"state"`
NotDestroyed map[string]SetError `json:"notDestroyed"`
}
func (j *Client) DeleteEmails(accountId string, destroy []string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string) (DeletedEmails, SessionState, Language, Error) {
@@ -633,11 +634,10 @@ func (j *Client) DeleteEmails(accountId string, destroy []string, session *Sessi
if err != nil {
return DeletedEmails{}, err
}
if len(setResponse.NotDestroyed) != len(destroy) {
// error occured
// TODO(pbleser-oc) handle submission errors
}
return DeletedEmails{State: setResponse.NewState}, nil
return DeletedEmails{
State: setResponse.NewState,
NotDestroyed: setResponse.NotDestroyed,
}, nil
})
}

View File

@@ -144,7 +144,7 @@ func (h *HttpJmapClient) GetSession(sessionUrl *url.URL, username string, logger
}
if res.StatusCode < 200 || res.StatusCode > 299 {
h.listener.OnFailedRequestWithStatus(endpoint, res.StatusCode)
logger.Error().Str(logHttpStatus, res.Status).Int(logHttpStatusCode, res.StatusCode).Msg("HTTP response status code is not 200")
logger.Error().Str(logHttpStatus, log.SafeString(res.Status)).Int(logHttpStatusCode, res.StatusCode).Msg("HTTP response status code is not 200")
return SessionResponse{}, SimpleError{code: JmapErrorServerResponse, err: fmt.Errorf("JMAP API response status is %v", res.Status)}
}
h.listener.OnSuccessfulRequest(endpoint, res.StatusCode)
@@ -168,7 +168,7 @@ func (h *HttpJmapClient) GetSession(sessionUrl *url.URL, username string, logger
var data SessionResponse
err = json.Unmarshal(body, &data)
if err != nil {
logger.Error().Str(logHttpUrl, sessionUrlStr).Err(err).Msg("failed to decode JSON payload from .well-known/jmap response")
logger.Error().Str(logHttpUrl, log.SafeString(sessionUrlStr)).Err(err).Msg("failed to decode JSON payload from .well-known/jmap response")
h.listener.OnResponseBodyUnmarshallingError(endpoint, err)
return SessionResponse{}, SimpleError{code: JmapErrorDecodingResponseBody, err: err}
}
@@ -212,7 +212,7 @@ func (h *HttpJmapClient) Command(ctx context.Context, logger *log.Logger, sessio
language := Language(res.Header.Get("Content-Language"))
if res.StatusCode < 200 || res.StatusCode > 299 {
h.listener.OnFailedRequestWithStatus(endpoint, res.StatusCode)
logger.Error().Str(logEndpoint, endpoint).Str(logHttpStatus, res.Status).Msg("HTTP response status code is not 2xx")
logger.Error().Str(logEndpoint, endpoint).Str(logHttpStatus, log.SafeString(res.Status)).Msg("HTTP response status code is not 2xx")
return nil, language, SimpleError{code: JmapErrorServerResponse, err: err}
}
if res.Body != nil {
@@ -259,7 +259,7 @@ func (h *HttpJmapClient) UploadBinary(ctx context.Context, logger *log.Logger, s
language := Language(res.Header.Get("Content-Language"))
if res.StatusCode < 200 || res.StatusCode > 299 {
h.listener.OnFailedRequestWithStatus(endpoint, res.StatusCode)
logger.Error().Str(logHttpStatus, res.Status).Int(logHttpStatusCode, res.StatusCode).Msg("HTTP response status code is not 2xx")
logger.Error().Str(logHttpStatus, log.SafeString(res.Status)).Int(logHttpStatusCode, res.StatusCode).Msg("HTTP response status code is not 2xx")
return UploadedBlob{}, language, SimpleError{code: JmapErrorServerResponse, err: err}
}
if res.Body != nil {
@@ -282,7 +282,7 @@ func (h *HttpJmapClient) UploadBinary(ctx context.Context, logger *log.Logger, s
var result UploadedBlob
err = json.Unmarshal(responseBody, &result)
if err != nil {
logger.Error().Str(logHttpUrl, uploadUrl).Err(err).Msg("failed to decode JSON payload from the upload response")
logger.Error().Str(logHttpUrl, log.SafeString(uploadUrl)).Err(err).Msg("failed to decode JSON payload from the upload response")
h.listener.OnResponseBodyUnmarshallingError(endpoint, err)
return UploadedBlob{}, language, SimpleError{code: JmapErrorDecodingResponseBody, err: err}
}
@@ -316,7 +316,7 @@ func (h *HttpJmapClient) DownloadBinary(ctx context.Context, logger *log.Logger,
}
if res.StatusCode < 200 || res.StatusCode > 299 {
h.listener.OnFailedRequestWithStatus(endpoint, res.StatusCode)
logger.Error().Str(logHttpStatus, res.Status).Int(logHttpStatusCode, res.StatusCode).Msg("HTTP response status code is not 2xx")
logger.Error().Str(logHttpStatus, log.SafeString(res.Status)).Int(logHttpStatusCode, res.StatusCode).Msg("HTTP response status code is not 2xx")
return nil, language, SimpleError{code: JmapErrorServerResponse, err: err}
}
h.listener.OnSuccessfulRequest(endpoint, res.StatusCode)

View File

@@ -152,7 +152,7 @@ type senderGenerator struct {
senders []sender
}
func newSenderGenerator(domain string, numSenders int) senderGenerator {
func newSenderGenerator(numSenders int) senderGenerator {
senders := make([]sender, numSenders)
for i := range numSenders {
person := gofakeit.Person()
@@ -494,7 +494,7 @@ func (s *StalwartTest) fill(folder string, count int) ([]filledMail, int, error)
bccName := "HR"
bccAddress := fmt.Sprintf("corporate@%s", domain)
sg := newSenderGenerator(domain, senders)
sg := newSenderGenerator(senders)
thread := 0
mails := make([]filledMail, count)
for i := 0; i < count; thread++ {

View File

@@ -2067,14 +2067,14 @@ type Email struct {
// (as referenced by the blobId, i.e., the number of octets in the file the user would download).
//
// [RFC5322]: https://www.rfc-editor.org/rfc/rfc5322.html
Size int `json:"size"`
Size int `json:"size,omitzero"`
// The date the Email was received by the message store.
//
// This is the internal date in IMAP [RFC3501].
//
// [RFC3501]: https://www.rfc-editor.org/rfc/rfc3501.html
ReceivedAt time.Time `json:"receivedAt,omitempty"`
ReceivedAt time.Time `json:"receivedAt,omitzero"`
// This is a list of all header fields [RFC5322], in the same order they appear in the message.
//
@@ -2116,7 +2116,7 @@ type Email struct {
Subject string `json:"subject,omitempty"`
// The value is identical to the value of header:Date:asDate.
SentAt time.Time `json:"sentAt,omitempty"`
SentAt time.Time `json:"sentAt,omitzero"`
// This is the full MIME structure of the message body, without recursing into message/rfc822 or message/global parts.
//
@@ -2817,10 +2817,56 @@ type EmailCreate struct {
type EmailUpdate map[string]any
type EmailSetCommand struct {
AccountId string `json:"accountId"`
Create map[string]EmailCreate `json:"create,omitempty"`
Update map[string]EmailUpdate `json:"update,omitempty"`
Destroy []string `json:"destroy,omitempty"`
// The id of the account to use.
AccountId string `json:"accountId"`
// This is a state string as returned by the `Email/get` method.
//
// If supplied, the string must match the current state; otherwise, the method will be aborted and a
// `stateMismatch` error returned.
//
// If null, any changes will be applied to the current state.
IfInState string `json:"ifInState,omitempty"`
// A map of a creation id (a temporary id set by the client) to Email objects,
// or null if no objects are to be created.
//
// The Email object type definition may define default values for properties.
//
// Any such property may be omitted by the client.
//
// The client MUST omit any properties that may only be set by the server.
Create map[string]EmailCreate `json:"create,omitempty"`
// A map of an id to a `Patch` object to apply to the current Email object with that id,
// or null if no objects are to be updated.
//
// A `PatchObject` is of type `String[*]` and represents an unordered set of patches.
//
// The keys are a path in JSON Pointer Format [@!RFC6901], with an implicit leading `/` (i.e., prefix each key
// with `/` before applying the JSON Pointer evaluation algorithm).
//
// All paths MUST also conform to the following restrictions; if there is any violation, the update
// MUST be rejected with an `invalidPatch` error:
// !- The pointer MUST NOT reference inside an array (i.e., you MUST NOT insert/delete from an array; the array MUST be replaced in its entirety instead).
// !- All parts prior to the last (i.e., the value after the final slash) MUST already exist on the object being patched.
// !- There MUST NOT be two patches in the `PatchObject` where the pointer of one is the prefix of the pointer of the other, e.g., `"alerts/1/offset"` and `"alerts"`.
//
// The value associated with each pointer determines how to apply that patch:
// !- If null, set to the default value if specified for this property; otherwise, remove the property from the patched object. If the key is not present in the parent, this a no-op.
// ç- Anything else: The value to set for this property (this may be a replacement or addition to the object being patched).
//
// Any server-set properties MAY be included in the patch if their value is identical to the current server value
// (before applying the patches to the object). Otherwise, the update MUST be rejected with an `invalidProperties` `SetError`.
//
// This patch definition is designed such that an entire Email object is also a valid `PatchObject`.
//
// The client may choose to optimise network usage by just sending the diff or may send the whole object; the server
// processes it the same either way.
Update map[string]EmailUpdate `json:"update,omitempty"`
// A list of ids for Email objects to permanently delete, or null if no objects are to be destroyed.
Destroy []string `json:"destroy,omitempty"`
}
type EmailSetResponse struct {
@@ -2842,7 +2888,7 @@ type EmailSetResponse struct {
// that were omitted by the client and thus set to a default by the server.
//
// This argument is null if no Email objects were successfully created.
Created map[string]Email `json:"created,omitempty"`
Created map[string]*Email `json:"created,omitempty"`
// The keys in this map are the ids of all Emails that were successfully updated.
//
@@ -2852,7 +2898,7 @@ type EmailSetResponse struct {
// This lets the client know of any changes to server-set or computed properties.
//
// This argument is null if no Email objects were successfully updated.
Updated map[string]Email `json:"updated,omitempty"`
Updated map[string]*Email `json:"updated,omitempty"`
// A list of Email ids for records that were successfully destroyed, or null if none.
Destroyed []string `json:"destroyed,omitempty"`
@@ -4609,6 +4655,7 @@ var CommandResponseTypeMap = map[Command]func() any{
CommandEmailQuery: func() any { return EmailQueryResponse{} },
CommandEmailChanges: func() any { return EmailChangesResponse{} },
CommandEmailGet: func() any { return EmailGetResponse{} },
CommandEmailSet: func() any { return EmailSetResponse{} },
CommandEmailSubmissionGet: func() any { return EmailSubmissionGetResponse{} },
CommandEmailSubmissionSet: func() any { return EmailSubmissionSetResponse{} },
CommandThreadGet: func() any { return ThreadGetResponse{} },