2021-05-24 17:11:48 +00:00
|
|
|
package main
|
|
|
|
|
|
|
|
import (
|
|
|
|
"encoding/json"
|
|
|
|
"io/ioutil"
|
|
|
|
"net/http"
|
|
|
|
"strconv"
|
|
|
|
"time"
|
|
|
|
|
|
|
|
"github.com/knadh/listmonk/models"
|
2021-12-09 15:21:07 +00:00
|
|
|
"github.com/labstack/echo/v4"
|
2021-05-24 17:11:48 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
// handleGetBounces handles retrieval of bounce records.
|
|
|
|
func handleGetBounces(c echo.Context) error {
|
|
|
|
var (
|
|
|
|
app = c.Get("app").(*App)
|
2022-11-08 18:05:31 +00:00
|
|
|
pg = app.paginator.NewFromURL(c.Request().URL.Query())
|
2021-05-24 17:11:48 +00:00
|
|
|
|
|
|
|
id, _ = strconv.Atoi(c.Param("id"))
|
|
|
|
campID, _ = strconv.Atoi(c.QueryParam("campaign_id"))
|
|
|
|
source = c.FormValue("source")
|
|
|
|
orderBy = c.FormValue("order_by")
|
|
|
|
order = c.FormValue("order")
|
|
|
|
)
|
|
|
|
|
2022-04-03 15:24:40 +00:00
|
|
|
// Fetch one bounce.
|
2021-05-24 17:11:48 +00:00
|
|
|
if id > 0 {
|
2022-04-03 15:24:40 +00:00
|
|
|
out, err := app.core.GetBounce(id)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
return c.JSON(http.StatusOK, okResp{out})
|
2021-05-24 17:11:48 +00:00
|
|
|
}
|
|
|
|
|
2022-05-01 09:49:40 +00:00
|
|
|
res, total, err := app.core.QueryBounces(campID, 0, source, orderBy, order, pg.Offset, pg.Limit)
|
2022-04-03 15:24:40 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
2021-05-24 17:11:48 +00:00
|
|
|
}
|
|
|
|
|
2022-04-03 15:24:40 +00:00
|
|
|
// No results.
|
|
|
|
var out models.PageResults
|
|
|
|
if len(res) == 0 {
|
2021-05-24 17:11:48 +00:00
|
|
|
out.Results = []models.Bounce{}
|
|
|
|
return c.JSON(http.StatusOK, okResp{out})
|
|
|
|
}
|
|
|
|
|
|
|
|
// Meta.
|
2022-04-03 15:24:40 +00:00
|
|
|
out.Results = res
|
2022-05-01 09:49:40 +00:00
|
|
|
out.Total = total
|
2021-05-24 17:11:48 +00:00
|
|
|
out.Page = pg.Page
|
|
|
|
out.PerPage = pg.PerPage
|
|
|
|
|
|
|
|
return c.JSON(http.StatusOK, okResp{out})
|
|
|
|
}
|
|
|
|
|
|
|
|
// handleGetSubscriberBounces retrieves a subscriber's bounce records.
|
|
|
|
func handleGetSubscriberBounces(c echo.Context) error {
|
|
|
|
var (
|
2022-04-03 15:24:40 +00:00
|
|
|
app = c.Get("app").(*App)
|
|
|
|
subID, _ = strconv.Atoi(c.Param("id"))
|
2021-05-24 17:11:48 +00:00
|
|
|
)
|
|
|
|
|
2022-04-03 15:24:40 +00:00
|
|
|
if subID < 1 {
|
2021-05-24 17:11:48 +00:00
|
|
|
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("globals.messages.invalidID"))
|
|
|
|
}
|
|
|
|
|
2022-05-01 09:49:40 +00:00
|
|
|
out, _, err := app.core.QueryBounces(0, subID, "", "", "", 0, 1000)
|
2022-04-03 15:24:40 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
2021-05-24 17:11:48 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return c.JSON(http.StatusOK, okResp{out})
|
|
|
|
}
|
|
|
|
|
|
|
|
// handleDeleteBounces handles bounce deletion, either a single one (ID in the URI), or a list.
|
|
|
|
func handleDeleteBounces(c echo.Context) error {
|
|
|
|
var (
|
|
|
|
app = c.Get("app").(*App)
|
|
|
|
pID = c.Param("id")
|
|
|
|
all, _ = strconv.ParseBool(c.QueryParam("all"))
|
2022-04-03 15:24:40 +00:00
|
|
|
IDs = []int{}
|
2021-05-24 17:11:48 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
// Is it an /:id call?
|
|
|
|
if pID != "" {
|
2022-04-03 15:24:40 +00:00
|
|
|
id, _ := strconv.Atoi(pID)
|
2021-05-24 17:11:48 +00:00
|
|
|
if id < 1 {
|
|
|
|
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("globals.messages.invalidID"))
|
|
|
|
}
|
|
|
|
IDs = append(IDs, id)
|
|
|
|
} else if !all {
|
|
|
|
// Multiple IDs.
|
|
|
|
i, err := parseStringIDs(c.Request().URL.Query()["id"])
|
|
|
|
if err != nil {
|
|
|
|
return echo.NewHTTPError(http.StatusBadRequest,
|
|
|
|
app.i18n.Ts("globals.messages.invalidID", "error", err.Error()))
|
|
|
|
}
|
|
|
|
|
|
|
|
if len(i) == 0 {
|
|
|
|
return echo.NewHTTPError(http.StatusBadRequest,
|
|
|
|
app.i18n.Ts("globals.messages.invalidID"))
|
|
|
|
}
|
|
|
|
IDs = i
|
|
|
|
}
|
|
|
|
|
2022-04-03 15:24:40 +00:00
|
|
|
if err := app.core.DeleteBounces(IDs); err != nil {
|
|
|
|
return err
|
2021-05-24 17:11:48 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return c.JSON(http.StatusOK, okResp{true})
|
|
|
|
}
|
|
|
|
|
|
|
|
// handleBounceWebhook renders the HTML preview of a template.
|
|
|
|
func handleBounceWebhook(c echo.Context) error {
|
|
|
|
var (
|
|
|
|
app = c.Get("app").(*App)
|
|
|
|
service = c.Param("service")
|
|
|
|
|
|
|
|
bounces []models.Bounce
|
|
|
|
)
|
|
|
|
|
2022-02-28 13:19:50 +00:00
|
|
|
// Read the request body instead of using c.Bind() to read to save the entire raw request as meta.
|
2021-05-24 17:11:48 +00:00
|
|
|
rawReq, err := ioutil.ReadAll(c.Request().Body)
|
|
|
|
if err != nil {
|
|
|
|
app.log.Printf("error reading ses notification body: %v", err)
|
|
|
|
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.Ts("globals.messages.internalError"))
|
|
|
|
}
|
|
|
|
|
|
|
|
switch true {
|
|
|
|
// Native internal webhook.
|
|
|
|
case service == "":
|
|
|
|
var b models.Bounce
|
|
|
|
if err := json.Unmarshal(rawReq, &b); err != nil {
|
2023-04-11 06:03:13 +00:00
|
|
|
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.Ts("globals.messages.invalidData")+":"+err.Error())
|
2021-05-24 17:11:48 +00:00
|
|
|
}
|
|
|
|
|
2021-09-25 07:27:55 +00:00
|
|
|
if bv, err := validateBounceFields(b, app); err != nil {
|
2021-05-24 17:11:48 +00:00
|
|
|
return err
|
2021-09-25 07:27:55 +00:00
|
|
|
} else {
|
|
|
|
b = bv
|
2021-05-24 17:11:48 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if len(b.Meta) == 0 {
|
|
|
|
b.Meta = json.RawMessage("{}")
|
|
|
|
}
|
|
|
|
|
|
|
|
if b.CreatedAt.Year() == 0 {
|
|
|
|
b.CreatedAt = time.Now()
|
|
|
|
}
|
|
|
|
|
|
|
|
bounces = append(bounces, b)
|
|
|
|
|
|
|
|
// Amazon SES.
|
|
|
|
case service == "ses" && app.constants.BounceSESEnabled:
|
|
|
|
switch c.Request().Header.Get("X-Amz-Sns-Message-Type") {
|
|
|
|
// SNS webhook registration confirmation. Only after these are processed will the endpoint
|
|
|
|
// start getting bounce notifications.
|
|
|
|
case "SubscriptionConfirmation", "UnsubscribeConfirmation":
|
|
|
|
if err := app.bounce.SES.ProcessSubscription(rawReq); err != nil {
|
|
|
|
app.log.Printf("error processing SNS (SES) subscription: %v", err)
|
|
|
|
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("globals.messages.invalidData"))
|
|
|
|
}
|
|
|
|
break
|
|
|
|
|
|
|
|
// Bounce notification.
|
|
|
|
case "Notification":
|
|
|
|
b, err := app.bounce.SES.ProcessBounce(rawReq)
|
|
|
|
if err != nil {
|
|
|
|
app.log.Printf("error processing SES notification: %v", err)
|
|
|
|
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("globals.messages.invalidData"))
|
|
|
|
}
|
|
|
|
bounces = append(bounces, b)
|
|
|
|
|
|
|
|
default:
|
|
|
|
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("globals.messages.invalidData"))
|
|
|
|
}
|
|
|
|
|
|
|
|
// SendGrid.
|
|
|
|
case service == "sendgrid" && app.constants.BounceSendgridEnabled:
|
|
|
|
var (
|
|
|
|
sig = c.Request().Header.Get("X-Twilio-Email-Event-Webhook-Signature")
|
|
|
|
ts = c.Request().Header.Get("X-Twilio-Email-Event-Webhook-Timestamp")
|
|
|
|
)
|
|
|
|
|
|
|
|
// Sendgrid sends multiple bounces.
|
|
|
|
bs, err := app.bounce.Sendgrid.ProcessBounce(sig, ts, rawReq)
|
|
|
|
if err != nil {
|
|
|
|
app.log.Printf("error processing sendgrid notification: %v", err)
|
|
|
|
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("globals.messages.invalidData"))
|
|
|
|
}
|
|
|
|
bounces = append(bounces, bs...)
|
|
|
|
|
2023-08-31 15:57:34 +00:00
|
|
|
// Postmark.
|
|
|
|
case service == "postmark" && app.constants.BouncePostmarkEnabled:
|
|
|
|
bs, err := app.bounce.Postmark.ProcessBounce(rawReq, c)
|
|
|
|
if err != nil {
|
|
|
|
app.log.Printf("error processing postmark notification: %v", err)
|
|
|
|
if _, ok := err.(*echo.HTTPError); ok {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("globals.messages.invalidData"))
|
|
|
|
}
|
|
|
|
bounces = append(bounces, bs...)
|
|
|
|
|
2021-05-24 17:11:48 +00:00
|
|
|
default:
|
|
|
|
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.Ts("bounces.unknownService"))
|
|
|
|
}
|
|
|
|
|
|
|
|
// Record bounces if any.
|
|
|
|
for _, b := range bounces {
|
|
|
|
if err := app.bounce.Record(b); err != nil {
|
|
|
|
app.log.Printf("error recording bounce: %v", err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return c.JSON(http.StatusOK, okResp{true})
|
|
|
|
}
|
|
|
|
|
2021-09-25 07:27:55 +00:00
|
|
|
func validateBounceFields(b models.Bounce, app *App) (models.Bounce, error) {
|
2021-05-24 17:11:48 +00:00
|
|
|
if b.Email == "" && b.SubscriberUUID == "" {
|
2023-04-11 06:03:13 +00:00
|
|
|
return b, echo.NewHTTPError(http.StatusBadRequest, app.i18n.Ts("globals.messages.invalidFields", "name", "email / subscriber_uuid"))
|
2021-05-24 17:11:48 +00:00
|
|
|
}
|
|
|
|
|
2021-09-25 07:27:55 +00:00
|
|
|
if b.SubscriberUUID != "" && !reUUID.MatchString(b.SubscriberUUID) {
|
2023-04-11 06:03:13 +00:00
|
|
|
return b, echo.NewHTTPError(http.StatusBadRequest, app.i18n.Ts("globals.messages.invalidFields", "name", "subscriber_uuid"))
|
2021-08-22 10:13:35 +00:00
|
|
|
}
|
|
|
|
|
2021-09-25 07:27:55 +00:00
|
|
|
if b.Email != "" {
|
|
|
|
em, err := app.importer.SanitizeEmail(b.Email)
|
|
|
|
if err != nil {
|
|
|
|
return b, echo.NewHTTPError(http.StatusBadRequest, err.Error())
|
|
|
|
}
|
|
|
|
b.Email = em
|
2021-05-24 17:11:48 +00:00
|
|
|
}
|
|
|
|
|
2023-04-11 06:03:13 +00:00
|
|
|
if b.Type != models.BounceTypeHard && b.Type != models.BounceTypeSoft && b.Type != models.BounceTypeComplaint {
|
|
|
|
return b, echo.NewHTTPError(http.StatusBadRequest, app.i18n.Ts("globals.messages.invalidFields", "name", "type"))
|
2021-05-24 17:11:48 +00:00
|
|
|
}
|
|
|
|
|
2021-09-25 07:27:55 +00:00
|
|
|
return b, nil
|
2021-05-24 17:11:48 +00:00
|
|
|
}
|