A webmail client. Forked from https://git.sr.ht/~migadu/alps
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

295 lines
6.0 KiB

  1. package koushin
  2. import (
  3. "bufio"
  4. "fmt"
  5. "sort"
  6. "strconv"
  7. "strings"
  8. "github.com/emersion/go-imap"
  9. imapclient "github.com/emersion/go-imap/client"
  10. "github.com/emersion/go-message"
  11. "github.com/emersion/go-message/textproto"
  12. )
  13. func (s *Server) connectIMAP() (*imapclient.Client, error) {
  14. var c *imapclient.Client
  15. var err error
  16. if s.imap.tls {
  17. c, err = imapclient.DialTLS(s.imap.host, nil)
  18. if err != nil {
  19. return nil, err
  20. }
  21. } else {
  22. c, err = imapclient.Dial(s.imap.host)
  23. if err != nil {
  24. return nil, err
  25. }
  26. if !s.imap.insecure {
  27. if err := c.StartTLS(nil); err != nil {
  28. c.Close()
  29. return nil, err
  30. }
  31. }
  32. }
  33. return c, err
  34. }
  35. func listMailboxes(conn *imapclient.Client) ([]*imap.MailboxInfo, error) {
  36. ch := make(chan *imap.MailboxInfo, 10)
  37. done := make(chan error, 1)
  38. go func() {
  39. done <- conn.List("", "*", ch)
  40. }()
  41. var mailboxes []*imap.MailboxInfo
  42. for mbox := range ch {
  43. mailboxes = append(mailboxes, mbox)
  44. }
  45. if err := <-done; err != nil {
  46. return nil, err
  47. }
  48. sort.Slice(mailboxes, func(i, j int) bool {
  49. return mailboxes[i].Name < mailboxes[j].Name
  50. })
  51. return mailboxes, nil
  52. }
  53. func ensureMailboxSelected(conn *imapclient.Client, mboxName string) error {
  54. mbox := conn.Mailbox()
  55. if mbox == nil || mbox.Name != mboxName {
  56. if _, err := conn.Select(mboxName, false); err != nil {
  57. return err
  58. }
  59. }
  60. return nil
  61. }
  62. type imapMessage struct {
  63. *imap.Message
  64. }
  65. func textPartPath(bs *imap.BodyStructure) ([]int, bool) {
  66. if bs.Disposition != "" && !strings.EqualFold(bs.Disposition, "inline") {
  67. return nil, false
  68. }
  69. if strings.EqualFold(bs.MIMEType, "text") {
  70. return []int{1}, true
  71. }
  72. if !strings.EqualFold(bs.MIMEType, "multipart") {
  73. return nil, false
  74. }
  75. textPartNum := -1
  76. for i, part := range bs.Parts {
  77. num := i + 1
  78. if strings.EqualFold(part.MIMEType, "multipart") {
  79. if subpath, ok := textPartPath(part); ok {
  80. return append([]int{num}, subpath...), true
  81. }
  82. }
  83. if !strings.EqualFold(part.MIMEType, "text") {
  84. continue
  85. }
  86. var pick bool
  87. switch strings.ToLower(part.MIMESubType) {
  88. case "plain":
  89. pick = true
  90. case "html":
  91. pick = textPartNum < 0
  92. }
  93. if pick {
  94. textPartNum = num
  95. }
  96. }
  97. if textPartNum > 0 {
  98. return []int{textPartNum}, true
  99. }
  100. return nil, false
  101. }
  102. func (msg *imapMessage) TextPartName() string {
  103. if msg.BodyStructure == nil {
  104. return ""
  105. }
  106. path, ok := textPartPath(msg.BodyStructure)
  107. if !ok {
  108. return ""
  109. }
  110. l := make([]string, len(path))
  111. for i, partNum := range path {
  112. l[i] = strconv.Itoa(partNum)
  113. }
  114. return strings.Join(l, ".")
  115. }
  116. type IMAPPartNode struct {
  117. Path []int
  118. MIMEType string
  119. Filename string
  120. Children []IMAPPartNode
  121. }
  122. func (node IMAPPartNode) PathString() string {
  123. l := make([]string, len(node.Path))
  124. for i, partNum := range node.Path {
  125. l[i] = strconv.Itoa(partNum)
  126. }
  127. return strings.Join(l, ".")
  128. }
  129. func (node IMAPPartNode) IsText() bool {
  130. return strings.HasPrefix(strings.ToLower(node.MIMEType), "text/")
  131. }
  132. func (node IMAPPartNode) String() string {
  133. if node.Filename != "" {
  134. return fmt.Sprintf("%s (%s)", node.Filename, node.MIMEType)
  135. } else {
  136. return node.MIMEType
  137. }
  138. }
  139. func imapPartTree(bs *imap.BodyStructure, path []int) *IMAPPartNode {
  140. if !strings.EqualFold(bs.MIMEType, "multipart") && len(path) == 0 {
  141. path = []int{1}
  142. }
  143. var filename string
  144. if strings.EqualFold(bs.Disposition, "attachment") {
  145. filename = bs.DispositionParams["filename"]
  146. }
  147. node := &IMAPPartNode{
  148. Path: path,
  149. MIMEType: strings.ToLower(bs.MIMEType + "/" + bs.MIMESubType),
  150. Filename: filename,
  151. Children: make([]IMAPPartNode, len(bs.Parts)),
  152. }
  153. for i, part := range bs.Parts {
  154. num := i + 1
  155. partPath := append([]int(nil), path...)
  156. partPath = append(partPath, num)
  157. node.Children[i] = *imapPartTree(part, partPath)
  158. }
  159. return node
  160. }
  161. func (msg *imapMessage) PartTree() *IMAPPartNode {
  162. if msg.BodyStructure == nil {
  163. return nil
  164. }
  165. return imapPartTree(msg.BodyStructure, nil)
  166. }
  167. func listMessages(conn *imapclient.Client, mboxName string) ([]imapMessage, error) {
  168. if err := ensureMailboxSelected(conn, mboxName); err != nil {
  169. return nil, err
  170. }
  171. n := uint32(50)
  172. mbox := conn.Mailbox()
  173. from := uint32(1)
  174. to := mbox.Messages
  175. if mbox.Messages > n {
  176. from = mbox.Messages - n
  177. }
  178. seqSet := new(imap.SeqSet)
  179. seqSet.AddRange(from, to)
  180. fetch := []imap.FetchItem{imap.FetchEnvelope, imap.FetchUid, imap.FetchBodyStructure}
  181. ch := make(chan *imap.Message, 10)
  182. done := make(chan error, 1)
  183. go func() {
  184. done <- conn.Fetch(seqSet, fetch, ch)
  185. }()
  186. msgs := make([]imapMessage, 0, n)
  187. for msg := range ch {
  188. msgs = append(msgs, imapMessage{msg})
  189. }
  190. if err := <-done; err != nil {
  191. return nil, err
  192. }
  193. // Reverse list of messages
  194. for i := len(msgs)/2 - 1; i >= 0; i-- {
  195. opp := len(msgs) - 1 - i
  196. msgs[i], msgs[opp] = msgs[opp], msgs[i]
  197. }
  198. return msgs, nil
  199. }
  200. func getMessagePart(conn *imapclient.Client, mboxName string, uid uint32, partPath []int) (*imapMessage, *message.Entity, error) {
  201. if err := ensureMailboxSelected(conn, mboxName); err != nil {
  202. return nil, nil, err
  203. }
  204. seqSet := new(imap.SeqSet)
  205. seqSet.AddNum(uid)
  206. var partHeaderSection imap.BodySectionName
  207. partHeaderSection.Peek = true
  208. partHeaderSection.Specifier = imap.HeaderSpecifier
  209. partHeaderSection.Path = partPath
  210. var partBodySection imap.BodySectionName
  211. partBodySection.Peek = true
  212. partBodySection.Specifier = imap.TextSpecifier
  213. partBodySection.Path = partPath
  214. fetch := []imap.FetchItem{
  215. imap.FetchEnvelope,
  216. imap.FetchUid,
  217. imap.FetchBodyStructure,
  218. partHeaderSection.FetchItem(),
  219. partBodySection.FetchItem(),
  220. }
  221. ch := make(chan *imap.Message, 1)
  222. if err := conn.UidFetch(seqSet, fetch, ch); err != nil {
  223. return nil, nil, err
  224. }
  225. msg := <-ch
  226. if msg == nil {
  227. return nil, nil, fmt.Errorf("server didn't return message")
  228. }
  229. headerReader := bufio.NewReader(msg.GetBody(&partHeaderSection))
  230. h, err := textproto.ReadHeader(headerReader)
  231. if err != nil {
  232. return nil, nil, err
  233. }
  234. part, err := message.New(message.Header{h}, msg.GetBody(&partBodySection))
  235. if err != nil {
  236. return nil, nil, err
  237. }
  238. return &imapMessage{msg}, part, nil
  239. }