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.
 
 
 
 

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