@agenticmail/api 0.7.13 → 0.7.15

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/index.js CHANGED
@@ -2191,6 +2191,24 @@ function createMailRoutes(accountManager2, config, db, gatewayManager) {
2191
2191
  next(err);
2192
2192
  }
2193
2193
  });
2194
+ router.post("/mail/messages/:uid/star", requireAgent, async (req, res, next) => {
2195
+ try {
2196
+ const agent = req.agent;
2197
+ const uid = parseInt(req.params.uid);
2198
+ if (isNaN(uid) || uid < 1) {
2199
+ res.status(400).json({ error: "Invalid UID" });
2200
+ return;
2201
+ }
2202
+ const starred = req.body?.starred !== false;
2203
+ const folder = req.body?.folder || req.query.folder || "INBOX";
2204
+ const password = getAgentPassword(agent);
2205
+ const receiver = await getReceiver(agent.stalwartPrincipal, password, config);
2206
+ await receiver.setStarred(uid, starred, folder);
2207
+ res.json({ ok: true, starred });
2208
+ } catch (err) {
2209
+ next(err);
2210
+ }
2211
+ });
2194
2212
  router.post("/mail/messages/:uid/move", requireAgent, async (req, res, next) => {
2195
2213
  try {
2196
2214
  const agent = req.agent;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agenticmail/api",
3
- "version": "0.7.13",
3
+ "version": "0.7.15",
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",
package/public/index.html CHANGED
@@ -23,12 +23,14 @@
23
23
 
24
24
  <!-- ─── App shell ─────────────────────────────────────────────────── -->
25
25
  <div class="app" id="app" style="display:none">
26
- <!-- Top bar -->
26
+ <!-- Top bar — three-column grid: left chrome, centered search, right buttons. -->
27
27
  <header class="topbar">
28
- <button class="menu-btn" id="menu-btn" title="Menu" data-icon="menu"></button>
29
- <div class="brand">
30
- <img src="/branding/agenticmail-logo.png" alt="AgenticMail" class="brand-logo" />
31
- <span class="brand-name">AgenticMail</span>
28
+ <div class="topbar-left">
29
+ <button class="menu-btn" id="menu-btn" title="Menu" data-icon="menu"></button>
30
+ <div class="brand">
31
+ <img src="/branding/agenticmail-logo.png" alt="AgenticMail" class="brand-logo" />
32
+ <span class="brand-name">AgenticMail</span>
33
+ </div>
32
34
  </div>
33
35
  <div class="search-container">
34
36
  <span class="search-icon" data-icon="search"></span>
@@ -36,11 +38,12 @@
36
38
  <span id="search-hint" class="search-hint"></span>
37
39
  <button id="search-clear" class="search-clear-btn" title="Clear (Esc)" data-icon="close"></button>
38
40
  </div>
39
- <div class="topbar-spacer"></div>
40
- <button class="icon-btn" id="refresh-btn" title="Refresh (r)" data-icon="refresh"></button>
41
- <button id="profile-btn" class="profile-trigger" title="Account">
42
- <span id="profile-avatar"></span>
43
- </button>
41
+ <div class="topbar-right">
42
+ <button class="icon-btn" id="refresh-btn" title="Refresh (r)" data-icon="refresh"></button>
43
+ <button id="profile-btn" class="profile-trigger" title="Account">
44
+ <span id="profile-avatar"></span>
45
+ </button>
46
+ </div>
44
47
  <div id="profile-menu" class="profile-menu">
45
48
  <div class="profile-menu-section">Inboxes</div>
46
49
  <div id="profile-menu-list"></div>
@@ -4,7 +4,7 @@ import { state } from './state.js';
4
4
  import { escapeHtml, toast } from './utils.js';
5
5
  import { formatDate } from './time.js';
6
6
  import { parseSearch, matchesSearch, highlightTerm } from './search.js';
7
- import { apiGet } from './api.js';
7
+ import { apiGet, apiPost } from './api.js';
8
8
  import { FOLDERS } from './sidebar.js';
9
9
  import { icon } from './icons.js';
10
10
 
@@ -81,13 +81,22 @@ export async function ensureFolderCache(agent) {
81
81
 
82
82
  export async function loadList(agent, folder) {
83
83
  const root = document.getElementById('content');
84
+ // Gmail-style toolbar above the list: select-all checkbox,
85
+ // refresh, more-options spacer, count + pagination on the right.
86
+ // Identical layout for every folder so Sent / Drafts / Spam /
87
+ // Trash all share the same UX as Inbox.
84
88
  root.innerHTML = `
85
- <div class="list-header">
86
- <span class="folder-title">${escapeHtml(folderTitle(folder))}</span>
89
+ <div class="list-toolbar">
90
+ <label class="list-select-all" title="Select all">
91
+ <input type="checkbox" id="list-select-all-input" />
92
+ </label>
93
+ <button class="icon-btn list-refresh" title="Refresh" id="list-refresh-btn">${icon('refresh', { size: 18 })}</button>
94
+ <div class="list-toolbar-spacer"></div>
87
95
  <span class="count-text" id="list-count"></span>
88
96
  </div>
89
97
  <div class="list-rows" id="list-rows"><div class="empty">Loading…</div></div>
90
98
  `;
99
+ document.getElementById('list-refresh-btn')?.addEventListener('click', () => loadList(agent, folder));
91
100
  await ensureFolderCache(agent);
92
101
 
93
102
  // Resolve the real IMAP folder. Starred reuses INBOX + a client-
@@ -159,6 +168,12 @@ export function renderList() {
159
168
  return;
160
169
  }
161
170
 
171
+ // Gmail-style single-line row: checkbox · star · sender · subject
172
+ // — preview · date. Subject and preview sit on the same line
173
+ // separated by an em-dash; CSS truncates the joint cell with
174
+ // ellipsis so longer preview lines never wrap. Identical markup
175
+ // for every folder so Sent / Drafts / Spam etc render the same
176
+ // way Inbox does.
162
177
  root.innerHTML = filtered.map(m => {
163
178
  const unread = !flagsHas(m.flags, '\\Seen');
164
179
  const starred = flagsHas(m.flags, '\\Flagged');
@@ -166,23 +181,19 @@ export function renderList() {
166
181
  const fromName = m.from?.[0]?.name || fromAddr;
167
182
  const subject = m.subject ?? '(no subject)';
168
183
  const date = formatDate(m.date);
169
- const starIcon = icon(starred ? 'starFilled' : 'starOutline', { size: 18 });
170
- // Compact the preview body for the row: collapse whitespace,
171
- // strip quoted-reply chevrons, cap at a comfortable two-line
172
- // length. CSS handles the actual line clamp.
184
+ const starIcon = icon(starred ? 'starFilled' : 'starOutline', { size: 16 });
173
185
  const cleanPreview = (m.preview ?? '')
174
186
  .replace(/^>+ ?/gm, '')
175
187
  .replace(/\s+/g, ' ')
176
- .trim()
177
- .slice(0, 280);
188
+ .trim();
178
189
  return `
179
190
  <div class="list-row ${unread ? 'unread' : ''}" data-uid="${m.uid}">
180
- <span class="star ${starred ? 'starred' : ''}" data-action="star">${starIcon}</span>
181
- <span class="dot"></span>
182
- <span class="from">${highlightTerm(fromName, hlTerm)}</span>
191
+ <label class="row-check" data-action="select"><input type="checkbox" /></label>
192
+ <span class="star ${starred ? 'starred' : ''}" data-action="star" data-uid="${m.uid}">${starIcon}</span>
193
+ <span class="from" title="${escapeHtml(fromAddr)}">${highlightTerm(fromName, hlTerm)}</span>
183
194
  <span class="subject-cell">
184
195
  <span class="subject">${highlightTerm(subject, hlTerm)}</span>
185
- <span class="preview">${highlightTerm(cleanPreview, hlTerm)}</span>
196
+ ${cleanPreview ? `<span class="preview-sep"> — </span><span class="preview">${highlightTerm(cleanPreview, hlTerm)}</span>` : ''}
186
197
  </span>
187
198
  <span class="date">${escapeHtml(date)}</span>
188
199
  </div>
@@ -191,9 +202,17 @@ export function renderList() {
191
202
 
192
203
  root.querySelectorAll('.list-row').forEach(el => {
193
204
  el.addEventListener('click', (e) => {
194
- if (e.target.closest('[data-action="star"]')) {
205
+ // Star click — toggle via API and optimistically update the
206
+ // local flags so the icon flips without a reload.
207
+ const starEl = e.target.closest('[data-action="star"]');
208
+ if (starEl) {
209
+ e.stopPropagation();
210
+ toggleStar(Number(el.dataset.uid), starEl);
211
+ return;
212
+ }
213
+ // Checkbox click — swallow so we don't navigate.
214
+ if (e.target.closest('[data-action="select"]')) {
195
215
  e.stopPropagation();
196
- toast('Starring not wired through API yet.');
197
216
  return;
198
217
  }
199
218
  const uid = Number(el.dataset.uid);
@@ -202,6 +221,49 @@ export function renderList() {
202
221
  });
203
222
  }
204
223
 
224
+ /**
225
+ * Toggle the IMAP \Flagged flag on a message via the API. Updates
226
+ * the in-memory message object on success so renderList reflects
227
+ * the new state without a full reload — and reverts on failure so
228
+ * the icon doesn't drift from server truth.
229
+ */
230
+ async function toggleStar(uid, starEl) {
231
+ const agent = state.selectedAgent;
232
+ if (!agent) return;
233
+ const msg = state.messages.find(m => m.uid === uid);
234
+ if (!msg) return;
235
+ const wasStarred = flagsHas(msg.flags, '\\Flagged');
236
+ const nextStarred = !wasStarred;
237
+
238
+ // Optimistic UI flip.
239
+ starEl.classList.toggle('starred', nextStarred);
240
+ starEl.innerHTML = icon(nextStarred ? 'starFilled' : 'starOutline', { size: 16 });
241
+
242
+ // Local flags mutation so a re-render keeps the new state.
243
+ const imap = state.folderNames?.[state.selectedFolder] ?? 'INBOX';
244
+ if (Array.isArray(msg.flags)) {
245
+ msg.flags = nextStarred
246
+ ? Array.from(new Set([...msg.flags, '\\Flagged']))
247
+ : msg.flags.filter(f => f !== '\\Flagged');
248
+ } else {
249
+ msg.flags = nextStarred ? ['\\Flagged'] : [];
250
+ }
251
+
252
+ try {
253
+ await apiPost(`/mail/messages/${uid}/star`, { starred: nextStarred, folder: imap }, { agentKey: agent.apiKey });
254
+ } catch (err) {
255
+ // Revert on failure.
256
+ starEl.classList.toggle('starred', wasStarred);
257
+ starEl.innerHTML = icon(wasStarred ? 'starFilled' : 'starOutline', { size: 16 });
258
+ if (Array.isArray(msg.flags)) {
259
+ msg.flags = wasStarred
260
+ ? Array.from(new Set([...msg.flags, '\\Flagged']))
261
+ : msg.flags.filter(f => f !== '\\Flagged');
262
+ }
263
+ toast(`Star failed: ${err.message}`, true);
264
+ }
265
+ }
266
+
205
267
  export function clearSearch() {
206
268
  const input = document.getElementById('search-input');
207
269
  if (input) {
package/public/styles.css CHANGED
@@ -65,12 +65,22 @@ a { color: var(--accent-strong); }
65
65
  }
66
66
 
67
67
  /* ─── Top bar ──────────────────────────────────────────────────────── */
68
+ /* Three-column grid: [menu+brand] · [centered search] · [right buttons].
69
+ A flex topbar with `flex: 1` on both the search and a trailing
70
+ spacer split the remaining space evenly so the search drifted
71
+ left of the visual middle. The grid centers the search column
72
+ in the actual page width regardless of how wide the side groups
73
+ get. */
68
74
  .topbar {
69
- display: flex; align-items: center; gap: 8px;
75
+ display: grid;
76
+ grid-template-columns: 1fr auto 1fr;
77
+ align-items: center; gap: 8px;
70
78
  padding: 8px 16px;
71
79
  background: var(--bg-soft);
72
80
  position: relative; z-index: 10;
73
81
  }
82
+ .topbar-left { display: flex; align-items: center; gap: 8px; justify-self: start; }
83
+ .topbar-right { display: flex; align-items: center; gap: 4px; justify-self: end; }
74
84
  .menu-btn {
75
85
  width: 40px; height: 40px; border-radius: 50%;
76
86
  display: flex; align-items: center; justify-content: center;
@@ -101,7 +111,12 @@ a { color: var(--accent-strong); }
101
111
  }
102
112
 
103
113
  .search-container {
104
- flex: 1; max-width: 720px;
114
+ /* Lives in the centered grid column of `.topbar`. Width caps at
115
+ 720px (Gmail's actual cap), but stretches to fill the column
116
+ when the viewport is narrower. */
117
+ width: 720px;
118
+ max-width: 100%;
119
+ min-width: 280px;
105
120
  position: relative;
106
121
  }
107
122
  .search-input {
@@ -138,7 +153,6 @@ a { color: var(--accent-strong); }
138
153
  }
139
154
  .search-hint.show { display: block; }
140
155
 
141
- .topbar-spacer { flex: 1; }
142
156
  .icon-btn {
143
157
  width: 40px; height: 40px; border-radius: 50%;
144
158
  display: flex; align-items: center; justify-content: center;
@@ -267,27 +281,43 @@ a { color: var(--accent-strong); }
267
281
  .main.sidebar-open .sidebar { transform: translateX(0); }
268
282
  .main.sidebar-open .sidebar-backdrop { display: block; }
269
283
  .content { border-radius: 0; margin: 0; }
270
- .topbar { padding: 8px 8px; gap: 4px; }
284
+ /* On mobile, drop the centered grid in favour of [chrome | search | account].
285
+ The search expands to fill whatever space is left. */
286
+ .topbar {
287
+ padding: 8px 8px; gap: 4px;
288
+ grid-template-columns: auto 1fr auto;
289
+ }
271
290
  .brand { min-width: auto; }
272
- .brand-name { font-size: 18px; }
273
- .search-container { max-width: none; }
291
+ .brand-name { font-size: 18px; display: none; }
292
+ .search-container { width: auto; min-width: 0; }
274
293
  .search-input { height: 40px; font-size: 14px; }
275
- /* List rows lose the from column on narrow screens; the subject
276
- gets full width with the sender folded into the preview. */
294
+ /* On narrow screens, drop the checkbox + From column. Sender goes
295
+ in a small line above subject+preview to mimic Gmail's mobile
296
+ two-row stack. */
277
297
  .list-row {
278
- grid-template-columns: 24px 24px 1fr 70px;
279
- height: 56px;
280
- padding: 0 12px;
298
+ grid-template-columns: 32px 1fr 64px;
299
+ height: auto;
300
+ min-height: 64px;
301
+ padding: 8px 12px;
302
+ align-items: center;
303
+ }
304
+ .list-row .row-check { display: none; }
305
+ .list-row .from {
306
+ grid-column: 2 / 3;
307
+ font-size: 13px;
308
+ padding-right: 0;
281
309
  }
282
- .list-row .from { display: none; }
283
310
  .list-row .subject-cell {
284
- flex-direction: column;
285
- gap: 2px;
286
- align-items: flex-start;
311
+ grid-column: 2 / 3;
312
+ grid-row: 2;
313
+ white-space: normal;
314
+ display: -webkit-box;
315
+ -webkit-line-clamp: 2;
316
+ -webkit-box-orient: vertical;
317
+ overflow: hidden;
287
318
  }
288
- .list-row .subject { max-width: none; font-size: 14px; }
289
- .list-row .preview { font-size: 13px; }
290
- .list-row .preview::before { content: ''; }
319
+ .list-row .star { grid-row: 1 / span 2; }
320
+ .list-row .date { grid-row: 1; }
291
321
  .message-header { padding: 16px 16px 8px; }
292
322
  .message-subject { font-size: 18px; }
293
323
  .message-body { padding: 8px 16px 24px; max-width: none; }
@@ -298,7 +328,6 @@ a { color: var(--accent-strong); }
298
328
  .compose-modal { width: 100%; max-height: 100vh; border-radius: 0; }
299
329
  .compose-body textarea { min-height: 40vh; }
300
330
  /* Hide non-essential top-bar buttons on narrow screens. */
301
- .topbar-spacer { flex: 0; }
302
331
  #refresh-btn { display: none; }
303
332
  }
304
333
  @media (min-width: 801px) {
@@ -312,8 +341,11 @@ a { color: var(--accent-strong); }
312
341
  overflow-y: auto;
313
342
  }
314
343
  .compose-btn {
344
+ /* Gmail's Compose button is 48px tall with 16px corner radius —
345
+ prominent but not pill-shaped. Earlier this was 56px + ~28px
346
+ radius which read as a giant capsule and dominated the sidebar. */
315
347
  display: inline-flex; align-items: center; gap: 12px;
316
- height: 56px; padding: 0 24px 0 16px;
348
+ height: 48px; padding: 0 24px 0 16px;
317
349
  margin-bottom: 16px;
318
350
  background: var(--pink); color: white;
319
351
  border-radius: 16px;
@@ -364,46 +396,49 @@ a { color: var(--accent-strong); }
364
396
  display: flex; flex-direction: column;
365
397
  }
366
398
 
367
- /* List header (above the list itself) */
368
- .list-header {
369
- display: flex; align-items: center; gap: 8px;
370
- padding: 8px 16px; height: 48px;
371
- border-bottom: 1px solid transparent;
399
+ /* List toolbar (sticky, matches Gmail's row of buttons above the list).
400
+ Same markup for every folder so Sent / Drafts / Spam render
401
+ identically to Inbox — no per-folder UX divergence. */
402
+ .list-toolbar {
403
+ display: flex; align-items: center; gap: 4px;
404
+ padding: 4px 16px; height: 48px;
405
+ border-bottom: 1px solid var(--line);
372
406
  position: sticky; top: 0; z-index: 5;
373
407
  background: var(--bg);
374
408
  flex-shrink: 0;
375
409
  }
376
- .list-header .icon-btn { width: 36px; height: 36px; font-size: 16px; }
377
- .list-header .count-text {
378
- margin-left: auto;
379
- font-size: 12px; color: var(--muted);
410
+ .list-select-all {
411
+ width: 40px; height: 40px;
412
+ display: flex; align-items: center; justify-content: center;
413
+ cursor: pointer; border-radius: 50%;
380
414
  }
381
- .list-header .folder-title {
382
- font: 500 18px/1 'Google Sans', sans-serif;
383
- color: var(--ink); margin-left: 8px;
415
+ .list-select-all:hover { background: var(--bg-hover); }
416
+ .list-toolbar .list-refresh { width: 40px; height: 40px; }
417
+ .list-toolbar-spacer { flex: 1; }
418
+ .list-toolbar .count-text {
419
+ font-size: 12px; color: var(--muted);
384
420
  }
385
421
 
386
- /* List rows */
422
+ /* Gmail-style compact rows.
423
+ Single line per message; subject + preview share one truncated
424
+ cell so longer previews tail off with ellipsis instead of
425
+ wrapping. Same row shape for every folder. */
387
426
  .list-rows {
388
427
  flex: 1; overflow-y: auto;
389
428
  }
390
429
  .list-row {
391
430
  display: grid;
392
- grid-template-columns: 24px 24px 200px 1fr 100px;
393
- /* Top-align so a two-line preview can grow downward without
394
- pushing the star + date out of alignment with the subject. */
395
- align-items: flex-start; gap: 0;
396
- padding: 10px 16px; min-height: 64px;
431
+ grid-template-columns: 36px 32px 180px 1fr 90px;
432
+ align-items: center; gap: 0;
433
+ padding: 0 16px; height: 36px;
397
434
  cursor: pointer;
398
- border-bottom: 1px solid var(--bg-soft);
435
+ border-bottom: 1px solid var(--line);
399
436
  position: relative;
400
437
  }
401
- @media (max-width: 1000px) { .list-row { grid-template-columns: 24px 24px 160px 1fr 80px; } }
402
- /* Pull star + dot + date back into the visual midline of the subject. */
403
- .list-row .star, .list-row .dot, .list-row .date { padding-top: 2px; }
438
+ @media (max-width: 1100px) { .list-row { grid-template-columns: 36px 32px 140px 1fr 80px; } }
404
439
  .list-row:hover {
405
- background: var(--bg-row-hover);
406
- box-shadow: inset 0 0 0 1px rgba(0,0,0,.05);
440
+ background: var(--bg);
441
+ box-shadow: inset 1px 0 0 var(--line), inset -1px 0 0 var(--line), 0 1px 3px rgba(0,0,0,.08);
407
442
  z-index: 1;
408
443
  }
409
444
  .list-row.unread { background: var(--bg); }
@@ -413,52 +448,39 @@ a { color: var(--accent-strong); }
413
448
  .list-row:not(.unread) .from, .list-row:not(.unread) .subject {
414
449
  color: var(--read-text);
415
450
  }
451
+ .list-row .row-check {
452
+ width: 36px; height: 36px;
453
+ display: flex; align-items: center; justify-content: center;
454
+ cursor: pointer;
455
+ }
456
+ .list-row .row-check input { cursor: pointer; }
416
457
  .list-row .star {
417
- font-size: 16px; color: #dadce0; cursor: pointer;
418
- width: 24px; height: 24px;
458
+ width: 32px; height: 32px;
419
459
  display: flex; align-items: center; justify-content: center;
420
- border-radius: 50%;
460
+ border-radius: 50%; cursor: pointer;
461
+ color: #dadce0;
421
462
  }
422
- .list-row .star:hover { background: var(--bg-hover); }
463
+ .list-row .star:hover { background: var(--bg-hover); color: var(--muted); }
423
464
  .list-row .star.starred { color: #f4b400; }
424
465
  .list-row .from {
425
466
  font-size: 14px;
426
467
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
427
- padding-right: 8px;
468
+ padding-right: 12px;
428
469
  }
429
470
  .list-row .subject-cell {
430
- /* Stacked: subject on top, two-line preview underneath. */
431
- display: flex; flex-direction: column; gap: 2px;
432
- overflow: hidden;
433
- min-width: 0;
434
- }
435
- .list-row .subject {
436
471
  font-size: 14px;
437
472
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
473
+ min-width: 0;
474
+ display: block; /* one line, ellipsis at the end */
438
475
  }
439
- .list-row .preview {
440
- font-size: 13px; color: var(--muted); line-height: 1.35;
441
- /* 2-line clamp via -webkit-box (works in every shipping browser
442
- including Firefox/Safari). The break-word stops one long URL
443
- from blowing out the layout. */
444
- display: -webkit-box;
445
- -webkit-line-clamp: 2;
446
- -webkit-box-orient: vertical;
447
- overflow: hidden;
448
- word-break: break-word;
449
- }
476
+ .list-row .subject { font-weight: inherit; }
477
+ .list-row .preview-sep { color: var(--muted); }
478
+ .list-row .preview { color: var(--muted); font-weight: 400; }
450
479
  .list-row .date {
451
480
  font-size: 12px; color: var(--muted); font-weight: 500;
452
481
  text-align: right; padding-right: 4px;
453
482
  }
454
483
  .list-row.unread .date { color: var(--unread-bold); font-weight: 700; }
455
- .list-row .dot {
456
- width: 8px; height: 8px; border-radius: 50%;
457
- background: var(--pink);
458
- display: none;
459
- margin: 0 auto;
460
- }
461
- .list-row.unread .dot { display: block; }
462
484
 
463
485
  mark.search-hl {
464
486
  background: #fff475; color: inherit;
@@ -486,12 +508,22 @@ mark.search-hl {
486
508
  background: var(--bg);
487
509
  position: sticky; top: 0; z-index: 5;
488
510
  flex-shrink: 0;
511
+ /* Toolbar spans full width (so the back button sticks to the
512
+ left edge of the content pane) but its action group could be
513
+ centered later if needed. Keeping it full-width matches Gmail. */
489
514
  }
490
515
  .message-toolbar .icon-btn { width: 36px; height: 36px; font-size: 16px; }
491
516
  .message-toolbar .toolbar-spacer { flex: 1; }
492
517
 
518
+ /* Centered reading column for the open message — Gmail caps body
519
+ content around ~840px so long-line text doesn't sprawl across
520
+ ultrawide displays. Header, body, and attachments all share
521
+ the same centering rule so they line up vertically. */
493
522
  .message-header {
494
523
  padding: 32px 32px 16px;
524
+ max-width: 840px;
525
+ margin: 0 auto;
526
+ width: 100%;
495
527
  }
496
528
  .message-subject {
497
529
  font: 400 22px/1.3 'Google Sans', sans-serif;
@@ -520,7 +552,9 @@ mark.search-hl {
520
552
  font-size: 14px; line-height: 1.65;
521
553
  color: var(--ink);
522
554
  white-space: pre-wrap; word-wrap: break-word;
523
- max-width: 800px;
555
+ max-width: 840px;
556
+ margin: 0 auto;
557
+ width: 100%;
524
558
  }
525
559
  .message-body h1, .message-body h2, .message-body h3 {
526
560
  color: var(--pink); margin: 1.2em 0 .4em;
@@ -558,6 +592,9 @@ mark.search-hl {
558
592
  .message-attachments {
559
593
  padding: 12px 32px; border-top: 1px solid var(--line);
560
594
  display: flex; flex-wrap: wrap; gap: 8px;
595
+ max-width: 840px;
596
+ margin: 0 auto;
597
+ width: 100%;
561
598
  }
562
599
  .message-attachment {
563
600
  display: inline-flex; align-items: center; gap: 8px;