Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions apps/web/src/instrumentation.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { env } from "./env";
import { isCloud , isEmailCleanupEnabled } from "./utils/common";
import { initDomainVerificationJob } from "~/server/jobs/domain-verification-job";
import { isCloud, isEmailCleanupEnabled } from "~/utils/common";

let initialized = false;

Expand All @@ -25,6 +25,10 @@ export async function register() {
await import("~/server/jobs/usage-job");
}

if (process.env.REDIS_URL) {
await initDomainVerificationJob();
}

if (isEmailCleanupEnabled()) {
await import("~/server/jobs/cleanup-email-bodies");
}
Expand Down
155 changes: 155 additions & 0 deletions apps/web/src/server/email-templates/DomainVerificationStatusEmail.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
import React from "react";
import { Container, Text } from "jsx-email";
import { render } from "jsx-email";
import { DomainStatus } from "@prisma/client";
import { EmailButton } from "~/server/email-templates/components/EmailButton";
import { EmailFooter } from "~/server/email-templates/components/EmailFooter";
import { EmailHeader } from "~/server/email-templates/components/EmailHeader";
import { EmailLayout } from "~/server/email-templates/components/EmailLayout";

interface DomainVerificationStatusEmailProps {
domainName: string;
currentStatus: DomainStatus;
previousStatus: DomainStatus;
verificationError?: string | null;
domainUrl: string;
}

function formatDomainStatus(status: DomainStatus) {
return status.toLowerCase().replaceAll("_", " ");
}

function getTitle(currentStatus: DomainStatus, previousStatus: DomainStatus) {
if (currentStatus === DomainStatus.SUCCESS) {
return previousStatus === DomainStatus.SUCCESS
? "Domain verification checked"
: "Your domain is verified";
}

if (previousStatus === DomainStatus.SUCCESS) {
return "Your domain status changed";
}

return "Your domain verification needs attention";
}

export function DomainVerificationStatusEmail({
domainName,
currentStatus,
previousStatus,
verificationError,
domainUrl,
}: DomainVerificationStatusEmailProps) {
const isSuccess = currentStatus === DomainStatus.SUCCESS;
const preview = `${domainName} is now ${formatDomainStatus(currentStatus)}`;

return (
<EmailLayout preview={preview}>
<EmailHeader title={getTitle(currentStatus, previousStatus)} />

<Container style={{ padding: "20px 0", textAlign: "left" as const }}>
<Text
style={{
fontSize: "16px",
color: "#374151",
margin: "0 0 16px 0",
lineHeight: "1.6",
textAlign: "left" as const,
}}
>
Hey,
</Text>

{isSuccess ? (
<Text
style={{
fontSize: "15px",
color: "#4b5563",
margin: "0 0 16px 0",
lineHeight: "1.6",
textAlign: "left" as const,
}}
>
Your domain <strong>{domainName}</strong> is now verified, and you
can start sending emails.
</Text>
) : (
<Text
style={{
fontSize: "15px",
color: "#4b5563",
margin: "0 0 16px 0",
lineHeight: "1.6",
textAlign: "left" as const,
}}
>
Your domain <strong>{domainName}</strong> could not be verified
because the DNS records are not set up correctly yet. Please review
your DNS settings and try again.
</Text>
)}

{verificationError ? (
<Container
style={{
backgroundColor: "#fef2f2",
border: "1px solid #fecaca",
padding: "12px 16px",
margin: "0 0 24px 0",
borderRadius: "4px",
}}
>
<Text
style={{
margin: 0,
color: "#991b1b",
fontSize: 14,
textAlign: "left" as const,
}}
>
Verification error: {verificationError}
</Text>
</Container>
) : null}

<Text
style={{
fontSize: "14px",
color: "#6b7280",
margin: "0 0 24px 0",
lineHeight: "1.6",
textAlign: "left" as const,
}}
>
Open your domain settings to review records and verification details.
</Text>

<Container style={{ margin: "0 0 32px 0", textAlign: "left" as const }}>
<EmailButton href={domainUrl}>Open domain settings</EmailButton>
</Container>

<Text
style={{
fontSize: "14px",
color: "#6b7280",
margin: "0",
lineHeight: "1.6",
textAlign: "left" as const,
}}
>
Thanks,
<br />
useSend Team
</Text>
</Container>

<EmailFooter />
</EmailLayout>
);
}

export async function renderDomainVerificationStatusEmail(
props: DomainVerificationStatusEmailProps,
): Promise<string> {
return render(<DomainVerificationStatusEmail {...props} />);
}
4 changes: 4 additions & 0 deletions apps/web/src/server/email-templates/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ export {
UsageLimitReachedEmail,
renderUsageLimitReachedEmail,
} from "./UsageLimitReachedEmail";
export {
DomainVerificationStatusEmail,
renderDomainVerificationStatusEmail,
} from "./DomainVerificationStatusEmail";

export * from "./components/EmailLayout";
export * from "./components/EmailHeader";
Expand Down
90 changes: 90 additions & 0 deletions apps/web/src/server/jobs/domain-verification-job.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { Queue, Worker } from "bullmq";
import { db } from "~/server/db";
import { logger } from "~/server/logger/log";
import { getRedis, BULL_PREFIX } from "~/server/redis";
import {
DOMAIN_VERIFICATION_QUEUE,
DEFAULT_QUEUE_OPTIONS,
} from "~/server/queue/queue-constants";
import {
isDomainVerificationDue,
refreshDomainVerification,
} from "~/server/service/domain-service";

let initialized = false;

export async function runDueDomainVerifications() {
const domains = await db.domain.findMany({
orderBy: {
createdAt: "asc",
},
});

for (const domain of domains) {
try {
const isDue = await isDomainVerificationDue(domain);
if (!isDue) {
continue;
}

await refreshDomainVerification(domain);
} catch (error) {
logger.error(
{ err: error, domainId: domain.id },
"[DomainVerificationJob]: Failed to refresh domain verification",
);
}
}
}

export async function initDomainVerificationJob() {
if (initialized) {
return;
}

const connection = getRedis();
const domainVerificationQueue = new Queue(DOMAIN_VERIFICATION_QUEUE, {
connection,
prefix: BULL_PREFIX,
skipVersionCheck: true,
});

const worker = new Worker(
DOMAIN_VERIFICATION_QUEUE,
async () => {
await runDueDomainVerifications();
},
{
connection,
concurrency: 1,
prefix: BULL_PREFIX,
skipVersionCheck: true,
},
);

await domainVerificationQueue.upsertJobScheduler(
"domain-verification-hourly",
{
pattern: "0 * * * *",
tz: "UTC",
},
{
opts: {
...DEFAULT_QUEUE_OPTIONS,
},
},
);

worker.on("completed", (job) => {
logger.info({ jobId: job.id }, "[DomainVerificationJob]: Job completed");
});

worker.on("failed", (job, err) => {
logger.error(
{ err, jobId: job?.id },
"[DomainVerificationJob]: Job failed",
);
});

initialized = true;
}
Loading
Loading