asgard.git

commit 839ef5599b9b931a67aa0f0bf4a34be2546a5765

Author: Adam <git@apiote.xyz>

reformat mimir

 gersemi.go | 14 +
 imap_mailbox.go | 16 ++
 imap_message.go | 30 +++
 main.go | 1 
 mimir.go | 367 ++++++++++++++++++++------------------------------


diff --git a/gersemi.go b/gersemi.go
index d3a1723a19135dc949d71e06d391a2caa11ba046..6cb645800ec349fff881754282692cbb93ed9aa5 100644
--- a/gersemi.go
+++ b/gersemi.go
@@ -81,6 +81,7 @@ func gersemi(config Config) error {
 	timeout, _ := time.ParseDuration("60s")
 	mailbox := &GersemiMailbox{
 		Mailbox: Mailbox{
+			mboxName: config.Gersemi.ImapInbox,
 			imapAdr:  config.Gersemi.ImapAddress,
 			imapUser: config.Gersemi.ImapUsername,
 			imapPass: config.Gersemi.ImapPassword,
@@ -101,7 +102,8 @@ 		Tee(login).
 		Bind(selectInbox).
 		Tee(checkEmptyBox).
 		Map(fetchMessages).
-		Bind(createTransactions).
+		Tee(createTransactions).
+		Tee(checkFetchError).
 		Recover(ignoreEmptyBox).
 		Recover(disconnect)
 
@@ -129,7 +131,7 @@
 	return m, nil
 }
 
-func createTransactions(am AbstractMailbox) (AbstractMailbox, error) {
+func createTransactions(am AbstractMailbox) error {
 	m := am.(*GersemiMailbox)
 	for msg := range m.messages() {
 		r := gott.R[AbstractImapMessage]{
@@ -157,13 +159,13 @@ 			Bind(doRequest).
 			Bind(handleHttpError).
 			SafeTee(moveGersemiMessage).
 			Recover(ignoreGersemiInvalidMessage).
-			Recover(recoverGersemiMalformedMessage).
-			Recover(recoverGersemiErroredMessages)
+			Recover(recoverMalformedMessage).
+			Recover(recoverErroredMessages)
 		if r.E != nil {
-			return m, r.E
+			return r.E
 		}
 	}
-	return m, nil
+	return nil
 }
 
 func matchRegex(m *GersemiImapMessage, match []string, groupNames []string) *GersemiImapMessage {




diff --git a/imap_mailbox.go b/imap_mailbox.go
index 50d3a471530675998722344c441f294ad434c7ca..8fb216b411057b598e906bc5bfecf4c4622d1f50 100644
--- a/imap_mailbox.go
+++ b/imap_mailbox.go
@@ -9,6 +9,7 @@ 	"github.com/emersion/go-imap/client"
 )
 
 type AbstractMailbox interface {
+	mailboxName() string
 	imapAddress() string
 	imapUsername() string
 	imapPassword() string
@@ -26,6 +27,7 @@ 	setupChannels()
 }
 
 type Mailbox struct {
+	mboxName string
 	imapAdr  string
 	imapUser string
 	imapPass string
@@ -36,6 +38,10 @@ 	mbox *imap.MailboxStatus
 	sect *imap.BodySectionName
 	msgs chan *imap.Message
 	dn   chan error
+}
+
+func (m Mailbox) mailboxName() string {
+	return m.mboxName
 }
 
 func (m Mailbox) imapAddress() string {
@@ -102,7 +108,7 @@ 	return m.client().Login(m.imapUsername(), m.imapPassword())
 }
 
 func selectInbox(m AbstractMailbox) (AbstractMailbox, error) {
-	mbox, err := m.client().Select("INBOX", false) // todo configure inbox name
+	mbox, err := m.client().Select(m.mailboxName(), false)
 	m.setMailbox(mbox)
 	return m, err
 }
@@ -126,6 +132,14 @@ 	go func() {
 		m.done() <- m.client().Fetch(seqset, items, m.messages())
 	}()
 	return m
+}
+
+func checkFetchError(m AbstractMailbox) error {
+	return <-m.done()
+}
+
+func expunge(m AbstractMailbox) error {
+	return m.client().Expunge(nil)
 }
 
 func ignoreEmptyBox(m AbstractMailbox, e error) (AbstractMailbox, error) {




diff --git a/imap_message.go b/imap_message.go
index 3a9c5a4eed2cd864f071ff1cf5679cfa441bdf95..009faf5732bab9c22f6e5dbb7f5532e1afa0a885 100644
--- a/imap_message.go
+++ b/imap_message.go
@@ -3,6 +3,7 @@
 import (
 	"bytes"
 	"errors"
+	"fmt"
 	"io"
 	"log"
 
@@ -97,6 +98,31 @@ 	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 {
+			p, err := mr.NextPart()
+			if err == io.EOF {
+				break
+			} else if err != nil {
+				return nil, fmt.Errorf("while reading next part: %w", err)
+			}
+			return getNextPart(p, ID, mimetype)
+		}
+	} else {
+		t, _, err := message.Header.ContentType()
+		if err != nil {
+			return nil, fmt.Errorf("while getting content type: %w", err)
+		}
+		if t == mimetype {
+			return message.Body, nil
+		}
+	}
+	return nil, MalformedMessageError{
+		Cause:     errors.New(mimetype + " not found"),
+		MessageID: ID,
+	}
+}
 
 func getBody(m AbstractImapMessage) (AbstractImapMessage, error) {
 	reader, err := getNextPart(m.mimeMessage(), m.message().Envelope.MessageId, m.mimeType())
@@ -110,7 +136,7 @@ 	m.setMessageBody(body)
 	return m, err
 }
 
-func recoverGersemiMalformedMessage(m AbstractImapMessage, err error) (AbstractImapMessage, error) {
+func recoverMalformedMessage(m AbstractImapMessage, err error) (AbstractImapMessage, error) {
 	var malformedMessageError MalformedMessageError
 	if errors.As(err, &malformedMessageError) {
 		err = nil
@@ -120,7 +146,7 @@ 	}
 	return m, err
 }
 
-func recoverGersemiErroredMessages(m AbstractImapMessage, err error) (AbstractImapMessage, error) {
+func recoverErroredMessages(m AbstractImapMessage, err error) (AbstractImapMessage, error) {
 	log.Printf("message %s errored: %v\n", m.message().Envelope.MessageId, err)
 	return m, nil
 }




diff --git a/main.go b/main.go
index 9ae3be702b042e8452699e0f28d891b43055af5c..0a9e6f631518e8bd356ac25811a3a8d544ab3b18 100644
--- a/main.go
+++ b/main.go
@@ -62,6 +62,7 @@ 	Firefly           string
 	ImapAddress       string
 	ImapUsername      string
 	ImapPassword      string
+	ImapInbox         string
 	MessageMime       string
 	DoneFolder        string
 	DefaultSource     string




diff --git a/mimir.go b/mimir.go
index 8c0bd0cd9fc6f14d1ab548dc9abbffe7878de933..57f7b776a5c99d7cdc5e560c2453ee7b479527b5 100644
--- a/mimir.go
+++ b/mimir.go
@@ -34,7 +34,6 @@ 	"database/sql"
 	"errors"
 	"fmt"
 	"html/template"
-	"io"
 	"log"
 	"net/http"
 	"regexp"
@@ -44,7 +43,6 @@
 	"apiote.xyz/p/gott/v2"
 	"github.com/emersion/go-imap"
 	"github.com/emersion/go-imap/client"
-	"github.com/emersion/go-message"
 	_ "github.com/emersion/go-message/charset"
 	"github.com/emersion/go-msgauth/dkim"
 )
@@ -71,282 +69,211 @@ func (l ListingPage) NextPage() int {
 	return l.Page + 1
 }
 
-type mimirStruct struct {
-	mboxName string
-	c        *client.Client
-	config   Config
-	db       *sql.DB
-
+type MimirMailbox struct {
+	Mailbox
 	categories     []string
 	categoryRegexp *regexp.Regexp
-	mbox           *imap.MailboxStatus
-	messages       chan *imap.Message
-	section        *imap.BodySectionName
-	done           chan error
+	db             *sql.DB
+}
 
-	message             *imap.Message
-	category            string
-	messageBytes        []byte
-	dkimStatus          bool
-	mimeMessage         *message.Entity
-	plainTextBodyReader io.Reader
-	plainTextBody       []byte
-	recipients          []string
+type MimirImapMessage struct {
+	ImapMessage
+	categoryRegexp *regexp.Regexp
+	categories     []string
+	category       string
+	dkimStatus     bool
+	db             *sql.DB
+	config         Config
+	recipients     []string
+	client         *client.Client
+	mboxName       string
 }
 
-func getCategories(s mimirStruct) (mimirStruct, error) {
-	s.categories = s.config.Mimir.Categories
-	if len(s.categories) == 0 {
-		return s, errors.New("no categories defined")
+func mimir(db *sql.DB, config Config) error {
+	r := gott.R[AbstractMailbox]{
+		S: &MimirMailbox{
+			Mailbox: Mailbox{
+				mboxName: config.Mimir.ImapInbox,
+				imapAdr:  config.Mimir.ImapAddress,
+				imapUser: config.Mimir.ImapUsername,
+				imapPass: config.Mimir.ImapPassword,
+				conf:     config,
+			},
+			db: db,
+		},
+	}.
+		Bind(getCategories).
+		Bind(prepareCategoryRegexp).
+		Bind(connect).
+		Tee(login).
+		Bind(selectInbox).
+		Tee(checkEmptyBox).
+		Map(fetchMessages).
+		Tee(archiveMessages).
+		Tee(checkFetchError).
+		Tee(expunge).
+		Recover(ignoreEmptyBox).
+		Recover(disconnect)
+
+	return r.E
+}
+
+func getCategories(am AbstractMailbox) (AbstractMailbox, error) {
+	m := am.(*MimirMailbox)
+	m.categories = m.config().Mimir.Categories
+	if len(m.categories) == 0 {
+		return m, errors.New("no categories defined")
 	}
-	return s, nil
+	return m, nil
 }
 
-func prepareCategoryRegexp(s mimirStruct) (mimirStruct, error) {
-	if !strings.Contains(s.config.Mimir.RecipientTemplate, "[:]") {
-		return s, errors.New("recipient template does not contain ‘[:]’")
+func prepareCategoryRegexp(am AbstractMailbox) (AbstractMailbox, error) {
+	m := am.(*MimirMailbox)
+	if !strings.Contains(m.config().Mimir.RecipientTemplate, "[:]") {
+		return m, errors.New("recipient template does not contain ‘[:]’")
 	}
-	recipientRegexp := strings.Replace(s.config.Mimir.RecipientTemplate, "[:]", "(.*)", 1)
+	recipientRegexp := strings.Replace(m.config().Mimir.RecipientTemplate, "[:]", "(.*)", 1)
 	r, err := regexp.Compile(recipientRegexp)
-	s.categoryRegexp = r
-	return s, err
+	m.categoryRegexp = r
+	return m, err
 }
 
-func selectMimirInbox(s mimirStruct) (mimirStruct, error) {
-	mbox, err := s.c.Select(s.mboxName, true)
-	s.mbox = mbox
-	return s, err
+func archiveMessages(am AbstractMailbox) error {
+	m := am.(*MimirMailbox)
+	for msg := range m.messages() {
+		r := gott.R[AbstractImapMessage]{
+			S: &MimirImapMessage{
+				ImapMessage: ImapMessage{
+					msg:      msg,
+					sect:     m.section(),
+					mimetype: "text/plain",
+				},
+				categoryRegexp: m.categoryRegexp,
+				categories:     m.categories,
+				db:             m.db,
+				config:         m.config(),
+				client:         m.cli,
+				mboxName:       m.mboxName,
+			},
+		}.
+			Bind(getMessageCategory).
+			Bind(readBody).
+			Bind(verifyDkim).
+			Bind(parseMimeMessage).
+			Bind(getBody).
+			Bind(readBody).
+			Tee(archiveMessage).
+			Tee(updateTopicRecipients).
+			Bind(getMessageRecipients).
+			Tee(forwardMimirMessage).
+			Recover(recoverMalformedMessage).
+			Recover(recoverMimirUnknownCategory).
+			Tee(removeMimirMessage).
+			Recover(recoverErroredMessages)
+		if r.E != nil {
+			return r.E
+		}
+	}
+	return nil
 }
 
-func getMessageCategory(s mimirStruct) (mimirStruct, error) {
+func getMessageCategory(am AbstractImapMessage) (AbstractImapMessage, error) {
+	m := am.(*MimirImapMessage)
 	categoryInAddress := ""
-	recipients := append(s.message.Envelope.To, s.message.Envelope.Cc...)
+	recipients := append(m.message().Envelope.To, m.message().Envelope.Cc...)
 	for _, recipient := range recipients {
-		matches := s.categoryRegexp.FindStringSubmatch(recipient.Address())
+		matches := m.categoryRegexp.FindStringSubmatch(recipient.Address())
 		if len(matches) != 2 {
 			continue
 		}
 		categoryInAddress = matches[1]
-		for _, category := range s.categories {
+		for _, category := range m.categories {
 			if matches[1] == category {
-				s.category = category
-				return s, nil
+				m.category = category
+				return m, nil
 			}
 		}
 	}
-	return s, UnknownCategoryError{
-		MessageID: s.message.Envelope.MessageId,
+	return m, UnknownCategoryError{
+		MessageID: m.message().Envelope.MessageId,
 		Category:  categoryInAddress,
 	}
 }
 
-func readMimirBody(s mimirStruct) (mimirStruct, error) {
-	r := s.message.GetBody(s.section)
-	if r == nil {
-		return s, MalformedMessageError{
-			Cause:     errors.New("no body in message"),
-			MessageID: s.message.Envelope.MessageId,
-		}
-	}
-	messageBytes, err := io.ReadAll(r)
-	s.messageBytes = messageBytes
-	return s, err
-}
-
-func verifyDkim(s mimirStruct) (mimirStruct, error) {
+func verifyDkim(am AbstractImapMessage) (AbstractImapMessage, error) {
+	m := am.(*MimirImapMessage)
 	dkimStatus := false
-	r := bytes.NewReader(s.messageBytes)
+	r := bytes.NewReader(m.messageBytes())
 	verifications, err := dkim.Verify(r)
 	if err != nil {
-		return s, err
+		return m, err
 	}
 	for _, v := range verifications {
-		if v.Err == nil && s.message.Envelope.From[0].HostName == v.Domain {
+		if v.Err == nil && m.message().Envelope.From[0].HostName == v.Domain {
 			dkimStatus = true
 		}
 	}
-	s.dkimStatus = dkimStatus
-	return s, nil
+	m.dkimStatus = dkimStatus
+	return m, nil
 }
 
-func getNextPart(message *message.Entity, ID string, mimetype string) (io.Reader, error) {
-	if mr := message.MultipartReader(); mr != nil {
-		for {
-			p, err := mr.NextPart()
-			if err == io.EOF {
-				break
-			} else if err != nil {
-				return nil, fmt.Errorf("while reading next part: %w", err)
-			}
-			return getNextPart(p, ID, mimetype)
-		}
-	} else {
-		t, _, err := message.Header.ContentType()
-		if err != nil {
-			return nil, fmt.Errorf("while getting content type: %w", err)
-		}
-		if t == mimetype {
-			return message.Body, nil
-		}
-	}
-	return nil, MalformedMessageError{
-		Cause:     errors.New(mimetype + " not found"),
-		MessageID: ID,
-	}
-}
-
-func getPlainTextBody(s mimirStruct) (mimirStruct, error) {
-	body, err := getNextPart(s.mimeMessage, s.message.Envelope.MessageId, "text/plain")
-	s.plainTextBodyReader = body
-	return s, err
-}
-
-func readPlainTextBody(s mimirStruct) (mimirStruct, error) {
-	body, err := io.ReadAll(s.plainTextBodyReader)
-	s.plainTextBody = body
-	return s, err
-}
-
-func archiveMessage(s mimirStruct) error {
-	messageID := s.message.Envelope.MessageId
-	subject := s.message.Envelope.Subject
-	date := s.message.Envelope.Date.UTC()
-	inReplyTo := s.message.Envelope.InReplyTo
-	sender := s.message.Envelope.From[0]
+func archiveMessage(am AbstractImapMessage) error {
+	m := am.(*MimirImapMessage)
+	messageID := m.message().Envelope.MessageId
+	subject := m.message().Envelope.Subject
+	date := m.message().Envelope.Date.UTC()
+	inReplyTo := m.message().Envelope.InReplyTo
+	sender := m.message().Envelope.From[0]
 	log.Printf("archiving %s\n", messageID)
-	return addArchiveEntry(s.db, messageID, s.category, subject, s.plainTextBody, date, inReplyTo, s.dkimStatus, sender, string(s.messageBytes))
+	return addArchiveEntry(m.db, messageID, m.category, subject, m.messageBody(), date, inReplyTo, m.dkimStatus, sender, string(m.messageBytes()))
 }
 
-func updateTopicRecipients(s mimirStruct) error {
+func updateTopicRecipients(am AbstractImapMessage) error {
+	m := am.(*MimirImapMessage)
 	var sender *imap.Address
-	if len(s.message.Envelope.ReplyTo) > 0 {
-		sender = s.message.Envelope.ReplyTo[0]
+	if len(m.message().Envelope.ReplyTo) > 0 {
+		sender = m.message().Envelope.ReplyTo[0]
 	} else {
-		sender = s.message.Envelope.From[0]
+		sender = m.message().Envelope.From[0]
 	}
-	messageID := s.message.Envelope.MessageId
-	return updateRecipients(s.db, sender, messageID)
+	messageID := m.message().Envelope.MessageId
+	return updateRecipients(m.db, sender, messageID)
 }
 
-func getMessageRecipients(s mimirStruct) (mimirStruct, error) {
-	sender := s.message.Envelope.From[0]
-	messageID := s.message.Envelope.MessageId
-	recipients, err := getRecipients(s.db, messageID, sender)
-	if sender.Address() != s.config.Mimir.PersonalAddress {
-		recipients = append(recipients, strings.Replace(s.config.Mimir.ForwardAddress, "[:]", s.category, 1))
+func getMessageRecipients(am AbstractImapMessage) (AbstractImapMessage, error) {
+	m := am.(*MimirImapMessage)
+	sender := m.message().Envelope.From[0]
+	messageID := m.message().Envelope.MessageId
+	recipients, err := getRecipients(m.db, messageID, sender)
+	if sender.Address() != m.config.Mimir.PersonalAddress {
+		recipients = append(recipients, strings.Replace(m.config.Mimir.ForwardAddress, "[:]", m.category, 1))
 	}
-	s.recipients = recipients
-	return s, err
+	m.recipients = recipients
+	return m, err
 }
 
-func forwardMimirMessage(s mimirStruct) error {
-	messageID := s.message.Envelope.MessageId
-	inReplyTo := s.message.Envelope.InReplyTo
-	subject := s.message.Envelope.Subject
-	sender := s.message.Envelope.From[0]
-	log.Printf("forwarding %s to %v\n", messageID, s.recipients)
-	return forwardMessage(s.config, s.category, messageID, inReplyTo, subject, s.plainTextBody, s.recipients, sender)
+func forwardMimirMessage(am AbstractImapMessage) error {
+	m := am.(*MimirImapMessage)
+	messageID := m.message().Envelope.MessageId
+	inReplyTo := m.message().Envelope.InReplyTo
+	subject := m.message().Envelope.Subject
+	sender := m.message().Envelope.From[0]
+	log.Printf("forwarding %s to %v\n", messageID, m.recipients)
+	return forwardMessage(m.config, m.category, messageID, inReplyTo, subject, m.messageBody(), m.recipients, sender)
 }
 
-func recoverMalformedMessage(s mimirStruct, err error) (mimirStruct, error) {
-	var malformedMessageError MalformedMessageError
-	if errors.As(err, &malformedMessageError) {
-		err = nil
-		log.Println(malformedMessageError.Error())
-		log.Println(string(s.messageBytes))
-	}
-	return s, err
-}
-
-func recoverUnknownCategory(s mimirStruct, err error) (mimirStruct, error) {
+func recoverMimirUnknownCategory(m AbstractImapMessage, err error) (AbstractImapMessage, error) {
 	var unknownCategoryError UnknownCategoryError
 	if errors.As(err, &unknownCategoryError) {
 		err = nil
 		log.Println(unknownCategoryError.Error())
 	}
-	return s, err
-}
-
-func removeMimirMessage(s mimirStruct) error {
-	return removeMessage(s.c, s.message.Uid, s.mboxName)
-}
-
-func recoverErroredMessages(s mimirStruct, err error) (mimirStruct, error) {
-	log.Printf("message %s errored: %v\n", s.message.Envelope.MessageId, err)
-	return s, nil
-}
-
-func archiveMessages(s mimirStruct) (mimirStruct, error) {
-	for msg := range s.messages {
-		s.message = msg
-		r := gott.R[mimirStruct]{S: s}.
-			Bind(getMessageCategory).
-			Bind(readMimirBody).
-			Bind(verifyDkim).
-			Bind(parseMimeMessage).
-			Bind(getPlainTextBody).
-			Bind(readPlainTextBody).
-			Tee(archiveMessage).
-			Tee(updateTopicRecipients).
-			Bind(getMessageRecipients).
-			Tee(forwardMimirMessage).
-			Recover(recoverMalformedMessage).
-			Recover(recoverUnknownCategory).
-			Tee(removeMimirMessage).
-			Recover(recoverErroredMessages)
-		if r.E != nil {
-			return s, r.E
-		}
-	}
-	return s, nil
+	return m, err
 }
 
-func checkFetchError(s mimirStruct) error {
-	return <-s.done
-}
-
-func expunge(s mimirStruct) error {
-	return s.c.Expunge(nil)
-}
-
-func archiveInbox(db *sql.DB, mboxName string, c *client.Client, config Config) error {
-	r := gott.R[mimirStruct]{
-		S: mimirStruct{
-			mboxName: mboxName,
-			c:        c,
-			config:   config,
-			db:       db,
-		},
-	}.
-		Bind(getCategories).
-		Bind(prepareCategoryRegexp).
-		Bind(selectMimirInbox).
-		Tee(checkEmptyBox).
-		Map(fetchMessages).
-		Bind(archiveMessages).
-		Tee(checkFetchError).
-		Tee(expunge).
-		Recover(ignoreEmptyBox)
-
-	return r.E
-}
-
-func mimir(db *sql.DB, config Config) {
-	c, err := client.DialTLS(config.Mimir.ImapAddress, nil)
-	if err != nil {
-		log.Fatalln(err)
-	}
-	log.Println("Connected")
-	defer c.Close()
-	defer c.Logout()
-	if err := c.Login(config.Mimir.ImapUsername, config.Mimir.ImapPassword); err != nil {
-		log.Fatalln(err)
-	}
-	log.Println("Logged in")
-	err = archiveInbox(db, config.Mimir.ImapInbox, c, config)
-	if err != nil {
-		log.Fatalln(err)
-	}
+func removeMimirMessage(am AbstractImapMessage) error {
+	m := am.(*MimirImapMessage)
+	return removeMessage(m.client, m.message().Uid, m.mboxName)
 }
 
 func mimir_serve(db *sql.DB) func(w http.ResponseWriter, r *http.Request) {