@agenticmail/api 0.9.22 → 0.9.23

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agenticmail/api",
3
- "version": "0.9.22",
3
+ "version": "0.9.23",
4
4
  "description": "REST API server for AgenticMail — email and SMS endpoints for AI agents",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -85,8 +85,23 @@ export function openReply(replyAll) {
85
85
  document.getElementById('compose-wake').value = '';
86
86
  document.getElementById('compose-subject').value =
87
87
  (msg.subject ?? '').startsWith('Re:') ? msg.subject : `Re: ${msg.subject ?? ''}`;
88
+ // Quote header — extend the standard "On <date>, <addr> wrote:"
89
+ // line with To: / Cc: lines pulled from the original message so
90
+ // the recipient of THIS reply can see who was on the previous
91
+ // round of the thread. The renderer in message-view.js parses
92
+ // these optional lines and surfaces them in the thread-quote
93
+ // header alongside the sender name.
94
+ const fmtAddrList = (arr) => (arr || [])
95
+ .map(a => (typeof a === 'string' ? a : (a?.address ?? '')))
96
+ .filter(Boolean)
97
+ .join(', ');
98
+ const origTo = fmtAddrList(msg.to);
99
+ const origCc = fmtAddrList(msg.cc);
100
+ const headerLines = [`On ${msg.date}, ${fromAddr} wrote:`];
101
+ if (origTo) headerLines.push(`To: ${origTo}`);
102
+ if (origCc) headerLines.push(`Cc: ${origCc}`);
88
103
  const quoted = (msg.text ?? '').split('\n').map(l => `> ${l}`).join('\n');
89
- const stub = `\n\nOn ${msg.date}, ${fromAddr} wrote:\n${quoted}`;
104
+ const stub = `\n\n${headerLines.join('\n')}\n${quoted}`;
90
105
  document.getElementById('compose-body').value = stub;
91
106
  pendingAttachments = [];
92
107
  renderAttachmentChips();
@@ -239,6 +239,14 @@ function renderBodyWithThreading(src) {
239
239
  // angle-bracket form `<addr@host>`. Date is anything up to the
240
240
  // comma; addr is anything not whitespace + an @.
241
241
  const headerRe = /^On (.+?), <?([^\s<>]+@[^\s<>]+)>? wrote:\s*$/;
242
+ // Optional follow-up address lines emitted by AgenticMail's
243
+ // reply builders: `To: a@x, b@y` / `Cc: …` / `Bcc: …`. These
244
+ // sit between the `wrote:` line and the first `> ` quoted body
245
+ // line; the parser collects them so the rendered thread-quote
246
+ // header can show the previous round's full audience, not just
247
+ // the sender. Optional — older replies (pre-0.9.32) won't have
248
+ // them and degrade to sender-only.
249
+ const audienceRe = /^(To|Cc|Bcc):\s*(.+)$/i;
242
250
 
243
251
  while (i < lines.length) {
244
252
  const m = lines[i].match(headerRe);
@@ -251,6 +259,23 @@ function renderBodyWithThreading(src) {
251
259
  const dateRaw = m[1];
252
260
  const sender = m[2];
253
261
  i++;
262
+ // Collect optional audience lines (To/Cc/Bcc) immediately
263
+ // after the wrote: header. Stop at the first line that
264
+ // doesn't match; non-matching content falls through to the
265
+ // body-quote collection loop below.
266
+ let toAddrs = '';
267
+ let ccAddrs = '';
268
+ let bccAddrs = '';
269
+ while (i < lines.length) {
270
+ const a = lines[i].match(audienceRe);
271
+ if (!a) break;
272
+ const field = a[1].toLowerCase();
273
+ const value = a[2].trim();
274
+ if (field === 'to') toAddrs = value;
275
+ else if (field === 'cc') ccAddrs = value;
276
+ else if (field === 'bcc') bccAddrs = value;
277
+ i++;
278
+ }
254
279
  // Collect contiguous `>` lines (with possible blank-line gaps
255
280
  // inside the quote, which most clients tolerate). Stop at the
256
281
  // first non-quote, non-blank line.
@@ -267,13 +292,13 @@ function renderBodyWithThreading(src) {
267
292
  }
268
293
  break;
269
294
  }
270
- out.push(renderThreadQuote(dateRaw, sender, quoted.join('\n')));
295
+ out.push(renderThreadQuote(dateRaw, sender, quoted.join('\n'), { to: toAddrs, cc: ccAddrs, bcc: bccAddrs }));
271
296
  }
272
297
  flushProse();
273
298
  return out.join('');
274
299
  }
275
300
 
276
- function renderThreadQuote(dateRaw, sender, quotedBody) {
301
+ function renderThreadQuote(dateRaw, sender, quotedBody, audience = {}) {
277
302
  // Try to format the ISO date through the same helper the
278
303
  // message header uses; fall back to the raw string on parse fail.
279
304
  const friendlyDate = (() => {
@@ -282,6 +307,17 @@ function renderThreadQuote(dateRaw, sender, quotedBody) {
282
307
  return dateRaw;
283
308
  })();
284
309
  const sub = renderBodyWithThreading(quotedBody); // recurse for nested threads
310
+ // Render the optional audience lines (To/Cc/Bcc) inside the
311
+ // thread-quote header so the reader can see who was on the
312
+ // previous round. Missing values are simply omitted — a quote
313
+ // from an older email that didn't include them degrades cleanly
314
+ // to the sender + date line.
315
+ const audienceRow = (label, value) => value
316
+ ? `<div class="thread-quote-audience-row"><span class="thread-quote-audience-label">${label}:</span> <span class="thread-quote-audience-value">${escapeHtml(value)}</span></div>`
317
+ : '';
318
+ const audienceBlock = (audience.to || audience.cc || audience.bcc)
319
+ ? `<div class="thread-quote-audience">${audienceRow('To', audience.to)}${audienceRow('Cc', audience.cc)}${audienceRow('Bcc', audience.bcc)}</div>`
320
+ : '';
285
321
  return `
286
322
  <div class="thread-quote">
287
323
  <div class="thread-quote-head">
@@ -290,6 +326,7 @@ function renderThreadQuote(dateRaw, sender, quotedBody) {
290
326
  <span class="thread-quote-dot">·</span>
291
327
  <span class="thread-quote-date">${escapeHtml(friendlyDate)}</span>
292
328
  </div>
329
+ ${audienceBlock}
293
330
  <div class="thread-quote-body">${sub}</div>
294
331
  </div>
295
332
  `;
package/public/styles.css CHANGED
@@ -938,6 +938,30 @@ mark.search-hl {
938
938
  .thread-quote-from { font-weight: 500; color: var(--ink-soft); }
939
939
  .thread-quote-dot { opacity: .5; }
940
940
  .thread-quote-date { color: var(--muted); }
941
+ /* Audience block (To / Cc / Bcc) below the quote header — surfaces
942
+ * who was on the previous round of the thread, not just the sender.
943
+ * Optional: missing values are simply omitted, no row rendered. */
944
+ .thread-quote-audience {
945
+ margin: -4px 0 8px;
946
+ padding-left: 30px; /* line up with sender name (past avatar) */
947
+ font-size: 12px;
948
+ color: var(--muted);
949
+ line-height: 1.5;
950
+ }
951
+ .thread-quote-audience-row { display: block; }
952
+ .thread-quote-audience-label {
953
+ display: inline-block;
954
+ min-width: 28px;
955
+ color: var(--muted);
956
+ font-weight: 500;
957
+ text-transform: uppercase;
958
+ letter-spacing: 0.04em;
959
+ font-size: 10.5px;
960
+ }
961
+ .thread-quote-audience-value {
962
+ color: var(--ink-soft);
963
+ word-break: break-word;
964
+ }
941
965
  .thread-quote-body {
942
966
  color: var(--ink-soft);
943
967
  }