Skip to content

Commit d47e621

Browse files
authored
Merge pull request #59 from atxp-dev/claude/add-email-attachments-L2vPD
2 parents cc9c698 + 52d3aac commit d47e621

File tree

3 files changed

+143
-8
lines changed

3 files changed

+143
-8
lines changed

packages/atxp/src/commands/email.ts

Lines changed: 105 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,67 @@
11
import { callTool } from '../call-tool.js';
22
import chalk from 'chalk';
3+
import { readFileSync } from 'fs';
4+
import path from 'path';
35

46
const SERVER = 'email.mcp.atxp.ai';
57

68
interface EmailOptions {
79
to?: string;
810
subject?: string;
911
body?: string;
12+
attach?: string[];
13+
}
14+
15+
interface Attachment {
16+
filename: string;
17+
contentType: string;
18+
content: string;
19+
}
20+
21+
const MIME_TYPES: Record<string, string> = {
22+
'.pdf': 'application/pdf',
23+
'.png': 'image/png',
24+
'.jpg': 'image/jpeg',
25+
'.jpeg': 'image/jpeg',
26+
'.gif': 'image/gif',
27+
'.webp': 'image/webp',
28+
'.svg': 'image/svg+xml',
29+
'.txt': 'text/plain',
30+
'.csv': 'text/csv',
31+
'.json': 'application/json',
32+
'.xml': 'application/xml',
33+
'.html': 'text/html',
34+
'.htm': 'text/html',
35+
'.zip': 'application/zip',
36+
'.gz': 'application/gzip',
37+
'.tar': 'application/x-tar',
38+
'.doc': 'application/msword',
39+
'.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
40+
'.xls': 'application/vnd.ms-excel',
41+
'.xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
42+
'.ppt': 'application/vnd.ms-powerpoint',
43+
'.pptx': 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
44+
'.mp3': 'audio/mpeg',
45+
'.wav': 'audio/wav',
46+
'.mp4': 'video/mp4',
47+
'.webm': 'video/webm',
48+
};
49+
50+
function getMimeType(filePath: string): string {
51+
const ext = path.extname(filePath).toLowerCase();
52+
return MIME_TYPES[ext] || 'application/octet-stream';
53+
}
54+
55+
function loadAttachments(filePaths: string[]): Attachment[] {
56+
return filePaths.map((filePath) => {
57+
const resolved = path.resolve(filePath);
58+
const content = readFileSync(resolved);
59+
return {
60+
filename: path.basename(resolved),
61+
contentType: getMimeType(resolved),
62+
content: content.toString('base64'),
63+
};
64+
});
1065
}
1166

1267
function showEmailHelp(): void {
@@ -26,6 +81,7 @@ function showEmailHelp(): void {
2681
console.log(' ' + chalk.yellow('--to') + ' ' + chalk.gray('<email>') + ' ' + 'Recipient email address (required for send)');
2782
console.log(' ' + chalk.yellow('--subject') + ' ' + chalk.gray('<text>') + ' ' + 'Email subject line (required for send)');
2883
console.log(' ' + chalk.yellow('--body') + ' ' + chalk.gray('<text>') + ' ' + 'Email body content (required)');
84+
console.log(' ' + chalk.yellow('--attach') + ' ' + chalk.gray('<file>') + ' ' + 'Attach a file (repeatable)');
2985
console.log();
3086
console.log(chalk.bold('Get Attachment Options:'));
3187
console.log(' ' + chalk.yellow('--message') + ' ' + chalk.gray('<id>') + ' ' + 'Message ID (required)');
@@ -35,7 +91,10 @@ function showEmailHelp(): void {
3591
console.log(' npx atxp email inbox');
3692
console.log(' npx atxp email read msg_abc123');
3793
console.log(' npx atxp email send --to user@example.com --subject "Hello" --body "Hi there!"');
94+
console.log(' npx atxp email send --to user@example.com --subject "Report" --body "See attached." --attach report.pdf');
95+
console.log(' npx atxp email send --to user@example.com --subject "Files" --body "Two files." --attach a.pdf --attach b.png');
3896
console.log(' npx atxp email reply msg_abc123 --body "Thanks for your message!"');
97+
console.log(' npx atxp email reply msg_abc123 --body "Updated version attached." --attach report-v2.pdf');
3998
console.log(' npx atxp email search "invoice"');
4099
console.log(' npx atxp email delete msg_abc123');
41100
console.log(' npx atxp email get-attachment --message msg_abc123 --index 0');
@@ -135,10 +194,19 @@ async function checkInbox(): Promise<void> {
135194

136195
for (const email of parsed.messages) {
137196
const readIndicator = email.read === false ? chalk.yellow(' [UNREAD]') : '';
138-
console.log(chalk.gray('ID: ' + email.messageId) + readIndicator);
197+
const attachIndicator = email.attachments && email.attachments.length > 0
198+
? chalk.magenta(` [${email.attachments.length} attachment(s)]`)
199+
: '';
200+
console.log(chalk.gray('ID: ' + email.messageId) + readIndicator + attachIndicator);
139201
console.log(chalk.bold('From: ') + email.from);
140202
console.log(chalk.bold('Subject: ') + email.subject);
141203
console.log(chalk.bold('Date: ') + email.date);
204+
if (email.attachments && email.attachments.length > 0) {
205+
for (let i = 0; i < email.attachments.length; i++) {
206+
const att = email.attachments[i];
207+
console.log(chalk.gray(` [${i}] ${att.filename} (${att.contentType}, ${att.size} bytes)`));
208+
}
209+
}
142210
console.log(chalk.gray('─'.repeat(50)));
143211
}
144212

@@ -224,11 +292,19 @@ async function sendEmail(options: EmailOptions): Promise<void> {
224292
process.exit(1);
225293
}
226294

227-
const result = await callTool(SERVER, 'email_send_email', {
228-
to,
229-
subject,
230-
body,
231-
});
295+
const args: Record<string, unknown> = { to, subject, body };
296+
297+
if (options.attach && options.attach.length > 0) {
298+
try {
299+
args.attachments = loadAttachments(options.attach);
300+
} catch (err: unknown) {
301+
const msg = err instanceof Error ? err.message : String(err);
302+
console.error(chalk.red('Error reading attachment: ' + msg));
303+
process.exit(1);
304+
}
305+
}
306+
307+
const result = await callTool(SERVER, 'email_send_email', args);
232308

233309
try {
234310
const parsed = JSON.parse(result);
@@ -264,7 +340,19 @@ async function replyToEmail(messageId?: string, options?: EmailOptions): Promise
264340
process.exit(1);
265341
}
266342

267-
const result = await callTool(SERVER, 'email_reply', { messageId, body });
343+
const args: Record<string, unknown> = { messageId, body };
344+
345+
if (options?.attach && options.attach.length > 0) {
346+
try {
347+
args.attachments = loadAttachments(options.attach);
348+
} catch (err: unknown) {
349+
const msg = err instanceof Error ? err.message : String(err);
350+
console.error(chalk.red('Error reading attachment: ' + msg));
351+
process.exit(1);
352+
}
353+
}
354+
355+
const result = await callTool(SERVER, 'email_reply', args);
268356

269357
try {
270358
const parsed = JSON.parse(result);
@@ -314,10 +402,19 @@ async function searchEmails(query?: string): Promise<void> {
314402

315403
for (const email of parsed.messages) {
316404
const readIndicator = email.read === false ? chalk.yellow(' [UNREAD]') : '';
317-
console.log(chalk.gray('ID: ' + email.messageId) + readIndicator);
405+
const attachIndicator = email.attachments && email.attachments.length > 0
406+
? chalk.magenta(` [${email.attachments.length} attachment(s)]`)
407+
: '';
408+
console.log(chalk.gray('ID: ' + email.messageId) + readIndicator + attachIndicator);
318409
console.log(chalk.bold('From: ') + email.from);
319410
console.log(chalk.bold('Subject: ') + email.subject);
320411
console.log(chalk.bold('Date: ') + email.date);
412+
if (email.attachments && email.attachments.length > 0) {
413+
for (let i = 0; i < email.attachments.length; i++) {
414+
const att = email.attachments[i];
415+
console.log(chalk.gray(` [${i}] ${att.filename} (${att.contentType}, ${att.size} bytes)`));
416+
}
417+
}
321418
console.log(chalk.gray('─'.repeat(50)));
322419
}
323420
} catch {

packages/atxp/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ interface EmailOptions {
4848
to?: string;
4949
subject?: string;
5050
body?: string;
51+
attach?: string[];
5152
}
5253

5354
interface PhoneOptionsLocal {
@@ -251,6 +252,7 @@ function parseArgs(): {
251252
to: getArgValue('--to', ''),
252253
subject: getArgValue('--subject', ''),
253254
body: getArgValue('--body', ''),
255+
attach: getAllArgValues('--attach'),
254256
};
255257

256258
// Parse phone options

skills/atxp/SKILL.md

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -300,6 +300,42 @@ Each agent gets a unique address: `{user_id}@atxp.email`. Claim a username ($1.0
300300
| `npx atxp@latest email claim-username <username>` | $1.00 | Claim a username so your email becomes `{username}@atxp.email` instead of `{user_id}@atxp.email`. Username: 3-32 chars, starts with letter, lowercase alphanumeric/hyphens/underscores. |
301301
| `npx atxp@latest email release-username` | Free | Release username |
302302

303+
#### Email Attachments
304+
305+
**Sending attachments:** Use the `--attach` flag (repeatable) with `email send` or `email reply` to attach local files. The CLI reads each file, detects its MIME type from the extension, and base64-encodes the content automatically.
306+
307+
```bash
308+
# Send with one attachment
309+
npx atxp@latest email send --to user@example.com --subject "Report" --body "See attached." --attach report.pdf
310+
311+
# Send with multiple attachments
312+
npx atxp@latest email send --to user@example.com --subject "Files" --body "Two files." --attach report.pdf --attach chart.png
313+
314+
# Reply with an attachment
315+
npx atxp@latest email reply msg_abc123 --body "Updated version attached." --attach report-v2.pdf
316+
```
317+
318+
**Receiving attachments:** When listing emails (`email inbox`, `email search`) or reading a message (`email read`), attachment metadata (filename, MIME type, size) is displayed automatically. Attachment content is **not** included inline — use `email get-attachment` to download.
319+
320+
```bash
321+
# Download a specific attachment by index (0-based)
322+
npx atxp@latest email get-attachment --message msg_abc123 --index 0
323+
```
324+
325+
**MCP tool parameters for attachments:**
326+
327+
The `email_send_email` and `email_reply` MCP tools accept an optional `attachments` array:
328+
329+
| Field | Type | Description |
330+
|-------|------|-------------|
331+
| `filename` | string | Display name (e.g. `"report.pdf"`) |
332+
| `contentType` | string | MIME type (e.g. `"application/pdf"`) |
333+
| `content` | string | File bytes, base64-encoded |
334+
335+
The `email_get_attachment` MCP tool accepts `messageId` (string) and `attachmentIndex` (zero-based integer) and returns the file content as base64.
336+
337+
**Limits:** Total message size (body + all attachments) must not exceed 10 MB. Base64 encoding adds ~33% overhead, so the effective raw payload is ~7.5 MB per message.
338+
303339
### Phone
304340

305341
Register a phone number to send/receive SMS and make/receive voice calls. The phone command is async — calls and inbound messages arrive asynchronously, so check `phone calls` and `phone sms` for updates.

0 commit comments

Comments
 (0)