diff options
Diffstat (limited to 'plugins/base')
-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 |
4 files changed, 283 insertions, 282 deletions
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 |