mirror of
https://gitlab.com/ric_harvey/MailHog.git
synced 2024-11-24 14:54:03 +00:00
359 lines
10 KiB
Go
359 lines
10 KiB
Go
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)
|
|
}
|