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:
@ -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
|
||||||
|
@ -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")
|
||||||
|
@ -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"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -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 {
|
||||||
|
@ -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"
|
||||||
|
@ -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)
|
||||||
|
@ -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) }}
|
||||||
|
@ -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"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -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"
|
||||||
|
@ -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"
|
||||||
|
@ -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"
|
||||||
)
|
)
|
||||||
|
|
Reference in New Issue
Block a user