diff --git a/package-lock.json b/package-lock.json
index 7feab22..12ddbdb 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -13,6 +13,7 @@
"bootstrap-icons": "^1.9.1",
"bootstrap5-tags": "^1.6.1",
"color-hash": "^2.0.2",
+ "modern-screenshot": "^4.4.30",
"moment": "^2.29.4",
"prismjs": "^1.29.0",
"rapidoc": "^9.3.4",
@@ -1717,6 +1718,11 @@
"integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==",
"optional": true
},
+ "node_modules/modern-screenshot": {
+ "version": "4.4.30",
+ "resolved": "https://registry.npmjs.org/modern-screenshot/-/modern-screenshot-4.4.30.tgz",
+ "integrity": "sha512-rC6SC40NEP04qZqthKM+W3xttz3NuNOpyMFMZ+P//zoBxsSrQbrBGYL/Sp1h6U9TKmIzyj3vEymVwdcwl7EEiA=="
+ },
"node_modules/moment": {
"version": "2.29.4",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz",
diff --git a/package.json b/package.json
index c5c12e4..7ebd43b 100644
--- a/package.json
+++ b/package.json
@@ -14,6 +14,7 @@
"bootstrap-icons": "^1.9.1",
"bootstrap5-tags": "^1.6.1",
"color-hash": "^2.0.2",
+ "modern-screenshot": "^4.4.30",
"moment": "^2.29.4",
"prismjs": "^1.29.0",
"rapidoc": "^9.3.4",
diff --git a/server/handlers/proxy.go b/server/handlers/proxy.go
new file mode 100644
index 0000000..292c9b6
--- /dev/null
+++ b/server/handlers/proxy.go
@@ -0,0 +1,147 @@
+// Package handlers contains a specific handlers
+package handlers
+
+import (
+ "fmt"
+ "io"
+ "net/http"
+ "net/url"
+ "regexp"
+ "strings"
+ "time"
+
+ "github.com/axllent/mailpit/config"
+ "github.com/axllent/mailpit/utils/logger"
+)
+
+var linkRe = regexp.MustCompile(`(?i)^https?:\/\/`)
+
+// ProxyHandler is used to proxy assets for printing
+func ProxyHandler(w http.ResponseWriter, r *http.Request) {
+ uri := strings.TrimSpace(r.URL.Query().Get("url"))
+ if uri == "" {
+ logger.Log().Warn("[proxy] URL missing")
+ httpError(w, "Error: URL missing")
+ return
+ }
+
+ if !linkRe.MatchString(uri) {
+ logger.Log().Warnf("[proxy] invalid URL %s", uri)
+ httpError(w, "Error: invalid URL")
+ return
+ }
+
+ client := &http.Client{
+ Timeout: 10 * time.Second,
+ }
+
+ req, err := http.NewRequest("GET", uri, nil)
+ if err != nil {
+ logger.Log().Warnf("[proxy] %s", err.Error())
+ httpError(w, err.Error())
+ return
+ }
+
+ // use requesting useragent
+ req.Header.Set("User-Agent", r.UserAgent())
+
+ resp, err := client.Do(req)
+ if err != nil {
+ logger.Log().Warnf("[proxy] %s", err.Error())
+ httpError(w, err.Error())
+ return
+ }
+
+ defer resp.Body.Close()
+ body, err := io.ReadAll(resp.Body)
+ if err != nil {
+ logger.Log().Warnf("[proxy] %s", err.Error())
+ httpError(w, err.Error())
+ return
+ }
+
+ // relay common headers
+ if resp.Header.Get("content-type") != "" {
+ w.Header().Set("content-type", resp.Header.Get("content-type"))
+ }
+ if resp.Header.Get("last-modified") != "" {
+ w.Header().Set("last-modified", resp.Header.Get("last-modified"))
+ }
+ if resp.Header.Get("content-disposition") != "" {
+ w.Header().Set("content-disposition", resp.Header.Get("content-disposition"))
+ }
+ if resp.Header.Get("cache-control") != "" {
+ w.Header().Set("cache-control", resp.Header.Get("cache-control"))
+ }
+
+ // replace url() values with proxy address, eg: fonts & images
+ if strings.HasPrefix(resp.Header.Get("content-type"), "text/css") {
+ var re = regexp.MustCompile(`(?mi)(url\((\'|\")?([^\)\'\"]+)(\'|\")?\))`)
+ body = re.ReplaceAllFunc(body, func(s []byte) []byte {
+ parts := re.FindStringSubmatch(string(s))
+
+ // don't resolve inline `data:..`
+ if strings.HasPrefix(parts[3], "data:") {
+ return []byte(parts[3])
+ }
+
+ address, err := absoluteURL(parts[3], uri)
+ if err != nil {
+ logger.Log().Error(err)
+ return []byte(parts[3])
+ }
+
+ return []byte("url(" + parts[2] + config.Webroot + "proxy?url=" + url.QueryEscape(address) + parts[4] + ")")
+ })
+ }
+
+ logger.Log().Debugf("[proxy] %s (%d)", uri, resp.StatusCode)
+
+ // relay status code - WriteHeader must come after Header.Set()
+ w.WriteHeader(resp.StatusCode)
+
+ w.Write(body)
+}
+
+// AbsoluteURL will return a full URL regardless whether it is relative or absolute
+func absoluteURL(link, baseURL string) (string, error) {
+ // scheme relative links, eg
+
+
+
+
+
+