diff --git a/.env.local.example b/.env.local.example index 82865410..dea4471d 100644 --- a/.env.local.example +++ b/.env.local.example @@ -15,6 +15,8 @@ # ************************************ platform ******************************************** ############################################################################################ PLATFORM_URL=http://localhost:3300 +# Generate by running in a terminal: openssl rand -hex 32 +PLATFORM_SECRET=secretsecretsecret diff --git a/apps/mail-bridge/postal-db/functions.ts b/apps/mail-bridge/postal-db/functions.ts index d4ccda5d..45fc9968 100644 --- a/apps/mail-bridge/postal-db/functions.ts +++ b/apps/mail-bridge/postal-db/functions.ts @@ -119,6 +119,23 @@ export type GetDomainDNSRecordsOutput = optimal: string; acceptable: string; }; + mtaSts: { + dns: { + valid: boolean; + name: string; + value: string; + }; + tls: { + valid: boolean; + name: string; + value: string; + }; + policy: { + valid: boolean; + name: string; + value: string; + }; + }; } | { error: string }; @@ -168,6 +185,23 @@ export async function getDomainDNSRecords( name: '', optimal: '', acceptable: '' + }, + mtaSts: { + dns: { + valid: false, + name: '', + value: '' + }, + tls: { + valid: false, + name: '', + value: '' + }, + policy: { + valid: false, + name: '', + value: '' + } } }; @@ -313,7 +347,7 @@ export async function getDomainDNSRecords( } records.mx.name = domainInfo.name; records.mx.priority = 1; - records.mx.value = `mx.${postalServerUrl}`; + records.mx.value = `${postalServerUrl}`; records.mx.valid = true; if (domainInfo.mxStatus !== 'OK' || forceReverify) { @@ -356,6 +390,40 @@ export async function getDomainDNSRecords( records.dmarc.name = '_dmarc'; records.dmarc.optimal = buildDmarcRecord({ p: 'reject' }); records.dmarc.acceptable = buildDmarcRecord({ p: 'quarantine' }); + + const mtaStsDnsRecord = await lookupTXT(`_mta-sts.${domainInfo.name}`); + records.mtaSts.dns.name = '_mta-sts'; + records.mtaSts.dns.value = `v=STSv1; id=${Date.now()}`; + if (mtaStsDnsRecord.success && mtaStsDnsRecord.data.length > 0) { + records.mtaSts.dns.valid = + mtaStsDnsRecord.data.filter( + (_) => _.startsWith('v=STSv1;') && _.includes('id=') + ).length === 1; + } + + const mtaStsTlsRecord = await lookupTXT(`_smtp._tls.${domainInfo.name}`); + records.mtaSts.tls.name = '_smtp._tls'; + records.mtaSts.tls.value = `v=TLSRPTv1; rua=mailto:tlsrpt@reports.uninbox.com`; + if (mtaStsTlsRecord.success && mtaStsTlsRecord.data.length > 0) { + records.mtaSts.tls.valid = + mtaStsTlsRecord.data.filter( + (_) => + _.startsWith('v=TLSRPTv1;') && + _.includes('rua=') && + _.includes('mailto:tlsrpt@reports.uninbox.com') + ).length === 1; + } + + const mtaStsPolicyRecord = await lookupCNAME(`mta-sts.${domainInfo.name}`); + records.mtaSts.policy.name = 'mta-sts'; + records.mtaSts.policy.value = `mta-sts.${postalServerUrl}`; + if ( + mtaStsPolicyRecord.success && + mtaStsPolicyRecord.data.includes(records.mtaSts.policy.value) + ) { + records.mtaSts.policy.valid = true; + } + return records; } diff --git a/apps/mail-bridge/trpc/routers/domainRouter.ts b/apps/mail-bridge/trpc/routers/domainRouter.ts index 85611ad5..f20564ab 100644 --- a/apps/mail-bridge/trpc/routers/domainRouter.ts +++ b/apps/mail-bridge/trpc/routers/domainRouter.ts @@ -178,6 +178,23 @@ export const domainRouter = router({ name: 'localhost', acceptable: 'v=DMARC1; p=quarantine;', optimal: 'v=DMARC1; p=reject;' + }, + mtaSts: { + dns: { + valid: true, + name: 'localhost', + value: 'v=STSv1; id=123456789' + }, + tls: { + valid: true, + name: 'localhost', + value: 'v=TLSRPTv1; rua=mailto:tlsrpt@localhost' + }, + policy: { + valid: true, + name: 'localhost', + value: 'mta-sts.localhost' + } } } satisfies GetDomainDNSRecordsOutput; } diff --git a/apps/platform/routes/services.ts b/apps/platform/routes/services.ts index f9ec1dac..66839eb6 100644 --- a/apps/platform/routes/services.ts +++ b/apps/platform/routes/services.ts @@ -25,3 +25,5 @@ servicesApi.post( return c.json({ results }); } ); + +servicesApi.post(); diff --git a/packages/caddy-mta-sts/Caddyfile b/packages/caddy-mta-sts/Caddyfile new file mode 100644 index 00000000..930c86bd --- /dev/null +++ b/packages/caddy-mta-sts/Caddyfile @@ -0,0 +1,13 @@ +{ + on_demand_tls { + ask {env.PLATFORM_URL}/caddy-check/{env.PLATFORM_SECRET} + } +} + +https:// { + tls { + on_demand + } + root * ./files + file_server +} diff --git a/packages/caddy-mta-sts/files/.well-known/mta-sts.txt b/packages/caddy-mta-sts/files/.well-known/mta-sts.txt new file mode 100644 index 00000000..d3caea1e --- /dev/null +++ b/packages/caddy-mta-sts/files/.well-known/mta-sts.txt @@ -0,0 +1,4 @@ +version: STSv1 +mode: enforce +mx: *.e.uninbox.com +max_age: 86400 \ No newline at end of file diff --git a/packages/database/schema.ts b/packages/database/schema.ts index 5f889eaf..5f3d34c6 100644 --- a/packages/database/schema.ts +++ b/packages/database/schema.ts @@ -568,6 +568,9 @@ export const domains = mysqlTable( returnPathDnsValid: boolean('return_path_dns_valid') .notNull() .default(false), + mtaStsDnsValid: boolean('mta_sts_dns_valid').notNull().default(false), + mtaStsTlsValid: boolean('mta_sts_tls_valid').notNull().default(false), + mtaStsPolicyValid: boolean('mta_sts_policy_valid').notNull().default(false), lastDnsCheckAt: timestamp('last_dns_check_at'), disabledAt: timestamp('disabled_at'), verifiedAt: timestamp('verified_at'),