-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmailer.js
More file actions
184 lines (156 loc) · 5.68 KB
/
mailer.js
File metadata and controls
184 lines (156 loc) · 5.68 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
// mailer.js — Resend + templates (robust, utan debug)
import fs from "fs";
import path from "path";
import mustache from "mustache";
// Miljö
const FROM = (process.env.MAIL_FROM || "noreply@tuninghelpdesk.com")
.trim()
.replace(/^["']|["']$/g, ""); // ta bort ev. kringliggande citationstecken
const BASE = process.env.PUBLIC_BASE_URL || "http://localhost:3000";
const RESEND_API_KEY = process.env.RESEND_API_KEY;
// -------------------- Resend core --------------------
async function sendViaResend({ from = FROM, to, subject, text, html }) {
if (!RESEND_API_KEY) throw new Error("Missing RESEND_API_KEY");
const payload = { from, to: Array.isArray(to) ? to : [to], subject, text, html };
const resp = await fetch("https://api.resend.com/emails", {
method: "POST",
headers: {
Authorization: `Bearer ${RESEND_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify(payload),
});
if (!resp.ok) {
const errText = await resp.text().catch(() => "");
throw new Error(`Resend error ${resp.status}: ${errText || resp.statusText}`);
}
return resp.json();
}
export async function sendMail({ from = FROM, to, subject, text, html }) {
return sendViaResend({ from, to, subject, text, html });
}
// -------------------- Template rendering --------------------
function safeRead(filePath) {
try {
return fs.readFileSync(filePath, "utf8");
} catch {
return null;
}
}
function renderTemplate(fileName, vars) {
const emailsDir = path.join(process.cwd(), "emails");
const layout = safeRead(path.join(emailsDir, "layout.html"));
const partial = safeRead(path.join(emailsDir, fileName));
// Om mallar saknas -> enkel HTML så mailen ändå skickas
if (!layout || !partial) {
const simple = [
`<h1 style="font-family:system-ui,Segoe UI,Roboto,Inter,sans-serif">${escapeHtml(vars.subject || "Meddelande")}</h1>`,
vars.preheader ? `<p>${escapeHtml(vars.preheader)}</p>` : "",
vars.intro ? `<p>${escapeHtml(vars.intro)}</p>` : "",
vars.bodyHtml || "",
vars.url ? `<p><a href="${vars.url}">${escapeHtml(vars.ctaLabel || "Öppna")}</a></p>` : "",
].join("\n");
return simple;
}
const body = mustache.render(partial, vars);
// injicera baseUrl + body i layout (layout använder {{{content}}} och {{baseUrl}})
return mustache.render(layout, { ...vars, baseUrl: BASE, content: body });
}
// -------------------- DB helpers --------------------
function tableHasColumn(db, table, col) {
try {
const cols = db.prepare(`PRAGMA table_info(${table})`).all();
return cols.some((c) => c.name === col);
} catch {
return false;
}
}
export function getStaffEmails(db) {
const hasActive = tableHasColumn(db, "users", "active");
const whereActive = hasActive ? `AND COALESCE(active,1)=1` : ``;
const rows = db
.prepare(
`
SELECT email
FROM users
WHERE LOWER(role) IN ('admin','support')
${whereActive}
AND email IS NOT NULL
AND TRIM(email) <> ''
`
)
.all();
return rows.map((r) => String(r.email).trim()).filter((e) => e.includes("@"));
}
export function getAdminEmails(db) {
const hasActive = tableHasColumn(db, "users", "active");
const whereActive = hasActive ? `AND COALESCE(active,1)=1` : ``;
const rows = db
.prepare(
`
SELECT email
FROM users
WHERE LOWER(role)='admin'
${whereActive}
AND email IS NOT NULL
AND TRIM(email) <> ''
`
)
.all();
return rows.map((r) => String(r.email).trim()).filter((e) => e.includes("@"));
}
// -------------------- Notifierare (matchar templates) --------------------
export async function sendNewQuestionNotifications(db, { id, title, authorName }) {
const toList = getStaffEmails(db);
if (!toList.length) return;
const url = `${BASE}/questions/${encodeURIComponent(id)}`;
const subject = `Ny fråga: ${title}`;
// new-question.html väntar: {{author}}, {{title}}, {{url}}
const html = renderTemplate("new-question.html", {
author: authorName || "okänd",
title,
url,
subject,
});
const text = `Ny fråga av ${authorName || "okänd"}\nTitel: ${title}\nÖppna: ${url}`;
return sendViaResend({ to: toList, subject, text, html });
}
export async function sendQuestionAnswered(db, { id, title, userId }) {
const row = db.prepare(`SELECT email, name FROM users WHERE id=?`).get(userId);
if (!row || !row.email) return;
const url = `${BASE}/questions/${encodeURIComponent(id)}`;
const subject = `Ditt svar är klart: ${title}`;
// question-answered.html väntar: {{name}}, {{title}}, {{url}}
const html = renderTemplate("question-answered.html", {
name: row.name || "",
title,
url,
subject,
});
const text = `Hej ${row.name || ""}!\nDin fråga har besvarats.\nTitel: ${title}\nLäs svaret: ${url}`;
return sendViaResend({ to: row.email, subject, text, html });
}
export async function sendNewFeedbackNotifications(db, { id, category, message }) {
const toList = getAdminEmails(db);
if (!toList.length) return;
const url = `${BASE}/admin/feedback`;
const subject = `Ny feedback: ${category || "okänd kategori"}`;
// feedback.html väntar: {{category}}, {{message}}, {{url}}
const html = renderTemplate("feedback.html", {
category: category || "okänd kategori",
message: message || "",
url,
subject,
});
const text = `Ny feedback (${category || "okänd kategori"})\n${message || ""}\nVisa i admin: ${url}`;
return sendViaResend({ to: toList, subject, text, html });
}
// -------------------- Utils --------------------
function escapeHtml(s = "") {
return String(s)
.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">")
.replace(/"/g, """)
.replace(/'/g, "'");
}