Skip to content

Commit 6eb1392

Browse files
committed
Chore: Replace gorilla/mux dependency with stdlib routing
1 parent d904e7f commit 6eb1392

9 files changed

Lines changed: 88 additions & 98 deletions

File tree

go.mod

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ require (
99
github.com/axllent/semver v1.0.0
1010
github.com/goccy/go-yaml v1.19.2
1111
github.com/gomarkdown/markdown v0.0.0-20260412113850-134a5b2cce7f
12-
github.com/gorilla/mux v1.8.1
1312
github.com/gorilla/websocket v1.5.3
1413
github.com/jhillyerd/enmime/v2 v2.3.0
1514
github.com/klauspost/compress v1.18.5

go.sum

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,6 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
4747
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
4848
github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
4949
github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0=
50-
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
51-
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
5250
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
5351
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
5452
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=

server/apiv1/message.go

Lines changed: 5 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import (
99
"net/url"
1010

1111
"github.com/axllent/mailpit/internal/storage"
12-
"github.com/gorilla/mux"
1312
)
1413

1514
// GetMessage (method: GET) returns the Message as JSON
@@ -32,9 +31,7 @@ func GetMessage(w http.ResponseWriter, r *http.Request) {
3231
// 400: ErrorResponse
3332
// 404: NotFoundResponse
3433

35-
vars := mux.Vars(r)
36-
37-
id := vars["id"]
34+
id := r.PathValue("id")
3835

3936
if id == "latest" {
4037
var err error
@@ -78,9 +75,7 @@ func GetHeaders(w http.ResponseWriter, r *http.Request) {
7875
// 400: ErrorResponse
7976
// 404: NotFoundResponse
8077

81-
vars := mux.Vars(r)
82-
83-
id := vars["id"]
78+
id := r.PathValue("id")
8479

8580
if id == "latest" {
8681
var err error
@@ -133,10 +128,8 @@ func DownloadAttachment(w http.ResponseWriter, r *http.Request) {
133128
// 400: ErrorResponse
134129
// 404: NotFoundResponse
135130

136-
vars := mux.Vars(r)
137-
138-
id := vars["id"]
139-
partID := vars["partID"]
131+
id := r.PathValue("id")
132+
partID := r.PathValue("partID")
140133

141134
if id == "latest" {
142135
var err error
@@ -183,9 +176,7 @@ func DownloadRaw(w http.ResponseWriter, r *http.Request) {
183176
// 400: ErrorResponse
184177
// 404: NotFoundResponse
185178

186-
vars := mux.Vars(r)
187-
188-
id := vars["id"]
179+
id := r.PathValue("id")
189180
dl := r.FormValue("dl")
190181

191182
if id == "latest" {

server/apiv1/other.go

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import (
1111
"github.com/axllent/mailpit/internal/linkcheck"
1212
"github.com/axllent/mailpit/internal/spamassassin"
1313
"github.com/axllent/mailpit/internal/storage"
14-
"github.com/gorilla/mux"
1514
"github.com/jhillyerd/enmime/v2"
1615
)
1716

@@ -35,8 +34,7 @@ func HTMLCheck(w http.ResponseWriter, r *http.Request) {
3534
// 400: ErrorResponse
3635
// 404: NotFoundResponse
3736

38-
vars := mux.Vars(r)
39-
id := vars["id"]
37+
id := r.PathValue("id")
4038

4139
if id == "latest" {
4240
var err error
@@ -105,8 +103,7 @@ func LinkCheck(w http.ResponseWriter, r *http.Request) {
105103
return
106104
}
107105

108-
vars := mux.Vars(r)
109-
id := vars["id"]
106+
id := r.PathValue("id")
110107

111108
if id == "latest" {
112109
var err error
@@ -158,8 +155,7 @@ func SpamAssassinCheck(w http.ResponseWriter, r *http.Request) {
158155
// 400: ErrorResponse
159156
// 404: NotFoundResponse
160157

161-
vars := mux.Vars(r)
162-
id := vars["id"]
158+
id := r.PathValue("id")
163159

164160
if id == "latest" {
165161
var err error

server/apiv1/release.go

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ import (
1313
"github.com/axllent/mailpit/internal/smtpd"
1414
"github.com/axllent/mailpit/internal/storage"
1515
"github.com/axllent/mailpit/internal/tools"
16-
"github.com/gorilla/mux"
1716
"github.com/lithammer/shortuuid/v4"
1817
)
1918

@@ -45,9 +44,7 @@ func ReleaseMessage(w http.ResponseWriter, r *http.Request) {
4544
return
4645
}
4746

48-
vars := mux.Vars(r)
49-
50-
id := vars["id"]
47+
id := r.PathValue("id")
5148

5249
msg, err := storage.GetMessageRaw(id)
5350
if err != nil {

server/apiv1/tags.go

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import (
66

77
"github.com/axllent/mailpit/internal/storage"
88
"github.com/axllent/mailpit/server/websockets"
9-
"github.com/gorilla/mux"
109
)
1110

1211
// GetAllTags (method: GET) will get all tags currently in use
@@ -97,9 +96,7 @@ func RenameTag(w http.ResponseWriter, r *http.Request) {
9796
// 200: OKResponse
9897
// 400: ErrorResponse
9998

100-
vars := mux.Vars(r)
101-
102-
tag := vars["tag"]
99+
tag := r.PathValue("tag")
103100

104101
decoder := json.NewDecoder(r.Body)
105102

@@ -141,9 +138,7 @@ func DeleteTag(w http.ResponseWriter, r *http.Request) {
141138
// 200: OKResponse
142139
// 400: ErrorResponse
143140

144-
vars := mux.Vars(r)
145-
146-
tag := vars["tag"]
141+
tag := r.PathValue("tag")
147142

148143
if err := storage.DeleteTag(tag); err != nil {
149144
httpError(w, err.Error())

server/apiv1/testing.go

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import (
1111
"github.com/axllent/mailpit/internal/logger"
1212
"github.com/axllent/mailpit/internal/storage"
1313
"github.com/axllent/mailpit/internal/tools"
14-
"github.com/gorilla/mux"
1514
"golang.org/x/net/html"
1615
"golang.org/x/net/html/atom"
1716
)
@@ -38,9 +37,8 @@ func GetMessageHTML(w http.ResponseWriter, r *http.Request) {
3837
// 400: ErrorResponse
3938
// 404: NotFoundResponse
4039

41-
vars := mux.Vars(r)
42-
43-
id := vars["id"]
40+
path := strings.TrimPrefix(r.URL.Path, config.Webroot+"view/")
41+
id := strings.TrimSuffix(path, ".html")
4442

4543
if id == "latest" {
4644
var err error
@@ -123,9 +121,8 @@ func GetMessageText(w http.ResponseWriter, r *http.Request) {
123121
// 400: ErrorResponse
124122
// 404: NotFoundResponse
125123

126-
vars := mux.Vars(r)
127-
128-
id := vars["id"]
124+
path := strings.TrimPrefix(r.URL.Path, config.Webroot+"view/")
125+
id := strings.TrimSuffix(path, ".txt")
129126

130127
if id == "latest" {
131128
var err error

server/apiv1/thumbnails.go

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ import (
1212

1313
"github.com/axllent/mailpit/internal/logger"
1414
"github.com/axllent/mailpit/internal/storage"
15-
"github.com/gorilla/mux"
1615
"github.com/jhillyerd/enmime/v2"
1716
"github.com/kovidgoyal/imaging"
1817
)
@@ -42,10 +41,8 @@ func Thumbnail(w http.ResponseWriter, r *http.Request) {
4241
// 200: BinaryResponse
4342
// 400: ErrorResponse
4443

45-
vars := mux.Vars(r)
46-
47-
id := vars["id"]
48-
partID := vars["partID"]
44+
id := r.PathValue("id")
45+
partID := r.PathValue("partID")
4946

5047
a, err := storage.GetAttachmentPart(id, partID)
5148
if err != nil {

server/server.go

Lines changed: 71 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,6 @@ import (
2828
"github.com/axllent/mailpit/server/apiv1"
2929
"github.com/axllent/mailpit/server/handlers"
3030
"github.com/axllent/mailpit/server/websockets"
31-
"github.com/gorilla/mux"
3231
"github.com/lithammer/shortuuid/v4"
3332
)
3433

@@ -64,37 +63,43 @@ func Listen() {
6463
r := apiRoutes()
6564

6665
// kubernetes probes
67-
r.HandleFunc(config.Webroot+"livez", handlers.HealthzHandler)
68-
r.HandleFunc(config.Webroot+"readyz", handlers.ReadyzHandler(isReady))
66+
r.HandleFunc("GET "+config.Webroot+"livez", handlers.HealthzHandler)
67+
r.HandleFunc("GET "+config.Webroot+"readyz", handlers.ReadyzHandler(isReady))
6968

7069
// proxy handler for screenshots
71-
r.HandleFunc(config.Webroot+"proxy", middleWareFunc(handlers.ProxyHandler)).Methods("GET")
70+
r.HandleFunc("GET "+config.Webroot+"proxy", middleWareFunc(handlers.ProxyHandler))
7271

7372
// virtual filesystem for /dist/ & some individual files
74-
r.PathPrefix(config.Webroot + "dist/").Handler(middleWareFunc(embedController))
75-
r.PathPrefix(config.Webroot + "api/").Handler(middleWareFunc(embedController))
76-
r.Path(config.Webroot + "favicon.ico").Handler(middleWareFunc(embedController))
77-
r.Path(config.Webroot + "favicon.svg").Handler(middleWareFunc(embedController))
78-
r.Path(config.Webroot + "mailpit.svg").Handler(middleWareFunc(embedController))
79-
r.Path(config.Webroot + "notification.png").Handler(middleWareFunc(embedController))
73+
r.Handle("GET "+config.Webroot+"dist/", middleWareFunc(embedController))
74+
r.Handle("GET "+config.Webroot+"api/", middleWareFunc(embedController))
75+
r.Handle("GET "+config.Webroot+"favicon.ico", middleWareFunc(embedController))
76+
r.Handle("GET "+config.Webroot+"favicon.svg", middleWareFunc(embedController))
77+
r.Handle("GET "+config.Webroot+"mailpit.svg", middleWareFunc(embedController))
78+
r.Handle("GET "+config.Webroot+"notification.png", middleWareFunc(embedController))
8079

8180
// redirect to webroot if no trailing slash
8281
if config.Webroot != "/" {
8382
redirect := strings.TrimRight(config.Webroot, "/")
84-
r.HandleFunc(redirect, middleWareFunc(addSlashToWebroot)).Methods("GET")
83+
r.HandleFunc("GET "+redirect, middleWareFunc(addSlashToWebroot))
8584
}
8685

8786
// UI shortcut
88-
r.HandleFunc(config.Webroot+"view/latest", middleWareFunc(handlers.RedirectToLatestMessage)).Methods("GET")
89-
90-
// frontend testing
91-
r.HandleFunc(config.Webroot+"view/{id}.html", middleWareFunc(apiv1.GetMessageHTML)).Methods("GET")
92-
r.HandleFunc(config.Webroot+"view/{id}.txt", middleWareFunc(apiv1.GetMessageText)).Methods("GET")
93-
94-
// web UI via virtual index.html
95-
r.PathPrefix(config.Webroot + "view/").Handler(middleWareFunc(index)).Methods("GET")
96-
r.Path(config.Webroot + "search").Handler(middleWareFunc(index)).Methods("GET")
97-
r.Path(config.Webroot).Handler(middleWareFunc(index)).Methods("GET")
87+
r.HandleFunc("GET "+config.Webroot+"view/latest", middleWareFunc(handlers.RedirectToLatestMessage))
88+
89+
// frontend testing + web UI via virtual index.html
90+
// Go's ServeMux wildcards must span a full path segment so {id}.html is invalid;
91+
// viewHandler dispatches on the path suffix instead.
92+
r.HandleFunc("GET "+config.Webroot+"view/", middleWareFunc(viewHandler))
93+
94+
r.Handle("GET "+config.Webroot+"search", middleWareFunc(index))
95+
// Exact-match the webroot; stdlib "/" is always a subtree so we guard inside.
96+
r.HandleFunc("GET "+config.Webroot, func(w http.ResponseWriter, r *http.Request) {
97+
if r.URL.Path != config.Webroot {
98+
http.NotFound(w, r)
99+
return
100+
}
101+
middleWareFunc(index)(w, r)
102+
})
98103

99104
if auth.UICredentials != nil {
100105
logger.Log().Info("[http] enabling basic authentication")
@@ -165,51 +170,51 @@ func Listen() {
165170
}
166171
}
167172

168-
func apiRoutes() *mux.Router {
169-
r := mux.NewRouter()
173+
func apiRoutes() *http.ServeMux {
174+
r := http.NewServeMux()
170175

171176
// API V1
172-
r.HandleFunc(config.Webroot+"api/v1/messages", middleWareFunc(apiv1.GetMessages)).Methods("GET")
173-
r.HandleFunc(config.Webroot+"api/v1/messages", middleWareFunc(apiv1.SetReadStatus)).Methods("PUT")
174-
r.HandleFunc(config.Webroot+"api/v1/messages", middleWareFunc(apiv1.DeleteMessages)).Methods("DELETE")
175-
r.HandleFunc(config.Webroot+"api/v1/search", middleWareFunc(apiv1.Search)).Methods("GET")
176-
r.HandleFunc(config.Webroot+"api/v1/search", middleWareFunc(apiv1.DeleteSearch)).Methods("DELETE")
177-
r.HandleFunc(config.Webroot+"api/v1/send", sendAPIAuthMiddleware(apiv1.SendMessageHandler)).Methods("POST")
178-
r.HandleFunc(config.Webroot+"api/v1/tags", middleWareFunc(apiv1.GetAllTags)).Methods("GET")
179-
r.HandleFunc(config.Webroot+"api/v1/tags", middleWareFunc(apiv1.SetMessageTags)).Methods("PUT")
180-
r.HandleFunc(config.Webroot+"api/v1/tags/{tag}", middleWareFunc(apiv1.RenameTag)).Methods("PUT")
181-
r.HandleFunc(config.Webroot+"api/v1/tags/{tag}", middleWareFunc(apiv1.DeleteTag)).Methods("DELETE")
182-
r.HandleFunc(config.Webroot+"api/v1/message/{id}/part/{partID}", middleWareFunc(apiv1.DownloadAttachment)).Methods("GET")
183-
r.HandleFunc(config.Webroot+"api/v1/message/{id}/part/{partID}/thumb", middleWareFunc(apiv1.Thumbnail)).Methods("GET")
184-
r.HandleFunc(config.Webroot+"api/v1/message/{id}/headers", middleWareFunc(apiv1.GetHeaders)).Methods("GET")
185-
r.HandleFunc(config.Webroot+"api/v1/message/{id}/raw", middleWareFunc(apiv1.DownloadRaw)).Methods("GET")
186-
r.HandleFunc(config.Webroot+"api/v1/message/{id}/release", middleWareFunc(apiv1.ReleaseMessage)).Methods("POST")
187-
r.HandleFunc(config.Webroot+"api/v1/message/{id}/html-check", middleWareFunc(apiv1.HTMLCheck)).Methods("GET")
188-
r.HandleFunc(config.Webroot+"api/v1/message/{id}/link-check", middleWareFunc(apiv1.LinkCheck)).Methods("GET")
177+
r.HandleFunc("GET "+config.Webroot+"api/v1/messages", middleWareFunc(apiv1.GetMessages))
178+
r.HandleFunc("PUT "+config.Webroot+"api/v1/messages", middleWareFunc(apiv1.SetReadStatus))
179+
r.HandleFunc("DELETE "+config.Webroot+"api/v1/messages", middleWareFunc(apiv1.DeleteMessages))
180+
r.HandleFunc("GET "+config.Webroot+"api/v1/search", middleWareFunc(apiv1.Search))
181+
r.HandleFunc("DELETE "+config.Webroot+"api/v1/search", middleWareFunc(apiv1.DeleteSearch))
182+
r.HandleFunc("POST "+config.Webroot+"api/v1/send", sendAPIAuthMiddleware(apiv1.SendMessageHandler))
183+
r.HandleFunc("GET "+config.Webroot+"api/v1/tags", middleWareFunc(apiv1.GetAllTags))
184+
r.HandleFunc("PUT "+config.Webroot+"api/v1/tags", middleWareFunc(apiv1.SetMessageTags))
185+
r.HandleFunc("PUT "+config.Webroot+"api/v1/tags/{tag}", middleWareFunc(apiv1.RenameTag))
186+
r.HandleFunc("DELETE "+config.Webroot+"api/v1/tags/{tag}", middleWareFunc(apiv1.DeleteTag))
187+
r.HandleFunc("GET "+config.Webroot+"api/v1/message/{id}/part/{partID}", middleWareFunc(apiv1.DownloadAttachment))
188+
r.HandleFunc("GET "+config.Webroot+"api/v1/message/{id}/part/{partID}/thumb", middleWareFunc(apiv1.Thumbnail))
189+
r.HandleFunc("GET "+config.Webroot+"api/v1/message/{id}/headers", middleWareFunc(apiv1.GetHeaders))
190+
r.HandleFunc("GET "+config.Webroot+"api/v1/message/{id}/raw", middleWareFunc(apiv1.DownloadRaw))
191+
r.HandleFunc("POST "+config.Webroot+"api/v1/message/{id}/release", middleWareFunc(apiv1.ReleaseMessage))
192+
r.HandleFunc("GET "+config.Webroot+"api/v1/message/{id}/html-check", middleWareFunc(apiv1.HTMLCheck))
193+
r.HandleFunc("GET "+config.Webroot+"api/v1/message/{id}/link-check", middleWareFunc(apiv1.LinkCheck))
189194
if config.EnableSpamAssassin != "" {
190-
r.HandleFunc(config.Webroot+"api/v1/message/{id}/sa-check", middleWareFunc(apiv1.SpamAssassinCheck)).Methods("GET")
195+
r.HandleFunc("GET "+config.Webroot+"api/v1/message/{id}/sa-check", middleWareFunc(apiv1.SpamAssassinCheck))
191196
}
192-
r.HandleFunc(config.Webroot+"api/v1/message/{id}", middleWareFunc(apiv1.GetMessage)).Methods("GET")
193-
r.HandleFunc(config.Webroot+"api/v1/info", middleWareFunc(apiv1.AppInfo)).Methods("GET")
194-
r.HandleFunc(config.Webroot+"api/v1/webui", middleWareFunc(apiv1.WebUIConfig)).Methods("GET")
195-
r.HandleFunc(config.Webroot+"api/v1/swagger.json", middleWareFunc(swaggerBasePath)).Methods("GET")
197+
r.HandleFunc("GET "+config.Webroot+"api/v1/message/{id}", middleWareFunc(apiv1.GetMessage))
198+
r.HandleFunc("GET "+config.Webroot+"api/v1/info", middleWareFunc(apiv1.AppInfo))
199+
r.HandleFunc("GET "+config.Webroot+"api/v1/webui", middleWareFunc(apiv1.WebUIConfig))
200+
r.HandleFunc("GET "+config.Webroot+"api/v1/swagger.json", middleWareFunc(swaggerBasePath))
196201

197202
// Chaos
198-
r.HandleFunc(config.Webroot+"api/v1/chaos", middleWareFunc(apiv1.GetChaos)).Methods("GET")
199-
r.HandleFunc(config.Webroot+"api/v1/chaos", middleWareFunc(apiv1.SetChaos)).Methods("PUT")
203+
r.HandleFunc("GET "+config.Webroot+"api/v1/chaos", middleWareFunc(apiv1.GetChaos))
204+
r.HandleFunc("PUT "+config.Webroot+"api/v1/chaos", middleWareFunc(apiv1.SetChaos))
200205

201206
// Prometheus metrics (if enabled and using existing server)
202207
if prometheus.GetMode() == "integrated" {
203-
r.HandleFunc(config.Webroot+"metrics", middleWareFunc(func(w http.ResponseWriter, r *http.Request) {
208+
r.HandleFunc("GET "+config.Webroot+"metrics", middleWareFunc(func(w http.ResponseWriter, r *http.Request) {
204209
prometheus.GetHandler().ServeHTTP(w, r)
205-
})).Methods("GET")
210+
}))
206211
}
207212

208213
// web UI websocket
209-
r.HandleFunc(config.Webroot+"api/events", middleWareFunc(apiWebsocket)).Methods("GET")
214+
r.HandleFunc("GET "+config.Webroot+"api/events", middleWareFunc(apiWebsocket))
210215

211216
// return blank 200 response for OPTIONS requests for CORS
212-
r.PathPrefix(config.Webroot + "api/v1/").Handler(middleWareFunc(apiv1.GetOptions)).Methods("OPTIONS")
217+
r.Handle("OPTIONS "+config.Webroot+"api/v1/", middleWareFunc(apiv1.GetOptions))
213218

214219
return r
215220
}
@@ -345,6 +350,21 @@ func addSlashToWebroot(w http.ResponseWriter, r *http.Request) {
345350
http.Redirect(w, r, config.Webroot, http.StatusFound)
346351
}
347352

353+
// viewHandler routes /view/ requests based on path suffix.
354+
// Go's ServeMux requires wildcards to span a full path segment,
355+
// so patterns like /view/{id}.html are invalid; we dispatch manually here.
356+
func viewHandler(w http.ResponseWriter, r *http.Request) {
357+
path := strings.TrimPrefix(r.URL.Path, config.Webroot+"view/")
358+
switch {
359+
case strings.HasSuffix(path, ".html"):
360+
apiv1.GetMessageHTML(w, r)
361+
case strings.HasSuffix(path, ".txt"):
362+
apiv1.GetMessageText(w, r)
363+
default:
364+
index(w, r)
365+
}
366+
}
367+
348368
// Websocket to broadcast changes.
349369
// Authentication and CORS are handled by middleWareFunc before this is reached.
350370
func apiWebsocket(w http.ResponseWriter, r *http.Request) {

0 commit comments

Comments
 (0)