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() { +}