Skip to content

Commit 7f30ef1

Browse files
committed
feat: mta-sts support
1 parent b68ceff commit 7f30ef1

File tree

10 files changed

+162
-3
lines changed

10 files changed

+162
-3
lines changed

.env.local.example

+2
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
# ************************************ platform ********************************************
1616
############################################################################################
1717
PLATFORM_URL=http://localhost:3300
18+
# Generate by running in a terminal: openssl rand -hex 32
19+
PLATFORM_SECRET=secretsecretsecret
1820

1921

2022

apps/mail-bridge/postal-db/functions.ts

+69-1
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,23 @@ export type GetDomainDNSRecordsOutput =
117117
optimal: string;
118118
acceptable: string;
119119
};
120+
mtaSts: {
121+
dns: {
122+
valid: boolean;
123+
name: string;
124+
value: string;
125+
};
126+
tls: {
127+
valid: boolean;
128+
name: string;
129+
value: string;
130+
};
131+
policy: {
132+
valid: boolean;
133+
name: string;
134+
value: string;
135+
};
136+
};
120137
}
121138
| { error: string };
122139

@@ -166,6 +183,23 @@ export async function getDomainDNSRecords(
166183
name: '',
167184
optimal: '',
168185
acceptable: ''
186+
},
187+
mtaSts: {
188+
dns: {
189+
valid: false,
190+
name: '',
191+
value: ''
192+
},
193+
tls: {
194+
valid: false,
195+
name: '',
196+
value: ''
197+
},
198+
policy: {
199+
valid: false,
200+
name: '',
201+
value: ''
202+
}
169203
}
170204
};
171205

@@ -317,7 +351,7 @@ export async function getDomainDNSRecords(
317351
}
318352
records.mx.name = domainInfo.name;
319353
records.mx.priority = 1;
320-
records.mx.value = `mx.${postalServerUrl}`;
354+
records.mx.value = `${postalServerUrl}`;
321355
records.mx.valid = true;
322356

323357
if (domainInfo.mxStatus !== 'OK' || forceReverify) {
@@ -360,6 +394,40 @@ export async function getDomainDNSRecords(
360394
records.dmarc.name = '_dmarc';
361395
records.dmarc.optimal = buildDmarcRecord({ p: 'reject' });
362396
records.dmarc.acceptable = buildDmarcRecord({ p: 'quarantine' });
397+
398+
const mtaStsDnsRecord = await lookupTXT(`_mta-sts.${domainInfo.name}`);
399+
records.mtaSts.dns.name = '_mta-sts';
400+
records.mtaSts.dns.value = `v=STSv1; id=${Date.now()}`;
401+
if (mtaStsDnsRecord.success && mtaStsDnsRecord.data.length > 0) {
402+
records.mtaSts.dns.valid =
403+
mtaStsDnsRecord.data.filter(
404+
(_) => _.startsWith('v=STSv1;') && _.includes('id=')
405+
).length === 1;
406+
}
407+
408+
const mtaStsTlsRecord = await lookupTXT(`_smtp._tls.${domainInfo.name}`);
409+
records.mtaSts.tls.name = '_smtp._tls';
410+
records.mtaSts.tls.value = `v=TLSRPTv1; rua=mailto:[email protected]`;
411+
if (mtaStsTlsRecord.success && mtaStsTlsRecord.data.length > 0) {
412+
records.mtaSts.tls.valid =
413+
mtaStsTlsRecord.data.filter(
414+
(_) =>
415+
_.startsWith('v=TLSRPTv1;') &&
416+
_.includes('rua=') &&
417+
_.includes('mailto:[email protected]')
418+
).length === 1;
419+
}
420+
421+
const mtaStsPolicyRecord = await lookupCNAME(`mta-sts.${domainInfo.name}`);
422+
records.mtaSts.policy.name = 'mta-sts';
423+
records.mtaSts.policy.value = `mta-sts.${postalServerUrl}`;
424+
if (
425+
mtaStsPolicyRecord.success &&
426+
mtaStsPolicyRecord.data.includes(records.mtaSts.policy.value)
427+
) {
428+
records.mtaSts.policy.valid = true;
429+
}
430+
363431
return records;
364432
}
365433

apps/mail-bridge/trpc/routers/domainRouter.ts

+17
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,23 @@ export const domainRouter = router({
175175
name: 'localhost',
176176
acceptable: 'v=DMARC1; p=quarantine;',
177177
optimal: 'v=DMARC1; p=reject;'
178+
},
179+
mtaSts: {
180+
dns: {
181+
valid: true,
182+
name: 'localhost',
183+
value: 'v=STSv1; id=123456789'
184+
},
185+
tls: {
186+
valid: true,
187+
name: 'localhost',
188+
value: 'v=TLSRPTv1; rua=mailto:tlsrpt@localhost'
189+
},
190+
policy: {
191+
valid: true,
192+
name: 'localhost',
193+
value: 'mta-sts.localhost'
194+
}
178195
}
179196
} satisfies GetDomainDNSRecordsOutput;
180197
}

apps/platform/nitro.config.ts

+3
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,9 @@ export default defineNitroConfig({
4848
runtimeConfig: {
4949
primaryDomain: process.env.PRIMARY_DOMAIN || 'localhost',
5050
mailDomains: mailDomains,
51+
platform: {
52+
secret: process.env.PLATFORM_SECRET || ''
53+
},
5154
auth: {
5255
baseUrl: process.env.WEBAPP_URL || 'http://localhost:3000',
5356
secret: process.env.WEBAPP_AUTH_SECRET,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { useRuntimeConfig } from '#imports';
2+
import { db } from '@u22n/database';
3+
import { eq } from '@u22n/database/orm';
4+
import { domains } from '@u22n/database/schema';
5+
import {
6+
eventHandler,
7+
getQuery,
8+
getRouterParam,
9+
send,
10+
setResponseStatus
11+
} from 'h3';
12+
13+
export default eventHandler(async (event) => {
14+
const secret = getRouterParam(event, 'secret');
15+
if (useRuntimeConfig().platform.secret !== secret) {
16+
setResponseStatus(event, 401);
17+
return send(event, 'Unauthorized');
18+
}
19+
20+
const domain = getQuery(event).domain;
21+
if (!domain || typeof domain !== 'string') {
22+
setResponseStatus(event, 400);
23+
return send(event, 'Bad Request');
24+
}
25+
26+
if (!domain.startsWith('mta-sts.')) {
27+
setResponseStatus(event, 400);
28+
return send(event, 'Bad Request');
29+
}
30+
31+
const rootDomain = domain.replace(/^mta-sts\./, '');
32+
const domainResponse = await db.query.domains.findFirst({
33+
where: eq(domains.domain, rootDomain)
34+
});
35+
36+
if (!domainResponse) {
37+
setResponseStatus(event, 403);
38+
return send(event, 'Forbidden');
39+
}
40+
41+
setResponseStatus(event, 200);
42+
return send(event, 'Ok');
43+
});

apps/platform/trpc/routers/orgRouter/mail/domainsRouter.ts

+7-1
Original file line numberDiff line numberDiff line change
@@ -299,7 +299,10 @@ export const domainsRouter = router({
299299
spfDnsValid: dnsRecords.spf.valid,
300300
returnPathDnsValid: dnsRecords.returnPath.valid,
301301
verification: dnsRecords.verification.valid,
302-
dmarkPolicy: dnsRecords.dmarc.policy
302+
dmarcPolicy: dnsRecords.dmarc.policy,
303+
mtaStsDns: dnsRecords.mtaSts.dns.valid,
304+
mtaStsTls: dnsRecords.mtaSts.tls.valid,
305+
mtaStsPolicy: dnsRecords.mtaSts.policy.valid
303306
};
304307

305308
// take all dns Records and count how many are valid, if all are valid then allOk
@@ -365,6 +368,9 @@ export const domainsRouter = router({
365368
dkimDnsValid: dnsStatus.dkimDnsValid,
366369
spfDnsValid: dnsStatus.spfDnsValid,
367370
returnPathDnsValid: dnsStatus.returnPathDnsValid,
371+
mtaStsDnsValid: dnsStatus.mtaStsDns,
372+
mtaStsTlsValid: dnsStatus.mtaStsTls,
373+
mtaStsPolicyValid: dnsStatus.mtaStsPolicy,
368374
receivingMode: domainReceivingMode,
369375
sendingMode: domainSendingMode,
370376
lastDnsCheckAt: new Date(),

apps/web-app/pages/[orgSlug]/settings/org/mail/domains/[domainId].vue

+1-1
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,7 @@
140140
{
141141
label: 'DMARC-Record',
142142
slot: 'dmarc-record',
143-
status: domainDnsQuery.value?.dnsStatus?.dmarkPolicy || null
143+
status: domainDnsQuery.value?.dnsStatus?.dmarcPolicy || null
144144
}
145145
];
146146
});

packages/caddy-mta-sts/Caddyfile

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
on_demand_tls {
3+
ask {env.PLATFORM_URL}/caddy-check/{env.PLATFORM_SECRET}
4+
}
5+
}
6+
7+
https:// {
8+
tls {
9+
on_demand
10+
}
11+
root * ./files
12+
file_server
13+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
version: STSv1
2+
mode: enforce
3+
mx: *.e.uninbox.com
4+
max_age: 86400

packages/database/schema.ts

+3
Original file line numberDiff line numberDiff line change
@@ -500,6 +500,9 @@ export const domains = mysqlTable(
500500
returnPathDnsValid: boolean('return_path_dns_valid')
501501
.notNull()
502502
.default(false),
503+
mtaStsDnsValid: boolean('mta_sts_dns_valid').notNull().default(false),
504+
mtaStsTlsValid: boolean('mta_sts_tls_valid').notNull().default(false),
505+
mtaStsPolicyValid: boolean('mta_sts_policy_valid').notNull().default(false),
503506
lastDnsCheckAt: timestamp('last_dns_check_at'),
504507
disabledAt: timestamp('disabled_at'),
505508
verifiedAt: timestamp('verified_at'),

0 commit comments

Comments
 (0)