1
0
mirror of https://github.com/axllent/mailpit.git synced 2025-07-03 00:46:58 +02:00

Merge branch 'release/v1.2.7'

This commit is contained in:
Ralph Slooten
2022-10-31 22:15:25 +13:00
18 changed files with 86 additions and 46 deletions

View File

@ -2,6 +2,12 @@
Notable changes to Mailpit will be documented in this file. Notable changes to Mailpit will be documented in this file.
## v1.2.7
### Feature
- Allow custom webroot
## v1.2.6 ## v1.2.6
### API ### API

View File

@ -6,10 +6,10 @@ import (
"strconv" "strconv"
"github.com/axllent/mailpit/config" "github.com/axllent/mailpit/config"
"github.com/axllent/mailpit/logger"
"github.com/axllent/mailpit/server" "github.com/axllent/mailpit/server"
"github.com/axllent/mailpit/smtpd" "github.com/axllent/mailpit/smtpd"
"github.com/axllent/mailpit/storage" "github.com/axllent/mailpit/storage"
"github.com/axllent/mailpit/utils/logger"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@ -103,6 +103,9 @@ func init() {
if len(os.Getenv("MP_SMTP_SSL_KEY")) > 0 { if len(os.Getenv("MP_SMTP_SSL_KEY")) > 0 {
config.SMTPSSLKey = os.Getenv("MP_SMTP_SSL_KEY") config.SMTPSSLKey = os.Getenv("MP_SMTP_SSL_KEY")
} }
if len(os.Getenv("MP_WEBROOT")) > 0 {
config.Webroot = os.Getenv("MP_WEBROOT")
}
// deprecated 2022/08/06 // deprecated 2022/08/06
if len(os.Getenv("MP_AUTH_FILE")) > 0 { if len(os.Getenv("MP_AUTH_FILE")) > 0 {
@ -127,6 +130,7 @@ func init() {
rootCmd.Flags().StringVarP(&config.SMTPListen, "smtp", "s", config.SMTPListen, "SMTP bind interface and port") rootCmd.Flags().StringVarP(&config.SMTPListen, "smtp", "s", config.SMTPListen, "SMTP bind interface and port")
rootCmd.Flags().StringVarP(&config.HTTPListen, "listen", "l", config.HTTPListen, "HTTP bind interface and port for UI") rootCmd.Flags().StringVarP(&config.HTTPListen, "listen", "l", config.HTTPListen, "HTTP bind interface and port for UI")
rootCmd.Flags().IntVarP(&config.MaxMessages, "max", "m", config.MaxMessages, "Max number of messages to store") rootCmd.Flags().IntVarP(&config.MaxMessages, "max", "m", config.MaxMessages, "Max number of messages to store")
rootCmd.Flags().StringVar(&config.Webroot, "webroot", config.Webroot, "Set the webroot for web UI & API")
rootCmd.Flags().StringVar(&config.UIAuthFile, "ui-auth-file", config.UIAuthFile, "A password file for web UI authentication") rootCmd.Flags().StringVar(&config.UIAuthFile, "ui-auth-file", config.UIAuthFile, "A password file for web UI authentication")
rootCmd.Flags().StringVar(&config.UISSLCert, "ui-ssl-cert", config.UISSLCert, "SSL certificate for web UI - requires ui-ssl-key") rootCmd.Flags().StringVar(&config.UISSLCert, "ui-ssl-cert", config.UISSLCert, "SSL certificate for web UI - requires ui-ssl-key")

View File

@ -6,7 +6,7 @@ import (
"runtime" "runtime"
"github.com/axllent/mailpit/config" "github.com/axllent/mailpit/config"
"github.com/axllent/mailpit/updater" "github.com/axllent/mailpit/utils/updater"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )

View File

@ -3,9 +3,11 @@ package config
import ( import (
"errors" "errors"
"fmt" "fmt"
"net/url"
"os" "os"
"path/filepath" "path/filepath"
"regexp" "regexp"
"strings"
"github.com/tg123/go-htpasswd" "github.com/tg123/go-htpasswd"
) )
@ -44,6 +46,9 @@ var (
// UIAuth used for euthentication // UIAuth used for euthentication
UIAuth *htpasswd.File UIAuth *htpasswd.File
// Webroot to define the base path for the UI and API
Webroot = "/"
// SMTPSSLCert file // SMTPSSLCert file
SMTPSSLCert string SMTPSSLCert string
@ -139,6 +144,16 @@ func VerifyConfig() error {
SMTPAuth = a SMTPAuth = a
} }
if strings.Contains(Webroot, " ") {
return fmt.Errorf("Webroot cannot contain spaces (%s)", Webroot)
}
s, err := url.JoinPath("/", Webroot, "/")
if err != nil {
return err
}
Webroot = s
return nil return nil
} }

View File

@ -13,7 +13,7 @@ import (
"os" "os"
"os/user" "os/user"
"github.com/axllent/mailpit/logger" "github.com/axllent/mailpit/utils/logger"
flag "github.com/spf13/pflag" flag "github.com/spf13/pflag"
) )

View File

@ -8,7 +8,7 @@ import (
"github.com/axllent/mailpit/config" "github.com/axllent/mailpit/config"
"github.com/axllent/mailpit/storage" "github.com/axllent/mailpit/storage"
"github.com/axllent/mailpit/updater" "github.com/axllent/mailpit/utils/updater"
) )
type appVersion struct { type appVersion struct {

View File

@ -10,8 +10,8 @@ import (
"net/http" "net/http"
"strings" "strings"
"github.com/axllent/mailpit/logger"
"github.com/axllent/mailpit/storage" "github.com/axllent/mailpit/storage"
"github.com/axllent/mailpit/utils/logger"
"github.com/disintegration/imaging" "github.com/disintegration/imaging"
"github.com/gorilla/mux" "github.com/gorilla/mux"
"github.com/jhillyerd/enmime" "github.com/jhillyerd/enmime"

View File

@ -10,9 +10,9 @@ import (
"strings" "strings"
"github.com/axllent/mailpit/config" "github.com/axllent/mailpit/config"
"github.com/axllent/mailpit/logger"
"github.com/axllent/mailpit/server/apiv1" "github.com/axllent/mailpit/server/apiv1"
"github.com/axllent/mailpit/server/websockets" "github.com/axllent/mailpit/server/websockets"
"github.com/axllent/mailpit/utils/logger"
"github.com/gorilla/mux" "github.com/gorilla/mux"
) )
@ -34,10 +34,17 @@ func Listen() {
r := defaultRoutes() r := defaultRoutes()
// web UI websocket // web UI websocket
r.HandleFunc("/api/events", apiWebsocket).Methods("GET") r.HandleFunc(config.Webroot+"api/events", apiWebsocket).Methods("GET")
// virtual filesystem for others // virtual filesystem for others
r.PathPrefix("/").Handler(middlewareHandler(http.FileServer(http.FS(serverRoot)))) r.PathPrefix(config.Webroot).Handler(middlewareHandler(http.StripPrefix(config.Webroot, http.FileServer(http.FS(serverRoot)))))
// redirect to webroot if no trailing slash
if config.Webroot != "/" {
redir := strings.TrimRight(config.Webroot, "/")
r.HandleFunc(redir, middleWareFunc(addSlashToWebroot)).Methods("GET")
}
http.Handle("/", r) http.Handle("/", r)
if config.UIAuthFile != "" { if config.UIAuthFile != "" {
@ -45,10 +52,10 @@ func Listen() {
} }
if config.UISSLCert != "" && config.UISSLKey != "" { if config.UISSLCert != "" && config.UISSLKey != "" {
logger.Log().Infof("[http] starting secure server on https://%s", config.HTTPListen) logger.Log().Infof("[http] starting secure server on https://%s%s", config.HTTPListen, config.Webroot)
logger.Log().Fatal(http.ListenAndServeTLS(config.HTTPListen, config.UISSLCert, config.UISSLKey, nil)) logger.Log().Fatal(http.ListenAndServeTLS(config.HTTPListen, config.UISSLCert, config.UISSLKey, nil))
} else { } else {
logger.Log().Infof("[http] starting server on http://%s", config.HTTPListen) logger.Log().Infof("[http] starting server on http://%s%s", config.HTTPListen, config.Webroot)
logger.Log().Fatal(http.ListenAndServe(config.HTTPListen, nil)) logger.Log().Fatal(http.ListenAndServe(config.HTTPListen, nil))
} }
} }
@ -57,16 +64,16 @@ func defaultRoutes() *mux.Router {
r := mux.NewRouter() r := mux.NewRouter()
// API V1 // API V1
r.HandleFunc("/api/v1/messages", middleWareFunc(apiv1.GetMessages)).Methods("GET") r.HandleFunc(config.Webroot+"api/v1/messages", middleWareFunc(apiv1.GetMessages)).Methods("GET")
r.HandleFunc("/api/v1/messages", middleWareFunc(apiv1.SetReadStatus)).Methods("PUT") r.HandleFunc(config.Webroot+"api/v1/messages", middleWareFunc(apiv1.SetReadStatus)).Methods("PUT")
r.HandleFunc("/api/v1/messages", middleWareFunc(apiv1.DeleteMessages)).Methods("DELETE") r.HandleFunc(config.Webroot+"api/v1/messages", middleWareFunc(apiv1.DeleteMessages)).Methods("DELETE")
r.HandleFunc("/api/v1/search", middleWareFunc(apiv1.Search)).Methods("GET") r.HandleFunc(config.Webroot+"api/v1/search", middleWareFunc(apiv1.Search)).Methods("GET")
r.HandleFunc("/api/v1/message/{id}/part/{partID}", middleWareFunc(apiv1.DownloadAttachment)).Methods("GET") r.HandleFunc(config.Webroot+"api/v1/message/{id}/part/{partID}", middleWareFunc(apiv1.DownloadAttachment)).Methods("GET")
r.HandleFunc("/api/v1/message/{id}/part/{partID}/thumb", middleWareFunc(apiv1.Thumbnail)).Methods("GET") r.HandleFunc(config.Webroot+"api/v1/message/{id}/part/{partID}/thumb", middleWareFunc(apiv1.Thumbnail)).Methods("GET")
r.HandleFunc("/api/v1/message/{id}/raw", middleWareFunc(apiv1.DownloadRaw)).Methods("GET") r.HandleFunc(config.Webroot+"api/v1/message/{id}/raw", middleWareFunc(apiv1.DownloadRaw)).Methods("GET")
r.HandleFunc("/api/v1/message/{id}/headers", middleWareFunc(apiv1.Headers)).Methods("GET") r.HandleFunc(config.Webroot+"api/v1/message/{id}/headers", middleWareFunc(apiv1.Headers)).Methods("GET")
r.HandleFunc("/api/v1/message/{id}", middleWareFunc(apiv1.GetMessage)).Methods("GET") r.HandleFunc(config.Webroot+"api/v1/message/{id}", middleWareFunc(apiv1.GetMessage)).Methods("GET")
r.HandleFunc("/api/v1/info", middleWareFunc(apiv1.AppInfo)).Methods("GET") r.HandleFunc(config.Webroot+"api/v1/info", middleWareFunc(apiv1.AppInfo)).Methods("GET")
return r return r
} }
@ -152,6 +159,11 @@ func middlewareHandler(h http.Handler) http.Handler {
}) })
} }
// Redirect to webroot
func addSlashToWebroot(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, config.Webroot, http.StatusFound)
}
// Websocket to broadcast changes // Websocket to broadcast changes
func apiWebsocket(w http.ResponseWriter, r *http.Request) { func apiWebsocket(w http.ResponseWriter, r *http.Request) {
websockets.ServeWs(websockets.MessageHub, w, r) websockets.ServeWs(websockets.MessageHub, w, r)

View File

@ -195,14 +195,14 @@ export default {
if (a.ContentID != '') { if (a.ContentID != '') {
d.HTML = d.HTML.replace( d.HTML = d.HTML.replace(
new RegExp('cid:' + a.ContentID, 'g'), new RegExp('cid:' + a.ContentID, 'g'),
window.location.origin + '/api/v1/message/' + d.ID + '/part/' + a.PartID window.location.origin + window.location.pathname + 'api/v1/message/' + d.ID + '/part/' + a.PartID
); );
} }
if (a.FileName.match(/^[a-zA-Z0-9\_\-\.]+$/)) { if (a.FileName.match(/^[a-zA-Z0-9\_\-\.]+$/)) {
// some old email clients use the filename // some old email clients use the filename
d.HTML = d.HTML.replace( d.HTML = d.HTML.replace(
new RegExp('src=(\'|")' + a.FileName + '(\'|")', 'g'), new RegExp('src=(\'|")' + a.FileName + '(\'|")', 'g'),
'src="' + window.location.origin + '/api/v1/message/' + d.ID + '/part/' + a.PartID + '"' 'src="' + window.location.origin + window.location.pathname + 'api/v1/message/' + d.ID + '/part/' + a.PartID + '"'
); );
} }
} }
@ -214,14 +214,14 @@ export default {
if (a.ContentID != '') { if (a.ContentID != '') {
d.HTML = d.HTML.replace( d.HTML = d.HTML.replace(
new RegExp('cid:' + a.ContentID, 'g'), new RegExp('cid:' + a.ContentID, 'g'),
window.location.origin + '/api/v1/message/' + d.ID + '/part/' + a.PartID window.location.origin + window.location.pathname + 'api/v1/message/' + d.ID + '/part/' + a.PartID
); );
} }
if (a.FileName.match(/^[a-zA-Z0-9\_\-\.]+$/)) { if (a.FileName.match(/^[a-zA-Z0-9\_\-\.]+$/)) {
// some old email clients use the filename // some old email clients use the filename
d.HTML = d.HTML.replace( d.HTML = d.HTML.replace(
new RegExp('src=(\'|")' + a.FileName + '(\'|")', 'g'), new RegExp('src=(\'|")' + a.FileName + '(\'|")', 'g'),
'src="' + window.location.origin + '/api/v1/message/' + d.ID + '/part/' + a.PartID + '"' 'src="' + window.location.origin + window.location.pathname + 'api/v1/message/' + d.ID + '/part/' + a.PartID + '"'
); );
} }
} }
@ -587,7 +587,8 @@ export default {
<span v-else> <span v-else>
<small> <small>
{{ formatNumber(start + 1) }}-{{ formatNumber(start + items.length) }} <small>of</small> {{ {{ formatNumber(start + 1) }}-{{ formatNumber(start + items.length) }} <small>of</small> {{
formatNumber(total) }} formatNumber(total)
}}
</small> </small>
<button class="btn btn-outline-light ms-2 me-1" :disabled="!canPrev" v-on:click="viewPrev" <button class="btn btn-outline-light ms-2 me-1" :disabled="!canPrev" v-on:click="viewPrev"
v-if="!searching" :title="'View previous ' + limit + ' messages'"> v-if="!searching" :title="'View previous ' + limit + ' messages'">
@ -689,11 +690,13 @@ export default {
</div> </div>
<div class="text-truncate d-lg-none privacy"> <div class="text-truncate d-lg-none privacy">
<span v-if="message.From" :title="message.From.Address">{{ message.From.Name ? <span v-if="message.From" :title="message.From.Address">{{ message.From.Name ?
message.From.Name : message.From.Address }}</span> message.From.Name : message.From.Address
}}</span>
</div> </div>
<div class="text-truncate d-none d-lg-block privacy"> <div class="text-truncate d-none d-lg-block privacy">
<b v-if="message.From" :title="message.From.Address">{{ message.From.Name ? <b v-if="message.From" :title="message.From.Address">{{ message.From.Name ?
message.From.Name : message.From.Address }}</b> message.From.Name : message.From.Address
}}</b>
</div> </div>
<div class="d-none d-lg-block text-truncate text-muted small privacy"> <div class="d-none d-lg-block text-truncate text-muted small privacy">
{{ getPrimaryEmailTo(message) }} {{ getPrimaryEmailTo(message) }}

View File

@ -9,7 +9,7 @@ import (
"time" "time"
"github.com/axllent/mailpit/config" "github.com/axllent/mailpit/config"
"github.com/axllent/mailpit/logger" "github.com/axllent/mailpit/utils/logger"
"github.com/gorilla/websocket" "github.com/gorilla/websocket"
) )

View File

@ -7,7 +7,7 @@ package websockets
import ( import (
"encoding/json" "encoding/json"
"github.com/axllent/mailpit/logger" "github.com/axllent/mailpit/utils/logger"
) )
// Hub maintains the set of active clients and broadcasts messages to the // Hub maintains the set of active clients and broadcasts messages to the

View File

@ -7,8 +7,8 @@ import (
"regexp" "regexp"
"github.com/axllent/mailpit/config" "github.com/axllent/mailpit/config"
"github.com/axllent/mailpit/logger"
"github.com/axllent/mailpit/storage" "github.com/axllent/mailpit/storage"
"github.com/axllent/mailpit/utils/logger"
"github.com/mhale/smtpd" "github.com/mhale/smtpd"
) )

View File

@ -20,8 +20,8 @@ import (
"github.com/GuiaBolso/darwin" "github.com/GuiaBolso/darwin"
"github.com/axllent/mailpit/config" "github.com/axllent/mailpit/config"
"github.com/axllent/mailpit/logger"
"github.com/axllent/mailpit/server/websockets" "github.com/axllent/mailpit/server/websockets"
"github.com/axllent/mailpit/utils/logger"
"github.com/jhillyerd/enmime" "github.com/jhillyerd/enmime"
"github.com/klauspost/compress/zstd" "github.com/klauspost/compress/zstd"
"github.com/leporo/sqlf" "github.com/leporo/sqlf"

View File

@ -10,8 +10,8 @@ import (
"time" "time"
"github.com/axllent/mailpit/config" "github.com/axllent/mailpit/config"
"github.com/axllent/mailpit/logger"
"github.com/axllent/mailpit/server/websockets" "github.com/axllent/mailpit/server/websockets"
"github.com/axllent/mailpit/utils/logger"
"github.com/jhillyerd/enmime" "github.com/jhillyerd/enmime"
"github.com/k3a/html2text" "github.com/k3a/html2text"
"github.com/leporo/sqlf" "github.com/leporo/sqlf"

View File

@ -13,7 +13,7 @@ import (
"path/filepath" "path/filepath"
"runtime" "runtime"
"github.com/axllent/mailpit/logger" "github.com/axllent/mailpit/utils/logger"
"github.com/axllent/semver" "github.com/axllent/semver"
) )