· 3 months ago · Jul 06, 2025, 06:25 AM
1async function sendResetEmail(email, resetToken) {
2 const resetLink = `${process.env.FRONTEND_SERVER}/reset-password?token=${resetToken}`;
3 const subject = 'Password Reset Request';
4 const body = `
5 <html>
6 <body>
7 <p>Click the following link to reset your password:</p>
8 <a href="${resetLink}" style="color: #1a73e8; text-decoration: none;">${resetLink}</a>
9 </body>
10 </html>
11 `;
12 const data = {
13 fromAddress: 'info@pensa.club',
14 toAddress: email,
15 subject,
16 content: body,
17 };
18 return sendZohoEmailRaw(data);
19}
20
21async function forwardEmailsViaZoho({ name, userEmail, subject, body, toAddresses }) {
22 const formattedSubject = `[Contact Form] ${name} <${userEmail}> | Subject - ${subject}`;
23 const formattedBody = `
24 <html>
25 <body style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto; background: #f9f9f9;">
26 <div style="
27 padding: 12px 0 12px 16px;
28 margin: 20px 0 0 0;
29 text-align: left;
30 color: #222;
31 font-size: 20px;
32 font-weight: bold;
33 background: #fff;
34 position: relative;
35 overflow: hidden;
36 ">
37 <table width="100%" cellpadding="0" cellspacing="0" style="background: #fff; margin-bottom: 20px;">
38 <tr>
39 <td width="8" style="
40 background: linear-gradient(to bottom, #f47920, #2986c7);
41 background-color: #f47920;
42 border-radius: 4px;
43 ">
44
45 </td>
46 <td style="
47 padding: 4px 0 4px 12px;
48 color: #222;
49 font-size: 20px;
50 font-weight: bold;
51 ">
52 ${subject}
53 </td>
54 </tr>
55 </table>
56 </div>
57 <div style="background: #fff; padding: 20px; border-radius: 0 0 8px 8px; box-shadow: 0 4px 24px rgba(0,0,0,0.08); margin-top: 16px;">
58 <p><strong>From:</strong> ${name}</p>
59 <p><strong>Email:</strong> <a href="mailto:${userEmail}">${userEmail}</a></p>
60 <hr style="margin: 20px 0;">
61 <table cellpadding="0" cellspacing="0" style="margin-bottom: 10px;">
62 <tr>
63 <td width="8" style="
64 background: linear-gradient(to bottom, #f47920, #2986c7);
65 background-color: #f47920;
66 border-radius: 4px;
67 ">
68
69 </td>
70 <td style="
71 padding: 4px 0 4px 12px;
72 color: #222;
73 font-size: 18px;
74 font-weight: bold;
75 ">
76 Message:
77 </td>
78 </tr>
79 </table>
80 <div style="background: #f7f7f7; padding: 16px; border-radius: 6px; color: #222; font-size: 16px; line-height: 1.6; max-height: 300px; overflow-y: auto; word-break: break-word;">
81 ${body}
82 </div>
83 </div>
84 </body>
85 </html>
86 `;
87 const data = {
88 fromAddress: 'info@pensa.club',
89 toAddress: Array.isArray(toAddresses) ? toAddresses.join(',') : toAddresses,
90 subject: formattedSubject,
91 content: formattedBody,
92 };
93 return sendZohoEmailRaw(data);
94}
95
96async function sendZohoEmailRaw(data) {
97 const url = `https://mail.zoho.eu/api/accounts/${process.env.ZOHO_ACCOUNT_ID}/messages`;
98
99 if (!process.env.ZOHO_ACCESS_TOKEN) {
100 process.env.ZOHO_ACCESS_TOKEN = await getZohoAccessToken();
101 }
102
103 let response = await fetch(url, {
104 method: 'POST',
105 headers: {
106 Authorization: `Zoho-oauthtoken ${process.env.ZOHO_ACCESS_TOKEN}`,
107 'Content-Type': 'application/json',
108 },
109 body: JSON.stringify(data),
110 });
111
112 if (!response.ok) {
113 const errorData = await response.json();
114 if (errorData.data && errorData.data.errorCode === 'INVALID_OAUTHTOKEN') {
115 process.env.ZOHO_ACCESS_TOKEN = await getZohoAccessToken();
116 response = await fetch(url, {
117 method: 'POST',
118 headers: {
119 Authorization: `Zoho-oauthtoken ${process.env.ZOHO_ACCESS_TOKEN}`,
120 'Content-Type': 'application/json',
121 },
122 body: JSON.stringify(data),
123 });
124 }
125 if (!response.ok) {
126 throw new Error(`Failed to send email: ${JSON.stringify(errorData)}`);
127 }
128 }
129
130 return response.json();
131}
132
133async function getZohoAccessToken() {
134 const clientId = process.env.ZOHO_CLIENT_ID;
135 const clientSecret = process.env.ZOHO_CLIENT_SECRET;
136 const refreshToken = process.env.ZOHO_REFRESH_TOKEN;
137
138 if (!clientId || !clientSecret || !refreshToken) {
139 throw new Error('Missing required environment variables for Zoho access token.');
140 }
141
142 const url = `https://accounts.zoho.eu/oauth/v2/token`;
143 const params = new URLSearchParams();
144 params.append('refresh_token', refreshToken);
145 params.append('client_id', clientId);
146 params.append('client_secret', clientSecret);
147 params.append('grant_type', 'refresh_token');
148
149 try {
150 const response = await fetch(url, {
151 method: 'POST',
152 headers: {
153 'Content-Type': 'application/x-www-form-urlencoded',
154 },
155 body: params.toString(),
156 });
157
158 if (!response.ok) {
159 const errorData = await response.json();
160 throw new Error(`Failed to fetch access token: ${errorData.error}`);
161 }
162
163 const responseData = await response.json();
164 return responseData.access_token;
165 } catch (error) {
166 console.error('Error fetching access token:', error);
167 throw error;
168 }
169}
170
171async function sendProjectEmail({
172 to,
173 subject,
174 message,
175 title,
176 description,
177 category,
178 applicationDeadline,
179 currentParticipants,
180 maxParticipants,
181 status,
182 location,
183 link,
184}) {
185 const formattedSubject = `${subject}`;
186 const formattedBody = `
187 <html>
188 <head>
189 <meta charset="UTF-8">
190 <meta name="viewport" content="width=device-width, initial-scale=1.0">
191 </head>
192 <body style="font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; margin: 0; padding: 0; background: #f5f7fa; color: #222;">
193 <div style="max-width: 800px; margin: 0 auto; background: #ffffff;">
194 <!-- Header Table for Email Compatibility -->
195 <table width="100%" border="0" cellspacing="0" cellpadding="0" style="background: linear-gradient(to right, #FF7A3D, #FF965B); border-radius: 12px 12px 0 0;">
196 <tr>
197 <td align="center" style="padding: 32px 24px 0 24px;">
198 <!-- Circle with Rocket -->
199 <table border="0" cellspacing="0" cellpadding="0" style="margin: 0 auto;">
200 <tr>
201 <td align="center" valign="middle" style="
202 background: rgba(255,255,255,0.15);
203 border: none;
204 border-radius: 50%;
205 width: 80px;
206 height: 80px;
207 text-align: center;
208 vertical-align: middle;
209 padding-bottom: 5px;
210 background-clip: padding-box;
211 box-shadow: 0 0 0 0 transparent;
212 ">
213 <span style="font-size: 40px; line-height: 80px; display: inline-block; width: 80px; height: 80px; margin-top: -2px;">🚀</span>
214 </td>
215 </tr>
216 </table>
217 <!-- Pensa Club -->
218 <div style="color: #fff; font-size: 28px; font-weight: 700; margin: 8px 0 4px 0; text-shadow: 0 2px 4px rgba(0,0,0,0.1); text-align: center;">
219 Pensa Club
220 </div>
221 <!-- Subtitle -->
222 <div style="color: rgba(255,255,255,0.9); font-size: 16px; font-weight: 400; margin: 0 0 12px 0; text-align: center;">
223 Социална платформа
224 </div>
225 </td>
226 </tr>
227 </table>
228
229 <!-- Content -->
230 <div style="padding: 24px 24px 16px 24px;">
231 <div style="
232 background: #f8fafc;
233 border-left: 4px solid #667eea;
234 padding: 14px 18px;
235 border-radius: 8px;
236 margin-bottom: 18px;
237 ">
238 <div style="
239 color: #222;
240 font-size: 18px;
241 font-weight: 600;
242 margin-bottom: 6px;
243 ">
244 ${subject}
245 </div>
246 </div>
247
248 <div style="
249 color: #222;
250 font-size: 16px;
251 line-height: 1.6;
252 margin-bottom: 18px;
253 ">
254 ${message}
255 </div>
256
257 <!-- Project Info -->
258 <div style="
259 background: linear-gradient(135deg, #e6fffa 0%, #f0fff4 100%);
260 border: 1px solid #9ae6b4;
261 border-radius: 12px;
262 padding: 14px 18px;
263 margin-bottom: 18px;
264 ">
265 <h3 style="
266 color: #222;
267 font-size: 16px;
268 font-weight: 600;
269 margin: 0 0 8px 0;
270 ">
271 📋 Информация за проекта
272 </h3>
273 <p style="color: #222; font-size: 15px; margin: 0; line-height: 1.5;">
274 <strong>Проект:</strong> ${title || ''}<br>
275 <strong>Описание:</strong> ${description || ''}<br>
276 <strong>Категория:</strong> ${category || '—'}<br>
277 <strong>Краен срок за кандидатстване:</strong> ${applicationDeadline ? new Date(applicationDeadline).toLocaleDateString('bg-BG') : '—'
278 }<br>
279 <strong>Участници:</strong> ${currentParticipants || 0} / ${maxParticipants || '—'}<br>
280 <strong>Статус:</strong> ${status || '—'}<br>
281 <strong>Локация:</strong> ${location || '—'}
282 </p>
283 </div>
284
285 <!-- CTA -->
286 <div style="text-align: center; margin: 18px 0;">
287 <a href="${link}" style="
288 background: linear-gradient(to right, #FF7A3D, #FF965B);
289 color: white;
290 padding: 10px 22px;
291 border-radius: 8px;
292 text-decoration: none;
293 font-weight: 600;
294 font-size: 16px;
295 display: inline-block;
296 box-shadow: 0 4px 12px rgb(247, 154, 79, 0.3);
297 ">
298 🌐 Виж страницата на проекта
299 </a>
300 </div>
301 </div>
302
303 <!-- Footer: always visible, no overflow, black text -->
304 <div style="
305 background: #f7fafc;
306 padding: 14px 24px;
307 text-align: center;
308 border-top: 1px solid #e2e8f0;
309 border-radius: 0 0 12px 12px;
310 color: #222;
311 ">
312 <p style="color: #222; font-size: 14px; margin: 0 0 8px 0;">
313 Това съобщение е изпратено чрез платформата <strong>Pensa Club</strong>.
314 </p>
315 <p style="color: #222; font-size: 12px; margin: 0;">
316 Ако имате въпроси, свържете се с нас на <a href="mailto:info@pensa.club" style="color: #222;">info@pensa.club</a>
317 </p>
318 <p style="color: #222; font-size: 11px; margin: 8px 0 0 0;">
319 Благодарим Ви, че сте част от нашата общност!
320 </p>
321 </div>
322 </div>
323 </body>
324 </html>
325 `;
326
327 const emailAddresses = Array.isArray(to) ? to : [to];
328
329 for (const email of emailAddresses) {
330 const data = {
331 fromAddress: 'info@pensa.club',
332 toAddress: email,
333 subject: formattedSubject,
334 content: formattedBody,
335 };
336 await sendZohoEmailRaw(data);
337 }
338}
339
340module.exports = {
341 sendResetEmail,
342 forwardEmailsViaZoho,
343 sendProjectEmail,
344};
345