Section 9.2. Example Service: Weather Updates

Using Email to SMS Gateways > Example Service: Weather Updates

9.2. Example Service: Weather Updates

To show how we might use an email to SMS gateway, let's build an application that will automatically send users the weather forecast for their zip code once per day.

To register for our service, users will browse to a web page where they will provide their phone number, zip code, and the name of the carrier.

Shortly thereafter, they'll receive a text message on their mobile phone from our service that looks something like this:

info@myweathertxt.com /Your code is: 7f6bc210

We provide this code to ensure that the user isn't signing up someone else for our service. They will then enter that code onto our web page to confirm their registration.

Thereafter, once a day, at a time of our choosing (let's say 7:00 p.m. PST), the user will receive a weather forecast on her phone for the next day that will look like:

info@myweathertxt.com /Weather for Seattle, WA on 31 May 2007 will be Partly Cloudy with High/Low of 78/53

We'll also add code to allow users to stop receiving forecasts. All they have to do is reply to a forecast with the word stop.

9.2.1. How to: Weather updates using email to SMS

Here's what you'll need to get started:

  • SMTP and IMAP access: To implement our example, you'll need a mail account with an SMTP server and an IMAP server that you can access. Many free services such as Gmail and Hotmail do not typically provide both SMTP and IMAP access. Most hosting providers do. Some free email services are starting to add IMAP support, so it may be worth checking to see if your favorite one does.Note, however, that a similar solution with POP and SMTP is of course also possible.

  • Web server: A web server that can execute Python CGI scripts (exactly the same as in the aggregator example). This example has quite a few files, which may make it seem a bit intimidating. Rest assured they are all quite small. Place these in the root path of your web server:

    • config.py

    • createDb.py

    • index.html

    • challenge.py

    • smsEmailSender.py

    • register_new_phone.py

    • verify_phone.py

    • get_forecast.py

    • do_forecasts.py

    • smsEmailServer.py

    • responseServer.py

  • SQLite v2 or greater: If you already have Python installed, you most likely also have SQLite. SQLite is a fast and easy to use single user SQL database. You can learn more about it here: http://www.sqlite.org/.

  • Scheduled tasks: Your web server also needs to have the ability to run a process at scheduled intervals. This is accomplished using cron on Unix and the Windows Task Scheduler on Windows.

9.2.1.1. Static configuration data

The config.py file will hold configuration information for our service, such as the user name and password we use to access our SMTP server. It will also store the name of the database file with our user list in it.

Create a file called config.py with the following content and place it in your web server's root directory.

config.py: #CHANGE THIS db_file = "database.db" # the name of the file user data is stored in server = "mail.myhost.com" # your SMTP server's URL account = "myaccount" # the user name for accessing your SMTP server password = "mypasswd" # the password for your SMTP server email_address = "myemail@mydomain.net" # the return address for outgoing email     

9.2.1.2. Setting up the database

We're going to use SQLite to store our user's data in a SQL database. Before we can use the database we'll need to set up the tables for our application. CreateDb.py will take care of this for us.

createDb.py:

#!/usr/bin/python import sys import config from pysqlite2 import dbapi2 as sqlite tables = ["users"] table_descs = [   """        CREATE TABLE users(               phonenumber VARCHAR(10),               provider VARCHAR(30),               zipcode VARCHAR(5),               challenge VARCHAR(8),               verified BOOLEAN DEFAULT FALSE,               PRIMARY KEY(phonenumber)        );        """, ] def main():        con = sqlite.connect(config.db_file)        con = con.cursor()        #sqlite2 doesn't support IF EXISTS        for t in tables:               try:                      con.execute("DROP TABLE %s" % t)               except:                      pass        for t in table_descs:               print t               con.execute(t) if __name__ == '__main__':        main()     

A complete explanation of SQL and how it works is outside the scope of this short cut. Our example should be pretty easy to understand, though. This script is set up in such a way as to allow many tables to be created, though for this example we'll only be making one.

We store the SQL command to create the users table in the table_descs list. It's a simple table with a few strings, including the user's phone number, provider, and zip code. The verified field lets us know whether or not the user has validated that they actually have the phone with the phone number they registered (recall the validation step from the example). We set phonenumber to be the primary key because we know it will be unique, and lookups will often be done by phone number.

Once you upload this script to your server, you'll want to run it (just like you run other python scripts). The rest of the code connects to a SQLite database with the name we set up in config.py and then executes our CREATE TABLE command.

9.2.1.3. The registration service

It's important that any service that pushes SMSs to users allows its users to sign-up and remove themselves from that service. Our service will let the user sign-up by providing his phone number, provider, and zip code.

index.html: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"> <html> <head>        <title>Weather Updates</title> </head> <body> <p>First, register your phone</p> <p>Then confirm it by typing the confirmation code into the form</p> <p>You'll receive a weather forecast for the next day, every day</p> <hr/>        <h3> Register </h3>        <form action="register_new_phone.py" method="get">               <p>Phone Number (10 Digit US only): <input type="text" name="phonenumber" size="10"/></p>               <p>Zip: <input type="text" name="zip" size="5"/></p>               <p>Provider:               <select name="provider">                      <option value="tmobile">T-Mobile</option>                      <option value="cingular">Cingular/AT&T</option>                      <option value="verizon">Verizon</option>                      <option value="sprint">Sprint</option>                      <option value="nextel">Nextel</option>               </select>               </p>               <p><input type="submit" value="Register"/></p>        </form> <hr/>        <h3> Verify </h3>        <form action="verify_phone.py" method="get">               <p>Phone Number: <input type="text" name="phonenumber" size="10"/></p>               <p>Code: <input type="text" name="code" size="8"/></p>               <p><input type="submit" value="Verify"/></p>        </form> <hr/> </body> </html>     

There are two forms on this page. The first one allows initial registration, which will send a code to the entered phone number. The second one allows the user to verify that he owns the phone he entered by entering the secret code and his phone number.

You'll notice that the form action for the first form is register_new_phone.py and the action for the second is verify_phone.py. Registration requires that we perform two additional tasks that we haven't covered yet:

We'll cover these tasks first, and then tie them all together to register a phone.

9.2.1.4. Generating unique codes

There are a lot of mechanisms that could be used to generate unique codes. In order to be useful for SMS, codes need to be:

For our application, we're SMS'ing a code to the user's phone and having him enter it on a web page. You might think it should be easier to display a code to the user on the web page itself and have the user SMS it to you. However, this approach can't be used when using email to implement an SMS service because users can't typically can't initiate communication with an email service from SMS.

Challenge.py will create our verification code.

challenge.py: import sys import random import sha CHALLENGE_LENGTH = 8 def generate_challenge():        nonce = str(random.random())        challenge = sha.sha(nonce).hexdigest()[0:CHALLENGE_LENGTH]        return challenge

First, we import random and sha. The challenge length is set to be 8 characters.

One function, generate_challenge is defined. The function takes the user's phone number as an input. First, it generates a random number and converts it to a string. Then it uses the sha algorithm to generate a cryptographic hash of the random number, gets the result in hex, and truncates the hash to the first 8 characters.

There are a many mechanisms for generating user readable characters from the random number (including just passing the random number to the user). We use a cryptographic hash to help protect against the compromise of a weak random number generator.

The challenge is returned to the caller after its generated.

9.2.1.5. Sending an SMS via email

This is the part most of you have probably been waiting for. We're going to connect to our SMTP server and send SMSs using the carriers' SMTP gateways.

smsEmailSender.py: import sys import smtplib emails = {        "tmobile" : "tmomail.net",        "cingular" : "mobile.mycingular.net",        "verizon" : "vtext.com",        "sprint" : "messaging.sprintpcs.com",        "nextel" : 'messaging.nextel.com', } class SmsEmailSender(object):        def __init__(self, server, account, password, from_addr):               self.__smtp = smtplib.SMTP()               self.__account = account               self.__password = password               self.__from_address = from_addr               self.__server = server        def sendsms(self, phone_number, provider, body, from_addr = None):               if not from_addr:                      from_addr = self.__from_address               self.__smtp = smtplib.SMTP()               self.__smtp.connect(self.__server)               self.__smtp.starttls()               self.__smtp.login(self.__account, self.__password)               self.__smtp.sendmail(                      from_addr,                      "%s@%s" % (phone_number, emails[provider]),                      "%s" % (body)               )     

First, we define a dictionary that maps user-friendly provider names to the mail address suffixes for those providers. A nearly complete database of these mappings is available through the CoreSMS project (http://www.corelevel.com/sms/).

SmsEmailSender is defined as a class to allow easier reuse. The SMTP connection could also be reused, but in our class we recreate it each time we call sendmail, as the error handling for the other case is more complicated.

SmsEmailSender takes four parameters in its constructor:

We could use these values from our configuration and not require them to be passed in. SmsEmailSender is designed to be independent of configuration mechanisms and parameters so that it can be reused in other projects.

Our class has only one function, sendsms. The function takes five parameters:

Next we connect to the server and log in. Calling starttls before sending your password ensures that it will be transmitted encrypted over the wire, instead of in plaintext (the default for SMTP).

The actual SMTP sendmail function simply requires you to pass it the address that the email will be from, the address you are sending the mail to, and the entire body of the message.

9.2.2. Processing a registration

Our form submits a GET request to a CGI script register_new_phone.py, passing parameters describing the user. The script generates a unique code.

register_new_phone.py:

#!/usr/bin/python import sys import re import cgi import cgitb; cgitb.enable() import smsEmailSender import challenge import config from pysqlite2 import dbapi2 as sqlite def result(text):        print "Content-type: text/html\n\n"        print """        <html>               <head><title>Registration Code Sent</title></head>               <meta http-equiv="refresh" content="5; URL=/">               <body>                      %s               </body>        </html>        """ % text        sys.exit(0) form = cgi.FieldStorage() phonenumber = form.getfirst("phonenumber") zipcode = form.getfirst("zip") provider = form.getfirst("provider") if not phonenumber or len(phonenumber) > 10:        result("Please enter a valid 10 digit phone number") phonenumber = phonenumber[0:10] if not re.match("[0-9]{10}", phonenumber):        result("Please enter a valid 10 digit phone number") if not zipcode or len(zipcode) > 5 or not re.match("[0-9]{5}", zipcode):        result("Please enter a valid 5 digit zipcode") if not provider in smsEmailSender.emails:        result("Your cell phone provider is not yet supported.") con = sqlite.connect(config.db_file) cur = con.cursor() serv = smsEmailSender.SmsEmailSender(        config.server,        config.account,        config.password,        config.email_address ) cur.execute("SELECT * FROM users WHERE phonenumber = ?", [phonenumber]) res = cur.fetchone() if res:        if res[4]:               result("This phone has already been registered and verified")        serv.sendsms(res[0], res[1], "Your code is: %s" % res[3])        result("That number has already been registered, resending code") else:        challenge = challenge.generate_challenge()        cur.execute("INSERT INTO users VALUES(?,?,?,?,?)", [phonenumber, provider, zipcode, challenge, False])        cur.close()        con.commit()        serv.sendsms(phonenumber, provider, "Your code is: %s" % challenge)        result(        """        <p> The registration code has been sent to your phone. </p>        <p> Please return to the home page and enter it to verify your phone number. </p>        <p> Redirecting in 5 seconds...</p>        """)     

There's quite a bit of code here. The first part of this script is identical to the one described in the aggregator chapter. We import needed modules, define our function to display results to the user, and we grab our three parameters from the query (phonennumber, zip,provider).

The next three blocks of conditionals validates that all the input parameters are like we expect them to be:

If any of these conditions aren't met, we call result and return an informative error message to the user.

If everything checks out, we go ahead and open a connection to our database and get a cursor object. Cursor objects are transactions in Python's database API.

We instantiate a new SmsEmailSender object because we'll be using it in a few lines.

If the user is already registered, we want to let them know that and not reregister him, so we use our database connection to look for a user with the phone number that was passed onto us.

If we get a result, we do one of two things:

If the user isn't in the database, we generate a new challenge for him, and insert a new record into the database. It's important to call commit on cursor objects if they change a database's state, otherwise the changes won't have any effect.

Now we send the user his registration code via the sendsms method we wrote earlier. We call result and inform him that the code has been sent to his phone. As a courtesy to the user, our result HTML has a meta-refresh directive in it that will send him back to the home page in five seconds.

9.2.2.1. Verifying users

Once the user receives his registration code, he'll enter it into the form on the front page.

verify_phone.py:

#!/usr/bin/python import sys import re import cgi import cgitb; cgitb.enable() import config from pysqlite2 import dbapi2 as sqlite def result(text):        print "Content-type: text/html\n\n"        print """        <html>               <head><title>Verified</title></head>               <meta http-equiv="refresh" content="5; URL=/">               <body>                      %s               </body>        </html>        """ % text        sys.exit(0) form = cgi.FieldStorage() phonenumber = form.getfirst("phonenumber") code = form.getfirst("code") if not phonenumber or len(phonenumber) > 10:        result("Please enter a valid 10 digit phone number") phonenumber = phonenumber[0:10] if not re.match("[0-9]{10}", phonenumber):        result("Please enter a valid 10 digit phone number") if not code or len(code) != 8:        result("Invalid Code") con = sqlite.connect(config.db_file) cur = con.cursor() cur.execute("SELECT * FROM users WHERE phonenumber = ?", [phonenumber]) res = cur.fetchone() if res and code == res[3]:        cur.execute("UPDATE users SET verified = 'True' WHERE phonenumber = ?", [phonenumber])        con.commit()        result("<p> Success - Phone Registered </p>") else:        result("Invalid Code")     

Just as in the last CGI script, we import needed libraries, define our result function, and get our two parameters from the form: phonenumber and code. We then validate our parameters.

If our parameters are valid we connect to the database and look for a record with the phone number of the one that was passed in. If there is no record or the record doesn't have the same challenge code stored in it as the user passed to us, we error out.

If the code matches, we update our database by setting the verified field to True. A message indicating success is returned to the user.

9.2.2.2. Getting the Weather

Now that we have registered and verified users, we need to get forecasts for them. Luckily for us, Yahoo has a great RSS API for retrieving weather forecasts at (http://developer.yahoo.com/weather/).

Our code talks to the web service defined there and grabs tomorrow's forecast:

get_forecast.py:

import re import urllib from xml.dom.minidom import parse yweather_ns = "http://xml.weather.yahoo.com/ns/rss/1.0" def get_forecast(zipcode):        res = urllib.urlopen("http://weather.yahooapis.com/ forecastrss?p=%s&u=f"%zipcode)        dom = parse(res)        fdict = {}        #get the location        loc = dom.getElementsByTagNameNS(yweather_ns, "location")[0]        fdict["city"] = loc.getAttribute("city")        fdict["state"] = loc.getAttribute("region")        #get tomorrow's forecast        forecast = dom.getElementsByTagNameNS(yweather_ns,"forecast")[1]        fdict["date"] = forecast.getAttribute("date")        fdict["high"] = forecast.getAttribute("high")        fdict["low"] = forecast.getAttribute("low")        fdict["text"] = forecast.getAttribute("text")        return fdict if __name__ == "__main__":        get_forecast("98112")     

We use the python module minidom to handle XML processing. We define one function, get_forecast that takes the zip code for which to obtain forecast data.

The function first fetches the forecast (using urllib) and then uses the parse command of minidom to create a DOM.

Next, we look up the location element and grab the city and state of our forecast and put them into a dictionary.

There are two forecasts included with each request today's and tomorrow's. Our service gives people the next day's weather in the evening the day before, so we want to grab tomorrow's forecast.

We retrieve a list of all the forecast nodes and grab the second one (index '0' is the first forecast). That element is used to get the date of the forecast, the high and low temperatures, and a textual description of the expected conditions. All of these elements are added into a dictionary that is then returned to the caller.

9.2.2.3. Sending our forecasts

Now that we can get a forecast, and we know who wants forecasts for which locations, we need to write a script that will loop through our users, get their forecasts, and send them.

do_forecasts.py:

#!/usr/bin/python import sys import re import smsEmailSender import config from get_forecast import * from pysqlite2 import dbapi2 as sqlite con = sqlite.connect(config.db_file) serv = smsEmailSender.SmsEmailSender(        config.server,        config.account,        config.password,        config.email_address ) cur = con.cursor() users = cur.execute("SELECT phonenumber, provider, zipcode FROM users WHERE verified='True'") for u in users.fetchall():        fd = get_forecast(u[2])        forecast = "Weather for %(city)s, %(state)s on %(date)s will be %(text)s with High/Low of %(high)s/%(low)s" % fd        serv.sendsms(u[0], u[1], "Daily Forecast", forecast)     

The first thing we do here is import the modules we need, including our own smsEmailSender, config, and get_forecast modules.

Next we connect to our database and instantiate an SmsEmailSender. A cursor object is created and used to find all the users whose verified flag is set to True.

The fetchall command is used to retrieve a list of users that we will iterate over. For each user, we get a forecast for his zip code. The zip code is stored in the third element (index 2) of the returned row, because that's how we asked for it to be returned in our SELECT statement.

Next we use Python's string formatting to create a text string that looks like the one in the example at the start. The slightly different form of string formatting used here allows you to explicitly grab elements of a dictionary instead of relying on positioning alone.

After creating the message, we use our SmsEmailSenderinstance, calling its sendsms member with the phone number (element 0 of the returned row), the provider (element 1), a subject of "Daily Forecast," and the text of our forecast.

This script will loop through all of our users and SMS them tomorrow's forecast.

9.2.2.4. Scheduling

Now we need to get do_forecasts.py to run once a day. On Unix you'll type:

       crontab -e

This will open your crontab file in an editor (probably vi, but you can change your editor by setting the EDITOR shell variable) and make it look like this:

SHELL=/bin/bash PATH=/bin:/usr/bin:/usr/local/bin 00 19 * * * cd WEBDIR; python do_forecasts.py

Replace WEBDIR with the absolute path to the directory where all the files from this project are placed. This will cause your script to be run once a day at 7:00 p.m. (19:00). If you want it to run at a different time in order to test it, just change the "00 19" to be the 24-hour time, minute first, of when you want it to run.

If you're implementing this service on Windows, you can use the Windows Task Scheduler to implement something similar.

Wow. That was a decent bit of code, but we've implemented our first SMTP-based SMS service.

9.2.2.5. Receiving SMSs via email

While most carriers don't enable initiating a conversation with an email service from SMS, most do allow users to respond. This can be useful for your service. For example, you may wish to allow users to quit via SMS, or to ask for the forecast at some time other than that scheduled.

To demonstrate how we would go about receiving responses from SMS via email, we'll create code to allow the user to stop receiving forecasts by replying "stop." This is a little more complicated than the previous bits of code we've gone over, as it uses threading. It relies on our config.py and requires that the same account you use to send mail can be used to access an IMAP server.

First, we'll create a class that will poll for new messages and dispatch them to a callback when they are received.

smsEmailServer.py:

#!/usr/bin/python import sys import time import imaplib import threading import email import traceback class SmsEmailServer(object):        def __init__(self, server, account, password, on_email_func, polling_interval = 10):               self.__account = account               self.__password = password               self.__on_email = on_email_func               self.__polling_interval = polling_interval               self.__running = False               self.__server = server               self.__imap = imaplib.IMAP4_SSL(server)               self.__running = True               self.__poll_thread = threading.Thread(target = self.__poll_mail_server)               self.__poll_thread.start()        def shutdown(self):               if self.__running:                      self.__running = False                      self.__poll_thread.join()        def __poll_mail_server(self):               while (self.__running):                      try:                            self.__imap = imaplib.IMAP4_SSL(self.__server)                            self.__imap.login(self.__account, self.__password)                            self.__imap.select()                            typ, data = self.__imap.search(None, 'ALL')                            for num in data[0].split():                                   typ, data = self.__imap.fetch(num, '(RFC822)')                                   m = email.message_from_string(data[0][1])                                   self.__on_email(m)                                   self.__imap.store(num, "+FLAGS", "(\\Deleted)")                            self.__imap.expunge()                            time.sleep(self.__polling_interval)                      except:                            traceback.print_exc()               self.__imap.logout()     

We start by importing modules that we need, most notably: imaplib,threading, and email.

We then define a class called SmsEmailServer. It takes the standard server, account, and password parameters that SmsEmailSender used.

It takes a function as a parameter for on_email_func.This is a function that will be called whenever an email message is received (which is what SMS replies look like an email).

It also takes an optional polling_interval parameter, which determines how often it looks for new mail. The default poll interval is 10 seconds.

SmsEmailServer sets a bunch of variables that it will use inside its polling thread, launches the polling thread, and returns.

The polling thread runs the function __poll_mail_server. This function has a loop that will run forever unless shutdown is called from another thread (ResponseServer.py, in this example).

Inside the loop we connect to the IMAP server over SSL and login. The IMAP connection can be reused, but it makes the code more complicated, so it is not presented here.

The rest of the loop is esoteric IMAP specific code that you needn't worry about, except for the part that creates a new email object and calls on_email_func with results.

After that function is called our code flags the message for deletion to make sure we don't process it again.

Being able to receive emails alone doesn't help us interact with our users. We'll need to process them. This script creates a new SmsEmailServer to listen for incoming SMSs. When it gets one it checks to see if the first word is "stop." If it is, we'll see if the sender is one of our users and remove them from the system

response_server.py:

#!/usr/bin/python import sys import time import config from smsEmailServer import * from smsEmailSender import * from pysqlite2 import dbapi2 as sqlite def on_email(email):        contents = None        #some phones send mms' as responses        phonenumber = email["From"].split("@")[0].strip()        if email.is_multipart():               for p in email.get_payload():                      #we only support plain text...                      if p.get_content_type() == "text/plain":                            contents = p.get_payload()                            break        else:               contents = email.get_payload()        if contents[0:4].lower() == "stop":               print "Got stop from: %s" % phonenumber               con = sqlite.connect(config.db_file)               cur = con.cursor()               cur.execute("SELECT * FROM users WHERE phonenumber = ?", [phonenumber])               u = cur.fetchone()               if u:                      cur.execute("DELETE FROM users WHERE phonenumber = ?", [phonenumber])                      cur.close()                      con.commit()                      serv = SmsEmailSender(                            config.server,                            config.account,                            config.password,                            config.email_address                      )                      serv.sendsms(u[0], u[1], "You'll no longer receive forecasts from us.")                      print "User Removed"               else:                      print "No registered user with this phonenumber"        else:               print "Received Unknown Command: %s" % contents def main():        se = SmsEmailServer(config.server, config.account, config.password, on_email)        try:               while True:                      time.sleep(.5)        except:               print "\n Shutting Down..."               se.shutdown() if __name__ == '__main__':        main()     

The main function just creates a new SmsEmailServer and sits in a loop hitting Ctrl-C (or Ctrl-Break) will stop the process. When it creates the server it registers the on_email function as the handler for received email. SmsEmailServer will call our on_email function every time it gets a message.

The on_email function looks kind of complicated. The code at the start of our function grabs the From address out of the message. This is typically the phone number of the originating user. However, some phones, such as the T-Mobile Sidekick, don't behave this way. Instead, they send mail from an email address account associated with the phone. See the 'Response addresses' blurb at the bottom of this chapter for a method to deal with this.

To further complicate things, some phones send MMSs as responses to SMSs sent via email. This is a bit easier for us to deal with, however, because we still know who the message is from, it just happens to be in an unexpected format. MMS's are multipart MIME messages. So, after our code gets the From address, it checks to see if the message is multipart. If it is, it looks for the plain text portion and considers that the message content.

Next it looks to see if "stop" is the first word. If it isn't, we bail out and do nothing (add your own custom handling here; for example, you can allow users to look up the forecast for their city).

If it is "stop," we have a bit of work to do. First we connect to the database and look for a user with a phone number equal to the From address. If there's a match, we remove the entry from the database. Right after we remove the entry, we send a confirmation SMS to the user who asked to be removed.

If we can't find a user with a phone number equal to the From address, we simply do nothing.

In order to receive SMSs, you'll need to leave this script running on a server. The machine needs to be the same one that the web frontend is running on, because the database is stored in a file. Contact your hosting provider for information on how to set up a background process, as it varies from provider to provider.

NOTE

The address each SMS appears to be from is an important consideration when designing your service. The user will always see the From address at the start of every message, and any responses will be delivered to that address.

For example, if you send mail to 2065551212@tmomail.net from myservice@mydomain.com, your user will receive an SMS that looks something like:

myservice@mydomain.com / body of your message

Conversely, when your service receives a reply from an SMS user, you'll want to know who sent you the message.

The typical way to do this is to simply look at the address of the sender when checking mail for the service. Most emails will appear to be from something similar to 1112223333@carrier.net. You could strip the number of the front portion, verify that the phone number is a registered user, and respond as appropriate given the content of the request.

This approach works for most services, but it's less than ideal for a couple of reasons:

As an alternative, you may pick a unique From address for every outgoing message or conversation and store it in a table. In this sense, the unique address is exactly like a session key on a web site. This approach has several advantages:

 

 



How to Build an SMS Service
How to Build an SMS Service
ISBN: 789742233
EAN: N/A
Year: 2007
Pages: 52
BUY ON AMAZON

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net