gws-axi 0.20.0 → 0.20.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/commands/gmail/compose.d.ts +18 -9
- package/dist/src/commands/gmail/compose.js +21 -21
- package/dist/src/commands/gmail/compose.js.map +1 -1
- package/dist/src/commands/gmail/compose.test.js +35 -69
- package/dist/src/commands/gmail/compose.test.js.map +1 -1
- package/dist/src/commands/gmail/draft.d.ts +1 -1
- package/dist/src/commands/gmail/draft.js +3 -3
- package/package.json +1 -1
|
@@ -1,14 +1,23 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* RFC 5322 message builder for draft creation.
|
|
3
3
|
*
|
|
4
|
-
* Drafts go out as `
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
4
|
+
* Drafts go out as a single `text/html` part carrying the rendered `--body`
|
|
5
|
+
* markdown. Gmail's composer adopts exactly one part of a draft as its editing
|
|
6
|
+
* surface and discards the rest, so a message offering only HTML leaves it no
|
|
7
|
+
* choice — the composer opens in rich text, our markup survives the human's
|
|
8
|
+
* edits, and Gmail generates the `text/plain` alternative itself on send.
|
|
9
|
+
*
|
|
10
|
+
* Both alternatives were tried and rejected against live sends:
|
|
11
|
+
*
|
|
12
|
+
* - Single-part `text/plain` (the original bug) drops the composer into
|
|
13
|
+
* plain-text mode. On send Gmail hard-wraps at ~70 columns with real CRLFs
|
|
14
|
+
* and ships no HTML alternative, so recipients get ragged columns nothing
|
|
15
|
+
* can reflow — while the compose window still shows the original unwrapped
|
|
16
|
+
* paragraphs, hiding the damage until the message is gone.
|
|
17
|
+
* - `multipart/alternative` (plain + HTML) fixes the wrapping but is a coin
|
|
18
|
+
* flip: two identically-built drafts behaved differently, one keeping our
|
|
19
|
+
* rendered markup and one adopting the plain part and delivering the raw
|
|
20
|
+
* markdown source to the reader — `**bold**` and `[text](url)` and all.
|
|
12
21
|
*
|
|
13
22
|
* See specs/commands/gmail-draft.md ("Message structure", "Body rendering").
|
|
14
23
|
*/
|
|
@@ -19,7 +28,7 @@ export interface ComposeFields {
|
|
|
19
28
|
cc?: string[];
|
|
20
29
|
bcc?: string[];
|
|
21
30
|
subject: string;
|
|
22
|
-
/** Markdown source
|
|
31
|
+
/** Markdown source, rendered to the HTML body. */
|
|
23
32
|
body: string;
|
|
24
33
|
}
|
|
25
34
|
/** Split a comma-separated recipient flag into trimmed, non-empty addresses. */
|
|
@@ -1,18 +1,26 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* RFC 5322 message builder for draft creation.
|
|
3
3
|
*
|
|
4
|
-
* Drafts go out as `
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
4
|
+
* Drafts go out as a single `text/html` part carrying the rendered `--body`
|
|
5
|
+
* markdown. Gmail's composer adopts exactly one part of a draft as its editing
|
|
6
|
+
* surface and discards the rest, so a message offering only HTML leaves it no
|
|
7
|
+
* choice — the composer opens in rich text, our markup survives the human's
|
|
8
|
+
* edits, and Gmail generates the `text/plain` alternative itself on send.
|
|
9
|
+
*
|
|
10
|
+
* Both alternatives were tried and rejected against live sends:
|
|
11
|
+
*
|
|
12
|
+
* - Single-part `text/plain` (the original bug) drops the composer into
|
|
13
|
+
* plain-text mode. On send Gmail hard-wraps at ~70 columns with real CRLFs
|
|
14
|
+
* and ships no HTML alternative, so recipients get ragged columns nothing
|
|
15
|
+
* can reflow — while the compose window still shows the original unwrapped
|
|
16
|
+
* paragraphs, hiding the damage until the message is gone.
|
|
17
|
+
* - `multipart/alternative` (plain + HTML) fixes the wrapping but is a coin
|
|
18
|
+
* flip: two identically-built drafts behaved differently, one keeping our
|
|
19
|
+
* rendered markup and one adopting the plain part and delivering the raw
|
|
20
|
+
* markdown source to the reader — `**bold**` and `[text](url)` and all.
|
|
12
21
|
*
|
|
13
22
|
* See specs/commands/gmail-draft.md ("Message structure", "Body rendering").
|
|
14
23
|
*/
|
|
15
|
-
import { randomUUID } from "node:crypto";
|
|
16
24
|
import { marked } from "marked";
|
|
17
25
|
/** Split a comma-separated recipient flag into trimmed, non-empty addresses. */
|
|
18
26
|
export function parseRecipients(value) {
|
|
@@ -45,7 +53,7 @@ export function renderMarkdown(body) {
|
|
|
45
53
|
return `<html><body>${fragment}</body></html>`;
|
|
46
54
|
}
|
|
47
55
|
/** Base64 with CRLF line endings wrapped at 76 columns (RFC 2045). */
|
|
48
|
-
function
|
|
56
|
+
function encodeBody(text) {
|
|
49
57
|
return Buffer.from(text, "utf8")
|
|
50
58
|
.toString("base64")
|
|
51
59
|
.replace(/(.{76})/g, "$1\r\n");
|
|
@@ -55,9 +63,6 @@ function encodePart(text) {
|
|
|
55
63
|
* `users.drafts.create` / `users.messages.send` `raw` fields.
|
|
56
64
|
*/
|
|
57
65
|
export function buildRawMessage(fields) {
|
|
58
|
-
// Unique per message, so it can never collide with body content — no
|
|
59
|
-
// scanning the body for the boundary, no retry loop.
|
|
60
|
-
const boundary = `=_gws-axi_${randomUUID()}`;
|
|
61
66
|
const headers = [`From: ${fields.from}`, `To: ${fields.to.join(", ")}`];
|
|
62
67
|
if (fields.cc?.length)
|
|
63
68
|
headers.push(`Cc: ${fields.cc.join(", ")}`);
|
|
@@ -65,14 +70,9 @@ export function buildRawMessage(fields) {
|
|
|
65
70
|
headers.push(`Bcc: ${fields.bcc.join(", ")}`);
|
|
66
71
|
headers.push(`Subject: ${encodeHeaderValue(fields.subject)}`);
|
|
67
72
|
headers.push("MIME-Version: 1.0");
|
|
68
|
-
headers.push(
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
const parts = [
|
|
72
|
-
['Content-Type: text/plain; charset="UTF-8"', fields.body],
|
|
73
|
-
['Content-Type: text/html; charset="UTF-8"', renderMarkdown(fields.body)],
|
|
74
|
-
].map(([contentType, content]) => `--${boundary}\r\n${contentType}\r\nContent-Transfer-Encoding: base64\r\n\r\n${encodePart(content)}`);
|
|
75
|
-
const raw = `${headers.join("\r\n")}\r\n\r\n${parts.join("\r\n")}\r\n--${boundary}--`;
|
|
73
|
+
headers.push('Content-Type: text/html; charset="UTF-8"');
|
|
74
|
+
headers.push("Content-Transfer-Encoding: base64");
|
|
75
|
+
const raw = `${headers.join("\r\n")}\r\n\r\n${encodeBody(renderMarkdown(fields.body))}`;
|
|
76
76
|
return Buffer.from(raw, "utf8").toString("base64url");
|
|
77
77
|
}
|
|
78
78
|
//# sourceMappingURL=compose.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"compose.js","sourceRoot":"","sources":["../../../../src/commands/gmail/compose.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"compose.js","sourceRoot":"","sources":["../../../../src/commands/gmail/compose.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAahC,gFAAgF;AAChF,MAAM,UAAU,eAAe,CAAC,KAAa;IAC3C,OAAO,KAAK;SACT,KAAK,CAAC,GAAG,CAAC;SACV,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;SACpB,MAAM,CAAC,OAAO,CAAC,CAAC;AACrB,CAAC;AAED;;;;GAIG;AACH,SAAS,iBAAiB,CAAC,KAAa;IACtC,IAAI,CAAC,GAAG,KAAK,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,EAAE,CAAC,WAAW,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,IAAI,CAAC;QAAE,OAAO,KAAK,CAAC;IAC7E,OAAO,aAAa,MAAM,CAAC,IAAI,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC,QAAQ,CAAC,QAAQ,CAAC,IAAI,CAAC;AACxE,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,cAAc,CAAC,IAAY;IACzC,MAAM,QAAQ,GAAG,MAAM,CAAC,KAAK,CAAC,IAAI,EAAE,EAAE,GAAG,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC,CAAC;IAC/E,OAAO,eAAe,QAAQ,gBAAgB,CAAC;AACjD,CAAC;AAED,sEAAsE;AACtE,SAAS,UAAU,CAAC,IAAY;IAC9B,OAAO,MAAM,CAAC,IAAI,CAAC,IAAI,EAAE,MAAM,CAAC;SAC7B,QAAQ,CAAC,QAAQ,CAAC;SAClB,OAAO,CAAC,UAAU,EAAE,QAAQ,CAAC,CAAC;AACnC,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,eAAe,CAAC,MAAqB;IACnD,MAAM,OAAO,GAAa,CAAC,SAAS,MAAM,CAAC,IAAI,EAAE,EAAE,OAAO,MAAM,CAAC,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IAClF,IAAI,MAAM,CAAC,EAAE,EAAE,MAAM;QAAE,OAAO,CAAC,IAAI,CAAC,OAAO,MAAM,CAAC,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IACnE,IAAI,MAAM,CAAC,GAAG,EAAE,MAAM;QAAE,OAAO,CAAC,IAAI,CAAC,QAAQ,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IACtE,OAAO,CAAC,IAAI,CAAC,YAAY,iBAAiB,CAAC,MAAM,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;IAC9D,OAAO,CAAC,IAAI,CAAC,mBAAmB,CAAC,CAAC;IAClC,OAAO,CAAC,IAAI,CAAC,0CAA0C,CAAC,CAAC;IACzD,OAAO,CAAC,IAAI,CAAC,mCAAmC,CAAC,CAAC;IAElD,MAAM,GAAG,GAAG,GAAG,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,WAAW,UAAU,CAAC,cAAc,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,EAAE,CAAC;IACxF,OAAO,MAAM,CAAC,IAAI,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC;AACxD,CAAC"}
|
|
@@ -1,28 +1,13 @@
|
|
|
1
1
|
import { describe, expect, it } from "vitest";
|
|
2
2
|
import { buildRawMessage, parseRecipients } from "./compose.js";
|
|
3
|
-
/** Decode a base64url raw message into its header block and decoded
|
|
3
|
+
/** Decode a base64url raw message into its header block and decoded HTML body. */
|
|
4
4
|
function decodeRaw(raw) {
|
|
5
5
|
const text = Buffer.from(raw, "base64url").toString("utf8");
|
|
6
6
|
const idx = text.indexOf("\r\n\r\n");
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
const parts = text
|
|
12
|
-
.slice(idx + 4)
|
|
13
|
-
.split(`--${boundary}`)
|
|
14
|
-
.slice(1, -1)
|
|
15
|
-
.map((chunk) => {
|
|
16
|
-
const split = chunk.indexOf("\r\n\r\n");
|
|
17
|
-
const partHeaders = chunk.slice(0, split);
|
|
18
|
-
const b64 = chunk.slice(split + 4).replace(/\r\n/g, "");
|
|
19
|
-
return {
|
|
20
|
-
contentType: /Content-Type: ([^;\r\n]+)/.exec(partHeaders)?.[1] ?? "",
|
|
21
|
-
content: Buffer.from(b64, "base64").toString("utf8"),
|
|
22
|
-
};
|
|
23
|
-
});
|
|
24
|
-
const find = (type) => parts.find((p) => p.contentType === type)?.content ?? "";
|
|
25
|
-
return { header, boundary, parts, plain: find("text/plain"), html: find("text/html") };
|
|
7
|
+
return {
|
|
8
|
+
header: text.slice(0, idx),
|
|
9
|
+
body: Buffer.from(text.slice(idx + 4).replace(/\r\n/g, ""), "base64").toString("utf8"),
|
|
10
|
+
};
|
|
26
11
|
}
|
|
27
12
|
describe("parseRecipients", () => {
|
|
28
13
|
it("splits, trims, and drops empties", () => {
|
|
@@ -37,76 +22,58 @@ describe("parseRecipients", () => {
|
|
|
37
22
|
});
|
|
38
23
|
});
|
|
39
24
|
describe("buildRawMessage", () => {
|
|
40
|
-
it("emits the core headers and a
|
|
25
|
+
it("emits the core headers and a single text/html body", () => {
|
|
41
26
|
const raw = buildRawMessage({
|
|
42
27
|
from: "me@x.com",
|
|
43
28
|
to: ["alice@x.com", "bob@x.com"],
|
|
44
29
|
subject: "Hello",
|
|
45
30
|
body: "Line one\nLine two",
|
|
46
31
|
});
|
|
47
|
-
const { header,
|
|
32
|
+
const { header, body } = decodeRaw(raw);
|
|
48
33
|
expect(header).toContain("From: me@x.com");
|
|
49
34
|
expect(header).toContain("To: alice@x.com, bob@x.com");
|
|
50
35
|
expect(header).toContain("Subject: Hello");
|
|
51
|
-
expect(header).toContain(
|
|
52
|
-
|
|
53
|
-
expect(
|
|
54
|
-
});
|
|
55
|
-
it("delimits both parts and terminates the multipart body", () => {
|
|
56
|
-
const raw = buildRawMessage({
|
|
57
|
-
from: "me@x.com",
|
|
58
|
-
to: ["a@x.com"],
|
|
59
|
-
subject: "s",
|
|
60
|
-
body: "hello",
|
|
61
|
-
});
|
|
62
|
-
const { boundary } = decodeRaw(raw);
|
|
63
|
-
const text = Buffer.from(raw, "base64url").toString("utf8");
|
|
64
|
-
expect(text.split(`--${boundary}\r\n`)).toHaveLength(3); // two part delimiters
|
|
65
|
-
expect(text.endsWith(`--${boundary}--`)).toBe(true);
|
|
66
|
-
});
|
|
67
|
-
it("uses a fresh boundary per message", () => {
|
|
68
|
-
const fields = { from: "me@x.com", to: ["a@x.com"], subject: "s", body: "b" };
|
|
69
|
-
expect(decodeRaw(buildRawMessage(fields)).boundary).not.toBe(decodeRaw(buildRawMessage(fields)).boundary);
|
|
36
|
+
expect(header).toContain('Content-Type: text/html; charset="UTF-8"');
|
|
37
|
+
expect(body).toMatch(/^<html><body>/);
|
|
38
|
+
expect(body).toMatch(/<\/body><\/html>$/);
|
|
70
39
|
});
|
|
71
|
-
it("
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
40
|
+
it("offers no text/plain alternative for the composer to adopt", () => {
|
|
41
|
+
// A plain part reopens the coin flip that delivered raw markdown to the
|
|
42
|
+
// reader — see specs/commands/gmail-draft.md.
|
|
43
|
+
const { header } = decodeRaw(buildRawMessage({ from: "me@x.com", to: ["a@x.com"], subject: "s", body: "b" }));
|
|
44
|
+
expect(header).not.toContain("multipart");
|
|
45
|
+
expect(header).not.toContain("text/plain");
|
|
46
|
+
expect(header).not.toContain("boundary");
|
|
75
47
|
});
|
|
76
|
-
it("renders markdown
|
|
77
|
-
const {
|
|
48
|
+
it("renders markdown to HTML", () => {
|
|
49
|
+
const { body } = decodeRaw(buildRawMessage({
|
|
78
50
|
from: "me@x.com",
|
|
79
51
|
to: ["a@x.com"],
|
|
80
52
|
subject: "s",
|
|
81
|
-
body: "Some **bold** text.\n\n- one\n- two",
|
|
53
|
+
body: "Some **bold** text and a [link](https://axi.md).\n\n- one\n- two",
|
|
82
54
|
}));
|
|
83
|
-
expect(
|
|
84
|
-
expect(
|
|
85
|
-
expect(
|
|
86
|
-
expect(html).toMatch(/<\/body><\/html>$/);
|
|
55
|
+
expect(body).toContain("<strong>bold</strong>");
|
|
56
|
+
expect(body).toContain('<a href="https://axi.md">link</a>');
|
|
57
|
+
expect(body).toContain("<li>one</li>");
|
|
87
58
|
});
|
|
88
59
|
it("maps a blank line to a new paragraph and a single newline to <br>", () => {
|
|
89
|
-
const {
|
|
60
|
+
const { body } = decodeRaw(buildRawMessage({
|
|
90
61
|
from: "me@x.com",
|
|
91
62
|
to: ["a@x.com"],
|
|
92
63
|
subject: "s",
|
|
93
64
|
body: "First para.\n\nSecond line one\nsecond line two",
|
|
94
65
|
}));
|
|
95
|
-
|
|
96
|
-
expect(
|
|
97
|
-
// ...and the intra-paragraph newline survives as a break, not a space.
|
|
98
|
-
expect(html).toContain("Second line one<br>second line two");
|
|
66
|
+
expect(body.match(/<p>/g)).toHaveLength(2);
|
|
67
|
+
expect(body).toContain("Second line one<br>second line two");
|
|
99
68
|
});
|
|
100
|
-
it("never breaks a long paragraph inside the decoded
|
|
69
|
+
it("never breaks a long paragraph inside the decoded body", () => {
|
|
101
70
|
// The 76-column wrapping is base64 transport only — it must not survive
|
|
102
71
|
// decoding, or the recipient sees the ragged columns this format exists
|
|
103
72
|
// to prevent.
|
|
104
|
-
const
|
|
105
|
-
const {
|
|
106
|
-
expect(
|
|
107
|
-
expect(
|
|
108
|
-
expect(html).not.toContain("\r\n");
|
|
109
|
-
expect(html).toContain(body);
|
|
73
|
+
const text = `${"word ".repeat(60).trim()}.`;
|
|
74
|
+
const { body } = decodeRaw(buildRawMessage({ from: "me@x.com", to: ["a@x.com"], subject: "s", body: text }));
|
|
75
|
+
expect(body).not.toContain("\r\n");
|
|
76
|
+
expect(body).toContain(text);
|
|
110
77
|
});
|
|
111
78
|
it("includes Cc/Bcc only when provided", () => {
|
|
112
79
|
const withCc = decodeRaw(buildRawMessage({
|
|
@@ -123,8 +90,8 @@ describe("buildRawMessage", () => {
|
|
|
123
90
|
expect(without).not.toContain("Cc:");
|
|
124
91
|
expect(without).not.toContain("Bcc:");
|
|
125
92
|
});
|
|
126
|
-
it("RFC 2047 encodes non-ASCII subjects but leaves
|
|
127
|
-
const { header,
|
|
93
|
+
it("RFC 2047 encodes non-ASCII subjects but leaves the body decodable", () => {
|
|
94
|
+
const { header, body } = decodeRaw(buildRawMessage({
|
|
128
95
|
from: "me@x.com",
|
|
129
96
|
to: ["a@x.com"],
|
|
130
97
|
subject: "café ☕",
|
|
@@ -132,8 +99,7 @@ describe("buildRawMessage", () => {
|
|
|
132
99
|
}));
|
|
133
100
|
expect(header).toContain("Subject: =?UTF-8?B?");
|
|
134
101
|
expect(header).not.toContain("café");
|
|
135
|
-
expect(
|
|
136
|
-
expect(html).toContain("naïve résumé — ☕");
|
|
102
|
+
expect(body).toContain("naïve résumé — ☕");
|
|
137
103
|
});
|
|
138
104
|
it("produces valid base64url (no +,/,= padding chars)", () => {
|
|
139
105
|
const raw = buildRawMessage({
|
|
@@ -143,7 +109,7 @@ describe("buildRawMessage", () => {
|
|
|
143
109
|
body: "x".repeat(200), // force base64 line wrapping
|
|
144
110
|
});
|
|
145
111
|
expect(raw).not.toMatch(/[+/=]/);
|
|
146
|
-
expect(decodeRaw(raw).
|
|
112
|
+
expect(decodeRaw(raw).body).toContain("x".repeat(200));
|
|
147
113
|
});
|
|
148
114
|
});
|
|
149
115
|
//# sourceMappingURL=compose.test.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"compose.test.js","sourceRoot":"","sources":["../../../../src/commands/gmail/compose.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAC;AAC9C,OAAO,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;
|
|
1
|
+
{"version":3,"file":"compose.test.js","sourceRoot":"","sources":["../../../../src/commands/gmail/compose.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export declare const DRAFT_HELP = "usage: gws-axi gmail draft --to <emails> --subject <text> --body <markdown> [flags]\nflags[8]:\n --to <emails> REQUIRED \u2014 comma-separated recipient addresses\n --subject <text> Subject line (default: empty)\n --body <text> Body text. Pass via quoted string or shell heredoc\n --body-file <path> Read the body from a file instead of --body (mutually\n exclusive with --body; use for long/multi-line bodies)\n --cc <emails> Comma-separated Cc addresses\n --bcc <emails> Comma-separated Bcc addresses\n --thread <thread-id> Attach the draft to an existing thread (reply draft)\n --account <email> REQUIRED when 2+ accounts are authenticated\nexamples:\n gws-axi gmail draft --to alice@x.com --subject \"Re: budget\" --body \"Looks good \u2014 approving.\"\n gws-axi gmail draft --to a@x.com,b@x.com --subject Hi --body-file ./note.txt\n gws-axi gmail draft --to alice@x.com --subject \"Re: thread\" --body \"...\" --thread 1899abcd\nnotes:\n Creates a DRAFT only \u2014 gws-axi never sends mail. Review and send from the\n Gmail UI. The body is markdown, rendered to
|
|
1
|
+
export declare const DRAFT_HELP = "usage: gws-axi gmail draft --to <emails> --subject <text> --body <markdown> [flags]\nflags[8]:\n --to <emails> REQUIRED \u2014 comma-separated recipient addresses\n --subject <text> Subject line (default: empty)\n --body <text> Body text. Pass via quoted string or shell heredoc\n --body-file <path> Read the body from a file instead of --body (mutually\n exclusive with --body; use for long/multi-line bodies)\n --cc <emails> Comma-separated Cc addresses\n --bcc <emails> Comma-separated Bcc addresses\n --thread <thread-id> Attach the draft to an existing thread (reply draft)\n --account <email> REQUIRED when 2+ accounts are authenticated\nexamples:\n gws-axi gmail draft --to alice@x.com --subject \"Re: budget\" --body \"Looks good \u2014 approving.\"\n gws-axi gmail draft --to a@x.com,b@x.com --subject Hi --body-file ./note.txt\n gws-axi gmail draft --to alice@x.com --subject \"Re: thread\" --body \"...\" --thread 1899abcd\nnotes:\n Creates a DRAFT only \u2014 gws-axi never sends mail. Review and send from the\n Gmail UI. The body is markdown, rendered to a single text/html part so\n recipients get reflowable, formatted text (Gmail adds the plain-text\n alternative itself on send).\n DO NOT hard-wrap the body. Write each paragraph as one long line \u2014 every\n newline you write becomes a line break in the sent message, so a body\n pre-wrapped at 72/80 columns arrives locked to that width on every screen.\n Blank line = new paragraph. GFM applies: **bold**, lists, tables, fenced\n code blocks (use those for text meant literally).\noutput:\n Returns `action: drafted` plus the new draft_id, message_id, recipients,\n and subject. A help line links to where to review/send it.\n";
|
|
2
2
|
export declare const SEND_HELP = "usage: gws-axi gmail send \u2014 INTENTIONALLY OUT OF SCOPE\nstatus: not supported by design\nnotes:\n gws-axi deliberately does not send mail. It can draft messages for you, but\n sending is left to a human in the Gmail UI so an automated agent can't email\n people on your behalf. Use `gws-axi gmail draft` to compose, then review and\n send the draft yourself in Gmail.\n";
|
|
3
3
|
export declare function gmailDraftCommand(account: string, args: string[]): Promise<string>;
|
|
@@ -20,9 +20,9 @@ examples:
|
|
|
20
20
|
gws-axi gmail draft --to alice@x.com --subject "Re: thread" --body "..." --thread 1899abcd
|
|
21
21
|
notes:
|
|
22
22
|
Creates a DRAFT only — gws-axi never sends mail. Review and send from the
|
|
23
|
-
Gmail UI. The body is markdown, rendered to
|
|
24
|
-
|
|
25
|
-
|
|
23
|
+
Gmail UI. The body is markdown, rendered to a single text/html part so
|
|
24
|
+
recipients get reflowable, formatted text (Gmail adds the plain-text
|
|
25
|
+
alternative itself on send).
|
|
26
26
|
DO NOT hard-wrap the body. Write each paragraph as one long line — every
|
|
27
27
|
newline you write becomes a line break in the sent message, so a body
|
|
28
28
|
pre-wrapped at 72/80 columns arrives locked to that width on every screen.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gws-axi",
|
|
3
|
-
"version": "0.20.
|
|
3
|
+
"version": "0.20.1",
|
|
4
4
|
"description": "AXI-compliant Google Workspace CLI — token-efficient TOON output, agent-guided auth setup, unified interface for Gmail, Calendar, Docs, Drive, Slides",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|