? tests
? translations
Index: mail_cron.php
===================================================================
RCS file: mail_cron.php
diff -N mail_cron.php
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ mail_cron.php	15 Mar 2009 21:42:38 -0000
@@ -0,0 +1,13 @@
+<?php
+// $Id$
+
+/**
+ * @file
+ * Handles incoming requests to fire off throttled mail tasks.
+ */
+
+include_once './includes/bootstrap.inc';
+drupal_bootstrap(DRUPAL_BOOTSTRAP_FULL);
+module_load_include('inc', 'simplenews', 'simplenews.cron');
+simplenews_cron_run();
+
Index: simplenews.admin.inc
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/simplenews/simplenews.admin.inc,v
retrieving revision 1.55
diff -u -p -r1.55 simplenews.admin.inc
--- simplenews.admin.inc	14 Mar 2009 02:01:38 -0000	1.55
+++ simplenews.admin.inc	15 Mar 2009 21:42:41 -0000
@@ -895,44 +895,115 @@ function simplenews_admin_settings_mail(
   $address_default = variable_get('site_mail', ini_get('sendmail_from'));
   $form = array();
 
-  $form['simplenews_mail_backend']['simplenews_use_cron'] = array(
+  $form['simplenews_use_cron'] = array(
     '#type' => 'checkbox',
     '#title' => t('Use cron to send newsletters'),
     '#default_value' => variable_get('simplenews_use_cron', TRUE),
     '#description' => t('When checked cron will be used to send newsletters (recommended). Test newsletters and confirmation emails will be send immediately. Leave unchecked for testing purposes.'),
   );
-  $throttle = drupal_map_assoc(array(1, 10, 20, 50, 100, 200, 500, 1000, 2000, 5000, 10000, 20000));
-  $throttle[999999] = t('Unlimited');
-  if (function_exists('getrusage')) {
-    $description_extra = '<br />'. t('Cron execution must not exceed the PHP maximum execution time of %max seconds. You find the time spend to send emails in the <a href="/admin/reports/dblog">Recent log entries</a>.', array('%max' => ini_get('max_execution_time')));
-  }
-  else {
-    $description_extra = '<br />'. t('Cron execution must not exceed the PHP maximum execution time of %max seconds.', array('%max' => ini_get('max_execution_time')));
-  }
-  $form['simplenews_mail_backend']['simplenews_throttle'] = array(
+
+  $form['host_mail_limit'] = array(
+    '#type' => 'markup',
+    '#title' => t('Cron mail limit'),
+    '#description' => t('The maximum number of emails that will be send. Use you hosters mail limit as starting point.'),
+    '#theme' => 'simplenews_input_sets',
+  );
+  $form['host_mail_limit']['simplenews_host_mail_limit_count'] = array(
+    '#type' => 'textfield',
+    '#field_suffix' => t('emails'),
+    '#size' => 10,
+    '#default_value' => variable_get('simplenews_host_mail_limit_count', '100'),
+  );
+  $form['host_mail_limit']['simplenews_host_mail_limit_time_value'] = array(
+    '#type' => 'textfield',
+    '#field_prefix' => t('per'),
+    '#size' => 10,
+    '#default_value' => variable_get('simplenews_host_mail_limit_time_value', '1'),
+  );
+  $form['host_mail_limit']['simplenews_host_mail_limit_time_unit'] = array(
     '#type' => 'select',
-    '#title' => t('Cron throttle'),
-    '#options' => $throttle,
-    '#default_value' => variable_get('simplenews_throttle', 20),
-    '#description' => t('Sets the numbers of newsletters sent per cron run. Failure to send will also be counted.') . $description_extra,
+    '#options' => array(
+      '1' => t('Seconds'),
+      '60' => t('Minutes'),
+      '3600' => t('Hours'),
+    ),
+    '#default_value' => variable_get('simplenews_host_mail_limit_time_unit', '3600'),
+  );
+
+  $form['cron_duration'] = array(
+    '#type' => 'markup',
+    '#title' => t('Mail cron duration'),
+    '#description' => t('Time between mail_cron.php calls. May differ from cron.php'),
+    '#theme' => 'simplenews_input_sets',
+  );
+  $form['cron_duration']['simplenews_mail_cron_duration_value'] = array(
+    '#type' => 'textfield',
+    '#default_value' => variable_get('simplenews_mail_cron_duration_value', '1'),
+    '#size' => 10,
   );
-  $form['simplenews_mail_backend']['simplenews_spool_expire'] = array(
+  $form['cron_duration']['simplenews_mail_cron_duration_unit'] = array(
+    '#type' => 'select',
+    '#options' => array(
+      '1' => t('Seconds'),
+      '60' => t('Minutes'),
+      '3600' => t('Hours'),
+    ),
+    '#default_value' => variable_get('simplenews_mail_cron_duration_unit', '3600'),
+  );
+
+  $form['simplenews_mail_throttle'] = array(
+    '#type' => 'radios',
+    '#title' => t('Mail method'),
+    '#options' => array('batch' => t('Batch'), 'throttle' => t('Throttle')),
+    '#default_value' => variable_get('simplenews_mail_throttle', 'batch'),
+    '#description' => t('Batch sends maximum limit in one batch and waits until batch time elapsed. Throttle sends emails one by one spread out over the batch time.'),
+  );
+/*
+  $form['simplenews_spool_expire'] = array(
     '#type' => 'select',
     '#title' => t('Mail spool expiration'),
     '#options' => array(0 => t('Immediate'), 1 => t('1 day'), 7 => t('1 week'), 14 => t('2 weeks')),
     '#default_value' => variable_get('simplenews_spool_expire', 0),
     '#description' => t('Newsletter mails are spooled. How long must messages be retained in the spool after successfull sending. Keeping the message in the spool allows mail statistics (which is not yet implemented). If cron is not used, immediate expiration is advised.'),
   );
-  $form['simplenews_mail_backend']['simplenews_debug'] = array(
+*/
+  $form['simplenews_debug'] = array(
     '#type' => 'checkbox',
     '#title' => t('Log emails'),
     '#default_value' => variable_get('simplenews_debug', FALSE),
     '#description' => t('When checked all outgoing simplenews emails are logged in the system log. A logged email does not guarantee that it is send or will be delivered. It only indicates that a message is send to the PHP mail() function. No status information is available of delivery by the PHP mail() function.'),
   );
+  $form['#validate'][] = 'simplenews_admin_settings_mail_validate';
+  $form['#submit'][] = 'simplenews_admin_settings_mail_submit';
 
   return system_settings_form($form);
 }
 
+function simplenews_admin_settings_mail_validate($form, &$form_state) {
+  $form_values = $form_state['values'];
+  if ($form_values['simplenews_host_mail_limit_count'] < 1) {
+    form_set_error('simplenews_host_mail_limit_count', t("Enter a positive number"));
+  }
+  if ($form_values['simplenews_host_mail_limit_time_value'] < 1) {
+    form_set_error('simplenews_host_mail_limit_time_value', t("Enter a positive number"));
+  }
+  if ($form_values['simplenews_mail_cron_duration_value'] < 1) {
+    form_set_error('simplenews_mail_cron_duration_value', t("Enter a positive number"));
+  }
+}
+
+function simplenews_admin_settings_mail_submit($form, &$form_state) {
+  $form_values = $form_state['values'];
+  if ($form_values['op'] == t('Reset to defaults')) {
+    variable_set('simplenews_host_mail_limit_time', 3600);
+    variable_set('simplenews_mail_cron_duration', 3600);
+  }
+  else {
+    variable_set('simplenews_host_mail_limit_time', $form_values['simplenews_host_mail_limit_time_value'] * $form_values['simplenews_host_mail_limit_time_unit']);
+    variable_set('simplenews_mail_cron_duration', $form_values['simplenews_mail_cron_duration_value'] * $form_values['simplenews_mail_cron_duration_unit']);
+  }
+}
+
 /**
  * Menu callback: Simplenews admin settings - Subscription.
  */
@@ -1264,3 +1335,30 @@ function template_preprocess_simplenews_
   }
   $variables['alt'] = $variables['title'] = $title[$variables['status']];
 }
+
+/**
+ * Theme mail limit settings.
+ */
+function theme_simplenews_input_sets($form) {
+  // Assuming only one of these fields contains #title and #descriptions
+  // The input fields are displayed in-line.
+
+  $title = $form['#title'];
+  unset($form['#title']);
+  $description = $form['#description'];
+  unset($form['#description']);
+
+  array_unshift($form, array('title' => array(
+    '#type' => 'markup',
+    '#prefix' => '<div class="form-item simplenews-input-sets">',
+    '#value' => '<label>'. $title .':</label>',
+  )));
+  $form['description'] = array(
+    '#type' => 'markup',
+    '#value' => '<div class="description">'. $description .'</div>',
+    '#weight' => '0.0099',
+    '#suffix' => '</div>',
+  );
+
+  return drupal_render($form);
+}
\ No newline at end of file
Index: simplenews.cron.inc
===================================================================
RCS file: simplenews.cron.inc
diff -N simplenews.cron.inc
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ simplenews.cron.inc	15 Mar 2009 21:42:41 -0000
@@ -0,0 +1,270 @@
+<?php
+
+/**
+ * Executes a cron run when called
+ * @return
+ * Returns TRUE if ran successfully
+ */
+function simplenews_cron_run() {
+  // Limit the execution time to prevent being kicked out before we're finished.
+  // In case we are kicked out, simplenews_cron_cleanup() cleans up the mess we left behind.
+  if (ini_get('safe_mode')) {
+    $php_max_allowed_time = ini_get('max_execution_time') * SIMPLENEWS_EXECUTION_TIME_CUT_OFF;
+  }
+  else {
+    set_time_limit(SIMPLENEWS_MAX_EXECUTION_TIME);
+    $php_max_allowed_time = SIMPLENEWS_MAX_EXECUTION_TIME * SIMPLENEWS_EXECUTION_TIME_CUT_OFF;
+  }
+    
+
+  // Fetch the cron semaphore
+  $semaphore = variable_get('simplenews_cron_semaphore', FALSE);
+
+  if ($semaphore) {
+    if (time() - $semaphore > variable_get('simplenews_mail_cron_duration', '3600')) {
+      // Either cron has been running for more than an hour or the semaphore
+      // was not reset due to a database error.
+      watchdog('cron', 'Simplenews mail cron has been running for more than an hour and is most likely stuck.', array(), WATCHDOG_ERROR);
+
+      // Release cron semaphore
+      variable_del('simplenews_cron_semaphore');
+    }
+    else {
+      // Cron is still running normally.
+      watchdog('cron', 'Attempting to re-run mail cron while it is already running.', array(), WATCHDOG_WARNING);
+    }
+  }
+  else {
+    // Register shutdown callback
+    register_shutdown_function('simplenews_cron_cleanup');
+
+    // Lock cron semaphore
+    variable_set('simplenews_cron_semaphore', time());
+    
+    // We limit the run time to prevent being overrun by the next cron.
+    $cron_max_allowed_time = variable_get('simplenews_mail_cron_duration', '3600') * SIMPLENEWS_CRON_CUT_OFF;
+    $cron_start_time = time();
+    $cron_deadline = $cron_start_time + $cron_max_allowed_time;
+    if (!function_exists('getrusage')) {
+      _simplenews_measure_usec('start');
+    }
+    
+    // We send in two flavours: throttle and batch.
+    // Throttle sends emails one by one with a lag inbetween. The lag is
+    // determined by the hosters mail limit.
+    // Batches sends one batch of emails and waits for the next batch.
+    /*
+     * Throttle:
+     * |  |  |  |  |  |  | |  |  |  |  |  |  | |  |  |  |  |  |  |
+     * -------------------+-------------------+-------------------+
+     *
+     * Batch:
+     * |||||||             |||||||             |||||||             
+     * -------------------+-------------------+-------------------+
+     */
+    if (variable_get('simplenews_mail_throttle', 'batch') == 'throttle') {
+      $batch_size = 1;
+    }
+    else {
+      $batch_size = variable_get('simplenews_host_mail_limit_count', '100');
+    } 
+    $batch_duration = variable_get('simplenews_host_mail_limit_time', '3600') * $batch_size / variable_get('simplenews_host_mail_limit_count', '100') ;
+
+    $mail_sent = 0;
+    $mail_fail = 0;
+    $batch_eta = time();
+    
+    /* user settings:
+     * - limit per time interval (e.g. 500 mails)   variable_get('simplenews_host_mail_limit_count', '100');
+     * - time interval (e.g. 1 hour)                variable_get('simplenews_host_mail_limit_time', '3600');
+     * - mail cron duration (e.g. 1 hour)           variable_get('simplenews_mail_cron_duration', '3600');
+     * - use batch or throttle (default: batch)     variable_get('simplenews_mail_throttle', 'batch');
+     *
+     * System parameters
+     * - safety period (% of cron interval)         SIMPLENEWS_CRON_CUT_OFF
+     * - lag interval (min: 1sec.)                  $lag
+     * - batch size (# mail)                        $batch_size
+     * - PHP max execution time                     $php_max_allowed_time
+     * - Maximum duration of a cron job             $cron_max_allowed_time
+     * 
+     *
+     * Future: (auto)detect
+     * - cron interval: based on detection of duplicate calls to mail_cron.php
+     * - php execution time out: based on a timer in database which is reset when mail_cron ends normally
+     */
+
+    /*
+     * set_time_limit($seconds)
+     * ini_get("safe_mode") == TRUE
+     * 
+     * Precautions in case of Safe mode
+     * "Safe mode sets a maximum script execution time": max_execution_time can not be changed by set_time_limit()
+     *
+     * When PHP process is killed by max execution time or host, functions registered by register_shutdown_function() are still executed!
+     *
+     *   max_execution_time  integer
+     *   This sets the maximum time in seconds a script is allowed to run before it is terminated by the parser.
+     *   This helps prevent poorly written scripts from tying up the server. The default setting is 30.
+     *   When running PHP from the command line the default setting is 0.
+     *   
+     *   You can not change this setting with ini_set() when running in safe mode.
+     *   The only workaround is to turn off safe mode or by changing the time limit in the php.ini.
+     *   
+     *   Your web server can have other timeout configurations that may also interrupt PHP execution.
+     *   Apache has a Timeout directive and IIS has a CGI timeout function. Both default to 300 seconds.
+     *   See your web server documentation for specific details.
+     */
+
+    /* Precautions in case of Windows systems
+     * Windows systems do not implement getrusage(). This makes it impossible to measure the PHP execution time.
+     * Is microtime() an alternative for window systems?
+     */
+
+
+    do {
+      // For each batch of n emails we plan a duration. We send the n emails
+      // and wait the remaining time of duration before the next batch.
+      // Batch ETA is in clock time.
+      $batch_eta += $batch_duration;
+//print "batch_duration=$batch_duration\n";
+//print "batch_eta=$batch_eta\n";
+//print "lag (plan)=$lag \n";
+//print "batch_size=$batch_size \n";
+
+      // Send batch of emails and record the results.
+      $result = simplenews_mail_spool(NULL, NULL, $batch_size);
+      $mail_fail += $result['mail_fail'];
+      $mail_sent += $result['mail_sent'];
+
+//print "mail_fail=$mail_fail \n";
+//print "mail_sent=$mail_sent \n";
+
+      // Check for cron time overrun or expected overrun during next batch, in
+      // which case we cancel this cron operation.
+      $predicted_time = _simplenews_average_usec();
+//print "predicted_time=$predicted_time \n";
+      if ($result['cron_time_exceeded'] = $batch_eta >= $cron_deadline || $batch_eta + (int)$predicted_time/1000000 >= $cron_deadline) {
+        break;
+      }
+
+      // If all emails have been send or if php execution time has exceeded
+      // we cancel the cron operation.
+      if ($result['spool_empty'] || $result['php_time_exceeded']) {
+        break;
+      }
+
+      // Calculate the time we have to wait until the next batch.
+      // Waiting time is calculated in clock time (integer, full seconds).
+      $batch_remaining_time = $batch_eta - time();
+      $lag = $batch_remaining_time < 0 ? 0 : $batch_remaining_time;
+
+      /*
+       * TODO
+       * Check if we systematically overrun the batch time. Which is caused by
+       * a too high number of emails for the server to handle in the available
+       * batch time.
+       * In this case we can 1. report, 2. decrease the batch size or 3. increase the batch time.
+       */
+
+//print "batch_remaining_time=$batch_remaining_time \n";
+//print "lag (check)=$lag \n";
+    
+      // Wait until end of batch time.
+      _simplenews_sleep((int)$lag);
+
+//print "-------------------------------------\n";
+//flush();
+    } while (true);
+
+    // Debugging
+    $debug[] = $result['all_sent'] ? 'All emails sent' : '';
+    $debug[] = $result['spool_empty'] ? 'Spool is empty' : '';
+    $debug[] = $result['php_time_exceeded'] ? 'PHP execution time exceeded' : '';
+    $debug[] = $result['batch_time_exceeded'] ? 'Batch time elapsed' : '';
+    $debug[] = 'Emails sent: '. $mail_sent;
+    $debug[] = 'Emails failed: '. $mail_fail;
+//print implode(', ', $debug);
+//print t('Simplenews mail cron run completed. %succes emails sent, %fail emails failed (will be resend later).', array('%succes' => $mail_sent, '%fail' => $mail_fail));
+
+    // Record results
+    variable_set('simplenews_last_cron', time());
+    variable_set('simplenews_last_sent', $mail_fail + $mail_sent);
+    variable_set('simplenews_last_php_time', _simplenews_measure_usec());
+    if ($mail_fail) {
+      //TODO Plural translation
+      watchdog('cron', 'Simplenews mail cron completed. %succes emails sent, %fail emails failed (will be resend later).', array('%succes' => $mail_sent, '%fail' => $mail_fail), WATCHDOG_NOTICE);      
+    }
+    else {
+      watchdog('cron', 'Simplenews mail cron completed. %succes emails sent, 0 emails failed.', array('%succes' => $mail_sent), WATCHDOG_NOTICE);
+    }
+
+    // Release cron semaphore
+    variable_del('simplenews_cron_semaphore');
+
+    // Return TRUE so other functions can check if it did run successfully
+    return TRUE;
+  }
+}
+
+function simplenews_cron_cleanup() {
+  // See if the semaphore is still locked.
+  if (variable_get('simplenews_cron_semaphore', FALSE)) {
+    watchdog('cron', 'Mail cron run exceeded the time limit and was aborted.', array(), WATCHDOG_WARNING);
+
+    // Release cron semaphore
+    variable_del('simplenews_cron_semaphore');
+  }
+}
+
+/**
+ * Calculate the moving average of php execution time of the last n function calls.
+ *
+ * n is set in SIMPLENEWS_AVERAGE_BATCH_COUNT
+ *
+ * @return float
+ *   Average number of microseconds
+ */
+function _simplenews_average_usec() {
+  static $previous_usec = array();
+  
+  $usec = _simplenews_measure_usec();
+  array_unshift($previous_usec, $usec);
+  $count = count($previous_usec);
+  if ($count == SIMPLENEWS_AVERAGE_BATCH_COUNT + 1) {
+    unset($previous_usec[SIMPLENEWS_AVERAGE_BATCH_COUNT]);
+  }
+  $average = array_sum($previous_usec) / $count;
+
+  // The first averages are not reliable as prediction.
+  // We return half to stay on the safe side.
+  if ($count > 2) {  
+    return $average;
+  }
+  else {
+    return $average / 2;
+  }
+}
+
+/**
+ * Sleep and time adjustment.
+ *
+ * In a Windows based PHP system we measure the php execution time by 
+ * cmeasuring the lock time except for the duration of the sleep.
+ * This function pauses the time measurement before sleep and resumes afterwards.
+ *
+ * @param int $seconds
+ *   Number of seconds to sleep.
+ */
+function _simplenews_sleep($seconds) {
+  // Linux based PHP
+  if (function_exists('getrusage')) {
+    sleep($seconds);
+  }
+  // Windows based PHP
+  else {
+    _simplenews_measure_usec('pause');
+    sleep($seconds);
+    _simplenews_measure_usec('resume');
+  }
+  return;
+}
Index: simplenews.css
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/simplenews/simplenews.css,v
retrieving revision 1.7
diff -u -p -r1.7 simplenews.css
--- simplenews.css	3 May 2008 13:21:19 -0000	1.7
+++ simplenews.css	15 Mar 2009 21:42:41 -0000
@@ -28,3 +28,12 @@
 .block-simplenews .issues-list .newsletter-created {
   display: none;
 }
+
+/* Simplenews admin */
+
+.simplenews-input-sets .form-item
+{
+  display: inline;
+  white-space: nowrap;
+  margin-right: 0.5ex;
+}
\ No newline at end of file
Index: simplenews.module
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/simplenews/simplenews.module,v
retrieving revision 1.206
diff -u -p -r1.206 simplenews.module
--- simplenews.module	14 Mar 2009 02:01:38 -0000	1.206
+++ simplenews.module	15 Mar 2009 21:42:46 -0000
@@ -54,9 +54,31 @@ define('SIMPLENEWS_SPOOL_SEND', 2);
 define('SIMPLENEWS_SEND_CHECK_INTERVAL', 100);
 
 /**
- * AT 80% OF PHP MAX EXECUTION TIME EMAIL SENDING IS INTERRUPTED
+ * EMAIL SENDING IS INTERRUPTED AT PERCENTAGE OF MAX EXECUTION TIME
  */
-define('SIMPLENEWS_SEND_TIME_LIMIT', 0.8);
+define('SIMPLENEWS_EXECUTION_TIME_CUT_OFF', 0.9);
+
+/**
+ * EMAIL SENDING IS INTERRUPTED AT PERCENTAGE OF CRON TIME
+ */
+define('SIMPLENEWS_CRON_CUT_OFF', 0.95);
+
+/**
+ * INCREASED MAX PHP EXECUTION TIME DURING mail_cron EXECUTION
+ */
+define('SIMPLENEWS_MAX_EXECUTION_TIME', 600);
+
+/**
+ * NUMBER OF BATCHES OVER WHICH THE MOVING AVERAGE OF PHP EXECUTION TIME IS CALCULATED.
+ */
+define('SIMPLENEWS_AVERAGE_BATCH_COUNT', 10);
+
+/**
+ * SENDING EMAILS TAKES LONGER THAN THE AVAILABLE BATCH DURATION.
+ * THE BATCH DURATION IS INCREASED WITH A FACTOR OF THE SURPLUS TIME.
+ * NEW TIME = OLD TIME + SURPLUS TIME * SIMPLENEWS_BATCH_DURATION_CORRECTION
+ */
+//define('SIMPLENEWS_BATCH_DURATION_CORRECTION', 2);
 
 /**
  * Implementation of hook_perm().
@@ -1418,7 +1440,7 @@ function simplenews_send_test($node) {
         $result = simplenews_mail_mail($node->nid, $node->vid, $mail, 'test');
       }
     }
-    if ($result) {
+    if ($result['result']) {
       drupal_set_message(t('Test newsletter sent to %recipient.', array('%recipient' => $node->simplenews['test_address'])));
     }
   }
@@ -1673,34 +1695,57 @@ function simplenews_mail_mail($nid, $vid
     watchdog('simplenews', 'Newsletter not send: node does not exist (nid = @nid; vid = @vid).', array('@nid' => $message['nid'], '@vid' => $message['vid']), WATCHDOG_ERROR);
   }
 
-  return isset($message['result']) ? $message['result'] : FALSE;
+  return $message;
 }
 
 /**
  * Send simplenews newsletters from the spool.
  *
- * Iindividual newsletter emails are stored in database spool.
+ * Individual newsletter emails are stored in database spool.
  * Sending is triggered by cron or immediately when the node is saved.
  * Mail data is retreived from the spool, rendered and send one by one
  * If sending is succesful the message is marked as send in the spool.
- */
-function simplenews_mail_spool($nid = NULL, $vid = NULL, $limit = NULL) {
+ *
+ * @param int $nid
+ *   node id
+ * @param int $vid
+ *   version id
+ * @param int $limit
+ *   maximum number of emails to send
+ * @return array of results
+ *   'php_time_exceeded'  TRUE: php max execution time has almost elapsed
+ *   'all_sent'           TRUE: all
+ *   'spool_empty'        TRUE: all emails in the spool have been sent.
+ *   'mail_sent'          number of emails successfully sent
+ *   'mail_fail'          number of emails failed sending
+ */
+function simplenews_mail_spool($nid = NULL, $vid = NULL, $limit = 0) {
+  $result['php_time_exceeded'] = FALSE;
+  $result['spool_empty'] = FALSE;
+  $mail_sent = 0;
+  $mail_fail = 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 ($messages = simplenews_get_spool(SIMPLENEWS_SPOOL_PENDING, $nid, $vid, $limit)) {
-    $mail_sent = array();
-    _simplenews_measure_usec(TRUE);
+    _simplenews_measure_usec('start');
 
     foreach ($messages as $key => $message) {
       $result = simplenews_mail_mail($message['nid'], $message['vid'], $message['mail']);
+      $result['result'] = TRUE;
 
       // Update spool status if mail is sent succesfully.
       // This is not optimal for performance but prevents duplicate emails
       // in case of PHP execution time overrun.
-      if ($result) {
+      if ($result['result']) {
+        // TODO:
+        // move this to the end of the foreach loop instead of after each email.
+        // also put this in register_shutdown_function() to be exected in case of php time overrun
         simplenews_update_spool(array($key), SIMPLENEWS_SPOOL_SEND);
-        $mail_sent[] = $key;
+        $mail_sent++;
+      }
+      else {
+        $mail_fail++;
       }
 
       // Check every n emails if we exceed the limit.
@@ -1710,29 +1755,36 @@ function simplenews_mail_spool($nid = NU
         $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')) {
+        if ($elapsed > SIMPLENEWS_EXECUTION_TIME_CUT_OFF * ini_get('max_execution_time')) {
           watchdog('simplenews', 'Sending interrupted: PHP maximum execution time almost exceeded. Remaining newsletters will be send 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/settings/simplenews/mail')), WATCHDOG_WARNING);
+          $result['php_time_exceeded'] = TRUE;
           break;
         }
       }
     }
 
-    // Update sent status for newsletter admin panel.
-    simplenews_send_status_update();
+    // Log results and elapsed time.
+    watchdog('simplenews', '%count emails sent in %sec seconds.', array('%count' => count($mail_sent), '%sec' => round(_simplenews_measure_usec(), 1)));
 
-    // Report sent result and elapsed time. On Windows systems retrusage() is
-    // not implemented and hence no elapsed time is available.
-    if (function_exists('getrusage')) {
-      watchdog('simplenews', '%count emails sent in %sec seconds.', array('%count' => count($mail_sent), '%sec' => round(_simplenews_measure_usec(), 1)));
-    }
-    else {
-      watchdog('simplenews', '%count emails sent.', array('%count' => count($mail_sent)));
-    }
-    
+    // Update results for reporting in newsletter admin panel.
+    simplenews_send_status_update();
     variable_set('simplenews_last_cron', time());
     variable_set('simplenews_last_sent', count($mail_sent));
   }
+  else {
+    $result['spool_empty'] = TRUE;
+  }
 
+  if ($mail_sent + $mail_fail < $limit) {
+    $result['all_sent'] = TRUE;
+    $result['spool_empty'] = TRUE;
+  }
+  else {
+    $result['all_sent'] = ($mail_sent + $mail_fail) == $limit;
+  }
+  $result['mail_sent'] = $mail_sent;
+  $result['mail_fail'] = $mail_fail;
+  return $result;
 }
 
 /**
@@ -1773,10 +1825,15 @@ function simplenews_save_spool($message)
  *  $message['status']
  *  $message['time']
  */
-function simplenews_get_spool($status, $nid = NULL, $vid = NULL, $limit = 999999) {
+function simplenews_get_spool($status, $nid = NULL, $vid = NULL, $limit = 0) {
   $messages = array();
 
-  $result = db_query_range("SELECT * FROM {simplenews_mail_spool} s WHERE s.status = %d ORDER BY s.timestamp ASC", $status, 0, $limit);
+  if ($limit) {
+    $result = db_query_range("SELECT * FROM {simplenews_mail_spool} s WHERE s.status = %d ORDER BY s.timestamp ASC", $status, 0, $limit);
+  }
+  else {
+    $result = db_query("SELECT * FROM {simplenews_mail_spool} s WHERE s.status = %d ORDER BY s.timestamp ASC", $status);
+  }
   while ($data = db_fetch_array($result)) {
     $message = array();
     foreach ($data as $key => $value) {
@@ -1882,6 +1939,7 @@ function simplenews_send_status_update()
       db_query("UPDATE {simplenews_newsletters} SET s_status = '%s' WHERE nid = %d AND vid = %d", SIMPLENEWS_STATUS_SEND_READY, $nid, $vid);
     }
   }
+  return $result;
 }
 
 /**
@@ -2263,22 +2321,39 @@ function _simplenews_subscription_confir
 /**
  * Helper function to measure PHP execution time in microseconds.
  *
- * @param bool $start TRUE reset the time and start counting.
+ * Windows systems don't implement getrusage(), we use microtime() instead.
+ * Because microtime is a clock that keeps ticking during sleep() and usleep()
+ * the measurement must be pauzed before and resumed after sleep.
+ * 
+ * @param string $op
+ *   'start' resets the timer.
+ *   'pauze' pauzes the timer (use for Windows system only).
+ *   'resume' continues timing (use for Windows system only).
  * @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 0;
-  }
-  
+function _simplenews_measure_usec($op = '') {
   static $start_time;
-  $usage = getrusage();
-  $now = (float)($dat["ru_stime.tv_sec"] .'.'. $dat["ru_stime.tv_usec"]) + (float)($usage["ru_utime.tv_sec"] .'.'. $usage["ru_utime.tv_usec"]);
+  static $pauze_time;
 
-  if ($start) {
-    $start_time = $now;
-    return 0;
+  if (function_exists('getrusage')) {
+    $usage = getrusage();
+    $now = (float)($dat["ru_stime.tv_sec"] .'.'. $dat["ru_stime.tv_usec"]) + (float)($usage["ru_utime.tv_sec"] .'.'. $usage["ru_utime.tv_usec"]);
+  }
+  else {
+    $now = microtime(TRUE);
+  }
+
+  switch ($op) {
+    case 'start':
+      $start_time = $now;
+      break;
+    case 'pause':
+      $pauze_time = $now;
+      break;
+    case 'resume':
+      $pauzed = $now - $pauze_time;
+      $start_time += $pauzed;
+      break;
   }
   return $now - $start_time;
 }
@@ -2336,6 +2411,10 @@ function simplenews_theme() {
       'file' => 'simplenews.admin.inc',
       'arguments' => array('form' => NULL),
     ),
+    'simplenews_input_sets' => array(
+      'file' => 'simplenews.admin.inc',
+      'arguments' => array('form' => NULL),
+    ),
   );
 }
 
