strapi-plugin-magic-mail 2.9.1 → 2.9.2

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.
@@ -1007,6 +1007,10 @@ const attributes = {
1007
1007
  type: "boolean",
1008
1008
  "default": true,
1009
1009
  configurable: false
1010
+ },
1011
+ trackingFallbackUrl: {
1012
+ type: "string",
1013
+ configurable: false
1010
1014
  }
1011
1015
  };
1012
1016
  const require$$7 = {
@@ -48884,7 +48888,11 @@ const schemas = {
48884
48888
  defaultFromName: headerSafe.optional(),
48885
48889
  defaultFromEmail: emailString.optional().or(z2.literal("")),
48886
48890
  unsubscribeUrl: z2.string().url().optional().or(z2.literal("")),
48887
- enableUnsubscribeHeader: z2.boolean().optional()
48891
+ enableUnsubscribeHeader: z2.boolean().optional(),
48892
+ // Where to redirect the recipient when a tracking link is no longer
48893
+ // resolvable (e.g. retention cleanup removed the row). If empty the
48894
+ // tracker renders a static HTML fallback page instead.
48895
+ trackingFallbackUrl: z2.string().url().optional().or(z2.literal(""))
48888
48896
  }),
48889
48897
  // ── Content-API send payloads ───────────────────────────────────────────
48890
48898
  // Bounded attachments to prevent OOM from huge base64 payloads.
@@ -49344,7 +49352,7 @@ var oauthState = {
49344
49352
  verifyAndConsumeState: verifyAndConsumeState$1
49345
49353
  };
49346
49354
  const { createState, verifyAndConsumeState } = oauthState;
49347
- function escapeHtml(str2) {
49355
+ function escapeHtml$1(str2) {
49348
49356
  return String(str2 || "").replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#039;");
49349
49357
  }
49350
49358
  function escapeJs(str2) {
@@ -49409,7 +49417,7 @@ var oauth$3 = {
49409
49417
  </head>
49410
49418
  <body>
49411
49419
  <div class="error">[ERROR] OAuth Authorization Failed</div>
49412
- <p>Error: ${escapeHtml(error2)}</p>
49420
+ <p>Error: ${escapeHtml$1(error2)}</p>
49413
49421
  <p>You can close this window and try again.</p>
49414
49422
  <script>
49415
49423
  setTimeout(() => window.close(), 3000);
@@ -49520,7 +49528,7 @@ var oauth$3 = {
49520
49528
  </head>
49521
49529
  <body>
49522
49530
  <div class="error">[ERROR] OAuth Authorization Failed</div>
49523
- <p>Error: ${escapeHtml(error2)}</p>
49531
+ <p>Error: ${escapeHtml$1(error2)}</p>
49524
49532
  <p>You can close this window and try again.</p>
49525
49533
  <script>
49526
49534
  setTimeout(() => window.close(), 3000);
@@ -49628,7 +49636,7 @@ var oauth$3 = {
49628
49636
  </head>
49629
49637
  <body>
49630
49638
  <div class="error">[ERROR] OAuth Authorization Failed</div>
49631
- <p>Error: ${escapeHtml(error2)}</p>
49639
+ <p>Error: ${escapeHtml$1(error2)}</p>
49632
49640
  <p>You can close this window and try again.</p>
49633
49641
  <script>
49634
49642
  setTimeout(() => window.close(), 3000);
@@ -50711,6 +50719,52 @@ var emailDesigner$3 = ({ strapi: strapi2 }) => ({
50711
50719
  const EMAIL_LOG_UID$1 = "plugin::magic-mail.email-log";
50712
50720
  const EMAIL_EVENT_UID$1 = "plugin::magic-mail.email-event";
50713
50721
  const EMAIL_ACCOUNT_UID$1 = "plugin::magic-mail.email-account";
50722
+ const escapeHtml = (value) => String(value ?? "").replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
50723
+ const renderTrackingFallbackHtml = (reason, fallbackUrl) => {
50724
+ const safeReason = escapeHtml(reason);
50725
+ const safeUrl = fallbackUrl ? escapeHtml(fallbackUrl) : "";
50726
+ const refreshMeta = fallbackUrl ? `<meta http-equiv="refresh" content="3;url=${safeUrl}">` : "";
50727
+ const manualLink = fallbackUrl ? `<p style="margin-top:24px;font-size:14px;color:#6b7280;">
50728
+ You will be redirected in a few seconds. If nothing happens,
50729
+ <a href="${safeUrl}" style="color:#4f46e5;">click here</a>.
50730
+ </p>` : `<p style="margin-top:24px;font-size:14px;color:#6b7280;">
50731
+ You can safely close this tab.
50732
+ </p>`;
50733
+ return `<!doctype html>
50734
+ <html lang="en">
50735
+ <head>
50736
+ <meta charset="utf-8">
50737
+ <meta name="viewport" content="width=device-width,initial-scale=1">
50738
+ ${refreshMeta}
50739
+ <title>Link unavailable</title>
50740
+ <style>
50741
+ body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; background:#f9fafb; margin:0; padding:40px 20px; color:#111827; }
50742
+ .card { max-width: 480px; margin: 60px auto; background:#fff; border-radius: 12px; padding: 32px; box-shadow: 0 1px 3px rgba(0,0,0,0.08); text-align:center; }
50743
+ h1 { font-size: 22px; margin: 0 0 12px; }
50744
+ p { font-size: 15px; line-height: 1.5; color:#374151; }
50745
+ </style>
50746
+ </head>
50747
+ <body>
50748
+ <main class="card">
50749
+ <h1>This link is no longer available</h1>
50750
+ <p>${safeReason}</p>
50751
+ ${manualLink}
50752
+ </main>
50753
+ </body>
50754
+ </html>`;
50755
+ };
50756
+ const respondWithTrackingFallback = async (ctx, reason) => {
50757
+ let fallbackUrl = null;
50758
+ try {
50759
+ const settings = await strapi.plugin("magic-mail").service("plugin-settings").getSettings();
50760
+ fallbackUrl = settings?.trackingFallbackUrl || null;
50761
+ } catch (err) {
50762
+ strapi.log.debug(`[magic-mail] Could not load tracking fallback setting: ${err.message}`);
50763
+ }
50764
+ ctx.status = 410;
50765
+ ctx.type = "text/html; charset=utf-8";
50766
+ ctx.body = renderTrackingFallbackHtml(reason, fallbackUrl);
50767
+ };
50714
50768
  var analytics$3 = ({ strapi: strapi2 }) => ({
50715
50769
  /**
50716
50770
  * Tracking pixel endpoint
@@ -50731,7 +50785,18 @@ var analytics$3 = ({ strapi: strapi2 }) => ({
50731
50785
  ctx.body = pixel;
50732
50786
  },
50733
50787
  /**
50734
- * Click tracking endpoint with open-redirect protection
50788
+ * Click tracking endpoint with open-redirect protection.
50789
+ *
50790
+ * Resolves the destination URL exclusively from the database — never
50791
+ * from query parameters — so the endpoint cannot be used as an open
50792
+ * redirect. When the URL is no longer resolvable (retention cleanup
50793
+ * deleted the row, the hash is wrong, the stored URL is malformed),
50794
+ * the end-user used to receive a Strapi JSON error envelope, which is
50795
+ * the single biggest UX regression in any tracking setup. Now the
50796
+ * user sees a branded HTML fallback page that either redirects to the
50797
+ * admin-configured `trackingFallbackUrl` or apologises and invites
50798
+ * them to close the tab.
50799
+ *
50735
50800
  * GET /magic-mail/track/click/:emailId/:linkHash/:recipientHash
50736
50801
  */
50737
50802
  async trackClick(ctx) {
@@ -50744,15 +50809,25 @@ var analytics$3 = ({ strapi: strapi2 }) => ({
50744
50809
  strapi2.log.error("[magic-mail] Error getting original URL:", err.message);
50745
50810
  }
50746
50811
  if (!url) {
50747
- return ctx.badRequest("Invalid or expired tracking link");
50812
+ return respondWithTrackingFallback(
50813
+ ctx,
50814
+ "The page behind this link is no longer tracked. It may have been removed by our retention policy."
50815
+ );
50748
50816
  }
50749
50817
  try {
50750
50818
  const parsed = new URL(url);
50751
50819
  if (!["http:", "https:"].includes(parsed.protocol)) {
50752
- return ctx.badRequest("Invalid URL protocol");
50820
+ strapi2.log.warn(`[magic-mail] Blocked non-http(s) tracking URL for email ${emailId}`);
50821
+ return respondWithTrackingFallback(
50822
+ ctx,
50823
+ "This link points to an unsupported destination and cannot be opened for your safety."
50824
+ );
50753
50825
  }
50754
50826
  } catch {
50755
- return ctx.badRequest("Invalid URL format");
50827
+ return respondWithTrackingFallback(
50828
+ ctx,
50829
+ "This link is no longer valid."
50830
+ );
50756
50831
  }
50757
50832
  try {
50758
50833
  await strapi2.plugin("magic-mail").service("analytics").recordClick(emailId, linkHash, recipientHash, url, ctx.request);
@@ -63354,7 +63429,7 @@ var oauth$1 = ({ strapi: strapi2 }) => ({
63354
63429
  return account;
63355
63430
  }
63356
63431
  });
63357
- const version = "2.9.0";
63432
+ const version = "2.9.1";
63358
63433
  const require$$2 = {
63359
63434
  version
63360
63435
  };
@@ -65238,7 +65313,8 @@ var pluginSettings$1 = ({ strapi: strapi2 }) => ({
65238
65313
  defaultFromName: null,
65239
65314
  defaultFromEmail: null,
65240
65315
  unsubscribeUrl: null,
65241
- enableUnsubscribeHeader: true
65316
+ enableUnsubscribeHeader: true,
65317
+ trackingFallbackUrl: null
65242
65318
  }
65243
65319
  });
65244
65320
  strapi2.log.info("[magic-mail] [SETTINGS] Created default plugin settings");
@@ -65253,7 +65329,8 @@ var pluginSettings$1 = ({ strapi: strapi2 }) => ({
65253
65329
  defaultFromName: null,
65254
65330
  defaultFromEmail: null,
65255
65331
  unsubscribeUrl: null,
65256
- enableUnsubscribeHeader: true
65332
+ enableUnsubscribeHeader: true,
65333
+ trackingFallbackUrl: null
65257
65334
  };
65258
65335
  }
65259
65336
  },
@@ -65269,7 +65346,8 @@ var pluginSettings$1 = ({ strapi: strapi2 }) => ({
65269
65346
  trackingBaseUrl: data.trackingBaseUrl?.trim() || null,
65270
65347
  defaultFromName: data.defaultFromName?.trim() || null,
65271
65348
  defaultFromEmail: data.defaultFromEmail?.trim()?.toLowerCase() || null,
65272
- unsubscribeUrl: data.unsubscribeUrl?.trim() || null
65349
+ unsubscribeUrl: data.unsubscribeUrl?.trim() || null,
65350
+ trackingFallbackUrl: data.trackingFallbackUrl?.trim() || null
65273
65351
  };
65274
65352
  let settings = await strapi2.documents(SETTINGS_UID).findFirst({});
65275
65353
  if (settings) {
@@ -65287,7 +65365,8 @@ var pluginSettings$1 = ({ strapi: strapi2 }) => ({
65287
65365
  defaultFromName: sanitizedData.defaultFromName,
65288
65366
  defaultFromEmail: sanitizedData.defaultFromEmail,
65289
65367
  unsubscribeUrl: sanitizedData.unsubscribeUrl,
65290
- enableUnsubscribeHeader: sanitizedData.enableUnsubscribeHeader ?? true
65368
+ enableUnsubscribeHeader: sanitizedData.enableUnsubscribeHeader ?? true,
65369
+ trackingFallbackUrl: sanitizedData.trackingFallbackUrl
65291
65370
  }
65292
65371
  });
65293
65372
  strapi2.log.info("[magic-mail] [SETTINGS] Created plugin settings");
@@ -971,6 +971,10 @@ const attributes = {
971
971
  type: "boolean",
972
972
  "default": true,
973
973
  configurable: false
974
+ },
975
+ trackingFallbackUrl: {
976
+ type: "string",
977
+ configurable: false
974
978
  }
975
979
  };
976
980
  const require$$7 = {
@@ -48848,7 +48852,11 @@ const schemas = {
48848
48852
  defaultFromName: headerSafe.optional(),
48849
48853
  defaultFromEmail: emailString.optional().or(z2.literal("")),
48850
48854
  unsubscribeUrl: z2.string().url().optional().or(z2.literal("")),
48851
- enableUnsubscribeHeader: z2.boolean().optional()
48855
+ enableUnsubscribeHeader: z2.boolean().optional(),
48856
+ // Where to redirect the recipient when a tracking link is no longer
48857
+ // resolvable (e.g. retention cleanup removed the row). If empty the
48858
+ // tracker renders a static HTML fallback page instead.
48859
+ trackingFallbackUrl: z2.string().url().optional().or(z2.literal(""))
48852
48860
  }),
48853
48861
  // ── Content-API send payloads ───────────────────────────────────────────
48854
48862
  // Bounded attachments to prevent OOM from huge base64 payloads.
@@ -49308,7 +49316,7 @@ var oauthState = {
49308
49316
  verifyAndConsumeState: verifyAndConsumeState$1
49309
49317
  };
49310
49318
  const { createState, verifyAndConsumeState } = oauthState;
49311
- function escapeHtml(str2) {
49319
+ function escapeHtml$1(str2) {
49312
49320
  return String(str2 || "").replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#039;");
49313
49321
  }
49314
49322
  function escapeJs(str2) {
@@ -49373,7 +49381,7 @@ var oauth$3 = {
49373
49381
  </head>
49374
49382
  <body>
49375
49383
  <div class="error">[ERROR] OAuth Authorization Failed</div>
49376
- <p>Error: ${escapeHtml(error2)}</p>
49384
+ <p>Error: ${escapeHtml$1(error2)}</p>
49377
49385
  <p>You can close this window and try again.</p>
49378
49386
  <script>
49379
49387
  setTimeout(() => window.close(), 3000);
@@ -49484,7 +49492,7 @@ var oauth$3 = {
49484
49492
  </head>
49485
49493
  <body>
49486
49494
  <div class="error">[ERROR] OAuth Authorization Failed</div>
49487
- <p>Error: ${escapeHtml(error2)}</p>
49495
+ <p>Error: ${escapeHtml$1(error2)}</p>
49488
49496
  <p>You can close this window and try again.</p>
49489
49497
  <script>
49490
49498
  setTimeout(() => window.close(), 3000);
@@ -49592,7 +49600,7 @@ var oauth$3 = {
49592
49600
  </head>
49593
49601
  <body>
49594
49602
  <div class="error">[ERROR] OAuth Authorization Failed</div>
49595
- <p>Error: ${escapeHtml(error2)}</p>
49603
+ <p>Error: ${escapeHtml$1(error2)}</p>
49596
49604
  <p>You can close this window and try again.</p>
49597
49605
  <script>
49598
49606
  setTimeout(() => window.close(), 3000);
@@ -50675,6 +50683,52 @@ var emailDesigner$3 = ({ strapi: strapi2 }) => ({
50675
50683
  const EMAIL_LOG_UID$1 = "plugin::magic-mail.email-log";
50676
50684
  const EMAIL_EVENT_UID$1 = "plugin::magic-mail.email-event";
50677
50685
  const EMAIL_ACCOUNT_UID$1 = "plugin::magic-mail.email-account";
50686
+ const escapeHtml = (value) => String(value ?? "").replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
50687
+ const renderTrackingFallbackHtml = (reason, fallbackUrl) => {
50688
+ const safeReason = escapeHtml(reason);
50689
+ const safeUrl = fallbackUrl ? escapeHtml(fallbackUrl) : "";
50690
+ const refreshMeta = fallbackUrl ? `<meta http-equiv="refresh" content="3;url=${safeUrl}">` : "";
50691
+ const manualLink = fallbackUrl ? `<p style="margin-top:24px;font-size:14px;color:#6b7280;">
50692
+ You will be redirected in a few seconds. If nothing happens,
50693
+ <a href="${safeUrl}" style="color:#4f46e5;">click here</a>.
50694
+ </p>` : `<p style="margin-top:24px;font-size:14px;color:#6b7280;">
50695
+ You can safely close this tab.
50696
+ </p>`;
50697
+ return `<!doctype html>
50698
+ <html lang="en">
50699
+ <head>
50700
+ <meta charset="utf-8">
50701
+ <meta name="viewport" content="width=device-width,initial-scale=1">
50702
+ ${refreshMeta}
50703
+ <title>Link unavailable</title>
50704
+ <style>
50705
+ body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; background:#f9fafb; margin:0; padding:40px 20px; color:#111827; }
50706
+ .card { max-width: 480px; margin: 60px auto; background:#fff; border-radius: 12px; padding: 32px; box-shadow: 0 1px 3px rgba(0,0,0,0.08); text-align:center; }
50707
+ h1 { font-size: 22px; margin: 0 0 12px; }
50708
+ p { font-size: 15px; line-height: 1.5; color:#374151; }
50709
+ </style>
50710
+ </head>
50711
+ <body>
50712
+ <main class="card">
50713
+ <h1>This link is no longer available</h1>
50714
+ <p>${safeReason}</p>
50715
+ ${manualLink}
50716
+ </main>
50717
+ </body>
50718
+ </html>`;
50719
+ };
50720
+ const respondWithTrackingFallback = async (ctx, reason) => {
50721
+ let fallbackUrl = null;
50722
+ try {
50723
+ const settings = await strapi.plugin("magic-mail").service("plugin-settings").getSettings();
50724
+ fallbackUrl = settings?.trackingFallbackUrl || null;
50725
+ } catch (err) {
50726
+ strapi.log.debug(`[magic-mail] Could not load tracking fallback setting: ${err.message}`);
50727
+ }
50728
+ ctx.status = 410;
50729
+ ctx.type = "text/html; charset=utf-8";
50730
+ ctx.body = renderTrackingFallbackHtml(reason, fallbackUrl);
50731
+ };
50678
50732
  var analytics$3 = ({ strapi: strapi2 }) => ({
50679
50733
  /**
50680
50734
  * Tracking pixel endpoint
@@ -50695,7 +50749,18 @@ var analytics$3 = ({ strapi: strapi2 }) => ({
50695
50749
  ctx.body = pixel;
50696
50750
  },
50697
50751
  /**
50698
- * Click tracking endpoint with open-redirect protection
50752
+ * Click tracking endpoint with open-redirect protection.
50753
+ *
50754
+ * Resolves the destination URL exclusively from the database — never
50755
+ * from query parameters — so the endpoint cannot be used as an open
50756
+ * redirect. When the URL is no longer resolvable (retention cleanup
50757
+ * deleted the row, the hash is wrong, the stored URL is malformed),
50758
+ * the end-user used to receive a Strapi JSON error envelope, which is
50759
+ * the single biggest UX regression in any tracking setup. Now the
50760
+ * user sees a branded HTML fallback page that either redirects to the
50761
+ * admin-configured `trackingFallbackUrl` or apologises and invites
50762
+ * them to close the tab.
50763
+ *
50699
50764
  * GET /magic-mail/track/click/:emailId/:linkHash/:recipientHash
50700
50765
  */
50701
50766
  async trackClick(ctx) {
@@ -50708,15 +50773,25 @@ var analytics$3 = ({ strapi: strapi2 }) => ({
50708
50773
  strapi2.log.error("[magic-mail] Error getting original URL:", err.message);
50709
50774
  }
50710
50775
  if (!url) {
50711
- return ctx.badRequest("Invalid or expired tracking link");
50776
+ return respondWithTrackingFallback(
50777
+ ctx,
50778
+ "The page behind this link is no longer tracked. It may have been removed by our retention policy."
50779
+ );
50712
50780
  }
50713
50781
  try {
50714
50782
  const parsed = new URL(url);
50715
50783
  if (!["http:", "https:"].includes(parsed.protocol)) {
50716
- return ctx.badRequest("Invalid URL protocol");
50784
+ strapi2.log.warn(`[magic-mail] Blocked non-http(s) tracking URL for email ${emailId}`);
50785
+ return respondWithTrackingFallback(
50786
+ ctx,
50787
+ "This link points to an unsupported destination and cannot be opened for your safety."
50788
+ );
50717
50789
  }
50718
50790
  } catch {
50719
- return ctx.badRequest("Invalid URL format");
50791
+ return respondWithTrackingFallback(
50792
+ ctx,
50793
+ "This link is no longer valid."
50794
+ );
50720
50795
  }
50721
50796
  try {
50722
50797
  await strapi2.plugin("magic-mail").service("analytics").recordClick(emailId, linkHash, recipientHash, url, ctx.request);
@@ -63318,7 +63393,7 @@ var oauth$1 = ({ strapi: strapi2 }) => ({
63318
63393
  return account;
63319
63394
  }
63320
63395
  });
63321
- const version = "2.9.0";
63396
+ const version = "2.9.1";
63322
63397
  const require$$2 = {
63323
63398
  version
63324
63399
  };
@@ -65202,7 +65277,8 @@ var pluginSettings$1 = ({ strapi: strapi2 }) => ({
65202
65277
  defaultFromName: null,
65203
65278
  defaultFromEmail: null,
65204
65279
  unsubscribeUrl: null,
65205
- enableUnsubscribeHeader: true
65280
+ enableUnsubscribeHeader: true,
65281
+ trackingFallbackUrl: null
65206
65282
  }
65207
65283
  });
65208
65284
  strapi2.log.info("[magic-mail] [SETTINGS] Created default plugin settings");
@@ -65217,7 +65293,8 @@ var pluginSettings$1 = ({ strapi: strapi2 }) => ({
65217
65293
  defaultFromName: null,
65218
65294
  defaultFromEmail: null,
65219
65295
  unsubscribeUrl: null,
65220
- enableUnsubscribeHeader: true
65296
+ enableUnsubscribeHeader: true,
65297
+ trackingFallbackUrl: null
65221
65298
  };
65222
65299
  }
65223
65300
  },
@@ -65233,7 +65310,8 @@ var pluginSettings$1 = ({ strapi: strapi2 }) => ({
65233
65310
  trackingBaseUrl: data.trackingBaseUrl?.trim() || null,
65234
65311
  defaultFromName: data.defaultFromName?.trim() || null,
65235
65312
  defaultFromEmail: data.defaultFromEmail?.trim()?.toLowerCase() || null,
65236
- unsubscribeUrl: data.unsubscribeUrl?.trim() || null
65313
+ unsubscribeUrl: data.unsubscribeUrl?.trim() || null,
65314
+ trackingFallbackUrl: data.trackingFallbackUrl?.trim() || null
65237
65315
  };
65238
65316
  let settings = await strapi2.documents(SETTINGS_UID).findFirst({});
65239
65317
  if (settings) {
@@ -65251,7 +65329,8 @@ var pluginSettings$1 = ({ strapi: strapi2 }) => ({
65251
65329
  defaultFromName: sanitizedData.defaultFromName,
65252
65330
  defaultFromEmail: sanitizedData.defaultFromEmail,
65253
65331
  unsubscribeUrl: sanitizedData.unsubscribeUrl,
65254
- enableUnsubscribeHeader: sanitizedData.enableUnsubscribeHeader ?? true
65332
+ enableUnsubscribeHeader: sanitizedData.enableUnsubscribeHeader ?? true,
65333
+ trackingFallbackUrl: sanitizedData.trackingFallbackUrl
65255
65334
  }
65256
65335
  });
65257
65336
  strapi2.log.info("[magic-mail] [SETTINGS] Created plugin settings");
package/package.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "2.9.1",
2
+ "version": "2.9.2",
3
3
  "keywords": [
4
4
  "strapi",
5
5
  "strapi-plugin",