#!/usr/bin/env python
""" SendMMS - Script showing how to send a binary-encoded MMS to a network
operator's MMSC via a GSM phone/modem attached to the PC.
This script/module implements a very limited subset of the WSP and WTP layers
of the Wireless Application Protocol (WAP), in order to post binary MMS
data to an MMSC (through a WAP gateway), via a GSM phone/modem connected
to the PC.
This will create the MMS message, and attempt to send it to an operator's
MMSC via a WSP POST operation. It does NOT currently initiate the GPRS
connection via the GSM modem - use a ppp dialer (e.g. wvdial) to do that
before running this script. In other words, this script assumes that the
specified WAP gateway and MMSC are reachable through some pre-configured
network interface (so you might want to set up some routing information if
you are using a GSM modem).
@note: If you are new to python-mms, look at the "encode.py" and "decode.py"
scripts first before reading this one.
@author: Francois Aucamp <faucamp@csir.co.za>
@license: GNU LGPL
"""
import sys
import array
# Import python-mms
from mms.wsp_pdu import Decoder, Encoder, WSPEncodingAssignments
from mms.iterator import PreviewIterator
from mms import MMSMessage, MMSMessagePage
import socket, time
class WTP:
""" This class implements a very limited subset of the WTP layer """
pduTypes = {0x00: None, # Not Used
0x01: 'Invoke',
0x02: 'Result',
0x03: 'Ack',
0x04: 'Abort',
0x05: 'Segmented Invoke',
0x06: 'Segmented Result',
0x07: 'Negative Ack'}
abortTypes = {0x00: 'PROVIDER',
0x01: 'USER'}
abortReasons = {0x00: 'UNKNOWN',
0x01: 'PROTOERR',
0x02: 'INVALIDTID',
0x03: 'NOTIMPLEMENTEDCL2',
0x04: 'NOTIMPLEMENTEDSAR',
0x05: 'NOTIMPLEMENTEDUACK',
0x06: 'WTPVERSIONONE',
0x07: 'CAPTEMPEXCEEDED',
0x08: 'NORESPONSE',
0x09: 'MESSAGETOOLARGE',
0x10: 'NOTIMPLEMENTEDESAR'}
def __init__(self, gatewayHost, gatewayPort=9201):
self.udpSocket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)
self.tidCounter = 0
# Currently "active" WTP transactions (their IDs)
self.activeTransactions = []
self.gatewayHost = gatewayHost
self.gatewayPort = gatewayPort
def invoke(self, wspPDU):
""" Invoke (send) a request via WTP, and get the response.
This method automatically assigns a new unique transaction ID to the
transmitted PDU.
@return: an iterator over the bytes read from the response
@rtype: mms.iterator.previewIterator
"""
self.tidCounter += 1
print '>> WTP: Invoke, transaction ID: %d' % self.tidCounter
pdu = self.encodeInvokePDU(self.tidCounter) + wspPDU
self._sendPDU(pdu)
self.activeTransactions.append(self.tidCounter)
return self._parseResponse(self._receiveData())
def ack(self, transactionID):
print '>> WTP: Ack, transaction ID: %d' % transactionID
self._sendPDU(self.encodeAckPDU(transactionID))
def _sendPDU(self, pdu):
""" Transmits a PDU through the socket
@param pdu: The PDU to send (a sequence of bytes)
@type pdu: list
"""
data = ''
for char in pdu:
data += chr(char)
self.udpSocket.sendto(data, (self.gatewayHost, self.gatewayPort))
def _receiveData(self):
""" Read data from the UDP socket
@return: The data read from the socket
@rtype: str
"""
done = False
response = ''
while not done:
buff = self.udpSocket.recv(1024)
response += buff
if len(buff) < 1024:
done = True
return response
def _parseResponse(self, responseData):
""" Interpret data read from the socket (at the WTP layer level)
@param responseData: A buffer containing data to interpret
@type responseData: str
"""
byteArray = array.array('B')
for char in responseData:
byteArray.append(ord(char))
byteIter = PreviewIterator(byteArray)
pduType, transactionID = self._decodePDU(byteIter)
if pduType == 'Result':
self.ack(transactionID)
return byteIter
@staticmethod
def encodeInvokePDU(tid):
""" Builds a WTP Invoke PDU
@param tid: The transaction ID for this PDU
@type tid: int
@return: the WTP invoke PDU as a sequence of bytes
@rtype: list
The WTP Invoke PDU structure is defined in WAP-224, section 8.3.1::
Bit| 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7
Octet | | | | | | | |
1 |CON| PDU Type = Invoke |GTR|TDR|RID
2 | TID
3 |
4 |Version |TIDnew| U/P | RES |RES| TCL
...where bit 0 is the most significant bit.
Invoke PDU type = 0x01 = 0 0 0 1
GTR is 0 and TDR is 1 (check: maybe make both 1: segmentation not supported)
RID is set to 0 (not retransmitted)
TCL is 0x02 == 1 0 (transaction class 2)
Version is 0x00 (according to WAP-224, section 8.3.1)
Thus, for our Invoke, this is::
Bit| 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7
Octet | | | | | | | |
1 | 0 | 0 | 0 | 0 | 1 | 0 | 1 | 0
2 | TID
3 | TID
4 | 0 | 0 | 0 | 1 | 0 | 0 | 1 | 0
"""
#TODO: check GTR and TDR values (probably should rather be 11, for segmentation not supported)
pdu = [0x0a] # 0000 1010
pdu.extend(WTP._encodeTID(tid))
pdu.append(0x12) # 0001 0010
return pdu
@staticmethod
def encodeAckPDU(tid):
""" Builds a WTP Ack PDU (acknowledge)
@param tid: The transaction ID for this PDU
@type tid: int
@return: the WTP invoke PDU as a sequence of bytes
@rtype: list
The WTP PDU structure is defined in WAP-224, section 8
The ACK PDU structure is described in WAP-224, section 8.3.3::
Bit| 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7
Octet | | | | | | | |
1 |CON|PDU Type = Acknowledgement|Tve/Tok|RES|RID
2 TID
3
...where bit 0 is the most significant bit.
Thus, for our ACK, this is::
Bit| 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7
Octet | | | | | | | |
1 | 0 | 0 | 0 | 1 | 1 | 0 | 0 | 0
| PDU type = 0x03 = 0011 |
2 TID
3 TID
"""
pdu = [0x18] # binary: 00011000
pdu.extend(WTP._encodeTID(tid))
return pdu
def _decodePDU(self, byteIter):
""" Reads and decodes a WTP PDU from the sequence of bytes starting at
the byte pointed to by C{dataIter.next()}.
@param byteIter: an iterator over a sequence of bytes
@type byteIteror: mms.iterator.PreviewIterator
@note: If the PDU type is correctly determined, byteIter will be
modified in order to read past the amount of bytes required
by the PDU type.
@return: The PDU type, and the transaction ID, in the format:
(str:<pdu_type>, int:<transaction_id>)
@rtype: tuple
"""
byte = byteIter.preview()
byteIter.resetPreview()
# Get the PDU type
pduType = (byte >> 3) & 0x0f
pduValue = (None, None)
if pduType not in WTP.pduTypes:
#TODO: maybe raise some error or something
print 'Error - unknown WTP PDU type: %s' % hex(pduType)
else:
print '<< WTP: %s' % WTP.pduTypes[pduType],
try:
exec 'pduValue = self._decode%sPDU(byteIter)' % WTP.pduTypes[pduType]
except:
print 'A fatal error occurred, probably due to an unimplemented feature.\n'
raise
# after this follows the WSP pdu(s)....
return pduValue
def _decodeResultPDU(self, byteIter):
""" Decodes a WTP Result PDU
@param byteIter: an iterator over a sequence of bytes
@type byteIteror: mms.iterator.PreviewIterator
The WTP Result PDU structure is defined in WAP-224, section 8.3.2::
Bit| 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7
Octet | | | | | | | |
1 |CON| PDU Type = Result |Tve/Tok|RES|RID
2 TID
3
The WTP Result PDU Type is 0x02, according to WAP-224, table 11
"""
# Read in 3 bytes
bytes = []
for i in range(3):
bytes.append(byteIter.next())
pduType = (bytes[0] >> 3) & 0x0f
# Get the transaction ID
transactionID = WTP._decodeTID(bytes[1:])
print 'transaction ID: %d' % transactionID
if transactionID in self.activeTransactions:
self.activeTransactions.remove(transactionID)
return (WTP.pduTypes[pduType], transactionID)
def _decodeAckPDU(self, byteIter):
""" Decodes a WTP Result PDU
@param byteIter: an iterator over a sequence of bytes
@type byteIteror: mms.iterator.PreviewIterator
The ACK PDU structure is described in WAP-224, section 8.3.3::
Bit| 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7
Octet | | | | | | | |
1 |CON|PDU Type = Acknowledgement|Tve/Tok|RES|RID
2 TID
3
The WTP Result PDU Type is 0x03, according to WAP-224, table 11
"""
# Read in 3 bytes
bytes = []
for i in range(3):
bytes.append(byteIter.next())
pduType = (bytes[0] >> 3) & 0x0f
# Get the transaction ID
transactionID = WTP._decodeTID(bytes[1:])
print 'transaction ID: %d' % transactionID
if transactionID not in self.activeTransactions:
self.activeTransactions.append(transactionID)
return (WTP.pduTypes[pduType], transactionID)
def _decodeAbortPDU(self, byteIter):
""" Decodes a WTP Abort PDU
@param byteIter: an iterator over a sequence of bytes
@type byteIteror: mms.iterator.PreviewIterator
The WTP Result PDU structure is defined in WAP-224, section 8.3.2::
Bit| 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7
Octet | | | | | | | |
1 |CON| PDU Type = Result | Abort type
2 TID
3
4 Abort reason
The WTP Abort PDU Type is 0x04, according to WAP-224, table 11
"""
# Read in 4 bytes
bytes = []
for i in range(4):
bytes.append(byteIter.next())
pduType = (bytes[0] >> 3) & 0x0f
abortType = bytes[0] & 0x07
abortReason = bytes[3]
if abortType in self.abortTypes:
abortType = self.abortTypes[abortType]
else:
abortType = str(abortType)
if abortReason in self.abortReasons:
abortReason = self.abortReasons[abortReason]
else:
abortReason = str(abortReason)
# Get the transaction ID
transactionID = WTP._decodeTID(bytes[1:3])
print 'transaction ID: %d' % transactionID
if transactionID in self.activeTransactions:
self.activeTransactions.remove(transactionID)
print 'WTP: Abort, type: %s, reason: %s' % (abortType, abortReason)
return (WTP.pduTypes[pduType], transactionID)
@staticmethod
def _encodeTID(transactionID):
""" Encodes the specified transaction ID into the format used in
WTP PDUs (makes sure it spans 2 bytes)
From WAP-224, section 7.8.1: The TID is 16-bits but the high order bit
is used to indicate the direction. This means that the TID space is
2**15. The TID is an unsigned integer.
@param transactionID: The transaction ID to encode
@type transactionID: int
@return: The encoded transaction ID as a sequence of bytes
@rtype: list
"""
if transactionID > 0x7FFF:
raise ValueError, 'Transaction ID too large (must fit into 15 bits): %d' % transactionID
else:
encodedTID = [transactionID & 0xFF]
encodedTID.insert(0, transactionID >> 8)
return encodedTID
@staticmethod
def _decodeTID(bytes):
""" Decodes the transaction ID contained in <bytes>
From WAP-224, section 7.8.1: The TID is 16-bits but the high order bit
is used to indicate the direction. This means that the TID space is
2**15. The TID is an unsigned integer.
@param bytes: The byte sequence containing the transaction ID
@type bytes: list
@return: The decoded transaction ID
@rtype: int
"""
tid = bytes[0] << 8
tid |= bytes[1]
# make unsigned
tid &= 0x7f
return tid
class WSP:
""" This class implements a very limited subset of the WSP layer.
It uses python-mms's WSP PDU encoding module for almost all encodings,
and essentially just glues it together into a limited WSP layer. """
def __init__(self, wapGatewayHost, wapGatewayPort=9201):
self.serverSessionID = -1
self.capabilities = {'ClientSDUSize': 261120,
'ServerSDUSize': 261120}
self.headers = [('User-Agent', 'MobilEd MMS Channel'),
('Accept', 'text/plain'),
('Accept', 'application/vnd.wap.mms-message')]
self.wtp = WTP(wapGatewayHost, wapGatewayPort)
def connect(self):
""" Sends a WSP Connect message to the gateway, including any
configured capabilities. It also updates the WSP object to reflect
the status of the WSP connection """
print '>> WSP: Connect'
response = self.wtp.invoke(self.encodeConnectPDU())
self._decodePDU(response)
def disconnect(self):
""" Sends a WSP Connect message to the gateway, including any
configured capabilities. It also updates the WSP object to reflect
the status of the WSP connection """
print '>> WSP: Disconnect'
self.wtp.invoke(self.encodeDisconnectPDU(self.serverSessionID))
self.serverSessionID = -1
def post(self, uri, contentType, data):
""" Performs a WSP POST """
if type(data) == array.array:
data = data.tolist()
print '>> WSP: Post'
pdu = self.encodePostPDU(uri, contentType) + data
response = self.wtp.invoke(pdu)
self._decodePDU(response)
def get(self, uri):
""" Performs a WSP GET """
response = self.wtp.invoke(self.encodeGetPDU(uri))
self._decodePDU(response)
def encodeConnectPDU(self):
""" Sends a WSP connect request (S-Connect.req, i.e. Connect PDU) to
the WAP gateway
This PDU is described in WAP-230, section 8.2.2, and is sent to
initiate the creation of a WSP session. Its field structure::
Field Name Type Description
=============== ================= =================
Version uint8 WSP protocol version
CapabilitiesLen uintvar Length of the Capabilities field
HeadersLen uintvar Length of the Headers field
Capabilities <CapabilitiesLen>
octets S-Connect.req::Requested Capabilities
Headers <HeadersLen>
octets S-Connect.req::Client Headers
"""
pdu = []
pdu.append(0x01) # Type: "Connect"
# Version field - we are using version 1.0
pdu.extend(Encoder.encodeVersionValue('1.0'))
# Add capabilities
capabilities = []
for capability in self.capabilities:
# Unimplemented/broken capabilities are not added
try:
exec 'capabilities.extend(WSP._encodeCapabilty%s(self.capabilities[capability]))' % capability
except:
pass
# Add and encode headers
headers = array.array('B')
for hdr, hdrValue in self.headers:
headers.extend(Encoder.encodeHeader(hdr, hdrValue))
# Add capabilities and headers to PDU (including their lengths)
pdu.extend(Encoder.encodeUintvar(len(capabilities)))
pdu.extend(Encoder.encodeUintvar(len(headers)))
pdu.extend(capabilities)
pdu.extend(headers)
return pdu
@staticmethod
def encodePostPDU(uri, contentType):
""" Builds a WSP POST PDU
@note: This method does not add the <Data> part at the end of the PDU;
this should be appended manually to the result of this method.
The WSP Post PDU is defined in WAP-230, section 8.2.3.2::
Table 10. Post Fields
Name Type Source
========== ======================== ========================================
UriLen uintvar Length of the URI field
HeadersLen uintvar Length of the ContentType and Headers fields
combined
Uri UriLen octets S-MethodInvoke.req::Request URI or
S-Unit-MethodInvoke.req::Request URI
ContentType multiple octets S-MethodInvoke.req::Request Headers or
S-Unit-MethodInvoke.req::Request Headers
Headers (HeadersLen - length of S-MethodInvoke.req::Request Headers or
ContentType) octets S-Unit-MethodInvoke.req::Request Headers
Data multiple octets S-MethodInvoke.req::Request Body or
S-Unit-MethodInvoke.req::Request Body
"""
#TODO: remove this, or make it dynamic or something:
headers = [('Accept', 'application/vnd.wap.mms-message')]
pdu = [0x60] # Type: "Post"
# UriLen:
pdu.extend(Encoder.encodeUintvar(len(uri)))
# HeadersLen:
encodedContentType = Encoder.encodeContentTypeValue(contentType, {})
encodedHeaders = []
for hdr, hdrValue in headers:
encodedHeaders.extend(Encoder.encodeHeader(hdr, hdrValue))
headersLen = len(encodedContentType) + len(encodedHeaders)
pdu.extend(Encoder.encodeUintvar(headersLen))
# URI - this should NOT be null-terminated (according to WAP-230 section 8.2.3.2)
for char in uri:
pdu.append(ord(char))
# Content-Type:
pdu.extend(encodedContentType)
# Headers:
pdu.extend(encodedHeaders)
return pdu
@staticmethod
def encodeGetPDU(uri):
""" Builds a WSP GET PDU
The WSP Get PDU is defined in WAP-230, section 8.2.3.1::
Name Type Source
====== ============ =======================
URILen uintvar Length of the URI field
URI URILen octets S-MethodInvoke.req::Request URI or
S-Unit-MethodInvoke.req::Request URI
Headers multiple S-MethodInvoke.req::Request Headers or
octets S-Unit-MethodInvoke.req::Request Headers
"""
# UriLen:
pdu.extend(Encoder.encodeUintvar(len(uri)))
# URI - this should NOT be null-terminated (according to WAP-230 section 8.2.3.1)
for char in uri:
pdu.append(ord(char))
headers = []
#TODO: not sure if these should go here...
for hdr, hdrValue in self.headers:
headers.extend(Encoder.encodeHeader(hdr, hdrValue))
pdu.extend(headers)
return pdu
@staticmethod
def encodeDisconnectPDU(serverSessionID):
""" Builds a WSP Disconnect PDU
The Disconnect PDU is sent to terminate a session. It structure is
defined in WAP-230, section 8.2.2.4::
Name Type Source
=============== ======= ===================
ServerSessionId uintvar Session_ID variable
"""
pdu = [0x05] # Type: "Disconnect"
pdu.extend(Encoder.encodeUintvar(serverSessionID))
return pdu
def _decodePDU(self, byteIter):
""" Reads and decodes a WSP PDU from the sequence of bytes starting at
the byte pointed to by C{dataIter.next()}.
@param byteIter: an iterator over a sequence of bytes
@type byteIteror: mms.iterator.PreviewIterator
@note: If the PDU type is correctly determined, byteIter will be
modified in order to read past the amount of bytes required
by the PDU type.
"""
pduType = Decoder.decodeUint8(byteIter)
if pduType not in WSPEncodingAssignments.wspPDUTypes:
#TODO: maybe raise some error or something
print 'Error - unknown WSP PDU type: %s' % hex(pduType)
raise TypeError
pduType = WSPEncodingAssignments.wspPDUTypes[pduType]
print '<< WSP: %s' % pduType
pduValue = None
try:
exec 'pduValue = self._decode%sPDU(byteIter)' % pduType
except:
print 'A fatal error occurred, probably due to an unimplemented feature.\n'
raise
return pduValue
def _decodeConnectReplyPDU(self, byteIter):
""" The WSP ConnectReply PDU is sent in response to a S-Connect.req
PDU. It is defined in WAP-230, section 8.2.2.2.
All WSP PDU headers start with a type (uint8) byte (we do not
implement connectionless WSP, thus we don't prepend TIDs to the WSP
header). The WSP PDU types are specified in WAP-230, table 34.
ConnectReply PDU Fields::
Name Type Source
=============== ================= =====================================
ServerSessionId Uintvar Session_ID variable
CapabilitiesLen Uintvar Length of Capabilities field
HeadersLen Uintvar Length of the Headers field
Capabilities <CapabilitiesLen> S-Connect.res::Negotiated Capabilities
octets
Headers <HeadersLen> S-Connect.res::Server Headers
octets
@param byteIters: an iterator over the sequence of bytes containing
the ConnectReply PDU
@type bytes: mms.iterator.PreviewIterator
"""
self.serverSessionID = Decoder.decodeUintvar(byteIter)
capabilitiesLen = Decoder.decodeUintvar(byteIter)
headersLen = Decoder.decodeUintvar(byteIter)
# Stub to decode capabilities (currently we ignore these)
cFieldBytes = []
for i in range(capabilitiesLen):
cFieldBytes.append(byteIter.next())
cIter = PreviewIterator(cFieldBytes)
# Stub to decode headers (currently we ignore these)
hdrFieldBytes = []
for i in range(headersLen):
hdrFieldBytes.append(byteIter.next())
hdrIter = PreviewIterator(hdrFieldBytes)
def _decodeReplyPDU(self, byteIter):
""" The WSP Reply PDU is the generic response PDU used to return
information from the server in response to a request. It is defined in
WAP-230, section 8.2.3.3.
All WSP PDU headers start with a type (uint8) byte (we do not
implement connectionless WSP, thus we don't prepend TIDs to the WSP
header). The WSP PDU types are specified in WAP-230, table 34.
Reply PDU Fields::
Name Type
=============== =================
Status Uint8
HeadersLen Uintvar
ContentType multiple octects
Headers <HeadersLen> - len(ContentType) octets
Data multiple octects
@param byteIters: an iterator over the sequence of bytes containing
the ConnectReply PDU
@type bytes: mms.iterator.PreviewIterator
"""
status = Decoder.decodeUint8(byteIter)
headersLen = Decoder.decodeUintvar(byteIter)
# Stub to decode headers (currently we ignore these)
hdrFieldBytes = []
for i in range(headersLen):
hdrFieldBytes.append(byteIter.next())
hdrIter = PreviewIterator(hdrFieldBytes)
contentType, parameters = Decoder.decodeContentTypeValue(hdrIter)
while True:
try:
hdr, value = Decoder.decodeHeader(hdrIter)
except StopIteration:
break
# Read the data
data = []
while True:
try:
data.append(byteIter.next())
except StopIteration:
break
@staticmethod
def _encodeCapabiltyClientSDUSize(size):
""" Encodes the Client-SDU-Size capability (Client Service Data Unit);
described in WAP-230, section 8.3.2.1
This defines the maximum size (in octets) of WTP Service Data Units
@param size: The requested SDU size to negotiate (in octets)
@type size: int
"""
identifier = Encoder.encodeShortInteger(0x00)
parameters = Encoder.encodeUintvar(size)
length = Encoder.encodeUintvar(len(identifier) + len(parameters))
capability = length
capability.extend(identifier)
capability.extend(parameters)
return capability
@staticmethod
def _encodeCapabilityServerSDUSize(size):
""" Encodes the Client-SDU-Size capability (Server Service Data Unit);
described in WAP-230, section 8.3.2.1.
This defines the maximum size (in octets) of WTP Service Data Units
@param size: The requested SDU size to negotiate (in octets)
@type size: int
"""
identifier = Encoder.encodeShortInteger(0x01)
parameters = Encoder.encodeUintvar(size)
length = Encoder.encodeUintvar(len(identifier) + len(parameters))
capability = length
capability.extend(identifier)
capability.extend(parameters)
return capability
def createMMSMessage(destinationNumber):
""" This function does the same as the the "encode.py" example script,
except that it does not save the compiled MMS message to disk """
slide1 = MMSMessagePage()
slide1.addImage('content/pymms.jpg')
slide1.addText('This is the first slide, with a static image and some text.')
slide2 = MMSMessagePage()
slide2.setDuration(4500)
slide2.addImage('content/smile.jpg', 1500)
slide2.addText('This second slide has some timing effects.', 500, 3500)
slide3 = MMSMessagePage()
slide3.addText('And now, a text-only slide. The end.')
message = MMSMessage()
message.headers['Subject'] = 'Test MMS'
message.addPage(slide1)
message.addPage(slide2)
message.addPage(slide3)
message.toFile('dump.mms')
return message
if __name__ == '__main__':
if len(sys.argv) < 2:
print 'Usage: %s DESTINATION_NUMBER [MMS_MESSAGE_FILE [WAP_GATEWAY_HOST] [MMSC_ADDRESS]]' % sys.argv[0]
print '\nMMSC_ADDRESS can include the port number and path.\nFor example: %s 0821231234 sample.mms wap.operator.org mmsc.operator.net:1981' % sys.argv[0]
sys.exit(1)
# Our default settings is to use a Kannel WAP gateway running on localhost,
# and a Mbuni MMSC (mmsproxy), running on localhost with mms-port = 1981
destNumber = sys.argv[1]
if len(sys.argv) >= 3:
mmsFile = sys.argv[2]
else:
mmsFile = None
if len(sys.argv) >= 4:
wapGatewayHost = sys.argv[3]
else:
wapGatewayHost = 'localhost'
if len(sys.argv) >= 5:
mmscAddress = sys.argv[4]
else:
mmscAddress = 'localhost:1981'
# Load/create the actual MMS message
if mmsFile:
msg = MMSMessage.fromFile(mmsFile)
else:
msg = createMMSMessage(destNumber)
# Add the destination
msg.headers['To'] = str(destNumber) + '/TYPE=PLMN'
# ...and modify the message type for sending (in case it wasn't already)
msg.headers['Message-Type'] = 'm-send-req'
# Create a WSP connection to a WAP gateway (e.g. Kannel)
wsp = WSP(wapGatewayHost)
wsp.connect()
# Send the MMS data to the WAP gateway
wsp.post('http://%s' % mmscAddress, 'application/vnd.wap.mms-message', msg.encode())
wsp.disconnect()