Skip to content

Commit

Permalink
thaomaniac: Apply code sort by date - reference mailhog#313
Browse files Browse the repository at this point in the history
  • Loading branch information
thaomaniac committed May 29, 2024
1 parent e6fa068 commit 1454015
Show file tree
Hide file tree
Showing 5 changed files with 648 additions and 2 deletions.
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ RUN apk --no-cache add --virtual build-dependencies \
git \
&& mkdir -p /root/gocode \
&& export GOPATH=/root/gocode \
&& go install github.com/mailhog/MailHog@latest
&& go install github.com/thaomaniac/MailHog@latest

FROM alpine:3
# Add mailhog user/group with uid/gid 1000.
Expand Down
23 changes: 23 additions & 0 deletions mailhog-server-api/api.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package api

import (
gohttp "net/http"

"github.com/gorilla/pat"
"github.com/mailhog/MailHog-Server/config"
)

func CreateAPI(conf *config.Config, r gohttp.Handler) {
apiv1 := createAPIv1(conf, r.(*pat.Router))
apiv2 := createAPIv2(conf, r.(*pat.Router))

go func() {
for {
select {
case msg := <-conf.MessageChan:
apiv1.messageChan <- msg
apiv2.messageChan <- msg
}
}
}()
}
359 changes: 359 additions & 0 deletions mailhog-server-api/v1.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,359 @@
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)
}
Loading

0 comments on commit 1454015

Please sign in to comment.