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.
 
 
 
 

626 lines
14 KiB

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