[
  {
    "path": ".gitignore",
    "content": "# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\nlerna-debug.log*\n.pnpm-debug.log*\n.wrangler\n\n# Diagnostic reports (https://nodejs.org/api/report.html)\nreport.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json\n\n# Runtime data\npids\n*.pid\n*.seed\n*.pid.lock\n\n# Directory for instrumented libs generated by jscoverage/JSCover\nlib-cov\n\n# Coverage directory used by tools like istanbul\ncoverage\n*.lcov\n\n# nyc test coverage\n.nyc_output\n\n# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)\n.grunt\n\n# Bower dependency directory (https://bower.io/)\nbower_components\n\n# node-waf configuration\n.lock-wscript\n\n# Compiled binary addons (https://nodejs.org/api/addons.html)\nbuild/Release\n\n# Dependency directories\nnode_modules/\njspm_packages/\n\n# Snowpack dependency directory (https://snowpack.dev/)\nweb_modules/\n\n# TypeScript cache\n*.tsbuildinfo\n\n# Optional npm cache directory\n.npm\n\n# Optional eslint cache\n.eslintcache\n\n# Optional stylelint cache\n.stylelintcache\n\n# Microbundle cache\n.rpt2_cache/\n.rts2_cache_cjs/\n.rts2_cache_es/\n.rts2_cache_umd/\n\n# Optional REPL history\n.node_repl_history\n\n# Output of 'npm pack'\n*.tgz\n\n# Yarn Integrity file\n.yarn-integrity\n\n# dotenv environment variable files\n.env\n.env.development.local\n.env.test.local\n.env.production.local\n.env.local\n\n# parcel-bundler cache (https://parceljs.org/)\n.cache\n.parcel-cache\n\n# Next.js build output\n.next\nout\n\n# Nuxt.js build / generate output\n.nuxt\ndist\n\n# Gatsby files\n.cache/\n# Comment in the public line in if your project uses Gatsby and not Next.js\n# https://nextjs.org/blog/next-9-1#public-directory-support\n# public\n\n# vuepress build output\n.vuepress/dist\n\n# vuepress v2.x temp and cache directory\n.temp\n.cache\n\n# Docusaurus cache and generated files\n.docusaurus\n\n# Serverless directories\n.serverless/\n\n# FuseBox cache\n.fusebox/\n\n# DynamoDB Local files\n.dynamodb/\n\n# TernJS port file\n.tern-port\n\n# Stores VSCode versions used for testing VSCode extensions\n.vscode-test\n\n# yarn v2\n.yarn/cache\n.yarn/unplugged\n.yarn/build-state.yml\n.yarn/install-state.gz\n.pnp.*\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2025 Chenliwen\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "# Fake Mail - The free temporary email service\n\n![](./images/fakemail.png)\n\n📪 Website: [https://mail.fakeact.fun](https://mail.fakeact.fun)\n\n## Fake email generator, delete emails 2 hours after receiving\n\nThis is a temporary email service that uses Cloudflare Workers to create a temporary email address and view the received email in web browser.\n\n` /app ` - Astro ssr\n\n` /mailbox ` - Cloudflare Worker\n\n## Multiple email addresses\n\nuse [clerk](https://clerk.com/) to login && register\n\nData is stored encrypted on cloudflare D1\n\n## License\n\nFake Mail is licensed under the [MIT License](https://github.com/CH563/fakemail/blob/main/LICENSE)."
  },
  {
    "path": "app/.gitignore",
    "content": "# build output\ndist/\n\n# generated types\n.astro/\n\n# dependencies\nnode_modules/\n\n# logs\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\npnpm-debug.log*\n\n# environment variables\n.env\n.env.production\n\n# macOS-specific files\n.DS_Store\n\n# jetbrains setting folder\n.idea/\nwrangler.toml\nworker-configuration.d.ts\n.env.example"
  },
  {
    "path": "app/.vscode/extensions.json",
    "content": "{\n  \"recommendations\": [\"astro-build.astro-vscode\"],\n  \"unwantedRecommendations\": []\n}\n"
  },
  {
    "path": "app/.vscode/launch.json",
    "content": "{\n  \"version\": \"0.2.0\",\n  \"configurations\": [\n    {\n      \"command\": \"./node_modules/.bin/astro dev\",\n      \"name\": \"Development server\",\n      \"request\": \"launch\",\n      \"type\": \"node-terminal\"\n    }\n  ]\n}\n"
  },
  {
    "path": "app/README.md",
    "content": "# Astro Starter Kit: Basics\n\n```sh\nnpm create astro@latest -- --template basics\n```\n\n[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/withastro/astro/tree/latest/examples/basics)\n[![Open with CodeSandbox](https://assets.codesandbox.io/github/button-edit-lime.svg)](https://codesandbox.io/p/sandbox/github/withastro/astro/tree/latest/examples/basics)\n[![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/withastro/astro?devcontainer_path=.devcontainer/basics/devcontainer.json)\n\n> 🧑‍🚀 **Seasoned astronaut?** Delete this file. Have fun!\n\n![just-the-basics](https://github.com/withastro/astro/assets/2244813/a0a5533c-a856-4198-8470-2d67b1d7c554)\n\n## 🚀 Project Structure\n\nInside of your Astro project, you'll see the following folders and files:\n\n```text\n/\n├── public/\n│   └── favicon.svg\n├── src/\n│   ├── layouts/\n│   │   └── Layout.astro\n│   └── pages/\n│       └── index.astro\n└── package.json\n```\n\nTo learn more about the folder structure of an Astro project, refer to [our guide on project structure](https://docs.astro.build/en/basics/project-structure/).\n\n## 🧞 Commands\n\nAll commands are run from the root of the project, from a terminal:\n\n| Command                   | Action                                           |\n| :------------------------ | :----------------------------------------------- |\n| `npm install`             | Installs dependencies                            |\n| `npm run dev`             | Starts local dev server at `localhost:4321`      |\n| `npm run build`           | Build your production site to `./dist/`          |\n| `npm run preview`         | Preview your build locally, before deploying     |\n| `npm run astro ...`       | Run CLI commands like `astro add`, `astro check` |\n| `npm run astro -- --help` | Get help using the Astro CLI                     |\n\n## 👀 Want to learn more?\n\nFeel free to check [our documentation](https://docs.astro.build) or jump into our [Discord server](https://astro.build/chat).\n"
  },
  {
    "path": "app/astro.config.mjs",
    "content": "// @ts-check\nimport { defineConfig } from 'astro/config';\n\nimport react from '@astrojs/react';\n\nimport tailwind from '@astrojs/tailwind';\n\nimport cloudflare from '@astrojs/cloudflare';\n\nimport clerk from '@clerk/astro';\n\n// https://astro.build/config\nexport default defineConfig({\n  output: 'server',\n  site: 'https://mail.fakeact.fun',\n  integrations: [react(), tailwind({\n    applyBaseStyles: false,\n  }), clerk()],\n  adapter: cloudflare({\n    platformProxy: {\n      enabled: true,\n      configPath: 'wrangler.toml'\n    }\n  }),\n  vite: {\n    resolve: {\n      // Use react-dom/server.edge instead of react-dom/server.browser for React 19.\n      // Without this, MessageChannel from node:worker_threads needs to be polyfilled.\n      alias: import.meta.env.PROD ? {\n        \"react-dom/server\": \"react-dom/server.edge\",\n      } : {},\n    }\n  }\n});"
  },
  {
    "path": "app/components.json",
    "content": "{\n  \"$schema\": \"https://ui.shadcn.com/schema.json\",\n  \"style\": \"new-york\",\n  \"rsc\": false,\n  \"tsx\": true,\n  \"tailwind\": {\n    \"config\": \"tailwind.config.mjs\",\n    \"css\": \"src/styles/globals.css\",\n    \"baseColor\": \"slate\",\n    \"cssVariables\": true,\n    \"prefix\": \"\"\n  },\n  \"aliases\": {\n    \"components\": \"@/components\",\n    \"utils\": \"@/lib/utils\",\n    \"ui\": \"@/components/ui\",\n    \"lib\": \"@/lib\",\n    \"hooks\": \"@/hooks\"\n  },\n  \"iconLibrary\": \"lucide\"\n}"
  },
  {
    "path": "app/email-platform-schema.sql",
    "content": "-- 用户邮箱地址表\nCREATE TABLE user_email_addresses (\n    id INTEGER PRIMARY KEY AUTOINCREMENT,   -- 主键ID\n    user_id TEXT NOT NULL,               -- 关联的用户ID\n    email_address TEXT NOT NULL,            -- 邮箱地址\n    alias TEXT,                             -- 邮箱备注名\n    is_active INTEGER DEFAULT 1,            -- 邮箱是否激活\n    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,  -- 创建时间\n    updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,  -- 更新时间\n    UNIQUE (email_address)\n);\nCREATE INDEX idx_user_email_addresses_user_id ON user_email_addresses(user_id);\n\n-- 邮件主表\nCREATE TABLE emails (\n    id INTEGER PRIMARY KEY AUTOINCREMENT,   -- 主键ID\n    message_id TEXT NOT NULL,               -- 邮件唯一标识符\n    user_email_id INTEGER NOT NULL,         -- 关联的用户邮箱ID\n    subject TEXT,                           -- 邮件主题\n    sender TEXT NOT NULL,                   -- 发件人\n    recipient TEXT NOT NULL,                -- 收件人\n    cc TEXT,                                -- 抄送人列表\n    bcc TEXT,                               -- 密送人列表\n    content_type TEXT,                      -- 内容类型\n    body_text TEXT,                         -- 纯文本内容\n    body_html TEXT,                         -- HTML格式内容\n    received_at DATETIME NOT NULL,          -- 接收时间\n    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,  -- 创建时间\n    updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,  -- 更新时间\n    UNIQUE (message_id)\n);\nCREATE INDEX idx_emails_user_email_id ON emails(user_email_id);\nCREATE INDEX idx_emails_received_at ON emails(received_at);\n\n-- 邮件状态表\nCREATE TABLE email_status (\n    id INTEGER PRIMARY KEY AUTOINCREMENT,   -- 主键ID\n    email_id INTEGER NOT NULL,              -- 关联的邮件ID\n    user_id TEXT NOT NULL,               -- 关联的用户ID\n    is_read INTEGER DEFAULT 0,              -- 是否已读\n    is_starred INTEGER DEFAULT 0,           -- 是否标星\n    is_archived INTEGER DEFAULT 0,          -- 是否归档\n    notes TEXT,                             -- 用户备注\n    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,  -- 创建时间\n    updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,  -- 更新时间\n    UNIQUE (email_id, user_id)\n);\nCREATE INDEX idx_email_status_user_id ON email_status(user_id);\n\n-- 邮件标签表\nCREATE TABLE email_tags (\n    id INTEGER PRIMARY KEY AUTOINCREMENT,   -- 主键ID\n    name TEXT NOT NULL,                     -- 标签名称\n    user_id TEXT NOT NULL,               -- 关联的用户ID\n    color TEXT,                             -- 标签颜色\n    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,  -- 创建时间\n    UNIQUE (name, user_id)\n);\n\n-- 邮件-标签关联表\nCREATE TABLE email_tag_relations (\n    id INTEGER PRIMARY KEY AUTOINCREMENT,   -- 主键ID\n    email_id INTEGER NOT NULL,              -- 关联的邮件ID\n    tag_id INTEGER NOT NULL,                -- 关联的标签ID\n    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,  -- 创建时间\n    UNIQUE (email_id, tag_id)\n);\nCREATE INDEX idx_email_tag_relations_tag_id ON email_tag_relations(tag_id);"
  },
  {
    "path": "app/env.d.ts",
    "content": "/// <reference types=\"astro/client\" />\n\ntype KVNamespace = import('@cloudflare/workers-types').KVNamespace;\ntype D1Database = import('@cloudflare/workers-types').D1Database;\ntype ENV = {\n    POST_DB: KVNamespace;\n    MAIL_DB: D1Database;\n    DKIM_PRIVATE_KEY: string;\n    PUBLIC_TURNSTILE_SITE_KEY: string;\n    SECRET_KEY: string;\n    PUBLIC_CLERK_PUBLISHABLE_KEY: string;\n    CLERK_SECRET_KEY: string;\n    PUBLIC_CLERK_SIGN_IN_URL: string;\n    PUBLIC_CLERK_SIGN_UP_URL: string;\n};\n\n// use a default runtime configuration (advanced mode).\ntype Runtime = import('@astrojs/cloudflare').Runtime<ENV>;\ndeclare namespace App {\n    interface Locals extends Runtime {}\n}\n\n\ninterface ImportMetaEnv {\n    readonly SECRET_KEY: string;\n    readonly PUBLIC_TURNSTILE_SITE_KEY: string;\n    readonly PUBLIC_CLERK_PUBLISHABLE_KEY: string;\n    readonly CLERK_SECRET_KEY: string;\n    readonly PUBLIC_CLERK_SIGN_IN_URL: string;\n    readonly PUBLIC_CLERK_SIGN_UP_URL: string;\n    // 更多环境变量…\n}\n\ninterface ImportMeta {\n    readonly env: ImportMetaEnv;\n}"
  },
  {
    "path": "app/package.json",
    "content": "{\n  \"name\": \"app\",\n  \"type\": \"module\",\n  \"version\": \"0.0.1\",\n  \"scripts\": {\n    \"dev\": \"wrangler types && astro dev\",\n    \"build\": \"astro build\",\n    \"preview\": \"astro preview\",\n    \"astro\": \"astro\"\n  },\n  \"dependencies\": {\n    \"@astrojs/cloudflare\": \"^12.1.0\",\n    \"@astrojs/node\": \"^9.0.0\",\n    \"@astrojs/react\": \"^4.1.2\",\n    \"@astrojs/tailwind\": \"^5.1.4\",\n    \"@clerk/astro\": \"^2.1.4\",\n    \"@marsidev/react-turnstile\": \"^1.1.0\",\n    \"@radix-ui/react-alert-dialog\": \"^1.1.4\",\n    \"@radix-ui/react-avatar\": \"^1.1.2\",\n    \"@radix-ui/react-dialog\": \"^1.1.4\",\n    \"@radix-ui/react-dropdown-menu\": \"^2.1.4\",\n    \"@radix-ui/react-scroll-area\": \"^1.2.2\",\n    \"@radix-ui/react-select\": \"^2.1.4\",\n    \"@radix-ui/react-separator\": \"^1.1.1\",\n    \"@radix-ui/react-slot\": \"^1.1.1\",\n    \"@radix-ui/react-tooltip\": \"^1.1.6\",\n    \"@types/react\": \"^19.0.2\",\n    \"@types/react-dom\": \"^19.0.2\",\n    \"astro\": \"^5.1.2\",\n    \"astro-seo\": \"^0.8.4\",\n    \"class-variance-authority\": \"^0.7.1\",\n    \"clsx\": \"^2.1.1\",\n    \"dayjs\": \"^1.11.13\",\n    \"jotai\": \"^2.11.0\",\n    \"lucide-react\": \"^0.469.0\",\n    \"next-themes\": \"^0.4.4\",\n    \"random-words\": \"^2.0.1\",\n    \"react\": \"^19.0.0\",\n    \"react-dom\": \"^19.0.0\",\n    \"react-resizable-panels\": \"^2.1.7\",\n    \"sonner\": \"^1.7.1\",\n    \"tailwind-merge\": \"^2.6.0\",\n    \"tailwindcss\": \"^3.4.17\",\n    \"tailwindcss-animate\": \"^1.0.7\"\n  },\n  \"devDependencies\": {\n    \"@cloudflare/workers-types\": \"^4.20241230.0\",\n    \"wrangler\": \"^3.101.0\"\n  }\n}"
  },
  {
    "path": "app/public/robots.txt",
    "content": "User-agent: *\nAllow: /"
  },
  {
    "path": "app/src/components/Bar.astro",
    "content": "---\nimport { SignedIn, UserButton } from '@clerk/astro/components';\nimport { ModeToggle } from '@/components/ModeToggle';\n---\n\n<div class=\"py-2 px-4 shrink-0 bg-gray-700 dark:bg-gray-800 flex gap-2 items-center\" style=`background-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32' width='32' height='32' fill='none' stroke='rgb(148 163 184 / 0.05)'%3e%3cpath d='M0 .5H31.5V32'/%3e%3c/svg%3e\")`>\n    <a href=\"/\" title=\"FakeMail\"><img class='h-7' src='/favicon.svg' alt='FakeMail' title=\"FakeMail\" /></a>\n    <h1 class='font-extrabold text-center text-md text-green-500'>\n        FakeMail\n    </h1>\n    <p class=\"text-xs text-gray-400\">- Data is stored encrypted on cloudflare D1</p>\n    <div class=\"flex-1\"></div>\n    <ModeToggle client:load />\n    <SignedIn>\n        <UserButton />\n    </SignedIn>\n</div>"
  },
  {
    "path": "app/src/components/Container.tsx",
    "content": "import { useState } from 'react';\nimport { Turnstile } from '@marsidev/react-turnstile';\nimport Lists from '@/components/Lists';\n\ntype Status = 'error' | 'expired' | 'solved';\n\nconst key = import.meta.env.PUBLIC_TURNSTILE_SITE_KEY || '1x00000000000000000000AA';\n\nexport default ({ siteKey }: {siteKey?: string}) => {\n    const [status, setStatus] = useState<Status | null>(null)\n    if (status === 'solved' || import.meta.env.DEV) return <Lists />\n    return <Turnstile\n        className='mx-auto text-center py-6'\n        siteKey={siteKey || key}\n        onError={() => setStatus('error')}\n        onExpire={() => setStatus('expired')}\n        onSuccess={() => {\n            setTimeout(() => {\n                setStatus('solved');\n            }, 1000)\n        }}\n    />\n};"
  },
  {
    "path": "app/src/components/Footer.astro",
    "content": "---\nimport shoteasy from '@/assets/shoteasy.svg'\nimport fakeact from '@/assets/fakeact.svg'\n---\n\n<div class=\"container mx-auto p-4\">\n    <h3 class=\"text-center font-semibold p-4 my-2\">More tools</h3>\n    <div class=\"flex flex-col gap-4 justify-center\">\n        <div class=\"text-center\">\n            <a href=\"https://shoteasy.fun/\" target=\"_blank\" title=\"ShotEasy - Screenshot and image tools\" class=\"inline-block text-xs p-2 rounded-md hover:bg-gray-50 dark:hover:bg-gray-900\">\n                <img src={shoteasy.src} class=\"mx-auto mb-2 w-8\" alt=\"ShotEasy\" />\n                <p class=\"font-medium\">ShotEasy</p>\n                <p class=\"text-gray-500 dark:text-gray-400 mt-0.5\">Screenshot & image tools</p>\n            </a>\n        </div>\n        <div class=\"text-center\">\n            <a href=\"https://fakeact.fun/\" target=\"_blank\" title=\"Fakeact - Fake identity generator\" class=\"inline-block text-xs p-2 rounded-md hover:bg-gray-50 dark:hover:bg-gray-900\">\n                <img src={fakeact.src} class=\"mx-auto mb-2 w-8\" alt=\"Fakeact\" />\n                <p class=\"font-medium\">Fakeact</p>\n                <p class=\"text-gray-500 dark:text-gray-400 mt-0.5\">Fake identity generator</p>\n            </a>\n        </div>\n    </div>\n</div>\n<footer>\n    <div class=\"container mx-auto p-4 pt-10\">\n        <div class='text-center text-xs text-gray-400 border-t pt-4'>\n            Copyright © 2025 FakeMail\n        </div>\n    </div>\n</footer>"
  },
  {
    "path": "app/src/components/Generator.tsx",
    "content": "import { useState } from 'react';\nimport { Copy, Check, RotateCw } from 'lucide-react';\nimport { Button } from '@/components/ui/button';\nimport { Input } from '@/components/ui/input';\nimport {\n    Tooltip,\n    TooltipContent,\n    TooltipProvider,\n    TooltipTrigger,\n} from '@/components/ui/tooltip';\nimport {\n    AlertDialog,\n    AlertDialogAction,\n    AlertDialogCancel,\n    AlertDialogContent,\n    AlertDialogDescription,\n    AlertDialogFooter,\n    AlertDialogHeader,\n    AlertDialogTitle,\n} from '@/components/ui/alert-dialog';\nimport { useCopyToClipboard } from '@/hooks/useCopyToClipboard';\nimport { useGeneratorMail } from '@/hooks/useGeneratorMail';\n\nexport default () => {\n    const [address, generatorNewMail] = useGeneratorMail();\n    const [isCopy, setIsCopy] = useState<boolean>(false);\n    const [copiedText, copy] = useCopyToClipboard();\n    const [open, setOpen] = useState(false);\n    const handleFocus = (event: any) => event.target.select();\n    const handleCopy = (text: string) => {\n        if (isCopy) return;\n        copy(text)\n            .then(() => {\n                setIsCopy(true);\n                setTimeout(() => {\n                    setIsCopy(false);\n                }, 1500);\n            })\n            .catch((error) => {\n                console.error('Failed to copy!', error);\n            });\n    };\n    return (\n        <TooltipProvider delayDuration={0}>\n            <div className='flex flex-col items-center gap-3 sm:flex-row'>\n                <Tooltip>\n                    <TooltipTrigger asChild>\n                        <Input\n                            className='sm:flex-1 rounded-full border-none text-center sm:text-left bg-slate-800/40 dark:bg-slate-600/40 px-8 py-6 text-sm md:text-xl text-white'\n                            readOnly\n                            onFocus={handleFocus}\n                            value={address}\n                        />\n                    </TooltipTrigger>\n                    <TooltipContent>\n                        <p>Your fake email address</p>\n                    </TooltipContent>\n                </Tooltip>\n            <div className='flex gap-3 items-center'>\n                    <Tooltip>\n                        <TooltipTrigger asChild>\n                            <Button\n                                variant='outline'\n                                className='rounded-full py-6 px-8 shrink-0 sm:px-4'\n                                onClick={() => handleCopy(address)}\n                            >\n                                {isCopy ? <Check /> : <Copy />}\n                            </Button>\n                        </TooltipTrigger>\n                        <TooltipContent>\n                            <p>Copy to clipboard</p>\n                        </TooltipContent>\n                    </Tooltip>\n                    <Tooltip>\n                        <TooltipTrigger asChild>\n                            <Button\n                                variant='destructive'\n                                className='rounded-full py-6 shrink-0'\n                                onClick={() => setOpen(true)}\n                            >\n                                <RotateCw /> Change\n                            </Button>\n                        </TooltipTrigger>\n                        <TooltipContent>\n                            <p>Generate a new email address</p>\n                        </TooltipContent>\n                    </Tooltip>\n                    <AlertDialog open={open} onOpenChange={setOpen}>\n                        <AlertDialogContent>\n                            <AlertDialogHeader>\n                                <AlertDialogTitle>\n                                    Generate a new address?\n                                </AlertDialogTitle>\n                                <AlertDialogDescription>\n                                    This will discard the current temporary address and create a new one. You will no longer receive mail at the old address.\n                                </AlertDialogDescription>\n                            </AlertDialogHeader>\n                            <AlertDialogFooter>\n                                <AlertDialogCancel>Cancel</AlertDialogCancel>\n                                <AlertDialogAction asChild>\n                                    <Button onClick={generatorNewMail}>Yes, generate new address</Button>\n                                </AlertDialogAction>\n                            </AlertDialogFooter>\n                        </AlertDialogContent>\n                    </AlertDialog>\n                </div>\n            </div>\n        </TooltipProvider>\n    );\n};\n"
  },
  {
    "path": "app/src/components/Header.astro",
    "content": "---\nimport { LayoutDashboard } from 'lucide-react';\nimport Generator from '@/components/Generator';\nimport { ModeToggle } from '@/components/ModeToggle';\n---\n\n<header class=\"bg-gray-700 dark:bg-gray-900\">\n    <div class=\"p-4 pt-0\" style=`background-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32' width='32' height='32' fill='none' stroke='rgb(148 163 184 / 0.05)'%3e%3cpath d='M0 .5H31.5V32'/%3e%3c/svg%3e\")`>\n        <div class=\"flex justify-end items-center gap-1 pb-2 pt-3\">\n            <a\n                href=\"/dashboard\"\n                title=\"Dashboard\"\n                class=\"text-xs flex gap-1 h-9 items-center text-white bg-gray-900/50 rounded-full px-4 py-1 hover:bg-white/10\"\n            ><LayoutDashboard size={16} />Dashboard</a>\n            <ModeToggle client:load />\n            <a\n                href='https://github.com/CH563/fakemail'\n                target='_blank'\n                class='inline-flex h-9 w-9 rounded-full items-center justify-center hover:bg-gray-900/40'\n            >\n                <span\n                    class='block w-5 h-5 align-middle bg-center'\n                    style=`background-image: url(\"data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9IiNmZmZmZmYiIHN0cm9rZS13aWR0aD0iMiIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIiBjbGFzcz0ibHVjaWRlIGx1Y2lkZS1naXRodWIiPjxwYXRoIGQ9Ik0xNSAyMnYtNGE0LjggNC44IDAgMCAwLTEtMy41YzMgMCA2LTIgNi01LjUuMDgtMS4yNS0uMjctMi40OC0xLTMuNS4yOC0xLjE1LjI4LTIuMzUgMC0zLjUgMCAwLTEgMC0zIDEuNS0yLjY0LS41LTUuMzYtLjUtOCAwQzYgMiA1IDIgNSAyYy0uMyAxLjE1LS4zIDIuMzUgMCAzLjVBNS40MDMgNS40MDMgMCAwIDAgNCA5YzAgMy41IDMgNS41IDYgNS41LS4zOS40OS0uNjggMS4wNS0uODUgMS42NS0uMTcuNi0uMjIgMS4yMy0uMTUgMS44NXY0Ii8+PHBhdGggZD0iTTkgMThjLTQuNTEgMi01LTItNy0yIi8+PC9zdmc+\");background-size:100%`\n                ></span>\n            </a>\n        </div>\n        <div class='container mx-auto max-w-3xl'>\n            <div class='pt-4'>\n                <img class='mx-auto w-10' src='/favicon.svg' alt='Fakeact' title=\"Fakeact\" />\n            </div>\n            <h1 class='font-extrabold text-center text-xl pb-6 text-green-500'>\n                FakeMail\n            </h1>\n            <h2 class='text-gray-100 font-semibold text-3xl text-center mb-4'>\n                Fake email generator,<br /> delete emails 2 hours after receiving\n            </h2>\n            <p class='text-gray-400 text-sm text-center mb-4'>\n                Avoid spam, promo mail, and bots. Keep your real inbox clean and secure. FakeMail gives you temporary, anonymous, free disposable email addresses—no sign-up required to try.\n            </p>\n            <h2 class='text-gray-100 font-semibold text-xl text-center mb-2'>Your temporary email address</h2>\n            <div class=\"border border-dashed bg-gray-500/10 border-gray-500 rounded-xl p-4 mb-4 min-h-[84px]\">\n                <Generator client:load />\n            </div>\n        </div>\n    </div>\n</header>\n"
  },
  {
    "path": "app/src/components/Lists.tsx",
    "content": "import { useState, useEffect, useRef, memo } from 'react';\nimport { Inbox, Mails, RefreshCw, Loader2 } from 'lucide-react';\nimport dayjs from 'dayjs';\nimport utc from 'dayjs/plugin/utc';\nimport { MailCard } from '@/components/ui/mail-card';\nimport { Button } from '@/components/ui/button';\nimport { Toaster } from \"@/components/ui/sonner\";\nimport { toast } from \"sonner\"\n\ndayjs.extend(utc);\n\nconst countDownTime = 30;\n\nconst CountdownNumber = memo(({ value }: { value: number }) => (<span className='text-green-600 mx-1'>{value}</span>));\nconst CountDownComp = (({ value }: { value: number }) => {\n    const [countDown, setCountDown] = useState<number>(countDownTime);\n    useEffect(() => {\n        setCountDown(value);\n        const intervalId = setInterval(() => {\n            setCountDown((prevCount) => prevCount - 1);\n        }, 1000);\n        return () => {\n            clearInterval(intervalId);\n        };\n    }, [value]);\n    return (\n        <CountdownNumber value={countDown} />\n    );\n});\nconst MailCardComp = memo(({ mails, toDelete }: { mails: any[], toDelete: (mail: any) => void}) => mails.map((mail: any, index: number) => {\n    if (mail) {\n        return (\n            <MailCard\n                key={mail.suffix}\n                sender={mail.sender}\n                subject={mail.subject}\n                name={mail.name}\n                date={dayjs.utc(mail.date).local().format('YYYY-MM-DD HH:mm:ss')}\n                content={mail['content-plain-formatted'] || mail['content-plain'] || mail['content-html']}\n                defaultShow={index === 0}\n                toDelete={() => toDelete(mail)}\n            />\n        )\n    }\n}));\n\nexport default () => {\n    const [mails, setMails] = useState([]);\n    const [stats, setStats] = useState<{count: number}>({count: 0});\n    const [loading, setLoading] = useState<boolean>(false);\n    const intervalId = useRef<any>(null);\n    const countDownId = useRef<any>(null);\n    const intervalStop = useRef<number>(0);\n    const countDown = useRef<number>(countDownTime);\n    const fetchData = async () => {\n        clearInterval(countDownId.current);\n        if (intervalStop.current > 15) clearInterval(intervalId.current);\n        try {\n            const address = localStorage.getItem('receivingEmail');\n            if (!address) return;\n            setLoading(true);\n            const response = await fetch(`/api/get?address=${address}`); // Replace with your API endpoint\n            if (!response.ok) {\n                throw new Error('Network response was not ok.');\n            }\n            countDown.current = countDownTime;\n            countDownId.current = setInterval(() => {\n                countDown.current = countDown.current - 1;\n            }, 1000);\n            intervalStop.current = intervalStop.current++;\n            const data = await response.json();\n            if (data.mails && data.mails.length) {\n                const arr = data.mails.filter((e: any) => e);\n                arr.sort((a: any, b: any) => dayjs(b.date).unix() - dayjs(a.date).unix());\n                setMails(arr);\n                if (arr.length) clearInterval(intervalId.current);\n            } else {\n                setMails([]);\n            }\n            if (Number(data.stats.count)) setStats({count: Number(data.stats.count)});\n            setLoading(false);\n        } catch (error) {\n            setLoading(false);\n        }\n    };\n    const refresh = () => {\n        clearInterval(intervalId.current);\n        intervalStop.current = 0;\n        fetchData();\n        intervalId.current = setInterval(fetchData, countDownTime * 1000);\n    }\n    const toDelete = async (mail: any) => {\n        const response = await fetch('/api/delete', {\n            method: 'POST',\n            body: JSON.stringify({\n                key: `${mail.recipient}-${mail.suffix}`\n            })\n        }); // Replace with your API endpoint\n        if (!response.ok) {\n            throw new Error('Network response was not ok.');\n        }\n        setMails(mails.filter((e: any) => e.suffix !== mail.suffix));\n        toast(\"The mail has been deleted!\");\n    }\n\n    useEffect(() => {\n        fetchData();\n        intervalId.current = setInterval(fetchData, countDownTime * 1000);\n        return () => {\n            clearInterval(intervalId.current);\n        };\n    }, []);\n    const emptyDom = (<div className='border border-dashed border-gray-400 rounded-xl p-4'>\n        <div className='text-center text-gray-400'>\n            <Inbox className='mx-auto' />\n            <p>No email received yet</p>\n        </div>\n    </div>);\n    return (\n        <>\n            <div className='flex justify-between items-center py-2'>\n                <h2 className=\"text-center font-semibold flex gap-1 items-center\"><Mails size={18} />Mail Inbox{mails.length ? `(${mails.length})` : ''}</h2>\n                <div className='flex gap-2 items-center'>\n                {(intervalStop.current < 15 && !mails.length && !loading) && <div className='flex items-center text-xs text-gray-500'><div className=\"animate-ping w-1 h-1 rounded-full bg-green-600 mr-2\" /> Refresh after <CountDownComp value={countDown.current} /> s</div>}\n                    <Button size='xs' variant='outline' onClick={refresh} disabled={loading}>\n                        {loading ? <Loader2 className=\"animate-spin\" /> : <RefreshCw />}\n                    </Button>\n                </div>\n            </div>\n            {!mails.length ? emptyDom : <MailCardComp mails={mails} toDelete={toDelete} />}\n            <div className='py-4 text-xs text-gray-400'>\n                -- We've received<span className='mx-1 text-lg text-green-600/70 italic font-semibold'>{stats.count.toLocaleString()}</span>emails so far.\n            </div>\n            <Toaster />\n        </>\n    );\n};\n"
  },
  {
    "path": "app/src/components/Main.astro",
    "content": "---\nimport Container from '@/components/Container';\nconst env = import.meta.env.PROD ? Astro.locals.runtime.env : import.meta.env;\n---\n\n<main>\n    <div class=\"container mx-auto max-w-3xl px-2 lg:px-0\">\n        <Container client:load siteKey={env.PUBLIC_TURNSTILE_SITE_KEY} />\n        <div class=\"px-2 md:px-0 [&_p]:text-sm [&_p]:text-gray-600 dark:[&_p]:text-gray-300 [&_p]:text-justify [&_p]:break-keep [&_p+p]:mt-3 [&_h2]:p-4 [&_h2]:mt-8 [&_h2]:text-center [&_h2]:font-semibold\">\n            <h2>What is the FakeMail?</h2>\n            <p class=\"text-sm text-gray-600\"><strong>FakeMail (Temporary email)</strong> — a free email service that lets you receive mail at a temporary address that self-destructs after a set time. It is also known as tempmail, 10minutemail, throwaway email, fake email generator, burner mail or trash mail. Many forums, Wi‑Fi networks, and sites ask for registration before you can view content, post, or download. FakeMail is a throwaway email service that helps you avoid spam and stay safe.</p>\n            <h2>Why would you need a fake email address?</h2>\n            <p>Services like Amazon Prime, Hulu, and Netflix offer limited-time trials; a disposable email address is one way to sign up. Some people use a different temporary address after a trial ends. Use responsibly and in line with each service’s terms.</p>\n            <p>Offline or online retailers often require an email address to access offers, which can lead to a flood of spam. A temporary email address makes it easy to avoid those unwanted messages.</p>\n            <p>While temporary email is sometimes associated with less legitimate use, there are many good reasons to use a disposable email service: trials, one-off signups, and keeping your real inbox clean.</p>\n            <h2>Cloudflare temporary email</h2>\n            <p class=\"\">This temporary email service runs on Cloudflare Workers. You get a temporary address and can read received mail in your browser.</p>\n        </div>\n    </div>\n</main>"
  },
  {
    "path": "app/src/components/ModeToggle.tsx",
    "content": "import * as React from 'react';\nimport { Moon, Sun } from 'lucide-react';\n\nimport { Button } from '@/components/ui/button';\n\nexport function ModeToggle() {\n    const [theme, setThemeState] = React.useState<\n        'theme-light' | 'dark' | 'system'\n    >('theme-light');\n\n    React.useEffect(() => {\n        const isDarkMode = document.documentElement.classList.contains('dark');\n        setThemeState(isDarkMode ? 'dark' : 'theme-light');\n    }, []);\n\n    React.useEffect(() => {\n        const isDark =\n            theme === 'dark' ||\n            (theme === 'system' &&\n                window.matchMedia('(prefers-color-scheme: dark)').matches);\n        document.documentElement.classList[isDark ? 'add' : 'remove']('dark');\n    }, [theme]);\n\n    return (\n        <Button\n            variant=\"ghost\"\n            size='icon'\n            className='rounded-full text-white hover:text-white hover:bg-slate-800/50 hover:dark:bg-slate-800/50'\n            onClick={() => setThemeState(theme === 'dark' ? 'theme-light' : 'dark')}\n        >\n            <Sun className='h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0' />\n            <Moon className='absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100' />\n            <span className='sr-only'>Toggle theme</span>\n        </Button>\n    );\n}\n"
  },
  {
    "path": "app/src/components/mail/AccountSwitcher.tsx",
    "content": "import { useState } from 'react';\nimport { cn } from '@/lib/utils';\nimport {\n    Select,\n    SelectContent,\n    SelectItem,\n    SelectTrigger,\n    SelectValue,\n} from '@/components/ui/select';\n\ninterface AccountSwitcherProps {\n    isCollapsed: boolean;\n    accounts: {\n        id: number;\n        email_address: string;\n    }[];\n    onAccountChange: (email: string) => void;\n}\n\nexport function AccountSwitcher({\n    isCollapsed,\n    accounts,\n    onAccountChange\n}: AccountSwitcherProps) {\n    const [selectedAccount, setSelectedAccount] = useState<string>(\n        accounts[0].email_address\n    );\n    const handleChanged = (email: string) => {\n        setSelectedAccount(email);\n        onAccountChange(email);\n    }\n    return (\n        <Select\n            defaultValue={selectedAccount}\n            onValueChange={handleChanged}\n        >\n            <SelectTrigger\n                className={cn(\n                    'flex items-center gap-2 [&>span]:line-clamp-1 [&>span]:flex [&>span]:w-full [&>span]:items-center [&>span]:gap-1 [&>span]:truncate [&_svg]:h-4 [&_svg]:w-4 [&_svg]:shrink-0',\n                    isCollapsed &&\n                        'flex h-9 w-9 shrink-0 items-center justify-center p-0 [&>span]:w-auto [&>svg]:hidden'\n                )}\n                aria-label='Select account'\n            >\n                <SelectValue placeholder='Select an account'>\n                    <div className='w-5 h-5 rounded-full shrink-0 overflow-hidden text-center leading-5 bg-slate-700 text-white text-xs uppercase'>{selectedAccount.substring(0, 1)}</div>\n                    <span className={cn('', isCollapsed && 'hidden')}>\n                        {selectedAccount}\n                    </span>\n                </SelectValue>\n            </SelectTrigger>\n            <SelectContent>\n                {accounts.map((account) => (\n                    <SelectItem key={account.email_address} value={account.email_address}>\n                        <div className='flex items-center gap-1.5 [&_svg]:h-4 [&_svg]:w-4 [&_svg]:shrink-0 [&_svg]:text-foreground'>\n                        <div className='w-5 h-5 rounded-full shrink-0 overflow-hidden text-center leading-5 bg-slate-700 text-white text-xs uppercase'>{account.email_address.substring(0, 1)}</div>\n                            {account.email_address}\n                        </div>\n                    </SelectItem>\n                ))}\n            </SelectContent>\n        </Select>\n    );\n}\n"
  },
  {
    "path": "app/src/components/mail/Accounts.tsx",
    "content": "import { useRef, useState } from 'react';\nimport { Button } from '@/components/ui/button';\nimport {\n    Dialog,\n    DialogContent,\n    DialogDescription,\n    DialogFooter,\n    DialogHeader,\n    DialogTitle,\n    DialogTrigger,\n} from \"@/components/ui/dialog\";\nimport { Badge } from '@/components/ui/badge';\nimport { Textarea } from '@/components/ui/textarea';\nimport { Trash2, CirclePlus, FilePenLine, Loader2 } from \"lucide-react\";\nimport { Toaster } from \"@/components/ui/sonner\";\nimport { toast } from \"sonner\";\nimport type { AccountsList } from './data';\nimport dayjs from 'dayjs';\nimport utc from 'dayjs/plugin/utc';\n\ndayjs.extend(utc);\n\ntype AccountsProps = {\n    list: AccountsList[]\n}\n\nexport const Accounts = ({ list = [] }: AccountsProps) => {\n    const remarkRef = useRef<HTMLTextAreaElement>(null);\n    const remarkEditRef = useRef<HTMLTextAreaElement>(null);\n    const [showDialog, setShowDialog] = useState(false);\n    const [editAccount, setEditAccount] = useState<AccountsList | null>(null);\n    const [loading, setLoading] = useState(false);\n    const toGenerate = async () => {\n        const response = await fetch('/api/generate', {\n            method: 'POST',\n            headers: {\n                'Content-Type': 'application/json',\n            },\n            body: JSON.stringify({ remark: remarkRef.current?.value }),\n        });\n        if (response.ok) {\n            const data = await response.json();\n            if (data.code === 200) {\n                return window.location.reload();\n            }\n            toast.error(data.msg);\n        } else {\n            toast.error(response.statusText);\n        }\n    }\n    const toSave = async () => {\n        setLoading(true);\n        const remark = (remarkEditRef.current?.value || '').trim();\n        const response = await fetch('/api/remarkMail', {\n            method: 'POST',\n            headers: {\n                'Content-Type': 'application/json',\n            },\n            body: JSON.stringify({\n                id: editAccount?.id,\n                remark\n            }),\n        });\n        if (response.ok) {\n            const data = await response.json();\n            if (data.code === 200) {\n                list.forEach((account) => {\n                    if (account.id === editAccount?.id) {\n                        account.alias = remark;\n                    }\n                });\n                setShowDialog(false);\n            }\n            toast.error(data.msg);\n        } else {\n            toast.error(response.statusText);\n        }\n        setLoading(false);\n    }\n    const toEdit = (account: AccountsList) => {\n        setEditAccount(account);\n        setShowDialog(true);\n    }\n    const onDialogChange = (e: boolean) => {\n        setShowDialog(e);\n        if (!e) {\n            setEditAccount(null);\n        }\n    }\n    return (\n        <div className='grid gap-1'>\n            {list.map((account) => (\n                <div className='grid border-b grid-cols-12 px-4 py-2 items-start gap-2 hover:bg-gray-500/10 cursor-pointer text-sm' key={account.id}>\n                    <div className='col-span-4 leading-4 py-2 flex items-center gap-1'>\n                    <div className='w-5 h-5 rounded-full shrink-0 overflow-hidden text-center leading-5 bg-slate-700 text-white text-xs uppercase'>{account.email_address.substring(0, 1)}</div>\n                        <div className='flex-1 w-0 truncate'>{account.email_address}</div>\n                    </div>\n                    <div className='col-span-2 leading-4 py-2 text-gray-600 dark:text-gray-400 text-xs'>{dayjs.utc(account.created_at).local().format('YYYY-MM-DD HH:mm:ss')}</div>\n                    <div className='col-span-5 leading-4 py-2 whitespace-pre-wrap'>{account.alias}</div>\n                    <div className='col-span-1'>\n                        <Button variant='ghost' size='icon' onClick={() => toEdit(account)}><FilePenLine className='h-4 w-4' /></Button>\n                        <Button variant='ghost' size='icon' onClick={() => toast('Developing, please wait~')}><Trash2 className='h-4 w-4' color='red' /></Button>\n                    </div>\n                </div>\n            ))}\n            <div className='p-4 text-center'>\n                <Dialog>\n                    <DialogTrigger asChild>\n                        <Button variant='default' size='sm' disabled={list.length > 4}><CirclePlus /> Add a email address</Button>\n                    </DialogTrigger>\n                    <DialogContent className=\"sm:max-w-[425px]\">\n                        <DialogHeader>\n                        <DialogTitle>Add a email address</DialogTitle>\n                        <DialogDescription asChild>\n                            <div className='text-sm'>Generate a random email address <Badge>@fakeact.fun</Badge></div>\n                        </DialogDescription>\n                        </DialogHeader>\n                        <div className=\"grid gap-4 py-4\">\n                            <Textarea ref={remarkRef} rows={6} placeholder=\"Type the remark here.(Optional)\" maxLength={120} />\n                        </div>\n                        <DialogFooter>\n                            <Button type=\"submit\" onClick={toGenerate}>Yes, add it</Button>\n                        </DialogFooter>\n                    </DialogContent>\n                </Dialog>\n            </div>\n            <Toaster />\n            <Dialog open={showDialog} onOpenChange={onDialogChange}>\n                <DialogContent className=\"sm:max-w-[425px]\">\n                    <DialogHeader>\n                        <DialogTitle>Edit</DialogTitle>\n                        <DialogDescription asChild>\n                            <div className='text-sm'><Badge>{editAccount?.email_address}</Badge></div>\n                        </DialogDescription>\n                    </DialogHeader>\n                    <div className=\"grid gap-4\">\n                        <Textarea id='remarkEdit' ref={remarkEditRef} defaultValue={editAccount?.alias || ''} rows={6} placeholder=\"Type the remark here.(Optional)\" maxLength={120} />\n                    </div>\n                    <DialogFooter className='gap-2'>\n                        <Button disabled={loading} variant=\"secondary\" onClick={() => setShowDialog(false)}>Cancel</Button>\n                        <Button disabled={loading} onClick={toSave}>{loading ? <Loader2 className=\"animate-spin\" /> : ''}Save it</Button>\n                    </DialogFooter>\n                </DialogContent>\n            </Dialog>\n        </div>\n    );\n}"
  },
  {
    "path": "app/src/components/mail/Mail.tsx",
    "content": "import { useRef, useState } from 'react';\nimport {\n    Archive,\n    File,\n    Inbox,\n    Search,\n    Send,\n    Copy,\n    Check,\n    Settings,\n    RefreshCw\n} from \"lucide-react\";\nimport {\n    TooltipProvider,\n    Tooltip,\n    TooltipContent,\n    TooltipTrigger\n} from '@/components/ui/tooltip';\nimport {\n    ResizableHandle,\n    ResizablePanel,\n    ResizablePanelGroup,\n} from \"@/components/ui/resizable\";\nimport { Skeleton } from \"@/components/ui/skeleton\";\nimport { Separator } from \"@/components/ui/separator\";\nimport { Input } from \"@/components/ui/input\";\nimport { Toaster } from \"@/components/ui/sonner\";\nimport { toast } from \"sonner\";\nimport { cn } from '@/lib/utils';\nimport { useCopyToClipboard } from '@/hooks/useCopyToClipboard';\nimport { AccountSwitcher } from './AccountSwitcher';\nimport { Nav } from './Nav';\nimport { MailList } from './MailList';\nimport { MailDisplay } from './MailDisplay';\nimport { useMail } from './useMail';\nimport { Button, buttonVariants } from '../ui/button';\nimport type { MailsList } from './data';\n\ninterface Account {\n    id: number\n    email_address: string\n}\n\n\ninterface MailProps {\n    accounts: Account[]\n    mails: MailsList[]\n    defaultLayout: number[] | undefined\n    defaultCollapsed?: boolean\n    navCollapsedSize: number\n}\n\nexport default ({\n    accounts,\n    mails,\n    defaultLayout = [20, 32, 48],\n    defaultCollapsed = false,\n    navCollapsedSize,\n}: MailProps) => {\n    const abortController = useRef<AbortController | null>(null);\n    const [isCollapsed, setIsCollapsed] = useState(defaultCollapsed);\n    const [loading, setLoading] = useState<boolean>(false);\n    const [mailsList, setMailsList] = useState<MailsList[]>(mails);\n    const [isCopy, setIsCopy] = useState<boolean>(false);\n    const [currentAccount, setCurrentAccount] = useState<Account>(accounts[0]);\n    const [copiedText, copy] = useCopyToClipboard();\n    const [mail] = useMail();\n    const fetchData = async (id?: number) => {\n        if (!id) return;\n        try {\n            setLoading(true);\n            abortController.current?.abort();\n            abortController.current = new AbortController();\n            const response = await fetch(`/api/getMails?id=${id}`, { signal: abortController.current.signal });\n            if (!response.ok) {\n                throw new Error('Network response was not ok.');\n            }\n            const data = await response.json();\n            if (data.code === 502) {\n                // redirect to login page\n                window.location.href = '/sign-in';\n            }\n            setMailsList(data?.mails || []);\n            setLoading(false);\n        } catch (error) {\n            setLoading(false);\n        }\n        abortController.current = null;\n    }\n    const handleAccountChange = (email: string) => {\n        const currentAccount = accounts.find((account) => account.email_address === email) || accounts[0];\n        setCurrentAccount(currentAccount);\n        fetchData(currentAccount?.id);\n    }\n    const updateStatus = (messageId: string, status: number = 0) => {\n        setMailsList(mailsList.map(item => {\n            if (item.message_id === messageId) item.is_read = status;\n            return item;\n        }));\n    }\n    const handleUnread = async (messageId?: string) => {\n        if (!messageId) return true;\n        const response = await fetch('/api/updateStatus', {\n            method: 'POST',\n            headers: {\n                'Content-Type': 'application/json',\n            },\n            body: JSON.stringify({ message_id: messageId, is_read: 0 }),\n        })\n        if (response.ok) {\n            const data = await response.json();\n            if (data.code === 200) {\n                updateStatus(messageId);\n                return true;\n            }\n        }\n        return false;\n    }\n    const toDelete = async (messageId?: string) => {\n        if (!messageId) return true;\n        const response = await fetch('/api/deleteMails', {\n            method: 'POST',\n            headers: {\n                'Content-Type': 'application/json',\n            },\n            body: JSON.stringify({ message_id: messageId }),\n        });\n        if (response.ok) {\n            const data = await response.json();\n            if (data.code === 200) {\n                setMailsList(mailsList.filter(e => e.message_id !== messageId));\n                toast.success('Delete Success!');\n                return true;\n            }\n            toast.error(data.message);\n        }\n        return false;\n    }\n    const toCopy = () => {\n        if (isCopy) return;\n        copy(currentAccount.email_address)\n            .then(() => {\n                setIsCopy(true);\n                toast.success('Copied!');\n                setTimeout(() => {\n                    setIsCopy(false);\n                }, 1500);\n            })\n            .catch((error) => {\n                console.error('Failed to copy!', error);\n            });\n    }\n    return (\n        <TooltipProvider delayDuration={0}>\n            <ResizablePanelGroup\n                direction=\"horizontal\"\n                onLayout={(sizes: number[]) => {\n                    document.cookie = `react-resizable-panels:layout:mail=${JSON.stringify(\n                        sizes\n                    )}`\n                }}\n                className=\"h-full flex-1 items-stretch\"\n            >\n                <ResizablePanel\n                    defaultSize={defaultLayout[0]}\n                    collapsedSize={navCollapsedSize}\n                    collapsible={true}\n                    minSize={15}\n                    maxSize={20}\n                    onCollapse={() => {\n                        setIsCollapsed(true)\n                        document.cookie = `react-resizable-panels:collapsed=${JSON.stringify(true)}`\n                    }}\n                    onResize={() => {\n                        setIsCollapsed(false)\n                        document.cookie = `react-resizable-panels:collapsed=${JSON.stringify(\n                            false\n                        )}`\n                    }}\n                    className={cn(\"flex flex-col\", isCollapsed && \"min-w-[50px] transition-all duration-300 ease-in-out\")}\n                >\n                    <div className={cn(\"flex h-[52px] items-center justify-center\", isCollapsed ? \"h-[52px]\" : \"px-2\")}>\n                        <AccountSwitcher isCollapsed={isCollapsed} accounts={accounts} onAccountChange={handleAccountChange} />\n                    </div>\n                    <Separator />\n                    <Nav\n                        isCollapsed={isCollapsed}\n                        links={[\n                            {\n                                title: \"Inbox\",\n                                label: mailsList.length ? `${mailsList.length}` : \"\",\n                                icon: Inbox,\n                                variant: \"default\",\n                            },\n                            {\n                                title: \"Drafts\",\n                                label: \"\",\n                                icon: File,\n                                variant: \"ghost\",\n                            },\n                            {\n                                title: \"Sent\",\n                                label: \"\",\n                                icon: Send,\n                                variant: \"ghost\",\n                            },\n                            {\n                                title: \"Archive\",\n                                label: \"\",\n                                icon: Archive,\n                                variant: \"ghost\",\n                            },\n                        ]}\n                    />\n                    <Separator />\n                    <div className='flex-1'></div>\n                    <div className='py-4 px-2'>\n                        <a href='./settings' className={cn('w-full', buttonVariants({ variant: 'outline'}))}>\n                            <Settings />\n                            {isCollapsed ? '' : `My email addresses (${accounts.length})`}\n                        </a>\n                    </div>\n                </ResizablePanel>\n                <ResizableHandle withHandle />\n                <ResizablePanel defaultSize={defaultLayout[1]} minSize={20}>\n                    <div className=\"flex items-center px-4 py-2\">\n                        <h1 className=\"text-xl font-bold\">Inbox</h1>\n                        <Tooltip>\n                            <TooltipTrigger asChild>\n                                <Button\n                                    variant='ghost'\n                                    size='icon'\n                                    className='ml-auto'\n                                    disabled={loading}\n                                    onClick={() => fetchData(currentAccount.id)}\n                                >\n                                    <RefreshCw className={cn('h-4 w-4', loading && 'animate-spin')} />\n                                    <span className='sr-only'>Refresh</span>\n                                </Button>\n                            </TooltipTrigger>\n                            <TooltipContent>Refresh inbox</TooltipContent>\n                        </Tooltip>\n                        <Tooltip>\n                            <TooltipTrigger asChild>\n                                <Button\n                                    variant='ghost'\n                                    size='icon'\n                                    className=''\n                                    disabled={isCopy}\n                                    onClick={toCopy}\n                                >\n                                    {isCopy ? <Check size={16} color='green' /> :\n                                        <>\n                                            <Copy className='h-4 w-4' />\n                                            <span className='sr-only'>Copy</span>\n                                        </>\n                                    }\n                                </Button>\n                            </TooltipTrigger>\n                            <TooltipContent>Copy current email address</TooltipContent>\n                        </Tooltip>\n                    </div>\n                    <Separator />\n                    <div className=\"bg-background/95 p-4 backdrop-blur supports-[backdrop-filter]:bg-background/60\">\n                        <form>\n                            <div className=\"relative\">\n                            <Search className=\"absolute left-2 top-2.5 h-4 w-4 text-muted-foreground\" />\n                            <Input placeholder=\"Search\" className=\"pl-8\" />\n                            </div>\n                        </form>\n                    </div>\n                    {loading ? <div className='px-4'>\n                        <div className='flex flex-col items-start gap-2 rounded-lg border p-3 text-left text-sm transition-all'>\n                            <Skeleton className='w-3/4 h-3' />\n                            <Skeleton className='w-1/4 h-3' />\n                            <Skeleton className='w-3/4 h-2' />\n                        </div>\n                    </div> : <MailList items={mailsList} updateStatus={(messageId) => updateStatus(messageId, 1)} />}\n                </ResizablePanel>\n                <ResizableHandle withHandle />\n                <ResizablePanel defaultSize={defaultLayout[2]} minSize={30}>\n                    <MailDisplay\n                        mail={mailsList.find((item) => item.message_id === mail.selected) || null}\n                        currentAccount={currentAccount.email_address}\n                        toDelete={toDelete}\n                        handleUnread={handleUnread}\n                    />\n                </ResizablePanel>\n            </ResizablePanelGroup>\n            <Toaster />\n        </TooltipProvider>\n    );\n};"
  },
  {
    "path": "app/src/components/mail/MailDisplay.tsx",
    "content": "import {\n    Archive,\n    Forward,\n    MoreVertical,\n    Reply,\n    ReplyAll,\n    Trash2,\n    Loader2\n} from \"lucide-react\";\nimport {\n    DropdownMenuContent,\n    DropdownMenuItem,\n    DropdownMenu,\n    DropdownMenuTrigger,\n} from \"@/components/ui/dropdown-menu\";\nimport {\n    AlertDialog,\n    AlertDialogAction,\n    AlertDialogCancel,\n    AlertDialogContent,\n    AlertDialogDescription,\n    AlertDialogFooter,\n    AlertDialogHeader,\n    AlertDialogTitle,\n} from '@/components/ui/alert-dialog';\nimport { toast } from \"sonner\";\nimport {\n    Avatar,\n    AvatarFallback,\n    AvatarImage,\n} from \"@/components/ui/avatar\";\nimport {\n    Tooltip,\n    TooltipContent,\n    TooltipTrigger,\n} from '@/components/ui/tooltip';\nimport { Button } from '@/components/ui/button';\nimport { Separator } from '@/components/ui/separator';\nimport dayjs from 'dayjs';\nimport utc from 'dayjs/plugin/utc';\n\nimport type { MailsList } from \"./data\";\nimport { useState } from 'react';\n\ndayjs.extend(utc);\n\ninterface MailDisplayProps {\n    mail: MailsList | null\n    currentAccount: string\n    toDelete: (messageId?: string) => Promise<boolean>;\n    handleUnread: (messageId?: string) => Promise<boolean>;\n}\n\nexport function MailDisplay({ mail, currentAccount, toDelete, handleUnread }: MailDisplayProps) {\n    const [openStatus, setOpenStatus] = useState(false);\n    const [loading, setLoading] = useState(false);\n    const handleDelete = async () => {\n        setLoading(true);\n        const isOk = await toDelete(mail?.message_id);\n        setLoading(false);\n        if (isOk) {\n            setOpenStatus(false);\n        }\n    }\n    return (\n        <div className='flex h-full flex-col'>\n            <div className='flex items-center p-2'>\n                <div className='flex items-center gap-2'>\n                    <Tooltip>\n                        <TooltipTrigger asChild>\n                            <Button\n                                variant='ghost'\n                                size='icon'\n                                disabled={!mail}\n                                onClick={() => toast('Archive is developing~')}\n                            >\n                                <Archive className='h-4 w-4' />\n                                <span className='sr-only'>Archive</span>\n                            </Button>\n                        </TooltipTrigger>\n                        <TooltipContent>Archive</TooltipContent>\n                    </Tooltip>\n                    <Tooltip>\n                        <TooltipTrigger asChild>\n                            <Button\n                                variant='ghost'\n                                size='icon'\n                                disabled={!mail}\n                                onClick={() => setOpenStatus(true)}\n                            >\n                                <Trash2 className='h-4 w-4' />\n                                <span className='sr-only'>Delete mail</span>\n                            </Button>\n                        </TooltipTrigger>\n                        <TooltipContent>Delete mail</TooltipContent>\n                    </Tooltip>\n                    <AlertDialog open={openStatus}>\n                        <AlertDialogContent>\n                            <AlertDialogHeader>\n                                <AlertDialogTitle>\n                                    Are you absolutely sure?\n                                </AlertDialogTitle>\n                                <AlertDialogDescription>\n                                    Delete this mail cannot be undone. This will\n                                    permanently remove your data from our servers.\n                                </AlertDialogDescription>\n                            </AlertDialogHeader>\n                            <AlertDialogFooter>\n                                <AlertDialogCancel onClick={() => setOpenStatus(false)}>\n                                    Cancel\n                                </AlertDialogCancel>\n                                <AlertDialogAction asChild>\n                                    <Button variant=\"destructive\" disabled={loading} onClick={handleDelete}>{loading ? <Loader2 className=\"animate-spin\" /> : 'Delete'}</Button>\n                                </AlertDialogAction>\n                            </AlertDialogFooter>\n                        </AlertDialogContent>\n                    </AlertDialog>\n                    <Separator orientation='vertical' className='mx-1 h-6' />\n                </div>\n                <div className='ml-auto flex items-center gap-2'>\n                    {/* <Tooltip>\n                        <TooltipTrigger asChild>\n                            <Button\n                                variant='ghost'\n                                size='icon'\n                                disabled={!mail}\n                            >\n                                <Reply className='h-4 w-4' />\n                                <span className='sr-only'>Reply</span>\n                            </Button>\n                        </TooltipTrigger>\n                        <TooltipContent>Reply</TooltipContent>\n                    </Tooltip>\n                    <Tooltip>\n                        <TooltipTrigger asChild>\n                            <Button\n                                variant='ghost'\n                                size='icon'\n                                disabled={!mail}\n                            >\n                                <ReplyAll className='h-4 w-4' />\n                                <span className='sr-only'>Reply all</span>\n                            </Button>\n                        </TooltipTrigger>\n                        <TooltipContent>Reply all</TooltipContent>\n                    </Tooltip>\n                    <Tooltip>\n                        <TooltipTrigger asChild>\n                            <Button\n                                variant='ghost'\n                                size='icon'\n                                disabled={!mail}\n                            >\n                                <Forward className='h-4 w-4' />\n                                <span className='sr-only'>Forward</span>\n                            </Button>\n                        </TooltipTrigger>\n                        <TooltipContent>Forward</TooltipContent>\n                    </Tooltip> */}\n                </div>\n                <Separator orientation='vertical' className='mx-2 h-6' />\n                <DropdownMenu>\n                    <DropdownMenuTrigger asChild>\n                        <Button variant='ghost' size='icon' disabled={!mail}>\n                            <MoreVertical className='h-4 w-4' />\n                            <span className='sr-only'>More</span>\n                        </Button>\n                    </DropdownMenuTrigger>\n                    <DropdownMenuContent align='end'>\n                        <DropdownMenuItem onSelect={() => handleUnread(mail?.message_id)}>Mark as unread</DropdownMenuItem>\n                        {/* <DropdownMenuItem>Add label</DropdownMenuItem> */}\n                    </DropdownMenuContent>\n                </DropdownMenu>\n            </div>\n            <Separator />\n            {mail ? (\n                <div className='flex flex-1 flex-col'>\n                    <div className='flex items-start p-4'>\n                        <div className='flex items-start gap-4 text-sm'>\n                            <Avatar>\n                                <AvatarImage alt={mail.senderName || mail.sender} />\n                                <AvatarFallback>\n                                    {(mail.senderName || mail.sender).split(' ').map((chunk) => chunk[0]).join('')}\n                                </AvatarFallback>\n                            </Avatar>\n                            <div className='grid gap-1'>\n                                <div className='flex gap-1 items-center text-xs'>\n                                    <span className='font-semibold'>{mail.senderName || mail.sender}</span>\n                                    {mail.senderName && <span className='text-gray-400'>&lt;{mail.sender}&gt;</span>}\n                                </div>\n                                <div className='line-clamp-1 text-xs'>\n                                    {mail.subject}\n                                </div>\n                                <div className='line-clamp-1 text-xs text-gray-400'>\n                                    <span className='font-medium text-gray-600 dark:text-gray-300'>\n                                        Recipient:\n                                    </span>{' '}\n                                    {currentAccount}\n                                    {mail.cc && (\n                                        <>\n                                            <span className='ml-2 font-medium text-gray-600 dark:text-gray-300'>CC:</span>{' '}\n                                            {mail.cc}\n                                        </>\n                                    )}\n                                </div>\n                                {/* <div className='line-clamp-1 text-xs text-gray-400'>\n                                    <span className='font-medium text-gray-600 dark:text-gray-300'>\n                                        Reply-To:\n                                    </span>{' '}\n                                    {mail.senderName || mail.sender}\n                                </div> */}\n                            </div>\n                        </div>\n                        {mail.received_at && (\n                            <div className='ml-auto text-xs text-muted-foreground'>\n                                {dayjs\n                                    .utc(mail.received_at)\n                                    .local()\n                                    .format('YYYY-MM-DD HH:mm:ss')}\n                            </div>\n                        )}\n                    </div>\n                    <Separator />\n                    <div className='flex-1 p-4 text-sm' dangerouslySetInnerHTML={{ __html: mail.content }}></div>\n                </div>\n            ) : (\n                <div className='p-8 text-center text-muted-foreground'>\n                    No message selected\n                </div>\n            )}\n        </div>\n    );\n}"
  },
  {
    "path": "app/src/components/mail/MailList.tsx",
    "content": "import { ScrollArea } from '@/components/ui/scroll-area';\nimport { cn } from '@/lib/utils';\nimport dayjs from 'dayjs';\nimport utc from 'dayjs/plugin/utc';\nimport relativeTime from 'dayjs/plugin/relativeTime';\nimport { Inbox } from 'lucide-react';\nimport type { MailsList } from './data';\nimport { useMail } from './useMail';\n\ndayjs.extend(utc);\ndayjs.extend(relativeTime);\n\ninterface MailListProps {\n    items: MailsList[];\n    updateStatus?: (messageId: string) => void;\n}\n\nexport function MailList({ items, updateStatus = () => {} }: MailListProps) {\n    const [mail, setMail] = useMail();\n    const handleSelect = (messageId: string) => {\n        setMail({\n            ...mail,\n            selected: messageId,\n        });\n\n        fetch('/api/updateStatus', {\n            method: 'POST',\n            headers: {\n                'Content-Type': 'application/json',\n            },\n            body: JSON.stringify({ message_id: messageId, is_read: 1 }),\n        }).then(async (response) => {\n            if (response.ok) {\n                const data = await response.json();\n                if (data.code === 200) {\n                    updateStatus(messageId);\n                }\n            }\n        })\n    }\n    if (items.length === 0) return <div className='p-4 text-center text-xs text-gray-500 [&_p]:mt-2'><Inbox className='mx-auto' /><p>No email received yet</p></div>\n    return (\n        <ScrollArea className='h-screen'>\n            <div className='flex flex-col gap-2 p-4 pt-0'>\n                {items.map((item) => (\n                    <button\n                        key={item.message_id}\n                        className={cn(\n                            'flex flex-col items-start gap-2 rounded-lg border p-3 text-left text-sm transition-all hover:bg-accent',\n                            mail.selected === item.message_id && 'bg-muted'\n                        )}\n                        onClick={() => handleSelect(item.message_id)}\n                    >\n                        <div className='flex w-full flex-col gap-1'>\n                            <div className='flex items-center'>\n                                <div className='flex items-center gap-2'>\n                                    <div className='font-semibold'>\n                                        {item.senderName || item.sender}\n                                    </div>\n                                    {!item.is_read && (\n                                        <span className='flex h-2 w-2 rounded-full bg-blue-600' />\n                                    )}\n                                </div>\n                                <div\n                                    className={cn(\n                                        'ml-auto text-xs',\n                                        mail.selected === item.message_id\n                                            ? 'text-foreground'\n                                            : 'text-muted-foreground'\n                                    )}\n                                >\n                                    {dayjs(item.received_at).fromNow()}\n                                </div>\n                            </div>\n                            <div className='text-xs font-medium'>\n                                {item.subject}\n                            </div>\n                        </div>\n                        <div className='line-clamp-2 text-xs text-muted-foreground'>\n                            {item.content && item.content.substring(0, 300)}\n                        </div>\n                    </button>\n                ))}\n            </div>\n        </ScrollArea>\n    );\n}\n"
  },
  {
    "path": "app/src/components/mail/Nav.tsx",
    "content": "import { cn } from '@/lib/utils';\nimport type { LucideIcon } from 'lucide-react';\nimport {\n    Tooltip,\n    TooltipContent,\n    TooltipTrigger,\n} from '@/components/ui/tooltip';\nimport { buttonVariants } from \"@/components/ui/button\"\nimport { toast } from \"sonner\";\n\ninterface NavProps {\n    isCollapsed: boolean;\n    links: {\n        title: string;\n        label?: string;\n        icon: LucideIcon;\n        variant: 'default' | 'ghost';\n    }[];\n}\n\nexport function Nav({ links, isCollapsed }: NavProps) {\n    return (\n        <div\n            data-collapsed={isCollapsed}\n            className='group flex flex-col gap-4 py-2 data-[collapsed=true]:py-2'\n        >\n            <nav className='grid gap-1 px-2 group-[[data-collapsed=true]]:justify-center group-[[data-collapsed=true]]:px-2'>\n                {links.map((link, index) =>\n                    isCollapsed ? (\n                        <Tooltip key={index} delayDuration={0}>\n                            <TooltipTrigger asChild>\n                                <div\n                                    className={cn(\n                                        buttonVariants({\n                                            variant: link.variant,\n                                            size: 'icon',\n                                        }),\n                                        'h-9 w-9 cursor-pointer',\n                                        link.variant === 'default' &&\n                                            'dark:bg-muted dark:text-muted-foreground dark:hover:bg-muted dark:hover:text-white'\n                                    )}\n                                    onClick={() => {\n                                        if (link.variant !== 'default') toast(`${link.title} is developing~`);\n                                    }}\n                                >\n                                    <link.icon className='h-4 w-4' />\n                                    <span className='sr-only'>\n                                        {link.title}\n                                    </span>\n                                </div>\n                            </TooltipTrigger>\n                            <TooltipContent\n                                side='right'\n                                className='flex items-center gap-4'\n                            >\n                                {link.title}\n                                {link.label && (\n                                    <span className='ml-auto text-muted-foreground'>\n                                        {link.label}\n                                    </span>\n                                )}\n                            </TooltipContent>\n                        </Tooltip>\n                    ) : (\n                        <div\n                            key={index}\n                            className={cn(\n                                buttonVariants({\n                                    variant: link.variant,\n                                    size: 'sm',\n                                }),\n                                link.variant === 'default' &&\n                                    'dark:bg-muted dark:text-white dark:hover:bg-muted dark:hover:text-white',\n                                'justify-start cursor-pointer'\n                            )}\n                            onClick={() => {\n                                if (link.variant !== 'default') toast(`${link.title} is developing~`);\n                            }}\n                        >\n                            <link.icon className='mr-2 h-4 w-4' />\n                            {link.title}\n                            {link.label && (\n                                <span\n                                    className={cn(\n                                        'ml-auto',\n                                        link.variant === 'default' &&\n                                            'text-background dark:text-white'\n                                    )}\n                                >\n                                    {link.label}\n                                </span>\n                            )}\n                        </div>\n                    )\n                )}\n            </nav>\n        </div>\n    );\n}\n"
  },
  {
    "path": "app/src/components/mail/data.tsx",
    "content": "export const mails = [\n  {\n    id: \"61c35085-72d7-42b4-8d62-738f700d4b92\",\n    name: \"AdsPower\",\n    email: \"captcha@email.adspower.net\",\n    subject: \"AdsPower登录验证码\",\n    text: \"您好：\\nAdsPower登录验证码为【926493】，5分钟内有效，请尽快使用。\\n请不要将该验证码泄露给他人。\\n感谢您的使用！\\nAdsPower团队\",\n    html: \"<p><title>adspower</title>\\n</p>\\n<table align=\\\"center\\\" border=\\\"0\\\" cellpadding=\\\"0\\\" cellspacing=\\\"0\\\" width=\\\"100%\\\" style=\\\"border-collapse: collapse;margin-bottom: 40px;\\\">\\n<tbody>\\n<tr>\\n\\t<td>\\n\\t\\t<table align=\\\"center\\\" border=\\\"0\\\" cellpadding=\\\"3\\\" cellspacing=\\\"0\\\" width=\\\"600\\\" style=\\\"border-collapse: collapse;margin-bottom: 20px;\\\">\\n\\t\\t<tbody>\\n\\t\\t<tr>\\n\\t\\t\\t<td width=\\\"68\\\">\\n\\t\\t\\t\\t<img style=\\\"display:block;\\\" alt=\\\"logo\\\" border=\\\"0\\\" height=\\\"60\\\" src=\\\"https://download.adspower.net/email/logo.png\\\" width=\\\"60\\\">\\n\\t\\t\\t</td>\\n\\t\\t\\t<td style=\\\"font-size:30.0px;\\\" align=\\\"left\\\">AdsPower\\n\\t\\t\\t</td>\\n\\t\\t</tr>\\n\\t\\t</tbody>\\n\\t\\t</table>\\n\\t</td>\\n</tr>\\n<tr>\\n\\t<td>\\n\\t\\t<table align=\\\"center\\\" border=\\\"0\\\" cellpadding=\\\"8\\\" cellspacing=\\\"0\\\" width=\\\"600\\\" style=\\\"border-collapse: collapse;padding-bottom:20px\\\">\\n\\t\\t<tbody>\\n\\t\\t<tr>\\n\\t\\t\\t<td>\\n\\t\\t\\t\\t<p style=\\\"margin-bottom: 20px;font-weight: 500; \\\">亲爱的【equal.here374@fakeact.fun】，您好：\\n\\t\\t\\t\\t</p>\\n\\t\\t\\t\\t<p style=\\\" margin-bottom: 20px; padding-left: 10px; \\\">\\n\\t\\t\\t\\t\\tAdsPower登录验证码为【926493】，5分钟内有效，请尽快使用。\\n\\t\\t\\t\\t</p>\\n\\t\\t\\t\\t<p style=\\\" margin-bottom: 20px; padding-left: 10px; \\\">\\n\\t\\t\\t\\t\\t请不要将该验证码泄露给他人。\\n\\t\\t\\t\\t</p>\\n\\t\\t\\t\\t<p style=\\\"margin-bottom: 30px; padding-left: 10px; \\\">\\n\\t\\t\\t\\t\\t感谢您的使用！<br>\\n\\t\\t\\t\\t\\tAdsPower团队\\n\\t\\t\\t\\t</p>\\n\\t\\t\\t</td>\\n\\t\\t</tr>\\n\\t\\t</tbody>\\n\\t\\t</table>\\n\\t</td>\\n</tr>\\n<tr>\\n\\t<td>\\n\\t\\t<table align=\\\"center\\\" border=\\\"0\\\" cellpadding=\\\"8\\\" cellspacing=\\\"0\\\" width=\\\"600\\\" style=\\\"border-collapse: collapse;padding-bottom:20px\\\">\\n\\t\\t<tbody>\\n\\t\\t<tr>\\n\\t\\t\\t<td style=\\\"font-size: 14px;color: #aaa;\\\">此为系统邮件，请勿回复!\\n\\t\\t\\t</td>\\n\\t\\t\\t<td>\\n\\t\\t\\t</td>\\n\\t\\t</tr>\\n\\t\\t</tbody>\\n\\t\\t</table>\\n\\t</td>\\n</tr>\\n<tr>\\n\\t<td align=\\\"center\\\">\\n\\t\\t<table align=\\\"center\\\" border=\\\"0\\\" cellpadding=\\\"7\\\" cellspacing=\\\"0\\\" width=\\\"600\\\" style=\\\"border-collapse: collapse;padding-bottom:20px;background-color: #f7f7f7; font-size: 14px;\\\">\\n\\t\\t<tbody>\\n\\t\\t<tr>\\n\\t\\t\\t<td>如需要帮助，请联系我们：\\n\\t\\t\\t</td>\\n\\t\\t\\t<td width=\\\"90\\\" align=\\\"right\\\" rowspan=\\\"3\\\">\\n\\t\\t\\t\\t<img border=\\\"0\\\" style=\\\"display:inline-block;\\\" src=\\\"https://download.adspower.net/email/qr_code.jpg\\\" alt=\\\"qr_code icon\\\" width=\\\"80\\\" height=\\\"80\\\">\\n\\t\\t\\t</td>\\n\\t\\t</tr>\\n\\t\\t<tr>\\n\\t\\t\\t<td align=\\\"left\\\" style=\\\"line-height: normal;\\\">\\n\\t\\t\\t\\t<img border=\\\"0\\\" style=\\\"display:inline-block;\\\" src=\\\"https://download.adspower.net/email/email.png\\\" alt=\\\"email_icon\\\" width=\\\"16\\\" height=\\\"16\\\">\\n\\t\\t\\t\\t通过发送邮件至 support@adspower.net 联系客户支持\\n\\t\\t\\t</td>\\n\\t\\t\\t<td>\\n\\t\\t\\t</td>\\n\\t\\t</tr>\\n\\t\\t<tr>\\n\\t\\t\\t<td align=\\\"left\\\" style=\\\"line-height: normal;\\\">\\n\\t\\t\\t\\t<img border=\\\"0\\\" style=\\\"display:inline-block;\\\" src=\\\"https://download.adspower.net/email/kefu.png\\\" alt=\\\"kefu_icon\\\" width=\\\"16\\\" height=\\\"16\\\">\\n\\t\\t\\t\\t登入网址或APP后通过\\n\\t\\t\\t\\t<a href=\\\"https://www.adspower.net/\\\" target=\\\"_blank\\\" style=\\\"text-decoration: none; color:blue;\\\">在线咨询</a>\\n\\t\\t\\t\\t联系我们\\n\\t\\t\\t</td>\\n\\t\\t\\t<td>\\n\\t\\t\\t</td>\\n\\t\\t</tr>\\n\\t\\t<tr>\\n\\t\\t\\t<td align=\\\"left\\\" style=\\\"line-height: normal;\\\" colspan=\\\"2\\\">\\n\\t\\t\\t\\t<img border=\\\"0\\\" style=\\\"display:inline-block;\\\" src=\\\"https://download.adspower.net/email/wechat.png\\\" alt=\\\"wechat_icon\\\" width=\\\"16\\\" height=\\\"16\\\">\\n\\t\\t\\t\\t用微信扫描右方二维码，关注AdsPower官方公众号，然后发消息给我们\\n\\t\\t\\t</td>\\n\\t\\t</tr>\\n\\t\\t</tbody>\\n\\t\\t</table>\\n\\t</td>\\n</tr>\\n</tbody>\\n</table>\\n\",\n    date: \"2025-01-10T13:15:00\",\n    read: false,\n    labels: [\"work\", \"budget\"],\n  },\n  {\n    id: \"8f7b5db9-d935-4e42-8e05-1f1d0a3dfb97\",\n    name: \"Michael Wilson\",\n    email: \"michaelwilson@example.com\",\n    subject: \"Important Announcement\",\n    text: \"I have an important announcement to make during our team meeting. It pertains to a strategic shift in our approach to the upcoming product launch. We've received valuable feedback from our beta testers, and I believe it's time to make some adjustments to better meet our customers' needs.\\n\\nThis change is crucial to our success, and I look forward to discussing it with the team. Please be prepared to share your insights during the meeting.\\n\\nRegards, Michael\",\n    date: \"2023-03-10T15:00:00\",\n    read: false,\n    labels: [\"meeting\", \"work\", \"important\"],\n  },\n  {\n    id: \"1f0f2c02-e299-40de-9b1d-86ef9e42126b\",\n    name: \"Sarah Brown\",\n    email: \"sarahbrown@example.com\",\n    subject: \"Re: Feedback on Proposal\",\n    text: \"Thank you for your feedback on the proposal. It looks great! I'm pleased to hear that you found it promising. The team worked diligently to address all the key points you raised, and I believe we now have a strong foundation for the project.\\n\\nI've attached the revised proposal for your review.\\n\\nPlease let me know if you have any further comments or suggestions. Looking forward to your response.\\n\\nBest regards, Sarah\",\n    date: \"2023-02-15T16:30:00\",\n    read: true,\n    labels: [\"work\"],\n  },\n  {\n    id: \"17c0a96d-4415-42b1-8b4f-764efab57f66\",\n    name: \"David Lee\",\n    email: \"davidlee@example.com\",\n    subject: \"New Project Idea\",\n    text: \"I have an exciting new project idea to discuss with you. It involves expanding our services to target a niche market that has shown considerable growth in recent months.\\n\\nI've prepared a detailed proposal outlining the potential benefits and the strategy for execution.\\n\\nThis project has the potential to significantly impact our business positively. Let's set up a meeting to dive into the details and determine if it aligns with our current goals.\\n\\nBest regards, David\",\n    date: \"2023-01-28T17:45:00\",\n    read: false,\n    labels: [\"meeting\", \"work\", \"important\"],\n  },\n  {\n    id: \"2f0130cb-39fc-44c4-bb3c-0a4337edaaab\",\n    name: \"Olivia Wilson\",\n    email: \"oliviawilson@example.com\",\n    subject: \"Vacation Plans\",\n    text: \"Let's plan our vacation for next month. What do you think? I've been thinking of visiting a tropical paradise, and I've put together some destination options.\\n\\nI believe it's time for us to unwind and recharge. Please take a look at the options and let me know your preferences.\\n\\nWe can start making arrangements to ensure a smooth and enjoyable trip.\\n\\nExcited to hear your thoughts! Olivia\",\n    date: \"2022-12-20T18:30:00\",\n    read: true,\n    labels: [\"personal\"],\n  },\n  {\n    id: \"de305d54-75b4-431b-adb2-eb6b9e546014\",\n    name: \"James Martin\",\n    email: \"jamesmartin@example.com\",\n    subject: \"Re: Conference Registration\",\n    text: \"I've completed the registration for the conference next month. The event promises to be a great networking opportunity, and I'm looking forward to attending the various sessions and connecting with industry experts.\\n\\nI've also attached the conference schedule for your reference.\\n\\nIf there are any specific topics or sessions you'd like me to explore, please let me know. It's an exciting event, and I'll make the most of it.\\n\\nBest regards, James\",\n    date: \"2022-11-30T19:15:00\",\n    read: true,\n    labels: [\"work\", \"conference\"],\n  },\n  {\n    id: \"7dd90c63-00f6-40f3-bd87-5060a24e8ee7\",\n    name: \"Sophia White\",\n    email: \"sophiawhite@example.com\",\n    subject: \"Team Dinner\",\n    text: \"Let's have a team dinner next week to celebrate our success. We've achieved some significant milestones, and it's time to acknowledge our hard work and dedication.\\n\\nI've made reservations at a lovely restaurant, and I'm sure it'll be an enjoyable evening.\\n\\nPlease confirm your availability and any dietary preferences. Looking forward to a fun and memorable dinner with the team!\\n\\nBest, Sophia\",\n    date: \"2022-11-05T20:30:00\",\n    read: false,\n    labels: [\"meeting\", \"work\"],\n  },\n  {\n    id: \"99a88f78-3eb4-4d87-87b7-7b15a49a0a05\",\n    name: \"Daniel Johnson\",\n    email: \"danieljohnson@example.com\",\n    subject: \"Feedback Request\",\n    text: \"I'd like your feedback on the latest project deliverables. We've made significant progress, and I value your input to ensure we're on the right track.\\n\\nI've attached the deliverables for your review, and I'm particularly interested in any areas where you think we can further enhance the quality or efficiency.\\n\\nYour feedback is invaluable, and I appreciate your time and expertise. Let's work together to make this project a success.\\n\\nRegards, Daniel\",\n    date: \"2022-10-22T09:30:00\",\n    read: false,\n    labels: [\"work\"],\n  },\n  {\n    id: \"f47ac10b-58cc-4372-a567-0e02b2c3d479\",\n    name: \"Ava Taylor\",\n    email: \"avataylor@example.com\",\n    subject: \"Re: Meeting Agenda\",\n    text: \"Here's the agenda for our meeting next week. I've included all the topics we need to cover, as well as time allocations for each.\\n\\nIf you have any additional items to discuss or any specific points to address, please let me know, and we can integrate them into the agenda.\\n\\nIt's essential that our meeting is productive and addresses all relevant matters.\\n\\nLooking forward to our meeting! Ava\",\n    date: \"2022-10-10T10:45:00\",\n    read: true,\n    labels: [\"meeting\", \"work\"],\n  },\n  {\n    id: \"c1a0ecb4-2540-49c5-86f8-21e5ce79e4e6\",\n    name: \"William Anderson\",\n    email: \"williamanderson@example.com\",\n    subject: \"Product Launch Update\",\n    text: \"The product launch is on track. I'll provide an update during our call. We've made substantial progress in the development and marketing of our new product.\\n\\nI'm excited to share the latest updates with you during our upcoming call. It's crucial that we coordinate our efforts to ensure a successful launch. Please come prepared with any questions or insights you may have.\\n\\nLet's make this product launch a resounding success!\\n\\nBest regards, William\",\n    date: \"2022-09-20T12:00:00\",\n    read: false,\n    labels: [\"meeting\", \"work\", \"important\"],\n  },\n  {\n    id: \"ba54eefd-4097-4949-99f2-2a9ae4d1a836\",\n    name: \"Mia Harris\",\n    email: \"miaharris@example.com\",\n    subject: \"Re: Travel Itinerary\",\n    text: \"I've received the travel itinerary. It looks great! Thank you for your prompt assistance in arranging the details. I've reviewed the schedule and the accommodations, and everything seems to be in order. I'm looking forward to the trip, and I'm confident it'll be a smooth and enjoyable experience.\\n\\nIf there are any specific activities or attractions you recommend at our destination, please feel free to share your suggestions.\\n\\nExcited for the trip! Mia\",\n    date: \"2022-09-10T13:15:00\",\n    read: true,\n    labels: [\"personal\", \"travel\"],\n  },\n  {\n    id: \"df09b6ed-28bd-4e0c-85a9-9320ec5179aa\",\n    name: \"Ethan Clark\",\n    email: \"ethanclark@example.com\",\n    subject: \"Team Building Event\",\n    text: \"Let's plan a team-building event for our department. Team cohesion and morale are vital to our success, and I believe a well-organized team-building event can be incredibly beneficial. I've done some research and have a few ideas for fun and engaging activities.\\n\\nPlease let me know your thoughts and availability. We want this event to be both enjoyable and productive.\\n\\nTogether, we'll strengthen our team and boost our performance.\\n\\nRegards, Ethan\",\n    date: \"2022-08-25T15:30:00\",\n    read: false,\n    labels: [\"meeting\", \"work\"],\n  },\n  {\n    id: \"d67c1842-7f8b-4b4b-9be1-1b3b1ab4611d\",\n    name: \"Chloe Hall\",\n    email: \"chloehall@example.com\",\n    subject: \"Re: Budget Approval\",\n    text: \"The budget has been approved. We can proceed with the project. I'm delighted to inform you that our budget proposal has received the green light from the finance department. This is a significant milestone, and it means we can move forward with the project as planned.\\n\\nI've attached the finalized budget for your reference. Let's ensure that we stay on track and deliver the project on time and within budget.\\n\\nIt's an exciting time for us! Chloe\",\n    date: \"2022-08-10T16:45:00\",\n    read: true,\n    labels: [\"work\", \"budget\"],\n  },\n  {\n    id: \"6c9a7f94-8329-4d70-95d3-51f68c186ae1\",\n    name: \"Samuel Turner\",\n    email: \"samuelturner@example.com\",\n    subject: \"Weekend Hike\",\n    text: \"Who's up for a weekend hike in the mountains? I've been craving some outdoor adventure, and a hike in the mountains sounds like the perfect escape. If you're up for the challenge, we can explore some scenic trails and enjoy the beauty of nature.\\n\\nI've done some research and have a few routes in mind.\\n\\nLet me know if you're interested, and we can plan the details.\\n\\nIt's sure to be a memorable experience! Samuel\",\n    date: \"2022-07-28T17:30:00\",\n    read: false,\n    labels: [\"personal\"],\n  },\n]\n\nexport type Mail = (typeof mails)[number]\n\nexport const accounts = [\n  {\n    label: \"Equal Here\",\n    email: \"equal.here374@fakeact.fun\"\n  },\n  {\n    label: \"Alicia Koch\",\n    email: \"blicia@fakemail.fun\",\n  },\n  {\n    label: \"Alicia Koch\",\n    email: \"clicia@fakemail.fun\",\n  },\n]\n\nexport type Account = (typeof accounts)[number]\n\nexport const contacts = [\n  {\n    name: \"Emma Johnson\",\n    email: \"emma.johnson@example.com\",\n  },\n  {\n    name: \"Liam Wilson\",\n    email: \"liam.wilson@example.com\",\n  },\n  {\n    name: \"Olivia Davis\",\n    email: \"olivia.davis@example.com\",\n  },\n  {\n    name: \"Noah Martinez\",\n    email: \"noah.martinez@example.com\",\n  },\n  {\n    name: \"Ava Taylor\",\n    email: \"ava.taylor@example.com\",\n  },\n  {\n    name: \"Lucas Brown\",\n    email: \"lucas.brown@example.com\",\n  },\n  {\n    name: \"Sophia Smith\",\n    email: \"sophia.smith@example.com\",\n  },\n  {\n    name: \"Ethan Wilson\",\n    email: \"ethan.wilson@example.com\",\n  },\n  {\n    name: \"Isabella Jackson\",\n    email: \"isabella.jackson@example.com\",\n  },\n  {\n    name: \"Mia Clark\",\n    email: \"mia.clark@example.com\",\n  },\n  {\n    name: \"Mason Lee\",\n    email: \"mason.lee@example.com\",\n  },\n  {\n    name: \"Layla Harris\",\n    email: \"layla.harris@example.com\",\n  },\n  {\n    name: \"William Anderson\",\n    email: \"william.anderson@example.com\",\n  },\n  {\n    name: \"Ella White\",\n    email: \"ella.white@example.com\",\n  },\n  {\n    name: \"James Thomas\",\n    email: \"james.thomas@example.com\",\n  },\n  {\n    name: \"Harper Lewis\",\n    email: \"harper.lewis@example.com\",\n  },\n  {\n    name: \"Benjamin Moore\",\n    email: \"benjamin.moore@example.com\",\n  },\n  {\n    name: \"Aria Hall\",\n    email: \"aria.hall@example.com\",\n  },\n  {\n    name: \"Henry Turner\",\n    email: \"henry.turner@example.com\",\n  },\n  {\n    name: \"Scarlett Adams\",\n    email: \"scarlett.adams@example.com\",\n  },\n]\n\nexport type Contact = (typeof contacts)[number]\n\nexport type MailsList = {\n  message_id: string\n  subject: string\n  sender: string\n  senderName: string\n  cc: string\n  content: string\n  received_at: string\n  is_read:number\n}\n\nexport type AccountsList = { id: number; email_address: string; alias: string; created_at: string; };\n  "
  },
  {
    "path": "app/src/components/mail/useMail.ts",
    "content": "import { atom, useAtom } from 'jotai';\n\nimport { type Mail } from './data';\n\ntype Config = {\n    selected: Mail['id'] | null;\n};\n\nconst configAtom = atom<Config>({\n    selected: null,\n});\n\nexport function useMail() {\n    return useAtom(configAtom);\n}\n"
  },
  {
    "path": "app/src/components/ui/alert-dialog.tsx",
    "content": "import * as React from \"react\"\nimport * as AlertDialogPrimitive from \"@radix-ui/react-alert-dialog\"\n\nimport { cn } from \"@/lib/utils\"\nimport { buttonVariants } from \"@/components/ui/button\"\n\nconst AlertDialog = AlertDialogPrimitive.Root\n\nconst AlertDialogTrigger = AlertDialogPrimitive.Trigger\n\nconst AlertDialogPortal = AlertDialogPrimitive.Portal\n\nconst AlertDialogOverlay = React.forwardRef<\n  React.ElementRef<typeof AlertDialogPrimitive.Overlay>,\n  React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay>\n>(({ className, ...props }, ref) => (\n  <AlertDialogPrimitive.Overlay\n    className={cn(\n      \"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0\",\n      className\n    )}\n    {...props}\n    ref={ref}\n  />\n))\nAlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName\n\nconst AlertDialogContent = React.forwardRef<\n  React.ElementRef<typeof AlertDialogPrimitive.Content>,\n  React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content>\n>(({ className, ...props }, ref) => (\n  <AlertDialogPortal>\n    <AlertDialogOverlay />\n    <AlertDialogPrimitive.Content\n      ref={ref}\n      className={cn(\n        \"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg\",\n        className\n      )}\n      {...props}\n    />\n  </AlertDialogPortal>\n))\nAlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName\n\nconst AlertDialogHeader = ({\n  className,\n  ...props\n}: React.HTMLAttributes<HTMLDivElement>) => (\n  <div\n    className={cn(\n      \"flex flex-col space-y-2 text-center sm:text-left\",\n      className\n    )}\n    {...props}\n  />\n)\nAlertDialogHeader.displayName = \"AlertDialogHeader\"\n\nconst AlertDialogFooter = ({\n  className,\n  ...props\n}: React.HTMLAttributes<HTMLDivElement>) => (\n  <div\n    className={cn(\n      \"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2\",\n      className\n    )}\n    {...props}\n  />\n)\nAlertDialogFooter.displayName = \"AlertDialogFooter\"\n\nconst AlertDialogTitle = React.forwardRef<\n  React.ElementRef<typeof AlertDialogPrimitive.Title>,\n  React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Title>\n>(({ className, ...props }, ref) => (\n  <AlertDialogPrimitive.Title\n    ref={ref}\n    className={cn(\"text-lg font-semibold\", className)}\n    {...props}\n  />\n))\nAlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName\n\nconst AlertDialogDescription = React.forwardRef<\n  React.ElementRef<typeof AlertDialogPrimitive.Description>,\n  React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Description>\n>(({ className, ...props }, ref) => (\n  <AlertDialogPrimitive.Description\n    ref={ref}\n    className={cn(\"text-sm text-muted-foreground\", className)}\n    {...props}\n  />\n))\nAlertDialogDescription.displayName =\n  AlertDialogPrimitive.Description.displayName\n\nconst AlertDialogAction = React.forwardRef<\n  React.ElementRef<typeof AlertDialogPrimitive.Action>,\n  React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Action>\n>(({ className, ...props }, ref) => (\n  <AlertDialogPrimitive.Action\n    ref={ref}\n    className={cn(buttonVariants(), className)}\n    {...props}\n  />\n))\nAlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName\n\nconst AlertDialogCancel = React.forwardRef<\n  React.ElementRef<typeof AlertDialogPrimitive.Cancel>,\n  React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Cancel>\n>(({ className, ...props }, ref) => (\n  <AlertDialogPrimitive.Cancel\n    ref={ref}\n    className={cn(\n      buttonVariants({ variant: \"outline\" }),\n      \"mt-2 sm:mt-0\",\n      className\n    )}\n    {...props}\n  />\n))\nAlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName\n\nexport {\n  AlertDialog,\n  AlertDialogPortal,\n  AlertDialogOverlay,\n  AlertDialogTrigger,\n  AlertDialogContent,\n  AlertDialogHeader,\n  AlertDialogFooter,\n  AlertDialogTitle,\n  AlertDialogDescription,\n  AlertDialogAction,\n  AlertDialogCancel,\n}\n"
  },
  {
    "path": "app/src/components/ui/avatar.tsx",
    "content": "import * as React from \"react\"\nimport * as AvatarPrimitive from \"@radix-ui/react-avatar\"\n\nimport { cn } from \"@/lib/utils\"\n\nconst Avatar = React.forwardRef<\n  React.ElementRef<typeof AvatarPrimitive.Root>,\n  React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>\n>(({ className, ...props }, ref) => (\n  <AvatarPrimitive.Root\n    ref={ref}\n    className={cn(\n      \"relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full\",\n      className\n    )}\n    {...props}\n  />\n))\nAvatar.displayName = AvatarPrimitive.Root.displayName\n\nconst AvatarImage = React.forwardRef<\n  React.ElementRef<typeof AvatarPrimitive.Image>,\n  React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>\n>(({ className, ...props }, ref) => (\n  <AvatarPrimitive.Image\n    ref={ref}\n    className={cn(\"aspect-square h-full w-full\", className)}\n    {...props}\n  />\n))\nAvatarImage.displayName = AvatarPrimitive.Image.displayName\n\nconst AvatarFallback = React.forwardRef<\n  React.ElementRef<typeof AvatarPrimitive.Fallback>,\n  React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>\n>(({ className, ...props }, ref) => (\n  <AvatarPrimitive.Fallback\n    ref={ref}\n    className={cn(\n      \"flex h-full w-full items-center justify-center rounded-full bg-muted\",\n      className\n    )}\n    {...props}\n  />\n))\nAvatarFallback.displayName = AvatarPrimitive.Fallback.displayName\n\nexport { Avatar, AvatarImage, AvatarFallback }\n"
  },
  {
    "path": "app/src/components/ui/badge.tsx",
    "content": "import * as React from \"react\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\n\nimport { cn } from \"@/lib/utils\"\n\nconst badgeVariants = cva(\n  \"inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2\",\n  {\n    variants: {\n      variant: {\n        default:\n          \"border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80\",\n        secondary:\n          \"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80\",\n        destructive:\n          \"border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80\",\n        outline: \"text-foreground\",\n      },\n    },\n    defaultVariants: {\n      variant: \"default\",\n    },\n  }\n)\n\nexport interface BadgeProps\n  extends React.HTMLAttributes<HTMLDivElement>,\n    VariantProps<typeof badgeVariants> {}\n\nfunction Badge({ className, variant, ...props }: BadgeProps) {\n  return (\n    <div className={cn(badgeVariants({ variant }), className)} {...props} />\n  )\n}\n\nexport { Badge, badgeVariants }\n"
  },
  {
    "path": "app/src/components/ui/button.tsx",
    "content": "import * as React from \"react\"\nimport { Slot } from \"@radix-ui/react-slot\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\n\nimport { cn } from \"@/lib/utils\"\n\nconst buttonVariants = cva(\n  \"inline-flex items-center justify-center gap-2 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 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0\",\n  {\n    variants: {\n      variant: {\n        default:\n          \"bg-primary text-primary-foreground shadow hover:bg-primary/90\",\n        destructive:\n          \"bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90\",\n        outline:\n          \"border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground\",\n        secondary:\n          \"bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80\",\n        ghost: \"hover:bg-accent hover:text-accent-foreground\",\n        link: \"text-primary underline-offset-4 hover:underline\",\n      },\n      size: {\n        default: \"h-9 px-4 py-2\",\n        sm: \"h-8 rounded-md px-3 text-xs\",\n        lg: \"h-10 rounded-md px-8\",\n        icon: \"h-9 w-9\",\n        xs: \"h-7 w-7\"\n      },\n    },\n    defaultVariants: {\n      variant: \"default\",\n      size: \"default\",\n    },\n  }\n)\n\nexport interface ButtonProps\n  extends React.ButtonHTMLAttributes<HTMLButtonElement>,\n    VariantProps<typeof buttonVariants> {\n  asChild?: boolean\n}\n\nconst Button = React.forwardRef<HTMLButtonElement, ButtonProps>(\n  ({ className, variant, size, asChild = false, ...props }, ref) => {\n    const Comp = asChild ? Slot : \"button\"\n    return (\n      <Comp\n        className={cn(buttonVariants({ variant, size, className }))}\n        ref={ref}\n        {...props}\n      />\n    )\n  }\n)\nButton.displayName = \"Button\"\n\nexport { Button, buttonVariants }\n"
  },
  {
    "path": "app/src/components/ui/dialog.tsx",
    "content": "import * as React from \"react\"\nimport * as DialogPrimitive from \"@radix-ui/react-dialog\"\nimport { X } from \"lucide-react\"\n\nimport { cn } from \"@/lib/utils\"\n\nconst Dialog = DialogPrimitive.Root\n\nconst DialogTrigger = DialogPrimitive.Trigger\n\nconst DialogPortal = DialogPrimitive.Portal\n\nconst DialogClose = DialogPrimitive.Close\n\nconst DialogOverlay = React.forwardRef<\n  React.ElementRef<typeof DialogPrimitive.Overlay>,\n  React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>\n>(({ className, ...props }, ref) => (\n  <DialogPrimitive.Overlay\n    ref={ref}\n    className={cn(\n      \"fixed inset-0 z-50 bg-black/80  data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0\",\n      className\n    )}\n    {...props}\n  />\n))\nDialogOverlay.displayName = DialogPrimitive.Overlay.displayName\n\nconst DialogContent = React.forwardRef<\n  React.ElementRef<typeof DialogPrimitive.Content>,\n  React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>\n>(({ className, children, ...props }, ref) => (\n  <DialogPortal>\n    <DialogOverlay />\n    <DialogPrimitive.Content\n      ref={ref}\n      className={cn(\n        \"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg\",\n        className\n      )}\n      {...props}\n    >\n      {children}\n      <DialogPrimitive.Close className=\"absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground\">\n        <X className=\"h-4 w-4\" />\n        <span className=\"sr-only\">Close</span>\n      </DialogPrimitive.Close>\n    </DialogPrimitive.Content>\n  </DialogPortal>\n))\nDialogContent.displayName = DialogPrimitive.Content.displayName\n\nconst DialogHeader = ({\n  className,\n  ...props\n}: React.HTMLAttributes<HTMLDivElement>) => (\n  <div\n    className={cn(\n      \"flex flex-col space-y-1.5 text-center sm:text-left\",\n      className\n    )}\n    {...props}\n  />\n)\nDialogHeader.displayName = \"DialogHeader\"\n\nconst DialogFooter = ({\n  className,\n  ...props\n}: React.HTMLAttributes<HTMLDivElement>) => (\n  <div\n    className={cn(\n      \"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2\",\n      className\n    )}\n    {...props}\n  />\n)\nDialogFooter.displayName = \"DialogFooter\"\n\nconst DialogTitle = React.forwardRef<\n  React.ElementRef<typeof DialogPrimitive.Title>,\n  React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>\n>(({ className, ...props }, ref) => (\n  <DialogPrimitive.Title\n    ref={ref}\n    className={cn(\n      \"text-lg font-semibold leading-none tracking-tight\",\n      className\n    )}\n    {...props}\n  />\n))\nDialogTitle.displayName = DialogPrimitive.Title.displayName\n\nconst DialogDescription = React.forwardRef<\n  React.ElementRef<typeof DialogPrimitive.Description>,\n  React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>\n>(({ className, ...props }, ref) => (\n  <DialogPrimitive.Description\n    ref={ref}\n    className={cn(\"text-sm text-muted-foreground\", className)}\n    {...props}\n  />\n))\nDialogDescription.displayName = DialogPrimitive.Description.displayName\n\nexport {\n  Dialog,\n  DialogPortal,\n  DialogOverlay,\n  DialogTrigger,\n  DialogClose,\n  DialogContent,\n  DialogHeader,\n  DialogFooter,\n  DialogTitle,\n  DialogDescription,\n}\n"
  },
  {
    "path": "app/src/components/ui/dropdown-menu.tsx",
    "content": "import * as React from \"react\"\nimport * as DropdownMenuPrimitive from \"@radix-ui/react-dropdown-menu\"\nimport { Check, ChevronRight, Circle } from \"lucide-react\"\n\nimport { cn } from \"@/lib/utils\"\n\nconst DropdownMenu = DropdownMenuPrimitive.Root\n\nconst DropdownMenuTrigger = DropdownMenuPrimitive.Trigger\n\nconst DropdownMenuGroup = DropdownMenuPrimitive.Group\n\nconst DropdownMenuPortal = DropdownMenuPrimitive.Portal\n\nconst DropdownMenuSub = DropdownMenuPrimitive.Sub\n\nconst DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup\n\nconst DropdownMenuSubTrigger = React.forwardRef<\n  React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,\n  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {\n    inset?: boolean\n  }\n>(({ className, inset, children, ...props }, ref) => (\n  <DropdownMenuPrimitive.SubTrigger\n    ref={ref}\n    className={cn(\n      \"flex cursor-default gap-2 select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0\",\n      inset && \"pl-8\",\n      className\n    )}\n    {...props}\n  >\n    {children}\n    <ChevronRight className=\"ml-auto\" />\n  </DropdownMenuPrimitive.SubTrigger>\n))\nDropdownMenuSubTrigger.displayName =\n  DropdownMenuPrimitive.SubTrigger.displayName\n\nconst DropdownMenuSubContent = React.forwardRef<\n  React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,\n  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>\n>(({ className, ...props }, ref) => (\n  <DropdownMenuPrimitive.SubContent\n    ref={ref}\n    className={cn(\n      \"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2\",\n      className\n    )}\n    {...props}\n  />\n))\nDropdownMenuSubContent.displayName =\n  DropdownMenuPrimitive.SubContent.displayName\n\nconst DropdownMenuContent = React.forwardRef<\n  React.ElementRef<typeof DropdownMenuPrimitive.Content>,\n  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>\n>(({ className, sideOffset = 4, ...props }, ref) => (\n  <DropdownMenuPrimitive.Portal>\n    <DropdownMenuPrimitive.Content\n      ref={ref}\n      sideOffset={sideOffset}\n      className={cn(\n        \"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md\",\n        \"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2\",\n        className\n      )}\n      {...props}\n    />\n  </DropdownMenuPrimitive.Portal>\n))\nDropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName\n\nconst DropdownMenuItem = React.forwardRef<\n  React.ElementRef<typeof DropdownMenuPrimitive.Item>,\n  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {\n    inset?: boolean\n  }\n>(({ className, inset, ...props }, ref) => (\n  <DropdownMenuPrimitive.Item\n    ref={ref}\n    className={cn(\n      \"relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&>svg]:size-4 [&>svg]:shrink-0\",\n      inset && \"pl-8\",\n      className\n    )}\n    {...props}\n  />\n))\nDropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName\n\nconst DropdownMenuCheckboxItem = React.forwardRef<\n  React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,\n  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>\n>(({ className, children, checked, ...props }, ref) => (\n  <DropdownMenuPrimitive.CheckboxItem\n    ref={ref}\n    className={cn(\n      \"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50\",\n      className\n    )}\n    checked={checked}\n    {...props}\n  >\n    <span className=\"absolute left-2 flex h-3.5 w-3.5 items-center justify-center\">\n      <DropdownMenuPrimitive.ItemIndicator>\n        <Check className=\"h-4 w-4\" />\n      </DropdownMenuPrimitive.ItemIndicator>\n    </span>\n    {children}\n  </DropdownMenuPrimitive.CheckboxItem>\n))\nDropdownMenuCheckboxItem.displayName =\n  DropdownMenuPrimitive.CheckboxItem.displayName\n\nconst DropdownMenuRadioItem = React.forwardRef<\n  React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,\n  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>\n>(({ className, children, ...props }, ref) => (\n  <DropdownMenuPrimitive.RadioItem\n    ref={ref}\n    className={cn(\n      \"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50\",\n      className\n    )}\n    {...props}\n  >\n    <span className=\"absolute left-2 flex h-3.5 w-3.5 items-center justify-center\">\n      <DropdownMenuPrimitive.ItemIndicator>\n        <Circle className=\"h-2 w-2 fill-current\" />\n      </DropdownMenuPrimitive.ItemIndicator>\n    </span>\n    {children}\n  </DropdownMenuPrimitive.RadioItem>\n))\nDropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName\n\nconst DropdownMenuLabel = React.forwardRef<\n  React.ElementRef<typeof DropdownMenuPrimitive.Label>,\n  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {\n    inset?: boolean\n  }\n>(({ className, inset, ...props }, ref) => (\n  <DropdownMenuPrimitive.Label\n    ref={ref}\n    className={cn(\n      \"px-2 py-1.5 text-sm font-semibold\",\n      inset && \"pl-8\",\n      className\n    )}\n    {...props}\n  />\n))\nDropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName\n\nconst DropdownMenuSeparator = React.forwardRef<\n  React.ElementRef<typeof DropdownMenuPrimitive.Separator>,\n  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>\n>(({ className, ...props }, ref) => (\n  <DropdownMenuPrimitive.Separator\n    ref={ref}\n    className={cn(\"-mx-1 my-1 h-px bg-muted\", className)}\n    {...props}\n  />\n))\nDropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName\n\nconst DropdownMenuShortcut = ({\n  className,\n  ...props\n}: React.HTMLAttributes<HTMLSpanElement>) => {\n  return (\n    <span\n      className={cn(\"ml-auto text-xs tracking-widest opacity-60\", className)}\n      {...props}\n    />\n  )\n}\nDropdownMenuShortcut.displayName = \"DropdownMenuShortcut\"\n\nexport {\n  DropdownMenu,\n  DropdownMenuTrigger,\n  DropdownMenuContent,\n  DropdownMenuItem,\n  DropdownMenuCheckboxItem,\n  DropdownMenuRadioItem,\n  DropdownMenuLabel,\n  DropdownMenuSeparator,\n  DropdownMenuShortcut,\n  DropdownMenuGroup,\n  DropdownMenuPortal,\n  DropdownMenuSub,\n  DropdownMenuSubContent,\n  DropdownMenuSubTrigger,\n  DropdownMenuRadioGroup,\n}\n"
  },
  {
    "path": "app/src/components/ui/input.tsx",
    "content": "import * as React from \"react\"\n\nimport { cn } from \"@/lib/utils\"\n\nconst Input = React.forwardRef<HTMLInputElement, React.ComponentProps<\"input\">>(\n  ({ className, type, ...props }, ref) => {\n    return (\n      <input\n        type={type}\n        className={cn(\n          \"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm\",\n          className\n        )}\n        ref={ref}\n        {...props}\n      />\n    )\n  }\n)\nInput.displayName = \"Input\"\n\nexport { Input }\n"
  },
  {
    "path": "app/src/components/ui/mail-card.tsx",
    "content": "import { useState } from 'react';\nimport { cn } from '@/lib/utils';\nimport { Mail, ChevronDown, ChevronUp, Trash2 } from 'lucide-react';\nimport { Button } from '@/components/ui/button';\nimport {\n    AlertDialog,\n    AlertDialogAction,\n    AlertDialogCancel,\n    AlertDialogContent,\n    AlertDialogDescription,\n    AlertDialogFooter,\n    AlertDialogHeader,\n    AlertDialogTitle,\n    AlertDialogTrigger,\n} from '@/components/ui/alert-dialog';\n\ninterface MailData {\n    className?: string;\n    sender: string;\n    subject: string;\n    date: string;\n    name?: string;\n    content: string;\n    defaultShow: boolean;\n    toDelete?: () => void;\n}\n\nexport const MailCard = ({\n    className,\n    sender,\n    subject,\n    date,\n    name,\n    content,\n    defaultShow,\n    toDelete\n}: MailData) => {\n    const [show, setShow] = useState<boolean>(defaultShow);\n    return (\n        <div\n            className={cn(\n                'border border-gray-300 rounded-xl p-4 grid gap-1 mb-3 hover:border-green-600 hover:bg-green-50/10',\n                show && 'border-2',\n                className\n            )}\n        >\n            <div className='flex gap-2 sm:flex-row sm:items-center flex-col'>\n                <div className='flex-1 grid gap-1'>\n                    <div\n                        className={cn(\n                            'flex gap-1 items-center text-xs text-gray-500 dark:text-gray-400 flex-1 pr-4',\n                            show && 'text-gray-700'\n                        )}\n                    >\n                        <Mail size={16} />\n                        {name ? `${name} <${sender}>` : sender}\n                    </div>\n                    <div className='text-sm font-semibold'>{subject}</div>\n                </div>\n                <div className='flex gap-1.5 justify-between border-t pt-1 sm:border-none sm:pt-0 items-center shrink-0'>\n                    <div className='text-xs text-gray-600 whitespace-nowrap'>\n                        {date}\n                    </div>\n                    <Button\n                        variant='outline'\n                        size='sm'\n                        className='shrink-0'\n                        onClick={() => setShow(!show)}\n                    >\n                        {show ? <ChevronUp /> : <ChevronDown />}\n                    </Button>\n                </div>\n            </div>\n            {show && (\n                <div className='pt-4 border-t leading-4 text-sm'>\n                    <div dangerouslySetInnerHTML={{ __html: content }}></div>\n                    <div className='border-t pt-2 text-right'>\n                        <AlertDialog>\n                            <AlertDialogTrigger asChild>\n                                <Button size='xs' variant='ghost'>\n                                    <Trash2 size={14} color='red' />\n                                </Button>\n                            </AlertDialogTrigger>\n                            <AlertDialogContent>\n                                <AlertDialogHeader>\n                                    <AlertDialogTitle>\n                                        Are you absolutely sure?\n                                    </AlertDialogTitle>\n                                    <AlertDialogDescription>\n                                        Delete this mail cannot be undone. This will\n                                        permanently remove your data from our servers.\n                                    </AlertDialogDescription>\n                                </AlertDialogHeader>\n                                <AlertDialogFooter>\n                                    <AlertDialogCancel>\n                                        Cancel\n                                    </AlertDialogCancel>\n                                    <AlertDialogAction asChild>\n                                        <Button variant=\"destructive\" onClick={toDelete}>Delete</Button>\n                                    </AlertDialogAction>\n                                </AlertDialogFooter>\n                            </AlertDialogContent>\n                        </AlertDialog>\n                    </div>\n                </div>\n            )}\n        </div>\n    );\n};\n"
  },
  {
    "path": "app/src/components/ui/resizable.tsx",
    "content": "import { GripVertical } from \"lucide-react\"\nimport * as ResizablePrimitive from \"react-resizable-panels\"\n\nimport { cn } from \"@/lib/utils\"\n\nconst ResizablePanelGroup = ({\n  className,\n  ...props\n}: React.ComponentProps<typeof ResizablePrimitive.PanelGroup>) => (\n  <ResizablePrimitive.PanelGroup\n    className={cn(\n      \"flex h-full w-full data-[panel-group-direction=vertical]:flex-col\",\n      className\n    )}\n    {...props}\n  />\n)\n\nconst ResizablePanel = ResizablePrimitive.Panel\n\nconst ResizableHandle = ({\n  withHandle,\n  className,\n  ...props\n}: React.ComponentProps<typeof ResizablePrimitive.PanelResizeHandle> & {\n  withHandle?: boolean\n}) => (\n  <ResizablePrimitive.PanelResizeHandle\n    className={cn(\n      \"relative flex w-px items-center justify-center bg-border after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring focus-visible:ring-offset-1 data-[panel-group-direction=vertical]:h-px data-[panel-group-direction=vertical]:w-full data-[panel-group-direction=vertical]:after:left-0 data-[panel-group-direction=vertical]:after:h-1 data-[panel-group-direction=vertical]:after:w-full data-[panel-group-direction=vertical]:after:-translate-y-1/2 data-[panel-group-direction=vertical]:after:translate-x-0 [&[data-panel-group-direction=vertical]>div]:rotate-90\",\n      className\n    )}\n    {...props}\n  >\n    {withHandle && (\n      <div className=\"z-10 flex h-4 w-3 items-center justify-center rounded-sm border bg-border\">\n        <GripVertical className=\"h-2.5 w-2.5\" />\n      </div>\n    )}\n  </ResizablePrimitive.PanelResizeHandle>\n)\n\nexport { ResizablePanelGroup, ResizablePanel, ResizableHandle }\n"
  },
  {
    "path": "app/src/components/ui/scroll-area.tsx",
    "content": "import * as React from \"react\"\nimport * as ScrollAreaPrimitive from \"@radix-ui/react-scroll-area\"\n\nimport { cn } from \"@/lib/utils\"\n\nconst ScrollArea = React.forwardRef<\n  React.ElementRef<typeof ScrollAreaPrimitive.Root>,\n  React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root>\n>(({ className, children, ...props }, ref) => (\n  <ScrollAreaPrimitive.Root\n    ref={ref}\n    className={cn(\"relative overflow-hidden\", className)}\n    {...props}\n  >\n    <ScrollAreaPrimitive.Viewport className=\"h-full w-full rounded-[inherit]\">\n      {children}\n    </ScrollAreaPrimitive.Viewport>\n    <ScrollBar />\n    <ScrollAreaPrimitive.Corner />\n  </ScrollAreaPrimitive.Root>\n))\nScrollArea.displayName = ScrollAreaPrimitive.Root.displayName\n\nconst ScrollBar = React.forwardRef<\n  React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>,\n  React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>\n>(({ className, orientation = \"vertical\", ...props }, ref) => (\n  <ScrollAreaPrimitive.ScrollAreaScrollbar\n    ref={ref}\n    orientation={orientation}\n    className={cn(\n      \"flex touch-none select-none transition-colors\",\n      orientation === \"vertical\" &&\n        \"h-full w-2.5 border-l border-l-transparent p-[1px]\",\n      orientation === \"horizontal\" &&\n        \"h-2.5 flex-col border-t border-t-transparent p-[1px]\",\n      className\n    )}\n    {...props}\n  >\n    <ScrollAreaPrimitive.ScrollAreaThumb className=\"relative flex-1 rounded-full bg-border\" />\n  </ScrollAreaPrimitive.ScrollAreaScrollbar>\n))\nScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName\n\nexport { ScrollArea, ScrollBar }\n"
  },
  {
    "path": "app/src/components/ui/select.tsx",
    "content": "import * as React from \"react\"\nimport * as SelectPrimitive from \"@radix-ui/react-select\"\nimport { Check, ChevronDown, ChevronUp } from \"lucide-react\"\n\nimport { cn } from \"@/lib/utils\"\n\nconst Select = SelectPrimitive.Root\n\nconst SelectGroup = SelectPrimitive.Group\n\nconst SelectValue = SelectPrimitive.Value\n\nconst SelectTrigger = React.forwardRef<\n  React.ElementRef<typeof SelectPrimitive.Trigger>,\n  React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>\n>(({ className, children, ...props }, ref) => (\n  <SelectPrimitive.Trigger\n    ref={ref}\n    className={cn(\n      \"flex h-9 w-full items-center justify-between whitespace-nowrap rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1\",\n      className\n    )}\n    {...props}\n  >\n    {children}\n    <SelectPrimitive.Icon asChild>\n      <ChevronDown className=\"h-4 w-4 opacity-50\" />\n    </SelectPrimitive.Icon>\n  </SelectPrimitive.Trigger>\n))\nSelectTrigger.displayName = SelectPrimitive.Trigger.displayName\n\nconst SelectScrollUpButton = React.forwardRef<\n  React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,\n  React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>\n>(({ className, ...props }, ref) => (\n  <SelectPrimitive.ScrollUpButton\n    ref={ref}\n    className={cn(\n      \"flex cursor-default items-center justify-center py-1\",\n      className\n    )}\n    {...props}\n  >\n    <ChevronUp className=\"h-4 w-4\" />\n  </SelectPrimitive.ScrollUpButton>\n))\nSelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName\n\nconst SelectScrollDownButton = React.forwardRef<\n  React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,\n  React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>\n>(({ className, ...props }, ref) => (\n  <SelectPrimitive.ScrollDownButton\n    ref={ref}\n    className={cn(\n      \"flex cursor-default items-center justify-center py-1\",\n      className\n    )}\n    {...props}\n  >\n    <ChevronDown className=\"h-4 w-4\" />\n  </SelectPrimitive.ScrollDownButton>\n))\nSelectScrollDownButton.displayName =\n  SelectPrimitive.ScrollDownButton.displayName\n\nconst SelectContent = React.forwardRef<\n  React.ElementRef<typeof SelectPrimitive.Content>,\n  React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>\n>(({ className, children, position = \"popper\", ...props }, ref) => (\n  <SelectPrimitive.Portal>\n    <SelectPrimitive.Content\n      ref={ref}\n      className={cn(\n        \"relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2\",\n        position === \"popper\" &&\n          \"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1\",\n        className\n      )}\n      position={position}\n      {...props}\n    >\n      <SelectScrollUpButton />\n      <SelectPrimitive.Viewport\n        className={cn(\n          \"p-1\",\n          position === \"popper\" &&\n            \"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]\"\n        )}\n      >\n        {children}\n      </SelectPrimitive.Viewport>\n      <SelectScrollDownButton />\n    </SelectPrimitive.Content>\n  </SelectPrimitive.Portal>\n))\nSelectContent.displayName = SelectPrimitive.Content.displayName\n\nconst SelectLabel = React.forwardRef<\n  React.ElementRef<typeof SelectPrimitive.Label>,\n  React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>\n>(({ className, ...props }, ref) => (\n  <SelectPrimitive.Label\n    ref={ref}\n    className={cn(\"px-2 py-1.5 text-sm font-semibold\", className)}\n    {...props}\n  />\n))\nSelectLabel.displayName = SelectPrimitive.Label.displayName\n\nconst SelectItem = React.forwardRef<\n  React.ElementRef<typeof SelectPrimitive.Item>,\n  React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>\n>(({ className, children, ...props }, ref) => (\n  <SelectPrimitive.Item\n    ref={ref}\n    className={cn(\n      \"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-2 pr-8 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50\",\n      className\n    )}\n    {...props}\n  >\n    <span className=\"absolute right-2 flex h-3.5 w-3.5 items-center justify-center\">\n      <SelectPrimitive.ItemIndicator>\n        <Check className=\"h-4 w-4\" />\n      </SelectPrimitive.ItemIndicator>\n    </span>\n    <SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>\n  </SelectPrimitive.Item>\n))\nSelectItem.displayName = SelectPrimitive.Item.displayName\n\nconst SelectSeparator = React.forwardRef<\n  React.ElementRef<typeof SelectPrimitive.Separator>,\n  React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>\n>(({ className, ...props }, ref) => (\n  <SelectPrimitive.Separator\n    ref={ref}\n    className={cn(\"-mx-1 my-1 h-px bg-muted\", className)}\n    {...props}\n  />\n))\nSelectSeparator.displayName = SelectPrimitive.Separator.displayName\n\nexport {\n  Select,\n  SelectGroup,\n  SelectValue,\n  SelectTrigger,\n  SelectContent,\n  SelectLabel,\n  SelectItem,\n  SelectSeparator,\n  SelectScrollUpButton,\n  SelectScrollDownButton,\n}\n"
  },
  {
    "path": "app/src/components/ui/separator.tsx",
    "content": "import * as React from \"react\"\nimport * as SeparatorPrimitive from \"@radix-ui/react-separator\"\n\nimport { cn } from \"@/lib/utils\"\n\nconst Separator = React.forwardRef<\n  React.ElementRef<typeof SeparatorPrimitive.Root>,\n  React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>\n>(\n  (\n    { className, orientation = \"horizontal\", decorative = true, ...props },\n    ref\n  ) => (\n    <SeparatorPrimitive.Root\n      ref={ref}\n      decorative={decorative}\n      orientation={orientation}\n      className={cn(\n        \"shrink-0 bg-border\",\n        orientation === \"horizontal\" ? \"h-[1px] w-full\" : \"h-full w-[1px]\",\n        className\n      )}\n      {...props}\n    />\n  )\n)\nSeparator.displayName = SeparatorPrimitive.Root.displayName\n\nexport { Separator }\n"
  },
  {
    "path": "app/src/components/ui/skeleton.tsx",
    "content": "import { cn } from \"@/lib/utils\"\n\nfunction Skeleton({\n  className,\n  ...props\n}: React.HTMLAttributes<HTMLDivElement>) {\n  return (\n    <div\n      className={cn(\"animate-pulse rounded-md bg-primary/10\", className)}\n      {...props}\n    />\n  )\n}\n\nexport { Skeleton }\n"
  },
  {
    "path": "app/src/components/ui/sonner.tsx",
    "content": "import { useTheme } from \"next-themes\"\nimport { Toaster as Sonner } from \"sonner\"\n\ntype ToasterProps = React.ComponentProps<typeof Sonner>\n\nconst Toaster = ({ ...props }: ToasterProps) => {\n  const { theme = \"system\" } = useTheme()\n\n  return (\n    <Sonner\n      theme={theme as ToasterProps[\"theme\"]}\n      className=\"toaster group\"\n      toastOptions={{\n        classNames: {\n          toast:\n            \"group toast group-[.toaster]:bg-background group-[.toaster]:text-foreground group-[.toaster]:border-border group-[.toaster]:shadow-lg\",\n          description: \"group-[.toast]:text-muted-foreground\",\n          actionButton:\n            \"group-[.toast]:bg-primary group-[.toast]:text-primary-foreground\",\n          cancelButton:\n            \"group-[.toast]:bg-muted group-[.toast]:text-muted-foreground\",\n        },\n      }}\n      {...props}\n    />\n  )\n}\n\nexport { Toaster }\n"
  },
  {
    "path": "app/src/components/ui/textarea.tsx",
    "content": "import * as React from \"react\"\n\nimport { cn } from \"@/lib/utils\"\n\nconst Textarea = React.forwardRef<\n  HTMLTextAreaElement,\n  React.ComponentProps<\"textarea\">\n>(({ className, ...props }, ref) => {\n  return (\n    <textarea\n      className={cn(\n        \"flex min-h-[60px] w-full rounded-md border border-input bg-transparent px-3 py-2 text-base shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm\",\n        className\n      )}\n      ref={ref}\n      {...props}\n    />\n  )\n})\nTextarea.displayName = \"Textarea\"\n\nexport { Textarea }\n"
  },
  {
    "path": "app/src/components/ui/tooltip.tsx",
    "content": "import * as React from \"react\"\nimport * as TooltipPrimitive from \"@radix-ui/react-tooltip\"\n\nimport { cn } from \"@/lib/utils\"\n\nconst TooltipProvider = TooltipPrimitive.Provider\n\nconst Tooltip = TooltipPrimitive.Root\n\nconst TooltipTrigger = TooltipPrimitive.Trigger\n\nconst TooltipContent = React.forwardRef<\n  React.ElementRef<typeof TooltipPrimitive.Content>,\n  React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>\n>(({ className, sideOffset = 4, ...props }, ref) => (\n  <TooltipPrimitive.Portal>\n    <TooltipPrimitive.Content\n      ref={ref}\n      sideOffset={sideOffset}\n      className={cn(\n        \"z-50 overflow-hidden rounded-md bg-primary px-3 py-1.5 text-xs text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2\",\n        className\n      )}\n      {...props}\n    />\n  </TooltipPrimitive.Portal>\n))\nTooltipContent.displayName = TooltipPrimitive.Content.displayName\n\nexport { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }\n"
  },
  {
    "path": "app/src/hooks/useCopyToClipboard.ts",
    "content": "import { useCallback, useState } from 'react';\n\ntype CopiedValue = string | null;\n\ntype CopyFn = (text: string) => Promise<boolean>;\n\nexport function useCopyToClipboard(): [CopiedValue, CopyFn] {\n    const [copiedText, setCopiedText] = useState<CopiedValue>(null);\n\n    const copy: CopyFn = useCallback(async (text) => {\n        if (!navigator?.clipboard) {\n            console.warn('Clipboard not supported');\n            return false;\n        }\n\n        // Try to save to clipboard then save it in the state if worked\n        try {\n            await navigator.clipboard.writeText(text);\n            setCopiedText(text);\n            return true;\n        } catch (error) {\n            console.warn('Copy failed', error);\n            setCopiedText(null);\n            return false;\n        }\n    }, []);\n\n    return [copiedText, copy];\n}\n"
  },
  {
    "path": "app/src/hooks/useGeneratorMail.ts",
    "content": "import { useEffect, useState } from 'react';\nimport { getRandomMail } from '@/lib/store';\n\nexport const useGeneratorMail = (): [string, () => void] => {\n    const [address, setAddress] = useState<string>('');\n\n    const generatorNewMail = () => {\n        const mailAddress = getRandomMail(true);\n        setAddress(mailAddress);\n    }\n\n    useEffect(() => {\n        const mailAddress = getRandomMail();\n        setAddress(mailAddress);\n    }, []);\n\n    return [address, generatorNewMail];\n}"
  },
  {
    "path": "app/src/layouts/Layout.astro",
    "content": "---\nimport '@/styles/globals.css';\nimport { SEO } from 'astro-seo';\nconst { pathname, origin } = Astro.url;\nconst resolvedImageWithDomain = `${origin}/image.png`;\nconst canonicalURL = new URL(pathname, origin);\nconst { title, description, image } = Astro.props;\nconst headTitle = title || 'FakeMail';\nconst headDesc =\n    description ||\n    'Free temporary email (temp mail) service. Get a disposable email address instantly. Emails auto-delete after 2 hours.';\nconst ogImage = image || resolvedImageWithDomain;\n---\n\n<!doctype html>\n<html lang='en'>\n    <head>\n        <meta charset='UTF-8' />\n        <meta name='viewport' content='width=device-width' />\n        <link rel='icon' type='image/svg+xml' href='/favicon.svg' />\n        <meta\n            name='keywords'\n            content='fake, temp mail, fake mail, free, temporary, email, disposable, mail, email address'\n        />\n        <meta name='generator' content={Astro.generator} />\n        <SEO\n            title={headTitle}\n            description={headDesc}\n            canonical={canonicalURL}\n            twitter={{\n                creator: '@LiWen563',\n                site: '@LiWen563',\n                card: 'summary_large_image',\n                imageAlt: headTitle,\n                description: headDesc,\n            }}\n            openGraph={{\n                basic: {\n                    url: canonicalURL,\n                    type: 'website',\n                    title: headTitle,\n                    image: ogImage,\n                },\n                image: {\n                    url: ogImage,\n                    alt: headTitle,\n                },\n            }}\n        />\n        <script async src=\"https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-4182383692865291\" crossorigin=\"anonymous\"></script>\n    </head>\n    <body>\n        <slot />\n    </body>\n</html>\n\n<script is:inline>\n    const getThemePreference = () => {\n        if (\n            typeof localStorage !== 'undefined' &&\n            localStorage.getItem('theme')\n        ) {\n            return localStorage.getItem('theme');\n        }\n        return window.matchMedia('(prefers-color-scheme: dark)').matches\n            ? 'dark'\n            : 'light';\n    };\n    const isDark = getThemePreference() === 'dark';\n    document.documentElement.classList[isDark ? 'add' : 'remove']('dark');\n\n    if (typeof localStorage !== 'undefined') {\n        const observer = new MutationObserver(() => {\n            const isDark = document.documentElement.classList.contains('dark');\n            localStorage.setItem('theme', isDark ? 'dark' : 'light');\n        });\n        observer.observe(document.documentElement, {\n            attributes: true,\n            attributeFilter: ['class'],\n        });\n    }\n</script>\n"
  },
  {
    "path": "app/src/lib/demo.ts",
    "content": "export default {\n    status: 'ok',\n    code: 200,\n    msg: 'Mail available',\n    stats: { count: '9832' },\n    mails: [\n        {\n            suffix: '5cc52bcf',\n            recipient: 'men.strip311@fakeact.fun',\n            sender: 'test@gmail.com',\n            name: 'Test User',\n            subject: 'Your verification code is 662144',\n            'content-plain':\n                'Verification Code\\n\\nEnter this code to continue the verification process:\\n662144\\n\\nYour code will remain valid for 10 minutes. Do NOT share with anyone.\\nIf you have any questions, contact support at support@carv.io\\n\\n',\n            'content-plain-formatted':\n                'Verification Code<br><br>Enter this code to continue the verification process:<br>662144<br><br>Your code will remain valid for 10 minutes. Do NOT share with anyone.<br>If you have any questions, contact support at support@carv.io<br><br>',\n            'content-html':'',\n            date: '2025-01-05T14:58:45.000Z',\n        },\n        {\n            suffix: '34e222s',\n            recipient: 'men.strip311@fakeact.fun',\n            sender: 'test@gmail.com',\n            subject: 'Your verification code is 33322222',\n            'content-plain':\n                'Verification Code\\n\\nEnter this code to continue the verification process:\\n33322222\\n\\nYour code will remain valid for 10 minutes. Do NOT share with anyone.\\nIf you have any questions, contact support at support@carv.io\\n\\n',\n            'content-plain-formatted':\n                'Verification Code<br><br>Enter this code to continue the verification process:<br>33322222<br><br>Your code will remain valid for 10 minutes. Do NOT share with anyone.<br>If you have any questions, contact support at support@carv.io<br><br>',\n            'content-html': '',\n            date: '2025-01-06T14:58:45.000Z',\n        },\n    ],\n};\n"
  },
  {
    "path": "app/src/lib/store.ts",
    "content": "import { generate } from 'random-words';\n\nexport const getRandomMail = (isNew: boolean = false) => {\n    const domain = '@fakeact.fun';\n    if (!isNew) {\n        const receivingEmail = window?.localStorage.getItem('receivingEmail');\n        if (receivingEmail && receivingEmail.includes(domain)) {\n            return receivingEmail;\n        }\n    }\n    const words = generate({ exactly: 2, maxLength: 5 });\n    const alt =\n        words[0] + '.' + words[1] + Math.floor(Math.random() * 1000) + domain;\n    window?.localStorage.setItem('receivingEmail', alt);\n    return alt;\n};\n"
  },
  {
    "path": "app/src/lib/utils.ts",
    "content": "import { clsx, type ClassValue } from \"clsx\";\nimport { twMerge } from \"tailwind-merge\";\n\nexport function cn(...inputs: ClassValue[]) {\n  return twMerge(clsx(inputs))\n};"
  },
  {
    "path": "app/src/middleware.ts",
    "content": "import { clerkMiddleware, createRouteMatcher } from '@clerk/astro/server';\n\nconst isProtectedRoute = createRouteMatcher(['/dashboard(.*)', '/settings(.*)']);\n\nexport const onRequest = clerkMiddleware((auth, context) => {\n\n    const { redirectToSignIn, userId } = auth();\n\n    if (!userId && isProtectedRoute(context.request)) {\n        // Add custom logic to run before redirecting\n\n        return redirectToSignIn({\n            returnBackUrl: context.request.url,\n            \n        });\n    }\n}, {\n    publishableKey: import.meta.env.CLERK_PUBLISHABLE_KEY,\n    secretKey: import.meta.env.CLERK_SECRET_KEY,\n    signInUrl: '/sign-in',\n    signUpUrl: '/sign-up',\n});\n"
  },
  {
    "path": "app/src/pages/api/delete.ts",
    "content": "import type { APIRoute, APIContext } from 'astro';\n\nexport const POST: APIRoute = async ({ request, locals }: APIContext) => {\n    const { key } = await request.json();\n    if (!key) {\n        return new Response(JSON.stringify({\n            status: 'bad request',\n            code: 400,\n            msg: \"'key' params required!\",\n        }));\n    }\n    \n    await locals.runtime.env.POST_DB?.delete(key);\n\n    return new Response(\n        JSON.stringify({\n            status: 'ok',\n            code: 200,\n            msg: 'Mail deleted',\n        })\n    );\n};\n"
  },
  {
    "path": "app/src/pages/api/deleteMails.ts",
    "content": "import type { APIRoute, APIContext } from 'astro';\n\nexport const POST: APIRoute = async ({ request, locals }: APIContext) => {\n    const { message_id } = await request.json();\n    if (message_id === undefined || message_id === '') {\n        return new Response(JSON.stringify({\n            status: 'bad request',\n            code: 400,\n            msg: \"'message_id' query required!\",\n        }));\n    }\n    const { userId } = locals.auth();\n    if (!userId) {\n        return new Response(JSON.stringify({\n            status: 'bad request',\n            code: 400,\n            msg: \"please login!\",\n        }));\n    }\n    await locals.runtime.env.MAIL_DB?.batch([\n        locals.runtime.env.MAIL_DB?.prepare('DELETE FROM email_tag_relations WHERE email_id = (SELECT id FROM emails WHERE message_id = ?)').bind(message_id),\n        locals.runtime.env.MAIL_DB?.prepare('DELETE FROM email_status WHERE email_id = (SELECT id FROM emails WHERE message_id = ?) AND user_id = ?').bind(message_id, userId),\n        locals.runtime.env.MAIL_DB?.prepare('DELETE FROM emails WHERE message_id = ?').bind(message_id)\n    ]);\n    return new Response(\n        JSON.stringify({\n            status: 'ok',\n            code: 200,\n            msg: 'Success',\n        })\n    );\n}"
  },
  {
    "path": "app/src/pages/api/generate.ts",
    "content": "import { generate } from 'random-words';\nimport type { APIRoute, APIContext } from 'astro';\n\nexport const POST: APIRoute = async ({ request, locals }: APIContext) => {\n    const domain = '@fakeact.fun';\n    const { remark } = await request.json();\n    if ((remark || '').length > 400) return new Response(JSON.stringify({\n        status: 'bad request',\n        code: 400,\n        msg: \"remark is too long!\",\n    }));\n    const { userId } = locals.auth();\n    if (!userId) {\n        return new Response(JSON.stringify({\n            status: 'bad request',\n            code: 400,\n            msg: \"please login!\",\n        }));\n    }\n    const stmt = locals.runtime.env.MAIL_DB?.prepare('Select count(*) from user_email_addresses where user_id = ?').bind(userId);\n    const returnValue = await stmt.run().catch((e) => {\n        console.error(e);\n        return { results: [] };\n    });\n    const results = returnValue.results as { 'count(*)': number }[];\n    if (results[0]['count(*)'] >= 5) {\n        return new Response(JSON.stringify({\n            status: 'bad request',\n            code: 400,\n            msg: \"You can't create more than 5 accounts!\",\n        }));\n    }\n    const words = generate({ exactly: 2, maxLength: 5 });\n    const alt = words[0] + '.' + words[1] + Math.floor(Math.random() * 1000) + domain;\n    const stmt2 = locals.runtime.env.MAIL_DB?.prepare('Insert into user_email_addresses (user_id, email_address, alias) values (?, ?, ?)').bind(userId, alt, remark);\n    const msg = await stmt2.run().then(() => {\n        return {\n            status: 'ok',\n            code: 200,\n            msg: 'Success',\n        }\n    }).catch((e) => {\n        console.error(e);\n        return {\n            status: 'bad request',\n            code: 400,\n            msg: 'Something wrong or exists, please try again!',\n        }\n    });\n    return new Response(JSON.stringify(msg));\n};"
  },
  {
    "path": "app/src/pages/api/get.ts",
    "content": "import type { APIRoute, APIContext } from 'astro';\nimport demo from '@/lib/demo';\n\nfunction validateString(str: string): boolean {\n    const regex = /^[a-zA-Z0-9]+\\.[a-zA-Z0-9]+\\d{3}$/;\n    return regex.test(str);\n}\n\nexport const GET: APIRoute = async ({ request, locals }: APIContext) => {\n    const address = new URL(request.url).searchParams.get('address');\n    if (import.meta.env.DEV) {\n        return new Response(JSON.stringify(demo));\n    }\n    if (address === undefined || address === '') {\n        return new Response(JSON.stringify({\n            status: 'bad request',\n            code: 400,\n            msg: \"'address' query required!\",\n        }));\n    }\n\n    const addresName = (address || '').split('@')[0];\n    if (!validateString(addresName)) {\n        return new Response(JSON.stringify({\n            status: 'bad request',\n            code: 400,\n            msg: \"'address' query invalid!\",\n        }));\n    }\n\n    // get mails with prefix (user@example.com) - suffix (-8dh2m901) acts as identifier\n    const mailKeys = await locals.runtime.env.POST_DB?.get(`${address}-keys`);\n    const statsCount = await locals.runtime.env.POST_DB?.get('stats-count');\n    let mailKeysArr: string[] = [];\n    try {\n        mailKeysArr = JSON.parse(mailKeys || '[]');\n    } catch (error) {\n        //\n    }\n\n    // if no emails are stored under the prefix key, return empty json\n    if (!mailKeys || mailKeysArr.length === 0) {\n        return new Response(JSON.stringify({\n            status: 'ok',\n            code: 200,\n            msg: 'No available emails',\n            stats: {\n                count: statsCount,\n            },\n            mails: [],\n        }));\n    }\n\n    // create array of received mails\n    let mails = [];\n    for (const key of mailKeysArr) {\n        const mail_res = await locals.runtime.env.POST_DB.get(`${address}-${key}`);\n        // convert string back to JSON\n        // @ts-ignore\n        if (mail_res) mails.push(JSON.parse(mail_res));\n    }\n\n    return new Response(JSON.stringify({\n        status: 'ok',\n        code: 200,\n        msg: 'Mail available',\n        stats: {\n            count: statsCount,\n        },\n        mails,\n    }));\n};\n"
  },
  {
    "path": "app/src/pages/api/getMails.ts",
    "content": "import type { APIRoute, APIContext } from 'astro';\n\nexport const GET: APIRoute = async ({ request, locals }: APIContext) => {\n    const id = new URL(request.url).searchParams.get('id');\n    if (id === undefined || id === '') {\n        return new Response(JSON.stringify({\n            status: 'bad request',\n            code: 400,\n            msg: \"'id' query required!\",\n        }));\n    }\n    const { userId } = locals.auth();\n    if (!userId) {\n        // redirect to login page\n        return new Response(JSON.stringify({\n            status: 'bad request',\n            code: 502,\n            msg: \"please login!\",\n        }));\n    }\n    const stmt = locals.runtime.env.MAIL_DB?.prepare('Select id from user_email_addresses where user_id = ? and id = ?').bind(userId, id);\n    const returnValue = await stmt.run().catch((e) => {\n        console.error(e);\n        return { results: [] };\n    });\n    if (returnValue.results.length === 0) {\n        return new Response(JSON.stringify({\n            status: 'bad request',\n            code: 400,\n            msg: \"Email not found!\",\n        }));\n    }\n    const stmt2 = locals.runtime.env.MAIL_DB?.prepare('SELECT e.message_id, e.subject, e.cc, e.sender_name, e.sender, e.body_text, e.body_html, e.received_at, es.is_read FROM emails e JOIN email_status es ON es.email_id = e.id WHERE user_email_id = ? ORDER BY received_at DESC').bind(id);\n    const returnValue2 = await stmt2.run().catch((e) => {\n        console.error(e);\n        return { results: [] };\n    });\n    return new Response(JSON.stringify({\n        status: 'ok',\n        code: 200,\n        msg: 'Mail available',\n        mails: returnValue2.results.map(row => {\n            return {\n                message_id: row.message_id as string,\n                subject: row.subject as string,\n                sender: row.sender as string,\n                senderName: row.sender_name as string,\n                cc: row.cc as string,\n                content: (row.body_text || row.body_html) as string,\n                received_at: row.received_at as string,\n                is_read: row.is_read as number\n            }\n        })\n    }));\n};"
  },
  {
    "path": "app/src/pages/api/remarkMail.ts",
    "content": "import type { APIRoute, APIContext } from 'astro';\n\nexport const POST: APIRoute = async ({ request, locals }: APIContext) => {\n    const { id, remark } = await request.json();\n    if (!id) return new Response(JSON.stringify({\n        status: 'bad request',\n        code: 400,\n        msg: \"id is required!\",\n    }));\n    if ((remark || '').length > 400) return new Response(JSON.stringify({\n        status: 'bad request',\n        code: 400,\n        msg: \"remark is too long!\",\n    }));\n    const { userId } = locals.auth();\n    if (!userId) {\n        return new Response(JSON.stringify({\n            status: 'bad request',\n            code: 400,\n            msg: \"please login!\",\n        }));\n    }\n    const stmt = locals.runtime.env.MAIL_DB?.prepare('Update user_email_addresses set alias = ? where user_id = ? and id = ?').bind(remark, userId, id);\n    const msg = await stmt.run().then(() => {\n        return {\n            status: 'ok',\n            code: 200,\n            msg: 'Success',\n        }\n    }).catch((e) => {\n        console.error(e);\n        return {\n            status: 'bad request',\n            code: 400,\n            msg: 'Something wrong or exists, please try again!',\n        }\n    });\n    return new Response(JSON.stringify(msg));\n};"
  },
  {
    "path": "app/src/pages/api/updateStatus.ts",
    "content": "import type { APIRoute, APIContext } from 'astro';\n\nexport const POST: APIRoute = async ({ request, locals }: APIContext) => {\n    const { message_id, is_read } = await request.json();\n    if (message_id === undefined || message_id === '') {\n        return new Response(JSON.stringify({\n            status: 'bad request',\n            code: 400,\n            msg: \"'message_id' query required!\",\n        }));\n    }\n    const { userId } = locals.auth();\n    if (!userId) {\n        return new Response(JSON.stringify({\n            status: 'bad request',\n            code: 400,\n            msg: \"please login!\",\n        }));\n    }\n    const readStatus = +is_read === 0 ? 0 : 1;\n    const stmt = locals.runtime.env.MAIL_DB?.prepare(`\nINSERT OR REPLACE INTO email_status \n(email_id, user_id, is_read, created_at, updated_at)\nSELECT \n    e.id,\n    ?,\n    ?,\n    COALESCE((SELECT created_at FROM email_status WHERE email_id = e.id AND user_id = ?), CURRENT_TIMESTAMP),\n    CURRENT_TIMESTAMP\nFROM emails e\nJOIN user_email_addresses uea ON e.user_email_id = uea.id\nWHERE e.message_id = ? \nAND uea.user_id = ?\n    `).bind(userId, readStatus, userId, message_id, userId);\n    await stmt.run().catch(error => console.log(error));\n    return new Response(\n        JSON.stringify({\n            status: 'ok',\n            code: 200,\n            msg: 'Success',\n        })\n    );\n}"
  },
  {
    "path": "app/src/pages/dashboard.astro",
    "content": "---\nimport Layout from '@/layouts/Layout.astro';\nimport Bar from '@/components/Bar.astro';\nimport Mail from '@/components/mail/Mail';\nimport type { MailsList } from '@/components/mail/data';\nconst layout = Astro.cookies.get(\"react-resizable-panels:layout:mail\");\nconst collapsed = Astro.cookies.get(\"react-resizable-panels:collapsed\");\nconst defaultLayout = layout ? JSON.parse(layout.value) : undefined;\nconst defaultCollapsed = collapsed ? JSON.parse(collapsed.value) : undefined;\nconst { userId } = Astro.locals.auth();\nconst accounts: {id: number; email_address: string}[] = [];\nconst mails: MailsList[] = [];\nif(Astro.locals.runtime) {\n    const { env } = Astro.locals.runtime;\n    const stmt = env.MAIL_DB.prepare(\"SELECT id,email_address FROM user_email_addresses WHERE user_id = ?\").bind(userId);\n    const returnValue = await stmt.run().catch((e) => {\n        console.error(e);\n        return { results: [] };\n    });\n    if (returnValue.results.length) {\n        for(const row of returnValue.results) {\n            accounts.push({\n                id: row.id as number,\n                email_address: row.email_address as string\n            });\n        }\n        const stmt2 = env.MAIL_DB.prepare(\"SELECT e.message_id, e.subject, e.sender, e.sender_name, e.cc, e.body_text, e.body_html, e.received_at, es.is_read FROM emails e JOIN email_status es ON es.email_id = e.id WHERE user_email_id = ? ORDER BY received_at DESC\").bind(accounts[0].id);\n        const returnMails = await stmt2.run().catch((e) => {\n            console.error(e);\n            return { results: [] };\n        });\n        for(const row of returnMails.results) {\n            mails.push({\n                message_id: row.message_id as string,\n                subject: row.subject as string,\n                sender: row.sender as string,\n                senderName: row.sender_name as string,\n                cc: row.cc as string,\n                content: (row.body_text || row.body_html) as string,\n                received_at: row.received_at as string,\n                is_read: row.is_read as number\n            });\n        }\n    }\n}\n---\n\n<Layout>\n    <div class=\"flex flex-col h-screen\">\n        <Bar />\n        {accounts.length === 0 ?\n            <div class=\"flex-1 flex items-center justify-center\">\n                <div class=\"text-center\">\n                    <h2 class=\"text-2xl font-bold\">No temp addresses yet</h2>\n                    <p class=\"text-gray-500\">Go to Settings to create your first disposable email address. You can then receive and read mail here.</p>\n                    <a href=\"/settings\" class=\"text-blue-500 hover:underline\">Add email address</a>\n                </div>\n            </div>:\n            <Mail\n                accounts={accounts}\n                mails={mails}\n                defaultLayout={defaultLayout}\n                defaultCollapsed={defaultCollapsed}\n                navCollapsedSize={4}\n                client:load\n            />\n        }\n    </div>\n</Layout>"
  },
  {
    "path": "app/src/pages/index.astro",
    "content": "---\nimport Header from '../components/Header.astro';\nimport Main from '../components/Main.astro';\nimport Footer from '../components/Footer.astro';\nimport Layout from '@/layouts/Layout.astro';\n---\n\n<Layout\n    title=\"FakeMail - Free temporary email | Disposable email generator\"\n    description=\"FakeMail is a free temporary email (temp mail) service. Get a disposable email address instantly. Emails are automatically deleted after 2 hours. No sign-up required for one-time use; sign in to manage multiple addresses.\"\n>\n    <Header />\n    <Main />\n    <Footer />\n</Layout>\n\n<script type=\"application/ld+json\" set:html={JSON.stringify({\n    \"@context\": \"https://schema.org\",\n    \"@type\": \"WebApplication\",\n    \"name\": \"Fake Mail\",\n    \"description\": \"Fake email generator, delete emails 2 hours after receiving\",\n    \"operatingSystem\": \"ALL\",\n    \"applicationCategory\": \"BrowserApplication\",\n    \"browserRequirements\": \"requires HTML5 support\",\n    \"aggregateRating\": {\n        \"@type\": \"AggregateRating\",\n        \"ratingValue\": \"4.6\",\n        \"ratingCount\": \"4872\",\n        \"bestRating\": \"5\"\n    },\n    \"offers\": {\n        \"@type\": \"Offer\",\n        \"price\": \"0\",\n        \"priceCurrency\": \"USD\"\n    }\n})}/>\n"
  },
  {
    "path": "app/src/pages/settings.astro",
    "content": "---\nimport Layout from '@/layouts/Layout.astro';\nimport Bar from '@/components/Bar.astro';\nimport { ArrowLeft } from 'lucide-react';\nimport {Accounts} from '@/components/mail/Accounts';\nconst { userId } = Astro.locals.auth();\nconst accounts: {id: number; email_address: string; alias: string; created_at: string;}[] = [];\nif(Astro.locals.runtime) {\n    const { env } = Astro.locals.runtime;\n    const stmt = env.MAIL_DB.prepare(\"SELECT id,email_address,alias,created_at FROM user_email_addresses WHERE user_id = ?\").bind(userId);\n    const returnValue = await stmt.run().catch((e) => {\n        console.error(e);\n        return { results: [] };\n    });\n    if (returnValue.results.length) {\n        for(const row of returnValue.results) {\n            accounts.push({\n                id: row.id as number,\n                email_address: row.email_address as string,\n                alias: row.alias as string,\n                created_at: row.created_at as string\n            });\n        }\n    }\n}\n---\n\n<Layout>\n    <Bar />\n    <div class=\"container mx-auto\">\n        <div class=\"p-4\">\n            <a\n                href=\"/dashboard\"\n                title=\"Dashboard\"\n                class=\"inline-flex gap-1 mb-2 items-center border dark:border-gray-500 py-1.5 px-3 rounded-md text-xs bg-gray-700 dark:bg-gray-950 text-white hover:bg-gray-800 dark:hover:bg-gray-700\"\n            ><ArrowLeft size={16} />Back to Inbox</a>\n            <h1 class=\"text-2xl font-bold\">Settings</h1>\n            <p class=\"text-sm text-gray-500\">Manage your email addresses and notes.</p>\n        </div>\n        <div class=\"p-4\">\n            <Accounts client:load list={accounts} />\n        </div>\n    </div>\n</Layout>"
  },
  {
    "path": "app/src/pages/sign-in.astro",
    "content": "---\nimport { SignIn } from '@clerk/astro/components';\nimport Layout from '@/layouts/Layout.astro';\n---\n\n<Layout>\n    <div class=\"min-h-screen\" style=`background-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32' width='32' height='32' fill='none' stroke='rgb(148 163 184 / 0.05)'%3e%3cpath d='M0 .5H31.5V32'/%3e%3c/svg%3e\")`>\n        <div class=\"bg-gray-800/90\">\n            <div class=\"container mx-auto py-10 px-4 text-white text-center\">\n                <h1 class=\"font-semibold text-xl mb-2\">Login to keep data permanently</h1>\n                <p class=\"text-gray-400 text-sm\">You can record multiple email addresses and use them for free</p>\n            </div>\n        </div>\n        <div class=\"bg-gradient-to-b from-gray-800/90 from-50% to-50% to-transparent\">\n            <div class='container mx-auto flex flex-col items-center'>\n                <SignIn path=\"/sign-in\" />\n            </div>\n        </div>\n    </div>\n</Layout>\n"
  },
  {
    "path": "app/src/pages/sign-up.astro",
    "content": "---\nimport { SignUp } from '@clerk/astro/components';\nimport Layout from '@/layouts/Layout.astro';\n---\n\n<Layout>\n    <div class=\"min-h-screen\" style=`background-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32' width='32' height='32' fill='none' stroke='rgb(148 163 184 / 0.05)'%3e%3cpath d='M0 .5H31.5V32'/%3e%3c/svg%3e\")`>\n        <div class=\"bg-gray-800/90\">\n            <div class=\"container mx-auto py-10 px-4 text-white text-center\">\n                <h1 class=\"font-semibold text-xl mb-2\">Register to keep data permanently</h1>\n                <p class=\"text-gray-400 text-sm\">You can record multiple email addresses and use them for free</p>\n            </div>\n        </div>\n        <div class=\"bg-gradient-to-b from-gray-800/90 from-50% to-50% to-transparent\">\n            <div class='container mx-auto flex flex-col items-center'>\n                <SignUp path=\"/sign-up\" />\n            </div>\n        </div>\n    </div>\n</Layout>\n"
  },
  {
    "path": "app/src/styles/globals.css",
    "content": "@tailwind base;\n@tailwind components;\n@tailwind utilities;\n@layer base {\n  :root {\n    --background: 0 0% 100%;\n    --foreground: 222.2 84% 4.9%;\n    --card: 0 0% 100%;\n    --card-foreground: 222.2 84% 4.9%;\n    --popover: 0 0% 100%;\n    --popover-foreground: 222.2 84% 4.9%;\n    --primary: 222.2 47.4% 11.2%;\n    --primary-foreground: 210 40% 98%;\n    --secondary: 210 40% 96.1%;\n    --secondary-foreground: 222.2 47.4% 11.2%;\n    --muted: 210 40% 96.1%;\n    --muted-foreground: 215.4 16.3% 46.9%;\n    --accent: 210 40% 96.1%;\n    --accent-foreground: 222.2 47.4% 11.2%;\n    --destructive: 0 84.2% 60.2%;\n    --destructive-foreground: 210 40% 98%;\n    --border: 214.3 31.8% 91.4%;\n    --input: 214.3 31.8% 91.4%;\n    --ring: 222.2 84% 4.9%;\n    --chart-1: 12 76% 61%;\n    --chart-2: 173 58% 39%;\n    --chart-3: 197 37% 24%;\n    --chart-4: 43 74% 66%;\n    --chart-5: 27 87% 67%;\n    --radius: 0.5rem\n  }\n  .dark {\n    --background: 222.2 84% 4.9%;\n    --foreground: 210 40% 98%;\n    --card: 222.2 84% 4.9%;\n    --card-foreground: 210 40% 98%;\n    --popover: 222.2 84% 4.9%;\n    --popover-foreground: 210 40% 98%;\n    --primary: 210 40% 98%;\n    --primary-foreground: 222.2 47.4% 11.2%;\n    --secondary: 217.2 32.6% 17.5%;\n    --secondary-foreground: 210 40% 98%;\n    --muted: 217.2 32.6% 17.5%;\n    --muted-foreground: 215 20.2% 65.1%;\n    --accent: 217.2 32.6% 17.5%;\n    --accent-foreground: 210 40% 98%;\n    --destructive: 0 62.8% 30.6%;\n    --destructive-foreground: 210 40% 98%;\n    --border: 217.2 32.6% 17.5%;\n    --input: 217.2 32.6% 17.5%;\n    --ring: 212.7 26.8% 83.9%;\n    --chart-1: 220 70% 50%;\n    --chart-2: 160 60% 45%;\n    --chart-3: 30 80% 55%;\n    --chart-4: 280 65% 60%;\n    --chart-5: 340 75% 55%\n  }\n}\n@layer base {\n  * {\n    @apply border-border;\n  }\n  body {\n    @apply bg-background text-foreground;\n  }\n}\n"
  },
  {
    "path": "app/tailwind.config.mjs",
    "content": "/** @type {import('tailwindcss').Config} */\nexport default {\n    darkMode: ['class'],\n    content: ['./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}'],\n\ttheme: {\n    \textend: {\n    \t\tborderRadius: {\n    \t\t\tlg: 'var(--radius)',\n    \t\t\tmd: 'calc(var(--radius) - 2px)',\n    \t\t\tsm: 'calc(var(--radius) - 4px)'\n    \t\t},\n    \t\tcolors: {\n    \t\t\tbackground: 'hsl(var(--background))',\n    \t\t\tforeground: 'hsl(var(--foreground))',\n    \t\t\tcard: {\n    \t\t\t\tDEFAULT: 'hsl(var(--card))',\n    \t\t\t\tforeground: 'hsl(var(--card-foreground))'\n    \t\t\t},\n    \t\t\tpopover: {\n    \t\t\t\tDEFAULT: 'hsl(var(--popover))',\n    \t\t\t\tforeground: 'hsl(var(--popover-foreground))'\n    \t\t\t},\n    \t\t\tprimary: {\n    \t\t\t\tDEFAULT: 'hsl(var(--primary))',\n    \t\t\t\tforeground: 'hsl(var(--primary-foreground))'\n    \t\t\t},\n    \t\t\tsecondary: {\n    \t\t\t\tDEFAULT: 'hsl(var(--secondary))',\n    \t\t\t\tforeground: 'hsl(var(--secondary-foreground))'\n    \t\t\t},\n    \t\t\tmuted: {\n    \t\t\t\tDEFAULT: 'hsl(var(--muted))',\n    \t\t\t\tforeground: 'hsl(var(--muted-foreground))'\n    \t\t\t},\n    \t\t\taccent: {\n    \t\t\t\tDEFAULT: 'hsl(var(--accent))',\n    \t\t\t\tforeground: 'hsl(var(--accent-foreground))'\n    \t\t\t},\n    \t\t\tdestructive: {\n    \t\t\t\tDEFAULT: 'hsl(var(--destructive))',\n    \t\t\t\tforeground: 'hsl(var(--destructive-foreground))'\n    \t\t\t},\n    \t\t\tborder: 'hsl(var(--border))',\n    \t\t\tinput: 'hsl(var(--input))',\n    \t\t\tring: 'hsl(var(--ring))',\n    \t\t\tchart: {\n    \t\t\t\t'1': 'hsl(var(--chart-1))',\n    \t\t\t\t'2': 'hsl(var(--chart-2))',\n    \t\t\t\t'3': 'hsl(var(--chart-3))',\n    \t\t\t\t'4': 'hsl(var(--chart-4))',\n    \t\t\t\t'5': 'hsl(var(--chart-5))'\n    \t\t\t}\n    \t\t}\n    \t}\n    },\n\tplugins: [require(\"tailwindcss-animate\")],\n}\n"
  },
  {
    "path": "app/tsconfig.json",
    "content": "{\n  \"extends\": \"astro/tsconfigs/strict\",\n  \"include\": [\n    \".astro/types.d.ts\",\n    \"**/*\"\n  ],\n  \"exclude\": [\n    \"dist\"\n  ],\n  \"compilerOptions\": {\n    \"jsx\": \"react-jsx\",\n    \"jsxImportSource\": \"react\",\n    \"baseUrl\": \".\",\n    \"paths\": {\n      \"@/*\": [\n        \"./src/*\"\n      ]\n    }\n  }\n}"
  },
  {
    "path": "app/wrangler.example.toml",
    "content": "name = \"fakemail\"\nmain = \"./dist/_worker.js\"\ncompatibility_date = \"2025-01-14\"\ncompatibility_flags = [ \"nodejs_compat\" ]\nkv_namespaces = [\n    { binding = \"POST_DB\", id = \"b1002ec7a3d74b9cb64504ea6d6a5586\", preview_id = \"238c3081e8ae49f091c08df88cf6c079\" },\n]\n\nd1_databases = [\n    { binding = \"MAIL_DB\", database_name = \"fake-mail\", database_id = \"852bdefb-ec72-4589-8de3-f49bb2d01883\", preview_database_id = \"686ae900-aaea-4c69-8499-b0c8ea8c960d\"},\n]\n\n[vars]\nPUBLIC_TURNSTILE_SITE_KEY=\"0x4AAAAAAA4rlXXIP3FANjmG\"\nSECRET_KEY=\"xxx\"\nPUBLIC_CLERK_PUBLISHABLE_KEY=\"pk_test_xxxx\"\nCLERK_SECRET_KEY=\"sk_test_xxxx\"\nPUBLIC_CLERK_SIGN_IN_URL=\"/sign-in\"\nPUBLIC_CLERK_SIGN_UP_URL=\"/sign-up\"\n\n\n# npx wrangler d1 create fake-mail\n# npx wrangler d1 execute fake-mail --local --file=./email-platform-schema.sql\n# npx wrangler d1 execute fake-mail --local --command=\"SELECT COUNT(*) FROM user_email_addresses\"\n"
  },
  {
    "path": "mailbox/package.json",
    "content": "{\n  \"name\": \"mailbox\",\n  \"version\": \"1.0.0\",\n  \"main\": \"index.js\",\n  \"scripts\": {\n    \"start\": \"wrangler dev\",\n    \"deploy\": \"wrangler deploy\",\n    \"test\": \"echo \\\"Error: no test specified\\\" && exit 1\"\n  },\n  \"keywords\": [],\n  \"author\": \"\",\n  \"license\": \"MIT\",\n  \"description\": \"\",\n  \"dependencies\": {\n    \"postal-mime\": \"^2.3.2\"\n  },\n  \"devDependencies\": {\n    \"@cloudflare/workers-types\": \"^4.20241230.0\",\n    \"typescript\": \"^5.7.2\",\n    \"wrangler\": \"^3.99.0\"\n  }\n}\n"
  },
  {
    "path": "mailbox/src/index.ts",
    "content": "// @ts-ignore\nimport PostalMime from 'postal-mime';\nimport insertMails from './insertMails';\n\nexport interface Env {\n    POST_DB: KVNamespace;\n    MAIL_DB: D1Database;\n}\n\ninterface AccoutAddress {\n    id: number;\n    user_id: string;\n}\n\nexport default {\n    async email(message: ForwardableEmailMessage, env: Env) {\n        // parse ReadableStream message to email\n        const parser = new PostalMime();\n        const body = await new Response(message.raw).arrayBuffer();\n        const email = await parser.parse(body);\n\n        // count email for statistics\n        let prev_count = await env.POST_DB.get('stats-count');\n        if (prev_count === null) {\n            prev_count = '0';\n        }\n        await env.POST_DB.put('stats-count', String(parseInt(prev_count) + 1));\n\n        const sender = email.from.address;\n        const senderName = email.from.name;\n        const recipient = email.to?.length ? email.to[0].address : '';\n\n        if (!recipient) return;\n\n        // generate random string (len = 8)\n        const suffix = Math.random().toString(16).slice(2, 10);\n\n        // get D1 email address\n        const stmt = env.MAIL_DB.prepare('SELECT id, user_id FROM user_email_addresses WHERE email_address = ?').bind(recipient);\n        const returnValue = await stmt.run().catch((e) => {\n            console.error(e);\n            return { results: [] as AccoutAddress[] };\n        });\n\n        if (returnValue.results.length > 0) {\n            // if email exists in D1, insert email to D1\n            const { id, user_id } = returnValue.results[0] as AccoutAddress;\n            await insertMails({\n                mail_id: id,\n                user_id: user_id,\n                message_id: email.messageId || `${recipient}-${suffix}`,\n                subject: email.subject || '',\n                sender: sender || '',\n                senderName: senderName || '',\n                recipient,\n                content_type: 'text/html',\n                body_text: email.text || '',\n                body_html: email.html || '',\n                received_at: email.date || new Date().toISOString(),\n                cc: email.cc?.map((cc) => cc.address).join(',') || '',\n                bcc: email.bcc?.map((bcc) => bcc.address).join(',') || '',\n            }, env);\n            return;\n        }\n\n        let keys = await env.POST_DB.get(`${recipient}-keys`);\n        if (!keys) {\n            keys = JSON.stringify([suffix]);\n        } else {\n            const _keys = JSON.parse(keys);\n            _keys.push(suffix);\n            keys = JSON.stringify(_keys);\n        }\n        await env.POST_DB.put(`${recipient}-keys`, keys, {\n            expirationTtl: 7200,\n        });\n        // make key address followed by suffix (user@example.com-8dh2m901)\n        // suffix acts as the key, while the email is used for assignment\n        const key = recipient + '-' + suffix;\n\n        let formatted_content = email.text?.replaceAll('\\n', '<br>');\n\n        // for an example email JSON see example.json\n        const data = {\n            suffix: suffix,\n            recipient: recipient,\n            sender: sender,\n            name: senderName,\n            subject: email.subject,\n            'content-plain': email.text,\n            'content-plain-formatted': formatted_content,\n            'content-html': email.html,\n            date: email.date,\n        };\n\n        await env.POST_DB.put(key, JSON.stringify(data), {\n            expirationTtl: 7200,\n        });\n    },\n};\n"
  },
  {
    "path": "mailbox/src/insertMails.ts",
    "content": "interface Env {\n    MAIL_DB: D1Database;\n}\n\ninterface EmailData {\n    mail_id: number;\n    user_id: string;\n    message_id: string;\n    subject: string;\n    sender: string;\n    senderName: string;\n    recipient: string;\n    content_type: string;\n    body_text: string;\n    body_html: string;\n    received_at: string;\n    cc: string;\n    bcc: string;\n}\n\nconst insertMails = async (emailData: EmailData, env: Env) => {\n    try {\n        // 1. insert emails table\n        const emailResult = await env.MAIL_DB.prepare(`\n            INSERT INTO emails (message_id, user_email_id, subject, sender, sender_name, recipient, content_type, body_text, body_html, received_at, cc, bcc) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)\n        `).bind(\n            emailData.message_id,\n            emailData.mail_id,\n            emailData.subject,\n            emailData.sender,\n            emailData.senderName,\n            emailData.recipient,\n            emailData.content_type,\n            emailData.body_text,\n            emailData.body_html,\n            emailData.received_at,\n            emailData.cc,\n            emailData.bcc\n        ).run();\n\n        // 2. get email_id\n        const emailId = emailResult.meta.last_row_id;\n\n        // 3. insert email_status table\n        await env.MAIL_DB.prepare(`\n            INSERT INTO email_status (email_id, user_id, is_read, is_starred, is_archived) VALUES (?, ?, 0, 0, 0)\n        `).bind(emailId, emailData.user_id).run();\n\n        return { success: true, emailId };\n    } catch (error) {\n        console.error('Failed to insert email:', error);\n    }\n};\n\nexport default insertMails;"
  },
  {
    "path": "mailbox/tsconfig.json",
    "content": "{\n    \"compilerOptions\": {\n        \"target\": \"es2021\",\n        \"lib\": [\"es2021\"],\n        \"jsx\": \"react\",\n        \"module\": \"es2022\",\n        \"moduleResolution\": \"node\",\n        \"types\": [\"@cloudflare/workers-types\"],\n        \"resolveJsonModule\": true,\n        \"allowJs\": true,\n        \"checkJs\": false,\n        \"noEmit\": true,\n        \"isolatedModules\": true,\n        \"allowSyntheticDefaultImports\": true,\n        \"forceConsistentCasingInFileNames\": true,\n        \"strict\": true,\n        \"skipLibCheck\": true\n    }\n}\n"
  },
  {
    "path": "mailbox/wrangler.toml",
    "content": "name = \"temp-mail\"\nmain = \"src/index.ts\"\ncompatibility_date = \"2024-11-11\"\ncompatibility_flags = [ \"nodejs_compat\" ]\nkv_namespaces = [\n    { binding = \"POST_DB\", id = \"b1002ec7a3d74b9cb64504ea6d6a5586\", preview_id = \"238c3081e8ae49f091c08df88cf6c079\" },\n]\n\nd1_databases = [\n    { binding = \"MAIL_DB\", database_name = \"fake-mail\", database_id = \"852bdefb-ec72-4589-8de3-f49bb2d01883\", preview_database_id = \"686ae900-aaea-4c69-8499-b0c8ea8c960d\"},\n]\n"
  }
]