Move from sendgrid to nodemailer for emails

This commit is contained in:
Rotimi Best
2023-10-07 02:35:25 +01:00
parent 2040257916
commit 7a772dc3f0
9 changed files with 345 additions and 167 deletions
+2
View File
@@ -38,6 +38,7 @@
"lodash": "^4.17.21", "lodash": "^4.17.21",
"marked": "^6.0.0", "marked": "^6.0.0",
"netlify": "^13.1.10", "netlify": "^13.1.10",
"nodemailer": "^6.9.5",
"openai-edge": "^1.2.2", "openai-edge": "^1.2.2",
"pluralize": "^8.0.0", "pluralize": "^8.0.0",
"sirv": "^2.0.3", "sirv": "^2.0.3",
@@ -55,6 +56,7 @@
"@sveltejs/kit": "^1.22.4", "@sveltejs/kit": "^1.22.4",
"@tailwindcss/typography": "^0.5.9", "@tailwindcss/typography": "^0.5.9",
"@types/lodash": "^4.14.196", "@types/lodash": "^4.14.196",
"@types/nodemailer": "^6.4.11",
"autoprefixer": "^10.4.14", "autoprefixer": "^10.4.14",
"carbon-components-svelte": "^0.79.0", "carbon-components-svelte": "^0.79.0",
"carbon-icons-svelte": "^12.1.0", "carbon-icons-svelte": "^12.1.0",
@@ -1,41 +1,44 @@
import { supabase } from '$lib/utils/functions/supabase'; import { supabase } from '$lib/utils/functions/supabase';
export class Notification { export const NOTIFICATION_NAME = {
name = ''; WELCOME_TO_APP: 'WELCOME TO APP'
accessToken = ''; };
constructor(name: string) { const NAME_TO_PATH = {
this.name = name; [NOTIFICATION_NAME.WELCOME_TO_APP]: '/api/email/welcome'
} };
async getAccessToken() { const getAccessToken = async () => {
const { data } = await supabase.auth.getSession(); const { data } = await supabase.auth.getSession();
this.accessToken = data.session?.access_token || '';
return this.accessToken; return data.session?.access_token || '';
} };
async send(path: string, body: { [k: string]: string }) { export const triggerSendEmail = async (name: string, body: { [k: string]: string }) => {
try { try {
await this.getAccessToken(); const accessToken = await getAccessToken();
const response = await fetch(path, { const path = NAME_TO_PATH[name];
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: this.accessToken
},
body: JSON.stringify(body)
});
if (!response.ok) {
throw new Error(await response.text());
}
const result = await response.json();
console.log(`${this.name} sent email result`, result); if (!path) {
} catch (error) { console.error('Could not trigger send');
console.log(`${this.name} sent email error`, error); return;
} }
}
}
export class InviteNotification extends Notification {} const response = await fetch(path, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: accessToken
},
body: JSON.stringify(body)
});
if (!response.ok) {
throw new Error(await response.text());
}
const result = await response.json();
console.log(`${name} sent email result`, result);
} catch (error) {
console.log(`${name} sent email error`, error);
}
};
@@ -0,0 +1,58 @@
import nodemailer from 'nodemailer';
import { withEmailTemplate } from './template';
import { SMTP_HOST, SMTP_USER, SMTP_PASSWORD } from '$env/static/private';
const content = `
<div>
<h1>Welcome</h1>
<br/>
<p>Thank you so much for trying out ClassroomIO</p>
<a href="https://app.classroomio.com/login">Login</a><br/>
<br/>
Your ClassroomIO Team
</div>
`;
const transporter = nodemailer.createTransport({
host: SMTP_HOST,
port: 465,
secure: true,
auth: {
user: SMTP_USER,
pass: SMTP_PASSWORD
}
});
const getTransporter = async (): Promise<boolean> => {
return await new Promise((resolve, reject) => {
transporter.verify(function (error, success) {
if (error) {
console.log('Transporter error', error);
reject(error);
} else {
console.log('Server is ready to take our messages');
resolve(success);
}
});
});
};
export const sendEmail = async ({
to,
subject,
content
}: {
to: string;
subject: string;
content: string;
}) => {
await getTransporter();
const info = await transporter.sendMail({
from: '"Best from ClassroomIO" <[email protected]>', // sender address
to, // list of receivers
subject, // Subject line
html: withEmailTemplate(content) // html body
});
console.log('Message sent: %s', info.messageId);
};
@@ -0,0 +1,219 @@
export const withEmailTemplate = (content: string) =>
`<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1, minimum-scale=1"
/>
<base target="_blank" />
<style>
body {
background-color: #fff;
font-family: "Poppins", "Helvetica Neue", "Segoe UI", Helvetica,
sans-serif;
font-size: 15px;
font-weight: 300;
line-height: 26px;
margin: 0;
color: #1e293b;
}
pre {
background: #f4f4f4;
padding: 2px;
}
hr {
border-top: 1px dashed #94a3b8;
margin-top: 1rem;
margin-bottom: 1rem;
}
table {
width: 100%;
}
table td {
padding: 5px;
}
.socialicons {
max-width: 200px;
margin-left: auto;
margin-right: auto;
margin-top: 27px;
}
.wrap {
background-color: #f1f5f9;
padding: 30px;
max-width: 525px;
margin: 0 auto;
border-radius: 12px;
}
.brandcolor {
color: #00c4b8;
}
.tooltip {
background-color: #f1f5f9;
padding: 1rem;
border-radius: 1rem;
color: #475569;
margin-top: 15px;
margin-bottom: 15px;
}
.tooltip a {
color: #1e293b;
}
.button {
margin-top:12px;
background: #0f172a;
border-radius: 8px;
text-decoration: none !important;
color: #fff !important;
font-weight: 500;
padding: 10px 30px;
display: inline-block;
font-size: 0.9em;
}
.button:hover {
background: #0233BD;
}
.footer {
text-align: center;
font-size: 12px;
color: #cbd5e1;
}
.footer a {
color: #cbd5e1;
margin-right: 5px;
}
.gutter {
padding: 30px;
text-align: center;
}
img {
max-width: 100%;
height: auto;
}
.gutter img {
max-width: 280px;
}
a {
color: #00c4b8;
}
a:hover {
color: #00e6ca;
}
h1,
h2,
h3,
h4 {
font-weight: 600;
}
@media screen and (max-width: 600px) {
.wrap {
max-width: auto;
}
.gutter {
padding: 10px;
}
}
</style>
</head>
<body
style="
background-color: #fff;
font-family: 'Poppins', 'Helvetica Neue', 'Segoe UI', Helvetica,
sans-serif;
font-size: 15px;
line-height: 26px;
margin: 0;
color: #1e293b;
"
>
<div class="gutter" style="padding: 30px">
<a href="https://classroomio.com" target="_blank">
<img
src="https://koxqonvbkeakwvmdegcf.supabase.co/storage/v1/object/public/avatars/classroomio-logo-full.png"
alt="ClassroomIO Logo"
/></a>
</div>
<div
class="wrap"
style="
background-color: #f1f5f9;
padding: 30px;
max-width: 525px;
margin: 0 auto;
border-radius: 12px;
"
>
${content}
</div>
<div
class="footer"
style="text-align: center; font-size: 12px; color: #cbd5e1"
>
<table class="socialicons">
<tr>
<td>
<a target="_blank" href="https://twitter.com/classroomio"
><img
title="X"
src="https://koxqonvbkeakwvmdegcf.supabase.co/storage/v1/object/public/avatars/classroomio-x.png"
alt="X"
width="32"
/></a>
</td>
<td>
<a target="_blank" href="https://www.youtube.com/@ClassroomIO"
><img
title="Youtube"
src="https://koxqonvbkeakwvmdegcf.supabase.co/storage/v1/object/public/avatars/classroomio-youtube.png"
alt="Youtube"
width="32"
/></a>
</td>
<td>
<a target="_blank" href="https://www.facebook.com/classroomiohq"
><img
title="Facebook"
src="https://koxqonvbkeakwvmdegcf.supabase.co/storage/v1/object/public/avatars/classroomio-facebook.png"
alt="Facebook"
width="32"
/></a>
</td>
</tr>
</table>
<p style="padding-top: 8px; line-height: initial">
ClassroomIO ${new Date().getFullYear()}. All rights reserved.<br />
<a
style="text-decoration: none"
href="https://app.enzuzo.com/policies/tos/958fc978-5477-11ee-a03b-7b111830c594"
target="_blank"
>Terms</a
>
|
<a
style="text-decoration: none"
href="https://app.enzuzo.com/policies/privacy/958fc978-5477-11ee-a03b-7b111830c594"
target="_blank"
>Privacy Policy</a
>
</p>
</div>
</body>
</html>
`;
+23 -12
View File
@@ -1,9 +1,10 @@
import { render } from 'svelte-email'; import { render } from 'svelte-email';
import { json } from '@sveltejs/kit'; import { json } from '@sveltejs/kit';
import WelcomeTemplate from './template.svelte';
import { getSendgrid, SENDGRID_FROM } from '$lib/utils/services/sendgrid'; import { getSendgrid, SENDGRID_FROM } from '$lib/utils/services/sendgrid';
import { getSupabase } from '$lib/utils/functions/supabase'; import { getSupabase } from '$lib/utils/functions/supabase';
import { sendEmail } from '$lib/utils/services/notification/send';
const sendgrid = getSendgrid(); const sendgrid = getSendgrid();
const supabase = getSupabase(); const supabase = getSupabase();
@@ -28,19 +29,29 @@ export async function POST({ request }) {
return json({ success: false, message: 'Unauthenticated user' }, { status: 401 }); return json({ success: false, message: 'Unauthenticated user' }, { status: 401 });
} }
const options = { await sendEmail({
from: SENDGRID_FROM,
to, to,
subject: 'Thank you so so so much for choosing ClassroomIO!', subject: 'Thank you so so so much for choosing ClassroomIO!',
html: render({ content: `
template: WelcomeTemplate, <p>Dear ${name},</p>
props: { <p>My name is Best the CEO of ClassroomIO and I will personally like to welcome to the vibrant community of ClassroomIO!</p>
name <p>
} We're thrilled to have you join us in shaping the future of education. Your decision to embark on
}) this journey with us is deeply appreciated, and we're genuinely excited about the possibilities
}; that lie ahead.
</p>
sendgrid.send(options); <p>
Once again, welcome to the ClassroomIO family. We're here to help, to listen, and to celebrate
your successes. Get ready to unlock a world of possibilities in education!
</p>
<p>
If you have any question or face any issues just send me an email [email protected] 😃
</p>
<div>
<a href="https://app.classroomio.com">Go to Dashboard</a>
</div>
`
});
return json({ return json({
success: true, success: true,
@@ -1,13 +0,0 @@
<script lang="ts">
import EmailTemplate from '$lib/components/Email/EmailTemplate.svelte';
import Welcome from './welcome.svelte';
export let name = '';
</script>
<EmailTemplate
children={Welcome}
props={{
name
}}
/>
@@ -1,79 +0,0 @@
<script lang="ts">
import { Text, Section, Button, Link } from 'svelte-email';
export let name = '';
const fontFamily =
'-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen-Sans,Ubuntu,Cantarell,"Helvetica Neue",sans-serif';
const paragraph = {
fontFamily,
fontSize: '16px',
lineHeight: '26px'
};
const footerParagraph = {
...paragraph,
margin: '0px'
};
const btnContainer = {
textAlign: 'center' as const
};
const button = {
fontFamily,
backgroundColor: '#1d4ee2',
borderRadius: '5px',
color: '#fff',
fontSize: '16px',
textDecoration: 'none',
textAlign: 'center' as const,
display: 'block'
};
const linkStyle = {
color: '#1d4ee2'
};
</script>
<Text style={paragraph}>Dear {name},</Text>
<Text style={paragraph}>Welcome to the vibrant community of ClassroomIO!</Text>
<Text style={paragraph}>
We're thrilled to have you join us in shaping the future of education. Your decision to embark on
this journey with us is deeply appreciated, and we're genuinely excited about the possibilities
that lie ahead.
</Text>
<Text style={paragraph}>
At ClassroomIO, we understand the dedication and passion that educators like you bring to the
table. That's why we've built a platform that's not only designed to make your teaching experience
seamless but also to empower you to create transformative learning moments for your students.
</Text>
<Text style={paragraph}>
Your choice to use ClassroomIO is a testament to your commitment to innovation in education. It's
educators like you who drive progress and inspire change, and we're here to support you every step
of the way.
</Text>
<Text style={paragraph}>
Our team is working diligently behind the scenes, shipping new features and updates daily. This
means you can expect a platform that's constantly evolving, adapting to your needs, and staying at
the forefront of educational technology.
</Text>
<Text style={paragraph}>
We encourage you to explore ClassroomIO's range of features, from interactive quizzes and robust
course management tools to collaborative forums and AI-integrated lesson planning. Your feedback
is invaluable to us as we work together to refine and enhance these features to best suit your
teaching style and objectives.
</Text>
<Text style={paragraph}>
Once again, welcome to the ClassroomIO family. We're here to help, to listen, and to celebrate
your successes. Get ready to unlock a world of possibilities in education!
</Text>
<Section style={btnContainer}>
<Button pX={12} pY={12} style={button} href="https://app.classroomio.com">Go to Dashboard</Button>
</Section>
<Text style={footerParagraph}>Best regards</Text>
<Text style={footerParagraph}>Rotimi Best</Text>
<Text style={footerParagraph}
><Link style={linkStyle} href="https://classroomio.com">ClassroomIO</Link></Text
>
+8 -3
View File
@@ -10,9 +10,11 @@
import { blockedSubdomain } from '$lib/utils/constants/app'; import { blockedSubdomain } from '$lib/utils/constants/app';
import { welcomeModalStore } from '$lib/components/WelcomeModal/store'; import { welcomeModalStore } from '$lib/components/WelcomeModal/store';
import { getOrganizations } from '$lib/utils/services/org'; import { getOrganizations } from '$lib/utils/services/org';
import { sendWelcomeEmail } from './utils';
import { currentOrg } from '$lib/utils/store/org';
import { generateSitename } from '$lib/utils/functions/org'; import { generateSitename } from '$lib/utils/functions/org';
import {
triggerSendEmail,
NOTIFICATION_NAME
} from '$lib/utils/services/notification/notification';
interface OnboardingField { interface OnboardingField {
fullname?: string; fullname?: string;
@@ -182,7 +184,10 @@
loading = false; loading = false;
if (fields.fullname) { if (fields.fullname) {
sendWelcomeEmail($profile.email, fields.fullname); triggerSendEmail(NOTIFICATION_NAME.WELCOME_TO_APP, {
to: $profile.email,
name: fields.fullname
});
} }
console.log('data', data); console.log('data', data);
-28
View File
@@ -1,28 +0,0 @@
import { supabase } from '$lib/utils/functions/supabase';
export async function sendWelcomeEmail(to: string, name: string) {
const { data } = await supabase.auth.getSession();
const accessToken = data.session?.access_token || '';
try {
const response = await fetch('/api/email/welcome', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: accessToken
},
body: JSON.stringify({
to,
name
})
});
if (!response.ok) {
throw new Error(await response.text());
}
const result = await response.json();
console.log('Sent confirmation email', result);
} catch (error) {
console.error('Error sending confirmation email', error);
}
}