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.
 
 
 
 

119 lines
2.4 KiB

  1. package alpsviewtext
  2. import (
  3. "bufio"
  4. "fmt"
  5. "html/template"
  6. "net/url"
  7. "strings"
  8. "git.sr.ht/~migadu/alps"
  9. alpsbase "git.sr.ht/~migadu/alps/plugins/base"
  10. "github.com/emersion/go-message"
  11. "gitlab.com/golang-commonmark/linkify"
  12. )
  13. // TODO: dim quotes and "On xxx, xxx wrote:" lines
  14. const (
  15. tplStr = `<pre>{{range .}}{{.}}{{end}}</pre>`
  16. linkTplStr = `<a href="{{.Href}}" target="_blank" rel="nofollow noopener">{{.Text}}</a>`
  17. )
  18. var tpl *template.Template
  19. func init() {
  20. tpl = template.Must(template.New("view-text.html").Parse(tplStr))
  21. template.Must(tpl.New("view-text-link.html").Parse(linkTplStr))
  22. }
  23. type linkRenderData struct {
  24. Href string
  25. Text string
  26. }
  27. var allowedSchemes = map[string]bool{
  28. "http": true,
  29. "https": true,
  30. "mailto": true,
  31. "ftp": true,
  32. "sftp": true,
  33. "ftps": true,
  34. "tel": true,
  35. }
  36. func executeTemplate(name string, data interface{}) (template.HTML, error) {
  37. var sb strings.Builder
  38. err := tpl.ExecuteTemplate(&sb, name, data)
  39. if err != nil {
  40. return "", err
  41. }
  42. return template.HTML(sb.String()), nil
  43. }
  44. type viewer struct{}
  45. func (viewer) ViewMessagePart(ctx *alps.Context, msg *alpsbase.IMAPMessage, part *message.Entity) (interface{}, error) {
  46. mimeType, _, err := part.Header.ContentType()
  47. if err != nil {
  48. return nil, err
  49. }
  50. if !strings.EqualFold(mimeType, "text/plain") {
  51. return nil, alpsbase.ErrViewUnsupported
  52. }
  53. var tokens []interface{}
  54. scanner := bufio.NewScanner(part.Body)
  55. for scanner.Scan() {
  56. l := scanner.Text()
  57. i := 0
  58. for _, link := range linkify.Links(l) {
  59. href := l[link.Start:link.End]
  60. if link.Scheme == "" {
  61. href = "https://" + href
  62. } else if !strings.HasPrefix(href, link.Scheme) {
  63. href = link.Scheme + href
  64. }
  65. u, err := url.Parse(href)
  66. if err != nil {
  67. continue
  68. }
  69. if !allowedSchemes[u.Scheme] {
  70. continue
  71. }
  72. // TODO: redirect mailto links to the composer
  73. if i < link.Start {
  74. tokens = append(tokens, l[i:link.Start])
  75. }
  76. tok, err := executeTemplate("view-text-link.html", linkRenderData{
  77. Href: href,
  78. Text: l[link.Start:link.End],
  79. })
  80. if err != nil {
  81. return nil, err
  82. }
  83. tokens = append(tokens, tok)
  84. i = link.End
  85. }
  86. if i < len(l) {
  87. tokens = append(tokens, l[i:])
  88. }
  89. tokens = append(tokens, "\n")
  90. }
  91. if err := scanner.Err(); err != nil {
  92. return nil, fmt.Errorf("failed to read part body: %v", err)
  93. }
  94. return executeTemplate("view-text.html", tokens)
  95. }
  96. func init() {
  97. alpsbase.RegisterViewer(viewer{})
  98. }