@aglyn/shared-util-email 1.0.0-beta.203 → 1.0.0-beta.204

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.
Files changed (44) hide show
  1. package/package.json +3 -3
  2. package/src/index.d.ts +3 -0
  3. package/src/index.js +9 -0
  4. package/src/index.js.map +1 -1
  5. package/src/lib/email-design-chrome.d.ts +61 -0
  6. package/src/lib/email-design-chrome.js +99 -0
  7. package/src/lib/email-design-chrome.js.map +1 -0
  8. package/src/lib/email-linkify.d.ts +3 -1
  9. package/src/lib/email-linkify.js +2 -2
  10. package/src/lib/email-linkify.js.map +1 -1
  11. package/src/lib/email-palette.d.ts +137 -0
  12. package/src/lib/email-palette.js +756 -0
  13. package/src/lib/email-palette.js.map +1 -0
  14. package/src/lib/email-render.d.ts +25 -0
  15. package/src/lib/email-render.js +27 -9
  16. package/src/lib/email-render.js.map +1 -1
  17. package/src/lib/held-page.d.ts +117 -0
  18. package/src/lib/held-page.js +172 -0
  19. package/src/lib/held-page.js.map +1 -0
  20. package/src/lib/host-email-render.d.ts +34 -7
  21. package/src/lib/host-email-render.js +69 -25
  22. package/src/lib/host-email-render.js.map +1 -1
  23. package/src/lib/hosted-page-screen.js +8 -6
  24. package/src/lib/hosted-page-screen.js.map +1 -1
  25. package/src/lib/outbound-phishing-screen.d.ts +24 -0
  26. package/src/lib/outbound-phishing-screen.js +81 -2
  27. package/src/lib/outbound-phishing-screen.js.map +1 -1
  28. package/src/lib/risk-notice-catalog.d.ts +204 -0
  29. package/src/lib/risk-notice-catalog.js +1702 -0
  30. package/src/lib/risk-notice-catalog.js.map +1 -0
  31. package/src/lib/risk-notice-emails.d.ts +25 -0
  32. package/src/lib/risk-notice-emails.js +182 -0
  33. package/src/lib/risk-notice-emails.js.map +1 -0
  34. package/src/lib/sender-readiness.d.ts +155 -0
  35. package/src/lib/sender-readiness.js +432 -0
  36. package/src/lib/sender-readiness.js.map +1 -0
  37. package/src/lib/system-email-catalog.js +7 -1
  38. package/src/lib/system-email-catalog.js.map +1 -1
  39. package/src/lib/tenant-email-catalog.d.ts +7 -0
  40. package/src/lib/tenant-email-catalog.js +183 -13
  41. package/src/lib/tenant-email-catalog.js.map +1 -1
  42. package/src/lib/text-email-html.d.ts +26 -0
  43. package/src/lib/text-email-html.js +56 -1
  44. package/src/lib/text-email-html.js.map +1 -1
@@ -74,7 +74,8 @@
74
74
  text: 'Reference: {{booking.ref}}',
75
75
  variant: 'caption'
76
76
  }
77
- ]
77
+ ],
78
+ footerReason: 'You’re receiving this because you booked with {{host.businessName}}.'
78
79
  },
79
80
  {
80
81
  key: 'booking-reminder',
@@ -107,7 +108,8 @@
107
108
  text: 'Hi {{name}}, this is a reminder that "{{service.name}}" is ' + 'coming up on {{when}}.',
108
109
  variant: 'body'
109
110
  }
110
- ]
111
+ ],
112
+ footerReason: 'You’re receiving this because you have a booking with {{host.businessName}}.'
111
113
  },
112
114
  // ── Commerce (fixed for now; flips to `besigner` when the send site is
113
115
  // wired to renderHostEmail — AGL-770) ────────────────────────────────
@@ -157,7 +159,8 @@
157
159
  text: 'Order reference: {{order.ref}}',
158
160
  variant: 'caption'
159
161
  }
160
- ]
162
+ ],
163
+ footerReason: 'You’re receiving this because you placed an order with {{host.businessName}}.'
161
164
  },
162
165
  {
163
166
  key: 'sale-notification',
@@ -220,7 +223,8 @@
220
223
  text: 'Buyer: {{buyer.email}} · Order {{order.ref}}',
221
224
  variant: 'caption'
222
225
  }
223
- ]
226
+ ],
227
+ footerReason: 'You’re receiving this because you manage {{host.businessName}}.'
224
228
  },
225
229
  {
226
230
  key: 'reservation-confirmed',
@@ -283,7 +287,8 @@
283
287
  text: 'Reference: {{reservation.ref}}',
284
288
  variant: 'caption'
285
289
  }
286
- ]
290
+ ],
291
+ footerReason: 'You’re receiving this because you made a reservation with {{host.businessName}}.'
287
292
  },
288
293
  {
289
294
  key: 'gift-card',
@@ -326,7 +331,8 @@
326
331
  text: 'Enter it at checkout to apply the balance.',
327
332
  variant: 'body'
328
333
  }
329
- ]
334
+ ],
335
+ footerReason: 'You’re receiving this because a gift card from {{host.businessName}} was sent to this address.'
330
336
  },
331
337
  {
332
338
  key: 'supplier-fulfillment',
@@ -367,7 +373,8 @@
367
373
  label: 'View product',
368
374
  href: '{{product.url}}'
369
375
  }
370
- ]
376
+ ],
377
+ footerReason: 'You’re receiving this because you asked {{host.businessName}} to tell you when this was back.'
371
378
  },
372
379
  {
373
380
  key: 'abandoned-cart',
@@ -403,7 +410,8 @@
403
410
  text: 'Your items are held but not reserved, so they may sell out.',
404
411
  variant: 'caption'
405
412
  }
406
- ]
413
+ ],
414
+ footerReason: 'You’re receiving this because you started a checkout with {{host.businessName}}.'
407
415
  },
408
416
  {
409
417
  key: 'member-post',
@@ -411,15 +419,175 @@
411
419
  description: 'Notifies members when new members-only content is posted.',
412
420
  pluginId: 'commerce',
413
421
  plugin: 'Commerce',
414
- control: 'fixed'
422
+ control: 'besigner',
423
+ defaultSubject: '{{post.title}}',
424
+ mergeTokens: [
425
+ {
426
+ name: 'post.title',
427
+ description: 'The post’s title',
428
+ sample: 'This month in the studio'
429
+ },
430
+ {
431
+ name: 'post.body',
432
+ description: 'The post, as its author wrote it',
433
+ sample: 'Three new pieces are up for members first.'
434
+ }
435
+ ],
436
+ defaultBody: [
437
+ {
438
+ block: 'text',
439
+ text: '{{post.title}}',
440
+ variant: 'heading'
441
+ },
442
+ {
443
+ block: 'text',
444
+ text: '{{post.body}}',
445
+ variant: 'body'
446
+ }
447
+ ],
448
+ footerReason: 'You’re receiving this because you’re a member of {{host.businessName}}.'
415
449
  },
416
450
  {
417
451
  key: 'member-password-reset',
418
452
  name: 'Member password reset',
419
- description: "Resets a site member's password — a store-member account, separate " + 'from a console login.',
453
+ description: "Resets a site member's password — a store-member account, separate " + 'from a console login. Sent when the member asks, and when a site ' + 'administrator starts a reset for them.',
420
454
  pluginId: 'commerce',
421
455
  plugin: 'Commerce',
422
- control: 'fixed'
456
+ control: 'besigner',
457
+ defaultSubject: 'Reset your {{site.name}} password',
458
+ mergeTokens: [
459
+ {
460
+ name: 'site.name',
461
+ description: 'The site’s name',
462
+ sample: 'Northwind Coffee'
463
+ },
464
+ {
465
+ name: 'reset.intro',
466
+ description: 'Who started the reset',
467
+ sample: 'Someone asked to reset the password for your Northwind Coffee account.'
468
+ },
469
+ {
470
+ name: 'resetUrl',
471
+ description: 'The one-time reset link',
472
+ sample: 'https://shop.example.com/recover?token=…'
473
+ },
474
+ {
475
+ name: 'reset.note',
476
+ description: 'How long the link lasts, and what to do if unexpected',
477
+ sample: 'The link works once and expires in 1 hour. If you did not ask for ' + 'this, you can safely ignore this email — your password is unchanged.'
478
+ }
479
+ ],
480
+ defaultBody: [
481
+ {
482
+ block: 'text',
483
+ text: 'Reset your password',
484
+ variant: 'heading'
485
+ },
486
+ {
487
+ block: 'text',
488
+ text: '{{reset.intro}}',
489
+ variant: 'body'
490
+ },
491
+ {
492
+ block: 'button',
493
+ label: 'Set a new password',
494
+ href: '{{resetUrl}}'
495
+ },
496
+ {
497
+ block: 'text',
498
+ text: '{{reset.note}}',
499
+ variant: 'caption'
500
+ }
501
+ ],
502
+ footerReason: 'You’re receiving this because a password reset was started for your ' + '{{host.businessName}} account.'
503
+ },
504
+ {
505
+ key: 'member-password-changed',
506
+ name: 'Member password changed',
507
+ description: 'Tells a site member that a site administrator set a new password on ' + 'their account.',
508
+ pluginId: 'commerce',
509
+ plugin: 'Commerce',
510
+ control: 'besigner',
511
+ defaultSubject: 'Your {{site.name}} password was changed',
512
+ mergeTokens: [
513
+ {
514
+ name: 'site.name',
515
+ description: 'The site’s name',
516
+ sample: 'Northwind Coffee'
517
+ },
518
+ {
519
+ name: 'signInUrl',
520
+ description: 'Where the member signs back in',
521
+ sample: 'https://shop.example.com'
522
+ }
523
+ ],
524
+ defaultBody: [
525
+ {
526
+ block: 'text',
527
+ text: 'Your password was changed',
528
+ variant: 'heading'
529
+ },
530
+ {
531
+ block: 'text',
532
+ text: 'An administrator of {{site.name}} set a new password on your ' + 'account. You have been signed out on every device and will need ' + 'the new password to sign back in.',
533
+ variant: 'body'
534
+ },
535
+ {
536
+ block: 'button',
537
+ label: 'Sign in',
538
+ href: '{{signInUrl}}'
539
+ },
540
+ {
541
+ block: 'text',
542
+ text: 'If you did not expect this, contact the site owner.',
543
+ variant: 'caption'
544
+ }
545
+ ],
546
+ footerReason: 'You’re receiving this because the password on your ' + '{{host.businessName}} account was changed.'
547
+ },
548
+ {
549
+ key: 'newsletter-confirmation',
550
+ name: 'Subscription confirmation',
551
+ description: 'Asks someone who signed up for a site’s emails to confirm the address ' + 'before anything is sent to it.',
552
+ pluginId: 'commerce',
553
+ plugin: 'Commerce',
554
+ control: 'besigner',
555
+ defaultSubject: 'Confirm your subscription',
556
+ mergeTokens: [
557
+ {
558
+ name: 'stream.name',
559
+ description: 'What they signed up for',
560
+ sample: 'our newsletter'
561
+ },
562
+ {
563
+ name: 'confirmUrl',
564
+ description: 'The confirmation link',
565
+ sample: 'https://shop.example.com/subscribe/confirm?token=…'
566
+ }
567
+ ],
568
+ defaultBody: [
569
+ {
570
+ block: 'text',
571
+ text: 'Confirm your subscription',
572
+ variant: 'heading'
573
+ },
574
+ {
575
+ block: 'text',
576
+ text: 'Please confirm that you want to receive {{stream.name}} at this address.',
577
+ variant: 'body'
578
+ },
579
+ {
580
+ block: 'button',
581
+ label: 'Confirm my subscription',
582
+ href: '{{confirmUrl}}'
583
+ },
584
+ {
585
+ block: 'text',
586
+ text: 'The link works for three days. If you did not sign up, ignore this ' + 'message — nothing will be sent.',
587
+ variant: 'caption'
588
+ }
589
+ ],
590
+ footerReason: 'You’re receiving this because this address was entered on ' + '{{host.businessName}}’s signup form.'
423
591
  },
424
592
  // ── Marketing / Workflows (authored in their own UIs) ──────────────────
425
593
  {
@@ -429,7 +597,8 @@
429
597
  pluginId: 'marketing',
430
598
  plugin: 'Marketing',
431
599
  control: 'external',
432
- authoredIn: 'Marketing → Campaigns'
600
+ authoredIn: 'Marketing → Campaigns',
601
+ footerReason: 'You’re receiving this email from {{host.businessName}}.'
433
602
  },
434
603
  {
435
604
  key: 'workflow-email',
@@ -438,7 +607,8 @@
438
607
  pluginId: 'workflows',
439
608
  plugin: 'Workflows',
440
609
  control: 'external',
441
- authoredIn: 'the workflow that sends it'
610
+ authoredIn: 'the workflow that sends it',
611
+ footerReason: 'You’re receiving this because you’re in touch with {{host.businessName}}.'
442
612
  }
443
613
  ];
444
614
  /** Firestore subcollection under a host holding its designed templates. */ export const TENANT_EMAIL_COLLECTION = 'emailTemplates';
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/shared/util/email/src/lib/tenant-email-catalog.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type {\n SystemEmailDefaultBlock,\n SystemEmailMergeToken,\n} from './system-email-catalog'\n\n/**\n * How a site owner controls a tenant email's copy (AGL-769/770):\n *\n * `besigner` — designable here, per site, in the email besigner. The send\n * site resolves the designed template and falls back to built-in copy.\n * `external` — already authored in another part of the console (a marketing\n * campaign, a workflow), so the reference links there instead of offering a\n * second editor.\n * `fixed` — fixed copy inside the plugin, not customizable yet. Listed for\n * visibility; becomes `besigner` once its send site is wired.\n */\nexport type TenantEmailControl = 'besigner' | 'external' | 'fixed'\n\nexport interface TenantEmailEntry {\n /** Stable key — the `hosts/{hostId}/emailTemplates` document id. */\n key: string\n name: string\n description: string\n /** Plugin that owns the send (matches org.enabledPlugins). */\n pluginId: string\n plugin: string\n control: TenantEmailControl\n /** `external`: where the copy is authored, shown as the link/label. */\n authoredIn?: string\n /** `besigner`: subject used when no template is published. */\n defaultSubject?: string\n /** `besigner`: tokens the send site supplies. */\n mergeTokens?: readonly SystemEmailMergeToken[]\n /** `besigner`: starting content the editor seeds and the default renders. */\n defaultBody?: readonly SystemEmailDefaultBlock[]\n /**\n * Entitlement flag the SEND is gated on, when one is (AGL-2081).\n *\n * `enabledPlugins` decides whether a template appears at all; this is the\n * narrower question of whether the send behind an appearing template can\n * actually happen. `abandoned-cart` is the case that named it: the template\n * has always been listed and designable on every plan, while\n * `process-abandoned.ts` refuses to send it without `abandonedCart` — so\n * the Emails page showed a template that could never send, and read\n * exactly like one that could.\n *\n * A plain string rather than `keyof OrgFeatureFlags` to keep this lib off\n * the billing lib's dependency edge; the console resolves it through\n * `checkEntitlement`, and a spec asserts every value here is a real flag.\n */\n requiresFeature?: string\n}\n\n/**\n * The transactional emails a SITE sends to its own end-users (AGL-769/770).\n *\n * Distinct from the platform system emails (mail Aglyn sends about the\n * account). These are tenant-owned: a site sends them to its customers. Flat\n * list keyed for the host-scoped template store and the `renderHostEmail`\n * resolver; the console UI groups by `pluginId`. Code-defined and fixed — the\n * set is whatever the plugins actually send.\n */\nexport const TENANT_EMAILS: readonly TenantEmailEntry[] = [\n // ── Bookings ──────────────────────────────────────────────────────────\n {\n key: 'booking-confirmed',\n name: 'Booking confirmed',\n description: 'Confirms a booking to the customer, for paid and free services.',\n pluginId: 'bookings',\n plugin: 'Bookings',\n control: 'besigner',\n defaultSubject: 'Booking confirmed: {{service.name}}',\n mergeTokens: [\n { name: 'name', description: \"The customer's name\", sample: 'Alex' },\n {\n name: 'service.name',\n description: 'The booked service',\n sample: 'Consultation',\n },\n {\n name: 'when',\n description: 'Formatted date and time of the booking',\n sample: 'Monday, June 1, 2026 at 9:00 AM',\n },\n {\n name: 'timezone',\n description: 'Timezone the time is shown in',\n sample: 'UTC',\n },\n {\n name: 'booking.ref',\n description: 'Booking reference id',\n sample: 'bk_123',\n },\n ],\n defaultBody: [\n { block: 'text', text: 'Booking confirmed', variant: 'heading' },\n {\n block: 'text',\n text:\n 'Hi {{name}}, your booking for \"{{service.name}}\" is confirmed for ' +\n '{{when}} ({{timezone}}).',\n variant: 'body',\n },\n { block: 'text', text: 'Reference: {{booking.ref}}', variant: 'caption' },\n ],\n },\n {\n key: 'booking-reminder',\n name: 'Booking reminder',\n description: 'Reminds the customer of an upcoming booking.',\n pluginId: 'bookings',\n plugin: 'Bookings',\n control: 'besigner',\n defaultSubject: 'Reminder: {{service.name}} is coming up',\n mergeTokens: [\n { name: 'name', description: \"The customer's name\", sample: 'Alex' },\n {\n name: 'service.name',\n description: 'The booked service',\n sample: 'Consultation',\n },\n {\n name: 'when',\n description: 'Formatted date and time of the booking',\n sample: 'Tomorrow at 9:00 AM',\n },\n ],\n defaultBody: [\n {\n block: 'text',\n text:\n 'Hi {{name}}, this is a reminder that \"{{service.name}}\" is ' +\n 'coming up on {{when}}.',\n variant: 'body',\n },\n ],\n },\n // ── Commerce (fixed for now; flips to `besigner` when the send site is\n // wired to renderHostEmail — AGL-770) ────────────────────────────────\n {\n key: 'order-receipt',\n name: 'Order receipt',\n description: 'Sent to the buyer after a successful order or checkout.',\n pluginId: 'commerce',\n plugin: 'Commerce',\n control: 'besigner',\n defaultSubject: 'Receipt for your order',\n mergeTokens: [\n {\n name: 'order.summary',\n description: 'The ordered items, one per line (with any license/download links)',\n sample: 'House Blend × 2 — $24.00',\n },\n {\n name: 'order.total',\n description: 'Order total',\n sample: '$24.00',\n },\n {\n name: 'order.ref',\n description: 'Order reference id',\n sample: 'cs_test_123',\n },\n ],\n defaultBody: [\n { block: 'text', text: 'Thanks for your purchase!', variant: 'heading' },\n { block: 'text', text: '{{order.summary}}', variant: 'body' },\n { block: 'text', text: 'Total: {{order.total}}', variant: 'body' },\n {\n block: 'text',\n text: 'Order reference: {{order.ref}}',\n variant: 'caption',\n },\n ],\n },\n {\n key: 'sale-notification',\n name: 'New sale',\n description: 'Notifies the seller when an order is placed.',\n pluginId: 'commerce',\n plugin: 'Commerce',\n control: 'besigner',\n defaultSubject: 'New order on {{site.name}}',\n mergeTokens: [\n {\n name: 'site.name',\n description: 'The site the sale happened on',\n sample: 'Northwind Coffee',\n },\n {\n name: 'order.summary',\n description: 'The ordered items',\n sample: 'House Blend — $12.00',\n },\n { name: 'order.total', description: 'Order total', sample: '$12.00' },\n {\n name: 'buyer.email',\n description: \"The buyer's email\",\n sample: 'buyer@example.com',\n },\n {\n name: 'order.ref',\n description: 'Order reference id',\n sample: 'cs_test_123',\n },\n ],\n defaultBody: [\n { block: 'text', text: 'You made a sale', variant: 'heading' },\n {\n block: 'text',\n text: 'A new order came in on {{site.name}}.',\n variant: 'body',\n },\n { block: 'text', text: '{{order.summary}}', variant: 'body' },\n { block: 'text', text: 'Total: {{order.total}}', variant: 'body' },\n {\n block: 'text',\n text: 'Buyer: {{buyer.email}} · Order {{order.ref}}',\n variant: 'caption',\n },\n ],\n },\n {\n key: 'reservation-confirmed',\n name: 'Reservation confirmed',\n description: 'Confirms a paid reservation to the customer.',\n pluginId: 'commerce',\n plugin: 'Commerce',\n control: 'besigner',\n defaultSubject: 'Reservation confirmed',\n mergeTokens: [\n {\n name: 'reservation.checkIn',\n description: 'Check-in date',\n sample: 'Mon, 01 Jun 2026',\n },\n {\n name: 'reservation.nights',\n description: 'Number of nights',\n sample: '2',\n },\n {\n name: 'reservation.paid',\n description: 'Amount paid today',\n sample: '$240.00',\n },\n {\n name: 'reservation.ref',\n description: 'Reservation reference id',\n sample: 'resv_123',\n },\n ],\n defaultBody: [\n { block: 'text', text: 'Reservation confirmed', variant: 'heading' },\n { block: 'text', text: 'Your stay is confirmed!', variant: 'body' },\n {\n block: 'text',\n text: 'Check-in: {{reservation.checkIn}}',\n variant: 'body',\n },\n { block: 'text', text: 'Nights: {{reservation.nights}}', variant: 'body' },\n {\n block: 'text',\n text: 'Paid today: {{reservation.paid}}',\n variant: 'body',\n },\n {\n block: 'text',\n text: 'Reference: {{reservation.ref}}',\n variant: 'caption',\n },\n ],\n },\n {\n key: 'gift-card',\n name: 'Gift card delivery',\n description: 'Delivers a purchased gift card to its recipient.',\n pluginId: 'commerce',\n plugin: 'Commerce',\n control: 'besigner',\n defaultSubject: 'Your gift card',\n mergeTokens: [\n {\n name: 'giftcard.code',\n description: 'The gift card code',\n sample: 'GC-ABC123DEF456',\n },\n {\n name: 'giftcard.value',\n description: 'The gift card value',\n sample: '$25.00',\n },\n ],\n defaultBody: [\n { block: 'text', text: 'Your gift card', variant: 'heading' },\n {\n block: 'text',\n text: 'Gift card code: {{giftcard.code}}',\n variant: 'body',\n },\n { block: 'text', text: 'Value: {{giftcard.value}}', variant: 'body' },\n {\n block: 'text',\n text: 'Enter it at checkout to apply the balance.',\n variant: 'body',\n },\n ],\n },\n {\n key: 'supplier-fulfillment',\n name: 'Supplier fulfillment',\n description: 'Tells a dropship supplier there is a new order to fulfill.',\n pluginId: 'commerce',\n plugin: 'Commerce',\n control: 'fixed',\n },\n {\n key: 'back-in-stock',\n name: 'Back in stock',\n description: 'Tells a customer a product they watched is available again.',\n pluginId: 'commerce',\n plugin: 'Commerce',\n control: 'besigner',\n defaultSubject: 'Back in stock: {{product.name}}',\n mergeTokens: [\n {\n name: 'product.name',\n description: 'The product that restocked',\n sample: 'House Blend',\n },\n {\n name: 'product.url',\n description: 'Link to the product page',\n sample: '/products/house-blend',\n },\n ],\n defaultBody: [\n {\n block: 'text',\n text:\n '{{product.name}} is available again — grab it before it sells out.',\n variant: 'body',\n },\n { block: 'button', label: 'View product', href: '{{product.url}}' },\n ],\n },\n {\n key: 'abandoned-cart',\n name: 'Abandoned cart',\n description: 'Reminds a shopper of items left in their cart.',\n // The cron refuses to send this without the entitlement\n // (`process-abandoned.ts:92`), so the console has to say so (AGL-2081).\n requiresFeature: 'abandonedCart',\n pluginId: 'commerce',\n plugin: 'Commerce',\n control: 'besigner',\n defaultSubject: 'You left something in your cart',\n mergeTokens: [\n {\n name: 'cart.url',\n description: 'Link back to the shopper cart',\n sample: 'https://shop.example.com/cart',\n },\n ],\n defaultBody: [\n {\n block: 'text',\n text: 'Your cart is still waiting — pick up where you left off.',\n variant: 'body',\n },\n { block: 'button', label: 'Return to cart', href: '{{cart.url}}' },\n {\n block: 'text',\n text:\n 'Your items are held but not reserved, so they may sell out.',\n variant: 'caption',\n },\n ],\n },\n {\n key: 'member-post',\n name: 'New member post',\n description: 'Notifies members when new members-only content is posted.',\n pluginId: 'commerce',\n plugin: 'Commerce',\n control: 'fixed',\n },\n {\n key: 'member-password-reset',\n name: 'Member password reset',\n description:\n \"Resets a site member's password — a store-member account, separate \" +\n 'from a console login.',\n pluginId: 'commerce',\n plugin: 'Commerce',\n control: 'fixed',\n },\n // ── Marketing / Workflows (authored in their own UIs) ──────────────────\n {\n key: 'campaign',\n name: 'Campaign broadcast',\n description: \"A campaign sent to the site's subscriber list.\",\n pluginId: 'marketing',\n plugin: 'Marketing',\n control: 'external',\n authoredIn: 'Marketing → Campaigns',\n },\n {\n key: 'workflow-email',\n name: 'Workflow email step',\n description:\n \"An email sent by a workflow's send-email action when its trigger \" +\n 'fires.',\n pluginId: 'workflows',\n plugin: 'Workflows',\n control: 'external',\n authoredIn: 'the workflow that sends it',\n },\n]\n\n/** Firestore subcollection under a host holding its designed templates. */\nexport const TENANT_EMAIL_COLLECTION = 'emailTemplates'\n\nexport function getTenantEmail(key: string): TenantEmailEntry | undefined {\n return TENANT_EMAILS.find((entry) => entry.key === key)\n}\n\n/** True when a site owner can design this email in the besigner. */\nexport function isTenantEmailEditable(entry: TenantEmailEntry): boolean {\n return entry.control === 'besigner'\n}\n"],"names":["TENANT_EMAILS","key","name","description","pluginId","plugin","control","defaultSubject","mergeTokens","sample","defaultBody","block","text","variant","label","href","requiresFeature","authoredIn","TENANT_EMAIL_COLLECTION","getTenantEmail","find","entry","isTenantEmailEditable"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/shared/util/email/src/lib/tenant-email-catalog.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type {\n SystemEmailDefaultBlock,\n SystemEmailMergeToken,\n} from './system-email-catalog'\n\n/**\n * How a site owner controls a tenant email's copy (AGL-769/770):\n *\n * `besigner` — designable here, per site, in the email besigner. The send\n * site resolves the designed template and falls back to built-in copy.\n * `external` — already authored in another part of the console (a marketing\n * campaign, a workflow), so the reference links there instead of offering a\n * second editor.\n * `fixed` — fixed copy inside the plugin, not customizable yet. Listed for\n * visibility; becomes `besigner` once its send site is wired.\n */\nexport type TenantEmailControl = 'besigner' | 'external' | 'fixed'\n\nexport interface TenantEmailEntry {\n /** Stable key — the `hosts/{hostId}/emailTemplates` document id. */\n key: string\n name: string\n description: string\n /** Plugin that owns the send (matches org.enabledPlugins). */\n pluginId: string\n plugin: string\n control: TenantEmailControl\n /** `external`: where the copy is authored, shown as the link/label. */\n authoredIn?: string\n /** `besigner`: subject used when no template is published. */\n defaultSubject?: string\n /** `besigner`: tokens the send site supplies. */\n mergeTokens?: readonly SystemEmailMergeToken[]\n /** `besigner`: starting content the editor seeds and the default renders. */\n defaultBody?: readonly SystemEmailDefaultBlock[]\n /**\n * The footer's \"why am I getting this\" line (AGL-3370), under the built-in\n * copy and under every text-only send of this kind, in the site's header\n * and footer. Names the site by `{{host.businessName}}`, which every site\n * email resolves; never the platform, which the reader has not heard of.\n */\n footerReason?: string\n /**\n * Entitlement flag the SEND is gated on, when one is (AGL-2081).\n *\n * `enabledPlugins` decides whether a template appears at all; this is the\n * narrower question of whether the send behind an appearing template can\n * actually happen. `abandoned-cart` is the case that named it: the template\n * has always been listed and designable on every plan, while\n * `process-abandoned.ts` refuses to send it without `abandonedCart` — so\n * the Emails page showed a template that could never send, and read\n * exactly like one that could.\n *\n * A plain string rather than `keyof OrgFeatureFlags` to keep this lib off\n * the billing lib's dependency edge; the console resolves it through\n * `checkEntitlement`, and a spec asserts every value here is a real flag.\n */\n requiresFeature?: string\n}\n\n/**\n * The transactional emails a SITE sends to its own end-users (AGL-769/770).\n *\n * Distinct from the platform system emails (mail Aglyn sends about the\n * account). These are tenant-owned: a site sends them to its customers. Flat\n * list keyed for the host-scoped template store and the `renderHostEmail`\n * resolver; the console UI groups by `pluginId`. Code-defined and fixed — the\n * set is whatever the plugins actually send.\n */\nexport const TENANT_EMAILS: readonly TenantEmailEntry[] = [\n // ── Bookings ──────────────────────────────────────────────────────────\n {\n key: 'booking-confirmed',\n name: 'Booking confirmed',\n description: 'Confirms a booking to the customer, for paid and free services.',\n pluginId: 'bookings',\n plugin: 'Bookings',\n control: 'besigner',\n defaultSubject: 'Booking confirmed: {{service.name}}',\n mergeTokens: [\n { name: 'name', description: \"The customer's name\", sample: 'Alex' },\n {\n name: 'service.name',\n description: 'The booked service',\n sample: 'Consultation',\n },\n {\n name: 'when',\n description: 'Formatted date and time of the booking',\n sample: 'Monday, June 1, 2026 at 9:00 AM',\n },\n {\n name: 'timezone',\n description: 'Timezone the time is shown in',\n sample: 'UTC',\n },\n {\n name: 'booking.ref',\n description: 'Booking reference id',\n sample: 'bk_123',\n },\n ],\n defaultBody: [\n { block: 'text', text: 'Booking confirmed', variant: 'heading' },\n {\n block: 'text',\n text:\n 'Hi {{name}}, your booking for \"{{service.name}}\" is confirmed for ' +\n '{{when}} ({{timezone}}).',\n variant: 'body',\n },\n { block: 'text', text: 'Reference: {{booking.ref}}', variant: 'caption' },\n ],\n footerReason:\n 'You’re receiving this because you booked with {{host.businessName}}.',\n },\n {\n key: 'booking-reminder',\n name: 'Booking reminder',\n description: 'Reminds the customer of an upcoming booking.',\n pluginId: 'bookings',\n plugin: 'Bookings',\n control: 'besigner',\n defaultSubject: 'Reminder: {{service.name}} is coming up',\n mergeTokens: [\n { name: 'name', description: \"The customer's name\", sample: 'Alex' },\n {\n name: 'service.name',\n description: 'The booked service',\n sample: 'Consultation',\n },\n {\n name: 'when',\n description: 'Formatted date and time of the booking',\n sample: 'Tomorrow at 9:00 AM',\n },\n ],\n defaultBody: [\n {\n block: 'text',\n text:\n 'Hi {{name}}, this is a reminder that \"{{service.name}}\" is ' +\n 'coming up on {{when}}.',\n variant: 'body',\n },\n ],\n footerReason:\n 'You’re receiving this because you have a booking with {{host.businessName}}.',\n },\n // ── Commerce (fixed for now; flips to `besigner` when the send site is\n // wired to renderHostEmail — AGL-770) ────────────────────────────────\n {\n key: 'order-receipt',\n name: 'Order receipt',\n description: 'Sent to the buyer after a successful order or checkout.',\n pluginId: 'commerce',\n plugin: 'Commerce',\n control: 'besigner',\n defaultSubject: 'Receipt for your order',\n mergeTokens: [\n {\n name: 'order.summary',\n description: 'The ordered items, one per line (with any license/download links)',\n sample: 'House Blend × 2 — $24.00',\n },\n {\n name: 'order.total',\n description: 'Order total',\n sample: '$24.00',\n },\n {\n name: 'order.ref',\n description: 'Order reference id',\n sample: 'cs_test_123',\n },\n ],\n defaultBody: [\n { block: 'text', text: 'Thanks for your purchase!', variant: 'heading' },\n { block: 'text', text: '{{order.summary}}', variant: 'body' },\n { block: 'text', text: 'Total: {{order.total}}', variant: 'body' },\n {\n block: 'text',\n text: 'Order reference: {{order.ref}}',\n variant: 'caption',\n },\n ],\n footerReason:\n 'You’re receiving this because you placed an order with {{host.businessName}}.',\n },\n {\n key: 'sale-notification',\n name: 'New sale',\n description: 'Notifies the seller when an order is placed.',\n pluginId: 'commerce',\n plugin: 'Commerce',\n control: 'besigner',\n defaultSubject: 'New order on {{site.name}}',\n mergeTokens: [\n {\n name: 'site.name',\n description: 'The site the sale happened on',\n sample: 'Northwind Coffee',\n },\n {\n name: 'order.summary',\n description: 'The ordered items',\n sample: 'House Blend — $12.00',\n },\n { name: 'order.total', description: 'Order total', sample: '$12.00' },\n {\n name: 'buyer.email',\n description: \"The buyer's email\",\n sample: 'buyer@example.com',\n },\n {\n name: 'order.ref',\n description: 'Order reference id',\n sample: 'cs_test_123',\n },\n ],\n defaultBody: [\n { block: 'text', text: 'You made a sale', variant: 'heading' },\n {\n block: 'text',\n text: 'A new order came in on {{site.name}}.',\n variant: 'body',\n },\n { block: 'text', text: '{{order.summary}}', variant: 'body' },\n { block: 'text', text: 'Total: {{order.total}}', variant: 'body' },\n {\n block: 'text',\n text: 'Buyer: {{buyer.email}} · Order {{order.ref}}',\n variant: 'caption',\n },\n ],\n footerReason:\n 'You’re receiving this because you manage {{host.businessName}}.',\n },\n {\n key: 'reservation-confirmed',\n name: 'Reservation confirmed',\n description: 'Confirms a paid reservation to the customer.',\n pluginId: 'commerce',\n plugin: 'Commerce',\n control: 'besigner',\n defaultSubject: 'Reservation confirmed',\n mergeTokens: [\n {\n name: 'reservation.checkIn',\n description: 'Check-in date',\n sample: 'Mon, 01 Jun 2026',\n },\n {\n name: 'reservation.nights',\n description: 'Number of nights',\n sample: '2',\n },\n {\n name: 'reservation.paid',\n description: 'Amount paid today',\n sample: '$240.00',\n },\n {\n name: 'reservation.ref',\n description: 'Reservation reference id',\n sample: 'resv_123',\n },\n ],\n defaultBody: [\n { block: 'text', text: 'Reservation confirmed', variant: 'heading' },\n { block: 'text', text: 'Your stay is confirmed!', variant: 'body' },\n {\n block: 'text',\n text: 'Check-in: {{reservation.checkIn}}',\n variant: 'body',\n },\n { block: 'text', text: 'Nights: {{reservation.nights}}', variant: 'body' },\n {\n block: 'text',\n text: 'Paid today: {{reservation.paid}}',\n variant: 'body',\n },\n {\n block: 'text',\n text: 'Reference: {{reservation.ref}}',\n variant: 'caption',\n },\n ],\n footerReason:\n 'You’re receiving this because you made a reservation with {{host.businessName}}.',\n },\n {\n key: 'gift-card',\n name: 'Gift card delivery',\n description: 'Delivers a purchased gift card to its recipient.',\n pluginId: 'commerce',\n plugin: 'Commerce',\n control: 'besigner',\n defaultSubject: 'Your gift card',\n mergeTokens: [\n {\n name: 'giftcard.code',\n description: 'The gift card code',\n sample: 'GC-ABC123DEF456',\n },\n {\n name: 'giftcard.value',\n description: 'The gift card value',\n sample: '$25.00',\n },\n ],\n defaultBody: [\n { block: 'text', text: 'Your gift card', variant: 'heading' },\n {\n block: 'text',\n text: 'Gift card code: {{giftcard.code}}',\n variant: 'body',\n },\n { block: 'text', text: 'Value: {{giftcard.value}}', variant: 'body' },\n {\n block: 'text',\n text: 'Enter it at checkout to apply the balance.',\n variant: 'body',\n },\n ],\n footerReason:\n 'You’re receiving this because a gift card from {{host.businessName}} was sent to this address.',\n },\n {\n key: 'supplier-fulfillment',\n name: 'Supplier fulfillment',\n description: 'Tells a dropship supplier there is a new order to fulfill.',\n pluginId: 'commerce',\n plugin: 'Commerce',\n control: 'fixed',\n },\n {\n key: 'back-in-stock',\n name: 'Back in stock',\n description: 'Tells a customer a product they watched is available again.',\n pluginId: 'commerce',\n plugin: 'Commerce',\n control: 'besigner',\n defaultSubject: 'Back in stock: {{product.name}}',\n mergeTokens: [\n {\n name: 'product.name',\n description: 'The product that restocked',\n sample: 'House Blend',\n },\n {\n name: 'product.url',\n description: 'Link to the product page',\n sample: '/products/house-blend',\n },\n ],\n defaultBody: [\n {\n block: 'text',\n text:\n '{{product.name}} is available again — grab it before it sells out.',\n variant: 'body',\n },\n { block: 'button', label: 'View product', href: '{{product.url}}' },\n ],\n footerReason:\n 'You’re receiving this because you asked {{host.businessName}} to tell you when this was back.',\n },\n {\n key: 'abandoned-cart',\n name: 'Abandoned cart',\n description: 'Reminds a shopper of items left in their cart.',\n // The cron refuses to send this without the entitlement\n // (`process-abandoned.ts:92`), so the console has to say so (AGL-2081).\n requiresFeature: 'abandonedCart',\n pluginId: 'commerce',\n plugin: 'Commerce',\n control: 'besigner',\n defaultSubject: 'You left something in your cart',\n mergeTokens: [\n {\n name: 'cart.url',\n description: 'Link back to the shopper cart',\n sample: 'https://shop.example.com/cart',\n },\n ],\n defaultBody: [\n {\n block: 'text',\n text: 'Your cart is still waiting — pick up where you left off.',\n variant: 'body',\n },\n { block: 'button', label: 'Return to cart', href: '{{cart.url}}' },\n {\n block: 'text',\n text:\n 'Your items are held but not reserved, so they may sell out.',\n variant: 'caption',\n },\n ],\n footerReason:\n 'You’re receiving this because you started a checkout with {{host.businessName}}.',\n },\n {\n key: 'member-post',\n name: 'New member post',\n description: 'Notifies members when new members-only content is posted.',\n pluginId: 'commerce',\n plugin: 'Commerce',\n control: 'besigner',\n defaultSubject: '{{post.title}}',\n mergeTokens: [\n { name: 'post.title', description: 'The post’s title', sample: 'This month in the studio' },\n {\n name: 'post.body',\n description: 'The post, as its author wrote it',\n sample: 'Three new pieces are up for members first.',\n },\n ],\n defaultBody: [\n { block: 'text', text: '{{post.title}}', variant: 'heading' },\n { block: 'text', text: '{{post.body}}', variant: 'body' },\n ],\n footerReason: 'You’re receiving this because you’re a member of {{host.businessName}}.',\n },\n {\n key: 'member-password-reset',\n name: 'Member password reset',\n description:\n \"Resets a site member's password — a store-member account, separate \" +\n 'from a console login. Sent when the member asks, and when a site ' +\n 'administrator starts a reset for them.',\n pluginId: 'commerce',\n plugin: 'Commerce',\n control: 'besigner',\n defaultSubject: 'Reset your {{site.name}} password',\n mergeTokens: [\n { name: 'site.name', description: 'The site’s name', sample: 'Northwind Coffee' },\n {\n name: 'reset.intro',\n description: 'Who started the reset',\n sample: 'Someone asked to reset the password for your Northwind Coffee account.',\n },\n {\n name: 'resetUrl',\n description: 'The one-time reset link',\n sample: 'https://shop.example.com/recover?token=…',\n },\n {\n name: 'reset.note',\n description: 'How long the link lasts, and what to do if unexpected',\n sample:\n 'The link works once and expires in 1 hour. If you did not ask for ' +\n 'this, you can safely ignore this email — your password is unchanged.',\n },\n ],\n defaultBody: [\n { block: 'text', text: 'Reset your password', variant: 'heading' },\n { block: 'text', text: '{{reset.intro}}', variant: 'body' },\n { block: 'button', label: 'Set a new password', href: '{{resetUrl}}' },\n { block: 'text', text: '{{reset.note}}', variant: 'caption' },\n ],\n footerReason:\n 'You’re receiving this because a password reset was started for your ' +\n '{{host.businessName}} account.',\n },\n {\n key: 'member-password-changed',\n name: 'Member password changed',\n description:\n 'Tells a site member that a site administrator set a new password on ' +\n 'their account.',\n pluginId: 'commerce',\n plugin: 'Commerce',\n control: 'besigner',\n defaultSubject: 'Your {{site.name}} password was changed',\n mergeTokens: [\n { name: 'site.name', description: 'The site’s name', sample: 'Northwind Coffee' },\n {\n name: 'signInUrl',\n description: 'Where the member signs back in',\n sample: 'https://shop.example.com',\n },\n ],\n defaultBody: [\n { block: 'text', text: 'Your password was changed', variant: 'heading' },\n {\n block: 'text',\n text:\n 'An administrator of {{site.name}} set a new password on your ' +\n 'account. You have been signed out on every device and will need ' +\n 'the new password to sign back in.',\n variant: 'body',\n },\n { block: 'button', label: 'Sign in', href: '{{signInUrl}}' },\n {\n block: 'text',\n text: 'If you did not expect this, contact the site owner.',\n variant: 'caption',\n },\n ],\n footerReason:\n 'You’re receiving this because the password on your ' +\n '{{host.businessName}} account was changed.',\n },\n {\n key: 'newsletter-confirmation',\n name: 'Subscription confirmation',\n description:\n 'Asks someone who signed up for a site’s emails to confirm the address ' +\n 'before anything is sent to it.',\n pluginId: 'commerce',\n plugin: 'Commerce',\n control: 'besigner',\n defaultSubject: 'Confirm your subscription',\n mergeTokens: [\n {\n name: 'stream.name',\n description: 'What they signed up for',\n sample: 'our newsletter',\n },\n {\n name: 'confirmUrl',\n description: 'The confirmation link',\n sample: 'https://shop.example.com/subscribe/confirm?token=…',\n },\n ],\n defaultBody: [\n { block: 'text', text: 'Confirm your subscription', variant: 'heading' },\n {\n block: 'text',\n text: 'Please confirm that you want to receive {{stream.name}} at this address.',\n variant: 'body',\n },\n { block: 'button', label: 'Confirm my subscription', href: '{{confirmUrl}}' },\n {\n block: 'text',\n text:\n 'The link works for three days. If you did not sign up, ignore this ' +\n 'message — nothing will be sent.',\n variant: 'caption',\n },\n ],\n footerReason:\n 'You’re receiving this because this address was entered on ' +\n '{{host.businessName}}’s signup form.',\n },\n // ── Marketing / Workflows (authored in their own UIs) ──────────────────\n {\n key: 'campaign',\n name: 'Campaign broadcast',\n description: \"A campaign sent to the site's subscriber list.\",\n pluginId: 'marketing',\n plugin: 'Marketing',\n control: 'external',\n authoredIn: 'Marketing → Campaigns',\n footerReason:\n 'You’re receiving this email from {{host.businessName}}.',\n },\n {\n key: 'workflow-email',\n name: 'Workflow email step',\n description:\n \"An email sent by a workflow's send-email action when its trigger \" +\n 'fires.',\n pluginId: 'workflows',\n plugin: 'Workflows',\n control: 'external',\n authoredIn: 'the workflow that sends it',\n footerReason:\n 'You’re receiving this because you’re in touch with {{host.businessName}}.',\n },\n]\n\n/** Firestore subcollection under a host holding its designed templates. */\nexport const TENANT_EMAIL_COLLECTION = 'emailTemplates'\n\nexport function getTenantEmail(key: string): TenantEmailEntry | undefined {\n return TENANT_EMAILS.find((entry) => entry.key === key)\n}\n\n/** True when a site owner can design this email in the besigner. */\nexport function isTenantEmailEditable(entry: TenantEmailEntry): boolean {\n return entry.control === 'besigner'\n}\n"],"names":["TENANT_EMAILS","key","name","description","pluginId","plugin","control","defaultSubject","mergeTokens","sample","defaultBody","block","text","variant","footerReason","label","href","requiresFeature","authoredIn","TENANT_EMAIL_COLLECTION","getTenantEmail","find","entry","isTenantEmailEditable"],"mappings":"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"}
@@ -14,6 +14,7 @@
14
14
  * See the License for the specific language governing permissions and
15
15
  * limitations under the License.
16
16
  */
17
+ import { type EmailChrome, type EmailTheme, type RenderedEmail } from './email-render';
17
18
  /**
18
19
  * THE HTML PART EVERY MESSAGE GETS.
19
20
  *
@@ -67,4 +68,29 @@
67
68
  * gets the same treatment whichever body it carries.
68
69
  */
69
70
  export declare function renderTextEmailHtml(text: string, subject?: string, preheader?: string): string;
71
+ /**
72
+ * A plain-text message in a sender's header and footer (AGL-3370).
73
+ *
74
+ * For mail whose body is text somebody typed or a sender composed, sent
75
+ * under a brand: a workflow's email step, a typed campaign, a member post a
76
+ * site has not designed. {@link renderTextEmailHtml} is the unbranded card
77
+ * for text nobody made a brand decision about; this is the same words, one
78
+ * paragraph per blank-line-separated block and their links live, drawn
79
+ * inside the chrome the caller chose. The plain-text part carries the
80
+ * footer's lines too.
81
+ *
82
+ * Merge tokens are not substituted: the text arrives resolved, no merge map
83
+ * is passed, and a `{{…}}` left in it is the author's own and stays as typed.
84
+ */
85
+ export declare function renderFramedTextEmail(input: {
86
+ text: string;
87
+ subject?: string;
88
+ preheader?: string;
89
+ chrome: EmailChrome;
90
+ /** The sender's theme: the links take its accent. */
91
+ theme?: EmailTheme;
92
+ /** The sender's origin and host, which a logo stored as a media reference resolves against. */
93
+ mediaOrigin?: string;
94
+ mediaHostId?: string;
95
+ }): RenderedEmail;
70
96
  export default renderTextEmailHtml;
@@ -1,3 +1,4 @@
1
+ import { _ as _extends } from "@swc/helpers/_/_extends";
1
2
  /**
2
3
  * @license
3
4
  * Copyright 2026 Aglyn LLC
@@ -14,7 +15,7 @@
14
15
  * See the License for the specific language governing permissions and
15
16
  * limitations under the License.
16
17
  */ import { linkifyEscapedText } from "./email-linkify.js";
17
- import { escapeEmailHtml } from "./email-render.js";
18
+ import { escapeEmailHtml, renderEmailHtml } from "./email-render.js";
18
19
  /**
19
20
  * THE HTML PART EVERY MESSAGE GETS.
20
21
  *
@@ -73,6 +74,60 @@ import { escapeEmailHtml } from "./email-render.js";
73
74
  const paragraphs = body.split(/\n\s*\n/).map((block)=>block.trim()).filter(Boolean).map((block)=>`<p style="margin:0 0 16px;">` + linkifyEscapedText(escapeEmailHtml(block)).replace(/\n/g, '<br />') + `</p>`).join('');
74
75
  return `<!DOCTYPE html><html><head><meta charset="utf-8" />` + `<meta name="viewport" content="width=device-width, initial-scale=1" />` + `<title>${escapeEmailHtml(String(subject != null ? subject : ''))}</title></head>` + `<body style="margin:0;padding:0;background-color:#f4f4f4;">` + preheaderHtml + `<table role="presentation" width="100%" cellpadding="0" cellspacing="0" ` + `style="background-color:#f4f4f4;"><tr><td style="padding:24px 8px;">` + `<table role="presentation" width="600" cellpadding="0" cellspacing="0" ` + `align="center" style="max-width:600px;width:100%;margin:0 auto;">` + `<tr><td style="padding:24px;background-color:#ffffff;border-radius:8px;` + `font-family:Helvetica, Arial, sans-serif;font-size:15px;line-height:1.6;` + `color:#1a1a1a;">` + paragraphs + `</td></tr></table></td></tr></table></body></html>`;
75
76
  }
77
+ /**
78
+ * A plain-text message in a sender's header and footer (AGL-3370).
79
+ *
80
+ * For mail whose body is text somebody typed or a sender composed, sent
81
+ * under a brand: a workflow's email step, a typed campaign, a member post a
82
+ * site has not designed. {@link renderTextEmailHtml} is the unbranded card
83
+ * for text nobody made a brand decision about; this is the same words, one
84
+ * paragraph per blank-line-separated block and their links live, drawn
85
+ * inside the chrome the caller chose. The plain-text part carries the
86
+ * footer's lines too.
87
+ *
88
+ * Merge tokens are not substituted: the text arrives resolved, no merge map
89
+ * is passed, and a `{{…}}` left in it is the author's own and stays as typed.
90
+ */ export function renderFramedTextEmail(input) {
91
+ var _input_text, _input_subject, _input_preheader;
92
+ const blocks = String((_input_text = input.text) != null ? _input_text : '').trim().split(/\n\s*\n/).map((block)=>block.trim()).filter(Boolean);
93
+ const nodes = {
94
+ root: {
95
+ componentId: 'div',
96
+ nodes: [
97
+ 'section'
98
+ ]
99
+ },
100
+ section: {
101
+ componentId: 'emailSection',
102
+ props: {
103
+ padding: 24
104
+ },
105
+ nodes: blocks.map((_, index)=>`p${index}`)
106
+ }
107
+ };
108
+ blocks.forEach((block, index)=>{
109
+ nodes[`p${index}`] = {
110
+ componentId: 'emailText',
111
+ props: {
112
+ children: block,
113
+ variant: 'body'
114
+ }
115
+ };
116
+ });
117
+ const rendered = renderEmailHtml(_extends({
118
+ nodes: nodes,
119
+ subject: (_input_subject = input.subject) != null ? _input_subject : '',
120
+ preheader: (_input_preheader = input.preheader) != null ? _input_preheader : '',
121
+ // No author markup reaches a text block, so no policy is exercised.
122
+ sanitize: (html)=>html,
123
+ chrome: input.chrome
124
+ }, input.theme, input.mediaOrigin ? {
125
+ mediaOrigin: input.mediaOrigin
126
+ } : {}, input.mediaHostId ? {
127
+ mediaHostId: input.mediaHostId
128
+ } : {}));
129
+ return rendered;
130
+ }
76
131
  export default renderTextEmailHtml;
77
132
 
78
133
  //# sourceMappingURL=text-email-html.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/shared/util/email/src/lib/text-email-html.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { linkifyEscapedText } from './email-linkify'\nimport { escapeEmailHtml } from './email-render'\n\n/**\n * THE HTML PART EVERY MESSAGE GETS.\n *\n * ## What was wrong\n *\n * `sendEmail` forwarded exactly what it was handed, and almost nothing hands\n * it `html`. Twelve senders have no HTML path at all; the other twenty-seven\n * write `...(designed?.html ? { html: designed.html } : {})`, which produces\n * an HTML part only when a staff-designed template is published for that key.\n * With none published, every message on the domain went out as `\"html\": \"\"` —\n * a `text/plain` part and nothing else.\n *\n * Two consequences, and the second is the one that hid the first:\n *\n * 1. A URL in a plain-text part is not a link. It renders as the characters\n * of the URL; whether it is clickable at all is the mail client's guess,\n * and a long one wraps mid-string and stops being followable.\n * 2. **Click tracking cannot work.** Resend measures clicks by rewriting\n * `<a href>` in the HTML part. No HTML part means no anchors, which means\n * no rewriting, which means the click rate is structurally 0% and would\n * have stayed there however long anyone waited on it.\n *\n * ## Why it lives here rather than at the call sites\n *\n * The same reasoning `contextTag` records one file over: threading an HTML\n * body through thirty-nine call sites asks thirty-nine places to remember,\n * which is the shape that produces the fortieth that does not. A sender that\n * builds real HTML still wins — this is a fallback, consulted only when\n * `html` is absent — so no existing caller changes and no designed template\n * is overridden.\n *\n * ## What it is not\n *\n * Not a template. It carries no logo, no brand color and no footer, because\n * it stands in for copy that was written to be read as plain text and it must\n * not imply a design decision nobody made. It is the plain-text body, in a\n * readable column, with its links actually linked.\n */\n\n/**\n * Renders a plain-text body as the HTML part of the same message.\n *\n * Blank-line-separated blocks become paragraphs and single newlines become\n * `<br />`, which is what the senders' template literals already mean by\n * them. Returns an empty string for empty input so a caller can treat \"no\n * text\" and \"no html\" the same way.\n *\n * @param text The plain-text body being sent alongside this.\n * @param subject Used only for the document `<title>`.\n * @param preheader The hidden line inboxes show after the subject. Same\n * markup `renderEmailHtml` emits for a designed email, so a campaign\n * gets the same treatment whichever body it carries.\n */\nexport function renderTextEmailHtml(\n text: string,\n subject = '',\n preheader = '',\n): string {\n const body = String(text ?? '').trim()\n if (!body) return ''\n\n const preview = String(preheader ?? '').trim()\n const preheaderHtml = preview\n ? `<div style=\"display:none;max-height:0;overflow:hidden;mso-hide:all;\">` +\n escapeEmailHtml(preview) +\n `</div>`\n : ''\n\n const paragraphs = body\n .split(/\\n\\s*\\n/)\n .map((block) => block.trim())\n .filter(Boolean)\n .map(\n (block) =>\n `<p style=\"margin:0 0 16px;\">` +\n linkifyEscapedText(escapeEmailHtml(block)).replace(/\\n/g, '<br />') +\n `</p>`,\n )\n .join('')\n\n return (\n `<!DOCTYPE html><html><head><meta charset=\"utf-8\" />` +\n `<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />` +\n `<title>${escapeEmailHtml(String(subject ?? ''))}</title></head>` +\n `<body style=\"margin:0;padding:0;background-color:#f4f4f4;\">` +\n preheaderHtml +\n `<table role=\"presentation\" width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" ` +\n `style=\"background-color:#f4f4f4;\"><tr><td style=\"padding:24px 8px;\">` +\n `<table role=\"presentation\" width=\"600\" cellpadding=\"0\" cellspacing=\"0\" ` +\n `align=\"center\" style=\"max-width:600px;width:100%;margin:0 auto;\">` +\n `<tr><td style=\"padding:24px;background-color:#ffffff;border-radius:8px;` +\n `font-family:Helvetica, Arial, sans-serif;font-size:15px;line-height:1.6;` +\n `color:#1a1a1a;\">` +\n paragraphs +\n `</td></tr></table></td></tr></table></body></html>`\n )\n}\n\nexport default renderTextEmailHtml\n"],"names":["linkifyEscapedText","escapeEmailHtml","renderTextEmailHtml","text","subject","preheader","body","String","trim","preview","preheaderHtml","paragraphs","split","map","block","filter","Boolean","replace","join"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,kBAAkB,QAAQ,qBAAiB;AACpD,SAASC,eAAe,QAAQ,oBAAgB;AAEhD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAqCC,GAED;;;;;;;;;;;;;CAaC,GACD,OAAO,SAASC,oBACdC,IAAY,EACZC,UAAU,EAAE,EACZC,YAAY,EAAE;IAEd,MAAMC,OAAOC,OAAOJ,eAAAA,OAAQ,IAAIK,IAAI;IACpC,IAAI,CAACF,MAAM,OAAO;IAElB,MAAMG,UAAUF,OAAOF,oBAAAA,YAAa,IAAIG,IAAI;IAC5C,MAAME,gBAAgBD,UAClB,CAAC,qEAAqE,CAAC,GACvER,gBAAgBQ,WAChB,CAAC,MAAM,CAAC,GACR;IAEJ,MAAME,aAAaL,KAChBM,KAAK,CAAC,WACNC,GAAG,CAAC,CAACC,QAAUA,MAAMN,IAAI,IACzBO,MAAM,CAACC,SACPH,GAAG,CACF,CAACC,QACC,CAAC,4BAA4B,CAAC,GAC9Bd,mBAAmBC,gBAAgBa,QAAQG,OAAO,CAAC,OAAO,YAC1D,CAAC,IAAI,CAAC,EAETC,IAAI,CAAC;IAER,OACE,CAAC,mDAAmD,CAAC,GACrD,CAAC,sEAAsE,CAAC,GACxE,CAAC,OAAO,EAAEjB,gBAAgBM,OAAOH,kBAAAA,UAAW,KAAK,eAAe,CAAC,GACjE,CAAC,2DAA2D,CAAC,GAC7DM,gBACA,CAAC,wEAAwE,CAAC,GAC1E,CAAC,oEAAoE,CAAC,GACtE,CAAC,uEAAuE,CAAC,GACzE,CAAC,iEAAiE,CAAC,GACnE,CAAC,uEAAuE,CAAC,GACzE,CAAC,wEAAwE,CAAC,GAC1E,CAAC,gBAAgB,CAAC,GAClBC,aACA,CAAC,kDAAkD,CAAC;AAExD;AAEA,eAAeT,oBAAmB"}
1
+ {"version":3,"sources":["../../../../../../../libs/shared/util/email/src/lib/text-email-html.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { linkifyEscapedText } from './email-linkify'\nimport {\n escapeEmailHtml,\n renderEmailHtml,\n type EmailChrome,\n type EmailTheme,\n type RenderedEmail,\n} from './email-render'\n\n/**\n * THE HTML PART EVERY MESSAGE GETS.\n *\n * ## What was wrong\n *\n * `sendEmail` forwarded exactly what it was handed, and almost nothing hands\n * it `html`. Twelve senders have no HTML path at all; the other twenty-seven\n * write `...(designed?.html ? { html: designed.html } : {})`, which produces\n * an HTML part only when a staff-designed template is published for that key.\n * With none published, every message on the domain went out as `\"html\": \"\"` —\n * a `text/plain` part and nothing else.\n *\n * Two consequences, and the second is the one that hid the first:\n *\n * 1. A URL in a plain-text part is not a link. It renders as the characters\n * of the URL; whether it is clickable at all is the mail client's guess,\n * and a long one wraps mid-string and stops being followable.\n * 2. **Click tracking cannot work.** Resend measures clicks by rewriting\n * `<a href>` in the HTML part. No HTML part means no anchors, which means\n * no rewriting, which means the click rate is structurally 0% and would\n * have stayed there however long anyone waited on it.\n *\n * ## Why it lives here rather than at the call sites\n *\n * The same reasoning `contextTag` records one file over: threading an HTML\n * body through thirty-nine call sites asks thirty-nine places to remember,\n * which is the shape that produces the fortieth that does not. A sender that\n * builds real HTML still wins — this is a fallback, consulted only when\n * `html` is absent — so no existing caller changes and no designed template\n * is overridden.\n *\n * ## What it is not\n *\n * Not a template. It carries no logo, no brand color and no footer, because\n * it stands in for copy that was written to be read as plain text and it must\n * not imply a design decision nobody made. It is the plain-text body, in a\n * readable column, with its links actually linked.\n */\n\n/**\n * Renders a plain-text body as the HTML part of the same message.\n *\n * Blank-line-separated blocks become paragraphs and single newlines become\n * `<br />`, which is what the senders' template literals already mean by\n * them. Returns an empty string for empty input so a caller can treat \"no\n * text\" and \"no html\" the same way.\n *\n * @param text The plain-text body being sent alongside this.\n * @param subject Used only for the document `<title>`.\n * @param preheader The hidden line inboxes show after the subject. Same\n * markup `renderEmailHtml` emits for a designed email, so a campaign\n * gets the same treatment whichever body it carries.\n */\nexport function renderTextEmailHtml(\n text: string,\n subject = '',\n preheader = '',\n): string {\n const body = String(text ?? '').trim()\n if (!body) return ''\n\n const preview = String(preheader ?? '').trim()\n const preheaderHtml = preview\n ? `<div style=\"display:none;max-height:0;overflow:hidden;mso-hide:all;\">` +\n escapeEmailHtml(preview) +\n `</div>`\n : ''\n\n const paragraphs = body\n .split(/\\n\\s*\\n/)\n .map((block) => block.trim())\n .filter(Boolean)\n .map(\n (block) =>\n `<p style=\"margin:0 0 16px;\">` +\n linkifyEscapedText(escapeEmailHtml(block)).replace(/\\n/g, '<br />') +\n `</p>`,\n )\n .join('')\n\n return (\n `<!DOCTYPE html><html><head><meta charset=\"utf-8\" />` +\n `<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />` +\n `<title>${escapeEmailHtml(String(subject ?? ''))}</title></head>` +\n `<body style=\"margin:0;padding:0;background-color:#f4f4f4;\">` +\n preheaderHtml +\n `<table role=\"presentation\" width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" ` +\n `style=\"background-color:#f4f4f4;\"><tr><td style=\"padding:24px 8px;\">` +\n `<table role=\"presentation\" width=\"600\" cellpadding=\"0\" cellspacing=\"0\" ` +\n `align=\"center\" style=\"max-width:600px;width:100%;margin:0 auto;\">` +\n `<tr><td style=\"padding:24px;background-color:#ffffff;border-radius:8px;` +\n `font-family:Helvetica, Arial, sans-serif;font-size:15px;line-height:1.6;` +\n `color:#1a1a1a;\">` +\n paragraphs +\n `</td></tr></table></td></tr></table></body></html>`\n )\n}\n\n/**\n * A plain-text message in a sender's header and footer (AGL-3370).\n *\n * For mail whose body is text somebody typed or a sender composed, sent\n * under a brand: a workflow's email step, a typed campaign, a member post a\n * site has not designed. {@link renderTextEmailHtml} is the unbranded card\n * for text nobody made a brand decision about; this is the same words, one\n * paragraph per blank-line-separated block and their links live, drawn\n * inside the chrome the caller chose. The plain-text part carries the\n * footer's lines too.\n *\n * Merge tokens are not substituted: the text arrives resolved, no merge map\n * is passed, and a `{{…}}` left in it is the author's own and stays as typed.\n */\nexport function renderFramedTextEmail(input: {\n text: string\n subject?: string\n preheader?: string\n chrome: EmailChrome\n /** The sender's theme: the links take its accent. */\n theme?: EmailTheme\n /** The sender's origin and host, which a logo stored as a media reference resolves against. */\n mediaOrigin?: string\n mediaHostId?: string\n}): RenderedEmail {\n const blocks = String(input.text ?? '')\n .trim()\n .split(/\\n\\s*\\n/)\n .map((block) => block.trim())\n .filter(Boolean)\n const nodes: Record<string, unknown> = {\n root: { componentId: 'div', nodes: ['section'] },\n section: {\n componentId: 'emailSection',\n props: { padding: 24 },\n nodes: blocks.map((_, index) => `p${index}`),\n },\n }\n blocks.forEach((block, index) => {\n nodes[`p${index}`] = {\n componentId: 'emailText',\n props: { children: block, variant: 'body' },\n }\n })\n const rendered = renderEmailHtml({\n nodes: nodes as never,\n subject: input.subject ?? '',\n preheader: input.preheader ?? '',\n // No author markup reaches a text block, so no policy is exercised.\n sanitize: (html: string) => html,\n chrome: input.chrome,\n ...input.theme,\n ...(input.mediaOrigin ? { mediaOrigin: input.mediaOrigin } : {}),\n ...(input.mediaHostId ? { mediaHostId: input.mediaHostId } : {}),\n })\n return rendered\n}\n\nexport default renderTextEmailHtml\n"],"names":["linkifyEscapedText","escapeEmailHtml","renderEmailHtml","renderTextEmailHtml","text","subject","preheader","body","String","trim","preview","preheaderHtml","paragraphs","split","map","block","filter","Boolean","replace","join","renderFramedTextEmail","input","blocks","nodes","root","componentId","section","props","padding","_","index","forEach","children","variant","rendered","sanitize","html","chrome","theme","mediaOrigin","mediaHostId"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,kBAAkB,QAAQ,qBAAiB;AACpD,SACEC,eAAe,EACfC,eAAe,QAIV,oBAAgB;AAEvB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAqCC,GAED;;;;;;;;;;;;;CAaC,GACD,OAAO,SAASC,oBACdC,IAAY,EACZC,UAAU,EAAE,EACZC,YAAY,EAAE;IAEd,MAAMC,OAAOC,OAAOJ,eAAAA,OAAQ,IAAIK,IAAI;IACpC,IAAI,CAACF,MAAM,OAAO;IAElB,MAAMG,UAAUF,OAAOF,oBAAAA,YAAa,IAAIG,IAAI;IAC5C,MAAME,gBAAgBD,UAClB,CAAC,qEAAqE,CAAC,GACvET,gBAAgBS,WAChB,CAAC,MAAM,CAAC,GACR;IAEJ,MAAME,aAAaL,KAChBM,KAAK,CAAC,WACNC,GAAG,CAAC,CAACC,QAAUA,MAAMN,IAAI,IACzBO,MAAM,CAACC,SACPH,GAAG,CACF,CAACC,QACC,CAAC,4BAA4B,CAAC,GAC9Bf,mBAAmBC,gBAAgBc,QAAQG,OAAO,CAAC,OAAO,YAC1D,CAAC,IAAI,CAAC,EAETC,IAAI,CAAC;IAER,OACE,CAAC,mDAAmD,CAAC,GACrD,CAAC,sEAAsE,CAAC,GACxE,CAAC,OAAO,EAAElB,gBAAgBO,OAAOH,kBAAAA,UAAW,KAAK,eAAe,CAAC,GACjE,CAAC,2DAA2D,CAAC,GAC7DM,gBACA,CAAC,wEAAwE,CAAC,GAC1E,CAAC,oEAAoE,CAAC,GACtE,CAAC,uEAAuE,CAAC,GACzE,CAAC,iEAAiE,CAAC,GACnE,CAAC,uEAAuE,CAAC,GACzE,CAAC,wEAAwE,CAAC,GAC1E,CAAC,gBAAgB,CAAC,GAClBC,aACA,CAAC,kDAAkD,CAAC;AAExD;AAEA;;;;;;;;;;;;;CAaC,GACD,OAAO,SAASQ,sBAAsBC,KAUrC;QACuBA,aAqBXA,gBACEA;IAtBb,MAAMC,SAASd,QAAOa,cAAAA,MAAMjB,IAAI,YAAViB,cAAc,IACjCZ,IAAI,GACJI,KAAK,CAAC,WACNC,GAAG,CAAC,CAACC,QAAUA,MAAMN,IAAI,IACzBO,MAAM,CAACC;IACV,MAAMM,QAAiC;QACrCC,MAAM;YAAEC,aAAa;YAAOF,OAAO;gBAAC;aAAU;QAAC;QAC/CG,SAAS;YACPD,aAAa;YACbE,OAAO;gBAAEC,SAAS;YAAG;YACrBL,OAAOD,OAAOR,GAAG,CAAC,CAACe,GAAGC,QAAU,CAAC,CAAC,EAAEA,OAAO;QAC7C;IACF;IACAR,OAAOS,OAAO,CAAC,CAAChB,OAAOe;QACrBP,KAAK,CAAC,CAAC,CAAC,EAAEO,OAAO,CAAC,GAAG;YACnBL,aAAa;YACbE,OAAO;gBAAEK,UAAUjB;gBAAOkB,SAAS;YAAO;QAC5C;IACF;IACA,MAAMC,WAAWhC,gBAAgB;QAC/BqB,OAAOA;QACPlB,OAAO,GAAEgB,iBAAAA,MAAMhB,OAAO,YAAbgB,iBAAiB;QAC1Bf,SAAS,GAAEe,mBAAAA,MAAMf,SAAS,YAAfe,mBAAmB;QAC9B,oEAAoE;QACpEc,UAAU,CAACC,OAAiBA;QAC5BC,QAAQhB,MAAMgB,MAAM;OACjBhB,MAAMiB,KAAK,EACVjB,MAAMkB,WAAW,GAAG;QAAEA,aAAalB,MAAMkB,WAAW;IAAC,IAAI,CAAC,GAC1DlB,MAAMmB,WAAW,GAAG;QAAEA,aAAanB,MAAMmB,WAAW;IAAC,IAAI,CAAC;IAEhE,OAAON;AACT;AAEA,eAAe/B,oBAAmB"}