diff --git a/internal/tools/html.go b/internal/tools/html.go index 6e5e883..753835b 100644 --- a/internal/tools/html.go +++ b/internal/tools/html.go @@ -17,3 +17,34 @@ func GetHTMLAttributeVal(e *html.Node, key string) (string, error) { return "", fmt.Errorf("%s not found", key) } + +// SetHTMLAttributeVal sets an attribute on a node. +func SetHTMLAttributeVal(n *html.Node, key, val string) { + for i := range n.Attr { + a := &n.Attr[i] + if a.Key == key { + a.Val = val + return + } + } + n.Attr = append(n.Attr, html.Attribute{ + Key: key, + Val: val, + }) +} + +// WalkHTML traverses the entire HTML tree and calls fn on each node. +func WalkHTML(n *html.Node, fn func(*html.Node)) { + if n == nil { + return + } + + fn(n) + + // Each node has a pointer to its first child and next sibling. To traverse + // all children of a node, we need to start from its first child and then + // traverse the next sibling until nil. + for c := n.FirstChild; c != nil; c = c.NextSibling { + WalkHTML(c, fn) + } +} diff --git a/server/apiv1/testing.go b/server/apiv1/testing.go index 7c005d5..6eadbc8 100644 --- a/server/apiv1/testing.go +++ b/server/apiv1/testing.go @@ -1,14 +1,19 @@ package apiv1 import ( + "bytes" "fmt" "net/http" "regexp" "strings" "github.com/axllent/mailpit/config" + "github.com/axllent/mailpit/internal/logger" "github.com/axllent/mailpit/internal/storage" + "github.com/axllent/mailpit/internal/tools" "github.com/gorilla/mux" + "golang.org/x/net/html" + "golang.org/x/net/html/atom" ) // swagger:parameters GetMessageHTMLParams @@ -17,7 +22,19 @@ type getMessageHTMLParams struct { // // in: path // required: true + // example: B79PgsotENzGwk4CCbAcAq ID string + + // If this is route is to be embedded in an iframe, set embed to `1` in the URL to add `target="_blank"` and `rel="noreferrer noopener"` to all links. + // + // In addition, a small script will be added to the end of the document to post (postMessage()) the height of the document back to the parent window for optional iframe height resizing. + // + // Note that this will also *transform* the message into a full HTML document (if it isn't already), so this option is useful for viewing but not programmatic testing. + // + // in: query + // required: false + // type: string + Embed string `json:"embed"` } // GetMessageHTML (method: GET) returns a rendered version of a message's HTML part @@ -68,9 +85,43 @@ func GetMessageHTML(w http.ResponseWriter, r *http.Request) { return } - html := linkInlineImages(msg) + htmlStr := linkInlineImages(msg) + + // If embed=1 is set, then we will add target="_blank" and rel="noreferrer noopener" to all links + if r.URL.Query().Get("embed") == "1" { + doc, err := html.Parse(strings.NewReader(htmlStr)) + if err != nil { + logger.Log().Error(err.Error()) + } else { + // Walk the entire HTML tree. + tools.WalkHTML(doc, func(n *html.Node) { + if n.Type == html.ElementNode && n.DataAtom == atom.A { + // Set attributes on all anchors with external links. + tools.SetHTMLAttributeVal(n, "target", "_blank") + tools.SetHTMLAttributeVal(n, "rel", "noreferrer noopener") + } + + b := bytes.Buffer{} + html.Render(&b, doc) + htmlStr = b.String() + }) + + nonce := r.Header.Get("mp-nonce") + + js := `` + + htmlStr = strings.ReplaceAll(htmlStr, "", js+"") + } + } + w.Header().Add("Content-Type", "text/html; charset=utf-8") - _, _ = w.Write([]byte(html)) + _, _ = w.Write([]byte(htmlStr)) } // swagger:parameters GetMessageTextParams diff --git a/server/ui/api/v1/swagger.json b/server/ui/api/v1/swagger.json index f2e23d4..ad6e91b 100644 --- a/server/ui/api/v1/swagger.json +++ b/server/ui/api/v1/swagger.json @@ -998,10 +998,18 @@ "parameters": [ { "type": "string", + "example": "B79PgsotENzGwk4CCbAcAq", "description": "Message database ID or \"latest\"", "name": "ID", "in": "path", "required": true + }, + { + "type": "string", + "x-go-name": "Embed", + "description": "If this is route is to be embedded in an iframe, set embed to `1` in the URL to add `target=\"_blank\"` and `rel=\"noreferrer noopener\"` to all links.\n\nIn addition, a small script will be added to the end of the document to post (postMessage()) the height of the document back to the parent window for optional iframe height resizing.\n\nNote that this will also *transform* the message into a full HTML document (if it isn't already), so this option is useful for viewing but not programmatic testing.", + "name": "embed", + "in": "query" } ], "responses": {