mirror of
https://github.com/TwiN/gatus.git
synced 2024-12-14 11:58:04 +00:00
Fix #117: Implement email alerts
This commit is contained in:
parent
0331c18401
commit
f6336eac4e
53 changed files with 3062 additions and 626 deletions
39
README.md
39
README.md
|
@ -40,6 +40,7 @@ Have any feedback or want to share your good/bad experience with Gatus? Feel fre
|
|||
- [Client configuration](#client-configuration)
|
||||
- [Alerting](#alerting)
|
||||
- [Configuring Discord alerts](#configuring-discord-alerts)
|
||||
- [Configuring Email alerts](#configuring-email-alerts)
|
||||
- [Configuring Mattermost alerts](#configuring-mattermost-alerts)
|
||||
- [Configuring Messagebird alerts](#configuring-messagebird-alerts)
|
||||
- [Configuring PagerDuty alerts](#configuring-pagerduty-alerts)
|
||||
|
@ -348,6 +349,44 @@ endpoints:
|
|||
```
|
||||
|
||||
|
||||
#### Configuring Email alerts
|
||||
| Parameter | Description | Default |
|
||||
|:---------------------------------- |:------------------------------------------------- |:-------------- |
|
||||
| `alerting.email` | Configuration for alerts of type `email` | `{}` |
|
||||
| `alerting.email.from` | Email used to send the alert | Required `""` |
|
||||
| `alerting.email.password` | Password of the email used to send the alert | Required `""` |
|
||||
| `alerting.email.host` | Host of the mail server (e.g. `smtp.gmail.com`) | Required `""` |
|
||||
| `alerting.email.port` | Port the mail server is listening to (e.g. `587`) | Required `0` |
|
||||
| `alerting.email.to` | Email(s) to send the alerts to | Required `""` |
|
||||
| `alerting.email.default-alert` | Default alert configuration. <br />See [Setting a default alert](#setting-a-default-alert) | N/A |
|
||||
|
||||
```yaml
|
||||
alerting:
|
||||
email:
|
||||
from: "from@example.com"
|
||||
password: "hunter2"
|
||||
host: "mail.example.com"
|
||||
port: 587
|
||||
to: "recipient1@example.com,recipient2@example.com"
|
||||
|
||||
endpoints:
|
||||
- name: website
|
||||
url: "https://twin.sh/health"
|
||||
interval: 5m
|
||||
conditions:
|
||||
- "[STATUS] == 200"
|
||||
- "[BODY].status == UP"
|
||||
- "[RESPONSE_TIME] < 300"
|
||||
alerts:
|
||||
- type: email
|
||||
enabled: true
|
||||
description: "healthcheck failed"
|
||||
send-on-resolved: true
|
||||
```
|
||||
|
||||
**NOTE:** Some mail servers are painfully slow.
|
||||
|
||||
|
||||
#### Configuring Mattermost alerts
|
||||
| Parameter | Description | Default |
|
||||
|:----------------------------------- |:------------------------------------------------------------------------------------------- |:-------------- |
|
||||
|
|
|
@ -11,6 +11,9 @@ const (
|
|||
// TypeDiscord is the Type for the discord alerting provider
|
||||
TypeDiscord Type = "discord"
|
||||
|
||||
// TypeEmail is the Type for the email alerting provider
|
||||
TypeEmail Type = "email"
|
||||
|
||||
// TypeMattermost is the Type for the mattermost alerting provider
|
||||
TypeMattermost Type = "mattermost"
|
||||
|
||||
|
|
|
@ -5,6 +5,7 @@ import (
|
|||
"github.com/TwiN/gatus/v3/alerting/provider"
|
||||
"github.com/TwiN/gatus/v3/alerting/provider/custom"
|
||||
"github.com/TwiN/gatus/v3/alerting/provider/discord"
|
||||
"github.com/TwiN/gatus/v3/alerting/provider/email"
|
||||
"github.com/TwiN/gatus/v3/alerting/provider/mattermost"
|
||||
"github.com/TwiN/gatus/v3/alerting/provider/messagebird"
|
||||
"github.com/TwiN/gatus/v3/alerting/provider/pagerduty"
|
||||
|
@ -17,31 +18,34 @@ import (
|
|||
// Config is the configuration for alerting providers
|
||||
type Config struct {
|
||||
// Custom is the configuration for the custom alerting provider
|
||||
Custom *custom.AlertProvider `yaml:"custom"`
|
||||
Custom *custom.AlertProvider `yaml:"custom,omitempty"`
|
||||
|
||||
// Discord is the configuration for the discord alerting provider
|
||||
Discord *discord.AlertProvider `yaml:"discord"`
|
||||
Discord *discord.AlertProvider `yaml:"discord,omitempty"`
|
||||
|
||||
// Email is the configuration for the email alerting provider
|
||||
Email *email.AlertProvider `yaml:"email,omitempty"`
|
||||
|
||||
// Mattermost is the configuration for the mattermost alerting provider
|
||||
Mattermost *mattermost.AlertProvider `yaml:"mattermost"`
|
||||
Mattermost *mattermost.AlertProvider `yaml:"mattermost,omitempty"`
|
||||
|
||||
// Messagebird is the configuration for the messagebird alerting provider
|
||||
Messagebird *messagebird.AlertProvider `yaml:"messagebird"`
|
||||
Messagebird *messagebird.AlertProvider `yaml:"messagebird,omitempty"`
|
||||
|
||||
// PagerDuty is the configuration for the pagerduty alerting provider
|
||||
PagerDuty *pagerduty.AlertProvider `yaml:"pagerduty"`
|
||||
PagerDuty *pagerduty.AlertProvider `yaml:"pagerduty,omitempty"`
|
||||
|
||||
// Slack is the configuration for the slack alerting provider
|
||||
Slack *slack.AlertProvider `yaml:"slack"`
|
||||
Slack *slack.AlertProvider `yaml:"slack,omitempty"`
|
||||
|
||||
// Teams is the configuration for the teams alerting provider
|
||||
Teams *teams.AlertProvider `yaml:"teams"`
|
||||
Teams *teams.AlertProvider `yaml:"teams,omitempty"`
|
||||
|
||||
// Telegram is the configuration for the telegram alerting provider
|
||||
Telegram *telegram.AlertProvider `yaml:"telegram"`
|
||||
Telegram *telegram.AlertProvider `yaml:"telegram,omitempty"`
|
||||
|
||||
// Twilio is the configuration for the twilio alerting provider
|
||||
Twilio *twilio.AlertProvider `yaml:"twilio"`
|
||||
Twilio *twilio.AlertProvider `yaml:"twilio,omitempty"`
|
||||
}
|
||||
|
||||
// GetAlertingProviderByAlertType returns an provider.AlertProvider by its corresponding alert.Type
|
||||
|
@ -59,6 +63,12 @@ func (config Config) GetAlertingProviderByAlertType(alertType alert.Type) provid
|
|||
return nil
|
||||
}
|
||||
return config.Discord
|
||||
case alert.TypeEmail:
|
||||
if config.Email == nil {
|
||||
// Since we're returning an interface, we need to explicitly return nil, even if the provider itself is nil
|
||||
return nil
|
||||
}
|
||||
return config.Email
|
||||
case alert.TypeMattermost:
|
||||
if config.Mattermost == nil {
|
||||
// Since we're returning an interface, we need to explicitly return nil, even if the provider itself is nil
|
||||
|
|
|
@ -38,11 +38,6 @@ func (provider *AlertProvider) IsValid() bool {
|
|||
return len(provider.URL) > 0 && provider.ClientConfig != nil
|
||||
}
|
||||
|
||||
// ToCustomAlertProvider converts the provider into a custom.AlertProvider
|
||||
func (provider *AlertProvider) ToCustomAlertProvider(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) *AlertProvider {
|
||||
return provider
|
||||
}
|
||||
|
||||
// GetAlertStatePlaceholderValue returns the Placeholder value for ALERT_TRIGGERED_OR_RESOLVED if configured
|
||||
func (provider *AlertProvider) GetAlertStatePlaceholderValue(resolved bool) string {
|
||||
status := "TRIGGERED"
|
||||
|
@ -105,27 +100,26 @@ func (provider *AlertProvider) buildHTTPRequest(endpointName, alertDescription s
|
|||
return request
|
||||
}
|
||||
|
||||
// Send a request to the alert provider and return the body
|
||||
func (provider *AlertProvider) Send(endpointName, alertDescription string, resolved bool) ([]byte, error) {
|
||||
func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) error {
|
||||
if os.Getenv("MOCK_ALERT_PROVIDER") == "true" {
|
||||
if os.Getenv("MOCK_ALERT_PROVIDER_ERROR") == "true" {
|
||||
return nil, errors.New("error")
|
||||
return errors.New("error")
|
||||
}
|
||||
return []byte("{}"), nil
|
||||
return nil
|
||||
}
|
||||
request := provider.buildHTTPRequest(endpointName, alertDescription, resolved)
|
||||
request := provider.buildHTTPRequest(endpoint.Name, alert.GetDescription(), resolved)
|
||||
response, err := client.GetHTTPClient(provider.ClientConfig).Do(request)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return err
|
||||
}
|
||||
if response.StatusCode > 399 {
|
||||
body, err := ioutil.ReadAll(response.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("call to provider alert returned status code %d", response.StatusCode)
|
||||
return fmt.Errorf("call to provider alert returned status code %d", response.StatusCode)
|
||||
}
|
||||
return nil, fmt.Errorf("call to provider alert returned status code %d: %s", response.StatusCode, string(body))
|
||||
return fmt.Errorf("call to provider alert returned status code %d: %s", response.StatusCode, string(body))
|
||||
}
|
||||
return ioutil.ReadAll(response.Body)
|
||||
return err
|
||||
}
|
||||
|
||||
// GetDefaultAlert returns the provider's default alert configuration
|
||||
|
|
|
@ -3,9 +3,6 @@ package custom
|
|||
import (
|
||||
"io/ioutil"
|
||||
"testing"
|
||||
|
||||
"github.com/TwiN/gatus/v3/alerting/alert"
|
||||
"github.com/TwiN/gatus/v3/core"
|
||||
)
|
||||
|
||||
func TestAlertProvider_IsValid(t *testing.T) {
|
||||
|
@ -59,17 +56,6 @@ func TestAlertProvider_buildHTTPRequestWhenTriggered(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestAlertProvider_ToCustomAlertProvider(t *testing.T) {
|
||||
provider := AlertProvider{URL: "https://example.com"}
|
||||
customAlertProvider := provider.ToCustomAlertProvider(&core.Endpoint{}, &alert.Alert{}, &core.Result{}, true)
|
||||
if customAlertProvider == nil {
|
||||
t.Fatal("customAlertProvider shouldn't have been nil")
|
||||
}
|
||||
if customAlertProvider.URL != "https://example.com" {
|
||||
t.Error("expected URL to be https://example.com, got", customAlertProvider.URL)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAlertProvider_buildHTTPRequestWithCustomPlaceholder(t *testing.T) {
|
||||
const (
|
||||
ExpectedURL = "https://example.com/endpoint-name?event=test&description=alert-description"
|
||||
|
|
|
@ -1,11 +1,15 @@
|
|||
package discord
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/TwiN/gatus/v3/alerting/alert"
|
||||
"github.com/TwiN/gatus/v3/alerting/provider/custom"
|
||||
"github.com/TwiN/gatus/v3/client"
|
||||
"github.com/TwiN/gatus/v3/core"
|
||||
)
|
||||
|
||||
|
@ -22,8 +26,36 @@ func (provider *AlertProvider) IsValid() bool {
|
|||
return len(provider.WebhookURL) > 0
|
||||
}
|
||||
|
||||
// ToCustomAlertProvider converts the provider into a custom.AlertProvider
|
||||
func (provider *AlertProvider) ToCustomAlertProvider(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) *custom.AlertProvider {
|
||||
// Send an alert using the provider
|
||||
func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) error {
|
||||
if os.Getenv("MOCK_ALERT_PROVIDER") == "true" {
|
||||
if os.Getenv("MOCK_ALERT_PROVIDER_ERROR") == "true" {
|
||||
return errors.New("error")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
buffer := bytes.NewBuffer([]byte(provider.buildRequestBody(endpoint, alert, result, resolved)))
|
||||
request, err := http.NewRequest(http.MethodPost, provider.WebhookURL, buffer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
request.Header.Set("Content-Type", "application/json")
|
||||
response, err := client.GetHTTPClient(nil).Do(request)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if response.StatusCode > 399 {
|
||||
body, err := ioutil.ReadAll(response.Body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("call to provider alert returned status code %d", response.StatusCode)
|
||||
}
|
||||
return fmt.Errorf("call to provider alert returned status code %d: %s", response.StatusCode, string(body))
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// buildRequestBody builds the request body for the provider
|
||||
func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) string {
|
||||
var message, results string
|
||||
var colorCode int
|
||||
if resolved {
|
||||
|
@ -46,10 +78,7 @@ func (provider *AlertProvider) ToCustomAlertProvider(endpoint *core.Endpoint, al
|
|||
if alertDescription := alert.GetDescription(); len(alertDescription) > 0 {
|
||||
description = ":\\n> " + alertDescription
|
||||
}
|
||||
return &custom.AlertProvider{
|
||||
URL: provider.WebhookURL,
|
||||
Method: http.MethodPost,
|
||||
Body: fmt.Sprintf(`{
|
||||
return fmt.Sprintf(`{
|
||||
"content": "",
|
||||
"embeds": [
|
||||
{
|
||||
|
@ -65,9 +94,7 @@ func (provider *AlertProvider) ToCustomAlertProvider(endpoint *core.Endpoint, al
|
|||
]
|
||||
}
|
||||
]
|
||||
}`, message, description, colorCode, results),
|
||||
Headers: map[string]string{"Content-Type": "application/json"},
|
||||
}
|
||||
}`, message, description, colorCode, results)
|
||||
}
|
||||
|
||||
// GetDefaultAlert returns the provider's default alert configuration
|
||||
|
|
|
@ -2,8 +2,6 @@ package discord
|
|||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/TwiN/gatus/v3/alerting/alert"
|
||||
|
@ -21,50 +19,51 @@ func TestAlertProvider_IsValid(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestAlertProvider_ToCustomAlertProviderWithResolvedAlert(t *testing.T) {
|
||||
provider := AlertProvider{WebhookURL: "http://example.com"}
|
||||
alertDescription := "test"
|
||||
customAlertProvider := provider.ToCustomAlertProvider(&core.Endpoint{Name: "svc"}, &alert.Alert{Description: &alertDescription}, &core.Result{ConditionResults: []*core.ConditionResult{{Condition: "SUCCESSFUL_CONDITION", Success: true}}}, true)
|
||||
if customAlertProvider == nil {
|
||||
t.Fatal("customAlertProvider shouldn't have been nil")
|
||||
func TestAlertProvider_buildRequestBody(t *testing.T) {
|
||||
firstDescription := "description-1"
|
||||
secondDescription := "description-2"
|
||||
scenarios := []struct {
|
||||
Name string
|
||||
Provider AlertProvider
|
||||
Alert alert.Alert
|
||||
Resolved bool
|
||||
ExpectedBody string
|
||||
}{
|
||||
{
|
||||
Name: "triggered",
|
||||
Provider: AlertProvider{},
|
||||
Alert: alert.Alert{Description: &firstDescription, SuccessThreshold: 5, FailureThreshold: 3},
|
||||
Resolved: false,
|
||||
ExpectedBody: "{\n \"content\": \"\",\n \"embeds\": [\n {\n \"title\": \":helmet_with_white_cross: Gatus\",\n \"description\": \"An alert for **endpoint-name** has been triggered due to having failed 3 time(s) in a row:\\n> description-1\",\n \"color\": 15158332,\n \"fields\": [\n {\n \"name\": \"Condition results\",\n \"value\": \":x: - `[CONNECTED] == true`\\n:x: - `[STATUS] == 200`\\n\",\n \"inline\": false\n }\n ]\n }\n ]\n}",
|
||||
},
|
||||
{
|
||||
Name: "resolved",
|
||||
Provider: AlertProvider{},
|
||||
Alert: alert.Alert{Description: &secondDescription, SuccessThreshold: 5, FailureThreshold: 3},
|
||||
Resolved: true,
|
||||
ExpectedBody: "{\n \"content\": \"\",\n \"embeds\": [\n {\n \"title\": \":helmet_with_white_cross: Gatus\",\n \"description\": \"An alert for **endpoint-name** has been resolved after passing successfully 5 time(s) in a row:\\n> description-2\",\n \"color\": 3066993,\n \"fields\": [\n {\n \"name\": \"Condition results\",\n \"value\": \":white_check_mark: - `[CONNECTED] == true`\\n:white_check_mark: - `[STATUS] == 200`\\n\",\n \"inline\": false\n }\n ]\n }\n ]\n}",
|
||||
},
|
||||
}
|
||||
if !strings.Contains(customAlertProvider.Body, "resolved") {
|
||||
t.Error("customAlertProvider.Body should've contained the substring resolved")
|
||||
}
|
||||
if customAlertProvider.URL != "http://example.com" {
|
||||
t.Errorf("expected URL to be %s, got %s", "http://example.com", customAlertProvider.URL)
|
||||
}
|
||||
if customAlertProvider.Method != http.MethodPost {
|
||||
t.Errorf("expected method to be %s, got %s", http.MethodPost, customAlertProvider.Method)
|
||||
}
|
||||
body := make(map[string]interface{})
|
||||
err := json.Unmarshal([]byte(customAlertProvider.Body), &body)
|
||||
if err != nil {
|
||||
t.Error("expected body to be valid JSON, got error:", err.Error())
|
||||
}
|
||||
if expected := "An alert for **svc** has been resolved after passing successfully 0 time(s) in a row:\n> test"; expected != body["embeds"].([]interface{})[0].(map[string]interface{})["description"] {
|
||||
t.Errorf("expected $.embeds[0].description to be %s, got %s", expected, body["embeds"].([]interface{})[0].(map[string]interface{})["description"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestAlertProvider_ToCustomAlertProviderWithTriggeredAlert(t *testing.T) {
|
||||
provider := AlertProvider{WebhookURL: "http://example.com"}
|
||||
customAlertProvider := provider.ToCustomAlertProvider(&core.Endpoint{}, &alert.Alert{}, &core.Result{ConditionResults: []*core.ConditionResult{{Condition: "UNSUCCESSFUL_CONDITION", Success: false}}}, false)
|
||||
if customAlertProvider == nil {
|
||||
t.Fatal("customAlertProvider shouldn't have been nil")
|
||||
}
|
||||
if !strings.Contains(customAlertProvider.Body, "triggered") {
|
||||
t.Error("customAlertProvider.Body should've contained the substring triggered")
|
||||
}
|
||||
if customAlertProvider.URL != "http://example.com" {
|
||||
t.Errorf("expected URL to be %s, got %s", "http://example.com", customAlertProvider.URL)
|
||||
}
|
||||
if customAlertProvider.Method != http.MethodPost {
|
||||
t.Errorf("expected method to be %s, got %s", http.MethodPost, customAlertProvider.Method)
|
||||
}
|
||||
body := make(map[string]interface{})
|
||||
err := json.Unmarshal([]byte(customAlertProvider.Body), &body)
|
||||
if err != nil {
|
||||
t.Error("expected body to be valid JSON, got error:", err.Error())
|
||||
for _, scenario := range scenarios {
|
||||
t.Run(scenario.Name, func(t *testing.T) {
|
||||
body := scenario.Provider.buildRequestBody(
|
||||
&core.Endpoint{Name: "endpoint-name"},
|
||||
&scenario.Alert,
|
||||
&core.Result{
|
||||
ConditionResults: []*core.ConditionResult{
|
||||
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
||||
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
||||
},
|
||||
},
|
||||
scenario.Resolved,
|
||||
)
|
||||
if body != scenario.ExpectedBody {
|
||||
t.Errorf("expected %s, got %s", scenario.ExpectedBody, body)
|
||||
}
|
||||
out := make(map[string]interface{})
|
||||
if err := json.Unmarshal([]byte(body), &out); err != nil {
|
||||
t.Error("expected body to be valid JSON, got error:", err.Error())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
79
alerting/provider/email/email.go
Normal file
79
alerting/provider/email/email.go
Normal file
|
@ -0,0 +1,79 @@
|
|||
package email
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/TwiN/gatus/v3/alerting/alert"
|
||||
"github.com/TwiN/gatus/v3/core"
|
||||
gomail "gopkg.in/mail.v2"
|
||||
)
|
||||
|
||||
// AlertProvider is the configuration necessary for sending an alert using SMTP
|
||||
type AlertProvider struct {
|
||||
From string `yaml:"from"`
|
||||
Password string `yaml:"password"`
|
||||
Host string `yaml:"host"`
|
||||
Port int `yaml:"port"`
|
||||
To string `yaml:"to"`
|
||||
|
||||
// DefaultAlert is the default alert configuration to use for endpoints with an alert of the appropriate type
|
||||
DefaultAlert *alert.Alert `yaml:"default-alert,omitempty"`
|
||||
}
|
||||
|
||||
// IsValid returns whether the provider's configuration is valid
|
||||
func (provider *AlertProvider) IsValid() bool {
|
||||
return len(provider.From) > 0 && len(provider.Password) > 0 && len(provider.Host) > 0 && len(provider.To) > 0 && provider.Port > 0 && provider.Port < math.MaxUint16
|
||||
}
|
||||
|
||||
// Send an alert using the provider
|
||||
func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) error {
|
||||
if os.Getenv("MOCK_ALERT_PROVIDER") == "true" {
|
||||
if os.Getenv("MOCK_ALERT_PROVIDER_ERROR") == "true" {
|
||||
return errors.New("error")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
subject, body := provider.buildMessageSubjectAndBody(endpoint, alert, result, resolved)
|
||||
m := gomail.NewMessage()
|
||||
m.SetHeader("From", provider.From)
|
||||
m.SetHeader("To", strings.Split(provider.To, ",")...)
|
||||
m.SetHeader("Subject", subject)
|
||||
m.SetBody("text/plain", body)
|
||||
d := gomail.NewDialer(provider.Host, provider.Port, provider.From, provider.Password)
|
||||
return d.DialAndSend(m)
|
||||
}
|
||||
|
||||
// buildMessageSubjectAndBody builds the message subject and body
|
||||
func (provider *AlertProvider) buildMessageSubjectAndBody(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) (string, string) {
|
||||
var subject, message, results string
|
||||
if resolved {
|
||||
subject = fmt.Sprintf("[%s] Alert resolved", endpoint.Name)
|
||||
message = fmt.Sprintf("An alert for %s has been resolved after passing successfully %d time(s) in a row", endpoint.Name, alert.SuccessThreshold)
|
||||
} else {
|
||||
subject = fmt.Sprintf("[%s] Alert triggered", endpoint.Name)
|
||||
message = fmt.Sprintf("An alert for %s has been triggered due to having failed %d time(s) in a row", endpoint.Name, alert.FailureThreshold)
|
||||
}
|
||||
for _, conditionResult := range result.ConditionResults {
|
||||
var prefix string
|
||||
if conditionResult.Success {
|
||||
prefix = "✅"
|
||||
} else {
|
||||
prefix = "❌"
|
||||
}
|
||||
results += fmt.Sprintf("%s %s\n", prefix, conditionResult.Condition)
|
||||
}
|
||||
var description string
|
||||
if alertDescription := alert.GetDescription(); len(alertDescription) > 0 {
|
||||
description = "\n\nAlert description: " + alertDescription
|
||||
}
|
||||
return subject, message + description + "\n\nCondition results:\n" + results
|
||||
}
|
||||
|
||||
// GetDefaultAlert returns the provider's default alert configuration
|
||||
func (provider AlertProvider) GetDefaultAlert() *alert.Alert {
|
||||
return provider.DefaultAlert
|
||||
}
|
70
alerting/provider/email/email_test.go
Normal file
70
alerting/provider/email/email_test.go
Normal file
|
@ -0,0 +1,70 @@
|
|||
package email
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/TwiN/gatus/v3/alerting/alert"
|
||||
"github.com/TwiN/gatus/v3/core"
|
||||
)
|
||||
|
||||
func TestAlertProvider_IsValid(t *testing.T) {
|
||||
invalidProvider := AlertProvider{}
|
||||
if invalidProvider.IsValid() {
|
||||
t.Error("provider shouldn't have been valid")
|
||||
}
|
||||
validProvider := AlertProvider{From: "from@example.com", Password: "password", Host: "smtp.gmail.com", Port: 587, To: "to@example.com"}
|
||||
if !validProvider.IsValid() {
|
||||
t.Error("provider should've been valid")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAlertProvider_buildRequestBody(t *testing.T) {
|
||||
firstDescription := "description-1"
|
||||
secondDescription := "description-2"
|
||||
scenarios := []struct {
|
||||
Name string
|
||||
Provider AlertProvider
|
||||
Alert alert.Alert
|
||||
Resolved bool
|
||||
ExpectedSubject string
|
||||
ExpectedBody string
|
||||
}{
|
||||
{
|
||||
Name: "triggered",
|
||||
Provider: AlertProvider{},
|
||||
Alert: alert.Alert{Description: &firstDescription, SuccessThreshold: 5, FailureThreshold: 3},
|
||||
Resolved: false,
|
||||
ExpectedSubject: "[endpoint-name] Alert triggered",
|
||||
ExpectedBody: "An alert for endpoint-name has been triggered due to having failed 3 time(s) in a row\n\nAlert description: description-1\n\nCondition results:\n❌ [CONNECTED] == true\n❌ [STATUS] == 200\n",
|
||||
},
|
||||
{
|
||||
Name: "resolved",
|
||||
Provider: AlertProvider{},
|
||||
Alert: alert.Alert{Description: &secondDescription, SuccessThreshold: 5, FailureThreshold: 3},
|
||||
Resolved: true,
|
||||
ExpectedSubject: "[endpoint-name] Alert resolved",
|
||||
ExpectedBody: "An alert for endpoint-name has been resolved after passing successfully 5 time(s) in a row\n\nAlert description: description-2\n\nCondition results:\n✅ [CONNECTED] == true\n✅ [STATUS] == 200\n",
|
||||
},
|
||||
}
|
||||
for _, scenario := range scenarios {
|
||||
t.Run(scenario.Name, func(t *testing.T) {
|
||||
subject, body := scenario.Provider.buildMessageSubjectAndBody(
|
||||
&core.Endpoint{Name: "endpoint-name"},
|
||||
&scenario.Alert,
|
||||
&core.Result{
|
||||
ConditionResults: []*core.ConditionResult{
|
||||
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
||||
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
||||
},
|
||||
},
|
||||
scenario.Resolved,
|
||||
)
|
||||
if subject != scenario.ExpectedSubject {
|
||||
t.Errorf("expected subject to be %s, got %s", scenario.ExpectedSubject, subject)
|
||||
}
|
||||
if body != scenario.ExpectedBody {
|
||||
t.Errorf("expected body to be %s, got %s", scenario.ExpectedBody, body)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -1,11 +1,14 @@
|
|||
package mattermost
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/TwiN/gatus/v3/alerting/alert"
|
||||
"github.com/TwiN/gatus/v3/alerting/provider/custom"
|
||||
"github.com/TwiN/gatus/v3/client"
|
||||
"github.com/TwiN/gatus/v3/core"
|
||||
)
|
||||
|
@ -29,10 +32,37 @@ func (provider *AlertProvider) IsValid() bool {
|
|||
return len(provider.WebhookURL) > 0
|
||||
}
|
||||
|
||||
// ToCustomAlertProvider converts the provider into a custom.AlertProvider
|
||||
func (provider *AlertProvider) ToCustomAlertProvider(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) *custom.AlertProvider {
|
||||
var message string
|
||||
var color string
|
||||
// Send an alert using the provider
|
||||
func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) error {
|
||||
if os.Getenv("MOCK_ALERT_PROVIDER") == "true" {
|
||||
if os.Getenv("MOCK_ALERT_PROVIDER_ERROR") == "true" {
|
||||
return errors.New("error")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
buffer := bytes.NewBuffer([]byte(provider.buildRequestBody(endpoint, alert, result, resolved)))
|
||||
request, err := http.NewRequest(http.MethodPost, provider.WebhookURL, buffer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
request.Header.Set("Content-Type", "application/json")
|
||||
response, err := client.GetHTTPClient(provider.ClientConfig).Do(request)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if response.StatusCode > 399 {
|
||||
body, err := ioutil.ReadAll(response.Body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("call to provider alert returned status code %d", response.StatusCode)
|
||||
}
|
||||
return fmt.Errorf("call to provider alert returned status code %d: %s", response.StatusCode, string(body))
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// buildRequestBody builds the request body for the provider
|
||||
func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) string {
|
||||
var message, color string
|
||||
if resolved {
|
||||
message = fmt.Sprintf("An alert for *%s* has been resolved after passing successfully %d time(s) in a row", endpoint.Name, alert.SuccessThreshold)
|
||||
color = "#36A64F"
|
||||
|
@ -54,11 +84,7 @@ func (provider *AlertProvider) ToCustomAlertProvider(endpoint *core.Endpoint, al
|
|||
if alertDescription := alert.GetDescription(); len(alertDescription) > 0 {
|
||||
description = ":\\n> " + alertDescription
|
||||
}
|
||||
return &custom.AlertProvider{
|
||||
URL: provider.WebhookURL,
|
||||
Method: http.MethodPost,
|
||||
ClientConfig: provider.ClientConfig,
|
||||
Body: fmt.Sprintf(`{
|
||||
return fmt.Sprintf(`{
|
||||
"text": "",
|
||||
"username": "gatus",
|
||||
"icon_url": "https://raw.githubusercontent.com/TwiN/gatus/master/.github/assets/logo.png",
|
||||
|
@ -83,9 +109,7 @@ func (provider *AlertProvider) ToCustomAlertProvider(endpoint *core.Endpoint, al
|
|||
]
|
||||
}
|
||||
]
|
||||
}`, message, message, description, color, endpoint.URL, results),
|
||||
Headers: map[string]string{"Content-Type": "application/json"},
|
||||
}
|
||||
}`, message, message, description, color, endpoint.URL, results)
|
||||
}
|
||||
|
||||
// GetDefaultAlert returns the provider's default alert configuration
|
||||
|
|
|
@ -2,8 +2,6 @@ package mattermost
|
|||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/TwiN/gatus/v3/alerting/alert"
|
||||
|
@ -21,50 +19,51 @@ func TestAlertProvider_IsValid(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestAlertProvider_ToCustomAlertProviderWithResolvedAlert(t *testing.T) {
|
||||
provider := AlertProvider{WebhookURL: "http://example.org"}
|
||||
alertDescription := "test"
|
||||
customAlertProvider := provider.ToCustomAlertProvider(&core.Endpoint{Name: "svc"}, &alert.Alert{Description: &alertDescription}, &core.Result{ConditionResults: []*core.ConditionResult{{Condition: "SUCCESSFUL_CONDITION", Success: true}}}, true)
|
||||
if customAlertProvider == nil {
|
||||
t.Fatal("customAlertProvider shouldn't have been nil")
|
||||
func TestAlertProvider_buildRequestBody(t *testing.T) {
|
||||
firstDescription := "description-1"
|
||||
secondDescription := "description-2"
|
||||
scenarios := []struct {
|
||||
Name string
|
||||
Provider AlertProvider
|
||||
Alert alert.Alert
|
||||
Resolved bool
|
||||
ExpectedBody string
|
||||
}{
|
||||
{
|
||||
Name: "triggered",
|
||||
Provider: AlertProvider{},
|
||||
Alert: alert.Alert{Description: &firstDescription, SuccessThreshold: 5, FailureThreshold: 3},
|
||||
Resolved: false,
|
||||
ExpectedBody: "{\n \"text\": \"\",\n \"username\": \"gatus\",\n \"icon_url\": \"https://raw.githubusercontent.com/TwiN/gatus/master/.github/assets/logo.png\",\n \"attachments\": [\n {\n \"title\": \":rescue_worker_helmet: Gatus\",\n \"fallback\": \"Gatus - An alert for *endpoint-name* has been triggered due to having failed 3 time(s) in a row\",\n \"text\": \"An alert for *endpoint-name* has been triggered due to having failed 3 time(s) in a row:\\n> description-1\",\n \"short\": false,\n \"color\": \"#DD0000\",\n \"fields\": [\n {\n \"title\": \"URL\",\n \"value\": \"\",\n \"short\": false\n },\n {\n \"title\": \"Condition results\",\n \"value\": \":x: - `[CONNECTED] == true`\\n:x: - `[STATUS] == 200`\\n\",\n \"short\": false\n }\n ]\n }\n ]\n}",
|
||||
},
|
||||
{
|
||||
Name: "resolved",
|
||||
Provider: AlertProvider{},
|
||||
Alert: alert.Alert{Description: &secondDescription, SuccessThreshold: 5, FailureThreshold: 3},
|
||||
Resolved: true,
|
||||
ExpectedBody: "{\n \"text\": \"\",\n \"username\": \"gatus\",\n \"icon_url\": \"https://raw.githubusercontent.com/TwiN/gatus/master/.github/assets/logo.png\",\n \"attachments\": [\n {\n \"title\": \":rescue_worker_helmet: Gatus\",\n \"fallback\": \"Gatus - An alert for *endpoint-name* has been resolved after passing successfully 5 time(s) in a row\",\n \"text\": \"An alert for *endpoint-name* has been resolved after passing successfully 5 time(s) in a row:\\n> description-2\",\n \"short\": false,\n \"color\": \"#36A64F\",\n \"fields\": [\n {\n \"title\": \"URL\",\n \"value\": \"\",\n \"short\": false\n },\n {\n \"title\": \"Condition results\",\n \"value\": \":white_check_mark: - `[CONNECTED] == true`\\n:white_check_mark: - `[STATUS] == 200`\\n\",\n \"short\": false\n }\n ]\n }\n ]\n}",
|
||||
},
|
||||
}
|
||||
if !strings.Contains(customAlertProvider.Body, "resolved") {
|
||||
t.Error("customAlertProvider.Body should've contained the substring resolved")
|
||||
}
|
||||
if customAlertProvider.URL != "http://example.org" {
|
||||
t.Errorf("expected URL to be %s, got %s", "http://example.org", customAlertProvider.URL)
|
||||
}
|
||||
if customAlertProvider.Method != http.MethodPost {
|
||||
t.Errorf("expected method to be %s, got %s", http.MethodPost, customAlertProvider.Method)
|
||||
}
|
||||
body := make(map[string]interface{})
|
||||
err := json.Unmarshal([]byte(customAlertProvider.Body), &body)
|
||||
if err != nil {
|
||||
t.Error("expected body to be valid JSON, got error:", err.Error())
|
||||
}
|
||||
if expected := "An alert for *svc* has been resolved after passing successfully 0 time(s) in a row:\n> test"; expected != body["attachments"].([]interface{})[0].(map[string]interface{})["text"] {
|
||||
t.Errorf("expected $.attachments[0].description to be %s, got %s", expected, body["attachments"].([]interface{})[0].(map[string]interface{})["text"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestAlertProvider_ToCustomAlertProviderWithTriggeredAlert(t *testing.T) {
|
||||
provider := AlertProvider{WebhookURL: "http://example.org"}
|
||||
customAlertProvider := provider.ToCustomAlertProvider(&core.Endpoint{}, &alert.Alert{}, &core.Result{ConditionResults: []*core.ConditionResult{{Condition: "UNSUCCESSFUL_CONDITION", Success: false}}}, false)
|
||||
if customAlertProvider == nil {
|
||||
t.Fatal("customAlertProvider shouldn't have been nil")
|
||||
}
|
||||
if !strings.Contains(customAlertProvider.Body, "triggered") {
|
||||
t.Error("customAlertProvider.Body should've contained the substring triggered")
|
||||
}
|
||||
if customAlertProvider.URL != "http://example.org" {
|
||||
t.Errorf("expected URL to be %s, got %s", "http://example.org", customAlertProvider.URL)
|
||||
}
|
||||
if customAlertProvider.Method != http.MethodPost {
|
||||
t.Errorf("expected method to be %s, got %s", http.MethodPost, customAlertProvider.Method)
|
||||
}
|
||||
body := make(map[string]interface{})
|
||||
err := json.Unmarshal([]byte(customAlertProvider.Body), &body)
|
||||
if err != nil {
|
||||
t.Error("expected body to be valid JSON, got error:", err.Error())
|
||||
for _, scenario := range scenarios {
|
||||
t.Run(scenario.Name, func(t *testing.T) {
|
||||
body := scenario.Provider.buildRequestBody(
|
||||
&core.Endpoint{Name: "endpoint-name"},
|
||||
&scenario.Alert,
|
||||
&core.Result{
|
||||
ConditionResults: []*core.ConditionResult{
|
||||
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
||||
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
||||
},
|
||||
},
|
||||
scenario.Resolved,
|
||||
)
|
||||
if body != scenario.ExpectedBody {
|
||||
t.Errorf("expected %s, got %s", scenario.ExpectedBody, body)
|
||||
}
|
||||
out := make(map[string]interface{})
|
||||
if err := json.Unmarshal([]byte(body), &out); err != nil {
|
||||
t.Error("expected body to be valid JSON, got error:", err.Error())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,11 +1,15 @@
|
|||
package messagebird
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/TwiN/gatus/v3/alerting/alert"
|
||||
"github.com/TwiN/gatus/v3/alerting/provider/custom"
|
||||
"github.com/TwiN/gatus/v3/client"
|
||||
"github.com/TwiN/gatus/v3/core"
|
||||
)
|
||||
|
||||
|
@ -20,7 +24,7 @@ type AlertProvider struct {
|
|||
Recipients string `yaml:"recipients"`
|
||||
|
||||
// DefaultAlert is the default alert configuration to use for endpoints with an alert of the appropriate type
|
||||
DefaultAlert *alert.Alert `yaml:"default-alert"`
|
||||
DefaultAlert *alert.Alert `yaml:"default-alert,omitempty"`
|
||||
}
|
||||
|
||||
// IsValid returns whether the provider's configuration is valid
|
||||
|
@ -28,28 +32,49 @@ func (provider *AlertProvider) IsValid() bool {
|
|||
return len(provider.AccessKey) > 0 && len(provider.Originator) > 0 && len(provider.Recipients) > 0
|
||||
}
|
||||
|
||||
// ToCustomAlertProvider converts the provider into a custom.AlertProvider
|
||||
// Reference doc for messagebird https://developers.messagebird.com/api/sms-messaging/#send-outbound-sms
|
||||
func (provider *AlertProvider) ToCustomAlertProvider(endpoint *core.Endpoint, alert *alert.Alert, _ *core.Result, resolved bool) *custom.AlertProvider {
|
||||
// Send an alert using the provider
|
||||
// Reference doc for messagebird: https://developers.messagebird.com/api/sms-messaging/#send-outbound-sms
|
||||
func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) error {
|
||||
if os.Getenv("MOCK_ALERT_PROVIDER") == "true" {
|
||||
if os.Getenv("MOCK_ALERT_PROVIDER_ERROR") == "true" {
|
||||
return errors.New("error")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
buffer := bytes.NewBuffer([]byte(provider.buildRequestBody(endpoint, alert, result, resolved)))
|
||||
request, err := http.NewRequest(http.MethodPost, restAPIURL, buffer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
request.Header.Set("Content-Type", "application/json")
|
||||
request.Header.Set("Authorization", fmt.Sprintf("AccessKey %s", provider.AccessKey))
|
||||
response, err := client.GetHTTPClient(nil).Do(request)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if response.StatusCode > 399 {
|
||||
body, err := ioutil.ReadAll(response.Body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("call to provider alert returned status code %d", response.StatusCode)
|
||||
}
|
||||
return fmt.Errorf("call to provider alert returned status code %d: %s", response.StatusCode, string(body))
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// buildRequestBody builds the request body for the provider
|
||||
func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) string {
|
||||
var message string
|
||||
if resolved {
|
||||
message = fmt.Sprintf("RESOLVED: %s - %s", endpoint.Name, alert.GetDescription())
|
||||
} else {
|
||||
message = fmt.Sprintf("TRIGGERED: %s - %s", endpoint.Name, alert.GetDescription())
|
||||
}
|
||||
return &custom.AlertProvider{
|
||||
URL: restAPIURL,
|
||||
Method: http.MethodPost,
|
||||
Body: fmt.Sprintf(`{
|
||||
return fmt.Sprintf(`{
|
||||
"originator": "%s",
|
||||
"recipients": "%s",
|
||||
"body": "%s"
|
||||
}`, provider.Originator, provider.Recipients, message),
|
||||
Headers: map[string]string{
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": fmt.Sprintf("AccessKey %s", provider.AccessKey),
|
||||
},
|
||||
}
|
||||
}`, provider.Originator, provider.Recipients, message)
|
||||
}
|
||||
|
||||
// GetDefaultAlert returns the provider's default alert configuration
|
||||
|
|
|
@ -2,8 +2,6 @@ package messagebird
|
|||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/TwiN/gatus/v3/alerting/alert"
|
||||
|
@ -25,54 +23,51 @@ func TestMessagebirdAlertProvider_IsValid(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestAlertProvider_ToCustomAlertProviderWithResolvedAlert(t *testing.T) {
|
||||
provider := AlertProvider{
|
||||
AccessKey: "1",
|
||||
Originator: "1",
|
||||
Recipients: "1",
|
||||
func TestAlertProvider_buildRequestBody(t *testing.T) {
|
||||
firstDescription := "description-1"
|
||||
secondDescription := "description-2"
|
||||
scenarios := []struct {
|
||||
Name string
|
||||
Provider AlertProvider
|
||||
Alert alert.Alert
|
||||
Resolved bool
|
||||
ExpectedBody string
|
||||
}{
|
||||
{
|
||||
Name: "triggered",
|
||||
Provider: AlertProvider{AccessKey: "1", Originator: "2", Recipients: "3"},
|
||||
Alert: alert.Alert{Description: &firstDescription, SuccessThreshold: 5, FailureThreshold: 3},
|
||||
Resolved: false,
|
||||
ExpectedBody: "{\n \"originator\": \"2\",\n \"recipients\": \"3\",\n \"body\": \"TRIGGERED: endpoint-name - description-1\"\n}",
|
||||
},
|
||||
{
|
||||
Name: "resolved",
|
||||
Provider: AlertProvider{AccessKey: "4", Originator: "5", Recipients: "6"},
|
||||
Alert: alert.Alert{Description: &secondDescription, SuccessThreshold: 5, FailureThreshold: 3},
|
||||
Resolved: true,
|
||||
ExpectedBody: "{\n \"originator\": \"5\",\n \"recipients\": \"6\",\n \"body\": \"RESOLVED: endpoint-name - description-2\"\n}",
|
||||
},
|
||||
}
|
||||
customAlertProvider := provider.ToCustomAlertProvider(&core.Endpoint{}, &alert.Alert{}, &core.Result{}, true)
|
||||
if customAlertProvider == nil {
|
||||
t.Fatal("customAlertProvider shouldn't have been nil")
|
||||
}
|
||||
if !strings.Contains(customAlertProvider.Body, "RESOLVED") {
|
||||
t.Error("customAlertProvider.Body should've contained the substring RESOLVED")
|
||||
}
|
||||
if customAlertProvider.URL != "https://rest.messagebird.com/messages" {
|
||||
t.Errorf("expected URL to be %s, got %s", "https://rest.messagebird.com/messages", customAlertProvider.URL)
|
||||
}
|
||||
if customAlertProvider.Method != http.MethodPost {
|
||||
t.Errorf("expected method to be %s, got %s", http.MethodPost, customAlertProvider.Method)
|
||||
}
|
||||
body := make(map[string]interface{})
|
||||
err := json.Unmarshal([]byte(customAlertProvider.Body), &body)
|
||||
if err != nil {
|
||||
t.Error("expected body to be valid JSON, got error:", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func TestAlertProvider_ToCustomAlertProviderWithTriggeredAlert(t *testing.T) {
|
||||
provider := AlertProvider{
|
||||
AccessKey: "1",
|
||||
Originator: "1",
|
||||
Recipients: "1",
|
||||
}
|
||||
customAlertProvider := provider.ToCustomAlertProvider(&core.Endpoint{}, &alert.Alert{}, &core.Result{}, false)
|
||||
if customAlertProvider == nil {
|
||||
t.Fatal("customAlertProvider shouldn't have been nil")
|
||||
}
|
||||
if !strings.Contains(customAlertProvider.Body, "TRIGGERED") {
|
||||
t.Error("customAlertProvider.Body should've contained the substring TRIGGERED")
|
||||
}
|
||||
if customAlertProvider.URL != "https://rest.messagebird.com/messages" {
|
||||
t.Errorf("expected URL to be %s, got %s", "https://rest.messagebird.com/messages", customAlertProvider.URL)
|
||||
}
|
||||
if customAlertProvider.Method != http.MethodPost {
|
||||
t.Errorf("expected method to be %s, got %s", http.MethodPost, customAlertProvider.Method)
|
||||
}
|
||||
body := make(map[string]interface{})
|
||||
err := json.Unmarshal([]byte(customAlertProvider.Body), &body)
|
||||
if err != nil {
|
||||
t.Error("expected body to be valid JSON, got error:", err.Error())
|
||||
for _, scenario := range scenarios {
|
||||
t.Run(scenario.Name, func(t *testing.T) {
|
||||
body := scenario.Provider.buildRequestBody(
|
||||
&core.Endpoint{Name: "endpoint-name"},
|
||||
&scenario.Alert,
|
||||
&core.Result{
|
||||
ConditionResults: []*core.ConditionResult{
|
||||
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
||||
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
||||
},
|
||||
},
|
||||
scenario.Resolved,
|
||||
)
|
||||
if body != scenario.ExpectedBody {
|
||||
t.Errorf("expected %s, got %s", scenario.ExpectedBody, body)
|
||||
}
|
||||
out := make(map[string]interface{})
|
||||
if err := json.Unmarshal([]byte(body), &out); err != nil {
|
||||
t.Error("expected body to be valid JSON, got error:", err.Error())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,11 +1,17 @@
|
|||
package pagerduty
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/TwiN/gatus/v3/alerting/alert"
|
||||
"github.com/TwiN/gatus/v3/alerting/provider/custom"
|
||||
"github.com/TwiN/gatus/v3/client"
|
||||
"github.com/TwiN/gatus/v3/core"
|
||||
)
|
||||
|
||||
|
@ -18,10 +24,10 @@ type AlertProvider struct {
|
|||
IntegrationKey string `yaml:"integration-key"`
|
||||
|
||||
// DefaultAlert is the default alert configuration to use for endpoints with an alert of the appropriate type
|
||||
DefaultAlert *alert.Alert `yaml:"default-alert"`
|
||||
DefaultAlert *alert.Alert `yaml:"default-alert,omitempty"`
|
||||
|
||||
// Overrides is a list of Override that may be prioritized over the default configuration
|
||||
Overrides []Override `yaml:"overrides"`
|
||||
Overrides []Override `yaml:"overrides,omitempty"`
|
||||
}
|
||||
|
||||
// Override is a case under which the default integration is overridden
|
||||
|
@ -45,10 +51,55 @@ func (provider *AlertProvider) IsValid() bool {
|
|||
return len(provider.IntegrationKey) == 32 || len(provider.Overrides) != 0
|
||||
}
|
||||
|
||||
// ToCustomAlertProvider converts the provider into a custom.AlertProvider
|
||||
// Send an alert using the provider
|
||||
//
|
||||
// relevant: https://developer.pagerduty.com/docs/events-api-v2/trigger-events/
|
||||
func (provider *AlertProvider) ToCustomAlertProvider(endpoint *core.Endpoint, alert *alert.Alert, _ *core.Result, resolved bool) *custom.AlertProvider {
|
||||
// Relevant: https://developer.pagerduty.com/docs/events-api-v2/trigger-events/
|
||||
func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) error {
|
||||
if os.Getenv("MOCK_ALERT_PROVIDER") == "true" {
|
||||
if os.Getenv("MOCK_ALERT_PROVIDER_ERROR") == "true" {
|
||||
return errors.New("error")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
buffer := bytes.NewBuffer([]byte(provider.buildRequestBody(endpoint, alert, result, resolved)))
|
||||
request, err := http.NewRequest(http.MethodPost, restAPIURL, buffer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
request.Header.Set("Content-Type", "application/json")
|
||||
response, err := client.GetHTTPClient(nil).Do(request)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if response.StatusCode > 399 {
|
||||
body, err := ioutil.ReadAll(response.Body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("call to provider alert returned status code %d", response.StatusCode)
|
||||
}
|
||||
return fmt.Errorf("call to provider alert returned status code %d: %s", response.StatusCode, string(body))
|
||||
}
|
||||
if alert.IsSendingOnResolved() {
|
||||
if resolved {
|
||||
// The alert has been resolved and there's no error, so we can clear the alert's ResolveKey
|
||||
alert.ResolveKey = ""
|
||||
} else {
|
||||
// We need to retrieve the resolve key from the response
|
||||
body, err := ioutil.ReadAll(response.Body)
|
||||
var payload pagerDutyResponsePayload
|
||||
if err = json.Unmarshal(body, &payload); err != nil {
|
||||
// Silently fail. We don't want to create tons of alerts just because we failed to parse
|
||||
// the body.
|
||||
log.Printf("[pagerduty][Send] Ran into error unmarshaling pagerduty response: %s", err.Error())
|
||||
} else {
|
||||
alert.ResolveKey = payload.DedupKey
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// buildRequestBody builds the request body for the provider
|
||||
func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) string {
|
||||
var message, eventAction, resolveKey string
|
||||
if resolved {
|
||||
message = fmt.Sprintf("RESOLVED: %s - %s", endpoint.Name, alert.GetDescription())
|
||||
|
@ -59,10 +110,7 @@ func (provider *AlertProvider) ToCustomAlertProvider(endpoint *core.Endpoint, al
|
|||
eventAction = "trigger"
|
||||
resolveKey = ""
|
||||
}
|
||||
return &custom.AlertProvider{
|
||||
URL: restAPIURL,
|
||||
Method: http.MethodPost,
|
||||
Body: fmt.Sprintf(`{
|
||||
return fmt.Sprintf(`{
|
||||
"routing_key": "%s",
|
||||
"dedup_key": "%s",
|
||||
"event_action": "%s",
|
||||
|
@ -71,11 +119,7 @@ func (provider *AlertProvider) ToCustomAlertProvider(endpoint *core.Endpoint, al
|
|||
"source": "%s",
|
||||
"severity": "critical"
|
||||
}
|
||||
}`, provider.getIntegrationKeyForGroup(endpoint.Group), resolveKey, eventAction, message, endpoint.Name),
|
||||
Headers: map[string]string{
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
}
|
||||
}`, provider.getIntegrationKeyForGroup(endpoint.Group), resolveKey, eventAction, message, endpoint.Name)
|
||||
}
|
||||
|
||||
// getIntegrationKeyForGroup returns the appropriate pagerduty integration key for a given group
|
||||
|
@ -94,3 +138,9 @@ func (provider *AlertProvider) getIntegrationKeyForGroup(group string) string {
|
|||
func (provider AlertProvider) GetDefaultAlert() *alert.Alert {
|
||||
return provider.DefaultAlert
|
||||
}
|
||||
|
||||
type pagerDutyResponsePayload struct {
|
||||
Status string `json:"status"`
|
||||
Message string `json:"message"`
|
||||
DedupKey string `json:"dedup_key"`
|
||||
}
|
||||
|
|
|
@ -2,8 +2,6 @@ package pagerduty
|
|||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/TwiN/gatus/v3/alerting/alert"
|
||||
|
@ -57,107 +55,41 @@ func TestAlertProvider_IsValidWithOverride(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestAlertProvider_ToCustomAlertProviderWithResolvedAlert(t *testing.T) {
|
||||
provider := AlertProvider{IntegrationKey: "00000000000000000000000000000000"}
|
||||
customAlertProvider := provider.ToCustomAlertProvider(&core.Endpoint{}, &alert.Alert{}, &core.Result{}, true)
|
||||
if customAlertProvider == nil {
|
||||
t.Fatal("customAlertProvider shouldn't have been nil")
|
||||
}
|
||||
if !strings.Contains(customAlertProvider.Body, "RESOLVED") {
|
||||
t.Error("customAlertProvider.Body should've contained the substring RESOLVED")
|
||||
}
|
||||
if customAlertProvider.URL != "https://events.pagerduty.com/v2/enqueue" {
|
||||
t.Errorf("expected URL to be %s, got %s", "https://events.pagerduty.com/v2/enqueue", customAlertProvider.URL)
|
||||
}
|
||||
if customAlertProvider.Method != http.MethodPost {
|
||||
t.Errorf("expected method to be %s, got %s", http.MethodPost, customAlertProvider.Method)
|
||||
}
|
||||
body := make(map[string]interface{})
|
||||
err := json.Unmarshal([]byte(customAlertProvider.Body), &body)
|
||||
if err != nil {
|
||||
t.Error("expected body to be valid JSON, got error:", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func TestAlertProvider_ToCustomAlertProviderWithResolvedAlertAndOverride(t *testing.T) {
|
||||
provider := AlertProvider{
|
||||
IntegrationKey: "",
|
||||
Overrides: []Override{
|
||||
{
|
||||
IntegrationKey: "00000000000000000000000000000000",
|
||||
Group: "group",
|
||||
},
|
||||
func TestAlertProvider_buildRequestBody(t *testing.T) {
|
||||
description := "test"
|
||||
scenarios := []struct {
|
||||
Name string
|
||||
Provider AlertProvider
|
||||
Alert alert.Alert
|
||||
Resolved bool
|
||||
ExpectedBody string
|
||||
}{
|
||||
{
|
||||
Name: "triggered",
|
||||
Provider: AlertProvider{IntegrationKey: "00000000000000000000000000000000"},
|
||||
Alert: alert.Alert{Description: &description},
|
||||
Resolved: false,
|
||||
ExpectedBody: "{\n \"routing_key\": \"00000000000000000000000000000000\",\n \"dedup_key\": \"\",\n \"event_action\": \"trigger\",\n \"payload\": {\n \"summary\": \"TRIGGERED: - test\",\n \"source\": \"\",\n \"severity\": \"critical\"\n }\n}",
|
||||
},
|
||||
{
|
||||
Name: "resolved",
|
||||
Provider: AlertProvider{IntegrationKey: "00000000000000000000000000000000"},
|
||||
Alert: alert.Alert{Description: &description, ResolveKey: "key"},
|
||||
Resolved: true,
|
||||
ExpectedBody: "{\n \"routing_key\": \"00000000000000000000000000000000\",\n \"dedup_key\": \"key\",\n \"event_action\": \"resolve\",\n \"payload\": {\n \"summary\": \"RESOLVED: - test\",\n \"source\": \"\",\n \"severity\": \"critical\"\n }\n}",
|
||||
},
|
||||
}
|
||||
customAlertProvider := provider.ToCustomAlertProvider(&core.Endpoint{}, &alert.Alert{}, &core.Result{}, true)
|
||||
if customAlertProvider == nil {
|
||||
t.Fatal("customAlertProvider shouldn't have been nil")
|
||||
}
|
||||
if !strings.Contains(customAlertProvider.Body, "RESOLVED") {
|
||||
t.Error("customAlertProvider.Body should've contained the substring RESOLVED")
|
||||
}
|
||||
if customAlertProvider.URL != "https://events.pagerduty.com/v2/enqueue" {
|
||||
t.Errorf("expected URL to be %s, got %s", "https://events.pagerduty.com/v2/enqueue", customAlertProvider.URL)
|
||||
}
|
||||
if customAlertProvider.Method != http.MethodPost {
|
||||
t.Errorf("expected method to be %s, got %s", http.MethodPost, customAlertProvider.Method)
|
||||
}
|
||||
body := make(map[string]interface{})
|
||||
err := json.Unmarshal([]byte(customAlertProvider.Body), &body)
|
||||
if err != nil {
|
||||
t.Error("expected body to be valid JSON, got error:", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func TestAlertProvider_ToCustomAlertProviderWithTriggeredAlert(t *testing.T) {
|
||||
provider := AlertProvider{IntegrationKey: "00000000000000000000000000000000"}
|
||||
customAlertProvider := provider.ToCustomAlertProvider(&core.Endpoint{}, &alert.Alert{}, &core.Result{}, false)
|
||||
if customAlertProvider == nil {
|
||||
t.Fatal("customAlertProvider shouldn't have been nil")
|
||||
}
|
||||
if !strings.Contains(customAlertProvider.Body, "TRIGGERED") {
|
||||
t.Error("customAlertProvider.Body should've contained the substring TRIGGERED")
|
||||
}
|
||||
if customAlertProvider.URL != "https://events.pagerduty.com/v2/enqueue" {
|
||||
t.Errorf("expected URL to be %s, got %s", "https://events.pagerduty.com/v2/enqueue", customAlertProvider.URL)
|
||||
}
|
||||
if customAlertProvider.Method != http.MethodPost {
|
||||
t.Errorf("expected method to be %s, got %s", http.MethodPost, customAlertProvider.Method)
|
||||
}
|
||||
body := make(map[string]interface{})
|
||||
err := json.Unmarshal([]byte(customAlertProvider.Body), &body)
|
||||
if err != nil {
|
||||
t.Error("expected body to be valid JSON, got error:", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func TestAlertProvider_ToCustomAlertProviderWithTriggeredAlertAndOverride(t *testing.T) {
|
||||
provider := AlertProvider{
|
||||
IntegrationKey: "",
|
||||
Overrides: []Override{
|
||||
{
|
||||
IntegrationKey: "00000000000000000000000000000000",
|
||||
Group: "group",
|
||||
},
|
||||
},
|
||||
}
|
||||
customAlertProvider := provider.ToCustomAlertProvider(&core.Endpoint{}, &alert.Alert{}, &core.Result{}, false)
|
||||
if customAlertProvider == nil {
|
||||
t.Fatal("customAlertProvider shouldn't have been nil")
|
||||
}
|
||||
if !strings.Contains(customAlertProvider.Body, "TRIGGERED") {
|
||||
t.Error("customAlertProvider.Body should've contained the substring TRIGGERED")
|
||||
}
|
||||
if customAlertProvider.URL != "https://events.pagerduty.com/v2/enqueue" {
|
||||
t.Errorf("expected URL to be %s, got %s", "https://events.pagerduty.com/v2/enqueue", customAlertProvider.URL)
|
||||
}
|
||||
if customAlertProvider.Method != http.MethodPost {
|
||||
t.Errorf("expected method to be %s, got %s", http.MethodPost, customAlertProvider.Method)
|
||||
}
|
||||
body := make(map[string]interface{})
|
||||
err := json.Unmarshal([]byte(customAlertProvider.Body), &body)
|
||||
if err != nil {
|
||||
t.Error("expected body to be valid JSON, got error:", err.Error())
|
||||
for _, scenario := range scenarios {
|
||||
t.Run(scenario.Name, func(t *testing.T) {
|
||||
body := scenario.Provider.buildRequestBody(&core.Endpoint{}, &scenario.Alert, &core.Result{}, scenario.Resolved)
|
||||
if body != scenario.ExpectedBody {
|
||||
t.Errorf("expected %s, got %s", scenario.ExpectedBody, body)
|
||||
}
|
||||
out := make(map[string]interface{})
|
||||
if err := json.Unmarshal([]byte(body), &out); err != nil {
|
||||
t.Error("expected body to be valid JSON, got error:", err.Error())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -4,6 +4,7 @@ import (
|
|||
"github.com/TwiN/gatus/v3/alerting/alert"
|
||||
"github.com/TwiN/gatus/v3/alerting/provider/custom"
|
||||
"github.com/TwiN/gatus/v3/alerting/provider/discord"
|
||||
"github.com/TwiN/gatus/v3/alerting/provider/email"
|
||||
"github.com/TwiN/gatus/v3/alerting/provider/mattermost"
|
||||
"github.com/TwiN/gatus/v3/alerting/provider/messagebird"
|
||||
"github.com/TwiN/gatus/v3/alerting/provider/pagerduty"
|
||||
|
@ -19,11 +20,11 @@ type AlertProvider interface {
|
|||
// IsValid returns whether the provider's configuration is valid
|
||||
IsValid() bool
|
||||
|
||||
// ToCustomAlertProvider converts the provider into a custom.AlertProvider
|
||||
ToCustomAlertProvider(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) *custom.AlertProvider
|
||||
|
||||
// GetDefaultAlert returns the provider's default alert configuration
|
||||
GetDefaultAlert() *alert.Alert
|
||||
|
||||
// Send an alert using the provider
|
||||
Send(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) error
|
||||
}
|
||||
|
||||
// ParseWithDefaultAlert parses an Endpoint alert by using the provider's default alert as a baseline
|
||||
|
@ -52,6 +53,7 @@ var (
|
|||
// Validate interface implementation on compile
|
||||
_ AlertProvider = (*custom.AlertProvider)(nil)
|
||||
_ AlertProvider = (*discord.AlertProvider)(nil)
|
||||
_ AlertProvider = (*email.AlertProvider)(nil)
|
||||
_ AlertProvider = (*mattermost.AlertProvider)(nil)
|
||||
_ AlertProvider = (*messagebird.AlertProvider)(nil)
|
||||
_ AlertProvider = (*pagerduty.AlertProvider)(nil)
|
||||
|
|
|
@ -1,11 +1,15 @@
|
|||
package slack
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/TwiN/gatus/v3/alerting/alert"
|
||||
"github.com/TwiN/gatus/v3/alerting/provider/custom"
|
||||
"github.com/TwiN/gatus/v3/client"
|
||||
"github.com/TwiN/gatus/v3/core"
|
||||
)
|
||||
|
||||
|
@ -22,8 +26,36 @@ func (provider *AlertProvider) IsValid() bool {
|
|||
return len(provider.WebhookURL) > 0
|
||||
}
|
||||
|
||||
// ToCustomAlertProvider converts the provider into a custom.AlertProvider
|
||||
func (provider *AlertProvider) ToCustomAlertProvider(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) *custom.AlertProvider {
|
||||
// Send an alert using the provider
|
||||
func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) error {
|
||||
if os.Getenv("MOCK_ALERT_PROVIDER") == "true" {
|
||||
if os.Getenv("MOCK_ALERT_PROVIDER_ERROR") == "true" {
|
||||
return errors.New("error")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
buffer := bytes.NewBuffer([]byte(provider.buildRequestBody(endpoint, alert, result, resolved)))
|
||||
request, err := http.NewRequest(http.MethodPost, provider.WebhookURL, buffer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
request.Header.Set("Content-Type", "application/json")
|
||||
response, err := client.GetHTTPClient(nil).Do(request)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if response.StatusCode > 399 {
|
||||
body, err := ioutil.ReadAll(response.Body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("call to provider alert returned status code %d", response.StatusCode)
|
||||
}
|
||||
return fmt.Errorf("call to provider alert returned status code %d: %s", response.StatusCode, string(body))
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// buildRequestBody builds the request body for the provider
|
||||
func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) string {
|
||||
var message, color, results string
|
||||
if resolved {
|
||||
message = fmt.Sprintf("An alert for *%s* has been resolved after passing successfully %d time(s) in a row", endpoint.Name, alert.SuccessThreshold)
|
||||
|
@ -45,10 +77,7 @@ func (provider *AlertProvider) ToCustomAlertProvider(endpoint *core.Endpoint, al
|
|||
if alertDescription := alert.GetDescription(); len(alertDescription) > 0 {
|
||||
description = ":\\n> " + alertDescription
|
||||
}
|
||||
return &custom.AlertProvider{
|
||||
URL: provider.WebhookURL,
|
||||
Method: http.MethodPost,
|
||||
Body: fmt.Sprintf(`{
|
||||
return fmt.Sprintf(`{
|
||||
"text": "",
|
||||
"attachments": [
|
||||
{
|
||||
|
@ -65,9 +94,7 @@ func (provider *AlertProvider) ToCustomAlertProvider(endpoint *core.Endpoint, al
|
|||
]
|
||||
}
|
||||
]
|
||||
}`, message, description, color, results),
|
||||
Headers: map[string]string{"Content-Type": "application/json"},
|
||||
}
|
||||
}`, message, description, color, results)
|
||||
}
|
||||
|
||||
// GetDefaultAlert returns the provider's default alert configuration
|
||||
|
|
|
@ -2,8 +2,6 @@ package slack
|
|||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/TwiN/gatus/v3/alerting/alert"
|
||||
|
@ -21,50 +19,51 @@ func TestAlertProvider_IsValid(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestAlertProvider_ToCustomAlertProviderWithResolvedAlert(t *testing.T) {
|
||||
provider := AlertProvider{WebhookURL: "http://example.com"}
|
||||
alertDescription := "test"
|
||||
customAlertProvider := provider.ToCustomAlertProvider(&core.Endpoint{Name: "svc"}, &alert.Alert{Description: &alertDescription}, &core.Result{ConditionResults: []*core.ConditionResult{{Condition: "SUCCESSFUL_CONDITION", Success: true}}}, true)
|
||||
if customAlertProvider == nil {
|
||||
t.Fatal("customAlertProvider shouldn't have been nil")
|
||||
func TestAlertProvider_buildRequestBody(t *testing.T) {
|
||||
firstDescription := "description-1"
|
||||
secondDescription := "description-2"
|
||||
scenarios := []struct {
|
||||
Name string
|
||||
Provider AlertProvider
|
||||
Alert alert.Alert
|
||||
Resolved bool
|
||||
ExpectedBody string
|
||||
}{
|
||||
{
|
||||
Name: "triggered",
|
||||
Provider: AlertProvider{},
|
||||
Alert: alert.Alert{Description: &firstDescription, SuccessThreshold: 5, FailureThreshold: 3},
|
||||
Resolved: false,
|
||||
ExpectedBody: "{\n \"text\": \"\",\n \"attachments\": [\n {\n \"title\": \":helmet_with_white_cross: Gatus\",\n \"text\": \"An alert for *endpoint-name* has been triggered due to having failed 3 time(s) in a row:\\n> description-1\",\n \"short\": false,\n \"color\": \"#DD0000\",\n \"fields\": [\n {\n \"title\": \"Condition results\",\n \"value\": \":x: - `[CONNECTED] == true`\\n:x: - `[STATUS] == 200`\\n\",\n \"short\": false\n }\n ]\n }\n ]\n}",
|
||||
},
|
||||
{
|
||||
Name: "resolved",
|
||||
Provider: AlertProvider{},
|
||||
Alert: alert.Alert{Description: &secondDescription, SuccessThreshold: 5, FailureThreshold: 3},
|
||||
Resolved: true,
|
||||
ExpectedBody: "{\n \"text\": \"\",\n \"attachments\": [\n {\n \"title\": \":helmet_with_white_cross: Gatus\",\n \"text\": \"An alert for *endpoint-name* has been resolved after passing successfully 5 time(s) in a row:\\n> description-2\",\n \"short\": false,\n \"color\": \"#36A64F\",\n \"fields\": [\n {\n \"title\": \"Condition results\",\n \"value\": \":white_check_mark: - `[CONNECTED] == true`\\n:white_check_mark: - `[STATUS] == 200`\\n\",\n \"short\": false\n }\n ]\n }\n ]\n}",
|
||||
},
|
||||
}
|
||||
if !strings.Contains(customAlertProvider.Body, "resolved") {
|
||||
t.Error("customAlertProvider.Body should've contained the substring resolved")
|
||||
}
|
||||
if customAlertProvider.URL != "http://example.com" {
|
||||
t.Errorf("expected URL to be %s, got %s", "http://example.com", customAlertProvider.URL)
|
||||
}
|
||||
if customAlertProvider.Method != http.MethodPost {
|
||||
t.Errorf("expected method to be %s, got %s", http.MethodPost, customAlertProvider.Method)
|
||||
}
|
||||
body := make(map[string]interface{})
|
||||
err := json.Unmarshal([]byte(customAlertProvider.Body), &body)
|
||||
if err != nil {
|
||||
t.Error("expected body to be valid JSON, got error:", err.Error())
|
||||
}
|
||||
if expected := "An alert for *svc* has been resolved after passing successfully 0 time(s) in a row:\n> test"; expected != body["attachments"].([]interface{})[0].(map[string]interface{})["text"] {
|
||||
t.Errorf("expected $.attachments[0].description to be %s, got %s", expected, body["attachments"].([]interface{})[0].(map[string]interface{})["text"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestAlertProvider_ToCustomAlertProviderWithTriggeredAlert(t *testing.T) {
|
||||
provider := AlertProvider{WebhookURL: "http://example.com"}
|
||||
customAlertProvider := provider.ToCustomAlertProvider(&core.Endpoint{}, &alert.Alert{}, &core.Result{ConditionResults: []*core.ConditionResult{{Condition: "UNSUCCESSFUL_CONDITION", Success: false}}}, false)
|
||||
if customAlertProvider == nil {
|
||||
t.Fatal("customAlertProvider shouldn't have been nil")
|
||||
}
|
||||
if !strings.Contains(customAlertProvider.Body, "triggered") {
|
||||
t.Error("customAlertProvider.Body should've contained the substring triggered")
|
||||
}
|
||||
if customAlertProvider.URL != "http://example.com" {
|
||||
t.Errorf("expected URL to be %s, got %s", "http://example.com", customAlertProvider.URL)
|
||||
}
|
||||
if customAlertProvider.Method != http.MethodPost {
|
||||
t.Errorf("expected method to be %s, got %s", http.MethodPost, customAlertProvider.Method)
|
||||
}
|
||||
body := make(map[string]interface{})
|
||||
err := json.Unmarshal([]byte(customAlertProvider.Body), &body)
|
||||
if err != nil {
|
||||
t.Error("expected body to be valid JSON, got error:", err.Error())
|
||||
for _, scenario := range scenarios {
|
||||
t.Run(scenario.Name, func(t *testing.T) {
|
||||
body := scenario.Provider.buildRequestBody(
|
||||
&core.Endpoint{Name: "endpoint-name"},
|
||||
&scenario.Alert,
|
||||
&core.Result{
|
||||
ConditionResults: []*core.ConditionResult{
|
||||
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
||||
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
||||
},
|
||||
},
|
||||
scenario.Resolved,
|
||||
)
|
||||
if body != scenario.ExpectedBody {
|
||||
t.Errorf("expected %s, got %s", scenario.ExpectedBody, body)
|
||||
}
|
||||
out := make(map[string]interface{})
|
||||
if err := json.Unmarshal([]byte(body), &out); err != nil {
|
||||
t.Error("expected body to be valid JSON, got error:", err.Error())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,11 +1,15 @@
|
|||
package teams
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/TwiN/gatus/v3/alerting/alert"
|
||||
"github.com/TwiN/gatus/v3/alerting/provider/custom"
|
||||
"github.com/TwiN/gatus/v3/client"
|
||||
"github.com/TwiN/gatus/v3/core"
|
||||
)
|
||||
|
||||
|
@ -22,10 +26,37 @@ func (provider *AlertProvider) IsValid() bool {
|
|||
return len(provider.WebhookURL) > 0
|
||||
}
|
||||
|
||||
// ToCustomAlertProvider converts the provider into a custom.AlertProvider
|
||||
func (provider *AlertProvider) ToCustomAlertProvider(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) *custom.AlertProvider {
|
||||
var message string
|
||||
var color string
|
||||
// Send an alert using the provider
|
||||
func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) error {
|
||||
if os.Getenv("MOCK_ALERT_PROVIDER") == "true" {
|
||||
if os.Getenv("MOCK_ALERT_PROVIDER_ERROR") == "true" {
|
||||
return errors.New("error")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
buffer := bytes.NewBuffer([]byte(provider.buildRequestBody(endpoint, alert, result, resolved)))
|
||||
request, err := http.NewRequest(http.MethodPost, provider.WebhookURL, buffer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
request.Header.Set("Content-Type", "application/json")
|
||||
response, err := client.GetHTTPClient(nil).Do(request)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if response.StatusCode > 399 {
|
||||
body, err := ioutil.ReadAll(response.Body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("call to provider alert returned status code %d", response.StatusCode)
|
||||
}
|
||||
return fmt.Errorf("call to provider alert returned status code %d: %s", response.StatusCode, string(body))
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// buildRequestBody builds the request body for the provider
|
||||
func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) string {
|
||||
var message, color string
|
||||
if resolved {
|
||||
message = fmt.Sprintf("An alert for *%s* has been resolved after passing successfully %d time(s) in a row", endpoint.Name, alert.SuccessThreshold)
|
||||
color = "#36A64F"
|
||||
|
@ -47,10 +78,7 @@ func (provider *AlertProvider) ToCustomAlertProvider(endpoint *core.Endpoint, al
|
|||
if alertDescription := alert.GetDescription(); len(alertDescription) > 0 {
|
||||
description = ":\\n> " + alertDescription
|
||||
}
|
||||
return &custom.AlertProvider{
|
||||
URL: provider.WebhookURL,
|
||||
Method: http.MethodPost,
|
||||
Body: fmt.Sprintf(`{
|
||||
return fmt.Sprintf(`{
|
||||
"@type": "MessageCard",
|
||||
"@context": "http://schema.org/extensions",
|
||||
"themeColor": "%s",
|
||||
|
@ -66,9 +94,7 @@ func (provider *AlertProvider) ToCustomAlertProvider(endpoint *core.Endpoint, al
|
|||
"text": "%s"
|
||||
}
|
||||
]
|
||||
}`, color, message, description, endpoint.URL, results),
|
||||
Headers: map[string]string{"Content-Type": "application/json"},
|
||||
}
|
||||
}`, color, message, description, endpoint.URL, results)
|
||||
}
|
||||
|
||||
// GetDefaultAlert returns the provider's default alert configuration
|
||||
|
|
|
@ -2,8 +2,6 @@ package teams
|
|||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/TwiN/gatus/v3/alerting/alert"
|
||||
|
@ -21,50 +19,51 @@ func TestAlertProvider_IsValid(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestAlertProvider_ToCustomAlertProviderWithResolvedAlert(t *testing.T) {
|
||||
provider := AlertProvider{WebhookURL: "http://example.org"}
|
||||
alertDescription := "test"
|
||||
customAlertProvider := provider.ToCustomAlertProvider(&core.Endpoint{Name: "svc"}, &alert.Alert{Description: &alertDescription}, &core.Result{ConditionResults: []*core.ConditionResult{{Condition: "SUCCESSFUL_CONDITION", Success: true}}}, true)
|
||||
if customAlertProvider == nil {
|
||||
t.Fatal("customAlertProvider shouldn't have been nil")
|
||||
func TestAlertProvider_buildRequestBody(t *testing.T) {
|
||||
firstDescription := "description-1"
|
||||
secondDescription := "description-2"
|
||||
scenarios := []struct {
|
||||
Name string
|
||||
Provider AlertProvider
|
||||
Alert alert.Alert
|
||||
Resolved bool
|
||||
ExpectedBody string
|
||||
}{
|
||||
{
|
||||
Name: "triggered",
|
||||
Provider: AlertProvider{},
|
||||
Alert: alert.Alert{Description: &firstDescription, SuccessThreshold: 5, FailureThreshold: 3},
|
||||
Resolved: false,
|
||||
ExpectedBody: "{\n \"@type\": \"MessageCard\",\n \"@context\": \"http://schema.org/extensions\",\n \"themeColor\": \"#DD0000\",\n \"title\": \"🚨 Gatus\",\n \"text\": \"An alert for *endpoint-name* has been triggered due to having failed 3 time(s) in a row:\\n> description-1\",\n \"sections\": [\n {\n \"activityTitle\": \"URL\",\n \"text\": \"\"\n },\n {\n \"activityTitle\": \"Condition results\",\n \"text\": \"❌ - `[CONNECTED] == true`<br/>❌ - `[STATUS] == 200`<br/>\"\n }\n ]\n}",
|
||||
},
|
||||
{
|
||||
Name: "resolved",
|
||||
Provider: AlertProvider{},
|
||||
Alert: alert.Alert{Description: &secondDescription, SuccessThreshold: 5, FailureThreshold: 3},
|
||||
Resolved: true,
|
||||
ExpectedBody: "{\n \"@type\": \"MessageCard\",\n \"@context\": \"http://schema.org/extensions\",\n \"themeColor\": \"#36A64F\",\n \"title\": \"🚨 Gatus\",\n \"text\": \"An alert for *endpoint-name* has been resolved after passing successfully 5 time(s) in a row:\\n> description-2\",\n \"sections\": [\n {\n \"activityTitle\": \"URL\",\n \"text\": \"\"\n },\n {\n \"activityTitle\": \"Condition results\",\n \"text\": \"✅ - `[CONNECTED] == true`<br/>✅ - `[STATUS] == 200`<br/>\"\n }\n ]\n}",
|
||||
},
|
||||
}
|
||||
if !strings.Contains(customAlertProvider.Body, "resolved") {
|
||||
t.Error("customAlertProvider.Body should've contained the substring resolved")
|
||||
}
|
||||
if customAlertProvider.URL != "http://example.org" {
|
||||
t.Errorf("expected URL to be %s, got %s", "http://example.org", customAlertProvider.URL)
|
||||
}
|
||||
if customAlertProvider.Method != http.MethodPost {
|
||||
t.Errorf("expected method to be %s, got %s", http.MethodPost, customAlertProvider.Method)
|
||||
}
|
||||
body := make(map[string]interface{})
|
||||
err := json.Unmarshal([]byte(customAlertProvider.Body), &body)
|
||||
if err != nil {
|
||||
t.Error("expected body to be valid JSON, got error:", err.Error())
|
||||
}
|
||||
if expected := "An alert for *svc* has been resolved after passing successfully 0 time(s) in a row:\n> test"; expected != body["text"] {
|
||||
t.Errorf("expected $.text to be %s, got %s", expected, body["text"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestAlertProvider_ToCustomAlertProviderWithTriggeredAlert(t *testing.T) {
|
||||
provider := AlertProvider{WebhookURL: "http://example.org"}
|
||||
customAlertProvider := provider.ToCustomAlertProvider(&core.Endpoint{}, &alert.Alert{}, &core.Result{ConditionResults: []*core.ConditionResult{{Condition: "UNSUCCESSFUL_CONDITION", Success: false}}}, false)
|
||||
if customAlertProvider == nil {
|
||||
t.Fatal("customAlertProvider shouldn't have been nil")
|
||||
}
|
||||
if !strings.Contains(customAlertProvider.Body, "triggered") {
|
||||
t.Error("customAlertProvider.Body should've contained the substring triggered")
|
||||
}
|
||||
if customAlertProvider.URL != "http://example.org" {
|
||||
t.Errorf("expected URL to be %s, got %s", "http://example.org", customAlertProvider.URL)
|
||||
}
|
||||
if customAlertProvider.Method != http.MethodPost {
|
||||
t.Errorf("expected method to be %s, got %s", http.MethodPost, customAlertProvider.Method)
|
||||
}
|
||||
body := make(map[string]interface{})
|
||||
err := json.Unmarshal([]byte(customAlertProvider.Body), &body)
|
||||
if err != nil {
|
||||
t.Error("expected body to be valid JSON, got error:", err.Error())
|
||||
for _, scenario := range scenarios {
|
||||
t.Run(scenario.Name, func(t *testing.T) {
|
||||
body := scenario.Provider.buildRequestBody(
|
||||
&core.Endpoint{Name: "endpoint-name"},
|
||||
&scenario.Alert,
|
||||
&core.Result{
|
||||
ConditionResults: []*core.ConditionResult{
|
||||
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
||||
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
||||
},
|
||||
},
|
||||
scenario.Resolved,
|
||||
)
|
||||
if body != scenario.ExpectedBody {
|
||||
t.Errorf("expected %s, got %s", scenario.ExpectedBody, body)
|
||||
}
|
||||
out := make(map[string]interface{})
|
||||
if err := json.Unmarshal([]byte(body), &out); err != nil {
|
||||
t.Error("expected body to be valid JSON, got error:", err.Error())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,11 +1,15 @@
|
|||
package telegram
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/TwiN/gatus/v3/alerting/alert"
|
||||
"github.com/TwiN/gatus/v3/alerting/provider/custom"
|
||||
"github.com/TwiN/gatus/v3/client"
|
||||
"github.com/TwiN/gatus/v3/core"
|
||||
)
|
||||
|
||||
|
@ -23,8 +27,36 @@ func (provider *AlertProvider) IsValid() bool {
|
|||
return len(provider.Token) > 0 && len(provider.ID) > 0
|
||||
}
|
||||
|
||||
// ToCustomAlertProvider converts the provider into a custom.AlertProvider
|
||||
func (provider *AlertProvider) ToCustomAlertProvider(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) *custom.AlertProvider {
|
||||
// Send an alert using the provider
|
||||
func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) error {
|
||||
if os.Getenv("MOCK_ALERT_PROVIDER") == "true" {
|
||||
if os.Getenv("MOCK_ALERT_PROVIDER_ERROR") == "true" {
|
||||
return errors.New("error")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
buffer := bytes.NewBuffer([]byte(provider.buildRequestBody(endpoint, alert, result, resolved)))
|
||||
request, err := http.NewRequest(http.MethodPost, fmt.Sprintf("https://api.telegram.org/bot%s/sendMessage", provider.Token), buffer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
request.Header.Set("Content-Type", "application/json")
|
||||
response, err := client.GetHTTPClient(nil).Do(request)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if response.StatusCode > 399 {
|
||||
body, err := ioutil.ReadAll(response.Body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("call to provider alert returned status code %d", response.StatusCode)
|
||||
}
|
||||
return fmt.Errorf("call to provider alert returned status code %d: %s", response.StatusCode, string(body))
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// buildRequestBody builds the request body for the provider
|
||||
func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) string {
|
||||
var message, results string
|
||||
if resolved {
|
||||
message = fmt.Sprintf("An alert for *%s* has been resolved:\\n—\\n _healthcheck passing successfully %d time(s) in a row_\\n— ", endpoint.Name, alert.FailureThreshold)
|
||||
|
@ -46,12 +78,7 @@ func (provider *AlertProvider) ToCustomAlertProvider(endpoint *core.Endpoint, al
|
|||
} else {
|
||||
text = fmt.Sprintf("⛑ *Gatus* \\n%s \\n*Condition results*\\n%s", message, results)
|
||||
}
|
||||
return &custom.AlertProvider{
|
||||
URL: fmt.Sprintf("https://api.telegram.org/bot%s/sendMessage", provider.Token),
|
||||
Method: http.MethodPost,
|
||||
Body: fmt.Sprintf(`{"chat_id": "%s", "text": "%s", "parse_mode": "MARKDOWN"}`, provider.ID, text),
|
||||
Headers: map[string]string{"Content-Type": "application/json"},
|
||||
}
|
||||
return fmt.Sprintf(`{"chat_id": "%s", "text": "%s", "parse_mode": "MARKDOWN"}`, provider.ID, text)
|
||||
}
|
||||
|
||||
// GetDefaultAlert returns the provider's default alert configuration
|
||||
|
|
|
@ -2,9 +2,6 @@ package telegram
|
|||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/TwiN/gatus/v3/alerting/alert"
|
||||
|
@ -22,70 +19,51 @@ func TestAlertProvider_IsValid(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestAlertProvider_ToCustomAlertProviderWithResolvedAlert(t *testing.T) {
|
||||
provider := AlertProvider{Token: "123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11", ID: "12345678"}
|
||||
customAlertProvider := provider.ToCustomAlertProvider(&core.Endpoint{}, &alert.Alert{}, &core.Result{ConditionResults: []*core.ConditionResult{{Condition: "SUCCESSFUL_CONDITION", Success: true}}}, true)
|
||||
if customAlertProvider == nil {
|
||||
t.Fatal("customAlertProvider shouldn't have been nil")
|
||||
func TestAlertProvider_buildRequestBody(t *testing.T) {
|
||||
firstDescription := "description-1"
|
||||
secondDescription := "description-2"
|
||||
scenarios := []struct {
|
||||
Name string
|
||||
Provider AlertProvider
|
||||
Alert alert.Alert
|
||||
Resolved bool
|
||||
ExpectedBody string
|
||||
}{
|
||||
{
|
||||
Name: "triggered",
|
||||
Provider: AlertProvider{ID: "123"},
|
||||
Alert: alert.Alert{Description: &firstDescription, SuccessThreshold: 5, FailureThreshold: 3},
|
||||
Resolved: false,
|
||||
ExpectedBody: "{\"chat_id\": \"123\", \"text\": \"⛑ *Gatus* \\nAn alert for *endpoint-name* has been triggered:\\n—\\n _healthcheck failed 3 time(s) in a row_\\n— \\n*Description* \\n_description-1_ \\n\\n*Condition results*\\n❌ - `[CONNECTED] == true`\\n❌ - `[STATUS] == 200`\\n\", \"parse_mode\": \"MARKDOWN\"}",
|
||||
},
|
||||
{
|
||||
Name: "resolved",
|
||||
Provider: AlertProvider{ID: "123"},
|
||||
Alert: alert.Alert{Description: &secondDescription, SuccessThreshold: 5, FailureThreshold: 3},
|
||||
Resolved: true,
|
||||
ExpectedBody: "{\"chat_id\": \"123\", \"text\": \"⛑ *Gatus* \\nAn alert for *endpoint-name* has been resolved:\\n—\\n _healthcheck passing successfully 3 time(s) in a row_\\n— \\n*Description* \\n_description-2_ \\n\\n*Condition results*\\n✅ - `[CONNECTED] == true`\\n✅ - `[STATUS] == 200`\\n\", \"parse_mode\": \"MARKDOWN\"}",
|
||||
},
|
||||
}
|
||||
if !strings.Contains(customAlertProvider.Body, "resolved") {
|
||||
t.Error("customAlertProvider.Body should've contained the substring resolved")
|
||||
}
|
||||
if customAlertProvider.URL != fmt.Sprintf("https://api.telegram.org/bot%s/sendMessage", provider.Token) {
|
||||
t.Errorf("expected URL to be %s, got %s", fmt.Sprintf("https://api.telegram.org/bot%s/sendMessage", provider.Token), customAlertProvider.URL)
|
||||
}
|
||||
if customAlertProvider.Method != http.MethodPost {
|
||||
t.Errorf("expected method to be %s, got %s", http.MethodPost, customAlertProvider.Method)
|
||||
}
|
||||
body := make(map[string]interface{})
|
||||
err := json.Unmarshal([]byte(customAlertProvider.Body), &body)
|
||||
//_, err := json.Marshal(customAlertProvider.Body)
|
||||
if err != nil {
|
||||
t.Error("expected body to be valid JSON, got error:", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func TestAlertProvider_ToCustomAlertProviderWithTriggeredAlert(t *testing.T) {
|
||||
provider := AlertProvider{Token: "123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11", ID: "0123456789"}
|
||||
description := "Healthcheck Successful"
|
||||
customAlertProvider := provider.ToCustomAlertProvider(&core.Endpoint{}, &alert.Alert{Description: &description}, &core.Result{ConditionResults: []*core.ConditionResult{{Condition: "UNSUCCESSFUL_CONDITION", Success: false}}}, false)
|
||||
if customAlertProvider == nil {
|
||||
t.Fatal("customAlertProvider shouldn't have been nil")
|
||||
}
|
||||
if !strings.Contains(customAlertProvider.Body, "triggered") {
|
||||
t.Error("customAlertProvider.Body should've contained the substring triggered")
|
||||
}
|
||||
if customAlertProvider.URL != fmt.Sprintf("https://api.telegram.org/bot%s/sendMessage", provider.Token) {
|
||||
t.Errorf("expected URL to be %s, got %s", fmt.Sprintf("https://api.telegram.org/bot%s/sendMessage", provider.Token), customAlertProvider.URL)
|
||||
}
|
||||
if customAlertProvider.Method != http.MethodPost {
|
||||
t.Errorf("expected method to be %s, got %s", http.MethodPost, customAlertProvider.Method)
|
||||
}
|
||||
body := make(map[string]interface{})
|
||||
err := json.Unmarshal([]byte(customAlertProvider.Body), &body)
|
||||
if err != nil {
|
||||
t.Error("expected body to be valid JSON, got error:", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func TestAlertProvider_ToCustomAlertProviderWithDescription(t *testing.T) {
|
||||
provider := AlertProvider{Token: "123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11", ID: "0123456789"}
|
||||
customAlertProvider := provider.ToCustomAlertProvider(&core.Endpoint{}, &alert.Alert{}, &core.Result{ConditionResults: []*core.ConditionResult{{Condition: "UNSUCCESSFUL_CONDITION", Success: false}}}, false)
|
||||
if customAlertProvider == nil {
|
||||
t.Fatal("customAlertProvider shouldn't have been nil")
|
||||
}
|
||||
if !strings.Contains(customAlertProvider.Body, "triggered") {
|
||||
t.Error("customAlertProvider.Body should've contained the substring triggered")
|
||||
}
|
||||
if customAlertProvider.URL != fmt.Sprintf("https://api.telegram.org/bot%s/sendMessage", provider.Token) {
|
||||
t.Errorf("expected URL to be %s, got %s", fmt.Sprintf("https://api.telegram.org/bot%s/sendMessage", provider.Token), customAlertProvider.URL)
|
||||
}
|
||||
if customAlertProvider.Method != http.MethodPost {
|
||||
t.Errorf("expected method to be %s, got %s", http.MethodPost, customAlertProvider.Method)
|
||||
}
|
||||
body := make(map[string]interface{})
|
||||
err := json.Unmarshal([]byte(customAlertProvider.Body), &body)
|
||||
if err != nil {
|
||||
t.Error("expected body to be valid JSON, got error:", err.Error())
|
||||
for _, scenario := range scenarios {
|
||||
t.Run(scenario.Name, func(t *testing.T) {
|
||||
body := scenario.Provider.buildRequestBody(
|
||||
&core.Endpoint{Name: "endpoint-name"},
|
||||
&scenario.Alert,
|
||||
&core.Result{
|
||||
ConditionResults: []*core.ConditionResult{
|
||||
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
||||
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
||||
},
|
||||
},
|
||||
scenario.Resolved,
|
||||
)
|
||||
if body != scenario.ExpectedBody {
|
||||
t.Errorf("expected %s, got %s", scenario.ExpectedBody, body)
|
||||
}
|
||||
out := make(map[string]interface{})
|
||||
if err := json.Unmarshal([]byte(body), &out); err != nil {
|
||||
t.Error("expected body to be valid JSON, got error:", err.Error())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,13 +1,17 @@
|
|||
package twilio
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
|
||||
"github.com/TwiN/gatus/v3/alerting/alert"
|
||||
"github.com/TwiN/gatus/v3/alerting/provider/custom"
|
||||
"github.com/TwiN/gatus/v3/client"
|
||||
"github.com/TwiN/gatus/v3/core"
|
||||
)
|
||||
|
||||
|
@ -27,27 +31,48 @@ func (provider *AlertProvider) IsValid() bool {
|
|||
return len(provider.Token) > 0 && len(provider.SID) > 0 && len(provider.From) > 0 && len(provider.To) > 0
|
||||
}
|
||||
|
||||
// ToCustomAlertProvider converts the provider into a custom.AlertProvider
|
||||
func (provider *AlertProvider) ToCustomAlertProvider(endpoint *core.Endpoint, alert *alert.Alert, _ *core.Result, resolved bool) *custom.AlertProvider {
|
||||
// Send an alert using the provider
|
||||
func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) error {
|
||||
if os.Getenv("MOCK_ALERT_PROVIDER") == "true" {
|
||||
if os.Getenv("MOCK_ALERT_PROVIDER_ERROR") == "true" {
|
||||
return errors.New("error")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
buffer := bytes.NewBuffer([]byte(provider.buildRequestBody(endpoint, alert, result, resolved)))
|
||||
request, err := http.NewRequest(http.MethodPost, fmt.Sprintf("https://api.twilio.com/2010-04-01/Accounts/%s/Messages.json", provider.SID), buffer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
request.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
request.Header.Set("Authorization", fmt.Sprintf("Basic %s", base64.StdEncoding.EncodeToString([]byte(provider.SID+":"+provider.Token))))
|
||||
response, err := client.GetHTTPClient(nil).Do(request)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if response.StatusCode > 399 {
|
||||
body, err := ioutil.ReadAll(response.Body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("call to provider alert returned status code %d", response.StatusCode)
|
||||
}
|
||||
return fmt.Errorf("call to provider alert returned status code %d: %s", response.StatusCode, string(body))
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// buildRequestBody builds the request body for the provider
|
||||
func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) string {
|
||||
var message string
|
||||
if resolved {
|
||||
message = fmt.Sprintf("RESOLVED: %s - %s", endpoint.Name, alert.GetDescription())
|
||||
} else {
|
||||
message = fmt.Sprintf("TRIGGERED: %s - %s", endpoint.Name, alert.GetDescription())
|
||||
}
|
||||
return &custom.AlertProvider{
|
||||
URL: fmt.Sprintf("https://api.twilio.com/2010-04-01/Accounts/%s/Messages.json", provider.SID),
|
||||
Method: http.MethodPost,
|
||||
Body: url.Values{
|
||||
"To": {provider.To},
|
||||
"From": {provider.From},
|
||||
"Body": {message},
|
||||
}.Encode(),
|
||||
Headers: map[string]string{
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
"Authorization": fmt.Sprintf("Basic %s", base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%s:%s", provider.SID, provider.Token)))),
|
||||
},
|
||||
}
|
||||
return url.Values{
|
||||
"To": {provider.To},
|
||||
"From": {provider.From},
|
||||
"Body": {message},
|
||||
}.Encode()
|
||||
}
|
||||
|
||||
// GetDefaultAlert returns the provider's default alert configuration
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
package twilio
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/TwiN/gatus/v3/alerting/alert"
|
||||
|
@ -25,54 +23,47 @@ func TestTwilioAlertProvider_IsValid(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestAlertProvider_ToCustomAlertProviderWithResolvedAlert(t *testing.T) {
|
||||
provider := AlertProvider{
|
||||
SID: "1",
|
||||
Token: "2",
|
||||
From: "3",
|
||||
To: "4",
|
||||
func TestAlertProvider_buildRequestBody(t *testing.T) {
|
||||
firstDescription := "description-1"
|
||||
secondDescription := "description-2"
|
||||
scenarios := []struct {
|
||||
Name string
|
||||
Provider AlertProvider
|
||||
Alert alert.Alert
|
||||
Resolved bool
|
||||
ExpectedBody string
|
||||
}{
|
||||
{
|
||||
Name: "triggered",
|
||||
Provider: AlertProvider{SID: "1", Token: "2", From: "3", To: "4"},
|
||||
Alert: alert.Alert{Description: &firstDescription, SuccessThreshold: 5, FailureThreshold: 3},
|
||||
Resolved: false,
|
||||
ExpectedBody: "Body=TRIGGERED%3A+endpoint-name+-+description-1&From=3&To=4",
|
||||
},
|
||||
{
|
||||
Name: "resolved",
|
||||
Provider: AlertProvider{SID: "1", Token: "2", From: "3", To: "4"},
|
||||
Alert: alert.Alert{Description: &secondDescription, SuccessThreshold: 5, FailureThreshold: 3},
|
||||
Resolved: true,
|
||||
ExpectedBody: "Body=RESOLVED%3A+endpoint-name+-+description-2&From=3&To=4",
|
||||
},
|
||||
}
|
||||
description := "alert-description"
|
||||
customAlertProvider := provider.ToCustomAlertProvider(&core.Endpoint{Name: "endpoint-name"}, &alert.Alert{Description: &description}, &core.Result{}, true)
|
||||
if customAlertProvider == nil {
|
||||
t.Fatal("customAlertProvider shouldn't have been nil")
|
||||
}
|
||||
if !strings.Contains(customAlertProvider.Body, "RESOLVED") {
|
||||
t.Error("customAlertProvider.Body should've contained the substring RESOLVED")
|
||||
}
|
||||
if customAlertProvider.URL != "https://api.twilio.com/2010-04-01/Accounts/1/Messages.json" {
|
||||
t.Errorf("expected URL to be %s, got %s", "https://api.twilio.com/2010-04-01/Accounts/1/Messages.json", customAlertProvider.URL)
|
||||
}
|
||||
if customAlertProvider.Method != http.MethodPost {
|
||||
t.Errorf("expected method to be %s, got %s", http.MethodPost, customAlertProvider.Method)
|
||||
}
|
||||
if customAlertProvider.Body != "Body=RESOLVED%3A+endpoint-name+-+alert-description&From=3&To=4" {
|
||||
t.Errorf("expected body to be %s, got %s", "Body=RESOLVED%3A+endpoint-name+-+alert-description&From=3&To=4", customAlertProvider.Body)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAlertProvider_ToCustomAlertProviderWithTriggeredAlert(t *testing.T) {
|
||||
provider := AlertProvider{
|
||||
SID: "4",
|
||||
Token: "3",
|
||||
From: "2",
|
||||
To: "1",
|
||||
}
|
||||
description := "alert-description"
|
||||
customAlertProvider := provider.ToCustomAlertProvider(&core.Endpoint{Name: "endpoint-name"}, &alert.Alert{Description: &description}, &core.Result{}, false)
|
||||
if customAlertProvider == nil {
|
||||
t.Fatal("customAlertProvider shouldn't have been nil")
|
||||
}
|
||||
if !strings.Contains(customAlertProvider.Body, "TRIGGERED") {
|
||||
t.Error("customAlertProvider.Body should've contained the substring TRIGGERED")
|
||||
}
|
||||
if customAlertProvider.URL != "https://api.twilio.com/2010-04-01/Accounts/4/Messages.json" {
|
||||
t.Errorf("expected URL to be %s, got %s", "https://api.twilio.com/2010-04-01/Accounts/4/Messages.json", customAlertProvider.URL)
|
||||
}
|
||||
if customAlertProvider.Method != http.MethodPost {
|
||||
t.Errorf("expected method to be %s, got %s", http.MethodPost, customAlertProvider.Method)
|
||||
}
|
||||
if customAlertProvider.Body != "Body=TRIGGERED%3A+endpoint-name+-+alert-description&From=2&To=1" {
|
||||
t.Errorf("expected body to be %s, got %s", "Body=TRIGGERED%3A+endpoint-name+-+alert-description&From=2&To=1", customAlertProvider.Body)
|
||||
for _, scenario := range scenarios {
|
||||
t.Run(scenario.Name, func(t *testing.T) {
|
||||
body := scenario.Provider.buildRequestBody(
|
||||
&core.Endpoint{Name: "endpoint-name"},
|
||||
&scenario.Alert,
|
||||
&core.Result{
|
||||
ConditionResults: []*core.ConditionResult{
|
||||
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
||||
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
||||
},
|
||||
},
|
||||
scenario.Resolved,
|
||||
)
|
||||
if body != scenario.ExpectedBody {
|
||||
t.Errorf("expected %s, got %s", scenario.ExpectedBody, body)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -274,6 +274,7 @@ func validateAlertingConfig(alertingConfig *alerting.Config, endpoints []*core.E
|
|||
alertTypes := []alert.Type{
|
||||
alert.TypeCustom,
|
||||
alert.TypeDiscord,
|
||||
alert.TypeEmail,
|
||||
alert.TypeMattermost,
|
||||
alert.TypeMessagebird,
|
||||
alert.TypePagerDuty,
|
||||
|
|
|
@ -39,7 +39,6 @@ type Config struct {
|
|||
Every []string `yaml:"every"`
|
||||
|
||||
durationToStartFromMidnight time.Duration
|
||||
timeLocation *time.Location
|
||||
}
|
||||
|
||||
func GetDefaultConfig() *Config {
|
||||
|
|
2
go.mod
2
go.mod
|
@ -32,6 +32,8 @@ require (
|
|||
golang.org/x/tools v0.1.7 // indirect
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
|
||||
google.golang.org/protobuf v1.27.1 // indirect
|
||||
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
|
||||
gopkg.in/mail.v2 v2.3.1
|
||||
gopkg.in/yaml.v2 v2.4.0
|
||||
lukechampine.com/uint128 v1.1.1 // indirect
|
||||
modernc.org/cc/v3 v3.35.8 // indirect
|
||||
|
|
4
go.sum
4
go.sum
|
@ -523,12 +523,16 @@ google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQ
|
|||
google.golang.org/protobuf v1.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ=
|
||||
google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
||||
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk=
|
||||
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk=
|
||||
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=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||
gopkg.in/mail.v2 v2.3.1 h1:WYFn/oANrAGP2C0dcV6/pbkPzv8yGzqTjPmTeO7qoXk=
|
||||
gopkg.in/mail.v2 v2.3.1/go.mod h1:htwXN1Qh09vZJ1NVKxQqHPBaCBbzKhp5GzuJEA4VJWw=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
|
|
20
vendor/gopkg.in/alexcesaro/quotedprintable.v3/LICENSE
generated
vendored
Normal file
20
vendor/gopkg.in/alexcesaro/quotedprintable.v3/LICENSE
generated
vendored
Normal file
|
@ -0,0 +1,20 @@
|
|||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2014 Alexandre Cesaro
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in
|
||||
the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
16
vendor/gopkg.in/alexcesaro/quotedprintable.v3/README.md
generated
vendored
Normal file
16
vendor/gopkg.in/alexcesaro/quotedprintable.v3/README.md
generated
vendored
Normal file
|
@ -0,0 +1,16 @@
|
|||
# quotedprintable
|
||||
|
||||
## Introduction
|
||||
|
||||
Package quotedprintable implements quoted-printable and message header encoding
|
||||
as specified by RFC 2045 and RFC 2047.
|
||||
|
||||
It is a copy of the Go 1.5 package `mime/quotedprintable`. It also includes
|
||||
the new functions of package `mime` concerning RFC 2047.
|
||||
|
||||
This code has minor changes with the standard library code in order to work
|
||||
with Go 1.0 and newer.
|
||||
|
||||
## Documentation
|
||||
|
||||
https://godoc.org/gopkg.in/alexcesaro/quotedprintable.v3
|
279
vendor/gopkg.in/alexcesaro/quotedprintable.v3/encodedword.go
generated
vendored
Normal file
279
vendor/gopkg.in/alexcesaro/quotedprintable.v3/encodedword.go
generated
vendored
Normal file
|
@ -0,0 +1,279 @@
|
|||
package quotedprintable
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
// A WordEncoder is a RFC 2047 encoded-word encoder.
|
||||
type WordEncoder byte
|
||||
|
||||
const (
|
||||
// BEncoding represents Base64 encoding scheme as defined by RFC 2045.
|
||||
BEncoding = WordEncoder('b')
|
||||
// QEncoding represents the Q-encoding scheme as defined by RFC 2047.
|
||||
QEncoding = WordEncoder('q')
|
||||
)
|
||||
|
||||
var (
|
||||
errInvalidWord = errors.New("mime: invalid RFC 2047 encoded-word")
|
||||
)
|
||||
|
||||
// Encode returns the encoded-word form of s. If s is ASCII without special
|
||||
// characters, it is returned unchanged. The provided charset is the IANA
|
||||
// charset name of s. It is case insensitive.
|
||||
func (e WordEncoder) Encode(charset, s string) string {
|
||||
if !needsEncoding(s) {
|
||||
return s
|
||||
}
|
||||
return e.encodeWord(charset, s)
|
||||
}
|
||||
|
||||
func needsEncoding(s string) bool {
|
||||
for _, b := range s {
|
||||
if (b < ' ' || b > '~') && b != '\t' {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// encodeWord encodes a string into an encoded-word.
|
||||
func (e WordEncoder) encodeWord(charset, s string) string {
|
||||
buf := getBuffer()
|
||||
defer putBuffer(buf)
|
||||
|
||||
buf.WriteString("=?")
|
||||
buf.WriteString(charset)
|
||||
buf.WriteByte('?')
|
||||
buf.WriteByte(byte(e))
|
||||
buf.WriteByte('?')
|
||||
|
||||
if e == BEncoding {
|
||||
w := base64.NewEncoder(base64.StdEncoding, buf)
|
||||
io.WriteString(w, s)
|
||||
w.Close()
|
||||
} else {
|
||||
enc := make([]byte, 3)
|
||||
for i := 0; i < len(s); i++ {
|
||||
b := s[i]
|
||||
switch {
|
||||
case b == ' ':
|
||||
buf.WriteByte('_')
|
||||
case b <= '~' && b >= '!' && b != '=' && b != '?' && b != '_':
|
||||
buf.WriteByte(b)
|
||||
default:
|
||||
enc[0] = '='
|
||||
enc[1] = upperhex[b>>4]
|
||||
enc[2] = upperhex[b&0x0f]
|
||||
buf.Write(enc)
|
||||
}
|
||||
}
|
||||
}
|
||||
buf.WriteString("?=")
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
const upperhex = "0123456789ABCDEF"
|
||||
|
||||
// A WordDecoder decodes MIME headers containing RFC 2047 encoded-words.
|
||||
type WordDecoder struct {
|
||||
// CharsetReader, if non-nil, defines a function to generate
|
||||
// charset-conversion readers, converting from the provided
|
||||
// charset into UTF-8.
|
||||
// Charsets are always lower-case. utf-8, iso-8859-1 and us-ascii charsets
|
||||
// are handled by default.
|
||||
// One of the the CharsetReader's result values must be non-nil.
|
||||
CharsetReader func(charset string, input io.Reader) (io.Reader, error)
|
||||
}
|
||||
|
||||
// Decode decodes an encoded-word. If word is not a valid RFC 2047 encoded-word,
|
||||
// word is returned unchanged.
|
||||
func (d *WordDecoder) Decode(word string) (string, error) {
|
||||
fields := strings.Split(word, "?") // TODO: remove allocation?
|
||||
if len(fields) != 5 || fields[0] != "=" || fields[4] != "=" || len(fields[2]) != 1 {
|
||||
return "", errInvalidWord
|
||||
}
|
||||
|
||||
content, err := decode(fields[2][0], fields[3])
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
buf := getBuffer()
|
||||
defer putBuffer(buf)
|
||||
|
||||
if err := d.convert(buf, fields[1], content); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return buf.String(), nil
|
||||
}
|
||||
|
||||
// DecodeHeader decodes all encoded-words of the given string. It returns an
|
||||
// error if and only if CharsetReader of d returns an error.
|
||||
func (d *WordDecoder) DecodeHeader(header string) (string, error) {
|
||||
// If there is no encoded-word, returns before creating a buffer.
|
||||
i := strings.Index(header, "=?")
|
||||
if i == -1 {
|
||||
return header, nil
|
||||
}
|
||||
|
||||
buf := getBuffer()
|
||||
defer putBuffer(buf)
|
||||
|
||||
buf.WriteString(header[:i])
|
||||
header = header[i:]
|
||||
|
||||
betweenWords := false
|
||||
for {
|
||||
start := strings.Index(header, "=?")
|
||||
if start == -1 {
|
||||
break
|
||||
}
|
||||
cur := start + len("=?")
|
||||
|
||||
i := strings.Index(header[cur:], "?")
|
||||
if i == -1 {
|
||||
break
|
||||
}
|
||||
charset := header[cur : cur+i]
|
||||
cur += i + len("?")
|
||||
|
||||
if len(header) < cur+len("Q??=") {
|
||||
break
|
||||
}
|
||||
encoding := header[cur]
|
||||
cur++
|
||||
|
||||
if header[cur] != '?' {
|
||||
break
|
||||
}
|
||||
cur++
|
||||
|
||||
j := strings.Index(header[cur:], "?=")
|
||||
if j == -1 {
|
||||
break
|
||||
}
|
||||
text := header[cur : cur+j]
|
||||
end := cur + j + len("?=")
|
||||
|
||||
content, err := decode(encoding, text)
|
||||
if err != nil {
|
||||
betweenWords = false
|
||||
buf.WriteString(header[:start+2])
|
||||
header = header[start+2:]
|
||||
continue
|
||||
}
|
||||
|
||||
// Write characters before the encoded-word. White-space and newline
|
||||
// characters separating two encoded-words must be deleted.
|
||||
if start > 0 && (!betweenWords || hasNonWhitespace(header[:start])) {
|
||||
buf.WriteString(header[:start])
|
||||
}
|
||||
|
||||
if err := d.convert(buf, charset, content); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
header = header[end:]
|
||||
betweenWords = true
|
||||
}
|
||||
|
||||
if len(header) > 0 {
|
||||
buf.WriteString(header)
|
||||
}
|
||||
|
||||
return buf.String(), nil
|
||||
}
|
||||
|
||||
func decode(encoding byte, text string) ([]byte, error) {
|
||||
switch encoding {
|
||||
case 'B', 'b':
|
||||
return base64.StdEncoding.DecodeString(text)
|
||||
case 'Q', 'q':
|
||||
return qDecode(text)
|
||||
}
|
||||
return nil, errInvalidWord
|
||||
}
|
||||
|
||||
func (d *WordDecoder) convert(buf *bytes.Buffer, charset string, content []byte) error {
|
||||
switch {
|
||||
case strings.EqualFold("utf-8", charset):
|
||||
buf.Write(content)
|
||||
case strings.EqualFold("iso-8859-1", charset):
|
||||
for _, c := range content {
|
||||
buf.WriteRune(rune(c))
|
||||
}
|
||||
case strings.EqualFold("us-ascii", charset):
|
||||
for _, c := range content {
|
||||
if c >= utf8.RuneSelf {
|
||||
buf.WriteRune(unicode.ReplacementChar)
|
||||
} else {
|
||||
buf.WriteByte(c)
|
||||
}
|
||||
}
|
||||
default:
|
||||
if d.CharsetReader == nil {
|
||||
return fmt.Errorf("mime: unhandled charset %q", charset)
|
||||
}
|
||||
r, err := d.CharsetReader(strings.ToLower(charset), bytes.NewReader(content))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err = buf.ReadFrom(r); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// hasNonWhitespace reports whether s (assumed to be ASCII) contains at least
|
||||
// one byte of non-whitespace.
|
||||
func hasNonWhitespace(s string) bool {
|
||||
for _, b := range s {
|
||||
switch b {
|
||||
// Encoded-words can only be separated by linear white spaces which does
|
||||
// not include vertical tabs (\v).
|
||||
case ' ', '\t', '\n', '\r':
|
||||
default:
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// qDecode decodes a Q encoded string.
|
||||
func qDecode(s string) ([]byte, error) {
|
||||
dec := make([]byte, len(s))
|
||||
n := 0
|
||||
for i := 0; i < len(s); i++ {
|
||||
switch c := s[i]; {
|
||||
case c == '_':
|
||||
dec[n] = ' '
|
||||
case c == '=':
|
||||
if i+2 >= len(s) {
|
||||
return nil, errInvalidWord
|
||||
}
|
||||
b, err := readHexByte(s[i+1], s[i+2])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dec[n] = b
|
||||
i += 2
|
||||
case (c <= '~' && c >= ' ') || c == '\n' || c == '\r' || c == '\t':
|
||||
dec[n] = c
|
||||
default:
|
||||
return nil, errInvalidWord
|
||||
}
|
||||
n++
|
||||
}
|
||||
|
||||
return dec[:n], nil
|
||||
}
|
26
vendor/gopkg.in/alexcesaro/quotedprintable.v3/pool.go
generated
vendored
Normal file
26
vendor/gopkg.in/alexcesaro/quotedprintable.v3/pool.go
generated
vendored
Normal file
|
@ -0,0 +1,26 @@
|
|||
// +build go1.3
|
||||
|
||||
package quotedprintable
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"sync"
|
||||
)
|
||||
|
||||
var bufPool = sync.Pool{
|
||||
New: func() interface{} {
|
||||
return new(bytes.Buffer)
|
||||
},
|
||||
}
|
||||
|
||||
func getBuffer() *bytes.Buffer {
|
||||
return bufPool.Get().(*bytes.Buffer)
|
||||
}
|
||||
|
||||
func putBuffer(buf *bytes.Buffer) {
|
||||
if buf.Len() > 1024 {
|
||||
return
|
||||
}
|
||||
buf.Reset()
|
||||
bufPool.Put(buf)
|
||||
}
|
24
vendor/gopkg.in/alexcesaro/quotedprintable.v3/pool_go12.go
generated
vendored
Normal file
24
vendor/gopkg.in/alexcesaro/quotedprintable.v3/pool_go12.go
generated
vendored
Normal file
|
@ -0,0 +1,24 @@
|
|||
// +build !go1.3
|
||||
|
||||
package quotedprintable
|
||||
|
||||
import "bytes"
|
||||
|
||||
var ch = make(chan *bytes.Buffer, 32)
|
||||
|
||||
func getBuffer() *bytes.Buffer {
|
||||
select {
|
||||
case buf := <-ch:
|
||||
return buf
|
||||
default:
|
||||
}
|
||||
return new(bytes.Buffer)
|
||||
}
|
||||
|
||||
func putBuffer(buf *bytes.Buffer) {
|
||||
buf.Reset()
|
||||
select {
|
||||
case ch <- buf:
|
||||
default:
|
||||
}
|
||||
}
|
121
vendor/gopkg.in/alexcesaro/quotedprintable.v3/reader.go
generated
vendored
Normal file
121
vendor/gopkg.in/alexcesaro/quotedprintable.v3/reader.go
generated
vendored
Normal file
|
@ -0,0 +1,121 @@
|
|||
// Package quotedprintable implements quoted-printable encoding as specified by
|
||||
// RFC 2045.
|
||||
package quotedprintable
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
)
|
||||
|
||||
// Reader is a quoted-printable decoder.
|
||||
type Reader struct {
|
||||
br *bufio.Reader
|
||||
rerr error // last read error
|
||||
line []byte // to be consumed before more of br
|
||||
}
|
||||
|
||||
// NewReader returns a quoted-printable reader, decoding from r.
|
||||
func NewReader(r io.Reader) *Reader {
|
||||
return &Reader{
|
||||
br: bufio.NewReader(r),
|
||||
}
|
||||
}
|
||||
|
||||
func fromHex(b byte) (byte, error) {
|
||||
switch {
|
||||
case b >= '0' && b <= '9':
|
||||
return b - '0', nil
|
||||
case b >= 'A' && b <= 'F':
|
||||
return b - 'A' + 10, nil
|
||||
// Accept badly encoded bytes.
|
||||
case b >= 'a' && b <= 'f':
|
||||
return b - 'a' + 10, nil
|
||||
}
|
||||
return 0, fmt.Errorf("quotedprintable: invalid hex byte 0x%02x", b)
|
||||
}
|
||||
|
||||
func readHexByte(a, b byte) (byte, error) {
|
||||
var hb, lb byte
|
||||
var err error
|
||||
if hb, err = fromHex(a); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if lb, err = fromHex(b); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return hb<<4 | lb, nil
|
||||
}
|
||||
|
||||
func isQPDiscardWhitespace(r rune) bool {
|
||||
switch r {
|
||||
case '\n', '\r', ' ', '\t':
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
var (
|
||||
crlf = []byte("\r\n")
|
||||
lf = []byte("\n")
|
||||
softSuffix = []byte("=")
|
||||
)
|
||||
|
||||
// Read reads and decodes quoted-printable data from the underlying reader.
|
||||
func (r *Reader) Read(p []byte) (n int, err error) {
|
||||
// Deviations from RFC 2045:
|
||||
// 1. in addition to "=\r\n", "=\n" is also treated as soft line break.
|
||||
// 2. it will pass through a '\r' or '\n' not preceded by '=', consistent
|
||||
// with other broken QP encoders & decoders.
|
||||
for len(p) > 0 {
|
||||
if len(r.line) == 0 {
|
||||
if r.rerr != nil {
|
||||
return n, r.rerr
|
||||
}
|
||||
r.line, r.rerr = r.br.ReadSlice('\n')
|
||||
|
||||
// Does the line end in CRLF instead of just LF?
|
||||
hasLF := bytes.HasSuffix(r.line, lf)
|
||||
hasCR := bytes.HasSuffix(r.line, crlf)
|
||||
wholeLine := r.line
|
||||
r.line = bytes.TrimRightFunc(wholeLine, isQPDiscardWhitespace)
|
||||
if bytes.HasSuffix(r.line, softSuffix) {
|
||||
rightStripped := wholeLine[len(r.line):]
|
||||
r.line = r.line[:len(r.line)-1]
|
||||
if !bytes.HasPrefix(rightStripped, lf) && !bytes.HasPrefix(rightStripped, crlf) {
|
||||
r.rerr = fmt.Errorf("quotedprintable: invalid bytes after =: %q", rightStripped)
|
||||
}
|
||||
} else if hasLF {
|
||||
if hasCR {
|
||||
r.line = append(r.line, '\r', '\n')
|
||||
} else {
|
||||
r.line = append(r.line, '\n')
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
b := r.line[0]
|
||||
|
||||
switch {
|
||||
case b == '=':
|
||||
if len(r.line[1:]) < 2 {
|
||||
return n, io.ErrUnexpectedEOF
|
||||
}
|
||||
b, err = readHexByte(r.line[1], r.line[2])
|
||||
if err != nil {
|
||||
return n, err
|
||||
}
|
||||
r.line = r.line[2:] // 2 of the 3; other 1 is done below
|
||||
case b == '\t' || b == '\r' || b == '\n':
|
||||
break
|
||||
case b < ' ' || b > '~':
|
||||
return n, fmt.Errorf("quotedprintable: invalid unescaped byte 0x%02x in body", b)
|
||||
}
|
||||
p[0] = b
|
||||
p = p[1:]
|
||||
r.line = r.line[1:]
|
||||
n++
|
||||
}
|
||||
return n, nil
|
||||
}
|
166
vendor/gopkg.in/alexcesaro/quotedprintable.v3/writer.go
generated
vendored
Normal file
166
vendor/gopkg.in/alexcesaro/quotedprintable.v3/writer.go
generated
vendored
Normal file
|
@ -0,0 +1,166 @@
|
|||
package quotedprintable
|
||||
|
||||
import "io"
|
||||
|
||||
const lineMaxLen = 76
|
||||
|
||||
// A Writer is a quoted-printable writer that implements io.WriteCloser.
|
||||
type Writer struct {
|
||||
// Binary mode treats the writer's input as pure binary and processes end of
|
||||
// line bytes as binary data.
|
||||
Binary bool
|
||||
|
||||
w io.Writer
|
||||
i int
|
||||
line [78]byte
|
||||
cr bool
|
||||
}
|
||||
|
||||
// NewWriter returns a new Writer that writes to w.
|
||||
func NewWriter(w io.Writer) *Writer {
|
||||
return &Writer{w: w}
|
||||
}
|
||||
|
||||
// Write encodes p using quoted-printable encoding and writes it to the
|
||||
// underlying io.Writer. It limits line length to 76 characters. The encoded
|
||||
// bytes are not necessarily flushed until the Writer is closed.
|
||||
func (w *Writer) Write(p []byte) (n int, err error) {
|
||||
for i, b := range p {
|
||||
switch {
|
||||
// Simple writes are done in batch.
|
||||
case b >= '!' && b <= '~' && b != '=':
|
||||
continue
|
||||
case isWhitespace(b) || !w.Binary && (b == '\n' || b == '\r'):
|
||||
continue
|
||||
}
|
||||
|
||||
if i > n {
|
||||
if err := w.write(p[n:i]); err != nil {
|
||||
return n, err
|
||||
}
|
||||
n = i
|
||||
}
|
||||
|
||||
if err := w.encode(b); err != nil {
|
||||
return n, err
|
||||
}
|
||||
n++
|
||||
}
|
||||
|
||||
if n == len(p) {
|
||||
return n, nil
|
||||
}
|
||||
|
||||
if err := w.write(p[n:]); err != nil {
|
||||
return n, err
|
||||
}
|
||||
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
// Close closes the Writer, flushing any unwritten data to the underlying
|
||||
// io.Writer, but does not close the underlying io.Writer.
|
||||
func (w *Writer) Close() error {
|
||||
if err := w.checkLastByte(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return w.flush()
|
||||
}
|
||||
|
||||
// write limits text encoded in quoted-printable to 76 characters per line.
|
||||
func (w *Writer) write(p []byte) error {
|
||||
for _, b := range p {
|
||||
if b == '\n' || b == '\r' {
|
||||
// If the previous byte was \r, the CRLF has already been inserted.
|
||||
if w.cr && b == '\n' {
|
||||
w.cr = false
|
||||
continue
|
||||
}
|
||||
|
||||
if b == '\r' {
|
||||
w.cr = true
|
||||
}
|
||||
|
||||
if err := w.checkLastByte(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := w.insertCRLF(); err != nil {
|
||||
return err
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if w.i == lineMaxLen-1 {
|
||||
if err := w.insertSoftLineBreak(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
w.line[w.i] = b
|
||||
w.i++
|
||||
w.cr = false
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *Writer) encode(b byte) error {
|
||||
if lineMaxLen-1-w.i < 3 {
|
||||
if err := w.insertSoftLineBreak(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
w.line[w.i] = '='
|
||||
w.line[w.i+1] = upperhex[b>>4]
|
||||
w.line[w.i+2] = upperhex[b&0x0f]
|
||||
w.i += 3
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// checkLastByte encodes the last buffered byte if it is a space or a tab.
|
||||
func (w *Writer) checkLastByte() error {
|
||||
if w.i == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
b := w.line[w.i-1]
|
||||
if isWhitespace(b) {
|
||||
w.i--
|
||||
if err := w.encode(b); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *Writer) insertSoftLineBreak() error {
|
||||
w.line[w.i] = '='
|
||||
w.i++
|
||||
|
||||
return w.insertCRLF()
|
||||
}
|
||||
|
||||
func (w *Writer) insertCRLF() error {
|
||||
w.line[w.i] = '\r'
|
||||
w.line[w.i+1] = '\n'
|
||||
w.i += 2
|
||||
|
||||
return w.flush()
|
||||
}
|
||||
|
||||
func (w *Writer) flush() error {
|
||||
if _, err := w.w.Write(w.line[:w.i]); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
w.i = 0
|
||||
return nil
|
||||
}
|
||||
|
||||
func isWhitespace(b byte) bool {
|
||||
return b == ' ' || b == '\t'
|
||||
}
|
17
vendor/gopkg.in/mail.v2/.gitignore
generated
vendored
Normal file
17
vendor/gopkg.in/mail.v2/.gitignore
generated
vendored
Normal file
|
@ -0,0 +1,17 @@
|
|||
|
||||
|
||||
# Binaries for programs and plugins
|
||||
*.exe
|
||||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
|
||||
# Test binary, build with `go test -c`
|
||||
*.test
|
||||
|
||||
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||
*.out
|
||||
|
||||
|
||||
# IDE's
|
||||
.idea/
|
25
vendor/gopkg.in/mail.v2/.travis.yml
generated
vendored
Normal file
25
vendor/gopkg.in/mail.v2/.travis.yml
generated
vendored
Normal file
|
@ -0,0 +1,25 @@
|
|||
language: go
|
||||
|
||||
go:
|
||||
- 1.2
|
||||
- 1.3
|
||||
- 1.4
|
||||
- 1.5
|
||||
- 1.6
|
||||
- 1.7
|
||||
- 1.8
|
||||
- 1.9
|
||||
- master
|
||||
|
||||
# safelist
|
||||
branches:
|
||||
only:
|
||||
- master
|
||||
- v2
|
||||
|
||||
notifications:
|
||||
email: false
|
||||
|
||||
before_install:
|
||||
- mkdir -p $GOPATH/src/gopkg.in &&
|
||||
ln -s ../github.com/go-mail/mail $GOPATH/src/gopkg.in/mail.v2
|
88
vendor/gopkg.in/mail.v2/CHANGELOG.md
generated
vendored
Normal file
88
vendor/gopkg.in/mail.v2/CHANGELOG.md
generated
vendored
Normal file
|
@ -0,0 +1,88 @@
|
|||
# Change Log
|
||||
All notable changes to this project will be documented in this file.
|
||||
This project adheres to [Semantic Versioning](http://semver.org/).
|
||||
|
||||
## *Unreleased*
|
||||
|
||||
## [2.3.1] - 2018-11-12
|
||||
|
||||
### Fixed
|
||||
|
||||
- #39: Reverts addition of Go modules `go.mod` manifest.
|
||||
|
||||
## [2.3.0] - 2018-11-10
|
||||
|
||||
### Added
|
||||
|
||||
- #12: Adds `SendError` to provide additional info about the cause and index of
|
||||
a failed attempt to transmit a batch of messages.
|
||||
- go-gomail#78: Adds new `Message` methods for attaching and embedding
|
||||
`io.Reader`s: `AttachReader` and `EmbedReader`.
|
||||
|
||||
### Fixed
|
||||
|
||||
- #26: Fixes RFC 1341 compliance by properly capitalizing the
|
||||
`MIME-Version` header.
|
||||
- #30: Fixes IO errors being silently dropped in `Message.WriteTo`.
|
||||
|
||||
## [2.2.0] - 2018-03-01
|
||||
|
||||
### Added
|
||||
|
||||
- #20: Adds `Message.SetBoundary` to allow specifying a custom MIME boundary.
|
||||
- #22: Adds `Message.SetBodyWriter` to make it easy to use text/template and
|
||||
html/template for message bodies. Contributed by Quantcast.
|
||||
- #25: Adds `Dialer.StartTLSPolicy` so that `MandatoryStartTLS` can be required,
|
||||
or `NoStartTLS` can disable it. Contributed by Quantcast.
|
||||
|
||||
## [2.1.0] - 2017-12-14
|
||||
|
||||
### Added
|
||||
|
||||
- go-gomail#40: Adds `Dialer.LocalName` field to allow specifying the hostname
|
||||
sent with SMTP's HELO command.
|
||||
- go-gomail#47: `Message.SetBody`, `Message.AddAlternative`, and
|
||||
`Message.AddAlternativeWriter` allow specifying the encoding of message parts.
|
||||
- `Dialer.Dial`'s returned `SendCloser` automatically redials after a timeout.
|
||||
- go-gomail#55, go-gomail#56: Adds `Rename` to allow specifying filename
|
||||
of an attachment.
|
||||
- go-gomail#100: Exports `NetDialTimeout` to allow setting a custom dialer.
|
||||
- go-gomail#70: Adds `Dialer.Timeout` field to allow specifying a timeout for
|
||||
dials, reads, and writes.
|
||||
|
||||
### Changed
|
||||
|
||||
- go-gomail#52: `Dialer.Dial` automatically uses CRAM-MD5 when available.
|
||||
- `Dialer.Dial` specifies a default timeout of 10 seconds.
|
||||
- Gomail is forked from <https://github.com/go-gomail/gomail/> to
|
||||
<https://github.com/go-mail/mail/>.
|
||||
|
||||
### Deprecated
|
||||
|
||||
- go-gomail#52: `NewPlainDialer` is deprecated in favor of `NewDialer`.
|
||||
|
||||
### Fixed
|
||||
|
||||
- go-gomail#41, go-gomail#42: Fixes a panic when a `Message` contains a
|
||||
nil header.
|
||||
- go-gomail#44: Fixes `AddAlternativeWriter` replacing the message body instead
|
||||
of adding a body part.
|
||||
- go-gomail#53: Folds long header lines for RFC 2047 compliance.
|
||||
- go-gomail#54: Fixes `Message.FormatAddress` when name is blank.
|
||||
|
||||
## [2.0.0] - 2015-09-02
|
||||
|
||||
- Mailer has been removed. It has been replaced by Dialer and Sender.
|
||||
- `File` type and the `CreateFile` and `OpenFile` functions have been removed.
|
||||
- `Message.Attach` and `Message.Embed` have a new signature.
|
||||
- `Message.GetBodyWriter` has been removed. Use `Message.AddAlternativeWriter`
|
||||
instead.
|
||||
- `Message.Export` has been removed. `Message.WriteTo` can be used instead.
|
||||
- `Message.DelHeader` has been removed.
|
||||
- The `Bcc` header field is no longer sent. It is far more simpler and
|
||||
efficient: the same message is sent to all recipients instead of sending a
|
||||
different email to each Bcc address.
|
||||
- LoginAuth has been removed. `NewPlainDialer` now implements the LOGIN
|
||||
authentication mechanism when needed.
|
||||
- Go 1.2 is now required instead of Go 1.3. No external dependency are used when
|
||||
using Go 1.5.
|
20
vendor/gopkg.in/mail.v2/CONTRIBUTING.md
generated
vendored
Normal file
20
vendor/gopkg.in/mail.v2/CONTRIBUTING.md
generated
vendored
Normal file
|
@ -0,0 +1,20 @@
|
|||
Thank you for contributing to Gomail! Here are a few guidelines:
|
||||
|
||||
## Bugs
|
||||
|
||||
If you think you found a bug, create an issue and supply the minimum amount
|
||||
of code triggering the bug so it can be reproduced.
|
||||
|
||||
|
||||
## Fixing a bug
|
||||
|
||||
If you want to fix a bug, you can send a pull request. It should contains a
|
||||
new test or update an existing one to cover that bug.
|
||||
|
||||
|
||||
## New feature proposal
|
||||
|
||||
If you think Gomail lacks a feature, you can open an issue or send a pull
|
||||
request. I want to keep Gomail code and API as simple as possible so please
|
||||
describe your needs so we can discuss whether this feature should be added to
|
||||
Gomail or not.
|
20
vendor/gopkg.in/mail.v2/LICENSE
generated
vendored
Normal file
20
vendor/gopkg.in/mail.v2/LICENSE
generated
vendored
Normal file
|
@ -0,0 +1,20 @@
|
|||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2014 Alexandre Cesaro
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in
|
||||
the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
129
vendor/gopkg.in/mail.v2/README.md
generated
vendored
Normal file
129
vendor/gopkg.in/mail.v2/README.md
generated
vendored
Normal file
|
@ -0,0 +1,129 @@
|
|||
# Gomail
|
||||
[![Build Status](https://travis-ci.org/go-mail/mail.svg?branch=master)](https://travis-ci.org/go-mail/mail) [![Code Coverage](http://gocover.io/_badge/github.com/go-mail/mail)](http://gocover.io/github.com/go-mail/mail) [![Documentation](https://godoc.org/github.com/go-mail/mail?status.svg)](https://godoc.org/github.com/go-mail/mail)
|
||||
|
||||
This is an actively maintained fork of [Gomail][1] and includes fixes and
|
||||
improvements for a number of outstanding issues. The current progress is
|
||||
as follows:
|
||||
|
||||
- [x] Timeouts and retries can be specified outside of the 10 second default.
|
||||
- [x] Proxying is supported through specifying a custom [NetDialTimeout][2].
|
||||
- [ ] Filenames are properly encoded for non-ASCII characters.
|
||||
- [ ] Email addresses are properly encoded for non-ASCII characters.
|
||||
- [ ] Embedded files and attachments are tested for their existence.
|
||||
- [ ] An `io.Reader` can be supplied when embedding and attaching files.
|
||||
|
||||
See [Transitioning Existing Codebases][3] for more information on switching.
|
||||
|
||||
[1]: https://github.com/go-gomail/gomail
|
||||
[2]: https://godoc.org/gopkg.in/mail.v2#NetDialTimeout
|
||||
[3]: #transitioning-existing-codebases
|
||||
|
||||
## Introduction
|
||||
|
||||
Gomail is a simple and efficient package to send emails. It is well tested and
|
||||
documented.
|
||||
|
||||
Gomail can only send emails using an SMTP server. But the API is flexible and it
|
||||
is easy to implement other methods for sending emails using a local Postfix, an
|
||||
API, etc.
|
||||
|
||||
It requires Go 1.2 or newer. With Go 1.5, no external dependencies are used.
|
||||
|
||||
|
||||
## Features
|
||||
|
||||
Gomail supports:
|
||||
- Attachments
|
||||
- Embedded images
|
||||
- HTML and text templates
|
||||
- Automatic encoding of special characters
|
||||
- SSL and TLS
|
||||
- Sending multiple emails with the same SMTP connection
|
||||
|
||||
|
||||
## Documentation
|
||||
|
||||
https://godoc.org/github.com/go-mail/mail
|
||||
|
||||
|
||||
## Download
|
||||
|
||||
If you're already using a dependency manager, like [dep][dep], use the following
|
||||
import path:
|
||||
|
||||
```
|
||||
github.com/go-mail/mail
|
||||
```
|
||||
|
||||
If you *aren't* using vendoring, `go get` the [Gopkg.in](http://gopkg.in)
|
||||
import path:
|
||||
|
||||
```
|
||||
gopkg.in/mail.v2
|
||||
```
|
||||
|
||||
[dep]: https://github.com/golang/dep#readme
|
||||
|
||||
## Examples
|
||||
|
||||
See the [examples in the documentation](https://godoc.org/github.com/go-mail/mail#example-package).
|
||||
|
||||
|
||||
## FAQ
|
||||
|
||||
### x509: certificate signed by unknown authority
|
||||
|
||||
If you get this error it means the certificate used by the SMTP server is not
|
||||
considered valid by the client running Gomail. As a quick workaround you can
|
||||
bypass the verification of the server's certificate chain and host name by using
|
||||
`SetTLSConfig`:
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
|
||||
"gopkg.in/mail.v2"
|
||||
)
|
||||
|
||||
func main() {
|
||||
d := mail.NewDialer("smtp.example.com", 587, "user", "123456")
|
||||
d.TLSConfig = &tls.Config{InsecureSkipVerify: true}
|
||||
|
||||
// Send emails using d.
|
||||
}
|
||||
```
|
||||
|
||||
Note, however, that this is insecure and should not be used in production.
|
||||
|
||||
### Transitioning Existing Codebases
|
||||
|
||||
If you're already using the original Gomail, switching is as easy as updating
|
||||
the import line to:
|
||||
|
||||
```
|
||||
import gomail "gopkg.in/mail.v2"
|
||||
```
|
||||
|
||||
## Contribute
|
||||
|
||||
Contributions are more than welcome! See [CONTRIBUTING.md](CONTRIBUTING.md) for
|
||||
more info.
|
||||
|
||||
|
||||
## Change log
|
||||
|
||||
See [CHANGELOG.md](CHANGELOG.md).
|
||||
|
||||
|
||||
## License
|
||||
|
||||
[MIT](LICENSE)
|
||||
|
||||
|
||||
## Support & Contact
|
||||
|
||||
You can ask questions on the [Gomail
|
||||
thread](https://groups.google.com/d/topic/golang-nuts/jMxZHzvvEVg/discussion)
|
||||
in the Go mailing-list.
|
49
vendor/gopkg.in/mail.v2/auth.go
generated
vendored
Normal file
49
vendor/gopkg.in/mail.v2/auth.go
generated
vendored
Normal file
|
@ -0,0 +1,49 @@
|
|||
package mail
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/smtp"
|
||||
)
|
||||
|
||||
// loginAuth is an smtp.Auth that implements the LOGIN authentication mechanism.
|
||||
type loginAuth struct {
|
||||
username string
|
||||
password string
|
||||
host string
|
||||
}
|
||||
|
||||
func (a *loginAuth) Start(server *smtp.ServerInfo) (string, []byte, error) {
|
||||
if !server.TLS {
|
||||
advertised := false
|
||||
for _, mechanism := range server.Auth {
|
||||
if mechanism == "LOGIN" {
|
||||
advertised = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !advertised {
|
||||
return "", nil, errors.New("gomail: unencrypted connection")
|
||||
}
|
||||
}
|
||||
if server.Name != a.host {
|
||||
return "", nil, errors.New("gomail: wrong host name")
|
||||
}
|
||||
return "LOGIN", nil, nil
|
||||
}
|
||||
|
||||
func (a *loginAuth) Next(fromServer []byte, more bool) ([]byte, error) {
|
||||
if !more {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
switch {
|
||||
case bytes.Equal(fromServer, []byte("Username:")):
|
||||
return []byte(a.username), nil
|
||||
case bytes.Equal(fromServer, []byte("Password:")):
|
||||
return []byte(a.password), nil
|
||||
default:
|
||||
return nil, fmt.Errorf("gomail: unexpected server challenge: %s", fromServer)
|
||||
}
|
||||
}
|
6
vendor/gopkg.in/mail.v2/doc.go
generated
vendored
Normal file
6
vendor/gopkg.in/mail.v2/doc.go
generated
vendored
Normal file
|
@ -0,0 +1,6 @@
|
|||
// Package gomail provides a simple interface to compose emails and to mail them
|
||||
// efficiently.
|
||||
//
|
||||
// More info on Github: https://github.com/go-mail/mail
|
||||
//
|
||||
package mail
|
16
vendor/gopkg.in/mail.v2/errors.go
generated
vendored
Normal file
16
vendor/gopkg.in/mail.v2/errors.go
generated
vendored
Normal file
|
@ -0,0 +1,16 @@
|
|||
package mail
|
||||
|
||||
import "fmt"
|
||||
|
||||
// A SendError represents the failure to transmit a Message, detailing the cause
|
||||
// of the failure and index of the Message within a batch.
|
||||
type SendError struct {
|
||||
// Index specifies the index of the Message within a batch.
|
||||
Index uint
|
||||
Cause error
|
||||
}
|
||||
|
||||
func (err *SendError) Error() string {
|
||||
return fmt.Sprintf("gomail: could not send email %d: %v",
|
||||
err.Index+1, err.Cause)
|
||||
}
|
359
vendor/gopkg.in/mail.v2/message.go
generated
vendored
Normal file
359
vendor/gopkg.in/mail.v2/message.go
generated
vendored
Normal file
|
@ -0,0 +1,359 @@
|
|||
package mail
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Message represents an email.
|
||||
type Message struct {
|
||||
header header
|
||||
parts []*part
|
||||
attachments []*file
|
||||
embedded []*file
|
||||
charset string
|
||||
encoding Encoding
|
||||
hEncoder mimeEncoder
|
||||
buf bytes.Buffer
|
||||
boundary string
|
||||
}
|
||||
|
||||
type header map[string][]string
|
||||
|
||||
type part struct {
|
||||
contentType string
|
||||
copier func(io.Writer) error
|
||||
encoding Encoding
|
||||
}
|
||||
|
||||
// NewMessage creates a new message. It uses UTF-8 and quoted-printable encoding
|
||||
// by default.
|
||||
func NewMessage(settings ...MessageSetting) *Message {
|
||||
m := &Message{
|
||||
header: make(header),
|
||||
charset: "UTF-8",
|
||||
encoding: QuotedPrintable,
|
||||
}
|
||||
|
||||
m.applySettings(settings)
|
||||
|
||||
if m.encoding == Base64 {
|
||||
m.hEncoder = bEncoding
|
||||
} else {
|
||||
m.hEncoder = qEncoding
|
||||
}
|
||||
|
||||
return m
|
||||
}
|
||||
|
||||
// Reset resets the message so it can be reused. The message keeps its previous
|
||||
// settings so it is in the same state that after a call to NewMessage.
|
||||
func (m *Message) Reset() {
|
||||
for k := range m.header {
|
||||
delete(m.header, k)
|
||||
}
|
||||
m.parts = nil
|
||||
m.attachments = nil
|
||||
m.embedded = nil
|
||||
}
|
||||
|
||||
func (m *Message) applySettings(settings []MessageSetting) {
|
||||
for _, s := range settings {
|
||||
s(m)
|
||||
}
|
||||
}
|
||||
|
||||
// A MessageSetting can be used as an argument in NewMessage to configure an
|
||||
// email.
|
||||
type MessageSetting func(m *Message)
|
||||
|
||||
// SetCharset is a message setting to set the charset of the email.
|
||||
func SetCharset(charset string) MessageSetting {
|
||||
return func(m *Message) {
|
||||
m.charset = charset
|
||||
}
|
||||
}
|
||||
|
||||
// SetEncoding is a message setting to set the encoding of the email.
|
||||
func SetEncoding(enc Encoding) MessageSetting {
|
||||
return func(m *Message) {
|
||||
m.encoding = enc
|
||||
}
|
||||
}
|
||||
|
||||
// Encoding represents a MIME encoding scheme like quoted-printable or base64.
|
||||
type Encoding string
|
||||
|
||||
const (
|
||||
// QuotedPrintable represents the quoted-printable encoding as defined in
|
||||
// RFC 2045.
|
||||
QuotedPrintable Encoding = "quoted-printable"
|
||||
// Base64 represents the base64 encoding as defined in RFC 2045.
|
||||
Base64 Encoding = "base64"
|
||||
// Unencoded can be used to avoid encoding the body of an email. The headers
|
||||
// will still be encoded using quoted-printable encoding.
|
||||
Unencoded Encoding = "8bit"
|
||||
)
|
||||
|
||||
// SetBoundary sets a custom multipart boundary.
|
||||
func (m *Message) SetBoundary(boundary string) {
|
||||
m.boundary = boundary
|
||||
}
|
||||
|
||||
// SetHeader sets a value to the given header field.
|
||||
func (m *Message) SetHeader(field string, value ...string) {
|
||||
m.encodeHeader(value)
|
||||
m.header[field] = value
|
||||
}
|
||||
|
||||
func (m *Message) encodeHeader(values []string) {
|
||||
for i := range values {
|
||||
values[i] = m.encodeString(values[i])
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Message) encodeString(value string) string {
|
||||
return m.hEncoder.Encode(m.charset, value)
|
||||
}
|
||||
|
||||
// SetHeaders sets the message headers.
|
||||
func (m *Message) SetHeaders(h map[string][]string) {
|
||||
for k, v := range h {
|
||||
m.SetHeader(k, v...)
|
||||
}
|
||||
}
|
||||
|
||||
// SetAddressHeader sets an address to the given header field.
|
||||
func (m *Message) SetAddressHeader(field, address, name string) {
|
||||
m.header[field] = []string{m.FormatAddress(address, name)}
|
||||
}
|
||||
|
||||
// FormatAddress formats an address and a name as a valid RFC 5322 address.
|
||||
func (m *Message) FormatAddress(address, name string) string {
|
||||
if name == "" {
|
||||
return address
|
||||
}
|
||||
|
||||
enc := m.encodeString(name)
|
||||
if enc == name {
|
||||
m.buf.WriteByte('"')
|
||||
for i := 0; i < len(name); i++ {
|
||||
b := name[i]
|
||||
if b == '\\' || b == '"' {
|
||||
m.buf.WriteByte('\\')
|
||||
}
|
||||
m.buf.WriteByte(b)
|
||||
}
|
||||
m.buf.WriteByte('"')
|
||||
} else if hasSpecials(name) {
|
||||
m.buf.WriteString(bEncoding.Encode(m.charset, name))
|
||||
} else {
|
||||
m.buf.WriteString(enc)
|
||||
}
|
||||
m.buf.WriteString(" <")
|
||||
m.buf.WriteString(address)
|
||||
m.buf.WriteByte('>')
|
||||
|
||||
addr := m.buf.String()
|
||||
m.buf.Reset()
|
||||
return addr
|
||||
}
|
||||
|
||||
func hasSpecials(text string) bool {
|
||||
for i := 0; i < len(text); i++ {
|
||||
switch c := text[i]; c {
|
||||
case '(', ')', '<', '>', '[', ']', ':', ';', '@', '\\', ',', '.', '"':
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// SetDateHeader sets a date to the given header field.
|
||||
func (m *Message) SetDateHeader(field string, date time.Time) {
|
||||
m.header[field] = []string{m.FormatDate(date)}
|
||||
}
|
||||
|
||||
// FormatDate formats a date as a valid RFC 5322 date.
|
||||
func (m *Message) FormatDate(date time.Time) string {
|
||||
return date.Format(time.RFC1123Z)
|
||||
}
|
||||
|
||||
// GetHeader gets a header field.
|
||||
func (m *Message) GetHeader(field string) []string {
|
||||
return m.header[field]
|
||||
}
|
||||
|
||||
// SetBody sets the body of the message. It replaces any content previously set
|
||||
// by SetBody, SetBodyWriter, AddAlternative or AddAlternativeWriter.
|
||||
func (m *Message) SetBody(contentType, body string, settings ...PartSetting) {
|
||||
m.SetBodyWriter(contentType, newCopier(body), settings...)
|
||||
}
|
||||
|
||||
// SetBodyWriter sets the body of the message. It can be useful with the
|
||||
// text/template or html/template packages.
|
||||
func (m *Message) SetBodyWriter(contentType string, f func(io.Writer) error, settings ...PartSetting) {
|
||||
m.parts = []*part{m.newPart(contentType, f, settings)}
|
||||
}
|
||||
|
||||
// AddAlternative adds an alternative part to the message.
|
||||
//
|
||||
// It is commonly used to send HTML emails that default to the plain text
|
||||
// version for backward compatibility. AddAlternative appends the new part to
|
||||
// the end of the message. So the plain text part should be added before the
|
||||
// HTML part. See http://en.wikipedia.org/wiki/MIME#Alternative
|
||||
func (m *Message) AddAlternative(contentType, body string, settings ...PartSetting) {
|
||||
m.AddAlternativeWriter(contentType, newCopier(body), settings...)
|
||||
}
|
||||
|
||||
func newCopier(s string) func(io.Writer) error {
|
||||
return func(w io.Writer) error {
|
||||
_, err := io.WriteString(w, s)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// AddAlternativeWriter adds an alternative part to the message. It can be
|
||||
// useful with the text/template or html/template packages.
|
||||
func (m *Message) AddAlternativeWriter(contentType string, f func(io.Writer) error, settings ...PartSetting) {
|
||||
m.parts = append(m.parts, m.newPart(contentType, f, settings))
|
||||
}
|
||||
|
||||
func (m *Message) newPart(contentType string, f func(io.Writer) error, settings []PartSetting) *part {
|
||||
p := &part{
|
||||
contentType: contentType,
|
||||
copier: f,
|
||||
encoding: m.encoding,
|
||||
}
|
||||
|
||||
for _, s := range settings {
|
||||
s(p)
|
||||
}
|
||||
|
||||
return p
|
||||
}
|
||||
|
||||
// A PartSetting can be used as an argument in Message.SetBody,
|
||||
// Message.SetBodyWriter, Message.AddAlternative or Message.AddAlternativeWriter
|
||||
// to configure the part added to a message.
|
||||
type PartSetting func(*part)
|
||||
|
||||
// SetPartEncoding sets the encoding of the part added to the message. By
|
||||
// default, parts use the same encoding than the message.
|
||||
func SetPartEncoding(e Encoding) PartSetting {
|
||||
return PartSetting(func(p *part) {
|
||||
p.encoding = e
|
||||
})
|
||||
}
|
||||
|
||||
type file struct {
|
||||
Name string
|
||||
Header map[string][]string
|
||||
CopyFunc func(w io.Writer) error
|
||||
}
|
||||
|
||||
func (f *file) setHeader(field, value string) {
|
||||
f.Header[field] = []string{value}
|
||||
}
|
||||
|
||||
// A FileSetting can be used as an argument in Message.Attach or Message.Embed.
|
||||
type FileSetting func(*file)
|
||||
|
||||
// SetHeader is a file setting to set the MIME header of the message part that
|
||||
// contains the file content.
|
||||
//
|
||||
// Mandatory headers are automatically added if they are not set when sending
|
||||
// the email.
|
||||
func SetHeader(h map[string][]string) FileSetting {
|
||||
return func(f *file) {
|
||||
for k, v := range h {
|
||||
f.Header[k] = v
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Rename is a file setting to set the name of the attachment if the name is
|
||||
// different than the filename on disk.
|
||||
func Rename(name string) FileSetting {
|
||||
return func(f *file) {
|
||||
f.Name = name
|
||||
}
|
||||
}
|
||||
|
||||
// SetCopyFunc is a file setting to replace the function that runs when the
|
||||
// message is sent. It should copy the content of the file to the io.Writer.
|
||||
//
|
||||
// The default copy function opens the file with the given filename, and copy
|
||||
// its content to the io.Writer.
|
||||
func SetCopyFunc(f func(io.Writer) error) FileSetting {
|
||||
return func(fi *file) {
|
||||
fi.CopyFunc = f
|
||||
}
|
||||
}
|
||||
|
||||
// AttachReader attaches a file using an io.Reader
|
||||
func (m *Message) AttachReader(name string, r io.Reader, settings ...FileSetting) {
|
||||
m.attachments = m.appendFile(m.attachments, fileFromReader(name, r), settings)
|
||||
}
|
||||
|
||||
// Attach attaches the files to the email.
|
||||
func (m *Message) Attach(filename string, settings ...FileSetting) {
|
||||
m.attachments = m.appendFile(m.attachments, fileFromFilename(filename), settings)
|
||||
}
|
||||
|
||||
// EmbedReader embeds the images to the email.
|
||||
func (m *Message) EmbedReader(name string, r io.Reader, settings ...FileSetting) {
|
||||
m.embedded = m.appendFile(m.embedded, fileFromReader(name, r), settings)
|
||||
}
|
||||
|
||||
// Embed embeds the images to the email.
|
||||
func (m *Message) Embed(filename string, settings ...FileSetting) {
|
||||
m.embedded = m.appendFile(m.embedded, fileFromFilename(filename), settings)
|
||||
}
|
||||
|
||||
func fileFromFilename(name string) *file {
|
||||
return &file{
|
||||
Name: filepath.Base(name),
|
||||
Header: make(map[string][]string),
|
||||
CopyFunc: func(w io.Writer) error {
|
||||
h, err := os.Open(name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := io.Copy(w, h); err != nil {
|
||||
h.Close()
|
||||
return err
|
||||
}
|
||||
return h.Close()
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func fileFromReader(name string, r io.Reader) *file {
|
||||
return &file{
|
||||
Name: filepath.Base(name),
|
||||
Header: make(map[string][]string),
|
||||
CopyFunc: func(w io.Writer) error {
|
||||
if _, err := io.Copy(w, r); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Message) appendFile(list []*file, f *file, settings []FileSetting) []*file {
|
||||
for _, s := range settings {
|
||||
s(f)
|
||||
}
|
||||
|
||||
if list == nil {
|
||||
return []*file{f}
|
||||
}
|
||||
|
||||
return append(list, f)
|
||||
}
|
21
vendor/gopkg.in/mail.v2/mime.go
generated
vendored
Normal file
21
vendor/gopkg.in/mail.v2/mime.go
generated
vendored
Normal file
|
@ -0,0 +1,21 @@
|
|||
// +build go1.5
|
||||
|
||||
package mail
|
||||
|
||||
import (
|
||||
"mime"
|
||||
"mime/quotedprintable"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var newQPWriter = quotedprintable.NewWriter
|
||||
|
||||
type mimeEncoder struct {
|
||||
mime.WordEncoder
|
||||
}
|
||||
|
||||
var (
|
||||
bEncoding = mimeEncoder{mime.BEncoding}
|
||||
qEncoding = mimeEncoder{mime.QEncoding}
|
||||
lastIndexByte = strings.LastIndexByte
|
||||
)
|
25
vendor/gopkg.in/mail.v2/mime_go14.go
generated
vendored
Normal file
25
vendor/gopkg.in/mail.v2/mime_go14.go
generated
vendored
Normal file
|
@ -0,0 +1,25 @@
|
|||
// +build !go1.5
|
||||
|
||||
package mail
|
||||
|
||||
import "gopkg.in/alexcesaro/quotedprintable.v3"
|
||||
|
||||
var newQPWriter = quotedprintable.NewWriter
|
||||
|
||||
type mimeEncoder struct {
|
||||
quotedprintable.WordEncoder
|
||||
}
|
||||
|
||||
var (
|
||||
bEncoding = mimeEncoder{quotedprintable.BEncoding}
|
||||
qEncoding = mimeEncoder{quotedprintable.QEncoding}
|
||||
lastIndexByte = func(s string, c byte) int {
|
||||
for i := len(s) - 1; i >= 0; i-- {
|
||||
|
||||
if s[i] == c {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
)
|
116
vendor/gopkg.in/mail.v2/send.go
generated
vendored
Normal file
116
vendor/gopkg.in/mail.v2/send.go
generated
vendored
Normal file
|
@ -0,0 +1,116 @@
|
|||
package mail
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
stdmail "net/mail"
|
||||
)
|
||||
|
||||
// Sender is the interface that wraps the Send method.
|
||||
//
|
||||
// Send sends an email to the given addresses.
|
||||
type Sender interface {
|
||||
Send(from string, to []string, msg io.WriterTo) error
|
||||
}
|
||||
|
||||
// SendCloser is the interface that groups the Send and Close methods.
|
||||
type SendCloser interface {
|
||||
Sender
|
||||
Close() error
|
||||
}
|
||||
|
||||
// A SendFunc is a function that sends emails to the given addresses.
|
||||
//
|
||||
// The SendFunc type is an adapter to allow the use of ordinary functions as
|
||||
// email senders. If f is a function with the appropriate signature, SendFunc(f)
|
||||
// is a Sender object that calls f.
|
||||
type SendFunc func(from string, to []string, msg io.WriterTo) error
|
||||
|
||||
// Send calls f(from, to, msg).
|
||||
func (f SendFunc) Send(from string, to []string, msg io.WriterTo) error {
|
||||
return f(from, to, msg)
|
||||
}
|
||||
|
||||
// Send sends emails using the given Sender.
|
||||
func Send(s Sender, msg ...*Message) error {
|
||||
for i, m := range msg {
|
||||
if err := send(s, m); err != nil {
|
||||
return &SendError{Cause: err, Index: uint(i)}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func send(s Sender, m *Message) error {
|
||||
from, err := m.getFrom()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
to, err := m.getRecipients()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := s.Send(from, to, m); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Message) getFrom() (string, error) {
|
||||
from := m.header["Sender"]
|
||||
if len(from) == 0 {
|
||||
from = m.header["From"]
|
||||
if len(from) == 0 {
|
||||
return "", errors.New(`gomail: invalid message, "From" field is absent`)
|
||||
}
|
||||
}
|
||||
|
||||
return parseAddress(from[0])
|
||||
}
|
||||
|
||||
func (m *Message) getRecipients() ([]string, error) {
|
||||
n := 0
|
||||
for _, field := range []string{"To", "Cc", "Bcc"} {
|
||||
if addresses, ok := m.header[field]; ok {
|
||||
n += len(addresses)
|
||||
}
|
||||
}
|
||||
list := make([]string, 0, n)
|
||||
|
||||
for _, field := range []string{"To", "Cc", "Bcc"} {
|
||||
if addresses, ok := m.header[field]; ok {
|
||||
for _, a := range addresses {
|
||||
addr, err := parseAddress(a)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
list = addAddress(list, addr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return list, nil
|
||||
}
|
||||
|
||||
func addAddress(list []string, addr string) []string {
|
||||
for _, a := range list {
|
||||
if addr == a {
|
||||
return list
|
||||
}
|
||||
}
|
||||
|
||||
return append(list, addr)
|
||||
}
|
||||
|
||||
func parseAddress(field string) (string, error) {
|
||||
addr, err := stdmail.ParseAddress(field)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("gomail: invalid address %q: %v", field, err)
|
||||
}
|
||||
return addr.Address, nil
|
||||
}
|
292
vendor/gopkg.in/mail.v2/smtp.go
generated
vendored
Normal file
292
vendor/gopkg.in/mail.v2/smtp.go
generated
vendored
Normal file
|
@ -0,0 +1,292 @@
|
|||
package mail
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/smtp"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// A Dialer is a dialer to an SMTP server.
|
||||
type Dialer struct {
|
||||
// Host represents the host of the SMTP server.
|
||||
Host string
|
||||
// Port represents the port of the SMTP server.
|
||||
Port int
|
||||
// Username is the username to use to authenticate to the SMTP server.
|
||||
Username string
|
||||
// Password is the password to use to authenticate to the SMTP server.
|
||||
Password string
|
||||
// Auth represents the authentication mechanism used to authenticate to the
|
||||
// SMTP server.
|
||||
Auth smtp.Auth
|
||||
// SSL defines whether an SSL connection is used. It should be false in
|
||||
// most cases since the authentication mechanism should use the STARTTLS
|
||||
// extension instead.
|
||||
SSL bool
|
||||
// TLSConfig represents the TLS configuration used for the TLS (when the
|
||||
// STARTTLS extension is used) or SSL connection.
|
||||
TLSConfig *tls.Config
|
||||
// StartTLSPolicy represents the TLS security level required to
|
||||
// communicate with the SMTP server.
|
||||
//
|
||||
// This defaults to OpportunisticStartTLS for backwards compatibility,
|
||||
// but we recommend MandatoryStartTLS for all modern SMTP servers.
|
||||
//
|
||||
// This option has no effect if SSL is set to true.
|
||||
StartTLSPolicy StartTLSPolicy
|
||||
// LocalName is the hostname sent to the SMTP server with the HELO command.
|
||||
// By default, "localhost" is sent.
|
||||
LocalName string
|
||||
// Timeout to use for read/write operations. Defaults to 10 seconds, can
|
||||
// be set to 0 to disable timeouts.
|
||||
Timeout time.Duration
|
||||
// Whether we should retry mailing if the connection returned an error,
|
||||
// defaults to true.
|
||||
RetryFailure bool
|
||||
}
|
||||
|
||||
// NewDialer returns a new SMTP Dialer. The given parameters are used to connect
|
||||
// to the SMTP server.
|
||||
func NewDialer(host string, port int, username, password string) *Dialer {
|
||||
return &Dialer{
|
||||
Host: host,
|
||||
Port: port,
|
||||
Username: username,
|
||||
Password: password,
|
||||
SSL: port == 465,
|
||||
Timeout: 10 * time.Second,
|
||||
RetryFailure: true,
|
||||
}
|
||||
}
|
||||
|
||||
// NewPlainDialer returns a new SMTP Dialer. The given parameters are used to
|
||||
// connect to the SMTP server.
|
||||
//
|
||||
// Deprecated: Use NewDialer instead.
|
||||
func NewPlainDialer(host string, port int, username, password string) *Dialer {
|
||||
return NewDialer(host, port, username, password)
|
||||
}
|
||||
|
||||
// NetDialTimeout specifies the DialTimeout function to establish a connection
|
||||
// to the SMTP server. This can be used to override dialing in the case that a
|
||||
// proxy or other special behavior is needed.
|
||||
var NetDialTimeout = net.DialTimeout
|
||||
|
||||
// Dial dials and authenticates to an SMTP server. The returned SendCloser
|
||||
// should be closed when done using it.
|
||||
func (d *Dialer) Dial() (SendCloser, error) {
|
||||
conn, err := NetDialTimeout("tcp", addr(d.Host, d.Port), d.Timeout)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if d.SSL {
|
||||
conn = tlsClient(conn, d.tlsConfig())
|
||||
}
|
||||
|
||||
c, err := smtpNewClient(conn, d.Host)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if d.Timeout > 0 {
|
||||
conn.SetDeadline(time.Now().Add(d.Timeout))
|
||||
}
|
||||
|
||||
if d.LocalName != "" {
|
||||
if err := c.Hello(d.LocalName); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if !d.SSL && d.StartTLSPolicy != NoStartTLS {
|
||||
ok, _ := c.Extension("STARTTLS")
|
||||
if !ok && d.StartTLSPolicy == MandatoryStartTLS {
|
||||
err := StartTLSUnsupportedError{
|
||||
Policy: d.StartTLSPolicy}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if ok {
|
||||
if err := c.StartTLS(d.tlsConfig()); err != nil {
|
||||
c.Close()
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if d.Auth == nil && d.Username != "" {
|
||||
if ok, auths := c.Extension("AUTH"); ok {
|
||||
if strings.Contains(auths, "CRAM-MD5") {
|
||||
d.Auth = smtp.CRAMMD5Auth(d.Username, d.Password)
|
||||
} else if strings.Contains(auths, "LOGIN") &&
|
||||
!strings.Contains(auths, "PLAIN") {
|
||||
d.Auth = &loginAuth{
|
||||
username: d.Username,
|
||||
password: d.Password,
|
||||
host: d.Host,
|
||||
}
|
||||
} else {
|
||||
d.Auth = smtp.PlainAuth("", d.Username, d.Password, d.Host)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if d.Auth != nil {
|
||||
if err = c.Auth(d.Auth); err != nil {
|
||||
c.Close()
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return &smtpSender{c, conn, d}, nil
|
||||
}
|
||||
|
||||
func (d *Dialer) tlsConfig() *tls.Config {
|
||||
if d.TLSConfig == nil {
|
||||
return &tls.Config{ServerName: d.Host}
|
||||
}
|
||||
return d.TLSConfig
|
||||
}
|
||||
|
||||
// StartTLSPolicy constants are valid values for Dialer.StartTLSPolicy.
|
||||
type StartTLSPolicy int
|
||||
|
||||
const (
|
||||
// OpportunisticStartTLS means that SMTP transactions are encrypted if
|
||||
// STARTTLS is supported by the SMTP server. Otherwise, messages are
|
||||
// sent in the clear. This is the default setting.
|
||||
OpportunisticStartTLS StartTLSPolicy = iota
|
||||
// MandatoryStartTLS means that SMTP transactions must be encrypted.
|
||||
// SMTP transactions are aborted unless STARTTLS is supported by the
|
||||
// SMTP server.
|
||||
MandatoryStartTLS
|
||||
// NoStartTLS means encryption is disabled and messages are sent in the
|
||||
// clear.
|
||||
NoStartTLS = -1
|
||||
)
|
||||
|
||||
func (policy *StartTLSPolicy) String() string {
|
||||
switch *policy {
|
||||
case OpportunisticStartTLS:
|
||||
return "OpportunisticStartTLS"
|
||||
case MandatoryStartTLS:
|
||||
return "MandatoryStartTLS"
|
||||
case NoStartTLS:
|
||||
return "NoStartTLS"
|
||||
default:
|
||||
return fmt.Sprintf("StartTLSPolicy:%v", *policy)
|
||||
}
|
||||
}
|
||||
|
||||
// StartTLSUnsupportedError is returned by Dial when connecting to an SMTP
|
||||
// server that does not support STARTTLS.
|
||||
type StartTLSUnsupportedError struct {
|
||||
Policy StartTLSPolicy
|
||||
}
|
||||
|
||||
func (e StartTLSUnsupportedError) Error() string {
|
||||
return "gomail: " + e.Policy.String() + " required, but " +
|
||||
"SMTP server does not support STARTTLS"
|
||||
}
|
||||
|
||||
func addr(host string, port int) string {
|
||||
return fmt.Sprintf("%s:%d", host, port)
|
||||
}
|
||||
|
||||
// DialAndSend opens a connection to the SMTP server, sends the given emails and
|
||||
// closes the connection.
|
||||
func (d *Dialer) DialAndSend(m ...*Message) error {
|
||||
s, err := d.Dial()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer s.Close()
|
||||
|
||||
return Send(s, m...)
|
||||
}
|
||||
|
||||
type smtpSender struct {
|
||||
smtpClient
|
||||
conn net.Conn
|
||||
d *Dialer
|
||||
}
|
||||
|
||||
func (c *smtpSender) retryError(err error) bool {
|
||||
if !c.d.RetryFailure {
|
||||
return false
|
||||
}
|
||||
|
||||
if nerr, ok := err.(net.Error); ok && nerr.Timeout() {
|
||||
return true
|
||||
}
|
||||
|
||||
return err == io.EOF
|
||||
}
|
||||
|
||||
func (c *smtpSender) Send(from string, to []string, msg io.WriterTo) error {
|
||||
if c.d.Timeout > 0 {
|
||||
c.conn.SetDeadline(time.Now().Add(c.d.Timeout))
|
||||
}
|
||||
|
||||
if err := c.Mail(from); err != nil {
|
||||
if c.retryError(err) {
|
||||
// This is probably due to a timeout, so reconnect and try again.
|
||||
sc, derr := c.d.Dial()
|
||||
if derr == nil {
|
||||
if s, ok := sc.(*smtpSender); ok {
|
||||
*c = *s
|
||||
return c.Send(from, to, msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
for _, addr := range to {
|
||||
if err := c.Rcpt(addr); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
w, err := c.Data()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err = msg.WriteTo(w); err != nil {
|
||||
w.Close()
|
||||
return err
|
||||
}
|
||||
|
||||
return w.Close()
|
||||
}
|
||||
|
||||
func (c *smtpSender) Close() error {
|
||||
return c.Quit()
|
||||
}
|
||||
|
||||
// Stubbed out for tests.
|
||||
var (
|
||||
tlsClient = tls.Client
|
||||
smtpNewClient = func(conn net.Conn, host string) (smtpClient, error) {
|
||||
return smtp.NewClient(conn, host)
|
||||
}
|
||||
)
|
||||
|
||||
type smtpClient interface {
|
||||
Hello(string) error
|
||||
Extension(string) (bool, string)
|
||||
StartTLS(*tls.Config) error
|
||||
Auth(smtp.Auth) error
|
||||
Mail(string) error
|
||||
Rcpt(string) error
|
||||
Data() (io.WriteCloser, error)
|
||||
Quit() error
|
||||
Close() error
|
||||
}
|
313
vendor/gopkg.in/mail.v2/writeto.go
generated
vendored
Normal file
313
vendor/gopkg.in/mail.v2/writeto.go
generated
vendored
Normal file
|
@ -0,0 +1,313 @@
|
|||
package mail
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"io"
|
||||
"mime"
|
||||
"mime/multipart"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// WriteTo implements io.WriterTo. It dumps the whole message into w.
|
||||
func (m *Message) WriteTo(w io.Writer) (int64, error) {
|
||||
mw := &messageWriter{w: w}
|
||||
mw.writeMessage(m)
|
||||
return mw.n, mw.err
|
||||
}
|
||||
|
||||
func (w *messageWriter) writeMessage(m *Message) {
|
||||
if _, ok := m.header["MIME-Version"]; !ok {
|
||||
w.writeString("MIME-Version: 1.0\r\n")
|
||||
}
|
||||
if _, ok := m.header["Date"]; !ok {
|
||||
w.writeHeader("Date", m.FormatDate(now()))
|
||||
}
|
||||
w.writeHeaders(m.header)
|
||||
|
||||
if m.hasMixedPart() {
|
||||
w.openMultipart("mixed", m.boundary)
|
||||
}
|
||||
|
||||
if m.hasRelatedPart() {
|
||||
w.openMultipart("related", m.boundary)
|
||||
}
|
||||
|
||||
if m.hasAlternativePart() {
|
||||
w.openMultipart("alternative", m.boundary)
|
||||
}
|
||||
for _, part := range m.parts {
|
||||
w.writePart(part, m.charset)
|
||||
}
|
||||
if m.hasAlternativePart() {
|
||||
w.closeMultipart()
|
||||
}
|
||||
|
||||
w.addFiles(m.embedded, false)
|
||||
if m.hasRelatedPart() {
|
||||
w.closeMultipart()
|
||||
}
|
||||
|
||||
w.addFiles(m.attachments, true)
|
||||
if m.hasMixedPart() {
|
||||
w.closeMultipart()
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Message) hasMixedPart() bool {
|
||||
return (len(m.parts) > 0 && len(m.attachments) > 0) || len(m.attachments) > 1
|
||||
}
|
||||
|
||||
func (m *Message) hasRelatedPart() bool {
|
||||
return (len(m.parts) > 0 && len(m.embedded) > 0) || len(m.embedded) > 1
|
||||
}
|
||||
|
||||
func (m *Message) hasAlternativePart() bool {
|
||||
return len(m.parts) > 1
|
||||
}
|
||||
|
||||
type messageWriter struct {
|
||||
w io.Writer
|
||||
n int64
|
||||
writers [3]*multipart.Writer
|
||||
partWriter io.Writer
|
||||
depth uint8
|
||||
err error
|
||||
}
|
||||
|
||||
func (w *messageWriter) openMultipart(mimeType, boundary string) {
|
||||
mw := multipart.NewWriter(w)
|
||||
if boundary != "" {
|
||||
mw.SetBoundary(boundary)
|
||||
}
|
||||
contentType := "multipart/" + mimeType + ";\r\n boundary=" + mw.Boundary()
|
||||
w.writers[w.depth] = mw
|
||||
|
||||
if w.depth == 0 {
|
||||
w.writeHeader("Content-Type", contentType)
|
||||
w.writeString("\r\n")
|
||||
} else {
|
||||
w.createPart(map[string][]string{
|
||||
"Content-Type": {contentType},
|
||||
})
|
||||
}
|
||||
w.depth++
|
||||
}
|
||||
|
||||
func (w *messageWriter) createPart(h map[string][]string) {
|
||||
w.partWriter, w.err = w.writers[w.depth-1].CreatePart(h)
|
||||
}
|
||||
|
||||
func (w *messageWriter) closeMultipart() {
|
||||
if w.depth > 0 {
|
||||
w.writers[w.depth-1].Close()
|
||||
w.depth--
|
||||
}
|
||||
}
|
||||
|
||||
func (w *messageWriter) writePart(p *part, charset string) {
|
||||
w.writeHeaders(map[string][]string{
|
||||
"Content-Type": {p.contentType + "; charset=" + charset},
|
||||
"Content-Transfer-Encoding": {string(p.encoding)},
|
||||
})
|
||||
w.writeBody(p.copier, p.encoding)
|
||||
}
|
||||
|
||||
func (w *messageWriter) addFiles(files []*file, isAttachment bool) {
|
||||
for _, f := range files {
|
||||
if _, ok := f.Header["Content-Type"]; !ok {
|
||||
mediaType := mime.TypeByExtension(filepath.Ext(f.Name))
|
||||
if mediaType == "" {
|
||||
mediaType = "application/octet-stream"
|
||||
}
|
||||
f.setHeader("Content-Type", mediaType+`; name="`+f.Name+`"`)
|
||||
}
|
||||
|
||||
if _, ok := f.Header["Content-Transfer-Encoding"]; !ok {
|
||||
f.setHeader("Content-Transfer-Encoding", string(Base64))
|
||||
}
|
||||
|
||||
if _, ok := f.Header["Content-Disposition"]; !ok {
|
||||
var disp string
|
||||
if isAttachment {
|
||||
disp = "attachment"
|
||||
} else {
|
||||
disp = "inline"
|
||||
}
|
||||
f.setHeader("Content-Disposition", disp+`; filename="`+f.Name+`"`)
|
||||
}
|
||||
|
||||
if !isAttachment {
|
||||
if _, ok := f.Header["Content-ID"]; !ok {
|
||||
f.setHeader("Content-ID", "<"+f.Name+">")
|
||||
}
|
||||
}
|
||||
w.writeHeaders(f.Header)
|
||||
w.writeBody(f.CopyFunc, Base64)
|
||||
}
|
||||
}
|
||||
|
||||
func (w *messageWriter) Write(p []byte) (int, error) {
|
||||
if w.err != nil {
|
||||
return 0, errors.New("gomail: cannot write as writer is in error")
|
||||
}
|
||||
|
||||
var n int
|
||||
n, w.err = w.w.Write(p)
|
||||
w.n += int64(n)
|
||||
return n, w.err
|
||||
}
|
||||
|
||||
func (w *messageWriter) writeString(s string) {
|
||||
if w.err != nil { // do nothing when in error
|
||||
return
|
||||
}
|
||||
var n int
|
||||
n, w.err = io.WriteString(w.w, s)
|
||||
w.n += int64(n)
|
||||
}
|
||||
|
||||
func (w *messageWriter) writeHeader(k string, v ...string) {
|
||||
w.writeString(k)
|
||||
if len(v) == 0 {
|
||||
w.writeString(":\r\n")
|
||||
return
|
||||
}
|
||||
w.writeString(": ")
|
||||
|
||||
// Max header line length is 78 characters in RFC 5322 and 76 characters
|
||||
// in RFC 2047. So for the sake of simplicity we use the 76 characters
|
||||
// limit.
|
||||
charsLeft := 76 - len(k) - len(": ")
|
||||
|
||||
for i, s := range v {
|
||||
// If the line is already too long, insert a newline right away.
|
||||
if charsLeft < 1 {
|
||||
if i == 0 {
|
||||
w.writeString("\r\n ")
|
||||
} else {
|
||||
w.writeString(",\r\n ")
|
||||
}
|
||||
charsLeft = 75
|
||||
} else if i != 0 {
|
||||
w.writeString(", ")
|
||||
charsLeft -= 2
|
||||
}
|
||||
|
||||
// While the header content is too long, fold it by inserting a newline.
|
||||
for len(s) > charsLeft {
|
||||
s = w.writeLine(s, charsLeft)
|
||||
charsLeft = 75
|
||||
}
|
||||
w.writeString(s)
|
||||
if i := lastIndexByte(s, '\n'); i != -1 {
|
||||
charsLeft = 75 - (len(s) - i - 1)
|
||||
} else {
|
||||
charsLeft -= len(s)
|
||||
}
|
||||
}
|
||||
w.writeString("\r\n")
|
||||
}
|
||||
|
||||
func (w *messageWriter) writeLine(s string, charsLeft int) string {
|
||||
// If there is already a newline before the limit. Write the line.
|
||||
if i := strings.IndexByte(s, '\n'); i != -1 && i < charsLeft {
|
||||
w.writeString(s[:i+1])
|
||||
return s[i+1:]
|
||||
}
|
||||
|
||||
for i := charsLeft - 1; i >= 0; i-- {
|
||||
if s[i] == ' ' {
|
||||
w.writeString(s[:i])
|
||||
w.writeString("\r\n ")
|
||||
return s[i+1:]
|
||||
}
|
||||
}
|
||||
|
||||
// We could not insert a newline cleanly so look for a space or a newline
|
||||
// even if it is after the limit.
|
||||
for i := 75; i < len(s); i++ {
|
||||
if s[i] == ' ' {
|
||||
w.writeString(s[:i])
|
||||
w.writeString("\r\n ")
|
||||
return s[i+1:]
|
||||
}
|
||||
if s[i] == '\n' {
|
||||
w.writeString(s[:i+1])
|
||||
return s[i+1:]
|
||||
}
|
||||
}
|
||||
|
||||
// Too bad, no space or newline in the whole string. Just write everything.
|
||||
w.writeString(s)
|
||||
return ""
|
||||
}
|
||||
|
||||
func (w *messageWriter) writeHeaders(h map[string][]string) {
|
||||
if w.depth == 0 {
|
||||
for k, v := range h {
|
||||
if k != "Bcc" {
|
||||
w.writeHeader(k, v...)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
w.createPart(h)
|
||||
}
|
||||
}
|
||||
|
||||
func (w *messageWriter) writeBody(f func(io.Writer) error, enc Encoding) {
|
||||
var subWriter io.Writer
|
||||
if w.depth == 0 {
|
||||
w.writeString("\r\n")
|
||||
subWriter = w.w
|
||||
} else {
|
||||
subWriter = w.partWriter
|
||||
}
|
||||
|
||||
if enc == Base64 {
|
||||
wc := base64.NewEncoder(base64.StdEncoding, newBase64LineWriter(subWriter))
|
||||
w.err = f(wc)
|
||||
wc.Close()
|
||||
} else if enc == Unencoded {
|
||||
w.err = f(subWriter)
|
||||
} else {
|
||||
wc := newQPWriter(subWriter)
|
||||
w.err = f(wc)
|
||||
wc.Close()
|
||||
}
|
||||
}
|
||||
|
||||
// As required by RFC 2045, 6.7. (page 21) for quoted-printable, and
|
||||
// RFC 2045, 6.8. (page 25) for base64.
|
||||
const maxLineLen = 76
|
||||
|
||||
// base64LineWriter limits text encoded in base64 to 76 characters per line
|
||||
type base64LineWriter struct {
|
||||
w io.Writer
|
||||
lineLen int
|
||||
}
|
||||
|
||||
func newBase64LineWriter(w io.Writer) *base64LineWriter {
|
||||
return &base64LineWriter{w: w}
|
||||
}
|
||||
|
||||
func (w *base64LineWriter) Write(p []byte) (int, error) {
|
||||
n := 0
|
||||
for len(p)+w.lineLen > maxLineLen {
|
||||
w.w.Write(p[:maxLineLen-w.lineLen])
|
||||
w.w.Write([]byte("\r\n"))
|
||||
p = p[maxLineLen-w.lineLen:]
|
||||
n += maxLineLen - w.lineLen
|
||||
w.lineLen = 0
|
||||
}
|
||||
|
||||
w.w.Write(p)
|
||||
w.lineLen += len(p)
|
||||
|
||||
return n + len(p), nil
|
||||
}
|
||||
|
||||
// Stubbed out for testing.
|
||||
var now = time.Now
|
6
vendor/modules.txt
vendored
6
vendor/modules.txt
vendored
|
@ -154,6 +154,12 @@ google.golang.org/protobuf/types/descriptorpb
|
|||
google.golang.org/protobuf/types/known/anypb
|
||||
google.golang.org/protobuf/types/known/durationpb
|
||||
google.golang.org/protobuf/types/known/timestamppb
|
||||
# gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc
|
||||
## explicit
|
||||
gopkg.in/alexcesaro/quotedprintable.v3
|
||||
# gopkg.in/mail.v2 v2.3.1
|
||||
## explicit
|
||||
gopkg.in/mail.v2
|
||||
# gopkg.in/yaml.v2 v2.4.0
|
||||
## explicit; go 1.15
|
||||
gopkg.in/yaml.v2
|
||||
|
|
|
@ -1,11 +1,9 @@
|
|||
package watchdog
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"log"
|
||||
|
||||
"github.com/TwiN/gatus/v3/alerting"
|
||||
"github.com/TwiN/gatus/v3/alerting/alert"
|
||||
"github.com/TwiN/gatus/v3/core"
|
||||
)
|
||||
|
||||
|
@ -38,24 +36,7 @@ func handleAlertsToTrigger(endpoint *core.Endpoint, result *core.Result, alertin
|
|||
alertProvider := alertingConfig.GetAlertingProviderByAlertType(endpointAlert.Type)
|
||||
if alertProvider != nil && alertProvider.IsValid() {
|
||||
log.Printf("[watchdog][handleAlertsToTrigger] Sending %s alert because alert for endpoint=%s with description='%s' has been TRIGGERED", endpointAlert.Type, endpoint.Name, endpointAlert.GetDescription())
|
||||
customAlertProvider := alertProvider.ToCustomAlertProvider(endpoint, endpointAlert, result, false)
|
||||
// TODO: retry on error
|
||||
var err error
|
||||
// We need to extract the DedupKey from PagerDuty's response
|
||||
if endpointAlert.Type == alert.TypePagerDuty {
|
||||
var body []byte
|
||||
if body, err = customAlertProvider.Send(endpoint.Name, endpointAlert.GetDescription(), false); err == nil {
|
||||
var response pagerDutyResponse
|
||||
if err = json.Unmarshal(body, &response); err != nil {
|
||||
log.Printf("[watchdog][handleAlertsToTrigger] Ran into error unmarshaling pagerduty response: %s", err.Error())
|
||||
} else {
|
||||
endpointAlert.ResolveKey = response.DedupKey
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// All other alert types don't need to extract anything from the body, so we can just send the request right away
|
||||
_, err = customAlertProvider.Send(endpoint.Name, endpointAlert.GetDescription(), false)
|
||||
}
|
||||
err := alertProvider.Send(endpoint, endpointAlert, result, false)
|
||||
if err != nil {
|
||||
log.Printf("[watchdog][handleAlertsToTrigger] Failed to send an alert for endpoint=%s: %s", endpoint.Name, err.Error())
|
||||
} else {
|
||||
|
@ -82,15 +63,9 @@ func handleAlertsToResolve(endpoint *core.Endpoint, result *core.Result, alertin
|
|||
alertProvider := alertingConfig.GetAlertingProviderByAlertType(endpointAlert.Type)
|
||||
if alertProvider != nil && alertProvider.IsValid() {
|
||||
log.Printf("[watchdog][handleAlertsToResolve] Sending %s alert because alert for endpoint=%s with description='%s' has been RESOLVED", endpointAlert.Type, endpoint.Name, endpointAlert.GetDescription())
|
||||
customAlertProvider := alertProvider.ToCustomAlertProvider(endpoint, endpointAlert, result, true)
|
||||
// TODO: retry on error
|
||||
_, err := customAlertProvider.Send(endpoint.Name, endpointAlert.GetDescription(), true)
|
||||
err := alertProvider.Send(endpoint, endpointAlert, result, true)
|
||||
if err != nil {
|
||||
log.Printf("[watchdog][handleAlertsToResolve] Failed to send an alert for endpoint=%s: %s", endpoint.Name, err.Error())
|
||||
} else {
|
||||
if endpointAlert.Type == alert.TypePagerDuty {
|
||||
endpointAlert.ResolveKey = ""
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log.Printf("[watchdog][handleAlertsToResolve] Not sending alert of type=%s despite being RESOLVED, because the provider wasn't configured properly", endpointAlert.Type)
|
||||
|
@ -98,9 +73,3 @@ func handleAlertsToResolve(endpoint *core.Endpoint, result *core.Result, alertin
|
|||
}
|
||||
endpoint.NumberOfFailuresInARow = 0
|
||||
}
|
||||
|
||||
type pagerDutyResponse struct {
|
||||
Status string `json:"status"`
|
||||
Message string `json:"message"`
|
||||
DedupKey string `json:"dedup_key"`
|
||||
}
|
||||
|
|
|
@ -74,6 +74,7 @@ func execute(endpoint *core.Endpoint, alertingConfig *alerting.Config, maintenan
|
|||
result.Duration.Round(time.Millisecond),
|
||||
)
|
||||
if !maintenanceConfig.IsUnderMaintenance() {
|
||||
// TODO: Consider moving this after the monitoring lock is unlocked? I mean, how much noise can a single alerting provider cause...
|
||||
HandleAlerting(endpoint, result, alertingConfig, debug)
|
||||
} else if debug {
|
||||
log.Println("[watchdog][execute] Not handling alerting because currently in the maintenance window")
|
||||
|
|
Loading…
Reference in a new issue