diff --git a/.idea/.gitignore b/.idea/.gitignore deleted file mode 100644 index 13566b8..0000000 --- a/.idea/.gitignore +++ /dev/null @@ -1,8 +0,0 @@ -# Default ignored files -/shelf/ -/workspace.xml -# Editor-based HTTP Client requests -/httpRequests/ -# Datasource local storage ignored files -/dataSources/ -/dataSources.local.xml diff --git a/.idea/discord.xml b/.idea/discord.xml deleted file mode 100644 index d8e9561..0000000 --- a/.idea/discord.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index 3ce3588..8a2c9e5 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,5 +1,9 @@ + + diff --git a/.idea/sqldialects.xml b/.idea/sqldialects.xml deleted file mode 100644 index 56782ca..0000000 --- a/.idea/sqldialects.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/api/api.go b/api/api.go index 9f98bf0..d7ffec6 100644 --- a/api/api.go +++ b/api/api.go @@ -3,7 +3,8 @@ package api import ( "encoding/json" "github.com/1f349/lotus/imap" - json2 "github.com/1f349/lotus/imap/json" + "github.com/1f349/lotus/imap/marshal" + imap2 "github.com/emersion/go-imap" "github.com/julienschmidt/httprouter" "log" "net/http" @@ -14,18 +15,105 @@ func SetupApiServer(listen string, auth func(callback AuthCallback) httprouter.H r := httprouter.New() // === ACCOUNT === - r.GET("/account", auth(func(rw http.ResponseWriter, req *http.Request, params httprouter.Params, b AuthClaims) { + r.GET("/identities", auth(func(rw http.ResponseWriter, req *http.Request, params httprouter.Params, b AuthClaims) { // TODO(melon): find users aliases and other account data })) // === SMTP === r.POST("/message", auth(MessageSender(send))) + r.Handle(http.MethodConnect, "/", func(rw http.ResponseWriter, req *http.Request, params httprouter.Params) { + + }) + // === IMAP === - type statusJson struct { + type mailboxStatusJson struct { Folder string `json:"folder"` } - r.GET("/status", auth(imapClient(recv, func(rw http.ResponseWriter, req *http.Request, params httprouter.Params, cli *imap.Client, t statusJson) { + r.GET("/mailbox/status", auth(imapClient(recv, func(rw http.ResponseWriter, req *http.Request, params httprouter.Params, cli *imap.Client, t mailboxStatusJson) error { + status, err := cli.Status(t.Folder) + if err != nil { + return err + } + return json.NewEncoder(rw).Encode(status) + }))) + + type mailboxListJson struct { + Folder string `json:"folder"` + Pattern string `json:"pattern"` + } + r.GET("/mailbox/list", auth(imapClient(recv, func(rw http.ResponseWriter, req *http.Request, params httprouter.Params, cli *imap.Client, t mailboxListJson) error { + list, err := cli.List(t.Folder, t.Pattern) + if err != nil { + return err + } + return json.NewEncoder(rw).Encode(list) + }))) + + type mailboxCreateJson struct { + Name string `json:"name"` + } + r.POST("/mailbox/create", auth(imapClient(recv, func(rw http.ResponseWriter, req *http.Request, params httprouter.Params, cli *imap.Client, t mailboxCreateJson) error { + err := cli.Create(t.Name) + if err != nil { + return err + } + return json.NewEncoder(rw).Encode(map[string]string{"Status": "OK"}) + }))) + + type messagesListJson struct { + Folder string `json:"folder"` + Start uint32 `json:"start"` + End uint32 `json:"end"` + Limit uint32 `json:"limit"` + } + r.GET("/list-messages", auth(imapClient(recv, func(rw http.ResponseWriter, req *http.Request, params httprouter.Params, cli *imap.Client, t messagesListJson) error { + messages, err := cli.Fetch(t.Folder, t.Start, t.End, t.Limit) + if err != nil { + return err + } + return json.NewEncoder(rw).Encode(marshal.MessageSliceJson(messages)) + }))) + + type messagesSearchJson struct { + Folder string `json:"folder"` + SeqNum imap2.SeqSet + Uid imap2.SeqSet + Since time.Time + Before time.Time + SentSince time.Time + SentBefore time.Time + Body []string + Text []string + WithFlags []string + WithoutFlags []string + Larger uint32 + Smaller uint32 + } + r.GET("/search-messages", auth(imapClient(recv, func(rw http.ResponseWriter, req *http.Request, params httprouter.Params, cli *imap.Client, t messagesSearchJson) error { + status, err := cli.Search(&imap2.SearchCriteria{ + SeqNum: t.SeqNum, + Uid: t.Uid, + Since: time.Time{}, + Before: time.Time{}, + SentSince: time.Time{}, + SentBefore: time.Time{}, + Header: nil, + Body: nil, + Text: nil, + WithFlags: nil, + WithoutFlags: nil, + Larger: 0, + Smaller: 0, + Not: nil, + Or: nil, + }) + if err != nil { + return err + } + return json.NewEncoder(rw).Encode(status) + }))) + r.POST("/update-messages-flags", auth(imapClient(recv, func(rw http.ResponseWriter, req *http.Request, params httprouter.Params, cli *imap.Client, t mailboxStatusJson) { status, err := cli.Status(t.Folder) if err != nil { rw.WriteHeader(http.StatusForbidden) @@ -39,7 +127,7 @@ func SetupApiServer(listen string, auth func(callback AuthCallback) httprouter.H rw.WriteHeader(http.StatusForbidden) return } - err = json.NewEncoder(rw).Encode(json2.ListMessagesJson(messages)) + err = json.NewEncoder(rw).Encode(marshal.ListMessagesJson(messages)) if err != nil { log.Println("list-messages json encode error:", err) } @@ -96,7 +184,7 @@ func apiError(rw http.ResponseWriter, code int, m string) { }) } -type IcCallback[T any] func(rw http.ResponseWriter, req *http.Request, params httprouter.Params, cli *imap.Client, t T) +type IcCallback[T any] func(rw http.ResponseWriter, req *http.Request, params httprouter.Params, cli *imap.Client, t T) error func imapClient[T any](recv Imap, cb IcCallback[T]) AuthCallback { return func(rw http.ResponseWriter, req *http.Request, params httprouter.Params, b AuthClaims) { @@ -114,6 +202,9 @@ func imapClient[T any](recv Imap, cb IcCallback[T]) AuthCallback { rw.WriteHeader(http.StatusInternalServerError) return } - cb(rw, req, params, cli, t) + err = cb(rw, req, params, cli, t) + if err != nil { + log.Println("[ImapClient] Error:", err) + } } } diff --git a/imap/marshal/message.go b/imap/marshal/message.go new file mode 100644 index 0000000..1c87a72 --- /dev/null +++ b/imap/marshal/message.go @@ -0,0 +1,38 @@ +package marshal + +import ( + "encoding/json" + "github.com/emersion/go-imap" +) + +var _, _ json.Marshaler = &MessageSliceJson{}, &MessageJson{} + +type MessageSliceJson []*imap.Message + +func (m MessageSliceJson) MarshalJSON() ([]byte, error) { + a := make([]MessageJson, len(m)) + for i := range a { + a[i] = MessageJson(*m[i]) + } + return json.Marshal(a) +} + +type MessageJson imap.Message + +func (m MessageJson) MarshalJSON() ([]byte, error) { + body := make(map[string]imap.Literal, len(m.Body)) + for k, v := range m.Body { + body[string(k.FetchItem())] = v + } + return json.Marshal(map[string]any{ + "SeqNum": m.SeqNum, + "Items": m.Items, + "Envelope": m.Envelope, + "BodyStructure": m.BodyStructure, + "Flags": m.Flags, + "InternalDate": m.InternalDate, + "Size": m.Size, + "Uid": m.Uid, + "$Body": body, + }) +} diff --git a/imap/marshal/seqset.go b/imap/marshal/seqset.go new file mode 100644 index 0000000..8f36c1d --- /dev/null +++ b/imap/marshal/seqset.go @@ -0,0 +1,7 @@ +package marshal + +import imap2 "github.com/emersion/go-imap" + +type SeqSet imap2.SeqSet + +var json.mar