#!/bin/bash
# This file is in the public domain.
# Transmit a Challenger TAN by SMS using the Telesign Messaging 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:
#   TELESIGN_AUTH_TOKEN
#
# Exit codes (see challenger-send-sms-telesign(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.
# Telesign cannot distinguish "too short" from "too long" (both are
# 11000) and cannot detect a landline without its separate PhoneID product, so
# this helper never reports 11 or 12.
#
#    0 delivered    201 accepted
#   13 not mobile    14 invalid number    15 blocked
#   20 offline       21 expired           22 carrier
#   30 provider      31 rejected          32 rate limited     33 no verdict
#   40 config        41 authorization     42 balance
#   50 unclassified

set -eu

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

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

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

API="https://rest-ww.telesign.com/v1/messaging"

# Private files keep message text and credentials out of subprocess arguments.
umask 077
WORKDIR=$(mktemp -d "${TMPDIR:-/tmp}/telesign-sms-XXXXXX")
trap 'rm -rf -- "$WORKDIR"' EXIT
trap 'exit 129' HUP
trap 'exit 130' INT
trap 'exit 143' TERM
printf '%s' "$MESSAGE" >"$WORKDIR/message"
BODY="$WORKDIR/response"
# A Basic token must fit on one HTTP header line.
if [[ $TELESIGN_AUTH_TOKEN == *$'\r'* || $TELESIGN_AUTH_TOKEN == *$'\n'* ]]; then
  echo "TELESIGN_AUTH_TOKEN contains a line break" >&2
  exit 40
fi
printf 'Authorization: Basic %s\n' "$TELESIGN_AUTH_TOKEN" >"$WORKDIR/auth"

# Map a Telesign status code onto our exit codes.  Telesign's documentation is
# explicit that callers should branch on this field rather than on the HTTP
# status, and it is present in the body of failed requests too -- which is why
# curl --fail must not be used here: it would discard exactly this field, and
# an invalid number (11000, HTTP 400) would be indistinguishable from any other
# error.  A return of 201 means "no verdict yet, keep polling".
classify() {
  case "$1" in
  200)
    echo 0
    ;;
  # Accepted, in progress, or Telesign lost the delivery receipt.
  203 | 250 | 251 | 290 | 292 | 293 | 295)
    echo 201
    ;;
  # SMS not supported by this phone, carrier, plan or user.
  222)
    echo 13
    ;;
  # 11000 covers malformed, too short and too long alike.
  11000 | 11001 | 211 | 220 | 507 | 513)
    echo 14
    ;;
  229 | 230 | 233 | 237 | 238 | 510 | 515 | 518 | 520 | 521 | 10032)
    echo 15
    ;;
  # 511 is the *subscriber* running out of credit, not our account.
  210 | 512 | 514 | 511)
    echo 20
    ;;
  221)
    echo 21
    ;;
  207 | 502 | 503 | 504)
    echo 22
    ;;
  500 | 505 | 506 | 599 | 10000)
    echo 30
    ;;
  231 | 508 | 509 | 517 | 11003 | 11004 | 11009 | 11010 | 11011 | 11013 | 11016 | 11017 | 11027 | 11034 | 11035 | 12000 | 12009 | 12017)
    echo 31
    ;;
  10019 | 10020)
    echo 32
    ;;
  501 | 10002 | 10003 | 10004 | 10005 | 10006 | 10007 | 10008 | 10009 | 10010 | 10011 | 10014 | 10015 | 10022 | 10028 | 10031 | 10033 | 10035)
    echo 41
    ;;
  10030)
    echo 42
    ;;
  *)
    echo 50
    ;;
  esac
}

HTTP=$(curl --silent --connect-timeout 5 --max-time 10 \
  --output "$BODY" \
  --write-out '%{http_code}' \
  --url "$API" \
  --request POST \
  --header "@$WORKDIR/auth" \
  --header "Content-Type: application/x-www-form-urlencoded" \
  --data account_lifecycle_event=transact \
  --data-urlencode "phone_number=$PHONE_NUMBER" \
  --data-urlencode "message@$WORKDIR/message" \
  --data message_type=OTP) || {
  echo "Telesign: could not be reached" 1>&2
  exit 30
}

STATUS_CODE=$(jq -r '.status.code | select(type == "number" and . == floor)' <"$BODY" 2>/dev/null || true)

if [ -z "$STATUS_CODE" ]; then
  echo "Telesign: unexpected reply (HTTP $HTTP) sending to $PHONE_NUMBER" 1>&2
  case "$HTTP" in
  401 | 403) exit 41 ;;
  402) exit 42 ;;
  429) exit 32 ;;
  4??) exit 31 ;;
  *) exit 30 ;;
  esac
fi

RESULT=$(classify "$STATUS_CODE")

if [ 0 -eq "$RESULT" ]; then
  echo "Telesign: delivered to $PHONE_NUMBER" 1>&2
  exit 0
fi

if [ 201 -ne "$RESULT" ]; then
  echo "Telesign: submission failed with helper status $RESULT for $PHONE_NUMBER" 1>&2
  exit "$RESULT"
fi

REFERENCE_ID=$(jq -r '.reference_id | select(type == "string")' <"$BODY" 2>/dev/null || true)

if [ -z "$REFERENCE_ID" ]; then
  echo "Telesign: accepted message for $PHONE_NUMBER but returned no reference_id" 1>&2
  exit 201
fi

# 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
  curl --silent --connect-timeout 5 --max-time "$REMAINING" \
    --output "$BODY" \
    --url "$API/$REFERENCE_ID" \
    --header "@$WORKDIR/auth" || continue
  STATUS_CODE=$(jq -r '.status.code | select(type == "number" and . == floor)' <"$BODY" 2>/dev/null || true)
  if [ -z "$STATUS_CODE" ]; then
    continue
  fi
  RESULT=$(classify "$STATUS_CODE")
  if [ 201 -ne "$RESULT" ]; then
    if [ 0 -eq "$RESULT" ]; then
      echo "Telesign: delivered to $PHONE_NUMBER" 1>&2
    else
      echo "Telesign: delivery failed with helper status $RESULT for $PHONE_NUMBER" 1>&2
    fi
    exit "$RESULT"
  fi
done

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