mirror of
https://github.com/axllent/mailpit.git
synced 2026-03-11 18:47:02 +00:00
Compare commits
39 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9b67792669 | ||
|
|
8739428136 | ||
|
|
97ec3e839b | ||
|
|
56d61ae24b | ||
|
|
d43560d45b | ||
|
|
a0e69a202a | ||
|
|
fc95241521 | ||
|
|
831157a52e | ||
|
|
18c3847deb | ||
|
|
21134c5bbc | ||
|
|
b34877b3ff | ||
|
|
47d6e319e3 | ||
|
|
a64e964c39 | ||
|
|
e5703d0805 | ||
|
|
c004c1065e | ||
|
|
af93444374 | ||
|
|
840bc94190 | ||
|
|
4e2d4d6365 | ||
|
|
7446f52205 | ||
|
|
d4218df1cf | ||
|
|
2b18b1bee1 | ||
|
|
a3f83ea5ce | ||
|
|
52405915fa | ||
|
|
636918dd0e | ||
|
|
3fb926f015 | ||
|
|
0af6850d34 | ||
|
|
66660b9074 | ||
|
|
3b43a803af | ||
|
|
ec3dd0c196 | ||
|
|
38240ae96d | ||
|
|
d0087423db | ||
|
|
1ac8e3a79f | ||
|
|
67dedd8acc | ||
|
|
4f6caca352 | ||
|
|
b6fdcd4ec5 | ||
|
|
044525fcca | ||
|
|
0ab4210640 | ||
|
|
e902806ea2 | ||
|
|
f2b6ba0d69 |
10
.github/workflows/build-docker.yml
vendored
10
.github/workflows/build-docker.yml
vendored
@@ -8,16 +8,16 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v2
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_ACCESS_TOKEN }}
|
||||
@@ -30,7 +30,7 @@ jobs:
|
||||
version_extractor_regex: 'v(.*)$'
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v4
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
# platforms: linux/386,linux/amd64,linux/arm,linux/arm64
|
||||
|
||||
2
.github/workflows/codeql-analysis.yml
vendored
2
.github/workflows/codeql-analysis.yml
vendored
@@ -38,7 +38,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
|
||||
4
.github/workflows/release-build.yml
vendored
4
.github/workflows/release-build.yml
vendored
@@ -5,7 +5,7 @@ on:
|
||||
name: Build & release
|
||||
jobs:
|
||||
releases-matrix:
|
||||
name: Release Go Binary
|
||||
name: Build
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
@@ -21,7 +21,7 @@ jobs:
|
||||
- goarch: arm
|
||||
goos: windows
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
# build the assets
|
||||
- uses: actions/setup-node@v3
|
||||
|
||||
2
.github/workflows/tests.yml
vendored
2
.github/workflows/tests.yml
vendored
@@ -15,7 +15,7 @@ jobs:
|
||||
- uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: ${{ matrix.go-version }}
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/cache@v3
|
||||
with:
|
||||
path: |
|
||||
|
||||
52
CHANGELOG.md
52
CHANGELOG.md
@@ -2,6 +2,58 @@
|
||||
|
||||
Notable changes to Mailpit will be documented in this file.
|
||||
|
||||
## [v1.9.7]
|
||||
|
||||
### Fix
|
||||
- Enable delete button when new messages arrive
|
||||
|
||||
### Libs
|
||||
- Update node modules
|
||||
- Downgrade microcosm-cc/bluemonday, revert to Go 1.20
|
||||
- Update Go modules & minimum Go version (1.21)
|
||||
|
||||
|
||||
## [v1.9.6]
|
||||
|
||||
### Libs
|
||||
- Update node modules
|
||||
- Update Go modules
|
||||
|
||||
### UI
|
||||
- Display message previews on separate line ([#175](https://github.com/axllent/mailpit/issues/175))
|
||||
|
||||
|
||||
## [v1.9.5]
|
||||
|
||||
### Feature
|
||||
- Add `reindex` subcommand to reindex all messages
|
||||
- Display email previews ([#175](https://github.com/axllent/mailpit/issues/175))
|
||||
|
||||
### Fix
|
||||
- HTML message preview background color when switching themes in Chrome
|
||||
- Correctly detect tags in search (UI)
|
||||
|
||||
### Tests
|
||||
- Add message summary tests
|
||||
- Add snippet tests
|
||||
|
||||
|
||||
## [v1.9.4]
|
||||
|
||||
### Chore
|
||||
- Remove some flags deprecated 08/2022
|
||||
|
||||
### Feature
|
||||
- Set auth credentials directly from environment variables
|
||||
|
||||
### Libs
|
||||
- Update node modules
|
||||
- Update Go modules
|
||||
|
||||
### UI
|
||||
- Add option to delete a message after release
|
||||
|
||||
|
||||
## [v1.9.3]
|
||||
|
||||
### Chore
|
||||
|
||||
@@ -51,6 +51,8 @@ Mailpit runs as a single binary and can be installed in different ways:
|
||||
|
||||
Install Mailpit with `brew install mailpit`.
|
||||
|
||||
To run automatically run mailpit in the background, run `brew services start mailpit`.
|
||||
|
||||
|
||||
### Install via bash script (Linux & Mac)
|
||||
|
||||
@@ -80,7 +82,7 @@ To build Mailpit from source see [building from source](https://github.com/axlle
|
||||
|
||||
Run `mailpit -h` to see options. More information can be seen in [the docs](https://github.com/axllent/mailpit/wiki/Runtime-options).
|
||||
|
||||
|
||||
If installed using homebrew, you may run `brew services start mailpit` to run always run mailpit automatically.
|
||||
|
||||
### Testing Mailpit
|
||||
|
||||
|
||||
40
cmd/reindex.go
Normal file
40
cmd/reindex.go
Normal file
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
Copyright © 2022-Now() Ralph Slooten
|
||||
This file is part of a CLI application.
|
||||
*/
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/axllent/mailpit/config"
|
||||
"github.com/axllent/mailpit/internal/logger"
|
||||
"github.com/axllent/mailpit/internal/storage"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// reindexCmd represents the reindex command
|
||||
var reindexCmd = &cobra.Command{
|
||||
Use: "reindex <database>",
|
||||
Short: "Reindex the database",
|
||||
Long: `This will reindex all messages in the entire database.
|
||||
|
||||
If you have several thousand messages in your mailbox, then it is advised to shut down
|
||||
Mailpit while you reindex as this process will likely result in database locking issues.`,
|
||||
Args: cobra.ExactArgs(1),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
config.DataFile = args[0]
|
||||
config.MaxMessages = 0
|
||||
|
||||
if err := storage.InitDB(); err != nil {
|
||||
logger.Log().Error(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
storage.ReindexAll()
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(reindexCmd)
|
||||
}
|
||||
76
cmd/root.go
76
cmd/root.go
@@ -8,6 +8,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/axllent/mailpit/config"
|
||||
"github.com/axllent/mailpit/internal/auth"
|
||||
"github.com/axllent/mailpit/internal/logger"
|
||||
"github.com/axllent/mailpit/internal/storage"
|
||||
"github.com/axllent/mailpit/server"
|
||||
@@ -91,7 +92,7 @@ func init() {
|
||||
rootCmd.Flags().BoolVar(&config.DisableHTMLCheck, "disable-html-check", config.DisableHTMLCheck, "Disable the HTML check functionality (web UI & API)")
|
||||
rootCmd.Flags().BoolVar(&config.BlockRemoteCSSAndFonts, "block-remote-css-and-fonts", config.BlockRemoteCSSAndFonts, "Block access to remote CSS & fonts")
|
||||
|
||||
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 & API authentication")
|
||||
rootCmd.Flags().StringVar(&config.UITLSCert, "ui-tls-cert", config.UITLSCert, "TLS certificate for web UI (HTTPS) - requires ui-tls-key")
|
||||
rootCmd.Flags().StringVar(&config.UITLSKey, "ui-tls-key", config.UITLSKey, "TLS key for web UI (HTTPS) - requires ui-tls-cert")
|
||||
|
||||
@@ -109,22 +110,6 @@ func init() {
|
||||
rootCmd.Flags().BoolVarP(&logger.QuietLogging, "quiet", "q", logger.QuietLogging, "Quiet logging (errors only)")
|
||||
rootCmd.Flags().BoolVarP(&logger.VerboseLogging, "verbose", "v", logger.VerboseLogging, "Verbose logging")
|
||||
|
||||
// deprecated flags 2022/08/06
|
||||
rootCmd.Flags().StringVarP(&config.UIAuthFile, "auth-file", "a", config.UIAuthFile, "A password file for web UI authentication")
|
||||
rootCmd.Flags().StringVar(&config.UITLSCert, "ssl-cert", config.UITLSCert, "SSL certificate - requires ssl-key")
|
||||
rootCmd.Flags().StringVar(&config.UITLSKey, "ssl-key", config.UITLSKey, "SSL key - requires ssl-cert")
|
||||
rootCmd.Flags().Lookup("auth-file").Hidden = true
|
||||
rootCmd.Flags().Lookup("auth-file").Deprecated = "use --ui-auth-file"
|
||||
rootCmd.Flags().Lookup("ssl-cert").Hidden = true
|
||||
rootCmd.Flags().Lookup("ssl-cert").Deprecated = "use --ui-tls-cert"
|
||||
rootCmd.Flags().Lookup("ssl-key").Hidden = true
|
||||
rootCmd.Flags().Lookup("ssl-key").Deprecated = "use --ui-tls-key"
|
||||
|
||||
// deprecated flags 2022/08/30
|
||||
rootCmd.Flags().StringVar(&config.DataFile, "data", config.DataFile, "Database file to store persistent data")
|
||||
rootCmd.Flags().Lookup("data").Hidden = true
|
||||
rootCmd.Flags().Lookup("data").Deprecated = "use --db-file"
|
||||
|
||||
// deprecated flags 2023/03/12
|
||||
rootCmd.Flags().StringVar(&config.UITLSCert, "ui-ssl-cert", config.UITLSCert, "SSL certificate for web UI - requires ui-ssl-key")
|
||||
rootCmd.Flags().StringVar(&config.UITLSKey, "ui-ssl-key", config.UITLSKey, "SSL key for web UI - requires ui-ssl-cert")
|
||||
@@ -143,9 +128,7 @@ func init() {
|
||||
// Load settings from environment
|
||||
func initConfigFromEnv() {
|
||||
// inherit from environment if provided
|
||||
if len(os.Getenv("MP_DATA_FILE")) > 0 {
|
||||
config.DataFile = os.Getenv("MP_DATA_FILE")
|
||||
}
|
||||
config.DataFile = os.Getenv("MP_DATA_FILE")
|
||||
if len(os.Getenv("MP_SMTP_BIND_ADDR")) > 0 {
|
||||
config.SMTPListen = os.Getenv("MP_SMTP_BIND_ADDR")
|
||||
}
|
||||
@@ -160,26 +143,16 @@ func initConfigFromEnv() {
|
||||
}
|
||||
|
||||
// UI
|
||||
if len(os.Getenv("MP_UI_AUTH_FILE")) > 0 {
|
||||
config.UIAuthFile = os.Getenv("MP_UI_AUTH_FILE")
|
||||
}
|
||||
if len(os.Getenv("MP_UI_TLS_CERT")) > 0 {
|
||||
config.UITLSCert = os.Getenv("MP_UI_TLS_CERT")
|
||||
}
|
||||
if len(os.Getenv("MP_UI_TLS_KEY")) > 0 {
|
||||
config.UITLSKey = os.Getenv("MP_UI_TLS_KEY")
|
||||
}
|
||||
config.UIAuthFile = os.Getenv("MP_UI_AUTH_FILE")
|
||||
auth.SetUIAuth(os.Getenv("MP_UI_AUTH"))
|
||||
config.UITLSCert = os.Getenv("MP_UI_TLS_CERT")
|
||||
config.UITLSKey = os.Getenv("MP_UI_TLS_KEY")
|
||||
|
||||
// SMTP
|
||||
if len(os.Getenv("MP_SMTP_AUTH_FILE")) > 0 {
|
||||
config.SMTPAuthFile = os.Getenv("MP_SMTP_AUTH_FILE")
|
||||
}
|
||||
if len(os.Getenv("MP_SMTP_TLS_CERT")) > 0 {
|
||||
config.SMTPTLSCert = os.Getenv("MP_SMTP_TLS_CERT")
|
||||
}
|
||||
if len(os.Getenv("MP_SMTP_TLS_KEY")) > 0 {
|
||||
config.SMTPTLSKey = os.Getenv("MP_SMTP_TLS_KEY")
|
||||
}
|
||||
config.SMTPAuthFile = os.Getenv("MP_SMTP_AUTH_FILE")
|
||||
auth.SetSMTPAuth(os.Getenv("MP_SMTP_AUTH"))
|
||||
config.SMTPTLSCert = os.Getenv("MP_SMTP_TLS_CERT")
|
||||
config.SMTPTLSKey = os.Getenv("MP_SMTP_TLS_KEY")
|
||||
if getEnabledFromEnv("MP_SMTP_AUTH_ACCEPT_ANY") {
|
||||
config.SMTPAuthAcceptAny = true
|
||||
}
|
||||
@@ -191,9 +164,7 @@ func initConfigFromEnv() {
|
||||
}
|
||||
|
||||
// Relay server config
|
||||
if len(os.Getenv("MP_SMTP_RELAY_CONFIG")) > 0 {
|
||||
config.SMTPRelayConfigFile = os.Getenv("MP_SMTP_RELAY_CONFIG")
|
||||
}
|
||||
config.SMTPRelayConfigFile = os.Getenv("MP_SMTP_RELAY_CONFIG")
|
||||
if getEnabledFromEnv("MP_SMTP_RELAY_ALL") {
|
||||
config.SMTPRelayAllIncoming = true
|
||||
}
|
||||
@@ -227,39 +198,22 @@ func initConfigFromEnv() {
|
||||
|
||||
// load deprecated settings from environment and warn
|
||||
func initDeprecatedConfigFromEnv() {
|
||||
// deprecated 2022/08/06
|
||||
if len(os.Getenv("MP_AUTH_FILE")) > 0 {
|
||||
fmt.Println("ENV MP_AUTH_FILE has been deprecated, use MP_UI_AUTH_FILE")
|
||||
config.UIAuthFile = os.Getenv("MP_AUTH_FILE")
|
||||
}
|
||||
// deprecated 2022/08/06
|
||||
if len(os.Getenv("MP_SSL_CERT")) > 0 {
|
||||
fmt.Println("ENV MP_SSL_CERT has been deprecated, use MP_UI_TLS_CERT")
|
||||
config.UITLSCert = os.Getenv("MP_SSL_CERT")
|
||||
}
|
||||
// deprecated 2022/08/06
|
||||
if len(os.Getenv("MP_SSL_KEY")) > 0 {
|
||||
fmt.Println("ENV MP_SSL_KEY has been deprecated, use MP_UI_TLS_KEY")
|
||||
config.UITLSKey = os.Getenv("MP_TLS_KEY")
|
||||
}
|
||||
// deprecated 2022/08/28
|
||||
if len(os.Getenv("MP_DATA_DIR")) > 0 {
|
||||
fmt.Println("ENV MP_DATA_DIR has been deprecated, use MP_DATA_FILE")
|
||||
config.DataFile = os.Getenv("MP_DATA_DIR")
|
||||
}
|
||||
// deprecated 2023/03/12
|
||||
if len(os.Getenv("MP_UI_SSL_CERT")) > 0 {
|
||||
fmt.Println("ENV MP_UI_SSL_CERT has been deprecated, use MP_UI_TLS_CERT")
|
||||
config.UITLSCert = os.Getenv("MP_UI_SSL_CERT")
|
||||
}
|
||||
// deprecated 2023/03/12
|
||||
if len(os.Getenv("MP_UI_SSL_KEY")) > 0 {
|
||||
fmt.Println("ENV MP_UI_SSL_KEY has been deprecated, use MP_UI_TLS_KEY")
|
||||
config.UITLSKey = os.Getenv("MP_UI_SSL_KEY")
|
||||
}
|
||||
// deprecated 2023/03/12
|
||||
if len(os.Getenv("MP_SMTP_SSL_CERT")) > 0 {
|
||||
fmt.Println("ENV MP_SMTP_CERT has been deprecated, use MP_SMTP_TLS_CERT")
|
||||
config.SMTPTLSCert = os.Getenv("MP_SMTP_SSL_CERT")
|
||||
}
|
||||
// deprecated 2023/03/12
|
||||
if len(os.Getenv("MP_SMTP_SSL_KEY")) > 0 {
|
||||
fmt.Println("ENV MP_SMTP_KEY has been deprecated, use MP_SMTP_TLS_KEY")
|
||||
config.SMTPTLSKey = os.Getenv("MP_SMTP_SMTP_KEY")
|
||||
|
||||
@@ -10,9 +10,9 @@ import (
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/axllent/mailpit/internal/auth"
|
||||
"github.com/axllent/mailpit/internal/logger"
|
||||
"github.com/axllent/mailpit/internal/tools"
|
||||
"github.com/tg123/go-htpasswd"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
@@ -38,12 +38,9 @@ var (
|
||||
// UITLSKey file
|
||||
UITLSKey string
|
||||
|
||||
// UIAuthFile for basic authentication
|
||||
// UIAuthFile for UI & API authentication
|
||||
UIAuthFile string
|
||||
|
||||
// UIAuth used for authentication
|
||||
UIAuth *htpasswd.File
|
||||
|
||||
// Webroot to define the base path for the UI and API
|
||||
Webroot = "/"
|
||||
|
||||
@@ -56,9 +53,6 @@ var (
|
||||
// SMTPAuthFile for SMTP authentication
|
||||
SMTPAuthFile string
|
||||
|
||||
// SMTPAuthConfig used for authentication auto-generated from SMTPAuthFile
|
||||
SMTPAuthConfig *htpasswd.File
|
||||
|
||||
// SMTPAuthAllowInsecure allows PLAIN & LOGIN unencrypted authentication
|
||||
SMTPAuthAllowInsecure bool
|
||||
|
||||
@@ -161,12 +155,13 @@ func VerifyConfig() error {
|
||||
if !isFile(UIAuthFile) {
|
||||
return fmt.Errorf("HTTP password file not found: %s", UIAuthFile)
|
||||
}
|
||||
|
||||
a, err := htpasswd.New(UIAuthFile, htpasswd.DefaultSystems, nil)
|
||||
b, err := os.ReadFile(UIAuthFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
UIAuth = a
|
||||
if err := auth.SetUIAuth(string(b)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if UITLSCert != "" && UITLSKey == "" || UITLSCert == "" && UITLSKey != "" {
|
||||
@@ -202,18 +197,21 @@ func VerifyConfig() error {
|
||||
return fmt.Errorf("SMTP password file not found: %s", SMTPAuthFile)
|
||||
}
|
||||
|
||||
if SMTPAuthAcceptAny {
|
||||
return errors.New("SMTP authentication can either use --smtp-auth-file or --smtp-auth-accept-any")
|
||||
}
|
||||
|
||||
a, err := htpasswd.New(SMTPAuthFile, htpasswd.DefaultSystems, nil)
|
||||
b, err := os.ReadFile(SMTPAuthFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
SMTPAuthConfig = a
|
||||
|
||||
if err := auth.SetSMTPAuth(string(b)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if SMTPTLSCert == "" && (SMTPAuthFile != "" || SMTPAuthAcceptAny) && !SMTPAuthAllowInsecure {
|
||||
if auth.SMTPCredentials != nil && SMTPAuthAcceptAny {
|
||||
return errors.New("SMTP authentication cannot use both credentials and --smtp-auth-accept-any")
|
||||
}
|
||||
|
||||
if SMTPTLSCert == "" && (auth.SMTPCredentials != nil || SMTPAuthAcceptAny) && !SMTPAuthAllowInsecure {
|
||||
return errors.New("SMTP authentication requires TLS encryption, run with `--smtp-auth-allow-insecure` to allow insecure authentication")
|
||||
}
|
||||
|
||||
|
||||
18
go.mod
18
go.mod
@@ -15,6 +15,7 @@ require (
|
||||
github.com/klauspost/compress v1.17.0
|
||||
github.com/leporo/sqlf v1.4.0
|
||||
github.com/mhale/smtpd v0.8.0
|
||||
github.com/microcosm-cc/bluemonday v1.0.25
|
||||
github.com/reiver/go-telnet v0.0.0-20180421082511-9ff0b2ab096e
|
||||
github.com/satori/go.uuid v1.2.0
|
||||
github.com/sirupsen/logrus v1.9.3
|
||||
@@ -22,16 +23,17 @@ require (
|
||||
github.com/spf13/pflag v1.0.5
|
||||
github.com/tg123/go-htpasswd v1.2.1
|
||||
github.com/vanng822/go-premailer v1.20.2
|
||||
golang.org/x/net v0.15.0
|
||||
golang.org/x/net v0.17.0
|
||||
golang.org/x/text v0.13.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
modernc.org/sqlite v1.25.0
|
||||
modernc.org/sqlite v1.26.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/DATA-DOG/go-sqlmock v1.5.0 // indirect
|
||||
github.com/GehirnInc/crypt v0.0.0-20230320061759-8cc1b52080c5 // indirect
|
||||
github.com/andybalholm/cascadia v1.3.2 // indirect
|
||||
github.com/aymerick/douceur v0.2.0 // indirect
|
||||
github.com/cention-sany/utf7 v0.0.0-20170124080048-26cad61bd60a // indirect
|
||||
github.com/cznic/ql v1.2.0 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
@@ -52,18 +54,18 @@ require (
|
||||
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
github.com/vanng822/css v1.0.1 // indirect
|
||||
golang.org/x/crypto v0.13.0 // indirect
|
||||
golang.org/x/image v0.12.0 // indirect
|
||||
golang.org/x/mod v0.12.0 // indirect
|
||||
golang.org/x/sys v0.12.0 // indirect
|
||||
golang.org/x/tools v0.13.0 // indirect
|
||||
golang.org/x/crypto v0.14.0 // indirect
|
||||
golang.org/x/image v0.13.0 // indirect
|
||||
golang.org/x/mod v0.13.0 // indirect
|
||||
golang.org/x/sys v0.13.0 // indirect
|
||||
golang.org/x/tools v0.14.0 // indirect
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
|
||||
lukechampine.com/uint128 v1.3.0 // indirect
|
||||
modernc.org/cc/v3 v3.41.0 // indirect
|
||||
modernc.org/ccgo/v3 v3.16.15 // indirect
|
||||
modernc.org/libc v1.24.1 // indirect
|
||||
modernc.org/mathutil v1.6.0 // indirect
|
||||
modernc.org/memory v1.7.1 // indirect
|
||||
modernc.org/memory v1.7.2 // indirect
|
||||
modernc.org/opt v0.1.3 // indirect
|
||||
modernc.org/strutil v1.2.0 // indirect
|
||||
modernc.org/token v1.1.0 // indirect
|
||||
|
||||
38
go.sum
38
go.sum
@@ -13,6 +13,8 @@ github.com/andybalholm/cascadia v1.3.2 h1:3Xi6Dw5lHF15JtdcmAHD3i1+T8plmv7BQ/nsVi
|
||||
github.com/andybalholm/cascadia v1.3.2/go.mod h1:7gtRlve5FxPPgIgX36uWBX58OdBsSS6lUvCFb+h7KvU=
|
||||
github.com/axllent/semver v0.0.1 h1:QqF+KSGxgj8QZzSXAvKFqjGWE5792ksOnQhludToK8E=
|
||||
github.com/axllent/semver v0.0.1/go.mod h1:2xSPzvG8n9mRfdtxSvWvfTfQGWfHsMsHO1iZnKATMSc=
|
||||
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
|
||||
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
|
||||
github.com/cention-sany/utf7 v0.0.0-20170124080048-26cad61bd60a h1:MISbI8sU/PSK/ztvmWKFcI7UGb5/HQT7B+i3a2myKgI=
|
||||
github.com/cention-sany/utf7 v0.0.0-20170124080048-26cad61bd60a/go.mod h1:2GxOXOlEPAMFPfp014mK1SWq8G8BN8o7/dfYqJrVGn8=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
@@ -97,6 +99,8 @@ github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwp
|
||||
github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
|
||||
github.com/mhale/smtpd v0.8.0 h1:5JvdsehCg33PQrZBvFyDMMUDQmvbzVpZgKob7eYBJc0=
|
||||
github.com/mhale/smtpd v0.8.0/go.mod h1:MQl+y2hwIEQCXtNhe5+55n0GZOjSmeqORDIXbqUL3x4=
|
||||
github.com/microcosm-cc/bluemonday v1.0.25 h1:4NEwSfiJ+Wva0VxN5B8OwMicaJvD8r9tlJWm9rtloEg=
|
||||
github.com/microcosm-cc/bluemonday v1.0.25/go.mod h1:ZIOjCQp1OrzBBPIJmfX4qDYFuhU02nx4bn030ixfHLE=
|
||||
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
|
||||
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
@@ -152,15 +156,15 @@ github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5t
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.13.0 h1:mvySKfSWJ+UKUii46M40LOvyWfN0s2U+46/jDd0e6Ck=
|
||||
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
|
||||
golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc=
|
||||
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
|
||||
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/image v0.12.0 h1:w13vZbU4o5rKOFFR8y7M+c4A5jXDC0uXTdHYRP8X2DQ=
|
||||
golang.org/x/image v0.12.0/go.mod h1:Lu90jvHG7GfemOIcldsh9A2hS01ocl6oNO7ype5mEnk=
|
||||
golang.org/x/image v0.13.0 h1:3cge/F/QTkNLauhf2QoE9zp+7sr+ZcL4HnoZmdwg9sg=
|
||||
golang.org/x/image v0.13.0/go.mod h1:6mmbMOeV28HuMTgA6OSRkdXKYw/t5W9Uwn2Yv1r3Yxk=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc=
|
||||
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.13.0 h1:I/DsJXRlw/8l/0c24sM9yb0T4z9liZTduXvdAWYiysY=
|
||||
golang.org/x/mod v0.13.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
@@ -173,12 +177,12 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
|
||||
golang.org/x/net v0.15.0 h1:ugBLEUaxABaB5AJqW9enI0ACdci2RUd4eP51NTBvuJ8=
|
||||
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
||||
golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
|
||||
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E=
|
||||
golang.org/x/sync v0.4.0 h1:zxkM55ReGkDlKSM+Fu41A+zmbZuaPVbGMzvvdUPznYQ=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
@@ -191,8 +195,8 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o=
|
||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
|
||||
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
@@ -210,8 +214,8 @@ golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.13.0 h1:Iey4qkscZuv0VvIt8E0neZjtPVQFSc870HQ448QgEmQ=
|
||||
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
||||
golang.org/x/tools v0.14.0 h1:jvNa2pY0M4r62jkRQ6RwEZZyPcymeL9XZMLBbV7U2nc=
|
||||
golang.org/x/tools v0.14.0/go.mod h1:uYBEerGOWcJyEORxN+Ek8+TT266gXkNlHdJBwexUsBg=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
@@ -233,12 +237,12 @@ modernc.org/libc v1.24.1 h1:uvJSeCKL/AgzBo2yYIPPTy82v21KgGnizcGYfBHaNuM=
|
||||
modernc.org/libc v1.24.1/go.mod h1:FmfO1RLrU3MHJfyi9eYYmZBfi/R+tqZ6+hQ3yQQUkak=
|
||||
modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4=
|
||||
modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo=
|
||||
modernc.org/memory v1.7.1 h1:9J+2/GKTlV503mk3yv8QJ6oEpRCUrRy0ad8TXEPoV8M=
|
||||
modernc.org/memory v1.7.1/go.mod h1:NO4NVCQy0N7ln+T9ngWqOQfi7ley4vpwvARR+Hjw95E=
|
||||
modernc.org/memory v1.7.2 h1:Klh90S215mmH8c9gO98QxQFsY+W451E8AnzjoE2ee1E=
|
||||
modernc.org/memory v1.7.2/go.mod h1:NO4NVCQy0N7ln+T9ngWqOQfi7ley4vpwvARR+Hjw95E=
|
||||
modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4=
|
||||
modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
|
||||
modernc.org/sqlite v1.25.0 h1:AFweiwPNd/b3BoKnBOfFm+Y260guGMF+0UFk0savqeA=
|
||||
modernc.org/sqlite v1.25.0/go.mod h1:FL3pVXie73rg3Rii6V/u5BoHlSoyeZeIgKZEgHARyCU=
|
||||
modernc.org/sqlite v1.26.0 h1:SocQdLRSYlA8W99V8YH0NES75thx19d9sB/aFc4R8Lw=
|
||||
modernc.org/sqlite v1.26.0/go.mod h1:FL3pVXie73rg3Rii6V/u5BoHlSoyeZeIgKZEgHARyCU=
|
||||
modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA=
|
||||
modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0=
|
||||
modernc.org/tcl v1.15.2 h1:C4ybAYCGJw968e+Me18oW55kD/FexcHbqH2xak1ROSY=
|
||||
|
||||
69
internal/auth/auth.go
Normal file
69
internal/auth/auth.go
Normal file
@@ -0,0 +1,69 @@
|
||||
// Package auth handles the web UI and SMTP authentication
|
||||
package auth
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/tg123/go-htpasswd"
|
||||
)
|
||||
|
||||
var (
|
||||
// UICredentials passwords
|
||||
UICredentials *htpasswd.File
|
||||
// SMTPCredentials passwords
|
||||
SMTPCredentials *htpasswd.File
|
||||
)
|
||||
|
||||
// SetUIAuth will set Basic Auth credentials required for the UI & API
|
||||
func SetUIAuth(s string) error {
|
||||
var err error
|
||||
|
||||
credentials := credentialsFromString(s)
|
||||
if len(credentials) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
r := strings.NewReader(strings.Join(credentials, "\n"))
|
||||
|
||||
UICredentials, err = htpasswd.NewFromReader(r, htpasswd.DefaultSystems, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetSMTPAuth will set SMTP credentials
|
||||
func SetSMTPAuth(s string) error {
|
||||
var err error
|
||||
|
||||
credentials := credentialsFromString(s)
|
||||
if len(credentials) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
r := strings.NewReader(strings.Join(credentials, "\n"))
|
||||
|
||||
SMTPCredentials, err = htpasswd.NewFromReader(r, htpasswd.DefaultSystems, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func credentialsFromString(s string) []string {
|
||||
// split string by any whitespace character
|
||||
re := regexp.MustCompile(`\s+`)
|
||||
|
||||
words := re.Split(s, -1)
|
||||
credentials := []string{}
|
||||
for _, w := range words {
|
||||
if w != "" {
|
||||
credentials = append(credentials, w)
|
||||
}
|
||||
}
|
||||
|
||||
return credentials
|
||||
}
|
||||
@@ -18,9 +18,9 @@ import (
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/GuiaBolso/darwin"
|
||||
"github.com/axllent/mailpit/config"
|
||||
"github.com/axllent/mailpit/internal/logger"
|
||||
"github.com/axllent/mailpit/internal/tools"
|
||||
"github.com/axllent/mailpit/server/websockets"
|
||||
"github.com/jhillyerd/enmime"
|
||||
"github.com/klauspost/compress/zstd"
|
||||
@@ -42,81 +42,8 @@ var (
|
||||
// zstd compression encoder & decoder
|
||||
dbEncoder, _ = zstd.NewWriter(nil)
|
||||
dbDecoder, _ = zstd.NewReader(nil)
|
||||
|
||||
dbMigrations = []darwin.Migration{
|
||||
{
|
||||
Version: 1.0,
|
||||
Description: "Creating tables",
|
||||
Script: `CREATE TABLE IF NOT EXISTS mailbox (
|
||||
Sort INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
ID TEXT NOT NULL,
|
||||
Data BLOB,
|
||||
Search TEXT,
|
||||
Read INTEGER
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_sort ON mailbox (Sort);
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_id ON mailbox (ID);
|
||||
CREATE INDEX IF NOT EXISTS idx_read ON mailbox (Read);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS mailbox_data (
|
||||
ID TEXT KEY NOT NULL,
|
||||
Email BLOB
|
||||
);
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_data_id ON mailbox_data (ID);`,
|
||||
},
|
||||
{
|
||||
Version: 1.1,
|
||||
Description: "Create tags column",
|
||||
Script: `ALTER TABLE mailbox ADD COLUMN Tags Text NOT NULL DEFAULT '[]';
|
||||
CREATE INDEX IF NOT EXISTS idx_tags ON mailbox (Tags);`,
|
||||
},
|
||||
{
|
||||
Version: 1.2,
|
||||
Description: "Creating new mailbox format",
|
||||
Script: `CREATE TABLE IF NOT EXISTS mailboxtmp (
|
||||
Created INTEGER NOT NULL,
|
||||
ID TEXT NOT NULL,
|
||||
MessageID TEXT NOT NULL,
|
||||
Subject TEXT NOT NULL,
|
||||
Metadata TEXT,
|
||||
Size INTEGER NOT NULL,
|
||||
Inline INTEGER NOT NULL,
|
||||
Attachments INTEGER NOT NULL,
|
||||
Read INTEGER,
|
||||
Tags TEXT,
|
||||
SearchText TEXT
|
||||
);
|
||||
INSERT INTO mailboxtmp
|
||||
(Created, ID, MessageID, Subject, Metadata, Size, Inline, Attachments, SearchText, Read, Tags)
|
||||
SELECT
|
||||
Sort, ID, '', json_extract(Data, '$.Subject'),Data,
|
||||
json_extract(Data, '$.Size'), json_extract(Data, '$.Inline'), json_extract(Data, '$.Attachments'),
|
||||
Search, Read, Tags
|
||||
FROM mailbox;
|
||||
|
||||
DROP TABLE IF EXISTS mailbox;
|
||||
ALTER TABLE mailboxtmp RENAME TO mailbox;
|
||||
CREATE INDEX IF NOT EXISTS idx_created ON mailbox (Created);
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_id ON mailbox (ID);
|
||||
CREATE INDEX IF NOT EXISTS idx_message_id ON mailbox (MessageID);
|
||||
CREATE INDEX IF NOT EXISTS idx_subject ON mailbox (Subject);
|
||||
CREATE INDEX IF NOT EXISTS idx_size ON mailbox (Size);
|
||||
CREATE INDEX IF NOT EXISTS idx_inline ON mailbox (Inline);
|
||||
CREATE INDEX IF NOT EXISTS idx_attachments ON mailbox (Attachments);
|
||||
CREATE INDEX IF NOT EXISTS idx_read ON mailbox (Read);
|
||||
CREATE INDEX IF NOT EXISTS idx_tags ON mailbox (Tags);`,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
// DBMailSummary struct for storing mail summary
|
||||
type DBMailSummary struct {
|
||||
From *mail.Address
|
||||
To []*mail.Address
|
||||
Cc []*mail.Address
|
||||
Bcc []*mail.Address
|
||||
}
|
||||
|
||||
// InitDB will initialise the database
|
||||
func InitDB() error {
|
||||
p := config.DataFile
|
||||
@@ -178,15 +105,6 @@ func InitDB() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Create tables and apply migrations if required
|
||||
func dbApplyMigrations() error {
|
||||
driver := darwin.NewGenericDriver(db, darwin.SqliteDialect{})
|
||||
|
||||
d := darwin.New(driver, dbMigrations, nil)
|
||||
|
||||
return d.Migrate()
|
||||
}
|
||||
|
||||
// Close will close the database, and delete if a temporary table
|
||||
func Close() {
|
||||
if db != nil {
|
||||
@@ -281,10 +199,11 @@ func Store(body []byte) (string, error) {
|
||||
size := len(body)
|
||||
inline := len(env.Inlines)
|
||||
attachments := len(env.Attachments)
|
||||
snippet := tools.CreateSnippet(env.Text, env.HTML)
|
||||
|
||||
// insert mail summary data
|
||||
_, err = tx.Exec("INSERT INTO mailbox(Created, ID, MessageID, Subject, Metadata, Size, Inline, Attachments, SearchText, Tags, Read) values(?,?,?,?,?,?,?,?,?,?,0)",
|
||||
created.UnixMilli(), id, messageID, subject, string(summaryJSON), size, inline, attachments, searchText, string(tagJSON))
|
||||
_, err = tx.Exec("INSERT INTO mailbox(Created, ID, MessageID, Subject, Metadata, Size, Inline, Attachments, SearchText, Tags, Read, Snippet) values(?,?,?,?,?,?,?,?,?,?,0, ?)",
|
||||
created.UnixMilli(), id, messageID, subject, string(summaryJSON), size, inline, attachments, searchText, string(tagJSON), snippet)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@@ -312,6 +231,7 @@ func Store(body []byte) (string, error) {
|
||||
c.Subject = subject
|
||||
c.Size = size
|
||||
c.Tags = tagData
|
||||
c.Snippet = snippet
|
||||
|
||||
websockets.Broadcast("new", c)
|
||||
|
||||
@@ -328,7 +248,7 @@ func List(start, limit int) ([]MessageSummary, error) {
|
||||
results := []MessageSummary{}
|
||||
|
||||
q := sqlf.From("mailbox").
|
||||
Select(`Created, ID, MessageID, Subject, Metadata, Size, Attachments, Read, Tags`).
|
||||
Select(`Created, ID, MessageID, Subject, Metadata, Size, Attachments, Read, Tags, Snippet`).
|
||||
OrderBy("Created DESC").
|
||||
Limit(limit).
|
||||
Offset(start)
|
||||
@@ -343,9 +263,10 @@ func List(start, limit int) ([]MessageSummary, error) {
|
||||
var attachments int
|
||||
var tags string
|
||||
var read int
|
||||
var snippet string
|
||||
em := MessageSummary{}
|
||||
|
||||
if err := row.Scan(&created, &id, &messageID, &subject, &metadata, &size, &attachments, &read, &tags); err != nil {
|
||||
if err := row.Scan(&created, &id, &messageID, &subject, &metadata, &size, &attachments, &read, &tags, &snippet); err != nil {
|
||||
logger.Log().Error(err)
|
||||
return
|
||||
}
|
||||
@@ -367,6 +288,7 @@ func List(start, limit int) ([]MessageSummary, error) {
|
||||
em.Size = size
|
||||
em.Attachments = attachments
|
||||
em.Read = read == 1
|
||||
em.Snippet = snippet
|
||||
|
||||
results = append(results, em)
|
||||
}); err != nil {
|
||||
|
||||
@@ -117,6 +117,38 @@ func TestRetrieveMimeEmail(t *testing.T) {
|
||||
assertEqual(t, len(inlineData.Content), msg.Inline[0].Size, "inline attachment size does not match")
|
||||
}
|
||||
|
||||
func TestMessageSummary(t *testing.T) {
|
||||
setup()
|
||||
defer Close()
|
||||
|
||||
t.Log("Testing message summary")
|
||||
|
||||
if _, err := Store(testMimeEmail); err != nil {
|
||||
t.Log("error ", err)
|
||||
t.Fail()
|
||||
}
|
||||
|
||||
summaries, err := List(0, 1)
|
||||
if err != nil {
|
||||
t.Log("error ", err)
|
||||
t.Fail()
|
||||
}
|
||||
|
||||
assertEqual(t, len(summaries), 1, "Expected 1 result")
|
||||
|
||||
msg := summaries[0]
|
||||
|
||||
assertEqual(t, msg.From.Name, "Sender Smith", "\"From\" name does not match")
|
||||
assertEqual(t, msg.From.Address, "sender2@example.com", "\"From\" address does not match")
|
||||
assertEqual(t, msg.Subject, "inline + attachment", "subject does not match")
|
||||
assertEqual(t, len(msg.To), 1, "incorrect number of recipients")
|
||||
assertEqual(t, msg.To[0].Name, "Recipient Ross", "\"To\" name does not match")
|
||||
assertEqual(t, msg.To[0].Address, "recipient2@example.com", "\"To\" address does not match")
|
||||
assertEqual(t, msg.Snippet, "Message with inline image and attachment:", "\"Snippet\" does does not match")
|
||||
assertEqual(t, msg.Attachments, 1, "Expected 1 attachment")
|
||||
assertEqual(t, msg.MessageID, "33af2ac1-c33d-9738-35e3-a6daf90bbd89@gmail.com", "\"MessageID\" does not match")
|
||||
}
|
||||
|
||||
func BenchmarkImportText(b *testing.B) {
|
||||
setup()
|
||||
defer Close()
|
||||
|
||||
84
internal/storage/migrations.go
Normal file
84
internal/storage/migrations.go
Normal file
@@ -0,0 +1,84 @@
|
||||
package storage
|
||||
|
||||
import "github.com/GuiaBolso/darwin"
|
||||
|
||||
var (
|
||||
dbMigrations = []darwin.Migration{
|
||||
{
|
||||
Version: 1.0,
|
||||
Description: "Creating tables",
|
||||
Script: `CREATE TABLE IF NOT EXISTS mailbox (
|
||||
Sort INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
ID TEXT NOT NULL,
|
||||
Data BLOB,
|
||||
Search TEXT,
|
||||
Read INTEGER
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_sort ON mailbox (Sort);
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_id ON mailbox (ID);
|
||||
CREATE INDEX IF NOT EXISTS idx_read ON mailbox (Read);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS mailbox_data (
|
||||
ID TEXT KEY NOT NULL,
|
||||
Email BLOB
|
||||
);
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_data_id ON mailbox_data (ID);`,
|
||||
},
|
||||
{
|
||||
Version: 1.1,
|
||||
Description: "Create tags column",
|
||||
Script: `ALTER TABLE mailbox ADD COLUMN Tags Text NOT NULL DEFAULT '[]';
|
||||
CREATE INDEX IF NOT EXISTS idx_tags ON mailbox (Tags);`,
|
||||
},
|
||||
{
|
||||
Version: 1.2,
|
||||
Description: "Creating new mailbox format",
|
||||
Script: `CREATE TABLE IF NOT EXISTS mailboxtmp (
|
||||
Created INTEGER NOT NULL,
|
||||
ID TEXT NOT NULL,
|
||||
MessageID TEXT NOT NULL,
|
||||
Subject TEXT NOT NULL,
|
||||
Metadata TEXT,
|
||||
Size INTEGER NOT NULL,
|
||||
Inline INTEGER NOT NULL,
|
||||
Attachments INTEGER NOT NULL,
|
||||
Read INTEGER,
|
||||
Tags TEXT,
|
||||
SearchText TEXT
|
||||
);
|
||||
INSERT INTO mailboxtmp
|
||||
(Created, ID, MessageID, Subject, Metadata, Size, Inline, Attachments, SearchText, Read, Tags)
|
||||
SELECT
|
||||
Sort, ID, '', json_extract(Data, '$.Subject'),Data,
|
||||
json_extract(Data, '$.Size'), json_extract(Data, '$.Inline'), json_extract(Data, '$.Attachments'),
|
||||
Search, Read, Tags
|
||||
FROM mailbox;
|
||||
|
||||
DROP TABLE IF EXISTS mailbox;
|
||||
ALTER TABLE mailboxtmp RENAME TO mailbox;
|
||||
CREATE INDEX IF NOT EXISTS idx_created ON mailbox (Created);
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_id ON mailbox (ID);
|
||||
CREATE INDEX IF NOT EXISTS idx_message_id ON mailbox (MessageID);
|
||||
CREATE INDEX IF NOT EXISTS idx_subject ON mailbox (Subject);
|
||||
CREATE INDEX IF NOT EXISTS idx_size ON mailbox (Size);
|
||||
CREATE INDEX IF NOT EXISTS idx_inline ON mailbox (Inline);
|
||||
CREATE INDEX IF NOT EXISTS idx_attachments ON mailbox (Attachments);
|
||||
CREATE INDEX IF NOT EXISTS idx_read ON mailbox (Read);
|
||||
CREATE INDEX IF NOT EXISTS idx_tags ON mailbox (Tags);`,
|
||||
},
|
||||
{
|
||||
Version: 1.3,
|
||||
Description: "Create snippet column",
|
||||
Script: `ALTER TABLE mailbox ADD COLUMN Snippet Text NOT NULL DEFAULT '';`,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
// Create tables and apply migrations if required
|
||||
func dbApplyMigrations() error {
|
||||
driver := darwin.NewGenericDriver(db, darwin.SqliteDialect{})
|
||||
|
||||
d := darwin.New(driver, dbMigrations, nil)
|
||||
|
||||
return d.Migrate()
|
||||
}
|
||||
184
internal/storage/reindex.go
Normal file
184
internal/storage/reindex.go
Normal file
@@ -0,0 +1,184 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"database/sql"
|
||||
"os"
|
||||
|
||||
"github.com/axllent/mailpit/internal/logger"
|
||||
"github.com/axllent/mailpit/internal/tools"
|
||||
"github.com/jhillyerd/enmime"
|
||||
"github.com/leporo/sqlf"
|
||||
)
|
||||
|
||||
// ReindexAll will regenerate the search text and snippet for a message
|
||||
// and update the database.
|
||||
func ReindexAll() {
|
||||
ids := []string{}
|
||||
var i string
|
||||
chunkSize := 1000
|
||||
|
||||
finished := 0
|
||||
|
||||
err := sqlf.Select("ID").To(&i).
|
||||
From("mailbox").
|
||||
OrderBy("Created DESC").
|
||||
QueryAndClose(nil, db, func(row *sql.Rows) {
|
||||
ids = append(ids, i)
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
logger.Log().Error(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
total := len(ids)
|
||||
|
||||
chunks := chunkBy(ids, chunkSize)
|
||||
|
||||
logger.Log().Infof("Reindexing %d messages", total)
|
||||
|
||||
// fmt.Println(len(ids), " = ", len(chunks), "chunks")
|
||||
|
||||
type updateStruct struct {
|
||||
ID string
|
||||
SearchText string
|
||||
Snippet string
|
||||
}
|
||||
|
||||
for _, ids := range chunks {
|
||||
updates := []updateStruct{}
|
||||
|
||||
for _, id := range ids {
|
||||
raw, err := GetMessageRaw(id)
|
||||
if err != nil {
|
||||
logger.Log().Error(err)
|
||||
continue
|
||||
}
|
||||
|
||||
r := bytes.NewReader(raw)
|
||||
|
||||
env, err := enmime.ReadEnvelope(r)
|
||||
if err != nil {
|
||||
logger.Log().Error(err)
|
||||
continue
|
||||
}
|
||||
|
||||
searchText := createSearchText(env)
|
||||
snippet := tools.CreateSnippet(env.Text, env.HTML)
|
||||
|
||||
u := updateStruct{}
|
||||
u.ID = id
|
||||
u.SearchText = searchText
|
||||
u.Snippet = snippet
|
||||
|
||||
updates = append(updates, u)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
tx, err := db.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
logger.Log().Error(err)
|
||||
continue
|
||||
}
|
||||
|
||||
// roll back if it fails
|
||||
defer tx.Rollback()
|
||||
|
||||
// insert mail summary data
|
||||
for _, u := range updates {
|
||||
_, err = tx.Exec("UPDATE mailbox SET SearchText = ?, Snippet = ? WHERE ID = ?", u.SearchText, u.Snippet, u.ID)
|
||||
if err != nil {
|
||||
logger.Log().Error(err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
logger.Log().Error(err)
|
||||
continue
|
||||
}
|
||||
|
||||
finished += len(updates)
|
||||
|
||||
logger.Log().Printf("Reindexed: %d / %d (%d%%)", finished, total, finished*100/total)
|
||||
}
|
||||
}
|
||||
|
||||
// Reindex will regenerate the search text and snippet for a message
|
||||
// and update the database.
|
||||
func Reindex(id string) error {
|
||||
// ids := []string{}
|
||||
// var i string
|
||||
// // chunkSize := 100
|
||||
|
||||
// err := sqlf.Select("ID").To(&i).From("mailbox_data").QueryAndClose(nil, db, func(row *sql.Rows) {
|
||||
// ids = append(ids, id)
|
||||
// })
|
||||
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
|
||||
// chunks := chunkBy(ids, 100)
|
||||
|
||||
// fmt.Println(len(ids), " = ", len(chunks), "chunks")
|
||||
|
||||
// return nil
|
||||
|
||||
raw, err := GetMessageRaw(id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
r := bytes.NewReader(raw)
|
||||
|
||||
env, err := enmime.ReadEnvelope(r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
searchText := createSearchText(env)
|
||||
snippet := tools.CreateSnippet(env.Text, env.HTML)
|
||||
|
||||
// return nil
|
||||
|
||||
// ctx := context.Background()
|
||||
// tx, err := db.BeginTx(ctx, nil)
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
|
||||
// // roll back if it fails
|
||||
// defer tx.Rollback()
|
||||
|
||||
// // insert mail summary data
|
||||
// _, err = tx.Exec("UPDATE mailbox SET SearchText = ?, Snippet = ? WHERE ID = ?", searchText, snippet, id)
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
|
||||
// return tx.Commit()
|
||||
|
||||
_, err = sqlf.Update("mailbox").
|
||||
Set("SearchText", searchText).
|
||||
Set("Snippet", snippet).
|
||||
Where("ID = ?", id).
|
||||
ExecAndClose(context.Background(), db)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// ctx := context.Background()
|
||||
// tx, err := db.BeginTx(ctx, nil)
|
||||
// if err != nil {
|
||||
// return "", err
|
||||
// }
|
||||
|
||||
func chunkBy[T any](items []T, chunkSize int) (chunks [][]T) {
|
||||
for chunkSize < len(items) {
|
||||
items, chunks = items[chunkSize:], append(chunks, items[0:chunkSize:chunkSize])
|
||||
}
|
||||
return append(chunks, items)
|
||||
}
|
||||
@@ -38,11 +38,12 @@ func Search(search string, start, limit int) ([]MessageSummary, int, error) {
|
||||
var size int
|
||||
var attachments int
|
||||
var tags string
|
||||
var snippet string
|
||||
var read int
|
||||
var ignore string
|
||||
em := MessageSummary{}
|
||||
|
||||
if err := row.Scan(&created, &id, &messageID, &subject, &metadata, &size, &attachments, &read, &tags, &ignore, &ignore, &ignore, &ignore); err != nil {
|
||||
if err := row.Scan(&created, &id, &messageID, &subject, &metadata, &size, &attachments, &read, &tags, &snippet, &ignore, &ignore, &ignore, &ignore); err != nil {
|
||||
logger.Log().Error(err)
|
||||
return
|
||||
}
|
||||
@@ -64,6 +65,7 @@ func Search(search string, start, limit int) ([]MessageSummary, int, error) {
|
||||
em.Size = size
|
||||
em.Attachments = attachments
|
||||
em.Read = read == 1
|
||||
em.Snippet = snippet
|
||||
|
||||
allResults = append(allResults, em)
|
||||
}); err != nil {
|
||||
@@ -109,9 +111,10 @@ func DeleteSearch(search string) error {
|
||||
var attachments int
|
||||
var tags string
|
||||
var read int
|
||||
var snippet string
|
||||
var ignore string
|
||||
|
||||
if err := row.Scan(&created, &id, &messageID, &subject, &metadata, &size, &attachments, &read, &tags, &ignore, &ignore, &ignore, &ignore); err != nil {
|
||||
if err := row.Scan(&created, &id, &messageID, &subject, &metadata, &size, &attachments, &read, &tags, &snippet, &ignore, &ignore, &ignore, &ignore); err != nil {
|
||||
logger.Log().Error(err)
|
||||
return
|
||||
}
|
||||
@@ -193,7 +196,7 @@ func searchQueryBuilder(searchString string) *sqlf.Stmt {
|
||||
args := tools.ArgsParser(searchString)
|
||||
|
||||
q := sqlf.From("mailbox").
|
||||
Select(`Created, ID, MessageID, Subject, Metadata, Size, Attachments, Read, Tags,
|
||||
Select(`Created, ID, MessageID, Subject, Metadata, Size, Attachments, Read, Tags, Snippet,
|
||||
IFNULL(json_extract(Metadata, '$.To'), '{}') as ToJSON,
|
||||
IFNULL(json_extract(Metadata, '$.From'), '{}') as FromJSON,
|
||||
IFNULL(json_extract(Metadata, '$.Cc'), '{}') as CcJSON,
|
||||
|
||||
@@ -89,6 +89,8 @@ type MessageSummary struct {
|
||||
Size int
|
||||
// Whether the message has any attachments
|
||||
Attachments int
|
||||
// Message snippet includes up to 250 characters
|
||||
Snippet string
|
||||
}
|
||||
|
||||
// MailboxStats struct for quick mailbox total/read lookups
|
||||
@@ -98,6 +100,14 @@ type MailboxStats struct {
|
||||
Tags []string
|
||||
}
|
||||
|
||||
// DBMailSummary struct for storing mail summary
|
||||
type DBMailSummary struct {
|
||||
From *mail.Address
|
||||
To []*mail.Address
|
||||
Cc []*mail.Address
|
||||
Bcc []*mail.Address
|
||||
}
|
||||
|
||||
// AttachmentSummary returns a summary of the attachment without any binary data
|
||||
func AttachmentSummary(a *enmime.Part) Attachment {
|
||||
o := Attachment{}
|
||||
|
||||
@@ -108,10 +108,9 @@ Content-Transfer-Encoding: 7bit
|
||||
<meta http-equiv="content-type" content="text/html; charset=UTF-8">
|
||||
</head>
|
||||
<body>
|
||||
Message with inline image and attachment:<br>
|
||||
<h1>Message with inline image and attachment:</h1>
|
||||
<br>
|
||||
<img src="cid:part1.845LaYlX.wtWMpWwa@gmail.com"
|
||||
moz-do-not-send="false"><br>
|
||||
<p><img src="cid:part1.845LaYlX.wtWMpWwa@gmail.com"></p>
|
||||
<br>
|
||||
<br>
|
||||
</body>
|
||||
|
||||
@@ -62,6 +62,9 @@ func createSearchText(env *enmime.Envelope) string {
|
||||
|
||||
// CleanString removes unwanted characters from stored search text and search queries
|
||||
func cleanString(str string) string {
|
||||
// replace \uFEFF with space, see https://github.com/golang/go/issues/42274#issuecomment-1017258184
|
||||
str = strings.ReplaceAll(str, string('\uFEFF'), " ")
|
||||
|
||||
// remove/replace new lines
|
||||
re := regexp.MustCompile(`(\r?\n|\t|>|<|"|\,|;|\(|\))`)
|
||||
str = re.ReplaceAllString(str, " ")
|
||||
|
||||
@@ -2,7 +2,9 @@ package tools
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/microcosm-cc/bluemonday"
|
||||
"golang.org/x/net/html"
|
||||
)
|
||||
|
||||
@@ -17,3 +19,12 @@ func GetHTMLAttributeVal(e *html.Node, key string) (string, error) {
|
||||
|
||||
return "", fmt.Errorf("%s not found", key)
|
||||
}
|
||||
|
||||
// StripHTML returns text from an HTML string
|
||||
func stripHTML(h string) string {
|
||||
p := bluemonday.StrictPolicy()
|
||||
// // ensure joining html elements are spaced apart, eg table cells etc
|
||||
h = strings.ReplaceAll(h, "><", "> <")
|
||||
// return p.Sanitize(h)
|
||||
return html.UnescapeString(p.Sanitize(h))
|
||||
}
|
||||
|
||||
46
internal/tools/snippets.go
Normal file
46
internal/tools/snippets.go
Normal file
@@ -0,0 +1,46 @@
|
||||
package tools
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// CreateSnippet returns a message snippet. It will use the HTML version (if it exists)
|
||||
// otherwise the text version.
|
||||
func CreateSnippet(text, html string) string {
|
||||
text = strings.TrimSpace(text)
|
||||
html = strings.TrimSpace(html)
|
||||
limit := 200
|
||||
spaceRe := regexp.MustCompile(`\s+`)
|
||||
nlRe := regexp.MustCompile(`\r?\n`)
|
||||
|
||||
if text == "" && html == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
if html != "" {
|
||||
data := nlRe.ReplaceAllString(stripHTML(html), " ")
|
||||
// replace \uFEFF with space, see https://github.com/golang/go/issues/42274#issuecomment-1017258184
|
||||
data = strings.ReplaceAll(data, string('\uFEFF'), " ")
|
||||
data = strings.TrimSpace(spaceRe.ReplaceAllString(data, " "))
|
||||
|
||||
if len(data) <= limit {
|
||||
return data
|
||||
}
|
||||
|
||||
return data[0:limit] + "..."
|
||||
}
|
||||
|
||||
if text != "" {
|
||||
// replace \uFEFF with space, see https://github.com/golang/go/issues/42274#issuecomment-1017258184
|
||||
text = strings.ReplaceAll(text, string('\uFEFF'), " ")
|
||||
text = strings.TrimSpace(spaceRe.ReplaceAllString(text, " "))
|
||||
if len(text) <= limit {
|
||||
return text
|
||||
}
|
||||
|
||||
return text[0:limit] + "..."
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
@@ -43,3 +43,29 @@ func TestCleanTag(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSnippets(t *testing.T) {
|
||||
tests := map[string]string{}
|
||||
tests["this is a test"] = "this is a test"
|
||||
tests["thiS IS a Test"] = "thiS IS a Test"
|
||||
tests["thiS IS a Test :-)"] = "thiS IS a Test :-)"
|
||||
tests["<h1>This is a test.</h1> "] = "This is a test."
|
||||
tests["this_is-a test "] = "this_is-a test"
|
||||
tests["this_is-a&^%%(*)@ test"] = "this_is-a&^%%(*)@ test"
|
||||
tests["<h1>Heading</h1><p>Paragraph</p>"] = "Heading Paragraph"
|
||||
tests[`<h1>Heading</h1>
|
||||
<p>Paragraph</p>`] = "Heading Paragraph"
|
||||
tests[`<h1>Heading</h1><p> <a href="https://github.com">linked text</a></p>`] = "Heading linked text"
|
||||
// broken html
|
||||
tests[`<h1>Heading</h3><p> <a href="https://github.com">linked text.`] = "Heading linked text."
|
||||
// truncation to 200 chars + ...
|
||||
tests["abcdefghijklmnopqrstuvwxyx0123456789 abcdefghijklmnopqrstuvwxyx0123456789 abcdefghijklmnopqrstuvwxyx0123456789 abcdefghijklmnopqrstuvwxyx0123456789 abcdefghijklmnopqrstuvwxyx0123456789 abcdefghijklmnopqrstuvwxyx0123456789 abcdefghijklmnopqrstuvwxyx0123456789 abcdefghijklmnopqrstuvwxyx0123456789 abcdefghijklmnopqrstuvwxyx0123456789"] = "abcdefghijklmnopqrstuvwxyx0123456789 abcdefghijklmnopqrstuvwxyx0123456789 abcdefghijklmnopqrstuvwxyx0123456789 abcdefghijklmnopqrstuvwxyx0123456789 abcdefghijklmnopqrstuvwxyx0123456789 abcdefghijklmno..."
|
||||
|
||||
for str, expected := range tests {
|
||||
res := CreateSnippet(str, str)
|
||||
if res != expected {
|
||||
t.Log("CreateSnippet error:", res, "!=", expected)
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
629
package-lock.json
generated
629
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -15,6 +15,7 @@ import (
|
||||
"text/template"
|
||||
|
||||
"github.com/axllent/mailpit/config"
|
||||
"github.com/axllent/mailpit/internal/auth"
|
||||
"github.com/axllent/mailpit/internal/logger"
|
||||
"github.com/axllent/mailpit/internal/storage"
|
||||
"github.com/axllent/mailpit/server/apiv1"
|
||||
@@ -79,8 +80,8 @@ func Listen() {
|
||||
// put it all together
|
||||
http.Handle("/", r)
|
||||
|
||||
if config.UIAuthFile != "" {
|
||||
logger.Log().Info("[http] enabling web UI basic authentication")
|
||||
if auth.UICredentials != nil {
|
||||
logger.Log().Info("[http] enabling basic authentication")
|
||||
}
|
||||
|
||||
// Mark the application here as ready
|
||||
@@ -158,7 +159,7 @@ func middleWareFunc(fn http.HandlerFunc) http.HandlerFunc {
|
||||
w.Header().Set("Access-Control-Allow-Headers", "*")
|
||||
}
|
||||
|
||||
if config.UIAuthFile != "" {
|
||||
if auth.UICredentials != nil {
|
||||
user, pass, ok := r.BasicAuth()
|
||||
|
||||
if !ok {
|
||||
@@ -166,7 +167,21 @@ func middleWareFunc(fn http.HandlerFunc) http.HandlerFunc {
|
||||
return
|
||||
}
|
||||
|
||||
if !config.UIAuth.Match(user, pass) {
|
||||
if !auth.UICredentials.Match(user, pass) {
|
||||
basicAuthResponse(w)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if auth.UICredentials != nil {
|
||||
user, pass, ok := r.BasicAuth()
|
||||
|
||||
if !ok {
|
||||
basicAuthResponse(w)
|
||||
return
|
||||
}
|
||||
|
||||
if !auth.UICredentials.Match(user, pass) {
|
||||
basicAuthResponse(w)
|
||||
return
|
||||
}
|
||||
@@ -197,7 +212,7 @@ func middlewareHandler(h http.Handler) http.Handler {
|
||||
w.Header().Set("Access-Control-Allow-Headers", "*")
|
||||
}
|
||||
|
||||
if config.UIAuthFile != "" {
|
||||
if auth.UICredentials != nil {
|
||||
user, pass, ok := r.BasicAuth()
|
||||
|
||||
if !ok {
|
||||
@@ -205,7 +220,7 @@ func middlewareHandler(h http.Handler) http.Handler {
|
||||
return
|
||||
}
|
||||
|
||||
if !config.UIAuth.Match(user, pass) {
|
||||
if !auth.UICredentials.Match(user, pass) {
|
||||
basicAuthResponse(w)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/axllent/mailpit/config"
|
||||
"github.com/axllent/mailpit/internal/auth"
|
||||
"github.com/axllent/mailpit/internal/logger"
|
||||
"github.com/axllent/mailpit/internal/storage"
|
||||
"github.com/mhale/smtpd"
|
||||
@@ -129,7 +130,7 @@ func mailHandler(origin net.Addr, from string, to []string, data []byte) error {
|
||||
}
|
||||
|
||||
func authHandler(remoteAddr net.Addr, mechanism string, username []byte, password []byte, _ []byte) (bool, error) {
|
||||
allow := config.SMTPAuthConfig.Match(string(username), string(password))
|
||||
allow := auth.SMTPCredentials.Match(string(username), string(password))
|
||||
if allow {
|
||||
logger.Log().Debugf("[smtpd] allow %s login:%q from:%s", mechanism, string(username), cleanIP(remoteAddr))
|
||||
} else {
|
||||
@@ -149,14 +150,14 @@ func authHandlerAny(remoteAddr net.Addr, mechanism string, username []byte, _ []
|
||||
// Listen starts the SMTPD server
|
||||
func Listen() error {
|
||||
if config.SMTPAuthAllowInsecure {
|
||||
if config.SMTPAuthFile != "" {
|
||||
logger.Log().Infof("[smtpd] enabling login auth via %s (insecure)", config.SMTPAuthFile)
|
||||
if auth.SMTPCredentials != nil {
|
||||
logger.Log().Info("[smtpd] enabling login auth (insecure)")
|
||||
} else if config.SMTPAuthAcceptAny {
|
||||
logger.Log().Info("[smtpd] enabling all auth (insecure)")
|
||||
}
|
||||
} else {
|
||||
if config.SMTPAuthFile != "" {
|
||||
logger.Log().Infof("[smtpd] enabling login auth via %s (TLS)", config.SMTPAuthFile)
|
||||
if auth.SMTPCredentials != nil {
|
||||
logger.Log().Info("[smtpd] enabling login auth (TLS)")
|
||||
} else if config.SMTPAuthAcceptAny {
|
||||
logger.Log().Info("[smtpd] enabling any auth (TLS)")
|
||||
}
|
||||
@@ -181,7 +182,7 @@ func listenAndServe(addr string, handler smtpd.Handler, authHandler smtpd.AuthHa
|
||||
srv.AuthMechs = map[string]bool{"CRAM-MD5": false, "PLAIN": true, "LOGIN": true}
|
||||
}
|
||||
|
||||
if config.SMTPAuthFile != "" {
|
||||
if auth.SMTPCredentials != nil {
|
||||
srv.AuthMechs = map[string]bool{"CRAM-MD5": false, "PLAIN": true, "LOGIN": true}
|
||||
srv.AuthHandler = authHandler
|
||||
srv.AuthRequired = true
|
||||
|
||||
@@ -94,11 +94,29 @@
|
||||
}
|
||||
|
||||
.message {
|
||||
.subject {
|
||||
color: $text-muted;
|
||||
|
||||
b {
|
||||
color: $list-group-color;
|
||||
}
|
||||
|
||||
small {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
&.read {
|
||||
color: $text-muted;
|
||||
|
||||
b {
|
||||
opacity: 0.7;
|
||||
font-weight: normal;
|
||||
color: $list-group-color;
|
||||
}
|
||||
|
||||
small {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
&.selected {
|
||||
|
||||
@@ -141,8 +141,13 @@ export default {
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-6 col-xxl-7 mt-2 mt-lg-0">
|
||||
<div><b>{{ message.Subject != "" ? message.Subject : "[ no subject ]" }}</b></div>
|
||||
<div>
|
||||
<div class="subject text-truncate">
|
||||
<b>{{ message.Subject != "" ? message.Subject : "[ no subject ]" }}</b>
|
||||
</div>
|
||||
<div v-if="message.Snippet != ''" class="small text-muted text-truncate">
|
||||
{{ message.Snippet }}
|
||||
</div>
|
||||
<div v-if="message.Tags.length">
|
||||
<RouterLink class="badge me-1" v-for="t in message.Tags" :to="'/search?q=' + tagEncodeURI(t)"
|
||||
:style="mailbox.showTagColors ? { backgroundColor: colorHash(t) } : { backgroundColor: '#6c757d' }"
|
||||
:title="'Filter messages tagged with ' + t">
|
||||
|
||||
@@ -81,7 +81,7 @@ export default {
|
||||
</button>
|
||||
|
||||
<button class="list-group-item list-group-item-action" data-bs-toggle="modal"
|
||||
data-bs-target="#DeleteAllModal" :disabled="!mailbox.count">
|
||||
data-bs-target="#DeleteAllModal" :disabled="!mailbox.total">
|
||||
<i class="bi bi-trash-fill me-1 text-danger"></i>
|
||||
Delete all
|
||||
</button>
|
||||
|
||||
@@ -19,7 +19,7 @@ export default {
|
||||
return false
|
||||
}
|
||||
|
||||
let re = new RegExp(`\\b[^\-!]tag:"?${tag}"?\\b`, 'i')
|
||||
let re = new RegExp(`(^|\\s)tag:"?${tag}"?($|\\s)`, 'i')
|
||||
return query.match(re)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -443,7 +443,7 @@ export default {
|
||||
aria-labelledby="nav-html-tab" tabindex="0">
|
||||
<div id="responsive-view" :class="scaleHTMLPreview" :style="responsiveSizes[scaleHTMLPreview]">
|
||||
<iframe target-blank="" class="tab-pane d-block" id="preview-html" :srcdoc="sanitizeHTML(message.HTML)"
|
||||
v-on:load="resizeIframe" frameborder="0" style="width: 100%; height: 100%;">
|
||||
v-on:load="resizeIframe" frameborder="0" style="width: 100%; height: 100%; background: #fff;">
|
||||
</iframe>
|
||||
</div>
|
||||
<Attachments v-if="allAttachments(message).length" :message="message"
|
||||
|
||||
@@ -14,9 +14,12 @@ export default {
|
||||
AjaxLoader,
|
||||
},
|
||||
|
||||
emits: ['delete'],
|
||||
|
||||
data() {
|
||||
return {
|
||||
addresses: [],
|
||||
deleteAfterRelease: false,
|
||||
mailbox,
|
||||
allAddresses: [],
|
||||
}
|
||||
@@ -62,6 +65,9 @@ export default {
|
||||
|
||||
self.post(self.resolve('/api/v1/message/' + self.message.ID + '/release'), data, function (response) {
|
||||
self.modal("ReleaseModal").hide()
|
||||
if (self.deleteAfterRelease) {
|
||||
self.$emit('delete')
|
||||
}
|
||||
})
|
||||
}, 100)
|
||||
}
|
||||
@@ -108,6 +114,18 @@ export default {
|
||||
<div class="invalid-feedback">Invalid email address</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<div class="col-sm-10 offset-sm-2">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" v-model="deleteAfterRelease"
|
||||
id="DeleteAfterRelease">
|
||||
<label class="form-check-label" for="DeleteAfterRelease">
|
||||
Delete the message after release
|
||||
</label>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-text text-center" v-if="mailbox.uiConfig.MessageRelay.RecipientAllowlist != ''">
|
||||
Note: A recipient allowlist has been configured. Any mail address not matching it will be rejected.
|
||||
<br class="d-none d-md-inline">
|
||||
|
||||
@@ -341,6 +341,6 @@ export default {
|
||||
|
||||
<AboutMailpit modals />
|
||||
<AjaxLoader :loading="loading" />
|
||||
<Release v-if="message" ref="ReleaseRef" :message="message" />
|
||||
<Release v-if="message" ref="ReleaseRef" :message="message" @delete="deleteMessage" />
|
||||
<Screenshot v-if="message" ref="ScreenshotRef" :message="message" />
|
||||
</template>
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/axllent/mailpit/config"
|
||||
"github.com/axllent/mailpit/internal/auth"
|
||||
"github.com/axllent/mailpit/internal/logger"
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
@@ -99,19 +99,17 @@ func (c *Client) writePump() {
|
||||
|
||||
// ServeWs handles websocket requests from the peer.
|
||||
func ServeWs(hub *Hub, w http.ResponseWriter, r *http.Request) {
|
||||
if config.UIAuthFile != "" {
|
||||
if config.UIAuthFile != "" {
|
||||
user, pass, ok := r.BasicAuth()
|
||||
if auth.UICredentials != nil {
|
||||
user, pass, ok := r.BasicAuth()
|
||||
|
||||
if !ok {
|
||||
basicAuthResponse(w)
|
||||
return
|
||||
}
|
||||
if !ok {
|
||||
basicAuthResponse(w)
|
||||
return
|
||||
}
|
||||
|
||||
if !config.UIAuth.Match(user, pass) {
|
||||
basicAuthResponse(w)
|
||||
return
|
||||
}
|
||||
if !auth.UICredentials.Match(user, pass) {
|
||||
basicAuthResponse(w)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user