diff --git a/pkg/jmap/email.go b/pkg/jmap/email.go deleted file mode 100644 index 0c230dfbbc..0000000000 --- a/pkg/jmap/email.go +++ /dev/null @@ -1,10 +0,0 @@ -package jmap - -import "time" - -type Email struct { - From string - Subject string - HasAttachments bool - Received time.Time -} diff --git a/pkg/jmap/http_jmap_api_client.go b/pkg/jmap/http_jmap_api_client.go index 050b2feb85..c555d504ae 100644 --- a/pkg/jmap/http_jmap_api_client.go +++ b/pkg/jmap/http_jmap_api_client.go @@ -58,24 +58,25 @@ func (h *HttpJmapApiClient) authWithUsername(logger *log.Logger, username string return nil } -func (h *HttpJmapApiClient) GetWellKnown(username string, logger *log.Logger) (WellKnownJmap, error) { +func (h *HttpJmapApiClient) GetWellKnown(username string, logger *log.Logger) (WellKnownResponse, error) { wellKnownUrl := h.baseurl + "/.well-known/jmap" req, err := http.NewRequest(http.MethodGet, wellKnownUrl, nil) if err != nil { logger.Error().Err(err).Msgf("failed to create GET request for %v", wellKnownUrl) - return WellKnownJmap{}, err + return WellKnownResponse{}, err } h.authWithUsername(logger, username, req) + req.Header.Add("Cache-Control", "no-cache, no-store, must-revalidate") // spec recommendation res, err := h.client.Do(req) if err != nil { logger.Error().Err(err).Msgf("failed to perform GET %v", wellKnownUrl) - return WellKnownJmap{}, err + return WellKnownResponse{}, err } if res.StatusCode != 200 { logger.Error().Str("status", res.Status).Msg("HTTP response status code is not 200") - return WellKnownJmap{}, fmt.Errorf("HTTP response status is %v", res.Status) + return WellKnownResponse{}, fmt.Errorf("HTTP response status is %v", res.Status) } if res.Body != nil { defer func(Body io.ReadCloser) { @@ -89,14 +90,14 @@ func (h *HttpJmapApiClient) GetWellKnown(username string, logger *log.Logger) (W body, err := io.ReadAll(res.Body) if err != nil { logger.Error().Err(err).Msg("failed to read response body") - return WellKnownJmap{}, err + return WellKnownResponse{}, err } - var data WellKnownJmap + var data WellKnownResponse err = json.Unmarshal(body, &data) if err != nil { logger.Error().Str("url", wellKnownUrl).Err(err).Msg("failed to decode JSON payload from .well-known/jmap response") - return WellKnownJmap{}, err + return WellKnownResponse{}, err } return data, nil diff --git a/pkg/jmap/jmap.go b/pkg/jmap/jmap.go index 01206a121c..167e8880c8 100644 --- a/pkg/jmap/jmap.go +++ b/pkg/jmap/jmap.go @@ -8,88 +8,172 @@ import ( "github.com/opencloud-eu/opencloud/pkg/log" ) -const ( - JmapCore = "urn:ietf:params:jmap:core" - JmapMail = "urn:ietf:params:jmap:mail" -) - -type JmapClient struct { - wellKnown JmapWellKnownClient - api JmapApiClient +type Client struct { + wellKnown WellKnownClient + api ApiClient } -func NewJmapClient(wellKnown JmapWellKnownClient, api JmapApiClient) JmapClient { - return JmapClient{ +func NewClient(wellKnown WellKnownClient, api ApiClient) Client { + return Client{ wellKnown: wellKnown, api: api, } } -type JmapContext struct { +type Session struct { + Username string AccountId string JmapUrl string } -func NewJmapContext(wellKnown WellKnownJmap) (JmapContext, error) { - // TODO validate - return JmapContext{ - AccountId: wellKnown.PrimaryAccounts[JmapMail], - JmapUrl: wellKnown.ApiUrl, - }, nil -} - -func (j *JmapClient) FetchJmapContext(username string, logger *log.Logger) (JmapContext, error) { - wk, err := j.wellKnown.GetWellKnown(username, logger) - if err != nil { - return JmapContext{}, err - } - return NewJmapContext(wk) -} - type ContextKey int const ( ContextAccountId ContextKey = iota ContextOperationId + ContextUsername ) -func (j *JmapClient) validate(jmapContext JmapContext) error { - if jmapContext.AccountId == "" { - return fmt.Errorf("AccountId not set") - } - return nil +func (s Session) DecorateSession(ctx context.Context) context.Context { + ctx = context.WithValue(ctx, ContextUsername, s.Username) + ctx = context.WithValue(ctx, ContextAccountId, s.AccountId) + return ctx } -func (j *JmapClient) GetMailboxes(jc JmapContext, ctx context.Context, logger *log.Logger) (JmapFolders, error) { - if err := j.validate(jc); err != nil { - return JmapFolders{}, err - } +const ( + logUsername = "username" + logAccountId = "account-id" +) - logger.Info().Str("command", "Mailbox/get").Str("accountId", jc.AccountId).Msg("GetMailboxes") - cmd := simpleCommand("Mailbox/get", map[string]any{"accountId": jc.AccountId}) +func (s Session) DecorateLogger(l log.Logger) log.Logger { + return log.Logger{ + Logger: l.With().Str(logUsername, s.Username).Str(logAccountId, s.AccountId).Logger(), + } +} + +func NewSession(wellKnownResponse WellKnownResponse) (Session, error) { + username := wellKnownResponse.Username + if username == "" { + return Session{}, fmt.Errorf("well-known response has no username") + } + accountId := wellKnownResponse.PrimaryAccounts[JmapMail] + if accountId == "" { + return Session{}, fmt.Errorf("PrimaryAccounts in well-known response has no entry for %v", JmapMail) + } + apiUrl := wellKnownResponse.ApiUrl + if apiUrl == "" { + return Session{}, fmt.Errorf("well-known response has no API URL") + } + return Session{ + Username: username, + AccountId: accountId, + JmapUrl: apiUrl, + }, nil +} + +func (j *Client) FetchSession(username string, logger *log.Logger) (Session, error) { + wk, err := j.wellKnown.GetWellKnown(username, logger) + if err != nil { + return Session{}, err + } + return NewSession(wk) +} + +func (j *Client) GetMailboxes(session Session, ctx context.Context, logger *log.Logger) (Folders, error) { + logger.Info().Str("command", "Mailbox/get").Str("accountId", session.AccountId).Msg("GetMailboxes") + cmd := simpleCommand("Mailbox/get", map[string]any{"accountId": session.AccountId}) commandCtx := context.WithValue(ctx, ContextOperationId, "GetMailboxes") - return command(j.api, logger, commandCtx, &cmd, func(body *[]byte) (JmapFolders, error) { + return command(j.api, logger, commandCtx, &cmd, func(body *[]byte) (Folders, error) { var data JmapCommandResponse err := json.Unmarshal(*body, &data) if err != nil { logger.Error().Err(err).Msg("failed to deserialize body JSON payload") - var zero JmapFolders + var zero Folders return zero, err } return parseMailboxGetResponse(data) }) } -func (j *JmapClient) EmailQuery(jc JmapContext, ctx context.Context, logger *log.Logger, mailboxId string) (Emails, error) { - if err := j.validate(jc); err != nil { - return Emails{}, err +func (j *Client) GetEmails(session Session, ctx context.Context, logger *log.Logger, mailboxId string, offset int, limit int, fetchBodies bool, maxBodyValueBytes int) (Emails, error) { + cmd := make([][]any, 2) + cmd[0] = []any{ + "Email/query", + map[string]any{ + "accountId": session.AccountId, + "filter": map[string]any{ + "inMailbox": mailboxId, + }, + "sort": []map[string]any{ + { + "isAscending": false, + "property": "receivedAt", + }, + }, + "collapseThreads": true, + "position": offset, + "limit": limit, + "calculateTotal": true, + }, + "0", } + cmd[1] = []any{ + "Email/get", + map[string]any{ + "accountId": session.AccountId, + "fetchAllBodyValues": fetchBodies, + "maxBodyValueBytes": maxBodyValueBytes, + "#ids": map[string]any{ + "name": "Email/query", + "path": "/ids/*", + "resultOf": "0", + }, + }, + "1", + } + commandCtx := context.WithValue(ctx, ContextOperationId, "GetEmails") + logger = &log.Logger{Logger: logger.With().Str("mailboxId", mailboxId).Bool("fetchBodies", fetchBodies).Int("offset", offset).Int("limit", limit).Logger()} + + return command(j.api, logger, commandCtx, &cmd, func(body *[]byte) (Emails, error) { + var data JmapCommandResponse + err := json.Unmarshal(*body, &data) + if err != nil { + logger.Error().Err(err).Msg("failed to unmarshal response payload") + return Emails{}, err + } + first := retrieveResponseMatch(&data, 3, "Email/get", "1") + if first == nil { + return Emails{Emails: []Email{}, State: data.SessionState}, nil + } + if len(first) != 3 { + return Emails{}, fmt.Errorf("wrong Email/get response payload size, expecting a length of 3 but it is %v", len(first)) + } + + payload := first[1].(map[string]any) + list, listExists := payload["list"].([]any) + if !listExists { + return Emails{}, fmt.Errorf("wrong Email/get response payload size, expecting a length of 3 but it is %v", len(first)) + } + + emails := make([]Email, 0, len(list)) + for _, elem := range list { + email, err := mapEmail(elem.(map[string]any), fetchBodies, logger) + if err != nil { + return Emails{}, err + } + emails = append(emails, email) + } + return Emails{Emails: emails, State: data.SessionState}, nil + }) +} + +func (j *Client) EmailThreadsQuery(session Session, ctx context.Context, logger *log.Logger, mailboxId string) (Emails, error) { cmd := make([][]any, 4) cmd[0] = []any{ "Email/query", map[string]any{ - "accountId": jc.AccountId, + "accountId": session.AccountId, "filter": map[string]any{ "inMailbox": mailboxId, }, @@ -109,7 +193,7 @@ func (j *JmapClient) EmailQuery(jc JmapContext, ctx context.Context, logger *log cmd[1] = []any{ "Email/get", map[string]any{ - "accountId": jc.AccountId, + "accountId": session.AccountId, "#ids": map[string]any{ "resultOf": "0", "name": "Email/query", @@ -122,7 +206,7 @@ func (j *JmapClient) EmailQuery(jc JmapContext, ctx context.Context, logger *log cmd[2] = []any{ "Thread/get", map[string]any{ - "accountId": jc.AccountId, + "accountId": session.AccountId, "#ids": map[string]any{ "resultOf": "1", "name": "Email/get", @@ -134,7 +218,7 @@ func (j *JmapClient) EmailQuery(jc JmapContext, ctx context.Context, logger *log cmd[3] = []any{ "Email/get", map[string]any{ - "accountId": jc.AccountId, + "accountId": session.AccountId, "#ids": map[string]any{ "resultOf": "2", "name": "Thread/get", @@ -155,7 +239,7 @@ func (j *JmapClient) EmailQuery(jc JmapContext, ctx context.Context, logger *log "3", } - commandCtx := context.WithValue(ctx, ContextOperationId, "EmailQuery") + commandCtx := context.WithValue(ctx, ContextOperationId, "EmailThreadsQuery") return command(j.api, logger, commandCtx, &cmd, func(body *[]byte) (Emails, error) { var data JmapCommandResponse err := json.Unmarshal(*body, &data) @@ -179,7 +263,7 @@ func (j *JmapClient) EmailQuery(jc JmapContext, ctx context.Context, logger *log emails := make([]Email, 0, len(list)) for _, elem := range list { - email, err := mapEmail(elem.(map[string]any)) + email, err := mapEmail(elem.(map[string]any), false, logger) if err != nil { return Emails{}, err } diff --git a/pkg/jmap/jmap_api_client.go b/pkg/jmap/jmap_api_client.go index 1a054742c3..3dea8937c9 100644 --- a/pkg/jmap/jmap_api_client.go +++ b/pkg/jmap/jmap_api_client.go @@ -6,6 +6,6 @@ import ( "github.com/opencloud-eu/opencloud/pkg/log" ) -type JmapApiClient interface { +type ApiClient interface { Command(ctx context.Context, logger *log.Logger, request map[string]any) ([]byte, error) } diff --git a/pkg/jmap/jmap_model.go b/pkg/jmap/jmap_model.go index 7262889e65..f551963e2d 100644 --- a/pkg/jmap/jmap_model.go +++ b/pkg/jmap/jmap_model.go @@ -1,6 +1,19 @@ package jmap -type WellKnownJmap struct { +import ( + "encoding/json" + "fmt" + "strconv" + "time" +) + +const ( + JmapCore = "urn:ietf:params:jmap:core" + JmapMail = "urn:ietf:params:jmap:mail" +) + +type WellKnownResponse struct { + Username string `json:"username"` ApiUrl string `json:"apiUrl"` PrimaryAccounts map[string]string `json:"primaryAccounts"` } @@ -14,7 +27,7 @@ type JmapFolder struct { TotalThreads int UnreadThreads int } -type JmapFolders struct { +type Folders struct { Folders []JmapFolder state string } @@ -24,7 +37,130 @@ type JmapCommandResponse struct { SessionState string `json:"sessionState"` } +type Email struct { + Id string + MessageId string + BlobId string + ThreadId string + Size int + From string + Subject string + HasAttachments bool + Received time.Time + Preview string + Bodies map[string]string +} + type Emails struct { Emails []Email State string } + +type Command string + +const ( + EmailGet Command = "Email/get" + EmailQuery Command = "Email/query" + ThreadGet Command = "Thread/get" +) + +type Invocation struct { + Command Command + Parameters any + Tag string +} + +func (i *Invocation) MarshalJSON() ([]byte, error) { + arr := []any{string(i.Command), i.Parameters, i.Tag} + return json.Marshal(arr) +} +func strarr(value any) ([]string, error) { + switch v := value.(type) { + case []string: + return v, nil + case int: + return []string{strconv.FormatInt(int64(v), 10)}, nil + case float32: + return []string{strconv.FormatFloat(float64(v), 'f', -1, 32)}, nil + case float64: + return []string{strconv.FormatFloat(v, 'f', -1, 64)}, nil + case string: + return []string{v}, nil + default: + return nil, fmt.Errorf("unsupported string array type") + } +} +func (i *Invocation) UnmarshalJSON(bs []byte) error { + arr := []any{} + json.Unmarshal(bs, &arr) + i.Command = Command(arr[0].(string)) + payload := arr[1].(map[string]any) + switch i.Command { + case EmailQuery: + ids, err := strarr(payload["ids"]) + if err != nil { + return err + } + i.Parameters = EmailQueryResponse{ + AccountId: payload["accountId"].(string), + QueryState: payload["queryState"].(string), + CanCalculateChanges: payload["canCalculateChanges"].(bool), + Position: payload["position"].(int), + Ids: ids, + Total: payload["total"].(int), + } + default: + return &json.UnsupportedTypeError{} + } + i.Tag = arr[2].(string) + return nil +} + +func NewInvocation(command Command, parameters map[string]any, tag string) Invocation { + return Invocation{ + Command: command, + Parameters: parameters, + Tag: tag, + } +} + +type Request struct { + Using []string `json:"using"` + MethodCalls []Invocation `json:"methodCalls"` + CreatedIds map[string]string `json:"createdIds,omitempty"` +} + +func NewRequest(methodCalls ...Invocation) (Request, error) { + return Request{ + Using: []string{JmapCore, JmapMail}, + MethodCalls: methodCalls, + CreatedIds: nil, + }, nil +} + +// TODO: NewRequestWithIds + +type Response struct { + MethodResponses []Invocation `json:"methodResponses"` + CreatedIds map[string]string `json:"createdIds,omitempty"` + SessionState string `json:"sessionState"` +} + +type EmailQueryResponse struct { + AccountId string `json:"accountId"` + QueryState string `json:"queryState"` + CanCalculateChanges bool `json:"canCalculateChanges"` + Position int `json:"position"` + Ids []string `json:"ids"` + Total int `json:"total"` +} +type Thread struct { + ThreadId string `json:"threadId"` + Id string `json:"id"` +} +type EmailGetResponse struct { + AccountId string `json:"accountId"` + State string `json:"state"` + List []Thread `json:"list"` + NotFound []any `json:"notFound"` +} diff --git a/pkg/jmap/jmap_model_test.go b/pkg/jmap/jmap_model_test.go new file mode 100644 index 0000000000..4664279523 --- /dev/null +++ b/pkg/jmap/jmap_model_test.go @@ -0,0 +1,117 @@ +package jmap_test + +import ( + "encoding/json" + "testing" + + "github.com/opencloud-eu/opencloud/pkg/jmap" + "github.com/stretchr/testify/require" +) + +func TestRequestSerialization(t *testing.T) { + require := require.New(t) + + request, err := jmap.NewRequest( + jmap.NewInvocation(jmap.EmailGet, map[string]any{ + "accountId": "j", + "queryState": "aaa", + "ids": []string{"a", "b"}, + "total": 1, + }, "0"), + ) + require.NoError(err) + + require.Len(request.MethodCalls, 1) + require.Equal("0", request.MethodCalls[0].Tag) + + requestAsJson, err := json.Marshal(request) + require.NoError(err) + require.Equal(`{"using":["urn:ietf:params:jmap:core","urn:ietf:params:jmap:mail"],"methodCalls":[["Email/get",{"accountId":"j","ids":["a","b"],"queryState":"aaa","total":1},"0"]]}`, string(requestAsJson)) +} + +const mails2 = `{"methodResponses":[ + ["Email/query",{ + "accountId":"j", + "queryState":"sqcakzewfqdk7oay", + "canCalculateChanges":true, + "position":0, + "ids":["fmaaaabh"], + "total":1 + }, "0"], + ["Email/get", { + "accountId":"j", + "state":"sqcakzewfqdk7oay", + "list":[ + { + "threadId":"bl", + "id":"fmaaaabh" + } + ], + "notFound":[] + }, "1"], + ["Thread/get",{ + "accountId":"j", + "state":"sqcakzewfqdk7oay", + "list":[ + { + "id":"bl", + "emailIds":["fmaaaabh"] + } + ], + "notFound":[] + }, "2"], + ["Email/get",{ + "accountId":"j", + "state":"sqcakzewfqdk7oay", + "list":[ + { + "threadId":"bl", + "mailboxIds":{"a":true}, + "keywords":{}, + "hasAttachment":false, + "from":[ + {"name":"current generally", "email":"current.generally@example.com"} + ], + "subject":"eros auctor proin", + "receivedAt":"2025-04-30T09:47:44Z", + "size":15423, + "preview":"Lorem ipsum dolor sit amet consectetur adipiscing elit sed urna tristique himenaeos eu a mattis laoreet aliquet enim. Magnis est facilisis nibh nisl vitae nisi mauris nostra velit donec erat pellentesque sagittis ligula turpis suscipit ultricies. Morbi ...", + "id":"fmaaaabh" + } + ], + "notFound":[] + }, "3"] + ], "sessionState":"3e25b2a0" + }` + +func TestResponseDeserialization(t *testing.T) { + require := require.New(t) + + var response jmap.Response + err := json.Unmarshal([]byte(mails2), &response) + require.NoError(err) + + t.Log(response) + + require.Len(response.MethodResponses, 4) + require.Nil(response.CreatedIds) + require.Equal("3e25b2a0", response.SessionState) + require.Equal(jmap.EmailQuery, response.MethodResponses[0].Command) + require.Equal(map[string]any{ + "accountId": "j", + "queryState": "sqcakzewfqdk7oay", + "canCalculateChanges": true, + "position": 0.0, + "ids": []any{"fmaaaabh"}, + "total": 1.0, + }, response.MethodResponses[0].Parameters) + + require.Equal("0", response.MethodResponses[0].Tag) + require.Equal(jmap.EmailGet, response.MethodResponses[1].Command) + require.Equal("1", response.MethodResponses[1].Tag) + require.Equal(jmap.ThreadGet, response.MethodResponses[2].Command) + require.Equal("2", response.MethodResponses[2].Tag) + require.Equal(jmap.EmailGet, response.MethodResponses[3].Command) + require.Equal("3", response.MethodResponses[3].Tag) + +} diff --git a/pkg/jmap/jmap_test.go b/pkg/jmap/jmap_test.go index ae9f1c496c..fa28ca29c6 100644 --- a/pkg/jmap/jmap_test.go +++ b/pkg/jmap/jmap_test.go @@ -165,71 +165,17 @@ const mailboxes = `{"methodResponses": [ },"0"] ], "sessionState":"3e25b2a0" }` -const mails2 = `{"methodResponses":[ - ["Email/query",{ - "accountId":"j", - "queryState":"sqcakzewfqdk7oay", - "canCalculateChanges":true, - "position":0, - "ids":["fmaaaabh"], - "total":1 - }, "0"], - ["Email/get", { - "accountId":"j", - "state":"sqcakzewfqdk7oay", - "list":[ - { - "threadId":"bl", - "id":"fmaaaabh" - } - ], - "notFound":[] - }, "1"], - ["Thread/get",{ - "accountId":"j", - "state":"sqcakzewfqdk7oay", - "list":[ - { - "id":"bl", - "emailIds":["fmaaaabh"] - } - ], - "notFound":[] - }, "2 "], - ["Email/get",{ - "accountId":"j", - "state":"sqcakzewfqdk7oay", - "list":[ - { - "threadId":"bl", - "mailboxIds":{"a":true}, - "keywords":{}, - "hasAttachment":false, - "from":[ - {"name":"current generally", "email":"current.generally@example.com"} - ], - "subject":"eros auctor proin", - "receivedAt":"2025-04-30T09:47:44Z", - "size":15423, - "preview":"Lorem ipsum dolor sit amet consectetur adipiscing elit sed urna tristique himenaeos eu a mattis laoreet aliquet enim. Magnis est facilisis nibh nisl vitae nisi mauris nostra velit donec erat pellentesque sagittis ligula turpis suscipit ultricies. Morbi ...", - "id":"fmaaaabh" - } - ], - "notFound":[] - }, "3"] - ], "sessionState":"3e25b2a0" - }` type TestJmapWellKnownClient struct { t *testing.T } -func NewTestJmapWellKnownClient(t *testing.T) JmapWellKnownClient { +func NewTestJmapWellKnownClient(t *testing.T) WellKnownClient { return &TestJmapWellKnownClient{t: t} } -func (t *TestJmapWellKnownClient) GetWellKnown(username string, logger *log.Logger) (WellKnownJmap, error) { - return WellKnownJmap{ +func (t *TestJmapWellKnownClient) GetWellKnown(username string, logger *log.Logger) (WellKnownResponse, error) { + return WellKnownResponse{ ApiUrl: "test://", PrimaryAccounts: map[string]string{JmapMail: generateRandomString(2 + seededRand.Intn(10))}, }, nil @@ -239,7 +185,7 @@ type TestJmapApiClient struct { t *testing.T } -func NewTestJmapApiClient(t *testing.T) JmapApiClient { +func NewTestJmapApiClient(t *testing.T) ApiClient { return &TestJmapApiClient{t: t} } @@ -275,15 +221,15 @@ func TestRequests(t *testing.T) { wkClient := NewTestJmapWellKnownClient(t) logger := log.NopLogger() ctx := context.Background() - client := NewJmapClient(wkClient, apiClient) + client := NewClient(wkClient, apiClient) - jc := JmapContext{AccountId: "123", JmapUrl: "test://"} + session := Session{AccountId: "123", JmapUrl: "test://"} - folders, err := client.GetMailboxes(jc, ctx, &logger) + folders, err := client.GetMailboxes(session, ctx, &logger) require.NoError(err) require.Len(folders.Folders, 5) - emails, err := client.EmailQuery(jc, ctx, &logger, "Inbox") + emails, err := client.EmailThreadsQuery(session, ctx, &logger, "Inbox") require.NoError(err) require.Len(emails.Emails, 1) diff --git a/pkg/jmap/jmap_tools.go b/pkg/jmap/jmap_tools.go index ee9a999b46..85525f9d71 100644 --- a/pkg/jmap/jmap_tools.go +++ b/pkg/jmap/jmap_tools.go @@ -7,7 +7,7 @@ import ( "github.com/opencloud-eu/opencloud/pkg/log" ) -func command[T any](api JmapApiClient, +func command[T any](api ApiClient, logger *log.Logger, ctx context.Context, methodCalls *[][]any, @@ -62,7 +62,7 @@ func mapFolder(item map[string]any) JmapFolder { } } -func parseMailboxGetResponse(data JmapCommandResponse) (JmapFolders, error) { +func parseMailboxGetResponse(data JmapCommandResponse) (Folders, error) { first := data.MethodResponses[0] params := first[1] payload := params.(map[string]any) @@ -74,10 +74,20 @@ func parseMailboxGetResponse(data JmapCommandResponse) (JmapFolders, error) { folder := mapFolder(item) folders = append(folders, folder) } - return JmapFolders{Folders: folders, state: state}, nil + return Folders{Folders: folders, state: state}, nil } -func mapEmail(elem map[string]any) (Email, error) { +func firstFromStringArray(obj map[string]any, key string) string { + ary, ok := obj[key] + if ok { + if ary := ary.([]any); len(ary) > 0 { + return ary[0].(string) + } + } + return "" +} + +func mapEmail(elem map[string]any, fetchBodies bool, logger *log.Logger) (Email, error) { fromList := elem["from"].([]any) from := fromList[0].(map[string]any) var subject string @@ -100,11 +110,70 @@ func mapEmail(elem map[string]any) (Email, error) { return Email{}, err } + bodies := map[string]string{} + if fetchBodies { + bodyValuesAny, ok := elem["bodyValues"] + if ok { + bodyValues := bodyValuesAny.(map[string]any) + textBody, ok := elem["textBody"].([]any) + if ok && len(textBody) > 0 { + pick := textBody[0].(map[string]any) + mime := pick["type"].(string) + partId := pick["partId"].(string) + content, ok := bodyValues[partId] + if ok { + m := content.(map[string]any) + value, ok = m["value"] + if ok { + bodies[mime] = value.(string) + } else { + logger.Warn().Msg("textBody part has no value") + } + } else { + logger.Warn().Msgf("textBody references non-existent partId=%v", partId) + } + } else { + logger.Warn().Msgf("no textBody: %v", elem) + } + htmlBody, ok := elem["htmlBody"].([]any) + if ok && len(htmlBody) > 0 { + pick := htmlBody[0].(map[string]any) + mime := pick["type"].(string) + partId := pick["partId"].(string) + content, ok := bodyValues[partId] + if ok { + m := content.(map[string]any) + value, ok = m["value"] + if ok { + bodies[mime] = value.(string) + } else { + logger.Warn().Msg("htmlBody part has no value") + } + } else { + logger.Warn().Msgf("htmlBody references non-existent partId=%v", partId) + } + } else { + logger.Warn().Msg("no htmlBody") + } + } else { + logger.Warn().Msg("no bodies found in email") + } + } else { + bodies = nil + } + return Email{ + Id: elem["id"].(string), + MessageId: firstFromStringArray(elem, "messageId"), + BlobId: elem["blobId"].(string), + ThreadId: elem["threadId"].(string), + Size: int(elem["size"].(float64)), From: from["email"].(string), Subject: subject, HasAttachments: hasAttachments, Received: received, + Preview: elem["preview"].(string), + Bodies: bodies, }, nil } diff --git a/pkg/jmap/jmap_tools_test.go b/pkg/jmap/jmap_tools_test.go new file mode 100644 index 0000000000..b8d0a9b4f0 --- /dev/null +++ b/pkg/jmap/jmap_tools_test.go @@ -0,0 +1,295 @@ +package jmap + +import ( + "encoding/json" + "testing" + + "github.com/opencloud-eu/opencloud/pkg/log" + "github.com/stretchr/testify/require" +) + +const jmap_email_with_text_and_html_bodies = ` + { + "id": "mk2aaadcx", + "blobId": "cby92nwhy2pswwygvnavdnv0zc3kffdafeauqko2lyw1qvtnjhztwaya72ma", + "threadId": "dcx", + "mailboxIds": { + "a": true + }, + "keywords": {}, + "size": 9284, + "receivedAt": "2025-05-30T08:53:32Z", + "messageId": [ + "1748595212.4933355@example.com" + ], + "inReplyTo": null, + "references": null, + "sender": null, + "from": [ + { + "name": "Superb Openly", + "email": "superb.openly@example.com" + } + ], + "to": [ + { + "name": "alan", + "email": "alan@example.com" + } + ], + "cc": null, + "bcc": null, + "replyTo": null, + "subject": "libero ad blandit rutrum lacinia consectetur sem", + "sentAt": "2025-05-30T10:53:32+02:00", + "hasAttachment": false, + "preview": "Diam egestas imperdiet non eu quam semper euismod netus venenatis ante magnis mus finibus maecenas nec cras ac commodo nascetur aliquet habitasse porta velit felis tempus ligula vulputate. Elit dolor fames neque hac nunc ornare nibh facilisis nisl finib...", + "bodyValues": { + "1": { + "isEncodingProblem": false, + "isTruncated": false, + "value": "Diam egestas imperdiet non eu quam semper euismod netus venenatis ante magnis mus finibus maecenas nec cras ac commodo nascetur aliquet habitasse porta velit felis tempus ligula vulputate. Elit dolor fames neque hac nunc ornare nibh facilisis nisl finibus magna senectus montes vulputate justo dis cras interdum. Convallis montes urna iaculis etiam mauris lorem tristique accumsan erat tincidunt posuere quis felis primis dolor a ipsum hendrerit parturient dictum pulvinar phasellus id porttitor. Etiam mi sollicitudin justo eu natoque eros malesuada nostra vulputate maximus habitant arcu fames imperdiet odio at netus eget maecenas elit parturient hendrerit nibh dui augue quisque tellus amet platea sit. Lectus risus potenti bibendum gravida fringilla sollicitudin sit enim consectetur ipsum accumsan parturient lorem varius sagittis rutrum montes vehicula nec mus hendrerit hac malesuada vel ac integer elementum.\nEuismod donec aliquet suspendisse mi blandit faucibus egestas adipiscing purus congue id himenaeos aenean per. Nullam habitasse est volutpat montes laoreet posuere eget suscipit, ultrices interdum mi nulla ac at eleifend praesent dis nostra massa habitant sapien integer porta consequat amet. Ut conubia amet vulputate ridiculus euismod fermentum libero auctor, mus donec eros a netus ad condimentum morbi facilisi neque tellus feugiat class erat metus inceptos.\nAenean himenaeos ridiculus risus dictum taciti dui quisque penatibus interdum magnis sollicitudin commodo tempor ultrices dapibus mi tempus ullamcorper nibh volutpat justo consequat fusce amet hendrerit laoreet dignissim sit venenatis semper libero mus suscipit. Quis aptent non varius porttitor aliquam iaculis justo facilisi nostra sodales imperdiet integer odio tincidunt quisque rhoncus ullamcorper laoreet tristique dolor. Blandit fringilla adipiscing dictumst euismod magnis volutpat tortor mollis varius elementum nostra litora porttitor habitant convallis risus urna consectetur eleifend suspendisse auctor posuere. Senectus mauris purus a dis tincidunt parturient tortor proin facilisis taciti tellus egestas dui ante in turpis adipiscing lacus neque quisque sagittis tristique suscipit est vestibulum nullam. Pellentesque massa ligula lobortis habitasse rutrum finibus fermentum hac egestas augue aliquet efficitur volutpat mattis ac imperdiet malesuada id etiam turpis tempus tellus interdum quisque at pulvinar nullam proin velit dictumst. Habitant rutrum sit dignissim porta luctus aenean volutpat aliquam arcu lacus tincidunt augue mattis porttitor neque congue risus nostra ridiculus dui molestie maximus libero justo faucibus.\nNisl condimentum pulvinar vulputate quam ante urna habitasse suscipit, volutpat lorem venenatis sem dignissim sapien penatibus ipsum felis faucibus eget velit sociosqu dictumst arcu viverra erat. Vitae auctor lobortis etiam ligula maecenas aptent fringilla, tempus pellentesque euismod neque sociosqu posuere curabitur venenatis dis elit inceptos ullamcorper natoque. Suspendisse elementum semper diam luctus odio fringilla sem nascetur blandit nam cubilia integer senectus in sociosqu sollicitudin nisi parturient. Ante maximus donec hac malesuada nisl quam massa nunc justo conubia fringilla tellus natoque scelerisque cubilia litora.\nAliquet morbi ligula quisque dapibus ultrices eros sem malesuada lobortis massa litora vestibulum varius commodo egestas tincidunt aenean ullamcorper at duis velit auctor parturient. Feugiat natoque posuere orci rhoncus ante mus quam, quis non sapien ut purus volutpat himenaeos et senectus fermentum placerat elementum augue. Natoque id mauris vel mus molestie elementum fames hac consectetur sed platea ad eget efficitur maecenas conubia morbi justo vivamus placerat curae pretium nisi ipsum imperdiet. Velit eros volutpat efficitur pharetra natoque primis luctus nunc lacus fusce dolor sagittis porta maecenas odio rutrum dis consectetur nam metus venenatis ut. Iaculis turpis luctus per orci taciti vehicula amet ad integer, quis litora mauris praesent ullamcorper cursus faucibus at eros dictum dolor morbi mus semper senectus laoreet felis torquent. Phasellus senectus nibh ornare dui convallis orci consequat enim justo etiam himenaeos dictum velit dis magna tempor maecenas fermentum luctus morbi molestie praesent condimentum hendrerit penatibus nisl tempus." + }, + "2": { + "isEncodingProblem": false, + "isTruncated": false, + "value": "

Diam egestas imperdiet non eu quam semper euismod netus venenatis ante magnis mus finibus maecenas nec cras ac commodo nascetur aliquet habitasse porta velit felis tempus ligula vulputate. Elit dolor fames neque hac nunc ornare nibh facilisis nisl finibus magna senectus montes vulputate justo dis cras interdum. Convallis montes urna iaculis etiam mauris lorem tristique accumsan erat tincidunt posuere quis felis primis dolor a ipsum hendrerit parturient dictum pulvinar phasellus id porttitor. Etiam mi sollicitudin justo eu natoque eros malesuada nostra vulputate maximus habitant arcu fames imperdiet odio at netus eget maecenas elit parturient hendrerit nibh dui augue quisque tellus amet platea sit. Lectus risus potenti bibendum gravida fringilla sollicitudin sit enim consectetur ipsum accumsan parturient lorem varius sagittis rutrum montes vehicula nec mus hendrerit hac malesuada vel ac integer elementum.

\n

Euismod donec aliquet suspendisse mi blandit faucibus egestas adipiscing purus congue id himenaeos aenean per. Nullam habitasse est volutpat montes laoreet posuere eget suscipit, ultrices interdum mi nulla ac at eleifend praesent dis nostra massa habitant sapien integer porta consequat amet. Ut conubia amet vulputate ridiculus euismod fermentum libero auctor, mus donec eros a netus ad condimentum morbi facilisi neque tellus feugiat class erat metus inceptos.

\n

Aenean himenaeos ridiculus risus dictum taciti dui quisque penatibus interdum magnis sollicitudin commodo tempor ultrices dapibus mi tempus ullamcorper nibh volutpat justo consequat fusce amet hendrerit laoreet dignissim sit venenatis semper libero mus suscipit. Quis aptent non varius porttitor aliquam iaculis justo facilisi nostra sodales imperdiet integer odio tincidunt quisque rhoncus ullamcorper laoreet tristique dolor. Blandit fringilla adipiscing dictumst euismod magnis volutpat tortor mollis varius elementum nostra litora porttitor habitant convallis risus urna consectetur eleifend suspendisse auctor posuere. Senectus mauris purus a dis tincidunt parturient tortor proin facilisis taciti tellus egestas dui ante in turpis adipiscing lacus neque quisque sagittis tristique suscipit est vestibulum nullam. Pellentesque massa ligula lobortis habitasse rutrum finibus fermentum hac egestas augue aliquet efficitur volutpat mattis ac imperdiet malesuada id etiam turpis tempus tellus interdum quisque at pulvinar nullam proin velit dictumst. Habitant rutrum sit dignissim porta luctus aenean volutpat aliquam arcu lacus tincidunt augue mattis porttitor neque congue risus nostra ridiculus dui molestie maximus libero justo faucibus.

\n

Nisl condimentum pulvinar vulputate quam ante urna habitasse suscipit, volutpat lorem venenatis sem dignissim sapien penatibus ipsum felis faucibus eget velit sociosqu dictumst arcu viverra erat. Vitae auctor lobortis etiam ligula maecenas aptent fringilla, tempus pellentesque euismod neque sociosqu posuere curabitur venenatis dis elit inceptos ullamcorper natoque. Suspendisse elementum semper diam luctus odio fringilla sem nascetur blandit nam cubilia integer senectus in sociosqu sollicitudin nisi parturient. Ante maximus donec hac malesuada nisl quam massa nunc justo conubia fringilla tellus natoque scelerisque cubilia litora.

\n

Aliquet morbi ligula quisque dapibus ultrices eros sem malesuada lobortis massa litora vestibulum varius commodo egestas tincidunt aenean ullamcorper at duis velit auctor parturient. Feugiat natoque posuere orci rhoncus ante mus quam, quis non sapien ut purus volutpat himenaeos et senectus fermentum placerat elementum augue. Natoque id mauris vel mus molestie elementum fames hac consectetur sed platea ad eget efficitur maecenas conubia morbi justo vivamus placerat curae pretium nisi ipsum imperdiet. Velit eros volutpat efficitur pharetra natoque primis luctus nunc lacus fusce dolor sagittis porta maecenas odio rutrum dis consectetur nam metus venenatis ut. Iaculis turpis luctus per orci taciti vehicula amet ad integer, quis litora mauris praesent ullamcorper cursus faucibus at eros dictum dolor morbi mus semper senectus laoreet felis torquent. Phasellus senectus nibh ornare dui convallis orci consequat enim justo etiam himenaeos dictum velit dis magna tempor maecenas fermentum luctus morbi molestie praesent condimentum hendrerit penatibus nisl tempus.

" + } + }, + "textBody": [ + { + "partId": "1", + "blobId": "cfy92nwhy2pswwygvnavdnv0zc3kffdafeauqko2lyw1qvtnjhztwaya72mmga3iee", + "size": 4328, + "name": null, + "type": "text/plain", + "charset": "utf-8", + "disposition": null, + "cid": null, + "language": null, + "location": null + } + ], + "htmlBody": [ + { + "partId": "2", + "blobId": "cfy92nwhy2pswwygvnavdnv0zc3kffdafeauqko2lyw1qvtnjhztwaya72mimjulei", + "size": 4363, + "name": null, + "type": "text/html", + "charset": "utf-8", + "disposition": null, + "cid": null, + "language": null, + "location": null + } + ], + "attachments": [] + } +` + +const jmap_email_with_text_body = ` + { + "id": "mliaaadc7", + "blobId": "cc0tuhkv1lncttirzg9p97wd7k7gezaz9fbwjir31wrcvkykbm1zkaya9ima", + "threadId": "dc7", + "mailboxIds": { + "a": true + }, + "keywords": {}, + "size": 4080, + "receivedAt": "2025-05-30T09:59:55Z", + "messageId": [ + "1748599195.5902335@example.com" + ], + "inReplyTo": null, + "references": null, + "sender": null, + "from": [ + { + "name": "Cunning Properly", + "email": "cunning.properly@example.com" + } + ], + "to": [ + { + "name": "alan", + "email": "alan@example.com" + } + ], + "cc": null, + "bcc": null, + "replyTo": null, + "subject": "Parturient Nostra Orci", + "sentAt": "2025-05-30T11:59:55+02:00", + "hasAttachment": false, + "preview": "Et magnis pulvinar congue aliquet tincidunt morbi lobortis mattis mus litora malesuada fringilla varius ullamcorper parturient fames accumsan faucibus erat. Magna id est cras eu a netus orci ridiculus lobortis urna dis ipsum at fermentum mi lacinia quis...", + "bodyValues": { + "0": { + "isEncodingProblem": false, + "isTruncated": false, + "value": "Et magnis pulvinar congue aliquet tincidunt morbi lobortis mattis mus litora malesuada fringilla varius ullamcorper parturient fames accumsan faucibus erat. Magna id est cras eu a netus orci ridiculus lobortis urna dis ipsum at fermentum mi lacinia quis fames. Cursus ipsum gravida libero ultricies pretium montes rutrum suscipit tempor hac dapibus senectus commodo elementum leo nullam auctor litora pulvinar finibus. Ad nulla torquent quis mollis phasellus sodales aliquet lacinia varius, adipiscing enim habitant et netus egestas eu tempor malesuada mattis hac fusce integer diam inceptos venenatis turpis. Sem senectus aptent non dolor hendrerit magna mauris facilisis justo quam fringilla cursus gravida praesent malesuada taciti odio etiam magnis nostra vivamus. Tempus fames faucibus massa rutrum sit habitant morbi curabitur integer erat et condimentum tincidunt tempor libero vulputate maecenas rhoncus turpis congue a luctus aenean tristique lacinia cursus est fusce non mollis justo euismod facilisis egestas.\nAuctor maecenas vestibulum aenean accumsan eros ex potenti sociosqu, fusce sapien quis faucibus aliquam vivamus tristique hendrerit in per fermentum cras sodales curabitur scelerisque. Finibus metus adipiscing taciti eget rutrum vitae arcu torquent, dignissim at nibh venenatis facilisis molestie erat augue massa feugiat aliquam sollicitudin elementum cursus in. Est neque cras aenean felis justo euismod adipiscing magnis sagittis ut massa aliquet malesuada laoreet velit purus suspendisse bibendum pharetra litora ultrices diam ullamcorper volutpat venenatis egestas. Non laoreet eu interdum sodales phasellus morbi risus maecenas parturient auctor senectus urna ornare faucibus sociosqu habitant nisi cubilia viverra diam fames condimentum tempor scelerisque iaculis lacus elit feugiat adipiscing vivamus. Euismod volutpat gravida fames nascetur ridiculus iaculis habitasse vulputate habitant netus varius rhoncus ultrices porttitor himenaeos lorem libero congue turpis parturient quisque nostra aliquet in sem curabitur eleifend accumsan faucibus per pellentesque. Nibh auctor dictum vivamus eros ex gravida hac torquent purus suscipit fames lacus sagittis condimentum morbi dui litora cras duis iaculis massa porta praesent sapien. Ultricies tortor phasellus mus erat metus nisi malesuada augue sollicitudin convallis egestas ultrices arcu luctus tempus molestie facilisis nam scelerisque feugiat. Nibh imperdiet accumsan fermentum auctor et neque blandit elementum id eget justo suscipit interdum etiam mus tempus diam cursus nunc malesuada aliquam vestibulum. Arcu facilisi curae sed mi felis commodo, sapien neque aenean nullam rutrum torquent lectus fringilla rhoncus eros elit molestie.\nAptent fringilla cubilia sed duis non eu vulputate dis efficitur per ad venenatis dictumst egestas commodo blandit. Conubia finibus curae molestie egestas interdum mollis aliquam venenatis penatibus habitant varius natoque aptent nec. Mattis hac commodo integer donec gravida himenaeos vivamus primis rhoncus nam cursus erat nibh nascetur elementum felis duis in volutpat aliquet nulla vehicula placerat est. Placerat dis est aenean laoreet convallis metus sit mi, porttitor ullamcorper risus augue commodo dictumst nisi platea cubilia maximus elit volutpat hac rutrum suspendisse. Lacinia taciti justo non ligula vivamus aliquam luctus tellus dictumst vulputate interdum per aptent a metus eu mauris hac ex montes senectus blandit proin. Proin ullamcorper habitant justo pharetra felis commodo parturient scelerisque rutrum suspendisse ad ante cubilia pulvinar est vivamus quisque imperdiet vestibulum varius aliquam enim. Mollis aliquam metus montes dapibus volutpat maecenas fermentum massa tempor condimentum rhoncus lacinia tincidunt accumsan leo nunc elementum maximus fringilla dui augue." + } + }, + "textBody": [ + { + "partId": "0", + "blobId": "cg0tuhkv1lncttirzg9p97wd7k7gezaz9fbwjir31wrcvkykbm1zkaya9imiuaxgdu", + "size": 3814, + "name": null, + "type": "text/plain", + "charset": "utf-8", + "disposition": null, + "cid": null, + "language": null, + "location": null + } + ], + "htmlBody": [ + { + "partId": "0", + "blobId": "cg0tuhkv1lncttirzg9p97wd7k7gezaz9fbwjir31wrcvkykbm1zkaya9imiuaxgdu", + "size": 3814, + "name": null, + "type": "text/plain", + "charset": "utf-8", + "disposition": null, + "cid": null, + "language": null, + "location": null + } + ], + "attachments": [] + } +` + +const jmap_email_with_html_body = ` + { + "id": "mleaaadcz", + "blobId": "cdrahu0j7gjhl3jscjnzt0ursycvwn29u9uxjlrlcpeinrm2r0yz1aya9ema", + "threadId": "dcz", + "mailboxIds": { + "a": true + }, + "keywords": {}, + "size": 10556, + "receivedAt": "2025-05-30T09:59:55Z", + "messageId": [ + "1748599195.3428368@example.com" + ], + "inReplyTo": null, + "references": null, + "sender": null, + "from": [ + { + "name": "Eminent Extremely", + "email": "eminent.extremely@example.com" + } + ], + "to": [ + { + "name": "alan", + "email": "alan@example.com" + } + ], + "cc": null, + "bcc": null, + "replyTo": null, + "subject": "Et Magnis Pulvinar Congue Aliquet Tincidunt Morbi Lobortis Mattis", + "sentAt": "2025-05-30T11:59:55+02:00", + "hasAttachment": false, + "preview": "Lorem ipsum dolor sit amet consectetur adipiscing elit, montes aenean lectus porttitor mauris ridiculus rutrum et inceptos torquent congue tristique dictumst nullam suspendisse. Lobortis ad per habitasse volutpat proin posuere convallis dapibus tristiqu...", + "bodyValues": { + "0": { + "isEncodingProblem": false, + "isTruncated": false, + "value": "

Lorem ipsum dolor sit amet consectetur adipiscing elit, montes aenean lectus porttitor mauris ridiculus rutrum et inceptos torquent congue tristique dictumst nullam suspendisse. Lobortis ad per habitasse volutpat proin posuere convallis dapibus tristique lacinia placerat scelerisque curabitur sed aenean sodales pharetra est nisl odio sagittis platea in. Venenatis semper inceptos laoreet orci aliquam natoque magna id tempor lacus duis convallis molestie ridiculus vivamus etiam tortor ultrices blandit dictum finibus volutpat. Finibus amet donec justo lectus senectus morbi convallis a hendrerit malesuada neque nisl ad nulla per nunc praesent.

\n

Elit dolor nostra vehicula massa placerat convallis dictum natoque commodo diam, ultricies nam consequat inceptos torquent himenaeos risus eleifend scelerisque dui cursus libero nisl neque fusce montes metus proin cras donec nibh. Tempus sodales fames consectetur in integer aliquet odio maecenas est sapien risus parturient lorem aliquam viverra mattis feugiat eu platea ex tempor mi efficitur a. Ut vestibulum nibh et himenaeos taciti nisl class pretium maximus est ultrices fermentum nunc mus dapibus vel massa venenatis facilisis non nascetur leo. Scelerisque metus nisi suspendisse pharetra fames malesuada pretium dictumst, etiam potenti molestie vestibulum habitasse aenean velit ridiculus condimentum ut montes at tortor arcu curae id luctus.

\n

Nulla sollicitudin vestibulum vulputate urna sem etiam senectus turpis ac tempus, laoreet natoque metus justo dapibus purus libero fringilla aenean orci integer imperdiet duis curabitur feugiat blandit proin consequat quam velit ante. Scelerisque elit tincidunt feugiat primis risus amet ac interdum varius luctus quis dui consectetur platea conubia senectus mus efficitur mauris cubilia libero magnis egestas elementum ultricies. Elit dapibus finibus proin aliquet etiam nibh quam laoreet senectus primis a mattis vel montes massa porta dui commodo velit mi bibendum cubilia sed euismod. Posuere consequat velit mauris in sollicitudin id dolor nisl placerat magna aliquet sed metus curae. Penatibus commodo cubilia ex velit leo ultricies ipsum dignissim molestie curae lectus, integer a risus bibendum varius ornare laoreet fermentum fusce duis luctus ultrices nostra sem id nascetur dictum tempor morbi aliquet mauris. Duis posuere enim odio nisl condimentum nunc eleifend nullam primis maecenas, tellus pretium congue nascetur lacinia in lorem vel quisque lectus proin laoreet consectetur faucibus aliquet montes ad sodales commodo vestibulum. Fames odio luctus donec habitasse neque posuere purus quis penatibus netus mi lobortis suspendisse vehicula eu lorem erat libero in scelerisque leo dapibus tristique amet.

\n

Vulputate erat consectetur cras iaculis nascetur lectus pulvinar fames est ut malesuada natoque hac orci euismod rhoncus ad faucibus nostra aptent sociosqu. Leo primis dictumst libero platea nisl at mauris eu fusce penatibus, nunc maximus sodales montes facilisis pharetra ex ipsum class curae aenean parturient tortor massa morbi cras varius ut augue vivamus elementum. Lorem eget facilisi nec varius elementum mattis aliquam praesent blandit dapibus aliquet ornare montes malesuada taciti netus egestas lacus morbi. A nascetur nibh commodo sodales consequat nullam taciti risus, viverra proin quam quis elementum libero molestie fusce egestas curae augue mattis nisl montes senectus. Elementum tempus in dictum sagittis ac hac feugiat lorem efficitur consequat neque per tellus penatibus. Ut suscipit tempus nec tincidunt potenti libero luctus eleifend auctor pulvinar ultrices purus imperdiet dignissim at mus et montes phasellus maecenas hac egestas nulla porta. Placerat sed netus consectetur dis duis varius elementum convallis nostra natoque. Massa morbi ante egestas sit feugiat fusce conubia imperdiet vestibulum maximus mollis himenaeos porttitor auctor aliquet neque suscipit rhoncus viverra natoque vivamus posuere commodo arcu quis cras.

\n

Habitant sem ullamcorper euismod libero curabitur orci urna felis senectus lacus nunc congue morbi molestie adipiscing per sed pharetra magna ut arcu convallis consectetur non. Lorem vel id elementum lobortis netus scelerisque diam fames volutpat tristique congue justo penatibus bibendum sociosqu adipiscing est auctor habitasse ullamcorper cursus quis. Risus quis fermentum vehicula adipiscing erat orci, aptent proin gravida habitasse porttitor mattis ipsum praesent ligula feugiat ad efficitur integer. Lorem maecenas venenatis per suscipit accumsan aliquet penatibus faucibus facilisi facilisis sodales platea suspendisse euismod. Turpis posuere nisi ut tempor aliquet dapibus cursus ante mauris sed auctor dictum egestas nullam sapien porttitor justo pretium. Cursus lorem quisque sem leo convallis molestie etiam conubia pretium lacinia ultricies vestibulum nec sodales natoque commodo dictumst volutpat fames parturient justo cubilia augue velit purus aliquam. Nisl integer mus ultricies laoreet congue vivamus cras ultrices orci fames quis non tempor vel libero at nulla malesuada fermentum dolor lacus ornare ut sodales adipiscing eros nascetur lectus. Platea turpis libero habitasse a praesent cras sem eros hendrerit finibus integer tempor ipsum sapien in nostra litora sit montes risus iaculis class at torquent non magna suspendisse purus dictumst vulputate mi sodales curabitur scelerisque.

\n

Metus laoreet morbi erat ligula gravida non montes aliquam et bibendum tempus pharetra posuere nulla eleifend ante tortor habitant. Suscipit nisl proin natoque mollis ligula commodo scelerisque leo pellentesque per senectus adipiscing quis varius aenean curae phasellus magnis aptent felis nec nibh nisi erat lobortis auctor vehicula molestie. Fames purus velit bibendum maecenas tortor ultricies maximus convallis rhoncus inceptos per porta ipsum eu non habitant lacinia pellentesque. Ante et platea id at tempus magna orci etiam feugiat habitant conubia nascetur aenean. Purus nostra lectus lobortis etiam est lacus luctus laoreet sed ac lacinia quis at egestas class ridiculus litora eleifend urna porttitor enim.

\n

Faucibus felis integer eleifend in molestie eget platea tincidunt dui nec aliquam ultricies sodales quam porttitor facilisi potenti facilisis nisl vehicula tempus curae arcu. Magnis aliquet mi per mollis egestas porta montes ut pulvinar arcu neque adipiscing duis feugiat vel senectus quis facilisi elit nibh felis sodales ullamcorper diam sollicitudin ad. Venenatis hendrerit eget quisque sagittis facilisi quam non sociosqu curae enim, potenti augue dapibus ullamcorper auctor mi dignissim etiam viverra orci commodo laoreet inceptos pellentesque adipiscing class ac sem luctus faucibus fringilla. Urna ante in class auctor facilisi risus himenaeos, vitae malesuada dui mattis mollis aenean cras porttitor dignissim praesent egestas pretium condimentum aliquam. Arcu fringilla dictumst turpis vitae ex tempus vehicula efficitur maximus tincidunt pulvinar praesent nulla odio lacus ridiculus fames pharetra mauris ornare felis aenean penatibus taciti dignissim fusce diam orci vel. Habitant lectus primis risus nisi dolor erat senectus eros, felis varius sit phasellus quisque congue blandit bibendum ante est ligula nostra aliquet aptent magna purus. Pharetra eu hendrerit pulvinar magnis primis quisque integer in mus pellentesque suspendisse lacinia sem dictumst nisl auctor maximus platea.

\n

Nascetur tortor ac placerat facilisi integer litora sit varius duis efficitur sapien, hendrerit diam accumsan elit montes vehicula magnis consequat nostra justo parturient torquent pretium interdum a tincidunt dictum magna vel etiam ut dolor ullamcorper. Tincidunt aliquam lectus id velit class ad malesuada auctor litora consectetur aptent pharetra etiam dolor et tristique lacus.

\n

Non quis nullam urna ligula aptent curabitur odio lacus suspendisse lacinia molestie mus morbi elementum maximus interdum a purus enim sem sapien scelerisque lobortis et phasellus. Elementum vulputate vehicula posuere iaculis sodales fames urna rhoncus purus, laoreet metus ornare sem consequat mollis nibh lorem parturient adipiscing porttitor pretium placerat habitasse libero eleifend enim morbi. Massa tellus viverra nascetur leo aenean nisl vivamus malesuada at ipsum lobortis rutrum accumsan senectus dignissim elit fermentum integer praesent a purus proin faucibus aptent ad adipiscing imperdiet convallis mauris sodales. Sed iaculis mauris ut nunc fusce justo et venenatis libero litora eget aliquet penatibus gravida interdum phasellus turpis ullamcorper cubilia duis leo ex mattis vel cras donec lacinia sodales malesuada id elementum. Mus ultrices ullamcorper suspendisse nec dapibus senectus fermentum felis netus non magna congue neque bibendum dignissim ipsum aenean integer curae facilisi donec. Dolor purus nibh diam facilisis erat etiam mollis consectetur semper, vestibulum suscipit mauris egestas venenatis neque varius dignissim pulvinar ligula lobortis morbi aliquam eros nullam suspendisse orci tortor. Accumsan rutrum tempus arcu eros convallis vel natoque commodo eget diam mollis himenaeos proin placerat suspendisse duis taciti. Urna gravida a mus lacinia aliquam justo in lectus nec sed habitasse penatibus et ex massa vel commodo facilisis rutrum taciti odio torquent inceptos imperdiet sociosqu montes cursus nostra suscipit quam venenatis. Mattis nulla congue interdum gravida ornare ac sed, sagittis iaculis sem parturient netus proin maecenas dignissim rhoncus efficitur condimentum egestas dis litora. Odio imperdiet facilisi tempus ipsum donec tortor dictumst sem finibus parturient aptent molestie pretium risus sagittis pellentesque nisi litora congue cras viverra enim tempor vehicula platea.

\n

Sodales pretium egestas libero viverra lobortis interdum amet quis fames neque convallis dictumst sollicitudin eros felis nec. Nibh nisi sit nam magna elit fames habitasse sollicitudin libero lacus luctus porttitor enim conubia dolor suscipit aptent platea dictum habitant primis imperdiet taciti. Lobortis dui scelerisque feugiat venenatis vehicula tristique mi iaculis efficitur imperdiet aliquet sociosqu ipsum ornare sed gravida amet platea nisl mollis consectetur ex ac.

" + } + }, + "textBody": [ + { + "partId": "0", + "blobId": "chrahu0j7gjhl3jscjnzt0ursycvwn29u9uxjlrlcpeinrm2r0yz1aya9emlqaueka", + "size": 10244, + "name": null, + "type": "text/html", + "charset": "utf-8", + "disposition": null, + "cid": null, + "language": null, + "location": null + } + ], + "htmlBody": [ + { + "partId": "0", + "blobId": "chrahu0j7gjhl3jscjnzt0ursycvwn29u9uxjlrlcpeinrm2r0yz1aya9emlqaueka", + "size": 10244, + "name": null, + "type": "text/html", + "charset": "utf-8", + "disposition": null, + "cid": null, + "language": null, + "location": null + } + ], + "attachments": [] + } +` + +func TestMapEmailWithTextAndHtmlBodies(t *testing.T) { + require := require.New(t) + + var elem map[string]any + err := json.Unmarshal([]byte(jmap_email_with_text_and_html_bodies), &elem) + require.NoError(err) + + logger := log.NopLogger() + + email, err := mapEmail(elem, true, &logger) + require.NoError(err) + require.Equal("libero ad blandit rutrum lacinia consectetur sem", email.Subject) + require.Equal("Diam egestas imperdiet non eu quam semper euismod netus venenatis ante magnis mus finibus maecenas nec cras ac commodo nascetur aliquet habitasse porta velit felis tempus ligula vulputate. Elit dolor fames neque hac nunc ornare nibh facilisis nisl finib...", email.Preview) + require.Len(email.Bodies, 2) + require.Contains(email.Bodies, "text/html") + require.Equal("

Diam egestas imperdiet non eu quam semper euismod netus venenatis ante magnis mus finibus maecenas nec cras ac commodo nascetur aliquet habitasse porta velit felis tempus ligula vulputate. Elit dolor fames neque hac nunc ornare nibh facilisis nisl finibus magna senectus montes vulputate justo dis cras interdum. Convallis montes urna iaculis etiam mauris lorem tristique accumsan erat tincidunt posuere quis felis primis dolor a ipsum hendrerit parturient dictum pulvinar phasellus id porttitor. Etiam mi sollicitudin justo eu natoque eros malesuada nostra vulputate maximus habitant arcu fames imperdiet odio at netus eget maecenas elit parturient hendrerit nibh dui augue quisque tellus amet platea sit. Lectus risus potenti bibendum gravida fringilla sollicitudin sit enim consectetur ipsum accumsan parturient lorem varius sagittis rutrum montes vehicula nec mus hendrerit hac malesuada vel ac integer elementum.

\n

Euismod donec aliquet suspendisse mi blandit faucibus egestas adipiscing purus congue id himenaeos aenean per. Nullam habitasse est volutpat montes laoreet posuere eget suscipit, ultrices interdum mi nulla ac at eleifend praesent dis nostra massa habitant sapien integer porta consequat amet. Ut conubia amet vulputate ridiculus euismod fermentum libero auctor, mus donec eros a netus ad condimentum morbi facilisi neque tellus feugiat class erat metus inceptos.

\n

Aenean himenaeos ridiculus risus dictum taciti dui quisque penatibus interdum magnis sollicitudin commodo tempor ultrices dapibus mi tempus ullamcorper nibh volutpat justo consequat fusce amet hendrerit laoreet dignissim sit venenatis semper libero mus suscipit. Quis aptent non varius porttitor aliquam iaculis justo facilisi nostra sodales imperdiet integer odio tincidunt quisque rhoncus ullamcorper laoreet tristique dolor. Blandit fringilla adipiscing dictumst euismod magnis volutpat tortor mollis varius elementum nostra litora porttitor habitant convallis risus urna consectetur eleifend suspendisse auctor posuere. Senectus mauris purus a dis tincidunt parturient tortor proin facilisis taciti tellus egestas dui ante in turpis adipiscing lacus neque quisque sagittis tristique suscipit est vestibulum nullam. Pellentesque massa ligula lobortis habitasse rutrum finibus fermentum hac egestas augue aliquet efficitur volutpat mattis ac imperdiet malesuada id etiam turpis tempus tellus interdum quisque at pulvinar nullam proin velit dictumst. Habitant rutrum sit dignissim porta luctus aenean volutpat aliquam arcu lacus tincidunt augue mattis porttitor neque congue risus nostra ridiculus dui molestie maximus libero justo faucibus.

\n

Nisl condimentum pulvinar vulputate quam ante urna habitasse suscipit, volutpat lorem venenatis sem dignissim sapien penatibus ipsum felis faucibus eget velit sociosqu dictumst arcu viverra erat. Vitae auctor lobortis etiam ligula maecenas aptent fringilla, tempus pellentesque euismod neque sociosqu posuere curabitur venenatis dis elit inceptos ullamcorper natoque. Suspendisse elementum semper diam luctus odio fringilla sem nascetur blandit nam cubilia integer senectus in sociosqu sollicitudin nisi parturient. Ante maximus donec hac malesuada nisl quam massa nunc justo conubia fringilla tellus natoque scelerisque cubilia litora.

\n

Aliquet morbi ligula quisque dapibus ultrices eros sem malesuada lobortis massa litora vestibulum varius commodo egestas tincidunt aenean ullamcorper at duis velit auctor parturient. Feugiat natoque posuere orci rhoncus ante mus quam, quis non sapien ut purus volutpat himenaeos et senectus fermentum placerat elementum augue. Natoque id mauris vel mus molestie elementum fames hac consectetur sed platea ad eget efficitur maecenas conubia morbi justo vivamus placerat curae pretium nisi ipsum imperdiet. Velit eros volutpat efficitur pharetra natoque primis luctus nunc lacus fusce dolor sagittis porta maecenas odio rutrum dis consectetur nam metus venenatis ut. Iaculis turpis luctus per orci taciti vehicula amet ad integer, quis litora mauris praesent ullamcorper cursus faucibus at eros dictum dolor morbi mus semper senectus laoreet felis torquent. Phasellus senectus nibh ornare dui convallis orci consequat enim justo etiam himenaeos dictum velit dis magna tempor maecenas fermentum luctus morbi molestie praesent condimentum hendrerit penatibus nisl tempus.

", email.Bodies["text/html"]) + require.Contains(email.Bodies, "text/plain") + require.Equal("Diam egestas imperdiet non eu quam semper euismod netus venenatis ante magnis mus finibus maecenas nec cras ac commodo nascetur aliquet habitasse porta velit felis tempus ligula vulputate. Elit dolor fames neque hac nunc ornare nibh facilisis nisl finibus magna senectus montes vulputate justo dis cras interdum. Convallis montes urna iaculis etiam mauris lorem tristique accumsan erat tincidunt posuere quis felis primis dolor a ipsum hendrerit parturient dictum pulvinar phasellus id porttitor. Etiam mi sollicitudin justo eu natoque eros malesuada nostra vulputate maximus habitant arcu fames imperdiet odio at netus eget maecenas elit parturient hendrerit nibh dui augue quisque tellus amet platea sit. Lectus risus potenti bibendum gravida fringilla sollicitudin sit enim consectetur ipsum accumsan parturient lorem varius sagittis rutrum montes vehicula nec mus hendrerit hac malesuada vel ac integer elementum.\nEuismod donec aliquet suspendisse mi blandit faucibus egestas adipiscing purus congue id himenaeos aenean per. Nullam habitasse est volutpat montes laoreet posuere eget suscipit, ultrices interdum mi nulla ac at eleifend praesent dis nostra massa habitant sapien integer porta consequat amet. Ut conubia amet vulputate ridiculus euismod fermentum libero auctor, mus donec eros a netus ad condimentum morbi facilisi neque tellus feugiat class erat metus inceptos.\nAenean himenaeos ridiculus risus dictum taciti dui quisque penatibus interdum magnis sollicitudin commodo tempor ultrices dapibus mi tempus ullamcorper nibh volutpat justo consequat fusce amet hendrerit laoreet dignissim sit venenatis semper libero mus suscipit. Quis aptent non varius porttitor aliquam iaculis justo facilisi nostra sodales imperdiet integer odio tincidunt quisque rhoncus ullamcorper laoreet tristique dolor. Blandit fringilla adipiscing dictumst euismod magnis volutpat tortor mollis varius elementum nostra litora porttitor habitant convallis risus urna consectetur eleifend suspendisse auctor posuere. Senectus mauris purus a dis tincidunt parturient tortor proin facilisis taciti tellus egestas dui ante in turpis adipiscing lacus neque quisque sagittis tristique suscipit est vestibulum nullam. Pellentesque massa ligula lobortis habitasse rutrum finibus fermentum hac egestas augue aliquet efficitur volutpat mattis ac imperdiet malesuada id etiam turpis tempus tellus interdum quisque at pulvinar nullam proin velit dictumst. Habitant rutrum sit dignissim porta luctus aenean volutpat aliquam arcu lacus tincidunt augue mattis porttitor neque congue risus nostra ridiculus dui molestie maximus libero justo faucibus.\nNisl condimentum pulvinar vulputate quam ante urna habitasse suscipit, volutpat lorem venenatis sem dignissim sapien penatibus ipsum felis faucibus eget velit sociosqu dictumst arcu viverra erat. Vitae auctor lobortis etiam ligula maecenas aptent fringilla, tempus pellentesque euismod neque sociosqu posuere curabitur venenatis dis elit inceptos ullamcorper natoque. Suspendisse elementum semper diam luctus odio fringilla sem nascetur blandit nam cubilia integer senectus in sociosqu sollicitudin nisi parturient. Ante maximus donec hac malesuada nisl quam massa nunc justo conubia fringilla tellus natoque scelerisque cubilia litora.\nAliquet morbi ligula quisque dapibus ultrices eros sem malesuada lobortis massa litora vestibulum varius commodo egestas tincidunt aenean ullamcorper at duis velit auctor parturient. Feugiat natoque posuere orci rhoncus ante mus quam, quis non sapien ut purus volutpat himenaeos et senectus fermentum placerat elementum augue. Natoque id mauris vel mus molestie elementum fames hac consectetur sed platea ad eget efficitur maecenas conubia morbi justo vivamus placerat curae pretium nisi ipsum imperdiet. Velit eros volutpat efficitur pharetra natoque primis luctus nunc lacus fusce dolor sagittis porta maecenas odio rutrum dis consectetur nam metus venenatis ut. Iaculis turpis luctus per orci taciti vehicula amet ad integer, quis litora mauris praesent ullamcorper cursus faucibus at eros dictum dolor morbi mus semper senectus laoreet felis torquent. Phasellus senectus nibh ornare dui convallis orci consequat enim justo etiam himenaeos dictum velit dis magna tempor maecenas fermentum luctus morbi molestie praesent condimentum hendrerit penatibus nisl tempus.", email.Bodies["text/plain"]) + require.Equal("1748595212.4933355@example.com", email.MessageId) + require.False(email.HasAttachments) + require.Equal("cby92nwhy2pswwygvnavdnv0zc3kffdafeauqko2lyw1qvtnjhztwaya72ma", email.BlobId) + +} + +func TestMapEmailWithHtmlBody(t *testing.T) { + require := require.New(t) + + var elem map[string]any + err := json.Unmarshal([]byte(jmap_email_with_html_body), &elem) + require.NoError(err) + + logger := log.NopLogger() + + email, err := mapEmail(elem, true, &logger) + require.NoError(err) + require.Len(email.Bodies, 1) + require.Contains(email.Bodies, "text/html") + require.Equal("

Lorem ipsum dolor sit amet consectetur adipiscing elit, montes aenean lectus porttitor mauris ridiculus rutrum et inceptos torquent congue tristique dictumst nullam suspendisse. Lobortis ad per habitasse volutpat proin posuere convallis dapibus tristique lacinia placerat scelerisque curabitur sed aenean sodales pharetra est nisl odio sagittis platea in. Venenatis semper inceptos laoreet orci aliquam natoque magna id tempor lacus duis convallis molestie ridiculus vivamus etiam tortor ultrices blandit dictum finibus volutpat. Finibus amet donec justo lectus senectus morbi convallis a hendrerit malesuada neque nisl ad nulla per nunc praesent.

\n

Elit dolor nostra vehicula massa placerat convallis dictum natoque commodo diam, ultricies nam consequat inceptos torquent himenaeos risus eleifend scelerisque dui cursus libero nisl neque fusce montes metus proin cras donec nibh. Tempus sodales fames consectetur in integer aliquet odio maecenas est sapien risus parturient lorem aliquam viverra mattis feugiat eu platea ex tempor mi efficitur a. Ut vestibulum nibh et himenaeos taciti nisl class pretium maximus est ultrices fermentum nunc mus dapibus vel massa venenatis facilisis non nascetur leo. Scelerisque metus nisi suspendisse pharetra fames malesuada pretium dictumst, etiam potenti molestie vestibulum habitasse aenean velit ridiculus condimentum ut montes at tortor arcu curae id luctus.

\n

Nulla sollicitudin vestibulum vulputate urna sem etiam senectus turpis ac tempus, laoreet natoque metus justo dapibus purus libero fringilla aenean orci integer imperdiet duis curabitur feugiat blandit proin consequat quam velit ante. Scelerisque elit tincidunt feugiat primis risus amet ac interdum varius luctus quis dui consectetur platea conubia senectus mus efficitur mauris cubilia libero magnis egestas elementum ultricies. Elit dapibus finibus proin aliquet etiam nibh quam laoreet senectus primis a mattis vel montes massa porta dui commodo velit mi bibendum cubilia sed euismod. Posuere consequat velit mauris in sollicitudin id dolor nisl placerat magna aliquet sed metus curae. Penatibus commodo cubilia ex velit leo ultricies ipsum dignissim molestie curae lectus, integer a risus bibendum varius ornare laoreet fermentum fusce duis luctus ultrices nostra sem id nascetur dictum tempor morbi aliquet mauris. Duis posuere enim odio nisl condimentum nunc eleifend nullam primis maecenas, tellus pretium congue nascetur lacinia in lorem vel quisque lectus proin laoreet consectetur faucibus aliquet montes ad sodales commodo vestibulum. Fames odio luctus donec habitasse neque posuere purus quis penatibus netus mi lobortis suspendisse vehicula eu lorem erat libero in scelerisque leo dapibus tristique amet.

\n

Vulputate erat consectetur cras iaculis nascetur lectus pulvinar fames est ut malesuada natoque hac orci euismod rhoncus ad faucibus nostra aptent sociosqu. Leo primis dictumst libero platea nisl at mauris eu fusce penatibus, nunc maximus sodales montes facilisis pharetra ex ipsum class curae aenean parturient tortor massa morbi cras varius ut augue vivamus elementum. Lorem eget facilisi nec varius elementum mattis aliquam praesent blandit dapibus aliquet ornare montes malesuada taciti netus egestas lacus morbi. A nascetur nibh commodo sodales consequat nullam taciti risus, viverra proin quam quis elementum libero molestie fusce egestas curae augue mattis nisl montes senectus. Elementum tempus in dictum sagittis ac hac feugiat lorem efficitur consequat neque per tellus penatibus. Ut suscipit tempus nec tincidunt potenti libero luctus eleifend auctor pulvinar ultrices purus imperdiet dignissim at mus et montes phasellus maecenas hac egestas nulla porta. Placerat sed netus consectetur dis duis varius elementum convallis nostra natoque. Massa morbi ante egestas sit feugiat fusce conubia imperdiet vestibulum maximus mollis himenaeos porttitor auctor aliquet neque suscipit rhoncus viverra natoque vivamus posuere commodo arcu quis cras.

\n

Habitant sem ullamcorper euismod libero curabitur orci urna felis senectus lacus nunc congue morbi molestie adipiscing per sed pharetra magna ut arcu convallis consectetur non. Lorem vel id elementum lobortis netus scelerisque diam fames volutpat tristique congue justo penatibus bibendum sociosqu adipiscing est auctor habitasse ullamcorper cursus quis. Risus quis fermentum vehicula adipiscing erat orci, aptent proin gravida habitasse porttitor mattis ipsum praesent ligula feugiat ad efficitur integer. Lorem maecenas venenatis per suscipit accumsan aliquet penatibus faucibus facilisi facilisis sodales platea suspendisse euismod. Turpis posuere nisi ut tempor aliquet dapibus cursus ante mauris sed auctor dictum egestas nullam sapien porttitor justo pretium. Cursus lorem quisque sem leo convallis molestie etiam conubia pretium lacinia ultricies vestibulum nec sodales natoque commodo dictumst volutpat fames parturient justo cubilia augue velit purus aliquam. Nisl integer mus ultricies laoreet congue vivamus cras ultrices orci fames quis non tempor vel libero at nulla malesuada fermentum dolor lacus ornare ut sodales adipiscing eros nascetur lectus. Platea turpis libero habitasse a praesent cras sem eros hendrerit finibus integer tempor ipsum sapien in nostra litora sit montes risus iaculis class at torquent non magna suspendisse purus dictumst vulputate mi sodales curabitur scelerisque.

\n

Metus laoreet morbi erat ligula gravida non montes aliquam et bibendum tempus pharetra posuere nulla eleifend ante tortor habitant. Suscipit nisl proin natoque mollis ligula commodo scelerisque leo pellentesque per senectus adipiscing quis varius aenean curae phasellus magnis aptent felis nec nibh nisi erat lobortis auctor vehicula molestie. Fames purus velit bibendum maecenas tortor ultricies maximus convallis rhoncus inceptos per porta ipsum eu non habitant lacinia pellentesque. Ante et platea id at tempus magna orci etiam feugiat habitant conubia nascetur aenean. Purus nostra lectus lobortis etiam est lacus luctus laoreet sed ac lacinia quis at egestas class ridiculus litora eleifend urna porttitor enim.

\n

Faucibus felis integer eleifend in molestie eget platea tincidunt dui nec aliquam ultricies sodales quam porttitor facilisi potenti facilisis nisl vehicula tempus curae arcu. Magnis aliquet mi per mollis egestas porta montes ut pulvinar arcu neque adipiscing duis feugiat vel senectus quis facilisi elit nibh felis sodales ullamcorper diam sollicitudin ad. Venenatis hendrerit eget quisque sagittis facilisi quam non sociosqu curae enim, potenti augue dapibus ullamcorper auctor mi dignissim etiam viverra orci commodo laoreet inceptos pellentesque adipiscing class ac sem luctus faucibus fringilla. Urna ante in class auctor facilisi risus himenaeos, vitae malesuada dui mattis mollis aenean cras porttitor dignissim praesent egestas pretium condimentum aliquam. Arcu fringilla dictumst turpis vitae ex tempus vehicula efficitur maximus tincidunt pulvinar praesent nulla odio lacus ridiculus fames pharetra mauris ornare felis aenean penatibus taciti dignissim fusce diam orci vel. Habitant lectus primis risus nisi dolor erat senectus eros, felis varius sit phasellus quisque congue blandit bibendum ante est ligula nostra aliquet aptent magna purus. Pharetra eu hendrerit pulvinar magnis primis quisque integer in mus pellentesque suspendisse lacinia sem dictumst nisl auctor maximus platea.

\n

Nascetur tortor ac placerat facilisi integer litora sit varius duis efficitur sapien, hendrerit diam accumsan elit montes vehicula magnis consequat nostra justo parturient torquent pretium interdum a tincidunt dictum magna vel etiam ut dolor ullamcorper. Tincidunt aliquam lectus id velit class ad malesuada auctor litora consectetur aptent pharetra etiam dolor et tristique lacus.

\n

Non quis nullam urna ligula aptent curabitur odio lacus suspendisse lacinia molestie mus morbi elementum maximus interdum a purus enim sem sapien scelerisque lobortis et phasellus. Elementum vulputate vehicula posuere iaculis sodales fames urna rhoncus purus, laoreet metus ornare sem consequat mollis nibh lorem parturient adipiscing porttitor pretium placerat habitasse libero eleifend enim morbi. Massa tellus viverra nascetur leo aenean nisl vivamus malesuada at ipsum lobortis rutrum accumsan senectus dignissim elit fermentum integer praesent a purus proin faucibus aptent ad adipiscing imperdiet convallis mauris sodales. Sed iaculis mauris ut nunc fusce justo et venenatis libero litora eget aliquet penatibus gravida interdum phasellus turpis ullamcorper cubilia duis leo ex mattis vel cras donec lacinia sodales malesuada id elementum. Mus ultrices ullamcorper suspendisse nec dapibus senectus fermentum felis netus non magna congue neque bibendum dignissim ipsum aenean integer curae facilisi donec. Dolor purus nibh diam facilisis erat etiam mollis consectetur semper, vestibulum suscipit mauris egestas venenatis neque varius dignissim pulvinar ligula lobortis morbi aliquam eros nullam suspendisse orci tortor. Accumsan rutrum tempus arcu eros convallis vel natoque commodo eget diam mollis himenaeos proin placerat suspendisse duis taciti. Urna gravida a mus lacinia aliquam justo in lectus nec sed habitasse penatibus et ex massa vel commodo facilisis rutrum taciti odio torquent inceptos imperdiet sociosqu montes cursus nostra suscipit quam venenatis. Mattis nulla congue interdum gravida ornare ac sed, sagittis iaculis sem parturient netus proin maecenas dignissim rhoncus efficitur condimentum egestas dis litora. Odio imperdiet facilisi tempus ipsum donec tortor dictumst sem finibus parturient aptent molestie pretium risus sagittis pellentesque nisi litora congue cras viverra enim tempor vehicula platea.

\n

Sodales pretium egestas libero viverra lobortis interdum amet quis fames neque convallis dictumst sollicitudin eros felis nec. Nibh nisi sit nam magna elit fames habitasse sollicitudin libero lacus luctus porttitor enim conubia dolor suscipit aptent platea dictum habitant primis imperdiet taciti. Lobortis dui scelerisque feugiat venenatis vehicula tristique mi iaculis efficitur imperdiet aliquet sociosqu ipsum ornare sed gravida amet platea nisl mollis consectetur ex ac.

", email.Bodies["text/html"]) +} + +func TestMapEmailWithTextBody(t *testing.T) { + require := require.New(t) + + var elem map[string]any + err := json.Unmarshal([]byte(jmap_email_with_text_body), &elem) + require.NoError(err) + + logger := log.NopLogger() + + email, err := mapEmail(elem, true, &logger) + require.NoError(err) + require.Len(email.Bodies, 1) + require.Contains(email.Bodies, "text/plain") + require.Equal("Et magnis pulvinar congue aliquet tincidunt morbi lobortis mattis mus litora malesuada fringilla varius ullamcorper parturient fames accumsan faucibus erat. Magna id est cras eu a netus orci ridiculus lobortis urna dis ipsum at fermentum mi lacinia quis fames. Cursus ipsum gravida libero ultricies pretium montes rutrum suscipit tempor hac dapibus senectus commodo elementum leo nullam auctor litora pulvinar finibus. Ad nulla torquent quis mollis phasellus sodales aliquet lacinia varius, adipiscing enim habitant et netus egestas eu tempor malesuada mattis hac fusce integer diam inceptos venenatis turpis. Sem senectus aptent non dolor hendrerit magna mauris facilisis justo quam fringilla cursus gravida praesent malesuada taciti odio etiam magnis nostra vivamus. Tempus fames faucibus massa rutrum sit habitant morbi curabitur integer erat et condimentum tincidunt tempor libero vulputate maecenas rhoncus turpis congue a luctus aenean tristique lacinia cursus est fusce non mollis justo euismod facilisis egestas.\nAuctor maecenas vestibulum aenean accumsan eros ex potenti sociosqu, fusce sapien quis faucibus aliquam vivamus tristique hendrerit in per fermentum cras sodales curabitur scelerisque. Finibus metus adipiscing taciti eget rutrum vitae arcu torquent, dignissim at nibh venenatis facilisis molestie erat augue massa feugiat aliquam sollicitudin elementum cursus in. Est neque cras aenean felis justo euismod adipiscing magnis sagittis ut massa aliquet malesuada laoreet velit purus suspendisse bibendum pharetra litora ultrices diam ullamcorper volutpat venenatis egestas. Non laoreet eu interdum sodales phasellus morbi risus maecenas parturient auctor senectus urna ornare faucibus sociosqu habitant nisi cubilia viverra diam fames condimentum tempor scelerisque iaculis lacus elit feugiat adipiscing vivamus. Euismod volutpat gravida fames nascetur ridiculus iaculis habitasse vulputate habitant netus varius rhoncus ultrices porttitor himenaeos lorem libero congue turpis parturient quisque nostra aliquet in sem curabitur eleifend accumsan faucibus per pellentesque. Nibh auctor dictum vivamus eros ex gravida hac torquent purus suscipit fames lacus sagittis condimentum morbi dui litora cras duis iaculis massa porta praesent sapien. Ultricies tortor phasellus mus erat metus nisi malesuada augue sollicitudin convallis egestas ultrices arcu luctus tempus molestie facilisis nam scelerisque feugiat. Nibh imperdiet accumsan fermentum auctor et neque blandit elementum id eget justo suscipit interdum etiam mus tempus diam cursus nunc malesuada aliquam vestibulum. Arcu facilisi curae sed mi felis commodo, sapien neque aenean nullam rutrum torquent lectus fringilla rhoncus eros elit molestie.\nAptent fringilla cubilia sed duis non eu vulputate dis efficitur per ad venenatis dictumst egestas commodo blandit. Conubia finibus curae molestie egestas interdum mollis aliquam venenatis penatibus habitant varius natoque aptent nec. Mattis hac commodo integer donec gravida himenaeos vivamus primis rhoncus nam cursus erat nibh nascetur elementum felis duis in volutpat aliquet nulla vehicula placerat est. Placerat dis est aenean laoreet convallis metus sit mi, porttitor ullamcorper risus augue commodo dictumst nisi platea cubilia maximus elit volutpat hac rutrum suspendisse. Lacinia taciti justo non ligula vivamus aliquam luctus tellus dictumst vulputate interdum per aptent a metus eu mauris hac ex montes senectus blandit proin. Proin ullamcorper habitant justo pharetra felis commodo parturient scelerisque rutrum suspendisse ad ante cubilia pulvinar est vivamus quisque imperdiet vestibulum varius aliquam enim. Mollis aliquam metus montes dapibus volutpat maecenas fermentum massa tempor condimentum rhoncus lacinia tincidunt accumsan leo nunc elementum maximus fringilla dui augue.", email.Bodies["text/plain"]) +} diff --git a/pkg/jmap/jmap_well_known_client.go b/pkg/jmap/jmap_well_known_client.go index c96ca0bc17..0cdcbbd5fe 100644 --- a/pkg/jmap/jmap_well_known_client.go +++ b/pkg/jmap/jmap_well_known_client.go @@ -4,6 +4,6 @@ import ( "github.com/opencloud-eu/opencloud/pkg/log" ) -type JmapWellKnownClient interface { - GetWellKnown(username string, logger *log.Logger) (WellKnownJmap, error) +type WellKnownClient interface { + GetWellKnown(username string, logger *log.Logger) (WellKnownResponse, error) } diff --git a/services/graph/pkg/config/config.go b/services/graph/pkg/config/config.go index a91d306e7e..ca48036e8f 100644 --- a/services/graph/pkg/config/config.go +++ b/services/graph/pkg/config/config.go @@ -186,9 +186,11 @@ type MasterAuth struct { } type Mail struct { - Master MasterAuth `yaml:"master"` - BaseUrl string `yaml:"base_url" env:"OC_JMAP_BASE_URL;GROUPWARE_BASE_URL"` - JmapUrl string `yaml:"jmap_url" env:"OC_JMAP_JMAP_URL;GROUPWARE_JMAP_URL"` - Timeout time.Duration `yaml:"timeout" env:"OC_JMAP_TIMEOUT"` - ContextCacheTTL time.Duration `yaml:"context_cache_ttl" env:"OC_JMAP_CONTEXT_CACHE_TTL"` + Master MasterAuth `yaml:"master"` + BaseUrl string `yaml:"base_url" env:"GROUPWARE_BASE_URL"` + JmapUrl string `yaml:"jmap_url" env:"GROUPWARE_JMAP_URL"` + Timeout time.Duration `yaml:"timeout" env:"GROUPWARE_JMAP_TIMEOUT"` + SessionCacheTTL time.Duration `yaml:"session_cache_ttl" env:"GROUPWARE_SESSION_CACHE_TTL"` + DefaultEmailLimit int `yaml:"default_email_limit" env:"GROUPWARE_JMAP_DEFAULT_EMAIL_LIMIT"` + MaxBodyValueBytes int `yaml:"max_body_value_bytes" env:"GROUPWARE_JMAP_MAx_BODY_VALUE_BYTES"` } diff --git a/services/graph/pkg/config/defaults/defaultconfig.go b/services/graph/pkg/config/defaults/defaultconfig.go index 3333b6101a..55d066c27b 100644 --- a/services/graph/pkg/config/defaults/defaultconfig.go +++ b/services/graph/pkg/config/defaults/defaultconfig.go @@ -140,10 +140,12 @@ func DefaultConfig() *config.Config { Username: "master", Password: "admin", }, - BaseUrl: "https://stalwart.opencloud.test", - JmapUrl: "https://stalwart.opencloud.test/jmap", - Timeout: time.Duration(3 * time.Second), - ContextCacheTTL: time.Duration(1 * time.Hour), + BaseUrl: "https://stalwart.opencloud.test", + JmapUrl: "https://stalwart.opencloud.test/jmap", + Timeout: time.Duration(3 * time.Second), + SessionCacheTTL: time.Duration(1 * time.Hour), + DefaultEmailLimit: 100, + MaxBodyValueBytes: 0, }, } } diff --git a/services/graph/pkg/service/v0/groupware.go b/services/graph/pkg/service/v0/groupware.go index 529492da2c..aa8f2dec72 100644 --- a/services/graph/pkg/service/v0/groupware.go +++ b/services/graph/pkg/service/v0/groupware.go @@ -4,6 +4,7 @@ import ( "context" "crypto/tls" "net/http" + "strconv" "time" "github.com/opencloud-eu/opencloud/pkg/jmap" @@ -16,11 +17,23 @@ import ( "github.com/jellydator/ttlcache/v3" ) +const ( + logFolderId = "folder-id" + logQuery = "query" +) + type Groupware struct { - logger *log.Logger - jmapClient jmap.JmapClient - contextCache *ttlcache.Cache[string, jmap.JmapContext] - usernameProvider jmap.HttpJmapUsernameProvider // we also need it for ourselves for now + logger *log.Logger + jmapClient jmap.Client + sessionCache *ttlcache.Cache[string, jmap.Session] + usernameProvider jmap.HttpJmapUsernameProvider // we also need it for ourselves for now + defaultEmailLimit int + maxBodyValueBytes int +} + +type ItemBody struct { + Content string `json:"content"` + ContentType string `json:"contentType"` // text|html } type Message struct { @@ -30,6 +43,8 @@ type Message struct { HasAttachments bool `json:"hasAttachments"` InternetMessageId string `json:"InternetMessageId"` Subject string `json:"subject"` + BodyPreview string `json:"bodyPreview"` + Body ItemBody `json:"body"` } func NewGroupware(logger *log.Logger, config *config.Config) *Groupware { @@ -37,6 +52,8 @@ func NewGroupware(logger *log.Logger, config *config.Config) *Groupware { jmapUrl := config.Mail.JmapUrl masterUsername := config.Mail.Master.Username masterPassword := config.Mail.Master.Password + defaultEmailLimit := config.Mail.DefaultEmailLimit + maxBodyValueBytes := config.Mail.MaxBodyValueBytes tr := http.DefaultTransport.(*http.Transport).Clone() tr.ResponseHeaderTimeout = time.Duration(config.Mail.Timeout) @@ -56,38 +73,40 @@ func NewGroupware(logger *log.Logger, config *config.Config) *Groupware { masterPassword, ) - jmapClient := jmap.NewJmapClient(api, api) + jmapClient := jmap.NewClient(api, api) - loader := ttlcache.LoaderFunc[string, jmap.JmapContext]( - func(c *ttlcache.Cache[string, jmap.JmapContext], key string) *ttlcache.Item[string, jmap.JmapContext] { - jmapContext, err := jmapClient.FetchJmapContext(key, logger) + loader := ttlcache.LoaderFunc[string, jmap.Session]( + func(c *ttlcache.Cache[string, jmap.Session], key string) *ttlcache.Item[string, jmap.Session] { + jmapContext, err := jmapClient.FetchSession(key, logger) if err != nil { logger.Error().Err(err).Str("username", key).Msg("failed to retrieve well-known") return nil } - item := c.Set(key, jmapContext, config.Mail.ContextCacheTTL) + item := c.Set(key, jmapContext, config.Mail.SessionCacheTTL) return item }, ) - contextCache := ttlcache.New( - ttlcache.WithTTL[string, jmap.JmapContext]( - config.Mail.ContextCacheTTL, + sessionCache := ttlcache.New( + ttlcache.WithTTL[string, jmap.Session]( + config.Mail.SessionCacheTTL, ), - ttlcache.WithDisableTouchOnHit[string, jmap.JmapContext](), + ttlcache.WithDisableTouchOnHit[string, jmap.Session](), ttlcache.WithLoader(loader), ) - go contextCache.Start() + go sessionCache.Start() return &Groupware{ - logger: logger, - jmapClient: jmapClient, - contextCache: contextCache, - usernameProvider: jmapUsernameProvider, + logger: logger, + jmapClient: jmapClient, + sessionCache: sessionCache, + usernameProvider: jmapUsernameProvider, + defaultEmailLimit: defaultEmailLimit, + maxBodyValueBytes: maxBodyValueBytes, } } -func pickInbox(folders jmap.JmapFolders) string { +func pickInbox(folders jmap.Folders) string { for _, folder := range folders.Folders { if folder.Role == "inbox" { return folder.Id @@ -96,14 +115,14 @@ func pickInbox(folders jmap.JmapFolders) string { return "" } -func (g Groupware) context(ctx context.Context, logger *log.Logger) (jmap.JmapContext, error) { +func (g Groupware) session(ctx context.Context, logger *log.Logger) (jmap.Session, error) { username, err := g.usernameProvider.GetUsername(ctx, logger) if err != nil { logger.Error().Err(err).Msg("failed to retrieve username") - return jmap.JmapContext{}, err + return jmap.Session{}, err } - item := g.contextCache.Get(username) + item := g.sessionCache.Get(username) return item.Value(), nil } @@ -112,38 +131,109 @@ func (g Groupware) GetMessages(w http.ResponseWriter, r *http.Request) { logger := g.logger.SubloggerWithRequestID(ctx) - jmapContext, err := g.context(ctx, &logger) + session, err := g.session(ctx, &logger) if err != nil { - logger.Error().Err(err).Interface("query", r.URL.Query()).Msg("failed to determine Jmap context") + logger.Error().Err(err).Interface(logQuery, r.URL.Query()).Msg("failed to determine JMAP session") errorcode.ServiceNotAvailable.Render(w, r, http.StatusInternalServerError, err.Error()) return } - ctx = context.WithValue(ctx, jmap.ContextAccountId, jmapContext.AccountId) + ctx = session.DecorateSession(ctx) + logger = session.DecorateLogger(logger) + + offset, ok, _ := parseNumericParam(r, "$skip", 0) + if ok { + logger = log.Logger{Logger: logger.With().Int("$skip", offset).Logger()} + } + limit, ok, _ := parseNumericParam(r, "$top", g.defaultEmailLimit) + if ok { + logger = log.Logger{Logger: logger.With().Int("$top", limit).Logger()} + } logger.Debug().Msg("fetching folders") - folders, err := g.jmapClient.GetMailboxes(jmapContext, ctx, &logger) + folders, err := g.jmapClient.GetMailboxes(session, ctx, &logger) if err != nil { - logger.Error().Err(err).Interface("query", r.URL.Query()).Msg("could not retrieve mailboxes") + logger.Error().Err(err).Interface(logQuery, r.URL.Query()).Msg("could not retrieve mailboxes") errorcode.ServiceNotAvailable.Render(w, r, http.StatusInternalServerError, err.Error()) return } inboxId := pickInbox(folders) - logger.Debug().Str("mailboxId", inboxId).Msg("fetching emails from inbox") - emails, err := g.jmapClient.EmailQuery(jmapContext, ctx, &logger, inboxId) + // TODO handle not found + logger = log.Logger{Logger: logger.With().Str(logFolderId, inboxId).Logger()} + + logger.Debug().Msg("fetching emails from inbox") + emails, err := g.jmapClient.GetEmails(session, ctx, &logger, inboxId, offset, limit, true, g.maxBodyValueBytes) if err != nil { - logger.Error().Err(err).Interface("query", r.URL.Query()).Msg("could not retrieve emails from inbox") + logger.Error().Err(err).Interface(logQuery, r.URL.Query()).Msg("could not retrieve emails from inbox") errorcode.ServiceNotAvailable.Render(w, r, http.StatusInternalServerError, err.Error()) return } messages := make([]Message, 0, len(emails.Emails)) for _, email := range emails.Emails { - message := Message{Id: "todo", Subject: email.Subject} // TODO more email fields + message := message(email, logger) messages = append(messages, message) } render.Status(r, http.StatusOK) render.JSON(w, r, messages) } + +// https://learn.microsoft.com/en-us/graph/api/resources/message?view=graph-rest-1.0:w +func message(email jmap.Email, logger log.Logger) Message { + var body ItemBody + switch len(email.Bodies) { + case 0: + logger.Info().Msgf("zero bodies: %v", email) + case 1: + logger.Info().Msg("1 body") + for mime, content := range email.Bodies { + body = ItemBody{Content: content, ContentType: mime} + logger.Debug().Msgf("one body: %v", mime) + } + default: + content, ok := email.Bodies["text/html"] + if ok { + body = ItemBody{Content: content, ContentType: "text/html"} + logger.Info().Msgf("%v bodies: picked text/html", len(email.Bodies)) + } else { + content, ok = email.Bodies["text/plain"] + if ok { + body = ItemBody{Content: content, ContentType: "text/plain"} + logger.Info().Msgf("%v bodies: picked text/plain", len(email.Bodies)) + } else { + logger.Info().Msgf("%v bodies: neither text/html nor text/plain", len(email.Bodies)) + for mime, content := range email.Bodies { + body = ItemBody{Content: content, ContentType: mime} + logger.Info().Msgf("%v bodies: picked first: %v", len(email.Bodies), mime) + break + } + } + } + } + + return Message{ + Id: email.Id, + Subject: email.Subject, + CreatedDateTime: email.Received, + ReceivedDateTime: email.Received, + HasAttachments: email.HasAttachments, + InternetMessageId: email.MessageId, + BodyPreview: email.Preview, + Body: body, + } // TODO more email fields +} + +func parseNumericParam(r *http.Request, param string, defaultValue int) (int, bool, error) { + str := r.URL.Query().Get(param) + if str == "" { + return defaultValue, false, nil + } + + value, err := strconv.ParseInt(str, 10, 0) + if err != nil { + return defaultValue, false, nil + } + return int(value), true, nil +} diff --git a/services/groupware/pkg/service/http/v0/service.go b/services/groupware/pkg/service/http/v0/service.go index a011c3ccaf..4f026ab369 100644 --- a/services/groupware/pkg/service/http/v0/service.go +++ b/services/groupware/pkg/service/http/v0/service.go @@ -52,7 +52,7 @@ func NewService(opts ...Option) Service { } type Groupware struct { - jmapClient jmap.JmapClient + jmapClient jmap.Client usernameProvider jmap.HttpJmapUsernameProvider config *config.Config logger *log.Logger @@ -62,7 +62,7 @@ type Groupware struct { func NewGroupware(config *config.Config, logger *log.Logger, mux *chi.Mux) *Groupware { usernameProvider := jmap.NewRevaContextHttpJmapUsernameProvider() httpApiClient := httpApiClient(config, usernameProvider) - jmapClient := jmap.NewJmapClient(httpApiClient, httpApiClient) + jmapClient := jmap.NewClient(httpApiClient, httpApiClient) return &Groupware{ jmapClient: jmapClient, usernameProvider: usernameProvider, @@ -115,7 +115,7 @@ func (g Groupware) WellDefined(w http.ResponseWriter, r *http.Request) { return } - jmapContext, err := g.jmapClient.FetchJmapContext(username, &logger) + jmapContext, err := g.jmapClient.FetchSession(username, &logger) if err != nil { return }