diff options
Diffstat (limited to 'it_mail.class')
-rw-r--r-- | it_mail.class | 483 |
1 files changed, 483 insertions, 0 deletions
diff --git a/it_mail.class b/it_mail.class new file mode 100644 index 0000000..f10e84d --- /dev/null +++ b/it_mail.class @@ -0,0 +1,483 @@ +<?php +/* +** $Id$ +** +** mail.class - Create a mail object, add header and content and send it +** +** ITools - the Internet Tools Library +** +** Copyright (C) 1995-2003 by the ITools Authors. +** This program is free software; you can redistribute it and/or +** modify it under the terms of either the GNU General Public License +** or the GNU Lesser General Public License, as published by the Free +** Software Foundation. See http://www.gnu.org/licenses/ for details. +*/ + +define('IT_MAIL_PLAIN', 0); +define('IT_MAIL_HTML', 1); + +/* Return codes of it_mail::check_email() */ +define('IT_MAIL_CHECKEMAIL_INVALID', 0); +define('IT_MAIL_CHECKEMAIL_MAILBOXFULL', 5); +define('IT_MAIL_CHECKEMAIL_OK', 10); + +/** + * Construct an email message with headers, body (plaintext and/or HTML) and + * send it. Also provides utility function to check email address validity.<br> + * <em>Example:</em><br> + * <code><nobr> + * if (!it_mail::check_email($to))<br> + * die("Invalid email address '$to'\n");<br> + * <br> + * $mail = new it_mail(array('To' => $to, 'From' => 'itools@gna.ch', 'Subject' => 'Example'));<br> + * $mail->add_body('Plain text message body');<br> + * $mail->add_body('HTML message with <a href="http://gna.ch/">link</a>', IT_MAIL_HTML);<br> + * $mail->send();<br> + * </nobr></code> + */ +class it_mail +{ + var $header_names = array(); + var $header_values = array(); + var $body = array(); + var $attachments = array(); + + var $to = array(); + var $subject = ""; + var $cc = array(); + var $bcc = array(); + var $flags = ""; + var $charset = 'iso-8859-1'; + +/** + * Construct a new email message. Headers and body can be added later. + * Note: Headers To, Cc, Bcc can be arrays + * @param $headers Array of headers for this email, e.g. From, To and Subject + */ +function it_mail($headers = "") +{ + if (is_array($headers)) + { + foreach ($headers as $header => $value) + $this->add_header($header, $value); + } +} + + +/** + * Add header line to the this email message. Can be called repeatedly. + * Note: Headers To, Cc, Bcc can be arrays + * @param $header Header to be added, e.g. Cc, Bcc or X-My-Header + * @param $value Value of header, e.g. itools@gna.ch for Bcc header + */ +function add_header($header, $value) +{ + $value = $this->header_escape($value); + + switch ($header) + { + case 'To': + if (is_array($value)) + { + foreach ($value as $val) + $this->to[] = $val; + } + else + $this->to[] = $value; + break; + + case 'Subject': + $this->subject .= $value; + break; + + case 'Cc': + if (is_array($value)) + { + foreach ($value as $val) + $this->cc[] = $val; + } + else + $this->cc[] = $value; + break; + + case 'Bcc': + if (is_array($value)) + { + foreach ($value as $val) + $this->bcc[] = $val; + } + else + $this->bcc[] = $value; + break; + + case 'charset': + $this->charset = $value; + break; + + case 'Errors-To': + $this->flags = "-f " . escapeshellarg(preg_replace('/.*<([^>]+)>.*/', '$1', $value)); + /* FALLTHROUGH */ + default: + $this->header_names[] = $header; + $this->header_values[] = $value; + break; + } +} + + +/** + * Add body part to this email message. Can be called repeatedly. + * @param $text Text to be added to email message + * @param $type Type of text, one of IT_MAIL_PLAIN (default) or IT_MAIL_HTML + */ +function add_body($text, $type = IT_MAIL_PLAIN) +{ + switch ($type) + { + case IT_MAIL_PLAIN: + case IT_MAIL_HTML: + $this->body[$type] .= $text; + break; + + default: + it::fatal("it_mail::add_body invalid type $type"); + break; + } +} + + +/** + * Add attachment to this email message. Can be called repeatedly. + * @param $data Data to be attached + * @param $name Name of attached file + * @param $mimetype MIME-Type of attached file + * @return 'cid:'.Content-ID of this attachment + */ +function add_attachment($data, $mimetype = "application/octet-stream", $name = '') +{ + if ($name == '') + $name = 'Attachment' . (count($this->attachments) + 1); + + $cid = md5(uniqid(rand())); + $this->attachments[] = array ('mimetype' => $mimetype, 'data' => $data, 'encode' => $encode, 'name' => $name, 'cid' => $cid); + + return 'cid:'.$cid; +} + + +/** + * Add file attachment to this email message. Can be called repeatedly. + * @param $filename File to be attached + * @param $name Name of attached file as stored in mail + * @param $mimetype MIME-Type of attached file + */ +function add_file($filename, $mimetype = "application/octet-stream", $name = '') +{ + if ($name == '') + $name = basename($filename); + + if ($file = @fopen($filename, "r")) + { + if ($data = fread($file, @filesize($filename))) + $result = $this->add_attachment($data, $mimetype, $name); + + fclose($file); + } + + return $result; +} + + +/** + * Send this email message + * @return True if mail was sent + */ +function send() +{ + $to = join(",", $this->to); + $headers = array(); + + if (count($this->cc) > 0) + $headers[] = "Cc: " . join(",", $this->cc); + + if (count($this->bcc) > 0) + $headers[] = "Bcc: " . join(",", $this->bcc); + + for ($i = 0; $i < count($this->header_names); $i++) + $headers[] = $this->header_names[$i] . ': ' . $this->header_values[$i]; + + + /* Automatically add doctype if none given */ + if ($this->body[IT_MAIL_HTML] && !eregi('^<!doctype', $this->body[IT_MAIL_HTML])) + { + $this->body[IT_MAIL_HTML] = '<!doctype html public "-//w3c//dtd html 4.01 transitional//en">' . "\n" . $this->body[IT_MAIL_HTML]; + } + + + $headers[] = "MIME-Version: 1.0"; + + if ($this->attachments) + { + /* Attachments need multipart MIME mail */ + $boundary1 = md5(uniqid(rand())); + $mixedtype = "Content-Type: multipart/mixed; boundary=\"$boundary1\""; + + $headers[] = $mixedtype; + $text .= "This is a multi-part message in MIME format."; + $text .= "\n--$boundary1\n"; + } + + /* Headers for plain and HTML content */ + $plaintype = "Content-Type: text/plain; charset=".$this->charset."\nContent-Transfer-Encoding: 8bit"; + $htmltype = "Content-Type: text/html; charset=".$this->charset."\nContent-Transfer-Encoding: 8bit"; + + if ($this->body[IT_MAIL_PLAIN]) + { + if ($this->body[IT_MAIL_HTML]) + { + $boundary2 = md5(uniqid(rand())); + $alternativetype = "Content-Type: multipart/alternative; boundary=\"$boundary2\""; + + /* Plain and HTML */ + if ($this->attachments) + $text .= "$alternativetype\n\n"; + else + $headers[] = $alternativetype; + + $text .= "--$boundary2\n$plaintype\n\n"; + $text .= $this->body[IT_MAIL_PLAIN]; + $text .= "\n--$boundary2\n$htmltype\n\n"; + $text .= $this->body[IT_MAIL_HTML]; + $text .= "\n--$boundary2--\n"; + } + else + { + /* Just plain */ + if ($this->attachments) + $text .= "$plaintype\n\n"; + else + $headers[] = $plaintype; + + $text .= $this->body[IT_MAIL_PLAIN]; + } + } + else if ($this->body[IT_MAIL_HTML]) + { + /* Just HTML */ + if ($this->attachments) + { + if (strstr($this->body[IT_MAIL_HTML], "cid:")) + { + $boundary3 = md5(uniqid(rand())); + $text .= "Content-Type: multipart/related; boundary=\"$boundary3\"\n\n--$boundary3\n"; + } + + $text .= "$htmltype\n\n"; + } + else + $headers[] = $htmltype; + + $text .= $this->body[IT_MAIL_HTML]; + } + + if ($this->attachments) + { + $text .= "\n"; + $boundary = $boundary3 ? $boundary3 : $boundary1; + + foreach ($this->attachments as $attachment) + { + $text .= "\n--$boundary\nContent-Type: {$attachment['mimetype']}; name=\"{$attachment['name']}\"\nContent-Transfer-Encoding: base64\nContent-ID: <{$attachment['cid']}>\nContent-Disposition: inline; filename=\"{$attachment['name']}\"\n\n"; + $text .= chunk_split(base64_encode($attachment['data'])); + } + + if ($boundary3) + $text .= "\n--$boundary3--\n\n"; + + $text .= "--$boundary1--\n"; + } + + return mail($to, $this->subject, $text, join("\n", $headers), $this->flags); +} + + +/** + * INTERNAL: Escape header line with ?=iso-8859-1? if necessary, e.g. in Subject line + * @param $string String to be escaped + * @return String escape suitable for sending in header line + */ +function header_escape($string) +{ + return preg_replace('/[\x80-\xff]/e', "sprintf('=?iso-8859-1?Q?=%02X?=', ord('\\0'))", $string); +} + +/** + * Make string safe to be used in "$fullname <$email>": Remove illegal + * characters and enclose in double quotes. + * @param $string Fulle name to be escaped + * @return String to be safely used in "$fullname <$email>" for To: etc. + */ +function fullname_escape($string) +{ + return '"' . preg_replace('/["\x00-\x1f]/', '', $string) . '"'; +} + +/** + * INTERNAL: Send SMTP command and return true if result is 2XX + * @param $fp Connection to SMTP server opened using fsockopen + * @param $command Command to send, e.g. HELO gna.ch + * @param $anwer String containing full answer from SMTP server + * @param timeoutok Whether a timeout is considered ok + * @param $failcode Lowest SMTP result code which is considered a failure (default 300) + * @return True if SMTP result code is lower than $failcode + * + */ +function send_smtp_cmd($fp, $cmd, &$answer, $timeoutok = false, $failcode = 300) +{ + $result = false; + $answer = ''; + + if (!$cmd || (!feof($fp) && fwrite($fp, "$cmd\r\n"))) + { + while (!feof($fp) && ereg('^(...)(.?)(.*)$', fgets($fp, 1024), $regs)) + { + $answer .= $regs[0]; + $resultcode = intval($regs[1]); + + if ($regs[2] != '-') /* Multi line response? */ + { + if (($resultcode >= 100) && ($resultcode < $failcode)) + $result = true; + + break; + } + } + } + + /* A timeout can be considered ok for SLOW mail servers */ + if ($answer == '') + $result = $timeoutok; + + //echo ">>it_mail::send_smtp_cmd " . time() . " '$cmd' => $result: $answer"; + + return $result; +} + + +/** + * Check if given email address is valid (syntax, MX record). If you set + * checkmailbox to true it also connects to MX and asks if mailbox is valid. + * Note: Connecting to the MX can be slow. + * @param $email email to be check + * @param $checkmailbox True if you want to connect to MX and check account + * @return One of IT_MAIL_CHECKEMAIL_INVALID (0), IT_MAIL_CHECKEMAIL_MAILBOXFULL + * or IT_MAIL_CHECKEMAIL_OK (both != 0). + */ +function check_email($email, $checkmailbox = true) +{ + $result = IT_MAIL_CHECKEMAIL_INVALID; + + /* Check if username starts with www. or not well-formed => reject */ + if (!ereg('^www\.', $email) && eregi('^[a-z0-9&_+.-]+@([a-z0-9.-]+\.[a-z]+)$', $email, $regs)) + { + $domain = $regs[1]; + + if (!getmxrr($domain, $mxhosts, $weights)) + { + /* If we find no MX we check if domain exists */ + if (gethostbynamel($domain)) + { + $mxhosts = array($domain); + $weights = array(1); + } + } + + if ($mxhosts) /* We need at least one valid MX */ + { + if ($checkmailbox) + { + $mx = array(); + + for ($i = 0; $i < count($mxhosts); $i++) + $mx[$mxhosts[$i]] = $weights[$i]; + + asort($mx, SORT_NUMERIC); + $port = getservbyname('smtp', 'tcp'); + $from = 'itools@gna.ch'; + + /* Determine domain and email for test, skip if called as it_mail::check_email() */ + if (is_object($this)) + { + for ($i = 0; $i < count($this->header_names); $i++) + { + if ($this->header_names[$i] == 'From') + { + $from = $this->header_values[$i]; + break; + } + } + } + + if (eregi('@([a-z0-9.-]+\.[a-z]+)$', $from, $regs)) + $fromdomain = $regs[1]; + else + $fromdomain = 'gna.ch'; + + $finished = false; + $connected = 0; + + if (function_exists('stream_set_timeout')) + $timeout = 'stream_set_timeout'; + else + $timeout = 'socket_set_timeout'; + + foreach ($mx as $mxhost => $weight) + { + //echo ">>it_mail::check_email: " . time() . " checking $mxhost\n"; + if ($fp = @fsockopen($mxhost, $port, $errno, $errstr, 5)) + { + $connected++; + $timeout($fp, 45); + $answer = ''; + + if (it_mail::send_smtp_cmd($fp, '', $answer) && it_mail::send_smtp_cmd($fp, "HELO $fromdomain", $answer) && it_mail::send_smtp_cmd($fp, "MAIL FROM: <$from>", $answer)) + { + $timeout($fp, 2); + $timeoutok = ($domain != 'bluewin.ch'); + + if (it_mail::send_smtp_cmd($fp, "RCPT TO: <$email>", $answer, $timeoutok, 500)) # 450 is often used for Greylisting + $result = IT_MAIL_CHECKEMAIL_OK; + else if (eregi('quota|full|exceeded storage', $answer)) + $result = IT_MAIL_CHECKEMAIL_MAILBOXFULL; + + /* Do not try other MX if we got this far */ + $finished = true; + } + + $timeout($fp, 0, 1); + + it_mail::send_smtp_cmd($fp, 'RSET', $answer); + + it_mail::send_smtp_cmd($fp, 'QUIT', $answer); + fclose($fp); + } + + if ($finished) + break; + } + + /* If we cannot connect to _any_ MX it could be because of temporary overload / virus attack */ + if (!$connected) + $result = IT_MAIL_CHECKEMAIL_OK; + } + else + $result = IT_MAIL_CHECKEMAIL_OK; + } + } + + return $result; +} + +} + +?> |