From 7f262849822098e95719e7ab776d541aadeedfec Mon Sep 17 00:00:00 2001 From: TheRealToxicDev Date: Fri, 23 Oct 2026 08:15:54 -0600 Subject: [PATCH] fix: known issues and reports --- CHANGELOG.md | 10 +- .../app/(dashboard)/dmarc/dmarc-reports.tsx | 136 +++++++++++------- apps/web/src/server/api/routers/dmarc.ts | 10 +- .../server/service/cloudflare-dns-service.ts | 2 +- .../cloudflare-dns-service.unit.test.ts | 25 +++- .../server/service/domain-region-service.ts | 2 +- apps/web/src/server/service/domain-service.ts | 6 +- .../server/service/inbound-domain-service.ts | 13 +- .../inbound-domain-service.unit.test.ts | 7 + apps/web/src/server/service/team-service.ts | 8 +- 10 files changed, 146 insertions(+), 73 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5ebedc9..bbaa523 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,10 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 --- -## [Unreleased] +## [0.9.4] - 2026-10-23 ### Fixed - The receiving health check's test email is sent every 10 minutes again. Before, it only went out once after each restart, so the check reported receiving as degraded when it was working +- Emails from free teams no longer fail with "createdAt.getTime is not a function". The cached team came back with its dates as text, which broke the new-account sending limit check +- Domains that are themselves a subdomain (like `sub.example.com`) no longer show false "DNS records changed" warnings. The DNS check looked for the subdomain twice, for example `mail.sub1.sub2.example.com` +- Receiving DNS records for a sending domain that is a subdomain are now named from the root domain, the same as the sending records, so they can be copied straight into your DNS provider +- The DMARC page accepts the hosted report address (`you+dmarc@in.bytesend.cloud`) as well as the one on your receiving domain, instead of saying the address wasn't added +- The DMARC report panel no longer runs off the side of the screen on mobile. Each source is now a card with a clear pass or fail result --- @@ -1089,7 +1094,8 @@ Initial public beta release of ByteSend: an all-in-one email infrastructure plat --- -[Unreleased]: https://github.com/ByteSendApp/ByteSend/compare/v0.9.3...HEAD +[Unreleased]: https://github.com/ByteSendApp/ByteSend/compare/v0.9.4...HEAD +[0.9.4]: https://github.com/ByteSendApp/ByteSend/compare/v0.9.3...v0.9.4 [0.9.3]: https://github.com/ByteSendApp/ByteSend/compare/v0.9.2...v0.9.3 [0.9.2]: https://github.com/ByteSendApp/ByteSend/compare/v0.9.1...v0.9.2 [0.9.1]: https://github.com/ByteSendApp/ByteSend/compare/v0.9.0...v0.9.1 diff --git a/apps/web/src/app/(dashboard)/dmarc/dmarc-reports.tsx b/apps/web/src/app/(dashboard)/dmarc/dmarc-reports.tsx index 43ade30..9b95d45 100644 --- a/apps/web/src/app/(dashboard)/dmarc/dmarc-reports.tsx +++ b/apps/web/src/app/(dashboard)/dmarc/dmarc-reports.tsx @@ -68,7 +68,7 @@ export function DmarcReports({ days, domain }: { days: DmarcRange; domain: strin !open && setSelected(null)}> - + DMARC report Every source and result in one DMARC report. {selected ? : null} @@ -80,21 +80,83 @@ export function DmarcReports({ days, domain }: { days: DmarcRange; domain: strin type AuthResult = { domain: string; selector?: string | null; scope?: string | null; result: string }; -function AuthResults({ results }: { results: unknown }) { +function AuthResults({ label, results }: { label: string; results: unknown }) { const list = (results as AuthResult[]) ?? []; - if (list.length === 0) return none; return ( - - {list.map((r, i) => ( - - {r.result || "?"} - - {r.domain} - {r.selector ? ` (${r.selector})` : ""} - - - ))} - +
+

{label}

+ {list.length === 0 ? ( +

None

+ ) : ( +
    + {list.map((r, i) => ( +
  • + {r.result || "?"} + + {r.domain} + {r.selector ? · {r.selector} : null} + +
  • + ))} +
+ )} +
+ ); +} + +type SourceRecord = { + id: string; + sourceIp: string; + sourceHost: string | null; + count: number; + disposition: string; + dkim: string; + spf: string; + headerFrom: string; + envelopeFrom: string | null; + dkimResults: unknown; + spfResults: unknown; + viaByteSend: boolean; +}; + +function SourceCard({ record }: { record: SourceRecord }) { + const passed = record.dkim === "pass" || record.spf === "pass"; + return ( +
+
+
+
+ {passed ? "Passed DMARC" : "Failed DMARC"} + {record.viaByteSend ? ByteSend : null} + {record.disposition !== "none" ? {record.disposition} : null} +
+

{record.sourceHost ?? record.sourceIp}

+ {record.sourceHost ?

{record.sourceIp}

: null} +
+
+

{record.count.toLocaleString()}

+

{record.count === 1 ? "message" : "messages"}

+
+
+ +
+
+
From
+
{record.headerFrom}
+
+ {record.envelopeFrom ? ( +
+
Envelope from
+
{record.envelopeFrom}
+
+ ) : null} +
+ +
+ + +
+
); } @@ -105,10 +167,10 @@ function ReportDetails({ id }: { id: string }) { const policy = report.policy as { p?: string | null; sp?: string | null; pct?: string | null; adkim?: string | null; aspf?: string | null }; return ( -
-
-

{report.orgName}

-

+

+
+

{report.orgName}

+

{report.domain} · {period(report.beginAt, report.endAt)}

@@ -121,7 +183,7 @@ function ReportDetails({ id }: { id: string }) { { label: "Published policy", value: ( - + p={policy.p ?? "?"} {policy.sp ? ` sp=${policy.sp}` : ""} pct={policy.pct ?? "100"} adkim={policy.adkim ?? "r"} aspf={policy.aspf ?? "r"} @@ -134,38 +196,12 @@ function ReportDetails({ id }: { id: string }) {
-

Sources

+
+

Sources

+

A source passes DMARC when DKIM or SPF passes for your domain.

+
{report.records.map((record) => ( - - - {record.viaByteSend ? ByteSend : null} - {record.sourceHost ? `${record.sourceHost} (${record.sourceIp})` : record.sourceIp} - - ), - }, - { label: "Messages", value: record.count.toLocaleString() }, - { - label: "Result", - value: ( - - DKIM {record.dkim} - SPF {record.spf} - {record.disposition !== "none" ? {record.disposition} : null} - - ), - }, - { label: "From", value: record.headerFrom }, - ...(record.envelopeFrom ? [{ label: "Envelope from", value: record.envelopeFrom }] : []), - { label: "DKIM signatures", value: }, - { label: "SPF checks", value: }, - ]} - /> - + ))}
diff --git a/apps/web/src/server/api/routers/dmarc.ts b/apps/web/src/server/api/routers/dmarc.ts index b0213d5..a2b425f 100644 --- a/apps/web/src/server/api/routers/dmarc.ts +++ b/apps/web/src/server/api/routers/dmarc.ts @@ -45,10 +45,16 @@ export const dmarcRouter = createTRPCRouter({ const orgDomains = [...new Set(domains.map((d) => tldts.getDomain(d.name) ?? d.name))]; const checks = await Promise.all( orgDomains.map(async (orgDomain) => { - const address = addresses.byOrgDomain.get(orgDomain) ?? addresses.hosted; - const check = await checkDmarcRecord(orgDomain, address); + const preferred = addresses.byOrgDomain.get(orgDomain) ?? addresses.hosted; + const check = await checkDmarcRecord(orgDomain, preferred); + const usesHosted = + !check.reportsToUs && + Boolean(addresses.hosted && check.record?.toLowerCase().includes(`mailto:${addresses.hosted.toLowerCase()}`)); + const address = usesHosted ? addresses.hosted : preferred; return { ...check, + reportsToUs: check.reportsToUs || usesHosted, + suggested: usesHosted ? check.record : check.suggested, address, sendingDomains: domains.filter((d) => (tldts.getDomain(d.name) ?? d.name) === orgDomain).map((d) => d.name), }; diff --git a/apps/web/src/server/service/cloudflare-dns-service.ts b/apps/web/src/server/service/cloudflare-dns-service.ts index 502daa1..5db2dc5 100644 --- a/apps/web/src/server/service/cloudflare-dns-service.ts +++ b/apps/web/src/server/service/cloudflare-dns-service.ts @@ -61,7 +61,7 @@ export function desiredRecords(domain: Domain): DesiredRecord[] { const receiving = domain.inboundEnabled ? receivingRecords(domain).map((record): DesiredRecord => ({ type: record.type, - name: record.name === ROOT_RECEIVING ? domain.name : `${record.name}.${domain.name}`, + name: record.name === ROOT_RECEIVING ? zone : `${record.name}.${zone}`, content: record.value, priority: record.priority, kind: record.type === "MX" ? "mx" : "dkim", diff --git a/apps/web/src/server/service/cloudflare-dns-service.unit.test.ts b/apps/web/src/server/service/cloudflare-dns-service.unit.test.ts index 00a1bc4..f40ea93 100644 --- a/apps/web/src/server/service/cloudflare-dns-service.unit.test.ts +++ b/apps/web/src/server/service/cloudflare-dns-service.unit.test.ts @@ -3,12 +3,15 @@ import type { Domain } from "@prisma/client"; vi.mock("../logger/log", () => ({ logger: { info: vi.fn(), warn: vi.fn(), error: vi.fn() } })); vi.mock("./domain-service", () => ({ - buildDnsRecords: (domain: Domain) => [ - { type: "MX", name: "mail", value: `feedback-smtp.${domain.region}.amazonses.com`, priority: "10" }, - { type: "TXT", name: "bytesend._domainkey", value: `p=${domain.publicKey}` }, - { type: "TXT", name: "mail", value: "v=spf1 include:amazonses.com ~all" }, - { type: "TXT", name: "_dmarc", value: "v=DMARC1; p=none;" }, - ], + buildDnsRecords: (domain: Domain) => { + const suffix = domain.subdomain ? `.${domain.subdomain}` : ""; + return [ + { type: "MX", name: `mail${suffix}`, value: `feedback-smtp.${domain.region}.amazonses.com`, priority: "10" }, + { type: "TXT", name: `bytesend._domainkey${suffix}`, value: `p=${domain.publicKey}` }, + { type: "TXT", name: `mail${suffix}`, value: "v=spf1 include:amazonses.com ~all" }, + { type: "TXT", name: "_dmarc", value: "v=DMARC1; p=none;" }, + ]; + }, })); vi.mock("./inbound-domain-service", () => ({ receivingRecords: () => [] })); @@ -37,6 +40,16 @@ describe("desiredRecords", () => { "TXT _dmarc.acme.com", ]); }); + + it("names a subdomain's records once, under the root zone", () => { + const sub = { ...domain, name: "trans.acme.com", subdomain: "trans" } as Domain; + expect(desiredRecords(sub).map((r) => `${r.type} ${r.name}`)).toEqual([ + "MX mail.trans.acme.com", + "TXT bytesend._domainkey.trans.acme.com", + "TXT mail.trans.acme.com", + "TXT _dmarc.acme.com", + ]); + }); }); describe("planRecord", () => { diff --git a/apps/web/src/server/service/domain-region-service.ts b/apps/web/src/server/service/domain-region-service.ts index dd28d83..044ec8b 100644 --- a/apps/web/src/server/service/domain-region-service.ts +++ b/apps/web/src/server/service/domain-region-service.ts @@ -142,7 +142,7 @@ export class DomainRegionService { const [identity, dkimDns, mxDns] = await Promise.all([ ses.getDomainIdentity(domain.name, domain.pendingRegion), checkDkimDnsRecord(domain.pendingDkimSelector, domain.name, domain.pendingPublicKey), - checkMxDnsRecord(`${mailDomainLabel(domain)}.${domain.name}`, domain.pendingRegion), + checkMxDnsRecord(`mail.${domain.name}`, domain.pendingRegion), ]); const dnsPrecheck: { dkim: DnsCheckResult; mx: DnsCheckResult } = { dkim: dkimDns, mx: mxDns }; diff --git a/apps/web/src/server/service/domain-service.ts b/apps/web/src/server/service/domain-service.ts index b156a3b..830bf5f 100644 --- a/apps/web/src/server/service/domain-service.ts +++ b/apps/web/src/server/service/domain-service.ts @@ -228,8 +228,7 @@ async function recheckDnsHealth(domain: Domain) { export async function checkDomainDnsHealth( domain: Pick, ): Promise { - const subdomainSuffix = domain.subdomain ? `.${domain.subdomain}` : ""; - const mailDomain = `mail${subdomainSuffix}.${domain.name}`; + const mailDomain = `mail.${domain.name}`; const dkimSelector = domain.dkimSelector ?? "bytesend"; const [dkim, spf, mx] = await Promise.all([ @@ -703,8 +702,7 @@ export async function refreshDomainVerification( const verificationState = await getDomainVerificationState(domain.id); const previousStatus = domain.status; - const subdomainSuffix = domain.subdomain ? `.${domain.subdomain}` : ""; - const mailDomain = `mail${subdomainSuffix}.${domain.name}`; + const mailDomain = `mail.${domain.name}`; const dkimSelector = domain.dkimSelector ?? "bytesend"; const [domainIdentity, dkimDns, spfDns, mxDns] = await Promise.all([ diff --git a/apps/web/src/server/service/inbound-domain-service.ts b/apps/web/src/server/service/inbound-domain-service.ts index 9e3b086..ef30daa 100644 --- a/apps/web/src/server/service/inbound-domain-service.ts +++ b/apps/web/src/server/service/inbound-domain-service.ts @@ -12,12 +12,13 @@ const INBOUND_DKIM_SELECTOR = "bytesend-inbound"; type ReceivingDomain = Pick< Domain, - "id" | "name" | "region" | "status" | "inboundEnabled" | "inboundSubdomain" | "inboundStatus" | "inboundCheckedAt" | "inboundDkimSelector" | "inboundPublicKey" + "id" | "name" | "subdomain" | "region" | "status" | "inboundEnabled" | "inboundSubdomain" | "inboundStatus" | "inboundCheckedAt" | "inboundDkimSelector" | "inboundPublicKey" >; export const RECEIVING_DOMAIN_SELECT = { id: true, name: true, + subdomain: true, region: true, status: true, inboundEnabled: true, @@ -43,17 +44,17 @@ export function receivingFqdn(domain: Pick) return isRootReceiving(domain) ? domain.name : `${domain.inboundSubdomain}.${domain.name}`; } -export function receivingRecords(domain: ReceivingDomain) { +export function receivingRecords(domain: Pick) { if (!domain.inboundSubdomain) return []; + const host = isRootReceiving(domain) ? "" : domain.inboundSubdomain; + const relativeToZone = (...labels: string[]) => [...labels, domain.subdomain].filter(Boolean).join(".") || ROOT_RECEIVING; const records: Array<{ type: "MX" | "TXT"; name: string; value: string; priority?: number }> = [ - { type: "MX", name: isRootReceiving(domain) ? ROOT_RECEIVING : domain.inboundSubdomain, value: inboundMxHost(), priority: 10 }, + { type: "MX", name: relativeToZone(host), value: inboundMxHost(), priority: 10 }, ]; if (domain.inboundDkimSelector && domain.inboundPublicKey) { records.push({ type: "TXT", - name: isRootReceiving(domain) - ? `${domain.inboundDkimSelector}._domainkey` - : `${domain.inboundDkimSelector}._domainkey.${domain.inboundSubdomain}`, + name: relativeToZone(`${domain.inboundDkimSelector}._domainkey`, host), value: `p=${domain.inboundPublicKey}`, }); } diff --git a/apps/web/src/server/service/inbound-domain-service.unit.test.ts b/apps/web/src/server/service/inbound-domain-service.unit.test.ts index a48386e..3cb5ed3 100644 --- a/apps/web/src/server/service/inbound-domain-service.unit.test.ts +++ b/apps/web/src/server/service/inbound-domain-service.unit.test.ts @@ -43,6 +43,13 @@ describe("receiving addresses", () => { { type: "TXT", name: "bytesend-inbound._domainkey", value: "p=KEY" }, ]); }); + + it("names records from the root domain when the sending domain is a subdomain", () => { + const sub = { ...base, name: "trans.acme.com", subdomain: "trans", inboundDkimSelector: "bytesend-inbound", inboundPublicKey: "KEY" }; + expect(receivingFqdn({ ...sub, inboundSubdomain: "reply" })).toBe("reply.trans.acme.com"); + expect(receivingRecords({ ...sub, inboundSubdomain: "reply" }).map((r) => r.name)).toEqual(["reply.trans", "bytesend-inbound._domainkey.reply.trans"]); + expect(receivingRecords({ ...sub, inboundSubdomain: "@" }).map((r) => r.name)).toEqual(["trans", "bytesend-inbound._domainkey.trans"]); + }); }); describe("enableReceiving", () => { diff --git a/apps/web/src/server/service/team-service.ts b/apps/web/src/server/service/team-service.ts index ba81a24..2b5acce 100644 --- a/apps/web/src/server/service/team-service.ts +++ b/apps/web/src/server/service/team-service.ts @@ -43,7 +43,13 @@ export class TeamService { const redis = getRedis(); const raw = await redis.get(TeamService.cacheKey(teamId)); if (raw) { - return JSON.parse(raw) as Team; + const cached = JSON.parse(raw) as Team; + return { + ...cached, + createdAt: new Date(cached.createdAt), + updatedAt: new Date(cached.updatedAt), + sendingApprovedAt: cached.sendingApprovedAt ? new Date(cached.sendingApprovedAt) : null, + }; } const fresh = await TeamService.refreshTeamCache(teamId); if (!fresh) {