/* Helps with some of the rough parts of `nodemailer`. It's main job is to craft templates from `emails/`, load configurations from `secrets/email.json`, and craft fancy HTML emails from those. The file `emails/config.json` also contains a lot of additional template variables that you can use to add more dynamic text to send emails. For example, it currenlty has the company name, address, and unsubscribe links. It also has some of the email testing found in the `admin/pages/EmailDNS.svelte` and `admin/pages/EmailSend.svelte` testing tools. If you want to learn how to confirm your email setup is working look at `dns_check`. */ import nodemailer from "nodemailer"; import fs from 'fs/promises'; import _ from 'lodash'; import logging from "./logging.js"; import { promises } from "dns"; const log = logging.create("lib/email.js"); // TODO: move this out to an easy to access/change location let configuration = {}; /* Configures the `nodemailer` transporter based on the configuration in `secrets/email.json`. ___FOOTGUN___: When you run with `npm run DANGER_ADMIN` it sets the `process.env.DANGER_ADMIN` and that triggers the `nodemailer` console logger. This also happens if you set `DEBUG=1` on the command line so that `process.env.DEBUG` is set. Currently there's no way to prevent this, so if you want to run in `DANGER_ADMIN/DEBUG` mode _and_ send emails to your mail server then you'll have to hack `configure_transporter`. */ const configure_transporter = async () => { if(process.env.DANGER_ADMIN || process.env.DEBUG) { configuration = { streamTransport: true, debug: true, logger: log, newline: 'windows' } } else { configuration = JSON.parse(await fs.readFile("./secrets/email.json")); console.log("EMAIL CONFIG", configuration); } return nodemailer.createTransport(configuration); } /* ___TODO___: Trash hacks to get the config out for the mail testing tool. */ export const get_config = () => configuration; /* The currently configured `nodemailer` transporter, if you want to raw `nodemailer` work. */ export const transporter = await configure_transporter(); /* Load the `.html` and `.txt` templates from `email/` based on the name. Files are mapped as `emails/${name}.html` or `emails/${name}.txt`. These two templates will make the basis of an email that supports both text and HTML display. __FOOTGUN__: The HTML templates are incredibly generic and probably cause spam detectors to go crazy. I got them from somewhere just to get started but they need a full rewrite. If you're using them consider stripping the HTML templates to the ground and hand crafting your own to avoid spam triggers. + `name string` -- The name of the template to load. + ___return___ {html:, txt:} -- The `.html` and `.txt` templates as keys. */ export const load_templates = async (name) => { const html = await fs.readFile(`emails/${name}.html`); const txt = await fs.readFile(`emails/${name}.txt`); return { html: _.template(html), txt: _.template(txt) } } /* When `process.env.DEBUG` is set (using `DEBUG=1` on the CLI) this will write all emails to the `debug/emails` directory so you can load them in a browser or text editor to see how they'll look without sending them to a client. + `templates Object` -- The templates you get from `load_templates`. + `name String` -- The name to write the files to so `name=x` would write `x.html` and `x.txt`. */ export const debug_templates = async (templates, name) => { if(process.env.DEBUG) { log.debug(`Writing debug email for ${name} to debug/emails/${name}.(html|txt)`); await fs.mkdir("debug/emails", { recursive: true}); log.debug(`Emails written to debug/emails/${name}.(html|txt)`); await fs.writeFile(`debug/emails/${name}.html`, templates.html); await fs.writeFile(`debug/emails/${name}.txt`, templates.text); } } /* Uses the antiquated `transporter.sendMail` callback to send the email, but wraps it in a promise so you get logging on errors, and a result returned like a modern `async` function. + `data Object` -- The nodemailer configuration for the email to send. */ export const send_email = async (data) => { const result = new Promise((resolve, reject) => { transporter.sendMail(data, (err, info) => { try { if(err) { log.error(err); resolve(err); } else { if(process.env.DEBUG) { log.debug(info.envelope); log.debug(info.messageId); // I only do this when I'm lazy // info.message.pipe(process.stdout); } resolve(undefined); } } catch(error) { resolve(error); } }); }); return result; } const add_reverse_error = (result, error) => { if(result.reverse_errors === undefined) { result.reverse_errors = []; } let res_error = { error: {...error} }; res_error.message = error.message; result.reverse_errors.push(res_error); } /* Performs a check of the DNS records for `hostname` to make sure they have all the settings most email providers demand. It's simple but catches a lot of missing information like reverse DNS records, SPF records, and DMARC. + `hostname String` -- The host to query and analyze, */ export const dns_check = async (hostname) => { let result = { ip4: {}, ip6: {} }; const res = new promises.Resolver(); res.setServers(['8.8.8.8']); try { result.ip4.host = await res.resolve4(hostname); } catch(error) { // the errors in dns are stupid. You only get .message if you call it. result.ip4.error = {...error}; result.ip4.error.message = error.message; } // no point doing reverse DNS if no IP4 address if(result.ip4.host) { result.ip4.reverse = []; for(let ip4 of result.ip4.host) { try { let host = await res.reverse(ip4); result.ip4.reverse.push(host); } catch(error) { add_reverse_error(res.ip4, error); } } } try { result.ip6.host = await res.resolve6(hostname); } catch(error) { result.ip6.error = {...error}; result.ip6.error.message = error.message; } // no point in doing reverse DNS if no address if(result.ip6.host) { result.ip6.reverse = []; for(let ip6 of result.ip6.host) { try { let host = await res.reverse(ip6); result.ip6.reverse.push(host); } catch(error) { add_reverse_error(result.ip6, error); } } } try { result.mx = await res.resolveMx(hostname); } catch(error) { result.mx_error = {...error}; result.mx_error.message = error.message; } try { result.spf = await res.resolveTxt(hostname); } catch(error) { result.spf_error = {...error}; result.spf_error.message = error.message; } try { result.dmarc = await res.resolveTxt(`_dmarc.${hostname}`); } catch(error) { result.dmarc_error = {...error}; result.dmarc_error.message = error.message; } return result; }