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