forked from codechefPesuecc/CodeChef-PESUECC-Chapter
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathemail.ts
More file actions
153 lines (138 loc) · 4.97 KB
/
Copy pathemail.ts
File metadata and controls
153 lines (138 loc) · 4.97 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
/**
* Provider-agnostic transactional email.
*
* With no provider key configured it uses the "console" transport — it logs the
* message server-side instead of sending — so the whole OTP flow is testable in
* dev without an external service. Set `RESEND_API_KEY` (and `EMAIL_FROM`) to
* send for real; adding another provider (SES, Postmark, …) is one more branch
* in `sendEmail`.
*/
export interface EmailMessage {
to: string;
subject: string;
text: string;
html?: string;
}
export type EmailTransport = "gmail" | "console";
export function emailTransport(): EmailTransport {
return process.env.GMAIL_REFRESH_TOKEN ? "gmail" : "console";
}
/** True when emails are only logged instead of sent (no provider configured). */
export function isConsoleTransport(): boolean {
return emailTransport() === "console";
}
/**
* Whether it's safe to return a secret (a password-reset link or an OTP) in the
* HTTP response, for local testing without an inbox. ONLY true in development
* with the console transport — never in production, even if no email provider is
* configured. This is the guard that stops reset links / codes from leaking to
* an unauthenticated caller in a misconfigured prod deploy.
*/
export function canRevealSecretInResponse(): boolean {
return isConsoleTransport() && process.env.NODE_ENV !== "production";
}
function encodeBase64Url(str: string): string {
if (typeof btoa !== "undefined") {
// Browser / Edge / Node polyfill
const b64 = btoa(unescape(encodeURIComponent(str)));
return b64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
}
// Fallback for native Node if needed
return Buffer.from(str, "utf-8").toString("base64url");
}
async function getGmailAccessToken(): Promise<string | null> {
const tokenUrl = "https://oauth2.googleapis.com/token";
const params = new URLSearchParams({
client_id: process.env.GMAIL_CLIENT_ID || "",
client_secret: process.env.GMAIL_CLIENT_SECRET || "",
refresh_token: process.env.GMAIL_REFRESH_TOKEN || "",
grant_type: "refresh_token",
});
try {
const res = await fetch(tokenUrl, {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: params.toString(),
});
if (!res.ok) {
console.error("[email] Failed to refresh Gmail token:", res.status, await res.text());
return null;
}
const data = await res.json() as { access_token: string };
return data.access_token;
} catch (err) {
console.error("[email] Error refreshing Gmail token:", err);
return null;
}
}
export async function sendEmail(
msg: EmailMessage,
): Promise<{ ok: boolean; error?: string }> {
if (emailTransport() === "gmail") {
try {
const accessToken = await getGmailAccessToken();
if (!accessToken) {
return { ok: false, error: "Authentication with email provider failed." };
}
const from = process.env.EMAIL_FROM ?? "CodeChef PESUECC <noreply@gmail.com>";
const headers = [
`From: ${from}`,
`To: ${msg.to}`,
`Subject: =?utf-8?B?${encodeBase64Url(msg.subject)}?=`,
"MIME-Version: 1.0",
];
// When HTML is present, send multipart/alternative (plain-text fallback +
// branded HTML) — better rendering coverage and deliverability than
// HTML-only. Otherwise a simple text/plain message.
let rawEmail: string;
if (msg.html) {
const boundary = "=_arena_alt_7c1f2a";
rawEmail = [
...headers,
`Content-Type: multipart/alternative; boundary="${boundary}"`,
"",
`--${boundary}`,
"Content-Type: text/plain; charset=utf-8",
"",
msg.text,
`--${boundary}`,
"Content-Type: text/html; charset=utf-8",
"",
msg.html,
`--${boundary}--`,
"",
].join("\r\n");
} else {
rawEmail = [
...headers,
"Content-Type: text/plain; charset=utf-8",
"",
msg.text,
].join("\r\n");
}
const base64UrlEmail = encodeBase64Url(rawEmail);
const res = await fetch("https://gmail.googleapis.com/gmail/v1/users/me/messages/send", {
method: "POST",
headers: {
Authorization: `Bearer ${accessToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ raw: base64UrlEmail }),
});
if (!res.ok) {
const detail = await res.text().catch(() => "");
console.error("[email] gmail api failed:", res.status, detail);
return { ok: false, error: `Email provider returned ${res.status}.` };
}
return { ok: true };
} catch (e) {
console.error("[email] gmail api error:", e);
return { ok: false, error: "Could not reach the email provider." };
}
}
// console transport (dev): log instead of send.
console.log(
`\n[email:dev] To: ${msg.to}\n[email:dev] Subject: ${msg.subject}\n[email:dev] ${msg.text}\n`,
);
return { ok: true };
}