Files
warmbly/internal/api/handler/internal_sync.go
Matthew Meszaros 44da2f464f feat: add the control plane for mailbox sync fair use, so a mailbox syncs under an operator-editable policy and its progress survives worker replacement: a sync section on the instance settings document (backfill window in days, backfill cap per mailbox, daily new-mail budget per mailbox and per organization, each clamped on read and write with compiled defaults in constants.go), models.SyncPolicy and models.SyncState with a provider-shaped jsonb SyncCursor, a new email_sync_state table plus an index on tasks.message_id that the reply lookup was scanning sequentially without, an EmailSyncStateRepository whose Put also stamps email_accounts.last_synced_at which nothing had written since the baseline so every admin and dashboard Last synced surface read NULL, an OrganizationID and Sync block on the ADD_EMAIL payload resolved by the loader from instance settings and the saved state and, for IMAP, the saved unibox_mailboxes folder cursors that the loader had never populated so every worker restart re-walked every folder from scratch, a SYNC_STATE consumer handler that persists the relay and publishes ACCOUNT_SYNC_STATE plus a warning when the import completes or fair use flips, an internal own-conversation endpoint the worker's priority lane asks whether a new message replies to a campaign task, a mapped message or a stored thread, GET /emails/:id/sync for the dashboard, and SYNC_FLOOD and SYNC_FAIR_USE mail error codes with user copy for the two patterns that deactivate a mailbox
2026-08-18 08:43:20 -07:00

46 lines
1.4 KiB
Go

package handler
import (
"net/http"
"strings"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
)
// InternalSyncOwnConversation answers the worker's priority-lane question:
// does any of the given RFC message ids, or the provider thread id, belong to
// a message this mailbox sent or already holds? Workers call it once per new
// message so a reply to outreach is admitted ahead of fair-use throttling.
//
// GET /api/v1/internal/sync/own-conversation?user_id=&email_id=&message_ids=a,b&thread_id=
// -> 200 {"own": bool}
func (h *Handler) InternalSyncOwnConversation(c *gin.Context) {
if h.EmailSyncState == nil {
c.JSON(http.StatusOK, gin.H{"own": false})
return
}
userID, err := uuid.Parse(c.Query("user_id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid user_id"})
return
}
emailID, err := uuid.Parse(c.Query("email_id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid email_id"})
return
}
var ids []string
for _, raw := range strings.Split(c.Query("message_ids"), ",") {
if id := strings.TrimSpace(raw); id != "" {
ids = append(ids, id)
}
}
own, err := h.EmailSyncState.IsOwnConversation(c.Request.Context(), userID, emailID, ids, c.Query("thread_id"))
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"own": own})
}