asgard.git

commit 8e2c8ba56e4da15c4b290a4ec2ffd2d7300feae2

Author: Adam <git@apiote.xyz>

týr

 config_example.dirty | 12 +
 db.go | 130 ++++++++++++++++
 go.mod | 26 +++
 go.sum | 105 +++++++++++++
 imap.go | 68 ++++++++
 main.go | 101 ++++++++++++
 templates/tyr.html | 28 +++
 týr.go | 369 ++++++++++++++++++++++++++++++++++++++++++++++
 vor.go | 4 


diff --git a/config_example.dirty b/config_example.dirty
new file mode 100644
index 0000000000000000000000000000000000000000..46b8773f32b45a1b1373935c94c1e7468fb283cb
--- /dev/null
+++ b/config_example.dirty
@@ -0,0 +1,12 @@
+(
+	('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')
+)




diff --git a/db.go b/db.go
new file mode 100644
index 0000000000000000000000000000000000000000..f17d7f2af7137e50e2872c1b2d0f2b1cbee96b55
--- /dev/null
+++ b/db.go
@@ -0,0 +1,130 @@
+package main
+
+import (
+	"database/sql"
+	"fmt"
+	"strings"
+	"time"
+
+	"github.com/emersion/go-imap"
+
+	_ "github.com/mattn/go-sqlite3"
+)
+
+func migrate() (*sql.DB, error) {
+	db, err := open()
+	_, err = db.Exec(`create table tyr_knownAddresses(address_from text, address_to text, ban boolean, unique(address, direction))`)
+	if err != nil && err.Error() != "table tyr_knownAddresses already exists" {
+		return nil, err
+	}
+	_, err = db.Exec(`create table tyr_locks(address text unique, token text, date date)`)
+	if err != nil && err.Error() != "table tyr_locks already exists" {
+		return nil, err
+	}
+	_, err = db.Exec(`create table mimir_archive(message_id text primary key, subject text, body text, date datetime, in_reply_to text, dkim_status bool, sender text, recipients text, foreign key(in_reply_to) references mimir_archive(message_id))`)
+	if err != nil && err.Error() != "table mimir_archive already exists" {
+		return nil, err
+	}
+
+	return db, nil
+}
+
+func open() (*sql.DB, error) {
+	db, err := sql.Open("sqlite3", "asgard.db")
+	if err != nil {
+		return nil, err
+	}
+	return db, nil
+}
+
+func getAddressLock(db *sql.DB, address string) (Lock, error) {
+	address = strings.ToLower(address)
+	lock := Lock{
+		address: address,
+	}
+	row := db.QueryRow(`select token, date from tyr_locks where address = ?`, address)
+	err := row.Scan(&lock.token, &lock.date)
+	if err == sql.ErrNoRows {
+		return Lock{}, nil
+	} else {
+		return lock, err
+	}
+}
+
+func getLock(db *sql.DB, token string) (Lock, error) {
+	lock := Lock{
+		token: token,
+	}
+	row := db.QueryRow(`select address, date from tyr_locks where token = ?`, token)
+	err := row.Scan(&lock.address, &lock.date)
+	if err == sql.ErrNoRows {
+		return Lock{}, nil
+	} else {
+		return lock, err
+	}
+}
+
+func listLocks(db *sql.DB) ([]Lock, error) {
+	locks := []Lock{}
+	rows, err := db.Query(`select address, token from tyr_locks`)
+	if err != nil {
+		return locks, err
+	}
+	for rows.Next() {
+		lock := Lock{}
+		err := rows.Scan(&lock.address, &lock.token)
+		if err != nil {
+			return locks, err
+		}
+		locks = append(locks, lock)
+	}
+	return locks, nil
+}
+
+func insertLock(db *sql.DB, lock Lock) error {
+	_, err := db.Exec(`insert into tyr_locks values(?, ?, ?) on
+	                  conflict(address) do nothing`,
+		lock.address, lock.token, lock.date)
+	return err
+}
+
+func deleteLock(db *sql.DB, lock Lock) error {
+	_, err := db.Exec(`delete from tyr_locks where address = ?`, lock.address)
+	return err
+}
+
+func updateLock(db *sql.DB, lock Lock) error {
+	_, err := db.Exec(`update tyr_locks set date = ? where address = ?`, lock.date, lock.address)
+	return err
+}
+
+func getKnownAddress(db *sql.DB, address string) ([]KnownAddress, error) {
+	knownAddresses := []KnownAddress{}
+
+	rows, err := db.Query(`select address_from, address_to, ban from tyr_knownAddresses where address_from = ?`, address)
+	if err != nil {
+		return []KnownAddress{}, err
+	}
+	for rows.Next() {
+		knownAddress := KnownAddress{}
+		err := rows.Scan(&knownAddress.addressFrom, &knownAddress.addressTo, &knownAddress.ban)
+		if err != nil {
+			return []KnownAddress{}, err
+		}
+		knownAddresses = append(knownAddresses, knownAddress)
+	}
+	return knownAddresses, nil
+}
+
+func insertKnownAddress(db *sql.DB, address KnownAddress) error {
+	_, err := db.Exec(`insert into tyr_knownAddresses values(?, ?, ?) on
+	                  conflict(address_to, address_from) do nothing`,
+		address.addressFrom, address.addressTo, address.ban)
+	return err
+}
+
+func addArchiveEntry(messageID, subject string, body []byte, date time.Time, inReplyTo string, dkim bool, sender *imap.Address, recipients []*imap.Address) {
+	fmt.Printf("adding Archive Entry for %s “%s”, %v, in reply to %s with dkim %v from %s\n", messageID, subject, date, inReplyTo, dkim, sender)
+	fmt.Printf("body: %s\n", body)
+	// todo
+}




diff --git a/go.mod b/go.mod
new file mode 100644
index 0000000000000000000000000000000000000000..728276240c0fb25c59a8a09e6ee3ed8ee9a11591
--- /dev/null
+++ b/go.mod
@@ -0,0 +1,26 @@
+module git.apiote.xyz/me/asgard
+
+go 1.17
+
+require (
+	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
+)
+
+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
+	golang.org/x/sys v0.0.0-20211015200801-69063c4bb744 // indirect
+	golang.org/x/text v0.3.7 // indirect
+)




diff --git a/go.sum b/go.sum
new file mode 100644
index 0000000000000000000000000000000000000000..9735682019ee27b3dfd64d3f637d3a0a28f182f3
--- /dev/null
+++ b/go.sum
@@ -0,0 +1,105 @@
+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=
+github.com/ProtonMail/go-mime v0.0.0-20190923161245-9b5a4261663a h1:W6RrgN/sTxg1msqzFFb+G80MFmpjMw61IU+slm+wln4=
+github.com/ProtonMail/go-mime v0.0.0-20190923161245-9b5a4261663a/go.mod h1:NYt+V3/4rEeDuaev/zw1zCq8uqVEuPHzDPo3OZrlGJ4=
+github.com/ProtonMail/gopenpgp/v2 v2.2.4 h1:PEke+LAMLH9CplflEl8WqGyz2IiDoiiipKkB+3cEWFQ=
+github.com/ProtonMail/gopenpgp/v2 v2.2.4/go.mod h1:ygdaHbrbWFPhKjmXii0zOs3/xlSR/01GaVePKqv19Hc=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/emersion/go-imap v1.2.0 h1:lyUQ3+EVM21/qbWE/4Ya5UG9r5+usDxlg4yfp3TgHFA=
+github.com/emersion/go-imap v1.2.0/go.mod h1:Qlx1FSx2FTxjnjWpIlVNEuX+ylerZQNFE5NsmKFSejY=
+github.com/emersion/go-imap-move v0.0.0-20210907172020-fe4558f9c872 h1:HGBfonz0q/zq7y3ew+4oy4emHSvk6bkmV0mdDG3E77M=
+github.com/emersion/go-imap-move v0.0.0-20210907172020-fe4558f9c872/go.mod h1:QuMaZcKFDVI0yCrnAbPLfbwllz1wtOrZH8/vZ5yzp4w=
+github.com/emersion/go-message v0.11.2/go.mod h1:C4jnca5HOTo4bGN9YdqNQM9sITuT3Y0K6bSUw9RklvY=
+github.com/emersion/go-message v0.14.1/go.mod h1:N1JWdZQ2WRUalmdHAX308CWBq747VJ8oUorFI3VCBwU=
+github.com/emersion/go-message v0.15.0 h1:urgKGqt2JAc9NFJcgncQcohHdiYb803YTH9OQwHBHIY=
+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=
+github.com/emersion/go-smtp v0.15.0 h1:3+hMGMGrqP/lqd7qoxZc1hTU8LY8gHV9RFGWlqSDmP8=
+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=
+github.com/mattn/go-sqlite3 v1.14.9/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
+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=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
+github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
+github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
+github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
+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=
+golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56/go.mod h1:JhuoJpWY28nO4Vef9tZUw9qufEGTyX1+7lmHxV5q5G4=
+golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
+golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
+golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
+golang.org/x/mobile v0.0.0-20200801112145-973feb4309de/go.mod h1:skQtrUTUwhdJvXM/2KKJzY8pDgNr9I/FOMqDVRPBUS4=
+golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
+golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
+golang.org/x/mod v0.1.1-0.20191209134235-331c550502dd/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
+golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+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-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=
+golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
+golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.5-0.20201125200606-c27b9fd57aec/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
+golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20200117012304-6edc0a871e69/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+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/imap.go b/imap.go
new file mode 100644
index 0000000000000000000000000000000000000000..c07f4189fce56145992da3292ecdf97404ece8ba
--- /dev/null
+++ b/imap.go
@@ -0,0 +1,68 @@
+package main
+
+import (
+	"log"
+
+	"github.com/emersion/go-imap"
+	"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())
+	moveClient := move.NewClient(c)
+	seqSet := new(imap.SeqSet)
+	seqSet.AddNum(msg.Uid)
+	moveClient.UidMoveWithFallback(seqSet, "Junk")
+}
+
+func moveToQuarantine(c *client.Client, seqSet *imap.SeqSet) error {
+	moveClient := move.NewClient(c)
+	if !seqSet.Empty() {
+		log.Println("moving multiple to quarantine")
+		err := moveClient.UidMoveWithFallback(seqSet, "Quarantine")
+		return err
+	}
+	return nil
+}
+
+func moveFromQuarantine(c *client.Client, mbox *imap.MailboxStatus, address, dest string) error {
+	log.Printf("moving %s from quarantine to %s\n", address, dest)
+	moveClient := move.NewClient(c)
+	from := uint32(1)
+	to := mbox.Messages
+	allMessagesSet := new(imap.SeqSet)
+	allMessagesSet.AddRange(from, to)
+	var err error = nil
+
+	messages := make(chan *imap.Message, 10)
+	done := make(chan error, 1)
+	go func() {
+		done <- c.Fetch(allMessagesSet, []imap.FetchItem{imap.FetchEnvelope, imap.FetchUid}, messages)
+	}()
+
+	moveSet := new(imap.SeqSet)
+	for msg := range messages {
+		sender := msg.Envelope.From[0]
+		if sender.Address() == address {
+			moveSet.AddNum(msg.Uid)
+		}
+	}
+	if !moveSet.Empty() {
+		err = moveClient.UidMoveWithFallback(moveSet, dest)
+	}
+	if err := <-done; err != nil {
+		return err
+	}
+	return err
+}
+
+func sendRepeatedQuarantine(address *imap.Address, lock Lock) {
+	// todo
+	log.Printf("sending repeated quarantine to %s from %+v\n", address.Address(), lock)
+}
+
+func sendQuarantine(address *imap.Address) {
+	// todo
+	log.Printf("sending quarantine to %s\n", address.Address())
+}




diff --git a/main.go b/main.go
new file mode 100644
index 0000000000000000000000000000000000000000..c6745665b6b14a8d5b012f7966108a229167340b
--- /dev/null
+++ b/main.go
@@ -0,0 +1,101 @@
+package main
+
+import (
+	"log"
+	"net/http"
+	"os"
+
+	"notabug.org/apiote/go-dirty"
+)
+
+type Config struct {
+	ImapAddress          string
+	ImapUsername         string
+	ImapPassword         string
+	ImapFolderInbox      string
+	ImapFolderArchive    string
+	ImapFolderDrafts     string
+	ImapFolderJunk       string
+	ImapFolderQuarantine string
+	ImapFolderSent       string
+	ImapFolderTrash      string
+}
+
+func readConfig() (Config, error) {
+	file, err := os.Open("config.dirty")
+	if err != nil {
+		if os.IsNotExist(err) {
+			return Config{}, err
+		} else {
+			log.Printf("error opening configuration %v\n", err)
+			return Config{}, err
+		}
+	}
+	defer file.Close()
+	config := Config{}
+	dirty.LoadStruct(file, &config)
+	return config, nil
+}
+
+func main() {
+	config, err := readConfig()
+	if err != nil {
+		log.Fatalln(err)
+	}
+	//log.Printf("running with conifig\n%+v\n", config)
+	db, err := migrate()
+	if err != nil {
+		log.Fatalln(err)
+	}
+	defer db.Close()
+	log.Println("Migrated")
+
+	switch os.Args[1] {
+	case "hermodr":
+		fallthrough
+	case "hermóðr":
+		hermodr()
+
+	case "tyr":
+		fallthrough
+	case "týr":
+		if len(os.Args) == 2 {
+			tyr(db, config)
+		} else {
+			switch os.Args[2] {
+			case "list":
+				tyr_lists_locks(db)
+			case "offend":
+				if len(os.Args) == 3 {
+					log.Fatalln("missing token")
+				}
+				tyr_release(db, config, os.Args[3], "*", config.ImapFolderJunk)
+			case "release":
+				if len(os.Args) == 3 {
+					log.Fatalln("missing token (and recipient)")
+				}
+				addressTo := ""
+				if len(os.Args) == 4 {
+					addressTo = "*"
+				} else {
+					addressTo = os.Args[4]
+				}
+				tyr_release(db, config, os.Args[3], addressTo, config.ImapFolderInbox)
+			}
+		}
+
+	case "mimir":
+		fallthrough
+	case "mímir":
+		mimir(db)
+
+	case "serve":
+		http.HandleFunc("/tyr", tyr_serve)
+		http.HandleFunc("/mimir", mimir_serve)
+		e := http.ListenAndServe(":8081", nil)
+		if e != nil {
+			log.Println(e)
+		}
+	}
+
+}




diff --git a/templates/tyr.html b/templates/tyr.html
new file mode 100644
index 0000000000000000000000000000000000000000..076b0b168a32289deb394e91d2218e015d389e40
--- /dev/null
+++ b/templates/tyr.html
@@ -0,0 +1,28 @@
+<!DOCTYPE html>
+<html>
+	<head>
+		<meta charset="utf-8" />
+		<title>Týr</title>
+	</head>
+	<body>
+		{{ if eq .Error `address` }}
+		<p>Address ‘{{.Address}}’ not found in locks</p>
+		{{ else if eq .Error `token` }}
+		<p>Token invalid for address ‘{{.Address}}’ </p>
+		{{ else if eq .Error `captcha` }}
+		<p>Wrong solution for captcha</p>
+		{{ else if eq .Error `success` }}
+		<p>Your messages have been release from quarantine.<br/>
+		Mail from You will now go straight to Inbox.</p>
+		{{ else }}
+		<form method="POST">
+			<input name="address" value="{{.Address}}" /><br/>
+			<input name="token" value="{{.Token}}" /><br/>
+			<input type="hidden" name="captcha_result" value="{{.Captcha}}" /><br/>
+			<label for="captcha">Type ‘quarter to three p.m.’ in format HH:MM</label><br/>
+			<input id="captcha" name="captcha" type="time" /><br/>
+			<input type="submit" />
+		</form>
+		{{ end }}
+	</body>
+</html>




diff --git a/týr.go b/týr.go
new file mode 100644
index 0000000000000000000000000000000000000000..02463fdbb2fe69ebd68e2f60be41bf4ad455e38e
--- /dev/null
+++ b/týr.go
@@ -0,0 +1,369 @@
+package main
+
+import (
+	"bytes"
+	"crypto/sha256"
+	"database/sql"
+	"fmt"
+	"html/template"
+	"io"
+	"log"
+	"math/rand"
+	"net/http"
+	"strconv"
+	"strings"
+	"time"
+
+	"github.com/emersion/go-imap"
+	"github.com/emersion/go-imap/client"
+)
+
+type KnownAddress struct {
+	addressFrom string
+	addressTo   string
+	ban         bool
+}
+
+func (a KnownAddress) empty() bool {
+	return a.addressFrom == "" && a.addressTo == ""
+}
+
+type Lock struct {
+	address string
+	token   string
+	date    time.Time
+}
+
+func NewLock(address string) Lock {
+	token := strconv.FormatUint(rand.Uint64(), 16)
+	lock := Lock{
+		address: address,
+		token:   token,
+		date:    time.Now(),
+	}
+	return lock
+}
+
+func (l Lock) empty() bool {
+	return l.address == "" && l.token == "" && l.date.IsZero()
+}
+
+/* ASGARD */
+
+func addSentTo(db *sql.DB, c *client.Client, mbox *imap.MailboxStatus) error {
+	// todo also release from Quarantine
+	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)
+	go func() {
+		done <- c.Fetch(seqset, []imap.FetchItem{imap.FetchEnvelope}, messages)
+	}()
+
+	for msg := range messages {
+		recipients := append(msg.Envelope.To, msg.Envelope.Cc...)
+		for _, recipient := range recipients {
+			knownAddress := KnownAddress{
+				addressFrom: recipient.Address(),
+				addressTo:   "*",
+				ban:         false,
+			}
+			err := insertKnownAddress(db, knownAddress)
+			if err != nil {
+				log.Println(err)
+				continue
+			}
+		}
+	}
+
+	if err := <-done; err != nil {
+		return err
+	}
+	return nil
+}
+
+func listInboxes(c *client.Client, config Config) ([]*imap.MailboxInfo, error) {
+	mailboxes := make(chan *imap.MailboxInfo, 10)
+	inboxes := []*imap.MailboxInfo{}
+	done := make(chan error, 1)
+	go func() {
+		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 {
+			inboxes = append(inboxes, m)
+		}
+	}
+
+	if err := <-done; err != nil {
+		return inboxes, err
+	}
+	return inboxes, nil
+}
+
+func checkInbox(db *sql.DB, c *client.Client, mbox *imap.MailboxStatus) error {
+	rand.Seed(time.Now().UnixNano())
+	from := uint32(1)
+	to := mbox.Messages
+	seqset := new(imap.SeqSet)
+	moveSet := new(imap.SeqSet)
+	seqset.AddRange(from, to)
+
+	messages := make(chan *imap.Message, 10)
+	done := make(chan error, 1)
+	go func() {
+		done <- c.Fetch(seqset, []imap.FetchItem{imap.FetchEnvelope, imap.FetchFlags, imap.FetchUid}, messages)
+	}()
+
+messagesLoop:
+	for msg := range messages {
+		for _, flag := range msg.Flags {
+			if flag == imap.FlaggedFlag {
+				log.Printf("Ignoring %s as flagged\n", msg.Envelope.Subject)
+				continue messagesLoop
+			}
+		}
+		recipients := append(msg.Envelope.To, msg.Envelope.Cc...)
+		recipients_ := map[string]struct{}{}
+		for _, recipient := range recipients {
+			recipients_[recipient.Address()] = struct{}{}
+		}
+		sender := msg.Envelope.From[0]
+
+		senderRows, err := getKnownAddress(db, sender.Address())
+		if err != nil {
+			log.Println(err)
+			continue
+		}
+		lock, err := getAddressLock(db, sender.Address())
+		if err != nil {
+			log.Println(err)
+			continue
+		}
+
+		for _, senderRow := range senderRows {
+			if _, present := recipients_[senderRow.addressTo]; present || senderRow.addressTo == "*" {
+				if senderRow.ban {
+					moveToJunk(c, msg)
+					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)
+				}
+				continue messagesLoop
+			}
+		}
+
+		if !lock.empty() {
+			now := time.Now()
+			weekBefore := now.AddDate(0, 0, -7)
+			if lock.date.Before(weekBefore) {
+				sendRepeatedQuarantine(sender, lock)
+				lock.date = time.Now()
+				updateLock(db, lock)
+			} else {
+				log.Printf("lock %+v is still valid\n", lock)
+			}
+			log.Printf("moving repeated %v : %s from %s to quarantine\n", msg.Envelope.Date, msg.Envelope.Subject, msg.Envelope.From[0].Address())
+			moveSet.AddNum(msg.Uid)
+			continue
+		}
+
+		sendQuarantine(sender)
+		lock = NewLock(sender.Address())
+		insertLock(db, lock)
+		log.Printf("moving %v : %s from %s to quarantine\n", msg.Envelope.Date, msg.Envelope.Subject, msg.Envelope.From[0].Address())
+		moveSet.AddNum(msg.Uid)
+	}
+
+	if err := <-done; err != nil {
+		return err
+	}
+	return moveToQuarantine(c, moveSet)
+}
+
+func tyr(db *sql.DB, config Config) {
+	c, err := client.DialTLS(config.ImapAddress, nil)
+	if err != nil {
+		log.Fatalln(err)
+	}
+	log.Println("Connected")
+	defer c.Logout()
+	if err := c.Login(config.ImapUsername, config.ImapPassword); err != nil {
+		log.Fatalln(err)
+	}
+	log.Println("Logged in")
+
+	mbox, err := c.Select(config.ImapFolderSent, false)
+	if err != nil {
+		log.Fatalln(err)
+	}
+	err = addSentTo(db, c, mbox)
+	if err != nil {
+		log.Fatalln(err)
+	}
+
+	inboxes, err := listInboxes(c, config)
+	if err != nil {
+		log.Fatalln(err)
+	}
+	for _, inbox := range inboxes {
+		mbox, err := c.Select(inbox.Name, false)
+		if err != nil {
+			log.Fatalln(err)
+		}
+		checkInbox(db, c, mbox)
+	}
+}
+
+func tyr_lists_locks(db *sql.DB) {
+	locks, err := listLocks(db)
+	if err != nil {
+		log.Fatalln(err)
+	}
+	if len(locks) == 0 {
+		fmt.Println("no locks")
+	}
+	for _, lock := range locks {
+		fmt.Printf("%s: %s\n", lock.token, lock.address)
+	}
+}
+
+func tyr_release(db *sql.DB, config Config, token, addressTo, dest string) {
+	lock, err := getLock(db, token)
+	if err != nil {
+		log.Fatalln(err)
+	}
+	err = releaseQuarantine(db, config, lock, addressTo, dest)
+	if err != nil {
+		log.Fatalln(err)
+	}
+}
+
+type TyrData struct {
+	Address string
+	Token   string
+	Captcha string
+	Error   string
+}
+
+func releaseQuarantine(db *sql.DB, config Config, lock Lock, addressTo, dest string) error {
+	deleteLock(db, lock)
+	knownAddress := KnownAddress{
+		addressFrom: lock.address,
+		addressTo:   addressTo,
+		ban:         false,
+	}
+	if dest == config.ImapFolderJunk {
+		knownAddress.ban = true
+	}
+	err := insertKnownAddress(db, knownAddress)
+	if err != nil {
+		return err
+	}
+	c, err := client.DialTLS(config.ImapAddress, nil)
+	if err != nil {
+		return err
+	}
+	defer c.Logout()
+	if err := c.Login(config.ImapUsername, config.ImapPassword); err != nil {
+		log.Fatalln(err)
+	}
+	mbox, err := c.Select(config.ImapFolderQuarantine, false)
+	if err != nil {
+		log.Fatalln(err)
+	}
+	moveFromQuarantine(c, mbox, lock.address, dest)
+	return nil
+}
+
+func tyr_serve(w http.ResponseWriter, r *http.Request) {
+	config, err := readConfig() // fixme shouldn’t read config every time
+	if err != nil {
+		log.Fatalln(err)
+	}
+	r.ParseForm()
+	formAddress := r.Form.Get("address")
+	formToken := r.Form.Get("token")
+	formError := r.Form.Get("error")
+	if r.Method == "GET" {
+		tyrData := TyrData{
+			Address: formAddress,
+			Token:   formToken,
+			Captcha: "$696a04444feea781aeca9c546e220e0981aff4a8db0b2998decdf13265a95c31", // todo with salt and randomised time
+			Error:   formError,
+		}
+		t, _ := template.ParseFiles("templates/tyr.html")
+		b := bytes.NewBuffer([]byte{})
+		_ = t.Execute(b, tyrData)
+		io.Copy(w, b)
+	} else if r.Method == "POST" {
+		formCaptcha := r.Form.Get("captcha")
+		captchaResult := strings.Split(r.Form.Get("captcha_result"), "$")
+		shaCaptcha := sha256.Sum256([]byte(formCaptcha + captchaResult[0]))
+		hexCaptcha := fmt.Sprintf("%x", shaCaptcha)
+		if hexCaptcha != captchaResult[1] {
+			w.Header().Add("Location", "?error=captcha")
+			w.WriteHeader(303)
+			return
+		}
+
+		db, err := open()
+		if err != nil {
+			w.WriteHeader(500)
+			w.Write([]byte(err.Error()))
+			return
+		}
+		defer db.Close()
+		lock, err := getAddressLock(db, "*")
+		if err != nil {
+			w.WriteHeader(500)
+			w.Write([]byte(err.Error()))
+			return
+		}
+		if lock.token != "" && lock.token == formToken {
+			lock.address = formAddress
+			err = releaseQuarantine(db, config, lock, "*", config.ImapFolderInbox)
+			if err != nil {
+				w.WriteHeader(500)
+				w.Write([]byte(err.Error()))
+			} else {
+				w.Header().Add("Location", "?error=success")
+				w.WriteHeader(303)
+			}
+			return
+		}
+
+		lock, err = getAddressLock(db, formAddress)
+		if err != nil {
+			w.WriteHeader(500)
+			w.Write([]byte(err.Error()))
+			return
+		}
+		if lock.token == "" {
+			w.Header().Add("Location", "?error=address&address="+formAddress)
+			w.WriteHeader(303)
+			return
+		}
+		if lock.token != formToken {
+			w.Header().Add("Location", "?error=token&address="+formAddress)
+			w.WriteHeader(303)
+			return
+		}
+		err = releaseQuarantine(db, config, lock, "*", config.ImapFolderInbox)
+		if err != nil {
+			w.WriteHeader(500)
+			w.Write([]byte(err.Error()))
+		} else {
+			w.Header().Add("Location", "?error=success")
+			w.WriteHeader(303)
+		}
+		return
+	} else {
+		w.WriteHeader(405)
+	}
+}




diff --git a/vor.go b/vor.go
new file mode 100644
index 0000000000000000000000000000000000000000..8ccb8f0d76044d30c1a2c4a852d5adc7c1b3250f
--- /dev/null
+++ b/vor.go
@@ -0,0 +1,4 @@
+package main
+
+func vor() {
+}