A webmail client. Forked from https://git.sr.ht/~migadu/alps
選択できるのは25トピックまでです。 トピックは、先頭が英数字で、英数字とダッシュ('-')を使用した35文字以内のものにしてください。
 
 
 
 

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