asgard.git

commit 0dfd439d35910faff2cf6553978457bb23ec5ba8

Author: Adam Evyčędo <git@apiote.xyz>

change first ēostre to gott

 eostre/eostre.go | 382 +++++++++++++++++++++++++++++++++----------------


diff --git a/eostre/eostre.go b/eostre/eostre.go
index 8a922db590ed75f4cd884e29188a64d153593cad..fb87b149521d9467a17fe0429d3cd31f5bfede83 100644
--- a/eostre/eostre.go
+++ b/eostre/eostre.go
@@ -11,156 +11,290 @@ 	"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-imap/client"
 	"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) {
-	c, err := client.DialTLS(config.Eostre.ImapAddress, nil)
-	if err != nil {
-		log.Fatalln(err)
+	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++
+		}
 	}
-	log.Println("Connected")
-	defer c.Logout()
-	if err := c.Login(config.Eostre.ImapUsername, config.Eostre.ImapPassword); err != nil {
-		log.Fatalln(err)
+	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()}
 	}
-	log.Println("Logged in")
-	mbox, err := c.Select("INBOX", false)
-	if err != nil {
-		log.Fatalln(err)
+	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()
 	}
-	from := uint32(1)
-	to := mbox.Messages
-	seqset := new(imap.SeqSet)
-	seqset.AddRange(from, to)
-	messages := make(chan *imap.Message, 10)
-	done := make(chan error, 1)
-	section := &imap.BodySectionName{}
-	go func() {
-		done <- c.Fetch(seqset, []imap.FetchItem{imap.FetchEnvelope, section.FetchItem(), imap.FetchFlags, imap.FetchUid}, messages)
-	}()
-	delSeqset := new(imap.SeqSet)
-	doneMessages := 0
-	for msg := range messages {
-		log.Println("doing", msg.Uid)
-		subject := msg.Envelope.Subject
-		sender := msg.Envelope.From[0]
-		if sender.Address() != config.Eostre.AuthorisedSender {
-			// todo remove message
-			log.Printf("ignoring from %s as not authorised\n", sender)
-			continue
-		}
-		bodyReader := msg.GetBody(section)
-		if bodyReader == nil {
-			log.Printf("body for %d is nil\n", msg.Uid)
-		}
-		m, err := message.Read(bodyReader)
-		if err != nil {
-			log.Fatalln(err)
-		}
-		t, params, err := m.Header.ContentType()
-		if err != nil {
-			log.Fatalln(err)
-		}
-		var part *message.Entity
-		if t == "text/plain" {
-			part = m
-		} else if t == "multipart/encrypted" && params["protocol"] == "application/pgp-encrypted" {
-			mr := m.MultipartReader()
-			for {
-				p, err := mr.NextPart()
-				if err == io.EOF {
-					break
-				} else if err != nil {
-					return 0, fmt.Errorf("while reading next part: %w", err)
-				}
-				t, _, err := p.Header.ContentType()
+	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 {
-					log.Fatalln(err)
+					return em, fmt.Errorf("while reading body: %w", err)
 				}
-				if t == "application/octet-stream" {
-					bodyReader := p.Body
-					body, err := io.ReadAll(bodyReader)
-					if err != nil {
-						log.Fatalln(err)
-					}
-					decrypted, err := helper.DecryptVerifyMessageArmored(config.Eostre.PublicKey, config.Eostre.PrivateKey, []byte(config.Eostre.PrivateKeyPass), string(body))
-					if err != nil {
-						log.Fatalln(err)
-					}
-					part, err = message.Read(strings.NewReader(decrypted))
-					if err != nil {
-						log.Fatalln(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
 			}
-		} else {
-			log.Printf("%d is not PGP encrypted and is not plain-text\n", msg.Uid)
-			continue
 		}
-		encryptedSubject := part.Header.Get("Subject")
-		if encryptedSubject != "" {
-			subject = encryptedSubject
+	}
+	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,
 		}
-		partBodyReader := part.Body
-		body, err := io.ReadAll(partBodyReader)
-		if err != nil {
-			log.Fatalln(err)
+	}
+	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
 		}
-		asciidoc, _, _ := strings.Cut(string(body), "\n-- ")
+	}
+	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
+}
 
-		filename := msg.Envelope.Date.Format("20060102.html")
-		_, err = os.Stat(filename)
-		if err == nil {
-			asciidoc = "=== " + msg.Envelope.Date.Format("03:04 -0700") + "\n\n_" + subject + "_\n\n" + asciidoc
-		} else {
-			if errors.Is(err, os.ErrNotExist) {
-				asciidoc = "== " + msg.Envelope.Date.Format("Jan 2") + "\n\n_" + subject + "_\n\n" + asciidoc
-			} else {
-				log.Fatalln(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
+}
 
-		reader := strings.NewReader(asciidoc)
-		writer := bytes.NewBuffer([]byte{})
-		config := configuration.NewConfiguration()
-		libasciidoc.Convert(reader, writer, config)
+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)
+}
 
-		html := string(writer.Bytes())
-		html = strings.ReplaceAll(html, "<div class=\"sect2\">\n", "")
-		html = strings.ReplaceAll(html, "<div class=\"sect1\">\n", "")
-		html = strings.ReplaceAll(html, "<div class=\"sectionbody\">\n", "")
-		html = strings.ReplaceAll(html, "<div class=\"paragraph\">\n", "")
-		html = strings.ReplaceAll(html, "</div>\n", "")
-		f, err := os.OpenFile(filename, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0600)
-		if err != nil {
-			log.Fatalln(err)
-		}
-		defer f.Close()
-		f.WriteString(html)
-		delSeqset.AddNum(msg.Uid)
-		doneMessages++
+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
 	}
-	if !delSeqset.Empty() {
+	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 = c.Select("INBOX", false)
-		if err != nil {
-			log.Fatalln(err)
-		}
-		err = c.UidStore(delSeqset, item, flags, nil)
-		if err != nil {
-			log.Fatalln(err)
-		}
-		return doneMessages, c.Expunge(nil)
+		err := em.Client().UidStore(em.delSeqset, item, flags, nil)
+		return err
 	}
-	return doneMessages, nil
+	return nil
 }