#!/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"

# Total time we are willing to wait for a delivery confirmation.  A message
# still in flight when this expires is reported as accepted (1), not failed.
# Note that ASPSMS retries a buffered message for up to 24 hours, so a handset
# that is switched off will always leave us at the deadline.
DEADLINE=$(( SECONDS + 5 ))

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 -fsS \
    --get \
    "$API/SendSimpleSMS" \
    --data-urlencode "Userkey=$ASPSMS_USERKEY" \
    --data-urlencode "Password=$ASPSMS_PASSWORD" \
    --data-urlencode "Operation=SendTextSMS" \
    --data-urlencode "MSISDN=$PHONE" \
    --data-urlencode "MessageData=$MESSAGE" \
    --data-urlencode "TransactionReferenceNumber=$TRANSREF"
)" || {
    echo "Communication with ASPSMS failed" 1>&2
    exit 30
}

ERROR_CODE=$(jq -r '.ErrorCode // empty' <<<"$SEND_JSON")
ERROR_DESC=$(jq -r '.ErrorDescription // empty' <<<"$SEND_JSON")

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: $ERROR_DESC" 1>&2
        exit 41
        ;;
    5)
        echo "ASPSMS: $ERROR_DESC" 1>&2
        exit 42
        ;;
    2 | 6 | 7 | 24)
        echo "ASPSMS: $ERROR_DESC" 1>&2
        exit 30
        ;;
    "")
        echo "ASPSMS: unexpected response:" 1>&2
        echo "$SEND_JSON" 1>&2
        exit 30
        ;;
    *)
        echo "ASPSMS: $ERROR_DESC" 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
}

while [ "$SECONDS" -lt "$DEADLINE" ]
do
    sleep 1
    STATUS_JSON="$(
    curl -fsS \
        --get \
        "$API/SendingStat03/Get" \
        --data-urlencode "Userkey=$ASPSMS_USERKEY" \
        --data-urlencode "Password=$ASPSMS_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 // empty) else empty end' <<<"$STATUS_JSON")
    REASON=$(jq -r 'if type=="array" then (.[0].RSN // "") else "" end' <<<"$STATUS_JSON")

    case "$DELIVERY" in
        0)
            echo "ASPSMS: delivered to $PHONE" 1>&2
            exit 0
            ;;
        2)
            RESULT=$(classify_reason "$REASON")
            echo "ASPSMS: delivery to $PHONE failed, reason $REASON" 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
