mirror of
https://github.com/axllent/mailpit.git
synced 2026-06-30 07:56:06 +00:00
Merge branch 'release/v1.30.2'
This commit is contained in:
2
.github/workflows/build-docker-edge.yml
vendored
2
.github/workflows/build-docker-edge.yml
vendored
@@ -8,7 +8,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
fetch-depth: 0 # required for github-action-get-previous-tag
|
||||
persist-credentials: false
|
||||
|
||||
2
.github/workflows/build-docker.yml
vendored
2
.github/workflows/build-docker.yml
vendored
@@ -8,7 +8,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
|
||||
2
.github/workflows/build-release.yml
vendored
2
.github/workflows/build-release.yml
vendored
@@ -21,7 +21,7 @@ jobs:
|
||||
- goarch: arm
|
||||
goos: windows
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
|
||||
8
.github/workflows/codeql-analysis.yml
vendored
8
.github/workflows/codeql-analysis.yml
vendored
@@ -38,13 +38,13 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4.35.5
|
||||
uses: github/codeql-action/init@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||
@@ -58,7 +58,7 @@ jobs:
|
||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||
# If this step fails, then you should remove it and run the build manually (see below)
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4.35.5
|
||||
uses: github/codeql-action/autobuild@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2
|
||||
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
|
||||
@@ -71,4 +71,4 @@ jobs:
|
||||
# ./location_of_script_within_repo/buildscript.sh
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4.35.5
|
||||
uses: github/codeql-action/analyze@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2
|
||||
|
||||
4
.github/workflows/tests-rqlite.yml
vendored
4
.github/workflows/tests-rqlite.yml
vendored
@@ -17,7 +17,7 @@ jobs:
|
||||
# the HTTP address the rqlite node should advertise
|
||||
HTTP_ADV_ADDR: "localhost:4001"
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Setup Go
|
||||
@@ -25,7 +25,7 @@ jobs:
|
||||
with:
|
||||
go-version: 'stable'
|
||||
cache-dependency-path: "**/*.sum"
|
||||
- run: go test -p 1 ./internal/storage ./server ./internal/smtpd ./internal/pop3 ./internal/tools ./internal/html2text ./internal/htmlcheck ./internal/linkcheck -v
|
||||
- run: go test -p 1 ./internal/storage ./server ./internal/smtpd ./internal/pop3 ./internal/tools ./internal/html2text ./internal/htmlcheck ./internal/linkcheck ./internal/healthcheck -v
|
||||
env:
|
||||
# set Mailpit to use the rqlite service container
|
||||
MP_DATABASE: "http://localhost:4001"
|
||||
|
||||
4
.github/workflows/tests.yml
vendored
4
.github/workflows/tests.yml
vendored
@@ -16,7 +16,7 @@ jobs:
|
||||
with:
|
||||
go-version: ${{ matrix.go-version }}
|
||||
cache: false
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Set up Go environment
|
||||
@@ -33,7 +33,7 @@ jobs:
|
||||
# https://olegk.dev/github-actions-and-go
|
||||
run: gofmt -s -w . && git diff --exit-code
|
||||
- name: Run Go tests
|
||||
run: go test -p 1 ./internal/storage ./server ./internal/smtpd ./internal/pop3 ./internal/tools ./internal/html2text ./internal/htmlcheck ./internal/linkcheck ./internal/shortuuid -v
|
||||
run: go test -p 1 ./internal/storage ./server ./internal/smtpd ./internal/pop3 ./internal/tools ./internal/html2text ./internal/htmlcheck ./internal/linkcheck ./internal/shortuuid ./internal/healthcheck -v
|
||||
- name: Run Go benchmarking
|
||||
run: go test -p 1 ./internal/storage ./internal/html2text -bench=.
|
||||
|
||||
|
||||
22
CHANGELOG.md
22
CHANGELOG.md
@@ -2,6 +2,28 @@
|
||||
|
||||
Notable changes to Mailpit will be documented in this file.
|
||||
|
||||
## [v1.30.2]
|
||||
|
||||
### Security
|
||||
- Fix incomplete SSRF protection in IsInternalIP() detection for IPv6 transition mechanisms (GHSA-w4mc-hhc6-xp28)
|
||||
|
||||
### Feature
|
||||
- Add wait support to readyz ([#697](https://github.com/axllent/mailpit/issues/697))
|
||||
|
||||
### Chore
|
||||
- Compress websocket messages once per broadcast to improve performance ([#695](https://github.com/axllent/mailpit/issues/695))
|
||||
- Toggle websocket compression using HTTP compression setting
|
||||
- Update Github Actions dependencies
|
||||
- Update Go dependencies
|
||||
- Update node dependencies
|
||||
|
||||
### Fix
|
||||
- Adjust header setting order in error response functions ([#699](https://github.com/axllent/mailpit/issues/699))
|
||||
|
||||
### Test
|
||||
- Add readyz tests
|
||||
|
||||
|
||||
## [v1.30.1]
|
||||
|
||||
### Security
|
||||
|
||||
@@ -1,20 +1,19 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/axllent/mailpit/config"
|
||||
"github.com/axllent/mailpit/internal/healthcheck"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var (
|
||||
useHTTPS bool
|
||||
useHTTPS bool
|
||||
readyzWait bool
|
||||
readyzTimeout time.Duration
|
||||
)
|
||||
|
||||
// readyzCmd represents the healthcheck command
|
||||
@@ -22,33 +21,25 @@ var readyzCmd = &cobra.Command{
|
||||
Use: "readyz",
|
||||
Short: "Run a healthcheck to test if Mailpit is running",
|
||||
Long: `This command connects to the /readyz endpoint of a running Mailpit server
|
||||
and exits with a status of 0 if the connection is successful, else with a
|
||||
and exits with a status of 0 if the connection is successful, else with a
|
||||
status 1 if unhealthy.
|
||||
|
||||
If running within Docker, it should automatically detect environment
|
||||
settings to determine the HTTP bind interface & port.
|
||||
`,
|
||||
Run: func(_ *cobra.Command, _ []string) {
|
||||
webroot := strings.TrimRight(path.Join("/", config.Webroot, "/"), "/") + "/"
|
||||
proto := "http"
|
||||
if useHTTPS {
|
||||
proto = "https"
|
||||
uri := healthcheck.URI(config.HTTPListen, config.Webroot, useHTTPS)
|
||||
client := healthcheck.NewClient()
|
||||
|
||||
var err error
|
||||
if readyzWait {
|
||||
err = healthcheck.Wait(client, uri, readyzTimeout)
|
||||
} else {
|
||||
err = healthcheck.Check(client, uri)
|
||||
}
|
||||
|
||||
uri := fmt.Sprintf("%s://%s%sreadyz", proto, config.HTTPListen, webroot)
|
||||
|
||||
conf := &http.Transport{
|
||||
IdleConnTimeout: time.Second * 5,
|
||||
ExpectContinueTimeout: time.Second * 5,
|
||||
TLSHandshakeTimeout: time.Second * 5,
|
||||
// do not verify TLS if this instance is using HTTPS as we connect using IP
|
||||
// so won't be the same as the cert
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, // #nosec
|
||||
}
|
||||
client := &http.Client{Transport: conf}
|
||||
|
||||
res, err := client.Get(uri)
|
||||
if err != nil || res.StatusCode != 200 {
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
},
|
||||
@@ -74,4 +65,6 @@ func init() {
|
||||
readyzCmd.Flags().StringVarP(&config.HTTPListen, "listen", "l", config.HTTPListen, "Set the HTTP bind interface & port")
|
||||
readyzCmd.Flags().StringVar(&config.Webroot, "webroot", config.Webroot, "Set the webroot for web UI & API")
|
||||
readyzCmd.Flags().BoolVar(&useHTTPS, "https", useHTTPS, "Connect via HTTPS (ignores HTTPS validation)")
|
||||
readyzCmd.Flags().BoolVar(&readyzWait, "wait", readyzWait, "Wait until Mailpit is ready instead of checking once")
|
||||
readyzCmd.Flags().DurationVar(&readyzTimeout, "timeout", 30*time.Second, "Maximum time to wait when --wait is set")
|
||||
}
|
||||
|
||||
30
go.mod
30
go.mod
@@ -8,10 +8,10 @@ require (
|
||||
github.com/axllent/ghru/v2 v2.2.3
|
||||
github.com/axllent/semver v1.0.0
|
||||
github.com/goccy/go-yaml v1.19.2
|
||||
github.com/gomarkdown/markdown v0.0.0-20260417124207-7d523f7318df
|
||||
github.com/gomarkdown/markdown v0.0.0-20260614204949-e08cff860f76
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/gorilla/websocket v1.5.3
|
||||
github.com/jhillyerd/enmime/v2 v2.4.0
|
||||
github.com/jhillyerd/enmime/v2 v2.4.1
|
||||
github.com/klauspost/compress v1.18.6
|
||||
github.com/kovidgoyal/imaging v1.8.21
|
||||
github.com/leporo/sqlf v1.4.0
|
||||
@@ -19,18 +19,18 @@ require (
|
||||
github.com/rqlite/gorqlite v0.0.0-20260504155303-50d445fd0ab9
|
||||
github.com/spf13/cobra v1.10.2
|
||||
github.com/spf13/pflag v1.0.10
|
||||
github.com/tg123/go-htpasswd v1.2.4
|
||||
github.com/vanng822/go-premailer v1.33.0
|
||||
golang.org/x/crypto v0.52.0
|
||||
golang.org/x/net v0.55.0
|
||||
golang.org/x/text v0.37.0
|
||||
github.com/tg123/go-htpasswd v1.2.5
|
||||
github.com/vanng822/go-premailer v1.34.0
|
||||
golang.org/x/crypto v0.53.0
|
||||
golang.org/x/net v0.56.0
|
||||
golang.org/x/text v0.38.0
|
||||
golang.org/x/time v0.15.0
|
||||
modernc.org/sqlite v1.51.0
|
||||
modernc.org/sqlite v1.52.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/GehirnInc/crypt v0.0.0-20230320061759-8cc1b52080c5 // indirect
|
||||
github.com/andybalholm/cascadia v1.3.3 // indirect
|
||||
github.com/andybalholm/cascadia v1.3.4 // indirect
|
||||
github.com/cention-sany/utf7 v0.0.0-20170124080048-26cad61bd60a // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/clipperhouse/displaywidth v0.11.0 // indirect
|
||||
@@ -44,9 +44,9 @@ require (
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/kovidgoyal/go-parallel v1.1.1 // indirect
|
||||
github.com/kovidgoyal/go-shm v1.0.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
github.com/mattn/go-colorable v0.1.15 // indirect
|
||||
github.com/mattn/go-isatty v0.0.22 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.23 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.24 // indirect
|
||||
github.com/ncruces/go-strftime v1.0.0 // indirect
|
||||
github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 // indirect
|
||||
github.com/olekukonko/errors v1.3.0 // indirect
|
||||
@@ -57,10 +57,10 @@ 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/image v0.41.0 // indirect
|
||||
golang.org/x/mod v0.36.0 // indirect
|
||||
golang.org/x/sys v0.45.0 // indirect
|
||||
modernc.org/libc v1.72.5 // indirect
|
||||
golang.org/x/image v0.42.0 // indirect
|
||||
golang.org/x/mod v0.37.0 // indirect
|
||||
golang.org/x/sys v0.46.0 // indirect
|
||||
modernc.org/libc v1.73.4 // indirect
|
||||
modernc.org/mathutil v1.7.1 // indirect
|
||||
modernc.org/memory v1.11.0 // indirect
|
||||
)
|
||||
|
||||
141
go.sum
141
go.sum
@@ -2,8 +2,8 @@ github.com/GehirnInc/crypt v0.0.0-20230320061759-8cc1b52080c5 h1:IEjq88XO4PuBDcv
|
||||
github.com/GehirnInc/crypt v0.0.0-20230320061759-8cc1b52080c5/go.mod h1:exZ0C/1emQJAw5tHOaUDyY1ycttqBAPcxuzf7QbY6ec=
|
||||
github.com/PuerkitoBio/goquery v1.12.0 h1:pAcL4g3WRXekcB9AU/y1mbKez2dbY2AajVhtkO8RIBo=
|
||||
github.com/PuerkitoBio/goquery v1.12.0/go.mod h1:802ej+gV2y7bbIhOIoPY5sT183ZW0YFofScC4q/hIpQ=
|
||||
github.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kktS1LM=
|
||||
github.com/andybalholm/cascadia v1.3.3/go.mod h1:xNd9bqTn98Ln4DwST8/nG+H0yuB8Hmgu1YHNnWw0GeA=
|
||||
github.com/andybalholm/cascadia v1.3.4 h1:vM2lgh0Vru9Vwyfm4cQqWP2HHMW0u0+2PAW7Q38Qufg=
|
||||
github.com/andybalholm/cascadia v1.3.4/go.mod h1:BLRmbRjpEtNKieZOCCvYj4RqN+KRA41GBe/5O+G93kM=
|
||||
github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de h1:FxWPpzIjnTlhPwqqXc4/vE0f7GvRjuAsbW+HOIe8KnA=
|
||||
github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de/go.mod h1:DCaWoUhZrYW9p1lxo/cm8EmUOOzAPSEZNGF2DK1dJgw=
|
||||
github.com/axllent/ghru/v2 v2.2.3 h1:nLzbq7jLiYQMxYPU4uBdgKL4jzAaMkBfAif3igpGaaE=
|
||||
@@ -34,9 +34,8 @@ github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
|
||||
github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
||||
github.com/gogs/chardet v0.0.0-20211120154057-b7413eaefb8f h1:3BSP1Tbs2djlpprl7wCLuiqMaUh5SJkkzI2gDs+FgLs=
|
||||
github.com/gogs/chardet v0.0.0-20211120154057-b7413eaefb8f/go.mod h1:Pcatq5tYkCW2Q6yrR2VRHlbHpZ/R4/7qyL1TCF7vl14=
|
||||
github.com/gomarkdown/markdown v0.0.0-20260417124207-7d523f7318df h1:Mwihr/o+v4L5h56rwHLOE20+hh7Okhwno5BHz3zDuao=
|
||||
github.com/gomarkdown/markdown v0.0.0-20260417124207-7d523f7318df/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/gomarkdown/markdown v0.0.0-20260614204949-e08cff860f76 h1:Ltt9ldIaSYEsjA7sPY2c8r9dOmnKM1vlzhh3dxlhBHM=
|
||||
github.com/gomarkdown/markdown v0.0.0-20260614204949-e08cff860f76/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
|
||||
@@ -53,8 +52,8 @@ github.com/inbucket/html2text v1.0.0 h1:N5kza++4uBBDJ2Z3KUnTRyPNoBcW+YfOgNiNmNB+
|
||||
github.com/inbucket/html2text v1.0.0/go.mod h1:5TrhXQKGU+LXurODaSm55Y9eXoPBRnYiOz4x2XfUoJU=
|
||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
github.com/jhillyerd/enmime/v2 v2.4.0 h1:6bPyyg2OPXEK1fKsLT89DntZf05LqaL2cIx+cvkEXTo=
|
||||
github.com/jhillyerd/enmime/v2 v2.4.0/go.mod h1:TLpvqImPiumRecsJK5TYseRw2bPg3g0EtWc+SfU7cMs=
|
||||
github.com/jhillyerd/enmime/v2 v2.4.1 h1:VkBX8GJJ/wbQofWsKP3egRqgXcwmxlY94YUmXTj08kE=
|
||||
github.com/jhillyerd/enmime/v2 v2.4.1/go.mod h1:TLpvqImPiumRecsJK5TYseRw2bPg3g0EtWc+SfU7cMs=
|
||||
github.com/klauspost/compress v1.18.6 h1:2jupLlAwFm95+YDR+NwD2MEfFO9d4z4Prjl1XXDjuao=
|
||||
github.com/klauspost/compress v1.18.6/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
|
||||
github.com/kovidgoyal/go-parallel v1.1.1 h1:1OzpNjtrUkBPq3UaqrnvOoB2F9RttSt811uiUXyI7ok=
|
||||
@@ -65,13 +64,13 @@ github.com/kovidgoyal/imaging v1.8.21 h1:95S2+dowTeKJJHNpf6lnScvIennTr2H0zQotu+p
|
||||
github.com/kovidgoyal/imaging v1.8.21/go.mod h1:976F+zjiQeZ7sd87Pxlm0a64S/w9bImSIWg3sSk1rdQ=
|
||||
github.com/leporo/sqlf v1.4.0 h1:SyWnX/8GSGOzVmanG0Ub1c04mR9nNl6Tq3IeFKX2/4c=
|
||||
github.com/leporo/sqlf v1.4.0/go.mod h1:pgN9yKsAnQ+2ewhbZogr98RcasUjPsHF3oXwPPhHvBw=
|
||||
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||
github.com/mattn/go-colorable v0.1.15 h1:+u9SLTRGnXv73cEsnsmoZBom+dMU88B2M0aDcWy0/jY=
|
||||
github.com/mattn/go-colorable v0.1.15/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||
github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4=
|
||||
github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4=
|
||||
github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
|
||||
github.com/mattn/go-runewidth v0.0.23 h1:7ykA0T0jkPpzSvMS5i9uoNn2Xy3R383f9HDx3RybWcw=
|
||||
github.com/mattn/go-runewidth v0.0.23/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
|
||||
github.com/mattn/go-runewidth v0.0.24 h1:cpokDiIn0MGnhdHwuWnJBITySJ20QyNGnY2kR/ay2DU=
|
||||
github.com/mattn/go-runewidth v0.0.24/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
|
||||
github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y=
|
||||
github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
|
||||
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
|
||||
@@ -113,105 +112,41 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO
|
||||
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/tg123/go-htpasswd v1.2.4 h1:HgH8KKCjdmo7jjXWN9k1nefPBd7Be3tFCTjc2jPraPU=
|
||||
github.com/tg123/go-htpasswd v1.2.4/go.mod h1:EKThQok9xHkun6NBMynNv6Jmu24A33XdZzzl4Q7H1+0=
|
||||
github.com/tg123/go-htpasswd v1.2.5 h1:h+QdWCAp/FebK6fqjsqg9RGYcgEMcaiKNDV+Mg6uk3E=
|
||||
github.com/tg123/go-htpasswd v1.2.5/go.mod h1:grOqB+sLpkA5ousKWPDRS2colmiBSGxlpuXrm8HxtXs=
|
||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||
github.com/vanng822/css v1.0.1 h1:10yiXc4e8NI8ldU6mSrWmSWMuyWgPr9DZ63RSlsgDw8=
|
||||
github.com/vanng822/css v1.0.1/go.mod h1:tcnB1voG49QhCrwq1W0w5hhGasvOg+VQp9i9H1rCM1w=
|
||||
github.com/vanng822/go-premailer v1.33.0 h1:nglIpKn/7e3kIAwYByiH5xpauFur7RwAucqyZ59hcic=
|
||||
github.com/vanng822/go-premailer v1.33.0/go.mod h1:LGYI7ym6FQ7KcHN16LiQRF+tlan7qwhP1KEhpTINFpo=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
github.com/vanng822/go-premailer v1.34.0 h1:CW7RUnjCfXrkuCbgC2wi/Cub7IwKslJWD/OkIBlcQUk=
|
||||
github.com/vanng822/go-premailer v1.34.0/go.mod h1:LGYI7ym6FQ7KcHN16LiQRF+tlan7qwhP1KEhpTINFpo=
|
||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
|
||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
||||
golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988=
|
||||
golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc=
|
||||
golang.org/x/image v0.41.0 h1:8wS72eGJMJaBxK6okTzd4WaXumUlTVlb753MlsSvTCo=
|
||||
golang.org/x/image v0.41.0/go.mod h1:uIc348UZMSvS5Z65CVZ7iDPaNobNFEPeJ4kbqTOszmA=
|
||||
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/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4=
|
||||
golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
|
||||
golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8=
|
||||
golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww=
|
||||
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/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
||||
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
||||
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY=
|
||||
golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
|
||||
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=
|
||||
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
|
||||
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
|
||||
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||
golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
|
||||
golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
|
||||
golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto=
|
||||
golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio=
|
||||
golang.org/x/image v0.42.0 h1:1gSs6ehNWXLbkHBIPcWztk3D/6aIA/8hauiAYtlodVY=
|
||||
golang.org/x/image v0.42.0/go.mod h1:rrpelvGFt+kLPAjPM4HeWPgrl0FtafueU//e5N0qk/Q=
|
||||
golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ=
|
||||
golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0=
|
||||
golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o=
|
||||
golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec=
|
||||
golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM=
|
||||
golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw=
|
||||
golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE=
|
||||
golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4=
|
||||
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
|
||||
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
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/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||
golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c=
|
||||
golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/tools v0.45.0 h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8=
|
||||
golang.org/x/tools v0.45.0/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
modernc.org/cc/v4 v4.28.2 h1:3tQ0lf2ADtoby2EtSP+J7IE2SHwEJdP8ioR59wx7XpY=
|
||||
modernc.org/cc/v4 v4.28.2/go.mod h1:OnovgIhbbMXMu1aISnJ0wvVD1KnW+cAUJkIrAWh+kVI=
|
||||
modernc.org/ccgo/v4 v4.34.2 h1:mxsy2FdrB6+qG3NfXefz1AmWv0ehOSDO4jxgxd7h9yo=
|
||||
modernc.org/ccgo/v4 v4.34.2/go.mod h1:1L7us56+kAKu04p25EATpmBBvhbcqqZ85ibqWVwVgog=
|
||||
modernc.org/cc/v4 v4.28.4 h1:Hd/4Es+MBj+/7hSdZaisNyu6bv3V0Dp2MdllyfqaH+c=
|
||||
modernc.org/cc/v4 v4.28.4/go.mod h1:OnovgIhbbMXMu1aISnJ0wvVD1KnW+cAUJkIrAWh+kVI=
|
||||
modernc.org/ccgo/v4 v4.34.4 h1:OVnSOWQjVKOYkFxoHYB+qQmSHK5gqMqARM+K9DpR/Ws=
|
||||
modernc.org/ccgo/v4 v4.34.4/go.mod h1:qdKqE8FNIYyysougB1RX9MxCzp5oJOcQXSobANJ4TuE=
|
||||
modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM=
|
||||
modernc.org/fileutil v1.4.0/go.mod h1:EqdKFDxiByqxLk8ozOxObDSfcVOv/54xDs/DUHdvCUU=
|
||||
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
|
||||
@@ -220,8 +155,8 @@ modernc.org/gc/v3 v3.1.3 h1:6QAplYyVO+KdPW3pGnqmJDUxtkec8ooEWvks/hhU3lc=
|
||||
modernc.org/gc/v3 v3.1.3/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
|
||||
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
|
||||
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
|
||||
modernc.org/libc v1.72.5 h1:m2OGx9Ser1VvTS4Z9ZJlWs+CBMxutLaTiAWkNz+NB9U=
|
||||
modernc.org/libc v1.72.5/go.mod h1:np0N7KDJ7eUtMZmOqVZNldrZyG+DHLl2B5pg8Hbar3U=
|
||||
modernc.org/libc v1.73.4 h1:+ra4Ui8ngyt8HDcO1FTDPWlkAh6yOdaO2yAoh8MddQA=
|
||||
modernc.org/libc v1.73.4/go.mod h1:DXZ3eO8qMCNn2SnmTNCiC71nJ9Rcq3PsnpU6Vc4rWK8=
|
||||
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
|
||||
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
|
||||
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
|
||||
@@ -230,8 +165,8 @@ modernc.org/opt v0.2.0 h1:tGyef5ApycA7FSEOMraay9SaTk5zmbx7Tu+cJs4QKZg=
|
||||
modernc.org/opt v0.2.0/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
|
||||
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
|
||||
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
|
||||
modernc.org/sqlite v1.51.0 h1:aH/MMSoayAIhozZ7uJbVTT9QO/VhzBf0J9tymmmuC/U=
|
||||
modernc.org/sqlite v1.51.0/go.mod h1:tcNzv5p84E0skkmJn038y+hWJbLQXQqEnQfeh5r2JLM=
|
||||
modernc.org/sqlite v1.52.0 h1:p4dhYh2tXZCiyaqHwRVJDjIGKWyXayiQpThxgDzJaxo=
|
||||
modernc.org/sqlite v1.52.0/go.mod h1:tcNzv5p84E0skkmJn038y+hWJbLQXQqEnQfeh5r2JLM=
|
||||
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
|
||||
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
|
||||
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
||||
|
||||
70
internal/healthcheck/healthcheck.go
Normal file
70
internal/healthcheck/healthcheck.go
Normal file
@@ -0,0 +1,70 @@
|
||||
// Package healthcheck probes a running Mailpit instance's /readyz endpoint.
|
||||
package healthcheck
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"path"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// PollInterval is the delay between polls in Wait. Exported as a variable so
|
||||
// tests can shorten it.
|
||||
var PollInterval = time.Second
|
||||
|
||||
// URI builds the readyz URL from a listen address, webroot, and TLS flag.
|
||||
func URI(listen, webroot string, https bool) string {
|
||||
proto := "http"
|
||||
if https {
|
||||
proto = "https"
|
||||
}
|
||||
root := strings.TrimRight(path.Join("/", webroot, "/"), "/") + "/"
|
||||
return fmt.Sprintf("%s://%s%sreadyz", proto, listen, root)
|
||||
}
|
||||
|
||||
// NewClient returns an HTTP client suitable for probing a Mailpit readyz
|
||||
// endpoint. TLS verification is disabled because probes typically connect via
|
||||
// IP, which won't match the server certificate.
|
||||
func NewClient() *http.Client {
|
||||
return &http.Client{Transport: &http.Transport{
|
||||
IdleConnTimeout: time.Second * 5,
|
||||
ExpectContinueTimeout: time.Second * 5,
|
||||
TLSHandshakeTimeout: time.Second * 5,
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, // #nosec
|
||||
}}
|
||||
}
|
||||
|
||||
// Check makes a single readiness probe. Returns nil if the server responds
|
||||
// with 200 OK.
|
||||
func Check(client *http.Client, uri string) error {
|
||||
res, err := client.Get(uri)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() { _ = res.Body.Close() }()
|
||||
|
||||
if res.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("unexpected status: %s", res.Status)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Wait polls uri until Check succeeds or timeout elapses.
|
||||
func Wait(client *http.Client, uri string, timeout time.Duration) error {
|
||||
deadline := time.Now().Add(timeout)
|
||||
|
||||
for {
|
||||
if err := Check(client, uri); err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if time.Now().After(deadline) {
|
||||
return fmt.Errorf("timed out after %s waiting for Mailpit to become ready", timeout)
|
||||
}
|
||||
|
||||
time.Sleep(PollInterval)
|
||||
}
|
||||
}
|
||||
88
internal/healthcheck/healthcheck_test.go
Normal file
88
internal/healthcheck/healthcheck_test.go
Normal file
@@ -0,0 +1,88 @@
|
||||
package healthcheck
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestCheck(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}))
|
||||
t.Cleanup(srv.Close)
|
||||
|
||||
if err := Check(srv.Client(), srv.URL); err != nil {
|
||||
t.Fatalf("Check() error = %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWaitRetriesUntilSuccess(t *testing.T) {
|
||||
oldPoll := PollInterval
|
||||
PollInterval = time.Millisecond
|
||||
t.Cleanup(func() { PollInterval = oldPoll })
|
||||
|
||||
var calls int
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
calls++
|
||||
if calls == 1 {
|
||||
w.WriteHeader(http.StatusServiceUnavailable)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}))
|
||||
t.Cleanup(srv.Close)
|
||||
|
||||
if err := Wait(srv.Client(), srv.URL, 100*time.Millisecond); err != nil {
|
||||
t.Fatalf("Wait() error = %v", err)
|
||||
}
|
||||
|
||||
if calls < 2 {
|
||||
t.Fatalf("Wait() calls = %d, want at least 2", calls)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWaitTimesOut(t *testing.T) {
|
||||
oldPoll := PollInterval
|
||||
PollInterval = time.Millisecond
|
||||
t.Cleanup(func() { PollInterval = oldPoll })
|
||||
|
||||
var calls int
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
calls++
|
||||
w.WriteHeader(http.StatusServiceUnavailable)
|
||||
}))
|
||||
t.Cleanup(srv.Close)
|
||||
|
||||
if err := Wait(srv.Client(), srv.URL, 5*time.Millisecond); err == nil {
|
||||
t.Fatal("Wait() error = nil, want timeout")
|
||||
}
|
||||
|
||||
if calls == 0 {
|
||||
t.Fatal("Wait() did not call the endpoint")
|
||||
}
|
||||
}
|
||||
|
||||
func TestURI(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
listen string
|
||||
webroot string
|
||||
https bool
|
||||
want string
|
||||
}{
|
||||
{"plain", "127.0.0.1:8025", "", false, "http://127.0.0.1:8025/readyz"},
|
||||
{"https", "127.0.0.1:8025", "", true, "https://127.0.0.1:8025/readyz"},
|
||||
{"webroot", "127.0.0.1:8025", "/mailpit", false, "http://127.0.0.1:8025/mailpit/readyz"},
|
||||
{"webroot trailing slash", "127.0.0.1:8025", "/mailpit/", false, "http://127.0.0.1:8025/mailpit/readyz"},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
if got := URI(tc.listen, tc.webroot, tc.https); got != tc.want {
|
||||
t.Errorf("URI() = %q, want %q", got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,35 +1,111 @@
|
||||
package tools
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"net"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
// cgnatRange is the CGNAT shared address space (RFC 6598), not covered by net.IP.IsPrivate().
|
||||
// CGNAT (Carrier-Grade NAT) is a technique used by ISPs to conserve IPv4 addresses. Instead of assigning a unique
|
||||
// public IP to every customer, the ISP places many customers behind a shared NAT, then gives them all addresses
|
||||
// from the reserved 100.64.0.0/10 range (RFC 6598) on their internal network.
|
||||
var cgnatRange = func() *net.IPNet {
|
||||
_, cidr, _ := net.ParseCIDR("100.64.0.0/10")
|
||||
var (
|
||||
// cgnatRange is the CGNAT shared address space (RFC 6598), not covered by net.IP.IsPrivate().
|
||||
// CGNAT (Carrier-Grade NAT) is a technique used by ISPs to conserve IPv4 addresses. Instead of assigning a unique
|
||||
// public IP to every customer, the ISP places many customers behind a shared NAT, then gives them all addresses
|
||||
// from the reserved 100.64.0.0/10 range (RFC 6598) on their internal network.
|
||||
cgnatRange = mustCIDR("100.64.0.0/10")
|
||||
|
||||
// IPv6 transition prefixes that embed an IPv4 destination. Go's net.IP.Is* family
|
||||
// does not decode these, so an IPv6 literal of one of these forms can carry a
|
||||
// private/link-local IPv4 destination past the stdlib checks. See golang/go#79925.
|
||||
nat64WellKnown = mustCIDR("64:ff9b::/96") // RFC 6052
|
||||
nat64LocalUse = mustCIDR("64:ff9b:1::/48") // RFC 8215
|
||||
sixToFour = mustCIDR("2002::/16") // RFC 3056
|
||||
teredo = mustCIDR("2001::/32") // RFC 4380
|
||||
ipv4Compatible = mustCIDR("::/96") // RFC 4291 §2.5.5.1
|
||||
// IPv4-mapped IPv6 (::ffff:0:0/96, RFC 4291 §2.5.5.2) is normalised by net.IP.To4,
|
||||
// so the stdlib Is* checks above already see the embedded IPv4 - no decode needed.
|
||||
|
||||
// Direct IPv6 prefixes outside the scope of Go's stdlib Is* family.
|
||||
deprecatedSiteLocal = mustCIDR("fec0::/10") // RFC 3879 / RFC 4291 §2.5.7 — deprecated, still routable on dual-stack hosts
|
||||
documentationPrefix = mustCIDR("2001:db8::/32") // RFC 3849 — documentation only, must not appear in real traffic
|
||||
)
|
||||
|
||||
// MustCIDR is a helper for use in global var initialisation.
|
||||
func mustCIDR(s string) *net.IPNet {
|
||||
_, cidr, _ := net.ParseCIDR(s)
|
||||
|
||||
return cidr
|
||||
}()
|
||||
}
|
||||
|
||||
// IsInternalIP checks if the given IP address is an internal IP address (e.g., loopback, private, link-local, or multicast).
|
||||
// IsLoopback — 127.0.0.0/8, ::1
|
||||
// IsPrivate — 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, fc00::/7
|
||||
// IsLinkLocalUnicast — 169.254.0.0/16, fe80::/10 (covers cloud metadata 169.254.169.254)
|
||||
// IsLinkLocalMulticast — 224.0.0.0/24, ff02::/16
|
||||
// IsUnspecified — 0.0.0.0, ::
|
||||
// IsMulticast — 224.0.0.0/4, ff00::/8
|
||||
// CGNAT — 100.64.0.0/10 (RFC 6598) (Carrier-Grade NAT)
|
||||
// IsLoopback - 127.0.0.0/8, ::1
|
||||
// IsPrivate - 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, fc00::/7
|
||||
// IsLinkLocalUnicast - 169.254.0.0/16, fe80::/10 (covers cloud metadata 169.254.169.254)
|
||||
// IsLinkLocalMulticast - 224.0.0.0/24, ff02::/16
|
||||
// IsUnspecified - 0.0.0.0, ::
|
||||
// IsMulticast - 224.0.0.0/4, ff00::/8
|
||||
// CGNAT - 100.64.0.0/10 (RFC 6598) (Carrier-Grade NAT)
|
||||
// IPv6 transition forms - NAT64 (RFC 6052/8215), 6to4 (RFC 3056), Teredo (RFC 4380),
|
||||
// IPv4-compatible (RFC 4291) - re-checked against their embedded IPv4.
|
||||
func IsInternalIP(ip net.IP) bool {
|
||||
return ip.IsLoopback() ||
|
||||
if ip.IsLoopback() ||
|
||||
ip.IsPrivate() ||
|
||||
ip.IsLinkLocalUnicast() ||
|
||||
ip.IsLinkLocalMulticast() ||
|
||||
ip.IsUnspecified() ||
|
||||
ip.IsMulticast() ||
|
||||
cgnatRange.Contains(ip)
|
||||
cgnatRange.Contains(ip) ||
|
||||
deprecatedSiteLocal.Contains(ip) ||
|
||||
documentationPrefix.Contains(ip) {
|
||||
return true
|
||||
}
|
||||
|
||||
if embeddedV4, ok := embeddedIPv4(ip); ok {
|
||||
return IsInternalIP(embeddedV4)
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// embeddedIPv4 returns the IPv4 destination encoded in ip, if ip is an IPv6 form
|
||||
// documented to carry one. Without this, an IPv6 literal like 64:ff9b::a9fe:a9fe
|
||||
// (NAT64 wrapping 169.254.169.254) bypasses the stdlib Is* checks above.
|
||||
func embeddedIPv4(ip net.IP) (net.IP, bool) {
|
||||
// Skip addresses that are already IPv4 (4-byte or IPv4-mapped IPv6) - those are
|
||||
// covered by the stdlib Is* checks via To4 normalisation. Re-entering here would
|
||||
// recurse infinitely, because To16 turns an IPv4 back into ::ffff:<ipv4>.
|
||||
if ip.To4() != nil {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
ip16 := ip.To16()
|
||||
if ip16 == nil || len(ip16) != net.IPv6len {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
switch {
|
||||
case nat64WellKnown.Contains(ip16), nat64LocalUse.Contains(ip16),
|
||||
ipv4Compatible.Contains(ip16):
|
||||
// Last 32 bits are the embedded IPv4.
|
||||
return net.IPv4(ip16[12], ip16[13], ip16[14], ip16[15]).To4(), true
|
||||
case sixToFour.Contains(ip16):
|
||||
// Bits 16..47 are the embedded IPv4.
|
||||
return net.IPv4(ip16[2], ip16[3], ip16[4], ip16[5]).To4(), true
|
||||
case teredo.Contains(ip16):
|
||||
// Bits 96..127 are the embedded IPv4 XOR'd with 0xFFFFFFFF.
|
||||
x := binary.BigEndian.Uint32(ip16[12:16]) ^ 0xFFFFFFFF
|
||||
b := make([]byte, 4)
|
||||
binary.BigEndian.PutUint32(b, x)
|
||||
return net.IPv4(b[0], b[1], b[2], b[3]).To4(), true
|
||||
case ip16[10] == 0x5e && ip16[11] == 0xfe:
|
||||
// ISATAP (RFC 5214) - interface identifier ends with :5efe:<ipv4>. The /64
|
||||
// prefix is not fixed (any subnet can carry ISATAP), so match structurally
|
||||
// on bytes 10-11 and treat bytes 12-15 as the embedded IPv4. Must run after
|
||||
// the fixed-prefix cases above (Teredo can legitimately have 5efe in bytes
|
||||
// 10-11; its embedding takes precedence).
|
||||
return net.IPv4(ip16[12], ip16[13], ip16[14], ip16[15]).To4(), true
|
||||
}
|
||||
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// IsValidLinkURL checks if the provided string is a valid URL with http or https scheme and a non-empty hostname.
|
||||
|
||||
@@ -19,11 +19,27 @@ func TestIsInternalIP(t *testing.T) {
|
||||
"224.0.0.1", // multicast
|
||||
"100.64.0.1", // CGNAT start
|
||||
"100.127.255.255", // CGNAT end
|
||||
// IPv6 transition forms embedding an internal IPv4 destination — golang/go#79925.
|
||||
"64:ff9b::a9fe:a9fe", // NAT64 well-known (RFC 6052) wrapping 169.254.169.254
|
||||
"64:ff9b:1::a9fe:a9fe", // NAT64 local-use (RFC 8215) wrapping 169.254.169.254
|
||||
"2002:a9fe:a9fe::", // 6to4 (RFC 3056) wrapping 169.254.169.254
|
||||
"::a9fe:a9fe", // IPv4-compatible IPv6 (RFC 4291) wrapping 169.254.169.254
|
||||
"64:ff9b::7f00:1", // NAT64 wrapping 127.0.0.1
|
||||
"2002:0a00:0001::", // 6to4 wrapping 10.0.0.1
|
||||
"::ffff:169.254.169.254", // IPv4-mapped (also caught by stdlib via To4)
|
||||
"::5efe:a9fe:a9fe", // ISATAP (RFC 5214) wrapping 169.254.169.254
|
||||
"2001:db8::5efe:7f00:1", // ISATAP under a documentation prefix wrapping 127.0.0.1
|
||||
"fec0::1", // deprecated site-local (RFC 3879 / RFC 4291 §2.5.7)
|
||||
"2001:db8::1", // documentation prefix (RFC 3849)
|
||||
"2001:db8::5efe:0808:0808", // documentation prefix (blocked regardless of embedded IPv4)
|
||||
}
|
||||
external := []string{
|
||||
"8.8.8.8",
|
||||
"1.1.1.1",
|
||||
"100.128.0.1", // just outside CGNAT range
|
||||
"100.128.0.1", // just outside CGNAT range
|
||||
"2001:4860:4860::8888", // Google public DNS over IPv6
|
||||
"2002:0808:0808::", // 6to4 wrapping 8.8.8.8 (public IPv4)
|
||||
"64:ff9b::0808:0808", // NAT64 wrapping 8.8.8.8 (public IPv4)
|
||||
}
|
||||
|
||||
for _, s := range internal {
|
||||
|
||||
1058
package-lock.json
generated
1058
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -16,8 +16,8 @@ import (
|
||||
func fourOFour(w http.ResponseWriter) {
|
||||
w.Header().Set("Referrer-Policy", "no-referrer")
|
||||
w.Header().Set("Content-Security-Policy", config.ContentSecurityPolicy)
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
w.Header().Set("Content-Type", "text/plain")
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
_, _ = fmt.Fprint(w, "404 page not found")
|
||||
}
|
||||
|
||||
@@ -25,8 +25,8 @@ func fourOFour(w http.ResponseWriter) {
|
||||
func httpError(w http.ResponseWriter, msg string) {
|
||||
w.Header().Set("Referrer-Policy", "no-referrer")
|
||||
w.Header().Set("Content-Security-Policy", config.ContentSecurityPolicy)
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
w.Header().Set("Content-Type", "text/plain")
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
_, _ = fmt.Fprint(w, msg)
|
||||
}
|
||||
|
||||
@@ -34,10 +34,10 @@ func httpError(w http.ResponseWriter, msg string) {
|
||||
func httpJSONError(w http.ResponseWriter, msg string) {
|
||||
w.Header().Set("Referrer-Policy", "no-referrer")
|
||||
w.Header().Set("Content-Security-Policy", config.ContentSecurityPolicy)
|
||||
w.Header().Add("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
e := struct{ Error string }{Error: msg}
|
||||
|
||||
w.Header().Add("Content-Type", "application/json")
|
||||
if err := json.NewEncoder(w).Encode(e); err != nil {
|
||||
httpError(w, err.Error())
|
||||
}
|
||||
|
||||
@@ -79,6 +79,10 @@ func setCORSOrigins() {
|
||||
corsAllowOrigins[asciiFoldString(host)] = true
|
||||
}
|
||||
|
||||
if len(corsAllowOrigins) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
if _, wildCard := corsAllowOrigins["*"]; wildCard {
|
||||
// reset to just wildcard
|
||||
corsAllowOrigins = make(map[string]bool)
|
||||
|
||||
@@ -8,6 +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"
|
||||
@@ -32,7 +33,7 @@ var (
|
||||
var upgrader = websocket.Upgrader{
|
||||
ReadBufferSize: 1024,
|
||||
WriteBufferSize: 1024,
|
||||
EnableCompression: true,
|
||||
EnableCompression: !config.DisableHTTPCompression,
|
||||
CheckOrigin: func(_ *http.Request) bool {
|
||||
// origin is checked via server.go's CORS settings
|
||||
return true
|
||||
@@ -47,7 +48,7 @@ type Client struct {
|
||||
conn *websocket.Conn
|
||||
|
||||
// Buffered channel of outbound messages.
|
||||
send chan []byte
|
||||
send chan *websocket.PreparedMessage
|
||||
}
|
||||
|
||||
// ReadPump is used here solely to monitor the connection, not to actually receive messages.
|
||||
@@ -90,7 +91,7 @@ func (c *Client) writePump() {
|
||||
return
|
||||
}
|
||||
|
||||
if err := c.conn.WriteMessage(websocket.TextMessage, message); err != nil {
|
||||
if err := c.conn.WritePreparedMessage(message); err != nil {
|
||||
return
|
||||
}
|
||||
case <-ticker.C:
|
||||
@@ -124,7 +125,7 @@ func ServeWs(hub *Hub, w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
client := &Client{hub: hub, conn: conn, send: make(chan []byte, 256)}
|
||||
client := &Client{hub: hub, conn: conn, send: make(chan *websocket.PreparedMessage, 256)}
|
||||
client.hub.register <- client
|
||||
|
||||
// Allow collection of memory referenced by the caller by doing all work in new goroutines.
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/axllent/mailpit/internal/logger"
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
|
||||
// Hub maintains the set of active clients and broadcasts messages to the
|
||||
@@ -61,9 +62,14 @@ func (h *Hub) Run() {
|
||||
h.clientCount.Add(-1)
|
||||
}
|
||||
case message := <-h.Broadcast:
|
||||
prepared, err := websocket.NewPreparedMessage(websocket.TextMessage, message)
|
||||
if err != nil {
|
||||
logger.Log().Errorf("[websocket] error preparing message: %s", err.Error())
|
||||
continue
|
||||
}
|
||||
for client := range h.Clients {
|
||||
select {
|
||||
case client.send <- message:
|
||||
case client.send <- prepared:
|
||||
default:
|
||||
close(client.send)
|
||||
delete(h.Clients, client)
|
||||
|
||||
Reference in New Issue
Block a user