Skip to content

Commit 1454015

Browse files
committed
thaomaniac: Apply code sort by date - reference mailhog#313
1 parent e6fa068 commit 1454015

File tree

5 files changed

+648
-2
lines changed

5 files changed

+648
-2
lines changed

Dockerfile

+1-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ RUN apk --no-cache add --virtual build-dependencies \
99
git \
1010
&& mkdir -p /root/gocode \
1111
&& export GOPATH=/root/gocode \
12-
&& go install github.com/mailhog/MailHog@latest
12+
&& go install github.com/thaomaniac/MailHog@latest
1313

1414
FROM alpine:3
1515
# Add mailhog user/group with uid/gid 1000.

mailhog-server-api/api.go

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package api
2+
3+
import (
4+
gohttp "net/http"
5+
6+
"github.com/gorilla/pat"
7+
"github.com/mailhog/MailHog-Server/config"
8+
)
9+
10+
func CreateAPI(conf *config.Config, r gohttp.Handler) {
11+
apiv1 := createAPIv1(conf, r.(*pat.Router))
12+
apiv2 := createAPIv2(conf, r.(*pat.Router))
13+
14+
go func() {
15+
for {
16+
select {
17+
case msg := <-conf.MessageChan:
18+
apiv1.messageChan <- msg
19+
apiv2.messageChan <- msg
20+
}
21+
}
22+
}()
23+
}

mailhog-server-api/v1.go

+359
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,359 @@
1+
package api
2+
3+
import (
4+
"encoding/base64"
5+
"encoding/json"
6+
"net/http"
7+
"net/smtp"
8+
"strconv"
9+
"strings"
10+
"time"
11+
12+
"github.com/gorilla/pat"
13+
"github.com/ian-kent/go-log/log"
14+
"github.com/mailhog/MailHog-Server/config"
15+
"github.com/mailhog/data"
16+
"github.com/mailhog/storage"
17+
18+
"github.com/ian-kent/goose"
19+
)
20+
21+
// APIv1 implements version 1 of the MailHog API
22+
//
23+
// The specification has been frozen and will eventually be deprecated.
24+
// Only bug fixes and non-breaking changes will be applied here.
25+
//
26+
// Any changes/additions should be added in APIv2.
27+
type APIv1 struct {
28+
config *config.Config
29+
messageChan chan *data.Message
30+
}
31+
32+
// FIXME should probably move this into APIv1 struct
33+
var stream *goose.EventStream
34+
35+
// ReleaseConfig is an alias to preserve go package API
36+
type ReleaseConfig config.OutgoingSMTP
37+
38+
func createAPIv1(conf *config.Config, r *pat.Router) *APIv1 {
39+
log.Println("Creating API v1 with WebPath: " + conf.WebPath)
40+
apiv1 := &APIv1{
41+
config: conf,
42+
messageChan: make(chan *data.Message),
43+
}
44+
45+
stream = goose.NewEventStream()
46+
47+
r.Path(conf.WebPath + "/api/v1/messages").Methods("GET").HandlerFunc(apiv1.messages)
48+
r.Path(conf.WebPath + "/api/v1/messages").Methods("DELETE").HandlerFunc(apiv1.delete_all)
49+
r.Path(conf.WebPath + "/api/v1/messages").Methods("OPTIONS").HandlerFunc(apiv1.defaultOptions)
50+
51+
r.Path(conf.WebPath + "/api/v1/messages/{id}").Methods("GET").HandlerFunc(apiv1.message)
52+
r.Path(conf.WebPath + "/api/v1/messages/{id}").Methods("DELETE").HandlerFunc(apiv1.delete_one)
53+
r.Path(conf.WebPath + "/api/v1/messages/{id}").Methods("OPTIONS").HandlerFunc(apiv1.defaultOptions)
54+
55+
r.Path(conf.WebPath + "/api/v1/messages/{id}/download").Methods("GET").HandlerFunc(apiv1.download)
56+
r.Path(conf.WebPath + "/api/v1/messages/{id}/download").Methods("OPTIONS").HandlerFunc(apiv1.defaultOptions)
57+
58+
r.Path(conf.WebPath + "/api/v1/messages/{id}/mime/part/{part}/download").Methods("GET").HandlerFunc(apiv1.download_part)
59+
r.Path(conf.WebPath + "/api/v1/messages/{id}/mime/part/{part}/download").Methods("OPTIONS").HandlerFunc(apiv1.defaultOptions)
60+
61+
r.Path(conf.WebPath + "/api/v1/messages/{id}/release").Methods("POST").HandlerFunc(apiv1.release_one)
62+
r.Path(conf.WebPath + "/api/v1/messages/{id}/release").Methods("OPTIONS").HandlerFunc(apiv1.defaultOptions)
63+
64+
r.Path(conf.WebPath + "/api/v1/events").Methods("GET").HandlerFunc(apiv1.eventstream)
65+
r.Path(conf.WebPath + "/api/v1/events").Methods("OPTIONS").HandlerFunc(apiv1.defaultOptions)
66+
67+
go func() {
68+
keepaliveTicker := time.Tick(time.Minute)
69+
for {
70+
select {
71+
case msg := <-apiv1.messageChan:
72+
log.Println("Got message in APIv1 event stream")
73+
bytes, _ := json.MarshalIndent(msg, "", " ")
74+
json := string(bytes)
75+
log.Printf("Sending content: %s\n", json)
76+
apiv1.broadcast(json)
77+
case <-keepaliveTicker:
78+
apiv1.keepalive()
79+
}
80+
}
81+
}()
82+
83+
return apiv1
84+
}
85+
86+
func (apiv1 *APIv1) defaultOptions(w http.ResponseWriter, req *http.Request) {
87+
if len(apiv1.config.CORSOrigin) > 0 {
88+
w.Header().Add("Access-Control-Allow-Origin", apiv1.config.CORSOrigin)
89+
w.Header().Add("Access-Control-Allow-Methods", "OPTIONS,GET,POST,DELETE")
90+
w.Header().Add("Access-Control-Allow-Headers", "Content-Type")
91+
}
92+
}
93+
94+
func (apiv1 *APIv1) broadcast(json string) {
95+
log.Println("[APIv1] BROADCAST /api/v1/events")
96+
b := []byte(json)
97+
stream.Notify("data", b)
98+
}
99+
100+
// keepalive sends an empty keep alive message.
101+
//
102+
// This not only can keep connections alive, but also will detect broken
103+
// connections. Without this it is possible for the server to become
104+
// unresponsive due to too many open files.
105+
func (apiv1 *APIv1) keepalive() {
106+
log.Println("[APIv1] KEEPALIVE /api/v1/events")
107+
stream.Notify("keepalive", []byte{})
108+
}
109+
110+
func (apiv1 *APIv1) eventstream(w http.ResponseWriter, req *http.Request) {
111+
log.Println("[APIv1] GET /api/v1/events")
112+
113+
//apiv1.defaultOptions(session)
114+
if len(apiv1.config.CORSOrigin) > 0 {
115+
w.Header().Add("Access-Control-Allow-Origin", apiv1.config.CORSOrigin)
116+
w.Header().Add("Access-Control-Allow-Methods", "OPTIONS,GET,POST,DELETE")
117+
}
118+
119+
stream.AddReceiver(w)
120+
}
121+
122+
func (apiv1 *APIv1) messages(w http.ResponseWriter, req *http.Request) {
123+
log.Println("[APIv1] GET /api/v1/messages")
124+
125+
apiv1.defaultOptions(w, req)
126+
127+
// TODO start, limit
128+
switch apiv1.config.Storage.(type) {
129+
case *storage.MongoDB:
130+
messages, _ := apiv1.config.Storage.(*storage.MongoDB).List(0, 1000)
131+
bytes, _ := json.Marshal(messages)
132+
w.Header().Add("Content-Type", "text/json")
133+
w.Write(bytes)
134+
case *storage.InMemory:
135+
messages, _ := apiv1.config.Storage.(*storage.InMemory).List(0, 1000)
136+
bytes, _ := json.Marshal(messages)
137+
w.Header().Add("Content-Type", "text/json")
138+
w.Write(bytes)
139+
default:
140+
w.WriteHeader(500)
141+
}
142+
}
143+
144+
func (apiv1 *APIv1) message(w http.ResponseWriter, req *http.Request) {
145+
id := req.URL.Query().Get(":id")
146+
log.Printf("[APIv1] GET /api/v1/messages/%s\n", id)
147+
148+
apiv1.defaultOptions(w, req)
149+
150+
message, err := apiv1.config.Storage.Load(id)
151+
if err != nil {
152+
log.Printf("- Error: %s", err)
153+
w.WriteHeader(500)
154+
return
155+
}
156+
157+
bytes, err := json.Marshal(message)
158+
if err != nil {
159+
log.Printf("- Error: %s", err)
160+
w.WriteHeader(500)
161+
return
162+
}
163+
164+
w.Header().Set("Content-Type", "text/json")
165+
w.Write(bytes)
166+
}
167+
168+
func (apiv1 *APIv1) download(w http.ResponseWriter, req *http.Request) {
169+
id := req.URL.Query().Get(":id")
170+
log.Printf("[APIv1] GET /api/v1/messages/%s\n", id)
171+
172+
apiv1.defaultOptions(w, req)
173+
174+
w.Header().Set("Content-Type", "message/rfc822")
175+
w.Header().Set("Content-Disposition", "attachment; filename=\""+id+".eml\"")
176+
177+
switch apiv1.config.Storage.(type) {
178+
case *storage.MongoDB:
179+
message, _ := apiv1.config.Storage.(*storage.MongoDB).Load(id)
180+
for h, l := range message.Content.Headers {
181+
for _, v := range l {
182+
w.Write([]byte(h + ": " + v + "\r\n"))
183+
}
184+
}
185+
w.Write([]byte("\r\n" + message.Content.Body))
186+
case *storage.InMemory:
187+
message, _ := apiv1.config.Storage.(*storage.InMemory).Load(id)
188+
for h, l := range message.Content.Headers {
189+
for _, v := range l {
190+
w.Write([]byte(h + ": " + v + "\r\n"))
191+
}
192+
}
193+
w.Write([]byte("\r\n" + message.Content.Body))
194+
default:
195+
w.WriteHeader(500)
196+
}
197+
}
198+
199+
func (apiv1 *APIv1) download_part(w http.ResponseWriter, req *http.Request) {
200+
id := req.URL.Query().Get(":id")
201+
part := req.URL.Query().Get(":part")
202+
log.Printf("[APIv1] GET /api/v1/messages/%s/mime/part/%s/download\n", id, part)
203+
204+
// TODO extension from content-type?
205+
apiv1.defaultOptions(w, req)
206+
207+
w.Header().Set("Content-Disposition", "attachment; filename=\""+id+"-part-"+part+"\"")
208+
209+
message, _ := apiv1.config.Storage.Load(id)
210+
contentTransferEncoding := ""
211+
pid, _ := strconv.Atoi(part)
212+
for h, l := range message.MIME.Parts[pid].Headers {
213+
for _, v := range l {
214+
switch strings.ToLower(h) {
215+
case "content-disposition":
216+
// Prevent duplicate "content-disposition"
217+
w.Header().Set(h, v)
218+
case "content-transfer-encoding":
219+
if contentTransferEncoding == "" {
220+
contentTransferEncoding = v
221+
}
222+
fallthrough
223+
default:
224+
w.Header().Add(h, v)
225+
}
226+
}
227+
}
228+
body := []byte(message.MIME.Parts[pid].Body)
229+
if strings.ToLower(contentTransferEncoding) == "base64" {
230+
var e error
231+
body, e = base64.StdEncoding.DecodeString(message.MIME.Parts[pid].Body)
232+
if e != nil {
233+
log.Printf("[APIv1] Decoding base64 encoded body failed: %s", e)
234+
}
235+
}
236+
w.Write(body)
237+
}
238+
239+
func (apiv1 *APIv1) delete_all(w http.ResponseWriter, req *http.Request) {
240+
log.Println("[APIv1] POST /api/v1/messages")
241+
242+
apiv1.defaultOptions(w, req)
243+
244+
w.Header().Add("Content-Type", "text/json")
245+
246+
err := apiv1.config.Storage.DeleteAll()
247+
if err != nil {
248+
log.Println(err)
249+
w.WriteHeader(500)
250+
return
251+
}
252+
253+
w.WriteHeader(200)
254+
}
255+
256+
func (apiv1 *APIv1) release_one(w http.ResponseWriter, req *http.Request) {
257+
id := req.URL.Query().Get(":id")
258+
log.Printf("[APIv1] POST /api/v1/messages/%s/release\n", id)
259+
260+
apiv1.defaultOptions(w, req)
261+
262+
w.Header().Add("Content-Type", "text/json")
263+
msg, _ := apiv1.config.Storage.Load(id)
264+
265+
decoder := json.NewDecoder(req.Body)
266+
var cfg ReleaseConfig
267+
err := decoder.Decode(&cfg)
268+
if err != nil {
269+
log.Printf("Error decoding request body: %s", err)
270+
w.WriteHeader(500)
271+
w.Write([]byte("Error decoding request body"))
272+
return
273+
}
274+
275+
log.Printf("%+v", cfg)
276+
277+
log.Printf("Got message: %s", msg.ID)
278+
279+
if cfg.Save {
280+
if _, ok := apiv1.config.OutgoingSMTP[cfg.Name]; ok {
281+
log.Printf("Server already exists named %s", cfg.Name)
282+
w.WriteHeader(400)
283+
return
284+
}
285+
cf := config.OutgoingSMTP(cfg)
286+
apiv1.config.OutgoingSMTP[cfg.Name] = &cf
287+
log.Printf("Saved server with name %s", cfg.Name)
288+
}
289+
290+
if len(cfg.Name) > 0 {
291+
if c, ok := apiv1.config.OutgoingSMTP[cfg.Name]; ok {
292+
log.Printf("Using server with name: %s", cfg.Name)
293+
cfg.Name = c.Name
294+
if len(cfg.Email) == 0 {
295+
cfg.Email = c.Email
296+
}
297+
cfg.Host = c.Host
298+
cfg.Port = c.Port
299+
cfg.Username = c.Username
300+
cfg.Password = c.Password
301+
cfg.Mechanism = c.Mechanism
302+
} else {
303+
log.Printf("Server not found: %s", cfg.Name)
304+
w.WriteHeader(400)
305+
return
306+
}
307+
}
308+
309+
log.Printf("Releasing to %s (via %s:%s)", cfg.Email, cfg.Host, cfg.Port)
310+
311+
bytes := make([]byte, 0)
312+
for h, l := range msg.Content.Headers {
313+
for _, v := range l {
314+
bytes = append(bytes, []byte(h+": "+v+"\r\n")...)
315+
}
316+
}
317+
bytes = append(bytes, []byte("\r\n"+msg.Content.Body)...)
318+
319+
var auth smtp.Auth
320+
321+
if len(cfg.Username) > 0 || len(cfg.Password) > 0 {
322+
log.Printf("Found username/password, using auth mechanism: [%s]", cfg.Mechanism)
323+
switch cfg.Mechanism {
324+
case "CRAMMD5":
325+
auth = smtp.CRAMMD5Auth(cfg.Username, cfg.Password)
326+
case "PLAIN":
327+
auth = smtp.PlainAuth("", cfg.Username, cfg.Password, cfg.Host)
328+
default:
329+
log.Printf("Error - invalid authentication mechanism")
330+
w.WriteHeader(400)
331+
return
332+
}
333+
}
334+
335+
err = smtp.SendMail(cfg.Host+":"+cfg.Port, auth, "nobody@"+apiv1.config.Hostname, []string{cfg.Email}, bytes)
336+
if err != nil {
337+
log.Printf("Failed to release message: %s", err)
338+
w.WriteHeader(500)
339+
return
340+
}
341+
log.Printf("Message released successfully")
342+
}
343+
344+
func (apiv1 *APIv1) delete_one(w http.ResponseWriter, req *http.Request) {
345+
id := req.URL.Query().Get(":id")
346+
347+
log.Printf("[APIv1] POST /api/v1/messages/%s/delete\n", id)
348+
349+
apiv1.defaultOptions(w, req)
350+
351+
w.Header().Add("Content-Type", "text/json")
352+
err := apiv1.config.Storage.DeleteOne(id)
353+
if err != nil {
354+
log.Println(err)
355+
w.WriteHeader(500)
356+
return
357+
}
358+
w.WriteHeader(200)
359+
}

0 commit comments

Comments
 (0)