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