Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions src/commands/emails/attachment.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { Command } from '@commander-js/extra-typings';
import { runGet } from '../../lib/actions';
import type { GlobalOpts } from '../../lib/client';
import { buildHelpText } from '../../lib/help-text';
import { pickId } from '../../lib/prompts';
import { attachmentPickerConfig, emailPickerConfig } from './utils';

export const getAttachmentCommand = new Command('attachment')
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
.description('Retrieve a single attachment from a sent (outbound) email')
.argument('[emailId]', 'Email UUID')
.argument('[attachmentId]', 'Attachment UUID')
.addHelpText(
'after',
buildHelpText({
context:
'The download_url is a signed URL that expires in ~1 hour. Download the file directly:\n resend emails attachment <emailId> <attachmentId> --json | jq -r .download_url | xargs curl -O',
output:
' {"object":"attachment","id":"<uuid>","filename":"invoice.pdf","size":51200,"content_type":"application/pdf","content_disposition":"attachment","content_id":null,"download_url":"<signed-url>","expires_at":"<iso-date>"}',
errorCodes: ['auth_error', 'fetch_error'],
examples: [
'resend emails attachment <email-id> <attachment-id>',
'resend emails attachment <email-id> <attachment-id> --json',
],
}),
)
.action(async (emailIdArg, attachmentIdArg, _opts, cmd) => {
const globalOpts = cmd.optsWithGlobals() as GlobalOpts;
const emailId = await pickId(emailIdArg, emailPickerConfig, globalOpts);
const attachmentId = await pickId(
attachmentIdArg,
attachmentPickerConfig(emailId),
globalOpts,
);
await runGet(
{
loading: 'Fetching attachment...',
sdkCall: (resend) =>
resend.emails.attachments.get({ emailId, id: attachmentId }),
onInteractive: (data) => {
console.log(`${data.filename ?? '(unnamed)'}`);
console.log(`ID: ${data.id}`);
console.log(`Content-Type: ${data.content_type}`);
console.log(`Size: ${data.size} bytes`);
console.log(`Disposition: ${data.content_disposition}`);
console.log(`Download URL: ${data.download_url}`);
console.log(`Expires: ${data.expires_at}`);
},
},
globalOpts,
);
});
76 changes: 76 additions & 0 deletions src/commands/emails/attachments.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { Command } from '@commander-js/extra-typings';
import { runList } from '../../lib/actions';
import type { GlobalOpts } from '../../lib/client';
import { buildHelpText } from '../../lib/help-text';
import {
buildPaginationOpts,
parseLimitOpt,
printPaginationHint,
} from '../../lib/pagination';
import { pickId } from '../../lib/prompts';
import { emailPickerConfig, renderAttachmentsTable } from './utils';

export const listAttachmentsCommand = new Command('attachments')
.description('List attachments on a sent (outbound) email')
.argument('[emailId]', 'Email UUID')
.option(
'--limit <n>',
'Maximum number of attachments to return (1-100)',
'10',
)
.option(
'--after <cursor>',
'Return attachments after this cursor (next page)',
)
.option(
'--before <cursor>',
'Return attachments before this cursor (previous page)',
)
.addHelpText(
'after',
buildHelpText({
context:
'Each attachment has a download_url (signed, expires ~1 hour).\nUse the attachment sub-command to retrieve a single attachment with its download URL:\n resend emails attachment <emailId> <attachmentId>\n\ncontent_disposition: "inline" means the attachment is embedded in the HTML body (e.g. an image).\ncontent_disposition: "attachment" means it is a standalone file download.',
output:
' {"object":"list","has_more":false,"data":[{"id":"<uuid>","filename":"invoice.pdf","size":51200,"content_type":"application/pdf","content_disposition":"attachment","content_id":null,"download_url":"<url>","expires_at":"<iso-date>"}]}',
errorCodes: ['auth_error', 'invalid_limit', 'list_error'],
examples: [
'resend emails attachments <email-id>',
'resend emails attachments <email-id> --json',
'resend emails attachments <email-id> --limit 25 --json',
],
}),
)
.action(async (emailIdArg, opts, cmd) => {
const globalOpts = cmd.optsWithGlobals() as GlobalOpts;
const emailId = await pickId(emailIdArg, emailPickerConfig, globalOpts);

const limit = parseLimitOpt(opts.limit, globalOpts);
const paginationOpts = buildPaginationOpts(
limit,
opts.after,
opts.before,
globalOpts,
);

await runList(
{
loading: 'Fetching attachments...',
sdkCall: (resend) =>
resend.emails.attachments.list({
emailId,
...paginationOpts,
}),
onInteractive: (list) => {
console.log(renderAttachmentsTable(list.data));
printPaginationHint(list, `emails attachments ${emailId}`, {
limit,
before: opts.before,
apiKey: globalOpts.apiKey,
profile: globalOpts.profile,
});
},
},
globalOpts,
);
});
6 changes: 6 additions & 0 deletions src/commands/emails/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { Command } from '@commander-js/extra-typings';
import { buildHelpText } from '../../lib/help-text';
import { getAttachmentCommand } from './attachment';
import { listAttachmentsCommand } from './attachments';
import { batchCommand } from './batch';
import { cancelCommand } from './cancel';
import { getEmailCommand } from './get';
Expand All @@ -18,6 +20,8 @@ export const emailsCommand = new Command('emails')
'resend emails get <email-id>',
'resend emails batch --file ./emails.json',
'resend emails cancel <email-id>',
'resend emails attachments <email-id>',
'resend emails attachment <email-id> <attachment-id>',
'resend emails receiving list',
'resend emails receiving forward <email-id> --to delivered@resend.com --from onboarding@resend.com',
],
Expand All @@ -29,4 +33,6 @@ export const emailsCommand = new Command('emails')
.addCommand(batchCommand)
.addCommand(cancelCommand)
.addCommand(updateCommand)
.addCommand(listAttachmentsCommand)
.addCommand(getAttachmentCommand)
.addCommand(receivingCommand);
64 changes: 37 additions & 27 deletions src/commands/emails/receiving/attachment.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
import { Command } from '@commander-js/extra-typings';
import { runGet } from '../../../lib/actions';
import type { GlobalOpts } from '../../../lib/client';
import { requireClient } from '../../../lib/client';
import { buildHelpText } from '../../../lib/help-text';
import { outputResult } from '../../../lib/output';
import { withSpinner } from '../../../lib/spinner';
import { isInteractive } from '../../../lib/tty';
import { pickId } from '../../../lib/prompts';
import {
receivedAttachmentPickerConfig,
receivedEmailPickerConfig,
} from './utils';

export const getAttachmentCommand = new Command('attachment')
.description('Retrieve a single attachment from a received (inbound) email')
.argument('<emailId>', 'Received email UUID')
.argument('<attachmentId>', 'Attachment UUID')
.argument('[emailId]', 'Received email UUID')
.argument('[attachmentId]', 'Attachment UUID')
.addHelpText(
'after',
buildHelpText({
Expand All @@ -24,28 +26,36 @@ export const getAttachmentCommand = new Command('attachment')
],
}),
)
.action(async (emailId, attachmentId, _opts, cmd) => {
.action(async (emailIdArg, attachmentIdArg, _opts, cmd) => {
const globalOpts = cmd.optsWithGlobals() as GlobalOpts;
const resend = await requireClient(globalOpts);

const data = await withSpinner(
'Fetching attachment...',
() =>
resend.emails.receiving.attachments.get({ emailId, id: attachmentId }),
'fetch_error',
const emailId = await pickId(
emailIdArg,
receivedEmailPickerConfig,
globalOpts,
);
const attachmentId = await pickId(
attachmentIdArg,
receivedAttachmentPickerConfig(emailId),
globalOpts,
);
await runGet(
{
loading: 'Fetching attachment...',
sdkCall: (resend) =>
resend.emails.receiving.attachments.get({
emailId,
id: attachmentId,
}),
onInteractive: (data) => {
console.log(`${data.filename ?? '(unnamed)'}`);
console.log(`ID: ${data.id}`);
console.log(`Content-Type: ${data.content_type}`);
console.log(`Size: ${data.size} bytes`);
console.log(`Disposition: ${data.content_disposition}`);
console.log(`Download URL: ${data.download_url}`);
console.log(`Expires: ${data.expires_at}`);
},
},
globalOpts,
);

if (!globalOpts.json && isInteractive()) {
const d = data;
console.log(`${d.filename ?? '(unnamed)'}`);
console.log(`ID: ${d.id}`);
console.log(`Content-Type: ${d.content_type}`);
console.log(`Size: ${d.size} bytes`);
console.log(`Disposition: ${d.content_disposition}`);
console.log(`Download URL: ${d.download_url}`);
console.log(`Expires: ${d.expires_at}`);
} else {
outputResult(data, { json: globalOpts.json });
}
});
3 changes: 2 additions & 1 deletion src/commands/emails/receiving/attachments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ import {
printPaginationHint,
} from '../../../lib/pagination';
import { pickId } from '../../../lib/prompts';
import { receivedEmailPickerConfig, renderAttachmentsTable } from './utils';
import { renderAttachmentsTable } from '../utils';
import { receivedEmailPickerConfig } from './utils';

export const listAttachmentsCommand = new Command('attachments')
.description('List attachments on a received (inbound) email')
Expand Down
37 changes: 17 additions & 20 deletions src/commands/emails/receiving/utils.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,4 @@
import type {
ListAttachmentsResponseSuccess,
ListReceivingEmail,
} from 'resend';
import type { ListReceivingEmail } from 'resend';
import type { PickerConfig } from '../../../lib/prompts';
import { renderTable } from '../../../lib/table';

Expand All @@ -16,6 +13,22 @@ export const receivedEmailPickerConfig: PickerConfig<{
display: (e) => ({ label: e.subject || '(no subject)', hint: e.id }),
};

export function receivedAttachmentPickerConfig(
emailId: string,
): PickerConfig<{ id: string; filename?: string }> {
return {
resource: 'attachment',
resourcePlural: 'attachments',
fetchItems: (resend, { limit, after }) =>
resend.emails.receiving.attachments.list({
emailId,
limit,
...(after && { after }),
}),
display: (a) => ({ label: a.filename ?? '(unnamed)', hint: a.id }),
};
}

export function renderReceivingEmailsTable(
emails: ListReceivingEmail[],
): string {
Expand All @@ -32,19 +45,3 @@ export function renderReceivingEmailsTable(
'(no received emails)',
);
}

export function renderAttachmentsTable(
attachments: ListAttachmentsResponseSuccess['data'],
): string {
const rows = attachments.map((a) => [
a.filename ?? '(unnamed)',
a.content_type,
String(a.size),
a.id,
]);
return renderTable(
['Filename', 'Content-Type', 'Size (bytes)', 'ID'],
rows,
'(no attachments)',
);
}
34 changes: 34 additions & 0 deletions src/commands/emails/utils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import type { ListAttachmentsResponseSuccess } from 'resend';
import type { PickerConfig } from '../../lib/prompts';
import { renderTable } from '../../lib/table';

export const emailPickerConfig: PickerConfig<{
id: string;
Expand All @@ -10,3 +12,35 @@ export const emailPickerConfig: PickerConfig<{
resend.emails.list({ limit, ...(after && { after }) }),
display: (e) => ({ label: e.subject || '(no subject)', hint: e.id }),
};

export function attachmentPickerConfig(
emailId: string,
): PickerConfig<{ id: string; filename?: string }> {
return {
resource: 'attachment',
resourcePlural: 'attachments',
fetchItems: (resend, { limit, after }) =>
resend.emails.attachments.list({
emailId,
limit,
...(after && { after }),
}),
display: (a) => ({ label: a.filename ?? '(unnamed)', hint: a.id }),
};
}

export function renderAttachmentsTable(
attachments: ListAttachmentsResponseSuccess['data'],
): string {
const rows = attachments.map((a) => [
a.filename ?? '(unnamed)',
a.content_type,
String(a.size),
a.id,
]);
return renderTable(
['Filename', 'Content-Type', 'Size (bytes)', 'ID'],
rows,
'(no attachments)',
);
}
Loading