mirror of
https://github.com/axllent/mailpit.git
synced 2025-08-15 20:13:16 +02:00
Feature: Add optional query parameter for HTML message iframe embedding (#434)
This commit is contained in:
@@ -17,3 +17,34 @@ func GetHTMLAttributeVal(e *html.Node, key string) (string, error) {
|
|||||||
|
|
||||||
return "", fmt.Errorf("%s not found", key)
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@@ -1,14 +1,19 @@
|
|||||||
package apiv1
|
package apiv1
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/axllent/mailpit/config"
|
"github.com/axllent/mailpit/config"
|
||||||
|
"github.com/axllent/mailpit/internal/logger"
|
||||||
"github.com/axllent/mailpit/internal/storage"
|
"github.com/axllent/mailpit/internal/storage"
|
||||||
|
"github.com/axllent/mailpit/internal/tools"
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
|
"golang.org/x/net/html"
|
||||||
|
"golang.org/x/net/html/atom"
|
||||||
)
|
)
|
||||||
|
|
||||||
// swagger:parameters GetMessageHTMLParams
|
// swagger:parameters GetMessageHTMLParams
|
||||||
@@ -17,7 +22,19 @@ type getMessageHTMLParams struct {
|
|||||||
//
|
//
|
||||||
// in: path
|
// in: path
|
||||||
// required: true
|
// required: true
|
||||||
|
// example: B79PgsotENzGwk4CCbAcAq
|
||||||
ID string
|
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
|
// 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
|
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 := `<script nonce="` + nonce + `">
|
||||||
|
if (typeof window.parent == "object") {
|
||||||
|
window.addEventListener('load', function () {
|
||||||
|
window.parent.postMessage({ messageHeight: document.body.scrollHeight}, "*")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>`
|
||||||
|
|
||||||
|
htmlStr = strings.ReplaceAll(htmlStr, "</body>", js+"</body>")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
w.Header().Add("Content-Type", "text/html; charset=utf-8")
|
w.Header().Add("Content-Type", "text/html; charset=utf-8")
|
||||||
_, _ = w.Write([]byte(html))
|
_, _ = w.Write([]byte(htmlStr))
|
||||||
}
|
}
|
||||||
|
|
||||||
// swagger:parameters GetMessageTextParams
|
// swagger:parameters GetMessageTextParams
|
||||||
|
@@ -998,10 +998,18 @@
|
|||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
"type": "string",
|
"type": "string",
|
||||||
|
"example": "B79PgsotENzGwk4CCbAcAq",
|
||||||
"description": "Message database ID or \"latest\"",
|
"description": "Message database ID or \"latest\"",
|
||||||
"name": "ID",
|
"name": "ID",
|
||||||
"in": "path",
|
"in": "path",
|
||||||
"required": true
|
"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": {
|
"responses": {
|
||||||
|
Reference in New Issue
Block a user