Introduction

Before there was HTTP, there was FTP, NNTP, IMAP, POP3, and a whole alphabet soup of other protocols. Many people quickly embraced web browsers because the browser provided an integrated program that let them check their email, read newsgroups, transfer files, and view documents without worrying about the details surrounding the underlying means of communication. PHP provides functions, both natively and through PEAR, to use these other protocols. With them, you can use PHP to create web frontend applications that perform all sorts of network-enabled tasks, such as looking up domain names or sending web-based email. While PHP simplifies these jobs, it is important to understand the strengths and limitations of each protocol.

Section 17.2 to Section 17.4 cover the most popular feature of all: email. Section 17.2 shows how to send basic email messages. Section 17.3 describes MIME-encoded email, which enables you to send plain-text and HTML-formatted messages. The IMAP and POP3 protocols, which are used to read mailboxes, are discussed in Section 17.4.

The next two recipes discuss how to read newsgroups with NNTP. Newsgroups are similar to mailing lists, but instead of every person on the list receiving an email message, people can access a news server and view just the messages they're interested in. Newsgroups also allow threaded discussions, so its easy to trace a conversation through the archives. Section 17.5 discusses posting messages, while Section 17.6 covers retrieving messages.

Section 17.7 covers how to exchange files using FTP. FTP, or file transfer protocol, is a method for sending and receiving files across the Internet. FTP servers can require users to log in with a password or allow anonymous usage.

Searching LDAP servers is the topic of Section 17.8, while Section 17.9 discusses how to authenticate users against an LDAP server. LDAP servers are used as address books and as a centralized store for user information. They're optimized for information retrieval and can be configured to replicate their data to ensure high reliability and quick response times.

The chapter concludes with recipes on networking. Section 17.10 covers DNS lookups, both from domain name to IP and vice versa. The final recipe tells how to check if a host is up and accessible with PEAR's Ping module.

Other parts of the book deal with some network protocols as well. HTTP is covered in detail in Chapter 11. Those recipes discuss how to fetch URLs in a variety of different ways. Protocols that combine HTTP and XML are covered in Chapter 12. In that chapter, along with covering DOM and XSLT, we discuss the emerging area of web services, using the XML-RPC and SOAP protocols.

Sending Mail

Problem

You want to send an email message. This can be in direct response to a user's action, such as signing up for your site, or a recurring event at a set time, such as a weekly newsletter.

One good value for sendmail_path is /usr/lib/sendmail. Unfortunately, sendmail tends to jump around from system to system, so it can be hard to track down. If you can't find it, try /usr/sbin/sendmail or ask your system administrator.

Two useful flags to pass sendmail are -oi and -t. The -oi flag tells sendmail not to think a single dot (.) on a line is the end of the message. The -t flag makes sendmail parse the file for To: and other header lines.

If you prefer qmail, try using /var/qmail/bin/qmail-inject or /var/qmail/bin/sendmail.

If you're running Windows, you may want to use an SMTP server because most Windows machines don't have copies of sendmail installed. To do so, pass smtp:

In smtp mode, you can pass five optional parameters. The host is the SMTP server hostname; it defaults to localhost. The port is the connection port; it defaults to 25. To enable SMTP authentication, set auth to true. To allow the server to validate you, set username and password. SMTP functionality isn't restricted to Windows; it also works on Unix servers.

If you don't have PEAR's Mail class, you can use the built-in mail( ) function. The program mail( ) uses to send mail is specified in the sendmail_path configuration variable in your php.ini file. If you're running Windows, set the SMTP variable to the hostname of your SMTP server. Your From address comes from the sendmail_from variable.

The first parameter is the recipient's email address, the second is the message subject, and the last is the message body. You can also add extra headers with an optional fourth parameter. For example, here's how to add Reply-To and Organization headers:

Separate each header with \r\n, but don't add \r\n following the last header.

Regardless of which method you choose, it's a good idea to write a wrapper function to assist you in sending mail. Forcing all your mail through this function makes it easy to add logging and other checks to every message sent:

Here a message is written to the error log, recording the recipient of each message that's sent. This provides a time stamp that allows you to more easily track complaints that someone is trying to use the site to send spam. Another option is to create a list of "do not send" email addresses, which prevent those people from ever receiving another message from your site. You can also validate all recipient email addresses, which reduces the number of bounced messages.

Discussion

PEAR's Mail_mime class provides an object-oriented interface to all the behind-the-scenes details involved in creating an email message that contains both text and HTML parts. The class is similar to PEAR's Mail class, but instead of defining the body as a string of text, you create a Mail_mime object and call its methods to add parts to the body:

The Mail_mime::setTXTBody( ) and Mail_mime::setHTMLBody( ) methods add the plaintext and HTML body parts, respectively. Here, we pass in variables, but you can also pass a filename for Mail_mime to read. To use this option, pass true as the second parameter:

$text = '/path/to/email.txt';
$mime->setTXTBody($text, true);

To add an attachment to the message, such as a graphic or an archive, call Mail_mime::addAttachment( ) :

$file = '/path/to/file.png';
$mime->addAttachment($file,'image/png');

Pass the function to the location to the file and its MIME type.

Once the message is complete, do the final preparation and send it out:

Discussion

The underlying library PHP uses to support IMAP and POP3 offers a seemingly unending number of features that allow you to essentially write an entire mail client. With all those features, however, comes complexity. In fact, there are currently 63 different functions in PHP beginning with the word imap, and that doesn't take into account that some also speak POP3 and NNTP.

However, the basics of talking with a mail server are straightforward. Like many features in PHP, you begin by opening the connection and grabbing a handle:

$mail = imap_open('{mail.server.com:143}', 'username', 'password');

This opens an IMAP connection to the server named mail.server.com on port 143. It also passes along a username and password as the second and third arguments.

To open a POP3 connection instead, append /pop3 to the end of the server and port. Since POP3 usually runs on port 110, add :110 after the server name:

To encrypt your connection with SSL, add /ssl on to the end, just as you did with pop3. You also need to make sure your PHP installation is built with the --with-imap-ssl configuration option in addition to --with-imap. Also, you need to build the system IMAP library itself with SSL support. If you're using a self-signed certificate and wish to prevent an attempted validation, also add /novalidate-cert. Finally, most SSL connections talk on either port 993 or 995. All these options can come in any order, so the following is perfectly legal:

Surrounding a variable with curly braces inside of a double-quoted string, such as {$var}, is a way to tell PHP exactly which variable to interpolate. Therefore, to use interpolated variables in this first parameter to imap_open( ) , escape the opening {:

The imap_header( ) function returns an object with many fields. Useful ones include subject, fromaddress, and udate. All the fields are listed in Table 17-2 in Section 17.6.

The body element is just a string, but, if the message is a multipart message, such as one that contains both a HTML and a plain-text version, $body holds both parts and the MIME lines describing them:

If a message has multiple parts, $st->parts holds an array of objects describing them. The part property holds an integer describing the main body MIME type. Table 17-1 lists which numbers go with which MIME types. The subtype property holds the MIME subtype and tells if the part is plain, html, png, or another type, such as octet-stream.

Discussion

No built-in PHP functions can post a message to a newsgroup. Therefore, you must open a direct socket connection to the news server and send the commands to post the message. However, you can use imap_mail_compose( ) to format a post and create the headers and body for the message. Every message must have three headers: the From: address, the message Subject:, and the name of the newsgroup:

Create an array, $headers, to hold the message headers. You can directly assign the values for the From: and Subject: headers, but you can't do so for the Newsgroups: header. Because imap_mail_compose( ) is most frequently used to create email messages, the Newsgroups: header is not a predefined header. To work around this, you must instead add it with the custom_headers array element.

There is a different syntax for the custom_headers. Instead of placing the lowercase header name as the element name and the header value as the array value, place the entire header as an array value. Between the header name and value, add a colon followed by a space. Be sure to correctly spell Newsgroups: with a capital N and final s.

The message body can contain multiple parts. As a result, the body parameter passed to imap_mail_compose( ) is an array of arrays. In the Solution, there was only one part, so we directly assign values to $body[0]:

Each message part needs a MIME type and subtype. This message is ASCII, so the type is TYPETEXT, and the subtype is plain. Refer back to Table 17-1 in Section 17.4 for a listing of IMAP MIME type constants and what they represent. The contents.data field holds the message body.

To convert these arrays into a formatted string call imap_mail_compose($body, $headers) . It returns a post that looks like this:

The first parameter to fsockopen( ) is the hostname of the server, and the second is the port to use. If you don't know the name of your news server, try the hostnames news, nntp, or news-server in your domain: for example, news.example.com, nntp.example.com, or news-server.example.com. If none of these work, ask your system administrator. Traditionally, all news servers use port 119.

The first line tells the news server that you want to post a message. The second is the message itself. To signal the end of the message, place a period on a line by itself. Every line must have both a carriage return and a newline at the end. Close the connection by calling fclose($sh) .

Every message on the server is given a unique name, known as a Message-ID . If you want to reply to a message, take the Message-ID of the original message and use it as the value for a References header:

The function imap_open( ) takes four parameters. The first specifies the news server to use and the newsgroup to read. The server here is news.php.net , the news server that mirrors all the PHP mailing lists. Add /nntp to let the IMAP extension know you're reading news instead of mail, and specify 119 as a port; that's typically the port reserved for NNTP. NNTP stands for Network News Transport Protocol; it's used to communicate with news servers, just as HTTP communicates with web servers. The group is php.general, the main mailing list of the PHP community.

The middle two arguments to imap_open( ) are a username and password, in case you need to provide verification of your identity. Because news.php.net is open to all readers, leave them blank. Finally, pass the flag OP_ANONYMOUS, which tells IMAP you're an anonymous reader; it will not then keep a record of you in a special .newsrc file.

Once you're connected, you usually want to either get a general listing of recent messages or all the details about one specific message. Here's some code that displays recent messages:

To browse a listing of posts, you need to specify what you want by number. The first post ever to a group gets number 1, and the most recent post is the number returned from imap_num_msg( ). So, to get the last $n messages, loop from $last-$n+1 to $last.

Inside the loop, call imap_header( ) to pull out the header information about a post. The header contains all the metainformation but not the actual text of the message; that's stored in the body. Because the header is usually much smaller than the body, this allows you to quickly retrieve data for many posts without taking too much time.

Now pass imap_header( ) two parameters: the server connection handle and the message number. It returns an object with many properties, which are listed in Table 17-2.

The address you should reply to, if you're trying to contact the author

String

rjosephs@example.net

reply_to

Parsed version of reply_toaddress field

Object

Mailbox: "rjosephs", host: "example.net"

senderaddress

The person who sent the message; almost always identical to the from field, but if the from field doesn't uniquely identify who sent the message, this field does

String

Ralph Josephs <ralph@example.net>

sender

Parsed version of senderaddress field

Object

Personal: "Ralph Josephs", mailbox: "ralph", host: "example.net"

Recent

If the message is recent, or new since the last time the user checked for mail

String

Y or N

Unseen

If the message is unseen

String

Y or " "

Flagged

If the message is marked

String

Y or " "

Answered

If a reply has been sent to this message

String

Y or " "

Deleted

If the message is deleted

String

Y or " "

Draft

If the message is a draft

String

Y or " "

Size

Size of the message in bytes

String

1345

udate

Unix timestamp of message date

Int

1013480645

Mesgno

The number of the message in the group

String

34943

Some of the more useful fields are: size, subject, the from list, and udate. The size property is the size of the message in bytes; if it's 0, the message was either deleted or otherwise removed. The subject field is the subject of the post. The from list is more complicated. It's an array of objects; each element in the array holds an object with three properties: personal, mailbox and host. The personal field is the name of the poster: Homer Simpson. The mailbox field is the part of the email address before the @ sign: homer. The host is the part of the email address after the @ sign: thesimpsons.com. Usually, there's just one element in the from list array, because a message usually has just one sender.

Pull the $header->from object into $from because PHP can't directly access $header->from[0]->personal due to the array in the middle. Then combine $from[0]->mailbox and $from[0]->host to form the poster's email address. Use the ternary operator to assign the personal field as the poster's name, if one is supplied; otherwise, make it the email address.

The udate field is the posting time as an Unix timestamp. Use date( ) to convert it from seconds to a more human-friendly format.

The code to grab a single message is similar to one that grabs a sequence of message headers. The main difference is that you define a $body variable that's the result of three chained functions. Innermost, you call imap_fetchbody( ) to return the message body; it takes the same parameters as imap_header( ). You pass that to htmlspecialchars( ) to escape any HTML that may interfere with yours. That result then is passed to nl2br( ) , which converts all the carriage returns to XHTML <br /> tags; the message should now look correct on a web page.

To disconnect from the IMAP server and close the stream, pass the IMAP connection handle to imap_close( ) :

Discussion

FTP stands for File Transfer Protocol and is a method of exchanging files between one computer and another. Unlike with HTTP servers, it's easy to set up an FTP server to both send and receive files.

Using the built-in FTP functions doesn't require additional libraries, but you must specifically enable them with --enable-ftp . Because these functions are specialized to FTP, they're easy to use when transferring files.

All FTP transactions begin with establishing a connection from your computer, the local client, to another computer, the remote server:

$c = ftp_connect('ftp.example.com') or die("Can't connect");

Once connected, you need to send your username and password; the remote server can then authenticate you and allow you to enter:

ftp_login($c, $username, $password) or die("Can't login");

Some FTP servers support a feature known as anonymous FTP. Under anonymous FTP, users can log in without an account on the remote system. When you use anonymous FTP, your username is anonymous, and your password is your email address.

The ftp_put( ) function takes a file on your computer and copies it to the remote server; ftp_get( ) copies a file on the remote server to your computer. In the previous code, $remote is the pathname to the remote file, and $local points at the file on your computer.

There are two final parameters passed to these functions. The FTP_ASCII parameter, used here, transfers the file as if it were ASCII text. Under this option, linefeed endings are automatically converted as you move from one operating system to another. The other option is FTP_BINARY , which is used for nonplaintext files, so no linefeed conversions take place.

Use ftp_fget( ) and ftp_fput( ) to download or upload a file to an existing open file pointer (opened using fopen( )) instead of to a location on the filesystem. For example, here's how to retrieve a file and write it to the existing file pointer, $fp:

Finally, to disconnect from the remote host, call ftp_close( ) to log out:

ftp_close($c); or die("Can't close");

To adjust the amount of seconds the connection takes to time out, use ftp_set_option( ) :

// Up the time out value to two minutes:
set_time_limit(120)
$c = ftp_connect('ftp.example.com');
ftp_set_option($c, FTP_TIMEOUT_SEC, 120);

The default value is 90 seconds; however, the default max_execution_time of a PHP script is 30 seconds. So, if your connection times out too early, be sure to check both values.

To use the cURL extension, you must download cURL from http://curl.haxx.se/ and set the --with-curl configuration option when building PHP. To use cURL, start by creating a cURL handle with curl_init( ) , and then specify what you want to do using curl_setopt( ). The curl_setopt( ) function takes three parameters: a cURL resource, the name of a cURL constant to modify, and value to assign to the second parameter. In the Solution, the CURLOPT_FILE constant is used:

You pass the URL to use to curl_init( ). Because the URL begins with ftp://, cURL knows to use the FTP protocol. Instead of a separate call to log on to the remote server, you embed the username and password directly into the URL. Next, you set the location to store the file on your server. Now you open a file named $local for writing and pass the file handle to curl_setopt( ) as the value for CURLOPT_FILE. When cURL transfers the file, it automatically writes to the file handle. Once everything is configured, you call curl_exec( ) to initiate the transaction and then curl_close( ) to close the connection.

Discussion

LDAP stands for Lightweight Directory Access Protocol. An LDAP server stores directory information, such as names and addresses, and allows you to query it for results. In many ways, it's like a database, except that it's optimized for storing information about people.

In addition, instead of the flat structure provided by a database, an LDAP server allows you to organize people in a hierarchical fashion. For example, employees may be divided into marketing, technical, and operations divisions, or they can be split regionally into North America, Europe, and Asia. This makes it easy to find all employees of a particular subset of a company.

When using LDAP, the address repository is called as a data source. Each entry in the repository has a globally unique identifier, known as a distinguished name. The distinguished name includes both a person's name, but also their company information. For instance, John Q. Smith, who works at Example Inc., a U.S. company has a distinguished name of cn=John Q. Smith, o=Example Inc., c=US. In LDAP, cn stands for common name, o for organization, and c for country.

Passing only the connection handle, $ds, to ldap_bind( ) does an anonymous bind. To bind with a specific username and password, pass them as the second and third parameters, like so:

ldap_bind($ds, $username, $password) or die($php_errormsg);

Once logged in, you can request information. Because the information is arranged in a hierarchy, you need to indicate the base distinguished name as the second parameter. Finally, you pass in the search criteria. For example, here's how to find all people with a surname of Jones at company Example Inc. located in the country US:

Instead of doing count($e), use the precomputed record size located in $e['count']. Inside the loop, print the first common name and email address for each record. For example:

David Sklar (sklar@example.com)
Adam Trachtenberg (adam@example.com)

The ldap_search( ) function searches the entire tree equal to and below the distinguished name base. To restrict the results to a specific level, use ldap_list( ) . Because the search takes place over a smaller set of records, ldap_list( ) can be significantly faster than ldap_search( ).

See Also

Using LDAP for User Authentication

Problem

You want to restrict parts of your site to authenticated users. Instead of verifying people against a database or using HTTP Basic authorization, you want to use an LDAP server. Holding all user information in an LDAP server makes centralized user administration easier.

Discussion

LDAP servers are designed for address storage, lookup, and retrieval, and so are better to use than standard databases like MySQL or Oracle. LDAP servers are very fast, you can easily implement access control by granting different permissions to different groups of users, and many different programs can query the server. For example, most email clients can use an LDAP server as an address book, so if you address a message to "John Smith," the server replies with John's email address, jsmith@example.com.

PEAR's Auth class allows you to validate users against files, databases, and LDAP servers. The first parameter is the type of authentication to use, and the second is an array of information on how to validate users. For example:

This creates a new Auth object that validates against an LDAP server located at ldap.example.com and communicates over port 389. The base directory name is o=Example Inc., c=US, and usernames are checked against the uid attribute. The uid field stands for user identifier. This is normally a username for a web site or a login name for a general account. If your server doesn't store uid attributes for each user, you can substitute the cn attribute. The common name field holds a user's full name, such as "John Q. Smith."

The Auth::auth( ) method also takes an optional third parameter — the name of a function that displays the sign-in form. This form can be formatted however you wish; the only requirement is that the form input fields must be called username and password. Also, the form must submit the data using POST.

Once the Auth object is instantiated, authenticate a user by calling Auth::start( ) :

$auth->start();

If the user is already signed in, nothing happens. If the user is anonymous, the sign-in form is printed. To validate a user, Auth::start( ) connects to the LDAP server, does an anonymous bind, and searches for an address in which the user attribute specified in the constructor matches the username passed in by the form:

$options['userattr'] = = $_POST['username']

If Auth::start( ) finds exactly one person that fits this criteria, it retrieves the designated name for the user, and attempts to do an authenticated bind, using the designated name and password from the form as the login credentials. The LDAP server then compares the password to the userPassword attribute associated with the designated name. If it matches, the user is authenticated.

You can call Auth::getAuth( ) to return a boolean value describing a user's status:

Solution

Discussion

You can't trust the name returned by gethostbyaddr( ) . A DNS server with authority for a particular IP address can return any hostname at all. Usually, administrators set up DNS servers to reply with a correct hostname, but a malicious user may configure her DNS server to reply with incorrect hostnames. One way to combat this trickery is to call gethostbyname( ) on the hostname returned from gethostbyaddr( ) and make sure the name resolves to the original IP address.

If either function can't successfully look up the IP address or the domain name, it doesn't return false, but instead returns the argument passed to it. To check for failure, do this:

if ($host == ($ip = gethostbyname($host))) {
// failure
}

This assigns the return value of gethostbyname( ) to $ip and also checks that $ip is not equal to the original $host.

Sometimes a single hostname can map to multiple IP addresses. To find all hosts, use gethostbynamel( ) :

Discussion

The ping program tries to send a message from your machine to another. If everything goes well, you get a series of statistics chronicling the transaction. An error means that ping can't reach the host for some reason.

On error, Net_Ping::checkhost( ) returns false, and Net_Ping::ping( ) returns the constant PING_HOST_NOT_FOUND. If there's a problem running the ping program (because Net_Ping is really just a wrapper for the program), PING_FAILED is returned.

This regular expression searches for either a space or a slash. It then captures a sequence of one or more numbers and a decimal point. To avoid escaping /, we use the # nonstandard character as your delimiter.