mirror of
https://github.com/warmbly/warmbly.git
synced 2026-10-03 16:02:02 +00:00
231 lines
7.5 KiB
Go
231 lines
7.5 KiB
Go
package handler
|
|
|
|
import (
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
|
|
"github.com/warmbly/warmbly/internal/app/sendingdomain"
|
|
"github.com/warmbly/warmbly/internal/errx"
|
|
"github.com/warmbly/warmbly/internal/models"
|
|
"github.com/warmbly/warmbly/internal/pkg/mailhost"
|
|
)
|
|
|
|
// ListSendingDomains is GET /emails/domains: every domain the workspace sends
|
|
// from, with its authentication, tracking hosts and redirect.
|
|
func (h *Handler) ListSendingDomains(c *gin.Context) {
|
|
orgID, _, ok := importCaller(c)
|
|
if !ok {
|
|
return
|
|
}
|
|
list, xerr := h.SendingDomainService.Overview(c.Request.Context(), orgID)
|
|
if xerr != nil {
|
|
errx.Handle(c, xerr)
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, gin.H{"data": list})
|
|
}
|
|
|
|
// GetTrackingSuggestion is GET /emails/domains/:domain/tracking-suggestion.
|
|
func (h *Handler) GetTrackingSuggestion(c *gin.Context) {
|
|
orgID, _, ok := importCaller(c)
|
|
if !ok {
|
|
return
|
|
}
|
|
domains, xerr := h.SendingDomainService.Overview(c.Request.Context(), orgID)
|
|
if xerr != nil {
|
|
errx.Handle(c, xerr)
|
|
return
|
|
}
|
|
domain := mailhost.NormalizeDomain(strings.TrimPrefix(strings.ToLower(c.Param("domain")), "www."))
|
|
var inUse []models.TrackingDomainUse
|
|
found := false
|
|
for _, d := range domains {
|
|
if d.Domain == domain {
|
|
inUse, found = d.TrackingDomains, true
|
|
}
|
|
}
|
|
if !found {
|
|
errx.Handle(c, errx.NewWithIdentifier(errx.NotFound, sendingdomain.ErrIDNotYours, "The workspace has no mailbox on this domain."))
|
|
return
|
|
}
|
|
sug := h.SendingDomainService.TrackingSuggestion(c.Request.Context(), domain, inUse)
|
|
if sug == nil {
|
|
errx.Handle(c, errx.NewWithIdentifier(errx.BadRequest, sendingdomain.ErrIDNoTracking, "Open and click tracking is not set up on this instance."))
|
|
return
|
|
}
|
|
sug.VendorDomain = h.SendingDomainService.VendorLink(c.Request.Context(), orgID, domain)
|
|
c.JSON(http.StatusOK, sug)
|
|
}
|
|
|
|
// BulkDomainSetup is POST /emails/domains/bulk: one tracking subdomain and one
|
|
// root redirect for up to 100 domains, each through its vendor when the vendor
|
|
// can. Every row reports its own outcome. Safe to retry: each setting is set, not added.
|
|
func (h *Handler) BulkDomainSetup(c *gin.Context) {
|
|
orgID, userID, ok := importCaller(c)
|
|
if !ok {
|
|
return
|
|
}
|
|
var req sendingdomain.BulkInput
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
errx.Handle(c, errx.ErrInvalid)
|
|
return
|
|
}
|
|
rows, xerr := h.SendingDomainService.BulkSetup(c.Request.Context(), orgID, userID, req)
|
|
if xerr != nil {
|
|
errx.Handle(c, xerr)
|
|
return
|
|
}
|
|
meta := map[string]string{"domains": strconv.Itoa(len(rows)), "via": "bulk"}
|
|
if req.TrackingLabel != "" {
|
|
h.auditOrg(c, models.AuditActionUpdate, models.AuditEntityEmailAccount, nil, map[string]string{"tracking_label": req.TrackingLabel}, meta)
|
|
}
|
|
if req.RedirectURL != "" {
|
|
h.auditOrg(c, models.AuditActionUpdate, models.AuditEntityDomainRedirect, nil, map[string]string{"target_url": req.RedirectURL}, meta)
|
|
}
|
|
c.JSON(http.StatusOK, gin.H{"data": rows})
|
|
}
|
|
|
|
type domainTrackingRequest struct {
|
|
Host string `json:"host"`
|
|
}
|
|
|
|
// SetDomainTracking is PUT /emails/domains/:domain/tracking: one tracking host
|
|
// for every mailbox on the domain; an empty host clears it. Idempotent.
|
|
func (h *Handler) SetDomainTracking(c *gin.Context) {
|
|
orgID, _, ok := importCaller(c)
|
|
if !ok {
|
|
return
|
|
}
|
|
var req domainTrackingRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
errx.Handle(c, errx.ErrInvalid)
|
|
return
|
|
}
|
|
status, n, xerr := h.SendingDomainService.ApplyTracking(c.Request.Context(), orgID, c.Param("domain"), req.Host)
|
|
if xerr != nil {
|
|
errx.Handle(c, xerr)
|
|
return
|
|
}
|
|
h.auditOrg(c, models.AuditActionUpdate, models.AuditEntityEmailAccount, nil, map[string]string{"tracking_domain": req.Host}, map[string]string{"domain": c.Param("domain")})
|
|
c.JSON(http.StatusOK, gin.H{"tracking": status, "mailboxes": n})
|
|
}
|
|
|
|
// SetDomainRedirect is PUT /emails/domains/:domain/redirect. Idempotent: the
|
|
// same domain keeps its verification token, so the TXT record stays valid.
|
|
func (h *Handler) SetDomainRedirect(c *gin.Context) {
|
|
orgID, userID, ok := importCaller(c)
|
|
if !ok {
|
|
return
|
|
}
|
|
var req sendingdomain.RedirectInput
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
errx.Handle(c, errx.ErrInvalid)
|
|
return
|
|
}
|
|
r, xerr := h.SendingDomainService.SetRedirect(c.Request.Context(), orgID, userID, c.Param("domain"), req)
|
|
if xerr != nil {
|
|
errx.Handle(c, xerr)
|
|
return
|
|
}
|
|
h.auditOrg(c, models.AuditActionUpdate, models.AuditEntityDomainRedirect, &r.ID, nil, map[string]string{"domain": r.Domain})
|
|
c.JSON(http.StatusOK, r)
|
|
}
|
|
|
|
// VerifyDomainRedirect is POST /emails/domains/:domain/redirect/verify: check DNS now.
|
|
func (h *Handler) VerifyDomainRedirect(c *gin.Context) {
|
|
orgID, _, ok := importCaller(c)
|
|
if !ok {
|
|
return
|
|
}
|
|
r, xerr := h.SendingDomainService.VerifyRedirect(c.Request.Context(), orgID, c.Param("domain"))
|
|
if xerr != nil {
|
|
errx.Handle(c, xerr)
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, r)
|
|
}
|
|
|
|
// DeleteDomainRedirect is DELETE /emails/domains/:domain/redirect.
|
|
func (h *Handler) DeleteDomainRedirect(c *gin.Context) {
|
|
orgID, _, ok := importCaller(c)
|
|
if !ok {
|
|
return
|
|
}
|
|
if xerr := h.SendingDomainService.DeleteRedirect(c.Request.Context(), orgID, c.Param("domain")); xerr != nil {
|
|
errx.Handle(c, xerr)
|
|
return
|
|
}
|
|
h.auditOrg(c, models.AuditActionDelete, models.AuditEntityDomainRedirect, nil, nil, map[string]string{"domain": c.Param("domain")})
|
|
c.Status(http.StatusNoContent)
|
|
}
|
|
|
|
// InternalGetDomainRedirect is the tracking service's lookup for a verified bare domain.
|
|
//
|
|
// GET /api/v1/internal/domain-redirects/:host -> 200 {"target_url"} | 404
|
|
func (h *Handler) InternalGetDomainRedirect(c *gin.Context) {
|
|
if h.SendingDomainService == nil {
|
|
c.Status(http.StatusNotFound)
|
|
return
|
|
}
|
|
target, ok, err := h.SendingDomainService.Lookup(c.Request.Context(), c.Param("host"))
|
|
if err != nil {
|
|
c.Status(http.StatusServiceUnavailable)
|
|
return
|
|
}
|
|
if !ok {
|
|
c.Status(http.StatusNotFound)
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, gin.H{"target_url": target})
|
|
}
|
|
|
|
type vendorForwardingRequest struct {
|
|
URL string `json:"url"`
|
|
}
|
|
|
|
// SetDomainVendorForwarding is PUT /emails/domains/:domain/vendor-forwarding:
|
|
// the inbox vendor holding the domain forwards its root. An empty url removes
|
|
// it where the vendor allows. Idempotent; the service audits the change.
|
|
func (h *Handler) SetDomainVendorForwarding(c *gin.Context) {
|
|
orgID, userID, ok := importCaller(c)
|
|
if !ok {
|
|
return
|
|
}
|
|
var req vendorForwardingRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
errx.Handle(c, errx.ErrInvalid)
|
|
return
|
|
}
|
|
link, xerr := h.SendingDomainService.VendorForward(c.Request.Context(), orgID, userID, c.Param("domain"), req.URL)
|
|
if xerr != nil {
|
|
errx.Handle(c, xerr)
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, link)
|
|
}
|
|
|
|
// SetDomainVendorTracking is POST /emails/domains/:domain/vendor-tracking: the
|
|
// inbox vendor writes the tracking CNAME, then every mailbox on the domain uses
|
|
// the host. Safe to retry: the record is upserted and the host is set, not added.
|
|
func (h *Handler) SetDomainVendorTracking(c *gin.Context) {
|
|
orgID, _, ok := importCaller(c)
|
|
if !ok {
|
|
return
|
|
}
|
|
var req domainTrackingRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil || strings.TrimSpace(req.Host) == "" {
|
|
errx.Handle(c, errx.ErrInvalid)
|
|
return
|
|
}
|
|
status, n, xerr := h.SendingDomainService.VendorTracking(c.Request.Context(), orgID, c.Param("domain"), req.Host)
|
|
if xerr != nil {
|
|
errx.Handle(c, xerr)
|
|
return
|
|
}
|
|
h.auditOrg(c, models.AuditActionUpdate, models.AuditEntityEmailAccount, nil, map[string]string{"tracking_domain": req.Host}, map[string]string{"domain": c.Param("domain"), "via": "vendor"})
|
|
c.JSON(http.StatusOK, gin.H{"tracking": status, "mailboxes": n})
|
|
}
|