// // This program is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by the // Free Software Foundation, either version 3 of the License, or (at your // option) any later version. // // This program is distributed in the hope that it will be useful, but // *WITHOUT ANY WARRANTY*; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General // Public License for more details. // // You should have received a copy of the GNU General Public License along // with this program. If not, see . // Topic: Description // // Facilitates the creation of MIME compatible messages. It has useful // features like easy creation of alternative bodies (i.e. plain+html) and // multiple file attachments. It is *not* a complete implementation, but it // is very small (about 5kb without comments) which is a big plus for my // needs. // // Example: // // (begin example) // require_once 'email.inc'; // mb_internal_encoding('UTF-8'); // $msg = new email(); // $msg->charset = 'UTF-8'; // $msg->to = "Someone "; // $msg->from = "Myself "; // $msg->subject = "Friendly reminder service"; // $msg->addText("Hello Someone,\n\nThis is your friendly reminder.\n"); // $msg->addFile('image/png', '/tmp/test-file.png', 'reminder.png'); // $msg->send(); // (end example) // // Basically, you instantiate the class, set a few headers, add // some content parts (at least one) and either build to a string using // or send using PHP's mail() with . // Private Group: Private Properties // Private Property: bpRoot // // Used by to output MIME greeting only once. var $bpRoot; // Private Property: envelope // // Arrayhash of headers. Values are not encoded here. var $envelope; // Private Property: parts // // Indexed array of parts. Each part is an arrayhash with plenty of header // information possibly included (see for a full list) and // a 'data' key with either a string or another indexed array to represent // sub-parts. var $parts; // Group: Properties // Property: charset // // Character set to assume for all text parts attached. Default: 'UTF-8'. // Be sure to explicitly set PHP's mb_internal_encoding() to the same // character set as this property, or else headers will not be encoded // properly. var $charset; // Property: Overloading // // To define any header, set a property of the same name. If the header // name contains dashes, use underscores instead and they will be converted // to dashes. For example: // // > $msg = new email(); // > $msg->X_Mailer_Info = "My Custom Mailer v0.15"; // Private Group: Private Methods // Private Method: __set // // Property overloading. Replaces underscores with dashes, converts to // lowercase and adds to . function __set($name, $value) { $this->envelope[strtr(strtolower($name),'_','-')] = $value; } // Private Method: qBestEncoding // // Attempt Q and B encodings, return shortest with preference for Q. // // Parameters: // s - String to encode. // // Returns: // The encoded string, in the shortest of 'Q' or 'B' encoding, or 'Q' // encoding in the event of a tie. function qBestEncoding($s) { $q = mb_encode_mimeheader($s, $this->charset, 'Q', "\n"); $b = mb_encode_mimeheader($s, $this->charset, 'B', "\n"); return (strlen($q) > strlen($b)) ? $b : $q; } // Private Method: qEncode // // Encode a header part with max-length in mind. PHP's // mb_encode_mimeheader() used by splits lines to stay // within 75 characters wide. This doesn't work with our // which takes care of encoding the shortest pieces possible, instead of // PHP's default behavior of encoding the entire header or nothing. // // As a work-around, this wrapper is given a prefix which should be the // header line(s), including name, prior to the portion to be encoded. A // bogus prefix is added temporarily to PHP's mb_encode_mimeheader() and is // subsequently removed to guarantee that the final header line(s) will not // exceed 75 characters. // // Parameters: // prefix - The header string so far. // string - The new portion to be encoded. // // Returns: // The encoded version of /string/ with the shortest of 'Q' or 'B' // encoding, or 'Q' encoding in the event of a tie. function qEncode($prefix, $string) { if (strlen($prefix) < 68 && strpos($prefix, "\n") === false) { $p = str_repeat('X', strlen($prefix)-1) . ': '; return substr($this->qBestEncoding($p . $string), strlen($p)); } else { return $this->qBestEncoding($string); }; } // Private Method: headerEncode // // Prepare a header line for RFC822 compliance. The header's value is // encoded if high-ASCII characters are found, for the shortest portions // possible (not splitting words). Addresses are never encoded. A maximum // width of 75 characters is respected. // // Parameters: // name - The header's name (left of ':'.) // value - THe header's full value. // // Returns: // String which should be RFC822-compliant (maximum width, encoding...) function headerEncode($name, $value) { $out = ucfirst($name) . ':'; $words = explode(' ', $value); $bin = ''; $tmp = ''; foreach ($words as $word) { if ($bin) { if (ctype_print($word)) { if ($word[0] == '<') { $out .= ' ' . $this->qEncode($out, $bin) . $tmp . ' ' . $word; $bin = ''; $tmp = ''; } else { $tmp .= ' ' . $word; }; } else { $bin .= $tmp . ' ' . $word; $tmp = ''; }; } else { if (ctype_print($word)) { $out .= ' ' . $word; } else { $bin = $word; }; }; }; if ($bin) { if ($out[strlen($out)-1] != ' ') $out .= ' '; $out .= $this->qEncode($out, $bin) . $tmp; }; return wordwrap($out,75,"\n "); } // Private Method: mkPart // // Add useful defaults to a part arrayhash. Specifically, it adds character // set information if it's missing. It also adds "filename" if "name" is // supplied but not "filename". Finally, if "name" is supplied and type // isn't "message/*", "data" is encoded in Base64, "encoding" is changed // accordingly, and "disposition" is set to "attachment". // // Parameters: // part - The arrayhash to augment. // // Returns: // Essentially the input /part/ with possibly some defaults added. function mkPart($part) { if (preg_match('/^text\//i', $part['type'])) { if (!isset($part['charset'])) $part['charset'] = $this->charset; } elseif (!preg_match('/^message\//i', $part['type'])) { $part['data'] = chunk_split(base64_encode($part['data'])); $part['encoding'] = 'base64'; $part['disposition'] = 'attachment'; if (!isset($part['filename']) && isset($part['name'])) $part['filename'] = $part['name']; }; return $part; } // Private Method: buildParts // // Build body parts of message. Invoked by after headers are // created. This is the core of the multipart support. It is called // recursively if sub-parts are encountered in the source structure. // // Parameters: // parts - Array of parts to output. // type - Content type of the current context. // // Returns: // String of the MIME compliant body contents. function buildParts($parts, $type = 'multipart/mixed') { $out = ''; $boundary = uniqid('PHPemail'); if (count($parts) > 1) { $out .= "Content-Type: {$type}; boundary=\"$boundary\"\n\n"; if ($this->bpRoot) { $out .= "This is a multipart message in MIME format.\n\n"; $this->bpRoot = false; }; $out .= '--' . $boundary; foreach($parts as $part) { if (is_array($part['data'])) { $out .= "\n" . $this->buildParts($part['data'], $part['type']); } else { $out .= "\nContent-Type: {$part['type']}"; if (isset($part['charset'])) { $out .= "; charset=\"{$part['charset']}\""; }; if (isset($part['name'])) { $out .= "; name=\"{$part['name']}\""; }; if (isset($part['disposition'])) { $out .= "\nContent-Disposition: {$part['disposition']}"; if (isset($part['filename'])) { $out .= "; filename=\"{$part['filename']}\""; }; }; if (isset($part['encoding'])) { $out .= "\nContent-Transfer-Encoding: {$part['encoding']}"; } else { $out .= "\nContent-Transfer-Encoding: 8bit"; }; $out .= "\n\n" . $part['data']; }; $out .= "\n--{$boundary}"; }; $out .= "--\n"; } elseif (count($parts) == 1) { if (is_array($parts[0]['data'])) { $out .= $this->buildParts($parts[0]['data'], $parts[0]['type']); } else { $out .= "Content-Type: {$parts[0]['type']}"; if (isset($parts[0]['charset'])) { $out .= "; charset=\"{$parts[0]['charset']}\""; }; if (isset($parts[0]['name'])) { $out .= "; name=\"{$parts[0]['name']}\""; }; if (isset($parts[0]['disposition'])) { $out .= "\nContent-Disposition: {$parts[0]['disposition']}"; if (isset($parts[0]['filename'])) { $out .= "; filename=\"{$parts[0]['name']}\""; }; }; if (isset($parts[0]['encoding'])) { $out .= "\nContent-Transfer-Encoding: {$part[0]['encoding']}"; } else { $out .= "\nContent-Transfer-Encoding: 8bit"; }; $out .= "\n\n{$parts[0]['data']}"; }; }; return $out; } // Group: Methods // Constructor: email // // Instantiate the class. function email() { $this->envelope = Array(); $this->parts = Array(); $this->MIME_Version = '1.0'; $this->charset = 'UTF-8'; $this->bpRoot = true; } // Method: addData // // Add a raw data part to message. // // Netiquette: // // You should add text and HTML parts before binary file attachments. // // Parameters: // type - MIME type of the attachment. (i.e. 'text/plain') // displayname - File name to suggest to reader. // data - Actual raw data to include. // // Returns: // No return value. function addData($type, $displayname, $data) { $this->parts[] = $this->mkPart(Array( 'type' => $type, 'name' => $displayname, 'data' => $data)); } // Method: addFile // // Attach a file to message. // // Netiquette: // // You should add text and HTML parts before binary file attachments. // // Parameters: // type - MIME type of the attachment. (i.e. 'image/png') // filepath - Path on local file system. // displayname - File name to suggest to reader. // // Returns: // No return value. function addFile($type, $filepath, $displayname) { $this->addData($type, $displayname, file_get_contents($filepath)); } // Method: addText // // Attach plain text to message. // // Parameters: // text - String of text to attach. // // Returns: // No return value. function addText($text) { $this->parts[] = $this->mkPart(Array( 'type' => 'text/plain', 'data' => $text)); } // Method: addHTML // // Attach HTML to message. // // Parameters: // html - String of HTML to attach. // // Returns: // No return value. function addHTML($html) { $this->parts[] = $this->mkPart(Array( 'type' => 'text/html', 'data' => $html)); } // Method: addTextHTML // // Add a pair of text and HTML equivalents to message. This implements the // "multipart/alternative" type so viewers can expect the text and HTML to // represent the same contents. // // Parameters: // text - The plain text string. // html - The HTML string. // // Returns: // No return value. function addTextHTML($text, $html) { $this->parts[] = Array( 'type' => 'multipart/alternative', 'data' => Array( $this->mkPart(Array( 'type' => 'text/plain', 'data' => $text )), $this->mkPart(Array( 'type' => 'text/html', 'data' => $html )) ) ); } // Method: build // // Build message to a string. // // Caveats: // // If you intend to use PHP's mail(), you will need to split headers from // the body yourself since PHP needs headers separately. Something like // this: // // (begin example) // // Assuming your email is $msg: // $parts = preg_split('/\r?\n\r?\n/', $msg->build(true), 2); // mail($msg->getTo(), $msg->getSubject(), $parts[1], $parts[0]); // (end example) // // Parameters: // skipTS - Skip "To:" and "Subject:" header fields. Useful if you intend // to use PHP's mail(). // // Returns: // String containing the entire message ready to send (i.e. via sendmail.) function build($skipTS = false) { $out = ''; foreach($this->envelope as $name => $value) { if (!($skipTS && (($name == 'to') || ($name == 'subject')))) { $out .= $this->headerEncode($name, $value) . "\n"; }; }; $this->bpRoot = true; $out .= $this->buildParts($this->parts); return $out; } // Method: send // // Build and immediately send message. Note that you can modify some // headers and call or again on the current message. // This can be handy for mailing lists where only the destination changes // (and where using the "Bcc" field isn't appropriate, that is.) // // Internally, this uses PHP's popen() to invoke your PHP configuration's // "sendmail_path" directly. This avoids the extra overhead and formatting // limitations of PHP's built-in mail(). // // Returns: // FALSE if the pipe could not be opened, the termination status of the // sendmail process otherwise. function send() { $result = false; if ($fp = popen(ini_get('sendmail_path'), 'w')) { fwrite($fp, $this->build()); $result = pclose($fp); }; return $result; } } ?>