. Added for compatibility with user account object
* language = language object. User preferred of default language
* )
* NOTE: either snid, mail or uid is required.
*/
function simplenews_send_node($node, $accounts = array()) {
if (is_numeric($node)) {
$node = node_load($node);
}
if (is_object($node)) {
$spool_data['nid'] = $node->nid;
$spool_data['vid'] = $node->vid;
$spool_data['tid'] = $node->simplenews->tid;
// Get accounts subscribed to this newsletter.
// Using hook_simplenews_recipients modules can add recipients.
$recipients = simplenews_get_subscriptions_by_list($node->simplenews->tid);
foreach (module_implements('simplenews_recipients_alter') as $module) {
$function = $module . '_simplenews_recipients_alter';
$function($recipients, $node->simplenews->tid);
}
// Build data array of specified accounts.
// First we use the recipient data collected by hook_simplenews_recipients().
// If this fails we get the data from $accounts
if ($accounts) {
$temp_recipients = array();
foreach ($accounts as $account) {
if (isset($recipients[$account->mail])) {
$temp_recipients[$account->mail] = $recipients[$account->mail];
}
else {
// Set these accounts to 'subscribed'.
$account->status = 1;
$temp_recipients[$account->mail] = $account;
}
}
$recipients = $temp_recipients;
}
// To send the newsletter, the node id and target email addresses
// are stored in the spool.
// Only subscribed recipients are stored in the spool (status = 1).
db_query(
"INSERT INTO {simplenews_mail_spool} (mail, nid, vid, tid, status, timestamp, snid)
SELECT s.mail, :nid, :vid, t.tid, :stat, :time, s.snid
FROM {simplenews_subscriber} s
INNER JOIN {simplenews_subscription} t
ON s.snid = t.snid
WHERE s.activated = 1
AND t.tid = :tid
AND t.status = 1",
array(
':nid' => $spool_data['nid'],
':vid' => $spool_data['vid'],
':stat' => SIMPLENEWS_SPOOL_PENDING,
':time' => time(),
':tid' => $spool_data['tid'],
)
);
// When cron is not used the newsletter is send immediately to the emails
// in the spool. When cron is used newsletters are send to addresses in the
// spool during the next (and following) cron run.
if (variable_get('simplenews_use_cron', TRUE) == FALSE) {
simplenews_mail_spool($spool_data['nid'], $spool_data['vid'], 999999);
drupal_set_message(t('Newsletter sent.'));
simplenews_clear_spool();
}
else {
drupal_set_message(t('Newsletter pending.'));
}
}
}
/**
* Send test version of newsletter.
*
* @param integer or object $node Newsletter node to be sent. Integer = nid; Object = node object
*/
function simplenews_send_test($node, $test_addresses) {
if (is_numeric($node)) {
$node = node_load($node);
}
if (is_object($node)) {
// Prevent session information from being saved while sending.
if ($original_session = drupal_save_session()) {
drupal_save_session(FALSE);
}
// Force the current user to anonymous to ensure consistent permissions.
$original_user = $GLOBALS['user'];
$GLOBALS['user'] = drupal_anonymous_user();
// Send the test newsletter to the test address(es) specified in the node.
// Build array of test email addresses
// Send newsletter to test addresses.
// Emails are send direct, not using the spool.
$recipients = array('anonymous' => array(), 'user' => array());
foreach ($test_addresses as $mail) {
$mail = trim($mail);
if (!empty($mail)) {
$message = new stdClass();
$message->nid = $node->nid;
$message->vid = $node->vid;
$message->tid = $node->simplenews->tid;
$message->data = simplenews_get_subscription((object)array('mail' => $mail));
$account = _simplenews_user_load($mail);
$subscription = simplenews_get_subscription($account);
if ($account->uid) {
$recipients['user'][] = $account->name . ' <'.$mail.'>';
}
else {
$recipients['anonymous'][] = $mail;
}
//$tmpres = simplenews_mail_mail($node->nid, $node->vid, $mail, 'test');
$tmpres = simplenews_mail_mail($message, 'test');
}
}
if (count($recipients['user'])) {
$recipients_txt = implode(', ', $recipients['user']);
drupal_set_message(t('Test newsletter sent to user %recipient.', array('%recipient' => $recipients_txt)));
}
if (count($recipients['anonymous'])) {
$recipients_txt = implode(', ', $recipients['anonymous']);
drupal_set_message(t('Test newsletter sent to anonymous %recipient.', array('%recipient' => $recipients_txt)));
}
$GLOBALS['user'] = $original_user;
if ($original_session) {
drupal_save_session(TRUE);
}
}
}
/**
* Send a node to an email address.
*
* @param object $msgbase
* Mail message object as returned by simplenews_load_spool():
* $msgbase->nid
* $msgbase->vid
* $msgbase->tid
* $msgbase->data
* @param $key email key [node|test]
*
* @return TRUE if email is successfully delivered by php mail()
*/
function simplenews_mail_mail($msgbase, $key = 'node') {
static $cache;
$nid = $msgbase->nid;
$vid = $msgbase->vid;
$tid = $msgbase->tid;
$subscriber = $msgbase->data;
if (!$subscriber) {
$subscriber = simplenews_get_subscription((object)array('mail' => $msgbase->mail));
//$account = user_load($msgbase->uid));
}
$params['context']['account'] = $subscriber;
// Get node data for the mail
// Because node_load() only caches the most recent node revision
// we cache here based on nid and vid.
// TODO Investigate if this caching thing is still applicable to D7
if (isset($cache[$nid . ':' . $vid])) {
$node = $cache[$nid . ':' . $vid];
}
else {
$node = node_load($nid, $vid);
$cache[$nid . ':' . $vid] = $node;
}
if (is_object($node)) {
$params['context']['node'] = $node;
$params['context']['newsletter'] = simplenews_newsletter_load($tid);
$params['context']['category'] = simplenews_category_load($tid);
$params['from'] = _simplenews_set_from($params['context']['category']);
// Optional params for Mime Mail.
$params['plain'] = $params['context']['category']->format == 'plain' ? TRUE : NULL;
// @todo Create the plaintext portion of the message, we don't have $message['body'] here.
// $params['plaintext'] = $params['plain'] ? $message['body'] : simplenews_html_to_text($message['body'], TRUE);
// @todo Get the attachments. Upload module no longer exists for Drupal 7.
// $params['attachments'] = isset($params['context']['node']->files) ? $params['context']['node']->files : array();;
// Send mail
$message = drupal_mail('simplenews', $key, $subscriber->mail, $subscriber->language, $params, $params['from']['formatted']);
// Log sent result in watchdog.
if (variable_get('simplenews_debug', FALSE)) {
if (module_exists('mimemail')) {
$via_mimemail = t('Sent via Mime Mail');
}
//TODO Add line break before %mimemail.
if ($message['result']) {
watchdog('simplenews', 'Outgoing email. Message type: %type
Subject: %subject
Recipient: %to %mimemail', array('%type' => $key, '%to' => $message['to'], '%subject' => $message['subject'], '%mimemail' => $via_mimemail), WATCHDOG_DEBUG);
}
else {
watchdog('simplenews', 'Outgoing email failed. Message type: %type
Subject: %subject
Recipient: %to %mimemail', array('%type' => $key, '%to' => $message['to'], '%subject' => $message['subject'], '%mimemail' => $via_mimemail), WATCHDOG_ERROR);
}
}
// Build array of sent results for spool table and reporting.
if ($message['result']) {
$message['result'] = array(
'status' => SIMPLENEWS_SPOOL_DONE,
'error' => FALSE,
);
}
else {
// This error may be caused by faulty mailserver configuration or overload.
// Mark "pending" to keep trying.
$message['result'] = array(
'status' => SIMPLENEWS_SPOOL_PENDING,
'error' => TRUE,
);
}
}
else {
// Node could not be loaded. The node is probably deleted while pending to be sent.
// This error is not recoverable, mark "done".
$message['result'] = array(
'status' => SIMPLENEWS_SPOOL_DONE,
'error' => TRUE,
);
watchdog('simplenews', 'Newsletter not send: newsletter issue does not exist (nid = @nid; vid = @vid).', array('@nid' => $nid, '@vid' => $vid), WATCHDOG_ERROR);
}
return isset($message['result']) ? $message['result'] : FALSE;
}
/**
* Send simplenews newsletters from the spool.
*
* Individual newsletter emails are stored in database spool.
* Sending is triggered by cron or immediately when the node is saved.
* Mail data is retrieved from the spool, rendered and send one by one
* If sending is successful the message is marked as send in the spool.
* @todo Replace time(): http://drupal.org/node/224333#time
*/
function simplenews_mail_spool($nid = NULL, $vid = NULL, $limit = NULL) {
$check_counter = 0;
// Send pending messages from database cache
// A limited number of mails is retrieved from the spool
$limit = isset($limit) ? $limit : variable_get('simplenews_throttle', 20);
if ($spool_list = simplenews_get_spool(SIMPLENEWS_SPOOL_PENDING, $nid, $vid, $limit)) {
// Prevent session information from being saved while sending.
if ($original_session = drupal_save_session()) {
drupal_save_session(FALSE);
}
// Force the current user to anonymous to ensure consistent permissions.
$original_user = $GLOBALS['user'];
$GLOBALS['user'] = drupal_anonymous_user();
$count_fail = $count_success = 0;
_simplenews_measure_usec(TRUE);
foreach ($spool_list as $msid => $spool_data) {
$result = simplenews_mail_mail($spool_data);
// Update spool status.
// This is not optimal for performance but prevents duplicate emails
// in case of PHP execution time overrun.
simplenews_update_spool(array($msid), $result);
if ($result['status'] == SIMPLENEWS_SPOOL_DONE) {
$count_success++;
}
if ($result['error']) {
$count_fail++;
}
// Check every n emails if we exceed the limit.
// When PHP maximum execution time is almost elapsed we interrupt
// sending. The remainder will be sent during the next cron run.
if (++$check_counter >= SIMPLENEWS_SEND_CHECK_INTERVAL) {
$check_counter = 0;
// Break the sending if a percentage of max execution time was exceeded.
$elapsed = _simplenews_measure_usec();
if ($elapsed > SIMPLENEWS_SEND_TIME_LIMIT * $max_execution_time) {
watchdog('simplenews', 'Sending interrupted: PHP maximum execution time almost exceeded. Remaining newsletters will be sent during the next cron run. If this warning occurs regularly you should reduce the !cron_throttle_setting.', array('!cron_throttle_setting' => l(t('Cron throttle setting'), 'admin/config/simplenews/mail')), WATCHDOG_WARNING);
break;
}
}
}
// Report sent result and elapsed time. On Windows systems getrusage() is
// not implemented and hence no elapsed time is available.
if (function_exists('getrusage')) {
watchdog('simplenews', '%success emails sent in %sec seconds, %fail failed sending.', array('%success' => $count_success, '%sec' => round(_simplenews_measure_usec(), 1), '%fail' => $count_fail));
}
else {
watchdog('simplenews', '%success emails sent, %fail failed.', array('%success' => $count_success, '%fail' => $count_fail));
}
variable_set('simplenews_last_cron', REQUEST_TIME);
variable_set('simplenews_last_sent', $count_success);
// Restore the user.
$GLOBALS['user'] = $original_user;
if ($original_session) {
drupal_save_session(TRUE);
}
}
}
/**
* Save mail message in mail cache table.
*
* @param array $spool
* Data array to be stored in the spool table.
* $spool['mail']
* $spool['nid']
* $spool['vid']
* $spool['tid']
* $spool['status'] (Default: 1 = pending)
* $spool['time'] (default: current unix timestamp)
* @param array $spool Mail message array
* @todo Replace time(): http://drupal.org/node/224333#time
*/
function simplenews_save_spool($spool) {
$status = isset($spool['status']) ? $spool['status'] : SIMPLENEWS_SPOOL_PENDING;
$time = isset($spool['time']) ? $spool['time'] : REQUEST_TIME;
db_insert('simplenews_mail_spool')
->fields(array(
'mail' => $spool['mail'],
'nid' => $spool['nid'],
'vid' => $spool['vid'],
'tid' => $spool['tid'],
'snid' => $spool['snid'],
'status' => $status,
'timestamp' => $time,
'data' => serialize($spool['data']),
))
->execute();
}
/**
* Retrieve data from mail spool
*
* @param string $status Status of data to be retrieved (0 = hold, 1 = pending, 2 = send)
* @param integer $nid node id
* @param integer $vid node version id
* @param integer $limit The maximum number of mails to load from the spool
*
* @return array Spool data
* $spool['msid']
* $spool['mail']
* $spool['nid']
* $spool['tid']
* $spool['status']
* $spool['time']
* @todo Convert output to array of objects.
*/
function simplenews_get_spool($status, $nid = NULL, $vid = NULL, $limit = 0) {
$spool = array();
$query = db_select('simplenews_mail_spool', 's')
->fields('s')
->condition('s.status', $status)
->orderBy('s.timestamp', 'ASC');
if ($limit) {
$query->range(0, $limit);
}
foreach ($query->execute() as $row) {
if (strlen($row->data)) {
$row->data = unserialize($row->data);
}
else {
$row->data = simplenews_get_subscription((object)array('mail' => $row->mail));
}
$spool[$row->msid] = $row;
}
return $spool;
}
/**
* Update status of mail data in spool table.
*
* Time stamp is set to current time.
*
* @param array $msids
* Array of Mail spool ids to be updated
* @param array $data
* Array containing email sent results
* 'status' => (0 = hold, 1 = pending, 2 = send)
* 'error' => error id (optional; defaults to '')
*/
function simplenews_update_spool($msids, $data) {
db_update('simplenews_mail_spool')
->condition('msid', $msids)
->fields(array(
'status' => $data['status'],
'error' => isset($result['error']) ? (int)$data['error'] : 0,
'timestamp' => REQUEST_TIME,
))
->execute();
}
/**
* Count data in mail spool table.
*
* @param integer $nid newsletter node id
* @param integer $vid newsletter revision id
* @param string $status email sent status
*
* @return array Mail message array
*/
function simplenews_count_spool($nid, $vid, $status = SIMPLENEWS_SPOOL_PENDING) {
$query = db_query("SELECT msid FROM {simplenews_mail_spool} WHERE nid = :nid AND vid = :vid AND status = :status", array(':nid' => $nid, ':vid' => $vid, ':status' => $status));
return $query->rowCount();
}
/**
* Remove records from mail spool table.
*
* All records with status 'send' and time stamp before the expiration date
* are removed from the spool.
* @todo Replace time(): http://drupal.org/node/224333#time
*/
function simplenews_clear_spool() {
$expiration_time = REQUEST_TIME - variable_get('simplenews_spool_expire', 0) * 86400;
db_delete('simplenews_mail_spool')
->condition('status', SIMPLENEWS_SPOOL_DONE)
->condition('timestamp', $expiration_time, '<=')
->execute();
}
/**
* Update newsletter sent status.
*
* Set newsletter sent status based on email sent status in spool table.
* Translated and untranslated nodes get a different treatment.
*
* The spool table holds data for emails to be sent and (optionally)
* already send emails. The simplenews_newsletter table contains the overall
* sent status of each newsletter issue (node).
* Newsletter issues get the status pending when sending is initiated. As
* long as unsend emails exist in the spool, the status of the newsletter remains
* unsend. When no pending emails are found the newsletter status is set 'send'.
*
* Translated newsletters are a group of nodes that share the same tnid ({node}.tnid).
* Only one node of the group is found in the spool, but all nodes should share
* the same state. Therefore they are checked for the combined number of emails
* in the spool.
*/
function simplenews_send_status_update() {
$counts = array(); // number pending of emails in the spool
$sum = array(); // sum of emails in the spool per tnid (translation id)
$send = array(); // nodes with the status 'send'
// For each pending newsletter count the number of pending emails in the spool.
$query = db_select('simplenews_newsletter', 's');
$query->innerJoin('node', 'n', 's.nid = n.nid AND s.vid = n.vid');
$query->fields('s', array('nid', 'vid', 'tid'))
->fields('n', array('tnid'))
->condition('s.status', SIMPLENEWS_STATUS_SEND_PENDING);
foreach ($query->execute() as $newsletter) {
// nid-vid are combined in one unique key.
$counts[$newsletter->tnid][$newsletter->nid . '-' . $newsletter->vid] = simplenews_count_spool($newsletter->nid, $newsletter->vid);
}
// Determine which nodes are send per translation group and per individual node.
foreach ($counts as $tnid => $node_count) {
// The sum of emails per tnid is the combined status result for the group of translated nodes.
// Untranslated nodes have tnid == 0 which will be ignored later.
$sum[$tnid] = array_sum($node_count);
foreach ($node_count as $nidvid => $count) {
// Translated nodes (tnid != 0)
if ($tnid != '0' && $sum[$tnid] == '0') {
$send[] = $nidvid;
}
// Untranslated nodes (tnid == 0)
elseif ($tnid == '0' && $count == '0') {
$send[] = $nidvid;
}
}
}
// Update overall newsletter status
if (!empty($send)) {
foreach ($send as $nidvid) {
// Split the combined key 'nid-vid'
$nid = strtok($nidvid, '-');
$vid = strtok('-');
db_update('simplenews_newsletter')
->condition('nid', $nid)
->condition('vid', $vid)
->fields(array('status' => SIMPLENEWS_STATUS_SEND_READY))
->execute();
}
}
}
/**
* Build header array with priority and receipt confirmation settings.
*
* @param $node
* Newsletter category object.
* @param $from
* Newsletter from email address
*
* @return Header array with priority and receipt confirmation info
*/
function _simplenews_headers($category, $from) {
$headers = array();
// If receipt is requested, add headers.
if ($category->receipt) {
$headers['Disposition-Notification-To'] = $from;
$headers['X-Confirm-Reading-To'] = $from;
}
// Add priority if set.
switch ($category->priority) {
case SIMPLENEWS_PRIORITY_HIGHEST:
$headers['Priority'] = 'High';
$headers['X-Priority'] = '1';
$headers['X-MSMail-Priority'] = 'Highest';
break;
case SIMPLENEWS_PRIORITY_HIGH:
$headers['Priority'] = 'urgent';
$headers['X-Priority'] = '2';
$headers['X-MSMail-Priority'] = 'High';
break;
case SIMPLENEWS_PRIORITY_NORMAL:
$headers['Priority'] = 'normal';
$headers['X-Priority'] = '3';
$headers['X-MSMail-Priority'] = 'Normal';
break;
case SIMPLENEWS_PRIORITY_LOW:
$headers['Priority'] = 'non-urgent';
$headers['X-Priority'] = '4';
$headers['X-MSMail-Priority'] = 'Low';
break;
case SIMPLENEWS_PRIORITY_LOWEST:
$headers['Priority'] = 'non-urgent';
$headers['X-Priority'] = '5';
$headers['X-MSMail-Priority'] = 'Lowest';
break;
}
// Add general headers
$headers['Precedence'] = 'bulk';
return $headers;
}
/**
* Build formatted from-name and email for a mail object.
*
* Each newsletter category can have a different from address.
*
* @param $category
* Newsletter category object.
*
* @return Associative array with (un)formatted from address
* 'address' => From address
* 'formatted' => Formatted, mime encoded, from name and address
*/
function _simplenews_set_from($category = NULL) {
$address_default = variable_get('site_mail', ini_get('sendmail_from'));
$name_default = variable_get('site_name', 'Drupal');
if ($category) {
$address = $category->from_address;
$name = $category->from_name;
}
else {
$address = variable_get('simplenews_from_address', $address_default);
$name = variable_get('simplenews_from_name', $name_default);
}
// Windows based PHP systems don't accept formatted emails.
$formatted_address = substr(PHP_OS, 0, 3) == 'WIN' ? $address : '"' . mime_header_encode($name) . '" <' . $address . '>';
return array(
'address' => $address,
'formatted' => $formatted_address,
);
}
/**
* HTML to text conversion for HTML and special characters.
*
* Converts some special HTML characters in addition to drupal_html_to_text()
*
* @param string $text Source text with HTML and special characters
* @param boolean $inline_hyperlinks
* TRUE: URLs will be placed inline.
* FALSE: URLs will be converted to numbered reference list.
* @return string Target text with HTML and special characters replaced
*/
function simplenews_html_to_text($text, $inline_hyperlinks = TRUE) {
// By replacing tag by only its URL the URLs will be placed inline
// in the email body and are not converted to a numbered reference list
// by drupal_html_to_text().
// URL are converted to absolute URL as drupal_html_to_text() would have.
if ($inline_hyperlinks) {
$pattern = '@]+?href="([^"]*)"[^>]*?>(.+?)@is';
$text = preg_replace_callback($pattern, '_simplenews_absolute_mail_urls', $text);
}
// Replace some special characters before performing the drupal standard conversion.
$preg = _simplenews_html_replace();
$text = preg_replace(array_keys($preg), array_values($preg), $text);
// Perform standard drupal html to text conversion.
return drupal_html_to_text($text);
}
/**
* Helper function for simplenews_html_to_text().
*
* Replaces URLs with absolute URLs.
*/
function _simplenews_absolute_mail_urls($match) {
global $base_url, $base_path;
static $regexp;
$url = $label = '';
if ($match) {
if (empty($regexp)) {
$regexp = '@^' . preg_quote($base_path, '@') . '@';
}
list(, $url, $label) = $match;
$url = strpos($url, '://') ? $url : preg_replace($regexp, $base_url . '/', $url);
// If the link is formed by Drupal's URL filter, we only return the URL.
// The URL filter generates a label out of the original URL.
if (strpos($label, '...') === strlen($label) - 3) {
// Remove ellipsis from end of label.
$label = substr($label, 0, strlen($label) - 3);
}
if (strpos($url, $label) !== FALSE) {
return $url;
}
return $label . ' ' . $url;
}
}
/**
* Helper function for simplenews_html_to_text().
*
* List of preg* regular expression patterns to search for and replace with
*/
function _simplenews_html_replace() {
return array(
'/"/i' => '"',
'/>/i' => '>',
'/</i' => '<',
'/&/i' => '&',
'/©/i' => '(c)',
'/™/i' => '(tm)',
'/“/' => '"',
'/”/' => '"',
'/–/' => '-',
'/’/' => "'",
'/&/' => '&',
'/©/' => '(c)',
'/™/' => '(tm)',
'//' => '--',
'//' => '"',
'//' => '"',
'//' => '*',
'/®/i' => '(R)',
'/•/i' => '*',
'/€/i' => 'Euro ',
);
}
/**
* Helper function to measure PHP execution time in microseconds.
*
* @param bool $start TRUE reset the time and start counting.
* @return float: elapsed PHP execution time since start.
*/
function _simplenews_measure_usec($start = FALSE) {
// Windows systems don't implement getrusage(). There is no alternative.
if (!function_exists('getrusage')) {
return;
}
static $start_time;
$usage = getrusage();
$now = (float)($usage['ru_stime.tv_sec'] . '.' . $usage['ru_stime.tv_usec']) + (float)($usage['ru_utime.tv_sec'] . '.' . $usage['ru_utime.tv_usec']);
if ($start) {
$start_time = $now;
return;
}
return $now - $start_time;
}