A webmail client. Forked from https://git.sr.ht/~migadu/alps
Nelze vybrat více než 25 témat Téma musí začínat písmenem nebo číslem, může obsahovat pomlčky („-“) a může být dlouhé až 35 znaků.
 
 
 
 

163 řádky
3.9 KiB

  1. package alpsbase
  2. import (
  3. "bufio"
  4. "bytes"
  5. "net/textproto"
  6. "strings"
  7. "github.com/emersion/go-imap"
  8. )
  9. func searchCriteriaHeader(k, v string) *imap.SearchCriteria {
  10. return &imap.SearchCriteria{
  11. Header: map[string][]string{
  12. k: []string{v},
  13. },
  14. }
  15. }
  16. func searchCriteriaOr(criteria ...*imap.SearchCriteria) *imap.SearchCriteria {
  17. if criteria[0] == nil {
  18. criteria = criteria[1:]
  19. }
  20. or := criteria[0]
  21. for _, c := range criteria[1:] {
  22. or = &imap.SearchCriteria{
  23. Or: [][2]*imap.SearchCriteria{{or, c}},
  24. }
  25. }
  26. return or
  27. }
  28. func searchCriteriaAnd(criteria ...*imap.SearchCriteria) *imap.SearchCriteria {
  29. if criteria[0] == nil {
  30. criteria = criteria[1:]
  31. }
  32. and := criteria[0]
  33. for _, c := range criteria[1:] {
  34. // TODO: Maybe pitch the AND and OR functions to go-imap upstream
  35. if c.Header != nil {
  36. if and.Header == nil {
  37. and.Header = make(textproto.MIMEHeader)
  38. }
  39. for key, value := range c.Header {
  40. if _, ok := and.Header[key]; !ok {
  41. and.Header[key] = nil
  42. }
  43. and.Header[key] = append(and.Header[key], value...)
  44. }
  45. }
  46. and.Body = append(and.Body, c.Body...)
  47. and.Text = append(and.Text, c.Text...)
  48. and.WithFlags = append(and.WithFlags, c.WithFlags...)
  49. and.WithoutFlags = append(and.WithoutFlags, c.WithoutFlags...)
  50. // TODO: Merge more things
  51. }
  52. return and
  53. }
  54. // Splits search up into the longest string of non-functional parts and
  55. // functional parts
  56. //
  57. // Input: hello world foo:bar baz trains:"are cool"
  58. // Output: ["hello world", "foo:bar", "baz", "trains:are cool"]
  59. func splitSearchTokens(buf []byte, eof bool) (int, []byte, error) {
  60. if len(buf) == 0 {
  61. return 0, nil, nil
  62. }
  63. if buf[0] == ' ' {
  64. return 1, nil, nil
  65. }
  66. colon := bytes.IndexByte(buf, byte(':'))
  67. if colon == -1 && eof {
  68. return len(buf), buf, nil
  69. } else if colon == -1 {
  70. return 0, nil, nil
  71. } else {
  72. space := bytes.LastIndexByte(buf[:colon], byte(' '))
  73. if space != -1 {
  74. return space, buf[:space], nil
  75. }
  76. var (
  77. terminator int
  78. quoted bool
  79. )
  80. if colon + 1 < len(buf) && buf[colon+1] == byte('"') {
  81. terminator = bytes.IndexByte(buf[colon+2:], byte('"'))
  82. terminator += colon + 3
  83. quoted = true
  84. } else {
  85. terminator = bytes.IndexByte(buf[colon:], byte(' '))
  86. terminator += colon
  87. }
  88. if terminator == -1 {
  89. return 0, nil, nil
  90. } else if terminator == -1 && eof {
  91. terminator = len(buf)
  92. }
  93. if quoted {
  94. trimmed := append(buf[:colon+1], buf[colon+2:terminator-1]...)
  95. return terminator, trimmed, nil
  96. }
  97. return terminator, buf[:terminator], nil
  98. }
  99. }
  100. // TODO: Document search functionality somewhere
  101. func PrepareSearch(terms string) *imap.SearchCriteria {
  102. // XXX: If Migadu's IMAP servers can learn a better Full-Text Search then
  103. // we can probably start matching on the message bodies by default (gated
  104. // behind some kind of flag, perhaps)
  105. var criteria *imap.SearchCriteria
  106. scanner := bufio.NewScanner(strings.NewReader(terms))
  107. scanner.Split(splitSearchTokens)
  108. for scanner.Scan() {
  109. term := scanner.Text()
  110. if !strings.ContainsRune(term, ':') {
  111. criteria = searchCriteriaAnd(
  112. criteria,
  113. searchCriteriaOr(
  114. searchCriteriaHeader("From", term),
  115. searchCriteriaHeader("To", term),
  116. searchCriteriaHeader("Cc", term),
  117. searchCriteriaHeader("Subject", term),
  118. ),
  119. )
  120. } else {
  121. parts := strings.SplitN(term, ":", 2)
  122. key, value := parts[0], parts[1]
  123. switch strings.ToLower(key) {
  124. case "from":
  125. criteria = searchCriteriaAnd(
  126. criteria, searchCriteriaHeader("From", value))
  127. case "to":
  128. criteria = searchCriteriaAnd(
  129. criteria, searchCriteriaHeader("To", value))
  130. case "cc":
  131. criteria = searchCriteriaAnd(
  132. criteria, searchCriteriaHeader("Cc", value))
  133. case "subject":
  134. criteria = searchCriteriaAnd(
  135. criteria, searchCriteriaHeader("Subject", value))
  136. case "body":
  137. criteria = searchCriteriaAnd(
  138. criteria, &imap.SearchCriteria{Body: []string{value}})
  139. default:
  140. continue
  141. }
  142. }
  143. }
  144. return criteria
  145. }