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