Skip to content

Commit

Permalink
Feature: Set optional webhook for received messages (#195)
Browse files Browse the repository at this point in the history
  • Loading branch information
axllent committed Oct 20, 2023
1 parent f256d20 commit 140bdd6
Show file tree
Hide file tree
Showing 8 changed files with 107 additions and 0 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)


Expand Down
11 changes: 11 additions & 0 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand Down Expand Up @@ -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)")
Expand Down Expand Up @@ -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")
Expand Down
17 changes: 17 additions & 0 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ package config
import (
"errors"
"fmt"
"net/url"
"os"
"path"
"path/filepath"
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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 != "" {
Expand Down Expand Up @@ -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 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down
2 changes: 2 additions & 0 deletions internal/storage/database.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -234,6 +235,7 @@ func Store(body []byte) (string, error) {
c.Snippet = snippet

websockets.Broadcast("new", c)
webhook.Send(c)

dbLastAction = time.Now()

Expand Down
72 changes: 72 additions & 0 deletions server/webhook/webhook.go
Original file line number Diff line number Diff line change
@@ -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()
})
}()
}
1 change: 1 addition & 0 deletions server/websockets/hub.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 }()
Expand Down

0 comments on commit 140bdd6

Please sign in to comment.