asgard.git

commit 9c25fecfbf5d8e7fecaa4e5298ed3ad5e608f1a7

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