aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--go.mod2
-rw-r--r--go.sum2
-rw-r--r--plugins/base/imap.go10
-rw-r--r--plugins/base/public/message.html21
-rw-r--r--plugins/base/routes.go100
-rw-r--r--plugins/base/template.go22
6 files changed, 133 insertions, 24 deletions
diff --git a/go.mod b/go.mod
index 5b738dc..8a54e91 100644
--- a/go.mod
+++ b/go.mod
@@ -3,7 +3,7 @@ module git.sr.ht/~emersion/koushin
go 1.13
require (
- github.com/emersion/go-imap v1.0.3-0.20191213134403-f1c945935a36
+ github.com/emersion/go-imap v1.0.3-0.20191217110750-414e9a7e3dd8
github.com/emersion/go-imap-move v0.0.0-20190710073258-6e5a51a5b342
github.com/emersion/go-message v0.10.8
github.com/emersion/go-sasl v0.0.0-20191210011802-430746ea8b9b
diff --git a/go.sum b/go.sum
index 585c322..36e7fba 100644
--- a/go.sum
+++ b/go.sum
@@ -8,6 +8,8 @@ github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumC
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/emersion/go-imap v1.0.3-0.20191213134403-f1c945935a36 h1:CrPKMqbfsFwzOFqqXd43j40NfCKJUgm6niLJhklQkrA=
github.com/emersion/go-imap v1.0.3-0.20191213134403-f1c945935a36/go.mod h1:TjT+1ncDso8j/VXeUHcZeQknho5hjyQLqEIybJJjjDI=
+github.com/emersion/go-imap v1.0.3-0.20191217110750-414e9a7e3dd8 h1:gpT/I+R/CWdn6XqSTanqWRRXaqVP0tCCEkqldtMWCxs=
+github.com/emersion/go-imap v1.0.3-0.20191217110750-414e9a7e3dd8/go.mod h1:TjT+1ncDso8j/VXeUHcZeQknho5hjyQLqEIybJJjjDI=
github.com/emersion/go-imap-move v0.0.0-20190710073258-6e5a51a5b342 h1:5p1t3e1PomYgLWwEwhwEU5kVBwcyAcVrOpexv8AeZx0=
github.com/emersion/go-imap-move v0.0.0-20190710073258-6e5a51a5b342/go.mod h1:QuMaZcKFDVI0yCrnAbPLfbwllz1wtOrZH8/vZ5yzp4w=
github.com/emersion/go-message v0.10.8 h1:1l1Vb+0By9U1ITTH3FgKfJQWQ9sTI3N1smPe6SS3QXY=
diff --git a/plugins/base/imap.go b/plugins/base/imap.go
index 3ccc73a..433a63e 100644
--- a/plugins/base/imap.go
+++ b/plugins/base/imap.go
@@ -174,6 +174,15 @@ func (msg *IMAPMessage) PartTree() *IMAPPartNode {
return imapPartTree(msg.BodyStructure, nil)
}
+func (msg *IMAPMessage) HasFlag(flag string) bool {
+ for _, f := range msg.Flags {
+ if imap.CanonicalFlag(f) == flag {
+ return true
+ }
+ }
+ return false
+}
+
func listMessages(conn *imapclient.Client, mboxName string, page int) ([]IMAPMessage, error) {
if err := ensureMailboxSelected(conn, mboxName); err != nil {
return nil, err
@@ -295,6 +304,7 @@ func getMessagePart(conn *imapclient.Client, mboxName string, uid uint32, partPa
imap.FetchEnvelope,
imap.FetchUid,
imap.FetchBodyStructure,
+ imap.FetchFlags,
partHeaderSection.FetchItem(),
partBodySection.FetchItem(),
}
diff --git a/plugins/base/public/message.html b/plugins/base/public/message.html
index 0b7a8d5..56e6e6f 100644
--- a/plugins/base/public/message.html
+++ b/plugins/base/public/message.html
@@ -30,6 +30,25 @@
<input type="submit" value="Delete">
</form>
+{{if .Flags}}
+ <form method="post" action="{{.Message.Uid}}/flag">
+ <p>Flags:</p>
+ {{range $name, $has := .Flags}}
+ {{if ismutableflag $name}}
+ <input type="checkbox" name="flags" id="flag-{{$name}}"
+ value="{{$name}}" {{if $has}}checked{{end}}>
+ <label for="flag-{{$name}}">{{$name | formatflag}}</label>
+ <br>
+ {{else}}
+ {{if $has}}
+ <input type="hidden" name="flags" value="{{$name}}">
+ {{end}}
+ {{end}}
+ {{end}}
+ <input type="submit" value="Set flags">
+ </form>
+{{end}}
+
<ul>
<li>
<strong>Date</strong>: {{.Message.Envelope.Date | formatdate}}
@@ -67,7 +86,7 @@
{{.String}}
{{if eq $.PartPath .PathString}}</strong>{{end}}
</a>
- {{if gt (len .Children) 0}}
+ {{if .Children}}
<ul>
{{range .Children}}
<li>{{template "message-part-tree" (tuple $ .)}}</li>
diff --git a/plugins/base/routes.go b/plugins/base/routes.go
index c0f5817..2c8d653 100644
--- a/plugins/base/routes.go
+++ b/plugins/base/routes.go
@@ -18,6 +18,37 @@ import (
"github.com/labstack/echo/v4"
)
+func registerRoutes(p *koushin.GoPlugin) {
+ p.GET("/mailbox/:mbox", handleGetMailbox)
+ p.POST("/mailbox/:mbox", handleGetMailbox)
+
+ p.GET("/message/:mbox/:uid", func(ectx echo.Context) error {
+ ctx := ectx.(*koushin.Context)
+ return handleGetPart(ctx, false)
+ })
+ p.GET("/message/:mbox/:uid/raw", func(ectx echo.Context) error {
+ ctx := ectx.(*koushin.Context)
+ return handleGetPart(ctx, true)
+ })
+
+ p.GET("/login", handleLogin)
+ p.POST("/login", handleLogin)
+
+ p.GET("/logout", handleLogout)
+
+ p.GET("/compose", handleCompose)
+ p.POST("/compose", handleCompose)
+
+ p.GET("/message/:mbox/:uid/reply", handleCompose)
+ p.POST("/message/:mbox/:uid/reply", handleCompose)
+
+ p.POST("/message/:mbox/:uid/move", handleMove)
+
+ p.POST("/message/:mbox/:uid/delete", handleDelete)
+
+ p.POST("/message/:mbox/:uid/flag", handleSetFlags)
+}
+
type MailboxRenderData struct {
koushin.RenderData
Mailbox *imap.MailboxStatus
@@ -127,6 +158,7 @@ type MessageRenderData struct {
Body string
PartPath string
MailboxPage int
+ Flags map[string]bool
}
func handleGetPart(ctx *koushin.Context, raw bool) error {
@@ -193,6 +225,15 @@ func handleGetPart(ctx *koushin.Context, raw bool) error {
body = string(b)
}
+ flags := make(map[string]bool)
+ for _, f := range mbox.PermanentFlags {
+ f = imap.CanonicalFlag(f)
+ if f == imap.TryCreateFlag {
+ continue
+ }
+ flags[f] = msg.HasFlag(f)
+ }
+
return ctx.Render(http.StatusOK, "message.html", &MessageRenderData{
RenderData: *koushin.NewRenderData(ctx),
Mailboxes: mailboxes,
@@ -201,6 +242,7 @@ func handleGetPart(ctx *koushin.Context, raw bool) error {
Body: body,
PartPath: partPathString,
MailboxPage: int(mbox.Messages-msg.SeqNum) / messagesPerPage,
+ Flags: flags,
})
}
@@ -336,7 +378,7 @@ func handleMove(ectx echo.Context) error {
return err
}
- return ctx.Redirect(http.StatusFound, fmt.Sprintf("/mailbox/%v", to))
+ return ctx.Redirect(http.StatusFound, fmt.Sprintf("/mailbox/%v", url.PathEscape(to)))
}
func handleDelete(ectx echo.Context) error {
@@ -377,34 +419,48 @@ func handleDelete(ectx echo.Context) error {
return err
}
- return ctx.Redirect(http.StatusFound, fmt.Sprintf("/mailbox/%v", mboxName))
+ return ctx.Redirect(http.StatusFound, fmt.Sprintf("/mailbox/%v", url.PathEscape(mboxName)))
}
-func registerRoutes(p *koushin.GoPlugin) {
- p.GET("/mailbox/:mbox", handleGetMailbox)
- p.POST("/mailbox/:mbox", handleGetMailbox)
+func handleSetFlags(ectx echo.Context) error {
+ ctx := ectx.(*koushin.Context)
- p.GET("/message/:mbox/:uid", func(ectx echo.Context) error {
- ctx := ectx.(*koushin.Context)
- return handleGetPart(ctx, false)
- })
- p.GET("/message/:mbox/:uid/raw", func(ectx echo.Context) error {
- ctx := ectx.(*koushin.Context)
- return handleGetPart(ctx, true)
- })
+ mboxName, uid, err := parseMboxAndUid(ctx.Param("mbox"), ctx.Param("uid"))
+ if err != nil {
+ return echo.NewHTTPError(http.StatusBadRequest, err)
+ }
- p.GET("/login", handleLogin)
- p.POST("/login", handleLogin)
+ if err := ctx.Request().ParseForm(); err != nil {
+ return echo.NewHTTPError(http.StatusBadRequest, err)
+ }
+ flags, ok := ctx.Request().Form["flags"]
+ if !ok {
+ return echo.NewHTTPError(http.StatusBadRequest, "missing 'flags' form values")
+ }
- p.GET("/logout", handleLogout)
+ err = ctx.Session.DoIMAP(func(c *imapclient.Client) error {
+ if err := ensureMailboxSelected(c, mboxName); err != nil {
+ return err
+ }
- p.GET("/compose", handleCompose)
- p.POST("/compose", handleCompose)
+ var seqSet imap.SeqSet
+ seqSet.AddNum(uid)
- p.GET("/message/:mbox/:uid/reply", handleCompose)
- p.POST("/message/:mbox/:uid/reply", handleCompose)
+ storeItems := make([]interface{}, len(flags))
+ for i, f := range flags {
+ storeItems[i] = f
+ }
- p.POST("/message/:mbox/:uid/move", handleMove)
+ item := imap.FormatFlagsOp(imap.SetFlags, true)
+ if err := c.UidStore(&seqSet, item, storeItems, nil); err != nil {
+ return fmt.Errorf("failed to add deleted flag: %v", err)
+ }
- p.POST("/message/:mbox/:uid/delete", handleDelete)
+ return nil
+ })
+ if err != nil {
+ return err
+ }
+
+ return ctx.Redirect(http.StatusFound, fmt.Sprintf("/message/%v/%v", url.PathEscape(mboxName), uid))
}
diff --git a/plugins/base/template.go b/plugins/base/template.go
index 84c4e21..714ca3c 100644
--- a/plugins/base/template.go
+++ b/plugins/base/template.go
@@ -26,4 +26,26 @@ var templateFuncs = template.FuncMap{
"formatdate": func(t time.Time) string {
return t.Format("Mon Jan 02 15:04")
},
+ "formatflag": func(flag string) string {
+ switch flag {
+ case imap.SeenFlag:
+ return "Seen"
+ case imap.AnsweredFlag:
+ return "Answered"
+ case imap.FlaggedFlag:
+ return "Starred"
+ case imap.DraftFlag:
+ return "Draft"
+ default:
+ return flag
+ }
+ },
+ "ismutableflag": func(flag string) bool {
+ switch flag {
+ case imap.AnsweredFlag, imap.DeletedFlag, imap.DraftFlag:
+ return false
+ default:
+ return true
+ }
+ },
}