mirror of
https://github.com/opencloud-eu/opencloud.git
synced 2026-02-06 20:32:06 -05:00
groupware and jmap improvements and refactoring
This commit is contained in:
@@ -1,10 +0,0 @@
|
||||
package jmap
|
||||
|
||||
import "time"
|
||||
|
||||
type Email struct {
|
||||
From string
|
||||
Subject string
|
||||
HasAttachments bool
|
||||
Received time.Time
|
||||
}
|
||||
@@ -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
|
||||
|
||||
182
pkg/jmap/jmap.go
182
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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
117
pkg/jmap/jmap_model_test.go
Normal file
117
pkg/jmap/jmap_model_test.go
Normal file
@@ -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)
|
||||
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
295
pkg/jmap/jmap_tools_test.go
Normal file
295
pkg/jmap/jmap_tools_test.go
Normal file
File diff suppressed because one or more lines are too long
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user