diff --git a/README.md b/README.md index e2f57ce..312e4cb 100644 --- a/README.md +++ b/README.md @@ -189,6 +189,15 @@ This monorepo contains: - [x] Messages auto-create or use a thread - [x] Retrieve threads +### Domains (`/domains`) + +- [x] Create domain +- [x] List domains +- [x] Retrieve domain +- [x] Delete domain +- [x] Verify domain +- [x] Get domain DNS records + ### Tests - [x] Full API coverage diff --git a/api/README.md b/api/README.md deleted file mode 100644 index 898b670..0000000 --- a/api/README.md +++ /dev/null @@ -1,15 +0,0 @@ -# api - -To install dependencies: - -```bash -bun install -``` - -To run: - -```bash -bun run index.ts -``` - -This project was created using `bun init` in bun v1.3.0. [Bun](https://bun.com) is a fast all-in-one JavaScript runtime. diff --git a/api/controllers/DNSController.ts b/api/controllers/DNSController.ts index 550ac1e..7eae0bd 100644 --- a/api/controllers/DNSController.ts +++ b/api/controllers/DNSController.ts @@ -1,4 +1,4 @@ -import { resolveMx } from 'node:dns/promises'; +import { resolveMx, resolveTxt } from 'node:dns/promises'; import type { MxRecord } from 'node:dns'; export async function getDNSMXRecords({ @@ -13,3 +13,17 @@ export async function getDNSMXRecords({ return []; } } + +export async function getDNSTXTRecords({ + domain, +}: { + domain: string; +}): Promise { + try { + const records = await resolveTxt(domain); + return records[0] ?? []; + } catch (error) { + console.error('Error resolving TXT records:', error); + return []; + } +} diff --git a/api/controllers/DomainController.ts b/api/controllers/DomainController.ts index c17112e..c7e895c 100644 --- a/api/controllers/DomainController.ts +++ b/api/controllers/DomainController.ts @@ -1,7 +1,8 @@ import mongoose, { type HydratedDocument } from "mongoose"; import Domain from "../db/mongo/schemas/Domain"; -import { getDNSMXRecords } from "./DNSController"; +import { getDNSMXRecords, getDNSTXTRecords } from "./DNSController"; import type IDomain from "../models/Domain"; +import { getDomainVerificationStatus } from "./SESController"; export async function createDomain({ organizationId, @@ -91,7 +92,36 @@ export async function verifyDomainDNS({ if (mxRecordFound) { mxRecordFound.status = "verified"; } - domain.verified = mxRecordFound ? true : false; + + const dmarcRecord = await getDNSTXTRecords({ domain: `_dmarc.${domain.name}` }); + let dmarcRecordFound; + if (dmarcRecord && Array.isArray(dmarcRecord)) { + dmarcRecordFound = domain.records.find( + (domainRecord) => + domainRecord.type === "TXT" && + dmarcRecord.find((dnsRecord) => dnsRecord === domainRecord.value) + ); + } + if (dmarcRecordFound) { + dmarcRecordFound.status = "verified"; + } + + const spfRecord = await getDNSTXTRecords({ domain: domain.name }); + let spfRecordFound; + if (spfRecord && Array.isArray(spfRecord)) { + spfRecordFound = domain.records.find( + (domainRecord) => + domainRecord.type === "TXT" && + spfRecord.find((dnsRecord) => dnsRecord === domainRecord.value) + ); + } + if (spfRecordFound) { + spfRecordFound.status = "verified"; + } + + const verifiedDomainStatus = await getDomainVerificationStatus({ domain: domain.name }); + + domain.verified = verifiedDomainStatus.DkimAttributes?.[domain.name]?.DkimVerificationStatus === "Success" ? true : false; await domain.save(); return { verified: domain.verified, domain }; diff --git a/api/controllers/InboxController.ts b/api/controllers/InboxController.ts index cb7fd75..ac2077c 100644 --- a/api/controllers/InboxController.ts +++ b/api/controllers/InboxController.ts @@ -18,7 +18,7 @@ export async function createInbox({ ? new mongoose.Types.ObjectId(domain_id) : undefined; inbox.name = name; - inbox.email = email || await getNewRandomInboxEmail({ name }); + inbox.email = email || `${name.replace(/[^a-zA-Z0-9]/g, "-")}@${process.env.DEFAULT_EMAIL_DOMAIN}`; await inbox.save(); return inbox; } diff --git a/api/controllers/SESController.ts b/api/controllers/SESController.ts index 488fea4..df6d485 100644 --- a/api/controllers/SESController.ts +++ b/api/controllers/SESController.ts @@ -23,12 +23,12 @@ export const ses = new SESClient({ }, }); -export async function verifyEmailDomain({ domain }: { domain: string }) { +export async function getDomainVerificationDkimAttributes({ domain }: { domain: string }) { const command = new VerifyDomainDkimCommand({ Domain: domain }); return await ses.send(command); } -export async function domainVerificationStatus({ domain }: { domain: string }) { +export async function getDomainVerificationStatus({ domain }: { domain: string }) { return await ses.send( new GetIdentityDkimAttributesCommand({ Identities: [domain] }) ); @@ -63,6 +63,7 @@ export async function sendSESMessage({ }) { const mailOptions: any = { from: fromName ? `"${fromName}" <${from}>` : from, + replyTo: fromName ? `"${fromName}" <${from}>` : from, to: (to ?? []).join(", "), cc: (cc ?? []).join(", "), subject, diff --git a/api/db/mongo/schemas/Domain.ts b/api/db/mongo/schemas/Domain.ts index a88f01b..bfe9dda 100644 --- a/api/db/mongo/schemas/Domain.ts +++ b/api/db/mongo/schemas/Domain.ts @@ -25,6 +25,18 @@ const domainSchema = new mongoose.Schema( value: "inbound-smtp.us-east-2.amazonaws.com", status: "pending", }, + { + type: "TXT", + name: "_dmarc", + value: "v=DMARC1; p=reject;", + status: "pending", + }, + { + type: "TXT", + name: "@", + value: "v=spf1 include:amazonses.com ~all", + status: "pending", + }, ], }, }, diff --git a/api/routes/v1/domains/index.ts b/api/routes/v1/domains/index.ts index 741a2ab..ed1dc7b 100644 --- a/api/routes/v1/domains/index.ts +++ b/api/routes/v1/domains/index.ts @@ -9,6 +9,10 @@ import { getDomainsByOrganizationId, verifyDomainDNS, } from "../../../controllers/DomainController"; +import { + getDomainVerificationDkimAttributes, + getDomainVerificationStatus, +} from "../../../controllers/SESController"; const router = Router({ mergeParams: true }); @@ -28,7 +32,9 @@ router.post( .isString() .trim() .notEmpty() - .matches(/^(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$/) + .matches( + /^(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$/ + ) .withMessage("Invalid domain name format"), expressValidatorMiddleware, async ( @@ -82,6 +88,52 @@ router.post( } ); +router.get( + "/:domainId/dns", + async ( + req: Request<{ organizationId: string; domainId: string }, {}, {}>, + res: Response + ) => { + const domain = await getDomainByOrganizationIdAndName({ + organizationId: req.organization._id.toString(), + name: req.params.domainId, + }); + if (!domain) { + return res.status(404).json({ error: "Domain not found" }); + } + + const verifiedDomainDkimAttributes = + await getDomainVerificationDkimAttributes({ domain: domain.name }); + + if (!verifiedDomainDkimAttributes.DkimTokens) { + return res.status(400).json({ error: "Failed to get DKIM attributes" }); + } + + return res.json([ + { + type: "MX", + name: "@", + value: "inbound-smtp.us-east-2.amazonaws.com", + }, + { + type: "TXT", + name: "@", + value: "v=spf1 include:amazonses.com ~all", + }, + { + type: "TXT", + name: "_dmarc", + value: "v=DMARC1; p=reject;", + }, + ...verifiedDomainDkimAttributes.DkimTokens.map((token) => ({ + type: "CNAME", + name: `${token}_domainkey.${domain.name}`, + value: `${token}.dkim.amazonses.com`, + })) + ]); + } +); + router.delete( "/:domainId", async ( diff --git a/app/README.md b/app/README.md deleted file mode 100644 index 25b5821..0000000 --- a/app/README.md +++ /dev/null @@ -1,75 +0,0 @@ -# Nuxt Minimal Starter - -Look at the [Nuxt documentation](https://nuxt.com/docs/getting-started/introduction) to learn more. - -## Setup - -Make sure to install dependencies: - -```bash -# npm -npm install - -# pnpm -pnpm install - -# yarn -yarn install - -# bun -bun install -``` - -## Development Server - -Start the development server on `http://localhost:3000`: - -```bash -# npm -npm run dev - -# pnpm -pnpm dev - -# yarn -yarn dev - -# bun -bun run dev -``` - -## Production - -Build the application for production: - -```bash -# npm -npm run build - -# pnpm -pnpm build - -# yarn -yarn build - -# bun -bun run build -``` - -Locally preview production build: - -```bash -# npm -npm run preview - -# pnpm -pnpm preview - -# yarn -yarn preview - -# bun -bun run preview -``` - -Check out the [deployment documentation](https://nuxt.com/docs/getting-started/deployment) for more information. diff --git a/app/app/pages/domains.vue b/app/app/pages/domains.vue index a225bce..b16099e 100644 --- a/app/app/pages/domains.vue +++ b/app/app/pages/domains.vue @@ -60,16 +60,14 @@ -