2024-11-09 12:33:16 +13:00
package apiv1
import (
2025-02-05 15:25:15 +13:00
"bytes"
2024-11-09 12:33:16 +13:00
"fmt"
"net/http"
"regexp"
"strings"
"github.com/axllent/mailpit/config"
2025-02-05 15:25:15 +13:00
"github.com/axllent/mailpit/internal/logger"
2024-11-09 12:33:16 +13:00
"github.com/axllent/mailpit/internal/storage"
2025-02-05 15:25:15 +13:00
"github.com/axllent/mailpit/internal/tools"
2024-11-09 12:33:16 +13:00
"github.com/gorilla/mux"
2025-02-05 15:25:15 +13:00
"golang.org/x/net/html"
"golang.org/x/net/html/atom"
2024-11-09 12:33:16 +13:00
)
// swagger:parameters GetMessageHTMLParams
type getMessageHTMLParams struct {
// Message database ID or "latest"
//
// in: path
// required: true
ID string
2025-02-05 15:25:15 +13:00
// 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"`
2024-11-09 12:33:16 +13:00
}
// 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 GetMessageHTMLParams
//
// # 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
//
// Responses:
// 200: HTMLResponse
// 400: ErrorResponse
// 404: NotFoundResponse
vars := mux . Vars ( r )
id := vars [ "id" ]
if id == "latest" {
var err error
id , err = storage . LatestID ( r )
if err != nil {
w . WriteHeader ( 404 )
fmt . Fprint ( w , err . Error ())
return
}
}
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
}
2025-02-05 15:25:15 +13:00
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" )
}
})
2025-02-07 19:39:55 +13:00
b := bytes . Buffer {}
2025-02-08 15:15:25 +13:00
_ = html . Render ( & b , doc )
2025-02-07 19:39:55 +13:00
htmlStr = b . String ()
2025-02-05 15:25:15 +13:00
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>" )
}
}
2024-11-09 12:33:16 +13:00
w . Header (). Add ( "Content-Type" , "text/html; charset=utf-8" )
2025-02-05 15:25:15 +13:00
_ , _ = w . Write ([] byte ( htmlStr ))
2024-11-09 12:33:16 +13:00
}
// swagger:parameters GetMessageTextParams
type getMessageTextParams struct {
// Message database ID or "latest"
//
// in: path
// required: true
ID string
}
// GetMessageText (method: GET) returns a message's text part
func GetMessageText ( w http . ResponseWriter , r * http . Request ) {
// swagger:route GET /view/{ID}.txt testing GetMessageTextParams
//
// # 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
//
// Responses:
// 200: TextResponse
// 400: ErrorResponse
// 404: NotFoundResponse
vars := mux . Vars ( r )
id := vars [ "id" ]
if id == "latest" {
var err error
id , err = storage . LatestID ( r )
if err != nil {
w . WriteHeader ( 404 )
fmt . Fprint ( w , err . Error ())
return
}
}
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 rewrite all inline image paths to API URLs
func linkInlineImages ( 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
}