innerJoin('simplenews_subscription', 't', 's.snid = t.snid'); $select->addField('s', 'mail'); $select->addField('s', 'snid'); $select->addField('t', 'tid'); $select->addExpression($node->nid, 'nid'); $select->addExpression(SIMPLENEWS_SUBSCRIPTION_STATUS_SUBSCRIBED, 'status'); $select->addExpression(REQUEST_TIME, 'timestamp'); $select->condition('t.tid', $node->simplenews->tid); $select->condition('t.status', SIMPLENEWS_SUBSCRIPTION_STATUS_SUBSCRIBED); $select->condition('s.activated', SIMPLENEWS_SUBSCRIPTION_ACTIVE); db_insert('simplenews_mail_spool') ->from($select) ->execute(); // Update simplenews newsletter status to send pending. simplenews_newsletter_update_sent_status($node); } /** * Send mail spool immediatly if cron should not be used. * * @param $conditions * (Optional) Array of spool conditions which are applied to the query. */ function simplenews_mail_attempt_immediate_send(array $conditions = array(), $use_batch = TRUE) { if (variable_get('simplenews_use_cron', TRUE)) { return FALSE; } if ($use_batch) { // Set up as many send operations as necessary to send all mails with the // defined throttle amount. $throttle = variable_get('simplenews_throttle', 20); $spool_count = simplenews_count_spool($conditions); $num_operations = ceil($spool_count / $throttle); $operations = array(); for ($i = 0; $i < $num_operations; $i++) { $operations[] = array('simplenews_mail_spool', array($throttle, $conditions)); } // Add separate operations to clear the spool and updat the send status. $operations[] = array('simplenews_clear_spool', array()); $operations[] = array('simplenews_send_status_update', array()); $batch = array( 'operations' => $operations, 'title' => t('Sending mails'), 'file' => drupal_get_path('module', 'simplenews') . '/includes/simplenews.mail.inc', ); batch_set($batch); } else { // Send everything that matches the conditions immediatly. simplenews_mail_spool(SIMPLENEWS_UNLIMITED, $conditions); simplenews_clear_spool(); simplenews_send_status_update(); } return TRUE; } /** * Send test version of newsletter. * * @param mixed $node * The newsletter node to be sent. * * @ingroup issue */ function simplenews_send_test($node, $test_addresses) { // 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)) { $subscriber = simplenews_subscriber_load_by_mail($mail); if (!$subscriber) { // The source expects a subscriber object with mail and language set. // @todo: Find a cleaner way to do this. $subscriber = new stdClass(); $subscriber->uid = 0; $subscriber->mail = $mail; $subscriber->language = $GLOBALS['language']->language; } if (!empty($account->uid)) { $recipients['user'][] = $account->name . ' <' . $mail . '>'; } else { $recipients['anonymous'][] = $mail; } $source = new SimplenewsSourceNode($node, $subscriber); $source->setKey('test'); $result = simplenews_send_source($source); } } 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 $source * The source object.s * * @return boolean * TRUE if the email was successfully delivered; otherwise FALSE. * * @ingroup source */ function simplenews_send_source(SimplenewsSourceInterface $source) { $params['simplenews_source'] = $source; // Send mail. $message = drupal_mail('simplenews', $source->getKey(), $source->getRecipient(), $source->getLanguage(), $params, $source->getFromFormatted()); // Log sent result in watchdog. if (variable_get('simplenews_debug', FALSE)) { if ($message['result']) { watchdog('simplenews', 'Outgoing email. Message type: %type
Subject: %subject
Recipient: %to', array('%type' => $source->getKey(), '%to' => $message['to'], '%subject' => $message['subject']), WATCHDOG_DEBUG); } else { watchdog('simplenews', 'Outgoing email failed. Message type: %type
Subject: %subject
Recipient: %to', array('%type' => $source->getKey(), '%to' => $message['to'], '%subject' => $message['subject']), WATCHDOG_ERROR); } } // Build array of sent results for spool table and reporting. if ($message['result']) { $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. $result = array( 'status' => SIMPLENEWS_SPOOL_PENDING, 'error' => TRUE, ); } return $result; } /** * 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: Redesign API to allow language counter in multilingual sends. * * @param $limit * (Optional) The maximum number of mails to send. Defaults to * unlimited. * @param $conditions * (Optional) Array of spool conditions which are applied to the query. * * @return * Returns the amount of sent mails. * * @ingroup spool */ function simplenews_mail_spool($limit = SIMPLENEWS_UNLIMITED, array $conditions = array()) { $check_counter = 0; // Send pending messages from database cache. $spool_list = simplenews_get_spool($limit, $conditions); if ($spool_list) { // Switch to the anonymous user. simplenews_impersonate_user(drupal_anonymous_user()); $count_fail = $count_success = 0; _simplenews_measure_usec(TRUE); $spool = new SimplenewsSpool($spool_list); while ($source = $spool->nextSource()) { $source->setKey('node'); $result = simplenews_send_source($source); // Update spool status. // This is not optimal for performance but prevents duplicate emails // in case of PHP execution time overrun. foreach ($spool->getProcessed() as $msid => $row) { $row_result = isset($row->result) ? $row->result : $result; simplenews_update_spool(array($msid), $row_result); if ($row_result['status'] == SIMPLENEWS_SPOOL_DONE) { $count_success++; } if ($row_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 && ini_get('max_execution_time') > 0) { $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 * ini_get('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; } } // It is possible that all or at the end some results failed to get // prepared, report them separately. foreach ($spool->getProcessed() as $msid => $row) { $row_result = isset($row->result) ? $row->result : $result; simplenews_update_spool(array($msid), $row_result); if ($row_result['status'] == SIMPLENEWS_SPOOL_DONE) { $count_success++; } if ($row_result['error']) { $count_fail++; } } } // 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); simplenews_revert_user(); return $count_success; } } /** * Save mail message in mail cache table. * * @param array $spool * The message to be stored in the spool table, as an array containing the * following keys: * - mail * - nid * - tid * - status: (optional) Defaults to SIMPLENEWS_SPOOL_PENDING * - time: (optional) Defaults to REQUEST_TIME. * * @ingroup spool */ 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'], 'tid' => $spool['tid'], 'snid' => $spool['snid'], 'status' => $status, 'timestamp' => $time, 'data' => serialize($spool['data']), )) ->execute(); } /* * Returns the expiration time for IN_PROGRESS status. * * @return int * A unix timestamp. Any IN_PROGRESS messages with a timestamp older than * this will be re-allocated and re-sent. */ function simplenews_get_expiration_time() { $timeout = variable_get('simplenews_spool_progress_expiration', 3600); $expiration_time = REQUEST_TIME - $timeout; return $expiration_time; } /** * This function allocates messages to be sent in current run. * * Drupal acquire_lock guarantees that no concurrency issue happened. * If the message status is SIMPLENEWS_SPOOL_IN_PROGRESS but the maximum send * time has expired, the message id will be returned as a message which is not * allocated to another process. * * @param $limit * (Optional) The maximum number of mails to load from the spool. Defaults to * unlimited. * @param $conditions * (Optional) Array of conditions which are applied to the query. If not set, * status defaults to SIMPLENEWS_SPOOL_PENDING, SIMPLENEWS_SPOOL_IN_PROGRESS. * * @return * An array of message ids to be sent in the current run. * * @ingroup spool */ function simplenews_get_spool($limit = SIMPLENEWS_UNLIMITED, $conditions = array()) { $messages = array(); // Add default status condition if not set. if (!isset($conditions['status'])) { $conditions['status'] = array(SIMPLENEWS_SPOOL_PENDING, SIMPLENEWS_SPOOL_IN_PROGRESS); } // Special case for the status condition, the in progress actually only // includes spool items whose locking time has expired. So this need to build // an OR condition for them. $status_or = db_or(); $statuses = is_array($conditions['status']) ? $conditions['status'] : array($conditions['status']); foreach ($statuses as $status) { if ($status == SIMPLENEWS_SPOOL_IN_PROGRESS) { $status_or->condition(db_and() ->condition('status', $status) ->condition('s.timestamp', simplenews_get_expiration_time(), '<') ); } else { $status_or->condition('status', $status); } } unset($conditions['status']); $query = db_select('simplenews_mail_spool', 's') ->fields('s') ->condition($status_or) ->orderBy('s.timestamp', 'ASC'); // Add conditions. foreach ($conditions as $field => $value) { $query->condition($field, $value); } /* BEGIN CRITICAL SECTION */ // The semaphore ensures that multiple processes get different message ID's, // so that duplicate messages are not sent. if (lock_acquire('simplenews_acquire_mail')) { // Get message id's // Allocate messages if ($limit > 0) { $query->range(0, $limit); } foreach ($query->execute() as $message) { if (strlen($message->data)) { $message->data = unserialize($message->data); } else { $message->data = simplenews_subscriber_load_by_mail($message->mail); } $messages[$message->msid] = $message; } if (count($messages) > 0) { // Set the state and the timestamp of the messages simplenews_update_spool( array_keys($messages), array('status' => SIMPLENEWS_SPOOL_IN_PROGRESS) ); } lock_release('simplenews_acquire_mail'); } /* END CRITICAL SECTION */ return $messages; } /** * 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, with the following keys: * - status: An integer indicating the updated status. Must be one of: * - 0: hold * - 1: pending * - 2: send * - 3: in progress * - error: (optional) The error id. Defaults to 0 (no error). * * @ingroup spool */ 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 $conditions * (Optional) Array of conditions which are applied to the query. Defaults * * @return * Count of mail spool elements of the passed in arguments. * * @ingroup spool */ function simplenews_count_spool(array $conditions = array()) { // Add default status condition if not set. if (!isset($conditions['status'])) { $conditions['status'] = array(SIMPLENEWS_SPOOL_PENDING, SIMPLENEWS_SPOOL_IN_PROGRESS); } $query = db_select('simplenews_mail_spool'); // Add conditions. foreach ($conditions as $field => $value) { $query->condition($field, $value); } $query->addExpression('COUNT(*)', 'count'); return (int)$query ->execute() ->fetchField(); } /** * Remove old records from mail spool table. * * All records with status 'send' and time stamp before the expiration date * are removed from the spool. * * @return * Number of deleted spool rows. * * @ingroup spool */ function simplenews_clear_spool() { $expiration_time = REQUEST_TIME - variable_get('simplenews_spool_expire', 0) * 86400; return db_delete('simplenews_mail_spool') ->condition('status', SIMPLENEWS_SPOOL_DONE) ->condition('timestamp', $expiration_time, '<=') ->execute(); } /** * Remove records from mail spool table according to the conditions. * * @return Count deleted * * @ingroup spool */ function simplenews_delete_spool(array $conditions) { $query = db_delete('simplenews_mail_spool'); foreach ($conditions as $condition => $value) { $query->condition($condition, $value); } return $query->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. * * @ingroup issue */ 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'); $query->fields('s', array('nid', 'tid')) ->fields('n', array('tnid')) ->condition('s.status', SIMPLENEWS_STATUS_SEND_PENDING); foreach ($query->execute() as $newsletter) { $counts[$newsletter->tnid][$newsletter->nid] = simplenews_count_spool(array('nid' => $newsletter->nid)); } // 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 $nid => $count) { // Translated nodes (tnid != 0) if ($tnid != '0' && $sum[$tnid] == '0') { $send[] = $nid; } // Untranslated nodes (tnid == 0) elseif ($tnid == '0' && $count == '0') { $send[] = $nid; } } } // Update overall newsletter status if (!empty($send)) { foreach ($send as $nid) { db_update('simplenews_newsletter') ->condition('nid', $nid) ->fields(array('status' => SIMPLENEWS_STATUS_SEND_READY)) ->execute(); } } } /** * Build formatted from-name and email for a mail object. * * @return Associative array with (un)formatted from address * 'address' => From address * 'formatted' => Formatted, mime encoded, from name and address */ function _simplenews_set_from() { $address_default = variable_get('site_mail', ini_get('sendmail_from')); $name_default = variable_get('site_name', 'Drupal'); $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 : '"' . $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 * The 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 * The 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; $regexp = &drupal_static(__FUNCTION__); $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 * If TRUE, reset the time and start counting. * * @return float * The elapsed PHP execution time since the last start. */ function _simplenews_measure_usec($start = FALSE) { // Windows systems don't implement getrusage(). There is no alternative. if (!function_exists('getrusage')) { return; } $start_time = &drupal_static(__FUNCTION__); $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; } /** * Build subject and body of the test and normal newsletter email. * * @param array $message * Message array as used by hook_mail(). * @param array $source * The SimplenewsSource instance. * * @ingroup source */ function simplenews_build_newsletter_mail(&$message, SimplenewsSourceInterface $source) { // Get message data from source. $message['headers'] = $source->getHeaders($message['headers']); $message['subject'] = $source->getSubject(); $message['body']['body'] = $source->getBody(); $message['body']['footer'] = $source->getFooter(); // Optional params for HTML mails. if ($source->getFormat() == 'html') { $message['params']['plain'] = NULL; $message['params']['plaintext'] = $source->getPlainBody() . "\n" . $source->getPlainFooter(); $message['params']['attachments'] = $source->getAttachments(); } else { $message['params']['plain'] = TRUE; } } /** * Build subject and body of the subscribe confirmation email. * * @param array $message * Message array as used by hook_mail(). * @param array $params * Parameter array as used by hook_mail(). */ function simplenews_build_subscribe_mail(&$message, $params) { $context = $params['context']; $langcode = $message['language']; // Use formatted from address "name" $message['headers']['From'] = $params['from']['formatted']; $message['subject'] = simplenews_subscription_confirmation_text('subscribe_subject', $langcode); $message['subject'] = token_replace($message['subject'], $context, array('sanitize' => FALSE)); if (simplenews_user_is_subscribed($context['simplenews_subscriber']->mail, $context['category']->tid)) { $body = simplenews_subscription_confirmation_text('subscribe_subscribed', $langcode); } else { $body = simplenews_subscription_confirmation_text('subscribe_unsubscribed', $langcode); } $message['body'][] = token_replace($body, $context, array('sanitize' => FALSE)); } /** * Build subject and body of the subscribe confirmation email. * * @param array $message * Message array as used by hook_mail(). * @param array $params * Parameter array as used by hook_mail(). */ function simplenews_build_combined_mail(&$message, $params) { $context = $params['context']; $changes = $context['changes']; $langcode = $message['language']; // Use formatted from address "name" $message['headers']['From'] = $params['from']['formatted']; $message['subject'] = simplenews_subscription_confirmation_text('combined_subject', $langcode); $message['subject'] = token_replace($message['subject'], $context, array('sanitize' => FALSE)); $changes_list = ''; $actual_changes = 0; foreach (simplenews_confirmation_get_changes_list($context['simplenews_subscriber'], $changes, $langcode) as $tid => $change) { $changes_list .= ' - ' . $change . "\n"; // Count the actual changes. $subscribed = simplenews_user_is_subscribed($context['simplenews_subscriber']->mail, $tid); if ($changes[$tid] == 'subscribe' && !$subscribed || $changes[$tid] == 'unsubscribe' && $subscribed) { $actual_changes++; } } // If there are actual changes, use the combined_body key otherwise use the // one without a confirmation link. $body_key = $actual_changes ? 'combined_body' : 'combined_body_unchanged'; $body = simplenews_subscription_confirmation_text($body_key, $langcode); // The changes list is not an actual token. $body = str_replace('[changes-list]', $changes_list, $body); $message['body'][] = token_replace($body, $context, array('sanitize' => FALSE)); } /** * Build subject and body of the unsubscribe confirmation email. * * @param array $message * Message array as used by hook_mail(). * @param array $params * Parameter array as used by hook_mail(). */ function simplenews_build_unsubscribe_mail(&$message, $params) { $context = $params['context']; $langcode = $message['language']; // Use formatted from address "name" $message['headers']['From'] = $params['from']['formatted']; $message['subject'] = simplenews_subscription_confirmation_text('subscribe_subject', $langcode); $message['subject'] = token_replace($message['subject'], $context, array('sanitize' => FALSE)); if (simplenews_user_is_subscribed($context['simplenews_subscriber']->mail, $context['category']->tid)) { $body = simplenews_subscription_confirmation_text('unsubscribe_subscribed', $langcode); $message['body'][] = token_replace($body, $context, array('sanitize' => FALSE)); } else { $body = simplenews_subscription_confirmation_text('unsubscribe_unsubscribed', $langcode); $message['body'][] = token_replace($body, $context, array('sanitize' => FALSE)); } } /** * A mail sending implementation that captures sent messages to a variable. * * This class is for running tests or for development and does not convert HTML * to plaintext. */ class SimplenewsHTMLTestingMailSystem implements MailSystemInterface { /** * Implements MailSystemInterface::format(). */ public function format(array $message) { // Join the body array into one string. $message['body'] = implode("\n\n", $message['body']); // Wrap the mail body for sending. $message['body'] = drupal_wrap_mail($message['body']); return $message; } /** * Implements MailSystemInterface::mail(). */ public function mail(array $message) { $captured_emails = variable_get('drupal_test_email_collector', array()); $captured_emails[] = $message; // @todo: This is rather slow when sending 100 and more mails during tests. // Investigate in other methods like APC shared memory. variable_set('drupal_test_email_collector', $captured_emails); return TRUE; } }