Proper email attachment handling
This commit is contained in:
21
package-lock.json
generated
21
package-lock.json
generated
@@ -12,7 +12,8 @@
|
|||||||
"@supabase/supabase-js": "^2.50.0",
|
"@supabase/supabase-js": "^2.50.0",
|
||||||
"@sveltejs/adapter-node": "^5.2.12",
|
"@sveltejs/adapter-node": "^5.2.12",
|
||||||
"googleapis": "^150.0.1",
|
"googleapis": "^150.0.1",
|
||||||
"papaparse": "^5.5.3"
|
"papaparse": "^5.5.3",
|
||||||
|
"quoted-printable": "^1.0.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@sveltejs/kit": "^2.16.0",
|
"@sveltejs/kit": "^2.16.0",
|
||||||
@@ -2691,6 +2692,18 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/quoted-printable": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/quoted-printable/-/quoted-printable-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-cihC68OcGiQOjGiXuo5Jk6XHANTHl1K4JLk/xlEJRTIXfy19Sg6XzB95XonYgr+1rB88bCpr7WZE7D7AlZow4g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"utf8": "^2.1.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"quoted-printable": "bin/quoted-printable"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/readdirp": {
|
"node_modules/readdirp": {
|
||||||
"version": "4.1.2",
|
"version": "4.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
|
||||||
@@ -3050,6 +3063,12 @@
|
|||||||
"integrity": "sha512-XdVKMF4SJ0nP/O7XIPB0JwAEuT9lDIYnNsK8yGVe43y0AWoKeJNdv3ZNWh7ksJ6KqQFjOO6ox/VEitLnaVNufw==",
|
"integrity": "sha512-XdVKMF4SJ0nP/O7XIPB0JwAEuT9lDIYnNsK8yGVe43y0AWoKeJNdv3ZNWh7ksJ6KqQFjOO6ox/VEitLnaVNufw==",
|
||||||
"license": "BSD"
|
"license": "BSD"
|
||||||
},
|
},
|
||||||
|
"node_modules/utf8": {
|
||||||
|
"version": "2.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/utf8/-/utf8-2.1.2.tgz",
|
||||||
|
"integrity": "sha512-QXo+O/QkLP/x1nyi54uQiG0XrODxdysuQvE5dtVqv7F5K2Qb6FsN+qbr6KhF5wQ20tfcV3VQp0/2x1e1MRSPWg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/util-deprecate": {
|
"node_modules/util-deprecate": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||||
|
|||||||
@@ -33,6 +33,7 @@
|
|||||||
"@supabase/supabase-js": "^2.50.0",
|
"@supabase/supabase-js": "^2.50.0",
|
||||||
"@sveltejs/adapter-node": "^5.2.12",
|
"@sveltejs/adapter-node": "^5.2.12",
|
||||||
"googleapis": "^150.0.1",
|
"googleapis": "^150.0.1",
|
||||||
"papaparse": "^5.5.3"
|
"papaparse": "^5.5.3",
|
||||||
|
"quoted-printable": "^1.0.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { google } from 'googleapis';
|
import { google } from 'googleapis';
|
||||||
import { env } from '$env/dynamic/private';
|
import { env } from '$env/dynamic/private';
|
||||||
|
import quotedPrintable from 'quoted-printable'; // tiny, zero-dep package
|
||||||
|
|
||||||
export const scopes = ['https://www.googleapis.com/auth/gmail.send'];
|
export const scopes = ['https://www.googleapis.com/auth/gmail.send'];
|
||||||
|
|
||||||
@@ -27,51 +28,73 @@ export async function exchangeCodeForTokens(code: string) {
|
|||||||
|
|
||||||
export async function sendGmail(
|
export async function sendGmail(
|
||||||
refreshToken: string,
|
refreshToken: string,
|
||||||
{ to, subject, text }: { to: string; subject: string; text: string }
|
{ to, subject, text, qr_code }: { to: string; subject: string; text: string; qr_code: string }
|
||||||
) {
|
) {
|
||||||
const oauth = getOAuthClient();
|
const oauth = getOAuthClient();
|
||||||
oauth.setCredentials({ refresh_token: refreshToken });
|
oauth.setCredentials({ refresh_token: refreshToken });
|
||||||
|
|
||||||
const gmail = google.gmail({ version: 'v1', auth: oauth });
|
const gmail = google.gmail({ version: 'v1', auth: oauth });
|
||||||
|
|
||||||
const wrappedHtml = `<!DOCTYPE html>
|
const message_html =
|
||||||
|
`<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>${subject}</title>
|
|
||||||
<link href="https://fonts.googleapis.com/css?family=Lato:400,700&display=swap" rel="stylesheet">
|
|
||||||
<style>
|
<style>
|
||||||
body { font-family: 'Lato', Arial, sans-serif; margin: 0; padding: 0; background: #f9f9f9; }
|
@import url('https://fonts.googleapis.com/css2?family=Lato&display=swap');
|
||||||
.container {
|
|
||||||
max-width: 480px;
|
|
||||||
margin: 0 auto;
|
|
||||||
background: #fff;
|
|
||||||
border-radius: 8px;
|
|
||||||
box-shadow: 0 2px 8px rgba(0,0,0,0.04);
|
|
||||||
padding: 24px 16px;
|
|
||||||
word-break: break-word;
|
|
||||||
}
|
|
||||||
@media (max-width: 600px) {
|
|
||||||
.container { padding: 16px 4px; }
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body style="font-family: 'Lato', sans-serif; background-color: #f9f9f9; padding: 20px; margin: 0;">
|
||||||
<div class="container">${text}</div>
|
<div style="max-width: 600px; margin: auto; background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.05);">
|
||||||
|
<p style="white-space: pre-line;font-size: 16px; line-height: 1.5; color: #333;">${text}</p>
|
||||||
|
<img src="cid:qrCode1" alt="QR Code" style="display: block; margin: 20px auto; max-width: 50%; min-width: 200px; height: auto;" />
|
||||||
|
<div style="width: 100%; display: flex; flex-direction: row; justify-content: space-between">
|
||||||
|
<div style="height: 4px; width: 20%; background: #00aeef;"></div>
|
||||||
|
<div style="height: 4px; width: 20%; background: #ec008c;"></div>
|
||||||
|
<div style="height: 4px; width: 20%; background: #7ac143;"></div>
|
||||||
|
<div style="height: 4px; width: 20%; background: #f47b20;"></div>
|
||||||
|
<div style="height: 4px; width: 20%; background: #2e3192;"></div>
|
||||||
|
</div>
|
||||||
|
<div style="font-size: 12px; color: #999; padding-top: 0px; margin-top: 10px; line-height: 1.5; ">
|
||||||
|
<p>This email has been generated with the help of *insert software name*</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>`;
|
</html>`;
|
||||||
|
|
||||||
const message = [
|
|
||||||
`To: ${to}`,
|
|
||||||
'Content-Type: text/html; charset="UTF-8"',
|
|
||||||
'Content-Transfer-Encoding: 7bit',
|
|
||||||
`Subject: ${subject}`,
|
|
||||||
'',
|
|
||||||
wrappedHtml
|
|
||||||
].join('\n');
|
|
||||||
|
|
||||||
const raw = Buffer.from(message).toString('base64url');
|
const boundary = 'BOUNDARY';
|
||||||
|
const nl = '\r\n'; // RFC-5322 line ending
|
||||||
|
|
||||||
|
const htmlQP = quotedPrintable.encode(message_html);
|
||||||
|
const qrLines = qr_code.replace(/.{1,76}/g, '$&' + nl);
|
||||||
|
|
||||||
|
const rawParts = [
|
||||||
|
'MIME-Version: 1.0',
|
||||||
|
`To: ${to}`,
|
||||||
|
`Subject: ${subject}`,
|
||||||
|
`Content-Type: multipart/related; boundary="${boundary}"`,
|
||||||
|
'',
|
||||||
|
`--${boundary}`,
|
||||||
|
'Content-Type: text/html; charset="UTF-8"',
|
||||||
|
'Content-Transfer-Encoding: quoted-printable',
|
||||||
|
'',
|
||||||
|
htmlQP,
|
||||||
|
'',
|
||||||
|
`--${boundary}`,
|
||||||
|
'Content-Type: image/png',
|
||||||
|
'Content-Transfer-Encoding: base64',
|
||||||
|
'Content-ID: <qrCode1>',
|
||||||
|
'Content-Disposition: inline; filename="qr.png"',
|
||||||
|
'',
|
||||||
|
qrLines,
|
||||||
|
'',
|
||||||
|
`--${boundary}--`,
|
||||||
|
''
|
||||||
|
];
|
||||||
|
|
||||||
|
const rawMessage = rawParts.join(nl);
|
||||||
|
|
||||||
|
const raw = Buffer.from(rawMessage).toString('base64url');
|
||||||
|
|
||||||
await gmail.users.messages.send({
|
await gmail.users.messages.send({
|
||||||
userId: 'me',
|
userId: 'me',
|
||||||
|
|||||||
@@ -36,13 +36,13 @@ export const GET: RequestHandler = async ({ url }) => {
|
|||||||
|
|
||||||
/* ───────────── POST ───────────── */
|
/* ───────────── POST ───────────── */
|
||||||
export const POST: RequestHandler = async ({ request }) => {
|
export const POST: RequestHandler = async ({ request }) => {
|
||||||
const { action, refreshToken, to, subject, text } = await request.json();
|
const { action, refreshToken, to, subject, text, qr_code } = await request.json();
|
||||||
|
|
||||||
/* send e-mail */
|
/* send e-mail */
|
||||||
if (action === 'send') {
|
if (action === 'send') {
|
||||||
if (!refreshToken) return new Response('Missing token', { status: 401 });
|
if (!refreshToken) return new Response('Missing token', { status: 401 });
|
||||||
try {
|
try {
|
||||||
await sendGmail(refreshToken, { to, subject, text });
|
await sendGmail(refreshToken, { to, subject, text, qr_code });
|
||||||
return json({ ok: true });
|
return json({ ok: true });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
return new Response((err as Error).message, { status: 500 });
|
return new Response((err as Error).message, { status: 500 });
|
||||||
|
|||||||
Reference in New Issue
Block a user