diff --git a/smtp-server.js b/smtp-server.js index dd2ca29168..8fbd6ee75a 100644 --- a/smtp-server.js +++ b/smtp-server.js @@ -487,11 +487,15 @@ async function onAuth(auth, session, fn) { ); const verifications = []; - const records = await this.resolver.resolveTxt(domainName); - for (const record_ of records) { - const record = record_.join('').trim(); // join chunks together - if (record.startsWith(config.paidPrefix)) - verifications.push(record.replace(config.paidPrefix, '').trim()); + try { + const records = await this.resolver.resolveTxt(domainName); + for (const record_ of records) { + const record = record_.join('').trim(); // join chunks together + if (record.startsWith(config.paidPrefix)) + verifications.push(record.replace(config.paidPrefix, '').trim()); + } + } catch (err) { + logger.error(err, { session }); } if (verifications.length === 0) @@ -541,6 +545,7 @@ async function onAuth(auth, session, fn) { 'user', `id ${config.userFields.isBanned} ${config.userFields.smtpLimit}` ) + .select('+tokens.hash +tokens.salt') .lean() .exec(); @@ -583,7 +588,12 @@ async function onAuth(auth, session, fn) { } // ensure that the token is valid - if (!Aliases.isValidPassword(alias.tokens, auth.password.trim())) + const isValid = await Aliases.isValidPassword( + alias.tokens, + auth.password.trim() + ); + + if (!isValid) throw new SMTPError( `Invalid password, please try again or go to ${config.urls.web}/my-account/domains/${domainName}/aliases and click "Generate Password"`, { diff --git a/test/smtp/index.js b/test/smtp/index.js index 2490f27428..276469ba44 100644 --- a/test/smtp/index.js +++ b/test/smtp/index.js @@ -25,7 +25,7 @@ const createTangerine = require('#helpers/create-tangerine'); const env = require('#config/env'); const logger = require('#helpers/logger'); const processEmail = require('#helpers/process-email'); -const { Emails } = require('#models'); +const { Emails, Aliases } = require('#models'); const asyncMxConnect = pify(mxConnect); const IP_ADDRESS = ip.address(); @@ -185,6 +185,157 @@ Test`.trim() } }); +test('auth with pass as alias', async (t) => { + const smtp = new SMTP({ client: t.context.client }, true); + const port = await getPort(); + await smtp.listen(port); + + const user = await factory.create('user', { + plan: 'enhanced_protection', + [config.userFields.planSetAt]: dayjs().startOf('day').toDate() + }); + + await factory.create('payment', { + user: user._id, + amount: 300, + invoice_at: dayjs().startOf('day').toDate(), + method: 'free_beta_program', + duration: ms('30d'), + plan: user.plan, + kind: 'one-time' + }); + + await user.save(); + + const resolver = createTangerine(t.context.client, logger); + + const domain = await factory.create('domain', { + members: [{ user: user._id, group: 'admin' }], + plan: user.plan, + resolver, + has_smtp: true + }); + + const alias = await factory.create('alias', { + user: user._id, + domain: domain._id, + recipients: [user.email] + }); + + await alias.createToken(); + await alias.save(); + + // spoof dns records + const map = new Map(); + + map.set( + `txt:${domain.name}`, + resolver.spoofPacket( + domain.name, + 'TXT', + [`${config.paidPrefix}${domain.verification_record}`], + true + ) + ); + + // dkim + map.set( + `txt:${domain.dkim_key_selector}._domainkey.${domain.name}`, + resolver.spoofPacket( + `${domain.dkim_key_selector}._domainkey.${domain.name}`, + 'TXT', + [`v=DKIM1; k=rsa; p=${domain.dkim_public_key.toString('base64')};`], + true + ) + ); + + // spf + map.set( + `txt:${env.WEB_HOST}`, + resolver.spoofPacket( + `${env.WEB_HOST}`, + 'TXT', + [`v=spf1 ip4:${IP_ADDRESS} -all`], + true + ) + ); + + // cname + map.set( + `cname:${domain.return_path}.${domain.name}`, + resolver.spoofPacket( + `${domain.return_path}.${domain.name}`, + 'CNAME', + [env.WEB_HOST], + true + ) + ); + + // cname -> txt + map.set( + `txt:${domain.return_path}.${domain.name}`, + resolver.spoofPacket( + `${domain.return_path}.${domain.name}`, + 'TXT', + [`v=spf1 ip4:${IP_ADDRESS} -all`], + true + ) + ); + + // dmarc + map.set( + `txt:_dmarc.${domain.name}`, + resolver.spoofPacket( + `_dmarc.${domain.name}`, + 'TXT', + [ + // TODO: consume dmarc reports and parse dmarc-$domain + `v=DMARC1; p=reject; pct=100; rua=mailto:dmarc-${domain.id}@forwardemail.net;` + ], + true + ) + ); + + // store spoofed dns cache + await resolver.options.cache.mset(map); + + const transporter = nodemailer.createTransport({ + logger, + debug: true, + host: IP_ADDRESS, + port, + ignoreTLS: true, + secure: true, + tls: { + rejectUnauthorized: false + }, + auth: { + user: `${alias.name}@${domain.name}`, + pass: 'test' + } + }); + + const err = await t.throwsAsync( + transporter.sendMail({ + envelope: { + from: `${alias.name}@${domain.name}`, + to: 'test@test.com' + }, + raw: ` +To: test@test.com +From: test@test.com +Subject: test +Content-Type: text/plain; charset=us-ascii +Content-Transfer-Encoding: 7bit + +Test`.trim() + }) + ); + + t.is(err.responseCode, 535); + t.regex(err.message, /Invalid password/); +}); + test('smtp outbound auth', async (t) => { const smtp = new SMTP({ client: t.context.client }, false); const { resolver } = smtp; @@ -225,6 +376,10 @@ test('smtp outbound auth', async (t) => { t.true(typeof pass === 'string' && pass.length === 24); await alias.save(); + const isValid = await Aliases.isValidPassword(alias.tokens, pass); + + t.true(isValid); + const combos = [ { user: 'a', pass: 'a' }, { user: `beep@${domain.name}`, pass },