285 lines
7.2 KiB
Go
285 lines
7.2 KiB
Go
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
|
|
}
|