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.
 
 
 
 

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