# Linkpop Email

> Private email server for agency gigworkers and marketers. REST API for managing disposable mailboxes at @linkpop.site, password vault, 2FA/TOTP authenticator, shareable standalone mailbox links, and email attachments. Use when building integrations with LinkPop Mail, managing mailboxes programmatically, automating email workflows, or working with the password/2FA vault. Triggers on linkpop, private email, disposable mailbox, mailbox API, email management, password vault, TOTP authenticator, shareable mailbox.

- Skill: `officexapp/linkpop-email` (Agent Skill, multi-file: 2 files)
- Install (CLI): `npx skillmds@latest add officexapp/linkpop-email`
- Raw SKILL.md: https://api.skillmd.com/api/skills/officexapp/linkpop-email/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Integrations & APIs
- Author: officexapp (https://skillmd.com/u/officexapp)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/officexapp/linkpop-email

---


# LinkPop Private Email

Private email server for agency gigworkers, creators, and marketers who manage multiple social media accounts. Create unlimited disposable email addresses at `@linkpop.site` for platform signups, account management, and online services — without risking your primary inbox.

## Why LinkPop Mail?

Managing multiple social media accounts means juggling dozens of email addresses. One platform ban can cascade across every account tied to your personal Gmail. LinkPop Mail solves this:

- **Unlimited disposable mailboxes** — Spin up `anything@linkpop.site` in seconds for Instagram, TikTok, Twitter, or any platform signup
- **Built-in password manager** — Store login credentials scoped to each mailbox
- **Built-in 2FA authenticator** — Generate TOTP codes without a separate app, with QR code/webcam scanning
- **Shareable standalone mailbox links** — Hand off password-protected mailbox access to clients or team members via a standalone URL
- **Email attachments** — Private S3 storage with presigned URL access
- **Auto-forward & webhooks** — Real-time notifications for incoming emails (whitelisted users)
- **API access** — Full REST API for programmatic mailbox and email management

### Pricing

| Tier | Mailboxes | Monthly Emails | Passwords | 2FA | Price |
|:-----|:----------|:---------------|:----------|:----|:------|
| Free | 10 | 100 | 100 | 100 | $0/mo |
| Pro | 100 (unlimited with custom domain) | 1,000 (+$0.01/extra) | Unlimited | Unlimited | $9/mo |

---

## URLs

| Resource | Staging | Production |
|:---------|:--------|:-----------|
| **Frontend** | `https://mail-staging.cloud.linkpop.site` | `https://mail.cloud.linkpop.site` |
| **API Base** | `https://mail-staging.cloud.linkpop.site/api` | `https://mail.cloud.linkpop.site/api` |
| **Resend Webhook** | `https://mail-staging.cloud.linkpop.site/api/webhooks/resend` | `https://mail.cloud.linkpop.site/api/webhooks/resend` |

---

## Authentication

### OTP Login (Web Users)

1. `POST /auth/send-otp` with `{ "email": "user@example.com" }` — sends a 6-digit OTP
2. `POST /auth/verify-otp` with `{ "email": "user@example.com", "otp": "123456" }` — returns JWT token + user profile

### API Key (Programmatic Access)

After login, users have an API key in their settings. Pass it via header:

```
Authorization: Bearer <base64(user_id:secret)>
```

Rotate keys: `POST /auth/rotate-api-key`

### Share Token (Standalone Mailbox)

Password-protected access via share token:

1. `GET /share/:token/info` — returns mailbox address
2. `POST /share/:token/auth` with `{ "password": "..." }` — returns session token
3. Use session token for subsequent `/share/:token/*` requests

---

## API Endpoints

### Auth (`/auth`)

| Method | Path | Description |
|:-------|:-----|:------------|
| `POST` | `/auth/send-otp` | Send OTP to email. Body: `{ "email": "..." }` |
| `POST` | `/auth/verify-otp` | Verify OTP, login/register. Body: `{ "email": "...", "otp": "..." }` |
| `POST` | `/auth/login` | Email + password login. Body: `{ "email": "...", "password": "..." }` |
| `POST` | `/auth/register` | Register. Body: `{ "email": "...", "password": "..." }` |
| `GET` | `/auth/me` | Get current user profile |
| `POST` | `/auth/change-password` | Change password. Body: `{ "current_password": "...", "new_password": "..." }` |
| `POST` | `/auth/rotate-api-key` | Rotate API key, returns new key |

### Mailboxes (`/mailboxes`)

| Method | Path | Description |
|:-------|:-----|:------------|
| `GET` | `/mailboxes` | List all user mailboxes |
| `POST` | `/mailboxes` | Create mailbox. Body: `{ "address": "myname", "label": "optional label" }` |
| `GET` | `/mailboxes/:addr` | Get mailbox details |
| `PATCH` | `/mailboxes/:addr` | Update mailbox. Body: `{ "label": "...", "webhook_url": "...", "auto_forward": "...", "share_password": "..." }` |
| `DELETE` | `/mailboxes/:addr` | Delete mailbox |
| `POST` | `/mailboxes/:addr/share` | Create share link with password |
| `DELETE` | `/mailboxes/:addr/share` | Remove share link |

**Note:** Address format is `username@linkpop.site` (production) or `username.staging@linkpop.site` (staging). A random 12-character share password is auto-generated on creation.

### Emails (`/emails`)

| Method | Path | Description |
|:-------|:-----|:------------|
| `GET` | `/emails/mailbox/:addr` | List emails for a mailbox |
| `GET` | `/emails/all` | List all emails across all mailboxes |
| `GET` | `/emails/:addr/:id` | Get single email (marks as read) |
| `POST` | `/emails/reply` | Reply to email. Body: `{ "to": "...", "subject": "...", "html": "...", "in_reply_to": "..." }` |
| `POST` | `/emails/send` | Send new email (whitelisted only). Body: `{ "from_address": "...", "to": "...", "subject": "...", "html": "..." }` |
| `POST` | `/emails/forward` | Forward email (whitelisted only). Body: `{ "email_id": "...", "address": "...", "forward_to": "..." }` |
| `GET` | `/emails/attachments/:addr/:emailId/:attId` | Get presigned URL for attachment |

### Passwords (`/passwords`)

| Method | Path | Description |
|:-------|:-----|:------------|
| `GET` | `/passwords` | List passwords. Query: `?search=&mailbox=` |
| `POST` | `/passwords` | Create password entry. Body: `{ "mailbox_address": "...", "website": "...", "username": "...", "password": "...", "notes": "..." }` |
| `PATCH` | `/passwords/:id` | Update password entry |
| `DELETE` | `/passwords/:id` | Delete password. Query: `?mailbox_address=` |

### TOTP / 2FA (`/totp`)

| Method | Path | Description |
|:-------|:-----|:------------|
| `GET` | `/totp` | List TOTP entries. Query: `?mailbox=&search=` |
| `POST` | `/totp` | Create TOTP entry. Body: `{ "mailbox_address": "...", "label": "...", "secret": "...", "issuer": "..." }` or `{ "mailbox_address": "...", "uri": "otpauth://..." }` |
| `DELETE` | `/totp/:id` | Delete TOTP entry. Query: `?mailbox_address=` |

### Shared Mailbox (`/share/:token`)

Password-protected standalone access. All endpoints require prior auth via `POST /share/:token/auth`.

| Method | Path | Description |
|:-------|:-----|:------------|
| `GET` | `/share/:token/info` | Get mailbox address (for login screen) |
| `POST` | `/share/:token/auth` | Authenticate. Body: `{ "password": "..." }` |
| `GET` | `/share/:token/emails` | List emails |
| `GET` | `/share/:token/emails/:id` | Get single email |
| `GET` | `/share/:token/attachments/:emailId/:attId` | Get attachment URL |
| `GET` | `/share/:token/passwords` | List passwords |
| `POST` | `/share/:token/passwords` | Create password |
| `PATCH` | `/share/:token/passwords/:id` | Update password |
| `DELETE` | `/share/:token/passwords/:id` | Delete password |
| `GET` | `/share/:token/totp` | List TOTP entries |
| `POST` | `/share/:token/totp` | Create TOTP |
| `DELETE` | `/share/:token/totp/:id` | Delete TOTP |

### Admin (`/admin`)

Requires `X-Admin-Key` header matching `SUPERADMIN_KEY` env var.

| Method | Path | Description |
|:-------|:-----|:------------|
| `POST` | `/admin/login-as` | Login as any user. Body: `{ "user_id": "..." }` |
| `POST` | `/admin/set-tier` | Set user tier. Body: `{ "user_id": "...", "tier": "FREE_TIER\|PAID_TIER\|UNLIMITED_TIER" }` |
| `GET` | `/admin/stats/websites` | Password count per website. Query: `?website=` |
| `GET` | `/admin/whitelist` | List whitelist entries |
| `POST` | `/admin/whitelist` | Add whitelist. Body: `{ "identifier": "user:UUID" }` or `{ "identifier": "mailbox:addr@linkpop.site" }` |
| `DELETE` | `/admin/whitelist/:email` | Remove whitelist entry |

### Webhooks (`/webhooks`)

| Method | Path | Description |
|:-------|:-----|:------------|
| `POST` | `/webhooks/resend` | Resend inbound email webhook (called by Resend) |

---

## Data Models

### User

```typescript
interface User {
  user_id: string;
  email: string;
  tier: "FREE_TIER" | "PAID_TIER" | "UNLIMITED_TIER";
  api_key_id: string;
  api_key_secret: string; // hashed
  monthly_email_count: number;
  monthly_email_reset: string; // ISO date
  created_at: string;
  updated_at: string;
}
```

### Mailbox

```typescript
interface Mailbox {
  user_id: string;
  address: string; // e.g. "myname@linkpop.site"
  label?: string;
  share_token?: string;
  share_password?: string; // hashed, auto-generated 12 chars
  webhook_url?: string; // requires whitelist
  auto_forward?: string; // requires whitelist
  created_at: string;
  updated_at: string;
}
```

### Email

```typescript
interface Email {
  address: string; // mailbox address
  email_id: string;
  from: string;
  to: string;
  subject: string;
  text_body?: string;
  html_body?: string;
  attachments?: Attachment[];
  read: boolean;
  timestamp: string;
}

interface Attachment {
  id: string;
  filename: string;
  content_type: string;
  size: number;
  s3_key: string;
}
```

### Password Entry

```typescript
interface PasswordEntry {
  password_id: string;
  user_id: string;
  mailbox_address: string;
  website: string;
  username: string;
  password: string; // encrypted
  notes?: string;
  created_at: string;
  updated_at: string;
}
```

### TOTP Entry

```typescript
interface TOTPEntry {
  totp_id: string;
  user_id: string;
  mailbox_address: string;
  label: string;
  secret: string;
  issuer?: string;
  algorithm?: string;
  digits?: number;
  period?: number;
  created_at: string;
}
```

---

## Rate Limits by Tier

| Tier | Mailboxes | Emails/Month | Passwords | TOTP |
|:-----|:----------|:-------------|:----------|:-----|
| `FREE_TIER` | 10 | 100 | 100 | 100 |
| `PAID_TIER` | 100 | 1,000 | Unlimited | Unlimited |
| `UNLIMITED_TIER` | Unlimited | Unlimited | Unlimited | Unlimited |

---

## Whitelist System

Certain features require admin whitelisting:

- **Send emails** — `POST /emails/send`
- **Forward emails** — `POST /emails/forward`
- **Auto-forward** — Setting `auto_forward` on a mailbox
- **Webhooks** — Setting `webhook_url` on a mailbox

Whitelist can be granted at two levels:

- **User level:** `user:<user_id>` — applies to all mailboxes
- **Mailbox level:** `mailbox:<address>` — applies to specific mailbox only

---

## Architecture

- **Frontend:** React 18 + TypeScript + TailwindCSS + Zustand
- **Backend:** AWS Lambda (Node.js 20) + Express.js
- **Database:** DynamoDB (single table design, 3 GSIs)
- **Storage:** S3 (private attachments with presigned URLs, 365-day TTL)
- **Email:** Resend API (inbound webhook + outbound sending)
- **Infrastructure:** AWS CDK, CloudFront + S3 (frontend), API Gateway (backend)
- **Domain:** `linkpop.site` via Route53 + ACM SSL

### Stage Isolation

- **Staging:** Emails use `*.staging@linkpop.site`, frontend at `mail-staging.cloud.linkpop.site`
- **Production:** Emails use `*@linkpop.site`, frontend at `mail.cloud.linkpop.site`
- Resend webhook filters by stage suffix to prevent cross-stage contamination

---

## Usage Examples

### Create a Mailbox

```typescript
const API = "https://mail-staging.cloud.linkpop.site/api";
const TOKEN = "your-jwt-token";

const res = await fetch(`${API}/mailboxes`, {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    Authorization: `Bearer ${TOKEN}`,
  },
  body: JSON.stringify({
    address: "mycampaign",
    label: "Q1 Marketing Campaign",
  }),
});

const mailbox = await res.json();
// mailbox.address = "mycampaign.staging@linkpop.site"
// mailbox.share_token = "abc123..." (for standalone link)
// mailbox.share_password = "xK9m2..." (auto-generated)
```

### List Emails

```typescript
const emails = await fetch(`${API}/emails/mailbox/mycampaign.staging@linkpop.site`, {
  headers: { Authorization: `Bearer ${TOKEN}` },
}).then((r) => r.json());
```

### Store a Password

```typescript
await fetch(`${API}/passwords`, {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    Authorization: `Bearer ${TOKEN}`,
  },
  body: JSON.stringify({
    mailbox_address: "mycampaign.staging@linkpop.site",
    website: "instagram.com",
    username: "mycampaign_handle",
    password: "s3cur3p4ss",
    notes: "Client account for Q1 campaign",
  }),
});
```

### Add a TOTP Entry

```typescript
await fetch(`${API}/totp`, {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    Authorization: `Bearer ${TOKEN}`,
  },
  body: JSON.stringify({
    mailbox_address: "mycampaign.staging@linkpop.site",
    uri: "otpauth://totp/Instagram:mycampaign?secret=JBSWY3DPEHPK3PXP&issuer=Instagram",
  }),
});
```

### Access Shared Mailbox

```typescript
const SHARE_URL = "https://mail-staging.cloud.linkpop.site/api/share/abc123token";

// 1. Get mailbox info
const info = await fetch(`${SHARE_URL}/info`).then((r) => r.json());
// info.address = "mycampaign.staging@linkpop.site"

// 2. Authenticate
const auth = await fetch(`${SHARE_URL}/auth`, {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ password: "xK9m2..." }),
}).then((r) => r.json());

// 3. List emails
const emails = await fetch(`${SHARE_URL}/emails`, {
  headers: { Authorization: `Bearer ${auth.token}` },
}).then((r) => r.json());
```

---

## Error Handling

| Status | Meaning |
|:-------|:--------|
| 400 | Bad request / validation error |
| 401 | Unauthorized / invalid token |
| 403 | Forbidden / whitelist required |
| 404 | Resource not found |
| 429 | Rate limit exceeded (tier limit) |
| 500 | Internal server error |

Error response format:

```json
{
  "error": "Error message describing what went wrong"
}
```

---

## Deployment

```bash
npm run install:all    # Install all dependencies
npm run build:api      # Build Lambda API
npm run build:web      # Build React frontend
npm run deploy:staging # Deploy to staging via CDK
npm run deploy:production # Deploy to production via CDK
```

Deployment outputs are written to `deployment.staging.json` and `deployment.production.json`.

