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

573 行
12 KiB

  1. package alpsbase
  2. import (
  3. "bufio"
  4. "bytes"
  5. "fmt"
  6. "net/url"
  7. "sort"
  8. "strconv"
  9. "strings"
  10. "time"
  11. "github.com/emersion/go-imap"
  12. imapspecialuse "github.com/emersion/go-imap-specialuse"
  13. imapclient "github.com/emersion/go-imap/client"
  14. "github.com/emersion/go-message"
  15. "github.com/emersion/go-message/textproto"
  16. )
  17. type MailboxInfo struct {
  18. *imap.MailboxInfo
  19. Active bool
  20. Unseen int
  21. }
  22. func (mbox *MailboxInfo) URL() *url.URL {
  23. return &url.URL{
  24. Path: fmt.Sprintf("/mailbox/%v", url.PathEscape(mbox.Name)),
  25. }
  26. }
  27. func (mbox *MailboxInfo) HasAttr(flag string) bool {
  28. for _, attr := range mbox.Attributes {
  29. if attr == flag {
  30. return true
  31. }
  32. }
  33. return false
  34. }
  35. func listMailboxes(conn *imapclient.Client) ([]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 []MailboxInfo
  42. for mbox := range ch {
  43. mailboxes = append(mailboxes, MailboxInfo{mbox, false, -1})
  44. }
  45. if err := <-done; err != nil {
  46. return nil, fmt.Errorf("failed to list mailboxes: %v", err)
  47. }
  48. sort.Slice(mailboxes, func(i, j int) bool {
  49. if mailboxes[i].Name == "INBOX" {
  50. return true
  51. }
  52. if mailboxes[j].Name == "INBOX" {
  53. return false
  54. }
  55. return mailboxes[i].Name < mailboxes[j].Name
  56. })
  57. return mailboxes, nil
  58. }
  59. type MailboxStatus struct {
  60. *imap.MailboxStatus
  61. }
  62. func (mbox *MailboxStatus) URL() *url.URL {
  63. return &url.URL{
  64. Path: fmt.Sprintf("/mailbox/%v", url.PathEscape(mbox.Name)),
  65. }
  66. }
  67. func getMailboxStatus(conn *imapclient.Client, name string) (*MailboxStatus, error) {
  68. items := []imap.StatusItem{
  69. imap.StatusMessages,
  70. imap.StatusUidValidity,
  71. imap.StatusUnseen,
  72. }
  73. status, err := conn.Status(name, items)
  74. if err != nil {
  75. return nil, fmt.Errorf("failed to get mailbox status: %v", err)
  76. }
  77. return &MailboxStatus{status}, nil
  78. }
  79. type mailboxType int
  80. const (
  81. mailboxSent mailboxType = iota
  82. mailboxDrafts
  83. )
  84. func getMailboxByType(conn *imapclient.Client, mboxType mailboxType) (*MailboxInfo, error) {
  85. ch := make(chan *imap.MailboxInfo, 10)
  86. done := make(chan error, 1)
  87. go func() {
  88. done <- conn.List("", "%", ch)
  89. }()
  90. // TODO: configurable fallback names?
  91. var attr string
  92. var fallbackNames []string
  93. switch mboxType {
  94. case mailboxSent:
  95. attr = imapspecialuse.Sent
  96. fallbackNames = []string{"Sent"}
  97. case mailboxDrafts:
  98. attr = imapspecialuse.Drafts
  99. fallbackNames = []string{"Draft", "Drafts"}
  100. }
  101. var attrMatched bool
  102. var best *imap.MailboxInfo
  103. for mbox := range ch {
  104. for _, a := range mbox.Attributes {
  105. if attr == a {
  106. best = mbox
  107. attrMatched = true
  108. break
  109. }
  110. }
  111. if attrMatched {
  112. break
  113. }
  114. for _, fallback := range fallbackNames {
  115. if strings.EqualFold(fallback, mbox.Name) {
  116. best = mbox
  117. break
  118. }
  119. }
  120. }
  121. if err := <-done; err != nil {
  122. return nil, fmt.Errorf("failed to get mailbox with attribute %q: %v", attr, err)
  123. }
  124. if best == nil {
  125. return nil, nil
  126. }
  127. return &MailboxInfo{best, false, -1}, nil
  128. }
  129. func ensureMailboxSelected(conn *imapclient.Client, mboxName string) error {
  130. mbox := conn.Mailbox()
  131. if mbox == nil || mbox.Name != mboxName {
  132. if _, err := conn.Select(mboxName, false); err != nil {
  133. return fmt.Errorf("failed to select mailbox: %v", err)
  134. }
  135. }
  136. return nil
  137. }
  138. type IMAPMessage struct {
  139. *imap.Message
  140. Mailbox string
  141. }
  142. func (msg *IMAPMessage) URL() *url.URL {
  143. return &url.URL{
  144. Path: fmt.Sprintf("/message/%v/%v", url.PathEscape(msg.Mailbox), msg.Uid),
  145. }
  146. }
  147. func newIMAPPartNode(msg *IMAPMessage, path []int, part *imap.BodyStructure) *IMAPPartNode {
  148. filename, _ := part.Filename()
  149. return &IMAPPartNode{
  150. Path: path,
  151. MIMEType: strings.ToLower(part.MIMEType + "/" + part.MIMESubType),
  152. Filename: filename,
  153. Message: msg,
  154. }
  155. }
  156. func (msg *IMAPMessage) TextPart() *IMAPPartNode {
  157. if msg.BodyStructure == nil {
  158. return nil
  159. }
  160. var best *IMAPPartNode
  161. isTextPlain := false
  162. msg.BodyStructure.Walk(func(path []int, part *imap.BodyStructure) bool {
  163. if !strings.EqualFold(part.MIMEType, "text") {
  164. return true
  165. }
  166. if part.Disposition != "" && !strings.EqualFold(part.Disposition, "inline") {
  167. return true
  168. }
  169. switch strings.ToLower(part.MIMESubType) {
  170. case "plain":
  171. isTextPlain = true
  172. best = newIMAPPartNode(msg, path, part)
  173. case "html":
  174. if !isTextPlain {
  175. best = newIMAPPartNode(msg, path, part)
  176. }
  177. }
  178. return true
  179. })
  180. return best
  181. }
  182. func (msg *IMAPMessage) Attachments() []IMAPPartNode {
  183. if msg.BodyStructure == nil {
  184. return nil
  185. }
  186. var attachments []IMAPPartNode
  187. msg.BodyStructure.Walk(func(path []int, part *imap.BodyStructure) bool {
  188. if !strings.EqualFold(part.Disposition, "attachment") {
  189. return true
  190. }
  191. attachments = append(attachments, *newIMAPPartNode(msg, path, part))
  192. return true
  193. })
  194. return attachments
  195. }
  196. func pathsEqual(a, b []int) bool {
  197. if len(a) != len(b) {
  198. return false
  199. }
  200. for i := range a {
  201. if a[i] != b[i] {
  202. return false
  203. }
  204. }
  205. return true
  206. }
  207. func (msg *IMAPMessage) PartByPath(path []int) *IMAPPartNode {
  208. if msg.BodyStructure == nil {
  209. return nil
  210. }
  211. if len(path) == 0 {
  212. return newIMAPPartNode(msg, nil, msg.BodyStructure)
  213. }
  214. var result *IMAPPartNode
  215. msg.BodyStructure.Walk(func(p []int, part *imap.BodyStructure) bool {
  216. if result == nil && pathsEqual(path, p) {
  217. result = newIMAPPartNode(msg, p, part)
  218. }
  219. return result == nil
  220. })
  221. return result
  222. }
  223. func (msg *IMAPMessage) PartByID(id string) *IMAPPartNode {
  224. if msg.BodyStructure == nil || id == "" {
  225. return nil
  226. }
  227. var result *IMAPPartNode
  228. msg.BodyStructure.Walk(func(path []int, part *imap.BodyStructure) bool {
  229. if result == nil && part.Id == "<"+id+">" {
  230. result = newIMAPPartNode(msg, path, part)
  231. }
  232. return result == nil
  233. })
  234. return result
  235. }
  236. type IMAPPartNode struct {
  237. Path []int
  238. MIMEType string
  239. Filename string
  240. Children []IMAPPartNode
  241. Message *IMAPMessage
  242. }
  243. func (node IMAPPartNode) PathString() string {
  244. l := make([]string, len(node.Path))
  245. for i, partNum := range node.Path {
  246. l[i] = strconv.Itoa(partNum)
  247. }
  248. return strings.Join(l, ".")
  249. }
  250. func (node IMAPPartNode) URL(raw bool) *url.URL {
  251. u := node.Message.URL()
  252. if raw {
  253. u.Path += "/raw"
  254. }
  255. q := u.Query()
  256. q.Set("part", node.PathString())
  257. u.RawQuery = q.Encode()
  258. return u
  259. }
  260. func (node IMAPPartNode) IsText() bool {
  261. return strings.HasPrefix(strings.ToLower(node.MIMEType), "text/")
  262. }
  263. func (node IMAPPartNode) String() string {
  264. if node.Filename != "" {
  265. return fmt.Sprintf("%s (%s)", node.Filename, node.MIMEType)
  266. } else {
  267. return node.MIMEType
  268. }
  269. }
  270. func imapPartTree(msg *IMAPMessage, bs *imap.BodyStructure, path []int) *IMAPPartNode {
  271. if !strings.EqualFold(bs.MIMEType, "multipart") && len(path) == 0 {
  272. path = []int{1}
  273. }
  274. filename, _ := bs.Filename()
  275. node := &IMAPPartNode{
  276. Path: path,
  277. MIMEType: strings.ToLower(bs.MIMEType + "/" + bs.MIMESubType),
  278. Filename: filename,
  279. Children: make([]IMAPPartNode, len(bs.Parts)),
  280. Message: msg,
  281. }
  282. for i, part := range bs.Parts {
  283. num := i + 1
  284. partPath := append([]int(nil), path...)
  285. partPath = append(partPath, num)
  286. node.Children[i] = *imapPartTree(msg, part, partPath)
  287. }
  288. return node
  289. }
  290. func (msg *IMAPMessage) PartTree() *IMAPPartNode {
  291. if msg.BodyStructure == nil {
  292. return nil
  293. }
  294. return imapPartTree(msg, msg.BodyStructure, nil)
  295. }
  296. func (msg *IMAPMessage) HasFlag(flag string) bool {
  297. for _, f := range msg.Flags {
  298. if imap.CanonicalFlag(f) == flag {
  299. return true
  300. }
  301. }
  302. return false
  303. }
  304. func listMessages(conn *imapclient.Client, mbox *MailboxStatus, page, messagesPerPage int) ([]IMAPMessage, error) {
  305. if err := ensureMailboxSelected(conn, mbox.Name); err != nil {
  306. return nil, err
  307. }
  308. to := int(mbox.Messages) - page*messagesPerPage
  309. from := to - messagesPerPage + 1
  310. if from <= 0 {
  311. from = 1
  312. }
  313. if to <= 0 {
  314. return nil, nil
  315. }
  316. var seqSet imap.SeqSet
  317. seqSet.AddRange(uint32(from), uint32(to))
  318. fetch := []imap.FetchItem{imap.FetchFlags, imap.FetchEnvelope, imap.FetchUid, imap.FetchBodyStructure}
  319. ch := make(chan *imap.Message, 10)
  320. done := make(chan error, 1)
  321. go func() {
  322. done <- conn.Fetch(&seqSet, fetch, ch)
  323. }()
  324. msgs := make([]IMAPMessage, 0, to-from)
  325. for msg := range ch {
  326. msgs = append(msgs, IMAPMessage{msg, mbox.Name})
  327. }
  328. if err := <-done; err != nil {
  329. return nil, fmt.Errorf("failed to fetch message list: %v", err)
  330. }
  331. // Reverse list of messages
  332. for i := len(msgs)/2 - 1; i >= 0; i-- {
  333. opp := len(msgs) - 1 - i
  334. msgs[i], msgs[opp] = msgs[opp], msgs[i]
  335. }
  336. return msgs, nil
  337. }
  338. func searchMessages(conn *imapclient.Client, mboxName, query string, page, messagesPerPage int) (msgs []IMAPMessage, total int, err error) {
  339. if err := ensureMailboxSelected(conn, mboxName); err != nil {
  340. return nil, 0, err
  341. }
  342. criteria := PrepareSearch(query)
  343. nums, err := conn.Search(criteria)
  344. if err != nil {
  345. return nil, 0, fmt.Errorf("UID SEARCH failed: %v", err)
  346. }
  347. total = len(nums)
  348. from := page * messagesPerPage
  349. to := from + messagesPerPage
  350. if from >= len(nums) {
  351. return nil, total, nil
  352. }
  353. if to > len(nums) {
  354. to = len(nums)
  355. }
  356. nums = nums[from:to]
  357. indexes := make(map[uint32]int)
  358. for i, num := range nums {
  359. indexes[num] = i
  360. }
  361. var seqSet imap.SeqSet
  362. seqSet.AddNum(nums...)
  363. fetch := []imap.FetchItem{imap.FetchEnvelope, imap.FetchFlags, imap.FetchUid, imap.FetchBodyStructure}
  364. ch := make(chan *imap.Message, 10)
  365. done := make(chan error, 1)
  366. go func() {
  367. done <- conn.Fetch(&seqSet, fetch, ch)
  368. }()
  369. msgs = make([]IMAPMessage, len(nums))
  370. for msg := range ch {
  371. i, ok := indexes[msg.SeqNum]
  372. if !ok {
  373. continue
  374. }
  375. msgs[i] = IMAPMessage{msg, mboxName}
  376. }
  377. if err := <-done; err != nil {
  378. return nil, 0, fmt.Errorf("failed to fetch message list: %v", err)
  379. }
  380. return msgs, total, nil
  381. }
  382. func getMessagePart(conn *imapclient.Client, mboxName string, uid uint32, partPath []int) (*IMAPMessage, *message.Entity, error) {
  383. if err := ensureMailboxSelected(conn, mboxName); err != nil {
  384. return nil, nil, err
  385. }
  386. seqSet := new(imap.SeqSet)
  387. seqSet.AddNum(uid)
  388. var partHeaderSection imap.BodySectionName
  389. partHeaderSection.Peek = true
  390. if len(partPath) > 0 {
  391. partHeaderSection.Specifier = imap.MIMESpecifier
  392. } else {
  393. partHeaderSection.Specifier = imap.HeaderSpecifier
  394. }
  395. partHeaderSection.Path = partPath
  396. var partBodySection imap.BodySectionName
  397. if len(partPath) > 0 {
  398. partBodySection.Specifier = imap.EntireSpecifier
  399. } else {
  400. partBodySection.Specifier = imap.TextSpecifier
  401. }
  402. partBodySection.Path = partPath
  403. fetch := []imap.FetchItem{
  404. imap.FetchEnvelope,
  405. imap.FetchUid,
  406. imap.FetchBodyStructure,
  407. imap.FetchFlags,
  408. partHeaderSection.FetchItem(),
  409. partBodySection.FetchItem(),
  410. }
  411. ch := make(chan *imap.Message, 1)
  412. if err := conn.UidFetch(seqSet, fetch, ch); err != nil {
  413. return nil, nil, fmt.Errorf("failed to fetch message: %v", err)
  414. }
  415. msg := <-ch
  416. if msg == nil {
  417. return nil, nil, fmt.Errorf("server didn't return message")
  418. }
  419. body := msg.GetBody(&partHeaderSection)
  420. if body == nil {
  421. return nil, nil, fmt.Errorf("server didn't return message")
  422. }
  423. headerReader := bufio.NewReader(body)
  424. h, err := textproto.ReadHeader(headerReader)
  425. if err != nil {
  426. return nil, nil, fmt.Errorf("failed to read part header: %v", err)
  427. }
  428. part, err := message.New(message.Header{h}, msg.GetBody(&partBodySection))
  429. if err != nil {
  430. return nil, nil, fmt.Errorf("failed to create message reader: %v", err)
  431. }
  432. return &IMAPMessage{msg, mboxName}, part, nil
  433. }
  434. func markMessageAnswered(conn *imapclient.Client, mboxName string, uid uint32) error {
  435. if err := ensureMailboxSelected(conn, mboxName); err != nil {
  436. return err
  437. }
  438. seqSet := new(imap.SeqSet)
  439. seqSet.AddNum(uid)
  440. item := imap.FormatFlagsOp(imap.AddFlags, true)
  441. flags := []interface{}{imap.AnsweredFlag}
  442. return conn.UidStore(seqSet, item, flags, nil)
  443. }
  444. func appendMessage(c *imapclient.Client, msg *OutgoingMessage, mboxType mailboxType) (saved bool, err error) {
  445. mbox, err := getMailboxByType(c, mboxType)
  446. if err != nil {
  447. return false, err
  448. }
  449. if mbox == nil {
  450. return false, nil
  451. }
  452. // IMAP needs to know in advance the final size of the message, so
  453. // there's no way around storing it in a buffer here.
  454. var buf bytes.Buffer
  455. if err := msg.WriteTo(&buf); err != nil {
  456. return false, err
  457. }
  458. flags := []string{imap.SeenFlag}
  459. if mboxType == mailboxDrafts {
  460. flags = append(flags, imap.DraftFlag)
  461. }
  462. if err := c.Append(mbox.Name, flags, time.Now(), &buf); err != nil {
  463. return false, err
  464. }
  465. return true, nil
  466. }
  467. func deleteMessage(c *imapclient.Client, mboxName string, uid uint32) error {
  468. if err := ensureMailboxSelected(c, mboxName); err != nil {
  469. return err
  470. }
  471. seqSet := new(imap.SeqSet)
  472. seqSet.AddNum(uid)
  473. item := imap.FormatFlagsOp(imap.AddFlags, true)
  474. flags := []interface{}{imap.DeletedFlag}
  475. if err := c.UidStore(seqSet, item, flags, nil); err != nil {
  476. return err
  477. }
  478. return c.Expunge(nil)
  479. }