├── screenshots ├── create.png ├── edit.png ├── home.png ├── list.png ├── sample.png └── view.png ├── .gitignore ├── mailer.rb ├── dispatcher.go ├── web.go ├── memstore.go ├── assets.go ├── envelope.go ├── server.go ├── LICENSE ├── filter.go ├── main.go ├── rule.go ├── store.go ├── handlers.go ├── README.md └── views.go /screenshots/create.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ashaniray/mailhook/HEAD/screenshots/create.png -------------------------------------------------------------------------------- /screenshots/edit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ashaniray/mailhook/HEAD/screenshots/edit.png -------------------------------------------------------------------------------- /screenshots/home.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ashaniray/mailhook/HEAD/screenshots/home.png -------------------------------------------------------------------------------- /screenshots/list.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ashaniray/mailhook/HEAD/screenshots/list.png -------------------------------------------------------------------------------- /screenshots/sample.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ashaniray/mailhook/HEAD/screenshots/sample.png -------------------------------------------------------------------------------- /screenshots/view.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ashaniray/mailhook/HEAD/screenshots/view.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | *.test 24 | *.prof 25 | 26 | mailhook 27 | mailhook.db 28 | assets/ 29 | -------------------------------------------------------------------------------- /mailer.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'net/smtp' 4 | 5 | msgstr = < 7 | To: Destination Address 8 | Subject: test message 9 | Date: Sat, 23 Jun 2001 16:26:43 +0900 10 | Message-Id: 11 | 12 | This is a test message. 13 | END_OF_MESSAGE 14 | 15 | require 'net/smtp' 16 | Net::SMTP.start('localhost', 2025) do |smtp| 17 | smtp.send_message msgstr, 'bob@example.com', 'alice@example.com' 18 | end 19 | -------------------------------------------------------------------------------- /dispatcher.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | "strings" 7 | ) 8 | 9 | func dispatchPayload(ep string, payload string) { 10 | resp, err := http.Post(ep, "application/json", strings.NewReader(payload)) 11 | if err != nil { 12 | log.Println(err) 13 | return 14 | } 15 | 16 | log.Println(resp) 17 | } 18 | 19 | func StartDispatcher(dispIn chan Packet) { 20 | log.Println("starting dispatcher") 21 | 22 | for { 23 | packet := <-dispIn 24 | payload := MailStore.Get(packet.Key) 25 | 26 | log.Println("dispatching: message", packet.Key, "to", packet.Endpoints) 27 | 28 | for _, ep := range packet.Endpoints { 29 | go dispatchPayload(ep, payload) 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /web.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "net/http" 7 | ) 8 | 9 | func StartWebInterface(host string, port int) { 10 | addr := fmt.Sprintf("%s:%d", host, port) 11 | log.Println("starting admin web interface on", addr) 12 | 13 | http.HandleFunc("/assets/", AssetHandler) 14 | http.HandleFunc("/", AdminHandler) 15 | http.HandleFunc("/new/", NewRuleHandler) 16 | http.HandleFunc("/create/", CreateRuleHandler) 17 | http.HandleFunc("/view/", ViewRuleHandler) 18 | http.HandleFunc("/edit/", EditRuleHandler) 19 | http.HandleFunc("/update/", UpdateRuleHandler) 20 | http.HandleFunc("/delete/", DeleteRuleHandler) 21 | 22 | err := http.ListenAndServe(addr, nil) 23 | 24 | if err != nil { 25 | log.Println("ERROR:", err) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /memstore.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "crypto/md5" 5 | "fmt" 6 | "sync" 7 | ) 8 | 9 | type MemStore struct { 10 | Storage map[string]string 11 | Guard sync.Mutex 12 | } 13 | 14 | func NewMemStore() *MemStore { 15 | s := new(MemStore) 16 | s.Storage = make(map[string]string) 17 | return s 18 | } 19 | 20 | func (s *MemStore) Save(content string) string { 21 | s.Guard.Lock() 22 | defer s.Guard.Unlock() 23 | 24 | key := checksum(content) 25 | s.Storage[key] = content 26 | 27 | return key 28 | } 29 | 30 | func (s *MemStore) Get(key string) string { 31 | data, ok := s.Storage[key] 32 | 33 | if ok { 34 | return data 35 | } else { 36 | return "" 37 | } 38 | } 39 | 40 | func checksum(data string) string { 41 | return fmt.Sprintf("%x", md5.Sum([]byte(data))) 42 | } 43 | -------------------------------------------------------------------------------- /assets.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | "path" 7 | ) 8 | 9 | var assetMap = map[string]string{ 10 | ".css": "text/css", 11 | ".js": "application/javascript", 12 | ".woff2": "font/opentype", 13 | ".otf": "font/opentype", 14 | ".svg": "image/svg+xml", 15 | ".woff": "font/opentype", 16 | ".eot": "font/opentype", 17 | ".ttf": "font/opentype", 18 | } 19 | 20 | func AssetHandler(w http.ResponseWriter, r *http.Request) { 21 | asset := r.URL.Path[1:] 22 | ext := path.Ext(r.URL.Path) 23 | 24 | data, err := Asset(asset) 25 | if err != nil { 26 | log.Println("ERROR:", err) 27 | return 28 | } 29 | 30 | mt, ok := assetMap[ext] 31 | 32 | if ok { 33 | w.Header().Set("Content-Type", mt) 34 | } else { 35 | w.Header().Set("Content-Type", "text/plain") 36 | } 37 | 38 | w.Write(data) 39 | } 40 | -------------------------------------------------------------------------------- /envelope.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "github.com/bradfitz/go-smtpd/smtpd" 6 | "log" 7 | "strings" 8 | ) 9 | 10 | type Message struct { 11 | From string 12 | To []string 13 | Body string 14 | } 15 | 16 | type Envelope struct { 17 | *smtpd.BasicEnvelope 18 | msg Message 19 | } 20 | 21 | func (e *Envelope) AddRecipient(rcpt smtpd.MailAddress) error { 22 | e.msg.To = append(e.msg.To, rcpt.Email()) 23 | return e.BasicEnvelope.AddRecipient(rcpt) 24 | } 25 | 26 | func (e *Envelope) Write(line []byte) error { 27 | str := strings.Replace(string(line), "\n", " ", -1) 28 | str = strings.Replace(str, "\r", " ", -1) 29 | e.msg.Body += str 30 | return nil 31 | } 32 | 33 | func (e *Envelope) Close() error { 34 | messageInJson, err := json.Marshal(e.msg) 35 | if err != nil { 36 | log.Printf("Error occured while converting to json", err) 37 | return err 38 | } 39 | key := MailStore.Save(string(messageInJson)) 40 | log.Printf("The message received : %q", string(messageInJson)) 41 | globalOut <- key 42 | return nil 43 | } 44 | -------------------------------------------------------------------------------- /server.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/bradfitz/go-smtpd/smtpd" 5 | "log" 6 | "strconv" 7 | ) 8 | 9 | var myMessage Message 10 | 11 | var globalOut = make(chan string) 12 | 13 | func StartSMTPEndpoint(host string, port int) chan string { 14 | log.Printf("starting SMTP endpoint on %s:%d", host, port) 15 | smtpOut := make(chan string) 16 | 17 | go func() { 18 | for { 19 | out := <-globalOut 20 | smtpOut <- out 21 | } 22 | }() 23 | 24 | go func() { 25 | s := &smtpd.Server{ 26 | Addr: ":" + strconv.Itoa(port), 27 | Hostname: host, 28 | OnNewMail: onNewMail, 29 | } 30 | 31 | // entry point to the SMTP endpoint 32 | err := s.ListenAndServe() 33 | if err != nil { 34 | log.Fatalf("ListenAndServe: %v", err) 35 | } 36 | 37 | }() 38 | 39 | return smtpOut 40 | } 41 | 42 | func onNewMail(c smtpd.Connection, from smtpd.MailAddress) (smtpd.Envelope, error) { 43 | log.Printf("New mail received from %q", from) 44 | myMessage.From = from.Email() 45 | return &Envelope{new(smtpd.BasicEnvelope), myMessage}, nil 46 | } 47 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Ashani Ray 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /filter.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "log" 6 | ) 7 | 8 | type Packet struct { 9 | Key string 10 | Endpoints []string 11 | } 12 | 13 | func NewPacket(key string, eps []string) *Packet { 14 | return &Packet{Key: key, Endpoints: eps} 15 | } 16 | 17 | func lookupMessage(key string) *Message { 18 | mail := MailStore.Get(key) 19 | message := new(Message) 20 | json.Unmarshal([]byte(mail), message) 21 | return message 22 | 23 | } 24 | 25 | func applyRule(rule *Rule, key string, fout chan Packet) { 26 | msg := lookupMessage(key) 27 | if rule.Evaluate(msg) { 28 | fout <- *NewPacket(key, rule.Endpoints) 29 | } else { 30 | log.Println("Rule evaluated to false for", rule.Title, "ignoring", key) 31 | } 32 | } 33 | 34 | func applyRules(key string, fout chan Packet) { 35 | rules, err := DiskStore.GetAllRules() 36 | 37 | if err != nil { 38 | log.Println("ERROR:", err) 39 | return 40 | } 41 | 42 | for _, rule := range rules { 43 | go applyRule(rule, key, fout) 44 | } 45 | 46 | } 47 | 48 | func StartFilter(in chan string) chan Packet { 49 | filterOut := make(chan Packet) 50 | log.Println("starting filter ...") 51 | 52 | go func() { 53 | for { 54 | key := <-in 55 | log.Println("FILTER:", key) 56 | applyRules(key, filterOut) 57 | } 58 | }() 59 | 60 | return filterOut 61 | } 62 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "log" 6 | ) 7 | 8 | var ( 9 | smtpHost = flag.String("s", "0.0.0.0", "smtp server bind address.") 10 | smtpPort = flag.Int("p", 25, "smpt server port.") 11 | dbFile = flag.String("d", "mailhook.db", "specify rules database file.") 12 | adminHost = flag.String("a", "0.0.0.0", "web server bind address.") 13 | adminPort = flag.Int("q", 8080, "webserver port.") 14 | ) 15 | 16 | var MailStore = NewMemStore() 17 | 18 | func DummyRules() { 19 | r1 := NewRule("rule1", defaultJs, []string{"http://ep1.com", "http://epr2.com"}) 20 | r2 := NewRule("rule2", defaultJs, []string{"http://ep1.com", "http://epr2.com"}) 21 | r3 := NewRule("rule3", defaultJs, []string{"http://ep1.com", "http://epr2.com"}) 22 | r4 := NewRule("rule4", defaultJs, []string{"http://ep1.com", "http://epr2.com"}) 23 | 24 | DiskStore.SaveRule(r1) 25 | DiskStore.SaveRule(r2) 26 | DiskStore.SaveRule(r3) 27 | DiskStore.SaveRule(r4) 28 | 29 | rxs, _ := DiskStore.GetAllRules() 30 | 31 | for _, rx := range rxs { 32 | log.Println("RULE:", rx) 33 | } 34 | 35 | } 36 | 37 | func main() { 38 | flag.Parse() 39 | 40 | var err error 41 | DiskStore, err = NewStore(*dbFile) 42 | defer DiskStore.Close() 43 | 44 | if err != nil { 45 | log.Println("ERROR:", err) 46 | return 47 | } 48 | 49 | smtpOut := StartSMTPEndpoint(*smtpHost, *smtpPort) 50 | filterOut := StartFilter(smtpOut) 51 | go StartDispatcher(filterOut) 52 | 53 | StartWebInterface(*adminHost, *adminPort) 54 | } 55 | -------------------------------------------------------------------------------- /rule.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "encoding/gob" 6 | "github.com/pborman/uuid" 7 | "github.com/robertkrimen/otto" 8 | "log" 9 | ) 10 | 11 | type Rule struct { 12 | Id string 13 | Title string 14 | Script string 15 | Endpoints []string 16 | } 17 | 18 | func NewRule(title string, src string, eps []string) *Rule { 19 | return &Rule{ 20 | Id: uuid.NewUUID().String(), 21 | Title: title, 22 | Script: src, 23 | Endpoints: eps} 24 | } 25 | 26 | func NewRuleFromBytes(b []byte) (*Rule, error) { 27 | buf := bytes.NewBuffer(b) 28 | var r Rule 29 | dec := gob.NewDecoder(buf) 30 | err := dec.Decode(&r) 31 | return &r, err 32 | } 33 | 34 | func RuleBucket() []byte { 35 | return []byte("rules") 36 | } 37 | 38 | func (r *Rule) Bucket() []byte { 39 | return RuleBucket() 40 | } 41 | 42 | func (r *Rule) ToGob() []byte { 43 | var buff bytes.Buffer 44 | enc := gob.NewEncoder(&buff) 45 | 46 | err := enc.Encode(r) 47 | 48 | if err != nil { 49 | return nil 50 | } 51 | 52 | return buff.Bytes() 53 | } 54 | 55 | func (r *Rule) Evaluate(payload *Message) bool { 56 | result, err := evaluateRule(r.Script, payload) 57 | if err != nil { 58 | log.Println("ERROR:", err) 59 | return false 60 | } 61 | 62 | return result 63 | } 64 | 65 | func evaluateRule(src string, payload *Message) (bool, error) { 66 | js := otto.New() 67 | var ruleFunc otto.Value 68 | js.Set("rule", func(call otto.FunctionCall) otto.Value { 69 | ruleFunc = call.Argument(0) 70 | return otto.UndefinedValue() 71 | }) 72 | 73 | js.Run(src) 74 | arg, err := js.ToValue(payload) 75 | 76 | if err != nil { 77 | return false, err 78 | } 79 | 80 | ret, err := ruleFunc.Call(otto.NullValue(), arg) 81 | 82 | if err != nil { 83 | return false, err 84 | } 85 | 86 | return ret.ToBoolean() 87 | } 88 | -------------------------------------------------------------------------------- /store.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "github.com/boltdb/bolt" 6 | ) 7 | 8 | type Store struct { 9 | DB *bolt.DB 10 | } 11 | 12 | var DiskStore *Store 13 | 14 | func NewStore(name string) (*Store, error) { 15 | db, err := bolt.Open(name, 0600, nil) 16 | 17 | if err != nil { 18 | return nil, err 19 | } 20 | 21 | return &Store{DB: db}, nil 22 | } 23 | 24 | func (s *Store) Close() { 25 | s.DB.Close() 26 | } 27 | 28 | func (s *Store) SaveRule(r *Rule) error { 29 | err := s.DB.Update(func(tx *bolt.Tx) error { 30 | 31 | bucket, err := tx.CreateBucketIfNotExists(r.Bucket()) 32 | if err != nil { 33 | return err 34 | } 35 | 36 | err = bucket.Put([]byte(r.Id), r.ToGob()) 37 | 38 | if err != nil { 39 | return err 40 | } 41 | return nil 42 | }) 43 | 44 | return err 45 | } 46 | 47 | func (s *Store) GetRule(id string) (*Rule, error) { 48 | ret := make([]byte, 0) 49 | err := s.DB.View(func(tx *bolt.Tx) error { 50 | bucket := tx.Bucket(RuleBucket()) 51 | if bucket == nil { 52 | return errors.New("Bucket not found") 53 | } 54 | 55 | val := bucket.Get([]byte(id)) 56 | ret = make([]byte, len(val)) 57 | copy(ret, val) 58 | return nil 59 | }) 60 | r, err := NewRuleFromBytes(ret) 61 | return r, err 62 | } 63 | 64 | func (s *Store) GetAllRules() ([]*Rule, error) { 65 | ret := make([]*Rule, 0) 66 | s.DB.View(func(tx *bolt.Tx) error { 67 | 68 | bucket := tx.Bucket(RuleBucket()) 69 | if bucket == nil { 70 | return errors.New("Bucket not found") 71 | } 72 | c := bucket.Cursor() 73 | 74 | for k, v := c.First(); k != nil; k, v = c.Next() { 75 | b := make([]byte, len(v)) 76 | copy(b, v) 77 | r, err := NewRuleFromBytes(b) 78 | 79 | if err != nil { 80 | continue 81 | } 82 | ret = append(ret, r) 83 | } 84 | 85 | return nil 86 | }) 87 | return ret, nil 88 | } 89 | 90 | func (s *Store) DeleteRule(id string) error { 91 | err := s.DB.Update(func(tx *bolt.Tx) error { 92 | return tx.Bucket(RuleBucket()).Delete([]byte(id)) 93 | }) 94 | return err 95 | } 96 | -------------------------------------------------------------------------------- /handlers.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "html/template" 5 | "log" 6 | "net/http" 7 | "path" 8 | "strconv" 9 | "strings" 10 | ) 11 | 12 | const defaultJs = ` 13 | rule(function(data){ 14 | return true; 15 | }) 16 | ` 17 | 18 | func ErrorPage(w http.ResponseWriter, r *http.Request) { 19 | w.Write([]byte("Something went wrong :-(")) 20 | } 21 | 22 | func AdminHandler(w http.ResponseWriter, r *http.Request) { 23 | rules, err := DiskStore.GetAllRules() 24 | if err != nil { 25 | ErrorPage(w, r) 26 | return 27 | } 28 | 29 | tmpl := template.New("base") 30 | template.Must(tmpl.Parse(BaseTemplateStr)) 31 | template.Must(tmpl.Parse(AdminTemplateStr)) 32 | tmpl.Execute(w, rules) 33 | } 34 | 35 | func NewRuleHandler(w http.ResponseWriter, r *http.Request) { 36 | rule := NewRule("", defaultJs, []string{""}) 37 | tmpl := template.New("base") 38 | template.Must(tmpl.Parse(BaseTemplateStr)) 39 | template.Must(tmpl.Parse(NewTemplateStr)) 40 | tmpl.Execute(w, rule) 41 | 42 | } 43 | 44 | func CreateRuleHandler(w http.ResponseWriter, r *http.Request) { 45 | title := r.FormValue("title") 46 | script := r.FormValue("script") 47 | var endpoints []string 48 | for i := 0; ; i++ { 49 | endpoint := r.FormValue("endpoint_" + strconv.Itoa(i)) 50 | if endpoint == "" { 51 | break 52 | } else { 53 | endpoints = append(endpoints, endpoint) 54 | } 55 | } 56 | 57 | rule := NewRule(title, script, endpoints) 58 | 59 | err := DiskStore.SaveRule(rule) 60 | if err != nil { 61 | ErrorPage(w, r) 62 | return 63 | } 64 | 65 | http.Redirect(w, r, "/", http.StatusMovedPermanently) 66 | 67 | } 68 | 69 | func ViewRuleHandler(w http.ResponseWriter, r *http.Request) { 70 | id := strings.Replace(r.URL.Path, "/view/", "", 1) 71 | rule, err := DiskStore.GetRule(id) 72 | if err != nil { 73 | ErrorPage(w, r) 74 | return 75 | } 76 | 77 | tmpl := template.New("base") 78 | template.Must(tmpl.Parse(BaseTemplateStr)) 79 | template.Must(tmpl.Parse(ViewTemplateStr)) 80 | tmpl.Execute(w, rule) 81 | } 82 | 83 | func EditRuleHandler(w http.ResponseWriter, r *http.Request) { 84 | id := strings.Replace(r.URL.Path, "/edit/", "", 1) 85 | rule, err := DiskStore.GetRule(id) 86 | if err != nil { 87 | ErrorPage(w, r) 88 | return 89 | } 90 | tmpl := template.New("base") 91 | template.Must(tmpl.Parse(BaseTemplateStr)) 92 | template.Must(tmpl.Parse(EditTemplateStr)) 93 | tmpl.Execute(w, rule) 94 | } 95 | 96 | func UpdateRuleHandler(w http.ResponseWriter, r *http.Request) { 97 | id := r.FormValue("id") 98 | rule, err := DiskStore.GetRule(id) 99 | if err != nil { 100 | log.Println("ERROR:", err) 101 | ErrorPage(w, r) 102 | return 103 | } 104 | 105 | (*rule).Title = r.FormValue("title") 106 | (*rule).Script = r.FormValue("script") 107 | var endpoints []string 108 | for i := 0; ; i++ { 109 | endpoint := r.FormValue("endpoint_" + strconv.Itoa(i)) 110 | if endpoint == "" { 111 | break 112 | } else { 113 | endpoints = append(endpoints, endpoint) 114 | } 115 | } 116 | (*rule).Endpoints = endpoints 117 | 118 | err = DiskStore.SaveRule(rule) 119 | if err != nil { 120 | ErrorPage(w, r) 121 | return 122 | } 123 | 124 | http.Redirect(w, r, "/", http.StatusMovedPermanently) 125 | } 126 | 127 | func DeleteRuleHandler(w http.ResponseWriter, r *http.Request) { 128 | id := path.Base(r.URL.Path) 129 | err := DiskStore.DeleteRule(id) 130 | if err != nil { 131 | log.Println("ERROR:", err) 132 | ErrorPage(w, r) 133 | return 134 | } 135 | http.Redirect(w, r, "/", http.StatusMovedPermanently) 136 | } 137 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Mailhook 2 | 3 | Mailhook is a smtp server which triggers webhooks with the mail content as payload. It's behaviour 4 | can be highly customized using its web based admin interface and Javascript based rules. 5 | 6 | # Installation 7 | 8 | Before trying to install mailhook, make sure you have installed a recent version of [Go](https://golang.org/). 9 | 10 | To install mailhook just run the following command. 11 | 12 | `` 13 | $ go get github.com/gophergala2016/mailhook 14 | `` 15 | 16 | This command will download mailhook in `$GOPATH/src/github.com/gophergala2016/mailhook`. Now change directory to this folder 17 | and run `go build` to create the **mailhook** binary. Now place the mailhook binary to any folder in `$PATH` to run it form anywhere. 18 | 19 | 20 | # Usage 21 | 22 | To run mailhook (make sure mailhook is in $PATH) simply execute the `mailhook command`. For example to start mailhook at port 2025 23 | execute the following command. 24 | 25 | ``` 26 | $ ./mailhook -p 2025 27 | 2016/01/25 02:48:56 starting SMTP endpoint on 0.0.0.0:2025 28 | 2016/01/25 02:48:56 starting filter ... 29 | 2016/01/25 02:48:56 starting admin web interface on 0.0.0.0:8080 30 | 2016/01/25 02:48:56 starting dispatcher 31 | 32 | ``` 33 | 34 | By default mailhook start the SMTP interface on port 25 and the admin interface on port 8080, however it can be run to listen 35 | on other ports using the appropriate flag. The exhaustive list of commandline flags are listed below. 36 | 37 | 38 | ``` 39 | $ mailhook -h 40 | Usage of mailhook: 41 | -a string 42 | web server bind address. (default "0.0.0.0") 43 | -d string 44 | specify rules database file. (default "mailhook.db") 45 | -p int 46 | smpt server port. (default 25) 47 | -q int 48 | webserver port. (default 8080) 49 | -s string 50 | smtp server bind address. (default "0.0.0.0") 51 | 52 | ``` 53 | 54 | # Configuring Mailhook 55 | 56 | After running mailhook using the command described in previous section. You can configure mailhook using its web based 57 | admin interface. If mailhook is run with its default flags, the admin interface will listen on port 8080. Open a web 58 | browser and point http://localhost:8080 to access the admin interface. 59 | 60 | The following screen shows the admin UI when opened for the first time after installation. 61 | 62 | ![](https://github.com/gophergala2016/mailhook/blob/master/screenshots/home.png) 63 | 64 | Now click on the "Add Rule" button to start ading rules and endpoints. the image below shows the screen to create 65 | rules. 66 | 67 | 68 | ![](https://github.com/gophergala2016/mailhook/blob/master/screenshots/create.png) 69 | 70 | A sample rule with endpoints configures is shown in the following screenshot. 71 | 72 | ![](https://github.com/gophergala2016/mailhook/blob/master/screenshots/sample.png) 73 | 74 | for more screenshots see [here](https://github.com/gophergala2016/mailhook/blob/master/screenshots/). 75 | 76 | # Writing Mailhook rules 77 | Mailhook can be customized by javascript based rules dispatch webhooks. A sample rule is shown below. 78 | 79 | ``` 80 | rule(function(mail) { 81 | 82 | return true; 83 | }); 84 | ``` 85 | this is the simplest possible rule which always evaluates to `true`. If a rule evaluates to `true` mailhook dispatches the 86 | webhooks configured with that rule. If the rule function evaluates to `false` mailhook ignores the dispatching of the 87 | webhooks. 88 | 89 | The rule function is an anonymous function which receives a mail object as its argument. The structure of the mail argument 90 | is of the following form. 91 | 92 | ``` 93 | { 94 | From : "bob@example.com", 95 | To : ["alice@example.com", "eve@example.com"], 96 | Body : "This is the mail body" 97 | 98 | } 99 | ``` 100 | 101 | thus to access the various attributes of the mail object use `object.attribute` syntax. Make sure you access the mail attributes 102 | with capitalized attribute names e.g: `mail.From`. 103 | 104 | The following example script logs the attribute values of the mail object and returns false. 105 | 106 | ``` 107 | rule(function(mail) { 108 | 109 | console.log(mail.To); 110 | console.log(mail.From); 111 | console.log(mail.Body); 112 | return false; 113 | }); 114 | 115 | ``` 116 | 117 | 118 | # License 119 | 120 | MIT License. Click [here](https://raw.githubusercontent.com/gophergala2016/mailhook/master/LICENSE) to view the license. 121 | 122 | 123 | -------------------------------------------------------------------------------- /views.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | const BaseTemplateStr = ` 4 | {{ define "base" }} 5 | 6 | 7 | 8 | 9 | 10 | Mailhook 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 37 | 38 |
39 | {{ template "content" . }} 40 |
41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | {{ end }} 49 | ` 50 | 51 | const AdminTemplateStr = ` 52 | {{ define "content" }} 53 | 60 | {{ if . }} 61 | 62 | {{ range $key, $value := . }} 63 | 64 | 65 | 70 | 75 | 76 | {{ end }} 77 |
{{ $value.Title }} 66 | 67 | 68 | 69 | 71 | 72 | 73 | 74 |
78 | {{ else }} 79 |

You have not created any rule yet.

Create Rule

80 | {{ end }} 81 | {{ end }} 82 | ` 83 | 84 | const NewTemplateStr = ` 85 | {{ define "content" }} 86 | 87 |
88 | 89 |
90 | 91 | 92 |
93 | 94 |
95 |
96 | 97 |
98 | 99 |
100 | 101 |
102 |
103 | 104 |
105 |
106 | 107 |
108 | 109 |
110 |
111 | 112 |
113 |
114 |
115 | 116 |
117 |
118 | 119 | Cancel 120 |
121 |
122 | 123 |
124 | {{ end }} 125 | ` 126 | 127 | const EditTemplateStr = ` 128 | {{ define "content" }} 129 | 130 |
131 | 132 |
133 | 134 | 135 |
136 | 137 |
138 |
139 | 140 |
141 | 142 |
143 | 144 |
145 |
146 | 147 | {{ range $key, $value := .Endpoints }} 148 |
149 | 150 |
151 | 152 |
153 |
154 | {{ end }} 155 | 156 |
157 |
158 | 159 | Cancel 160 |
161 |
162 | 163 |
164 | {{ end }} 165 | ` 166 | 167 | const ViewTemplateStr = ` 168 | {{ define "content" }} 169 | 176 |
177 |
{{.Script}}
178 |
179 |
Endpoints
180 |
    181 | {{ range $key, $value := .Endpoints }} 182 |
  • {{$value}}
  • 183 | {{ end }} 184 |
185 |
186 |
187 | {{ end }} 188 | ` 189 | --------------------------------------------------------------------------------