# Decode SMS messages in PDU mode -- Adam Sampson <ats@offog.org>
# Written for Deeps' IRC bot...
#
# Rubbish spec at: http://www.usbdeveloper.com/GSMPage/gsmpage.htm#SMS
# More useful information here: http://www.dreamfabric.com/sms/
# The real spec, best of all:
# http://www.allsoft.co.za/downloads/getfile.php?filename=GSM_03.40_6.0.0.pdf

import sys

def tobinary(n):
	s = ""
	for i in range(8):
		s = ("%1d" % (n & 1)) + s
		n >>= 1
	return s

def bcd_decode(bs):
	s = "".join(["%1x%1x" % (b & 0xF, b >> 4) for b in bs])
	if s[-1] == "f":
		s = s[:-1]
	return s

def timestamp_decode(bs):
	if len(bs) != 7:
		return "timestamp-length-not-7"
	bs = [((n & 0xf) * 10) + (n >> 4) for n in bs]
	if bs[0] >= 90: # I don't know if this is the right cut-off point...
		year = 1900 + bs[0]
	else:
		year = 2000 + bs[0]
	timezone = bs[6]
	if timezone > 0x80:
		zone = "-%.2f" % ((timezone - 0x80) / 4,)
	else:
		zone = "+%.2f" % (timezone / 4,)
	return "%04d-%02d-%02d %02d:%02d:%02d GMT%s" % (year, bs[1], bs[2], bs[3], bs[4], bs[5], zone)

def unpack_sevenbit(bs, chop = 0):
	# Unpack 7-bit characters
	msgbytes = [] + bs
	msgbytes.reverse()
	asbinary = "".join(map(tobinary, msgbytes))
	if chop != 0:
		asbinary = asbinary[:-chop]
	chars = []
	while len(asbinary) >= 7:
		chars.append(int(asbinary[-7:], 2))
		asbinary = asbinary[:-7]
	return "".join(map(chr, chars))

def number_decode(bs):
	num_type = bs[0]
	bs = bs[1:]
	if num_type == 0x91:
		return "+" + bcd_decode(bs)
	elif num_type == 0xD0:
		return unpack_sevenbit(bs)
	else:
		return "unknown-number-type:%02x:" % (num_type,) + "".join(["%02x" % (n,) for n in bs])

# Should probably do some sort of period clearing-out...
fragment_cache = {}

def decode_pdu(s, handle_message):
	print "Message:", s

	bytes = [int(s[i:i+2], 16) for i in range(0, len(s), 2)]

	# Parse the SMSC header
	smsc_len = bytes[0]
	print "SMSC address:", number_decode(bytes[1:1 + smsc_len])
	pdu_pos = smsc_len + 1

	# Parse the PDU first octet
	first = bytes[pdu_pos]
	tp_mti = first & 0x03
	print "TP-MTI:", tp_mti
	if tp_mti != 0:
		print "Not an SMS-DELIVER message -- ignoring"
		return
	print "TP flags:",
	if first & 0x04 == 0:
		print "TP-MMS", # more messages to send
	if first & 0x20 != 0:
		print "TP-SRI", # status report indication
	tp_udhi = (first & 0x40) != 0
	if tp_udhi:
		print "TP-UDHI", # user data header indicator
	if first & 0x80 != 0:
		print "TP-RP", # reply path
	print

	# This length is in digits (the number is padded if odd)
	address_len = bytes[pdu_pos + 1]
	address_len_octets = (address_len + 1) / 2
	address_pos = pdu_pos + 2
	pid_pos = address_pos + 1 + address_len_octets
	from_number = number_decode(bytes[address_pos:pid_pos])
	print "Sender address:", from_number
	tp_pid = bytes[pid_pos]
	print "TP-PID Protocol identifier:", hex(tp_pid)
	if tp_pid != 0:
		print "Not a regular SMS message -- ignoring"
		return
	tp_dcs = bytes[pid_pos + 1]
	print "TP-DCS Data coding scheme:", hex(tp_dcs)
	# Make sure it's in the 7-bit packed format we understand
	if tp_dcs != 0:
		print "Not in 7-bit packed format -- ignoring"
		return
	print "TP-SCTS Time stamp:", timestamp_decode(bytes[pid_pos + 2:pid_pos + 9])
	tp_udl = bytes[pid_pos + 9]
	print "TP-UDL User data length:", tp_udl
	tp_ud = bytes[pid_pos + 10:]
	print "TP-UD data:", repr(tp_ud)

	# Note this is in the GSM 03.38 7-bit alphabet, not ASCII (although
	# it's close for the US-ASCII bits...)

	if tp_udhi:
		is_csm = False
		csm_ref = 0
		csm_total = 0
		csm_seq = 0

		# GDM 03.40 User Data Header present
		print "TP-UD:"
		udhl = tp_ud[0]
		udh = tp_ud[1:1 + udhl]
		print "  UDH User data header:", repr(udh)
		part = 1
		while len(udh) > 0:
			print "    Information Element", part
			iei = udh[0]
			print "      Identifier:", hex(iei)
			ie_len = udh[1]
			ie_data = udh[2:2 + ie_len]
			print "      Data:", repr(ie_data)
			if iei == 0:
				# The user data to follow is part of a longer
				# message that needs reassembling.
				print "      Data to follow is a fragment of a concatenated short message"
				is_csm = True
				(csm_ref, csm_total, csm_seq) = ie_data
				print "      Ref number:", csm_ref
				print "      Total:", csm_total
				print "      Sequence:", csm_seq
			udh = udh[2 + ie_len:]
			part += 1
		# We need to lose the padding bits before the start of the
		# seven-bit packed data, which means we need to figure out how
		# many there are...
		# See the diagram on page 58 of GSM_03.40_6.0.0.pdf.
		padding_size = ((7 * tp_udl) - (8 * (udhl + 1))) % 7
		ud = unpack_sevenbit(tp_ud[1 + udhl:], padding_size)
		print "  Remaining user data:", repr(ud)

		if is_csm:
			d = fragment_cache.setdefault(csm_ref, {})
			d[csm_seq] = ud
			msg_complete = True
			for i in range(1, csm_total + 1):
				if not d.has_key(i):
					msg_complete = False
			if msg_complete:
				msg = "".join([d[i] for i in range(1, csm_total + 1)])
				del fragment_cache[csm_ref]
				handle_message(msg, from_number)
	else:
		ud = unpack_sevenbit(tp_ud)
		print "  User data:", repr(ud)
		handle_message(ud, from_number)

	print
	sys.stdout.flush()

if __name__ == "__main__":
	pdus = [
		"0791448720900253040C914497035290960000500151614414400DD4F29C9E769F41E17338ED06",
		"0791448720003023440C91449703529096000050015132532240A00500037A020190E9339A9D3EA3E920FA1B1466B341E472193E079DD3EE73D85DA7EB41E7B41C1407C1CBF43228CC26E3416137390F3AABCFEAB3FAAC3EABCFEAB3FAAC3EABCFEAB3FAAC3EABCFEAB3FADC3EB7CFED73FBDC3EBF5D4416D9457411596457137D87B7E16438194E86BBCF6D16D9055D429548A28BE822BA882E6370196C2A8950E291E822BA88",
		"0791448720003023440C91449703529096000050015132537240310500037A02025C4417D1D52422894EE5B17824BA8EC423F1483C129BC725315464118FCDE011247C4A8B44",
		"07914477790706520414D06176198F0EE361F2321900005001610013334014C324350B9287D12079180D92A3416134480E",
		"0791448720003023440C91449703529096000050016121855140A005000301060190F5F31C447F83C8E5327CEE0221EBE73988FE0691CB65F8DC05028190F5F31C447F83C8E5327CEE028140C8FA790EA2BF41E472193E7781402064FD3C07D1DF2072B90C9FBB402010B27E9E83E86F10B95C86CF5D2064FD3C07D1DF2072B90C9FBB40C8FA790EA2BF41E472193E7781402064FD3C07D1DF2072B90C9FBB402010B27E9E83E8",
		"0791448720003023440C91449703529096000050016121850240A0050003010602DE2072B90C9FBB402010B27E9E83E86F10B95C86CF5D201008593FCF41F437885C2EC3E72E100884AC9FE720FA1B442E97E1731708593FCF41F437885C2EC3E72E100884AC9FE720FA1B442E97E17317080442D6CF7310FD0D2297CBF0B90B040221EBE73988FE0691CB65F8DC05028190F5F31C447F83C8E5327CEE028140C8FA790EA2BF41",
		"0791448720003023440C91449703529096000050016121854240A0050003010603C8E5327CEE0221EBE73988FE0691CB65F8DC05028190F5F31C447F83C8E5327CEE028140C8FA790EA2BF41E472193E7781402064FD3C07D1DF2072B90C9FBB402010B27E9E83E86F10B95C86CF5D201008593FCF41F437885C2EC3E72E10B27E9E83E86F10B95C86CF5D201008593FCF41F437885C2EC3E72E100884AC9FE720FA1B442E97E1",
		"0791448720003023400C91449703529096000050016121858240A0050003010604E62E100884AC9FE720FA1B442E97E17317080442D6CF7310FD0D2297CBF0B90B040221EBE73988FE0691CB65F8DC0542D6CF7310FD0D2297CBF0B90B040221EBE73988FE0691CB65F8DC05028190F5F31C447F83C8E5327CEE028140C8FA790EA2BF41E472193E7781402064FD3C07D1DF2072B90C9FBB402010B27E9E83E86F10B95C86CF5D",
		"0791448720003023400C91449703529096000050016121853340A005000301060540C8FA790EA2BF41E472193E7781402064FD3C07D1DF2072B90C9FBB402010B27E9E83E86F10B95C86CF5D201008593FCF41F437885C2EC3E72E100884AC9FE720FA1B442E97E17317080442D6CF7310FD0D2297CBF0B90B84AC9FE720FA1B442E97E17317080442D6CF7310FD0D2297CBF0B90B040221EBE73988FE0691CB65F8DC05028190",
		"0791448720003023440C914497035290960000500161218563402A050003010606EAE73988FE0691CB65F8DC05028190F5F31C447F83C8E5327CEE0281402010",
		]

	def handle_message(message, from_number):
		"""Called once a complete message is received."""
		print "Got message", repr(message), "from number", from_number

	for pdu in pdus:
		decode_pdu(pdu, handle_message)

