#!/bin/bash

# Bail on error
set -e

# Constants
NAME="twimsg"
DEFAULT_CONFIG_FILE='/etc/twilio.conf'
DEFAULT_TRUNCATE_LIMIT='3200'
BASE_URL='https://api.twilio.com/'
ACCOUNTS_PATH='/2010-04-01/Accounts/${ACCOUNT_SID}/Messages'
RESULT_XSL='/usr/share/twilio-utils/result.xsl'
CURL="/usr/bin/curl"
ICONV="/usr/bin/iconv"
XSLTPROC="/usr/bin/xsltproc"
DEFAULT_ENCODING="UTF-8"
TWILIO_ENCODING="UTF-8"
SMSLEN="/usr/bin/smslen"

# Usage message
usage()
{
    echo "Usage: ${NAME} [options] dest-number" 1>&2
    echo "Options:" 1>&2
    echo "    -C curlflag       Pass curlflag to curl(1)" 1>&2
    echo "    -c file           Specify config file (default \"${DEFAULT_CONFIG_FILE}\")" 1>&2
    echo "    -e encoding       Specify input encoding (default \"${DEFAULT_ENCODING}\")" 1>&2
    echo "    -F from-number    Specify sending phone number (E.164 format)" 1>&2
    echo "    -f file           Read message content from file (\`-' means stdin (the default))" 1>&2
    echo "    -m url            Include the image at the specified URL via MMS (may be repeated)" 1>&2
    echo "    -P                Ask for auth token from the terminal" 1>&2
    echo "    -p auth-token     Specify auth token (default read AUTH_TOKEN from config file)" 1>&2
    echo "    -S                Output message ID returned from Twilio to standard output" 1>&2
    echo "    -t limit          Truncate input after limit bytes (default ${DEFAULT_TRUNCATE_LIMIT})" 1>&2
    echo "    -u account-sid    Specify Account SID (default read ACCOUNT_SID from config file)" 1>&2
    echo "    -v                Enable verbose output" 1>&2
}

# Function to normalize a phone number to the way Twilio likes it
normalize()
{
    # If already in E.164 format, then leave it alone
    if [[ "${1}" =~ ^\+ ]]; then
        echo "${1}"
        return
    fi

    # Clean up NANP numbers
    echo ${1+"$@"} | sed -r \
      -e 's/[^0-9]//g' \
      -e 's/^1?([2-9][0-9]{2}[2-9][0-9]{2}[0-9]{4})$/+1\1/g'
}

# Parse flags passed in on the command line
CONFIG_FILE="${DEFAULT_CONFIG_FILE}"
ENCODING="${DEFAULT_ENCODING}"
TRUNCATE_LIMIT="${DEFAULT_TRUNCATE_LIMIT}"
OVERRIDE_FROM_NUMBER=""
OVERRIDE_ACCOUNT_SID=""
OVERRIDE_AUTH_TOKEN=""
INPUT_FILE="-"
CURL_FLAGS=""
OUTPUT_ID="false"
VERBOSE="false"
while [ ${#} -gt 0 ]; do
    case "$1" in
        -c)
            shift
            CONFIG_FILE="${1}"
            shift
            ;;
        -C)
            shift
            CURL_FLAGS="${CURL_FLAGS} ${1}"
            shift
            ;;
        -e)
            shift
            ENCODING="${1}"
            shift
            ;;
        -f)
            shift
            INPUT_FILE="${1}"
            shift
            ;;
        -F)
            shift
            OVERRIDE_FROM_NUMBER="${1}"
            shift
            ;;
        -m)
            shift
            IMAGE_URL="${1}"
            if ! [[ "${IMAGE_URL}" =~ ^https?://[^[:space:]]+$ ]]; then
                echo "${NAME}: invalid image URL \`${IMAGE_URL}'" 1>&2
                exit 1
            fi
            shift
            CURL_FLAGS="${CURL_FLAGS} --data-urlencode MediaUrl=${IMAGE_URL}"
            ;;
        -p)
            shift
            OVERRIDE_AUTH_TOKEN="${1}"
            shift
            ;;
        -P)
            OVERRIDE_AUTH_TOKEN="PROMPT"
            shift
            ;;
        -S)
            OUTPUT_ID="true"
            shift
            ;;
        -t)
            shift
            TRUNCATE_LIMIT="${1}"
            shift
            ;;
        -u)
            shift
            OVERRIDE_ACCOUNT_SID="${1}"
            shift
            ;;
        -v)
            VERBOSE="true"
            shift
            ;;
        -h|--help)
            usage
            exit
            ;;
        --)
            shift
            break
            ;;
        *)
            break
            ;;
    esac
done
case "${#}" in
    1)
        TO_NUMBER="$1"
        ;;
    *)
        usage
        exit 1
        ;;
esac

# Ensure config file is readable; if not, then what we need must be provided on the command line
if ! test -r "${CONFIG_FILE}"; then
    if test -z "${OVERRIDE_FROM_NUMBER}" -o -z "${OVERRIDE_ACCOUNT_SID}" -o -z "${OVERRIDE_AUTH_TOKEN}"; then
        echo "${NAME}: can't read ${CONFIG_FILE}" 1>&2
        exit 1
    fi
else
    # Parse config file
    . "${CONFIG_FILE}"
fi

# Override acccount ID from command line
if [ -n "${OVERRIDE_ACCOUNT_SID}" ]; then
    ACCOUNT_SID="${OVERRIDE_ACCOUNT_SID}"
fi

# Input auth token
if [ "${OVERRIDE_AUTH_TOKEN}" = 'PROMPT' ]; then
    read -s -p "Enter auth token for ${ACCOUNT_SID}: " OVERRIDE_AUTH_TOKEN
    echo 1>&2
fi

# Override some config with command line flags
if [ -n "${OVERRIDE_FROM_NUMBER}" ]; then
    FROM_NUMBER="${OVERRIDE_FROM_NUMBER}"
fi
if [ -n "${OVERRIDE_AUTH_TOKEN}" ]; then
    AUTH_TOKEN="${OVERRIDE_AUTH_TOKEN}"
fi

# Normalize numbers
FROM_NUMBER=`normalize "${FROM_NUMBER}"`
TO_NUMBER=`normalize "${TO_NUMBER}"`

# Sanity check stuff
if ! [[ "${FROM_NUMBER}" =~ ^(\+1[2-9][0-9]{2}[2-9][0-9]{2}[0-9]{4}|[0-9]{5,6})$ ]]; then
    echo "${NAME}: invalid source phone number \`${FROM_NUMBER}'" 1>&2
    exit 1
fi
if ! [[ "${TO_NUMBER}" =~ ^(\+1[2-9][0-9]{2}[2-9][0-9]{2}[0-9]{4}|[0-9]{5,6})$ ]]; then
    echo "${NAME}: invalid destination phone number \`${TO_NUMBER}'" 1>&2
    exit 1
fi
if ! [[ "${ACCOUNT_SID}" =~ ^AC[0-9a-f]{32}$ ]]; then
    echo "${NAME}: invalid account SID \`${ACCOUNT_SID}'" 1>&2
    exit 1
fi
if ! [[ "${AUTH_TOKEN}" =~ ^[0-9a-f]{32}$ ]]; then
    echo "${NAME}: invalid authentication token" 1>&2
    exit 1
fi

# Build URL
URL="`echo ${BASE_URL} | sed 's|/$||g'`""`eval echo ${ACCOUNTS_PATH}`"

# Create temporary files for message response and error
RESPONSE_FILE=`mktemp -q /tmp/${NAME}.XXXXXX`
if [ $? -ne 0 ]; then
    echo "${NAME}: can't create temporary file" 1>&2
    exit 1
fi
OUTPUT_FILE=`mktemp -q /tmp/${NAME}.XXXXXX`
if [ $? -ne 0 ]; then
    rm -f ${RESPONSE_FILE}
    echo "${NAME}: can't create temporary file" 1>&2
    exit 1
fi
trap "rm -f ${RESPONSE_FILE} ${OUTPUT_FILE}" 0 2 3 5 10 13 15

# Truncate input and post to Twilio
[ "${VERBOSE}" != 'true' ] || echo "${NAME}: posting data to ${URL}" 1>&2
cat "${INPUT_FILE}" \
  | "${ICONV}" -c -s -f "${ENCODING}" -t "${TWILIO_ENCODING}" \
  | "${SMSLEN}" -i "${TWILIO_ENCODING}" -t "${TRUNCATE_LIMIT}" \
  | "${CURL}" --silent \
  --user "${ACCOUNT_SID}:${AUTH_TOKEN}" \
  --data "From=${FROM_NUMBER}" \
  --data "To=${TO_NUMBER}" \
  --data-urlencode "Body@-" \
  --output "${RESPONSE_FILE}" \
  ${CURL_FLAGS} \
  "${URL}"

# Check result
CURL_RESULT="$?"
if [ "${CURL_RESULT}" -ne 0 ]; then
    echo "${NAME}: error sending request (curl returned ${CURL_RESULT})" 1>&2
    exit 1
fi
if [ "${VERBOSE}" = 'true' ]; then
    echo "${NAME}: received response:" 1>&2
    cat "${RESPONSE_FILE}" 1>&2
    echo 1>&2
fi

# Apply XSLT to returned XML to get error message
"${XSLTPROC}" --stringparam lsop sids "${RESULT_XSL}" "${RESPONSE_FILE}" > "${OUTPUT_FILE}" 2>&1
if [ $? -ne 0 ]; then
    echo -n "${NAME}: error parsing result:" 1>&2
    cat "${OUTPUT_FILE}" 1>&2
    exit 1
fi
if [ "${VERBOSE}" = 'true' ]; then
    echo "${NAME}: successfully parsed response:" 1>&2
    cat "${OUTPUT_FILE}" 1>&2
fi

# Get SMS ID or error response from output file
SMS_ID=`cat "${OUTPUT_FILE}"`
if ! [[ "${SMS_ID}" =~ ^SM[0-9a-f]{32}$ ]]; then
    echo -n "${NAME}: " 1>&2
    cat "${OUTPUT_FILE}" 1>&2
    exit 1
fi
if [ "${OUTPUT_ID}" = 'true' ]; then
    echo "${SMS_ID}"
fi

# Done
exit 0

