Compare commits
No commits in common. "8a10eef2f53d39f0095b645844e2f3d9f73e9038" and "a6efd7710e09a4c5ed27ff1a0646def81ab3c2fd" have entirely different histories.
8a10eef2f5
...
a6efd7710e
29 changed files with 479 additions and 1073 deletions
27
go.mod
27
go.mod
|
|
@ -5,7 +5,6 @@ go 1.24.6
|
||||||
require (
|
require (
|
||||||
github.com/JGLTechnologies/gin-rate-limit v1.5.6
|
github.com/JGLTechnologies/gin-rate-limit v1.5.6
|
||||||
github.com/emersion/go-smtp v0.24.0
|
github.com/emersion/go-smtp v0.24.0
|
||||||
github.com/getkin/kin-openapi v0.133.0
|
|
||||||
github.com/gin-gonic/gin v1.11.0
|
github.com/gin-gonic/gin v1.11.0
|
||||||
github.com/google/uuid v1.6.0
|
github.com/google/uuid v1.6.0
|
||||||
github.com/oapi-codegen/runtime v1.1.2
|
github.com/oapi-codegen/runtime v1.1.2
|
||||||
|
|
@ -16,27 +15,27 @@ require (
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect
|
|
||||||
github.com/bytedance/gopkg v0.1.3 // indirect
|
github.com/bytedance/gopkg v0.1.3 // indirect
|
||||||
github.com/bytedance/sonic v1.15.0 // indirect
|
github.com/bytedance/sonic v1.14.2 // indirect
|
||||||
github.com/bytedance/sonic/loader v0.5.0 // indirect
|
github.com/bytedance/sonic/loader v0.4.0 // indirect
|
||||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||||
github.com/cloudwego/base64x v0.1.6 // indirect
|
github.com/cloudwego/base64x v0.1.6 // indirect
|
||||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||||
github.com/dprotaso/go-yit v0.0.0-20220510233725-9ba8df137936 // indirect
|
github.com/dprotaso/go-yit v0.0.0-20220510233725-9ba8df137936 // indirect
|
||||||
github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6 // indirect
|
github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6 // indirect
|
||||||
github.com/gabriel-vasile/mimetype v1.4.12 // indirect
|
github.com/gabriel-vasile/mimetype v1.4.11 // indirect
|
||||||
|
github.com/getkin/kin-openapi v0.133.0 // indirect
|
||||||
github.com/gin-contrib/sse v1.1.0 // indirect
|
github.com/gin-contrib/sse v1.1.0 // indirect
|
||||||
github.com/go-openapi/jsonpointer v0.22.4 // indirect
|
github.com/go-openapi/jsonpointer v0.22.2 // indirect
|
||||||
github.com/go-openapi/swag/jsonname v0.25.4 // indirect
|
github.com/go-openapi/swag/jsonname v0.25.1 // indirect
|
||||||
github.com/go-playground/locales v0.14.1 // indirect
|
github.com/go-playground/locales v0.14.1 // indirect
|
||||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||||
github.com/go-playground/validator/v10 v10.30.1 // indirect
|
github.com/go-playground/validator/v10 v10.28.0 // indirect
|
||||||
github.com/goccy/go-json v0.10.5 // indirect
|
github.com/goccy/go-json v0.10.5 // indirect
|
||||||
github.com/goccy/go-yaml v1.19.2 // indirect
|
github.com/goccy/go-yaml v1.18.0 // indirect
|
||||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||||
github.com/jackc/pgx/v5 v5.8.0 // indirect
|
github.com/jackc/pgx/v5 v5.7.6 // indirect
|
||||||
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
||||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||||
github.com/jinzhu/now v1.1.5 // indirect
|
github.com/jinzhu/now v1.1.5 // indirect
|
||||||
|
|
@ -46,7 +45,7 @@ require (
|
||||||
github.com/leodido/go-urn v1.4.0 // indirect
|
github.com/leodido/go-urn v1.4.0 // indirect
|
||||||
github.com/mailru/easyjson v0.9.1 // indirect
|
github.com/mailru/easyjson v0.9.1 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
github.com/mattn/go-sqlite3 v1.14.33 // indirect
|
github.com/mattn/go-sqlite3 v1.14.32 // indirect
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||||
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect
|
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect
|
||||||
|
|
@ -56,8 +55,8 @@ require (
|
||||||
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||||
github.com/perimeterx/marshmallow v1.1.5 // indirect
|
github.com/perimeterx/marshmallow v1.1.5 // indirect
|
||||||
github.com/quic-go/qpack v0.6.0 // indirect
|
github.com/quic-go/qpack v0.6.0 // indirect
|
||||||
github.com/quic-go/quic-go v0.59.0 // indirect
|
github.com/quic-go/quic-go v0.57.0 // indirect
|
||||||
github.com/redis/go-redis/v9 v9.17.2 // indirect
|
github.com/redis/go-redis/v9 v9.16.0 // indirect
|
||||||
github.com/speakeasy-api/jsonpath v0.6.0 // indirect
|
github.com/speakeasy-api/jsonpath v0.6.0 // indirect
|
||||||
github.com/speakeasy-api/openapi-overlay v0.10.2 // indirect
|
github.com/speakeasy-api/openapi-overlay v0.10.2 // indirect
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||||
|
|
@ -72,7 +71,7 @@ require (
|
||||||
golang.org/x/sys v0.40.0 // indirect
|
golang.org/x/sys v0.40.0 // indirect
|
||||||
golang.org/x/text v0.33.0 // indirect
|
golang.org/x/text v0.33.0 // indirect
|
||||||
golang.org/x/tools v0.40.0 // indirect
|
golang.org/x/tools v0.40.0 // indirect
|
||||||
google.golang.org/protobuf v1.36.11 // indirect
|
google.golang.org/protobuf v1.36.10 // indirect
|
||||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
)
|
)
|
||||||
|
|
|
||||||
31
go.sum
31
go.sum
|
|
@ -1,9 +1,5 @@
|
||||||
github.com/JGLTechnologies/gin-rate-limit v1.5.6 h1:BrL2wXrF7SSqmB88YTGFVKMGVcjURMUeKqwQrlmzweI=
|
github.com/JGLTechnologies/gin-rate-limit v1.5.6 h1:BrL2wXrF7SSqmB88YTGFVKMGVcjURMUeKqwQrlmzweI=
|
||||||
github.com/JGLTechnologies/gin-rate-limit v1.5.6/go.mod h1:fwUuBegxLKm8+/4ST0zDFssRFTFaVZ7bH3ApK7iNZww=
|
github.com/JGLTechnologies/gin-rate-limit v1.5.6/go.mod h1:fwUuBegxLKm8+/4ST0zDFssRFTFaVZ7bH3ApK7iNZww=
|
||||||
github.com/RaveNoX/go-jsoncommentstrip v1.0.0/go.mod h1:78ihd09MekBnJnxpICcwzCMzGrKSKYe4AqU6PDYYpjk=
|
|
||||||
github.com/apapsch/go-jsonmerge/v2 v2.0.0 h1:axGnT1gRIfimI7gJifB699GoE/oq+F2MU7Dml6nw9rQ=
|
|
||||||
github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP2+08jFMw88y4klk=
|
|
||||||
github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w=
|
|
||||||
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
|
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
|
||||||
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
|
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
|
||||||
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
|
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
|
||||||
|
|
@ -12,12 +8,8 @@ github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M
|
||||||
github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM=
|
github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM=
|
||||||
github.com/bytedance/sonic v1.14.2 h1:k1twIoe97C1DtYUo+fZQy865IuHia4PR5RPiuGPPIIE=
|
github.com/bytedance/sonic v1.14.2 h1:k1twIoe97C1DtYUo+fZQy865IuHia4PR5RPiuGPPIIE=
|
||||||
github.com/bytedance/sonic v1.14.2/go.mod h1:T80iDELeHiHKSc0C9tubFygiuXoGzrkjKzX2quAx980=
|
github.com/bytedance/sonic v1.14.2/go.mod h1:T80iDELeHiHKSc0C9tubFygiuXoGzrkjKzX2quAx980=
|
||||||
github.com/bytedance/sonic v1.15.0 h1:/PXeWFaR5ElNcVE84U0dOHjiMHQOwNIx3K4ymzh/uSE=
|
|
||||||
github.com/bytedance/sonic v1.15.0/go.mod h1:tFkWrPz0/CUCLEF4ri4UkHekCIcdnkqXw9VduqpJh0k=
|
|
||||||
github.com/bytedance/sonic/loader v0.4.0 h1:olZ7lEqcxtZygCK9EKYKADnpQoYkRQxaeY2NYzevs+o=
|
github.com/bytedance/sonic/loader v0.4.0 h1:olZ7lEqcxtZygCK9EKYKADnpQoYkRQxaeY2NYzevs+o=
|
||||||
github.com/bytedance/sonic/loader v0.4.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo=
|
github.com/bytedance/sonic/loader v0.4.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo=
|
||||||
github.com/bytedance/sonic/loader v0.5.0 h1:gXH3KVnatgY7loH5/TkeVyXPfESoqSBSBEiDd5VjlgE=
|
|
||||||
github.com/bytedance/sonic/loader v0.5.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo=
|
|
||||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
||||||
|
|
@ -42,8 +34,6 @@ github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWo
|
||||||
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.11 h1:AQvxbp830wPhHTqc1u7nzoLT+ZFxGY7emj5DR5DYFik=
|
github.com/gabriel-vasile/mimetype v1.4.11 h1:AQvxbp830wPhHTqc1u7nzoLT+ZFxGY7emj5DR5DYFik=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.11/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
|
github.com/gabriel-vasile/mimetype v1.4.11/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw=
|
|
||||||
github.com/gabriel-vasile/mimetype v1.4.12/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
|
|
||||||
github.com/getkin/kin-openapi v0.133.0 h1:pJdmNohVIJ97r4AUFtEXRXwESr8b0bD721u/Tz6k8PQ=
|
github.com/getkin/kin-openapi v0.133.0 h1:pJdmNohVIJ97r4AUFtEXRXwESr8b0bD721u/Tz6k8PQ=
|
||||||
github.com/getkin/kin-openapi v0.133.0/go.mod h1:boAciF6cXk5FhPqe/NQeBTeenbjqU4LhWBf09ILVvWE=
|
github.com/getkin/kin-openapi v0.133.0/go.mod h1:boAciF6cXk5FhPqe/NQeBTeenbjqU4LhWBf09ILVvWE=
|
||||||
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
|
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
|
||||||
|
|
@ -52,12 +42,8 @@ github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk=
|
||||||
github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls=
|
github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls=
|
||||||
github.com/go-openapi/jsonpointer v0.22.2 h1:JDQEe4B9j6K3tQ7HQQTZfjR59IURhjjLxet2FB4KHyg=
|
github.com/go-openapi/jsonpointer v0.22.2 h1:JDQEe4B9j6K3tQ7HQQTZfjR59IURhjjLxet2FB4KHyg=
|
||||||
github.com/go-openapi/jsonpointer v0.22.2/go.mod h1:0lBbqeRsQ5lIanv3LHZBrmRGHLHcQoOXQnf88fHlGWo=
|
github.com/go-openapi/jsonpointer v0.22.2/go.mod h1:0lBbqeRsQ5lIanv3LHZBrmRGHLHcQoOXQnf88fHlGWo=
|
||||||
github.com/go-openapi/jsonpointer v0.22.4 h1:dZtK82WlNpVLDW2jlA1YCiVJFVqkED1MegOUy9kR5T4=
|
|
||||||
github.com/go-openapi/jsonpointer v0.22.4/go.mod h1:elX9+UgznpFhgBuaMQ7iu4lvvX1nvNsesQ3oxmYTw80=
|
|
||||||
github.com/go-openapi/swag/jsonname v0.25.1 h1:Sgx+qbwa4ej6AomWC6pEfXrA6uP2RkaNjA9BR8a1RJU=
|
github.com/go-openapi/swag/jsonname v0.25.1 h1:Sgx+qbwa4ej6AomWC6pEfXrA6uP2RkaNjA9BR8a1RJU=
|
||||||
github.com/go-openapi/swag/jsonname v0.25.1/go.mod h1:71Tekow6UOLBD3wS7XhdT98g5J5GR13NOTQ9/6Q11Zo=
|
github.com/go-openapi/swag/jsonname v0.25.1/go.mod h1:71Tekow6UOLBD3wS7XhdT98g5J5GR13NOTQ9/6Q11Zo=
|
||||||
github.com/go-openapi/swag/jsonname v0.25.4 h1:bZH0+MsS03MbnwBXYhuTttMOqk+5KcQ9869Vye1bNHI=
|
|
||||||
github.com/go-openapi/swag/jsonname v0.25.4/go.mod h1:GPVEk9CWVhNvWhZgrnvRA6utbAltopbKwDu8mXNUMag=
|
|
||||||
github.com/go-openapi/testify/v2 v2.0.2 h1:X999g3jeLcoY8qctY/c/Z8iBHTbwLz7R2WXd6Ub6wls=
|
github.com/go-openapi/testify/v2 v2.0.2 h1:X999g3jeLcoY8qctY/c/Z8iBHTbwLz7R2WXd6Ub6wls=
|
||||||
github.com/go-openapi/testify/v2 v2.0.2/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54=
|
github.com/go-openapi/testify/v2 v2.0.2/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54=
|
||||||
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||||
|
|
@ -68,8 +54,6 @@ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJn
|
||||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||||
github.com/go-playground/validator/v10 v10.28.0 h1:Q7ibns33JjyW48gHkuFT91qX48KG0ktULL6FgHdG688=
|
github.com/go-playground/validator/v10 v10.28.0 h1:Q7ibns33JjyW48gHkuFT91qX48KG0ktULL6FgHdG688=
|
||||||
github.com/go-playground/validator/v10 v10.28.0/go.mod h1:GoI6I1SjPBh9p7ykNE/yj3fFYbyDOpwMn5KXd+m2hUU=
|
github.com/go-playground/validator/v10 v10.28.0/go.mod h1:GoI6I1SjPBh9p7ykNE/yj3fFYbyDOpwMn5KXd+m2hUU=
|
||||||
github.com/go-playground/validator/v10 v10.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy06ntQJp0BBvFG0w=
|
|
||||||
github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM=
|
|
||||||
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
|
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
|
||||||
github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM=
|
github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM=
|
||||||
github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
|
github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
|
||||||
|
|
@ -77,8 +61,6 @@ github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
|
||||||
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||||
github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
|
github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
|
||||||
github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
||||||
github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
|
|
||||||
github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
|
||||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
||||||
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
||||||
|
|
@ -106,8 +88,6 @@ github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7Ulw
|
||||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
||||||
github.com/jackc/pgx/v5 v5.7.6 h1:rWQc5FwZSPX58r1OQmkuaNicxdmExaEz5A2DO2hUuTk=
|
github.com/jackc/pgx/v5 v5.7.6 h1:rWQc5FwZSPX58r1OQmkuaNicxdmExaEz5A2DO2hUuTk=
|
||||||
github.com/jackc/pgx/v5 v5.7.6/go.mod h1:aruU7o91Tc2q2cFp5h4uP3f6ztExVpyVv88Xl/8Vl8M=
|
github.com/jackc/pgx/v5 v5.7.6/go.mod h1:aruU7o91Tc2q2cFp5h4uP3f6ztExVpyVv88Xl/8Vl8M=
|
||||||
github.com/jackc/pgx/v5 v5.8.0 h1:TYPDoleBBme0xGSAX3/+NujXXtpZn9HBONkQC7IEZSo=
|
|
||||||
github.com/jackc/pgx/v5 v5.8.0/go.mod h1:QVeDInX2m9VyzvNeiCJVjCkNFqzsNb43204HshNSZKw=
|
|
||||||
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
|
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
|
||||||
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
||||||
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||||
|
|
@ -118,7 +98,6 @@ github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8Hm
|
||||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||||
github.com/juju/gnuflag v0.0.0-20171113085948-2ce1bb71843d/go.mod h1:2PavIy+JPciBPrBUjwbNvtwB6RQlve+hkpll6QSNmOE=
|
|
||||||
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
|
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
|
||||||
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||||
|
|
@ -136,8 +115,6 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE
|
||||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
github.com/mattn/go-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs=
|
github.com/mattn/go-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs=
|
||||||
github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||||
github.com/mattn/go-sqlite3 v1.14.33 h1:A5blZ5ulQo2AtayQ9/limgHEkFreKj1Dv226a1K73s0=
|
|
||||||
github.com/mattn/go-sqlite3 v1.14.33/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
|
||||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
|
|
@ -178,12 +155,8 @@ github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
|
||||||
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
|
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
|
||||||
github.com/quic-go/quic-go v0.57.0 h1:AsSSrrMs4qI/hLrKlTH/TGQeTMY0ib1pAOX7vA3AdqE=
|
github.com/quic-go/quic-go v0.57.0 h1:AsSSrrMs4qI/hLrKlTH/TGQeTMY0ib1pAOX7vA3AdqE=
|
||||||
github.com/quic-go/quic-go v0.57.0/go.mod h1:ly4QBAjHA2VhdnxhojRsCUOeJwKYg+taDlos92xb1+s=
|
github.com/quic-go/quic-go v0.57.0/go.mod h1:ly4QBAjHA2VhdnxhojRsCUOeJwKYg+taDlos92xb1+s=
|
||||||
github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw=
|
|
||||||
github.com/quic-go/quic-go v0.59.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU=
|
|
||||||
github.com/redis/go-redis/v9 v9.16.0 h1:OotgqgLSRCmzfqChbQyG1PHC3tLNR89DG4jdOERSEP4=
|
github.com/redis/go-redis/v9 v9.16.0 h1:OotgqgLSRCmzfqChbQyG1PHC3tLNR89DG4jdOERSEP4=
|
||||||
github.com/redis/go-redis/v9 v9.16.0/go.mod h1:u410H11HMLoB+TP67dz8rL9s6QW2j76l0//kSOd3370=
|
github.com/redis/go-redis/v9 v9.16.0/go.mod h1:u410H11HMLoB+TP67dz8rL9s6QW2j76l0//kSOd3370=
|
||||||
github.com/redis/go-redis/v9 v9.17.2 h1:P2EGsA4qVIM3Pp+aPocCJ7DguDHhqrXNhVcEp4ViluI=
|
|
||||||
github.com/redis/go-redis/v9 v9.17.2/go.mod h1:u410H11HMLoB+TP67dz8rL9s6QW2j76l0//kSOd3370=
|
|
||||||
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
|
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
|
||||||
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
|
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
|
||||||
github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
|
github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
|
||||||
|
|
@ -192,7 +165,6 @@ github.com/speakeasy-api/jsonpath v0.6.0 h1:IhtFOV9EbXplhyRqsVhHoBmmYjblIRh5D1/g
|
||||||
github.com/speakeasy-api/jsonpath v0.6.0/go.mod h1:ymb2iSkyOycmzKwbEAYPJV/yi2rSmvBCLZJcyD+VVWw=
|
github.com/speakeasy-api/jsonpath v0.6.0/go.mod h1:ymb2iSkyOycmzKwbEAYPJV/yi2rSmvBCLZJcyD+VVWw=
|
||||||
github.com/speakeasy-api/openapi-overlay v0.10.2 h1:VOdQ03eGKeiHnpb1boZCGm7x8Haj6gST0P3SGTX95GU=
|
github.com/speakeasy-api/openapi-overlay v0.10.2 h1:VOdQ03eGKeiHnpb1boZCGm7x8Haj6gST0P3SGTX95GU=
|
||||||
github.com/speakeasy-api/openapi-overlay v0.10.2/go.mod h1:n0iOU7AqKpNFfEt6tq7qYITC4f0yzVVdFw0S7hukemg=
|
github.com/speakeasy-api/openapi-overlay v0.10.2/go.mod h1:n0iOU7AqKpNFfEt6tq7qYITC4f0yzVVdFw0S7hukemg=
|
||||||
github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKkMo8ZTx3f+BZEkzsRUY10Xsm2mwU0=
|
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||||
|
|
@ -267,7 +239,6 @@ golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||||
golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
|
golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
|
||||||
golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
|
golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
|
||||||
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
|
|
||||||
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
|
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
|
||||||
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
|
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
|
@ -289,8 +260,6 @@ google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp0
|
||||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||||
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
|
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
|
||||||
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
|
||||||
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
|
|
|
||||||
|
|
@ -41,7 +41,6 @@ func declareFlags(o *Config) {
|
||||||
flag.DurationVar(&o.ReportRetention, "report-retention", o.ReportRetention, "How long to keep reports (e.g., 720h, 30d). 0 = keep forever")
|
flag.DurationVar(&o.ReportRetention, "report-retention", o.ReportRetention, "How long to keep reports (e.g., 720h, 30d). 0 = keep forever")
|
||||||
flag.UintVar(&o.RateLimit, "rate-limit", o.RateLimit, "API rate limit (requests per second per IP)")
|
flag.UintVar(&o.RateLimit, "rate-limit", o.RateLimit, "API rate limit (requests per second per IP)")
|
||||||
flag.Var(&URL{&o.SurveyURL}, "survey-url", "URL for user feedback survey")
|
flag.Var(&URL{&o.SurveyURL}, "survey-url", "URL for user feedback survey")
|
||||||
flag.StringVar(&o.CustomLogoURL, "custom-logo-url", o.CustomLogoURL, "URL for custom logo image in the web UI")
|
|
||||||
|
|
||||||
// Others flags are declared in some other files likes sources, storages, ... when they need specials configurations
|
// Others flags are declared in some other files likes sources, storages, ... when they need specials configurations
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -44,7 +44,6 @@ type Config struct {
|
||||||
ReportRetention time.Duration // How long to keep reports. 0 = keep forever
|
ReportRetention time.Duration // How long to keep reports. 0 = keep forever
|
||||||
RateLimit uint // API rate limit (requests per second per IP)
|
RateLimit uint // API rate limit (requests per second per IP)
|
||||||
SurveyURL url.URL // URL for user feedback survey
|
SurveyURL url.URL // URL for user feedback survey
|
||||||
CustomLogoURL string // URL for custom logo image in the web UI
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// DatabaseConfig contains database connection settings
|
// DatabaseConfig contains database connection settings
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
|
"io/ioutil"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
|
@ -66,10 +67,6 @@ func DeclareRoutes(cfg *config.Config, router *gin.Engine) {
|
||||||
appConfig["rbls"] = cfg.Analysis.RBLs
|
appConfig["rbls"] = cfg.Analysis.RBLs
|
||||||
}
|
}
|
||||||
|
|
||||||
if cfg.CustomLogoURL != "" {
|
|
||||||
appConfig["custom_logo_url"] = cfg.CustomLogoURL
|
|
||||||
}
|
|
||||||
|
|
||||||
if appcfg, err := json.MarshalIndent(appConfig, "", " "); err != nil {
|
if appcfg, err := json.MarshalIndent(appConfig, "", " "); err != nil {
|
||||||
log.Println("Unable to generate JSON config to inject in web application")
|
log.Println("Unable to generate JSON config to inject in web application")
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -143,7 +140,7 @@ func serveOrReverse(forced_url string, cfg *config.Config) gin.HandlerFunc {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
v, _ := io.ReadAll(resp.Body)
|
v, _ := ioutil.ReadAll(resp.Body)
|
||||||
|
|
||||||
v2 := strings.Replace(strings.Replace(string(v), "</head>", `{{ .Head }}<meta property="og:url" content="{{ .RootURL }}"></head>`, 1), "</body>", "{{ .Body }}</body>", 1)
|
v2 := strings.Replace(strings.Replace(string(v), "</head>", `{{ .Head }}<meta property="og:url" content="{{ .RootURL }}"></head>`, 1), "</body>", "{{ .Body }}</body>", 1)
|
||||||
|
|
||||||
|
|
@ -170,7 +167,7 @@ func serveOrReverse(forced_url string, cfg *config.Config) gin.HandlerFunc {
|
||||||
if indexTpl == nil {
|
if indexTpl == nil {
|
||||||
// Create template from file
|
// Create template from file
|
||||||
f, _ := Assets.Open("index.html")
|
f, _ := Assets.Open("index.html")
|
||||||
v, _ := io.ReadAll(f)
|
v, _ := ioutil.ReadAll(f)
|
||||||
|
|
||||||
v2 := strings.Replace(strings.Replace(string(v), "</head>", `{{ .Head }}<meta property="og:url" content="{{ .RootURL }}"></head>`, 1), "</body>", "{{ .Body }}</body>", 1)
|
v2 := strings.Replace(strings.Replace(string(v), "</head>", `{{ .Head }}<meta property="og:url" content="{{ .RootURL }}"></head>`, 1), "</body>", "{{ .Body }}</body>", 1)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,6 @@
|
||||||
:root {
|
:root {
|
||||||
--bs-primary: #1cb487;
|
--bs-primary: #1cb487;
|
||||||
--bs-primary-rgb: 28, 180, 135;
|
--bs-primary-rgb: 28, 180, 135;
|
||||||
--bs-link-color-rgb: 28, 180, 135;
|
|
||||||
--bs-link-hover-color-rgb: 17, 112, 84;
|
|
||||||
--bs-tertiary-bg: #e7e8e8;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
|
|
@ -11,10 +8,6 @@ body {
|
||||||
-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
.bg-tertiary {
|
|
||||||
background-color: var(--bs-tertiary-bg);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Animations */
|
/* Animations */
|
||||||
@keyframes fadeIn {
|
@keyframes fadeIn {
|
||||||
from {
|
from {
|
||||||
|
|
|
||||||
|
|
@ -96,442 +96,281 @@
|
||||||
</h4>
|
</h4>
|
||||||
</div>
|
</div>
|
||||||
<div class="list-group list-group-flush">
|
<div class="list-group list-group-flush">
|
||||||
<!-- IPREV -->
|
<!-- IPREV -->
|
||||||
{#if authentication.iprev}
|
{#if authentication.iprev}
|
||||||
<div class="list-group-item" id="authentication-iprev">
|
<div class="list-group-item" id="authentication-iprev">
|
||||||
<div class="d-flex align-items-start">
|
<div class="d-flex align-items-start">
|
||||||
<i
|
<i class="bi {getAuthResultIcon(authentication.iprev.result, true)} {getAuthResultClass(authentication.iprev.result, true)} me-2 fs-5"></i>
|
||||||
class="bi {getAuthResultIcon(
|
|
||||||
authentication.iprev.result,
|
|
||||||
true,
|
|
||||||
)} {getAuthResultClass(authentication.iprev.result, true)} me-2 fs-5"
|
|
||||||
></i>
|
|
||||||
<div>
|
|
||||||
<strong>IP Reverse DNS</strong>
|
|
||||||
<span
|
|
||||||
class="text-uppercase ms-2 {getAuthResultClass(
|
|
||||||
authentication.iprev.result,
|
|
||||||
true,
|
|
||||||
)}"
|
|
||||||
>
|
|
||||||
{authentication.iprev.result}
|
|
||||||
</span>
|
|
||||||
{#if authentication.iprev.ip}
|
|
||||||
<div class="small">
|
|
||||||
<strong>IP Address:</strong>
|
|
||||||
<span class="text-muted">{authentication.iprev.ip}</span>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
{#if authentication.iprev.hostname}
|
|
||||||
<div class="small">
|
|
||||||
<strong>Hostname:</strong>
|
|
||||||
<span class="text-muted">{authentication.iprev.hostname}</span>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
{#if authentication.iprev.details}
|
|
||||||
<pre
|
|
||||||
class="p-2 mb-0 {$theme === 'light'
|
|
||||||
? 'bg-light'
|
|
||||||
: 'bg-secondary'} text-muted small"
|
|
||||||
style="white-space: pre-wrap">{authentication.iprev.details}</pre>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<!-- SPF (Required) -->
|
|
||||||
<div class="list-group-item">
|
|
||||||
<div class="d-flex align-items-start" id="authentication-spf">
|
|
||||||
{#if authentication.spf}
|
|
||||||
<i
|
|
||||||
class="bi {getAuthResultIcon(
|
|
||||||
authentication.spf.result,
|
|
||||||
true,
|
|
||||||
)} {getAuthResultClass(authentication.spf.result, true)} me-2 fs-5"
|
|
||||||
></i>
|
|
||||||
<div>
|
|
||||||
<strong>SPF</strong>
|
|
||||||
<span
|
|
||||||
class="text-uppercase ms-2 {getAuthResultClass(
|
|
||||||
authentication.spf.result,
|
|
||||||
true,
|
|
||||||
)}"
|
|
||||||
>
|
|
||||||
{authentication.spf.result}
|
|
||||||
</span>
|
|
||||||
{#if authentication.spf.domain}
|
|
||||||
<div class="small">
|
|
||||||
<strong>Domain:</strong>
|
|
||||||
<span class="text-muted">{authentication.spf.domain}</span>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
{#if authentication.spf.details}
|
|
||||||
<pre
|
|
||||||
class="p-2 mb-0 {$theme === 'light'
|
|
||||||
? 'bg-light'
|
|
||||||
: 'bg-secondary'} text-muted small"
|
|
||||||
style="white-space: pre-wrap">{authentication.spf.details}</pre>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
{:else}
|
|
||||||
<i
|
|
||||||
class="bi {getAuthResultIcon('missing', true)} {getAuthResultClass(
|
|
||||||
'missing',
|
|
||||||
true,
|
|
||||||
)} me-2 fs-5"
|
|
||||||
></i>
|
|
||||||
<div>
|
|
||||||
<strong>SPF</strong>
|
|
||||||
<span class="text-uppercase ms-2 {getAuthResultClass('missing', true)}">
|
|
||||||
{getAuthResultText("missing")}
|
|
||||||
</span>
|
|
||||||
<div class="text-muted small">
|
|
||||||
SPF record is required for proper email authentication
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- DKIM (Required) -->
|
|
||||||
<div class="list-group-item" id="authentication-dkim">
|
|
||||||
{#if authentication.dkim && authentication.dkim.length > 0}
|
|
||||||
{#each authentication.dkim as dkim, i}
|
|
||||||
<div class="d-flex align-items-start" class:mt-3={i > 0}>
|
|
||||||
<i
|
|
||||||
class="bi {getAuthResultIcon(dkim.result, true)} {getAuthResultClass(
|
|
||||||
dkim.result,
|
|
||||||
true,
|
|
||||||
)} me-2 fs-5"
|
|
||||||
></i>
|
|
||||||
<div>
|
<div>
|
||||||
<strong>DKIM{authentication.dkim.length > 1 ? ` #${i + 1}` : ""}</strong
|
<strong>IP Reverse DNS</strong>
|
||||||
>
|
<span class="text-uppercase ms-2 {getAuthResultClass(authentication.iprev.result, true)}">
|
||||||
<span
|
{authentication.iprev.result}
|
||||||
class="text-uppercase ms-2 {getAuthResultClass(dkim.result, true)}"
|
|
||||||
>
|
|
||||||
{dkim.result}
|
|
||||||
</span>
|
</span>
|
||||||
{#if dkim.domain}
|
{#if authentication.iprev.ip}
|
||||||
<div class="small">
|
<div class="small">
|
||||||
<strong>Domain:</strong>
|
<strong>IP Address:</strong>
|
||||||
<span class="text-muted">{dkim.domain}</span>
|
<span class="text-muted">{authentication.iprev.ip}</span>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
{#if dkim.selector}
|
{#if authentication.iprev.hostname}
|
||||||
<div class="small">
|
<div class="small">
|
||||||
<strong>Selector:</strong>
|
<strong>Hostname:</strong>
|
||||||
<span class="text-muted">{dkim.selector}</span>
|
<span class="text-muted">{authentication.iprev.hostname}</span>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
{#if dkim.details}
|
{#if authentication.iprev.details}
|
||||||
<pre
|
<pre class="p-2 mb-0 {$theme === 'light' ? 'bg-light' : 'bg-secondary'} text-muted small" style="white-space: pre-wrap">{authentication.iprev.details}</pre>
|
||||||
class="p-2 mb-0 {$theme === 'light'
|
|
||||||
? 'bg-light'
|
|
||||||
: 'bg-secondary'} text-muted small"
|
|
||||||
style="white-space: pre-wrap">{dkim.details}</pre>
|
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
|
||||||
{:else}
|
|
||||||
<div class="d-flex align-items-start">
|
|
||||||
<i
|
|
||||||
class="bi {getAuthResultIcon('missing', true)} {getAuthResultClass(
|
|
||||||
'missing',
|
|
||||||
true,
|
|
||||||
)} me-2 fs-5"
|
|
||||||
></i>
|
|
||||||
<div>
|
|
||||||
<strong>DKIM</strong>
|
|
||||||
<span class="text-uppercase ms-2 {getAuthResultClass('missing', true)}">
|
|
||||||
{getAuthResultText("missing")}
|
|
||||||
</span>
|
|
||||||
<div class="text-muted small">
|
|
||||||
DKIM signature is required for proper email authentication
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- X-Google-DKIM (Optional) -->
|
<!-- SPF (Required) -->
|
||||||
{#if authentication.x_google_dkim}
|
<div class="list-group-item">
|
||||||
<div class="list-group-item" id="authentication-x-google-dkim">
|
<div class="d-flex align-items-start" id="authentication-spf">
|
||||||
<div class="d-flex align-items-start">
|
{#if authentication.spf}
|
||||||
<i
|
<i class="bi {getAuthResultIcon(authentication.spf.result, true)} {getAuthResultClass(authentication.spf.result, true)} me-2 fs-5"></i>
|
||||||
class="bi {getAuthResultIcon(
|
<div>
|
||||||
authentication.x_google_dkim.result,
|
<strong>SPF</strong>
|
||||||
false,
|
<span class="text-uppercase ms-2 {getAuthResultClass(authentication.spf.result, true)}">
|
||||||
)} {getAuthResultClass(
|
{authentication.spf.result}
|
||||||
authentication.x_google_dkim.result,
|
</span>
|
||||||
false,
|
{#if authentication.spf.domain}
|
||||||
)} me-2 fs-5"
|
<div class="small">
|
||||||
></i>
|
<strong>Domain:</strong>
|
||||||
<div>
|
<span class="text-muted">{authentication.spf.domain}</span>
|
||||||
<strong>X-Google-DKIM</strong>
|
</div>
|
||||||
<i
|
{/if}
|
||||||
class="bi bi-info-circle text-muted ms-1"
|
{#if authentication.spf.details}
|
||||||
title="Google's internal DKIM signature for messages routed through Gmail infrastructure"
|
<pre class="p-2 mb-0 {$theme === 'light' ? 'bg-light' : 'bg-secondary'} text-muted small" style="white-space: pre-wrap">{authentication.spf.details}</pre>
|
||||||
></i>
|
|
||||||
<span
|
|
||||||
class="text-uppercase ms-2 {getAuthResultClass(
|
|
||||||
authentication.x_google_dkim.result,
|
|
||||||
false,
|
|
||||||
)}"
|
|
||||||
>
|
|
||||||
{authentication.x_google_dkim.result}
|
|
||||||
</span>
|
|
||||||
{#if authentication.x_google_dkim.domain}
|
|
||||||
<div class="small">
|
|
||||||
<strong>Domain:</strong>
|
|
||||||
<span class="text-muted">{authentication.x_google_dkim.domain}</span
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
{#if authentication.x_google_dkim.selector}
|
|
||||||
<div class="small">
|
|
||||||
<strong>Selector:</strong>
|
|
||||||
<span class="text-muted"
|
|
||||||
>{authentication.x_google_dkim.selector}</span
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
{#if authentication.x_google_dkim.details}
|
|
||||||
<pre
|
|
||||||
class="p-2 mb-0 {$theme === 'light'
|
|
||||||
? 'bg-light'
|
|
||||||
: 'bg-secondary'} text-muted small"
|
|
||||||
style="white-space: pre-wrap">{authentication.x_google_dkim
|
|
||||||
.details}</pre>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<!-- X-Aligned-From (Disabled) -->
|
|
||||||
{#if authentication.x_aligned_from}
|
|
||||||
<div class="list-group-item" id="authentication-x-aligned-from">
|
|
||||||
<div class="d-flex align-items-start">
|
|
||||||
<i
|
|
||||||
class="bi {getAuthResultIcon(
|
|
||||||
authentication.x_aligned_from.result,
|
|
||||||
false,
|
|
||||||
)} {getAuthResultClass(
|
|
||||||
authentication.x_aligned_from.result,
|
|
||||||
false,
|
|
||||||
)} me-2 fs-5"
|
|
||||||
></i>
|
|
||||||
<div>
|
|
||||||
<strong>X-Aligned-From</strong>
|
|
||||||
<i
|
|
||||||
class="bi bi-info-circle text-muted ms-1"
|
|
||||||
title="Check that Mail From and Header From addresses are in alignment. See Domain Alignment section."
|
|
||||||
></i>
|
|
||||||
<span
|
|
||||||
class="text-uppercase ms-2 {getAuthResultClass(
|
|
||||||
authentication.x_aligned_from.result,
|
|
||||||
false,
|
|
||||||
)}"
|
|
||||||
>
|
|
||||||
{authentication.x_aligned_from.result}
|
|
||||||
</span>
|
|
||||||
{#if authentication.x_aligned_from.domain}
|
|
||||||
<div class="small">
|
|
||||||
<strong>Domain:</strong>
|
|
||||||
<span class="text-muted"
|
|
||||||
>{authentication.x_aligned_from.domain}</span
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
{#if authentication.x_aligned_from.details}
|
|
||||||
<pre
|
|
||||||
class="p-2 mb-0 {$theme === 'light'
|
|
||||||
? 'bg-light'
|
|
||||||
: 'bg-secondary'} text-muted small"
|
|
||||||
style="white-space: pre-wrap">{authentication.x_aligned_from
|
|
||||||
.details}</pre>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<!-- DMARC (Required) -->
|
|
||||||
<div class="list-group-item" id="authentication-dmarc">
|
|
||||||
<div class="d-flex align-items-start">
|
|
||||||
{#if authentication.dmarc}
|
|
||||||
<i
|
|
||||||
class="bi {getAuthResultIcon(
|
|
||||||
authentication.dmarc.result,
|
|
||||||
true,
|
|
||||||
)} {getAuthResultClass(authentication.dmarc.result, true)} me-2 fs-5"
|
|
||||||
></i>
|
|
||||||
<div>
|
|
||||||
<strong>DMARC</strong>
|
|
||||||
<span
|
|
||||||
class="text-uppercase ms-2 {getAuthResultClass(
|
|
||||||
authentication.dmarc.result,
|
|
||||||
true,
|
|
||||||
)}"
|
|
||||||
>
|
|
||||||
{authentication.dmarc.result}
|
|
||||||
</span>
|
|
||||||
{#if authentication.dmarc.domain}
|
|
||||||
<div class="small">
|
|
||||||
<strong>Domain:</strong>
|
|
||||||
<span class="text-muted">{authentication.dmarc.domain}</span>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
{#snippet DMARCPolicy(policy: string)}
|
|
||||||
<div class="small">
|
|
||||||
<strong>Policy:</strong>
|
|
||||||
<span
|
|
||||||
class="fw-bold"
|
|
||||||
class:text-success={policy == "reject"}
|
|
||||||
class:text-warning={policy == "quarantine"}
|
|
||||||
class:text-danger={policy == "none"}
|
|
||||||
class:bg-warning={policy != "none" &&
|
|
||||||
policy != "quarantine" &&
|
|
||||||
policy != "reject"}
|
|
||||||
>
|
|
||||||
{policy}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
{/snippet}
|
|
||||||
{#if authentication.dmarc.result != "none"}
|
|
||||||
{#if authentication.dmarc.details && authentication.dmarc.details.indexOf("policy.published-domain-policy=") > 0}
|
|
||||||
{@const policy = authentication.dmarc.details.replace(
|
|
||||||
/^.*policy.published-domain-policy=([^\s]+).*$/,
|
|
||||||
"$1",
|
|
||||||
)}
|
|
||||||
{@render DMARCPolicy(policy)}
|
|
||||||
{:else if authentication.dmarc.domain && dnsResults?.dmarc_record?.policy}
|
|
||||||
{@render DMARCPolicy(dnsResults.dmarc_record.policy)}
|
|
||||||
{/if}
|
{/if}
|
||||||
{/if}
|
|
||||||
{#if authentication.dmarc.details}
|
|
||||||
<pre
|
|
||||||
class="p-2 mb-0 {$theme === 'light'
|
|
||||||
? 'bg-light'
|
|
||||||
: 'bg-secondary'} text-muted small"
|
|
||||||
style="white-space: pre-wrap">{authentication.dmarc.details}</pre>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
{:else}
|
|
||||||
<i
|
|
||||||
class="bi {getAuthResultIcon('missing', true)} {getAuthResultClass(
|
|
||||||
'missing',
|
|
||||||
true,
|
|
||||||
)} me-2 fs-5"
|
|
||||||
></i>
|
|
||||||
<div>
|
|
||||||
<strong>DMARC</strong>
|
|
||||||
<span class="text-uppercase ms-2 {getAuthResultClass('missing', true)}">
|
|
||||||
{getAuthResultText("missing")}
|
|
||||||
</span>
|
|
||||||
<div class="text-muted small">
|
|
||||||
DMARC policy is required for proper email authentication
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
{:else}
|
||||||
{/if}
|
<i class="bi {getAuthResultIcon('missing', true)} {getAuthResultClass('missing', true)} me-2 fs-5"></i>
|
||||||
</div>
|
<div>
|
||||||
</div>
|
<strong>SPF</strong>
|
||||||
|
<span class="text-uppercase ms-2 {getAuthResultClass('missing', true)}">
|
||||||
<!-- BIMI (Optional) -->
|
{getAuthResultText('missing')}
|
||||||
<div class="list-group-item" id="authentication-bimi">
|
</span>
|
||||||
<div class="d-flex align-items-start">
|
<div class="text-muted small">SPF record is required for proper email authentication</div>
|
||||||
{#if authentication.bimi && authentication.bimi.result != "none"}
|
|
||||||
<i
|
|
||||||
class="bi {getAuthResultIcon(
|
|
||||||
authentication.bimi.result,
|
|
||||||
false,
|
|
||||||
)} {getAuthResultClass(authentication.bimi.result, false)} me-2 fs-5"
|
|
||||||
></i>
|
|
||||||
<div>
|
|
||||||
<strong>BIMI</strong>
|
|
||||||
<span
|
|
||||||
class="text-uppercase ms-2 {getAuthResultClass(
|
|
||||||
authentication.bimi.result,
|
|
||||||
false,
|
|
||||||
)}"
|
|
||||||
>
|
|
||||||
{authentication.bimi.result}
|
|
||||||
</span>
|
|
||||||
{#if authentication.bimi.details}
|
|
||||||
<pre
|
|
||||||
class="p-2 mb-0 {$theme === 'light'
|
|
||||||
? 'bg-light'
|
|
||||||
: 'bg-secondary'} text-muted small"
|
|
||||||
style="white-space: pre-wrap">{authentication.bimi.details}</pre>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
{:else if authentication.bimi && authentication.bimi.result == "none"}
|
|
||||||
<i class="bi bi-exclamation-circle-fill text-warning me-2 fs-5"></i>
|
|
||||||
<div>
|
|
||||||
<strong>BIMI</strong>
|
|
||||||
<span class="text-uppercase ms-2 text-warning"> NONE </span>
|
|
||||||
<div class="text-muted small">
|
|
||||||
Brand Indicators for Message Identification
|
|
||||||
</div>
|
</div>
|
||||||
{#if authentication.bimi.details}
|
{/if}
|
||||||
<pre
|
|
||||||
class="p-2 mb-0 {$theme === 'light'
|
|
||||||
? 'bg-light'
|
|
||||||
: 'bg-secondary'} text-muted small"
|
|
||||||
style="white-space: pre-wrap">{authentication.bimi.details}</pre>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
{:else}
|
|
||||||
<i class="bi bi-info-circle text-muted me-2 fs-5"></i>
|
|
||||||
<div>
|
|
||||||
<strong>BIMI</strong>
|
|
||||||
<span class="text-uppercase ms-2 text-muted"> Optional </span>
|
|
||||||
<div class="text-muted small">
|
|
||||||
Brand Indicators for Message Identification (optional enhancement)
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- ARC (Optional) -->
|
|
||||||
{#if authentication.arc}
|
|
||||||
<div class="list-group-item" id="authentication-arc">
|
|
||||||
<div class="d-flex align-items-start">
|
|
||||||
<i
|
|
||||||
class="bi {getAuthResultIcon(
|
|
||||||
authentication.arc.result,
|
|
||||||
false,
|
|
||||||
)} {getAuthResultClass(authentication.arc.result, false)} me-2 fs-5"
|
|
||||||
></i>
|
|
||||||
<div>
|
|
||||||
<strong>ARC</strong>
|
|
||||||
<span
|
|
||||||
class="text-uppercase ms-2 {getAuthResultClass(
|
|
||||||
authentication.arc.result,
|
|
||||||
false,
|
|
||||||
)}"
|
|
||||||
>
|
|
||||||
{authentication.arc.result}
|
|
||||||
</span>
|
|
||||||
{#if authentication.arc.chain_length}
|
|
||||||
<div class="text-muted small">
|
|
||||||
Chain length: {authentication.arc.chain_length}
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
{#if authentication.arc.details}
|
|
||||||
<pre
|
|
||||||
class="p-2 mb-0 {$theme === 'light'
|
|
||||||
? 'bg-light'
|
|
||||||
: 'bg-secondary'} text-muted small"
|
|
||||||
style="white-space: pre-wrap">{authentication.arc.details}</pre>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
|
||||||
|
<!-- DKIM (Required) -->
|
||||||
|
<div class="list-group-item" id="authentication-dkim">
|
||||||
|
{#if authentication.dkim && authentication.dkim.length > 0}
|
||||||
|
{#each authentication.dkim as dkim, i}
|
||||||
|
<div class="d-flex align-items-start" class:mt-3={i > 0}>
|
||||||
|
<i class="bi {getAuthResultIcon(dkim.result, true)} {getAuthResultClass(dkim.result, true)} me-2 fs-5"></i>
|
||||||
|
<div>
|
||||||
|
<strong>DKIM{authentication.dkim.length > 1 ? ` #${i + 1}` : ''}</strong>
|
||||||
|
<span class="text-uppercase ms-2 {getAuthResultClass(dkim.result, true)}">
|
||||||
|
{dkim.result}
|
||||||
|
</span>
|
||||||
|
{#if dkim.domain}
|
||||||
|
<div class="small">
|
||||||
|
<strong>Domain:</strong>
|
||||||
|
<span class="text-muted">{dkim.domain}</span>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{#if dkim.selector}
|
||||||
|
<div class="small">
|
||||||
|
<strong>Selector:</strong>
|
||||||
|
<span class="text-muted">{dkim.selector}</span>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{#if dkim.details}
|
||||||
|
<pre class="p-2 mb-0 {$theme === 'light' ? 'bg-light' : 'bg-secondary'} text-muted small" style="white-space: pre-wrap">{dkim.details}</pre>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
{:else}
|
||||||
|
<div class="d-flex align-items-start">
|
||||||
|
<i class="bi {getAuthResultIcon('missing', true)} {getAuthResultClass('missing', true)} me-2 fs-5"></i>
|
||||||
|
<div>
|
||||||
|
<strong>DKIM</strong>
|
||||||
|
<span class="text-uppercase ms-2 {getAuthResultClass('missing', true)}">
|
||||||
|
{getAuthResultText('missing')}
|
||||||
|
</span>
|
||||||
|
<div class="text-muted small">DKIM signature is required for proper email authentication</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- X-Google-DKIM (Optional) -->
|
||||||
|
{#if authentication.x_google_dkim}
|
||||||
|
<div class="list-group-item" id="authentication-x-google-dkim">
|
||||||
|
<div class="d-flex align-items-start">
|
||||||
|
<i class="bi {getAuthResultIcon(authentication.x_google_dkim.result, false)} {getAuthResultClass(authentication.x_google_dkim.result, false)} me-2 fs-5"></i>
|
||||||
|
<div>
|
||||||
|
<strong>X-Google-DKIM</strong>
|
||||||
|
<i class="bi bi-info-circle text-muted ms-1" title="Google's internal DKIM signature for messages routed through Gmail infrastructure"></i>
|
||||||
|
<span class="text-uppercase ms-2 {getAuthResultClass(authentication.x_google_dkim.result, false)}">
|
||||||
|
{authentication.x_google_dkim.result}
|
||||||
|
</span>
|
||||||
|
{#if authentication.x_google_dkim.domain}
|
||||||
|
<div class="small">
|
||||||
|
<strong>Domain:</strong>
|
||||||
|
<span class="text-muted">{authentication.x_google_dkim.domain}</span>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{#if authentication.x_google_dkim.selector}
|
||||||
|
<div class="small">
|
||||||
|
<strong>Selector:</strong>
|
||||||
|
<span class="text-muted">{authentication.x_google_dkim.selector}</span>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{#if authentication.x_google_dkim.details}
|
||||||
|
<pre class="p-2 mb-0 {$theme === 'light' ? 'bg-light' : 'bg-secondary'} text-muted small" style="white-space: pre-wrap">{authentication.x_google_dkim.details}</pre>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<!-- X-Aligned-From (Disabled) -->
|
||||||
|
{#if authentication.x_aligned_from}
|
||||||
|
<div class="list-group-item" id="authentication-x-aligned-from">
|
||||||
|
<div class="d-flex align-items-start">
|
||||||
|
<i class="bi {getAuthResultIcon(authentication.x_aligned_from.result, false)} {getAuthResultClass(authentication.x_aligned_from.result, false)} me-2 fs-5"></i>
|
||||||
|
<div>
|
||||||
|
<strong>X-Aligned-From</strong>
|
||||||
|
<i class="bi bi-info-circle text-muted ms-1" title="Check that Mail From and Header From addresses are in alignment. See Domain Alignment section."></i>
|
||||||
|
<span class="text-uppercase ms-2 {getAuthResultClass(authentication.x_aligned_from.result, false)}">
|
||||||
|
{authentication.x_aligned_from.result}
|
||||||
|
</span>
|
||||||
|
{#if authentication.x_aligned_from.domain}
|
||||||
|
<div class="small">
|
||||||
|
<strong>Domain:</strong>
|
||||||
|
<span class="text-muted">{authentication.x_aligned_from.domain}</span>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{#if authentication.x_aligned_from.details}
|
||||||
|
<pre class="p-2 mb-0 {$theme === 'light' ? 'bg-light' : 'bg-secondary'} text-muted small" style="white-space: pre-wrap">{authentication.x_aligned_from.details}</pre>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<!-- DMARC (Required) -->
|
||||||
|
<div class="list-group-item" id="authentication-dmarc">
|
||||||
|
<div class="d-flex align-items-start">
|
||||||
|
{#if authentication.dmarc}
|
||||||
|
<i class="bi {getAuthResultIcon(authentication.dmarc.result, true)} {getAuthResultClass(authentication.dmarc.result, true)} me-2 fs-5"></i>
|
||||||
|
<div>
|
||||||
|
<strong>DMARC</strong>
|
||||||
|
<span class="text-uppercase ms-2 {getAuthResultClass(authentication.dmarc.result, true)}">
|
||||||
|
{authentication.dmarc.result}
|
||||||
|
</span>
|
||||||
|
{#if authentication.dmarc.domain}
|
||||||
|
<div class="small">
|
||||||
|
<strong>Domain:</strong>
|
||||||
|
<span class="text-muted">{authentication.dmarc.domain}</span>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{#snippet DMARCPolicy(policy: string)}
|
||||||
|
<div class="small">
|
||||||
|
<strong>Policy:</strong>
|
||||||
|
<span
|
||||||
|
class="fw-bold"
|
||||||
|
class:text-success={policy == "reject"}
|
||||||
|
class:text-warning={policy == "quarantine"}
|
||||||
|
class:text-danger={policy == "none"}
|
||||||
|
class:bg-warning={policy != "none" && policy != "quarantine" && policy != "reject"}
|
||||||
|
>
|
||||||
|
{policy}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{/snippet}
|
||||||
|
{#if authentication.dmarc.result != "none"}
|
||||||
|
{#if authentication.dmarc.details && authentication.dmarc.details.indexOf("policy.published-domain-policy=") > 0}
|
||||||
|
{@const policy = authentication.dmarc.details.replace(/^.*policy.published-domain-policy=([^\s]+).*$/, "$1")}
|
||||||
|
{@render DMARCPolicy(policy)}
|
||||||
|
{:else if authentication.dmarc.domain && dnsResults?.dmarc_record?.policy}
|
||||||
|
{@render DMARCPolicy(dnsResults.dmarc_record.policy)}
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
|
{#if authentication.dmarc.details}
|
||||||
|
<pre class="p-2 mb-0 {$theme === 'light' ? 'bg-light' : 'bg-secondary'} text-muted small" style="white-space: pre-wrap">{authentication.dmarc.details}</pre>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<i class="bi {getAuthResultIcon('missing', true)} {getAuthResultClass('missing', true)} me-2 fs-5"></i>
|
||||||
|
<div>
|
||||||
|
<strong>DMARC</strong>
|
||||||
|
<span class="text-uppercase ms-2 {getAuthResultClass('missing', true)}">
|
||||||
|
{getAuthResultText('missing')}
|
||||||
|
</span>
|
||||||
|
<div class="text-muted small">DMARC policy is required for proper email authentication</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- BIMI (Optional) -->
|
||||||
|
<div class="list-group-item" id="authentication-bimi">
|
||||||
|
<div class="d-flex align-items-start">
|
||||||
|
{#if authentication.bimi && authentication.bimi.result != "none"}
|
||||||
|
<i class="bi {getAuthResultIcon(authentication.bimi.result, false)} {getAuthResultClass(authentication.bimi.result, false)} me-2 fs-5"></i>
|
||||||
|
<div>
|
||||||
|
<strong>BIMI</strong>
|
||||||
|
<span class="text-uppercase ms-2 {getAuthResultClass(authentication.bimi.result, false)}">
|
||||||
|
{authentication.bimi.result}
|
||||||
|
</span>
|
||||||
|
{#if authentication.bimi.details}
|
||||||
|
<pre class="p-2 mb-0 {$theme === 'light' ? 'bg-light' : 'bg-secondary'} text-muted small" style="white-space: pre-wrap">{authentication.bimi.details}</pre>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{:else if authentication.bimi && authentication.bimi.result == "none"}
|
||||||
|
<i class="bi bi-exclamation-circle-fill text-warning me-2 fs-5"></i>
|
||||||
|
<div>
|
||||||
|
<strong>BIMI</strong>
|
||||||
|
<span class="text-uppercase ms-2 text-warning">
|
||||||
|
NONE
|
||||||
|
</span>
|
||||||
|
<div class="text-muted small">Brand Indicators for Message Identification</div>
|
||||||
|
{#if authentication.bimi.details}
|
||||||
|
<pre class="p-2 mb-0 {$theme === 'light' ? 'bg-light' : 'bg-secondary'} text-muted small" style="white-space: pre-wrap">{authentication.bimi.details}</pre>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<i class="bi bi-info-circle text-muted me-2 fs-5"></i>
|
||||||
|
<div>
|
||||||
|
<strong>BIMI</strong>
|
||||||
|
<span class="text-uppercase ms-2 text-muted">
|
||||||
|
Optional
|
||||||
|
</span>
|
||||||
|
<div class="text-muted small">Brand Indicators for Message Identification (optional enhancement)</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ARC (Optional) -->
|
||||||
|
{#if authentication.arc}
|
||||||
|
<div class="list-group-item" id="authentication-arc">
|
||||||
|
<div class="d-flex align-items-start">
|
||||||
|
<i class="bi {getAuthResultIcon(authentication.arc.result, false)} {getAuthResultClass(authentication.arc.result, false)} me-2 fs-5"></i>
|
||||||
|
<div>
|
||||||
|
<strong>ARC</strong>
|
||||||
|
<span class="text-uppercase ms-2 {getAuthResultClass(authentication.arc.result, false)}">
|
||||||
|
{authentication.arc.result}
|
||||||
|
</span>
|
||||||
|
{#if authentication.arc.chain_length}
|
||||||
|
<div class="text-muted small">Chain length: {authentication.arc.chain_length}</div>
|
||||||
|
{/if}
|
||||||
|
{#if authentication.arc.details}
|
||||||
|
<pre class="p-2 mb-0 {$theme === 'light' ? 'bg-light' : 'bg-secondary'} text-muted small" style="white-space: pre-wrap">{authentication.arc.details}</pre>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,8 @@
|
||||||
import type { BlacklistCheck, ReceivedHop } from "$lib/api/types.gen";
|
import type { BlacklistCheck, ReceivedHop } from "$lib/api/types.gen";
|
||||||
import { getScoreColorClass } from "$lib/score";
|
import { getScoreColorClass } from "$lib/score";
|
||||||
import { theme } from "$lib/stores/theme";
|
import { theme } from "$lib/stores/theme";
|
||||||
import EmailPathCard from "./EmailPathCard.svelte";
|
|
||||||
import GradeDisplay from "./GradeDisplay.svelte";
|
import GradeDisplay from "./GradeDisplay.svelte";
|
||||||
|
import EmailPathCard from "./EmailPathCard.svelte";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
blacklists: Record<string, BlacklistCheck[]>;
|
blacklists: Record<string, BlacklistCheck[]>;
|
||||||
|
|
@ -16,7 +16,11 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="card shadow-sm" id="rbl-details">
|
<div class="card shadow-sm" id="rbl-details">
|
||||||
<div class="card-header" class:bg-white={$theme === "light"} class:bg-dark={$theme !== "light"}>
|
<div
|
||||||
|
class="card-header"
|
||||||
|
class:bg-white={$theme === 'light'}
|
||||||
|
class:bg-dark={$theme !== 'light'}
|
||||||
|
>
|
||||||
<h4 class="mb-0 d-flex justify-content-between align-items-center">
|
<h4 class="mb-0 d-flex justify-content-between align-items-center">
|
||||||
<span>
|
<span>
|
||||||
<i class="bi bi-shield-exclamation me-2"></i>
|
<i class="bi bi-shield-exclamation me-2"></i>
|
||||||
|
|
@ -50,19 +54,9 @@
|
||||||
<tbody>
|
<tbody>
|
||||||
{#each checks as check}
|
{#each checks as check}
|
||||||
<tr>
|
<tr>
|
||||||
<td title={check.response || "-"}>
|
<td title={check.response || '-'}>
|
||||||
<span
|
<span class="badge {check.listed ? 'bg-danger' : check.error ? 'bg-dark' : 'bg-success'}">
|
||||||
class="badge {check.listed
|
{check.error ? 'Error' : (check.listed ? 'Listed' : 'Clean')}
|
||||||
? 'bg-danger'
|
|
||||||
: check.error
|
|
||||||
? 'bg-dark'
|
|
||||||
: 'bg-success'}"
|
|
||||||
>
|
|
||||||
{check.error
|
|
||||||
? "Error"
|
|
||||||
: check.listed
|
|
||||||
? "Listed"
|
|
||||||
: "Clean"}
|
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td><code>{check.rbl}</code></td>
|
<td><code>{check.rbl}</code></td>
|
||||||
|
|
|
||||||
|
|
@ -36,28 +36,16 @@
|
||||||
<div class="row mb-3">
|
<div class="row mb-3">
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<div class="d-flex align-items-center mb-2">
|
<div class="d-flex align-items-center mb-2">
|
||||||
<i
|
<i class="bi {contentAnalysis.has_html ? 'bi-check-circle text-success' : 'bi-x-circle text-muted'} me-2"></i>
|
||||||
class="bi {contentAnalysis.has_html
|
|
||||||
? 'bi-check-circle text-success'
|
|
||||||
: 'bi-x-circle text-muted'} me-2"
|
|
||||||
></i>
|
|
||||||
<span>HTML Part</span>
|
<span>HTML Part</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="d-flex align-items-center mb-2">
|
<div class="d-flex align-items-center mb-2">
|
||||||
<i
|
<i class="bi {contentAnalysis.has_plaintext ? 'bi-check-circle text-success' : 'bi-x-circle text-muted'} me-2"></i>
|
||||||
class="bi {contentAnalysis.has_plaintext
|
|
||||||
? 'bi-check-circle text-success'
|
|
||||||
: 'bi-x-circle text-muted'} me-2"
|
|
||||||
></i>
|
|
||||||
<span>Plaintext Part</span>
|
<span>Plaintext Part</span>
|
||||||
</div>
|
</div>
|
||||||
{#if typeof contentAnalysis.has_unsubscribe_link === "boolean"}
|
{#if typeof contentAnalysis.has_unsubscribe_link === 'boolean'}
|
||||||
<div class="d-flex align-items-center mb-2">
|
<div class="d-flex align-items-center mb-2">
|
||||||
<i
|
<i class="bi {contentAnalysis.has_unsubscribe_link ? 'bi-check-circle text-success' : 'bi-x-circle text-warning'} me-2"></i>
|
||||||
class="bi {contentAnalysis.has_unsubscribe_link
|
|
||||||
? 'bi-check-circle text-success'
|
|
||||||
: 'bi-x-circle text-warning'} me-2"
|
|
||||||
></i>
|
|
||||||
<span>Unsubscribe Link</span>
|
<span>Unsubscribe Link</span>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
@ -86,14 +74,7 @@
|
||||||
<div class="mt-3">
|
<div class="mt-3">
|
||||||
<h5>Content Issues</h5>
|
<h5>Content Issues</h5>
|
||||||
{#each contentAnalysis.html_issues as issue}
|
{#each contentAnalysis.html_issues as issue}
|
||||||
<div
|
<div class="alert alert-{issue.severity === 'critical' || issue.severity === 'high' ? 'danger' : issue.severity === 'medium' ? 'warning' : 'info'} py-2 px-3 mb-2">
|
||||||
class="alert alert-{issue.severity === 'critical' ||
|
|
||||||
issue.severity === 'high'
|
|
||||||
? 'danger'
|
|
||||||
: issue.severity === 'medium'
|
|
||||||
? 'warning'
|
|
||||||
: 'info'} py-2 px-3 mb-2"
|
|
||||||
>
|
|
||||||
<div class="d-flex justify-content-between align-items-start">
|
<div class="d-flex justify-content-between align-items-start">
|
||||||
<div>
|
<div>
|
||||||
<strong>{issue.type}</strong>
|
<strong>{issue.type}</strong>
|
||||||
|
|
@ -137,17 +118,11 @@
|
||||||
{/if}
|
{/if}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<span
|
<span class="badge {link.status === 'valid' ? 'bg-success' : link.status === 'broken' ? 'bg-danger' : 'bg-warning'}">
|
||||||
class="badge {link.status === 'valid'
|
|
||||||
? 'bg-success'
|
|
||||||
: link.status === 'broken'
|
|
||||||
? 'bg-danger'
|
|
||||||
: 'bg-warning'}"
|
|
||||||
>
|
|
||||||
{link.status}
|
{link.status}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td>{link.http_code || "-"}</td>
|
<td>{link.http_code || '-'}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{/each}
|
{/each}
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|
@ -171,11 +146,11 @@
|
||||||
<tbody>
|
<tbody>
|
||||||
{#each contentAnalysis.images as image}
|
{#each contentAnalysis.images as image}
|
||||||
<tr>
|
<tr>
|
||||||
<td><small class="text-break">{image.src || "-"}</small></td>
|
<td><small class="text-break">{image.src || '-'}</small></td>
|
||||||
<td>
|
<td>
|
||||||
{#if image.has_alt}
|
{#if image.has_alt}
|
||||||
<i class="bi bi-check-circle text-success me-1"></i>
|
<i class="bi bi-check-circle text-success me-1"></i>
|
||||||
<small>{image.alt_text || "Present"}</small>
|
<small>{image.alt_text || 'Present'}</small>
|
||||||
{:else}
|
{:else}
|
||||||
<i class="bi bi-x-circle text-warning me-1"></i>
|
<i class="bi bi-x-circle text-warning me-1"></i>
|
||||||
<small class="text-muted">Missing</small>
|
<small class="text-muted">Missing</small>
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,15 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { DnsResults, DomainAlignment, ReceivedHop } from "$lib/api/types.gen";
|
import type { DomainAlignment, DnsResults, ReceivedHop } from "$lib/api/types.gen";
|
||||||
import { getScoreColorClass } from "$lib/score";
|
import { getScoreColorClass } from "$lib/score";
|
||||||
import { theme } from "$lib/stores/theme";
|
import { theme } from "$lib/stores/theme";
|
||||||
import BimiRecordDisplay from "./BimiRecordDisplay.svelte";
|
|
||||||
import DkimRecordsDisplay from "./DkimRecordsDisplay.svelte";
|
|
||||||
import DmarcRecordDisplay from "./DmarcRecordDisplay.svelte";
|
|
||||||
import GradeDisplay from "./GradeDisplay.svelte";
|
import GradeDisplay from "./GradeDisplay.svelte";
|
||||||
import MxRecordsDisplay from "./MxRecordsDisplay.svelte";
|
import MxRecordsDisplay from "./MxRecordsDisplay.svelte";
|
||||||
import PtrForwardRecordsDisplay from "./PtrForwardRecordsDisplay.svelte";
|
|
||||||
import PtrRecordsDisplay from "./PtrRecordsDisplay.svelte";
|
|
||||||
import SpfRecordsDisplay from "./SpfRecordsDisplay.svelte";
|
import SpfRecordsDisplay from "./SpfRecordsDisplay.svelte";
|
||||||
|
import DkimRecordsDisplay from "./DkimRecordsDisplay.svelte";
|
||||||
|
import DmarcRecordDisplay from "./DmarcRecordDisplay.svelte";
|
||||||
|
import BimiRecordDisplay from "./BimiRecordDisplay.svelte";
|
||||||
|
import PtrRecordsDisplay from "./PtrRecordsDisplay.svelte";
|
||||||
|
import PtrForwardRecordsDisplay from "./PtrForwardRecordsDisplay.svelte";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
domainAlignment?: DomainAlignment;
|
domainAlignment?: DomainAlignment;
|
||||||
|
|
@ -20,14 +20,7 @@
|
||||||
domainOnly?: boolean; // If true, only shows domain-level DNS records (no PTR, no DKIM, simplified view)
|
domainOnly?: boolean; // If true, only shows domain-level DNS records (no PTR, no DKIM, simplified view)
|
||||||
}
|
}
|
||||||
|
|
||||||
let {
|
let { domainAlignment, dnsResults, dnsGrade, dnsScore, receivedChain, domainOnly = false }: Props = $props();
|
||||||
domainAlignment,
|
|
||||||
dnsResults,
|
|
||||||
dnsGrade,
|
|
||||||
dnsScore,
|
|
||||||
receivedChain,
|
|
||||||
domainOnly = false,
|
|
||||||
}: Props = $props();
|
|
||||||
|
|
||||||
// Extract sender IP from first hop
|
// Extract sender IP from first hop
|
||||||
const senderIp = $derived(
|
const senderIp = $derived(
|
||||||
|
|
@ -74,10 +67,7 @@
|
||||||
{#if receivedChain && receivedChain.length > 0}
|
{#if receivedChain && receivedChain.length > 0}
|
||||||
<div class="mb-3 d-flex align-items-center gap-2">
|
<div class="mb-3 d-flex align-items-center gap-2">
|
||||||
<h4 class="mb-0 text-truncate">
|
<h4 class="mb-0 text-truncate">
|
||||||
Received from: <code
|
Received from: <code>{receivedChain[0].from} ({receivedChain[0].reverse || "Unknown"} [{receivedChain[0].ip}])</code>
|
||||||
>{receivedChain[0].from} ({receivedChain[0].reverse || "Unknown"} [{receivedChain[0]
|
|
||||||
.ip}])</code
|
|
||||||
>
|
|
||||||
</h4>
|
</h4>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
@ -98,13 +88,10 @@
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<div class="d-flex align-items-center gap-2 flex-wrap">
|
<div class="d-flex align-items-center gap-2 flex-wrap">
|
||||||
<h4 class="mb-0 text-truncate">
|
<h4 class="mb-0 text-truncate">
|
||||||
Return-Path Domain:
|
Return-Path Domain: <code>{dnsResults.rp_domain || dnsResults.from_domain}</code>
|
||||||
<code>{dnsResults.rp_domain || dnsResults.from_domain}</code>
|
|
||||||
</h4>
|
</h4>
|
||||||
{#if (domainAlignment && !domainAlignment.aligned && !domainAlignment.relaxed_aligned) || (domainAlignment && !domainAlignment.aligned && domainAlignment.relaxed_aligned && dnsResults.dmarc_record && dnsResults.dmarc_record.spf_alignment === "strict") || (!domainAlignment && dnsResults.rp_domain && dnsResults.rp_domain !== dnsResults.from_domain)}
|
{#if (domainAlignment && !domainAlignment.aligned && !domainAlignment.relaxed_aligned) || (domainAlignment && !domainAlignment.aligned && domainAlignment.relaxed_aligned && dnsResults.dmarc_record && dnsResults.dmarc_record.spf_alignment === "strict") || (!domainAlignment && dnsResults.rp_domain && dnsResults.rp_domain !== dnsResults.from_domain)}
|
||||||
<span class="badge bg-danger ms-2">
|
<span class="badge bg-danger ms-2"><i class="bi bi-exclamation-triangle-fill"></i> Differs from From domain</span>
|
||||||
<i class="bi bi-exclamation-triangle-fill"></i> Differs from From domain
|
|
||||||
</span>
|
|
||||||
<small>
|
<small>
|
||||||
<i class="bi bi-chevron-right"></i>
|
<i class="bi bi-chevron-right"></i>
|
||||||
<a href="#domain-alignment">See domain alignment</a>
|
<a href="#domain-alignment">See domain alignment</a>
|
||||||
|
|
@ -127,13 +114,10 @@
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<!-- SPF Records (for Return-Path Domain) -->
|
<!-- SPF Records (for Return-Path Domain) -->
|
||||||
<SpfRecordsDisplay
|
<SpfRecordsDisplay spfRecords={dnsResults.spf_records} dmarcRecord={dnsResults.dmarc_record} />
|
||||||
spfRecords={dnsResults.spf_records}
|
|
||||||
dmarcRecord={dnsResults.dmarc_record}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{#if !domainOnly}
|
{#if !domainOnly}
|
||||||
<hr class="my-4" />
|
<hr class="my-4">
|
||||||
|
|
||||||
<!-- From Domain Section -->
|
<!-- From Domain Section -->
|
||||||
<div class="mb-3 d-flex align-items-center gap-2">
|
<div class="mb-3 d-flex align-items-center gap-2">
|
||||||
|
|
@ -141,34 +125,31 @@
|
||||||
From Domain: <code>{dnsResults.from_domain}</code>
|
From Domain: <code>{dnsResults.from_domain}</code>
|
||||||
</h4>
|
</h4>
|
||||||
{#if dnsResults.rp_domain && dnsResults.rp_domain !== dnsResults.from_domain}
|
{#if dnsResults.rp_domain && dnsResults.rp_domain !== dnsResults.from_domain}
|
||||||
<span class="badge bg-danger ms-2">
|
<span class="badge bg-danger ms-2"><i class="bi bi-exclamation-triangle-fill"></i> Differs from Return-Path domain</span>
|
||||||
<i class="bi bi-exclamation-triangle-fill"></i> Differs from Return-Path
|
|
||||||
domain
|
|
||||||
</span>
|
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<!-- MX Records for From Domain -->
|
<!-- MX Records for From Domain -->
|
||||||
{#if dnsResults.from_mx_records && dnsResults.from_mx_records.length > 0}
|
{#if dnsResults.from_mx_records && dnsResults.from_mx_records.length > 0}
|
||||||
<MxRecordsDisplay
|
<MxRecordsDisplay
|
||||||
class="mb-4"
|
class="mb-4"
|
||||||
mxRecords={dnsResults.from_mx_records}
|
mxRecords={dnsResults.from_mx_records}
|
||||||
title="Mail Exchange Records for From Domain"
|
title="Mail Exchange Records for From Domain"
|
||||||
description="These MX records handle replies to emails sent from this domain."
|
description="These MX records handle replies to emails sent from this domain."
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if !domainOnly}
|
{#if !domainOnly}
|
||||||
<!-- DKIM Records -->
|
<!-- DKIM Records -->
|
||||||
<DkimRecordsDisplay dkimRecords={dnsResults.dkim_records} />
|
<DkimRecordsDisplay dkimRecords={dnsResults.dkim_records} />
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<!-- DMARC Record -->
|
<!-- DMARC Record -->
|
||||||
<DmarcRecordDisplay dmarcRecord={dnsResults.dmarc_record} />
|
<DmarcRecordDisplay dmarcRecord={dnsResults.dmarc_record} />
|
||||||
|
|
||||||
<!-- BIMI Record -->
|
<!-- BIMI Record -->
|
||||||
<BimiRecordDisplay bimiRecord={dnsResults.bimi_record} />
|
<BimiRecordDisplay bimiRecord={dnsResults.bimi_record} />
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -17,25 +17,15 @@
|
||||||
<div class="d-flex w-100 justify-content-between">
|
<div class="d-flex w-100 justify-content-between">
|
||||||
<h6 class="mb-1">
|
<h6 class="mb-1">
|
||||||
<span class="badge bg-primary me-2">{receivedChain.length - i}</span>
|
<span class="badge bg-primary me-2">{receivedChain.length - i}</span>
|
||||||
{hop.reverse || "-"}
|
{hop.reverse || '-'} {#if hop.ip}<span class="text-muted">({hop.ip})</span>{/if} → {hop.by || 'Unknown'}
|
||||||
{#if hop.ip}<span class="text-muted">({hop.ip})</span>{/if} → {hop.by ||
|
|
||||||
"Unknown"}
|
|
||||||
</h6>
|
</h6>
|
||||||
<small class="text-muted" title={hop.timestamp}>
|
<small class="text-muted" title={hop.timestamp}>{hop.timestamp ? new Intl.DateTimeFormat('default', { dateStyle: 'long', 'timeStyle': 'short' }).format(new Date(hop.timestamp)) : '-'}</small>
|
||||||
{hop.timestamp
|
|
||||||
? new Intl.DateTimeFormat("default", {
|
|
||||||
dateStyle: "long",
|
|
||||||
timeStyle: "short",
|
|
||||||
}).format(new Date(hop.timestamp))
|
|
||||||
: "-"}
|
|
||||||
</small>
|
|
||||||
</div>
|
</div>
|
||||||
{#if hop.with || hop.id}
|
{#if hop.with || hop.id}
|
||||||
<p class="mb-1 small d-flex gap-3">
|
<p class="mb-1 small d-flex gap-3">
|
||||||
{#if hop.with}
|
{#if hop.with}
|
||||||
<span>
|
<span>
|
||||||
<span class="text-muted">Protocol:</span>
|
<span class="text-muted">Protocol:</span> <code>{hop.with}</code>
|
||||||
<code>{hop.with}</code>
|
|
||||||
</span>
|
</span>
|
||||||
{/if}
|
{/if}
|
||||||
{#if hop.id}
|
{#if hop.id}
|
||||||
|
|
|
||||||
|
|
@ -44,7 +44,10 @@
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<strong class={getSizeClass(size)} style="color: {getGradeColor(grade)}; font-weight: 700;">
|
<strong
|
||||||
|
class={getSizeClass(size)}
|
||||||
|
style="color: {getGradeColor(grade)}; font-weight: 700;"
|
||||||
|
>
|
||||||
{#if grade}
|
{#if grade}
|
||||||
{grade}
|
{grade}
|
||||||
{:else}
|
{:else}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { DmarcRecord, HeaderAnalysis } from "$lib/api/types.gen";
|
import type { AuthResult, DmarcRecord, HeaderAnalysis } from "$lib/api/types.gen";
|
||||||
import { getScoreColorClass } from "$lib/score";
|
import { getScoreColorClass } from "$lib/score";
|
||||||
import { theme } from "$lib/stores/theme";
|
import { theme } from "$lib/stores/theme";
|
||||||
import GradeDisplay from "./GradeDisplay.svelte";
|
import GradeDisplay from "./GradeDisplay.svelte";
|
||||||
|
|
@ -38,14 +38,7 @@
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<h5>Issues</h5>
|
<h5>Issues</h5>
|
||||||
{#each headerAnalysis.issues as issue}
|
{#each headerAnalysis.issues as issue}
|
||||||
<div
|
<div class="alert alert-{issue.severity === 'critical' || issue.severity === 'high' ? 'danger' : issue.severity === 'medium' ? 'warning' : 'info'} py-2 px-3 mb-2">
|
||||||
class="alert alert-{issue.severity === 'critical' ||
|
|
||||||
issue.severity === 'high'
|
|
||||||
? 'danger'
|
|
||||||
: issue.severity === 'medium'
|
|
||||||
? 'warning'
|
|
||||||
: 'info'} py-2 px-3 mb-2"
|
|
||||||
>
|
|
||||||
<div class="d-flex justify-content-between align-items-start">
|
<div class="d-flex justify-content-between align-items-start">
|
||||||
<div>
|
<div>
|
||||||
<strong>{issue.header}</strong>
|
<strong>{issue.header}</strong>
|
||||||
|
|
@ -65,48 +58,24 @@
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if headerAnalysis.domain_alignment}
|
{#if headerAnalysis.domain_alignment}
|
||||||
{@const spfStrictAligned =
|
{@const spfStrictAligned = headerAnalysis.domain_alignment.from_domain === headerAnalysis.domain_alignment.return_path_domain}
|
||||||
headerAnalysis.domain_alignment.from_domain ===
|
{@const spfRelaxedAligned = headerAnalysis.domain_alignment.from_org_domain === headerAnalysis.domain_alignment.return_path_org_domain}
|
||||||
headerAnalysis.domain_alignment.return_path_domain}
|
|
||||||
{@const spfRelaxedAligned =
|
|
||||||
headerAnalysis.domain_alignment.from_org_domain ===
|
|
||||||
headerAnalysis.domain_alignment.return_path_org_domain}
|
|
||||||
<div class="card mb-3" id="domain-alignment">
|
<div class="card mb-3" id="domain-alignment">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<h5 class="mb-0">
|
<h5 class="mb-0">
|
||||||
<i
|
<i class="bi {headerAnalysis.domain_alignment.aligned ? 'bi-check-circle-fill text-success' : headerAnalysis.domain_alignment.relaxed_aligned ? 'bi-check-circle text-info' : 'bi-x-circle-fill text-danger'}"></i>
|
||||||
class="bi {headerAnalysis.domain_alignment.aligned
|
|
||||||
? 'bi-check-circle-fill text-success'
|
|
||||||
: headerAnalysis.domain_alignment.relaxed_aligned
|
|
||||||
? 'bi-check-circle text-info'
|
|
||||||
: 'bi-x-circle-fill text-danger'}"
|
|
||||||
></i>
|
|
||||||
Domain Alignment
|
Domain Alignment
|
||||||
</h5>
|
</h5>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<p class="card-text small text-muted">
|
<p class="card-text small text-muted">
|
||||||
Domain alignment ensures that the visible "From" domain matches the domain
|
Domain alignment ensures that the visible "From" domain matches the domain used for authentication (Return-Path or DKIM signature). Proper alignment is crucial for DMARC compliance, regardless of the policy. It helps prevent email spoofing by verifying that the sender domain is consistent across all authentication layers. Only one of the following lines needs to pass.
|
||||||
used for authentication (Return-Path or DKIM signature). Proper alignment is
|
|
||||||
crucial for DMARC compliance, regardless of the policy. It helps prevent
|
|
||||||
email spoofing by verifying that the sender domain is consistent across all
|
|
||||||
authentication layers. Only one of the following lines needs to pass.
|
|
||||||
</p>
|
</p>
|
||||||
{#if headerAnalysis.domain_alignment.issues && headerAnalysis.domain_alignment.issues.length > 0}
|
{#if headerAnalysis.domain_alignment.issues && headerAnalysis.domain_alignment.issues.length > 0}
|
||||||
<div class="mt-3">
|
<div class="mt-3">
|
||||||
{#each headerAnalysis.domain_alignment.issues as issue}
|
{#each headerAnalysis.domain_alignment.issues as issue}
|
||||||
<div
|
<div class="alert alert-{headerAnalysis.domain_alignment.relaxed_aligned ? 'info' : 'warning'} py-2 small mb-2">
|
||||||
class="alert alert-{headerAnalysis.domain_alignment
|
<i class="bi bi-{headerAnalysis.domain_alignment.relaxed_aligned ? 'info-circle' : 'exclamation-triangle'} me-1"></i>
|
||||||
.relaxed_aligned
|
|
||||||
? 'info'
|
|
||||||
: 'warning'} py-2 small mb-2"
|
|
||||||
>
|
|
||||||
<i
|
|
||||||
class="bi bi-{headerAnalysis.domain_alignment
|
|
||||||
.relaxed_aligned
|
|
||||||
? 'info-circle'
|
|
||||||
: 'exclamation-triangle'} me-1"
|
|
||||||
></i>
|
|
||||||
{issue}
|
{issue}
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
|
|
@ -115,10 +84,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="list-group list-group-flush">
|
<div class="list-group list-group-flush">
|
||||||
<div class="list-group-item d-flex ps-0">
|
<div class="list-group-item d-flex ps-0">
|
||||||
<div
|
<div class="d-flex align-items-center justify-content-center" style="writing-mode: vertical-rl; transform: rotate(180deg); font-size: 1.5rem; font-weight: bold; min-width: 3rem;">
|
||||||
class="d-flex align-items-center justify-content-center"
|
|
||||||
style="writing-mode: vertical-rl; transform: rotate(180deg); font-size: 1.5rem; font-weight: bold; min-width: 3rem;"
|
|
||||||
>
|
|
||||||
SPF
|
SPF
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-fill">
|
<div class="flex-fill">
|
||||||
|
|
@ -126,17 +92,9 @@
|
||||||
<div class="col-md-3">
|
<div class="col-md-3">
|
||||||
<small class="text-muted">Strict Alignment</small>
|
<small class="text-muted">Strict Alignment</small>
|
||||||
<div>
|
<div>
|
||||||
<span
|
<span class="badge" class:bg-success={spfStrictAligned} class:bg-danger={!spfStrictAligned}>
|
||||||
class="badge"
|
<i class="bi {spfStrictAligned ? 'bi-check-circle-fill' : 'bi-x-circle-fill'} me-1"></i>
|
||||||
class:bg-success={spfStrictAligned}
|
<strong>{spfStrictAligned ? 'Pass' : 'Fail'}</strong>
|
||||||
class:bg-danger={!spfStrictAligned}
|
|
||||||
>
|
|
||||||
<i
|
|
||||||
class="bi {spfStrictAligned
|
|
||||||
? 'bi-check-circle-fill'
|
|
||||||
: 'bi-x-circle-fill'} me-1"
|
|
||||||
></i>
|
|
||||||
<strong>{spfStrictAligned ? "Pass" : "Fail"}</strong>
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="small text-muted mt-1">Exact domain match</div>
|
<div class="small text-muted mt-1">Exact domain match</div>
|
||||||
|
|
@ -144,78 +102,38 @@
|
||||||
<div class="col-md-3">
|
<div class="col-md-3">
|
||||||
<small class="text-muted">Relaxed Alignment</small>
|
<small class="text-muted">Relaxed Alignment</small>
|
||||||
<div>
|
<div>
|
||||||
<span
|
<span class="badge" class:bg-success={spfRelaxedAligned} class:bg-danger={!spfRelaxedAligned}>
|
||||||
class="badge"
|
<i class="bi {spfRelaxedAligned ? 'bi-check-circle-fill' : 'bi-x-circle-fill'} me-1"></i>
|
||||||
class:bg-success={spfRelaxedAligned}
|
<strong>{spfRelaxedAligned ? 'Pass' : 'Fail'}</strong>
|
||||||
class:bg-danger={!spfRelaxedAligned}
|
|
||||||
>
|
|
||||||
<i
|
|
||||||
class="bi {spfRelaxedAligned
|
|
||||||
? 'bi-check-circle-fill'
|
|
||||||
: 'bi-x-circle-fill'} me-1"
|
|
||||||
></i>
|
|
||||||
<strong>{spfRelaxedAligned ? "Pass" : "Fail"}</strong>
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="small text-muted mt-1">
|
<div class="small text-muted mt-1">Organizational domain match</div>
|
||||||
Organizational domain match
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-3">
|
<div class="col-md-3">
|
||||||
<small class="text-muted">From Domain</small>
|
<small class="text-muted">From Domain</small>
|
||||||
<div>
|
<div><code>{headerAnalysis.domain_alignment.from_domain || '-'}</code></div>
|
||||||
<code>
|
|
||||||
{headerAnalysis.domain_alignment.from_domain || "-"}
|
|
||||||
</code>
|
|
||||||
</div>
|
|
||||||
{#if headerAnalysis.domain_alignment.from_org_domain && headerAnalysis.domain_alignment.from_org_domain !== headerAnalysis.domain_alignment.from_domain}
|
{#if headerAnalysis.domain_alignment.from_org_domain && headerAnalysis.domain_alignment.from_org_domain !== headerAnalysis.domain_alignment.from_domain}
|
||||||
<div class="small text-muted mt-1">
|
<div class="small text-muted mt-1">Org: <code>{headerAnalysis.domain_alignment.from_org_domain}</code></div>
|
||||||
Org:
|
|
||||||
<code>
|
|
||||||
{headerAnalysis.domain_alignment.from_org_domain}
|
|
||||||
</code>
|
|
||||||
</div>
|
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-3">
|
<div class="col-md-3">
|
||||||
<small class="text-muted">Return-Path Domain</small>
|
<small class="text-muted">Return-Path Domain</small>
|
||||||
<div>
|
<div><code>{headerAnalysis.domain_alignment.return_path_domain || '-'}</code></div>
|
||||||
<code>
|
|
||||||
{headerAnalysis.domain_alignment.return_path_domain ||
|
|
||||||
"-"}
|
|
||||||
</code>
|
|
||||||
</div>
|
|
||||||
{#if headerAnalysis.domain_alignment.return_path_org_domain && headerAnalysis.domain_alignment.return_path_org_domain !== headerAnalysis.domain_alignment.return_path_domain}
|
{#if headerAnalysis.domain_alignment.return_path_org_domain && headerAnalysis.domain_alignment.return_path_org_domain !== headerAnalysis.domain_alignment.return_path_domain}
|
||||||
<div class="small text-muted mt-1">
|
<div class="small text-muted mt-1">Org: <code>{headerAnalysis.domain_alignment.return_path_org_domain}</code></div>
|
||||||
Org:
|
|
||||||
<code>
|
|
||||||
{headerAnalysis.domain_alignment
|
|
||||||
.return_path_org_domain}
|
|
||||||
</code>
|
|
||||||
</div>
|
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Alignment Information based on DMARC policy -->
|
<!-- Alignment Information based on DMARC policy -->
|
||||||
{#if dmarcRecord && headerAnalysis.domain_alignment.return_path_domain && headerAnalysis.domain_alignment.return_path_domain !== headerAnalysis.domain_alignment.from_domain}
|
{#if dmarcRecord && headerAnalysis.domain_alignment.return_path_domain && headerAnalysis.domain_alignment.return_path_domain !== headerAnalysis.domain_alignment.from_domain}
|
||||||
<div
|
<div class="alert mt-2 mb-0 small py-2 {dmarcRecord.spf_alignment === 'strict' ? 'alert-warning' : 'alert-info'}">
|
||||||
class="alert mt-2 mb-0 small py-2 {dmarcRecord.spf_alignment ===
|
{#if dmarcRecord.spf_alignment === 'strict'}
|
||||||
'strict'
|
|
||||||
? 'alert-warning'
|
|
||||||
: 'alert-info'}"
|
|
||||||
>
|
|
||||||
{#if dmarcRecord.spf_alignment === "strict"}
|
|
||||||
<i class="bi bi-exclamation-triangle me-1"></i>
|
<i class="bi bi-exclamation-triangle me-1"></i>
|
||||||
<strong>Strict SPF alignment required</strong> — Your DMARC policy
|
<strong>Strict SPF alignment required</strong> — Your DMARC policy requires exact domain match. The Return-Path domain must exactly match the From domain for SPF to pass DMARC alignment.
|
||||||
requires exact domain match. The Return-Path domain must exactly
|
|
||||||
match the From domain for SPF to pass DMARC alignment.
|
|
||||||
{:else}
|
{:else}
|
||||||
<i class="bi bi-info-circle me-1"></i>
|
<i class="bi bi-info-circle me-1"></i>
|
||||||
<strong>Relaxed SPF alignment allowed</strong> — Your DMARC policy
|
<strong>Relaxed SPF alignment allowed</strong> — Your DMARC policy allows organizational domain matching. As long as both domains share the same organizational domain (e.g., mail.example.com and example.com), SPF alignment can pass.
|
||||||
allows organizational domain matching. As long as both domains
|
|
||||||
share the same organizational domain (e.g., mail.example.com
|
|
||||||
and example.com), SPF alignment can pass.
|
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
@ -223,16 +141,10 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#each headerAnalysis.domain_alignment.dkim_domains as dkim_domain}
|
{#each headerAnalysis.domain_alignment.dkim_domains as dkim_domain}
|
||||||
{@const dkim_aligned =
|
{@const dkim_aligned = dkim_domain.domain === headerAnalysis.domain_alignment.from_domain}
|
||||||
dkim_domain.domain === headerAnalysis.domain_alignment.from_domain}
|
{@const dkim_relaxed_aligned = dkim_domain.org_domain === headerAnalysis.domain_alignment.from_org_domain}
|
||||||
{@const dkim_relaxed_aligned =
|
|
||||||
dkim_domain.org_domain ===
|
|
||||||
headerAnalysis.domain_alignment.from_org_domain}
|
|
||||||
<div class="list-group-item d-flex ps-0">
|
<div class="list-group-item d-flex ps-0">
|
||||||
<div
|
<div class="d-flex align-items-center justify-content-center" style="writing-mode: vertical-rl; transform: rotate(180deg); font-size: 1.5rem; font-weight: bold; min-width: 3rem;">
|
||||||
class="d-flex align-items-center justify-content-center"
|
|
||||||
style="writing-mode: vertical-rl; transform: rotate(180deg); font-size: 1.5rem; font-weight: bold; min-width: 3rem;"
|
|
||||||
>
|
|
||||||
DKIM
|
DKIM
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-fill">
|
<div class="flex-fill">
|
||||||
|
|
@ -241,72 +153,35 @@
|
||||||
<div class="col-md-3">
|
<div class="col-md-3">
|
||||||
<small class="text-muted">Strict Alignment</small>
|
<small class="text-muted">Strict Alignment</small>
|
||||||
<div>
|
<div>
|
||||||
<span
|
<span class="badge" class:bg-success={dkim_aligned} class:bg-danger={!dkim_aligned}>
|
||||||
class="badge"
|
<i class="bi {dkim_aligned ? 'bi-check-circle-fill' : 'bi-x-circle-fill'} me-1"></i>
|
||||||
class:bg-success={dkim_aligned}
|
<strong>{dkim_aligned ? 'Pass' : 'Fail'}</strong>
|
||||||
class:bg-danger={!dkim_aligned}
|
|
||||||
>
|
|
||||||
<i
|
|
||||||
class="bi {dkim_aligned
|
|
||||||
? 'bi-check-circle-fill'
|
|
||||||
: 'bi-x-circle-fill'} me-1"
|
|
||||||
></i>
|
|
||||||
<strong>{dkim_aligned ? "Pass" : "Fail"}</strong
|
|
||||||
>
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="small text-muted mt-1">
|
<div class="small text-muted mt-1">Exact domain match</div>
|
||||||
Exact domain match
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-3">
|
<div class="col-md-3">
|
||||||
<small class="text-muted">Relaxed Alignment</small>
|
<small class="text-muted">Relaxed Alignment</small>
|
||||||
<div>
|
<div>
|
||||||
<span
|
<span class="badge" class:bg-success={dkim_relaxed_aligned} class:bg-danger={!dkim_relaxed_aligned}>
|
||||||
class="badge"
|
<i class="bi {dkim_relaxed_aligned ? 'bi-check-circle-fill' : 'bi-x-circle-fill'} me-1"></i>
|
||||||
class:bg-success={dkim_relaxed_aligned}
|
<strong>{dkim_relaxed_aligned ? 'Pass' : 'Fail'}</strong>
|
||||||
class:bg-danger={!dkim_relaxed_aligned}
|
|
||||||
>
|
|
||||||
<i
|
|
||||||
class="bi {dkim_relaxed_aligned
|
|
||||||
? 'bi-check-circle-fill'
|
|
||||||
: 'bi-x-circle-fill'} me-1"
|
|
||||||
></i>
|
|
||||||
<strong
|
|
||||||
>{dkim_relaxed_aligned
|
|
||||||
? "Pass"
|
|
||||||
: "Fail"}</strong
|
|
||||||
>
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="small text-muted mt-1">
|
<div class="small text-muted mt-1">Organizational domain match</div>
|
||||||
Organizational domain match
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-3">
|
<div class="col-md-3">
|
||||||
<small class="text-muted">From Domain</small>
|
<small class="text-muted">From Domain</small>
|
||||||
<div>
|
<div><code>{headerAnalysis.domain_alignment.from_domain || '-'}</code></div>
|
||||||
<code
|
|
||||||
>{headerAnalysis.domain_alignment.from_domain ||
|
|
||||||
"-"}</code
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
{#if headerAnalysis.domain_alignment.from_org_domain && headerAnalysis.domain_alignment.from_org_domain !== headerAnalysis.domain_alignment.from_domain}
|
{#if headerAnalysis.domain_alignment.from_org_domain && headerAnalysis.domain_alignment.from_org_domain !== headerAnalysis.domain_alignment.from_domain}
|
||||||
<div class="small text-muted mt-1">
|
<div class="small text-muted mt-1">Org: <code>{headerAnalysis.domain_alignment.from_org_domain}</code></div>
|
||||||
Org: <code
|
|
||||||
>{headerAnalysis.domain_alignment
|
|
||||||
.from_org_domain}</code
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-3">
|
<div class="col-md-3">
|
||||||
<small class="text-muted">Signature Domain</small>
|
<small class="text-muted">Signature Domain</small>
|
||||||
<div><code>{dkim_domain.domain || "-"}</code></div>
|
<div><code>{dkim_domain.domain || '-'}</code></div>
|
||||||
{#if dkim_domain.domain !== dkim_domain.org_domain}
|
{#if dkim_domain.domain !== dkim_domain.org_domain}
|
||||||
<div class="small text-muted mt-1">
|
<div class="small text-muted mt-1">Org: <code>{dkim_domain.org_domain}</code></div>
|
||||||
Org: <code>{dkim_domain.org_domain}</code>
|
|
||||||
</div>
|
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -314,25 +189,13 @@
|
||||||
<!-- Alignment Information based on DMARC policy -->
|
<!-- Alignment Information based on DMARC policy -->
|
||||||
{#if dmarcRecord && dkim_domain.domain !== headerAnalysis.domain_alignment.from_domain}
|
{#if dmarcRecord && dkim_domain.domain !== headerAnalysis.domain_alignment.from_domain}
|
||||||
{#if dkim_domain.org_domain === headerAnalysis.domain_alignment.from_org_domain}
|
{#if dkim_domain.org_domain === headerAnalysis.domain_alignment.from_org_domain}
|
||||||
<div
|
<div class="alert mt-2 mb-0 small py-2 {dmarcRecord.dkim_alignment === 'strict' ? 'alert-warning' : 'alert-info'}">
|
||||||
class="alert mt-2 mb-0 small py-2 {dmarcRecord.dkim_alignment ===
|
{#if dmarcRecord.dkim_alignment === 'strict'}
|
||||||
'strict'
|
|
||||||
? 'alert-warning'
|
|
||||||
: 'alert-info'}"
|
|
||||||
>
|
|
||||||
{#if dmarcRecord.dkim_alignment === "strict"}
|
|
||||||
<i class="bi bi-exclamation-triangle me-1"></i>
|
<i class="bi bi-exclamation-triangle me-1"></i>
|
||||||
<strong>Strict DKIM alignment required</strong> —
|
<strong>Strict DKIM alignment required</strong> — Your DMARC policy requires exact domain match. The DKIM signature domain must exactly match the From domain for DKIM to pass DMARC alignment.
|
||||||
Your DMARC policy requires exact domain match. The
|
|
||||||
DKIM signature domain must exactly match the From
|
|
||||||
domain for DKIM to pass DMARC alignment.
|
|
||||||
{:else}
|
{:else}
|
||||||
<i class="bi bi-info-circle me-1"></i>
|
<i class="bi bi-info-circle me-1"></i>
|
||||||
<strong>Relaxed DKIM alignment allowed</strong> —
|
<strong>Relaxed DKIM alignment allowed</strong> — Your DMARC policy allows organizational domain matching. As long as both domains share the same organizational domain (e.g., mail.example.com and example.com), DKIM alignment can pass.
|
||||||
Your DMARC policy allows organizational domain matching.
|
|
||||||
As long as both domains share the same organizational
|
|
||||||
domain (e.g., mail.example.com and example.com),
|
|
||||||
DKIM alignment can pass.
|
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
@ -361,9 +224,9 @@
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{#each Object.entries(headerAnalysis.headers).sort((a, b) => {
|
{#each Object.entries(headerAnalysis.headers).sort((a, b) => {
|
||||||
const importanceOrder = { required: 0, recommended: 1, optional: 2, newsletter: 3 };
|
const importanceOrder = { 'required': 0, 'recommended': 1, 'optional': 2, 'newsletter': 3 };
|
||||||
const aImportance = importanceOrder[a[1].importance || "optional"];
|
const aImportance = importanceOrder[a[1].importance || 'optional'];
|
||||||
const bImportance = importanceOrder[b[1].importance || "optional"];
|
const bImportance = importanceOrder[b[1].importance || 'optional'];
|
||||||
return aImportance - bImportance;
|
return aImportance - bImportance;
|
||||||
}) as [name, check]}
|
}) as [name, check]}
|
||||||
<tr>
|
<tr>
|
||||||
|
|
@ -372,39 +235,23 @@
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{#if check.importance}
|
{#if check.importance}
|
||||||
<small
|
<small class="text-{check.importance === 'required' ? 'danger' : check.importance === 'recommended' ? 'warning' : 'secondary'}">
|
||||||
class="text-{check.importance === 'required'
|
|
||||||
? 'danger'
|
|
||||||
: check.importance === 'recommended'
|
|
||||||
? 'warning'
|
|
||||||
: 'secondary'}"
|
|
||||||
>
|
|
||||||
{check.importance}
|
{check.importance}
|
||||||
</small>
|
</small>
|
||||||
{/if}
|
{/if}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<i
|
<i class="bi {check.present ? 'bi-check-circle text-success' : 'bi-x-circle text-danger'}"></i>
|
||||||
class="bi {check.present
|
|
||||||
? 'bi-check-circle text-success'
|
|
||||||
: 'bi-x-circle text-danger'}"
|
|
||||||
></i>
|
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{#if check.present && check.valid !== undefined}
|
{#if check.present && check.valid !== undefined}
|
||||||
<i
|
<i class="bi {check.valid ? 'bi-check-circle text-success' : 'bi-x-circle text-warning'}"></i>
|
||||||
class="bi {check.valid
|
|
||||||
? 'bi-check-circle text-success'
|
|
||||||
: 'bi-x-circle text-warning'}"
|
|
||||||
></i>
|
|
||||||
{:else}
|
{:else}
|
||||||
-
|
-
|
||||||
{/if}
|
{/if}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<small class="text-muted text-truncate" title={check.value}
|
<small class="text-muted text-truncate" title={check.value}>{check.value || '-'}</small>
|
||||||
>{check.value || "-"}</small
|
|
||||||
>
|
|
||||||
{#if check.issues && check.issues.length > 0}
|
{#if check.issues && check.issues.length > 0}
|
||||||
{#each check.issues as issue}
|
{#each check.issues as issue}
|
||||||
<div class="text-warning small">
|
<div class="text-warning small">
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { ClassValue } from "svelte/elements";
|
import type { ClassValue } from "svelte/elements";
|
||||||
|
|
||||||
import type { MxRecord } from "$lib/api/types.gen";
|
import type { MxRecord } from "$lib/api/types.gen";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { ScoreSummary } from "$lib/api/types.gen";
|
import type { ScoreSummary } from "$lib/api/types.gen";
|
||||||
import { theme } from "$lib/stores/theme";
|
|
||||||
import GradeDisplay from "./GradeDisplay.svelte";
|
import GradeDisplay from "./GradeDisplay.svelte";
|
||||||
|
import { theme } from "$lib/stores/theme";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
grade: string;
|
grade: string;
|
||||||
|
|
@ -58,10 +58,13 @@
|
||||||
<a href="#dns-details" class="text-decoration-none">
|
<a href="#dns-details" class="text-decoration-none">
|
||||||
<div
|
<div
|
||||||
class="p-2 rounded text-center summary-card"
|
class="p-2 rounded text-center summary-card"
|
||||||
class:bg-light={$theme === "light"}
|
class:bg-light={$theme === 'light'}
|
||||||
class:bg-secondary={$theme !== "light"}
|
class:bg-secondary={$theme !== 'light'}
|
||||||
>
|
>
|
||||||
<GradeDisplay grade={summary.dns_grade} score={summary.dns_score} />
|
<GradeDisplay
|
||||||
|
grade={summary.dns_grade}
|
||||||
|
score={summary.dns_score}
|
||||||
|
/>
|
||||||
<small class="text-muted d-block">DNS</small>
|
<small class="text-muted d-block">DNS</small>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
|
|
@ -70,8 +73,8 @@
|
||||||
<a href="#authentication-details" class="text-decoration-none">
|
<a href="#authentication-details" class="text-decoration-none">
|
||||||
<div
|
<div
|
||||||
class="p-2 rounded text-center summary-card"
|
class="p-2 rounded text-center summary-card"
|
||||||
class:bg-light={$theme === "light"}
|
class:bg-light={$theme === 'light'}
|
||||||
class:bg-secondary={$theme !== "light"}
|
class:bg-secondary={$theme !== 'light'}
|
||||||
>
|
>
|
||||||
<GradeDisplay
|
<GradeDisplay
|
||||||
grade={summary.authentication_grade}
|
grade={summary.authentication_grade}
|
||||||
|
|
@ -85,8 +88,8 @@
|
||||||
<a href="#rbl-details" class="text-decoration-none">
|
<a href="#rbl-details" class="text-decoration-none">
|
||||||
<div
|
<div
|
||||||
class="p-2 rounded text-center summary-card"
|
class="p-2 rounded text-center summary-card"
|
||||||
class:bg-light={$theme === "light"}
|
class:bg-light={$theme === 'light'}
|
||||||
class:bg-secondary={$theme !== "light"}
|
class:bg-secondary={$theme !== 'light'}
|
||||||
>
|
>
|
||||||
<GradeDisplay
|
<GradeDisplay
|
||||||
grade={summary.blacklist_grade}
|
grade={summary.blacklist_grade}
|
||||||
|
|
@ -100,8 +103,8 @@
|
||||||
<a href="#header-details" class="text-decoration-none">
|
<a href="#header-details" class="text-decoration-none">
|
||||||
<div
|
<div
|
||||||
class="p-2 rounded text-center summary-card"
|
class="p-2 rounded text-center summary-card"
|
||||||
class:bg-light={$theme === "light"}
|
class:bg-light={$theme === 'light'}
|
||||||
class:bg-secondary={$theme !== "light"}
|
class:bg-secondary={$theme !== 'light'}
|
||||||
>
|
>
|
||||||
<GradeDisplay
|
<GradeDisplay
|
||||||
grade={summary.header_grade}
|
grade={summary.header_grade}
|
||||||
|
|
@ -115,10 +118,13 @@
|
||||||
<a href="#spam-details" class="text-decoration-none">
|
<a href="#spam-details" class="text-decoration-none">
|
||||||
<div
|
<div
|
||||||
class="p-2 rounded text-center summary-card"
|
class="p-2 rounded text-center summary-card"
|
||||||
class:bg-light={$theme === "light"}
|
class:bg-light={$theme === 'light'}
|
||||||
class:bg-secondary={$theme !== "light"}
|
class:bg-secondary={$theme !== 'light'}
|
||||||
>
|
>
|
||||||
<GradeDisplay grade={summary.spam_grade} score={summary.spam_score} />
|
<GradeDisplay
|
||||||
|
grade={summary.spam_grade}
|
||||||
|
score={summary.spam_score}
|
||||||
|
/>
|
||||||
<small class="text-muted d-block">Spam Score</small>
|
<small class="text-muted d-block">Spam Score</small>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
|
|
@ -127,8 +133,8 @@
|
||||||
<a href="#content-details" class="text-decoration-none">
|
<a href="#content-details" class="text-decoration-none">
|
||||||
<div
|
<div
|
||||||
class="p-2 rounded text-center summary-card"
|
class="p-2 rounded text-center summary-card"
|
||||||
class:bg-light={$theme === "light"}
|
class:bg-light={$theme === 'light'}
|
||||||
class:bg-secondary={$theme !== "light"}
|
class:bg-secondary={$theme !== 'light'}
|
||||||
>
|
>
|
||||||
<GradeDisplay
|
<GradeDisplay
|
||||||
grade={summary.content_grade}
|
grade={summary.content_grade}
|
||||||
|
|
|
||||||
|
|
@ -61,26 +61,14 @@
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{#each Object.entries(spamassassin.test_details) as [testName, detail]}
|
{#each Object.entries(spamassassin.test_details) as [testName, detail]}
|
||||||
<tr
|
<tr class={detail.score > 0 ? 'table-warning' : detail.score < 0 ? 'table-success' : ''}>
|
||||||
class={detail.score > 0
|
|
||||||
? "table-warning"
|
|
||||||
: detail.score < 0
|
|
||||||
? "table-success"
|
|
||||||
: ""}
|
|
||||||
>
|
|
||||||
<td class="font-monospace">{testName}</td>
|
<td class="font-monospace">{testName}</td>
|
||||||
<td class="text-end">
|
<td class="text-end">
|
||||||
<span
|
<span class={detail.score > 0 ? 'text-danger fw-bold' : detail.score < 0 ? 'text-success fw-bold' : 'text-muted'}>
|
||||||
class={detail.score > 0
|
{detail.score > 0 ? '+' : ''}{detail.score.toFixed(1)}
|
||||||
? "text-danger fw-bold"
|
|
||||||
: detail.score < 0
|
|
||||||
? "text-success fw-bold"
|
|
||||||
: "text-muted"}
|
|
||||||
>
|
|
||||||
{detail.score > 0 ? "+" : ""}{detail.score.toFixed(1)}
|
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td class="small">{detail.description || ""}</td>
|
<td class="small">{detail.description || ''}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{/each}
|
{/each}
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|
@ -92,11 +80,7 @@
|
||||||
<strong>Tests Triggered:</strong>
|
<strong>Tests Triggered:</strong>
|
||||||
<div class="mt-2">
|
<div class="mt-2">
|
||||||
{#each spamassassin.tests as test}
|
{#each spamassassin.tests as test}
|
||||||
<span
|
<span class="badge {$theme === 'light' ? 'bg-light text-dark' : 'bg-secondary'} me-1 mb-1">{test}</span>
|
||||||
class="badge {$theme === 'light'
|
|
||||||
? 'bg-light text-dark'
|
|
||||||
: 'bg-secondary'} me-1 mb-1">{test}</span
|
|
||||||
>
|
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -105,10 +89,7 @@
|
||||||
{#if spamassassin.report}
|
{#if spamassassin.report}
|
||||||
<details class="mt-3">
|
<details class="mt-3">
|
||||||
<summary class="cursor-pointer fw-bold">Raw Report</summary>
|
<summary class="cursor-pointer fw-bold">Raw Report</summary>
|
||||||
<pre
|
<pre class="mt-2 small {$theme === 'light' ? 'bg-light' : 'bg-secondary'} p-3 rounded">{spamassassin.report}</pre>
|
||||||
class="mt-2 small {$theme === 'light'
|
|
||||||
? 'bg-light'
|
|
||||||
: 'bg-secondary'} p-3 rounded">{spamassassin.report}</pre>
|
|
||||||
</details>
|
</details>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -11,8 +11,8 @@
|
||||||
// Check if DMARC has strict policy (quarantine or reject)
|
// Check if DMARC has strict policy (quarantine or reject)
|
||||||
const dmarcStrict = $derived(
|
const dmarcStrict = $derived(
|
||||||
dmarcRecord?.valid &&
|
dmarcRecord?.valid &&
|
||||||
dmarcRecord?.policy &&
|
dmarcRecord?.policy &&
|
||||||
(dmarcRecord.policy === "quarantine" || dmarcRecord.policy === "reject"),
|
(dmarcRecord.policy === "quarantine" || dmarcRecord.policy === "reject")
|
||||||
);
|
);
|
||||||
|
|
||||||
// Compute overall validity
|
// Compute overall validity
|
||||||
|
|
@ -43,11 +43,7 @@
|
||||||
<span class="badge bg-secondary">SPF</span>
|
<span class="badge bg-secondary">SPF</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<p class="card-text small text-muted mb-0">
|
<p class="card-text small text-muted mb-0">SPF specifies which mail servers are authorized to send emails on behalf of your domain. Receiving servers check the sender's IP address against your SPF record to prevent email spoofing.</p>
|
||||||
SPF specifies which mail servers are authorized to send emails on behalf of your
|
|
||||||
domain. Receiving servers check the sender's IP address against your SPF record to
|
|
||||||
prevent email spoofing.
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="list-group list-group-flush">
|
<div class="list-group list-group-flush">
|
||||||
{#each spfRecords as spf, index}
|
{#each spfRecords as spf, index}
|
||||||
|
|
@ -80,31 +76,18 @@
|
||||||
{:else if spf.all_qualifier === "?"}
|
{:else if spf.all_qualifier === "?"}
|
||||||
<span class="badge bg-warning">Neutral (?all)</span>
|
<span class="badge bg-warning">Neutral (?all)</span>
|
||||||
{/if}
|
{/if}
|
||||||
{#if index === 0 || (index === 1 && spfRecords[0].record?.includes("redirect="))}
|
{#if index === 0 || (index === 1 && spfRecords[0].record?.includes('redirect='))}
|
||||||
<div
|
<div class="alert small mt-2" class:alert-warning={spf.all_qualifier !== '-'} class:alert-success={spf.all_qualifier === '-'}>
|
||||||
class="alert small mt-2"
|
{#if spf.all_qualifier === '-'}
|
||||||
class:alert-warning={spf.all_qualifier !== "-"}
|
All unauthorized servers will be rejected. This is the recommended strict policy.
|
||||||
class:alert-success={spf.all_qualifier === "-"}
|
|
||||||
>
|
|
||||||
{#if spf.all_qualifier === "-"}
|
|
||||||
All unauthorized servers will be rejected. This is the
|
|
||||||
recommended strict policy.
|
|
||||||
{:else if dmarcStrict}
|
{:else if dmarcStrict}
|
||||||
While your DMARC {dmarcRecord?.policy} policy provides some protection,
|
While your DMARC {dmarcRecord?.policy} policy provides some protection, consider using <code>-all</code> for better security with some old mailbox providers.
|
||||||
consider using <code>-all</code> for better security with some
|
{:else if spf.all_qualifier === '~'}
|
||||||
old mailbox providers.
|
Unauthorized servers will softfail. Consider using <code>-all</code> for stricter policy, though this rarely affects legitimate email deliverability.
|
||||||
{:else if spf.all_qualifier === "~"}
|
{:else if spf.all_qualifier === '+'}
|
||||||
Unauthorized servers will softfail. Consider using <code
|
All servers are allowed to send email. This severely weakens email authentication. Use <code>-all</code> for strict policy.
|
||||||
>-all</code
|
{:else if spf.all_qualifier === '?'}
|
||||||
> for stricter policy, though this rarely affects legitimate
|
No statement about unauthorized servers. Use <code>-all</code> for strict policy to prevent spoofing.
|
||||||
email deliverability.
|
|
||||||
{:else if spf.all_qualifier === "+"}
|
|
||||||
All servers are allowed to send email. This severely weakens
|
|
||||||
email authentication. Use <code>-all</code> for strict policy.
|
|
||||||
{:else if spf.all_qualifier === "?"}
|
|
||||||
No statement about unauthorized servers. Use <code
|
|
||||||
>-all</code
|
|
||||||
> for strict policy to prevent spoofing.
|
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
@ -112,16 +95,14 @@
|
||||||
{/if}
|
{/if}
|
||||||
{#if spf.record}
|
{#if spf.record}
|
||||||
<div class="mb-2">
|
<div class="mb-2">
|
||||||
<strong>Record:</strong><br />
|
<strong>Record:</strong><br>
|
||||||
<code class="d-block mt-1 text-break">{spf.record}</code>
|
<code class="d-block mt-1 text-break">{spf.record}</code>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
{#if spf.error}
|
{#if spf.error}
|
||||||
<div class="alert alert-{spf.valid ? 'warning' : 'danger'} mb-0 mt-2">
|
<div class="alert alert-{spf.valid ? 'warning' : 'danger'} mb-0 mt-2">
|
||||||
<i class="bi bi-{spf.valid ? 'exclamation-triangle' : 'x-circle'} me-1"
|
<i class="bi bi-{spf.valid ? 'exclamation-triangle' : 'x-circle'} me-1"></i>
|
||||||
></i>
|
<strong>{spf.valid ? 'Warning:' : 'Error:'}</strong> {spf.error}
|
||||||
<strong>{spf.valid ? "Warning:" : "Error:"}</strong>
|
|
||||||
{spf.error}
|
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -318,9 +318,7 @@
|
||||||
// BIMI
|
// BIMI
|
||||||
const bimiResult = report.authentication?.bimi;
|
const bimiResult = report.authentication?.bimi;
|
||||||
if (
|
if (
|
||||||
dmarcRecord &&
|
(dmarcRecord && dmarcRecord.valid && dmarcRecord.policy != "none") &&
|
||||||
dmarcRecord.valid &&
|
|
||||||
dmarcRecord.policy != "none" &&
|
|
||||||
(!bimiResult || bimiResult.result !== "skipped")
|
(!bimiResult || bimiResult.result !== "skipped")
|
||||||
) {
|
) {
|
||||||
const bimiRecord = report.dns_results?.bimi_record;
|
const bimiRecord = report.dns_results?.bimi_record;
|
||||||
|
|
@ -525,39 +523,19 @@
|
||||||
{#if segment.link}
|
{#if segment.link}
|
||||||
<a
|
<a
|
||||||
href={segment.link}
|
href={segment.link}
|
||||||
class="summary-link {segment.highlight
|
class="summary-link {segment.highlight ? getColorClass(segment.highlight.color) : ''} {segment.highlight?.bold ? 'highlighted' : ''} {segment.highlight?.emphasis ? 'fst-italic' : ''} {segment.highlight?.monospace ? 'font-monospace' : ''}"
|
||||||
? getColorClass(segment.highlight.color)
|
|
||||||
: ''} {segment.highlight?.bold ? 'highlighted' : ''} {segment.highlight
|
|
||||||
?.emphasis
|
|
||||||
? 'fst-italic'
|
|
||||||
: ''} {segment.highlight?.monospace ? 'font-monospace' : ''}"
|
|
||||||
>
|
>
|
||||||
{segment.text}
|
{segment.text}
|
||||||
</a>
|
</a>
|
||||||
{:else if segment.highlight}
|
{:else if segment.highlight}
|
||||||
<span
|
<span class="{getColorClass(segment.highlight.color)} {segment.highlight.bold ? 'highlighted' : ''} {segment.highlight?.emphasis ? 'fst-italic' : ''} {segment.highlight?.monospace ? 'font-monospace' : ''}">
|
||||||
class="{getColorClass(segment.highlight.color)} {segment.highlight.bold
|
|
||||||
? 'highlighted'
|
|
||||||
: ''} {segment.highlight?.emphasis ? 'fst-italic' : ''} {segment
|
|
||||||
.highlight?.monospace
|
|
||||||
? 'font-monospace'
|
|
||||||
: ''}"
|
|
||||||
>
|
|
||||||
{segment.text}
|
{segment.text}
|
||||||
</span>
|
</span>
|
||||||
{:else}
|
{:else}
|
||||||
{segment.text}
|
{segment.text}
|
||||||
{/if}
|
{/if}
|
||||||
{/each}
|
{/each}
|
||||||
Overall, your email received a grade <GradeDisplay
|
Overall, your email received a grade <GradeDisplay grade={report.grade} score={report.score} size="inline" />{#if report.grade == "A" || report.grade == "A+"}, well done 🎉{:else if report.grade == "C" || report.grade == "D"}: you should try to increase your score to ensure inbox delivery.{:else if report.grade == "E"}: you could have delivery issues with common providers.{:else if report.grade == "F"}: it will most likely be rejected by most providers.{:else}!{/if} Check the details below 🔽
|
||||||
grade={report.grade}
|
|
||||||
score={report.score}
|
|
||||||
size="inline"
|
|
||||||
/>{#if report.grade == "A" || report.grade == "A+"}, well done 🎉{:else if report.grade == "C" || report.grade == "D"}:
|
|
||||||
you should try to increase your score to ensure inbox delivery.{:else if report.grade == "E"}:
|
|
||||||
you could have delivery issues with common providers.{:else if report.grade == "F"}:
|
|
||||||
it will most likely be rejected by most providers.{:else}!{/if} Check the details below
|
|
||||||
🔽
|
|
||||||
</p>
|
</p>
|
||||||
{@render children?.()}
|
{@render children?.()}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,25 +1,25 @@
|
||||||
// Component exports
|
// Component exports
|
||||||
|
export { default as FeatureCard } from "./FeatureCard.svelte";
|
||||||
|
export { default as HowItWorksStep } from "./HowItWorksStep.svelte";
|
||||||
|
export { default as ScoreCard } from "./ScoreCard.svelte";
|
||||||
|
export { default as SummaryCard } from "./SummaryCard.svelte";
|
||||||
|
export { default as SpamAssassinCard } from "./SpamAssassinCard.svelte";
|
||||||
|
export { default as EmailAddressDisplay } from "./EmailAddressDisplay.svelte";
|
||||||
|
export { default as PendingState } from "./PendingState.svelte";
|
||||||
export { default as AuthenticationCard } from "./AuthenticationCard.svelte";
|
export { default as AuthenticationCard } from "./AuthenticationCard.svelte";
|
||||||
export { default as BimiRecordDisplay } from "./BimiRecordDisplay.svelte";
|
export { default as DnsRecordsCard } from "./DnsRecordsCard.svelte";
|
||||||
export { default as BlacklistCard } from "./BlacklistCard.svelte";
|
export { default as BlacklistCard } from "./BlacklistCard.svelte";
|
||||||
export { default as ContentAnalysisCard } from "./ContentAnalysisCard.svelte";
|
export { default as ContentAnalysisCard } from "./ContentAnalysisCard.svelte";
|
||||||
export { default as DkimRecordsDisplay } from "./DkimRecordsDisplay.svelte";
|
|
||||||
export { default as DmarcRecordDisplay } from "./DmarcRecordDisplay.svelte";
|
|
||||||
export { default as DnsRecordsCard } from "./DnsRecordsCard.svelte";
|
|
||||||
export { default as EmailAddressDisplay } from "./EmailAddressDisplay.svelte";
|
|
||||||
export { default as EmailPathCard } from "./EmailPathCard.svelte";
|
|
||||||
export { default as ErrorDisplay } from "./ErrorDisplay.svelte";
|
|
||||||
export { default as FeatureCard } from "./FeatureCard.svelte";
|
|
||||||
export { default as GradeDisplay } from "./GradeDisplay.svelte";
|
|
||||||
export { default as HeaderAnalysisCard } from "./HeaderAnalysisCard.svelte";
|
export { default as HeaderAnalysisCard } from "./HeaderAnalysisCard.svelte";
|
||||||
export { default as HowItWorksStep } from "./HowItWorksStep.svelte";
|
|
||||||
export { default as Logo } from "./Logo.svelte";
|
|
||||||
export { default as MxRecordsDisplay } from "./MxRecordsDisplay.svelte";
|
|
||||||
export { default as PendingState } from "./PendingState.svelte";
|
|
||||||
export { default as PtrForwardRecordsDisplay } from "./PtrForwardRecordsDisplay.svelte";
|
|
||||||
export { default as PtrRecordsDisplay } from "./PtrRecordsDisplay.svelte";
|
export { default as PtrRecordsDisplay } from "./PtrRecordsDisplay.svelte";
|
||||||
export { default as ScoreCard } from "./ScoreCard.svelte";
|
export { default as PtrForwardRecordsDisplay } from "./PtrForwardRecordsDisplay.svelte";
|
||||||
export { default as SpamAssassinCard } from "./SpamAssassinCard.svelte";
|
|
||||||
export { default as SpfRecordsDisplay } from "./SpfRecordsDisplay.svelte";
|
|
||||||
export { default as SummaryCard } from "./SummaryCard.svelte";
|
|
||||||
export { default as TinySurvey } from "./TinySurvey.svelte";
|
export { default as TinySurvey } from "./TinySurvey.svelte";
|
||||||
|
export { default as ErrorDisplay } from "./ErrorDisplay.svelte";
|
||||||
|
export { default as GradeDisplay } from "./GradeDisplay.svelte";
|
||||||
|
export { default as MxRecordsDisplay } from "./MxRecordsDisplay.svelte";
|
||||||
|
export { default as SpfRecordsDisplay } from "./SpfRecordsDisplay.svelte";
|
||||||
|
export { default as DmarcRecordDisplay } from "./DmarcRecordDisplay.svelte";
|
||||||
|
export { default as BimiRecordDisplay } from "./BimiRecordDisplay.svelte";
|
||||||
|
export { default as DkimRecordsDisplay } from "./DkimRecordsDisplay.svelte";
|
||||||
|
export { default as Logo } from "./Logo.svelte";
|
||||||
|
export { default as EmailPathCard } from "./EmailPathCard.svelte";
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,6 @@ import { writable } from "svelte/store";
|
||||||
interface AppConfig {
|
interface AppConfig {
|
||||||
report_retention?: number;
|
report_retention?: number;
|
||||||
survey_url?: string;
|
survey_url?: string;
|
||||||
custom_logo_url?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultConfig: AppConfig = {
|
const defaultConfig: AppConfig = {
|
||||||
|
|
|
||||||
|
|
@ -1,26 +1,5 @@
|
||||||
// This file is part of the happyDeliver (R) project.
|
|
||||||
// Copyright (c) 2025 happyDomain
|
|
||||||
// Authors: Pierre-Olivier Mercier, et al.
|
|
||||||
//
|
|
||||||
// This program is offered under a commercial and under the AGPL license.
|
|
||||||
// For commercial licensing, contact us at <contact@happydomain.org>.
|
|
||||||
//
|
|
||||||
// For AGPL licensing:
|
|
||||||
// This program is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Affero General Public License as published by
|
|
||||||
// the Free Software Foundation, either version 3 of the License, or
|
|
||||||
// (at your option) any later version.
|
|
||||||
//
|
|
||||||
// This program is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Affero General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
import { browser } from "$app/environment";
|
|
||||||
import { writable } from "svelte/store";
|
import { writable } from "svelte/store";
|
||||||
|
import { browser } from "$app/environment";
|
||||||
|
|
||||||
const getInitialTheme = () => {
|
const getInitialTheme = () => {
|
||||||
if (!browser) return "light";
|
if (!browser) return "light";
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { page } from "$app/state";
|
import { page } from "$app/stores";
|
||||||
import { ErrorDisplay } from "$lib/components";
|
import { ErrorDisplay } from "$lib/components";
|
||||||
|
|
||||||
let status = $derived(page.status);
|
let status = $derived($page.status);
|
||||||
let message = $derived(page.error?.message || "An unexpected error occurred");
|
let message = $derived($page.error?.message || "An unexpected error occurred");
|
||||||
|
|
||||||
function getErrorTitle(status: number): string {
|
function getErrorTitle(status: number): string {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,11 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import "bootstrap-icons/font/bootstrap-icons.css";
|
|
||||||
import "bootstrap/dist/css/bootstrap.min.css";
|
import "bootstrap/dist/css/bootstrap.min.css";
|
||||||
|
import "bootstrap-icons/font/bootstrap-icons.css";
|
||||||
import "../app.css";
|
import "../app.css";
|
||||||
|
|
||||||
import favicon from "$lib/assets/favicon.svg";
|
import favicon from '$lib/assets/favicon.svg';
|
||||||
|
|
||||||
import Logo from "$lib/components/Logo.svelte";
|
import Logo from "$lib/components/Logo.svelte";
|
||||||
import { appConfig } from "$lib/stores/config";
|
|
||||||
import { theme } from "$lib/stores/theme";
|
import { theme } from "$lib/stores/theme";
|
||||||
import { onMount } from "svelte";
|
import { onMount } from "svelte";
|
||||||
|
|
||||||
|
|
@ -26,19 +25,15 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
<link rel="icon" href={favicon} />
|
<link rel="icon" href={favicon} />
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<div class="min-vh-100 d-flex flex-column">
|
<div class="min-vh-100 d-flex flex-column">
|
||||||
<nav class="navbar navbar-expand-lg navbar-light shadow-sm">
|
<nav class="navbar navbar-expand-lg navbar-light shadow-sm">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<a class="navbar-brand fw-bold" href="/">
|
<a class="navbar-brand fw-bold" href="/">
|
||||||
{#if $appConfig.custom_logo_url}
|
<i class="bi bi-envelope-check me-2"></i>
|
||||||
<img src={$appConfig.custom_logo_url} alt="Logo" style="height: 25px;" />
|
<Logo color={$theme === "light" ? "black" : "white"} />
|
||||||
{:else}
|
|
||||||
<i class="bi bi-envelope-check me-2"></i>
|
|
||||||
<Logo color={$theme === "light" ? "black" : "white"} />
|
|
||||||
{/if}
|
|
||||||
</a>
|
</a>
|
||||||
<div>
|
<div>
|
||||||
<span class="d-none d-md-inline navbar-text text-primary small">
|
<span class="d-none d-md-inline navbar-text text-primary small">
|
||||||
|
|
@ -60,26 +55,7 @@
|
||||||
{@render children?.()}
|
{@render children?.()}
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<footer
|
<footer class="pt-3 pb-2 bg-dark text-light">
|
||||||
id="footer-classic"
|
|
||||||
class="px-4 px-md-5 py-2 bg-tertiary d-flex justify-content-between"
|
|
||||||
>
|
|
||||||
<a href="https://happydeliver.org/" target="_blank">Powered by happyDeliver</a>
|
|
||||||
<ul class="d-flex footer-nav">
|
|
||||||
<li>
|
|
||||||
<a
|
|
||||||
href="https://git.happydomain.org/happydeliver/-/blob/master/api/openapi.yaml?ref_type=heads"
|
|
||||||
target="_blank"
|
|
||||||
>
|
|
||||||
API
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li><a href="https://git.happydomain.org/happydeliver" target="_blank">Git</a></li>
|
|
||||||
<li><a href="https://feedback.happydeliver.org/" target="_blank">Feedback</a></li>
|
|
||||||
</ul>
|
|
||||||
</footer>
|
|
||||||
|
|
||||||
<footer id="footer-happydomain" class="d-none pt-3 pb-2 bg-dark text-light">
|
|
||||||
<div class="container mb-4">
|
<div class="container mb-4">
|
||||||
<div class="row row-cols-1 row-cols-md-2 row-cols-lg-4">
|
<div class="row row-cols-1 row-cols-md-2 row-cols-lg-4">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
|
|
@ -168,27 +144,6 @@
|
||||||
border-top: 3px solid #9332bb;
|
border-top: 3px solid #9332bb;
|
||||||
}
|
}
|
||||||
|
|
||||||
footer a {
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer-nav {
|
|
||||||
list-style: none;
|
|
||||||
padding: 0;
|
|
||||||
margin: 0;
|
|
||||||
gap: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer-nav li {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer-nav li:not(:last-child)::after {
|
|
||||||
content: "|";
|
|
||||||
margin: 0 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer-links {
|
.footer-links {
|
||||||
list-style: none;
|
list-style: none;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
|
@ -200,6 +155,7 @@
|
||||||
|
|
||||||
.footer-links a {
|
.footer-links a {
|
||||||
color: rgba(255, 255, 255, 0.7);
|
color: rgba(255, 255, 255, 0.7);
|
||||||
|
text-decoration: none;
|
||||||
transition: color 0.3s;
|
transition: color 0.3s;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,8 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { goto } from "$app/navigation";
|
import { goto } from "$app/navigation";
|
||||||
|
|
||||||
import { createTest as apiCreateTest } from "$lib/api";
|
import { createTest as apiCreateTest } from "$lib/api";
|
||||||
import { FeatureCard, HowItWorksStep } from "$lib/components";
|
|
||||||
import { appConfig } from "$lib/stores/config";
|
import { appConfig } from "$lib/stores/config";
|
||||||
|
import { FeatureCard, HowItWorksStep } from "$lib/components";
|
||||||
|
|
||||||
let loading = $state(false);
|
let loading = $state(false);
|
||||||
let error = $state<string | null>(null);
|
let error = $state<string | null>(null);
|
||||||
|
|
|
||||||
|
|
@ -15,10 +15,8 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
// Basic IPv4/IPv6 validation
|
// Basic IPv4/IPv6 validation
|
||||||
const ipv4Pattern =
|
const ipv4Pattern = /^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/;
|
||||||
/^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/;
|
const ipv6Pattern = /^(([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))$/;
|
||||||
const ipv6Pattern =
|
|
||||||
/^(([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))$/;
|
|
||||||
|
|
||||||
if (!ipv4Pattern.test(ip.trim()) && !ipv6Pattern.test(ip.trim())) {
|
if (!ipv4Pattern.test(ip.trim()) && !ipv6Pattern.test(ip.trim())) {
|
||||||
error = "Please enter a valid IPv4 or IPv6 address (e.g., 192.0.2.1)";
|
error = "Please enter a valid IPv4 or IPv6 address (e.g., 192.0.2.1)";
|
||||||
|
|
@ -50,8 +48,7 @@
|
||||||
Check IP Blacklist Status
|
Check IP Blacklist Status
|
||||||
</h1>
|
</h1>
|
||||||
<p class="lead text-muted">
|
<p class="lead text-muted">
|
||||||
Test an IP address against multiple DNS-based blacklists (RBLs) to check its
|
Test an IP address against multiple DNS-based blacklists (RBLs) to check its reputation.
|
||||||
reputation.
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -106,9 +103,7 @@
|
||||||
</h3>
|
</h3>
|
||||||
<ul class="list-unstyled mb-0 small">
|
<ul class="list-unstyled mb-0 small">
|
||||||
{#each $appConfig.rbls as rbl}
|
{#each $appConfig.rbls as rbl}
|
||||||
<li class="mb-2">
|
<li class="mb-2"><i class="bi bi-arrow-right me-2"></i>{rbl}</li>
|
||||||
<i class="bi bi-arrow-right me-2"></i>{rbl}
|
|
||||||
</li>
|
|
||||||
{/each}
|
{/each}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -123,9 +118,7 @@
|
||||||
Why Check Blacklists?
|
Why Check Blacklists?
|
||||||
</h3>
|
</h3>
|
||||||
<p class="small mb-2">
|
<p class="small mb-2">
|
||||||
DNS-based blacklists (RBLs) are used by email servers to identify
|
DNS-based blacklists (RBLs) are used by email servers to identify and block spam sources. Being listed can severely impact email deliverability.
|
||||||
and block spam sources. Being listed can severely impact email
|
|
||||||
deliverability.
|
|
||||||
</p>
|
</p>
|
||||||
<p class="small mb-3">
|
<p class="small mb-3">
|
||||||
This tool checks your IP against multiple popular RBLs to help you:
|
This tool checks your IP against multiple popular RBLs to help you:
|
||||||
|
|
@ -135,8 +128,7 @@
|
||||||
<i class="bi bi-arrow-right me-2"></i>Monitor IP reputation
|
<i class="bi bi-arrow-right me-2"></i>Monitor IP reputation
|
||||||
</li>
|
</li>
|
||||||
<li class="mb-1">
|
<li class="mb-1">
|
||||||
<i class="bi bi-arrow-right me-2"></i>Identify deliverability
|
<i class="bi bi-arrow-right me-2"></i>Identify deliverability issues
|
||||||
issues
|
|
||||||
</li>
|
</li>
|
||||||
<li class="mb-1">
|
<li class="mb-1">
|
||||||
<i class="bi bi-arrow-right me-2"></i>Take corrective action
|
<i class="bi bi-arrow-right me-2"></i>Take corrective action
|
||||||
|
|
@ -154,8 +146,7 @@
|
||||||
Need Complete Email Analysis?
|
Need Complete Email Analysis?
|
||||||
</h3>
|
</h3>
|
||||||
<p class="small mb-2">
|
<p class="small mb-2">
|
||||||
For comprehensive deliverability testing including DKIM verification, content
|
For comprehensive deliverability testing including DKIM verification, content analysis, spam scoring, and more:
|
||||||
analysis, spam scoring, and more:
|
|
||||||
</p>
|
</p>
|
||||||
<a href="/" class="btn btn-sm btn-outline-primary">
|
<a href="/" class="btn btn-sm btn-outline-primary">
|
||||||
<i class="bi bi-envelope-plus me-1"></i>
|
<i class="bi bi-envelope-plus me-1"></i>
|
||||||
|
|
@ -168,9 +159,7 @@
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.card {
|
.card {
|
||||||
transition:
|
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
||||||
transform 0.2s ease,
|
|
||||||
box-shadow 0.2s ease;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.card:hover {
|
.card:hover {
|
||||||
|
|
|
||||||
|
|
@ -80,8 +80,7 @@
|
||||||
<!-- Error State -->
|
<!-- Error State -->
|
||||||
<div class="card shadow-sm">
|
<div class="card shadow-sm">
|
||||||
<div class="card-body text-center py-5">
|
<div class="card-body text-center py-5">
|
||||||
<i class="bi bi-exclamation-triangle text-danger" style="font-size: 4rem;"
|
<i class="bi bi-exclamation-triangle text-danger" style="font-size: 4rem;"></i>
|
||||||
></i>
|
|
||||||
<h3 class="h4 mt-4">Check Failed</h3>
|
<h3 class="h4 mt-4">Check Failed</h3>
|
||||||
<p class="text-muted mb-4">{error}</p>
|
<p class="text-muted mb-4">{error}</p>
|
||||||
<button class="btn btn-primary" onclick={analyzeIP}>
|
<button class="btn btn-primary" onclick={analyzeIP}>
|
||||||
|
|
@ -99,33 +98,22 @@
|
||||||
<div class="row align-items-center">
|
<div class="row align-items-center">
|
||||||
<div class="col-md-6 text-center text-md-start mb-3 mb-md-0">
|
<div class="col-md-6 text-center text-md-start mb-3 mb-md-0">
|
||||||
<h2 class="h2 mb-2">
|
<h2 class="h2 mb-2">
|
||||||
<span class="font-monospace text-truncate">{result.ip}</span
|
<span class="font-monospace text-truncate">{result.ip}</span>
|
||||||
>
|
|
||||||
</h2>
|
</h2>
|
||||||
{#if result.listed_count === 0}
|
{#if result.listed_count === 0}
|
||||||
<div class="alert alert-success mb-0 d-inline-block">
|
<div class="alert alert-success mb-0 d-inline-block">
|
||||||
<i class="bi bi-check-circle me-2"></i>
|
<i class="bi bi-check-circle me-2"></i>
|
||||||
<strong>Not Listed</strong>
|
<strong>Not Listed</strong>
|
||||||
<p class="d-inline mb-0 mt-1 small">
|
<p class="d-inline mb-0 mt-1 small">
|
||||||
This IP address is not listed on any checked
|
This IP address is not listed on any checked blacklists.
|
||||||
blacklists.
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="alert alert-danger mb-0 d-inline-block">
|
<div class="alert alert-danger mb-0 d-inline-block">
|
||||||
<i class="bi bi-exclamation-triangle me-2"></i>
|
<i class="bi bi-exclamation-triangle me-2"></i>
|
||||||
<strong
|
<strong>Listed on {result.listed_count} blacklist{result.listed_count > 1 ? "s" : ""}</strong>
|
||||||
>Listed on {result.listed_count} blacklist{result.listed_count >
|
|
||||||
1
|
|
||||||
? "s"
|
|
||||||
: ""}</strong
|
|
||||||
>
|
|
||||||
<p class="mb-0 mt-1 small">
|
<p class="mb-0 mt-1 small">
|
||||||
This IP address is listed on {result.listed_count} of
|
This IP address is listed on {result.listed_count} of {result.checks.length} checked blacklist{result.checks.length > 1 ? "s" : ""}.
|
||||||
{result.checks.length} checked blacklist{result
|
|
||||||
.checks.length > 1
|
|
||||||
? "s"
|
|
||||||
: ""}.
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
@ -133,8 +121,8 @@
|
||||||
<div class="offset-md-3 col-md-3 text-center">
|
<div class="offset-md-3 col-md-3 text-center">
|
||||||
<div
|
<div
|
||||||
class="p-2 rounded text-center summary-card"
|
class="p-2 rounded text-center summary-card"
|
||||||
class:bg-light={$theme === "light"}
|
class:bg-light={$theme === 'light'}
|
||||||
class:bg-secondary={$theme !== "light"}
|
class:bg-secondary={$theme !== 'light'}
|
||||||
>
|
>
|
||||||
<GradeDisplay score={result.score} grade={result.grade} />
|
<GradeDisplay score={result.score} grade={result.grade} />
|
||||||
<small class="text-muted d-block">Blacklist Score</small>
|
<small class="text-muted d-block">Blacklist Score</small>
|
||||||
|
|
@ -166,36 +154,23 @@
|
||||||
</h3>
|
</h3>
|
||||||
{#if result.listed_count === 0}
|
{#if result.listed_count === 0}
|
||||||
<p class="mb-3">
|
<p class="mb-3">
|
||||||
<strong>Good news!</strong> This IP address is not currently listed
|
<strong>Good news!</strong> This IP address is not currently listed on any of the
|
||||||
on any of the checked DNS-based blacklists (RBLs). This indicates
|
checked DNS-based blacklists (RBLs). This indicates a good sender reputation
|
||||||
a good sender reputation and should not negatively impact email deliverability.
|
and should not negatively impact email deliverability.
|
||||||
</p>
|
</p>
|
||||||
{:else}
|
{:else}
|
||||||
<p class="mb-3">
|
<p class="mb-3">
|
||||||
<strong>Warning:</strong> This IP address is listed on {result.listed_count}
|
<strong>Warning:</strong> This IP address is listed on {result.listed_count} blacklist{result.listed_count > 1 ? "s" : ""}.
|
||||||
blacklist{result.listed_count > 1 ? "s" : ""}. Being listed can
|
Being listed can significantly impact email deliverability as many mail servers
|
||||||
significantly impact email deliverability as many mail servers
|
|
||||||
use these blacklists to filter incoming mail.
|
use these blacklists to filter incoming mail.
|
||||||
</p>
|
</p>
|
||||||
<div class="alert alert-warning">
|
<div class="alert alert-warning">
|
||||||
<h4 class="h6 mb-2">Recommended Actions:</h4>
|
<h4 class="h6 mb-2">Recommended Actions:</h4>
|
||||||
<ul class="mb-0 small">
|
<ul class="mb-0 small">
|
||||||
<li>
|
<li>Investigate the cause of the listing (compromised system, spam complaints, etc.)</li>
|
||||||
Investigate the cause of the listing (compromised
|
<li>Fix any security issues or stop sending practices that led to the listing</li>
|
||||||
system, spam complaints, etc.)
|
<li>Request delisting from each RBL (check their websites for removal procedures)</li>
|
||||||
</li>
|
<li>Monitor your IP reputation regularly to prevent future listings</li>
|
||||||
<li>
|
|
||||||
Fix any security issues or stop sending practices that
|
|
||||||
led to the listing
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
Request delisting from each RBL (check their websites
|
|
||||||
for removal procedures)
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
Monitor your IP reputation regularly to prevent future
|
|
||||||
listings
|
|
||||||
</li>
|
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
@ -211,8 +186,8 @@
|
||||||
</h3>
|
</h3>
|
||||||
<p class="mb-3">
|
<p class="mb-3">
|
||||||
This blacklist check tests IP reputation only. For comprehensive
|
This blacklist check tests IP reputation only. For comprehensive
|
||||||
deliverability testing including DKIM verification, content
|
deliverability testing including DKIM verification, content analysis,
|
||||||
analysis, spam scoring, and DNS configuration:
|
spam scoring, and DNS configuration:
|
||||||
</p>
|
</p>
|
||||||
<a href="/" class="btn btn-primary">
|
<a href="/" class="btn btn-primary">
|
||||||
<i class="bi bi-envelope-plus me-2"></i>
|
<i class="bi bi-envelope-plus me-2"></i>
|
||||||
|
|
|
||||||
|
|
@ -13,8 +13,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
// Basic domain validation
|
// Basic domain validation
|
||||||
const domainPattern =
|
const domainPattern = /^[a-zA-Z0-9][a-zA-Z0-9-]{0,61}[a-zA-Z0-9]?(\.[a-zA-Z0-9][a-zA-Z0-9-]{0,61}[a-zA-Z0-9]?)*$/;
|
||||||
/^[a-zA-Z0-9][a-zA-Z0-9-]{0,61}[a-zA-Z0-9]?(\.[a-zA-Z0-9][a-zA-Z0-9-]{0,61}[a-zA-Z0-9]?)*$/;
|
|
||||||
if (!domainPattern.test(domain.trim())) {
|
if (!domainPattern.test(domain.trim())) {
|
||||||
error = "Please enter a valid domain name (e.g., example.com)";
|
error = "Please enter a valid domain name (e.g., example.com)";
|
||||||
return;
|
return;
|
||||||
|
|
@ -100,18 +99,10 @@
|
||||||
What's Checked
|
What's Checked
|
||||||
</h3>
|
</h3>
|
||||||
<ul class="list-unstyled mb-0 small">
|
<ul class="list-unstyled mb-0 small">
|
||||||
<li class="mb-2">
|
<li class="mb-2"><i class="bi bi-arrow-right me-2"></i>MX Records</li>
|
||||||
<i class="bi bi-arrow-right me-2"></i>MX Records
|
<li class="mb-2"><i class="bi bi-arrow-right me-2"></i>SPF Records</li>
|
||||||
</li>
|
<li class="mb-2"><i class="bi bi-arrow-right me-2"></i>DMARC Policy</li>
|
||||||
<li class="mb-2">
|
<li class="mb-2"><i class="bi bi-arrow-right me-2"></i>BIMI Support</li>
|
||||||
<i class="bi bi-arrow-right me-2"></i>SPF Records
|
|
||||||
</li>
|
|
||||||
<li class="mb-2">
|
|
||||||
<i class="bi bi-arrow-right me-2"></i>DMARC Policy
|
|
||||||
</li>
|
|
||||||
<li class="mb-2">
|
|
||||||
<i class="bi bi-arrow-right me-2"></i>BIMI Support
|
|
||||||
</li>
|
|
||||||
<li class="mb-0">
|
<li class="mb-0">
|
||||||
<i class="bi bi-arrow-right me-2"></i>Disposable Domain Check
|
<i class="bi bi-arrow-right me-2"></i>Disposable Domain Check
|
||||||
</li>
|
</li>
|
||||||
|
|
@ -158,9 +149,7 @@
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.card {
|
.card {
|
||||||
transition:
|
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
||||||
transform 0.2s ease,
|
|
||||||
box-shadow 0.2s ease;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.card:hover {
|
.card:hover {
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,12 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { page } from "$app/state";
|
import { page } from "$app/stores";
|
||||||
import { onMount } from "svelte";
|
import { onMount } from "svelte";
|
||||||
|
|
||||||
import { testDomain } from "$lib/api";
|
import { testDomain } from "$lib/api";
|
||||||
import type { DomainTestResponse } from "$lib/api/types.gen";
|
import type { DomainTestResponse } from "$lib/api/types.gen";
|
||||||
import { DnsRecordsCard, GradeDisplay, TinySurvey } from "$lib/components";
|
import { DnsRecordsCard, GradeDisplay, TinySurvey } from "$lib/components";
|
||||||
import { theme } from "$lib/stores/theme";
|
import { theme } from "$lib/stores/theme";
|
||||||
|
|
||||||
let domain = $derived(page.params.domain);
|
let domain = $derived($page.params.domain);
|
||||||
let loading = $state(true);
|
let loading = $state(true);
|
||||||
let error = $state<string | null>(null);
|
let error = $state<string | null>(null);
|
||||||
let result = $state<DomainTestResponse | null>(null);
|
let result = $state<DomainTestResponse | null>(null);
|
||||||
|
|
@ -81,8 +80,7 @@
|
||||||
<!-- Error State -->
|
<!-- Error State -->
|
||||||
<div class="card shadow-sm">
|
<div class="card shadow-sm">
|
||||||
<div class="card-body text-center py-5">
|
<div class="card-body text-center py-5">
|
||||||
<i class="bi bi-exclamation-triangle text-danger" style="font-size: 4rem;"
|
<i class="bi bi-exclamation-triangle text-danger" style="font-size: 4rem;"></i>
|
||||||
></i>
|
|
||||||
<h3 class="h4 mt-4">Analysis Failed</h3>
|
<h3 class="h4 mt-4">Analysis Failed</h3>
|
||||||
<p class="text-muted mb-4">{error}</p>
|
<p class="text-muted mb-4">{error}</p>
|
||||||
<button class="btn btn-primary" onclick={analyzeDomain}>
|
<button class="btn btn-primary" onclick={analyzeDomain}>
|
||||||
|
|
@ -107,9 +105,8 @@
|
||||||
<i class="bi bi-exclamation-triangle me-2"></i>
|
<i class="bi bi-exclamation-triangle me-2"></i>
|
||||||
<strong>Disposable Email Provider Detected</strong>
|
<strong>Disposable Email Provider Detected</strong>
|
||||||
<p class="mb-0 mt-1 small">
|
<p class="mb-0 mt-1 small">
|
||||||
This domain is a known temporary/disposable email
|
This domain is a known temporary/disposable email service.
|
||||||
service. Emails from this domain may have lower
|
Emails from this domain may have lower deliverability.
|
||||||
deliverability.
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
|
|
@ -119,8 +116,8 @@
|
||||||
<div class="offset-md-3 col-md-3 text-center">
|
<div class="offset-md-3 col-md-3 text-center">
|
||||||
<div
|
<div
|
||||||
class="p-2 rounded text-center summary-card"
|
class="p-2 rounded text-center summary-card"
|
||||||
class:bg-light={$theme === "light"}
|
class:bg-light={$theme === 'light'}
|
||||||
class:bg-secondary={$theme !== "light"}
|
class:bg-secondary={$theme !== 'light'}
|
||||||
>
|
>
|
||||||
<GradeDisplay score={result.score} grade={result.grade} />
|
<GradeDisplay score={result.score} grade={result.grade} />
|
||||||
<small class="text-muted d-block">DNS</small>
|
<small class="text-muted d-block">DNS</small>
|
||||||
|
|
@ -153,8 +150,8 @@
|
||||||
</h3>
|
</h3>
|
||||||
<p class="mb-3">
|
<p class="mb-3">
|
||||||
This domain-only test checks DNS configuration. For comprehensive
|
This domain-only test checks DNS configuration. For comprehensive
|
||||||
deliverability testing including DKIM verification, content
|
deliverability testing including DKIM verification, content analysis,
|
||||||
analysis, spam scoring, and blacklist checks:
|
spam scoring, and blacklist checks:
|
||||||
</p>
|
</p>
|
||||||
<a href="/" class="btn btn-primary">
|
<a href="/" class="btn btn-primary">
|
||||||
<i class="bi bi-envelope-plus me-2"></i>
|
<i class="bi bi-envelope-plus me-2"></i>
|
||||||
|
|
|
||||||
|
|
@ -1,21 +1,20 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { page } from "$app/state";
|
|
||||||
import { onDestroy } from "svelte";
|
import { onDestroy } from "svelte";
|
||||||
|
import { page } from "$app/state";
|
||||||
import { getReport, getTest, reanalyzeReport } from "$lib/api";
|
import { getTest, getReport, reanalyzeReport } from "$lib/api";
|
||||||
import type { Report, Test } from "$lib/api/types.gen";
|
import type { Test, Report } from "$lib/api/types.gen";
|
||||||
import {
|
import {
|
||||||
|
ScoreCard,
|
||||||
|
SummaryCard,
|
||||||
|
SpamAssassinCard,
|
||||||
|
PendingState,
|
||||||
AuthenticationCard,
|
AuthenticationCard,
|
||||||
|
DnsRecordsCard,
|
||||||
BlacklistCard,
|
BlacklistCard,
|
||||||
ContentAnalysisCard,
|
ContentAnalysisCard,
|
||||||
DnsRecordsCard,
|
|
||||||
ErrorDisplay,
|
|
||||||
HeaderAnalysisCard,
|
HeaderAnalysisCard,
|
||||||
PendingState,
|
|
||||||
ScoreCard,
|
|
||||||
SpamAssassinCard,
|
|
||||||
SummaryCard,
|
|
||||||
TinySurvey,
|
TinySurvey,
|
||||||
|
ErrorDisplay,
|
||||||
} from "$lib/components";
|
} from "$lib/components";
|
||||||
|
|
||||||
let testId = $derived(page.params.test);
|
let testId = $derived(page.params.test);
|
||||||
|
|
@ -189,13 +188,7 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
<title>
|
<title>{report ? `Test${report.dns_results ? ` of ${report.dns_results.from_domain}` : ''} ${report.test_id?.slice(0, 7) || ''}` : (test ? `Test ${test.id.slice(0, 7)}` : "Loading...")} - happyDeliver</title>
|
||||||
{report
|
|
||||||
? `Test${report.dns_results ? ` of ${report.dns_results.from_domain}` : ""} ${report.test_id?.slice(0, 7) || ""}`
|
|
||||||
: test
|
|
||||||
? `Test ${test.id.slice(0, 7)}`
|
|
||||||
: "Loading..."} - happyDeliver
|
|
||||||
</title>
|
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<div class="container py-5">
|
<div class="container py-5">
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue