diff options
author | 2023-03-20 12:46:26 +0100 | |
---|---|---|
committer | 2023-04-30 16:18:12 +0200 | |
commit | f1d00df4dda7a35651359b76bad34a9d1262994d (patch) | |
tree | 18d9d8f37cb2adfdd004457caf6e41d7cdc2b9a9 | |
parent | eeedc9a387117ebb8cb9ee4d38eccbbf240783d0 (diff) | |
download | alps-f1d00df4dda7a35651359b76bad34a9d1262994d.tar.gz alps-f1d00df4dda7a35651359b76bad34a9d1262994d.tar.bz2 alps-f1d00df4dda7a35651359b76bad34a9d1262994d.zip |
Upgrade to go-imap v2
-rw-r--r-- | docs/example-go-plugin/plugin.go | 2 | ||||
-rw-r--r-- | go.mod | 5 | ||||
-rw-r--r-- | go.sum | 19 | ||||
-rw-r--r-- | imap.go | 29 | ||||
-rw-r--r-- | plugins/base/imap.go | 383 | ||||
-rw-r--r-- | plugins/base/routes.go | 136 | ||||
-rw-r--r-- | plugins/base/search.go | 28 | ||||
-rw-r--r-- | plugins/base/template.go | 18 | ||||
-rw-r--r-- | server.go | 2 | ||||
-rw-r--r-- | session.go | 34 | ||||
-rw-r--r-- | store.go | 34 | ||||
-rw-r--r-- | themes/alps/mailbox.html | 12 | ||||
-rw-r--r-- | themes/alps/message.html | 18 | ||||
-rw-r--r-- | themes/alps/util.html | 6 |
14 files changed, 352 insertions, 374 deletions
diff --git a/docs/example-go-plugin/plugin.go b/docs/example-go-plugin/plugin.go index 90877df..bd92447 100644 --- a/docs/example-go-plugin/plugin.go +++ b/docs/example-go-plugin/plugin.go @@ -17,7 +17,7 @@ func init() { // Setup a function called when the mailbox view is rendered p.Inject("mailbox.html", func(ctx *alps.Context, kdata alps.RenderData) error { data := kdata.(*alpsbase.MailboxRenderData) - fmt.Println("The mailbox view for " + data.Mailbox.Name + " is being rendered") + fmt.Println("The mailbox view for " + data.Mailbox.Name() + " is being rendered") // Set extra data that can be accessed from the mailbox.html template data.Extra["Example"] = "Hi from Go" return nil @@ -7,10 +7,7 @@ require ( github.com/chris-ramon/douceur v0.2.0 github.com/dustin/go-humanize v1.0.1 github.com/emersion/go-ical v0.0.0-20220601085725-0864dccc089f - github.com/emersion/go-imap v1.2.1 - github.com/emersion/go-imap-metadata v0.0.0-20200128185110-9d939d2a0915 - github.com/emersion/go-imap-move v0.0.0-20210907172020-fe4558f9c872 - github.com/emersion/go-imap-specialuse v0.0.0-20201101201809-1ab93d3d150e + github.com/emersion/go-imap/v2 v2.0.0-alpha.4 github.com/emersion/go-message v0.16.0 github.com/emersion/go-sasl v0.0.0-20220912192320-0145f2c60ead github.com/emersion/go-smtp v0.16.0 @@ -12,26 +12,15 @@ github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkp github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/emersion/go-ical v0.0.0-20220601085725-0864dccc089f h1:feGUUxxvOtWVOhTko8Cbmp33a+tU0IMZxMEmnkoAISQ= github.com/emersion/go-ical v0.0.0-20220601085725-0864dccc089f/go.mod h1:2MKFUgfNMULRxqZkadG1Vh44we3y5gJAtTBlVsx1BKQ= -github.com/emersion/go-imap v1.0.3/go.mod h1:yKASt+C3ZiDAiCSssxg9caIckWF/JG7ZQTO7GAmvicU= -github.com/emersion/go-imap v1.2.1 h1:+s9ZjMEjOB8NzZMVTM3cCenz2JrQIGGo5j1df19WjTA= -github.com/emersion/go-imap v1.2.1/go.mod h1:Qlx1FSx2FTxjnjWpIlVNEuX+ylerZQNFE5NsmKFSejY= -github.com/emersion/go-imap-metadata v0.0.0-20200128185110-9d939d2a0915 h1:8xzODjLqrfAJo+CNhX0Fp47vdVN0ZvmGV3CPt/Ex1nU= -github.com/emersion/go-imap-metadata v0.0.0-20200128185110-9d939d2a0915/go.mod h1:6mXMzbK9Ts0mrrBibqy56SqZpuFMry5AedTgu6qY5zM= -github.com/emersion/go-imap-move v0.0.0-20210907172020-fe4558f9c872 h1:HGBfonz0q/zq7y3ew+4oy4emHSvk6bkmV0mdDG3E77M= -github.com/emersion/go-imap-move v0.0.0-20210907172020-fe4558f9c872/go.mod h1:QuMaZcKFDVI0yCrnAbPLfbwllz1wtOrZH8/vZ5yzp4w= -github.com/emersion/go-imap-specialuse v0.0.0-20201101201809-1ab93d3d150e h1:AwVkRMFFUMNu+tx0jchwyoXhS2VClQSzTtByVuzxbsE= -github.com/emersion/go-imap-specialuse v0.0.0-20201101201809-1ab93d3d150e/go.mod h1:/nybxhI8kXom8Tw6BrHMl42usALvka6meORflnnYwe4= -github.com/emersion/go-message v0.11.1/go.mod h1:C4jnca5HOTo4bGN9YdqNQM9sITuT3Y0K6bSUw9RklvY= -github.com/emersion/go-message v0.15.0/go.mod h1:wQUEfE+38+7EW8p8aZ96ptg6bAb1iwdgej19uXASlE4= +github.com/emersion/go-imap/v2 v2.0.0-alpha.4 h1:zaSLXl0TmWMg/ddcn9nwGD0scb8j+Rk7Yul/oVFrYVw= +github.com/emersion/go-imap/v2 v2.0.0-alpha.4/go.mod h1:NQQIs7aGbZC7CuvEp9yfidW2TCstC3rUIo4k8LbqxzA= github.com/emersion/go-message v0.16.0 h1:uZLz8ClLv3V5fSFF/fFdW9jXjrZkXIpE1Fn8fKx7pO4= github.com/emersion/go-message v0.16.0/go.mod h1:pDJDgf/xeUIF+eicT6B/hPX/ZbEorKkUMPOxrPVG2eQ= -github.com/emersion/go-sasl v0.0.0-20191210011802-430746ea8b9b/go.mod h1:G/dpzLu16WtQpBfQ/z3LYiYJn3ZhKSGWn83fyoyQe/k= github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ= github.com/emersion/go-sasl v0.0.0-20220912192320-0145f2c60ead h1:fI1Jck0vUrXT8bnphprS1EoVRe2Q5CKCX8iDlpqjQ/Y= github.com/emersion/go-sasl v0.0.0-20220912192320-0145f2c60ead/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ= github.com/emersion/go-smtp v0.16.0 h1:eB9CY9527WdEZSs5sWisTmilDX7gG+Q/2IdRcmubpa8= github.com/emersion/go-smtp v0.16.0/go.mod h1:qm27SGYgoIPRot6ubfQ/GpiPy/g3PaZAVRxiO/sDUgQ= -github.com/emersion/go-textwrapper v0.0.0-20160606182133-d0e65e56babe/go.mod h1:aqO8z8wPrjkscevZJFVE1wXJrLpC5LtJG7fqLOsPb2U= github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594 h1:IbFBtwoTQyw0fIM5xv1HF+Y+3ZijDR839WMulgxCcUY= github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594/go.mod h1:aqO8z8wPrjkscevZJFVE1wXJrLpC5LtJG7fqLOsPb2U= github.com/emersion/go-vcard v0.0.0-20191221110513-5f81fa0d3cc7/go.mod h1:HMJKR5wlh/ziNp+sHEDV2ltblO4JD2+IdDOWtGcQBTM= @@ -56,7 +45,6 @@ github.com/labstack/echo/v4 v4.10.2 h1:n1jAhnq/elIFTHr1EYpiYtyKgx4RW9ccVgkqByZaN github.com/labstack/echo/v4 v4.10.2/go.mod h1:OEyqf2//K1DFdE57vw2DRgWY0M7s65IVQO2FzvI4J5k= github.com/labstack/gommon v0.4.0 h1:y7cvthEAEbU0yHOf4axH8ZG2NH8knB9iNSoTO8dyIk8= github.com/labstack/gommon v0.4.0/go.mod h1:uW6kP17uPlLJsD3ijUYn3/M5bAxtlZhMI6m3MFxTMTM= -github.com/martinlindhe/base36 v1.0.0/go.mod h1:+AtEs8xrBpCeYgSLoY/aJ6Wf37jtBuR0s35750M27+8= github.com/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= @@ -81,7 +69,6 @@ github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf/go.mod h1:RJID2RhlZKId02n github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= @@ -136,9 +123,7 @@ golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuX golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68= @@ -2,29 +2,42 @@ package alps import ( "fmt" + "io" + "mime" + "net" + "os" - "github.com/emersion/go-imap" - imapclient "github.com/emersion/go-imap/client" + "github.com/emersion/go-imap/v2/imapclient" "github.com/emersion/go-message/charset" ) -func init() { - imap.CharsetReader = charset.Reader -} - func (s *Server) dialIMAP() (*imapclient.Client, error) { + // TODO: don't print passwords to debug logs + var debugWriter io.Writer + if s.Options.Debug { + debugWriter = os.Stderr + } + + options := &imapclient.Options{ + DebugWriter: debugWriter, + WordDecoder: &mime.WordDecoder{ + CharsetReader: charset.Reader, + }, + } + var c *imapclient.Client var err error if s.imap.tls { - c, err = imapclient.DialTLS(s.imap.host, nil) + c, err = imapclient.DialTLS(s.imap.host, options) if err != nil { return nil, fmt.Errorf("failed to connect to IMAPS server: %v", err) } } else { - c, err = imapclient.Dial(s.imap.host) + conn, err := net.Dial("tcp", s.imap.host) if err != nil { return nil, fmt.Errorf("failed to connect to IMAP server: %v", err) } + c = imapclient.New(conn, options) if !s.imap.insecure { if err := c.StartTLS(nil); err != nil { c.Close() diff --git a/plugins/base/imap.go b/plugins/base/imap.go index 2ca5a7e..e3720d0 100644 --- a/plugins/base/imap.go +++ b/plugins/base/imap.go @@ -4,37 +4,41 @@ import ( "bufio" "bytes" "fmt" + "io" "net/url" "sort" "strconv" "strings" - "time" + //"time" "github.com/dustin/go-humanize" - "github.com/emersion/go-imap" - imapspecialuse "github.com/emersion/go-imap-specialuse" - imapclient "github.com/emersion/go-imap/client" + "github.com/emersion/go-imap/v2" + "github.com/emersion/go-imap/v2/imapclient" "github.com/emersion/go-message" "github.com/emersion/go-message/textproto" ) type MailboxInfo struct { - *imap.MailboxInfo + *imap.ListData Active bool Total int Unseen int } +func (mbox *MailboxInfo) Name() string { + return mbox.Mailbox +} + func (mbox *MailboxInfo) URL() *url.URL { return &url.URL{ - Path: fmt.Sprintf("/mailbox/%v", url.PathEscape(mbox.Name)), + Path: fmt.Sprintf("/mailbox/%v", url.PathEscape(mbox.Name())), } } func (mbox *MailboxInfo) HasAttr(flag string) bool { - for _, attr := range mbox.Attributes { - if attr == flag { + for _, attr := range mbox.Attrs { + if string(attr) == flag { return true } } @@ -42,50 +46,51 @@ func (mbox *MailboxInfo) HasAttr(flag string) bool { } func listMailboxes(conn *imapclient.Client) ([]MailboxInfo, error) { - ch := make(chan *imap.MailboxInfo, 10) - done := make(chan error, 1) - go func() { - done <- conn.List("", "*", ch) - }() - var mailboxes []MailboxInfo - for mbox := range ch { + list := conn.List("", "*", nil) + for { + mbox := list.Next() + if mbox == nil { + break + } mailboxes = append(mailboxes, MailboxInfo{mbox, false, -1, -1}) } - - if err := <-done; err != nil { + if err := list.Close(); err != nil { return nil, fmt.Errorf("failed to list mailboxes: %v", err) } sort.Slice(mailboxes, func(i, j int) bool { - if mailboxes[i].Name == "INBOX" { + if mailboxes[i].Mailbox == "INBOX" { return true } - if mailboxes[j].Name == "INBOX" { + if mailboxes[j].Mailbox == "INBOX" { return false } - return mailboxes[i].Name < mailboxes[j].Name + return mailboxes[i].Mailbox < mailboxes[j].Mailbox }) return mailboxes, nil } type MailboxStatus struct { - *imap.MailboxStatus + *imap.StatusData +} + +func (mbox *MailboxStatus) Name() string { + return mbox.Mailbox } func (mbox *MailboxStatus) URL() *url.URL { return &url.URL{ - Path: fmt.Sprintf("/mailbox/%v", url.PathEscape(mbox.Name)), + Path: fmt.Sprintf("/mailbox/%v", url.PathEscape(mbox.Name())), } } func getMailboxStatus(conn *imapclient.Client, name string) (*MailboxStatus, error) { - items := []imap.StatusItem{ - imap.StatusMessages, - imap.StatusUidValidity, - imap.StatusUnseen, - } - status, err := conn.Status(name, items) + status, err := conn.Status(name, &imap.StatusOptions{ + NumMessages: true, + UIDValidity: true, + NumUnseen: true, + }).Wait() if err != nil { return nil, fmt.Errorf("failed to get mailbox status: %v", err) } @@ -100,28 +105,29 @@ const ( ) func getMailboxByType(conn *imapclient.Client, mboxType mailboxType) (*MailboxInfo, error) { - ch := make(chan *imap.MailboxInfo, 10) - done := make(chan error, 1) - go func() { - done <- conn.List("", "%", ch) - }() - // TODO: configurable fallback names? - var attr string + var attr imap.MailboxAttr var fallbackNames []string switch mboxType { case mailboxSent: - attr = imapspecialuse.Sent + attr = imap.MailboxAttrSent fallbackNames = []string{"Sent"} case mailboxDrafts: - attr = imapspecialuse.Drafts + attr = imap.MailboxAttrDrafts fallbackNames = []string{"Draft", "Drafts"} } + list := conn.List("", "%", nil) + var attrMatched bool - var best *imap.MailboxInfo - for mbox := range ch { - for _, a := range mbox.Attributes { + var best *imap.ListData + for { + mbox := list.Next() + if mbox == nil { + break + } + + for _, a := range mbox.Attrs { if attr == a { best = mbox attrMatched = true @@ -133,14 +139,13 @@ func getMailboxByType(conn *imapclient.Client, mboxType mailboxType) (*MailboxIn } for _, fallback := range fallbackNames { - if strings.EqualFold(fallback, mbox.Name) { + if strings.EqualFold(fallback, mbox.Mailbox) { best = mbox break } } } - - if err := <-done; err != nil { + if err := list.Close(); err != nil { return nil, fmt.Errorf("failed to get mailbox with attribute %q: %v", attr, err) } @@ -151,9 +156,8 @@ func getMailboxByType(conn *imapclient.Client, mboxType mailboxType) (*MailboxIn } func ensureMailboxSelected(conn *imapclient.Client, mboxName string) error { - mbox := conn.Mailbox() - if mbox == nil || mbox.Name != mboxName { - if _, err := conn.Select(mboxName, false); err != nil { + if mbox := conn.Mailbox(); mbox == nil || mbox.Name != mboxName { + if _, err := conn.Select(mboxName, nil).Wait(); err != nil { return fmt.Errorf("failed to select mailbox: %v", err) } } @@ -161,26 +165,28 @@ func ensureMailboxSelected(conn *imapclient.Client, mboxName string) error { } type IMAPMessage struct { - *imap.Message + *imapclient.FetchMessageBuffer Mailbox string } func (msg *IMAPMessage) URL() *url.URL { return &url.URL{ - Path: fmt.Sprintf("/message/%v/%v", url.PathEscape(msg.Mailbox), msg.Uid), + Path: fmt.Sprintf("/message/%v/%v", url.PathEscape(msg.Mailbox), msg.UID), } } -func newIMAPPartNode(msg *IMAPMessage, path []int, part *imap.BodyStructure) *IMAPPartNode { - filename, _ := part.Filename() - return &IMAPPartNode{ +func newIMAPPartNode(msg *IMAPMessage, path []int, part imap.BodyStructure) *IMAPPartNode { + node := &IMAPPartNode{ Path: path, - MIMEType: strings.ToLower(part.MIMEType + "/" + part.MIMESubType), - Filename: filename, + MIMEType: part.MediaType(), Message: msg, - Size: part.Size, } + if singlePart, ok := part.(*imap.BodyStructureSinglePart); ok { + node.Filename = singlePart.Filename() + node.Size = singlePart.Size + } + return node } func (msg *IMAPMessage) TextPart() *IMAPPartNode { @@ -190,21 +196,26 @@ func (msg *IMAPMessage) TextPart() *IMAPPartNode { var best *IMAPPartNode isTextPlain := false - msg.BodyStructure.Walk(func(path []int, part *imap.BodyStructure) bool { - if !strings.EqualFold(part.MIMEType, "text") { + msg.BodyStructure.Walk(func(path []int, part imap.BodyStructure) bool { + singlePart, ok := part.(*imap.BodyStructureSinglePart) + if !ok { + return true + } + + if !strings.EqualFold(singlePart.Type, "text") { return true } - if part.Disposition != "" && !strings.EqualFold(part.Disposition, "inline") { + if disp := singlePart.Disposition(); disp != nil && !strings.EqualFold(disp.Value, "inline") { return true } - switch strings.ToLower(part.MIMESubType) { + switch strings.ToLower(singlePart.Subtype) { case "plain": isTextPlain = true - best = newIMAPPartNode(msg, path, part) + best = newIMAPPartNode(msg, path, singlePart) case "html": if !isTextPlain { - best = newIMAPPartNode(msg, path, part) + best = newIMAPPartNode(msg, path, singlePart) } } return true @@ -219,16 +230,21 @@ func (msg *IMAPMessage) HTMLPart() *IMAPPartNode { } var best *IMAPPartNode - msg.BodyStructure.Walk(func(path []int, part *imap.BodyStructure) bool { - if !strings.EqualFold(part.MIMEType, "text") { + msg.BodyStructure.Walk(func(path []int, part imap.BodyStructure) bool { + singlePart, ok := part.(*imap.BodyStructureSinglePart) + if !ok { + return true + } + + if !strings.EqualFold(singlePart.Type, "text") { return true } - if part.Disposition != "" && !strings.EqualFold(part.Disposition, "inline") { + if disp := singlePart.Disposition(); disp != nil && !strings.EqualFold(disp.Value, "inline") { return true } - if part.MIMESubType == "html" { - best = newIMAPPartNode(msg, path, part) + if singlePart.Subtype == "html" { + best = newIMAPPartNode(msg, path, singlePart) } return true }) @@ -242,12 +258,17 @@ func (msg *IMAPMessage) Attachments() []IMAPPartNode { } var attachments []IMAPPartNode - msg.BodyStructure.Walk(func(path []int, part *imap.BodyStructure) bool { - if !strings.EqualFold(part.Disposition, "attachment") { + msg.BodyStructure.Walk(func(path []int, part imap.BodyStructure) bool { + singlePart, ok := part.(*imap.BodyStructureSinglePart) + if !ok { + return true + } + + if disp := singlePart.Disposition(); disp == nil || !strings.EqualFold(disp.Value, "attachment") { return true } - attachments = append(attachments, *newIMAPPartNode(msg, path, part)) + attachments = append(attachments, *newIMAPPartNode(msg, path, singlePart)) return true }) return attachments @@ -274,7 +295,7 @@ func (msg *IMAPMessage) PartByPath(path []int) *IMAPPartNode { } var result *IMAPPartNode - msg.BodyStructure.Walk(func(p []int, part *imap.BodyStructure) bool { + msg.BodyStructure.Walk(func(p []int, part imap.BodyStructure) bool { if result == nil && pathsEqual(path, p) { result = newIMAPPartNode(msg, p, part) } @@ -289,9 +310,13 @@ func (msg *IMAPMessage) PartByID(id string) *IMAPPartNode { } var result *IMAPPartNode - msg.BodyStructure.Walk(func(path []int, part *imap.BodyStructure) bool { - if result == nil && part.Id == "<"+id+">" { - result = newIMAPPartNode(msg, path, part) + msg.BodyStructure.Walk(func(path []int, part imap.BodyStructure) bool { + singlePart, ok := part.(*imap.BodyStructureSinglePart) + if !ok { + return result == nil + } + if result == nil && singlePart.ID == "<"+id+">" { + result = newIMAPPartNode(msg, path, singlePart) } return result == nil }) @@ -342,29 +367,29 @@ func (node IMAPPartNode) String() string { } } -func imapPartTree(msg *IMAPMessage, bs *imap.BodyStructure, path []int) *IMAPPartNode { - if !strings.EqualFold(bs.MIMEType, "multipart") && len(path) == 0 { - path = []int{1} - } - - filename, _ := bs.Filename() - +func imapPartTree(msg *IMAPMessage, bs imap.BodyStructure, path []int) *IMAPPartNode { node := &IMAPPartNode{ Path: path, - MIMEType: strings.ToLower(bs.MIMEType + "/" + bs.MIMESubType), - Filename: filename, - Children: make([]IMAPPartNode, len(bs.Parts)), + MIMEType: bs.MediaType(), Message: msg, - Size: bs.Size, } - for i, part := range bs.Parts { - num := i + 1 + switch bs := bs.(type) { + case *imap.BodyStructureMultiPart: + for i, part := range bs.Children { + num := i + 1 - partPath := append([]int(nil), path...) - partPath = append(partPath, num) + partPath := append([]int(nil), path...) + partPath = append(partPath, num) - node.Children[i] = *imapPartTree(msg, part, partPath) + node.Children = append(node.Children, *imapPartTree(msg, part, partPath)) + } + case *imap.BodyStructureSinglePart: + if len(path) == 0 { + node.Path = []int{1} + } + node.Filename = bs.Filename() + node.Size = bs.Size } return node @@ -378,9 +403,9 @@ func (msg *IMAPMessage) PartTree() *IMAPPartNode { return imapPartTree(msg, msg.BodyStructure, nil) } -func (msg *IMAPMessage) HasFlag(flag string) bool { +func (msg *IMAPMessage) HasFlag(flag imap.Flag) bool { for _, f := range msg.Flags { - if imap.CanonicalFlag(f) == flag { + if f == flag { return true } } @@ -388,11 +413,11 @@ func (msg *IMAPMessage) HasFlag(flag string) bool { } func listMessages(conn *imapclient.Client, mbox *MailboxStatus, page, messagesPerPage int) ([]IMAPMessage, error) { - if err := ensureMailboxSelected(conn, mbox.Name); err != nil { + if err := ensureMailboxSelected(conn, mbox.Name()); err != nil { return nil, err } - to := int(mbox.Messages) - page*messagesPerPage + to := int(*mbox.NumMessages) - page*messagesPerPage from := to - messagesPerPage + 1 if from <= 0 { from = 1 @@ -401,29 +426,21 @@ func listMessages(conn *imapclient.Client, mbox *MailboxStatus, page, messagesPe return nil, nil } - var seqSet imap.SeqSet - seqSet.AddRange(uint32(from), uint32(to)) - - fetch := []imap.FetchItem{ - imap.FetchFlags, - imap.FetchEnvelope, - imap.FetchUid, - imap.FetchBodyStructure, + seqSet := imap.SeqSetRange(uint32(from), uint32(to)) + items := []imap.FetchItem{ + imap.FetchItemFlags, + imap.FetchItemEnvelope, + imap.FetchItemUID, + imap.FetchItemBodyStructure, } - - ch := make(chan *imap.Message, 10) - done := make(chan error, 1) - go func() { - done <- conn.Fetch(&seqSet, fetch, ch) - }() - - msgs := make([]IMAPMessage, 0, to-from) - for msg := range ch { - msgs = append(msgs, IMAPMessage{msg, mbox.Name}) + imapMsgs, err := conn.Fetch(seqSet, items, nil).Collect() + if err != nil { + return nil, fmt.Errorf("failed to fetch message list: %v", err) } - if err := <-done; err != nil { - return nil, fmt.Errorf("failed to fetch message list: %v", err) + var msgs []IMAPMessage + for _, msg := range imapMsgs { + msgs = append(msgs, IMAPMessage{msg, mbox.Name()}) } // Reverse list of messages @@ -441,10 +458,11 @@ func searchMessages(conn *imapclient.Client, mboxName, query string, page, messa } criteria := PrepareSearch(query) - nums, err := conn.Search(criteria) + data, err := conn.Search(criteria, nil).Wait() if err != nil { return nil, 0, fmt.Errorf("UID SEARCH failed: %v", err) } + nums := data.AllNums() total = len(nums) from := page * messagesPerPage @@ -462,24 +480,20 @@ func searchMessages(conn *imapclient.Client, mboxName, query string, page, messa indexes[num] = i } - var seqSet imap.SeqSet - seqSet.AddNum(nums...) - - fetch := []imap.FetchItem{ - imap.FetchEnvelope, - imap.FetchFlags, - imap.FetchUid, - imap.FetchBodyStructure, + seqSet := imap.SeqSetNum(nums...) + items := []imap.FetchItem{ + imap.FetchItemEnvelope, + imap.FetchItemFlags, + imap.FetchItemUID, + imap.FetchItemBodyStructure, + } + results, err := conn.Fetch(seqSet, items, nil).Collect() + if err != nil { + return nil, 0, fmt.Errorf("failed to fetch message list: %v", err) } - - ch := make(chan *imap.Message, 10) - done := make(chan error, 1) - go func() { - done <- conn.Fetch(&seqSet, fetch, ch) - }() msgs = make([]IMAPMessage, len(nums)) - for msg := range ch { + for _, msg := range results { i, ok := indexes[msg.SeqNum] if !ok { continue @@ -487,10 +501,6 @@ func searchMessages(conn *imapclient.Client, mboxName, query string, page, messa msgs[i] = IMAPMessage{msg, mboxName} } - if err := <-done; err != nil { - return nil, 0, fmt.Errorf("failed to fetch message list: %v", err) - } - return msgs, total, nil } @@ -499,58 +509,64 @@ func getMessagePart(conn *imapclient.Client, mboxName string, uid uint32, partPa return nil, nil, err } - seqSet := new(imap.SeqSet) - seqSet.AddNum(uid) + seqSet := imap.SeqSetNum(uid) - var partHeaderSection imap.BodySectionName - partHeaderSection.Peek = true + headerItem := &imap.FetchItemBodySection{ + Peek: true, + Part: partPath, + } if len(partPath) > 0 { - partHeaderSection.Specifier = imap.MIMESpecifier + headerItem.Specifier = imap.PartSpecifierMIME } else { - partHeaderSection.Specifier = imap.HeaderSpecifier + headerItem.Specifier = imap.PartSpecifierHeader } - partHeaderSection.Path = partPath - var partBodySection imap.BodySectionName + bodyItem := &imap.FetchItemBodySection{ + Part: partPath, + } if len(partPath) > 0 { - partBodySection.Specifier = imap.EntireSpecifier + bodyItem.Specifier = imap.PartSpecifierNone } else { - partBodySection.Specifier = imap.TextSpecifier + bodyItem.Specifier = imap.PartSpecifierText } - partBodySection.Path = partPath - fetch := []imap.FetchItem{ - imap.FetchEnvelope, - imap.FetchUid, - imap.FetchBodyStructure, - imap.FetchFlags, - imap.FetchRFC822Size, - partHeaderSection.FetchItem(), - partBodySection.FetchItem(), + items := []imap.FetchItem{ + imap.FetchItemEnvelope, + imap.FetchItemUID, + imap.FetchItemBodyStructure, + imap.FetchItemFlags, + imap.FetchItemRFC822Size, + headerItem, + bodyItem, } - ch := make(chan *imap.Message, 1) - if err := conn.UidFetch(seqSet, fetch, ch); err != nil { + // TODO: stream attachments + msgs, err := conn.UIDFetch(seqSet, items, nil).Collect() + if err != nil { return nil, nil, fmt.Errorf("failed to fetch message: %v", err) - } - - msg := <-ch - if msg == nil { + } else if len(msgs) == 0 { return nil, nil, fmt.Errorf("server didn't return message") } + msg := msgs[0] - body := msg.GetBody(&partHeaderSection) - if body == nil { - return nil, nil, fmt.Errorf("server didn't return message") + var headerBuf, bodyBuf []byte + for item, b := range msg.BodySection { + if item.Specifier == headerItem.Specifier { + headerBuf = b + } else if item.Specifier == bodyItem.Specifier { + bodyBuf = b + } + } + if headerBuf == nil || bodyBuf == nil { + return nil, nil, fmt.Errorf("server didn't return header and body") } - headerReader := bufio.NewReader(body) - h, err := textproto.ReadHeader(headerReader) + h, err := textproto.ReadHeader(bufio.NewReader(bytes.NewReader(headerBuf))) if err != nil { return nil, nil, fmt.Errorf("failed to read part header: %v", err) } - part, err := message.New(message.Header{h}, msg.GetBody(&partBodySection)) + part, err := message.New(message.Header{h}, bytes.NewReader(bodyBuf)) if err != nil { return nil, nil, fmt.Errorf("failed to create message reader: %v", err) } @@ -563,11 +579,12 @@ func markMessageAnswered(conn *imapclient.Client, mboxName string, uid uint32) e return err } - seqSet := new(imap.SeqSet) - seqSet.AddNum(uid) - item := imap.FormatFlagsOp(imap.AddFlags, true) - flags := []interface{}{imap.AnsweredFlag} - return conn.UidStore(seqSet, item, flags, nil) + seqSet := imap.SeqSetNum(uid) + return conn.UIDStore(seqSet, &imap.StoreFlags{ + Op: imap.StoreFlagsAdd, + Silent: true, + Flags: []imap.Flag{imap.FlagAnswered}, + }, nil).Close() } func appendMessage(c *imapclient.Client, msg *OutgoingMessage, mboxType mailboxType) (*MailboxInfo, error) { @@ -586,28 +603,36 @@ func appendMessage(c *imapclient.Client, msg *OutgoingMessage, mboxType mailboxT return nil, err } - flags := []string{imap.SeenFlag} + flags := []imap.Flag{imap.FlagSeen} if mboxType == mailboxDrafts { - flags = append(flags, imap.DraftFlag) + flags = append(flags, imap.FlagDraft) } - if err := c.Append(mbox.Name, flags, time.Now(), &buf); err != nil { + options := imap.AppendOptions{Flags: flags} + appendCmd := c.Append(mbox.Name(), int64(buf.Len()), &options) + defer appendCmd.Close() + if _, err := io.Copy(appendCmd, &buf); err != nil { + return nil, err + } + if err := appendCmd.Close(); err != nil { return nil, err } return mbox, nil } -func deleteMessage(c *imapclient.Client, mboxName string, uid uint32) error { - if err := ensureMailboxSelected(c, mboxName); err != nil { +func deleteMessage(conn *imapclient.Client, mboxName string, uid uint32) error { + if err := ensureMailboxSelected(conn, mboxName); err != nil { return err } - seqSet := new(imap.SeqSet) - seqSet.AddNum(uid) - item := imap.FormatFlagsOp(imap.AddFlags, true) - flags := []interface{}{imap.DeletedFlag} - if err := c.UidStore(seqSet, item, flags, nil); err != nil { + seqSet := imap.SeqSetNum(uid) + err := conn.UIDStore(seqSet, &imap.StoreFlags{ + Op: imap.StoreFlagsAdd, + Silent: true, + Flags: []imap.Flag{imap.FlagDeleted}, + }, nil).Close() + if err != nil { return err } - return c.Expunge(nil) + return conn.Expunge().Close() } diff --git a/plugins/base/routes.go b/plugins/base/routes.go index a7dabf1..7c3748d 100644 --- a/plugins/base/routes.go +++ b/plugins/base/routes.go @@ -7,15 +7,13 @@ import ( "io/ioutil" "mime" "net/http" - "net/textproto" "net/url" "strconv" "strings" "git.sr.ht/~migadu/alps" - "github.com/emersion/go-imap" - imapmove "github.com/emersion/go-imap-move" - imapclient "github.com/emersion/go-imap/client" + "github.com/emersion/go-imap/v2" + "github.com/emersion/go-imap/v2/imapclient" "github.com/emersion/go-message" "github.com/emersion/go-message/mail" "github.com/emersion/go-smtp" @@ -113,7 +111,7 @@ func (cc *CategorizedMailboxes) Append(mi MailboxInfo, status *MailboxStatus) { Info: &mi, Status: status, } - if name := mi.Name; name == "INBOX" { + if name := mi.Mailbox; name == "INBOX" { cc.Common.Inbox = details } else if name == "Drafts" { cc.Common.Drafts = details @@ -187,13 +185,13 @@ func newIMAPBaseRenderData(ctx *alps.Context, var categorized CategorizedMailboxes for i := range mailboxes { // Populate unseen & active states - if active != nil && mailboxes[i].Name == active.Name { + if active != nil && mailboxes[i].Name() == active.Mailbox { mailboxes[i].Active = true } - status := statuses[mailboxes[i].Name] + status := statuses[mailboxes[i].Name()] if status != nil { - mailboxes[i].Unseen = int(status.Unseen) - mailboxes[i].Total = int(status.Messages) + mailboxes[i].Unseen = int(*status.NumUnseen) + mailboxes[i].Total = int(*status.NumMessages) } categorized.Append(mailboxes[i], status) @@ -216,12 +214,12 @@ func handleGetMailbox(ctx *alps.Context) error { } mbox := ibase.Mailbox - title := mbox.Name + title := mbox.Name() if title == "INBOX" { title = "Inbox" } - if mbox.Unseen > 0 { - title = fmt.Sprintf("(%d) %s", mbox.Unseen, title) + if *mbox.NumUnseen > 0 { + title = fmt.Sprintf("(%d) %s", *mbox.NumUnseen, title) } ibase.BaseRenderData.WithTitle(title) @@ -248,7 +246,7 @@ func handleGetMailbox(ctx *alps.Context) error { err = ctx.Session.DoIMAP(func(c *imapclient.Client) error { var err error if query != "" { - msgs, total, err = searchMessages(c, mbox.Name, query, page, messagesPerPage) + msgs, total, err = searchMessages(c, mbox.Name(), query, page, messagesPerPage) } else { msgs, err = listMessages(c, mbox, page, messagesPerPage) } @@ -273,7 +271,7 @@ func handleGetMailbox(ctx *alps.Context) error { if page > 0 { prevPage = page - 1 } - if (page+1)*messagesPerPage < int(mbox.Messages) { + if (page+1)*messagesPerPage < int(*mbox.NumMessages) { nextPage = page + 1 } } @@ -309,7 +307,7 @@ func handleNewMailbox(ctx *alps.Context) error { } err := ctx.Session.DoIMAP(func(c *imapclient.Client) error { - return c.Create(name) + return c.Create(name, nil).Wait() }) if err != nil { @@ -335,11 +333,11 @@ func handleDeleteMailbox(ctx *alps.Context) error { } mbox := ibase.Mailbox - ibase.BaseRenderData.WithTitle("Delete folder '" + mbox.Name + "'") + ibase.BaseRenderData.WithTitle("Delete folder '" + mbox.Name() + "'") if ctx.Request().Method == http.MethodPost { ctx.Session.DoIMAP(func(c *imapclient.Client) error { - return c.Delete(mbox.Name) + return c.Delete(mbox.Name()).Wait() }) ctx.Session.PutNotice("Mailbox deleted.") return ctx.Redirect(http.StatusFound, "/mailbox/INBOX") @@ -429,11 +427,13 @@ func handleGetPart(ctx *alps.Context, raw bool) error { var msg *IMAPMessage var part *message.Entity + var selected *imapclient.SelectedMailbox err = ctx.Session.DoIMAP(func(c *imapclient.Client) error { var err error - if msg, part, err = getMessagePart(c, mbox.Name, uid, partPath); err != nil { + if msg, part, err = getMessagePart(c, mbox.Name(), uid, partPath); err != nil { return err } + selected = c.Mailbox() return nil }) if err != nil { @@ -485,12 +485,11 @@ func handleGetPart(ctx *alps.Context, raw bool) error { } flags := make(map[string]bool) - for _, f := range mbox.PermanentFlags { - f = imap.CanonicalFlag(f) - if f == imap.TryCreateFlag { + for _, f := range selected.PermanentFlags { + if f == imap.FlagWildcard { continue } - flags[f] = msg.HasFlag(f) + flags[string(f)] = msg.HasFlag(f) } ibase.BaseRenderData.WithTitle(msg.Envelope.Subject) @@ -500,7 +499,7 @@ func handleGetPart(ctx *alps.Context, raw bool) error { Message: msg, Part: msg.PartByPath(partPath), View: view, - MailboxPage: int(mbox.Messages-msg.SeqNum) / messagesPerPage, + MailboxPage: int(*mbox.NumMessages-msg.SeqNum) / messagesPerPage, Flags: flags, }) } @@ -686,20 +685,21 @@ func handleCompose(ctx *alps.Context, msg *OutgoingMessage, options *composeOpti } } - if err := ensureMailboxSelected(c, drafts.Name); err != nil { + if err := ensureMailboxSelected(c, drafts.Name()); err != nil { return err } - criteria := &imap.SearchCriteria{ - Header: make(textproto.MIMEHeader), + // TODO: use APPENDUID instead when available + criteria := imap.SearchCriteria{ + Header: []imap.SearchCriteriaHeaderField{ + {Key: "Message-Id", Value: msg.MessageID}, + }, } - criteria.Header.Add("Message-Id", msg.MessageID) - if uids, err := c.UidSearch(criteria); err != nil { + if data, err := c.UIDSearch(&criteria, nil).Wait(); err != nil { return err + } else if uids := data.AllNums(); len(uids) != 1 { + panic(fmt.Errorf("Duplicate message ID")) } else { - if len(uids) != 1 { - panic(fmt.Errorf("Duplicate message ID")) - } uid = uids[0] } return nil @@ -709,7 +709,7 @@ func handleCompose(ctx *alps.Context, msg *OutgoingMessage, options *composeOpti } ctx.Session.PutNotice("Message saved as draft.") return ctx.Redirect(http.StatusFound, fmt.Sprintf( - "/message/%s/%d/edit?part=1", drafts.Name, uid)) + "/message/%s/%d/edit?part=1", drafts.Mailbox, uid)) } else { return submitCompose(ctx, msg, options) } @@ -789,10 +789,10 @@ func handleCancelAttachment(ctx *alps.Context) error { return ctx.JSON(http.StatusOK, nil) } -func unwrapIMAPAddressList(addrs []*imap.Address) []string { +func unwrapIMAPAddressList(addrs []imap.Address) []string { l := make([]string, len(addrs)) for i, addr := range addrs { - l[i] = addr.Address() + l[i] = addr.Addr() } return l } @@ -852,7 +852,7 @@ func handleReply(ctx *alps.Context) error { hdr.GenerateMessageID() mid, _ := hdr.MessageID() msg.MessageID = "<" + mid + ">" - msg.InReplyTo = inReplyTo.Envelope.MessageId + msg.InReplyTo = inReplyTo.Envelope.MessageID // TODO: populate From from known user addresses and inReplyTo.Envelope.To replyTo := inReplyTo.Envelope.ReplyTo if len(replyTo) == 0 { @@ -987,12 +987,12 @@ func handleEdit(ctx *alps.Context) error { msg.Text = string(b) if len(source.Envelope.From) > 0 { - msg.From = source.Envelope.From[0].Address() + msg.From = source.Envelope.From[0].Addr() } msg.To = unwrapIMAPAddressList(source.Envelope.To) msg.Subject = source.Envelope.Subject msg.InReplyTo = source.Envelope.InReplyTo - msg.MessageID = source.Envelope.MessageId + msg.MessageID = source.Envelope.MessageID attachments := source.Attachments() for i := range attachments { @@ -1042,15 +1042,12 @@ func handleMove(ctx *alps.Context) error { } err = ctx.Session.DoIMAP(func(c *imapclient.Client) error { - mc := imapmove.NewClient(c) - if err := ensureMailboxSelected(c, mboxName); err != nil { return err } - var seqSet imap.SeqSet - seqSet.AddNum(uids...) - if err := mc.UidMoveWithFallback(&seqSet, to); err != nil { + seqSet := imap.SeqSetNum(uids...) + if _, err := c.UIDMove(seqSet, to).Wait(); err != nil { return fmt.Errorf("failed to move message: %v", err) } @@ -1093,25 +1090,20 @@ func handleDelete(ctx *alps.Context) error { return err } - var seqSet imap.SeqSet - seqSet.AddNum(uids...) - - item := imap.FormatFlagsOp(imap.AddFlags, true) - flags := []interface{}{imap.DeletedFlag} - if err := c.UidStore(&seqSet, item, flags, nil); err != nil { + seqSet := imap.SeqSetNum(uids...) + err := c.UIDStore(seqSet, &imap.StoreFlags{ + Op: imap.StoreFlagsAdd, + Silent: true, + Flags: []imap.Flag{imap.FlagDeleted}, + }, nil).Close() + if err != nil { return fmt.Errorf("failed to add deleted flag: %v", err) } - if err := c.Expunge(nil); err != nil { + if err := c.Expunge().Close(); err != nil { return fmt.Errorf("failed to expunge mailbox: %v", err) } - // Deleting a message invalidates our cached message count - // TODO: listen to async updates instead - if _, err := c.Select(mboxName, false); err != nil { - return fmt.Errorf("failed to select mailbox: %v", err) - } - return nil }) if err != nil { @@ -1155,34 +1147,36 @@ func handleSetFlags(ctx *alps.Context) error { actionStr = ctx.QueryParam("action") } - var op imap.FlagsOp + var op imap.StoreFlagsOp switch actionStr { case "", "set": - op = imap.SetFlags + op = imap.StoreFlagsSet case "add": - op = imap.AddFlags + op = imap.StoreFlagsAdd case "remove": - op = imap.RemoveFlags + op = imap.StoreFlagsDel default: return echo.NewHTTPError(http.StatusBadRequest, "invalid 'action' value") } + l := make([]imap.Flag, len(flags)) + for i, s := range flags { + l[i] = imap.Flag(s) + } + err = ctx.Session.DoIMAP(func(c *imapclient.Client) error { if err := ensureMailboxSelected(c, mboxName); err != nil { return err } - var seqSet imap.SeqSet - seqSet.AddNum(uids...) - - storeItems := make([]interface{}, len(flags)) - for i, f := range flags { - storeItems[i] = f - } - - item := imap.FormatFlagsOp(op, true) - if err := c.UidStore(&seqSet, item, storeItems, nil); err != nil { - return fmt.Errorf("failed to add deleted flag: %v", err) + seqSet := imap.SeqSetNum(uids...) + err := c.UIDStore(seqSet, &imap.StoreFlags{ + Op: op, + Silent: true, + Flags: l, + }, nil).Close() + if err != nil { + return fmt.Errorf("failed to set flags: %v", err) } return nil @@ -1194,7 +1188,7 @@ func handleSetFlags(ctx *alps.Context) error { if path := formOrQueryParam(ctx, "next"); path != "" { return ctx.Redirect(http.StatusFound, path) } - if len(uids) != 1 || (op == imap.RemoveFlags && len(flags) == 1 && flags[0] == imap.SeenFlag) { + if len(uids) != 1 || (op == imap.StoreFlagsDel && len(l) == 1 && l[0] == imap.FlagSeen) { // Redirecting to the message view would mark the message as read again return ctx.Redirect(http.StatusFound, fmt.Sprintf("/mailbox/%v", url.PathEscape(mboxName))) } diff --git a/plugins/base/search.go b/plugins/base/search.go index fd85f88..4b91d2c 100644 --- a/plugins/base/search.go +++ b/plugins/base/search.go @@ -3,16 +3,15 @@ package alpsbase import ( "bufio" "bytes" - "net/textproto" "strings" - "github.com/emersion/go-imap" + "github.com/emersion/go-imap/v2" ) func searchCriteriaHeader(k, v string) *imap.SearchCriteria { return &imap.SearchCriteria{ - Header: map[string][]string{ - k: []string{v}, + Header: []imap.SearchCriteriaHeaderField{ + {Key: k, Value: v}, }, } } @@ -24,7 +23,7 @@ func searchCriteriaOr(criteria ...*imap.SearchCriteria) *imap.SearchCriteria { or := criteria[0] for _, c := range criteria[1:] { or = &imap.SearchCriteria{ - Or: [][2]*imap.SearchCriteria{{or, c}}, + Or: [][2]imap.SearchCriteria{{*or, *c}}, } } return or @@ -36,24 +35,7 @@ func searchCriteriaAnd(criteria ...*imap.SearchCriteria) *imap.SearchCriteria { } and := criteria[0] for _, c := range criteria[1:] { - // TODO: Maybe pitch the AND and OR functions to go-imap upstream - if c.Header != nil { - if and.Header == nil { - and.Header = make(textproto.MIMEHeader) - } - - for key, value := range c.Header { - if _, ok := and.Header[key]; !ok { - and.Header[key] = nil - } - and.Header[key] = append(and.Header[key], value...) - } - } - and.Body = append(and.Body, c.Body...) - and.Text = append(and.Text, c.Text...) - and.WithFlags = append(and.WithFlags, c.WithFlags...) - and.WithoutFlags = append(and.WithoutFlags, c.WithoutFlags...) - // TODO: Merge more things + and.And(c) } return and } diff --git a/plugins/base/template.go b/plugins/base/template.go index 45ba7bf..9ce62fe 100644 --- a/plugins/base/template.go +++ b/plugins/base/template.go @@ -7,7 +7,7 @@ import ( "time" "github.com/dustin/go-humanize" - "github.com/emersion/go-imap" + "github.com/emersion/go-imap/v2" ) const ( @@ -23,23 +23,23 @@ var templateFuncs = template.FuncMap{ "formatdate": func(t time.Time) string { return t.Format("Mon Jan 02 15:04") }, - "formatflag": func(flag string) string { + "formatflag": func(flag imap.Flag) string { switch flag { - case imap.SeenFlag: + case imap.FlagSeen: return "Seen" - case imap.AnsweredFlag: + case imap.FlagAnswered: return "Answered" - case imap.FlaggedFlag: + case imap.FlagFlagged: return "Starred" - case imap.DraftFlag: + case imap.FlagDraft: return "Draft" default: - return flag + return string(flag) } }, - "ismutableflag": func(flag string) bool { + "ismutableflag": func(flag imap.Flag) bool { switch flag { - case imap.AnsweredFlag, imap.DeletedFlag, imap.DraftFlag: + case imap.FlagAnswered, imap.FlagDeleted, imap.FlagDraft: return false default: return true @@ -65,7 +65,7 @@ func newServer(e *echo.Echo, options *Options) (*Server, error) { return nil, err } - s.Sessions = newSessionManager(s.dialIMAP, s.dialSMTP, e.Logger, options.Debug) + s.Sessions = newSessionManager(s.dialIMAP, s.dialSMTP, e.Logger) return s, nil } @@ -7,11 +7,11 @@ import ( "fmt" "mime/multipart" "net/http" - "os" "sync" "time" - imapclient "github.com/emersion/go-imap/client" + "github.com/emersion/go-imap/v2" + "github.com/emersion/go-imap/v2/imapclient" "github.com/emersion/go-sasl" "github.com/emersion/go-smtp" "github.com/google/uuid" @@ -84,6 +84,11 @@ func (s *Session) DoIMAP(f func(*imapclient.Client) error) error { s.imapLocker.Lock() defer s.imapLocker.Unlock() + if s.imapConn != nil && s.imapConn.State() == imap.ConnStateLogout { + s.imapConn.Close() + s.imapConn = nil + } + if s.imapConn == nil { var err error s.imapConn, err = s.manager.connectIMAP(s.username, s.password) @@ -93,6 +98,8 @@ func (s *Session) DoIMAP(f func(*imapclient.Client) error) error { } } + // TODO: to avoid races wrt. disconnection, re-run f if it returns + // io.UnexpectedEOF return f(s.imapConn) } @@ -210,19 +217,17 @@ type SessionManager struct { dialIMAP DialIMAPFunc dialSMTP DialSMTPFunc logger echo.Logger - debug bool locker sync.Mutex sessions map[string]*Session // protected by locker } -func newSessionManager(dialIMAP DialIMAPFunc, dialSMTP DialSMTPFunc, logger echo.Logger, debug bool) *SessionManager { +func newSessionManager(dialIMAP DialIMAPFunc, dialSMTP DialSMTPFunc, logger echo.Logger) *SessionManager { return &SessionManager{ sessions: make(map[string]*Session), dialIMAP: dialIMAP, dialSMTP: dialSMTP, logger: logger, - debug: debug, } } @@ -238,15 +243,11 @@ func (sm *SessionManager) connectIMAP(username, password string) (*imapclient.Cl return nil, err } - if err := c.Login(username, password); err != nil { + if err := c.Login(username, password).Wait(); err != nil { c.Logout() return nil, AuthError{err} } - if sm.debug { - c.SetDebug(os.Stderr) - } - return c, nil } @@ -308,18 +309,7 @@ func (sm *SessionManager) Put(username, password string) (*Session, error) { alive := true for alive { - var loggedOut <-chan struct{} - s.imapLocker.Lock() - if s.imapConn != nil { - loggedOut = s.imapConn.LoggedOut() - } - s.imapLocker.Unlock() - select { - case <-loggedOut: - s.imapLocker.Lock() - s.imapConn = nil - s.imapLocker.Unlock() case <-s.pings: if !timer.Stop() { <-timer.C @@ -336,7 +326,7 @@ func (sm *SessionManager) Put(username, password string) (*Session, error) { s.imapLocker.Lock() if s.imapConn != nil { - s.imapConn.Logout() + s.imapConn.Close() } s.imapLocker.Unlock() @@ -6,8 +6,8 @@ import ( "reflect" "sync" - imapmetadata "github.com/emersion/go-imap-metadata" - imapclient "github.com/emersion/go-imap/client" + "github.com/emersion/go-imap/v2" + "github.com/emersion/go-imap/v2/imapclient" "github.com/labstack/echo/v4" ) @@ -76,12 +76,7 @@ var errIMAPMetadataUnsupported = fmt.Errorf("alps: IMAP server doesn't support M func newIMAPStore(session *Session) (*imapStore, error) { err := session.DoIMAP(func(c *imapclient.Client) error { - mc := imapmetadata.NewClient(c) - ok, err := mc.SupportMetadata() - if err != nil { - return fmt.Errorf("alps: failed to check for IMAP METADATA support: %v", err) - } - if !ok { + if caps := c.Caps(); !caps.Has(imap.CapMetadata) && !caps.Has(imap.CapMetadataServer) { return errIMAPMetadataUnsupported } return nil @@ -101,21 +96,23 @@ func (s *imapStore) Get(key string, out interface{}) error { return err } - var entries map[string]string + var entries map[string]*[]byte err := s.session.DoIMAP(func(c *imapclient.Client) error { - mc := imapmetadata.NewClient(c) - var err error - entries, err = mc.GetMetadata("", []string{s.key(key)}, nil) - return err + data, err := c.GetMetadata("", []string{s.key(key)}, nil).Wait() + if err != nil { + return err + } + entries = data.EntryValues + return nil }) if err != nil { return fmt.Errorf("alps: failed to fetch IMAP store entry %q: %v", key, err) } v, ok := entries[s.key(key)] - if !ok { + if !ok || v == nil { return ErrNoStoreEntry } - if err := json.Unmarshal([]byte(v), out); err != nil { + if err := json.Unmarshal(*v, out); err != nil { return fmt.Errorf("alps: failed to unmarshal IMAP store entry %q: %v", key, err) } return s.cache.Put(key, out) @@ -126,12 +123,9 @@ func (s *imapStore) Put(key string, v interface{}) error { if err != nil { return fmt.Errorf("alps: failed to marshal IMAP store entry %q: %v", key, err) } - entries := map[string]string{ - s.key(key): string(b), - } + entries := map[string]*[]byte{s.key(key): &b} err = s.session.DoIMAP(func(c *imapclient.Client) error { - mc := imapmetadata.NewClient(c) - return mc.SetMetadata("", entries) + return c.SetMetadata("", entries).Wait() }) if err != nil { return fmt.Errorf("alps: failed to put IMAP store entry %q: %v", key, err) diff --git a/themes/alps/mailbox.html b/themes/alps/mailbox.html index c6fd6c9..e33f8dd 100644 --- a/themes/alps/mailbox.html +++ b/themes/alps/mailbox.html @@ -23,15 +23,15 @@ {{ if and (not (.HasFlag "\\Deleted")) .Envelope }} <div class="message-list-checkbox {{$classes}}"> - <input type="checkbox" name="uids" value="{{.Uid}}" form="messages-form"> + <input type="checkbox" name="uids" value="{{.UID}}" form="messages-form"> </div> <div class="message-list-sender {{$classes}}"> {{ range .Envelope.From }} - <a href='?query=from:"{{.MailboxName}}@{{.HostName}}"'> - {{ if .PersonalName }} - {{.PersonalName}} + <a href='?query=from:"{{.Addr}}"'> + {{ if .Name }} + {{.Name}} {{ else }} - {{.MailboxName}}@{{.HostName}} + {{.Addr}} {{ end }} </a> {{ end }} @@ -41,7 +41,7 @@ {{if .HasFlag "\\Answered"}}<span class="Replied">↩</span>{{end}} {{if .HasFlag "$Forwarded"}}<span class="Forwarded">↪</span>{{end}} <form method="POST" action="/message/{{.Mailbox}}/flag"> - <input type="hidden" name="uids" value="{{.Message.Uid}}"> + <input type="hidden" name="uids" value="{{.UID}}"> {{ if .HasFlag "\\Flagged" -}} <input type="hidden" name="action" value="remove"> {{ else }} diff --git a/themes/alps/message.html b/themes/alps/message.html index 350ab0a..ee5aa02 100644 --- a/themes/alps/message.html +++ b/themes/alps/message.html @@ -15,7 +15,7 @@ {{ if and (ne .Mailbox.Name "Archive") (ne .Mailbox.Name "Drafts") (ne .Mailbox.Name "Sent") }} <form class="action-group" method="post" action="/message/{{.Mailbox.Name | pathescape}}/move"> - <input type="hidden" name="uids" value="{{.Message.Uid}}"> + <input type="hidden" name="uids" value="{{.Message.UID}}"> <input type="hidden" name="to" value="Archive"> <input type="hidden" name="next" value="{{$back}}"> <button>Archive</button> @@ -24,7 +24,7 @@ {{ if and (ne .Mailbox.Name "INBOX") (ne .Mailbox.Name "Sent") (ne .Mailbox.Name "Drafts") }} <form class="action-group" method="post" action="/message/{{.Mailbox.Name | pathescape}}/move"> - <input type="hidden" name="uids" value="{{.Message.Uid}}"> + <input type="hidden" name="uids" value="{{.Message.UID}}"> <input type="hidden" name="to" value="INBOX"> <button> {{ if (eq .Mailbox.Name "Junk") }} @@ -38,7 +38,7 @@ {{ if or (eq .Mailbox.Name "INBOX") (eq .Mailbox.Name "Trash") }} <form class="action-group" method="post" action="/message/{{.Mailbox.Name | pathescape}}/move"> - <input type="hidden" name="uids" value="{{.Message.Uid}}"> + <input type="hidden" name="uids" value="{{.Message.UID}}"> <input type="hidden" name="next" value="{{$back}}"> <input type="hidden" name="to" value="Junk"> <button>Report Spam</button> @@ -47,13 +47,13 @@ {{ if or (eq .Mailbox.Name "Trash") (eq .Mailbox.Name "Junk") }} <form class="action-group" method="post" action="/message/{{.Mailbox.Name | pathescape}}/delete"> - <input type="hidden" name="uids" value="{{.Message.Uid}}"> + <input type="hidden" name="uids" value="{{.Message.UID}}"> <input type="hidden" name="next" value="{{$back}}"> <button>Delete Permanently</button> </form> {{ else }} <form class="action-group" method="post" action="/message/{{.Mailbox.Name | pathescape}}/move"> - <input type="hidden" name="uids" value="{{.Message.Uid}}"> + <input type="hidden" name="uids" value="{{.Message.UID}}"> <input type="hidden" name="next" value="{{$back}}"> <input type="hidden" name="to" value="Trash"> <button>Delete</button> @@ -61,7 +61,7 @@ {{ end }} <form class="action-group" method="post" action="/message/{{.Mailbox.Name | pathescape}}/flag"> - <input type="hidden" name="uids" value="{{.Message.Uid}}"> + <input type="hidden" name="uids" value="{{.Message.UID}}"> <input type="hidden" name="action" value="remove"> <input type="hidden" name="flags" value="\Seen"> <input type="hidden" name="next" value="{{$back}}"> @@ -69,7 +69,7 @@ </form> <form class="action-group" method="post" action="/message/{{.Mailbox.Name | pathescape}}/move"> - <input type="hidden" name="uids" value="{{.Message.Uid}}"> + <input type="hidden" name="uids" value="{{.Message.UID}}"> <select class="action-group" name="to"> {{range .Mailboxes}} <option value="{{.Name}}" {{if eq .Name $.Mailbox.Name}}selected>Move to...{{else}}>{{.Name}}{{ end }}</option> @@ -176,8 +176,8 @@ {{define "addr-list"}} {{range $i, $addr := .}} {{if $i}},{{end}} - <strong>{{.PersonalName}}</strong> - <<a href="/compose?to={{.Address}}">{{.Address}}</a>> + <strong>{{.Name}}</strong> + <<a href="/compose?to={{.Addr}}">{{.Addr}}</a>> {{end}} {{end}} diff --git a/themes/alps/util.html b/themes/alps/util.html index 9cdfd86..bd580c3 100644 --- a/themes/alps/util.html +++ b/themes/alps/util.html @@ -9,10 +9,8 @@ {{- end -}} {{- if .Info.HasAttr "\\HasChildren" }}/{{ end }} </a> - {{ if .Status }} - {{ if .Status.Unseen }} - <span class="unseen">({{.Status.Unseen}})</span> - {{ end }} + {{ if gt .Info.Unseen 0 }} + <span class="unseen">({{.Info.Unseen}})</span> {{ end }} </li> {{ else }} |