1
0
mirror of https://github.com/axllent/mailpit.git synced 2025-03-17 21:18:19 +02:00

Feature: Set optional webhook for received messages (#195)

This commit is contained in:
Ralph Slooten 2023-10-20 23:15:58 +13:00
parent f256d205ed
commit 140bdd6c20
8 changed files with 107 additions and 0 deletions

View File

@ -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)

View File

@ -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")

View File

@ -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
View File

@ -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
View File

@ -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=

View File

@ -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
View 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()
})
}()
}

View File

@ -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 }()