TimeTrex/classes/modules/core/TTMail.class.php

434 lines
14 KiB
PHP

<?php
/*********************************************************************************
*
* TimeTrex is a Workforce Management program developed by
* TimeTrex Software Inc. Copyright (C) 2003 - 2021 TimeTrex Software Inc.
*
* This program is free software; you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License version 3 as published by
* the Free Software Foundation with the addition of the following permission
* added to Section 15 as permitted in Section 7(a): FOR ANY PART OF THE COVERED
* WORK IN WHICH THE COPYRIGHT IS OWNED BY TIMETREX, TIMETREX DISCLAIMS THE
* WARRANTY OF NON INFRINGEMENT OF THIRD PARTY RIGHTS.
*
* 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 Affero General Public License for more
* details.
*
*
* You should have received a copy of the GNU Affero General Public License along
* with this program; if not, see http://www.gnu.org/licenses or write to the Free
* Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
* 02110-1301 USA.
*
*
* You can contact TimeTrex headquarters at Unit 22 - 2475 Dobbin Rd. Suite
* #292 West Kelowna, BC V4T 2E9, Canada or at email address info@timetrex.com.
*
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU Affero General Public License version 3.
*
*
* In accordance with Section 7(b) of the GNU Affero General Public License
* version 3, these Appropriate Legal Notices must retain the display of the
* "Powered by TimeTrex" logo. If the display of the logo is not reasonably
* feasible for technical reasons, the Appropriate Legal Notices must display
* the words "Powered by TimeTrex".
*
********************************************************************************/
/**
* @package Core
*/
/*
Config options:
[mail]
delivery_method = mail/smtp/soap
smtp_host=mail.domain.com
smtp_port=25
smtp_username=test1
smtp_password=testpass
*/
class TTMail {
private $mime_obj = null;
private $mail_obj = null;
private $data = null;
public $default_mime_config = [
'html_charset' => 'UTF-8',
'text_charset' => 'UTF-8',
'head_charset' => 'UTF-8',
];
/**
* TTMail constructor.
*/
function __construct() {
//For some reason the EOL defaults to \r\n, which seems to screw with Amavis
//This also prevents wordwrapping at 70 chars.
if ( !defined( 'MAIL_MIMEPART_CRLF' ) ) {
define( 'MAIL_MIMEPART_CRLF', "\n" );
}
return true;
}
/**
* @return Mail_Mime|null
*/
function getMimeObject() {
if ( $this->mime_obj == null ) {
//require_once( 'Mail/mime.php' );
$this->mime_obj = @new Mail_mime();
}
return $this->mime_obj;
}
/**
* @return null|object
*/
function getMailObject() {
if ( $this->mail_obj == null ) {
//require_once( 'Mail.php' );
//Determine if use Mail/SMTP, or SOAP.
$delivery_method = $this->getDeliveryMethod();
if ( $delivery_method == 'mail' ) {
$this->mail_obj = Mail::factory( 'mail' );
} else if ( $delivery_method == 'sendmail' ) {
$this->mail_obj = Mail::factory( 'sendmail' );
} else if ( $delivery_method == 'smtp' ) {
$smtp_config = $this->getSMTPConfig();
$mail_config = [
'host' => $smtp_config['host'],
'port' => $smtp_config['port'],
];
if ( isset( $smtp_config['username'] ) && $smtp_config['username'] != '' ) {
//Removed 'user_name' as it wasn't working with postfix.
$mail_config['username'] = $smtp_config['username'];
$mail_config['password'] = $smtp_config['password'];
$mail_config['auth'] = true;
}
//Allow self-signed TLS certificates by default, which were disabled by default in PHP v5.6 -- See comments here: http://php.net/manual/en/migration56.openssl.php
//This should fix error messages like: authentication failure [SMTP: STARTTLS failed (code: 220, response: 2.0.0 SMTP server ready)
$mail_config['socket_options'] = [ 'ssl' => [ 'verify_peer' => false, 'verify_peer_name' => false, 'allow_self_signed' => true ] ];
$this->mail_obj = Mail::factory( 'smtp', $mail_config );
Debug::Arr( $mail_config, 'SMTP Config: ', __FILE__, __LINE__, __METHOD__, 10 );
}
}
return $this->mail_obj;
}
/**
* @return string
*/
function getDeliveryMethod() {
global $config_vars;
$possible_values = [ 'mail', 'soap', 'smtp', 'sendmail' ];
if ( isset( $config_vars['mail']['delivery_method'] ) && in_array( strtolower( trim( $config_vars['mail']['delivery_method'] ) ), $possible_values ) ) {
return $config_vars['mail']['delivery_method'];
}
if ( DEPLOYMENT_ON_DEMAND == true ) {
return 'mail';
}
return 'soap'; //Default to SOAP as it has a better chance of working than mail/SMTP
}
/**
* @return array
*/
function getSMTPConfig() {
global $config_vars;
$retarr = [
'host' => null,
'port' => 25,
'username' => null,
'password' => null,
];
if ( isset( $config_vars['mail']['smtp_host'] ) ) {
$retarr['host'] = $config_vars['mail']['smtp_host'];
}
if ( isset( $config_vars['mail']['smtp_port'] ) ) {
$retarr['port'] = $config_vars['mail']['smtp_port'];
}
if ( isset( $config_vars['mail']['smtp_username'] ) ) {
$retarr['username'] = $config_vars['mail']['smtp_username'];
}
if ( isset( $config_vars['mail']['smtp_password'] ) ) {
$retarr['password'] = $config_vars['mail']['smtp_password'];
}
return $retarr;
}
/**
* @return array
*/
function getMIMEHeaders() {
$mime_headers = @$this->getMIMEObject()->headers( $this->getHeaders(), true );
//Debug::Arr($this->data['headers'], 'MIME Headers: ', __FILE__, __LINE__, __METHOD__, 10);
return $mime_headers;
}
/**
* @return bool
*/
function getHeaders() {
if ( isset( $this->data['headers'] ) ) {
return $this->data['headers'];
}
return false;
}
/**
* @param $headers
* @param bool $include_default
* @return bool
*/
function setHeaders( $headers, $include_default = false ) {
$this->data['headers'] = $headers;
if ( $include_default == true ) {
//May have to go to base64 encoding all data for proper UTF-8 support.
$this->data['headers']['Content-type'] = 'text/html; charset="UTF-8"';
}
//Debug::Arr($this->data['headers'], 'Headers: ', __FILE__, __LINE__, __METHOD__, 10);
return true;
}
/**
* @return bool
*/
function getTo() {
if ( isset( $this->data['to'] ) ) {
return $this->data['to'];
}
return false;
}
/**
* @param $email
* @return bool
*/
function setTo( $email ) {
$this->data['to'] = $email;
return true;
}
/**
* @return bool
*/
function getBody() {
if ( isset( $this->data['body'] ) ) {
return $this->data['body'];
}
return false;
}
/**
* @param $body
* @return bool
*/
function setBody( $body ) {
$this->data['body'] = $body;
return true;
}
/**
* Adds a plain text body to an HTML email stating that it contains HTML content.
* @return mixed
*/
function setDefaultTXTBody() {
return @$this->getMIMEObject()->setTXTBody( TTi18n::getText( 'This email contains HTML content, please open in a HTML enabled email viewer.' ) ); //Having a text/plain body helps reduce spam score.
}
/**
* Extracts just the email address part from a string that may contain the name part, etc...
* @param $address
* @return mixed
*/
static function parseEmailAddress( $address ) {
if ( preg_match( '/(?<=[<\[]).*?(?=[>\]]$)/', $address, $match ) ) {
$retval = $match[0];
} else {
$retval = $address;
}
return filter_var( $retval, FILTER_VALIDATE_EMAIL ); //Make sure we filter the email address here, so if using -f params, we aren't exploitable, for example an email address like: "Attacker -Param2 -Param3"@test.com
}
/**
* @param bool $force
* @return bool
*/
function Send( $force = false ) {
global $config_vars;
Debug::Arr( $this->getTo(), 'Attempting to send email To: ', __FILE__, __LINE__, __METHOD__, 10 );
if ( $this->getTo() == false ) {
Debug::Text( 'To Address invalid...', __FILE__, __LINE__, __METHOD__, 10 );
return false;
}
if ( $this->getBody() == false ) {
Debug::Text( 'Body invalid...', __FILE__, __LINE__, __METHOD__, 10 );
return false;
}
Debug::Text( 'Sending Email: Body Size: ' . strlen( $this->getBody() ) . ' Method: ' . $this->getDeliveryMethod(), __FILE__, __LINE__, __METHOD__, 10 );
if ( PRODUCTION == false && $force !== true ) {
Debug::Text( 'Not in production mode, not sending emails...', __FILE__, __LINE__, __METHOD__, 10 );
//$to = 'root@localhost';
return false;
}
if ( DEMO_MODE == true ) {
Debug::Text( 'In DEMO mode, not sending emails...', __FILE__, __LINE__, __METHOD__, 10 );
return false;
}
//if ( !isset($this->data['headers']['Date']) ) {
// $this->data['headers']['Date'] = date( 'D, d M Y H:i:s O');
//}
$this->data['headers']['X-TimeTrex-Version'] = APPLICATION_VERSION;
$this->data['headers']['X-TimeTrex-Edition'] = getTTProductEditionName();
$this->data['headers']['X-TimeTrex-Hostname'] = Misc::getURLProtocol() . '://' . Misc::getHostName( true ) . Environment::getBaseURL();
if ( !is_array( $this->getTo() ) ) {
$to = [ $this->getTo() ];
} else {
//Even though the RFC says the local part of email address is case sensitive, it should never really be required in the real-world.
//This prevents duplicate emails from being sent to email@mydomain.com and Email@mydomain.com or EMAIL@mydomain.com
$to = Misc::arrayIUnique( $this->getTo() );
}
//When using SMTP, we have to manually send to each envelope-to, but make sure the original TO header is set.
$secondary_to = [];
if ( $this->getDeliveryMethod() == 'smtp' && isset( $this->data['headers']['Cc'] ) && $this->data['headers']['Cc'] != '' ) {
$secondary_to = array_merge( $secondary_to, array_map( 'trim', explode( ',', $this->data['headers']['Cc'] ) ) );
}
if ( $this->getDeliveryMethod() == 'smtp' && isset( $this->data['headers']['Bcc'] ) && $this->data['headers']['Bcc'] != '' ) {
$secondary_to = array_merge( $secondary_to, array_map( 'trim', explode( ',', $this->data['headers']['Bcc'] ) ) );
}
$secondary_to = array_diff( $secondary_to, $to ); //Make sure the CC/BCC doesn't contain any of the TO addresses, so we don't send duplicate emails.
$i = 0;
foreach ( $to as $recipient ) {
Debug::Text( $i . '. Recipient: ' . $recipient, __FILE__, __LINE__, __METHOD__, 10 );
//Make sure its at least a partially valid email address, otherwise just skip without returning FALSE.
// Without this a CC recipient of '"John Doe" <>' will fail and return FALSE making other functions think email failed.
if ( strpos( $recipient, '@' ) === false ) {
Debug::Text( ' Recipient email address is invalid, skipping...', __FILE__, __LINE__, __METHOD__, 10 );
continue;
}
$this->data['headers']['To'] = $recipient; //Always set the TO header to the primary recipient. When using SMTP method it won't do that automatically. We shouldn't be setting this in the case of a Bcc, then its not blind anymore.
//Check to see if they want to force a return-path for better bounce handling.
//However if the envelope from header does not match the From header
//It may trigger spam filtering due to email mismatch/forgery (EDT_SDHA_ADR_FRG)
if ( !isset( $this->data['headers']['Return-Path'] ) && isset( $config_vars['other']['email_return_path_local_part'] ) && $config_vars['other']['email_return_path_local_part'] != '' ) {
$this->data['headers']['Return-Path'] = Misc::getEmailReturnPathLocalPart( $recipient ) . '@' . Misc::getEmailDomain();
}
//Debug::Arr($this->getMIMEHeaders(), 'Sending Email To: '. $recipient, __FILE__, __LINE__, __METHOD__, 10);
switch ( $this->getDeliveryMethod() ) {
case 'smtp':
case 'sendmail':
case 'mail':
if ( $this->getDeliveryMethod() == 'mail' ) {
$this->getMailObject()->_params = '-t'; //The -t option specifies that exim should build the recipients list from the 'To', 'Cc', and 'Bcc' headers rather than from the arguments list.
if ( isset( $this->data['headers']['Return-Path'] ) ) {
$this->getMailObject()->_params .= ' -f' . $this->parseEmailAddress( $this->data['headers']['Return-Path'] );
} else if ( isset( $this->data['headers']['From'] ) ) {
$this->getMailObject()->_params .= ' -f' . $this->parseEmailAddress( $this->data['headers']['From'] );
}
}
$send_retval = $this->getMailObject()->send( $recipient, $this->getMIMEHeaders(), $this->getBody() );
if ( PEAR::isError( $send_retval ) ) {
Debug::Text( 'Send Email Failed... Error: ' . $send_retval->getMessage(), __FILE__, __LINE__, __METHOD__, 10 );
$send_retval = false;
}
//When using SMTP, we have to manually send to each envelope-to, but make sure the original TO header is set.
if ( $this->getDeliveryMethod() == 'smtp' && isset( $secondary_to ) && is_array( $secondary_to ) && count( $secondary_to ) > 0 ) {
$x = 0;
foreach ( $secondary_to as $cc_key => $cc_recipient ) {
//Make sure its at least a partially valid email address, otherwise just skip without returning FALSE.
// Without this a CC recipient of '"John Doe" <>' will fail and return FALSE making other functions think email failed.
if ( strpos( $cc_recipient, '@' ) === false ) {
Debug::Text( ' CC Recipient email address is invalid, skipping...', __FILE__, __LINE__, __METHOD__, 10 );
continue;
}
Debug::Text( ' ' . $x . '. CC Recipient: ' . $cc_recipient, __FILE__, __LINE__, __METHOD__, 10 );
$send_retval = $this->getMailObject()->send( $cc_recipient, $this->getMIMEHeaders(), $this->getBody() );
if ( PEAR::isError( $send_retval ) ) {
Debug::Text( 'Send Email Failed... Error: ' . $send_retval->getMessage(), __FILE__, __LINE__, __METHOD__, 10 );
$send_retval = false;
}
unset( $secondary_to[$cc_key] ); //Remove CC recipient from array so we don't send it multiple times if there are multiple recipients.
$x++;
}
}
break;
case 'soap':
$ttsc = new TimeTrexSoapClient();
$send_retval = $ttsc->sendEmail( $recipient, $this->getMIMEHeaders(), $this->getBody() );
break;
}
if ( $send_retval != true ) {
Debug::Arr( $send_retval, 'Send Email Failed To: ' . $recipient, __FILE__, __LINE__, __METHOD__, 10 );
}
$i++;
}
if ( $send_retval == true ) {
return true;
}
Debug::Arr( $send_retval, 'Send Email Failed!', __FILE__, __LINE__, __METHOD__, 10 );
return false;
}
}
?>