A webmail client. Forked from https://git.sr.ht/~migadu/alps
Du kannst nicht mehr als 25 Themen auswählen Themen müssen entweder mit einem Buchstaben oder einer Ziffer beginnen. Sie können Bindestriche („-“) enthalten und bis zu 35 Zeichen lang sein.
 
 
 
 

342 Zeilen
7.3 KiB

  1. package koushinbase
  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 listMailboxes(conn *imapclient.Client) ([]*imap.MailboxInfo, error) {
  14. ch := make(chan *imap.MailboxInfo, 10)
  15. done := make(chan error, 1)
  16. go func() {
  17. done <- conn.List("", "*", ch)
  18. }()
  19. var mailboxes []*imap.MailboxInfo
  20. for mbox := range ch {
  21. mailboxes = append(mailboxes, mbox)
  22. }
  23. if err := <-done; err != nil {
  24. return nil, fmt.Errorf("failed to list mailboxes: %v", err)
  25. }
  26. sort.Slice(mailboxes, func(i, j int) bool {
  27. return mailboxes[i].Name < mailboxes[j].Name
  28. })
  29. return mailboxes, nil
  30. }
  31. func ensureMailboxSelected(conn *imapclient.Client, mboxName string) error {
  32. mbox := conn.Mailbox()
  33. if mbox == nil || mbox.Name != mboxName {
  34. if _, err := conn.Select(mboxName, false); err != nil {
  35. return fmt.Errorf("failed to select mailbox: %v", err)
  36. }
  37. }
  38. return nil
  39. }
  40. type IMAPMessage struct {
  41. *imap.Message
  42. }
  43. func textPartPath(bs *imap.BodyStructure) ([]int, bool) {
  44. if bs.Disposition != "" && !strings.EqualFold(bs.Disposition, "inline") {
  45. return nil, false
  46. }
  47. if strings.EqualFold(bs.MIMEType, "text") {
  48. return []int{1}, true
  49. }
  50. if !strings.EqualFold(bs.MIMEType, "multipart") {
  51. return nil, false
  52. }
  53. textPartNum := -1
  54. for i, part := range bs.Parts {
  55. num := i + 1
  56. if strings.EqualFold(part.MIMEType, "multipart") {
  57. if subpath, ok := textPartPath(part); ok {
  58. return append([]int{num}, subpath...), true
  59. }
  60. }
  61. if !strings.EqualFold(part.MIMEType, "text") {
  62. continue
  63. }
  64. var pick bool
  65. switch strings.ToLower(part.MIMESubType) {
  66. case "plain":
  67. pick = true
  68. case "html":
  69. pick = textPartNum < 0
  70. }
  71. if pick {
  72. textPartNum = num
  73. }
  74. }
  75. if textPartNum > 0 {
  76. return []int{textPartNum}, true
  77. }
  78. return nil, false
  79. }
  80. func (msg *IMAPMessage) TextPartName() string {
  81. if msg.BodyStructure == nil {
  82. return ""
  83. }
  84. path, ok := textPartPath(msg.BodyStructure)
  85. if !ok {
  86. return ""
  87. }
  88. l := make([]string, len(path))
  89. for i, partNum := range path {
  90. l[i] = strconv.Itoa(partNum)
  91. }
  92. return strings.Join(l, ".")
  93. }
  94. type IMAPPartNode struct {
  95. Path []int
  96. MIMEType string
  97. Filename string
  98. Children []IMAPPartNode
  99. }
  100. func (node IMAPPartNode) PathString() string {
  101. l := make([]string, len(node.Path))
  102. for i, partNum := range node.Path {
  103. l[i] = strconv.Itoa(partNum)
  104. }
  105. return strings.Join(l, ".")
  106. }
  107. func (node IMAPPartNode) IsText() bool {
  108. return strings.HasPrefix(strings.ToLower(node.MIMEType), "text/")
  109. }
  110. func (node IMAPPartNode) String() string {
  111. if node.Filename != "" {
  112. return fmt.Sprintf("%s (%s)", node.Filename, node.MIMEType)
  113. } else {
  114. return node.MIMEType
  115. }
  116. }
  117. func imapPartTree(bs *imap.BodyStructure, path []int) *IMAPPartNode {
  118. if !strings.EqualFold(bs.MIMEType, "multipart") && len(path) == 0 {
  119. path = []int{1}
  120. }
  121. filename, _ := bs.Filename()
  122. node := &IMAPPartNode{
  123. Path: path,
  124. MIMEType: strings.ToLower(bs.MIMEType + "/" + bs.MIMESubType),
  125. Filename: filename,
  126. Children: make([]IMAPPartNode, len(bs.Parts)),
  127. }
  128. for i, part := range bs.Parts {
  129. num := i + 1
  130. partPath := append([]int(nil), path...)
  131. partPath = append(partPath, num)
  132. node.Children[i] = *imapPartTree(part, partPath)
  133. }
  134. return node
  135. }
  136. func (msg *IMAPMessage) PartTree() *IMAPPartNode {
  137. if msg.BodyStructure == nil {
  138. return nil
  139. }
  140. return imapPartTree(msg.BodyStructure, nil)
  141. }
  142. func (msg *IMAPMessage) HasFlag(flag string) bool {
  143. for _, f := range msg.Flags {
  144. if imap.CanonicalFlag(f) == flag {
  145. return true
  146. }
  147. }
  148. return false
  149. }
  150. func listMessages(conn *imapclient.Client, mboxName string, page int) ([]IMAPMessage, error) {
  151. if err := ensureMailboxSelected(conn, mboxName); err != nil {
  152. return nil, err
  153. }
  154. mbox := conn.Mailbox()
  155. to := int(mbox.Messages) - page*messagesPerPage
  156. from := to - messagesPerPage + 1
  157. if from <= 0 {
  158. from = 1
  159. }
  160. if to <= 0 {
  161. return nil, nil
  162. }
  163. var seqSet imap.SeqSet
  164. seqSet.AddRange(uint32(from), uint32(to))
  165. fetch := []imap.FetchItem{imap.FetchEnvelope, imap.FetchUid, imap.FetchBodyStructure}
  166. ch := make(chan *imap.Message, 10)
  167. done := make(chan error, 1)
  168. go func() {
  169. done <- conn.Fetch(&seqSet, fetch, ch)
  170. }()
  171. msgs := make([]IMAPMessage, 0, to-from)
  172. for msg := range ch {
  173. msgs = append(msgs, IMAPMessage{msg})
  174. }
  175. if err := <-done; err != nil {
  176. return nil, fmt.Errorf("failed to fetch message list: %v", err)
  177. }
  178. // Reverse list of messages
  179. for i := len(msgs)/2 - 1; i >= 0; i-- {
  180. opp := len(msgs) - 1 - i
  181. msgs[i], msgs[opp] = msgs[opp], msgs[i]
  182. }
  183. return msgs, nil
  184. }
  185. func searchMessages(conn *imapclient.Client, mboxName, query string, page int) (msgs []IMAPMessage, total int, err error) {
  186. if err := ensureMailboxSelected(conn, mboxName); err != nil {
  187. return nil, 0, err
  188. }
  189. criteria := imap.SearchCriteria{Text: []string{query}}
  190. nums, err := conn.Search(&criteria)
  191. if err != nil {
  192. return nil, 0, fmt.Errorf("UID SEARCH failed: %v", err)
  193. }
  194. total = len(nums)
  195. from := page * messagesPerPage
  196. to := from + messagesPerPage
  197. if from >= len(nums) {
  198. return nil, total, nil
  199. }
  200. if to > len(nums) {
  201. to = len(nums)
  202. }
  203. nums = nums[from:to]
  204. indexes := make(map[uint32]int)
  205. for i, num := range nums {
  206. indexes[num] = i
  207. }
  208. var seqSet imap.SeqSet
  209. seqSet.AddNum(nums...)
  210. fetch := []imap.FetchItem{imap.FetchEnvelope, imap.FetchUid, imap.FetchBodyStructure}
  211. ch := make(chan *imap.Message, 10)
  212. done := make(chan error, 1)
  213. go func() {
  214. done <- conn.Fetch(&seqSet, fetch, ch)
  215. }()
  216. msgs = make([]IMAPMessage, len(nums))
  217. for msg := range ch {
  218. i, ok := indexes[msg.SeqNum]
  219. if !ok {
  220. continue
  221. }
  222. msgs[i] = IMAPMessage{msg}
  223. }
  224. if err := <-done; err != nil {
  225. return nil, 0, fmt.Errorf("failed to fetch message list: %v", err)
  226. }
  227. return msgs, total, nil
  228. }
  229. func getMessagePart(conn *imapclient.Client, mboxName string, uid uint32, partPath []int) (*IMAPMessage, *message.Entity, error) {
  230. if err := ensureMailboxSelected(conn, mboxName); err != nil {
  231. return nil, nil, err
  232. }
  233. seqSet := new(imap.SeqSet)
  234. seqSet.AddNum(uid)
  235. var partHeaderSection imap.BodySectionName
  236. partHeaderSection.Peek = true
  237. if len(partPath) > 0 {
  238. partHeaderSection.Specifier = imap.MIMESpecifier
  239. } else {
  240. partHeaderSection.Specifier = imap.HeaderSpecifier
  241. }
  242. partHeaderSection.Path = partPath
  243. var partBodySection imap.BodySectionName
  244. partBodySection.Peek = true
  245. if len(partPath) > 0 {
  246. partBodySection.Specifier = imap.EntireSpecifier
  247. } else {
  248. partBodySection.Specifier = imap.TextSpecifier
  249. }
  250. partBodySection.Path = partPath
  251. fetch := []imap.FetchItem{
  252. imap.FetchEnvelope,
  253. imap.FetchUid,
  254. imap.FetchBodyStructure,
  255. imap.FetchFlags,
  256. partHeaderSection.FetchItem(),
  257. partBodySection.FetchItem(),
  258. }
  259. ch := make(chan *imap.Message, 1)
  260. if err := conn.UidFetch(seqSet, fetch, ch); err != nil {
  261. return nil, nil, fmt.Errorf("failed to fetch message: %v", err)
  262. }
  263. msg := <-ch
  264. if msg == nil {
  265. return nil, nil, fmt.Errorf("server didn't return message")
  266. }
  267. headerReader := bufio.NewReader(msg.GetBody(&partHeaderSection))
  268. h, err := textproto.ReadHeader(headerReader)
  269. if err != nil {
  270. return nil, nil, fmt.Errorf("failed to read part header: %v", err)
  271. }
  272. part, err := message.New(message.Header{h}, msg.GetBody(&partBodySection))
  273. if err != nil {
  274. return nil, nil, fmt.Errorf("failed to create message reader: %v", err)
  275. }
  276. return &IMAPMessage{msg}, part, nil
  277. }