123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301 |
- package eostre
- // todo make gott
- import (
- "bytes"
- "errors"
- "fmt"
- "io"
- "log"
- "os"
- "strings"
- "apiote.xyz/p/asgard/idavollr"
- "apiote.xyz/p/asgard/jotunheim"
- "apiote.xyz/p/gott/v2"
- "github.com/ProtonMail/gopenpgp/v2/helper"
- "github.com/bytesparadise/libasciidoc"
- "github.com/bytesparadise/libasciidoc/pkg/configuration"
- "github.com/emersion/go-imap"
- "github.com/emersion/go-message"
- _ "github.com/emersion/go-message/charset"
- )
- type UnauthorisedSenderError struct {
- sender string
- }
- func (e UnauthorisedSenderError) Error() string {
- return "message from unauthorised sender " + e.sender
- }
- type EostreMailbox struct {
- idavollr.Mailbox
- delSeqset *imap.SeqSet
- doneMessages int
- }
- type EostreImapMessage struct {
- idavollr.ImapMessage
- delSeqset *imap.SeqSet
- config jotunheim.Config
- mime string
- mimeParams map[string]string
- part *message.Entity
- subject string
- partBody []byte
- filename string
- asciidoc string
- writer *bytes.Buffer
- html string
- file *os.File
- }
- func Eostre(config jotunheim.Config) (int, error) {
- mailbox := &EostreMailbox{
- Mailbox: idavollr.Mailbox{
- MboxName: "INBOX",
- ImapAdr: config.Eostre.ImapAddress,
- ImapUser: config.Eostre.ImapUsername,
- ImapPass: config.Eostre.ImapPassword,
- Conf: config,
- },
- delSeqset: new(imap.SeqSet),
- }
- mailbox.SetupChannels()
- r := gott.R[idavollr.AbstractMailbox]{
- S: mailbox,
- }.
- Bind(idavollr.Connect).
- Tee(idavollr.Login).
- Bind(idavollr.SelectInbox).
- Tee(idavollr.CheckEmptyBox).
- Map(idavollr.FetchMessages).
- Bind(downloadEntries).
- Tee(deleteMessages).
- Tee(idavollr.Expunge)
- return r.S.(*EostreMailbox).doneMessages, r.E
- }
- func downloadEntries(m idavollr.AbstractMailbox) (idavollr.AbstractMailbox, error) {
- em := m.(*EostreMailbox)
- for msg := range em.Messages() {
- imapMessage := idavollr.ImapMessage{
- Msg: msg,
- Sect: m.Section(),
- Mimetype: "",
- }
- imapMessage.SetClient(m.Client())
- r := gott.R[idavollr.AbstractImapMessage]{
- S: &EostreImapMessage{
- ImapMessage: imapMessage,
- config: m.Config(),
- delSeqset: em.delSeqset,
- },
- }.
- Tee(checkSender).
- Bind(idavollr.ReadMessageBody).
- Bind(idavollr.ParseMimeMessage).
- Bind(getContentType).
- Map(getPlainPart).
- Bind(getEncryptedPart).
- Tee(checkSelectedPart).
- Map(getSubject).
- Bind(readPartBody).
- Bind(prepareAsciidoc).
- Bind(convertAsciidoc).
- Map(cleanHtml).
- Bind(openFile).
- Tee(writeFile).
- Recover(closeFile).
- SafeTee(markDeleteMessage).
- Recover(ignoreUnauthorisedSender).
- Recover(idavollr.RecoverMalformedMessage)
- if r.E != nil {
- log.Printf("message %s (%s) errored: %s\n", r.S.(*EostreImapMessage).subject, r.S.Message().Uid, r.E.Error())
- } else {
- em.doneMessages++
- }
- }
- return em, nil
- }
- func checkSender(m idavollr.AbstractImapMessage) error {
- em := m.(*EostreImapMessage)
- sender := m.Message().Envelope.From[0]
- if sender.Address() != em.config.Eostre.AuthorisedSender {
- return UnauthorisedSenderError{sender: sender.Address()}
- }
- return nil
- }
- func getContentType(m idavollr.AbstractImapMessage) (idavollr.AbstractImapMessage, error) {
- em := m.(*EostreImapMessage)
- t, params, err := m.MimeMessage().Header.ContentType()
- em.mime = t
- em.mimeParams = params
- return em, err
- }
- func getPlainPart(m idavollr.AbstractImapMessage) idavollr.AbstractImapMessage {
- em := m.(*EostreImapMessage)
- if em.mime == "text/plain" {
- em.part = m.MimeMessage()
- }
- return em
- }
- // TODO break up
- func getEncryptedPart(m idavollr.AbstractImapMessage) (idavollr.AbstractImapMessage, error) {
- em := m.(*EostreImapMessage)
- if em.mime == "multipart/encrypted" && em.mimeParams["protocol"] == "application/pgp-encrypted" {
- mr := m.MimeMessage().MultipartReader()
- for {
- p, err := mr.NextPart()
- if err == io.EOF {
- break
- } else if err != nil {
- return em, fmt.Errorf("while reading next part: %w", err)
- }
- t, _, err := p.Header.ContentType()
- if err != nil {
- return em, fmt.Errorf("while getting content type: %w", err)
- }
- if t == "application/octet-stream" {
- bodyReader := p.Body
- body, err := io.ReadAll(bodyReader)
- if err != nil {
- return em, fmt.Errorf("while reading body: %w", err)
- }
- decrypted, err := helper.DecryptVerifyMessageArmored(em.config.Eostre.PublicKey, em.config.Eostre.PrivateKey, []byte(em.config.Eostre.PrivateKeyPass), string(body))
- if err != nil {
- return em, fmt.Errorf("while decrypting body: %w", err)
- }
- em.part, err = message.Read(strings.NewReader(decrypted))
- return em, err
- }
- }
- }
- return em, nil
- }
- func checkSelectedPart(m idavollr.AbstractImapMessage) error {
- em := m.(*EostreImapMessage)
- if em.part == nil {
- return idavollr.MalformedMessageError{
- Cause: errors.New("text/plain or multipart/encrypted not found"),
- MessageID: m.Message().Envelope.MessageId,
- }
- }
- return nil
- }
- func getSubject(m idavollr.AbstractImapMessage) idavollr.AbstractImapMessage {
- em := m.(*EostreImapMessage)
- em.subject = em.Message().Envelope.Subject
- encryptedSubject := em.part.Header.Get("Subject")
- if encryptedSubject != "" {
- em.subject = encryptedSubject
- }
- return em
- }
- func readPartBody(m idavollr.AbstractImapMessage) (idavollr.AbstractImapMessage, error) {
- em := m.(*EostreImapMessage)
- body, err := io.ReadAll(em.part.Body)
- em.partBody = body
- return em, err
- }
- func prepareAsciidoc(m idavollr.AbstractImapMessage) (idavollr.AbstractImapMessage, error) {
- em := m.(*EostreImapMessage)
- em.asciidoc, _, _ = strings.Cut(string(em.partBody), "\n-- ")
- em.filename = em.Message().Envelope.Date.Format("20060102.html")
- _, err := os.Stat(em.filename)
- if err == nil {
- em.asciidoc = "=== " + em.Message().Envelope.Date.Format("03:04 -0700") + "\n\n_" + em.subject + "_\n\n" + em.asciidoc
- } else {
- if errors.Is(err, os.ErrNotExist) {
- em.asciidoc = "== " + em.Message().Envelope.Date.Format("Jan 2") + "\n\n_" + em.subject + "_\n\n" + em.asciidoc
- err = nil
- }
- }
- return em, err
- }
- func convertAsciidoc(m idavollr.AbstractImapMessage) (idavollr.AbstractImapMessage, error) {
- em := m.(*EostreImapMessage)
- reader := strings.NewReader(em.asciidoc)
- em.writer = bytes.NewBuffer([]byte{})
- config := configuration.NewConfiguration()
- _, err := libasciidoc.Convert(reader, em.writer, config)
- return em, err
- }
- func cleanHtml(m idavollr.AbstractImapMessage) idavollr.AbstractImapMessage {
- em := m.(*EostreImapMessage)
- em.html = string(em.writer.Bytes())
- em.html = strings.ReplaceAll(em.html, "<div class=\"sect2\">\n", "")
- em.html = strings.ReplaceAll(em.html, "<div class=\"sect1\">\n", "")
- em.html = strings.ReplaceAll(em.html, "<div class=\"sectionbody\">\n", "")
- em.html = strings.ReplaceAll(em.html, "<div class=\"paragraph\">\n", "")
- em.html = strings.ReplaceAll(em.html, "</div>\n", "")
- return em
- }
- func openFile(m idavollr.AbstractImapMessage) (idavollr.AbstractImapMessage, error) {
- em := m.(*EostreImapMessage)
- f, err := os.OpenFile(em.filename, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0600)
- em.file = f
- return em, err
- }
- func writeFile(m idavollr.AbstractImapMessage) error {
- em := m.(*EostreImapMessage)
- _, err := em.file.WriteString(em.html)
- return err
- }
- func closeFile(m idavollr.AbstractImapMessage, err error) (idavollr.AbstractImapMessage, error) {
- em := m.(*EostreImapMessage)
- if em.file != nil {
- em.file.Close()
- }
- return m, err
- }
- func markDeleteMessage(m idavollr.AbstractImapMessage) {
- em := m.(*EostreImapMessage)
- em.delSeqset.AddNum(em.Message().Uid)
- }
- func ignoreUnauthorisedSender(m idavollr.AbstractImapMessage, err error) (idavollr.AbstractImapMessage, error) {
- em := m.(*EostreImapMessage)
- var unauthorisedSenderError UnauthorisedSenderError
- if errors.As(err, &unauthorisedSenderError) {
- em.delSeqset.AddNum(em.Message().Uid)
- log.Printf("ignoring from %s as not authorised\n", unauthorisedSenderError.sender)
- return em, nil
- }
- return em, err
- }
- func deleteMessages(m idavollr.AbstractMailbox) error {
- em := m.(*EostreMailbox)
- if !em.delSeqset.Empty() {
- item := imap.FormatFlagsOp(imap.AddFlags, true)
- flags := []interface{}{imap.DeletedFlag}
- err := em.Client().UidStore(em.delSeqset, item, flags, nil)
- return err
- }
- return nil
- }
|