[Feat] Notify via Email When codeflash --all Completes (#1699)

### Without repo
<img width="744" alt="Screenshot 2025-07-08 at 1 16 26 PM"
src="https://github.com/user-attachments/assets/f095ce35-24be-4add-bb19-306220f09d74"
/>

### With repo

<img width="803" alt="Screenshot 2025-07-08 at 1 16 43 PM"
src="https://github.com/user-attachments/assets/c12fd194-abe1-410a-b78d-2c33957ca510"
/>

---------

Co-authored-by: Sarthak Agarwal <sarthak.saga@gmail.com>
This commit is contained in:
HeshamHM28 2025-07-24 18:29:41 +03:00 committed by GitHub
parent 7da78da953
commit fedadfc6ce
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 635 additions and 2 deletions

View file

@ -0,0 +1,60 @@
import * as Sentry from "@sentry/node"
import { prisma } from "@codeflash-ai/common"
import { loadAndRenderHtml, sendEmail } from "resend/email-service.js"
import { Response, Request } from "express"
export async function sendOptimizationCompletedEmail(req: Request, res: Response): Promise<void> {
const user = await prisma.users.findUnique({
where: { user_id: (req as any).userId },
})
if (user?.email && user?.name) {
const { repo, owner } = req.body
const showRepo = owner && repo
try {
const html = await loadAndRenderHtml("resend/completed_optimization_email_template.html", {
userName: user.github_username,
...{
PR: showRepo
? ` <div style="text-align: center; margin: 30px 0;">
<a href="https://github.com/${owner}/${repo}/pulls/app%2Fcodeflash-ai" class="cta-button">
<span class="pr-icon">🔀</span>
View Pull Requests
</a>
</div>
</div>`
: "",
repoHtml: showRepo
? `<a
href="https://github.com/${owner}/${repo}"
target="_blank"
style="text-decoration: none; color: inherit"
>
<div class="repo-info">
<img
class="repo-avatar"
src="https://github.com/${owner}.png?size=20"
alt="${owner}'s avatar"
/>
<div class="repo-name-text">${owner}/${repo}</div>
</div>
</a>`
: "",
},
})
await sendEmail({
to: user.email,
subject: `Codeflash: Optimization Completed${showRepo ? ` For ${owner}/${repo}` : ""}`,
html,
})
res.status(200).json({ status: "success", message: "Email has been successfully sent." })
return
} catch (error) {
console.log(error)
Sentry.captureException(error)
res.status(500).json({ status: "error", message: "Failed to send email." })
return
}
}
}

View file

@ -30,6 +30,8 @@ import {
} from "./endpoints/code-context-hash.js"
import { optimizationSuccess } from "./endpoints/optimiaztion-success.js"
import { handleSlackEvents } from "./endpoints/slack-events.js"
import { sendOptimizationCompletedEmail } from "./endpoints/send-completed-optimization-email.js"
const port = process.env.PORT ?? 3001
// Define a custom type for the wrapped Express app
const app = express()
@ -156,6 +158,8 @@ appExpress.get("/cfapi/cli-get-user", getUser)
// appExpress.getAsync("/cfapi/installed_repositories", installedRepositories)
appExpress.postAsync("/cfapi/test-repo", addRepositoryManually)
appExpress.postAsync("/cfapi/send-completion-email", sendOptimizationCompletedEmail)
appExpress.postAsync("/cfapi/mark-as-success", optimizationSuccess)
appExpress.postAsync("/cfapi/suggest-pr-changes", suggestPrChanges)

View file

@ -36,9 +36,11 @@
"express": "^4.19.2",
"express-rate-limit": "^7.5.0",
"http": "^0.0.1-security",
"marked": "^16.0.0",
"node-fetch": "^3.3.2",
"octokit": "^5.0.2",
"posthog-node": "^4.0.0",
"resend": "^4.6.0",
"simple-git-hooks": "^2.9.0",
"stripe": "^17.7.0",
"tsx": "^4.1.4"
@ -4376,6 +4378,24 @@
"@opentelemetry/api": "^1.3.0"
}
},
"node_modules/@react-email/render": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@react-email/render/-/render-1.1.2.tgz",
"integrity": "sha512-RnRehYN3v9gVlNMehHPHhyp2RQo7+pSkHDtXPvg3s0GbzM9SQMW4Qrf8GRNvtpLC4gsI+Wt0VatNRUFqjvevbw==",
"license": "MIT",
"dependencies": {
"html-to-text": "^9.0.5",
"prettier": "^3.5.3",
"react-promise-suspense": "^0.3.4"
},
"engines": {
"node": ">=18.0.0"
},
"peerDependencies": {
"react": "^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^18.0 || ^19.0 || ^19.0.0-rc"
}
},
"node_modules/@rtsao/scc": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz",
@ -4383,6 +4403,19 @@
"dev": true,
"license": "MIT"
},
"node_modules/@selderee/plugin-htmlparser2": {
"version": "0.11.0",
"resolved": "https://registry.npmjs.org/@selderee/plugin-htmlparser2/-/plugin-htmlparser2-0.11.0.tgz",
"integrity": "sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ==",
"license": "MIT",
"dependencies": {
"domhandler": "^5.0.3",
"selderee": "^0.11.0"
},
"funding": {
"url": "https://ko-fi.com/killymxi"
}
},
"node_modules/@sentry-internal/node-cpu-profiler": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/@sentry-internal/node-cpu-profiler/-/node-cpu-profiler-2.2.0.tgz",
@ -6572,7 +6605,6 @@
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz",
"integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
@ -6762,6 +6794,61 @@
"node": ">=6.0.0"
}
},
"node_modules/dom-serializer": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz",
"integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==",
"license": "MIT",
"dependencies": {
"domelementtype": "^2.3.0",
"domhandler": "^5.0.2",
"entities": "^4.2.0"
},
"funding": {
"url": "https://github.com/cheeriojs/dom-serializer?sponsor=1"
}
},
"node_modules/domelementtype": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz",
"integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/fb55"
}
],
"license": "BSD-2-Clause"
},
"node_modules/domhandler": {
"version": "5.0.3",
"resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz",
"integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==",
"license": "BSD-2-Clause",
"dependencies": {
"domelementtype": "^2.3.0"
},
"engines": {
"node": ">= 4"
},
"funding": {
"url": "https://github.com/fb55/domhandler?sponsor=1"
}
},
"node_modules/domutils": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz",
"integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==",
"license": "BSD-2-Clause",
"dependencies": {
"dom-serializer": "^2.0.0",
"domelementtype": "^2.3.0",
"domhandler": "^5.0.3"
},
"funding": {
"url": "https://github.com/fb55/domutils?sponsor=1"
}
},
"node_modules/dotenv": {
"version": "16.5.0",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.5.0.tgz",
@ -6855,6 +6942,18 @@
"node": ">= 0.8"
}
},
"node_modules/entities": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
"license": "BSD-2-Clause",
"engines": {
"node": ">=0.12"
},
"funding": {
"url": "https://github.com/fb55/entities?sponsor=1"
}
},
"node_modules/environment": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz",
@ -8491,6 +8590,41 @@
"dev": true,
"license": "MIT"
},
"node_modules/html-to-text": {
"version": "9.0.5",
"resolved": "https://registry.npmjs.org/html-to-text/-/html-to-text-9.0.5.tgz",
"integrity": "sha512-qY60FjREgVZL03vJU6IfMV4GDjGBIoOyvuFdpBDIX9yTlDw0TjxVBQp+P8NvpdIXNJvfWBTNul7fsAQJq2FNpg==",
"license": "MIT",
"dependencies": {
"@selderee/plugin-htmlparser2": "^0.11.0",
"deepmerge": "^4.3.1",
"dom-serializer": "^2.0.0",
"htmlparser2": "^8.0.2",
"selderee": "^0.11.0"
},
"engines": {
"node": ">=14"
}
},
"node_modules/htmlparser2": {
"version": "8.0.2",
"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz",
"integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==",
"funding": [
"https://github.com/fb55/htmlparser2?sponsor=1",
{
"type": "github",
"url": "https://github.com/sponsors/fb55"
}
],
"license": "MIT",
"dependencies": {
"domelementtype": "^2.3.0",
"domhandler": "^5.0.3",
"domutils": "^3.0.1",
"entities": "^4.4.0"
}
},
"node_modules/http": {
"version": "0.0.1-security",
"resolved": "https://registry.npmjs.org/http/-/http-0.0.1-security.tgz",
@ -10120,6 +10254,15 @@
"node": ">=6"
}
},
"node_modules/leac": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/leac/-/leac-0.6.0.tgz",
"integrity": "sha512-y+SqErxb8h7nE/fiEX07jsbuhrpO9lL8eca7/Y1nuWV2moNlXhyd59iDGcRf6moVyDMbmTNzL40SUyrFU/yDpg==",
"license": "MIT",
"funding": {
"url": "https://ko-fi.com/killymxi"
}
},
"node_modules/leven": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz",
@ -10593,6 +10736,18 @@
"tmpl": "1.0.5"
}
},
"node_modules/marked": {
"version": "16.0.0",
"resolved": "https://registry.npmjs.org/marked/-/marked-16.0.0.tgz",
"integrity": "sha512-MUKMXDjsD/eptB7GPzxo4xcnLS6oo7/RHimUMHEDRhUooPwmN9BEpMl7AEOJv3bmso169wHI2wUF9VQgL7zfmA==",
"license": "MIT",
"bin": {
"marked": "bin/marked.js"
},
"engines": {
"node": ">= 20"
}
},
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
@ -11234,6 +11389,19 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/parseley": {
"version": "0.12.1",
"resolved": "https://registry.npmjs.org/parseley/-/parseley-0.12.1.tgz",
"integrity": "sha512-e6qHKe3a9HWr0oMRVDTRhKce+bRO8VGQR3NyVwcjwrbhMmFCX9KszEV35+rn4AdilFAq9VPxP/Fe1wC9Qjd2lw==",
"license": "MIT",
"dependencies": {
"leac": "^0.6.0",
"peberminta": "^0.9.0"
},
"funding": {
"url": "https://ko-fi.com/killymxi"
}
},
"node_modules/parseurl": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
@ -11294,6 +11462,15 @@
"node": ">=8"
}
},
"node_modules/peberminta": {
"version": "0.9.0",
"resolved": "https://registry.npmjs.org/peberminta/-/peberminta-0.9.0.tgz",
"integrity": "sha512-XIxfHpEuSJbITd1H3EeQwpcZbTLHc+VVr8ANI9t5sit565tsI4/xK3KWTUFE2e6QiangUkh3B0jihzmGnNrRsQ==",
"license": "MIT",
"funding": {
"url": "https://ko-fi.com/killymxi"
}
},
"node_modules/pg-int8": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz",
@ -11512,7 +11689,6 @@
"version": "3.5.3",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.3.tgz",
"integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==",
"dev": true,
"license": "MIT",
"bin": {
"prettier": "bin/prettier.cjs"
@ -11705,6 +11881,29 @@
"node": ">= 0.8"
}
},
"node_modules/react": {
"version": "19.1.0",
"resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz",
"integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/react-dom": {
"version": "19.1.0",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz",
"integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==",
"license": "MIT",
"peer": true,
"dependencies": {
"scheduler": "^0.26.0"
},
"peerDependencies": {
"react": "^19.1.0"
}
},
"node_modules/react-is": {
"version": "18.3.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz",
@ -11712,6 +11911,21 @@
"dev": true,
"license": "MIT"
},
"node_modules/react-promise-suspense": {
"version": "0.3.4",
"resolved": "https://registry.npmjs.org/react-promise-suspense/-/react-promise-suspense-0.3.4.tgz",
"integrity": "sha512-I42jl7L3Ze6kZaq+7zXWSunBa3b1on5yfvUW6Eo/3fFOj6dZ5Bqmcd264nJbTK/gn1HjjILAjSwnZbV4RpSaNQ==",
"license": "MIT",
"dependencies": {
"fast-deep-equal": "^2.0.1"
}
},
"node_modules/react-promise-suspense/node_modules/fast-deep-equal": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz",
"integrity": "sha512-bCK/2Z4zLidyB4ReuIsvALH6w31YfAQDmXMqMx6FyfHqvBxtjC0eRumeSu4Bs3XtXwpyIywtSTrVT99BxY1f9w==",
"license": "MIT"
},
"node_modules/readable-stream": {
"version": "1.0.34",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz",
@ -11792,6 +12006,18 @@
"node": ">=8.6.0"
}
},
"node_modules/resend": {
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/resend/-/resend-4.6.0.tgz",
"integrity": "sha512-D5T2I82FvEUYFlrHzaDvVtr5ADHdhuoLaXgLFGABKyNtQgPWIuz0Vp2L2Evx779qjK37aF4kcw1yXJDHhA2JnQ==",
"license": "MIT",
"dependencies": {
"@react-email/render": "1.1.2"
},
"engines": {
"node": ">=18"
}
},
"node_modules/resolve": {
"version": "1.22.10",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz",
@ -12085,6 +12311,25 @@
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
"license": "MIT"
},
"node_modules/scheduler": {
"version": "0.26.0",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz",
"integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==",
"license": "MIT",
"peer": true
},
"node_modules/selderee": {
"version": "0.11.0",
"resolved": "https://registry.npmjs.org/selderee/-/selderee-0.11.0.tgz",
"integrity": "sha512-5TF+l7p4+OsnP8BCCvSyZiSPc4x4//p5uPwK8TCnVPJYRmU2aYKMpOXvw8zM5a5JvuuCGN1jmsMwuU2W02ukfA==",
"license": "MIT",
"dependencies": {
"parseley": "^0.12.0"
},
"funding": {
"url": "https://ko-fi.com/killymxi"
}
},
"node_modules/semver": {
"version": "7.7.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",

View file

@ -51,9 +51,11 @@
"express": "^4.19.2",
"express-rate-limit": "^7.5.0",
"http": "^0.0.1-security",
"marked": "^16.0.0",
"node-fetch": "^3.3.2",
"octokit": "^5.0.2",
"posthog-node": "^4.0.0",
"resend": "^4.6.0",
"simple-git-hooks": "^2.9.0",
"stripe": "^17.7.0",
"tsx": "^4.1.4"

View file

@ -0,0 +1,244 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Optimization Complete</title>
<style>
body {
margin: 0;
padding: 0;
font-family:
-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
background-color: hsl(0, 0%, 99%);
color: hsl(222.2, 84%, 4.9%);
line-height: 1.6;
}
.email-container {
max-width: 600px;
margin: 0 auto;
background-color: hsl(0, 0%, 100%);
border-radius: 8px;
overflow: hidden;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
.header {
background: linear-gradient(135deg, hsl(38, 100%, 63%), hsl(41, 88%, 70%));
padding: 30px 40px;
text-align: center;
color: hsl(0, 6%, 4%);
}
.logo-wrapper {
display: inline-block;
background-color: hsl(0, 0%, 100%);
padding: 10px 20px;
border-radius: 6px;
margin-bottom: 15px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.header h1 {
margin: 0;
font-size: 24px;
font-weight: 600;
}
.repo-info {
display: inline-flex;
align-items: center;
background-color: #f6f8fa;
border: 1px solid #d0d7de;
border-radius: 6px;
padding: 4px 12px;
gap: 8px;
font-size: 14px;
font-weight: 500;
transition: background-color 0.2s ease;
cursor: pointer;
max-height: 32px;
}
.repo-info:hover {
background-color: #eaeef2;
}
.repo-avatar {
width: 18px;
height: 18px;
border-radius: 50%;
}
.repo-name-text {
color: #0969da;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
/* Hide repo info if empty */
.repo-info:empty {
display: none;
}
.content {
padding: 40px;
}
.greeting {
font-size: 16px;
margin-bottom: 20px;
color: hsl(222.2, 84%, 4.9%);
}
.main-message {
background-color: hsl(41, 20%, 96%);
border-left: 4px solid hsl(38, 100%, 63%);
padding: 20px;
margin: 20px 0;
border-radius: 0 6px 6px 0;
}
.main-message h2 {
margin: 0 0 10px 0;
font-size: 18px;
color: hsl(38, 100%, 35%);
}
.main-message p {
margin: 0;
color: hsl(41, 8%, 46%);
}
.repo-details {
display: flex;
align-items: center;
gap: 12px;
margin-top: 16px;
padding: 12px;
background-color: rgba(0, 0, 0, 0.02);
border-radius: 6px;
}
.repo-details:empty {
display: none;
}
.repo-details img {
width: 40px;
height: 40px;
border-radius: 6px;
object-fit: cover;
}
.repo-details-text {
flex: 1;
}
.repo-name-text {
font-weight: 600;
color: hsl(222.2, 84%, 4.9%);
font-size: 14px;
}
.cta-button {
display: inline-block;
background-color: hsl(38, 100%, 63%);
color: hsl(0, 6%, 4%);
text-decoration: none;
padding: 12px 24px;
border-radius: 6px;
font-weight: 500;
margin: 20px 0;
transition: background-color 0.3s ease;
font-size: 16px;
}
.cta-button:hover {
background-color: hsl(38, 100%, 55%);
}
.cta-button:active {
transform: translateY(1px);
}
.pr-icon {
display: inline-block;
margin-right: 8px;
font-size: 14px;
}
.footer {
background-color: hsl(41, 20%, 96%);
padding: 20px 40px;
text-align: center;
color: hsl(41, 8%, 46%);
font-size: 14px;
}
.footer a {
color: hsl(38, 100%, 63%);
text-decoration: none;
}
.divider {
height: 1px;
background-color: hsl(41, 30%, 90%);
margin: 20px 0;
}
@media (max-width: 600px) {
.email-container {
margin: 0;
border-radius: 0;
}
.content {
padding: 20px;
}
.header {
padding: 20px;
}
}
</style>
</head>
<body>
<div class="email-container">
<div class="header">
<div class="logo-wrapper">
<img
src="https://app.codeflash.ai/images/codeflash_light.svg"
alt="Codeflash Logo"
style="height: 40px; width: auto; vertical-align: middle"
/>
</div>
<h1>Optimization Completed</h1>
{{repoHtml}}
</div>
<div class="content">
<div class="greeting">Dear {{userName}},</div>
<div class="main-message">
<h2>Optimization Completed</h2>
<p>We are pleased to inform you that your optimization has been successfully completed. A pull request has been created with the optimized code changes.</p>
</div>
{{PR}}
<div class="footer">
<p>
Best regards,<br />
Codeflash Team
</p>
<p style="margin-top: 15px; font-size: 12px">
This is an automated notification. Please do not reply to this email.
</p>
</div>
</div>
</body>
</html>

View file

@ -0,0 +1,78 @@
import { Resend } from "resend"
import dotenv from "dotenv"
import fs from "fs"
import path from "path"
import { marked } from "marked"
dotenv.config()
const resend = new Resend(process.env.RESEND_API_KEY!)
function escapeRegex(string: string) {
return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
}
function flattenObject(
obj: any,
prefix = "",
res: Record<string, string> = {},
): Record<string, string> {
for (const key in obj) {
const value = obj[key]
const fullKey = prefix ? `${prefix}.${key}` : key
if (typeof value === "object" && value !== null) {
flattenObject(value, fullKey, res)
} else {
res[fullKey] = String(value)
}
}
return res
}
export async function loadAndRenderHtml(
templatePath: string,
params: Record<string, any>,
): Promise<string> {
const rawHtml = fs.readFileSync(path.resolve(templatePath), "utf-8")
const flatParams = flattenObject(params)
if (flatParams.optimization_explanation) {
flatParams.optimization_explanation = await marked.parse(flatParams.optimization_explanation)
}
let renderedHtml = rawHtml
for (const [key, value] of Object.entries(flatParams)) {
const safeKey = escapeRegex(key)
const regex = new RegExp(`{{\\s*${safeKey}\\s*}}`, "g")
renderedHtml = renderedHtml.replace(regex, value ?? "")
}
return renderedHtml
}
export async function sendEmail({
to,
subject,
html,
}: {
to: string
subject: string
html: string
}) {
const fromEmail = process.env.RESEND_FROM_EMAIL
if (!fromEmail || fromEmail.trim() === "") {
throw new Error("RESEND_FROM_EMAIL environment variable is missing or empty")
}
// TODO: Add logic to check if the email was already sent (e.g., using a database or cache)
console.log(fromEmail)
console.log(`Preparing to send email to ${to} with subject "${subject}"`)
const response = await resend.emails.send({
from: fromEmail,
to: [to],
subject,
html,
})
console.log(`Email send response: ${JSON.stringify(response)}`)
return response
}