//
// 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;
}
}
?>