Index: privatemsg.module
===================================================================
RCS file: /cvs/drupal/contributions/modules/privatemsg/privatemsg.module,v
retrieving revision 1.70.2.30.2.91.2.27
diff -u -p -r1.70.2.30.2.91.2.27 privatemsg.module
--- privatemsg.module	10 Apr 2009 15:25:48 -0000	1.70.2.30.2.91.2.27
+++ privatemsg.module	11 Apr 2009 22:33:24 -0000
@@ -266,6 +266,38 @@ function private_message_settings() {
     '#default_value' => variable_get('privatemsg_display_loginmessage', TRUE),
     '#description' => t('This option can safely be disabled if the "New message indication" block is used instead.'),
   );
+
+  $form['flush_deleted'] = array(
+    '#type'        => 'fieldset',
+    '#collapsible' => TRUE,
+    '#collapsed'   => TRUE,
+    '#title'       => t('Flush deleted messages'),
+    '#description' => t('By default, deleted messages are only hidden from the
+                         user but still stored in the database. These settings
+                         control if and when messages should be removed.'),
+  );
+
+  $form['flush_deleted']['privatemsg_flush_enabled'] = array(
+    '#type'          => 'checkbox',
+    '#title'         => t('Flush deleted messages'),
+    '#default_value' => variable_get('privatemsg_flush_enabled', FALSE),
+    '#description'   => t('Enable the flushing of deleted messages. Requires that cron is enabled'),
+  );
+
+  $form['flush_deleted']['privatemsg_flush_days'] = array(
+    '#type' => 'select',
+    '#title' => t('Flush messages after they have been deleted for more days than'),
+    '#default_value' => variable_get('privatemsg_flush_days', 30),
+    '#options' => drupal_map_assoc(array(0, 1, 2, 5, 10, 30, 100)),
+  );
+
+  $form['flush_deleted']['privatemsg_flush_max'] = array(
+    '#type' => 'select',
+    '#title' => t('Maximum number of messages to flush per cron run'),
+    '#default_value' => variable_get('privatemsg_flush_max', 200),
+    '#options' => drupal_map_assoc(array(50, 100, 200, 500, 1000)),
+  );
+
   $form['privatemsg_listing'] = array(
     '#type' => 'fieldset',
     '#title' => t('Configure listings'),
@@ -293,6 +325,50 @@ function private_message_settings_submit
   drupal_rebuild_theme_registry();
 }
 
+/**
+ * hook_cron implementation, flushes deleted queries
+ */
+function privatemsg_cron() {
+  if (variable_get('privatemsg_flush_enabled', FALSE)) {
+    $query = _privatemsg_assemble_query('deleted', variable_get('privatemsg_flush_days', 30));
+    $result = db_query($query['query']);
+
+    $flushed = 0;
+    while (($row = db_fetch_array($result)) && ($flushed < variable_get('privatemsg_flush_max', 200))) {
+      $message = _privatemsg_load($row['mid']);
+      module_invoke_all('privatemsg_message_flush', $message);
+
+      // Delete recipients
+      db_query('DELETE FROM {pm_index} WHERE mid = %d', $row['mid']);
+      // Delete message
+      db_query('DELETE FROM {pm_message} WHERE mid = %d', $row['mid']);
+      $flushed++;
+    }
+  }
+}
+
+/**
+ * Sql function for the deleted messages query
+ *
+ * @param array $fragments Query-fragments
+ * @param int   $days      Select messages older than x days
+ */
+function privatemsg_sql_deleted(&$fragments, $days) {
+  $fragments['primary_table'] = '{pm_message} pm';
+
+  $fragments['select'][] = 'pm.mid';
+  $fragments['select'][] = 'MIN(pmi.deleted) as is_deleted';
+  $fragments['select'][] = 'MAX(pmi.deleted) as last_deleted';
+
+  $fragments['inner_join'][] = 'INNER JOIN {pm_index} pmi ON (pmi.mid = pm.mid)';
+
+  $fragments['group_by'][] = 'pm.mid';
+
+  $fragments['having'][] = 'MIN(pmi.deleted) > 0';
+  $fragments['having'][] = 'MAX(pmi.deleted) < %d';
+  $fragments['query_args'][] = time() - $days * 86400;
+}
+
 function privatemsg_theme() {
   return array(
     'privatemsg_view'    => array(
@@ -1080,17 +1156,12 @@ function privatemsg_message_change_delet
   }
   $message = _privatemsg_load($pmid, $account);
 
-  db_query('UPDATE {pm_index} SET deleted = %d WHERE mid = %d AND uid = %d', $delete, $pmid, $account->uid);
-
-  $result = db_query("SELECT MIN(deleted) AS deleted_by_all FROM {pm_index} WHERE mid = %d", $pmid);
-  $deleted = db_fetch_array($result);
-
-  $deleted_by_all = FALSE;
-  if ($deleted['deleted_by_all'] == 0) {
-    $deleted_by_all = TRUE;
+  $delete_value = 0;
+  if ($delete == TRUE) {
+    $delete_value = time();
   }
-
-  module_invoke_all('privatemsg_message_delete', $message, $deleted_by_all);
+  db_query('UPDATE {pm_index} SET deleted = %d WHERE mid = %d AND uid = %d', $delete_value, $pmid, $account->uid);
+  module_invoke_all('privatemsg_message_delete', $message, $delete);
 }
 
 function privatemsg_delete_submit($form, &$form_state) {
@@ -1300,13 +1371,15 @@ function _privatemsg_load($pmid, $accoun
   $query = _privatemsg_assemble_query('load', $pmid, $account);
 
   $result = db_query($query['query']);
-  $message = db_fetch_array($result);
-  $message['user'] = $account;
-  $returned = module_invoke_all('privatemsg_message_load', $message);
-  if (!empty($returned)) {
-    $message = array_merge_recursive($returned, $message);
+  if ($message = db_fetch_array($result)) {
+    $message['user'] = $account;
+    $returned = module_invoke_all('privatemsg_message_load', $message);
+    if (!empty($returned)) {
+      $message = array_merge_recursive($returned, $message);
+    }
+    return $message;
   }
-  return $message;
+  return FALSE;
 }
 
 function _privatemsg_assemble_query($query) {
@@ -1325,6 +1398,7 @@ function _privatemsg_assemble_query($que
   $INNER_JOIN = array();
   $WHERE = array();
   $GROUP_BY = array();
+  $HAVING   = array();
   $ORDER_BY = array();
   $QUERY_ARGS = array();
   $primary_table = '';
@@ -1334,6 +1408,7 @@ function _privatemsg_assemble_query($que
     'inner_join'  => $INNER_JOIN,
     'where'       => $WHERE,
     'group_by'    => $GROUP_BY,
+    'having'      => $HAVING,
     'order_by'    => $ORDER_BY,
     'query_args'  => $QUERY_ARGS,
     'primary_table'  => $primary_table,
@@ -1367,6 +1442,7 @@ function _privatemsg_assemble_query($que
   $INNER_JOIN = $fragments['inner_join'];
   $WHERE = $fragments['where'];
   $GROUP_BY = $fragments['group_by'];
+  $HAVING   = $fragments['having'];
   $ORDER_BY = $fragments['order_by'];
   $QUERY_ARGS = $fragments['query_args'];
   $primary_table = $fragments['primary_table'];
@@ -1380,33 +1456,28 @@ function _privatemsg_assemble_query($que
     $str_select = implode(", ", $SELECT);
     $query = "SELECT {$str_select} FROM ". $primary_table;
 
-    // Also build a count query which can be passed to pager_query to get a "page count" as that does not play well with queries including "GROUP BY".
-    // In most cases,  "COUNT(*)" is enough to get the count query, but in queries involving a GROUP BY, we want a count of the number of groups we have, not the count of elements inside each group.
-    // So we test if there is GROUP BY and if there is, count the number of distinct groups. If not, we go the normal wal and do a plain COUNT(*).
-    if (!empty($GROUP_BY)) {
-      // PostgreSQL does not support COUNT(sometextfield, someintfield), so I'm only using the first one
-      // Works fine for thread_id/list but may generate an error when a more complex GROUP BY is used.
-      $str_group_by_count = current($GROUP_BY);
-      $count = "SELECT COUNT(DISTINCT {$str_group_by_count}) FROM ". $primary_table;
-    }
-    else {
-      $count = "SELECT COUNT(*) FROM ". $primary_table;
-    }
-
     if (!empty($INNER_JOIN)) {
       $str_inner_join = implode(' ', $INNER_JOIN);
       $query .= " {$str_inner_join}";
-      $count .= " {$str_inner_join}";
     }
     if (!empty($WHERE)) {
       $str_where = '('. implode(') AND (', $WHERE) .')';
       $query .= " WHERE {$str_where}";
-      $count .= " WHERE {$str_where}";
     }
     if (!empty($GROUP_BY)) {
       $str_group_by = ' GROUP BY '. implode(", ", $GROUP_BY) ;
       $query .= " {$str_group_by}";
     }
+    if (!empty($HAVING)) {
+      $str_having = '('. implode(') AND (', $HAVING) .')';
+      $query .= " HAVING {$str_having}";
+    }
+
+    // Also build a count query which can be passed to pager_query to get a "page count" as that does not play well with queries including "GROUP BY" or "HAVING".
+    // This should work with both mysql and pgsql as outlined in http://drupal.org/node/303087#comment-1370752 .
+    // The COUNT query is created before adding the ORDER BY as that is not needed and will just slow things down.
+    $count = 'SELECT COUNT(*) FROM ('. $query .') as count';
+
     if (!empty($ORDER_BY)) {
       $str_order_by = ' ORDER BY '. implode(", ", $ORDER_BY) ;
       $query .= " {$str_order_by}";
Index: privatemsg.test
===================================================================
RCS file: /cvs/drupal/contributions/modules/privatemsg/privatemsg.test,v
retrieving revision 1.2.2.1
diff -u -p -r1.2.2.1 privatemsg.test
--- privatemsg.test	26 Jan 2009 19:53:27 -0000	1.2.2.1
+++ privatemsg.test	11 Apr 2009 22:33:25 -0000
@@ -110,6 +110,56 @@ class PrivatemsgTestCase extends DrupalW
     }
   }
 
+  function testPrivatemsgFlush()
+  {
+    $author = $this->drupalCreateUser(array('write privatemsg', 'read privatemsg'));
+    $recipient = $this->drupalCreateUser(array('write privatemsg', 'read privatemsg'));
+
+    // send 10 messages
+    for ($i = 0; $i < 10; $i++) {
+      privatemsg_new_thread('Message #'. $i, 'This is the body', array($recipient), $author);
+    }
+
+    // delete message 1, 3, 4, 6, 9 for author
+    foreach (array(1, 3, 4, 6, 9) as $pmid) {
+      privatemsg_message_change_delete($pmid, TRUE, $author);
+    }
+
+    // delete message 1, 2, 4, 6, 8 for recipient
+    foreach (array(1, 3, 4, 6, 9) as $pmid) {
+      privatemsg_message_change_delete($pmid, TRUE, $recipient);
+    }
+
+    // now, mid 1, 4 and 6 have been deleted by both
+
+    // flush configuration, enable, delay is default, 30 days
+    variable_set('privatemsg_flush_enabled', TRUE);
+
+    // Set back the deleted timestamp 35 days back of mid 4
+    db_query('UPDATE {pm_index} SET deleted = %d WHERE mid = 4',  time() - 35 * 86400);
+    // Set back the deleted timestamp of mid 6, but only 20 back
+    db_query('UPDATE {pm_index} SET deleted = %d WHERE mid = 6', time() - 20 * 86400);
+
+    // run flush
+    privatemsg_cron();
+
+    // check if the undeleted messages are still there:
+    foreach (array(2, 3, 5, 7, 8, 9, 10) as $pmid) {
+      $message = _privatemsg_load($pmid, $author);
+      $this->assertTrue(!empty($message), t('Undeleted message #%id is still in the system', array('%id' => $pmid)));
+    }
+
+    // check if the "recently" deleted  messages are still there:
+    foreach (array(1, 6) as $pmid) {
+      $message = _privatemsg_load($pmid, $author);
+      $this->assertTrue(!empty($message), t('Deleted message #%id is still in the system', array('%id' => $pmid)));
+    }
+
+    // mid 4 should have been deleted
+    $message = _privatemsg_load(4, $author);
+    $this->assertTrue(empty($message), t('Message #4 has been flushed'));
+  }
+
 
   /**
    * Implementation of tearDown().
Index: privatemsg.install
===================================================================
RCS file: /cvs/drupal/contributions/modules/privatemsg/privatemsg.install,v
retrieving revision 1.5.2.4.2.11.2.6
diff -u -p -r1.5.2.4.2.11.2.6 privatemsg.install
--- privatemsg.install	20 Feb 2009 08:30:27 -0000	1.5.2.4.2.11.2.6
+++ privatemsg.install	11 Apr 2009 22:33:25 -0000
@@ -387,4 +387,13 @@ function privatemsg_update_6000() {
   db_drop_table($ret, 'temp_pm_index');
 
   return $ret;
+}
+
+function privatemsg_update_6002() {
+  $ret = array();
+
+  $sql = "UPDATE {pm_index} SET deleted = %d WHERE deleted = 1";
+  $result = db_query($sql, time());
+  $ret[] = array('success' => $result !== FALSE, 'query' => check_plain($sql));
+  return $ret;
 }
\ No newline at end of file
