Skip to content
Open
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
15 changes: 12 additions & 3 deletions src/commands/openclaw.ts
Original file line number Diff line number Diff line change
Expand Up @@ -467,15 +467,20 @@ openmail send --to "recipient@example.com" --subject "Report" --body "<p>See att

\`--body\` accepts plain text or HTML — HTML is detected and rendered automatically.

Add \`--attach <path>\` to attach files (repeatable). The response includes
\`messageId\` and \`threadId\` — store \`threadId\` to continue the conversation
later. Subject is ignored when replying in a thread.
Add \`--attach <path>\` to attach files (repeatable). Add \`--cc <email>\` (repeatable)
to copy other recipients. The response includes \`messageId\` and \`threadId\` — store
\`threadId\` to continue the conversation later. \`--subject\` is optional when
\`--thread-id\` is set — the thread subject is used automatically.

**Always reply in the existing thread.** When the user asks you to reply
to an email, look up the thread with \`openmail inbox\` or
\`openmail threads list\` first, then use \`--thread-id\`. Never create a
new thread unless the user explicitly asks for one.

**When you were CC'd:** check \`deliveryRole\` and \`headerTo\` on the inbound
message (via \`threads get\`). If \`deliveryRole\` is \`cc\`, reply to \`headerTo\`
— not to \`fromAddr\`. OpenMail auto-CCs \`fromAddr\` on thread replies.

## Checking for new mail

**Always use \`threads list --is-read false\` to check for new mail.**
Expand Down Expand Up @@ -527,6 +532,10 @@ Each message has:
| \`id\` | Message identifier |
| \`threadId\` | Conversation thread |
| \`fromAddr\` | Sender address |
| \`toAddr\` | Envelope recipient (your inbox address on inbound) |
| \`headerTo\` | Who the sender addressed in the To header (use for CC replies) |
| \`deliveryRole\` | \`to\` or \`cc\` — primary recipient vs CC'd |
| \`cc\` | CC recipients |
| \`subject\` | Subject line |
| \`bodyText\` | Plain text body (use this) |
| \`attachments\` | Array with \`filename\`, \`url\`, \`sizeBytes\` |
Expand Down
6 changes: 4 additions & 2 deletions src/commands/send.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,11 +48,12 @@ export async function runSendCommand(
const noQuote = getBooleanFlag(parsed.flags, "no-quote");
const idempotencyKey = getStringFlag(parsed.flags, "idempotency-key");
const replyTo = getStringFlag(parsed.flags, "reply-to");
const cc = getRepeatedStringFlag("cc");
const attachPaths = getRepeatedStringFlag("attach");

if (!inboxId) throw new Error("missing inbox id; run `openmail init` or pass --inbox-id");
if (!to) throw new Error("missing --to");
if (!subject) throw new Error("missing --subject");
if (!threadId && !subject) throw new Error("missing --subject (optional when --thread-id is set)");
if (!body) throw new Error("missing --body");

let attachments: { path: string; filename: string; contentType: string }[] | undefined;
Expand Down Expand Up @@ -80,13 +81,14 @@ export async function runSendCommand(
return client.sendEmail({
inboxId,
to,
subject,
subject: subject ?? "",
body,
bodyHtml,
threadId,
includeQuote: noQuote ? false : undefined,
idempotencyKey,
replyTo,
cc: cc.length > 0 ? cc : undefined,
attachments,
});
}
6 changes: 4 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -361,14 +361,16 @@ function printHelp(topic?: string) {
"openmail send",
"",
"Usage:",
" send --to <email> --subject <text> --body <text> [--inbox-id <id>]",
" send --to <email> --body <text> [--subject <text>] [--inbox-id <id>]",
" [--thread-id <id>] [--no-quote] [--idempotency-key <key>]",
" [--reply-to <email>] [--attach <file>]",
" [--reply-to <email>] [--cc <email>] [--attach <file>]",
"",
"Options:",
" --subject <text> Required for new threads; optional with --thread-id",
" --body <text> Plain text or HTML (HTML is auto-detected and rendered)",
" --thread-id <id> Reply in a thread; quotes the previous message by default",
" --no-quote Send only your reply text (skip auto-quoted history)",
" --cc <email> CC recipient (repeatable)",
" --reply-to <email> Address replies should go to. Free plan: must be the",
" address of an inbox you own. Developer+: any address.",
" --attach <file> Attach a file (repeatable for multiple files)",
Expand Down
7 changes: 6 additions & 1 deletion src/lib/http.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ export class OpenMailHttpClient {
includeQuote?: boolean;
idempotencyKey?: string;
replyTo?: string;
cc?: string[];
attachments?: { path: string; filename: string; contentType: string }[];
}) {
const idempotencyKey = params.idempotencyKey ?? crypto.randomUUID();
Expand All @@ -82,6 +83,9 @@ export class OpenMailHttpClient {
formData.append("includeQuote", "false");
}
if (params.replyTo) formData.append("replyTo", params.replyTo);
if (params.cc?.length) {
for (const addr of params.cc) formData.append("cc", addr);
}

for (const att of params.attachments) {
const data = await readFile(att.path);
Expand All @@ -96,7 +100,7 @@ export class OpenMailHttpClient {
});
}

const payload: Record<string, string | boolean> = {
const payload: Record<string, unknown> = {
to: params.to,
subject: params.subject,
body: params.body,
Expand All @@ -105,6 +109,7 @@ export class OpenMailHttpClient {
if (params.threadId) payload.threadId = params.threadId;
if (params.includeQuote === false) payload.includeQuote = false;
if (params.replyTo) payload.replyTo = params.replyTo;
if (params.cc?.length) payload.cc = params.cc;

return this.post(url, payload, {
"Idempotency-Key": idempotencyKey,
Expand Down