diff --git a/server/apiv1/swagger.go b/server/apiv1/swagger.go index ee83f01..43e0646 100644 --- a/server/apiv1/swagger.go +++ b/server/apiv1/swagger.go @@ -81,6 +81,13 @@ type textResponse struct { Body string } +// HTML response +// swagger:response HTMLResponse +type htmlResponse struct { + // in: body + Body string +} + // Error response // swagger:response ErrorResponse type errorResponse struct { diff --git a/server/handlers/message-rendered.go b/server/handlers/message-rendered.go new file mode 100644 index 0000000..48894d3 --- /dev/null +++ b/server/handlers/message-rendered.go @@ -0,0 +1,163 @@ +package handlers + +import ( + "fmt" + "net/http" + "regexp" + "strings" + + "github.com/axllent/mailpit/config" + "github.com/axllent/mailpit/internal/storage" + "github.com/gorilla/mux" +) + +// GetMessageHTML (method: GET) returns a rendered version of a message's HTML part +func GetMessageHTML(w http.ResponseWriter, r *http.Request) { + // swagger:route GET /view/{ID}.html testing GetMessageHTML + // + // # Render message HTML part + // + // Renders just the message's HTML part which can be used for UI integration testing. + // Attached inline images are modified to link to the API provided they exist. + // Note that is the message does not contain a HTML part then an 404 error is returned. + // + // The ID can be set to `latest` to return the latest message. + // + // Produces: + // - text/html + // + // Schemes: http, https + // + // Parameters: + // + name: ID + // in: path + // description: Database ID or latest + // required: true + // type: string + // + // Responses: + // 200: HTMLResponse + // default: ErrorResponse + + vars := mux.Vars(r) + + id := vars["id"] + + if id == "latest" { + messages, err := storage.List(0, 1) + if err != nil { + httpError(w, err.Error()) + return + } + + if len(messages) == 0 { + w.WriteHeader(404) + fmt.Fprint(w, "Message not found") + return + } + + id = messages[0].ID + } + + msg, err := storage.GetMessage(id) + if err != nil { + w.WriteHeader(404) + fmt.Fprint(w, "Message not found") + return + } + if msg.HTML == "" { + w.WriteHeader(404) + fmt.Fprint(w, "This message does not contain a HTML part") + return + } + + html := linkInlinedImages(msg) + w.Header().Add("Content-Type", "text/html; charset=utf-8") + _, _ = w.Write([]byte(html)) +} + +// GetMessageText (method: GET) returns a message's text part +func GetMessageText(w http.ResponseWriter, r *http.Request) { + // swagger:route GET /view/{ID}.txt testing GetMessageText + // + // # Render message text part + // + // Renders just the message's text part which can be used for UI integration testing. + // + // The ID can be set to `latest` to return the latest message. + // + // Produces: + // - text/plain + // + // Schemes: http, https + // + // Parameters: + // + name: ID + // in: path + // description: Database ID or latest + // required: true + // type: string + // + // Responses: + // 200: TextResponse + // default: ErrorResponse + + vars := mux.Vars(r) + + id := vars["id"] + + if id == "latest" { + messages, err := storage.List(0, 1) + if err != nil { + httpError(w, err.Error()) + return + } + + if len(messages) == 0 { + w.WriteHeader(404) + fmt.Fprint(w, "Message not found") + return + } + + id = messages[0].ID + } + + msg, err := storage.GetMessage(id) + if err != nil { + w.WriteHeader(404) + fmt.Fprint(w, "Message not found") + return + } + + w.Header().Add("Content-Type", "text/plain; charset=utf-8") + _, _ = w.Write([]byte(msg.Text)) +} + +// This will remap all attachment images with relative paths +func linkInlinedImages(msg *storage.Message) string { + html := msg.HTML + + for _, a := range msg.Inline { + if a.ContentID != "" { + re := regexp.MustCompile(`(?i)(=["\']?)(cid:` + regexp.QuoteMeta(a.ContentID) + `)(["|\'|\\s|\\/|>|;])`) + u := config.Webroot + "api/v1/message/" + msg.ID + "/part/" + a.PartID + matches := re.FindAllStringSubmatch(html, -1) + for _, m := range matches { + html = strings.ReplaceAll(html, m[0], m[1]+u+m[3]) + } + } + } + + for _, a := range msg.Attachments { + if a.ContentID != "" { + re := regexp.MustCompile(`(?i)(=["\']?)(cid:` + regexp.QuoteMeta(a.ContentID) + `)(["|\'|\\s|\\/|>|;])`) + u := config.Webroot + "api/v1/message/" + msg.ID + "/part/" + a.PartID + matches := re.FindAllStringSubmatch(html, -1) + for _, m := range matches { + html = strings.ReplaceAll(html, m[0], m[1]+u+m[3]) + } + } + } + + return html +} diff --git a/server/server.go b/server/server.go index 7beb4c5..71d1f03 100644 --- a/server/server.go +++ b/server/server.go @@ -67,8 +67,14 @@ func Listen() { r.HandleFunc(redirect, middleWareFunc(addSlashToWebroot)).Methods("GET") } - // handle everything else with the virtual index.html - r.PathPrefix(config.Webroot).Handler(middleWareFunc(index)).Methods("GET") + // frontend testing + r.HandleFunc(config.Webroot+"view/{id}.html", handlers.GetMessageHTML).Methods("GET") + r.HandleFunc(config.Webroot+"view/{id}.txt", handlers.GetMessageText).Methods("GET") + + // web UI via virtual index.html + r.PathPrefix(config.Webroot + "view/").Handler(middleWareFunc(index)).Methods("GET") + r.Path(config.Webroot + "search").Handler(middleWareFunc(index)).Methods("GET") + r.Path(config.Webroot).Handler(middleWareFunc(index)).Methods("GET") // put it all together http.Handle("/", r) @@ -293,10 +299,6 @@ func index(w http.ResponseWriter, _ *http.Request) { buff.Bytes() - // f, err := embeddedFS.ReadFile("public/index.html") - // if err != nil { - // panic(err) - // } w.Header().Add("Content-Type", "text/html") _, _ = w.Write(buff.Bytes()) } diff --git a/server/ui/api/v1/swagger.json b/server/ui/api/v1/swagger.json index 825bde4..d0a85a5 100644 --- a/server/ui/api/v1/swagger.json +++ b/server/ui/api/v1/swagger.json @@ -654,6 +654,74 @@ } } } + }, + "/view/{ID}.html": { + "get": { + "description": "Renders just the message's HTML part which can be used for UI integration testing.\nAttached inline images are modified to link to the API provided they exist.\nNote that is the message does not contain a HTML part then an 404 error is returned.\n\nThe ID can be set to `latest` to return the latest message.", + "produces": [ + "text/html" + ], + "schemes": [ + "http", + "https" + ], + "tags": [ + "testing" + ], + "summary": "Render message HTML part", + "operationId": "GetMessageHTML", + "parameters": [ + { + "type": "string", + "description": "Database ID or latest", + "name": "ID", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "$ref": "#/responses/HTMLResponse" + }, + "default": { + "$ref": "#/responses/ErrorResponse" + } + } + } + }, + "/view/{ID}.txt": { + "get": { + "description": "Renders just the message's text part which can be used for UI integration testing.\n\nThe ID can be set to `latest` to return the latest message.", + "produces": [ + "text/plain" + ], + "schemes": [ + "http", + "https" + ], + "tags": [ + "testing" + ], + "summary": "Render message text part", + "operationId": "GetMessageText", + "parameters": [ + { + "type": "string", + "description": "Database ID or latest", + "name": "ID", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "$ref": "#/responses/TextResponse" + }, + "default": { + "$ref": "#/responses/ErrorResponse" + } + } + } } }, "definitions": { @@ -1300,6 +1368,9 @@ "ErrorResponse": { "description": "Error response" }, + "HTMLResponse": { + "description": "HTML response" + }, "InfoResponse": { "description": "Application information", "schema": {