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