xemail / cmd.js
command and library to send email
git clone http://git.nthia.dev/xemail

#!/usr/bin/env node
const VERSION = '1.0.0'
const fs = require('fs')
const { Readable } = require('stream')
const path = require('path')

let [args,argv] = require('./lib/args.js')(process.argv.slice(2), {
  boolean: [
    'h', 'help', 'utc', 'q', 'quiet', 'v', 'version',
    'no-date', 'raw', 'k', 'reject-unauthorized',
  ]
})
if (argv.has('help') || argv.has('h')) {
  usage()
  return process.exit(0)
}
if (argv.has('version') || argv.has('v')) {
  console.log(VERSION)
  return process.exit(0)
}

let subject = (argv.get('subject') ?? []).concat(argv.get('u') ?? [])[0]
let server = (argv.get('server') ?? []).concat(argv.get('s') ?? [])[0]
let to = (argv.get('to') ?? []).concat(argv.get('t') ?? [])
let from = (argv.get('from') ?? []).concat(argv.get('f') ?? [])[0]
let msgLines = (argv.get('msg') ?? []).concat(argv.get('m') ?? [])

let keyFile = (argv.get('key') ?? [])[0]
let certFile = (argv.get('cert') ?? [])[0]
let caFile = (argv.get('ca') ?? [])[0]
let key = null, cert = null, ca = null
if (keyFile !== undefined) key = fs.readFileSync(keyFile)
if (certFile !== undefined) cert = fs.readFileSync(certFile)
if (caFile !== undefined) cert = fs.readFileSync(caFile)

let m = null
let port = (argv.get('port') ?? []).concat(argv.get('p') ?? [])[0]
if (m = /^([^:]+):(\d+)$/.exec(server)) {
  server = m[1]
  port = m[2]
}

let missing = []
if (to.length === 0) missing.push('--to')
if (from === undefined) missing.push('--from')
if (missing.length > 0) {
  usage()
  console.error(`missing mandatory arguments: ${missing.join(', ')}\n`)
  return process.exit(1)
}
if (server === undefined) {
  let m = /(?:<[^@]+@([^>]+)\s*>|[^@]+@(\S+))/.exec(from)
  server = m[1] ?? m[2]
}

let date = (argv.get('date') ?? []).concat(argv.get('d') ?? [])[0]
let hasDateHeader = false
let headers = [ 'To: ' + to.join(','), 'From: ' + from ]
;(argv.get('header') ?? []).concat(argv.get('H') ?? []).forEach(h => {
  hasDateHeader &&= /^date\s*:/i.test(h)
  headers.push(h)
})

if (argv.has('no-date')) date = undefined
else if (!hasDateHeader && date === undefined) date = new Date

if (date !== undefined && argv.has('utc')) {
  headers.push('Date: ' + date.toUTCString())
} else if (date !== undefined) {
  headers.push('Date: ' + date.toString().replace(/\([^)]*\)/,''))
}
if (subject !== undefined) headers.push('Subject: ' + subject)

let rawHeaders = argv.has('raw')
let inStream = process.stdin
if (msgLines.length > 0 && rawHeaders) {
  inStream = Readable.from(msgLines.join('\r\n') + '\r\n')
} else if (msgLines.length > 0) {
  inStream = Readable.from(headers.join('\r\n') + '\r\n\r\n' + msgLines.join('\r\n') + '\r\n')
} else if (args.length > 0 && args[0] !== '-') {
  inStream = fs.createReadStream(args[0])
  if (!rawHeaders) inStream.push(headers.join('\r\n') + '\r\n\r\n')
} else if (!rawHeaders) {
  inStream.push(headers.join('\r\n') + '\r\n\r\n')
}

let opts = {
  to, from, server, port,
  rejectUnauthorized: (argv.get('reject-unauthorized') ?? []).concat(argv.get('k') ?? [])[0],
  key, cert, ca,
  data: inStream
}
require('./')(opts, (err,sent,code) => {
  if (err) {
    console.error(err)
    process.exit(1)
  } else if (!sent) {
    console.error('message not sent. code: ' + code)
    process.exit(1)
  } else if (!argv.has('quiet') && !argv.has('q')) {
    console.error('email sent successfully')
  }
})

function getCmd() { return path.basename(process.argv[1]) }
function usage() {
  console.error(`
    usage: ${getCmd()} {OPTIONS...} (MSG_FILE|-)

    Send an email message with the contents of MSG_FILE, stdin ("-"), -m, or --msg.

    -f FROM, --from=FROM
    -t TO, --to=TO

      Required arguments where FROM and TO are each email addresses.
      You can also format the addresses in the form "DISPLAY <ADDR@HOST>"
      which will set the appropriate header to set a display name
      different from the email address.

      Specify multiple recipients by using --to multiple times.

    -s SERVER, --server=SERVER
    -s SERVER:PORT, --server=SERVER:PORT
    -p PORT, --port PORT
 
      Connect to an smtp SERVER on PORT (default 25).
      If a SERVER isn't explicitly given, it is derived from FROM.

    -m MSG_LINE, --msg MSG_LINE

      Instead of reading from MSG_FILE or stdin, set lines of content from arguments.
      Each -m or --msg will add one line to the message body.

    -H FIELD:VALUE, --header=FIELD:VALUE

      Add "FIELD: VALUE" to the set of headers.

    --date=DATE

      Alias for --header=Date:DATE

    --no-date

      Do not automatically set a Date header.

    --utc

      Use UTC time for automatically sent Date headers.

    --raw

      Do not send any headers and do not encode the payload.
      MSG_FILE is responsible for setting headers and encoding.

    --cert=CERTFILE --key=KEYFILE

      Use a client-side tls certificate for the connection.

    --ca=CAFILE

      Use a custom certificate to verify the remote server.

    -k, --reject-unauthorized

      Do not validate the SSL certificate presented by the server.

    -v, --version

      Print the versinn of this software. (${VERSION})

    --help -h

      Show this message.

  `.trim().replace(/^ {4}/mg,'') + '\n')
}