A webmail client. Forked from https://git.sr.ht/~migadu/alps
Nie możesz wybrać więcej, niż 25 tematów Tematy muszą się zaczynać od litery lub cyfry, mogą zawierać myślniki ('-') i mogą mieć do 35 znaków.
 
 
 
 

589 wiersze
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 (msg *IMAPMessage) TextPartName() string {
  138. if msg.BodyStructure == nil {
  139. return ""
  140. }
  141. var best []int
  142. isTextPlain := false
  143. msg.BodyStructure.Walk(func(path []int, part *imap.BodyStructure) bool {
  144. if !strings.EqualFold(part.MIMEType, "text") {
  145. return true
  146. }
  147. if part.Disposition != "" && !strings.EqualFold(part.Disposition, "inline") {
  148. return true
  149. }
  150. switch strings.ToLower(part.MIMESubType) {
  151. case "plain":
  152. isTextPlain = true
  153. best = path
  154. case "html":
  155. if !isTextPlain {
  156. best = path
  157. }
  158. }
  159. return true
  160. })
  161. if best == nil {
  162. return ""
  163. }
  164. l := make([]string, len(best))
  165. for i, partNum := range best {
  166. l[i] = strconv.Itoa(partNum)
  167. }
  168. return strings.Join(l, ".")
  169. }
  170. func newIMAPPartNode(msg *IMAPMessage, path []int, part *imap.BodyStructure) *IMAPPartNode {
  171. filename, _ := part.Filename()
  172. return &IMAPPartNode{
  173. Path: path,
  174. MIMEType: strings.ToLower(part.MIMEType + "/" + part.MIMESubType),
  175. Filename: filename,
  176. Message: msg,
  177. }
  178. }
  179. func (msg *IMAPMessage) Attachments() []IMAPPartNode {
  180. if msg.BodyStructure == nil {
  181. return nil
  182. }
  183. var attachments []IMAPPartNode
  184. msg.BodyStructure.Walk(func(path []int, part *imap.BodyStructure) bool {
  185. if !strings.EqualFold(part.Disposition, "attachment") {
  186. return true
  187. }
  188. attachments = append(attachments, *newIMAPPartNode(msg, path, part))
  189. return true
  190. })
  191. return attachments
  192. }
  193. func pathsEqual(a, b []int) bool {
  194. if len(a) != len(b) {
  195. return false
  196. }
  197. for i := range a {
  198. if a[i] != b[i] {
  199. return false
  200. }
  201. }
  202. return true
  203. }
  204. func (msg *IMAPMessage) PartByPath(path []int) *IMAPPartNode {
  205. if msg.BodyStructure == nil {
  206. return nil
  207. }
  208. if len(path) == 0 {
  209. return newIMAPPartNode(msg, nil, msg.BodyStructure)
  210. }
  211. var result *IMAPPartNode
  212. msg.BodyStructure.Walk(func(p []int, part *imap.BodyStructure) bool {
  213. if result == nil && pathsEqual(path, p) {
  214. result = newIMAPPartNode(msg, p, part)
  215. }
  216. return result == nil
  217. })
  218. return result
  219. }
  220. func (msg *IMAPMessage) PartByID(id string) *IMAPPartNode {
  221. if msg.BodyStructure == nil || id == "" {
  222. return nil
  223. }
  224. var result *IMAPPartNode
  225. msg.BodyStructure.Walk(func(path []int, part *imap.BodyStructure) bool {
  226. if result == nil && part.Id == "<"+id+">" {
  227. result = newIMAPPartNode(msg, path, part)
  228. }
  229. return result == nil
  230. })
  231. return result
  232. }
  233. type IMAPPartNode struct {
  234. Path []int
  235. MIMEType string
  236. Filename string
  237. Children []IMAPPartNode
  238. Message *IMAPMessage
  239. }
  240. func (node IMAPPartNode) PathString() string {
  241. l := make([]string, len(node.Path))
  242. for i, partNum := range node.Path {
  243. l[i] = strconv.Itoa(partNum)
  244. }
  245. return strings.Join(l, ".")
  246. }
  247. func (node IMAPPartNode) URL(raw bool) *url.URL {
  248. u := node.Message.URL()
  249. if raw {
  250. u.Path += "/raw"
  251. }
  252. q := u.Query()
  253. q.Set("part", node.PathString())
  254. u.RawQuery = q.Encode()
  255. return u
  256. }
  257. func (node IMAPPartNode) IsText() bool {
  258. return strings.HasPrefix(strings.ToLower(node.MIMEType), "text/")
  259. }
  260. func (node IMAPPartNode) String() string {
  261. if node.Filename != "" {
  262. return fmt.Sprintf("%s (%s)", node.Filename, node.MIMEType)
  263. } else {
  264. return node.MIMEType
  265. }
  266. }
  267. func imapPartTree(msg *IMAPMessage, bs *imap.BodyStructure, path []int) *IMAPPartNode {
  268. if !strings.EqualFold(bs.MIMEType, "multipart") && len(path) == 0 {
  269. path = []int{1}
  270. }
  271. filename, _ := bs.Filename()
  272. node := &IMAPPartNode{
  273. Path: path,
  274. MIMEType: strings.ToLower(bs.MIMEType + "/" + bs.MIMESubType),
  275. Filename: filename,
  276. Children: make([]IMAPPartNode, len(bs.Parts)),
  277. Message: msg,
  278. }
  279. for i, part := range bs.Parts {
  280. num := i + 1
  281. partPath := append([]int(nil), path...)
  282. partPath = append(partPath, num)
  283. node.Children[i] = *imapPartTree(msg, part, partPath)
  284. }
  285. return node
  286. }
  287. func (msg *IMAPMessage) PartTree() *IMAPPartNode {
  288. if msg.BodyStructure == nil {
  289. return nil
  290. }
  291. return imapPartTree(msg, msg.BodyStructure, nil)
  292. }
  293. func (msg *IMAPMessage) HasFlag(flag string) bool {
  294. for _, f := range msg.Flags {
  295. if imap.CanonicalFlag(f) == flag {
  296. return true
  297. }
  298. }
  299. return false
  300. }
  301. func listMessages(conn *imapclient.Client, mboxName string, page, messagesPerPage int) ([]IMAPMessage, error) {
  302. if err := ensureMailboxSelected(conn, mboxName); err != nil {
  303. return nil, err
  304. }
  305. mbox := conn.Mailbox()
  306. to := int(mbox.Messages) - page*messagesPerPage
  307. from := to - messagesPerPage + 1
  308. if from <= 0 {
  309. from = 1
  310. }
  311. if to <= 0 {
  312. return nil, nil
  313. }
  314. var seqSet imap.SeqSet
  315. seqSet.AddRange(uint32(from), uint32(to))
  316. fetch := []imap.FetchItem{imap.FetchFlags, imap.FetchEnvelope, imap.FetchUid, imap.FetchBodyStructure}
  317. ch := make(chan *imap.Message, 10)
  318. done := make(chan error, 1)
  319. go func() {
  320. done <- conn.Fetch(&seqSet, fetch, ch)
  321. }()
  322. msgs := make([]IMAPMessage, 0, to-from)
  323. for msg := range ch {
  324. msgs = append(msgs, IMAPMessage{msg, mboxName})
  325. }
  326. if err := <-done; err != nil {
  327. return nil, fmt.Errorf("failed to fetch message list: %v", err)
  328. }
  329. // Reverse list of messages
  330. for i := len(msgs)/2 - 1; i >= 0; i-- {
  331. opp := len(msgs) - 1 - i
  332. msgs[i], msgs[opp] = msgs[opp], msgs[i]
  333. }
  334. return msgs, nil
  335. }
  336. func searchCriteriaHeader(k, v string) *imap.SearchCriteria {
  337. return &imap.SearchCriteria{
  338. Header: map[string][]string{
  339. k: []string{v},
  340. },
  341. }
  342. }
  343. func searchCriteriaOr(criteria ...*imap.SearchCriteria) *imap.SearchCriteria {
  344. or := criteria[0]
  345. for _, c := range criteria[1:] {
  346. or = &imap.SearchCriteria{
  347. Or: [][2]*imap.SearchCriteria{{or, c}},
  348. }
  349. }
  350. return or
  351. }
  352. func searchMessages(conn *imapclient.Client, mboxName, query string, page, messagesPerPage int) (msgs []IMAPMessage, total int, err error) {
  353. if err := ensureMailboxSelected(conn, mboxName); err != nil {
  354. return nil, 0, err
  355. }
  356. // TODO: full-text search on demand (can be slow)
  357. //criteria := &imap.SearchCriteria{Text: []string{query}}
  358. criteria := searchCriteriaOr(
  359. searchCriteriaHeader("From", query),
  360. searchCriteriaHeader("To", query),
  361. searchCriteriaHeader("Cc", query),
  362. searchCriteriaHeader("Subject", query),
  363. )
  364. nums, err := conn.Search(criteria)
  365. if err != nil {
  366. return nil, 0, fmt.Errorf("UID SEARCH failed: %v", err)
  367. }
  368. total = len(nums)
  369. from := page * messagesPerPage
  370. to := from + messagesPerPage
  371. if from >= len(nums) {
  372. return nil, total, nil
  373. }
  374. if to > len(nums) {
  375. to = len(nums)
  376. }
  377. nums = nums[from:to]
  378. indexes := make(map[uint32]int)
  379. for i, num := range nums {
  380. indexes[num] = i
  381. }
  382. var seqSet imap.SeqSet
  383. seqSet.AddNum(nums...)
  384. fetch := []imap.FetchItem{imap.FetchEnvelope, imap.FetchUid, imap.FetchBodyStructure}
  385. ch := make(chan *imap.Message, 10)
  386. done := make(chan error, 1)
  387. go func() {
  388. done <- conn.Fetch(&seqSet, fetch, ch)
  389. }()
  390. msgs = make([]IMAPMessage, len(nums))
  391. for msg := range ch {
  392. i, ok := indexes[msg.SeqNum]
  393. if !ok {
  394. continue
  395. }
  396. msgs[i] = IMAPMessage{msg, mboxName}
  397. }
  398. if err := <-done; err != nil {
  399. return nil, 0, fmt.Errorf("failed to fetch message list: %v", err)
  400. }
  401. return msgs, total, nil
  402. }
  403. func getMessagePart(conn *imapclient.Client, mboxName string, uid uint32, partPath []int) (*IMAPMessage, *message.Entity, error) {
  404. if err := ensureMailboxSelected(conn, mboxName); err != nil {
  405. return nil, nil, err
  406. }
  407. seqSet := new(imap.SeqSet)
  408. seqSet.AddNum(uid)
  409. var partHeaderSection imap.BodySectionName
  410. partHeaderSection.Peek = true
  411. if len(partPath) > 0 {
  412. partHeaderSection.Specifier = imap.MIMESpecifier
  413. } else {
  414. partHeaderSection.Specifier = imap.HeaderSpecifier
  415. }
  416. partHeaderSection.Path = partPath
  417. var partBodySection imap.BodySectionName
  418. if len(partPath) > 0 {
  419. partBodySection.Specifier = imap.EntireSpecifier
  420. } else {
  421. partBodySection.Specifier = imap.TextSpecifier
  422. }
  423. partBodySection.Path = partPath
  424. fetch := []imap.FetchItem{
  425. imap.FetchEnvelope,
  426. imap.FetchUid,
  427. imap.FetchBodyStructure,
  428. imap.FetchFlags,
  429. partHeaderSection.FetchItem(),
  430. partBodySection.FetchItem(),
  431. }
  432. ch := make(chan *imap.Message, 1)
  433. if err := conn.UidFetch(seqSet, fetch, ch); err != nil {
  434. return nil, nil, fmt.Errorf("failed to fetch message: %v", err)
  435. }
  436. msg := <-ch
  437. if msg == nil {
  438. return nil, nil, fmt.Errorf("server didn't return message")
  439. }
  440. headerReader := bufio.NewReader(msg.GetBody(&partHeaderSection))
  441. h, err := textproto.ReadHeader(headerReader)
  442. if err != nil {
  443. return nil, nil, fmt.Errorf("failed to read part header: %v", err)
  444. }
  445. part, err := message.New(message.Header{h}, msg.GetBody(&partBodySection))
  446. if err != nil {
  447. return nil, nil, fmt.Errorf("failed to create message reader: %v", err)
  448. }
  449. return &IMAPMessage{msg, mboxName}, part, nil
  450. }
  451. func markMessageAnswered(conn *imapclient.Client, mboxName string, uid uint32) error {
  452. if err := ensureMailboxSelected(conn, mboxName); err != nil {
  453. return err
  454. }
  455. seqSet := new(imap.SeqSet)
  456. seqSet.AddNum(uid)
  457. item := imap.FormatFlagsOp(imap.AddFlags, true)
  458. flags := []interface{}{imap.AnsweredFlag}
  459. return conn.UidStore(seqSet, item, flags, nil)
  460. }
  461. func appendMessage(c *imapclient.Client, msg *OutgoingMessage, mboxType mailboxType) (saved bool, err error) {
  462. mbox, err := getMailboxByType(c, mboxType)
  463. if err != nil {
  464. return false, err
  465. }
  466. if mbox == nil {
  467. return false, nil
  468. }
  469. // IMAP needs to know in advance the final size of the message, so
  470. // there's no way around storing it in a buffer here.
  471. var buf bytes.Buffer
  472. if err := msg.WriteTo(&buf); err != nil {
  473. return false, err
  474. }
  475. flags := []string{imap.SeenFlag}
  476. if mboxType == mailboxDrafts {
  477. flags = append(flags, imap.DraftFlag)
  478. }
  479. if err := c.Append(mbox.Name, flags, time.Now(), &buf); err != nil {
  480. return false, err
  481. }
  482. return true, nil
  483. }
  484. func deleteMessage(c *imapclient.Client, mboxName string, uid uint32) error {
  485. if err := ensureMailboxSelected(c, mboxName); err != nil {
  486. return err
  487. }
  488. seqSet := new(imap.SeqSet)
  489. seqSet.AddNum(uid)
  490. item := imap.FormatFlagsOp(imap.AddFlags, true)
  491. flags := []interface{}{imap.DeletedFlag}
  492. if err := c.UidStore(seqSet, item, flags, nil); err != nil {
  493. return err
  494. }
  495. return c.Expunge(nil)
  496. }