#!/bin/bash
# This file is in the public domain.
# Transmit a Challenger TAN by SMS using the ASPSMS web API.
#
# Usage:
#   echo "MESSAGE" | $0 +41791234567
#
# Note that unlike challenger-send-sms(1), the argument is the bare phone
# number: this script is invoked by the dispatcher, which has already extracted
# and syntax-checked it.
#
# Environment:
#   ASPSMS_USERKEY
#   ASPSMS_PASSWORD
#
# Exit codes (see challenger-send-sms-aspsms(1)).  Success is 0 or the
# 200-210 band; every other value is a failure.  The success band is up at
# 200 and not at 1 because libgnunetutil reports a failed exec() of the
# helper as exit code 1, which must not be mistaken for a delivery.
# ASPSMS has no error code for an unusable recipient number at all,
# so 11 and 12 are never reported and 14 can only arrive asynchronously, in the
# delivery report.  ASPSMS documents no rate limit, so 32 is never reported.
#
#    0 delivered    201 accepted
#   13 not mobile    14 invalid number    15 barred
#   20 offline       21 expired           22 carrier
#   30 provider      31 rejected          33 no verdict
#   40 config        41 authorization     42 balance
#   50 unclassified

set -euo pipefail

if [ -z "${ASPSMS_USERKEY:-}" ]; then
  echo "ASPSMS_USERKEY not set in environment" 1>&2
  exit 40
fi

if [ -z "${ASPSMS_PASSWORD:-}" ]; then
  echo "ASPSMS_PASSWORD not set in environment" 1>&2
  exit 40
fi

if [ $# -ne 1 ]; then
  echo "Usage: $0 <phone_number>" 1>&2
  exit 40
fi

PHONE="$1"
MESSAGE="$(cat -)"

if [ -z "$MESSAGE" ]; then
  echo "Empty message" 1>&2
  exit 40
fi

API="https://webapi.aspsms.com"

# Private files keep message text and credentials out of subprocess arguments.
umask 077
WORKDIR=$(mktemp -d "${TMPDIR:-/tmp}/aspsms-sms-XXXXXX")
trap 'rm -rf -- "$WORKDIR"' EXIT
trap 'exit 129' HUP
trap 'exit 130' INT
trap 'exit 143' TERM
printf '%s' "$MESSAGE" >"$WORKDIR/message"
printf '%s' "$ASPSMS_USERKEY" >"$WORKDIR/userkey"
printf '%s' "$ASPSMS_PASSWORD" >"$WORKDIR/password"

TRANSREF="$(date +%s)-$$-$RANDOM"

#
# Send the SMS.  ASPSMS answers with HTTP 200 even for authentication failures
# and reports the outcome in the integer ErrorCode field of the body, so
# curl --fail only ever trips on a genuine transport failure -- which is
# exactly what we want it to do here.
#
# The full ErrorCode table (1-25 published, 1-43 obtainable from the
# unauthenticated https://json.aspsms.com/ListAllStatusCodes endpoint) contains
# no code for a malformed or unallocated recipient number: ASPSMS accepts such
# a number with ErrorCode 1, consuming a credit, and only reveals the problem
# later as a delivery-report reason code.
#
SEND_JSON="$(
  curl -fs --connect-timeout 5 --max-time 10 \
    --get \
    "$API/SendSimpleSMS" \
    --data-urlencode "Userkey@$WORKDIR/userkey" \
    --data-urlencode "Password@$WORKDIR/password" \
    --data-urlencode "Operation=SendTextSMS" \
    --data-urlencode "MSISDN=$PHONE" \
    --data-urlencode "MessageData@$WORKDIR/message" \
    --data-urlencode "TransactionReferenceNumber=$TRANSREF"
)" || {
  echo "Communication with ASPSMS failed" 1>&2
  exit 30
}

ERROR_CODE=$(jq -r '.ErrorCode | select(type == "number" and . == floor)' <<<"$SEND_JSON" 2>/dev/null) || {
  echo "ASPSMS: invalid submission response" >&2
  exit 30
}

case "$ERROR_CODE" in
1) ;;
# Authorization failed, invalid UserKey, invalid password, user access
# denied, and the originator-authorization family: all mean the configured
# account cannot send as configured.
3 | 8 | 9 | 10 | 25 | 29 | 30 | 34)
  echo "ASPSMS: submission rejected" 1>&2
  exit 41
  ;;
5)
  echo "ASPSMS: submission rejected" 1>&2
  exit 42
  ;;
2 | 6 | 7 | 24)
  echo "ASPSMS: submission rejected" 1>&2
  exit 30
  ;;
"")
  echo "ASPSMS: unexpected response:" 1>&2
  exit 30
  ;;
*)
  echo "ASPSMS: submission rejected" 1>&2
  exit 31
  ;;
esac

#
# Poll the send log for a delivery report.
#
# DST is a number: -1 not yet submitted or rejected, 0 delivered, 1 buffered,
# 2 failed.  RSN is a zero-padded three-character *string*, so it must be
# matched as "000", never as 0.
#
classify_reason() {
  case "$1" in
  000 | 101 | 114 | 206)
    echo 14
    ;;
  100 | 102 | 106 | 111 | 115 | 117)
    echo 13
    ;;
  103 | 104)
    echo 15
    ;;
  107)
    echo 20
    ;;
  108)
    # At DST=2 this is "validity expired after retrying".  At DST=1 the
    # same code means the handset's memory is full, but we only ever
    # reach this classifier for a final failure.
    echo 21
    ;;
  # Originator not authorized: our own misconfiguration.
  030 | 031)
    echo 41
    ;;
  010 | 105 | 109 | 112 | 113 | 116 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 200)
    echo 22
    ;;
  001 | 002 | 003 | 004 | 005 | 006 | 007 | 008)
    echo 30
    ;;
  201 | 202 | 203 | 204 | 205 | 207 | 208)
    echo 31
    ;;
  *)
    echo 50
    ;;
  esac
}

# Acceptance starts a separate five-second window for delivery confirmation.
DEADLINE=$((SECONDS + 5))
while [ "$SECONDS" -lt "$DEADLINE" ]; do
  sleep 1
  REMAINING=$((DEADLINE - SECONDS))
  if [ "$REMAINING" -le 0 ]; then
    break
  fi
  STATUS_JSON="$(
    curl -fs --connect-timeout 5 --max-time "$REMAINING" \
      --get \
      "$API/SendingStat03/Get" \
      --data-urlencode "Userkey@$WORKDIR/userkey" \
      --data-urlencode "Password@$WORKDIR/password" \
      --data-urlencode "TransactionReferenceNumber=$TRANSREF"
  )" || continue

  # SendingStat03/Get returns an array of SMSMessagesWebAPI objects on
  # success, or an ASPSMSServerResult object ({"ErrorCode":..}) on error /
  # when no sendlog entry exists yet.  Guard against indexing the object
  # form, which would make jq error out and abort the script under `set -e`.
  DELIVERY=$(jq -r 'if type == "array" then (.[0].DST | select(type == "number" and . == floor)) else empty end' <<<"$STATUS_JSON" 2>/dev/null) || continue

  case "$DELIVERY" in
  0)
    echo "ASPSMS: delivered to $PHONE" 1>&2
    exit 0
    ;;
  2)
    REASON=$(jq -er '.[0].RSN | select(type == "string")' <<<"$STATUS_JSON" 2>/dev/null) || continue
    RESULT=$(classify_reason "$REASON")
    echo "ASPSMS: delivery to $PHONE failed with helper status $RESULT" 1>&2
    exit "$RESULT"
    ;;
  *)
    # 1 (buffered), -1 (not yet submitted) and an absent field all mean
    # "no verdict yet".
    continue
    ;;
  esac
done

echo "ASPSMS: accepted message for $PHONE, delivery not confirmed within the poll window" 1>&2
exit 201
