62 lines
1.7 KiB
TypeScript
62 lines
1.7 KiB
TypeScript
'use node';
|
|
|
|
import { v } from 'convex/values';
|
|
import { UseSend } from 'usesend-js';
|
|
|
|
import { internal } from './_generated/api';
|
|
import { internalAction } from './_generated/server';
|
|
|
|
/**
|
|
* Best-effort email dispatch for a notification. Runs the getForEmail query,
|
|
* sends via UseSend, and marks the notification emailed on success. Missing
|
|
* UseSend env is logged and swallowed — email is never allowed to throw.
|
|
*/
|
|
export const sendNotificationEmail = internalAction({
|
|
args: { notificationId: v.id('notifications') },
|
|
handler: async (ctx, { notificationId }) => {
|
|
const { to, title, body, link } = await ctx.runQuery(
|
|
internal.notifications.getForEmail,
|
|
{ notificationId },
|
|
);
|
|
|
|
if (!to) return;
|
|
|
|
const apiKey = process.env.USESEND_API_KEY;
|
|
const useSendUrl = process.env.USESEND_URL;
|
|
const from = process.env.USESEND_FROM_EMAIL;
|
|
if (!apiKey || !useSendUrl || !from) {
|
|
console.warn(
|
|
'Skipping notification email: USESEND_API_KEY, USESEND_URL, and USESEND_FROM_EMAIL must be set.',
|
|
);
|
|
return;
|
|
}
|
|
|
|
const text = link ? `${body}\n\n${link}` : body;
|
|
const html = `
|
|
<div style="max-width: 600px; margin: 0 auto; font-family: Arial, sans-serif;">
|
|
<h2>${title}</h2>
|
|
<p>${body}</p>
|
|
${link ? `<p><a href="${link}">${link}</a></p>` : ''}
|
|
</div>
|
|
`;
|
|
|
|
const useSend = new UseSend(apiKey, useSendUrl);
|
|
const result = await useSend.emails.send({
|
|
from,
|
|
to: [to],
|
|
subject: title,
|
|
text,
|
|
html,
|
|
});
|
|
|
|
if (result.error) {
|
|
console.error('UseSend error: ' + JSON.stringify(result.error));
|
|
return;
|
|
}
|
|
|
|
await ctx.runMutation(internal.notifications.markEmailed, {
|
|
notificationId,
|
|
});
|
|
},
|
|
});
|