Add in-memory storage, clean up config

This commit is contained in:
Ian Kent 2014-04-24 00:22:50 +01:00
parent 5ee0d32b80
commit e2f599a40d
10 changed files with 155 additions and 50 deletions

View file

@ -1,4 +1,4 @@
package mailhog package config
func DefaultConfig() *Config { func DefaultConfig() *Config {
return &Config{ return &Config{
@ -18,4 +18,5 @@ type Config struct {
MongoUri string MongoUri string
MongoDb string MongoDb string
MongoColl string MongoColl string
Storage interface{}
} }

View file

@ -6,7 +6,6 @@ import (
"time" "time"
"regexp" "regexp"
"labix.org/v2/mgo/bson" "labix.org/v2/mgo/bson"
"github.com/ian-kent/MailHog/mailhog"
) )
type Messages []Message type Messages []Message
@ -44,7 +43,7 @@ type MIMEBody struct {
Parts []*Content Parts []*Content
} }
func ParseSMTPMessage(c *mailhog.Config, m *SMTPMessage) *Message { func ParseSMTPMessage(m *SMTPMessage, hostname string) *Message {
arr := make([]*Path, 0) arr := make([]*Path, 0)
for _, path := range m.To { for _, path := range m.To {
arr = append(arr, PathFromString(path)) arr = append(arr, PathFromString(path))
@ -62,8 +61,8 @@ func ParseSMTPMessage(c *mailhog.Config, m *SMTPMessage) *Message {
msg.MIME = msg.Content.ParseMIMEBody() msg.MIME = msg.Content.ParseMIMEBody()
} }
msg.Content.Headers["Message-ID"] = []string{msg.Id + "@" + c.Hostname} msg.Content.Headers["Message-ID"] = []string{msg.Id + "@" + hostname}
msg.Content.Headers["Received"] = []string{"from " + m.Helo + " by " + c.Hostname + " (Go-MailHog)\r\n id " + msg.Id + "@" + c.Hostname + "; " + time.Now().Format(time.RFC1123Z)} msg.Content.Headers["Received"] = []string{"from " + m.Helo + " by " + hostname + " (Go-MailHog)\r\n id " + msg.Id + "@" + hostname + "; " + time.Now().Format(time.RFC1123Z)}
msg.Content.Headers["Return-Path"] = []string{"<" + m.From + ">"} msg.Content.Headers["Return-Path"] = []string{"<" + m.From + ">"}
return msg return msg
} }

View file

@ -5,25 +5,23 @@ import (
"encoding/json" "encoding/json"
"net/http" "net/http"
"regexp" "regexp"
"github.com/ian-kent/MailHog/mailhog" "github.com/ian-kent/MailHog/mailhog/config"
"github.com/ian-kent/MailHog/mailhog/storage" "github.com/ian-kent/MailHog/mailhog/storage"
"github.com/ian-kent/MailHog/mailhog/http/handler" "github.com/ian-kent/MailHog/mailhog/http/handler"
) )
type APIv1 struct { type APIv1 struct {
config *mailhog.Config config *config.Config
exitChannel chan int exitChannel chan int
server *http.Server server *http.Server
mongo *storage.MongoDB
} }
func CreateAPIv1(exitCh chan int, conf *mailhog.Config, server *http.Server, mongo *storage.MongoDB) *APIv1 { func CreateAPIv1(exitCh chan int, conf *config.Config, server *http.Server) *APIv1 {
log.Println("Creating API v1") log.Println("Creating API v1")
apiv1 := &APIv1{ apiv1 := &APIv1{
config: conf, config: conf,
exitChannel: exitCh, exitChannel: exitCh,
server: server, server: server,
mongo: mongo,
} }
server.Handler.(*handler.RegexpHandler).HandleFunc(regexp.MustCompile("^/api/v1/messages/?$"), apiv1.messages) server.Handler.(*handler.RegexpHandler).HandleFunc(regexp.MustCompile("^/api/v1/messages/?$"), apiv1.messages)
@ -38,10 +36,21 @@ func (apiv1 *APIv1) messages(w http.ResponseWriter, r *http.Request, route *hand
log.Println("[APIv1] GET /api/v1/messages") log.Println("[APIv1] GET /api/v1/messages")
// TODO start, limit // TODO start, limit
messages, _ := apiv1.mongo.List(0, 1000) switch apiv1.config.Storage.(type) {
bytes, _ := json.Marshal(messages) case *storage.MongoDB:
w.Header().Set("Content-Type", "text/json") messages, _ := apiv1.config.Storage.(*storage.MongoDB).List(0, 1000)
w.Write(bytes) bytes, _ := json.Marshal(messages)
w.Header().Set("Content-Type", "text/json")
w.Write(bytes)
case *storage.Memory:
messages, _ := apiv1.config.Storage.(*storage.Memory).List(0, 1000)
bytes, _ := json.Marshal(messages)
w.Header().Set("Content-Type", "text/json")
w.Write(bytes)
default:
w.Header().Set("Content-Type", "text/json")
w.Write([]byte("[]"))
}
} }
func (apiv1 *APIv1) message(w http.ResponseWriter, r *http.Request, route *handler.Route) { func (apiv1 *APIv1) message(w http.ResponseWriter, r *http.Request, route *handler.Route) {
@ -49,17 +58,33 @@ func (apiv1 *APIv1) message(w http.ResponseWriter, r *http.Request, route *handl
id := match[1] id := match[1]
log.Printf("[APIv1] GET /api/v1/messages/%s\n", id) log.Printf("[APIv1] GET /api/v1/messages/%s\n", id)
message, _ := apiv1.mongo.Load(id) switch apiv1.config.Storage.(type) {
bytes, _ := json.Marshal(message) case *storage.MongoDB:
w.Header().Set("Content-Type", "text/json") message, _ := apiv1.config.Storage.(*storage.MongoDB).Load(id)
w.Write(bytes) bytes, _ := json.Marshal(message)
w.Header().Set("Content-Type", "text/json")
w.Write(bytes)
case *storage.Memory:
message, _ := apiv1.config.Storage.(*storage.Memory).Load(id)
bytes, _ := json.Marshal(message)
w.Header().Set("Content-Type", "text/json")
w.Write(bytes)
default:
w.Header().Set("Content-Type", "text/json")
w.Write([]byte("[]"))
}
} }
func (apiv1 *APIv1) delete_all(w http.ResponseWriter, r *http.Request, route *handler.Route) { func (apiv1 *APIv1) delete_all(w http.ResponseWriter, r *http.Request, route *handler.Route) {
log.Println("[APIv1] POST /api/v1/messages/delete") log.Println("[APIv1] POST /api/v1/messages/delete")
w.Header().Set("Content-Type", "text/json") w.Header().Set("Content-Type", "text/json")
apiv1.mongo.DeleteAll() switch apiv1.config.Storage.(type) {
case *storage.MongoDB:
apiv1.config.Storage.(*storage.MongoDB).DeleteAll()
case *storage.Memory:
apiv1.config.Storage.(*storage.Memory).DeleteAll()
}
} }
func (apiv1 *APIv1) delete_one(w http.ResponseWriter, r *http.Request, route *handler.Route) { func (apiv1 *APIv1) delete_one(w http.ResponseWriter, r *http.Request, route *handler.Route) {
@ -68,5 +93,10 @@ func (apiv1 *APIv1) delete_one(w http.ResponseWriter, r *http.Request, route *ha
log.Printf("[APIv1] POST /api/v1/messages/%s/delete\n", id) log.Printf("[APIv1] POST /api/v1/messages/%s/delete\n", id)
w.Header().Set("Content-Type", "text/json") w.Header().Set("Content-Type", "text/json")
apiv1.mongo.DeleteOne(id) switch apiv1.config.Storage.(type) {
case *storage.MongoDB:
apiv1.config.Storage.(*storage.MongoDB).DeleteOne(id)
case *storage.Memory:
apiv1.config.Storage.(*storage.Memory).DeleteOne(id)
}
} }

View file

@ -3,9 +3,8 @@ package http
import ( import (
"regexp" "regexp"
"net/http" "net/http"
"github.com/ian-kent/MailHog/mailhog" "github.com/ian-kent/MailHog/mailhog/config"
"github.com/ian-kent/MailHog/mailhog/templates" "github.com/ian-kent/MailHog/mailhog/templates"
"github.com/ian-kent/MailHog/mailhog/storage"
"github.com/ian-kent/MailHog/mailhog/templates/images" "github.com/ian-kent/MailHog/mailhog/templates/images"
"github.com/ian-kent/MailHog/mailhog/templates/js" "github.com/ian-kent/MailHog/mailhog/templates/js"
"github.com/ian-kent/MailHog/mailhog/http/api" "github.com/ian-kent/MailHog/mailhog/http/api"
@ -13,7 +12,7 @@ import (
) )
var exitChannel chan int var exitChannel chan int
var config *mailhog.Config var cfg *config.Config
func web_exit(w http.ResponseWriter, r *http.Request, route *handler.Route) { func web_exit(w http.ResponseWriter, r *http.Request, route *handler.Route) {
web_headers(w) web_headers(w)
@ -44,9 +43,9 @@ func web_headers(w http.ResponseWriter) {
w.Header().Set("Content-Type", "text/html") w.Header().Set("Content-Type", "text/html")
} }
func Start(exitCh chan int, conf *mailhog.Config, mongo *storage.MongoDB) { func Start(exitCh chan int, conf *config.Config) {
exitChannel = exitCh exitChannel = exitCh
config = conf cfg = conf
server := &http.Server{ server := &http.Server{
Addr: conf.HTTPBindAddr, Addr: conf.HTTPBindAddr,
@ -58,7 +57,7 @@ func Start(exitCh chan int, conf *mailhog.Config, mongo *storage.MongoDB) {
server.Handler.(*handler.RegexpHandler).HandleFunc(regexp.MustCompile("^/images/hog.png$"), web_imgcontroller) server.Handler.(*handler.RegexpHandler).HandleFunc(regexp.MustCompile("^/images/hog.png$"), web_imgcontroller)
server.Handler.(*handler.RegexpHandler).HandleFunc(regexp.MustCompile("^/$"), web_index) server.Handler.(*handler.RegexpHandler).HandleFunc(regexp.MustCompile("^/$"), web_index)
api.CreateAPIv1(exitCh, conf, server, mongo) api.CreateAPIv1(exitCh, conf, server)
server.ListenAndServe() server.ListenAndServe()
} }

View file

@ -7,7 +7,7 @@ import (
"net" "net"
"strings" "strings"
"regexp" "regexp"
"github.com/ian-kent/MailHog/mailhog" "github.com/ian-kent/MailHog/mailhog/config"
"github.com/ian-kent/MailHog/mailhog/storage" "github.com/ian-kent/MailHog/mailhog/storage"
"github.com/ian-kent/MailHog/mailhog/data" "github.com/ian-kent/MailHog/mailhog/data"
) )
@ -15,10 +15,9 @@ import (
type Session struct { type Session struct {
conn *net.TCPConn conn *net.TCPConn
line string line string
conf *mailhog.Config conf *config.Config
state int state int
message *data.SMTPMessage message *data.SMTPMessage
mongo *storage.MongoDB
isTLS bool isTLS bool
} }
@ -34,8 +33,8 @@ const (
// TODO replace ".." lines with . in data // TODO replace ".." lines with . in data
func StartSession(conn *net.TCPConn, conf *mailhog.Config, mongo *storage.MongoDB) { func StartSession(conn *net.TCPConn, conf *config.Config) {
conv := &Session{conn, "", conf, ESTABLISH, &data.SMTPMessage{}, mongo, false} conv := &Session{conn, "", conf, ESTABLISH, &data.SMTPMessage{}, false}
conv.log("Starting session") conv.log("Starting session")
conv.Write("220", conv.conf.Hostname + " ESMTP Go-MailHog") conv.Write("220", conv.conf.Hostname + " ESMTP Go-MailHog")
conv.Read() conv.Read()
@ -82,7 +81,19 @@ func (c *Session) Parse() {
c.log("Got EOF, storing message and switching to MAIL state") c.log("Got EOF, storing message and switching to MAIL state")
//c.log("Full message data: %s", c.message.Data) //c.log("Full message data: %s", c.message.Data)
c.message.Data = strings.TrimSuffix(c.message.Data, "\r\n.\r\n") c.message.Data = strings.TrimSuffix(c.message.Data, "\r\n.\r\n")
id, err := c.mongo.Store(c.message) var id string
var err error
switch c.conf.Storage.(type) {
case *storage.MongoDB:
c.log("Storing message using MongoDB")
id, err = c.conf.Storage.(*storage.MongoDB).Store(c.message)
case *storage.Memory:
c.log("Storing message using Memory")
id, err = c.conf.Storage.(*storage.Memory).Store(c.message)
default:
c.log("Unknown storage type")
// TODO send error reply
}
c.state = MAIL c.state = MAIL
if err != nil { if err != nil {
c.log("Error storing message: %s", err) c.log("Error storing message: %s", err)

58
mailhog/storage/memory.go Normal file
View file

@ -0,0 +1,58 @@
package storage
import (
"github.com/ian-kent/MailHog/mailhog/config"
"github.com/ian-kent/MailHog/mailhog/data"
)
type Memory struct {
Config *config.Config
Messages map[string]*data.Message
MessageIndex []string
MessageRIndex map[string]int
}
func CreateMemory(c *config.Config) *Memory {
return &Memory{
Config: c,
Messages: make(map[string]*data.Message, 0),
MessageIndex: make([]string, 0),
MessageRIndex: make(map[string]int, 0),
}
}
func (memory *Memory) Store(m *data.SMTPMessage) (string, error) {
msg := data.ParseSMTPMessage(m, memory.Config.Hostname)
memory.Messages[msg.Id] = msg
memory.MessageIndex = append(memory.MessageIndex, msg.Id)
memory.MessageRIndex[msg.Id] = len(memory.MessageIndex)
return msg.Id, nil
}
func (memory *Memory) List(start int, limit int) ([]*data.Message, error) {
if limit > len(memory.MessageIndex) { limit = len(memory.MessageIndex) }
messages := make([]*data.Message, 0)
for _, m := range memory.MessageIndex[start:limit] {
messages = append(messages, memory.Messages[m])
}
return messages, nil;
}
func (memory *Memory) DeleteOne(id string) error {
index := memory.MessageRIndex[id];
delete(memory.Messages, id)
memory.MessageIndex = append(memory.MessageIndex[:index], memory.MessageIndex[index+1:]...)
delete(memory.MessageRIndex, id)
return nil
}
func (memory *Memory) DeleteAll() error {
memory.Messages = make(map[string]*data.Message, 0)
memory.MessageIndex = make([]string, 0)
memory.MessageRIndex = make(map[string]int, 0)
return nil
}
func (memory *Memory) Load(id string) (*data.Message, error) {
return memory.Messages[id], nil;
}

View file

@ -5,19 +5,20 @@ import (
"labix.org/v2/mgo" "labix.org/v2/mgo"
"labix.org/v2/mgo/bson" "labix.org/v2/mgo/bson"
"github.com/ian-kent/MailHog/mailhog/data" "github.com/ian-kent/MailHog/mailhog/data"
"github.com/ian-kent/MailHog/mailhog" "github.com/ian-kent/MailHog/mailhog/config"
) )
type MongoDB struct { type MongoDB struct {
Session *mgo.Session Session *mgo.Session
Config *mailhog.Config Config *config.Config
Collection *mgo.Collection Collection *mgo.Collection
} }
func CreateMongoDB(c *mailhog.Config) *MongoDB { func CreateMongoDB(c *config.Config) *MongoDB {
log.Printf("Connecting to MongoDB: %s\n", c.MongoUri)
session, err := mgo.Dial(c.MongoUri) session, err := mgo.Dial(c.MongoUri)
if(err != nil) { if(err != nil) {
log.Fatalf("Error connecting to MongoDB: %s", err) log.Printf("Error connecting to MongoDB: %s", err)
return nil return nil
} }
return &MongoDB{ return &MongoDB{
@ -28,7 +29,7 @@ func CreateMongoDB(c *mailhog.Config) *MongoDB {
} }
func (mongo *MongoDB) Store(m *data.SMTPMessage) (string, error) { func (mongo *MongoDB) Store(m *data.SMTPMessage) (string, error) {
msg := data.ParseSMTPMessage(mongo.Config, m) msg := data.ParseSMTPMessage(m, mongo.Config.Hostname)
err := mongo.Collection.Insert(msg) err := mongo.Collection.Insert(msg)
if err != nil { if err != nil {
log.Printf("Error inserting message: %s", err) log.Printf("Error inserting message: %s", err)

24
main.go
View file

@ -2,7 +2,7 @@ package main
import ( import (
"flag" "flag"
"github.com/ian-kent/MailHog/mailhog" "github.com/ian-kent/MailHog/mailhog/config"
"github.com/ian-kent/MailHog/mailhog/http" "github.com/ian-kent/MailHog/mailhog/http"
"github.com/ian-kent/MailHog/mailhog/smtp" "github.com/ian-kent/MailHog/mailhog/smtp"
"github.com/ian-kent/MailHog/mailhog/storage" "github.com/ian-kent/MailHog/mailhog/storage"
@ -11,11 +11,10 @@ import (
"os" "os"
) )
var conf *mailhog.Config var conf *config.Config
var mongo *storage.MongoDB
var exitCh chan int var exitCh chan int
func config() { func configure() {
var smtpbindaddr, httpbindaddr, hostname, mongouri, mongodb, mongocoll string var smtpbindaddr, httpbindaddr, hostname, mongouri, mongodb, mongocoll string
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(&smtpbindaddr, "smtpbindaddr", "0.0.0.0:1025", "SMTP bind interface and port, e.g. 0.0.0.0:1025 or just :1025")
@ -27,7 +26,7 @@ func config() {
flag.Parse() flag.Parse()
conf = &mailhog.Config{ conf = &config.Config{
SMTPBindAddr: smtpbindaddr, SMTPBindAddr: smtpbindaddr,
HTTPBindAddr: httpbindaddr, HTTPBindAddr: httpbindaddr,
Hostname: hostname, Hostname: hostname,
@ -36,11 +35,18 @@ func config() {
MongoColl: mongocoll, MongoColl: mongocoll,
} }
mongo = storage.CreateMongoDB(conf) s := storage.CreateMongoDB(conf)
if s == nil {
log.Println("MongoDB storage unavailable, using in-memory storage")
conf.Storage = storage.CreateMemory(conf)
} else {
log.Println("Connected to MongoDB")
conf.Storage = s
}
} }
func main() { func main() {
config() configure()
exitCh = make(chan int) exitCh = make(chan int)
go web_listen() go web_listen()
@ -57,7 +63,7 @@ func main() {
func web_listen() { func web_listen() {
log.Printf("[HTTP] Binding to address: %s\n", conf.HTTPBindAddr) log.Printf("[HTTP] Binding to address: %s\n", conf.HTTPBindAddr)
http.Start(exitCh, conf, mongo) http.Start(exitCh, conf)
} }
func smtp_listen() *net.TCPListener { func smtp_listen() *net.TCPListener {
@ -76,6 +82,6 @@ func smtp_listen() *net.TCPListener {
} }
defer conn.Close() defer conn.Close()
go smtp.StartSession(conn.(*net.TCPConn), conf, mongo) go smtp.StartSession(conn.(*net.TCPConn), conf)
} }
} }

View file

@ -1,7 +1,7 @@
package main package main
import ( import (
"github.com/ian-kent/MailHog/mailhog" "github.com/ian-kent/MailHog/mailhog/config"
"github.com/ian-kent/MailHog/mailhog/storage" "github.com/ian-kent/MailHog/mailhog/storage"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"net" "net"
@ -94,7 +94,7 @@ func TestBasicHappyPath(t *testing.T) {
assert.Nil(t, err) assert.Nil(t, err)
assert.Equal(t, string(buf[0:n]), "221 Bye\n") assert.Equal(t, string(buf[0:n]), "221 Bye\n")
s := storage.CreateMongoDB(mailhog.DefaultConfig()) s := storage.CreateMongoDB(config.DefaultConfig())
message, err := s.Load(match[1]) message, err := s.Load(match[1])
assert.Nil(t, err) assert.Nil(t, err)
assert.NotNil(t, message) assert.NotNil(t, message)

View file

@ -1,7 +1,7 @@
package main package main
import ( import (
"github.com/ian-kent/MailHog/mailhog" "github.com/ian-kent/MailHog/mailhog/config"
"github.com/ian-kent/MailHog/mailhog/storage" "github.com/ian-kent/MailHog/mailhog/storage"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"net" "net"
@ -112,7 +112,7 @@ func TestBasicMIMEHappyPath(t *testing.T) {
assert.Nil(t, err) assert.Nil(t, err)
assert.Equal(t, string(buf[0:n]), "221 Bye\n") assert.Equal(t, string(buf[0:n]), "221 Bye\n")
s := storage.CreateMongoDB(mailhog.DefaultConfig()) s := storage.CreateMongoDB(config.DefaultConfig())
message, err := s.Load(match[1]) message, err := s.Load(match[1])
assert.Nil(t, err) assert.Nil(t, err)
assert.NotNil(t, message) assert.NotNil(t, message)