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.
 
 
 
 

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