Full Code of CH563/fakemail for AI

main d6cabf7f4a33 cached
73 files
158.4 KB
40.4k tokens
53 symbols
1 requests
Download .txt
Repository: CH563/fakemail
Branch: main
Commit: d6cabf7f4a33
Files: 73
Total size: 158.4 KB

Directory structure:
gitextract_oaqzapim/

├── .gitignore
├── LICENSE
├── README.md
├── app/
│   ├── .gitignore
│   ├── .vscode/
│   │   ├── extensions.json
│   │   └── launch.json
│   ├── README.md
│   ├── astro.config.mjs
│   ├── components.json
│   ├── email-platform-schema.sql
│   ├── env.d.ts
│   ├── package.json
│   ├── public/
│   │   └── robots.txt
│   ├── src/
│   │   ├── components/
│   │   │   ├── Bar.astro
│   │   │   ├── Container.tsx
│   │   │   ├── Footer.astro
│   │   │   ├── Generator.tsx
│   │   │   ├── Header.astro
│   │   │   ├── Lists.tsx
│   │   │   ├── Main.astro
│   │   │   ├── ModeToggle.tsx
│   │   │   ├── mail/
│   │   │   │   ├── AccountSwitcher.tsx
│   │   │   │   ├── Accounts.tsx
│   │   │   │   ├── Mail.tsx
│   │   │   │   ├── MailDisplay.tsx
│   │   │   │   ├── MailList.tsx
│   │   │   │   ├── Nav.tsx
│   │   │   │   ├── data.tsx
│   │   │   │   └── useMail.ts
│   │   │   └── ui/
│   │   │       ├── alert-dialog.tsx
│   │   │       ├── avatar.tsx
│   │   │       ├── badge.tsx
│   │   │       ├── button.tsx
│   │   │       ├── dialog.tsx
│   │   │       ├── dropdown-menu.tsx
│   │   │       ├── input.tsx
│   │   │       ├── mail-card.tsx
│   │   │       ├── resizable.tsx
│   │   │       ├── scroll-area.tsx
│   │   │       ├── select.tsx
│   │   │       ├── separator.tsx
│   │   │       ├── skeleton.tsx
│   │   │       ├── sonner.tsx
│   │   │       ├── textarea.tsx
│   │   │       └── tooltip.tsx
│   │   ├── hooks/
│   │   │   ├── useCopyToClipboard.ts
│   │   │   └── useGeneratorMail.ts
│   │   ├── layouts/
│   │   │   └── Layout.astro
│   │   ├── lib/
│   │   │   ├── demo.ts
│   │   │   ├── store.ts
│   │   │   └── utils.ts
│   │   ├── middleware.ts
│   │   ├── pages/
│   │   │   ├── api/
│   │   │   │   ├── delete.ts
│   │   │   │   ├── deleteMails.ts
│   │   │   │   ├── generate.ts
│   │   │   │   ├── get.ts
│   │   │   │   ├── getMails.ts
│   │   │   │   ├── remarkMail.ts
│   │   │   │   └── updateStatus.ts
│   │   │   ├── dashboard.astro
│   │   │   ├── index.astro
│   │   │   ├── settings.astro
│   │   │   ├── sign-in.astro
│   │   │   └── sign-up.astro
│   │   └── styles/
│   │       └── globals.css
│   ├── tailwind.config.mjs
│   ├── tsconfig.json
│   └── wrangler.example.toml
└── mailbox/
    ├── package.json
    ├── src/
    │   ├── index.ts
    │   └── insertMails.ts
    ├── tsconfig.json
    └── wrangler.toml

================================================
FILE CONTENTS
================================================

================================================
FILE: .gitignore
================================================
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*
.wrangler

# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json

# Runtime data
pids
*.pid
*.seed
*.pid.lock

# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov

# Coverage directory used by tools like istanbul
coverage
*.lcov

# nyc test coverage
.nyc_output

# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt

# Bower dependency directory (https://bower.io/)
bower_components

# node-waf configuration
.lock-wscript

# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release

# Dependency directories
node_modules/
jspm_packages/

# Snowpack dependency directory (https://snowpack.dev/)
web_modules/

# TypeScript cache
*.tsbuildinfo

# Optional npm cache directory
.npm

# Optional eslint cache
.eslintcache

# Optional stylelint cache
.stylelintcache

# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/

# Optional REPL history
.node_repl_history

# Output of 'npm pack'
*.tgz

# Yarn Integrity file
.yarn-integrity

# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local

# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache

# Next.js build output
.next
out

# Nuxt.js build / generate output
.nuxt
dist

# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public

# vuepress build output
.vuepress/dist

# vuepress v2.x temp and cache directory
.temp
.cache

# Docusaurus cache and generated files
.docusaurus

# Serverless directories
.serverless/

# FuseBox cache
.fusebox/

# DynamoDB Local files
.dynamodb/

# TernJS port file
.tern-port

# Stores VSCode versions used for testing VSCode extensions
.vscode-test

# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*


================================================
FILE: LICENSE
================================================
MIT License

Copyright (c) 2025 Chenliwen

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


================================================
FILE: README.md
================================================
# Fake Mail - The free temporary email service

![](./images/fakemail.png)

📪 Website: [https://mail.fakeact.fun](https://mail.fakeact.fun)

## Fake email generator, delete emails 2 hours after receiving

This is a temporary email service that uses Cloudflare Workers to create a temporary email address and view the received email in web browser.

` /app ` - Astro ssr

` /mailbox ` - Cloudflare Worker

## Multiple email addresses

use [clerk](https://clerk.com/) to login && register

Data is stored encrypted on cloudflare D1

## License

Fake Mail is licensed under the [MIT License](https://github.com/CH563/fakemail/blob/main/LICENSE).

================================================
FILE: app/.gitignore
================================================
# build output
dist/

# generated types
.astro/

# dependencies
node_modules/

# logs
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*

# environment variables
.env
.env.production

# macOS-specific files
.DS_Store

# jetbrains setting folder
.idea/
wrangler.toml
worker-configuration.d.ts
.env.example

================================================
FILE: app/.vscode/extensions.json
================================================
{
  "recommendations": ["astro-build.astro-vscode"],
  "unwantedRecommendations": []
}


================================================
FILE: app/.vscode/launch.json
================================================
{
  "version": "0.2.0",
  "configurations": [
    {
      "command": "./node_modules/.bin/astro dev",
      "name": "Development server",
      "request": "launch",
      "type": "node-terminal"
    }
  ]
}


================================================
FILE: app/README.md
================================================
# Astro Starter Kit: Basics

```sh
npm create astro@latest -- --template basics
```

[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/withastro/astro/tree/latest/examples/basics)
[![Open with CodeSandbox](https://assets.codesandbox.io/github/button-edit-lime.svg)](https://codesandbox.io/p/sandbox/github/withastro/astro/tree/latest/examples/basics)
[![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/withastro/astro?devcontainer_path=.devcontainer/basics/devcontainer.json)

> 🧑‍🚀 **Seasoned astronaut?** Delete this file. Have fun!

![just-the-basics](https://github.com/withastro/astro/assets/2244813/a0a5533c-a856-4198-8470-2d67b1d7c554)

## 🚀 Project Structure

Inside of your Astro project, you'll see the following folders and files:

```text
/
├── public/
│   └── favicon.svg
├── src/
│   ├── layouts/
│   │   └── Layout.astro
│   └── pages/
│       └── index.astro
└── package.json
```

To 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/).

## 🧞 Commands

All commands are run from the root of the project, from a terminal:

| Command                   | Action                                           |
| :------------------------ | :----------------------------------------------- |
| `npm install`             | Installs dependencies                            |
| `npm run dev`             | Starts local dev server at `localhost:4321`      |
| `npm run build`           | Build your production site to `./dist/`          |
| `npm run preview`         | Preview your build locally, before deploying     |
| `npm run astro ...`       | Run CLI commands like `astro add`, `astro check` |
| `npm run astro -- --help` | Get help using the Astro CLI                     |

## 👀 Want to learn more?

Feel free to check [our documentation](https://docs.astro.build) or jump into our [Discord server](https://astro.build/chat).


================================================
FILE: app/astro.config.mjs
================================================
// @ts-check
import { defineConfig } from 'astro/config';

import react from '@astrojs/react';

import tailwind from '@astrojs/tailwind';

import cloudflare from '@astrojs/cloudflare';

import clerk from '@clerk/astro';

// https://astro.build/config
export default defineConfig({
  output: 'server',
  site: 'https://mail.fakeact.fun',
  integrations: [react(), tailwind({
    applyBaseStyles: false,
  }), clerk()],
  adapter: cloudflare({
    platformProxy: {
      enabled: true,
      configPath: 'wrangler.toml'
    }
  }),
  vite: {
    resolve: {
      // Use react-dom/server.edge instead of react-dom/server.browser for React 19.
      // Without this, MessageChannel from node:worker_threads needs to be polyfilled.
      alias: import.meta.env.PROD ? {
        "react-dom/server": "react-dom/server.edge",
      } : {},
    }
  }
});

================================================
FILE: app/components.json
================================================
{
  "$schema": "https://ui.shadcn.com/schema.json",
  "style": "new-york",
  "rsc": false,
  "tsx": true,
  "tailwind": {
    "config": "tailwind.config.mjs",
    "css": "src/styles/globals.css",
    "baseColor": "slate",
    "cssVariables": true,
    "prefix": ""
  },
  "aliases": {
    "components": "@/components",
    "utils": "@/lib/utils",
    "ui": "@/components/ui",
    "lib": "@/lib",
    "hooks": "@/hooks"
  },
  "iconLibrary": "lucide"
}

================================================
FILE: app/email-platform-schema.sql
================================================
-- 用户邮箱地址表
CREATE TABLE user_email_addresses (
    id INTEGER PRIMARY KEY AUTOINCREMENT,   -- 主键ID
    user_id TEXT NOT NULL,               -- 关联的用户ID
    email_address TEXT NOT NULL,            -- 邮箱地址
    alias TEXT,                             -- 邮箱备注名
    is_active INTEGER DEFAULT 1,            -- 邮箱是否激活
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,  -- 创建时间
    updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,  -- 更新时间
    UNIQUE (email_address)
);
CREATE INDEX idx_user_email_addresses_user_id ON user_email_addresses(user_id);

-- 邮件主表
CREATE TABLE emails (
    id INTEGER PRIMARY KEY AUTOINCREMENT,   -- 主键ID
    message_id TEXT NOT NULL,               -- 邮件唯一标识符
    user_email_id INTEGER NOT NULL,         -- 关联的用户邮箱ID
    subject TEXT,                           -- 邮件主题
    sender TEXT NOT NULL,                   -- 发件人
    recipient TEXT NOT NULL,                -- 收件人
    cc TEXT,                                -- 抄送人列表
    bcc TEXT,                               -- 密送人列表
    content_type TEXT,                      -- 内容类型
    body_text TEXT,                         -- 纯文本内容
    body_html TEXT,                         -- HTML格式内容
    received_at DATETIME NOT NULL,          -- 接收时间
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,  -- 创建时间
    updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,  -- 更新时间
    UNIQUE (message_id)
);
CREATE INDEX idx_emails_user_email_id ON emails(user_email_id);
CREATE INDEX idx_emails_received_at ON emails(received_at);

-- 邮件状态表
CREATE TABLE email_status (
    id INTEGER PRIMARY KEY AUTOINCREMENT,   -- 主键ID
    email_id INTEGER NOT NULL,              -- 关联的邮件ID
    user_id TEXT NOT NULL,               -- 关联的用户ID
    is_read INTEGER DEFAULT 0,              -- 是否已读
    is_starred INTEGER DEFAULT 0,           -- 是否标星
    is_archived INTEGER DEFAULT 0,          -- 是否归档
    notes TEXT,                             -- 用户备注
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,  -- 创建时间
    updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,  -- 更新时间
    UNIQUE (email_id, user_id)
);
CREATE INDEX idx_email_status_user_id ON email_status(user_id);

-- 邮件标签表
CREATE TABLE email_tags (
    id INTEGER PRIMARY KEY AUTOINCREMENT,   -- 主键ID
    name TEXT NOT NULL,                     -- 标签名称
    user_id TEXT NOT NULL,               -- 关联的用户ID
    color TEXT,                             -- 标签颜色
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,  -- 创建时间
    UNIQUE (name, user_id)
);

-- 邮件-标签关联表
CREATE TABLE email_tag_relations (
    id INTEGER PRIMARY KEY AUTOINCREMENT,   -- 主键ID
    email_id INTEGER NOT NULL,              -- 关联的邮件ID
    tag_id INTEGER NOT NULL,                -- 关联的标签ID
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,  -- 创建时间
    UNIQUE (email_id, tag_id)
);
CREATE INDEX idx_email_tag_relations_tag_id ON email_tag_relations(tag_id);

================================================
FILE: app/env.d.ts
================================================
/// <reference types="astro/client" />

type KVNamespace = import('@cloudflare/workers-types').KVNamespace;
type D1Database = import('@cloudflare/workers-types').D1Database;
type ENV = {
    POST_DB: KVNamespace;
    MAIL_DB: D1Database;
    DKIM_PRIVATE_KEY: string;
    PUBLIC_TURNSTILE_SITE_KEY: string;
    SECRET_KEY: string;
    PUBLIC_CLERK_PUBLISHABLE_KEY: string;
    CLERK_SECRET_KEY: string;
    PUBLIC_CLERK_SIGN_IN_URL: string;
    PUBLIC_CLERK_SIGN_UP_URL: string;
};

// use a default runtime configuration (advanced mode).
type Runtime = import('@astrojs/cloudflare').Runtime<ENV>;
declare namespace App {
    interface Locals extends Runtime {}
}


interface ImportMetaEnv {
    readonly SECRET_KEY: string;
    readonly PUBLIC_TURNSTILE_SITE_KEY: string;
    readonly PUBLIC_CLERK_PUBLISHABLE_KEY: string;
    readonly CLERK_SECRET_KEY: string;
    readonly PUBLIC_CLERK_SIGN_IN_URL: string;
    readonly PUBLIC_CLERK_SIGN_UP_URL: string;
    // 更多环境变量…
}

interface ImportMeta {
    readonly env: ImportMetaEnv;
}

================================================
FILE: app/package.json
================================================
{
  "name": "app",
  "type": "module",
  "version": "0.0.1",
  "scripts": {
    "dev": "wrangler types && astro dev",
    "build": "astro build",
    "preview": "astro preview",
    "astro": "astro"
  },
  "dependencies": {
    "@astrojs/cloudflare": "^12.1.0",
    "@astrojs/node": "^9.0.0",
    "@astrojs/react": "^4.1.2",
    "@astrojs/tailwind": "^5.1.4",
    "@clerk/astro": "^2.1.4",
    "@marsidev/react-turnstile": "^1.1.0",
    "@radix-ui/react-alert-dialog": "^1.1.4",
    "@radix-ui/react-avatar": "^1.1.2",
    "@radix-ui/react-dialog": "^1.1.4",
    "@radix-ui/react-dropdown-menu": "^2.1.4",
    "@radix-ui/react-scroll-area": "^1.2.2",
    "@radix-ui/react-select": "^2.1.4",
    "@radix-ui/react-separator": "^1.1.1",
    "@radix-ui/react-slot": "^1.1.1",
    "@radix-ui/react-tooltip": "^1.1.6",
    "@types/react": "^19.0.2",
    "@types/react-dom": "^19.0.2",
    "astro": "^5.1.2",
    "astro-seo": "^0.8.4",
    "class-variance-authority": "^0.7.1",
    "clsx": "^2.1.1",
    "dayjs": "^1.11.13",
    "jotai": "^2.11.0",
    "lucide-react": "^0.469.0",
    "next-themes": "^0.4.4",
    "random-words": "^2.0.1",
    "react": "^19.0.0",
    "react-dom": "^19.0.0",
    "react-resizable-panels": "^2.1.7",
    "sonner": "^1.7.1",
    "tailwind-merge": "^2.6.0",
    "tailwindcss": "^3.4.17",
    "tailwindcss-animate": "^1.0.7"
  },
  "devDependencies": {
    "@cloudflare/workers-types": "^4.20241230.0",
    "wrangler": "^3.101.0"
  }
}

================================================
FILE: app/public/robots.txt
================================================
User-agent: *
Allow: /

================================================
FILE: app/src/components/Bar.astro
================================================
---
import { SignedIn, UserButton } from '@clerk/astro/components';
import { ModeToggle } from '@/components/ModeToggle';
---

<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")`>
    <a href="/" title="FakeMail"><img class='h-7' src='/favicon.svg' alt='FakeMail' title="FakeMail" /></a>
    <h1 class='font-extrabold text-center text-md text-green-500'>
        FakeMail
    </h1>
    <p class="text-xs text-gray-400">- Data is stored encrypted on cloudflare D1</p>
    <div class="flex-1"></div>
    <ModeToggle client:load />
    <SignedIn>
        <UserButton />
    </SignedIn>
</div>

================================================
FILE: app/src/components/Container.tsx
================================================
import { useState } from 'react';
import { Turnstile } from '@marsidev/react-turnstile';
import Lists from '@/components/Lists';

type Status = 'error' | 'expired' | 'solved';

const key = import.meta.env.PUBLIC_TURNSTILE_SITE_KEY || '1x00000000000000000000AA';

export default ({ siteKey }: {siteKey?: string}) => {
    const [status, setStatus] = useState<Status | null>(null)
    if (status === 'solved' || import.meta.env.DEV) return <Lists />
    return <Turnstile
        className='mx-auto text-center py-6'
        siteKey={siteKey || key}
        onError={() => setStatus('error')}
        onExpire={() => setStatus('expired')}
        onSuccess={() => {
            setTimeout(() => {
                setStatus('solved');
            }, 1000)
        }}
    />
};

================================================
FILE: app/src/components/Footer.astro
================================================
---
import shoteasy from '@/assets/shoteasy.svg'
import fakeact from '@/assets/fakeact.svg'
---

<div class="container mx-auto p-4">
    <h3 class="text-center font-semibold p-4 my-2">More tools</h3>
    <div class="flex flex-col gap-4 justify-center">
        <div class="text-center">
            <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">
                <img src={shoteasy.src} class="mx-auto mb-2 w-8" alt="ShotEasy" />
                <p class="font-medium">ShotEasy</p>
                <p class="text-gray-500 dark:text-gray-400 mt-0.5">Screenshot & image tools</p>
            </a>
        </div>
        <div class="text-center">
            <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">
                <img src={fakeact.src} class="mx-auto mb-2 w-8" alt="Fakeact" />
                <p class="font-medium">Fakeact</p>
                <p class="text-gray-500 dark:text-gray-400 mt-0.5">Fake identity generator</p>
            </a>
        </div>
    </div>
</div>
<footer>
    <div class="container mx-auto p-4 pt-10">
        <div class='text-center text-xs text-gray-400 border-t pt-4'>
            Copyright © 2025 FakeMail
        </div>
    </div>
</footer>

================================================
FILE: app/src/components/Generator.tsx
================================================
import { useState } from 'react';
import { Copy, Check, RotateCw } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import {
    Tooltip,
    TooltipContent,
    TooltipProvider,
    TooltipTrigger,
} from '@/components/ui/tooltip';
import {
    AlertDialog,
    AlertDialogAction,
    AlertDialogCancel,
    AlertDialogContent,
    AlertDialogDescription,
    AlertDialogFooter,
    AlertDialogHeader,
    AlertDialogTitle,
} from '@/components/ui/alert-dialog';
import { useCopyToClipboard } from '@/hooks/useCopyToClipboard';
import { useGeneratorMail } from '@/hooks/useGeneratorMail';

export default () => {
    const [address, generatorNewMail] = useGeneratorMail();
    const [isCopy, setIsCopy] = useState<boolean>(false);
    const [copiedText, copy] = useCopyToClipboard();
    const [open, setOpen] = useState(false);
    const handleFocus = (event: any) => event.target.select();
    const handleCopy = (text: string) => {
        if (isCopy) return;
        copy(text)
            .then(() => {
                setIsCopy(true);
                setTimeout(() => {
                    setIsCopy(false);
                }, 1500);
            })
            .catch((error) => {
                console.error('Failed to copy!', error);
            });
    };
    return (
        <TooltipProvider delayDuration={0}>
            <div className='flex flex-col items-center gap-3 sm:flex-row'>
                <Tooltip>
                    <TooltipTrigger asChild>
                        <Input
                            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'
                            readOnly
                            onFocus={handleFocus}
                            value={address}
                        />
                    </TooltipTrigger>
                    <TooltipContent>
                        <p>Your fake email address</p>
                    </TooltipContent>
                </Tooltip>
            <div className='flex gap-3 items-center'>
                    <Tooltip>
                        <TooltipTrigger asChild>
                            <Button
                                variant='outline'
                                className='rounded-full py-6 px-8 shrink-0 sm:px-4'
                                onClick={() => handleCopy(address)}
                            >
                                {isCopy ? <Check /> : <Copy />}
                            </Button>
                        </TooltipTrigger>
                        <TooltipContent>
                            <p>Copy to clipboard</p>
                        </TooltipContent>
                    </Tooltip>
                    <Tooltip>
                        <TooltipTrigger asChild>
                            <Button
                                variant='destructive'
                                className='rounded-full py-6 shrink-0'
                                onClick={() => setOpen(true)}
                            >
                                <RotateCw /> Change
                            </Button>
                        </TooltipTrigger>
                        <TooltipContent>
                            <p>Generate a new email address</p>
                        </TooltipContent>
                    </Tooltip>
                    <AlertDialog open={open} onOpenChange={setOpen}>
                        <AlertDialogContent>
                            <AlertDialogHeader>
                                <AlertDialogTitle>
                                    Generate a new address?
                                </AlertDialogTitle>
                                <AlertDialogDescription>
                                    This will discard the current temporary address and create a new one. You will no longer receive mail at the old address.
                                </AlertDialogDescription>
                            </AlertDialogHeader>
                            <AlertDialogFooter>
                                <AlertDialogCancel>Cancel</AlertDialogCancel>
                                <AlertDialogAction asChild>
                                    <Button onClick={generatorNewMail}>Yes, generate new address</Button>
                                </AlertDialogAction>
                            </AlertDialogFooter>
                        </AlertDialogContent>
                    </AlertDialog>
                </div>
            </div>
        </TooltipProvider>
    );
};


================================================
FILE: app/src/components/Header.astro
================================================
---
import { LayoutDashboard } from 'lucide-react';
import Generator from '@/components/Generator';
import { ModeToggle } from '@/components/ModeToggle';
---

<header class="bg-gray-700 dark:bg-gray-900">
    <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")`>
        <div class="flex justify-end items-center gap-1 pb-2 pt-3">
            <a
                href="/dashboard"
                title="Dashboard"
                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"
            ><LayoutDashboard size={16} />Dashboard</a>
            <ModeToggle client:load />
            <a
                href='https://github.com/CH563/fakemail'
                target='_blank'
                class='inline-flex h-9 w-9 rounded-full items-center justify-center hover:bg-gray-900/40'
            >
                <span
                    class='block w-5 h-5 align-middle bg-center'
                    style=`background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9IiNmZmZmZmYiIHN0cm9rZS13aWR0aD0iMiIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIiBjbGFzcz0ibHVjaWRlIGx1Y2lkZS1naXRodWIiPjxwYXRoIGQ9Ik0xNSAyMnYtNGE0LjggNC44IDAgMCAwLTEtMy41YzMgMCA2LTIgNi01LjUuMDgtMS4yNS0uMjctMi40OC0xLTMuNS4yOC0xLjE1LjI4LTIuMzUgMC0zLjUgMCAwLTEgMC0zIDEuNS0yLjY0LS41LTUuMzYtLjUtOCAwQzYgMiA1IDIgNSAyYy0uMyAxLjE1LS4zIDIuMzUgMCAzLjVBNS40MDMgNS40MDMgMCAwIDAgNCA5YzAgMy41IDMgNS41IDYgNS41LS4zOS40OS0uNjggMS4wNS0uODUgMS42NS0uMTcuNi0uMjIgMS4yMy0uMTUgMS44NXY0Ii8+PHBhdGggZD0iTTkgMThjLTQuNTEgMi01LTItNy0yIi8+PC9zdmc+");background-size:100%`
                ></span>
            </a>
        </div>
        <div class='container mx-auto max-w-3xl'>
            <div class='pt-4'>
                <img class='mx-auto w-10' src='/favicon.svg' alt='Fakeact' title="Fakeact" />
            </div>
            <h1 class='font-extrabold text-center text-xl pb-6 text-green-500'>
                FakeMail
            </h1>
            <h2 class='text-gray-100 font-semibold text-3xl text-center mb-4'>
                Fake email generator,<br /> delete emails 2 hours after receiving
            </h2>
            <p class='text-gray-400 text-sm text-center mb-4'>
                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.
            </p>
            <h2 class='text-gray-100 font-semibold text-xl text-center mb-2'>Your temporary email address</h2>
            <div class="border border-dashed bg-gray-500/10 border-gray-500 rounded-xl p-4 mb-4 min-h-[84px]">
                <Generator client:load />
            </div>
        </div>
    </div>
</header>


================================================
FILE: app/src/components/Lists.tsx
================================================
import { useState, useEffect, useRef, memo } from 'react';
import { Inbox, Mails, RefreshCw, Loader2 } from 'lucide-react';
import dayjs from 'dayjs';
import utc from 'dayjs/plugin/utc';
import { MailCard } from '@/components/ui/mail-card';
import { Button } from '@/components/ui/button';
import { Toaster } from "@/components/ui/sonner";
import { toast } from "sonner"

dayjs.extend(utc);

const countDownTime = 30;

const CountdownNumber = memo(({ value }: { value: number }) => (<span className='text-green-600 mx-1'>{value}</span>));
const CountDownComp = (({ value }: { value: number }) => {
    const [countDown, setCountDown] = useState<number>(countDownTime);
    useEffect(() => {
        setCountDown(value);
        const intervalId = setInterval(() => {
            setCountDown((prevCount) => prevCount - 1);
        }, 1000);
        return () => {
            clearInterval(intervalId);
        };
    }, [value]);
    return (
        <CountdownNumber value={countDown} />
    );
});
const MailCardComp = memo(({ mails, toDelete }: { mails: any[], toDelete: (mail: any) => void}) => mails.map((mail: any, index: number) => {
    if (mail) {
        return (
            <MailCard
                key={mail.suffix}
                sender={mail.sender}
                subject={mail.subject}
                name={mail.name}
                date={dayjs.utc(mail.date).local().format('YYYY-MM-DD HH:mm:ss')}
                content={mail['content-plain-formatted'] || mail['content-plain'] || mail['content-html']}
                defaultShow={index === 0}
                toDelete={() => toDelete(mail)}
            />
        )
    }
}));

export default () => {
    const [mails, setMails] = useState([]);
    const [stats, setStats] = useState<{count: number}>({count: 0});
    const [loading, setLoading] = useState<boolean>(false);
    const intervalId = useRef<any>(null);
    const countDownId = useRef<any>(null);
    const intervalStop = useRef<number>(0);
    const countDown = useRef<number>(countDownTime);
    const fetchData = async () => {
        clearInterval(countDownId.current);
        if (intervalStop.current > 15) clearInterval(intervalId.current);
        try {
            const address = localStorage.getItem('receivingEmail');
            if (!address) return;
            setLoading(true);
            const response = await fetch(`/api/get?address=${address}`); // Replace with your API endpoint
            if (!response.ok) {
                throw new Error('Network response was not ok.');
            }
            countDown.current = countDownTime;
            countDownId.current = setInterval(() => {
                countDown.current = countDown.current - 1;
            }, 1000);
            intervalStop.current = intervalStop.current++;
            const data = await response.json();
            if (data.mails && data.mails.length) {
                const arr = data.mails.filter((e: any) => e);
                arr.sort((a: any, b: any) => dayjs(b.date).unix() - dayjs(a.date).unix());
                setMails(arr);
                if (arr.length) clearInterval(intervalId.current);
            } else {
                setMails([]);
            }
            if (Number(data.stats.count)) setStats({count: Number(data.stats.count)});
            setLoading(false);
        } catch (error) {
            setLoading(false);
        }
    };
    const refresh = () => {
        clearInterval(intervalId.current);
        intervalStop.current = 0;
        fetchData();
        intervalId.current = setInterval(fetchData, countDownTime * 1000);
    }
    const toDelete = async (mail: any) => {
        const response = await fetch('/api/delete', {
            method: 'POST',
            body: JSON.stringify({
                key: `${mail.recipient}-${mail.suffix}`
            })
        }); // Replace with your API endpoint
        if (!response.ok) {
            throw new Error('Network response was not ok.');
        }
        setMails(mails.filter((e: any) => e.suffix !== mail.suffix));
        toast("The mail has been deleted!");
    }

    useEffect(() => {
        fetchData();
        intervalId.current = setInterval(fetchData, countDownTime * 1000);
        return () => {
            clearInterval(intervalId.current);
        };
    }, []);
    const emptyDom = (<div className='border border-dashed border-gray-400 rounded-xl p-4'>
        <div className='text-center text-gray-400'>
            <Inbox className='mx-auto' />
            <p>No email received yet</p>
        </div>
    </div>);
    return (
        <>
            <div className='flex justify-between items-center py-2'>
                <h2 className="text-center font-semibold flex gap-1 items-center"><Mails size={18} />Mail Inbox{mails.length ? `(${mails.length})` : ''}</h2>
                <div className='flex gap-2 items-center'>
                {(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>}
                    <Button size='xs' variant='outline' onClick={refresh} disabled={loading}>
                        {loading ? <Loader2 className="animate-spin" /> : <RefreshCw />}
                    </Button>
                </div>
            </div>
            {!mails.length ? emptyDom : <MailCardComp mails={mails} toDelete={toDelete} />}
            <div className='py-4 text-xs text-gray-400'>
                -- We've received<span className='mx-1 text-lg text-green-600/70 italic font-semibold'>{stats.count.toLocaleString()}</span>emails so far.
            </div>
            <Toaster />
        </>
    );
};


================================================
FILE: app/src/components/Main.astro
================================================
---
import Container from '@/components/Container';
const env = import.meta.env.PROD ? Astro.locals.runtime.env : import.meta.env;
---

<main>
    <div class="container mx-auto max-w-3xl px-2 lg:px-0">
        <Container client:load siteKey={env.PUBLIC_TURNSTILE_SITE_KEY} />
        <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">
            <h2>What is the FakeMail?</h2>
            <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>
            <h2>Why would you need a fake email address?</h2>
            <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>
            <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>
            <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>
            <h2>Cloudflare temporary email</h2>
            <p class="">This temporary email service runs on Cloudflare Workers. You get a temporary address and can read received mail in your browser.</p>
        </div>
    </div>
</main>

================================================
FILE: app/src/components/ModeToggle.tsx
================================================
import * as React from 'react';
import { Moon, Sun } from 'lucide-react';

import { Button } from '@/components/ui/button';

export function ModeToggle() {
    const [theme, setThemeState] = React.useState<
        'theme-light' | 'dark' | 'system'
    >('theme-light');

    React.useEffect(() => {
        const isDarkMode = document.documentElement.classList.contains('dark');
        setThemeState(isDarkMode ? 'dark' : 'theme-light');
    }, []);

    React.useEffect(() => {
        const isDark =
            theme === 'dark' ||
            (theme === 'system' &&
                window.matchMedia('(prefers-color-scheme: dark)').matches);
        document.documentElement.classList[isDark ? 'add' : 'remove']('dark');
    }, [theme]);

    return (
        <Button
            variant="ghost"
            size='icon'
            className='rounded-full text-white hover:text-white hover:bg-slate-800/50 hover:dark:bg-slate-800/50'
            onClick={() => setThemeState(theme === 'dark' ? 'theme-light' : 'dark')}
        >
            <Sun className='h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0' />
            <Moon className='absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100' />
            <span className='sr-only'>Toggle theme</span>
        </Button>
    );
}


================================================
FILE: app/src/components/mail/AccountSwitcher.tsx
================================================
import { useState } from 'react';
import { cn } from '@/lib/utils';
import {
    Select,
    SelectContent,
    SelectItem,
    SelectTrigger,
    SelectValue,
} from '@/components/ui/select';

interface AccountSwitcherProps {
    isCollapsed: boolean;
    accounts: {
        id: number;
        email_address: string;
    }[];
    onAccountChange: (email: string) => void;
}

export function AccountSwitcher({
    isCollapsed,
    accounts,
    onAccountChange
}: AccountSwitcherProps) {
    const [selectedAccount, setSelectedAccount] = useState<string>(
        accounts[0].email_address
    );
    const handleChanged = (email: string) => {
        setSelectedAccount(email);
        onAccountChange(email);
    }
    return (
        <Select
            defaultValue={selectedAccount}
            onValueChange={handleChanged}
        >
            <SelectTrigger
                className={cn(
                    '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',
                    isCollapsed &&
                        'flex h-9 w-9 shrink-0 items-center justify-center p-0 [&>span]:w-auto [&>svg]:hidden'
                )}
                aria-label='Select account'
            >
                <SelectValue placeholder='Select an account'>
                    <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>
                    <span className={cn('', isCollapsed && 'hidden')}>
                        {selectedAccount}
                    </span>
                </SelectValue>
            </SelectTrigger>
            <SelectContent>
                {accounts.map((account) => (
                    <SelectItem key={account.email_address} value={account.email_address}>
                        <div className='flex items-center gap-1.5 [&_svg]:h-4 [&_svg]:w-4 [&_svg]:shrink-0 [&_svg]:text-foreground'>
                        <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>
                            {account.email_address}
                        </div>
                    </SelectItem>
                ))}
            </SelectContent>
        </Select>
    );
}


================================================
FILE: app/src/components/mail/Accounts.tsx
================================================
import { useRef, useState } from 'react';
import { Button } from '@/components/ui/button';
import {
    Dialog,
    DialogContent,
    DialogDescription,
    DialogFooter,
    DialogHeader,
    DialogTitle,
    DialogTrigger,
} from "@/components/ui/dialog";
import { Badge } from '@/components/ui/badge';
import { Textarea } from '@/components/ui/textarea';
import { Trash2, CirclePlus, FilePenLine, Loader2 } from "lucide-react";
import { Toaster } from "@/components/ui/sonner";
import { toast } from "sonner";
import type { AccountsList } from './data';
import dayjs from 'dayjs';
import utc from 'dayjs/plugin/utc';

dayjs.extend(utc);

type AccountsProps = {
    list: AccountsList[]
}

export const Accounts = ({ list = [] }: AccountsProps) => {
    const remarkRef = useRef<HTMLTextAreaElement>(null);
    const remarkEditRef = useRef<HTMLTextAreaElement>(null);
    const [showDialog, setShowDialog] = useState(false);
    const [editAccount, setEditAccount] = useState<AccountsList | null>(null);
    const [loading, setLoading] = useState(false);
    const toGenerate = async () => {
        const response = await fetch('/api/generate', {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
            },
            body: JSON.stringify({ remark: remarkRef.current?.value }),
        });
        if (response.ok) {
            const data = await response.json();
            if (data.code === 200) {
                return window.location.reload();
            }
            toast.error(data.msg);
        } else {
            toast.error(response.statusText);
        }
    }
    const toSave = async () => {
        setLoading(true);
        const remark = (remarkEditRef.current?.value || '').trim();
        const response = await fetch('/api/remarkMail', {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
            },
            body: JSON.stringify({
                id: editAccount?.id,
                remark
            }),
        });
        if (response.ok) {
            const data = await response.json();
            if (data.code === 200) {
                list.forEach((account) => {
                    if (account.id === editAccount?.id) {
                        account.alias = remark;
                    }
                });
                setShowDialog(false);
            }
            toast.error(data.msg);
        } else {
            toast.error(response.statusText);
        }
        setLoading(false);
    }
    const toEdit = (account: AccountsList) => {
        setEditAccount(account);
        setShowDialog(true);
    }
    const onDialogChange = (e: boolean) => {
        setShowDialog(e);
        if (!e) {
            setEditAccount(null);
        }
    }
    return (
        <div className='grid gap-1'>
            {list.map((account) => (
                <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}>
                    <div className='col-span-4 leading-4 py-2 flex items-center gap-1'>
                    <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>
                        <div className='flex-1 w-0 truncate'>{account.email_address}</div>
                    </div>
                    <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>
                    <div className='col-span-5 leading-4 py-2 whitespace-pre-wrap'>{account.alias}</div>
                    <div className='col-span-1'>
                        <Button variant='ghost' size='icon' onClick={() => toEdit(account)}><FilePenLine className='h-4 w-4' /></Button>
                        <Button variant='ghost' size='icon' onClick={() => toast('Developing, please wait~')}><Trash2 className='h-4 w-4' color='red' /></Button>
                    </div>
                </div>
            ))}
            <div className='p-4 text-center'>
                <Dialog>
                    <DialogTrigger asChild>
                        <Button variant='default' size='sm' disabled={list.length > 4}><CirclePlus /> Add a email address</Button>
                    </DialogTrigger>
                    <DialogContent className="sm:max-w-[425px]">
                        <DialogHeader>
                        <DialogTitle>Add a email address</DialogTitle>
                        <DialogDescription asChild>
                            <div className='text-sm'>Generate a random email address <Badge>@fakeact.fun</Badge></div>
                        </DialogDescription>
                        </DialogHeader>
                        <div className="grid gap-4 py-4">
                            <Textarea ref={remarkRef} rows={6} placeholder="Type the remark here.(Optional)" maxLength={120} />
                        </div>
                        <DialogFooter>
                            <Button type="submit" onClick={toGenerate}>Yes, add it</Button>
                        </DialogFooter>
                    </DialogContent>
                </Dialog>
            </div>
            <Toaster />
            <Dialog open={showDialog} onOpenChange={onDialogChange}>
                <DialogContent className="sm:max-w-[425px]">
                    <DialogHeader>
                        <DialogTitle>Edit</DialogTitle>
                        <DialogDescription asChild>
                            <div className='text-sm'><Badge>{editAccount?.email_address}</Badge></div>
                        </DialogDescription>
                    </DialogHeader>
                    <div className="grid gap-4">
                        <Textarea id='remarkEdit' ref={remarkEditRef} defaultValue={editAccount?.alias || ''} rows={6} placeholder="Type the remark here.(Optional)" maxLength={120} />
                    </div>
                    <DialogFooter className='gap-2'>
                        <Button disabled={loading} variant="secondary" onClick={() => setShowDialog(false)}>Cancel</Button>
                        <Button disabled={loading} onClick={toSave}>{loading ? <Loader2 className="animate-spin" /> : ''}Save it</Button>
                    </DialogFooter>
                </DialogContent>
            </Dialog>
        </div>
    );
}

================================================
FILE: app/src/components/mail/Mail.tsx
================================================
import { useRef, useState } from 'react';
import {
    Archive,
    File,
    Inbox,
    Search,
    Send,
    Copy,
    Check,
    Settings,
    RefreshCw
} from "lucide-react";
import {
    TooltipProvider,
    Tooltip,
    TooltipContent,
    TooltipTrigger
} from '@/components/ui/tooltip';
import {
    ResizableHandle,
    ResizablePanel,
    ResizablePanelGroup,
} from "@/components/ui/resizable";
import { Skeleton } from "@/components/ui/skeleton";
import { Separator } from "@/components/ui/separator";
import { Input } from "@/components/ui/input";
import { Toaster } from "@/components/ui/sonner";
import { toast } from "sonner";
import { cn } from '@/lib/utils';
import { useCopyToClipboard } from '@/hooks/useCopyToClipboard';
import { AccountSwitcher } from './AccountSwitcher';
import { Nav } from './Nav';
import { MailList } from './MailList';
import { MailDisplay } from './MailDisplay';
import { useMail } from './useMail';
import { Button, buttonVariants } from '../ui/button';
import type { MailsList } from './data';

interface Account {
    id: number
    email_address: string
}


interface MailProps {
    accounts: Account[]
    mails: MailsList[]
    defaultLayout: number[] | undefined
    defaultCollapsed?: boolean
    navCollapsedSize: number
}

export default ({
    accounts,
    mails,
    defaultLayout = [20, 32, 48],
    defaultCollapsed = false,
    navCollapsedSize,
}: MailProps) => {
    const abortController = useRef<AbortController | null>(null);
    const [isCollapsed, setIsCollapsed] = useState(defaultCollapsed);
    const [loading, setLoading] = useState<boolean>(false);
    const [mailsList, setMailsList] = useState<MailsList[]>(mails);
    const [isCopy, setIsCopy] = useState<boolean>(false);
    const [currentAccount, setCurrentAccount] = useState<Account>(accounts[0]);
    const [copiedText, copy] = useCopyToClipboard();
    const [mail] = useMail();
    const fetchData = async (id?: number) => {
        if (!id) return;
        try {
            setLoading(true);
            abortController.current?.abort();
            abortController.current = new AbortController();
            const response = await fetch(`/api/getMails?id=${id}`, { signal: abortController.current.signal });
            if (!response.ok) {
                throw new Error('Network response was not ok.');
            }
            const data = await response.json();
            if (data.code === 502) {
                // redirect to login page
                window.location.href = '/sign-in';
            }
            setMailsList(data?.mails || []);
            setLoading(false);
        } catch (error) {
            setLoading(false);
        }
        abortController.current = null;
    }
    const handleAccountChange = (email: string) => {
        const currentAccount = accounts.find((account) => account.email_address === email) || accounts[0];
        setCurrentAccount(currentAccount);
        fetchData(currentAccount?.id);
    }
    const updateStatus = (messageId: string, status: number = 0) => {
        setMailsList(mailsList.map(item => {
            if (item.message_id === messageId) item.is_read = status;
            return item;
        }));
    }
    const handleUnread = async (messageId?: string) => {
        if (!messageId) return true;
        const response = await fetch('/api/updateStatus', {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
            },
            body: JSON.stringify({ message_id: messageId, is_read: 0 }),
        })
        if (response.ok) {
            const data = await response.json();
            if (data.code === 200) {
                updateStatus(messageId);
                return true;
            }
        }
        return false;
    }
    const toDelete = async (messageId?: string) => {
        if (!messageId) return true;
        const response = await fetch('/api/deleteMails', {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
            },
            body: JSON.stringify({ message_id: messageId }),
        });
        if (response.ok) {
            const data = await response.json();
            if (data.code === 200) {
                setMailsList(mailsList.filter(e => e.message_id !== messageId));
                toast.success('Delete Success!');
                return true;
            }
            toast.error(data.message);
        }
        return false;
    }
    const toCopy = () => {
        if (isCopy) return;
        copy(currentAccount.email_address)
            .then(() => {
                setIsCopy(true);
                toast.success('Copied!');
                setTimeout(() => {
                    setIsCopy(false);
                }, 1500);
            })
            .catch((error) => {
                console.error('Failed to copy!', error);
            });
    }
    return (
        <TooltipProvider delayDuration={0}>
            <ResizablePanelGroup
                direction="horizontal"
                onLayout={(sizes: number[]) => {
                    document.cookie = `react-resizable-panels:layout:mail=${JSON.stringify(
                        sizes
                    )}`
                }}
                className="h-full flex-1 items-stretch"
            >
                <ResizablePanel
                    defaultSize={defaultLayout[0]}
                    collapsedSize={navCollapsedSize}
                    collapsible={true}
                    minSize={15}
                    maxSize={20}
                    onCollapse={() => {
                        setIsCollapsed(true)
                        document.cookie = `react-resizable-panels:collapsed=${JSON.stringify(true)}`
                    }}
                    onResize={() => {
                        setIsCollapsed(false)
                        document.cookie = `react-resizable-panels:collapsed=${JSON.stringify(
                            false
                        )}`
                    }}
                    className={cn("flex flex-col", isCollapsed && "min-w-[50px] transition-all duration-300 ease-in-out")}
                >
                    <div className={cn("flex h-[52px] items-center justify-center", isCollapsed ? "h-[52px]" : "px-2")}>
                        <AccountSwitcher isCollapsed={isCollapsed} accounts={accounts} onAccountChange={handleAccountChange} />
                    </div>
                    <Separator />
                    <Nav
                        isCollapsed={isCollapsed}
                        links={[
                            {
                                title: "Inbox",
                                label: mailsList.length ? `${mailsList.length}` : "",
                                icon: Inbox,
                                variant: "default",
                            },
                            {
                                title: "Drafts",
                                label: "",
                                icon: File,
                                variant: "ghost",
                            },
                            {
                                title: "Sent",
                                label: "",
                                icon: Send,
                                variant: "ghost",
                            },
                            {
                                title: "Archive",
                                label: "",
                                icon: Archive,
                                variant: "ghost",
                            },
                        ]}
                    />
                    <Separator />
                    <div className='flex-1'></div>
                    <div className='py-4 px-2'>
                        <a href='./settings' className={cn('w-full', buttonVariants({ variant: 'outline'}))}>
                            <Settings />
                            {isCollapsed ? '' : `My email addresses (${accounts.length})`}
                        </a>
                    </div>
                </ResizablePanel>
                <ResizableHandle withHandle />
                <ResizablePanel defaultSize={defaultLayout[1]} minSize={20}>
                    <div className="flex items-center px-4 py-2">
                        <h1 className="text-xl font-bold">Inbox</h1>
                        <Tooltip>
                            <TooltipTrigger asChild>
                                <Button
                                    variant='ghost'
                                    size='icon'
                                    className='ml-auto'
                                    disabled={loading}
                                    onClick={() => fetchData(currentAccount.id)}
                                >
                                    <RefreshCw className={cn('h-4 w-4', loading && 'animate-spin')} />
                                    <span className='sr-only'>Refresh</span>
                                </Button>
                            </TooltipTrigger>
                            <TooltipContent>Refresh inbox</TooltipContent>
                        </Tooltip>
                        <Tooltip>
                            <TooltipTrigger asChild>
                                <Button
                                    variant='ghost'
                                    size='icon'
                                    className=''
                                    disabled={isCopy}
                                    onClick={toCopy}
                                >
                                    {isCopy ? <Check size={16} color='green' /> :
                                        <>
                                            <Copy className='h-4 w-4' />
                                            <span className='sr-only'>Copy</span>
                                        </>
                                    }
                                </Button>
                            </TooltipTrigger>
                            <TooltipContent>Copy current email address</TooltipContent>
                        </Tooltip>
                    </div>
                    <Separator />
                    <div className="bg-background/95 p-4 backdrop-blur supports-[backdrop-filter]:bg-background/60">
                        <form>
                            <div className="relative">
                            <Search className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
                            <Input placeholder="Search" className="pl-8" />
                            </div>
                        </form>
                    </div>
                    {loading ? <div className='px-4'>
                        <div className='flex flex-col items-start gap-2 rounded-lg border p-3 text-left text-sm transition-all'>
                            <Skeleton className='w-3/4 h-3' />
                            <Skeleton className='w-1/4 h-3' />
                            <Skeleton className='w-3/4 h-2' />
                        </div>
                    </div> : <MailList items={mailsList} updateStatus={(messageId) => updateStatus(messageId, 1)} />}
                </ResizablePanel>
                <ResizableHandle withHandle />
                <ResizablePanel defaultSize={defaultLayout[2]} minSize={30}>
                    <MailDisplay
                        mail={mailsList.find((item) => item.message_id === mail.selected) || null}
                        currentAccount={currentAccount.email_address}
                        toDelete={toDelete}
                        handleUnread={handleUnread}
                    />
                </ResizablePanel>
            </ResizablePanelGroup>
            <Toaster />
        </TooltipProvider>
    );
};

================================================
FILE: app/src/components/mail/MailDisplay.tsx
================================================
import {
    Archive,
    Forward,
    MoreVertical,
    Reply,
    ReplyAll,
    Trash2,
    Loader2
} from "lucide-react";
import {
    DropdownMenuContent,
    DropdownMenuItem,
    DropdownMenu,
    DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import {
    AlertDialog,
    AlertDialogAction,
    AlertDialogCancel,
    AlertDialogContent,
    AlertDialogDescription,
    AlertDialogFooter,
    AlertDialogHeader,
    AlertDialogTitle,
} from '@/components/ui/alert-dialog';
import { toast } from "sonner";
import {
    Avatar,
    AvatarFallback,
    AvatarImage,
} from "@/components/ui/avatar";
import {
    Tooltip,
    TooltipContent,
    TooltipTrigger,
} from '@/components/ui/tooltip';
import { Button } from '@/components/ui/button';
import { Separator } from '@/components/ui/separator';
import dayjs from 'dayjs';
import utc from 'dayjs/plugin/utc';

import type { MailsList } from "./data";
import { useState } from 'react';

dayjs.extend(utc);

interface MailDisplayProps {
    mail: MailsList | null
    currentAccount: string
    toDelete: (messageId?: string) => Promise<boolean>;
    handleUnread: (messageId?: string) => Promise<boolean>;
}

export function MailDisplay({ mail, currentAccount, toDelete, handleUnread }: MailDisplayProps) {
    const [openStatus, setOpenStatus] = useState(false);
    const [loading, setLoading] = useState(false);
    const handleDelete = async () => {
        setLoading(true);
        const isOk = await toDelete(mail?.message_id);
        setLoading(false);
        if (isOk) {
            setOpenStatus(false);
        }
    }
    return (
        <div className='flex h-full flex-col'>
            <div className='flex items-center p-2'>
                <div className='flex items-center gap-2'>
                    <Tooltip>
                        <TooltipTrigger asChild>
                            <Button
                                variant='ghost'
                                size='icon'
                                disabled={!mail}
                                onClick={() => toast('Archive is developing~')}
                            >
                                <Archive className='h-4 w-4' />
                                <span className='sr-only'>Archive</span>
                            </Button>
                        </TooltipTrigger>
                        <TooltipContent>Archive</TooltipContent>
                    </Tooltip>
                    <Tooltip>
                        <TooltipTrigger asChild>
                            <Button
                                variant='ghost'
                                size='icon'
                                disabled={!mail}
                                onClick={() => setOpenStatus(true)}
                            >
                                <Trash2 className='h-4 w-4' />
                                <span className='sr-only'>Delete mail</span>
                            </Button>
                        </TooltipTrigger>
                        <TooltipContent>Delete mail</TooltipContent>
                    </Tooltip>
                    <AlertDialog open={openStatus}>
                        <AlertDialogContent>
                            <AlertDialogHeader>
                                <AlertDialogTitle>
                                    Are you absolutely sure?
                                </AlertDialogTitle>
                                <AlertDialogDescription>
                                    Delete this mail cannot be undone. This will
                                    permanently remove your data from our servers.
                                </AlertDialogDescription>
                            </AlertDialogHeader>
                            <AlertDialogFooter>
                                <AlertDialogCancel onClick={() => setOpenStatus(false)}>
                                    Cancel
                                </AlertDialogCancel>
                                <AlertDialogAction asChild>
                                    <Button variant="destructive" disabled={loading} onClick={handleDelete}>{loading ? <Loader2 className="animate-spin" /> : 'Delete'}</Button>
                                </AlertDialogAction>
                            </AlertDialogFooter>
                        </AlertDialogContent>
                    </AlertDialog>
                    <Separator orientation='vertical' className='mx-1 h-6' />
                </div>
                <div className='ml-auto flex items-center gap-2'>
                    {/* <Tooltip>
                        <TooltipTrigger asChild>
                            <Button
                                variant='ghost'
                                size='icon'
                                disabled={!mail}
                            >
                                <Reply className='h-4 w-4' />
                                <span className='sr-only'>Reply</span>
                            </Button>
                        </TooltipTrigger>
                        <TooltipContent>Reply</TooltipContent>
                    </Tooltip>
                    <Tooltip>
                        <TooltipTrigger asChild>
                            <Button
                                variant='ghost'
                                size='icon'
                                disabled={!mail}
                            >
                                <ReplyAll className='h-4 w-4' />
                                <span className='sr-only'>Reply all</span>
                            </Button>
                        </TooltipTrigger>
                        <TooltipContent>Reply all</TooltipContent>
                    </Tooltip>
                    <Tooltip>
                        <TooltipTrigger asChild>
                            <Button
                                variant='ghost'
                                size='icon'
                                disabled={!mail}
                            >
                                <Forward className='h-4 w-4' />
                                <span className='sr-only'>Forward</span>
                            </Button>
                        </TooltipTrigger>
                        <TooltipContent>Forward</TooltipContent>
                    </Tooltip> */}
                </div>
                <Separator orientation='vertical' className='mx-2 h-6' />
                <DropdownMenu>
                    <DropdownMenuTrigger asChild>
                        <Button variant='ghost' size='icon' disabled={!mail}>
                            <MoreVertical className='h-4 w-4' />
                            <span className='sr-only'>More</span>
                        </Button>
                    </DropdownMenuTrigger>
                    <DropdownMenuContent align='end'>
                        <DropdownMenuItem onSelect={() => handleUnread(mail?.message_id)}>Mark as unread</DropdownMenuItem>
                        {/* <DropdownMenuItem>Add label</DropdownMenuItem> */}
                    </DropdownMenuContent>
                </DropdownMenu>
            </div>
            <Separator />
            {mail ? (
                <div className='flex flex-1 flex-col'>
                    <div className='flex items-start p-4'>
                        <div className='flex items-start gap-4 text-sm'>
                            <Avatar>
                                <AvatarImage alt={mail.senderName || mail.sender} />
                                <AvatarFallback>
                                    {(mail.senderName || mail.sender).split(' ').map((chunk) => chunk[0]).join('')}
                                </AvatarFallback>
                            </Avatar>
                            <div className='grid gap-1'>
                                <div className='flex gap-1 items-center text-xs'>
                                    <span className='font-semibold'>{mail.senderName || mail.sender}</span>
                                    {mail.senderName && <span className='text-gray-400'>&lt;{mail.sender}&gt;</span>}
                                </div>
                                <div className='line-clamp-1 text-xs'>
                                    {mail.subject}
                                </div>
                                <div className='line-clamp-1 text-xs text-gray-400'>
                                    <span className='font-medium text-gray-600 dark:text-gray-300'>
                                        Recipient:
                                    </span>{' '}
                                    {currentAccount}
                                    {mail.cc && (
                                        <>
                                            <span className='ml-2 font-medium text-gray-600 dark:text-gray-300'>CC:</span>{' '}
                                            {mail.cc}
                                        </>
                                    )}
                                </div>
                                {/* <div className='line-clamp-1 text-xs text-gray-400'>
                                    <span className='font-medium text-gray-600 dark:text-gray-300'>
                                        Reply-To:
                                    </span>{' '}
                                    {mail.senderName || mail.sender}
                                </div> */}
                            </div>
                        </div>
                        {mail.received_at && (
                            <div className='ml-auto text-xs text-muted-foreground'>
                                {dayjs
                                    .utc(mail.received_at)
                                    .local()
                                    .format('YYYY-MM-DD HH:mm:ss')}
                            </div>
                        )}
                    </div>
                    <Separator />
                    <div className='flex-1 p-4 text-sm' dangerouslySetInnerHTML={{ __html: mail.content }}></div>
                </div>
            ) : (
                <div className='p-8 text-center text-muted-foreground'>
                    No message selected
                </div>
            )}
        </div>
    );
}

================================================
FILE: app/src/components/mail/MailList.tsx
================================================
import { ScrollArea } from '@/components/ui/scroll-area';
import { cn } from '@/lib/utils';
import dayjs from 'dayjs';
import utc from 'dayjs/plugin/utc';
import relativeTime from 'dayjs/plugin/relativeTime';
import { Inbox } from 'lucide-react';
import type { MailsList } from './data';
import { useMail } from './useMail';

dayjs.extend(utc);
dayjs.extend(relativeTime);

interface MailListProps {
    items: MailsList[];
    updateStatus?: (messageId: string) => void;
}

export function MailList({ items, updateStatus = () => {} }: MailListProps) {
    const [mail, setMail] = useMail();
    const handleSelect = (messageId: string) => {
        setMail({
            ...mail,
            selected: messageId,
        });

        fetch('/api/updateStatus', {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
            },
            body: JSON.stringify({ message_id: messageId, is_read: 1 }),
        }).then(async (response) => {
            if (response.ok) {
                const data = await response.json();
                if (data.code === 200) {
                    updateStatus(messageId);
                }
            }
        })
    }
    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>
    return (
        <ScrollArea className='h-screen'>
            <div className='flex flex-col gap-2 p-4 pt-0'>
                {items.map((item) => (
                    <button
                        key={item.message_id}
                        className={cn(
                            'flex flex-col items-start gap-2 rounded-lg border p-3 text-left text-sm transition-all hover:bg-accent',
                            mail.selected === item.message_id && 'bg-muted'
                        )}
                        onClick={() => handleSelect(item.message_id)}
                    >
                        <div className='flex w-full flex-col gap-1'>
                            <div className='flex items-center'>
                                <div className='flex items-center gap-2'>
                                    <div className='font-semibold'>
                                        {item.senderName || item.sender}
                                    </div>
                                    {!item.is_read && (
                                        <span className='flex h-2 w-2 rounded-full bg-blue-600' />
                                    )}
                                </div>
                                <div
                                    className={cn(
                                        'ml-auto text-xs',
                                        mail.selected === item.message_id
                                            ? 'text-foreground'
                                            : 'text-muted-foreground'
                                    )}
                                >
                                    {dayjs(item.received_at).fromNow()}
                                </div>
                            </div>
                            <div className='text-xs font-medium'>
                                {item.subject}
                            </div>
                        </div>
                        <div className='line-clamp-2 text-xs text-muted-foreground'>
                            {item.content && item.content.substring(0, 300)}
                        </div>
                    </button>
                ))}
            </div>
        </ScrollArea>
    );
}


================================================
FILE: app/src/components/mail/Nav.tsx
================================================
import { cn } from '@/lib/utils';
import type { LucideIcon } from 'lucide-react';
import {
    Tooltip,
    TooltipContent,
    TooltipTrigger,
} from '@/components/ui/tooltip';
import { buttonVariants } from "@/components/ui/button"
import { toast } from "sonner";

interface NavProps {
    isCollapsed: boolean;
    links: {
        title: string;
        label?: string;
        icon: LucideIcon;
        variant: 'default' | 'ghost';
    }[];
}

export function Nav({ links, isCollapsed }: NavProps) {
    return (
        <div
            data-collapsed={isCollapsed}
            className='group flex flex-col gap-4 py-2 data-[collapsed=true]:py-2'
        >
            <nav className='grid gap-1 px-2 group-[[data-collapsed=true]]:justify-center group-[[data-collapsed=true]]:px-2'>
                {links.map((link, index) =>
                    isCollapsed ? (
                        <Tooltip key={index} delayDuration={0}>
                            <TooltipTrigger asChild>
                                <div
                                    className={cn(
                                        buttonVariants({
                                            variant: link.variant,
                                            size: 'icon',
                                        }),
                                        'h-9 w-9 cursor-pointer',
                                        link.variant === 'default' &&
                                            'dark:bg-muted dark:text-muted-foreground dark:hover:bg-muted dark:hover:text-white'
                                    )}
                                    onClick={() => {
                                        if (link.variant !== 'default') toast(`${link.title} is developing~`);
                                    }}
                                >
                                    <link.icon className='h-4 w-4' />
                                    <span className='sr-only'>
                                        {link.title}
                                    </span>
                                </div>
                            </TooltipTrigger>
                            <TooltipContent
                                side='right'
                                className='flex items-center gap-4'
                            >
                                {link.title}
                                {link.label && (
                                    <span className='ml-auto text-muted-foreground'>
                                        {link.label}
                                    </span>
                                )}
                            </TooltipContent>
                        </Tooltip>
                    ) : (
                        <div
                            key={index}
                            className={cn(
                                buttonVariants({
                                    variant: link.variant,
                                    size: 'sm',
                                }),
                                link.variant === 'default' &&
                                    'dark:bg-muted dark:text-white dark:hover:bg-muted dark:hover:text-white',
                                'justify-start cursor-pointer'
                            )}
                            onClick={() => {
                                if (link.variant !== 'default') toast(`${link.title} is developing~`);
                            }}
                        >
                            <link.icon className='mr-2 h-4 w-4' />
                            {link.title}
                            {link.label && (
                                <span
                                    className={cn(
                                        'ml-auto',
                                        link.variant === 'default' &&
                                            'text-background dark:text-white'
                                    )}
                                >
                                    {link.label}
                                </span>
                            )}
                        </div>
                    )
                )}
            </nav>
        </div>
    );
}


================================================
FILE: app/src/components/mail/data.tsx
================================================
export const mails = [
  {
    id: "61c35085-72d7-42b4-8d62-738f700d4b92",
    name: "AdsPower",
    email: "captcha@email.adspower.net",
    subject: "AdsPower登录验证码",
    text: "您好:\nAdsPower登录验证码为【926493】,5分钟内有效,请尽快使用。\n请不要将该验证码泄露给他人。\n感谢您的使用!\nAdsPower团队",
    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",
    date: "2025-01-10T13:15:00",
    read: false,
    labels: ["work", "budget"],
  },
  {
    id: "8f7b5db9-d935-4e42-8e05-1f1d0a3dfb97",
    name: "Michael Wilson",
    email: "michaelwilson@example.com",
    subject: "Important Announcement",
    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",
    date: "2023-03-10T15:00:00",
    read: false,
    labels: ["meeting", "work", "important"],
  },
  {
    id: "1f0f2c02-e299-40de-9b1d-86ef9e42126b",
    name: "Sarah Brown",
    email: "sarahbrown@example.com",
    subject: "Re: Feedback on Proposal",
    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",
    date: "2023-02-15T16:30:00",
    read: true,
    labels: ["work"],
  },
  {
    id: "17c0a96d-4415-42b1-8b4f-764efab57f66",
    name: "David Lee",
    email: "davidlee@example.com",
    subject: "New Project Idea",
    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",
    date: "2023-01-28T17:45:00",
    read: false,
    labels: ["meeting", "work", "important"],
  },
  {
    id: "2f0130cb-39fc-44c4-bb3c-0a4337edaaab",
    name: "Olivia Wilson",
    email: "oliviawilson@example.com",
    subject: "Vacation Plans",
    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",
    date: "2022-12-20T18:30:00",
    read: true,
    labels: ["personal"],
  },
  {
    id: "de305d54-75b4-431b-adb2-eb6b9e546014",
    name: "James Martin",
    email: "jamesmartin@example.com",
    subject: "Re: Conference Registration",
    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",
    date: "2022-11-30T19:15:00",
    read: true,
    labels: ["work", "conference"],
  },
  {
    id: "7dd90c63-00f6-40f3-bd87-5060a24e8ee7",
    name: "Sophia White",
    email: "sophiawhite@example.com",
    subject: "Team Dinner",
    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",
    date: "2022-11-05T20:30:00",
    read: false,
    labels: ["meeting", "work"],
  },
  {
    id: "99a88f78-3eb4-4d87-87b7-7b15a49a0a05",
    name: "Daniel Johnson",
    email: "danieljohnson@example.com",
    subject: "Feedback Request",
    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",
    date: "2022-10-22T09:30:00",
    read: false,
    labels: ["work"],
  },
  {
    id: "f47ac10b-58cc-4372-a567-0e02b2c3d479",
    name: "Ava Taylor",
    email: "avataylor@example.com",
    subject: "Re: Meeting Agenda",
    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",
    date: "2022-10-10T10:45:00",
    read: true,
    labels: ["meeting", "work"],
  },
  {
    id: "c1a0ecb4-2540-49c5-86f8-21e5ce79e4e6",
    name: "William Anderson",
    email: "williamanderson@example.com",
    subject: "Product Launch Update",
    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",
    date: "2022-09-20T12:00:00",
    read: false,
    labels: ["meeting", "work", "important"],
  },
  {
    id: "ba54eefd-4097-4949-99f2-2a9ae4d1a836",
    name: "Mia Harris",
    email: "miaharris@example.com",
    subject: "Re: Travel Itinerary",
    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",
    date: "2022-09-10T13:15:00",
    read: true,
    labels: ["personal", "travel"],
  },
  {
    id: "df09b6ed-28bd-4e0c-85a9-9320ec5179aa",
    name: "Ethan Clark",
    email: "ethanclark@example.com",
    subject: "Team Building Event",
    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",
    date: "2022-08-25T15:30:00",
    read: false,
    labels: ["meeting", "work"],
  },
  {
    id: "d67c1842-7f8b-4b4b-9be1-1b3b1ab4611d",
    name: "Chloe Hall",
    email: "chloehall@example.com",
    subject: "Re: Budget Approval",
    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",
    date: "2022-08-10T16:45:00",
    read: true,
    labels: ["work", "budget"],
  },
  {
    id: "6c9a7f94-8329-4d70-95d3-51f68c186ae1",
    name: "Samuel Turner",
    email: "samuelturner@example.com",
    subject: "Weekend Hike",
    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",
    date: "2022-07-28T17:30:00",
    read: false,
    labels: ["personal"],
  },
]

export type Mail = (typeof mails)[number]

export const accounts = [
  {
    label: "Equal Here",
    email: "equal.here374@fakeact.fun"
  },
  {
    label: "Alicia Koch",
    email: "blicia@fakemail.fun",
  },
  {
    label: "Alicia Koch",
    email: "clicia@fakemail.fun",
  },
]

export type Account = (typeof accounts)[number]

export const contacts = [
  {
    name: "Emma Johnson",
    email: "emma.johnson@example.com",
  },
  {
    name: "Liam Wilson",
    email: "liam.wilson@example.com",
  },
  {
    name: "Olivia Davis",
    email: "olivia.davis@example.com",
  },
  {
    name: "Noah Martinez",
    email: "noah.martinez@example.com",
  },
  {
    name: "Ava Taylor",
    email: "ava.taylor@example.com",
  },
  {
    name: "Lucas Brown",
    email: "lucas.brown@example.com",
  },
  {
    name: "Sophia Smith",
    email: "sophia.smith@example.com",
  },
  {
    name: "Ethan Wilson",
    email: "ethan.wilson@example.com",
  },
  {
    name: "Isabella Jackson",
    email: "isabella.jackson@example.com",
  },
  {
    name: "Mia Clark",
    email: "mia.clark@example.com",
  },
  {
    name: "Mason Lee",
    email: "mason.lee@example.com",
  },
  {
    name: "Layla Harris",
    email: "layla.harris@example.com",
  },
  {
    name: "William Anderson",
    email: "william.anderson@example.com",
  },
  {
    name: "Ella White",
    email: "ella.white@example.com",
  },
  {
    name: "James Thomas",
    email: "james.thomas@example.com",
  },
  {
    name: "Harper Lewis",
    email: "harper.lewis@example.com",
  },
  {
    name: "Benjamin Moore",
    email: "benjamin.moore@example.com",
  },
  {
    name: "Aria Hall",
    email: "aria.hall@example.com",
  },
  {
    name: "Henry Turner",
    email: "henry.turner@example.com",
  },
  {
    name: "Scarlett Adams",
    email: "scarlett.adams@example.com",
  },
]

export type Contact = (typeof contacts)[number]

export type MailsList = {
  message_id: string
  subject: string
  sender: string
  senderName: string
  cc: string
  content: string
  received_at: string
  is_read:number
}

export type AccountsList = { id: number; email_address: string; alias: string; created_at: string; };
  

================================================
FILE: app/src/components/mail/useMail.ts
================================================
import { atom, useAtom } from 'jotai';

import { type Mail } from './data';

type Config = {
    selected: Mail['id'] | null;
};

const configAtom = atom<Config>({
    selected: null,
});

export function useMail() {
    return useAtom(configAtom);
}


================================================
FILE: app/src/components/ui/alert-dialog.tsx
================================================
import * as React from "react"
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"

import { cn } from "@/lib/utils"
import { buttonVariants } from "@/components/ui/button"

const AlertDialog = AlertDialogPrimitive.Root

const AlertDialogTrigger = AlertDialogPrimitive.Trigger

const AlertDialogPortal = AlertDialogPrimitive.Portal

const AlertDialogOverlay = React.forwardRef<
  React.ElementRef<typeof AlertDialogPrimitive.Overlay>,
  React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
  <AlertDialogPrimitive.Overlay
    className={cn(
      "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",
      className
    )}
    {...props}
    ref={ref}
  />
))
AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName

const AlertDialogContent = React.forwardRef<
  React.ElementRef<typeof AlertDialogPrimitive.Content>,
  React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content>
>(({ className, ...props }, ref) => (
  <AlertDialogPortal>
    <AlertDialogOverlay />
    <AlertDialogPrimitive.Content
      ref={ref}
      className={cn(
        "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",
        className
      )}
      {...props}
    />
  </AlertDialogPortal>
))
AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName

const AlertDialogHeader = ({
  className,
  ...props
}: React.HTMLAttributes<HTMLDivElement>) => (
  <div
    className={cn(
      "flex flex-col space-y-2 text-center sm:text-left",
      className
    )}
    {...props}
  />
)
AlertDialogHeader.displayName = "AlertDialogHeader"

const AlertDialogFooter = ({
  className,
  ...props
}: React.HTMLAttributes<HTMLDivElement>) => (
  <div
    className={cn(
      "flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
      className
    )}
    {...props}
  />
)
AlertDialogFooter.displayName = "AlertDialogFooter"

const AlertDialogTitle = React.forwardRef<
  React.ElementRef<typeof AlertDialogPrimitive.Title>,
  React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Title>
>(({ className, ...props }, ref) => (
  <AlertDialogPrimitive.Title
    ref={ref}
    className={cn("text-lg font-semibold", className)}
    {...props}
  />
))
AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName

const AlertDialogDescription = React.forwardRef<
  React.ElementRef<typeof AlertDialogPrimitive.Description>,
  React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Description>
>(({ className, ...props }, ref) => (
  <AlertDialogPrimitive.Description
    ref={ref}
    className={cn("text-sm text-muted-foreground", className)}
    {...props}
  />
))
AlertDialogDescription.displayName =
  AlertDialogPrimitive.Description.displayName

const AlertDialogAction = React.forwardRef<
  React.ElementRef<typeof AlertDialogPrimitive.Action>,
  React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Action>
>(({ className, ...props }, ref) => (
  <AlertDialogPrimitive.Action
    ref={ref}
    className={cn(buttonVariants(), className)}
    {...props}
  />
))
AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName

const AlertDialogCancel = React.forwardRef<
  React.ElementRef<typeof AlertDialogPrimitive.Cancel>,
  React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Cancel>
>(({ className, ...props }, ref) => (
  <AlertDialogPrimitive.Cancel
    ref={ref}
    className={cn(
      buttonVariants({ variant: "outline" }),
      "mt-2 sm:mt-0",
      className
    )}
    {...props}
  />
))
AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName

export {
  AlertDialog,
  AlertDialogPortal,
  AlertDialogOverlay,
  AlertDialogTrigger,
  AlertDialogContent,
  AlertDialogHeader,
  AlertDialogFooter,
  AlertDialogTitle,
  AlertDialogDescription,
  AlertDialogAction,
  AlertDialogCancel,
}


================================================
FILE: app/src/components/ui/avatar.tsx
================================================
import * as React from "react"
import * as AvatarPrimitive from "@radix-ui/react-avatar"

import { cn } from "@/lib/utils"

const Avatar = React.forwardRef<
  React.ElementRef<typeof AvatarPrimitive.Root>,
  React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>
>(({ className, ...props }, ref) => (
  <AvatarPrimitive.Root
    ref={ref}
    className={cn(
      "relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full",
      className
    )}
    {...props}
  />
))
Avatar.displayName = AvatarPrimitive.Root.displayName

const AvatarImage = React.forwardRef<
  React.ElementRef<typeof AvatarPrimitive.Image>,
  React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>
>(({ className, ...props }, ref) => (
  <AvatarPrimitive.Image
    ref={ref}
    className={cn("aspect-square h-full w-full", className)}
    {...props}
  />
))
AvatarImage.displayName = AvatarPrimitive.Image.displayName

const AvatarFallback = React.forwardRef<
  React.ElementRef<typeof AvatarPrimitive.Fallback>,
  React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>
>(({ className, ...props }, ref) => (
  <AvatarPrimitive.Fallback
    ref={ref}
    className={cn(
      "flex h-full w-full items-center justify-center rounded-full bg-muted",
      className
    )}
    {...props}
  />
))
AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName

export { Avatar, AvatarImage, AvatarFallback }


================================================
FILE: app/src/components/ui/badge.tsx
================================================
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"

import { cn } from "@/lib/utils"

const badgeVariants = cva(
  "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",
  {
    variants: {
      variant: {
        default:
          "border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80",
        secondary:
          "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
        destructive:
          "border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80",
        outline: "text-foreground",
      },
    },
    defaultVariants: {
      variant: "default",
    },
  }
)

export interface BadgeProps
  extends React.HTMLAttributes<HTMLDivElement>,
    VariantProps<typeof badgeVariants> {}

function Badge({ className, variant, ...props }: BadgeProps) {
  return (
    <div className={cn(badgeVariants({ variant }), className)} {...props} />
  )
}

export { Badge, badgeVariants }


================================================
FILE: app/src/components/ui/button.tsx
================================================
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"

import { cn } from "@/lib/utils"

const buttonVariants = cva(
  "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",
  {
    variants: {
      variant: {
        default:
          "bg-primary text-primary-foreground shadow hover:bg-primary/90",
        destructive:
          "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
        outline:
          "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
        secondary:
          "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
        ghost: "hover:bg-accent hover:text-accent-foreground",
        link: "text-primary underline-offset-4 hover:underline",
      },
      size: {
        default: "h-9 px-4 py-2",
        sm: "h-8 rounded-md px-3 text-xs",
        lg: "h-10 rounded-md px-8",
        icon: "h-9 w-9",
        xs: "h-7 w-7"
      },
    },
    defaultVariants: {
      variant: "default",
      size: "default",
    },
  }
)

export interface ButtonProps
  extends React.ButtonHTMLAttributes<HTMLButtonElement>,
    VariantProps<typeof buttonVariants> {
  asChild?: boolean
}

const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
  ({ className, variant, size, asChild = false, ...props }, ref) => {
    const Comp = asChild ? Slot : "button"
    return (
      <Comp
        className={cn(buttonVariants({ variant, size, className }))}
        ref={ref}
        {...props}
      />
    )
  }
)
Button.displayName = "Button"

export { Button, buttonVariants }


================================================
FILE: app/src/components/ui/dialog.tsx
================================================
import * as React from "react"
import * as DialogPrimitive from "@radix-ui/react-dialog"
import { X } from "lucide-react"

import { cn } from "@/lib/utils"

const Dialog = DialogPrimitive.Root

const DialogTrigger = DialogPrimitive.Trigger

const DialogPortal = DialogPrimitive.Portal

const DialogClose = DialogPrimitive.Close

const DialogOverlay = React.forwardRef<
  React.ElementRef<typeof DialogPrimitive.Overlay>,
  React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
  <DialogPrimitive.Overlay
    ref={ref}
    className={cn(
      "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",
      className
    )}
    {...props}
  />
))
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName

const DialogContent = React.forwardRef<
  React.ElementRef<typeof DialogPrimitive.Content>,
  React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
>(({ className, children, ...props }, ref) => (
  <DialogPortal>
    <DialogOverlay />
    <DialogPrimitive.Content
      ref={ref}
      className={cn(
        "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",
        className
      )}
      {...props}
    >
      {children}
      <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">
        <X className="h-4 w-4" />
        <span className="sr-only">Close</span>
      </DialogPrimitive.Close>
    </DialogPrimitive.Content>
  </DialogPortal>
))
DialogContent.displayName = DialogPrimitive.Content.displayName

const DialogHeader = ({
  className,
  ...props
}: React.HTMLAttributes<HTMLDivElement>) => (
  <div
    className={cn(
      "flex flex-col space-y-1.5 text-center sm:text-left",
      className
    )}
    {...props}
  />
)
DialogHeader.displayName = "DialogHeader"

const DialogFooter = ({
  className,
  ...props
}: React.HTMLAttributes<HTMLDivElement>) => (
  <div
    className={cn(
      "flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
      className
    )}
    {...props}
  />
)
DialogFooter.displayName = "DialogFooter"

const DialogTitle = React.forwardRef<
  React.ElementRef<typeof DialogPrimitive.Title>,
  React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
>(({ className, ...props }, ref) => (
  <DialogPrimitive.Title
    ref={ref}
    className={cn(
      "text-lg font-semibold leading-none tracking-tight",
      className
    )}
    {...props}
  />
))
DialogTitle.displayName = DialogPrimitive.Title.displayName

const DialogDescription = React.forwardRef<
  React.ElementRef<typeof DialogPrimitive.Description>,
  React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
>(({ className, ...props }, ref) => (
  <DialogPrimitive.Description
    ref={ref}
    className={cn("text-sm text-muted-foreground", className)}
    {...props}
  />
))
DialogDescription.displayName = DialogPrimitive.Description.displayName

export {
  Dialog,
  DialogPortal,
  DialogOverlay,
  DialogTrigger,
  DialogClose,
  DialogContent,
  DialogHeader,
  DialogFooter,
  DialogTitle,
  DialogDescription,
}


================================================
FILE: app/src/components/ui/dropdown-menu.tsx
================================================
import * as React from "react"
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
import { Check, ChevronRight, Circle } from "lucide-react"

import { cn } from "@/lib/utils"

const DropdownMenu = DropdownMenuPrimitive.Root

const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger

const DropdownMenuGroup = DropdownMenuPrimitive.Group

const DropdownMenuPortal = DropdownMenuPrimitive.Portal

const DropdownMenuSub = DropdownMenuPrimitive.Sub

const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup

const DropdownMenuSubTrigger = React.forwardRef<
  React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
    inset?: boolean
  }
>(({ className, inset, children, ...props }, ref) => (
  <DropdownMenuPrimitive.SubTrigger
    ref={ref}
    className={cn(
      "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",
      inset && "pl-8",
      className
    )}
    {...props}
  >
    {children}
    <ChevronRight className="ml-auto" />
  </DropdownMenuPrimitive.SubTrigger>
))
DropdownMenuSubTrigger.displayName =
  DropdownMenuPrimitive.SubTrigger.displayName

const DropdownMenuSubContent = React.forwardRef<
  React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
>(({ className, ...props }, ref) => (
  <DropdownMenuPrimitive.SubContent
    ref={ref}
    className={cn(
      "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",
      className
    )}
    {...props}
  />
))
DropdownMenuSubContent.displayName =
  DropdownMenuPrimitive.SubContent.displayName

const DropdownMenuContent = React.forwardRef<
  React.ElementRef<typeof DropdownMenuPrimitive.Content>,
  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
>(({ className, sideOffset = 4, ...props }, ref) => (
  <DropdownMenuPrimitive.Portal>
    <DropdownMenuPrimitive.Content
      ref={ref}
      sideOffset={sideOffset}
      className={cn(
        "z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 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",
        className
      )}
      {...props}
    />
  </DropdownMenuPrimitive.Portal>
))
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName

const DropdownMenuItem = React.forwardRef<
  React.ElementRef<typeof DropdownMenuPrimitive.Item>,
  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
    inset?: boolean
  }
>(({ className, inset, ...props }, ref) => (
  <DropdownMenuPrimitive.Item
    ref={ref}
    className={cn(
      "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",
      inset && "pl-8",
      className
    )}
    {...props}
  />
))
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName

const DropdownMenuCheckboxItem = React.forwardRef<
  React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
>(({ className, children, checked, ...props }, ref) => (
  <DropdownMenuPrimitive.CheckboxItem
    ref={ref}
    className={cn(
      "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",
      className
    )}
    checked={checked}
    {...props}
  >
    <span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
      <DropdownMenuPrimitive.ItemIndicator>
        <Check className="h-4 w-4" />
      </DropdownMenuPrimitive.ItemIndicator>
    </span>
    {children}
  </DropdownMenuPrimitive.CheckboxItem>
))
DropdownMenuCheckboxItem.displayName =
  DropdownMenuPrimitive.CheckboxItem.displayName

const DropdownMenuRadioItem = React.forwardRef<
  React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
>(({ className, children, ...props }, ref) => (
  <DropdownMenuPrimitive.RadioItem
    ref={ref}
    className={cn(
      "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",
      className
    )}
    {...props}
  >
    <span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
      <DropdownMenuPrimitive.ItemIndicator>
        <Circle className="h-2 w-2 fill-current" />
      </DropdownMenuPrimitive.ItemIndicator>
    </span>
    {children}
  </DropdownMenuPrimitive.RadioItem>
))
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName

const DropdownMenuLabel = React.forwardRef<
  React.ElementRef<typeof DropdownMenuPrimitive.Label>,
  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
    inset?: boolean
  }
>(({ className, inset, ...props }, ref) => (
  <DropdownMenuPrimitive.Label
    ref={ref}
    className={cn(
      "px-2 py-1.5 text-sm font-semibold",
      inset && "pl-8",
      className
    )}
    {...props}
  />
))
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName

const DropdownMenuSeparator = React.forwardRef<
  React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
>(({ className, ...props }, ref) => (
  <DropdownMenuPrimitive.Separator
    ref={ref}
    className={cn("-mx-1 my-1 h-px bg-muted", className)}
    {...props}
  />
))
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName

const DropdownMenuShortcut = ({
  className,
  ...props
}: React.HTMLAttributes<HTMLSpanElement>) => {
  return (
    <span
      className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
      {...props}
    />
  )
}
DropdownMenuShortcut.displayName = "DropdownMenuShortcut"

export {
  DropdownMenu,
  DropdownMenuTrigger,
  DropdownMenuContent,
  DropdownMenuItem,
  DropdownMenuCheckboxItem,
  DropdownMenuRadioItem,
  DropdownMenuLabel,
  DropdownMenuSeparator,
  DropdownMenuShortcut,
  DropdownMenuGroup,
  DropdownMenuPortal,
  DropdownMenuSub,
  DropdownMenuSubContent,
  DropdownMenuSubTrigger,
  DropdownMenuRadioGroup,
}


================================================
FILE: app/src/components/ui/input.tsx
================================================
import * as React from "react"

import { cn } from "@/lib/utils"

const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<"input">>(
  ({ className, type, ...props }, ref) => {
    return (
      <input
        type={type}
        className={cn(
          "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",
          className
        )}
        ref={ref}
        {...props}
      />
    )
  }
)
Input.displayName = "Input"

export { Input }


================================================
FILE: app/src/components/ui/mail-card.tsx
================================================
import { useState } from 'react';
import { cn } from '@/lib/utils';
import { Mail, ChevronDown, ChevronUp, Trash2 } from 'lucide-react';
import { Button } from '@/components/ui/button';
import {
    AlertDialog,
    AlertDialogAction,
    AlertDialogCancel,
    AlertDialogContent,
    AlertDialogDescription,
    AlertDialogFooter,
    AlertDialogHeader,
    AlertDialogTitle,
    AlertDialogTrigger,
} from '@/components/ui/alert-dialog';

interface MailData {
    className?: string;
    sender: string;
    subject: string;
    date: string;
    name?: string;
    content: string;
    defaultShow: boolean;
    toDelete?: () => void;
}

export const MailCard = ({
    className,
    sender,
    subject,
    date,
    name,
    content,
    defaultShow,
    toDelete
}: MailData) => {
    const [show, setShow] = useState<boolean>(defaultShow);
    return (
        <div
            className={cn(
                'border border-gray-300 rounded-xl p-4 grid gap-1 mb-3 hover:border-green-600 hover:bg-green-50/10',
                show && 'border-2',
                className
            )}
        >
            <div className='flex gap-2 sm:flex-row sm:items-center flex-col'>
                <div className='flex-1 grid gap-1'>
                    <div
                        className={cn(
                            'flex gap-1 items-center text-xs text-gray-500 dark:text-gray-400 flex-1 pr-4',
                            show && 'text-gray-700'
                        )}
                    >
                        <Mail size={16} />
                        {name ? `${name} <${sender}>` : sender}
                    </div>
                    <div className='text-sm font-semibold'>{subject}</div>
                </div>
                <div className='flex gap-1.5 justify-between border-t pt-1 sm:border-none sm:pt-0 items-center shrink-0'>
                    <div className='text-xs text-gray-600 whitespace-nowrap'>
                        {date}
                    </div>
                    <Button
                        variant='outline'
                        size='sm'
                        className='shrink-0'
                        onClick={() => setShow(!show)}
                    >
                        {show ? <ChevronUp /> : <ChevronDown />}
                    </Button>
                </div>
            </div>
            {show && (
                <div className='pt-4 border-t leading-4 text-sm'>
                    <div dangerouslySetInnerHTML={{ __html: content }}></div>
                    <div className='border-t pt-2 text-right'>
                        <AlertDialog>
                            <AlertDialogTrigger asChild>
                                <Button size='xs' variant='ghost'>
                                    <Trash2 size={14} color='red' />
                                </Button>
                            </AlertDialogTrigger>
                            <AlertDialogContent>
                                <AlertDialogHeader>
                                    <AlertDialogTitle>
                                        Are you absolutely sure?
                                    </AlertDialogTitle>
                                    <AlertDialogDescription>
                                        Delete this mail cannot be undone. This will
                                        permanently remove your data from our servers.
                                    </AlertDialogDescription>
                                </AlertDialogHeader>
                                <AlertDialogFooter>
                                    <AlertDialogCancel>
                                        Cancel
                                    </AlertDialogCancel>
                                    <AlertDialogAction asChild>
                                        <Button variant="destructive" onClick={toDelete}>Delete</Button>
                                    </AlertDialogAction>
                                </AlertDialogFooter>
                            </AlertDialogContent>
                        </AlertDialog>
                    </div>
                </div>
            )}
        </div>
    );
};


================================================
FILE: app/src/components/ui/resizable.tsx
================================================
import { GripVertical } from "lucide-react"
import * as ResizablePrimitive from "react-resizable-panels"

import { cn } from "@/lib/utils"

const ResizablePanelGroup = ({
  className,
  ...props
}: React.ComponentProps<typeof ResizablePrimitive.PanelGroup>) => (
  <ResizablePrimitive.PanelGroup
    className={cn(
      "flex h-full w-full data-[panel-group-direction=vertical]:flex-col",
      className
    )}
    {...props}
  />
)

const ResizablePanel = ResizablePrimitive.Panel

const ResizableHandle = ({
  withHandle,
  className,
  ...props
}: React.ComponentProps<typeof ResizablePrimitive.PanelResizeHandle> & {
  withHandle?: boolean
}) => (
  <ResizablePrimitive.PanelResizeHandle
    className={cn(
      "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",
      className
    )}
    {...props}
  >
    {withHandle && (
      <div className="z-10 flex h-4 w-3 items-center justify-center rounded-sm border bg-border">
        <GripVertical className="h-2.5 w-2.5" />
      </div>
    )}
  </ResizablePrimitive.PanelResizeHandle>
)

export { ResizablePanelGroup, ResizablePanel, ResizableHandle }


================================================
FILE: app/src/components/ui/scroll-area.tsx
================================================
import * as React from "react"
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"

import { cn } from "@/lib/utils"

const ScrollArea = React.forwardRef<
  React.ElementRef<typeof ScrollAreaPrimitive.Root>,
  React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root>
>(({ className, children, ...props }, ref) => (
  <ScrollAreaPrimitive.Root
    ref={ref}
    className={cn("relative overflow-hidden", className)}
    {...props}
  >
    <ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit]">
      {children}
    </ScrollAreaPrimitive.Viewport>
    <ScrollBar />
    <ScrollAreaPrimitive.Corner />
  </ScrollAreaPrimitive.Root>
))
ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName

const ScrollBar = React.forwardRef<
  React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>,
  React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>
>(({ className, orientation = "vertical", ...props }, ref) => (
  <ScrollAreaPrimitive.ScrollAreaScrollbar
    ref={ref}
    orientation={orientation}
    className={cn(
      "flex touch-none select-none transition-colors",
      orientation === "vertical" &&
        "h-full w-2.5 border-l border-l-transparent p-[1px]",
      orientation === "horizontal" &&
        "h-2.5 flex-col border-t border-t-transparent p-[1px]",
      className
    )}
    {...props}
  >
    <ScrollAreaPrimitive.ScrollAreaThumb className="relative flex-1 rounded-full bg-border" />
  </ScrollAreaPrimitive.ScrollAreaScrollbar>
))
ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName

export { ScrollArea, ScrollBar }


================================================
FILE: app/src/components/ui/select.tsx
================================================
import * as React from "react"
import * as SelectPrimitive from "@radix-ui/react-select"
import { Check, ChevronDown, ChevronUp } from "lucide-react"

import { cn } from "@/lib/utils"

const Select = SelectPrimitive.Root

const SelectGroup = SelectPrimitive.Group

const SelectValue = SelectPrimitive.Value

const SelectTrigger = React.forwardRef<
  React.ElementRef<typeof SelectPrimitive.Trigger>,
  React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
  <SelectPrimitive.Trigger
    ref={ref}
    className={cn(
      "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",
      className
    )}
    {...props}
  >
    {children}
    <SelectPrimitive.Icon asChild>
      <ChevronDown className="h-4 w-4 opacity-50" />
    </SelectPrimitive.Icon>
  </SelectPrimitive.Trigger>
))
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName

const SelectScrollUpButton = React.forwardRef<
  React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
  React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
>(({ className, ...props }, ref) => (
  <SelectPrimitive.ScrollUpButton
    ref={ref}
    className={cn(
      "flex cursor-default items-center justify-center py-1",
      className
    )}
    {...props}
  >
    <ChevronUp className="h-4 w-4" />
  </SelectPrimitive.ScrollUpButton>
))
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName

const SelectScrollDownButton = React.forwardRef<
  React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
  React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
>(({ className, ...props }, ref) => (
  <SelectPrimitive.ScrollDownButton
    ref={ref}
    className={cn(
      "flex cursor-default items-center justify-center py-1",
      className
    )}
    {...props}
  >
    <ChevronDown className="h-4 w-4" />
  </SelectPrimitive.ScrollDownButton>
))
SelectScrollDownButton.displayName =
  SelectPrimitive.ScrollDownButton.displayName

const SelectContent = React.forwardRef<
  React.ElementRef<typeof SelectPrimitive.Content>,
  React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
>(({ className, children, position = "popper", ...props }, ref) => (
  <SelectPrimitive.Portal>
    <SelectPrimitive.Content
      ref={ref}
      className={cn(
        "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",
        position === "popper" &&
          "data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
        className
      )}
      position={position}
      {...props}
    >
      <SelectScrollUpButton />
      <SelectPrimitive.Viewport
        className={cn(
          "p-1",
          position === "popper" &&
            "h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]"
        )}
      >
        {children}
      </SelectPrimitive.Viewport>
      <SelectScrollDownButton />
    </SelectPrimitive.Content>
  </SelectPrimitive.Portal>
))
SelectContent.displayName = SelectPrimitive.Content.displayName

const SelectLabel = React.forwardRef<
  React.ElementRef<typeof SelectPrimitive.Label>,
  React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
>(({ className, ...props }, ref) => (
  <SelectPrimitive.Label
    ref={ref}
    className={cn("px-2 py-1.5 text-sm font-semibold", className)}
    {...props}
  />
))
SelectLabel.displayName = SelectPrimitive.Label.displayName

const SelectItem = React.forwardRef<
  React.ElementRef<typeof SelectPrimitive.Item>,
  React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
>(({ className, children, ...props }, ref) => (
  <SelectPrimitive.Item
    ref={ref}
    className={cn(
      "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",
      className
    )}
    {...props}
  >
    <span className="absolute right-2 flex h-3.5 w-3.5 items-center justify-center">
      <SelectPrimitive.ItemIndicator>
        <Check className="h-4 w-4" />
      </SelectPrimitive.ItemIndicator>
    </span>
    <SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
  </SelectPrimitive.Item>
))
SelectItem.displayName = SelectPrimitive.Item.displayName

const SelectSeparator = React.forwardRef<
  React.ElementRef<typeof SelectPrimitive.Separator>,
  React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
>(({ className, ...props }, ref) => (
  <SelectPrimitive.Separator
    ref={ref}
    className={cn("-mx-1 my-1 h-px bg-muted", className)}
    {...props}
  />
))
SelectSeparator.displayName = SelectPrimitive.Separator.displayName

export {
  Select,
  SelectGroup,
  SelectValue,
  SelectTrigger,
  SelectContent,
  SelectLabel,
  SelectItem,
  SelectSeparator,
  SelectScrollUpButton,
  SelectScrollDownButton,
}


================================================
FILE: app/src/components/ui/separator.tsx
================================================
import * as React from "react"
import * as SeparatorPrimitive from "@radix-ui/react-separator"

import { cn } from "@/lib/utils"

const Separator = React.forwardRef<
  React.ElementRef<typeof SeparatorPrimitive.Root>,
  React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
>(
  (
    { className, orientation = "horizontal", decorative = true, ...props },
    ref
  ) => (
    <SeparatorPrimitive.Root
      ref={ref}
      decorative={decorative}
      orientation={orientation}
      className={cn(
        "shrink-0 bg-border",
        orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]",
        className
      )}
      {...props}
    />
  )
)
Separator.displayName = SeparatorPrimitive.Root.displayName

export { Separator }


================================================
FILE: app/src/components/ui/skeleton.tsx
================================================
import { cn } from "@/lib/utils"

function Skeleton({
  className,
  ...props
}: React.HTMLAttributes<HTMLDivElement>) {
  return (
    <div
      className={cn("animate-pulse rounded-md bg-primary/10", className)}
      {...props}
    />
  )
}

export { Skeleton }


================================================
FILE: app/src/components/ui/sonner.tsx
================================================
import { useTheme } from "next-themes"
import { Toaster as Sonner } from "sonner"

type ToasterProps = React.ComponentProps<typeof Sonner>

const Toaster = ({ ...props }: ToasterProps) => {
  const { theme = "system" } = useTheme()

  return (
    <Sonner
      theme={theme as ToasterProps["theme"]}
      className="toaster group"
      toastOptions={{
        classNames: {
          toast:
            "group toast group-[.toaster]:bg-background group-[.toaster]:text-foreground group-[.toaster]:border-border group-[.toaster]:shadow-lg",
          description: "group-[.toast]:text-muted-foreground",
          actionButton:
            "group-[.toast]:bg-primary group-[.toast]:text-primary-foreground",
          cancelButton:
            "group-[.toast]:bg-muted group-[.toast]:text-muted-foreground",
        },
      }}
      {...props}
    />
  )
}

export { Toaster }


================================================
FILE: app/src/components/ui/textarea.tsx
================================================
import * as React from "react"

import { cn } from "@/lib/utils"

const Textarea = React.forwardRef<
  HTMLTextAreaElement,
  React.ComponentProps<"textarea">
>(({ className, ...props }, ref) => {
  return (
    <textarea
      className={cn(
        "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",
        className
      )}
      ref={ref}
      {...props}
    />
  )
})
Textarea.displayName = "Textarea"

export { Textarea }


================================================
FILE: app/src/components/ui/tooltip.tsx
================================================
import * as React from "react"
import * as TooltipPrimitive from "@radix-ui/react-tooltip"

import { cn } from "@/lib/utils"

const TooltipProvider = TooltipPrimitive.Provider

const Tooltip = TooltipPrimitive.Root

const TooltipTrigger = TooltipPrimitive.Trigger

const TooltipContent = React.forwardRef<
  React.ElementRef<typeof TooltipPrimitive.Content>,
  React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
>(({ className, sideOffset = 4, ...props }, ref) => (
  <TooltipPrimitive.Portal>
    <TooltipPrimitive.Content
      ref={ref}
      sideOffset={sideOffset}
      className={cn(
        "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",
        className
      )}
      {...props}
    />
  </TooltipPrimitive.Portal>
))
TooltipContent.displayName = TooltipPrimitive.Content.displayName

export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }


================================================
FILE: app/src/hooks/useCopyToClipboard.ts
================================================
import { useCallback, useState } from 'react';

type CopiedValue = string | null;

type CopyFn = (text: string) => Promise<boolean>;

export function useCopyToClipboard(): [CopiedValue, CopyFn] {
    const [copiedText, setCopiedText] = useState<CopiedValue>(null);

    const copy: CopyFn = useCallback(async (text) => {
        if (!navigator?.clipboard) {
            console.warn('Clipboard not supported');
            return false;
        }

        // Try to save to clipboard then save it in the state if worked
        try {
            await navigator.clipboard.writeText(text);
            setCopiedText(text);
            return true;
        } catch (error) {
            console.warn('Copy failed', error);
            setCopiedText(null);
            return false;
        }
    }, []);

    return [copiedText, copy];
}


================================================
FILE: app/src/hooks/useGeneratorMail.ts
================================================
import { useEffect, useState } from 'react';
import { getRandomMail } from '@/lib/store';

export const useGeneratorMail = (): [string, () => void] => {
    const [address, setAddress] = useState<string>('');

    const generatorNewMail = () => {
        const mailAddress = getRandomMail(true);
        setAddress(mailAddress);
    }

    useEffect(() => {
        const mailAddress = getRandomMail();
        setAddress(mailAddress);
    }, []);

    return [address, generatorNewMail];
}

================================================
FILE: app/src/layouts/Layout.astro
================================================
---
import '@/styles/globals.css';
import { SEO } from 'astro-seo';
const { pathname, origin } = Astro.url;
const resolvedImageWithDomain = `${origin}/image.png`;
const canonicalURL = new URL(pathname, origin);
const { title, description, image } = Astro.props;
const headTitle = title || 'FakeMail';
const headDesc =
    description ||
    'Free temporary email (temp mail) service. Get a disposable email address instantly. Emails auto-delete after 2 hours.';
const ogImage = image || resolvedImageWithDomain;
---

<!doctype html>
<html lang='en'>
    <head>
        <meta charset='UTF-8' />
        <meta name='viewport' content='width=device-width' />
        <link rel='icon' type='image/svg+xml' href='/favicon.svg' />
        <meta
            name='keywords'
            content='fake, temp mail, fake mail, free, temporary, email, disposable, mail, email address'
        />
        <meta name='generator' content={Astro.generator} />
        <SEO
            title={headTitle}
            description={headDesc}
            canonical={canonicalURL}
            twitter={{
                creator: '@LiWen563',
                site: '@LiWen563',
                card: 'summary_large_image',
                imageAlt: headTitle,
                description: headDesc,
            }}
            openGraph={{
                basic: {
                    url: canonicalURL,
                    type: 'website',
                    title: headTitle,
                    image: ogImage,
                },
                image: {
                    url: ogImage,
                    alt: headTitle,
                },
            }}
        />
        <script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-4182383692865291" crossorigin="anonymous"></script>
    </head>
    <body>
        <slot />
    </body>
</html>

<script is:inline>
    const getThemePreference = () => {
        if (
            typeof localStorage !== 'undefined' &&
            localStorage.getItem('theme')
        ) {
            return localStorage.getItem('theme');
        }
        return window.matchMedia('(prefers-color-scheme: dark)').matches
            ? 'dark'
            : 'light';
    };
    const isDark = getThemePreference() === 'dark';
    document.documentElement.classList[isDark ? 'add' : 'remove']('dark');

    if (typeof localStorage !== 'undefined') {
        const observer = new MutationObserver(() => {
            const isDark = document.documentElement.classList.contains('dark');
            localStorage.setItem('theme', isDark ? 'dark' : 'light');
        });
        observer.observe(document.documentElement, {
            attributes: true,
            attributeFilter: ['class'],
        });
    }
</script>


================================================
FILE: app/src/lib/demo.ts
================================================
export default {
    status: 'ok',
    code: 200,
    msg: 'Mail available',
    stats: { count: '9832' },
    mails: [
        {
            suffix: '5cc52bcf',
            recipient: 'men.strip311@fakeact.fun',
            sender: 'test@gmail.com',
            name: 'Test User',
            subject: 'Your verification code is 662144',
            'content-plain':
                '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',
            'content-plain-formatted':
                '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>',
            'content-html':'',
            date: '2025-01-05T14:58:45.000Z',
        },
        {
            suffix: '34e222s',
            recipient: 'men.strip311@fakeact.fun',
            sender: 'test@gmail.com',
            subject: 'Your verification code is 33322222',
            'content-plain':
                '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',
            'content-plain-formatted':
                '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>',
            'content-html': '',
            date: '2025-01-06T14:58:45.000Z',
        },
    ],
};


================================================
FILE: app/src/lib/store.ts
================================================
import { generate } from 'random-words';

export const getRandomMail = (isNew: boolean = false) => {
    const domain = '@fakeact.fun';
    if (!isNew) {
        const receivingEmail = window?.localStorage.getItem('receivingEmail');
        if (receivingEmail && receivingEmail.includes(domain)) {
            return receivingEmail;
        }
    }
    const words = generate({ exactly: 2, maxLength: 5 });
    const alt =
        words[0] + '.' + words[1] + Math.floor(Math.random() * 1000) + domain;
    window?.localStorage.setItem('receivingEmail', alt);
    return alt;
};


================================================
FILE: app/src/lib/utils.ts
================================================
import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";

export function cn(...inputs: ClassValue[]) {
  return twMerge(clsx(inputs))
};

================================================
FILE: app/src/middleware.ts
================================================
import { clerkMiddleware, createRouteMatcher } from '@clerk/astro/server';

const isProtectedRoute = createRouteMatcher(['/dashboard(.*)', '/settings(.*)']);

export const onRequest = clerkMiddleware((auth, context) => {

    const { redirectToSignIn, userId } = auth();

    if (!userId && isProtectedRoute(context.request)) {
        // Add custom logic to run before redirecting

        return redirectToSignIn({
            returnBackUrl: context.request.url,
            
        });
    }
}, {
    publishableKey: import.meta.env.CLERK_PUBLISHABLE_KEY,
    secretKey: import.meta.env.CLERK_SECRET_KEY,
    signInUrl: '/sign-in',
    signUpUrl: '/sign-up',
});


================================================
FILE: app/src/pages/api/delete.ts
================================================
import type { APIRoute, APIContext } from 'astro';

export const POST: APIRoute = async ({ request, locals }: APIContext) => {
    const { key } = await request.json();
    if (!key) {
        return new Response(JSON.stringify({
            status: 'bad request',
            code: 400,
            msg: "'key' params required!",
        }));
    }
    
    await locals.runtime.env.POST_DB?.delete(key);

    return new Response(
        JSON.stringify({
            status: 'ok',
            code: 200,
            msg: 'Mail deleted',
        })
    );
};


================================================
FILE: app/src/pages/api/deleteMails.ts
================================================
import type { APIRoute, APIContext } from 'astro';

export const POST: APIRoute = async ({ request, locals }: APIContext) => {
    const { message_id } = await request.json();
    if (message_id === undefined || message_id === '') {
        return new Response(JSON.stringify({
            status: 'bad request',
            code: 400,
            msg: "'message_id' query required!",
        }));
    }
    const { userId } = locals.auth();
    if (!userId) {
        return new Response(JSON.stringify({
            status: 'bad request',
            code: 400,
            msg: "please login!",
        }));
    }
    await locals.runtime.env.MAIL_DB?.batch([
        locals.runtime.env.MAIL_DB?.prepare('DELETE FROM email_tag_relations WHERE email_id = (SELECT id FROM emails WHERE message_id = ?)').bind(message_id),
        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),
        locals.runtime.env.MAIL_DB?.prepare('DELETE FROM emails WHERE message_id = ?').bind(message_id)
    ]);
    return new Response(
        JSON.stringify({
            status: 'ok',
            code: 200,
            msg: 'Success',
        })
    );
}

================================================
FILE: app/src/pages/api/generate.ts
================================================
import { generate } from 'random-words';
import type { APIRoute, APIContext } from 'astro';

export const POST: APIRoute = async ({ request, locals }: APIContext) => {
    const domain = '@fakeact.fun';
    const { remark } = await request.json();
    if ((remark || '').length > 400) return new Response(JSON.stringify({
        status: 'bad request',
        code: 400,
        msg: "remark is too long!",
    }));
    const { userId } = locals.auth();
    if (!userId) {
        return new Response(JSON.stringify({
            status: 'bad request',
            code: 400,
            msg: "please login!",
        }));
    }
    const stmt = locals.runtime.env.MAIL_DB?.prepare('Select count(*) from user_email_addresses where user_id = ?').bind(userId);
    const returnValue = await stmt.run().catch((e) => {
        console.error(e);
        return { results: [] };
    });
    const results = returnValue.results as { 'count(*)': number }[];
    if (results[0]['count(*)'] >= 5) {
        return new Response(JSON.stringify({
            status: 'bad request',
            code: 400,
            msg: "You can't create more than 5 accounts!",
        }));
    }
    const words = generate({ exactly: 2, maxLength: 5 });
    const alt = words[0] + '.' + words[1] + Math.floor(Math.random() * 1000) + domain;
    const stmt2 = locals.runtime.env.MAIL_DB?.prepare('Insert into user_email_addresses (user_id, email_address, alias) values (?, ?, ?)').bind(userId, alt, remark);
    const msg = await stmt2.run().then(() => {
        return {
            status: 'ok',
            code: 200,
            msg: 'Success',
        }
    }).catch((e) => {
        console.error(e);
        return {
            status: 'bad request',
            code: 400,
            msg: 'Something wrong or exists, please try again!',
        }
    });
    return new Response(JSON.stringify(msg));
};

================================================
FILE: app/src/pages/api/get.ts
================================================
import type { APIRoute, APIContext } from 'astro';
import demo from '@/lib/demo';

function validateString(str: string): boolean {
    const regex = /^[a-zA-Z0-9]+\.[a-zA-Z0-9]+\d{3}$/;
    return regex.test(str);
}

export const GET: APIRoute = async ({ request, locals }: APIContext) => {
    const address = new URL(request.url).searchParams.get('address');
    if (import.meta.env.DEV) {
        return new Response(JSON.stringify(demo));
    }
    if (address === undefined || address === '') {
        return new Response(JSON.stringify({
            status: 'bad request',
            code: 400,
            msg: "'address' query required!",
        }));
    }

    const addresName = (address || '').split('@')[0];
    if (!validateString(addresName)) {
        return new Response(JSON.stringify({
            status: 'bad request',
            code: 400,
            msg: "'address' query invalid!",
        }));
    }

    // get mails with prefix (user@example.com) - suffix (-8dh2m901) acts as identifier
    const mailKeys = await locals.runtime.env.POST_DB?.get(`${address}-keys`);
    const statsCount = await locals.runtime.env.POST_DB?.get('stats-count');
    let mailKeysArr: string[] = [];
    try {
        mailKeysArr = JSON.parse(mailKeys || '[]');
    } catch (error) {
        //
    }

    // if no emails are stored under the prefix key, return empty json
    if (!mailKeys || mailKeysArr.length === 0) {
        return new Response(JSON.stringify({
            status: 'ok',
            code: 200,
            msg: 'No available emails',
            stats: {
                count: statsCount,
            },
            mails: [],
        }));
    }

    // create array of received mails
    let mails = [];
    for (const key of mailKeysArr) {
        const mail_res = await locals.runtime.env.POST_DB.get(`${address}-${key}`);
        // convert string back to JSON
        // @ts-ignore
        if (mail_res) mails.push(JSON.parse(mail_res));
    }

    return new Response(JSON.stringify({
        status: 'ok',
        code: 200,
        msg: 'Mail available',
        stats: {
            count: statsCount,
        },
        mails,
    }));
};


================================================
FILE: app/src/pages/api/getMails.ts
================================================
import type { APIRoute, APIContext } from 'astro';

export const GET: APIRoute = async ({ request, locals }: APIContext) => {
    const id = new URL(request.url).searchParams.get('id');
    if (id === undefined || id === '') {
        return new Response(JSON.stringify({
            status: 'bad request',
            code: 400,
            msg: "'id' query required!",
        }));
    }
    const { userId } = locals.auth();
    if (!userId) {
        // redirect to login page
        return new Response(JSON.stringify({
            status: 'bad request',
            code: 502,
            msg: "please login!",
        }));
    }
    const stmt = locals.runtime.env.MAIL_DB?.prepare('Select id from user_email_addresses where user_id = ? and id = ?').bind(userId, id);
    const returnValue = await stmt.run().catch((e) => {
        console.error(e);
        return { results: [] };
    });
    if (returnValue.results.length === 0) {
        return new Response(JSON.stringify({
            status: 'bad request',
            code: 400,
            msg: "Email not found!",
        }));
    }
    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);
    const returnValue2 = await stmt2.run().catch((e) => {
        console.error(e);
        return { results: [] };
    });
    return new Response(JSON.stringify({
        status: 'ok',
        code: 200,
        msg: 'Mail available',
        mails: returnValue2.results.map(row => {
            return {
                message_id: row.message_id as string,
                subject: row.subject as string,
                sender: row.sender as string,
                senderName: row.sender_name as string,
                cc: row.cc as string,
                content: (row.body_text || row.body_html) as string,
                received_at: row.received_at as string,
                is_read: row.is_read as number
            }
        })
    }));
};

================================================
FILE: app/src/pages/api/remarkMail.ts
================================================
import type { APIRoute, APIContext } from 'astro';

export const POST: APIRoute = async ({ request, locals }: APIContext) => {
    const { id, remark } = await request.json();
    if (!id) return new Response(JSON.stringify({
        status: 'bad request',
        code: 400,
        msg: "id is required!",
    }));
    if ((remark || '').length > 400) return new Response(JSON.stringify({
        status: 'bad request',
        code: 400,
        msg: "remark is too long!",
    }));
    const { userId } = locals.auth();
    if (!userId) {
        return new Response(JSON.stringify({
            status: 'bad request',
            code: 400,
            msg: "please login!",
        }));
    }
    const stmt = locals.runtime.env.MAIL_DB?.prepare('Update user_email_addresses set alias = ? where user_id = ? and id = ?').bind(remark, userId, id);
    const msg = await stmt.run().then(() => {
        return {
            status: 'ok',
            code: 200,
            msg: 'Success',
        }
    }).catch((e) => {
        console.error(e);
        return {
            status: 'bad request',
            code: 400,
            msg: 'Something wrong or exists, please try again!',
        }
    });
    return new Response(JSON.stringify(msg));
};

================================================
FILE: app/src/pages/api/updateStatus.ts
================================================
import type { APIRoute, APIContext } from 'astro';

export const POST: APIRoute = async ({ request, locals }: APIContext) => {
    const { message_id, is_read } = await request.json();
    if (message_id === undefined || message_id === '') {
        return new Response(JSON.stringify({
            status: 'bad request',
            code: 400,
            msg: "'message_id' query required!",
        }));
    }
    const { userId } = locals.auth();
    if (!userId) {
        return new Response(JSON.stringify({
            status: 'bad request',
            code: 400,
            msg: "please login!",
        }));
    }
    const readStatus = +is_read === 0 ? 0 : 1;
    const stmt = locals.runtime.env.MAIL_DB?.prepare(`
INSERT OR REPLACE INTO email_status 
(email_id, user_id, is_read, created_at, updated_at)
SELECT 
    e.id,
    ?,
    ?,
    COALESCE((SELECT created_at FROM email_status WHERE email_id = e.id AND user_id = ?), CURRENT_TIMESTAMP),
    CURRENT_TIMESTAMP
FROM emails e
JOIN user_email_addresses uea ON e.user_email_id = uea.id
WHERE e.message_id = ? 
AND uea.user_id = ?
    `).bind(userId, readStatus, userId, message_id, userId);
    await stmt.run().catch(error => console.log(error));
    return new Response(
        JSON.stringify({
            status: 'ok',
            code: 200,
            msg: 'Success',
        })
    );
}

================================================
FILE: app/src/pages/dashboard.astro
================================================
---
import Layout from '@/layouts/Layout.astro';
import Bar from '@/components/Bar.astro';
import Mail from '@/components/mail/Mail';
import type { MailsList } from '@/components/mail/data';
const layout = Astro.cookies.get("react-resizable-panels:layout:mail");
const collapsed = Astro.cookies.get("react-resizable-panels:collapsed");
const defaultLayout = layout ? JSON.parse(layout.value) : undefined;
const defaultCollapsed = collapsed ? JSON.parse(collapsed.value) : undefined;
const { userId } = Astro.locals.auth();
const accounts: {id: number; email_address: string}[] = [];
const mails: MailsList[] = [];
if(Astro.locals.runtime) {
    const { env } = Astro.locals.runtime;
    const stmt = env.MAIL_DB.prepare("SELECT id,email_address FROM user_email_addresses WHERE user_id = ?").bind(userId);
    const returnValue = await stmt.run().catch((e) => {
        console.error(e);
        return { results: [] };
    });
    if (returnValue.results.length) {
        for(const row of returnValue.results) {
            accounts.push({
                id: row.id as number,
                email_address: row.email_address as string
            });
        }
        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);
        const returnMails = await stmt2.run().catch((e) => {
            console.error(e);
            return { results: [] };
        });
        for(const row of returnMails.results) {
            mails.push({
                message_id: row.message_id as string,
                subject: row.subject as string,
                sender: row.sender as string,
                senderName: row.sender_name as string,
                cc: row.cc as string,
                content: (row.body_text || row.body_html) as string,
                received_at: row.received_at as string,
                is_read: row.is_read as number
            });
        }
    }
}
---

<Layout>
    <div class="flex flex-col h-screen">
        <Bar />
        {accounts.length === 0 ?
            <div class="flex-1 flex items-center justify-center">
                <div class="text-center">
                    <h2 class="text-2xl font-bold">No temp addresses yet</h2>
                    <p class="text-gray-500">Go to Settings to create your first disposable email address. You can then receive and read mail here.</p>
                    <a href="/settings" class="text-blue-500 hover:underline">Add email address</a>
                </div>
            </div>:
            <Mail
                accounts={accounts}
                mails={mails}
                defaultLayout={defaultLayout}
                defaultCollapsed={defaultCollapsed}
                navCollapsedSize={4}
                client:load
            />
        }
    </div>
</Layout>

================================================
FILE: app/src/pages/index.astro
================================================
---
import Header from '../components/Header.astro';
import Main from '../components/Main.astro';
import Footer from '../components/Footer.astro';
import Layout from '@/layouts/Layout.astro';
---

<Layout
    title="FakeMail - Free temporary email | Disposable email generator"
    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."
>
    <Header />
    <Main />
    <Footer />
</Layout>

<script type="application/ld+json" set:html={JSON.stringify({
    "@context": "https://schema.org",
    "@type": "WebApplication",
    "name": "Fake Mail",
    "description": "Fake email generator, delete emails 2 hours after receiving",
    "operatingSystem": "ALL",
    "applicationCategory": "BrowserApplication",
    "browserRequirements": "requires HTML5 support",
    "aggregateRating": {
        "@type": "AggregateRating",
        "ratingValue": "4.6",
        "ratingCount": "4872",
        "bestRating": "5"
    },
    "offers": {
        "@type": "Offer",
        "price": "0",
        "priceCurrency": "USD"
    }
})}/>


================================================
FILE: app/src/pages/settings.astro
================================================
---
import Layout from '@/layouts/Layout.astro';
import Bar from '@/components/Bar.astro';
import { ArrowLeft } from 'lucide-react';
import {Accounts} from '@/components/mail/Accounts';
const { userId } = Astro.locals.auth();
const accounts: {id: number; email_address: string; alias: string; created_at: string;}[] = [];
if(Astro.locals.runtime) {
    const { env } = Astro.locals.runtime;
    const stmt = env.MAIL_DB.prepare("SELECT id,email_address,alias,created_at FROM user_email_addresses WHERE user_id = ?").bind(userId);
    const returnValue = await stmt.run().catch((e) => {
        console.error(e);
        return { results: [] };
    });
    if (returnValue.results.length) {
        for(const row of returnValue.results) {
            accounts.push({
                id: row.id as number,
                email_address: row.email_address as string,
                alias: row.alias as string,
                created_at: row.created_at as string
            });
        }
    }
}
---

<Layout>
    <Bar />
    <div class="container mx-auto">
        <div class="p-4">
            <a
                href="/dashboard"
                title="Dashboard"
                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"
            ><ArrowLeft size={16} />Back to Inbox</a>
            <h1 class="text-2xl font-bold">Settings</h1>
            <p class="text-sm text-gray-500">Manage your email addresses and notes.</p>
        </div>
        <div class="p-4">
            <Accounts client:load list={accounts} />
        </div>
    </div>
</Layout>

================================================
FILE: app/src/pages/sign-in.astro
================================================
---
import { SignIn } from '@clerk/astro/components';
import Layout from '@/layouts/Layout.astro';
---

<Layout>
    <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")`>
        <div class="bg-gray-800/90">
            <div class="container mx-auto py-10 px-4 text-white text-center">
                <h1 class="font-semibold text-xl mb-2">Login to keep data permanently</h1>
                <p class="text-gray-400 text-sm">You can record multiple email addresses and use them for free</p>
            </div>
        </div>
        <div class="bg-gradient-to-b from-gray-800/90 from-50% to-50% to-transparent">
            <div class='container mx-auto flex flex-col items-center'>
                <SignIn path="/sign-in" />
            </div>
        </div>
    </div>
</Layout>


================================================
FILE: app/src/pages/sign-up.astro
================================================
---
import { SignUp } from '@clerk/astro/components';
import Layout from '@/layouts/Layout.astro';
---

<Layout>
    <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")`>
        <div class="bg-gray-800/90">
            <div class="container mx-auto py-10 px-4 text-white text-center">
                <h1 class="font-semibold text-xl mb-2">Register to keep data permanently</h1>
                <p class="text-gray-400 text-sm">You can record multiple email addresses and use them for free</p>
            </div>
        </div>
        <div class="bg-gradient-to-b from-gray-800/90 from-50% to-50% to-transparent">
            <div class='container mx-auto flex flex-col items-center'>
                <SignUp path="/sign-up" />
            </div>
        </div>
    </div>
</Layout>


================================================
FILE: app/src/styles/globals.css
================================================
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
  :root {
    --background: 0 0% 100%;
    --foreground: 222.2 84% 4.9%;
    --card: 0 0% 100%;
    --card-foreground: 222.2 84% 4.9%;
    --popover: 0 0% 100%;
    --popover-foreground: 222.2 84% 4.9%;
    --primary: 222.2 47.4% 11.2%;
    --primary-foreground: 210 40% 98%;
    --secondary: 210 40% 96.1%;
    --secondary-foreground: 222.2 47.4% 11.2%;
    --muted: 210 40% 96.1%;
    --muted-foreground: 215.4 16.3% 46.9%;
    --accent: 210 40% 96.1%;
    --accent-foreground: 222.2 47.4% 11.2%;
    --destructive: 0 84.2% 60.2%;
    --destructive-foreground: 210 40% 98%;
    --border: 214.3 31.8% 91.4%;
    --input: 214.3 31.8% 91.4%;
    --ring: 222.2 84% 4.9%;
    --chart-1: 12 76% 61%;
    --chart-2: 173 58% 39%;
    --chart-3: 197 37% 24%;
    --chart-4: 43 74% 66%;
    --chart-5: 27 87% 67%;
    --radius: 0.5rem
  }
  .dark {
    --background: 222.2 84% 4.9%;
    --foreground: 210 40% 98%;
    --card: 222.2 84% 4.9%;
    --card-foreground: 210 40% 98%;
    --popover: 222.2 84% 4.9%;
    --popover-foreground: 210 40% 98%;
    --primary: 210 40% 98%;
    --primary-foreground: 222.2 47.4% 11.2%;
    --secondary: 217.2 32.6% 17.5%;
    --secondary-foreground: 210 40% 98%;
    --muted: 217.2 32.6% 17.5%;
    --muted-foreground: 215 20.2% 65.1%;
    --accent: 217.2 32.6% 17.5%;
    --accent-foreground: 210 40% 98%;
    --destructive: 0 62.8% 30.6%;
    --destructive-foreground: 210 40% 98%;
    --border: 217.2 32.6% 17.5%;
    --input: 217.2 32.6% 17.5%;
    --ring: 212.7 26.8% 83.9%;
    --chart-1: 220 70% 50%;
    --chart-2: 160 60% 45%;
    --chart-3: 30 80% 55%;
    --chart-4: 280 65% 60%;
    --chart-5: 340 75% 55%
  }
}
@layer base {
  * {
    @apply border-border;
  }
  body {
    @apply bg-background text-foreground;
  }
}


================================================
FILE: app/tailwind.config.mjs
================================================
/** @type {import('tailwindcss').Config} */
export default {
    darkMode: ['class'],
    content: ['./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}'],
	theme: {
    	extend: {
    		borderRadius: {
    			lg: 'var(--radius)',
    			md: 'calc(var(--radius) - 2px)',
    			sm: 'calc(var(--radius) - 4px)'
    		},
    		colors: {
    			background: 'hsl(var(--background))',
    			foreground: 'hsl(var(--foreground))',
    			card: {
    				DEFAULT: 'hsl(var(--card))',
    				foreground: 'hsl(var(--card-foreground))'
    			},
    			popover: {
    				DEFAULT: 'hsl(var(--popover))',
    				foreground: 'hsl(var(--popover-foreground))'
    			},
    			primary: {
    				DEFAULT: 'hsl(var(--primary))',
    				foreground: 'hsl(var(--primary-foreground))'
    			},
    			secondary: {
    				DEFAULT: 'hsl(var(--secondary))',
    				foreground: 'hsl(var(--secondary-foreground))'
    			},
    			muted: {
    				DEFAULT: 'hsl(var(--muted))',
    				foreground: 'hsl(var(--muted-foreground))'
    			},
    			accent: {
    				DEFAULT: 'hsl(var(--accent))',
    				foreground: 'hsl(var(--accent-foreground))'
    			},
    			destructive: {
    				DEFAULT: 'hsl(var(--destructive))',
    				foreground: 'hsl(var(--destructive-foreground))'
    			},
    			border: 'hsl(var(--border))',
    			input: 'hsl(var(--input))',
    			ring: 'hsl(var(--ring))',
    			chart: {
    				'1': 'hsl(var(--chart-1))',
    				'2': 'hsl(var(--chart-2))',
    				'3': 'hsl(var(--chart-3))',
    				'4': 'hsl(var(--chart-4))',
    				'5': 'hsl(var(--chart-5))'
    			}
    		}
    	}
    },
	plugins: [require("tailwindcss-animate")],
}


================================================
FILE: app/tsconfig.json
================================================
{
  "extends": "astro/tsconfigs/strict",
  "include": [
    ".astro/types.d.ts",
    "**/*"
  ],
  "exclude": [
    "dist"
  ],
  "compilerOptions": {
    "jsx": "react-jsx",
    "jsxImportSource": "react",
    "baseUrl": ".",
    "paths": {
      "@/*": [
        "./src/*"
      ]
    }
  }
}

================================================
FILE: app/wrangler.example.toml
================================================
name = "fakemail"
main = "./dist/_worker.js"
compatibility_date = "2025-01-14"
compatibility_flags = [ "nodejs_compat" ]
kv_namespaces = [
    { binding = "POST_DB", id = "b1002ec7a3d74b9cb64504ea6d6a5586", preview_id = "238c3081e8ae49f091c08df88cf6c079" },
]

d1_databases = [
    { binding = "MAIL_DB", database_name = "fake-mail", database_id = "852bdefb-ec72-4589-8de3-f49bb2d01883", preview_database_id = "686ae900-aaea-4c69-8499-b0c8ea8c960d"},
]

[vars]
PUBLIC_TURNSTILE_SITE_KEY="0x4AAAAAAA4rlXXIP3FANjmG"
SECRET_KEY="xxx"
PUBLIC_CLERK_PUBLISHABLE_KEY="pk_test_xxxx"
CLERK_SECRET_KEY="sk_test_xxxx"
PUBLIC_CLERK_SIGN_IN_URL="/sign-in"
PUBLIC_CLERK_SIGN_UP_URL="/sign-up"


# npx wrangler d1 create fake-mail
# npx wrangler d1 execute fake-mail --local --file=./email-platform-schema.sql
# npx wrangler d1 execute fake-mail --local --command="SELECT COUNT(*) FROM user_email_addresses"


================================================
FILE: mailbox/package.json
================================================
{
  "name": "mailbox",
  "version": "1.0.0",
  "main": "index.js",
  "scripts": {
    "start": "wrangler dev",
    "deploy": "wrangler deploy",
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "MIT",
  "description": "",
  "dependencies": {
    "postal-mime": "^2.3.2"
  },
  "devDependencies": {
    "@cloudflare/workers-types": "^4.20241230.0",
    "typescript": "^5.7.2",
    "wrangler": "^3.99.0"
  }
}


================================================
FILE: mailbox/src/index.ts
================================================
// @ts-ignore
import PostalMime from 'postal-mime';
import insertMails from './insertMails';

export interface Env {
    POST_DB: KVNamespace;
    MAIL_DB: D1Database;
}

interface AccoutAddress {
    id: number;
    user_id: string;
}

export default {
    async email(message: ForwardableEmailMessage, env: Env) {
        // parse ReadableStream message to email
        const parser = new PostalMime();
        const body = await new Response(message.raw).arrayBuffer();
        const email = await parser.parse(body);

        // count email for statistics
        let prev_count = await env.POST_DB.get('stats-count');
        if (prev_count === null) {
            prev_count = '0';
        }
        await env.POST_DB.put('stats-count', String(parseInt(prev_count) + 1));

        const sender = email.from.address;
        const senderName = email.from.name;
        const recipient = email.to?.length ? email.to[0].address : '';

        if (!recipient) return;

        // generate random string (len = 8)
        const suffix = Math.random().toString(16).slice(2, 10);

        // get D1 email address
        const stmt = env.MAIL_DB.prepare('SELECT id, user_id FROM user_email_addresses WHERE email_address = ?').bind(recipient);
        const returnValue = await stmt.run().catch((e) => {
            console.error(e);
            return { results: [] as AccoutAddress[] };
        });

        if (returnValue.results.length > 0) {
            // if email exists in D1, insert email to D1
            const { id, user_id } = returnValue.results[0] as AccoutAddress;
            await insertMails({
                mail_id: id,
                user_id: user_id,
                message_id: email.messageId || `${recipient}-${suffix}`,
                subject: email.subject || '',
                sender: sender || '',
                senderName: senderName || '',
                recipient,
                content_type: 'text/html',
                body_text: email.text || '',
                body_html: email.html || '',
                received_at: email.date || new Date().toISOString(),
                cc: email.cc?.map((cc) => cc.address).join(',') || '',
                bcc: email.bcc?.map((bcc) => bcc.address).join(',') || '',
            }, env);
            return;
        }

        let keys = await env.POST_DB.get(`${recipient}-keys`);
        if (!keys) {
            keys = JSON.stringify([suffix]);
        } else {
            const _keys = JSON.parse(keys);
            _keys.push(suffix);
            keys = JSON.stringify(_keys);
        }
        await env.POST_DB.put(`${recipient}-keys`, keys, {
            expirationTtl: 7200,
        });
        // make key address followed by suffix (user@example.com-8dh2m901)
        // suffix acts as the key, while the email is used for assignment
        const key = recipient + '-' + suffix;

        let formatted_content = email.text?.replaceAll('\n', '<br>');

        // for an example email JSON see example.json
        const data = {
            suffix: suffix,
            recipient: recipient,
            sender: sender,
            name: senderName,
            subject: email.subject,
            'content-plain': email.text,
            'content-plain-formatted': formatted_content,
            'content-html': email.html,
            date: email.date,
        };

        await env.POST_DB.put(key, JSON.stringify(data), {
            expirationTtl: 7200,
        });
    },
};


================================================
FILE: mailbox/src/insertMails.ts
================================================
interface Env {
    MAIL_DB: D1Database;
}

interface EmailData {
    mail_id: number;
    user_id: string;
    message_id: string;
    subject: string;
    sender: string;
    senderName: string;
    recipient: string;
    content_type: string;
    body_text: string;
    body_html: string;
    received_at: string;
    cc: string;
    bcc: string;
}

const insertMails = async (emailData: EmailData, env: Env) => {
    try {
        // 1. insert emails table
        const emailResult = await env.MAIL_DB.prepare(`
            INSERT INTO emails (message_id, user_email_id, subject, sender, sender_name, recipient, content_type, body_text, body_html, received_at, cc, bcc) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
        `).bind(
            emailData.message_id,
            emailData.mail_id,
            emailData.subject,
            emailData.sender,
            emailData.senderName,
            emailData.recipient,
            emailData.content_type,
            emailData.body_text,
            emailData.body_html,
            emailData.received_at,
            emailData.cc,
            emailData.bcc
        ).run();

        // 2. get email_id
        const emailId = emailResult.meta.last_row_id;

        // 3. insert email_status table
        await env.MAIL_DB.prepare(`
            INSERT INTO email_status (email_id, user_id, is_read, is_starred, is_archived) VALUES (?, ?, 0, 0, 0)
        `).bind(emailId, emailData.user_id).run();

        return { success: true, emailId };
    } catch (error) {
        console.error('Failed to insert email:', error);
    }
};

export default insertMails;

================================================
FILE: mailbox/tsconfig.json
================================================
{
    "compilerOptions": {
        "target": "es2021",
        "lib": ["es2021"],
        "jsx": "react",
        "module": "es2022",
        "moduleResolution": "node",
        "types": ["@cloudflare/workers-types"],
        "resolveJsonModule": true,
        "allowJs": true,
        "checkJs": false,
        "noEmit": true,
        "isolatedModules": true,
        "allowSyntheticDefaultImports": true,
        "forceConsistentCasingInFileNames": true,
        "strict": true,
        "skipLibCheck": true
    }
}


================================================
FILE: mailbox/wrangler.toml
================================================
name = "temp-mail"
main = "src/index.ts"
compatibility_date = "2024-11-11"
compatibility_flags = [ "nodejs_compat" ]
kv_namespaces = [
    { binding = "POST_DB", id = "b1002ec7a3d74b9cb64504ea6d6a5586", preview_id = "238c3081e8ae49f091c08df88cf6c079" },
]

d1_databases = [
    { binding = "MAIL_DB", database_name = "fake-mail", database_id = "852bdefb-ec72-4589-8de3-f49bb2d01883", preview_database_id = "686ae900-aaea-4c69-8499-b0c8ea8c960d"},
]
Download .txt
gitextract_oaqzapim/

├── .gitignore
├── LICENSE
├── README.md
├── app/
│   ├── .gitignore
│   ├── .vscode/
│   │   ├── extensions.json
│   │   └── launch.json
│   ├── README.md
│   ├── astro.config.mjs
│   ├── components.json
│   ├── email-platform-schema.sql
│   ├── env.d.ts
│   ├── package.json
│   ├── public/
│   │   └── robots.txt
│   ├── src/
│   │   ├── components/
│   │   │   ├── Bar.astro
│   │   │   ├── Container.tsx
│   │   │   ├── Footer.astro
│   │   │   ├── Generator.tsx
│   │   │   ├── Header.astro
│   │   │   ├── Lists.tsx
│   │   │   ├── Main.astro
│   │   │   ├── ModeToggle.tsx
│   │   │   ├── mail/
│   │   │   │   ├── AccountSwitcher.tsx
│   │   │   │   ├── Accounts.tsx
│   │   │   │   ├── Mail.tsx
│   │   │   │   ├── MailDisplay.tsx
│   │   │   │   ├── MailList.tsx
│   │   │   │   ├── Nav.tsx
│   │   │   │   ├── data.tsx
│   │   │   │   └── useMail.ts
│   │   │   └── ui/
│   │   │       ├── alert-dialog.tsx
│   │   │       ├── avatar.tsx
│   │   │       ├── badge.tsx
│   │   │       ├── button.tsx
│   │   │       ├── dialog.tsx
│   │   │       ├── dropdown-menu.tsx
│   │   │       ├── input.tsx
│   │   │       ├── mail-card.tsx
│   │   │       ├── resizable.tsx
│   │   │       ├── scroll-area.tsx
│   │   │       ├── select.tsx
│   │   │       ├── separator.tsx
│   │   │       ├── skeleton.tsx
│   │   │       ├── sonner.tsx
│   │   │       ├── textarea.tsx
│   │   │       └── tooltip.tsx
│   │   ├── hooks/
│   │   │   ├── useCopyToClipboard.ts
│   │   │   └── useGeneratorMail.ts
│   │   ├── layouts/
│   │   │   └── Layout.astro
│   │   ├── lib/
│   │   │   ├── demo.ts
│   │   │   ├── store.ts
│   │   │   └── utils.ts
│   │   ├── middleware.ts
│   │   ├── pages/
│   │   │   ├── api/
│   │   │   │   ├── delete.ts
│   │   │   │   ├── deleteMails.ts
│   │   │   │   ├── generate.ts
│   │   │   │   ├── get.ts
│   │   │   │   ├── getMails.ts
│   │   │   │   ├── remarkMail.ts
│   │   │   │   └── updateStatus.ts
│   │   │   ├── dashboard.astro
│   │   │   ├── index.astro
│   │   │   ├── settings.astro
│   │   │   ├── sign-in.astro
│   │   │   └── sign-up.astro
│   │   └── styles/
│   │       └── globals.css
│   ├── tailwind.config.mjs
│   ├── tsconfig.json
│   └── wrangler.example.toml
└── mailbox/
    ├── package.json
    ├── src/
    │   ├── index.ts
    │   └── insertMails.ts
    ├── tsconfig.json
    └── wrangler.toml
Download .txt
SYMBOL INDEX (53 symbols across 22 files)

FILE: app/email-platform-schema.sql
  type user_email_addresses (line 2) | CREATE TABLE user_email_addresses (
  type idx_user_email_addresses_user_id (line 12) | CREATE INDEX idx_user_email_addresses_user_id ON user_email_addresses(us...
  type emails (line 15) | CREATE TABLE emails (
  type idx_emails_user_email_id (line 32) | CREATE INDEX idx_emails_user_email_id ON emails(user_email_id)
  type idx_emails_received_at (line 33) | CREATE INDEX idx_emails_received_at ON emails(received_at)
  type email_status (line 36) | CREATE TABLE email_status (
  type idx_email_status_user_id (line 48) | CREATE INDEX idx_email_status_user_id ON email_status(user_id)
  type email_tags (line 51) | CREATE TABLE email_tags (
  type email_tag_relations (line 61) | CREATE TABLE email_tag_relations (
  type idx_email_tag_relations_tag_id (line 68) | CREATE INDEX idx_email_tag_relations_tag_id ON email_tag_relations(tag_id)

FILE: app/env.d.ts
  type KVNamespace (line 3) | type KVNamespace = import('@cloudflare/workers-types').KVNamespace;
  type D1Database (line 4) | type D1Database = import('@cloudflare/workers-types').D1Database;
  type ENV (line 5) | type ENV = {
  type Runtime (line 18) | type Runtime = import('@astrojs/cloudflare').Runtime
  type Locals (line 20) | interface Locals extends Runtime {}
  type ImportMetaEnv (line 24) | interface ImportMetaEnv {
  type ImportMeta (line 34) | interface ImportMeta {

FILE: app/src/components/Container.tsx
  type Status (line 5) | type Status = 'error' | 'expired' | 'solved';

FILE: app/src/components/ModeToggle.tsx
  function ModeToggle (line 6) | function ModeToggle() {

FILE: app/src/components/mail/AccountSwitcher.tsx
  type AccountSwitcherProps (line 11) | interface AccountSwitcherProps {
  function AccountSwitcher (line 20) | function AccountSwitcher({

FILE: app/src/components/mail/Accounts.tsx
  type AccountsProps (line 23) | type AccountsProps = {

FILE: app/src/components/mail/Mail.tsx
  type Account (line 39) | interface Account {
  type MailProps (line 45) | interface MailProps {

FILE: app/src/components/mail/MailDisplay.tsx
  type MailDisplayProps (line 47) | interface MailDisplayProps {
  function MailDisplay (line 54) | function MailDisplay({ mail, currentAccount, toDelete, handleUnread }: M...

FILE: app/src/components/mail/MailList.tsx
  type MailListProps (line 13) | interface MailListProps {
  function MailList (line 18) | function MailList({ items, updateStatus = () => {} }: MailListProps) {

FILE: app/src/components/mail/Nav.tsx
  type NavProps (line 11) | interface NavProps {
  function Nav (line 21) | function Nav({ links, isCollapsed }: NavProps) {

FILE: app/src/components/mail/data.tsx
  type Mail (line 145) | type Mail = (typeof mails)[number]
  type Account (line 162) | type Account = (typeof accounts)[number]
  type Contact (line 247) | type Contact = (typeof contacts)[number]
  type MailsList (line 249) | type MailsList = {
  type AccountsList (line 260) | type AccountsList = { id: number; email_address: string; alias: string; ...

FILE: app/src/components/mail/useMail.ts
  type Config (line 5) | type Config = {
  function useMail (line 13) | function useMail() {

FILE: app/src/components/ui/badge.tsx
  type BadgeProps (line 26) | interface BadgeProps
  function Badge (line 30) | function Badge({ className, variant, ...props }: BadgeProps) {

FILE: app/src/components/ui/button.tsx
  type ButtonProps (line 38) | interface ButtonProps

FILE: app/src/components/ui/mail-card.tsx
  type MailData (line 17) | interface MailData {

FILE: app/src/components/ui/skeleton.tsx
  function Skeleton (line 3) | function Skeleton({

FILE: app/src/components/ui/sonner.tsx
  type ToasterProps (line 4) | type ToasterProps = React.ComponentProps<typeof Sonner>

FILE: app/src/hooks/useCopyToClipboard.ts
  type CopiedValue (line 3) | type CopiedValue = string | null;
  type CopyFn (line 5) | type CopyFn = (text: string) => Promise<boolean>;
  function useCopyToClipboard (line 7) | function useCopyToClipboard(): [CopiedValue, CopyFn] {

FILE: app/src/lib/utils.ts
  function cn (line 4) | function cn(...inputs: ClassValue[]) {

FILE: app/src/pages/api/get.ts
  function validateString (line 4) | function validateString(str: string): boolean {

FILE: mailbox/src/index.ts
  type Env (line 5) | interface Env {
  type AccoutAddress (line 10) | interface AccoutAddress {
  method email (line 16) | async email(message: ForwardableEmailMessage, env: Env) {

FILE: mailbox/src/insertMails.ts
  type Env (line 1) | interface Env {
  type EmailData (line 5) | interface EmailData {
Condensed preview — 73 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (174K chars).
[
  {
    "path": ".gitignore",
    "chars": 2057,
    "preview": "# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\nlerna-debug.log*\n.pnpm-debug.log*\n.wrangler\n\n# Diagnost"
  },
  {
    "path": "LICENSE",
    "chars": 1066,
    "preview": "MIT License\n\nCopyright (c) 2025 Chenliwen\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\n"
  },
  {
    "path": "README.md",
    "chars": 642,
    "preview": "# Fake Mail - The free temporary email service\n\n![](./images/fakemail.png)\n\n📪 Website: [https://mail.fakeact.fun](https:"
  },
  {
    "path": "app/.gitignore",
    "chars": 316,
    "preview": "# build output\ndist/\n\n# generated types\n.astro/\n\n# dependencies\nnode_modules/\n\n# logs\nnpm-debug.log*\nyarn-debug.log*\nyar"
  },
  {
    "path": "app/.vscode/extensions.json",
    "chars": 87,
    "preview": "{\n  \"recommendations\": [\"astro-build.astro-vscode\"],\n  \"unwantedRecommendations\": []\n}\n"
  },
  {
    "path": "app/.vscode/launch.json",
    "chars": 207,
    "preview": "{\n  \"version\": \"0.2.0\",\n  \"configurations\": [\n    {\n      \"command\": \"./node_modules/.bin/astro dev\",\n      \"name\": \"Dev"
  },
  {
    "path": "app/README.md",
    "chars": 2052,
    "preview": "# Astro Starter Kit: Basics\n\n```sh\nnpm create astro@latest -- --template basics\n```\n\n[![Open in StackBlitz](https://deve"
  },
  {
    "path": "app/astro.config.mjs",
    "chars": 845,
    "preview": "// @ts-check\nimport { defineConfig } from 'astro/config';\n\nimport react from '@astrojs/react';\n\nimport tailwind from '@a"
  },
  {
    "path": "app/components.json",
    "chars": 451,
    "preview": "{\n  \"$schema\": \"https://ui.shadcn.com/schema.json\",\n  \"style\": \"new-york\",\n  \"rsc\": false,\n  \"tsx\": true,\n  \"tailwind\": "
  },
  {
    "path": "app/email-platform-schema.sql",
    "chars": 2814,
    "preview": "-- 用户邮箱地址表\nCREATE TABLE user_email_addresses (\n    id INTEGER PRIMARY KEY AUTOINCREMENT,   -- 主键ID\n    user_id TEXT NOT "
  },
  {
    "path": "app/env.d.ts",
    "chars": 1032,
    "preview": "/// <reference types=\"astro/client\" />\n\ntype KVNamespace = import('@cloudflare/workers-types').KVNamespace;\ntype D1Datab"
  },
  {
    "path": "app/package.json",
    "chars": 1457,
    "preview": "{\n  \"name\": \"app\",\n  \"type\": \"module\",\n  \"version\": \"0.0.1\",\n  \"scripts\": {\n    \"dev\": \"wrangler types && astro dev\",\n  "
  },
  {
    "path": "app/public/robots.txt",
    "chars": 22,
    "preview": "User-agent: *\nAllow: /"
  },
  {
    "path": "app/src/components/Bar.astro",
    "chars": 845,
    "preview": "---\nimport { SignedIn, UserButton } from '@clerk/astro/components';\nimport { ModeToggle } from '@/components/ModeToggle'"
  },
  {
    "path": "app/src/components/Container.tsx",
    "chars": 773,
    "preview": "import { useState } from 'react';\nimport { Turnstile } from '@marsidev/react-turnstile';\nimport Lists from '@/components"
  },
  {
    "path": "app/src/components/Footer.astro",
    "chars": 1436,
    "preview": "---\nimport shoteasy from '@/assets/shoteasy.svg'\nimport fakeact from '@/assets/fakeact.svg'\n---\n\n<div class=\"container m"
  },
  {
    "path": "app/src/components/Generator.tsx",
    "chars": 4655,
    "preview": "import { useState } from 'react';\nimport { Copy, Check, RotateCw } from 'lucide-react';\nimport { Button } from '@/compon"
  },
  {
    "path": "app/src/components/Header.astro",
    "chars": 3054,
    "preview": "---\nimport { LayoutDashboard } from 'lucide-react';\nimport Generator from '@/components/Generator';\nimport { ModeToggle "
  },
  {
    "path": "app/src/components/Lists.tsx",
    "chars": 5778,
    "preview": "import { useState, useEffect, useRef, memo } from 'react';\nimport { Inbox, Mails, RefreshCw, Loader2 } from 'lucide-reac"
  },
  {
    "path": "app/src/components/Main.astro",
    "chars": 2008,
    "preview": "---\nimport Container from '@/components/Container';\nconst env = import.meta.env.PROD ? Astro.locals.runtime.env : import"
  },
  {
    "path": "app/src/components/ModeToggle.tsx",
    "chars": 1362,
    "preview": "import * as React from 'react';\nimport { Moon, Sun } from 'lucide-react';\n\nimport { Button } from '@/components/ui/butto"
  },
  {
    "path": "app/src/components/mail/AccountSwitcher.tsx",
    "chars": 2456,
    "preview": "import { useState } from 'react';\nimport { cn } from '@/lib/utils';\nimport {\n    Select,\n    SelectContent,\n    SelectIt"
  },
  {
    "path": "app/src/components/mail/Accounts.tsx",
    "chars": 6509,
    "preview": "import { useRef, useState } from 'react';\nimport { Button } from '@/components/ui/button';\nimport {\n    Dialog,\n    Dial"
  },
  {
    "path": "app/src/components/mail/Mail.tsx",
    "chars": 11916,
    "preview": "import { useRef, useState } from 'react';\nimport {\n    Archive,\n    File,\n    Inbox,\n    Search,\n    Send,\n    Copy,\n   "
  },
  {
    "path": "app/src/components/mail/MailDisplay.tsx",
    "chars": 10371,
    "preview": "import {\n    Archive,\n    Forward,\n    MoreVertical,\n    Reply,\n    ReplyAll,\n    Trash2,\n    Loader2\n} from \"lucide-rea"
  },
  {
    "path": "app/src/components/mail/MailList.tsx",
    "chars": 3641,
    "preview": "import { ScrollArea } from '@/components/ui/scroll-area';\nimport { cn } from '@/lib/utils';\nimport dayjs from 'dayjs';\ni"
  },
  {
    "path": "app/src/components/mail/Nav.tsx",
    "chars": 4288,
    "preview": "import { cn } from '@/lib/utils';\nimport type { LucideIcon } from 'lucide-react';\nimport {\n    Tooltip,\n    TooltipConte"
  },
  {
    "path": "app/src/components/mail/data.tsx",
    "chars": 14953,
    "preview": "export const mails = [\n  {\n    id: \"61c35085-72d7-42b4-8d62-738f700d4b92\",\n    name: \"AdsPower\",\n    email: \"captcha@ema"
  },
  {
    "path": "app/src/components/mail/useMail.ts",
    "chars": 251,
    "preview": "import { atom, useAtom } from 'jotai';\n\nimport { type Mail } from './data';\n\ntype Config = {\n    selected: Mail['id'] | "
  },
  {
    "path": "app/src/components/ui/alert-dialog.tsx",
    "chars": 4419,
    "preview": "import * as React from \"react\"\nimport * as AlertDialogPrimitive from \"@radix-ui/react-alert-dialog\"\n\nimport { cn } from "
  },
  {
    "path": "app/src/components/ui/avatar.tsx",
    "chars": 1405,
    "preview": "import * as React from \"react\"\nimport * as AvatarPrimitive from \"@radix-ui/react-avatar\"\n\nimport { cn } from \"@/lib/util"
  },
  {
    "path": "app/src/components/ui/badge.tsx",
    "chars": 1140,
    "preview": "import * as React from \"react\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\n\nimport { cn } from \"@/"
  },
  {
    "path": "app/src/components/ui/button.tsx",
    "chars": 1924,
    "preview": "import * as React from \"react\"\nimport { Slot } from \"@radix-ui/react-slot\"\nimport { cva, type VariantProps } from \"class"
  },
  {
    "path": "app/src/components/ui/dialog.tsx",
    "chars": 3835,
    "preview": "import * as React from \"react\"\nimport * as DialogPrimitive from \"@radix-ui/react-dialog\"\nimport { X } from \"lucide-react"
  },
  {
    "path": "app/src/components/ui/dropdown-menu.tsx",
    "chars": 7402,
    "preview": "import * as React from \"react\"\nimport * as DropdownMenuPrimitive from \"@radix-ui/react-dropdown-menu\"\nimport { Check, Ch"
  },
  {
    "path": "app/src/components/ui/input.tsx",
    "chars": 768,
    "preview": "import * as React from \"react\"\n\nimport { cn } from \"@/lib/utils\"\n\nconst Input = React.forwardRef<HTMLInputElement, React"
  },
  {
    "path": "app/src/components/ui/mail-card.tsx",
    "chars": 4211,
    "preview": "import { useState } from 'react';\nimport { cn } from '@/lib/utils';\nimport { Mail, ChevronDown, ChevronUp, Trash2 } from"
  },
  {
    "path": "app/src/components/ui/resizable.tsx",
    "chars": 1709,
    "preview": "import { GripVertical } from \"lucide-react\"\nimport * as ResizablePrimitive from \"react-resizable-panels\"\n\nimport { cn } "
  },
  {
    "path": "app/src/components/ui/scroll-area.tsx",
    "chars": 1642,
    "preview": "import * as React from \"react\"\nimport * as ScrollAreaPrimitive from \"@radix-ui/react-scroll-area\"\n\nimport { cn } from \"@"
  },
  {
    "path": "app/src/components/ui/select.tsx",
    "chars": 5618,
    "preview": "import * as React from \"react\"\nimport * as SelectPrimitive from \"@radix-ui/react-select\"\nimport { Check, ChevronDown, Ch"
  },
  {
    "path": "app/src/components/ui/separator.tsx",
    "chars": 756,
    "preview": "import * as React from \"react\"\nimport * as SeparatorPrimitive from \"@radix-ui/react-separator\"\n\nimport { cn } from \"@/li"
  },
  {
    "path": "app/src/components/ui/skeleton.tsx",
    "chars": 266,
    "preview": "import { cn } from \"@/lib/utils\"\n\nfunction Skeleton({\n  className,\n  ...props\n}: React.HTMLAttributes<HTMLDivElement>) {"
  },
  {
    "path": "app/src/components/ui/sonner.tsx",
    "chars": 880,
    "preview": "import { useTheme } from \"next-themes\"\nimport { Toaster as Sonner } from \"sonner\"\n\ntype ToasterProps = React.ComponentPr"
  },
  {
    "path": "app/src/components/ui/textarea.tsx",
    "chars": 649,
    "preview": "import * as React from \"react\"\n\nimport { cn } from \"@/lib/utils\"\n\nconst Textarea = React.forwardRef<\n  HTMLTextAreaEleme"
  },
  {
    "path": "app/src/components/ui/tooltip.tsx",
    "chars": 1203,
    "preview": "import * as React from \"react\"\nimport * as TooltipPrimitive from \"@radix-ui/react-tooltip\"\n\nimport { cn } from \"@/lib/ut"
  },
  {
    "path": "app/src/hooks/useCopyToClipboard.ts",
    "chars": 836,
    "preview": "import { useCallback, useState } from 'react';\n\ntype CopiedValue = string | null;\n\ntype CopyFn = (text: string) => Promi"
  },
  {
    "path": "app/src/hooks/useGeneratorMail.ts",
    "chars": 490,
    "preview": "import { useEffect, useState } from 'react';\nimport { getRandomMail } from '@/lib/store';\n\nexport const useGeneratorMail"
  },
  {
    "path": "app/src/layouts/Layout.astro",
    "chars": 2769,
    "preview": "---\nimport '@/styles/globals.css';\nimport { SEO } from 'astro-seo';\nconst { pathname, origin } = Astro.url;\nconst resolv"
  },
  {
    "path": "app/src/lib/demo.ts",
    "chars": 1855,
    "preview": "export default {\n    status: 'ok',\n    code: 200,\n    msg: 'Mail available',\n    stats: { count: '9832' },\n    mails: [\n"
  },
  {
    "path": "app/src/lib/store.ts",
    "chars": 578,
    "preview": "import { generate } from 'random-words';\n\nexport const getRandomMail = (isNew: boolean = false) => {\n    const domain = "
  },
  {
    "path": "app/src/lib/utils.ts",
    "chars": 168,
    "preview": "import { clsx, type ClassValue } from \"clsx\";\nimport { twMerge } from \"tailwind-merge\";\n\nexport function cn(...inputs: C"
  },
  {
    "path": "app/src/middleware.ts",
    "chars": 667,
    "preview": "import { clerkMiddleware, createRouteMatcher } from '@clerk/astro/server';\n\nconst isProtectedRoute = createRouteMatcher("
  },
  {
    "path": "app/src/pages/api/delete.ts",
    "chars": 560,
    "preview": "import type { APIRoute, APIContext } from 'astro';\n\nexport const POST: APIRoute = async ({ request, locals }: APIContext"
  },
  {
    "path": "app/src/pages/api/deleteMails.ts",
    "chars": 1256,
    "preview": "import type { APIRoute, APIContext } from 'astro';\n\nexport const POST: APIRoute = async ({ request, locals }: APIContext"
  },
  {
    "path": "app/src/pages/api/generate.ts",
    "chars": 1887,
    "preview": "import { generate } from 'random-words';\nimport type { APIRoute, APIContext } from 'astro';\n\nexport const POST: APIRoute"
  },
  {
    "path": "app/src/pages/api/get.ts",
    "chars": 2182,
    "preview": "import type { APIRoute, APIContext } from 'astro';\nimport demo from '@/lib/demo';\n\nfunction validateString(str: string):"
  },
  {
    "path": "app/src/pages/api/getMails.ts",
    "chars": 2146,
    "preview": "import type { APIRoute, APIContext } from 'astro';\n\nexport const GET: APIRoute = async ({ request, locals }: APIContext)"
  },
  {
    "path": "app/src/pages/api/remarkMail.ts",
    "chars": 1256,
    "preview": "import type { APIRoute, APIContext } from 'astro';\n\nexport const POST: APIRoute = async ({ request, locals }: APIContext"
  },
  {
    "path": "app/src/pages/api/updateStatus.ts",
    "chars": 1362,
    "preview": "import type { APIRoute, APIContext } from 'astro';\n\nexport const POST: APIRoute = async ({ request, locals }: APIContext"
  },
  {
    "path": "app/src/pages/dashboard.astro",
    "chars": 2989,
    "preview": "---\nimport Layout from '@/layouts/Layout.astro';\nimport Bar from '@/components/Bar.astro';\nimport Mail from '@/component"
  },
  {
    "path": "app/src/pages/index.astro",
    "chars": 1206,
    "preview": "---\nimport Header from '../components/Header.astro';\nimport Main from '../components/Main.astro';\nimport Footer from '.."
  },
  {
    "path": "app/src/pages/settings.astro",
    "chars": 1694,
    "preview": "---\nimport Layout from '@/layouts/Layout.astro';\nimport Bar from '@/components/Bar.astro';\nimport { ArrowLeft } from 'lu"
  },
  {
    "path": "app/src/pages/sign-in.astro",
    "chars": 978,
    "preview": "---\nimport { SignIn } from '@clerk/astro/components';\nimport Layout from '@/layouts/Layout.astro';\n---\n\n<Layout>\n    <di"
  },
  {
    "path": "app/src/pages/sign-up.astro",
    "chars": 981,
    "preview": "---\nimport { SignUp } from '@clerk/astro/components';\nimport Layout from '@/layouts/Layout.astro';\n---\n\n<Layout>\n    <di"
  },
  {
    "path": "app/src/styles/globals.css",
    "chars": 1831,
    "preview": "@tailwind base;\n@tailwind components;\n@tailwind utilities;\n@layer base {\n  :root {\n    --background: 0 0% 100%;\n    --fo"
  },
  {
    "path": "app/tailwind.config.mjs",
    "chars": 1653,
    "preview": "/** @type {import('tailwindcss').Config} */\nexport default {\n    darkMode: ['class'],\n    content: ['./src/**/*.{astro,h"
  },
  {
    "path": "app/tsconfig.json",
    "chars": 294,
    "preview": "{\n  \"extends\": \"astro/tsconfigs/strict\",\n  \"include\": [\n    \".astro/types.d.ts\",\n    \"**/*\"\n  ],\n  \"exclude\": [\n    \"dis"
  },
  {
    "path": "app/wrangler.example.toml",
    "chars": 893,
    "preview": "name = \"fakemail\"\nmain = \"./dist/_worker.js\"\ncompatibility_date = \"2025-01-14\"\ncompatibility_flags = [ \"nodejs_compat\" ]"
  },
  {
    "path": "mailbox/package.json",
    "chars": 468,
    "preview": "{\n  \"name\": \"mailbox\",\n  \"version\": \"1.0.0\",\n  \"main\": \"index.js\",\n  \"scripts\": {\n    \"start\": \"wrangler dev\",\n    \"depl"
  },
  {
    "path": "mailbox/src/index.ts",
    "chars": 3479,
    "preview": "// @ts-ignore\nimport PostalMime from 'postal-mime';\nimport insertMails from './insertMails';\n\nexport interface Env {\n   "
  },
  {
    "path": "mailbox/src/insertMails.ts",
    "chars": 1619,
    "preview": "interface Env {\n    MAIL_DB: D1Database;\n}\n\ninterface EmailData {\n    mail_id: number;\n    user_id: string;\n    message_"
  },
  {
    "path": "mailbox/tsconfig.json",
    "chars": 518,
    "preview": "{\n    \"compilerOptions\": {\n        \"target\": \"es2021\",\n        \"lib\": [\"es2021\"],\n        \"jsx\": \"react\",\n        \"modul"
  },
  {
    "path": "mailbox/wrangler.toml",
    "chars": 449,
    "preview": "name = \"temp-mail\"\nmain = \"src/index.ts\"\ncompatibility_date = \"2024-11-11\"\ncompatibility_flags = [ \"nodejs_compat\" ]\nkv_"
  }
]

About this extraction

This page contains the full source code of the CH563/fakemail GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 73 files (158.4 KB), approximately 40.4k tokens, and a symbol index with 53 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!