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) {