HEX
Server: Apache/2.4.52 (Ubuntu)
System: Linux ip-172-31-4-197 6.8.0-1036-aws #38~22.04.1-Ubuntu SMP Fri Aug 22 15:44:33 UTC 2025 x86_64
User: ubuntu (1000)
PHP: 7.4.33
Disabled: pcntl_alarm,pcntl_fork,pcntl_waitpid,pcntl_wait,pcntl_wifexited,pcntl_wifstopped,pcntl_wifsignaled,pcntl_wifcontinued,pcntl_wexitstatus,pcntl_wtermsig,pcntl_wstopsig,pcntl_signal,pcntl_signal_get_handler,pcntl_signal_dispatch,pcntl_get_last_error,pcntl_strerror,pcntl_sigprocmask,pcntl_sigwaitinfo,pcntl_sigtimedwait,pcntl_exec,pcntl_getpriority,pcntl_setpriority,pcntl_async_signals,pcntl_unshare,
Upload Files
File: //var/www/api-management/node_modules/preview-email/index.js
const childProcess = require('child_process');
const fs = require('fs');
const http = require('http');
const os = require('os');
const path = require('path');
const process = require('process');
const util = require('util');
const { Buffer } = require('buffer');
const displayNotification = require('display-notification');
const getPort = require('get-port');
const nodemailer = require('nodemailer');
const open = require('open');
const pEvent = require('p-event');
const pWaitFor = require('p-wait-for');
const pug = require('pug');
const uuid = require('uuid');
const { isCI } = require('ci-info');
const { simpleParser } = require('mailparser');

const debug = util.debuglog('preview-email');
const isMacOS = os.platform() === 'darwin';
const writeFile = util.promisify(fs.writeFile);
const transport = nodemailer.createTransport({
  streamTransport: true,
  buffer: true
});
const templateFilePath = path.join(__dirname, 'template.pug');
const renderFilePromise = util.promisify(pug.renderFile);

const previewEmail = async (message, options) => {
  options = {
    dir: os.tmpdir(),
    id: uuid.v4(),
    open: { wait: false },
    template: templateFilePath,
    urlTransform: (path) => `file://${path}`,
    openSimulator: process.env.NODE_ENV !== 'test',
    returnHTML: false,
    // <https://nodemailer.com/extras/mailparser/#options>
    simpleParser: {},
    hasDownloadOriginalButton: true,
    ...options
  };

  debug('message', message, 'options', options);

  let raw;
  let base64;
  if (Buffer.isBuffer(message)) {
    raw = message;
    if (options.hasDownloadOriginalButton) base64 = message.toString('base64');
  } else if (typeof message === 'string') {
    raw = message;
    if (options.hasDownloadOriginalButton)
      base64 = Buffer.from(message).toString('base64');
  } else if (typeof message === 'object') {
    const response = await transport.sendMail(message);
    raw = response.message;
    if (options.hasDownloadOriginalButton)
      base64 = Buffer.from(response.message).toString('base64');
  } else {
    throw new TypeError('Message argument is required');
  }

  const parsed = await simpleParser(raw, options.simpleParser);
  if (options.hasDownloadOriginalButton) parsed.base64 = base64;

  const html = await renderFilePromise(
    options.template,
    Object.assign(parsed, {
      cache: true,
      pretty: true
    })
  );

  const filePath = `${options.dir}/${options.id}.html`;
  const url = options.urlTransform(filePath);

  if (!options.returnHTML) {
    await writeFile(filePath, html);
    if (options.open) await open(url, options.open);
  }

  //
  // if on macOS then send a toast notification about XCode and Simulator for iOS
  // App Store: <https://apps.apple.com/us/app/xcode/id497799835?mt=12>
  // Developer Website: <https://developer.apple.com/download/all/?q=xcode>
  // open -a Simulator
  // `xcrun simctl openurl booted ${url}`
  //
  if (isMacOS && !isCI && options.openSimulator) {
    try {
      // <https://github.com/sindresorhus/open/blob/05ba9e150cc1a2629e518a9cc19b586c6ca3f269/index.js#L205-L222>
      const simulator = childProcess.spawn('open', ['-a', 'Simulator']);
      await new Promise((resolve, reject) => {
        simulator.once('error', reject);
        simulator.once('close', (exitCode) => {
          if (exitCode !== 0)
            return reject(
              new Error(
                'Install XCode from the macOS App Store or Apple Developer Website to continue.'
              )
            );
          resolve(simulator);
        });
      });

      // wait for the simulator to have been booted
      // xcrun simctl list devices booted -j
      await pWaitFor(async () => {
        const devices = childProcess.spawn('xcrun', [
          'simctl',
          'list',
          'devices',
          'booted',
          '-j'
        ]);
        let stdout = '';
        devices.stdout.on('data', (data) => {
          stdout += data;
        });
        await new Promise((resolve, reject) => {
          devices.once('error', reject);
          devices.once('close', () => {
            resolve();
          });
        });
        let booted = false;
        try {
          const json = JSON.parse(stdout);
          for (const device of Object.keys(json.devices)) {
            for (const output of json.devices[device]) {
              if (output.state === 'Booted') {
                booted = true;
                break;
              }
            }
          }
        } catch (err) {
          debug(err);
        }

        return booted;
      });

      // let done = false;
      const server = http.createServer((req, res) => {
        pEvent(res, 'close').then(() => {
          debug('end');
          // done = true;
        });
        debug('request made');
        res.writeHead(200, { 'Content-Type': 'text/html' });
        res.write(html);
        res.end();
      });
      const port = await getPort();

      await new Promise((resolve, reject) => {
        server.listen(port, (err) => {
          if (err) return reject(err);
          debug('server started');
          resolve();
        });
      });

      const emlFilePath = `${options.dir}/${options.id}.eml`;
      await writeFile(emlFilePath, raw);
      debug('emlFilePath', emlFilePath);
      const xcrun = childProcess.spawn('xcrun', [
        'simctl',
        'openurl',
        'booted',
        emlFilePath
      ]);
      await new Promise((resolve, reject) => {
        xcrun.once('error', reject);
        xcrun.once('close', (exitCode) => {
          if (exitCode === 72)
            return reject(
              new Error(
                `Could not open URL in booted Simulator; make sure Simulator is running.`
              )
            );
          resolve(xcrun);
        });
      });

      /*
      const v = await cryptoRandomString({ length: 10, type: 'alphanumeric' });

      const xcrun = childProcess.spawn('xcrun', [
        'simctl',
        'openurl',
        'booted',
        `http://127.0.0.1:${port}/?v=${v}#html`
      ]);
      await new Promise((resolve, reject) => {
        xcrun.once('error', reject);
        xcrun.once('close', (exitCode) => {
          if (exitCode === 72)
            return reject(
              new Error(
                `Could not open URL in booted Simulator; make sure Simulator is running.`
              )
            );
          resolve(xcrun);
        });
      });

      await pWaitFor(() => done);
      */

      // display notification
      await displayNotification({
        title: 'iOS Simulator Preview',
        subtitle: 'Preview is ready!',
        text: 'Open Simulator to preview and Safari Web Inspector to inspect.',
        sound: 'Bottle'
      });

      await new Promise((resolve, reject) => {
        server.close((err) => {
          if (err) return reject(err);
          resolve();
        });
      });
    } catch (err) {
      debug(err);
      // display notification
      await displayNotification({
        title: 'iOS Simulator Preview',
        subtitle: 'Preview emails on iOS',
        text: err.message,
        sound: 'Bottle'
      });
    }
  }

  return options.returnHTML ? html : url;
};

module.exports = previewEmail;