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