aboutsummaryrefslogtreecommitdiffstats
path: root/plugins/base/imap.go
diff options
context:
space:
mode:
authorLibravatarLibravatar Simon Ser <[email protected]> 2019-12-16 12:51:42 +0100
committerLibravatarLibravatar Simon Ser <[email protected]> 2019-12-16 12:52:44 +0100
commitd897eeee5c4d163891d0b6a8f85d328ccada7575 (patch)
tree39d2428b8f9cb677d79e66cb3763cdaed6147616 /plugins/base/imap.go
parente83844fbad63a0d6fc2d29a8a412c95f2a419b56 (diff)
downloadalps-d897eeee5c4d163891d0b6a8f85d328ccada7575.tar.gz
alps-d897eeee5c4d163891d0b6a8f85d328ccada7575.tar.bz2
alps-d897eeee5c4d163891d0b6a8f85d328ccada7575.zip
Introduce base plugin
This plugin offers base IMAP/SMTP functionality. References: https://todo.sr.ht/~sircmpwn/koushin/39
Diffstat (limited to 'plugins/base/imap.go')
-rw-r--r--plugins/base/imap.go277
1 files changed, 277 insertions, 0 deletions
diff --git a/plugins/base/imap.go b/plugins/base/imap.go
new file mode 100644
index 0000000..93f3c4e
--- /dev/null
+++ b/plugins/base/imap.go
@@ -0,0 +1,277 @@
+package koushinbase
+
+import (
+ "bufio"
+ "fmt"
+ "sort"
+ "strconv"
+ "strings"
+
+ "github.com/emersion/go-imap"
+ imapclient "github.com/emersion/go-imap/client"
+ "github.com/emersion/go-message"
+ "github.com/emersion/go-message/textproto"
+)
+
+func listMailboxes(conn *imapclient.Client) ([]*imap.MailboxInfo, error) {
+ ch := make(chan *imap.MailboxInfo, 10)
+ done := make(chan error, 1)
+ go func() {
+ done <- conn.List("", "*", ch)
+ }()
+
+ var mailboxes []*imap.MailboxInfo
+ for mbox := range ch {
+ mailboxes = append(mailboxes, mbox)
+ }
+
+ if err := <-done; err != nil {
+ return nil, fmt.Errorf("failed to list mailboxes: %v", err)
+ }
+
+ sort.Slice(mailboxes, func(i, j int) bool {
+ return mailboxes[i].Name < mailboxes[j].Name
+ })
+ return mailboxes, nil
+}
+
+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 {
+ return fmt.Errorf("failed to select mailbox: %v", err)
+ }
+ }
+ return nil
+}
+
+type imapMessage struct {
+ *imap.Message
+}
+
+func textPartPath(bs *imap.BodyStructure) ([]int, bool) {
+ if bs.Disposition != "" && !strings.EqualFold(bs.Disposition, "inline") {
+ return nil, false
+ }
+
+ if strings.EqualFold(bs.MIMEType, "text") {
+ return []int{1}, true
+ }
+
+ if !strings.EqualFold(bs.MIMEType, "multipart") {
+ return nil, false
+ }
+
+ textPartNum := -1
+ for i, part := range bs.Parts {
+ num := i + 1
+
+ if strings.EqualFold(part.MIMEType, "multipart") {
+ if subpath, ok := textPartPath(part); ok {
+ return append([]int{num}, subpath...), true
+ }
+ }
+ if !strings.EqualFold(part.MIMEType, "text") {
+ continue
+ }
+
+ var pick bool
+ switch strings.ToLower(part.MIMESubType) {
+ case "plain":
+ pick = true
+ case "html":
+ pick = textPartNum < 0
+ }
+
+ if pick {
+ textPartNum = num
+ }
+ }
+
+ if textPartNum > 0 {
+ return []int{textPartNum}, true
+ }
+ return nil, false
+}
+
+func (msg *imapMessage) TextPartName() string {
+ if msg.BodyStructure == nil {
+ return ""
+ }
+
+ path, ok := textPartPath(msg.BodyStructure)
+ if !ok {
+ return ""
+ }
+
+ l := make([]string, len(path))
+ for i, partNum := range path {
+ l[i] = strconv.Itoa(partNum)
+ }
+
+ return strings.Join(l, ".")
+}
+
+type IMAPPartNode struct {
+ Path []int
+ MIMEType string
+ Filename string
+ Children []IMAPPartNode
+}
+
+func (node IMAPPartNode) PathString() string {
+ l := make([]string, len(node.Path))
+ for i, partNum := range node.Path {
+ l[i] = strconv.Itoa(partNum)
+ }
+
+ return strings.Join(l, ".")
+}
+
+func (node IMAPPartNode) IsText() bool {
+ return strings.HasPrefix(strings.ToLower(node.MIMEType), "text/")
+}
+
+func (node IMAPPartNode) String() string {
+ if node.Filename != "" {
+ return fmt.Sprintf("%s (%s)", node.Filename, node.MIMEType)
+ } else {
+ return node.MIMEType
+ }
+}
+
+func imapPartTree(bs *imap.BodyStructure, path []int) *IMAPPartNode {
+ if !strings.EqualFold(bs.MIMEType, "multipart") && len(path) == 0 {
+ path = []int{1}
+ }
+
+ filename, _ := bs.Filename()
+
+ node := &IMAPPartNode{
+ Path: path,
+ MIMEType: strings.ToLower(bs.MIMEType + "/" + bs.MIMESubType),
+ Filename: filename,
+ Children: make([]IMAPPartNode, len(bs.Parts)),
+ }
+
+ for i, part := range bs.Parts {
+ num := i + 1
+
+ partPath := append([]int(nil), path...)
+ partPath = append(partPath, num)
+
+ node.Children[i] = *imapPartTree(part, partPath)
+ }
+
+ return node
+}
+
+func (msg *imapMessage) PartTree() *IMAPPartNode {
+ if msg.BodyStructure == nil {
+ return nil
+ }
+
+ return imapPartTree(msg.BodyStructure, nil)
+}
+
+func listMessages(conn *imapclient.Client, mboxName string, page int) ([]imapMessage, error) {
+ if err := ensureMailboxSelected(conn, mboxName); err != nil {
+ return nil, err
+ }
+
+ mbox := conn.Mailbox()
+ to := int(mbox.Messages) - page*messagesPerPage
+ from := to - messagesPerPage + 1
+ if from <= 0 {
+ from = 1
+ }
+ if to <= 0 {
+ return nil, nil
+ }
+
+ seqSet := new(imap.SeqSet)
+ seqSet.AddRange(uint32(from), uint32(to))
+
+ fetch := []imap.FetchItem{imap.FetchEnvelope, imap.FetchUid, imap.FetchBodyStructure}
+
+ 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})
+ }
+
+ if err := <-done; err != nil {
+ return nil, fmt.Errorf("failed to fetch message list: %v", err)
+ }
+
+ // Reverse list of messages
+ for i := len(msgs)/2 - 1; i >= 0; i-- {
+ opp := len(msgs) - 1 - i
+ msgs[i], msgs[opp] = msgs[opp], msgs[i]
+ }
+
+ return msgs, nil
+}
+
+func getMessagePart(conn *imapclient.Client, mboxName string, uid uint32, partPath []int) (*imapMessage, *message.Entity, error) {
+ if err := ensureMailboxSelected(conn, mboxName); err != nil {
+ return nil, nil, err
+ }
+
+ seqSet := new(imap.SeqSet)
+ seqSet.AddNum(uid)
+
+ var partHeaderSection imap.BodySectionName
+ partHeaderSection.Peek = true
+ if len(partPath) > 0 {
+ partHeaderSection.Specifier = imap.MIMESpecifier
+ } else {
+ partHeaderSection.Specifier = imap.HeaderSpecifier
+ }
+ partHeaderSection.Path = partPath
+
+ var partBodySection imap.BodySectionName
+ partBodySection.Peek = true
+ if len(partPath) > 0 {
+ partBodySection.Specifier = imap.EntireSpecifier
+ } else {
+ partBodySection.Specifier = imap.TextSpecifier
+ }
+ partBodySection.Path = partPath
+
+ fetch := []imap.FetchItem{
+ imap.FetchEnvelope,
+ imap.FetchUid,
+ imap.FetchBodyStructure,
+ partHeaderSection.FetchItem(),
+ partBodySection.FetchItem(),
+ }
+
+ ch := make(chan *imap.Message, 1)
+ if err := conn.UidFetch(seqSet, fetch, ch); err != nil {
+ return nil, nil, fmt.Errorf("failed to fetch message: %v", err)
+ }
+
+ msg := <-ch
+ if msg == nil {
+ return nil, nil, fmt.Errorf("server didn't return message")
+ }
+
+ headerReader := bufio.NewReader(msg.GetBody(&partHeaderSection))
+ h, err := textproto.ReadHeader(headerReader)
+ 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))
+ if err != nil {
+ return nil, nil, fmt.Errorf("failed to create message reader: %v", err)
+ }
+
+ return &imapMessage{msg}, part, nil
+}