1
0
mirror of https://github.com/axllent/mailpit.git synced 2025-06-15 00:05:15 +02:00

Feature: Add Chaos functionality to test integration handling of SMTP error responses (#402, #110, #144 & #268)

Closes #405
This commit is contained in:
Ralph Slooten
2025-01-25 12:17:15 +13:00
parent 2a6ab0476b
commit 4d86297169
10 changed files with 636 additions and 55 deletions

View File

@ -48,6 +48,7 @@ including image thumbnails), including optional [HTTPS](https://mailpit.axllent.
- [SMTP relaying](https://mailpit.axllent.org/docs/configuration/smtp-relay/) (message release) - relay messages via a different SMTP server including an optional allowlist of accepted recipients - [SMTP relaying](https://mailpit.axllent.org/docs/configuration/smtp-relay/) (message release) - relay messages via a different SMTP server including an optional allowlist of accepted recipients
- Fast message [storing & processing](https://mailpit.axllent.org/docs/configuration/email-storage/) - ingesting 100-200 emails per second over SMTP depending on CPU, network speed & email size, - Fast message [storing & processing](https://mailpit.axllent.org/docs/configuration/email-storage/) - ingesting 100-200 emails per second over SMTP depending on CPU, network speed & email size,
easily handling tens of thousands of emails, with automatic email pruning (by default keeping the most recent 500 emails) easily handling tens of thousands of emails, with automatic email pruning (by default keeping the most recent 500 emails)
- [Chaos](ttps://mailpit.axllent.org/docs/integration/chaos/) feature to enable configurable SMTP errors to test application resilience
- `List-Unsubscribe` syntax validation - `List-Unsubscribe` syntax validation
- Optional [webhook](https://mailpit.axllent.org/docs/integration/webhook/) for received messages - Optional [webhook](https://mailpit.axllent.org/docs/integration/webhook/) for received messages

View File

@ -10,6 +10,7 @@ import (
"github.com/axllent/mailpit/internal/auth" "github.com/axllent/mailpit/internal/auth"
"github.com/axllent/mailpit/internal/logger" "github.com/axllent/mailpit/internal/logger"
"github.com/axllent/mailpit/internal/smtpd" "github.com/axllent/mailpit/internal/smtpd"
"github.com/axllent/mailpit/internal/smtpd/chaos"
"github.com/axllent/mailpit/internal/storage" "github.com/axllent/mailpit/internal/storage"
"github.com/axllent/mailpit/internal/tools" "github.com/axllent/mailpit/internal/tools"
"github.com/axllent/mailpit/server" "github.com/axllent/mailpit/server"
@ -122,6 +123,10 @@ func init() {
rootCmd.Flags().BoolVar(&config.SMTPRelayAll, "smtp-relay-all", config.SMTPRelayAll, "Auto-relay all new messages via external SMTP server (caution!)") rootCmd.Flags().BoolVar(&config.SMTPRelayAll, "smtp-relay-all", config.SMTPRelayAll, "Auto-relay all new messages via external SMTP server (caution!)")
rootCmd.Flags().StringVar(&config.SMTPRelayMatching, "smtp-relay-matching", config.SMTPRelayMatching, "Auto-relay new messages to only matching recipients (regular expression)") rootCmd.Flags().StringVar(&config.SMTPRelayMatching, "smtp-relay-matching", config.SMTPRelayMatching, "Auto-relay new messages to only matching recipients (regular expression)")
// Chaos
rootCmd.Flags().BoolVar(&chaos.Enabled, "enable-chaos", chaos.Enabled, "Enable Chaos functionality (API / web UI)")
rootCmd.Flags().StringVar(&config.ChaosTriggers, "chaos-triggers", config.ChaosTriggers, "Enable Chaos & set the triggers for SMTP server")
// POP3 server // POP3 server
rootCmd.Flags().StringVar(&config.POP3Listen, "pop3", config.POP3Listen, "POP3 server bind interface and port") rootCmd.Flags().StringVar(&config.POP3Listen, "pop3", config.POP3Listen, "POP3 server bind interface and port")
rootCmd.Flags().StringVar(&config.POP3AuthFile, "pop3-auth-file", config.POP3AuthFile, "A password file for POP3 server authentication (enables POP3 server)") rootCmd.Flags().StringVar(&config.POP3AuthFile, "pop3-auth-file", config.POP3AuthFile, "A password file for POP3 server authentication (enables POP3 server)")
@ -281,6 +286,10 @@ func initConfigFromEnv() {
config.SMTPRelayConfig.AllowedRecipients = os.Getenv("MP_SMTP_RELAY_ALLOWED_RECIPIENTS") config.SMTPRelayConfig.AllowedRecipients = os.Getenv("MP_SMTP_RELAY_ALLOWED_RECIPIENTS")
config.SMTPRelayConfig.BlockedRecipients = os.Getenv("MP_SMTP_RELAY_BLOCKED_RECIPIENTS") config.SMTPRelayConfig.BlockedRecipients = os.Getenv("MP_SMTP_RELAY_BLOCKED_RECIPIENTS")
// Chaos
chaos.Enabled = getEnabledFromEnv("MP_ENABLE_CHAOS")
config.ChaosTriggers = os.Getenv("MP_CHAOS_TRIGGERS")
// POP3 server // POP3 server
if len(os.Getenv("MP_POP3_BIND_ADDR")) > 0 { if len(os.Getenv("MP_POP3_BIND_ADDR")) > 0 {
config.POP3Listen = os.Getenv("MP_POP3_BIND_ADDR") config.POP3Listen = os.Getenv("MP_POP3_BIND_ADDR")

View File

@ -15,6 +15,7 @@ import (
"github.com/axllent/mailpit/internal/auth" "github.com/axllent/mailpit/internal/auth"
"github.com/axllent/mailpit/internal/logger" "github.com/axllent/mailpit/internal/logger"
"github.com/axllent/mailpit/internal/smtpd/chaos"
"github.com/axllent/mailpit/internal/spamassassin" "github.com/axllent/mailpit/internal/spamassassin"
"github.com/axllent/mailpit/internal/tools" "github.com/axllent/mailpit/internal/tools"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
@ -176,6 +177,9 @@ var (
// RepoBinaryName on Github for updater // RepoBinaryName on Github for updater
RepoBinaryName = "mailpit" RepoBinaryName = "mailpit"
// ChaosTriggers are parsed and set in the chaos module
ChaosTriggers string
// DisableHTMLCheck DEPRECATED 2024/04/13 - kept here to display console warning only // DisableHTMLCheck DEPRECATED 2024/04/13 - kept here to display console warning only
DisableHTMLCheck = false DisableHTMLCheck = false
@ -344,6 +348,14 @@ func VerifyConfig() error {
return errors.New("[smtp] authentication requires STARTTLS or TLS encryption, run with `--smtp-auth-allow-insecure` to allow insecure authentication") return errors.New("[smtp] authentication requires STARTTLS or TLS encryption, run with `--smtp-auth-allow-insecure` to allow insecure authentication")
} }
if err := parseChaosTriggers(); err != nil {
return fmt.Errorf("[chaos] %s", err.Error())
}
if chaos.Enabled {
logger.Log().Info("[chaos] is enabled")
}
// POP3 server // POP3 server
if POP3TLSCert != "" { if POP3TLSCert != "" {
POP3TLSCert = filepath.Clean(POP3TLSCert) POP3TLSCert = filepath.Clean(POP3TLSCert)
@ -602,6 +614,39 @@ func validateRelayConfig() error {
return nil return nil
} }
func parseChaosTriggers() error {
if ChaosTriggers == "" {
return nil
}
re := regexp.MustCompile(`^([a-zA-Z0-0]+):(\d\d\d):(\d+(\.\d)?)$`)
parts := strings.Split(ChaosTriggers, ",")
for _, p := range parts {
p = strings.TrimSpace(p)
if !re.MatchString(p) {
return fmt.Errorf("invalid argument: %s", p)
}
matches := re.FindAllStringSubmatch(p, 1)
key := matches[0][1]
errorCode, err := strconv.Atoi(matches[0][2])
if err != nil {
return err
}
probability, err := strconv.Atoi(matches[0][3])
if err != nil {
return err
}
if err := chaos.Set(key, errorCode, probability); err != nil {
return err
}
}
return nil
}
// IsFile returns whether a file exists and is readable // IsFile returns whether a file exists and is readable
func isFile(path string) bool { func isFile(path string) bool {
f, err := os.Open(filepath.Clean(path)) f, err := os.Open(filepath.Clean(path))

View File

@ -0,0 +1,121 @@
// Package chaos is used to simulate Chaos engineering (random failures) in the SMTPD server.
// See https://en.wikipedia.org/wiki/Chaos_engineering
// See https://mailpit.axllent.org/docs/integration/chaos/
package chaos
import (
"crypto/rand"
"fmt"
"math/big"
"strings"
"github.com/axllent/mailpit/internal/logger"
)
var (
// Enabled is a flag to enable or disable support for chaos
Enabled = false
// Config is the global Chaos configuration
Config = Triggers{
Sender: Trigger{ErrorCode: 451, Probability: 0},
Recipient: Trigger{ErrorCode: 451, Probability: 0},
Authentication: Trigger{ErrorCode: 535, Probability: 0},
}
)
// Triggers for the Chaos configuration
//
// swagger:model Triggers
type Triggers struct {
// Sender trigger to fail on From, Sender
Sender Trigger
// Recipient trigger to fail on To, Cc, Bcc
Recipient Trigger
// Authentication trigger to fail while authenticating (auth must be configured)
Authentication Trigger
}
// Trigger for Chaos
type Trigger struct {
// SMTP error code to return. The value must range from 400 to 599.
// required: true
// example: 451
ErrorCode int
// Probability (chance) of triggering the error. The value must range from 0 to 100.
// required: true
// example: 5
Probability int
}
// SetFromStruct will set a whole map of chaos configurations (ie: API)
func SetFromStruct(c Triggers) error {
if c.Sender.ErrorCode == 0 {
c.Sender.ErrorCode = 451 // default
}
if c.Recipient.ErrorCode == 0 {
c.Recipient.ErrorCode = 451 // default
}
if c.Authentication.ErrorCode == 0 {
c.Authentication.ErrorCode = 535 // default
}
if err := Set("Sender", c.Sender.ErrorCode, c.Sender.Probability); err != nil {
return err
}
if err := Set("Recipient", c.Recipient.ErrorCode, c.Recipient.Probability); err != nil {
return err
}
if err := Set("Authentication", c.Authentication.ErrorCode, c.Authentication.Probability); err != nil {
return err
}
return nil
}
// Set will set the chaos configuration for the given key (CLI & setMap())
func Set(key string, errorCode int, probability int) error {
Enabled = true
if errorCode < 400 || errorCode > 599 {
return fmt.Errorf("error code must be between 400 and 599")
}
if probability > 100 || probability < 0 {
return fmt.Errorf("probability must be between 0 and 100")
}
key = strings.ToLower(key)
switch key {
case "sender":
Config.Sender = Trigger{ErrorCode: errorCode, Probability: probability}
logger.Log().Infof("[chaos] Sender to return %d error with %d%% probability", errorCode, probability)
case "recipient", "recipients":
Config.Recipient = Trigger{ErrorCode: errorCode, Probability: probability}
logger.Log().Infof("[chaos] Recipient to return %d error with %d%% probability", errorCode, probability)
case "auth", "authentication":
Config.Authentication = Trigger{ErrorCode: errorCode, Probability: probability}
logger.Log().Infof("[chaos] Authentication to return %d error with %d%% probability", errorCode, probability)
default:
return fmt.Errorf("unknown key %s", key)
}
return nil
}
// Trigger will return whether the Chaos rule is triggered based on the configuration
// and a randomly-generated percentage value.
func (c Trigger) Trigger() (bool, int) {
if !Enabled || c.Probability == 0 {
return false, 0
}
nBig, _ := rand.Int(rand.Reader, big.NewInt(100))
// rand.IntN(100) will return 0-99, whereas probability is 1-100,
// so value must be less than (not <=) to the probability to trigger
return int(nBig.Int64()) < c.Probability, c.ErrorCode
}

View File

@ -1,7 +1,7 @@
// Package smtpd implements a basic SMTP server. // Package smtpd implements a basic SMTP server.
// //
// This is a modified version of https://github.com/mhale/smtpd to // This is a modified version of https://github.com/mhale/smtpd to
// add optional support for unix sockets. // add support for unix sockets and Mailpit Chaos.
package smtpd package smtpd
import ( import (
@ -22,6 +22,8 @@ import (
"sync" "sync"
"sync/atomic" "sync/atomic"
"time" "time"
"github.com/axllent/mailpit/internal/smtpd/chaos"
) )
var ( var (
@ -411,6 +413,12 @@ loop:
if match == nil { if match == nil {
s.writef("501 5.5.4 Syntax error in parameters or arguments (invalid FROM parameter)") s.writef("501 5.5.4 Syntax error in parameters or arguments (invalid FROM parameter)")
} else { } else {
// Mailpit Chaos
if fail, code := chaos.Config.Sender.Trigger(); fail {
s.writef("%d Chaos sender error", code)
break
}
// Validate the SIZE parameter if one was sent. // Validate the SIZE parameter if one was sent.
if len(match[2]) > 0 { // A parameter is present if len(match[2]) > 0 { // A parameter is present
sizeMatch := mailFromSizeRE.FindStringSubmatch(match[3]) sizeMatch := mailFromSizeRE.FindStringSubmatch(match[3])
@ -439,6 +447,7 @@ loop:
s.writef("250 2.1.0 Ok") s.writef("250 2.1.0 Ok")
} }
} }
to = nil to = nil
buffer.Reset() buffer.Reset()
case "RCPT": case "RCPT":
@ -459,10 +468,17 @@ loop:
if match == nil { if match == nil {
s.writef("501 5.5.4 Syntax error in parameters or arguments (invalid TO parameter)") s.writef("501 5.5.4 Syntax error in parameters or arguments (invalid TO parameter)")
} else { } else {
// Mailpit Chaos
if fail, code := chaos.Config.Recipient.Trigger(); fail {
s.writef("%d Chaos recipient error", code)
break
}
// RFC 5321 specifies support for minimum of 100 recipients is required. // RFC 5321 specifies support for minimum of 100 recipients is required.
if s.srv.MaxRecipients == 0 { if s.srv.MaxRecipients == 0 {
s.srv.MaxRecipients = 100 s.srv.MaxRecipients = 100
} }
if len(to) == s.srv.MaxRecipients { if len(to) == s.srv.MaxRecipients {
s.writef("452 4.5.3 Too many recipients") s.writef("452 4.5.3 Too many recipients")
} else { } else {
@ -685,6 +701,12 @@ loop:
break break
} }
// Mailpit Chaos
if fail, code := chaos.Config.Authentication.Trigger(); fail {
s.writef("%d Chaos authentication error", code)
break
}
// RFC 4954 also specifies that ESMTP code 5.5.4 ("Invalid command arguments") should be returned // RFC 4954 also specifies that ESMTP code 5.5.4 ("Invalid command arguments") should be returned
// when attempting to use an unsupported authentication type. // when attempting to use an unsupported authentication type.
// Many servers return 5.7.4 ("Security features not supported") instead. // Many servers return 5.7.4 ("Security features not supported") instead.

View File

@ -6,6 +6,7 @@ import (
"net/http" "net/http"
"github.com/axllent/mailpit/config" "github.com/axllent/mailpit/config"
"github.com/axllent/mailpit/internal/smtpd/chaos"
"github.com/axllent/mailpit/internal/stats" "github.com/axllent/mailpit/internal/stats"
) )
@ -67,6 +68,9 @@ type webUIConfiguration struct {
// Whether SpamAssassin is enabled // Whether SpamAssassin is enabled
SpamAssassin bool SpamAssassin bool
// Whether Chaos support is enabled at runtime
ChaosEnabled bool
// Whether messages with duplicate IDs are ignored // Whether messages with duplicate IDs are ignored
DuplicatesIgnored bool DuplicatesIgnored bool
} }
@ -112,6 +116,7 @@ func WebUIConfig(w http.ResponseWriter, _ *http.Request) {
} }
conf.SpamAssassin = config.EnableSpamAssassin != "" conf.SpamAssassin = config.EnableSpamAssassin != ""
conf.ChaosEnabled = chaos.Enabled
conf.DuplicatesIgnored = config.IgnoreDuplicateIDs conf.DuplicatesIgnored = config.IgnoreDuplicateIDs
w.Header().Add("Content-Type", "application/json") w.Header().Add("Content-Type", "application/json")

112
server/apiv1/chaos.go Normal file
View File

@ -0,0 +1,112 @@
package apiv1
import (
"encoding/json"
"net/http"
"github.com/axllent/mailpit/internal/smtpd/chaos"
)
// ChaosTriggers is the Chaos configuration
//
// swagger:model Triggers
type ChaosTriggers chaos.Triggers
// Response for the Chaos triggers configuration
// swagger:response ChaosResponse
type chaosResponse struct {
// The current Chaos triggers
//
// in: body
Body ChaosTriggers
}
// GetChaos returns the current Chaos triggers
func GetChaos(w http.ResponseWriter, _ *http.Request) {
// swagger:route GET /api/v1/chaos testing getChaos
//
// # Get Chaos triggers
//
// Returns the current Chaos triggers configuration.
// This API route will return an error if Chaos is not enabled at runtime.
//
// Produces:
// - application/json
//
// Schemes: http, https
//
// Responses:
// 200: ChaosResponse
// 400: ErrorResponse
if !chaos.Enabled {
httpError(w, "Chaos is not enabled")
return
}
conf := chaos.Config
w.Header().Add("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(conf); err != nil {
httpError(w, err.Error())
}
}
// swagger:parameters setChaosParams
type setChaosParams struct {
// in: body
Body ChaosTriggers
}
// SetChaos sets the Chaos configuration.
func SetChaos(w http.ResponseWriter, r *http.Request) {
// swagger:route PUT /api/v1/chaos testing setChaosParams
//
// # Set Chaos triggers
//
// Set the Chaos triggers configuration and return the updated values.
// This API route will return an error if Chaos is not enabled at runtime.
//
// If any triggers are omitted from the request, then those are reset to their
// default values with a 0% probability (ie: disabled).
// Setting a blank `{}` will reset all triggers to their default values.
//
// Consumes:
// - application/json
//
// Produces:
// - application/json
//
// Schemes: http, https
//
// Responses:
// 200: ChaosResponse
// 400: ErrorResponse
if !chaos.Enabled {
httpError(w, "Chaos is not enabled")
return
}
data := chaos.Triggers{}
decoder := json.NewDecoder(r.Body)
err := decoder.Decode(&data)
if err != nil {
httpError(w, err.Error())
return
}
if err := chaos.SetFromStruct(data); err != nil {
httpError(w, err.Error())
return
}
conf := chaos.Config
w.Header().Add("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(conf); err != nil {
httpError(w, err.Error())
}
}

View File

@ -183,6 +183,10 @@ func apiRoutes() *mux.Router {
r.HandleFunc(config.Webroot+"api/v1/webui", middleWareFunc(apiv1.WebUIConfig)).Methods("GET") r.HandleFunc(config.Webroot+"api/v1/webui", middleWareFunc(apiv1.WebUIConfig)).Methods("GET")
r.HandleFunc(config.Webroot+"api/v1/swagger.json", middleWareFunc(swaggerBasePath)).Methods("GET") r.HandleFunc(config.Webroot+"api/v1/swagger.json", middleWareFunc(swaggerBasePath)).Methods("GET")
// Chaos
r.HandleFunc(config.Webroot+"api/v1/chaos", middleWareFunc(apiv1.GetChaos)).Methods("GET")
r.HandleFunc(config.Webroot+"api/v1/chaos", middleWareFunc(apiv1.SetChaos)).Methods("PUT")
// web UI websocket // web UI websocket
r.HandleFunc(config.Webroot+"api/events", apiWebsocket).Methods("GET") r.HandleFunc(config.Webroot+"api/events", apiWebsocket).Methods("GET")

View File

@ -12,6 +12,8 @@ export default {
mailbox, mailbox,
theme: localStorage.getItem('theme') ? localStorage.getItem('theme') : 'auto', theme: localStorage.getItem('theme') ? localStorage.getItem('theme') : 'auto',
timezones, timezones,
chaosConfig: false,
chaosUpdated: false,
} }
}, },
@ -23,6 +25,13 @@ export default {
localStorage.setItem('theme', v) localStorage.setItem('theme', v)
} }
this.setTheme() this.setTheme()
},
chaosConfig: {
handler() {
this.chaosUpdated = true
},
deep: true
} }
}, },
@ -44,6 +53,24 @@ export default {
document.documentElement.setAttribute('data-bs-theme', this.theme) document.documentElement.setAttribute('data-bs-theme', this.theme)
} }
}, },
loadChaos() {
this.get(this.resolve('/api/v1/chaos'), null, (response) => {
this.chaosConfig = response.data
this.$nextTick(() => {
this.chaosUpdated = false
})
})
},
saveChaos() {
this.put(this.resolve('/api/v1/chaos'), this.chaosConfig, (response) => {
this.chaosConfig = response.data
this.$nextTick(() => {
this.chaosUpdated = false
})
})
}
} }
} }
</script> </script>
@ -54,11 +81,27 @@ export default {
<div class="modal-dialog modal-lg"> <div class="modal-dialog modal-lg">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
<h5 class="modal-title" id="SettingsModalLabel">Mailpit UI settings</h5> <h5 class="modal-title" id="SettingsModalLabel">Mailpit settings</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<div class="mb-3"> <ul class="nav nav-tabs" id="myTab" role="tablist" v-if="mailbox.uiConfig.ChaosEnabled">
<li class="nav-item" role="presentation">
<button class="nav-link active" id="ui-tab" data-bs-toggle="tab"
data-bs-target="#ui-tab-pane" type="button" role="tab" aria-controls="ui-tab-pane"
aria-selected="true">Web UI</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="chaos-tab" data-bs-toggle="tab"
data-bs-target="#chaos-tab-pane" type="button" role="tab" aria-controls="chaos-tab-pane"
aria-selected="false" @click="loadChaos">Chaos</button>
</li>
</ul>
<div class="tab-content">
<div class="tab-pane fade show active" id="ui-tab-pane" role="tabpanel" aria-labelledby="ui-tab"
tabindex="0">
<div class="my-3">
<label for="theme" class="form-label">Mailpit theme</label> <label for="theme" class="form-label">Mailpit theme</label>
<select class="form-select" v-model="theme" id="theme"> <select class="form-select" v-model="theme" id="theme">
<option value="auto">Auto (detect from browser)</option> <option value="auto">Auto (detect from browser)</option>
@ -68,7 +111,8 @@ export default {
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label for="timezone" class="form-label">Timezone (for date searches)</label> <label for="timezone" class="form-label">Timezone (for date searches)</label>
<select class="form-select tz" v-model="mailbox.timeZone" id="timezone" data-allow-same="true"> <select class="form-select tz" v-model="mailbox.timeZone" id="timezone"
data-allow-same="true">
<option disabled hidden value="">Select a timezone...</option> <option disabled hidden value="">Select a timezone...</option>
<option v-for="t in timezones" :value="t.tzCode">{{ t.label }}</option> <option v-for="t in timezones" :value="t.tzCode">{{ t.label }}</option>
</select> </select>
@ -110,10 +154,118 @@ export default {
</div> </div>
</div> </div>
</div> </div>
<div class="tab-pane fade" id="chaos-tab-pane" role="tabpanel" aria-labelledby="chaos-tab"
tabindex="0" v-if="mailbox.uiConfig.ChaosEnabled">
<p class="my-3">
<b>Chaos</b> allows you to set random SMTP failures and response codes at various
stages in a SMTP transaction to test application resilience
(<a href="https://mailpit.axllent.org/docs/integration/chaos/" target="_blank">
see documentation
</a>).
</p>
<ul>
<li>
<code>Response code</code> is the SMTP error code returned by the server if this
error is triggered. Error codes must range between 400 and 599.
</li>
<li>
<code>Error probability</code> is the % chance that the error will occur per message
delivery, where <code>0</code>(%) is disabled and <code>100</code>(%) wil always
trigger. A probability of <code>50</code> will trigger on approximately 50% of
messages received.
</li>
</ul>
<template v-if="chaosConfig">
<div class="mt-4 mb-4" :class="chaosUpdated ? 'was-validated' : ''">
<div class="mb-4">
<label>Trigger: <code>Sender</code></label>
<div class="form-text">
Trigger an error response based on the sender (From / Sender).
</div>
<div class="row mt-1">
<div class="col">
<label class="form-label">
Response code
</label>
<input type="number" class="form-control"
v-model.number="chaosConfig.Sender.ErrorCode" min="400" max="599"
required>
</div>
<div class="col">
<label class="form-label">
Error probability ({{ chaosConfig.Sender.Probability }}%)
</label>
<input type="range" class="form-range mt-1" min="0" max="100"
v-model.number="chaosConfig.Sender.Probability">
</div>
</div>
</div>
<div class="mb-4">
<label>Trigger: <code>Recipient</code></label>
<div class="form-text">
Trigger an error response based on the recipients (To, Cc, Bcc).
</div>
<div class="row mt-1">
<div class="col">
<label class="form-label">
Response code
</label>
<input type="number" class="form-control"
v-model.number="chaosConfig.Recipient.ErrorCode" min="400" max="599"
required>
</div>
<div class="col">
<label class="form-label">
Error probability ({{ chaosConfig.Recipient.Probability }}%)
</label>
<input type="range" class="form-range mt-1" min="0" max="100"
v-model.number="chaosConfig.Recipient.Probability">
</div>
</div>
</div>
<div class="mb-4">
<label>Trigger: <code>Authentication</code></label>
<div class="form-text">
Trigger an authentication error response.
Note that SMTP authentication must be configured too.
</div>
<div class="row mt-1">
<div class="col">
<label class="form-label">
Response code
</label>
<input type="number" class="form-control"
v-model.number="chaosConfig.Authentication.ErrorCode" min="400"
max="599" required>
</div>
<div class="col">
<label class="form-label">
Error probability ({{ chaosConfig.Authentication.Probability }}%)
</label>
<input type="range" class="form-range mt-1" min="0" max="100"
v-model.number="chaosConfig.Authentication.Probability">
</div>
</div>
</div>
</div>
<div v-if="chaosUpdated" class="mb-3 text-center">
<button class="btn btn-success" @click="saveChaos">Update Chaos</button>
</div>
</template>
</div>
</div>
<div class="modal-footer"> <div class="modal-footer">
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Close</button> <button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Close</button>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div>
</template> </template>

View File

@ -23,6 +23,66 @@
"version": "v1" "version": "v1"
}, },
"paths": { "paths": {
"/api/v1/chaos": {
"get": {
"description": "Returns the current Chaos triggers configuration.\nThis API route will return an error if Chaos is not enabled at runtime.",
"produces": [
"application/json"
],
"schemes": [
"http",
"https"
],
"tags": [
"testing"
],
"summary": "Get Chaos triggers",
"operationId": "getChaos",
"responses": {
"200": {
"$ref": "#/responses/ChaosResponse"
},
"400": {
"$ref": "#/responses/ErrorResponse"
}
}
},
"put": {
"description": "Set the Chaos triggers configuration and return the updated values.\nThis API route will return an error if Chaos is not enabled at runtime.\n\nIf any triggers are omitted from the request, then those are reset to their\ndefault values with a 0% probability (ie: disabled).\nSetting a blank `{}` will reset all triggers to their default values.",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"schemes": [
"http",
"https"
],
"tags": [
"testing"
],
"summary": "Set Chaos triggers",
"operationId": "setChaosParams",
"parameters": [
{
"name": "Body",
"in": "body",
"schema": {
"$ref": "#/definitions/Triggers"
}
}
],
"responses": {
"200": {
"$ref": "#/responses/ChaosResponse"
},
"400": {
"$ref": "#/responses/ErrorResponse"
}
}
}
},
"/api/v1/info": { "/api/v1/info": {
"get": { "get": {
"description": "Returns basic runtime information, message totals and latest release version.", "description": "Returns basic runtime information, message totals and latest release version.",
@ -139,7 +199,7 @@
"https" "https"
], ],
"tags": [ "tags": [
"Other" "other"
], ],
"summary": "HTML check", "summary": "HTML check",
"operationId": "HTMLCheckParams", "operationId": "HTMLCheckParams",
@ -179,7 +239,7 @@
"https" "https"
], ],
"tags": [ "tags": [
"Other" "other"
], ],
"summary": "Link check", "summary": "Link check",
"operationId": "LinkCheckParams", "operationId": "LinkCheckParams",
@ -414,7 +474,7 @@
"https" "https"
], ],
"tags": [ "tags": [
"Other" "other"
], ],
"summary": "SpamAssassin check", "summary": "SpamAssassin check",
"operationId": "SpamAssassinCheckParams", "operationId": "SpamAssassinCheckParams",
@ -1816,10 +1876,54 @@
"x-go-name": "Result", "x-go-name": "Result",
"x-go-package": "github.com/axllent/mailpit/internal/spamassassin" "x-go-package": "github.com/axllent/mailpit/internal/spamassassin"
}, },
"Trigger": {
"description": "Trigger for Chaos",
"type": "object",
"required": [
"ErrorCode",
"Probability"
],
"properties": {
"ErrorCode": {
"description": "SMTP error code to return. The value must range from 400 to 599.",
"type": "integer",
"format": "int64",
"example": 451
},
"Probability": {
"description": "Probability (chance) of triggering the error. The value must range from 0 to 100.",
"type": "integer",
"format": "int64",
"example": 5
}
},
"x-go-package": "github.com/axllent/mailpit/internal/smtpd/chaos"
},
"Triggers": {
"description": "ChaosTriggers is the Chaos configuration",
"type": "object",
"properties": {
"Authentication": {
"$ref": "#/definitions/Trigger"
},
"Recipient": {
"$ref": "#/definitions/Trigger"
},
"Sender": {
"$ref": "#/definitions/Trigger"
}
},
"x-go-package": "github.com/axllent/mailpit/internal/smtpd/chaos",
"$ref": "#/definitions/Triggers"
},
"WebUIConfiguration": { "WebUIConfiguration": {
"description": "Response includes global web UI settings", "description": "Response includes global web UI settings",
"type": "object", "type": "object",
"properties": { "properties": {
"ChaosEnabled": {
"description": "Whether Chaos support is enabled at runtime",
"type": "boolean"
},
"DuplicatesIgnored": { "DuplicatesIgnored": {
"description": "Whether messages with duplicate IDs are ignored", "description": "Whether messages with duplicate IDs are ignored",
"type": "boolean" "type": "boolean"
@ -1885,6 +1989,12 @@
"type": "string" "type": "string"
} }
}, },
"ChaosResponse": {
"description": "Response for the Chaos triggers configuration",
"schema": {
"$ref": "#/definitions/Triggers"
}
},
"ErrorResponse": { "ErrorResponse": {
"description": "Server error will return with a 400 status code\nwith the error message in the body", "description": "Server error will return with a 400 status code\nwith the error message in the body",
"schema": { "schema": {