Skip to content

Commit

Permalink
feat: resend to SMTP client (#523)
Browse files Browse the repository at this point in the history
Co-authored-by: 7HR4IZ3 <[email protected]>
  • Loading branch information
hughcrt and 7HR4IZ3 authored Aug 30, 2024
1 parent ea09229 commit e6c1d2a
Show file tree
Hide file tree
Showing 17 changed files with 803 additions and 619 deletions.
1,022 changes: 527 additions & 495 deletions package-lock.json

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions packages/backend/.env.example
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
DATABASE_URL="postgresql://postgres:password@your-host:5432/postgres"
JWT_SECRET=yoursupersecret
APP_URL=http://localhost:8080
API_URL=http://localhost:3333


# optional (for the playground, evaluation and radar features)
# Optional (for the playground, evaluation and radar features)
LUNARY_PUBLIC_KEY=259d2d94-9446-478a-ae04-484de705b522
OPENAI_API_KEY=sk-...
OPENROUTER_API_KEY=sk-...
Expand Down
6 changes: 4 additions & 2 deletions packages/backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
"koa-router": "^12.0.1",
"lunary": "^0.7.2",
"node-cron": "^3.0.3",
"nodemailer": "^6.9.14",
"openai": "^4.28.4",
"p-queue": "^8.0.1",
"postgres": "^3.4.4",
Expand All @@ -55,6 +56,7 @@
"@types/koa-ratelimit": "^5.0.5",
"@types/koa-router": "^7.4.8",
"@types/node": "^20.11.11",
"@types/node-cron": "^3.0.11"
"@types/node-cron": "^3.0.11",
"@types/nodemailer": "^6.4.15"
}
}
}
4 changes: 2 additions & 2 deletions packages/backend/src/api/v1/auth/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { sendVerifyEmail } from "@/src/emails"
import { Db } from "@/src/types"
import config from "@/src/utils/config"
import sql from "@/src/utils/db"
import { sendVerifyEmail } from "@/src/utils/emails"
import Context from "@/src/utils/koa"
import { sendSlackMessage } from "@/src/utils/notifications"
import Router from "koa-router"
Expand All @@ -15,7 +16,6 @@ import {
verifyJWT,
verifyPassword,
} from "./utils"
import config from "@/src/utils/config"

const auth = new Router({
prefix: "/auth",
Expand Down
3 changes: 1 addition & 2 deletions packages/backend/src/api/v1/auth/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,7 @@ import * as argon2 from "argon2"

import bcrypt from "bcrypt"
import { validateUUID } from "@/src/utils/misc"
import { sendEmail } from "@/src/utils/sendEmail"
import { RESET_PASSWORD } from "@/src/utils/emails"
import { sendEmail, RESET_PASSWORD } from "@/src/emails"

export function sanitizeEmail(email: string) {
const [username, domain] = email.toLowerCase().trim().split("@")
Expand Down
19 changes: 11 additions & 8 deletions packages/backend/src/api/v1/users.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
import Router from "koa-router"
import sql from "@/src/utils/db"
import Router from "koa-router"
import {
INVITE_EMAIL,
WELCOME_EMAIL,
sendEmail,
sendVerifyEmail,
} from "@/src/utils/emails"
WELCOME_EMAIL,
} from "@/src/emails"
import { checkAccess } from "@/src/utils/authorization"
import Context from "@/src/utils/koa"
import { jwtVerify } from "jose"
import { roles } from "shared"
import { z } from "zod"
import { sendEmail } from "@/src/utils/sendEmail"
import { signJWT } from "./auth/utils"
import { roles } from "shared"
import { checkAccess } from "@/src/utils/authorization"
import Context from "@/src/utils/koa"

const users = new Router({
prefix: "/users",
Expand Down Expand Up @@ -95,7 +95,10 @@ users.get("/me", async (ctx: Context) => {
})

users.get("/verify-email", async (ctx: Context) => {
const token = ctx.request.query.token as string
const bodySchema = z.object({
token: z.string(),
})
const { token } = bodySchema.parse(ctx.request.query)

const {
payload: { email },
Expand Down
12 changes: 6 additions & 6 deletions packages/backend/src/api/webhooks/stripe.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,3 @@
import { sendEmail } from "@/src/utils/sendEmail"
import {
CANCELED_EMAIL,
UPGRADE_EMAIL,
FULLY_CANCELED_EMAIL,
} from "@/src/utils/emails"
import { sendSlackMessage } from "@/src/utils/notifications"
import Stripe from "stripe"
import stripe from "@/src/utils/stripe"
Expand All @@ -12,6 +6,12 @@ import sql from "@/src/utils/db"
import Router from "koa-router"
import { Context } from "koa"
import { clearUndefined } from "@/src/utils/ingest"
import {
sendEmail,
CANCELED_EMAIL,
UPGRADE_EMAIL,
FULLY_CANCELED_EMAIL,
} from "@/src/emails"

const router = new Router({
prefix: "/stripe",
Expand Down
3 changes: 3 additions & 0 deletions packages/backend/src/emails/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from "./templates"
export * from "./sender"
export * from "./utils"
48 changes: 48 additions & 0 deletions packages/backend/src/emails/sender.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import nodemailer from "nodemailer"
import config from "../utils/config"
import sql from "../utils/db"

export interface MailOptions {
subject: string
to: string
from: string
text: string
}

const transporter = nodemailer.createTransport({
host: config.SMTP_HOST,
port: config.SMTP_PORT,
secure: true,
auth: {
user: config.SMTP_USER,
pass: config.SMTP_PASSWORD,
},
})

export async function sendEmail(body: MailOptions) {
if (
!config.SMTP_HOST ||
!config.SMTP_PORT ||
!config.SMTP_USER ||
!config.SMTP_PASSWORD
) {
return console.warn(
"[EMAIL] SMTP environment variables are not set. Skipping email sending.",
)
}

// TODO: extract to another function
const blockList = await sql`select email from _email_block_list`
const blockedEmails = blockList.map(({ email }) => email)

if (blockedEmails.includes(body.to)) {
return console.warn("[EMAIL] Email in the block list, skipping sending.")
}

// TODO: should probably have an temp email server that checks if test account receives the mails
if (body.to === "[email protected]") {
return console.warn("[EMAIL] Not sending email to test account")
}

await transporter.sendMail(body)
}
Original file line number Diff line number Diff line change
@@ -1,57 +1,43 @@
import { signJWT } from "@/src/api/v1/auth/utils"
import { sendEmail } from "./sendEmail"
import { MailOptions } from "."
import config from "../utils/config"
import { extractFirstName } from "./utils"

function sanitizeName(name: string): string {
return name.replace(/\s+/g, " ").trim()
}

function extractFirstName(name: string): string {
if (!name) return "there"
const sanitizedName = sanitizeName(name)
return sanitizedName.split(" ")[0]
}

export async function sendVerifyEmail(email: string, name: string = "") {
const token = await signJWT({ email })

const confirmLink = `${process.env.API_URL}/v1/users/verify-email?token=${token}`

await sendEmail(CONFIRM_EMAIL(email, name, confirmLink))
}

export function INVITE_EMAIL(email: string, orgName: string, link: string) {
export function INVITE_EMAIL(
email: string,
orgName: string,
inviteLink: string,
): MailOptions {
return {
subject: `You've been invited to Lunary`,
to: [email],
reply_to: "[email protected]",
from: process.env.GENERIC_SENDER,
subject: "You've been invited to Lunary",
to: email,
from: config.GENERIC_SENDER_ADDRESS!,
text: `Hi,
You've been invited to join ${orgName} on Lunary.
Please click on the following link to accept the invitation:
${link}
${inviteLink}
We're looking forward to having you on board!
You can reply to this email if you have any question.
Thanks
- The Lunary team`,
- The Lunary team
`,
}
}

export function CONFIRM_EMAIL(
email: string,
name: string,
confirmLink: string,
) {
): MailOptions {
return {
subject: `confirm your email`,
to: [email],
reply_to: "[email protected]",
from: process.env.GENERIC_SENDER,
subject: `Confirm your email`,
to: email,
from: config.GENERIC_SENDER_ADDRESS!,

text: `Hi ${extractFirstName(name)},
Expand All @@ -68,12 +54,14 @@ Thanks
}
}

export function RESET_PASSWORD(email: string, confirmLink: string) {
export function RESET_PASSWORD(
email: string,
confirmLink: string,
): MailOptions {
return {
subject: `Reset your password`,
to: [email],
reply_to: "[email protected]",
from: process.env.GENERIC_SENDER,
to: email,
from: config.GENERIC_SENDER_ADDRESS!,
text: `Hi,
Please click on the link below to reset your password:
Expand All @@ -86,12 +74,15 @@ You can reply to this email if you have any question.
}
}

export function WELCOME_EMAIL(email: string, name: string, projectId: string) {
export function WELCOME_EMAIL(
email: string,
name: string,
projectId: string,
): MailOptions {
return {
subject: `welcome to Lunary`,
to: [email],
reply_to: "[email protected]",
from: process.env.PERSONAL_SENDER || process.env.GENERIC_SENDER,
subject: `Welcome to Lunary`,
to: email,
from: config.PERSONAL_SENDER_ADDRESS || config.GENERIC_SENDER_ADDRESS!,
text: `Hi ${extractFirstName(name)},
I'm Vince, co-founder of lunary.
Expand All @@ -111,12 +102,15 @@ Vince`,
}
}

export function UPGRADE_EMAIL(email: string, name: string, plan: string) {
export function UPGRADE_EMAIL(
email: string,
name: string,
plan: string,
): MailOptions {
return {
subject: `Your account has been upgraded`,
to: [email],
reply_to: "[email protected]",
from: process.env.GENERIC_SENDER,
to: email,
from: config.GENERIC_SENDER_ADDRESS!,
text: `Hi ${extractFirstName(name)},
Your account has been upgraded to the ${plan} plan.
Expand All @@ -125,16 +119,15 @@ The extra features and higher limits are now available to you.
Reply to this email if you have any question.
- The Lunary Team`,
- The Lunary team`,
}
}

export function CANCELED_EMAIL(email: string, name: string) {
export function CANCELED_EMAIL(email: string, name: string): MailOptions {
return {
subject: `Important: subscription canceled`,
to: [email],
from: process.env.GENERIC_SENDER,
reply_to: "[email protected]",
to: email,
from: config.GENERIC_SENDER_ADDRESS!,
text: `Hi ${extractFirstName(name)},
You have canceled your subscription. We're sad to see you go :(
Expand All @@ -143,22 +136,21 @@ At the end of your billing period, your account will be downgraded to the free p
*Important: any data older than 30 days (free plan limits) will be permanently deleted.*
If this was a mistake, you can upgrade again at any time here: https://app.lunary.ai/billing
If this was a mistake, you can upgrade again at any time here: ${process.env.APP_URL}/billing
Would you mind telling us why you canceled? We're always looking to improve.
Thank you for trying Lunary.
Vince & Hugh - founders of Lunary`,
- The Lunary team`,
}
}

export function FULLY_CANCELED_EMAIL(email: string, name: string) {
export function FULLY_CANCELED_EMAIL(email: string, name: string): MailOptions {
return {
subject: `Sorry to see you go..`,
reply_to: "[email protected]",
to: [email],
from: process.env.GENERIC_SENDER,
subject: `Sorry to see you go...`,
to: email,
from: config.GENERIC_SENDER_ADDRESS!,
text: `Hi ${extractFirstName(name)},
Your account has been downgraded to the free plan.
Expand All @@ -173,27 +165,26 @@ If you can take 30 seconds to reply to this email with one of the following reas
4. It's too expensive
5. Other: ____________
If this was a mistake, you can upgrade again at any time here: https://app.lunary.ai/billing
If this was a mistake, you can upgrade again at any time here: ${process.env.APP_URL}/billing
Thank you for trying Lunary.
Vince & Hugh - founders of Lunary`,
- The Lunary team`,
}
}

export function LIMITED_EMAIL(email: string, name: string) {
export function LIMITED_EMAIL(email: string, name: string): MailOptions {
return {
subject: `Action Required: Events limit reached`,
to: [email],
reply_to: "[email protected]",
from: process.env.GENERIC_SENDER,
to: email,
from: config.GENERIC_SENDER_ADDRESS!,
text: `Hi ${extractFirstName(name)},
Congratulations! You've reached your free ingested event limit for the month, which means you're making great use of Lunary.
As a result, your account has been temporarily limited (don't worry, your data is safe and sound).
To continue enjoying our services without interruption, please consider upgrading your account here: https://app.lunary.ai/billing
To continue enjoying our services without interruption, please consider upgrading your account here: ${process.env.APP_URL}/billing
If you have any questions, feel free to reply to this email.
Expand Down
Loading

0 comments on commit e6c1d2a

Please sign in to comment.