Files
warmbly/internal/api/handler/internal_sync.go
T

146 lines
5.0 KiB
Go

package handler
import (
"net/http"
"strconv"
"strings"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"github.com/warmbly/warmbly/internal/models"
"github.com/warmbly/warmbly/internal/observability/errs"
"github.com/warmbly/warmbly/internal/repository"
)
// 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})
}
// InternalSyncFolderMessages answers the worker's expunge reconciliation for
// one IMAP folder: what does the platform still hold here, keyed by UID? The
// worker compares it with the folder's current UID set and removes the rows
// the server no longer reports. uid_validity scopes the answer to the current
// generation, so a UIDVALIDITY change is never read as a folder-wide expunge.
//
// GET /api/v1/internal/sync/folder-messages?user_id=&email_id=&folder_path=&uid_validity=
// -> 200 {"messages":[{"uid":1,"id":"...","message_id":"..."}]}
func (h *Handler) InternalSyncFolderMessages(c *gin.Context) {
if h.EmailSyncState == nil {
c.JSON(http.StatusOK, gin.H{"messages": []repository.StoredFolderMessage{}})
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
}
folderPath := c.Query("folder_path")
if folderPath == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "folder_path required"})
return
}
uidValidity, err := strconv.ParseUint(c.Query("uid_validity"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid uid_validity"})
return
}
messages, err := h.EmailSyncState.ListFolderMessages(c.Request.Context(), userID, emailID, folderPath, uint32(uidValidity))
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"messages": messages})
}
// maxProviderFolderMessages bounds one provider-folder listing.
const maxProviderFolderMessages = 1000
// InternalSyncProviderFolderMessages answers the Gmail folder reconciliation:
// which rows does the platform still believe the provider has in these
// folders? The worker checks each against Gmail's labels and reports the ones
// that moved.
//
// GET /api/v1/internal/sync/provider-folder-messages?user_id=&email_id=&folders=inbox,archive&limit=
// -> 200 {"messages":[{"id":"...","provider_id":"...","provider_folder":"inbox","internal_date":"..."}]}
func (h *Handler) InternalSyncProviderFolderMessages(c *gin.Context) {
if h.EmailSyncState == nil {
c.JSON(http.StatusOK, gin.H{"messages": []repository.ProviderFolderMessage{}})
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 folders []string
for _, raw := range strings.Split(c.Query("folders"), ",") {
f := strings.TrimSpace(raw)
if f == "" {
continue
}
if !models.ValidFolder(f) {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid folder"})
return
}
folders = append(folders, f)
}
if len(folders) == 0 {
c.JSON(http.StatusBadRequest, gin.H{"error": "folders required"})
return
}
limit, err := strconv.Atoi(c.Query("limit"))
if err != nil || limit < 1 || limit > maxProviderFolderMessages {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid limit"})
return
}
messages, err := h.EmailSyncState.ListProviderFolderMessages(c.Request.Context(), userID, emailID, folders, limit)
if err != nil {
errs.CaptureException(err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "could not list stored messages"})
return
}
c.JSON(http.StatusOK, gin.H{"messages": messages})
}