redbrick/static-site

View on GitHub
lib/emailNewPosts.js

Summary

Maintainability
B
5 hrs
Test Coverage
const fs = require('fs');
const path = require('path');
const async = require('async');
const parseFrontMatter = require('hexo-front-matter').parse;
const moment = require('moment');
const wrap80 = require('wordwrap')(80);
const yaml = require('js-yaml');
const logger = require('./logger');

const smtpTransport = require('./smtpTransport');
const sendMailTransport = require('./sendMailTransport');
const utils = require('./utils');
const readFileAsString = utils.readFileAsString;
const readFileAsArray = utils.readFileAsArray;
const writeArrayToFile = utils.writeArrayToFile;

const configFile = fs.readFileSync('_config.yml', 'utf8');
const config = yaml.safeLoad(configFile).server;
const sendMailDefault = true;

const postsDirectory = path.join(process.cwd(), 'source', '_posts');
const emailLogFilename = path.join(process.cwd(), 'logs', 'email_update_log');
const mailingListFilename = path.join(process.cwd(), 'mailing_list');

function sendEmail ({ senderName, subject, body }, address, callback) {
  const mailOptions = {
    from   : `${senderName} <${config.email.auth.user}>`,
    to     : address,
    subject: `[${config.mailsubject}] ${subject}`,
    text   : body,
  };
  if (!config.useSendMail || sendMailDefault) {
    smtpTransport.sendMail(mailOptions, err => {
      if (err) {
        logger.error(`SMTP failed: ${err}\nUnable to send email "${subject}" to recipient: ${address}`);
      }
      callback(err);
    });
  } else {
    sendMailTransport.sendMail(mailOptions, err => {
      if (err) {
        logger.error(`sendMail failed: ${err}\nUnable to send email "${subject}" to recipient: ${address}`);
      }
      callback(err);
    });
  }
}

function sendEmails (addresses, emailData, callback) {
  async.each(addresses, sendEmail.bind(null, emailData), err => {
    if (err) {
      logger.error(`Failed to send email "${emailData.subject}" to some recipients.`);
    } else {
      logger.info(`Email update for "${emailData.subject}" succeeded.`);
    }
    callback(err);
  });
}

function getEmailBody ({ frontMatter, contents, slug }) {
  const title = frontMatter.title;
  const date = new Date(frontMatter.date);
  const author = frontMatter.author;
  const tags = (frontMatter.tags || []).map(tag => `#${tag}`).join(', ');
  const plainTextContents = contents.split(/<[\w\W]+>/).join('').split(/!\[[\w\W]*]\([\w\W]*\)/).join(''); // strip html // strip images
  const directory = moment(date).format('YYYY/MM/DD');
  const permalink = `${config.siteroot}/${path.join(directory, slug)}`;
  return wrap80(`${title}\n${moment(date).format('DD/MM/YYYY')}\ntags: ${tags}\nposted by ${author}\n\nSee full post at: ${permalink}\n\n${plainTextContents}`);
}

function bail (err, callback) {
  logger.error('Unable to complete email update for new posts:');
  if (typeof callback === 'function') {
    callback(err);
  } else {
    logger.error(err);
  }
}

function emailNewPosts (callback) {
  logger.info('Sending email update(s) for any new posts...');
  readFileAsArray(mailingListFilename, (err, addresses) => {
    if (err) {
      return bail(`Unable to read file: ${mailingListFilename}`, callback);
    }
    const realAddresses = addresses.map(a => a.trim()).filter(a => /^.+@.+$/.test(a)); // barebones email address validation
    readFileAsArray(emailLogFilename, (err, log) => {
      if (err) {
        return bail(`Unable to read file: ${emailLogFilename}`, callback);
      }
      // if date doesn't exist, choose beginning of (unix) time
      const lastUpdated = new Date(log[0] || 0);
      fs.readdir(postsDirectory, (err, filenames) => {
        if (err) {
          return bail(`Unable to read directory ${postsDirectory}`, callback);
        }
        const lastCheckedDirectory = new Date();
        const postFilenames = filenames.filter(filename => filename.indexOf('.md') !== -1).map(filename => path.join(postsDirectory, filename));
        async.map(postFilenames, readFileAsString, (err, posts) => {
          if (err) {
            return bail(`Unable to read files in directory: ${postsDirectory}`, callback);
          }
          const emailDataList = posts
            .map((post, index) => ({
              // cut out front matter and surrounding whitespace
              contents: post.replace(/---[\w\W]*---/, '').trim(),

              frontMatter: parseFrontMatter(post),

              // get filename without parent directory and without '.md'
              slug: postFilenames[index].split('/').slice(-1)[0].slice(0, -3),
            }))
            .filter(({ frontMatter }) => {
              const date = frontMatter.date;
              return new Date(date) > lastUpdated;
            })
            .map(postData => ({
              subject   : postData.frontMatter.title,
              body      : getEmailBody(postData),
              senderName: postData.frontMatter.author,
            }));
          async.each(emailDataList, sendEmails.bind(null, realAddresses), err => {
            if (err) {
              logger.error('Failed to send some emails.');
            } else if (emailDataList.length) {
              logger.info(`Email update(s) successfully sent to ${realAddresses.length} recipients.`);
            } else {
              logger.info('No email updates were necessary.');
            }
            const updatedEmailLog = [lastCheckedDirectory.toISOString()].concat(log);
            writeArrayToFile(emailLogFilename, updatedEmailLog, err => {
              if (err) {
                return bail('Unable to update email log after send.', callback);
              }
              if (typeof callback === 'function') {
                callback();
              }
            });
          });
        });
      });
    });
  });
}

module.exports = emailNewPosts;