Skip to content

Commit ba6fd9e

Browse files
authored
Merge pull request #2640 from ankaboot-source/docs/update-readme-features
feat(campaigns): refresh OAuth tokens before verifying sender availability
2 parents 1b4f727 + b2a3996 commit ba6fd9e

File tree

2 files changed

+56
-24
lines changed

2 files changed

+56
-24
lines changed

supabase/functions/email-campaigns/index.ts

Lines changed: 28 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ const PUBLIC_CAMPAIGN_BASE_URL = resolveCampaignBaseUrlFromEnv((key) =>
3333
Deno.env.get(key),
3434
);
3535
const SMTP_USER = normalizeEmail(Deno.env.get("SMTP_USER") || "");
36+
const LEADMINER_FRONTEND_HOST = Deno.env.get("LEADMINER_FRONTEND_HOST") || "";
3637
const DEFAULT_SENDER_DAILY_LIMIT = 1000;
3738
const MAX_SENDER_DAILY_LIMIT = 2000;
3839
const PROCESSING_BATCH_SIZE = 300;
@@ -284,8 +285,13 @@ function extractErrorMessage(error: unknown): string {
284285

285286
function toTextFromHtml(html: string): string {
286287
return html
288+
.replace(/<br\s*\/?>/gi, "\n")
289+
.replace(/<\/p>/gi, "\n\n")
290+
.replace(/<\/div>/gi, "\n\n")
291+
.replace(/<\/li>/gi, "\n")
287292
.replace(/<[^>]*>/g, " ")
288-
.replace(/\s+/g, " ")
293+
.replace(/\n{3,}/g, "\n\n")
294+
.replace(/[ \t]+/g, " ")
289295
.trim();
290296
}
291297

@@ -577,6 +583,7 @@ async function resolveSenderOptions(authorization: string, userEmail: string) {
577583
await getUserMiningSources(authorization),
578584
);
579585

586+
// Refresh expired OAuth tokens
580587
for (let i = 0; i < sources.length; i++) {
581588
const source = sources[i];
582589
const credentialIssue = getSenderCredentialIssue(source);
@@ -1130,7 +1137,7 @@ app.post("/campaigns/preview", authMiddleware, async (c: Context) => {
11301137
),
11311138
footerTextTemplate,
11321139
ownerEmail,
1133-
unsubscribeUrl: buildUnsubscribeUrl(crypto.randomUUID()),
1140+
unsubscribeUrl: buildUnsubscribeUrl("preview-unsubscribe"),
11341141
senderName,
11351142
plainTextOnly,
11361143
});
@@ -1768,6 +1775,16 @@ app.get("/unsubscribe/:token", async (c: Context) => {
17681775
const token = c.req.param("token");
17691776
const supabaseAdmin = createSupabaseAdmin();
17701777

1778+
if (token === "preview-unsubscribe") {
1779+
return new Response(null, {
1780+
status: 302,
1781+
headers: {
1782+
...corsHeaders,
1783+
Location: `${LEADMINER_FRONTEND_HOST}/unsubscribe/success?preview=true`,
1784+
},
1785+
});
1786+
}
1787+
17711788
const { data: recipient, error } = await supabaseAdmin
17721789
.schema("private")
17731790
.from("email_campaign_recipients")
@@ -1776,16 +1793,13 @@ app.get("/unsubscribe/:token", async (c: Context) => {
17761793
.single();
17771794

17781795
if (error || !recipient) {
1779-
return new Response(
1780-
"<html><body><h1>Invalid unsubscribe link</h1><p>This link is not valid anymore.</p></body></html>",
1781-
{
1782-
status: 404,
1783-
headers: {
1784-
...corsHeaders,
1785-
"Content-Type": "text/html; charset=utf-8",
1786-
},
1796+
return new Response(null, {
1797+
status: 302,
1798+
headers: {
1799+
...corsHeaders,
1800+
Location: `${LEADMINER_FRONTEND_HOST}/unsubscribe/failure`,
17871801
},
1788-
);
1802+
});
17891803
}
17901804

17911805
await supabaseAdmin
@@ -1812,12 +1826,11 @@ app.get("/unsubscribe/:token", async (c: Context) => {
18121826
event_type: "unsubscribe",
18131827
});
18141828

1815-
const html =
1816-
"<html><body><h1>You are unsubscribed</h1><p>You will no longer receive campaign emails from this sender.</p></body></html>";
1817-
return new Response(html, {
1829+
return new Response(null, {
1830+
status: 302,
18181831
headers: {
18191832
...corsHeaders,
1820-
"Content-Type": "text/html; charset=utf-8",
1833+
Location: `${LEADMINER_FRONTEND_HOST}/unsubscribe/success`,
18211834
},
18221835
});
18231836
});

supabase/functions/email-campaigns/sender-options.ts

Lines changed: 28 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -26,15 +26,16 @@ export async function refreshOAuthToken(
2626

2727
if (kind === "google") {
2828
tokenUrl = "https://oauth2.googleapis.com/token";
29-
clientId = Deno.env.get("GOOGLE_OAUTH_CLIENT_ID") || "";
30-
clientSecret = Deno.env.get("GOOGLE_OAUTH_CLIENT_SECRET") || "";
29+
clientId = Deno.env.get("GOOGLE_CLIENT_ID") || "";
30+
clientSecret = Deno.env.get("GOOGLE_SECRET") || "";
3131
} else {
3232
tokenUrl = "https://login.microsoftonline.com/common/oauth2/v2.0/token";
33-
clientId = Deno.env.get("AZURE_OAUTH_CLIENT_ID") || "";
34-
clientSecret = Deno.env.get("AZURE_OAUTH_CLIENT_SECRET") || "";
33+
clientId = Deno.env.get("AZURE_CLIENT_ID") || "";
34+
clientSecret = Deno.env.get("AZURE_SECRET") || "";
3535
}
3636

3737
if (!clientId || !clientSecret) {
38+
console.warn("OAuth client ID or secret not configured");
3839
return null;
3940
}
4041

@@ -45,11 +46,6 @@ export async function refreshOAuthToken(
4546
grant_type: "refresh_token",
4647
});
4748

48-
const expiresAtInput = Number(source.credentials.expiresAt);
49-
if (!Number.isFinite(expiresAtInput) || expiresAtInput <= 0) {
50-
console.warn("Missing or invalid expiresAt in source credentials");
51-
}
52-
5349
try {
5450
const response = await fetch(tokenUrl, {
5551
method: "POST",
@@ -88,6 +84,29 @@ export async function refreshOAuthToken(
8884
}
8985
}
9086

87+
export async function updateMiningSourceCredentials(
88+
supabaseAdmin: ReturnType<typeof createSupabaseAdmin>,
89+
email: string,
90+
credentials: Record<string, unknown>,
91+
): Promise<boolean> {
92+
if (!email || typeof email !== "string") {
93+
console.error("Invalid email provided to updateMiningSourceCredentials");
94+
return false;
95+
}
96+
97+
const { error } = await supabaseAdmin
98+
.from("mining_sources")
99+
.update({ credentials })
100+
.eq("email", email);
101+
102+
if (error) {
103+
console.error("Failed to update mining source credentials:", error);
104+
return false;
105+
}
106+
107+
return true;
108+
}
109+
91110
export function listUniqueSenderSources(
92111
sources: MiningSourceCredential[],
93112
): MiningSourceCredential[] {

0 commit comments

Comments
 (0)