// email.js // Copyright (C) 2022 DTP Technologies, LLC // License: Apache-2.0 'use strict'; const nodemailer = require('nodemailer'); const uuidv4 = require('uuid').v4; const mongoose = require('mongoose'); const EmailBlacklist = mongoose.model('EmailBlacklist'); const EmailVerify = mongoose.model('EmailVerify'); const EmailLog = mongoose.model('EmailLog'); const disposableEmailDomains = require('disposable-email-provider-domains'); const emailValidator = require('email-validator'); const emailDomainCheck = require('email-domain-check'); const { SiteService, SiteError } = require('../../lib/site-lib'); class EmailService extends SiteService { constructor (dtp) { super(dtp, module.exports); } async start ( ) { await super.start(); if (process.env.DTP_EMAIL_SERVICE !== 'enabled') { this.log.info("DTP_EMAIL_SERVICE is disabled, the system can't send email and will not try."); return; } const SMTP_PORT = parseInt(process.env.DTP_EMAIL_SMTP_PORT || '587', 10); this.log.info('creating SMTP transport', { host: process.env.DTP_EMAIL_SMTP_HOST, port: SMTP_PORT, }); this.transport = nodemailer.createTransport({ host: process.env.DTP_EMAIL_SMTP_HOST, port: SMTP_PORT, secure: process.env.DTP_EMAIL_SMTP_SECURE === 'enabled', auth: { user: process.env.DTP_EMAIL_SMTP_USER, pass: process.env.DTP_EMAIL_SMTP_PASS, }, pool: true, maxConnections: parseInt(process.env.DTP_EMAIL_SMTP_POOL_MAX_CONN || '5', 10), maxMessages: parseInt(process.env.DTP_EMAIL_SMTP_POOL_MAX_MSGS || '5', 10), }); this.templates = { html: { userEmail: this.loadAppTemplate('html', 'user-email.pug'), welcome: this.loadAppTemplate('html', 'welcome.pug'), }, text: { userEmail: this.loadAppTemplate('text', 'user-email.pug'), welcome: this.loadAppTemplate('text', 'welcome.pug'), }, }; } async renderTemplate (templateId, templateType, templateModel) { this.log.debug('rendering email template', { templateId, templateType }); return this.templates[templateType][templateId](templateModel); } async send (message) { const NOW = new Date(); await this.checkEmailAddress(message.to); this.log.info('sending email', { to: message.to, subject: message.subject }); const response = await this.transport.sendMail(message); const log = await EmailLog.create({ created: NOW, from: message.from, to: message.to, to_lc: message.to.toLowerCase(), subject: message.subject, messageId: response.messageId, }); return { response, log }; } async checkEmailAddress (emailAddress) { this.log.debug('validating email address', { emailAddress }); if (!emailValidator.validate(emailAddress)) { throw new Error('Email address is invalid'); } const domainCheck = await emailDomainCheck(emailAddress); this.log.debug('email domain check', { domainCheck }); if (!domainCheck) { throw new Error('Email address is invalid'); } await this.isEmailBlacklisted(emailAddress); } async isEmailBlacklisted (emailAddress) { emailAddress = emailAddress.toLowerCase().trim(); const domain = emailAddress.split('@')[1]; this.log.debug('checking email domain for blacklist', { domain }); if (disposableEmailDomains.domains.includes(domain)) { this.log.alert('blacklisted email domain blocked', { emailAddress, domain }); throw new Error('Invalid email address'); } const blacklistRecord = await EmailBlacklist.findOne({ email: emailAddress }); if (blacklistRecord) { throw new Error('Email address has requested to not receive emails', { blacklistRecord }); } return false; } async createVerificationToken (user) { const NOW = new Date(); const verify = new EmailVerify(); verify.created = NOW; verify.user = user._id; verify.token = uuidv4(); await verify.save(); this.log.info('created email verification token for user', { user: user._id }); return verify.toObject(); } async verifyToken (token) { const NOW = new Date(); const { user: userService } = this.dtp.services; // fetch the token from the db const emailVerify = await EmailVerify .findOne({ token: token }) .populate(this.populateEmailVerify) .lean(); // verify that the token is at least valid (it exists) if (!emailVerify) { this.log.error('email verify token not found', { token }); throw new SiteError(403, 'Email verification token is invalid'); } // verify that it hasn't already been verified (user clicked link more than once) if (emailVerify.verified) { this.log.error('email verify token already claimed', { token }); throw new SiteError(403, 'Email verification token is invalid'); } this.log.info('marking user email verified', { userId: emailVerify.user._id }); await userService.setEmailVerification(emailVerify.user, true); await EmailVerify.updateOne({ _id: emailVerify._id }, { $set: { verified: NOW } }); } async removeVerificationTokensForUser (user) { this.log.info('removing all pending email address verification tokens for user', { user: user._id }); await EmailVerify.deleteMany({ user: user._id }); } } module.exports = { slug: 'email', name: 'email', create: (dtp) => { return new EmailService(dtp); }, };