AjaxMail is built using PHP for the server-side language and MySQL for the database. A database is necessary to keep track of information relating to specific messages, such as what folder they are in and whether they have been read. Both of these can be accomplished by setting specific flags for a message in the database.
There are two folders in AjaxMail: Inbox and Trash. When a message is deleted from the Inbox, it is moved to the Trash. The message is permanently deleted when the Trash is emptied; otherwise, the message remains in the Trash. (It is also possible to restore a message from the Trash and place it back in the Inbox.) Even though this chapter uses only these two folders, you may use as many folders as you wish.
Each time a request is made to the server, AjaxMail checks to see if there are any new messages in the specified POP3 e-mail account. If there are, the messages are downloaded and saved into the MySQL database. The messages are then read out of the database and sent back to the client.
AjaxMail uses several open source software libraries to achieve its functionality:
zXml Library: The cross-browser XML JavaScript library used throughout this book. Available at www.nczonline.net/downloads/.
Douglas Crockford's JSON JavaScript Library: The JavaScript JSON parser. Available at www.json.org.
PHPMailer: A PHP SMTP e-mail sending solution. Available at http://phpmailer.sourceforge.net/.
JSON-PHP: The PHP JSON library. Available at http://mike.teczno.com/json.html.
POP3Lib: A PHP POP3 mail interface written by one of your authors, Jeremy McPeak. Available at www.wdonline.com/php/pop3lib.zip.
Note that all these resources are included in the book's example code downloads, available at www.wrox.com.
Because AjaxMail will need e-mails to be stored in the database, several tables must be created. If you have sufficient rights to create a new database on the MySQL server, you should create a database named AjaxMail. (You may also use any other database that is already set up.) There are three tables to add: AjaxMailFolders,AjaxMailMessages, and AjaxMailAttachments.
The first table, AjaxMailFolders, defines the various folders available in AjaxMail:
CREATE TABLE AjaxMailFolders ( FolderId int(11) NOT NULL auto_increment, Name text NOT NULL, PRIMARY KEY (FolderId) ); INSERT INTO AjaxMailFolders VALUES (1, 'Inbox'); INSERT INTO AjaxMailFolders VALUES (2, 'Trash');
Each folder in AjaxMail is assigned a FolderId (an autoincrementing primary key) and a name. For the purposes of this chapter, there are only two folders: Inbox and Trash. You can feel free to add more to suit your own needs.
The AjaxMailMessages table holds each e-mail's information. It consists of 11 columns: a unique identification number (autoincremented so you don't need to worry about it), the different fields of an e-mail (To, From, Subject, and so on), what folder it exists in (Inbox, Trash, and so on), and whether the user has read the e-mail. You can create the table using the following SQL statement:
CREATE TABLE AjaxMailMessages ( MessageId int(11) NOT NULL auto_increment, 'To' text NOT NULL, CC text NOT NULL, BCC text NOT NULL, 'From' text NOT NULL, Subject text NOT NULL, Date bigint(20) default NULL, Message text NOT NULL, HasAttachments tinyint(1) NOT NULL default '0', Unread tinyint(1) NOT NULL default '1', FolderId int(11) NOT NULL default '0', PRIMARY KEY (MessageId) );
The MessageId field is an autoincrementing field and provides the e-mail with a unique ID number in the database; it is also the table's primary key. The To, CC, BCC, From, Subject, Date, and Message fields are parts of an e-mail message. (To and From must be enclosed in backtick symbols because they are keywords in SQL.) The HasAttachments and Unread fields are tinyint, which means they can have values of 0 or 1 (false or true). Finally, the FolderId field contains the ID number of the folder in which the message is stored. This enables you to select messages that exist only in the Inbox or Trash folders.
If an e-mail contains any attachments, they are stored in the AjaxMailAttachments table:
CREATE TABLE AjaxMailAttachments ( AttachmentId int(11) NOT NULL auto_increment, MessageId int(11) NOT NULL default '0', Filename text NOT NULL, ContentType text NOT NULL, Size int(11) NOT NULL default '0', Data longtext NOT NULL, PRIMARY KEY (AttachmentId) )
Like the AjaxMailMessages table, the AjaxMailAttachments table contains an autoincrementing field. This filed is called AttachmentId and provides each attachment with a unique ID number. The next field, MessageId, houses the message ID of the e-mail to which it was attached (this number matches the MessageId field in AjaxMailMessages). Next, the Filename and ContentType columns store the attachment's reported file name and content-type (both are necessary to enable the user to download the attachment later). Last, the Size and Data fields store the attachment's size (in bytes) and the binary/text data of the file, respectively.
Much like any application, AjaxMail relies on a configuration file, called config.inc.php, to provide information required to function properly. As this information is required in many different areas of the application, it's best to store it as constants. In PHP, constants give you the advantage of being available in every scope of the application, meaning that you don't need to define them using the global keyword as you do with other global variables.
To create a constant in PHP, use the define() method, passing in the name of the constant (as a string) and its value. For example:
define("MY_CONSTANT", "my value");
The first group of constants relates directly to your MySQL database:
define("DB_USER", "root"); define("DB_PASSWORD", "password"); define("DB_SERVER", "localhost"); define("DB_NAME", "AjaxMail");
These constants are used when connecting to the database and must be replaced to reflect your database settings. Next, some constants are needed to provide information about your POP3 server:
define("POP3_USER", "test@domain.com"); define("POP3_PASSWORD", "password"); define("POP3_SERVER", "mail.domain.com");
Once again, these constants must be replaced with the information specific to your POP3 server. As you may have guessed, you also must supply some information about the SMTP server:
define("SMTP_DO_AUTHORIZATION", true); define("SMTP_USER", "test@domain.com"); define("SMTP_PASSWORD", "password"); define("SMTP_SERVER", "mail.domain.com"); define("EMAIL_FROM_ADDRESS", "test@domain.com"); define("EMAIL_FROM_NAME", "Joe Somebody");
The first four lines set constant variables relating to user authentication for your SMTP server. The first variable sets whether or not your SMTP server requires user authentication to send e-mail. If set to true, the SMTP_USER and SMTP_PASSWORD must be set. (false means no authentication is required to send mail through the SMTP server.)
The second group of SMTP settings defines the user settings. You should set EMAIL_FROM_ADDRESS to contain your e-mail address, and set EMAIL_FROM_NAME to contain your name. When you send an e-mail, your recipient will see these values as the sender.
The final setting is the MESSAGES_PER_PAGE constant, which defines how many e-mail messages should be displayed per page:
define("MESSAGES_PER_PAGE", 10);
These constants are used throughout the application: when retrieving e-mail, connecting to the database, sending mail, and even when displaying the information.
The code contained in the file called AjaxMail.inc.php serves as the workhorse of the server-side application. This file houses the AjaxMailbox class, which is the primary interface by which all the mail is handled. A few helper classes, mainly used for JSON encoding, also exist in this file.
The AjaxMailbox class, which you will build in this section, begins with an empty class declaration:
class AjaxMailbox { //more code here }
It's the responsibility of AjaxMailbox to handle all the interaction with the database. To facilitate this communication, several methods relate just to the database.
The first method is connect(), which, as you may expect, initiates a connection to the database:
class AjaxMailbox { function connect() { $conn = mysql_connect(DB_SERVER, DB_USER, DB_PASSWORD) or die("Could not connect : ". mysql_error()); mysql_select_db(DB_NAME); return $conn; } //more code here }
Using the database constants defined in config.inc.php, this method creates a database connection and stores it in the variable $conn. Then, the specific database is selected and $conn is returned. Of course, you also need to be able to disconnect from the database:
class AjaxMailbox { function connect() { $conn = mysql_connect(DB_SERVER, DB_USER, DB_PASSWORD) or die("Could not connect : ". mysql_error()); mysql_select_db(DB_NAME); return $conn; } function disconnect($conn) { mysql_close($conn); } //more code here }
The disconnect() method accepts a connection object (the same one returned from connect()) and uses mysql_close() to close that connection.
During development of a database application, you may sometimes end up with bad data in your database tables. At such times, it's best just to clear all the data from the tables and start fresh. As a means of database maintenance, AjaxMailbox has a method, clearAll(), that does just this:
class AjaxMailbox { //connect and disconnect methods function clearAll() { $conn = $this->connect(); $query = "truncate table AjaxMailMessages"; mysql_query($query,$conn); $query = "truncate table AjaxMailAttachments"; mysql_query($query,$conn); $this->disconnect($conn); } //more code here }
This method begins by calling connect() to create a database connection. Then, two SQL statements are executed, using the TRUNCATE command to clear out both AjaxMailMessages and AjaxMailAttachments. The TRUNCATE command is used for two reasons. First, it is generally faster than deleting every row in a table, and second, it clears the AUTO_INCREMENT handler, so any fields that automatically increment will start back at 1. The last step is to disconnect from the database by calling disconnect().
Retrieving e-mail from a POP3 server is not an easy task, and it is beyond the scope of this book to walk you through the lengthy process. Instead, AjaxMail uses POP3Lib to interface with the POP3 server. This library contains numerous classes that aid in this type of communication. These classes are used in the checkMail() method, which is responsible for downloading messages from the POP3 server and inserting them into the database.
The method begins by creating a new instance of the Pop3 class, which is the POP3Lib main class for communicating with a POP3 server. Its constructor accepts four arguments, three of which are required. The first argument is the POP3 server name, the second is the user name, the third is the password for that user name, and the fourth (optional) argument is the port at which to connect to the POP3 server (default is 110). To create a connection to the POP3 server, use the login() method. This method returns a Boolean value indicating the success (true) or failure (false) of the login attempt:
class AjaxMailbox { //database methods function checkMail() { $pop = new Pop3(POP3_SERVER, POP3_USER, POP3_PASSWORD); if ($pop->login()) { //Email downloading/database manipulation code here. } } //more code here }
The checkMail() method begins by creating a Pop3 object using the constant information defined in config.inc.php. Then, the login() method is called to try connecting to the server.
With a successful login, the Pop3 object retrieves the number of messages found on the server and assigns this value to the mailCount property. Additionally, the messages array is initialized and populated with the header information of all e-mails residing on the server (the header information includes to, from, subject, date, and attachment information). Each item in the messages array at this point is a Pop3Header object.
To retrieve the entire e-mail, which includes the headers, message, and attachments, you must call the getEmails() method. This method completely repopulates the messages array with Pop3Message objects that contain all the e-mail information. A Pop3Message object has the following properties and methods:
Property/Method |
Description |
---|---|
from |
The sender's e-mail address. |
to |
The recipient's e-mail address. This property also contains all recipients if the e-mail was sent with multiple addresses in the To field. |
subject |
The subject of the e-mail. |
cc |
The recipient information held in the CC field. |
bcc |
If you receive an e-mail as a blind carbon copy, your e-mail address is in this property. |
date |
The date of the e-mail in RFC 2822 format. |
unixTimeStamp |
The date in a Unix timestamp (number of seconds since midnight on January 1, 1970). |
hasAttachments |
A Boolean value indicating whether the e-mail contains one or more attachments. |
attachments |
An array of attachments sent with the e-mail. |
getTextMessage() |
Retrieves the plain text body of an e-mail. |
getHTMLMessage() |
Retrieves the HTML body of an e-mail (if any). |
These properties and methods are used to extract information about an e-mail and insert it into the database.
After a successful login, the first thing to do is to check for any e-mails on the server. You can do so by using the mailCount property. If mailCount is greater than zero, getEmails() is called to retrieve all the e-mail information and a database connection is made, anticipating the insertion of new e-mails:
class AjaxMailbox { //database methods function checkMail() { $pop = new Pop3(POP3_SERVER, POP3_USER, POP3_PASSWORD); if ($pop->login()) { if ($pop->mailCount > 0) { $conn = $this->connect(); $pop->getEmails(); //more code here $this->disconnect($conn); } $pop->logoff(); } } //more code here }
In this code snippet, the disconnect() and logoff() methods are called in their appropriate locations. The logoff() method, as you may have guessed, closes the connection to the POP3 server.
With all the e-mail information now retrieved, you can begin inserting data into the database by iterating over the e-mails in the messages array:
class AjaxMailbox { //database methods function checkMail() { $pop = new Pop3(POP3_SERVER, POP3_USER, POP3_PASSWORD); if ($pop->login()) { if ($pop->mailCount > 0) { $conn = $this->connect(); $pop->getEmails(); foreach ($pop->messages as $message) { $query = "insert into AjaxMailMessages('To',CC,BCC,'From',"; $query .= "Subject,'Date',Message,HasAttachments,FolderId,Unread)"; $query .= "values('%s','%s','%s','%s','%s',%s,'%s'," .$message->hasAttachments.",1,1)"; $query = sprintf($query, (addslashes($message->to)), (addslashes($message->cc)), (addslashes($message->bcc)), (addslashes($message->from)), (addslashes($message->subject)), $message->unixTimeStamp, (addslashes($message->getTextMessage())) ); $result = mysql_query($query, $conn); //more code here } $this->disconnect($conn); } $pop->logoff(); } } //more code here }
A foreach loop is used to iterate over the messages array. For each message, a SQL INSERT statement is created and then executed. Since the SQL statement is so long, sprintf() is used to insert the information into the right location. Note that each value, (aside from unixTimeStamp) must be encoded using addslashes() so that the string will be proper SQL syntax. The statement is executing using mysql_query(). The only remaining part is to deal with attachments.
You can determine if a message has attachments by using the hasAttachments property of a message. If there are attachments, you first must retrieve the ID of the most recently added e-mail message. (Remember, attachments are tied to the e-mail from which they were attached.) After the ID is determined, SQL INSERT statements are created for each attachment:
class AjaxMailbox { //database methods function checkMail() { $pop = new Pop3(POP3_SERVER, POP3_USER, POP3_PASSWORD); if ($pop->login()) { if ($pop->mailCount > 0) { $conn = $this->connect(); $pop->getEmails(); foreach ($pop->messages as $message) { $query = "insert into AjaxMailMessages('To',CC,BCC,'From',"; $query .= "Subject,'Date',Message,HasAttachments,FolderId,Unread)"; $query .= "values('%s','%s','%s','%s','%s',%s,'%s'," .$message->hasAttachments.",1,1)"; $query = sprintf($query, (addslashes($message->to)), (addslashes($message->cc)), (addslashes($message->bcc)), (addslashes($message->from)), (addslashes($message->subject)), $message->unixTimeStamp, (addslashes($message->getTextMessage())) ); $result = mysql_query($query, $conn); if ($message->hasAttachments) { $messageId = mysql_insert_id($conn); foreach ($message->attachments as $attachment) { $query = "insert into AjaxMailAttachments(MessageId,"; $query .= "Filename, ContentType, Size, Data)"; $query .= "values($messageId, '%s', '%s', '%s', '%s')"; $query = sprintf($query, addslashes($attachment->fileName), $attachment->contentType, strlen($attachment->data), addslashes($attachment->data)); mysql_query($query, $conn); } } } $this->disconnect($conn); } $pop->logoff(); } } //more code here }
The most recently inserted ID can be retrieved using mysql_insert_id(). Then, the attachments are iterated over using a foreach loop. Each item in the attachments array is a Pop3Attachment object. This class represents attachment data for a particular e-mail and contains three properties: contentType, which contains the attachment's MIME content-type, fileName, which represents the file name of the attachment, and data, which contains the actual attachment file data. Depending on the content-type of the attachment, data can be either binary or plain text information.
Once again, the sprintf() function is used to format the query string. Notice the use of strlen() on the attachment data; this retrieves the size of the data in bytes for easy retrieval later on. After the string has been formatted, the query is run against the database to insert the data into AjaxMailAttachments. This concludes the checkMail() method.
This method is never called directly; instead, it is called whenever a request to the server is made. In essence, checkMail() is piggy-backed onto other requests so that the user is always viewing the most recent data.
Probably the most common operation of the application is to retrieve a list of e-mails to display to the user. The method responsible for this operation, getFolderPage(), accepts two arguments: the ID number of the folder and the page number to retrieve:
class AjaxMailbox { //database methods //check mail method function getFolderPage($folder, $page) { $this->checkMail(); //more code here } }
When called, getFolderPage() first calls checkMail() to ensure that the most recent data is available in the database. If there are any new messages, they will be inserted into the database so that any queries run thereafter will be up-to-date.
The next step is to build a JSON string to send back to the client. To aid in this, a generic class called JSONObject is defined:
class JSONObject { }
A JSONObject instance is used merely to hold data until it is time to be serialized into the JSON format. For getFolderPage(), this object contains information about the folder. The JSON data contains many useful bits of information: the total number of messages (messageCount), the current page (page), the total number of pages (pageCount), the folder number (folder), the first message returned (firstMessage), the total number of unread messages (unreadCount), and finally an array of messages in the page (messages). The data structure looks like this:
{ "messageCount":0, "page":1, "pageCount":1, "folder":1, "firstMessage":0, "unreadCount": 0, "messages":[] }
The JSONObject is created and initialized with several properties relating to information in the database:
class AjaxMailbox { //database methods //check mail method function getFolderPage($folder, $page) { $this->checkMail(); $conn = $this->connect(); $query = "select count(MessageId) as count from AjaxMailMessages"; $query .= "where FolderId=$folder"; $result = mysql_query($query, $conn); $row = mysql_fetch_assoc($result); $info = new JSONObject(); $info->messageCount = (int) $row["count"]; $info->page = $page; $info->pageCount = (int) ceil($info->messageCount/MESSAGES_PER_PAGE); $info->folder = $folder; $firstMessageNum = ($page-1) * MESSAGES_PER_PAGE; $info->firstMessage = $firstMessageNum+1; $info->messages = array(); $info->unreadCount = $this->getUnreadCount($conn); //more code here } }
Using the SQL count() function, you can easily retrieve the total number of messages in a given folder. A JSONObject is created and stored in $info, and its messageCount property is set to the value retrieved from the database. Next, the page number is assigned to the page property (this is the same value that was passed into the method). The pageCount property determines how many total pages exist for the current folder. This is done by dividing the messageCount by the MESSAGES_PER_PAGE constant and applying the mathematical ceiling function (essentially, round up to the nearest whole number). Then, the folder ID is assigned to the folder property.
Next, the index of the first message to display on the page is calculated and stored in $firstMessageNum. This number is important because it keeps the database from retrieving too much information. The $info object is assigned a property of firstMessage that is equal to $firstMessageNum plus one. This is done because this value will be displayed to the user, and you never want to show message number zero; the first message should always be message number one. A property called messages is created and initialized to an empty array; this will contain message objects later.
The last step in this section of code is to create a property named unreadCount and assign it the number of unread messages in the database. To do so, use the getUnreadCount() method, defined as follows:
class AjaxMailbox { //other methods function getUnreadCount($conn) { $query = "select count(MessageId) as UnreadCount from AjaxMailMessages"; $query .= "where FolderId=1 and Unread=1"; $result = mysql_query($query, $conn); $row = mysql_fetch_assoc($result); return intval($row["UnreadCount"]); } //other methods }
After getting this information, it's time to retrieve specific e-mail messages. To do so, execute a query on all messages in a given folder, ordered by the date. This is where the first message number comes into play; by adding a LIMIT statement to the end of the query, you can ensure the exact messages are contained in the result set. By specifying the first message number and then the total number of messages, the LIMIT statement retrieves just those messages:
class AjaxMailbox { //database methods //check mail method function getFolderPage($folder, $page) { $this->checkMail(); $conn = $this->connect(); $query = "select count(MessageId) as count from AjaxMailMessages"; $query .= "where FolderId=$folder"; $result = mysql_query($query, $conn); $row = mysql_fetch_assoc($result); $info = new JSONObject(); $info->messageCount = (int) $row["count"]; $info->page = $page; $info->pageCount = (int) ceil($info->messageCount/MESSAGES_PER_PAGE); $info->folder = $folder; $firstMessageNum = ($page-1) * MESSAGES_PER_PAGE; $info->firstMessage = $firstMessageNum+1; $info->messages = array(); $info->unreadCount = $this->getUnreadCount($conn); $query = "select * from AjaxMailMessages where FolderId=$folder"; $query .= "order by date desc limit $firstMessageNum, "; $query .= MESSAGES_PER_PAGE; $result = mysql_query($query, $conn); //more code here } }
The complete SQL statement selects all messages where the value in FolderId matches $folder and orders the returned rows by date in descending order. It also starts the selection from the value in $firstMessageNum, and retrieves only the amount specified by MESSAGES_PER_PAGE.
At this point, there are two possible scenarios: either the database returned results or it didn't. In the application, it is important to know when either situation takes place. Thankfully, it is easy to discern when a query is not successful. The mysql_query() function returns false on an unsuccessful query; therefore, you can check to see if a query failed by checking the $result variable. If there is an error, it can be returned in a property of the $info object. Otherwise, you'll need to iterate through the rows that were returned, creating a new JSONObject for each message and adding it to the messages array:
class AjaxMailbox { //database methods //check mail method function getFolderPage($folder, $page) { $this->checkMail(); $conn = $this->connect(); $query = "select count(MessageId) as count from AjaxMailMessages"; $query .= "where FolderId=$folder"; $result = mysql_query($query, $conn); $row = mysql_fetch_assoc($result); $info = new JSONObject(); $info->messageCount = (int) $row["count"]; $info->page = $page; $info->pageCount = (int) ceil($info->messageCount/MESSAGES_PER_PAGE); $info->folder = $folder; $firstMessageNum = ($page-1) * MESSAGES_PER_PAGE; $info->firstMessage = $firstMessageNum+1; $info->messages = array(); $info->unreadCount = $this->getUnreadCount($conn); $query = "select * from AjaxMailMessages where FolderId=$folder"; $query .= "order by date desc limit $firstMessageNum, "; $query .= MESSAGES_PER_PAGE; $result = mysql_query($query, $conn); if (!$result) { $info->error = mysql_error($conn); } else { while ($row = mysql_fetch_assoc($result)) { $message = new JSONObject(); $message->id = $row['MessageId']; $message->from = $row['From']; $message->subject = $row['Subject']; $message->date = date("M j Y", intval($row["Date"])); $message->hasAttachments = ($row['HasAttachments'] == 1); $message->unread = ($row['Unread'] == 1); $info->messages[] = $message; } } $this->disconnect($conn); return $info; } }
In this code, the $result variable is checked. If the query failed, an error property is added to the $info object and assigned the error message retrieved from mysql_error(). Client-side code can then check this property to determine if an error occurred. If the query executed successfully, a new instance of JSONObject is created to contain the message information; this is stored in $message. This object is populated with all the information from the $row object, paying particular attention to format the message date so that it displays the month, day, and year only. (This eliminates the need for JavaScript to format the date.) Also, since the HasAttachments and Unread fields are bits, they are compared to the number 1 so that the corresponding properties on $message are filled with Boolean values instead of integers. The last line inside of the while loop adds the $message object to the end of the messages array.
After that is completed, you can safely disconnect from the database (using disconnect()) and return the $info object. It is up to the process using getFolderPage() to JSON-encode the object to be sent to the client.
Retrieving a specific message involves two helper classes, AjaxMailMessage and AjaxMailAttachmentHeader, and a method of the AjaxMailbox class called getMessage(). The two helper classes are used purely to store information that will later be JSON-encoded and sent to the client.
The first helper class, AjaxMailMessage, represents a single e-mail message:
class AjaxMailMessage { var $to; var $from; var $cc; var $bcc; var $subject; var $message; var $date; var $attachments; var $unread; var $hasAttachments; var $id; function AjaxMailMessage() { $this->attachments = array(); } }
The properties of this class resemble those of the field names in the database; the sole exception is the attachments property, which is an array of attachments associated with this e-mail. The JSON structure of the AjaxMailMessage class looks like this:
{ to : "to", from : "from", cc : "cc", bcc : "bcc", subject : "subject", message : "message", date : "date", attachments : [], unread : false, hasAttachments : true, id : 1 }
The attachments array actually contains instances of AjaxMailAttachmentHeader, which provide general information about an attachment without containing the actual binary or text data:
class AjaxMailAttachmentHeader { var $id; var $filename; var $size; function AjaxMailAttachmentHeader($id, $filename, $size) { $this->id = $id; $this->filename = $filename; $this->size = "" . (round($size/1024*100)/100)." KB"; } }
The constructor for this class accepts three arguments: the attachment ID (the value of the AttachmentId column of the AjaxMailAttachments table), the file name, and the size of the attachment in bytes. The size is converted into a string (indicated by the number of kilobytes in the file) by dividing the size by 1024 and then rounding to the nearest hundredth of a kilobyte (so you can get a string such as "0.55 KB"). When JSON-encoded, the AjaxMailAttachmentHeader object is added to the previous JSON structure, as follows:
{ "to" : "to", "from" : "from", "cc" : "cc", "bcc" : "bcc", "subject" : "subject", "message" : "message", "date" : "date", "attachments" : [ { "id" : 1, "filename" : "filename", "size" : "1KB" } ], "unread" : false, "hasAttachments" : true, "id" : 1 }
The getMessage() method utilizes these two classes when assembling the data for transmission to the client. This method takes one argument, the message ID number that corresponds to the MessageId column in the AjaxMailMessages table:
class AjaxMailbox { //other methods function getMessage($messageId) { $conn = $this->connect(); //get the information $query = "select MessageId, 'To', 'From', CC, BCC, Subject, Date, "; $query .= "Message, HasAttachments, Unread from AjaxMailMessages where"; $query .= "MessageId=$messageId"; $result = mysql_query($query, $conn); $row = mysql_fetch_assoc($result); //more code here } //other methods }
This method begins by making a connection to the database using the connect() method. Then, a query to retrieve the various parts of the e-mail is created (stored in $query) and executed, with the results ending up in the $row object.
The next step is to create an AjaxMailMessage object and populate it with all the data from the database:
class AjaxMailbox { //other methods function getMessage($messageId) { $conn = $this->connect(); //get the information $query = "select MessageId, 'To', 'From', CC, BCC, Subject, Date, "; $query .= "Message, HasAttachments, Unread from AjaxMailMessages where"; $query .= "MessageId=$messageId"; $result = mysql_query($query, $conn); $row = mysql_fetch_assoc($result); $message = new AjaxMailMessage(); $message->id = $row["MessageId"]; $message->to = $row["To"]; $message->cc = $row["CC"]; $message->bcc = $row["BCC"]; $message->unread = ($row["Unread"]==1); $message->from = $row["From"]; $message->subject = $row["Subject"]; $message->date = date("M j, Y h:i A", intval($row["Date"])); $message->hasAttachments = ($row["HasAttachments"]==1); $message->unreadCount = $this->getUnreadCount($conn); $message->message = $row["Message"]; //more code here } //other methods }
As with getFolderPage(), the database fields represented as bits are compared to 1 to get a Boolean value. The date is also formatted into a longer string, one that contains both the date and time (formatted as in "Oct 28, 2005 05:17 AM"). You'll also notice that the unreadCount property is added to the message. Although this doesn't pertain to the message itself, it helps to keep the user interface updated with the most recent number of unread mails in the database.
The last part of this method is to return information about the attachments (if any).
class AjaxMailbox { //other methods function getMessage($messageId) { $conn = $this->connect(); //get the information $query = "select MessageId, 'To', 'From', CC, BCC, Subject, Date, "; $query .= "Message, HasAttachments, Unread from AjaxMailMessages where"; $query .= "MessageId=$messageId"; $result = mysql_query($query, $conn); $row = mysql_fetch_assoc($result); $message = new AjaxMailMessage(); $message->id = $row["MessageId"]; $message->to = $row["To"]; $message->cc = $row["CC"]; $message->bcc = $row["BCC"]; $message->unread = ($row["Unread"]==1); $message->from = $row["From"]; $message->subject = $row["Subject"]; $message->date = date("M j, Y h:i A", intval($row["Date"])); $message->hasAttachments = ($row["HasAttachments"]==1); $message->unreadCount = $this->getUnreadCount($conn); $message->message = $row["Message"]; if ($message->hasAttachments) { $query = "select AttachmentId,Filename,Size from AjaxMailAttachments"; $query .= "where MessageId=$messageId"; $result = mysql_query($query, $conn); while ($row = mysql_fetch_assoc($result)) { $message->attachments[] = new AjaxMailAttachmentHeader( $row["AttachmentId"], $row["Filename"], (int) $row["Size"]); } } $this->disconnect($conn); return $message; } //other methods }
In this section of code, you begin by verifying whether there are any attachments on the e-mail. If an attachment exists, a query is run to return all the attachments in the database. Note that the actual contents of the attachment aren't returned, just the attachment ID, file name, and size. Using a while loop to iterate over the results, a new AjaxMailAttachmentHeader is created for each attachment and added to the $message object's attachments array. After that, you need only disconnect from the database and return the $message object. Once again, it is up to the process using this method to JSONencode the returned object.
AjaxMail relies on the PHPMailer library (http://phpmailer.sourceforge.net) to send e-mails. This full-featured library enables you to send mail either through an SMTP server or the sendmail application (www.sendmail.org). As discussed earlier, AjaxMail uses SMTP exclusively.
The method used to send mail is called sendMail(). This method accepts four arguments, with only the first three being required. These arguments are $to (the string containing the e-mail addresses to send to), $subject (the subject of the e-mail), $message (the body of the e-mail), and $cc (which can optionally specify who to send a carbon copy to).
The first step in this method is to create an instance of the PHPMailer class and assign the To and CC fields. You can add these by using the AddAddress() and AddCC() methods of the PHPMailer object, respectively. Each of these accepts two arguments: the e-mail address and the real name of the person. This presents a problem in that the $to and $cc arguments can contain multiple e-mail addresses separated by semicolons or commas, and may consist of name <e-mail> pairs. For example, an e-mail sent to two recipients without carbon copying could look like this:
Joe Somebody <joe@somebody.com>; Jim Somebody <jim@somebody.com>
You must take this into account when sending mail using PHPMailer:
class AjaxMailbox { //other methods here function sendMail($to, $subject, $message, $cc="") { $mailer = new PHPMailer(); $tos = preg_split ("/;|,/", $to); foreach ($tos as $to) { preg_match("/(.*?)<?(.*?)>?/i", $to, $matches); $mailer->AddAddress($matches[2],str_replace('"','',$matches[1])); } if ($cc != "") { $ccs = preg_split ("/;|,/", $cc); foreach ($ccs as $cc) { preg_match("/(.*?)<?(.*?)>?/i", $cc, $matches); $mailer->AddCC($matches[2],str_replace('"','',$matches[1])); } } //more code here } //other methods here }
The first line in the method creates an instance of PHPMailer. In the next line, the $to string is split by both semicolons and commas with the preg_split() function, which returns an array of e-mail addresses. Then, iterating through the $tos array, the code checks for a match to real name <email> with the preg_match() function. The regular expression used in the preg_match() function returns an array with three matches. The first is the entire string, the second is the real name if it exists, and the third is the e-mail address. You can then add the addresses by using AddAddress() and passing in the second and third matches. Since the real name may be enclosed in quotes, str_replace() is used to strip out any quotes that may be in the real name part of the string. This same process is repeated for the $cc string, where the AddCC() method is used.
Note |
You will always have three elements in the $matches array, even if no name is in the string. |
Next, you need to assign the pertinent SMTP information to the $mailer object, along with the subject and message body. Then, you can send the e-mail:
class AjaxMailbox { //other methods here function sendMail($to, $subject, $message, $cc="") { $mailer = new PHPMailer(); $tos = preg_split ("/;|,/", $to); foreach ($tos as $to) { preg_match("/(.*?)<?(.*?)>?/i", $to, $matches); $mailer->AddAddress($matches[2],str_replace('"','',$matches[1])); } if ($cc != "") { $ccs = preg_split ("/;|,/", $cc); foreach ($ccs as $cc) { preg_match("/(.*?)<?(.*?)>?/i", $cc, $matches); $mailer->AddCC($matches[2],str_replace('"','',$matches[1])); } } $mailer->Subject = $subject; $mailer->Body = $message; $mailer->From = EMAIL_FROM_ADDRESS; $mailer->FromName = EMAIL_FROM_NAME; $mailer->SMTPAuth = SMTP_DO_AUTHORIZATION; $mailer->Username = SMTP_USER; $mailer->Password = SMTP_PASSWORD; $mailer->Host = SMTP_SERVER; $mailer->Mailer = "smtp"; $mailer->Send(); $mailer->SmtpClose(); //more code here } //other methods here }
For the first two properties, Subject and Body, simply use the values that were passed into the method. You set their values equal to those passed to the method. Next, the From and FromName properties are set to the constant values from config.inc.php; the first represents the sender's e-mail address, and the second contains the sender's real name (which many e-mail clients simply display as the sender).
The properties following those are the SMTP authorization settings. Some SMTP servers require authentication to send e-mail messages and some don't. If SMTPAuth is false, PHPMailer attempts to send e-mails without sending the Username and Password. If true, the class sends those values to the server in an attempt to authorize the sending of the e-mail.
The final two properties before sending an e-mail are the SMTP server and the method of which to send. The Host property is assigned to SMTP_SERVER and the Mailer property is set to "smtp", indicating the type of mailer being used (as opposed to "sendmail").
After setting those properties, you can invoke the Send() method to actually send the e-mail and then call SmtpClose() to close the SMTP connection. But the method isn't quite done yet. The client still needs to know if the e-mail message was sent successfully. To do that, you'll need to create a response object containing information about the transmission:
class AjaxMailbox { //other methods here function sendMail($to, $subject, $message, $cc="") { $mailer = new PHPMailer(); $tos = preg_split ("/;|,/", $to); foreach ($tos as $to) { preg_match("/(.*?)<?(.*?)>?/i", $to, $matches); $mailer->AddAddress($matches[2],str_replace('"','',$matches[1])); } if ($cc != "") { $ccs = preg_split ("/;|,/", $cc); foreach ($ccs as $cc) { preg_match("/(.*?)<?(.*?)>?/i", $cc, $matches); $mailer->AddCC($matches[2],str_replace('"','',$matches[1])); } } $mailer->Subject = $subject; $mailer->Body = $message; $mailer->From = EMAIL_FROM_ADDRESS; $mailer->FromName = EMAIL_FROM_NAME; $mailer->SMTPAuth = SMTP_DO_AUTHORIZATION; $mailer->Username = SMTP_USER; $mailer->Password = SMTP_PASSWORD; $mailer->Host = SMTP_SERVER; $mailer->Mailer = "smtp"; $mailer->Send(); $mailer->SmtpClose(); $response = new JSONObject(); if ($mailer->IsError()) { $response->error = true; $response->message = $mailer->ErrorInfo; } else { $response->error = false; $response->message = "Your message has been sent."; } return $response; } //other methods here }
A JSONObject is instantiated to carry the information back to the client. PHPMailer provides a method called IsError(), which returns a Boolean value indicating the success or failure of the sending process. If it returns true, that means the e-mail was not sent successfully, so the $response object has its error property set to true and the error message is extracted from the ErrorInfo property of $mailer. Otherwise, the error property is set to false and a simple confirmation message is sent. The last step is to return the $response object.
When attachments are stored in the database, you need a way to get them back out. The getAttachment() method provides all the information necessary to enable a user to download an attachment. This method takes one argument, the attachment ID, and returns an AjaxMailAttachment object. The AjaxMailAttachment class is another helper that encapsulates all the information about an attachment:
class AjaxMailAttachment { var $contentType; var $filename; var $size; var $data; function AjaxMailAttachment($contentType, $filename, $size, $data) { $this->contentType = $contentType; $this->filename = $filename; $this->size = $size; $this->data = $data; } }
The getAttachment() method itself is fairly straightforward:
class AjaxMailbox { //other methods here function getAttachment($attachmentId) { $conn = $this->connect(); $query = "select * from AjaxMailAttachments where "; $query .= "AttachmentId=$attachmentId"; $result = mysql_query($query, $conn); $row = mysql_fetch_assoc($result); $this->disconnect($conn); return new AjaxMailAttachment( $row["ContentType"], $row["Filename"], $row["Size"], $row["Data"] ); } //other methods here }
This code connects to the database with the connect() method and performs the database query. This particular query selects all fields from AjaxMailAttachments where AttachmentId is equal to the method's argument. After running the query, the database connection is closed and an AjaxMailAttachment object is returned containing all the information about the attachment.
Four methods in the AjaxMailbox class deal with moving messages to and from the Trash. The first method, deleteMessage(), doesn't actually delete the e-mail message; instead, it updates the FolderId column in the database to have a value of 2, meaning that the message now resides in the Trash. This method accepts one argument, the identification number of the message:
class AjaxMailbox { //other methods here function deleteMessage($messageId) { $conn = $this->connect(); $query = "update AjaxMailMessages set FolderId=2 where "; $query .= "MessageId=$messageId"; mysql_query($query,$conn); $this->disconnect($conn); } //other methods here }
This method simply connects to the database, runs the SQL statement to change the FolderId, and then disconnects from the database. Of course, you can also restore a message from the Trash once it has been moved there. To do so, simply set the FolderId back to 1; this is the job of the restoreMessage() method.
The restoreMessage() method also accepts one argument, the message ID, and follows the same basic algorithm:
class AjaxMailbox { //other methods here function restoreMessage($messageId) { $conn = $this->connect(); $query = "update AjaxMailMessages set FolderId=1 where "; $query .= "MessageId=$messageId"; mysql_query($query,$conn); $this->disconnect($conn); } //other methods here }
This method mirrors deleteMethod(), with the only difference being the value of FolderId to be set.
From time to time, there will be a lot of e-mail messages in the Trash. There may come a time when the user decides that he or she no longer needs them and the Trash should be emptied. The emptyTrash() method deletes every message with a FolderId value of 2 as well as any attachments those messages may have had.
The emptyTrash() method relies on two queries to delete the message and attachment information in the database. The first query deletes the attachments of messages in the Trash, and the second query deletes the messages themselves:
class AjaxMailbox { //other methods here function emptyTrash() { $conn = $this->connect(); $query = "delete from AjaxMailAttachments where MessageId in "; $query .= "(select MessageId from AjaxMailMessages where FolderId=2)"; mysql_query($query, $conn); $query = "delete from AjaxMailMessages where FolderId=2"; mysql_query($query,$conn); $this->disconnect($conn); } //other methods here }
The first query uses a feature called sub-querying to select MessageIds of messages that are in the Trash. Sub-queries are a feature in MySQL 4 and above (if you use MySQL 3.x, you need to upgrade before using this code). The second query is very straightforward, simply deleting all messages with a FolderId of 2. The last step, of course, is to disconnect from the database.
Nearly every e-mail client marks messages as unread when they first arrive. This feature enables users to keep track of the messages they previously read and easily tell which messages are new. The methods responsible for this feature in AjaxMail resemble those of deleting and restoring messages because they simply accept a message ID as an argument and then update a single column in the database.
The first method, markMessageAsRead(), marks the message as read after the user opens it:
class AjaxMailbox { //other methods here function markMessageAsRead($messageId) { $conn = $this->connect(); $query = "update AjaxMailMessages set Unread=0 where MessageId=$messageId"; mysql_query($query,$conn); $this->disconnect($conn); } //other methods here }
This code runs an UPDATE statement that sets the message's Unread column to 0, specifying the message as read.
Similarly, the method to mark a message as unread performs almost the exact same query:
class AjaxMailbox { //other methods here function markMessageAsUnread($messageId) { $conn = $this->connect(); $query = "update AjaxMailMessages set Unread=1 where MessageId=$messageId"; mysql_query($query,$conn); $this->disconnect($conn); } //other methods here }
The only difference between the markMessageAsUnread() method and the markMessageAsRead() method is the value the Unread column is assigned when you run the query.
AjaxMail, like many other PHP applications, relies on an action-based architecture to perform certain operations. In other words, the application queries a separate PHP file that handles certain actions and executes code according to the action. There are several files that perform action requests from the client in different ways.
The AjaxMailAction.php. file is one of the files used by the client to perform various actions. Your first step in writing this file is to include all the required files. Because this file uses the AjaxMailbox class, you need to include quite a few files, including the config.inc.php file, the four files in POP3Lib, AjaxMail.inc.php, and JSON.php for JSON encoding:
require_once("inc/config.inc.php"); require_once("inc/pop3lib/pop3.class.php"); require_once("inc/pop3lib/pop3message.class.php"); require_once("inc/pop3lib/pop3header.class.php"); require_once("inc/pop3lib/pop3attachment.class.php"); require_once("inc/AjaxMail.inc.php"); require_once("inc/JSON.php");
You also need to set several headers:
header("Content-Type: text/plain"); header("Cache-Control: No-Cache"); header("Pragma: No-Cache");
The first header sets the Content-Type to text/plain, a requirement because this page returns a JSON-encoded string as opposed to HTML or XML. Because this file will be used repeatedly, you must include the No-Cache headers described in Chapter 3 to avoid incorrect data.
When using AjaxMailAction.php, at least three pieces of information are sent: the action to perform, the current folder ID, and the page number. An optional fourth piece of information, a message ID, can be sent as well. So, the query string for this file may look something like this:
AjaxMailAction.php?action=myAction&page=1&folder=1&id=123
Because the message ID is used only in certain circumstances, you don't have to retrieve it until needed. In the meantime, you can retrieve the three other arguments as follows:
$folder = $_GET["folder"]; $page = (int) $_GET["page"]; $action = $_GET["action"];
This code retrieves the values of the variables in the query string. The page number is cast to an integer value for compatibility with methods in AjaxMailbox. Next, create an instance of AjaxMailbox and JSON, as well as a variable named $output, which will be filled with a JSON string:
$mailbox = new AjaxMailbox(); $oJSON = new JSON(); $output = "";
The next step is to perform the desired action. Using a switch statement on the $action enables you to easily determine what should be done. There are two actions that need the message ID argument, delete and restore:
switch($action) { case "delete": $mailbox->deleteMessage($_GET["id"]); break; case "restore": $mailbox->restoreMessage($_GET["id"]); break; case "empty": $mailbox->emptyTrash(); break; case "getfolder": //no extra processing needed break; }
This code performs a specific operation based on the $action string. In the case of delete, the deleteMessage() method is called and the message ID parameter is passed in. For restore, the restoreMessage() method is called with the message ID. If empty is the action, the emptyTrash() method is called. Otherwise, if the action is getfolder, no additional operation is required. This is because AjaxMailAction.php always returns JSON-encoded folder information regardless of the action that is performed:
$info = $mailbox->getFolderPage($folder, $page); $output = $oJSON->encode($info); echo $output;
Here, the getFolderPage() method is used to retrieve a list of e-mails to return to the client. Remember, getFolderPage() checks for new messages before returning a list, so you will have the most recent information. The result of getFolderPage() is encoded using $oJSON->encode() and then output to the client using the echo operator.
AjaxMail uses both XMLHttp and a hidden iframe to make requests back to the server. The AjaxMailNavigate.php file is used inside the hidden iframe and, as such, must contain valid HTML and JavaScript code. This file expects the same query string as AjaxMailAction.php because it uses the same information.
The first part of this file is the PHP code that performs the requested action:
<?php require_once("inc/config.inc.php"); require_once("inc/pop3lib/pop3.class.php"); require_once("inc/pop3lib/pop3message.class.php"); require_once("inc/pop3lib/pop3header.class.php"); require_once("inc/pop3lib/pop3attachment.class.php"); require_once("inc/AjaxMail.inc.php"); require_once("inc/JSON.php"); header("Cache-control: No-Cache"); header("Pragma: No-Cache"); $folder = $_GET["folder"]; $page = (int) $_GET["page"]; $id = ""; if (isset($_GET["id"])) { $id = (int) $_GET["id"]; } $action = $_GET["action"]; $mailbox = new AjaxMailbox(); $oJSON = new JSON(); $output = ""; switch($action) { case "getfolder": $info = $mailbox->getFolderPage($folder, $page); $output = $oJSON->encode($info); break; case "getmessage": $message = $mailbox->getMessage($id); if ($message->unread) { $mailbox->markMessageAsRead($id); } $output = $oJSON->encode($message); break; default: $output = "null"; } ?>
This file requires the same include files as AjaxMailAction.php, although it needs only the no cache headers because the content being returned is HTML (not plain/text). Next, the information is pulled out of the query string and stored in variables. New instances of AjaxMailbox and JSON are created in anticipation of performing an action.
As with AjaxMailAction.php, the $action variable is placed into a switch statement to determine what to do. The getfolder action calls getFolderPage() to retrieve the information for the given page in the given folder. The result is JSON-encoded and stored in the $output variable.
If the action is getmessage, the getMessage() method is called. If the message hasn't been read, it is marked as read. The message is then JSON-encoded and assigned to the $output variable. If the $action is something else, $output is assigned a value of null.
The next part of the page is the HTML content:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns=" http://www.w3.org/1999/xhtml" xml:lang=" en" lang=" en"> <head> <title>Ajax Mail Navigate</title> </head> <body> <script language=" JavaScript" type=" text/javascript"> //<![CDATA[ window.onload = function () { var oInfo = <?php echo $output ?>; <?php switch($action) { case "getfolder": echo "parent.oMailbox.displayFolder(oInfo);"; break; case "getmessage": echo "parent.oMailbox.displayMessage(oInfo);"; break; case "compose": echo "parent.oMailbox.displayCompose();"; break; case "reply": echo "parent.oMailbox.displayReply();"; break; case "replyall": echo "parent.oMailbox.displayReplyAll();"; break; case "forward": echo "parent.oMailbox.displayForward();"; break; } ?> }; //]]> </script> </body> </html>
In this part of the page, the $output variable is output to the page into the JavaScript variable oInfo. Because $output is either null or a JSON-encoded string, it is valid JavaScript. The variable is assigned in the window.onload event handler. Then, based on the $action, a different JavaScript method is output to the page and called.
To handle the sending of e-mail from the client, the AjaxMailSend.php file is used. Its sole purpose is to gather the information from the server and then send the e-mail. It needs to include config.inc.php, JSON.php, and AjaxMail.inc.php, as with the other files. However, it doesn't need to include the POP3Lib files because there will be no interaction with the POP3 server. Instead, the PHPMailer files class.phpmailer.php and class.smtp.php must be included:
<?php require_once("inc/config.inc.php"); require_once("inc/phpmailer/class.phpmailer.php"); require_once("inc/phpmailer/class.smtp.php"); require_once("inc/JSON.php"); require_once("inc/AjaxMail.inc.php"); header("Content-Type: text/plain"); header("Cache-control: No-Cache"); header("Pragma: No-Cache"); $to = $_POST["txtTo"]; $cc = $_POST["txtCC"]; $subject = $_POST["txtSubject"]; $message = $_POST["txtMessage"]; $mailbox = new AjaxMailbox(); $oJSON = new JSON(); $response = $mailbox->sendMail($to, $subject, $message, $cc); $output = $oJSON->encode($response); echo $output; ?>
You'll note that the same headers are set for this page as they are for AjaxMailAction.php because it will also return a JSON-encoded string. The next section gathers the information from the submitted form. Then, new instances of AjaxMailbox and JSON are created. The information from the form is passed into the sendMail() method, and the response is JSON-encoded and then output using echo.
The last file, AjaxMailAttachment.php, facilitates the downloading of a specific attachment. This file accepts a single query string parameter: the ID of the attachment to download. To do this, you need to once again include all the POP3Lib files, config.inc.php, and AjaxMail.inc.php:
<?php require_once("inc/config.inc.php"); require_once("inc/pop3lib/pop3.class.php"); require_once("inc/pop3lib/pop3message.class.php"); require_once("inc/pop3lib/pop3header.class.php"); require_once("inc/pop3lib/pop3attachment.class.php"); require_once("inc/AjaxMail.inc.php"); $id = $_GET["id"]; $mailbox = new AjaxMailbox(); $attachment = $mailbox->getAttachment($id); header("Content-Type: $attachment->contentType"); header("Content-Disposition: attachment; filename=$attachment->filename"); echo $attachment->data; ?>
After including the required files, the attachment ID is retrieved and stored in $id. A new AjaxMailbox object is created and getAttachment() is called to retrieve the specific attachment information. Next, the content-type header is set to the content-type of the attachment (retrieved from $attachment->contentType) and the content-disposition header is set to attachment, passing in the file name of the attachment. This second header does two things. First, it forces the browser to show a dialog box asking if you want to open the file or save it; second, it suggests the file name to use when downloading the file. The last part of the file outputs the attachment data to the page, effectively mimicking a direct file download.