Skip to content

Commit

Permalink
Merge branch 'release/v1.8.3'
Browse files Browse the repository at this point in the history
  • Loading branch information
axllent committed Sep 6, 2023
2 parents 7c42540 + 2ebaaa0 commit d489675
Show file tree
Hide file tree
Showing 12 changed files with 485 additions and 161 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/release-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ jobs:
- run: npm run package

# build the binaries
- uses: wangyoucao577/go-release-action@v1.39
- uses: wangyoucao577/go-release-action@v1.40
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
goos: ${{ matrix.goos }}
Expand Down
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,18 @@

Notable changes to Mailpit will be documented in this file.

## [v1.8.3]

### Feature
- HTML screenshots

### Libs
- Update node modules

### UI
- Group message tabs on mobile


## [v1.8.2]

### Build
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,9 @@ Mailpit was originally **inspired** by MailHog which is now [no longer maintaine
- Web UI to view emails (formatted HTML, highlighted HTML source, text, headers, raw source and MIME attachments including image thumbnails)
- HTML check to test & score mail client compatibility with HTML emails
- Link check to test message links (HTML & text) & linked images
- Light & dark web UI theme with auto-detect
- Screenshots of HTML messages via web UI
- Mobile and tablet HTML preview toggle in desktop mode
- Light & dark web UI theme with auto-detect
- Advanced mail search ([see wiki](https://github.com/axllent/mailpit/wiki/Mail-search))
- Message tagging ([see wiki](https://github.com/axllent/mailpit/wiki/Tagging))
- Real-time web UI updates using web sockets for new mail
Expand Down
194 changes: 100 additions & 94 deletions package-lock.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 1 addition & 1 deletion server/handlers/k8healthz.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package handlers

import "net/http"

// Healthz is a liveness probe
// HealthzHandler is a liveness probe
func HealthzHandler(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusOK)
}
147 changes: 147 additions & 0 deletions server/handlers/proxy.go
Original file line number Diff line number Diff line change
@@ -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 <script src="//example.com/script.js">
if len(link) > 1 && link[0:2] == "//" {
base, err := url.Parse(baseURL)
if err != nil {
return link, err
}
link = base.Scheme + ":" + link
}

u, err := url.Parse(link)
if err != nil {
return link, err
}

// remove hashes
u.Fragment = ""

base, err := url.Parse(baseURL)
if err != nil {
return link, err
}

result := base.ResolveReference(u)

// ensure link is HTTP(S)
if result.Scheme != "http" && result.Scheme != "https" {
return link, fmt.Errorf("Invalid URL: %s", result.String())
}

return result.String(), nil
}

// HTTPError returns a basic error message (400 response)
func httpError(w http.ResponseWriter, msg string) {
w.Header().Set("Referrer-Policy", "no-referrer")
w.Header().Set("Content-Security-Policy", config.ContentSecurityPolicy)
w.WriteHeader(http.StatusBadRequest)
w.Header().Set("Content-Type", "text/plain")
fmt.Fprint(w, msg)
}
7 changes: 5 additions & 2 deletions server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,11 @@ func Listen() {
r := defaultRoutes()

// kubernetes probes
r.HandleFunc("/livez", handlers.HealthzHandler)
r.HandleFunc("/readyz", handlers.ReadyzHandler(isReady))
r.HandleFunc(config.Webroot+"livez", handlers.HealthzHandler)
r.HandleFunc(config.Webroot+"readyz", handlers.ReadyzHandler(isReady))

// proxy handler for screenshots
r.HandleFunc(config.Webroot+"proxy", middleWareFunc(handlers.ProxyHandler)).Methods("GET")

// web UI websocket
r.HandleFunc(config.Webroot+"api/events", apiWebsocket).Methods("GET")
Expand Down
57 changes: 39 additions & 18 deletions server/ui-src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import commonMixins from './mixins.js'
import Message from './templates/Message.vue'
import MessageSummary from './templates/MessageSummary.vue'
import MessageRelease from './templates/MessageRelease.vue'
import MessageScreenshot from './templates/MessageScreenshot.vue'
import MessageToast from './templates/MessageToast.vue'
import ThemeToggle from './templates/ThemeToggle.vue'
import moment from 'moment'
Expand All @@ -15,6 +16,7 @@ export default {
Message,
MessageSummary,
MessageRelease,
MessageScreenshot,
MessageToast,
ThemeToggle,
},
Expand Down Expand Up @@ -697,6 +699,10 @@ export default {
clearMessageToast: function () {
this.toastMessage = false
},
screenshotMessageHTML: function () {
this.$refs.MessageScreenshotRef.initScreenshot()
}
}
}
Expand Down Expand Up @@ -752,29 +758,42 @@ export default {
HTML body
</button>
</li>
<!-- <MessageScreenshot :message="message"></MessageScreenshot> -->
<li v-if="message.HTML">
<button class="dropdown-item" @click="screenshotMessageHTML()">
HTML screenshot
</button>
</li>
<li v-if="message.Text">
<button v-on:click="downloadMessageBody(message.Text, 'txt')" class="dropdown-item">
Text body
</button>
</li>
<li v-if="allAttachments(message).length">
<hr class="dropdown-divider">
</li>
<li v-for="part in allAttachments(message)">
<a :href="'api/v1/message/' + message.ID + '/part/' + part.PartID" type="button"
class="row m-0 dropdown-item d-flex" target="_blank"
:title="part.FileName != '' ? part.FileName : '[ unknown ]'" style="min-width: 350px">
<div class="col-auto p-0 pe-1">
<i class="bi" :class="attachmentIcon(part)"></i>
</div>
<div class="col text-truncate p-0 pe-1">
{{ part.FileName != '' ? part.FileName : '[ unknown ]' }}
</div>
<div class="col-auto text-muted small p-0">
{{ getFileSize(part.Size) }}
</div>
</a>
</li>
<template v-if="allAttachments(message).length">
<li>
<hr class="dropdown-divider">
</li>
<li>
<h6 class="dropdown-header">
Attachment<template v-if="allAttachments(message).length > 1">s</template>
</h6>
</li>
<li v-for="part in allAttachments(message)">
<a :href="'api/v1/message/' + message.ID + '/part/' + part.PartID" type="button"
class="row m-0 dropdown-item d-flex" target="_blank"
:title="part.FileName != '' ? part.FileName : '[ unknown ]'" style="min-width: 350px">
<div class="col-auto p-0 pe-1">
<i class="bi" :class="attachmentIcon(part)"></i>
</div>
<div class="col text-truncate p-0 pe-1">
{{ part.FileName != '' ? part.FileName : '[ unknown ]' }}
</div>
<div class="col-auto text-muted small p-0">
{{ getFileSize(part.Size) }}
</div>
</a>
</li>
</template>
</ul>
</div>
</div>
Expand Down Expand Up @@ -1203,4 +1222,6 @@ export default {
</div>

<MessageToast v-if="toastMessage" :message="toastMessage" @clearMessageToast="clearMessageToast"></MessageToast>

<MessageScreenshot ref="MessageScreenshotRef" :message="message"></MessageScreenshot>
</template>
5 changes: 5 additions & 0 deletions server/ui-src/assets/styles.scss
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,11 @@ body.blur {
}
}

// bootstrap5-tags
.tags-badge {
display: flex;
}

#DownloadBtn {
@include media-breakpoint-down(sm) {
position: static;
Expand Down
Loading

0 comments on commit d489675

Please sign in to comment.