From 4f4f9210135ebe560d5a40663b793956ffb20091 Mon Sep 17 00:00:00 2001 From: Ian Kent Date: Sun, 20 Apr 2014 15:35:59 +0100 Subject: [PATCH] Add HTTP server and assets --- mailhog/config.go | 14 ++++- mailhog/data/message.go | 1 + mailhog/http/server.go | 50 ++++++++++++++++ mailhog/smtp/session.go | 3 +- mailhog/storage/mongodb.go | 10 ++-- mailhog/templates/images/hog.go | 60 +++++++++++++++++++ mailhog/templates/index.go | 90 +++++++++++++++++++++++++++++ mailhog/templates/js/controllers.go | 43 ++++++++++++++ mailhog/templates/layout.go | 64 ++++++++++++++++++++ main.go | 53 ++++++++++++----- main_test.go | 3 +- 11 files changed, 366 insertions(+), 25 deletions(-) create mode 100644 mailhog/http/server.go create mode 100644 mailhog/templates/images/hog.go create mode 100644 mailhog/templates/index.go create mode 100644 mailhog/templates/js/controllers.go create mode 100644 mailhog/templates/layout.go diff --git a/mailhog/config.go b/mailhog/config.go index f1410e9..4aeb093 100644 --- a/mailhog/config.go +++ b/mailhog/config.go @@ -2,12 +2,20 @@ package mailhog func DefaultConfig() *Config { return &Config{ - BindAddr: "0.0.0.0:1025", - Hostname: "mailhog.example", + SMTPBindAddr: "0.0.0.0:1025", + HTTPBindAddr: "0.0.0.0:8025", + Hostname: "mailhog.example", + MongoUri: "127.0.0.1:27017", + MongoDb: "mailhog", + MongoColl: "messages", } } type Config struct { - BindAddr string + SMTPBindAddr string + HTTPBindAddr string Hostname string + MongoUri string + MongoDb string + MongoColl string } diff --git a/mailhog/data/message.go b/mailhog/data/message.go index abb72e0..c024091 100644 --- a/mailhog/data/message.go +++ b/mailhog/data/message.go @@ -89,6 +89,7 @@ func ContentFromString(data string) *Content { if(strings.Contains(hdr, ": ")) { y := strings.SplitN(hdr, ": ", 2) key, value := y[0], y[1] + // TODO multiple header fields h[key] = []string{value} } else { log.Printf("Found invalid header: '%s'", hdr) diff --git a/mailhog/http/server.go b/mailhog/http/server.go new file mode 100644 index 0000000..3ba77ec --- /dev/null +++ b/mailhog/http/server.go @@ -0,0 +1,50 @@ +package http + +import ( + "fmt" + "net/http" + "github.com/ian-kent/MailHog/mailhog" + "github.com/ian-kent/MailHog/mailhog/templates" + "github.com/ian-kent/MailHog/mailhog/templates/images" + "github.com/ian-kent/MailHog/mailhog/templates/js" +) + +var exitChannel chan int + +func web_exit(w http.ResponseWriter, r *http.Request) { + web_headers(w) + fmt.Fprint(w, "Exiting MailHog!") + exitChannel <- 1 +} + +func web_index(w http.ResponseWriter, r *http.Request) { + web_headers(w) + fmt.Fprint(w, web_render(templates.Index())) +} + +func web_jscontroller(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "text/javascript") + fmt.Fprint(w, js.Controllers()) +} + +func web_imgcontroller(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "image/png") + w.Write(images.Hog()) +} + +func web_render(content string) string { + return templates.Layout(content) +} + +func web_headers(w http.ResponseWriter) { + w.Header().Set("Content-Type", "text/html") +} + +func Start(exitCh chan int, conf *mailhog.Config) { + exitChannel = exitCh + http.HandleFunc("/exit", web_exit) + http.HandleFunc("/js/controllers.js", web_jscontroller) + http.HandleFunc("/images/hog.png", web_imgcontroller) + http.HandleFunc("/", web_index) + http.ListenAndServe(conf.HTTPBindAddr, nil) +} diff --git a/mailhog/smtp/session.go b/mailhog/smtp/session.go index d742fe2..fed3a56 100644 --- a/mailhog/smtp/session.go +++ b/mailhog/smtp/session.go @@ -28,7 +28,6 @@ const ( DONE ) -// TODO add Received/Return-Path headers // TODO replace ".." lines with . in data func StartSession(conn *net.TCPConn, conf *mailhog.Config) { @@ -39,7 +38,7 @@ func StartSession(conn *net.TCPConn, conf *mailhog.Config) { } func (c *Session) log(message string, args ...interface{}) { - message = strings.Join([]string{"[%s, %d]", message}, " ") + message = strings.Join([]string{"[SMTP %s, %d]", message}, " ") args = append([]interface{}{c.conn.RemoteAddr(), c.state}, args...) log.Printf(message, args...) } diff --git a/mailhog/storage/mongodb.go b/mailhog/storage/mongodb.go index 3ac11ed..e6d9d97 100644 --- a/mailhog/storage/mongodb.go +++ b/mailhog/storage/mongodb.go @@ -10,13 +10,13 @@ import ( func Store(c *mailhog.Config, m *data.SMTPMessage) (string, error) { msg := data.ParseSMTPMessage(c, m) - session, err := mgo.Dial("localhost:27017") + session, err := mgo.Dial(c.MongoUri) if(err != nil) { log.Printf("Error connecting to MongoDB: %s", err) return "", err } defer session.Close() - err = session.DB("mailhog").C("messages").Insert(msg) + err = session.DB(c.MongoDb).C(c.MongoColl).Insert(msg) if err != nil { log.Printf("Error inserting message: %s", err) return "", err @@ -24,15 +24,15 @@ func Store(c *mailhog.Config, m *data.SMTPMessage) (string, error) { return msg.Id, nil } -func Load(id string) (*data.Message, error) { - session, err := mgo.Dial("localhost:27017") +func Load(c *mailhog.Config, id string) (*data.Message, error) { + session, err := mgo.Dial(c.MongoUri) if(err != nil) { log.Printf("Error connecting to MongoDB: %s", err) return nil, err } defer session.Close() result := &data.Message{} - err = session.DB("mailhog").C("messages").Find(bson.M{"id": id}).One(&result) + err = session.DB(c.MongoDb).C(c.MongoColl).Find(bson.M{"id": id}).One(&result) if err != nil { log.Printf("Error loading message: %s", err) return nil, err diff --git a/mailhog/templates/images/hog.go b/mailhog/templates/images/hog.go new file mode 100644 index 0000000..25f8163 --- /dev/null +++ b/mailhog/templates/images/hog.go @@ -0,0 +1,60 @@ +package images + +import ( + "encoding/base64" +) + +func Hog() []byte { + hog := `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 +qgAAAABJRU5ErkJggg==` + data, _ := base64.StdEncoding.DecodeString(hog) + return data +} diff --git a/mailhog/templates/index.go b/mailhog/templates/index.go new file mode 100644 index 0000000..e11f0fc --- /dev/null +++ b/mailhog/templates/index.go @@ -0,0 +1,90 @@ +package templates + +func Index() string { + return ` + + + +
+ + + + + + + + + + + + + + + + + +
FromToSubjectReceivedActions
+ {{ message.from.mailbox }}@{{ message.from.domain }} + + + {{ to.mailbox }}@{{ to.domain }} + + + {{ message.content.headers.Subject }} + + {{ date(message.created) }} + + +
+
+
+ + + + + +
+ {{ header }} + + {{ value }} +
+ {{ preview.content.body }} +
+`; +} \ No newline at end of file diff --git a/mailhog/templates/js/controllers.go b/mailhog/templates/js/controllers.go new file mode 100644 index 0000000..e9befaa --- /dev/null +++ b/mailhog/templates/js/controllers.go @@ -0,0 +1,43 @@ +package js + +func Controllers() string { + return ` +var mailhogApp = angular.module('mailhogApp', []); + +mailhogApp.controller('MailCtrl', function ($scope, $http) { + $scope.refresh = function() { + $http.get('/api/v1/messages').success(function(data) { + $scope.messages = data; + }); + } + $scope.refresh(); + + $scope.date = function(timestamp) { + return (new Date(timestamp)).toString(); + }; + + $scope.selectMessage = function(message) { + $scope.preview = message; + } + + $scope.deleteAll = function() { + $('#confirm-delete-all').modal('show'); + } + + $scope.deleteAllConfirm = function() { + $('#confirm-delete-all').modal('hide'); + $http.post('/api/v1/messages/delete').success(function() { + $scope.refresh(); + $scope.preview = null; + }); + } + + $scope.deleteOne = function(message) { + $http.post('/api/v1/messages/' + message._id + '/delete').success(function() { + if($scope.preview._id == message._id) $scope.preview = null; + $scope.refresh(); + }); + } +}); +`; +} \ No newline at end of file diff --git a/mailhog/templates/layout.go b/mailhog/templates/layout.go new file mode 100644 index 0000000..b5d76dc --- /dev/null +++ b/mailhog/templates/layout.go @@ -0,0 +1,64 @@ +package templates + +import ( + "strings" +) + +func Layout(content string) string { + html := ` + + + + MailHog + + + + + + + + + + + <%= content %> + + +`; + return strings.Replace(html, "<%= content %>", content, -1); +} \ No newline at end of file diff --git a/main.go b/main.go index 42e2937..16031b5 100644 --- a/main.go +++ b/main.go @@ -4,36 +4,70 @@ import ( "flag" "log" "net" + "os" "github.com/ian-kent/MailHog/mailhog" + "github.com/ian-kent/MailHog/mailhog/http" "github.com/ian-kent/MailHog/mailhog/smtp" ) var conf *mailhog.Config +var exitCh chan int func config() { - var listen, hostname string + var smtpbindaddr, httpbindaddr, hostname, mongouri, mongodb, mongocoll string - flag.StringVar(&listen, "listen", "0.0.0.0:1025", "Bind interface and port, e.g. 0.0.0.0:1025 or just :1025") + flag.StringVar(&smtpbindaddr, "smtpbindaddr", "0.0.0.0:1025", "SMTP bind interface and port, e.g. 0.0.0.0:1025 or just :1025") + flag.StringVar(&httpbindaddr, "httpbindaddr", "0.0.0.0:8025", "HTTP bind interface and port, e.g. 0.0.0.0:8025 or just :8025") flag.StringVar(&hostname, "hostname", "mailhog.example", "Hostname for EHLO/HELO response, e.g. mailhog.example") + flag.StringVar(&mongouri, "mongouri", "127.0.0.1:27017", "MongoDB URI, e.g. 127.0.0.1:27017") + flag.StringVar(&mongodb, "mongodb", "mailhog", "MongoDB database, e.g. mailhog") + flag.StringVar(&mongocoll, "mongocoll", "messages", "MongoDB collection, e.g. messages") flag.Parse() conf = &mailhog.Config{ - BindAddr: listen, + SMTPBindAddr: smtpbindaddr, + HTTPBindAddr: httpbindaddr, Hostname: hostname, + MongoUri: mongouri, + MongoDb: mongodb, + MongoColl: mongocoll, } } func main() { config() - ln := listen(conf.BindAddr) + exitCh = make(chan int) + go web_listen() + go smtp_listen() + + for { + select { + case <-exitCh: + log.Printf("Received exit signal") + os.Exit(0) + } + } +} + +func web_listen() { + log.Printf("[HTTP] Binding to address: %s\n", conf.HTTPBindAddr) + http.Start(exitCh, conf) +} + +func smtp_listen() (*net.TCPListener) { + log.Printf("[SMTP] Binding to address: %s\n", conf.SMTPBindAddr) + ln, err := net.Listen("tcp", conf.SMTPBindAddr) + if err != nil { + log.Fatalf("[SMTP] Error listening on socket: %s\n", err) + } defer ln.Close() for { conn, err := ln.Accept() if err != nil { - log.Printf("Error accepting connection: %s\n", err) + log.Printf("[SMTP] Error accepting connection: %s\n", err) continue } defer conn.Close() @@ -41,12 +75,3 @@ func main() { go smtp.StartSession(conn.(*net.TCPConn), conf) } } - -func listen(bind string) (*net.TCPListener) { - log.Printf("Binding to address: %s\n", bind) - ln, err := net.Listen("tcp", bind) - if err != nil { - log.Fatalf("Error listening on socket: %s\n", err) - } - return ln.(*net.TCPListener) -} diff --git a/main_test.go b/main_test.go index 0204bdc..20e0707 100644 --- a/main_test.go +++ b/main_test.go @@ -6,6 +6,7 @@ import ( "net" "strings" "regexp" + "github.com/ian-kent/MailHog/mailhog" "github.com/ian-kent/MailHog/mailhog/storage" ) @@ -86,7 +87,7 @@ func TestBasicHappyPath(t *testing.T) { assert.Nil(t, err) assert.Equal(t, string(buf[0:n]), "221 Bye\n") - message, err := storage.Load(match[1]) + message, err := storage.Load(mailhog.DefaultConfig(), match[1]) assert.Nil(t, err) assert.NotNil(t, message)