PageRenderTime 599ms CodeModel.GetById 40ms RepoModel.GetById 5ms app.codeStats 0ms

/widgets/msgviewer.go

https://git.sr.ht/~sircmpwn/aerc/
Go | 712 lines | 634 code | 67 blank | 11 comment | 152 complexity | 2b33e6c66ba5d5f6945af5a7c303583b MD5 | raw file
  1. package widgets
  2. import (
  3. "bufio"
  4. "errors"
  5. "fmt"
  6. "io"
  7. "os/exec"
  8. "regexp"
  9. "strings"
  10. "github.com/danwakefield/fnmatch"
  11. "github.com/gdamore/tcell"
  12. "github.com/google/shlex"
  13. "github.com/mattn/go-runewidth"
  14. "git.sr.ht/~sircmpwn/aerc/config"
  15. "git.sr.ht/~sircmpwn/aerc/lib"
  16. "git.sr.ht/~sircmpwn/aerc/lib/ui"
  17. "git.sr.ht/~sircmpwn/aerc/models"
  18. )
  19. var ansi = regexp.MustCompile("\x1B\\[[0-?]*[ -/]*[@-~]")
  20. var _ ProvidesMessages = (*MessageViewer)(nil)
  21. type MessageViewer struct {
  22. ui.Invalidatable
  23. acct *AccountView
  24. conf *config.AercConfig
  25. err error
  26. grid *ui.Grid
  27. switcher *PartSwitcher
  28. msg lib.MessageView
  29. }
  30. type PartSwitcher struct {
  31. ui.Invalidatable
  32. parts []*PartViewer
  33. selected int
  34. showHeaders bool
  35. alwaysShowMime bool
  36. height int
  37. mv *MessageViewer
  38. }
  39. func NewMessageViewer(acct *AccountView,
  40. conf *config.AercConfig, msg lib.MessageView) *MessageViewer {
  41. hf := HeaderLayoutFilter{
  42. layout: HeaderLayout(conf.Viewer.HeaderLayout),
  43. keep: func(msg *models.MessageInfo, header string) bool {
  44. if fmtHeader(msg, header, "2") != "" {
  45. return true
  46. }
  47. return false
  48. },
  49. }
  50. layout := hf.forMessage(msg.MessageInfo())
  51. header, headerHeight := layout.grid(
  52. func(header string) ui.Drawable {
  53. return &HeaderView{
  54. Name: header,
  55. Value: fmtHeader(msg.MessageInfo(), header,
  56. acct.UiConfig().TimestampFormat),
  57. }
  58. },
  59. )
  60. rows := []ui.GridSpec{
  61. {ui.SIZE_EXACT, headerHeight},
  62. }
  63. if msg.PGPDetails() != nil {
  64. height := 1
  65. if msg.PGPDetails().IsSigned && msg.PGPDetails().IsEncrypted {
  66. height = 2
  67. }
  68. rows = append(rows, ui.GridSpec{ui.SIZE_EXACT, height})
  69. }
  70. rows = append(rows, []ui.GridSpec{
  71. {ui.SIZE_EXACT, 1},
  72. {ui.SIZE_WEIGHT, 1},
  73. }...)
  74. grid := ui.NewGrid().Rows(rows).Columns([]ui.GridSpec{
  75. {ui.SIZE_WEIGHT, 1},
  76. })
  77. switcher := &PartSwitcher{}
  78. err := createSwitcher(acct, switcher, conf, msg)
  79. if err != nil {
  80. return &MessageViewer{
  81. err: err,
  82. grid: grid,
  83. msg: msg,
  84. }
  85. }
  86. grid.AddChild(header).At(0, 0)
  87. if msg.PGPDetails() != nil {
  88. grid.AddChild(NewPGPInfo(msg.PGPDetails())).At(1, 0)
  89. grid.AddChild(ui.NewFill(' ')).At(2, 0)
  90. grid.AddChild(switcher).At(3, 0)
  91. } else {
  92. grid.AddChild(ui.NewFill(' ')).At(1, 0)
  93. grid.AddChild(switcher).At(2, 0)
  94. }
  95. mv := &MessageViewer{
  96. acct: acct,
  97. conf: conf,
  98. grid: grid,
  99. msg: msg,
  100. switcher: switcher,
  101. }
  102. switcher.mv = mv
  103. return mv
  104. }
  105. func fmtHeader(msg *models.MessageInfo, header string, timefmt string) string {
  106. switch header {
  107. case "From":
  108. return models.FormatAddresses(msg.Envelope.From)
  109. case "To":
  110. return models.FormatAddresses(msg.Envelope.To)
  111. case "Cc":
  112. return models.FormatAddresses(msg.Envelope.Cc)
  113. case "Bcc":
  114. return models.FormatAddresses(msg.Envelope.Bcc)
  115. case "Date":
  116. return msg.Envelope.Date.Local().Format(timefmt)
  117. case "Subject":
  118. return msg.Envelope.Subject
  119. case "Labels":
  120. return strings.Join(msg.Labels, ", ")
  121. default:
  122. return msg.RFC822Headers.Get(header)
  123. }
  124. }
  125. func enumerateParts(acct *AccountView, conf *config.AercConfig,
  126. msg lib.MessageView, body *models.BodyStructure,
  127. index []int) ([]*PartViewer, error) {
  128. var parts []*PartViewer
  129. for i, part := range body.Parts {
  130. curindex := append(index, i+1)
  131. if part.MIMEType == "multipart" {
  132. // Multipart meta-parts are faked
  133. pv := &PartViewer{part: part}
  134. parts = append(parts, pv)
  135. subParts, err := enumerateParts(
  136. acct, conf, msg, part, curindex)
  137. if err != nil {
  138. return nil, err
  139. }
  140. parts = append(parts, subParts...)
  141. continue
  142. }
  143. pv, err := NewPartViewer(acct, conf, msg, part, curindex)
  144. if err != nil {
  145. return nil, err
  146. }
  147. parts = append(parts, pv)
  148. }
  149. return parts, nil
  150. }
  151. func createSwitcher(acct *AccountView, switcher *PartSwitcher,
  152. conf *config.AercConfig, msg lib.MessageView) error {
  153. var err error
  154. switcher.selected = -1
  155. switcher.showHeaders = conf.Viewer.ShowHeaders
  156. switcher.alwaysShowMime = conf.Viewer.AlwaysShowMime
  157. if len(msg.BodyStructure().Parts) == 0 {
  158. switcher.selected = 0
  159. pv, err := NewPartViewer(acct, conf, msg, msg.BodyStructure(), []int{1})
  160. if err != nil {
  161. return err
  162. }
  163. switcher.parts = []*PartViewer{pv}
  164. pv.OnInvalidate(func(_ ui.Drawable) {
  165. switcher.Invalidate()
  166. })
  167. } else {
  168. switcher.parts, err = enumerateParts(acct, conf, msg,
  169. msg.BodyStructure(), []int{})
  170. if err != nil {
  171. return err
  172. }
  173. selectedPriority := -1
  174. fmt.Printf("Selecting best message from %v\n", conf.Viewer.Alternatives)
  175. for i, pv := range switcher.parts {
  176. pv.OnInvalidate(func(_ ui.Drawable) {
  177. switcher.Invalidate()
  178. })
  179. // Switch to user's preferred mimetype
  180. if switcher.selected == -1 && pv.part.MIMEType != "multipart" {
  181. switcher.selected = i
  182. }
  183. mime := strings.ToLower(pv.part.MIMEType) +
  184. "/" + strings.ToLower(pv.part.MIMESubType)
  185. for idx, m := range conf.Viewer.Alternatives {
  186. if m != mime {
  187. continue
  188. }
  189. priority := len(conf.Viewer.Alternatives) - idx
  190. if priority > selectedPriority {
  191. selectedPriority = priority
  192. switcher.selected = i
  193. }
  194. }
  195. }
  196. }
  197. return nil
  198. }
  199. func (mv *MessageViewer) Draw(ctx *ui.Context) {
  200. if mv.err != nil {
  201. ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ', tcell.StyleDefault)
  202. ctx.Printf(0, 0, tcell.StyleDefault, "%s", mv.err.Error())
  203. return
  204. }
  205. mv.grid.Draw(ctx)
  206. }
  207. func (mv *MessageViewer) MouseEvent(localX int, localY int, event tcell.Event) {
  208. if mv.err != nil {
  209. return
  210. }
  211. mv.grid.MouseEvent(localX, localY, event)
  212. }
  213. func (mv *MessageViewer) Invalidate() {
  214. mv.grid.Invalidate()
  215. }
  216. func (mv *MessageViewer) OnInvalidate(fn func(d ui.Drawable)) {
  217. mv.grid.OnInvalidate(func(_ ui.Drawable) {
  218. fn(mv)
  219. })
  220. }
  221. func (mv *MessageViewer) Store() *lib.MessageStore {
  222. return mv.msg.Store()
  223. }
  224. func (mv *MessageViewer) SelectedAccount() *AccountView {
  225. return mv.acct
  226. }
  227. func (mv *MessageViewer) SelectedMessage() (*models.MessageInfo, error) {
  228. if mv.msg == nil {
  229. return nil, errors.New("no message selected")
  230. }
  231. return mv.msg.MessageInfo(), nil
  232. }
  233. func (mv *MessageViewer) MarkedMessages() ([]*models.MessageInfo, error) {
  234. store := mv.Store()
  235. return msgInfoFromUids(store, store.Marked())
  236. }
  237. func (mv *MessageViewer) ToggleHeaders() {
  238. switcher := mv.switcher
  239. mv.conf.Viewer.ShowHeaders = !mv.conf.Viewer.ShowHeaders
  240. err := createSwitcher(mv.acct, switcher, mv.conf, mv.msg)
  241. if err != nil {
  242. mv.acct.Logger().Printf(
  243. "warning: error during create switcher - %v", err)
  244. }
  245. switcher.Invalidate()
  246. }
  247. func (mv *MessageViewer) SelectedMessagePart() *PartInfo {
  248. switcher := mv.switcher
  249. part := switcher.parts[switcher.selected]
  250. return &PartInfo{
  251. Index: part.index,
  252. Msg: part.msg.MessageInfo(),
  253. Part: part.part,
  254. }
  255. }
  256. func (mv *MessageViewer) PreviousPart() {
  257. switcher := mv.switcher
  258. for {
  259. switcher.selected--
  260. if switcher.selected < 0 {
  261. switcher.selected = len(switcher.parts) - 1
  262. }
  263. if switcher.parts[switcher.selected].part.MIMEType != "multipart" {
  264. break
  265. }
  266. }
  267. mv.Invalidate()
  268. }
  269. func (mv *MessageViewer) NextPart() {
  270. switcher := mv.switcher
  271. for {
  272. switcher.selected++
  273. if switcher.selected >= len(switcher.parts) {
  274. switcher.selected = 0
  275. }
  276. if switcher.parts[switcher.selected].part.MIMEType != "multipart" {
  277. break
  278. }
  279. }
  280. mv.Invalidate()
  281. }
  282. func (mv *MessageViewer) Close() error {
  283. mv.switcher.Cleanup()
  284. return nil
  285. }
  286. func (ps *PartSwitcher) Invalidate() {
  287. ps.DoInvalidate(ps)
  288. }
  289. func (ps *PartSwitcher) Focus(focus bool) {
  290. if ps.parts[ps.selected].term != nil {
  291. ps.parts[ps.selected].term.Focus(focus)
  292. }
  293. }
  294. func (ps *PartSwitcher) Event(event tcell.Event) bool {
  295. return ps.parts[ps.selected].Event(event)
  296. }
  297. func (ps *PartSwitcher) Draw(ctx *ui.Context) {
  298. height := len(ps.parts)
  299. if height == 1 && !ps.alwaysShowMime {
  300. ps.parts[ps.selected].Draw(ctx)
  301. return
  302. }
  303. // TODO: cap height and add scrolling for messages with many parts
  304. ps.height = ctx.Height()
  305. y := ctx.Height() - height
  306. for i, part := range ps.parts {
  307. style := tcell.StyleDefault.Reverse(ps.selected == i)
  308. ctx.Fill(0, y+i, ctx.Width(), 1, ' ', style)
  309. name := fmt.Sprintf("%s/%s",
  310. strings.ToLower(part.part.MIMEType),
  311. strings.ToLower(part.part.MIMESubType))
  312. if filename, ok := part.part.DispositionParams["filename"]; ok {
  313. name += fmt.Sprintf(" (%s)", filename)
  314. } else if filename, ok := part.part.Params["name"]; ok {
  315. // workaround golang not supporting RFC2231 besides ASCII and UTF8
  316. name += fmt.Sprintf(" (%s)", filename)
  317. }
  318. ctx.Printf(len(part.index)*2, y+i, style, "%s", name)
  319. }
  320. ps.parts[ps.selected].Draw(ctx.Subcontext(
  321. 0, 0, ctx.Width(), ctx.Height()-height))
  322. }
  323. func (ps *PartSwitcher) MouseEvent(localX int, localY int, event tcell.Event) {
  324. switch event := event.(type) {
  325. case *tcell.EventMouse:
  326. switch event.Buttons() {
  327. case tcell.Button1:
  328. height := len(ps.parts)
  329. y := ps.height - height
  330. if localY < y && ps.parts[ps.selected].term != nil {
  331. ps.parts[ps.selected].term.MouseEvent(localX, localY, event)
  332. }
  333. for i, _ := range ps.parts {
  334. if localY != y+i {
  335. continue
  336. }
  337. if ps.parts[i].part.MIMEType == "multipart" {
  338. continue
  339. }
  340. if ps.parts[ps.selected].term != nil {
  341. ps.parts[ps.selected].term.Focus(false)
  342. }
  343. ps.selected = i
  344. ps.Invalidate()
  345. if ps.parts[ps.selected].term != nil {
  346. ps.parts[ps.selected].term.Focus(true)
  347. }
  348. }
  349. case tcell.WheelDown:
  350. height := len(ps.parts)
  351. y := ps.height - height
  352. if localY < y && ps.parts[ps.selected].term != nil {
  353. ps.parts[ps.selected].term.MouseEvent(localX, localY, event)
  354. }
  355. if ps.parts[ps.selected].term != nil {
  356. ps.parts[ps.selected].term.Focus(false)
  357. }
  358. ps.mv.NextPart()
  359. if ps.parts[ps.selected].term != nil {
  360. ps.parts[ps.selected].term.Focus(true)
  361. }
  362. case tcell.WheelUp:
  363. height := len(ps.parts)
  364. y := ps.height - height
  365. if localY < y && ps.parts[ps.selected].term != nil {
  366. ps.parts[ps.selected].term.MouseEvent(localX, localY, event)
  367. }
  368. if ps.parts[ps.selected].term != nil {
  369. ps.parts[ps.selected].term.Focus(false)
  370. }
  371. ps.mv.PreviousPart()
  372. if ps.parts[ps.selected].term != nil {
  373. ps.parts[ps.selected].term.Focus(true)
  374. }
  375. }
  376. }
  377. }
  378. func (ps *PartSwitcher) Cleanup() {
  379. for _, partViewer := range ps.parts {
  380. partViewer.Cleanup()
  381. }
  382. }
  383. func (mv *MessageViewer) Event(event tcell.Event) bool {
  384. return mv.switcher.Event(event)
  385. }
  386. func (mv *MessageViewer) Focus(focus bool) {
  387. mv.switcher.Focus(focus)
  388. }
  389. type PartViewer struct {
  390. ui.Invalidatable
  391. err error
  392. fetched bool
  393. filter *exec.Cmd
  394. index []int
  395. msg lib.MessageView
  396. pager *exec.Cmd
  397. pagerin io.WriteCloser
  398. part *models.BodyStructure
  399. showHeaders bool
  400. sink io.WriteCloser
  401. source io.Reader
  402. term *Terminal
  403. selecter *Selecter
  404. grid *ui.Grid
  405. }
  406. func NewPartViewer(acct *AccountView, conf *config.AercConfig,
  407. msg lib.MessageView, part *models.BodyStructure,
  408. index []int) (*PartViewer, error) {
  409. var (
  410. filter *exec.Cmd
  411. pager *exec.Cmd
  412. pipe io.WriteCloser
  413. pagerin io.WriteCloser
  414. term *Terminal
  415. )
  416. cmd, err := shlex.Split(conf.Viewer.Pager)
  417. if err != nil {
  418. return nil, err
  419. }
  420. pager = exec.Command(cmd[0], cmd[1:]...)
  421. info := msg.MessageInfo()
  422. for _, f := range conf.Filters {
  423. mime := strings.ToLower(part.MIMEType) +
  424. "/" + strings.ToLower(part.MIMESubType)
  425. switch f.FilterType {
  426. case config.FILTER_MIMETYPE:
  427. if fnmatch.Match(f.Filter, mime, 0) {
  428. filter = exec.Command("sh", "-c", f.Command)
  429. }
  430. case config.FILTER_HEADER:
  431. var header string
  432. switch f.Header {
  433. case "subject":
  434. header = info.Envelope.Subject
  435. case "from":
  436. header = models.FormatAddresses(info.Envelope.From)
  437. case "to":
  438. header = models.FormatAddresses(info.Envelope.To)
  439. case "cc":
  440. header = models.FormatAddresses(info.Envelope.Cc)
  441. }
  442. if f.Regex.Match([]byte(header)) {
  443. filter = exec.Command("sh", "-c", f.Command)
  444. }
  445. }
  446. if filter != nil {
  447. break
  448. }
  449. }
  450. if filter != nil {
  451. if pipe, err = filter.StdinPipe(); err != nil {
  452. return nil, err
  453. }
  454. if pagerin, _ = pager.StdinPipe(); err != nil {
  455. return nil, err
  456. }
  457. if term, err = NewTerminal(pager); err != nil {
  458. return nil, err
  459. }
  460. }
  461. grid := ui.NewGrid().Rows([]ui.GridSpec{
  462. {ui.SIZE_EXACT, 3}, // Message
  463. {ui.SIZE_EXACT, 1}, // Selector
  464. {ui.SIZE_WEIGHT, 1},
  465. }).Columns([]ui.GridSpec{
  466. {ui.SIZE_WEIGHT, 1},
  467. })
  468. selecter := NewSelecter([]string{"Save message", "Pipe to command"}, 0).
  469. OnChoose(func(option string) {
  470. switch option {
  471. case "Save message":
  472. acct.aerc.BeginExCommand("save ")
  473. case "Pipe to command":
  474. acct.aerc.BeginExCommand("pipe ")
  475. }
  476. })
  477. grid.AddChild(selecter).At(2, 0)
  478. pv := &PartViewer{
  479. filter: filter,
  480. index: index,
  481. msg: msg,
  482. pager: pager,
  483. pagerin: pagerin,
  484. part: part,
  485. showHeaders: conf.Viewer.ShowHeaders,
  486. sink: pipe,
  487. term: term,
  488. selecter: selecter,
  489. grid: grid,
  490. }
  491. if term != nil {
  492. term.OnStart = func() {
  493. pv.attemptCopy()
  494. }
  495. term.OnInvalidate(func(_ ui.Drawable) {
  496. pv.Invalidate()
  497. })
  498. }
  499. return pv, nil
  500. }
  501. func (pv *PartViewer) SetSource(reader io.Reader) {
  502. pv.source = reader
  503. pv.attemptCopy()
  504. }
  505. func (pv *PartViewer) attemptCopy() {
  506. if pv.source != nil && pv.pager != nil && pv.pager.Process != nil {
  507. if pv.filter != nil {
  508. stdout, _ := pv.filter.StdoutPipe()
  509. stderr, _ := pv.filter.StderrPipe()
  510. pv.filter.Start()
  511. ch := make(chan interface{})
  512. go func() {
  513. _, err := io.Copy(pv.pagerin, stdout)
  514. if err != nil {
  515. pv.err = err
  516. pv.Invalidate()
  517. }
  518. stdout.Close()
  519. ch <- nil
  520. }()
  521. go func() {
  522. _, err := io.Copy(pv.pagerin, stderr)
  523. if err != nil {
  524. pv.err = err
  525. pv.Invalidate()
  526. }
  527. stderr.Close()
  528. ch <- nil
  529. }()
  530. go func() {
  531. <-ch
  532. <-ch
  533. pv.filter.Wait()
  534. pv.pagerin.Close()
  535. }()
  536. }
  537. go func() {
  538. info := pv.msg.MessageInfo()
  539. if pv.showHeaders && info.RFC822Headers != nil {
  540. // header need to bypass the filter, else we run into issues
  541. // with the filter messing with newlines etc.
  542. // hence all writes in this block go directly to the pager
  543. fields := info.RFC822Headers.Fields()
  544. for fields.Next() {
  545. var value string
  546. var err error
  547. if value, err = fields.Text(); err != nil {
  548. // better than nothing, use the non decoded version
  549. value = fields.Value()
  550. }
  551. field := fmt.Sprintf(
  552. "%s: %s\n", fields.Key(), value)
  553. pv.pagerin.Write([]byte(field))
  554. }
  555. // virtual header
  556. if len(info.Labels) != 0 {
  557. labels := fmtHeader(info, "Labels", "")
  558. pv.pagerin.Write([]byte(fmt.Sprintf("Labels: %s\n", labels)))
  559. }
  560. pv.pagerin.Write([]byte{'\n'})
  561. }
  562. if pv.part.MIMEType == "text" {
  563. scanner := bufio.NewScanner(pv.source)
  564. for scanner.Scan() {
  565. text := scanner.Text()
  566. text = ansi.ReplaceAllString(text, "")
  567. io.WriteString(pv.sink, text+"\n")
  568. }
  569. } else {
  570. io.Copy(pv.sink, pv.source)
  571. }
  572. pv.sink.Close()
  573. }()
  574. }
  575. }
  576. func (pv *PartViewer) Invalidate() {
  577. pv.DoInvalidate(pv)
  578. }
  579. func (pv *PartViewer) Draw(ctx *ui.Context) {
  580. if pv.filter == nil {
  581. // TODO: Let them download it directly or something
  582. ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ', tcell.StyleDefault)
  583. ctx.Printf(0, 0, tcell.StyleDefault.Foreground(tcell.ColorRed),
  584. "No filter configured for this mimetype ('%s/%s')",
  585. pv.part.MIMEType, pv.part.MIMESubType,
  586. )
  587. ctx.Printf(0, 2, tcell.StyleDefault,
  588. "You can still :save the message or :pipe it to an external command")
  589. pv.selecter.Focus(true)
  590. pv.grid.Draw(ctx)
  591. return
  592. }
  593. if !pv.fetched {
  594. pv.msg.FetchBodyPart(pv.msg.BodyStructure(),
  595. pv.index, pv.SetSource)
  596. pv.fetched = true
  597. }
  598. if pv.err != nil {
  599. ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ', tcell.StyleDefault)
  600. ctx.Printf(0, 0, tcell.StyleDefault, "%s", pv.err.Error())
  601. return
  602. }
  603. pv.term.Draw(ctx)
  604. }
  605. func (pv *PartViewer) Cleanup() {
  606. if pv.pager != nil && pv.pager.Process != nil {
  607. pv.pager.Process.Kill()
  608. pv.pager = nil
  609. }
  610. }
  611. func (pv *PartViewer) Event(event tcell.Event) bool {
  612. if pv.term != nil {
  613. return pv.term.Event(event)
  614. }
  615. return pv.selecter.Event(event)
  616. }
  617. type HeaderView struct {
  618. ui.Invalidatable
  619. Name string
  620. Value string
  621. }
  622. func (hv *HeaderView) Draw(ctx *ui.Context) {
  623. name := hv.Name
  624. size := runewidth.StringWidth(name)
  625. lim := ctx.Width() - size - 1
  626. value := runewidth.Truncate(" "+hv.Value, lim, "…")
  627. var (
  628. hstyle tcell.Style
  629. vstyle tcell.Style
  630. )
  631. // TODO: Make this more robust and less dumb
  632. if hv.Name == "PGP" {
  633. vstyle = tcell.StyleDefault.Foreground(tcell.ColorGreen)
  634. hstyle = tcell.StyleDefault.Bold(true)
  635. } else {
  636. vstyle = tcell.StyleDefault
  637. hstyle = tcell.StyleDefault.Bold(true)
  638. }
  639. ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ', vstyle)
  640. ctx.Printf(0, 0, hstyle, "%s", name)
  641. ctx.Printf(size, 0, vstyle, "%s", value)
  642. }
  643. func (hv *HeaderView) Invalidate() {
  644. hv.DoInvalidate(hv)
  645. }