Add a form to set message flags
References: https://todo.sr.ht/~sircmpwn/koushin/36
This commit is contained in:
parent
3aea768cad
commit
2a2a48c7fa
6 changed files with 137 additions and 28 deletions
2
go.mod
2
go.mod
|
@ -3,7 +3,7 @@ module git.sr.ht/~emersion/koushin
|
||||||
go 1.13
|
go 1.13
|
||||||
|
|
||||||
require (
|
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-imap-move v0.0.0-20190710073258-6e5a51a5b342
|
||||||
github.com/emersion/go-message v0.10.8
|
github.com/emersion/go-message v0.10.8
|
||||||
github.com/emersion/go-sasl v0.0.0-20191210011802-430746ea8b9b
|
github.com/emersion/go-sasl v0.0.0-20191210011802-430746ea8b9b
|
||||||
|
|
2
go.sum
2
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/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 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.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 h1:5p1t3e1PomYgLWwEwhwEU5kVBwcyAcVrOpexv8AeZx0=
|
||||||
github.com/emersion/go-imap-move v0.0.0-20190710073258-6e5a51a5b342/go.mod h1:QuMaZcKFDVI0yCrnAbPLfbwllz1wtOrZH8/vZ5yzp4w=
|
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=
|
github.com/emersion/go-message v0.10.8 h1:1l1Vb+0By9U1ITTH3FgKfJQWQ9sTI3N1smPe6SS3QXY=
|
||||||
|
|
|
@ -174,6 +174,15 @@ func (msg *IMAPMessage) PartTree() *IMAPPartNode {
|
||||||
return imapPartTree(msg.BodyStructure, nil)
|
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) {
|
func listMessages(conn *imapclient.Client, mboxName string, page int) ([]IMAPMessage, error) {
|
||||||
if err := ensureMailboxSelected(conn, mboxName); err != nil {
|
if err := ensureMailboxSelected(conn, mboxName); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -295,6 +304,7 @@ func getMessagePart(conn *imapclient.Client, mboxName string, uid uint32, partPa
|
||||||
imap.FetchEnvelope,
|
imap.FetchEnvelope,
|
||||||
imap.FetchUid,
|
imap.FetchUid,
|
||||||
imap.FetchBodyStructure,
|
imap.FetchBodyStructure,
|
||||||
|
imap.FetchFlags,
|
||||||
partHeaderSection.FetchItem(),
|
partHeaderSection.FetchItem(),
|
||||||
partBodySection.FetchItem(),
|
partBodySection.FetchItem(),
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,6 +30,25 @@
|
||||||
<input type="submit" value="Delete">
|
<input type="submit" value="Delete">
|
||||||
</form>
|
</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>
|
<ul>
|
||||||
<li>
|
<li>
|
||||||
<strong>Date</strong>: {{.Message.Envelope.Date | formatdate}}
|
<strong>Date</strong>: {{.Message.Envelope.Date | formatdate}}
|
||||||
|
@ -67,7 +86,7 @@
|
||||||
{{.String}}
|
{{.String}}
|
||||||
{{if eq $.PartPath .PathString}}</strong>{{end}}
|
{{if eq $.PartPath .PathString}}</strong>{{end}}
|
||||||
</a>
|
</a>
|
||||||
{{if gt (len .Children) 0}}
|
{{if .Children}}
|
||||||
<ul>
|
<ul>
|
||||||
{{range .Children}}
|
{{range .Children}}
|
||||||
<li>{{template "message-part-tree" (tuple $ .)}}</li>
|
<li>{{template "message-part-tree" (tuple $ .)}}</li>
|
||||||
|
|
|
@ -18,6 +18,37 @@ import (
|
||||||
"github.com/labstack/echo/v4"
|
"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 {
|
type MailboxRenderData struct {
|
||||||
koushin.RenderData
|
koushin.RenderData
|
||||||
Mailbox *imap.MailboxStatus
|
Mailbox *imap.MailboxStatus
|
||||||
|
@ -127,6 +158,7 @@ type MessageRenderData struct {
|
||||||
Body string
|
Body string
|
||||||
PartPath string
|
PartPath string
|
||||||
MailboxPage int
|
MailboxPage int
|
||||||
|
Flags map[string]bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleGetPart(ctx *koushin.Context, raw bool) error {
|
func handleGetPart(ctx *koushin.Context, raw bool) error {
|
||||||
|
@ -193,6 +225,15 @@ func handleGetPart(ctx *koushin.Context, raw bool) error {
|
||||||
body = string(b)
|
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{
|
return ctx.Render(http.StatusOK, "message.html", &MessageRenderData{
|
||||||
RenderData: *koushin.NewRenderData(ctx),
|
RenderData: *koushin.NewRenderData(ctx),
|
||||||
Mailboxes: mailboxes,
|
Mailboxes: mailboxes,
|
||||||
|
@ -201,6 +242,7 @@ func handleGetPart(ctx *koushin.Context, raw bool) error {
|
||||||
Body: body,
|
Body: body,
|
||||||
PartPath: partPathString,
|
PartPath: partPathString,
|
||||||
MailboxPage: int(mbox.Messages-msg.SeqNum) / messagesPerPage,
|
MailboxPage: int(mbox.Messages-msg.SeqNum) / messagesPerPage,
|
||||||
|
Flags: flags,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -336,7 +378,7 @@ func handleMove(ectx echo.Context) error {
|
||||||
return err
|
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 {
|
func handleDelete(ectx echo.Context) error {
|
||||||
|
@ -377,34 +419,48 @@ func handleDelete(ectx echo.Context) error {
|
||||||
return err
|
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) {
|
func handleSetFlags(ectx echo.Context) error {
|
||||||
p.GET("/mailbox/:mbox", handleGetMailbox)
|
|
||||||
p.POST("/mailbox/:mbox", handleGetMailbox)
|
|
||||||
|
|
||||||
p.GET("/message/:mbox/:uid", func(ectx echo.Context) error {
|
|
||||||
ctx := ectx.(*koushin.Context)
|
ctx := ectx.(*koushin.Context)
|
||||||
return handleGetPart(ctx, false)
|
|
||||||
})
|
mboxName, uid, err := parseMboxAndUid(ctx.Param("mbox"), ctx.Param("uid"))
|
||||||
p.GET("/message/:mbox/:uid/raw", func(ectx echo.Context) error {
|
if err != nil {
|
||||||
ctx := ectx.(*koushin.Context)
|
return echo.NewHTTPError(http.StatusBadRequest, err)
|
||||||
return handleGetPart(ctx, true)
|
}
|
||||||
|
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
|
||||||
|
err = ctx.Session.DoIMAP(func(c *imapclient.Client) error {
|
||||||
|
if err := ensureMailboxSelected(c, mboxName); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var seqSet imap.SeqSet
|
||||||
|
seqSet.AddNum(uid)
|
||||||
|
|
||||||
|
storeItems := make([]interface{}, len(flags))
|
||||||
|
for i, f := range flags {
|
||||||
|
storeItems[i] = f
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
})
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
p.GET("/login", handleLogin)
|
return ctx.Redirect(http.StatusFound, fmt.Sprintf("/message/%v/%v", url.PathEscape(mboxName), uid))
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,4 +26,26 @@ var templateFuncs = template.FuncMap{
|
||||||
"formatdate": func(t time.Time) string {
|
"formatdate": func(t time.Time) string {
|
||||||
return t.Format("Mon Jan 02 15:04")
|
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
|
||||||
|
}
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue