eostre.go 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301
  1. package eostre
  2. // todo make gott
  3. import (
  4. "bytes"
  5. "errors"
  6. "fmt"
  7. "io"
  8. "log"
  9. "os"
  10. "strings"
  11. "apiote.xyz/p/asgard/idavollr"
  12. "apiote.xyz/p/asgard/jotunheim"
  13. "apiote.xyz/p/gott/v2"
  14. "github.com/ProtonMail/gopenpgp/v2/helper"
  15. "github.com/bytesparadise/libasciidoc"
  16. "github.com/bytesparadise/libasciidoc/pkg/configuration"
  17. "github.com/emersion/go-imap"
  18. "github.com/emersion/go-message"
  19. _ "github.com/emersion/go-message/charset"
  20. )
  21. type UnauthorisedSenderError struct {
  22. sender string
  23. }
  24. func (e UnauthorisedSenderError) Error() string {
  25. return "message from unauthorised sender " + e.sender
  26. }
  27. type EostreMailbox struct {
  28. idavollr.Mailbox
  29. delSeqset *imap.SeqSet
  30. doneMessages int
  31. }
  32. type EostreImapMessage struct {
  33. idavollr.ImapMessage
  34. delSeqset *imap.SeqSet
  35. config jotunheim.Config
  36. mime string
  37. mimeParams map[string]string
  38. part *message.Entity
  39. subject string
  40. partBody []byte
  41. filename string
  42. asciidoc string
  43. writer *bytes.Buffer
  44. html string
  45. file *os.File
  46. }
  47. func Eostre(config jotunheim.Config) (int, error) {
  48. mailbox := &EostreMailbox{
  49. Mailbox: idavollr.Mailbox{
  50. MboxName: "INBOX",
  51. ImapAdr: config.Eostre.ImapAddress,
  52. ImapUser: config.Eostre.ImapUsername,
  53. ImapPass: config.Eostre.ImapPassword,
  54. Conf: config,
  55. },
  56. delSeqset: new(imap.SeqSet),
  57. }
  58. mailbox.SetupChannels()
  59. r := gott.R[idavollr.AbstractMailbox]{
  60. S: mailbox,
  61. }.
  62. Bind(idavollr.Connect).
  63. Tee(idavollr.Login).
  64. Bind(idavollr.SelectInbox).
  65. Tee(idavollr.CheckEmptyBox).
  66. Map(idavollr.FetchMessages).
  67. Bind(downloadEntries).
  68. Tee(deleteMessages).
  69. Tee(idavollr.Expunge)
  70. return r.S.(*EostreMailbox).doneMessages, r.E
  71. }
  72. func downloadEntries(m idavollr.AbstractMailbox) (idavollr.AbstractMailbox, error) {
  73. em := m.(*EostreMailbox)
  74. for msg := range em.Messages() {
  75. imapMessage := idavollr.ImapMessage{
  76. Msg: msg,
  77. Sect: m.Section(),
  78. Mimetype: "",
  79. }
  80. imapMessage.SetClient(m.Client())
  81. r := gott.R[idavollr.AbstractImapMessage]{
  82. S: &EostreImapMessage{
  83. ImapMessage: imapMessage,
  84. config: m.Config(),
  85. delSeqset: em.delSeqset,
  86. },
  87. }.
  88. Tee(checkSender).
  89. Bind(idavollr.ReadMessageBody).
  90. Bind(idavollr.ParseMimeMessage).
  91. Bind(getContentType).
  92. Map(getPlainPart).
  93. Bind(getEncryptedPart).
  94. Tee(checkSelectedPart).
  95. Map(getSubject).
  96. Bind(readPartBody).
  97. Bind(prepareAsciidoc).
  98. Bind(convertAsciidoc).
  99. Map(cleanHtml).
  100. Bind(openFile).
  101. Tee(writeFile).
  102. Recover(closeFile).
  103. SafeTee(markDeleteMessage).
  104. Recover(ignoreUnauthorisedSender).
  105. Recover(idavollr.RecoverMalformedMessage)
  106. if r.E != nil {
  107. log.Printf("message %s (%s) errored: %s\n", r.S.(*EostreImapMessage).subject, r.S.Message().Uid, r.E.Error())
  108. } else {
  109. em.doneMessages++
  110. }
  111. }
  112. return em, nil
  113. }
  114. func checkSender(m idavollr.AbstractImapMessage) error {
  115. em := m.(*EostreImapMessage)
  116. sender := m.Message().Envelope.From[0]
  117. if sender.Address() != em.config.Eostre.AuthorisedSender {
  118. return UnauthorisedSenderError{sender: sender.Address()}
  119. }
  120. return nil
  121. }
  122. func getContentType(m idavollr.AbstractImapMessage) (idavollr.AbstractImapMessage, error) {
  123. em := m.(*EostreImapMessage)
  124. t, params, err := m.MimeMessage().Header.ContentType()
  125. em.mime = t
  126. em.mimeParams = params
  127. return em, err
  128. }
  129. func getPlainPart(m idavollr.AbstractImapMessage) idavollr.AbstractImapMessage {
  130. em := m.(*EostreImapMessage)
  131. if em.mime == "text/plain" {
  132. em.part = m.MimeMessage()
  133. }
  134. return em
  135. }
  136. // TODO break up
  137. func getEncryptedPart(m idavollr.AbstractImapMessage) (idavollr.AbstractImapMessage, error) {
  138. em := m.(*EostreImapMessage)
  139. if em.mime == "multipart/encrypted" && em.mimeParams["protocol"] == "application/pgp-encrypted" {
  140. mr := m.MimeMessage().MultipartReader()
  141. for {
  142. p, err := mr.NextPart()
  143. if err == io.EOF {
  144. break
  145. } else if err != nil {
  146. return em, fmt.Errorf("while reading next part: %w", err)
  147. }
  148. t, _, err := p.Header.ContentType()
  149. if err != nil {
  150. return em, fmt.Errorf("while getting content type: %w", err)
  151. }
  152. if t == "application/octet-stream" {
  153. bodyReader := p.Body
  154. body, err := io.ReadAll(bodyReader)
  155. if err != nil {
  156. return em, fmt.Errorf("while reading body: %w", err)
  157. }
  158. decrypted, err := helper.DecryptVerifyMessageArmored(em.config.Eostre.PublicKey, em.config.Eostre.PrivateKey, []byte(em.config.Eostre.PrivateKeyPass), string(body))
  159. if err != nil {
  160. return em, fmt.Errorf("while decrypting body: %w", err)
  161. }
  162. em.part, err = message.Read(strings.NewReader(decrypted))
  163. return em, err
  164. }
  165. }
  166. }
  167. return em, nil
  168. }
  169. func checkSelectedPart(m idavollr.AbstractImapMessage) error {
  170. em := m.(*EostreImapMessage)
  171. if em.part == nil {
  172. return idavollr.MalformedMessageError{
  173. Cause: errors.New("text/plain or multipart/encrypted not found"),
  174. MessageID: m.Message().Envelope.MessageId,
  175. }
  176. }
  177. return nil
  178. }
  179. func getSubject(m idavollr.AbstractImapMessage) idavollr.AbstractImapMessage {
  180. em := m.(*EostreImapMessage)
  181. em.subject = em.Message().Envelope.Subject
  182. encryptedSubject := em.part.Header.Get("Subject")
  183. if encryptedSubject != "" {
  184. em.subject = encryptedSubject
  185. }
  186. return em
  187. }
  188. func readPartBody(m idavollr.AbstractImapMessage) (idavollr.AbstractImapMessage, error) {
  189. em := m.(*EostreImapMessage)
  190. body, err := io.ReadAll(em.part.Body)
  191. em.partBody = body
  192. return em, err
  193. }
  194. func prepareAsciidoc(m idavollr.AbstractImapMessage) (idavollr.AbstractImapMessage, error) {
  195. em := m.(*EostreImapMessage)
  196. em.asciidoc, _, _ = strings.Cut(string(em.partBody), "\n-- ")
  197. em.filename = em.Message().Envelope.Date.Format("20060102.html")
  198. _, err := os.Stat(em.filename)
  199. if err == nil {
  200. em.asciidoc = "=== " + em.Message().Envelope.Date.Format("03:04 -0700") + "\n\n_" + em.subject + "_\n\n" + em.asciidoc
  201. } else {
  202. if errors.Is(err, os.ErrNotExist) {
  203. em.asciidoc = "== " + em.Message().Envelope.Date.Format("Jan 2") + "\n\n_" + em.subject + "_\n\n" + em.asciidoc
  204. err = nil
  205. }
  206. }
  207. return em, err
  208. }
  209. func convertAsciidoc(m idavollr.AbstractImapMessage) (idavollr.AbstractImapMessage, error) {
  210. em := m.(*EostreImapMessage)
  211. reader := strings.NewReader(em.asciidoc)
  212. em.writer = bytes.NewBuffer([]byte{})
  213. config := configuration.NewConfiguration()
  214. _, err := libasciidoc.Convert(reader, em.writer, config)
  215. return em, err
  216. }
  217. func cleanHtml(m idavollr.AbstractImapMessage) idavollr.AbstractImapMessage {
  218. em := m.(*EostreImapMessage)
  219. em.html = string(em.writer.Bytes())
  220. em.html = strings.ReplaceAll(em.html, "<div class=\"sect2\">\n", "")
  221. em.html = strings.ReplaceAll(em.html, "<div class=\"sect1\">\n", "")
  222. em.html = strings.ReplaceAll(em.html, "<div class=\"sectionbody\">\n", "")
  223. em.html = strings.ReplaceAll(em.html, "<div class=\"paragraph\">\n", "")
  224. em.html = strings.ReplaceAll(em.html, "</div>\n", "")
  225. return em
  226. }
  227. func openFile(m idavollr.AbstractImapMessage) (idavollr.AbstractImapMessage, error) {
  228. em := m.(*EostreImapMessage)
  229. f, err := os.OpenFile(em.filename, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0600)
  230. em.file = f
  231. return em, err
  232. }
  233. func writeFile(m idavollr.AbstractImapMessage) error {
  234. em := m.(*EostreImapMessage)
  235. _, err := em.file.WriteString(em.html)
  236. return err
  237. }
  238. func closeFile(m idavollr.AbstractImapMessage, err error) (idavollr.AbstractImapMessage, error) {
  239. em := m.(*EostreImapMessage)
  240. if em.file != nil {
  241. em.file.Close()
  242. }
  243. return m, err
  244. }
  245. func markDeleteMessage(m idavollr.AbstractImapMessage) {
  246. em := m.(*EostreImapMessage)
  247. em.delSeqset.AddNum(em.Message().Uid)
  248. }
  249. func ignoreUnauthorisedSender(m idavollr.AbstractImapMessage, err error) (idavollr.AbstractImapMessage, error) {
  250. em := m.(*EostreImapMessage)
  251. var unauthorisedSenderError UnauthorisedSenderError
  252. if errors.As(err, &unauthorisedSenderError) {
  253. em.delSeqset.AddNum(em.Message().Uid)
  254. log.Printf("ignoring from %s as not authorised\n", unauthorisedSenderError.sender)
  255. return em, nil
  256. }
  257. return em, err
  258. }
  259. func deleteMessages(m idavollr.AbstractMailbox) error {
  260. em := m.(*EostreMailbox)
  261. if !em.delSeqset.Empty() {
  262. item := imap.FormatFlagsOp(imap.AddFlags, true)
  263. flags := []interface{}{imap.DeletedFlag}
  264. err := em.Client().UidStore(em.delSeqset, item, flags, nil)
  265. return err
  266. }
  267. return nil
  268. }