Files
mrbs-equbao-2026/lib/MRBS/MailQueue.php
T
人事系统开发 1ba6efd8ed MRBS 1.12.2 等保2.0二级整改完整提交
包含:登录失败锁定、90天密码有效期、30分钟会话超时、
强制改密、登录审计日志、屏幕水印、企业背景图、
备案信息固定底部、favicon、JS空集合保护、
会话过期体验优化(403 JSON)、display_errors 关闭、
固定 key 根治 Integrity check failed 等全部改动

注意:config.inc.php/.htaccess/.user.ini 含敏感信息,
通过 .gitignore 排除,勿推送到公开仓库。
2026-09-09 16:55:02 +08:00

415 lines
15 KiB
PHP

<?php
declare(strict_types=1);
namespace MRBS;
use Mail_mimePart;
use PHPMailer\PHPMailer\Exception;
use PHPMailer\PHPMailer\SMTP;
require_once MRBS_ROOT . '/functions_mail.inc';
// A class for handling a mail queue. Normally the queue is flushed on shutdown using a
// register_shutdown_function() callback. However the class could easily be extended so that the
// mail queue is held in the database and emptied by a cron job. This would enable rate limits to
// be adhered to more accurately, as there would be just one queue.
class MailQueue
{
protected static $mails = array();
/**
* Add an email to the queue for sending
*
* @param array $addresses an array of addresses, each being a comma
* separated list of email addresses. Indexed by
* 'from'
* 'reply_to'
* 'to'
* 'cc'
* 'bcc'
* @param string $subject email subject
* @param string $text_body text part of body
* @param string|null $html_body HTML part of body
* @param array|null $attachment file to attach. An array consisting of
* 'content' the file or data to attach
* 'method' the iCalendar METHOD
* 'name' the name to give it
* @param string $charset character set used in body
* @return void
*/
public static function add(
array $addresses,
string $subject,
string $text_body,
?string $html_body,
?array $attachment,
string $charset = 'us-ascii'
) : void
{
global $mail_settings;
// Don't do anything if mail has been disabled. Useful for testing MRBS without
// sending emails to those who don't want them
if ($mail_settings['disabled'])
{
mail_debug("Mail disabled: not adding message to queue");
return;
}
// And no point in doing anything if there are no recipients
if (self::getNRecipients($addresses) == 0)
{
mail_debug("No recipients: not adding message to queue");
return;
}
$mail = array();
foreach(array('addresses', 'subject', 'text_body', 'html_body', 'attachment', 'charset') as $var)
{
$mail[$var] = $$var;
}
self::$mails[] = $mail;
}
public static function flush() : void
{
foreach (self::$mails as $mail)
{
self::sendMail(
$mail['addresses'],
$mail['subject'],
$mail['text_body'],
$mail['html_body'],
$mail['attachment'],
$mail['charset']
);
}
}
/**
* Get the number of recipients in the addresses array.
*/
protected static function getNRecipients(array $addresses) : int
{
$result = 0;
if (!empty($addresses))
{
foreach (['to', 'cc', 'bcc'] as $key)
{
if (!empty($addresses[$key]))
{
$result += count(parse_addresses($addresses[$key]));
}
}
}
return $result;
}
/**
* Send an email
*
* @param array $addresses an array of addresses, each being a comma
* separated list of email addresses. Indexed by
* 'from'
* 'reply_to'
* 'to'
* 'cc'
* 'bcc'
* @param string $subject email subject
* @param string $text_body text part of body
* @param string|null $html_body HTML part of body
* @param array|null $attachment file to attach. An array consisting of
* 'content' the file or data to attach
* 'method' the iCalendar METHOD
* 'name' the name to give it
* @param string $charset character set used in body
* @return bool TRUE on success, FALSE on failure
* @throws Exception
*/
protected static function sendMail(
array $addresses,
string $subject,
string $text_body,
?string $html_body,
?array $attachment,
string $charset = 'us-ascii'
) : bool
{
// Modify the include path because this is run after shutdown when the working directory may have
// changed (see the note in https://www.php.net/manual/en/function.register-shutdown-function.php).
// Put the new path at the beginning of the list in case there is an open_basedir restriction in effect
// and one of the paths triggers a warning preventing the remaining paths from being reached.
set_include_path(MRBS_ROOT . PATH_SEPARATOR . get_include_path());
require_once 'Mail/mimePart.php';
// We use the PHPMailer class to handle the sending of mail. However PHPMailer does not
// provide the versatility we need to be able to construct the MIME parts necessary for
// iCalendar applications, so we use the PEAR Mail_Mime package to do that. See the
// discussion at https://github.com/PHPMailer/PHPMailer/issues/175
global $mail_settings, $sendmail_settings, $smtp_settings;
static $last_mail_sent = null;
static $last_n_addresses = null;
$mail = new Mailer($mail_settings, $sendmail_settings, $smtp_settings);
mail_debug("Preparing to send email ...");
if ($mail_settings['debug'])
{
$mail->Debugoutput = ($mail_settings['debug_output'] == 'log') ? 'error_log' : 'html';
$mail->SMTPDebug = SMTP::DEBUG_CONNECTION; // show connection status, client -> server and server -> client messages
}
$eol = "\n"; // EOL sequence to use in mail headers. Need "\n" for mail backend
if (empty($addresses['from']))
{
if (isset($mail_settings['from']))
{
$addresses['from'] = $mail_settings['from'];
}
else
{
trigger_error('$mail_settings["from"] has not been set in the config file.', E_USER_NOTICE);
}
}
$mail->CharSet = Language::MAIL_CHARSET; // PHPMailer defaults to 'iso-8859-1'
$mail->AllowEmpty = true; // remove this for production
if (!empty($addresses['reply_to']))
{
$reply_to_addresses = parse_addresses($addresses['reply_to']);
foreach ($reply_to_addresses as $reply_to_address)
{
$mail->addReplyTo($reply_to_address['address'], $reply_to_address['name']);
}
}
if (isset($addresses['from']))
{
$from_addresses = parse_addresses($addresses['from']);
$mail->setFrom($from_addresses[0]['address'], $from_addresses[0]['name']);
}
$to_addresses = parse_addresses($addresses['to']);
foreach ($to_addresses as $to_address)
{
$mail->addAddress($to_address['address'], $to_address['name']);
}
if (isset($addresses['cc']))
{
$cc_addresses = parse_addresses($addresses['cc']);
foreach ($cc_addresses as $cc_address)
{
// for cases where the mail server refuses to send emails with cc set, put the cc addresses on the to line
if ($mail_settings['treat_cc_as_to'])
{
$mail->addAddress($cc_address['address'], $cc_address['name']);
}
// otherwise treat them normally, adding them to the cc line
else
{
$mail->addCC($cc_address['address'], $cc_address['name']);
}
}
}
if (isset($addresses['bcc']))
{
$bcc_addresses = parse_addresses($addresses['bcc']);
foreach ($bcc_addresses as $bcc_address)
{
$mail->addBCC($bcc_address['address'], $bcc_address['name']);
}
}
$mail->Subject = $subject;
// Build the email. We're going to use the "alternative" subtype which means
// that we order the sub parts according to how faithful they are to the original,
// putting the least faithful first, ie the ordinary plain text version. The
// email client then uses the most faithful version that it can handle. (See
// https://www.rfc-editor.org/rfc/rfc2046#section-5.1.4)
//
// If we are also adding the iCalendar information then we enclose this alternative
// mime subtype in an outer mime type which is mixed. This is necessary so that
// the widest variety of calendar applications can access the calendar information.
// So depending on whether we are sending iCalendar information we will have a Mime
// structure that looks like this:
//
// With iCalendar info Without iCalendar info
// ------------------- ----------------------
//
// multipart/mixed mutlipart/alternative
// multipart/alternative text/plain
// text/plain text/html
// text/html
// text/calendar
// application/ics
// First of all build the inner mime type, ie the multipart/alternative type.
// If we're not sending iCalendar information this will become the outer,
// otherwise we'll then enclose it in an outer mime type.
$mime_params = array();
$mime_params['eol'] = $eol;
$mime_params['content_type'] = "multipart/alternative";
$mime_inner = new Mail_mimePart('', $mime_params);
// Add the text part
$mime_params['content_type'] = "text/plain";
$mime_params['cid'] = generate_global_uid('text');
$mime_params['encoding'] = "8bit";
$mime_params['charset'] = $charset;
$mime_inner->addSubPart($text_body, $mime_params);
unset($mime_params['cid']);
// Add the HTML mail
if (isset($html_body))
{
$mime_params['content_type'] = "text/html";
$mime_params['cid'] = generate_global_uid('html');
$mime_inner->addSubPart($html_body, $mime_params);
unset($mime_params['cid']);
}
if (empty($attachment))
{
// If we're not sending iCalendar information we've now got everything,
// so we'll make the "inner" section the complete mime.
$mime = $mime_inner;
}
else
{
// Otherwise we need to carry on and add the text version of the iCalendar
$mime_params['content_type'] = "text/calendar; method=" . $attachment['method'];
// The encoding needs to be base64, because Postfix will otherwise not preserve
// CRLF sequences and these are mandatory terminators in the iCalendar file
$mime_params['encoding'] = "base64";
$mime_inner->addSubPart($attachment['content'], $mime_params);
// and then enclose the inner section in a multipart/mixed outer section.
// First create the outer section
$mime_params = array();
$mime_params['eol'] = $eol;
$mime_params['content_type'] = "multipart/mixed";
$mime = new Mail_mimePart('', $mime_params);
// Now add the inner section as the first sub part
$mime_inner = $mime_inner->encode();
$mime_params = array();
$mime_params['eol'] = $eol;
$mime_params['encoding'] = "8bit";
$mime_params['content_type'] = $mime_inner['headers']['Content-Type'];
$mime->addSubPart($mime_inner['body'], $mime_params);
// And add the attachment as the second sub part
$mime_params['content_type'] = "application/ics";
$mime_params['encoding'] = "base64";
$mime_params['disposition'] = "attachment";
$mime_params['dfilename'] = $attachment['name'];
$mime->addSubPart($attachment['content'], $mime_params);
}
// Encode the result
$mime = $mime->encode();
// Construct the MIMEHeader and then call preSend() which sets up the
// headers. However it also sets up the body, which we don't want,
// so we have to set the MIMEBody after calling preSend().
//
// This is not ideal since $MIMEHeader and $MIMEBody are protected
// properties of the PHPMailer class. In the future we may have to extend
// the class to do what we want, unless the features we need are added.
$mime_header = '';
foreach ($mime['headers'] as $name => $value)
{
if ($name == 'Content-Type')
{
$mail->ContentType = $value;
}
$mime_header .= $mail->headerLine($name, $value);
}
$mail->set('MIMEHeader', $mime_header);
if ($mail->preSend())
{
$mail->set('MIMEBody', $mime['body']);
mail_debug("Using backend '" . $mail_settings['admin_backend'] . "'");
mail_debug("From: " . ($addresses['from'] ?? ''));
mail_debug("To: " . ($addresses['to'] ?? ''));
mail_debug("Cc: " . ($addresses['cc'] ?? ''));
mail_debug("Bcc: " . ($addresses['bcc'] ?? ''));
// Throttle the rate of mail sending if required
if (!empty($mail_settings['rate_limit']))
{
$microtime_now = round(microtime(true), 6);
if (isset($last_mail_sent))
{
$diff = round($microtime_now - $last_mail_sent, 6);
mail_debug("Last mail sent $diff seconds ago to $last_n_addresses addresses");
$min_gap = round($last_n_addresses/$mail_settings['rate_limit'], 6);
if ($min_gap > $diff)
{
$sleep_seconds = round($min_gap - $diff, 6);
mail_debug("Too soon to send the next mail; sleeping for $sleep_seconds seconds");
usleep(intval($sleep_seconds * 1000000));
}
}
$last_n_addresses = self::getNRecipients($addresses);
$last_mail_sent = $microtime_now;
}
// PHPMailer uses escapeshellarg() and escapeshellcmd(). In many installations these will
// have been disabled. If they have been disabled PHP will generate a warning and the functions
// will return NULL. PHPMailer uses the functions to test if the sender address is shell safe
// and can be used with the -f option. If the escapeshell*() functions are disabled, mail
// will still be sent, but -f will not be used. [Note that if a function is disabled you cannot
// redeclare it, so writing emulations of escapeshellarg() and escapeshellcmd() is not an option.]
// As mail still gets through, the warning message will cause error logs to fill up rapidly, so we
// suppress the standard errors in the cases when they will be generated and issue our own NOTICE error.
$disabled_functions = ini_get('disable_functions');
if (!empty($disabled_functions) && (mb_strpos($disabled_functions, 'escapeshell') !== FALSE) &&
in_array($mail_settings['admin_backend'], array('mail', 'sendmail')))
{
$message = "Your PHP system has one or both of the escapeshellarg() and escapeshellcmd() functions " .
"disabled and you are using the '" . $mail_settings['admin_backend'] . "' backend. " .
"PHPMailer will therefore not have used the -f option when sending mail.";
mail_debug($message);
trigger_error($message, E_USER_NOTICE);
$result = @$mail->postSend();
}
else
{
$result = $mail->postSend();
}
if ($result)
{
mail_debug('Email sent successfully');
return true;
}
}
error_log('Error sending email: ' . $mail->ErrorInfo);
mail_debug('Failed to send email: ' . $mail->ErrorInfo);
return false;
}
}