. */ /** * Send mail via SMTP protocol * * @global DEBUG: if defined and set to true, mails will be send to DEBUG_RCPT instead of rcpt as defined in the constructor * @global DEBUG_RCPT: e-mail address where mails should be send in debug mode. * @global MYSQL_INSTANCE: name of a global variable containing an instance of the MySQL class that is logged in to the database. * If not available, queuing of messages that cannot be send is not possible. * @global MYSQL_MAILOUT_TABLE: name of the MySQL table used for storing outgoing mails. * * Note: for the queuing of mail to work, the following table definition needs to be created beforehand: * CREATE TABLE `".MYSQL_MAILOUT_TABLE."` ( * `id` int(11) NOT NULL auto_increment, * `date` datetime NOT NULL default '0000-00-00 00:00:00', * `rcpt` varchar(128) NOT NULL default '', * `subject` varchar(64) NOT NULL default '', * `mail` blob NOT NULL, * PRIMARY KEY (`id`) * ) ENGINE=MyISAM DEFAULT CHARSET=utf8 PACK_KEYS=0 AUTO_INCREMENT=1 ; * * @version 1.0 * @date 2.11.2009 * @author Wim van Ravesteijn * @license http://opensource.org/licenses/gpl-license.php GNU Public License */ class MailOut { private $smtphost; // hostname of SMTP server private $smtpport; // port of SMTP server private $timeout; // timeout in seconds private $domain; // domain used as default private $from; // sender private $rcpt; // array of recipients private $messages; // headers and bodies private $boundary; // message boundary private $dlvMsg; // delivery message private $dlvRetNr; // delivery return code private $failed = false; // did constructor fail private $errorMsg = ""; // textual representation of last error const MAIL_FAIL = 0; const MAIL_OK = 1; const MAIL_QUEUE = 2; /** * Default constructor * * @param string from: envelop sender of the mail * @param string/array rcpt: recipient(s) of the mail. May be single address or an array of addresses * @param string domain (optional): used for creating boundary and message-id, as well as HELO host when smtphost!="localhost" * @param string smtphost (optional): hostname of the (outgoing) SMTP server * @param int smtpport (optional): port number of the (outgoing) SMTP server * @param float timeout (optional): timeout for the connection in seconds */ public function __construct($from, $rcpt, $domain="", $smtphost=SMTP_HOST, $smtpport=SMTP_PORT, $timeout=10) { // Handle from if( $this->checkEmail($from) ) { $this->from = $from; }else { $this->errorMsg = "Invalid from: ".$from; $this->failed = true; return false; } // Handle rcpt if( DEBUG ) { $this->rcpt = array(DEBUG_RCPT); // In case we are in debug mode, always send mail to DEBUG_RCPT, we don't want to spam users by accident }else { // Only continue if all rcpts are valid. if( is_array($rcpt) ) { for( $i=0; $icheckEmail($rcpt[$i]) ) { $this->errorMsg = "Invalid rcpt: ".$rcpt[$i]; $this->failed = true; return false; } } $this->rcpt = $rcpt; }else { if( $this->checkEmail($rcpt) ) { $this->rcpt = array($rcpt); }else { $this->errorMsg = "Invalid rcpt: ".$rcpt; $this->failed = true; return false; } } } // Handle smtphost $this->smtphost = $smtphost; // Handle smtpport if( is_int($smtpport) ) { $this->smtpport = $smtpport; }else { $this->errorMsg = "Invalid port number (".$smtpport."), please provide an int."; $this->failed = true; return false; } // Handle timeout if( is_int($timeout) ) { $this->timeout = $timeout; }else { $this->errorMsg = "Invalid timeout (".$timeout."), please provide a float."; $this->failed = true; return false; } // Handle domain if( strlen($domain)>0 ) $this->domain = $domain; elseif( strlen($_SERVER['SERVER_NAME'])>0 ) $this->domain = $_SERVER['SERVER_NAME']; else $this->domain = "localhost"; // Set initial values $this->messages = array(); $this->boundary = date('YmdHis') . '_' . mt_rand(10000, 99999) . "/" . $this->domain; $this->errorMsg = ""; $this->dlvMsg = ""; $this->dlvRetNr = 0; return true; } /** * Default destructor */ public function __destruct() { } /** * Add a header to the message * * @param string header: the header to be add * @param int msg (optional): the message number (0 for main header, >=1 for attachment headers). * @return boolean true in case of success, false in case of error (unrecognised format) */ public function addHeader($header, $msg=0) { if( preg_match("/^(.*?):(.*)$/i", $header, $matches) ) { $title = $matches[1]; $value = $matches[2]; if( $this->isAllowDuplicateHeader($title) ) { // duplicates allowed $this->messages[$msg]['header'][] = $header; return true; }else { // no duplicates allowed, check if header already is present if( isset($this->messages[$msg]) ) { for( $i=0; $imessages[$msg]['header']); $i++ ) { if( preg_match("/^".$title.":(.*)$/i", $this->messages[$msg]['header'][$i]) ) { // duplicate found $this->messages[$msg]['header'][$i] = $header; return true; } } } // Seems this is not a duplicate $this->messages[$msg]['header'][] = $header; return true; } }else { $this->errorMsg = "Invalid header: ".$header; return false; } } /** * Encode a header to become valid MIME * * @param string header: header to be encoded * @return string encoded header */ public function encodeHeader($header) { return mb_encode_mimeheader($header, "UTF-8"); } /** * Add a body to the message * * Note: for attachments, first a Content-type header should be set * * @param string body: the body to be add * @param int msg (optional): the message number (0 for main body, >=1 for attachments). * @return boolean true in case of success, false in case of error */ public function addMessage($body, $msg=0) { if( $msg>0 AND !isset($this->messages[$msg]) ) { $this->errorMsg = "Need to set Content-type header first before adding a body"; return false; }else { if( $msg>0 ) { for( $i=0; $imessages[$msg]['header']); $i++ ) { if( preg_match("/^Content-type:/i", $this->messages[$msg]['header'][$i]) ) { // Ok $this->messages[$msg]['body'] = $body; return true; } } $this->errorMsg = "Unable to add message body, no content-type header set for this attachment"; return false; }else { $this->messages[$msg]['body'] = $body; return true; } } } /** * Set the From header * * @param string from: the value of the From header (name + e-mail address) * @return boolean true in case of success, false in case of error */ public function setFrom($from) { return $this->addHeader("From: ".$from); } /** * Set the Subject header * * @param string subject: the subject of the message * @return boolean true in case of success, false in case of error */ public function setSubject($subject) { return $this->addHeader("Subject: ".$subject); } /** * Send the message * * @param boolean sendLaterOnError: if true the message will be queued for later sending * @return int MAIL_FAIL on error, MAIL_QUEUE in case message is queued for later sending or MAIL_OK in case message has been send */ public function send($sendLaterOnError=false) { $this->checkHeader(); if( $this->failed OR !is_array($this->rcpt) ) { return self::MAIL_FAIL; } if( $stream=$this->initStream($this->from, $this->rcpt) ) { // headers $this->sendHeaders($stream, 0); // message $this->sendBody($stream, 0); // attachment for( $i=1; $imessages); $i++ ) { // boundary $this->writeToStream($stream, "\n--".$this->boundary."\n"); // headers $this->sendHeaders($stream, $i); // message $this->sendBody($stream, $i); } if( count($this->messages)>1 ) { // final boundary $this->writeToStream($stream, "\n--".$this->boundary."--\n"); } // close mail if( $this->finalizeStream($stream) ) { return self::MAIL_OK; }else { if( $sendLaterOnError ) { if( $this->queue() ) return self::MAIL_QUEUE; else return self::MAIL_FAIL; }else { return self::MAIL_FAIL; } } }else { // failed $this->errorMsg = "Failed communicating headers with mail server (error ".$this->dlvRetNr."): ".$this->dlvMsg; if( $sendLaterOnError ) { if( $this->queue() ) return self::MAIL_QUEUE; else return self::MAIL_FAIL; } return self::MAIL_FAIL; } } /** * Queue the message for later sending * * @return boolean true in case of successful queuing, false otherwise */ public function queue() { $this->checkHeader(); if( $this->failed OR !is_array($this->rcpt) ) { return false; } if( !defined($_GLOBAL[MYSQL_INSTANCE]) ) return false; // cannot queue $query = "INSERT INTO `".$_GLOBAL[MYSQL_INSTANCE]->escape(MYSQL_MAILOUT_TABLE)."` (`date`, `rcpt`, `subject`, `mail`) VALUES ('".$_GLOBAL[MYSQL_INSTANCE]->escape(date("Y-m-d H:i:s"))."', '"; if( is_array($this->rcpt) ) { $query .= $_GLOBAL[MYSQL_INSTANCE]->escape($this->rcpt[0]); for( $i=1; $ircpt); $i++ ) { $query .= $_GLOBAL[MYSQL_INSTANCE]->escape(", ".$this->rcpt[$i]); } } $query .= "', '"; for( $i=0; $imessages[0]['header']); $i++ ) { if( preg_match("/^Subject:\s*(.*)$/i", $this->messages[0]['header'][$i], $matches) ) { $query .= $_GLOBAL[MYSQL_INSTANCE]->escape($matches[1]); } } $query .= "', '".$_GLOBAL[MYSQL_INSTANCE]->escape(serialize($this))."')"; if( $_GLOBAL[MYSQL_INSTANCE]->query($query) AND $_GLOBAL[MYSQL_INSTANCE]->getaffected()==1 ) { return true; }else { return false; } } /** * Get the last delivery message text * * @return string textual representation of the last error */ public function getDeliveryMsg() { return $this->dlvMsg; } /** * Get the last delivery return number * * @return int numerical respresentation of the last error */ public function getDeliveryRetNr() { return $this->dlvRetNr; } /** * Get the last error * * @return string last error message generated */ public function getErrorMsg() { return $this->errorMsg; } /** * Check if the e-mail address is properly formatted * * @param string Email: e-mail address to be validated * @return boolean true in case e-mail address is properly formatted, false otherwise */ public function checkEmail($Email) { // Check if the supplied e-mail address is a valid e-mail address. If yes, return true, else return false $re="/(^(\w|\.|-|\+)+@(\w|-)+(\.(\w|-)+)*\.[a-zA-Z]{2,4}$)/"; if( preg_match($re,$Email) ) { //Regex matches, now check MX if( getmxrr(substr($Email, strpos($Email,"@")+1), $mxhosts) ) { return true; }else { $this->errorMsg = "Domain part of e-mail address does not have any MX server listed."; return false; } }else { $this->errorMsg = "The e-mail address is not properly formatted."; return false; } } /** * Validate the main headers */ private function checkHeader() { $k = array(); // Store all header names we use for( $i=0; $imessages[0]['header']); $i++ ) { if( preg_match("/^(.*?):(.*)$/i", $this->messages[0]['header'][$i], $matches) ) { $k[] = $matches[1]; } } if( !in_array("From", $k) ) $this->addHeader("From: ".$this->from); if( !in_array("To", $k) ) { if( is_array($this->rcpt) ) { $to = $this->rcpt[0]; for( $i=1; $ircpt); $i++ ) { $to .= $this->rcpt[$i]; } $this->addHeader("To: ".$to); }else { $this->addHeader("To: ".$this->rcpt); } } if( !in_array("Date", $k) ) $this->addHeader("Date: ".date("r")); if( !in_array("Message-ID", $k) ) $this->addHeader("Message-ID: <" . date('YmdHis') . '.' . mt_rand(10000, 99999) . "@" . $this->domain . ">"); if( !in_array("Content-Type", $k) ) { if( count($this->messages)>1 ) $this->addHeader("Content-Type: multipart/mixed; boundary=\"".$this->boundary."\""); else $this->addHeader("Content-Type: text/plain; charset=UTF-8"); }else { if( count($this->messages)>1 ) { // Make sure we have a multipart/* message and a boundary $ok = false; for( $i=0; $imessages[0]['header']); $i++ ) { if( preg_match("/^Content-Type:(.*)$/i", $this->messages[0]['header'][$i], $matches) ) { if( preg_match("/^\s*multipart\/[a-z-]+;.*boundary=\"".$this->boundary."\".*$/i", $matches[1]) ) { $ok = true; } } } if( !$ok ) { // header does not mention multipart message, override... $this->addHeader("Content-Type: multipart/mixed; boundary=\"".$this->boundary."\""); } } } $this->addHeader("X-Mailer: AEGEE Online Membership System"); if( isset($_SERVER['REMOTE_ADDR']) ) $this->addHeader("X-Posting-Host: ".$_SERVER['REMOTE_ADDR']); } /** * Send the headers of a message * * @param resource stream the socket used to communicate with the SMTP server. * @param int msg (optional): the message number (0 for main body, >=1 for attachments). */ private function sendHeaders($stream, $msg=0) { $headerorder = array("Message-ID", "Date", "From", "To", "Cc", "Subject", "Content-Type"); $headers = $this->messages[$msg]['header']; for( $i=0; $iwriteToStream($stream, $this->headerLimit($headers[$j]."\n")); unset($headers[$j]); $headers = array_merge($headers); } } } for( $i=0; $iwriteToStream($stream, $this->headerLimit($headers[$i]."\n")); } $this->writeToStream($stream, "\n"); } /** * Send the body of a message * * @param resource stream the socket used to communicate with the SMTP server. * @param int msg (optional): the message number (0 for main body, >=1 for attachments). */ private function sendBody($stream, $msg=0) { $this->writeToStream($stream, $this->messageLimit($this->messages[$msg]['body'])); $this->writeToStream($stream, "\n"); } /** * Limit the length of headers. Break over several lines in case it is too long. * * @todo Implement this function * * @param string header field to be cut over several lines in case it is too long. * @return string same header field, possibly split over several lines. */ private function headerLimit($header) { return $header; } /** * Limit the length of lines. Break over several lines in case it is too long. * * @todo Implement this function * * @param string message to be cut over several lines in case lines are too long. * @return string same message, possibly split over several lines. */ private function messageLimit($message) { return $message; } /** * Check if a header type is allowed to appear several times in the same header block * * @param string header type to be checked * @return boolean true in case this header type may appear several times in the same header block and false otherwise */ private function isAllowDuplicateHeader($header) { return in_array(strtolower($header), array('received')); } /** * Check if the contents can be send to the SMTP socket * * @param &string contents to be checked */ private function preWriteToStream(&$s) { if( $s ) { if( $s{0} == '.' ) $s = '.' . $s; $s = str_replace("\n.","\n..",$s); } } /** * Initialise a socket to send out a mail * * @param string from sender of the message (SMTP envelop sender) * @param array rcpt recipients of the message (SMTP envelop recipient) * @return resource identifier to the socket, or false in case of error. */ private function initStream($from, $rcpt) { $stream = @fsockopen($this->smtphost, $this->smtpport, $errorNumber, $errorString, $this->timeout); if( !$stream ) { $this->dlvMsg = $errorString; $this->dlvRetNr = $errorNumber; return false; } stream_set_timeout($stream, $this->timeout); $tmp = fgets($stream, 1024); if( $this->errorCheck($tmp, $stream) ) { return false; } if( $this->smtphost=="localhost" ) $helodomain = "localhost"; else $helodomain = $this->domain; /* Lets introduce ourselves */ fputs($stream, "EHLO ".$helodomain."\r\n"); $tmp = fgets($stream,1024); if( $this->errorCheck($tmp,$stream) ) { // fall back to HELO if EHLO is not supported if( $this->dlvRetNr == '500' ) { fputs($stream, "HELO ".$helodomain."\r\n"); $tmp = fgets($stream,1024); if( $this->errorCheck($tmp,$stream) ) { return false; } }else { return false; } } /* Ok, who is sending the message? */ fputs($stream, "MAIL FROM: <".$from.">\r\n"); $tmp = fgets($stream, 1024); if( $this->errorCheck($tmp, $stream) ) { return false; } /* send who the recipients are */ for( $i = 0, $cnt = count($rcpt); $i < $cnt; $i++ ) { fputs($stream, "RCPT TO: <".$rcpt[$i].">\r\n"); $tmp = fgets($stream, 1024); if( $this->errorCheck($tmp, $stream) ) { return false; } } /* Lets start sending the actual message */ fputs($stream, "DATA\r\n"); $tmp = fgets($stream, 1024); if( $this->errorCheck($tmp, $stream) ) { return false; } return $stream; } /** * Send the data to the socket * * @param resource stream identifier of the socket used for communicating with the SMTP server * @param string data to be send to the socket */ private function writeToStream($stream, $data) { $this->preWriteToStream($data); fputs($stream, $data); } /** * Finish the message and close the socket * * @param resource stream identifier of the socket used for communicating with the SMTP server * @return boolean true in case of successful sending of the message, false otherwise */ private function finalizeStream($stream) { fputs($stream, "\r\n.\r\n"); /* end the DATA part */ $tmp = fgets($stream, 1024); $this->errorCheck($tmp, $stream); if( $this->dlvRetNr != 250 ) { return false; } fputs($stream, "QUIT\r\n"); /* log off */ fclose($stream); return true; } /** * Check if an SMTP reply is an error and set an error message) * * @param string line * @param resource stream identifier of the socket used for communicating with the SMTP server * @return boolean true in case of error, false otherwise */ private function errorCheck($line, $stream) { $errNum = substr($line, 0, 3); $this->dlvRetNr = $errNum; $serverMsg = substr($line, 4); while( substr($line, 0, 4) == ($errNum.'-') ) { $line = fgets($stream, 1024); $serverMsg .= substr($line, 4); } if( ((int) $errNum{0}) < 4 ) { return false; } switch( $errNum ) { case '421': $message = _("Service not available, closing channel"); break; case '432': $message = _("A password transition is needed"); break; case '450': $message = _("Requested mail action not taken: mailbox unavailable"); break; case '451': $message = _("Requested action aborted: error in processing"); break; case '452': $message = _("Requested action not taken: insufficient system storage"); break; case '454': $message = _("Temporary authentication failure"); break; case '500': $message = _("Syntax error; command not recognized"); break; case '501': $message = _("Syntax error in parameters or arguments"); break; case '502': $message = _("Command not implemented"); break; case '503': $message = _("Bad sequence of commands"); break; case '504': $message = _("Command parameter not implemented"); break; case '530': $message = _("Authentication required"); break; case '534': $message = _("Authentication mechanism is too weak"); break; case '535': $message = _("Authentication failed"); break; case '538': $message = _("Encryption required for requested authentication mechanism"); break; case '550': $message = _("Requested action not taken: mailbox unavailable"); break; case '551': $message = _("User not local; please try forwarding"); break; case '552': $message = _("Requested mail action aborted: exceeding storage allocation"); break; case '553': $message = _("Requested action not taken: mailbox name not allowed"); break; case '554': $message = _("Transaction failed"); break; default: $message = _("Unknown response"); break; } $this->dlvMsg = $message; $this->dlvServerMsg = nl2br(htmlspecialchars($serverMsg)); return true; } } ?>