Skip to content

Commit 490d2ec

Browse files
authored
fix: track quoted-printable links and disable tracking for plain text (#2644)
* fix: track quoted-printable links and disable tracking for plain text - Fix click tracking regex to match Quoted-Printable encoded hrefs (href=3D"...") - Add escapeRegExp helper for safe regex replacement - Disable trackClick and trackOpen checkboxes when plain text only is enabled - Add appropriate translations for disabled tracking state * fix: remove unused isPreview variable in unsubscribe/failure.vue * style: fix formatting issues in frontend files
1 parent aecdd70 commit 490d2ec

File tree

6 files changed

+59
-13
lines changed

6 files changed

+59
-13
lines changed

frontend/src/components/campaigns/CampaignComposerDialog.vue

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -243,10 +243,45 @@
243243
</div>
244244

245245
<div class="flex items-center gap-2">
246-
<Checkbox v-model="form.trackClick" binary input-id="track-click" />
247-
<label for="track-click">{{ t('track_click') }}</label>
246+
<Checkbox
247+
v-model="form.trackClick"
248+
binary
249+
input-id="track-click"
250+
:disabled="form.plainTextOnly"
251+
/>
252+
<label
253+
for="track-click"
254+
:class="{ 'opacity-50': form.plainTextOnly }"
255+
>{{ t('track_click') }}</label
256+
>
257+
<i
258+
v-tooltip.top="
259+
form.plainTextOnly
260+
? t('track_disabled_plain_text')
261+
: t('track_click_help')
262+
"
263+
class="pi pi-info-circle text-xs text-surface-500"
264+
/>
265+
</div>
266+
267+
<div class="flex items-center gap-2 md:col-span-2">
268+
<Checkbox
269+
v-model="form.trackOpen"
270+
binary
271+
input-id="track-open"
272+
:disabled="form.plainTextOnly"
273+
/>
274+
<label
275+
for="track-open"
276+
:class="{ 'opacity-50': form.plainTextOnly }"
277+
>{{ t('track_open') }}</label
278+
>
248279
<i
249-
v-tooltip.top="t('track_click_help')"
280+
v-tooltip.top="
281+
form.plainTextOnly
282+
? t('track_disabled_plain_text')
283+
: t('track_open_help')
284+
"
250285
class="pi pi-info-circle text-xs text-surface-500"
251286
/>
252287
</div>
@@ -1101,6 +1136,8 @@ watch(
11011136
(isPlainTextOnly) => {
11021137
if (isPlainTextOnly) {
11031138
form.bodyTextTemplate = normalizeBodyText();
1139+
form.trackClick = false;
1140+
form.trackOpen = false;
11041141
}
11051142
},
11061143
);
@@ -1177,6 +1214,7 @@ watch(
11771214
"track_open_help": "Adds a tracking pixel to measure opens.",
11781215
"track_click": "Track clicking",
11791216
"track_click_help": "Converts links into tracked links for click analytics.",
1217+
"track_disabled_plain_text": "Disabled for plain text emails",
11801218
"plain_text_only": "Send pure text (no HTML)",
11811219
"plain_text_only_help": "Disables rich formatting and sends a plain text email.",
11821220
"footer_template": "Compliance footer",
@@ -1269,6 +1307,7 @@ watch(
12691307
"track_open_help": "Ajoute un pixel de suivi pour mesurer les ouvertures.",
12701308
"track_click": "Suivre les clics",
12711309
"track_click_help": "Transforme les liens pour mesurer les clics.",
1310+
"track_disabled_plain_text": "Désactivé pour les emails texte brut",
12721311
"plain_text_only": "Envoyer en texte brut (sans HTML)",
12731312
"plain_text_only_help": "Désactive la mise en forme riche et envoie un email texte uniquement.",
12741313
"footer_template": "Pied de page conformité",

frontend/src/pages/sources.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -243,7 +243,7 @@ function getIcon(type: string) {
243243
function isActiveMiningSource(source: MiningSource): boolean {
244244
return Boolean(
245245
$leadminer.activeMiningSource?.email === source.email &&
246-
$leadminer.miningTask,
246+
$leadminer.miningTask,
247247
);
248248
}
249249

frontend/src/pages/unsubscribe/failure.vue

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,4 @@
2929
</div>
3030
</template>
3131

32-
<script setup lang="ts">
33-
const $route = useRoute();
34-
const isPreview = Boolean($route.query.preview);
35-
</script>
32+
<script setup lang="ts"></script>

frontend/src/plugins/error-handler.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,8 @@ const EXPECTED_FAULTY_STATUS_CODES = [402];
2929
function isExpectedFaultyCode(err: unknown) {
3030
return Boolean(
3131
isFetchError(err) &&
32-
err.response &&
33-
EXPECTED_FAULTY_STATUS_CODES.includes(err.response.status),
32+
err.response &&
33+
EXPECTED_FAULTY_STATUS_CODES.includes(err.response.status),
3434
);
3535
}
3636

frontend/src/stores/filters.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ const areToggledFilters = computed(
5353
function checkValidStatus(statusValue = filters.value.status.value) {
5454
return Boolean(
5555
statusValue.length === VALID_STATUSES.length &&
56-
statusValue.every((status: string) => VALID_STATUSES.includes(status)),
56+
statusValue.every((status: string) => VALID_STATUSES.includes(status)),
5757
);
5858
}
5959

supabase/functions/email-campaigns/index.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,10 @@ function escapeHtml(value: string): string {
191191
.replaceAll("'", "&#39;");
192192
}
193193

194+
function escapeRegExp(value: string): string {
195+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
196+
}
197+
194198
function requireFallbackSenderEmail() {
195199
if (!SMTP_USER) {
196200
throw new Error("SMTP_USER is not configured");
@@ -895,7 +899,8 @@ async function injectTrackers(
895899
let updatedHtml = html;
896900

897901
if (trackClick) {
898-
const hrefRegex = /href\s*=\s*"([^"]+)"/g;
902+
// Match both href="..." and href=3D"..." (quoted-printable encoded)
903+
const hrefRegex = /href\s*=\s*(?:3D\s*)?["']([^"']+)["']/gi;
899904
const matches = [...updatedHtml.matchAll(hrefRegex)];
900905

901906
for (const match of matches) {
@@ -911,8 +916,13 @@ async function injectTrackers(
911916
originalUrl,
912917
);
913918
const trackedUrl = `${PUBLIC_CAMPAIGN_BASE_URL}/functions/v1/email-campaigns/track/click/${token}`;
919+
920+
// Replace both quoted-printable encoded (href=3D"...") and regular (href="...")
914921
updatedHtml = updatedHtml.replace(
915-
`href="${originalUrl}"`,
922+
new RegExp(
923+
`href\\s*=\\s*(?:3D\\s*)?["']${escapeRegExp(originalUrl)}["']`,
924+
"gi",
925+
),
916926
`href="${trackedUrl}"`,
917927
);
918928
}

0 commit comments

Comments
 (0)