1
0
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:
Ralph Slooten
2025-02-05 15:25:15 +13:00
parent f4d6dd5c39
commit 0c63c29769
3 changed files with 92 additions and 2 deletions

View File

@@ -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)
}
}

View File

@@ -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

View File

@@ -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": {