MailHog/vendor/github.com/mailhog/MailHog-Server/api/v1.go

360 lines
10 KiB
Go
Raw Normal View History

2017-04-16 23:42:21 +00:00
package api
import (
"encoding/base64"
"encoding/json"
"net/http"
"net/smtp"
"strconv"
"strings"
"time"
"github.com/gorilla/pat"
"github.com/ian-kent/go-log/log"
"github.com/mailhog/MailHog-Server/config"
"github.com/mailhog/data"
"github.com/mailhog/storage"
"github.com/ian-kent/goose"
)
// APIv1 implements version 1 of the MailHog API
//
// The specification has been frozen and will eventually be deprecated.
// Only bug fixes and non-breaking changes will be applied here.
//
// Any changes/additions should be added in APIv2.
type APIv1 struct {
config *config.Config
messageChan chan *data.Message
}
// FIXME should probably move this into APIv1 struct
var stream *goose.EventStream
// ReleaseConfig is an alias to preserve go package API
type ReleaseConfig config.OutgoingSMTP
func createAPIv1(conf *config.Config, r *pat.Router) *APIv1 {
log.Println("Creating API v1 with WebPath: " + conf.WebPath)
apiv1 := &APIv1{
config: conf,
messageChan: make(chan *data.Message),
}
stream = goose.NewEventStream()
r.Path(conf.WebPath + "/api/v1/messages").Methods("GET").HandlerFunc(apiv1.messages)
r.Path(conf.WebPath + "/api/v1/messages").Methods("DELETE").HandlerFunc(apiv1.delete_all)
r.Path(conf.WebPath + "/api/v1/messages").Methods("OPTIONS").HandlerFunc(apiv1.defaultOptions)
r.Path(conf.WebPath + "/api/v1/messages/{id}").Methods("GET").HandlerFunc(apiv1.message)
r.Path(conf.WebPath + "/api/v1/messages/{id}").Methods("DELETE").HandlerFunc(apiv1.delete_one)
r.Path(conf.WebPath + "/api/v1/messages/{id}").Methods("OPTIONS").HandlerFunc(apiv1.defaultOptions)
r.Path(conf.WebPath + "/api/v1/messages/{id}/download").Methods("GET").HandlerFunc(apiv1.download)
r.Path(conf.WebPath + "/api/v1/messages/{id}/download").Methods("OPTIONS").HandlerFunc(apiv1.defaultOptions)
r.Path(conf.WebPath + "/api/v1/messages/{id}/mime/part/{part}/download").Methods("GET").HandlerFunc(apiv1.download_part)
r.Path(conf.WebPath + "/api/v1/messages/{id}/mime/part/{part}/download").Methods("OPTIONS").HandlerFunc(apiv1.defaultOptions)
r.Path(conf.WebPath + "/api/v1/messages/{id}/release").Methods("POST").HandlerFunc(apiv1.release_one)
r.Path(conf.WebPath + "/api/v1/messages/{id}/release").Methods("OPTIONS").HandlerFunc(apiv1.defaultOptions)
r.Path(conf.WebPath + "/api/v1/events").Methods("GET").HandlerFunc(apiv1.eventstream)
r.Path(conf.WebPath + "/api/v1/events").Methods("OPTIONS").HandlerFunc(apiv1.defaultOptions)
go func() {
keepaliveTicker := time.Tick(time.Minute)
for {
select {
case msg := <-apiv1.messageChan:
log.Println("Got message in APIv1 event stream")
bytes, _ := json.MarshalIndent(msg, "", " ")
json := string(bytes)
log.Printf("Sending content: %s\n", json)
apiv1.broadcast(json)
case <-keepaliveTicker:
apiv1.keepalive()
}
}
}()
return apiv1
}
func (apiv1 *APIv1) defaultOptions(w http.ResponseWriter, req *http.Request) {
if len(apiv1.config.CORSOrigin) > 0 {
w.Header().Add("Access-Control-Allow-Origin", apiv1.config.CORSOrigin)
w.Header().Add("Access-Control-Allow-Methods", "OPTIONS,GET,POST,DELETE")
w.Header().Add("Access-Control-Allow-Headers", "Content-Type")
}
}
func (apiv1 *APIv1) broadcast(json string) {
log.Println("[APIv1] BROADCAST /api/v1/events")
b := []byte(json)
stream.Notify("data", b)
}
// keepalive sends an empty keep alive message.
//
// This not only can keep connections alive, but also will detect broken
// connections. Without this it is possible for the server to become
// unresponsive due to too many open files.
func (apiv1 *APIv1) keepalive() {
log.Println("[APIv1] KEEPALIVE /api/v1/events")
stream.Notify("keepalive", []byte{})
}
func (apiv1 *APIv1) eventstream(w http.ResponseWriter, req *http.Request) {
log.Println("[APIv1] GET /api/v1/events")
//apiv1.defaultOptions(session)
if len(apiv1.config.CORSOrigin) > 0 {
w.Header().Add("Access-Control-Allow-Origin", apiv1.config.CORSOrigin)
w.Header().Add("Access-Control-Allow-Methods", "OPTIONS,GET,POST,DELETE")
}
stream.AddReceiver(w)
}
func (apiv1 *APIv1) messages(w http.ResponseWriter, req *http.Request) {
log.Println("[APIv1] GET /api/v1/messages")
apiv1.defaultOptions(w, req)
// TODO start, limit
switch apiv1.config.Storage.(type) {
case *storage.MongoDB:
messages, _ := apiv1.config.Storage.(*storage.MongoDB).List(0, 1000)
bytes, _ := json.Marshal(messages)
w.Header().Add("Content-Type", "text/json")
w.Write(bytes)
case *storage.InMemory:
messages, _ := apiv1.config.Storage.(*storage.InMemory).List(0, 1000)
bytes, _ := json.Marshal(messages)
w.Header().Add("Content-Type", "text/json")
w.Write(bytes)
default:
w.WriteHeader(500)
}
}
func (apiv1 *APIv1) message(w http.ResponseWriter, req *http.Request) {
id := req.URL.Query().Get(":id")
log.Printf("[APIv1] GET /api/v1/messages/%s\n", id)
apiv1.defaultOptions(w, req)
message, err := apiv1.config.Storage.Load(id)
if err != nil {
log.Printf("- Error: %s", err)
w.WriteHeader(500)
return
}
bytes, err := json.Marshal(message)
if err != nil {
log.Printf("- Error: %s", err)
w.WriteHeader(500)
return
}
w.Header().Set("Content-Type", "text/json")
w.Write(bytes)
}
func (apiv1 *APIv1) download(w http.ResponseWriter, req *http.Request) {
id := req.URL.Query().Get(":id")
log.Printf("[APIv1] GET /api/v1/messages/%s\n", id)
apiv1.defaultOptions(w, req)
w.Header().Set("Content-Type", "message/rfc822")
w.Header().Set("Content-Disposition", "attachment; filename=\""+id+".eml\"")
switch apiv1.config.Storage.(type) {
case *storage.MongoDB:
message, _ := apiv1.config.Storage.(*storage.MongoDB).Load(id)
for h, l := range message.Content.Headers {
for _, v := range l {
w.Write([]byte(h + ": " + v + "\r\n"))
}
}
w.Write([]byte("\r\n" + message.Content.Body))
case *storage.InMemory:
message, _ := apiv1.config.Storage.(*storage.InMemory).Load(id)
for h, l := range message.Content.Headers {
for _, v := range l {
w.Write([]byte(h + ": " + v + "\r\n"))
}
}
w.Write([]byte("\r\n" + message.Content.Body))
default:
w.WriteHeader(500)
}
}
func (apiv1 *APIv1) download_part(w http.ResponseWriter, req *http.Request) {
id := req.URL.Query().Get(":id")
part := req.URL.Query().Get(":part")
log.Printf("[APIv1] GET /api/v1/messages/%s/mime/part/%s/download\n", id, part)
// TODO extension from content-type?
apiv1.defaultOptions(w, req)
w.Header().Set("Content-Disposition", "attachment; filename=\""+id+"-part-"+part+"\"")
message, _ := apiv1.config.Storage.Load(id)
contentTransferEncoding := ""
pid, _ := strconv.Atoi(part)
for h, l := range message.MIME.Parts[pid].Headers {
for _, v := range l {
switch strings.ToLower(h) {
case "content-disposition":
// Prevent duplicate "content-disposition"
w.Header().Set(h, v)
case "content-transfer-encoding":
if contentTransferEncoding == "" {
contentTransferEncoding = v
}
fallthrough
default:
w.Header().Add(h, v)
}
}
}
body := []byte(message.MIME.Parts[pid].Body)
if strings.ToLower(contentTransferEncoding) == "base64" {
var e error
body, e = base64.StdEncoding.DecodeString(message.MIME.Parts[pid].Body)
if e != nil {
log.Printf("[APIv1] Decoding base64 encoded body failed: %s", e)
}
}
w.Write(body)
}
func (apiv1 *APIv1) delete_all(w http.ResponseWriter, req *http.Request) {
log.Println("[APIv1] POST /api/v1/messages")
apiv1.defaultOptions(w, req)
w.Header().Add("Content-Type", "text/json")
err := apiv1.config.Storage.DeleteAll()
if err != nil {
log.Println(err)
w.WriteHeader(500)
return
}
w.WriteHeader(200)
}
func (apiv1 *APIv1) release_one(w http.ResponseWriter, req *http.Request) {
id := req.URL.Query().Get(":id")
log.Printf("[APIv1] POST /api/v1/messages/%s/release\n", id)
apiv1.defaultOptions(w, req)
w.Header().Add("Content-Type", "text/json")
msg, _ := apiv1.config.Storage.Load(id)
decoder := json.NewDecoder(req.Body)
var cfg ReleaseConfig
err := decoder.Decode(&cfg)
if err != nil {
log.Printf("Error decoding request body: %s", err)
w.WriteHeader(500)
w.Write([]byte("Error decoding request body"))
return
}
log.Printf("%+v", cfg)
log.Printf("Got message: %s", msg.ID)
if cfg.Save {
if _, ok := apiv1.config.OutgoingSMTP[cfg.Name]; ok {
log.Printf("Server already exists named %s", cfg.Name)
w.WriteHeader(400)
return
}
cf := config.OutgoingSMTP(cfg)
apiv1.config.OutgoingSMTP[cfg.Name] = &cf
log.Printf("Saved server with name %s", cfg.Name)
}
if len(cfg.Name) > 0 {
if c, ok := apiv1.config.OutgoingSMTP[cfg.Name]; ok {
log.Printf("Using server with name: %s", cfg.Name)
cfg.Name = c.Name
if len(cfg.Email) == 0 {
cfg.Email = c.Email
}
cfg.Host = c.Host
cfg.Port = c.Port
cfg.Username = c.Username
cfg.Password = c.Password
cfg.Mechanism = c.Mechanism
} else {
log.Printf("Server not found: %s", cfg.Name)
w.WriteHeader(400)
return
}
}
log.Printf("Releasing to %s (via %s:%s)", cfg.Email, cfg.Host, cfg.Port)
bytes := make([]byte, 0)
for h, l := range msg.Content.Headers {
for _, v := range l {
bytes = append(bytes, []byte(h+": "+v+"\r\n")...)
}
}
bytes = append(bytes, []byte("\r\n"+msg.Content.Body)...)
var auth smtp.Auth
if len(cfg.Username) > 0 || len(cfg.Password) > 0 {
log.Printf("Found username/password, using auth mechanism: [%s]", cfg.Mechanism)
switch cfg.Mechanism {
case "CRAMMD5":
auth = smtp.CRAMMD5Auth(cfg.Username, cfg.Password)
case "PLAIN":
auth = smtp.PlainAuth("", cfg.Username, cfg.Password, cfg.Host)
default:
log.Printf("Error - invalid authentication mechanism")
w.WriteHeader(400)
return
}
}
err = smtp.SendMail(cfg.Host+":"+cfg.Port, auth, "nobody@"+apiv1.config.Hostname, []string{cfg.Email}, bytes)
if err != nil {
log.Printf("Failed to release message: %s", err)
w.WriteHeader(500)
return
}
log.Printf("Message released successfully")
}
func (apiv1 *APIv1) delete_one(w http.ResponseWriter, req *http.Request) {
id := req.URL.Query().Get(":id")
log.Printf("[APIv1] POST /api/v1/messages/%s/delete\n", id)
apiv1.defaultOptions(w, req)
w.Header().Add("Content-Type", "text/json")
err := apiv1.config.Storage.DeleteOne(id)
if err != nil {
log.Println(err)
w.WriteHeader(500)
return
}
w.WriteHeader(200)
}