You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

232 lines
6.9 KiB

/*
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 === "1" || 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;
}