12 Source code for the WPA4 list management system

contents

12.1 Purpose of the WPA4 list management system

This is a demonstration CGI application, which provides a fully working mailing list management facility. This is simple enough for student study, test, installation and modification, while being sufficiently robust and useful to demonstrate the use of Python language and coding techniques covered in the WPA4 module within a production CGI webserver environment accessible to the public over the Internet.

12.2 Use of CGI programs and Python modules

The Python modules described in the notes for weeks 9,10 and 11 are imported and used by these CGI programs. The cgiutils2.py module provides a variety of functions, e.g. for starting and ending HTML outputs, sending HTML forms, generating PINs and sending messages by mail. The tablcgi.py module provides a class with methods to search, load, save, display and update (i.e. add, modify, and delete rows) a simple database table.

12.3 Database tables and the table_details.py module

This application uses 2 database tables.

The prospects table stores details for prospective members. For each member the email address, Pin and time PIN was requested are stored. A prospect record is created when someone with an unknown email address requests a PIN. This record will either be used to enable a prospect to become a member, or will be timed out after 48 hours.

The members table stores details of list members. For each member the email address, PIN, student number, name and number of postings (initially 0) are stored. list members may post messages to all other members, view membership details and unsubscribe themselves.

table_details.py is a set of database table definitions for the members and prospects tables used by the above 3 CGI programs. These definitions are imported as one table definition object for each table (mtab and ptab). These definitions are used when constructing table objects using the tablcgi.table() class.

# table_details.py: details of tables used within WPA IV
# simple mailing list management system. These details are coded
# in this file and imported into programs which access
# members and prospects tables, enabling common details to
# edited in one place.

# trivial table definitions class used to simplify naming
class table_def:
  pass

# data for prospects table
ptab=table_def()
ptab.meta={
  "keys":["address","pin","time_requested"],
  "colheads":["Address","PIN","Request Time"],
  "formats":["%s","%s","%s"]
}
ptab.file="prospects.pkl"
ptab.uniq_col="address"

# data for members table
mtab=table_def()
mtab.meta={
  "keys":["address","pin","snum","name","posts"],
  "colheads":["Address","PIN","Student No.","Name","No. posts"],
  "formats":["%s","%s","%s","%s","%d"]}
mtab.file="members.pkl"
mtab.uniq_col="address"

12.4 entry.form

This form is used to provide the input required for entry.cgi .

<h2> WPA4 Mailing List Access Form</h2>
<p>
This mailing list is for TIC students taking Website Programming
Applications IV.  This list may also be activated or deactivated
at any time, to suit course-delivery requirements.  </p><p>
Messages sent using this list must be relevant to this module, and
will be emailed to all list members.<p> WPA IV students are
asked to join this list and post a message to say a few words
about themselves when they join. Messages sent will include
your name and student number. Anyone who joins this list but
who does not post anything within a reasonable time period of joining
risks being silently removed. Students are asked to leave this
list when they have finished studying the module.</p><p>

Please bear in mind that any information you enter or post here is
accessible to all list users, so this must not include any information
which you wish to keep
confidential. You may prefer to use a throw-away email address if
you do not want other list users to know your main one. Politeness to
other list users is expected.</p><p>

<b>If you are new to this list or have forgotten your PIN number,
use this form to enter your email address and request a PIN.
All other actions will require your email address and PIN.</b><p>

<form action="entry.cgi" method="post"> <p> Please enter your email address. *
<INPUT TYPE="text" NAME="Email" SIZE="25" MAXLENGTH="50">
<p> Please enter your PIN number
<INPUT TYPE="text" NAME="PIN" SIZE="4" MAXLENGTH="4">
<BR>If you don't have a PIN number or have forgotten it, this form
will send a PIN to your email address.<p> Select option:<br>
<input TYPE=Radio NAME="option" VALUE="add">add me to the list <br>
<input TYPE=Radio NAME="option" VALUE="del">remove me from the list<br>
<input TYPE=Radio NAME="option" VALUE="post">send a message to the list<br>
<input TYPE=Radio NAME="option" VALUE="view">view list member details<br>
<input TYPE=Radio NAME="option" VALUE="pin">send me a PIN<br>
<INPUT TYPE="submit" VALUE="Go"> </form>

12.5 entry.cgi

This program is the starting point for all operations. If run without input it will send the entry form to the browser. Other operations are either provided directly (unsubscribe and send pin) or are handled by sending the add form or the post form.

#!/usr/bin/python
# change top line to the path of the Python interpreter on your system

""" entry.cgi This file is the gateway program for a
    simple mailing list management system.
    Richard Kay last changed: 19 April 2002
"""

# uncomment next 3 lines to debug script
#import sys
#sys.stderr=sys.stdout
#print "Content-type: text/plain\n"

import cgiutils2
import tablcgi
from table_details import ptab,mtab

def send_pin(email):
  # need to access members table to check if user is a member
  members=tablcgi.table(mtab.meta,mtab.file,mtab.uniq_col)
  ismember=members.has_key(email)
  # need to access prospects table to check if user is a prospect
  prospects=tablcgi.table(ptab.meta,ptab.file,ptab.uniq_col)
  isprospect=prospects.has_key(email)
  # if already a member resend PIN
  if ismember:
    index=members.find(email)
    pin=members.data[index]["pin"]
  # if already a prospect resend PIN
  elif isprospect:
    index=prospects.find(email)
    pin=prospects.data[index]["pin"]
  else:
    # add record to prospects
    pin=cgiutils2.make_pin()
    import time
    time_requested=int(time.time())
    # open prospects table and add record
    prospects=tablcgi.table(ptab.meta,ptab.file,ptab.uniq_col)
    new_prospect={"address":email,"pin":pin,"time_requested":time_requested}
    if prospects.addrow(new_prospect):
      print "<P> prospective list member details recorded<P>"
    else:
      errormes="""Prospective member list not updated, probably
        due to high server demand. Please try again later
        and inform the webmaster if this problem persists."""
      cgiutils2.html_end(error=errormes)
      return

  # send PIN to address entered
  message="From: webmaster@copsewood.net\n"
  message+="To: %s\n" % email
  if not ismember:
    message+="Subject: Mailing list PIN request \n\n"
  else:
    message+="Subject: Mailing list PIN resend \n\n"
  message+="Someone (presumably you) entered your email address\n"
  message+="requesting access to the WPA IV mailing list.\n"
  message+="\n"
  message+="The PIN needed for access is: %s\n" % pin
  message+="\n"
  message+="To confirm membership please visit: "
  message+="http://copsewood.net/wpa4list/entry.cgi \n"
  message+="and select option: add me to the list.\n"
  message+="\n"
  message+="If this message is in error please ignore it. However,\n"
  message+="if this error persists please contact abuse@copsewood.net .\n"
  message+="\n"
  fromad="webmaster@copsewood.net"
  cgiutils2.send_mail(fromad,email,message)
  print "<p>Your PIN has been sent.</p>"
  cgiutils2.html_end(received=1)

def main():
  cgiutils2.html_header(title="WPA IV mailing list manager")

  if len(cgiutils2.keys()) == 0:
    # if no keys send the form to the browser
    cgiutils2.send_form("entry.form")
    cgiutils2.html_end(want_form=1)
    return
  elif not cgiutils2.has_required(["Email","option"]):
    cgiutils2.html_end(error="You havn't input all required values.")
    return

  # has required keys - process form
  email=cgiutils2.firstval("Email")
  if not cgiutils2.is_email(email):
    cgiutils2.html_end(error="Invalid email address.")
    return

  option=cgiutils2.firstval("option",default="none")
  if not cgiutils2.is_valid(option,allowed='^add$|^del$|^post$|^view$|^pin$'):
    cgiutils2.html_end(error="Invalid option radio button value.")
    return

  # check if a PIN was entered and validate it
  pinentry=cgiutils2.firstval("PIN",default="")
  if pinentry and not cgiutils2.is_valid(pinentry,allowed='^[1-9][0-9]{3,3}$'):
    cgiutils2.html_end(error="Invalid PIN format.")
    return
  if pinentry:
    # check its the correct PIN for this address
    members=tablcgi.table(mtab.meta,mtab.file,mtab.uniq_col)
    ismember=members.has_key(email)
    prospects=tablcgi.table(ptab.meta,ptab.file,ptab.uniq_col)
    isprospect=prospects.has_key(email)
    if isprospect:
      index=prospects.find(email)
      pin=prospects.data[index]["pin"]
    elif ismember:
      index=members.find(email)
      pin=members.data[index]["pin"]
    else: # (not ismember) and (not isprospect)
      cgiutils2.html_end(error="PIN entered but unknown email address.")
      return
    if int(pinentry) != int(pin):
      cgiutils2.html_end(error="Incorrect PIN entered.")
      return
  elif not option == "pin":
    # Without a valid pin other actions are prohibited
    cgiutils2.html_end(error="No PIN entered.")
    return

  # If we've got this far, there should either be a valid option and PIN
  # or we have a prospective member requesting a PIN.
  if option == "add":
    # send the add member form
    cgiutils2.send_form("add.form",[email,pin])
    cgiutils2.html_end(want_form=1)
  elif option == "del":
    # delete the user
    if members.delrow(email):
      cgiutils2.html_end(received=1)
    else:
      errormes="""Members list not updated, probably
        due to high server demand. Please try again later
        and inform the webmaster if this problem persists."""
      cgiutils2.html_end(error=errormes)
  elif option == "post":
    # send post message to list form
    cgiutils2.send_form("post.form",[email,pin])
    cgiutils2.html_end(want_form=1)
  elif option == "view":
    members.tab2html(skip_cols=["pin"],bgcolor='"#BBFFFF"')
    cgiutils2.html_end()
  elif option == "pin":
    # send member or prospect pin
    send_pin(email)
  else:
    # should have trapped this one earlier ???
    cgiutils2.html_end(error="Invalid option value (2nd trap).")
  return


if __name__ == "__main__":
  try:
    main()
  except:
    import traceback
    print "error detected in entry.cgi main()"
    traceback.print_exc()

12.6 add.form

This form provides the input required by add.cgi. It is sent with hidden field %s escapes changed to the values of address and PIN input by the entry.cgi user using the entry.form .

<p> Use this form to confirm TIC WPA IV list membership.<p>
<form action="add.cgi" method="post">
<INPUT TYPE="hidden" NAME="address" VALUE="%s" >
<INPUT TYPE="hidden" NAME="pin" VALUE="%s" >
Please enter your name. *
<INPUT TYPE="text" NAME="Name" SIZE="25" MAXLENGTH="40"><p>
Please enter your student number. *
<INPUT TYPE="text" NAME="Snum" SIZE="8" MAXLENGTH="8"> <p>
<INPUT TYPE="submit" VALUE="Join List"> </form>

12.7 add.cgi

This program is used to confirm list membership. It times out prospect records more than 48 hours old. It checks the PIN and address it is submitted with against the prospects table, and if all details are correct it removes the entry from the prospects table and adds an entry to the members table.

#!/usr/bin/python
# change top line to the path of the Python interpreter on your system

""" add.cgi This file is the program which handles member additions
    within a simple mailing list management system.
    Richard Kay last changed: 22 April 2002
"""

# uncomment next 3 lines to debug script
# import sys
# sys.stderr=sys.stdout
# print "Content-type: text/plain\n"

import cgiutils2
import tablcgi
from table_details import mtab,ptab # import table details

prospect_timeout=60*60*48 # seconds in 48 hours

def timeout_prospects():
  # times out prospect records for new PINs requested > 48 hours ago
  import time
  prospects=tablcgi.table(ptab.meta,ptab.file,ptab.uniq_col)
  time_now=int(time.time()) # time in seconds since 1/1/1970
  for row in prospects.data:
    if time_now - row["time_requested"] > prospect_timeout:
      # remove timed out prospect record
      prospects.delrow(row["address"])

def main():
  timeout_prospects()
  cgiutils2.html_header(title="WPA IV add list member program")

  if len(cgiutils2.keys()) == 0:
    # if no keys authentication data missing
    cgiutils2.html_end(error="Missing user identification details.")
    return
  elif not cgiutils2.has_required(["address","pin","Name","Snum"]):
    cgiutils2.html_end(error="You havn't input all required values.")
    return

  # has required keys - process form
  email=cgiutils2.firstval("address")
  if not cgiutils2.is_email(email):
    cgiutils2.html_end(error="Invalid email address.")
    return

  # validate PIN
  pinentry=cgiutils2.firstval("pin")
  if not cgiutils2.is_valid(pinentry,allowed='^[1-9][0-9]{3,3}$'):
    cgiutils2.html_end(error="Invalid PIN format.")
    return
  # check its the correct PIN for this address
  prospects=tablcgi.table(ptab.meta,ptab.file,ptab.uniq_col)
  isprospect=prospects.has_key(email)
  if not isprospect:
    cgiutils2.html_end(error="Can't add address not in prospectives.")
    return
  else:
    index=prospects.find(email)
    pin=prospects.data[index]["pin"]
    if int(pinentry) != int(pin):
      cgiutils2.html_end(error="Incorrect PIN entered.")
      return

  # If we've got this far, there should be a valid address and PIN
  # and user is a prospective member

  # clean up name
  name=cgiutils2.firstval("Name")
  name=cgiutils2.make_clean(name,r"[^\w\- ]")

  # validate student number
  snum=cgiutils2.firstval("Snum",default="none")
  if not cgiutils2.is_valid(snum,allowed='^[eE0-9][0-9]{7,7}$'):
    cgiutils2.html_end(error="Invalid student number.")
    return

  # add to members and remove from prospects
  new_member={"address":email,"pin":pin,"snum":snum,"name":name,"posts":0}
  members=tablcgi.table(mtab.meta,mtab.file,mtab.uniq_col)
  if members.addrow(new_member):
    # successfully added member, so try to remove prospect record. It
    # doesn't matter much if removing prospect record fails due to
    # file locking as old prospects are automatically removed later.
    prospects.delrow(email)
    cgiutils2.html_end(received=1)
  else:
    errormes="""Member list not updated, probably
    due to high server demand. Please try again later
    and inform the webmaster if this problem persists."""
    cgiutils2.html_end(error=errormes)
  return

if __name__ == "__main__":
  try:
    main()
  except:
    import traceback
    print "error detected in add.cgi main()"
    traceback.print_exc()

12.8 post.form

This form provides the input required by post.cgi. It is sent with hidden field %s escapes changed to the values of address and PIN input by the entry.cgi user using the entry.form .

<p> Use this form to send a message to all users of the WPAIV list.
This will include your name, address and student number.
<p> New users are asked to say a few words about themselves.<p>
<form action="post.cgi" method="post">
<INPUT TYPE="hidden" NAME="address" VALUE="%s" >
<INPUT TYPE="hidden" NAME="pin" VALUE="%s" >
<p>Enter subject of message: *<br>
<INPUT TYPE="text" NAME="subject" SIZE="50" MAXLENGTH="65"
VALUE="Website Application Programming IV"><p>
Enter message to be posted to the list: *<br> <
TEXTAREA NAME="comments" ROWS="10" COLS="65"></TEXTAREA><p>
<INPUT TYPE="submit" VALUE="Send Message"> </form>

12.9 post.cgi

This program sends messages to all members. It confirms the address and PIN details collected from the hidden address and pin fields in the post form. If the details are valid it increments the number of posts count for the member and sends the message by email to all list members.

#!/usr/bin/python
# change top line to the path of the Python interpreter on your system

""" post.cgi This file is the program which handles postings
    within a simple mailing list management system.
    Richard Kay last changed: 22 April 2002
"""

# uncomment next 3 lines to debug script
#import sys
#sys.stderr=sys.stdout
#print "Content-type: text/plain\n"

import cgiutils2
import tablcgi
from table_details import mtab # import member table details

def post_message(members,fromad,subject,message_body):
  # get comma delimited list of addresses
  address_list=[]
  for row in members.data:
    address_list.append(row["address"])

  # increment posts count for member
  index=members.find(fromad)
  row=members.data[index]
  person=row["name"]
  row["posts"]+=1
  if not members.modrow(row): # file busy - minor error ?
    print "<p> Couldn't increment member posting count. No worry.<p>"

  # compose message
  message="From: %s (%s)\n" % (person,fromad)
  line="Subject: [WPA IV]  %s\n\n" % subject
  message+=line
  line="%s\n" % message_body
  message+=line
  print "<pre>"
  print "To: ",
  for address in address_list:
    print "%s " % address,
  print "\n%s\n</pre>" % message
  cgiutils2.send_mail(fromad,address_list,message,debug=0)
  cgiutils2.html_end(received=1)

def main():
  cgiutils2.html_header(title="WPA IV post form response")

  if len(cgiutils2.keys()) == 0:
    # if no keys authentication data missing
    cgiutils2.html_end(error="Missing user identification details.")
    return
  elif not cgiutils2.has_required(["address","pin","comments","subject"]):
    cgiutils2.html_end(error="You havn't input all required values.")
    return

  # has required keys - process form
  email=cgiutils2.firstval("address")
  if not cgiutils2.is_email(email):
    cgiutils2.html_end(error="Invalid email address.")
    return

  # validate PIN
  pinentry=cgiutils2.firstval("pin")
  if not cgiutils2.is_valid(pinentry,allowed='^[1-9][0-9]{3,3}$'):
    cgiutils2.html_end(error="Invalid PIN format.")
    return
  # check its the correct PIN for this address
  members=tablcgi.table(mtab.meta,mtab.file,mtab.uniq_col)
  ismember=members.has_key(email)
  if not ismember:
    cgiutils2.html_end(error="Incorrect email address.")
    return
  else:
    index=members.find(email)
    pin=members.data[index]["pin"]
    if int(pinentry) != int(pin):
      cgiutils2.html_end(error="Incorrect PIN entered.")
      return

  # If we've got this far, there should be a valid address and PIN

  # clean up subject line and message body
  message_body=cgiutils2.firstval("comments")
  message_body=cgiutils2.make_clean(message_body)
  subject=cgiutils2.firstval("subject")
  subject=cgiutils2.make_clean(subject,r"[^\w \.\,\;\:\/\\]")

  # send message to list
  post_message(members,email,subject,message_body)
  return

if __name__ == "__main__":
  try:
    main()
  except:
    print "error detected in post.cgi main()"
    import traceback
    traceback.print_exc()