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"