mirror of
				https://github.com/axllent/mailpit.git
				synced 2025-10-31 00:07:43 +02:00 
			
		
		
		
	Feature: Set optional webhook for received messages (#195)
This commit is contained in:
		| @@ -37,6 +37,7 @@ Mailpit was originally **inspired** by MailHog which is now [no longer maintaine | ||||
| - Optional HTTPS for web UI ([see wiki](https://github.com/axllent/mailpit/wiki/HTTPS)) | ||||
| - Optional basic authentication for web UI ([see wiki](https://github.com/axllent/mailpit/wiki/Basic-authentication)) | ||||
| - A simple REST API ([see docs](docs/apiv1/README.md)) | ||||
| - Optional webhook for received messages ([see docs](https://github.com/axllent/mailpit/wiki/Webhook)) | ||||
| - Multi-architecture [Docker images](https://github.com/axllent/mailpit/wiki/Docker-images) | ||||
|  | ||||
|  | ||||
|   | ||||
							
								
								
									
										11
									
								
								cmd/root.go
									
									
									
									
									
								
							
							
						
						
									
										11
									
								
								cmd/root.go
									
									
									
									
									
								
							| @@ -13,6 +13,7 @@ import ( | ||||
| 	"github.com/axllent/mailpit/internal/storage" | ||||
| 	"github.com/axllent/mailpit/server" | ||||
| 	"github.com/axllent/mailpit/server/smtpd" | ||||
| 	"github.com/axllent/mailpit/server/webhook" | ||||
| 	"github.com/spf13/cobra" | ||||
| ) | ||||
|  | ||||
| @@ -105,6 +106,8 @@ func init() { | ||||
|  | ||||
| 	rootCmd.Flags().StringVar(&config.SMTPRelayConfigFile, "smtp-relay-config", config.SMTPRelayConfigFile, "SMTP configuration file to allow releasing messages") | ||||
| 	rootCmd.Flags().BoolVar(&config.SMTPRelayAllIncoming, "smtp-relay-all", config.SMTPRelayAllIncoming, "Relay all incoming messages via external SMTP server (caution!)") | ||||
| 	rootCmd.Flags().StringVar(&config.WebhookURL, "webhook-url", config.WebhookURL, "Send a webhook request for new messages") | ||||
| 	rootCmd.Flags().IntVar(&webhook.RateLimit, "webhook-limit", webhook.RateLimit, "Limit webhook requests per second") | ||||
|  | ||||
| 	rootCmd.Flags().StringVarP(&config.SMTPCLITags, "tag", "t", config.SMTPCLITags, "Tag new messages matching filters") | ||||
| 	rootCmd.Flags().BoolVarP(&logger.QuietLogging, "quiet", "q", logger.QuietLogging, "Quiet logging (errors only)") | ||||
| @@ -169,6 +172,14 @@ func initConfigFromEnv() { | ||||
| 		config.SMTPRelayAllIncoming = true | ||||
| 	} | ||||
|  | ||||
| 	// Webhook | ||||
| 	if len(os.Getenv("MP_WEBHOOK_URL")) > 0 { | ||||
| 		config.WebhookURL = os.Getenv("MP_WEBHOOK_URL") | ||||
| 	} | ||||
| 	if len(os.Getenv("MP_WEBHOOK_LIMIT")) > 0 { | ||||
| 		webhook.RateLimit, _ = strconv.Atoi(os.Getenv("MP_WEBHOOK_LIMIT")) | ||||
| 	} | ||||
|  | ||||
| 	// Misc options | ||||
| 	if len(os.Getenv("MP_WEBROOT")) > 0 { | ||||
| 		config.Webroot = os.Getenv("MP_WEBROOT") | ||||
|   | ||||
| @@ -4,6 +4,7 @@ package config | ||||
| import ( | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"net/url" | ||||
| 	"os" | ||||
| 	"path" | ||||
| 	"path/filepath" | ||||
| @@ -94,6 +95,9 @@ var ( | ||||
| 	// Use with extreme caution! | ||||
| 	SMTPRelayAllIncoming = false | ||||
|  | ||||
| 	// WebhookURL for calling | ||||
| 	WebhookURL string | ||||
|  | ||||
| 	// ContentSecurityPolicy for HTTP server - set via VerifyConfig() | ||||
| 	ContentSecurityPolicy string | ||||
|  | ||||
| @@ -223,6 +227,10 @@ func VerifyConfig() error { | ||||
| 	s := strings.TrimRight(path.Join("/", Webroot, "/"), "/") + "/" | ||||
| 	Webroot = s | ||||
|  | ||||
| 	if WebhookURL != "" && !isValidURL(WebhookURL) { | ||||
| 		return fmt.Errorf("Webhook URL does not appear to be a valid URL (%s)", WebhookURL) | ||||
| 	} | ||||
|  | ||||
| 	SMTPTags = []AutoTag{} | ||||
|  | ||||
| 	if SMTPCLITags != "" { | ||||
| @@ -349,3 +357,12 @@ func isDir(path string) bool { | ||||
|  | ||||
| 	return true | ||||
| } | ||||
|  | ||||
| func isValidURL(s string) bool { | ||||
| 	u, err := url.ParseRequestURI(s) | ||||
| 	if err != nil { | ||||
| 		return false | ||||
| 	} | ||||
|  | ||||
| 	return strings.HasPrefix(u.Scheme, "http") | ||||
| } | ||||
|   | ||||
							
								
								
									
										1
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						
									
										1
									
								
								go.mod
									
									
									
									
									
								
							| @@ -23,6 +23,7 @@ require ( | ||||
| 	github.com/vanng822/go-premailer v1.20.2 | ||||
| 	golang.org/x/net v0.17.0 | ||||
| 	golang.org/x/text v0.13.0 | ||||
| 	golang.org/x/time v0.3.0 | ||||
| 	gopkg.in/yaml.v3 v3.0.1 | ||||
| 	modernc.org/sqlite v1.26.0 | ||||
| ) | ||||
|   | ||||
							
								
								
									
										2
									
								
								go.sum
									
									
									
									
									
								
							
							
						
						
									
										2
									
								
								go.sum
									
									
									
									
									
								
							| @@ -192,6 +192,8 @@ golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= | ||||
| golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= | ||||
| golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= | ||||
| golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= | ||||
| golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= | ||||
| golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= | ||||
| golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= | ||||
| golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= | ||||
| golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= | ||||
|   | ||||
| @@ -21,6 +21,7 @@ import ( | ||||
| 	"github.com/axllent/mailpit/config" | ||||
| 	"github.com/axllent/mailpit/internal/logger" | ||||
| 	"github.com/axllent/mailpit/internal/tools" | ||||
| 	"github.com/axllent/mailpit/server/webhook" | ||||
| 	"github.com/axllent/mailpit/server/websockets" | ||||
| 	"github.com/google/uuid" | ||||
| 	"github.com/jhillyerd/enmime" | ||||
| @@ -234,6 +235,7 @@ func Store(body []byte) (string, error) { | ||||
| 	c.Snippet = snippet | ||||
|  | ||||
| 	websockets.Broadcast("new", c) | ||||
| 	webhook.Send(c) | ||||
|  | ||||
| 	dbLastAction = time.Now() | ||||
|  | ||||
|   | ||||
							
								
								
									
										72
									
								
								server/webhook/webhook.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										72
									
								
								server/webhook/webhook.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,72 @@ | ||||
| // Package webhook will optionally call a preconfigured endpoint | ||||
| package webhook | ||||
|  | ||||
| import ( | ||||
| 	"bytes" | ||||
| 	"encoding/json" | ||||
| 	"net/http" | ||||
| 	"time" | ||||
|  | ||||
| 	"github.com/axllent/mailpit/config" | ||||
| 	"github.com/axllent/mailpit/internal/logger" | ||||
| 	"golang.org/x/time/rate" | ||||
| ) | ||||
|  | ||||
| var ( | ||||
| 	// RateLimit is the minimum number of seconds between requests | ||||
| 	RateLimit = 1 | ||||
|  | ||||
| 	rl rate.Sometimes | ||||
|  | ||||
| 	rateLimiterSet bool | ||||
| ) | ||||
|  | ||||
| // Send will post the MessageSummary to a webhook (if configured) | ||||
| func Send(msg interface{}) { | ||||
| 	if config.WebhookURL == "" { | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	if !rateLimiterSet { | ||||
| 		if RateLimit > 0 { | ||||
| 			rl = rate.Sometimes{Interval: time.Duration(RateLimit) * time.Second} | ||||
| 		} else { | ||||
| 			// run 1000 per second - ie: do not limit | ||||
| 			rl = rate.Sometimes{First: 1000, Interval: time.Second} | ||||
| 		} | ||||
| 		rateLimiterSet = true | ||||
| 	} | ||||
|  | ||||
| 	go func() { | ||||
| 		rl.Do(func() { | ||||
| 			b, err := json.Marshal(msg) | ||||
| 			if err != nil { | ||||
| 				logger.Log().Errorf("[webhook] invalid data: %s", err) | ||||
| 				return | ||||
| 			} | ||||
|  | ||||
| 			req, err := http.NewRequest("POST", config.WebhookURL, bytes.NewBuffer(b)) | ||||
| 			if err != nil { | ||||
| 				logger.Log().Errorf("[webhook] error: %s", err) | ||||
| 				return | ||||
| 			} | ||||
|  | ||||
| 			req.Header.Set("User-Agent", "Mailpit/"+config.Version) | ||||
| 			req.Header.Set("Content-Type", "application/json") | ||||
|  | ||||
| 			client := &http.Client{} | ||||
| 			resp, err := client.Do(req) | ||||
| 			if err != nil { | ||||
| 				logger.Log().Errorf("[webhook] error sending data: %s", err) | ||||
| 				return | ||||
| 			} | ||||
|  | ||||
| 			if resp.StatusCode < 200 || resp.StatusCode > 299 { | ||||
| 				logger.Log().Warningf("[webhook] %s returned a %d status", config.WebhookURL, resp.StatusCode) | ||||
| 				return | ||||
| 			} | ||||
|  | ||||
| 			defer resp.Body.Close() | ||||
| 		}) | ||||
| 	}() | ||||
| } | ||||
| @@ -80,6 +80,7 @@ func Broadcast(t string, msg interface{}) { | ||||
|  | ||||
| 	if err != nil { | ||||
| 		logger.Log().Errorf("[websocket] broadcast received invalid data: %s", err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	go func() { MessageHub.Broadcast <- b }() | ||||
|   | ||||
		Reference in New Issue
	
	Block a user