mirror of
https://github.com/mosh-hamedani/helpdesk.git
synced 2026-05-21 11:58:19 +02:00
Add the ability to send emails
This commit is contained in:
parent
f98646d7ac
commit
f5cd564852
5
bun.lock
5
bun.lock
|
|
@ -70,6 +70,7 @@
|
|||
"@prisma/adapter-pg": "^7.3.0",
|
||||
"@prisma/client": "^7.3.0",
|
||||
"@sendgrid/inbound-mail-parser": "^8.0.0",
|
||||
"@sendgrid/mail": "^8.1.6",
|
||||
"@types/multer": "^2.0.0",
|
||||
"ai": "^6.0.100",
|
||||
"better-auth": "^1.4.18",
|
||||
|
|
@ -559,10 +560,14 @@
|
|||
|
||||
"@sec-ant/readable-stream": ["@sec-ant/readable-stream@0.4.1", "", {}, "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg=="],
|
||||
|
||||
"@sendgrid/client": ["@sendgrid/client@8.1.6", "", { "dependencies": { "@sendgrid/helpers": "^8.0.0", "axios": "^1.12.0" } }, "sha512-/BHu0hqwXNHr2aLhcXU7RmmlVqrdfrbY9KpaNj00KZHlVOVoRxRVrpOCabIB+91ISXJ6+mLM9vpaVUhK6TwBWA=="],
|
||||
|
||||
"@sendgrid/helpers": ["@sendgrid/helpers@8.0.0", "", { "dependencies": { "deepmerge": "^4.2.2" } }, "sha512-Ze7WuW2Xzy5GT5WRx+yEv89fsg/pgy3T1E3FS0QEx0/VvRmigMZ5qyVGhJz4SxomegDkzXv/i0aFPpHKN8qdAA=="],
|
||||
|
||||
"@sendgrid/inbound-mail-parser": ["@sendgrid/inbound-mail-parser@8.0.0", "", { "dependencies": { "@sendgrid/helpers": "^8.0.0", "mailparser": "^2.3.4" } }, "sha512-y7tH39f3uszeYlq6SGiO3npMpzAGsTWqIl46YjvJNHvaYP7B85YGcINIhm4CPdBA+dA+2wFYbDyvxFVt4ZhpcA=="],
|
||||
|
||||
"@sendgrid/mail": ["@sendgrid/mail@8.1.6", "", { "dependencies": { "@sendgrid/client": "^8.1.5", "@sendgrid/helpers": "^8.0.0" } }, "sha512-/ZqxUvKeEztU9drOoPC/8opEPOk+jLlB2q4+xpx6HVLq6aFu3pMpalkTpAQz8XfRfpLp8O25bh6pGPcHDCYpqg=="],
|
||||
|
||||
"@sindresorhus/merge-streams": ["@sindresorhus/merge-streams@4.0.0", "", {}, "sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ=="],
|
||||
|
||||
"@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
|
||||
|
|
|
|||
|
|
@ -16,5 +16,8 @@ WEBHOOK_SECRET="" # Required for inbound email webhook
|
|||
|
||||
OPENAI_API_KEY="" # Required for AI polish feature
|
||||
|
||||
SENDGRID_API_KEY="" # Required for sending outbound emails
|
||||
SENDGRID_FROM_EMAIL="" # Verified sender address (e.g. support@yourdomain.com)
|
||||
|
||||
SEED_ADMIN_EMAIL="admin@example.com"
|
||||
SEED_ADMIN_PASSWORD="" # Use a strong password
|
||||
|
|
@ -20,6 +20,7 @@
|
|||
"@prisma/adapter-pg": "^7.3.0",
|
||||
"@prisma/client": "^7.3.0",
|
||||
"@sendgrid/inbound-mail-parser": "^8.0.0",
|
||||
"@sendgrid/mail": "^8.1.6",
|
||||
"@types/multer": "^2.0.0",
|
||||
"ai": "^6.0.100",
|
||||
"better-auth": "^1.4.18",
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import type { PgBoss } from "pg-boss";
|
|||
import { generateText } from "ai";
|
||||
import { openai } from "@ai-sdk/openai";
|
||||
import prisma from "../db";
|
||||
import { sendEmailJob } from "./send-email";
|
||||
|
||||
const QUEUE_NAME = "auto-resolve-ticket";
|
||||
|
||||
|
|
@ -17,6 +18,7 @@ interface AutoResolveJobData {
|
|||
subject: string;
|
||||
body: string;
|
||||
senderName: string;
|
||||
senderEmail: string;
|
||||
}
|
||||
|
||||
export async function registerAutoResolveWorker(boss: PgBoss): Promise<void> {
|
||||
|
|
@ -27,7 +29,7 @@ export async function registerAutoResolveWorker(boss: PgBoss): Promise<void> {
|
|||
});
|
||||
|
||||
await boss.work<AutoResolveJobData>(QUEUE_NAME, async (jobs) => {
|
||||
const { ticketId, subject, body, senderName } = jobs[0]!.data;
|
||||
const { ticketId, subject, body, senderName, senderEmail } = jobs[0]!.data;
|
||||
const firstName = senderName.split(" ")[0];
|
||||
|
||||
await prisma.ticket.update({
|
||||
|
|
@ -85,6 +87,12 @@ export async function registerAutoResolveWorker(boss: PgBoss): Promise<void> {
|
|||
data: { status: "resolved" },
|
||||
}),
|
||||
]);
|
||||
|
||||
await sendEmailJob({
|
||||
to: senderEmail,
|
||||
subject: `Re: ${subject}`,
|
||||
body: response,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -94,6 +102,7 @@ export async function sendAutoResolveJob(ticket: {
|
|||
subject: string;
|
||||
body: string;
|
||||
senderName: string;
|
||||
senderEmail: string;
|
||||
}): Promise<void> {
|
||||
const { boss } = await import("./queue");
|
||||
await boss.send(QUEUE_NAME, {
|
||||
|
|
@ -101,5 +110,6 @@ export async function sendAutoResolveJob(ticket: {
|
|||
subject: ticket.subject,
|
||||
body: ticket.body,
|
||||
senderName: ticket.senderName,
|
||||
senderEmail: ticket.senderEmail,
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { PgBoss } from "pg-boss";
|
||||
import { registerClassifyWorker } from "./classify-ticket";
|
||||
import { registerAutoResolveWorker } from "./auto-resolve-ticket";
|
||||
import { registerSendEmailWorker } from "./send-email";
|
||||
|
||||
const boss = new PgBoss({
|
||||
connectionString: process.env.DATABASE_URL!,
|
||||
|
|
@ -15,6 +16,7 @@ export async function startQueue(): Promise<void> {
|
|||
|
||||
await registerClassifyWorker(boss);
|
||||
await registerAutoResolveWorker(boss);
|
||||
await registerSendEmailWorker(boss);
|
||||
|
||||
console.log("Job queue started");
|
||||
}
|
||||
|
|
|
|||
40
server/src/lib/send-email.ts
Normal file
40
server/src/lib/send-email.ts
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
import sgMail from "@sendgrid/mail";
|
||||
import type { PgBoss } from "pg-boss";
|
||||
|
||||
const QUEUE_NAME = "send-email";
|
||||
|
||||
interface SendEmailJobData {
|
||||
to: string;
|
||||
subject: string;
|
||||
body: string;
|
||||
bodyHtml?: string;
|
||||
}
|
||||
|
||||
export async function registerSendEmailWorker(boss: PgBoss): Promise<void> {
|
||||
await boss.createQueue(QUEUE_NAME, {
|
||||
retryLimit: 3,
|
||||
retryDelay: 30,
|
||||
retryBackoff: true,
|
||||
});
|
||||
|
||||
await boss.work<SendEmailJobData>(QUEUE_NAME, async (jobs) => {
|
||||
const { to, subject, body, bodyHtml } = jobs[0]!.data;
|
||||
|
||||
sgMail.setApiKey(process.env.SENDGRID_API_KEY!);
|
||||
|
||||
await sgMail.send({
|
||||
to,
|
||||
from: process.env.SENDGRID_FROM_EMAIL!,
|
||||
subject,
|
||||
text: body,
|
||||
...(bodyHtml && { html: bodyHtml }),
|
||||
});
|
||||
|
||||
console.log(`Email sent to ${to} — subject: "${subject}"`);
|
||||
});
|
||||
}
|
||||
|
||||
export async function sendEmailJob(data: SendEmailJobData): Promise<void> {
|
||||
const { boss } = await import("./queue");
|
||||
await boss.send(QUEUE_NAME, data);
|
||||
}
|
||||
|
|
@ -6,6 +6,7 @@ import { generateText } from "ai";
|
|||
import { openai } from "@ai-sdk/openai";
|
||||
import { createReplySchema, polishReplySchema } from "core/schemas/replies.ts";
|
||||
import prisma from "../db";
|
||||
import { sendEmailJob } from "../lib/send-email";
|
||||
|
||||
const router = Router({ mergeParams: true });
|
||||
|
||||
|
|
@ -57,6 +58,12 @@ router.post("/", requireAuth, async (req, res) => {
|
|||
include: { user: { select: { id: true, name: true } } },
|
||||
});
|
||||
|
||||
await sendEmailJob({
|
||||
to: ticket.senderEmail,
|
||||
subject: `Re: ${ticket.subject}`,
|
||||
body: data.body,
|
||||
});
|
||||
|
||||
res.status(201).json(reply);
|
||||
});
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue