Skip to content

Commit 62e46b9

Browse files
committed
feat: add email service
1 parent 37752e3 commit 62e46b9

File tree

3 files changed

+206
-0
lines changed

3 files changed

+206
-0
lines changed

apps/api/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
"helmet": "^7.2.0",
3636
"jsonwebtoken": "^9.0.2",
3737
"razorpay": "^2.9.6",
38+
"zeptomail": "^6.2.1",
3839
"zod": "^4.1.9"
3940
}
4041
}
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
import { SendMailClient } from "zeptomail";
2+
3+
interface EmailRecipient {
4+
address: string;
5+
name: string;
6+
}
7+
8+
interface SendEmailInput {
9+
to: EmailRecipient[];
10+
subject: string;
11+
htmlBody: string;
12+
textBody?: string;
13+
}
14+
15+
// Initialize ZeptoMail client
16+
const initializeEmailClient = () => {
17+
const url =
18+
process.env.ZEPTOMAIL_URL || "https://api.zeptomail.in/v1.1/email";
19+
const tokenFromEnv = process.env.ZEPTOMAIL_TOKEN;
20+
21+
if (!tokenFromEnv) {
22+
throw new Error(
23+
"ZEPTOMAIL_TOKEN is not configured in environment variables"
24+
);
25+
}
26+
27+
// If token doesn't start with "Zoho-enczapikey", add it
28+
// This allows storing the token without the prefix to avoid space issues in Docker
29+
const token = tokenFromEnv.startsWith("Zoho-enczapikey")
30+
? tokenFromEnv
31+
: `Zoho-enczapikey ${tokenFromEnv}`;
32+
33+
return new SendMailClient({ url, token });
34+
};
35+
36+
export const emailService = {
37+
/**
38+
* Send a generic email
39+
*/
40+
async sendEmail(input: SendEmailInput): Promise<boolean> {
41+
try {
42+
const client = initializeEmailClient();
43+
const fromAddress = process.env.ZEPTOMAIL_FROM_ADDRESS || "hi@opensox.ai";
44+
const fromName =
45+
process.env.ZEPTOMAIL_FROM_NAME || "Ajeet from Opensox AI";
46+
47+
await client.sendMail({
48+
from: {
49+
address: fromAddress,
50+
name: fromName,
51+
},
52+
to: input.to.map((recipient) => ({
53+
email_address: {
54+
address: recipient.address,
55+
name: recipient.name,
56+
},
57+
})),
58+
subject: input.subject,
59+
htmlbody: input.htmlBody,
60+
...(input.textBody && { textbody: input.textBody }),
61+
});
62+
63+
return true;
64+
} catch (error) {
65+
if (process.env.NODE_ENV !== "production") {
66+
console.error("Email sending error:", error);
67+
}
68+
throw new Error("Failed to send email");
69+
}
70+
},
71+
72+
/**
73+
* Send premium subscription confirmation email
74+
* This is sent when a user subscribes to Opensox Premium
75+
*/
76+
async sendPremiumSubscriptionEmail(
77+
email: string,
78+
firstName: string
79+
): Promise<boolean> {
80+
const htmlBody = `
81+
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto; padding: 20px;">
82+
<p style="color: #333; line-height: 1.8; font-size: 16px;">
83+
Hi ${firstName},
84+
</p>
85+
86+
<p style="color: #333; line-height: 1.8; font-size: 16px;">
87+
I am Ajeet, founder of Opensox AI.
88+
</p>
89+
90+
<p style="color: #333; line-height: 1.8; font-size: 16px;">
91+
First of all, <strong>thank you for believing in me and Opensox AI.</strong>
92+
</p>
93+
94+
<p style="color: #333; line-height: 1.8; font-size: 16px;">
95+
Throughout this journey, I will make sure you get the best value for your money.
96+
</p>
97+
98+
<p style="color: #333; line-height: 1.8; font-size: 16px;">
99+
To get started, please book your slot in the cal meet.
100+
</p>
101+
102+
<p style="color: #333; line-height: 1.8; font-size: 16px;">
103+
Let's have a chat over a meeting so that I can understand where you are currently and how I can help you move ahead.
104+
</p>
105+
106+
<p style="color: #333; line-height: 1.8; font-size: 16px;">
107+
Soon, I'll add you to the private Slack so that we can talk regularly.
108+
</p>
109+
110+
<p style="color: #333; line-height: 1.8; font-size: 16px;">
111+
Again, thank you :)
112+
</p>
113+
114+
<div style="margin: 30px 0; text-align: center;">
115+
<a href="https://cal.com/ajeetunc/secret"
116+
style="background-color: #007bff; color: white; padding: 14px 32px;
117+
text-decoration: none; border-radius: 6px; display: inline-block;
118+
font-weight: 600; font-size: 16px;">
119+
📅 Book Your Meeting
120+
</a>
121+
</div>
122+
123+
<p style="color: #333; line-height: 1.8; font-size: 16px;">
124+
<strong>Cal:</strong> <a href="https://cal.com/ajeetunc/secret" style="color: #007bff;">https://cal.com/ajeetunc/secret</a>
125+
</p>
126+
127+
<p style="color: #333; line-height: 1.8; font-size: 16px; margin-top: 30px;">
128+
Best,<br/>
129+
Ajeet from Opensox.ai
130+
</p>
131+
</div>
132+
`;
133+
134+
const textBody = `Hi ${firstName},
135+
136+
I am Ajeet, founder of Opensox AI.
137+
138+
First of all, thank you for believing in me and Opensox AI.
139+
140+
Throughout this journey, I will make sure you get the best value for your money.
141+
142+
To get started, please book your slot in the cal meet.
143+
144+
Let's have a chat over a meeting so that I can understand where you are currently and how I can help you move ahead.
145+
146+
Soon, I'll add you to the private Slack so that we can talk regularly.
147+
148+
Again, thank you :)
149+
150+
Cal: https://cal.com/ajeetunc/secret
151+
152+
Best,
153+
Ajeet from Opensox.ai`;
154+
155+
return this.sendEmail({
156+
to: [{ address: email, name: firstName }],
157+
subject: "Congratulations! You are in.",
158+
htmlBody,
159+
textBody,
160+
});
161+
},
162+
};

pnpm-lock.yaml

Lines changed: 43 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)