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
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;
|
|
}
|
|
|