mirror of
https://github.com/beilunyang/moemail.git
synced 2026-05-06 20:02:52 +08:00
Merge branch 'feat/cli'
This commit is contained in:
39
.github/workflows/publish-cli.yml
vendored
Normal file
39
.github/workflows/publish-cli.yml
vendored
Normal file
@@ -0,0 +1,39 @@
|
||||
name: Publish CLI
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'cli-v*'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v2
|
||||
with:
|
||||
version: 9
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Setup Bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Build
|
||||
run: cd packages/cli && bun build ./src/index.ts --outdir ./dist --target=node
|
||||
|
||||
- name: Publish to npm
|
||||
run: cd packages/cli && npm publish --access public
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { NextResponse } from "next/server"
|
||||
import { auth } from "@/lib/auth"
|
||||
import { getUserId } from "@/lib/apiKey"
|
||||
import { createDb } from "@/lib/db"
|
||||
import { emails, messages } from "@/lib/schema"
|
||||
import { eq } from "drizzle-orm"
|
||||
@@ -49,8 +49,8 @@ export async function POST(
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
try {
|
||||
const session = await auth()
|
||||
if (!session?.user?.id) {
|
||||
const userId = await getUserId()
|
||||
if (!userId) {
|
||||
return NextResponse.json(
|
||||
{ error: "未授权" },
|
||||
{ status: 401 }
|
||||
@@ -60,7 +60,7 @@ export async function POST(
|
||||
const { id } = await params
|
||||
const db = createDb()
|
||||
|
||||
const permissionResult = await checkSendPermission(session.user.id)
|
||||
const permissionResult = await checkSendPermission(userId)
|
||||
if (!permissionResult.canSend) {
|
||||
return NextResponse.json(
|
||||
{ error: permissionResult.error },
|
||||
@@ -90,7 +90,7 @@ export async function POST(
|
||||
)
|
||||
}
|
||||
|
||||
if (email.userId !== session.user.id) {
|
||||
if (email.userId !== userId) {
|
||||
return NextResponse.json(
|
||||
{ error: "无权访问此邮箱" },
|
||||
{ status: 403 }
|
||||
|
||||
2
packages/cli/.gitignore
vendored
Normal file
2
packages/cli/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
node_modules/
|
||||
dist/
|
||||
83
packages/cli/README.md
Normal file
83
packages/cli/README.md
Normal file
@@ -0,0 +1,83 @@
|
||||
# MoeMail CLI
|
||||
|
||||
Agent-first CLI for MoeMail temporary email service
|
||||
|
||||
## Install
|
||||
|
||||
```bash
|
||||
npm i -g moemail-cli
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
|
||||
### 1. Configure default domain
|
||||
```bash
|
||||
moemail config --domain moemail.app
|
||||
```
|
||||
|
||||
### 2. Create a temporary email
|
||||
```bash
|
||||
moemail create --expiry 1h
|
||||
```
|
||||
|
||||
### 3. Wait for messages
|
||||
```bash
|
||||
moemail wait --email-id <email_id> --timeout 120
|
||||
```
|
||||
|
||||
## Command Reference
|
||||
|
||||
| Command | Description | Key Flags |
|
||||
|---------|-------------|-----------|
|
||||
| `config` | Set default domain and options | `--domain <domain>`, `--expiry <duration>` |
|
||||
| `create` | Create a temporary email address | `--domain <domain>`, `--expiry <duration>`, `--json` |
|
||||
| `list` | List all temporary emails | `--json` |
|
||||
| `wait` | Wait for incoming messages | `--email-id <id>`, `--timeout <seconds>`, `--json` |
|
||||
| `read` | Read email message content | `--email-id <id>`, `--message-id <id>`, `--json` |
|
||||
| `send` | Send email from temporary address | `--email-id <id>`, `--to <address>`, `--subject <text>`, `--body <text>`, `--json` |
|
||||
| `delete` | Delete temporary email | `--email-id <id>` |
|
||||
|
||||
## Agent Workflow Example
|
||||
|
||||
The CLI is designed to support agent-first automation. Here's a typical workflow:
|
||||
|
||||
```bash
|
||||
# Create temporary email and extract details
|
||||
EMAIL=$(moemail create --domain moemail.app --expiry 1h --json)
|
||||
EMAIL_ID=$(echo $EMAIL | jq -r '.id')
|
||||
ADDRESS=$(echo $EMAIL | jq -r '.address')
|
||||
|
||||
# Use ADDRESS for signup or service registration...
|
||||
|
||||
# Wait for verification email
|
||||
MSG=$(moemail wait --email-id $EMAIL_ID --timeout 120 --json)
|
||||
MSG_ID=$(echo $MSG | jq -r '.messageId')
|
||||
|
||||
# Read message content
|
||||
CONTENT=$(moemail read --email-id $EMAIL_ID --message-id $MSG_ID --json)
|
||||
|
||||
# Extract verification code from CONTENT...
|
||||
|
||||
# Cleanup
|
||||
moemail delete --email-id $EMAIL_ID
|
||||
```
|
||||
|
||||
## JSON Output
|
||||
|
||||
All commands support `--json` flag for structured output, making them ideal for agent automation:
|
||||
|
||||
- **Success**: Command output in JSON format to stdout
|
||||
- **Errors**: Error messages written to stderr
|
||||
- **Exit Codes**:
|
||||
- `0`: Command succeeded
|
||||
- `1`: Runtime error (invalid input, service error)
|
||||
- `2`: Configuration error (missing domain, invalid credentials)
|
||||
|
||||
## Project Links
|
||||
|
||||
- **Main Project**: https://github.com/beilunyang/moemail
|
||||
- **Issues & Feedback**: https://github.com/beilunyang/moemail/issues
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
19
packages/cli/package.json
Normal file
19
packages/cli/package.json
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"name": "@moemail/cli",
|
||||
"version": "0.1.0",
|
||||
"description": "Agent-first CLI for MoeMail temporary email service",
|
||||
"type": "module",
|
||||
"bin": {
|
||||
"moemail": "dist/index.js"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "bun build ./src/index.ts --outdir ./dist --target=node",
|
||||
"dev": "bun run ./src/index.ts"
|
||||
},
|
||||
"files": ["dist"],
|
||||
"keywords": ["email", "temporary", "cli", "agent", "ai"],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"commander": "^12.0.0"
|
||||
}
|
||||
}
|
||||
23
packages/cli/pnpm-lock.yaml
generated
Normal file
23
packages/cli/pnpm-lock.yaml
generated
Normal file
@@ -0,0 +1,23 @@
|
||||
lockfileVersion: '9.0'
|
||||
|
||||
settings:
|
||||
autoInstallPeers: true
|
||||
excludeLinksFromLockfile: false
|
||||
|
||||
importers:
|
||||
|
||||
.:
|
||||
dependencies:
|
||||
commander:
|
||||
specifier: ^12.0.0
|
||||
version: 12.1.0
|
||||
|
||||
packages:
|
||||
|
||||
commander@12.1.0:
|
||||
resolution: {integrity: sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
snapshots:
|
||||
|
||||
commander@12.1.0: {}
|
||||
84
packages/cli/src/api.ts
Normal file
84
packages/cli/src/api.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
import { loadConfig } from "./config.js";
|
||||
import { log } from "./output.js";
|
||||
|
||||
export class ApiError extends Error {
|
||||
constructor(
|
||||
public status: number,
|
||||
message: string,
|
||||
) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
|
||||
async function request(
|
||||
method: string,
|
||||
path: string,
|
||||
body?: Record<string, unknown>,
|
||||
): Promise<unknown> {
|
||||
const config = loadConfig();
|
||||
|
||||
if (!config.apiUrl) {
|
||||
log("Error: API URL not configured. Run: moemail config set api-url <url>");
|
||||
process.exit(2);
|
||||
}
|
||||
if (!config.apiKey) {
|
||||
log("Error: API Key not configured. Run: moemail config set api-key <key>");
|
||||
process.exit(2);
|
||||
}
|
||||
|
||||
const url = `${config.apiUrl.replace(/\/$/, "")}${path}`;
|
||||
const headers: Record<string, string> = {
|
||||
"X-API-Key": config.apiKey,
|
||||
};
|
||||
if (body) {
|
||||
headers["Content-Type"] = "application/json";
|
||||
}
|
||||
|
||||
const res = await fetch(url, {
|
||||
method,
|
||||
headers,
|
||||
body: body ? JSON.stringify(body) : undefined,
|
||||
});
|
||||
|
||||
if (res.status === 401 || res.status === 403) {
|
||||
log("Error: Authentication failed. Check your API Key.");
|
||||
process.exit(2);
|
||||
}
|
||||
|
||||
if (res.status === 204) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const data = await res.json();
|
||||
|
||||
if (!res.ok) {
|
||||
throw new ApiError(res.status, (data as any).error || `HTTP ${res.status}`);
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
export const api = {
|
||||
getConfig: () => request("GET", "/api/config"),
|
||||
|
||||
createEmail: (body: { name?: string; expiryTime: number; domain: string }) =>
|
||||
request("POST", "/api/emails/generate", body as any),
|
||||
|
||||
listEmails: (cursor?: string) =>
|
||||
request("GET", `/api/emails${cursor ? `?cursor=${cursor}` : ""}`),
|
||||
|
||||
listMessages: (emailId: string, cursor?: string) =>
|
||||
request("GET", `/api/emails/${emailId}${cursor ? `?cursor=${cursor}` : ""}`),
|
||||
|
||||
getMessage: (emailId: string, messageId: string) =>
|
||||
request("GET", `/api/emails/${emailId}/${messageId}`),
|
||||
|
||||
deleteEmail: (emailId: string) =>
|
||||
request("DELETE", `/api/emails/${emailId}`),
|
||||
|
||||
deleteMessage: (emailId: string, messageId: string) =>
|
||||
request("DELETE", `/api/emails/${emailId}/${messageId}`),
|
||||
|
||||
sendEmail: (emailId: string, body: { to: string; subject: string; content: string }) =>
|
||||
request("POST", `/api/emails/${emailId}/send`, body),
|
||||
};
|
||||
28
packages/cli/src/commands/config.ts
Normal file
28
packages/cli/src/commands/config.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { Command } from "commander";
|
||||
import { loadConfig, saveConfig } from "../config.js";
|
||||
|
||||
export function registerConfigCommand(program: Command) {
|
||||
const cmd = program.command("config").description("Configure API endpoint and API Key");
|
||||
|
||||
cmd
|
||||
.command("set <key> <value>")
|
||||
.description("Set a config value (api-url or api-key)")
|
||||
.action((key: string, value: string) => {
|
||||
try {
|
||||
saveConfig(key, value);
|
||||
console.error(`Set ${key} successfully.`);
|
||||
} catch (e: any) {
|
||||
console.error(e.message);
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
|
||||
cmd
|
||||
.command("list")
|
||||
.description("Show current configuration")
|
||||
.action(() => {
|
||||
const config = loadConfig();
|
||||
console.log(`api-url: ${config.apiUrl || "(not set)"}`);
|
||||
console.log(`api-key: ${config.apiKey ? config.apiKey.slice(0, 6) + "..." : "(not set)"}`);
|
||||
});
|
||||
}
|
||||
62
packages/cli/src/commands/create.ts
Normal file
62
packages/cli/src/commands/create.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
import { Command } from "commander";
|
||||
import { api } from "../api.js";
|
||||
import { log, printJson, printText, msToIso } from "../output.js";
|
||||
|
||||
const EXPIRY_MAP: Record<string, number> = {
|
||||
"1h": 3600000,
|
||||
"24h": 86400000,
|
||||
"3d": 259200000,
|
||||
permanent: 0,
|
||||
};
|
||||
|
||||
export function registerCreateCommand(program: Command) {
|
||||
program
|
||||
.command("create")
|
||||
.description("Create a temporary email address")
|
||||
.option("--name <name>", "email prefix")
|
||||
.option("--domain <domain>", "email domain")
|
||||
.option("--expiry <expiry>", "1h | 24h | 3d | permanent", "1h")
|
||||
.action(async (opts) => {
|
||||
const json = program.opts().json;
|
||||
const expiryTime = EXPIRY_MAP[opts.expiry];
|
||||
if (expiryTime === undefined) {
|
||||
log(`Error: Invalid expiry "${opts.expiry}". Valid: 1h, 24h, 3d, permanent`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
try {
|
||||
let domain = opts.domain;
|
||||
if (!domain) {
|
||||
const config = (await api.getConfig()) as any;
|
||||
const domains = config.emailDomains?.split(",").map((d: string) => d.trim());
|
||||
if (!domains?.length) {
|
||||
log("Error: No email domains configured on server.");
|
||||
process.exit(1);
|
||||
}
|
||||
domain = domains[0];
|
||||
}
|
||||
|
||||
const result = (await api.createEmail({
|
||||
name: opts.name,
|
||||
expiryTime,
|
||||
domain,
|
||||
})) as any;
|
||||
|
||||
const expiresAt =
|
||||
expiryTime === 0
|
||||
? null
|
||||
: msToIso(Date.now() + expiryTime);
|
||||
|
||||
if (json) {
|
||||
printJson({ id: result.id, address: result.email, expiresAt });
|
||||
} else {
|
||||
const expiryLabel = opts.expiry === "permanent" ? "permanent" : `expires in ${opts.expiry}`;
|
||||
printText(`Created: ${result.email} (${expiryLabel})`);
|
||||
printText(`ID: ${result.id}`);
|
||||
}
|
||||
} catch (e: any) {
|
||||
log(`Error: ${e.message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
}
|
||||
34
packages/cli/src/commands/delete.ts
Normal file
34
packages/cli/src/commands/delete.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { Command } from "commander";
|
||||
import { api } from "../api.js";
|
||||
import { log, printJson, printText } from "../output.js";
|
||||
|
||||
export function registerDeleteCommand(program: Command) {
|
||||
program
|
||||
.command("delete")
|
||||
.description("Delete a mailbox or message")
|
||||
.requiredOption("--email-id <id>", "email ID")
|
||||
.option("--message-id <id>", "message ID (omit to delete entire mailbox)")
|
||||
.action(async (opts) => {
|
||||
const json = program.opts().json;
|
||||
try {
|
||||
if (opts.messageId) {
|
||||
await api.deleteMessage(opts.emailId, opts.messageId);
|
||||
if (json) {
|
||||
printJson({ success: true });
|
||||
} else {
|
||||
printText(`Deleted message ${opts.messageId}`);
|
||||
}
|
||||
} else {
|
||||
await api.deleteEmail(opts.emailId);
|
||||
if (json) {
|
||||
printJson({ success: true });
|
||||
} else {
|
||||
printText(`Deleted mailbox ${opts.emailId}`);
|
||||
}
|
||||
}
|
||||
} catch (e: any) {
|
||||
log(`Error: ${e.message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
}
|
||||
67
packages/cli/src/commands/list.ts
Normal file
67
packages/cli/src/commands/list.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import { Command } from "commander";
|
||||
import { api } from "../api.js";
|
||||
import { log, printJson, printText, msToIso } from "../output.js";
|
||||
|
||||
export function registerListCommand(program: Command) {
|
||||
program
|
||||
.command("list")
|
||||
.description("List mailboxes or messages")
|
||||
.option("--email-id <id>", "list messages in this mailbox")
|
||||
.option("--cursor <cursor>", "pagination cursor")
|
||||
.action(async (opts) => {
|
||||
const json = program.opts().json;
|
||||
try {
|
||||
if (opts.emailId) {
|
||||
const data = (await api.listMessages(opts.emailId, opts.cursor)) as any;
|
||||
if (json) {
|
||||
printJson({
|
||||
messages: data.messages.map((m: any) => ({
|
||||
id: m.id,
|
||||
from: m.from_address,
|
||||
subject: m.subject,
|
||||
receivedAt: m.received_at ? msToIso(m.received_at) : null,
|
||||
})),
|
||||
nextCursor: data.nextCursor,
|
||||
total: data.total,
|
||||
});
|
||||
} else {
|
||||
if (!data.messages.length) {
|
||||
printText("No messages.");
|
||||
} else {
|
||||
for (const m of data.messages) {
|
||||
printText(`[${m.id}] From: ${m.from_address} — ${m.subject}`);
|
||||
}
|
||||
printText(`Total: ${data.total}`);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Note: GET /api/emails returns raw Drizzle ORM objects.
|
||||
// expiresAt is a Date serialized to ISO string (not epoch ms).
|
||||
const data = (await api.listEmails(opts.cursor)) as any;
|
||||
if (json) {
|
||||
printJson({
|
||||
emails: data.emails.map((e: any) => ({
|
||||
id: e.id,
|
||||
address: e.address,
|
||||
expiresAt: e.expiresAt || null,
|
||||
})),
|
||||
nextCursor: data.nextCursor,
|
||||
total: data.total,
|
||||
});
|
||||
} else {
|
||||
if (!data.emails.length) {
|
||||
printText("No mailboxes.");
|
||||
} else {
|
||||
for (const e of data.emails) {
|
||||
printText(`[${e.id}] ${e.address}`);
|
||||
}
|
||||
printText(`Total: ${data.total}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: any) {
|
||||
log(`Error: ${e.message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
}
|
||||
45
packages/cli/src/commands/read.ts
Normal file
45
packages/cli/src/commands/read.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { Command } from "commander";
|
||||
import { api } from "../api.js";
|
||||
import { log, printJson, printText, msToIso } from "../output.js";
|
||||
|
||||
export function registerReadCommand(program: Command) {
|
||||
program
|
||||
.command("read")
|
||||
.description("Read an email message")
|
||||
.requiredOption("--email-id <id>", "email ID")
|
||||
.requiredOption("--message-id <id>", "message ID")
|
||||
.option("--format <format>", "text | html", "text")
|
||||
.action(async (opts) => {
|
||||
const json = program.opts().json;
|
||||
try {
|
||||
const data = (await api.getMessage(opts.emailId, opts.messageId)) as any;
|
||||
const msg = data.message;
|
||||
|
||||
if (json) {
|
||||
printJson({
|
||||
id: msg.id,
|
||||
from: msg.from_address,
|
||||
to: msg.to_address,
|
||||
subject: msg.subject,
|
||||
content: msg.content,
|
||||
html: msg.html,
|
||||
receivedAt: msg.received_at ? msToIso(msg.received_at) : null,
|
||||
type: msg.type,
|
||||
});
|
||||
} else {
|
||||
printText(`From: ${msg.from_address}`);
|
||||
printText(`To: ${msg.to_address}`);
|
||||
printText(`Subject: ${msg.subject}`);
|
||||
printText(`---`);
|
||||
if (opts.format === "html") {
|
||||
printText(msg.html || "(no HTML content)");
|
||||
} else {
|
||||
printText(msg.content || "(no text content)");
|
||||
}
|
||||
}
|
||||
} catch (e: any) {
|
||||
log(`Error: ${e.message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
}
|
||||
35
packages/cli/src/commands/send.ts
Normal file
35
packages/cli/src/commands/send.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { Command } from "commander";
|
||||
import { api } from "../api.js";
|
||||
import { log, printJson, printText } from "../output.js";
|
||||
|
||||
export function registerSendCommand(program: Command) {
|
||||
program
|
||||
.command("send")
|
||||
.description("Send an email from a temporary address")
|
||||
.requiredOption("--email-id <id>", "email ID to send from")
|
||||
.requiredOption("--to <address>", "recipient email address")
|
||||
.requiredOption("--subject <subject>", "email subject")
|
||||
.requiredOption("--content <content>", "email body text")
|
||||
.action(async (opts) => {
|
||||
const json = program.opts().json;
|
||||
try {
|
||||
const result = (await api.sendEmail(opts.emailId, {
|
||||
to: opts.to,
|
||||
subject: opts.subject,
|
||||
content: opts.content,
|
||||
})) as any;
|
||||
|
||||
if (json) {
|
||||
printJson({
|
||||
success: true,
|
||||
remainingEmails: result.remainingEmails,
|
||||
});
|
||||
} else {
|
||||
printText(`Email sent successfully. Remaining today: ${result.remainingEmails}`);
|
||||
}
|
||||
} catch (e: any) {
|
||||
log(`Error: ${e.message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
}
|
||||
62
packages/cli/src/commands/wait.ts
Normal file
62
packages/cli/src/commands/wait.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
import { Command } from "commander";
|
||||
import { api } from "../api.js";
|
||||
import { log, printJson, printText, msToIso } from "../output.js";
|
||||
|
||||
function sleep(ms: number): Promise<void> {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
export function registerWaitCommand(program: Command) {
|
||||
program
|
||||
.command("wait")
|
||||
.description("Wait for a new email to arrive")
|
||||
.requiredOption("--email-id <id>", "email ID to watch")
|
||||
.option("--timeout <seconds>", "max wait time in seconds", "120")
|
||||
.option("--interval <seconds>", "poll interval in seconds", "5")
|
||||
.action(async (opts) => {
|
||||
const json = program.opts().json;
|
||||
const timeout = parseInt(opts.timeout, 10);
|
||||
const interval = parseInt(opts.interval, 10);
|
||||
const emailId = opts.emailId;
|
||||
|
||||
try {
|
||||
const initial = (await api.listMessages(emailId)) as any;
|
||||
const knownIds = new Set<string>(initial.messages.map((m: any) => m.id));
|
||||
|
||||
const startTime = Date.now();
|
||||
|
||||
while (true) {
|
||||
const elapsed = Math.floor((Date.now() - startTime) / 1000);
|
||||
if (elapsed >= timeout) {
|
||||
log(`Timeout: no new messages received within ${timeout}s`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
log(`Polling... (${elapsed}/${timeout}s)`);
|
||||
await sleep(interval * 1000);
|
||||
|
||||
const current = (await api.listMessages(emailId)) as any;
|
||||
const newMessages = current.messages.filter((m: any) => !knownIds.has(m.id));
|
||||
|
||||
if (newMessages.length > 0) {
|
||||
const msg = newMessages[0];
|
||||
if (json) {
|
||||
printJson({
|
||||
messageId: msg.id,
|
||||
from: msg.from_address,
|
||||
subject: msg.subject,
|
||||
receivedAt: msg.received_at ? msToIso(msg.received_at) : null,
|
||||
});
|
||||
} else {
|
||||
printText(`New message from ${msg.from_address}: "${msg.subject}"`);
|
||||
printText(`Message ID: ${msg.id}`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
} catch (e: any) {
|
||||
log(`Error: ${e.message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
}
|
||||
56
packages/cli/src/config.ts
Normal file
56
packages/cli/src/config.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
||||
import { homedir } from "os";
|
||||
import { join } from "path";
|
||||
|
||||
export interface CliConfig {
|
||||
apiUrl: string;
|
||||
apiKey: string;
|
||||
}
|
||||
|
||||
const CONFIG_DIR = join(homedir(), ".moemail");
|
||||
const CONFIG_FILE = join(CONFIG_DIR, "config.json");
|
||||
|
||||
export function loadConfig(): CliConfig {
|
||||
const config: CliConfig = { apiUrl: "", apiKey: "" };
|
||||
|
||||
// File config
|
||||
if (existsSync(CONFIG_FILE)) {
|
||||
try {
|
||||
const raw = JSON.parse(readFileSync(CONFIG_FILE, "utf-8"));
|
||||
if (raw.apiUrl) config.apiUrl = raw.apiUrl;
|
||||
if (raw.apiKey) config.apiKey = raw.apiKey;
|
||||
} catch {}
|
||||
}
|
||||
|
||||
// Env overrides (higher priority)
|
||||
if (process.env.MOEMAIL_API_URL) config.apiUrl = process.env.MOEMAIL_API_URL;
|
||||
if (process.env.MOEMAIL_API_KEY) config.apiKey = process.env.MOEMAIL_API_KEY;
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
export function saveConfig(key: string, value: string): void {
|
||||
if (!existsSync(CONFIG_DIR)) {
|
||||
mkdirSync(CONFIG_DIR, { recursive: true });
|
||||
}
|
||||
|
||||
let config: Record<string, string> = {};
|
||||
if (existsSync(CONFIG_FILE)) {
|
||||
try {
|
||||
config = JSON.parse(readFileSync(CONFIG_FILE, "utf-8"));
|
||||
} catch {}
|
||||
}
|
||||
|
||||
const keyMap: Record<string, string> = {
|
||||
"api-url": "apiUrl",
|
||||
"api-key": "apiKey",
|
||||
};
|
||||
|
||||
const configKey = keyMap[key];
|
||||
if (!configKey) {
|
||||
throw new Error(`Unknown config key: ${key}. Valid keys: api-url, api-key`);
|
||||
}
|
||||
|
||||
config[configKey] = value;
|
||||
writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
|
||||
}
|
||||
27
packages/cli/src/index.ts
Normal file
27
packages/cli/src/index.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
#!/usr/bin/env node
|
||||
import { Command } from "commander";
|
||||
import { registerConfigCommand } from "./commands/config.js";
|
||||
import { registerCreateCommand } from "./commands/create.js";
|
||||
import { registerListCommand } from "./commands/list.js";
|
||||
import { registerWaitCommand } from "./commands/wait.js";
|
||||
import { registerReadCommand } from "./commands/read.js";
|
||||
import { registerDeleteCommand } from "./commands/delete.js";
|
||||
import { registerSendCommand } from "./commands/send.js";
|
||||
|
||||
const program = new Command();
|
||||
|
||||
program
|
||||
.name("moemail")
|
||||
.description("MoeMail CLI — Agent-friendly temporary email tool")
|
||||
.version("0.1.0")
|
||||
.option("--json", "output as JSON");
|
||||
|
||||
registerConfigCommand(program);
|
||||
registerCreateCommand(program);
|
||||
registerListCommand(program);
|
||||
registerWaitCommand(program);
|
||||
registerReadCommand(program);
|
||||
registerDeleteCommand(program);
|
||||
registerSendCommand(program);
|
||||
|
||||
program.parse();
|
||||
27
packages/cli/src/output.ts
Normal file
27
packages/cli/src/output.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* Print JSON to stdout (for --json mode).
|
||||
*/
|
||||
export function printJson(data: unknown): void {
|
||||
console.log(JSON.stringify(data));
|
||||
}
|
||||
|
||||
/**
|
||||
* Print human-readable text to stdout.
|
||||
*/
|
||||
export function printText(text: string): void {
|
||||
console.log(text);
|
||||
}
|
||||
|
||||
/**
|
||||
* Log to stderr (progress, errors — never pollutes stdout).
|
||||
*/
|
||||
export function log(message: string): void {
|
||||
console.error(message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert epoch ms timestamp to ISO 8601 string.
|
||||
*/
|
||||
export function msToIso(ms: number): string {
|
||||
return new Date(ms).toISOString();
|
||||
}
|
||||
14
packages/cli/tsconfig.json
Normal file
14
packages/cli/tsconfig.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"esModuleInterop": true,
|
||||
"strict": true,
|
||||
"outDir": "dist",
|
||||
"rootDir": "src",
|
||||
"declaration": true,
|
||||
"skipLibCheck": true
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
24
public/llms.txt
Normal file
24
public/llms.txt
Normal file
@@ -0,0 +1,24 @@
|
||||
# MoeMail
|
||||
|
||||
> Temporary email service with CLI tool for AI Agents
|
||||
|
||||
MoeMail provides disposable email addresses. Install the CLI for programmatic access:
|
||||
|
||||
## CLI Tool
|
||||
|
||||
Install: npm i -g moemail-cli
|
||||
|
||||
Setup: moemail config set api-url https://moemail.app && moemail config set api-key YOUR_KEY
|
||||
|
||||
Commands:
|
||||
- moemail create --domain <domain> --expiry <1h|24h|3d|permanent> --json
|
||||
- moemail list --json
|
||||
- moemail list --email-id <id> --json
|
||||
- moemail wait --email-id <id> --timeout <seconds> --json
|
||||
- moemail read --email-id <id> --message-id <id> --json
|
||||
- moemail send --email-id <id> --to <addr> --subject <subj> --content <body> --json
|
||||
- moemail delete --email-id <id> --json
|
||||
|
||||
Typical workflow: create email → use address for signup → wait for verification → read content → extract code → delete
|
||||
|
||||
All commands support --json for structured output. Exit code 0 = success, 1 = failure, 2 = auth error.
|
||||
Reference in New Issue
Block a user