Fix #117: Implement email alerts
This commit is contained in:
		
							
								
								
									
										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) |   - [Client configuration](#client-configuration) | ||||||
|   - [Alerting](#alerting) |   - [Alerting](#alerting) | ||||||
|     - [Configuring Discord alerts](#configuring-discord-alerts) |     - [Configuring Discord alerts](#configuring-discord-alerts) | ||||||
|  |     - [Configuring Email alerts](#configuring-email-alerts) | ||||||
|     - [Configuring Mattermost alerts](#configuring-mattermost-alerts) |     - [Configuring Mattermost alerts](#configuring-mattermost-alerts) | ||||||
|     - [Configuring Messagebird alerts](#configuring-messagebird-alerts) |     - [Configuring Messagebird alerts](#configuring-messagebird-alerts) | ||||||
|     - [Configuring PagerDuty alerts](#configuring-pagerduty-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 | #### Configuring Mattermost alerts | ||||||
| | Parameter                           | Description                                                                                 | Default        | | | Parameter                           | Description                                                                                 | Default        | | ||||||
| |:----------------------------------- |:------------------------------------------------------------------------------------------- |:-------------- | | |:----------------------------------- |:------------------------------------------------------------------------------------------- |:-------------- | | ||||||
|  | |||||||
| @ -11,6 +11,9 @@ const ( | |||||||
| 	// TypeDiscord is the Type for the discord alerting provider | 	// TypeDiscord is the Type for the discord alerting provider | ||||||
| 	TypeDiscord Type = "discord" | 	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 is the Type for the mattermost alerting provider | ||||||
| 	TypeMattermost Type = "mattermost" | 	TypeMattermost Type = "mattermost" | ||||||
|  |  | ||||||
|  | |||||||
| @ -5,6 +5,7 @@ import ( | |||||||
| 	"github.com/TwiN/gatus/v3/alerting/provider" | 	"github.com/TwiN/gatus/v3/alerting/provider" | ||||||
| 	"github.com/TwiN/gatus/v3/alerting/provider/custom" | 	"github.com/TwiN/gatus/v3/alerting/provider/custom" | ||||||
| 	"github.com/TwiN/gatus/v3/alerting/provider/discord" | 	"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/mattermost" | ||||||
| 	"github.com/TwiN/gatus/v3/alerting/provider/messagebird" | 	"github.com/TwiN/gatus/v3/alerting/provider/messagebird" | ||||||
| 	"github.com/TwiN/gatus/v3/alerting/provider/pagerduty" | 	"github.com/TwiN/gatus/v3/alerting/provider/pagerduty" | ||||||
| @ -17,31 +18,34 @@ import ( | |||||||
| // Config is the configuration for alerting providers | // Config is the configuration for alerting providers | ||||||
| type Config struct { | type Config struct { | ||||||
| 	// Custom is the configuration for the custom alerting provider | 	// 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 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 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 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 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 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 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 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 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 | // 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 nil | ||||||
| 		} | 		} | ||||||
| 		return config.Discord | 		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: | 	case alert.TypeMattermost: | ||||||
| 		if config.Mattermost == nil { | 		if config.Mattermost == nil { | ||||||
| 			// Since we're returning an interface, we need to explicitly return nil, even if the provider itself is 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 | 	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 | // GetAlertStatePlaceholderValue returns the Placeholder value for ALERT_TRIGGERED_OR_RESOLVED if configured | ||||||
| func (provider *AlertProvider) GetAlertStatePlaceholderValue(resolved bool) string { | func (provider *AlertProvider) GetAlertStatePlaceholderValue(resolved bool) string { | ||||||
| 	status := "TRIGGERED" | 	status := "TRIGGERED" | ||||||
| @ -105,27 +100,26 @@ func (provider *AlertProvider) buildHTTPRequest(endpointName, alertDescription s | |||||||
| 	return request | 	return request | ||||||
| } | } | ||||||
|  |  | ||||||
| // Send a request to the alert provider and return the body | func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) error { | ||||||
| func (provider *AlertProvider) Send(endpointName, alertDescription string, resolved bool) ([]byte, error) { |  | ||||||
| 	if os.Getenv("MOCK_ALERT_PROVIDER") == "true" { | 	if os.Getenv("MOCK_ALERT_PROVIDER") == "true" { | ||||||
| 		if os.Getenv("MOCK_ALERT_PROVIDER_ERROR") == "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) | 	response, err := client.GetHTTPClient(provider.ClientConfig).Do(request) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return nil, err | 		return err | ||||||
| 	} | 	} | ||||||
| 	if response.StatusCode > 399 { | 	if response.StatusCode > 399 { | ||||||
| 		body, err := ioutil.ReadAll(response.Body) | 		body, err := ioutil.ReadAll(response.Body) | ||||||
| 		if err != nil { | 		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 | // GetDefaultAlert returns the provider's default alert configuration | ||||||
|  | |||||||
| @ -3,9 +3,6 @@ package custom | |||||||
| import ( | import ( | ||||||
| 	"io/ioutil" | 	"io/ioutil" | ||||||
| 	"testing" | 	"testing" | ||||||
|  |  | ||||||
| 	"github.com/TwiN/gatus/v3/alerting/alert" |  | ||||||
| 	"github.com/TwiN/gatus/v3/core" |  | ||||||
| ) | ) | ||||||
|  |  | ||||||
| func TestAlertProvider_IsValid(t *testing.T) { | 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) { | func TestAlertProvider_buildHTTPRequestWithCustomPlaceholder(t *testing.T) { | ||||||
| 	const ( | 	const ( | ||||||
| 		ExpectedURL  = "https://example.com/endpoint-name?event=test&description=alert-description" | 		ExpectedURL  = "https://example.com/endpoint-name?event=test&description=alert-description" | ||||||
|  | |||||||
| @ -1,11 +1,15 @@ | |||||||
| package discord | package discord | ||||||
|  |  | ||||||
| import ( | import ( | ||||||
|  | 	"bytes" | ||||||
|  | 	"errors" | ||||||
| 	"fmt" | 	"fmt" | ||||||
|  | 	"io/ioutil" | ||||||
| 	"net/http" | 	"net/http" | ||||||
|  | 	"os" | ||||||
|  |  | ||||||
| 	"github.com/TwiN/gatus/v3/alerting/alert" | 	"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" | 	"github.com/TwiN/gatus/v3/core" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| @ -22,8 +26,36 @@ func (provider *AlertProvider) IsValid() bool { | |||||||
| 	return len(provider.WebhookURL) > 0 | 	return len(provider.WebhookURL) > 0 | ||||||
| } | } | ||||||
|  |  | ||||||
| // ToCustomAlertProvider converts the provider into a custom.AlertProvider | // Send an alert using the provider | ||||||
| func (provider *AlertProvider) ToCustomAlertProvider(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) *custom.AlertProvider { | 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 message, results string | ||||||
| 	var colorCode int | 	var colorCode int | ||||||
| 	if resolved { | 	if resolved { | ||||||
| @ -46,10 +78,7 @@ func (provider *AlertProvider) ToCustomAlertProvider(endpoint *core.Endpoint, al | |||||||
| 	if alertDescription := alert.GetDescription(); len(alertDescription) > 0 { | 	if alertDescription := alert.GetDescription(); len(alertDescription) > 0 { | ||||||
| 		description = ":\\n> " + alertDescription | 		description = ":\\n> " + alertDescription | ||||||
| 	} | 	} | ||||||
| 	return &custom.AlertProvider{ | 	return fmt.Sprintf(`{ | ||||||
| 		URL:    provider.WebhookURL, |  | ||||||
| 		Method: http.MethodPost, |  | ||||||
| 		Body: fmt.Sprintf(`{ |  | ||||||
|   "content": "", |   "content": "", | ||||||
|   "embeds": [ |   "embeds": [ | ||||||
|     { |     { | ||||||
| @ -65,9 +94,7 @@ func (provider *AlertProvider) ToCustomAlertProvider(endpoint *core.Endpoint, al | |||||||
|       ] |       ] | ||||||
|     } |     } | ||||||
|   ] |   ] | ||||||
| }`, message, description, colorCode, results), | }`, message, description, colorCode, results) | ||||||
| 		Headers: map[string]string{"Content-Type": "application/json"}, |  | ||||||
| 	} |  | ||||||
| } | } | ||||||
|  |  | ||||||
| // GetDefaultAlert returns the provider's default alert configuration | // GetDefaultAlert returns the provider's default alert configuration | ||||||
|  | |||||||
| @ -2,8 +2,6 @@ package discord | |||||||
|  |  | ||||||
| import ( | import ( | ||||||
| 	"encoding/json" | 	"encoding/json" | ||||||
| 	"net/http" |  | ||||||
| 	"strings" |  | ||||||
| 	"testing" | 	"testing" | ||||||
|  |  | ||||||
| 	"github.com/TwiN/gatus/v3/alerting/alert" | 	"github.com/TwiN/gatus/v3/alerting/alert" | ||||||
| @ -21,50 +19,51 @@ func TestAlertProvider_IsValid(t *testing.T) { | |||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
| func TestAlertProvider_ToCustomAlertProviderWithResolvedAlert(t *testing.T) { | func TestAlertProvider_buildRequestBody(t *testing.T) { | ||||||
| 	provider := AlertProvider{WebhookURL: "http://example.com"} | 	firstDescription := "description-1" | ||||||
| 	alertDescription := "test" | 	secondDescription := "description-2" | ||||||
| 	customAlertProvider := provider.ToCustomAlertProvider(&core.Endpoint{Name: "svc"}, &alert.Alert{Description: &alertDescription}, &core.Result{ConditionResults: []*core.ConditionResult{{Condition: "SUCCESSFUL_CONDITION", Success: true}}}, true) | 	scenarios := []struct { | ||||||
| 	if customAlertProvider == nil { | 		Name         string | ||||||
| 		t.Fatal("customAlertProvider shouldn't have been nil") | 		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") { | 	for _, scenario := range scenarios { | ||||||
| 		t.Error("customAlertProvider.Body should've contained the substring resolved") | 		t.Run(scenario.Name, func(t *testing.T) { | ||||||
| 	} | 			body := scenario.Provider.buildRequestBody( | ||||||
| 	if customAlertProvider.URL != "http://example.com" { | 				&core.Endpoint{Name: "endpoint-name"}, | ||||||
| 		t.Errorf("expected URL to be %s, got %s", "http://example.com", customAlertProvider.URL) | 				&scenario.Alert, | ||||||
| 	} | 				&core.Result{ | ||||||
| 	if customAlertProvider.Method != http.MethodPost { | 					ConditionResults: []*core.ConditionResult{ | ||||||
| 		t.Errorf("expected method to be %s, got %s", http.MethodPost, customAlertProvider.Method) | 						{Condition: "[CONNECTED] == true", Success: scenario.Resolved}, | ||||||
| 	} | 						{Condition: "[STATUS] == 200", Success: scenario.Resolved}, | ||||||
| 	body := make(map[string]interface{}) | 					}, | ||||||
| 	err := json.Unmarshal([]byte(customAlertProvider.Body), &body) | 				}, | ||||||
| 	if err != nil { | 				scenario.Resolved, | ||||||
| 		t.Error("expected body to be valid JSON, got error:", err.Error()) | 			) | ||||||
| 	} | 			if body != scenario.ExpectedBody { | ||||||
| 	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 %s, got %s", scenario.ExpectedBody, body) | ||||||
| 		t.Errorf("expected $.embeds[0].description to be %s, got %s", expected, body["embeds"].([]interface{})[0].(map[string]interface{})["description"]) | 			} | ||||||
| 	} | 			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()) | ||||||
| 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()) |  | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  | |||||||
							
								
								
									
										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 | package mattermost | ||||||
|  |  | ||||||
| import ( | import ( | ||||||
|  | 	"bytes" | ||||||
|  | 	"errors" | ||||||
| 	"fmt" | 	"fmt" | ||||||
|  | 	"io/ioutil" | ||||||
| 	"net/http" | 	"net/http" | ||||||
|  | 	"os" | ||||||
|  |  | ||||||
| 	"github.com/TwiN/gatus/v3/alerting/alert" | 	"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/client" | ||||||
| 	"github.com/TwiN/gatus/v3/core" | 	"github.com/TwiN/gatus/v3/core" | ||||||
| ) | ) | ||||||
| @ -29,10 +32,37 @@ func (provider *AlertProvider) IsValid() bool { | |||||||
| 	return len(provider.WebhookURL) > 0 | 	return len(provider.WebhookURL) > 0 | ||||||
| } | } | ||||||
|  |  | ||||||
| // ToCustomAlertProvider converts the provider into a custom.AlertProvider | // Send an alert using the provider | ||||||
| func (provider *AlertProvider) ToCustomAlertProvider(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) *custom.AlertProvider { | func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) error { | ||||||
| 	var message string | 	if os.Getenv("MOCK_ALERT_PROVIDER") == "true" { | ||||||
| 	var color string | 		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 { | 	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) | 		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" | 		color = "#36A64F" | ||||||
| @ -54,11 +84,7 @@ func (provider *AlertProvider) ToCustomAlertProvider(endpoint *core.Endpoint, al | |||||||
| 	if alertDescription := alert.GetDescription(); len(alertDescription) > 0 { | 	if alertDescription := alert.GetDescription(); len(alertDescription) > 0 { | ||||||
| 		description = ":\\n> " + alertDescription | 		description = ":\\n> " + alertDescription | ||||||
| 	} | 	} | ||||||
| 	return &custom.AlertProvider{ | 	return fmt.Sprintf(`{ | ||||||
| 		URL:          provider.WebhookURL, |  | ||||||
| 		Method:       http.MethodPost, |  | ||||||
| 		ClientConfig: provider.ClientConfig, |  | ||||||
| 		Body: fmt.Sprintf(`{ |  | ||||||
|   "text": "", |   "text": "", | ||||||
|   "username": "gatus", |   "username": "gatus", | ||||||
|   "icon_url": "https://raw.githubusercontent.com/TwiN/gatus/master/.github/assets/logo.png", |   "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), | }`, message, message, description, color, endpoint.URL, results) | ||||||
| 		Headers: map[string]string{"Content-Type": "application/json"}, |  | ||||||
| 	} |  | ||||||
| } | } | ||||||
|  |  | ||||||
| // GetDefaultAlert returns the provider's default alert configuration | // GetDefaultAlert returns the provider's default alert configuration | ||||||
|  | |||||||
| @ -2,8 +2,6 @@ package mattermost | |||||||
|  |  | ||||||
| import ( | import ( | ||||||
| 	"encoding/json" | 	"encoding/json" | ||||||
| 	"net/http" |  | ||||||
| 	"strings" |  | ||||||
| 	"testing" | 	"testing" | ||||||
|  |  | ||||||
| 	"github.com/TwiN/gatus/v3/alerting/alert" | 	"github.com/TwiN/gatus/v3/alerting/alert" | ||||||
| @ -21,50 +19,51 @@ func TestAlertProvider_IsValid(t *testing.T) { | |||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
| func TestAlertProvider_ToCustomAlertProviderWithResolvedAlert(t *testing.T) { | func TestAlertProvider_buildRequestBody(t *testing.T) { | ||||||
| 	provider := AlertProvider{WebhookURL: "http://example.org"} | 	firstDescription := "description-1" | ||||||
| 	alertDescription := "test" | 	secondDescription := "description-2" | ||||||
| 	customAlertProvider := provider.ToCustomAlertProvider(&core.Endpoint{Name: "svc"}, &alert.Alert{Description: &alertDescription}, &core.Result{ConditionResults: []*core.ConditionResult{{Condition: "SUCCESSFUL_CONDITION", Success: true}}}, true) | 	scenarios := []struct { | ||||||
| 	if customAlertProvider == nil { | 		Name         string | ||||||
| 		t.Fatal("customAlertProvider shouldn't have been nil") | 		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") { | 	for _, scenario := range scenarios { | ||||||
| 		t.Error("customAlertProvider.Body should've contained the substring resolved") | 		t.Run(scenario.Name, func(t *testing.T) { | ||||||
| 	} | 			body := scenario.Provider.buildRequestBody( | ||||||
| 	if customAlertProvider.URL != "http://example.org" { | 				&core.Endpoint{Name: "endpoint-name"}, | ||||||
| 		t.Errorf("expected URL to be %s, got %s", "http://example.org", customAlertProvider.URL) | 				&scenario.Alert, | ||||||
| 	} | 				&core.Result{ | ||||||
| 	if customAlertProvider.Method != http.MethodPost { | 					ConditionResults: []*core.ConditionResult{ | ||||||
| 		t.Errorf("expected method to be %s, got %s", http.MethodPost, customAlertProvider.Method) | 						{Condition: "[CONNECTED] == true", Success: scenario.Resolved}, | ||||||
| 	} | 						{Condition: "[STATUS] == 200", Success: scenario.Resolved}, | ||||||
| 	body := make(map[string]interface{}) | 					}, | ||||||
| 	err := json.Unmarshal([]byte(customAlertProvider.Body), &body) | 				}, | ||||||
| 	if err != nil { | 				scenario.Resolved, | ||||||
| 		t.Error("expected body to be valid JSON, got error:", err.Error()) | 			) | ||||||
| 	} | 			if body != scenario.ExpectedBody { | ||||||
| 	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 %s, got %s", scenario.ExpectedBody, body) | ||||||
| 		t.Errorf("expected $.attachments[0].description to be %s, got %s", expected, body["attachments"].([]interface{})[0].(map[string]interface{})["text"]) | 			} | ||||||
| 	} | 			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()) | ||||||
| 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()) |  | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  | |||||||
| @ -1,11 +1,15 @@ | |||||||
| package messagebird | package messagebird | ||||||
|  |  | ||||||
| import ( | import ( | ||||||
|  | 	"bytes" | ||||||
|  | 	"errors" | ||||||
| 	"fmt" | 	"fmt" | ||||||
|  | 	"io/ioutil" | ||||||
| 	"net/http" | 	"net/http" | ||||||
|  | 	"os" | ||||||
|  |  | ||||||
| 	"github.com/TwiN/gatus/v3/alerting/alert" | 	"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" | 	"github.com/TwiN/gatus/v3/core" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| @ -20,7 +24,7 @@ type AlertProvider struct { | |||||||
| 	Recipients string `yaml:"recipients"` | 	Recipients string `yaml:"recipients"` | ||||||
|  |  | ||||||
| 	// DefaultAlert is the default alert configuration to use for endpoints with an alert of the appropriate type | 	// 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 | // 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 | 	return len(provider.AccessKey) > 0 && len(provider.Originator) > 0 && len(provider.Recipients) > 0 | ||||||
| } | } | ||||||
|  |  | ||||||
| // ToCustomAlertProvider converts the provider into a custom.AlertProvider | // Send an alert using the provider | ||||||
| // Reference doc for messagebird https://developers.messagebird.com/api/sms-messaging/#send-outbound-sms | // 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 { | 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 | 	var message string | ||||||
| 	if resolved { | 	if resolved { | ||||||
| 		message = fmt.Sprintf("RESOLVED: %s - %s", endpoint.Name, alert.GetDescription()) | 		message = fmt.Sprintf("RESOLVED: %s - %s", endpoint.Name, alert.GetDescription()) | ||||||
| 	} else { | 	} else { | ||||||
| 		message = fmt.Sprintf("TRIGGERED: %s - %s", endpoint.Name, alert.GetDescription()) | 		message = fmt.Sprintf("TRIGGERED: %s - %s", endpoint.Name, alert.GetDescription()) | ||||||
| 	} | 	} | ||||||
| 	return &custom.AlertProvider{ | 	return fmt.Sprintf(`{ | ||||||
| 		URL:    restAPIURL, |  | ||||||
| 		Method: http.MethodPost, |  | ||||||
| 		Body: fmt.Sprintf(`{ |  | ||||||
|   "originator": "%s", |   "originator": "%s", | ||||||
|   "recipients": "%s", |   "recipients": "%s", | ||||||
|   "body": "%s" |   "body": "%s" | ||||||
| }`, provider.Originator, provider.Recipients, message), | }`, provider.Originator, provider.Recipients, message) | ||||||
| 		Headers: map[string]string{ |  | ||||||
| 			"Content-Type":  "application/json", |  | ||||||
| 			"Authorization": fmt.Sprintf("AccessKey %s", provider.AccessKey), |  | ||||||
| 		}, |  | ||||||
| 	} |  | ||||||
| } | } | ||||||
|  |  | ||||||
| // GetDefaultAlert returns the provider's default alert configuration | // GetDefaultAlert returns the provider's default alert configuration | ||||||
|  | |||||||
| @ -2,8 +2,6 @@ package messagebird | |||||||
|  |  | ||||||
| import ( | import ( | ||||||
| 	"encoding/json" | 	"encoding/json" | ||||||
| 	"net/http" |  | ||||||
| 	"strings" |  | ||||||
| 	"testing" | 	"testing" | ||||||
|  |  | ||||||
| 	"github.com/TwiN/gatus/v3/alerting/alert" | 	"github.com/TwiN/gatus/v3/alerting/alert" | ||||||
| @ -25,54 +23,51 @@ func TestMessagebirdAlertProvider_IsValid(t *testing.T) { | |||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
| func TestAlertProvider_ToCustomAlertProviderWithResolvedAlert(t *testing.T) { | func TestAlertProvider_buildRequestBody(t *testing.T) { | ||||||
| 	provider := AlertProvider{ | 	firstDescription := "description-1" | ||||||
| 		AccessKey:  "1", | 	secondDescription := "description-2" | ||||||
| 		Originator: "1", | 	scenarios := []struct { | ||||||
| 		Recipients: "1", | 		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) | 	for _, scenario := range scenarios { | ||||||
| 	if customAlertProvider == nil { | 		t.Run(scenario.Name, func(t *testing.T) { | ||||||
| 		t.Fatal("customAlertProvider shouldn't have been nil") | 			body := scenario.Provider.buildRequestBody( | ||||||
| 	} | 				&core.Endpoint{Name: "endpoint-name"}, | ||||||
| 	if !strings.Contains(customAlertProvider.Body, "RESOLVED") { | 				&scenario.Alert, | ||||||
| 		t.Error("customAlertProvider.Body should've contained the substring RESOLVED") | 				&core.Result{ | ||||||
| 	} | 					ConditionResults: []*core.ConditionResult{ | ||||||
| 	if customAlertProvider.URL != "https://rest.messagebird.com/messages" { | 						{Condition: "[CONNECTED] == true", Success: scenario.Resolved}, | ||||||
| 		t.Errorf("expected URL to be %s, got %s", "https://rest.messagebird.com/messages", customAlertProvider.URL) | 						{Condition: "[STATUS] == 200", Success: scenario.Resolved}, | ||||||
| 	} | 					}, | ||||||
| 	if customAlertProvider.Method != http.MethodPost { | 				}, | ||||||
| 		t.Errorf("expected method to be %s, got %s", http.MethodPost, customAlertProvider.Method) | 				scenario.Resolved, | ||||||
| 	} | 			) | ||||||
| 	body := make(map[string]interface{}) | 			if body != scenario.ExpectedBody { | ||||||
| 	err := json.Unmarshal([]byte(customAlertProvider.Body), &body) | 				t.Errorf("expected %s, got %s", scenario.ExpectedBody, body) | ||||||
| 	if err != nil { | 			} | ||||||
| 		t.Error("expected body to be valid JSON, got error:", err.Error()) | 			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()) | ||||||
|  | 			} | ||||||
| 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()) |  | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  | |||||||
| @ -1,11 +1,17 @@ | |||||||
| package pagerduty | package pagerduty | ||||||
|  |  | ||||||
| import ( | import ( | ||||||
|  | 	"bytes" | ||||||
|  | 	"encoding/json" | ||||||
|  | 	"errors" | ||||||
| 	"fmt" | 	"fmt" | ||||||
|  | 	"io/ioutil" | ||||||
|  | 	"log" | ||||||
| 	"net/http" | 	"net/http" | ||||||
|  | 	"os" | ||||||
|  |  | ||||||
| 	"github.com/TwiN/gatus/v3/alerting/alert" | 	"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" | 	"github.com/TwiN/gatus/v3/core" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| @ -18,10 +24,10 @@ type AlertProvider struct { | |||||||
| 	IntegrationKey string `yaml:"integration-key"` | 	IntegrationKey string `yaml:"integration-key"` | ||||||
|  |  | ||||||
| 	// DefaultAlert is the default alert configuration to use for endpoints with an alert of the appropriate type | 	// 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 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 | // 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 | 	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/ | // 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 { | 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 | 	var message, eventAction, resolveKey string | ||||||
| 	if resolved { | 	if resolved { | ||||||
| 		message = fmt.Sprintf("RESOLVED: %s - %s", endpoint.Name, alert.GetDescription()) | 		message = fmt.Sprintf("RESOLVED: %s - %s", endpoint.Name, alert.GetDescription()) | ||||||
| @ -59,10 +110,7 @@ func (provider *AlertProvider) ToCustomAlertProvider(endpoint *core.Endpoint, al | |||||||
| 		eventAction = "trigger" | 		eventAction = "trigger" | ||||||
| 		resolveKey = "" | 		resolveKey = "" | ||||||
| 	} | 	} | ||||||
| 	return &custom.AlertProvider{ | 	return fmt.Sprintf(`{ | ||||||
| 		URL:    restAPIURL, |  | ||||||
| 		Method: http.MethodPost, |  | ||||||
| 		Body: fmt.Sprintf(`{ |  | ||||||
|   "routing_key": "%s", |   "routing_key": "%s", | ||||||
|   "dedup_key": "%s", |   "dedup_key": "%s", | ||||||
|   "event_action": "%s", |   "event_action": "%s", | ||||||
| @ -71,11 +119,7 @@ func (provider *AlertProvider) ToCustomAlertProvider(endpoint *core.Endpoint, al | |||||||
|     "source": "%s", |     "source": "%s", | ||||||
|     "severity": "critical" |     "severity": "critical" | ||||||
|   } |   } | ||||||
| }`, provider.getIntegrationKeyForGroup(endpoint.Group), resolveKey, eventAction, message, endpoint.Name), | }`, provider.getIntegrationKeyForGroup(endpoint.Group), resolveKey, eventAction, message, endpoint.Name) | ||||||
| 		Headers: map[string]string{ |  | ||||||
| 			"Content-Type": "application/json", |  | ||||||
| 		}, |  | ||||||
| 	} |  | ||||||
| } | } | ||||||
|  |  | ||||||
| // getIntegrationKeyForGroup returns the appropriate pagerduty integration key for a given group | // 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 { | func (provider AlertProvider) GetDefaultAlert() *alert.Alert { | ||||||
| 	return provider.DefaultAlert | 	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 ( | import ( | ||||||
| 	"encoding/json" | 	"encoding/json" | ||||||
| 	"net/http" |  | ||||||
| 	"strings" |  | ||||||
| 	"testing" | 	"testing" | ||||||
|  |  | ||||||
| 	"github.com/TwiN/gatus/v3/alerting/alert" | 	"github.com/TwiN/gatus/v3/alerting/alert" | ||||||
| @ -57,107 +55,41 @@ func TestAlertProvider_IsValidWithOverride(t *testing.T) { | |||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
| func TestAlertProvider_ToCustomAlertProviderWithResolvedAlert(t *testing.T) { | func TestAlertProvider_buildRequestBody(t *testing.T) { | ||||||
| 	provider := AlertProvider{IntegrationKey: "00000000000000000000000000000000"} | 	description := "test" | ||||||
| 	customAlertProvider := provider.ToCustomAlertProvider(&core.Endpoint{}, &alert.Alert{}, &core.Result{}, true) | 	scenarios := []struct { | ||||||
| 	if customAlertProvider == nil { | 		Name         string | ||||||
| 		t.Fatal("customAlertProvider shouldn't have been nil") | 		Provider     AlertProvider | ||||||
| 	} | 		Alert        alert.Alert | ||||||
| 	if !strings.Contains(customAlertProvider.Body, "RESOLVED") { | 		Resolved     bool | ||||||
| 		t.Error("customAlertProvider.Body should've contained the substring RESOLVED") | 		ExpectedBody string | ||||||
| 	} | 	}{ | ||||||
| 	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) | 			Name:         "triggered", | ||||||
| 	} | 			Provider:     AlertProvider{IntegrationKey: "00000000000000000000000000000000"}, | ||||||
| 	if customAlertProvider.Method != http.MethodPost { | 			Alert:        alert.Alert{Description: &description}, | ||||||
| 		t.Errorf("expected method to be %s, got %s", http.MethodPost, customAlertProvider.Method) | 			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}", | ||||||
| 	body := make(map[string]interface{}) | 		}, | ||||||
| 	err := json.Unmarshal([]byte(customAlertProvider.Body), &body) | 		{ | ||||||
| 	if err != nil { | 			Name:         "resolved", | ||||||
| 		t.Error("expected body to be valid JSON, got error:", err.Error()) | 			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}", | ||||||
| func TestAlertProvider_ToCustomAlertProviderWithResolvedAlertAndOverride(t *testing.T) { |  | ||||||
| 	provider := AlertProvider{ |  | ||||||
| 		IntegrationKey: "", |  | ||||||
| 		Overrides: []Override{ |  | ||||||
| 			{ |  | ||||||
| 				IntegrationKey: "00000000000000000000000000000000", |  | ||||||
| 				Group:          "group", |  | ||||||
| 			}, |  | ||||||
| 		}, | 		}, | ||||||
| 	} | 	} | ||||||
| 	customAlertProvider := provider.ToCustomAlertProvider(&core.Endpoint{}, &alert.Alert{}, &core.Result{}, true) | 	for _, scenario := range scenarios { | ||||||
| 	if customAlertProvider == nil { | 		t.Run(scenario.Name, func(t *testing.T) { | ||||||
| 		t.Fatal("customAlertProvider shouldn't have been nil") | 			body := scenario.Provider.buildRequestBody(&core.Endpoint{}, &scenario.Alert, &core.Result{}, scenario.Resolved) | ||||||
| 	} | 			if body != scenario.ExpectedBody { | ||||||
| 	if !strings.Contains(customAlertProvider.Body, "RESOLVED") { | 				t.Errorf("expected %s, got %s", scenario.ExpectedBody, body) | ||||||
| 		t.Error("customAlertProvider.Body should've contained the substring RESOLVED") | 			} | ||||||
| 	} | 			out := make(map[string]interface{}) | ||||||
| 	if customAlertProvider.URL != "https://events.pagerduty.com/v2/enqueue" { | 			if err := json.Unmarshal([]byte(body), &out); err != nil { | ||||||
| 		t.Errorf("expected URL to be %s, got %s", "https://events.pagerduty.com/v2/enqueue", customAlertProvider.URL) | 				t.Error("expected body to be valid JSON, got error:", err.Error()) | ||||||
| 	} | 			} | ||||||
| 	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()) |  | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
|  | |||||||
| @ -4,6 +4,7 @@ import ( | |||||||
| 	"github.com/TwiN/gatus/v3/alerting/alert" | 	"github.com/TwiN/gatus/v3/alerting/alert" | ||||||
| 	"github.com/TwiN/gatus/v3/alerting/provider/custom" | 	"github.com/TwiN/gatus/v3/alerting/provider/custom" | ||||||
| 	"github.com/TwiN/gatus/v3/alerting/provider/discord" | 	"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/mattermost" | ||||||
| 	"github.com/TwiN/gatus/v3/alerting/provider/messagebird" | 	"github.com/TwiN/gatus/v3/alerting/provider/messagebird" | ||||||
| 	"github.com/TwiN/gatus/v3/alerting/provider/pagerduty" | 	"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 returns whether the provider's configuration is valid | ||||||
| 	IsValid() bool | 	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 returns the provider's default alert configuration | ||||||
| 	GetDefaultAlert() *alert.Alert | 	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 | // 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 | 	// Validate interface implementation on compile | ||||||
| 	_ AlertProvider = (*custom.AlertProvider)(nil) | 	_ AlertProvider = (*custom.AlertProvider)(nil) | ||||||
| 	_ AlertProvider = (*discord.AlertProvider)(nil) | 	_ AlertProvider = (*discord.AlertProvider)(nil) | ||||||
|  | 	_ AlertProvider = (*email.AlertProvider)(nil) | ||||||
| 	_ AlertProvider = (*mattermost.AlertProvider)(nil) | 	_ AlertProvider = (*mattermost.AlertProvider)(nil) | ||||||
| 	_ AlertProvider = (*messagebird.AlertProvider)(nil) | 	_ AlertProvider = (*messagebird.AlertProvider)(nil) | ||||||
| 	_ AlertProvider = (*pagerduty.AlertProvider)(nil) | 	_ AlertProvider = (*pagerduty.AlertProvider)(nil) | ||||||
|  | |||||||
| @ -1,11 +1,15 @@ | |||||||
| package slack | package slack | ||||||
|  |  | ||||||
| import ( | import ( | ||||||
|  | 	"bytes" | ||||||
|  | 	"errors" | ||||||
| 	"fmt" | 	"fmt" | ||||||
|  | 	"io/ioutil" | ||||||
| 	"net/http" | 	"net/http" | ||||||
|  | 	"os" | ||||||
|  |  | ||||||
| 	"github.com/TwiN/gatus/v3/alerting/alert" | 	"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" | 	"github.com/TwiN/gatus/v3/core" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| @ -22,8 +26,36 @@ func (provider *AlertProvider) IsValid() bool { | |||||||
| 	return len(provider.WebhookURL) > 0 | 	return len(provider.WebhookURL) > 0 | ||||||
| } | } | ||||||
|  |  | ||||||
| // ToCustomAlertProvider converts the provider into a custom.AlertProvider | // Send an alert using the provider | ||||||
| func (provider *AlertProvider) ToCustomAlertProvider(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) *custom.AlertProvider { | 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 | 	var message, color, results string | ||||||
| 	if resolved { | 	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) | 		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 { | 	if alertDescription := alert.GetDescription(); len(alertDescription) > 0 { | ||||||
| 		description = ":\\n> " + alertDescription | 		description = ":\\n> " + alertDescription | ||||||
| 	} | 	} | ||||||
| 	return &custom.AlertProvider{ | 	return fmt.Sprintf(`{ | ||||||
| 		URL:    provider.WebhookURL, |  | ||||||
| 		Method: http.MethodPost, |  | ||||||
| 		Body: fmt.Sprintf(`{ |  | ||||||
|   "text": "", |   "text": "", | ||||||
|   "attachments": [ |   "attachments": [ | ||||||
|     { |     { | ||||||
| @ -65,9 +94,7 @@ func (provider *AlertProvider) ToCustomAlertProvider(endpoint *core.Endpoint, al | |||||||
|       ] |       ] | ||||||
|     } |     } | ||||||
|   ] |   ] | ||||||
| }`, message, description, color, results), | }`, message, description, color, results) | ||||||
| 		Headers: map[string]string{"Content-Type": "application/json"}, |  | ||||||
| 	} |  | ||||||
| } | } | ||||||
|  |  | ||||||
| // GetDefaultAlert returns the provider's default alert configuration | // GetDefaultAlert returns the provider's default alert configuration | ||||||
|  | |||||||
| @ -2,8 +2,6 @@ package slack | |||||||
|  |  | ||||||
| import ( | import ( | ||||||
| 	"encoding/json" | 	"encoding/json" | ||||||
| 	"net/http" |  | ||||||
| 	"strings" |  | ||||||
| 	"testing" | 	"testing" | ||||||
|  |  | ||||||
| 	"github.com/TwiN/gatus/v3/alerting/alert" | 	"github.com/TwiN/gatus/v3/alerting/alert" | ||||||
| @ -21,50 +19,51 @@ func TestAlertProvider_IsValid(t *testing.T) { | |||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
| func TestAlertProvider_ToCustomAlertProviderWithResolvedAlert(t *testing.T) { | func TestAlertProvider_buildRequestBody(t *testing.T) { | ||||||
| 	provider := AlertProvider{WebhookURL: "http://example.com"} | 	firstDescription := "description-1" | ||||||
| 	alertDescription := "test" | 	secondDescription := "description-2" | ||||||
| 	customAlertProvider := provider.ToCustomAlertProvider(&core.Endpoint{Name: "svc"}, &alert.Alert{Description: &alertDescription}, &core.Result{ConditionResults: []*core.ConditionResult{{Condition: "SUCCESSFUL_CONDITION", Success: true}}}, true) | 	scenarios := []struct { | ||||||
| 	if customAlertProvider == nil { | 		Name         string | ||||||
| 		t.Fatal("customAlertProvider shouldn't have been nil") | 		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") { | 	for _, scenario := range scenarios { | ||||||
| 		t.Error("customAlertProvider.Body should've contained the substring resolved") | 		t.Run(scenario.Name, func(t *testing.T) { | ||||||
| 	} | 			body := scenario.Provider.buildRequestBody( | ||||||
| 	if customAlertProvider.URL != "http://example.com" { | 				&core.Endpoint{Name: "endpoint-name"}, | ||||||
| 		t.Errorf("expected URL to be %s, got %s", "http://example.com", customAlertProvider.URL) | 				&scenario.Alert, | ||||||
| 	} | 				&core.Result{ | ||||||
| 	if customAlertProvider.Method != http.MethodPost { | 					ConditionResults: []*core.ConditionResult{ | ||||||
| 		t.Errorf("expected method to be %s, got %s", http.MethodPost, customAlertProvider.Method) | 						{Condition: "[CONNECTED] == true", Success: scenario.Resolved}, | ||||||
| 	} | 						{Condition: "[STATUS] == 200", Success: scenario.Resolved}, | ||||||
| 	body := make(map[string]interface{}) | 					}, | ||||||
| 	err := json.Unmarshal([]byte(customAlertProvider.Body), &body) | 				}, | ||||||
| 	if err != nil { | 				scenario.Resolved, | ||||||
| 		t.Error("expected body to be valid JSON, got error:", err.Error()) | 			) | ||||||
| 	} | 			if body != scenario.ExpectedBody { | ||||||
| 	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 %s, got %s", scenario.ExpectedBody, body) | ||||||
| 		t.Errorf("expected $.attachments[0].description to be %s, got %s", expected, body["attachments"].([]interface{})[0].(map[string]interface{})["text"]) | 			} | ||||||
| 	} | 			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()) | ||||||
| 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()) |  | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  | |||||||
| @ -1,11 +1,15 @@ | |||||||
| package teams | package teams | ||||||
|  |  | ||||||
| import ( | import ( | ||||||
|  | 	"bytes" | ||||||
|  | 	"errors" | ||||||
| 	"fmt" | 	"fmt" | ||||||
|  | 	"io/ioutil" | ||||||
| 	"net/http" | 	"net/http" | ||||||
|  | 	"os" | ||||||
|  |  | ||||||
| 	"github.com/TwiN/gatus/v3/alerting/alert" | 	"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" | 	"github.com/TwiN/gatus/v3/core" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| @ -22,10 +26,37 @@ func (provider *AlertProvider) IsValid() bool { | |||||||
| 	return len(provider.WebhookURL) > 0 | 	return len(provider.WebhookURL) > 0 | ||||||
| } | } | ||||||
|  |  | ||||||
| // ToCustomAlertProvider converts the provider into a custom.AlertProvider | // Send an alert using the provider | ||||||
| func (provider *AlertProvider) ToCustomAlertProvider(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) *custom.AlertProvider { | func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) error { | ||||||
| 	var message string | 	if os.Getenv("MOCK_ALERT_PROVIDER") == "true" { | ||||||
| 	var color string | 		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 { | 	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) | 		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" | 		color = "#36A64F" | ||||||
| @ -47,10 +78,7 @@ func (provider *AlertProvider) ToCustomAlertProvider(endpoint *core.Endpoint, al | |||||||
| 	if alertDescription := alert.GetDescription(); len(alertDescription) > 0 { | 	if alertDescription := alert.GetDescription(); len(alertDescription) > 0 { | ||||||
| 		description = ":\\n> " + alertDescription | 		description = ":\\n> " + alertDescription | ||||||
| 	} | 	} | ||||||
| 	return &custom.AlertProvider{ | 	return fmt.Sprintf(`{ | ||||||
| 		URL:    provider.WebhookURL, |  | ||||||
| 		Method: http.MethodPost, |  | ||||||
| 		Body: fmt.Sprintf(`{ |  | ||||||
|   "@type": "MessageCard", |   "@type": "MessageCard", | ||||||
|   "@context": "http://schema.org/extensions", |   "@context": "http://schema.org/extensions", | ||||||
|   "themeColor": "%s", |   "themeColor": "%s", | ||||||
| @ -66,9 +94,7 @@ func (provider *AlertProvider) ToCustomAlertProvider(endpoint *core.Endpoint, al | |||||||
|       "text": "%s" |       "text": "%s" | ||||||
|     } |     } | ||||||
|   ] |   ] | ||||||
| }`, color, message, description, endpoint.URL, results), | }`, color, message, description, endpoint.URL, results) | ||||||
| 		Headers: map[string]string{"Content-Type": "application/json"}, |  | ||||||
| 	} |  | ||||||
| } | } | ||||||
|  |  | ||||||
| // GetDefaultAlert returns the provider's default alert configuration | // GetDefaultAlert returns the provider's default alert configuration | ||||||
|  | |||||||
| @ -2,8 +2,6 @@ package teams | |||||||
|  |  | ||||||
| import ( | import ( | ||||||
| 	"encoding/json" | 	"encoding/json" | ||||||
| 	"net/http" |  | ||||||
| 	"strings" |  | ||||||
| 	"testing" | 	"testing" | ||||||
|  |  | ||||||
| 	"github.com/TwiN/gatus/v3/alerting/alert" | 	"github.com/TwiN/gatus/v3/alerting/alert" | ||||||
| @ -21,50 +19,51 @@ func TestAlertProvider_IsValid(t *testing.T) { | |||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
| func TestAlertProvider_ToCustomAlertProviderWithResolvedAlert(t *testing.T) { | func TestAlertProvider_buildRequestBody(t *testing.T) { | ||||||
| 	provider := AlertProvider{WebhookURL: "http://example.org"} | 	firstDescription := "description-1" | ||||||
| 	alertDescription := "test" | 	secondDescription := "description-2" | ||||||
| 	customAlertProvider := provider.ToCustomAlertProvider(&core.Endpoint{Name: "svc"}, &alert.Alert{Description: &alertDescription}, &core.Result{ConditionResults: []*core.ConditionResult{{Condition: "SUCCESSFUL_CONDITION", Success: true}}}, true) | 	scenarios := []struct { | ||||||
| 	if customAlertProvider == nil { | 		Name         string | ||||||
| 		t.Fatal("customAlertProvider shouldn't have been nil") | 		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") { | 	for _, scenario := range scenarios { | ||||||
| 		t.Error("customAlertProvider.Body should've contained the substring resolved") | 		t.Run(scenario.Name, func(t *testing.T) { | ||||||
| 	} | 			body := scenario.Provider.buildRequestBody( | ||||||
| 	if customAlertProvider.URL != "http://example.org" { | 				&core.Endpoint{Name: "endpoint-name"}, | ||||||
| 		t.Errorf("expected URL to be %s, got %s", "http://example.org", customAlertProvider.URL) | 				&scenario.Alert, | ||||||
| 	} | 				&core.Result{ | ||||||
| 	if customAlertProvider.Method != http.MethodPost { | 					ConditionResults: []*core.ConditionResult{ | ||||||
| 		t.Errorf("expected method to be %s, got %s", http.MethodPost, customAlertProvider.Method) | 						{Condition: "[CONNECTED] == true", Success: scenario.Resolved}, | ||||||
| 	} | 						{Condition: "[STATUS] == 200", Success: scenario.Resolved}, | ||||||
| 	body := make(map[string]interface{}) | 					}, | ||||||
| 	err := json.Unmarshal([]byte(customAlertProvider.Body), &body) | 				}, | ||||||
| 	if err != nil { | 				scenario.Resolved, | ||||||
| 		t.Error("expected body to be valid JSON, got error:", err.Error()) | 			) | ||||||
| 	} | 			if body != scenario.ExpectedBody { | ||||||
| 	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 %s, got %s", scenario.ExpectedBody, body) | ||||||
| 		t.Errorf("expected $.text to be %s, got %s", expected, body["text"]) | 			} | ||||||
| 	} | 			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()) | ||||||
| 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()) |  | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  | |||||||
| @ -1,11 +1,15 @@ | |||||||
| package telegram | package telegram | ||||||
|  |  | ||||||
| import ( | import ( | ||||||
|  | 	"bytes" | ||||||
|  | 	"errors" | ||||||
| 	"fmt" | 	"fmt" | ||||||
|  | 	"io/ioutil" | ||||||
| 	"net/http" | 	"net/http" | ||||||
|  | 	"os" | ||||||
|  |  | ||||||
| 	"github.com/TwiN/gatus/v3/alerting/alert" | 	"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" | 	"github.com/TwiN/gatus/v3/core" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| @ -23,8 +27,36 @@ func (provider *AlertProvider) IsValid() bool { | |||||||
| 	return len(provider.Token) > 0 && len(provider.ID) > 0 | 	return len(provider.Token) > 0 && len(provider.ID) > 0 | ||||||
| } | } | ||||||
|  |  | ||||||
| // ToCustomAlertProvider converts the provider into a custom.AlertProvider | // Send an alert using the provider | ||||||
| func (provider *AlertProvider) ToCustomAlertProvider(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) *custom.AlertProvider { | 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 | 	var message, results string | ||||||
| 	if resolved { | 	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) | 		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 { | 	} else { | ||||||
| 		text = fmt.Sprintf("⛑ *Gatus* \\n%s \\n*Condition results*\\n%s", message, results) | 		text = fmt.Sprintf("⛑ *Gatus* \\n%s \\n*Condition results*\\n%s", message, results) | ||||||
| 	} | 	} | ||||||
| 	return &custom.AlertProvider{ | 	return fmt.Sprintf(`{"chat_id": "%s", "text": "%s", "parse_mode": "MARKDOWN"}`, provider.ID, text) | ||||||
| 		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"}, |  | ||||||
| 	} |  | ||||||
| } | } | ||||||
|  |  | ||||||
| // GetDefaultAlert returns the provider's default alert configuration | // GetDefaultAlert returns the provider's default alert configuration | ||||||
|  | |||||||
| @ -2,9 +2,6 @@ package telegram | |||||||
|  |  | ||||||
| import ( | import ( | ||||||
| 	"encoding/json" | 	"encoding/json" | ||||||
| 	"fmt" |  | ||||||
| 	"net/http" |  | ||||||
| 	"strings" |  | ||||||
| 	"testing" | 	"testing" | ||||||
|  |  | ||||||
| 	"github.com/TwiN/gatus/v3/alerting/alert" | 	"github.com/TwiN/gatus/v3/alerting/alert" | ||||||
| @ -22,70 +19,51 @@ func TestAlertProvider_IsValid(t *testing.T) { | |||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
| func TestAlertProvider_ToCustomAlertProviderWithResolvedAlert(t *testing.T) { | func TestAlertProvider_buildRequestBody(t *testing.T) { | ||||||
| 	provider := AlertProvider{Token: "123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11", ID: "12345678"} | 	firstDescription := "description-1" | ||||||
| 	customAlertProvider := provider.ToCustomAlertProvider(&core.Endpoint{}, &alert.Alert{}, &core.Result{ConditionResults: []*core.ConditionResult{{Condition: "SUCCESSFUL_CONDITION", Success: true}}}, true) | 	secondDescription := "description-2" | ||||||
| 	if customAlertProvider == nil { | 	scenarios := []struct { | ||||||
| 		t.Fatal("customAlertProvider shouldn't have been nil") | 		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") { | 	for _, scenario := range scenarios { | ||||||
| 		t.Error("customAlertProvider.Body should've contained the substring resolved") | 		t.Run(scenario.Name, func(t *testing.T) { | ||||||
| 	} | 			body := scenario.Provider.buildRequestBody( | ||||||
| 	if customAlertProvider.URL != fmt.Sprintf("https://api.telegram.org/bot%s/sendMessage", provider.Token) { | 				&core.Endpoint{Name: "endpoint-name"}, | ||||||
| 		t.Errorf("expected URL to be %s, got %s", fmt.Sprintf("https://api.telegram.org/bot%s/sendMessage", provider.Token), customAlertProvider.URL) | 				&scenario.Alert, | ||||||
| 	} | 				&core.Result{ | ||||||
| 	if customAlertProvider.Method != http.MethodPost { | 					ConditionResults: []*core.ConditionResult{ | ||||||
| 		t.Errorf("expected method to be %s, got %s", http.MethodPost, customAlertProvider.Method) | 						{Condition: "[CONNECTED] == true", Success: scenario.Resolved}, | ||||||
| 	} | 						{Condition: "[STATUS] == 200", Success: scenario.Resolved}, | ||||||
| 	body := make(map[string]interface{}) | 					}, | ||||||
| 	err := json.Unmarshal([]byte(customAlertProvider.Body), &body) | 				}, | ||||||
| 	//_, err := json.Marshal(customAlertProvider.Body) | 				scenario.Resolved, | ||||||
| 	if err != nil { | 			) | ||||||
| 		t.Error("expected body to be valid JSON, got error:", err.Error()) | 			if body != scenario.ExpectedBody { | ||||||
| 	} | 				t.Errorf("expected %s, got %s", scenario.ExpectedBody, body) | ||||||
| } | 			} | ||||||
|  | 			out := make(map[string]interface{}) | ||||||
| func TestAlertProvider_ToCustomAlertProviderWithTriggeredAlert(t *testing.T) { | 			if err := json.Unmarshal([]byte(body), &out); err != nil { | ||||||
| 	provider := AlertProvider{Token: "123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11", ID: "0123456789"} | 				t.Error("expected body to be valid JSON, got error:", err.Error()) | ||||||
| 	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()) |  | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  | |||||||
| @ -1,13 +1,17 @@ | |||||||
| package twilio | package twilio | ||||||
|  |  | ||||||
| import ( | import ( | ||||||
|  | 	"bytes" | ||||||
| 	"encoding/base64" | 	"encoding/base64" | ||||||
|  | 	"errors" | ||||||
| 	"fmt" | 	"fmt" | ||||||
|  | 	"io/ioutil" | ||||||
| 	"net/http" | 	"net/http" | ||||||
| 	"net/url" | 	"net/url" | ||||||
|  | 	"os" | ||||||
|  |  | ||||||
| 	"github.com/TwiN/gatus/v3/alerting/alert" | 	"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" | 	"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 | 	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 | // Send an alert using the provider | ||||||
| func (provider *AlertProvider) ToCustomAlertProvider(endpoint *core.Endpoint, alert *alert.Alert, _ *core.Result, resolved bool) *custom.AlertProvider { | 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 | 	var message string | ||||||
| 	if resolved { | 	if resolved { | ||||||
| 		message = fmt.Sprintf("RESOLVED: %s - %s", endpoint.Name, alert.GetDescription()) | 		message = fmt.Sprintf("RESOLVED: %s - %s", endpoint.Name, alert.GetDescription()) | ||||||
| 	} else { | 	} else { | ||||||
| 		message = fmt.Sprintf("TRIGGERED: %s - %s", endpoint.Name, alert.GetDescription()) | 		message = fmt.Sprintf("TRIGGERED: %s - %s", endpoint.Name, alert.GetDescription()) | ||||||
| 	} | 	} | ||||||
| 	return &custom.AlertProvider{ | 	return url.Values{ | ||||||
| 		URL:    fmt.Sprintf("https://api.twilio.com/2010-04-01/Accounts/%s/Messages.json", provider.SID), | 		"To":   {provider.To}, | ||||||
| 		Method: http.MethodPost, | 		"From": {provider.From}, | ||||||
| 		Body: url.Values{ | 		"Body": {message}, | ||||||
| 			"To":   {provider.To}, | 	}.Encode() | ||||||
| 			"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)))), |  | ||||||
| 		}, |  | ||||||
| 	} |  | ||||||
| } | } | ||||||
|  |  | ||||||
| // GetDefaultAlert returns the provider's default alert configuration | // GetDefaultAlert returns the provider's default alert configuration | ||||||
|  | |||||||
| @ -1,8 +1,6 @@ | |||||||
| package twilio | package twilio | ||||||
|  |  | ||||||
| import ( | import ( | ||||||
| 	"net/http" |  | ||||||
| 	"strings" |  | ||||||
| 	"testing" | 	"testing" | ||||||
|  |  | ||||||
| 	"github.com/TwiN/gatus/v3/alerting/alert" | 	"github.com/TwiN/gatus/v3/alerting/alert" | ||||||
| @ -25,54 +23,47 @@ func TestTwilioAlertProvider_IsValid(t *testing.T) { | |||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
| func TestAlertProvider_ToCustomAlertProviderWithResolvedAlert(t *testing.T) { | func TestAlertProvider_buildRequestBody(t *testing.T) { | ||||||
| 	provider := AlertProvider{ | 	firstDescription := "description-1" | ||||||
| 		SID:   "1", | 	secondDescription := "description-2" | ||||||
| 		Token: "2", | 	scenarios := []struct { | ||||||
| 		From:  "3", | 		Name         string | ||||||
| 		To:    "4", | 		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" | 	for _, scenario := range scenarios { | ||||||
| 	customAlertProvider := provider.ToCustomAlertProvider(&core.Endpoint{Name: "endpoint-name"}, &alert.Alert{Description: &description}, &core.Result{}, true) | 		t.Run(scenario.Name, func(t *testing.T) { | ||||||
| 	if customAlertProvider == nil { | 			body := scenario.Provider.buildRequestBody( | ||||||
| 		t.Fatal("customAlertProvider shouldn't have been nil") | 				&core.Endpoint{Name: "endpoint-name"}, | ||||||
| 	} | 				&scenario.Alert, | ||||||
| 	if !strings.Contains(customAlertProvider.Body, "RESOLVED") { | 				&core.Result{ | ||||||
| 		t.Error("customAlertProvider.Body should've contained the substring RESOLVED") | 					ConditionResults: []*core.ConditionResult{ | ||||||
| 	} | 						{Condition: "[CONNECTED] == true", Success: scenario.Resolved}, | ||||||
| 	if customAlertProvider.URL != "https://api.twilio.com/2010-04-01/Accounts/1/Messages.json" { | 						{Condition: "[STATUS] == 200", Success: scenario.Resolved}, | ||||||
| 		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 { | 				scenario.Resolved, | ||||||
| 		t.Errorf("expected method to be %s, got %s", http.MethodPost, customAlertProvider.Method) | 			) | ||||||
| 	} | 			if body != scenario.ExpectedBody { | ||||||
| 	if customAlertProvider.Body != "Body=RESOLVED%3A+endpoint-name+-+alert-description&From=3&To=4" { | 				t.Errorf("expected %s, got %s", scenario.ExpectedBody, body) | ||||||
| 		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) |  | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  | |||||||
| @ -274,6 +274,7 @@ func validateAlertingConfig(alertingConfig *alerting.Config, endpoints []*core.E | |||||||
| 	alertTypes := []alert.Type{ | 	alertTypes := []alert.Type{ | ||||||
| 		alert.TypeCustom, | 		alert.TypeCustom, | ||||||
| 		alert.TypeDiscord, | 		alert.TypeDiscord, | ||||||
|  | 		alert.TypeEmail, | ||||||
| 		alert.TypeMattermost, | 		alert.TypeMattermost, | ||||||
| 		alert.TypeMessagebird, | 		alert.TypeMessagebird, | ||||||
| 		alert.TypePagerDuty, | 		alert.TypePagerDuty, | ||||||
|  | |||||||
| @ -39,7 +39,6 @@ type Config struct { | |||||||
| 	Every []string `yaml:"every"` | 	Every []string `yaml:"every"` | ||||||
|  |  | ||||||
| 	durationToStartFromMidnight time.Duration | 	durationToStartFromMidnight time.Duration | ||||||
| 	timeLocation                *time.Location |  | ||||||
| } | } | ||||||
|  |  | ||||||
| func GetDefaultConfig() *Config { | 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/tools v0.1.7 // indirect | ||||||
| 	golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect | 	golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect | ||||||
| 	google.golang.org/protobuf v1.27.1 // 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 | 	gopkg.in/yaml.v2 v2.4.0 | ||||||
| 	lukechampine.com/uint128 v1.1.1 // indirect | 	lukechampine.com/uint128 v1.1.1 // indirect | ||||||
| 	modernc.org/cc/v3 v3.35.8 // 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 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ= | ||||||
| google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= | 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/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 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-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 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= | ||||||
| gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= | ||||||
| gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= | 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/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/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.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= | ||||||
| gopkg.in/yaml.v2 v2.2.2/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 | ||||||
|  | [](https://travis-ci.org/go-mail/mail) [](http://gocover.io/github.com/go-mail/mail) [](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/anypb | ||||||
| google.golang.org/protobuf/types/known/durationpb | google.golang.org/protobuf/types/known/durationpb | ||||||
| google.golang.org/protobuf/types/known/timestamppb | 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 | # gopkg.in/yaml.v2 v2.4.0 | ||||||
| ## explicit; go 1.15 | ## explicit; go 1.15 | ||||||
| gopkg.in/yaml.v2 | gopkg.in/yaml.v2 | ||||||
|  | |||||||
| @ -1,11 +1,9 @@ | |||||||
| package watchdog | package watchdog | ||||||
|  |  | ||||||
| import ( | import ( | ||||||
| 	"encoding/json" |  | ||||||
| 	"log" | 	"log" | ||||||
|  |  | ||||||
| 	"github.com/TwiN/gatus/v3/alerting" | 	"github.com/TwiN/gatus/v3/alerting" | ||||||
| 	"github.com/TwiN/gatus/v3/alerting/alert" |  | ||||||
| 	"github.com/TwiN/gatus/v3/core" | 	"github.com/TwiN/gatus/v3/core" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| @ -38,24 +36,7 @@ func handleAlertsToTrigger(endpoint *core.Endpoint, result *core.Result, alertin | |||||||
| 		alertProvider := alertingConfig.GetAlertingProviderByAlertType(endpointAlert.Type) | 		alertProvider := alertingConfig.GetAlertingProviderByAlertType(endpointAlert.Type) | ||||||
| 		if alertProvider != nil && alertProvider.IsValid() { | 		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()) | 			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) | 			err := alertProvider.Send(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) |  | ||||||
| 			} |  | ||||||
| 			if err != nil { | 			if err != nil { | ||||||
| 				log.Printf("[watchdog][handleAlertsToTrigger] Failed to send an alert for endpoint=%s: %s", endpoint.Name, err.Error()) | 				log.Printf("[watchdog][handleAlertsToTrigger] Failed to send an alert for endpoint=%s: %s", endpoint.Name, err.Error()) | ||||||
| 			} else { | 			} else { | ||||||
| @ -82,15 +63,9 @@ func handleAlertsToResolve(endpoint *core.Endpoint, result *core.Result, alertin | |||||||
| 		alertProvider := alertingConfig.GetAlertingProviderByAlertType(endpointAlert.Type) | 		alertProvider := alertingConfig.GetAlertingProviderByAlertType(endpointAlert.Type) | ||||||
| 		if alertProvider != nil && alertProvider.IsValid() { | 		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()) | 			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) | 			err := alertProvider.Send(endpoint, endpointAlert, result, true) | ||||||
| 			// TODO: retry on error |  | ||||||
| 			_, err := customAlertProvider.Send(endpoint.Name, endpointAlert.GetDescription(), true) |  | ||||||
| 			if err != nil { | 			if err != nil { | ||||||
| 				log.Printf("[watchdog][handleAlertsToResolve] Failed to send an alert for endpoint=%s: %s", endpoint.Name, err.Error()) | 				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 { | 		} else { | ||||||
| 			log.Printf("[watchdog][handleAlertsToResolve] Not sending alert of type=%s despite being RESOLVED, because the provider wasn't configured properly", endpointAlert.Type) | 			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 | 	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), | 		result.Duration.Round(time.Millisecond), | ||||||
| 	) | 	) | ||||||
| 	if !maintenanceConfig.IsUnderMaintenance() { | 	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) | 		HandleAlerting(endpoint, result, alertingConfig, debug) | ||||||
| 	} else if debug { | 	} else if debug { | ||||||
| 		log.Println("[watchdog][execute] Not handling alerting because currently in the maintenance window") | 		log.Println("[watchdog][execute] Not handling alerting because currently in the maintenance window") | ||||||
|  | |||||||
		Reference in New Issue
	
	Block a user