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

Merge branch 'release/v1.13.0'

This commit is contained in:
Ralph Slooten 2024-01-21 14:32:15 +13:00
commit 7f31fb716a
27 changed files with 1622 additions and 308 deletions

@ -2,6 +2,23 @@
Notable changes to Mailpit will be documented in this file.
## [v1.13.0]
### Chore
- Compress compiled assets with `npm run build`
- Update Go modules
- Update node modules
### Feature
- Add option to disable SMTP reverse DNS (rDNS) lookup ([#230](https://github.com/axllent/mailpit/issues/230))
- Display List-Unsubscribe & List-Unsubscribe-Post header info with syntax validation ([#236](https://github.com/axllent/mailpit/issues/236))
- Add optional SpamAssassin integration to display scores ([#233](https://github.com/axllent/mailpit/issues/233))
### Fix
- Display multiple whitespace characters in message subject & recipient names ([#238](https://github.com/axllent/mailpit/issues/238))
- Sendmail support for `-f 'Name <email[@example](https://github.com/example).com>'` format
## [v1.12.1]
### Chore

@ -38,7 +38,9 @@ including image thumbnails), including optional [HTTPS](https://mailpit.axllent.
- Optional [basic authentication](https://mailpit.axllent.org/docs/configuration/frontend-authentication/) for web UI & API
- [HTML check](https://mailpit.axllent.org/docs/usage/html-check/) to test & score mail client compatibility with HTML emails
- [Link check](https://mailpit.axllent.org/docs/usage/link-check/) to test message links (HTML & text) & linked images
- [Spam check](https://mailpit.axllent.org/docs/usage/spamassassin/) to test message "spamminess" using a running SpamAssassin server
- [Create screenshots](https://mailpit.axllent.org/docs/usage/html-screenshots/) of HTML messages via web UI
- `List-Unsubscribe` syntax validation
- Mobile and tablet HTML preview toggle in desktop mode
- Advanced [mail search](https://mailpit.axllent.org/docs/usage/search-filters/)
- [Message tagging](https://mailpit.axllent.org/docs/usage/tagging/)

@ -91,6 +91,7 @@ func init() {
rootCmd.Flags().BoolVar(&config.IgnoreDuplicateIDs, "ignore-duplicate-ids", config.IgnoreDuplicateIDs, "Ignore duplicate messages (by Message-Id)")
rootCmd.Flags().BoolVar(&config.DisableHTMLCheck, "disable-html-check", config.DisableHTMLCheck, "Disable the HTML check functionality (web UI & API)")
rootCmd.Flags().BoolVar(&config.BlockRemoteCSSAndFonts, "block-remote-css-and-fonts", config.BlockRemoteCSSAndFonts, "Block access to remote CSS & fonts")
rootCmd.Flags().StringVar(&config.EnableSpamAssassin, "enable-spamassassin", config.EnableSpamAssassin, "Enable integration with SpamAssassin")
rootCmd.Flags().StringVar(&config.UIAuthFile, "ui-auth-file", config.UIAuthFile, "A password file for web UI & API authentication")
rootCmd.Flags().StringVar(&config.UITLSCert, "ui-tls-cert", config.UITLSCert, "TLS certificate for web UI (HTTPS) - requires ui-tls-key")
@ -104,6 +105,7 @@ func init() {
rootCmd.Flags().BoolVar(&config.SMTPStrictRFCHeaders, "smtp-strict-rfc-headers", config.SMTPStrictRFCHeaders, "Return SMTP error if message headers contain <CR><CR><LF>")
rootCmd.Flags().IntVar(&config.SMTPMaxRecipients, "smtp-max-recipients", config.SMTPMaxRecipients, "Maximum SMTP recipients allowed")
rootCmd.Flags().StringVar(&config.SMTPAllowedRecipients, "smtp-allowed-recipients", config.SMTPAllowedRecipients, "Only allow SMTP recipients matching a regular expression (default allow all)")
rootCmd.Flags().BoolVar(&smtpd.DisableReverseDNS, "smtp-disable-rdns", smtpd.DisableReverseDNS, "Disable SMTP reverse DNS lookups")
rootCmd.Flags().StringVar(&config.SMTPRelayConfigFile, "smtp-relay-config", config.SMTPRelayConfigFile, "SMTP configuration file to allow releasing messages")
rootCmd.Flags().BoolVar(&config.SMTPRelayAllIncoming, "smtp-relay-all", config.SMTPRelayAllIncoming, "Relay all incoming messages via external SMTP server (caution!)")
@ -174,6 +176,9 @@ func initConfigFromEnv() {
if len(os.Getenv("MP_SMTP_ALLOWED_RECIPIENTS")) > 0 {
config.SMTPAllowedRecipients = os.Getenv("MP_SMTP_ALLOWED_RECIPIENTS")
}
if getEnabledFromEnv("MP_SMTP_DISABLE_RDNS") {
smtpd.DisableReverseDNS = true
}
// Relay server config
config.SMTPRelayConfigFile = os.Getenv("MP_SMTP_RELAY_CONFIG")
@ -208,6 +213,9 @@ func initConfigFromEnv() {
if getEnabledFromEnv("MP_BLOCK_REMOTE_CSS_AND_FONTS") {
config.BlockRemoteCSSAndFonts = true
}
if len(os.Getenv("MP_ENABLE_SPAMASSASSIN")) > 0 {
config.EnableSpamAssassin = os.Getenv("MP_ENABLE_SPAMASSASSIN")
}
if getEnabledFromEnv("MP_ALLOW_UNTRUSTED_TLS") {
config.AllowUntrustedTLS = true
}

@ -13,6 +13,7 @@ import (
"github.com/axllent/mailpit/internal/auth"
"github.com/axllent/mailpit/internal/logger"
"github.com/axllent/mailpit/internal/spamassassin"
"github.com/axllent/mailpit/internal/tools"
"gopkg.in/yaml.v3"
)
@ -106,6 +107,9 @@ var (
// Use with extreme caution!
SMTPRelayAllIncoming = false
// EnableSpamAssassin must be either <host>:<port> or "postmark"
EnableSpamAssassin string
// WebhookURL for calling
WebhookURL string
@ -245,6 +249,16 @@ func VerifyConfig() error {
return fmt.Errorf("Webhook URL does not appear to be a valid URL (%s)", WebhookURL)
}
if EnableSpamAssassin != "" {
spamassassin.SetService(EnableSpamAssassin)
logger.Log().Infof("[spamassassin] enabled via %s", EnableSpamAssassin)
if err := spamassassin.Ping(); err != nil {
logger.Log().Warnf("[spamassassin] ping: %s", err.Error())
} else {
}
}
SMTPTags = []AutoTag{}
if SMTPCLITags != "" {

14
go.mod

@ -14,14 +14,14 @@ require (
github.com/jhillyerd/enmime v1.1.0
github.com/klauspost/compress v1.17.4
github.com/leporo/sqlf v1.4.0
github.com/mhale/smtpd v0.8.1
github.com/mhale/smtpd v0.8.2
github.com/reiver/go-telnet v0.0.0-20180421082511-9ff0b2ab096e
github.com/sirupsen/logrus v1.9.3
github.com/spf13/cobra v1.8.0
github.com/spf13/pflag v1.0.5
github.com/tg123/go-htpasswd v1.2.2
github.com/vanng822/go-premailer v1.20.2
golang.org/x/net v0.19.0
golang.org/x/net v0.20.0
golang.org/x/text v0.14.0
golang.org/x/time v0.5.0
gopkg.in/yaml.v3 v3.0.1
@ -51,16 +51,16 @@ require (
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/vanng822/css v1.0.1 // indirect
golang.org/x/crypto v0.17.0 // indirect
golang.org/x/image v0.14.0 // indirect
golang.org/x/crypto v0.18.0 // indirect
golang.org/x/image v0.15.0 // indirect
golang.org/x/mod v0.14.0 // indirect
golang.org/x/sys v0.15.0 // indirect
golang.org/x/tools v0.16.1 // indirect
golang.org/x/sys v0.16.0 // indirect
golang.org/x/tools v0.17.0 // indirect
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
lukechampine.com/uint128 v1.3.0 // indirect
modernc.org/cc/v3 v3.41.0 // indirect
modernc.org/ccgo/v3 v3.16.15 // indirect
modernc.org/libc v1.38.0 // indirect
modernc.org/libc v1.40.6 // indirect
modernc.org/mathutil v1.6.0 // indirect
modernc.org/memory v1.7.2 // indirect
modernc.org/opt v0.1.3 // indirect

30
go.sum

@ -90,8 +90,8 @@ github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZ
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y=
github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/mhale/smtpd v0.8.1 h1:O02u8O3eYAGxZCGf4E98WjyB+rA3DVFZtchEialjX4s=
github.com/mhale/smtpd v0.8.1/go.mod h1:MQl+y2hwIEQCXtNhe5+55n0GZOjSmeqORDIXbqUL3x4=
github.com/mhale/smtpd v0.8.2 h1:rHKOMHeFoDvcq8Na9ErCbNcjlWTSyGtznOmJpWsOzuc=
github.com/mhale/smtpd v0.8.2/go.mod h1:MQl+y2hwIEQCXtNhe5+55n0GZOjSmeqORDIXbqUL3x4=
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
@ -141,11 +141,11 @@ github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5t
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k=
golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc=
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.14.0 h1:tNgSxAFe3jC4uYqvZdTr84SZoM1KfwdC9SKIFrLjFn4=
golang.org/x/image v0.14.0/go.mod h1:HUYqC05R2ZcZ3ejNQsIHQDQiwWM4JBqmm6MKANTp4LE=
golang.org/x/image v0.15.0 h1:kOELfmgrmJlw4Cdb7g/QGuB3CvDrXbqEIww/pNtNBm8=
golang.org/x/image v0.15.0/go.mod h1:HUYqC05R2ZcZ3ejNQsIHQDQiwWM4JBqmm6MKANTp4LE=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0=
@ -161,12 +161,12 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c=
golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U=
golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo=
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE=
golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@ -179,8 +179,8 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
@ -199,8 +199,8 @@ golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGm
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.16.1 h1:TLyB3WofjdOEepBHAU20JdNC1Zbg87elYofWYAY5oZA=
golang.org/x/tools v0.16.1/go.mod h1:kYVVN6I1mBNoB1OX+noeBjbRk4IUEPa7JJ+TJMEooJ0=
golang.org/x/tools v0.17.0 h1:FvmRgNOcs3kOa+T20R1uhfP9F6HgG2mfxDv1vrx1Htc=
golang.org/x/tools v0.17.0/go.mod h1:xsh6VxdV005rRVaS6SSAf9oiAqljS7UZUacMZ8Bnsps=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
@ -218,8 +218,8 @@ modernc.org/ccgo/v3 v3.16.15 h1:KbDR3ZAVU+wiLyMESPtbtE/Add4elztFyfsWoNTgxS0=
modernc.org/ccgo/v3 v3.16.15/go.mod h1:yT7B+/E2m43tmMOT51GMoM98/MtHIcQQSleGnddkUNI=
modernc.org/ccorpus v1.11.6 h1:J16RXiiqiCgua6+ZvQot4yUuUy8zxgqbqEEUuGPlISk=
modernc.org/httpfs v1.0.6 h1:AAgIpFZRXuYnkjftxTAZwMIiwEqAfk8aVB2/oA6nAeM=
modernc.org/libc v1.38.0 h1:o4Lpk0zNDSdsjfEXnF1FGXWQ9PDi1NOdWcLP5n13FGo=
modernc.org/libc v1.38.0/go.mod h1:YAXkAZ8ktnkCKaN9sw/UDeUVkGYJ/YquGO4FTi5nmHE=
modernc.org/libc v1.40.6 h1:141JHq3SjhOOCjECBgD4K8VgTFOy19CnHwroC08DAig=
modernc.org/libc v1.40.6/go.mod h1:YAXkAZ8ktnkCKaN9sw/UDeUVkGYJ/YquGO4FTi5nmHE=
modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4=
modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo=
modernc.org/memory v1.7.2 h1:Klh90S215mmH8c9gO98QxQFsY+W451E8AnzjoE2ee1E=

@ -0,0 +1,100 @@
// Package postmark uses the free https://spamcheck.postmarkapp.com/
// See https://spamcheck.postmarkapp.com/doc/ for more details.
package postmark
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"regexp"
"strings"
"time"
)
// Response struct
type Response struct {
Success bool `json:"success"`
Message string `json:"message"` // for errors only
Score string `json:"score"`
Rules []Rule `json:"rules"`
Report string `json:"report"` // ignored
}
// Rule struct
type Rule struct {
Score string `json:"score"`
// Name not returned by postmark but rather extracted from description
Name string `json:"name"`
Description string `json:"description"`
}
// Check will post the email data to Postmark
func Check(email []byte, timeout int) (Response, error) {
r := Response{}
// '{"email":"raw dump of email", "options":"short"}'
var d struct {
// The raw dump of the email to be filtered, including all headers.
Email string `json:"email"`
// Default "long". Must either be "long" for a full report of processing rules, or "short" for a score request.
Options string `json:"options"`
}
d.Email = string(email)
d.Options = "long"
data, err := json.Marshal(d)
if err != nil {
return r, err
}
client := http.Client{
Timeout: time.Duration(timeout) * time.Second,
}
resp, err := client.Post("https://spamcheck.postmarkapp.com/filter", "application/json",
bytes.NewBuffer(data))
if err != nil {
return r, err
}
defer resp.Body.Close()
err = json.NewDecoder(resp.Body).Decode(&r)
// remove trailing line spaces for all lines in report
re := regexp.MustCompile("\r?\n")
lines := re.Split(r.Report, -1)
reportLines := []string{}
for _, l := range lines {
line := strings.TrimRight(l, " ")
reportLines = append(reportLines, line)
}
reportRaw := strings.Join(reportLines, "\n")
// join description lines to make a single line per rule
re2 := regexp.MustCompile("\n ")
report := re2.ReplaceAllString(reportRaw, "")
for i, rule := range r.Rules {
// populate rule name
r.Rules[i].Name = nameFromReport(rule.Score, rule.Description, report)
}
return r, err
}
// Extract the name of the test from the report as Postmark does not include this in the JSON reports
func nameFromReport(score, description, report string) string {
score = regexp.QuoteMeta(score)
description = regexp.QuoteMeta(description)
str := fmt.Sprintf("%s\\s+([A-Z0-9\\_]+)\\s+%s", score, description)
re := regexp.MustCompile(str)
matches := re.FindAllStringSubmatch(report, 1)
if len(matches) > 0 && len(matches[0]) == 2 {
return strings.TrimSpace(matches[0][1])
}
return ""
}

@ -0,0 +1,147 @@
// Package spamassassin will return results from either a SpamAssassin server or
// Postmark's public API depending on configuration
package spamassassin
import (
"errors"
"math"
"strconv"
"strings"
"github.com/axllent/mailpit/internal/spamassassin/postmark"
"github.com/axllent/mailpit/internal/spamassassin/spamc"
)
var (
// Service to use, either "<host>:<ip>" for self-hosted SpamAssassin or "postmark"
service string
// SpamScore is the score at which a message is determined to be spam
spamScore = 5.0
// Timeout in seconds
timeout = 8
)
// Result is a SpamAssassin result
//
// swagger:model SpamAssassinResponse
type Result struct {
// Whether the message is spam or not
IsSpam bool
// If populated will return an error string
Error string
// Total spam score based on triggered rules
Score float64
// Spam rules triggered
Rules []Rule
}
// Rule struct
type Rule struct {
// Spam rule score
Score float64
// SpamAssassin rule name
Name string
// SpamAssassin rule description
Description string
}
// SetService defines which service should be used.
func SetService(s string) {
switch s {
case "postmark":
service = "postmark"
default:
service = s
}
}
// SetTimeout defines the timeout
func SetTimeout(t int) {
if t > 0 {
timeout = t
}
}
// Ping returns whether a service is active or not
func Ping() error {
if service == "postmark" {
return nil
}
var client *spamc.Client
if strings.HasPrefix("unix:", service) {
client = spamc.NewUnix(strings.TrimLeft(service, "unix:"))
} else {
client = spamc.NewTCP(service, timeout)
}
return client.Ping()
}
// Check will return a Result
func Check(msg []byte) (Result, error) {
r := Result{Score: 0}
if service == "" {
return r, errors.New("no SpamAssassin service defined")
}
if service == "postmark" {
res, err := postmark.Check(msg, timeout)
if err != nil {
r.Error = err.Error()
return r, nil
}
resFloat, err := strconv.ParseFloat(res.Score, 32)
if err == nil {
r.Score = round1dm(resFloat)
r.IsSpam = resFloat >= spamScore
}
r.Error = res.Message
for _, pr := range res.Rules {
rule := Rule{}
value, err := strconv.ParseFloat(pr.Score, 32)
if err == nil {
rule.Score = round1dm(value)
}
rule.Name = pr.Name
rule.Description = pr.Description
r.Rules = append(r.Rules, rule)
}
} else {
var client *spamc.Client
if strings.HasPrefix("unix:", service) {
client = spamc.NewUnix(strings.TrimLeft(service, "unix:"))
} else {
client = spamc.NewTCP(service, timeout)
}
res, err := client.Report(msg)
if err != nil {
r.Error = err.Error()
return r, nil
}
r.IsSpam = res.Score >= spamScore
r.Score = round1dm(res.Score)
r.Rules = []Rule{}
for _, sr := range res.Rules {
rule := Rule{}
value, err := strconv.ParseFloat(sr.Points, 32)
if err == nil {
rule.Score = round1dm(value)
}
rule.Name = sr.Name
rule.Description = sr.Description
r.Rules = append(r.Rules, rule)
}
}
return r, nil
}
// Round to one decimal place
func round1dm(n float64) float64 {
return math.Floor(n*10) / 10
}

@ -0,0 +1,245 @@
// Package spamc provides a client for the SpamAssassin spamd protocol.
// http://svn.apache.org/repos/asf/spamassassin/trunk/spamd/PROTOCOL
//
// Modified to add timeouts from https://github.com/cgt/spamc
package spamc
import (
"bufio"
"fmt"
"io"
"net"
"regexp"
"strconv"
"strings"
"time"
)
// ProtoVersion is the protocol version
const ProtoVersion = "1.5"
var (
spamInfoRe = regexp.MustCompile(`(.+)\/(.+) (\d+) (.+)`)
spamMainRe = regexp.MustCompile(`^Spam: (.+) ; (.+) . (.+)$`)
spamDetailsRe = regexp.MustCompile(`^\s?(-?[0-9\.]+)\s([a-zA-Z0-9_]*)(\W*)(.*)`)
)
// connection is like net.Conn except that it also has a CloseWrite method.
// CloseWrite is implemented by net.TCPConn and net.UnixConn, but for some
// reason it is not present in the net.Conn interface.
type connection interface {
net.Conn
CloseWrite() error
}
// Client is a spamd client.
type Client struct {
net string
addr string
timeout int
}
// NewTCP returns a *Client that connects to spamd via the given TCP address.
func NewTCP(addr string, timeout int) *Client {
return &Client{"tcp", addr, timeout}
}
// NewUnix returns a *Client that connects to spamd via the given Unix socket.
func NewUnix(addr string) *Client {
return &Client{"unix", addr, 0}
}
// Rule represents a matched SpamAssassin rule.
type Rule struct {
Points string
Name string
Description string
}
// Result struct
type Result struct {
ResponseCode int
Message string
Spam bool
Score float64
Threshold float64
Rules []Rule
}
// dial connects to spamd through TCP or a Unix socket.
func (c *Client) dial() (connection, error) {
if c.net == "tcp" {
tcpAddr, err := net.ResolveTCPAddr("tcp", c.addr)
if err != nil {
return nil, err
}
return net.DialTCP("tcp", nil, tcpAddr)
} else if c.net == "unix" {
unixAddr, err := net.ResolveUnixAddr("unix", c.addr)
if err != nil {
return nil, err
}
return net.DialUnix("unix", nil, unixAddr)
}
panic("Client.net must be either \"tcp\" or \"unix\"")
}
// Report checks if message is spam or not, and returns score plus report
func (c *Client) Report(email []byte) (Result, error) {
output, err := c.report(email)
if err != nil {
return Result{}, err
}
return c.parseOutput(output), nil
}
func (c *Client) report(email []byte) ([]string, error) {
conn, err := c.dial()
if err != nil {
return nil, err
}
defer conn.Close()
if err := conn.SetDeadline(time.Now().Add(time.Duration(c.timeout) * time.Second)); err != nil {
return nil, err
}
bw := bufio.NewWriter(conn)
_, err = bw.WriteString("REPORT SPAMC/" + ProtoVersion + "\r\n")
if err != nil {
return nil, err
}
_, err = bw.WriteString("Content-length: " + strconv.Itoa(len(email)) + "\r\n\r\n")
if err != nil {
return nil, err
}
_, err = bw.Write(email)
if err != nil {
return nil, err
}
err = bw.Flush()
if err != nil {
return nil, err
}
// Client is supposed to close its writing side of the connection
// after sending its request.
err = conn.CloseWrite()
if err != nil {
return nil, err
}
var (
lines []string
br = bufio.NewReader(conn)
)
for {
line, err := br.ReadString('\n')
if err == io.EOF {
break
}
if err != nil {
return nil, err
}
line = strings.TrimRight(line, " \t\r\n")
lines = append(lines, line)
}
// join lines, and replace multi-line descriptions with single line for each
tmp := strings.Join(lines, "\n")
re := regexp.MustCompile("\n ")
n := re.ReplaceAllString(tmp, " ")
//split lines again
return strings.Split(n, "\n"), nil
}
func (c *Client) parseOutput(output []string) Result {
var result Result
var reachedRules bool
for _, row := range output {
// header
if spamInfoRe.MatchString(row) {
res := spamInfoRe.FindStringSubmatch(row)
if len(res) == 5 {
resCode, err := strconv.Atoi(res[3])
if err == nil {
result.ResponseCode = resCode
}
result.Message = res[4]
continue
}
}
// summary
if spamMainRe.MatchString(row) {
res := spamMainRe.FindStringSubmatch(row)
if len(res) == 4 {
if strings.ToLower(res[1]) == "true" || strings.ToLower(res[1]) == "yes" {
result.Spam = true
} else {
result.Spam = false
}
resFloat, err := strconv.ParseFloat(res[2], 32)
if err == nil {
result.Score = resFloat
continue
}
resFloat, err = strconv.ParseFloat(res[3], 32)
if err == nil {
result.Threshold = resFloat
continue
}
}
}
if strings.HasPrefix(row, "Content analysis details") {
reachedRules = true
continue
}
// details
// row = strings.Trim(row, " \t\r\n")
if reachedRules && spamDetailsRe.MatchString(row) {
res := spamDetailsRe.FindStringSubmatch(row)
if len(res) == 5 {
rule := Rule{Points: res[1], Name: res[2], Description: res[4]}
result.Rules = append(result.Rules, rule)
}
}
}
return result
}
// Ping the spamd
func (c *Client) Ping() error {
conn, err := c.dial()
if err != nil {
return err
}
defer conn.Close()
if err := conn.SetDeadline(time.Now().Add(time.Duration(c.timeout) * time.Second)); err != nil {
return err
}
_, err = io.WriteString(conn, fmt.Sprintf("PING SPAMC/%s\r\n\r\n", ProtoVersion))
if err != nil {
return err
}
err = conn.CloseWrite()
if err != nil {
return err
}
br := bufio.NewReader(conn)
for {
_, err = br.ReadSlice('\n')
if err == io.EOF {
break
}
if err != nil {
return err
}
}
return nil
}

@ -405,6 +405,20 @@ func GetMessage(id string) (*Message, error) {
}
}
// get List-Unsubscribe links if set
obj.ListUnsubscribe = ListUnsubscribe{}
obj.ListUnsubscribe.Links = []string{}
if env.GetHeader("List-Unsubscribe") != "" {
l := env.GetHeader("List-Unsubscribe")
links, err := tools.ListUnsubscribeParser(l)
obj.ListUnsubscribe.Header = l
obj.ListUnsubscribe.Links = links
if err != nil {
obj.ListUnsubscribe.Errors = err.Error()
}
obj.ListUnsubscribe.HeaderPost = env.GetHeader("List-Unsubscribe-Post")
}
// mark message as read
if err := MarkRead(id); err != nil {
return &obj, err

@ -29,6 +29,9 @@ type Message struct {
ReturnPath string
// Message subject
Subject string
// List-Unsubscribe header information
// swagger:ignore
ListUnsubscribe ListUnsubscribe
// Message date if set, else date received
Date time.Time
// Message tags
@ -122,3 +125,16 @@ func AttachmentSummary(a *enmime.Part) Attachment {
return o
}
// ListUnsubscribe contains a summary of List-Unsubscribe & List-Unsubscribe-Post headers
// including validation of the link structure
type ListUnsubscribe struct {
// List-Unsubscribe header value
Header string
// Detected links, maximum one email and one HTTP(S)
Links []string
// Validation errors if any
Errors string
// List-Unsubscribe-Post value if set
HeaderPost string
}

@ -0,0 +1,99 @@
package tools
import (
"fmt"
"net/url"
"regexp"
"strings"
)
// ListUnsubscribeParser will attempt to parse a `List-Unsubscribe` header and return
// a slide of addresses (mail & URLs)
func ListUnsubscribeParser(v string) ([]string, error) {
var results = []string{}
var re = regexp.MustCompile(`(?mU)<(.*)>`)
var reJoins = regexp.MustCompile(`(?imUs)>(.*)<`)
var reValidJoinChars = regexp.MustCompile(`(?imUs)^(\s+)?,(\s+)?$`)
var reWrapper = regexp.MustCompile(`(?imUs)^<(.*)>$`)
var reMailTo = regexp.MustCompile(`^mailto:[a-zA-Z0-9]`)
var reHTTP = regexp.MustCompile(`^(?i)https?://[a-zA-Z0-9]`)
var reSpaces = regexp.MustCompile(`\s`)
var reComments = regexp.MustCompile(`(?mUs)\(.*\)`)
var hasMailTo bool
var hasHTTP bool
v = strings.TrimSpace(v)
comments := reComments.FindAllStringSubmatch(v, -1)
for _, c := range comments {
// strip comments
v = strings.Replace(v, c[0], "", -1)
v = strings.TrimSpace(v)
}
if !re.MatchString(v) {
return results, fmt.Errorf("\"%s\" no valid unsubscribe links found", v)
}
errors := []string{}
if !reWrapper.MatchString(v) {
return results, fmt.Errorf("\"%s\" should be enclosed in <>", v)
}
matches := re.FindAllStringSubmatch(v, -1)
if len(matches) > 2 {
errors = append(errors, fmt.Sprintf("\"%s\" should include a maximum of one email and one HTTP link", v))
} else {
splits := reJoins.FindAllStringSubmatch(v, -1)
for _, g := range splits {
if !reValidJoinChars.MatchString(g[1]) {
return results, fmt.Errorf("\"%s\" <> should be split with a comma and optional spaces", v)
}
}
for _, m := range matches {
r := m[1]
if reSpaces.MatchString(r) {
errors = append(errors, fmt.Sprintf("\"%s\" should not contain spaces", r))
continue
}
if reMailTo.MatchString(r) {
if hasMailTo {
errors = append(errors, fmt.Sprintf("\"%s\" should only contain one mailto:", r))
continue
}
hasMailTo = true
} else if reHTTP.MatchString(r) {
if hasHTTP {
errors = append(errors, fmt.Sprintf("\"%s\" should only contain one HTTP link", r))
continue
}
hasHTTP = true
} else {
errors = append(errors, fmt.Sprintf("\"%s\" should start with either http(s):// or mailto:", r))
continue
}
_, err := url.ParseRequestURI(r)
if err != nil {
errors = append(errors, err.Error())
continue
}
results = append(results, r)
}
}
var err error
if len(errors) > 0 {
err = fmt.Errorf("%s", strings.Join(errors, ", "))
}
return results, err
}

@ -69,3 +69,51 @@ func TestSnippets(t *testing.T) {
}
}
}
func TestListUnsubscribeParser(t *testing.T) {
tests := map[string]bool{}
// should pass
tests["<mailto:unsubscribe@example.com>"] = true
tests["<https://example.com>"] = true
tests["<HTTPS://EXAMPLE.COM>"] = true
tests["<mailto:unsubscribe@example.com>, <http://example.com>"] = true
tests["<mailto:unsubscribe@example.com>, <https://example.com>"] = true
tests["<https://example.com>, <mailto:unsubscribe@example.com>"] = true
tests["<https://example.com> , <mailto:unsubscribe@example.com>"] = true
tests["<https://example.com> ,<mailto:unsubscribe@example.com>"] = true
tests["<mailto:unsubscribe@example.com>,<https://example.com>"] = true
tests[`<https://example.com> ,
<mailto:unsubscribe@example.com>`] = true
tests["<mailto:unsubscribe@example.com?subject=unsubscribe%20me>"] = true
tests["(Use this command to get off the list) <mailto:unsubscribe@example.com?subject=unsubscribe%20me>"] = true
tests["<mailto:unsubscribe@example.com> (Use this command to get off the list)"] = true
tests["(Use this command to get off the list) <mailto:unsubscribe@example.com>, (Click this link to unsubscribe) <http://example.com>"] = true
// should fail
tests["mailto:unsubscribe@example.com"] = false // no <>
tests["<mailto::unsubscribe@example.com>"] = false // ::
tests["https://example.com/"] = false // no <>
tests["mailto:unsubscribe@example.com, <https://example.com/>"] = false // no <>
tests["<MAILTO:unsubscribe@example.com>"] = false // capitals
tests["<mailto:unsubscribe@example.com>, <mailto:test2@example.com>"] = false // two emails
tests["<http://exampl\\e2.com>, <http://example2.com>"] = false // two links
tests["<http://example.com>, <mailto:unsubscribe@example.com>, <http://example2.com>"] = false // two links
tests["<mailto:unsubscribe@example.com>, <example.com>"] = false // no mailto || http(s)
tests["<mailto: unsubscribe@example.com>, <unsubscribe@lol.com>"] = false // space
tests["<mailto:unsubscribe@example.com?subject=unsubscribe me>"] = false // space
tests["<http:///example.com>"] = false // http:///
for search, expected := range tests {
_, err := ListUnsubscribeParser(search)
hasError := err != nil
if expected == hasError {
if err != nil {
t.Logf("ListUnsubscribeParser: %v", err)
} else {
t.Logf("ListUnsubscribeParser: \"%s\" expected: %v", search, expected)
}
t.Fail()
}
}
}

559
package-lock.json generated

@ -51,9 +51,9 @@
}
},
"node_modules/@babel/runtime-corejs3": {
"version": "7.23.7",
"resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.23.7.tgz",
"integrity": "sha512-ER55qzLREVA5YxeyQ3Qu48tgsF2ZrFjFjUS6V6wF0cikSw+goBJgB9PBRM1T6+Ah4iiM+sxmfS/Sy/jdzFfhiQ==",
"version": "7.23.8",
"resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.23.8.tgz",
"integrity": "sha512-2ZzmcDugdm0/YQKFVYsXiwUN7USPX8PM7cytpb4PFl87fM+qYPSvTZX//8tyeJB1j0YDmafBJEbl5f8NfLyuKw==",
"dependencies": {
"core-js-pure": "^3.30.2",
"regenerator-runtime": "^0.14.0"
@ -466,12 +466,12 @@
}
},
"node_modules/@swagger-api/apidom-ast": {
"version": "0.89.0",
"resolved": "https://registry.npmjs.org/@swagger-api/apidom-ast/-/apidom-ast-0.89.0.tgz",
"integrity": "sha512-Rqfzqo8On7ddhmsKFWsCLsfCJRlOYbIM1itYnxpnj2wxrxQ8v0b91ecFU/Hs/NgDuncvbZYf7gD+71g0QAJrww==",
"version": "0.92.0",
"resolved": "https://registry.npmjs.org/@swagger-api/apidom-ast/-/apidom-ast-0.92.0.tgz",
"integrity": "sha512-j9vuKaYZP3mAGXUcKeWIkSToxPPCBLJcLEfjSEh14P0n6NRJp7Yg19SA+IwHdIvOAfJonuebj/lhPOMjzd6P1g==",
"dependencies": {
"@babel/runtime-corejs3": "^7.20.7",
"@swagger-api/apidom-error": "^0.89.0",
"@swagger-api/apidom-error": "^0.92.0",
"@types/ramda": "~0.29.6",
"ramda": "~0.29.1",
"ramda-adjunct": "^4.1.1",
@ -480,13 +480,13 @@
}
},
"node_modules/@swagger-api/apidom-core": {
"version": "0.89.0",
"resolved": "https://registry.npmjs.org/@swagger-api/apidom-core/-/apidom-core-0.89.0.tgz",
"integrity": "sha512-GVjcvNEh1aPeWZHoVxPx9jMwff0nKPkKjuKyTOrMCCCGIO92J5o42qYxcerW4FTKlnpXvc2vObl0B5X5yh2jIA==",
"version": "0.92.0",
"resolved": "https://registry.npmjs.org/@swagger-api/apidom-core/-/apidom-core-0.92.0.tgz",
"integrity": "sha512-PK1zlS0UCcE5dIPtSy8/+oWfXAVf7b/iM3LRaPgaFGF5b8qa6S/zmROTh10Yjug9v9Vnuq8opEhyHkGyl+WdSA==",
"dependencies": {
"@babel/runtime-corejs3": "^7.20.7",
"@swagger-api/apidom-ast": "^0.89.0",
"@swagger-api/apidom-error": "^0.89.0",
"@swagger-api/apidom-ast": "^0.92.0",
"@swagger-api/apidom-error": "^0.92.0",
"@types/ramda": "~0.29.6",
"minim": "~0.23.8",
"ramda": "~0.29.1",
@ -496,36 +496,36 @@
}
},
"node_modules/@swagger-api/apidom-error": {
"version": "0.89.0",
"resolved": "https://registry.npmjs.org/@swagger-api/apidom-error/-/apidom-error-0.89.0.tgz",
"integrity": "sha512-e2xt6Mjf58yfotElZUvM1aglvlTGN8pcJR/kotNc+JmYBTw9gzB8mDjBya4z1Ze0Z++Cp2FMTVpd8n0QceQqKQ==",
"version": "0.92.0",
"resolved": "https://registry.npmjs.org/@swagger-api/apidom-error/-/apidom-error-0.92.0.tgz",
"integrity": "sha512-wo7xCvTpWr5Lpt/ly1L4bhZ6W7grgtAg7SK/d8FNZR85zPJXM4FPMpcRtKktfWJ/RikQJT/g5DjI33iTqB6z/w==",
"dependencies": {
"@babel/runtime-corejs3": "^7.20.7"
}
},
"node_modules/@swagger-api/apidom-json-pointer": {
"version": "0.89.0",
"resolved": "https://registry.npmjs.org/@swagger-api/apidom-json-pointer/-/apidom-json-pointer-0.89.0.tgz",
"integrity": "sha512-42D4HG2hsBU3qYX2yKW743/4dGp0rKyjtal3s+Rdae46rQuqXOItU7PQLYyORpM4Pka6wTwAKlhnz3raYH4zPQ==",
"version": "0.92.0",
"resolved": "https://registry.npmjs.org/@swagger-api/apidom-json-pointer/-/apidom-json-pointer-0.92.0.tgz",
"integrity": "sha512-VmZ1EXE7BWX+ndeeh9t1uFRql5jbPRmAcglUfdtu3jlg6fOqXzzgx9qFpRz9GhpMHWEGFm1ymd8tMAa1CvgcHw==",
"dependencies": {
"@babel/runtime-corejs3": "^7.20.7",
"@swagger-api/apidom-core": "^0.89.0",
"@swagger-api/apidom-error": "^0.89.0",
"@swagger-api/apidom-core": "^0.92.0",
"@swagger-api/apidom-error": "^0.92.0",
"@types/ramda": "~0.29.6",
"ramda": "~0.29.1",
"ramda-adjunct": "^4.0.0"
}
},
"node_modules/@swagger-api/apidom-ns-api-design-systems": {
"version": "0.89.0",
"resolved": "https://registry.npmjs.org/@swagger-api/apidom-ns-api-design-systems/-/apidom-ns-api-design-systems-0.89.0.tgz",
"integrity": "sha512-RQzXwWi0GXIo1Y89KfgaCA8B/vic094YRtZbj/Y7tzxTvFwhtBdpHn0ur/Nm+zSb+FlFq0YZZS7jIJ/ekPB1FQ==",
"version": "0.92.0",
"resolved": "https://registry.npmjs.org/@swagger-api/apidom-ns-api-design-systems/-/apidom-ns-api-design-systems-0.92.0.tgz",
"integrity": "sha512-wXEXhw0wDQIPTUqff953h44oQZr29DcoAzZfROWlGtOLItGDDMjhfIYiRg1406mXA4N7d5d0vNi9V/HXkxItQw==",
"optional": true,
"dependencies": {
"@babel/runtime-corejs3": "^7.20.7",
"@swagger-api/apidom-core": "^0.89.0",
"@swagger-api/apidom-error": "^0.89.0",
"@swagger-api/apidom-ns-openapi-3-1": "^0.89.0",
"@swagger-api/apidom-core": "^0.92.0",
"@swagger-api/apidom-error": "^0.92.0",
"@swagger-api/apidom-ns-openapi-3-1": "^0.92.0",
"@types/ramda": "~0.29.6",
"ramda": "~0.29.1",
"ramda-adjunct": "^4.1.1",
@ -533,14 +533,14 @@
}
},
"node_modules/@swagger-api/apidom-ns-asyncapi-2": {
"version": "0.89.0",
"resolved": "https://registry.npmjs.org/@swagger-api/apidom-ns-asyncapi-2/-/apidom-ns-asyncapi-2-0.89.0.tgz",
"integrity": "sha512-3JMHw/cyqHSTKpAGWtC0jjnlhI2qqhd3nBdlDbWCk329bVoLncSzUaXk3ozmRb9qeZdnrEHYb0H9WaeByT0lGA==",
"version": "0.92.0",
"resolved": "https://registry.npmjs.org/@swagger-api/apidom-ns-asyncapi-2/-/apidom-ns-asyncapi-2-0.92.0.tgz",
"integrity": "sha512-FmJLT3GqzT4HK7Mwh54cXZ4PZt58yKVtJAKWKJ0dg2/Gim0AKJWf6t6B3Z9ZFUiKyehbqP4K7gSM7qGL0tKe2Q==",
"optional": true,
"dependencies": {
"@babel/runtime-corejs3": "^7.20.7",
"@swagger-api/apidom-core": "^0.89.0",
"@swagger-api/apidom-ns-json-schema-draft-7": "^0.89.0",
"@swagger-api/apidom-core": "^0.92.0",
"@swagger-api/apidom-ns-json-schema-draft-7": "^0.92.0",
"@types/ramda": "~0.29.6",
"ramda": "~0.29.1",
"ramda-adjunct": "^4.1.1",
@ -548,13 +548,13 @@
}
},
"node_modules/@swagger-api/apidom-ns-json-schema-draft-4": {
"version": "0.89.0",
"resolved": "https://registry.npmjs.org/@swagger-api/apidom-ns-json-schema-draft-4/-/apidom-ns-json-schema-draft-4-0.89.0.tgz",
"integrity": "sha512-7gXy3BPLkS7p7dmz9Hbf7ia4lH0NAaW2i7GcQdpX48pAUTR0/7Y+BPd38sgRxIOpebReWxnoAcKAfkak/KCQ3A==",
"version": "0.92.0",
"resolved": "https://registry.npmjs.org/@swagger-api/apidom-ns-json-schema-draft-4/-/apidom-ns-json-schema-draft-4-0.92.0.tgz",
"integrity": "sha512-7s2EKjCQwRXbK4Y4AGpVkyn1AANCxOUFSHebo1h2katyVeAopV0LJmbXH5yQedTltV0k3BIjnd7hS+7dI846Pw==",
"dependencies": {
"@babel/runtime-corejs3": "^7.20.7",
"@swagger-api/apidom-ast": "^0.89.0",
"@swagger-api/apidom-core": "^0.89.0",
"@swagger-api/apidom-ast": "^0.92.0",
"@swagger-api/apidom-core": "^0.92.0",
"@types/ramda": "~0.29.6",
"ramda": "~0.29.1",
"ramda-adjunct": "^4.1.1",
@ -562,15 +562,15 @@
}
},
"node_modules/@swagger-api/apidom-ns-json-schema-draft-6": {
"version": "0.89.0",
"resolved": "https://registry.npmjs.org/@swagger-api/apidom-ns-json-schema-draft-6/-/apidom-ns-json-schema-draft-6-0.89.0.tgz",
"integrity": "sha512-Ed3hpPAhHJHs25HoBt4ySrfbfUSlOdU4uXyVsumjLSHSSQxd6NfIovKOSdYllSRYXrTmkfE50DrentYozDpBfQ==",
"version": "0.92.0",
"resolved": "https://registry.npmjs.org/@swagger-api/apidom-ns-json-schema-draft-6/-/apidom-ns-json-schema-draft-6-0.92.0.tgz",
"integrity": "sha512-zur80x04jesXVzlU9sLZhW4giO9RfOouI7L/H8v2wUlcBvjaPBn1tIqrURw2VEHKAcJORhTRusQCR21vnFot2g==",
"optional": true,
"dependencies": {
"@babel/runtime-corejs3": "^7.20.7",
"@swagger-api/apidom-core": "^0.89.0",
"@swagger-api/apidom-error": "^0.89.0",
"@swagger-api/apidom-ns-json-schema-draft-4": "^0.89.0",
"@swagger-api/apidom-core": "^0.92.0",
"@swagger-api/apidom-error": "^0.92.0",
"@swagger-api/apidom-ns-json-schema-draft-4": "^0.92.0",
"@types/ramda": "~0.29.6",
"ramda": "~0.29.1",
"ramda-adjunct": "^4.1.1",
@ -578,15 +578,15 @@
}
},
"node_modules/@swagger-api/apidom-ns-json-schema-draft-7": {
"version": "0.89.0",
"resolved": "https://registry.npmjs.org/@swagger-api/apidom-ns-json-schema-draft-7/-/apidom-ns-json-schema-draft-7-0.89.0.tgz",
"integrity": "sha512-VF33y3qettfHiS7FtenRfqYpGkZSlXb+KqSNKefuPvp7l1EjR3lnl+pszCKcIIXXTcz5Cgt6OVx9dHsdjsNW1g==",
"version": "0.92.0",
"resolved": "https://registry.npmjs.org/@swagger-api/apidom-ns-json-schema-draft-7/-/apidom-ns-json-schema-draft-7-0.92.0.tgz",
"integrity": "sha512-DSY7lY98XHnc0wg0V38ZmBPs5HWuRuSb6G+n5Z+qs5RRodh1x5BrTIY6M0Yk3oJVbbEoFGmF0VlTe6vHf44pbw==",
"optional": true,
"dependencies": {
"@babel/runtime-corejs3": "^7.20.7",
"@swagger-api/apidom-core": "^0.89.0",
"@swagger-api/apidom-error": "^0.89.0",
"@swagger-api/apidom-ns-json-schema-draft-6": "^0.89.0",
"@swagger-api/apidom-core": "^0.92.0",
"@swagger-api/apidom-error": "^0.92.0",
"@swagger-api/apidom-ns-json-schema-draft-6": "^0.92.0",
"@types/ramda": "~0.29.6",
"ramda": "~0.29.1",
"ramda-adjunct": "^4.1.1",
@ -594,15 +594,15 @@
}
},
"node_modules/@swagger-api/apidom-ns-openapi-2": {
"version": "0.89.0",
"resolved": "https://registry.npmjs.org/@swagger-api/apidom-ns-openapi-2/-/apidom-ns-openapi-2-0.89.0.tgz",
"integrity": "sha512-M9k9heFnVGbuo36oiynOULk6ROWDHBpInftmZUSYvsfvgsQDCLK+rZvMr9lrk4cSUV7OcqSG9r4NdrXt7dZxYg==",
"version": "0.92.0",
"resolved": "https://registry.npmjs.org/@swagger-api/apidom-ns-openapi-2/-/apidom-ns-openapi-2-0.92.0.tgz",
"integrity": "sha512-OJlSTvPzK+zqzd2xXeWkF50z08Wlpygc98eVzZjYI0Af8mz7x6R5T9BCP5p6ZlQoO9OTvk4gfv7ViWXCdamObg==",
"optional": true,
"dependencies": {
"@babel/runtime-corejs3": "^7.20.7",
"@swagger-api/apidom-core": "^0.89.0",
"@swagger-api/apidom-error": "^0.89.0",
"@swagger-api/apidom-ns-json-schema-draft-4": "^0.89.0",
"@swagger-api/apidom-core": "^0.92.0",
"@swagger-api/apidom-error": "^0.92.0",
"@swagger-api/apidom-ns-json-schema-draft-4": "^0.92.0",
"@types/ramda": "~0.29.6",
"ramda": "~0.29.1",
"ramda-adjunct": "^4.1.1",
@ -610,14 +610,14 @@
}
},
"node_modules/@swagger-api/apidom-ns-openapi-3-0": {
"version": "0.89.0",
"resolved": "https://registry.npmjs.org/@swagger-api/apidom-ns-openapi-3-0/-/apidom-ns-openapi-3-0-0.89.0.tgz",
"integrity": "sha512-9kbGRhjt+cpN6eqrwJ3GktoEGLXP2/9wDTQIUiII8jpjSRDwX8fzKMCvaQgGU3Id0gIG3KFVscvv15Z+n8PHMw==",
"version": "0.92.0",
"resolved": "https://registry.npmjs.org/@swagger-api/apidom-ns-openapi-3-0/-/apidom-ns-openapi-3-0-0.92.0.tgz",
"integrity": "sha512-VGha4RRnoeoAZBWLGy37YsBzwICM3ZFNyCk2Dwpaqfg9zFN+E6BL2CtIbkxvFkMdwaMURmDItiQsw28pF0tOgQ==",
"dependencies": {
"@babel/runtime-corejs3": "^7.20.7",
"@swagger-api/apidom-core": "^0.89.0",
"@swagger-api/apidom-error": "^0.89.0",
"@swagger-api/apidom-ns-json-schema-draft-4": "^0.89.0",
"@swagger-api/apidom-core": "^0.92.0",
"@swagger-api/apidom-error": "^0.92.0",
"@swagger-api/apidom-ns-json-schema-draft-4": "^0.92.0",
"@types/ramda": "~0.29.6",
"ramda": "~0.29.1",
"ramda-adjunct": "^4.1.1",
@ -625,14 +625,29 @@
}
},
"node_modules/@swagger-api/apidom-ns-openapi-3-1": {
"version": "0.89.0",
"resolved": "https://registry.npmjs.org/@swagger-api/apidom-ns-openapi-3-1/-/apidom-ns-openapi-3-1-0.89.0.tgz",
"integrity": "sha512-QlmETSbV6XL+AutyEvXcw78paizZSFgGWsqxMJKj9nZgdh217dLvvt0V5vWdE5fK5p4hlzHfRR7kO0Ong8sGSw==",
"version": "0.92.0",
"resolved": "https://registry.npmjs.org/@swagger-api/apidom-ns-openapi-3-1/-/apidom-ns-openapi-3-1-0.92.0.tgz",
"integrity": "sha512-xZD+JxifYhDoTjn76K2ZT3xNoXBQChaKfSkJr4l5Xh9Guuk0IcsPTUDRpuytuZZXVez0O401XFoUso/mZRTjkA==",
"dependencies": {
"@babel/runtime-corejs3": "^7.20.7",
"@swagger-api/apidom-ast": "^0.89.0",
"@swagger-api/apidom-core": "^0.89.0",
"@swagger-api/apidom-ns-openapi-3-0": "^0.89.0",
"@swagger-api/apidom-ast": "^0.92.0",
"@swagger-api/apidom-core": "^0.92.0",
"@swagger-api/apidom-ns-openapi-3-0": "^0.92.0",
"@types/ramda": "~0.29.6",
"ramda": "~0.29.1",
"ramda-adjunct": "^4.1.1",
"stampit": "^4.3.2"
}
},
"node_modules/@swagger-api/apidom-ns-workflows-1": {
"version": "0.92.0",
"resolved": "https://registry.npmjs.org/@swagger-api/apidom-ns-workflows-1/-/apidom-ns-workflows-1-0.92.0.tgz",
"integrity": "sha512-gl1dF+SrRHK4lLiwaK4PMjL9A5z28cW9xiMWCxRyppX/I2bVTVVOfgdAyqLWsFA0gopmITWesJxohRumG35fTw==",
"optional": true,
"dependencies": {
"@babel/runtime-corejs3": "^7.20.7",
"@swagger-api/apidom-core": "^0.92.0",
"@swagger-api/apidom-ns-openapi-3-1": "^0.92.0",
"@types/ramda": "~0.29.6",
"ramda": "~0.29.1",
"ramda-adjunct": "^4.1.1",
@ -640,200 +655,228 @@
}
},
"node_modules/@swagger-api/apidom-parser-adapter-api-design-systems-json": {
"version": "0.89.0",
"resolved": "https://registry.npmjs.org/@swagger-api/apidom-parser-adapter-api-design-systems-json/-/apidom-parser-adapter-api-design-systems-json-0.89.0.tgz",
"integrity": "sha512-sYr5E0RKZqupgMzvUCM0nDMskl8YPrzYJ0MFW91NJvL3rBzeShBqZ+dB62UzDIAXwjZEtvEEaC9eGIPf7IxvEg==",
"version": "0.92.0",
"resolved": "https://registry.npmjs.org/@swagger-api/apidom-parser-adapter-api-design-systems-json/-/apidom-parser-adapter-api-design-systems-json-0.92.0.tgz",
"integrity": "sha512-i07FeLdNobWzHT9LnfsdOix+XrlZN/KnQL1RODPzxWk7i7ya2e4uc3JemyHh4Tnv04G8JV32SQqtzOtMteJsdA==",
"optional": true,
"dependencies": {
"@babel/runtime-corejs3": "^7.20.7",
"@swagger-api/apidom-core": "^0.89.0",
"@swagger-api/apidom-ns-api-design-systems": "^0.89.0",
"@swagger-api/apidom-parser-adapter-json": "^0.89.0",
"@swagger-api/apidom-core": "^0.92.0",
"@swagger-api/apidom-ns-api-design-systems": "^0.92.0",
"@swagger-api/apidom-parser-adapter-json": "^0.92.0",
"@types/ramda": "~0.29.6",
"ramda": "~0.29.1",
"ramda-adjunct": "^4.0.0"
}
},
"node_modules/@swagger-api/apidom-parser-adapter-api-design-systems-yaml": {
"version": "0.89.0",
"resolved": "https://registry.npmjs.org/@swagger-api/apidom-parser-adapter-api-design-systems-yaml/-/apidom-parser-adapter-api-design-systems-yaml-0.89.0.tgz",
"integrity": "sha512-mbl2wOMY62S2GgGMon1IpsosPQ5zn+rwW8xnZAX/LDUMB5YLmk2THghckyxjxdSBqkJ9jJMEuoHk+RInT6qBzQ==",
"version": "0.92.0",
"resolved": "https://registry.npmjs.org/@swagger-api/apidom-parser-adapter-api-design-systems-yaml/-/apidom-parser-adapter-api-design-systems-yaml-0.92.0.tgz",
"integrity": "sha512-bbjFkU0D4zqaZnd8/m1Kyx2UuHpri8ZxLdT1TiXqHweSfRQcNt4VYt0bjWBnnGGBMkHElgYbX5ov6kHvPf3wJg==",
"optional": true,
"dependencies": {
"@babel/runtime-corejs3": "^7.20.7",
"@swagger-api/apidom-core": "^0.89.0",
"@swagger-api/apidom-ns-api-design-systems": "^0.89.0",
"@swagger-api/apidom-parser-adapter-yaml-1-2": "^0.89.0",
"@swagger-api/apidom-core": "^0.92.0",
"@swagger-api/apidom-ns-api-design-systems": "^0.92.0",
"@swagger-api/apidom-parser-adapter-yaml-1-2": "^0.92.0",
"@types/ramda": "~0.29.6",
"ramda": "~0.29.1",
"ramda-adjunct": "^4.0.0"
}
},
"node_modules/@swagger-api/apidom-parser-adapter-asyncapi-json-2": {
"version": "0.89.0",
"resolved": "https://registry.npmjs.org/@swagger-api/apidom-parser-adapter-asyncapi-json-2/-/apidom-parser-adapter-asyncapi-json-2-0.89.0.tgz",
"integrity": "sha512-0p4/HuGfp4xiddfJw9FrMTEaRikhOZLkR7it+U2P1X3LagrVqI3dTivT/TMMA4xaVuczoKrBC/A1SySboarU/w==",
"version": "0.92.0",
"resolved": "https://registry.npmjs.org/@swagger-api/apidom-parser-adapter-asyncapi-json-2/-/apidom-parser-adapter-asyncapi-json-2-0.92.0.tgz",
"integrity": "sha512-Q7gudmGA5TUGbbr0QYNQkndktP91C0WE7uDDS2IwCBtHroRDiMPFCjzE9dsjIST5WnP+LUXmxG1Bv0NLTWcSUg==",
"optional": true,
"dependencies": {
"@babel/runtime-corejs3": "^7.20.7",
"@swagger-api/apidom-core": "^0.89.0",
"@swagger-api/apidom-ns-asyncapi-2": "^0.89.0",
"@swagger-api/apidom-parser-adapter-json": "^0.89.0",
"@swagger-api/apidom-core": "^0.92.0",
"@swagger-api/apidom-ns-asyncapi-2": "^0.92.0",
"@swagger-api/apidom-parser-adapter-json": "^0.92.0",
"@types/ramda": "~0.29.6",
"ramda": "~0.29.1",
"ramda-adjunct": "^4.0.0"
}
},
"node_modules/@swagger-api/apidom-parser-adapter-asyncapi-yaml-2": {
"version": "0.89.0",
"resolved": "https://registry.npmjs.org/@swagger-api/apidom-parser-adapter-asyncapi-yaml-2/-/apidom-parser-adapter-asyncapi-yaml-2-0.89.0.tgz",
"integrity": "sha512-t14f5RtN6eD37JknahpQpNnrv68QqvA0EQc1cJPskxclh1MRqmvT+oo9R0rXPh2isqr9nhG8UfySdj+5jS5qVA==",
"version": "0.92.0",
"resolved": "https://registry.npmjs.org/@swagger-api/apidom-parser-adapter-asyncapi-yaml-2/-/apidom-parser-adapter-asyncapi-yaml-2-0.92.0.tgz",
"integrity": "sha512-V5/VdDj0aeOKp+3AtvPSz2b0HosJfYkHPjNvPU5eafLSzqzMIR/evYq5BvKWoJL1IvLdjoEPqDVVaEZluHZTew==",
"optional": true,
"dependencies": {
"@babel/runtime-corejs3": "^7.20.7",
"@swagger-api/apidom-core": "^0.89.0",
"@swagger-api/apidom-ns-asyncapi-2": "^0.89.0",
"@swagger-api/apidom-parser-adapter-yaml-1-2": "^0.89.0",
"@swagger-api/apidom-core": "^0.92.0",
"@swagger-api/apidom-ns-asyncapi-2": "^0.92.0",
"@swagger-api/apidom-parser-adapter-yaml-1-2": "^0.92.0",
"@types/ramda": "~0.29.6",
"ramda": "~0.29.1",
"ramda-adjunct": "^4.0.0"
}
},
"node_modules/@swagger-api/apidom-parser-adapter-json": {
"version": "0.89.0",
"resolved": "https://registry.npmjs.org/@swagger-api/apidom-parser-adapter-json/-/apidom-parser-adapter-json-0.89.0.tgz",
"integrity": "sha512-UREIb9iLcthuk76iqkFggEdaxYrGNhMOUxU8q/K085bSd+2emLC5yI0yWauEufZPcyTnhu6ZIAsR1lCs6dXkIA==",
"version": "0.92.0",
"resolved": "https://registry.npmjs.org/@swagger-api/apidom-parser-adapter-json/-/apidom-parser-adapter-json-0.92.0.tgz",
"integrity": "sha512-KA1Nn6FN0zTA5JhRazwYN9voTDlmExID7Jwz6GXmY826OXqeT4Yl0Egyo1aLYrfT0S73vhC4LVqpdORWLGdZtg==",
"optional": true,
"dependencies": {
"@babel/runtime-corejs3": "^7.20.7",
"@swagger-api/apidom-ast": "^0.89.0",
"@swagger-api/apidom-core": "^0.89.0",
"@swagger-api/apidom-error": "^0.89.0",
"@swagger-api/apidom-ast": "^0.92.0",
"@swagger-api/apidom-core": "^0.92.0",
"@swagger-api/apidom-error": "^0.92.0",
"@types/ramda": "~0.29.6",
"ramda": "~0.29.1",
"ramda-adjunct": "^4.1.1",
"stampit": "^4.3.2",
"tree-sitter": "=0.20.4",
"tree-sitter-json": "=0.20.1",
"web-tree-sitter": "=0.20.3"
}
},
"node_modules/@swagger-api/apidom-parser-adapter-openapi-json-2": {
"version": "0.89.0",
"resolved": "https://registry.npmjs.org/@swagger-api/apidom-parser-adapter-openapi-json-2/-/apidom-parser-adapter-openapi-json-2-0.89.0.tgz",
"integrity": "sha512-thpuSntNPIKxaY7RcrCyick4946HcYGokw2ie/iRYIM9GbHZPBjcJMEl3+UTq/WsvBxQWIcrVrY0G7VKC/ZwDQ==",
"version": "0.92.0",
"resolved": "https://registry.npmjs.org/@swagger-api/apidom-parser-adapter-openapi-json-2/-/apidom-parser-adapter-openapi-json-2-0.92.0.tgz",
"integrity": "sha512-8OlvjcvI/GuOFJJxN+Mc4tJSo9UWuJdzQtQOtO4k3QwWwS28hGvRTjQ5PpsXAVZoLJMAbDuRdREYD9qeIKvM2g==",
"optional": true,
"dependencies": {
"@babel/runtime-corejs3": "^7.20.7",
"@swagger-api/apidom-core": "^0.89.0",
"@swagger-api/apidom-ns-openapi-2": "^0.89.0",
"@swagger-api/apidom-parser-adapter-json": "^0.89.0",
"@swagger-api/apidom-core": "^0.92.0",
"@swagger-api/apidom-ns-openapi-2": "^0.92.0",
"@swagger-api/apidom-parser-adapter-json": "^0.92.0",
"@types/ramda": "~0.29.6",
"ramda": "~0.29.1",
"ramda-adjunct": "^4.0.0"
}
},
"node_modules/@swagger-api/apidom-parser-adapter-openapi-json-3-0": {
"version": "0.89.0",
"resolved": "https://registry.npmjs.org/@swagger-api/apidom-parser-adapter-openapi-json-3-0/-/apidom-parser-adapter-openapi-json-3-0-0.89.0.tgz",
"integrity": "sha512-t+VkLdxnt55Wao+lHgA975W3KO7+jNiGFlwbLJO89wRgBcJz9Y1wG267/S+UjwdDHqjdVZvPK8w2uKODRohLTA==",
"version": "0.92.0",
"resolved": "https://registry.npmjs.org/@swagger-api/apidom-parser-adapter-openapi-json-3-0/-/apidom-parser-adapter-openapi-json-3-0-0.92.0.tgz",
"integrity": "sha512-kzE4COaNobKIUjGsdqqXgO/LruaQHs2kTzOzHPUTR1TH1ZlB2t8MTV+6LJzGNG3IB3QSfZDd7KBEYWklsCTyTA==",
"optional": true,
"dependencies": {
"@babel/runtime-corejs3": "^7.20.7",
"@swagger-api/apidom-core": "^0.89.0",
"@swagger-api/apidom-ns-openapi-3-0": "^0.89.0",
"@swagger-api/apidom-parser-adapter-json": "^0.89.0",
"@swagger-api/apidom-core": "^0.92.0",
"@swagger-api/apidom-ns-openapi-3-0": "^0.92.0",
"@swagger-api/apidom-parser-adapter-json": "^0.92.0",
"@types/ramda": "~0.29.6",
"ramda": "~0.29.1",
"ramda-adjunct": "^4.0.0"
}
},
"node_modules/@swagger-api/apidom-parser-adapter-openapi-json-3-1": {
"version": "0.89.0",
"resolved": "https://registry.npmjs.org/@swagger-api/apidom-parser-adapter-openapi-json-3-1/-/apidom-parser-adapter-openapi-json-3-1-0.89.0.tgz",
"integrity": "sha512-46vdRGoBVgiDTb5iMWBT7+19HlY0jkX/KRA828K/cXF/huThGlcfQkq3UEneHFPGV2KpMETrzSC9Pk6UBB1rJA==",
"version": "0.92.0",
"resolved": "https://registry.npmjs.org/@swagger-api/apidom-parser-adapter-openapi-json-3-1/-/apidom-parser-adapter-openapi-json-3-1-0.92.0.tgz",
"integrity": "sha512-4gkIXfKGwEKZQ6+kxp4EdFBlAc7Kjq8GAgaC7ilGTSSxIaz5hBHBOJoe3cXWpQ/WlXiOyNCy7WdbuKRpUDKIdg==",
"optional": true,
"dependencies": {
"@babel/runtime-corejs3": "^7.20.7",
"@swagger-api/apidom-core": "^0.89.0",
"@swagger-api/apidom-ns-openapi-3-1": "^0.89.0",
"@swagger-api/apidom-parser-adapter-json": "^0.89.0",
"@swagger-api/apidom-core": "^0.92.0",
"@swagger-api/apidom-ns-openapi-3-1": "^0.92.0",
"@swagger-api/apidom-parser-adapter-json": "^0.92.0",
"@types/ramda": "~0.29.6",
"ramda": "~0.29.1",
"ramda-adjunct": "^4.0.0"
}
},
"node_modules/@swagger-api/apidom-parser-adapter-openapi-yaml-2": {
"version": "0.89.0",
"resolved": "https://registry.npmjs.org/@swagger-api/apidom-parser-adapter-openapi-yaml-2/-/apidom-parser-adapter-openapi-yaml-2-0.89.0.tgz",
"integrity": "sha512-CYzFa2nsTn8FFDa8xfFIxx1APKbJohPtcv5b4sHE7rU+Xj1Yw3EQhedbVN1d/RK+1zI8p8oo7CY/Ed93GoXZLQ==",
"version": "0.92.0",
"resolved": "https://registry.npmjs.org/@swagger-api/apidom-parser-adapter-openapi-yaml-2/-/apidom-parser-adapter-openapi-yaml-2-0.92.0.tgz",
"integrity": "sha512-TIY9cytYhA3yUf+5PcwsH9UjzKy5V4nGUtK6n5RvcL4btaGQA2LUB5CiV/1nSvYLNjYjGxhtB3haZDbHe3/gyw==",
"optional": true,
"dependencies": {
"@babel/runtime-corejs3": "^7.20.7",
"@swagger-api/apidom-core": "^0.89.0",
"@swagger-api/apidom-ns-openapi-2": "^0.89.0",
"@swagger-api/apidom-parser-adapter-yaml-1-2": "^0.89.0",
"@swagger-api/apidom-core": "^0.92.0",
"@swagger-api/apidom-ns-openapi-2": "^0.92.0",
"@swagger-api/apidom-parser-adapter-yaml-1-2": "^0.92.0",
"@types/ramda": "~0.29.6",
"ramda": "~0.29.1",
"ramda-adjunct": "^4.0.0"
}
},
"node_modules/@swagger-api/apidom-parser-adapter-openapi-yaml-3-0": {
"version": "0.89.0",
"resolved": "https://registry.npmjs.org/@swagger-api/apidom-parser-adapter-openapi-yaml-3-0/-/apidom-parser-adapter-openapi-yaml-3-0-0.89.0.tgz",
"integrity": "sha512-yf7mwRAlAPz+EDJfrVFKz73cQ6BJsS+HQZkoyeF2xfK3UA4Y9NxfEaF6/7qY9WWw6NVO2XHJ+cgWXAacLxNqag==",
"version": "0.92.0",
"resolved": "https://registry.npmjs.org/@swagger-api/apidom-parser-adapter-openapi-yaml-3-0/-/apidom-parser-adapter-openapi-yaml-3-0-0.92.0.tgz",
"integrity": "sha512-AUwtAxeautYtiwifNCmv6Kjs7ksptRFxcQ3sgLv2bP3f9t5jzcI9NhmgJNdbRfohHYaHMwTuUESrfsTdBgKlAA==",
"optional": true,
"dependencies": {
"@babel/runtime-corejs3": "^7.20.7",
"@swagger-api/apidom-core": "^0.89.0",
"@swagger-api/apidom-ns-openapi-3-0": "^0.89.0",
"@swagger-api/apidom-parser-adapter-yaml-1-2": "^0.89.0",
"@swagger-api/apidom-core": "^0.92.0",
"@swagger-api/apidom-ns-openapi-3-0": "^0.92.0",
"@swagger-api/apidom-parser-adapter-yaml-1-2": "^0.92.0",
"@types/ramda": "~0.29.6",
"ramda": "~0.29.1",
"ramda-adjunct": "^4.0.0"
}
},
"node_modules/@swagger-api/apidom-parser-adapter-openapi-yaml-3-1": {
"version": "0.89.0",
"resolved": "https://registry.npmjs.org/@swagger-api/apidom-parser-adapter-openapi-yaml-3-1/-/apidom-parser-adapter-openapi-yaml-3-1-0.89.0.tgz",
"integrity": "sha512-nf01AYWcMHjA4RK1lGJxUftNn+ISS12u0yn2hWRx/epIFz2vbUoNwe5+9XdxVPDZ0sUlrcMcUz9ZweHOUo/t9w==",
"version": "0.92.0",
"resolved": "https://registry.npmjs.org/@swagger-api/apidom-parser-adapter-openapi-yaml-3-1/-/apidom-parser-adapter-openapi-yaml-3-1-0.92.0.tgz",
"integrity": "sha512-gMR4zUZ/RrjVJVr6DnqwsCsnlplGXJk6O9UKbkoBsiom81dkcHx68BmWA2oM2lYVGKx+G8WVmVDo2EJaZvZYGg==",
"optional": true,
"dependencies": {
"@babel/runtime-corejs3": "^7.20.7",
"@swagger-api/apidom-core": "^0.89.0",
"@swagger-api/apidom-ns-openapi-3-1": "^0.89.0",
"@swagger-api/apidom-parser-adapter-yaml-1-2": "^0.89.0",
"@swagger-api/apidom-core": "^0.92.0",
"@swagger-api/apidom-ns-openapi-3-1": "^0.92.0",
"@swagger-api/apidom-parser-adapter-yaml-1-2": "^0.92.0",
"@types/ramda": "~0.29.6",
"ramda": "~0.29.1",
"ramda-adjunct": "^4.0.0"
}
},
"node_modules/@swagger-api/apidom-parser-adapter-workflows-json-1": {
"version": "0.92.0",
"resolved": "https://registry.npmjs.org/@swagger-api/apidom-parser-adapter-workflows-json-1/-/apidom-parser-adapter-workflows-json-1-0.92.0.tgz",
"integrity": "sha512-tyLiSxEKeU6mhClFjNxrTQJA2aSgfEF7LJ/ZcJgvREsvyk6ns3op9wN2SXw4UmD+657IgN0aUPihh92aEXKovA==",
"optional": true,
"dependencies": {
"@babel/runtime-corejs3": "^7.20.7",
"@swagger-api/apidom-core": "^0.92.0",
"@swagger-api/apidom-ns-workflows-1": "^0.92.0",
"@swagger-api/apidom-parser-adapter-json": "^0.92.0",
"@types/ramda": "~0.29.6",
"ramda": "~0.29.1",
"ramda-adjunct": "^4.0.0"
}
},
"node_modules/@swagger-api/apidom-parser-adapter-workflows-yaml-1": {
"version": "0.92.0",
"resolved": "https://registry.npmjs.org/@swagger-api/apidom-parser-adapter-workflows-yaml-1/-/apidom-parser-adapter-workflows-yaml-1-0.92.0.tgz",
"integrity": "sha512-0Nr+5oAocuw3SZXcO8WEqnU7GGWP7O6GrsFafD6KLBL05v3I0erPfmnWQjWh6jBeXv8r5W69WEQItzES0DBJjA==",
"optional": true,
"dependencies": {
"@babel/runtime-corejs3": "^7.20.7",
"@swagger-api/apidom-core": "^0.92.0",
"@swagger-api/apidom-ns-workflows-1": "^0.92.0",
"@swagger-api/apidom-parser-adapter-yaml-1-2": "^0.92.0",
"@types/ramda": "~0.29.6",
"ramda": "~0.29.1",
"ramda-adjunct": "^4.0.0"
}
},
"node_modules/@swagger-api/apidom-parser-adapter-yaml-1-2": {
"version": "0.89.0",
"resolved": "https://registry.npmjs.org/@swagger-api/apidom-parser-adapter-yaml-1-2/-/apidom-parser-adapter-yaml-1-2-0.89.0.tgz",
"integrity": "sha512-q94xmhPznSQRMl7MC+LsCb/n+Az7HlYTblYOv88dZBwmHJKvpSdmaAzRFhoJUMwbBFXN6Qr0w40a80dX0FD62g==",
"version": "0.92.0",
"resolved": "https://registry.npmjs.org/@swagger-api/apidom-parser-adapter-yaml-1-2/-/apidom-parser-adapter-yaml-1-2-0.92.0.tgz",
"integrity": "sha512-cFLqlhehMuY5WRdU1780Vno6iWpjMlr7CfOOloZW1rKf2lvojn0c4eDsyfWFaB2DgE+Xd4CWl55McuaPZMngsw==",
"optional": true,
"dependencies": {
"@babel/runtime-corejs3": "^7.20.7",
"@swagger-api/apidom-ast": "^0.89.0",
"@swagger-api/apidom-core": "^0.89.0",
"@swagger-api/apidom-error": "^0.89.0",
"@swagger-api/apidom-ast": "^0.92.0",
"@swagger-api/apidom-core": "^0.92.0",
"@swagger-api/apidom-error": "^0.92.0",
"@types/ramda": "~0.29.6",
"ramda": "~0.29.1",
"ramda-adjunct": "^4.1.1",
"stampit": "^4.3.2",
"tree-sitter": "=0.20.4",
"tree-sitter-yaml": "=0.5.0",
"web-tree-sitter": "=0.20.3"
}
},
"node_modules/@swagger-api/apidom-reference": {
"version": "0.89.0",
"resolved": "https://registry.npmjs.org/@swagger-api/apidom-reference/-/apidom-reference-0.89.0.tgz",
"integrity": "sha512-o5305pzG3LOli/D8gybR3M4BZRFbefSNos1nTGHrWIFFLjH9ZHa1sUc76WVvNAqwCFfs/j2IXjuIHiQkeJL2Ow==",
"version": "0.92.0",
"resolved": "https://registry.npmjs.org/@swagger-api/apidom-reference/-/apidom-reference-0.92.0.tgz",
"integrity": "sha512-G/qJBTpXCdwPsc5dqPjX+vAfhvtnhIFqnKtEZ71wnEvF7TpIxdeZKKfqpg+Zxi7MSuZD/Gpkr4J/eP0lO0fAdA==",
"dependencies": {
"@babel/runtime-corejs3": "^7.20.7",
"@swagger-api/apidom-core": "^0.89.0",
"@swagger-api/apidom-core": "^0.92.0",
"@types/ramda": "~0.29.6",
"axios": "^1.4.0",
"minimatch": "^7.4.3",
@ -843,24 +886,27 @@
"stampit": "^4.3.2"
},
"optionalDependencies": {
"@swagger-api/apidom-error": "^0.89.0",
"@swagger-api/apidom-json-pointer": "^0.89.0",
"@swagger-api/apidom-ns-asyncapi-2": "^0.89.0",
"@swagger-api/apidom-ns-openapi-2": "^0.89.0",
"@swagger-api/apidom-ns-openapi-3-0": "^0.89.0",
"@swagger-api/apidom-ns-openapi-3-1": "^0.89.0",
"@swagger-api/apidom-parser-adapter-api-design-systems-json": "^0.89.0",
"@swagger-api/apidom-parser-adapter-api-design-systems-yaml": "^0.89.0",
"@swagger-api/apidom-parser-adapter-asyncapi-json-2": "^0.89.0",
"@swagger-api/apidom-parser-adapter-asyncapi-yaml-2": "^0.89.0",
"@swagger-api/apidom-parser-adapter-json": "^0.89.0",
"@swagger-api/apidom-parser-adapter-openapi-json-2": "^0.89.0",
"@swagger-api/apidom-parser-adapter-openapi-json-3-0": "^0.89.0",
"@swagger-api/apidom-parser-adapter-openapi-json-3-1": "^0.89.0",
"@swagger-api/apidom-parser-adapter-openapi-yaml-2": "^0.89.0",
"@swagger-api/apidom-parser-adapter-openapi-yaml-3-0": "^0.89.0",
"@swagger-api/apidom-parser-adapter-openapi-yaml-3-1": "^0.89.0",
"@swagger-api/apidom-parser-adapter-yaml-1-2": "^0.89.0"
"@swagger-api/apidom-error": "^0.92.0",
"@swagger-api/apidom-json-pointer": "^0.92.0",
"@swagger-api/apidom-ns-asyncapi-2": "^0.92.0",
"@swagger-api/apidom-ns-openapi-2": "^0.92.0",
"@swagger-api/apidom-ns-openapi-3-0": "^0.92.0",
"@swagger-api/apidom-ns-openapi-3-1": "^0.92.0",
"@swagger-api/apidom-ns-workflows-1": "^0.92.0",
"@swagger-api/apidom-parser-adapter-api-design-systems-json": "^0.92.0",
"@swagger-api/apidom-parser-adapter-api-design-systems-yaml": "^0.92.0",
"@swagger-api/apidom-parser-adapter-asyncapi-json-2": "^0.92.0",
"@swagger-api/apidom-parser-adapter-asyncapi-yaml-2": "^0.92.0",
"@swagger-api/apidom-parser-adapter-json": "^0.92.0",
"@swagger-api/apidom-parser-adapter-openapi-json-2": "^0.92.0",
"@swagger-api/apidom-parser-adapter-openapi-json-3-0": "^0.92.0",
"@swagger-api/apidom-parser-adapter-openapi-json-3-1": "^0.92.0",
"@swagger-api/apidom-parser-adapter-openapi-yaml-2": "^0.92.0",
"@swagger-api/apidom-parser-adapter-openapi-yaml-3-0": "^0.92.0",
"@swagger-api/apidom-parser-adapter-openapi-yaml-3-1": "^0.92.0",
"@swagger-api/apidom-parser-adapter-workflows-json-1": "^0.92.0",
"@swagger-api/apidom-parser-adapter-workflows-yaml-1": "^0.92.0",
"@swagger-api/apidom-parser-adapter-yaml-1-2": "^0.92.0"
}
},
"node_modules/@types/bootstrap": {
@ -892,49 +938,49 @@
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw=="
},
"node_modules/@vue/compiler-core": {
"version": "3.4.3",
"resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.4.3.tgz",
"integrity": "sha512-u8jzgFg0EDtSrb/hG53Wwh1bAOQFtc1ZCegBpA/glyvTlgHl+tq13o1zvRfLbegYUw/E4mSTGOiCnAJ9SJ+lsg==",
"version": "3.4.15",
"resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.4.15.tgz",
"integrity": "sha512-XcJQVOaxTKCnth1vCxEChteGuwG6wqnUHxAm1DO3gCz0+uXKaJNx8/digSz4dLALCy8n2lKq24jSUs8segoqIw==",
"dependencies": {
"@babel/parser": "^7.23.6",
"@vue/shared": "3.4.3",
"@vue/shared": "3.4.15",
"entities": "^4.5.0",
"estree-walker": "^2.0.2",
"source-map-js": "^1.0.2"
}
},
"node_modules/@vue/compiler-dom": {
"version": "3.4.3",
"resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.4.3.tgz",
"integrity": "sha512-oGF1E9/htI6JWj/lTJgr6UgxNCtNHbM6xKVreBWeZL9QhRGABRVoWGAzxmtBfSOd+w0Zi5BY0Es/tlJrN6WgEg==",
"version": "3.4.15",
"resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.4.15.tgz",
"integrity": "sha512-wox0aasVV74zoXyblarOM3AZQz/Z+OunYcIHe1OsGclCHt8RsRm04DObjefaI82u6XDzv+qGWZ24tIsRAIi5MQ==",
"dependencies": {
"@vue/compiler-core": "3.4.3",
"@vue/shared": "3.4.3"
"@vue/compiler-core": "3.4.15",
"@vue/shared": "3.4.15"
}
},
"node_modules/@vue/compiler-sfc": {
"version": "3.4.3",
"resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.4.3.tgz",
"integrity": "sha512-NuJqb5is9I4uzv316VRUDYgIlPZCG8D+ARt5P4t5UDShIHKL25J3TGZAUryY/Aiy0DsY7srJnZL5ryB6DD63Zw==",
"version": "3.4.15",
"resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.4.15.tgz",
"integrity": "sha512-LCn5M6QpkpFsh3GQvs2mJUOAlBQcCco8D60Bcqmf3O3w5a+KWS5GvYbrrJBkgvL1BDnTp+e8q0lXCLgHhKguBA==",
"dependencies": {
"@babel/parser": "^7.23.6",
"@vue/compiler-core": "3.4.3",
"@vue/compiler-dom": "3.4.3",
"@vue/compiler-ssr": "3.4.3",
"@vue/shared": "3.4.3",
"@vue/compiler-core": "3.4.15",
"@vue/compiler-dom": "3.4.15",
"@vue/compiler-ssr": "3.4.15",
"@vue/shared": "3.4.15",
"estree-walker": "^2.0.2",
"magic-string": "^0.30.5",
"postcss": "^8.4.32",
"postcss": "^8.4.33",
"source-map-js": "^1.0.2"
}
},
"node_modules/@vue/compiler-ssr": {
"version": "3.4.3",
"resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.4.3.tgz",
"integrity": "sha512-wnYQtMBkeFSxgSSQbYGQeXPhQacQiog2c6AlvMldQH6DB+gSXK/0F6DVXAJfEiuBSgBhUc8dwrrG5JQcqwalsA==",
"version": "3.4.15",
"resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.4.15.tgz",
"integrity": "sha512-1jdeQyiGznr8gjFDadVmOJqZiLNSsMa5ZgqavkPZ8O2wjHv0tVuAEsw5hTdUoUW4232vpBbL/wJhzVW/JwY1Uw==",
"dependencies": {
"@vue/compiler-dom": "3.4.3",
"@vue/shared": "3.4.3"
"@vue/compiler-dom": "3.4.15",
"@vue/shared": "3.4.15"
}
},
"node_modules/@vue/devtools-api": {
@ -943,48 +989,48 @@
"integrity": "sha512-+KpckaAQyfbvshdDW5xQylLni1asvNSGme1JFs8I1+/H5pHEhqUKMEQD/qn3Nx5+/nycBq11qAEi8lk+LXI2dA=="
},
"node_modules/@vue/reactivity": {
"version": "3.4.3",
"resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.4.3.tgz",
"integrity": "sha512-q5f9HLDU+5aBKizXHAx0w4whkIANs1Muiq9R5YXm0HtorSlflqv9u/ohaMxuuhHWCji4xqpQ1eL04WvmAmGnFg==",
"version": "3.4.15",
"resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.4.15.tgz",
"integrity": "sha512-55yJh2bsff20K5O84MxSvXKPHHt17I2EomHznvFiJCAZpJTNW8IuLj1xZWMLELRhBK3kkFV/1ErZGHJfah7i7w==",
"dependencies": {
"@vue/shared": "3.4.3"
"@vue/shared": "3.4.15"
}
},
"node_modules/@vue/runtime-core": {
"version": "3.4.3",
"resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.4.3.tgz",
"integrity": "sha512-C1r6QhB1qY7D591RCSFhMULyzL9CuyrGc+3PpB0h7dU4Qqw6GNyo4BNFjHZVvsWncrUlKX3DIKg0Y7rNNr06NQ==",
"version": "3.4.15",
"resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.4.15.tgz",
"integrity": "sha512-6E3by5m6v1AkW0McCeAyhHTw+3y17YCOKG0U0HDKDscV4Hs0kgNT5G+GCHak16jKgcCDHpI9xe5NKb8sdLCLdw==",
"dependencies": {
"@vue/reactivity": "3.4.3",
"@vue/shared": "3.4.3"
"@vue/reactivity": "3.4.15",
"@vue/shared": "3.4.15"
}
},
"node_modules/@vue/runtime-dom": {
"version": "3.4.3",
"resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.4.3.tgz",
"integrity": "sha512-wrsprg7An5Ec+EhPngWdPuzkp0BEUxAKaQtN9dPU/iZctPyD9aaXmVtehPJerdQxQale6gEnhpnfywNw3zOv2A==",
"version": "3.4.15",
"resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.4.15.tgz",
"integrity": "sha512-EVW8D6vfFVq3V/yDKNPBFkZKGMFSvZrUQmx196o/v2tHKdwWdiZjYUBS+0Ez3+ohRyF8Njwy/6FH5gYJ75liUw==",
"dependencies": {
"@vue/runtime-core": "3.4.3",
"@vue/shared": "3.4.3",
"@vue/runtime-core": "3.4.15",
"@vue/shared": "3.4.15",
"csstype": "^3.1.3"
}
},
"node_modules/@vue/server-renderer": {
"version": "3.4.3",
"resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.4.3.tgz",
"integrity": "sha512-BUxt8oVGMKKsqSkM1uU3d3Houyfy4WAc2SpSQRebNd+XJGATVkW/rO129jkyL+kpB/2VRKzE63zwf5RtJ3XuZw==",
"version": "3.4.15",
"resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.4.15.tgz",
"integrity": "sha512-3HYzaidu9cHjrT+qGUuDhFYvF/j643bHC6uUN9BgM11DVy+pM6ATsG6uPBLnkwOgs7BpJABReLmpL3ZPAsUaqw==",
"dependencies": {
"@vue/compiler-ssr": "3.4.3",
"@vue/shared": "3.4.3"
"@vue/compiler-ssr": "3.4.15",
"@vue/shared": "3.4.15"
},
"peerDependencies": {
"vue": "3.4.3"
"vue": "3.4.15"
}
},
"node_modules/@vue/shared": {
"version": "3.4.3",
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.3.tgz",
"integrity": "sha512-rIwlkkP1n4uKrRzivAKPZIEkHiuwY5mmhMJ2nZKCBLz8lTUlE73rQh4n1OnnMurXt1vcUNyH4ZPfdh8QweTjpQ=="
"version": "3.4.15",
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.15.tgz",
"integrity": "sha512-KzfPTxVaWfB+eGcGdbSf4CWdaXcGDqckoeXUh7SB3fZdEtzPCK2Vq9B/lRRL3yutax/LWITz+SwvgyOxz5V75g=="
},
"node_modules/anymatch": {
"version": "3.1.3",
@ -1010,11 +1056,11 @@
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
},
"node_modules/axios": {
"version": "1.6.3",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.6.3.tgz",
"integrity": "sha512-fWyNdeawGam70jXSVlKl+SUNVcL6j6W79CuSIPfi6HnDUmSCH6gyUys/HrqHeA/wU0Az41rRgean494d0Jb+ww==",
"version": "1.6.5",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.6.5.tgz",
"integrity": "sha512-Ii012v05KEVuUoFWmMW/UQv9aRIc3ZwkWDcM+h5Il8izZCtRVpDUfwpoFf7eOtajT3QiGR4yDUx7lPqHJULgbg==",
"dependencies": {
"follow-redirects": "^1.15.0",
"follow-redirects": "^1.15.4",
"form-data": "^4.0.0",
"proxy-from-env": "^1.1.0"
}
@ -1114,9 +1160,9 @@
}
},
"node_modules/bootstrap-icons": {
"version": "1.11.2",
"resolved": "https://registry.npmjs.org/bootstrap-icons/-/bootstrap-icons-1.11.2.tgz",
"integrity": "sha512-TgdiPv+IM9tgDb+dsxrnGIyocsk85d2M7T0qIgkvPedZeoZfyeG/j+yiAE4uHCEayKef2RP05ahQ0/e9Sv75Wg==",
"version": "1.11.3",
"resolved": "https://registry.npmjs.org/bootstrap-icons/-/bootstrap-icons-1.11.3.tgz",
"integrity": "sha512-+3lpHrCw/it2/7lBL15VR0HEumaBss0+f/Lb6ZvHISn1mlK83jjFpooTLsMWbIjJMDjDjOExMsTxnXSIT4k4ww==",
"funding": [
{
"type": "github",
@ -1253,9 +1299,9 @@
}
},
"node_modules/core-js-pure": {
"version": "3.35.0",
"resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.35.0.tgz",
"integrity": "sha512-f+eRYmkou59uh7BPcyJ8MC76DiGhspj1KMxVIcF24tzP8NA9HVa1uC7BTW2tgx7E1QVCzDzsgp7kArrzhlz8Ew==",
"version": "3.35.1",
"resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.35.1.tgz",
"integrity": "sha512-zcIdi/CL3MWbBJYo5YCeVAAx+Sy9yJE9I3/u9LkFABwbeaPhTMRWraM8mYFp9jW5Z50hOy7FVzCc8dCrpZqtIQ==",
"hasInstallScript": true,
"funding": {
"type": "opencollective",
@ -1445,9 +1491,9 @@
}
},
"node_modules/follow-redirects": {
"version": "1.15.4",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.4.tgz",
"integrity": "sha512-Cr4D/5wlrb0z9dgERpUL3LrmPKVDsETIJhaCMeDfuFYcqa5bldGV6wBsAN6X/vxlXQtFBMrXdXxdL8CbDTGniw==",
"version": "1.15.5",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz",
"integrity": "sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==",
"funding": [
{
"type": "individual",
@ -1888,9 +1934,9 @@
"optional": true
},
"node_modules/node-abi": {
"version": "3.52.0",
"resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.52.0.tgz",
"integrity": "sha512-JJ98b02z16ILv7859irtXn4oUaFWADtvkzy2c0IAatNVX2Mc9Yoh8z6hZInn3QwvMEYhHuQloYi+TTQy67SIdQ==",
"version": "3.54.0",
"resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.54.0.tgz",
"integrity": "sha512-p7eGEiQil0YUV3ItH4/tBb781L5impVmmx2E9FRKF7d18XXzp4PGT2tdYMFY6wQqgxD0IwNZOiSJ0/K0fSi/OA==",
"optional": true,
"dependencies": {
"semver": "^7.3.5"
@ -1988,9 +2034,9 @@
}
},
"node_modules/postcss": {
"version": "8.4.32",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.32.tgz",
"integrity": "sha512-D/kj5JNu6oo2EIy+XL/26JEDTlIbB8hw85G8StOE6L74RQAVVP5rej6wxCNqyMbR4RkPfqvezVbPw81Ngd6Kcw==",
"version": "8.4.33",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.33.tgz",
"integrity": "sha512-Kkpbhhdjw2qQs2O2DGX+8m5OVqEcbB9HRBvuYM9pgrjEFUg30A9LmXNlTAUj4S9kgtGyrMbTzVjH7E+s5Re2yg==",
"funding": [
{
"type": "opencollective",
@ -2218,9 +2264,9 @@
"optional": true
},
"node_modules/sass": {
"version": "1.69.7",
"resolved": "https://registry.npmjs.org/sass/-/sass-1.69.7.tgz",
"integrity": "sha512-rzj2soDeZ8wtE2egyLXgOOHQvaC2iosZrkF6v3EUG+tBwEvhqUCzm0VP3k9gHF9LXbSrRhT5SksoI56Iw8NPnQ==",
"version": "1.70.0",
"resolved": "https://registry.npmjs.org/sass/-/sass-1.70.0.tgz",
"integrity": "sha512-uUxNQ3zAHeAx5nRFskBnrWzDUJrrvpCPD5FNAoRvTi0WwremlheES3tg+56PaVtCs5QDRX5CBLxxKMDJMEa1WQ==",
"dev": true,
"dependencies": {
"chokidar": ">=3.0.0 <4.0.0",
@ -2250,14 +2296,15 @@
}
},
"node_modules/set-function-length": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.1.1.tgz",
"integrity": "sha512-VoaqjbBJKiWtg4yRcKBQ7g7wnGnLV3M8oLvVWwOk2PdYY6PEFegR1vezXR0tw6fZGF9csVakIRjrJiy2veSBFQ==",
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.0.tgz",
"integrity": "sha512-4DBHDoyHlM1IRPGYcoxexgh67y4ueR53FKV1yyxwFMY7aCqcN/38M1+SwZ/qJQ8iLv7+ck385ot4CcisOAPT9w==",
"dependencies": {
"define-data-property": "^1.1.1",
"get-intrinsic": "^1.2.1",
"function-bind": "^1.1.2",
"get-intrinsic": "^1.2.2",
"gopd": "^1.0.1",
"has-property-descriptors": "^1.0.0"
"has-property-descriptors": "^1.0.1"
},
"engines": {
"node": ">= 0.4"
@ -2374,16 +2421,16 @@
}
},
"node_modules/swagger-client": {
"version": "3.24.6",
"resolved": "https://registry.npmjs.org/swagger-client/-/swagger-client-3.24.6.tgz",
"integrity": "sha512-vgolnwLjsLCLe3mA9yOuXqmslVzxRpjz0fTBWwPtDGvYSU8FMVra0FGevw+N2OQ80UE1rOqgv4Te0AfvzMyR8g==",
"version": "3.25.0",
"resolved": "https://registry.npmjs.org/swagger-client/-/swagger-client-3.25.0.tgz",
"integrity": "sha512-p143zWkIhgyh2E5+3HPFMlCw3WkV9RbX9HyftfBdiccCbOlmHdcJC0XEJZxcm+ZA+80DORs0F30/mzk7sx4iwA==",
"dependencies": {
"@babel/runtime-corejs3": "^7.22.15",
"@swagger-api/apidom-core": ">=0.83.0 <1.0.0",
"@swagger-api/apidom-error": ">=0.83.0 <1.0.0",
"@swagger-api/apidom-json-pointer": ">=0.83.0 <1.0.0",
"@swagger-api/apidom-ns-openapi-3-1": ">=0.83.0 <1.0.0",
"@swagger-api/apidom-reference": ">=0.83.0 <1.0.0",
"@swagger-api/apidom-core": ">=0.90.0 <1.0.0",
"@swagger-api/apidom-error": ">=0.90.0 <1.0.0",
"@swagger-api/apidom-json-pointer": ">=0.90.0 <1.0.0",
"@swagger-api/apidom-ns-openapi-3-1": ">=0.90.0 <1.0.0",
"@swagger-api/apidom-reference": ">=0.90.0 <1.0.0",
"cookie": "~0.6.0",
"deepmerge": "~4.3.0",
"fast-json-patch": "^3.0.0-1",
@ -2496,9 +2543,9 @@
}
},
"node_modules/types-ramda": {
"version": "0.29.6",
"resolved": "https://registry.npmjs.org/types-ramda/-/types-ramda-0.29.6.tgz",
"integrity": "sha512-VJoOk1uYNh9ZguGd3eZvqkdhD4hTGtnjRBUx5Zc0U9ftmnCgiWcSj/lsahzKunbiwRje1MxxNkEy1UdcXRCpYw==",
"version": "0.29.7",
"resolved": "https://registry.npmjs.org/types-ramda/-/types-ramda-0.29.7.tgz",
"integrity": "sha512-8KBxZGJwUF3MpRkkJauSpvfHXk8Ssq15QXGuCBTDGeKd9PfheokkC3wAKRV3djej9O31Qa5M7Owsg8hF0GjtAw==",
"dependencies": {
"ts-toolbelt": "^9.6.0"
}
@ -2526,15 +2573,15 @@
"optional": true
},
"node_modules/vue": {
"version": "3.4.3",
"resolved": "https://registry.npmjs.org/vue/-/vue-3.4.3.tgz",
"integrity": "sha512-GjN+culMAGv/mUbkIv8zMKItno8npcj5gWlXkSxf1SPTQf8eJ4A+YfHIvQFyL1IfuJcMl3soA7SmN1fRxbf/wA==",
"version": "3.4.15",
"resolved": "https://registry.npmjs.org/vue/-/vue-3.4.15.tgz",
"integrity": "sha512-jC0GH4KkWLWJOEQjOpkqU1bQsBwf4R1rsFtw5GQJbjHVKWDzO6P0nWWBTmjp1xSemAioDFj1jdaK1qa3DnMQoQ==",
"dependencies": {
"@vue/compiler-dom": "3.4.3",
"@vue/compiler-sfc": "3.4.3",
"@vue/runtime-dom": "3.4.3",
"@vue/server-renderer": "3.4.3",
"@vue/shared": "3.4.3"
"@vue/compiler-dom": "3.4.15",
"@vue/compiler-sfc": "3.4.15",
"@vue/runtime-dom": "3.4.15",
"@vue/server-renderer": "3.4.15",
"@vue/shared": "3.4.15"
},
"peerDependencies": {
"typescript": "*"
@ -2568,9 +2615,9 @@
}
},
"node_modules/web-streams-polyfill": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.2.1.tgz",
"integrity": "sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q==",
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.2.tgz",
"integrity": "sha512-3pRGuxRF5gpuZc0W+EpwQRmCD7gRqcDOMt688KmdlDAgAyaB1XlN0zq2njfDNm44XVdIouE7pZ6GzbdyH47uIQ==",
"engines": {
"node": ">= 8"
}

@ -3,7 +3,7 @@
"version": "0.0.0",
"private": true,
"scripts": {
"build": "node esbuild.config.mjs",
"build": "MINIFY=true node esbuild.config.mjs",
"watch": "WATCH=true node esbuild.config.mjs",
"package": "MINIFY=true node esbuild.config.mjs",
"update-caniemail": "wget -O utils/html-check/caniemail-data.json https://www.caniemail.com/api/data.json"

@ -157,7 +157,13 @@ func Run() {
}
}
err = smtp.SendMail(SMTPAddr, nil, FromAddr, addresses, body)
from, err := mail.ParseAddress(FromAddr)
if err != nil {
fmt.Fprintln(os.Stderr, "invalid from address")
os.Exit(11)
}
err = smtp.SendMail(SMTPAddr, nil, from.Address, addresses, body)
if err != nil {
fmt.Fprintln(os.Stderr, "error sending mail")
logger.Log().Fatal(err)

@ -15,6 +15,7 @@ import (
"github.com/axllent/mailpit/internal/htmlcheck"
"github.com/axllent/mailpit/internal/linkcheck"
"github.com/axllent/mailpit/internal/logger"
"github.com/axllent/mailpit/internal/spamassassin"
"github.com/axllent/mailpit/internal/storage"
"github.com/axllent/mailpit/internal/tools"
"github.com/axllent/mailpit/server/smtpd"
@ -821,6 +822,56 @@ func LinkCheck(w http.ResponseWriter, r *http.Request) {
_, _ = w.Write(bytes)
}
// SpamAssassinCheck returns a summary of SpamAssassin results (if enabled)
func SpamAssassinCheck(w http.ResponseWriter, r *http.Request) {
// swagger:route GET /api/v1/message/{ID}/sa-check Other SpamAssassinCheck
//
// # SpamAssassin check (beta)
//
// Returns the SpamAssassin (if enabled) summary of the message.
//
// NOTE: This feature is currently in beta and is documented for reference only.
// Please do not integrate with it (yet) as there may be changes.
//
// Produces:
// - application/json
//
// Schemes: http, https
//
// Responses:
// 200: SpamAssassinResponse
// default: ErrorResponse
vars := mux.Vars(r)
id := vars["id"]
if id == "latest" {
var err error
id, err = storage.LatestID(r)
if err != nil {
w.WriteHeader(404)
fmt.Fprint(w, err.Error())
return
}
}
msg, err := storage.GetMessageRaw(id)
if err != nil {
fourOFour(w)
return
}
summary, err := spamassassin.Check(msg)
if err != nil {
httpError(w, err.Error())
return
}
bytes, _ := json.Marshal(summary)
w.Header().Add("Content-Type", "application/json")
_, _ = w.Write(bytes)
}
// FourOFour returns a basic 404 message
func fourOFour(w http.ResponseWriter) {
w.Header().Set("Referrer-Policy", "no-referrer")

@ -3,6 +3,7 @@ package apiv1
import (
"github.com/axllent/mailpit/internal/htmlcheck"
"github.com/axllent/mailpit/internal/linkcheck"
"github.com/axllent/mailpit/internal/spamassassin"
"github.com/axllent/mailpit/internal/storage"
)
@ -50,3 +51,6 @@ type HTMLCheckResponse = htmlcheck.Response
// LinkCheckResponse summary
type LinkCheckResponse = linkcheck.Response
// SpamAssassinResponse summary
type SpamAssassinResponse = spamassassin.Result

@ -146,6 +146,16 @@ type linkCheckParams struct {
Follow string `json:"follow"`
}
// swagger:parameters SpamAssassinCheck
type spamAssassinCheckParams struct {
// Message database ID or "latest"
//
// in: path
// description: Message database ID or "latest"
// required: true
ID string
}
// Binary data response inherits the attachment's content type
// swagger:response BinaryResponse
type binaryResponse string

@ -26,6 +26,9 @@ type webUIConfiguration struct {
// Whether the HTML check has been globally disabled
DisableHTMLCheck bool
// Whether SpamAssassin is enabled
SpamAssassin bool
}
// WebUIConfig returns configuration settings for the web UI.
@ -55,6 +58,7 @@ func WebUIConfig(w http.ResponseWriter, _ *http.Request) {
}
conf.DisableHTMLCheck = config.DisableHTMLCheck
conf.SpamAssassin = config.EnableSpamAssassin != ""
bytes, _ := json.Marshal(conf)

@ -123,6 +123,9 @@ func apiRoutes() *mux.Router {
r.HandleFunc(config.Webroot+"api/v1/message/{id}/html-check", middleWareFunc(apiv1.HTMLCheck)).Methods("GET")
}
r.HandleFunc(config.Webroot+"api/v1/message/{id}/link-check", middleWareFunc(apiv1.LinkCheck)).Methods("GET")
if config.EnableSpamAssassin != "" {
r.HandleFunc(config.Webroot+"api/v1/message/{id}/sa-check", middleWareFunc(apiv1.SpamAssassinCheck)).Methods("GET")
}
r.HandleFunc(config.Webroot+"api/v1/message/{id}", middleWareFunc(apiv1.GetMessage)).Methods("GET")
r.HandleFunc(config.Webroot+"api/v1/info", middleWareFunc(apiv1.AppInfo)).Methods("GET")
r.HandleFunc(config.Webroot+"api/v1/webui", middleWareFunc(apiv1.WebUIConfig)).Methods("GET")

@ -18,6 +18,11 @@ import (
"github.com/mhale/smtpd"
)
var (
// DisableReverseDNS allows rDNS to be disabled
DisableReverseDNS bool
)
func mailHandler(origin net.Addr, from string, to []string, data []byte) error {
if !config.SMTPStrictRFCHeaders {
// replace all <CR><CR><LF> (\r\r\n) with <CR><LF> (\r\n)
@ -191,14 +196,15 @@ func Listen() error {
func listenAndServe(addr string, handler smtpd.Handler, authHandler smtpd.AuthHandler) error {
srv := &smtpd.Server{
Addr: addr,
Handler: handler,
HandlerRcpt: handlerRcpt,
Appname: "Mailpit",
Hostname: "",
AuthHandler: nil,
AuthRequired: false,
MaxRecipients: config.SMTPMaxRecipients,
Addr: addr,
Handler: handler,
HandlerRcpt: handlerRcpt,
Appname: "Mailpit",
Hostname: "",
AuthHandler: nil,
AuthRequired: false,
MaxRecipients: config.SMTPMaxRecipients,
DisableReverseDNS: DisableReverseDNS,
}
if config.SMTPAuthAllowInsecure {

@ -64,6 +64,11 @@
}
}
.link {
@extend a;
cursor: pointer;
}
.loader {
position: fixed;
top: 0;
@ -124,6 +129,14 @@
}
}
.text-spaces-nowrap {
white-space: pre;
}
.text-spaces {
white-space: pre-wrap;
}
#nav-plain-text .text-view,
#nav-source {
white-space: pre;
@ -146,6 +159,7 @@
padding-right: 1.5rem;
font-weight: normal;
vertical-align: top;
min-width: 120px;
}
td {
@ -319,6 +333,12 @@ body.blur {
}
}
.dropdown-menu.checks {
.dropdown-item {
min-width: 190px;
}
}
// bootstrap5-tags
.tags-badge {
display: flex;

@ -122,13 +122,13 @@ export default {
{{ getRelativeCreated(message) }}
</div>
<div class="text-truncate d-lg-none privacy">
<span v-if="message.From" :title="message.From.Address">{{
<span v-if="message.From" :title="'From: ' + message.From.Address">{{
message.From.Name ?
message.From.Name : message.From.Address
}}</span>
</div>
<div class="text-truncate d-none d-lg-block privacy">
<b v-if="message.From" :title="message.From.Address">{{
<b v-if="message.From" :title="'From: ' + message.From.Address">{{
message.From.Name ?
message.From.Name : message.From.Address
}}</b>
@ -141,7 +141,7 @@ export default {
</div>
</div>
<div class="col-lg-6 col-xxl-7 mt-2 mt-lg-0">
<div class="subject text-truncate">
<div class="subject text-truncate text-spaces-nowrap">
<b>{{ message.Subject != "" ? message.Subject : "[ no subject ]" }}</b>
</div>
<div v-if="message.Snippet != ''" class="small text-muted text-truncate">

@ -1,11 +1,13 @@
<script>
import Attachments from './Attachments.vue'
import HTMLCheck from './HTMLCheck.vue'
import Headers from './Headers.vue'
import HTMLCheck from './HTMLCheck.vue'
import LinkCheck from './LinkCheck.vue'
import SpamAssassin from './SpamAssassin.vue'
import Prism from 'prismjs'
import Tags from 'bootstrap5-tags'
import { Tooltip } from 'bootstrap'
import commonMixins from '../../mixins/CommonMixins'
import { mailbox } from '../../stores/mailbox'
@ -19,6 +21,7 @@ export default {
Headers,
HTMLCheck,
LinkCheck,
SpamAssassin,
},
mixins: [commonMixins],
@ -34,7 +37,10 @@ export default {
htmlScore: false,
htmlScoreColor: false,
linkCheckErrors: false,
spamScore: false,
spamScoreColor: false,
showMobileButtons: false,
showUnsubscribe: false,
scaleHTMLPreview: 'display',
// keys names match bootstrap icon names
responsiveSizes: {
@ -117,6 +123,9 @@ export default {
})
})
const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip"]');
[...tooltipTriggerList].map(tooltipTriggerEl => new Tooltip(tooltipTriggerEl))
// delay 0.2s until vue has rendered the iframe content
window.setTimeout(function () {
let p = document.getElementById('preview-html')
@ -230,7 +239,7 @@ export default {
<th class="small">From</th>
<td class="privacy">
<span v-if="message.From">
<span v-if="message.From.Name">{{ message.From.Name + " " }}</span>
<span v-if="message.From.Name" class="text-spaces">{{ message.From.Name + " " }}</span>
<span v-if="message.From.Address" class="small">
&lt;<a :href="searchURI(message.From.Address)" class="text-body">
{{ message.From.Address }}
@ -240,15 +249,23 @@ export default {
<span v-else>
[ Unknown ]
</span>
<span v-if="message.ListUnsubscribe.Header != ''" class="small ms-3 link"
:title="showUnsubscribe ? 'Hide unsubscribe information' : 'Show unsubscribe information'"
@click="showUnsubscribe = !showUnsubscribe">
Unsubscribe
<i class="bi bi bi-info-circle"
:class="{ 'text-danger': message.ListUnsubscribe.Errors != '' }"></i>
</span>
</td>
</tr>
<tr class="small">
<th>To</th>
<td class="privacy">
<span v-if="message.To && message.To.length" v-for="(t, i) in message.To">
<span v-if="message.To && message.To.length" v-for="( t, i ) in message.To ">
<template v-if="i > 0">, </template>
<span>
{{ t.Name }}
<span class="text-spaces">{{ t.Name }}</span>
&lt;<a :href="searchURI(t.Address)" class="text-body">
{{ t.Address }}
</a>&gt;
@ -260,9 +277,9 @@ export default {
<tr v-if="message.Cc && message.Cc.length" class="small">
<th>Cc</th>
<td class="privacy">
<span v-for="(t, i) in message.Cc">
<span v-for="( t, i ) in message.Cc ">
<template v-if="i > 0">,</template>
{{ t.Name }}
<span class="text-spaces">{{ t.Name }}</span>
&lt;<a :href="searchURI(t.Address)" class="text-body">
{{ t.Address }}
</a>&gt;
@ -272,9 +289,9 @@ export default {
<tr v-if="message.Bcc && message.Bcc.length" class="small">
<th>Bcc</th>
<td class="privacy">
<span v-for="(t, i) in message.Bcc">
<span v-for="( t, i ) in message.Bcc ">
<template v-if="i > 0">,</template>
{{ t.Name }}
<span class="text-spaces">{{ t.Name }}</span>
&lt;<a :href="searchURI(t.Address)" class="text-body">
{{ t.Address }}
</a>&gt;
@ -284,9 +301,9 @@ export default {
<tr v-if="message.ReplyTo && message.ReplyTo.length" class="small">
<th class="text-nowrap">Reply-To</th>
<td class="privacy text-body-secondary text-break">
<span v-for="(t, i) in message.ReplyTo">
<span v-for="( t, i ) in message.ReplyTo ">
<template v-if="i > 0">,</template>
{{ t.Name }}
<span class="text-spaces">{{ t.Name }}</span>
&lt;<a :href="searchURI(t.Address)" class="text-body-secondary">
{{ t.Address }}
</a>&gt;
@ -305,7 +322,7 @@ export default {
<tr>
<th class="small">Subject</th>
<td>
<strong v-if="message.Subject != ''">{{ message.Subject }}</strong>
<strong v-if="message.Subject != ''" class="text-spaces">{{ message.Subject }}</strong>
<small class="text-body-secondary" v-else>[ no subject ]</small>
</td>
</tr>
@ -324,11 +341,34 @@ export default {
data-separator="|,|">
<option value="">Type a tag...</option>
<!-- you need at least one option with the placeholder -->
<option v-for="t in mailbox.tags" :value="t">{{ t }}</option>
<option v-for=" t in mailbox.tags " :value="t">{{ t }}</option>
</select>
<div class="invalid-feedback">Invalid tag name</div>
</td>
</tr>
<tr v-if="message.ListUnsubscribe.Header != ''" class="small"
:class="showUnsubscribe ? '' : 'd-none'">
<th>Unsubscribe</th>
<td>
<span v-if="message.ListUnsubscribe.Links.length" class="text-secondary small me-2">
<template v-for="(u, i) in message.ListUnsubscribe.Links">
<template v-if="i > 0">, </template>
&lt;{{ u }}&gt;
</template>
</span>
<i class="bi bi-info-circle text-success me-2 link"
v-if="message.ListUnsubscribe.HeaderPost != ''" data-bs-toggle="tooltip"
data-bs-placement="top" data-bs-custom-class="custom-tooltip"
:data-bs-title="'List-Unsubscribe-Post: ' + message.ListUnsubscribe.HeaderPost">
</i>
<i class="bi bi-exclamation-circle text-danger link"
v-if="message.ListUnsubscribe.Errors != ''" data-bs-toggle="tooltip"
data-bs-placement="top" data-bs-custom-class="custom-tooltip"
:data-bs-title="message.ListUnsubscribe.Errors">
</i>
</td>
</tr>
</tbody>
</table>
</div>
@ -386,13 +426,14 @@ export default {
<button class="nav-link dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">
Checks
</button>
<ul class="dropdown-menu">
<ul class="dropdown-menu checks">
<li>
<button class="dropdown-item" id="nav-html-check-tab" data-bs-toggle="tab"
data-bs-target="#nav-html-check" type="button" role="tab" aria-controls="nav-html"
aria-selected="false" v-if="!mailbox.uiConfig.DisableHTMLCheck && message.HTML != ''">
HTML Check
<span class="badge rounded-pill p-1" :class="htmlScoreColor" v-if="htmlScore !== false">
<span class="badge rounded-pill p-1 float-end" :class="htmlScoreColor"
v-if="htmlScore !== false">
<small>{{ Math.floor(htmlScore) }}%</small>
</span>
</button>
@ -402,12 +443,25 @@ export default {
data-bs-target="#nav-link-check" type="button" role="tab" aria-controls="nav-link-check"
aria-selected="false">
Link Check
<i class="bi bi-check-all text-success" v-if="linkCheckErrors === 0"></i>
<span class="badge rounded-pill bg-danger" v-else-if="linkCheckErrors > 0">
<span class="badge rounded-pill bg-success float-end" v-if="linkCheckErrors === 0">
<small>0</small>
</span>
<span class="badge rounded-pill bg-danger float-end" v-else-if="linkCheckErrors > 0">
<small>{{ formatNumber(linkCheckErrors) }}</small>
</span>
</button>
</li>
<li v-if="mailbox.uiConfig.SpamAssassin">
<button class="dropdown-item" id="nav-spam-check-tab" data-bs-toggle="tab"
data-bs-target="#nav-spam-check" type="button" role="tab" aria-controls="nav-html"
aria-selected="false">
Spam Analysis
<span class="badge rounded-pill float-end" :class="spamScoreColor"
v-if="spamScore !== false">
<small>{{ spamScore }}</small>
</span>
</button>
</li>
</ul>
</div>
<button class="d-none d-xl-inline-block nav-link position-relative" id="nav-html-check-tab"
@ -427,9 +481,17 @@ export default {
<small>{{ formatNumber(linkCheckErrors) }}</small>
</span>
</button>
<button class="d-none d-xl-inline-block nav-link position-relative" id="nav-spam-check-tab"
data-bs-toggle="tab" data-bs-target="#nav-spam-check" type="button" role="tab" aria-controls="nav-html"
aria-selected="false" v-if="mailbox.uiConfig.SpamAssassin">
Spam Analysis
<span class="badge rounded-pill" :class="spamScoreColor" v-if="spamScore !== false">
<small>{{ spamScore }}</small>
</span>
</button>
<div class="d-none d-lg-block ms-auto me-3" v-if="showMobileButtons">
<template v-for="vals, key in responsiveSizes">
<template v-for=" vals, key in responsiveSizes ">
<button class="btn" :disabled="scaleHTMLPreview == key" :title="'Switch to ' + key + ' view'"
v-on:click="scaleHTMLPreview = key">
<i class="bi" :class="'bi-' + key"></i>
@ -472,6 +534,11 @@ export default {
<HTMLCheck v-if="!mailbox.uiConfig.DisableHTMLCheck && message.HTML != ''" :message="message"
@setHtmlScore="(n) => htmlScore = n" @set-badge-style="(v) => htmlScoreColor = v" />
</div>
<div class="tab-pane fade" id="nav-spam-check" role="tabpanel" aria-labelledby="nav-spam-check-tab"
tabindex="0">
<SpamAssassin v-if="mailbox.uiConfig.SpamAssassin" :message="message" @setSpamScore="(n) => spamScore = n"
@set-badge-style="(v) => spamScoreColor = v" />
</div>
<div class="tab-pane fade" id="nav-link-check" role="tabpanel" aria-labelledby="nav-html-check-tab"
tabindex="0">
<LinkCheck :message="message" @setLinkErrors="(n) => linkCheckErrors = n" />

@ -0,0 +1,297 @@
<script>
import Donut from 'vue-css-donut-chart/src/components/Donut.vue'
import axios from 'axios'
import commonMixins from '../../mixins/CommonMixins'
export default {
props: {
message: Object,
},
components: {
Donut,
},
emits: ["setSpamScore", "setBadgeStyle"],
mixins: [commonMixins],
data() {
return {
error: false,
check: false,
}
},
mounted() {
this.doCheck()
},
watch: {
message: {
handler() {
this.$emit('setSpamScore', false)
this.doCheck()
},
deep: true
},
},
methods: {
doCheck: function () {
this.check = false
let self = this
// ignore any error, do not show loader
axios.get(self.resolve('/api/v1/message/' + self.message.ID + '/sa-check'), null)
.then(function (result) {
self.check = result.data
self.error = false
self.setIcons()
})
.catch(function (error) {
// handle error
if (error.response && error.response.data) {
// The request was made and the server responded with a status code
// that falls out of the range of 2xx
if (error.response.data.Error) {
self.error = error.response.data.Error
} else {
self.error = error.response.data
}
} else if (error.request) {
// The request was made but no response was received
// `error.request` is an instance of XMLHttpRequest in the browser and an instance of
// http.ClientRequest in node.js
self.error = 'Error sending data to the server. Please try again.'
} else {
// Something happened in setting up the request that triggered an Error
self.error = error.message
}
})
},
badgeStyle: function (ignorePadding = false) {
let badgeStyle = 'bg-success'
if (this.check.Error) {
badgeStyle = 'bg-warning text-primary'
}
else if (this.check.IsSpam) {
badgeStyle = 'bg-danger'
} else if (this.check.Score >= 4) {
badgeStyle = 'bg-warning text-primary'
}
if (!ignorePadding && String(this.check.Score).includes('.')) {
badgeStyle += " p-1"
}
return badgeStyle
},
setIcons: function () {
let score = this.check.Score
if (this.check.Error && this.check.Error != '') {
score = '!'
}
let badgeStyle = this.badgeStyle()
this.$emit('setBadgeStyle', badgeStyle)
this.$emit('setSpamScore', score)
},
},
computed: {
graphSections: function () {
let score = this.check.Score
let p = Math.round(score / 5 * 100)
if (p > 100) {
p = 100
} else if (p < 0) {
p = 0
}
let c = '#ffc107'
if (this.check.IsSpam) {
c = '#dc3545'
}
return [
{
label: score + ' / 5',
value: p,
color: c
},
];
},
}
}
</script>
<template>
<div class="row mb-3 w-100 align-items-center">
<div class="col">
<h4 class="mb-0">Spam Analysis</h4>
</div>
<div class="col-auto">
<button class="btn btn-outline-secondary" data-bs-toggle="modal" data-bs-target="#AboutSpamAnalysis">
<i class="bi bi-info-circle-fill"></i>
Help
</button>
</div>
</div>
<template v-if="error || check.Error != ''">
<p>Your message could not be checked</p>
<div class="alert alert-warning" v-if="error">
{{ error }}
</div>
<div class="alert alert-warning" v-else>
There was an error contacting the configured SpamAssassin server: {{ check.Error }}
</div>
</template>
<template v-else-if="check">
<div class="row w-100 mt-5">
<div class="col-xl-5 mb-2">
<Donut :sections="graphSections" background="var(--bs-body-bg)" :size="230" unit="px" :thickness="20"
:total="100" :start-angle="270" :auto-adjust-text-size="true" foreground="#198754">
<h2 class="m-0" :class="scoreColor" @click="scrollToWarnings">
{{ check.Score }} / 5
</h2>
<div class="text-body mt-2">
<span v-if="check.IsSpam" class="text-white badge rounded-pill bg-danger p-2">Spam</span>
<span v-else class="badge rounded-pill p-2" :class="badgeStyle()">Not spam</span>
</div>
</Donut>
</div>
<div class="col-xl-7">
<div class="row w-100 py-2 border-bottom">
<div class="col-2 col-lg-1">
<strong>Score</strong>
</div>
<div class="col-10 col-lg-5">
<strong>Rule <span class="d-none d-lg-inline">name</span></strong>
</div>
<div class="col-auto d-none d-lg-block">
<strong>Description</strong>
</div>
</div>
<div class="row w-100 py-2 border-bottom small" v-for="r in check.Rules">
<div class="col-2 col-lg-1">
{{ r.Score }}
</div>
<div class="col-10 col-lg-5">
{{ r.Name }}
</div>
<div class="col-auto col-lg-6 mt-2 mt-lg-0 offset-2 offset-lg-0">
{{ r.Description }}
</div>
</div>
</div>
</div>
</template>
<div class="modal fade" id="AboutSpamAnalysis" tabindex="-1" aria-labelledby="AboutSpamAnalysisLabel"
aria-hidden="true">
<div class="modal-dialog modal-lg modal-dialog-scrollable">
<div class="modal-content">
<div class="modal-header">
<h1 class="modal-title fs-5" id="AboutSpamAnalysisLabel">About Spam Analysis</h1>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<p>
Spam Analysis is currently in beta. Constructive feedback is welcome via
<a href="https://github.com/axllent/mailpit/issues" target="_blank">GitHub</a>.
</p>
<div class="accordion" id="SpamAnalysisAboutAccordion">
<div class="accordion-item">
<h2 class="accordion-header">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse"
data-bs-target="#col1" aria-expanded="false" aria-controls="col1">
What is Spam Analysis?
</button>
</h2>
<div id="col1" class="accordion-collapse collapse" data-bs-parent="#SpamAnalysisAboutAccordion">
<div class="accordion-body">
<p>
Mailpit integrates with SpamAssassin to provide you with some insight into the
"spamminess" of your messages. It sends your complete message (including any
attachments) to a running SpamAssassin server and then displays the results returned
by SpamAssassin.
</p>
</div>
</div>
</div>
<div class="accordion-item">
<h2 class="accordion-header">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse"
data-bs-target="#col2" aria-expanded="false" aria-controls="col2">
How does the point system work?
</button>
</h2>
<div id="col2" class="accordion-collapse collapse" data-bs-parent="#SpamAnalysisAboutAccordion">
<div class="accordion-body">
<p>
The default spam threshold is <code>5</code>, meaning any score lower than 5 is
considered ham (not spam), and any score of 5 or above is spam.
</p>
<p>
SpamAssassin will also return the tests which are triggered by the message. These
tests can differ depending on the configuration of your SpamAssassin server. The
total of this score makes up the the "spamminess" of the message.
</p>
</div>
</div>
</div>
<div class="accordion-item">
<h2 class="accordion-header">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse"
data-bs-target="#col3" aria-expanded="false" aria-controls="col3">
But I don't agree with the results...
</button>
</h2>
<div id="col3" class="accordion-collapse collapse" data-bs-parent="#SpamAnalysisAboutAccordion">
<div class="accordion-body">
<p>
Mailpit does not manipulate the results nor determine the "spamminess" of
your message. The result is what SpamAssassin returns, and it entirely
dependent on how SpamAssassin is set up and optionally trained.
</p>
<p>
This tool is simply provided as an aid to assist you. If you are running your own
instance of SpamAssassin, then you look into your SpamAssassin configuration.
</p>
</div>
</div>
</div>
<div class="accordion-item">
<h2 class="accordion-header">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse"
data-bs-target="#col4" aria-expanded="false" aria-controls="col4">
Where can I find more information about the triggered rules?
</button>
</h2>
<div id="col4" class="accordion-collapse collapse" data-bs-parent="#SpamAnalysisAboutAccordion">
<div class="accordion-body">
<p>
Unfortunately the current <a href="https://spamassassin.apache.org/"
target="_blank">SpamAssassin website</a> no longer contains any relative
documentation
about these, most likely because the rules come from different locations and change
often. You will need to search the internet for these yourself.
</p>
</div>
</div>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
</template>

@ -366,6 +366,43 @@
}
}
},
"/api/v1/message/{ID}/sa-check": {
"get": {
"description": "Returns the SpamAssassin (if enabled) summary of the message.\n\nNOTE: This feature is currently in beta and is documented for reference only.\nPlease do not integrate with it (yet) as there may be changes.",
"produces": [
"application/json"
],
"schemes": [
"http",
"https"
],
"tags": [
"Other"
],
"summary": "SpamAssassin check (beta)",
"operationId": "SpamAssassinCheck",
"parameters": [
{
"type": "string",
"description": "Message database ID or \"latest\"",
"name": "ID",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "SpamAssassinResponse",
"schema": {
"$ref": "#/definitions/SpamAssassinResponse"
}
},
"default": {
"$ref": "#/responses/ErrorResponse"
}
}
}
},
"/api/v1/messages": {
"get": {
"description": "Returns messages from the mailbox ordered from newest to oldest.",
@ -1299,6 +1336,54 @@
},
"x-go-package": "github.com/axllent/mailpit/server/apiv1"
},
"Rule": {
"description": "Rule struct",
"type": "object",
"properties": {
"Description": {
"description": "SpamAssassin rule description",
"type": "string"
},
"Name": {
"description": "SpamAssassin rule name",
"type": "string"
},
"Score": {
"description": "Spam rule score",
"type": "number",
"format": "double"
}
},
"x-go-package": "github.com/axllent/mailpit/internal/spamassassin"
},
"SpamAssassinResponse": {
"description": "Result is a SpamAssassin result",
"type": "object",
"properties": {
"Error": {
"description": "If populated will return an error string",
"type": "string"
},
"IsSpam": {
"description": "Whether the message is spam or not",
"type": "boolean"
},
"Rules": {
"description": "Spam rules triggered",
"type": "array",
"items": {
"$ref": "#/definitions/Rule"
}
},
"Score": {
"description": "Total spam score based on triggered rules",
"type": "number",
"format": "double"
}
},
"x-go-name": "Result",
"x-go-package": "github.com/axllent/mailpit/internal/spamassassin"
},
"WebUIConfiguration": {
"description": "Response includes global web UI settings",
"type": "object",
@ -1328,6 +1413,10 @@
"type": "string"
}
}
},
"SpamAssassin": {
"description": "Whether SpamAssassin is enabled",
"type": "boolean"
}
},
"x-go-name": "webUIConfiguration",