asgard.git

commit e30ed0061ad7e68c7848692fb25cc6c21ab5c77c

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

Merge branch 'gottv2'

 eostre/eostre.go | 382 +++++++++++++++++++++++++++++++---------------
 eostre/eostre_2.go | 1 
 gersemi/gersemi.go | 2 
 hermodr/hermodr.go | 2 
 idavollr/message.go | 3 
 mkfile | 2 


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
 }




diff --git a/eostre/eostre_2.go b/eostre/eostre_2.go
index b6a30cb5f4ccb9a09baaf8c14065bad3521830e8..c96dc6b597c415a4da9176222f667deb7401ec84 100644
--- a/eostre/eostre_2.go
+++ b/eostre/eostre_2.go
@@ -115,6 +115,7 @@ 	home, err := os.UserHomeDir()
 	if err != nil {
 		return err
 	}
+	// TODO find also in /usr/share/asgard
 	_, err = exec.Command(path.Join(home, ".local/share/asgard/eostre.sh")).Output()
 	return err
 }




diff --git a/gersemi/gersemi.go b/gersemi/gersemi.go
index c41bf6e3f965ad765b06016fc615c6d3ae3b637e..0f3a225fa2e2c412c926fc426ac5b2b35c23709f 100644
--- a/gersemi/gersemi.go
+++ b/gersemi/gersemi.go
@@ -164,7 +164,7 @@ 			Recover(ignoreInvalidMessage).
 			Recover(idavollr.RecoverMalformedMessage).
 			Recover(idavollr.RecoverErroredMessages)
 		if r.E != nil {
-			return r.E
+			log.Printf("while processing message %s: %v", msg.Envelope.Subject, r.E)
 		}
 	}
 	return nil




diff --git a/hermodr/hermodr.go b/hermodr/hermodr.go
index bc191aa24b153ae53c09cbca6fa905c2186860de..a378e03f840efb566d330e304ff036f4921cbb3a 100644
--- a/hermodr/hermodr.go
+++ b/hermodr/hermodr.go
@@ -138,7 +138,7 @@ 		hm.armour +
 		"\r\n" +
 		"\r\n" +
 		"-----------------------997d365ae018229dc62ea2ff6b617cac--\r\n")
-	err := smtp.SendMail(hm.config.Hermodr.SmtpServer, nil, hm.config.Hermodr.SmtpUsername, to, msg)
+	err := smtp.SendMail(hm.config.Hermodr.SmtpServer, nil, hm.config.Hermodr.SmtpUsername, to, msg) // TODO send with login
 	return err
 }
 




diff --git a/idavollr/message.go b/idavollr/message.go
index cfdd81b36afc1e8e7f5eb673f469d69f998dc64c..cebdfab05f61ee54ce66a8b132c401c60a9ffafa 100644
--- a/idavollr/message.go
+++ b/idavollr/message.go
@@ -83,7 +83,7 @@ func (m ImapMessage) MessageBody() []byte {
 	return m.bdy
 }
 
-func (m ImapMessage) SetClient(c *client.Client) {
+func (m *ImapMessage) SetClient(c *client.Client) {
 	m.cli = c
 }
 
@@ -110,6 +110,7 @@ 	message, err := message.Read(r)
 	m.SetMimeMessage(message)
 	return m, err
 }
+
 func getNextPart(message *message.Entity, ID string, mimetype string) (io.Reader, error) {
 	if mr := message.MultipartReader(); mr != nil {
 		for {




diff --git a/mkfile b/mkfile
index ebe69a346d1f193c872e820724b7ce7ce8729a2d..3b07ebf38487981043d707c15e92fd20d7f11fc0 100644
--- a/mkfile
+++ b/mkfile
@@ -1,2 +1,2 @@
-asgard: `ls *.go`
+asgard: eostre/eostre.go eostre/eostre_2.go gersemi/gersemi.go hermodr/hermodr.go himinbjorg/address.go himinbjorg/db.go himinbjorg/knownAddress.go himinbjorg/lock.go himinbjorg/message.go idavollr/errors.go idavollr/imap.go idavollr/mailbox.go idavollr/message.go jotunheim/config.go main.go mimir/mimir.go tyr/tyr.go vor/vor.go
 	 export CGO_CFLAGS="-D_LARGEFILE64_SOURCE"; go build -v -ldflags "-s -w"