asgard.git

commit b0ef56afc7e0ebdf2c28f1112978410a3e5888d3

Author: Adam <git@apiote.xyz>

remove unicode characters from code

 config_example.dirty | 6 +++---
  | 16 ++++++++--------
 main.go | 20 +++++++++++---------
  | 26 +++++++++++++-------------


diff --git a/config_example.dirty b/config_example.dirty
index 46c016fc3d578cd7fc21edf14fa1d1c3d3209072..3a05fcd7768fef9be41845afa5b8046fc155156b 100644
--- a/config_example.dirty
+++ b/config_example.dirty
@@ -1,5 +1,5 @@
 (
-	('týr'
+	('tyr'
 		(
 			('imapAddress' '')
 			('imapUsername' '')
@@ -13,7 +13,7 @@ 			('imapFolderSent' 'Sent')
 			('imapFolderTrash' 'Trash')
 		)
 	)
-	('hermóðr'
+	('hermodr'
 		(
 			('imapAddress' '')
 			('imapUsername' '')
@@ -28,7 +28,7 @@ `
 			)
 		)
 	)
-	('mímir'
+	('mimir'
 		(
 			('imapAddress' '')
 			('imapUsername' '')




diff --git a/hermodr.go b/hermodr.go
new file mode 100644
index 0000000000000000000000000000000000000000..2b070345719cf5c7da2e8cd44b6bd8199eab0ec1
--- /dev/null
+++ b/hermodr.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.Hermodr.ImapAddress)
+	result.Client = c
+	return result, err
+}
+
+func login(args ...interface{}) error {
+	result := args[0].(Result)
+	err := result.Client.Login(result.Config.Hermodr.ImapUsername, result.Config.Hermodr.ImapPassword)
+	return err
+}
+
+func selectInbox(args ...interface{}) (interface{}, error) {
+	result := args[0].(Result)
+	mbox, err := result.Client.Select(result.Config.Hermodr.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.Hermodr.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.Hermodr.Recipient}
+	msg := strings.NewReader("To: " + result.Config.Hermodr.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.Hermodr.SmtpServer, nil, result.Config.Hermodr.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.Hermodr.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/hermóðr.go b/hermóðr.go
deleted file mode 100644
index 9daa468382c9132ab914fb1a154743ccb47b97ea..0000000000000000000000000000000000000000
--- a/hermóðr.go
+++ /dev/null
@@ -1,211 +0,0 @@
-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/main.go b/main.go
index 9088a9409b5a561b0c04f06af8afb341a79dfcfa..cb4c544bef27a02fe40a33c3304616d278e50a5e 100644
--- a/main.go
+++ b/main.go
@@ -8,7 +8,7 @@
 	"apiote.xyz/p/go-dirty"
 )
 
-type TýrConfig struct {
+type TyrConfig struct {
 	ImapAddress          string
 	ImapUsername         string
 	ImapPassword         string
@@ -21,7 +21,7 @@ 	ImapFolderQuarantine string
 	ImapFolderSent       string
 }
 
-type HermóðrConfig struct {
+type HermodrConfig struct {
 	ImapAddress          string
 	ImapUsername         string
 	ImapPassword         string
@@ -32,7 +32,7 @@ 	SmtpServer           string
 	SmtpUsername         string
 	PublicKey            string
 }
-type MímirConfig struct {
+type MimirConfig struct {
 	ImapAddress       string
 	ImapUsername      string
 	ImapPassword      string
@@ -43,12 +43,13 @@ 	ForwardAddress    string
 	PersonalAddress   string
 	SmtpAddress       string
 	SmtpSender        string
+	Companion         string
 }
 
 type Config struct {
-	Týr     TýrConfig
-	Hermóðr HermóðrConfig
-	Mímir   MímirConfig
+	Tyr     TyrConfig
+	Hermodr HermodrConfig
+	Mimir   MimirConfig
 }
 
 func readConfig() (Config, error) {
@@ -99,7 +100,7 @@ 			case "offend":
 				if len(os.Args) == 3 {
 					log.Fatalln("missing token")
 				}
-				tyr_release(db, config, os.Args[3], "*", config.Týr.ImapFolderJunk)
+				tyr_release(db, config, os.Args[3], "*", config.Tyr.ImapFolderJunk)
 			case "release":
 				if len(os.Args) == 3 {
 					log.Fatalln("missing token (and recipient)")
@@ -110,7 +111,7 @@ 					addressTo = "*"
 				} else {
 					addressTo = os.Args[4]
 				}
-				tyr_release(db, config, os.Args[3], addressTo, config.Týr.ImapFolderInbox)
+				tyr_release(db, config, os.Args[3], addressTo, config.Tyr.ImapFolderInbox)
 			}
 		}
 
@@ -121,7 +122,8 @@ 		mimir(db, config)
 
 	case "serve":
 		http.HandleFunc("/tyr", tyr_serve)
-		http.HandleFunc("/mimir", mimir_serve)
+		http.HandleFunc("/mimir", mimir_serve(db))
+		http.HandleFunc("/mimir/", mimir_serve(db))
 		e := http.ListenAndServe(":8081", nil)
 		if e != nil {
 			log.Println(e)




diff --git a/tyr.go b/tyr.go
new file mode 100644
index 0000000000000000000000000000000000000000..0a988c0fabb447063bbb0f4331c2bafdeb6373f0
--- /dev/null
+++ b/tyr.go
@@ -0,0 +1,375 @@
+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.Tyr.ImapFolderArchive && m.Name != config.Tyr.ImapFolderDrafts && m.Name != config.Tyr.ImapFolderJunk &&
+			m.Name != config.Tyr.ImapFolderQuarantine && m.Name != config.Tyr.ImapFolderSent && m.Name != config.Tyr.ImapFolderTrash {
+			inboxes = append(inboxes, m)
+		}
+	}
+
+	if err := <-done; err != nil {
+		return inboxes, err
+	}
+	return inboxes, nil
+}
+
+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
+	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 {
+					moveMsg(c, msg, config.Tyr.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)
+				}
+				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 moveMultiple(c, moveSet, config.Tyr.ImapFolderQuarantine)
+}
+
+func tyr(db *sql.DB, config Config) {
+	c, err := client.DialTLS(config.Tyr.ImapAddress, nil)
+	if err != nil {
+		log.Fatalln(err)
+	}
+	log.Println("Connected")
+	defer c.Logout()
+	if err := c.Login(config.Tyr.ImapUsername, config.Tyr.ImapPassword); err != nil {
+		log.Fatalln(err)
+	}
+	log.Println("Logged in")
+
+	mbox, err := c.Select(config.Tyr.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)
+	}
+inboxLoop:
+	for _, inbox := range inboxes {
+		for _, attribute := range inbox.Attributes {
+			if attribute == "\\Noselect" {
+				continue inboxLoop
+			}
+		}
+		mbox, err := c.Select(inbox.Name, false)
+		if err != nil {
+			log.Fatalln(err)
+		}
+		checkInbox(db, config, 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.Tyr.ImapFolderJunk {
+		knownAddress.ban = true
+	}
+	err := insertKnownAddress(db, knownAddress)
+	if err != nil {
+		return err
+	}
+	c, err := client.DialTLS(config.Tyr.ImapAddress, nil)
+	if err != nil {
+		return err
+	}
+	defer c.Logout()
+	if err := c.Login(config.Tyr.ImapUsername, config.Tyr.ImapPassword); err != nil {
+		log.Fatalln(err)
+	}
+	mbox, err := c.Select(config.Tyr.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.Tyr.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.Tyr.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/týr.go b/týr.go
deleted file mode 100644
index c477f859179074682f13cc6083189dec64580464..0000000000000000000000000000000000000000
--- a/týr.go
+++ /dev/null
@@ -1,375 +0,0 @@
-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.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)
-		}
-	}
-
-	if err := <-done; err != nil {
-		return inboxes, err
-	}
-	return inboxes, nil
-}
-
-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
-	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 {
-					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)
-				}
-				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 moveMultiple(c, moveSet, config.Týr.ImapFolderQuarantine)
-}
-
-func tyr(db *sql.DB, config Config) {
-	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.Týr.ImapUsername, config.Týr.ImapPassword); err != nil {
-		log.Fatalln(err)
-	}
-	log.Println("Logged in")
-
-	mbox, err := c.Select(config.Týr.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)
-	}
-inboxLoop:
-	for _, inbox := range inboxes {
-		for _, attribute := range inbox.Attributes {
-			if attribute == "\\Noselect" {
-				continue inboxLoop
-			}
-		}
-		mbox, err := c.Select(inbox.Name, false)
-		if err != nil {
-			log.Fatalln(err)
-		}
-		checkInbox(db, config, 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.Týr.ImapFolderJunk {
-		knownAddress.ban = true
-	}
-	err := insertKnownAddress(db, knownAddress)
-	if err != nil {
-		return err
-	}
-	c, err := client.DialTLS(config.Týr.ImapAddress, nil)
-	if err != nil {
-		return err
-	}
-	defer c.Logout()
-	if err := c.Login(config.Týr.ImapUsername, config.Týr.ImapPassword); err != nil {
-		log.Fatalln(err)
-	}
-	mbox, err := c.Select(config.Týr.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.Týr.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.Týr.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)
-	}
-}