From 28b3b032794c1be0c42a5cc36417215d5e8b270e Mon Sep 17 00:00:00 2001 From: Thomas Ruoff Date: Thu, 22 Dec 2022 23:00:44 +0100 Subject: [PATCH] use nodemailer instead of sendgrid --- helpers/mail.ts | 162 +++++++++++++---------------- package-lock.json | 127 ++++------------------ package.json | 6 +- pages/api/bookings/[uuid]/index.ts | 18 ++-- pages/api/bookings/index.ts | 28 +++-- 5 files changed, 130 insertions(+), 211 deletions(-) diff --git a/helpers/mail.ts b/helpers/mail.ts index 16a788a..c1a0e1e 100644 --- a/helpers/mail.ts +++ b/helpers/mail.ts @@ -1,32 +1,46 @@ import { Booking } from '../db/booking' import { getBaseURL } from '../helpers/url' -import { log } from '../helpers/log' import { daysFormatFrontend } from './date' import { generateCalendarEntry } from './ical' -import sgMail from '@sendgrid/mail' +import nodemailer from 'nodemailer'; -const SENDGRID_API_KEY = process.env.SENDGRID_API_KEY -const ADMIN_EMAIL = process.env.ADMIN_EMAIL -const FROM_EMAIL = process.env.FROM_EMAIL +const smtpUser = process.env.SMTP_USER +const smtpPass = process.env.SMTP_PASS +const adminEmail = process.env.ADMIN_EMAIL +const fromEmail = process.env.FROM_EMAIL -if (!SENDGRID_API_KEY) { - throw new Error('NO SENDGRID_API_KEY set!') +if (!smtpUser) { + throw new Error('NO SMTP_USER set!') } -if (!ADMIN_EMAIL) { +if (!smtpPass) { + throw new Error('NO SMTP_PASS set!') +} + +if (!adminEmail) { throw new Error('No ADMIN_EMAIL set!') } -if (!FROM_EMAIL) { +if (!fromEmail) { throw new Error('No FROM_EMAIL set!') } -sgMail.setApiKey(SENDGRID_API_KEY) +let transporter = nodemailer.createTransport({ + host: "wirtanen.uberspace.de", + port: 587, + secure: false, + auth: { + user: smtpUser, + pass: smtpPass, + }, + logger: true, +}); const footer = ` Viele Grüße +-- Thomas Ruoff Pfadi Bussle Wart @@ -105,115 +119,85 @@ MfG` export async function sendReceivedBookingAdminMail( booking: Booking ): Promise { - try { - await sendMail({ - to: [{ email: ADMIN_EMAIL }], - from: { email: FROM_EMAIL, name: 'Pfadi-Bussle Wart' }, - subject: `Buchung für ${booking.days} eingegangen!`, - textPlainContent: getReceivedBookingAdminText(booking), - }) - } catch (error) { - log.error(`Failed in sendReceivedBookingMail for ${booking.uuid}`, error) - } + await sendMail({ + to: [{ address: adminEmail, name: 'Pfadi-Bussle Wart' }], + from: { address: fromEmail, name: 'Pfadi-Bussle Wart' }, + subject: `Buchung für ${booking.days} eingegangen!`, + text: getReceivedBookingAdminText(booking), + }) } export async function sendReceivedBookingBookerMail( booking: Booking ): Promise { - try { - await sendMail({ - to: [{ email: booking.email, name: booking.name }], - from: { email: FROM_EMAIL, name: 'Pfadi-Bussle Wart' }, - subject: `Deine Pfadi-Bussle Buchung ist eingegangen!`, - textPlainContent: getReceivedBookingBookerText(booking), - }) - } catch (error) { - log.error(`Failed in sendReceivedBookingMail for ${booking.uuid}`, error) - } + await sendMail({ + to: [{ address: booking.email, name: booking.name }], + from: { address: fromEmail, name: 'Pfadi-Bussle Wart' }, + subject: `Deine Pfadi-Bussle Buchung ist eingegangen!`, + text: getReceivedBookingBookerText(booking), + }) } export async function sendBookingConfirmed(booking: Booking): Promise { - try { - await sendMail({ - to: [{ email: booking.email, name: booking.name }], - from: { email: FROM_EMAIL, name: 'Pfadi-Bussle Wart' }, - subject: `Deine Pfadi-Bussle Buchung wurde bestätigt!`, - textPlainContent: getBookingConfirmedText(booking), - attachments: [ - { - content: Buffer.from(generateCalendarEntry(booking)).toString( - 'base64' - ), - type: 'text/calendar', - filename: 'pfadibussle-buchung.ics', - }, - ], - }) - } catch (error) { - log.error(`Failed in sendBookingConfirmedMail for ${booking.uuid}`, error) - } + await sendMail({ + to: [{ address: booking.email, name: booking.name }], + from: { address: fromEmail, name: 'Pfadi-Bussle Wart' }, + subject: `Deine Pfadi-Bussle Buchung wurde bestätigt!`, + text: getBookingConfirmedText(booking), + attachments: [ + { + content: Buffer.from(generateCalendarEntry(booking)).toString( + 'base64' + ), + type: 'text/calendar', + filename: 'pfadibussle-buchung.ics', + }, + ], + }) } export async function sendBookingRejected(booking: Booking): Promise { - try { - await sendMail({ - to: [{ email: booking.email, name: booking.name }], - from: { email: FROM_EMAIL, name: 'Pfadi-Bussle Wart' }, - subject: `Deine Pfadi-Bussle Buchung wurde abgelehnt!`, - textPlainContent: getBookingRejectedText(booking), - }) - } catch (error) { - log.error(`Failed in sendBookingRejectedMail for ${booking.uuid}`, error) - } + await sendMail({ + to: [{ address: booking.email, name: booking.name }], + from: { address: fromEmail, name: 'Pfadi-Bussle Wart' }, + subject: `Deine Pfadi-Bussle Buchung wurde abgelehnt!`, + text: getBookingRejectedText(booking), + }) } export async function sendBookingCanceled(booking: Booking): Promise { - try { - await sendMail({ - to: [{ email: booking.email, name: booking.name }], - from: { email: FROM_EMAIL, name: 'Pfadi-Bussle Wart' }, - subject: `Deine Pfadi-Bussle Buchung wurde storniert!`, - textPlainContent: getBookingCanceledText(booking), - }) - } catch (error) { - log.error(`Failed in sendBookingCanceledMail for ${booking.uuid}`, error) - } + await sendMail({ + to: [{ address: booking.email, name: booking.name }], + from: { address: fromEmail, name: 'Pfadi-Bussle Wart' }, + subject: `Deine Pfadi-Bussle Buchung wurde storniert!`, + text: getBookingCanceledText(booking), + }) } async function sendMail({ to, from, subject, - textPlainContent, + text, attachments, }: { - to: { email: string; name?: string }[] - from: { email: string; name?: string } + to: { address: string; name: string }[] + from: { address: string; name: string } subject: string - textPlainContent: string + text: string attachments?: { content: string type?: string filename: string }[] }): Promise { - const data = { + + // send mail with defined transport object + await transporter.sendMail({ to, from, subject, - text: textPlainContent, + text, attachments, - } - - try { - await sgMail.send(data) - } catch (error) { - log.error('Failed to send email', error) - - if (error.response) { - log.error('Failed to send email', error.response.body) - } - - // TODO: stuff into DB if failed and retry later - } -} + }); +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 384845c..c4c67e0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,6 @@ "version": "0.1.0", "dependencies": { "@next-auth/mongodb-adapter": "1.1.1", - "@sendgrid/mail": "7.7.0", "@vercel/analytics": "^0.1.4", "autoprefixer": "10.4.13", "classnames": "2.3.2", @@ -22,13 +21,14 @@ "next-auth": "4.18.7", "next-axiom": "0.16.0", "next-mdx-remote": "4.2.0", - "nodemailer": "6.8.0", + "nodemailer": "^6.8.0", "react": "18.2.0", "react-calendar": "4.0.0", "react-dom": "18.2.0", "uuid": "9.0.0" }, "devDependencies": { + "@types/nodemailer": "^6.4.6", "@types/react": "18.0.26", "@types/react-calendar": "3.9.0", "@types/uuid": "8.3.4", @@ -3047,41 +3047,6 @@ "integrity": "sha512-WiBSI6JBIhC6LRIsB2Kwh8DsGTlbBU+mLRxJmAe3LjHTdkDpwIbEOZgoXBbZilk/vlfjK8i6nKRAvIRn1XaIMw==", "dev": true }, - "node_modules/@sendgrid/client": { - "version": "7.7.0", - "resolved": "https://registry.npmjs.org/@sendgrid/client/-/client-7.7.0.tgz", - "integrity": "sha512-SxH+y8jeAQSnDavrTD0uGDXYIIkFylCo+eDofVmZLQ0f862nnqbC3Vd1ej6b7Le7lboyzQF6F7Fodv02rYspuA==", - "dependencies": { - "@sendgrid/helpers": "^7.7.0", - "axios": "^0.26.0" - }, - "engines": { - "node": "6.* || 8.* || >=10.*" - } - }, - "node_modules/@sendgrid/helpers": { - "version": "7.7.0", - "resolved": "https://registry.npmjs.org/@sendgrid/helpers/-/helpers-7.7.0.tgz", - "integrity": "sha512-3AsAxfN3GDBcXoZ/y1mzAAbKzTtUZ5+ZrHOmWQ279AuaFXUNCh9bPnRpN504bgveTqoW+11IzPg3I0WVgDINpw==", - "dependencies": { - "deepmerge": "^4.2.2" - }, - "engines": { - "node": ">= 6.0.0" - } - }, - "node_modules/@sendgrid/mail": { - "version": "7.7.0", - "resolved": "https://registry.npmjs.org/@sendgrid/mail/-/mail-7.7.0.tgz", - "integrity": "sha512-5+nApPE9wINBvHSUxwOxkkQqM/IAAaBYoP9hw7WwgDNQPxraruVqHizeTitVtKGiqWCKm2mnjh4XGN3fvFLqaw==", - "dependencies": { - "@sendgrid/client": "^7.7.0", - "@sendgrid/helpers": "^7.7.0" - }, - "engines": { - "node": "6.* || 8.* || >=10.*" - } - }, "node_modules/@sinclair/typebox": { "version": "0.24.19", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.24.19.tgz", @@ -3274,6 +3239,15 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-14.0.27.tgz", "integrity": "sha512-kVrqXhbclHNHGu9ztnAwSncIgJv/FaxmzXJvGXNdcCpV1b8u1/Mi6z6m0vwy0LzKeXFTPLH0NzwmoJ3fNCIq0g==" }, + "node_modules/@types/nodemailer": { + "version": "6.4.6", + "resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-6.4.6.tgz", + "integrity": "sha512-pD6fL5GQtUKvD2WnPmg5bC2e8kWCAPDwMPmHe/ohQbW+Dy0EcHgZ2oCSuPlWNqk74LS5BVMig1SymQbFMPPK3w==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/prettier": { "version": "2.7.1", "resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.7.1.tgz", @@ -3755,14 +3729,6 @@ "node": ">=4" } }, - "node_modules/axios": { - "version": "0.26.1", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.26.1.tgz", - "integrity": "sha512-fPwcX4EvnSHuInCMItEhAGnaSEXRBjtzh9fOtsE6E1G6p7vl7edEeZe11QHf18+6+9gR5PbKV/sGKNaD8YaMeA==", - "dependencies": { - "follow-redirects": "^1.14.8" - } - }, "node_modules/axobject-query": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-2.2.0.tgz", @@ -4463,6 +4429,7 @@ "version": "4.2.2", "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.2.2.tgz", "integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==", + "dev": true, "engines": { "node": ">=0.10.0" } @@ -5667,25 +5634,6 @@ "integrity": "sha512-WIWGi2L3DyTUvUrwRKgGi9TwxQMUEqPOPQBVi71R96jZXJdFskXEmf54BoZaS1kknGODoIGASGEzBUYdyMCBJg==", "dev": true }, - "node_modules/follow-redirects": { - "version": "1.15.1", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.1.tgz", - "integrity": "sha512-yLAMQs+k0b2m7cVxpS1VKJVvoz7SS9Td1zss3XRwXj+ZDH00RJgnuLx7E44wx02kQLrdM3aOOy+FpzS7+8OizA==", - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/RubenVerborgh" - } - ], - "engines": { - "node": ">=4.0" - }, - "peerDependenciesMeta": { - "debug": { - "optional": true - } - } - }, "node_modules/fraction.js": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.2.0.tgz", @@ -13911,32 +13859,6 @@ "integrity": "sha512-WiBSI6JBIhC6LRIsB2Kwh8DsGTlbBU+mLRxJmAe3LjHTdkDpwIbEOZgoXBbZilk/vlfjK8i6nKRAvIRn1XaIMw==", "dev": true }, - "@sendgrid/client": { - "version": "7.7.0", - "resolved": "https://registry.npmjs.org/@sendgrid/client/-/client-7.7.0.tgz", - "integrity": "sha512-SxH+y8jeAQSnDavrTD0uGDXYIIkFylCo+eDofVmZLQ0f862nnqbC3Vd1ej6b7Le7lboyzQF6F7Fodv02rYspuA==", - "requires": { - "@sendgrid/helpers": "^7.7.0", - "axios": "^0.26.0" - } - }, - "@sendgrid/helpers": { - "version": "7.7.0", - "resolved": "https://registry.npmjs.org/@sendgrid/helpers/-/helpers-7.7.0.tgz", - "integrity": "sha512-3AsAxfN3GDBcXoZ/y1mzAAbKzTtUZ5+ZrHOmWQ279AuaFXUNCh9bPnRpN504bgveTqoW+11IzPg3I0WVgDINpw==", - "requires": { - "deepmerge": "^4.2.2" - } - }, - "@sendgrid/mail": { - "version": "7.7.0", - "resolved": "https://registry.npmjs.org/@sendgrid/mail/-/mail-7.7.0.tgz", - "integrity": "sha512-5+nApPE9wINBvHSUxwOxkkQqM/IAAaBYoP9hw7WwgDNQPxraruVqHizeTitVtKGiqWCKm2mnjh4XGN3fvFLqaw==", - "requires": { - "@sendgrid/client": "^7.7.0", - "@sendgrid/helpers": "^7.7.0" - } - }, "@sinclair/typebox": { "version": "0.24.19", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.24.19.tgz", @@ -14131,6 +14053,15 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-14.0.27.tgz", "integrity": "sha512-kVrqXhbclHNHGu9ztnAwSncIgJv/FaxmzXJvGXNdcCpV1b8u1/Mi6z6m0vwy0LzKeXFTPLH0NzwmoJ3fNCIq0g==" }, + "@types/nodemailer": { + "version": "6.4.6", + "resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-6.4.6.tgz", + "integrity": "sha512-pD6fL5GQtUKvD2WnPmg5bC2e8kWCAPDwMPmHe/ohQbW+Dy0EcHgZ2oCSuPlWNqk74LS5BVMig1SymQbFMPPK3w==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, "@types/prettier": { "version": "2.7.1", "resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.7.1.tgz", @@ -14468,14 +14399,6 @@ "integrity": "sha512-gd1kmb21kwNuWr6BQz8fv6GNECPBnUasepcoLbekws23NVBLODdsClRZ+bQ8+9Uomf3Sm3+Vwn0oYG9NvwnJCw==", "dev": true }, - "axios": { - "version": "0.26.1", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.26.1.tgz", - "integrity": "sha512-fPwcX4EvnSHuInCMItEhAGnaSEXRBjtzh9fOtsE6E1G6p7vl7edEeZe11QHf18+6+9gR5PbKV/sGKNaD8YaMeA==", - "requires": { - "follow-redirects": "^1.14.8" - } - }, "axobject-query": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-2.2.0.tgz", @@ -14947,7 +14870,8 @@ "deepmerge": { "version": "4.2.2", "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.2.2.tgz", - "integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==" + "integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==", + "dev": true }, "define-lazy-prop": { "version": "2.0.0", @@ -15855,11 +15779,6 @@ "integrity": "sha512-WIWGi2L3DyTUvUrwRKgGi9TwxQMUEqPOPQBVi71R96jZXJdFskXEmf54BoZaS1kknGODoIGASGEzBUYdyMCBJg==", "dev": true }, - "follow-redirects": { - "version": "1.15.1", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.1.tgz", - "integrity": "sha512-yLAMQs+k0b2m7cVxpS1VKJVvoz7SS9Td1zss3XRwXj+ZDH00RJgnuLx7E44wx02kQLrdM3aOOy+FpzS7+8OizA==" - }, "fraction.js": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.2.0.tgz", diff --git a/package.json b/package.json index 6183e5f..8e250ea 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,6 @@ }, "dependencies": { "@next-auth/mongodb-adapter": "1.1.1", - "@sendgrid/mail": "7.7.0", "@vercel/analytics": "^0.1.4", "autoprefixer": "10.4.13", "classnames": "2.3.2", @@ -25,13 +24,14 @@ "next-auth": "4.18.7", "next-axiom": "0.16.0", "next-mdx-remote": "4.2.0", - "nodemailer": "6.8.0", + "nodemailer": "^6.8.0", "react": "18.2.0", "react-calendar": "4.0.0", "react-dom": "18.2.0", "uuid": "9.0.0" }, "devDependencies": { + "@types/nodemailer": "^6.4.6", "@types/react": "18.0.26", "@types/react-calendar": "3.9.0", "@types/uuid": "8.3.4", @@ -52,4 +52,4 @@ "^.+\\.(ts|tsx)$": "ts-jest" } } -} +} \ No newline at end of file diff --git a/pages/api/bookings/[uuid]/index.ts b/pages/api/bookings/[uuid]/index.ts index 45ddd2b..c86ceb2 100644 --- a/pages/api/bookings/[uuid]/index.ts +++ b/pages/api/bookings/[uuid]/index.ts @@ -63,12 +63,16 @@ export default async function userHandler( const { current, previous } = await patchBooking(uuid, req.body) // TODO: this should really go into the schema - if (wasRejected(previous, current)) { - sendBookingRejected(current) - } else if (wasConfirmed(previous, current)) { - sendBookingConfirmed(current) - } else if (wasCanceled(previous, current)) { - sendBookingCanceled(current) + try { + if (wasRejected(previous, current)) { + sendBookingRejected(current) + } else if (wasConfirmed(previous, current)) { + sendBookingConfirmed(current) + } else if (wasCanceled(previous, current)) { + sendBookingCanceled(current) + } + } catch (error) { + log.error(`Failed to send booking ${current} for booking ${uuid}`) } res.status(200).json(current) @@ -81,4 +85,4 @@ export default async function userHandler( res.setHeader('Allow', ['PATCH']) res.status(405).end(`Method ${method} Not Allowed`) } -} +} \ No newline at end of file diff --git a/pages/api/bookings/index.ts b/pages/api/bookings/index.ts index cdad299..f3cb082 100644 --- a/pages/api/bookings/index.ts +++ b/pages/api/bookings/index.ts @@ -34,17 +34,29 @@ export default async function userHandler( `received booking ${booking.uuid} from ${booking.email}`, booking ) - await sendReceivedBookingAdminMail(booking) - log.info(`send booking ${booking.uuid} received to admin`, booking) - await sendReceivedBookingBookerMail(booking) - log.info( - `send booking ${booking.uuid} received to {booking.email}`, - booking - ) + try { + await sendReceivedBookingAdminMail(booking) + log.info(`send booking ${booking.uuid} received to admin`, booking) + } catch (error) { + log.error(`failed to send booking ${booking.uuid} received to admin`, booking) + } + try { + await sendReceivedBookingBookerMail(booking) + log.info( + `send booking ${booking.uuid} received to {booking.email}`, + booking + ) + } catch (error) { + log.error( + `failed to send booking ${booking.uuid} received to {booking.email}`, + booking + ) + + } res.status(200).json(booking) break default: res.setHeader('Allow', ['POST']) res.status(405).end(`Method ${method} Not Allowed`) } -} +} \ No newline at end of file