diff --git a/CHANGELOG.md b/CHANGELOG.md index 9901869..fa59e81 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 '` format + + ## [v1.12.1] ### Chore diff --git a/README.md b/README.md index d98b990..938684b 100644 --- a/README.md +++ b/README.md @@ -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/) diff --git a/cmd/root.go b/cmd/root.go index 03a3878..1eb06de 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -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 ") 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 } diff --git a/config/config.go b/config/config.go index 15aa286..1694011 100644 --- a/config/config.go +++ b/config/config.go @@ -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 : 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 != "" { diff --git a/go.mod b/go.mod index 33f550b..9b6190f 100644 --- a/go.mod +++ b/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 diff --git a/go.sum b/go.sum index decffd2..10ad90e 100644 --- a/go.sum +++ b/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= diff --git a/internal/spamassassin/postmark/postmark.go b/internal/spamassassin/postmark/postmark.go new file mode 100644 index 0000000..854a232 --- /dev/null +++ b/internal/spamassassin/postmark/postmark.go @@ -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 "" +} diff --git a/internal/spamassassin/spamassassin.go b/internal/spamassassin/spamassassin.go new file mode 100644 index 0000000..57f0cc9 --- /dev/null +++ b/internal/spamassassin/spamassassin.go @@ -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 ":" 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 +} diff --git a/internal/spamassassin/spamc/spamc.go b/internal/spamassassin/spamc/spamc.go new file mode 100644 index 0000000..195fc52 --- /dev/null +++ b/internal/spamassassin/spamc/spamc.go @@ -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 +} diff --git a/internal/storage/database.go b/internal/storage/database.go index 6ae1276..ec8acfe 100644 --- a/internal/storage/database.go +++ b/internal/storage/database.go @@ -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 diff --git a/internal/storage/structs.go b/internal/storage/structs.go index 2ad9721..ec92ec9 100644 --- a/internal/storage/structs.go +++ b/internal/storage/structs.go @@ -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 +} diff --git a/internal/tools/listunsubscribeparser.go b/internal/tools/listunsubscribeparser.go new file mode 100644 index 0000000..ee5e573 --- /dev/null +++ b/internal/tools/listunsubscribeparser.go @@ -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 +} diff --git a/internal/tools/tools_test.go b/internal/tools/tools_test.go index ad2321d..d98529e 100644 --- a/internal/tools/tools_test.go +++ b/internal/tools/tools_test.go @@ -69,3 +69,51 @@ func TestSnippets(t *testing.T) { } } } + +func TestListUnsubscribeParser(t *testing.T) { + tests := map[string]bool{} + + // should pass + tests[""] = true + tests[""] = true + tests[""] = true + tests[", "] = true + tests[", "] = true + tests[", "] = true + tests[" , "] = true + tests[" ,"] = true + tests[","] = true + tests[` , + `] = true + tests[""] = true + tests["(Use this command to get off the list) "] = true + tests[" (Use this command to get off the list)"] = true + tests["(Use this command to get off the list) , (Click this link to unsubscribe) "] = true + + // should fail + tests["mailto:unsubscribe@example.com"] = false // no <> + tests[""] = false // :: + tests["https://example.com/"] = false // no <> + tests["mailto:unsubscribe@example.com, "] = false // no <> + tests[""] = false // capitals + tests[", "] = false // two emails + tests[", "] = false // two links + tests[", , "] = false // two links + tests[", "] = false // no mailto || http(s) + tests[", "] = false // space + tests[""] = false // space + tests[""] = 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() + } + } +} diff --git a/package-lock.json b/package-lock.json index 6ccc47d..dd011df 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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" } diff --git a/package.json b/package.json index 1ae34d4..7be3edf 100644 --- a/package.json +++ b/package.json @@ -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" diff --git a/sendmail/cmd/cmd.go b/sendmail/cmd/cmd.go index 44e9501..87c1155 100644 --- a/sendmail/cmd/cmd.go +++ b/sendmail/cmd/cmd.go @@ -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) diff --git a/server/apiv1/api.go b/server/apiv1/api.go index 533a6fc..453592e 100644 --- a/server/apiv1/api.go +++ b/server/apiv1/api.go @@ -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") diff --git a/server/apiv1/structs.go b/server/apiv1/structs.go index 8bf1290..dfe9e0a 100644 --- a/server/apiv1/structs.go +++ b/server/apiv1/structs.go @@ -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 diff --git a/server/apiv1/swagger.go b/server/apiv1/swagger.go index 7bdd36a..8dffcf0 100644 --- a/server/apiv1/swagger.go +++ b/server/apiv1/swagger.go @@ -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 diff --git a/server/apiv1/webui.go b/server/apiv1/webui.go index a7ac0cd..d5a79aa 100644 --- a/server/apiv1/webui.go +++ b/server/apiv1/webui.go @@ -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) diff --git a/server/server.go b/server/server.go index a91c686..7f601d8 100644 --- a/server/server.go +++ b/server/server.go @@ -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") diff --git a/server/smtpd/smtpd.go b/server/smtpd/smtpd.go index 70d8867..282e369 100644 --- a/server/smtpd/smtpd.go +++ b/server/smtpd/smtpd.go @@ -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 (\r\r\n) with (\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 { diff --git a/server/ui-src/assets/styles.scss b/server/ui-src/assets/styles.scss index 44593d3..d3d4b2f 100644 --- a/server/ui-src/assets/styles.scss +++ b/server/ui-src/assets/styles.scss @@ -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; diff --git a/server/ui-src/components/ListMessages.vue b/server/ui-src/components/ListMessages.vue index e0f2ee7..e35d0ee 100644 --- a/server/ui-src/components/ListMessages.vue +++ b/server/ui-src/components/ListMessages.vue @@ -122,13 +122,13 @@ export default { {{ getRelativeCreated(message) }}
- {{ + {{ message.From.Name ? message.From.Name : message.From.Address }}
- {{ + {{ message.From.Name ? message.From.Name : message.From.Address }} @@ -141,7 +141,7 @@ export default {
-
+
{{ message.Subject != "" ? message.Subject : "[ no subject ]" }}
diff --git a/server/ui-src/components/message/Message.vue b/server/ui-src/components/message/Message.vue index eb2c67a..2a04ce9 100644 --- a/server/ui-src/components/message/Message.vue +++ b/server/ui-src/components/message/Message.vue @@ -1,11 +1,13 @@ + + diff --git a/server/ui/api/v1/swagger.json b/server/ui/api/v1/swagger.json index f1c61a3..b49e4ca 100644 --- a/server/ui/api/v1/swagger.json +++ b/server/ui/api/v1/swagger.json @@ -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",