diff --git a/package-lock.json b/package-lock.json index 7bf4e88..229e9d3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,8 @@ "@supabase/supabase-js": "^2.50.0", "@sveltejs/adapter-node": "^5.2.12", "googleapis": "^150.0.1", - "papaparse": "^5.5.3" + "papaparse": "^5.5.3", + "quoted-printable": "^1.0.1" }, "devDependencies": { "@sveltejs/kit": "^2.16.0", @@ -2691,6 +2692,18 @@ "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": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", @@ -3050,6 +3063,12 @@ "integrity": "sha512-XdVKMF4SJ0nP/O7XIPB0JwAEuT9lDIYnNsK8yGVe43y0AWoKeJNdv3ZNWh7ksJ6KqQFjOO6ox/VEitLnaVNufw==", "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": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", diff --git a/package.json b/package.json index d856c29..1530aa6 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "@supabase/supabase-js": "^2.50.0", "@sveltejs/adapter-node": "^5.2.12", "googleapis": "^150.0.1", - "papaparse": "^5.5.3" + "papaparse": "^5.5.3", + "quoted-printable": "^1.0.1" } } diff --git a/src/lib/google.ts b/src/lib/google.ts index 4da818f..c62e915 100644 --- a/src/lib/google.ts +++ b/src/lib/google.ts @@ -1,5 +1,6 @@ import { google } from 'googleapis'; 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']; @@ -27,51 +28,73 @@ export async function exchangeCodeForTokens(code: string) { export async function sendGmail( 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(); oauth.setCredentials({ refresh_token: refreshToken }); const gmail = google.gmail({ version: 'v1', auth: oauth }); - const wrappedHtml = ` + const message_html = + ` - - - - ${subject} - - - - -
${text}
- + + + + +
+

${text}

+ QR Code +
+
+
+
+
+
+
+
+

This email has been generated with the help of *insert software name*

+
+
+ `; - 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: ', + '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({ userId: 'me', diff --git a/src/routes/private/api/gmail/+server.ts b/src/routes/private/api/gmail/+server.ts index 8cd7ce3..e0ab49a 100644 --- a/src/routes/private/api/gmail/+server.ts +++ b/src/routes/private/api/gmail/+server.ts @@ -36,13 +36,13 @@ export const GET: RequestHandler = async ({ url }) => { /* ───────────── POST ───────────── */ 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 */ if (action === 'send') { if (!refreshToken) return new Response('Missing token', { status: 401 }); try { - await sendGmail(refreshToken, { to, subject, text }); + await sendGmail(refreshToken, { to, subject, text, qr_code }); return json({ ok: true }); } catch (err) { return new Response((err as Error).message, { status: 500 });