package mail import ( "context" "errors" "github.com/jackc/pgx/v5" "github.com/ultisuite/ulti-backend/internal/nextcloud" ) const driveSourceUltimail = "ultimail" func (s *Service) SetDriveUploader(uploader DriveUploader) { s.driveUploader = uploader } func (s *Service) SaveAttachmentToDrive( ctx context.Context, externalID, email, sub, displayName, messageID, attachmentID, folderPath string, ) (string, error) { if s.driveUploader == nil { return "", ErrDriveUnavailable } if s.storage == nil { return "", errors.New("object storage unavailable") } folderPath = normalizeDriveFolder(folderPath) userID, err := s.ensureMessageOwned(ctx, externalID, messageID) if err != nil { return "", err } var ( filename string contentType string size int64 s3Key string existing string ) err = s.db.QueryRow(ctx, ` SELECT a.filename, a.content_type, a.size, a.s3_key, COALESCE(a.drive_path, '') FROM attachments a WHERE a.id = $1 AND a.message_id = $2 `, attachmentID, messageID).Scan(&filename, &contentType, &size, &s3Key, &existing) if err != nil { if errors.Is(err, pgx.ErrNoRows) { return "", ErrAttachmentNotFound } return "", err } if existing != "" { return existing, nil } if folderPath != "/" { if err := s.driveUploader.EnsureNextcloudFolder(ctx, email, sub, displayName, folderPath); err != nil { return "", err } } destPath, err := uniqueDriveFilePath(ctx, s.driveUploader, email, sub, displayName, folderPath, filename) if err != nil { return "", err } obj, err := s.storage.Get(ctx, s3Key) if err != nil { return "", err } defer obj.Close() destPath = nextcloud.NormalizeClientPath(destPath) if err := s.driveUploader.UploadFile(ctx, email, sub, displayName, destPath, obj, contentType, size); err != nil { return "", err } s.driveUploader.NotifyFileChanged(externalID, destPath) if err := s.recordDriveFileSource(ctx, userID, destPath, driveSourceUltimail); err != nil { return "", err } _, err = s.db.Exec(ctx, ` UPDATE attachments SET drive_path = $1 WHERE id = $2 AND message_id = $3 `, destPath, attachmentID, messageID) if err != nil { return "", err } return destPath, nil } func (s *Service) SaveMessageAttachmentsToDrive( ctx context.Context, externalID, email, sub, displayName, messageID, folderPath string, ) ([]map[string]any, error) { list, err := s.ListMessageAttachments(ctx, externalID, messageID) if err != nil { return nil, err } out := make([]map[string]any, 0, len(list)) for _, att := range list { if att["is_inline"] == true { continue } id, _ := att["id"].(string) if id == "" { continue } drivePath, err := s.SaveAttachmentToDrive(ctx, externalID, email, sub, displayName, messageID, id, folderPath) if err != nil { return nil, err } entry := make(map[string]any, len(att)+1) for k, v := range att { entry[k] = v } entry["drive_path"] = drivePath out = append(out, entry) } return out, nil } func (s *Service) recordDriveFileSource(ctx context.Context, userID, filePath, source string) error { filePath = nextcloud.NormalizeClientPath(filePath) _, err := s.db.Exec(ctx, ` INSERT INTO drive_file_sources (user_id, file_path, source) VALUES ($1, $2, $3) ON CONFLICT (user_id, file_path) DO UPDATE SET source = EXCLUDED.source `, userID, filePath, source) return err }