Author: Adam <git@apiote.xyz>
hermóðr, and start of mímir
config_example.dirty | 48 ++++++++-- go.mod | 3 go.sum | 14 -- hermóðr.go | 211 +++++++++++++++++++++++++++++++++++++++++++++ imap.go | 12 +- main.go | 45 +++++++-- mímir.go | 118 +++++++++++++++++++++++++ týr.go | 30 +++---
diff --git a/config_example.dirty b/config_example.dirty index 46b8773f32b45a1b1373935c94c1e7468fb283cb..ccc7372d04dfeb22bdc5d2ff40ed138c8a8c178b 100644 --- a/config_example.dirty +++ b/config_example.dirty @@ -1,12 +1,40 @@ ( - ('imapAddress' 'imap.example.com:993') - ('imapUsername' 'user@example.com') - ('imapPassword' 'password') - ('imapFolderInbox' 'Inbox') - ('imapFolderArchive' 'Archive') - ('imapFolderDrafts' 'Drafts') - ('imapFolderJunk' 'Junk') - ('imapFolderQuarantine' 'Quarantine') - ('imapFolderSent' 'Sent') - ('imapFolderTrash' 'Trash') + ('týr' + ( + ('imapAddress' '') + ('imapUsername' '') + ('imapPassword' '') + ('imapFolderInbox' 'Inbox') + ('imapFolderJunk' 'Junk') + ('imapFolderArchive' 'Archive') + ('imapFolderDrafts' 'Drafts') + ('imapFolderQuarantine' 'Quarantine') + ('imapFolderSent' 'Sent') + ('imapFolderTrash' 'Trash') + ) + ) + ('hermóðr' + ( + ('imapAddress' '') + ('imapUsername' '') + ('imapPassword' '') + ('imapFolderInbox' 'INBOX') + ('imapFolderRedirected' 'redirected') + ('recipient' '') + ('smtpServer' '') + ('smtpUsername' '') + ('publicKey' ` +` + ) + ) + ) + ('mímir' + ( + ('imapAddress' '') + ('imapUsername' '') + ('imapPassword' '') + ('imapInboxes' ()) + ('imapFolderSent' 'Sent') + ) + ) ) diff --git a/go.mod b/go.mod index 728276240c0fb25c59a8a09e6ee3ed8ee9a11591..024ee0c8ff9622e480aef1fd8f77c432aace1fad 100644 --- a/go.mod +++ b/go.mod @@ -3,13 +3,13 @@ go 1.17 require ( + apiote.xyz/p/go-dirty v0.0.0-20211022164923-8652e7927cd7 github.com/ProtonMail/gopenpgp/v2 v2.2.4 github.com/emersion/go-imap v1.2.0 github.com/emersion/go-imap-move v0.0.0-20210907172020-fe4558f9c872 github.com/emersion/go-msgauth v0.6.5 github.com/emersion/go-smtp v0.15.0 github.com/mattn/go-sqlite3 v1.14.9 - notabug.org/apiote/go-dirty v0.0.0-20211019163451-a733b268151f notabug.org/apiote/gott v1.1.2 ) @@ -17,7 +17,6 @@ require ( github.com/ProtonMail/go-crypto v0.0.0-20210920160938-87db9fbc61c7 // indirect github.com/ProtonMail/go-mime v0.0.0-20190923161245-9b5a4261663a // indirect github.com/emersion/go-sasl v0.0.0-20211008083017-0b9dcfb154ac // indirect - github.com/konsorten/go-windows-terminal-sequences v1.0.3 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/sirupsen/logrus v1.8.1 // indirect golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 // indirect diff --git a/go.sum b/go.sum index 9735682019ee27b3dfd64d3f637d3a0a28f182f3..b130b7163bcb05d68c0008faf8d2217e1e9c2926 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +apiote.xyz/p/go-dirty v0.0.0-20211022164923-8652e7927cd7 h1:ab0pCRm0uGxLVUMg2fpRL7Jzz/vcI0i3sMam82l2cXU= +apiote.xyz/p/go-dirty v0.0.0-20211022164923-8652e7927cd7/go.mod h1:8QnoYcdnf+1AmRhC7GBUKuCl/wRpfI3Bd58JqDbJNtw= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/ProtonMail/go-crypto v0.0.0-20210920160938-87db9fbc61c7 h1:DSqTh6nEes/uO8BlNcGk8PzZsxY2sN9ZL//veWBdTRI= github.com/ProtonMail/go-crypto v0.0.0-20210920160938-87db9fbc61c7/go.mod h1:z4/9nQmJSSwwds7ejkxaJwO37dru3geImFUdJlaLzQo= @@ -19,7 +21,6 @@ github.com/emersion/go-message v0.15.0/go.mod h1:wQUEfE+38+7EW8p8aZ96ptg6bAb1iwdgej19uXASlE4= github.com/emersion/go-milter v0.3.2/go.mod h1:ablHK0pbLB83kMFBznp/Rj8aV+Kc3jw8cxzzmCNLIOY= github.com/emersion/go-msgauth v0.6.5 h1:UaXBtrjYBM3SWw9BBODeSp0uYtScx3CuIF7/RQfkeWo= github.com/emersion/go-msgauth v0.6.5/go.mod h1:/jbQISFJgtT12T8akRs20l+wI4HcyN/kWy7VRdHEAmA= -github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21 h1:OJyUGMJTzHTd1XQp98QTaHernxMYzRaOasRir9hUlFQ= github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ= github.com/emersion/go-sasl v0.0.0-20211008083017-0b9dcfb154ac h1:tn/OQ2PmwQ0XFVgAHfjlLyqMewry25Rz7jWnVoh4Ggs= github.com/emersion/go-sasl v0.0.0-20211008083017-0b9dcfb154ac/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ= @@ -28,10 +29,7 @@ github.com/emersion/go-smtp v0.15.0/go.mod h1:qm27SGYgoIPRot6ubfQ/GpiPy/g3PaZAVRxiO/sDUgQ= github.com/emersion/go-textwrapper v0.0.0-20160606182133-d0e65e56babe/go.mod h1:aqO8z8wPrjkscevZJFVE1wXJrLpC5LtJG7fqLOsPb2U= github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594 h1:IbFBtwoTQyw0fIM5xv1HF+Y+3ZijDR839WMulgxCcUY= github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594/go.mod h1:aqO8z8wPrjkscevZJFVE1wXJrLpC5LtJG7fqLOsPb2U= -github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/konsorten/go-windows-terminal-sequences v1.0.3 h1:CE8S1cTafDpPvMhIxNJKvHsGVBgn1xWYf1NbHQhywc8= -github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/martinlindhe/base36 v1.0.0/go.mod h1:+AtEs8xrBpCeYgSLoY/aJ6Wf37jtBuR0s35750M27+8= github.com/martinlindhe/base36 v1.1.0/go.mod h1:+AtEs8xrBpCeYgSLoY/aJ6Wf37jtBuR0s35750M27+8= github.com/mattn/go-sqlite3 v1.14.9 h1:10HX2Td0ocZpYEjhilsuo6WWtUqttj2Kb0KtD86/KYA= @@ -40,7 +38,6 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= @@ -54,7 +51,6 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= -golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2 h1:It14KIkyBFYkHkwZ7k45minvA9aorojkyjGk9KJ5B/w= golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 h1:7I4JAnoQBe7ZtJcBaYHi5UtiO8tQHbUSXxL+pnGRANg= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= @@ -75,8 +71,8 @@ golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68 h1:nxC68pudNYkKU6jWhgrqdreuFiOQWj1Fs7T3VrH4Pjw= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211015200801-69063c4bb744 h1:KzbpndAYEM+4oHRp9JmB2ewj0NHHxO3Z0g7Gus2O1kk= golang.org/x/sys v0.0.0-20211015200801-69063c4bb744/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= @@ -97,9 +93,5 @@ golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -notabug.org/apiote/go-dirty v0.0.0-20211019154812-210944925679 h1:aiMJbTBmhuIPLg/FUZoupc3IfLx8v0DwRLuL7e0hVsY= -notabug.org/apiote/go-dirty v0.0.0-20211019154812-210944925679/go.mod h1:RWD4Phpy80kyv9MgCb4V1OFxvkabvYnfuDtuXRySzj0= -notabug.org/apiote/go-dirty v0.0.0-20211019163451-a733b268151f h1:/RA20wfxzNz8emSqWxOG46ix+mjAMBecEmO1JqWiRjk= -notabug.org/apiote/go-dirty v0.0.0-20211019163451-a733b268151f/go.mod h1:RWD4Phpy80kyv9MgCb4V1OFxvkabvYnfuDtuXRySzj0= notabug.org/apiote/gott v1.1.2 h1:Z22X9/8XrK5M5oARoE2fh3sJGPAJ84GuyGg2nKOjweQ= notabug.org/apiote/gott v1.1.2/go.mod h1:Z9hFvCdzZkFSegBkLa6n0X6AuUiw2BwgG4MFLgBMjD4= diff --git a/hermóðr.go b/hermóðr.go new file mode 100644 index 0000000000000000000000000000000000000000..9daa468382c9132ab914fb1a154743ccb47b97ea --- /dev/null +++ b/hermóðr.go @@ -0,0 +1,211 @@ +package main + +import ( + "fmt" + "io" + "net/mail" + "os" + "strings" + + "github.com/ProtonMail/gopenpgp/v2/helper" + "github.com/emersion/go-imap" + "github.com/emersion/go-imap/client" + "github.com/emersion/go-smtp" + "notabug.org/apiote/gott" +) + +type EmptyMessageError struct{} + +func (EmptyMessageError) Error() string { + return "Server didn't return message body" +} + +type Result struct { + Config Config + + Client *client.Client + Mailbox *imap.MailboxStatus + Literal imap.Literal + Message *mail.Message + Body string + PlainText string + Armour string +} + +func connect(args ...interface{}) (interface{}, error) { + result := args[0].(Result) + c, err := client.Dial(result.Config.Hermóðr.ImapAddress) + result.Client = c + return result, err +} + +func login(args ...interface{}) error { + result := args[0].(Result) + err := result.Client.Login(result.Config.Hermóðr.ImapUsername, result.Config.Hermóðr.ImapPassword) + return err +} + +func selectInbox(args ...interface{}) (interface{}, error) { + result := args[0].(Result) + mbox, err := result.Client.Select(result.Config.Hermóðr.ImapFolderInbox, false) + result.Mailbox = mbox + return result, err +} + +func redirectMessages(args ...interface{}) (interface{}, error) { + result := args[0].(Result) + + for result.Mailbox.Messages > 0 { + r, err := gott.NewResult(result). + Bind(getMessage). + Bind(readMessage). + Bind(readBody). + Map(composePlaintextBody). + Bind(encrypt). + Tee(send). + Tee(markRead). + Tee(moveMessage). + Bind(selectInbox). + Finish() + if err != nil { + return r, err + } + result = r.(Result) + } + return result, nil +} +func getMessage(args ...interface{}) (interface{}, error) { + result := args[0].(Result) + + seqset := new(imap.SeqSet) + seqset.AddNum(1) + + section := &imap.BodySectionName{} + items := []imap.FetchItem{section.FetchItem()} + + messages := make(chan *imap.Message, 1) + done := make(chan error, 1) + go func() { + done <- result.Client.Fetch(seqset, items, messages) + }() + + msg := <-messages + r := msg.GetBody(section) + if r == nil { + return result, EmptyMessageError{} + } + result.Literal = r + + err := <-done + return result, err +} + +func readMessage(args ...interface{}) (interface{}, error) { + result := args[0].(Result) + m, err := mail.ReadMessage(result.Literal) + result.Message = m + return result, err +} + +func readBody(args ...interface{}) (interface{}, error) { + result := args[0].(Result) + body, err := io.ReadAll(result.Message.Body) + result.Body = string(body) + return result, err +} + +func composePlaintextBody(args ...interface{}) interface{} { + result := args[0].(Result) + + header := result.Message.Header + plainText := "Content-Type: " + header.Get("Content-Type") + "; protected-headers=\"v1\"\r\n" + plainText += "From: " + header.Get("From") + "\r\n" + plainText += "Message-ID: " + header.Get("Message-ID") + "\r\n" + plainText += "Subject: " + header.Get("Subject") + "\r\n" + plainText += "\r\n" + plainText += string(result.Body) + result.PlainText = plainText + return result +} + +func encrypt(args ...interface{}) (interface{}, error) { + result := args[0].(Result) + armour, err := helper.EncryptMessageArmored(result.Config.Hermóðr.PublicKey, result.PlainText) + result.Armour = armour + return result, err +} + +func send(args ...interface{}) error { + result := args[0].(Result) + + from := result.Message.Header.Get("From") + date := result.Message.Header.Get("Date") + messageID := result.Message.Header.Get("Message-ID") + to := []string{result.Config.Hermóðr.Recipient} + msg := strings.NewReader("To: " + result.Config.Hermóðr.Recipient + "\r\n" + + "From: " + from + "\r\n" + + "Date: " + date + "\r\n" + + "Message-ID: " + messageID + "\r\n" + + "MIME-Version: 1.0\r\n" + + "Subject: ...\r\n" + + "Content-Type: multipart/encrypted; protocol=\"application/pgp-encrypted\"; boundary=\"---------------------997d365ae018229dc62ea2ff6b617cac\"; charset=utf-8\r\n" + + "\r\n" + + "This is an OpenPGP/MIME encrypted message (RFC 4880 and 3156)\r\n" + + "-----------------------997d365ae018229dc62ea2ff6b617cac\r\n" + + "Content-Type: application/pgp-encrypted\r\n" + + "Content-Description: PGP/MIME version identification\r\n" + + "\r\n" + + "Version: 1\r\n" + + "\r\n" + + "-----------------------997d365ae018229dc62ea2ff6b617cac\r\n" + + "Content-Type: application/octet-stream; name=\"encrypted.asc\"\r\n" + + "Content-Description: OpenPGP encrypted message\r\n" + + "Content-Disposition: inline; filename=\"encrypted.asc\"\r\n" + + "\r\n" + + result.Armour + + "\r\n" + + "\r\n" + + "-----------------------997d365ae018229dc62ea2ff6b617cac--\r\n") + err := smtp.SendMail(result.Config.Hermóðr.SmtpServer, nil, result.Config.Hermóðr.SmtpUsername, to, msg) + return err +} + +func markRead(args ...interface{}) error { + result := args[0].(Result) + + seqset := new(imap.SeqSet) + seqset.AddNum(1) + item := imap.FormatFlagsOp(imap.AddFlags, true) + flags := []interface{}{imap.SeenFlag} + err := result.Client.Store(seqset, item, flags, nil) + return err +} + +func moveMessage(args ...interface{}) error { + result := args[0].(Result) + + seqset := new(imap.SeqSet) + seqset.AddNum(1) + err := result.Client.Copy(seqset, result.Config.Hermóðr.ImapFolderRedirected) + return err +} + +func hermodr(config Config) { + r, err := gott.NewResult(Result{Config: config}). + SetLevelLog(gott.Debug). + Bind(connect). + Tee(login). + Bind(selectInbox). + Bind(redirectMessages). + Finish() + + if err != nil { + fmt.Fprintf(os.Stderr, "Error: %v", err) + return + } + // Don't forget to logout + if r.(Result).Client != nil { + r.(Result).Client.Logout() + } + +} diff --git a/imap.go b/imap.go index c07f4189fce56145992da3292ecdf97404ece8ba..558b05ce07e2589fb9f311a943df90c6e15c1a87 100644 --- a/imap.go +++ b/imap.go @@ -8,19 +8,19 @@ "github.com/emersion/go-imap-move" "github.com/emersion/go-imap/client" ) -func moveToJunk(c *client.Client, msg *imap.Message) { - log.Printf("moving %v : %s from %s to junk\n", msg.Envelope.Date, msg.Envelope.Subject, msg.Envelope.From[0].Address()) +func moveMsg(c *client.Client, msg *imap.Message, dest string) { + log.Printf("moving %v : %s from %s to %s\n", msg.Envelope.Date, msg.Envelope.Subject, msg.Envelope.From[0].Address(), dest) moveClient := move.NewClient(c) seqSet := new(imap.SeqSet) seqSet.AddNum(msg.Uid) - moveClient.UidMoveWithFallback(seqSet, "Junk") + moveClient.UidMoveWithFallback(seqSet, dest) } -func moveToQuarantine(c *client.Client, seqSet *imap.SeqSet) error { +func moveMultiple(c *client.Client, seqSet *imap.SeqSet, dest string) error { moveClient := move.NewClient(c) if !seqSet.Empty() { - log.Println("moving multiple to quarantine") - err := moveClient.UidMoveWithFallback(seqSet, "Quarantine") + log.Println("moving multiple to "+dest) + err := moveClient.UidMoveWithFallback(seqSet, dest) return err } return nil diff --git a/main.go b/main.go index c6745665b6b14a8d5b012f7966108a229167340b..b51d017da987908a16c857644032aec30d3f9e6f 100644 --- a/main.go +++ b/main.go @@ -5,20 +5,45 @@ "log" "net/http" "os" - "notabug.org/apiote/go-dirty" + "apiote.xyz/p/go-dirty" ) -type Config struct { +type TýrConfig struct { ImapAddress string ImapUsername string ImapPassword string ImapFolderInbox string + ImapFolderJunk string ImapFolderArchive string + ImapFolderTrash string ImapFolderDrafts string - ImapFolderJunk string ImapFolderQuarantine string ImapFolderSent string - ImapFolderTrash string +} + +type HermóðrConfig struct { + ImapAddress string + ImapUsername string + ImapPassword string + ImapFolderInbox string + ImapFolderRedirected string + Recipient string + SmtpServer string + SmtpUsername string + PublicKey string +} +type MímirConfig struct { + ImapAddress string + ImapUsername string + ImapPassword string + ImapInboxes []string + ImapFolderSent string +} + +type Config struct { + Týr TýrConfig + Hermóðr HermóðrConfig + Mímir MímirConfig } func readConfig() (Config, error) { @@ -33,8 +58,8 @@ } } defer file.Close() config := Config{} - dirty.LoadStruct(file, &config) - return config, nil + err = dirty.LoadStruct(file, &config) + return config, err } func main() { @@ -54,7 +79,7 @@ switch os.Args[1] { case "hermodr": fallthrough case "hermóðr": - hermodr() + hermodr(config) case "tyr": fallthrough @@ -69,7 +94,7 @@ case "offend": if len(os.Args) == 3 { log.Fatalln("missing token") } - tyr_release(db, config, os.Args[3], "*", config.ImapFolderJunk) + tyr_release(db, config, os.Args[3], "*", config.Týr.ImapFolderJunk) case "release": if len(os.Args) == 3 { log.Fatalln("missing token (and recipient)") @@ -80,14 +105,14 @@ addressTo = "*" } else { addressTo = os.Args[4] } - tyr_release(db, config, os.Args[3], addressTo, config.ImapFolderInbox) + tyr_release(db, config, os.Args[3], addressTo, config.Týr.ImapFolderInbox) } } case "mimir": fallthrough case "mímir": - mimir(db) + mimir(db, config) case "serve": http.HandleFunc("/tyr", tyr_serve) diff --git a/mímir.go b/mímir.go new file mode 100644 index 0000000000000000000000000000000000000000..f88b2f696f4038e4d12fec8db3c365b70bebce12 --- /dev/null +++ b/mímir.go @@ -0,0 +1,118 @@ +package main + +import ( + "bytes" + "fmt" + "io" + "log" + "net/http" + "net/mail" + + "database/sql" + "github.com/emersion/go-imap" + "github.com/emersion/go-imap/client" + "github.com/emersion/go-msgauth/dkim" +) + +func archiveInbox(mboxName string, c *client.Client) error { + mbox, err := c.Select(mboxName, true) + if err != nil { + return err + } + fmt.Printf("reading %d messages from %s\n", mbox.Messages, mboxName) + if mbox.Messages == 0 { + return nil + } + + from := uint32(1) + to := mbox.Messages + seqset := new(imap.SeqSet) + seqset.AddRange(from, to) + + section := &imap.BodySectionName{} + items := []imap.FetchItem{imap.FetchEnvelope, section.FetchItem()} + messages := make(chan *imap.Message, 10) + done := make(chan error, 1) + go func() { + done <- c.Fetch(seqset, items, messages) + }() + for msg := range messages { + // if any recipient ~= git\+[^@]+@apiote.xyz and no recipient ~= git@apiote.xyz + // and if sender ~= git\+[^@]+@apiote.xyz + recipients := append(msg.Envelope.To, msg.Envelope.Cc...) + sender := msg.Envelope.From[0] + messageID := msg.Envelope.MessageId + subject := msg.Envelope.Subject + date := msg.Envelope.Date.UTC() + inReplyTo := msg.Envelope.InReplyTo + dkimStatus := false + r := msg.GetBody(section) + if r == nil { + fmt.Println("here") + // todo return custom error + return err + } + messageBytes, err := io.ReadAll(r) + if err != nil { + panic(err) + } + r = bytes.NewReader(messageBytes) + verifications, err := dkim.Verify(r) + if err != nil { + fmt.Println("there") + return err + } + for _, v := range verifications { + if v.Err == nil && sender.HostName == v.Domain { + dkimStatus = true + } + } + + r = bytes.NewReader(messageBytes) + m, err := mail.ReadMessage(r) + if err != nil { + fmt.Println("tere") + return err + } + body, err := io.ReadAll(m.Body) + if err != nil { + fmt.Println("tee") + return err + } + // select part text/plain from body + + addArchiveEntry(messageID, subject, body, date, inReplyTo, dkimStatus, sender, recipients) + } + + if err := <-done; err != nil { + return err + } + return nil +} + +func mimir(db *sql.DB, config Config) { + c, err := client.DialTLS(config.Mímir.ImapAddress, nil) + if err != nil { + log.Fatalln(err) + } + log.Println("Connected") + defer c.Logout() + if err := c.Login(config.Mímir.ImapUsername, config.Mímir.ImapPassword); err != nil { + log.Fatalln(err) + } + log.Println("Logged in") + for _, inbox := range config.Mímir.ImapInboxes { + err = archiveInbox(inbox, c) + if err != nil { + log.Fatalln(err) + } + } + err = archiveInbox(config.Mímir.ImapFolderSent, c) // todo archive only mail that is reply-to of what is already in db + if err != nil { + log.Fatalln(err) + } +} + +func mimir_serve(w http.ResponseWriter, r *http.Request) { + // todo +} diff --git a/týr.go b/týr.go index 02463fdbb2fe69ebd68e2f60be41bf4ad455e38e..8c6f6fc03d6b79766be2c8f5ad331acc26c9f9ab 100644 --- a/týr.go +++ b/týr.go @@ -94,8 +94,8 @@ done <- c.List("", "*", mailboxes) }() for m := range mailboxes { - if m.Name != config.ImapFolderArchive && m.Name != config.ImapFolderDrafts && m.Name != config.ImapFolderJunk && - m.Name != config.ImapFolderQuarantine && m.Name != config.ImapFolderSent && m.Name != config.ImapFolderTrash { + if m.Name != config.Týr.ImapFolderArchive && m.Name != config.Týr.ImapFolderDrafts && m.Name != config.Týr.ImapFolderJunk && + m.Name != config.Týr.ImapFolderQuarantine && m.Name != config.Týr.ImapFolderSent && m.Name != config.Týr.ImapFolderTrash { inboxes = append(inboxes, m) } } @@ -106,7 +106,7 @@ } return inboxes, nil } -func checkInbox(db *sql.DB, c *client.Client, mbox *imap.MailboxStatus) error { +func checkInbox(db *sql.DB, config Config, c *client.Client, mbox *imap.MailboxStatus) error { rand.Seed(time.Now().UnixNano()) from := uint32(1) to := mbox.Messages @@ -149,7 +149,7 @@ for _, senderRow := range senderRows { if _, present := recipients_[senderRow.addressTo]; present || senderRow.addressTo == "*" { if senderRow.ban { - moveToJunk(c, msg) + moveMsg(c, msg, config.Týr.ImapFolderJunk) log.Printf("%s -> %s is a known offender\n", senderRow.addressFrom, senderRow.addressTo) } else { log.Printf("%s -> %s is a known friend\n", senderRow.addressFrom, senderRow.addressTo) @@ -183,22 +183,22 @@ if err := <-done; err != nil { return err } - return moveToQuarantine(c, moveSet) + return moveMultiple(c, moveSet, config.Týr.ImapFolderQuarantine) } func tyr(db *sql.DB, config Config) { - c, err := client.DialTLS(config.ImapAddress, nil) + c, err := client.DialTLS(config.Týr.ImapAddress, nil) if err != nil { log.Fatalln(err) } log.Println("Connected") defer c.Logout() - if err := c.Login(config.ImapUsername, config.ImapPassword); err != nil { + if err := c.Login(config.Týr.ImapUsername, config.Týr.ImapPassword); err != nil { log.Fatalln(err) } log.Println("Logged in") - mbox, err := c.Select(config.ImapFolderSent, false) + mbox, err := c.Select(config.Týr.ImapFolderSent, false) if err != nil { log.Fatalln(err) } @@ -216,7 +216,7 @@ mbox, err := c.Select(inbox.Name, false) if err != nil { log.Fatalln(err) } - checkInbox(db, c, mbox) + checkInbox(db, config, c, mbox) } } @@ -258,22 +258,22 @@ addressFrom: lock.address, addressTo: addressTo, ban: false, } - if dest == config.ImapFolderJunk { + if dest == config.Týr.ImapFolderJunk { knownAddress.ban = true } err := insertKnownAddress(db, knownAddress) if err != nil { return err } - c, err := client.DialTLS(config.ImapAddress, nil) + c, err := client.DialTLS(config.Týr.ImapAddress, nil) if err != nil { return err } defer c.Logout() - if err := c.Login(config.ImapUsername, config.ImapPassword); err != nil { + if err := c.Login(config.Týr.ImapUsername, config.Týr.ImapPassword); err != nil { log.Fatalln(err) } - mbox, err := c.Select(config.ImapFolderQuarantine, false) + mbox, err := c.Select(config.Týr.ImapFolderQuarantine, false) if err != nil { log.Fatalln(err) } @@ -327,7 +327,7 @@ return } if lock.token != "" && lock.token == formToken { lock.address = formAddress - err = releaseQuarantine(db, config, lock, "*", config.ImapFolderInbox) + err = releaseQuarantine(db, config, lock, "*", config.Týr.ImapFolderInbox) if err != nil { w.WriteHeader(500) w.Write([]byte(err.Error())) @@ -354,7 +354,7 @@ w.Header().Add("Location", "?error=token&address="+formAddress) w.WriteHeader(303) return } - err = releaseQuarantine(db, config, lock, "*", config.ImapFolderInbox) + err = releaseQuarantine(db, config, lock, "*", config.Týr.ImapFolderInbox) if err != nil { w.WriteHeader(500) w.Write([]byte(err.Error()))