feat: split a unibox message's provider placement into its own provider_folder column (migration 000146) so Archive and Delete in the thread header survive the next sync without the sync losing the ability to follow a real provider move, and narrow PATCH /unibox/folder to inbox/archive/trash behind the unibox feature gate with an audit entry so the move reaches every teammate's list live

This commit is contained in:
Matthew Meszaros
2026-09-11 03:23:19 -07:00
parent 727ddb1482
commit da4b89da0b
12 changed files with 539 additions and 44 deletions
+15 -1
View File
@@ -387,9 +387,15 @@ func (h *Handler) UniboxMarkSeen(c *gin.Context) {
c.JSON(http.StatusOK, resp)
}
// UniboxMoveFolder re-files messages (Delete = trash, Archive = archive).
// UniboxMoveFolder re-files messages (Archive = archive, Delete = trash,
// Move to inbox = inbox). Org-scoped like /seen: the inbox is shared, so any
// member with unibox access may file it. Naturally idempotent, so no
// Idempotency-Key: the body names the destination, not a delta.
// PATCH /unibox/folder
func (h *Handler) UniboxMoveFolder(c *gin.Context) {
if !h.gateUnibox(c) {
return
}
orgID := middleware.GetOrganizationID(c)
if orgID == nil {
errx.Handle(c, errx.ErrUser)
@@ -408,6 +414,14 @@ func (h *Handler) UniboxMoveFolder(c *gin.Context) {
return
}
// Audited so the spine broadcasts it: a teammate looking at the same list
// has to lose the thread too, and there is no sync event behind this one.
h.auditOrg(c, models.AuditActionUpdate, models.AuditEntityUnibox, nil, nil, map[string]string{
"action": "move_folder",
"folder": data.Folder,
"messages": strconv.Itoa(len(data.EmailIDs)),
})
c.JSON(http.StatusOK, resp)
}