From 1a8f95117e91078f1cfaa8e584c58a21e687c926 Mon Sep 17 00:00:00 2001 From: rustmailer Date: Wed, 19 Nov 2025 02:14:37 +0800 Subject: [PATCH] initial commit --- .gitignore | 3 + .rustfmt.toml | 7 + Cargo.lock | 6343 ++++++++++++++ Cargo.toml | 114 + about.hbs | 70 + about.toml | 18 + build.rs | 14 + build/release.sh | 1 + build/rustmailer.service | 32 + cliff.toml | 58 + docker/Dockerfile | 36 + docker/build.sh | 31 + env/rustmailer.env | 44 + src/main.rs | 76 + src/modules/account/dispatcher.rs | 60 + src/modules/account/entity.rs | 120 + src/modules/account/migration.rs | 357 + src/modules/account/mod.rs | 25 + src/modules/account/payload.rs | 172 + src/modules/account/since.rs | 284 + src/modules/account/state.rs | 340 + src/modules/autoconfig/entity.rs | 75 + src/modules/autoconfig/load.rs | 114 + src/modules/autoconfig/mod.rs | 86 + src/modules/autoconfig/tests.rs | 24 + src/modules/cache/imap/mailbox.rs | 207 + src/modules/cache/imap/mod.rs | 66 + src/modules/cache/imap/sync/flow.rs | 379 + src/modules/cache/imap/sync/mod.rs | 113 + src/modules/cache/imap/sync/rebuild.rs | 200 + src/modules/cache/imap/sync/sync_folders.rs | 184 + src/modules/cache/imap/sync/sync_type.rs | 68 + src/modules/cache/imap/task.rs | 117 + src/modules/cache/mod.rs | 33 + src/modules/common/auth.rs | 232 + src/modules/common/error.rs | 47 + src/modules/common/log.rs | 172 + src/modules/common/mod.rs | 233 + src/modules/common/paginated.rs | 72 + src/modules/common/periodic.rs | 120 + src/modules/common/rustls.rs | 41 + src/modules/common/signal.rs | 55 + src/modules/common/timeout.rs | 74 + src/modules/common/tls.rs | 54 + src/modules/common/validator.rs | 43 + src/modules/context/controller.rs | 72 + src/modules/context/executors.rs | 110 + src/modules/context/mod.rs | 32 + src/modules/context/status.rs | 50 + src/modules/dashboard/mod.rs | 109 + src/modules/database/manager.rs | 118 + src/modules/database/mod.rs | 492 ++ src/modules/envelope/extractor.rs | 146 + src/modules/envelope/mod.rs | 20 + src/modules/error/code.rs | 86 + src/modules/error/handler.rs | 76 + src/modules/error/mod.rs | 111 + src/modules/imap/capabilities.rs | 51 + src/modules/imap/client.rs | 251 + src/modules/imap/executor.rs | 241 + src/modules/imap/manager.rs | 155 + src/modules/imap/mod.rs | 29 + src/modules/imap/oauth2.rs | 41 + src/modules/imap/pool.rs | 60 + src/modules/imap/session.rs | 52 + src/modules/imap/stats.rs | 86 + src/modules/imap/tests.rs | 48 + src/modules/indexer/envelope.rs | 194 + src/modules/indexer/fields.rs | 69 + src/modules/indexer/manager.rs | 1412 +++ src/modules/indexer/mod.rs | 25 + src/modules/indexer/schema.rs | 128 + src/modules/indexer/tests.rs | 229 + src/modules/logger/file.rs | 65 + src/modules/logger/mod.rs | 74 + src/modules/mailbox/list.rs | 99 + src/modules/mailbox/mod.rs | 20 + src/modules/message/content.rs | 147 + src/modules/message/delete.rs | 31 + src/modules/message/list.rs | 69 + src/modules/message/mod.rs | 24 + src/modules/message/search.rs | 80 + src/modules/message/tags.rs | 35 + src/modules/mod.rs | 40 + src/modules/oauth2/entity.rs | 275 + src/modules/oauth2/flow.rs | 295 + src/modules/oauth2/mod.rs | 25 + src/modules/oauth2/pending.rs | 138 + src/modules/oauth2/refresh/mod.rs | 91 + src/modules/oauth2/task.rs | 43 + src/modules/oauth2/token.rs | 271 + src/modules/rest/api/access_token.rs | 156 + src/modules/rest/api/account.rs | 207 + src/modules/rest/api/auto_config.rs | 55 + src/modules/rest/api/mailbox.rs | 60 + src/modules/rest/api/message.rs | 218 + src/modules/rest/api/mod.rs | 74 + src/modules/rest/api/oauth2.rs | 228 + src/modules/rest/api/system.rs | 120 + src/modules/rest/assets.rs | 24 + src/modules/rest/mod.rs | 139 + src/modules/rest/public/login.rs | 45 + src/modules/rest/public/mod.rs | 22 + src/modules/rest/public/oauth2.rs | 94 + src/modules/rest/public/status.rs | 26 + src/modules/rest/response.rs | 84 + src/modules/settings/cli.rs | 246 + src/modules/settings/dir.rs | 81 + src/modules/settings/mod.rs | 23 + src/modules/settings/proxy.rs | 145 + src/modules/settings/system.rs | 70 + src/modules/tasks/mod.rs | 29 + src/modules/token/mod.rs | 357 + src/modules/token/payload.rs | 122 + src/modules/token/root.rs | 125 + src/modules/utils/encrypt.rs | 131 + src/modules/utils/mod.rs | 312 + src/modules/utils/net.rs | 163 + src/modules/utils/rate_limit/mod.rs | 103 + src/modules/utils/shutdown.rs | 44 + src/modules/utils/tls.rs | 78 + src/modules/version/mod.rs | 118 + web/.gitignore | 2 + web/.prettierignore | 18 + web/.prettierrc | 51 + web/components.json | 21 + web/cz.yaml | 7 + web/eslint.config.js | 34 + web/index.html | 20 + web/knip.config.ts | 7 + web/package.json | 98 + web/pnpm-lock.yaml | 7645 +++++++++++++++++ web/postcss.config.js | 6 + web/public/assets/favicon.svg | 438 + web/src/api/access-tokens/api.ts | 64 + web/src/api/account/api.ts | 105 + web/src/api/axiosInstance.ts | 61 + web/src/api/index.ts | 46 + web/src/api/mailbox/api.ts | 37 + web/src/api/mailbox/envelope/api.ts | 99 + web/src/api/oauth2/api.ts | 62 + web/src/api/search/api.ts | 43 + web/src/api/system/api.ts | 105 + web/src/assets/logo.svg | 438 + web/src/components/ace-editor.tsx | 79 + web/src/components/api-docs.tsx | 77 + web/src/components/button.tsx | 118 + web/src/components/command-menu.tsx | 110 + web/src/components/confirm-dialog.tsx | 86 + web/src/components/date-picker.tsx | 51 + web/src/components/editor-with-draft.tsx | 110 + web/src/components/layout/app-sidebar.tsx | 63 + .../components/layout/data/sidebar-data.ts | 92 + web/src/components/layout/fixed-header.tsx | 38 + web/src/components/layout/github.tsx | 48 + web/src/components/layout/header.tsx | 67 + web/src/components/layout/main.tsx | 44 + web/src/components/layout/nav-group.tsx | 199 + web/src/components/layout/nav-user.tsx | 126 + web/src/components/layout/notification.tsx | 179 + web/src/components/layout/types.ts | 49 + web/src/components/long-text.tsx | 104 + web/src/components/mail-iframe.tsx | 41 + web/src/components/multi-select.tsx | 398 + web/src/components/pagination.tsx | 117 + web/src/components/password-input.tsx | 58 + web/src/components/profile-dropdown.tsx | 67 + web/src/components/select-dropdown.tsx | 81 + web/src/components/table-skeleton.tsx | 94 + web/src/components/theme-switch.tsx | 48 + web/src/components/tree-view.tsx | 529 ++ web/src/components/ui/accordion.tsx | 55 + web/src/components/ui/alert-dialog.tsx | 138 + web/src/components/ui/alert.tsx | 58 + web/src/components/ui/avatar.tsx | 47 + web/src/components/ui/badge.tsx | 35 + web/src/components/ui/button.tsx | 57 + web/src/components/ui/calendar.tsx | 69 + web/src/components/ui/card.tsx | 76 + web/src/components/ui/chart.tsx | 363 + web/src/components/ui/checkbox.tsx | 27 + web/src/components/ui/collapsible.tsx | 9 + web/src/components/ui/command.tsx | 162 + web/src/components/ui/dialog.tsx | 131 + web/src/components/ui/drawer.tsx | 117 + web/src/components/ui/dropdown-menu.tsx | 198 + web/src/components/ui/form.tsx | 175 + web/src/components/ui/hover-card.tsx | 27 + web/src/components/ui/input.tsx | 21 + web/src/components/ui/label.tsx | 23 + web/src/components/ui/navigation-menu.tsx | 128 + web/src/components/ui/popover.tsx | 30 + web/src/components/ui/progress.tsx | 26 + web/src/components/ui/radio-group.tsx | 41 + web/src/components/ui/resizable.tsx | 43 + web/src/components/ui/scroll-area.tsx | 55 + web/src/components/ui/select.tsx | 156 + web/src/components/ui/separator.tsx | 28 + web/src/components/ui/sheet.tsx | 155 + web/src/components/ui/sidebar.tsx | 772 ++ web/src/components/ui/skeleton.tsx | 15 + web/src/components/ui/switch.tsx | 26 + web/src/components/ui/table.tsx | 119 + web/src/components/ui/tabs.tsx | 52 + web/src/components/ui/textarea.tsx | 21 + web/src/components/ui/toast.tsx | 126 + web/src/components/ui/toaster.tsx | 33 + web/src/components/ui/tooltip.tsx | 29 + web/src/components/virtualized-select.tsx | 315 + web/src/context/search-context.tsx | 65 + web/src/context/theme-context.tsx | 78 + .../components/access-token-table.tsx | 152 + .../components/account-action.tsx | 41 + .../account-detail-table-toolbar.tsx | 44 + .../components/accounts-detail-columns.tsx | 50 + .../components/accounts-detail-dialog.tsx | 66 + .../components/accounts-detail-table.tsx | 145 + .../access-tokens/components/acl-action.tsx | 42 + .../components/acl-detail-dialog.tsx | 97 + .../components/action-dialog.tsx | 387 + .../access-tokens/components/columns.tsx | 127 + .../components/data-table-column-header.tsx | 89 + .../components/data-table-pagination.tsx | 122 + .../components/data-table-row-actions.tsx | 82 + .../components/data-table-toolbar.tsx | 44 + .../components/delete-dialog.tsx | 128 + .../features/access-tokens/context/index.tsx | 53 + web/src/features/access-tokens/data/schema.ts | 45 + web/src/features/access-tokens/index.tsx | 153 + .../accounts/components/account-detail.tsx | 179 + .../accounts/components/action-dialog.tsx | 499 ++ .../features/accounts/components/columns.tsx | 138 + .../accounts/components/complete-step.tsx | 60 + .../components/data-table-column-header.tsx | 89 + .../components/data-table-pagination.tsx | 122 + .../components/data-table-row-actions.tsx | 113 + .../components/data-table-toolbar.tsx | 44 + .../accounts/components/delete-dialog.tsx | 136 + .../accounts/components/enable-action.tsx | 92 + .../components/folder-sync-progress.tsx | 65 + .../accounts/components/nosync-dialog.tsx | 257 + .../accounts/components/oauth2-action.tsx | 56 + .../accounts/components/oauth2-tokens.tsx | 179 + .../components/running-state-action.tsx | 45 + .../components/running-state-dialog.tsx | 291 + .../features/accounts/components/step1.tsx | 86 + .../features/accounts/components/step2.tsx | 196 + .../features/accounts/components/step3.tsx | 239 + .../features/accounts/components/step4.tsx | 121 + .../accounts/components/sync-folders.tsx | 183 + .../features/accounts/components/table.tsx | 153 + web/src/features/accounts/context/index.tsx | 53 + web/src/features/accounts/data/schema.ts | 63 + web/src/features/accounts/index.tsx | 216 + .../auth/sign-in/components/logout.tsx | 61 + .../sign-in/components/user-auth-form.tsx | 146 + web/src/features/auth/sign-in/index.tsx | 43 + web/src/features/dashboard/index.tsx | 423 + web/src/features/errors/forbidden.tsx | 44 + web/src/features/errors/general-error.tsx | 55 + web/src/features/errors/maintenance-error.tsx | 38 + web/src/features/errors/not-found-error.tsx | 44 + .../features/errors/unauthorized-error.tsx | 44 + .../mailbox/components/account-switcher.tsx | 57 + .../mailbox/components/bulk-actions.tsx | 169 + .../mailbox/components/delete-dialog.tsx | 113 + .../components/mail-display-drawer.tsx | 53 + .../features/mailbox/components/mail-list.tsx | 202 + .../mailbox/components/mail-message-view.tsx | 314 + web/src/features/mailbox/components/mail.tsx | 267 + .../mailbox/components/mailbox-detail.tsx | 90 + .../mailbox/components/thread-dialog.tsx | 217 + web/src/features/mailbox/context/index.tsx | 61 + web/src/features/mailbox/index.tsx | 47 + web/src/features/oauth2-result/index.tsx | 127 + .../oauth2/components/action-dialog.tsx | 638 ++ .../oauth2/components/authorize-dialog.tsx | 133 + .../features/oauth2/components/columns.tsx | 131 + .../components/data-table-column-header.tsx | 89 + .../components/data-table-pagination.tsx | 122 + .../components/data-table-row-actions.tsx | 95 + .../oauth2/components/data-table-toolbar.tsx | 44 + .../oauth2/components/delete-dialog.tsx | 127 + .../oauth2/components/enable-action.tsx | 92 + .../oauth2/components/oauth2-table.tsx | 152 + web/src/features/oauth2/context/index.tsx | 53 + web/src/features/oauth2/data/schema.ts | 34 + web/src/features/oauth2/index.tsx | 144 + web/src/features/search/add-tag-dialog.tsx | 219 + web/src/features/search/bulk-actions.tsx | 180 + web/src/features/search/context/index.tsx | 58 + web/src/features/search/delete-dialog.tsx | 111 + web/src/features/search/index.tsx | 196 + .../features/search/mail-display-dialog.tsx | 59 + web/src/features/search/mail-list.tsx | 273 + web/src/features/search/mail-message-view.tsx | 342 + web/src/features/search/search-form.tsx | 470 + web/src/features/search/tag-facet.tsx | 125 + web/src/features/search/thread-dialog.tsx | 217 + .../settings/components/content-section.tsx | 50 + .../settings/components/sidebar-nav.tsx | 108 + web/src/features/settings/index.tsx | 62 + .../proxy/components/action-dialog.tsx | 257 + .../settings/proxy/components/columns.tsx | 79 + .../components/data-table-column-header.tsx | 89 + .../components/data-table-pagination.tsx | 122 + .../components/data-table-row-actions.tsx | 82 + .../proxy/components/data-table-toolbar.tsx | 44 + .../proxy/components/delete-dialog.tsx | 126 + .../settings/proxy/components/table.tsx | 153 + .../features/settings/proxy/context/index.tsx | 53 + .../features/settings/proxy/data/schema.ts | 25 + web/src/features/settings/proxy/index.tsx | 115 + web/src/features/settings/root/index.tsx | 226 + web/src/hooks/use-available-tags.ts | 66 + web/src/hooks/use-dialog-state.tsx | 37 + web/src/hooks/use-minimal-account-list.tsx | 51 + web/src/hooks/use-mobile.tsx | 38 + web/src/hooks/use-proxy.tsx | 52 + web/src/hooks/use-search-messages.ts | 89 + web/src/hooks/use-toast.ts | 208 + web/src/hooks/use-update-tags.ts | 58 + web/src/index.css | 148 + web/src/lib/build-tree.ts | 117 + web/src/lib/utils.ts | 113 + web/src/main.tsx | 145 + web/src/routeTree.gen.ts | 691 ++ web/src/routes/(auth)/500.tsx | 25 + web/src/routes/(auth)/sign-in.tsx | 25 + web/src/routes/(errors)/401.lazy.tsx | 25 + web/src/routes/(errors)/403.lazy.tsx | 25 + web/src/routes/(errors)/404.lazy.tsx | 25 + web/src/routes/(errors)/500.lazy.tsx | 25 + web/src/routes/(errors)/503.lazy.tsx | 25 + web/src/routes/__root.tsx | 47 + .../access-tokens/index.lazy.tsx | 25 + .../_authenticated/accounts/index.lazy.tsx | 25 + .../_authenticated/api-docs/index.lazy.tsx | 25 + web/src/routes/_authenticated/index.tsx | 25 + .../_authenticated/mailboxes/index.lazy.tsx | 25 + .../oauth2-result/index.lazy.tsx | 25 + .../_authenticated/oauth2/index.lazy.tsx | 25 + web/src/routes/_authenticated/route.tsx | 55 + .../_authenticated/search/index.lazy.tsx | 25 + .../_authenticated/settings/index.lazy.tsx | 25 + .../_authenticated/settings/proxy.lazy.tsx | 25 + .../_authenticated/settings/root.lazy.tsx | 27 + .../_authenticated/settings/route.lazy.tsx | 25 + web/src/stores/authStore.ts | 50 + web/src/vite-env.d.ts | 1 + web/tailwind.config.js | 98 + web/tsconfig.app.json | 32 + web/tsconfig.json | 13 + web/tsconfig.node.json | 24 + web/vite.config.ts | 18 + 355 files changed, 54089 insertions(+) create mode 100644 .gitignore create mode 100644 .rustfmt.toml create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 about.hbs create mode 100644 about.toml create mode 100644 build.rs create mode 100644 build/release.sh create mode 100644 build/rustmailer.service create mode 100644 cliff.toml create mode 100644 docker/Dockerfile create mode 100644 docker/build.sh create mode 100644 env/rustmailer.env create mode 100644 src/main.rs create mode 100644 src/modules/account/dispatcher.rs create mode 100644 src/modules/account/entity.rs create mode 100644 src/modules/account/migration.rs create mode 100644 src/modules/account/mod.rs create mode 100644 src/modules/account/payload.rs create mode 100644 src/modules/account/since.rs create mode 100644 src/modules/account/state.rs create mode 100644 src/modules/autoconfig/entity.rs create mode 100644 src/modules/autoconfig/load.rs create mode 100644 src/modules/autoconfig/mod.rs create mode 100644 src/modules/autoconfig/tests.rs create mode 100644 src/modules/cache/imap/mailbox.rs create mode 100644 src/modules/cache/imap/mod.rs create mode 100644 src/modules/cache/imap/sync/flow.rs create mode 100644 src/modules/cache/imap/sync/mod.rs create mode 100644 src/modules/cache/imap/sync/rebuild.rs create mode 100644 src/modules/cache/imap/sync/sync_folders.rs create mode 100644 src/modules/cache/imap/sync/sync_type.rs create mode 100644 src/modules/cache/imap/task.rs create mode 100644 src/modules/cache/mod.rs create mode 100644 src/modules/common/auth.rs create mode 100644 src/modules/common/error.rs create mode 100644 src/modules/common/log.rs create mode 100644 src/modules/common/mod.rs create mode 100644 src/modules/common/paginated.rs create mode 100644 src/modules/common/periodic.rs create mode 100644 src/modules/common/rustls.rs create mode 100644 src/modules/common/signal.rs create mode 100644 src/modules/common/timeout.rs create mode 100644 src/modules/common/tls.rs create mode 100644 src/modules/common/validator.rs create mode 100644 src/modules/context/controller.rs create mode 100644 src/modules/context/executors.rs create mode 100644 src/modules/context/mod.rs create mode 100644 src/modules/context/status.rs create mode 100644 src/modules/dashboard/mod.rs create mode 100644 src/modules/database/manager.rs create mode 100644 src/modules/database/mod.rs create mode 100644 src/modules/envelope/extractor.rs create mode 100644 src/modules/envelope/mod.rs create mode 100644 src/modules/error/code.rs create mode 100644 src/modules/error/handler.rs create mode 100644 src/modules/error/mod.rs create mode 100644 src/modules/imap/capabilities.rs create mode 100644 src/modules/imap/client.rs create mode 100644 src/modules/imap/executor.rs create mode 100644 src/modules/imap/manager.rs create mode 100644 src/modules/imap/mod.rs create mode 100644 src/modules/imap/oauth2.rs create mode 100644 src/modules/imap/pool.rs create mode 100644 src/modules/imap/session.rs create mode 100644 src/modules/imap/stats.rs create mode 100644 src/modules/imap/tests.rs create mode 100644 src/modules/indexer/envelope.rs create mode 100644 src/modules/indexer/fields.rs create mode 100644 src/modules/indexer/manager.rs create mode 100644 src/modules/indexer/mod.rs create mode 100644 src/modules/indexer/schema.rs create mode 100644 src/modules/indexer/tests.rs create mode 100644 src/modules/logger/file.rs create mode 100644 src/modules/logger/mod.rs create mode 100644 src/modules/mailbox/list.rs create mode 100644 src/modules/mailbox/mod.rs create mode 100644 src/modules/message/content.rs create mode 100644 src/modules/message/delete.rs create mode 100644 src/modules/message/list.rs create mode 100644 src/modules/message/mod.rs create mode 100644 src/modules/message/search.rs create mode 100644 src/modules/message/tags.rs create mode 100644 src/modules/mod.rs create mode 100644 src/modules/oauth2/entity.rs create mode 100644 src/modules/oauth2/flow.rs create mode 100644 src/modules/oauth2/mod.rs create mode 100644 src/modules/oauth2/pending.rs create mode 100644 src/modules/oauth2/refresh/mod.rs create mode 100644 src/modules/oauth2/task.rs create mode 100644 src/modules/oauth2/token.rs create mode 100644 src/modules/rest/api/access_token.rs create mode 100644 src/modules/rest/api/account.rs create mode 100644 src/modules/rest/api/auto_config.rs create mode 100644 src/modules/rest/api/mailbox.rs create mode 100644 src/modules/rest/api/message.rs create mode 100644 src/modules/rest/api/mod.rs create mode 100644 src/modules/rest/api/oauth2.rs create mode 100644 src/modules/rest/api/system.rs create mode 100644 src/modules/rest/assets.rs create mode 100644 src/modules/rest/mod.rs create mode 100644 src/modules/rest/public/login.rs create mode 100644 src/modules/rest/public/mod.rs create mode 100644 src/modules/rest/public/oauth2.rs create mode 100644 src/modules/rest/public/status.rs create mode 100644 src/modules/rest/response.rs create mode 100644 src/modules/settings/cli.rs create mode 100644 src/modules/settings/dir.rs create mode 100644 src/modules/settings/mod.rs create mode 100644 src/modules/settings/proxy.rs create mode 100644 src/modules/settings/system.rs create mode 100644 src/modules/tasks/mod.rs create mode 100644 src/modules/token/mod.rs create mode 100644 src/modules/token/payload.rs create mode 100644 src/modules/token/root.rs create mode 100644 src/modules/utils/encrypt.rs create mode 100644 src/modules/utils/mod.rs create mode 100644 src/modules/utils/net.rs create mode 100644 src/modules/utils/rate_limit/mod.rs create mode 100644 src/modules/utils/shutdown.rs create mode 100644 src/modules/utils/tls.rs create mode 100644 src/modules/version/mod.rs create mode 100644 web/.gitignore create mode 100644 web/.prettierignore create mode 100644 web/.prettierrc create mode 100644 web/components.json create mode 100644 web/cz.yaml create mode 100644 web/eslint.config.js create mode 100644 web/index.html create mode 100644 web/knip.config.ts create mode 100644 web/package.json create mode 100644 web/pnpm-lock.yaml create mode 100644 web/postcss.config.js create mode 100644 web/public/assets/favicon.svg create mode 100644 web/src/api/access-tokens/api.ts create mode 100644 web/src/api/account/api.ts create mode 100644 web/src/api/axiosInstance.ts create mode 100644 web/src/api/index.ts create mode 100644 web/src/api/mailbox/api.ts create mode 100644 web/src/api/mailbox/envelope/api.ts create mode 100644 web/src/api/oauth2/api.ts create mode 100644 web/src/api/search/api.ts create mode 100644 web/src/api/system/api.ts create mode 100644 web/src/assets/logo.svg create mode 100644 web/src/components/ace-editor.tsx create mode 100644 web/src/components/api-docs.tsx create mode 100644 web/src/components/button.tsx create mode 100644 web/src/components/command-menu.tsx create mode 100644 web/src/components/confirm-dialog.tsx create mode 100644 web/src/components/date-picker.tsx create mode 100644 web/src/components/editor-with-draft.tsx create mode 100644 web/src/components/layout/app-sidebar.tsx create mode 100644 web/src/components/layout/data/sidebar-data.ts create mode 100644 web/src/components/layout/fixed-header.tsx create mode 100644 web/src/components/layout/github.tsx create mode 100644 web/src/components/layout/header.tsx create mode 100644 web/src/components/layout/main.tsx create mode 100644 web/src/components/layout/nav-group.tsx create mode 100644 web/src/components/layout/nav-user.tsx create mode 100644 web/src/components/layout/notification.tsx create mode 100644 web/src/components/layout/types.ts create mode 100644 web/src/components/long-text.tsx create mode 100644 web/src/components/mail-iframe.tsx create mode 100644 web/src/components/multi-select.tsx create mode 100644 web/src/components/pagination.tsx create mode 100644 web/src/components/password-input.tsx create mode 100644 web/src/components/profile-dropdown.tsx create mode 100644 web/src/components/select-dropdown.tsx create mode 100644 web/src/components/table-skeleton.tsx create mode 100644 web/src/components/theme-switch.tsx create mode 100644 web/src/components/tree-view.tsx create mode 100644 web/src/components/ui/accordion.tsx create mode 100644 web/src/components/ui/alert-dialog.tsx create mode 100644 web/src/components/ui/alert.tsx create mode 100644 web/src/components/ui/avatar.tsx create mode 100644 web/src/components/ui/badge.tsx create mode 100644 web/src/components/ui/button.tsx create mode 100644 web/src/components/ui/calendar.tsx create mode 100644 web/src/components/ui/card.tsx create mode 100644 web/src/components/ui/chart.tsx create mode 100644 web/src/components/ui/checkbox.tsx create mode 100644 web/src/components/ui/collapsible.tsx create mode 100644 web/src/components/ui/command.tsx create mode 100644 web/src/components/ui/dialog.tsx create mode 100644 web/src/components/ui/drawer.tsx create mode 100644 web/src/components/ui/dropdown-menu.tsx create mode 100644 web/src/components/ui/form.tsx create mode 100644 web/src/components/ui/hover-card.tsx create mode 100644 web/src/components/ui/input.tsx create mode 100644 web/src/components/ui/label.tsx create mode 100644 web/src/components/ui/navigation-menu.tsx create mode 100644 web/src/components/ui/popover.tsx create mode 100644 web/src/components/ui/progress.tsx create mode 100644 web/src/components/ui/radio-group.tsx create mode 100644 web/src/components/ui/resizable.tsx create mode 100644 web/src/components/ui/scroll-area.tsx create mode 100644 web/src/components/ui/select.tsx create mode 100644 web/src/components/ui/separator.tsx create mode 100644 web/src/components/ui/sheet.tsx create mode 100644 web/src/components/ui/sidebar.tsx create mode 100644 web/src/components/ui/skeleton.tsx create mode 100644 web/src/components/ui/switch.tsx create mode 100644 web/src/components/ui/table.tsx create mode 100644 web/src/components/ui/tabs.tsx create mode 100644 web/src/components/ui/textarea.tsx create mode 100644 web/src/components/ui/toast.tsx create mode 100644 web/src/components/ui/toaster.tsx create mode 100644 web/src/components/ui/tooltip.tsx create mode 100644 web/src/components/virtualized-select.tsx create mode 100644 web/src/context/search-context.tsx create mode 100644 web/src/context/theme-context.tsx create mode 100644 web/src/features/access-tokens/components/access-token-table.tsx create mode 100644 web/src/features/access-tokens/components/account-action.tsx create mode 100644 web/src/features/access-tokens/components/account-detail-table-toolbar.tsx create mode 100644 web/src/features/access-tokens/components/accounts-detail-columns.tsx create mode 100644 web/src/features/access-tokens/components/accounts-detail-dialog.tsx create mode 100644 web/src/features/access-tokens/components/accounts-detail-table.tsx create mode 100644 web/src/features/access-tokens/components/acl-action.tsx create mode 100644 web/src/features/access-tokens/components/acl-detail-dialog.tsx create mode 100644 web/src/features/access-tokens/components/action-dialog.tsx create mode 100644 web/src/features/access-tokens/components/columns.tsx create mode 100644 web/src/features/access-tokens/components/data-table-column-header.tsx create mode 100644 web/src/features/access-tokens/components/data-table-pagination.tsx create mode 100644 web/src/features/access-tokens/components/data-table-row-actions.tsx create mode 100644 web/src/features/access-tokens/components/data-table-toolbar.tsx create mode 100644 web/src/features/access-tokens/components/delete-dialog.tsx create mode 100644 web/src/features/access-tokens/context/index.tsx create mode 100644 web/src/features/access-tokens/data/schema.ts create mode 100644 web/src/features/access-tokens/index.tsx create mode 100644 web/src/features/accounts/components/account-detail.tsx create mode 100644 web/src/features/accounts/components/action-dialog.tsx create mode 100644 web/src/features/accounts/components/columns.tsx create mode 100644 web/src/features/accounts/components/complete-step.tsx create mode 100644 web/src/features/accounts/components/data-table-column-header.tsx create mode 100644 web/src/features/accounts/components/data-table-pagination.tsx create mode 100644 web/src/features/accounts/components/data-table-row-actions.tsx create mode 100644 web/src/features/accounts/components/data-table-toolbar.tsx create mode 100644 web/src/features/accounts/components/delete-dialog.tsx create mode 100644 web/src/features/accounts/components/enable-action.tsx create mode 100644 web/src/features/accounts/components/folder-sync-progress.tsx create mode 100644 web/src/features/accounts/components/nosync-dialog.tsx create mode 100644 web/src/features/accounts/components/oauth2-action.tsx create mode 100644 web/src/features/accounts/components/oauth2-tokens.tsx create mode 100644 web/src/features/accounts/components/running-state-action.tsx create mode 100644 web/src/features/accounts/components/running-state-dialog.tsx create mode 100644 web/src/features/accounts/components/step1.tsx create mode 100644 web/src/features/accounts/components/step2.tsx create mode 100644 web/src/features/accounts/components/step3.tsx create mode 100644 web/src/features/accounts/components/step4.tsx create mode 100644 web/src/features/accounts/components/sync-folders.tsx create mode 100644 web/src/features/accounts/components/table.tsx create mode 100644 web/src/features/accounts/context/index.tsx create mode 100644 web/src/features/accounts/data/schema.ts create mode 100644 web/src/features/accounts/index.tsx create mode 100644 web/src/features/auth/sign-in/components/logout.tsx create mode 100644 web/src/features/auth/sign-in/components/user-auth-form.tsx create mode 100644 web/src/features/auth/sign-in/index.tsx create mode 100644 web/src/features/dashboard/index.tsx create mode 100644 web/src/features/errors/forbidden.tsx create mode 100644 web/src/features/errors/general-error.tsx create mode 100644 web/src/features/errors/maintenance-error.tsx create mode 100644 web/src/features/errors/not-found-error.tsx create mode 100644 web/src/features/errors/unauthorized-error.tsx create mode 100644 web/src/features/mailbox/components/account-switcher.tsx create mode 100644 web/src/features/mailbox/components/bulk-actions.tsx create mode 100644 web/src/features/mailbox/components/delete-dialog.tsx create mode 100644 web/src/features/mailbox/components/mail-display-drawer.tsx create mode 100644 web/src/features/mailbox/components/mail-list.tsx create mode 100644 web/src/features/mailbox/components/mail-message-view.tsx create mode 100644 web/src/features/mailbox/components/mail.tsx create mode 100644 web/src/features/mailbox/components/mailbox-detail.tsx create mode 100644 web/src/features/mailbox/components/thread-dialog.tsx create mode 100644 web/src/features/mailbox/context/index.tsx create mode 100644 web/src/features/mailbox/index.tsx create mode 100644 web/src/features/oauth2-result/index.tsx create mode 100644 web/src/features/oauth2/components/action-dialog.tsx create mode 100644 web/src/features/oauth2/components/authorize-dialog.tsx create mode 100644 web/src/features/oauth2/components/columns.tsx create mode 100644 web/src/features/oauth2/components/data-table-column-header.tsx create mode 100644 web/src/features/oauth2/components/data-table-pagination.tsx create mode 100644 web/src/features/oauth2/components/data-table-row-actions.tsx create mode 100644 web/src/features/oauth2/components/data-table-toolbar.tsx create mode 100644 web/src/features/oauth2/components/delete-dialog.tsx create mode 100644 web/src/features/oauth2/components/enable-action.tsx create mode 100644 web/src/features/oauth2/components/oauth2-table.tsx create mode 100644 web/src/features/oauth2/context/index.tsx create mode 100644 web/src/features/oauth2/data/schema.ts create mode 100644 web/src/features/oauth2/index.tsx create mode 100644 web/src/features/search/add-tag-dialog.tsx create mode 100644 web/src/features/search/bulk-actions.tsx create mode 100644 web/src/features/search/context/index.tsx create mode 100644 web/src/features/search/delete-dialog.tsx create mode 100644 web/src/features/search/index.tsx create mode 100644 web/src/features/search/mail-display-dialog.tsx create mode 100644 web/src/features/search/mail-list.tsx create mode 100644 web/src/features/search/mail-message-view.tsx create mode 100644 web/src/features/search/search-form.tsx create mode 100644 web/src/features/search/tag-facet.tsx create mode 100644 web/src/features/search/thread-dialog.tsx create mode 100644 web/src/features/settings/components/content-section.tsx create mode 100644 web/src/features/settings/components/sidebar-nav.tsx create mode 100644 web/src/features/settings/index.tsx create mode 100644 web/src/features/settings/proxy/components/action-dialog.tsx create mode 100644 web/src/features/settings/proxy/components/columns.tsx create mode 100644 web/src/features/settings/proxy/components/data-table-column-header.tsx create mode 100644 web/src/features/settings/proxy/components/data-table-pagination.tsx create mode 100644 web/src/features/settings/proxy/components/data-table-row-actions.tsx create mode 100644 web/src/features/settings/proxy/components/data-table-toolbar.tsx create mode 100644 web/src/features/settings/proxy/components/delete-dialog.tsx create mode 100644 web/src/features/settings/proxy/components/table.tsx create mode 100644 web/src/features/settings/proxy/context/index.tsx create mode 100644 web/src/features/settings/proxy/data/schema.ts create mode 100644 web/src/features/settings/proxy/index.tsx create mode 100644 web/src/features/settings/root/index.tsx create mode 100644 web/src/hooks/use-available-tags.ts create mode 100644 web/src/hooks/use-dialog-state.tsx create mode 100644 web/src/hooks/use-minimal-account-list.tsx create mode 100644 web/src/hooks/use-mobile.tsx create mode 100644 web/src/hooks/use-proxy.tsx create mode 100644 web/src/hooks/use-search-messages.ts create mode 100644 web/src/hooks/use-toast.ts create mode 100644 web/src/hooks/use-update-tags.ts create mode 100644 web/src/index.css create mode 100644 web/src/lib/build-tree.ts create mode 100644 web/src/lib/utils.ts create mode 100644 web/src/main.tsx create mode 100644 web/src/routeTree.gen.ts create mode 100644 web/src/routes/(auth)/500.tsx create mode 100644 web/src/routes/(auth)/sign-in.tsx create mode 100644 web/src/routes/(errors)/401.lazy.tsx create mode 100644 web/src/routes/(errors)/403.lazy.tsx create mode 100644 web/src/routes/(errors)/404.lazy.tsx create mode 100644 web/src/routes/(errors)/500.lazy.tsx create mode 100644 web/src/routes/(errors)/503.lazy.tsx create mode 100644 web/src/routes/__root.tsx create mode 100644 web/src/routes/_authenticated/access-tokens/index.lazy.tsx create mode 100644 web/src/routes/_authenticated/accounts/index.lazy.tsx create mode 100644 web/src/routes/_authenticated/api-docs/index.lazy.tsx create mode 100644 web/src/routes/_authenticated/index.tsx create mode 100644 web/src/routes/_authenticated/mailboxes/index.lazy.tsx create mode 100644 web/src/routes/_authenticated/oauth2-result/index.lazy.tsx create mode 100644 web/src/routes/_authenticated/oauth2/index.lazy.tsx create mode 100644 web/src/routes/_authenticated/route.tsx create mode 100644 web/src/routes/_authenticated/search/index.lazy.tsx create mode 100644 web/src/routes/_authenticated/settings/index.lazy.tsx create mode 100644 web/src/routes/_authenticated/settings/proxy.lazy.tsx create mode 100644 web/src/routes/_authenticated/settings/root.lazy.tsx create mode 100644 web/src/routes/_authenticated/settings/route.lazy.tsx create mode 100644 web/src/stores/authStore.ts create mode 100644 web/src/vite-env.d.ts create mode 100644 web/tailwind.config.js create mode 100644 web/tsconfig.app.json create mode 100644 web/tsconfig.json create mode 100644 web/tsconfig.node.json create mode 100644 web/vite.config.ts diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..454e0a3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/target +.vscode +.idea diff --git a/.rustfmt.toml b/.rustfmt.toml new file mode 100644 index 0000000..fc993b6 --- /dev/null +++ b/.rustfmt.toml @@ -0,0 +1,7 @@ +edition = "2021" +newline_style = "unix" +normalize_comments = true +wrap_comments = true +format_code_in_doc_comments = true +imports_granularity = "Crate" +group_imports = "StdExternalCrate" diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..2876cfa --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,6343 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "adler2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" + +[[package]] +name = "aead" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fc95d1bdb8e6666b2b217308eeeb09f2d6728d104be3e31916cc74d15420331" +dependencies = [ + "generic-array", +] + +[[package]] +name = "aes" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "884391ef1066acaa41e766ba8f596341b96e93ce34f9a43e7d24bf0a0eaf0561" +dependencies = [ + "aes-soft", + "aesni", + "cipher 0.2.5", +] + +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher 0.4.4", + "cpufeatures", +] + +[[package]] +name = "aes-gcm" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5278b5fabbb9bd46e24aa69b2fdea62c99088e0a950a9be40e3e0101298f88da" +dependencies = [ + "aead", + "aes 0.6.0", + "cipher 0.2.5", + "ctr", + "ghash", + "subtle", +] + +[[package]] +name = "aes-soft" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be14c7498ea50828a38d0e24a765ed2effe92a705885b57d029cd67d45744072" +dependencies = [ + "cipher 0.2.5", + "opaque-debug", +] + +[[package]] +name = "aesni" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea2e11f5e94c2f7d386164cc2aa1f97823fed6f259e486940a71c174dd01b0ce" +dependencies = [ + "cipher 0.2.5", + "opaque-debug", +] + +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "getrandom 0.3.1", + "once_cell", + "version_check", + "zerocopy 0.8.25", +] + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "alloc-no-stdlib" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" + +[[package]] +name = "alloc-stdlib" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" +dependencies = [ + "alloc-no-stdlib", +] + +[[package]] +name = "allocator-api2" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anstream" +version = "0.6.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64e15c1ab1f89faffbf04a634d5e1962e9074f2741eef6d97f3c4e322426d526" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1" + +[[package]] +name = "anstyle-parse" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb47de1e80c2b463c735db5b217a0ddc39d612e7ac9e2e96a5aed1f57616c1cb" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d36fc52c7f6c869915e99412912f22093507da8d9e942ceaf66fe4b7c14422a" +dependencies = [ + "windows-sys 0.52.0", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5bf74e1b6e971609db8ca7a9ce79fd5768ab6ae46441c572e46cf596f59e57f8" +dependencies = [ + "anstyle", + "windows-sys 0.52.0", +] + +[[package]] +name = "anyhow" +version = "1.0.98" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" + +[[package]] +name = "arbitrary" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dde20b3d026af13f561bdd0f15edf01fc734f0dafcedbaf42bba506a9517f223" +dependencies = [ + "derive_arbitrary", +] + +[[package]] +name = "arc-swap" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457" + +[[package]] +name = "async-channel" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81953c529336010edd6d8e358f886d9581267795c61b19475b71314bffa46d35" +dependencies = [ + "concurrent-queue", + "event-listener 2.5.3", + "futures-core", +] + +[[package]] +name = "async-channel" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89b47800b0be77592da0afd425cc03468052844aff33b84e33cc696f64e77b6a" +dependencies = [ + "concurrent-queue", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-compression" +version = "0.4.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cb8f1d480b0ea3783ab015936d2a55c87e219676f0c0b7dec61494043f21857" +dependencies = [ + "brotli", + "flate2", + "futures-core", + "memchr", + "pin-project-lite", + "tokio", + "zstd", + "zstd-safe", +] + +[[package]] +name = "async-executor" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30ca9a001c1e8ba5149f91a74362376cc6bc5b919d92d988668657bd570bdcec" +dependencies = [ + "async-task", + "concurrent-queue", + "fastrand 2.1.1", + "futures-lite 2.3.0", + "slab", +] + +[[package]] +name = "async-global-executor" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05b1b633a2115cd122d73b955eadd9916c18c8f510ec9cd1686404c60ad1c29c" +dependencies = [ + "async-channel 2.3.1", + "async-executor", + "async-io", + "async-lock", + "blocking", + "futures-lite 2.3.0", + "once_cell", +] + +[[package]] +name = "async-imap" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8da885da5980f3934831e6370445c0e0e44ef251d7792308b39e908915a41d09" +dependencies = [ + "async-channel 2.3.1", + "async-compression", + "base64 0.22.1", + "bytes 1.11.0", + "chrono", + "futures", + "imap-proto", + "log", + "nom", + "pin-project", + "pin-utils", + "self_cell", + "stop-token", + "thiserror 1.0.68", + "tokio", +] + +[[package]] +name = "async-io" +version = "2.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "444b0228950ee6501b3568d3c93bf1176a1fdbc3b758dcd9475046d30f4dc7e8" +dependencies = [ + "async-lock", + "cfg-if", + "concurrent-queue", + "futures-io", + "futures-lite 2.3.0", + "parking", + "polling", + "rustix 0.38.37", + "slab", + "tracing", + "windows-sys 0.59.0", +] + +[[package]] +name = "async-lock" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff6e472cdea888a4bd64f342f09b3f50e1886d32afe8df3d663c01140b811b18" +dependencies = [ + "event-listener 5.3.1", + "event-listener-strategy", + "pin-project-lite", +] + +[[package]] +name = "async-std" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "730294c1c08c2e0f85759590518f6333f0d5a0a766a27d519c1b244c3dfd8a24" +dependencies = [ + "async-channel 1.9.0", + "async-global-executor", + "async-io", + "async-lock", + "crossbeam-utils", + "futures-channel", + "futures-core", + "futures-io", + "futures-lite 2.3.0", + "gloo-timers", + "kv-log-macro", + "log", + "memchr", + "once_cell", + "pin-project-lite", + "pin-utils", + "slab", + "wasm-bindgen-futures", +] + +[[package]] +name = "async-task" +version = "4.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" + +[[package]] +name = "async-trait" +version = "0.1.88" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e539d3fca749fcee5236ab05e93a52867dd549cc157c8cb7f99595f3cedffdb5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" + +[[package]] +name = "autoconfig" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3dc5ca11ac66da7ba331bd707c8d6bca90a559c107aada6ee6abf1d4d5dbc2c" +dependencies = [ + "bytes 1.11.0", + "futures", + "regex", + "serde", + "serde-xml-rs", + "surf", + "trust-dns-proto", + "trust-dns-resolver", +] + +[[package]] +name = "aws-lc-rs" +version = "1.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879b6c89592deb404ba4dc0ae6b58ffd1795c78991cbb5b8bc441c48a070440d" +dependencies = [ + "aws-lc-sys", + "zeroize", +] + +[[package]] +name = "aws-lc-sys" +version = "0.32.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2b715a6010afb9e457ca2b7c9d2b9c344baa8baed7b38dc476034c171b32575" +dependencies = [ + "bindgen", + "cc", + "cmake", + "dunce", + "fs_extra", + "libloading", +] + +[[package]] +name = "base-x" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cbbc9d0964165b47557570cce6c952866c2678457aca742aafc9fb771d30270" + +[[package]] +name = "base64" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" + +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bb8" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "212d8b8e1a22743d9241575c6ba822cf9c8fef34771c86ab7e477a4fbfd254e5" +dependencies = [ + "futures-util", + "parking_lot", + "tokio", +] + +[[package]] +name = "bichon" +version = "0.0.1" +dependencies = [ + "ahash", + "async-imap", + "autoconfig", + "base64 0.22.1", + "bb8", + "bytes 1.11.0", + "cacache", + "chrono", + "clap", + "dashmap", + "email_address", + "encoding_rs", + "futures", + "gethostname 1.1.0", + "governor", + "hex", + "html2text", + "http 1.3.1", + "imap-proto", + "itertools 0.14.0", + "itoa", + "lru 0.16.2", + "mail-parser", + "mail-send", + "mimalloc", + "mime_guess", + "murmur3", + "native_db", + "native_model", + "num_cpus", + "oauth2", + "openssl-sys", + "poem", + "poem-derive", + "poem-openapi", + "rand 0.9.2", + "regex", + "reqwest", + "ring", + "rust-embed", + "rustls", + "rustls-pki-types", + "semver 1.0.27", + "serde", + "serde_json", + "snafu", + "sysinfo", + "tantivy", + "tempfile", + "time 0.3.44", + "timeago", + "tokio", + "tokio-io-timeout", + "tokio-rustls", + "tokio-socks", + "tracing", + "tracing-appender", + "tracing-subscriber", + "url", + "urlencoding", + "utf7-imap", + "webpki-roots 1.0.4", +] + +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + +[[package]] +name = "bindgen" +version = "0.72.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" +dependencies = [ + "bitflags", + "cexpr", + "clang-sys", + "itertools 0.13.0", + "log", + "prettyplease", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", + "syn 2.0.100", +] + +[[package]] +name = "bitflags" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" + +[[package]] +name = "bitpacking" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c1d3e2bfd8d06048a179f7b17afc3188effa10385e7b00dc65af6aae732ea92" +dependencies = [ + "crunchy", +] + +[[package]] +name = "block-buffer" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "blocking" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "703f41c54fc768e63e091340b424302bb1c29ef4aa0c7f10fe849dfb114d29ea" +dependencies = [ + "async-channel 2.3.1", + "async-task", + "futures-io", + "futures-lite 2.3.0", + "piper", +] + +[[package]] +name = "bon" +version = "3.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebeb9aaf9329dff6ceb65c689ca3db33dbf15f324909c60e4e5eef5701ce31b1" +dependencies = [ + "bon-macros", + "rustversion", +] + +[[package]] +name = "bon-macros" +version = "3.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77e9d642a7e3a318e37c2c9427b5a6a48aa1ad55dcd986f3034ab2239045a645" +dependencies = [ + "darling 0.21.3", + "ident_case", + "prettyplease", + "proc-macro2", + "quote", + "rustversion", + "syn 2.0.100", +] + +[[package]] +name = "brotli" +version = "7.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc97b8f16f944bba54f0433f07e30be199b6dc2bd25937444bbad560bcea29bd" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor", +] + +[[package]] +name = "brotli-decompressor" +version = "4.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a334ef7c9e23abf0ce748e8cd309037da93e606ad52eb372e4ce327a0dcfbdfd" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", +] + +[[package]] +name = "bumpalo" +version = "3.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" + +[[package]] +name = "bytecount" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ce89b21cab1437276d2650d57e971f9d548a2d9037cc231abdc0562b97498ce" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e4cec68f03f32e44924783795810fa50a7035d8c8ebe78580ad7e6c703fba38" + +[[package]] +name = "bytes" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" + +[[package]] +name = "bzip2" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdb116a6ef3f6c3698828873ad02c3014b3c85cadb88496095628e3ef1e347f8" +dependencies = [ + "bzip2-sys", + "libc", +] + +[[package]] +name = "bzip2-sys" +version = "0.1.11+1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "736a955f3fa7875102d57c82b8cac37ec45224a07fd32d58f9f7a186b6cd4cdc" +dependencies = [ + "cc", + "libc", + "pkg-config", +] + +[[package]] +name = "cacache" +version = "13.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c5063741c7b2e260bbede781cf4679632dd90e2718e99f7715e46824b65670b" +dependencies = [ + "digest 0.10.7", + "either", + "futures", + "hex", + "libc", + "memmap2 0.5.10", + "miette", + "reflink-copy", + "serde", + "serde_derive", + "serde_json", + "sha1 0.10.6", + "sha2 0.10.9", + "ssri", + "tempfile", + "thiserror 1.0.68", + "tokio", + "tokio-stream", + "walkdir", +] + +[[package]] +name = "camino" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b96ec4966b5813e2c0507c1f86115c8c5abaadc3980879c3424042a02fd1ad3" +dependencies = [ + "serde", +] + +[[package]] +name = "cargo-platform" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24b1f0365a6c6bb4020cd05806fd0d33c44d38046b8bd7f0e40814b9763cabfc" +dependencies = [ + "serde", +] + +[[package]] +name = "cargo_metadata" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4acbb09d9ee8e23699b9634375c72795d095bf268439da88562cf9b501f181fa" +dependencies = [ + "camino", + "cargo-platform", + "semver 1.0.27", + "serde", + "serde_json", +] + +[[package]] +name = "cc" +version = "1.2.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1d05d92f4b1fd76aad469d46cdd858ca761576082cd37df81416691e50199fb" +dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "census" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f4c707c6a209cbe82d10abd08e1ea8995e9ea937d2550646e02798948992be0" + +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "chrono" +version = "0.4.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-link 0.2.1", +] + +[[package]] +name = "cipher" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12f8e7987cbd042a63249497f41aed09f8e65add917ea6566effbc56578d6801" +dependencies = [ + "generic-array", +] + +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + +[[package]] +name = "clang-sys" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" +dependencies = [ + "glob", + "libc", + "libloading", +] + +[[package]] +name = "clap" +version = "4.5.51" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c26d721170e0295f191a69bd9a1f93efcdb0aff38684b61ab5750468972e5f5" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.51" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75835f0c7bf681bfd05abe44e965760fea999a5286c6eb2d59883634fd02011a" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.49" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "clap_lex" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" + +[[package]] +name = "cmake" +version = "0.1.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7caa3f9de89ddbe2c607f4101924c5abec803763ae9534e4f4d7d8f84aa81f0" +dependencies = [ + "cc", +] + +[[package]] +name = "colorchoice" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0" + +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "const_fn" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f8a2ca5ac02d09563609681103aada9e1777d54fc57a5acd7a41404f9c93b6e" + +[[package]] +name = "constant_time_eq" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6" + +[[package]] +name = "cookie" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03a5d7b21829bc7b4bf4754a978a241ae54ea55a40f92bb20216e54096f4b951" +dependencies = [ + "aes-gcm", + "base64 0.13.1", + "hkdf", + "hmac 0.10.1", + "percent-encoding", + "rand 0.8.5", + "sha2 0.9.9", + "time 0.2.27", + "version_check", +] + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "608697df725056feaccfa42cffdaeeec3fccc4ffc38358ecd19b243e716a78e0" +dependencies = [ + "libc", +] + +[[package]] +name = "cpuid-bool" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcb25d077389e53838a8158c8e99174c5a9d902dee4904320db714f3c653ffba" + +[[package]] +name = "crc" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9710d3b3739c2e349eb44fe848ad0b7c8cb1e42bd87ee49371df2f7acaf3e675" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" + +[[package]] +name = "crc32fast" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "critical-section" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" + +[[package]] +name = "crossbeam-channel" +version = "0.5.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33480d6946193aa8033910124896ca395333cae7e2d1113d1fef6c3272217df2" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" + +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "crypto-mac" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4857fd85a0c34b3c3297875b747c1e02e06b6a0ea32dd892d8192b9ce0813ea6" +dependencies = [ + "generic-array", + "subtle", +] + +[[package]] +name = "ctr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb4a30d54f7443bf3d6191dcd486aca19e67cb3c49fa7a06a319966346707e7f" +dependencies = [ + "cipher 0.2.5", +] + +[[package]] +name = "curl" +version = "0.4.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9fb4d13a1be2b58f14d60adba57c9834b78c62fd86c3e76a148f732686e9265" +dependencies = [ + "curl-sys", + "libc", + "openssl-probe", + "openssl-sys", + "schannel", + "socket2 0.5.10", + "windows-sys 0.52.0", +] + +[[package]] +name = "curl-sys" +version = "0.4.80+curl-8.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55f7df2eac63200c3ab25bde3b2268ef2ee56af3d238e76d61f01c3c49bff734" +dependencies = [ + "cc", + "libc", + "libnghttp2-sys", + "libz-sys", + "openssl-sys", + "pkg-config", + "vcpkg", + "windows-sys 0.52.0", +] + +[[package]] +name = "darling" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f63b86c8a8826a49b8c21f08a2d07338eec8d900540f8630dc76284be802989" +dependencies = [ + "darling_core 0.20.10", + "darling_macro 0.20.10", +] + +[[package]] +name = "darling" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0" +dependencies = [ + "darling_core 0.21.3", + "darling_macro 0.21.3", +] + +[[package]] +name = "darling_core" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95133861a8032aaea082871032f5815eb9e98cef03fa916ab4500513994df9e5" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.100", +] + +[[package]] +name = "darling_core" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1247195ecd7e3c85f83c8d2a366e4210d588e802133e1e355180a9870b517ea4" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.100", +] + +[[package]] +name = "darling_macro" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" +dependencies = [ + "darling_core 0.20.10", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "darling_macro" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" +dependencies = [ + "darling_core 0.21.3", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "dashmap" +version = "6.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf" +dependencies = [ + "cfg-if", + "crossbeam-utils", + "hashbrown 0.14.5", + "lock_api", + "once_cell", + "parking_lot_core", +] + +[[package]] +name = "data-encoding" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8566979429cf69b49a5c740c60791108e86440e8be149bbea4fe54d2c32d6e2" + +[[package]] +name = "deflate64" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da692b8d1080ea3045efaab14434d40468c3d8657e42abddfffca87b428f4c1b" + +[[package]] +name = "deranged" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d630bccd429a5bb5a64b5e94f693bfc48c9f8566418fda4c494cc94f911f87cc" +dependencies = [ + "powerfmt", + "serde", +] + +[[package]] +name = "derive_arbitrary" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30542c1ad912e0e3d22a1935c290e12e8a29d704a420177a31faad4a601a0800" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "derive_more" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", + "unicode-xid", +] + +[[package]] +name = "digest" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" +dependencies = [ + "generic-array", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer 0.10.4", + "crypto-common", + "subtle", +] + +[[package]] +name = "discard" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "212d0f5754cb6769937f4501cc0e67f4f4483c8d2c3e1e922ee9edbe4ab4c7c0" + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "downcast-rs" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "117240f60069e65410b3ae1bb213295bd828f707b5bec6596a1afc8793ce0cbc" + +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + +[[package]] +name = "either" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" + +[[package]] +name = "email_address" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e079f19b08ca6239f47f8ba8509c11cf3ea30095831f7fed61441475edd8c449" +dependencies = [ + "serde", +] + +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "enum-as-inner" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9720bba047d567ffc8a3cba48bf19126600e249ab7f128e9233e6376976a116" +dependencies = [ + "heck 0.4.1", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "enum-as-inner" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1e6a265c649f3f5979b601d26f1d05ada116434c87741c9493cb56218f76cbc" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "errno" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + +[[package]] +name = "error-chain" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d2f06b9cac1506ece98fe3231e3cc9c4410ec3d5b1f24ae1c8946f0742cdefc" +dependencies = [ + "version_check", +] + +[[package]] +name = "event-listener" +version = "2.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" + +[[package]] +name = "event-listener" +version = "5.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6032be9bd27023a771701cc49f9f053c751055f71efb2e0ae5c15809093675ba" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f214dc438f977e6d4e3500aaa277f5ad94ca83fbbd9b1a15713ce2344ccc5a1" +dependencies = [ + "event-listener 5.3.1", + "pin-project-lite", +] + +[[package]] +name = "fastdivide" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afc2bd4d5a73106dd53d10d73d3401c2f32730ba2c0b93ddb888a8983680471" + +[[package]] +name = "fastrand" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" +dependencies = [ + "instant", +] + +[[package]] +name = "fastrand" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8c02a5121d4ea3eb16a80748c74f5549a5665e4c21333c6098f283870fbdea6" + +[[package]] +name = "find-msvc-tools" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0399f9d26e5191ce32c498bebd31e7a3ceabc2745f0ac54af3f335126c3f24b3" + +[[package]] +name = "flate2" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a3d7db9596fecd151c5f638c0ee5d5bd487b6e0ea232e5dc96d5250f6f94b1d" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "flume" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bebadab126f8120d410b677ed95eee4ba6eb7c6dd8e34a5ec88a08050e26132" +dependencies = [ + "futures-core", + "futures-sink", + "spinning_top 0.2.5", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f81ec6369c545a7d40e4589b5597581fa1c441fe1cce96dd1de43159910a36a2" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "fs4" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8640e34b88f7652208ce9e88b1a37a2ae95227d84abec377ccd3c5cfeb141ed4" +dependencies = [ + "rustix 1.0.3", + "windows-sys 0.59.0", +] + +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + +[[package]] +name = "futf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df420e2e84819663797d1ec6544b13c5be84629e7bb00dc960d6917db2987843" +dependencies = [ + "mac", + "new_debug_unreachable", +] + +[[package]] +name = "futures" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-executor" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-lite" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49a9d51ce47660b1e808d3c990b4709f2f415d928835a17dfd16991515c46bce" +dependencies = [ + "fastrand 1.9.0", + "futures-core", + "futures-io", + "memchr", + "parking", + "pin-project-lite", + "waker-fn", +] + +[[package]] +name = "futures-lite" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52527eb5074e35e9339c6b4e8d12600c7128b68fb25dcb9fa9dec18f7c25f3a5" +dependencies = [ + "fastrand 2.1.1", + "futures-core", + "futures-io", + "parking", + "pin-project-lite", +] + +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-timer" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "generator" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc6bd114ceda131d3b1d665eba35788690ad37f5916457286b32ab6fd3c438dd" +dependencies = [ + "cfg-if", + "libc", + "log", + "rustversion", + "windows 0.58.0", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "gethostname" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc3655aa6818d65bc620d6911f05aa7b6aeb596291e1e9f79e52df85583d1e30" +dependencies = [ + "rustix 0.38.37", + "windows-targets 0.52.6", +] + +[[package]] +name = "gethostname" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bd49230192a3797a9a4d6abe9b3eed6f7fa4c8a8a4947977c6f80025f92cbd8" +dependencies = [ + "rustix 1.0.3", + "windows-link 0.2.1", +] + +[[package]] +name = "getrandom" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.9.0+wasi-snapshot-preview1", +] + +[[package]] +name = "getrandom" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi 0.11.0+wasi-snapshot-preview1", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43a49c392881ce6d5c3b8cb70f98717b7c07aabbdff06687b9030dbfbe2725f8" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi 0.13.3+wasi-0.2.2", + "wasm-bindgen", + "windows-targets 0.52.6", +] + +[[package]] +name = "ghash" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97304e4cd182c3846f7575ced3890c53012ce534ad9114046b0a9e00bb30a375" +dependencies = [ + "opaque-debug", + "polyval", +] + +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + +[[package]] +name = "gloo-timers" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbb143cf96099802033e0d4f4963b19fd2e0b728bcf076cd9cf7f6634f092994" +dependencies = [ + "futures-channel", + "futures-core", + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "governor" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e23d5986fd4364c2fb7498523540618b4b8d92eec6c36a02e565f66748e2f79" +dependencies = [ + "cfg-if", + "dashmap", + "futures-sink", + "futures-timer", + "futures-util", + "getrandom 0.3.1", + "hashbrown 0.16.0", + "nonzero_ext", + "parking_lot", + "portable-atomic", + "quanta", + "rand 0.9.2", + "smallvec", + "spinning_top 0.3.0", + "web-time", +] + +[[package]] +name = "h2" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "524e8ac6999421f49a846c2d4411f337e53497d8ec55d67753beffa43c5d9205" +dependencies = [ + "atomic-waker", + "bytes 1.11.0", + "fnv", + "futures-core", + "futures-sink", + "http 1.3.1", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + +[[package]] +name = "hashbrown" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash 0.1.3", +] + +[[package]] +name = "hashbrown" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash 0.2.0", +] + +[[package]] +name = "hashify" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f208758247e68e239acaa059e72e4ce1f30f2a4b6523f19c1b923d25b7e9cceb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "headers" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "322106e6bd0cba2d5ead589ddb8150a13d7c4217cf80d7c4f682ca994ccc6aa9" +dependencies = [ + "base64 0.21.7", + "bytes 1.11.0", + "headers-core", + "http 1.3.1", + "httpdate", + "mime", + "sha1 0.10.6", +] + +[[package]] +name = "headers-core" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54b4a22553d4242c49fddb9ba998a99962b5cc6f22cb5a3482bec22522403ce4" +dependencies = [ + "http 1.3.1", +] + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hermit-abi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" + +[[package]] +name = "hermit-abi" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc" + +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hickory-proto" +version = "0.25.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8a6fe56c0038198998a6f217ca4e7ef3a5e51f46163bd6dd60b5c71ca6c6502" +dependencies = [ + "async-trait", + "bitflags", + "bytes 1.11.0", + "cfg-if", + "data-encoding", + "enum-as-inner 0.6.1", + "futures-channel", + "futures-io", + "futures-util", + "idna 1.1.0", + "ipnet", + "once_cell", + "rand 0.9.2", + "ring", + "rustls", + "rustls-pki-types", + "thiserror 2.0.12", + "time 0.3.44", + "tinyvec", + "tokio", + "tokio-rustls", + "tracing", + "url", +] + +[[package]] +name = "hickory-resolver" +version = "0.25.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc62a9a99b0bfb44d2ab95a7208ac952d31060efc16241c87eaf36406fecf87a" +dependencies = [ + "cfg-if", + "futures-util", + "hickory-proto", + "ipconfig", + "moka", + "once_cell", + "parking_lot", + "rand 0.9.2", + "resolv-conf", + "rustls", + "smallvec", + "thiserror 2.0.12", + "tokio", + "tokio-rustls", + "tracing", +] + +[[package]] +name = "hkdf" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51ab2f639c231793c5f6114bdb9bbe50a7dbbfcd7c7c6bd8475dec2d991e964f" +dependencies = [ + "digest 0.9.0", + "hmac 0.10.1", +] + +[[package]] +name = "hmac" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1441c6b1e930e2817404b5046f1f989899143a12bf92de603b69f4e0aee1e15" +dependencies = [ + "crypto-mac", + "digest 0.9.0", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest 0.10.7", +] + +[[package]] +name = "hostname" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c731c3e10504cc8ed35cfe2f1db4c9274c3d35fa486e3b31df46f068ef3e867" +dependencies = [ + "libc", + "match_cfg", + "winapi", +] + +[[package]] +name = "html2text" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ec3405a4163878f29357e1156eacd5c9d72803b62925f190c865d1cae8105d5" +dependencies = [ + "html5ever", + "tendril", + "thiserror 2.0.12", + "unicode-width 0.2.2", +] + +[[package]] +name = "html5ever" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55d958c2f74b664487a2035fe1dadb032c48718a03b63f3ab0b8537db8549ed4" +dependencies = [ + "log", + "markup5ever", + "match_token", +] + +[[package]] +name = "htmlescape" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9025058dae765dee5070ec375f591e2ba14638c63feff74f13805a72e523163" + +[[package]] +name = "http" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" +dependencies = [ + "bytes 1.11.0", + "fnv", + "itoa", +] + +[[package]] +name = "http" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" +dependencies = [ + "bytes 1.11.0", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes 1.11.0", + "http 1.3.1", +] + +[[package]] +name = "http-body-util" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f" +dependencies = [ + "bytes 1.11.0", + "futures-util", + "http 1.3.1", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "http-client" +version = "6.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1947510dc91e2bf586ea5ffb412caad7673264e14bb39fb9078da114a94ce1a5" +dependencies = [ + "async-std", + "async-trait", + "cfg-if", + "http-types", + "isahc", + "log", +] + +[[package]] +name = "http-types" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e9b187a72d63adbfba487f48095306ac823049cb504ee195541e91c7775f5ad" +dependencies = [ + "anyhow", + "async-channel 1.9.0", + "async-std", + "base64 0.13.1", + "cookie", + "futures-lite 1.13.0", + "infer", + "pin-project-lite", + "rand 0.7.3", + "serde", + "serde_json", + "serde_qs", + "serde_urlencoded", + "url", +] + +[[package]] +name = "httparse" +version = "1.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fcc0b4a115bf80b728eb8ea024ad5bd707b615bfed49e0665b6e0f86fd082d9" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hyper" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc2b571658e38e0c01b1fdca3bbbe93c00d3d71693ff2770043f8c29bc7d6f80" +dependencies = [ + "bytes 1.11.0", + "futures-channel", + "futures-util", + "h2", + "http 1.3.1", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08afdbb5c31130e3034af566421053ab03787c640246a446327f550d11bcb333" +dependencies = [ + "futures-util", + "http 1.3.1", + "hyper", + "hyper-util", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", + "webpki-roots 0.26.10", +] + +[[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes 1.11.0", + "http-body-util", + "hyper", + "hyper-util", + "native-tls", + "tokio", + "tokio-native-tls", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d9b05277c7e8da2c93a568989bb6207bef0112e8d17df7a6eda4a3cf143bc5e" +dependencies = [ + "base64 0.22.1", + "bytes 1.11.0", + "futures-channel", + "futures-core", + "futures-util", + "http 1.3.1", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2 0.6.0", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "hyperloglogplus" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "621debdf94dcac33e50475fdd76d34d5ea9c0362a834b9db08c3024696c1fbe3" +dependencies = [ + "serde", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core 0.52.0", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "icu_collections" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locid" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_locid_transform" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_locid_transform_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_locid_transform_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e" + +[[package]] +name = "icu_normalizer" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "utf16_iter", + "utf8_iter", + "write16", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516" + +[[package]] +name = "icu_properties" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locid_transform", + "icu_properties_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569" + +[[package]] +name = "icu_provider" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_provider_macros", + "stable_deref_trait", + "tinystr", + "writeable", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_provider_macros" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8" +dependencies = [ + "matches", + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "imap-proto" +version = "0.16.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba1f9b30846c3d04371159ef3a0413ce7c1ae0a8c619cd255c60b3d902553f22" +dependencies = [ + "nom", +] + +[[package]] +name = "indexmap" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68b900aa2f7301e21c36462b170ee99994de34dff39a4a6a528e80e7376d07e5" +dependencies = [ + "equivalent", + "hashbrown 0.14.5", +] + +[[package]] +name = "infer" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64e9829a50b42bb782c1df523f78d332fe371b10c661e78b7a3c34b0198e9fac" + +[[package]] +name = "inout" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5" +dependencies = [ + "generic-array", +] + +[[package]] +name = "instant" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "ipconfig" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b58db92f96b720de98181bbbe63c831e87005ab460c1bf306eb2622b4707997f" +dependencies = [ + "socket2 0.5.10", + "widestring", + "windows-sys 0.48.0", + "winreg", +] + +[[package]] +name = "ipnet" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "187674a687eed5fe42285b40c6291f9a01517d415fad1c3cbc6a9f778af7fcd4" + +[[package]] +name = "iri-string" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbc5ebe9c3a1a7a5127f920a418f7585e9e758e911d0466ed004f393b0e380b2" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + +[[package]] +name = "isahc" +version = "0.9.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2948a0ce43e2c2ef11d7edf6816508998d99e13badd1150be0914205df9388a" +dependencies = [ + "bytes 0.5.6", + "crossbeam-utils", + "curl", + "curl-sys", + "flume", + "futures-lite 1.13.0", + "http 0.2.12", + "log", + "once_cell", + "slab", + "sluice", + "tracing", + "tracing-futures", + "url", + "waker-fn", +] + +[[package]] +name = "isolang" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe50d48c77760c55188549098b9a7f6e37ae980c586a24693d6b01c3b2010c3c" +dependencies = [ + "phf", +] + +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + +[[package]] +name = "jobserver" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48d1dbcbbeb6a7fec7e059840aa538bd62aaccf972c7346c4d9d2059312853d0" +dependencies = [ + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "kv-log-macro" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de8b303297635ad57c9f5059fd9cee7a47f8e8daa09df0fcd07dd39fb22977f" +dependencies = [ + "log", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "levenshtein_automata" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c2cdeb66e45e9f36bfad5bbdb4d2384e70936afbee843c6f6543f0c551ebb25" + +[[package]] +name = "libc" +version = "0.2.174" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" + +[[package]] +name = "libloading" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07033963ba89ebaf1584d767badaa2e8fcec21aedea6b8c0346d487d49c28667" +dependencies = [ + "cfg-if", + "windows-targets 0.52.6", +] + +[[package]] +name = "libm" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" + +[[package]] +name = "libmimalloc-sys" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "667f4fec20f29dfc6bc7357c582d91796c169ad7e2fce709468aefeb2c099870" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "libnghttp2-sys" +version = "0.1.11+1.64.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b6c24e48a7167cffa7119da39d577fa482e66c688a4aac016bee862e1a713c4" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "libz-sys" +version = "1.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b70e7a7df205e92a1a4cd9aaae7898dac0aa555503cc0a649494d0d60e7651d" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "linked-hash-map" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" + +[[package]] +name = "linux-raw-sys" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" + +[[package]] +name = "linux-raw-sys" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe7db12097d22ec582439daf8618b8fdd1a7bef6270e9af3b1ebcd30893cf413" + +[[package]] +name = "litemap" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "643cb0b8d4fcc284004d5fd0d67ccf61dfffadb7f75e1e71bc420f4688a3a704" + +[[package]] +name = "lock_api" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "lockfree-object-pool" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9374ef4228402d4b7e403e5838cb880d9ee663314b0a900d5a6aabf0c213552e" + +[[package]] +name = "log" +version = "0.4.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" +dependencies = [ + "value-bag", +] + +[[package]] +name = "loom" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "419e0dc8046cb947daa77eb95ae174acfbddb7673b4151f56d1eed8e93fbfaca" +dependencies = [ + "cfg-if", + "generator", + "scoped-tls", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "lru" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" +dependencies = [ + "hashbrown 0.15.2", +] + +[[package]] +name = "lru" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96051b46fc183dc9cd4a223960ef37b9af631b55191852a8274bfef064cda20f" +dependencies = [ + "hashbrown 0.16.0", +] + +[[package]] +name = "lru-cache" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31e24f1ad8321ca0e8a1e0ac13f23cb668e6f5466c2c57319f6a5cf1cc8e3b1c" +dependencies = [ + "linked-hash-map", +] + +[[package]] +name = "lz4_flex" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08ab2867e3eeeca90e844d1940eab391c9dc5228783db2ed999acbc0a9ed375a" + +[[package]] +name = "lzma-rs" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "297e814c836ae64db86b36cf2a557ba54368d03f6afcd7d947c266692f71115e" +dependencies = [ + "byteorder", + "crc", +] + +[[package]] +name = "mac" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" + +[[package]] +name = "mail-auth" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "948946ae9bf1bb98f6101dad1a6b473f140eeb633e74d48c732464796924073b" +dependencies = [ + "ahash", + "flate2", + "hickory-resolver", + "mail-builder", + "mail-parser", + "quick-xml 0.37.2", + "quick_cache", + "ring", + "rustls-pemfile", + "serde", + "serde_json", + "zip", +] + +[[package]] +name = "mail-builder" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d75c72a0bf2070b4c2aa384f173439569a9e0e97293b90aad3469b6d9c183ec4" +dependencies = [ + "gethostname 0.5.0", +] + +[[package]] +name = "mail-parser" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcf4390741c4e6fa330bdeccdfb580815dbb462952de91838b723357985119a3" +dependencies = [ + "encoding_rs", + "hashify", + "serde", +] + +[[package]] +name = "mail-send" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "114a4e27f3cfaf8918783e8fa4149b820c813b1bedc7755e20e12eff4518331e" +dependencies = [ + "base64 0.22.1", + "gethostname 1.1.0", + "mail-auth", + "mail-builder", + "md5", + "rand 0.9.2", + "rustls", + "rustls-pki-types", + "smtp-proto", + "tokio", + "tokio-rustls", + "webpki-roots 1.0.4", +] + +[[package]] +name = "markup5ever" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "311fe69c934650f8f19652b3946075f0fc41ad8757dbb68f1ca14e7900ecc1c3" +dependencies = [ + "log", + "tendril", + "web_atoms", +] + +[[package]] +name = "match_cfg" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffbee8634e0d45d258acb448e7eaab3fce7a0a467395d4d9f228e3c1f01fb2e4" + +[[package]] +name = "match_token" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac84fd3f360fcc43dc5f5d186f02a94192761a080e8bc58621ad4d12296a58cf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + +[[package]] +name = "matches" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5" + +[[package]] +name = "md5" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae960838283323069879657ca3de837e9f7bbb4c7bf6ea7f1b290d5e9476d2e0" + +[[package]] +name = "measure_time" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51c55d61e72fc3ab704396c5fa16f4c184db37978ae4e94ca8959693a235fc0e" +dependencies = [ + "log", +] + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "memmap2" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83faa42c0a078c393f6b29d5db232d8be22776a891f8f56e5284faee4a20b327" +dependencies = [ + "libc", +] + +[[package]] +name = "memmap2" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843a98750cd611cc2965a8213b53b43e715f13c37a9e096c6408e69990961db7" +dependencies = [ + "libc", +] + +[[package]] +name = "miette" +version = "5.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59bb584eaeeab6bd0226ccf3509a69d7936d148cf3d036ad350abe35e8c6856e" +dependencies = [ + "miette-derive", + "once_cell", + "thiserror 1.0.68", + "unicode-width 0.1.14", +] + +[[package]] +name = "miette-derive" +version = "5.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49e7bc1560b95a3c4a25d03de42fe76ca718ab92d1a22a55b9b4cf67b3ae635c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "mimalloc" +version = "0.1.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1ee66a4b64c74f4ef288bcbb9192ad9c3feaad75193129ac8509af543894fd8" +dependencies = [ + "libmimalloc-sys", +] + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "mime_guess" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" +dependencies = [ + "mime", + "unicase", +] + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "miniz_oxide" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3be647b768db090acb35d5ec5db2b0e1f1de11133ca123b9eacf5137868f892a" +dependencies = [ + "adler2", +] + +[[package]] +name = "mio" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" +dependencies = [ + "hermit-abi 0.3.9", + "libc", + "wasi 0.11.0+wasi-snapshot-preview1", + "windows-sys 0.52.0", +] + +[[package]] +name = "moka" +version = "0.12.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9321642ca94a4282428e6ea4af8cc2ca4eac48ac7a6a4ea8f33f76d0ce70926" +dependencies = [ + "crossbeam-channel", + "crossbeam-epoch", + "crossbeam-utils", + "loom", + "parking_lot", + "portable-atomic", + "rustc_version 0.4.1", + "smallvec", + "tagptr", + "thiserror 1.0.68", + "uuid", +] + +[[package]] +name = "multer" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83e87776546dc87511aa5ee218730c92b666d7264ab6ed41f9d215af9cd5224b" +dependencies = [ + "bytes 1.11.0", + "encoding_rs", + "futures-util", + "http 1.3.1", + "httparse", + "memchr", + "mime", + "spin", + "tokio", + "version_check", +] + +[[package]] +name = "murmur3" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9252111cf132ba0929b6f8e030cac2a24b507f3a4d6db6fb2896f27b354c714b" + +[[package]] +name = "murmurhash32" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2195bf6aa996a481483b29d62a7663eed3fe39600c460e323f8ff41e90bdd89b" + +[[package]] +name = "native-tls" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "native_db" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b13e4a851c3b641728712a946c4cf2bf7eb8dbcfd2cbc35f7ba4b19ce59f4ac" +dependencies = [ + "native_db_macro", + "native_model", + "redb 1.5.1", + "redb 2.6.2", + "semver 1.0.27", + "serde", + "skeptic", + "thiserror 1.0.68", +] + +[[package]] +name = "native_db_macro" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0809fa4be7c67b5fcc5aea1b43644fa4bd4fa57f5a956a84018628fba51c2b11" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "native_model" +version = "0.4.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c10f4542302b7fa69ef18b49d93106e27f20b59d695555121d9ed22fe5d716a8" +dependencies = [ + "anyhow", + "bincode", + "native_model_macro", + "serde", + "skeptic", + "thiserror 1.0.68", + "zerocopy 0.8.25", +] + +[[package]] +name = "native_model_macro" +version = "0.4.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f385f3d57adaea8d8868e65a0bc821bcb8ba2228bbf87a1c3c6144ac48f3791" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "new_debug_unreachable" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" + +[[package]] +name = "nix" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" +dependencies = [ + "bitflags", + "cfg-if", + "cfg_aliases", + "libc", +] + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "nonzero_ext" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38bf9645c8b145698bb0b18a4637dcacbc421ea49bef2317e4fd8065a387cf21" + +[[package]] +name = "ntapi" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8a3895c6391c39d7fe7ebc444a87eb2991b2a0bc718fdabd071eec617fc68e4" +dependencies = [ + "winapi", +] + +[[package]] +name = "nu-ansi-term" +version = "0.50.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4a28e057d01f97e61255210fcff094d74ed0466038633e95017f5beb68e4399" +dependencies = [ + "windows-sys 0.52.0", +] + +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", + "libm", +] + +[[package]] +name = "num_cpus" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" +dependencies = [ + "hermit-abi 0.5.2", + "libc", +] + +[[package]] +name = "num_threads" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9" +dependencies = [ + "libc", +] + +[[package]] +name = "oauth2" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51e219e79014df21a225b1860a479e2dcd7cbd9130f4defd4bd0e191ea31d67d" +dependencies = [ + "base64 0.22.1", + "chrono", + "getrandom 0.2.15", + "http 1.3.1", + "rand 0.8.5", + "reqwest", + "serde", + "serde_json", + "serde_path_to_error", + "sha2 0.10.9", + "thiserror 1.0.68", + "url", +] + +[[package]] +name = "objc2-core-foundation" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c10c2894a6fed806ade6027bcd50662746363a9589d3ec9d9bef30a4e4bc166" +dependencies = [ + "bitflags", +] + +[[package]] +name = "objc2-io-kit" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71c1c64d6120e51cd86033f67176b1cb66780c2efe34dec55176f77befd93c0a" +dependencies = [ + "libc", + "objc2-core-foundation", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +dependencies = [ + "critical-section", + "portable-atomic", +] + +[[package]] +name = "oneshot" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ce411919553d3f9fa53a0880544cda985a112117a0444d5ff1e870a893d6ea" + +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + +[[package]] +name = "openssl" +version = "0.10.73" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8505734d46c8ab1e19a1dce3aef597ad87dcb4c37e7188231769bd6bd51cebf8" +dependencies = [ + "bitflags", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "openssl-probe" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" + +[[package]] +name = "openssl-src" +version = "300.5.1+3.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "735230c832b28c000e3bc117119e6466a663ec73506bc0a9907ea4187508e42a" +dependencies = [ + "cc", +] + +[[package]] +name = "openssl-sys" +version = "0.9.111" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82cab2d520aa75e3c58898289429321eb788c3106963d0dc886ec7a5f4adc321" +dependencies = [ + "cc", + "libc", + "openssl-src", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "ownedbytes" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fbd56f7631767e61784dc43f8580f403f4475bd4aaa4da003e6295e1bab4a7e" +dependencies = [ + "stable_deref_trait", +] + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + +[[package]] +name = "parking_lot" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets 0.52.6", +] + +[[package]] +name = "pbkdf2" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" +dependencies = [ + "digest 0.10.7", + "hmac 0.12.1", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "phf" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc" +dependencies = [ + "phf_shared", +] + +[[package]] +name = "phf_codegen" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a" +dependencies = [ + "phf_generator", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" +dependencies = [ + "phf_shared", + "rand 0.8.5", +] + +[[package]] +name = "phf_shared" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90fcb95eef784c2ac79119d1dd819e162b5da872ce6f3c3abe1e8ca1c082f72b" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pin-project" +version = "1.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be57f64e946e500c8ee36ef6331845d40a93055567ec57e8fae13efd33759b95" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c0f5fad0874fc7abcd4d750e76917eaebbecaa2c20bde22e1dbeeba8beb758c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "piper" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96c8c490f422ef9a4efd2cb5b42b76c8613d7e7dfc1caf667b8a3350a5acc066" +dependencies = [ + "atomic-waker", + "fastrand 2.1.1", + "futures-io", +] + +[[package]] +name = "pkg-config" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" + +[[package]] +name = "poem" +version = "3.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f977080932c87287147dca052951c3e2696f8759863f6b4e4c0c9ffe7a4cc8b" +dependencies = [ + "async-compression", + "bytes 1.11.0", + "futures-util", + "headers", + "hex", + "http 1.3.1", + "http-body-util", + "hyper", + "hyper-util", + "mime", + "mime_guess", + "multer", + "nix", + "parking_lot", + "percent-encoding", + "pin-project-lite", + "poem-derive", + "quick-xml 0.36.2", + "regex", + "rfc7239", + "rust-embed", + "rustls-pemfile", + "serde", + "serde_json", + "serde_urlencoded", + "serde_yaml", + "smallvec", + "sync_wrapper", + "tempfile", + "thiserror 2.0.12", + "tokio", + "tokio-rustls", + "tokio-stream", + "tokio-util", + "tracing", + "wildmatch", +] + +[[package]] +name = "poem-derive" +version = "3.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "056e2fea6de1cb240ffe23cfc4fc370b629f8be83b5f27e16b7acd5231a72de4" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "poem-openapi" +version = "5.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ccbcc395bf4dd03df1da32da351b6b6732e4074ce27ddec315650e52a2be44c" +dependencies = [ + "base64 0.22.1", + "bytes 1.11.0", + "derive_more", + "email_address", + "futures-util", + "indexmap", + "itertools 0.14.0", + "mime", + "num-traits", + "poem", + "poem-openapi-derive", + "quick-xml 0.36.2", + "regex", + "serde", + "serde_json", + "serde_urlencoded", + "serde_yaml", + "thiserror 2.0.12", + "tokio", +] + +[[package]] +name = "poem-openapi-derive" +version = "5.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3ddc21868273fd9159ea7e8b4e224c7b368577be53f23536b9dc9f37d285e8" +dependencies = [ + "darling 0.20.10", + "http 1.3.1", + "indexmap", + "mime", + "proc-macro-crate", + "proc-macro2", + "quote", + "regex", + "syn 2.0.100", + "thiserror 2.0.12", +] + +[[package]] +name = "polling" +version = "3.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc2790cd301dec6cd3b7a025e4815cf825724a51c98dccfe6a3e55f05ffb6511" +dependencies = [ + "cfg-if", + "concurrent-queue", + "hermit-abi 0.4.0", + "pin-project-lite", + "rustix 0.38.37", + "tracing", + "windows-sys 0.59.0", +] + +[[package]] +name = "polyval" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eebcc4aa140b9abd2bc40d9c3f7ccec842679cd79045ac3a7ac698c1a064b7cd" +dependencies = [ + "cpuid-bool", + "opaque-debug", + "universal-hash", +] + +[[package]] +name = "portable-atomic" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "280dc24453071f1b63954171985a0b0d30058d287960968b9b2aca264c8d4ee6" + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" +dependencies = [ + "zerocopy 0.7.35", +] + +[[package]] +name = "precomputed-hash" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" + +[[package]] +name = "prettyplease" +version = "0.2.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64d1ec885c64d0457d564db4ec299b2dae3f9c02808b8ad9c3a089c591b18033" +dependencies = [ + "proc-macro2", + "syn 2.0.100", +] + +[[package]] +name = "proc-macro-crate" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecf48c7ca261d60b74ab1a7b20da18bede46776b2e55535cb958eb595c5fa7b" +dependencies = [ + "toml_edit", +] + +[[package]] +name = "proc-macro-hack" +version = "0.5.20+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068" + +[[package]] +name = "proc-macro2" +version = "1.0.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "pulldown-cmark" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57206b407293d2bcd3af849ce869d52068623f19e1b5ff8e8778e3309439682b" +dependencies = [ + "bitflags", + "memchr", + "unicase", +] + +[[package]] +name = "quanta" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3bd1fe6824cea6538803de3ff1bc0cf3949024db3d43c9643024bfb33a807c0e" +dependencies = [ + "crossbeam-utils", + "libc", + "once_cell", + "raw-cpuid", + "wasi 0.11.0+wasi-snapshot-preview1", + "web-sys", + "winapi", +] + +[[package]] +name = "quick-error" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" + +[[package]] +name = "quick-xml" +version = "0.36.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7649a7b4df05aed9ea7ec6f628c67c9953a43869b8bc50929569b2999d443fe" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "quick-xml" +version = "0.37.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "165859e9e55f79d67b96c5d96f4e88b6f2695a1972849c15a6a3f5c59fc2c003" +dependencies = [ + "memchr", +] + +[[package]] +name = "quick_cache" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d7c94f8935a9df96bb6380e8592c70edf497a643f94bd23b2f76b399385dbf4" +dependencies = [ + "ahash", + "equivalent", + "hashbrown 0.14.5", + "parking_lot", +] + +[[package]] +name = "quinn" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c7c5fdde3cdae7203427dc4f0a68fe0ed09833edc525a03456b153b79828684" +dependencies = [ + "bytes 1.11.0", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2 0.5.10", + "thiserror 1.0.68", + "tokio", + "tracing", +] + +[[package]] +name = "quinn-proto" +version = "0.11.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fadfaed2cd7f389d0161bb73eeb07b7b78f8691047a6f3e73caaeae55310a4a6" +dependencies = [ + "bytes 1.11.0", + "rand 0.8.5", + "ring", + "rustc-hash", + "rustls", + "slab", + "thiserror 1.0.68", + "tinyvec", + "tracing", +] + +[[package]] +name = "quinn-udp" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fe68c2e9e1a1234e218683dbdf9f9dfcb094113c5ac2b938dfcb9bab4c4140b" +dependencies = [ + "libc", + "once_cell", + "socket2 0.5.10", + "tracing", + "windows-sys 0.59.0", +] + +[[package]] +name = "quote" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" +dependencies = [ + "getrandom 0.1.16", + "libc", + "rand_chacha 0.2.2", + "rand_core 0.5.1", + "rand_hc", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.0", +] + +[[package]] +name = "rand_chacha" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" +dependencies = [ + "ppv-lite86", + "rand_core 0.5.1", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.0", +] + +[[package]] +name = "rand_core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" +dependencies = [ + "getrandom 0.1.16", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.15", +] + +[[package]] +name = "rand_core" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b08f3c9802962f7e1b25113931d94f43ed9725bebc59db9d0c3e9a23b67e15ff" +dependencies = [ + "getrandom 0.3.1", + "zerocopy 0.8.25", +] + +[[package]] +name = "rand_distr" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32cb0b9bc82b0a0876c2dd994a7e7a2683d3e7390ca40e6886785ef0c7e3ee31" +dependencies = [ + "num-traits", + "rand 0.8.5", +] + +[[package]] +name = "rand_hc" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" +dependencies = [ + "rand_core 0.5.1", +] + +[[package]] +name = "raw-cpuid" +version = "11.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6df7ab838ed27997ba19a4664507e6f82b41fe6e20be42929332156e5e85146" +dependencies = [ + "bitflags", +] + +[[package]] +name = "rayon" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "redb" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd7f82ecd6ba647a39dd1a7172b8a1cd9453c0adee6da20cb553d83a9a460fa5" +dependencies = [ + "libc", +] + +[[package]] +name = "redb" +version = "2.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59b38b05028f398f08bea4691640503ec25fcb60b82fb61ce1f8fd1f4fccd3f7" +dependencies = [ + "libc", +] + +[[package]] +name = "redox_syscall" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62871f2d65009c0256aed1b9cfeeb8ac272833c404e13d53d400cd0dad7a2ac0" +dependencies = [ + "bitflags", +] + +[[package]] +name = "reflink-copy" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc31414597d1cd7fdd2422798b7652a6329dda0fe0219e6335a13d5bcaa9aeb6" +dependencies = [ + "cfg-if", + "rustix 0.38.37", + "windows 0.58.0", +] + +[[package]] +name = "regex" +version = "1.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" + +[[package]] +name = "reqwest" +version = "0.12.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d0946410b9f7b082a427e4ef5c8ff541a88b357bc6c637c40db3a68ac70a36f" +dependencies = [ + "base64 0.22.1", + "bytes 1.11.0", + "futures-channel", + "futures-core", + "futures-util", + "http 1.3.1", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-tls", + "hyper-util", + "js-sys", + "log", + "native-tls", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-native-tls", + "tokio-rustls", + "tokio-util", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", + "webpki-roots 1.0.4", +] + +[[package]] +name = "resolv-conf" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52e44394d2086d010551b14b53b1f24e31647570cd1deb0379e2c21b329aba00" +dependencies = [ + "hostname", + "quick-error", +] + +[[package]] +name = "rfc7239" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b106a85eeb5b0336d16d6a20eab857f92861d4fbb1eb9a239866fb98fb6a1063" +dependencies = [ + "uncased", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.15", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rust-embed" +version = "8.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "947d7f3fad52b283d261c4c99a084937e2fe492248cb9a68a8435a861b8798ca" +dependencies = [ + "rust-embed-impl", + "rust-embed-utils", + "walkdir", +] + +[[package]] +name = "rust-embed-impl" +version = "8.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fa2c8c9e8711e10f9c4fd2d64317ef13feaab820a4c51541f1a8c8e2e851ab2" +dependencies = [ + "proc-macro2", + "quote", + "rust-embed-utils", + "syn 2.0.100", + "walkdir", +] + +[[package]] +name = "rust-embed-utils" +version = "8.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60b161f275cb337fe0a44d924a5f4df0ed69c2c39519858f931ce61c779d3475" +dependencies = [ + "sha2 0.10.9", + "walkdir", +] + +[[package]] +name = "rust-stemmers" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e46a2036019fdb888131db7a4c847a1063a7493f971ed94ea82c67eada63ca54" +dependencies = [ + "serde", + "serde_derive", +] + +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + +[[package]] +name = "rustc_version" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a" +dependencies = [ + "semver 0.9.0", +] + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver 1.0.27", +] + +[[package]] +name = "rustix" +version = "0.38.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8acb788b847c24f28525660c4d7758620a7210875711f79e7f663cc152726811" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys 0.4.14", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustix" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e56a18552996ac8d29ecc3b190b4fdbb2d91ca4ec396de7bbffaf43f3d637e96" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys 0.9.3", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustls" +version = "0.23.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "533f54bc6a7d4f647e46ad909549eda97bf5afc1585190ef692b4286b198bd8f" +dependencies = [ + "aws-lc-rs", + "log", + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pemfile" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "196fe16b00e106300d3e45ecfcb764fa292a535d7326a29a5875c579c7417425" +dependencies = [ + "base64 0.22.1", + "rustls-pki-types", +] + +[[package]] +name = "rustls-pki-types" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94182ad936a0c91c324cd46c6511b9510ed16af436d7b5bab34beab0afd55f7a" +dependencies = [ + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10b3f4191e8a80e6b43eebabfac91e5dcecebb27a71f04e820c47ec41d314bf" +dependencies = [ + "aws-lc-rs", + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e819f2bc632f285be6d7cd36e25940d45b2391dd6d9b939e79de557f7014248" + +[[package]] +name = "ryu" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schannel" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9aaafd5a2b6e3d657ff009d82fbd630b6bd54dd4eb06f21693925cdf80f9b8b" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "scoped-tls" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "security-framework" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +dependencies = [ + "bitflags", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea4a292869320c0272d7bc55a5a6aafaff59b4f63404a003887b679a2e05b4b6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "self_cell" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d369a96f978623eb3dc28807c4852d6cc617fed53da5d3c400feff1ef34a714a" + +[[package]] +name = "semver" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403" +dependencies = [ + "semver-parser", +] + +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" +dependencies = [ + "serde", + "serde_core", +] + +[[package]] +name = "semver-parser" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde-xml-rs" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb3aa78ecda1ebc9ec9847d5d3aba7d618823446a049ba2491940506da6e2782" +dependencies = [ + "log", + "serde", + "thiserror 1.0.68", + "xml-rs", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "serde_json" +version = "1.0.145" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", + "serde_core", +] + +[[package]] +name = "serde_path_to_error" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af99884400da37c88f5e9146b7f1fd0fbcae8f6eec4e9da38b67d05486f814a6" +dependencies = [ + "itoa", + "serde", +] + +[[package]] +name = "serde_qs" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7715380eec75f029a4ef7de39a9200e0a63823176b759d055b613f5a87df6a6" +dependencies = [ + "percent-encoding", + "serde", + "thiserror 1.0.68", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_yaml" +version = "0.9.34+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" +dependencies = [ + "indexmap", + "itoa", + "ryu", + "serde", + "unsafe-libyaml", +] + +[[package]] +name = "sha-1" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5058ada175748e33390e40e872bd0fe59a19f265d0158daa551c5a88a76009c" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest 0.10.7", +] + +[[package]] +name = "sha1" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1da05c97445caa12d05e848c4a4fcbbea29e748ac28f7e80e9b010392063770" +dependencies = [ + "sha1_smol", +] + +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest 0.10.7", +] + +[[package]] +name = "sha1_smol" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbfa15b3dddfee50a0fff136974b3e1bde555604ba463834a7eb7deb6417705d" + +[[package]] +name = "sha2" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800" +dependencies = [ + "block-buffer 0.9.0", + "cfg-if", + "cpufeatures", + "digest 0.9.0", + "opaque-debug", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest 0.10.7", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook-registry" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" +dependencies = [ + "libc", +] + +[[package]] +name = "simd-adler32" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" + +[[package]] +name = "siphasher" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" + +[[package]] +name = "skeptic" +version = "0.13.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16d23b015676c90a0f01c197bfdc786c20342c73a0afdda9025adb0bc42940a8" +dependencies = [ + "bytecount", + "cargo_metadata", + "error-chain", + "glob", + "pulldown-cmark", + "tempfile", + "walkdir", +] + +[[package]] +name = "sketches-ddsketch" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1e9a774a6c28142ac54bb25d25562e6bcf957493a184f15ad4eebccb23e410a" +dependencies = [ + "serde", +] + +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + +[[package]] +name = "sluice" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d7400c0eff44aa2fcb5e31a5f24ba9716ed90138769e4977a2ba6014ae63eb5" +dependencies = [ + "async-channel 1.9.0", + "futures-core", + "futures-io", +] + +[[package]] +name = "smallvec" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" + +[[package]] +name = "smtp-proto" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d55cc1c74d3b758d7dd1fa4dc4cf694cad2732cac14f304228477c2b0ce6233a" + +[[package]] +name = "snafu" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e84b3f4eacbf3a1ce05eac6763b4d629d60cbc94d632e4092c54ade71f1e1a2" +dependencies = [ + "snafu-derive", +] + +[[package]] +name = "snafu-derive" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1c97747dbf44bb1ca44a561ece23508e99cb592e862f22222dcf42f51d1e451" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "socket2" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "socket2" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" + +[[package]] +name = "spinning_top" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b9eb1a2f4c41445a3a0ff9abc5221c5fcd28e1f13cd7c0397706f9ac938ddb0" +dependencies = [ + "lock_api", +] + +[[package]] +name = "spinning_top" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d96d2d1d716fb500937168cc09353ffdc7a012be8475ac7308e1bdf0e3923300" +dependencies = [ + "lock_api", +] + +[[package]] +name = "ssri" +version = "9.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da7a2b3c2bc9693bcb40870c4e9b5bf0d79f9cb46273321bf855ec513e919082" +dependencies = [ + "base64 0.21.7", + "digest 0.10.7", + "hex", + "miette", + "serde", + "sha-1", + "sha2 0.10.9", + "thiserror 1.0.68", + "xxhash-rust", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + +[[package]] +name = "standback" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e113fb6f3de07a243d434a56ec6f186dfd51cb08448239fe7bcae73f87ff28ff" +dependencies = [ + "version_check", +] + +[[package]] +name = "stdweb" +version = "0.4.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d022496b16281348b52d0e30ae99e01a73d737b2f45d38fed4edf79f9325a1d5" +dependencies = [ + "discard", + "rustc_version 0.2.3", + "stdweb-derive", + "stdweb-internal-macros", + "stdweb-internal-runtime", + "wasm-bindgen", +] + +[[package]] +name = "stdweb-derive" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c87a60a40fccc84bef0652345bbbbbe20a605bf5d0ce81719fc476f5c03b50ef" +dependencies = [ + "proc-macro2", + "quote", + "serde", + "serde_derive", + "syn 1.0.109", +] + +[[package]] +name = "stdweb-internal-macros" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58fa5ff6ad0d98d1ffa8cb115892b6e69d67799f6763e162a1c9db421dc22e11" +dependencies = [ + "base-x", + "proc-macro2", + "quote", + "serde", + "serde_derive", + "serde_json", + "sha1 0.6.1", + "syn 1.0.109", +] + +[[package]] +name = "stdweb-internal-runtime" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "213701ba3370744dcd1a12960caa4843b3d68b4d1c0a5d575e0d65b2ee9d16c0" + +[[package]] +name = "stop-token" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af91f480ee899ab2d9f8435bfdfc14d08a5754bd9d3fef1f1a1c23336aad6c8b" +dependencies = [ + "async-channel 1.9.0", + "cfg-if", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "string_cache" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf776ba3fa74f83bf4b63c3dcbbf82173db2632ed8452cb2d891d33f459de70f" +dependencies = [ + "new_debug_unreachable", + "parking_lot", + "phf_shared", + "precomputed-hash", + "serde", +] + +[[package]] +name = "string_cache_codegen" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c711928715f1fe0fe509c53b43e993a9a557babc2d0a3567d0a3006f1ac931a0" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "surf" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "718b1ae6b50351982dedff021db0def601677f2120938b070eadb10ba4038dd7" +dependencies = [ + "async-std", + "async-trait", + "cfg-if", + "encoding_rs", + "futures-util", + "getrandom 0.2.15", + "http-client", + "http-types", + "log", + "mime_guess", + "once_cell", + "pin-project-lite", + "serde", + "serde_json", + "web-sys", +] + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "sysinfo" +version = "0.37.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16607d5caffd1c07ce073528f9ed972d88db15dd44023fa57142963be3feb11f" +dependencies = [ + "libc", + "memchr", + "ntapi", + "objc2-core-foundation", + "objc2-io-kit", + "windows 0.60.0", +] + +[[package]] +name = "tagptr" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417" + +[[package]] +name = "tantivy" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "502915c7381c5cb2d2781503962610cb880ad8f1a0ca95df1bae645d5ebf2545" +dependencies = [ + "aho-corasick", + "arc-swap", + "base64 0.22.1", + "bitpacking", + "bon", + "byteorder", + "census", + "crc32fast", + "crossbeam-channel", + "downcast-rs", + "fastdivide", + "fnv", + "fs4", + "futures-channel", + "futures-util", + "htmlescape", + "hyperloglogplus", + "itertools 0.14.0", + "levenshtein_automata", + "log", + "lru 0.12.5", + "lz4_flex", + "measure_time", + "memmap2 0.9.8", + "once_cell", + "oneshot", + "rayon", + "regex", + "rust-stemmers", + "rustc-hash", + "serde", + "serde_json", + "sketches-ddsketch", + "smallvec", + "tantivy-bitpacker", + "tantivy-columnar", + "tantivy-common", + "tantivy-fst", + "tantivy-query-grammar", + "tantivy-sstable", + "tantivy-stacker", + "tantivy-tokenizer-api", + "tempfile", + "thiserror 2.0.12", + "time 0.3.44", + "uuid", + "winapi", + "zstd", +] + +[[package]] +name = "tantivy-bitpacker" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3b04eed5108d8283607da6710fe17a7663523440eaf7ea5a1a440d19a1448b6" +dependencies = [ + "bitpacking", +] + +[[package]] +name = "tantivy-columnar" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b628488ae936c83e92b5c4056833054ca56f76c0e616aee8339e24ac89119cd" +dependencies = [ + "downcast-rs", + "fastdivide", + "itertools 0.14.0", + "serde", + "tantivy-bitpacker", + "tantivy-common", + "tantivy-sstable", + "tantivy-stacker", +] + +[[package]] +name = "tantivy-common" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f880aa7cab0c063a47b62596d10991cdd0b6e0e0575d9c5eeb298b307a25de55" +dependencies = [ + "async-trait", + "byteorder", + "ownedbytes", + "serde", + "time 0.3.44", +] + +[[package]] +name = "tantivy-fst" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d60769b80ad7953d8a7b2c70cdfe722bbcdcac6bccc8ac934c40c034d866fc18" +dependencies = [ + "byteorder", + "regex-syntax", + "utf8-ranges", +] + +[[package]] +name = "tantivy-query-grammar" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "768fccdc84d60d86235d42d7e4c33acf43c418258ff5952abf07bd7837fcd26b" +dependencies = [ + "nom", + "serde", + "serde_json", +] + +[[package]] +name = "tantivy-sstable" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8292095d1a8a2c2b36380ec455f910ab52dde516af36321af332c93f20ab7d5" +dependencies = [ + "futures-util", + "itertools 0.14.0", + "tantivy-bitpacker", + "tantivy-common", + "tantivy-fst", + "zstd", +] + +[[package]] +name = "tantivy-stacker" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23d38a379411169f0b3002c9cba61cdfe315f757e9d4f239c00c282497a0749d" +dependencies = [ + "murmurhash32", + "rand_distr", + "tantivy-common", +] + +[[package]] +name = "tantivy-tokenizer-api" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23024f6aeb25ceb1a0e27740c84bdb0fae52626737b7e9a9de6ad5aa25c7b038" +dependencies = [ + "serde", +] + +[[package]] +name = "tempfile" +version = "3.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" +dependencies = [ + "fastrand 2.1.1", + "getrandom 0.3.1", + "once_cell", + "rustix 1.0.3", + "windows-sys 0.61.1", +] + +[[package]] +name = "tendril" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d24a120c5fc464a3458240ee02c299ebcb9d67b5249c8848b09d639dca8d7bb0" +dependencies = [ + "futf", + "mac", + "utf-8", +] + +[[package]] +name = "thiserror" +version = "1.0.68" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02dd99dc800bbb97186339685293e1cc5d9df1f8fae2d0aecd9ff1c77efea892" +dependencies = [ + "thiserror-impl 1.0.68", +] + +[[package]] +name = "thiserror" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" +dependencies = [ + "thiserror-impl 2.0.12", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.68" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7c61ec9a6f64d2793d8a45faba21efbe3ced62a886d44c36a009b2b519b4c7e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "thread_local" +version = "1.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" +dependencies = [ + "cfg-if", + "once_cell", +] + +[[package]] +name = "time" +version = "0.2.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4752a97f8eebd6854ff91f1c1824cd6160626ac4bd44287f7f4ea2035a02a242" +dependencies = [ + "const_fn", + "libc", + "standback", + "stdweb", + "time-macros 0.1.1", + "version_check", + "winapi", +] + +[[package]] +name = "time" +version = "0.3.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d" +dependencies = [ + "deranged", + "itoa", + "libc", + "num-conv", + "num_threads", + "powerfmt", + "serde", + "time-core", + "time-macros 0.2.24", +] + +[[package]] +name = "time-core" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b" + +[[package]] +name = "time-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "957e9c6e26f12cb6d0dd7fc776bb67a706312e7299aed74c8dd5b17ebb27e2f1" +dependencies = [ + "proc-macro-hack", + "time-macros-impl", +] + +[[package]] +name = "time-macros" +version = "0.2.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "time-macros-impl" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd3c141a1b43194f3f56a1411225df8646c55781d5f26db825b3d98507eb482f" +dependencies = [ + "proc-macro-hack", + "proc-macro2", + "quote", + "standback", + "syn 1.0.109", +] + +[[package]] +name = "timeago" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05030782ebd7d1295cce15a98c8805de6e70776c95f8e3468f84f7f198824f49" +dependencies = [ + "chrono", + "isolang", +] + +[[package]] +name = "tinystr" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "445e881f4f6d382d5f27c034e25eb92edd7c784ceab92a0937db7f2e9471b938" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408" +dependencies = [ + "bytes 1.11.0", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2 0.6.0", + "tokio-macros", + "windows-sys 0.61.1", +] + +[[package]] +name = "tokio-io-timeout" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bd86198d9ee903fedd2f9a2e72014287c0d9167e4ae43b5853007205dda1b76" +dependencies = [ + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-macros" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-socks" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d4770b8024672c1101b3f6733eab95b18007dbe0847a8afe341fcf79e06043f" +dependencies = [ + "either", + "futures-util", + "thiserror 1.0.68", + "tokio", +] + +[[package]] +name = "tokio-stream" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14307c986784f72ef81c89db7d9e28d6ac26d16213b109ea501696195e6e3ce5" +dependencies = [ + "bytes 1.11.0", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml_datetime" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" + +[[package]] +name = "toml_edit" +version = "0.22.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5" +dependencies = [ + "indexmap", + "toml_datetime", + "winnow", +] + +[[package]] +name = "tower" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" +dependencies = [ + "bitflags", + "bytes 1.11.0", + "futures-util", + "http 1.3.1", + "http-body", + "iri-string", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-appender" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3566e8ce28cc0a3fe42519fc80e6b4c943cc4c8cef275620eb8dac2d3d4e06cf" +dependencies = [ + "crossbeam-channel", + "thiserror 1.0.68", + "time 0.3.44", + "tracing-subscriber", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "tracing-core" +version = "0.1.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-futures" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97d095ae15e245a057c8e8451bab9b3ee1e1f68e9ba2b4fbc18d0ac5237835f2" +dependencies = [ + "pin-project", + "tracing", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-serde" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "704b1aeb7be0d0a84fc9828cae51dab5970fee5088f83d1dd7ee6f6246fc6ff1" +dependencies = [ + "serde", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2054a14f5307d601f88daf0553e1cbf472acc4f2c51afab632431cdcd72124d5" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex-automata", + "serde", + "serde_json", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", + "tracing-serde", +] + +[[package]] +name = "trust-dns-proto" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f7f83d1e4a0e4358ac54c5c3681e5d7da5efc5a7a632c90bb6d6669ddd9bc26" +dependencies = [ + "async-trait", + "cfg-if", + "data-encoding", + "enum-as-inner 0.5.1", + "futures-channel", + "futures-io", + "futures-util", + "idna 0.2.3", + "ipnet", + "lazy_static", + "rand 0.8.5", + "smallvec", + "thiserror 1.0.68", + "tinyvec", + "tokio", + "tracing", + "url", +] + +[[package]] +name = "trust-dns-resolver" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aff21aa4dcefb0a1afbfac26deb0adc93888c7d295fb63ab273ef276ba2b7cfe" +dependencies = [ + "cfg-if", + "futures-util", + "ipconfig", + "lazy_static", + "lru-cache", + "parking_lot", + "resolv-conf", + "smallvec", + "thiserror 1.0.68", + "tokio", + "tracing", + "trust-dns-proto", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "typenum" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" + +[[package]] +name = "uncased" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1b88fcfe09e89d3866a5c11019378088af2d24c3fbd4f0543f96b479ec90697" +dependencies = [ + "version_check", +] + +[[package]] +name = "unicase" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7d2d4dafb69621809a81864c9c1b864479e1235c0dd4e199924b9742439ed89" +dependencies = [ + "version_check", +] + +[[package]] +name = "unicode-bidi" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" + +[[package]] +name = "unicode-ident" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" + +[[package]] +name = "unicode-normalization" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5033c97c4262335cded6d6fc3e5c18ab755e1a3dc96376350f3d8e9f009ad956" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-width" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" + +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "universal-hash" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8326b2c654932e3e4f9196e69d08fdf7cfd718e1dc6f66b347e6024a0c961402" +dependencies = [ + "generic-array", + "subtle", +] + +[[package]] +name = "unsafe-libyaml" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" +dependencies = [ + "form_urlencoded", + "idna 1.1.0", + "percent-encoding", + "serde", +] + +[[package]] +name = "urlencoding" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + +[[package]] +name = "utf16_iter" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" + +[[package]] +name = "utf7-imap" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e326365261fc2761f0809dfb6032810534a0427bbd8f0edf546f6afeef89f5d" +dependencies = [ + "base64 0.13.1", + "encoding_rs", + "regex", +] + +[[package]] +name = "utf8-ranges" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fcfc827f90e53a02eaef5e535ee14266c1d569214c6aa70133a624d8a3164ba" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "uuid" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8c5f0a0af699448548ad1a2fbf920fb4bee257eae39953ba95cb84891a0446a" +dependencies = [ + "getrandom 0.2.15", + "serde", +] + +[[package]] +name = "valuable" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" + +[[package]] +name = "value-bag" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ef4c4aa54d5d05a279399bfa921ec387b7aba77caf7a682ae8d86785b8fdad2" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "waker-fn" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "317211a0dc0ceedd78fb2ca9a44aed3d7b9b26f81870d485c07122b4350673b7" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.9.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasi" +version = "0.13.3+wasi-0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26816d2e1a4a36a2940b96c5296ce403917633dff8f3440e9b236ed6f6bacad2" +dependencies = [ + "wit-bindgen-rt", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" +dependencies = [ + "bumpalo", + "log", + "proc-macro2", + "quote", + "syn 2.0.100", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61e9300f63a621e96ed275155c108eb6f843b6a26d053f122ab69724559dc8ed" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-streams" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b65dc4c90b63b118468cf747d8bf3566c1913ef60be765b5730ead9e0a3ba129" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "web-sys" +version = "0.3.70" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26fdeaafd9bd129f65e7c031593c24d62186301e0c72c8978fa1678be7d532c0" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web_atoms" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57ffde1dc01240bdf9992e3205668b235e59421fd085e8a317ed98da0178d414" +dependencies = [ + "phf", + "phf_codegen", + "string_cache", + "string_cache_codegen", +] + +[[package]] +name = "webpki-roots" +version = "0.26.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37493cadf42a2a939ed404698ded7fb378bf301b5011f973361779a3a74f8c93" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "webpki-roots" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2878ef029c47c6e8cf779119f20fcf52bde7ad42a731b2a304bc221df17571e" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "widestring" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7219d36b6eac893fa81e84ebe06485e7dcbb616177469b142df14f1f4deb1311" + +[[package]] +name = "wildmatch" +version = "2.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3928939971918220fed093266b809d1ee4ec6c1a2d72692ff6876898f3b16c19" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd04d41d93c4992d421894c18c8b43496aa748dd4c081bac0dc93eb0489272b6" +dependencies = [ + "windows-core 0.58.0", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows" +version = "0.60.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddf874e74c7a99773e62b1c671427abf01a425e77c3d3fb9fb1e4883ea934529" +dependencies = [ + "windows-collections", + "windows-core 0.60.1", + "windows-future", + "windows-link 0.1.0", + "windows-numerics", +] + +[[package]] +name = "windows-collections" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5467f79cc1ba3f52ebb2ed41dbb459b8e7db636cc3429458d9a852e15bc24dec" +dependencies = [ + "windows-core 0.60.1", +] + +[[package]] +name = "windows-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-core" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ba6d44ec8c2591c134257ce647b7ea6b20335bf6379a27dac5f1641fcf59f99" +dependencies = [ + "windows-implement 0.58.0", + "windows-interface 0.58.0", + "windows-result 0.2.0", + "windows-strings 0.1.0", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-core" +version = "0.60.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca21a92a9cae9bf4ccae5cf8368dce0837100ddf6e6d57936749e85f152f6247" +dependencies = [ + "windows-implement 0.59.0", + "windows-interface 0.59.1", + "windows-link 0.1.0", + "windows-result 0.3.1", + "windows-strings 0.3.1", +] + +[[package]] +name = "windows-future" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a787db4595e7eb80239b74ce8babfb1363d8e343ab072f2ffe901400c03349f0" +dependencies = [ + "windows-core 0.60.1", + "windows-link 0.1.0", +] + +[[package]] +name = "windows-implement" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "windows-implement" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83577b051e2f49a058c308f17f273b570a6a758386fc291b5f6a934dd84e48c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "windows-interface" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "windows-interface" +version = "0.59.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "windows-link" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dccfd733ce2b1753b03b6d3c65edf020262ea35e20ccdf3e288043e6dd620e3" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-numerics" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "005dea54e2f6499f2cee279b8f703b3cf3b5734a2d8d21867c8f44003182eeed" +dependencies = [ + "windows-core 0.60.1", + "windows-link 0.1.0", +] + +[[package]] +name = "windows-result" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-result" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06374efe858fab7e4f881500e6e86ec8bc28f9462c47e5a9941a0142ad86b189" +dependencies = [ + "windows-link 0.1.0", +] + +[[package]] +name = "windows-strings" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" +dependencies = [ + "windows-result 0.2.0", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-strings" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87fa48cc5d406560701792be122a10132491cff9d0aeb23583cc2dcafc847319" +dependencies = [ + "windows-link 0.1.0", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.61.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f109e41dd4a3c848907eb83d5a42ea98b3769495597450cf6d153507b166f0f" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "winnow" +version = "0.6.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c52ac009d615e79296318c1bcce2d422aaca15ad08515e344feeda07df67a587" +dependencies = [ + "memchr", +] + +[[package]] +name = "winreg" +version = "0.50.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" +dependencies = [ + "cfg-if", + "windows-sys 0.48.0", +] + +[[package]] +name = "wit-bindgen-rt" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3268f3d866458b787f390cf61f4bbb563b922d091359f9608842999eaee3943c" +dependencies = [ + "bitflags", +] + +[[package]] +name = "write16" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" + +[[package]] +name = "writeable" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" + +[[package]] +name = "xml-rs" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af4e2e2f7cba5a093896c1e150fbfe177d1883e7448200efb81d40b9d339ef26" + +[[package]] +name = "xxhash-rust" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a5cbf750400958819fb6178eaa83bee5cd9c29a26a40cc241df8c70fdd46984" + +[[package]] +name = "yoke" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c5b1314b079b0930c31e3af543d8ee1757b1951ae1e1565ec704403a7240ca5" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28cc31741b18cb6f1d5ff12f5b7523e3d6eb0852bbbad19d73905511d9849b95" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +dependencies = [ + "byteorder", + "zerocopy-derive 0.7.35", +] + +[[package]] +name = "zerocopy" +version = "0.8.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1702d9583232ddb9174e01bb7c15a2ab8fb1bc6f227aa1233858c351a3ba0cb" +dependencies = [ + "zerocopy-derive 0.8.25", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28a6e20d751156648aa063f3800b706ee209a32c0b4d9f24be3d980b01be55ef" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "zerofrom" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91ec111ce797d0e0784a1116d0ddcdbea84322cd79e5d5ad173daeba4f93ab55" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ea7b4a3637ea8669cedf0f1fd5c286a17f3de97b8dd5a70a6c167a1730e63a5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "zerovec" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "zip" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc5e4288ea4057ae23afc69a4472434a87a2495cafce6632fd1c4ec9f5cf3494" +dependencies = [ + "aes 0.8.4", + "arbitrary", + "bzip2", + "constant_time_eq", + "crc32fast", + "crossbeam-utils", + "deflate64", + "displaydoc", + "flate2", + "hmac 0.12.1", + "indexmap", + "lzma-rs", + "memchr", + "pbkdf2", + "rand 0.8.5", + "sha1 0.10.6", + "thiserror 1.0.68", + "time 0.3.44", + "zeroize", + "zopfli", + "zstd", +] + +[[package]] +name = "zopfli" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5019f391bac5cf252e93bbcc53d039ffd62c7bfb7c150414d61369afe57e946" +dependencies = [ + "bumpalo", + "crc32fast", + "lockfree-object-pool", + "log", + "once_cell", + "simd-adler32", +] + +[[package]] +name = "zstd" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcf2b778a664581e31e389454a7072dab1647606d44f7feea22cd5abb9c9f3f9" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "7.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54a3ab4db68cea366acc5c897c7b4d4d1b8994a9cd6e6f841f8964566a419059" +dependencies = [ + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.13+zstd.1.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38ff0f21cfee8f97d94cef41359e0c89aa6113028ab0291aa8ca0038995a95aa" +dependencies = [ + "cc", + "pkg-config", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..4313ade --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,114 @@ +[package] +name = "bichon" +version = "0.0.1" +edition = "2021" + +[[bin]] +name = "bichon" +path = "src/main.rs" + + +[features] +default = [] +vendored-openssl = ["openssl-sys"] + +[profile.release] +strip = true +lto = true +opt-level = 3 +codegen-units = 1 + +[dependencies] +chrono = "0.4.42" +clap = { version = "4.5.51", features = ["derive", "env"] } +mimalloc = "0.1.48" +native_db = "0.8.2" +itertools = "0.14.0" +native_model = "0.4.20" +poem = { version = "3.1.12", features = ["embed", "compression", "rustls"] } +poem-derive = "3.1.12" +poem-openapi = { version = "5.1.16", features = [ + "openapi-explorer", + "rapidoc", + "scalar", + "redoc", + "swagger-ui", + "email", +] } +ring = { version = "0.17.14", features = ["std"] } +serde = { version = "1.0.228", features = ["derive"] } +serde_json = "1.0.145" +tokio = { version = "1.48.0", features = ["full"] } +tracing = "0.1.41" +tracing-appender = "0.2.3" +tracing-subscriber = { version = "0.3.20", features = ["env-filter", "json"] } +base64 = "0.22.1" +snafu = "0.8.9" +reqwest = { version = "0.12.24", default-features = false, features = [ + "json", + "stream", + "native-tls", + # "rustls-tls", + "blocking", + "socks", +] } +tokio-socks = "0.5.2" +http = "1.3.1" +regex = "1.12.2" +email_address = "0.2.9" +futures = "0.3.31" +utf7-imap = "0.3.2" +imap-proto = "0.16.6" +mail-parser = { version = '0.11.1', features = ["serde"] } +mail-send = "0.5.2" +tokio-rustls = { version = "0.26.4", default-features = false, features = [ + "ring", + "tls12", +] } +timeago = "0.5.0" +ahash = "0.8.12" +oauth2 = { version = "5.0.0", features = ["reqwest-blocking"] } +url = { version = "2.5.7", features = ["serde"] } +sysinfo = "0.37.2" +num_cpus = "1.17.0" +cacache = { version = "13.1.0", default-features = false, features = [ + "tokio-runtime", + "mmap", +] } +rand = "0.9.2" +encoding_rs = "0.8.35" +async-imap = { version = "0.11.1", default-features = false, features = [ + "runtime-tokio", + "compress", +] } +webpki-roots = "1.0.4" +rustls = { version = "0.23.35", default-features = false, features = ["ring"] } +rustls-pki-types = "1.13.0" +tokio-io-timeout = "1.2.1" +bb8 = "0.9.0" +semver = "1.0.27" +governor = "0.10.2" +lru = "0.16.2" +mime_guess = "2.0.5" +hex = "0.4.3" +time = { version = "0.3.44", features = [ + "formatting", + "parsing", + "local-offset", +] } +rust-embed = "8.9.0" +murmur3 = "0.5.2" +autoconfig = "0.4.0" +urlencoding = "2.1.3" +dashmap = "6.1.0" +# Statically links OpenSSL by compiling from source, avoiding system library dependencies +openssl-sys = { version = "0.9.111", optional = true, features = ["vendored"] } +gethostname = "1.1.0" +tantivy = { version = "0.25.0", features = ["quickwit", "zstd-compression"] } +itoa = "1.0.15" +html2text = "0.16.2" +bytes = "1.11.0" +[dev-dependencies] +#bincode = "1.3.3" +#secret-lib = "1.0.0" +tempfile = "3.23.0" diff --git a/about.hbs b/about.hbs new file mode 100644 index 0000000..d8d59b3 --- /dev/null +++ b/about.hbs @@ -0,0 +1,70 @@ + + + + + + + +
+
+

Third Party Licenses

+

This page lists the licenses of the projects used in Bichon.

+
+ +

Overview of licenses:

+ + +

All license text:

+ +
+ + + diff --git a/about.toml b/about.toml new file mode 100644 index 0000000..768397e --- /dev/null +++ b/about.toml @@ -0,0 +1,18 @@ +accepted = [ + "Apache-2.0", + "MIT", + "BSD-3-Clause", + "MPL-2.0", + "ISC", + "Unicode-3.0", + "BSL-1.0", + "Unicode-DFS-2016", + "CDLA-Permissive-2.0", + "Zlib", + "Apache-2.0 WITH LLVM-exception", + "0BSD", + "BSD-2-Clause", + "NCSA", + "CC0-1.0", + "OpenSSL" +] diff --git a/build.rs b/build.rs new file mode 100644 index 0000000..ef46b47 --- /dev/null +++ b/build.rs @@ -0,0 +1,14 @@ +use std::{io::Result, process::Command}; + +fn main() -> Result<()> { + let output = Command::new("git") + .args(&["rev-parse", "--short", "HEAD"]) + .output() + .expect("Failed to get git commit hash"); + let git_hash = String::from_utf8(output.stdout) + .expect("Invalid UTF-8") + .trim() + .to_string(); + println!("cargo:rustc-env=GIT_HASH={}", git_hash); + Ok(()) +} diff --git a/build/release.sh b/build/release.sh new file mode 100644 index 0000000..7f1b114 --- /dev/null +++ b/build/release.sh @@ -0,0 +1 @@ +cargo build --release \ No newline at end of file diff --git a/build/rustmailer.service b/build/rustmailer.service new file mode 100644 index 0000000..55f8bb5 --- /dev/null +++ b/build/rustmailer.service @@ -0,0 +1,32 @@ +[Unit] +Description=Bichon Email Service +After=network.target +Wants=network.target + +[Service] +Type=simple +User=bichon +Group=bichon +WorkingDirectory=/opt/bichon +ExecStart=/opt/bichon/bichon +Restart=always +RestartSec=5s +EnvironmentFile=/etc/bichon.conf + +StandardOutput=null +StandardError=null +LogsDirectory=bichon +LogsDirectoryMode=0755 + +LimitNOFILE=65536 +LimitCORE=0 + +PrivateTmp=true +NoNewPrivileges=true +ProtectSystem=strict +ProtectHome=read-only +PrivateDevices=true +RestrictSUIDSGID=true + +[Install] +WantedBy=multi-user.target \ No newline at end of file diff --git a/cliff.toml b/cliff.toml new file mode 100644 index 0000000..ca9270b --- /dev/null +++ b/cliff.toml @@ -0,0 +1,58 @@ +# configuration file for git-cliff (0.1.0) + +[changelog] +# changelog header +header = """ +# Bichon Changelog\n +""" +# template for the changelog body +# https://tera.netlify.app/docs/#introduction +body = """ +{% if version %}\ + ## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }} +{% else %}\ + ## [unreleased] +{% endif %}\ +{% for group, commits in commits | group_by(attribute="group") %} + ### {{ group | upper_first }} + {% for commit in commits %} + - {% if commit.breaking %}[**breaking**] {% endif %}{{ commit.message | upper_first }}\ + {% endfor %} +{% endfor %}\n +""" +# remove the leading and trailing whitespace from the template +trim = true +# changelog footer +footer = """ + +""" + +[git] +# parse the commits based on https://www.conventionalcommits.org +conventional_commits = false +# filter out the commits that are not conventional +filter_unconventional = false +# regex for preprocessing the commit messages +commit_preprocessors = [ + { pattern = '\((\w+\s)?#([0-9]+)\)', replace = "([#${2}](https://github.com/rustmailer/bichon/issues/${2}))"}, +] +# regex for parsing and grouping commits +commit_parsers = [ + { message = "^feat\\(frontend\\):", group = "Frontend Features" }, + { message = "^fix\\(backend\\):", group = "Backend Fixes" }, + { message = "^docs:", group = "Documentation" }, + { message = "^ci:", group = "CI/CD" }, + { message = ".*", group = "Others" }, +] +# filter out the commits that are not matched by commit parsers +filter_commits = false +# glob pattern for matching git tags +tag_pattern = "^[0-9]+(\\.[0-9]+){2}$" +# regex for skipping tags +#skip_tags = "v0.1.0-beta.1" +# regex for ignoring tags +ignore_tags = "" +# sort the tags chronologically +date_order = false +# sort the commits inside sections by oldest/newest order +sort_commits = "oldest" \ No newline at end of file diff --git a/docker/Dockerfile b/docker/Dockerfile new file mode 100644 index 0000000..302635d --- /dev/null +++ b/docker/Dockerfile @@ -0,0 +1,36 @@ +# Use Debian bullseye-slim as the base image for newer OpenSSL +FROM ubuntu:24.04 +ARG CRATE_VERSION + +# Set maintainer metadata +LABEL maintainer="rustmailer " +LABEL version="${CRATE_VERSION}" +LABEL description="Dockerized Bichon service" + +# Set working directory +WORKDIR /opt/bichon + +# Copy compiled binary (ensure it's statically linked or compatible with bullseye) +COPY bichon /opt/bichon/bichon +COPY LICENSE /opt/bichon/ +COPY license.html /opt/bichon/ +# Set proper permissions +RUN chmod +x /opt/bichon/bichon + +# Install ca-certificates to ensure HTTPS certificate verification works correctly +RUN apt update && apt install -y ca-certificates && rm -rf /var/lib/apt/lists/* + +# Create data directory +RUN mkdir -p /data + +# Expose default ports +EXPOSE 15630 + +# Set volume and working directory +WORKDIR /data + +HEALTHCHECK --interval=30s --timeout=5s --start-period=5s --retries=3 \ + CMD curl -fs http://localhost:15630/api/status || exit 1 + +# Entrypoint remains the binary +CMD ["/opt/bichon/bichon"] diff --git a/docker/build.sh b/docker/build.sh new file mode 100644 index 0000000..2e01c8b --- /dev/null +++ b/docker/build.sh @@ -0,0 +1,31 @@ +#!/bin/bash + +# Step 0: Build frontend with pnpm in ../web +echo "Step 0: Building frontend in ../web using pnpm..." +cd ../web || { echo "Failed to enter ../web directory"; exit 1; } +pnpm run build || { echo "Frontend build failed"; exit 1; } + +# Step 1: Build Rust backend with cargo in project root +echo "Step 1: Building Rust backend with cargo..." +cd ../ || { echo "Failed to enter project root directory"; exit 1; } +cargo build --release || { echo "Rust backend build failed"; exit 1; } + +# Step 2: Back to docker directory +echo "Step 2: Returning to docker directory..." +cd docker || { echo "Failed to enter docker directory"; exit 1; } + +# Step 3: Extract version from Cargo.toml +VERSION=$(sed -n 's/^version = "\(.*\)"/\1/p' ../Cargo.toml) +echo "Step 3: Extracted version: $VERSION" + +# Step 4: Copy the compiled RustMailer binary to current directory (docker/) +cp ../target/release/bichon . +echo "Step 4: Copied bichon binary" + +# Step 5: Build Docker image with version tag +sudo docker build --build-arg CRATE_VERSION=$VERSION -t bichon:$VERSION . +echo "Step 5: Built Docker image with tag bichon:$VERSION" + +# Step 6: Tag local image for Docker Hub +docker tag bichon:$VERSION billydong/bichon:$VERSION +echo "Step 6: Tagged image as billydong/bichon:$VERSION" diff --git a/env/rustmailer.env b/env/rustmailer.env new file mode 100644 index 0000000..d7aff0d --- /dev/null +++ b/env/rustmailer.env @@ -0,0 +1,44 @@ +# Log level: trace, debug, info, warn, error +RUSTMAILER_LOG_LEVEL=info + +# HTTP server port +RUSTMAILER_HTTP_PORT=15630 + +# Public URL for this RustMailer instance +RUSTMAILER_PUBLIC_URL=http://localhost:15630 + +# Enable ANSI-colored logs (for terminal output) +RUSTMAILER_ANSI_LOGS=false + +# Enable JSON-formatted logs (for machine parsing) +RUSTMAILER_JSON_LOGS=false + +# Persist logs to files +RUSTMAILER_LOG_TO_FILE=false + +# Max number of server log files to retain +RUSTMAILER_MAX_SERVER_LOG_FILES=5 + +# Encryption password for stored secrets (change this in production!) +RUSTMAILER_ENCRYPT_PASSWORD=change-this-default-password-now + +# Root directory for persistent storage (adjust to your environment) +RUSTMAILER_ROOT_DIR=/data/bichon-data + +# Enable API access token validation +RUSTMAILER_ENABLE_ACCESS_TOKEN=false + +# IP address to bind the HTTP and gRPC servers to (default: 0.0.0.0) +RUSTMAILER_BIND_IP= + +# Comma-separated list of allowed CORS origins (e.g. https://app.example.com) +RUSTMAILER_CORS_ORIGINS= + +# Cache duration (in seconds) for CORS preflight responses +RUSTMAILER_CORS_MAX_AGE=86400 + +# Enable HTTPS for REST API server +RUSTMAILER_ENABLE_REST_HTTPS=false + +# Enable HTTP response compression (gzip or brotli) +RUSTMAILER_HTTP_COMPRESSION_ENABLED=true diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..7a6da72 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,76 @@ +// +// Copyright (c) 2025 rustmailer.com (https://rustmailer.com) +// +// This file is part of the Bichon Email Archiving Project +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +use mimalloc::MiMalloc; +use modules::{ + common::rustls::RustMailerTls, + context::{executors::EmailClientExecutors, Initialize}, + error::BichonResult, + logger, + rest::start_http_server, + tasks::PeriodicTasks, + token::root::ensure_root_token, +}; +use tracing::info; + +use crate::modules::{common::signal::SignalManager, settings::dir::DataDirManager}; + +mod modules; + +#[global_allocator] +static GLOBAL: MiMalloc = MiMalloc; + +static LOGO: &str = r#" + _ _ _ +| | (_) | | +| |__ _ ___ | |__ ___ _ __ +| '_ \ | | / __|| '_ \ / _ \ | '_ \ +| |_) || || (__ | | | || (_) || | | | +|_.__/ |_| \___||_| |_| \___/ |_| |_| + +"#; +#[tokio::main] +async fn main() -> BichonResult<()> { + logger::initialize_logging(); + info!("{}", LOGO); + info!("Starting bichon-server"); + info!("Version: {}", bichon_version!()); + info!("Git: [{}]", env!("GIT_HASH")); + info!("GitHub: https://github.com/rustmailer/bichon"); + + if let Err(error) = initialize().await { + eprintln!("{:?}", error); + return Err(error); + } + + start_http_server().await?; + Ok(()) +} + +/// Initialize the system by validating settings and starting necessary tasks. +async fn initialize() -> BichonResult<()> { + // SETTINGS.validate()?; + SignalManager::initialize().await?; + DataDirManager::initialize().await?; + ensure_root_token().await?; + RustMailerTls::initialize().await?; + EmailClientExecutors::initialize().await?; + PeriodicTasks::start_background_tasks(); + Ok(()) +} diff --git a/src/modules/account/dispatcher.rs b/src/modules/account/dispatcher.rs new file mode 100644 index 0000000..bc7600e --- /dev/null +++ b/src/modules/account/dispatcher.rs @@ -0,0 +1,60 @@ +// +// Copyright (c) 2025 rustmailer.com (https://rustmailer.com) +// +// This file is part of the Bichon Email Archiving Project +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +use crate::modules::account::state::AccountRunningState; +use std::sync::LazyLock; +use tokio::sync::mpsc; +use tracing::error; + +pub static STATUS_DISPATCHER: LazyLock = LazyLock::new(ErrorDispatcher::new); + +pub struct ErrorDispatcher { + channel: mpsc::Sender<(u64, String)>, +} + +impl ErrorDispatcher { + pub fn new() -> Self { + let (tx, mut rx) = mpsc::channel::<(u64, String)>(100); + + tokio::spawn(async move { + while let Some((account_id, error)) = rx.recv().await { + match AccountRunningState::append_error_message(account_id, error).await { + Ok(()) => {} + Err(error) => { + error!( + "Failed to append error for account: {}. Error: {:#?}", + &account_id, error + ); + } + } + } + }); + + ErrorDispatcher { channel: tx } + } + + pub async fn append_error(&self, account_id: u64, error: String) { + if let Err(e) = self.channel.send((account_id, error.clone())).await { + error!( + "Failed to dispatch status update for account: {}, Error: {}. Channel error: {:?}", + &account_id, error, e + ); + } + } +} diff --git a/src/modules/account/entity.rs b/src/modules/account/entity.rs new file mode 100644 index 0000000..4081112 --- /dev/null +++ b/src/modules/account/entity.rs @@ -0,0 +1,120 @@ +// +// Copyright (c) 2025 rustmailer.com (https://rustmailer.com) +// +// This file is part of the Bichon Email Archiving Project +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +use crate::{encrypt, modules::error::BichonResult}; + +use poem_openapi::{Enum, Object}; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Default, Debug, Eq, PartialEq, Serialize, Deserialize, Object)] +pub struct ImapConfig { + /// IMAP server hostname or IP address + #[oai(validator(max_length = 253, pattern = r"^[a-zA-Z0-9\-\.]+$"))] + pub host: String, + /// IMAP server port number + #[oai(validator(minimum(value = "1"), maximum(value = "65535")))] + pub port: u16, + /// Connection encryption method + pub encryption: Encryption, + /// Authentication configuration + pub auth: AuthConfig, + /// Optional proxy ID for establishing the connection. + /// - If `None` or not provided, the client will connect directly to the IMAP server. + /// - If `Some(proxy_id)`, the client will use the pre-configured proxy with the given ID. + pub use_proxy: Option, +} + +impl ImapConfig { + pub fn try_encrypt_password(self) -> BichonResult { + Ok(Self { + host: self.host, + port: self.port, + encryption: self.encryption, + auth: self.auth.encrypt()?, + use_proxy: self.use_proxy, + }) + } +} + + +#[derive(Enum, Default, Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] +pub enum AuthType { + /// Standard password authentication (PLAIN/LOGIN) + #[default] + Password, + /// OAuth 2.0 authentication (SASL XOAUTH2) + OAuth2, +} + +#[derive(Object, Default, Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] +pub struct AuthConfig { + ///Authentication method to use + pub auth_type: AuthType, + /// Credential secret for Password authentication. + /// + /// Users should provide a plaintext password (1 to 256 characters). + /// The server will encrypt the password using AES-256-GCM and securely store it. + /// The plaintext password is never stored, so users must remember it for authentication. + #[oai(validator(max_length = 256, min_length = 1))] + pub password: Option, +} + +impl AuthConfig { + pub fn encrypt(self) -> BichonResult { + match self.password { + Some(password) => Ok(Self { + auth_type: self.auth_type, + password: Some(encrypt!(&password)?), + }), + None => Ok(self), + } + } +} + +impl AuthConfig { + pub fn validate(&self) -> Result<(), &'static str> { + match self.auth_type { + AuthType::Password if self.password.is_none() => { + Err("When auth_type is Passwd, password must not be None.") + } + _ => Ok(()), + } + } +} + +#[derive(Clone, Default, Debug, Eq, PartialEq, Serialize, Deserialize, Enum)] +pub enum Encryption { + /// SSL/TLS encrypted connection + #[default] + Ssl, + /// StartTLS encryption + StartTls, + /// Unencrypted connection + None, +} + +impl From for Encryption { + fn from(value: bool) -> Self { + if value { + Self::Ssl + } else { + Self::None + } + } +} diff --git a/src/modules/account/migration.rs b/src/modules/account/migration.rs new file mode 100644 index 0000000..30db562 --- /dev/null +++ b/src/modules/account/migration.rs @@ -0,0 +1,357 @@ +// +// Copyright (c) 2025 rustmailer.com (https://rustmailer.com) +// +// This file is part of the Bichon Email Archiving Project +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +use native_db::*; +use native_model::{native_model, Model}; + +use poem_openapi::{Enum, Object}; +use serde::{Deserialize, Serialize}; +use std::collections::BTreeSet; +use tracing::info; + +use crate::{ + encrypt, + modules::{ + account::{entity::ImapConfig, since::DateSince, state::AccountRunningState}, + cache::imap::mailbox::MailBox, + database::{insert_impl, list_all_impl}, + error::BichonResult, + indexer::manager::{EML_INDEX_MANAGER, ENVELOPE_INDEX_MANAGER}, + }, + utc_now, +}; + +use crate::id; +use crate::modules::account::payload::AccountCreateRequest; +use crate::modules::account::payload::AccountUpdateRequest; +use crate::modules::account::payload::MinimalAccount; +use crate::modules::cache::imap::task::SYNC_TASKS; +use crate::modules::context::controller::SYNC_CONTROLLER; +use crate::modules::context::executors::MAIL_CONTEXT; +use crate::modules::database::count_by_unique_secondary_key_impl; +use crate::modules::database::delete_impl; +use crate::modules::database::manager::DB_MANAGER; +use crate::modules::database::{ + paginate_query_primary_scan_all_impl, secondary_find_impl, update_impl, +}; +use crate::modules::error::code::ErrorCode; +use crate::modules::oauth2::token::OAuth2AccessToken; +use crate::modules::rest::response::DataPage; +use crate::modules::token::AccessToken; +use crate::raise_error; + +pub type AccountModel = AccountV1; + +#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize, Enum)] +pub enum AccountType { + #[default] + IMAP, + NoSync, +} + +#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize, Object)] +#[native_model(id = 4, version = 1)] +#[native_db(primary_key(pk -> String))] +pub struct AccountV1 { + #[secondary_key(unique)] + pub id: u64, + pub imap: Option, + pub enabled: bool, + #[oai(validator(custom = "crate::modules::common::validator::EmailValidator"))] + pub email: String, + pub name: Option, + pub capabilities: Option>, + pub date_since: Option, + pub folder_limit: Option, + pub sync_folders: Option>, + pub account_type: AccountType, + pub sync_interval_min: Option, + pub known_folders: Option>, + pub created_at: i64, + pub updated_at: i64, + pub use_proxy: Option, +} + +impl AccountV1 { + fn pk(&self) -> String { + format!("{}_{}", self.created_at, self.id) + } + + pub fn new(request: AccountCreateRequest) -> BichonResult { + Ok(Self { + id: id!(64), + email: request.email, + name: request.name, + imap: request.imap.map(|i| i.try_encrypt_password()).transpose()?, + enabled: request.enabled, + capabilities: None, + date_since: request.date_since, + sync_folders: None, + known_folders: None, + account_type: request.account_type, + sync_interval_min: request.sync_interval_min, + created_at: utc_now!(), + updated_at: utc_now!(), + use_proxy: request.use_proxy, + folder_limit: request.folder_limit, + }) + } + + pub async fn check_account_active(account_id: u64) -> BichonResult { + let account = + secondary_find_impl::(DB_MANAGER.meta_db(), AccountV1Key::id, account_id) + .await? + .ok_or_else(|| { + raise_error!( + format!("Account id='{account_id}' not found"), + ErrorCode::ResourceNotFound + ) + })?; + + if !account.enabled { + return Err(raise_error!( + format!("Account id='{account_id}' is disabled"), + ErrorCode::AccountDisabled + )); + } + Ok(account) + } + + /// Fetches an `AccountEntity` by its `id`. + pub async fn get(account_id: u64) -> BichonResult { + let result: AccountModel = Self::find(account_id).await?.ok_or_else(|| { + raise_error!( + format!("Account with ID '{account_id}' not found"), + ErrorCode::ResourceNotFound + ) + })?; + Ok(result) + } + + pub async fn find(account_id: u64) -> BichonResult> { + secondary_find_impl::(DB_MANAGER.meta_db(), AccountV1Key::id, account_id) + .await + } + + /// Saves the current `AccountEntity` by persisting it to storage. + pub async fn save(&self) -> BichonResult<()> { + insert_impl(DB_MANAGER.meta_db(), self.to_owned()).await + } + + pub async fn create_account(request: AccountCreateRequest) -> BichonResult { + let entity = request.create_entity()?; + entity.save().await?; + if matches!(entity.account_type, AccountType::IMAP) { + SYNC_CONTROLLER + .trigger_start(entity.id, entity.email.clone()) + .await; + } + Ok(entity) + } + + pub async fn update( + account_id: u64, + request: AccountUpdateRequest, + validate: bool, + ) -> BichonResult<()> { + let account = AccountModel::get(account_id).await?; + if validate { + request.validate_update_request(&account)?; + } + update_impl( + DB_MANAGER.meta_db(), + move |_| Ok(account), + move |current| Self::apply_update_fields(current, request), + ) + .await?; + + Ok(()) + } + + pub async fn delete(account_id: u64) -> BichonResult<()> { + let account = Self::get(account_id).await?; + if let Err(error) = Self::cleanup_account_resources_sequential(&account).await { + tracing::error!( + "[CLEANUP_ACCOUNT_ERROR] Account {}: failed to cleanup resources: {:#?}", + account_id, + error + ); + return Err(error); + } + Ok(()) + } + + async fn delete_account(account_id: u64) -> BichonResult<()> { + delete_impl(DB_MANAGER.meta_db(), move|rw|{ + rw.get().secondary::(AccountV1Key::id, account_id).map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))? + .ok_or_else(||raise_error!(format!("The account entity with id={account_id} that you want to delete was not found."), ErrorCode::ResourceNotFound)) + }).await + } + + async fn cleanup_account_resources_sequential(account: &AccountModel) -> BichonResult<()> { + if matches!(account.account_type, AccountType::IMAP) { + SYNC_TASKS.stop(account.id).await?; + AccountRunningState::delete(account.id).await?; + MAIL_CONTEXT.clean_account(account.id).await?; + } + OAuth2AccessToken::try_delete(account.id).await?; + AccessToken::cleanup_account(account.id).await?; + MailBox::clean(account.id).await?; + ENVELOPE_INDEX_MANAGER + .delete_account_envelopes(account.id) + .await?; + EML_INDEX_MANAGER + .delete_account_envelopes(account.id) + .await?; + Self::delete_account(account.id).await?; + info!("Sequential cleanup completed for account: {}", account.id); + Ok(()) + } + + pub async fn update_sync_folders( + account_id: u64, + sync_folders: Vec, + ) -> BichonResult<()> { + update_impl(DB_MANAGER.meta_db(), move |rw| { + rw.get().secondary::(AccountV1Key::id, account_id).map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))? + .ok_or_else(|| raise_error!(format!("When trying to update account sync_folders, the corresponding record was not found. account_id={}", account_id), ErrorCode::ResourceNotFound)) + }, |current|{ + let mut updated = current.clone(); + updated.sync_folders = Some(sync_folders); + Ok(updated) + }).await?; + Ok(()) + } + + pub async fn update_known_folders( + account_id: u64, + known_folders: BTreeSet, + ) -> BichonResult<()> { + update_impl(DB_MANAGER.meta_db(), move |rw| { + rw.get().secondary::(AccountV1Key::id, account_id).map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))? + .ok_or_else(|| raise_error!(format!("When trying to update account known_folders, the corresponding record was not found. account_id={}", account_id), ErrorCode::ResourceNotFound)) + }, |current|{ + let mut updated = current.clone(); + updated.known_folders = Some(known_folders); + Ok(updated) + }).await?; + Ok(()) + } + + pub async fn update_capabilities( + account_id: u64, + capabilities: Vec, + ) -> BichonResult<()> { + update_impl(DB_MANAGER.meta_db(), move |rw| { + rw.get().secondary::(AccountV1Key::id, account_id).map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))? + .ok_or_else(|| raise_error!(format!("When trying to update account capabilities, the corresponding record was not found. account_id={}", account_id), ErrorCode::ResourceNotFound)) + }, |current|{ + let mut updated = current.clone(); + updated.capabilities = Some(capabilities); + Ok(updated) + }).await?; + Ok(()) + } + + /// Retrieves a list of all `AccountEntity` instances. + pub async fn list_all() -> BichonResult> { + list_all_impl(DB_MANAGER.meta_db()).await + } + + pub async fn minimal_list() -> BichonResult> { + let result = list_all_impl(DB_MANAGER.meta_db()) + .await? + .into_iter() + .filter(|a: &AccountModel| a.enabled) + .map(|account: AccountModel| MinimalAccount { + id: account.id, + email: account.email, + }) + .collect::>(); + Ok(result) + } + + pub async fn count() -> BichonResult { + count_by_unique_secondary_key_impl::(DB_MANAGER.meta_db(), AccountV1Key::id) + .await + } + + pub async fn paginate_list( + page: Option, + page_size: Option, + desc: Option, + ) -> BichonResult> { + paginate_query_primary_scan_all_impl(DB_MANAGER.meta_db(), page, page_size, desc) + .await + .map(DataPage::from) + } + + // This method applies the updates from the request to the old account entity + fn apply_update_fields( + old: &AccountModel, + request: AccountUpdateRequest, + ) -> BichonResult { + let mut new = old.clone(); + + if let Some(date_since) = request.date_since { + new.date_since = Some(date_since); + } + + if let Some(folder_limit) = request.folder_limit { + new.folder_limit = Some(folder_limit); + } + + if let Some(name) = &request.name { + new.name = Some(name.clone()); + } + + if matches!(old.account_type, AccountType::IMAP) { + if let Some(imap) = &request.imap { + let mut new_imap = imap.clone(); + if let Some(password) = &new_imap.auth.password { + let encrypted_password = encrypt!(password)?; + new_imap.auth.password = Some(encrypted_password); + } + new.imap = Some(new_imap); + } + + if let Some(folder_names) = request.sync_folders { + new.sync_folders = Some(folder_names); + } + if let Some(sync_interval_min) = &request.sync_interval_min { + new.sync_interval_min = Some(*sync_interval_min); + } + if let Some(use_proxy) = request.use_proxy { + new.use_proxy = Some(use_proxy); + } + } + + if matches!(old.account_type, AccountType::NoSync) { + if let Some(email) = &request.email { + new.email = email.clone(); + } + } + + if let Some(enabled) = request.enabled { + new.enabled = enabled; + } + new.updated_at = utc_now!(); + Ok(new) + } +} diff --git a/src/modules/account/mod.rs b/src/modules/account/mod.rs new file mode 100644 index 0000000..5d879a4 --- /dev/null +++ b/src/modules/account/mod.rs @@ -0,0 +1,25 @@ +// +// Copyright (c) 2025 rustmailer.com (https://rustmailer.com) +// +// This file is part of the Bichon Email Archiving Project +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +pub mod dispatcher; +pub mod entity; +pub mod payload; +pub mod since; +pub mod state; +pub mod migration; diff --git a/src/modules/account/payload.rs b/src/modules/account/payload.rs new file mode 100644 index 0000000..91c70e9 --- /dev/null +++ b/src/modules/account/payload.rs @@ -0,0 +1,172 @@ +// +// Copyright (c) 2025 rustmailer.com (https://rustmailer.com) +// +// This file is part of the Bichon Email Archiving Project +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +use std::collections::BTreeSet; + +use crate::modules::account::entity::ImapConfig; +use crate::modules::account::migration::{AccountModel, AccountType}; +use crate::modules::account::since::DateSince; +use crate::modules::error::code::ErrorCode; +use crate::modules::error::BichonResult; +use crate::modules::token::AccountInfo; +use crate::{raise_error, validate_email}; +use poem_openapi::Object; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize, Object)] +pub struct AccountCreateRequest { + #[oai(validator(custom = "crate::modules::common::validator::EmailValidator"))] + pub email: String, + pub name: Option, + pub imap: Option, + pub enabled: bool, + pub date_since: Option, + pub account_type: AccountType, + #[oai(validator(minimum(value = "100")))] + pub folder_limit: Option, + #[oai(validator(minimum(value = "10"), maximum(value = "480")))] + pub sync_interval_min: Option, + pub use_proxy: Option, +} + +impl AccountCreateRequest { + pub fn create_entity(self) -> BichonResult { + if let Some(date_since) = self.date_since.as_ref() { + date_since.validate()?; + } + match self.account_type { + AccountType::IMAP => { + match &self.imap { + Some(imap) => Self::validate_request(imap, &self.email)?, + None => { + return Err(raise_error!( + "IMAP configuration is required for IMAP account type".into(), + ErrorCode::InvalidParameter + )) + } + } + if self.sync_interval_min.is_none() { + return Err(raise_error!( + "`sync_interval_min` is required for IMAP account type".into(), + ErrorCode::InvalidParameter + )); + } + } + AccountType::NoSync => {} + } + Ok(AccountModel::new(self)?) + } + + fn validate_request(imap: &ImapConfig, email: &str) -> BichonResult<()> { + imap.auth + .validate() + .map_err(|e| raise_error!(e.to_owned(), ErrorCode::InvalidParameter))?; + validate_email!(email)?; + Ok(()) + } +} + +#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize, Object)] +pub struct AccountUpdateRequest { + pub email: Option, + /// Represents the account activation status. + /// + /// If this value is `false`, all account-related resources will be unavailable + /// and any attempts to access them should return an error indicating the account + /// is inactive. + pub enabled: Option, + /// Display name for the account (optional) + pub name: Option, + /// IMAP server configuration + pub imap: Option, + /// Controls initial synchronization time range + /// + /// When dealing with large mailboxes, this restricts scanning to: + /// - Messages after specified starting point + /// - Or within sliding window + /// + /// ### Use Cases + /// - Event-driven systems (only sync recent actionable emails) + /// - First-time sync optimization for large accounts + /// - Reducing server load during resyncs + pub date_since: Option, + /// Max emails to sync for this folder. + /// If not set, sync all emails. + /// otherwise sync up to `n` most recent emails (min 10). + #[oai(validator(minimum(value = "100")))] + pub folder_limit: Option, + /// Configuration for selective folder (mailbox/label) synchronization + /// + /// - For IMAP/SMTP accounts: + /// Stores the mailbox names, since IMAP mailboxes do not have stable IDs. + /// Synchronization is keyed by the folder name. + /// + /// - For Gmail API accounts: + /// A Gmail label is treated as a mailbox (model mapping). + /// Since label names can be easily changed, the stable `labelId` is recorded here + /// instead of the label name. + /// + /// Defaults to standard folders (`INBOX`, `Sent`) if empty. + /// Modified folders will be automatically synced on the next update. + pub sync_folders: Option>, + /// Incremental sync interval (seconds) + #[oai(validator(minimum(value = "10"), maximum(value = "480")))] + pub sync_interval_min: Option, + /// Optional proxy ID for establishing the connection to external APIs (e.g., Gmail, Outlook). + /// - If `None` or not provided, the client will connect directly to the API server. + /// - If `Some(proxy_id)`, the client will use the pre-configured proxy with the given ID for API requests. + pub use_proxy: Option, +} + +impl AccountUpdateRequest { + pub fn validate_update_request(&self, account: &AccountModel) -> BichonResult<()> { + if let Some(date_since) = self.date_since.as_ref() { + date_since.validate()?; + } + if matches!(account.account_type, AccountType::IMAP) { + if let Some(mailboxes) = self.sync_folders.as_ref() { + if mailboxes.is_empty() { + return Err(raise_error!( + "Invalid configuration: 'sync_folders' cannot be empty. \ + If you are modifying the subscription list, please provide at least one mailbox to subscribe to.".into(), ErrorCode::InvalidParameter + )); + } + } + } + Ok(()) + } +} + +#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize, Object)] + +pub struct MinimalAccount { + pub id: u64, + pub email: String, +} + +pub fn filter_accessible_accounts<'a>( + all_accounts: &'a [MinimalAccount], + allowed: &BTreeSet, +) -> Vec { + all_accounts + .iter() + .filter(|acct| allowed.iter().any(|a| a.id == acct.id)) + .cloned() + .collect() +} diff --git a/src/modules/account/since.rs b/src/modules/account/since.rs new file mode 100644 index 0000000..5ae340f --- /dev/null +++ b/src/modules/account/since.rs @@ -0,0 +1,284 @@ +// +// Copyright (c) 2025 rustmailer.com (https://rustmailer.com) +// +// This file is part of the Bichon Email Archiving Project +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +use crate::{ + modules::error::{code::ErrorCode, BichonResult}, + raise_error, +}; +use chrono::{Datelike, Days, Local, Months, NaiveDate, Utc}; +use poem_openapi::{Enum, Object}; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize, Object)] +pub struct DateSince { + /// Absolute date boundary in ISO 8601 format (YYYY-MM-DD) + /// + /// ### Validation Rules + /// - Must match exact format `^\d{4}-\d{2}-\d{2}$` + /// - Date must be logically valid (e.g. no 2025-05-01) + /// + /// ### Example + /// ```json + /// { + /// "fixed": "2025-05-01" + /// } + /// ``` + #[oai(validator(pattern = r"^\d{4}-\d{2}-\d{2}$"))] + pub fixed: Option, + /// Relative time period from current date + /// + /// ### Constraints + /// - Value must be ≥ 1 + /// - Units support day/month/year granularity + /// + /// ### Example + /// ```json + /// { + /// "relative": { + /// "unit": "Days", + /// "value": 7 + /// } + /// } + /// ``` + pub relative: Option, +} + +#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize, Enum)] +pub enum Unit { + #[default] + Days, + Months, + Years, +} + +#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize, Object)] +pub struct RelativeDate { + /// The time unit to use for the offset (days, months, or years) + pub unit: Unit, + /// The quantity of time units to offset (must be a positive integer) + #[oai(validator(minimum(value = "1")))] + pub value: u32, +} + +impl RelativeDate { + pub fn validate_date(&self) -> BichonResult<()> { + if self.value == 0 { + return Err(raise_error!( + "Value must be greater than 0".into(), + ErrorCode::InvalidParameter + )); + } + + let now = Local::now(); + let date = match self.unit { + Unit::Days => now.checked_sub_days(Days::new(self.value as u64)), + Unit::Months => now.checked_sub_months(Months::new(self.value)), + Unit::Years => now.checked_sub_months(Months::new(self.value * 12)), + }; + + let date = date.ok_or_else(|| { + raise_error!( + "Invalid date: the calculated date is earlier than 1970 or an overflow occurred." + .into(), + ErrorCode::InvalidParameter + ) + })?; + + let naive_date = date.date_naive(); + + // Check if the date is before 1970 + if naive_date.year() < 1970 { + return Err(raise_error!( + format!( + "Date cannot be earlier than 1970-01-01. Provided: '{}'", + naive_date + ), + ErrorCode::InvalidParameter + )); + } + + Ok(()) + } + + fn compute_date(&self) -> BichonResult> { + if self.value == 0 { + return Err(raise_error!( + "Value must be greater than 0".into(), + ErrorCode::InvalidParameter + )); + } + + let now = Local::now(); + let date = match self.unit { + Unit::Days => now.checked_sub_days(Days::new(self.value as u64)), + Unit::Months => now.checked_sub_months(Months::new(self.value)), + Unit::Years => now.checked_sub_months(Months::new(self.value * 12)), + }; + + let date = date.ok_or_else(|| { + raise_error!( + "Invalid date: the calculated date is earlier than 1970 or an overflow occurred." + .into(), + ErrorCode::InvalidParameter + ) + })?; + + let naive_date = date.date_naive(); + if naive_date.year() < 1970 { + return Err(raise_error!( + format!( + "Date cannot be earlier than 1970-01-01. Provided: '{}'", + naive_date + ), + ErrorCode::InvalidParameter + )); + } + + Ok(date) + } + + pub fn calculate_date(&self) -> BichonResult { + let date = self.compute_date()?; + Ok(date.format("%d-%b-%Y").to_string()) + } +} + +impl DateSince { + pub fn validate(&self) -> BichonResult<()> { + match (&self.fixed, &self.relative) { + // If only `relative` is provided + (None, Some(r)) => { + r.validate_date()?; + } + // If only `fixed` is provided + (Some(fixed), None) => { + self.validate_fixed_date(fixed)?; + } + // If both or neither are provided + _ => { + return Err(raise_error!( + "Invalid input: You must provide either 'fixed' or 'relative', but not both." + .to_string(), + ErrorCode::InvalidParameter + )); + } + } + Ok(()) + } + + fn validate_fixed_date(&self, fixed: &str) -> BichonResult<()> { + // Try to parse the input string as YYYY-MM-DD + let date = NaiveDate::parse_from_str(fixed, "%Y-%m-%d").map_err(|_| { + raise_error!( + format!( + "Invalid date format. Expected 'YYYY-MM-DD'. Example: '2024-11-19'. Provided: '{}'", + fixed + ), + ErrorCode::InvalidParameter + ) + })?; + + let now = Utc::now().date_naive(); + + // Check if the date is in the future + if date >= now { + return Err(raise_error!( + format!( + "Date cannot be in the future. Provided: '{}', Today: '{}'", + fixed, + now.format("%Y-%m-%d") + ), + ErrorCode::InvalidParameter + )); + } + + // Check if the date is before 1970 + if date.year() < 1970 { + return Err(raise_error!( + format!( + "Date cannot be earlier than 1970-01-01. Provided: '{}'", + fixed + ), + ErrorCode::InvalidParameter + )); + } + + Ok(()) + } + + fn format_user_date(&self, fixed: &str) -> BichonResult { + let date = NaiveDate::parse_from_str(fixed, "%Y-%m-%d").map_err(|_| { + raise_error!( + format!( + "Invalid date format. Expected 'YYYY-MM-DD'. Example: '2024-11-19'. Provided: '{}'", + fixed + ), + ErrorCode::InvalidParameter + ) + })?; + // Format the date into "%d-%b-%Y" format + Ok(date.format("%d-%b-%Y").to_string()) + } + + pub fn since_date(&self) -> BichonResult { + // Handle the case where only one of `fixed` or `relative` is provided + if let Some(r) = &self.relative { + // If `relative` is provided, calculate the date + r.calculate_date() + } else if let Some(f) = &self.fixed { + // If `fixed` is provided, format the date + self.format_user_date(f) + } else { + // If neither is provided, return an error + Err(raise_error!( + "You must provide either a 'fixed' or 'relative' date.".to_string(), + ErrorCode::InvalidParameter + )) + } + } +} + +#[cfg(test)] +mod test { + use crate::modules::account::since::{DateSince, RelativeDate, Unit}; + + #[test] + fn test1() { + let e = DateSince { + fixed: Some("2014-09-12".to_string()), + relative: None, + }; + + e.validate().unwrap(); + + println!("{}", e.since_date().unwrap()); + + let e = DateSince { + fixed: None, + relative: Some(RelativeDate { + unit: Unit::Days, + value: 1, + }), + }; + + e.validate().unwrap(); + + println!("{}", e.since_date().unwrap()); + } +} diff --git a/src/modules/account/state.rs b/src/modules/account/state.rs new file mode 100644 index 0000000..4e44bd7 --- /dev/null +++ b/src/modules/account/state.rs @@ -0,0 +1,340 @@ +// +// Copyright (c) 2025 rustmailer.com (https://rustmailer.com) +// +// This file is part of the Bichon Email Archiving Project +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +use crate::{ + modules::{ + database::{async_find_impl, delete_impl, manager::DB_MANAGER, update_impl, upsert_impl}, + error::{code::ErrorCode, BichonResult}, + }, + raise_error, utc_now, +}; +use native_db::*; +use native_model::{native_model, Model}; +use poem_openapi::Object; +use serde::{Deserialize, Serialize}; +use std::collections::BTreeMap; + +const ERROR_COUNT_PER_ACCOUNT: usize = 30; + +#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize, Object)] +pub struct MailboxBatchProgress { + pub total_batches: u32, + pub current_batch: u32, +} + +#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize, Object)] +#[native_model(id = 2, version = 1)] +#[native_db] +pub struct AccountRunningState { + #[primary_key] + pub account_id: u64, + pub last_incremental_sync_start: i64, + pub last_incremental_sync_end: Option, + pub errors: Vec, + pub is_initial_sync_completed: bool, + pub progress: Option>, + pub initial_sync_start_time: Option, + pub initial_sync_end_time: Option, + pub initial_sync_failed_time: Option, +} + +#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize, Object)] +pub struct AccountError { + pub error: String, + pub at: i64, +} + +impl AccountRunningState { + pub async fn add(account_id: u64) -> BichonResult<()> { + let info = AccountRunningState { + account_id, + last_incremental_sync_start: 0, + last_incremental_sync_end: None, + errors: vec![], + is_initial_sync_completed: false, + progress: None, + initial_sync_start_time: None, + initial_sync_end_time: None, + initial_sync_failed_time: None, + }; + upsert_impl(DB_MANAGER.envelope_db(), info).await + } + + pub async fn get(account_id: u64) -> BichonResult> { + async_find_impl(DB_MANAGER.envelope_db(), account_id).await + } + + async fn update_account_running_state( + account_id: u64, + updater: impl FnOnce(&AccountRunningState) -> BichonResult + Send + 'static, + ) -> BichonResult<()> { + update_impl( + DB_MANAGER.envelope_db(), + move |rw| { + rw.get() + .primary::(account_id) + .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))? + .ok_or_else(|| { + raise_error!( + format!("Cannot find sync info of account={}", account_id), + ErrorCode::ResourceNotFound + ) + }) + }, + updater, + ) + .await?; + Ok(()) + } + + pub async fn delete(account_id: u64) -> BichonResult<()> { + if Self::get(account_id).await?.is_none() { + return Ok(()); + } + + delete_impl(DB_MANAGER.envelope_db(), move |rw| { + rw.get() + .primary::(account_id) + .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))? + .ok_or_else(|| { + raise_error!( + format!( + "AccountRunningState '{}' not found during deletion process.", + account_id + ), + ErrorCode::ResourceNotFound + ) + }) + }) + .await + } + + pub async fn set_initial_sync_start(account_id: u64) -> BichonResult<()> { + Self::update_account_running_state(account_id, move |current| { + let mut updated = current.clone(); + updated.initial_sync_start_time = Some(utc_now!()); + Ok(updated) + }) + .await + } + + pub async fn set_initial_sync_completed(account_id: u64) -> BichonResult<()> { + Self::update_account_running_state(account_id, move |current| { + let mut updated = current.clone(); + updated.is_initial_sync_completed = true; + updated.initial_sync_end_time = Some(utc_now!()); + Ok(updated) + }) + .await + } + + pub async fn set_initial_sync_failed(account_id: u64) -> BichonResult<()> { + Self::update_account_running_state(account_id, move |current| { + let mut updated = current.clone(); + updated.initial_sync_failed_time = Some(utc_now!()); + Ok(updated) + }) + .await + } + + pub async fn set_current_sync_batch_number( + account_id: u64, + syncing_folder: String, + batch_number: u32, + ) -> BichonResult<()> { + Self::update_account_running_state(account_id, move |current| { + let mut updated = current.clone(); + let mut progress_map = updated.progress.clone().unwrap_or_default(); + let entry = + progress_map + .entry(syncing_folder.to_string()) + .or_insert(MailboxBatchProgress { + total_batches: 0, + current_batch: 0, + }); + entry.current_batch = batch_number; + updated.progress = Some(progress_map); + Ok(updated) + }) + .await + } + + pub async fn set_folder_initial_sync_completed( + account_id: u64, + syncing_folder: String, + ) -> BichonResult<()> { + Self::update_account_running_state(account_id, move |current| { + let mut updated = current.clone(); + let mut progress_map = updated.progress.clone().unwrap_or_default(); + let entry = + progress_map + .entry(syncing_folder.to_string()) + .or_insert(MailboxBatchProgress { + total_batches: 0, + current_batch: 0, + }); + entry.current_batch = entry.total_batches; + updated.progress = Some(progress_map); + Ok(updated) + }) + .await + } + + pub async fn set_initial_current_syncing_folder( + account_id: u64, + current_syncing_folder: String, + total_sync_batches: u32, + ) -> BichonResult<()> { + Self::update_account_running_state(account_id, move |current| { + let mut updated = current.clone(); + let mut progress_map = updated.progress.clone().unwrap_or_default(); + progress_map.insert( + current_syncing_folder.clone(), + MailboxBatchProgress { + total_batches: total_sync_batches, + current_batch: 0, + }, + ); + updated.progress = Some(progress_map); + Ok(updated) + }) + .await + } + + pub async fn set_incremental_sync_start(account_id: u64) -> BichonResult<()> { + Self::update_account_running_state(account_id, move |current| { + let mut updated = current.clone(); + updated.last_incremental_sync_start = utc_now!(); + updated.last_incremental_sync_end = None; + Ok(updated) + }) + .await + } + + pub async fn set_incremental_sync_end(account_id: u64) -> BichonResult<()> { + Self::update_account_running_state(account_id, move |current| { + let mut updated = current.clone(); + updated.last_incremental_sync_end = Some(utc_now!()); + Ok(updated) + }) + .await + } + + pub async fn append_error_message(account_id: u64, error: String) -> BichonResult<()> { + Self::update_account_running_state(account_id, move |current| { + let mut updated = current.clone(); + updated.append_error_log(error); + Ok(updated) + }) + .await + } + + pub fn append_error_log(&mut self, error: String) { + let new_error = AccountError { + error, + at: utc_now!(), + }; + + self.errors.push(new_error); + if self.errors.len() > ERROR_COUNT_PER_ACCOUNT { + self.errors.remove(0); + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_insert_single_error() { + let mut account_state = AccountRunningState { + account_id: 1000u64, + last_incremental_sync_start: 1000, + last_incremental_sync_end: Some(2000), + errors: Vec::new(), + ..Default::default() + }; + + account_state.append_error_log(String::from("Error 1")); + assert_eq!(account_state.errors.len(), 1); + assert_eq!(account_state.errors[0].error, "Error 1"); + } + + #[test] + fn test_insert_multiple_errors() { + let mut account_state = AccountRunningState { + account_id: 1000u64, + last_incremental_sync_start: 1000, + last_incremental_sync_end: Some(2000), + errors: Vec::new(), + ..Default::default() + }; + + for i in 1..=5 { + account_state.append_error_log(format!("Error {}", i)); + } + + assert_eq!(account_state.errors.len(), 5); + assert_eq!(account_state.errors[4].error, "Error 5"); + } + + #[test] + fn test_error_limit_exceeded() { + let mut account_state = AccountRunningState { + account_id: 1000u64, + last_incremental_sync_start: 1000, + last_incremental_sync_end: Some(2000), + errors: Vec::new(), + ..Default::default() + }; + + for i in 1..=25 { + account_state.append_error_log(format!("Error {}", i)); + } + + // Should only keep the last 10 errors + assert_eq!(account_state.errors.len(), ERROR_COUNT_PER_ACCOUNT); + assert_eq!(account_state.errors[0].error, "Error 6"); + assert_eq!(account_state.errors[19].error, "Error 25"); + } + + #[test] + fn test_insert_error_after_limit() { + let mut account_state = AccountRunningState { + account_id: 1000u64, + last_incremental_sync_start: 1000, + last_incremental_sync_end: Some(2000), + errors: Vec::new(), + ..Default::default() + }; + + // Insert exactly 10 errors + for i in 1..=20 { + account_state.append_error_log(format!("Error {}", i)); + } + + // Insert one more error to exceed the limit + account_state.append_error_log(String::from("Error 21")); + + assert_eq!(account_state.errors.len(), ERROR_COUNT_PER_ACCOUNT); + assert_eq!(account_state.errors[0].error, "Error 2"); // The first error is removed + assert_eq!(account_state.errors[19].error, "Error 21"); // The last inserted error + } +} diff --git a/src/modules/autoconfig/entity.rs b/src/modules/autoconfig/entity.rs new file mode 100644 index 0000000..262b509 --- /dev/null +++ b/src/modules/autoconfig/entity.rs @@ -0,0 +1,75 @@ +// +// Copyright (c) 2025 rustmailer.com (https://rustmailer.com) +// +// This file is part of the Bichon Email Archiving Project +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +use autoconfig::config::OAuth2Config as XOAuth2Config; +use poem_openapi::Object; +use serde::{Deserialize, Serialize}; + +use crate::modules::account::entity::Encryption; + +#[derive(Debug, Clone, Default, Eq, PartialEq, Serialize, Deserialize, Object)] +pub struct ServerConfig { + /// server hostname or IP address + pub host: String, + /// server port number + pub port: u16, + /// Connection encryption method + pub encryption: Encryption, +} + +impl ServerConfig { + pub fn new(host: String, port: u16, encryption: Encryption) -> Self { + Self { + host, + port, + encryption, + } + } +} + +#[derive(Debug, Clone, Default, Eq, PartialEq, Serialize, Deserialize, Object)] +pub struct OAuth2Config { + /// The authorization server's issuer identifier URL + pub issuer: String, + /// List of scopes requested by the client + pub scope: Vec, + /// URL of the authorization server's authorization endpoint + pub auth_url: String, + /// URL of the authorization server's token endpoint + pub token_url: String, +} + +impl From<&XOAuth2Config> for OAuth2Config { + fn from(value: &XOAuth2Config) -> Self { + Self { + issuer: value.issuer().into(), + scope: value.scope().into_iter().map(Into::into).collect(), + auth_url: value.auth_url().into(), + token_url: value.token_url().into(), + } + } +} + +#[derive(Debug, Clone, Default, Eq, PartialEq, Serialize, Deserialize, Object)] +pub struct MailServerConfig { + /// IMAP server configuration + pub imap: ServerConfig, + /// OAuth 2.0 client configuration parameters + pub oauth2: Option, +} diff --git a/src/modules/autoconfig/load.rs b/src/modules/autoconfig/load.rs new file mode 100644 index 0000000..186b303 --- /dev/null +++ b/src/modules/autoconfig/load.rs @@ -0,0 +1,114 @@ +// +// Copyright (c) 2025 rustmailer.com (https://rustmailer.com) +// +// This file is part of the Bichon Email Archiving Project +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +use crate::modules::autoconfig::entity::{MailServerConfig, ServerConfig}; +use crate::modules::error::code::ErrorCode; +use crate::{ + modules::{ + account::entity::Encryption, autoconfig::CachedMailSettings, error::BichonResult, + }, + raise_error, +}; +use autoconfig::config::{Server, ServerType}; +use email_address::EmailAddress; +use std::str::FromStr; +use tracing::error; + +pub async fn resolve_autoconfig( + email: impl AsRef, +) -> BichonResult> { + let email = email.as_ref(); + let email_address = EmailAddress::from_str(email).map_err(|error| { + raise_error!( + format!("Invalid email address: {email:#?}. {error:#?}"), + ErrorCode::InvalidParameter + ) + })?; + + let domain = email_address.domain(); + // try read local cache first + if let Some(cached_entity) = CachedMailSettings::get(domain).await? { + return Ok(Some(cached_entity.config)); + } + + let config = autoconfig::from_addr(email_address.email().as_ref()) + .await + .map_err(|e| { + error!(email = %email, domain = %domain, error = ?e, "Autoconfig fetch failed"); + raise_error!( + format!( + "Failed to fetch autoconfig for email '{}': {:#?}", + email_address.email(), + e + ), + ErrorCode::AutoconfigFetchFailed + ) + })?; + + let imap_server = config + .email_provider() + .incoming_servers() + .into_iter() + .find(|s| matches!(s.server_type(), ServerType::Imap)); + + let imap_server = match imap_server { + Some(imap) => imap, + None => return Ok(None), + }; + + let get_encryption = |server: &Server| { + server + .security_type() + .map_or(Encryption::None, |encryption| match encryption { + autoconfig::config::SecurityType::Plain => Encryption::None, + autoconfig::config::SecurityType::Starttls => Encryption::StartTls, + autoconfig::config::SecurityType::Tls => Encryption::Ssl, + }) + }; + + let get_port = |server: &Server, encryption: &Encryption, tls_port: u16, non_tls_port: u16| { + server.port().map_or_else( + || match encryption { + Encryption::StartTls => tls_port, + _ => non_tls_port, + }, + ToOwned::to_owned, + ) + }; + + let get_hostname = |server: &Server, default_prefix: &str| { + server.hostname().map_or_else( + || format!("{}.{}", default_prefix, domain), + ToOwned::to_owned, + ) + }; + + let imap_encryption = get_encryption(imap_server); + let imap_config = ServerConfig::new( + get_hostname(imap_server, "imap"), + get_port(imap_server, &imap_encryption, 993, 143), + imap_encryption, + ); + let result = MailServerConfig { + imap: imap_config, + oauth2: config.oauth2().map(|f| f.into()), + }; + CachedMailSettings::add(domain.into(), result.clone()).await?; + Ok(Some(result)) +} diff --git a/src/modules/autoconfig/mod.rs b/src/modules/autoconfig/mod.rs new file mode 100644 index 0000000..387c13e --- /dev/null +++ b/src/modules/autoconfig/mod.rs @@ -0,0 +1,86 @@ +// +// Copyright (c) 2025 rustmailer.com (https://rustmailer.com) +// +// This file is part of the Bichon Email Archiving Project +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +use crate::modules::database::manager::DB_MANAGER; +use crate::modules::database::{delete_impl, async_find_impl, upsert_impl}; +use crate::modules::error::code::ErrorCode; +use crate::raise_error; +use crate::{ + modules::autoconfig::entity::MailServerConfig, modules::error::BichonResult, utc_now, +}; +use native_db::*; +use native_model::{native_model, Model}; +use serde::{Deserialize, Serialize}; + +pub mod entity; +pub mod load; +#[cfg(test)] +mod tests; + +const EXPIRE_TIME_MS: i64 = 30 * 24 * 60 * 60 * 1000; + +#[derive(Clone, Debug, Default, PartialEq, Eq, Deserialize, Serialize)] +#[native_model(id = 3, version = 1)] +#[native_db] +pub struct CachedMailSettings { + #[primary_key] + pub domain: String, + pub config: MailServerConfig, + pub created_at: i64, +} + +impl CachedMailSettings { + pub async fn add(domain: String, config: MailServerConfig) -> BichonResult<()> { + Self { + domain, + config, + created_at: utc_now!(), + } + .save() + .await + } + + async fn save(&self) -> BichonResult<()> { + upsert_impl(DB_MANAGER.meta_db(), self.to_owned()).await + } + + pub async fn get(domain: &str) -> BichonResult> { + if let Some(found) = + async_find_impl::(DB_MANAGER.meta_db(), domain.to_string()).await? + { + if (utc_now!() - found.created_at) > EXPIRE_TIME_MS { + let domain = domain.to_string(); + delete_impl(DB_MANAGER.meta_db(), |rw| { + rw.get() + .primary::(domain) + .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))? + .ok_or_else(|| { + raise_error!("auto config cache miss".into(), ErrorCode::InternalError) + }) + }) + .await?; + Ok(None) + } else { + Ok(Some(found)) + } + } else { + Ok(None) + } + } +} diff --git a/src/modules/autoconfig/tests.rs b/src/modules/autoconfig/tests.rs new file mode 100644 index 0000000..c48d7c1 --- /dev/null +++ b/src/modules/autoconfig/tests.rs @@ -0,0 +1,24 @@ +// +// Copyright (c) 2025 rustmailer.com (https://rustmailer.com) +// +// This file is part of the Bichon Email Archiving Project +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +#[tokio::test] +async fn test() { + let config = autoconfig::from_addr("test@gmail.com").await.unwrap(); + println!("{:#?}", config); +} diff --git a/src/modules/cache/imap/mailbox.rs b/src/modules/cache/imap/mailbox.rs new file mode 100644 index 0000000..f3aa4c8 --- /dev/null +++ b/src/modules/cache/imap/mailbox.rs @@ -0,0 +1,207 @@ +// +// Copyright (c) 2025 rustmailer.com (https://rustmailer.com) +// +// This file is part of the Bichon Email Archiving Project +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +use crate::{ + decode_mailbox_name, encode_mailbox_name, + modules::{ + database::{ + batch_delete_impl, batch_insert_impl, batch_upsert_impl, filter_by_secondary_key_impl, + manager::DB_MANAGER, + }, + error::{code::ErrorCode, BichonResult}, + }, + raise_error, +}; +use async_imap::types::{Name, NameAttribute}; +use itertools::Itertools; +use native_db::*; +use native_model::{native_model, Model}; +use poem_openapi::{Enum, Object}; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize, Object)] +#[native_model(id = 1, version = 1)] +#[native_db] +pub struct MailBox { + /// The unique identifier for the mailbox + #[primary_key] + pub id: u64, + /// The ID of the account associated with the mailbox + #[secondary_key] + pub account_id: u64, + /// The unique, decoded, human-readable name of the mailbox (e.g., "INBOX", "Sent Items"). + /// This is the decoded name as presented to users, derived from the IMAP server's mailbox name + /// (e.g., after decoding UTF-7 or other encodings per RFC 3501). + pub name: String, + /// Optional delimiter used to separate mailbox names in a hierarchy (e.g., "/" or "."). + /// Used in IMAP to structure nested mailboxes (e.g., "INBOX/Archive"). + pub delimiter: Option, + /// List of attributes associated with the mailbox (e.g., `\NoSelect`, `\Deleted`). + /// These indicate special properties, such as whether the mailbox can hold messages. + pub attributes: Vec, + /// The number of messages that currently exist in the mailbox. + pub exists: u32, + /// Optional number of unseen messages in the mailbox (i.e., messages without the `\Seen` flag). + pub unseen: Option, + /// The next unique identifier (UID) that will be assigned to a new message in the mailbox. + /// If `None`, the IMAP server has not provided this information. + pub uid_next: Option, + /// The validity identifier for UIDs in this mailbox, used to ensure UID consistency across sessions. + /// If `None`, the IMAP server has not provided this information. + pub uid_validity: Option, +} + +impl MailBox { + pub fn encoded_name(&self) -> String { + encode_mailbox_name!(&self.name) + } + + // pub async fn batch_delete(mailboxes: Vec) -> BichonResult<()> { + // batch_delete_impl(DB_MANAGER.envelope_db(), move |rw| { + // let mut to_deleted = Vec::new(); + // for mailbox in mailboxes { + // let retrived = rw + // .get() + // .primary::(mailbox.id) + // .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))?; + // if let Some(retrived) = retrived { + // to_deleted.push(retrived); + // } + // } + // Ok(to_deleted) + // }) + // .await?; + // Ok(()) + // } + + // pub async fn get(id: u64) -> RustMailerResult { + // let result = async_find_impl::(DB_MANAGER.envelope_db(), id).await?; + // Ok(result.ok_or_else(|| { + // raise_error!( + // format!("mailbox {} not found", id), + // ErrorCode::InternalError + // ) + // })?) + // } + + // pub async fn delete(id: u64) -> BichonResult<()> { + // delete_impl(DB_MANAGER.envelope_db(), move |rw| { + // rw.get() + // .primary::(id) + // .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))? + // .ok_or_else(|| raise_error!("mailbox missing".into(), ErrorCode::InternalError)) + // }) + // .await + // } + + pub async fn list_all(account_id: u64) -> BichonResult> { + filter_by_secondary_key_impl(DB_MANAGER.envelope_db(), MailBoxKey::account_id, account_id) + .await + } + + pub async fn batch_insert(mailboxes: &[MailBox]) -> BichonResult<()> { + batch_insert_impl(DB_MANAGER.envelope_db(), mailboxes.to_vec()).await + } + + pub async fn batch_upsert(mailboxes: &[MailBox]) -> BichonResult<()> { + batch_upsert_impl(DB_MANAGER.envelope_db(), mailboxes.to_vec()).await + } + + pub async fn clean(account_id: u64) -> BichonResult<()> { + batch_delete_impl(DB_MANAGER.envelope_db(), move |rw| { + let mailboxes: Vec = rw + .scan() + .secondary::(MailBoxKey::account_id) + .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))? + .start_with(account_id) + .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))? + .try_collect() + .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))?; + Ok(mailboxes) + }) + .await?; + Ok(()) + } +} + +#[derive(Clone, Debug, Eq, PartialEq, Deserialize, Serialize, Object)] +pub struct Attribute { + pub attr: AttributeEnum, + pub extension: Option, +} + +impl Attribute { + pub fn new(attr: AttributeEnum, extension: Option) -> Self { + Self { attr, extension } + } +} + +#[derive(Clone, Debug, Eq, PartialEq, Deserialize, Serialize, Enum)] +pub enum AttributeEnum { + NoInferiors, + NoSelect, + Marked, + Unmarked, + All, + Archive, + Drafts, + Flagged, + Junk, + Sent, + Trash, + Extension, + Unknown, +} + +impl From<&Name> for MailBox { + fn from(value: &Name) -> Self { + let name = decode_mailbox_name!(value.name().to_string()); + let delimiter = value.delimiter().map(|f| f.to_owned()); + let attributes: Vec = value.attributes().iter().map(|na| na.into()).collect(); + //The remaining parts will be supplemented during the examine_mailbox process. + MailBox { + name, + delimiter, + attributes, + ..Default::default() //has_synced is initialized to false here + } + } +} + +impl From<&NameAttribute<'_>> for Attribute { + fn from(value: &NameAttribute) -> Self { + match value { + NameAttribute::NoInferiors => Attribute::new(AttributeEnum::NoInferiors, None), + NameAttribute::NoSelect => Attribute::new(AttributeEnum::NoSelect, None), + NameAttribute::Marked => Attribute::new(AttributeEnum::Marked, None), + NameAttribute::Unmarked => Attribute::new(AttributeEnum::Unmarked, None), + NameAttribute::All => Attribute::new(AttributeEnum::All, None), + NameAttribute::Archive => Attribute::new(AttributeEnum::Archive, None), + NameAttribute::Drafts => Attribute::new(AttributeEnum::Drafts, None), + NameAttribute::Flagged => Attribute::new(AttributeEnum::Flagged, None), + NameAttribute::Junk => Attribute::new(AttributeEnum::Junk, None), + NameAttribute::Sent => Attribute::new(AttributeEnum::Sent, None), + NameAttribute::Trash => Attribute::new(AttributeEnum::Trash, None), + NameAttribute::Extension(s) => { + Attribute::new(AttributeEnum::Extension, Some(s.to_string())) + } + _ => Attribute::new(AttributeEnum::Unknown, None), + } + } +} diff --git a/src/modules/cache/imap/mod.rs b/src/modules/cache/imap/mod.rs new file mode 100644 index 0000000..fce4671 --- /dev/null +++ b/src/modules/cache/imap/mod.rs @@ -0,0 +1,66 @@ +// +// Copyright (c) 2025 rustmailer.com (https://rustmailer.com) +// +// This file is part of the Bichon Email Archiving Project +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +use std::sync::LazyLock; + +use crate::modules::{account::state::AccountRunningState, database::ModelsAdapter}; +use ahash::{AHashMap, AHashSet}; +use mailbox::MailBox; +use native_db::Models; + +pub mod mailbox; +pub mod sync; +pub mod task; + +pub static MAILBOX_MODELS: LazyLock = LazyLock::new(|| { + let mut adapter = ModelsAdapter::new(); + adapter.register_model::(); + adapter.register_model::(); + adapter.models +}); + +pub fn find_missing_mailboxes( + local_mailboxes: &[MailBox], + server_mailboxes: &[MailBox], +) -> Vec { + let local_names: AHashSet<_> = local_mailboxes.iter().map(|m| &m.name).collect(); + server_mailboxes + .iter() + .filter(|m| !local_names.contains(&m.name)) + .cloned() + .collect() +} + +pub fn find_intersecting_mailboxes( + local_mailboxes: &[MailBox], + remote_mailboxes: &[MailBox], +) -> Vec<(MailBox, MailBox)> { + let local_map: AHashMap<_, _> = local_mailboxes + .iter() + .map(|m| (m.name.clone(), m.clone())) + .collect(); + remote_mailboxes + .iter() + .filter_map(|m| { + local_map + .get(&m.name) + .map(|local_mailbox| (local_mailbox.clone(), m.clone())) + }) + .collect() +} diff --git a/src/modules/cache/imap/sync/flow.rs b/src/modules/cache/imap/sync/flow.rs new file mode 100644 index 0000000..b52d6f7 --- /dev/null +++ b/src/modules/cache/imap/sync/flow.rs @@ -0,0 +1,379 @@ +// +// Copyright (c) 2025 rustmailer.com (https://rustmailer.com) +// +// This file is part of the Bichon Email Archiving Project +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +use crate::{ + modules::{ + account::{migration::AccountModel, state::AccountRunningState}, + cache::{ + imap::{ + find_intersecting_mailboxes, find_missing_mailboxes, + mailbox::MailBox, + sync::rebuild::{rebuild_mailbox_cache, rebuild_mailbox_cache_since_date}, + }, + SEMAPHORE, + }, + context::executors::MAIL_CONTEXT, + error::{code::ErrorCode, BichonError, BichonResult}, + indexer::manager::ENVELOPE_INDEX_MANAGER, + }, + raise_error, +}; +use std::time::Instant; +use tracing::{debug, error, info, warn}; + +pub const BATCH_SIZE: u32 = 50; + +pub async fn fetch_and_save_since_date( + account: &AccountModel, + date: &str, + mailbox: &MailBox, +) -> BichonResult { + let account_id = account.id; + let executor = MAIL_CONTEXT.imap(account_id).await?; + let uid_list = executor + .uid_search(&mailbox.encoded_name(), format!("SINCE {date}").as_str()) + .await?; + + let len = uid_list.len(); + if len == 0 { + return Ok(0); + } + + let folder_limit = account.folder_limit; + // sort small -> bigger + let mut uid_vec: Vec = uid_list.into_iter().collect(); + uid_vec.sort(); + + if let Some(limit) = folder_limit { + let limit = limit.max(100) as usize; + if len > limit { + uid_vec = uid_vec.split_off(len - limit as usize); + } + } + + // let semaphore = Arc::new(Semaphore::new(5)); + + let uid_batches = generate_uid_sequence_hashset(uid_vec, BATCH_SIZE as usize, false); + AccountRunningState::set_initial_current_syncing_folder( + account_id, + mailbox.name.clone(), + uid_batches.len() as u32, + ) + .await?; + for (index, batch) in uid_batches.into_iter().enumerate() { + AccountRunningState::set_current_sync_batch_number( + account_id, + mailbox.name.clone(), + (index + 1) as u32, + ) + .await?; + let executor = MAIL_CONTEXT.imap(account_id).await?; + // Fetch metadata for the current batch of UIDs + executor + .uid_batch_retrieve_emails(account_id, mailbox.id, &batch, &mailbox.encoded_name()) + .await?; + } + Ok(len) +} + +pub async fn fetch_and_save_full_mailbox( + account: &AccountModel, + mailbox: &MailBox, + total: u32, +) -> BichonResult { + let mailbox_id = mailbox.id; + let account_id = account.id; + let folder_limit = account.folder_limit; + let total_to_fetch = match folder_limit { + Some(limit) if limit < total => total.min(limit.max(100)), + _ => total, + }; + let page_size = if let Some(limit) = folder_limit { + limit.max(100).min(BATCH_SIZE as u32) + } else { + BATCH_SIZE as u32 + }; + + let total_batches = total_to_fetch.div_ceil(page_size); + let desc = folder_limit.is_some(); + + let mut inserted_count = 0; + + AccountRunningState::set_initial_current_syncing_folder( + account_id, + mailbox.name.clone(), + total_batches, + ) + .await?; + info!( + "Starting full mailbox sync for '{}', total={}, limit={:?}, batches={}, desc={}", + mailbox.name, total, folder_limit, total_batches, desc + ); + for page in 1..=total_batches { + AccountRunningState::set_current_sync_batch_number(account_id, mailbox.name.clone(), page) + .await?; + let executor = MAIL_CONTEXT.imap(account_id).await?; + let count = executor + .batch_retrieve_emails( + account_id, + mailbox_id, + page as u64, + page_size as u64, + &mailbox.encoded_name(), + desc, + ) + .await?; + inserted_count += count; + info!( + "Batch insertion completed for mailbox: {}, current page: {}, inserted count: {}", + &mailbox.name, page, count + ); + } + Ok(inserted_count) +} + +/// # Example +/// +/// ```rust +/// use std::collections::HashSet; +/// +/// let mut uids = HashSet::new(); +/// uids.extend([1, 2, 3, 5, 6, 7, 9, 10, 11, 15]); +/// +/// let chunks = generate_uid_sequence_hashset(uids, 6, false); +/// assert_eq!(chunks, vec![ +/// "1:3,5:7".to_string(), +/// "9:11,15".to_string() +/// ]); +/// ``` +/// +/// This splits the UIDs into chunks of 6, compresses each chunk into ranges, +/// and returns a vector like: `["1:3,5:7", "9:11,15"]`. +/// +pub fn generate_uid_sequence_hashset( + unique_nums: Vec, + chunk_size: usize, + desc: bool, +) -> Vec { + assert!(!unique_nums.is_empty()); + // let mut nums: Vec = unique_nums.into_iter().collect(); + // nums.sort(); + let mut nums = unique_nums; + if desc { + nums.reverse(); + } + + let mut result = Vec::new(); + + for chunk in nums.chunks(chunk_size) { + let compressed = compress_uid_list(chunk.to_vec()); + result.push(compressed); + } + + result +} + +pub fn compress_uid_list(nums: Vec) -> String { + if nums.is_empty() { + return String::new(); + } + + let mut sorted_nums = nums; + sorted_nums.sort(); + + let mut result = Vec::new(); + let mut current_range_start = sorted_nums[0]; + let mut current_range_end = sorted_nums[0]; + + for &n in sorted_nums.iter().skip(1) { + if n == current_range_end + 1 { + current_range_end = n; + } else { + if current_range_start == current_range_end { + result.push(current_range_start.to_string()); + } else { + result.push(format!("{}:{}", current_range_start, current_range_end)); + } + current_range_start = n; + current_range_end = n; + } + } + + if current_range_start == current_range_end { + result.push(current_range_start.to_string()); + } else { + result.push(format!("{}:{}", current_range_start, current_range_end)); + } + + result.join(",") +} + +pub async fn reconcile_mailboxes( + account: &AccountModel, + remote_mailboxes: &[MailBox], + local_mailboxes: &[MailBox], +) -> BichonResult<()> { + let start_time = Instant::now(); + let existing_mailboxes = find_intersecting_mailboxes(local_mailboxes, remote_mailboxes); + let account_id = account.id; + if !existing_mailboxes.is_empty() { + let mut mailboxes_to_update = Vec::with_capacity(existing_mailboxes.len()); + for (local_mailbox, remote_mailbox) in &existing_mailboxes { + if local_mailbox.uid_validity != remote_mailbox.uid_validity { + if remote_mailbox.uid_validity.is_none() { + warn!( + "Account {}: Mailbox '{}' has invalid uid_validity (None). Skipping sync for this mailbox.", + account_id, local_mailbox.name + ); + continue; + } + info!( + "Account {}: Mailbox '{}' detected with changed uid_validity (local: {:#?}, remote: {:#?}). \ + The mailbox data may be invalid, resetting its envelopes and rebuilding the cache.", + account_id, local_mailbox.name, &local_mailbox.uid_validity, &remote_mailbox.uid_validity + ); + + match &account.date_since { + Some(date_since) => { + rebuild_mailbox_cache_since_date( + account, + local_mailbox.id, + date_since, + remote_mailbox, + ) + .await?; + } + None => { + rebuild_mailbox_cache(account, local_mailbox, remote_mailbox).await?; + } + } + } else { + perform_incremental_sync(account, local_mailbox, remote_mailbox).await?; + } + if let Some(state) = AccountRunningState::get(account.id).await? { + if !state.is_initial_sync_completed { + AccountRunningState::set_folder_initial_sync_completed( + account_id, + local_mailbox.name.clone(), + ) + .await?; + } + } + + mailboxes_to_update.push(remote_mailbox.clone()); + } + //The metadata of this mailbox must only be updated after a successful synchronization; + //otherwise, it may cause synchronization errors and result in missing emails in the local sync results. + MailBox::batch_upsert(&mailboxes_to_update).await?; + } + + debug!( + "Checked mailbox folders for account ID: {}. Compared local and server folders to identify changes. Elapsed time: {} seconds", + account.id, + start_time.elapsed().as_secs() + ); + + let missing_mailboxes = find_missing_mailboxes(local_mailboxes, remote_mailboxes); + if !missing_mailboxes.is_empty() { + MailBox::batch_insert(&missing_mailboxes).await?; + let mut handles = Vec::new(); + + for mailbox in &missing_mailboxes { + if mailbox.exists > 0 { + let account = account.clone(); + let mailbox = mailbox.clone(); + match SEMAPHORE.clone().acquire_owned().await { + Ok(permit) => { + let handle: tokio::task::JoinHandle> = + tokio::spawn(async move { + let _permit = permit; + match &account.date_since { + Some(date_since) => { + rebuild_mailbox_cache_since_date( + &account, mailbox.id, date_since, &mailbox, + ) + .await + } + None => { + rebuild_mailbox_cache(&account, &mailbox, &mailbox).await + } + } + }); + handles.push(handle); + } + Err(err) => { + error!("Failed to acquire semaphore permit, error: {:#?}", err); + } + } + } + } + + for task in handles { + match task.await { + Ok(Ok(())) => {} + Ok(Err(err)) => return Err(err), + Err(e) => return Err(raise_error!(format!("{:#?}", e), ErrorCode::InternalError)), + } + } + } + Ok(()) +} + +//only check new emails and sync +async fn perform_incremental_sync( + account: &AccountModel, + local_mailbox: &MailBox, + remote_mailbox: &MailBox, +) -> BichonResult<()> { + if remote_mailbox.exists > 0 { + let local_max_uid = ENVELOPE_INDEX_MANAGER + .get_max_uid(account.id, local_mailbox.id) + .await?; + match local_max_uid { + Some(max_uid) => { + let executor = MAIL_CONTEXT.imap(account.id).await?; + executor + .fetch_new_mail(account.id, local_mailbox, max_uid + 1) + .await?; + } + None => { + info!( + "No maximum UID found in index for mailbox, assuming local cache is missing." + ); + + match &account.date_since { + Some(date_since) => { + fetch_and_save_since_date( + account, + date_since.since_date()?.as_str(), + remote_mailbox, + ) + .await?; + } + None => { + fetch_and_save_full_mailbox(account, remote_mailbox, remote_mailbox.exists) + .await?; + } + } + } + } + } + + Ok(()) +} diff --git a/src/modules/cache/imap/sync/mod.rs b/src/modules/cache/imap/sync/mod.rs new file mode 100644 index 0000000..fefa9c7 --- /dev/null +++ b/src/modules/cache/imap/sync/mod.rs @@ -0,0 +1,113 @@ +// +// Copyright (c) 2025 rustmailer.com (https://rustmailer.com) +// +// This file is part of the Bichon Email Archiving Project +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +use crate::{ + modules::{ + account::{ + dispatcher::STATUS_DISPATCHER, + migration::{AccountModel, AccountType}, + state::AccountRunningState, + }, + cache::imap::mailbox::MailBox, + error::BichonResult, + }, + utc_now, +}; +use flow::reconcile_mailboxes; +use rebuild::{rebuild_cache, rebuild_cache_since_date}; +use std::time::Instant; +use sync_folders::get_sync_folders; +use sync_type::{determine_sync_type, SyncType}; +use tracing::debug; + +pub mod flow; +pub mod rebuild; +pub mod sync_folders; +pub mod sync_type; + +pub async fn execute_imap_sync(account: &AccountModel) -> BichonResult<()> { + assert_eq!(account.account_type, AccountType::IMAP); + let start_time = Instant::now(); + let account_id = account.id; + let sync_type = determine_sync_type(account).await?; + + if matches!(sync_type, SyncType::SkipSync) { + return Ok(()); + } + let remote_mailboxes = get_sync_folders(account).await?; + if matches!(sync_type, SyncType::InitialSync) { + AccountRunningState::set_initial_sync_start(account_id).await?; + let result = match &account.date_since { + Some(date_since) => { + rebuild_cache_since_date(account, &remote_mailboxes, date_since).await + } + None => rebuild_cache(account, &remote_mailboxes).await, + }; + match result { + Ok(_) => { + AccountRunningState::set_initial_sync_completed(account_id).await?; + } + Err(e) => { + STATUS_DISPATCHER + .append_error( + account_id, + format!("Initial sync failed for the account, error: {:#?}", e), + ) + .await; + AccountRunningState::set_initial_sync_failed(account_id).await?; + } + } + return Ok(()); + } + + if let Some(state) = AccountRunningState::get(account_id).await? { + let now = utc_now!(); + const COOLDOWN_MS: i64 = 60 * 1000; + let mut should_skip = false; + if let Some(time) = state.initial_sync_end_time { + if now - time < COOLDOWN_MS { + should_skip = true; + } + } + if let Some(time) = state.initial_sync_failed_time { + if now - time < COOLDOWN_MS { + should_skip = true; + } + } + if should_skip { + return Ok(()); + } + } + AccountRunningState::set_incremental_sync_start(account.id).await?; + let local_mailboxes = MailBox::list_all(account_id).await?; + reconcile_mailboxes(account, &remote_mailboxes, &local_mailboxes).await?; + let elapsed_time = start_time.elapsed().as_secs(); + debug!( + "Account{{{}}} Incremental sync completed: {} seconds elapsed.", + account.email, elapsed_time + ); + + if let Some(state) = AccountRunningState::get(account.id).await? { + if !state.is_initial_sync_completed { + AccountRunningState::set_initial_sync_completed(account_id).await?; + } + } + AccountRunningState::set_incremental_sync_end(account_id).await?; + Ok(()) +} diff --git a/src/modules/cache/imap/sync/rebuild.rs b/src/modules/cache/imap/sync/rebuild.rs new file mode 100644 index 0000000..77ec860 --- /dev/null +++ b/src/modules/cache/imap/sync/rebuild.rs @@ -0,0 +1,200 @@ +// +// Copyright (c) 2025 rustmailer.com (https://rustmailer.com) +// +// This file is part of the Bichon Email Archiving Project +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +use crate::{ + modules::{ + account::{migration::AccountModel, since::DateSince}, + cache::{ + imap::{ + mailbox::MailBox, + sync::flow::{fetch_and_save_full_mailbox, fetch_and_save_since_date}, + }, + SEMAPHORE, + }, + error::{code::ErrorCode, BichonError, BichonResult}, + indexer::manager::{EML_INDEX_MANAGER, ENVELOPE_INDEX_MANAGER}, + }, + raise_error, +}; +use std::time::Instant; +use tracing::{error, info}; + +pub async fn rebuild_cache( + account: &AccountModel, + remote_mailboxes: &[MailBox], +) -> BichonResult<()> { + let start_time = Instant::now(); + let mut total_inserted = 0; + MailBox::batch_insert(remote_mailboxes).await?; + + let mut handles = Vec::new(); + for mailbox in remote_mailboxes { + if mailbox.exists == 0 { + info!( + "Account {}: Mailbox '{}' on the remote server has no emails. Skipping fetch for this mailbox.", + account.id, &mailbox.name + ); + continue; + } + let account = account.clone(); + let mailbox = mailbox.clone(); + match SEMAPHORE.clone().acquire_owned().await { + Ok(permit) => { + let handle: tokio::task::JoinHandle> = + tokio::spawn(async move { + let _permit = permit; // Ensure permit is released when task finishes + fetch_and_save_full_mailbox(&account, &mailbox, mailbox.exists).await + }); + handles.push(handle); + } + Err(err) => { + error!("Failed to acquire semaphore permit, error: {:#?}", err); + } + } + } + for task in handles { + match task.await { + Ok(Ok(count)) => { + total_inserted += count; + } + Ok(Err(err)) => return Err(err), + Err(e) => return Err(raise_error!(format!("{:#?}", e), ErrorCode::InternalError)), + } + } + let elapsed_time = start_time.elapsed().as_secs(); + info!( + "Rebuild account cache completed: {} envelopes inserted. {} secs elapsed. \ + This is a full data fetch as there was no local cache data available.", + total_inserted, elapsed_time + ); + Ok(()) +} + +pub async fn rebuild_cache_since_date( + account: &AccountModel, + remote_mailboxes: &[MailBox], + date_since: &DateSince, +) -> BichonResult<()> { + let start_time = Instant::now(); + let mut total_inserted = 0; + let date = date_since.since_date()?; + MailBox::batch_insert(remote_mailboxes).await?; + + let mut handles = Vec::new(); + for mailbox in remote_mailboxes { + if mailbox.exists == 0 { + info!( + "Account {}: Mailbox '{}' on the remote server has no emails. Skipping fetch for this mailbox.", + account.id, &mailbox.name + ); + continue; + } + let account = account.clone(); + let mailbox = mailbox.clone(); + let date = date.clone(); + match SEMAPHORE.clone().acquire_owned().await { + Ok(permit) => { + let handle: tokio::task::JoinHandle> = + tokio::spawn(async move { + let _permit = permit; // Ensure permit is released when task finishes + fetch_and_save_since_date(&account, date.as_str(), &mailbox).await + }); + handles.push(handle); + } + Err(err) => { + error!("Failed to acquire semaphore permit, error: {:#?}", err); + } + } + } + for task in handles { + match task.await { + Ok(Ok(count)) => { + total_inserted += count; + } + Ok(Err(err)) => return Err(err), + Err(e) => return Err(raise_error!(format!("{:#?}", e), ErrorCode::InternalError)), + } + } + let elapsed_time = start_time.elapsed().as_secs(); + info!( + "Rebuild account cache completed: {} envelopes inserted. {} secs elapsed. \ + Data fetched from server starting from the specified date: {}.", + total_inserted, elapsed_time, date + ); + Ok(()) +} + +pub async fn rebuild_mailbox_cache( + account: &AccountModel, + local_mailbox: &MailBox, + remote_mailbox: &MailBox, +) -> BichonResult<()> { + ENVELOPE_INDEX_MANAGER + .delete_mailbox_envelopes(account.id, vec![local_mailbox.id]) + .await?; + EML_INDEX_MANAGER + .delete_mailbox_envelopes(account.id, vec![local_mailbox.id]) + .await?; + if remote_mailbox.exists == 0 { + info!( + "Account {}: Mailbox '{}' has no emails on the remote server. The mailbox is empty, no envelopes to fetch.", + account.id, + &local_mailbox.name + ); + return Ok(()); // Skip if the mailbox has no emails + } + + let inserted_count = + fetch_and_save_full_mailbox(account, remote_mailbox, remote_mailbox.exists).await?; + info!( + "Account {}: Successfully rebuild mailbox cache, inserted {} envelopes for mailbox '{}'.", + account.id, inserted_count, &local_mailbox.name + ); + Ok(()) +} + +pub async fn rebuild_mailbox_cache_since_date( + account: &AccountModel, + local_mailbox_id: u64, + date_since: &DateSince, + remote: &MailBox, +) -> BichonResult<()> { + ENVELOPE_INDEX_MANAGER + .delete_mailbox_envelopes(account.id, vec![local_mailbox_id]) + .await?; + EML_INDEX_MANAGER + .delete_mailbox_envelopes(account.id, vec![local_mailbox_id]) + .await?; + if remote.exists == 0 { + info!( + "Account {}: Mailbox '{}' has no emails on the remote server. The mailbox is empty, no envelopes to fetch.", + account.id, + &remote.name + ); + return Ok(()); // Skip if the mailbox has no emails + } + + let count = + fetch_and_save_since_date(account, date_since.since_date()?.as_str(), remote).await?; + info!( + "Account {}: Successfully rebuild mailbox cache, inserted {} envelopes for mailbox '{}'.", + account.id, count, &remote.name + ); + Ok(()) +} diff --git a/src/modules/cache/imap/sync/sync_folders.rs b/src/modules/cache/imap/sync/sync_folders.rs new file mode 100644 index 0000000..e0a9345 --- /dev/null +++ b/src/modules/cache/imap/sync/sync_folders.rs @@ -0,0 +1,184 @@ +// +// Copyright (c) 2025 rustmailer.com (https://rustmailer.com) +// +// This file is part of the Bichon Email Archiving Project +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +use std::collections::BTreeSet; + +use crate::{ + decode_mailbox_name, + modules::{ + account::migration::{AccountModel, AccountType}, + cache::imap::mailbox::{AttributeEnum, MailBox}, + context::executors::MAIL_CONTEXT, + error::{code::ErrorCode, BichonResult}, + mailbox::list::convert_names_to_mailboxes, + }, + raise_error, +}; +use async_imap::types::Name; +use tracing::{debug, info, warn}; + +pub async fn get_sync_folders(account: &AccountModel) -> BichonResult> { + assert_eq!(account.account_type, AccountType::IMAP); + let executor = MAIL_CONTEXT.imap(account.id).await?; + let names = executor.list_all_mailboxes().await?; + if names.is_empty() { + warn!( + "Account {}: No mailboxes returned from IMAP server.", + account.id + ); + return Err(raise_error!(format!( + "No mailboxes returned from IMAP server for account {}. This is unexpected and may indicate an issue with the IMAP server.", + &account.id + ), ErrorCode::ImapUnexpectedResult)); + } + let mailboxes: Vec<(MailBox, Name)> = names.into_iter().map(|n| ((&n).into(), n)).collect(); + + for (mailbox, _) in &mailboxes { + debug!( + "[MAILBOX DEBUG] Account {}: mailbox='{}', attributes={:?}", + account.id, mailbox.name, mailbox.attributes + ); + } + + detect_mailbox_changes( + account, + mailboxes.iter().map(|(m, _)| m.name.clone()).collect(), + ) + .await?; + let account = AccountModel::get(account.id).await?; + let subscribed = &account.sync_folders.unwrap_or_default(); + let is_noselect = |mailbox: &MailBox| { + mailbox + .attributes + .iter() + .any(|attr| matches!(attr.attr, AttributeEnum::NoSelect)) + }; + + let is_default_mailbox = |mailbox: &MailBox| { + mailbox.name.eq_ignore_ascii_case("INBOX") + || mailbox + .attributes + .iter() + .any(|attr| matches!(attr.attr, AttributeEnum::Sent)) + }; + + let mut matched_mailboxes: Vec<&Name> = if !subscribed.is_empty() { + mailboxes + .iter() + .filter(|(mailbox, _)| subscribed.contains(&mailbox.name) && !is_noselect(mailbox)) + .map(|(_, name)| name) + .collect() + } else { + Vec::new() + }; + + if matched_mailboxes.is_empty() { + matched_mailboxes = mailboxes + .iter() + .filter(|(mailbox, _)| !is_noselect(mailbox) && is_default_mailbox(mailbox)) + .map(|(_, name)| name) + .collect(); + + debug!( + "[MAILBOX DEBUG] Account {}: matched_mailboxes (default selection) = {:?}", + account.id, + matched_mailboxes + .iter() + .map(|n| decode_mailbox_name!(n.name().to_string())) + .collect::>() + ); + + if !matched_mailboxes.is_empty() { + let sync_folders: Vec = matched_mailboxes + .iter() + .map(|n| decode_mailbox_name!(n.name().to_string())) + .collect(); + AccountModel::update_sync_folders(account.id, sync_folders).await?; + } else { + warn!( + "Account {}: No subscribed mailboxes found. This is unexpected — IMAP server should at least provide INBOX.", + account.id + ); + return Err(raise_error!(format!( + "No subscribed mailboxes found for account {}. This is unexpected — IMAP server should at least provide INBOX.", + &account.id + ), ErrorCode::ImapUnexpectedResult)); + } + } + convert_names_to_mailboxes(account.id, matched_mailboxes).await +} + +pub async fn detect_mailbox_changes( + account: &AccountModel, + all_names: BTreeSet, +) -> BichonResult<()> { + if account.known_folders.is_none() { + // First time sync: just save without comparing + AccountModel::update_known_folders(account.id, all_names).await?; + return Ok(()); + } + let known_folders = account.known_folders.clone().unwrap_or_default(); + // Compute differences + let new_folders: Vec = all_names.difference(&known_folders).cloned().collect(); + let deleted_folders: Vec = known_folders.difference(&all_names).cloned().collect(); + + let has_changes = !new_folders.is_empty() || !deleted_folders.is_empty(); + let sync_folders = account.sync_folders.as_deref().unwrap_or_default(); + // Handle deleted folders in sync_folders + if !deleted_folders.is_empty() { + // Check if any deleted folders are in sync_folders + let remaining_sync_folders: Vec = sync_folders + .iter() + .filter(|folder| !deleted_folders.contains(folder)) + .cloned() + .collect(); + + // If sync_folders changed, update them + if remaining_sync_folders.len() != sync_folders.len() { + let removed_count = sync_folders.len() - remaining_sync_folders.len(); + info!( + "Account {}: Removed {} deleted folders from sync_folders", + account.id, removed_count + ); + // Note: When all subscribed folders are deleted (remaining_sync_folders empty), + // the system's default behavior is to automatically fall back to syncing + // only the default folders (INBOX and Sent) in subsequent operations + AccountModel::update_sync_folders(account.id, remaining_sync_folders).await?; + } + + info!( + "Account {}: Folders deleted: {:?}", + account.id, deleted_folders + ); + } + + // Fire events for new folders if needed + if !new_folders.is_empty() { + info!( + "Account {}: New folders detected: {:?}", + account.id, new_folders + ); + } + + // Update known folders only if there were changes + if has_changes { + AccountModel::update_known_folders(account.id, all_names).await?; + } + Ok(()) +} diff --git a/src/modules/cache/imap/sync/sync_type.rs b/src/modules/cache/imap/sync/sync_type.rs new file mode 100644 index 0000000..9c3abed --- /dev/null +++ b/src/modules/cache/imap/sync/sync_type.rs @@ -0,0 +1,68 @@ +// +// Copyright (c) 2025 rustmailer.com (https://rustmailer.com) +// +// This file is part of the Bichon Email Archiving Project +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +use crate::{ + modules::{ + account::{migration::AccountModel, state::AccountRunningState}, + error::BichonResult, + }, + utc_now, +}; + +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum SyncType { + /// Initial sync, used when fetching all messages for the first time. + InitialSync, + /// Incremental synchronization, typically used for updates or fetching new data since the last sync. + IncrementalSync, + /// Skip synchronization, used when it's not yet time to perform the next sync. + SkipSync, +} + +pub async fn determine_sync_type(account: &AccountModel) -> BichonResult { + Ok(match AccountRunningState::get(account.id).await? { + Some(info) => { + let now = utc_now!(); + let incremental_sync = is_time_for_incremental_sync( + now, + info.last_incremental_sync_start, + account.sync_interval_min.unwrap(), + ); + + if incremental_sync { + SyncType::IncrementalSync + } else { + SyncType::SkipSync + } + } + None => { + AccountRunningState::add(account.id).await?; + SyncType::InitialSync + } + }) +} + +/// Check if it's time for an incremental sync based on the provided interval. +fn is_time_for_incremental_sync( + now: i64, + last_incremental_sync_at: i64, + sync_interval_min: i64, +) -> bool { + now - last_incremental_sync_at > (sync_interval_min * 60 * 1000) +} diff --git a/src/modules/cache/imap/task.rs b/src/modules/cache/imap/task.rs new file mode 100644 index 0000000..a30fae6 --- /dev/null +++ b/src/modules/cache/imap/task.rs @@ -0,0 +1,117 @@ +// +// Copyright (c) 2025 rustmailer.com (https://rustmailer.com) +// +// This file is part of the Bichon Email Archiving Project +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +use crate::modules::account::entity::AuthType; +use crate::modules::cache::imap::sync::execute_imap_sync; +use crate::modules::common::periodic::{PeriodicTask, TaskHandle}; +use crate::modules::oauth2::token::OAuth2AccessToken; +use crate::modules::{ + account::{dispatcher::STATUS_DISPATCHER, migration::AccountModel}, + error::BichonResult, +}; +use crate::utc_now; +use dashmap::DashMap; +use std::sync::atomic::{AtomicI64, Ordering}; +use std::{sync::LazyLock, time::Duration}; +use tracing::{error, warn}; + +static _DESCRIPTION: &str = "This task periodically synchronizes mailbox data for a specified account, ensuring that all local data is up-to-date."; +const TASK_INTERVAL: Duration = Duration::from_secs(10); +pub static SYNC_TASKS: LazyLock = LazyLock::new(AccountSyncTask::new); +static LAST_WARN_TIME: AtomicI64 = AtomicI64::new(0); +const WARN_INTERVAL_MS: i64 = 600_000; + +pub struct AccountSyncTask { + tasks: DashMap, +} + +impl AccountSyncTask { + pub fn new() -> Self { + Self { + tasks: DashMap::new(), + } + } + + pub async fn start_account_sync_task(&self, account_id: u64, email: String) { + let task_name = format!("account-sync-task-{}-{}", account_id, &email); + let periodic_task = PeriodicTask::new(&task_name); + let task = move |param: Option| { + let account_id = param.unwrap(); + Box::pin(async move { + let account = AccountModel::get(account_id).await.ok(); + match account { + Some(account) => { + if !account.enabled { + let last = LAST_WARN_TIME.load(Ordering::Relaxed); + let now = utc_now!(); + if now - last >= WARN_INTERVAL_MS { + LAST_WARN_TIME.store(now, Ordering::Relaxed); + warn!( + "Account {}: Sync aborted. Account is currently disabled.", + account_id + ); + } + } else { + if let Some(imap) = &account.imap { + if let AuthType::OAuth2 = imap.auth.auth_type { + if OAuth2AccessToken::get(account.id).await?.is_none() { + if utc_now!() % 300_000 == 0 { + warn!("Account {}: Sync aborted. OAuth2 authorization not completed. Please visit the rustmailer admin page to authorize this account.", account_id); + } + return Ok(()); + } + } + } + if let Err(e) = execute_imap_sync(&account).await { + STATUS_DISPATCHER + .append_error( + account_id, + format!("error in account sync task: {:#?}", e), + ) + .await; + error!( + "Failed to synchronize mailbox data for '{}': {:?}", + account_id, e + ) + } + } + } + None => { + error!( + "Account {}: Sync aborted. Account entity not found.", + account_id + ); + } + } + Ok(()) + }) + }; + let handler = periodic_task.start(task, Some(account_id), TASK_INTERVAL, true, true); + self.tasks.insert(account_id, handler); + } + + pub async fn stop(&self, account_id: u64) -> BichonResult<()> { + if let Some((_, handler)) = self.tasks.remove(&account_id) { + handler.cancel().await; + } else { + warn!("No sync task found for account: {}", account_id); + } + Ok(()) + } +} diff --git a/src/modules/cache/mod.rs b/src/modules/cache/mod.rs new file mode 100644 index 0000000..95814a1 --- /dev/null +++ b/src/modules/cache/mod.rs @@ -0,0 +1,33 @@ +// +// Copyright (c) 2025 rustmailer.com (https://rustmailer.com) +// +// This file is part of the Bichon Email Archiving Project +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +use crate::modules::settings::cli::SETTINGS; +use std::sync::{Arc, LazyLock}; +use tokio::sync::Semaphore; + +pub mod imap; + +pub static SEMAPHORE: LazyLock> = LazyLock::new(|| { + Arc::new(Semaphore::new( + SETTINGS + .bichon_sync_concurrency + .map(|c| c as usize) + .unwrap_or(num_cpus::get() * 2), + )) +}); diff --git a/src/modules/common/auth.rs b/src/modules/common/auth.rs new file mode 100644 index 0000000..84eb091 --- /dev/null +++ b/src/modules/common/auth.rs @@ -0,0 +1,232 @@ +// +// Copyright (c) 2025 rustmailer.com (https://rustmailer.com) +// +// This file is part of the Bichon Email Archiving Project +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +use crate::{ + modules::{ + error::{code::ErrorCode, BichonResult}, + settings::{cli::SETTINGS, system::SystemSetting}, + token::{root::ROOT_TOKEN, AccessToken, AccountInfo}, + utils::rate_limit::RATE_LIMITER_MANAGER, + }, + raise_error, +}; +use governor::clock::{Clock, QuantaClock}; +use poem::{ + web::{ + headers::{authorization::Bearer, Authorization, HeaderMapExt}, + RealIp, + }, + Endpoint, FromRequest, Middleware, Request, RequestBody, Result, +}; +use serde::Deserialize; +use std::{collections::BTreeSet, net::IpAddr, sync::Arc}; + +use super::create_api_error_response; + +pub struct ApiGuard; + +pub struct ApiGuardEndpoint { + ep: E, +} + +impl Middleware for ApiGuard { + type Output = ApiGuardEndpoint; + + fn transform(&self, ep: E) -> Self::Output { + ApiGuardEndpoint { ep } + } +} + +#[derive(Deserialize)] +struct Param { + access_token: String, +} + +impl Endpoint for ApiGuardEndpoint { + type Output = E::Output; + + async fn call(&self, mut req: Request) -> Result { + let context = authorize_access(&req).await?; + req.set_data(Arc::new(context)); + self.ep.call(req).await + } +} + +#[derive(Clone, Debug, Default)] +pub struct ClientContext { + pub ip_addr: Option, + pub access_token: Option, + pub is_root: bool, +} + +impl ClientContext { + pub fn require_root(&self) -> BichonResult<()> { + if !SETTINGS.bichon_enable_access_token || self.is_root { + Ok(()) + } else { + Err(raise_error!( + "Root access required".into(), + ErrorCode::PermissionDenied + )) + } + } + + pub fn require_authorized(&self) -> BichonResult<()> { + if !SETTINGS.bichon_enable_access_token || self.is_root || self.access_token.is_some() { + Ok(()) + } else { + Err(raise_error!( + "Authorization required".into(), + ErrorCode::PermissionDenied + )) + } + } + + pub fn require_account_access(&self, account_id: u64) -> BichonResult<()> { + if !SETTINGS.bichon_enable_access_token || self.is_root { + return Ok(()); + } + + match &self.access_token { + Some(token) if token.can_access_account(account_id) => Ok(()), + _ => Err(raise_error!(format!( + "You do not have permission to access the requested email account (ID: {}). Please check your access rights or contact the administrator.", + account_id + ), ErrorCode::PermissionDenied)), + } + } + + pub fn accessible_accounts(&self) -> BichonResult>> { + if !SETTINGS.bichon_enable_access_token || self.is_root { + Ok(None) // All accounts are accessible + } else { + match &self.access_token { + Some(token) => Ok(Some(&token.accounts)), + None => Err(raise_error!( + "Missing access token".into(), + ErrorCode::PermissionDenied + )), + } + } + } +} + +impl<'a> FromRequest<'a> for ClientContext { + async fn from_request(req: &'a Request, _body: &mut RequestBody) -> Result { + extract_client_context(req).await + } +} + +pub async fn extract_client_context(req: &Request) -> Result { + if SETTINGS.bichon_enable_access_token { + let ip_addr = RealIp::from_request_without_body(req) + .await + .map_err(|_| { + create_api_error_response( + "Failed to parse client IP address", + ErrorCode::InvalidParameter, + ) + })? + .0 + .ok_or_else(|| { + create_api_error_response( + "Failed to parse client IP address", + ErrorCode::InvalidParameter, + ) + })?; + // Extract access token from Bearer header or query params + let bearer = req + .headers() + .typed_get::>() + .map(|auth| auth.0.token().to_string()) + .or_else(|| req.params::().ok().map(|param| param.access_token)); + + let token = bearer.ok_or_else(|| { + create_api_error_response("Valid access token not found", ErrorCode::PermissionDenied) + })?; + + // Check for root token + if let Ok(Some(root)) = SystemSetting::get(ROOT_TOKEN) { + if root.value == token { + return Ok(ClientContext { + ip_addr: Some(ip_addr), + access_token: None, + is_root: true, + }); + } + } + + // Validate and update access token + let validated_token = AccessToken::try_update_access_timestamp(&token) + .await + .map_err(|_| { + create_api_error_response("Invalid access token", ErrorCode::PermissionDenied) + })?; + + return Ok(ClientContext { + ip_addr: Some(ip_addr), + access_token: Some(validated_token), + is_root: false, + }); + } + + Ok(Default::default()) +} + +pub async fn authorize_access(req: &Request) -> Result { + let context = extract_client_context(&req).await?; + context.require_authorized().map_err(|error| { + create_api_error_response(&error.to_string(), ErrorCode::PermissionDenied) + })?; + + if let Some(access_token) = &context.access_token { + if let Some(access_control) = &access_token.acl { + if let Some(ip_addr) = context.ip_addr { + if let Some(whitelist) = &access_control.ip_whitelist { + if !whitelist.contains(&ip_addr.to_string()) { + return Err(create_api_error_response( + &format!("IP {} not in whitelist", ip_addr), + ErrorCode::PermissionDenied, + )); + } + } + } + + if let Some(rate_limit) = &access_control.rate_limit { + if let Err(not_until) = RATE_LIMITER_MANAGER + .check(&access_token.token, rate_limit.clone()) + .await + { + let wait_duration = not_until.wait_time_from(QuantaClock::default().now()); + return Err(create_api_error_response( + &format!( + "Rate limit: {}/{}s. Retry after {}s", + rate_limit.quota, + rate_limit.interval, + wait_duration.as_secs() + ), + ErrorCode::TooManyRequest, + )); + } + } + } + } + + Ok(context) +} diff --git a/src/modules/common/error.rs b/src/modules/common/error.rs new file mode 100644 index 0000000..a2e6cf8 --- /dev/null +++ b/src/modules/common/error.rs @@ -0,0 +1,47 @@ +// +// Copyright (c) 2025 rustmailer.com (https://rustmailer.com) +// +// This file is part of the Bichon Email Archiving Project +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +use poem::{Endpoint, IntoResponse, Middleware, Request, Response, Result}; + +use crate::modules::error::handler::error_handler; + +pub struct ErrorCapture; + +pub struct ErrorCaptureEndpoint { + ep: E, +} + +impl Middleware for ErrorCapture { + type Output = ErrorCaptureEndpoint; + + fn transform(&self, ep: E) -> Self::Output { + ErrorCaptureEndpoint { ep } + } +} + +impl Endpoint for ErrorCaptureEndpoint { + type Output = Response; + + async fn call(&self, req: Request) -> Result { + match self.ep.call(req).await { + Ok(response) => Ok(response.into_response()), + Err(error) => Ok(error_handler(error).await.into_response()), + } + } +} diff --git a/src/modules/common/log.rs b/src/modules/common/log.rs new file mode 100644 index 0000000..4ee5ea5 --- /dev/null +++ b/src/modules/common/log.rs @@ -0,0 +1,172 @@ +// +// Copyright (c) 2025 rustmailer.com (https://rustmailer.com) +// +// This file is part of the Bichon Email Archiving Project +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +use std::{ + num::NonZeroU32, + sync::{Arc, LazyLock}, + time::Instant, +}; + +use governor::{ + clock::{QuantaClock, QuantaInstant}, + middleware::NoOpMiddleware, + state::InMemoryState, + Quota, RateLimiter, +}; +use poem::{ + http::header, web::RealIp, Endpoint, FromRequest, IntoResponse, Middleware, Request, Response, + Result, +}; +use tracing::{error, info, warn, Instrument}; + + +pub type GovRateLimiter = RateLimiter< + governor::state::NotKeyed, + InMemoryState, + QuantaClock, + NoOpMiddleware, +>; + +static RATE_LIMITER: LazyLock = LazyLock::new(LogRateLimiter::new); +pub struct LogRateLimiter { + limiter: Arc, +} + +impl LogRateLimiter { + pub fn new() -> Self { + let quota = Quota::per_second(NonZeroU32::new(10).unwrap()); + let limiter = RateLimiter::direct(quota); + Self { + limiter: Arc::new(limiter), + } + } + + pub async fn should_log(&self, status: u16) -> bool { + let cost = match status { + 500_u16.. => NonZeroU32::new(1).unwrap(), // ERROR + 400_u16..=499_u16 => NonZeroU32::new(3).unwrap(), // WARN + _ => NonZeroU32::new(5).unwrap(), // INFO + }; + + self.limiter.check_n(cost).is_ok() + } +} + +#[derive(Default)] +pub struct Tracing; + +impl Middleware for Tracing { + type Output = TracingEndpoint; + + fn transform(&self, ep: E) -> Self::Output { + TracingEndpoint { inner: ep } + } +} + +/// Endpoint for the `Tracing` middleware. +pub struct TracingEndpoint { + inner: E, +} + +impl Endpoint for TracingEndpoint { + type Output = Response; + + async fn call(&self, req: Request) -> Result { + let remote_addr = RealIp::from_request_without_body(&req) + .await + .ok() + .and_then(|real_ip| real_ip.0) + .map(|addr| addr.to_string()) + .unwrap_or_else(|| req.remote_addr().to_string()); + let method = req.method().clone(); + let path = req.uri().path().to_string(); + let query = req.uri().query().map(|q| q.to_string()); + let referer = req + .headers() + .get(header::REFERER) + .and_then(|v| v.to_str().ok().map(|v| v.to_string())); + let content_length = req + .headers() + .get(header::CONTENT_LENGTH) + .and_then(|v| v.to_str().ok().map(|v| v.to_string())); + + let span = tracing::info_span!( + "request", + remote_addr = %remote_addr, + method = %method, + path = %path, + query = ?query, + referer = ?referer, + //user_agent = ?user_agent, + // forwarded = ?forwarded, + content_length = ?content_length, + ); + + async move { + let now = Instant::now(); + let res = self.inner.call(req).await; + let duration = now.elapsed(); + + match res { + Ok(resp) => { + let resp = resp.into_response(); + let status = resp.status().as_u16(); + log_response(status, duration).await; + Ok(resp) + } + Err(err) => { + let status = err.status().as_u16(); + log_response(status, duration).await; + Err(err) + } + } + } + .instrument(span) + .await + } +} + +#[inline] +async fn log_response(status: u16, duration: std::time::Duration) { + if RATE_LIMITER.should_log(status).await { + match status { + 500.. => { + error!( + status = %status, + duration = ?duration, + "request completed with server error" + ); + } + 400..=499 => { + warn!( + status = %status, + duration = ?duration, + "request completed with client error" + ); + } + _ => { + info!( + status = %status, + duration = ?duration, + "request completed successfully" + ); + } + } + } +} diff --git a/src/modules/common/mod.rs b/src/modules/common/mod.rs new file mode 100644 index 0000000..b518cea --- /dev/null +++ b/src/modules/common/mod.rs @@ -0,0 +1,233 @@ +// +// Copyright (c) 2025 rustmailer.com (https://rustmailer.com) +// +// This file is part of the Bichon Email Archiving Project +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +use super::error::code::ErrorCode; +use super::error::BichonError; +use mail_parser::{Addr as ImapAddr, Address as ImapAddress}; +use mail_send::mail_builder::headers::address::Address as SmtpAddress; +use mail_send::mail_builder::headers::address::EmailAddress as SmtpEmailAddress; +use poem::error::ResponseError; +use poem::Body; +use poem::{http::StatusCode, Error, Response}; +use poem_openapi::Object; +use regex::Regex; +use serde::{Deserialize, Serialize}; +use std::borrow::Cow; +use std::ops::Deref; +use tracing::error; + +pub mod auth; +pub mod error; +pub mod log; +pub mod paginated; +pub mod periodic; +pub mod rustls; +pub mod signal; +pub mod timeout; +pub mod tls; +pub mod validator; + +#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize, Object)] +pub struct Addr { + /// The optional display name associated with the email address (e.g., "John Doe"). + /// If `None`, no display name is specified. + pub name: Option, + /// The optional email address (e.g., "john.doe@example.com"). + /// If `None`, the address is unavailable, though typically at least one of `name` or `address` is provided. + pub address: Option, +} + +impl Addr { + pub fn parse(s: &str) -> Self { + let re = Regex::new(r#"(?:(?P.*)\s*)?<(?P[^<>]+)>"#).unwrap(); + if let Some(caps) = re.captures(s) { + let name: Option = caps.name("name").map(|m| m.as_str().trim().into()); + let email: Option = caps.name("email").map(|m| m.as_str().trim().into()); + Addr { + name: if let Some(n) = name { + if n.is_empty() { + None + } else { + Some(n) + } + } else { + None + }, + address: email, + } + } else { + let s_trimmed = s.trim(); + Addr { + name: None, + address: if s_trimmed.is_empty() { + None + } else { + Some(s_trimmed.into()) + }, + } + } + } +} + +impl std::fmt::Display for Addr { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match (&self.name, &self.address) { + (Some(name), Some(address)) => write!(f, "{} <{}>", name, address), + (None, Some(address)) => write!(f, "<{}>", address), + (Some(name), None) => write!(f, "{}", name), + (None, None) => write!(f, ""), + } + } +} + +impl<'x> From<&ImapAddr<'x>> for Addr { + fn from(original: &ImapAddr<'x>) -> Self { + Addr { + name: original.name.as_ref().map(|s| s.to_string()), + address: original.address.as_ref().map(|s| s.to_string()), + } + } +} + +#[derive(Debug, Clone, PartialEq)] +pub struct AddrVec(pub Vec); + +impl Deref for AddrVec { + type Target = Vec; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl<'x> From<&ImapAddress<'x>> for AddrVec { + fn from(original: &ImapAddress<'x>) -> Self { + let vec = match original { + ImapAddress::List(addrs) => addrs.iter().map(Addr::from).collect(), + ImapAddress::Group(groups) => groups + .iter() + .flat_map(|group| group.addresses.iter().map(Addr::from)) + .collect(), + }; + AddrVec(vec) + } +} + +impl<'x> From> for Addr { + fn from(email: SmtpEmailAddress<'x>) -> Self { + Addr { + name: email.name.map(|n| n.into_owned()), + address: Some(email.email.into_owned()), + } + } +} + +impl<'x> From<&SmtpAddress<'x>> for AddrVec { + fn from(address: &SmtpAddress<'x>) -> Self { + fn collect_addresses<'x>(address: &SmtpAddress<'x>, result: &mut Vec) { + match address { + SmtpAddress::Address(email) => { + let addr = Addr::from(email.clone()); + result.push(addr); + } + SmtpAddress::Group(group) => { + for addr in &group.addresses { + collect_addresses(addr, result); + } + } + SmtpAddress::List(list) => { + for addr in list { + collect_addresses(addr, result); + } + } + } + } + let mut addresses = Vec::new(); + collect_addresses(address, &mut addresses); + AddrVec(addresses) + } +} + +impl<'x> From for SmtpAddress<'x> { + fn from(addr: Addr) -> Self { + SmtpAddress::Address(SmtpEmailAddress { + name: addr.name.map(Cow::Owned), + email: Cow::Owned(addr.address.unwrap_or_default()), + }) + } +} + +// #[derive(Serialize)] +// pub struct ErrorResponse { +// pub message: String, +// } + +#[inline] +fn create_rust_mailer_error(message: &str, code: ErrorCode) -> BichonError { + BichonError::Generic { + message: message.into(), + location: snafu::Location::default(), + code, + } +} + +#[inline] +pub fn create_api_error_response(message: &str, code: ErrorCode) -> Error { + let rust_mailer_error = create_rust_mailer_error(message, code); + rust_mailer_error.into() +} + +impl ResponseError for BichonError { + fn status(&self) -> StatusCode { + match self { + BichonError::Generic { + message: _, + location: _, + code, + } => code.status(), + } + } + + fn as_response(&self) -> Response + where + Self: std::error::Error + Send + Sync + 'static, + { + match self { + BichonError::Generic { + message, + location, + code, + } => { + error!( + error_code = *code as u32, + error_message = %message, + error_location = ?location + ); + + let body = Body::from_json(serde_json::json!({ + "code": *code as u32, + "message": message.to_string(), + })) + .unwrap(); + + Response::builder().status(self.status()).body(body) + } + } + } +} diff --git a/src/modules/common/paginated.rs b/src/modules/common/paginated.rs new file mode 100644 index 0000000..c332586 --- /dev/null +++ b/src/modules/common/paginated.rs @@ -0,0 +1,72 @@ +// +// Copyright (c) 2025 rustmailer.com (https://rustmailer.com) +// +// This file is part of the Bichon Email Archiving Project +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +use std::cmp::min; + +use crate::{ + modules::{ + database::Paginated, + error::{code::ErrorCode, BichonResult}, + }, + raise_error, +}; + +pub fn paginate_vec( + items: &Vec, + page: Option, + page_size: Option, +) -> BichonResult> { + let total_items = items.len() as u64; + + let (offset, total_pages) = match (page, page_size) { + (Some(p), Some(s)) if p > 0 && s > 0 => { + let offset = (p - 1) * s; + let total_pages = if total_items > 0 { + (total_items + s - 1) / s + } else { + 0 + }; + (Some(offset), Some(total_pages)) + } + (Some(0), _) | (_, Some(0)) => { + return Err(raise_error!( + "'page' and 'page_size' must be greater than 0.".into(), + ErrorCode::InvalidParameter + )); + } + _ => (None, None), + }; + + let data = match offset { + Some(offset) if offset >= total_items => vec![], + Some(offset) => { + let end = min(offset + page_size.unwrap_or(total_items), total_items) as usize; + items[offset as usize..end].to_vec() + } + None => items.clone(), + }; + + Ok(Paginated::new( + page, + page_size, + total_items, + total_pages, + data, + )) +} diff --git a/src/modules/common/periodic.rs b/src/modules/common/periodic.rs new file mode 100644 index 0000000..7b89bf6 --- /dev/null +++ b/src/modules/common/periodic.rs @@ -0,0 +1,120 @@ +// +// Copyright (c) 2025 rustmailer.com (https://rustmailer.com) +// +// This file is part of the Bichon Email Archiving Project +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +use crate::modules::{common::signal::SIGNAL_MANAGER, error::BichonResult}; +use std::{future::Future, time::Duration}; +use tokio::{sync::oneshot, time::MissedTickBehavior}; +use tracing::{info, warn}; + +pub struct PeriodicTask { + name: String, +} + +pub struct TaskHandle { + cancel_sender: Option>, + join_handle: tokio::task::JoinHandle<()>, +} + +impl TaskHandle { + pub async fn cancel(self) { + if let Some(sender) = self.cancel_sender { + let _ = sender.send(()); + } + let _ = self.join_handle.await; + } +} + +impl PeriodicTask { + pub fn new(name: &str) -> Self { + Self { + name: name.to_owned(), + } + } + + /// If `enable_cancel` is true, allows cancellation through TaskHandle::cancel + pub fn start( + self, + task: T, + param: Option, + interval: Duration, + enable_cancel: bool, + run_immediately: bool, + ) -> TaskHandle + where + T: Fn(Option) -> F + Send + Sync + 'static, + F: Future> + Send + 'static, + { + info!("Task '{}' started", &self.name); + + let (cancel_sender_opt, cancel_receiver_opt) = if enable_cancel { + let (tx, rx) = oneshot::channel::<()>(); + (Some(tx), Some(rx)) + } else { + (None, None) + }; + + let name_clone = self.name.clone(); + + let join_handle = tokio::spawn(async move { + let mut interval = tokio::time::interval(interval); + interval.set_missed_tick_behavior(MissedTickBehavior::Skip); + let mut shutdown = SIGNAL_MANAGER.subscribe(); + + if !run_immediately { + interval.tick().await; // discard first immediate tick + } + let mut cancel_receiver = cancel_receiver_opt; + + loop { + tokio::select! { + _ = interval.tick() => { + match task(param).await { + Ok(()) => {}, + Err(e) => { + warn!("Task '{}' failed: {:?}", name_clone, e); + }, + } + } + // only enabled if cancel_receiver is Some + _ = async { + if let Some(ref mut rx) = cancel_receiver { + rx.await.ok() + } else { + futures::future::pending().await + } + } => { + info!("Task '{}' received cancellation signal", name_clone); + break; + } + _ = shutdown.recv() => { + info!("Task '{}' shutting down due to shutdown signal", name_clone); + break; + } + } + } + + info!("Task '{}' stopped", name_clone); + }); + + TaskHandle { + cancel_sender: cancel_sender_opt, + join_handle, + } + } +} diff --git a/src/modules/common/rustls.rs b/src/modules/common/rustls.rs new file mode 100644 index 0000000..44f1572 --- /dev/null +++ b/src/modules/common/rustls.rs @@ -0,0 +1,41 @@ +// +// Copyright (c) 2025 rustmailer.com (https://rustmailer.com) +// +// This file is part of the Bichon Email Archiving Project +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +use crate::{ + modules::{ + context::Initialize, + error::{code::ErrorCode, BichonResult}, + }, + raise_error, +}; + +pub struct RustMailerTls; + +impl Initialize for RustMailerTls { + async fn initialize() -> BichonResult<()> { + rustls::crypto::CryptoProvider::install_default(rustls::crypto::ring::default_provider()) + .map_err(|_| { + raise_error!( + "failed to set crypto provider".into(), + ErrorCode::InternalError + ) + }) + } +} + \ No newline at end of file diff --git a/src/modules/common/signal.rs b/src/modules/common/signal.rs new file mode 100644 index 0000000..b68cb43 --- /dev/null +++ b/src/modules/common/signal.rs @@ -0,0 +1,55 @@ +// +// Copyright (c) 2025 rustmailer.com (https://rustmailer.com) +// +// This file is part of the Bichon Email Archiving Project +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +use std::sync::LazyLock; + +use crate::modules::{ + context::Initialize, error::BichonResult, utils::shutdown::shutdown_signal, +}; +use tokio::sync::broadcast; + +pub static SIGNAL_MANAGER: LazyLock = LazyLock::new(SignalManager::new); + +pub struct SignalManager { + sender: broadcast::Sender<()>, +} + +impl SignalManager { + pub fn new() -> Self { + let (sender, _) = broadcast::channel(1); + SignalManager { sender } + } + + pub fn subscribe(&self) -> broadcast::Receiver<()> { + self.sender.subscribe() + } +} + +impl Initialize for SignalManager { + async fn initialize() -> BichonResult<()> { + tokio::spawn({ + async move { + shutdown_signal().await; + println!("\nSending shutdown signal..."); + let _ = SIGNAL_MANAGER.sender.send(()); + } + }); + Ok(()) + } +} diff --git a/src/modules/common/timeout.rs b/src/modules/common/timeout.rs new file mode 100644 index 0000000..98f1270 --- /dev/null +++ b/src/modules/common/timeout.rs @@ -0,0 +1,74 @@ +// +// Copyright (c) 2025 rustmailer.com (https://rustmailer.com) +// +// This file is part of the Bichon Email Archiving Project +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +use poem::{Endpoint, Middleware, Request, Result}; +use std::time::Duration; +use tracing::error; + +use crate::modules::error::code::ErrorCode; + +use super::create_api_error_response; + +pub const TIMEOUT_HEADER: &str = "X-RustMailer-Timeout-Seconds"; + +pub struct Timeout; + +impl Middleware for Timeout { + type Output = TimeoutEndpoint; + + fn transform(&self, ep: E) -> Self::Output { + TimeoutEndpoint { ep } + } +} + +pub struct TimeoutEndpoint { + ep: E, +} + +#[inline] +fn extract_timeout(req: &Request) -> Option { + if let Some(v) = req.header(TIMEOUT_HEADER) { + v.parse::().ok() + } else { + None + } +} + +impl Endpoint for TimeoutEndpoint { + type Output = E::Output; + + async fn call(&self, req: Request) -> Result { + let timeout = extract_timeout(&req); + let seconds = timeout.unwrap_or(30).min(600); + match tokio::time::timeout(Duration::from_secs(seconds), self.ep.call(req)).await { + Ok(Ok(response)) => Ok(response), // If the request completes successfully + Ok(Err(e)) => Err(e), // If the request returns an error + Err(_) => { + error!("Request timed out after {} seconds", seconds); + Err(create_api_error_response( + &format!( + "Request timed out after {} seconds (timeout set via X-RustMailer-Timeout-Seconds header, max allowed: 600 seconds)", + seconds + ), + ErrorCode::RequestTimeout, + )) + } + } + } +} diff --git a/src/modules/common/tls.rs b/src/modules/common/tls.rs new file mode 100644 index 0000000..ba2ccdf --- /dev/null +++ b/src/modules/common/tls.rs @@ -0,0 +1,54 @@ +// +// Copyright (c) 2025 rustmailer.com (https://rustmailer.com) +// +// This file is part of the Bichon Email Archiving Project +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +use poem::listener::{RustlsCertificate, RustlsConfig}; + +use crate::{ + modules::{ + error::{code::ErrorCode, BichonResult}, + settings::dir::DATA_DIR_MANAGER, + }, + raise_error, +}; + +pub fn rustls_config() -> BichonResult { + let cert = std::fs::read_to_string(&DATA_DIR_MANAGER.tls_cert).map_err(|e| { + raise_error!( + format!( + "Failed to read TLS certificate: '{}' (error: {})", + DATA_DIR_MANAGER.tls_cert.display(), + e + ), + ErrorCode::InternalError + ) + })?; + + let key = std::fs::read_to_string(&DATA_DIR_MANAGER.tls_key).map_err(|e| { + raise_error!( + format!( + "Failed to read TLS private key: '{}' (error: {})", + DATA_DIR_MANAGER.tls_key.display(), + e + ), + ErrorCode::InternalError + ) + })?; + let rustls_certificate = RustlsCertificate::new().cert(cert).key(key); + Ok(RustlsConfig::new().fallback(rustls_certificate)) +} diff --git a/src/modules/common/validator.rs b/src/modules/common/validator.rs new file mode 100644 index 0000000..b77a2ae --- /dev/null +++ b/src/modules/common/validator.rs @@ -0,0 +1,43 @@ +// +// Copyright (c) 2025 rustmailer.com (https://rustmailer.com) +// +// This file is part of the Bichon Email Archiving Project +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +use std::{ + fmt::{self, Display, Formatter}, + str::FromStr, +}; + +use email_address::EmailAddress; +use poem_openapi::Validator; + +pub struct EmailValidator; + +impl Display for EmailValidator { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + f.write_str("Not a valid email address") + } +} + +impl Validator for EmailValidator { + fn check(&self, value: &String) -> bool { + match EmailAddress::from_str(value) { + Ok(e) => &e.email() == value, + Err(_) => false, + } + } +} diff --git a/src/modules/context/controller.rs b/src/modules/context/controller.rs new file mode 100644 index 0000000..1194414 --- /dev/null +++ b/src/modules/context/controller.rs @@ -0,0 +1,72 @@ +// +// Copyright (c) 2025 rustmailer.com (https://rustmailer.com) +// +// This file is part of the Bichon Email Archiving Project +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +use crate::modules::{cache::imap::task::SYNC_TASKS, error::BichonResult}; +use std::{sync::LazyLock, time::Duration}; +use tokio::sync::mpsc; +use tracing::{error, info}; + +pub static SYNC_CONTROLLER: LazyLock = LazyLock::new(SyncController::new); + +pub struct SyncController { + channel: mpsc::Sender<(u64, String)>, // Channel to trigger account sync by account ID +} + +impl SyncController { + pub fn new() -> Self { + let (tx, mut rx) = mpsc::channel::<(u64, String)>(100); + + tokio::spawn(async move { + while let Some((account_id, email)) = rx.recv().await { + match Self::start_syncer(account_id, email.clone()).await { + Ok(Some(_)) => {} + Ok(None) => {} + Err(err) => { + error!( + "Failed to prepare and start syncer of account {{{}-{}}}, error: {:#?}", + &account_id, &email, err + ); + } + } + } + }); + + SyncController { channel: tx } + } + + /// Trigger synchronization for a specific account + pub async fn trigger_start(&self, account_id: u64, email: String) { + if let Err(e) = self.channel.send((account_id, email)).await { + error!( + "Failed to trigger synchronization for account={{{}}}, error: {:?}", + account_id, e + ); + } + } + + async fn start_syncer(account_id: u64, email: String) -> BichonResult> { + info!( + "Account syncer starting for account: {}-{}.", + account_id, email + ); + SYNC_TASKS.start_account_sync_task(account_id, email).await; + tokio::time::sleep(Duration::from_millis(100)).await; + Ok(Some(())) + } +} diff --git a/src/modules/context/executors.rs b/src/modules/context/executors.rs new file mode 100644 index 0000000..a615c5b --- /dev/null +++ b/src/modules/context/executors.rs @@ -0,0 +1,110 @@ +// +// Copyright (c) 2025 rustmailer.com (https://rustmailer.com) +// +// This file is part of the Bichon Email Archiving Project +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +use crate::modules::context::Initialize; +use crate::modules::error::code::ErrorCode; +use crate::raise_error; +use crate::{ + modules::{ + account::migration::AccountModel, + context::controller::SYNC_CONTROLLER, + error::BichonResult, + imap::{executor::ImapExecutor, pool::build_imap_pool}, + }, + utc_now, +}; +use dashmap::DashMap; +use std::sync::{Arc, LazyLock}; +use tracing::info; + +pub static MAIL_CONTEXT: LazyLock = + LazyLock::new(EmailClientExecutors::new); + +pub struct EmailClientExecutors { + start_at: i64, + imap: DashMap>, +} + +impl Initialize for EmailClientExecutors { + async fn initialize() -> BichonResult<()> { + MAIL_CONTEXT.start_account_syncers().await + } +} + +impl EmailClientExecutors { + pub fn new() -> Self { + Self { + start_at: utc_now!(), + imap: DashMap::new(), + } + } + pub fn uptime_ms(&self) -> i64 { + utc_now!() - self.start_at + } + + pub async fn imap(&self, account_id: u64) -> BichonResult> { + if let Some(executor) = self.imap.get(&account_id) { + return Ok(executor.value().clone()); + } + + let pool = build_imap_pool(account_id).await?; + let new_executor = Arc::new(ImapExecutor::new(pool)); + + match self.imap.try_entry(account_id) { + Some(dashmap::mapref::entry::Entry::Occupied(entry)) => Ok(entry.get().clone()), + Some(dashmap::mapref::entry::Entry::Vacant(entry)) => { + entry.insert(new_executor.clone()); + Ok(new_executor) + } + None => Err(raise_error!( + "DashMap locked".into(), + ErrorCode::InternalError + )), + } + } + + pub async fn clean_account(&self, account_id: u64) -> BichonResult<()> { + if self.imap.remove(&account_id).is_some() { + info!(account_id, "Closed IMAP pool for account"); + } + Ok(()) + } + + pub async fn start_account_syncers(&self) -> BichonResult<()> { + let accounts = AccountModel::list_all().await?; + let active_accounts: Vec = + accounts.into_iter().filter(|a| a.enabled).collect(); + + if active_accounts.is_empty() { + info!("No active accounts found for account initialization."); + return Ok(()); + } + info!( + "System has {} active accounts to initialize.", + active_accounts.len() + ); + for account in active_accounts { + SYNC_CONTROLLER + .trigger_start(account.id, account.email) + .await + } + + Ok(()) + } +} diff --git a/src/modules/context/mod.rs b/src/modules/context/mod.rs new file mode 100644 index 0000000..132d23b --- /dev/null +++ b/src/modules/context/mod.rs @@ -0,0 +1,32 @@ +// +// Copyright (c) 2025 rustmailer.com (https://rustmailer.com) +// +// This file is part of the Bichon Email Archiving Project +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +use crate::modules::error::BichonResult; + +pub mod controller; +pub mod executors; +pub mod status; + +pub trait Initialize { + async fn initialize() -> BichonResult<()>; +} + +pub trait RustMailTask { + fn start(); +} diff --git a/src/modules/context/status.rs b/src/modules/context/status.rs new file mode 100644 index 0000000..94b07e8 --- /dev/null +++ b/src/modules/context/status.rs @@ -0,0 +1,50 @@ +// +// Copyright (c) 2025 rustmailer.com (https://rustmailer.com) +// +// This file is part of the Bichon Email Archiving Project +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +use crate::modules::context::executors::MAIL_CONTEXT; +use chrono::Local; +use poem_openapi::Object; +use serde::Deserialize; +use serde::Serialize; +use std::time::Duration; +use timeago::Formatter; + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Object)] +pub struct BichonStatus { + /// The service uptime in milliseconds since it started. + pub uptime_ms: i64, + /// A human-readable string indicating the time elapsed since the service started (e.g., "2 hours ago"). + pub timeago: String, + /// The timezone in which the service is operating (e.g., "UTC" or "Asia/Tokyo"). + pub timezone: String, + /// The version of the RustMailer service currently running. + pub version: String, +} + +impl BichonStatus { + pub fn get() -> Self { + Self { + uptime_ms: MAIL_CONTEXT.uptime_ms(), + timeago: Formatter::new() + .convert(Duration::from_millis(MAIL_CONTEXT.uptime_ms() as u64)), + timezone: Local::now().offset().to_string(), + version: env!("CARGO_PKG_VERSION").into(), + } + } +} diff --git a/src/modules/dashboard/mod.rs b/src/modules/dashboard/mod.rs new file mode 100644 index 0000000..f570a13 --- /dev/null +++ b/src/modules/dashboard/mod.rs @@ -0,0 +1,109 @@ +// +// Copyright (c) 2025 rustmailer.com (https://rustmailer.com) +// +// This file is part of the Bichon Email Archiving Project +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +use poem_openapi::Object; +use serde::{Deserialize, Serialize}; +use tantivy::{schema::Value, TantivyDocument}; + +use crate::{ + modules::{ + account::migration::AccountModel, + error::{code::ErrorCode, BichonResult}, + indexer::{manager::ENVELOPE_INDEX_MANAGER, schema::SchemaTools}, + settings::dir::DATA_DIR_MANAGER, + utils::get_total_size, + }, + raise_error, +}; + +#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize, Object)] +pub struct DashboardStats { + pub account_count: usize, // Number of accounts + pub email_count: u64, // Total number of emails + pub total_size_bytes: u64, // Total size of all emails (in bytes) + pub storage_usage_bytes: u64, // Actual storage used (in bytes) + pub index_usage_bytes: u64, // Index storage size (in bytes) + pub recent_activity: Vec, // Email activity over recent days + pub top_senders: Vec, // Top 10 senders + pub top_accounts: Vec, // Top 10 accounts + pub with_attachment_count: u64, // Emails with attachments + pub without_attachment_count: u64, // Emails without attachments + pub top_largest_emails: Vec, // Top 10 largest emails +} + +impl DashboardStats { + pub async fn get() -> BichonResult { + let mut stat = ENVELOPE_INDEX_MANAGER.get_dashboard_stats().await?; + stat.top_largest_emails = ENVELOPE_INDEX_MANAGER.top_10_largest_emails().await?; + stat.email_count = ENVELOPE_INDEX_MANAGER.total_emails()?; + stat.account_count = AccountModel::count().await?; + stat.storage_usage_bytes = get_total_size(&DATA_DIR_MANAGER.eml_dir) + .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))?; + stat.index_usage_bytes = get_total_size(&DATA_DIR_MANAGER.envelope_dir) + .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))?; + Ok(stat) + } +} + +#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize, Object)] +pub struct TimeBucket { + pub timestamp_ms: i64, // Timestamp in milliseconds + pub count: u64, // Number of emails in this time bucket +} + +#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize, Object)] +pub struct Group { + pub key: String, + pub count: u64, // Number of emails from this sender +} + +#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize, Object)] +pub struct LargestEmail { + pub subject: String, // Email subject + pub size_bytes: u64, // Email size in bytes +} + +impl LargestEmail { + pub fn from_tantivy_doc(document: &TantivyDocument) -> BichonResult { + let fields = SchemaTools::envelope_fields(); + let value = document.get_first(fields.f_size).ok_or_else(|| { + raise_error!( + "miss 'size' field in tantivy document".into(), + ErrorCode::InternalError + ) + })?; + let size_bytes = value.as_u64().ok_or_else(|| { + raise_error!("'size' field is not a u64".into(), ErrorCode::InternalError) + })?; + let value = document.get_first(fields.f_subject).ok_or_else(|| { + raise_error!("'subject' field not found".into(), ErrorCode::InternalError) + })?; + let subject = value.as_str().map(|s| s.to_string()).ok_or_else(|| { + raise_error!( + "'subject' field is not a string".into(), + ErrorCode::InternalError + ) + })?; + let envelope = LargestEmail { + subject, + size_bytes, + }; + Ok(envelope) + } +} diff --git a/src/modules/database/manager.rs b/src/modules/database/manager.rs new file mode 100644 index 0000000..4cbf33c --- /dev/null +++ b/src/modules/database/manager.rs @@ -0,0 +1,118 @@ +// +// Copyright (c) 2025 rustmailer.com (https://rustmailer.com) +// +// This file is part of the Bichon Email Archiving Project +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +use crate::modules::cache::imap::MAILBOX_MODELS; +use crate::modules::error::{code::ErrorCode, BichonError}; +use crate::modules::settings::cli::SETTINGS; +use crate::modules::settings::dir::DATA_DIR_MANAGER; +use crate::modules::{database::META_MODELS, error::BichonResult}; +use crate::raise_error; +use native_db::{Builder, Database}; +use std::sync::{Arc, LazyLock}; +use tracing::info; + +pub static DB_MANAGER: LazyLock = LazyLock::new(DatabaseManager::new); + +/// Metadata database instance +pub struct DatabaseManager { + meta_db: Arc>, + /// Envelope database instance + envelope_db: Arc>, +} + +impl DatabaseManager { + fn new() -> Self { + let meta_db = Self::init_meta_database().expect("Failed to initialize metadata database"); + let envelope_db = + Self::init_evenlope_database().expect("Failed to initialize evenlope database"); + DatabaseManager { + meta_db, + envelope_db, + } + } + + /// Get a reference to the metadata database + pub fn meta_db(&self) -> &Arc> { + &self.meta_db + } + + pub fn envelope_db(&self) -> &Arc> { + &self.envelope_db + } + + /// Initialize metadata database with a fixed or configured file path + fn init_meta_database() -> BichonResult>> { + let mut database = Builder::new() + .set_cache_size( + SETTINGS + .bichon_metadata_cache_size + .unwrap_or(134217728) + .max(67108864), + ) //default 128MB + .create(&META_MODELS, DATA_DIR_MANAGER.meta_db.clone()) + .map_err(Self::handle_database_error)?; + + let rw = database + .rw_transaction() + .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))?; + rw.commit() + .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))?; + + database + .compact() + .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))?; + Ok(Arc::new(database)) + } + + fn init_evenlope_database() -> BichonResult>> { + info!( + "Initializing envelope database at: {:?}", + &DATA_DIR_MANAGER.mailbox_db + ); + + let mut database = Builder::new() + .set_cache_size( + SETTINGS + .bichon_envelope_cache_size + .unwrap_or(1073741824) + .max(67108864), + ) //default 1GB + .create(&MAILBOX_MODELS, DATA_DIR_MANAGER.mailbox_db.clone()) + .map_err(Self::handle_database_error)?; + + let rw = database + .rw_transaction() + .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))?; + rw.commit() + .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))?; + + database + .compact() + .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))?; + + Ok(Arc::new(database)) + } + + fn handle_database_error(error: native_db::db_type::Error) -> BichonError { + raise_error!( + format!("Failed to create database: {:?}", error), + ErrorCode::InternalError + ) + } +} diff --git a/src/modules/database/mod.rs b/src/modules/database/mod.rs new file mode 100644 index 0000000..66f62c1 --- /dev/null +++ b/src/modules/database/mod.rs @@ -0,0 +1,492 @@ +// +// Copyright (c) 2025 rustmailer.com (https://rustmailer.com) +// +// This file is part of the Bichon Email Archiving Project +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +use crate::modules::account::migration::AccountV1; +use crate::modules::autoconfig::CachedMailSettings; +use crate::modules::error::code::ErrorCode; +use crate::modules::error::BichonResult; +use crate::modules::oauth2::entity::OAuth2; +use crate::modules::oauth2::pending::OAuth2PendingEntity; +use crate::modules::oauth2::token::OAuth2AccessToken; +use crate::modules::settings::proxy::Proxy; +use crate::modules::settings::system::SystemSetting; +use crate::modules::token::AccessToken; +use crate::raise_error; +use db_type::{KeyOptions, ToKeyDefinition}; +use itertools::Itertools; +use native_db::*; +use serde::Serialize; +use std::sync::{Arc, LazyLock}; +use transaction::RwTransaction; + +pub mod manager; + +pub static META_MODELS: LazyLock = LazyLock::new(|| { + let mut adapter = ModelsAdapter::new(); + adapter.register_metadata_models(); + adapter.models +}); + +pub struct ModelsAdapter { + pub models: Models, +} + +impl ModelsAdapter { + pub fn new() -> Self { + ModelsAdapter { + models: Models::new(), + } + } + + pub fn register_model(&mut self) { + self.models.define::().expect("failed to define model "); + } + + pub fn register_metadata_models(&mut self) { + self.register_model::(); + self.register_model::(); + self.register_model::(); + self.register_model::(); + self.register_model::(); + self.register_model::(); + self.register_model::(); + self.register_model::(); + } +} + +pub async fn insert_impl( + database: &Arc>, + item: T, +) -> BichonResult<()> { + let db = database.clone(); + tokio::task::spawn_blocking(move || { + let rw_transaction = db + .rw_transaction() + .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))?; + rw_transaction + .insert(item) + .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))?; + rw_transaction + .commit() + .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))?; + Ok(()) + }) + .await + .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))? +} + +pub async fn batch_insert_impl( + database: &Arc>, + batch: Vec, +) -> BichonResult<()> { + let db = database.clone(); + tokio::task::spawn_blocking(move || { + let rw_transaction = db + .rw_transaction() + .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))?; + for item in batch { + rw_transaction + .insert(item) + .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))?; + } + rw_transaction + .commit() + .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))?; + Ok(()) + }) + .await + .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))? +} + +pub async fn batch_upsert_impl( + database: &Arc>, + batch: Vec, +) -> BichonResult<()> { + let db = database.clone(); + tokio::task::spawn_blocking(move || { + let rw_transaction = db + .rw_transaction() + .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))?; + for item in batch { + rw_transaction + .upsert(item) + .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))?; + } + rw_transaction + .commit() + .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))?; + Ok(()) + }) + .await + .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))? +} + +pub async fn upsert_impl( + database: &Arc>, + item: T, +) -> BichonResult<()> { + let db = database.clone(); + tokio::task::spawn_blocking(move || { + let rw_transaction = db + .rw_transaction() + .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))?; + rw_transaction + .upsert(item) + .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))?; + rw_transaction + .commit() + .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))?; + Ok(()) + }) + .await + .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))? +} + +pub async fn update_impl( + database: &Arc>, + current: impl FnOnce(&RwTransaction) -> BichonResult + Send + 'static, + updated: impl FnOnce(&T) -> BichonResult + Send + 'static, +) -> BichonResult { + let db = database.clone(); + tokio::task::spawn_blocking(move || { + let rw = db + .rw_transaction() + .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))?; + let current_item = current(&rw)?; + let updated_item = updated(¤t_item)?; + rw.update(current_item.clone(), updated_item) + .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))?; + rw.commit() + .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))?; + Ok(current_item) + }) + .await + .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))? +} + +// pub async fn batch_update_impl( +// database: &Arc>, +// filter: impl FnOnce(&RwTransaction) -> RustMailerResult> + Send + 'static, +// updated: impl FnOnce(&Vec) -> RustMailerResult> + Send + 'static, +// ) -> RustMailerResult> { +// let db = database.clone(); +// tokio::task::spawn_blocking(move || { +// let rw = db +// .rw_transaction() +// .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))?; +// let targets = filter(&rw)?; +// let tuples = updated(&targets)?; +// for (old, updated) in tuples { +// rw.update(old, updated) +// .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))?; +// } +// rw.commit() +// .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))?; +// Ok(targets) +// }) +// .await +// .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))? +// } + +pub async fn async_find_impl( + database: &Arc>, + key: impl ToKey + Send + 'static, +) -> BichonResult> { + let db = database.clone(); + tokio::task::spawn_blocking(move || { + let r_transaction = db + .r_transaction() + .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))?; + let entity: Option = r_transaction + .get() + .primary(key) + .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))?; + Ok(entity) + }) + .await + .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))? +} + +pub fn find_impl( + database: &Arc>, + key: &str, +) -> BichonResult> { + let db = database.clone(); + let r_transaction = db + .r_transaction() + .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))?; + let entity: Option = r_transaction + .get() + .primary(key) + .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))?; + Ok(entity) +} + +pub async fn delete_impl( + database: &Arc>, + delete: impl FnOnce(&RwTransaction) -> BichonResult + Send + 'static, +) -> BichonResult<()> { + let db = database.clone(); + tokio::task::spawn_blocking(move || { + let rw_transaction = db + .rw_transaction() + .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))?; + let to_delete = delete(&rw_transaction)?; + rw_transaction + .remove::(to_delete) + .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))?; + rw_transaction + .commit() + .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))?; + Ok(()) + }) + .await + .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))? +} + +pub async fn batch_delete_impl( + database: &Arc>, + delete: impl FnOnce(&RwTransaction) -> BichonResult> + Send + 'static, +) -> BichonResult { + let db = database.clone(); + tokio::task::spawn_blocking(move || { + let rw_transaction = db + .rw_transaction() + .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))?; + let to_delete = delete(&rw_transaction)?; + let delete_count = to_delete.len(); + for item in to_delete { + rw_transaction + .remove(item) + .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))?; + } + rw_transaction + .commit() + .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))?; + Ok(delete_count) + }) + .await + .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))? +} + +pub async fn list_all_impl( + database: &Arc>, +) -> BichonResult> { + let db = database.clone(); + tokio::task::spawn_blocking(move || { + let r_transaction = db + .r_transaction() + .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))?; + let entities: Vec = r_transaction + .scan() + .primary() + .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))? + .all() + .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))? + .try_collect() + .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))?; + Ok(entities) + }) + .await + .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))? +} + +// For tables with a creation timestamp, place the creation time at the front of the primary key. +// This allows sorting by time, as the data is stored in dictionary order based on the primary key. +// If reverse sorting by time is needed, the iterator can be reversed. +pub async fn paginate_query_primary_scan_all_impl< + T: ToInput + Serialize + std::fmt::Debug + std::marker::Unpin + Send + Sync + 'static, +>( + database: &Arc>, + page: Option, + page_size: Option, + desc: Option, +) -> BichonResult> { + let db = database.clone(); + + tokio::task::spawn_blocking(move || { + let r_transaction = db + .r_transaction() + .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))?; + let total_items = r_transaction + .len() + .primary::() + .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))?; + + // Validate page and page_size + let (offset, total_pages) = if let (Some(p), Some(s)) = (page, page_size) { + if p == 0 || s == 0 { + return Err(raise_error!( + "'page' and 'page_size' must be greater than 0.".into(), + ErrorCode::InvalidParameter + )); + } + let offset = (p - 1) * s; + let total_pages = if total_items > 0 { + (total_items as f64 / s as f64).ceil() as u64 + } else { + 0 + }; + (Some(offset), Some(total_pages)) + } else { + (None, None) + }; + + // Handle empty result early + if let Some(offset) = offset { + if offset >= total_items { + return Ok(Paginated::new( + page, + page_size, + total_items, + total_pages, + vec![], + )); + } + } + + let scan = r_transaction + .scan() + .primary() + .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))?; + let iter = scan + .all() + .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))?; + + // Collect items based on the reverse flag and pagination + let items: Vec = match desc { + Some(true) => iter + .rev() + .skip(offset.unwrap_or(0) as usize) + .take(page_size.unwrap_or(total_items) as usize) + .try_collect() + .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))?, + _ => iter + .skip(offset.unwrap_or(0) as usize) + .take(page_size.unwrap_or(total_items) as usize) + .try_collect() + .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))?, + }; + + Ok(Paginated::new( + page, + page_size, + total_items, + total_pages, + items, + )) + }) + .await + .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))? +} + +pub async fn filter_by_secondary_key_impl( + database: &Arc>, + key_def: impl ToKeyDefinition + Send + 'static, + start_with: impl ToKey + Send + 'static, +) -> BichonResult> { + let db = database.clone(); + tokio::task::spawn_blocking(move || { + let r_transaction = db + .r_transaction() + .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))?; + let entities: Vec = r_transaction + .scan() + .secondary(key_def) + .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))? + .start_with(start_with) + .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))? + .try_collect() + .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))?; + Ok(entities) + }) + .await + .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))? +} + +pub async fn count_by_unique_secondary_key_impl( + database: &Arc>, + key_def: impl ToKeyDefinition + Send + 'static, +) -> BichonResult { + let db = database.clone(); + tokio::task::spawn_blocking(move || { + let r_transaction = db + .r_transaction() + .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))?; + let count = r_transaction + .scan() + .secondary::(key_def) + .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))? + .all() + .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))? + .count(); + Ok(count) + }) + .await + .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))? +} + +pub async fn secondary_find_impl( + database: &Arc>, + key_def: impl ToKeyDefinition + Send + 'static, + key: impl ToKey + Send + 'static, +) -> BichonResult> { + let db = database.clone(); + tokio::task::spawn_blocking(move || { + let r_transaction = db + .r_transaction() + .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))?; + + let entities: Option = r_transaction + .get() + .secondary(key_def, key) + .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))?; + + Ok(entities) + }) + .await + .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))? +} + +#[derive(Debug)] +pub struct Paginated { + pub page: Option, + pub page_size: Option, + pub total_items: u64, + pub total_pages: Option, + pub items: Vec, +} + +impl Paginated { + pub fn new( + page: Option, + page_size: Option, + total_items: u64, + total_pages: Option, + items: Vec, + ) -> Self { + Paginated { + page, + page_size, + total_items, + total_pages, + items, + } + } +} diff --git a/src/modules/envelope/extractor.rs b/src/modules/envelope/extractor.rs new file mode 100644 index 0000000..ed6134a --- /dev/null +++ b/src/modules/envelope/extractor.rs @@ -0,0 +1,146 @@ +// +// Copyright (c) 2025 rustmailer.com (https://rustmailer.com) +// +// This file is part of the Bichon Email Archiving Project +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +use crate::modules::common::AddrVec; +use crate::modules::error::code::ErrorCode; +use crate::modules::error::BichonResult; +use crate::modules::utils::create_hash; +use crate::{calculate_hash, raise_error, utc_now}; +use crate::{id, modules::indexer::envelope::Envelope}; +use async_imap::types::Fetch; +use html2text::from_read; +use mail_parser::{Message, MessageParser, MimeHeaders}; + +pub fn extract_envelope(fetch: &Fetch, account_id: u64, mailbox_id: u64) -> BichonResult { + let internal_date = fetch + .internal_date() + .map(|d| d.timestamp_millis()) + .unwrap_or(0); + let uid = fetch.uid.unwrap_or(0); + let size = fetch.size.unwrap_or(0); + + let body = fetch + .body() + .ok_or_else(|| raise_error!("No body available".into(), ErrorCode::InternalError))?; + + let message = MessageParser::new().parse(body).ok_or_else(|| { + raise_error!( + "Email header parse result is not available".into(), + ErrorCode::InternalError + ) + })?; + + let text = if let Some(text) = message.body_text(0).map(|cow| cow.into_owned()) { + text + } else if let Some(html) = message.body_html(0).map(|cow| cow.into_owned()) { + from_read(html.as_bytes(), 0) + .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))? + } else { + String::new() + }; + + let message_id = message + .message_id() + .map(String::from) + .unwrap_or(generate_message_id()); + let in_reply_to = message.in_reply_to().as_text().map(String::from); + let references = extract_references(&message); + let thread_id = compute_thread_id(in_reply_to, references, &message_id); + let subject = message.subject().map(String::from).unwrap_or("".into()); + let date = message.date().map(|d| d.to_timestamp() * 1000).unwrap_or(0); + let bcc: Option> = message.bcc().map(|addr| { + AddrVec::from(addr) + .0 + .into_iter() + .filter_map(|a| a.address) + .collect() + }); + let cc: Option> = message.cc().map(|addr| { + AddrVec::from(addr) + .0 + .into_iter() + .filter_map(|a| a.address) + .collect() + }); + let to: Option> = message.to().map(|addr| { + AddrVec::from(addr) + .0 + .into_iter() + .filter_map(|a| a.address) + .collect() + }); + let from = message + .from() + .and_then(|addr| AddrVec::from(addr).0.into_iter().next()) + .and_then(|add| add.address) + .unwrap_or_else(|| "unknown".to_string()); + + let attachments: Vec = message + .attachments() + .filter_map(|att| att.attachment_name()) + .map(|name| name.to_string()) + .collect(); + let envelope = Envelope { + id: create_hash(account_id, &message_id), + message_id, + account_id, + mailbox_id, + uid, + subject, + text, + from, + to: to.unwrap_or_default(), + cc: cc.unwrap_or_default(), + bcc: bcc.unwrap_or_default(), + date, + internal_date, + size, + thread_id, + attachments, + tags: None, + }; + Ok(envelope) +} + +pub fn compute_thread_id( + in_reply_to: Option, + references: Option>, + message_id: &str, +) -> u64 { + if in_reply_to.is_some() && references.as_ref().map_or(false, |r| !r.is_empty()) { + return calculate_hash!(&references.as_ref().unwrap()[0]); + } + calculate_hash!(message_id) +} + +pub fn generate_message_id() -> String { + let ts = utc_now!(); + let pid = std::process::id(); + format!("<{:016x}.{}.{}@{}>", id!(128), ts, pid, "bichon") +} + +fn extract_references(message: &Message<'_>) -> Option> { + match message.references() { + mail_parser::HeaderValue::Text(cow) => Some(vec![cow.to_string()]), + mail_parser::HeaderValue::TextList(vec) => { + Some(vec.iter().map(|cow| cow.to_string()).collect()) + } + _ => None, + } +} diff --git a/src/modules/envelope/mod.rs b/src/modules/envelope/mod.rs new file mode 100644 index 0000000..872957e --- /dev/null +++ b/src/modules/envelope/mod.rs @@ -0,0 +1,20 @@ +// +// Copyright (c) 2025 rustmailer.com (https://rustmailer.com) +// +// This file is part of the Bichon Email Archiving Project +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +pub mod extractor; diff --git a/src/modules/error/code.rs b/src/modules/error/code.rs new file mode 100644 index 0000000..38057af --- /dev/null +++ b/src/modules/error/code.rs @@ -0,0 +1,86 @@ +// +// Copyright (c) 2025 rustmailer.com (https://rustmailer.com) +// +// This file is part of the Bichon Email Archiving Project +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +use poem::http::StatusCode; +use poem_openapi::Enum; + +#[derive(Copy, Clone, Debug, Enum, Eq, PartialEq)] +#[repr(u32)] +pub enum ErrorCode { + // Client-side errors (10000–10999) + InvalidParameter = 10000, + MissingConfiguration = 10020, + Incompatible = 10030, + PayloadTooLarge = 10070, + RequestTimeout = 10080, + MethodNotAllowed = 10090, + + // Authentication and authorization errors (20000–20999) + PermissionDenied = 20000, + AccountDisabled = 20010, + OAuth2ItemDisabled = 20050, + MissingRefreshToken = 20060, + + // Resource errors (30000–30999) + ResourceNotFound = 30000, + TooManyRequest = 30020, + + // Network connection errors (40000–40999) + NetworkError = 40000, + ConnectionTimeout = 40010, + ConnectionPoolTimeout = 40020, + HttpResponseError = 40030, + + // Mail service errors (50000–50999) + ImapCommandFailed = 50000, + ImapAuthenticationFailed = 50010, + ImapUnexpectedResult = 50020, + AutoconfigFetchFailed = 50060, + // Internal system errors (70000–70999) + InternalError = 70000, + UnhandledPoemError = 70010, +} + +impl ErrorCode { + pub fn status(&self) -> StatusCode { + match self { + ErrorCode::InvalidParameter + | ErrorCode::MissingConfiguration + | ErrorCode::Incompatible => StatusCode::BAD_REQUEST, + ErrorCode::PermissionDenied => StatusCode::UNAUTHORIZED, + ErrorCode::AccountDisabled | ErrorCode::OAuth2ItemDisabled => StatusCode::FORBIDDEN, + ErrorCode::ResourceNotFound => StatusCode::NOT_FOUND, + ErrorCode::RequestTimeout => StatusCode::REQUEST_TIMEOUT, + ErrorCode::PayloadTooLarge => StatusCode::PAYLOAD_TOO_LARGE, + ErrorCode::TooManyRequest => StatusCode::TOO_MANY_REQUESTS, + ErrorCode::InternalError + | ErrorCode::AutoconfigFetchFailed + | ErrorCode::ImapCommandFailed + | ErrorCode::ImapUnexpectedResult + | ErrorCode::HttpResponseError + | ErrorCode::ImapAuthenticationFailed + | ErrorCode::MissingRefreshToken + | ErrorCode::NetworkError + | ErrorCode::ConnectionTimeout + | ErrorCode::ConnectionPoolTimeout + | ErrorCode::UnhandledPoemError => StatusCode::INTERNAL_SERVER_ERROR, + ErrorCode::MethodNotAllowed => StatusCode::METHOD_NOT_ALLOWED, + } + } +} diff --git a/src/modules/error/handler.rs b/src/modules/error/handler.rs new file mode 100644 index 0000000..067de96 --- /dev/null +++ b/src/modules/error/handler.rs @@ -0,0 +1,76 @@ +// +// Copyright (c) 2025 rustmailer.com (https://rustmailer.com) +// +// This file is part of the Bichon Email Archiving Project +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +use crate::modules::error::{code::ErrorCode, ApiError, ApiErrorResponse, BichonError}; +use poem::IntoResponse; +use poem_openapi::payload::Json; + +pub async fn error_handler(error: poem::Error) -> impl poem::IntoResponse { + if error.is::() { + return error.into_response(); + } + + let error_mapping = [ + // Poem errors + ( + error.is::(), + ErrorCode::ResourceNotFound, + ), + ( + error.is::() + || error.is::() + || error.is::() + || error.is::() + || error.is::() + || error.is::() + || error.is::() + || error.is::(), + ErrorCode::InvalidParameter, + ), + ( + error.is::(), + ErrorCode::MethodNotAllowed, + ), + ( + error.is::(), + ErrorCode::PermissionDenied, + ), + ]; + + // Find the first matching error type + if let Some((_, error_code)) = error_mapping.iter().find(|(condition, _)| *condition) { + let api_error = ApiError::new_with_error_code(error.to_string(), *error_code as u32); + let mut response = + ApiErrorResponse::Generic(error_code.status(), Json(api_error)).into_response(); + response.set_status(error.status()); + return response; + } + // Handle other cases + if error.has_source() { + let api_error = + ApiError::new_with_error_code(error.to_string(), ErrorCode::UnhandledPoemError as u32); + let mut response = + ApiErrorResponse::Generic(ErrorCode::UnhandledPoemError.status(), Json(api_error)) + .into_response(); + response.set_status(error.status()); + response + } else { + error.into_response() + } +} diff --git a/src/modules/error/mod.rs b/src/modules/error/mod.rs new file mode 100644 index 0000000..0c0735a --- /dev/null +++ b/src/modules/error/mod.rs @@ -0,0 +1,111 @@ +// +// Copyright (c) 2025 rustmailer.com (https://rustmailer.com) +// +// This file is part of the Bichon Email Archiving Project +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +use std::{fmt::Formatter, u32}; + +use crate::raise_error; +use bb8::RunError; +use code::ErrorCode; +use poem::http::StatusCode; +use poem_openapi::{payload::Json, ApiResponse, Object}; +use snafu::{Location, Snafu}; + +pub mod code; +pub mod handler; + +#[derive(Debug, Snafu)] +#[snafu(visibility(pub))] +pub enum BichonError { + #[snafu(display("{message}"))] + Generic { + message: String, + #[snafu(implicit)] + location: Location, + code: ErrorCode, + }, +} + +pub type BichonResult = std::result::Result; + +impl From> for BichonError { + fn from(e: RunError) -> Self { + match e { + RunError::User(e) => e, + RunError::TimedOut => raise_error!( + "Timed out while attempting to acquire a connection from the pool".into(), + ErrorCode::ConnectionPoolTimeout + ), + } + } +} +#[derive(Debug, Clone, Object)] +pub struct ApiError { + pub message: String, + pub code: u32, +} + +impl From for ApiErrorResponse { + fn from(error: BichonError) -> Self { + match error { + BichonError::Generic { + message, + location, + code, + } => { + tracing::error!( + "API error occurred: [{:#?}] {} at {:?}", + code, + message, + location + ); + let api_error = ApiError { + message, + code: code as u32, + }; + ApiErrorResponse::Generic(code.status(), Json(api_error)) + } + } + } +} + +impl ApiError { + pub fn new(message: String, code: u32) -> Self { + Self { message, code } + } + + pub fn new_with_error_code( + error: ErrorType, + code: u32, + ) -> ApiError { + Self::new(format!("{:#}", error), code) + } +} + +impl std::fmt::Display for ApiError { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "Error({}): {}", self.code, self.message) + } +} + +impl std::error::Error for ApiError {} + +#[derive(Debug, Clone, ApiResponse)] +pub enum ApiErrorResponse { + Generic(StatusCode, Json), +} diff --git a/src/modules/imap/capabilities.rs b/src/modules/imap/capabilities.rs new file mode 100644 index 0000000..18932c4 --- /dev/null +++ b/src/modules/imap/capabilities.rs @@ -0,0 +1,51 @@ +// +// Copyright (c) 2025 rustmailer.com (https://rustmailer.com) +// +// This file is part of the Bichon Email Archiving Project +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +use crate::modules::error::code::ErrorCode; +use crate::modules::imap::session::SessionStream; +use crate::{modules::error::BichonResult, raise_error}; +use async_imap::types::Capability; +use async_imap::{types::Capabilities, Session}; + +pub async fn fetch_capabilities( + session: &mut Session>, +) -> BichonResult { + session + .capabilities() + .await + .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::ImapCommandFailed)) +} + +pub fn check_capabilities(capabilities: &Capabilities) -> BichonResult<()> { + if !capabilities.has_str("IMAP4rev1") { + return Err(raise_error!( + "Server does not support IMAP4rev1".into(), + ErrorCode::Incompatible + )); + } + Ok(()) +} + +pub fn capability_to_string(capability: &Capability) -> String { + match capability { + Capability::Imap4rev1 => "IMAP4rev1".into(), + Capability::Auth(v) => format!("AUTH={}", v), + Capability::Atom(v) => v.into(), + } +} diff --git a/src/modules/imap/client.rs b/src/modules/imap/client.rs new file mode 100644 index 0000000..f62e7c8 --- /dev/null +++ b/src/modules/imap/client.rs @@ -0,0 +1,251 @@ +// +// Copyright (c) 2025 rustmailer.com (https://rustmailer.com) +// +// This file is part of the Bichon Email Archiving Project +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +use crate::modules::account::entity::Encryption; +use crate::modules::error::code::ErrorCode; +use crate::modules::error::BichonResult; +use crate::modules::imap::session::SessionStream; +use crate::modules::imap::stats::StatsWrapper; +use crate::modules::utils::net::establish_tcp_connection_with_timeout; +use crate::modules::utils::net::establish_tls_connection; +use crate::modules::utils::tls::establish_tls_stream; +use crate::raise_error; +use async_imap::Client as ImapClient; +use async_imap::Session as ImapSession; +use std::net::SocketAddr; +use std::net::ToSocketAddrs; +use std::ops::Deref; +use std::ops::DerefMut; +use tokio::io::BufWriter; +use tracing::debug; + +#[derive(Debug)] +pub(crate) struct Client { + inner: ImapClient>, +} + +impl Deref for Client { + type Target = ImapClient>; + + fn deref(&self) -> &Self::Target { + &self.inner + } +} + +impl DerefMut for Client { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.inner + } +} + +fn alpn(port: u16) -> &'static [&'static str] { + if port == 993 { + &[] + } else { + &["imap"] + } +} + +impl Client { + fn new(stream: Box) -> Self { + Self { + inner: ImapClient::new(stream), + } + } + + pub(crate) async fn login( + self, + username: &str, + password: &str, + ) -> BichonResult>> { + let Client { inner, .. } = self; + let session = inner.login(username, password).await.map_err(|(e, _)| { + raise_error!(format!("{:#?}", e), ErrorCode::ImapAuthenticationFailed) + })?; + Ok(session) + } + + pub(crate) async fn authenticate( + self, + authenticator: impl async_imap::Authenticator, + ) -> BichonResult>> { + let Client { inner, .. } = self; + let session = inner + .authenticate("XOAUTH2", authenticator) + .await + .map_err(|(e, _)| { + raise_error!(format!("{:#?}", e), ErrorCode::ImapAuthenticationFailed) + })?; + Ok(session) + } + + pub async fn connection( + domain: &str, + encryption: &Encryption, + port: u16, + use_proxy: Option, + ) -> BichonResult { + let resolved_addr = Self::resolve_to_socket_addr(domain, port)?; + debug!("Attempting IMAP connection to {domain} ({resolved_addr})."); + match encryption { + Encryption::Ssl => { + Self::establish_secure_connection(resolved_addr, domain, use_proxy).await + } + Encryption::StartTls => { + Self::establish_starttls_connection(resolved_addr, domain, use_proxy).await + } + Encryption::None => Self::establish_insecure_connection(resolved_addr, use_proxy).await, + } + } + + async fn establish_secure_connection( + address: SocketAddr, + server_hostname: &str, + use_proxy: Option, + ) -> BichonResult { + // Establish the TLS connection with the specified parameters + let tls_stream = + establish_tls_connection(address, server_hostname, alpn(address.port()), use_proxy) + .await?; + let stats_stream = StatsWrapper::new(tls_stream); + // Wrap the TLS stream in a buffered writer for efficient IO + let buffered_stream = BufWriter::new(stats_stream); + // Create a SessionStream trait object for further communication + let session_stream = Box::new(buffered_stream); + // Initialize the client with the session stream + let mut client = Client::new(session_stream); + // Read and validate the greeting response + let _greeting = client + .read_response() + .await + .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::ImapCommandFailed))? + .ok_or_else(|| { + raise_error!( + "failed to read greeting".into(), + ErrorCode::ImapCommandFailed + ) + })?; + + // Return the established client + Ok(client) + } + + async fn establish_insecure_connection( + address: SocketAddr, + use_proxy: Option, + ) -> BichonResult { + // Establish the TCP connection without encryption + let tcp_stream = establish_tcp_connection_with_timeout(address, use_proxy).await?; + let stats_stream = StatsWrapper::new(tcp_stream); + // Wrap the TCP stream in a buffered writer for efficient IO + let buffered_stream = BufWriter::new(stats_stream); + // Create a SessionStream trait object for further communication + let session_stream: Box = Box::new(buffered_stream); + // Initialize the client with the session stream + let mut client = Client::new(session_stream); + + // Read and validate the greeting response + let _greeting = client + .read_response() + .await + .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::ImapCommandFailed))? + .ok_or_else(|| { + raise_error!( + "failed to read greeting".into(), + ErrorCode::ImapCommandFailed + ) + })?; + + // Return the established client + Ok(client) + } + + async fn establish_starttls_connection( + address: SocketAddr, + server_hostname: &str, + use_proxy: Option, + ) -> BichonResult { + // Establish the initial TCP connection + let tcp_stream = establish_tcp_connection_with_timeout(address, use_proxy).await?; + let stats_stream = StatsWrapper::new(tcp_stream); + // Wrap the TCP stream in a buffered writer for efficient IO + let buffered_tcp_stream = BufWriter::new(stats_stream); + + // Create a client for communication + let mut client = async_imap::Client::new(buffered_tcp_stream); + + // Read and validate the greeting response + let _greeting = client + .read_response() + .await + .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::ImapCommandFailed))? + .ok_or_else(|| { + raise_error!( + "failed to read greeting".into(), + ErrorCode::ImapCommandFailed + ) + })?; + + // Run the STARTTLS command to upgrade the connection to TLS + client + .run_command_and_check_ok("STARTTLS", None) + .await + .map_err(|_| { + raise_error!( + "STARTTLS command failed".into(), + ErrorCode::ImapCommandFailed + ) + })?; + + // Extract the TCP stream after running STARTTLS + let buffered_tcp_stream = client.into_inner(); + let tcp_stream = buffered_tcp_stream.into_inner(); + // Wrap the TCP stream in TLS encryption + let tls_stream = establish_tls_stream(server_hostname, &[], tcp_stream).await?; + // Wrap the TLS stream in a buffered writer + let buffered_stream = BufWriter::new(tls_stream); + // Create a SessionStream trait object for further communication + let session_stream: Box = Box::new(buffered_stream); + // Initialize the client with the session stream + let client = Client::new(session_stream); + // Return the established client + Ok(client) + } + + fn resolve_to_socket_addr(domain: &str, port: u16) -> BichonResult { + if domain.is_empty() || domain.contains(|c: char| !c.is_ascii() && c != '.') { + return Err(raise_error!( + "Invalid domain format".into(), + ErrorCode::InvalidParameter + )); + } + // Combine domain and port into a single address string + let address = format!("{}:{}", domain, port); + + // Resolve the address into a SocketAddr + let socket_addrs = address + .to_socket_addrs() + .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::NetworkError))?; + + // Return the first valid SocketAddr + socket_addrs.into_iter().next().ok_or_else(|| { + raise_error!("Unable to resolve address".into(), ErrorCode::NetworkError) + }) + } +} diff --git a/src/modules/imap/executor.rs b/src/modules/imap/executor.rs new file mode 100644 index 0000000..de74de7 --- /dev/null +++ b/src/modules/imap/executor.rs @@ -0,0 +1,241 @@ +// +// Copyright (c) 2025 rustmailer.com (https://rustmailer.com) +// +// This file is part of the Bichon Email Archiving Project +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +use crate::modules::account::state::AccountRunningState; +use crate::modules::cache::imap::mailbox::MailBox; +use crate::modules::cache::imap::sync::flow::{generate_uid_sequence_hashset, BATCH_SIZE}; +use crate::modules::envelope::extractor::extract_envelope; +use crate::modules::error::code::ErrorCode; +use crate::modules::indexer::manager::{EML_INDEX_MANAGER, ENVELOPE_INDEX_MANAGER}; +use crate::modules::indexer::schema::SchemaTools; +use crate::modules::{error::BichonResult, imap::manager::ImapConnectionManager}; +use crate::raise_error; +use async_imap::types::{Mailbox, Name}; +use bb8::Pool; +use futures::TryStreamExt; +use std::collections::HashSet; +use tantivy::doc; +use tracing::info; + +const BODY_FETCH_COMMAND: &str = "(UID INTERNALDATE RFC822.SIZE BODY.PEEK[])"; + +pub struct ImapExecutor { + pool: Pool, +} + +impl ImapExecutor { + pub fn new(pool: Pool) -> Self { + Self { pool } + } + + pub async fn list_all_mailboxes(&self) -> BichonResult> { + let mut session = self.pool.get().await?; + let list = session + .list(Some(""), Some("*")) + .await + .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::ImapCommandFailed))?; + let result = list + .try_collect::>() + .await + .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::ImapCommandFailed))?; + Ok(result) + } + + pub async fn examine_mailbox(&self, mailbox_name: &str) -> BichonResult { + let mut session = self.pool.get().await?; + session + .examine(mailbox_name) + .await + .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::ImapCommandFailed)) + } + + pub async fn uid_search(&self, mailbox_name: &str, query: &str) -> BichonResult> { + let mut session = self.pool.get().await?; + session + .examine(mailbox_name) + .await + .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::ImapCommandFailed))?; + let result = session + .uid_search(query) + .await + .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::ImapCommandFailed))?; + Ok(result) + } + + pub async fn fetch_new_mail( + &self, + account_id: u64, + mailbox: &MailBox, + start_uid: u64, + ) -> BichonResult<()> { + assert!(start_uid > 0, "start_uid must be greater than 0"); + let uid_list = self + .uid_search( + &mailbox.encoded_name(), + format!("UID {start_uid}:*").as_str(), + ) + .await?; + + let len = uid_list.len(); + if len == 0 { + return Ok(()); + } + info!( + "[account {}][mailbox {}] {} envelopes need to be fetched", + account_id, mailbox.name, len + ); + + let mut uid_vec: Vec = uid_list.into_iter().collect(); + uid_vec.sort(); + let uid_batches = generate_uid_sequence_hashset(uid_vec, BATCH_SIZE as usize, false); + + let too_many = len as u32 > 10 * BATCH_SIZE; + if too_many { + AccountRunningState::set_initial_current_syncing_folder( + account_id, + mailbox.name.clone(), + uid_batches.len() as u32, + ) + .await?; + } + + for (index, batch) in uid_batches.into_iter().enumerate() { + if too_many { + AccountRunningState::set_current_sync_batch_number( + account_id, + mailbox.name.clone(), + (index + 1) as u32, + ) + .await?; + } + self.uid_batch_retrieve_emails(account_id, mailbox.id, &batch, &mailbox.encoded_name()) + .await?; + } + Ok(()) + } + + pub async fn batch_retrieve_emails( + &self, + account_id: u64, + mailbox_id: u64, + page: u64, + page_size: u64, + encoded_mailbox_name: &str, + desc: bool, + ) -> BichonResult { + assert!(page > 0, "Page number must be greater than 0"); + assert!(page_size > 0, "Page size must be greater than 0"); + + let mut session = self.pool.get().await?; + let total = session + .examine(encoded_mailbox_name) + .await + .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::ImapCommandFailed))? + .exists as u64; + + if total == 0 { + return Ok(0); + } + + let (start, end) = if desc { + // Fetch messages starting from the newest (descending order) + let end = total.saturating_sub((page - 1) * page_size); + if end == 0 { + return Ok(0); + } + // Calculate start as end - page_size + 1 to avoid off-by-one errors + let start = end.saturating_sub(page_size - 1).max(1); + (start, end) + } else { + // Fetch messages starting from the oldest (ascending order) + let start = (page - 1) * page_size + 1; + if start > total { + return Ok(0); + } + // Calculate end, capped by the total number of messages + let end = (start + page_size - 1).min(total); + (start, end) + }; + + let sequence_set = format!("{}:{}", start, end); + info!( + "Fetching mailbox '{}' messages: sequence {} (page {}, page_size {}, desc={})", + encoded_mailbox_name, sequence_set, page, page_size, desc + ); + + let mut stream = session + .fetch(sequence_set.as_str(), BODY_FETCH_COMMAND) + .await + .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::ImapCommandFailed))?; + + let mut count = 0; + let fields = SchemaTools::eml_fields(); + while let Some(fetch) = stream + .try_next() + .await + .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::ImapCommandFailed))? + { + let envelope = extract_envelope(&fetch, account_id, mailbox_id)?; + ENVELOPE_INDEX_MANAGER + .add_document(envelope.id, envelope.to_document(mailbox_id)?) + .await; + let body = fetch.body().ok_or_else(|| { + raise_error!("missing a body".into(), ErrorCode::ImapUnexpectedResult) + })?; + EML_INDEX_MANAGER.add_document( envelope.id, doc!(fields.f_id => envelope.id, fields.f_account_id => account_id, fields.f_mailbox_id => mailbox_id, fields.f_eml => body)).await; + count += 1; + } + Ok(count) + } + + pub async fn uid_batch_retrieve_emails( + &self, + account_id: u64, + mailbox_id: u64, + uid_set: &str, + encoded_mailbox_name: &str, + ) -> BichonResult<()> { + let mut session = self.pool.get().await?; + session + .examine(encoded_mailbox_name) + .await + .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::ImapCommandFailed))?; + + let mut stream = session + .uid_fetch(uid_set, BODY_FETCH_COMMAND) + .await + .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::ImapCommandFailed))?; + let fields = SchemaTools::eml_fields(); + while let Some(fetch) = stream + .try_next() + .await + .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::ImapCommandFailed))? + { + let envelope = extract_envelope(&fetch, account_id, mailbox_id)?; + ENVELOPE_INDEX_MANAGER + .add_document(envelope.id, envelope.to_document(mailbox_id)?) + .await; + let body = fetch.body().ok_or_else(|| { + raise_error!("missing a body".into(), ErrorCode::ImapUnexpectedResult) + })?; + EML_INDEX_MANAGER.add_document( envelope.id, doc!(fields.f_id => envelope.id, fields.f_account_id => account_id, fields.f_mailbox_id => mailbox_id, fields.f_eml => body)).await; + } + Ok(()) + } +} diff --git a/src/modules/imap/manager.rs b/src/modules/imap/manager.rs new file mode 100644 index 0000000..d207b78 --- /dev/null +++ b/src/modules/imap/manager.rs @@ -0,0 +1,155 @@ +// +// Copyright (c) 2025 rustmailer.com (https://rustmailer.com) +// +// This file is part of the Bichon Email Archiving Project +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +use crate::modules::account::dispatcher::STATUS_DISPATCHER; +use crate::modules::account::entity::AuthType; +use crate::modules::account::migration::{AccountModel, AccountType}; +use crate::modules::error::code::ErrorCode; +use crate::modules::error::BichonResult; +use crate::modules::imap::capabilities::{ + capability_to_string, check_capabilities, fetch_capabilities, +}; +use crate::modules::imap::client::Client; +use crate::modules::imap::oauth2::OAuth2; +use crate::modules::imap::session::SessionStream; +use crate::modules::oauth2::token::OAuth2AccessToken; +use crate::{decrypt, raise_error}; +use async_imap::Session; +use tracing::error; + +#[derive(Debug)] +pub struct ImapConnectionManager { + pub account_id: u64, +} + +impl ImapConnectionManager { + pub fn new(account_id: u64) -> Self { + Self { account_id } + } + + pub async fn fetch_account(&self) -> BichonResult { + // Fetch the account entity in non-test environment + AccountModel::get(self.account_id).await + } + + async fn create_client(&self, account: &AccountModel) -> BichonResult { + assert_eq!(account.account_type, AccountType::IMAP); + let imap = account.imap.as_ref().unwrap(); + Client::connection(&imap.host, &imap.encryption, imap.port, imap.use_proxy).await + } + + async fn authenticate( + &self, + client: Client, + account: &AccountModel, + ) -> BichonResult>> { + assert_eq!(account.account_type, AccountType::IMAP); + let imap = account.imap.as_ref().unwrap(); + match &imap.auth.auth_type { + AuthType::Password => { + let password = &imap.auth.password.clone().ok_or_else(|| { + raise_error!( + "Imap auth type is Passwd, but password not set".into(), + ErrorCode::MissingConfiguration + ) + })?; + + let password = decrypt!(&password)?; + client.login(&account.email, &password).await + } + AuthType::OAuth2 => { + let record = OAuth2AccessToken::get(self.account_id).await?; + let access_token = record.and_then(|r| r.access_token).ok_or_else(|| { + raise_error!( + "Imap auth type is OAuth2, but OAuth2 authorization is not yet complete." + .into(), + ErrorCode::MissingConfiguration + ) + })?; + client + .authenticate(OAuth2::new(account.email.clone(), access_token)) + .await + } + } + } + + pub async fn build(&self) -> BichonResult>> { + let account = self.fetch_account().await?; + let client = match self.create_client(&account).await { + Ok(client) => client, + Err(error) => { + error!( + "Failed to create IMAP {}'s client: {:#?}", + &account.email, error + ); + STATUS_DISPATCHER + .append_error( + self.account_id, + format!("imap client connect error: {:#?}", error), + ) + .await; + return Err(error); + } + }; + + let mut session = match self.authenticate(client, &account).await { + Ok(session) => session, + Err(error) => { + error!("Failed to authenticate IMAP session: {:#?}", error); + + STATUS_DISPATCHER + .append_error( + self.account_id, + format!("imap client authenticate error: {:#?}", error), + ) + .await; + return Err(error); + } + }; + + match fetch_capabilities(&mut session).await { + Ok(capabilities) => { + let to_save: Vec = capabilities.iter().map(capability_to_string).collect(); + AccountModel::update_capabilities(self.account_id, to_save).await?; + if let Err(error) = check_capabilities(&capabilities) { + error!("Failed to check IMAP capabilities: {:#?}", error); + STATUS_DISPATCHER + .append_error( + self.account_id, + format!("imap client check capabilities error: {:#?}", error), + ) + .await; + return Err(error); + } + } + Err(error) => { + error!("Failed to fetch IMAP capabilities: {:#?}", error); + STATUS_DISPATCHER + .append_error( + self.account_id, + format!("imap client fetch capabilities error: {:#?}", error), + ) + .await; + return Err(error); + } + } + + Ok(session) + } +} diff --git a/src/modules/imap/mod.rs b/src/modules/imap/mod.rs new file mode 100644 index 0000000..898b201 --- /dev/null +++ b/src/modules/imap/mod.rs @@ -0,0 +1,29 @@ +// +// Copyright (c) 2025 rustmailer.com (https://rustmailer.com) +// +// This file is part of the Bichon Email Archiving Project +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +pub mod capabilities; +pub mod client; +pub mod executor; +pub mod manager; +pub mod oauth2; +pub mod pool; +pub mod session; +pub mod stats; +#[cfg(test)] +mod tests; diff --git a/src/modules/imap/oauth2.rs b/src/modules/imap/oauth2.rs new file mode 100644 index 0000000..039fd35 --- /dev/null +++ b/src/modules/imap/oauth2.rs @@ -0,0 +1,41 @@ +// +// Copyright (c) 2025 rustmailer.com (https://rustmailer.com) +// +// This file is part of the Bichon Email Archiving Project +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +#[derive(Debug)] +pub struct OAuth2 { + user: String, + access_token: String, +} + +impl OAuth2 { + pub fn new(user: String, access_token: String) -> Self { + Self { user, access_token } + } +} + +impl async_imap::Authenticator for OAuth2 { + type Response = String; + + fn process(&mut self, _data: &[u8]) -> Self::Response { + format!( + "user={}\x01auth=Bearer {}\x01\x01", + self.user, self.access_token + ) + } +} diff --git a/src/modules/imap/pool.rs b/src/modules/imap/pool.rs new file mode 100644 index 0000000..35f10c3 --- /dev/null +++ b/src/modules/imap/pool.rs @@ -0,0 +1,60 @@ +// +// Copyright (c) 2025 rustmailer.com (https://rustmailer.com) +// +// This file is part of the Bichon Email Archiving Project +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +use crate::modules::error::code::ErrorCode; +use crate::modules::error::{BichonError, BichonResult}; +use crate::modules::imap::{manager::ImapConnectionManager, session::SessionStream}; +use crate::raise_error; +use async_imap::Session; +use bb8::Pool; +use std::time::Duration; + +impl bb8::ManageConnection for ImapConnectionManager { + type Connection = Session>; + + type Error = BichonError; + + async fn connect(&self) -> BichonResult { + self.build().await + } + // call this function before using the connection + async fn is_valid(&self, conn: &mut Self::Connection) -> BichonResult<()> { + conn.noop() + .await + .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::ImapCommandFailed)) + } + + fn has_broken(&self, _: &mut Self::Connection) -> bool { + false + } +} + +pub async fn build_imap_pool(account_id: u64) -> BichonResult> { + let manager = ImapConnectionManager::new(account_id); + let pool = Pool::builder() + .connection_timeout(Duration::from_secs(30)) + .idle_timeout(Duration::from_secs(120)) + .retry_connection(true) + .max_size(10) + .test_on_check_out(true) + .build(manager) + .await?; + + Ok(pool) +} diff --git a/src/modules/imap/session.rs b/src/modules/imap/session.rs new file mode 100644 index 0000000..90a3285 --- /dev/null +++ b/src/modules/imap/session.rs @@ -0,0 +1,52 @@ +// +// Copyright (c) 2025 rustmailer.com (https://rustmailer.com) +// +// This file is part of the Bichon Email Archiving Project +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +use std::pin::Pin; +use tokio::io::{AsyncRead, AsyncWrite, BufWriter}; +use tokio_io_timeout::TimeoutStream; + +pub trait SessionStream: AsyncRead + AsyncWrite + Unpin + Send + Sync + std::fmt::Debug { + // Change the read timeout on the session stream. + // fn set_read_timeout(&mut self, timeout: Option); +} + +impl SessionStream for Box { + // fn set_read_timeout(&mut self, timeout: Option) { + // self.as_mut().set_read_timeout(timeout); + // } +} + +impl SessionStream for tokio_rustls::client::TlsStream { + // fn set_read_timeout(&mut self, timeout: Option) { + // self.get_mut().0.set_read_timeout(timeout); + // } +} + +impl SessionStream for BufWriter { + // fn set_read_timeout(&mut self, timeout: Option) { + // self.get_mut().set_read_timeout(timeout); + // } +} +impl SessionStream + for Pin>> +{ + // fn set_read_timeout(&mut self, timeout: Option) { + // self.as_mut().set_read_timeout_pinned(timeout); + // } +} diff --git a/src/modules/imap/stats.rs b/src/modules/imap/stats.rs new file mode 100644 index 0000000..bf47b24 --- /dev/null +++ b/src/modules/imap/stats.rs @@ -0,0 +1,86 @@ +// +// Copyright (c) 2025 rustmailer.com (https://rustmailer.com) +// +// This file is part of the Bichon Email Archiving Project +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +use std::pin::Pin; +use std::task::{Context, Poll}; +// use std::time::Duration; + +use tokio::io::{AsyncRead, AsyncWrite, ReadBuf}; + +use crate::modules::imap::session::SessionStream; + +pub struct StatsWrapper { + inner: T, +} + +impl StatsWrapper { + pub fn new(inner: T) -> Self { + Self { inner } + } +} + +impl AsyncRead for StatsWrapper { + fn poll_read( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &mut ReadBuf<'_>, + ) -> Poll> { + // let before = buf.filled().len(); + let result = Pin::new(&mut self.inner).poll_read(cx, buf); + // if let Poll::Ready(Ok(())) = &result { + // // let bytes_read = buf.filled().len() - before; + // } + result + } +} + +impl AsyncWrite for StatsWrapper { + fn poll_write( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &[u8], + ) -> Poll> { + let result = Pin::new(&mut self.inner).poll_write(cx, buf); + // if let Poll::Ready(Ok(bytes_written)) = &result { + // } + result + } + + fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + Pin::new(&mut self.inner).poll_flush(cx) + } + + fn poll_shutdown(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + Pin::new(&mut self.inner).poll_shutdown(cx) + } +} + +impl SessionStream for StatsWrapper { + // fn set_read_timeout(&mut self, timeout: Option) { + // self.inner.set_read_timeout(timeout); + // } +} + +impl std::fmt::Debug for StatsWrapper { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("StatsWrapper") + .field("inner", &self.inner) + .finish() + } +} diff --git a/src/modules/imap/tests.rs b/src/modules/imap/tests.rs new file mode 100644 index 0000000..ade5bc3 --- /dev/null +++ b/src/modules/imap/tests.rs @@ -0,0 +1,48 @@ +// +// Copyright (c) 2025 rustmailer.com (https://rustmailer.com) +// +// This file is part of the Bichon Email Archiving Project +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +use mail_parser::MessageParser; + +use crate::{base64_encode_url_safe, modules::{account::entity::Encryption, imap::client::Client}}; + +#[tokio::test] +async fn testxx() { + rustls::crypto::CryptoProvider::install_default(rustls::crypto::ring::default_provider()) + .unwrap(); + let client = Client::connection("imap.zoho.com".into(), &Encryption::Ssl, 993, None) + .await + .unwrap(); + let mut session = client.login("pollybase@zohomail.com", "xxx").await.unwrap(); + session.select("INBOX").await.unwrap(); + let result = session.uid_search("LARGER 1024").await.unwrap(); + println!("{:#?}", result); +} + +#[tokio::test] +async fn test1() { + let path = r"C:\Users\polly\Downloads\test.eml"; + let eml_data = std::fs::read(path).unwrap(); + let input = base64_encode_url_safe!(eml_data); + let message = MessageParser::default().parse(&input).unwrap(); + let parts = message.parts; + for part in parts { + println!("{}", part.is_message()); + println!("{}", part.is_multipart()); + } +} diff --git a/src/modules/indexer/envelope.rs b/src/modules/indexer/envelope.rs new file mode 100644 index 0000000..4007fbf --- /dev/null +++ b/src/modules/indexer/envelope.rs @@ -0,0 +1,194 @@ +// +// Copyright (c) 2025 rustmailer.com (https://rustmailer.com) +// +// This file is part of the Bichon Email Archiving Project +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +use crate::modules::error::code::ErrorCode; +use crate::modules::utils::create_hash; +use crate::modules::{error::BichonResult, indexer::schema::SchemaTools}; +use crate::raise_error; +use poem_openapi::Object; +use serde::{Deserialize, Serialize}; +use tantivy::schema::Facet; +use tantivy::{doc, schema::Value, TantivyDocument}; + +#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize, Object)] +pub struct Envelope { + pub id: u64, + pub message_id: String, + pub account_id: u64, + pub mailbox_id: u64, + pub uid: u32, + pub subject: String, + pub text: String, + pub from: String, + pub to: Vec, + pub cc: Vec, + pub bcc: Vec, + pub date: i64, + pub internal_date: i64, + pub size: u32, + pub thread_id: u64, + pub attachments: Vec, + pub tags: Option>, +} + +fn extract_u64_field( + document: &TantivyDocument, + field: tantivy::schema::Field, +) -> BichonResult { + let value = document.get_first(field).ok_or_else(|| { + raise_error!( + format!("miss '{}' field in tantivy document", stringify!(field)), + ErrorCode::InternalError + ) + })?; + value.as_u64().ok_or_else(|| { + raise_error!( + format!("'{}' field is not a u64", stringify!(field)), + ErrorCode::InternalError + ) + }) +} + +fn extract_i64_field( + document: &TantivyDocument, + field: tantivy::schema::Field, +) -> BichonResult { + let value = document.get_first(field).ok_or_else(|| { + raise_error!( + format!("miss '{}' field in tantivy document", stringify!(field)), + ErrorCode::InternalError + ) + })?; + value.as_i64().ok_or_else(|| { + raise_error!( + format!("'{}' field is not a i64", stringify!(field)), + ErrorCode::InternalError + ) + }) +} + +fn extract_string_field( + document: &TantivyDocument, + field: tantivy::schema::Field, +) -> BichonResult { + let value = document.get_first(field).ok_or_else(|| { + raise_error!( + format!("'{}' field not found", stringify!(field)), + ErrorCode::InternalError + ) + })?; + value.as_str().map(|s| s.to_string()).ok_or_else(|| { + raise_error!( + format!("'{}' field is not a string", stringify!(field)), + ErrorCode::InternalError + ) + }) +} + +fn extract_vec_string_field( + document: &TantivyDocument, + field: tantivy::schema::Field, +) -> BichonResult> { + let value = document + .get_all(field) + .filter_map(|v| v.as_str().map(|s| s.to_string())) + .collect(); + Ok(value) +} + +impl Envelope { + pub fn to_document(&self, mailbox_id: u64) -> BichonResult { + let fields = SchemaTools::envelope_fields(); + let mut doc = doc!(); + doc.add_u64(fields.f_id, self.id); + doc.add_text(fields.f_message_id, &self.message_id); + doc.add_u64(fields.f_account_id, self.account_id); + doc.add_u64(fields.f_mailbox_id, mailbox_id); + doc.add_u64(fields.f_uid, self.uid as u64); + doc.add_text(fields.f_subject, &self.subject); + doc.add_text(fields.f_text, &self.text); + doc.add_text(fields.f_from, &self.from); + for to in &self.to { + doc.add_text(fields.f_to, to); + } + for cc in &self.cc { + doc.add_text(fields.f_cc, cc); + } + for bcc in &self.bcc { + doc.add_text(fields.f_bcc, bcc); + } + doc.add_i64(fields.f_date, self.date); + doc.add_i64(fields.f_internal_date, self.internal_date); + doc.add_u64(fields.f_size, self.size as u64); + doc.add_u64(fields.f_thread_id, self.thread_id); + for att in &self.attachments { + doc.add_text(fields.f_attachments, att); + } + doc.add_bool(fields.f_has_attachment, self.attachments.len() > 0); + Ok(doc) + } + + pub async fn from_tantivy_doc(doc: &TantivyDocument) -> BichonResult { + let fields = SchemaTools::envelope_fields(); + let account_id = extract_u64_field(doc, fields.f_account_id)?; + let message_id = extract_string_field(doc, fields.f_message_id)?; + let mailbox_id = extract_u64_field(doc, fields.f_mailbox_id)?; + let id = create_hash(account_id, &message_id); + let full_text = extract_string_field(doc, fields.f_text)?; + + // Take up to the first 120 characters as a preview; + let preview = if full_text.chars().count() > 120 { + full_text.chars().take(120).collect::() + "..." + } else { + full_text + }; + + let tags: Vec = doc + .get_all(fields.f_tags) + .filter_map(|value| value.as_facet()) + .map(|facet_encoded_str| { + Facet::from_encoded(facet_encoded_str.as_bytes().to_vec()) + .ok() + .map(|facet| facet.to_string()) + }) + .flatten() + .collect(); + + let envelope = Envelope { + id, + account_id, + mailbox_id, + message_id: extract_string_field(doc, fields.f_message_id)?, + uid: extract_u64_field(doc, fields.f_uid)? as u32, + subject: extract_string_field(doc, fields.f_subject)?, + text: preview, + from: extract_string_field(doc, fields.f_from)?, + to: extract_vec_string_field(doc, fields.f_to)?, + cc: extract_vec_string_field(doc, fields.f_cc)?, + bcc: extract_vec_string_field(doc, fields.f_bcc)?, + date: extract_i64_field(doc, fields.f_date)?, + internal_date: extract_i64_field(doc, fields.f_internal_date)?, + size: extract_u64_field(doc, fields.f_size)? as u32, + thread_id: extract_u64_field(doc, fields.f_thread_id)?, + attachments: extract_vec_string_field(doc, fields.f_attachments)?, + tags: Some(tags), + }; + Ok(envelope) + } +} diff --git a/src/modules/indexer/fields.rs b/src/modules/indexer/fields.rs new file mode 100644 index 0000000..3382247 --- /dev/null +++ b/src/modules/indexer/fields.rs @@ -0,0 +1,69 @@ +// +// Copyright (c) 2025 rustmailer.com (https://rustmailer.com) +// +// This file is part of the Bichon Email Archiving Project +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +use tantivy::schema::Field; + +pub const F_MESSAGE_ID: &str = "message_id"; +pub const F_ACCOUNT_ID: &str = "account_id"; +pub const F_MAILBOX_ID: &str = "mailbox_id"; +pub const F_UID: &str = "uid"; +pub const F_SUBJECT: &str = "subject"; +pub const F_TEXT: &str = "text"; +pub const F_FROM: &str = "from"; +pub const F_TO: &str = "to"; +pub const F_CC: &str = "cc"; +pub const F_BCC: &str = "bcc"; +pub const F_DATE: &str = "date"; +pub const F_INTERNAL_DATE: &str = "internal_date"; +pub const F_SIZE: &str = "size"; +pub const F_THREAD_ID: &str = "thread_id"; +pub const F_ATTACHMENTS: &str = "attachments"; +pub const F_HAS_ATTACHMENT: &str = "has_attachment"; +pub const F_TAGS: &str = "tags"; + +pub const F_ID: &str = "id"; +pub struct EnvelopeFields { + pub f_id: Field, + pub f_message_id: Field, + pub f_account_id: Field, + pub f_mailbox_id: Field, + pub f_uid: Field, + pub f_subject: Field, + pub f_text: Field, + pub f_from: Field, + pub f_to: Field, + pub f_cc: Field, + pub f_bcc: Field, + pub f_date: Field, + pub f_internal_date: Field, + pub f_size: Field, + pub f_thread_id: Field, + pub f_attachments: Field, + pub f_has_attachment: Field, + pub f_tags: Field, +} + +pub const F_EML: &str = "eml"; + +pub struct EmlFields { + pub f_id: Field, + pub f_account_id: Field, + pub f_mailbox_id: Field, + pub f_eml: Field, +} diff --git a/src/modules/indexer/manager.rs b/src/modules/indexer/manager.rs new file mode 100644 index 0000000..8e1b4ce --- /dev/null +++ b/src/modules/indexer/manager.rs @@ -0,0 +1,1412 @@ +// +// Copyright (c) 2025 rustmailer.com (https://rustmailer.com) +// +// This file is part of the Bichon Email Archiving Project +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +use std::{ + collections::{HashMap, HashSet}, + ops::Bound, + path::PathBuf, + sync::{Arc, LazyLock}, + time::Duration, +}; + +use crate::modules::message::tags::TagCount; +use crate::{ + modules::{ + account::migration::AccountModel, + common::signal::SIGNAL_MANAGER, + dashboard::{DashboardStats, Group, LargestEmail, TimeBucket}, + error::{code::ErrorCode, BichonResult}, + indexer::{ + envelope::Envelope, + fields::{ + F_ACCOUNT_ID, F_FROM, F_HAS_ATTACHMENT, F_INTERNAL_DATE, F_MAILBOX_ID, F_SIZE, + F_TAGS, F_THREAD_ID, F_UID, + }, + schema::SchemaTools, + }, + message::search::SearchFilter, + rest::response::DataPage, + settings::dir::DATA_DIR_MANAGER, + }, + raise_error, utc_now, +}; +use chrono::Utc; +use mail_parser::{MessageParser, MimeHeaders}; +use serde_json::json; +use tantivy::{ + aggregation::{ + agg_req::Aggregations, + agg_result::{ + AggregationResult, AggregationResults, BucketEntries, BucketResult, MetricResult, + }, + AggregationCollector, Key, + }, + collector::{Count, FacetCollector, TopDocs}, + query::{AllQuery, BooleanQuery, Occur, Query, QueryParser, RangeQuery, TermQuery}, + schema::{Facet, IndexRecordOption, Value}, + store::{Compressor, ZstdCompressor}, + DocAddress, Index, IndexBuilder, IndexReader, IndexSettings, IndexWriter, Order, + TantivyDocument, Term, +}; +use tantivy::{indexer::UserOperation, Searcher}; +use tokio::{ + fs::File, + io::AsyncWriteExt, + sync::{mpsc, Mutex}, + task, +}; + +pub static ENVELOPE_INDEX_MANAGER: LazyLock = + LazyLock::new(EnvelopeIndexManager::new); +pub static EML_INDEX_MANAGER: LazyLock = LazyLock::new(EmlIndexManager::new); + +pub const ENVELOPE_BATCH_SIZE: usize = 1000; +pub const EML_BATCH_SIZE: usize = 200; + +const MAX_BUFFER_DURATION: Duration = Duration::from_secs(30); + +pub enum WriteMessage { + Document((u64, TantivyDocument)), + Shutdown, +} + +pub struct EnvelopeIndexManager { + index_writer: Arc>, + sender: mpsc::Sender, + reader: IndexReader, + query_parser: QueryParser, +} + +impl EnvelopeIndexManager { + pub fn new() -> Self { + let index = Self::open_or_create_index(&DATA_DIR_MANAGER.envelope_dir); + let index_writer = Arc::new(Mutex::new( + index + .writer_with_num_threads(8, 536_870_912) + .unwrap_or_else(|e| { + panic!( + "Failed to create IndexWriter with 8 threads and 512MB buffer for {:?}: {}", + DATA_DIR_MANAGER.envelope_dir, e + ) + }), + )); + let reader = index.reader().unwrap_or_else(|e| { + panic!( + "Failed to create IndexReader for {:?}: {}", + DATA_DIR_MANAGER.envelope_dir, e + ) + }); + let mut query_parser = + QueryParser::for_index(&index, SchemaTools::envelope_default_fields()); + query_parser.set_conjunction_by_default(); + + let (sender, mut receiver) = mpsc::channel::(1000); + task::spawn(async move { + let mut buffer: HashMap = + HashMap::with_capacity(ENVELOPE_BATCH_SIZE); + let mut interval = tokio::time::interval(MAX_BUFFER_DURATION); + let mut shutdown = SIGNAL_MANAGER.subscribe(); + loop { + tokio::select! { + maybe_msg = receiver.recv() => { + match maybe_msg { + Some(WriteMessage::Document((eid, doc))) => { + buffer.insert(eid, doc); + if buffer.len() >= ENVELOPE_BATCH_SIZE { + ENVELOPE_INDEX_MANAGER.drain_and_commit(&mut buffer).await; + } + } + Some(WriteMessage::Shutdown) => { + ENVELOPE_INDEX_MANAGER.drain_and_commit(&mut buffer).await; + break; + } + None => break, + } + } + _ = interval.tick() => { + if !buffer.is_empty() { + ENVELOPE_INDEX_MANAGER.drain_and_commit(&mut buffer).await; + } + } + _ = shutdown.recv() => { + let _ = ENVELOPE_INDEX_MANAGER.sender.send(WriteMessage::Shutdown).await; + } + } + } + }); + Self { + index_writer, + sender, + reader, + query_parser, + } + } + + pub async fn add_document(&self, eid: u64, doc: TantivyDocument) { + let _ = self.sender.send(WriteMessage::Document((eid, doc))).await; + } + + async fn drain_and_commit(&self, buffer: &mut HashMap) { + if buffer.is_empty() { + return; + } + let mut writer = self.index_writer.lock().await; + let mut operations = Vec::new(); + + for (eid, doc) in buffer.drain() { + let delete_term = Term::from_field_u64(SchemaTools::envelope_fields().f_id, eid); + operations.push(UserOperation::Delete(delete_term)); + operations.push(UserOperation::Add(doc)); + } + if let Err(e) = writer.run(operations) { + eprintln!("[FATAL] Tantivy run failed: {e:?}"); + std::process::exit(1); + } + + fatal_commit(&mut writer); + } + + fn open_or_create_index(index_dir: &PathBuf) -> Index { + if !index_dir.exists() { + std::fs::create_dir_all(&index_dir).unwrap_or_else(|e| { + panic!("Failed to create index directory {:?}: {}", index_dir, e) + }); + Index::create_in_dir(&index_dir, SchemaTools::envelope_schema()) + .unwrap_or_else(|e| panic!("Failed to create index in {:?}: {}", index_dir, e)) + } else { + open(&index_dir) + } + } + + pub fn total_emails(&self) -> BichonResult { + let searcher = self.create_searcher()?; + Ok(searcher.num_docs()) + } + + fn account_query(&self, account_id: u64) -> Box { + let account_term = + Term::from_field_u64(SchemaTools::envelope_fields().f_account_id, account_id); + Box::new(TermQuery::new(account_term, IndexRecordOption::Basic)) + } + + fn mailbox_query(&self, account_id: u64, mailbox_id: u64) -> Box { + let account_query = TermQuery::new( + Term::from_field_u64(SchemaTools::envelope_fields().f_account_id, account_id), + IndexRecordOption::Basic, + ); + let mailbox_query = TermQuery::new( + Term::from_field_u64(SchemaTools::envelope_fields().f_mailbox_id, mailbox_id), + IndexRecordOption::Basic, + ); + let boolean_query = BooleanQuery::new(vec![ + (Occur::Must, Box::new(account_query)), + (Occur::Must, Box::new(mailbox_query)), + ]); + Box::new(boolean_query) + } + + fn filter_query( + &self, + filter: SearchFilter, + parser: QueryParser, + ) -> BichonResult> { + let f = SchemaTools::envelope_fields(); + let mut subqueries: Vec<(Occur, Box)> = Vec::new(); + + if let Some(ref text) = filter.text { + let query = parser + .parse_query(text) + .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InvalidParameter))?; + subqueries.push((Occur::Must, Box::new(query))); + } + + if let Some(ref tags) = filter.tags { + if !tags.is_empty() { + let mut should_queries: Vec<(Occur, Box)> = Vec::new(); + + for tag in tags { + let facet = Facet::from_text(tag).map_err(|e| { + raise_error!(format!("{:#?}", e), ErrorCode::InvalidParameter) + })?; + + let term = Term::from_facet(f.f_tags, &facet); + + should_queries.push(( + Occur::Should, + Box::new(TermQuery::new(term, IndexRecordOption::Basic)), + )); + } + subqueries.push((Occur::Must, Box::new(BooleanQuery::new(should_queries)))); + } + } + + for (field, opt_value) in [ + (f.f_from, &filter.from), + (f.f_to, &filter.to), + (f.f_cc, &filter.cc), + (f.f_bcc, &filter.bcc), + ] { + if let Some(ref v) = opt_value { + let term = Term::from_field_text(field, v); + subqueries.push(( + Occur::Must, + Box::new(TermQuery::new(term, IndexRecordOption::Basic)), + )); + } + } + + if let Some(has) = filter.has_attachment { + if has { + subqueries.push(( + Occur::Must, + Box::new(TermQuery::new( + Term::from_field_bool(f.f_has_attachment, true.into()), + IndexRecordOption::Basic, + )), + )); + } + } + + if let Some(ref name) = filter.attachment_name { + let term = Term::from_field_text(f.f_attachments, name); + subqueries.push(( + Occur::Must, + Box::new(TermQuery::new(term, IndexRecordOption::Basic)), + )); + } + + let start_bound = if let Some(from) = filter.since { + Bound::Included(Term::from_field_i64(f.f_internal_date, from)) + } else { + Bound::Unbounded + }; + + let end_bound = if let Some(to) = filter.before { + Bound::Included(Term::from_field_i64(f.f_internal_date, to)) + } else { + Bound::Unbounded + }; + + if start_bound != Bound::Unbounded || end_bound != Bound::Unbounded { + let q = RangeQuery::new(start_bound, end_bound); + subqueries.push((Occur::Must, Box::new(q))); + } + + if let Some(account_id) = filter.account_id { + let term = Term::from_field_u64(f.f_account_id, account_id); + subqueries.push(( + Occur::Must, + Box::new(TermQuery::new(term, IndexRecordOption::Basic)), + )); + } + + if let Some(mailbox_id) = filter.mailbox_id { + let term = Term::from_field_u64(f.f_mailbox_id, mailbox_id); + subqueries.push(( + Occur::Must, + Box::new(TermQuery::new(term, IndexRecordOption::Basic)), + )); + } + + let start_bound = if let Some(from) = filter.min_size { + Bound::Included(Term::from_field_u64(f.f_size, from)) + } else { + Bound::Unbounded + }; + + let end_bound = if let Some(to) = filter.max_size { + Bound::Included(Term::from_field_u64(f.f_size, to)) + } else { + Bound::Unbounded + }; + + if start_bound != Bound::Unbounded || end_bound != Bound::Unbounded { + let q = RangeQuery::new(start_bound, end_bound); + subqueries.push((Occur::Must, Box::new(q))); + } + + if let Some(ref msg_id) = filter.message_id { + let term = Term::from_field_text(f.f_message_id, msg_id); + subqueries.push(( + Occur::Must, + Box::new(TermQuery::new(term, IndexRecordOption::Basic)), + )); + } + + if subqueries.is_empty() { + return Ok(Box::new(AllQuery)); + } + + Ok(Box::new(BooleanQuery::new(subqueries))) + } + + fn thread_query(&self, account_id: u64, thread_id: u64) -> Box { + let account_query = TermQuery::new( + Term::from_field_u64(SchemaTools::envelope_fields().f_account_id, account_id), + IndexRecordOption::Basic, + ); + let thread_query = TermQuery::new( + Term::from_field_u64(SchemaTools::envelope_fields().f_thread_id, thread_id), + IndexRecordOption::Basic, + ); + let boolean_query = BooleanQuery::new(vec![ + (Occur::Must, Box::new(account_query)), + (Occur::Must, Box::new(thread_query)), + ]); + Box::new(boolean_query) + } + + fn envelope_query(&self, account_id: u64, eid: u64) -> Box { + let account_id_query = TermQuery::new( + Term::from_field_u64(SchemaTools::envelope_fields().f_account_id, account_id), + IndexRecordOption::Basic, + ); + let envelope_id_query = TermQuery::new( + Term::from_field_u64(SchemaTools::envelope_fields().f_id, eid), + IndexRecordOption::Basic, + ); + let boolean_query = BooleanQuery::new(vec![ + (Occur::Must, Box::new(account_id_query)), + (Occur::Must, Box::new(envelope_id_query)), + ]); + Box::new(boolean_query) + } + + pub async fn delete_account_envelopes(&self, account_id: u64) -> BichonResult<()> { + let query = self.account_query(account_id); + let mut writer = self.index_writer.lock().await; + writer + .delete_query(query) + .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))?; + writer + .commit() + .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))?; + Ok(()) + } + + pub async fn delete_mailbox_envelopes( + &self, + account_id: u64, + mailbox_ids: Vec, + ) -> BichonResult<()> { + if mailbox_ids.is_empty() { + tracing::warn!("delete_mailbox_envelopes: mailbox_ids is empty, nothing to delete"); + return Ok(()); + } + let mut queries: Vec> = Vec::with_capacity(mailbox_ids.len()); + for mailbox_id in mailbox_ids { + queries.push(self.mailbox_query(account_id, mailbox_id)); + } + let mut writer = self.index_writer.lock().await; + for query in queries { + writer + .delete_query(query) + .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))?; + } + writer + .commit() + .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))?; + Ok(()) + } + + fn collect_facets_recursive( + searcher: &Searcher, + parent_facet: &str, + all_facets: &mut Vec, + ) -> BichonResult<()> { + let mut facet_collector = FacetCollector::for_field(F_TAGS); + facet_collector.add_facet(parent_facet); + let facet_counts = searcher + .search(&AllQuery, &facet_collector) + .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))?; + + for (facet, count) in facet_counts.get(parent_facet) { + all_facets.push(TagCount { + tag: facet.to_string(), + count, + }); + Self::collect_facets_recursive(searcher, &facet.to_string(), all_facets)?; + } + + Ok(()) + } + + pub async fn get_all_tags(&self) -> BichonResult> { + let searcher = self.reader.searcher(); + let mut all_facets = Vec::new(); + Self::collect_facets_recursive(&searcher, "/", &mut all_facets)?; + Ok(all_facets) + } + + pub async fn delete_envelopes_multi_account( + &self, + deletes: &HashMap>, // HashMap + ) -> BichonResult<()> { + if deletes.is_empty() { + tracing::warn!("delete_envelopes_multi_account: deletes is empty, nothing to delete"); + return Ok(()); + } + + let mut writer = self.index_writer.lock().await; + + for (account_id, envelope_ids) in deletes { + let unique_ids: HashSet = envelope_ids.iter().copied().collect(); + if unique_ids.is_empty() { + continue; + } + for eid in unique_ids { + let query = self.envelope_query(*account_id, eid); + writer + .delete_query(query) + .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))?; + } + } + writer + .commit() + .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))?; + + Ok(()) + } + + pub async fn update_envelope_tags( + &self, + updates: HashMap>, // HashMap + tags: Vec, + ) -> BichonResult<()> { + if updates.is_empty() { + tracing::warn!("update_envelope_tags: updates is empty, nothing to update"); + return Ok(()); + } + + let searcher = self.create_searcher()?; + let mut writer = self.index_writer.lock().await; + + let f_tags = SchemaTools::envelope_fields().f_tags; + let f_id = SchemaTools::envelope_fields().f_id; + let deduplicated_updates: HashMap> = updates + .into_iter() + .map(|(account_id, envelope_ids)| (account_id, envelope_ids.into_iter().collect())) + .collect(); + + let mut operations = Vec::new(); + + for (account_id, envelope_ids) in &deduplicated_updates { + for eid in envelope_ids { + let query = self.envelope_query(*account_id, *eid); + let docs = searcher + .search(query.as_ref(), &TopDocs::with_limit(1)) + .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))?; + + if let Some((_, doc_address)) = docs.first() { + let old_doc: TantivyDocument = searcher + .doc_async(*doc_address) + .await + .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))?; + + let mut new_doc = TantivyDocument::new(); + for (field, value) in old_doc.field_values() { + if field != f_tags { + new_doc.add_field_value(field, value); + } + } + for tag in &tags { + new_doc.add_facet(f_tags, tag); + } + + let delete_term = Term::from_field_u64(f_id, *eid); + operations.push(UserOperation::Delete(delete_term)); + operations.push(UserOperation::Add(new_doc)); + } + } + } + + writer + .run(operations) + .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))?; + + // commit + writer + .commit() + .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))?; + + Ok(()) + } + + pub async fn search( + &self, + filter: SearchFilter, + page: u64, + page_size: u64, + desc: bool, + ) -> BichonResult> { + assert!(page > 0, "Page number must be greater than 0"); + assert!(page_size > 0, "Page size must be greater than 0"); + let query = self.filter_query(filter, self.query_parser.clone())?; + let searcher = self.create_searcher()?; + let total = searcher + .search(&query, &Count) + .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))? + as u64; + + if total == 0 { + return Ok(DataPage { + current_page: Some(page), + page_size: Some(page_size), + total_items: 0, + items: vec![], + total_pages: Some(0), + }); + } + let offset = (page - 1) * page_size; + let total_pages = total.div_ceil(page_size); + if offset > total { + return Ok(DataPage { + current_page: Some(page), + page_size: Some(page_size), + total_items: total, + items: vec![], + total_pages: Some(total_pages), + }); + } + + let order = if desc { Order::Desc } else { Order::Asc }; + let mailbox_docs: Vec<(i64, DocAddress)> = searcher + .search( + &query, + &TopDocs::with_limit(page_size as usize) + .and_offset(offset as usize) + .order_by_fast_field(F_INTERNAL_DATE, order), + ) + .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))?; + let mut result = Vec::new(); + + for (_, doc_address) in mailbox_docs { + let doc: TantivyDocument = searcher + .doc_async(doc_address) + .await + .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))?; + let envelope = Envelope::from_tantivy_doc(&doc).await?; + result.push(envelope); + } + Ok(DataPage { + current_page: Some(page), + page_size: Some(page_size), + total_items: total, + items: result, + total_pages: Some(total_pages), + }) + } + + pub async fn list_mailbox_envelopes( + &self, + account_id: u64, + mailbox_id: u64, + page: u64, + page_size: u64, + desc: bool, + ) -> BichonResult> { + assert!(page > 0, "Page number must be greater than 0"); + assert!(page_size > 0, "Page size must be greater than 0"); + let searcher = self.create_searcher()?; + let total = self + .num_messages_in_mailbox(&searcher, account_id, mailbox_id) + .await?; + if total == 0 { + return Ok(DataPage { + current_page: Some(page), + page_size: Some(page_size), + total_items: 0, + items: vec![], + total_pages: Some(0), + }); + } + let offset = (page - 1) * page_size; + let total_pages = total.div_ceil(page_size); + if offset > total { + return Ok(DataPage { + current_page: Some(page), + page_size: Some(page_size), + total_items: total, + items: vec![], + total_pages: Some(total_pages), + }); + } + let query = self.mailbox_query(account_id, mailbox_id); + let order = if desc { Order::Desc } else { Order::Asc }; + let mailbox_docs: Vec<(i64, DocAddress)> = searcher + .search( + query.as_ref(), + &TopDocs::with_limit(page_size as usize) + .and_offset(offset as usize) + .order_by_fast_field(F_INTERNAL_DATE, order), + ) + .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))?; + let mut result = Vec::new(); + + for (_, doc_address) in mailbox_docs { + let doc: TantivyDocument = searcher + .doc_async(doc_address) + .await + .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))?; + let envelope = Envelope::from_tantivy_doc(&doc).await?; + result.push(envelope); + } + Ok(DataPage { + current_page: Some(page), + page_size: Some(page_size), + total_items: total, + items: result, + total_pages: Some(total_pages), + }) + } + + pub async fn list_thread_envelopes( + &self, + account_id: u64, + thread_id: u64, + page: u64, + page_size: u64, + desc: bool, + ) -> BichonResult> { + assert!(page > 0, "Page number must be greater than 0"); + assert!(page_size > 0, "Page size must be greater than 0"); + let searcher = self.create_searcher()?; + let total = self + .num_messages_in_thread(&searcher, account_id, thread_id) + .await?; + if total == 0 { + return Ok(DataPage { + current_page: Some(page), + page_size: Some(page_size), + total_items: 0, + items: vec![], + total_pages: Some(0), + }); + } + let offset = (page - 1) * page_size; + let total_pages = total.div_ceil(page_size); + if offset > total { + return Ok(DataPage { + current_page: Some(page), + page_size: Some(page_size), + total_items: total, + items: vec![], + total_pages: Some(total_pages), + }); + } + + let query = self.thread_query(account_id, thread_id); + + let order = if desc { Order::Desc } else { Order::Asc }; + let thread_docs: Vec<(i64, DocAddress)> = searcher + .search( + query.as_ref(), + &TopDocs::with_limit(page_size as usize) + .and_offset(offset as usize) + .order_by_fast_field(F_INTERNAL_DATE, order), + ) + .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))?; + let mut result = Vec::new(); + + for (_, doc_address) in thread_docs { + let doc: TantivyDocument = searcher + .doc_async(doc_address) + .await + .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))?; + let envelope = Envelope::from_tantivy_doc(&doc).await?; + result.push(envelope); + } + Ok(DataPage { + current_page: Some(page), + page_size: Some(page_size), + total_items: total, + items: result, + total_pages: Some(total_pages), + }) + } + + pub async fn top_10_largest_emails(&self) -> BichonResult> { + self.reader + .reload() + .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))?; + let searcher = self.reader.searcher(); + + let mailbox_docs: Vec<(u64, DocAddress)> = searcher + .search( + &AllQuery, + &TopDocs::with_limit(10).order_by_fast_field(F_SIZE, Order::Desc), + ) + .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))?; + let mut result = Vec::new(); + + for (_, doc_address) in mailbox_docs { + let doc: TantivyDocument = searcher + .doc_async(doc_address) + .await + .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))?; + let envelope = LargestEmail::from_tantivy_doc(&doc)?; + result.push(envelope); + } + Ok(result) + } + + pub async fn get_max_uid(&self, account_id: u64, mailbox_id: u64) -> BichonResult> { + self.reader + .reload() + .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))?; + let searcher = self.reader.searcher(); + + let query = self.mailbox_query(account_id, mailbox_id); + let agg_req: Aggregations = serde_json::from_value(json!({ + "max_uid": { + "max": { + "field": F_UID + } + } + })) + .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))?; + + let collector = AggregationCollector::from_aggs(agg_req, Default::default()); + + let agg_res = searcher + .search(query.as_ref(), &collector) + .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))?; + Ok(Self::extract_max_uid(&agg_res)) + } + + fn extract_max_uid(agg_res: &AggregationResults) -> Option { + agg_res.0.get("max_uid").and_then(|result| match result { + AggregationResult::MetricResult(MetricResult::Max(max)) => { + max.value.and_then(|value| { + (value >= 0.0 && value <= u64::MAX as f64).then(|| value.trunc() as u64) + }) + } + _ => None, + }) + } + + pub async fn num_messages_in_mailbox( + &self, + searcher: &Searcher, + account_id: u64, + mailbox_id: u64, + ) -> BichonResult { + let query = self.mailbox_query(account_id, mailbox_id); + + let agg_req: Aggregations = serde_json::from_value(json!({ + "mailbox_count": { + "value_count": { + "field": F_MAILBOX_ID + } + } + })) + .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))?; + + let collector = AggregationCollector::from_aggs(agg_req, Default::default()); + + let agg_res = searcher + .search(query.as_ref(), &collector) + .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))?; + Self::extract_mailbox_count(&agg_res) + } + + fn extract_mailbox_count(agg_res: &AggregationResults) -> BichonResult { + let Some(result) = agg_res.0.get("mailbox_count") else { + return Err(raise_error!( + "Missing aggregation result: mailbox_count".into(), + ErrorCode::InternalError + )); + }; + + match result { + AggregationResult::MetricResult(MetricResult::Count(count)) => { + Ok(count.value.map(|v| v as u64).ok_or_else(|| { + raise_error!( + "Failed to get count value from aggregation result: value is None".into(), + ErrorCode::InternalError + ) + })?) + } + other => Err(raise_error!( + format!("Unexpected aggregation result type: {other:?}"), + ErrorCode::InternalError + )), + } + } + + pub async fn num_messages_in_thread( + &self, + searcher: &Searcher, + account_id: u64, + thread_id: u64, + ) -> BichonResult { + let query = self.thread_query(account_id, thread_id); + + let agg_req: Aggregations = serde_json::from_value(json!({ + "thread_count": { + "value_count": { + "field": F_THREAD_ID + } + } + })) + .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))?; + let collector = AggregationCollector::from_aggs(agg_req, Default::default()); + + let agg_res = searcher + .search(query.as_ref(), &collector) + .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))?; + Self::extract_thread_count(&agg_res) + } + + fn extract_thread_count(agg_res: &AggregationResults) -> BichonResult { + let Some(result) = agg_res.0.get("thread_count") else { + return Err(raise_error!( + "Missing aggregation result: thread_count".into(), + ErrorCode::InternalError + )); + }; + + match result { + AggregationResult::MetricResult(MetricResult::Count(count)) => { + Ok(count.value.map(|v| v as u64).ok_or_else(|| { + raise_error!( + "Failed to get count value from aggregation result: value is None".into(), + ErrorCode::InternalError + ) + })?) + } + other => Err(raise_error!( + format!("Unexpected aggregation result type: {other:?}"), + ErrorCode::InternalError + )), + } + } + + fn create_searcher(&self) -> BichonResult { + self.reader + .reload() + .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))?; + Ok(self.reader.searcher()) + } + + pub async fn get_dashboard_stats(&self) -> BichonResult { + let searcher = self.create_searcher()?; + let now_ms = utc_now!(); + let week_ago_ms = (Utc::now() - Duration::from_secs(60 * 60 * 24 * 30)).timestamp_millis(); + + let aggregations: Aggregations = serde_json::from_value(json!({ + "total_size": { + "sum": { "field": F_SIZE } + }, + "recent_30d_histogram": { + "histogram": { + "field": F_INTERNAL_DATE, + "interval": 86400000, + "hard_bounds": { + "min": week_ago_ms, + "max": now_ms + } + } + }, + "top_from_values": { + "terms": { + "field": F_FROM, + "size": 10 + } + }, + "top_account_values": { + "terms": { + "field": F_ACCOUNT_ID, + "size": 10 + } + }, + "attachment_stats": { + "terms": { + "field": F_HAS_ATTACHMENT + } + } + })) + .unwrap(); + + let query = AllQuery; + let agg_collector = AggregationCollector::from_aggs(aggregations, Default::default()); + let agg_results = searcher + .search(&query, &agg_collector) + .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))?; + + let mut stats = DashboardStats::default(); + let total_size = agg_results.0.get("total_size").ok_or_else(|| { + raise_error!( + "missing 'total_size' aggregation result".into(), + ErrorCode::InternalError + ) + })?; + + if let AggregationResult::MetricResult(MetricResult::Sum(v)) = total_size { + let total_size = v.value.map(|v| v as u64).ok_or_else(|| { + raise_error!( + "'total_size' sum metric has no value".into(), + ErrorCode::InternalError + ) + })?; + stats.total_size_bytes = total_size; + } + + let recent_30d_histogram = agg_results.0.get("recent_30d_histogram").ok_or_else(|| { + raise_error!( + "missing 'recent_30d_histogram' aggregation result".into(), + ErrorCode::InternalError + ) + })?; + + let mut recent_activity = Vec::with_capacity(31); + if let AggregationResult::BucketResult(BucketResult::Histogram { buckets, .. }) = + recent_30d_histogram + { + if let BucketEntries::Vec(bucket_list) = buckets { + for entry in bucket_list { + if let Key::F64(ms) = entry.key { + recent_activity.push(TimeBucket { + timestamp_ms: ms as i64, + count: entry.doc_count, + }); + } + } + } + } + stats.recent_activity = recent_activity; + let mut top_senders = Vec::with_capacity(11); + let top_from_values = agg_results.0.get("top_from_values").unwrap(); + if let AggregationResult::BucketResult(BucketResult::Terms { buckets, .. }) = + top_from_values + { + for entry in buckets { + if let Key::Str(sender) = &entry.key { + top_senders.push(Group { + key: sender.clone(), + count: entry.doc_count, + }); + } + } + } + stats.top_senders = top_senders; + + let mut top_accounts = Vec::with_capacity(11); + let top_account_values = agg_results.0.get("top_account_values").unwrap(); + if let AggregationResult::BucketResult(BucketResult::Terms { buckets, .. }) = + top_account_values + { + for entry in buckets { + if let Key::U64(account_id) = &entry.key { + top_accounts.push(Group { + key: AccountModel::get(*account_id).await?.email, + count: entry.doc_count, + }); + } + } + } + stats.top_accounts = top_accounts; + + let attachment_stats = agg_results.0.get("attachment_stats").unwrap(); + if let AggregationResult::BucketResult(BucketResult::Terms { buckets, .. }) = + attachment_stats + { + for entry in buckets { + if let Key::U64(key) = entry.key { + if key == 0 { + stats.without_attachment_count = entry.doc_count; + } + if key == 1 { + stats.with_attachment_count = entry.doc_count; + } + } + } + } + Ok(stats) + } +} + +pub struct EmlIndexManager { + index_writer: Arc>, + sender: mpsc::Sender, + reader: IndexReader, +} + +impl EmlIndexManager { + pub fn new() -> Self { + let index = Self::open_or_create_index(&DATA_DIR_MANAGER.eml_dir); + let index_writer = Arc::new(Mutex::new( + index + .writer_with_num_threads(8, 536_870_912) + .unwrap_or_else(|e| { + panic!( + "Failed to create IndexWriter with 8 threads and 512MB buffer for {:?}: {}", + DATA_DIR_MANAGER.eml_dir, e + ) + }), + )); + let reader = index.reader().unwrap_or_else(|e| { + panic!( + "Failed to create IndexReader for {:?}: {}", + DATA_DIR_MANAGER.eml_dir, e + ) + }); + let (sender, mut receiver) = mpsc::channel::(100); + task::spawn(async move { + let mut buffer: HashMap = HashMap::with_capacity(EML_BATCH_SIZE); + let mut interval = tokio::time::interval(MAX_BUFFER_DURATION); + let mut shutdown = SIGNAL_MANAGER.subscribe(); + loop { + tokio::select! { + maybe_msg = receiver.recv() => { + match maybe_msg { + Some(WriteMessage::Document((eid, doc))) => { + buffer.insert(eid, doc); + if buffer.len() >= EML_BATCH_SIZE { + EML_INDEX_MANAGER.drain_and_commit(&mut buffer).await; + } + } + Some(WriteMessage::Shutdown) => { + EML_INDEX_MANAGER.drain_and_commit(&mut buffer).await; + break; + } + None => break, + } + } + _ = interval.tick() => { + if !buffer.is_empty() { + EML_INDEX_MANAGER.drain_and_commit(&mut buffer).await; + } + } + _ = shutdown.recv() => { + let _ = EML_INDEX_MANAGER.sender.send(WriteMessage::Shutdown).await; + } + } + } + }); + Self { + index_writer, + sender, + reader, + } + } + + pub async fn add_document(&self, eid: u64, doc: TantivyDocument) { + let _ = self.sender.send(WriteMessage::Document((eid, doc))).await; + } + + fn open_or_create_index(index_dir: &PathBuf) -> Index { + if !index_dir.exists() { + std::fs::create_dir_all(&index_dir).unwrap_or_else(|e| { + panic!("Failed to create index directory {:?}: {}", index_dir, e) + }); + IndexBuilder::new() + .schema(SchemaTools::eml_schema()) + .settings(IndexSettings { + docstore_compression: Compressor::Zstd(ZstdCompressor { + compression_level: Some(6), + }), + docstore_compress_dedicated_thread: true, + docstore_blocksize: 2_097_152, + }) + .create_in_dir(&index_dir) + .unwrap_or_else(|e| panic!("Failed to create index in {:?}: {}", index_dir, e)) + } else { + open(&index_dir) + } + } + + fn envelope_query(&self, account_id: u64, eid: u64) -> Box { + let account_id_query = TermQuery::new( + Term::from_field_u64(SchemaTools::eml_fields().f_account_id, account_id), + IndexRecordOption::Basic, + ); + let envelope_id_query = TermQuery::new( + Term::from_field_u64(SchemaTools::eml_fields().f_id, eid), + IndexRecordOption::Basic, + ); + let boolean_query = BooleanQuery::new(vec![ + (Occur::Must, Box::new(account_id_query)), + (Occur::Must, Box::new(envelope_id_query)), + ]); + Box::new(boolean_query) + } + + pub async fn get(&self, account_id: u64, eid: u64) -> BichonResult>> { + let searcher = self.reader.searcher(); + let query = self.envelope_query(account_id, eid); + let docs = searcher + .search(query.as_ref(), &TopDocs::with_limit(1)) + .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))?; + + if docs.is_empty() { + return Ok(None); + } + + let (_, doc_address) = docs.first().unwrap(); + let doc: TantivyDocument = searcher + .doc_async(*doc_address) + .await + .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))?; + let fields = SchemaTools::eml_fields(); + let value = doc.get_first(fields.f_eml).ok_or_else(|| { + raise_error!( + format!("miss '{}' field in tantivy document", stringify!(field)), + ErrorCode::InternalError + ) + })?; + let bytes = value.as_bytes().ok_or_else(|| { + raise_error!( + format!("'{}' field is not a bytes", stringify!(field)), + ErrorCode::InternalError + ) + })?; + + Ok(Some(bytes.to_vec())) + } + + pub async fn get_reader(&self, account_id: u64, eid: u64) -> BichonResult { + let data = self.get(account_id, eid).await?.ok_or_else(|| { + raise_error!( + format!("Email not found: account_id={}, eid={}", account_id, eid), + ErrorCode::ResourceNotFound + ) + })?; + let mut path = DATA_DIR_MANAGER.temp_dir.clone(); + + path.push(format!("{eid}.eml")); + { + let mut file = File::create(&path) + .await + .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))?; + file.write_all(&data) + .await + .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))?; + } + let file = File::open(&path) + .await + .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))?; + Ok(file) + } + + pub async fn get_attachment( + &self, + account_id: u64, + eid: u64, + file_name: &str, + ) -> BichonResult { + let data = self.get(account_id, eid).await?.ok_or_else(|| { + raise_error!( + format!("Email not found: account_id={}, eid={}", account_id, eid), + ErrorCode::ResourceNotFound + ) + })?; + let message = MessageParser::default().parse(&data).ok_or_else(|| { + raise_error!( + format!( + "Failed to parse email: account_id={}, eid={}", + account_id, eid + ), + ErrorCode::InternalError + ) + })?; + let target_attachment = message + .attachments() + .find(|p| p.attachment_name().is_some_and(|name| name == file_name)); + let content = match target_attachment { + Some(att) => att.contents(), + None => { + return Err(raise_error!( + "Attachment not found".into(), + ErrorCode::ResourceNotFound + )) + } + }; + let mut path = DATA_DIR_MANAGER.temp_dir.clone(); + path.push(format!("{eid}.{file_name}.eml")); + { + let mut file = File::create(&path) + .await + .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))?; + file.write_all(content) + .await + .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))?; + } + let file = File::open(&path) + .await + .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))?; + Ok(file) + } + + fn account_query(&self, account_id: u64) -> Box { + let account_term = Term::from_field_u64(SchemaTools::eml_fields().f_account_id, account_id); + Box::new(TermQuery::new(account_term, IndexRecordOption::Basic)) + } + + pub async fn delete_account_envelopes(&self, account_id: u64) -> BichonResult<()> { + let query = self.account_query(account_id); + let mut writer = self.index_writer.lock().await; + writer + .delete_query(query) + .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))?; + writer + .commit() + .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))?; + Ok(()) + } + + fn mailbox_query(&self, account_id: u64, mailbox_id: u64) -> Box { + let account_query = TermQuery::new( + Term::from_field_u64(SchemaTools::eml_fields().f_account_id, account_id), + IndexRecordOption::Basic, + ); + let mailbox_query = TermQuery::new( + Term::from_field_u64(SchemaTools::eml_fields().f_mailbox_id, mailbox_id), + IndexRecordOption::Basic, + ); + let boolean_query = BooleanQuery::new(vec![ + (Occur::Must, Box::new(account_query)), + (Occur::Must, Box::new(mailbox_query)), + ]); + Box::new(boolean_query) + } + + pub async fn delete_mailbox_envelopes( + &self, + account_id: u64, + mailbox_ids: Vec, + ) -> BichonResult<()> { + if mailbox_ids.is_empty() { + tracing::warn!("delete_mailbox_envelopes: mailbox_ids is empty, nothing to delete"); + return Ok(()); + } + let mut queries: Vec> = Vec::with_capacity(mailbox_ids.len()); + for mailbox_id in mailbox_ids { + queries.push(self.mailbox_query(account_id, mailbox_id)); + } + let mut writer = self.index_writer.lock().await; + for query in queries { + writer + .delete_query(query) + .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))?; + } + writer + .commit() + .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))?; + Ok(()) + } + + pub async fn delete_email_multi_account( + &self, + deletes: &HashMap>, // HashMap + ) -> BichonResult<()> { + if deletes.is_empty() { + tracing::warn!("delete_envelopes_multi_account: deletes is empty, nothing to delete"); + return Ok(()); + } + + let mut writer = self.index_writer.lock().await; + + for (account_id, envelope_ids) in deletes { + let unique_ids: HashSet = envelope_ids.iter().copied().collect(); + if unique_ids.is_empty() { + continue; + } + for eid in unique_ids { + let query = self.envelope_query(*account_id, eid); + writer + .delete_query(query) + .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))?; + } + } + writer + .commit() + .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))?; + + Ok(()) + } + + async fn drain_and_commit(&self, buffer: &mut HashMap) { + if buffer.is_empty() { + return; + } + let mut writer = self.index_writer.lock().await; + let mut operations = Vec::new(); + + for (eid, doc) in buffer.drain() { + let delete_term = Term::from_field_u64(SchemaTools::eml_fields().f_id, eid); + operations.push(UserOperation::Delete(delete_term)); + operations.push(UserOperation::Add(doc)); + } + if let Err(e) = writer.run(operations) { + eprintln!("[FATAL] Tantivy run failed: {e:?}"); + std::process::exit(1); + } + + fatal_commit(&mut writer); + } +} + +fn fatal_commit(writer: &mut IndexWriter) { + const MAX_RETRIES: usize = 3; + const RETRY_DELAY_MS: u64 = 1000; + + for attempt in 0..=MAX_RETRIES { + match writer.commit() { + Ok(_) => { + if attempt > 0 { + eprintln!("[INFO] Commit succeeded on attempt {}", attempt + 1); + } + return; + } + Err(e) => match &e { + tantivy::TantivyError::IoError(io_error) => { + if attempt < MAX_RETRIES { + eprintln!( + "[WARN] Commit failed (attempt {}/{}): {:?}. Retrying in {}ms...", + attempt + 1, + MAX_RETRIES + 1, + io_error, + RETRY_DELAY_MS * (attempt as u64 + 1) + ); + std::thread::sleep(std::time::Duration::from_millis( + RETRY_DELAY_MS * (attempt as u64 + 1), + )); + } else { + eprintln!( + "[FATAL] Tantivy commit failed after {} attempts: {:?}", + MAX_RETRIES + 1, + io_error + ); + std::process::exit(1); + } + } + _ => { + eprintln!("[FATAL] Tantivy commit failed with non-IO error: {e:?}"); + std::process::exit(1); + } + }, + } + } +} + +fn open(index_dir: &PathBuf) -> Index { + Index::open_in_dir(index_dir) + .unwrap_or_else(|e| panic!("Failed to open index in {:?}: {}", index_dir, e)) +} diff --git a/src/modules/indexer/mod.rs b/src/modules/indexer/mod.rs new file mode 100644 index 0000000..cd8d464 --- /dev/null +++ b/src/modules/indexer/mod.rs @@ -0,0 +1,25 @@ +// +// Copyright (c) 2025 rustmailer.com (https://rustmailer.com) +// +// This file is part of the Bichon Email Archiving Project +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +pub mod envelope; +pub mod fields; +pub mod manager; +pub mod schema; +#[cfg(test)] +mod tests; diff --git a/src/modules/indexer/schema.rs b/src/modules/indexer/schema.rs new file mode 100644 index 0000000..2386dd5 --- /dev/null +++ b/src/modules/indexer/schema.rs @@ -0,0 +1,128 @@ +// +// Copyright (c) 2025 rustmailer.com (https://rustmailer.com) +// +// This file is part of the Bichon Email Archiving Project +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +use std::sync::{Arc, LazyLock}; + +use crate::modules::indexer::fields::{EnvelopeFields, *}; +use tantivy::schema::{FacetOptions, Field, INDEXED}; +use tantivy::schema::{Schema, FAST, STORED, STRING, TEXT}; + +static ENVELOPE_FIELDS: LazyLock> = LazyLock::new(|| { + let (_, fields) = SchemaTools::create_envelope_schema(); + Arc::new(fields) +}); + +static EML_FIELDS: LazyLock> = LazyLock::new(|| { + let (_, fields) = SchemaTools::create_eml_schema(); + Arc::new(fields) +}); + +pub struct SchemaTools; + +impl SchemaTools { + pub fn envelope_schema() -> Schema { + let (schema, _) = Self::create_envelope_schema(); + schema + } + + pub fn eml_schema() -> Schema { + let (schema, _) = Self::create_eml_schema(); + schema + } + + pub fn envelope_fields() -> &'static EnvelopeFields { + &ENVELOPE_FIELDS + } + + pub fn eml_fields() -> &'static EmlFields { + &EML_FIELDS + } + + pub fn envelope_default_fields() -> Vec { + let fields = Self::envelope_fields(); + vec![fields.f_subject, fields.f_text, fields.f_attachments] + } + + pub fn create_envelope_schema() -> (Schema, EnvelopeFields) { + let mut builder = Schema::builder(); + let f_id = builder.add_u64_field(F_ID, INDEXED | STORED | FAST); + // Account/ Mailbox IDs: numeric, for filtering/aggregation + let f_account_id = builder.add_u64_field(F_ACCOUNT_ID, INDEXED | STORED | FAST); + let f_mailbox_id = builder.add_u64_field(F_MAILBOX_ID, INDEXED | STORED | FAST); + // UID: numeric, locate message + let f_uid = builder.add_u64_field(F_UID, INDEXED | STORED | FAST); + // Subject/body: tokenized for full-text search + let f_subject = builder.add_text_field(F_SUBJECT, TEXT | STORED); + let f_text = builder.add_text_field(F_TEXT, TEXT | STORED); + // Email addresses: exact match search + let f_from = builder.add_text_field(F_FROM, STRING | STORED | FAST); + let f_to = builder.add_text_field(F_TO, STRING | STORED); + let f_cc = builder.add_text_field(F_CC, STRING | STORED); + let f_bcc = builder.add_text_field(F_BCC, STRING | STORED); + // Date fields: numeric, range filtering + let f_date = builder.add_i64_field(F_DATE, STORED | FAST); + let f_internal_date = builder.add_i64_field(F_INTERNAL_DATE, STORED | FAST); + // Size: numeric, range filtering + let f_size = builder.add_u64_field(F_SIZE, STORED | FAST); + // Thread ID: numeric, filter by thread + let f_thread_id = builder.add_u64_field(F_THREAD_ID, INDEXED | STORED | FAST); + // Message-ID: unique identifier, no tokenization + let f_message_id = builder.add_text_field(F_MESSAGE_ID, STRING | STORED); + // Attachments: exact match search + let f_attachments = builder.add_text_field(F_ATTACHMENTS, TEXT | STORED); + let f_has_attachment = builder.add_bool_field(F_HAS_ATTACHMENT, INDEXED | STORED | FAST); + let f_tags = builder.add_facet_field(F_TAGS, FacetOptions::default().set_stored()); + let fields = EnvelopeFields { + f_id, + f_account_id, + f_mailbox_id, + f_uid, + f_subject, + f_text, + f_from, + f_to, + f_cc, + f_bcc, + f_date, + f_internal_date, + f_size, + f_thread_id, + f_message_id, + f_attachments, + f_has_attachment, + f_tags, + }; + (builder.build(), fields) + } + + pub fn create_eml_schema() -> (Schema, EmlFields) { + let mut builder = Schema::builder(); + let f_id = builder.add_u64_field(F_ID, INDEXED | FAST); + let f_account_id = builder.add_u64_field(F_ACCOUNT_ID, INDEXED | STORED | FAST); + let f_mailbox_id = builder.add_u64_field(F_MAILBOX_ID, INDEXED | STORED | FAST); + let f_eml = builder.add_bytes_field(F_EML, STORED); + let fields = EmlFields { + f_id, + f_account_id, + f_mailbox_id, + f_eml, + }; + (builder.build(), fields) + } +} diff --git a/src/modules/indexer/tests.rs b/src/modules/indexer/tests.rs new file mode 100644 index 0000000..1577d92 --- /dev/null +++ b/src/modules/indexer/tests.rs @@ -0,0 +1,229 @@ +// +// Copyright (c) 2025 rustmailer.com (https://rustmailer.com) +// +// This file is part of the Bichon Email Archiving Project +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +use std::{path::PathBuf, time::Duration}; + +use chrono::Utc; +use serde_json::json; +use tantivy::{ + aggregation::{ + agg_req::Aggregations, + agg_result::{AggregationResult, BucketEntries, BucketResult, MetricResult}, + AggregationCollector, Key, + }, + collector::TopDocs, + doc, + indexer::UserOperation, + query::{AllQuery, QueryParser, TermQuery}, + schema::{IndexRecordOption, Schema, Value}, + Index, IndexWriter, TantivyDocument, Term, +}; + +use crate::{ + modules::{ + dashboard::TimeBucket, + indexer::{ + fields::{F_FROM, F_HAS_ATTACHMENT, F_INTERNAL_DATE, F_SIZE}, + schema::SchemaTools, + }, + }, + utc_now, +}; + +#[tokio::test] +async fn test1() { + let index = Index::open_in_dir(PathBuf::from("E:/bichon-data/envelope")).unwrap(); + let reader = index.reader().unwrap(); + let mut query_parser = QueryParser::for_index(&index, SchemaTools::envelope_default_fields()); + query_parser.set_conjunction_by_default(); + let searcher = reader.searcher(); + + let now_ms = utc_now!(); + let week_ago_ms = (Utc::now() - Duration::from_secs(60 * 60 * 24 * 7)).timestamp_millis(); + let aggregations: Aggregations = serde_json::from_value(json!({ + "total_size": { + "sum": { "field": F_SIZE } + }, + "recent_7d_histogram": { + "histogram": { + "field": F_INTERNAL_DATE, + "interval": 86400000, + "hard_bounds": { + "min": week_ago_ms, + "max": now_ms + } + } + }, + "top_from_values": { + "terms": { + "field": F_FROM, + "size": 10 + } + }, + "attachment_stats": { + "terms": { + "field": F_HAS_ATTACHMENT + } + } + })) + .unwrap(); + + let query = AllQuery; + let agg_collector = AggregationCollector::from_aggs(aggregations, Default::default()); + let agg_results = searcher.search(&query, &agg_collector).unwrap(); + + let total_size = agg_results.0.get("total_size").unwrap(); + + if let AggregationResult::MetricResult(MetricResult::Sum(count)) = total_size { + let total_size = count.value.map(|v| v as u64).unwrap(); + println!("{:#?}", total_size); + } + + let recent_7d_histogram = agg_results.0.get("recent_7d_histogram").unwrap(); + + let mut recent_activity = Vec::with_capacity(15); + if let AggregationResult::BucketResult(BucketResult::Histogram { buckets, .. }) = + recent_7d_histogram + { + if let BucketEntries::Vec(bucket_list) = buckets { + for entry in bucket_list { + if let Key::F64(ms) = entry.key { + recent_activity.push(TimeBucket { + timestamp_ms: ms as i64, + count: entry.doc_count, + }); + } + } + } + } + println!("recent_activity: {:#?}", recent_activity); + + let top_from_values = agg_results.0.get("top_from_values").unwrap(); + + if let AggregationResult::BucketResult(BucketResult::Terms { buckets, .. }) = top_from_values { + println!("{:#?}", buckets); + } + + let attachment_stats = agg_results.0.get("attachment_stats").unwrap(); + + if let AggregationResult::BucketResult(BucketResult::Terms { buckets, .. }) = attachment_stats { + println!("{:#?}", buckets); + } + + //agg_results. +} + +#[tokio::test] +async fn test2() { + use tantivy::schema::{FAST, INDEXED, STORED, STRING}; + let mut builder = Schema::builder(); + let a = builder.add_u64_field("a", INDEXED | FAST); + let b = builder.add_text_field("b", STRING | STORED | FAST); + + let schema = builder.build(); + let index = Index::create_in_ram(schema); + let mut index_writer: IndexWriter = index.writer(50_000_000).unwrap(); + + let delete_term1 = Term::from_field_u64(a, 1u64); + let delete_term2 = Term::from_field_u64(a, 2u64); + let delete_term3 = Term::from_field_u64(a, 3u64); + + let operations = vec![ + //UserOperation::Delete(delete_term1), + UserOperation::Add(doc!( + a => 1u64, + b => "test1" + )), + //UserOperation::Delete(delete_term2), + UserOperation::Add(doc!( + a => 2u64, + b => "test1" + )), + //UserOperation::Delete(delete_term3), + UserOperation::Add(doc!( + a => 3u64, + b => "test1" + )), + ]; + + index_writer.run(operations).unwrap(); + index_writer.commit().unwrap(); + + let reader = index.reader().unwrap(); + + let searcher = reader.searcher(); + + let tq = TermQuery::new(Term::from_field_u64(a, 3), IndexRecordOption::Basic); + + let docs = searcher.search(&tq, &TopDocs::with_limit(1)).unwrap(); + + if let Some((_, doc_address)) = docs.first() { + let old_doc: TantivyDocument = searcher.doc_async(*doc_address).await.unwrap(); + + let mut new_doc = TantivyDocument::new(); + for (field, value) in old_doc.field_values() { + if field == a { + new_doc.add_field_value(a, value); + } + } + new_doc.add_text(b, "test2"); + + let delete_term = Term::from_field_u64(a, 3); + index_writer.delete_term(delete_term); + index_writer.commit().unwrap(); + index_writer.add_document(new_doc).unwrap(); + index_writer.commit().unwrap(); + } + + reader.reload().unwrap(); + let searcher = reader.searcher(); + let docs = searcher.search(&tq, &TopDocs::with_limit(1)).unwrap(); + + if let Some((_, doc_address)) = docs.first() { + let doc: TantivyDocument = searcher.doc_async(*doc_address).await.unwrap(); + for (field, value) in doc.field_values() { + if field == b { + let value = value.as_str(); + println!("{:#?}", value); + } + } + } else { + println!("not found") + } + + let delete_term = Term::from_field_u64(a, 3); + index_writer.delete_term(delete_term); + index_writer.commit().unwrap(); + + reader.reload().unwrap(); + let searcher = reader.searcher(); + let docs = searcher.search(&tq, &TopDocs::with_limit(1)).unwrap(); + + if let Some((_, doc_address)) = docs.first() { + let doc: TantivyDocument = searcher.doc_async(*doc_address).await.unwrap(); + for (field, value) in doc.field_values() { + if field == b { + let value = value.as_str(); + println!("{:#?}", value); + } + } + } else { + println!("not found") + } +} diff --git a/src/modules/logger/file.rs b/src/modules/logger/file.rs new file mode 100644 index 0000000..90f11ab --- /dev/null +++ b/src/modules/logger/file.rs @@ -0,0 +1,65 @@ +// +// Copyright (c) 2025 rustmailer.com (https://rustmailer.com) +// +// This file is part of the Bichon Email Archiving Project +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +use crate::modules::logger::{validate_log_level, LocalTimer}; +use crate::modules::settings::cli::SETTINGS; +use crate::modules::settings::dir::DATA_DIR_MANAGER; +use std::sync::OnceLock; +use tracing::level_filters::LevelFilter; +use tracing::Level; +use tracing_appender::non_blocking::{NonBlocking, WorkerGuard}; +use tracing_appender::rolling::{RollingFileAppender, Rotation}; +use tracing_subscriber::fmt; +use tracing_subscriber::layer::SubscriberExt; + +pub static LOG_WORKER_GUARD: OnceLock> = OnceLock::new(); + +pub fn setup_file_logger() -> Result<(), tracing::dispatcher::SetGlobalDefaultError> { + validate_log_level(&SETTINGS.bichon_log_level); + let level = SETTINGS.bichon_log_level.parse::().unwrap(); + let with_ansi = SETTINGS.bichon_ansi_logs; + + let (server_nonb, server_guard) = server_log_writer(); + LOG_WORKER_GUARD.set(vec![server_guard]).unwrap(); + + let server_layer = fmt::layer() + .with_timer(LocalTimer) + .with_ansi(with_ansi) + .with_level(true) + .with_writer(server_nonb) + .with_target(true); + + let subscriber = tracing_subscriber::registry() + .with(LevelFilter::from_level(level)) + .with(server_layer); + + // Set the combined subscriber as the global default + tracing::subscriber::set_global_default(subscriber) +} + +fn server_log_writer() -> (NonBlocking, WorkerGuard) { + let rolling = RollingFileAppender::builder() + .rotation(Rotation::DAILY) + .filename_prefix("server") + .max_log_files(SETTINGS.bichon_max_server_log_files) + .build(DATA_DIR_MANAGER.log_dir.clone()) + .expect("failed to initialize rolling file appender"); + let (nb, wg) = tracing_appender::non_blocking(rolling); + (nb, wg) +} diff --git a/src/modules/logger/mod.rs b/src/modules/logger/mod.rs new file mode 100644 index 0000000..eeeadea --- /dev/null +++ b/src/modules/logger/mod.rs @@ -0,0 +1,74 @@ +// +// Copyright (c) 2025 rustmailer.com (https://rustmailer.com) +// +// This file is part of the Bichon Email Archiving Project +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +use crate::logger::file::setup_file_logger; +use crate::modules::settings::cli::SETTINGS; +use chrono::Local; +use std::process; +use tracing::Level; +use tracing_subscriber::fmt::{format::Writer, time::FormatTime}; + +mod file; + +struct LocalTimer; + +impl FormatTime for LocalTimer { + fn format_time(&self, w: &mut Writer<'_>) -> std::fmt::Result { + write!(w, "{}", Local::now().format("%Y-%m-%dT%H:%M:%S%.3f%:z")) + } +} + +pub fn initialize_logging() { + if SETTINGS.bichon_log_to_file { + setup_file_logger().unwrap(); + } else { + setup_stdout_logger().unwrap(); + } +} + +fn setup_stdout_logger() -> Result<(), tracing::dispatcher::SetGlobalDefaultError> { + validate_log_level(&SETTINGS.bichon_log_level); + let level = SETTINGS.bichon_log_level.parse::().unwrap(); + let with_ansi = SETTINGS.bichon_ansi_logs; + + let format = tracing_subscriber::fmt::format() + .with_level(true) + .with_target(true) + .with_timer(LocalTimer); + + let subscriber = tracing_subscriber::fmt() + .with_max_level(level) + .with_ansi(with_ansi) + .with_writer(std::io::stdout) + .event_format(format) + .finish(); + + tracing::subscriber::set_global_default(subscriber) +} + +fn validate_log_level(value: &String) { + if value.parse::().is_err() { + eprintln!( + "Invalid log level specified. Use one of: error, warn, info, debug, trace. + The log level you currently specified is 'rustmailer_log_level'='{}'", + value + ); + process::exit(1); + } +} diff --git a/src/modules/mailbox/list.rs b/src/modules/mailbox/list.rs new file mode 100644 index 0000000..07ba0ff --- /dev/null +++ b/src/modules/mailbox/list.rs @@ -0,0 +1,99 @@ +// +// Copyright (c) 2025 rustmailer.com (https://rustmailer.com) +// +// This file is part of the Bichon Email Archiving Project +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +use crate::modules::account::migration::{AccountModel, AccountType}; +use crate::modules::cache::imap::mailbox::{Attribute, AttributeEnum, MailBox}; +use crate::modules::context::executors::MAIL_CONTEXT; +use crate::modules::error::code::ErrorCode; +use crate::modules::error::{BichonError, BichonResult}; +use crate::modules::utils::create_hash; +use crate::raise_error; +use async_imap::types::Name; + +pub async fn get_account_mailboxes(account_id: u64, remote: bool) -> BichonResult> { + let account = AccountModel::check_account_active(account_id).await?; + if remote { + if matches!(account.account_type, AccountType::IMAP) { + request_imap_all_mailbox_list(account_id).await + } else { + return Err(raise_error!( + "The 'remote' option can only be used with IMAP accounts.".into(), + ErrorCode::InvalidParameter + )); + } + } else { + MailBox::list_all(account_id).await + } +} + +pub async fn request_imap_all_mailbox_list(account_id: u64) -> BichonResult> { + let executor = MAIL_CONTEXT.imap(account_id).await?; + let names = executor.list_all_mailboxes().await?; + convert_names_to_mailboxes(account_id, names.iter()).await +} + +fn contains_no_select(attributes: &[Attribute]) -> bool { + attributes + .iter() + .any(|attr| attr.attr == AttributeEnum::NoSelect) +} + +pub async fn convert_names_to_mailboxes( + account_id: u64, + names: impl IntoIterator, +) -> BichonResult> { + // Preallocate enough space in the vector to avoid multiple reallocations + let mut tasks = Vec::new(); + + for name in names.into_iter() { + // Convert the name into a MailBox structure + let mailbox_name = name.name().to_string(); + let mut mailbox: MailBox = name.into(); + + if contains_no_select(&mailbox.attributes) { + continue; + } + mailbox.account_id = account_id; + mailbox.id = create_hash(account_id, &mailbox.name); + let task: tokio::task::JoinHandle> = + tokio::spawn(async move { + let executor = MAIL_CONTEXT.imap(account_id).await?; + let mx = executor.examine_mailbox(mailbox_name.as_str()).await?; + // Update the mailbox status information + mailbox.exists = mx.exists; // Number of messages in the mailbox + mailbox.unseen = mx.unseen; // Number of unseen messages + mailbox.uid_next = mx.uid_next; // Next unique identifier to be assigned + mailbox.uid_validity = mx.uid_validity; // Validity of the UIDs + Ok(mailbox) + }); + tasks.push(task); + } + + let mut mailboxes = Vec::new(); + + for task in tasks { + match task.await { + Ok(Ok(mailbox)) => mailboxes.push(mailbox), + Ok(Err(err)) => return Err(err), // Handle mailbox-level errors + Err(e) => return Err(raise_error!(format!("{:#?}", e), ErrorCode::InternalError)), // Handle task-level panics or errors + } + } + + Ok(mailboxes) +} diff --git a/src/modules/mailbox/mod.rs b/src/modules/mailbox/mod.rs new file mode 100644 index 0000000..a468f64 --- /dev/null +++ b/src/modules/mailbox/mod.rs @@ -0,0 +1,20 @@ +// +// Copyright (c) 2025 rustmailer.com (https://rustmailer.com) +// +// This file is part of the Bichon Email Archiving Project +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +pub mod list; diff --git a/src/modules/message/content.rs b/src/modules/message/content.rs new file mode 100644 index 0000000..51c05b5 --- /dev/null +++ b/src/modules/message/content.rs @@ -0,0 +1,147 @@ +// +// Copyright (c) 2025 rustmailer.com (https://rustmailer.com) +// +// This file is part of the Bichon Email Archiving Project +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +use crate::base64_encode; +use crate::modules::account::migration::AccountModel; +use crate::modules::error::code::ErrorCode; +use crate::modules::indexer::manager::EML_INDEX_MANAGER; +use crate::{modules::error::BichonResult, raise_error}; +use mail_parser::{MessageParser, MimeHeaders}; + +use poem_openapi::Object; +use serde::{Deserialize, Serialize}; + +/// Represents metadata of an attachment in a Gmail message. +/// +/// This struct stores information required to identify, download, +/// and render an attachment, including inline images embedded +/// in HTML emails. +#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize, Object)] +pub struct AttachmentInfo { + /// MIME content type of the attachment (e.g., `image/png`, `application/pdf`). + pub file_type: String, + /// Whether the attachment is marked as inline (true) or a regular file (false). + pub inline: bool, + /// Original filename of the attachment, if provided. + pub filename: String, + /// Size of the attachment in bytes. + pub size: usize, + pub content_id: Option, +} + +/// Represents the content of an email message in both plain text and HTML formats. +/// +/// This struct contains optional fields for plain text and HTML versions of +/// the email message body. At least one of them may be present. +/// +/// # Fields +/// +/// - `plain`: The plain text version of the message, if available. +/// - `html`: The HTML version of the message, if available. +#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize, Object)] +pub struct FullMessageContent { + /// Optional plain text version of the message. + pub text: Option, + /// Optional HTML version of the message. + pub html: Option, + // all Attachments include inline attachments + pub attachments: Option>, +} + +pub async fn retrieve_email_content( + account_id: u64, + id: u64, +) -> BichonResult { + AccountModel::check_account_active(account_id).await?; + let eml = EML_INDEX_MANAGER + .get(account_id, id) + .await? + .ok_or_else(|| { + raise_error!( + format!( + "Email record not found: account_id={} id={}", + account_id, id + ), + ErrorCode::ResourceNotFound + ) + })?; + let message = MessageParser::default().parse(&eml).ok_or_else(|| { + raise_error!( + format!( + "Failed to parse EML data (id={}) — the message may be corrupted.", + id + ), + ErrorCode::InternalError + ) + })?; + let mut html: Option = message.body_html(0).map(|cow| cow.into_owned()); + let text: Option = message.body_text(0).map(|cow| cow.into_owned()); + let mut attachments = Vec::new(); + for attachment in message.attachments() { + let content_type = attachment.content_type().ok_or_else(|| { + raise_error!( + format!("Attachment is missing Content-Type (email id={})", id), + ErrorCode::InternalError + ) + })?; + let filename = attachment + .attachment_name() + .map(|name| name.to_string()) + .unwrap_or_else(|| format!("email{}_attachment{}", id, attachment.raw_body_offset())); + + let disposition = attachment.content_disposition(); + + let file_type = format!( + "{}/{}", + content_type.c_type.as_ref(), + content_type.c_subtype.as_deref().unwrap_or("") + ); + + let inline = disposition.map(|d| d.is_inline()).unwrap_or(false); + + if inline { + if let Some(html1) = html.as_deref() { + if let Some(cid) = attachment.content_id() { + if html1.contains(cid) { + let data = attachment.contents(); + let base64_encoded = base64_encode!(data); + let html_content = html1.replace( + &format!("cid:{}", cid), + &format!("data:{};base64,{}", file_type, base64_encoded), + ); + html = Some(html_content); + } + } + } + } + + attachments.push(AttachmentInfo { + filename, + size: attachment.len(), + inline, + file_type, + content_id: attachment.content_id().map(Into::into), + }); + } + Ok(FullMessageContent { + text, + html, + attachments: Some(attachments), + }) +} diff --git a/src/modules/message/delete.rs b/src/modules/message/delete.rs new file mode 100644 index 0000000..c390c75 --- /dev/null +++ b/src/modules/message/delete.rs @@ -0,0 +1,31 @@ +// +// Copyright (c) 2025 rustmailer.com (https://rustmailer.com) +// +// This file is part of the Bichon Email Archiving Project +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +use crate::modules::error::BichonResult; +use crate::modules::indexer::manager::{EML_INDEX_MANAGER, ENVELOPE_INDEX_MANAGER}; +use std::collections::HashMap; + +pub async fn delete_messages_impl(request: HashMap>) -> BichonResult<()> { + EML_INDEX_MANAGER + .delete_email_multi_account(&request) + .await?; + ENVELOPE_INDEX_MANAGER + .delete_envelopes_multi_account(&request) + .await +} diff --git a/src/modules/message/list.rs b/src/modules/message/list.rs new file mode 100644 index 0000000..457a893 --- /dev/null +++ b/src/modules/message/list.rs @@ -0,0 +1,69 @@ +// +// Copyright (c) 2025 rustmailer.com (https://rustmailer.com) +// +// This file is part of the Bichon Email Archiving Project +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +use crate::{ + modules::{ + account::migration::AccountModel, + error::{code::ErrorCode, BichonResult}, + indexer::{envelope::Envelope, manager::ENVELOPE_INDEX_MANAGER}, + rest::response::DataPage, + }, + raise_error, +}; + +pub async fn list_messages_impl( + account_id: u64, + mailbox_id: u64, + page: u64, + page_size: u64, +) -> BichonResult> { + AccountModel::check_account_active(account_id).await?; + validate_pagination_params(page, page_size)?; + ENVELOPE_INDEX_MANAGER + .list_mailbox_envelopes(account_id, mailbox_id, page, page_size, true) + .await +} + +fn validate_pagination_params(page: u64, page_size: u64) -> BichonResult<()> { + if page == 0 || page_size == 0 { + return Err(raise_error!( + "Both page and page_size must be greater than 0.".into(), + ErrorCode::InvalidParameter + )); + } + if page_size > 500 { + return Err(raise_error!( + "The page_size exceeds the maximum allowed limit of 500.".into(), + ErrorCode::InvalidParameter + )); + } + Ok(()) +} + +pub async fn get_thread_messages( + account_id: u64, + thread_id: u64, + page: u64, + page_size: u64, +) -> BichonResult> { + AccountModel::check_account_active(account_id).await?; + ENVELOPE_INDEX_MANAGER + .list_thread_envelopes(account_id, thread_id, page, page_size, true) + .await +} diff --git a/src/modules/message/mod.rs b/src/modules/message/mod.rs new file mode 100644 index 0000000..4ff2941 --- /dev/null +++ b/src/modules/message/mod.rs @@ -0,0 +1,24 @@ +// +// Copyright (c) 2025 rustmailer.com (https://rustmailer.com) +// +// This file is part of the Bichon Email Archiving Project +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +pub mod content; +pub mod delete; +pub mod list; +pub mod search; +pub mod tags; diff --git a/src/modules/message/search.rs b/src/modules/message/search.rs new file mode 100644 index 0000000..c762eec --- /dev/null +++ b/src/modules/message/search.rs @@ -0,0 +1,80 @@ +// +// Copyright (c) 2025 rustmailer.com (https://rustmailer.com) +// +// This file is part of the Bichon Email Archiving Project +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +use poem_openapi::Object; +use serde::{Deserialize, Serialize}; + +use crate::{ + modules::{ + error::{code::ErrorCode, BichonResult}, + indexer::{envelope::Envelope, manager::ENVELOPE_INDEX_MANAGER}, + rest::response::DataPage, + }, + raise_error, +}; + +#[derive(Debug, Clone, Default, Eq, PartialEq, Serialize, Deserialize, Object)] +pub struct SearchFilter { + pub text: Option, + pub from: Option, + pub to: Option, + pub cc: Option, + pub bcc: Option, + pub since: Option, + pub before: Option, + pub account_id: Option, + pub mailbox_id: Option, + pub min_size: Option, + pub max_size: Option, + pub message_id: Option, + pub has_attachment: Option, + pub attachment_name: Option, + pub tags: Option>, +} + +#[derive(Debug, Clone, Default, Eq, PartialEq, Serialize, Deserialize, Object)] +pub struct SearchRequest { + filter: SearchFilter, + page: u64, + page_size: u64, +} +impl SearchRequest { + pub fn validate(&self) -> BichonResult<()> { + if self.page == 0 || self.page_size == 0 { + return Err(raise_error!( + "Both page and page_size must be greater than 0.".into(), + ErrorCode::InvalidParameter + )); + } + if self.page_size > 500 { + return Err(raise_error!( + "The page_size exceeds the maximum allowed limit of 500.".into(), + ErrorCode::InvalidParameter + )); + } + Ok(()) + } +} + +pub async fn search_messages_impl(request: SearchRequest) -> BichonResult> { + request.validate()?; + ENVELOPE_INDEX_MANAGER + .search(request.filter, request.page, request.page_size, true) + .await +} diff --git a/src/modules/message/tags.rs b/src/modules/message/tags.rs new file mode 100644 index 0000000..ac02943 --- /dev/null +++ b/src/modules/message/tags.rs @@ -0,0 +1,35 @@ +// +// Copyright (c) 2025 rustmailer.com (https://rustmailer.com) +// +// This file is part of the Bichon Email Archiving Project +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +use std::collections::HashMap; + +use poem_openapi::Object; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Default, Eq, PartialEq, Serialize, Deserialize, Object)] +pub struct UpdateTagsRequest { + pub updates: HashMap>, // account_id -> envelope_ids + pub tags: Vec, +} + +#[derive(Debug, Clone, Default, Eq, PartialEq, Serialize, Deserialize, Object)] +pub struct TagCount { + pub tag: String, + pub count: u64, +} diff --git a/src/modules/mod.rs b/src/modules/mod.rs new file mode 100644 index 0000000..f3fbf0c --- /dev/null +++ b/src/modules/mod.rs @@ -0,0 +1,40 @@ +// +// Copyright (c) 2025 rustmailer.com (https://rustmailer.com) +// +// This file is part of the Bichon Email Archiving Project +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +pub mod account; +pub mod autoconfig; +pub mod cache; +pub mod common; +pub mod context; +pub mod dashboard; +pub mod database; +pub mod envelope; +pub mod error; +pub mod imap; +pub mod indexer; +pub mod logger; +pub mod mailbox; +pub mod message; +pub mod oauth2; +pub mod rest; +pub mod settings; +pub mod tasks; +pub mod token; +pub mod utils; +pub mod version; diff --git a/src/modules/oauth2/entity.rs b/src/modules/oauth2/entity.rs new file mode 100644 index 0000000..bf23663 --- /dev/null +++ b/src/modules/oauth2/entity.rs @@ -0,0 +1,275 @@ +// +// Copyright (c) 2025 rustmailer.com (https://rustmailer.com) +// +// This file is part of the Bichon Email Archiving Project +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +use crate::{ + encrypt, id, + modules::{ + database::{ + delete_impl, insert_impl, manager::DB_MANAGER, paginate_query_primary_scan_all_impl, + secondary_find_impl, update_impl, + }, + error::{code::ErrorCode, BichonResult}, + rest::response::DataPage, + }, + raise_error, utc_now, +}; +use native_db::*; +use native_model::{native_model, Model}; +use poem_openapi::Object; +use serde::{Deserialize, Serialize}; +use std::collections::BTreeMap; + +/// Represents the OAuth2 configuration for a client, including initialization and runtime values. +#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize, Object)] +#[native_model(id = 5, version = 1)] +#[native_db(primary_key(pk -> String))] +pub struct OAuth2 { + /// A unique identifier for the OAuth2 configuration. + #[secondary_key(unique)] + pub id: u64, + /// A description of what this configuration is used for. + pub description: Option, + /// The client ID used for authenticating the application with the OAuth2 provider. + pub client_id: String, + /// The client secret used in conjunction with the client ID. + /// + /// Users should provide a plaintext secret. + /// The server will encrypt it using AES-256-GCM and securely store it. + /// The plaintext secret is never stored, so users must ensure it is valid for OAuth2 authentication. + pub client_secret: String, + /// The URL to redirect users to for OAuth2 authorization. + pub auth_url: String, + /// The URL to exchange authorization codes for access tokens. + pub token_url: String, + /// The URI where the OAuth2 provider will redirect to after authorization. + pub redirect_uri: String, + /// The scopes of access that are being requested (e.g., email, profile). + pub scopes: Option>, + /// Any additional parameters to include in the OAuth2 requests (e.g., access_type, prompt). + pub extra_params: Option>, + /// Indicates whether this configuration is enabled or disabled. + pub enabled: bool, + /// route OAuth through proxy (when direct access is blocked) + pub use_proxy: Option, + /// The timestamp when the configuration was created, in milliseconds since the Unix epoch. + pub created_at: i64, + /// The timestamp when the configuration was last updated, in milliseconds since the Unix epoch. + pub updated_at: i64, +} + +impl OAuth2 { + fn pk(&self) -> String { + format!("{}_{}", &self.created_at, &self.id) + } + + pub fn new(request: OAuth2CreateRequest) -> BichonResult { + let request = request.encrypt()?; + Ok(OAuth2 { + id: id!(64), + description: request.description, + client_id: request.client_id, + client_secret: request.client_secret, + auth_url: request.auth_url, + token_url: request.token_url, + redirect_uri: request.redirect_uri, + scopes: request.scopes, + extra_params: request.extra_params, + enabled: request.enabled, + created_at: utc_now!(), + updated_at: utc_now!(), + use_proxy: request.use_proxy, + }) + } + + pub async fn save(&self) -> BichonResult<()> { + insert_impl(DB_MANAGER.meta_db(), self.to_owned()).await?; + Ok(()) + } + + pub async fn paginate_list( + page: Option, + page_size: Option, + desc: Option, + ) -> BichonResult> { + paginate_query_primary_scan_all_impl(DB_MANAGER.meta_db(), page, page_size, desc) + .await + .map(DataPage::from) + } + + pub async fn get(id: u64) -> BichonResult> { + secondary_find_impl(DB_MANAGER.meta_db(), OAuth2Key::id, id).await + } + + pub async fn delete(id: u64) -> BichonResult<()> { + delete_impl(DB_MANAGER.meta_db(), move |rw| { + rw.get() + .secondary::(OAuth2Key::id, id) + .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))? + .ok_or_else(|| { + raise_error!( + format!( + "The oauth2 entity with id={id} that you want to delete was not found." + ), + ErrorCode::ResourceNotFound + ) + }) + }) + .await + } + + pub async fn update(id: u64, request: OAuth2UpdateRequest) -> BichonResult<()> { + update_impl( + DB_MANAGER.meta_db(), + move |rw| { + rw.get() + .secondary::(OAuth2Key::id, id) + .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))? + .ok_or_else(|| { + raise_error!( + format!("The oauth2 entity with id={id} that you want to modify was not found."), + ErrorCode::ResourceNotFound + ) + }) + }, + |current| apply_update(current, request), + ) + .await?; + + Ok(()) + } +} + +#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize, Object)] +pub struct OAuth2CreateRequest { + /// A description of what this configuration is used for. + pub description: Option, + + /// The client ID used for authenticating the application with the OAuth2 provider. + pub client_id: String, + + /// The client secret used in conjunction with the client ID. + pub client_secret: String, + + /// The URL to redirect users to for OAuth2 authorization. + pub auth_url: String, + + /// The URL to exchange authorization codes for access tokens. + pub token_url: String, + + /// The URI where the OAuth2 provider will redirect to after authorization. + pub redirect_uri: String, + + /// The scopes of access that are being requested (e.g., email, profile). + pub scopes: Option>, + + /// Any additional parameters to include in the OAuth2 requests (e.g., access_type, prompt). + pub extra_params: Option>, + + /// Indicates whether this configuration is enabled or disabled. + pub enabled: bool, + + /// route OAuth through proxy (when direct access is blocked) + pub use_proxy: Option, +} + +impl OAuth2CreateRequest { + pub fn encrypt(self) -> BichonResult { + Ok(Self { + description: self.description, + client_id: self.client_id, + client_secret: encrypt!(&self.client_secret)?, + auth_url: self.auth_url, + token_url: self.token_url, + redirect_uri: self.redirect_uri, + scopes: self.scopes, + extra_params: self.extra_params, + enabled: self.enabled, + use_proxy: self.use_proxy, + }) + } +} + +#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize, Object)] +pub struct OAuth2UpdateRequest { + /// A description of what this configuration is used for. + pub description: Option, + + /// The client ID used for authenticating the application with the OAuth2 provider. + pub client_id: Option, + + /// The client secret used in conjunction with the client ID. + pub client_secret: Option, + + /// The URL to redirect users to for OAuth2 authorization. + pub auth_url: Option, + + /// The URL to exchange authorization codes for access tokens. + pub token_url: Option, + + /// The URI where the OAuth2 provider will redirect to after authorization. + pub redirect_uri: Option, + + /// The scopes of access that are being requested (e.g., email, profile). + pub scopes: Option>, + + /// Any additional parameters to include in the OAuth2 requests (e.g., access_type, prompt). + pub extra_params: Option>, + + /// Indicates whether this configuration is enabled or disabled. + pub enabled: Option, + + /// route OAuth through proxy (when direct access is blocked) + pub use_proxy: Option, +} + +fn apply_update(old: &OAuth2, request: OAuth2UpdateRequest) -> BichonResult { + let mut new = old.clone(); + if request.description.is_some() { + new.description = request.description; + } + if let Some(client_id) = request.client_id { + new.client_id = client_id; + } + if let Some(client_secret) = request.client_secret { + new.client_secret = encrypt!(&client_secret)?; + } + if let Some(auth_url) = request.auth_url { + new.auth_url = auth_url; + } + if let Some(token_url) = request.token_url { + new.token_url = token_url; + } + if let Some(redirect_uri) = request.redirect_uri { + new.redirect_uri = redirect_uri; + } + if request.scopes.is_some() { + new.scopes = request.scopes; + } + if request.extra_params.is_some() { + new.extra_params = request.extra_params; + } + if let Some(enabled) = request.enabled { + new.enabled = enabled; + } + if let Some(use_proxy) = request.use_proxy { + new.use_proxy = Some(use_proxy); + } + new.updated_at = utc_now!(); + Ok(new) +} diff --git a/src/modules/oauth2/flow.rs b/src/modules/oauth2/flow.rs new file mode 100644 index 0000000..cf35eba --- /dev/null +++ b/src/modules/oauth2/flow.rs @@ -0,0 +1,295 @@ +// +// Copyright (c) 2025 rustmailer.com (https://rustmailer.com) +// +// This file is part of the Bichon Email Archiving Project +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +use crate::modules::error::code::ErrorCode; +use crate::modules::error::BichonResult; +use crate::modules::oauth2::{ + entity::OAuth2, pending::OAuth2PendingEntity, token::OAuth2AccessToken, +}; +use crate::modules::settings::proxy::Proxy; +use crate::{decrypt, encrypt, raise_error}; +use oauth2::{ + basic::BasicClient, AuthUrl, AuthorizationCode, ClientId, ClientSecret, CsrfToken, + PkceCodeChallenge, PkceCodeVerifier, RedirectUrl, RefreshToken, Scope, TokenResponse, TokenUrl, +}; +use poem_openapi::Object; +use serde::{Deserialize, Serialize}; + +pub type OAuth2Client = oauth2::Client< + oauth2::StandardErrorResponse, + oauth2::StandardTokenResponse, + oauth2::StandardTokenIntrospectionResponse< + oauth2::EmptyExtraTokenFields, + oauth2::basic::BasicTokenType, + >, + oauth2::StandardRevocableToken, + oauth2::StandardErrorResponse, + oauth2::EndpointSet, + oauth2::EndpointNotSet, + oauth2::EndpointNotSet, + oauth2::EndpointNotSet, + oauth2::EndpointSet, +>; + +#[derive(Debug, Clone, Default, Eq, PartialEq, Serialize, Deserialize, Object)] +pub struct AuthorizeUrlRequest { + /// The ID of the account for which the authorization URL is generated. + pub account_id: u64, + /// The name of the OAuth2 configuration to use for generating the authorization URL. + pub oauth2_id: u64, +} + +pub struct OAuth2Flow { + pub oauth2_id: u64, +} + +impl OAuth2Flow { + pub fn new(oauth2_id: u64) -> Self { + Self { oauth2_id } + } + + pub async fn authorize_url(&self, account_id: u64) -> BichonResult { + // Fetch OAuth2 entity or return a custom error if not found + let entity = self.fetch_oauth2_entity().await?; + + if !entity.enabled { + return Err(raise_error!( + format!( + "OAuth2 authentication is disabled for this client '{}'.", + self.oauth2_id + ), + ErrorCode::OAuth2ItemDisabled + )); + } + // Create and configure the OAuth2 client + let client = self.build_oauth2_client(&entity)?; + // Generate PKCE challenge and verifier + let (pkce_code_challenge, pkce_code_verifier) = PkceCodeChallenge::new_random_sha256(); + // Build the authorization URL request + + let mut request = client + .authorize_url(CsrfToken::new_random) + .set_pkce_challenge(pkce_code_challenge) + .add_scopes( + entity + .scopes + .unwrap_or(Vec::new()) + .into_iter() + .map(Scope::new), + ); + // Add extra parameters + if let Some(extra_params) = &entity.extra_params { + for (name, value) in extra_params { + request = request.add_extra_param(name.clone(), value.clone()); + } + } + // Extract authorization URL and CSRF state + let (authorize_url, csrf_state) = request.url(); + // Save the pending OAuth2 state + self.save_pending_oauth2_state( + account_id, + csrf_state.secret(), + pkce_code_verifier.secret(), + ) + .await?; + // Return the authorization URL + Ok(authorize_url.to_string()) + } + + pub async fn fetch_save_access_token( + &self, + account_id: u64, + code_verifier: &str, + code: &str, + ) -> BichonResult<()> { + let entity = self.fetch_oauth2_entity().await?; + let client = self.build_oauth2_client(&entity)?; + let http_client = build_http_client(entity.use_proxy).await?; + + let token_response = client + .exchange_code(AuthorizationCode::new(code.to_owned())) + .set_pkce_verifier(PkceCodeVerifier::new(code_verifier.to_owned())) + .request_async(&http_client) + .await + .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::HttpResponseError))?; + + let access_token = token_response.access_token().secret().to_owned(); + let refresh_token = token_response + .refresh_token() + .ok_or_else(|| { + raise_error!( + "Missing refresh token in the token response.".into(), + ErrorCode::MissingRefreshToken + ) + })? + .secret() + .to_owned(); + + self.save_oauth2_entity(account_id, access_token, refresh_token) + .await?; + + Ok(()) + } + + async fn save_oauth2_entity( + &self, + account_id: u64, + access_token: String, + refresh_token: String, + ) -> BichonResult<()> { + let token = + OAuth2AccessToken::create(account_id, self.oauth2_id, access_token, refresh_token)?; + token.save_or_update().await + } + + async fn update_oauth2_entity( + &self, + account_id: u64, + access_token: String, + refresh_token: String, + ) -> BichonResult<()> { + OAuth2AccessToken::set_access_token( + account_id, + encrypt!(&access_token)?, + encrypt!(&refresh_token)?, + ) + .await + } + + pub async fn refresh_access_token(&self, token: &OAuth2AccessToken) -> BichonResult<()> { + let entity = self.fetch_oauth2_entity().await?; + if !entity.enabled { + OAuth2AccessToken::delete_by_oauth2_id(token.oauth2_id).await?; + return Err(raise_error!( + "OAuth2 authentication is disabled for this client".into(), + ErrorCode::OAuth2ItemDisabled + )); + } + let client = self.build_oauth2_client(&entity)?; + let http_client = build_http_client(entity.use_proxy).await?; + + let refresh_token = token.refresh_token.clone().ok_or_else(|| { + raise_error!( + "refresh token is null".into(), + ErrorCode::MissingRefreshToken + ) + })?; + + let refresh_response = client + .exchange_refresh_token(&RefreshToken::new(refresh_token.clone())) + .add_scopes( + entity + .scopes + .unwrap_or(Vec::new()) + .into_iter() + .map(Scope::new), + ) + .request_async(&http_client) + .await + .map_err(|e| { + raise_error!( + format!( + "Failed to retrieve refresh token response: {}", + e.to_string() + ), + ErrorCode::HttpResponseError + ) + })?; + + let access_token = refresh_response.access_token().secret().to_owned(); + let new_refresh_token = refresh_response + .refresh_token() + .map(|r| r.secret().to_owned()) + .unwrap_or_else(|| refresh_token.clone()); + self.update_oauth2_entity(token.account_id, access_token, new_refresh_token) + .await?; + + Ok(()) + } + + // Helper function to fetch the OAuth2 entity + async fn fetch_oauth2_entity(&self) -> BichonResult { + OAuth2::get(self.oauth2_id).await?.ok_or_else(|| { + raise_error!( + format!("OAuth2 entity with id '{}' not found", self.oauth2_id), + ErrorCode::ResourceNotFound + ) + }) + } + + // Helper function to build the OAuth2 client + fn build_oauth2_client(&self, entity: &OAuth2) -> BichonResult { + let auth_url = AuthUrl::new(entity.auth_url.clone()) + .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InvalidParameter))?; + let token_url = TokenUrl::new(entity.token_url.clone()) + .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InvalidParameter))?; + let redirect_uri = RedirectUrl::new(entity.redirect_uri.clone()) + .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InvalidParameter))?; + + // Create and return the OAuth2 client + let client = BasicClient::new(ClientId::new(entity.client_id.clone())) + .set_client_secret(ClientSecret::new(decrypt!(&entity.client_secret)?)) + .set_auth_uri(auth_url) + .set_token_uri(token_url) + .set_redirect_uri(redirect_uri); + + Ok(client) + } + + // Helper function to save the pending OAuth2 state + async fn save_pending_oauth2_state( + &self, + account_id: u64, + csrf_state: &str, + pkce_code_verifier: &str, + ) -> BichonResult<()> { + OAuth2PendingEntity::new( + self.oauth2_id, + account_id, + csrf_state.to_owned(), + pkce_code_verifier.to_owned(), + ) + .save() + .await + } +} + +// Helper function to build the HTTP client +async fn build_http_client(use_proxy: Option) -> BichonResult { + if let Some(proxy_id) = use_proxy { + let proxy = Proxy::get(proxy_id).await?; + return oauth2::reqwest::ClientBuilder::new() + .redirect(oauth2::reqwest::redirect::Policy::none()) + .proxy(reqwest::Proxy::all(&proxy.url).map_err(|e| { + raise_error!( + format!( + "Failed to configure SOCKS5 proxy ({}): {:#?}. Please check", + &proxy.url, e + ), + ErrorCode::InternalError + ) + })?) + .build() + .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError)); + } + oauth2::reqwest::ClientBuilder::new() + .redirect(oauth2::reqwest::redirect::Policy::none()) + .build() + .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError)) +} diff --git a/src/modules/oauth2/mod.rs b/src/modules/oauth2/mod.rs new file mode 100644 index 0000000..e295edf --- /dev/null +++ b/src/modules/oauth2/mod.rs @@ -0,0 +1,25 @@ +// +// Copyright (c) 2025 rustmailer.com (https://rustmailer.com) +// +// This file is part of the Bichon Email Archiving Project +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +pub mod entity; +pub mod flow; +pub mod pending; +pub mod refresh; +pub mod task; +pub mod token; diff --git a/src/modules/oauth2/pending.rs b/src/modules/oauth2/pending.rs new file mode 100644 index 0000000..7e85293 --- /dev/null +++ b/src/modules/oauth2/pending.rs @@ -0,0 +1,138 @@ +// +// Copyright (c) 2025 rustmailer.com (https://rustmailer.com) +// +// This file is part of the Bichon Email Archiving Project +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +use crate::{ + modules::{ + database::{ + batch_delete_impl, delete_impl, async_find_impl, insert_impl, manager::DB_MANAGER, + }, + error::{code::ErrorCode, BichonResult}, + }, + raise_error, utc_now, +}; +use itertools::Itertools; +use native_db::*; +use native_model::{native_model, Model}; +use serde::{Deserialize, Serialize}; + +const EXPIRATION_DURATION_MS: i64 = 24 * 60 * 60 * 1000; + +#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)] +#[native_model(id = 6, version = 1)] +#[native_db] +pub struct OAuth2PendingEntity { + /// Unique identifier for the OAuth2 request record + pub oauth2_id: u64, + + pub account_id: u64, + /// CSRF protection state parameter used to verify the integrity of the authorization request + #[primary_key] + pub state: String, + + /// PKCE code verifier used in the authorization code exchange process to ensure security + pub code_verifier: String, + + /// Timestamp when the OAuth2 request was created, used to determine request expiration + pub created_at: i64, +} + +impl OAuth2PendingEntity { + pub fn new( + oauth2_id: u64, + account_id: u64, + state: String, + code_verifier: String, + ) -> Self { + Self { + oauth2_id, + account_id, + state, + code_verifier, + created_at: utc_now!(), + } + } + + pub async fn save(&self) -> BichonResult<()> { + insert_impl(DB_MANAGER.meta_db(), self.to_owned()).await + } + + pub async fn delete(state: &str) -> BichonResult<()> { + let state = state.to_string(); + delete_impl(DB_MANAGER.meta_db(), move |rw| { + rw.get().primary::(state.clone()) + .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))? + .ok_or_else(|| raise_error!(format!( + "The oauth2 pending entity with state={state} that you want to delete was not found." + ), ErrorCode::ResourceNotFound)) + }).await + } + + pub async fn clean() -> BichonResult<()> { + batch_delete_impl(DB_MANAGER.meta_db(), |rw| { + let all: Vec = rw + .scan() + .primary() + .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))? + .all() + .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))? + .try_collect() + .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))?; + + let now = utc_now!(); + let to_delete: Vec = all + .into_iter() + .filter(|e| now - e.created_at > EXPIRATION_DURATION_MS) + .collect(); + Ok(to_delete) + }) + .await?; + Ok(()) + } + + pub async fn get(state: &str) -> BichonResult> { + let entity = + async_find_impl::(DB_MANAGER.meta_db(), state.to_string()) + .await?; + + match entity { + Some(entity) => { + let state = state.to_string(); + if utc_now!() - entity.created_at > EXPIRATION_DURATION_MS { + delete_impl(DB_MANAGER.meta_db(), move |rw| { + rw.get() + .primary::(state) + .map_err(|e| { + raise_error!(format!("{:#?}", e), ErrorCode::InternalError) + })? + .ok_or_else(|| { + raise_error!( + "OAuth2 pending entity not found".into(), + ErrorCode::ResourceNotFound + ) + }) + }) + .await?; + return Ok(None); + } + Ok(Some(entity)) + } + None => Ok(None), + } + } +} diff --git a/src/modules/oauth2/refresh/mod.rs b/src/modules/oauth2/refresh/mod.rs new file mode 100644 index 0000000..57c73a9 --- /dev/null +++ b/src/modules/oauth2/refresh/mod.rs @@ -0,0 +1,91 @@ +// +// Copyright (c) 2025 rustmailer.com (https://rustmailer.com) +// +// This file is part of the Bichon Email Archiving Project +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +use crate::modules::common::periodic::PeriodicTask; +use crate::modules::context::RustMailTask; +use crate::modules::oauth2::token::EXTERNAL_OAUTH_APP_ID; +use crate::modules::oauth2::{flow::OAuth2Flow, token::OAuth2AccessToken}; +use crate::utc_now; +use std::time::Duration; +use tracing::{debug, error, info}; + +const TASK_INTERVAL: Duration = Duration::from_secs(60); // Interval set to 1 minute +const FIFTEEN_MINUTES: Duration = Duration::from_secs(45 * 60); +///This task cleans up expired OAuth2 pending authorizations that haven't been completed by users in a timely manner. +pub struct OAuth2RefreshTask; + +impl RustMailTask for OAuth2RefreshTask { + fn start() { + let periodic_task = PeriodicTask::new("oauth2-token-refresh-task"); + + let task = move |_: Option| { + Box::pin(async move { + debug!("Starting OAuth2 token refresh task"); + + // Try to retrieve all OAuth2 access tokens + match OAuth2AccessToken::list_all().await { + Ok(all_tokens) => { + let need_refresh: Vec = all_tokens + .into_iter() + .filter(|token| { + ((utc_now!() - token.updated_at) + > FIFTEEN_MINUTES.as_millis() as i64) + && token.oauth2_id != EXTERNAL_OAUTH_APP_ID + }) // Filter tokens older than 15 minutes + .collect(); + + if need_refresh.is_empty() { + debug!("No expired tokens need to be refreshed."); + } else { + debug!( + "Found {} tokens that need to be refreshed", + need_refresh.len() + ); + for token in need_refresh { + tokio::spawn(async move { + let flow = OAuth2Flow::new(token.oauth2_id.clone()); + if let Err(error) = flow.refresh_access_token(&token).await { + error!( + "Failed to refresh access token for {}: {}", + token.account_id, error + ); + } else { + info!( + "Successfully refreshed access token for {}", + token.account_id + ); + } + }); + } + } + } + Err(e) => { + // Log the error when retrieving tokens + error!("Failed to fetch OAuth2 tokens: {:?}", e); + } + } + + debug!("OAuth2 token refresh task completed"); + Ok(()) + }) + }; + + periodic_task.start(task, None, TASK_INTERVAL, false, true); + } +} diff --git a/src/modules/oauth2/task.rs b/src/modules/oauth2/task.rs new file mode 100644 index 0000000..ecfbd4e --- /dev/null +++ b/src/modules/oauth2/task.rs @@ -0,0 +1,43 @@ +// +// Copyright (c) 2025 rustmailer.com (https://rustmailer.com) +// +// This file is part of the Bichon Email Archiving Project +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +use crate::modules::{ + common::periodic::PeriodicTask, context::RustMailTask, oauth2::pending::OAuth2PendingEntity, +}; +use std::time::Duration; + +const TASK_INTERVAL: Duration = Duration::from_secs(6 * 60 * 60); + +///This task cleans up expired OAuth2 pending authorizations that haven't been completed by users in a timely manner. +pub struct OAuth2CleanTask; + +impl RustMailTask for OAuth2CleanTask { + fn start() { + let periodic_task = PeriodicTask::new("oauth2-pending-task-cleaner"); + + let task = move |_: Option| { + Box::pin(async move { + OAuth2PendingEntity::clean().await?; + Ok(()) + }) + }; + + periodic_task.start(task, None, TASK_INTERVAL, false, false); + } +} diff --git a/src/modules/oauth2/token.rs b/src/modules/oauth2/token.rs new file mode 100644 index 0000000..b52bdf7 --- /dev/null +++ b/src/modules/oauth2/token.rs @@ -0,0 +1,271 @@ +// +// Copyright (c) 2025 rustmailer.com (https://rustmailer.com) +// +// This file is part of the Bichon Email Archiving Project +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +use crate::{ + decrypt, encrypt, + modules::{ + database::{ + async_find_impl, delete_impl, insert_impl, list_all_impl, manager::DB_MANAGER, + update_impl, upsert_impl, + }, + error::{code::ErrorCode, BichonResult}, + oauth2::entity::OAuth2, + }, + raise_error, utc_now, +}; +use native_db::*; +use native_model::{native_model, Model}; +use poem_openapi::Object; +use serde::{Deserialize, Serialize}; + +pub const EXTERNAL_OAUTH_APP_ID: u64 = 0; + +#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize, Object)] +#[native_model(id = 7, version = 1)] +#[native_db] +pub struct OAuth2AccessToken { + /// The ID of the account associated with this access token. + #[primary_key] + pub account_id: u64, + /// The id of the OAuth2 configuration associated with this access token. + #[secondary_key] + pub oauth2_id: u64, + /// The OAuth2 access token used to authenticate requests to the provider. + pub access_token: Option, + /// The OAuth2 refresh token used to obtain new access tokens. + pub refresh_token: Option, + /// The timestamp when the token record was created, in milliseconds since the Unix epoch. + pub created_at: i64, + /// The timestamp when the token record was last updated, in milliseconds since the Unix epoch. + pub updated_at: i64, +} + +impl OAuth2AccessToken { + pub fn create( + account_id: u64, + oauth2_id: u64, + access_token: String, + refresh_token: String, + ) -> BichonResult { + Ok(Self { + account_id, + oauth2_id, + access_token: Some(encrypt!(&access_token)?), + refresh_token: Some(encrypt!(&refresh_token)?), + created_at: utc_now!(), + updated_at: utc_now!(), + }) + } + + pub async fn upsert_external_oauth_token( + account_id: u64, + request: ExternalOAuth2Request, + ) -> BichonResult<()> { + let now = utc_now!(); + request.validate().await?; + + let current = Self::get(account_id).await?; + match current { + Some(mut current) => { + // Update existing record + if let Some(oauth2_id) = request.oauth2_id { + current.oauth2_id = oauth2_id; + } + if let Some(access_token) = request.access_token { + current.access_token = Some(encrypt!(&access_token)?); + } + if let Some(refresh_token) = request.refresh_token { + current.refresh_token = Some(encrypt!(&refresh_token)?); + } + + current.updated_at = now; + upsert_impl(DB_MANAGER.meta_db(), current).await?; + } + None => { + // Insert new record + let entity = Self { + account_id, + oauth2_id: request.oauth2_id.unwrap_or(EXTERNAL_OAUTH_APP_ID), + access_token: request + .access_token + .as_ref() + .map(|token| encrypt!(token)) + .transpose()?, + refresh_token: request + .refresh_token + .as_ref() + .map(|token| encrypt!(token)) + .transpose()?, + created_at: now, + updated_at: now, + }; + insert_impl(DB_MANAGER.meta_db(), entity).await?; + } + } + Ok(()) + } + + // This function may be called multiple times for one account, so we use upsert. + pub async fn save_or_update(&self) -> BichonResult<()> { + upsert_impl(DB_MANAGER.meta_db(), self.clone()).await + } + + pub async fn get(account_id: u64) -> BichonResult> { + async_find_impl::(DB_MANAGER.meta_db(), account_id) + .await? + .map(|mut token| { + token.access_token = token.access_token.map(|t| decrypt!(&t)).transpose()?; + token.refresh_token = token.refresh_token.map(|t| decrypt!(&t)).transpose()?; + Ok(token) + }) + .transpose() + } + + pub async fn list_all() -> BichonResult> { + list_all_impl::(DB_MANAGER.meta_db()) + .await? + .into_iter() + .map(|mut token| { + token.access_token = token.access_token.map(|t| decrypt!(&t)).transpose()?; + token.refresh_token = token.refresh_token.map(|t| decrypt!(&t)).transpose()?; + Ok(token) + }) + .collect() + } + + pub async fn try_delete(account_id: u64) -> BichonResult<()> { + if Self::get(account_id).await?.is_none() { + return Ok(()); + } + + delete_impl(DB_MANAGER.meta_db(), move |rw|{ + rw.get().primary::(account_id) + .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))? + .ok_or_else(|| raise_error!(format!( + "The oauth2 access token entity with account_id={account_id} that you want to delete was not found." + ),ErrorCode::ResourceNotFound)) + }).await + } + + pub async fn delete_by_oauth2_id(oauth2_id: u64) -> BichonResult<()> { + delete_impl(DB_MANAGER.meta_db(), move |rw|{ + rw.get().secondary::(OAuth2AccessTokenKey::oauth2_id, oauth2_id) + .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))? + .ok_or_else(|| raise_error!(format!( + "The oauth2 access token entity with oauth2_id={oauth2_id} that you want to delete was not found." + ),ErrorCode::ResourceNotFound)) + }).await + } + + pub async fn set_access_token( + account_id: u64, + access_token: String, + refresh_token: String, + ) -> BichonResult<()> { + update_impl(DB_MANAGER.meta_db(), move |rw|{ + rw.get().primary::(account_id) + .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))? + .ok_or_else(|| raise_error!(format!( + "The oauth2 access token entity with account_id={account_id} that you want to modify was not found." + ),ErrorCode::ResourceNotFound)) + }, |current| { + let mut updated = current.clone(); + updated.access_token = Some(access_token); + updated.refresh_token = Some(refresh_token); + updated.updated_at = utc_now!(); + Ok(updated) + }).await?; + Ok(()) + } +} + +#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize, Object)] +pub struct ExternalOAuth2Request { + /// The id of the OAuth2 configuration associated with this access token. + pub oauth2_id: Option, + /// The OAuth2 access token used to authenticate requests to the provider. + pub access_token: Option, + /// The OAuth2 refresh token used to obtain new access tokens. + pub refresh_token: Option, +} + +impl ExternalOAuth2Request { + /// Validates the request. + /// + /// Ensures mutual dependency between oauth2_id and refresh_token: + /// - If `refresh_token` is provided, `oauth2_id` must also be present. + /// - If `oauth2_id` is provided, `refresh_token` must also be present. + pub async fn validate(&self) -> BichonResult<()> { + match (self.oauth2_id.is_some(), self.refresh_token.is_some()) { + (true, false) => { + return Err(raise_error!( + "refresh_token must be provided if oauth2_id is set".into(), + ErrorCode::InvalidParameter + )); + } + (false, true) => { + return Err(raise_error!( + "oauth2_id must be provided if refresh_token is set".into(), + ErrorCode::InvalidParameter + )); + } + _ => {} + } + + // Validate that oauth2_id exists in the database if provided + if let Some(oauth2_id) = self.oauth2_id { + let oauth2 = OAuth2::get(oauth2_id).await?; + if oauth2.is_none() { + return Err(raise_error!( + format!("OAuth2 configuration with id {} does not exist", oauth2_id), + ErrorCode::InvalidParameter + )); + } + } + + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use crate::modules::oauth2::token::OAuth2AccessToken; + + #[tokio::test] + async fn test1() { + let token = OAuth2AccessToken::create( + 1000u64, + 1020u64, + "access_token".into(), + "refresh_token".into(), + ) + .unwrap(); + token.save_or_update().await.unwrap(); + let token2 = OAuth2AccessToken::get(1000u64).await.unwrap().unwrap(); + assert_eq!(token2.access_token, Some("access_token".into())); + assert_eq!(token2.refresh_token, Some("refresh_token".into())); + + let tokens = OAuth2AccessToken::list_all().await.unwrap(); + assert_eq!(tokens.len(), 1); + + let first = tokens.first().unwrap(); + assert_eq!(first.access_token, Some("access_token".into())); + assert_eq!(first.refresh_token, Some("refresh_token".into())); + } +} diff --git a/src/modules/rest/api/access_token.rs b/src/modules/rest/api/access_token.rs new file mode 100644 index 0000000..231a430 --- /dev/null +++ b/src/modules/rest/api/access_token.rs @@ -0,0 +1,156 @@ +// +// Copyright (c) 2025 rustmailer.com (https://rustmailer.com) +// +// This file is part of the Bichon Email Archiving Project +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +use crate::modules::common::auth::ClientContext; +use crate::modules::rest::api::ApiTags; +use crate::modules::rest::ApiResult; +use crate::modules::token::payload::AccessTokenUpdateRequest; +use crate::modules::token::root::set_root_password; +use crate::modules::{ + token::payload::AccessTokenCreateRequest, + token::{root::reset_root_token, AccessToken}, +}; +use poem_openapi::payload::PlainText; +use poem_openapi::{param::Path, payload::Json, OpenApi}; + +pub struct AccessTokenApi; + +#[OpenApi(prefix_path = "/api/v1", tag = "ApiTags::AccessToken")] +impl AccessTokenApi { + /// Lists all access tokens in the system. + /// + /// Requires root privileges. + #[oai( + path = "/access-token-list", + method = "get", + operation_id = "list_access_tokens" + )] + async fn list_access_tokens( + &self, + context: ClientContext, + ) -> ApiResult>> { + context.require_root()?; + Ok(Json(AccessToken::list_all().await?)) + } + + /// Lists access tokens for a specific account. + /// + /// Requires root privileges. + #[oai( + path = "/access-token-list/:account_id", + method = "get", + operation_id = "list_account_access_tokens" + )] + async fn list_account_access_tokens( + &self, + /// The ID of the account whose tokens are to be retrieved. + account_id: Path, + context: ClientContext, + ) -> ApiResult>> { + context.require_root()?; + Ok(Json(AccessToken::list_account_tokens(account_id.0).await?)) + } + /// Deletes a specific access token. + /// + /// Requires root privileges. + #[oai( + path = "/access-token/:token", + method = "delete", + operation_id = "remove_access_token" + )] + async fn remove_access_token( + &self, + /// The access token to be deleted + token: Path, + context: ClientContext, + ) -> ApiResult<()> { + context.require_root()?; + Ok(AccessToken::delete(token.0.trim()).await?) + } + + /// Creates a new access token. + /// + /// Requires root privileges. + #[oai( + path = "/access-token", + method = "post", + operation_id = "create_access_token" + )] + async fn create_access_token( + &self, + context: ClientContext, + /// The request payload + payload: Json, + ) -> ApiResult> { + context.require_root()?; + Ok(PlainText(AccessToken::create(payload.0).await?)) + } + + /// Updates an existing access token. + /// + /// Requires root privileges. + #[oai( + path = "/access-token/:token", + method = "post", + operation_id = "update_access_token" + )] + async fn update_access_token( + &self, + context: ClientContext, + /// The access token to be updated. + token: Path, + /// The request payload. + payload: Json, + ) -> ApiResult<()> { + context.require_root()?; + Ok(AccessToken::update(token.0.trim(), payload.0).await?) + } + + /// Regenerates the root access token. + /// + /// Requires root privileges. + #[oai( + path = "/reset-root-token", + method = "post", + operation_id = "regenerate_root_token" + )] + async fn regenerate_root_token(&self, context: ClientContext) -> ApiResult> { + context.require_root()?; + Ok(PlainText(reset_root_token().await?)) + } + + /// Reset the Root user's password. + /// + /// Only callable by an already authenticated Root user. + /// This endpoint updates the Root password to `password_str` + /// and regenerates the `root_token`, invalidating any previous token. + #[oai( + path = "/reset-root-password", + method = "post", + operation_id = "reset_root_password" + )] + async fn reset_root_password( + &self, + password_str: PlainText, + context: ClientContext, + ) -> ApiResult<()> { + context.require_root()?; + Ok(set_root_password(password_str.0.trim()).await?) + } +} diff --git a/src/modules/rest/api/account.rs b/src/modules/rest/api/account.rs new file mode 100644 index 0000000..8d41714 --- /dev/null +++ b/src/modules/rest/api/account.rs @@ -0,0 +1,207 @@ +// +// Copyright (c) 2025 rustmailer.com (https://rustmailer.com) +// +// This file is part of the Bichon Email Archiving Project +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +use std::collections::BTreeSet; + +use crate::modules::account::payload::{ + filter_accessible_accounts, AccountCreateRequest, AccountUpdateRequest, MinimalAccount, +}; +use crate::modules::account::state::AccountRunningState; +use crate::modules::account::migration::AccountModel; +use crate::modules::common::auth::ClientContext; +use crate::modules::common::paginated::paginate_vec; +use crate::modules::error::code::ErrorCode; +use crate::modules::rest::api::ApiTags; +use crate::modules::rest::response::DataPage; +use crate::modules::rest::ApiResult; +use crate::modules::token::{AccessToken, AccountInfo}; +use crate::raise_error; +use poem::web::Path; +use poem_openapi::param::Query; +use poem_openapi::payload::Json; +use poem_openapi::OpenApi; + +pub struct AccountApi; + +#[OpenApi(prefix_path = "/api/v1", tag = "ApiTags::Account")] +impl AccountApi { + /// Get account details by account ID + #[oai( + path = "/account/:account_id", + method = "get", + operation_id = "get_account" + )] + async fn get_account( + &self, + /// The account ID to retrieve + account_id: Path, + context: ClientContext, + ) -> ApiResult> { + let account_id = account_id.0; + context.require_account_access(account_id)?; + Ok(Json(AccountModel::get(account_id).await?)) + } + + /// Delete an account by ID - WARNING: This permanently removes the account and all associated resources + #[oai( + path = "/account/:account_id", + method = "delete", + operation_id = "remove_account" + )] + async fn remove_account( + &self, + /// The account ID to delete + account_id: Path, + context: ClientContext, + ) -> ApiResult<()> { + let account_id = account_id.0; + context.require_account_access(account_id)?; + Ok(AccountModel::delete(account_id).await?) + } + + /// Create a new account + #[oai(path = "/account", method = "post", operation_id = "create_account")] + async fn create_account( + &self, + /// Account creation request payload + payload: Json, + context: ClientContext, + ) -> ApiResult> { + let account = AccountModel::create_account(payload.0).await?; + if let Some(access_token) = &context.access_token { + let account_info = AccountInfo { + id: account.id, + email: account.email.clone(), + }; + AccessToken::grant_account_access(&access_token.token, account_info).await?; + } + Ok(Json(account)) + } + + /// Update an existing account + #[oai( + path = "/account/:account_id", + method = "post", + operation_id = "update_account" + )] + async fn update_account( + &self, + /// The account ID to update + account_id: Path, + /// Account update request payload + payload: Json, + context: ClientContext, + ) -> ApiResult<()> { + let account_id = account_id.0; + context.require_account_access(account_id)?; + Ok(AccountModel::update(account_id, payload.0, true).await?) + } + + /// List accounts with optional pagination parameters + #[oai( + path = "/accounts", + method = "get", + operation_id = "list_accounts" + )] + async fn list_accounts( + &self, + /// Optional. The page number to retrieve (starting from 1). + page: Query>, + /// Optional. The number of items per page. + page_size: Query>, + /// Optional. Whether to sort the list in descending order. + desc: Query>, + context: ClientContext, + ) -> ApiResult>> { + let accessible_accounts = context.accessible_accounts()?; + + if accessible_accounts.is_none() { + return Ok(Json( + AccountModel::paginate_list(page.0, page_size.0, desc.0).await?, + )); + } + + let all_accounts = AccountModel::list_all().await?; + let allowed_ids: BTreeSet = + accessible_accounts.unwrap().iter().map(|a| a.id).collect(); + + let mut filtered_accounts: Vec = all_accounts + .into_iter() + .filter(|acct| allowed_ids.contains(&acct.id)) + .collect(); + + let sort_desc = desc.0.unwrap_or(true); + filtered_accounts.sort_by(|a, b| { + if sort_desc { + b.created_at.cmp(&a.created_at) + } else { + a.created_at.cmp(&b.created_at) + } + }); + let page_data = + paginate_vec(&filtered_accounts, page.0, page_size.0).map(DataPage::from)?; + Ok(Json(page_data)) + } + + /// Get the running state of an account + #[oai( + path = "/account-state/:account_id", + method = "get", + operation_id = "account_state" + )] + async fn account_state( + &self, + /// The account ID to check state for + account_id: Path, + context: ClientContext, + ) -> ApiResult> { + let account_id = account_id.0; + context.require_account_access(account_id)?; + let state = AccountRunningState::get(account_id).await?.ok_or_else(|| { + raise_error!( + "account running state is not found".into(), + ErrorCode::ResourceNotFound + ) + })?; + Ok(Json(state)) + } + + /// Get a minimal list of active accounts for use in selectors when creating account-related resources + /// + /// This endpoint provides a lightweight list of accounts containing only essential information (id and name). + /// It's primarily designed for UI selectors/dropdowns when creating or associating resources with accounts. + #[oai( + path = "/minimal-account-list", + method = "get", + operation_id = "minimal_accounts_list" + )] + async fn minimal_accounts_list( + &self, + context: ClientContext, + ) -> ApiResult>> { + let accessible_accounts = context.accessible_accounts()?; + + let minimal_list = AccountModel::minimal_list().await?; + let result = match accessible_accounts { + Some(set) => filter_accessible_accounts(&minimal_list, set), + None => minimal_list, + }; + Ok(Json(result)) + } +} diff --git a/src/modules/rest/api/auto_config.rs b/src/modules/rest/api/auto_config.rs new file mode 100644 index 0000000..299c703 --- /dev/null +++ b/src/modules/rest/api/auto_config.rs @@ -0,0 +1,55 @@ +// +// Copyright (c) 2025 rustmailer.com (https://rustmailer.com) +// +// This file is part of the Bichon Email Archiving Project +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +use crate::modules::autoconfig::entity::MailServerConfig; +use crate::modules::autoconfig::load::resolve_autoconfig; +use crate::modules::error::code::ErrorCode; +use crate::modules::rest::api::ApiTags; +use crate::modules::rest::ApiResult; +use crate::raise_error; +use poem::web::Path; +use poem_openapi::payload::Json; +use poem_openapi::OpenApi; + +pub struct AutoConfigApi; + +#[OpenApi(prefix_path = "/api/v1", tag = "ApiTags::AutoConfig")] +impl AutoConfigApi { + /// Retrieve mail server configuration for a given email address + #[oai( + path = "/autoconfig/:email_address", + method = "get", + operation_id = "autoconfig" + )] + async fn autoconfig( + &self, + /// The email address to lookup configuration for + email_address: Path + ) -> ApiResult> { + let result = resolve_autoconfig(email_address.0.trim()) + .await? + .ok_or_else(|| { + raise_error!( + "Unable to find account configuration information in the backend.".into(), + ErrorCode::ResourceNotFound + ) + })?; + Ok(Json(result)) + } +} diff --git a/src/modules/rest/api/mailbox.rs b/src/modules/rest/api/mailbox.rs new file mode 100644 index 0000000..155ec95 --- /dev/null +++ b/src/modules/rest/api/mailbox.rs @@ -0,0 +1,60 @@ +// +// Copyright (c) 2025 rustmailer.com (https://rustmailer.com) +// +// This file is part of the Bichon Email Archiving Project +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +use crate::modules::cache::imap::mailbox::MailBox; +use crate::modules::common::auth::ClientContext; +use crate::modules::mailbox::list::get_account_mailboxes; +use crate::modules::rest::api::ApiTags; +use crate::modules::rest::ApiResult; +use poem::web::Path; +use poem_openapi::param::Query; +use poem_openapi::payload::Json; +use poem_openapi::OpenApi; + +pub struct MailBoxApi; + +#[OpenApi(prefix_path = "/api/v1", tag = "ApiTags::Mailbox")] +impl MailBoxApi { + /// Returns all available mailboxes for the given account. + /// + /// - For IMAP/SMTP accounts, this corresponds to folders/mailboxes. + /// - For Gmail API accounts, this corresponds to labels visible via the + /// `list messages` API (serving as mailbox equivalents). + /// + /// Both account types support two modes: + /// - Using the local cache of mailboxes/labels. + /// - Querying the remote service directly for the latest state. + #[oai( + path = "/list-mailboxes/:account_id", + method = "get", + operation_id = "list_mailboxes" + )] + async fn list_mailboxes( + &self, + /// The unique identifier of the account. + account_id: Path, + remote: Query>, + context: ClientContext, + ) -> ApiResult>> { + let account_id = account_id.0; + context.require_account_access(account_id)?; + let remote = remote.0.unwrap_or(false); + Ok(Json(get_account_mailboxes(account_id, remote).await?)) + } +} diff --git a/src/modules/rest/api/message.rs b/src/modules/rest/api/message.rs new file mode 100644 index 0000000..5992624 --- /dev/null +++ b/src/modules/rest/api/message.rs @@ -0,0 +1,218 @@ +// +// Copyright (c) 2025 rustmailer.com (https://rustmailer.com) +// +// This file is part of the Bichon Email Archiving Project +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +use crate::modules::common::auth::ClientContext; +use crate::modules::indexer::envelope::Envelope; +use crate::modules::indexer::manager::EML_INDEX_MANAGER; +use crate::modules::indexer::manager::ENVELOPE_INDEX_MANAGER; +use crate::modules::message::content::{retrieve_email_content, FullMessageContent}; +use crate::modules::message::delete::delete_messages_impl; +use crate::modules::message::list::{get_thread_messages, list_messages_impl}; +use crate::modules::message::search::{search_messages_impl, SearchRequest}; +use crate::modules::message::tags::TagCount; +use crate::modules::message::tags::UpdateTagsRequest; +use crate::modules::rest::api::ApiTags; +use crate::modules::rest::response::DataPage; +use crate::modules::rest::ApiResult; +use crate::modules::rest::ErrorCode; +use crate::raise_error; +use poem::web::Path; +use poem::Body; +use poem_openapi::param::Query; +use poem_openapi::payload::{Attachment, AttachmentType, Json}; +use poem_openapi::OpenApi; +use std::collections::HashMap; +use tantivy::schema::Facet; + +pub struct MessageApi; + +#[OpenApi(prefix_path = "/api/v1", tag = "ApiTags::Message")] +impl MessageApi { + /// Deletes messages from a mailbox or moves them to the trash for the specified account. + #[oai( + path = "/delete-messages", + method = "post", + operation_id = "delete_messages" + )] + async fn delete_messages( + &self, + /// specifying the mailbox and messages to delete. + payload: Json>>, + context: ClientContext, + ) -> ApiResult<()> { + let request = payload.0; + for account_id in request.keys() { + context.require_account_access(*account_id)?; + } + Ok(delete_messages_impl(request).await?) + } + + /// Lists messages in a specified mailbox for the given account. + #[oai( + path = "/list-messages/:account_id", + method = "get", + operation_id = "list_messages" + )] + async fn list_messages( + &self, + account_id: Path, + mailbox_id: Query, + page: Query, + page_size: Query, + context: ClientContext, + ) -> ApiResult>> { + let account_id = account_id.0; + let mailbox_id = mailbox_id.0; + context.require_account_access(account_id)?; + Ok(Json( + list_messages_impl(account_id, mailbox_id, page.0, page_size.0).await?, + )) + } + + /// Lists messages in a specified mailbox for the given account. + #[oai( + path = "/search-messages", + method = "post", + operation_id = "search_messages" + )] + async fn search_messages( + &self, + payload: Json, + context: ClientContext, + ) -> ApiResult>> { + context.require_root()?; + Ok(Json(search_messages_impl(payload.0).await?)) + } + + /// Get thread's envelopes in a specified mailbox for the given account. + #[oai( + path = "/get-thread-messages/:account_id", + method = "get", + operation_id = "get_thread_messages" + )] + async fn get_thread_messages( + &self, + /// The ID of the account owning the mailbox. + account_id: Path, + // Thread ID + thread_id: Query, + /// The page number for pagination (1-based). + page: Query, + /// The number of messages per page. + page_size: Query, + context: ClientContext, + ) -> ApiResult>> { + let account_id = account_id.0; + let thread_id = thread_id.0; + context.require_account_access(account_id)?; + Ok(Json( + get_thread_messages(account_id, thread_id, page.0, page_size.0).await?, + )) + } + + /// Fetches the content of a specific email for the given account. + #[oai( + path = "/message-content/:account_id", + method = "get", + operation_id = "fetch_message_content" + )] + async fn fetch_message_content( + &self, + account_id: Path, + id: Query, + context: ClientContext, + ) -> ApiResult> { + let account_id = account_id.0; + context.require_account_access(account_id)?; + Ok(Json(retrieve_email_content(account_id, id.0).await?)) + } + + /// Fetches the full content of a specific email for the given account. + #[oai( + path = "/download-message/:account_id", + method = "get", + operation_id = "download_message" + )] + async fn download_message( + &self, + account_id: Path, + id: Query, + context: ClientContext, + ) -> ApiResult> { + let account_id = account_id.0; + context.require_account_access(account_id)?; + let id = id.0; + let reader = EML_INDEX_MANAGER.get_reader(account_id, id).await?; + let body = Body::from_async_read(reader); + let attachment = Attachment::new(body) + .attachment_type(AttachmentType::Attachment) + .filename(format!("{id}.eml")); + Ok(attachment) + } + + /// Downloads a specific attachment by filename. + #[oai( + path = "/download-attachment/:account_id", + method = "get", + operation_id = "download_attachment" + )] + async fn download_attachment( + &self, + account_id: Path, + id: Query, + name: Query, + context: ClientContext, + ) -> ApiResult> { + let account_id = account_id.0; + context.require_account_access(account_id)?; + let email_id = id.0; + let name = name.0.trim(); + let reader = EML_INDEX_MANAGER + .get_attachment(account_id, email_id, name) + .await?; + let body = Body::from_async_read(reader); + let attachment = Attachment::new(body) + .attachment_type(AttachmentType::Attachment) + .filename(name); + Ok(attachment) + } + /// Returns all facets in the index along with their document counts. + #[oai(path = "/all-tags", method = "get", operation_id = "get_all_tags")] + async fn get_all_tags(&self) -> ApiResult>> { + Ok(Json(ENVELOPE_INDEX_MANAGER.get_all_tags().await?)) + } + + /// Adds or removes facet tags for multiple emails across accounts. + #[oai( + path = "/update-tags", + method = "post", + operation_id = "update_envelope_tags" + )] + async fn update_envelope_tags(&self, req: Json) -> ApiResult<()> { + let req = req.0; + for tag in &req.tags { + Facet::from_text(tag) + .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InvalidParameter))?; + } + ENVELOPE_INDEX_MANAGER + .update_envelope_tags(req.updates, req.tags) + .await?; + Ok(()) + } +} diff --git a/src/modules/rest/api/mod.rs b/src/modules/rest/api/mod.rs new file mode 100644 index 0000000..e609425 --- /dev/null +++ b/src/modules/rest/api/mod.rs @@ -0,0 +1,74 @@ +// +// Copyright (c) 2025 rustmailer.com (https://rustmailer.com) +// +// This file is part of the Bichon Email Archiving Project +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +use access_token::AccessTokenApi; +use account::AccountApi; +use auto_config::AutoConfigApi; +use mailbox::MailBoxApi; +use message::MessageApi; +use oauth2::OAuth2Api; +use poem_openapi::{OpenApiService, Tags}; +use system::SystemApi; + +use crate::bichon_version; + +pub mod access_token; +pub mod account; +pub mod auto_config; +pub mod mailbox; +pub mod message; +pub mod oauth2; +pub mod system; + +#[derive(Tags)] +pub enum ApiTags { + AccessToken, + AutoConfig, + Account, + Mailbox, + OAuth2, + Message, + System, +} + +type RustMailOpenApi = ( + AccessTokenApi, + AutoConfigApi, + AccountApi, + SystemApi, + MailBoxApi, + OAuth2Api, + MessageApi, +); + +pub fn create_openapi_service() -> OpenApiService { + OpenApiService::new( + ( + AccessTokenApi, + AutoConfigApi, + AccountApi, + SystemApi, + MailBoxApi, + OAuth2Api, + MessageApi, + ), + "BichonApi", + bichon_version!(), + ) +} diff --git a/src/modules/rest/api/oauth2.rs b/src/modules/rest/api/oauth2.rs new file mode 100644 index 0000000..c3af8c3 --- /dev/null +++ b/src/modules/rest/api/oauth2.rs @@ -0,0 +1,228 @@ +// +// Copyright (c) 2025 rustmailer.com (https://rustmailer.com) +// +// This file is part of the Bichon Email Archiving Project +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +use crate::modules::common::auth::ClientContext; +use crate::modules::error::code::ErrorCode; +use crate::modules::oauth2::entity::{OAuth2, OAuth2CreateRequest, OAuth2UpdateRequest}; +use crate::modules::oauth2::flow::{AuthorizeUrlRequest, OAuth2Flow}; +use crate::modules::oauth2::token::{ExternalOAuth2Request, OAuth2AccessToken}; +use crate::modules::rest::api::ApiTags; +use crate::modules::rest::response::DataPage; +use crate::modules::rest::ApiResult; +use crate::raise_error; +use poem::web::Path; +use poem_openapi::param::Query; +use poem_openapi::payload::{Json, PlainText}; +use poem_openapi::OpenApi; + +pub struct OAuth2Api; + +#[OpenApi(prefix_path = "/api/v1", tag = "ApiTags::OAuth2")] +impl OAuth2Api { + /// Retrieves the OAuth2 configuration for a specified name. + /// + /// Requires root privileges. + /// This endpoint fetches the OAuth2 configuration identified by the given name. + #[oai( + path = "/oauth2/:id", + method = "get", + operation_id = "get_oauth2_config" + )] + async fn get_oauth2_config( + &self, + /// The name of the OAuth2 configuration to retrieve + id: Path, + context: ClientContext, + ) -> ApiResult> { + context.require_root()?; + let id = id.0; + Ok(Json(OAuth2::get(id).await?.ok_or_else(|| { + raise_error!( + format!("OAuth2 configuration id='{id}' not found"), + ErrorCode::ResourceNotFound + ) + })?)) + } + + /// Deletes an OAuth2 configuration by name. + /// + /// Requires root privileges. + /// This endpoint removes the OAuth2 configuration identified by the specified name. + #[oai( + path = "/oauth2/:id", + method = "delete", + operation_id = "remove_oauth2_config" + )] + async fn remove_oauth2_config( + &self, + /// The name of the OAuth2 configuration to retrieve + id: Path, + context: ClientContext, + ) -> ApiResult<()> { + context.require_root()?; + Ok(OAuth2::delete(id.0).await?) + } + + /// Creates a new OAuth2 configuration. + /// + /// Requires root privileges. + /// This endpoint creates a new OAuth2 configuration based on the provided request data. + #[oai( + path = "/oauth2", + method = "post", + operation_id = "create_oauth2_config" + )] + async fn create_oauth2_config( + &self, + /// A JSON payload containing the details for the new OAuth2 configuration + request: Json, + context: ClientContext, + ) -> ApiResult<()> { + context.require_root()?; + let entity = OAuth2::new(request.0)?; + Ok(entity.save().await?) + } + + /// Updates an existing OAuth2 configuration. + /// + /// Requires root privileges. + /// This endpoint updates the OAuth2 configuration identified by the specified name + #[oai( + path = "/oauth2/:id", + method = "post", + operation_id = "update_oauth2_config" + )] + async fn update_oauth2_config( + &self, + /// The name of the OAuth2 configuration to update + id: Path, + /// A JSON payload containing the updated configuration details + payload: Json, + context: ClientContext, + ) -> ApiResult<()> { + context.require_root()?; + Ok(OAuth2::update(id.0, payload.0).await?) + } + + /// Lists OAuth2 configurations with pagination and sorting options. + /// + /// This endpoint retrieves a paginated list of OAuth2 configurations, allowing for + /// optional pagination and sorting parameters. It requires root access. + #[oai( + path = "/oauth2-list", + method = "get", + operation_id = "list_oauth2_config" + )] + async fn list_oauth2_config( + &self, + /// Optional. The page number to retrieve (starting from 1). + page: Query>, + /// Optional. The number of items per page. + page_size: Query>, + /// Optional. Whether to sort the list in descending order. + desc: Query>, + context: ClientContext, + ) -> ApiResult>> { + context.require_root()?; + Ok(Json( + OAuth2::paginate_list(page.0, page_size.0, desc.0).await?, + )) + } + + /// Generates an OAuth2 authorization URL for a specific account. + /// + /// This endpoint creates an authorization URL for the specified OAuth2 configuration + /// and account ID. It requires root access and returns the URL as plain text. + #[oai( + path = "/oauth2-authorize-url", + method = "post", + operation_id = "create_oauth2_authorize_url" + )] + async fn create_oauth2_authorize_url( + &self, + /// A JSON payload containing the OAuth2 configuration name and account ID. + request: Json, + context: ClientContext, + ) -> ApiResult> { + context.require_root()?; + let request = request.0; + let flow = OAuth2Flow::new(request.oauth2_id); + Ok(PlainText(flow.authorize_url(request.account_id).await?)) + } + + /// Retrieves OAuth2 access tokens for a specified account. + /// + /// This endpoint fetches the OAuth2 access tokens associated with the given account ID. + #[oai( + path = "/oauth2-tokens/:account_id", + method = "get", + operation_id = "get_oauth2_tokens" + )] + async fn get_oauth2_tokens( + &self, + /// The ID of the account to retrieve access tokens for + account_id: Path, + context: ClientContext, + ) -> ApiResult> { + let account = account_id.0; + context.require_account_access(account)?; + Ok(Json(OAuth2AccessToken::get(account).await?.ok_or_else( + || { + raise_error!( + "OAuth2 access tokens not found".into(), + ErrorCode::ResourceNotFound + ) + }, + )?)) + } + + /// Configures an external OAuth2 token for a specified account. + /// + /// This endpoint allows two usage modes: + /// 1. If only an `access_token` is provided, RustMailer will store it directly. + /// - In this mode, RustMailer **cannot refresh** the token, since it has no + /// associated OAuth2 configuration or refresh token. + /// - The caller is responsible for periodically updating the access token + /// by calling this endpoint again. + /// 2. If both `oauth2_id` and `refresh_token` are provided, it means the external + /// OAuth2 authorization flow has been completed outside RustMailer. + /// - Since the OAuth2 configuration (including client_id and client_secret) + /// is already stored in RustMailer, the service can use the refresh token + /// to obtain new access tokens automatically. + /// + /// Note: The `oauth2_id` must reference a valid OAuth2 configuration + /// already created in RustMailer. + #[oai( + path = "/store-external-oauth2-token/:account_id", + method = "post", + operation_id = "store_external_oauth2_token" + )] + async fn store_external_oauth2_token( + &self, + account_id: Path, + request: Json, + context: ClientContext, + ) -> ApiResult<()> { + let account = account_id.0; + // Check account access permissions + context.require_account_access(account)?; + OAuth2AccessToken::upsert_external_oauth_token(account, request.0).await?; + Ok(()) + } +} diff --git a/src/modules/rest/api/system.rs b/src/modules/rest/api/system.rs new file mode 100644 index 0000000..9050410 --- /dev/null +++ b/src/modules/rest/api/system.rs @@ -0,0 +1,120 @@ +// +// Copyright (c) 2025 rustmailer.com (https://rustmailer.com) +// +// This file is part of the Bichon Email Archiving Project +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +use crate::modules::common::auth::ClientContext; +use crate::modules::dashboard::DashboardStats; +use crate::modules::error::code::ErrorCode; +use crate::modules::rest::api::ApiTags; +use crate::modules::rest::ApiResult; +use crate::modules::settings::proxy::Proxy; +use crate::modules::version::{fetch_notifications, Notifications}; +use crate::raise_error; +use poem_openapi::param::Path; +use poem_openapi::payload::{Json, PlainText}; +use poem_openapi::OpenApi; + +pub struct SystemApi; + +#[OpenApi(prefix_path = "/api/v1", tag = "ApiTags::System")] +impl SystemApi { + /// Retrieves important system notifications for the RustMail service. + /// + /// This endpoint returns a consolidated view of all critical system notifications including: + /// - Available version updates + /// - License expiration warnings + #[oai( + method = "get", + path = "/notifications", + operation_id = "get_notifications" + )] + async fn get_notifications(&self) -> ApiResult> { + let notification = fetch_notifications() + .await + .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))?; + Ok(Json(notification)) + } + + /// Get overall dashboard statistics. + /// + /// Returns various aggregated metrics about the mail system, such as + /// total email count, total storage size, index usage, top senders, + /// recent activity histogram, and more. + #[oai( + method = "get", + path = "/dashboard-stats", + operation_id = "get_dashboard_stats" + )] + async fn get_dashboard_stats(&self) -> ApiResult> { + let stats = DashboardStats::get().await?; + Ok(Json(stats)) + } + + /// Get the full list of SOCKS5 proxy configurations. + #[oai(method = "get", path = "/list-proxy", operation_id = "list_proxy")] + async fn list_proxy(&self) -> ApiResult>> { + let proxies = Proxy::list_all() + .await + .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))?; + Ok(Json(proxies)) + } + + /// Delete a specific proxy configuration by ID. Requires root permission. + #[oai(path = "/proxy/:id", method = "delete", operation_id = "remove_proxy")] + async fn remove_proxy( + &self, + /// The name of the OAuth2 configuration to retrieve + id: Path, + context: ClientContext, + ) -> ApiResult<()> { + context.require_root()?; + Ok(Proxy::delete(id.0).await?) + } + + /// Retrieve a specific proxy configuration by ID + #[oai(path = "/proxy/:id", method = "get", operation_id = "get_proxy")] + async fn get_proxy( + &self, + /// The name of the OAuth2 configuration to retrieve + id: Path, + context: ClientContext, + ) -> ApiResult> { + context.require_root()?; + Ok(Json(Proxy::get(id.0).await?)) + } + + /// Create a new proxy configuration. Requires root permission. + #[oai(path = "/proxy", method = "post", operation_id = "create_proxy")] + async fn create_proxy(&self, url: PlainText, context: ClientContext) -> ApiResult<()> { + context.require_root()?; + let entity = Proxy::new(url.0); + Ok(entity.save().await?) + } + + /// Update the URL of a specific proxy by ID. Requires root permission. + #[oai(path = "/proxy/:id", method = "post", operation_id = "update_proxy")] + async fn update_proxy( + &self, + id: Path, + url: PlainText, + context: ClientContext, + ) -> ApiResult<()> { + context.require_root()?; + Ok(Proxy::update(id.0, url.0).await?) + } +} diff --git a/src/modules/rest/assets.rs b/src/modules/rest/assets.rs new file mode 100644 index 0000000..1e6f9e6 --- /dev/null +++ b/src/modules/rest/assets.rs @@ -0,0 +1,24 @@ +// +// Copyright (c) 2025 rustmailer.com (https://rustmailer.com) +// +// This file is part of the Bichon Email Archiving Project +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +use rust_embed::RustEmbed; + +#[derive(RustEmbed)] +#[folder = "web/dist/"] +pub struct FrontEndAssets; diff --git a/src/modules/rest/mod.rs b/src/modules/rest/mod.rs new file mode 100644 index 0000000..48d9468 --- /dev/null +++ b/src/modules/rest/mod.rs @@ -0,0 +1,139 @@ +// +// Copyright (c) 2025 rustmailer.com (https://rustmailer.com) +// +// This file is part of the Bichon Email Archiving Project +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +use crate::modules::common::error::ErrorCapture; +use crate::modules::common::log::Tracing; +use crate::modules::common::tls::rustls_config; +use crate::modules::error::code::ErrorCode; +use crate::modules::error::handler::error_handler; +use crate::modules::error::BichonResult; +use crate::modules::rest::public::login::login; +use crate::modules::rest::public::status::get_status; +use crate::modules::{settings::cli::SETTINGS, utils::shutdown::shutdown_signal}; + +use super::error::ApiErrorResponse; +use crate::modules::common::auth::ApiGuard; +use crate::modules::common::timeout::{Timeout, TIMEOUT_HEADER}; +use crate::raise_error; +use api::create_openapi_service; +use assets::FrontEndAssets; +use http::HeaderValue; +use poem::endpoint::EmbeddedFilesEndpoint; +use poem::listener::{Listener, TcpListener}; +use poem::middleware::{CatchPanic, Compression, SetHeader}; +use poem::{endpoint::EmbeddedFileEndpoint, middleware::Cors, EndpointExt, Route, Server}; +use poem::{get, post}; +use public::oauth2::oauth2_callback; +use std::time::Duration; + +pub mod api; +pub mod assets; +pub mod public; +pub mod response; + +pub type ApiResult = std::result::Result; + +pub async fn start_http_server() -> BichonResult<()> { + let listener = TcpListener::bind(( + SETTINGS.bichon_bind_ip.clone().unwrap_or("0.0.0.0".into()), + SETTINGS.bichon_http_port as u16, + )); + + let listener = if SETTINGS.bichon_enable_rest_https { + listener.rustls(rustls_config()?).boxed() + } else { + listener.boxed() + }; + + let api_service = create_openapi_service() + .summary("A self-hosted IMAP/SMTP middleware designed for developers"); + + let swagger = api_service.swagger_ui(); + let redoc = api_service.redoc(); + let scalar = api_service.scalar(); + let spec_json = api_service.spec_endpoint(); + let spec_yaml = api_service.spec_endpoint_yaml(); + let openapi_explorer = api_service.openapi_explorer(); + + let open_api_route = Route::new() + .nest_no_strip("/api/v1", api_service) + .with(ApiGuard) + .with(ErrorCapture) + .with(Timeout) + .with(Tracing); + + let mut cors_origins = SETTINGS.bichon_cors_origins.clone(); + if cors_origins.is_empty() { + cors_origins = ["*".to_string()].into_iter().collect(); + } + + let cache_static = || { + SetHeader::new().overriding( + http::header::CACHE_CONTROL, + HeaderValue::from_static("max-age=86400"), + ) + }; + + let cors = Cors::new() + .allow_origins(cors_origins) + .allow_credentials(true) + .allow_methods(vec!["GET", "POST", "PUT", "DELETE", "OPTIONS", "HEAD"]) + .allow_headers(vec!["Content-Type", "Authorization", TIMEOUT_HEADER]) + .expose_headers(vec!["Accept"]) + .max_age(SETTINGS.bichon_cors_max_age); + + let route = Route::new() + .nest("/api-docs/swagger", swagger) + .nest("/api-docs/redoc", redoc) + .nest("/api-docs/explorer", openapi_explorer) + .nest("/api-docs/scalar", scalar) + .nest("/api-docs/spec.json", spec_json) + .nest("/api-docs/spec.yaml", spec_yaml) + .nest("/oauth2/callback", get(oauth2_callback)) + .nest("/api/status", get(get_status)) + .nest("/api/login", post(login)) + .nest_no_strip("/api/v1", open_api_route) + .nest_no_strip( + "/assets", + EmbeddedFilesEndpoint::::new().with(cache_static()), + ) + .at( + "/*", + EmbeddedFileEndpoint::::new("index.html"), + ) + .with(cors) + .with_if(SETTINGS.bichon_http_compression_enabled, Compression::new()) + .with(CatchPanic::new()); + + let server = Server::new(listener) + .name("RustMailer API Service") + .idle_timeout(Duration::from_secs(60)) + .run_with_graceful_shutdown( + route.catch_all_error(error_handler), + shutdown_signal(), + Some(Duration::from_secs(5)), + ); + println!( + "RustMailer API Service is now running on port {}.", + SETTINGS.bichon_http_port + ); + server + .await + .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError)) +} diff --git a/src/modules/rest/public/login.rs b/src/modules/rest/public/login.rs new file mode 100644 index 0000000..1487fb6 --- /dev/null +++ b/src/modules/rest/public/login.rs @@ -0,0 +1,45 @@ +// +// Copyright (c) 2025 rustmailer.com (https://rustmailer.com) +// +// This file is part of the Bichon Email Archiving Project +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +use crate::modules::token::root::check_root_password; +use poem::{handler, IntoResponse, Response}; +use tracing::error; + +/// Login endpoint for Root user +/// +/// Accepts a plain text password and returns the `root_token` +/// on successful authentication. +#[handler] +pub async fn login(password: String) -> Response { + match check_root_password(&password) { + Ok(root_token) => Response::builder() + .status(http::StatusCode::OK) + .content_type("text/plain") + .body(root_token) + .into_response(), + Err(e) => { + error!("Root login failed: {:?}", e); + Response::builder() + .status(http::StatusCode::UNAUTHORIZED) + .content_type("text/plain") + .body(e.to_string()) + .into_response() + } + } +} diff --git a/src/modules/rest/public/mod.rs b/src/modules/rest/public/mod.rs new file mode 100644 index 0000000..a3de2ac --- /dev/null +++ b/src/modules/rest/public/mod.rs @@ -0,0 +1,22 @@ +// +// Copyright (c) 2025 rustmailer.com (https://rustmailer.com) +// +// This file is part of the Bichon Email Archiving Project +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +pub mod login; +pub mod oauth2; +pub mod status; diff --git a/src/modules/rest/public/oauth2.rs b/src/modules/rest/public/oauth2.rs new file mode 100644 index 0000000..10eb52d --- /dev/null +++ b/src/modules/rest/public/oauth2.rs @@ -0,0 +1,94 @@ +// +// Copyright (c) 2025 rustmailer.com (https://rustmailer.com) +// +// This file is part of the Bichon Email Archiving Project +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +use crate::modules::oauth2::{flow::OAuth2Flow, pending::OAuth2PendingEntity}; +use poem::{ + handler, + web::{Query, Redirect}, + IntoResponse, Result, +}; +use serde::{Deserialize, Serialize}; +use tracing::error; + +#[derive(Serialize, Deserialize, Debug)] +pub struct OAuth2CallbackParams { + state: Option, + code: Option, +} +#[handler] +pub async fn oauth2_callback( + Query(params): Query, +) -> Result { + let (state, code) = match (¶ms.state, ¶ms.code) { + (Some(state), Some(code)) => (state, code), + (None, _) => { + let message = + "The state parameter is missing. Please initiate the OAuth2 process again."; + return Ok(Redirect::temporary(format!( + "/oauth2-result?error=missing_state&message={}", + urlencoding::encode(message) + )) + .into_response()); + } + (_, None) => { + let message = "The authorization code is missing. Please try the OAuth2 login again."; + return Ok(Redirect::temporary(format!( + "/oauth2-result?error=missing_code&message={}", + urlencoding::encode(message) + )) + .into_response()); + } + }; + + let pending = match OAuth2PendingEntity::get(state).await { + Ok(Some(pending)) => pending, + _ => { + let message = + "The provided state is invalid or expired. Please start the OAuth2 process again."; + return Ok(Redirect::temporary(format!( + "/oauth2-result?error=invalid_state&message={}", + urlencoding::encode(message) + )) + .into_response()); + } + }; + + let flow = OAuth2Flow::new(pending.oauth2_id); + if let Err(e) = flow + .fetch_save_access_token(pending.account_id, &pending.code_verifier, code) + .await + { + error!("Failed to save access token: {:#?}", e); + let message = format!( + "Failed to retrieve or save the access token. Error details: {:#?}", + e + ); + return Ok(Redirect::temporary(format!( + "/oauth2-result?error=token_fetch_failed&message={}", + urlencoding::encode(&message) + )) + .into_response()); + } + + if let Err(e) = OAuth2PendingEntity::delete(state).await { + error!("Failed to delete pending OAuth2 entity: {}", e); + } + + Ok(Redirect::temporary("/oauth2-result?success=true").into_response()) +} diff --git a/src/modules/rest/public/status.rs b/src/modules/rest/public/status.rs new file mode 100644 index 0000000..ed7dc75 --- /dev/null +++ b/src/modules/rest/public/status.rs @@ -0,0 +1,26 @@ +// +// Copyright (c) 2025 rustmailer.com (https://rustmailer.com) +// +// This file is part of the Bichon Email Archiving Project +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +use crate::modules::context::status::BichonStatus; +use poem::{handler, web::Json, IntoResponse}; + +#[handler] +pub async fn get_status() -> impl IntoResponse { + Json(BichonStatus::get()) +} diff --git a/src/modules/rest/response.rs b/src/modules/rest/response.rs new file mode 100644 index 0000000..a7e21c3 --- /dev/null +++ b/src/modules/rest/response.rs @@ -0,0 +1,84 @@ +// +// Copyright (c) 2025 rustmailer.com (https://rustmailer.com) +// +// This file is part of the Bichon Email Archiving Project +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +use poem_openapi::Object; +use serde::{Deserialize, Serialize}; + +use crate::modules::database::Paginated; + +/// Represents a paginated response containing a subset of items along with pagination metadata. +/// +/// This generic structure is commonly used to return paged data from list or search endpoints. +/// The type `S` represents the individual item type within the result set. +/// +/// # Type Parameters +/// - `S`: The type of each item in the `items` list. Must implement several traits for serialization +/// and OpenAPI documentation. +/// +/// # Fields +/// - `current_page`: The current page number (1-based). `None` if unspecified or not applicable. +/// - `page_size`: The number of items per page. `None` if unspecified. +/// - `total_items`: The total number of items matching the query. +/// - `items`: The list of items returned for the current page. +/// - `total_pages`: The total number of pages available. `None` if not calculated. +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Object)] +pub struct DataPage +where + S: Serialize + + std::fmt::Debug + + std::marker::Unpin + + Send + + Sync + + poem_openapi::types::Type + + poem_openapi::types::ParseFromJSON + + poem_openapi::types::ToJSON, +{ + /// The current page number (starting from 1). + pub current_page: Option, + /// The number of items per page. + pub page_size: Option, + /// The total number of items across all pages. + pub total_items: u64, + /// The list of items returned on the current page. + pub items: Vec, + /// The total number of pages. This is optional and may not be set if not calculated. + pub total_pages: Option, +} + +impl< + S: Serialize + + std::fmt::Debug + + std::marker::Unpin + + Send + + Sync + + poem_openapi::types::Type + + poem_openapi::types::ParseFromJSON + + poem_openapi::types::ToJSON, + > From> for DataPage +{ + fn from(paginated: Paginated) -> Self { + DataPage { + current_page: paginated.page, + page_size: paginated.page_size, + total_items: paginated.total_items, + total_pages: paginated.total_pages, + items: paginated.items, + } + } +} diff --git a/src/modules/settings/cli.rs b/src/modules/settings/cli.rs new file mode 100644 index 0000000..e34b7db --- /dev/null +++ b/src/modules/settings/cli.rs @@ -0,0 +1,246 @@ +// +// Copyright (c) 2025 rustmailer.com (https://rustmailer.com) +// +// This file is part of the Bichon Email Archiving Project +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +use clap::{builder::ValueParser, Parser, ValueEnum}; +use std::{collections::HashSet, env, fmt, path::PathBuf, sync::LazyLock}; + +pub static SETTINGS: LazyLock = LazyLock::new(Settings::parse); + +#[derive(Debug, Parser)] +#[clap( + name = "bichon", + about = "A self-hosted email synchronization and backup tool built in Rust", + version = env!("CARGO_PKG_VERSION") +)] +pub struct Settings { + /// bichon log level (default: "info") + #[clap( + long, + default_value = "info", + env, + help = "Set the log level for bichon" + )] + pub bichon_log_level: String, + + /// bichon HTTP port (default: 15630) + #[clap( + long, + default_value = "15630", + env, + help = "Set the HTTP port for bichon" + )] + pub bichon_http_port: i32, + + /// The IP address that the node binds to, in IPv4 format (e.g., 192.168.1.1). + #[clap( + long, + env, + default_value = "0.0.0.0", + help = "The IP address that the node binds to, in IPv4 format (e.g., 192.168.1.1). Required in cluster mode.", + value_parser = ValueParser::new(|s: &str| { + // Ensure the input is a valid IPv4 address + if s.parse::().is_err() { + return Err("The bind IP address must be a valid IPv4 address.".to_string()); + } + + // If the address is valid, return it + Ok(s.to_string()) + }) + )] + pub bichon_bind_ip: Option, + + /// RustMail public URL (default: "http://localhost:15630") + #[clap( + long, + default_value = "http://localhost:15630", + env, + help = "Set the public URL for bichon" + )] + pub bichon_public_url: String, + + /// CORS allowed origins (default: "*") + #[clap( + long, + default_value = "http://localhost:5173, http://localhost:15630, http://192.168.3.2:15630, *", + env, + help = "Set the allowed CORS origins (comma-separated list, e.g., \"https://example.com, https://another.com\")", + value_parser = ValueParser::new(|s: &str| -> Result, String> { + let set: HashSet = s.split(',') + .map(|origin| origin.trim().to_string()) + .filter(|origin| !origin.is_empty()) + .collect(); + Ok(set) + }) + )] + pub bichon_cors_origins: HashSet, + + /// CORS max age in seconds (default: 86400) + #[clap( + long, + default_value = "86400", + env, + help = "Set the CORS max age in seconds" + )] + pub bichon_cors_max_age: i32, + + /// Enable ANSI logs (default: false) + #[clap(long, default_value = "true", env, help = "Enable ANSI formatted logs")] + pub bichon_ansi_logs: bool, + + /// Enable log file output (default: false) + /// If false, logs will be printed to stdout + #[clap( + long, + default_value = "false", + env, + help = "Enable log file output (otherwise logs go to stdout)" + )] + pub bichon_log_to_file: bool, + + /// Enable JSON logs (default: false) + #[clap( + long, + default_value = "false", + env, + help = "Enable JSON formatted logs" + )] + pub bichon_json_logs: bool, + + /// Maximum number of log files (default: 5) + #[clap( + long, + default_value = "5", + env, + help = "Set the maximum number of server log files" + )] + pub bichon_max_server_log_files: usize, + + /// bichon encryption password + #[clap( + long, + default_value = "change-this-default-password-now", + env, + help = "Set the encryption password for bichon. ⚠️ Change this default in production!" + )] + pub bichon_encrypt_password: String, + + #[clap( + long, + env, + help = "Set the file path for bichon database", + value_parser = ValueParser::new(|s: &str| { + let path = PathBuf::from(s); + if !path.is_absolute() { + return Err("Path must be an absolute directory path".to_string()); + } + if !path.exists() { + return Err(format!("Path {:?} does not exist", path)); + } + if !path.is_dir() { + return Err(format!("Path {:?} is not a directory", path)); + } + Ok(s.to_string()) + }) + )] + pub bichon_root_dir: String, + + #[clap( + long, + env, + default_value = "134217728", + help = "Set the cache size for bichon metadata database in bytes" + )] + pub bichon_metadata_cache_size: Option, + + #[clap( + long, + env, + default_value = "1073741824", + help = "Set the cache size for envelope database in bytes" + )] + pub bichon_envelope_cache_size: Option, + + /// Enables or disables the access token mechanism for HTTP endpoints. + /// + /// When set to `true`, HTTP requests will be subject to access token validation. + /// If the `Authorization` header is missing or the token is invalid, the service will return a 401 Unauthorized response. + /// When set to `false`, access token validation will be skipped. + #[clap( + long, + default_value = "false", + env, + help = "Enables or disables the access token mechanism for HTTP endpoints." + )] + pub bichon_enable_access_token: bool, + + /// Enables or disables HTTPS for REST API endpoints. + /// + /// When set to `true`, the REST API will use HTTPS with a valid SSL/TLS certificate for secure communication. + /// If no valid certificate is configured or HTTPS cannot be established, the service will fail to start. + /// When set to `false`, the REST API will use plain HTTP without encryption. + #[clap( + long, + default_value = "false", + env, + help = "Enables or disables HTTPS for REST API endpoints." + )] + pub bichon_enable_rest_https: bool, + + #[clap( + long, + default_value = "true", + env, + help = "Enable compression for the open api server" + )] + pub bichon_http_compression_enabled: bool, + + #[clap( + long, + env, + help = "Maximum number of concurrent email sync tasks (default: number of CPU cores x 2)", + value_parser = clap::value_parser!(u16).range(1..) + )] + pub bichon_sync_concurrency: Option, +} + +#[derive(Clone, Copy, Debug, PartialEq, ValueEnum)] +pub enum CompressionAlgorithm { + #[clap(name = "none")] + None, + #[clap(name = "gzip")] + Gzip, + #[clap(name = "brotli")] + Brotli, + #[clap(name = "zstd")] + Zstd, + #[clap(name = "deflate")] + Deflate, +} + +impl fmt::Display for CompressionAlgorithm { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + CompressionAlgorithm::None => write!(f, "none"), + CompressionAlgorithm::Gzip => write!(f, "gzip"), + CompressionAlgorithm::Brotli => write!(f, "brotli"), + CompressionAlgorithm::Zstd => write!(f, "zstd"), + CompressionAlgorithm::Deflate => write!(f, "deflate"), + } + } +} diff --git a/src/modules/settings/dir.rs b/src/modules/settings/dir.rs new file mode 100644 index 0000000..3507ff2 --- /dev/null +++ b/src/modules/settings/dir.rs @@ -0,0 +1,81 @@ +// +// Copyright (c) 2025 rustmailer.com (https://rustmailer.com) +// +// This file is part of the Bichon Email Archiving Project +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +use crate::modules::context::Initialize; +use crate::modules::settings::cli::SETTINGS; +use crate::{ + modules::error::{code::ErrorCode, BichonResult}, + raise_error, +}; +use std::path::PathBuf; +use std::sync::LazyLock; + +pub const META_FILE: &str = "meta.db"; +pub const MAILBOX_FILE: &str = "mailbox.db"; +const ENVELOPE_DIR: &str = "envelope"; +const EML_DIR: &str = "eml"; +const TMP_DIR: &str = "tmp"; +const LOG_DIR: &str = "logs"; +const TLS_CERT: &str = "cert.pem"; +const TLS_KEY: &str = "key.pem"; + + +pub static DATA_DIR_MANAGER: LazyLock = + LazyLock::new(|| DataDirManager::new(PathBuf::from(&SETTINGS.bichon_root_dir))); + +#[derive(Debug)] +pub struct DataDirManager { + pub root_dir: PathBuf, + pub meta_db: PathBuf, + pub mailbox_db: PathBuf, + pub temp_dir: PathBuf, + pub tls_cert: PathBuf, + pub tls_key: PathBuf, + pub envelope_dir: PathBuf, + pub eml_dir: PathBuf, + pub log_dir: PathBuf +} + +impl Initialize for DataDirManager { + async fn initialize() -> BichonResult<()> { + std::fs::create_dir_all(&DATA_DIR_MANAGER.root_dir) + .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))?; + std::fs::create_dir_all(&DATA_DIR_MANAGER.log_dir) + .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))?; + std::fs::create_dir_all(&DATA_DIR_MANAGER.temp_dir) + .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))?; + Ok(()) + } +} + +impl DataDirManager { + pub fn new(root_dir: PathBuf) -> Self { + Self { + root_dir: root_dir.clone(), + meta_db: root_dir.join(META_FILE), + mailbox_db: root_dir.join(MAILBOX_FILE), + tls_key: root_dir.join(TLS_KEY), + tls_cert: root_dir.join(TLS_CERT), + log_dir: root_dir.join(LOG_DIR), + envelope_dir: root_dir.join(ENVELOPE_DIR), + temp_dir: root_dir.join(TMP_DIR), + eml_dir: root_dir.join(EML_DIR), + } + } +} diff --git a/src/modules/settings/mod.rs b/src/modules/settings/mod.rs new file mode 100644 index 0000000..e36694e --- /dev/null +++ b/src/modules/settings/mod.rs @@ -0,0 +1,23 @@ +// +// Copyright (c) 2025 rustmailer.com (https://rustmailer.com) +// +// This file is part of the Bichon Email Archiving Project +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +pub mod cli; +pub mod dir; +pub mod proxy; +pub mod system; diff --git a/src/modules/settings/proxy.rs b/src/modules/settings/proxy.rs new file mode 100644 index 0000000..df3845a --- /dev/null +++ b/src/modules/settings/proxy.rs @@ -0,0 +1,145 @@ +// +// Copyright (c) 2025 rustmailer.com (https://rustmailer.com) +// +// This file is part of the Bichon Email Archiving Project +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +use native_db::*; +use native_model::{native_model, Model}; +use poem_openapi::Object; +use serde::{Deserialize, Serialize}; + +use crate::{ + id, + modules::{ + database::{ + async_find_impl, delete_impl, insert_impl, list_all_impl, manager::DB_MANAGER, + update_impl, + }, + error::{code::ErrorCode, BichonResult}, + utils::net::parse_proxy_addr, + }, + raise_error, utc_now, +}; + +#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize, Object)] +#[native_model(id = 8, version = 1)] +#[native_db] +pub struct Proxy { + /// The unique identifier for this proxy configuration. + #[primary_key] + pub id: u64, + + /// The proxy URL (e.g., socks5://127.0.0.1:1080) used to route network requests. + pub url: String, + + /// The creation timestamp of this record, represented as milliseconds since the Unix epoch. + pub created_at: i64, + + /// The last update timestamp of this record, represented as milliseconds since the Unix epoch. + pub updated_at: i64, +} + +impl Proxy { + /// Create a new Proxy instance with the given URL and timestamps. + pub fn new(url: String) -> Self { + Self { + id: id!(64), + url, + created_at: utc_now!(), + updated_at: utc_now!(), + } + } + + pub async fn get(id: u64) -> BichonResult { + async_find_impl(DB_MANAGER.meta_db(), id) + .await? + .ok_or_else(|| { + raise_error!( + format!("Proxy with id={} not found", id), + ErrorCode::ResourceNotFound + ) + }) + } + + pub async fn list_all() -> BichonResult> { + list_all_impl(DB_MANAGER.meta_db()).await + } + + pub async fn delete(id: u64) -> BichonResult<()> { + delete_impl(DB_MANAGER.meta_db(), move |rw| { + rw.get() + .primary::(id) + .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))? + .ok_or_else(|| raise_error!("proxy missing".into(), ErrorCode::InternalError)) + }) + .await + } + + pub async fn update(id: u64, url: String) -> BichonResult<()> { + update_impl( + DB_MANAGER.meta_db(), + move |rw| { + rw.get() + .primary::(id) + .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))? + .ok_or_else(|| { + raise_error!( + format!("Proxy with id={} not found", id), + ErrorCode::ResourceNotFound + ) + }) + }, + move |current| { + let mut updated = current.clone(); + updated.url = url; + updated.updated_at = utc_now!(); + Ok(updated) + }, + ) + .await?; + Ok(()) + } + + pub async fn save(&self) -> BichonResult<()> { + self.validate()?; + insert_impl(DB_MANAGER.meta_db(), self.to_owned()).await + } + + /// Validate that the URL is a valid SOCKS5 proxy URL. + pub fn validate(&self) -> BichonResult<()> { + parse_proxy_addr(&self.url)?; + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_valid_proxy_urls() { + let urls = vec![ + "socks5://127.0.0.1:1080", + "http://127.0.0.1:8080", + ]; + + for url in urls { + let proxy = Proxy::new(url.to_string()); + assert!(proxy.validate().is_ok(), "URL should be valid: {}", url); + } + } +} diff --git a/src/modules/settings/system.rs b/src/modules/settings/system.rs new file mode 100644 index 0000000..4369bec --- /dev/null +++ b/src/modules/settings/system.rs @@ -0,0 +1,70 @@ +// +// Copyright (c) 2025 rustmailer.com (https://rustmailer.com) +// +// This file is part of the Bichon Email Archiving Project +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +use crate::modules::database::manager::DB_MANAGER; +use crate::modules::database::{find_impl, upsert_impl}; +use crate::modules::error::BichonResult; +use crate::utc_now; +use native_db::*; +use native_model::{native_model, Model}; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, Default, PartialEq, Eq, Deserialize, Serialize)] +#[native_model(id = 2, version = 1)] +#[native_db] +pub struct SystemSetting { + #[primary_key] + pub key: String, + pub value: String, + pub created_at: i64, + pub updated_at: i64, +} + +impl SystemSetting { + pub fn new(key: String, value: String) -> Self { + Self { + key, + value, + created_at: utc_now!(), + updated_at: utc_now!(), + } + } + //overwrite + pub async fn set(&self) -> BichonResult<()> { + upsert_impl(DB_MANAGER.meta_db(), self.to_owned()).await + } + + pub fn get(key: &str) -> BichonResult> { + find_impl(DB_MANAGER.meta_db(), key) + } + + // pub async fn list() -> RustMailerResult> { + // list_all_impl(DB_MANAGER.metadata_db()).await + // } + + pub fn get_existing_value(key: &str) -> BichonResult> { + let setting = Self::get(key)?; + Ok(setting.map(|s| s.value)) + } + + pub async fn set_value(key: &str, value: String) -> BichonResult<()> { + let setting = Self::new(key.to_string(), value); + setting.set().await + } +} diff --git a/src/modules/tasks/mod.rs b/src/modules/tasks/mod.rs new file mode 100644 index 0000000..35c31fe --- /dev/null +++ b/src/modules/tasks/mod.rs @@ -0,0 +1,29 @@ +// +// Copyright (c) 2025 rustmailer.com (https://rustmailer.com) +// +// This file is part of the Bichon Email Archiving Project +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +use crate::modules::context::RustMailTask; +use crate::modules::oauth2::{refresh::OAuth2RefreshTask, task::OAuth2CleanTask}; +pub struct PeriodicTasks; + +impl PeriodicTasks { + pub fn start_background_tasks() { + OAuth2CleanTask::start(); + OAuth2RefreshTask::start(); + } +} diff --git a/src/modules/token/mod.rs b/src/modules/token/mod.rs new file mode 100644 index 0000000..353044c --- /dev/null +++ b/src/modules/token/mod.rs @@ -0,0 +1,357 @@ +// +// Copyright (c) 2025 rustmailer.com (https://rustmailer.com) +// +// This file is part of the Bichon Email Archiving Project +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +use crate::modules::account::migration::AccountModel; +use crate::modules::database::delete_impl; +use crate::modules::database::manager::DB_MANAGER; +use crate::modules::database::{insert_impl, list_all_impl, update_impl}; +use crate::modules::token::payload::AccessTokenUpdateRequest; +use crate::raise_error; +use crate::{ + generate_token, modules::error::BichonResult, + modules::token::payload::AccessTokenCreateRequest, utc_now, +}; +use native_db::*; +use native_model::{native_model, Model}; +use poem_openapi::Object; +use serde::{Deserialize, Serialize}; +use std::collections::BTreeSet; +use std::net::IpAddr; + +use super::error::code::ErrorCode; + +pub mod payload; +pub mod root; + +#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize, Object)] +#[native_model(id = 1, version = 1)] +#[native_db] +pub struct AccessToken { + /// The unique token string used for authentication + #[primary_key] + pub token: String, + /// A set of account information associated with the token. + pub accounts: BTreeSet, + /// The timestamp (in milliseconds since epoch) when the token was created. + pub created_at: i64, + /// The timestamp (in milliseconds since epoch) when the token was last updated. + pub updated_at: i64, + /// An optional description of the token's purpose or usage. + pub description: Option, + /// The timestamp (in milliseconds since epoch) when the token was last used. + pub last_access_at: i64, + /// Optional access control settings + pub acl: Option, +} + +#[derive(Clone, Debug, Hash, PartialEq, Eq, Deserialize, Serialize, Object)] +pub struct AccountInfo { + /// The unique identifier for the account. + pub id: u64, + /// The email address associated with the account. + pub email: String, +} + +impl Ord for AccountInfo { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + self.id.cmp(&other.id) + } +} + +impl PartialOrd for AccountInfo { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize, Object)] +pub struct AccessControl { + /// An optional set of valid IPv4 or IPv6 addresses allowed to use the access token. + pub ip_whitelist: Option>, + /// An optional rate limit configuration for the access token. + pub rate_limit: Option, +} + +impl AccessControl { + pub fn validate(&self) -> BichonResult<()> { + if let Some(ip_whitelist) = &self.ip_whitelist { + for ip in ip_whitelist { + if ip.parse::().is_err() { + return Err(raise_error!( + format!("Invalid IP address: {}", ip), + ErrorCode::InvalidParameter + )); + } + } + } + + // Validate rate limit + if let Some(rate_limit) = &self.rate_limit { + if rate_limit.interval < 1 { + return Err(raise_error!( + "Rate limit interval must be at least 1 second".into(), + ErrorCode::InvalidParameter + )); + } + if rate_limit.quota < 1 { + return Err(raise_error!( + "Rate limit quota must be at least 1".into(), + ErrorCode::InvalidParameter + )); + } + } + + Ok(()) + } +} + +#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize, Object)] +pub struct RateLimit { + /// The time window in seconds for the rate limit. + pub interval: u64, + /// The maximum number of allowed requests within the time window. + pub quota: u32, +} + +impl AccessToken { + pub fn new( + token: String, + accounts: BTreeSet, + description: Option, + acl: Option, + ) -> Self { + Self { + token, + accounts, + created_at: utc_now!(), + updated_at: utc_now!(), + description, + last_access_at: Default::default(), + acl, + } + } + + pub async fn try_update_access_timestamp(token: &str) -> BichonResult { + let token = token.to_string(); + update_impl( + DB_MANAGER.meta_db(), + |rw| { + rw.get() + .primary::(token) + .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))? + .ok_or_else(|| { + raise_error!("Token not exist.".into(), ErrorCode::ResourceNotFound) + }) + }, + |current| { + let mut updated = current.clone(); + updated.last_access_at = utc_now!(); + Ok(updated) + }, + ) + .await + } + + pub async fn grant_account_access(token: &str, account: AccountInfo) -> BichonResult<()> { + let token = token.to_string(); + update_impl( + DB_MANAGER.meta_db(), + move |rw| { + rw.get() + .primary::(token.clone()) + .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))? + .ok_or_else(|| { + raise_error!( + format!( + "The access token with token={} that you want to modify was not found.", + token + ), + ErrorCode::ResourceNotFound + ) + }) + }, + |current| { + let mut updated = current.clone(); + updated.accounts.insert(account); + updated.updated_at = utc_now!(); + Ok(updated) + }, + ) + .await?; + Ok(()) + } + + pub async fn update(token: &str, request: AccessTokenUpdateRequest) -> BichonResult<()> { + if request.should_skip_update() { + return Err(raise_error!( + "No changes detected in access scopes, description, or accounts. \ + Please modify at least one of these fields to perform an update." + .into(), + ErrorCode::InvalidParameter + )); + } + request.validate().await?; + + let account_infos = if let Some(accounts) = &request.accounts { + let mut account_infos = BTreeSet::new(); + for account_id in accounts { + let account = AccountModel::get(*account_id).await?; + account_infos.insert(AccountInfo { + id: *account_id, + email: account.email, + }); + } + account_infos + } else { + BTreeSet::new() + }; + + let token = token.to_string(); + update_impl( + DB_MANAGER.meta_db(), + move |rw| { + rw.get() + .primary::(token.clone()) + .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))? + .ok_or_else(|| { + raise_error!( + format!( + "The access token with token={} that you want to modify was not found.", + token + ), + ErrorCode::ResourceNotFound + ) + }) + }, + move |current| { + let mut updated = current.clone(); + if let Some(description) = request.description { + updated.description = Some(description); + } + + if request.accounts.is_some() { + updated.accounts = account_infos; + } + + if let Some(acl) = request.acl { + updated.acl = Some(acl); + } + + updated.updated_at = utc_now!(); + Ok(updated) + }, + ) + .await?; + Ok(()) + } + + pub async fn create(request: AccessTokenCreateRequest) -> BichonResult { + // Validate request parameters first + request.validate().await?; + + let AccessTokenCreateRequest { + accounts, + description, + acl, + } = request; + + let mut account_infos = BTreeSet::new(); + for &account_id in &accounts { + let account = AccountModel::get(account_id).await?; + account_infos.insert(AccountInfo { + id: account_id, + email: account.email, + }); + } + + let token = generate_token!(128); + let access_token = AccessToken::new(token.clone(), account_infos, description, acl); + insert_impl(DB_MANAGER.meta_db(), access_token).await?; + Ok(token) + } + + pub async fn delete(token: &str) -> BichonResult<()> { + let token = token.to_string(); + delete_impl(DB_MANAGER.meta_db(), move |rw| { + rw.get() + .primary::(token.clone()) + .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))? + .ok_or_else(|| { + raise_error!( + format!("Token '{}' not found during deletion process.", token), + ErrorCode::ResourceNotFound + ) + }) + }) + .await + } + + pub async fn list_all() -> BichonResult> { + list_all_impl(DB_MANAGER.meta_db()).await + } + + pub async fn list_account_tokens(account_id: u64) -> BichonResult> { + let all = AccessToken::list_all().await?; + let result: Vec = all + .into_iter() + .filter(|e| { + e.accounts + .iter() + .any(|account_info| account_info.id == account_id) + }) + .collect(); + Ok(result) + } + + pub async fn cleanup_account(account_id: u64) -> BichonResult<()> { + let tokens = Self::list_account_tokens(account_id).await?; + if tokens.is_empty() { + return Ok(()); + } + + for token in tokens { + update_impl( + DB_MANAGER.meta_db(), + move |rw| { + rw.get() + .primary::(token.token.clone()) + .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))? + .ok_or_else(|| { + raise_error!( + format!("Cannot find access token, {}", token.token), + ErrorCode::ResourceNotFound + ) + }) + }, + move |current| { + let mut updated = current.clone(); + updated.updated_at = utc_now!(); + updated.accounts.retain(|account| account.id != account_id); + Ok(updated) + }, + ) + .await?; + } + Ok(()) + } + + pub fn can_access_account(&self, account_id: u64) -> bool { + self.accounts.iter().any(|account| account.id == account_id) + } +} diff --git a/src/modules/token/payload.rs b/src/modules/token/payload.rs new file mode 100644 index 0000000..fe7965d --- /dev/null +++ b/src/modules/token/payload.rs @@ -0,0 +1,122 @@ +// +// Copyright (c) 2025 rustmailer.com (https://rustmailer.com) +// +// This file is part of the Bichon Email Archiving Project +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +use std::collections::BTreeSet; + +use crate::{ + modules::{ + account::migration::AccountModel, + error::{code::ErrorCode, BichonResult}, + token::AccessControl, + }, + raise_error, +}; +use poem_openapi::Object; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, Default, PartialEq, Deserialize, Serialize, Object)] +pub struct AccessTokenCreateRequest { + /// A set of account information associated with the token. + pub accounts: BTreeSet, + /// An optional description of the token's purpose or usage. + #[oai(validator(max_length = "255"))] + pub description: Option, + /// Optional access control settings + pub acl: Option, +} + +impl AccessTokenCreateRequest { + pub async fn validate(&self) -> BichonResult<()> { + if let Some(acl) = &self.acl { + acl.validate()?; + } + + if self.accounts.is_empty() { + return Err(raise_error!( + "Account list cannot be empty. Please provide at least one valid account ID." + .into(), + ErrorCode::InvalidParameter + )); + } + + let mut not_found = Vec::new(); + for account_id in &self.accounts { + if AccountModel::find(*account_id).await?.is_none() { + not_found.push(*account_id); + } + } + if !not_found.is_empty() { + return Err(raise_error!( + format!("The following account IDs were not found: {}. Please provide valid account IDs.", not_found.iter().map(u64::to_string).collect::>().join(", ")).into(), + ErrorCode::InvalidParameter + )); + } + + Ok(()) + } +} + +#[derive(Clone, Debug, PartialEq, Deserialize, Serialize, Object)] +pub struct AccessTokenUpdateRequest { + /// A set of account information associated with the token. + pub accounts: Option>, + /// An optional description of the token's purpose or usage. + #[oai(validator(max_length = "255"))] + pub description: Option, + /// Optional access control settings + pub acl: Option, +} + +impl AccessTokenUpdateRequest { + pub async fn validate(&self) -> BichonResult<()> { + if let Some(acl) = &self.acl { + acl.validate()?; + } + if let Some(accounts) = &self.accounts { + if accounts.is_empty() { + return Err(raise_error!( + "Account list cannot be empty. Please provide at least one valid account ID." + .into(), + ErrorCode::InvalidParameter + )); + } + + let mut not_found = Vec::new(); + for account_id in accounts { + if AccountModel::find(*account_id).await?.is_none() { + not_found.push(*account_id); + } + } + if !not_found.is_empty() { + return Err(raise_error!( + format!("The following account IDs were not found: {}. Please provide valid account IDs.", not_found.iter().map(u64::to_string).collect::>().join(", ")).into(), + ErrorCode::InvalidParameter + )); + } + } + + Ok(()) + } +} + +impl AccessTokenUpdateRequest { + pub fn should_skip_update(&self) -> bool { + self.description.is_none() && self.accounts.is_none() && self.acl.is_none() + } +} diff --git a/src/modules/token/root.rs b/src/modules/token/root.rs new file mode 100644 index 0000000..95ddb9e --- /dev/null +++ b/src/modules/token/root.rs @@ -0,0 +1,125 @@ +// +// Copyright (c) 2025 rustmailer.com (https://rustmailer.com) +// +// This file is part of the Bichon Email Archiving Project +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +use crate::{ + decrypt, encrypt, generate_token, + modules::{ + error::{code::ErrorCode, BichonResult}, + settings::{dir::DATA_DIR_MANAGER, system::SystemSetting}, + }, + raise_error, +}; +use std::fs::File; +use std::io::Write; + +pub const ROOT_TOKEN: &str = "root-token"; +pub const ROOT_PASSWORD: &str = "root-password"; +pub const DEFAULT_ROOT_PASSWORD: &str = "root"; +pub const ROOT_TOKEN_FILE: &str = "root"; + +async fn get_or_generate( + key: &str, + generate: impl Fn() -> String, + save_file_name: Option<&str>, + force: bool, +) -> BichonResult { + if let Some(existing_value) = SystemSetting::get_existing_value(key)? { + if force { + // If force is true, write the existing value to the file + if let Some(filename) = save_file_name { + save_to_file(&existing_value.to_string(), filename).await?; + } + } + Ok(existing_value) + } else { + // If no value exists, generate a new value + let new_value = generate(); + SystemSetting::set_value(key, new_value.clone()).await?; + + // Write the new value to the file, if specified + if let Some(filename) = save_file_name { + save_to_file(&new_value.to_string(), filename).await?; + } + Ok(new_value) + } +} + +pub async fn ensure_root_token() -> BichonResult<()> { + get_or_generate( + ROOT_TOKEN, + || generate_token!(128), + Some(ROOT_TOKEN_FILE), + true, + ) + .await?; + Ok(()) +} + +pub async fn reset_root_token() -> BichonResult { + let new_token = generate_token!(128); + save_new_token(&new_token).await?; + save_to_file(&new_token, ROOT_TOKEN_FILE).await?; + Ok(new_token) +} + +async fn save_new_token(token: &str) -> BichonResult<()> { + let setting = SystemSetting::new(ROOT_TOKEN.to_string(), token.to_string()); + setting.set().await +} + +async fn save_to_file(content: &str, filename: &str) -> BichonResult<()> { + let file_path = DATA_DIR_MANAGER.root_dir.join(filename); + let mut file = File::create(&file_path) + .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))?; + writeln!(file, "{}", content) + .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))?; + Ok(()) +} + +pub fn check_root_password(password: &str) -> BichonResult { + let stored_encrypted_password = SystemSetting::get_existing_value(ROOT_PASSWORD)?; + let matched = match stored_encrypted_password { + Some(ref stored) => { + let decrypted = decrypt!(stored)?; + decrypted == password + } + None => DEFAULT_ROOT_PASSWORD == password, + }; + + if !matched { + return Err(raise_error!( + "Invalid password".into(), + ErrorCode::PermissionDenied + )); + } + + let root_token = SystemSetting::get_existing_value(ROOT_TOKEN)?.ok_or_else(|| { + raise_error!( + "Root token not found — this should never happen".into(), + ErrorCode::InternalError + ) + })?; + + Ok(root_token) +} + +pub async fn set_root_password(new_password: &str) -> BichonResult<()> { + let encrypted_password = encrypt!(new_password)?; + SystemSetting::set_value(ROOT_PASSWORD, encrypted_password).await +} diff --git a/src/modules/utils/encrypt.rs b/src/modules/utils/encrypt.rs new file mode 100644 index 0000000..61e0267 --- /dev/null +++ b/src/modules/utils/encrypt.rs @@ -0,0 +1,131 @@ +// +// Copyright (c) 2025 rustmailer.com (https://rustmailer.com) +// +// This file is part of the Bichon Email Archiving Project +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +use base64::{engine::general_purpose, Engine as _}; +use ring::aead::{Aad, BoundKey, Nonce, NonceSequence, OpeningKey, SealingKey, AES_256_GCM}; +use ring::pbkdf2::{self, derive}; +use ring::rand::{SecureRandom, SystemRandom}; +use std::num::NonZeroU32; + +use crate::modules::error::code::ErrorCode; +use crate::modules::error::BichonResult; +use crate::modules::settings::cli::SETTINGS; +use crate::raise_error; + +struct SingleNonceSequence([u8; 12]); + +impl SingleNonceSequence { + fn new(nonce: [u8; 12]) -> Self { + SingleNonceSequence(nonce) + } +} + +impl NonceSequence for SingleNonceSequence { + fn advance(&mut self) -> Result { + Ok(Nonce::assume_unique_for_key(self.0)) + } +} + +pub fn encrypt_string(plaintext: &str) -> BichonResult { + internal_encrypt_string(&SETTINGS.bichon_encrypt_password, plaintext) + .map_err(|_| raise_error!("Failed to encrypt string.".into(), ErrorCode::InternalError)) +} + +pub fn decrypt_string(data: &str) -> BichonResult { + internal_decrypt_string(&SETTINGS.bichon_encrypt_password, data).map_err(|_| { + raise_error!( + "Decryption failed, likely due to incorrect encryption key or corrupted data".into(), + ErrorCode::InternalError + ) + }) +} + +fn internal_encrypt_string( + password: &str, + plaintext: &str, +) -> Result { + let rng = SystemRandom::new(); + let mut salt = [0u8; 32]; + rng.fill(&mut salt)?; + let mut key = [0u8; 32]; + derive( + pbkdf2::PBKDF2_HMAC_SHA256, + NonZeroU32::new(100_000).unwrap(), + &salt, + password.as_bytes(), + &mut key, + ); + let mut nonce_bytes = [0u8; 12]; + rng.fill(&mut nonce_bytes)?; + let unbound_key = ring::aead::UnboundKey::new(&AES_256_GCM, &key)?; + let nonce_sequence = SingleNonceSequence::new(nonce_bytes); + let mut sealing_key = SealingKey::new(unbound_key, nonce_sequence); + let mut in_out = plaintext.as_bytes().to_vec(); + let aad = Aad::empty(); + sealing_key.seal_in_place_append_tag(aad, &mut in_out)?; + let mut result = Vec::with_capacity(32 + 12 + in_out.len()); + result.extend_from_slice(&salt); + result.extend_from_slice(&nonce_bytes); + result.extend_from_slice(&in_out); + Ok(general_purpose::URL_SAFE.encode(&result)) +} + +fn internal_decrypt_string(password: &str, data: &str) -> Result { + let data = general_purpose::URL_SAFE + .decode(data) + .map_err(|_| ring::error::Unspecified)?; + if data.len() < 32 + 12 { + return Err(ring::error::Unspecified); + } + let salt = &data[0..32]; + let nonce_bytes: [u8; 12] = data[32..44] + .try_into() + .map_err(|_| ring::error::Unspecified)?; + let ciphertext = &data[44..]; + let mut key = [0u8; 32]; + derive( + pbkdf2::PBKDF2_HMAC_SHA256, + NonZeroU32::new(100_000).unwrap(), + salt, + password.as_bytes(), + &mut key, + ); + let unbound_key = ring::aead::UnboundKey::new(&AES_256_GCM, &key)?; + let nonce_sequence = SingleNonceSequence::new(nonce_bytes); + let mut opening_key = OpeningKey::new(unbound_key, nonce_sequence); + let mut in_out = ciphertext.to_vec(); + let aad = Aad::empty(); + let decrypted_bytes = opening_key.open_in_place(aad, &mut in_out)?; + String::from_utf8(decrypted_bytes.to_vec()).map_err(|_| ring::error::Unspecified) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_encrypt_decrypt() { + let password = "my_secure_passwasdasdasdasdasord"; + let plaintext = "Helloasdasdasdasdasd, World!"; + let encrypted = internal_encrypt_string(password, plaintext).unwrap(); + println!("{}", &encrypted); + let decrypted = internal_decrypt_string(password, &encrypted).unwrap(); + assert_eq!(decrypted, plaintext); + } +} diff --git a/src/modules/utils/mod.rs b/src/modules/utils/mod.rs new file mode 100644 index 0000000..c35659c --- /dev/null +++ b/src/modules/utils/mod.rs @@ -0,0 +1,312 @@ +// +// Copyright (c) 2025 rustmailer.com (https://rustmailer.com) +// +// This file is part of the Bichon Email Archiving Project +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +use std::{fs, io, path::PathBuf}; + +use base64::{engine::general_purpose, Engine}; +use rand::{rng, Rng}; + +use super::error::code::ErrorCode; + +pub mod encrypt; +pub mod net; +pub mod rate_limit; +pub mod shutdown; +pub mod tls; + +#[macro_export] +macro_rules! bichon_version { + () => { + env!("CARGO_PKG_VERSION") + }; +} + +#[macro_export] +macro_rules! utc_now { + () => {{ + use chrono::Utc; + Utc::now().timestamp_millis() + }}; +} + +#[macro_export] +macro_rules! after_n_days_timestamp { + ($start_ts:expr, $days:expr) => {{ + const MILLIS_PER_DAY: i64 = 86_400_000; // 24 * 60 * 60 * 1000 + $start_ts + ($days as i64) * MILLIS_PER_DAY + }}; +} + +#[macro_export] +macro_rules! base64_encode { + ($bytes:expr) => {{ + use base64::{engine::general_purpose::STANDARD, *}; + STANDARD.encode($bytes) + }}; +} + +#[macro_export] +macro_rules! base64_decode { + ($key:expr) => {{ + use base64::{engine::general_purpose::STANDARD, *}; + STANDARD.decode($key).unwrap() + }}; +} + +#[macro_export] +macro_rules! base64_decode_url_safe { + ($key:expr) => {{ + use base64::{engine::general_purpose::URL_SAFE, *}; + URL_SAFE.decode($key) + }}; +} + +#[macro_export] +macro_rules! base64_encode_url_safe { + ($key:expr) => {{ + use base64::{engine::general_purpose::URL_SAFE, *}; + URL_SAFE.encode($key) + }}; +} + +#[macro_export] +macro_rules! product_public_key { + () => { + $crate::base64_decode!(r#"BNlT+WjdEls9VGfry+zKygx+UoypxSqsMBddMGxYgbhWOz7Xfh7YJXGMeby9jBtbz3rhSGrTuZCYA9uwwMMYkhI="#) + }; +} + +#[macro_export] +macro_rules! license_header { + () => { + "{\"alg\":\"ES256\",\"typ\":\"JWT\"}" + }; +} + +#[macro_export] +macro_rules! raise_error { + ($msg:expr, $code:expr) => { + $crate::modules::error::BichonError::Generic { + message: $msg, + location: snafu::Location::default(), + code: $code, + } + }; +} +#[macro_export] +macro_rules! run_with_timeout { + ($duration:expr, $task:expr, $err_msg:expr) => {{ + match tokio::time::timeout($duration, $task).await { + Ok(result) => Ok(result), + Err(_) => Err($err_msg), + } + }}; +} + +#[macro_export] +macro_rules! free_memory { + () => {{ + let mut sys = sysinfo::System::new_all(); + sys.refresh_memory(); + sys.free_memory() + }}; +} + +#[macro_export] +macro_rules! generate_token { + ($bit_strength:expr) => {{ + $crate::modules::utils::generate_token_impl($bit_strength) + }}; +} + +pub(crate) fn generate_token_impl(bit_strength: usize) -> String { + let byte_length = (bit_strength + 23) / 24 * 3; + let random_bytes: Vec = (0..byte_length).map(|_| rand::random::()).collect(); + let mut encoded = general_purpose::URL_SAFE.encode(&random_bytes); + + encoded = encoded + .chars() + .map(|c| { + if c == '/' || c == '+' || c == '-' || c == '_' { + make_single_random_char() + } else { + c + } + }) + .collect(); + + encoded +} + +fn make_single_random_char() -> char { + let random_bytes: [u8; 3] = rng().random(); + let encoded = general_purpose::URL_SAFE.encode(random_bytes); + encoded + .chars() + .find(|&c| c != '-' && c != '_' && c != '+' && c != '/') + .unwrap_or('a') +} + +#[macro_export] +macro_rules! ensure_access { + ($dir:expr) => {{ + $crate::modules::utils::ensure_dir_and_test_access($dir) + }}; +} + +#[macro_export] +macro_rules! decode_mailbox_name { + ($name:expr) => {{ + utf7_imap::decode_utf7_imap($name.to_string()) + }}; +} +#[macro_export] +macro_rules! encode_mailbox_name { + ($name:expr) => {{ + utf7_imap::encode_utf7_imap($name.to_string()) + }}; +} + +#[macro_export] +macro_rules! get_encoding { + ($label:expr) => { + match encoding_rs::Encoding::for_label($label.as_bytes()) { + None => None, + Some(encoding) => Some(encoding), + } + }; +} + +#[macro_export] +macro_rules! current_datetime { + () => {{ + use chrono::Local; + let now = Local::now(); + now.format("%Y%m%d%H%M").to_string() + }}; +} + +#[macro_export] +macro_rules! validate_email { + ($email:expr) => {{ + $crate::modules::utils::validate_email($email) + }}; +} + +#[macro_export] +macro_rules! encrypt { + ($plaintext:expr) => {{ + $crate::modules::utils::encrypt::encrypt_string($plaintext) + }}; +} + +#[macro_export] +macro_rules! decrypt { + ($plaintext:expr) => {{ + $crate::modules::utils::encrypt::decrypt_string($plaintext) + }}; +} + +pub fn validate_email(email: &str) -> crate::modules::error::BichonResult<()> { + use std::str::FromStr; + let email_address = email_address::EmailAddress::from_str(email).map_err(|_| { + raise_error!( + format!("Invalid email format : {}", email), + ErrorCode::InvalidParameter + ) + })?; + if email != email_address.email() { + return Err(raise_error!( + format!("Invalid email format: {}", email), + ErrorCode::InvalidParameter + )); + } + Ok(()) +} + +#[macro_export] +macro_rules! calculate_hash { + ($name:expr) => { + $crate::modules::utils::hash($name) + }; +} + +#[macro_export] +macro_rules! id { + ($bit_strength:expr) => {{ + // Generate a token with the given bit strength + let token = $crate::modules::utils::generate_token_impl($bit_strength); + // Hash the generated token + $crate::modules::utils::hash(&token) + }}; +} + +#[macro_export] +macro_rules! u64_to_str { + ($id:expr) => {{ + let mut buf = itoa::Buffer::new(); + buf.format($id) + }}; +} + +/// Generates a 64-bit hash from a string, ensuring the output is within JavaScript's safe integer range (0 to 2^53 - 1). +pub fn hash(s: &str) -> u64 { + let mut cursor = Vec::new(); + cursor.extend_from_slice(s.as_bytes()); + let mut cursor = std::io::Cursor::new(cursor); + let hash = murmur3::murmur3_x64_128(&mut cursor, 0).unwrap(); + (hash & 0x1F_FFFF_FFFF_FFFF) as u64 +} + +pub fn create_hash(account_id: u64, field: &str) -> u64 { + // Construct a buffer of bytes from account_id and mailbox_name + let mut buffer = Vec::new(); + buffer.extend_from_slice(&account_id.to_le_bytes()); // Convert u64 to bytes + buffer.push(b':'); // Separator + buffer.extend_from_slice(field.as_bytes()); // Add mailbox name + // Create a Cursor for the buffer + let mut cursor = std::io::Cursor::new(buffer); + // Compute the 128-bit Murmur3 hash and cast to u64 + let hash = murmur3::murmur3_x64_128(&mut cursor, 0).unwrap(); + (hash & 0x1F_FFFF_FFFF_FFFF) as u64 +} + +pub fn get_total_size(path: &PathBuf) -> io::Result { + if !path.exists() { + return Ok(0); + } + + if path.is_file() { + return Ok(fs::metadata(path)?.len()); + } + + let mut total_size = 0u64; + for entry in fs::read_dir(path)? { + let entry = entry?; + let entry_path = entry.path(); + + if entry_path.is_file() { + total_size += fs::metadata(&entry_path)?.len(); + } else if entry_path.is_dir() { + total_size += get_total_size(&entry_path)?; + } + } + + Ok(total_size) +} diff --git a/src/modules/utils/net.rs b/src/modules/utils/net.rs new file mode 100644 index 0000000..e11b51e --- /dev/null +++ b/src/modules/utils/net.rs @@ -0,0 +1,163 @@ +// +// Copyright (c) 2025 rustmailer.com (https://rustmailer.com) +// +// This file is part of the Bichon Email Archiving Project +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +use crate::modules::error::code::ErrorCode; +use crate::modules::settings::proxy::Proxy; +use crate::modules::utils::tls::establish_tls_stream; +use crate::modules::{error::BichonResult, imap::session::SessionStream}; +use crate::raise_error; +use std::net::SocketAddr; +use std::pin::Pin; +use std::time::Duration; +use tokio::net::TcpStream; +use tokio::time::timeout; +use tokio_io_timeout::TimeoutStream; +use tokio_socks::tcp::Socks5Stream; +use tracing::error; + +pub(crate) const TIMEOUT: Duration = Duration::from_secs(60); + +pub(crate) async fn establish_tcp_connection_with_timeout( + address: SocketAddr, + use_proxy: Option, +) -> BichonResult>>> { + // Establish the TCP connection with a timeout + let tcp_stream = connect_with_optional_proxy(use_proxy, address).await?; + + // Disable Nagle's algorithm for more efficient network communication + tcp_stream + .set_nodelay(true) + .map_err(|e| raise_error!(e.to_string(), ErrorCode::NetworkError))?; + + // Wrap the TCP stream in a TimeoutStream for timeout management + let mut timeout_stream = TimeoutStream::new(tcp_stream); + + // Set read and write timeouts + timeout_stream.set_write_timeout(Some(TIMEOUT)); + timeout_stream.set_read_timeout(Some(TIMEOUT)); + + // Return the timeout-wrapped TCP stream as a Pin + Ok(Box::pin(timeout_stream)) +} + +pub(crate) async fn establish_tls_connection( + address: SocketAddr, + server_hostname: &str, + alpn_protocols: &[&str], + use_proxy: Option, +) -> BichonResult { + // Establish the TCP connection with timeout + let tcp_stream = establish_tcp_connection_with_timeout(address, use_proxy).await?; + + // Wrap the TCP stream with TLS encryption + let tls_stream = establish_tls_stream(server_hostname, alpn_protocols, tcp_stream).await?; + + // Return the TLS stream wrapped in a SessionStream + Ok(tls_stream) +} + +pub fn parse_proxy_addr(input: &str) -> BichonResult { + // Normalize and check protocol prefix + let (scheme, stripped) = if let Some(rest) = input + .strip_prefix("socks5://") + .or_else(|| input.strip_prefix("SOCKS5://")) + .or_else(|| input.strip_prefix("Socks5://")) + { + ("socks5", rest) + } else if let Some(rest) = input + .strip_prefix("http://") + .or_else(|| input.strip_prefix("HTTP://")) + .or_else(|| input.strip_prefix("Http://")) + { + ("http", rest) + } else { + return Err(raise_error!( + format!( + "Invalid proxy URL: must start with 'http://' or 'socks5://', got '{}'", + input + ), + ErrorCode::InvalidParameter + )); + }; + + // Parse the remaining address + let addr = stripped.parse::().map_err(|e| { + raise_error!( + format!( + "Failed to parse {} proxy address '{}': {}", + scheme, stripped, e + ), + ErrorCode::InvalidParameter + ) + })?; + + Ok(addr) +} + +/// Try to connect via SOCKS5 proxy or TCP with timeout +async fn connect_with_optional_proxy( + use_proxy: Option, + address: SocketAddr, +) -> BichonResult { + // Try if proxy is enabled + if let Some(proxy_id) = use_proxy { + let proxy = Proxy::get(proxy_id).await?; + let proxy = parse_proxy_addr(&proxy.url)?; + return timeout(TIMEOUT, Socks5Stream::connect(proxy, address)) + .await + .map_err(|_| { + error!( + "SOCKS5 proxy connection to {} via {} timed out after {}s", + address, + proxy, + TIMEOUT.as_secs() + ); + raise_error!( + format!( + "SOCKS5 proxy connection to {} via {} timed out after {}s", + address, + proxy, + TIMEOUT.as_secs() + ), + ErrorCode::ConnectionTimeout + ) + })? + .map(|s| s.into_inner()) + .map_err(|e| raise_error!(e.to_string(), ErrorCode::NetworkError)); + } + // Fallback to direct TCP connection + timeout(TIMEOUT, TcpStream::connect(address)) + .await + .map_err(|_| { + error!( + "TCP connection to {} timed out after {}s", + address, + TIMEOUT.as_secs() + ); + raise_error!( + format!( + "TCP connection to {} timed out after {}s", + address, + TIMEOUT.as_secs() + ), + ErrorCode::ConnectionTimeout + ) + })? + .map_err(|e| raise_error!(e.to_string(), ErrorCode::NetworkError)) +} diff --git a/src/modules/utils/rate_limit/mod.rs b/src/modules/utils/rate_limit/mod.rs new file mode 100644 index 0000000..cf6dadf --- /dev/null +++ b/src/modules/utils/rate_limit/mod.rs @@ -0,0 +1,103 @@ +// +// Copyright (c) 2025 rustmailer.com (https://rustmailer.com) +// +// This file is part of the Bichon Email Archiving Project +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +use dashmap::DashMap; +use governor::{ + clock::{QuantaClock, QuantaInstant}, + middleware::NoOpMiddleware, + state::{InMemoryState, NotKeyed}, + NotUntil, Quota, RateLimiter, +}; +use std::{ + num::NonZero, + sync::{Arc, LazyLock}, + time::Duration, +}; + +use crate::modules::token::RateLimit; + +pub static RATE_LIMITER_MANAGER: LazyLock = LazyLock::new(TokenRateLimiter::new); + +pub struct TokenRateLimiter { + limiters: Arc< + DashMap< + String, + ( + Arc>, + RateLimit, + ), + >, + >, +} + +impl TokenRateLimiter { + pub fn new() -> Self { + TokenRateLimiter { + limiters: Arc::new(DashMap::new()), + } + } + + pub async fn check( + &self, + token: &str, + limit: RateLimit, + ) -> Result<(), NotUntil> { + let limiter = self.get_or_update_limiter(token, limit).await; + limiter.check() + } + + async fn get_or_update_limiter( + &self, + token: &str, + limit: RateLimit, + ) -> Arc> { + self.limiters + .entry(token.to_string()) + .and_modify(|(existing_limiter, current_limit)| { + if current_limit.interval != limit.interval || current_limit.quota != limit.quota { + let quota = Quota::with_period(Duration::from_secs(limit.interval)) + .unwrap() + .allow_burst(NonZero::new(limit.quota).unwrap()); + *existing_limiter = Arc::new(RateLimiter::direct_with_clock( + quota, + QuantaClock::default(), + )); + *current_limit = RateLimit { + interval: limit.interval, + quota: limit.quota, + }; + } + }) + .or_insert({ + let quota = Quota::with_period(Duration::from_secs(limit.interval)) + .unwrap() + .allow_burst(NonZero::new(limit.quota).unwrap()); + ( + Arc::new(RateLimiter::direct_with_clock( + quota, + QuantaClock::default(), + )), + limit, + ) + }) + .value() + .0 + .clone() + } +} \ No newline at end of file diff --git a/src/modules/utils/shutdown.rs b/src/modules/utils/shutdown.rs new file mode 100644 index 0000000..81c5dc3 --- /dev/null +++ b/src/modules/utils/shutdown.rs @@ -0,0 +1,44 @@ +// +// Copyright (c) 2025 rustmailer.com (https://rustmailer.com) +// +// This file is part of the Bichon Email Archiving Project +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +use tokio::signal; + +pub(crate) async fn shutdown_signal() { + let ctrl_c_signal = async { + signal::ctrl_c() + .await + .expect("Error installing Ctrl+C signal handler"); + }; + + #[cfg(unix)] + let terminate_signal = async { + signal::unix::signal(signal::unix::SignalKind::terminate()) + .expect("Error installing terminate signal handler") + .recv() + .await; + }; + + #[cfg(not(unix))] + let terminate_signal = std::future::pending::<()>(); + + tokio::select! { + _ = ctrl_c_signal => {}, + _ = terminate_signal => {}, + }; +} diff --git a/src/modules/utils/tls.rs b/src/modules/utils/tls.rs new file mode 100644 index 0000000..5f11587 --- /dev/null +++ b/src/modules/utils/tls.rs @@ -0,0 +1,78 @@ +// +// Copyright (c) 2025 rustmailer.com (https://rustmailer.com) +// +// This file is part of the Bichon Email Archiving Project +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +use crate::{ + modules::{ + error::{code::ErrorCode, BichonResult}, + imap::session::SessionStream, + }, + raise_error, +}; +use rustls::RootCertStore; +use std::sync::Arc; + +pub async fn establish_tls_stream( + server_hostname: &str, + alpn_protocols: &[&str], + stream: impl SessionStream + 'static, +) -> BichonResult { + let tls_stream = establish_rustls_stream(server_hostname, alpn_protocols, stream).await?; + let boxed_stream: Box = Box::new(tls_stream); + Ok(boxed_stream) +} + +pub async fn establish_rustls_stream( + server_hostname: &str, + alpn_protocols: &[&str], + stream: impl SessionStream, +) -> BichonResult { + // Create a root certificate store and add default trusted roots + let root_store = RootCertStore { + roots: webpki_roots::TLS_SERVER_ROOTS.into(), + }; + + // Configure the Rustls client with the root certs and no client authentication + let mut config = rustls::ClientConfig::builder() + //builder_with_provider( + // rustls::crypto::ring::default_provider().into(), + // ) + // .with_protocol_versions(&[&rustls::version::TLS13]) + // .unwrap() + .with_root_certificates(root_store) + .with_no_client_auth(); + + // Set the ALPN protocols + config.alpn_protocols = alpn_protocols + .iter() + .map(|s| s.as_bytes().to_vec()) + .collect(); + + let tls_connector = tokio_rustls::TlsConnector::from(Arc::new(config)); + + let server_name = rustls_pki_types::ServerName::try_from(server_hostname) + .map_err(|_| raise_error!("Invalid DNS name".into(), ErrorCode::NetworkError))? + .to_owned(); + + let tls_stream = tls_connector + .connect(server_name, stream) + .await + .map_err(|e| raise_error!(e.to_string(), ErrorCode::NetworkError))?; + + Ok(tls_stream) +} diff --git a/src/modules/version/mod.rs b/src/modules/version/mod.rs new file mode 100644 index 0000000..608cdb9 --- /dev/null +++ b/src/modules/version/mod.rs @@ -0,0 +1,118 @@ +// +// Copyright (c) 2025 rustmailer.com (https://rustmailer.com) +// +// This file is part of the Bichon Email Archiving Project +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +use poem_openapi::Object; +use serde::{Deserialize, Serialize}; + +use crate::{modules::error::BichonResult, bichon_version}; + +#[derive(Debug, Serialize, Deserialize, Object)] +pub struct ReleaseNotification { + /// Details of the latest release, if available. `None` if no release data is available. + pub latest: Option, + /// Indicates whether the latest release is newer than the current RustMailer service version. + pub is_newer: bool, + /// Optional error message if the release check failed (e.g., network or API issues). + pub error_message: Option, +} + +#[derive(Debug, Serialize, Deserialize, Object)] +pub struct Release { + /// The tag name of the release (e.g., "v1.2.3"). + pub tag_name: String, + /// The publication date of the release in string format (e.g., ISO 8601 format). + pub published_at: String, + /// The body of the release notes, typically in Markdown format. + pub body: String, + /// The URL to the release's web page (e.g., GitHub release page). + pub html_url: String, +} + +#[derive(Debug, Serialize, Deserialize, Object)] +pub struct Notifications { + pub release: ReleaseNotification, +} + +pub async fn fetch_notifications() -> BichonResult { + let current_version = bichon_version!(); + let release = check_new_release("rustmailer", "bichon", current_version).await; + Ok(Notifications { release }) +} + +async fn check_new_release(owner: &str, repo: &str, current_version: &str) -> ReleaseNotification { + let url = format!( + "https://api.github.com/repos/{}/{}/releases/latest", + owner, repo + ); + + let client = reqwest::Client::new(); + let response = match client + .get(&url) + .header("User-Agent", "reqwest") + .send() + .await + { + Ok(resp) => resp, + Err(e) => { + return ReleaseNotification { + latest: None, + is_newer: false, + error_message: Some(format!("Failed to send request: {}", e)), + } + } + }; + + if response.status().is_success() { + match response.json::().await { + Ok(release) => { + let is_newer = release.tag_name != current_version; + ReleaseNotification { + latest: Some(release), + is_newer, + error_message: None, + } + } + Err(e) => ReleaseNotification { + latest: None, + is_newer: false, + error_message: Some(format!("Failed to parse response: {}", e)), + }, + } + } else { + ReleaseNotification { + latest: None, + is_newer: false, + error_message: Some(format!("Request failed with status: {}", response.status())), + } + } +} + +#[cfg(test)] +mod test { + use crate::{modules::version::check_new_release, bichon_version}; + + #[tokio::test] + async fn test() { + let current_version = bichon_version!(); + println!("current_version: {}", bichon_version!()); + let result = check_new_release("rustmailer", "persistent-scheduler", current_version).await; + + println!("{:#?}", result); + } +} diff --git a/web/.gitignore b/web/.gitignore new file mode 100644 index 0000000..db4c6d9 --- /dev/null +++ b/web/.gitignore @@ -0,0 +1,2 @@ +dist +node_modules \ No newline at end of file diff --git a/web/.prettierignore b/web/.prettierignore new file mode 100644 index 0000000..dc943a1 --- /dev/null +++ b/web/.prettierignore @@ -0,0 +1,18 @@ +# Ignore everything +/* + +# Except these files & folders +!/src +!index.html +!package.json +!tailwind.config.js +!tsconfig.json +!tsconfig.node.json +!vite.config.ts +!.prettierrc +!README.md +!eslint.config.js +!postcss.config.js + +# Ignore auto generated routeTree.gen.ts +/src/routeTree.gen.ts \ No newline at end of file diff --git a/web/.prettierrc b/web/.prettierrc new file mode 100644 index 0000000..3cc15ba --- /dev/null +++ b/web/.prettierrc @@ -0,0 +1,51 @@ +{ + "arrowParens": "always", + "semi": false, + "tabWidth": 2, + "printWidth": 80, + "singleQuote": true, + "jsxSingleQuote": true, + "trailingComma": "es5", + "bracketSpacing": true, + "endOfLine": "lf", + "plugins": [ + "prettier-plugin-tailwindcss", + "@trivago/prettier-plugin-sort-imports" + ], + "importOrder": [ + "^path$", + "^vite$", + "^@vitejs/(.*)$", + "^react$", + "^react-dom/client$", + "^react/(.*)$", + "^globals$", + "^zod$", + "^axios$", + "^date-fns$", + "^js-cookie$", + "^react-hook-form$", + "^use-intl$", + "^@radix-ui/(.*)$", + "^@hookform/resolvers/zod$", + "^@tanstack/react-query$", + "^@tanstack/react-router$", + "^@tanstack/react-table$", + "^@tabler/icons-react$", + "", + "^@/assets/(.*)", + "^@/api/(.*)$", + "^@/stores/(.*)$", + "^@/lib/(.*)$", + "^@/utils/(.*)$", + "^@/constants/(.*)$", + "^@/context/(.*)$", + "^@/hooks/(.*)$", + "^@/components/layouts/(.*)$", + "^@/components/ui/(.*)$", + "^@/components/errors/(.*)$", + "^@/components/(.*)$", + "^@/features/(.*)$", + "^[./]" + ] +} diff --git a/web/components.json b/web/components.json new file mode 100644 index 0000000..299ea38 --- /dev/null +++ b/web/components.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": false, + "tsx": true, + "tailwind": { + "config": "tailwind.config.js", + "css": "src/index.css", + "baseColor": "slate", + "cssVariables": true, + "prefix": "" + }, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + }, + "iconLibrary": "lucide" +} \ No newline at end of file diff --git a/web/cz.yaml b/web/cz.yaml new file mode 100644 index 0000000..cc639d6 --- /dev/null +++ b/web/cz.yaml @@ -0,0 +1,7 @@ +--- +commitizen: + name: cz_conventional_commits + tag_format: v$version + update_changelog_on_bump: true + version_provider: npm + version_scheme: semver diff --git a/web/eslint.config.js b/web/eslint.config.js new file mode 100644 index 0000000..81920b6 --- /dev/null +++ b/web/eslint.config.js @@ -0,0 +1,34 @@ +import globals from 'globals' +import js from '@eslint/js' +import pluginQuery from '@tanstack/eslint-plugin-query' +import reactHooks from 'eslint-plugin-react-hooks' +import reactRefresh from 'eslint-plugin-react-refresh' +import tseslint from 'typescript-eslint' + +export default tseslint.config( + { ignores: ['dist', 'src/components/ui'] }, + { + extends: [ + js.configs.recommended, + ...tseslint.configs.recommended, + ...pluginQuery.configs['flat/recommended'], + ], + files: ['**/*.{ts,tsx}'], + languageOptions: { + ecmaVersion: 2020, + globals: globals.browser, + }, + plugins: { + 'react-hooks': reactHooks, + 'react-refresh': reactRefresh, + }, + rules: { + ...reactHooks.configs.recommended.rules, + 'react-refresh/only-export-components': [ + 'warn', + { allowConstantExport: true }, + ], + 'no-console': 'error', + }, + } +) diff --git a/web/index.html b/web/index.html new file mode 100644 index 0000000..fc0a40d --- /dev/null +++ b/web/index.html @@ -0,0 +1,20 @@ + + + + + + + + + Bichon + + + + + + +
+ + + + \ No newline at end of file diff --git a/web/knip.config.ts b/web/knip.config.ts new file mode 100644 index 0000000..f47aa12 --- /dev/null +++ b/web/knip.config.ts @@ -0,0 +1,7 @@ +import type { KnipConfig } from 'knip'; + +const config: KnipConfig = { + ignore: ['src/components/ui/**', 'src/routeTree.gen.ts'] +}; + +export default config; \ No newline at end of file diff --git a/web/package.json b/web/package.json new file mode 100644 index 0000000..2dba5c9 --- /dev/null +++ b/web/package.json @@ -0,0 +1,98 @@ +{ + "private": true, + "name": "bichon-ui", + "version": "0.0.1", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "lint": "eslint .", + "preview": "vite preview", + "format:check": "prettier --check .", + "format": "prettier --write .", + "knip": "knip" + }, + "dependencies": { + "@hookform/resolvers": "^3.9.1", + "@radix-ui/react-accordion": "^1.2.2", + "@radix-ui/react-alert-dialog": "^1.1.2", + "@radix-ui/react-avatar": "^1.1.1", + "@radix-ui/react-checkbox": "^1.1.2", + "@radix-ui/react-collapsible": "^1.1.1", + "@radix-ui/react-dialog": "^1.1.2", + "@radix-ui/react-dropdown-menu": "^2.1.2", + "@radix-ui/react-hover-card": "^1.1.4", + "@radix-ui/react-icons": "^1.3.2", + "@radix-ui/react-label": "^2.1.0", + "@radix-ui/react-navigation-menu": "^1.2.5", + "@radix-ui/react-popover": "^1.1.2", + "@radix-ui/react-progress": "^1.1.8", + "@radix-ui/react-radio-group": "^1.2.1", + "@radix-ui/react-scroll-area": "^1.2.1", + "@radix-ui/react-select": "^2.1.2", + "@radix-ui/react-separator": "^1.1.0", + "@radix-ui/react-slot": "^1.1.0", + "@radix-ui/react-switch": "^1.1.1", + "@radix-ui/react-tabs": "^1.1.1", + "@radix-ui/react-toast": "^1.2.2", + "@radix-ui/react-tooltip": "^1.1.4", + "@radix-ui/react-visually-hidden": "^1.1.0", + "@tabler/icons-react": "^3.24.0", + "@tanstack/react-query": "^5.62.3", + "@tanstack/react-router": "^1.86.1", + "@tanstack/react-table": "^8.20.5", + "@tanstack/react-virtual": "^3.11.2", + "ace-builds": "^1.37.1", + "axios": "^1.7.9", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "cmdk": "1.0.4", + "date-fns": "^3.6.0", + "file-saver": "^2.0.5", + "handlebars": "^4.7.8", + "js-cookie": "^3.0.5", + "lucide-react": "^0.468.0", + "react": "^18.3.1", + "react-ace": "^13.0.0", + "react-day-picker": "8.10.1", + "react-dom": "^18.3.1", + "react-hook-form": "^7.54.0", + "react-markdown": "^10.1.0", + "react-resizable-panels": "^2.1.7", + "recharts": "^2.14.1", + "remark-gfm": "^4.0.1", + "tailwind-merge": "^2.5.5", + "tailwindcss-animate": "^1.0.7", + "vaul": "^1.1.2", + "zod": "^3.23.8", + "zustand": "^5.0.2" + }, + "devDependencies": { + "@eslint/js": "^9.16.0", + "@faker-js/faker": "^9.3.0", + "@tanstack/eslint-plugin-query": "^5.62.1", + "@tanstack/react-query-devtools": "^5.62.3", + "@tanstack/router-devtools": "^1.86.1", + "@tanstack/router-plugin": "^1.86.0", + "@trivago/prettier-plugin-sort-imports": "^4.3.0", + "@types/file-saver": "^2.0.7", + "@types/js-cookie": "^3.0.6", + "@types/node": "^22.10.1", + "@types/react": "^18.3.18", + "@types/react-dom": "^18.3.5", + "@vitejs/plugin-react-swc": "^3.7.2", + "autoprefixer": "^10.4.20", + "eslint": "^9.16.0", + "eslint-plugin-react-hooks": "^5.1.0", + "eslint-plugin-react-refresh": "^0.4.16", + "globals": "^15.13.0", + "knip": "^5.41.1", + "postcss": "^8.4.49", + "prettier": "^3.4.2", + "prettier-plugin-tailwindcss": "^0.6.9", + "tailwindcss": "^3.4.16", + "typescript": "~5.7.2", + "typescript-eslint": "^8.17.0", + "vite": "^6.0.11" + } +} \ No newline at end of file diff --git a/web/pnpm-lock.yaml b/web/pnpm-lock.yaml new file mode 100644 index 0000000..8d26575 --- /dev/null +++ b/web/pnpm-lock.yaml @@ -0,0 +1,7645 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@hookform/resolvers': + specifier: ^3.9.1 + version: 3.9.1(react-hook-form@7.54.0(react@18.3.1)) + '@radix-ui/react-accordion': + specifier: ^1.2.2 + version: 1.2.2(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-alert-dialog': + specifier: ^1.1.2 + version: 1.1.2(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-avatar': + specifier: ^1.1.1 + version: 1.1.1(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-checkbox': + specifier: ^1.1.2 + version: 1.1.2(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-collapsible': + specifier: ^1.1.1 + version: 1.1.1(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-dialog': + specifier: ^1.1.2 + version: 1.1.2(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-dropdown-menu': + specifier: ^2.1.2 + version: 2.1.2(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-hover-card': + specifier: ^1.1.4 + version: 1.1.4(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-icons': + specifier: ^1.3.2 + version: 1.3.2(react@18.3.1) + '@radix-ui/react-label': + specifier: ^2.1.0 + version: 2.1.0(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-navigation-menu': + specifier: ^1.2.5 + version: 1.2.5(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-popover': + specifier: ^1.1.2 + version: 1.1.2(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-progress': + specifier: ^1.1.8 + version: 1.1.8(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-radio-group': + specifier: ^1.2.1 + version: 1.2.1(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-scroll-area': + specifier: ^1.2.1 + version: 1.2.1(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-select': + specifier: ^2.1.2 + version: 2.1.2(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-separator': + specifier: ^1.1.0 + version: 1.1.0(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-slot': + specifier: ^1.1.0 + version: 1.1.0(@types/react@18.3.18)(react@18.3.1) + '@radix-ui/react-switch': + specifier: ^1.1.1 + version: 1.1.1(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-tabs': + specifier: ^1.1.1 + version: 1.1.1(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-toast': + specifier: ^1.2.2 + version: 1.2.2(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-tooltip': + specifier: ^1.1.4 + version: 1.1.4(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-visually-hidden': + specifier: ^1.1.0 + version: 1.1.0(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@tabler/icons-react': + specifier: ^3.24.0 + version: 3.24.0(react@18.3.1) + '@tanstack/react-query': + specifier: ^5.62.3 + version: 5.62.3(react@18.3.1) + '@tanstack/react-router': + specifier: ^1.86.1 + version: 1.86.1(@tanstack/router-generator@1.86.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@tanstack/react-table': + specifier: ^8.20.5 + version: 8.20.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@tanstack/react-virtual': + specifier: ^3.11.2 + version: 3.11.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + ace-builds: + specifier: ^1.37.1 + version: 1.37.1 + axios: + specifier: ^1.7.9 + version: 1.7.9 + class-variance-authority: + specifier: ^0.7.1 + version: 0.7.1 + clsx: + specifier: ^2.1.1 + version: 2.1.1 + cmdk: + specifier: 1.0.4 + version: 1.0.4(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + date-fns: + specifier: ^3.6.0 + version: 3.6.0 + file-saver: + specifier: ^2.0.5 + version: 2.0.5 + handlebars: + specifier: ^4.7.8 + version: 4.7.8 + js-cookie: + specifier: ^3.0.5 + version: 3.0.5 + lucide-react: + specifier: ^0.468.0 + version: 0.468.0(react@18.3.1) + react: + specifier: ^18.3.1 + version: 18.3.1 + react-ace: + specifier: ^13.0.0 + version: 13.0.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react-day-picker: + specifier: 8.10.1 + version: 8.10.1(date-fns@3.6.0)(react@18.3.1) + react-dom: + specifier: ^18.3.1 + version: 18.3.1(react@18.3.1) + react-hook-form: + specifier: ^7.54.0 + version: 7.54.0(react@18.3.1) + react-markdown: + specifier: ^10.1.0 + version: 10.1.0(@types/react@18.3.18)(react@18.3.1) + react-resizable-panels: + specifier: ^2.1.7 + version: 2.1.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + recharts: + specifier: ^2.14.1 + version: 2.14.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + remark-gfm: + specifier: ^4.0.1 + version: 4.0.1 + tailwind-merge: + specifier: ^2.5.5 + version: 2.5.5 + tailwindcss-animate: + specifier: ^1.0.7 + version: 1.0.7(tailwindcss@3.4.16) + vaul: + specifier: ^1.1.2 + version: 1.1.2(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + zod: + specifier: ^3.23.8 + version: 3.23.8 + zustand: + specifier: ^5.0.2 + version: 5.0.2(@types/react@18.3.18)(react@18.3.1)(use-sync-external-store@1.2.2(react@18.3.1)) + devDependencies: + '@eslint/js': + specifier: ^9.16.0 + version: 9.16.0 + '@faker-js/faker': + specifier: ^9.3.0 + version: 9.3.0 + '@tanstack/eslint-plugin-query': + specifier: ^5.62.1 + version: 5.62.1(eslint@9.16.0(jiti@2.4.2))(typescript@5.7.2) + '@tanstack/react-query-devtools': + specifier: ^5.62.3 + version: 5.62.3(@tanstack/react-query@5.62.3(react@18.3.1))(react@18.3.1) + '@tanstack/router-devtools': + specifier: ^1.86.1 + version: 1.86.1(@tanstack/react-router@1.86.1(@tanstack/router-generator@1.86.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(csstype@3.1.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@tanstack/router-plugin': + specifier: ^1.86.0 + version: 1.86.0(vite@6.0.11(@types/node@22.10.1)(jiti@2.4.2)(sass@1.89.2)(terser@5.43.1)(tsx@4.19.2)(yaml@2.6.0))(webpack@5.99.9) + '@trivago/prettier-plugin-sort-imports': + specifier: ^4.3.0 + version: 4.3.0(prettier@3.4.2) + '@types/file-saver': + specifier: ^2.0.7 + version: 2.0.7 + '@types/js-cookie': + specifier: ^3.0.6 + version: 3.0.6 + '@types/node': + specifier: ^22.10.1 + version: 22.10.1 + '@types/react': + specifier: ^18.3.18 + version: 18.3.18 + '@types/react-dom': + specifier: ^18.3.5 + version: 18.3.5(@types/react@18.3.18) + '@vitejs/plugin-react-swc': + specifier: ^3.7.2 + version: 3.7.2(vite@6.0.11(@types/node@22.10.1)(jiti@2.4.2)(sass@1.89.2)(terser@5.43.1)(tsx@4.19.2)(yaml@2.6.0)) + autoprefixer: + specifier: ^10.4.20 + version: 10.4.20(postcss@8.4.49) + eslint: + specifier: ^9.16.0 + version: 9.16.0(jiti@2.4.2) + eslint-plugin-react-hooks: + specifier: ^5.1.0 + version: 5.1.0(eslint@9.16.0(jiti@2.4.2)) + eslint-plugin-react-refresh: + specifier: ^0.4.16 + version: 0.4.16(eslint@9.16.0(jiti@2.4.2)) + globals: + specifier: ^15.13.0 + version: 15.13.0 + knip: + specifier: ^5.41.1 + version: 5.41.1(@types/node@22.10.1)(typescript@5.7.2) + postcss: + specifier: ^8.4.49 + version: 8.4.49 + prettier: + specifier: ^3.4.2 + version: 3.4.2 + prettier-plugin-tailwindcss: + specifier: ^0.6.9 + version: 0.6.9(@trivago/prettier-plugin-sort-imports@4.3.0(prettier@3.4.2))(prettier@3.4.2) + tailwindcss: + specifier: ^3.4.16 + version: 3.4.16 + typescript: + specifier: ~5.7.2 + version: 5.7.2 + typescript-eslint: + specifier: ^8.17.0 + version: 8.17.0(eslint@9.16.0(jiti@2.4.2))(typescript@5.7.2) + vite: + specifier: ^6.0.11 + version: 6.0.11(@types/node@22.10.1)(jiti@2.4.2)(sass@1.89.2)(terser@5.43.1)(tsx@4.19.2)(yaml@2.6.0) + +packages: + + '@alloc/quick-lru@5.2.0': + resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} + engines: {node: '>=10'} + + '@ampproject/remapping@2.3.0': + resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} + engines: {node: '>=6.0.0'} + + '@babel/code-frame@7.26.2': + resolution: {integrity: sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==} + engines: {node: '>=6.9.0'} + + '@babel/compat-data@7.26.2': + resolution: {integrity: sha512-Z0WgzSEa+aUcdiJuCIqgujCshpMWgUpgOxXotrYPSA53hA3qopNaqcJpyr0hVb1FeWdnqFA35/fUtXgBK8srQg==} + engines: {node: '>=6.9.0'} + + '@babel/core@7.26.0': + resolution: {integrity: sha512-i1SLeK+DzNnQ3LL/CswPCa/E5u4lh1k6IAEphON8F+cXt0t9euTshDru0q7/IqMa1PMPz5RnHuHscF8/ZJsStg==} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.17.7': + resolution: {integrity: sha512-oLcVCTeIFadUoArDTwpluncplrYBmTCCZZgXCbgNGvOBBiSDDK3eWO4b/+eOTli5tKv1lg+a5/NAXg+nTcei1w==} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.26.2': + resolution: {integrity: sha512-zevQbhbau95nkoxSq3f/DC/SC+EEOUZd3DYqfSkMhY2/wfSeaHV1Ew4vk8e+x8lja31IbyuUa2uQ3JONqKbysw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-compilation-targets@7.25.9': + resolution: {integrity: sha512-j9Db8Suy6yV/VHa4qzrj9yZfZxhLWQdVnRlXxmKLYlhWUVB1sB2G5sxuWYXk/whHD9iW76PmNzxZ4UCnTQTVEQ==} + engines: {node: '>=6.9.0'} + + '@babel/helper-environment-visitor@7.24.7': + resolution: {integrity: sha512-DoiN84+4Gnd0ncbBOM9AZENV4a5ZiL39HYMyZJGZ/AZEykHYdJw0wW3kdcsh9/Kn+BRXHLkkklZ51ecPKmI1CQ==} + engines: {node: '>=6.9.0'} + + '@babel/helper-function-name@7.24.7': + resolution: {integrity: sha512-FyoJTsj/PEUWu1/TYRiXTIHc8lbw+TDYkZuoE43opPS5TrI7MyONBE1oNvfguEXAD9yhQRrVBnXdXzSLQl9XnA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-hoist-variables@7.24.7': + resolution: {integrity: sha512-MJJwhkoGy5c4ehfoRyrJ/owKeMl19U54h27YYftT0o2teQ3FJ3nQUf/I3LlJsX4l3qlw7WRXUmiyajvHXoTubQ==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.25.9': + resolution: {integrity: sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-transforms@7.26.0': + resolution: {integrity: sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-plugin-utils@7.25.9': + resolution: {integrity: sha512-kSMlyUVdWe25rEsRGviIgOWnoT/nfABVWlqt9N19/dIPWViAOW2s9wznP5tURbs/IDuNk4gPy3YdYRgH3uxhBw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-split-export-declaration@7.24.7': + resolution: {integrity: sha512-oy5V7pD+UvfkEATUKvIjvIAH/xCzfsFVw7ygW2SI6NClZzquT+mwdTfgfdbUiceh6iQO0CHtCPsyze/MZ2YbAA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-string-parser@7.25.9': + resolution: {integrity: sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.25.9': + resolution: {integrity: sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-option@7.25.9': + resolution: {integrity: sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw==} + engines: {node: '>=6.9.0'} + + '@babel/helpers@7.26.0': + resolution: {integrity: sha512-tbhNuIxNcVb21pInl3ZSjksLCvgdZy9KwJ8brv993QtIVKJBBkYXz4q4ZbAv31GdnC+R90np23L5FbEBlthAEw==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.26.2': + resolution: {integrity: sha512-DWMCZH9WA4Maitz2q21SRKHo9QXZxkDsbNZoVD62gusNtNBBqDg9i7uOhASfTfIGNzW+O+r7+jAlM8dwphcJKQ==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/plugin-syntax-jsx@7.25.9': + resolution: {integrity: sha512-ld6oezHQMZsZfp6pWtbjaNDF2tiiCYYDqQszHt5VV437lewP9aSi2Of99CK0D0XB21k7FLgnLcmQKyKzynfeAA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-typescript@7.25.9': + resolution: {integrity: sha512-hjMgRy5hb8uJJjUcdWunWVcoi9bGpJp8p5Ol1229PoN6aytsLwNMgmdftO23wnCLMfVmTwZDWMPNq/D1SY60JQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/runtime@7.26.0': + resolution: {integrity: sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw==} + engines: {node: '>=6.9.0'} + + '@babel/template@7.25.9': + resolution: {integrity: sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.23.2': + resolution: {integrity: sha512-azpe59SQ48qG6nu2CzcMLbxUudtN+dOM9kDbUqGq3HXUJRlo7i8fvPoxQUzYgLZ4cMVmuZgm8vvBpNeRhd6XSw==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.25.9': + resolution: {integrity: sha512-ZCuvfwOwlz/bawvAuvcj8rrithP2/N55Tzz342AkTvq4qaWbGfmCk/tKhNaV2cthijKrPAA8SRJV5WWe7IBMJw==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.17.0': + resolution: {integrity: sha512-TmKSNO4D5rzhL5bjWFcVHHLETzfQ/AmbKpKPOSjlP0WoHZ6L911fgoOKY4Alp/emzG4cHJdyN49zpgkbXFEHHw==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.26.0': + resolution: {integrity: sha512-Z/yiTPj+lDVnF7lWeKCIJzaIkI0vYO87dMpZ4bg4TDrFe4XXLFWL1TbXU27gBP3QccxV9mZICCrnjnYlJjXHOA==} + engines: {node: '>=6.9.0'} + + '@esbuild/aix-ppc64@0.23.1': + resolution: {integrity: sha512-6VhYk1diRqrhBAqpJEdjASR/+WVRtfjpqKuNw11cLiaWpAT/Uu+nokB+UJnevzy/P9C/ty6AOe0dwueMrGh/iQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/aix-ppc64@0.24.2': + resolution: {integrity: sha512-thpVCb/rhxE/BnMLQ7GReQLLN8q9qbHmI55F4489/ByVg2aQaQ6kbcLb6FHkocZzQhxc4gx0sCk0tJkKBFzDhA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.23.1': + resolution: {integrity: sha512-xw50ipykXcLstLeWH7WRdQuysJqejuAGPd30vd1i5zSyKK3WE+ijzHmLKxdiCMtH1pHz78rOg0BKSYOSB/2Khw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm64@0.24.2': + resolution: {integrity: sha512-cNLgeqCqV8WxfcTIOeL4OAtSmL8JjcN6m09XIgro1Wi7cF4t/THaWEa7eL5CMoMBdjoHOTh/vwTO/o2TRXIyzg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.23.1': + resolution: {integrity: sha512-uz6/tEy2IFm9RYOyvKl88zdzZfwEfKZmnX9Cj1BHjeSGNuGLuMD1kR8y5bteYmwqKm1tj8m4cb/aKEorr6fHWQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-arm@0.24.2': + resolution: {integrity: sha512-tmwl4hJkCfNHwFB3nBa8z1Uy3ypZpxqxfTQOcHX+xRByyYgunVbZ9MzUUfb0RxaHIMnbHagwAxuTL+tnNM+1/Q==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.23.1': + resolution: {integrity: sha512-nlN9B69St9BwUoB+jkyU090bru8L0NA3yFvAd7k8dNsVH8bi9a8cUAUSEcEEgTp2z3dbEDGJGfP6VUnkQnlReg==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/android-x64@0.24.2': + resolution: {integrity: sha512-B6Q0YQDqMx9D7rvIcsXfmJfvUYLoP722bgfBlO5cGvNVb5V/+Y7nhBE3mHV9OpxBf4eAS2S68KZztiPaWq4XYw==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.23.1': + resolution: {integrity: sha512-YsS2e3Wtgnw7Wq53XXBLcV6JhRsEq8hkfg91ESVadIrzr9wO6jJDMZnCQbHm1Guc5t/CdDiFSSfWP58FNuvT3Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-arm64@0.24.2': + resolution: {integrity: sha512-kj3AnYWc+CekmZnS5IPu9D+HWtUI49hbnyqk0FLEJDbzCIQt7hg7ucF1SQAilhtYpIujfaHr6O0UHlzzSPdOeA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.23.1': + resolution: {integrity: sha512-aClqdgTDVPSEGgoCS8QDG37Gu8yc9lTHNAQlsztQ6ENetKEO//b8y31MMu2ZaPbn4kVsIABzVLXYLhCGekGDqw==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/darwin-x64@0.24.2': + resolution: {integrity: sha512-WeSrmwwHaPkNR5H3yYfowhZcbriGqooyu3zI/3GGpF8AyUdsrrP0X6KumITGA9WOyiJavnGZUwPGvxvwfWPHIA==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.23.1': + resolution: {integrity: sha512-h1k6yS8/pN/NHlMl5+v4XPfikhJulk4G+tKGFIOwURBSFzE8bixw1ebjluLOjfwtLqY0kewfjLSrO6tN2MgIhA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-arm64@0.24.2': + resolution: {integrity: sha512-UN8HXjtJ0k/Mj6a9+5u6+2eZ2ERD7Edt1Q9IZiB5UZAIdPnVKDoG7mdTVGhHJIeEml60JteamR3qhsr1r8gXvg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.23.1': + resolution: {integrity: sha512-lK1eJeyk1ZX8UklqFd/3A60UuZ/6UVfGT2LuGo3Wp4/z7eRTRYY+0xOu2kpClP+vMTi9wKOfXi2vjUpO1Ro76g==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.24.2': + resolution: {integrity: sha512-TvW7wE/89PYW+IevEJXZ5sF6gJRDY/14hyIGFXdIucxCsbRmLUcjseQu1SyTko+2idmCw94TgyaEZi9HUSOe3Q==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.23.1': + resolution: {integrity: sha512-/93bf2yxencYDnItMYV/v116zff6UyTjo4EtEQjUBeGiVpMmffDNUyD9UN2zV+V3LRV3/on4xdZ26NKzn6754g==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm64@0.24.2': + resolution: {integrity: sha512-7HnAD6074BW43YvvUmE/35Id9/NB7BeX5EoNkK9obndmZBUk8xmJJeU7DwmUeN7tkysslb2eSl6CTrYz6oEMQg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.23.1': + resolution: {integrity: sha512-CXXkzgn+dXAPs3WBwE+Kvnrf4WECwBdfjfeYHpMeVxWE0EceB6vhWGShs6wi0IYEqMSIzdOF1XjQ/Mkm5d7ZdQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-arm@0.24.2': + resolution: {integrity: sha512-n0WRM/gWIdU29J57hJyUdIsk0WarGd6To0s+Y+LwvlC55wt+GT/OgkwoXCXvIue1i1sSNWblHEig00GBWiJgfA==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.23.1': + resolution: {integrity: sha512-VTN4EuOHwXEkXzX5nTvVY4s7E/Krz7COC8xkftbbKRYAl96vPiUssGkeMELQMOnLOJ8k3BY1+ZY52tttZnHcXQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-ia32@0.24.2': + resolution: {integrity: sha512-sfv0tGPQhcZOgTKO3oBE9xpHuUqguHvSo4jl+wjnKwFpapx+vUDcawbwPNuBIAYdRAvIDBfZVvXprIj3HA+Ugw==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.23.1': + resolution: {integrity: sha512-Vx09LzEoBa5zDnieH8LSMRToj7ir/Jeq0Gu6qJ/1GcBq9GkfoEAoXvLiW1U9J1qE/Y/Oyaq33w5p2ZWrNNHNEw==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-loong64@0.24.2': + resolution: {integrity: sha512-CN9AZr8kEndGooS35ntToZLTQLHEjtVB5n7dl8ZcTZMonJ7CCfStrYhrzF97eAecqVbVJ7APOEe18RPI4KLhwQ==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.23.1': + resolution: {integrity: sha512-nrFzzMQ7W4WRLNUOU5dlWAqa6yVeI0P78WKGUo7lg2HShq/yx+UYkeNSE0SSfSure0SqgnsxPvmAUu/vu0E+3Q==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-mips64el@0.24.2': + resolution: {integrity: sha512-iMkk7qr/wl3exJATwkISxI7kTcmHKE+BlymIAbHO8xanq/TjHaaVThFF6ipWzPHryoFsesNQJPE/3wFJw4+huw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.23.1': + resolution: {integrity: sha512-dKN8fgVqd0vUIjxuJI6P/9SSSe/mB9rvA98CSH2sJnlZ/OCZWO1DJvxj8jvKTfYUdGfcq2dDxoKaC6bHuTlgcw==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-ppc64@0.24.2': + resolution: {integrity: sha512-shsVrgCZ57Vr2L8mm39kO5PPIb+843FStGt7sGGoqiiWYconSxwTiuswC1VJZLCjNiMLAMh34jg4VSEQb+iEbw==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.23.1': + resolution: {integrity: sha512-5AV4Pzp80fhHL83JM6LoA6pTQVWgB1HovMBsLQ9OZWLDqVY8MVobBXNSmAJi//Csh6tcY7e7Lny2Hg1tElMjIA==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-riscv64@0.24.2': + resolution: {integrity: sha512-4eSFWnU9Hhd68fW16GD0TINewo1L6dRrB+oLNNbYyMUAeOD2yCK5KXGK1GH4qD/kT+bTEXjsyTCiJGHPZ3eM9Q==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.23.1': + resolution: {integrity: sha512-9ygs73tuFCe6f6m/Tb+9LtYxWR4c9yg7zjt2cYkjDbDpV/xVn+68cQxMXCjUpYwEkze2RcU/rMnfIXNRFmSoDw==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-s390x@0.24.2': + resolution: {integrity: sha512-S0Bh0A53b0YHL2XEXC20bHLuGMOhFDO6GN4b3YjRLK//Ep3ql3erpNcPlEFed93hsQAjAQDNsvcK+hV90FubSw==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.23.1': + resolution: {integrity: sha512-EV6+ovTsEXCPAp58g2dD68LxoP/wK5pRvgy0J/HxPGB009omFPv3Yet0HiaqvrIrgPTBuC6wCH1LTOY91EO5hQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/linux-x64@0.24.2': + resolution: {integrity: sha512-8Qi4nQcCTbLnK9WoMjdC9NiTG6/E38RNICU6sUNqK0QFxCYgoARqVqxdFmWkdonVsvGqWhmm7MO0jyTqLqwj0Q==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.24.2': + resolution: {integrity: sha512-wuLK/VztRRpMt9zyHSazyCVdCXlpHkKm34WUyinD2lzK07FAHTq0KQvZZlXikNWkDGoT6x3TD51jKQ7gMVpopw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.23.1': + resolution: {integrity: sha512-aevEkCNu7KlPRpYLjwmdcuNz6bDFiE7Z8XC4CPqExjTvrHugh28QzUXVOZtiYghciKUacNktqxdpymplil1beA==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.24.2': + resolution: {integrity: sha512-VefFaQUc4FMmJuAxmIHgUmfNiLXY438XrL4GDNV1Y1H/RW3qow68xTwjZKfj/+Plp9NANmzbH5R40Meudu8mmw==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.23.1': + resolution: {integrity: sha512-3x37szhLexNA4bXhLrCC/LImN/YtWis6WXr1VESlfVtVeoFJBRINPJ3f0a/6LV8zpikqoUg4hyXw0sFBt5Cr+Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-arm64@0.24.2': + resolution: {integrity: sha512-YQbi46SBct6iKnszhSvdluqDmxCJA+Pu280Av9WICNwQmMxV7nLRHZfjQzwbPs3jeWnuAhE9Jy0NrnJ12Oz+0A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.23.1': + resolution: {integrity: sha512-aY2gMmKmPhxfU+0EdnN+XNtGbjfQgwZj43k8G3fyrDM/UdZww6xrWxmDkuz2eCZchqVeABjV5BpildOrUbBTqA==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.24.2': + resolution: {integrity: sha512-+iDS6zpNM6EnJyWv0bMGLWSWeXGN/HTaF/LXHXHwejGsVi+ooqDfMCCTerNFxEkM3wYVcExkeGXNqshc9iMaOA==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/sunos-x64@0.23.1': + resolution: {integrity: sha512-RBRT2gqEl0IKQABT4XTj78tpk9v7ehp+mazn2HbUeZl1YMdaGAQqhapjGTCe7uw7y0frDi4gS0uHzhvpFuI1sA==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/sunos-x64@0.24.2': + resolution: {integrity: sha512-hTdsW27jcktEvpwNHJU4ZwWFGkz2zRJUz8pvddmXPtXDzVKTTINmlmga3ZzwcuMpUvLw7JkLy9QLKyGpD2Yxig==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.23.1': + resolution: {integrity: sha512-4O+gPR5rEBe2FpKOVyiJ7wNDPA8nGzDuJ6gN4okSA1gEOYZ67N8JPk58tkWtdtPeLz7lBnY6I5L3jdsr3S+A6A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-arm64@0.24.2': + resolution: {integrity: sha512-LihEQ2BBKVFLOC9ZItT9iFprsE9tqjDjnbulhHoFxYQtQfai7qfluVODIYxt1PgdoyQkz23+01rzwNwYfutxUQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.23.1': + resolution: {integrity: sha512-BcaL0Vn6QwCwre3Y717nVHZbAa4UBEigzFm6VdsVdT/MbZ38xoj1X9HPkZhbmaBGUD1W8vxAfffbDe8bA6AKnQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-ia32@0.24.2': + resolution: {integrity: sha512-q+iGUwfs8tncmFC9pcnD5IvRHAzmbwQ3GPS5/ceCyHdjXubwQWI12MKWSNSMYLJMq23/IUCvJMS76PDqXe1fxA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.23.1': + resolution: {integrity: sha512-BHpFFeslkWrXWyUPnbKm+xYYVYruCinGcftSBaa8zoF9hZO4BcSCFUvHVTtzpIY6YzUnYtuEhZ+C9iEXjxnasg==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@esbuild/win32-x64@0.24.2': + resolution: {integrity: sha512-7VTgWzgMGvup6aSqDPLiW5zHaxYJGTO4OokMjIlrCtf+VpEL+cXKtCvg723iguPYI5oaUNdS+/V7OU2gvXVWEg==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@eslint-community/eslint-utils@4.4.1': + resolution: {integrity: sha512-s3O3waFUrMV8P/XaF/+ZTp1X9XBZW1a4B97ZnjQF2KYWaFD2A8KyFBsrsfSjEmjn3RGWAIuvlneuZm3CUK3jbA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + + '@eslint-community/regexpp@4.12.1': + resolution: {integrity: sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + + '@eslint/config-array@0.19.1': + resolution: {integrity: sha512-fo6Mtm5mWyKjA/Chy1BYTdn5mGJoDNjC7C64ug20ADsRDGrA85bN3uK3MaKbeRkRuuIEAR5N33Jr1pbm411/PA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/core@0.9.1': + resolution: {integrity: sha512-GuUdqkyyzQI5RMIWkHhvTWLCyLo1jNK3vzkSyaExH5kHPDHcuL2VOpHjmMY+y3+NC69qAKToBqldTBgYeLSr9Q==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/eslintrc@3.2.0': + resolution: {integrity: sha512-grOjVNN8P3hjJn/eIETF1wwd12DdnwFDoyceUJLYYdkpbwq3nLi+4fqrTAONx7XDALqlL220wC/RHSC/QTI/0w==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/js@9.16.0': + resolution: {integrity: sha512-tw2HxzQkrbeuvyj1tG2Yqq+0H9wGoI2IMk4EOsQeX+vmd75FtJAzf+gTA69WF+baUKRYQ3x2kbLE08js5OsTVg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/object-schema@2.1.5': + resolution: {integrity: sha512-o0bhxnL89h5Bae5T318nFoFzGy+YE5i/gGkoPAgkmTVdRKTiv3p8JHevPiPaMwoloKfEiiaHlawCqaZMqRm+XQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/plugin-kit@0.2.4': + resolution: {integrity: sha512-zSkKow6H5Kdm0ZUQUB2kV5JIXqoG0+uH5YADhaEHswm664N9Db8dXSi0nMJpacpMf+MyyglF1vnZohpEg5yUtg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@faker-js/faker@9.3.0': + resolution: {integrity: sha512-r0tJ3ZOkMd9xsu3VRfqlFR6cz0V/jFYRswAIpC+m/DIfAUXq7g8N7wTAlhSANySXYGKzGryfDXwtwsY8TxEIDw==} + engines: {node: '>=18.0.0', npm: '>=9.0.0'} + + '@floating-ui/core@1.6.8': + resolution: {integrity: sha512-7XJ9cPU+yI2QeLS+FCSlqNFZJq8arvswefkZrYI1yQBbftw6FyrZOxYSh+9S7z7TpeWlRt9zJ5IhM1WIL334jA==} + + '@floating-ui/dom@1.6.12': + resolution: {integrity: sha512-NP83c0HjokcGVEMeoStg317VD9W7eDlGK7457dMBANbKA6GJZdc7rjujdgqzTaz93jkGgc5P/jeWbaCHnMNc+w==} + + '@floating-ui/react-dom@2.1.2': + resolution: {integrity: sha512-06okr5cgPzMNBy+Ycse2A6udMi4bqwW/zgBF/rwjcNqWkyr82Mcg8b0vjX8OJpZFy/FKjJmw6wV7t44kK6kW7A==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + + '@floating-ui/utils@0.2.8': + resolution: {integrity: sha512-kym7SodPp8/wloecOpcmSnWJsK7M0E5Wg8UcFA+uO4B9s5d0ywXOEro/8HM9x0rW+TljRzul/14UYz3TleT3ig==} + + '@hookform/resolvers@3.9.1': + resolution: {integrity: sha512-ud2HqmGBM0P0IABqoskKWI6PEf6ZDDBZkFqe2Vnl+mTHCEHzr3ISjjZyCwTjC/qpL25JC9aIDkloQejvMeq0ug==} + peerDependencies: + react-hook-form: ^7.0.0 + + '@humanfs/core@0.19.1': + resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} + engines: {node: '>=18.18.0'} + + '@humanfs/node@0.16.6': + resolution: {integrity: sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==} + engines: {node: '>=18.18.0'} + + '@humanwhocodes/module-importer@1.0.1': + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + + '@humanwhocodes/retry@0.3.1': + resolution: {integrity: sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==} + engines: {node: '>=18.18'} + + '@humanwhocodes/retry@0.4.1': + resolution: {integrity: sha512-c7hNEllBlenFTHBky65mhq8WD2kbN9Q6gk0bTk8lSBvc554jpXSkST1iePudpt7+A/AQvuHs9EMqjHDXMY1lrA==} + engines: {node: '>=18.18'} + + '@isaacs/cliui@8.0.2': + resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} + engines: {node: '>=12'} + + '@jridgewell/gen-mapping@0.3.5': + resolution: {integrity: sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==} + engines: {node: '>=6.0.0'} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/set-array@1.2.1': + resolution: {integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==} + engines: {node: '>=6.0.0'} + + '@jridgewell/source-map@0.3.10': + resolution: {integrity: sha512-0pPkgz9dY+bijgistcTTJ5mR+ocqRXLuhXHYdzoMmmoJ2C9S46RCm2GMUbatPEUK9Yjy26IrAy8D/M00lLkv+Q==} + + '@jridgewell/sourcemap-codec@1.5.0': + resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==} + + '@jridgewell/trace-mapping@0.3.25': + resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} + + '@nodelib/fs.scandir@2.1.5': + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + + '@nodelib/fs.stat@2.0.5': + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + + '@nodelib/fs.walk@1.2.8': + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + + '@parcel/watcher-android-arm64@2.5.1': + resolution: {integrity: sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [android] + + '@parcel/watcher-darwin-arm64@2.5.1': + resolution: {integrity: sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [darwin] + + '@parcel/watcher-darwin-x64@2.5.1': + resolution: {integrity: sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [darwin] + + '@parcel/watcher-freebsd-x64@2.5.1': + resolution: {integrity: sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [freebsd] + + '@parcel/watcher-linux-arm-glibc@2.5.1': + resolution: {integrity: sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA==} + engines: {node: '>= 10.0.0'} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@parcel/watcher-linux-arm-musl@2.5.1': + resolution: {integrity: sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==} + engines: {node: '>= 10.0.0'} + cpu: [arm] + os: [linux] + libc: [musl] + + '@parcel/watcher-linux-arm64-glibc@2.5.1': + resolution: {integrity: sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@parcel/watcher-linux-arm64-musl@2.5.1': + resolution: {integrity: sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@parcel/watcher-linux-x64-glibc@2.5.1': + resolution: {integrity: sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@parcel/watcher-linux-x64-musl@2.5.1': + resolution: {integrity: sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@parcel/watcher-win32-arm64@2.5.1': + resolution: {integrity: sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [win32] + + '@parcel/watcher-win32-ia32@2.5.1': + resolution: {integrity: sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ==} + engines: {node: '>= 10.0.0'} + cpu: [ia32] + os: [win32] + + '@parcel/watcher-win32-x64@2.5.1': + resolution: {integrity: sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [win32] + + '@parcel/watcher@2.5.1': + resolution: {integrity: sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==} + engines: {node: '>= 10.0.0'} + + '@pkgjs/parseargs@0.11.0': + resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} + engines: {node: '>=14'} + + '@radix-ui/number@1.1.0': + resolution: {integrity: sha512-V3gRzhVNU1ldS5XhAPTom1fOIo4ccrjjJgmE+LI2h/WaFpHmx0MQApT+KZHnx8abG6Avtfcz4WoEciMnpFT3HQ==} + + '@radix-ui/primitive@1.1.0': + resolution: {integrity: sha512-4Z8dn6Upk0qk4P74xBhZ6Hd/w0mPEzOOLxy4xiPXOXqjF7jZS0VAKk7/x/H6FyY2zCkYJqePf1G5KmkmNJ4RBA==} + + '@radix-ui/primitive@1.1.1': + resolution: {integrity: sha512-SJ31y+Q/zAyShtXJc8x83i9TYdbAfHZ++tUZnvjJJqFjzsdUnKsxPL6IEtBlxKkU7yzer//GQtZSV4GbldL3YA==} + + '@radix-ui/react-accordion@1.2.2': + resolution: {integrity: sha512-b1oh54x4DMCdGsB4/7ahiSrViXxaBwRPotiZNnYXjLha9vfuURSAZErki6qjDoSIV0eXx5v57XnTGVtGwnfp2g==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-alert-dialog@1.1.2': + resolution: {integrity: sha512-eGSlLzPhKO+TErxkiGcCZGuvbVMnLA1MTnyBksGOeGRGkxHiiJUujsjmNTdWTm4iHVSRaUao9/4Ur671auMghQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-arrow@1.1.0': + resolution: {integrity: sha512-FmlW1rCg7hBpEBwFbjHwCW6AmWLQM6g/v0Sn8XbP9NvmSZ2San1FpQeyPtufzOMSIx7Y4dzjlHoifhp+7NkZhw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-arrow@1.1.1': + resolution: {integrity: sha512-NaVpZfmv8SKeZbn4ijN2V3jlHA9ngBG16VnIIm22nUR0Yk8KUALyBxT3KYEUnNuch9sTE8UTsS3whzBgKOL30w==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-avatar@1.1.1': + resolution: {integrity: sha512-eoOtThOmxeoizxpX6RiEsQZ2wj5r4+zoeqAwO0cBaFQGjJwIH3dIX0OCxNrCyrrdxG+vBweMETh3VziQG7c1kw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-checkbox@1.1.2': + resolution: {integrity: sha512-/i0fl686zaJbDQLNKrkCbMyDm6FQMt4jg323k7HuqitoANm9sE23Ql8yOK3Wusk34HSLKDChhMux05FnP6KUkw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-collapsible@1.1.1': + resolution: {integrity: sha512-1///SnrfQHJEofLokyczERxQbWfCGQlQ2XsCZMucVs6it+lq9iw4vXy+uDn1edlb58cOZOWSldnfPAYcT4O/Yg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-collapsible@1.1.2': + resolution: {integrity: sha512-PliMB63vxz7vggcyq0IxNYk8vGDrLXVWw4+W4B8YnwI1s18x7YZYqlG9PLX7XxAJUi0g2DxP4XKJMFHh/iVh9A==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-collection@1.1.0': + resolution: {integrity: sha512-GZsZslMJEyo1VKm5L1ZJY8tGDxZNPAoUeQUIbKeJfoi7Q4kmig5AsgLMYYuyYbfjd8fBmFORAIwYAkXMnXZgZw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-collection@1.1.1': + resolution: {integrity: sha512-LwT3pSho9Dljg+wY2KN2mrrh6y3qELfftINERIzBUO9e0N+t0oMTyn3k9iv+ZqgrwGkRnLpNJrsMv9BZlt2yuA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-collection@1.1.2': + resolution: {integrity: sha512-9z54IEKRxIa9VityapoEYMuByaG42iSy1ZXlY2KcuLSEtq8x4987/N6m15ppoMffgZX72gER2uHe1D9Y6Unlcw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-compose-refs@1.1.0': + resolution: {integrity: sha512-b4inOtiaOnYf9KWyO3jAeeCG6FeyfY6ldiEPanbUjWd+xIk5wZeHa8yVwmrJ2vderhu/BQvzCrJI0lHd+wIiqw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-compose-refs@1.1.1': + resolution: {integrity: sha512-Y9VzoRDSJtgFMUCoiZBDVo084VQ5hfpXxVE+NgkdNsjiDBByiImMZKKhxMwCbdHvhlENG6a833CbFkOQvTricw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-compose-refs@1.1.2': + resolution: {integrity: sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-context@1.1.0': + resolution: {integrity: sha512-OKrckBy+sMEgYM/sMmqmErVn0kZqrHPJze+Ql3DzYsDDp0hl0L62nx/2122/Bvps1qz645jlcu2tD9lrRSdf8A==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-context@1.1.1': + resolution: {integrity: sha512-UASk9zi+crv9WteK/NU4PLvOoL3OuE6BWVKNF6hPRBtYBDXQ2u5iu3O59zUlJiTVvkyuycnqrztsHVJwcK9K+Q==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-context@1.1.3': + resolution: {integrity: sha512-ieIFACdMpYfMEjF0rEf5KLvfVyIkOz6PDGyNnP+u+4xQ6jny3VCgA4OgXOwNx2aUkxn8zx9fiVcM8CfFYv9Lxw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-dialog@1.1.2': + resolution: {integrity: sha512-Yj4dZtqa2o+kG61fzB0H2qUvmwBA2oyQroGLyNtBj1beo1khoQ3q1a2AO8rrQYjd8256CO9+N8L9tvsS+bnIyA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-direction@1.1.0': + resolution: {integrity: sha512-BUuBvgThEiAXh2DWu93XsT+a3aWrGqolGlqqw5VU1kG7p/ZH2cuDlM1sRLNnY3QcBS69UIz2mcKhMxDsdewhjg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-dismissable-layer@1.1.1': + resolution: {integrity: sha512-QSxg29lfr/xcev6kSz7MAlmDnzbP1eI/Dwn3Tp1ip0KT5CUELsxkekFEMVBEoykI3oV39hKT4TKZzBNMbcTZYQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-dismissable-layer@1.1.3': + resolution: {integrity: sha512-onrWn/72lQoEucDmJnr8uczSNTujT0vJnA/X5+3AkChVPowr8n1yvIKIabhWyMQeMvvmdpsvcyDqx3X1LEXCPg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-dismissable-layer@1.1.5': + resolution: {integrity: sha512-E4TywXY6UsXNRhFrECa5HAvE5/4BFcGyfTyK36gP+pAW1ed7UTK4vKwdr53gAJYwqbfCWC6ATvJa3J3R/9+Qrg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-dropdown-menu@2.1.2': + resolution: {integrity: sha512-GVZMR+eqK8/Kes0a36Qrv+i20bAPXSn8rCBTHx30w+3ECnR5o3xixAlqcVaYvLeyKUsm0aqyhWfmUcqufM8nYA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-focus-guards@1.1.1': + resolution: {integrity: sha512-pSIwfrT1a6sIoDASCSpFwOasEwKTZWDw/iBdtnqKO7v6FeOzYJ7U53cPzYFVR3geGGXgVHaH+CdngrrAzqUGxg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-focus-scope@1.1.0': + resolution: {integrity: sha512-200UD8zylvEyL8Bx+z76RJnASR2gRMuxlgFCPAe/Q/679a/r0eK3MBVYMb7vZODZcffZBdob1EGnky78xmVvcA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-hover-card@1.1.4': + resolution: {integrity: sha512-QSUUnRA3PQ2UhvoCv3eYvMnCAgGQW+sTu86QPuNb+ZMi+ZENd6UWpiXbcWDQ4AEaKF9KKpCHBeaJz9Rw6lRlaQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-icons@1.3.2': + resolution: {integrity: sha512-fyQIhGDhzfc9pK2kH6Pl9c4BDJGfMkPqkyIgYDthyNYoNg3wVhoJMMh19WS4Up/1KMPFVpNsT2q3WmXn2N1m6g==} + peerDependencies: + react: ^16.x || ^17.x || ^18.x || ^19.0.0 || ^19.0.0-rc + + '@radix-ui/react-id@1.1.0': + resolution: {integrity: sha512-EJUrI8yYh7WOjNOqpoJaf1jlFIH2LvtgAl+YcFqNCa+4hj64ZXmPkAKOFs/ukjz3byN6bdb/AVUqHkI8/uWWMA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-label@2.1.0': + resolution: {integrity: sha512-peLblDlFw/ngk3UWq0VnYaOLy6agTZZ+MUO/WhVfm14vJGML+xH4FAl2XQGLqdefjNb7ApRg6Yn7U42ZhmYXdw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-menu@2.1.2': + resolution: {integrity: sha512-lZ0R4qR2Al6fZ4yCCZzu/ReTFrylHFxIqy7OezIpWF4bL0o9biKo0pFIvkaew3TyZ9Fy5gYVrR5zCGZBVbO1zg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-navigation-menu@1.2.5': + resolution: {integrity: sha512-myMHHQUZ3ZLTi8W381/Vu43Ia0NqakkQZ2vzynMmTUtQQ9kNkjzhOwkZC9TAM5R07OZUVIQyHC06f/9JZJpvvA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-popover@1.1.2': + resolution: {integrity: sha512-u2HRUyWW+lOiA2g0Le0tMmT55FGOEWHwPFt1EPfbLly7uXQExFo5duNKqG2DzmFXIdqOeNd+TpE8baHWJCyP9w==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-popper@1.2.0': + resolution: {integrity: sha512-ZnRMshKF43aBxVWPWvbj21+7TQCvhuULWJ4gNIKYpRlQt5xGRhLx66tMp8pya2UkGHTSlhpXwmjqltDYHhw7Vg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-popper@1.2.1': + resolution: {integrity: sha512-3kn5Me69L+jv82EKRuQCXdYyf1DqHwD2U/sxoNgBGCB7K9TRc3bQamQ+5EPM9EvyPdli0W41sROd+ZU1dTCztw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-portal@1.1.2': + resolution: {integrity: sha512-WeDYLGPxJb/5EGBoedyJbT0MpoULmwnIPMJMSldkuiMsBAv7N1cRdsTWZWht9vpPOiN3qyiGAtbK2is47/uMFg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-portal@1.1.3': + resolution: {integrity: sha512-NciRqhXnGojhT93RPyDaMPfLH3ZSl4jjIFbZQ1b/vxvZEdHsBZ49wP9w8L3HzUQwep01LcWtkUvm0OVB5JAHTw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-presence@1.1.1': + resolution: {integrity: sha512-IeFXVi4YS1K0wVZzXNrbaaUvIJ3qdY+/Ih4eHFhWA9SwGR9UDX7Ck8abvL57C4cv3wwMvUE0OG69Qc3NCcTe/A==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-presence@1.1.2': + resolution: {integrity: sha512-18TFr80t5EVgL9x1SwF/YGtfG+l0BS0PRAlCWBDoBEiDQjeKgnNZRVJp/oVBl24sr3Gbfwc/Qpj4OcWTQMsAEg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-primitive@2.0.0': + resolution: {integrity: sha512-ZSpFm0/uHa8zTvKBDjLFWLo8dkr4MBsiDLz0g3gMUwqgLHz9rTaRRGYDgvZPtBJgYCBKXkS9fzmoySgr8CO6Cw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-primitive@2.0.1': + resolution: {integrity: sha512-sHCWTtxwNn3L3fH8qAfnF3WbUZycW93SM1j3NFDzXBiz8D6F5UTTy8G1+WFEaiCdvCVRJWj6N2R4Xq6HdiHmDg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-primitive@2.0.2': + resolution: {integrity: sha512-Ec/0d38EIuvDF+GZjcMU/Ze6MxntVJYO/fRlCPhCaVUyPY9WTalHJw54tp9sXeJo3tlShWpy41vQRgLRGOuz+w==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-primitive@2.1.4': + resolution: {integrity: sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-progress@1.1.8': + resolution: {integrity: sha512-+gISHcSPUJ7ktBy9RnTqbdKW78bcGke3t6taawyZ71pio1JewwGSJizycs7rLhGTvMJYCQB1DBK4KQsxs7U8dA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-radio-group@1.2.1': + resolution: {integrity: sha512-kdbv54g4vfRjja9DNWPMxKvXblzqbpEC8kspEkZ6dVP7kQksGCn+iZHkcCz2nb00+lPdRvxrqy4WrvvV1cNqrQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-roving-focus@1.1.0': + resolution: {integrity: sha512-EA6AMGeq9AEeQDeSH0aZgG198qkfHSbvWTf1HvoDmOB5bBG/qTxjYMWUKMnYiV6J/iP/J8MEFSuB2zRU2n7ODA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-scroll-area@1.2.1': + resolution: {integrity: sha512-FnM1fHfCtEZ1JkyfH/1oMiTcFBQvHKl4vD9WnpwkLgtF+UmnXMCad6ECPTaAjcDjam+ndOEJWgHyKDGNteWSHw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-select@2.1.2': + resolution: {integrity: sha512-rZJtWmorC7dFRi0owDmoijm6nSJH1tVw64QGiNIZ9PNLyBDtG+iAq+XGsya052At4BfarzY/Dhv9wrrUr6IMZA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-separator@1.1.0': + resolution: {integrity: sha512-3uBAs+egzvJBDZAzvb/n4NxxOYpnspmWxO2u5NbZ8Y6FM/NdrGSF9bop3Cf6F6C71z1rTSn8KV0Fo2ZVd79lGA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-slot@1.1.0': + resolution: {integrity: sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-slot@1.1.1': + resolution: {integrity: sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-slot@1.1.2': + resolution: {integrity: sha512-YAKxaiGsSQJ38VzKH86/BPRC4rh+b1Jpa+JneA5LRE7skmLPNAyeG8kPJj/oo4STLvlrs8vkf/iYyc3A5stYCQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-slot@1.2.4': + resolution: {integrity: sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-switch@1.1.1': + resolution: {integrity: sha512-diPqDDoBcZPSicYoMWdWx+bCPuTRH4QSp9J+65IvtdS0Kuzt67bI6n32vCj8q6NZmYW/ah+2orOtMwcX5eQwIg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-tabs@1.1.1': + resolution: {integrity: sha512-3GBUDmP2DvzmtYLMsHmpA1GtR46ZDZ+OreXM/N+kkQJOPIgytFWWTfDQmBQKBvaFS0Vno0FktdbVzN28KGrMdw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-toast@1.2.2': + resolution: {integrity: sha512-Z6pqSzmAP/bFJoqMAston4eSNa+ud44NSZTiZUmUen+IOZ5nBY8kzuU5WDBVyFXPtcW6yUalOHsxM/BP6Sv8ww==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-tooltip@1.1.4': + resolution: {integrity: sha512-QpObUH/ZlpaO4YgHSaYzrLO2VuO+ZBFFgGzjMUPwtiYnAzzNNDPJeEGRrT7qNOrWm/Jr08M1vlp+vTHtnSQ0Uw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-use-callback-ref@1.1.0': + resolution: {integrity: sha512-CasTfvsy+frcFkbXtSJ2Zu9JHpN8TYKxkgJGWbjiZhFivxaeW7rMeZt7QELGVLaYVfFMsKHjb7Ak0nMEe+2Vfw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-controllable-state@1.1.0': + resolution: {integrity: sha512-MtfMVJiSr2NjzS0Aa90NPTnvTSg6C/JLCV7ma0W6+OMV78vd8OyRpID+Ng9LxzsPbLeuBnWBA1Nq30AtBIDChw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-escape-keydown@1.1.0': + resolution: {integrity: sha512-L7vwWlR1kTTQ3oh7g1O0CBF3YCyyTj8NmhLR+phShpyA50HCfBFKVJTpshm9PzLiKmehsrQzTYTpX9HvmC9rhw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-layout-effect@1.1.0': + resolution: {integrity: sha512-+FPE0rOdziWSrH9athwI1R0HDVbWlEhd+FR+aSDk4uWGmSJ9Z54sdZVDQPZAinJhJXwfT+qnj969mCsT2gfm5w==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-previous@1.1.0': + resolution: {integrity: sha512-Z/e78qg2YFnnXcW88A4JmTtm4ADckLno6F7OXotmkQfeuCVaKuYzqAATPhVzl3delXE7CxIV8shofPn3jPc5Og==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-rect@1.1.0': + resolution: {integrity: sha512-0Fmkebhr6PiseyZlYAOtLS+nb7jLmpqTrJyv61Pe68MKYW6OWdRE2kI70TaYY27u7H0lajqM3hSMMLFq18Z7nQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-size@1.1.0': + resolution: {integrity: sha512-XW3/vWuIXHa+2Uwcc2ABSfcCledmXhhQPlGbfcRXbiUQI5Icjcg19BGCZVKKInYbvUCut/ufbbLLPFC5cbb1hw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-visually-hidden@1.1.0': + resolution: {integrity: sha512-N8MDZqtgCgG5S3aV60INAB475osJousYpZ4cTJ2cFbMpdHS5Y6loLTH8LPtkj2QN0x93J30HT/M3qJXM0+lyeQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-visually-hidden@1.1.2': + resolution: {integrity: sha512-1SzA4ns2M1aRlvxErqhLHsBHoS5eI5UUcI2awAMgGUp4LoaoWOKYmvqDY2s/tltuPkh3Yk77YF/r3IRj+Amx4Q==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/rect@1.1.0': + resolution: {integrity: sha512-A9+lCBZoaMJlVKcRBz2YByCG+Cp2t6nAnMnNba+XiWxnj6r4JUFqfsgwocMBZU9LPtdxC6wB56ySYpc7LQIoJg==} + + '@rollup/rollup-android-arm-eabi@4.26.0': + resolution: {integrity: sha512-gJNwtPDGEaOEgejbaseY6xMFu+CPltsc8/T+diUTTbOQLqD+bnrJq9ulH6WD69TqwqWmrfRAtUv30cCFZlbGTQ==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.26.0': + resolution: {integrity: sha512-YJa5Gy8mEZgz5JquFruhJODMq3lTHWLm1fOy+HIANquLzfIOzE9RA5ie3JjCdVb9r46qfAQY/l947V0zfGJ0OQ==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.26.0': + resolution: {integrity: sha512-ErTASs8YKbqTBoPLp/kA1B1Um5YSom8QAc4rKhg7b9tyyVqDBlQxy7Bf2wW7yIlPGPg2UODDQcbkTlruPzDosw==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.26.0': + resolution: {integrity: sha512-wbgkYDHcdWW+NqP2mnf2NOuEbOLzDblalrOWcPyY6+BRbVhliavon15UploG7PpBRQ2bZJnbmh8o3yLoBvDIHA==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.26.0': + resolution: {integrity: sha512-Y9vpjfp9CDkAG4q/uwuhZk96LP11fBz/bYdyg9oaHYhtGZp7NrbkQrj/66DYMMP2Yo/QPAsVHkV891KyO52fhg==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.26.0': + resolution: {integrity: sha512-A/jvfCZ55EYPsqeaAt/yDAG4q5tt1ZboWMHEvKAH9Zl92DWvMIbnZe/f/eOXze65aJaaKbL+YeM0Hz4kLQvdwg==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.26.0': + resolution: {integrity: sha512-paHF1bMXKDuizaMODm2bBTjRiHxESWiIyIdMugKeLnjuS1TCS54MF5+Y5Dx8Ui/1RBPVRE09i5OUlaLnv8OGnA==} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm-musleabihf@4.26.0': + resolution: {integrity: sha512-cwxiHZU1GAs+TMxvgPfUDtVZjdBdTsQwVnNlzRXC5QzIJ6nhfB4I1ahKoe9yPmoaA/Vhf7m9dB1chGPpDRdGXg==} + cpu: [arm] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-arm64-gnu@4.26.0': + resolution: {integrity: sha512-4daeEUQutGRCW/9zEo8JtdAgtJ1q2g5oHaoQaZbMSKaIWKDQwQ3Yx0/3jJNmpzrsScIPtx/V+1AfibLisb3AMQ==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm64-musl@4.26.0': + resolution: {integrity: sha512-eGkX7zzkNxvvS05ROzJ/cO/AKqNvR/7t1jA3VZDi2vRniLKwAWxUr85fH3NsvtxU5vnUUKFHKh8flIBdlo2b3Q==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-powerpc64le-gnu@4.26.0': + resolution: {integrity: sha512-Odp/lgHbW/mAqw/pU21goo5ruWsytP7/HCC/liOt0zcGG0llYWKrd10k9Fj0pdj3prQ63N5yQLCLiE7HTX+MYw==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-riscv64-gnu@4.26.0': + resolution: {integrity: sha512-MBR2ZhCTzUgVD0OJdTzNeF4+zsVogIR1U/FsyuFerwcqjZGvg2nYe24SAHp8O5sN8ZkRVbHwlYeHqcSQ8tcYew==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-s390x-gnu@4.26.0': + resolution: {integrity: sha512-YYcg8MkbN17fMbRMZuxwmxWqsmQufh3ZJFxFGoHjrE7bv0X+T6l3glcdzd7IKLiwhT+PZOJCblpnNlz1/C3kGQ==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-gnu@4.26.0': + resolution: {integrity: sha512-ZuwpfjCwjPkAOxpjAEjabg6LRSfL7cAJb6gSQGZYjGhadlzKKywDkCUnJ+KEfrNY1jH5EEoSIKLCb572jSiglA==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-musl@4.26.0': + resolution: {integrity: sha512-+HJD2lFS86qkeF8kNu0kALtifMpPCZU80HvwztIKnYwym3KnA1os6nsX4BGSTLtS2QVAGG1P3guRgsYyMA0Yhg==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rollup/rollup-win32-arm64-msvc@4.26.0': + resolution: {integrity: sha512-WUQzVFWPSw2uJzX4j6YEbMAiLbs0BUysgysh8s817doAYhR5ybqTI1wtKARQKo6cGop3pHnrUJPFCsXdoFaimQ==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.26.0': + resolution: {integrity: sha512-D4CxkazFKBfN1akAIY6ieyOqzoOoBV1OICxgUblWxff/pSjCA2khXlASUx7mK6W1oP4McqhgcCsu6QaLj3WMWg==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.26.0': + resolution: {integrity: sha512-2x8MO1rm4PGEP0xWbubJW5RtbNLk3puzAMaLQd3B3JHVw4KcHlmXcO+Wewx9zCoo7EUFiMlu/aZbCJ7VjMzAag==} + cpu: [x64] + os: [win32] + + '@snyk/github-codeowners@1.1.0': + resolution: {integrity: sha512-lGFf08pbkEac0NYgVf4hdANpAgApRjNByLXB+WBip3qj1iendOIyAwP2GKkKbQMNVy2r1xxDf0ssfWscoiC+Vw==} + engines: {node: '>=8.10'} + hasBin: true + + '@swc/core-darwin-arm64@1.9.2': + resolution: {integrity: sha512-nETmsCoY29krTF2PtspEgicb3tqw7Ci5sInTI03EU5zpqYbPjoPH99BVTjj0OsF53jP5MxwnLI5Hm21lUn1d6A==} + engines: {node: '>=10'} + cpu: [arm64] + os: [darwin] + + '@swc/core-darwin-x64@1.9.2': + resolution: {integrity: sha512-9gD+bwBz8ZByjP6nZTXe/hzd0tySIAjpDHgkFiUrc+5zGF+rdTwhcNrzxNHJmy6mw+PW38jqII4uspFHUqqxuQ==} + engines: {node: '>=10'} + cpu: [x64] + os: [darwin] + + '@swc/core-linux-arm-gnueabihf@1.9.2': + resolution: {integrity: sha512-kYq8ief1Qrn+WmsTWAYo4r+Coul4dXN6cLFjiPZ29Cv5pyU+GFvSPAB4bEdMzwy99rCR0u2P10UExaeCjurjvg==} + engines: {node: '>=10'} + cpu: [arm] + os: [linux] + + '@swc/core-linux-arm64-gnu@1.9.2': + resolution: {integrity: sha512-n0W4XiXlmEIVqxt+rD3ZpkogsEWUk1jJ+i5bQNgB+1JuWh0fBE8c/blDgTQXa0GB5lTPVDZQussgdNOCnAZwiA==} + engines: {node: '>=10'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@swc/core-linux-arm64-musl@1.9.2': + resolution: {integrity: sha512-8xzrOmsyCC1zrx2Wzx/h8dVsdewO1oMCwBTLc1gSJ/YllZYTb04pNm6NsVbzUX2tKddJVRgSJXV10j/NECLwpA==} + engines: {node: '>=10'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@swc/core-linux-x64-gnu@1.9.2': + resolution: {integrity: sha512-kZrNz/PjRQKcchWF6W292jk3K44EoVu1ad5w+zbS4jekIAxsM8WwQ1kd+yjUlN9jFcF8XBat5NKIs9WphJCVXg==} + engines: {node: '>=10'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@swc/core-linux-x64-musl@1.9.2': + resolution: {integrity: sha512-TTIpR4rjMkhX1lnFR+PSXpaL83TrQzp9znRdp2TzYrODlUd/R20zOwSo9vFLCyH6ZoD47bccY7QeGZDYT3nlRg==} + engines: {node: '>=10'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@swc/core-win32-arm64-msvc@1.9.2': + resolution: {integrity: sha512-+Eg2d4icItKC0PMjZxH7cSYFLWk0aIp94LNmOw6tPq0e69ax6oh10upeq0D1fjWsKLmOJAWEvnXlayZcijEXDw==} + engines: {node: '>=10'} + cpu: [arm64] + os: [win32] + + '@swc/core-win32-ia32-msvc@1.9.2': + resolution: {integrity: sha512-nLWBi4vZDdM/LkiQmPCakof8Dh1/t5EM7eudue04V1lIcqx9YHVRS3KMwEaCoHLGg0c312Wm4YgrWQd9vwZ5zQ==} + engines: {node: '>=10'} + cpu: [ia32] + os: [win32] + + '@swc/core-win32-x64-msvc@1.9.2': + resolution: {integrity: sha512-ik/k+JjRJBFkXARukdU82tSVx0CbExFQoQ78qTO682esbYXzjdB5eLVkoUbwen299pnfr88Kn4kyIqFPTje8Xw==} + engines: {node: '>=10'} + cpu: [x64] + os: [win32] + + '@swc/core@1.9.2': + resolution: {integrity: sha512-dYyEkO6mRYtZFpnOsnYzv9rY69fHAHoawYOjGOEcxk9WYtaJhowMdP/w6NcOKnz2G7GlZaenjkzkMa6ZeQeMsg==} + engines: {node: '>=10'} + peerDependencies: + '@swc/helpers': '*' + peerDependenciesMeta: + '@swc/helpers': + optional: true + + '@swc/counter@0.1.3': + resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==} + + '@swc/types@0.1.15': + resolution: {integrity: sha512-XKaZ+dzDIQ9Ot9o89oJQ/aluI17+VvUnIpYJTcZtvv1iYX6MzHh3Ik2CSR7MdPKpPwfZXHBeCingb2b4PoDVdw==} + + '@tabler/icons-react@3.24.0': + resolution: {integrity: sha512-m9c7TmlcDmKqvZAasG5rv1YvazZDrVEhNdNFa2d1Bzotc0dh+iceFdiZCEcYPDb5UcRyLAMvOaOC9y/5sfMMWw==} + peerDependencies: + react: '>= 16' + + '@tabler/icons@3.24.0': + resolution: {integrity: sha512-qNis9e90QcdxAGV3wNIeX0Ba2R7ktm0cnqOToKHJfC2kj3fvJwEVLsw63K0/fm7NW8rSZjDSTQRmMnSg8g/wrg==} + + '@tanstack/eslint-plugin-query@5.62.1': + resolution: {integrity: sha512-1886D5U+re1TW0wSH4/kUGG36yIoW5Wkz4twVEzlk3ZWmjF3XkRSWgB+Sc7n+Lyzt8usNV8ZqkZE6DA7IC47fQ==} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + + '@tanstack/history@1.85.3': + resolution: {integrity: sha512-62z1qXIILvjdkQyMTVPFQedHOc6kQgunz9GHV9jSy2z1ixsDqyI9GxNj3AWx8Ucmhjwd5/P+v3XN10bsb+FzRA==} + engines: {node: '>=12'} + + '@tanstack/query-core@5.62.3': + resolution: {integrity: sha512-Jp/nYoz8cnO7kqhOlSv8ke/0MJRJVGuZ0P/JO9KQ+f45mpN90hrerzavyTKeSoT/pOzeoOUkv1Xd0wPsxAWXfg==} + + '@tanstack/query-devtools@5.61.4': + resolution: {integrity: sha512-21Tw+u8E3IJJj4A/Bct4H0uBaDTEu7zBrR79FeSyY+mS2gx5/m316oDtJiKkILc819VSTYt+sFzODoJNcpPqZQ==} + + '@tanstack/react-query-devtools@5.62.3': + resolution: {integrity: sha512-4iaQap/iP5ErS094u1WehFntHtjRo6g5HJMvyHovBVbsxnvgPc6AtKAw7qxPPoKy6Wj5Bew0045eYP5phiiBmw==} + peerDependencies: + '@tanstack/react-query': ^5.62.3 + react: ^18 || ^19 + + '@tanstack/react-query@5.62.3': + resolution: {integrity: sha512-y2zDNKuhgiuMgsKkqd4AcsLIBiCfEO8U11AdrtAUihmLbRNztPrlcZqx2lH1GacZsx+y1qRRbCcJLYTtF1vKsw==} + peerDependencies: + react: ^18 || ^19 + + '@tanstack/react-router@1.86.1': + resolution: {integrity: sha512-n9/nb00S1vDvsKv4hC5AWTZ0TRIJYakTwOwcoeRttpB/47/mLBNSs64dPYPzm2K7oukTieqG5zeSkPIt1T/beA==} + engines: {node: '>=12'} + peerDependencies: + '@tanstack/router-generator': ^1.86.0 + react: '>=18' + react-dom: '>=18' + peerDependenciesMeta: + '@tanstack/router-generator': + optional: true + + '@tanstack/react-store@0.6.1': + resolution: {integrity: sha512-6gOopOpPp1cAXkEyTEv6tMbAywwFunvIdCKN/SpEiButUayjXU+Q5Sp5Y3hREN3VMR4OA5+RI5SPhhJoqP9e4w==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + '@tanstack/react-table@8.20.5': + resolution: {integrity: sha512-WEHopKw3znbUZ61s9i0+i9g8drmDo6asTWbrQh8Us63DAk/M0FkmIqERew6P71HI75ksZ2Pxyuf4vvKh9rAkiA==} + engines: {node: '>=12'} + peerDependencies: + react: '>=16.8' + react-dom: '>=16.8' + + '@tanstack/react-virtual@3.11.2': + resolution: {integrity: sha512-OuFzMXPF4+xZgx8UzJha0AieuMihhhaWG0tCqpp6tDzlFwOmNBPYMuLOtMJ1Tr4pXLHmgjcWhG6RlknY2oNTdQ==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + '@tanstack/router-devtools@1.86.1': + resolution: {integrity: sha512-X5MzhGTrpI1ISSv85nfhcmGYStqD2NgO0tVZihyv2g/UHl7uj9jM+3tpwzQvLTW6dd0RNjn6wQ1GCd6fsUeSqg==} + engines: {node: '>=12'} + peerDependencies: + '@tanstack/react-router': ^1.86.1 + react: '>=18' + react-dom: '>=18' + + '@tanstack/router-generator@1.86.0': + resolution: {integrity: sha512-6UrQmysjRNZZp1KnAR4aghqrHJKz+5TwGpq8w+kjVWj7ac6OLekqaWbF+LvfwRs03rLiRNxI9VYHmrSkCvIF+A==} + engines: {node: '>=12'} + + '@tanstack/router-plugin@1.86.0': + resolution: {integrity: sha512-cKBgHBva1amOV+86AKP3DwIT9DYH9keF7GDjqhEMzLHgsTAtJ8IqEQk87xnGw9z55acQJGaVGkP6xuRCACy9eQ==} + engines: {node: '>=12'} + peerDependencies: + '@rsbuild/core': '>=1.0.2' + vite: '>=5.0.0 || >=6.0.0' + webpack: '>=5.92.0' + peerDependenciesMeta: + '@rsbuild/core': + optional: true + vite: + optional: true + webpack: + optional: true + + '@tanstack/store@0.6.0': + resolution: {integrity: sha512-+m2OBglsjXcLmmKOX6/9v8BDOCtyxhMmZLsRUDswOOSdIIR9mvv6i0XNKsmTh3AlYU8c1mRcodC8/Vyf+69VlQ==} + + '@tanstack/table-core@8.20.5': + resolution: {integrity: sha512-P9dF7XbibHph2PFRz8gfBKEXEY/HJPOhym8CHmjF8y3q5mWpKx9xtZapXQUWCgkqvsK0R46Azuz+VaxD4Xl+Tg==} + engines: {node: '>=12'} + + '@tanstack/virtual-core@3.11.2': + resolution: {integrity: sha512-vTtpNt7mKCiZ1pwU9hfKPhpdVO2sVzFQsxoVBGtOSHxlrRRzYr8iQ2TlwbAcRYCcEiZ9ECAM8kBzH0v2+VzfKw==} + + '@tanstack/virtual-file-routes@1.81.9': + resolution: {integrity: sha512-jV5mWJrsh3QXHpb/by6udSqwva0qK50uYHpIXvKsLaxnlbjbLfflfPjFyRWXbMtZsnzCjSUqp5pm5/p+Wpaerg==} + engines: {node: '>=12'} + + '@trivago/prettier-plugin-sort-imports@4.3.0': + resolution: {integrity: sha512-r3n0onD3BTOVUNPhR4lhVK4/pABGpbA7bW3eumZnYdKaHkf1qEC+Mag6DPbGNuuh0eG8AaYj+YqmVHSiGslaTQ==} + peerDependencies: + '@vue/compiler-sfc': 3.x + prettier: 2.x - 3.x + peerDependenciesMeta: + '@vue/compiler-sfc': + optional: true + + '@types/babel__core@7.20.5': + resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} + + '@types/babel__generator@7.6.8': + resolution: {integrity: sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==} + + '@types/babel__template@7.4.4': + resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} + + '@types/babel__traverse@7.20.6': + resolution: {integrity: sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg==} + + '@types/d3-array@3.2.1': + resolution: {integrity: sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==} + + '@types/d3-color@3.1.3': + resolution: {integrity: sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==} + + '@types/d3-ease@3.0.2': + resolution: {integrity: sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==} + + '@types/d3-interpolate@3.0.4': + resolution: {integrity: sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==} + + '@types/d3-path@3.1.0': + resolution: {integrity: sha512-P2dlU/q51fkOc/Gfl3Ul9kicV7l+ra934qBFXCFhrZMOL6du1TM0pm1ThYvENukyOn5h9v+yMJ9Fn5JK4QozrQ==} + + '@types/d3-scale@4.0.8': + resolution: {integrity: sha512-gkK1VVTr5iNiYJ7vWDI+yUFFlszhNMtVeneJ6lUTKPjprsvLLI9/tgEGiXJOnlINJA8FyA88gfnQsHbybVZrYQ==} + + '@types/d3-shape@3.1.6': + resolution: {integrity: sha512-5KKk5aKGu2I+O6SONMYSNflgiP0WfZIQvVUMan50wHsLG1G94JlxEVnCpQARfTtzytuY0p/9PXXZb3I7giofIA==} + + '@types/d3-time@3.0.3': + resolution: {integrity: sha512-2p6olUZ4w3s+07q3Tm2dbiMZy5pCDfYwtLXXHUnVzXgQlZ/OyPtUz6OL382BkOuGlLXqfT+wqv8Fw2v8/0geBw==} + + '@types/d3-timer@3.0.2': + resolution: {integrity: sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==} + + '@types/debug@4.1.12': + resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==} + + '@types/eslint-scope@3.7.7': + resolution: {integrity: sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==} + + '@types/eslint@9.6.1': + resolution: {integrity: sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==} + + '@types/estree-jsx@1.0.5': + resolution: {integrity: sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==} + + '@types/estree@1.0.6': + resolution: {integrity: sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==} + + '@types/file-saver@2.0.7': + resolution: {integrity: sha512-dNKVfHd/jk0SkR/exKGj2ggkB45MAkzvWCaqLUUgkyjITkGNzH8H+yUwr+BLJUBjZOe9w8X3wgmXhZDRg1ED6A==} + + '@types/hast@3.0.4': + resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} + + '@types/js-cookie@3.0.6': + resolution: {integrity: sha512-wkw9yd1kEXOPnvEeEV1Go1MmxtBJL0RR79aOTAApecWFVu7w0NNXNqhcWgvw2YgZDYadliXkl14pa3WXw5jlCQ==} + + '@types/json-schema@7.0.15': + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + + '@types/mdast@4.0.4': + resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} + + '@types/ms@2.1.0': + resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} + + '@types/node@22.10.1': + resolution: {integrity: sha512-qKgsUwfHZV2WCWLAnVP1JqnpE6Im6h3Y0+fYgMTasNQ7V++CBX5OT1as0g0f+OyubbFqhf6XVNIsmN4IIhEgGQ==} + + '@types/prop-types@15.7.14': + resolution: {integrity: sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ==} + + '@types/react-dom@18.3.5': + resolution: {integrity: sha512-P4t6saawp+b/dFrUr2cvkVsfvPguwsxtH6dNIYRllMsefqFzkZk5UIjzyDOv5g1dXIPdG4Sp1yCR4Z6RCUsG/Q==} + peerDependencies: + '@types/react': ^18.0.0 + + '@types/react@18.3.18': + resolution: {integrity: sha512-t4yC+vtgnkYjNSKlFx1jkAhH8LgTo2N/7Qvi83kdEaUtMDiwpbLAktKDaAMlRcJ5eSxZkH74eEGt1ky31d7kfQ==} + + '@types/unist@2.0.11': + resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==} + + '@types/unist@3.0.3': + resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} + + '@typescript-eslint/eslint-plugin@8.17.0': + resolution: {integrity: sha512-HU1KAdW3Tt8zQkdvNoIijfWDMvdSweFYm4hWh+KwhPstv+sCmWb89hCIP8msFm9N1R/ooh9honpSuvqKWlYy3w==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + '@typescript-eslint/parser': ^8.0.0 || ^8.0.0-alpha.0 + eslint: ^8.57.0 || ^9.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@typescript-eslint/parser@8.17.0': + resolution: {integrity: sha512-Drp39TXuUlD49F7ilHHCG7TTg8IkA+hxCuULdmzWYICxGXvDXmDmWEjJYZQYgf6l/TFfYNE167m7isnc3xlIEg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@typescript-eslint/scope-manager@8.17.0': + resolution: {integrity: sha512-/ewp4XjvnxaREtqsZjF4Mfn078RD/9GmiEAtTeLQ7yFdKnqwTOgRMSvFz4et9U5RiJQ15WTGXPLj89zGusvxBg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/type-utils@8.17.0': + resolution: {integrity: sha512-q38llWJYPd63rRnJ6wY/ZQqIzPrBCkPdpIsaCfkR3Q4t3p6sb422zougfad4TFW9+ElIFLVDzWGiGAfbb/v2qw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@typescript-eslint/types@8.17.0': + resolution: {integrity: sha512-gY2TVzeve3z6crqh2Ic7Cr+CAv6pfb0Egee7J5UAVWCpVvDI/F71wNfolIim4FE6hT15EbpZFVUj9j5i38jYXA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/typescript-estree@8.17.0': + resolution: {integrity: sha512-JqkOopc1nRKZpX+opvKqnM3XUlM7LpFMD0lYxTqOTKQfCWAmxw45e3qlOCsEqEB2yuacujivudOFpCnqkBDNMw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@typescript-eslint/utils@8.17.0': + resolution: {integrity: sha512-bQC8BnEkxqG8HBGKwG9wXlZqg37RKSMY7v/X8VEWD8JG2JuTHuNK0VFvMPMUKQcbk6B+tf05k+4AShAEtCtJ/w==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@typescript-eslint/visitor-keys@8.17.0': + resolution: {integrity: sha512-1Hm7THLpO6ww5QU6H/Qp+AusUUl+z/CAm3cNZZ0jQvon9yicgO7Rwd+/WWRpMKLYV6p2UvdbR27c86rzCPpreg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@ungap/structured-clone@1.3.0': + resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} + + '@vitejs/plugin-react-swc@3.7.2': + resolution: {integrity: sha512-y0byko2b2tSVVf5Gpng1eEhX1OvPC7x8yns1Fx8jDzlJp4LS6CMkCPfLw47cjyoMrshQDoQw4qcgjsU9VvlCew==} + peerDependencies: + vite: ^4 || ^5 || ^6 + + '@webassemblyjs/ast@1.14.1': + resolution: {integrity: sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==} + + '@webassemblyjs/floating-point-hex-parser@1.13.2': + resolution: {integrity: sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==} + + '@webassemblyjs/helper-api-error@1.13.2': + resolution: {integrity: sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==} + + '@webassemblyjs/helper-buffer@1.14.1': + resolution: {integrity: sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==} + + '@webassemblyjs/helper-numbers@1.13.2': + resolution: {integrity: sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==} + + '@webassemblyjs/helper-wasm-bytecode@1.13.2': + resolution: {integrity: sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==} + + '@webassemblyjs/helper-wasm-section@1.14.1': + resolution: {integrity: sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==} + + '@webassemblyjs/ieee754@1.13.2': + resolution: {integrity: sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==} + + '@webassemblyjs/leb128@1.13.2': + resolution: {integrity: sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==} + + '@webassemblyjs/utf8@1.13.2': + resolution: {integrity: sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==} + + '@webassemblyjs/wasm-edit@1.14.1': + resolution: {integrity: sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==} + + '@webassemblyjs/wasm-gen@1.14.1': + resolution: {integrity: sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==} + + '@webassemblyjs/wasm-opt@1.14.1': + resolution: {integrity: sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==} + + '@webassemblyjs/wasm-parser@1.14.1': + resolution: {integrity: sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==} + + '@webassemblyjs/wast-printer@1.14.1': + resolution: {integrity: sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==} + + '@xtuc/ieee754@1.2.0': + resolution: {integrity: sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==} + + '@xtuc/long@4.2.2': + resolution: {integrity: sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==} + + ace-builds@1.37.1: + resolution: {integrity: sha512-6/jxFucA1z1C3hgLlVkTE5/znZ+iYvD301vfwtybiMc3k76IDykliCD0xh/eYZMJUfsJtaOQHZ2AJO5ey0PHWw==} + + acorn-jsx@5.3.2: + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + + acorn@8.14.0: + resolution: {integrity: sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==} + engines: {node: '>=0.4.0'} + hasBin: true + + aggregate-error@3.1.0: + resolution: {integrity: sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==} + engines: {node: '>=8'} + + ajv-formats@2.1.1: + resolution: {integrity: sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==} + peerDependencies: + ajv: ^8.0.0 + peerDependenciesMeta: + ajv: + optional: true + + ajv-keywords@5.1.0: + resolution: {integrity: sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==} + peerDependencies: + ajv: ^8.8.2 + + ajv@6.12.6: + resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + + ajv@8.17.1: + resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-regex@6.1.0: + resolution: {integrity: sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==} + engines: {node: '>=12'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + ansi-styles@6.2.1: + resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} + engines: {node: '>=12'} + + any-promise@1.3.0: + resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} + + anymatch@3.1.3: + resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} + engines: {node: '>= 8'} + + arg@5.0.2: + resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + aria-hidden@1.2.4: + resolution: {integrity: sha512-y+CcFFwelSXpLZk/7fMB2mUbGtX9lKycf1MWJ7CaTIERyitVlyQx6C+sxcROU2BAJ24OiZyK+8wj2i8AlBoS3A==} + engines: {node: '>=10'} + + asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + + autoprefixer@10.4.20: + resolution: {integrity: sha512-XY25y5xSv/wEoqzDyXXME4AFfkZI0P23z6Fs3YgymDnKJkCGOnkL0iTxCa85UTqaSgfcqyf3UA6+c7wUvx/16g==} + engines: {node: ^10 || ^12 || >=14} + hasBin: true + peerDependencies: + postcss: ^8.1.0 + + axios@1.7.9: + resolution: {integrity: sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw==} + + babel-dead-code-elimination@1.0.6: + resolution: {integrity: sha512-JxFi9qyRJpN0LjEbbjbN8g0ux71Qppn9R8Qe3k6QzHg2CaKsbUQtbn307LQGiDLGjV6JCtEFqfxzVig9MyDCHQ==} + + bail@2.0.2: + resolution: {integrity: sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==} + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + binary-extensions@2.3.0: + resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} + engines: {node: '>=8'} + + brace-expansion@1.1.11: + resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} + + brace-expansion@2.0.1: + resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} + + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + + browserslist@4.24.2: + resolution: {integrity: sha512-ZIc+Q62revdMcqC6aChtW4jz3My3klmCO1fEmINZY/8J3EpBg5/A/D0AKmBveUh6pgoeycoMkVMko84tuYS+Gg==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + buffer-from@1.1.2: + resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + + callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + + camelcase-css@2.0.1: + resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==} + engines: {node: '>= 6'} + + caniuse-lite@1.0.30001707: + resolution: {integrity: sha512-3qtRjw/HQSMlDWf+X79N206fepf4SOOU6SQLMaq/0KkZLmSjPxAkBOQQ+FxbHKfHmYLZFfdWsO3KA90ceHPSnw==} + + ccount@2.0.1: + resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} + + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + + character-entities-html4@2.1.0: + resolution: {integrity: sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==} + + character-entities-legacy@3.0.0: + resolution: {integrity: sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==} + + character-entities@2.0.2: + resolution: {integrity: sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==} + + character-reference-invalid@2.0.1: + resolution: {integrity: sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==} + + chokidar@3.6.0: + resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} + engines: {node: '>= 8.10.0'} + + chokidar@4.0.3: + resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} + engines: {node: '>= 14.16.0'} + + chrome-trace-event@1.0.4: + resolution: {integrity: sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==} + engines: {node: '>=6.0'} + + class-variance-authority@0.7.1: + resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==} + + clean-stack@2.2.0: + resolution: {integrity: sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==} + engines: {node: '>=6'} + + clone@1.0.4: + resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==} + engines: {node: '>=0.8'} + + clsx@2.1.1: + resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} + engines: {node: '>=6'} + + cmdk@1.0.4: + resolution: {integrity: sha512-AnsjfHyHpQ/EFeAnG216WY7A5LiYCoZzCSygiLvfXC3H3LFGCprErteUcszaVluGOhuOTbJS3jWHrSDYPBBygg==} + peerDependencies: + react: ^18 || ^19 || ^19.0.0-rc + react-dom: ^18 || ^19 || ^19.0.0-rc + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + + comma-separated-tokens@2.0.3: + resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==} + + commander@2.20.3: + resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} + + commander@4.1.1: + resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} + engines: {node: '>= 6'} + + concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + cross-spawn@7.0.5: + resolution: {integrity: sha512-ZVJrKKYunU38/76t0RMOulHOnUcbU9GbpWKAOZ0mhjr7CX6FVrH+4FrAapSOekrgFQ3f/8gwMEuIft0aKq6Hug==} + engines: {node: '>= 8'} + + cssesc@3.0.0: + resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} + engines: {node: '>=4'} + hasBin: true + + csstype@3.1.3: + resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} + + d3-array@3.2.4: + resolution: {integrity: sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==} + engines: {node: '>=12'} + + d3-color@3.1.0: + resolution: {integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==} + engines: {node: '>=12'} + + d3-ease@3.0.1: + resolution: {integrity: sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==} + engines: {node: '>=12'} + + d3-format@3.1.0: + resolution: {integrity: sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==} + engines: {node: '>=12'} + + d3-interpolate@3.0.1: + resolution: {integrity: sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==} + engines: {node: '>=12'} + + d3-path@3.1.0: + resolution: {integrity: sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==} + engines: {node: '>=12'} + + d3-scale@4.0.2: + resolution: {integrity: sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==} + engines: {node: '>=12'} + + d3-shape@3.2.0: + resolution: {integrity: sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==} + engines: {node: '>=12'} + + d3-time-format@4.1.0: + resolution: {integrity: sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==} + engines: {node: '>=12'} + + d3-time@3.1.0: + resolution: {integrity: sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==} + engines: {node: '>=12'} + + d3-timer@3.0.1: + resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==} + engines: {node: '>=12'} + + date-fns@3.6.0: + resolution: {integrity: sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==} + + debug@4.3.7: + resolution: {integrity: sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + decimal.js-light@2.5.1: + resolution: {integrity: sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==} + + decode-named-character-reference@1.1.0: + resolution: {integrity: sha512-Wy+JTSbFThEOXQIR2L6mxJvEs+veIzpmqD7ynWxMXGpnk3smkHQOp6forLdHsKpAMW9iJpaBBIxz285t1n1C3w==} + + deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + + defaults@1.0.4: + resolution: {integrity: sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==} + + delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + + dequal@2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} + + detect-libc@1.0.3: + resolution: {integrity: sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==} + engines: {node: '>=0.10'} + hasBin: true + + detect-node-es@1.1.0: + resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==} + + devlop@1.1.0: + resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} + + didyoumean@1.2.2: + resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==} + + diff-match-patch@1.0.5: + resolution: {integrity: sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw==} + + dlv@1.1.3: + resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==} + + dom-helpers@5.2.1: + resolution: {integrity: sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==} + + eastasianwidth@0.2.0: + resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + + easy-table@1.2.0: + resolution: {integrity: sha512-OFzVOv03YpvtcWGe5AayU5G2hgybsg3iqA6drU8UaoZyB9jLGMTrz9+asnLp/E+6qPh88yEI1gvyZFZ41dmgww==} + + electron-to-chromium@1.5.57: + resolution: {integrity: sha512-xS65H/tqgOwUBa5UmOuNSLuslDo7zho0y/lgQw35pnrqiZh7UOWHCeL/Bt6noJATbA6tpQJGCifsFsIRZj1Fqg==} + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + emoji-regex@9.2.2: + resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + + enhanced-resolve@5.18.0: + resolution: {integrity: sha512-0/r0MySGYG8YqlayBZ6MuCfECmHFdJ5qyPh8s8wa5Hnm6SaFLSK1VYCbj+NKp090Nm1caZhD+QTnmxO7esYGyQ==} + engines: {node: '>=10.13.0'} + + es-module-lexer@1.7.0: + resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} + + esbuild@0.23.1: + resolution: {integrity: sha512-VVNz/9Sa0bs5SELtn3f7qhJCDPCF5oMEl5cO9/SSinpE9hbPVvxbd572HH5AKiP7WD8INO53GgfDDhRjkylHEg==} + engines: {node: '>=18'} + hasBin: true + + esbuild@0.24.2: + resolution: {integrity: sha512-+9egpBW8I3CD5XPe0n6BfT5fxLzxrlDzqydF3aviG+9ni1lDC/OvMHcxqEFV0+LANZG5R1bFMWfUrjVsdwxJvA==} + engines: {node: '>=18'} + hasBin: true + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + escape-string-regexp@5.0.0: + resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==} + engines: {node: '>=12'} + + eslint-plugin-react-hooks@5.1.0: + resolution: {integrity: sha512-mpJRtPgHN2tNAvZ35AMfqeB3Xqeo273QxrHJsbBEPWODRM4r0yB6jfoROqKEYrOn27UtRPpcpHc2UqyBSuUNTw==} + engines: {node: '>=10'} + peerDependencies: + eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 + + eslint-plugin-react-refresh@0.4.16: + resolution: {integrity: sha512-slterMlxAhov/DZO8NScf6mEeMBBXodFUolijDvrtTxyezyLoTQaa73FyYus/VbTdftd8wBgBxPMRk3poleXNQ==} + peerDependencies: + eslint: '>=8.40' + + eslint-scope@5.1.1: + resolution: {integrity: sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==} + engines: {node: '>=8.0.0'} + + eslint-scope@8.2.0: + resolution: {integrity: sha512-PHlWUfG6lvPc3yvP5A4PNyBL1W8fkDUccmI21JUu/+GKZBoH/W5u6usENXUrWFRsyoW5ACUjFGgAFQp5gUlb/A==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint-visitor-keys@4.2.0: + resolution: {integrity: sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint@9.16.0: + resolution: {integrity: sha512-whp8mSQI4C8VXd+fLgSM0lh3UlmcFtVwUQjyKCFfsp+2ItAIYhlq/hqGahGqHE6cv9unM41VlqKk2VtKYR2TaA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + hasBin: true + peerDependencies: + jiti: '*' + peerDependenciesMeta: + jiti: + optional: true + + espree@10.3.0: + resolution: {integrity: sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + esquery@1.6.0: + resolution: {integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==} + engines: {node: '>=0.10'} + + esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + + estraverse@4.3.0: + resolution: {integrity: sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==} + engines: {node: '>=4.0'} + + estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + + estree-util-is-identifier-name@3.0.0: + resolution: {integrity: sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==} + + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + + eventemitter3@4.0.7: + resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==} + + events@3.3.0: + resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} + engines: {node: '>=0.8.x'} + + extend@3.0.2: + resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-equals@5.0.1: + resolution: {integrity: sha512-WF1Wi8PwwSY7/6Kx0vKXtw8RwuSGoM1bvDaJbu7MxDlR1vovZjIAKrnzyrThgAjm6JDTu0fVgWXDlMGspodfoQ==} + engines: {node: '>=6.0.0'} + + fast-glob@3.3.2: + resolution: {integrity: sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==} + engines: {node: '>=8.6.0'} + + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + + fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + + fast-uri@3.0.6: + resolution: {integrity: sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw==} + + fastq@1.17.1: + resolution: {integrity: sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==} + + file-entry-cache@8.0.0: + resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} + engines: {node: '>=16.0.0'} + + file-saver@2.0.5: + resolution: {integrity: sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA==} + + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + + find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + + flat-cache@4.0.1: + resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} + engines: {node: '>=16'} + + flatted@3.3.1: + resolution: {integrity: sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==} + + follow-redirects@1.15.9: + resolution: {integrity: sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + + foreground-child@3.3.0: + resolution: {integrity: sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==} + engines: {node: '>=14'} + + form-data@4.0.1: + resolution: {integrity: sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==} + engines: {node: '>= 6'} + + fraction.js@4.3.7: + resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + + get-nonce@1.0.1: + resolution: {integrity: sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==} + engines: {node: '>=6'} + + get-tsconfig@4.8.1: + resolution: {integrity: sha512-k9PN+cFBmaLWtVz29SkUoqU5O0slLuHJXt/2P+tMVFT+phsSGXGkp9t3rQIqdz0e+06EHNGs3oM6ZX1s2zHxRg==} + + glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + + glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + + glob-to-regexp@0.4.1: + resolution: {integrity: sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==} + + glob@10.4.5: + resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==} + hasBin: true + + globals@11.12.0: + resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==} + engines: {node: '>=4'} + + globals@14.0.0: + resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} + engines: {node: '>=18'} + + globals@15.13.0: + resolution: {integrity: sha512-49TewVEz0UxZjr1WYYsWpPrhyC/B/pA8Bq0fUmet2n+eR7yn0IvNzNaoBwnK6mdkzcN+se7Ez9zUgULTz2QH4g==} + engines: {node: '>=18'} + + goober@2.1.16: + resolution: {integrity: sha512-erjk19y1U33+XAMe1VTvIONHYoSqE4iS7BYUZfHaqeohLmnC0FdxEh7rQU+6MZ4OajItzjZFSRtVANrQwNq6/g==} + peerDependencies: + csstype: ^3.0.10 + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + graphemer@1.4.0: + resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} + + handlebars@4.7.8: + resolution: {integrity: sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==} + engines: {node: '>=0.4.7'} + hasBin: true + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + + hast-util-to-jsx-runtime@2.3.6: + resolution: {integrity: sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==} + + hast-util-whitespace@3.0.0: + resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==} + + html-url-attributes@3.0.1: + resolution: {integrity: sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==} + + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + + immutable@5.1.3: + resolution: {integrity: sha512-+chQdDfvscSF1SJqv2gn4SRO2ZyS3xL3r7IW/wWEEzrzLisnOlKiQu5ytC/BVNcS15C39WT2Hg/bjKjDMcu+zg==} + + import-fresh@3.3.0: + resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==} + engines: {node: '>=6'} + + imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + + indent-string@4.0.0: + resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} + engines: {node: '>=8'} + + inline-style-parser@0.2.4: + resolution: {integrity: sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q==} + + internmap@2.0.3: + resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==} + engines: {node: '>=12'} + + invariant@2.2.4: + resolution: {integrity: sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==} + + is-alphabetical@2.0.1: + resolution: {integrity: sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==} + + is-alphanumerical@2.0.1: + resolution: {integrity: sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==} + + is-binary-path@2.1.0: + resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} + engines: {node: '>=8'} + + is-core-module@2.15.1: + resolution: {integrity: sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==} + engines: {node: '>= 0.4'} + + is-decimal@2.0.1: + resolution: {integrity: sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-hexadecimal@2.0.1: + resolution: {integrity: sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==} + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + is-plain-obj@4.1.0: + resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} + engines: {node: '>=12'} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + jackspeak@3.4.3: + resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} + + javascript-natural-sort@0.7.1: + resolution: {integrity: sha512-nO6jcEfZWQXDhOiBtG2KvKyEptz7RVbpGP4vTD2hLBdmNQSsCiicO2Ioinv6UI4y9ukqnBpy+XZ9H6uLNgJTlw==} + + jest-worker@27.5.1: + resolution: {integrity: sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==} + engines: {node: '>= 10.13.0'} + + jiti@1.21.6: + resolution: {integrity: sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w==} + hasBin: true + + jiti@2.4.2: + resolution: {integrity: sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==} + hasBin: true + + js-cookie@3.0.5: + resolution: {integrity: sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==} + engines: {node: '>=14'} + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + js-yaml@4.1.0: + resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} + hasBin: true + + jsesc@2.5.2: + resolution: {integrity: sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==} + engines: {node: '>=4'} + hasBin: true + + jsesc@3.0.2: + resolution: {integrity: sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==} + engines: {node: '>=6'} + hasBin: true + + json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + + json-parse-even-better-errors@2.3.1: + resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} + + json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + + json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + + json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + + keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + + knip@5.41.1: + resolution: {integrity: sha512-yNpCCe2REU7U3VRvMASnXSEtfEC2HmOoDW9Vp9teQ9FktJYnuagvSZD3xWq8Ru7sPABkmvbC5TVWuMzIaeADNA==} + engines: {node: '>=18.6.0'} + hasBin: true + peerDependencies: + '@types/node': '>=18' + typescript: '>=5.0.4' + + levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + + lilconfig@3.1.3: + resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==} + engines: {node: '>=14'} + + lines-and-columns@1.2.4: + resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + + loader-runner@4.3.0: + resolution: {integrity: sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==} + engines: {node: '>=6.11.5'} + + locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + + lodash.get@4.4.2: + resolution: {integrity: sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==} + + lodash.isequal@4.5.0: + resolution: {integrity: sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==} + + lodash.merge@4.6.2: + resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + + lodash@4.17.21: + resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + + longest-streak@3.1.0: + resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==} + + loose-envify@1.4.0: + resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} + hasBin: true + + lru-cache@10.4.3: + resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + + lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + + lucide-react@0.468.0: + resolution: {integrity: sha512-6koYRhnM2N0GGZIdXzSeiNwguv1gt/FAjZOiPl76roBi3xKEXa4WmfpxgQwTTL4KipXjefrnf3oV4IsYhi4JFA==} + peerDependencies: + react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0-rc + + markdown-table@3.0.4: + resolution: {integrity: sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==} + + mdast-util-find-and-replace@3.0.2: + resolution: {integrity: sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==} + + mdast-util-from-markdown@2.0.2: + resolution: {integrity: sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA==} + + mdast-util-gfm-autolink-literal@2.0.1: + resolution: {integrity: sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==} + + mdast-util-gfm-footnote@2.1.0: + resolution: {integrity: sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==} + + mdast-util-gfm-strikethrough@2.0.0: + resolution: {integrity: sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==} + + mdast-util-gfm-table@2.0.0: + resolution: {integrity: sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==} + + mdast-util-gfm-task-list-item@2.0.0: + resolution: {integrity: sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==} + + mdast-util-gfm@3.1.0: + resolution: {integrity: sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==} + + mdast-util-mdx-expression@2.0.1: + resolution: {integrity: sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==} + + mdast-util-mdx-jsx@3.2.0: + resolution: {integrity: sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==} + + mdast-util-mdxjs-esm@2.0.1: + resolution: {integrity: sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==} + + mdast-util-phrasing@4.1.0: + resolution: {integrity: sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==} + + mdast-util-to-hast@13.2.0: + resolution: {integrity: sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA==} + + mdast-util-to-markdown@2.1.2: + resolution: {integrity: sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==} + + mdast-util-to-string@4.0.0: + resolution: {integrity: sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==} + + merge-stream@2.0.0: + resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} + + merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + + micromark-core-commonmark@2.0.3: + resolution: {integrity: sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==} + + micromark-extension-gfm-autolink-literal@2.1.0: + resolution: {integrity: sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==} + + micromark-extension-gfm-footnote@2.1.0: + resolution: {integrity: sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==} + + micromark-extension-gfm-strikethrough@2.1.0: + resolution: {integrity: sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==} + + micromark-extension-gfm-table@2.1.1: + resolution: {integrity: sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==} + + micromark-extension-gfm-tagfilter@2.0.0: + resolution: {integrity: sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==} + + micromark-extension-gfm-task-list-item@2.1.0: + resolution: {integrity: sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==} + + micromark-extension-gfm@3.0.0: + resolution: {integrity: sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==} + + micromark-factory-destination@2.0.1: + resolution: {integrity: sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==} + + micromark-factory-label@2.0.1: + resolution: {integrity: sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==} + + micromark-factory-space@2.0.1: + resolution: {integrity: sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==} + + micromark-factory-title@2.0.1: + resolution: {integrity: sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==} + + micromark-factory-whitespace@2.0.1: + resolution: {integrity: sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==} + + micromark-util-character@2.1.1: + resolution: {integrity: sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==} + + micromark-util-chunked@2.0.1: + resolution: {integrity: sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==} + + micromark-util-classify-character@2.0.1: + resolution: {integrity: sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==} + + micromark-util-combine-extensions@2.0.1: + resolution: {integrity: sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==} + + micromark-util-decode-numeric-character-reference@2.0.2: + resolution: {integrity: sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==} + + micromark-util-decode-string@2.0.1: + resolution: {integrity: sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==} + + micromark-util-encode@2.0.1: + resolution: {integrity: sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==} + + micromark-util-html-tag-name@2.0.1: + resolution: {integrity: sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==} + + micromark-util-normalize-identifier@2.0.1: + resolution: {integrity: sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==} + + micromark-util-resolve-all@2.0.1: + resolution: {integrity: sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==} + + micromark-util-sanitize-uri@2.0.1: + resolution: {integrity: sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==} + + micromark-util-subtokenize@2.1.0: + resolution: {integrity: sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==} + + micromark-util-symbol@2.0.1: + resolution: {integrity: sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==} + + micromark-util-types@2.0.2: + resolution: {integrity: sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==} + + micromark@4.0.2: + resolution: {integrity: sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==} + + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + + minimatch@3.1.2: + resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + + minimatch@9.0.5: + resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} + engines: {node: '>=16 || 14 >=14.17'} + + minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + + minipass@7.1.2: + resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} + engines: {node: '>=16 || 14 >=14.17'} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + mz@2.7.0: + resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} + + nanoid@3.3.8: + resolution: {integrity: sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + + neo-async@2.6.2: + resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} + + node-addon-api@7.1.1: + resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==} + + node-releases@2.0.18: + resolution: {integrity: sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==} + + normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + + normalize-range@0.1.2: + resolution: {integrity: sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==} + engines: {node: '>=0.10.0'} + + object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + + object-hash@3.0.0: + resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==} + engines: {node: '>= 6'} + + optionator@0.9.4: + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} + engines: {node: '>= 0.8.0'} + + p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + + p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + + p-map@4.0.0: + resolution: {integrity: sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==} + engines: {node: '>=10'} + + package-json-from-dist@1.0.1: + resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} + + parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + + parse-entities@4.0.2: + resolution: {integrity: sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==} + + parse-ms@4.0.0: + resolution: {integrity: sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==} + engines: {node: '>=18'} + + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + + path-scurry@1.11.1: + resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} + engines: {node: '>=16 || 14 >=14.18'} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + + picomatch@4.0.2: + resolution: {integrity: sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==} + engines: {node: '>=12'} + + pify@2.3.0: + resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==} + engines: {node: '>=0.10.0'} + + pirates@4.0.6: + resolution: {integrity: sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==} + engines: {node: '>= 6'} + + postcss-import@15.1.0: + resolution: {integrity: sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==} + engines: {node: '>=14.0.0'} + peerDependencies: + postcss: ^8.0.0 + + postcss-js@4.0.1: + resolution: {integrity: sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==} + engines: {node: ^12 || ^14 || >= 16} + peerDependencies: + postcss: ^8.4.21 + + postcss-load-config@4.0.2: + resolution: {integrity: sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==} + engines: {node: '>= 14'} + peerDependencies: + postcss: '>=8.0.9' + ts-node: '>=9.0.0' + peerDependenciesMeta: + postcss: + optional: true + ts-node: + optional: true + + postcss-nested@6.2.0: + resolution: {integrity: sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==} + engines: {node: '>=12.0'} + peerDependencies: + postcss: ^8.2.14 + + postcss-selector-parser@6.1.2: + resolution: {integrity: sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==} + engines: {node: '>=4'} + + postcss-value-parser@4.2.0: + resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} + + postcss@8.4.49: + resolution: {integrity: sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==} + engines: {node: ^10 || ^12 || >=14} + + prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + + prettier-plugin-tailwindcss@0.6.9: + resolution: {integrity: sha512-r0i3uhaZAXYP0At5xGfJH876W3HHGHDp+LCRUJrs57PBeQ6mYHMwr25KH8NPX44F2yGTvdnH7OqCshlQx183Eg==} + engines: {node: '>=14.21.3'} + peerDependencies: + '@ianvs/prettier-plugin-sort-imports': '*' + '@prettier/plugin-pug': '*' + '@shopify/prettier-plugin-liquid': '*' + '@trivago/prettier-plugin-sort-imports': '*' + '@zackad/prettier-plugin-twig-melody': '*' + prettier: ^3.0 + prettier-plugin-astro: '*' + prettier-plugin-css-order: '*' + prettier-plugin-import-sort: '*' + prettier-plugin-jsdoc: '*' + prettier-plugin-marko: '*' + prettier-plugin-multiline-arrays: '*' + prettier-plugin-organize-attributes: '*' + prettier-plugin-organize-imports: '*' + prettier-plugin-sort-imports: '*' + prettier-plugin-style-order: '*' + prettier-plugin-svelte: '*' + peerDependenciesMeta: + '@ianvs/prettier-plugin-sort-imports': + optional: true + '@prettier/plugin-pug': + optional: true + '@shopify/prettier-plugin-liquid': + optional: true + '@trivago/prettier-plugin-sort-imports': + optional: true + '@zackad/prettier-plugin-twig-melody': + optional: true + prettier-plugin-astro: + optional: true + prettier-plugin-css-order: + optional: true + prettier-plugin-import-sort: + optional: true + prettier-plugin-jsdoc: + optional: true + prettier-plugin-marko: + optional: true + prettier-plugin-multiline-arrays: + optional: true + prettier-plugin-organize-attributes: + optional: true + prettier-plugin-organize-imports: + optional: true + prettier-plugin-sort-imports: + optional: true + prettier-plugin-style-order: + optional: true + prettier-plugin-svelte: + optional: true + + prettier@3.4.2: + resolution: {integrity: sha512-e9MewbtFo+Fevyuxn/4rrcDAaq0IYxPGLvObpQjiZBMAzB9IGmzlnG9RZy3FFas+eBMu2vA0CszMeduow5dIuQ==} + engines: {node: '>=14'} + hasBin: true + + pretty-ms@9.2.0: + resolution: {integrity: sha512-4yf0QO/sllf/1zbZWYnvWw3NxCQwLXKzIj0G849LSufP15BXKM0rbD2Z3wVnkMfjdn/CB0Dpp444gYAACdsplg==} + engines: {node: '>=18'} + + prop-types@15.8.1: + resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} + + property-information@7.0.0: + resolution: {integrity: sha512-7D/qOz/+Y4X/rzSB6jKxKUsQnphO046ei8qxG59mtM3RG3DHgTK81HrxrmoDVINJb8NKT5ZsRbwHvQ6B68Iyhg==} + + proxy-from-env@1.1.0: + resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + + randombytes@2.1.0: + resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==} + + react-ace@13.0.0: + resolution: {integrity: sha512-PPk2O/ArHzDtbnK82QImfHYXwuiitRgHJf5AxwMQh9zciojbWsPmKJm1tMgWOYLCtGEz8/Dh3MxRxrXe7QcstQ==} + peerDependencies: + react: ^0.13.0 || ^0.14.0 || ^15.0.1 || ^16.0.0 || ^17.0.0 || ^18.0.0 + react-dom: ^0.13.0 || ^0.14.0 || ^15.0.1 || ^16.0.0 || ^17.0.0 || ^18.0.0 + + react-day-picker@8.10.1: + resolution: {integrity: sha512-TMx7fNbhLk15eqcMt+7Z7S2KF7mfTId/XJDjKE8f+IUcFn0l08/kI4FiYTL/0yuOLmEcbR4Fwe3GJf/NiiMnPA==} + peerDependencies: + date-fns: ^2.28.0 || ^3.0.0 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + + react-dom@18.3.1: + resolution: {integrity: sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==} + peerDependencies: + react: ^18.3.1 + + react-hook-form@7.54.0: + resolution: {integrity: sha512-PS05+UQy/IdSbJNojBypxAo9wllhHgGmyr8/dyGQcPoiMf3e7Dfb9PWYVRco55bLbxH9S+1yDDJeTdlYCSxO3A==} + engines: {node: '>=18.0.0'} + peerDependencies: + react: ^16.8.0 || ^17 || ^18 || ^19 + + react-is@16.13.1: + resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} + + react-is@18.3.1: + resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} + + react-markdown@10.1.0: + resolution: {integrity: sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ==} + peerDependencies: + '@types/react': '>=18' + react: '>=18' + + react-remove-scroll-bar@2.3.6: + resolution: {integrity: sha512-DtSYaao4mBmX+HDo5YWYdBWQwYIQQshUV/dVxFxK+KM26Wjwp1gZ6rv6OC3oujI6Bfu6Xyg3TwK533AQutsn/g==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + + react-remove-scroll@2.6.0: + resolution: {integrity: sha512-I2U4JVEsQenxDAKaVa3VZ/JeJZe0/2DxPWL8Tj8yLKctQJQiZM52pn/GWFpSp8dftjM3pSAHVJZscAnC/y+ySQ==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + + react-resizable-panels@2.1.7: + resolution: {integrity: sha512-JtT6gI+nURzhMYQYsx8DKkx6bSoOGFp7A3CwMrOb8y5jFHFyqwo9m68UhmXRw57fRVJksFn1TSlm3ywEQ9vMgA==} + peerDependencies: + react: ^16.14.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + react-dom: ^16.14.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + + react-smooth@4.0.1: + resolution: {integrity: sha512-OE4hm7XqR0jNOq3Qmk9mFLyd6p2+j6bvbPJ7qlB7+oo0eNcL2l7WQzG6MBnT3EXY6xzkLMUBec3AfewJdA0J8w==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 + + react-style-singleton@2.2.1: + resolution: {integrity: sha512-ZWj0fHEMyWkHzKYUr2Bs/4zU6XLmq9HsgBURm7g5pAVfyn49DgUiNgY2d4lXRlYSiCif9YBGpQleewkcqddc7g==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + + react-transition-group@4.4.5: + resolution: {integrity: sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==} + peerDependencies: + react: '>=16.6.0' + react-dom: '>=16.6.0' + + react@18.3.1: + resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==} + engines: {node: '>=0.10.0'} + + read-cache@1.0.0: + resolution: {integrity: sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==} + + readdirp@3.6.0: + resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} + engines: {node: '>=8.10.0'} + + readdirp@4.1.2: + resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} + engines: {node: '>= 14.18.0'} + + recharts-scale@0.4.5: + resolution: {integrity: sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w==} + + recharts@2.14.1: + resolution: {integrity: sha512-xtWulflkA+/xu4/QClBdtZYN30dbvTHjxjkh5XTMrH/CQ3WGDDPHHa/LLKCbgoqz0z3UaSH2/blV1i6VNMeh1g==} + engines: {node: '>=14'} + peerDependencies: + react: ^16.0.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.0.0 || ^17.0.0 || ^18.0.0 + + regenerator-runtime@0.14.1: + resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==} + + remark-gfm@4.0.1: + resolution: {integrity: sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==} + + remark-parse@11.0.0: + resolution: {integrity: sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==} + + remark-rehype@11.1.2: + resolution: {integrity: sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==} + + remark-stringify@11.0.0: + resolution: {integrity: sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==} + + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + + resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + + resolve-pkg-maps@1.0.0: + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + + resolve@1.22.8: + resolution: {integrity: sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==} + hasBin: true + + reusify@1.0.4: + resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + + rollup@4.26.0: + resolution: {integrity: sha512-ilcl12hnWonG8f+NxU6BlgysVA0gvY2l8N0R84S1HcINbW20bvwuCngJkkInV6LXhwRpucsW5k1ovDwEdBVrNg==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + + sass@1.89.2: + resolution: {integrity: sha512-xCmtksBKd/jdJ9Bt9p7nPKiuqrlBMBuuGkQlkhZjjQk3Ty48lv93k5Dq6OPkKt4XwxDJ7tvlfrTa1MPA9bf+QA==} + engines: {node: '>=14.0.0'} + hasBin: true + + scheduler@0.23.2: + resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==} + + schema-utils@4.3.2: + resolution: {integrity: sha512-Gn/JaSk/Mt9gYubxTtSn/QCV4em9mpAPiR1rqy/Ocu19u/G9J5WWdNoUT4SiV6mFC3y6cxyFcFwdzPM3FgxGAQ==} + engines: {node: '>= 10.13.0'} + + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + + semver@7.6.3: + resolution: {integrity: sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==} + engines: {node: '>=10'} + hasBin: true + + serialize-javascript@6.0.2: + resolution: {integrity: sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==} + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + + smol-toml@1.3.1: + resolution: {integrity: sha512-tEYNll18pPKHroYSmLLrksq233j021G0giwW7P3D24jC54pQ5W5BXMsQ/Mvw1OJCmEYDgY+lrzT+3nNUtoNfXQ==} + engines: {node: '>= 18'} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + source-map-support@0.5.21: + resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} + + source-map@0.5.7: + resolution: {integrity: sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==} + engines: {node: '>=0.10.0'} + + source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + + space-separated-tokens@2.0.2: + resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==} + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + string-width@5.1.2: + resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} + engines: {node: '>=12'} + + stringify-entities@4.0.4: + resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-ansi@7.1.0: + resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==} + engines: {node: '>=12'} + + strip-json-comments@3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + + strip-json-comments@5.0.1: + resolution: {integrity: sha512-0fk9zBqO67Nq5M/m45qHCJxylV/DhBlIOVExqgOMiCCrzrhU6tCibRXNqE3jwJLftzE9SNuZtYbpzcO+i9FiKw==} + engines: {node: '>=14.16'} + + style-to-js@1.1.16: + resolution: {integrity: sha512-/Q6ld50hKYPH3d/r6nr117TZkHR0w0kGGIVfpG9N6D8NymRPM9RqCUv4pRpJ62E5DqOYx2AFpbZMyCPnjQCnOw==} + + style-to-object@1.0.8: + resolution: {integrity: sha512-xT47I/Eo0rwJmaXC4oilDGDWLohVhR6o/xAQcPQN8q6QBuZVL8qMYL85kLmST5cPjAorwvqIA4qXTRQoYHaL6g==} + + sucrase@3.35.0: + resolution: {integrity: sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==} + engines: {node: '>=16 || 14 >=14.17'} + hasBin: true + + summary@2.1.0: + resolution: {integrity: sha512-nMIjMrd5Z2nuB2RZCKJfFMjgS3fygbeyGk9PxPPaJR1RIcyN9yn4A63Isovzm3ZtQuEkLBVgMdPup8UeLH7aQw==} + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + supports-color@8.1.1: + resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} + engines: {node: '>=10'} + + supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + + tailwind-merge@2.5.5: + resolution: {integrity: sha512-0LXunzzAZzo0tEPxV3I297ffKZPlKDrjj7NXphC8V5ak9yHC5zRmxnOe2m/Rd/7ivsOMJe3JZ2JVocoDdQTRBA==} + + tailwindcss-animate@1.0.7: + resolution: {integrity: sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA==} + peerDependencies: + tailwindcss: '>=3.0.0 || insiders' + + tailwindcss@3.4.16: + resolution: {integrity: sha512-TI4Cyx7gDiZ6r44ewaJmt0o6BrMCT5aK5e0rmJ/G9Xq3w7CX/5VXl/zIPEJZFUK5VEqwByyhqNPycPlvcK4ZNw==} + engines: {node: '>=14.0.0'} + hasBin: true + + tapable@2.2.1: + resolution: {integrity: sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==} + engines: {node: '>=6'} + + terser-webpack-plugin@5.3.14: + resolution: {integrity: sha512-vkZjpUjb6OMS7dhV+tILUW6BhpDR7P2L/aQSAv+Uwk+m8KATX9EccViHTJR2qDtACKPIYndLGCyl3FMo+r2LMw==} + engines: {node: '>= 10.13.0'} + peerDependencies: + '@swc/core': '*' + esbuild: '*' + uglify-js: '*' + webpack: ^5.1.0 + peerDependenciesMeta: + '@swc/core': + optional: true + esbuild: + optional: true + uglify-js: + optional: true + + terser@5.43.1: + resolution: {integrity: sha512-+6erLbBm0+LROX2sPXlUYx/ux5PyE9K/a92Wrt6oA+WDAoFTdpHE5tCYCI5PNzq2y8df4rA+QgHLJuR4jNymsg==} + engines: {node: '>=10'} + hasBin: true + + thenify-all@1.6.0: + resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} + engines: {node: '>=0.8'} + + thenify@3.3.1: + resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} + + tiny-invariant@1.3.3: + resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} + + tiny-warning@1.0.3: + resolution: {integrity: sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==} + + to-fast-properties@2.0.0: + resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==} + engines: {node: '>=4'} + + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + + trim-lines@3.0.1: + resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} + + trough@2.2.0: + resolution: {integrity: sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==} + + ts-api-utils@1.4.0: + resolution: {integrity: sha512-032cPxaEKwM+GT3vA5JXNzIaizx388rhsSW79vGRNGXfRRAdEAn2mvk36PvK5HnOchyWZ7afLEXqYCvPCrzuzQ==} + engines: {node: '>=16'} + peerDependencies: + typescript: '>=4.2.0' + + ts-interface-checker@0.1.13: + resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + tsx@4.19.2: + resolution: {integrity: sha512-pOUl6Vo2LUq/bSa8S5q7b91cgNSjctn9ugq/+Mvow99qW6x/UZYwzxy/3NmqoT66eHYfCVvFvACC58UBPFf28g==} + engines: {node: '>=18.0.0'} + hasBin: true + + type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + + typescript-eslint@8.17.0: + resolution: {integrity: sha512-409VXvFd/f1br1DCbuKNFqQpXICoTB+V51afcwG1pn1a3Cp92MqAUges3YjwEdQ0cMUoCIodjVDAYzyD8h3SYA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + typescript@5.7.2: + resolution: {integrity: sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==} + engines: {node: '>=14.17'} + hasBin: true + + uglify-js@3.19.3: + resolution: {integrity: sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==} + engines: {node: '>=0.8.0'} + hasBin: true + + undici-types@6.20.0: + resolution: {integrity: sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==} + + unified@11.0.5: + resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==} + + unist-util-is@6.0.0: + resolution: {integrity: sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==} + + unist-util-position@5.0.0: + resolution: {integrity: sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==} + + unist-util-stringify-position@4.0.0: + resolution: {integrity: sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==} + + unist-util-visit-parents@6.0.1: + resolution: {integrity: sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw==} + + unist-util-visit@5.0.0: + resolution: {integrity: sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==} + + unplugin@1.16.0: + resolution: {integrity: sha512-5liCNPuJW8dqh3+DM6uNM2EI3MLLpCKp/KY+9pB5M2S2SR2qvvDHhKgBOaTWEbZTAws3CXfB0rKTIolWKL05VQ==} + engines: {node: '>=14.0.0'} + + update-browserslist-db@1.1.1: + resolution: {integrity: sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + + use-callback-ref@1.3.2: + resolution: {integrity: sha512-elOQwe6Q8gqZgDA8mrh44qRTQqpIHDcZ3hXTLjBe1i4ph8XpNJnO+aQf3NaG+lriLopI4HMx9VjQLfPQ6vhnoA==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + + use-sidecar@1.1.2: + resolution: {integrity: sha512-epTbsLuzZ7lPClpz2TyryBfztm7m+28DlEv2ZCQ3MDr5ssiwyOwGH/e5F9CkfWjJ1t4clvI58yF822/GUkjjhw==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': ^16.9.0 || ^17.0.0 || ^18.0.0 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + + use-sync-external-store@1.2.2: + resolution: {integrity: sha512-PElTlVMwpblvbNqQ82d2n6RjStvdSoNe9FG28kNfz3WiXilJm4DdNkEzRhCZuIDwY8U08WVihhGR5iRqAwfDiw==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + + vaul@1.1.2: + resolution: {integrity: sha512-ZFkClGpWyI2WUQjdLJ/BaGuV6AVQiJ3uELGk3OYtP+B6yCO7Cmn9vPFXVJkRaGkOJu3m8bQMgtyzNHixULceQA==} + peerDependencies: + react: ^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc + + vfile-message@4.0.2: + resolution: {integrity: sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw==} + + vfile@6.0.3: + resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==} + + victory-vendor@36.9.2: + resolution: {integrity: sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==} + + vite@6.0.11: + resolution: {integrity: sha512-4VL9mQPKoHy4+FE0NnRE/kbY51TOfaknxAjt3fJbGJxhIpBZiqVzlZDEesWWsuREXHwNdAoOFZ9MkPEVXczHwg==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + peerDependencies: + '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 + jiti: '>=1.21.0' + less: '*' + lightningcss: ^1.21.0 + sass: '*' + sass-embedded: '*' + stylus: '*' + sugarss: '*' + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + jiti: + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + watchpack@2.4.4: + resolution: {integrity: sha512-c5EGNOiyxxV5qmTtAB7rbiXxi1ooX1pQKMLX/MIabJjRA0SJBQOjKF+KSVfHkr9U1cADPon0mRiVe/riyaiDUA==} + engines: {node: '>=10.13.0'} + + wcwidth@1.0.1: + resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==} + + webpack-sources@3.3.3: + resolution: {integrity: sha512-yd1RBzSGanHkitROoPFd6qsrxt+oFhg/129YzheDGqeustzX0vTZJZsSsQjVQC4yzBQ56K55XU8gaNCtIzOnTg==} + engines: {node: '>=10.13.0'} + + webpack-virtual-modules@0.6.2: + resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==} + + webpack@5.99.9: + resolution: {integrity: sha512-brOPwM3JnmOa+7kd3NsmOUOwbDAj8FT9xDsG3IW0MgbN9yZV7Oi/s/+MNQ/EcSMqw7qfoRyXPoeEWT8zLVdVGg==} + engines: {node: '>=10.13.0'} + hasBin: true + peerDependencies: + webpack-cli: '*' + peerDependenciesMeta: + webpack-cli: + optional: true + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + word-wrap@1.2.5: + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} + engines: {node: '>=0.10.0'} + + wordwrap@1.0.0: + resolution: {integrity: sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==} + + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + + wrap-ansi@8.1.0: + resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} + engines: {node: '>=12'} + + yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + + yaml@2.6.0: + resolution: {integrity: sha512-a6ae//JvKDEra2kdi1qzCyrJW/WZCgFi8ydDV+eXExl95t+5R+ijnqHJbz9tmMh8FUjx3iv2fCQ4dclAQlO2UQ==} + engines: {node: '>= 14'} + hasBin: true + + yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + + zod-validation-error@3.4.0: + resolution: {integrity: sha512-ZOPR9SVY6Pb2qqO5XHt+MkkTRxGXb4EVtnjc9JpXUOtUB1T9Ru7mZOT361AN3MsetVe7R0a1KZshJDZdgp9miQ==} + engines: {node: '>=18.0.0'} + peerDependencies: + zod: ^3.18.0 + + zod@3.23.8: + resolution: {integrity: sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==} + + zustand@5.0.2: + resolution: {integrity: sha512-8qNdnJVJlHlrKXi50LDqqUNmUbuBjoKLrYQBnoChIbVph7vni+sY+YpvdjXG9YLd/Bxr6scMcR+rm5H3aSqPaw==} + engines: {node: '>=12.20.0'} + peerDependencies: + '@types/react': '>=18.0.0' + immer: '>=9.0.6' + react: '>=18.0.0' + use-sync-external-store: '>=1.2.0' + peerDependenciesMeta: + '@types/react': + optional: true + immer: + optional: true + react: + optional: true + use-sync-external-store: + optional: true + + zwitch@2.0.4: + resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} + +snapshots: + + '@alloc/quick-lru@5.2.0': {} + + '@ampproject/remapping@2.3.0': + dependencies: + '@jridgewell/gen-mapping': 0.3.5 + '@jridgewell/trace-mapping': 0.3.25 + + '@babel/code-frame@7.26.2': + dependencies: + '@babel/helper-validator-identifier': 7.25.9 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/compat-data@7.26.2': {} + + '@babel/core@7.26.0': + dependencies: + '@ampproject/remapping': 2.3.0 + '@babel/code-frame': 7.26.2 + '@babel/generator': 7.26.2 + '@babel/helper-compilation-targets': 7.25.9 + '@babel/helper-module-transforms': 7.26.0(@babel/core@7.26.0) + '@babel/helpers': 7.26.0 + '@babel/parser': 7.26.2 + '@babel/template': 7.25.9 + '@babel/traverse': 7.25.9 + '@babel/types': 7.26.0 + convert-source-map: 2.0.0 + debug: 4.3.7 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/generator@7.17.7': + dependencies: + '@babel/types': 7.26.0 + jsesc: 2.5.2 + source-map: 0.5.7 + + '@babel/generator@7.26.2': + dependencies: + '@babel/parser': 7.26.2 + '@babel/types': 7.26.0 + '@jridgewell/gen-mapping': 0.3.5 + '@jridgewell/trace-mapping': 0.3.25 + jsesc: 3.0.2 + + '@babel/helper-compilation-targets@7.25.9': + dependencies: + '@babel/compat-data': 7.26.2 + '@babel/helper-validator-option': 7.25.9 + browserslist: 4.24.2 + lru-cache: 5.1.1 + semver: 6.3.1 + + '@babel/helper-environment-visitor@7.24.7': + dependencies: + '@babel/types': 7.26.0 + + '@babel/helper-function-name@7.24.7': + dependencies: + '@babel/template': 7.25.9 + '@babel/types': 7.26.0 + + '@babel/helper-hoist-variables@7.24.7': + dependencies: + '@babel/types': 7.26.0 + + '@babel/helper-module-imports@7.25.9': + dependencies: + '@babel/traverse': 7.25.9 + '@babel/types': 7.26.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.26.0(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-module-imports': 7.25.9 + '@babel/helper-validator-identifier': 7.25.9 + '@babel/traverse': 7.25.9 + transitivePeerDependencies: + - supports-color + + '@babel/helper-plugin-utils@7.25.9': {} + + '@babel/helper-split-export-declaration@7.24.7': + dependencies: + '@babel/types': 7.26.0 + + '@babel/helper-string-parser@7.25.9': {} + + '@babel/helper-validator-identifier@7.25.9': {} + + '@babel/helper-validator-option@7.25.9': {} + + '@babel/helpers@7.26.0': + dependencies: + '@babel/template': 7.25.9 + '@babel/types': 7.26.0 + + '@babel/parser@7.26.2': + dependencies: + '@babel/types': 7.26.0 + + '@babel/plugin-syntax-jsx@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-syntax-typescript@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/runtime@7.26.0': + dependencies: + regenerator-runtime: 0.14.1 + + '@babel/template@7.25.9': + dependencies: + '@babel/code-frame': 7.26.2 + '@babel/parser': 7.26.2 + '@babel/types': 7.26.0 + + '@babel/traverse@7.23.2': + dependencies: + '@babel/code-frame': 7.26.2 + '@babel/generator': 7.26.2 + '@babel/helper-environment-visitor': 7.24.7 + '@babel/helper-function-name': 7.24.7 + '@babel/helper-hoist-variables': 7.24.7 + '@babel/helper-split-export-declaration': 7.24.7 + '@babel/parser': 7.26.2 + '@babel/types': 7.26.0 + debug: 4.3.7 + globals: 11.12.0 + transitivePeerDependencies: + - supports-color + + '@babel/traverse@7.25.9': + dependencies: + '@babel/code-frame': 7.26.2 + '@babel/generator': 7.26.2 + '@babel/parser': 7.26.2 + '@babel/template': 7.25.9 + '@babel/types': 7.26.0 + debug: 4.3.7 + globals: 11.12.0 + transitivePeerDependencies: + - supports-color + + '@babel/types@7.17.0': + dependencies: + '@babel/helper-validator-identifier': 7.25.9 + to-fast-properties: 2.0.0 + + '@babel/types@7.26.0': + dependencies: + '@babel/helper-string-parser': 7.25.9 + '@babel/helper-validator-identifier': 7.25.9 + + '@esbuild/aix-ppc64@0.23.1': + optional: true + + '@esbuild/aix-ppc64@0.24.2': + optional: true + + '@esbuild/android-arm64@0.23.1': + optional: true + + '@esbuild/android-arm64@0.24.2': + optional: true + + '@esbuild/android-arm@0.23.1': + optional: true + + '@esbuild/android-arm@0.24.2': + optional: true + + '@esbuild/android-x64@0.23.1': + optional: true + + '@esbuild/android-x64@0.24.2': + optional: true + + '@esbuild/darwin-arm64@0.23.1': + optional: true + + '@esbuild/darwin-arm64@0.24.2': + optional: true + + '@esbuild/darwin-x64@0.23.1': + optional: true + + '@esbuild/darwin-x64@0.24.2': + optional: true + + '@esbuild/freebsd-arm64@0.23.1': + optional: true + + '@esbuild/freebsd-arm64@0.24.2': + optional: true + + '@esbuild/freebsd-x64@0.23.1': + optional: true + + '@esbuild/freebsd-x64@0.24.2': + optional: true + + '@esbuild/linux-arm64@0.23.1': + optional: true + + '@esbuild/linux-arm64@0.24.2': + optional: true + + '@esbuild/linux-arm@0.23.1': + optional: true + + '@esbuild/linux-arm@0.24.2': + optional: true + + '@esbuild/linux-ia32@0.23.1': + optional: true + + '@esbuild/linux-ia32@0.24.2': + optional: true + + '@esbuild/linux-loong64@0.23.1': + optional: true + + '@esbuild/linux-loong64@0.24.2': + optional: true + + '@esbuild/linux-mips64el@0.23.1': + optional: true + + '@esbuild/linux-mips64el@0.24.2': + optional: true + + '@esbuild/linux-ppc64@0.23.1': + optional: true + + '@esbuild/linux-ppc64@0.24.2': + optional: true + + '@esbuild/linux-riscv64@0.23.1': + optional: true + + '@esbuild/linux-riscv64@0.24.2': + optional: true + + '@esbuild/linux-s390x@0.23.1': + optional: true + + '@esbuild/linux-s390x@0.24.2': + optional: true + + '@esbuild/linux-x64@0.23.1': + optional: true + + '@esbuild/linux-x64@0.24.2': + optional: true + + '@esbuild/netbsd-arm64@0.24.2': + optional: true + + '@esbuild/netbsd-x64@0.23.1': + optional: true + + '@esbuild/netbsd-x64@0.24.2': + optional: true + + '@esbuild/openbsd-arm64@0.23.1': + optional: true + + '@esbuild/openbsd-arm64@0.24.2': + optional: true + + '@esbuild/openbsd-x64@0.23.1': + optional: true + + '@esbuild/openbsd-x64@0.24.2': + optional: true + + '@esbuild/sunos-x64@0.23.1': + optional: true + + '@esbuild/sunos-x64@0.24.2': + optional: true + + '@esbuild/win32-arm64@0.23.1': + optional: true + + '@esbuild/win32-arm64@0.24.2': + optional: true + + '@esbuild/win32-ia32@0.23.1': + optional: true + + '@esbuild/win32-ia32@0.24.2': + optional: true + + '@esbuild/win32-x64@0.23.1': + optional: true + + '@esbuild/win32-x64@0.24.2': + optional: true + + '@eslint-community/eslint-utils@4.4.1(eslint@9.16.0(jiti@2.4.2))': + dependencies: + eslint: 9.16.0(jiti@2.4.2) + eslint-visitor-keys: 3.4.3 + + '@eslint-community/regexpp@4.12.1': {} + + '@eslint/config-array@0.19.1': + dependencies: + '@eslint/object-schema': 2.1.5 + debug: 4.3.7 + minimatch: 3.1.2 + transitivePeerDependencies: + - supports-color + + '@eslint/core@0.9.1': + dependencies: + '@types/json-schema': 7.0.15 + + '@eslint/eslintrc@3.2.0': + dependencies: + ajv: 6.12.6 + debug: 4.3.7 + espree: 10.3.0 + globals: 14.0.0 + ignore: 5.3.2 + import-fresh: 3.3.0 + js-yaml: 4.1.0 + minimatch: 3.1.2 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + + '@eslint/js@9.16.0': {} + + '@eslint/object-schema@2.1.5': {} + + '@eslint/plugin-kit@0.2.4': + dependencies: + levn: 0.4.1 + + '@faker-js/faker@9.3.0': {} + + '@floating-ui/core@1.6.8': + dependencies: + '@floating-ui/utils': 0.2.8 + + '@floating-ui/dom@1.6.12': + dependencies: + '@floating-ui/core': 1.6.8 + '@floating-ui/utils': 0.2.8 + + '@floating-ui/react-dom@2.1.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@floating-ui/dom': 1.6.12 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + '@floating-ui/utils@0.2.8': {} + + '@hookform/resolvers@3.9.1(react-hook-form@7.54.0(react@18.3.1))': + dependencies: + react-hook-form: 7.54.0(react@18.3.1) + + '@humanfs/core@0.19.1': {} + + '@humanfs/node@0.16.6': + dependencies: + '@humanfs/core': 0.19.1 + '@humanwhocodes/retry': 0.3.1 + + '@humanwhocodes/module-importer@1.0.1': {} + + '@humanwhocodes/retry@0.3.1': {} + + '@humanwhocodes/retry@0.4.1': {} + + '@isaacs/cliui@8.0.2': + dependencies: + string-width: 5.1.2 + string-width-cjs: string-width@4.2.3 + strip-ansi: 7.1.0 + strip-ansi-cjs: strip-ansi@6.0.1 + wrap-ansi: 8.1.0 + wrap-ansi-cjs: wrap-ansi@7.0.0 + + '@jridgewell/gen-mapping@0.3.5': + dependencies: + '@jridgewell/set-array': 1.2.1 + '@jridgewell/sourcemap-codec': 1.5.0 + '@jridgewell/trace-mapping': 0.3.25 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/set-array@1.2.1': {} + + '@jridgewell/source-map@0.3.10': + dependencies: + '@jridgewell/gen-mapping': 0.3.5 + '@jridgewell/trace-mapping': 0.3.25 + optional: true + + '@jridgewell/sourcemap-codec@1.5.0': {} + + '@jridgewell/trace-mapping@0.3.25': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.0 + + '@nodelib/fs.scandir@2.1.5': + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + + '@nodelib/fs.stat@2.0.5': {} + + '@nodelib/fs.walk@1.2.8': + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.17.1 + + '@parcel/watcher-android-arm64@2.5.1': + optional: true + + '@parcel/watcher-darwin-arm64@2.5.1': + optional: true + + '@parcel/watcher-darwin-x64@2.5.1': + optional: true + + '@parcel/watcher-freebsd-x64@2.5.1': + optional: true + + '@parcel/watcher-linux-arm-glibc@2.5.1': + optional: true + + '@parcel/watcher-linux-arm-musl@2.5.1': + optional: true + + '@parcel/watcher-linux-arm64-glibc@2.5.1': + optional: true + + '@parcel/watcher-linux-arm64-musl@2.5.1': + optional: true + + '@parcel/watcher-linux-x64-glibc@2.5.1': + optional: true + + '@parcel/watcher-linux-x64-musl@2.5.1': + optional: true + + '@parcel/watcher-win32-arm64@2.5.1': + optional: true + + '@parcel/watcher-win32-ia32@2.5.1': + optional: true + + '@parcel/watcher-win32-x64@2.5.1': + optional: true + + '@parcel/watcher@2.5.1': + dependencies: + detect-libc: 1.0.3 + is-glob: 4.0.3 + micromatch: 4.0.8 + node-addon-api: 7.1.1 + optionalDependencies: + '@parcel/watcher-android-arm64': 2.5.1 + '@parcel/watcher-darwin-arm64': 2.5.1 + '@parcel/watcher-darwin-x64': 2.5.1 + '@parcel/watcher-freebsd-x64': 2.5.1 + '@parcel/watcher-linux-arm-glibc': 2.5.1 + '@parcel/watcher-linux-arm-musl': 2.5.1 + '@parcel/watcher-linux-arm64-glibc': 2.5.1 + '@parcel/watcher-linux-arm64-musl': 2.5.1 + '@parcel/watcher-linux-x64-glibc': 2.5.1 + '@parcel/watcher-linux-x64-musl': 2.5.1 + '@parcel/watcher-win32-arm64': 2.5.1 + '@parcel/watcher-win32-ia32': 2.5.1 + '@parcel/watcher-win32-x64': 2.5.1 + optional: true + + '@pkgjs/parseargs@0.11.0': + optional: true + + '@radix-ui/number@1.1.0': {} + + '@radix-ui/primitive@1.1.0': {} + + '@radix-ui/primitive@1.1.1': {} + + '@radix-ui/react-accordion@1.2.2(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.1 + '@radix-ui/react-collapsible': 1.1.2(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-collection': 1.1.1(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.1(@types/react@18.3.18)(react@18.3.1) + '@radix-ui/react-context': 1.1.1(@types/react@18.3.18)(react@18.3.1) + '@radix-ui/react-direction': 1.1.0(@types/react@18.3.18)(react@18.3.1) + '@radix-ui/react-id': 1.1.0(@types/react@18.3.18)(react@18.3.1) + '@radix-ui/react-primitive': 2.0.1(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.3.18)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.18 + '@types/react-dom': 18.3.5(@types/react@18.3.18) + + '@radix-ui/react-alert-dialog@1.1.2(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.0 + '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.18)(react@18.3.1) + '@radix-ui/react-context': 1.1.1(@types/react@18.3.18)(react@18.3.1) + '@radix-ui/react-dialog': 1.1.2(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-slot': 1.1.0(@types/react@18.3.18)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.18 + '@types/react-dom': 18.3.5(@types/react@18.3.18) + + '@radix-ui/react-arrow@1.1.0(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.18 + '@types/react-dom': 18.3.5(@types/react@18.3.18) + + '@radix-ui/react-arrow@1.1.1(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-primitive': 2.0.1(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.18 + '@types/react-dom': 18.3.5(@types/react@18.3.18) + + '@radix-ui/react-avatar@1.1.1(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-context': 1.1.1(@types/react@18.3.18)(react@18.3.1) + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.3.18)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.3.18)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.18 + '@types/react-dom': 18.3.5(@types/react@18.3.18) + + '@radix-ui/react-checkbox@1.1.2(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.0 + '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.18)(react@18.3.1) + '@radix-ui/react-context': 1.1.1(@types/react@18.3.18)(react@18.3.1) + '@radix-ui/react-presence': 1.1.1(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.3.18)(react@18.3.1) + '@radix-ui/react-use-previous': 1.1.0(@types/react@18.3.18)(react@18.3.1) + '@radix-ui/react-use-size': 1.1.0(@types/react@18.3.18)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.18 + '@types/react-dom': 18.3.5(@types/react@18.3.18) + + '@radix-ui/react-collapsible@1.1.1(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.0 + '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.18)(react@18.3.1) + '@radix-ui/react-context': 1.1.1(@types/react@18.3.18)(react@18.3.1) + '@radix-ui/react-id': 1.1.0(@types/react@18.3.18)(react@18.3.1) + '@radix-ui/react-presence': 1.1.1(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.3.18)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.3.18)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.18 + '@types/react-dom': 18.3.5(@types/react@18.3.18) + + '@radix-ui/react-collapsible@1.1.2(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.1 + '@radix-ui/react-compose-refs': 1.1.1(@types/react@18.3.18)(react@18.3.1) + '@radix-ui/react-context': 1.1.1(@types/react@18.3.18)(react@18.3.1) + '@radix-ui/react-id': 1.1.0(@types/react@18.3.18)(react@18.3.1) + '@radix-ui/react-presence': 1.1.2(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.0.1(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.3.18)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.3.18)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.18 + '@types/react-dom': 18.3.5(@types/react@18.3.18) + + '@radix-ui/react-collection@1.1.0(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.18)(react@18.3.1) + '@radix-ui/react-context': 1.1.0(@types/react@18.3.18)(react@18.3.1) + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-slot': 1.1.0(@types/react@18.3.18)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.18 + '@types/react-dom': 18.3.5(@types/react@18.3.18) + + '@radix-ui/react-collection@1.1.1(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.1(@types/react@18.3.18)(react@18.3.1) + '@radix-ui/react-context': 1.1.1(@types/react@18.3.18)(react@18.3.1) + '@radix-ui/react-primitive': 2.0.1(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-slot': 1.1.1(@types/react@18.3.18)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.18 + '@types/react-dom': 18.3.5(@types/react@18.3.18) + + '@radix-ui/react-collection@1.1.2(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.1(@types/react@18.3.18)(react@18.3.1) + '@radix-ui/react-context': 1.1.1(@types/react@18.3.18)(react@18.3.1) + '@radix-ui/react-primitive': 2.0.2(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-slot': 1.1.2(@types/react@18.3.18)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.18 + '@types/react-dom': 18.3.5(@types/react@18.3.18) + + '@radix-ui/react-compose-refs@1.1.0(@types/react@18.3.18)(react@18.3.1)': + dependencies: + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.18 + + '@radix-ui/react-compose-refs@1.1.1(@types/react@18.3.18)(react@18.3.1)': + dependencies: + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.18 + + '@radix-ui/react-compose-refs@1.1.2(@types/react@18.3.18)(react@18.3.1)': + dependencies: + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.18 + + '@radix-ui/react-context@1.1.0(@types/react@18.3.18)(react@18.3.1)': + dependencies: + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.18 + + '@radix-ui/react-context@1.1.1(@types/react@18.3.18)(react@18.3.1)': + dependencies: + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.18 + + '@radix-ui/react-context@1.1.3(@types/react@18.3.18)(react@18.3.1)': + dependencies: + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.18 + + '@radix-ui/react-dialog@1.1.2(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.0 + '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.18)(react@18.3.1) + '@radix-ui/react-context': 1.1.1(@types/react@18.3.18)(react@18.3.1) + '@radix-ui/react-dismissable-layer': 1.1.1(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-focus-guards': 1.1.1(@types/react@18.3.18)(react@18.3.1) + '@radix-ui/react-focus-scope': 1.1.0(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-id': 1.1.0(@types/react@18.3.18)(react@18.3.1) + '@radix-ui/react-portal': 1.1.2(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-presence': 1.1.1(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-slot': 1.1.0(@types/react@18.3.18)(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.3.18)(react@18.3.1) + aria-hidden: 1.2.4 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-remove-scroll: 2.6.0(@types/react@18.3.18)(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.18 + '@types/react-dom': 18.3.5(@types/react@18.3.18) + + '@radix-ui/react-direction@1.1.0(@types/react@18.3.18)(react@18.3.1)': + dependencies: + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.18 + + '@radix-ui/react-dismissable-layer@1.1.1(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.0 + '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.18)(react@18.3.1) + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.3.18)(react@18.3.1) + '@radix-ui/react-use-escape-keydown': 1.1.0(@types/react@18.3.18)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.18 + '@types/react-dom': 18.3.5(@types/react@18.3.18) + + '@radix-ui/react-dismissable-layer@1.1.3(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.1 + '@radix-ui/react-compose-refs': 1.1.1(@types/react@18.3.18)(react@18.3.1) + '@radix-ui/react-primitive': 2.0.1(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.3.18)(react@18.3.1) + '@radix-ui/react-use-escape-keydown': 1.1.0(@types/react@18.3.18)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.18 + '@types/react-dom': 18.3.5(@types/react@18.3.18) + + '@radix-ui/react-dismissable-layer@1.1.5(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.1 + '@radix-ui/react-compose-refs': 1.1.1(@types/react@18.3.18)(react@18.3.1) + '@radix-ui/react-primitive': 2.0.2(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.3.18)(react@18.3.1) + '@radix-ui/react-use-escape-keydown': 1.1.0(@types/react@18.3.18)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.18 + '@types/react-dom': 18.3.5(@types/react@18.3.18) + + '@radix-ui/react-dropdown-menu@2.1.2(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.0 + '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.18)(react@18.3.1) + '@radix-ui/react-context': 1.1.1(@types/react@18.3.18)(react@18.3.1) + '@radix-ui/react-id': 1.1.0(@types/react@18.3.18)(react@18.3.1) + '@radix-ui/react-menu': 2.1.2(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.3.18)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.18 + '@types/react-dom': 18.3.5(@types/react@18.3.18) + + '@radix-ui/react-focus-guards@1.1.1(@types/react@18.3.18)(react@18.3.1)': + dependencies: + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.18 + + '@radix-ui/react-focus-scope@1.1.0(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.18)(react@18.3.1) + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.3.18)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.18 + '@types/react-dom': 18.3.5(@types/react@18.3.18) + + '@radix-ui/react-hover-card@1.1.4(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.1 + '@radix-ui/react-compose-refs': 1.1.1(@types/react@18.3.18)(react@18.3.1) + '@radix-ui/react-context': 1.1.1(@types/react@18.3.18)(react@18.3.1) + '@radix-ui/react-dismissable-layer': 1.1.3(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-popper': 1.2.1(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-portal': 1.1.3(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-presence': 1.1.2(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.0.1(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.3.18)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.18 + '@types/react-dom': 18.3.5(@types/react@18.3.18) + + '@radix-ui/react-icons@1.3.2(react@18.3.1)': + dependencies: + react: 18.3.1 + + '@radix-ui/react-id@1.1.0(@types/react@18.3.18)(react@18.3.1)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.3.18)(react@18.3.1) + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.18 + + '@radix-ui/react-label@2.1.0(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.18 + '@types/react-dom': 18.3.5(@types/react@18.3.18) + + '@radix-ui/react-menu@2.1.2(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.0 + '@radix-ui/react-collection': 1.1.0(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.18)(react@18.3.1) + '@radix-ui/react-context': 1.1.1(@types/react@18.3.18)(react@18.3.1) + '@radix-ui/react-direction': 1.1.0(@types/react@18.3.18)(react@18.3.1) + '@radix-ui/react-dismissable-layer': 1.1.1(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-focus-guards': 1.1.1(@types/react@18.3.18)(react@18.3.1) + '@radix-ui/react-focus-scope': 1.1.0(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-id': 1.1.0(@types/react@18.3.18)(react@18.3.1) + '@radix-ui/react-popper': 1.2.0(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-portal': 1.1.2(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-presence': 1.1.1(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-roving-focus': 1.1.0(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-slot': 1.1.0(@types/react@18.3.18)(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.3.18)(react@18.3.1) + aria-hidden: 1.2.4 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-remove-scroll: 2.6.0(@types/react@18.3.18)(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.18 + '@types/react-dom': 18.3.5(@types/react@18.3.18) + + '@radix-ui/react-navigation-menu@1.2.5(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.1 + '@radix-ui/react-collection': 1.1.2(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.1(@types/react@18.3.18)(react@18.3.1) + '@radix-ui/react-context': 1.1.1(@types/react@18.3.18)(react@18.3.1) + '@radix-ui/react-direction': 1.1.0(@types/react@18.3.18)(react@18.3.1) + '@radix-ui/react-dismissable-layer': 1.1.5(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-id': 1.1.0(@types/react@18.3.18)(react@18.3.1) + '@radix-ui/react-presence': 1.1.2(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.0.2(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.3.18)(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.3.18)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.3.18)(react@18.3.1) + '@radix-ui/react-use-previous': 1.1.0(@types/react@18.3.18)(react@18.3.1) + '@radix-ui/react-visually-hidden': 1.1.2(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.18 + '@types/react-dom': 18.3.5(@types/react@18.3.18) + + '@radix-ui/react-popover@1.1.2(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.0 + '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.18)(react@18.3.1) + '@radix-ui/react-context': 1.1.1(@types/react@18.3.18)(react@18.3.1) + '@radix-ui/react-dismissable-layer': 1.1.1(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-focus-guards': 1.1.1(@types/react@18.3.18)(react@18.3.1) + '@radix-ui/react-focus-scope': 1.1.0(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-id': 1.1.0(@types/react@18.3.18)(react@18.3.1) + '@radix-ui/react-popper': 1.2.0(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-portal': 1.1.2(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-presence': 1.1.1(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-slot': 1.1.0(@types/react@18.3.18)(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.3.18)(react@18.3.1) + aria-hidden: 1.2.4 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-remove-scroll: 2.6.0(@types/react@18.3.18)(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.18 + '@types/react-dom': 18.3.5(@types/react@18.3.18) + + '@radix-ui/react-popper@1.2.0(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@floating-ui/react-dom': 2.1.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-arrow': 1.1.0(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.18)(react@18.3.1) + '@radix-ui/react-context': 1.1.0(@types/react@18.3.18)(react@18.3.1) + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.3.18)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.3.18)(react@18.3.1) + '@radix-ui/react-use-rect': 1.1.0(@types/react@18.3.18)(react@18.3.1) + '@radix-ui/react-use-size': 1.1.0(@types/react@18.3.18)(react@18.3.1) + '@radix-ui/rect': 1.1.0 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.18 + '@types/react-dom': 18.3.5(@types/react@18.3.18) + + '@radix-ui/react-popper@1.2.1(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@floating-ui/react-dom': 2.1.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-arrow': 1.1.1(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.1(@types/react@18.3.18)(react@18.3.1) + '@radix-ui/react-context': 1.1.1(@types/react@18.3.18)(react@18.3.1) + '@radix-ui/react-primitive': 2.0.1(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.3.18)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.3.18)(react@18.3.1) + '@radix-ui/react-use-rect': 1.1.0(@types/react@18.3.18)(react@18.3.1) + '@radix-ui/react-use-size': 1.1.0(@types/react@18.3.18)(react@18.3.1) + '@radix-ui/rect': 1.1.0 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.18 + '@types/react-dom': 18.3.5(@types/react@18.3.18) + + '@radix-ui/react-portal@1.1.2(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.3.18)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.18 + '@types/react-dom': 18.3.5(@types/react@18.3.18) + + '@radix-ui/react-portal@1.1.3(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-primitive': 2.0.1(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.3.18)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.18 + '@types/react-dom': 18.3.5(@types/react@18.3.18) + + '@radix-ui/react-presence@1.1.1(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.18)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.3.18)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.18 + '@types/react-dom': 18.3.5(@types/react@18.3.18) + + '@radix-ui/react-presence@1.1.2(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.1(@types/react@18.3.18)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.3.18)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.18 + '@types/react-dom': 18.3.5(@types/react@18.3.18) + + '@radix-ui/react-primitive@2.0.0(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-slot': 1.1.0(@types/react@18.3.18)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.18 + '@types/react-dom': 18.3.5(@types/react@18.3.18) + + '@radix-ui/react-primitive@2.0.1(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-slot': 1.1.1(@types/react@18.3.18)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.18 + '@types/react-dom': 18.3.5(@types/react@18.3.18) + + '@radix-ui/react-primitive@2.0.2(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-slot': 1.1.2(@types/react@18.3.18)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.18 + '@types/react-dom': 18.3.5(@types/react@18.3.18) + + '@radix-ui/react-primitive@2.1.4(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-slot': 1.2.4(@types/react@18.3.18)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.18 + '@types/react-dom': 18.3.5(@types/react@18.3.18) + + '@radix-ui/react-progress@1.1.8(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-context': 1.1.3(@types/react@18.3.18)(react@18.3.1) + '@radix-ui/react-primitive': 2.1.4(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.18 + '@types/react-dom': 18.3.5(@types/react@18.3.18) + + '@radix-ui/react-radio-group@1.2.1(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.0 + '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.18)(react@18.3.1) + '@radix-ui/react-context': 1.1.1(@types/react@18.3.18)(react@18.3.1) + '@radix-ui/react-direction': 1.1.0(@types/react@18.3.18)(react@18.3.1) + '@radix-ui/react-presence': 1.1.1(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-roving-focus': 1.1.0(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.3.18)(react@18.3.1) + '@radix-ui/react-use-previous': 1.1.0(@types/react@18.3.18)(react@18.3.1) + '@radix-ui/react-use-size': 1.1.0(@types/react@18.3.18)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.18 + '@types/react-dom': 18.3.5(@types/react@18.3.18) + + '@radix-ui/react-roving-focus@1.1.0(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.0 + '@radix-ui/react-collection': 1.1.0(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.18)(react@18.3.1) + '@radix-ui/react-context': 1.1.0(@types/react@18.3.18)(react@18.3.1) + '@radix-ui/react-direction': 1.1.0(@types/react@18.3.18)(react@18.3.1) + '@radix-ui/react-id': 1.1.0(@types/react@18.3.18)(react@18.3.1) + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.3.18)(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.3.18)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.18 + '@types/react-dom': 18.3.5(@types/react@18.3.18) + + '@radix-ui/react-scroll-area@1.2.1(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/number': 1.1.0 + '@radix-ui/primitive': 1.1.0 + '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.18)(react@18.3.1) + '@radix-ui/react-context': 1.1.1(@types/react@18.3.18)(react@18.3.1) + '@radix-ui/react-direction': 1.1.0(@types/react@18.3.18)(react@18.3.1) + '@radix-ui/react-presence': 1.1.1(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.3.18)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.3.18)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.18 + '@types/react-dom': 18.3.5(@types/react@18.3.18) + + '@radix-ui/react-select@2.1.2(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/number': 1.1.0 + '@radix-ui/primitive': 1.1.0 + '@radix-ui/react-collection': 1.1.0(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.18)(react@18.3.1) + '@radix-ui/react-context': 1.1.1(@types/react@18.3.18)(react@18.3.1) + '@radix-ui/react-direction': 1.1.0(@types/react@18.3.18)(react@18.3.1) + '@radix-ui/react-dismissable-layer': 1.1.1(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-focus-guards': 1.1.1(@types/react@18.3.18)(react@18.3.1) + '@radix-ui/react-focus-scope': 1.1.0(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-id': 1.1.0(@types/react@18.3.18)(react@18.3.1) + '@radix-ui/react-popper': 1.2.0(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-portal': 1.1.2(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-slot': 1.1.0(@types/react@18.3.18)(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.3.18)(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.3.18)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.3.18)(react@18.3.1) + '@radix-ui/react-use-previous': 1.1.0(@types/react@18.3.18)(react@18.3.1) + '@radix-ui/react-visually-hidden': 1.1.0(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + aria-hidden: 1.2.4 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-remove-scroll: 2.6.0(@types/react@18.3.18)(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.18 + '@types/react-dom': 18.3.5(@types/react@18.3.18) + + '@radix-ui/react-separator@1.1.0(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.18 + '@types/react-dom': 18.3.5(@types/react@18.3.18) + + '@radix-ui/react-slot@1.1.0(@types/react@18.3.18)(react@18.3.1)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.18)(react@18.3.1) + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.18 + + '@radix-ui/react-slot@1.1.1(@types/react@18.3.18)(react@18.3.1)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.1(@types/react@18.3.18)(react@18.3.1) + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.18 + + '@radix-ui/react-slot@1.1.2(@types/react@18.3.18)(react@18.3.1)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.1(@types/react@18.3.18)(react@18.3.1) + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.18 + + '@radix-ui/react-slot@1.2.4(@types/react@18.3.18)(react@18.3.1)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.18)(react@18.3.1) + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.18 + + '@radix-ui/react-switch@1.1.1(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.0 + '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.18)(react@18.3.1) + '@radix-ui/react-context': 1.1.1(@types/react@18.3.18)(react@18.3.1) + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.3.18)(react@18.3.1) + '@radix-ui/react-use-previous': 1.1.0(@types/react@18.3.18)(react@18.3.1) + '@radix-ui/react-use-size': 1.1.0(@types/react@18.3.18)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.18 + '@types/react-dom': 18.3.5(@types/react@18.3.18) + + '@radix-ui/react-tabs@1.1.1(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.0 + '@radix-ui/react-context': 1.1.1(@types/react@18.3.18)(react@18.3.1) + '@radix-ui/react-direction': 1.1.0(@types/react@18.3.18)(react@18.3.1) + '@radix-ui/react-id': 1.1.0(@types/react@18.3.18)(react@18.3.1) + '@radix-ui/react-presence': 1.1.1(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-roving-focus': 1.1.0(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.3.18)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.18 + '@types/react-dom': 18.3.5(@types/react@18.3.18) + + '@radix-ui/react-toast@1.2.2(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.0 + '@radix-ui/react-collection': 1.1.0(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.18)(react@18.3.1) + '@radix-ui/react-context': 1.1.1(@types/react@18.3.18)(react@18.3.1) + '@radix-ui/react-dismissable-layer': 1.1.1(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-portal': 1.1.2(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-presence': 1.1.1(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.3.18)(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.3.18)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.3.18)(react@18.3.1) + '@radix-ui/react-visually-hidden': 1.1.0(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.18 + '@types/react-dom': 18.3.5(@types/react@18.3.18) + + '@radix-ui/react-tooltip@1.1.4(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.0 + '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.18)(react@18.3.1) + '@radix-ui/react-context': 1.1.1(@types/react@18.3.18)(react@18.3.1) + '@radix-ui/react-dismissable-layer': 1.1.1(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-id': 1.1.0(@types/react@18.3.18)(react@18.3.1) + '@radix-ui/react-popper': 1.2.0(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-portal': 1.1.2(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-presence': 1.1.1(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-slot': 1.1.0(@types/react@18.3.18)(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.3.18)(react@18.3.1) + '@radix-ui/react-visually-hidden': 1.1.0(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.18 + '@types/react-dom': 18.3.5(@types/react@18.3.18) + + '@radix-ui/react-use-callback-ref@1.1.0(@types/react@18.3.18)(react@18.3.1)': + dependencies: + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.18 + + '@radix-ui/react-use-controllable-state@1.1.0(@types/react@18.3.18)(react@18.3.1)': + dependencies: + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.3.18)(react@18.3.1) + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.18 + + '@radix-ui/react-use-escape-keydown@1.1.0(@types/react@18.3.18)(react@18.3.1)': + dependencies: + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.3.18)(react@18.3.1) + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.18 + + '@radix-ui/react-use-layout-effect@1.1.0(@types/react@18.3.18)(react@18.3.1)': + dependencies: + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.18 + + '@radix-ui/react-use-previous@1.1.0(@types/react@18.3.18)(react@18.3.1)': + dependencies: + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.18 + + '@radix-ui/react-use-rect@1.1.0(@types/react@18.3.18)(react@18.3.1)': + dependencies: + '@radix-ui/rect': 1.1.0 + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.18 + + '@radix-ui/react-use-size@1.1.0(@types/react@18.3.18)(react@18.3.1)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.3.18)(react@18.3.1) + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.18 + + '@radix-ui/react-visually-hidden@1.1.0(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.18 + '@types/react-dom': 18.3.5(@types/react@18.3.18) + + '@radix-ui/react-visually-hidden@1.1.2(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-primitive': 2.0.2(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.18 + '@types/react-dom': 18.3.5(@types/react@18.3.18) + + '@radix-ui/rect@1.1.0': {} + + '@rollup/rollup-android-arm-eabi@4.26.0': + optional: true + + '@rollup/rollup-android-arm64@4.26.0': + optional: true + + '@rollup/rollup-darwin-arm64@4.26.0': + optional: true + + '@rollup/rollup-darwin-x64@4.26.0': + optional: true + + '@rollup/rollup-freebsd-arm64@4.26.0': + optional: true + + '@rollup/rollup-freebsd-x64@4.26.0': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.26.0': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.26.0': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.26.0': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.26.0': + optional: true + + '@rollup/rollup-linux-powerpc64le-gnu@4.26.0': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.26.0': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.26.0': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.26.0': + optional: true + + '@rollup/rollup-linux-x64-musl@4.26.0': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.26.0': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.26.0': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.26.0': + optional: true + + '@snyk/github-codeowners@1.1.0': + dependencies: + commander: 4.1.1 + ignore: 5.3.2 + p-map: 4.0.0 + + '@swc/core-darwin-arm64@1.9.2': + optional: true + + '@swc/core-darwin-x64@1.9.2': + optional: true + + '@swc/core-linux-arm-gnueabihf@1.9.2': + optional: true + + '@swc/core-linux-arm64-gnu@1.9.2': + optional: true + + '@swc/core-linux-arm64-musl@1.9.2': + optional: true + + '@swc/core-linux-x64-gnu@1.9.2': + optional: true + + '@swc/core-linux-x64-musl@1.9.2': + optional: true + + '@swc/core-win32-arm64-msvc@1.9.2': + optional: true + + '@swc/core-win32-ia32-msvc@1.9.2': + optional: true + + '@swc/core-win32-x64-msvc@1.9.2': + optional: true + + '@swc/core@1.9.2': + dependencies: + '@swc/counter': 0.1.3 + '@swc/types': 0.1.15 + optionalDependencies: + '@swc/core-darwin-arm64': 1.9.2 + '@swc/core-darwin-x64': 1.9.2 + '@swc/core-linux-arm-gnueabihf': 1.9.2 + '@swc/core-linux-arm64-gnu': 1.9.2 + '@swc/core-linux-arm64-musl': 1.9.2 + '@swc/core-linux-x64-gnu': 1.9.2 + '@swc/core-linux-x64-musl': 1.9.2 + '@swc/core-win32-arm64-msvc': 1.9.2 + '@swc/core-win32-ia32-msvc': 1.9.2 + '@swc/core-win32-x64-msvc': 1.9.2 + + '@swc/counter@0.1.3': {} + + '@swc/types@0.1.15': + dependencies: + '@swc/counter': 0.1.3 + + '@tabler/icons-react@3.24.0(react@18.3.1)': + dependencies: + '@tabler/icons': 3.24.0 + react: 18.3.1 + + '@tabler/icons@3.24.0': {} + + '@tanstack/eslint-plugin-query@5.62.1(eslint@9.16.0(jiti@2.4.2))(typescript@5.7.2)': + dependencies: + '@typescript-eslint/utils': 8.17.0(eslint@9.16.0(jiti@2.4.2))(typescript@5.7.2) + eslint: 9.16.0(jiti@2.4.2) + transitivePeerDependencies: + - supports-color + - typescript + + '@tanstack/history@1.85.3': {} + + '@tanstack/query-core@5.62.3': {} + + '@tanstack/query-devtools@5.61.4': {} + + '@tanstack/react-query-devtools@5.62.3(@tanstack/react-query@5.62.3(react@18.3.1))(react@18.3.1)': + dependencies: + '@tanstack/query-devtools': 5.61.4 + '@tanstack/react-query': 5.62.3(react@18.3.1) + react: 18.3.1 + + '@tanstack/react-query@5.62.3(react@18.3.1)': + dependencies: + '@tanstack/query-core': 5.62.3 + react: 18.3.1 + + '@tanstack/react-router@1.86.1(@tanstack/router-generator@1.86.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@tanstack/history': 1.85.3 + '@tanstack/react-store': 0.6.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + jsesc: 3.0.2 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + tiny-invariant: 1.3.3 + tiny-warning: 1.0.3 + optionalDependencies: + '@tanstack/router-generator': 1.86.0 + + '@tanstack/react-store@0.6.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@tanstack/store': 0.6.0 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + use-sync-external-store: 1.2.2(react@18.3.1) + + '@tanstack/react-table@8.20.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@tanstack/table-core': 8.20.5 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + '@tanstack/react-virtual@3.11.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@tanstack/virtual-core': 3.11.2 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + '@tanstack/router-devtools@1.86.1(@tanstack/react-router@1.86.1(@tanstack/router-generator@1.86.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(csstype@3.1.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@tanstack/react-router': 1.86.1(@tanstack/router-generator@1.86.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + clsx: 2.1.1 + goober: 2.1.16(csstype@3.1.3) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + transitivePeerDependencies: + - csstype + + '@tanstack/router-generator@1.86.0': + dependencies: + '@tanstack/virtual-file-routes': 1.81.9 + prettier: 3.4.2 + tsx: 4.19.2 + zod: 3.23.8 + + '@tanstack/router-plugin@1.86.0(vite@6.0.11(@types/node@22.10.1)(jiti@2.4.2)(sass@1.89.2)(terser@5.43.1)(tsx@4.19.2)(yaml@2.6.0))(webpack@5.99.9)': + dependencies: + '@babel/core': 7.26.0 + '@babel/generator': 7.26.2 + '@babel/parser': 7.26.2 + '@babel/plugin-syntax-jsx': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-syntax-typescript': 7.25.9(@babel/core@7.26.0) + '@babel/template': 7.25.9 + '@babel/traverse': 7.25.9 + '@babel/types': 7.26.0 + '@tanstack/router-generator': 1.86.0 + '@tanstack/virtual-file-routes': 1.81.9 + '@types/babel__core': 7.20.5 + '@types/babel__generator': 7.6.8 + '@types/babel__template': 7.4.4 + '@types/babel__traverse': 7.20.6 + babel-dead-code-elimination: 1.0.6 + chokidar: 3.6.0 + unplugin: 1.16.0 + zod: 3.23.8 + optionalDependencies: + vite: 6.0.11(@types/node@22.10.1)(jiti@2.4.2)(sass@1.89.2)(terser@5.43.1)(tsx@4.19.2)(yaml@2.6.0) + webpack: 5.99.9 + transitivePeerDependencies: + - supports-color + + '@tanstack/store@0.6.0': {} + + '@tanstack/table-core@8.20.5': {} + + '@tanstack/virtual-core@3.11.2': {} + + '@tanstack/virtual-file-routes@1.81.9': {} + + '@trivago/prettier-plugin-sort-imports@4.3.0(prettier@3.4.2)': + dependencies: + '@babel/generator': 7.17.7 + '@babel/parser': 7.26.2 + '@babel/traverse': 7.23.2 + '@babel/types': 7.17.0 + javascript-natural-sort: 0.7.1 + lodash: 4.17.21 + prettier: 3.4.2 + transitivePeerDependencies: + - supports-color + + '@types/babel__core@7.20.5': + dependencies: + '@babel/parser': 7.26.2 + '@babel/types': 7.26.0 + '@types/babel__generator': 7.6.8 + '@types/babel__template': 7.4.4 + '@types/babel__traverse': 7.20.6 + + '@types/babel__generator@7.6.8': + dependencies: + '@babel/types': 7.26.0 + + '@types/babel__template@7.4.4': + dependencies: + '@babel/parser': 7.26.2 + '@babel/types': 7.26.0 + + '@types/babel__traverse@7.20.6': + dependencies: + '@babel/types': 7.26.0 + + '@types/d3-array@3.2.1': {} + + '@types/d3-color@3.1.3': {} + + '@types/d3-ease@3.0.2': {} + + '@types/d3-interpolate@3.0.4': + dependencies: + '@types/d3-color': 3.1.3 + + '@types/d3-path@3.1.0': {} + + '@types/d3-scale@4.0.8': + dependencies: + '@types/d3-time': 3.0.3 + + '@types/d3-shape@3.1.6': + dependencies: + '@types/d3-path': 3.1.0 + + '@types/d3-time@3.0.3': {} + + '@types/d3-timer@3.0.2': {} + + '@types/debug@4.1.12': + dependencies: + '@types/ms': 2.1.0 + + '@types/eslint-scope@3.7.7': + dependencies: + '@types/eslint': 9.6.1 + '@types/estree': 1.0.6 + optional: true + + '@types/eslint@9.6.1': + dependencies: + '@types/estree': 1.0.6 + '@types/json-schema': 7.0.15 + optional: true + + '@types/estree-jsx@1.0.5': + dependencies: + '@types/estree': 1.0.6 + + '@types/estree@1.0.6': {} + + '@types/file-saver@2.0.7': {} + + '@types/hast@3.0.4': + dependencies: + '@types/unist': 3.0.3 + + '@types/js-cookie@3.0.6': {} + + '@types/json-schema@7.0.15': {} + + '@types/mdast@4.0.4': + dependencies: + '@types/unist': 3.0.3 + + '@types/ms@2.1.0': {} + + '@types/node@22.10.1': + dependencies: + undici-types: 6.20.0 + + '@types/prop-types@15.7.14': {} + + '@types/react-dom@18.3.5(@types/react@18.3.18)': + dependencies: + '@types/react': 18.3.18 + + '@types/react@18.3.18': + dependencies: + '@types/prop-types': 15.7.14 + csstype: 3.1.3 + + '@types/unist@2.0.11': {} + + '@types/unist@3.0.3': {} + + '@typescript-eslint/eslint-plugin@8.17.0(@typescript-eslint/parser@8.17.0(eslint@9.16.0(jiti@2.4.2))(typescript@5.7.2))(eslint@9.16.0(jiti@2.4.2))(typescript@5.7.2)': + dependencies: + '@eslint-community/regexpp': 4.12.1 + '@typescript-eslint/parser': 8.17.0(eslint@9.16.0(jiti@2.4.2))(typescript@5.7.2) + '@typescript-eslint/scope-manager': 8.17.0 + '@typescript-eslint/type-utils': 8.17.0(eslint@9.16.0(jiti@2.4.2))(typescript@5.7.2) + '@typescript-eslint/utils': 8.17.0(eslint@9.16.0(jiti@2.4.2))(typescript@5.7.2) + '@typescript-eslint/visitor-keys': 8.17.0 + eslint: 9.16.0(jiti@2.4.2) + graphemer: 1.4.0 + ignore: 5.3.2 + natural-compare: 1.4.0 + ts-api-utils: 1.4.0(typescript@5.7.2) + optionalDependencies: + typescript: 5.7.2 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/parser@8.17.0(eslint@9.16.0(jiti@2.4.2))(typescript@5.7.2)': + dependencies: + '@typescript-eslint/scope-manager': 8.17.0 + '@typescript-eslint/types': 8.17.0 + '@typescript-eslint/typescript-estree': 8.17.0(typescript@5.7.2) + '@typescript-eslint/visitor-keys': 8.17.0 + debug: 4.3.7 + eslint: 9.16.0(jiti@2.4.2) + optionalDependencies: + typescript: 5.7.2 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/scope-manager@8.17.0': + dependencies: + '@typescript-eslint/types': 8.17.0 + '@typescript-eslint/visitor-keys': 8.17.0 + + '@typescript-eslint/type-utils@8.17.0(eslint@9.16.0(jiti@2.4.2))(typescript@5.7.2)': + dependencies: + '@typescript-eslint/typescript-estree': 8.17.0(typescript@5.7.2) + '@typescript-eslint/utils': 8.17.0(eslint@9.16.0(jiti@2.4.2))(typescript@5.7.2) + debug: 4.3.7 + eslint: 9.16.0(jiti@2.4.2) + ts-api-utils: 1.4.0(typescript@5.7.2) + optionalDependencies: + typescript: 5.7.2 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/types@8.17.0': {} + + '@typescript-eslint/typescript-estree@8.17.0(typescript@5.7.2)': + dependencies: + '@typescript-eslint/types': 8.17.0 + '@typescript-eslint/visitor-keys': 8.17.0 + debug: 4.3.7 + fast-glob: 3.3.2 + is-glob: 4.0.3 + minimatch: 9.0.5 + semver: 7.6.3 + ts-api-utils: 1.4.0(typescript@5.7.2) + optionalDependencies: + typescript: 5.7.2 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/utils@8.17.0(eslint@9.16.0(jiti@2.4.2))(typescript@5.7.2)': + dependencies: + '@eslint-community/eslint-utils': 4.4.1(eslint@9.16.0(jiti@2.4.2)) + '@typescript-eslint/scope-manager': 8.17.0 + '@typescript-eslint/types': 8.17.0 + '@typescript-eslint/typescript-estree': 8.17.0(typescript@5.7.2) + eslint: 9.16.0(jiti@2.4.2) + optionalDependencies: + typescript: 5.7.2 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/visitor-keys@8.17.0': + dependencies: + '@typescript-eslint/types': 8.17.0 + eslint-visitor-keys: 4.2.0 + + '@ungap/structured-clone@1.3.0': {} + + '@vitejs/plugin-react-swc@3.7.2(vite@6.0.11(@types/node@22.10.1)(jiti@2.4.2)(sass@1.89.2)(terser@5.43.1)(tsx@4.19.2)(yaml@2.6.0))': + dependencies: + '@swc/core': 1.9.2 + vite: 6.0.11(@types/node@22.10.1)(jiti@2.4.2)(sass@1.89.2)(terser@5.43.1)(tsx@4.19.2)(yaml@2.6.0) + transitivePeerDependencies: + - '@swc/helpers' + + '@webassemblyjs/ast@1.14.1': + dependencies: + '@webassemblyjs/helper-numbers': 1.13.2 + '@webassemblyjs/helper-wasm-bytecode': 1.13.2 + optional: true + + '@webassemblyjs/floating-point-hex-parser@1.13.2': + optional: true + + '@webassemblyjs/helper-api-error@1.13.2': + optional: true + + '@webassemblyjs/helper-buffer@1.14.1': + optional: true + + '@webassemblyjs/helper-numbers@1.13.2': + dependencies: + '@webassemblyjs/floating-point-hex-parser': 1.13.2 + '@webassemblyjs/helper-api-error': 1.13.2 + '@xtuc/long': 4.2.2 + optional: true + + '@webassemblyjs/helper-wasm-bytecode@1.13.2': + optional: true + + '@webassemblyjs/helper-wasm-section@1.14.1': + dependencies: + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/helper-buffer': 1.14.1 + '@webassemblyjs/helper-wasm-bytecode': 1.13.2 + '@webassemblyjs/wasm-gen': 1.14.1 + optional: true + + '@webassemblyjs/ieee754@1.13.2': + dependencies: + '@xtuc/ieee754': 1.2.0 + optional: true + + '@webassemblyjs/leb128@1.13.2': + dependencies: + '@xtuc/long': 4.2.2 + optional: true + + '@webassemblyjs/utf8@1.13.2': + optional: true + + '@webassemblyjs/wasm-edit@1.14.1': + dependencies: + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/helper-buffer': 1.14.1 + '@webassemblyjs/helper-wasm-bytecode': 1.13.2 + '@webassemblyjs/helper-wasm-section': 1.14.1 + '@webassemblyjs/wasm-gen': 1.14.1 + '@webassemblyjs/wasm-opt': 1.14.1 + '@webassemblyjs/wasm-parser': 1.14.1 + '@webassemblyjs/wast-printer': 1.14.1 + optional: true + + '@webassemblyjs/wasm-gen@1.14.1': + dependencies: + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/helper-wasm-bytecode': 1.13.2 + '@webassemblyjs/ieee754': 1.13.2 + '@webassemblyjs/leb128': 1.13.2 + '@webassemblyjs/utf8': 1.13.2 + optional: true + + '@webassemblyjs/wasm-opt@1.14.1': + dependencies: + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/helper-buffer': 1.14.1 + '@webassemblyjs/wasm-gen': 1.14.1 + '@webassemblyjs/wasm-parser': 1.14.1 + optional: true + + '@webassemblyjs/wasm-parser@1.14.1': + dependencies: + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/helper-api-error': 1.13.2 + '@webassemblyjs/helper-wasm-bytecode': 1.13.2 + '@webassemblyjs/ieee754': 1.13.2 + '@webassemblyjs/leb128': 1.13.2 + '@webassemblyjs/utf8': 1.13.2 + optional: true + + '@webassemblyjs/wast-printer@1.14.1': + dependencies: + '@webassemblyjs/ast': 1.14.1 + '@xtuc/long': 4.2.2 + optional: true + + '@xtuc/ieee754@1.2.0': + optional: true + + '@xtuc/long@4.2.2': + optional: true + + ace-builds@1.37.1: {} + + acorn-jsx@5.3.2(acorn@8.14.0): + dependencies: + acorn: 8.14.0 + + acorn@8.14.0: {} + + aggregate-error@3.1.0: + dependencies: + clean-stack: 2.2.0 + indent-string: 4.0.0 + + ajv-formats@2.1.1(ajv@8.17.1): + optionalDependencies: + ajv: 8.17.1 + optional: true + + ajv-keywords@5.1.0(ajv@8.17.1): + dependencies: + ajv: 8.17.1 + fast-deep-equal: 3.1.3 + optional: true + + ajv@6.12.6: + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + + ajv@8.17.1: + dependencies: + fast-deep-equal: 3.1.3 + fast-uri: 3.0.6 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + optional: true + + ansi-regex@5.0.1: {} + + ansi-regex@6.1.0: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + ansi-styles@6.2.1: {} + + any-promise@1.3.0: {} + + anymatch@3.1.3: + dependencies: + normalize-path: 3.0.0 + picomatch: 2.3.1 + + arg@5.0.2: {} + + argparse@2.0.1: {} + + aria-hidden@1.2.4: + dependencies: + tslib: 2.8.1 + + asynckit@0.4.0: {} + + autoprefixer@10.4.20(postcss@8.4.49): + dependencies: + browserslist: 4.24.2 + caniuse-lite: 1.0.30001707 + fraction.js: 4.3.7 + normalize-range: 0.1.2 + picocolors: 1.1.1 + postcss: 8.4.49 + postcss-value-parser: 4.2.0 + + axios@1.7.9: + dependencies: + follow-redirects: 1.15.9 + form-data: 4.0.1 + proxy-from-env: 1.1.0 + transitivePeerDependencies: + - debug + + babel-dead-code-elimination@1.0.6: + dependencies: + '@babel/core': 7.26.0 + '@babel/parser': 7.26.2 + '@babel/traverse': 7.25.9 + '@babel/types': 7.26.0 + transitivePeerDependencies: + - supports-color + + bail@2.0.2: {} + + balanced-match@1.0.2: {} + + binary-extensions@2.3.0: {} + + brace-expansion@1.1.11: + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + + brace-expansion@2.0.1: + dependencies: + balanced-match: 1.0.2 + + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + + browserslist@4.24.2: + dependencies: + caniuse-lite: 1.0.30001707 + electron-to-chromium: 1.5.57 + node-releases: 2.0.18 + update-browserslist-db: 1.1.1(browserslist@4.24.2) + + buffer-from@1.1.2: + optional: true + + callsites@3.1.0: {} + + camelcase-css@2.0.1: {} + + caniuse-lite@1.0.30001707: {} + + ccount@2.0.1: {} + + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + character-entities-html4@2.1.0: {} + + character-entities-legacy@3.0.0: {} + + character-entities@2.0.2: {} + + character-reference-invalid@2.0.1: {} + + chokidar@3.6.0: + dependencies: + anymatch: 3.1.3 + braces: 3.0.3 + glob-parent: 5.1.2 + is-binary-path: 2.1.0 + is-glob: 4.0.3 + normalize-path: 3.0.0 + readdirp: 3.6.0 + optionalDependencies: + fsevents: 2.3.3 + + chokidar@4.0.3: + dependencies: + readdirp: 4.1.2 + optional: true + + chrome-trace-event@1.0.4: + optional: true + + class-variance-authority@0.7.1: + dependencies: + clsx: 2.1.1 + + clean-stack@2.2.0: {} + + clone@1.0.4: + optional: true + + clsx@2.1.1: {} + + cmdk@1.0.4(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + '@radix-ui/react-dialog': 1.1.2(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-id': 1.1.0(@types/react@18.3.18)(react@18.3.1) + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + use-sync-external-store: 1.2.2(react@18.3.1) + transitivePeerDependencies: + - '@types/react' + - '@types/react-dom' + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.0.0 + + comma-separated-tokens@2.0.3: {} + + commander@2.20.3: + optional: true + + commander@4.1.1: {} + + concat-map@0.0.1: {} + + convert-source-map@2.0.0: {} + + cross-spawn@7.0.5: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + cssesc@3.0.0: {} + + csstype@3.1.3: {} + + d3-array@3.2.4: + dependencies: + internmap: 2.0.3 + + d3-color@3.1.0: {} + + d3-ease@3.0.1: {} + + d3-format@3.1.0: {} + + d3-interpolate@3.0.1: + dependencies: + d3-color: 3.1.0 + + d3-path@3.1.0: {} + + d3-scale@4.0.2: + dependencies: + d3-array: 3.2.4 + d3-format: 3.1.0 + d3-interpolate: 3.0.1 + d3-time: 3.1.0 + d3-time-format: 4.1.0 + + d3-shape@3.2.0: + dependencies: + d3-path: 3.1.0 + + d3-time-format@4.1.0: + dependencies: + d3-time: 3.1.0 + + d3-time@3.1.0: + dependencies: + d3-array: 3.2.4 + + d3-timer@3.0.1: {} + + date-fns@3.6.0: {} + + debug@4.3.7: + dependencies: + ms: 2.1.3 + + decimal.js-light@2.5.1: {} + + decode-named-character-reference@1.1.0: + dependencies: + character-entities: 2.0.2 + + deep-is@0.1.4: {} + + defaults@1.0.4: + dependencies: + clone: 1.0.4 + optional: true + + delayed-stream@1.0.0: {} + + dequal@2.0.3: {} + + detect-libc@1.0.3: + optional: true + + detect-node-es@1.1.0: {} + + devlop@1.1.0: + dependencies: + dequal: 2.0.3 + + didyoumean@1.2.2: {} + + diff-match-patch@1.0.5: {} + + dlv@1.1.3: {} + + dom-helpers@5.2.1: + dependencies: + '@babel/runtime': 7.26.0 + csstype: 3.1.3 + + eastasianwidth@0.2.0: {} + + easy-table@1.2.0: + dependencies: + ansi-regex: 5.0.1 + optionalDependencies: + wcwidth: 1.0.1 + + electron-to-chromium@1.5.57: {} + + emoji-regex@8.0.0: {} + + emoji-regex@9.2.2: {} + + enhanced-resolve@5.18.0: + dependencies: + graceful-fs: 4.2.11 + tapable: 2.2.1 + + es-module-lexer@1.7.0: + optional: true + + esbuild@0.23.1: + optionalDependencies: + '@esbuild/aix-ppc64': 0.23.1 + '@esbuild/android-arm': 0.23.1 + '@esbuild/android-arm64': 0.23.1 + '@esbuild/android-x64': 0.23.1 + '@esbuild/darwin-arm64': 0.23.1 + '@esbuild/darwin-x64': 0.23.1 + '@esbuild/freebsd-arm64': 0.23.1 + '@esbuild/freebsd-x64': 0.23.1 + '@esbuild/linux-arm': 0.23.1 + '@esbuild/linux-arm64': 0.23.1 + '@esbuild/linux-ia32': 0.23.1 + '@esbuild/linux-loong64': 0.23.1 + '@esbuild/linux-mips64el': 0.23.1 + '@esbuild/linux-ppc64': 0.23.1 + '@esbuild/linux-riscv64': 0.23.1 + '@esbuild/linux-s390x': 0.23.1 + '@esbuild/linux-x64': 0.23.1 + '@esbuild/netbsd-x64': 0.23.1 + '@esbuild/openbsd-arm64': 0.23.1 + '@esbuild/openbsd-x64': 0.23.1 + '@esbuild/sunos-x64': 0.23.1 + '@esbuild/win32-arm64': 0.23.1 + '@esbuild/win32-ia32': 0.23.1 + '@esbuild/win32-x64': 0.23.1 + + esbuild@0.24.2: + optionalDependencies: + '@esbuild/aix-ppc64': 0.24.2 + '@esbuild/android-arm': 0.24.2 + '@esbuild/android-arm64': 0.24.2 + '@esbuild/android-x64': 0.24.2 + '@esbuild/darwin-arm64': 0.24.2 + '@esbuild/darwin-x64': 0.24.2 + '@esbuild/freebsd-arm64': 0.24.2 + '@esbuild/freebsd-x64': 0.24.2 + '@esbuild/linux-arm': 0.24.2 + '@esbuild/linux-arm64': 0.24.2 + '@esbuild/linux-ia32': 0.24.2 + '@esbuild/linux-loong64': 0.24.2 + '@esbuild/linux-mips64el': 0.24.2 + '@esbuild/linux-ppc64': 0.24.2 + '@esbuild/linux-riscv64': 0.24.2 + '@esbuild/linux-s390x': 0.24.2 + '@esbuild/linux-x64': 0.24.2 + '@esbuild/netbsd-arm64': 0.24.2 + '@esbuild/netbsd-x64': 0.24.2 + '@esbuild/openbsd-arm64': 0.24.2 + '@esbuild/openbsd-x64': 0.24.2 + '@esbuild/sunos-x64': 0.24.2 + '@esbuild/win32-arm64': 0.24.2 + '@esbuild/win32-ia32': 0.24.2 + '@esbuild/win32-x64': 0.24.2 + + escalade@3.2.0: {} + + escape-string-regexp@4.0.0: {} + + escape-string-regexp@5.0.0: {} + + eslint-plugin-react-hooks@5.1.0(eslint@9.16.0(jiti@2.4.2)): + dependencies: + eslint: 9.16.0(jiti@2.4.2) + + eslint-plugin-react-refresh@0.4.16(eslint@9.16.0(jiti@2.4.2)): + dependencies: + eslint: 9.16.0(jiti@2.4.2) + + eslint-scope@5.1.1: + dependencies: + esrecurse: 4.3.0 + estraverse: 4.3.0 + optional: true + + eslint-scope@8.2.0: + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-visitor-keys@3.4.3: {} + + eslint-visitor-keys@4.2.0: {} + + eslint@9.16.0(jiti@2.4.2): + dependencies: + '@eslint-community/eslint-utils': 4.4.1(eslint@9.16.0(jiti@2.4.2)) + '@eslint-community/regexpp': 4.12.1 + '@eslint/config-array': 0.19.1 + '@eslint/core': 0.9.1 + '@eslint/eslintrc': 3.2.0 + '@eslint/js': 9.16.0 + '@eslint/plugin-kit': 0.2.4 + '@humanfs/node': 0.16.6 + '@humanwhocodes/module-importer': 1.0.1 + '@humanwhocodes/retry': 0.4.1 + '@types/estree': 1.0.6 + '@types/json-schema': 7.0.15 + ajv: 6.12.6 + chalk: 4.1.2 + cross-spawn: 7.0.5 + debug: 4.3.7 + escape-string-regexp: 4.0.0 + eslint-scope: 8.2.0 + eslint-visitor-keys: 4.2.0 + espree: 10.3.0 + esquery: 1.6.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 8.0.0 + find-up: 5.0.0 + glob-parent: 6.0.2 + ignore: 5.3.2 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + json-stable-stringify-without-jsonify: 1.0.1 + lodash.merge: 4.6.2 + minimatch: 3.1.2 + natural-compare: 1.4.0 + optionator: 0.9.4 + optionalDependencies: + jiti: 2.4.2 + transitivePeerDependencies: + - supports-color + + espree@10.3.0: + dependencies: + acorn: 8.14.0 + acorn-jsx: 5.3.2(acorn@8.14.0) + eslint-visitor-keys: 4.2.0 + + esquery@1.6.0: + dependencies: + estraverse: 5.3.0 + + esrecurse@4.3.0: + dependencies: + estraverse: 5.3.0 + + estraverse@4.3.0: + optional: true + + estraverse@5.3.0: {} + + estree-util-is-identifier-name@3.0.0: {} + + esutils@2.0.3: {} + + eventemitter3@4.0.7: {} + + events@3.3.0: + optional: true + + extend@3.0.2: {} + + fast-deep-equal@3.1.3: {} + + fast-equals@5.0.1: {} + + fast-glob@3.3.2: + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.8 + + fast-json-stable-stringify@2.1.0: {} + + fast-levenshtein@2.0.6: {} + + fast-uri@3.0.6: + optional: true + + fastq@1.17.1: + dependencies: + reusify: 1.0.4 + + file-entry-cache@8.0.0: + dependencies: + flat-cache: 4.0.1 + + file-saver@2.0.5: {} + + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + + find-up@5.0.0: + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + + flat-cache@4.0.1: + dependencies: + flatted: 3.3.1 + keyv: 4.5.4 + + flatted@3.3.1: {} + + follow-redirects@1.15.9: {} + + foreground-child@3.3.0: + dependencies: + cross-spawn: 7.0.5 + signal-exit: 4.1.0 + + form-data@4.0.1: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + mime-types: 2.1.35 + + fraction.js@4.3.7: {} + + fsevents@2.3.3: + optional: true + + function-bind@1.1.2: {} + + gensync@1.0.0-beta.2: {} + + get-nonce@1.0.1: {} + + get-tsconfig@4.8.1: + dependencies: + resolve-pkg-maps: 1.0.0 + + glob-parent@5.1.2: + dependencies: + is-glob: 4.0.3 + + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + + glob-to-regexp@0.4.1: + optional: true + + glob@10.4.5: + dependencies: + foreground-child: 3.3.0 + jackspeak: 3.4.3 + minimatch: 9.0.5 + minipass: 7.1.2 + package-json-from-dist: 1.0.1 + path-scurry: 1.11.1 + + globals@11.12.0: {} + + globals@14.0.0: {} + + globals@15.13.0: {} + + goober@2.1.16(csstype@3.1.3): + dependencies: + csstype: 3.1.3 + + graceful-fs@4.2.11: {} + + graphemer@1.4.0: {} + + handlebars@4.7.8: + dependencies: + minimist: 1.2.8 + neo-async: 2.6.2 + source-map: 0.6.1 + wordwrap: 1.0.0 + optionalDependencies: + uglify-js: 3.19.3 + + has-flag@4.0.0: {} + + hasown@2.0.2: + dependencies: + function-bind: 1.1.2 + + hast-util-to-jsx-runtime@2.3.6: + dependencies: + '@types/estree': 1.0.6 + '@types/hast': 3.0.4 + '@types/unist': 3.0.3 + comma-separated-tokens: 2.0.3 + devlop: 1.1.0 + estree-util-is-identifier-name: 3.0.0 + hast-util-whitespace: 3.0.0 + mdast-util-mdx-expression: 2.0.1 + mdast-util-mdx-jsx: 3.2.0 + mdast-util-mdxjs-esm: 2.0.1 + property-information: 7.0.0 + space-separated-tokens: 2.0.2 + style-to-js: 1.1.16 + unist-util-position: 5.0.0 + vfile-message: 4.0.2 + transitivePeerDependencies: + - supports-color + + hast-util-whitespace@3.0.0: + dependencies: + '@types/hast': 3.0.4 + + html-url-attributes@3.0.1: {} + + ignore@5.3.2: {} + + immutable@5.1.3: + optional: true + + import-fresh@3.3.0: + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + + imurmurhash@0.1.4: {} + + indent-string@4.0.0: {} + + inline-style-parser@0.2.4: {} + + internmap@2.0.3: {} + + invariant@2.2.4: + dependencies: + loose-envify: 1.4.0 + + is-alphabetical@2.0.1: {} + + is-alphanumerical@2.0.1: + dependencies: + is-alphabetical: 2.0.1 + is-decimal: 2.0.1 + + is-binary-path@2.1.0: + dependencies: + binary-extensions: 2.3.0 + + is-core-module@2.15.1: + dependencies: + hasown: 2.0.2 + + is-decimal@2.0.1: {} + + is-extglob@2.1.1: {} + + is-fullwidth-code-point@3.0.0: {} + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-hexadecimal@2.0.1: {} + + is-number@7.0.0: {} + + is-plain-obj@4.1.0: {} + + isexe@2.0.0: {} + + jackspeak@3.4.3: + dependencies: + '@isaacs/cliui': 8.0.2 + optionalDependencies: + '@pkgjs/parseargs': 0.11.0 + + javascript-natural-sort@0.7.1: {} + + jest-worker@27.5.1: + dependencies: + '@types/node': 22.10.1 + merge-stream: 2.0.0 + supports-color: 8.1.1 + optional: true + + jiti@1.21.6: {} + + jiti@2.4.2: {} + + js-cookie@3.0.5: {} + + js-tokens@4.0.0: {} + + js-yaml@4.1.0: + dependencies: + argparse: 2.0.1 + + jsesc@2.5.2: {} + + jsesc@3.0.2: {} + + json-buffer@3.0.1: {} + + json-parse-even-better-errors@2.3.1: + optional: true + + json-schema-traverse@0.4.1: {} + + json-schema-traverse@1.0.0: + optional: true + + json-stable-stringify-without-jsonify@1.0.1: {} + + json5@2.2.3: {} + + keyv@4.5.4: + dependencies: + json-buffer: 3.0.1 + + knip@5.41.1(@types/node@22.10.1)(typescript@5.7.2): + dependencies: + '@nodelib/fs.walk': 1.2.8 + '@snyk/github-codeowners': 1.1.0 + '@types/node': 22.10.1 + easy-table: 1.2.0 + enhanced-resolve: 5.18.0 + fast-glob: 3.3.2 + jiti: 2.4.2 + js-yaml: 4.1.0 + minimist: 1.2.8 + picocolors: 1.1.1 + picomatch: 4.0.2 + pretty-ms: 9.2.0 + smol-toml: 1.3.1 + strip-json-comments: 5.0.1 + summary: 2.1.0 + typescript: 5.7.2 + zod: 3.23.8 + zod-validation-error: 3.4.0(zod@3.23.8) + + levn@0.4.1: + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + + lilconfig@3.1.3: {} + + lines-and-columns@1.2.4: {} + + loader-runner@4.3.0: + optional: true + + locate-path@6.0.0: + dependencies: + p-locate: 5.0.0 + + lodash.get@4.4.2: {} + + lodash.isequal@4.5.0: {} + + lodash.merge@4.6.2: {} + + lodash@4.17.21: {} + + longest-streak@3.1.0: {} + + loose-envify@1.4.0: + dependencies: + js-tokens: 4.0.0 + + lru-cache@10.4.3: {} + + lru-cache@5.1.1: + dependencies: + yallist: 3.1.1 + + lucide-react@0.468.0(react@18.3.1): + dependencies: + react: 18.3.1 + + markdown-table@3.0.4: {} + + mdast-util-find-and-replace@3.0.2: + dependencies: + '@types/mdast': 4.0.4 + escape-string-regexp: 5.0.0 + unist-util-is: 6.0.0 + unist-util-visit-parents: 6.0.1 + + mdast-util-from-markdown@2.0.2: + dependencies: + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + decode-named-character-reference: 1.1.0 + devlop: 1.1.0 + mdast-util-to-string: 4.0.0 + micromark: 4.0.2 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-decode-string: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + unist-util-stringify-position: 4.0.0 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-autolink-literal@2.0.1: + dependencies: + '@types/mdast': 4.0.4 + ccount: 2.0.1 + devlop: 1.1.0 + mdast-util-find-and-replace: 3.0.2 + micromark-util-character: 2.1.1 + + mdast-util-gfm-footnote@2.1.0: + dependencies: + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.2 + mdast-util-to-markdown: 2.1.2 + micromark-util-normalize-identifier: 2.0.1 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-strikethrough@2.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-from-markdown: 2.0.2 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-table@2.0.0: + dependencies: + '@types/mdast': 4.0.4 + devlop: 1.1.0 + markdown-table: 3.0.4 + mdast-util-from-markdown: 2.0.2 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-task-list-item@2.0.0: + dependencies: + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.2 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm@3.1.0: + dependencies: + mdast-util-from-markdown: 2.0.2 + mdast-util-gfm-autolink-literal: 2.0.1 + mdast-util-gfm-footnote: 2.1.0 + mdast-util-gfm-strikethrough: 2.0.0 + mdast-util-gfm-table: 2.0.0 + mdast-util-gfm-task-list-item: 2.0.0 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-mdx-expression@2.0.1: + dependencies: + '@types/estree-jsx': 1.0.5 + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.2 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-mdx-jsx@3.2.0: + dependencies: + '@types/estree-jsx': 1.0.5 + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + ccount: 2.0.1 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.2 + mdast-util-to-markdown: 2.1.2 + parse-entities: 4.0.2 + stringify-entities: 4.0.4 + unist-util-stringify-position: 4.0.0 + vfile-message: 4.0.2 + transitivePeerDependencies: + - supports-color + + mdast-util-mdxjs-esm@2.0.1: + dependencies: + '@types/estree-jsx': 1.0.5 + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.2 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-phrasing@4.1.0: + dependencies: + '@types/mdast': 4.0.4 + unist-util-is: 6.0.0 + + mdast-util-to-hast@13.2.0: + dependencies: + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + '@ungap/structured-clone': 1.3.0 + devlop: 1.1.0 + micromark-util-sanitize-uri: 2.0.1 + trim-lines: 3.0.1 + unist-util-position: 5.0.0 + unist-util-visit: 5.0.0 + vfile: 6.0.3 + + mdast-util-to-markdown@2.1.2: + dependencies: + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + longest-streak: 3.1.0 + mdast-util-phrasing: 4.1.0 + mdast-util-to-string: 4.0.0 + micromark-util-classify-character: 2.0.1 + micromark-util-decode-string: 2.0.1 + unist-util-visit: 5.0.0 + zwitch: 2.0.4 + + mdast-util-to-string@4.0.0: + dependencies: + '@types/mdast': 4.0.4 + + merge-stream@2.0.0: + optional: true + + merge2@1.4.1: {} + + micromark-core-commonmark@2.0.3: + dependencies: + decode-named-character-reference: 1.1.0 + devlop: 1.1.0 + micromark-factory-destination: 2.0.1 + micromark-factory-label: 2.0.1 + micromark-factory-space: 2.0.1 + micromark-factory-title: 2.0.1 + micromark-factory-whitespace: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-chunked: 2.0.1 + micromark-util-classify-character: 2.0.1 + micromark-util-html-tag-name: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-resolve-all: 2.0.1 + micromark-util-subtokenize: 2.1.0 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-autolink-literal@2.1.0: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-sanitize-uri: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-footnote@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-core-commonmark: 2.0.3 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-sanitize-uri: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-strikethrough@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-util-chunked: 2.0.1 + micromark-util-classify-character: 2.0.1 + micromark-util-resolve-all: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-table@2.1.1: + dependencies: + devlop: 1.1.0 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-tagfilter@2.0.0: + dependencies: + micromark-util-types: 2.0.2 + + micromark-extension-gfm-task-list-item@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm@3.0.0: + dependencies: + micromark-extension-gfm-autolink-literal: 2.1.0 + micromark-extension-gfm-footnote: 2.1.0 + micromark-extension-gfm-strikethrough: 2.1.0 + micromark-extension-gfm-table: 2.1.1 + micromark-extension-gfm-tagfilter: 2.0.0 + micromark-extension-gfm-task-list-item: 2.1.0 + micromark-util-combine-extensions: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-destination@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-label@2.0.1: + dependencies: + devlop: 1.1.0 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-space@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-types: 2.0.2 + + micromark-factory-title@2.0.1: + dependencies: + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-whitespace@2.0.1: + dependencies: + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-character@2.1.1: + dependencies: + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-chunked@2.0.1: + dependencies: + micromark-util-symbol: 2.0.1 + + micromark-util-classify-character@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-combine-extensions@2.0.1: + dependencies: + micromark-util-chunked: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-decode-numeric-character-reference@2.0.2: + dependencies: + micromark-util-symbol: 2.0.1 + + micromark-util-decode-string@2.0.1: + dependencies: + decode-named-character-reference: 1.1.0 + micromark-util-character: 2.1.1 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-symbol: 2.0.1 + + micromark-util-encode@2.0.1: {} + + micromark-util-html-tag-name@2.0.1: {} + + micromark-util-normalize-identifier@2.0.1: + dependencies: + micromark-util-symbol: 2.0.1 + + micromark-util-resolve-all@2.0.1: + dependencies: + micromark-util-types: 2.0.2 + + micromark-util-sanitize-uri@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-encode: 2.0.1 + micromark-util-symbol: 2.0.1 + + micromark-util-subtokenize@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-util-chunked: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-symbol@2.0.1: {} + + micromark-util-types@2.0.2: {} + + micromark@4.0.2: + dependencies: + '@types/debug': 4.1.12 + debug: 4.3.7 + decode-named-character-reference: 1.1.0 + devlop: 1.1.0 + micromark-core-commonmark: 2.0.3 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-chunked: 2.0.1 + micromark-util-combine-extensions: 2.0.1 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-encode: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-resolve-all: 2.0.1 + micromark-util-sanitize-uri: 2.0.1 + micromark-util-subtokenize: 2.1.0 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + transitivePeerDependencies: + - supports-color + + micromatch@4.0.8: + dependencies: + braces: 3.0.3 + picomatch: 2.3.1 + + mime-db@1.52.0: {} + + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + + minimatch@3.1.2: + dependencies: + brace-expansion: 1.1.11 + + minimatch@9.0.5: + dependencies: + brace-expansion: 2.0.1 + + minimist@1.2.8: {} + + minipass@7.1.2: {} + + ms@2.1.3: {} + + mz@2.7.0: + dependencies: + any-promise: 1.3.0 + object-assign: 4.1.1 + thenify-all: 1.6.0 + + nanoid@3.3.8: {} + + natural-compare@1.4.0: {} + + neo-async@2.6.2: {} + + node-addon-api@7.1.1: + optional: true + + node-releases@2.0.18: {} + + normalize-path@3.0.0: {} + + normalize-range@0.1.2: {} + + object-assign@4.1.1: {} + + object-hash@3.0.0: {} + + optionator@0.9.4: + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + word-wrap: 1.2.5 + + p-limit@3.1.0: + dependencies: + yocto-queue: 0.1.0 + + p-locate@5.0.0: + dependencies: + p-limit: 3.1.0 + + p-map@4.0.0: + dependencies: + aggregate-error: 3.1.0 + + package-json-from-dist@1.0.1: {} + + parent-module@1.0.1: + dependencies: + callsites: 3.1.0 + + parse-entities@4.0.2: + dependencies: + '@types/unist': 2.0.11 + character-entities-legacy: 3.0.0 + character-reference-invalid: 2.0.1 + decode-named-character-reference: 1.1.0 + is-alphanumerical: 2.0.1 + is-decimal: 2.0.1 + is-hexadecimal: 2.0.1 + + parse-ms@4.0.0: {} + + path-exists@4.0.0: {} + + path-key@3.1.1: {} + + path-parse@1.0.7: {} + + path-scurry@1.11.1: + dependencies: + lru-cache: 10.4.3 + minipass: 7.1.2 + + picocolors@1.1.1: {} + + picomatch@2.3.1: {} + + picomatch@4.0.2: {} + + pify@2.3.0: {} + + pirates@4.0.6: {} + + postcss-import@15.1.0(postcss@8.4.49): + dependencies: + postcss: 8.4.49 + postcss-value-parser: 4.2.0 + read-cache: 1.0.0 + resolve: 1.22.8 + + postcss-js@4.0.1(postcss@8.4.49): + dependencies: + camelcase-css: 2.0.1 + postcss: 8.4.49 + + postcss-load-config@4.0.2(postcss@8.4.49): + dependencies: + lilconfig: 3.1.3 + yaml: 2.6.0 + optionalDependencies: + postcss: 8.4.49 + + postcss-nested@6.2.0(postcss@8.4.49): + dependencies: + postcss: 8.4.49 + postcss-selector-parser: 6.1.2 + + postcss-selector-parser@6.1.2: + dependencies: + cssesc: 3.0.0 + util-deprecate: 1.0.2 + + postcss-value-parser@4.2.0: {} + + postcss@8.4.49: + dependencies: + nanoid: 3.3.8 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + prelude-ls@1.2.1: {} + + prettier-plugin-tailwindcss@0.6.9(@trivago/prettier-plugin-sort-imports@4.3.0(prettier@3.4.2))(prettier@3.4.2): + dependencies: + prettier: 3.4.2 + optionalDependencies: + '@trivago/prettier-plugin-sort-imports': 4.3.0(prettier@3.4.2) + + prettier@3.4.2: {} + + pretty-ms@9.2.0: + dependencies: + parse-ms: 4.0.0 + + prop-types@15.8.1: + dependencies: + loose-envify: 1.4.0 + object-assign: 4.1.1 + react-is: 16.13.1 + + property-information@7.0.0: {} + + proxy-from-env@1.1.0: {} + + punycode@2.3.1: {} + + queue-microtask@1.2.3: {} + + randombytes@2.1.0: + dependencies: + safe-buffer: 5.2.1 + optional: true + + react-ace@13.0.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + ace-builds: 1.37.1 + diff-match-patch: 1.0.5 + lodash.get: 4.4.2 + lodash.isequal: 4.5.0 + prop-types: 15.8.1 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + react-day-picker@8.10.1(date-fns@3.6.0)(react@18.3.1): + dependencies: + date-fns: 3.6.0 + react: 18.3.1 + + react-dom@18.3.1(react@18.3.1): + dependencies: + loose-envify: 1.4.0 + react: 18.3.1 + scheduler: 0.23.2 + + react-hook-form@7.54.0(react@18.3.1): + dependencies: + react: 18.3.1 + + react-is@16.13.1: {} + + react-is@18.3.1: {} + + react-markdown@10.1.0(@types/react@18.3.18)(react@18.3.1): + dependencies: + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + '@types/react': 18.3.18 + devlop: 1.1.0 + hast-util-to-jsx-runtime: 2.3.6 + html-url-attributes: 3.0.1 + mdast-util-to-hast: 13.2.0 + react: 18.3.1 + remark-parse: 11.0.0 + remark-rehype: 11.1.2 + unified: 11.0.5 + unist-util-visit: 5.0.0 + vfile: 6.0.3 + transitivePeerDependencies: + - supports-color + + react-remove-scroll-bar@2.3.6(@types/react@18.3.18)(react@18.3.1): + dependencies: + react: 18.3.1 + react-style-singleton: 2.2.1(@types/react@18.3.18)(react@18.3.1) + tslib: 2.8.1 + optionalDependencies: + '@types/react': 18.3.18 + + react-remove-scroll@2.6.0(@types/react@18.3.18)(react@18.3.1): + dependencies: + react: 18.3.1 + react-remove-scroll-bar: 2.3.6(@types/react@18.3.18)(react@18.3.1) + react-style-singleton: 2.2.1(@types/react@18.3.18)(react@18.3.1) + tslib: 2.8.1 + use-callback-ref: 1.3.2(@types/react@18.3.18)(react@18.3.1) + use-sidecar: 1.1.2(@types/react@18.3.18)(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.18 + + react-resizable-panels@2.1.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + react-smooth@4.0.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + fast-equals: 5.0.1 + prop-types: 15.8.1 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-transition-group: 4.4.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + + react-style-singleton@2.2.1(@types/react@18.3.18)(react@18.3.1): + dependencies: + get-nonce: 1.0.1 + invariant: 2.2.4 + react: 18.3.1 + tslib: 2.8.1 + optionalDependencies: + '@types/react': 18.3.18 + + react-transition-group@4.4.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + '@babel/runtime': 7.26.0 + dom-helpers: 5.2.1 + loose-envify: 1.4.0 + prop-types: 15.8.1 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + react@18.3.1: + dependencies: + loose-envify: 1.4.0 + + read-cache@1.0.0: + dependencies: + pify: 2.3.0 + + readdirp@3.6.0: + dependencies: + picomatch: 2.3.1 + + readdirp@4.1.2: + optional: true + + recharts-scale@0.4.5: + dependencies: + decimal.js-light: 2.5.1 + + recharts@2.14.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + clsx: 2.1.1 + eventemitter3: 4.0.7 + lodash: 4.17.21 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-is: 18.3.1 + react-smooth: 4.0.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + recharts-scale: 0.4.5 + tiny-invariant: 1.3.3 + victory-vendor: 36.9.2 + + regenerator-runtime@0.14.1: {} + + remark-gfm@4.0.1: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-gfm: 3.1.0 + micromark-extension-gfm: 3.0.0 + remark-parse: 11.0.0 + remark-stringify: 11.0.0 + unified: 11.0.5 + transitivePeerDependencies: + - supports-color + + remark-parse@11.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-from-markdown: 2.0.2 + micromark-util-types: 2.0.2 + unified: 11.0.5 + transitivePeerDependencies: + - supports-color + + remark-rehype@11.1.2: + dependencies: + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + mdast-util-to-hast: 13.2.0 + unified: 11.0.5 + vfile: 6.0.3 + + remark-stringify@11.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-to-markdown: 2.1.2 + unified: 11.0.5 + + require-from-string@2.0.2: + optional: true + + resolve-from@4.0.0: {} + + resolve-pkg-maps@1.0.0: {} + + resolve@1.22.8: + dependencies: + is-core-module: 2.15.1 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + + reusify@1.0.4: {} + + rollup@4.26.0: + dependencies: + '@types/estree': 1.0.6 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.26.0 + '@rollup/rollup-android-arm64': 4.26.0 + '@rollup/rollup-darwin-arm64': 4.26.0 + '@rollup/rollup-darwin-x64': 4.26.0 + '@rollup/rollup-freebsd-arm64': 4.26.0 + '@rollup/rollup-freebsd-x64': 4.26.0 + '@rollup/rollup-linux-arm-gnueabihf': 4.26.0 + '@rollup/rollup-linux-arm-musleabihf': 4.26.0 + '@rollup/rollup-linux-arm64-gnu': 4.26.0 + '@rollup/rollup-linux-arm64-musl': 4.26.0 + '@rollup/rollup-linux-powerpc64le-gnu': 4.26.0 + '@rollup/rollup-linux-riscv64-gnu': 4.26.0 + '@rollup/rollup-linux-s390x-gnu': 4.26.0 + '@rollup/rollup-linux-x64-gnu': 4.26.0 + '@rollup/rollup-linux-x64-musl': 4.26.0 + '@rollup/rollup-win32-arm64-msvc': 4.26.0 + '@rollup/rollup-win32-ia32-msvc': 4.26.0 + '@rollup/rollup-win32-x64-msvc': 4.26.0 + fsevents: 2.3.3 + + run-parallel@1.2.0: + dependencies: + queue-microtask: 1.2.3 + + safe-buffer@5.2.1: + optional: true + + sass@1.89.2: + dependencies: + chokidar: 4.0.3 + immutable: 5.1.3 + source-map-js: 1.2.1 + optionalDependencies: + '@parcel/watcher': 2.5.1 + optional: true + + scheduler@0.23.2: + dependencies: + loose-envify: 1.4.0 + + schema-utils@4.3.2: + dependencies: + '@types/json-schema': 7.0.15 + ajv: 8.17.1 + ajv-formats: 2.1.1(ajv@8.17.1) + ajv-keywords: 5.1.0(ajv@8.17.1) + optional: true + + semver@6.3.1: {} + + semver@7.6.3: {} + + serialize-javascript@6.0.2: + dependencies: + randombytes: 2.1.0 + optional: true + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + signal-exit@4.1.0: {} + + smol-toml@1.3.1: {} + + source-map-js@1.2.1: {} + + source-map-support@0.5.21: + dependencies: + buffer-from: 1.1.2 + source-map: 0.6.1 + optional: true + + source-map@0.5.7: {} + + source-map@0.6.1: {} + + space-separated-tokens@2.0.2: {} + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + string-width@5.1.2: + dependencies: + eastasianwidth: 0.2.0 + emoji-regex: 9.2.2 + strip-ansi: 7.1.0 + + stringify-entities@4.0.4: + dependencies: + character-entities-html4: 2.1.0 + character-entities-legacy: 3.0.0 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-ansi@7.1.0: + dependencies: + ansi-regex: 6.1.0 + + strip-json-comments@3.1.1: {} + + strip-json-comments@5.0.1: {} + + style-to-js@1.1.16: + dependencies: + style-to-object: 1.0.8 + + style-to-object@1.0.8: + dependencies: + inline-style-parser: 0.2.4 + + sucrase@3.35.0: + dependencies: + '@jridgewell/gen-mapping': 0.3.5 + commander: 4.1.1 + glob: 10.4.5 + lines-and-columns: 1.2.4 + mz: 2.7.0 + pirates: 4.0.6 + ts-interface-checker: 0.1.13 + + summary@2.1.0: {} + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + supports-color@8.1.1: + dependencies: + has-flag: 4.0.0 + optional: true + + supports-preserve-symlinks-flag@1.0.0: {} + + tailwind-merge@2.5.5: {} + + tailwindcss-animate@1.0.7(tailwindcss@3.4.16): + dependencies: + tailwindcss: 3.4.16 + + tailwindcss@3.4.16: + dependencies: + '@alloc/quick-lru': 5.2.0 + arg: 5.0.2 + chokidar: 3.6.0 + didyoumean: 1.2.2 + dlv: 1.1.3 + fast-glob: 3.3.2 + glob-parent: 6.0.2 + is-glob: 4.0.3 + jiti: 1.21.6 + lilconfig: 3.1.3 + micromatch: 4.0.8 + normalize-path: 3.0.0 + object-hash: 3.0.0 + picocolors: 1.1.1 + postcss: 8.4.49 + postcss-import: 15.1.0(postcss@8.4.49) + postcss-js: 4.0.1(postcss@8.4.49) + postcss-load-config: 4.0.2(postcss@8.4.49) + postcss-nested: 6.2.0(postcss@8.4.49) + postcss-selector-parser: 6.1.2 + resolve: 1.22.8 + sucrase: 3.35.0 + transitivePeerDependencies: + - ts-node + + tapable@2.2.1: {} + + terser-webpack-plugin@5.3.14(webpack@5.99.9): + dependencies: + '@jridgewell/trace-mapping': 0.3.25 + jest-worker: 27.5.1 + schema-utils: 4.3.2 + serialize-javascript: 6.0.2 + terser: 5.43.1 + webpack: 5.99.9 + optional: true + + terser@5.43.1: + dependencies: + '@jridgewell/source-map': 0.3.10 + acorn: 8.14.0 + commander: 2.20.3 + source-map-support: 0.5.21 + optional: true + + thenify-all@1.6.0: + dependencies: + thenify: 3.3.1 + + thenify@3.3.1: + dependencies: + any-promise: 1.3.0 + + tiny-invariant@1.3.3: {} + + tiny-warning@1.0.3: {} + + to-fast-properties@2.0.0: {} + + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + + trim-lines@3.0.1: {} + + trough@2.2.0: {} + + ts-api-utils@1.4.0(typescript@5.7.2): + dependencies: + typescript: 5.7.2 + + ts-interface-checker@0.1.13: {} + + tslib@2.8.1: {} + + tsx@4.19.2: + dependencies: + esbuild: 0.23.1 + get-tsconfig: 4.8.1 + optionalDependencies: + fsevents: 2.3.3 + + type-check@0.4.0: + dependencies: + prelude-ls: 1.2.1 + + typescript-eslint@8.17.0(eslint@9.16.0(jiti@2.4.2))(typescript@5.7.2): + dependencies: + '@typescript-eslint/eslint-plugin': 8.17.0(@typescript-eslint/parser@8.17.0(eslint@9.16.0(jiti@2.4.2))(typescript@5.7.2))(eslint@9.16.0(jiti@2.4.2))(typescript@5.7.2) + '@typescript-eslint/parser': 8.17.0(eslint@9.16.0(jiti@2.4.2))(typescript@5.7.2) + '@typescript-eslint/utils': 8.17.0(eslint@9.16.0(jiti@2.4.2))(typescript@5.7.2) + eslint: 9.16.0(jiti@2.4.2) + optionalDependencies: + typescript: 5.7.2 + transitivePeerDependencies: + - supports-color + + typescript@5.7.2: {} + + uglify-js@3.19.3: + optional: true + + undici-types@6.20.0: {} + + unified@11.0.5: + dependencies: + '@types/unist': 3.0.3 + bail: 2.0.2 + devlop: 1.1.0 + extend: 3.0.2 + is-plain-obj: 4.1.0 + trough: 2.2.0 + vfile: 6.0.3 + + unist-util-is@6.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-position@5.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-stringify-position@4.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-visit-parents@6.0.1: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.0 + + unist-util-visit@5.0.0: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.0 + unist-util-visit-parents: 6.0.1 + + unplugin@1.16.0: + dependencies: + acorn: 8.14.0 + webpack-virtual-modules: 0.6.2 + + update-browserslist-db@1.1.1(browserslist@4.24.2): + dependencies: + browserslist: 4.24.2 + escalade: 3.2.0 + picocolors: 1.1.1 + + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 + + use-callback-ref@1.3.2(@types/react@18.3.18)(react@18.3.1): + dependencies: + react: 18.3.1 + tslib: 2.8.1 + optionalDependencies: + '@types/react': 18.3.18 + + use-sidecar@1.1.2(@types/react@18.3.18)(react@18.3.1): + dependencies: + detect-node-es: 1.1.0 + react: 18.3.1 + tslib: 2.8.1 + optionalDependencies: + '@types/react': 18.3.18 + + use-sync-external-store@1.2.2(react@18.3.1): + dependencies: + react: 18.3.1 + + util-deprecate@1.0.2: {} + + vaul@1.1.2(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + '@radix-ui/react-dialog': 1.1.2(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + transitivePeerDependencies: + - '@types/react' + - '@types/react-dom' + + vfile-message@4.0.2: + dependencies: + '@types/unist': 3.0.3 + unist-util-stringify-position: 4.0.0 + + vfile@6.0.3: + dependencies: + '@types/unist': 3.0.3 + vfile-message: 4.0.2 + + victory-vendor@36.9.2: + dependencies: + '@types/d3-array': 3.2.1 + '@types/d3-ease': 3.0.2 + '@types/d3-interpolate': 3.0.4 + '@types/d3-scale': 4.0.8 + '@types/d3-shape': 3.1.6 + '@types/d3-time': 3.0.3 + '@types/d3-timer': 3.0.2 + d3-array: 3.2.4 + d3-ease: 3.0.1 + d3-interpolate: 3.0.1 + d3-scale: 4.0.2 + d3-shape: 3.2.0 + d3-time: 3.1.0 + d3-timer: 3.0.1 + + vite@6.0.11(@types/node@22.10.1)(jiti@2.4.2)(sass@1.89.2)(terser@5.43.1)(tsx@4.19.2)(yaml@2.6.0): + dependencies: + esbuild: 0.24.2 + postcss: 8.4.49 + rollup: 4.26.0 + optionalDependencies: + '@types/node': 22.10.1 + fsevents: 2.3.3 + jiti: 2.4.2 + sass: 1.89.2 + terser: 5.43.1 + tsx: 4.19.2 + yaml: 2.6.0 + + watchpack@2.4.4: + dependencies: + glob-to-regexp: 0.4.1 + graceful-fs: 4.2.11 + optional: true + + wcwidth@1.0.1: + dependencies: + defaults: 1.0.4 + optional: true + + webpack-sources@3.3.3: + optional: true + + webpack-virtual-modules@0.6.2: {} + + webpack@5.99.9: + dependencies: + '@types/eslint-scope': 3.7.7 + '@types/estree': 1.0.6 + '@types/json-schema': 7.0.15 + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/wasm-edit': 1.14.1 + '@webassemblyjs/wasm-parser': 1.14.1 + acorn: 8.14.0 + browserslist: 4.24.2 + chrome-trace-event: 1.0.4 + enhanced-resolve: 5.18.0 + es-module-lexer: 1.7.0 + eslint-scope: 5.1.1 + events: 3.3.0 + glob-to-regexp: 0.4.1 + graceful-fs: 4.2.11 + json-parse-even-better-errors: 2.3.1 + loader-runner: 4.3.0 + mime-types: 2.1.35 + neo-async: 2.6.2 + schema-utils: 4.3.2 + tapable: 2.2.1 + terser-webpack-plugin: 5.3.14(webpack@5.99.9) + watchpack: 2.4.4 + webpack-sources: 3.3.3 + transitivePeerDependencies: + - '@swc/core' + - esbuild + - uglify-js + optional: true + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + word-wrap@1.2.5: {} + + wordwrap@1.0.0: {} + + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrap-ansi@8.1.0: + dependencies: + ansi-styles: 6.2.1 + string-width: 5.1.2 + strip-ansi: 7.1.0 + + yallist@3.1.1: {} + + yaml@2.6.0: {} + + yocto-queue@0.1.0: {} + + zod-validation-error@3.4.0(zod@3.23.8): + dependencies: + zod: 3.23.8 + + zod@3.23.8: {} + + zustand@5.0.2(@types/react@18.3.18)(react@18.3.1)(use-sync-external-store@1.2.2(react@18.3.1)): + optionalDependencies: + '@types/react': 18.3.18 + react: 18.3.1 + use-sync-external-store: 1.2.2(react@18.3.1) + + zwitch@2.0.4: {} diff --git a/web/postcss.config.js b/web/postcss.config.js new file mode 100644 index 0000000..2e7af2b --- /dev/null +++ b/web/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/web/public/assets/favicon.svg b/web/public/assets/favicon.svg new file mode 100644 index 0000000..b6d7946 --- /dev/null +++ b/web/public/assets/favicon.svg @@ -0,0 +1,438 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/web/src/api/access-tokens/api.ts b/web/src/api/access-tokens/api.ts new file mode 100644 index 0000000..d1cf99a --- /dev/null +++ b/web/src/api/access-tokens/api.ts @@ -0,0 +1,64 @@ +// +// Copyright (c) 2025 rustmailer.com (https://rustmailer.com) +// +// This file is part of the Bichon Email Archiving Project +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +import axiosInstance from "@/api/axiosInstance"; +import { AccessToken } from "@/features/access-tokens/data/schema"; + +export const login = async (password: string) => { + const response = await axiosInstance.post(`/api/login`, password, { + headers: { + "Content-Type": "text/plain", + }, + }); + return response.data; +}; + +export const reset_root_token = async () => { + const response = await axiosInstance.post("/api/v1/reset-root-token"); + return response.data; +}; + +export const reset_root_password = async (password: string) => { + const response = await axiosInstance.post("/api/v1/reset-root-password", password, { + headers: { + "Content-Type": "text/plain", + }, + }); + return response.data; +}; + +export const list_access_tokens = async () => { + const response = await axiosInstance.get("/api/v1/access-token-list"); + return response.data; +}; + +export const create_access_token = async (data: Record) => { + const response = await axiosInstance.post("/api/v1/access-token", data); + return response.data; +} + +export const update_access_token = async (token: string, data: Record) => { + const response = await axiosInstance.post(`/api/v1/access-token/${token}`, data); + return response.data; +} + +export const delete_access_token = async (token: string) => { + const response = await axiosInstance.delete(`/api/v1/access-token/${token}`); + return response.data; +} \ No newline at end of file diff --git a/web/src/api/account/api.ts b/web/src/api/account/api.ts new file mode 100644 index 0000000..9d4170c --- /dev/null +++ b/web/src/api/account/api.ts @@ -0,0 +1,105 @@ +// +// Copyright (c) 2025 rustmailer.com (https://rustmailer.com) +// +// This file is part of the Bichon Email Archiving Project +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +import axiosInstance from "@/api/axiosInstance"; +import { AccountModel } from "@/features/accounts/data/schema"; +import { PaginatedResponse } from ".."; + +export interface MinimalAccount { + id: number; + email: string; +} + +export const minimal_account_list = async () => { + const response = await axiosInstance.get("/api/v1/minimal-account-list"); + return response.data; +}; + + +export interface ErrorMessage { + error: string; + at: number; // milliseconds timestamp +} + +export type ProgressMap = Record; + +export interface AccountRunningState { + account_id: number; + last_incremental_sync_start: number; + last_incremental_sync_end?: number, + errors: ErrorMessage[]; + is_initial_sync_completed: boolean; + progress?: ProgressMap; + initial_sync_start_time?: number; + initial_sync_end_time?: number; +} + + +export interface MailboxBatchProgress { + total_batches: number; + current_batch: number; +} + +export const account_state = async (account_id: number) => { + const response = await axiosInstance.get(`/api/v1/account-state/${account_id}`); + return response.data; +}; + +export const create_account = async (data: Record) => { + const response = await axiosInstance.post("/api/v1/account", data); + return response.data; +}; + +export const list_accounts = async () => { + const response = await axiosInstance.get>("/api/v1/accounts?desc=true"); + return response.data; +}; + +export const update_account = async (account_id: number, data: Record) => { + const response = await axiosInstance.post(`/api/v1/account/${account_id}`, data); + return response.data; +}; + +export const remove_account = async (account_id: number) => { + const response = await axiosInstance.delete(`/api/v1/account/${account_id}`); + return response.data; +}; + +export interface AutoConfigResult { + imap: ServerConfig; + oauth2?: OAuth2Config; +} + +export interface ServerConfig { + host: string; + port: number; + encryption: 'None' | 'Ssl' | 'StartTls'; +} + +export interface OAuth2Config { + issuer: string; + scope: string; + auth_url: string; + token_url: string; +} + +export const autoconfig = async (email: string) => { + const response = await axiosInstance.get(`/api/v1/autoconfig/${email}`); + return response.data; +}; diff --git a/web/src/api/axiosInstance.ts b/web/src/api/axiosInstance.ts new file mode 100644 index 0000000..49570d2 --- /dev/null +++ b/web/src/api/axiosInstance.ts @@ -0,0 +1,61 @@ +// +// Copyright (c) 2025 rustmailer.com (https://rustmailer.com) +// +// This file is part of the Bichon Email Archiving Project +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +import { getAccessToken } from "@/stores/authStore"; +import axios from "axios"; + +// Create an Axios instance +const baseURL = process.env.NODE_ENV === "production" + ? "/" // Production: relative to the current domain + : "http://localhost:15630"; // Development: Poem's backend server + +const axiosInstance = axios.create({ + baseURL, + timeout: 30000, // Timeout in milliseconds + headers: { + "Content-Type": "application/json", // Explicitly setting Content-Type to application/json + }, +}); + +// Add a request interceptor to include the access token in headers +axiosInstance.interceptors.request.use( + (config) => { + const accessToken = getAccessToken(); // Retrieve access token from localStorage + if (accessToken) { + config.headers.Authorization = `Bearer ${accessToken}`; + } + return config; + }, + (error) => { + // Handle request errors + return Promise.reject(error); + } +); + +// Add a response interceptor (optional) +axiosInstance.interceptors.response.use( + (response) => response, + (error) => { + // Handle response errors + //console.error("API error:", error.response?.data || error.message); + return Promise.reject(error); + } +); + +export default axiosInstance; diff --git a/web/src/api/index.ts b/web/src/api/index.ts new file mode 100644 index 0000000..db468d7 --- /dev/null +++ b/web/src/api/index.ts @@ -0,0 +1,46 @@ +// +// Copyright (c) 2025 rustmailer.com (https://rustmailer.com) +// +// This file is part of the Bichon Email Archiving Project +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +export interface PaginatedResponse { + current_page: number | null; + page_size: number | null; + total_items: number; + items: S[]; + total_pages: number | null; +} + + +export interface EmailEnvelope { + id: number; + message_id: string; + account_id: number; + uid: number; + subject: string; + text: string; + from: string; + to: string[]; + cc: string[]; + bcc: string[]; + date: number; + internal_date: number; + size: number; + thread_id: number, + attachments: string[]; + tags: string[]; +} \ No newline at end of file diff --git a/web/src/api/mailbox/api.ts b/web/src/api/mailbox/api.ts new file mode 100644 index 0000000..9a2ebbf --- /dev/null +++ b/web/src/api/mailbox/api.ts @@ -0,0 +1,37 @@ +// +// Copyright (c) 2025 rustmailer.com (https://rustmailer.com) +// +// This file is part of the Bichon Email Archiving Project +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +import axiosInstance from "@/api/axiosInstance"; + + +export interface MailboxData { + attributes: { attr: string; extension: string | null }[]; + delimiter: string | null; + exists: number; + id: number; + name: string; + uid_next: number | null; + uid_validity: number | null; + unseen: number | null; +} + +export const list_mailboxes = async (accountId: number, remote: boolean) => { + const response = await axiosInstance.get(`/api/v1/list-mailboxes/${accountId}?remote=${remote}`); + return response.data; +}; \ No newline at end of file diff --git a/web/src/api/mailbox/envelope/api.ts b/web/src/api/mailbox/envelope/api.ts new file mode 100644 index 0000000..f30bf60 --- /dev/null +++ b/web/src/api/mailbox/envelope/api.ts @@ -0,0 +1,99 @@ +// +// Copyright (c) 2025 rustmailer.com (https://rustmailer.com) +// +// This file is part of the Bichon Email Archiving Project +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +import { EmailEnvelope, PaginatedResponse } from "@/api"; +import axiosInstance from "@/api/axiosInstance"; +import { saveAs } from 'file-saver'; + +export const list_messages = async (accountId: number, mailbox_id: number, page: number, page_size: number) => { + const params = new URLSearchParams({ + mailbox_id: String(mailbox_id), + page: String(page), + page_size: String(page_size), + }); + + const response = await axiosInstance.get>( + `/api/v1/list-messages/${accountId}?${params.toString()}` + ); + return response.data; +}; + +export const get_thread_messages = async (accountId: number, thread_id: number, page: number, page_size: number) => { + const params = new URLSearchParams({ + thread_id: String(thread_id), + page: String(page), + page_size: String(page_size), + }); + + const response = await axiosInstance.get>( + `/api/v1/get-thread-messages/${accountId}?${params.toString()}` + ); + return response.data; +} + +export const download_attachment = async (accountId: number, id: number, attachmentFileName: string) => { + const response = await axiosInstance.get(`/api/v1/download-attachment/${accountId}?id=${id}&name=${attachmentFileName}`, { responseType: 'blob' }); + const blob = new Blob([response.data]); + saveAs(blob, attachmentFileName); +}; + + +export interface AttachmentInfo { + /** MIME content type of the attachment (e.g., `image/png`, `application/pdf`). */ + file_type: string; + /** Content-ID, used for inline attachments (referenced in HTML by `cid:` URLs). */ + content_id?: string; + /** Whether the attachment is marked as inline (true) or a regular file (false). */ + inline: boolean; + /** Original filename of the attachment, if provided. */ + filename: string; + /** Size of the attachment in bytes. */ + size: number; +} + +export interface MessageContentResponse { + text?: string; + html?: string; + attachments?: AttachmentInfo[] +} + +export const getContent = (messageContent: MessageContentResponse): string | null => { + if (messageContent.html) { + return messageContent.html; + } else if (messageContent.text) { + return messageContent.text; + } + return null; +}; + +export const load_message = async (accountId: number, id: number) => { + const response = await axiosInstance.get(`/api/v1/message-content/${accountId}?id=${id}`); + return response.data; +}; + +export const delete_messages = async (payload: Record) => { + const response = await axiosInstance.post("/api/v1/delete-messages", payload); + return response.data; +}; + +export const download_message = async (accountId: number, id: number) => { + const response = await axiosInstance.get(`/api/v1/download-message/${accountId}?id=${id}`, { responseType: 'blob' }); + const blob = new Blob([response.data]); + saveAs(blob, `${id}.eml`); +}; \ No newline at end of file diff --git a/web/src/api/oauth2/api.ts b/web/src/api/oauth2/api.ts new file mode 100644 index 0000000..bd1288d --- /dev/null +++ b/web/src/api/oauth2/api.ts @@ -0,0 +1,62 @@ +// +// Copyright (c) 2025 rustmailer.com (https://rustmailer.com) +// +// This file is part of the Bichon Email Archiving Project +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +import { OAuth2Entity } from "@/features/oauth2/data/schema"; +import axiosInstance from "../axiosInstance"; + +export const get_oauth2_list = async () => { + const response = await axiosInstance.get<{ items: OAuth2Entity[] }>("/api/v1/oauth2-list"); + return response.data; +}; + +export const delete_oauth2 = async (id: number) => { + const response = await axiosInstance.delete(`/api/v1/oauth2/${id}`); + return response.data; +}; + +export const create_oauth2 = async (data: Record) => { + const response = await axiosInstance.post("/api/v1/oauth2", data); + return response.data; +}; + +export const update_oauth2 = async (id: number, data: Record) => { + const response = await axiosInstance.post(`/api/v1/oauth2/${id}`, data); + return response.data; +}; + + +export const get_authorize_url = async (data: Record) => { + const response = await axiosInstance.post('/api/v1/oauth2-authorize-url', data); + return response.data; +}; + + +export interface OAuth2Tokens { + access_token: string; + account_id: string; + created_at: number; + oauth2_name: string; + refresh_token: string; + updated_at: number; +} + +export const get_oauth2_tokens = async (accountId: number) => { + const response = await axiosInstance.get(`/api/v1/oauth2-tokens/${accountId}`); + return response.data; +}; \ No newline at end of file diff --git a/web/src/api/search/api.ts b/web/src/api/search/api.ts new file mode 100644 index 0000000..99e803a --- /dev/null +++ b/web/src/api/search/api.ts @@ -0,0 +1,43 @@ +// +// Copyright (c) 2025 rustmailer.com (https://rustmailer.com) +// +// This file is part of the Bichon Email Archiving Project +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +import axiosInstance from "@/api/axiosInstance"; +import { EmailEnvelope, PaginatedResponse } from ".."; + +export const search_messages = async (payload: Record) => { + const response = await axiosInstance.post>("/api/v1/search-messages", payload); + return response.data; +}; + +export interface TagCount { + tag: string; + count: number; +} + +export const get_top_tags = async () => { + const response = await axiosInstance.get("/api/v1/all-tags"); + return response.data; +} + +export const update_tags = async (data: Record) => { + const response = await axiosInstance.post("/api/v1/update-tags", data); + return response.data; +}; + + diff --git a/web/src/api/system/api.ts b/web/src/api/system/api.ts new file mode 100644 index 0000000..6f14cb5 --- /dev/null +++ b/web/src/api/system/api.ts @@ -0,0 +1,105 @@ +// +// Copyright (c) 2025 rustmailer.com (https://rustmailer.com) +// +// This file is part of the Bichon Email Archiving Project +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +import axiosInstance from "@/api/axiosInstance"; +import { Proxy } from "@/features/settings/proxy/data/schema"; + +export interface Release { + tag_name: string; + published_at: string; + body: string; + html_url: string; +} + +export interface ReleaseNotification { + latest: Release | null; // `latest` can be null if the release information is not available + is_newer: boolean; + error_message: string | null; // New field to store error message when the request fails +} + +interface Notifications { + release: ReleaseNotification; +} + +export const get_notifications = async () => { + const response = await axiosInstance.get(`/api/v1/notifications`); + return response.data; +}; + +export interface DashboardStats { + account_count: number; // Number of accounts + email_count: number; // Total number of emails + total_size_bytes: number; // Total size of all emails (in bytes) + storage_usage_bytes: number; // Actual storage used (in bytes) + index_usage_bytes: number; // Index storage size (in bytes) + recent_activity: TimeBucket[]; // Email activity over recent days + top_senders: Group[]; // Top 10 senders + top_accounts: Group[]; // Top 10 senders + with_attachment_count: number; // Emails with attachments + without_attachment_count: number; // Emails without attachments + top_largest_emails: LargestEmail[]; // Top 10 largest emails +} + +export interface TimeBucket { + timestamp_ms: number; // Timestamp in milliseconds + count: number; // Number of emails in this time bucket +} + +export interface Group { + key: string; // Sender email or name + count: number; // Number of emails from this sender +} + +export interface LargestEmail { + subject: string; // Email subject + size_bytes: number; // Email size in bytes +} + +export const get_dashboard_stats = async () => { + const response = await axiosInstance.get(`/api/v1/dashboard-stats`); + return response.data; +}; + +export const list_proxy = async () => { + const response = await axiosInstance.get(`/api/v1/list-proxy`); + return response.data; +}; + +export const delete_proxy = async (id: number) => { + const response = await axiosInstance.delete(`/api/v1/proxy/${id}`); + return response.data; +}; + +export const update_proxy = async (id: number, url: string) => { + const response = await axiosInstance.post(`/api/v1/proxy/${id}`, url, { + headers: { + "Content-Type": "text/plain", + }, + }); + return response.data; +}; + +export const add_proxy = async (url: string) => { + const response = await axiosInstance.post(`/api/v1/proxy`, url, { + headers: { + "Content-Type": "text/plain", + }, + }); + return response.data; +}; \ No newline at end of file diff --git a/web/src/assets/logo.svg b/web/src/assets/logo.svg new file mode 100644 index 0000000..b6d7946 --- /dev/null +++ b/web/src/assets/logo.svg @@ -0,0 +1,438 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/web/src/components/ace-editor.tsx b/web/src/components/ace-editor.tsx new file mode 100644 index 0000000..0b7e640 --- /dev/null +++ b/web/src/components/ace-editor.tsx @@ -0,0 +1,79 @@ +// +// Copyright (c) 2025 rustmailer.com (https://rustmailer.com) +// +// This file is part of the Bichon Email Archiving Project +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +import React from 'react'; +import AceEditor from 'react-ace'; +import { cn } from '@/lib/utils'; +import 'ace-builds/src-noconflict/mode-handlebars'; +import 'ace-builds/src-noconflict/mode-json'; +import 'ace-builds/src-noconflict/mode-python'; +import 'ace-builds/src-noconflict/mode-markdown'; +import 'ace-builds/src-noconflict/theme-kuroir'; +import 'ace-builds/src-noconflict/theme-monokai'; +import 'ace-builds/src-noconflict/ext-language_tools'; + +interface ReactAceEditorProps { + value?: string; + onChange?: (value: string) => void; + placeholder?: string; + className?: string; + readOnly?: boolean; + theme?: 'kuroir' | 'monokai'; + mode?: 'handlebars' | 'json' | 'markdown' | 'python'; +} + +const ReactAceEditor: React.FC = ({ + value, + onChange, + placeholder, + className, + readOnly = false, + theme = 'github', + mode = 'handlebars' +}) => { + return ( +
+ +
+ ); +}; + +export default ReactAceEditor; \ No newline at end of file diff --git a/web/src/components/api-docs.tsx b/web/src/components/api-docs.tsx new file mode 100644 index 0000000..559bd45 --- /dev/null +++ b/web/src/components/api-docs.tsx @@ -0,0 +1,77 @@ +// +// Copyright (c) 2025 rustmailer.com (https://rustmailer.com) +// +// This file is part of the Bichon Email Archiving Project +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +import { Card } from "@/components/ui/card"; +import { FixedHeader } from "./layout/fixed-header"; +import { Main } from "./layout/main"; +import Logo from '@/assets/logo.svg' + +const docsOptions = [ + { name: "Swagger UI", path: "/api-docs/swagger" }, + { name: "ReDoc", path: "/api-docs/redoc" }, + { name: "OpenAPI Explorer", path: "/api-docs/explorer" }, + { name: "Scalar", path: "/api-docs/scalar" }, + { name: "Download Spec YAML", path: "/api-docs/spec.yaml" } +]; + +export default function APIDocs() { + const handleCardClick = (path: string) => { + // Open in new tab + window.open(path, '_blank', 'noopener,noreferrer'); + }; + + return ( + <> + + +
+
+
+

API Documentation

+

+ Choose your preferred API documentation type +

+
+
+
+
+
+ {docsOptions.map((option) => ( + handleCardClick(option.path)} + > +
+ RustMailer Logo +

{option.name}

+
+
+ ))} +
+
+
+
+ + ); +} \ No newline at end of file diff --git a/web/src/components/button.tsx b/web/src/components/button.tsx new file mode 100644 index 0000000..18d45f0 --- /dev/null +++ b/web/src/components/button.tsx @@ -0,0 +1,118 @@ +// +// Copyright (c) 2025 rustmailer.com (https://rustmailer.com) +// +// This file is part of the Bichon Email Archiving Project +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +import * as React from 'react' +import { Slot } from '@radix-ui/react-slot' +import { IconLoader2 } from '@tabler/icons-react' +import { cva, type VariantProps } from 'class-variance-authority' +import { cn } from '@/lib/utils' + +const buttonVariants = cva( + 'inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50', + { + variants: { + variant: { + default: + 'bg-primary text-primary-foreground shadow hover:bg-primary/90', + destructive: + 'bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90', + outline: + 'border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground', + secondary: + 'bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80', + ghost: 'hover:bg-accent hover:text-accent-foreground', + link: 'text-primary underline-offset-4 hover:underline', + }, + size: { + default: 'h-9 px-4 py-2', + sm: 'h-8 rounded-md px-3 text-xs', + lg: 'h-10 rounded-md px-8', + icon: 'h-9 w-9', + }, + }, + defaultVariants: { + variant: 'default', + size: 'default', + }, + } +) + +interface ButtonPropsBase + extends React.ButtonHTMLAttributes, + VariantProps {} + +type ButtonProps = ButtonPropsBase & + ( + | { asChild: true } + | { + asChild?: false + loading?: boolean + leftSection?: React.JSX.Element + rightSection?: React.JSX.Element + } + ) + +const Button = React.forwardRef( + ({ className, variant, size, children, ...props }, ref) => { + const { asChild, ...rest } = props + if (asChild) { + return ( + + {children} + + ) + } + + const { + loading = false, + leftSection, + rightSection, + disabled, + ...otherProps + } = props + + return ( + + ) + } +) +Button.displayName = 'Button' + +// eslint-disable-next-line react-refresh/only-export-components +export { Button, buttonVariants } diff --git a/web/src/components/command-menu.tsx b/web/src/components/command-menu.tsx new file mode 100644 index 0000000..f1253a6 --- /dev/null +++ b/web/src/components/command-menu.tsx @@ -0,0 +1,110 @@ +// +// Copyright (c) 2025 rustmailer.com (https://rustmailer.com) +// +// This file is part of the Bichon Email Archiving Project +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +import React from 'react' +import { useNavigate } from '@tanstack/react-router' +import { + IconArrowRightDashed, + IconMoon, + IconSun, +} from '@tabler/icons-react' +import { useSearch } from '@/context/search-context' +import { useTheme } from '@/context/theme-context' +import { + CommandDialog, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, + CommandSeparator, +} from '@/components/ui/command' +import { sidebarData } from './layout/data/sidebar-data' +import { ScrollArea } from './ui/scroll-area' + +export function CommandMenu() { + const navigate = useNavigate() + const { setTheme } = useTheme() + const { open, setOpen } = useSearch() + + const runCommand = React.useCallback( + (command: () => unknown) => { + setOpen(false) + command() + }, + [setOpen] + ) + + return ( + + + + + No results found. + {sidebarData.navGroups.map((group) => ( + + {group.items.map((navItem, i) => { + if (navItem.url) + return ( + { + runCommand(() => navigate({ to: navItem.url })) + }} + > +
+ +
+ {navItem.title} +
+ ) + + return navItem.items?.map((subItem, i) => ( + { + runCommand(() => navigate({ to: subItem.url })) + }} + > +
+ +
+ {subItem.title} +
+ )) + })} +
+ ))} + + + runCommand(() => setTheme('light'))}> + Light + + runCommand(() => setTheme('dark'))}> + + Dark + + +
+
+
+ ) +} diff --git a/web/src/components/confirm-dialog.tsx b/web/src/components/confirm-dialog.tsx new file mode 100644 index 0000000..fd9a147 --- /dev/null +++ b/web/src/components/confirm-dialog.tsx @@ -0,0 +1,86 @@ +// +// Copyright (c) 2025 rustmailer.com (https://rustmailer.com) +// +// This file is part of the Bichon Email Archiving Project +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +import { cn } from '@/lib/utils' +import { + AlertDialog, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from '@/components/ui/alert-dialog' +import { Button } from '@/components/ui/button' + +interface ConfirmDialogProps { + open: boolean + onOpenChange: (open: boolean) => void + title: React.ReactNode + disabled?: boolean + desc: React.JSX.Element | string + cancelBtnText?: string + confirmText?: React.ReactNode + destructive?: boolean + handleConfirm: () => void + isLoading?: boolean + className?: string + children?: React.ReactNode +} + +export function ConfirmDialog(props: ConfirmDialogProps) { + const { + title, + desc, + children, + className, + confirmText, + cancelBtnText, + destructive, + isLoading, + disabled = false, + handleConfirm, + ...actions + } = props + return ( + + + + {title} + +
{desc}
+
+
+ {children} + + + {cancelBtnText ?? 'Cancel'} + + + +
+
+ ) +} diff --git a/web/src/components/date-picker.tsx b/web/src/components/date-picker.tsx new file mode 100644 index 0000000..d3ed612 --- /dev/null +++ b/web/src/components/date-picker.tsx @@ -0,0 +1,51 @@ +import { format } from 'date-fns' +import { Calendar as CalendarIcon } from 'lucide-react' +import { Button } from '@/components/ui/button' +import { Calendar } from '@/components/ui/calendar' +import { + Popover, + PopoverContent, + PopoverTrigger, +} from '@/components/ui/popover' + +type DatePickerProps = { + selected: Date | undefined + onSelect: (date: Date | undefined) => void + placeholder?: string +} + +export function DatePicker({ + selected, + onSelect, + placeholder = 'Pick a date', +}: DatePickerProps) { + return ( + + + + + + + date > new Date() || date < new Date('1900-01-01') + } + /> + + + ) +} \ No newline at end of file diff --git a/web/src/components/editor-with-draft.tsx b/web/src/components/editor-with-draft.tsx new file mode 100644 index 0000000..bbf7033 --- /dev/null +++ b/web/src/components/editor-with-draft.tsx @@ -0,0 +1,110 @@ +// +// Copyright (c) 2025 rustmailer.com (https://rustmailer.com) +// +// This file is part of the Bichon Email Archiving Project +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +import { useEffect, useState } from 'react'; +import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'; +import { Button } from '@/components/ui/button'; +import { AlertTriangle } from 'lucide-react'; +import AceEditor from './ace-editor'; + +interface CodeEditorWithDraftProps { + value: string | undefined; + onChange: (value: string) => void; + localStorageKey: string; + mode?: 'handlebars' | 'json' | 'markdown' | 'python'; + theme?: 'kuroir' | 'monokai'; + placeholder?: string; + className?: string; +} + +export function CodeEditorWithDraft({ + value, + onChange, + localStorageKey, + mode, + theme, + placeholder, + className, +}: CodeEditorWithDraftProps) { + const [hasDraft, setHasDraft] = useState(false); + + useEffect(() => { + const saved = localStorage.getItem(localStorageKey); + if (saved && !value) { + setHasDraft(true); + } + }, []); + + useEffect(() => { + const save = () => { + if (value) { + localStorage.setItem(localStorageKey, value); + } + }; + const handle = setInterval(save, 2000); + return () => { + clearInterval(handle); + } + }, [value]); + + const handleRestore = () => { + const saved = localStorage.getItem(localStorageKey); + if (saved) { + onChange(saved); + setHasDraft(false); + } + }; + + const handleDiscard = () => { + localStorage.removeItem(localStorageKey); + setHasDraft(false); + }; + + return ( +
+ {hasDraft && ( + + +
+ Unsaved draft detected + + A draft was found in your browser. Would you like to restore it? +
+ + +
+
+
+
+ )} + +
+ ); +} diff --git a/web/src/components/layout/app-sidebar.tsx b/web/src/components/layout/app-sidebar.tsx new file mode 100644 index 0000000..e38a795 --- /dev/null +++ b/web/src/components/layout/app-sidebar.tsx @@ -0,0 +1,63 @@ +// +// Copyright (c) 2025 rustmailer.com (https://rustmailer.com) +// +// This file is part of the Bichon Email Archiving Project +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +import { + Sidebar, + SidebarContent, + SidebarHeader, + SidebarMenuButton, + useSidebar, +} from '@/components/ui/sidebar' +import { NavGroup } from '@/components/layout/nav-group' +import Logo from '@/assets/logo.svg' +import { sidebarData } from './data/sidebar-data' + +export function AppSidebar({ ...props }: React.ComponentProps) { + const { open } = useSidebar(); + return ( + + + +
+ Logo +
+
+ + Bichon + +
+
+
+ + {sidebarData.navGroups.map((props) => ( + + ))} + +
+ ) +} diff --git a/web/src/components/layout/data/sidebar-data.ts b/web/src/components/layout/data/sidebar-data.ts new file mode 100644 index 0000000..5c81644 --- /dev/null +++ b/web/src/components/layout/data/sidebar-data.ts @@ -0,0 +1,92 @@ +// +// Copyright (c) 2025 rustmailer.com (https://rustmailer.com) +// +// This file is part of the Bichon Email Archiving Project +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +import { + IconHelp, + IconLayoutDashboard, + IconLockAccess, + IconSettings +} from '@tabler/icons-react' +import { IdCard, Inbox, Mailbox, Search } from 'lucide-react' +import { type SidebarData } from '../types' + +export const sidebarData: SidebarData = { + navGroups: [ + { + title: 'General', + items: [ + { + title: 'Dashboard', + url: '/', + icon: IconLayoutDashboard, + } + ], + }, + { + title: 'Accounts', + items: [ + { + title: 'Accounts', + url: '/accounts', + icon: Inbox, + }, + { + title: 'MailBox', + url: '/mailboxes', + icon: Mailbox, + }, + { + title: 'Search', + url: '/search', + icon: Search, + } + ], + }, + { + title: 'Auth', + items: [ + { + title: 'OAuth2', + url: '/oauth2', + icon: IdCard, + }, + { + title: 'Access Tokens', + url: '/access-tokens', + icon: IconLockAccess, + } + ] + }, + { + title: 'Other', + items: [ + { + title: 'Settings', + url: '/settings', + icon: IconSettings, + }, + { + title: 'API Documentation', + url: '/api-docs', + icon: IconHelp, + }, + ], + }, + ], +} diff --git a/web/src/components/layout/fixed-header.tsx b/web/src/components/layout/fixed-header.tsx new file mode 100644 index 0000000..a30ec9e --- /dev/null +++ b/web/src/components/layout/fixed-header.tsx @@ -0,0 +1,38 @@ +// +// Copyright (c) 2025 rustmailer.com (https://rustmailer.com) +// +// This file is part of the Bichon Email Archiving Project +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +import { ThemeSwitch } from "../theme-switch"; +import { ProfileDropdown } from "../profile-dropdown"; +import { Header } from "./header"; +import { NotificationPopover } from "./notification"; +import { GithubLinkButton } from "./github"; + +export const FixedHeader = () => { + return ( +
+ {/* */} +
+ + + + +
+
+ ); +}; \ No newline at end of file diff --git a/web/src/components/layout/github.tsx b/web/src/components/layout/github.tsx new file mode 100644 index 0000000..f319b7b --- /dev/null +++ b/web/src/components/layout/github.tsx @@ -0,0 +1,48 @@ +// +// Copyright (c) 2025 rustmailer.com (https://rustmailer.com) +// +// This file is part of the Bichon Email Archiving Project +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +import React from "react"; +import { GitHubLogoIcon } from "@radix-ui/react-icons"; + +interface GithubLinkButtonProps { + /** GitHub repository or profile URL */ + href?: string; + /** Icon size (default: 20) */ + size?: number; + /** Optional tooltip title */ + title?: string; +} + +export const GithubLinkButton: React.FC = ({ + href = "https://github.com/rustmailer/bichon", + size = 20, + title = "View on GitHub", +}) => { + return ( + + + + ); +}; diff --git a/web/src/components/layout/header.tsx b/web/src/components/layout/header.tsx new file mode 100644 index 0000000..fe8f34f --- /dev/null +++ b/web/src/components/layout/header.tsx @@ -0,0 +1,67 @@ +// +// Copyright (c) 2025 rustmailer.com (https://rustmailer.com) +// +// This file is part of the Bichon Email Archiving Project +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +import React from 'react' +import { cn } from '@/lib/utils' +import { Separator } from '@/components/ui/separator' +import { SidebarTrigger } from '@/components/ui/sidebar' + +interface HeaderProps extends React.HTMLAttributes { + fixed?: boolean + ref?: React.Ref +} + +export const Header = ({ + className, + fixed, + children, + ...props +}: HeaderProps) => { + const [offset, setOffset] = React.useState(0) + + React.useEffect(() => { + const onScroll = () => { + setOffset(document.body.scrollTop || document.documentElement.scrollTop) + } + + // Add scroll listener to the body + document.addEventListener('scroll', onScroll, { passive: true }) + + // Clean up the event listener on unmount + return () => document.removeEventListener('scroll', onScroll) + }, []) + + return ( +
10 && fixed ? 'shadow' : 'shadow-none', + className + )} + {...props} + > + + + {children} +
+ ) +} + +Header.displayName = 'Header' diff --git a/web/src/components/layout/main.tsx b/web/src/components/layout/main.tsx new file mode 100644 index 0000000..58c72ab --- /dev/null +++ b/web/src/components/layout/main.tsx @@ -0,0 +1,44 @@ +// +// Copyright (c) 2025 rustmailer.com (https://rustmailer.com) +// +// This file is part of the Bichon Email Archiving Project +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +import React from 'react' +import { cn } from '@/lib/utils' + +interface MainProps extends React.HTMLAttributes { + fixed?: boolean; + higher?: boolean; + ref?: React.Ref; +} + +export const Main = ({ fixed, higher, ...props }: MainProps) => { + return ( +
+ ); +}; + +Main.displayName = 'Main'; + diff --git a/web/src/components/layout/nav-group.tsx b/web/src/components/layout/nav-group.tsx new file mode 100644 index 0000000..c50a45e --- /dev/null +++ b/web/src/components/layout/nav-group.tsx @@ -0,0 +1,199 @@ +// +// Copyright (c) 2025 rustmailer.com (https://rustmailer.com) +// +// This file is part of the Bichon Email Archiving Project +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +import { ReactNode } from 'react' +import { Link, useLocation } from '@tanstack/react-router' +import { ChevronRight } from 'lucide-react' +import { + Collapsible, + CollapsibleContent, + CollapsibleTrigger, +} from '@/components/ui/collapsible' +import { + SidebarGroup, + SidebarGroupLabel, + SidebarMenu, + SidebarMenuButton, + SidebarMenuItem, + SidebarMenuSub, + SidebarMenuSubButton, + SidebarMenuSubItem, + useSidebar, +} from '@/components/ui/sidebar' +import { Badge } from '../ui/badge' +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from '../ui/dropdown-menu' +import { NavCollapsible, NavItem, NavLink, type NavGroup } from './types' + +export function NavGroup({ title, items }: NavGroup) { + const { state } = useSidebar() + const href = useLocation({ select: (location) => location.href }) + return ( + + {title} + + {items.map((item) => { + const key = `${item.title}-${item.url}` + + if (!item.items) + return + + if (state === 'collapsed') + return ( + + ) + + return + })} + + + ) +} + +const NavBadge = ({ children }: { children: ReactNode }) => ( + {children} +) + +const SidebarMenuLink = ({ item, href }: { item: NavLink; href: string }) => { + const { setOpenMobile } = useSidebar() + return ( + + + setOpenMobile(false)}> + {item.icon && } + {item.title} + {item.badge && {item.badge}} + + + + ) +} + +const SidebarMenuCollapsible = ({ + item, + href, +}: { + item: NavCollapsible + href: string +}) => { + const { setOpenMobile } = useSidebar() + return ( + + + + + {item.icon && } + {item.title} + {item.badge && {item.badge}} + + + + + + {item.items.map((subItem) => ( + + + setOpenMobile(false)}> + {subItem.icon && } + {subItem.title} + {subItem.badge && {subItem.badge}} + + + + ))} + + + + + ) +} + +const SidebarMenuCollapsedDropdown = ({ + item, + href, +}: { + item: NavCollapsible + href: string +}) => { + return ( + + + + + {item.icon && } + {item.title} + {item.badge && {item.badge}} + + + + + + {item.title} {item.badge ? `(${item.badge})` : ''} + + + {item.items.map((sub) => ( + + + {sub.icon && } + {sub.title} + {sub.badge && ( + {sub.badge} + )} + + + ))} + + + + ) +} + +function checkIsActive(href: string, item: NavItem, mainNav = false) { + return ( + href === item.url || // /endpint?search=param + href.split('?')[0] === item.url || // endpoint + !!item?.items?.filter((i) => i.url === href).length || // if child nav is active + (mainNav && + href.split('/')[1] !== '' && + href.split('/')[1] === item?.url?.split('/')[1]) + ) +} diff --git a/web/src/components/layout/nav-user.tsx b/web/src/components/layout/nav-user.tsx new file mode 100644 index 0000000..bda03fb --- /dev/null +++ b/web/src/components/layout/nav-user.tsx @@ -0,0 +1,126 @@ +// +// Copyright (c) 2025 rustmailer.com (https://rustmailer.com) +// +// This file is part of the Bichon Email Archiving Project +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +import { + BadgeCheck, + Bell, + ChevronsUpDown, + CreditCard, + LogOut, + Sparkles, +} from 'lucide-react' +import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar' +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuGroup, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from '@/components/ui/dropdown-menu' +import { + SidebarMenu, + SidebarMenuButton, + SidebarMenuItem, + useSidebar, +} from '@/components/ui/sidebar' + +export function NavUser({ + user, +}: { + user: { + name: string + email: string + avatar: string + } +}) { + const { isMobile } = useSidebar() + + return ( + + + + + + + + SN + +
+ {user.name} + {user.email} +
+ +
+
+ + +
+ + + SN + +
+ {user.name} + {user.email} +
+
+
+ + + + + Upgrade to Pro + + + + + + + Account + + + + Billing + + + + Notifications + + + + + + Log out + +
+
+
+
+ ) +} diff --git a/web/src/components/layout/notification.tsx b/web/src/components/layout/notification.tsx new file mode 100644 index 0000000..816e890 --- /dev/null +++ b/web/src/components/layout/notification.tsx @@ -0,0 +1,179 @@ +// +// Copyright (c) 2025 rustmailer.com (https://rustmailer.com) +// +// This file is part of the Bichon Email Archiving Project +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +import { BellIcon, Loader2, ExternalLinkIcon } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { Badge } from "@/components/ui/badge"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover"; +import { ScrollArea } from "@/components/ui/scroll-area"; +import { useQuery } from "@tanstack/react-query"; +import { get_notifications } from "@/api/system/api"; +import ReactMarkdown from 'react-markdown'; +import remarkGfm from 'remark-gfm'; +import { useMemo } from "react"; + +interface Release { + tag_name: string; + published_at: string; + body: string; + html_url: string; +} + +interface BaseNotification { + type: string; +} + +interface ReleaseNotification extends BaseNotification { + type: 'new-release'; + data: Release; +} + +type ActiveNotification = ReleaseNotification; + + +export function NotificationPopover() { + const { data, isLoading } = useQuery({ + queryKey: ['system-notifications'], + queryFn: get_notifications, + staleTime: 1000 * 60 * 30, // 30 minutes + }); + + const activeNotifications = useMemo((): ActiveNotification[] => { + if (!data) return []; + + const notifications: ActiveNotification[] = []; + + if (data.release.is_newer && data.release.latest) { + notifications.push({ + type: 'new-release', + data: { + tag_name: data.release.latest.tag_name, + published_at: data.release.latest.published_at, + body: data.release.latest.body, + html_url: data.release.latest.html_url + } + }); + } + return notifications; + }, [data]); + + const showNotificationBadge = activeNotifications.length > 0; + + return ( + + + + + +
+

+ System Notifications + {showNotificationBadge && ` (${activeNotifications.length})`} +

+
+ + {isLoading ? ( +
+ +
+ ) : activeNotifications.length === 0 ? ( +
+ +

+ No new notifications +

+
+ ) : ( +
+ {activeNotifications.map((notification, index) => ( +
+ +
+ ))} +
+ )} +
+
+
+ ); +} + +function ReleaseNotificationView({ data }: { data: Release }) { + return ( +
+
+
+

+ {data.tag_name} +

+ + New Release + +
+

+ Released {data.published_at} +

+
+ +
+ + {data.body} + +
+ + {data.html_url && ( + + )} +
+ ); +} \ No newline at end of file diff --git a/web/src/components/layout/types.ts b/web/src/components/layout/types.ts new file mode 100644 index 0000000..e27d4ce --- /dev/null +++ b/web/src/components/layout/types.ts @@ -0,0 +1,49 @@ +// +// Copyright (c) 2025 rustmailer.com (https://rustmailer.com) +// +// This file is part of the Bichon Email Archiving Project +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +import { LinkProps } from '@tanstack/react-router' + +interface BaseNavItem { + title: string + badge?: string + icon?: React.ElementType +} + +type NavLink = BaseNavItem & { + url: LinkProps['to'] + items?: never +} + +type NavCollapsible = BaseNavItem & { + items: (BaseNavItem & { url: LinkProps['to'] })[] + url?: never +} + +type NavItem = NavCollapsible | NavLink + +interface NavGroup { + title: string + items: NavItem[] +} + +interface SidebarData { + navGroups: NavGroup[] +} + +export type { SidebarData, NavGroup, NavItem, NavCollapsible, NavLink } diff --git a/web/src/components/long-text.tsx b/web/src/components/long-text.tsx new file mode 100644 index 0000000..085bc96 --- /dev/null +++ b/web/src/components/long-text.tsx @@ -0,0 +1,104 @@ +// +// Copyright (c) 2025 rustmailer.com (https://rustmailer.com) +// +// This file is part of the Bichon Email Archiving Project +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +import { useEffect, useRef, useState } from 'react' +import { cn } from '@/lib/utils' +import { + Popover, + PopoverContent, + PopoverTrigger, +} from '@/components/ui/popover' +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from '@/components/ui/tooltip' + +interface Props { + children: React.ReactNode + className?: string + contentClassName?: string +} + +export default function LongText({ + children, + className = '', + contentClassName = '', +}: Props) { + const ref = useRef(null) + const [isOverflown, setIsOverflown] = useState(false) + + useEffect(() => { + if (checkOverflow(ref.current)) { + setIsOverflown(true) + return + } + + setIsOverflown(false) + }, []) + + if (!isOverflown) + return ( +
+ {children} +
+ ) + + return ( + <> +
+ + + +
+ {children} +
+
+ +

{children}

+
+
+
+
+
+ + +
+ {children} +
+
+ +

{children}

+
+
+
+ + ) +} + +const checkOverflow = (textContainer: HTMLDivElement | null) => { + if (textContainer) { + return ( + textContainer.offsetHeight < textContainer.scrollHeight || + textContainer.offsetWidth < textContainer.scrollWidth + ) + } + return false +} diff --git a/web/src/components/mail-iframe.tsx b/web/src/components/mail-iframe.tsx new file mode 100644 index 0000000..f338bcb --- /dev/null +++ b/web/src/components/mail-iframe.tsx @@ -0,0 +1,41 @@ +// +// Copyright (c) 2025 rustmailer.com (https://rustmailer.com) +// +// This file is part of the Bichon Email Archiving Project +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +import React from 'react'; + +interface EmailIframeProps { + emailHtml: string; + height?: number; +} + +const EmailIframe: React.FC = ({ emailHtml, height }) => { + const encodedHtml = encodeURIComponent(emailHtml); + const iframeSrc = `data:text/html;charset=utf-8,${encodedHtml}`; + + return ( +