package mail import ( "context" "errors" "fmt" "strings" "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgconn" "github.com/ultisuite/ulti-backend/internal/api/query" ) type UnifiedFoldersList struct { Folders []map[string]any `json:"folders"` Pagination query.PaginationMeta `json:"pagination,omitempty"` } func scanUnifiedFolderRow( id string, accountID *string, parentID *string, name, color string, sortOrder int, createdAt, updatedAt any, ) map[string]any { row := map[string]any{ "id": id, "name": name, "color": color, "sort_order": sortOrder, "created_at": createdAt, "updated_at": updatedAt, "scope": "global", } if accountID != nil && *accountID != "" { row["account_id"] = *accountID row["scope"] = "account" } if parentID != nil && *parentID != "" { row["parent_id"] = *parentID } return row } func (s *Service) ListUnifiedFolders(ctx context.Context, externalID, accountID string, params query.ListParams) (UnifiedFoldersList, error) { userID, err := s.ResolveUserID(ctx, externalID) if err != nil { return UnifiedFoldersList{}, err } countQuery := ` SELECT COUNT(*) FROM mail_unified_folders WHERE user_id = $1 ` listQuery := ` SELECT id, account_id, parent_id, name, color, sort_order, created_at, updated_at FROM mail_unified_folders WHERE user_id = $1 ` args := []any{userID} switch strings.TrimSpace(accountID) { case "", "all": // all scopes case "global": countQuery += ` AND account_id IS NULL` listQuery += ` AND account_id IS NULL` default: var owned bool if err := s.db.QueryRow(ctx, ` SELECT EXISTS( SELECT 1 FROM mail_accounts WHERE id = $1 AND user_id = $2 ) `, accountID, userID).Scan(&owned); err != nil { return UnifiedFoldersList{}, err } if !owned { return UnifiedFoldersList{}, ErrAccountNotFound } countQuery += ` AND account_id = $2` listQuery += ` AND account_id = $2` args = append(args, accountID) } var total int64 if err := s.db.QueryRow(ctx, countQuery, args...).Scan(&total); err != nil { return UnifiedFoldersList{}, err } listQuery += fmt.Sprintf(` ORDER BY sort_order ASC, name ASC LIMIT $%d OFFSET $%d`, len(args)+1, len(args)+2) args = append(args, params.Limit(), params.Offset()) rows, err := s.db.Query(ctx, listQuery, args...) if err != nil { return UnifiedFoldersList{}, err } defer rows.Close() folders := make([]map[string]any, 0) for rows.Next() { var id, name, color string var accountIDVal, parentIDVal *string var sortOrder int var createdAt, updatedAt any if err := rows.Scan(&id, &accountIDVal, &parentIDVal, &name, &color, &sortOrder, &createdAt, &updatedAt); err != nil { return UnifiedFoldersList{}, err } folders = append(folders, scanUnifiedFolderRow(id, accountIDVal, parentIDVal, name, color, sortOrder, createdAt, updatedAt)) } if err := rows.Err(); err != nil { return UnifiedFoldersList{}, err } return UnifiedFoldersList{ Folders: folders, Pagination: params.Meta(&total), }, nil } func (s *Service) CreateUnifiedFolder(ctx context.Context, userID string, req *createUnifiedFolderRequest) (string, error) { scopeAccountID := strings.TrimSpace(req.AccountID) parentID := strings.TrimSpace(req.ParentID) if scopeAccountID != "" { var owned bool if err := s.db.QueryRow(ctx, ` SELECT EXISTS(SELECT 1 FROM mail_accounts WHERE id = $1 AND user_id = $2) `, scopeAccountID, userID).Scan(&owned); err != nil { return "", err } if !owned { return "", ErrAccountNotFound } } if parentID != "" { var parentAccountID *string if err := s.db.QueryRow(ctx, ` SELECT account_id FROM mail_unified_folders WHERE id = $1 AND user_id = $2 `, parentID, userID).Scan(&parentAccountID); err != nil { if errors.Is(err, pgx.ErrNoRows) { return "", ErrNotFound } return "", err } if scopeAccountID == "" { if parentAccountID != nil { return "", ErrInvalidFolderScope } } else if parentAccountID == nil || *parentAccountID != scopeAccountID { return "", ErrInvalidFolderScope } } var id string var accountArg any if scopeAccountID == "" { accountArg = nil } else { accountArg = scopeAccountID } var parentArg any if parentID == "" { parentArg = nil } else { parentArg = parentID } err := s.db.QueryRow(ctx, ` INSERT INTO mail_unified_folders (user_id, account_id, parent_id, name, color, sort_order) VALUES ($1, $2, $3, $4, $5, $6) RETURNING id `, userID, accountArg, parentArg, strings.TrimSpace(req.Name), strings.TrimSpace(req.Color), req.SortOrder).Scan(&id) if err != nil { if isUniqueViolation(err) { return "", ErrDuplicateFolder } return "", err } return id, nil } func (s *Service) UpdateUnifiedFolder(ctx context.Context, externalID, folderID string, req *updateUnifiedFolderRequest) error { userID, err := s.ResolveUserID(ctx, externalID) if err != nil { return err } if req.ParentID != nil { parentID := strings.TrimSpace(*req.ParentID) if parentID != "" { var parentAccountID *string if err := s.db.QueryRow(ctx, ` SELECT account_id FROM mail_unified_folders WHERE id = $1 AND user_id = $2 `, parentID, userID).Scan(&parentAccountID); err != nil { if errors.Is(err, pgx.ErrNoRows) { return ErrNotFound } return err } var folderAccountID *string if err := s.db.QueryRow(ctx, ` SELECT account_id FROM mail_unified_folders WHERE id = $1 AND user_id = $2 `, folderID, userID).Scan(&folderAccountID); err != nil { if errors.Is(err, pgx.ErrNoRows) { return ErrNotFound } return err } if (folderAccountID == nil) != (parentAccountID == nil) { return ErrInvalidFolderScope } if folderAccountID != nil && parentAccountID != nil && *folderAccountID != *parentAccountID { return ErrInvalidFolderScope } } } var parentArg any if req.ParentID == nil { result, err := s.db.Exec(ctx, ` UPDATE mail_unified_folders SET name = $1, color = $2, sort_order = $3, updated_at = NOW() WHERE id = $4 AND user_id = $5 `, strings.TrimSpace(req.Name), strings.TrimSpace(req.Color), req.SortOrder, folderID, userID) if err != nil { return err } if result.RowsAffected() == 0 { return ErrNotFound } return nil } parentID := strings.TrimSpace(*req.ParentID) if parentID == "" { parentArg = nil } else { parentArg = parentID } result, err := s.db.Exec(ctx, ` UPDATE mail_unified_folders SET name = $1, color = $2, sort_order = $3, parent_id = $4, updated_at = NOW() WHERE id = $5 AND user_id = $6 `, strings.TrimSpace(req.Name), strings.TrimSpace(req.Color), req.SortOrder, parentArg, folderID, userID) if err != nil { return err } if result.RowsAffected() == 0 { return ErrNotFound } return nil } func (s *Service) DeleteUnifiedFolder(ctx context.Context, externalID, folderID string) error { result, err := s.db.Exec(ctx, ` DELETE FROM mail_unified_folders WHERE id = $1 AND user_id = (SELECT id FROM users WHERE external_id = $2) `, folderID, externalID) if err != nil { var pgErr *pgconn.PgError if errors.As(err, &pgErr) && pgErr.Code == "23503" { return ErrFolderHasChildren } return err } if result.RowsAffected() == 0 { return ErrNotFound } return nil }