Source file: src/selectmail.cpp

/* Copyright (C) 2004-2010 Daniel Verite

   This file is part of Manitou-Mail (see http://www.manitou-mail.org)

   This program is free software; you can redistribute it and/or modify
   it under the terms of the GNU General Public License version 2 as
   published by the Free Software Foundation.

   This program is distributed in the hope that it will be useful,
   but WITHOUT ANY WARRANTY; without even the implied warranty of
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   GNU General Public License for more details.

   You should have received a copy of the GNU General Public License
   along with this program; if not, write to the Free Software
   Foundation, Inc., 59 Temple Place - Suite 330,
   Boston, MA 02111-1307, USA.
*/

#include "main.h"
#include "selectmail.h"
#include "msg_list_window.h"
#include "db.h"
#include "tags.h"
#include "msg_status_cache.h"
#include "sqlquery.h"
#include "sqlstream.h"
#include "addresses.h"
#include "helper.h"
#include "icons.h"
#include "sql_editor.h"
#include "words.h"

#include <QLineEdit>
#include <QComboBox>
#include <QLabel>
#include <QButtonGroup>
#include <QSpinBox>
#include <QMessageBox>
#include <QApplication>
#include <QCursor>
#include <QPushButton>
#include <QToolButton>
#include <QTimer>
#include <QFontMetrics>
#include <QRadioButton>
#include <QDateTimeEdit>
#include <QCheckBox>
#include <QGridLayout>
#include <QKeyEvent>
#include <QHBoxLayout>


const int
msgs_filter::max_possible_prio=32767;

msgs_filter::msgs_filter()
{
  init();
}

void
msgs_filter::init()
{
  m_order=1;
  m_mailId=0;
  m_tag_id=0;
  m_thread_id=0;
  m_status=-1;
  m_status_set=0;
  m_status_unset=0;
  m_newer_than=0;
  m_max_results=get_config().get_number("max_msgs_per_selection");
  m_addresses_count=0;
  m_fetched=false;
  m_fetch_results=NULL;
  m_sAddress=QString::null;
  m_subject=QString::null;
  m_body_substring=QString::null;
  m_addr_to=QString::null;
  m_sql_stmt=QString::null;
  m_tag_name=QString::null;
  m_date_min=QDate();
  m_date_max=QDate();
  //  m_word=QString::null;
  m_words.clear();
  m_in_trash=false;
  m_auto_refresh=false;
  m_min_prio = max_possible_prio+1;
  m_nAddrType = 0;

  m_user_query=QString::null;

  // results
  m_date_bound=QString::null;
  m_mail_id_bound=0;
  
}

msgs_filter::~msgs_filter()
{
  if (m_fetch_results)
    delete m_fetch_results;
}

int
msgs_filter::add_address_selection (sql_query& q,
				    const QString email_addr,
				    int addr_type)
{
  mail_address addr;
  bool address_found;
  if (!addr.fetchByEmail(email_addr, &address_found) || !address_found) {
    // no address matches m_sAddress or an error has occurred when
    // trying to fetch: the result has to be empty
    return 0;
  }
  QString alias=QString("ma%1").arg(++m_addresses_count);
  q.add_table(QString("mail_addresses %1").arg(alias));
  q.add_clause(QString(alias) + ".addr_id", addr.id());
  q.add_clause(QString("m.mail_id=%1.mail_id").arg(alias));
  if (addr_type != 0) {
    q.add_clause(QString("%1.addr_type").arg(alias), addr_type);
  }
  return 1;
}


//static
void
msgs_filter::load_result_list(PGresult* res, std::list<mail_result>* l)
{
  if (res && PQresultStatus(res)==PGRES_TUPLES_OK) {
    DBG_PRINTF(5,"load_result_list %d results", PQntuples(res));
    bool utf8=false;
    db_cnx db;
    if (db.datab()->encoding()=="UTF8")
      utf8=true;
    for (int i=0; i < PQntuples(res); i++) {
      mail_result r;
      r.m_id = atoi(PQgetvalue(res, i , 0));
      r.m_from = PQgetvalue(res, i, 1);
      if (!utf8)
	r.m_subject = PQgetvalue(res, i, 2);
      else
	r.m_subject = QString::fromUtf8(PQgetvalue(res, i, 2));
      r.m_date = PQgetvalue(res, i, 3);
      r.m_thread_id = atoi(PQgetvalue(res, i , 4));
      r.m_status = atoi(PQgetvalue(res, i, 5));
      msg_status_cache::update(r.m_id, r.m_status);
      r.m_in_replyto = atoi(PQgetvalue(res, i, 6));
      if (!utf8)
	r.m_sender_name = PQgetvalue(res, i, 7);
      else
	r.m_sender_name = QString::fromUtf8(PQgetvalue(res, i, 7));
      r.m_pri = atoi(PQgetvalue(res, i, 8));
      r.m_flags = (uint)atoi(PQgetvalue(res, i, 9));
      l->push_back(r);
    }
  }
}

fetch_thread::fetch_thread()
{
  m_cnx=NULL;
}

// start the fetch. Overrides QThread::run()
void
fetch_thread::run()
{
  if (!m_cnx) return;
  DBG_PRINTF(5,"fetch_thread::run()");
  m_errstr=QString::null;
  PGconn* c = m_cnx->connection();
  QByteArray qb;
  if (m_cnx->datab()->encoding()=="UTF8") {
    qb = m_query.toUtf8();
  }
  else {
    qb = m_query.toLocal8Bit();
  }
  QTime start = QTime::currentTime();
  PGresult* res = PQexec(c, qb.constData());
  m_exec_time = start.elapsed();
  if (!res) {
    m_errstr = QObject::tr("Unspecified postgreSQL error");
  }
  else {
    if (PQresultStatus(res)==PGRES_TUPLES_OK)
      msgs_filter::load_result_list(res, m_results);
    else {
      QString pg_status=QString(PQresStatus(PQresultStatus(res)));
      if (!pg_status.isEmpty())
	m_errstr = QObject::tr("status: ")+pg_status+"\n";
      m_errstr += QString(PQresultErrorMessage(res));
      DBG_PRINTF(5, "PQexec error");
    }
  }
  if (res)
    PQclear(res);
}

// stop the fetch
void
fetch_thread::cancel()
{
  if (m_cnx) {
    DBG_PRINTF(5, "fetch_thread::cancel()");
    PGconn* c = m_cnx->connection();
    PQrequestCancel(c);
  }
}

void fetch_thread::release()
{
  if (m_cnx) {
    delete m_cnx;
    m_cnx=NULL;
  }
}

int
msgs_filter::parse_search_string(QString s, QStringList& words,
				 QStringList& substrs)
{
  int state=10;
  QString curr_word;
  QString curr_substr;
  uint len=s.length();
  for (uint i=0; i<len; i++) {
    QChar c=s.at(i);
    DBG_PRINTF(5, "p i=%u, char=%c, state=%d", i, c.toLatin1(), state);
    if (c==QChar('"')) {
      if (state==10) state=40;
      else if (state==40) {
	if (!curr_substr.isEmpty())
	  substrs.append(curr_substr);
	state=10;
      }
      else if (state==50 || state==60) { // A5
	curr_word.append(c);
	curr_substr.append(c);	
      }
      if (state==60) state=40;
    }
    else if (state==40 && c==QChar('\\')) {
      // next character is quoted
      state=50;
    }
    else if (!(c.isLetterOrNumber() || c=='_')) {
      // delimiter
      if (state==10 || state==40 || state==50) {
	if (!curr_word.isEmpty()) {
	  words.append(curr_word);
	  curr_word.truncate(0);
	}
      }
      if (state==40 || state==50) {
	curr_substr.append(c);
      }
    }
    else {
      // non-delimiter (word constituant)
      curr_word.append(c);	// A4
      if (state==40 || state==50 || state==60)
	curr_substr.append(c); // A6
      if (state==60) state=40;
    }
  }
  if (state!=10) {
    DBG_PRINTF(3, "parse error: state=%d", state);
  }
  if (!curr_word.isEmpty()) {
    words.append(curr_word);
  }
  return 0;
}

QString
msgs_filter::quote_like_arg(const QString& s)
{
  QString r=s;
  r.replace('\\', "\\\\");
  r.replace('_', "\\_");
  r.replace('\'', "\\'");
  r.replace('%', "\\%");
  r.append('%');
  r.prepend('%');
  return r;
}

/*
  Return values:
  0: database error
  1: OK
  2: the query would return no result (currently, it happens when a condition
   is set on an email address that doesn't exist in the ADDRESSES table,
   or a tag that's not in the TAGS table)

  'fetch_more' can be set on subsequent invocations to fetch more results
  (think "next page") based on the lower/higher (msg_date,mail_id) previously
  retrieved
*/
int
msgs_filter::build_query(sql_query& q, bool fetch_more/*=false*/)
{
  db_cnx db;
  try {
    bool unique=false;
    bool done_with_status=false;	// true when tests on status have been short-circuited
    QString sWhere;
    QString main_table;
    int status_set = m_status_set;
    int status_unset = m_status_unset;
    if (m_in_trash) {
      status_set |= mail_msg::statusTrashed;
    }
    else {
      status_unset |= mail_msg::statusTrashed;
    }

    main_table="mail m";
    if (m_tag_name == "(No tag set)") {
      /* optimize the cases of "no tag set" condition AND'ed with
	 some particular values for the status */
      if (m_status == -1 && !m_in_trash && m_status_set==0 && m_status_unset == mail_msg::statusArchived) {
	// current and not tagged
	q.add_clause(QString("m.mail_id in (SELECT ms.mail_id FROM mail_status ms"
			     " LEFT OUTER JOIN mail_tags mt ON mt.mail_id=ms.mail_id"
			     " WHERE  mt.mail_id is null AND ms.status&%1=0)")
		     .arg(mail_msg::statusArchived|mail_msg::statusTrashed));
	done_with_status=true;
      }
      else if (m_status==0) {
	// new and not tagged
	q.add_clause(QString("m.mail_id in (SELECT ms.mail_id FROM mail_status ms"
			     " LEFT OUTER JOIN mail_tags mt ON mt.mail_id=ms.mail_id"
			     " WHERE  mt.mail_id is null AND ms.status=0)"));
	done_with_status=true;
      }
      else {
	main_table += " LEFT OUTER JOIN mail_tags mt ON mt.mail_id=m.mail_id";
	q.add_clause(" mt.mail_id is null");
      }
    }
    q.add_table(main_table);

    // bounds. m_mail_id_bound and m_date_bound should be either both set or both unset
    if (fetch_more && m_mail_id_bound>0) {
      if (m_order>0) {
	q.add_clause(QString("m.mail_id>%1").arg(m_mail_id_bound));
      }
      else
	q.add_clause(QString("m.mail_id<%1").arg(m_mail_id_bound));
    }
    if (fetch_more && !m_date_bound.isEmpty()) {
      if (m_order>0) {
	// msgs fetched from oldest to newest
	q.add_clause(QString("msg_date>=to_date('%1','YYYYMMDDHH24MISS')").arg(m_date_bound));
      }
      else {
	q.add_clause(QString("msg_date<=to_date('%1','YYYYMMDDHH24MISS')").arg(m_date_bound));  
      }
    }

    if (!m_sql_stmt.isEmpty()) {
      q.add_clause(QString("m.mail_id in (") + m_sql_stmt + QString(")"));
    }

    if (m_min_prio <= max_possible_prio) {
      q.add_clause(QString("m.priority>=%1").arg(m_min_prio));
    }
    if (!m_sAddress.isEmpty()) {
      int addr_type;
      switch(m_nAddrType) {
	// is it really necessary to translate here? TODO: find a way
	// to get the caller to pass an addr_type from the mail_address enum
      case rFrom:
	addr_type = mail_address::addrFrom;
	break;
      case rTo:
	addr_type = mail_address::addrTo;
	break;
      case rCc:
	addr_type = mail_address::addrCc;
	break;
      default:
	addr_type = 0; // any
	break;
      }
      if (!add_address_selection(q, m_sAddress, addr_type))
	return 2;
    }
    if (!m_addr_to.isEmpty()) {
      if (!add_address_selection(q, m_addr_to, mail_address::addrTo))
	return 2;
    }
    //    if (!m_tag_name.isEmpty() && m_tag_name!="(No tag set)") {
    if (m_tag_id!=0) {
      q.add_table("mail_tags mq");
      q.add_clause(QString("mq.mail_id=m.mail_id and mq.tag=%1").arg(m_tag_id));
    }
    if (!m_subject.isEmpty()) {
      q.add_clause("subject", quote_like_arg(m_subject), " ILIKE ");
    }
    if (m_thread_id) {
      q.add_clause("thread_id", (int)m_thread_id);
    }
    if (!m_date_min.isNull() && m_date_min.isValid()) {
      QString date_clause;
      if (!m_date_max.isNull() && m_date_max.isValid()) {
	date_clause = QString("msg_date >= '%1'::date and msg_date<'%2'::date+1::int").arg(m_date_min.toString("yyyy/M/d")).arg(m_date_max.toString("yyyy/M/d"));
      }
      else {
	date_clause = QString("msg_date>='%1'::date").arg(m_date_min.toString("yyyy/M/d"));
      }
      q.add_clause(date_clause);
    }
    else if (!m_date_max.isNull() && m_date_max.isValid()) {
      q.add_clause(QString("msg_date<'%1'::date+1::int").arg(m_date_max.toString("yyyy/M/d")));
    }
    if (!m_body_substring.isEmpty()) {
      q.add_table("body b");
      q.add_clause(QString("strpos(b.bodytext,'") + m_body_substring + QString("')>0 and m.mail_id=b.mail_id"));
    }

    if (!m_words.empty()) {
      QStringList::Iterator it = m_words.begin();
      int i=0;
      wordsearch_resultset ws_rs;
      for (; it!=m_words.end(); ++it) {
	if (!db_word::is_non_indexable(*it)) {
	  db_word word;
	  word.set_text(*it);
	  word.fetch_vectors();
	  if (i++==0)
	    ws_rs.insert_word(word);
	  else
	    ws_rs.and_word(word);
	}
      }
      std::list<uint> mlist;
      QString in_list="m.mail_id in (";

      // get the list of mail_id corresponding to the words vectors
      // FIXME: using m_max_results is OK only if there are no other criteria
      // besides the wordsearch. Otherwise we can't know how much mail_id we should
      // extract from the word vector.
      // A possible way out would be to choose a reasonable fixed size and
      // issue several times the main SELECT until the maximum number results is reached
      // or there is no new result to expect, and then UNION all the results together
      if (fetch_more && m_mail_id_bound>0) {
	ws_rs.get_result_bits(mlist, m_mail_id_bound, (m_order>0 ? 1 : -1), m_max_results);
      }
      else {
	ws_rs.get_result_bits(mlist, 0, 0, m_max_results);
      }

      if (!mlist.empty()) {
	char b[11];
	std::list<uint>::iterator iit=mlist.begin();
	for (int cnt=0; iit!=mlist.end(); ++iit, ++cnt) {
	  if (cnt!=0)
	    in_list.append(',');
	  sprintf(b, "%u", *iit);
	  in_list.append(b);
	}
      }
      else {
	in_list.append('0');	// force no result
      }
      in_list.append(')');
//      DBG_PRINTF(4, "%s", in_list.latin1());
      q.add_clause(in_list);
      //unique=true;		// the DISTINCT clause will be needed
    }

    if (!m_substrs.empty()) {
      QStringList::Iterator it = m_substrs.begin();
      if (it!=m_substrs.end())
	q.add_table("body b");
      for (; it!=m_substrs.end(); ++it) {
	q.add_clause("bodytext ilike '" + quote_like_arg(*it) + "' AND m.mail_id=b.mail_id");
      }
    }

    if (m_mailId) {
      //sWhere.sprintf(" WHERE mail_id=%d", m_mailId);
      q.add_clause("mail_id", (int)m_mailId);
    }
    if (m_newer_than) {
      QString s;
      s.sprintf("(msg_date>=date_trunc('days',now()-interval '%d days'))", m_newer_than);
      //      s.sprintf("msg_day>=%d", days_to_now-(m_newer_than-1));
      q.add_clause(s);
    }
    // Selection on status bitmasks
    if ((status_set || status_unset) && !done_with_status && m_status==-1) {
      QString s;
      if (status_set) {
	if (!status_unset) {
	  s.sprintf("status&%d=%d", status_set, status_set);
	}
	else
	  s.sprintf("(status&%d=%d AND status&%d=0)", status_set, status_set, status_unset);
      }
      else {
	if (status_unset == mail_msg::statusArchived ||
	    status_unset == mail_msg::statusArchived+mail_msg::statusTrashed) {
	  // unprocessed messages: optimize by joining with mail_status
	  q.add_table("mail_status ms");
	  s.sprintf("ms.mail_id=m.mail_id AND ms.status&%d=0", status_unset);
	}
	else {
	  s.sprintf("status&%d=0", status_unset);
	}
      }
      q.add_clause(s);
    }
    if (m_status!=-1 && !done_with_status) {
      if (m_status==0) {
	// new messages: optimize by joining with mail_status
	q.add_table("mail_status ms");
	q.add_clause("ms.mail_id=m.mail_id AND ms.status=0");
      }
      else {
	QString s;
	s.sprintf("status=%d", m_status);
	q.add_clause(s);
      }
    }

    QString sFinal="ORDER BY msg_date";
    if (m_order<0)
      sFinal+=" DESC";
    sFinal+=",mail_id";
    if (m_order<0)
      sFinal+=" DESC";
    if (m_max_results>0) {
      QString s;
      s.sprintf(" LIMIT %u", m_max_results);
      sFinal+=s;
    }
    q.add_final(sFinal);

    QString select;
    if (unique) {
      // msg_date is added to the end of the select list because postgresql
      // does require it when DISTINCT is used
      select = "SELECT DISTINCT m.mail_id,sender,subject,to_char(msg_date,'YYYYMMDDHH24MISS'),thread_id,m.status,in_reply_to,sender_fullname,priority,flags,msg_date";
    }
    else {
      select = "SELECT m.mail_id,sender,subject,to_char(msg_date,'YYYYMMDDHH24MISS'),thread_id,m.status,in_reply_to,sender_fullname,priority,flags";
    }
    q.start(select);
    if (m_sql_stmt.isEmpty())
      m_user_query = q.subquery("m.mail_id");
    else
      m_user_query=m_sql_stmt;
  }
  catch(db_excpt& p) {
    DBEXCPT(p);
    return 0;
  }
  return 1;
}

/*
  Return values: same as build_query()
*/
int
msgs_filter::asynchronous_fetch(fetch_thread* t)
{
  m_fetched = true;
  sql_query q;
  int r = build_query(q);
  if (r==1) {
    // start the query only if it might return results
    if (m_fetch_results)
      delete m_fetch_results;
    m_fetch_results = new std::list<mail_result>;
    t->m_results = m_fetch_results;
    t->m_cnx = new db_cnx(true);
    if (!t->m_cnx->ping()) {
      DBG_PRINTF(3, "Connection to database lost. Trying to reconnect");
      if (!t->m_cnx->datab()->reconnect()) {
	DBG_PRINTF(3, "Failed to reconnect to the database");
	m_errmsg = QObject::tr("No connection to the database.");
	return 0;
      }
    }
    t->m_query = q.get();
    m_start_time = QTime::currentTime();
    t->start();
  }
  return r;
}

/*
  Fetch the selection into a mail list widget
  Return values:
   0. fetch error. m_errmsg may contain an error message
   1. OK
   2. A condition doesn't match even before running the main query
     (example: non-existing email address)

  'fetch_more' can be set on subsequent invocations to fetch more results
  (think "next page") based on the lower/higher (msg_date,mail_id) previously
  retrieved
*/
int
msgs_filter::fetch(mail_listview* qlv, bool fetch_more/*=false*/)
{
  m_errmsg=QString::null;
  m_fetched = true;
  int r=1;
  try {
    sql_query q;
    r=build_query(q, fetch_more);
    if (r==1) {
      db_cnx db;
      QString s=q.get();
      const char* query;
      QByteArray qb;
      if (db.datab()->encoding()=="UTF8") {
	qb = s.toUtf8();
      }
      else {
	qb = s.toLocal8Bit();
      }
      query = qb.constData();
      DBG_PRINTF(5,"%s", query);
      m_exec_time=0;
      m_start_time = QTime::currentTime();
#ifdef WITH_PGSQL
      PGconn* c=GETDB();
      PGresult* res = PQexec(c, query); // TODO: check for pgsql errors here
      if (res && PQresultStatus(res)==PGRES_TUPLES_OK) {
	m_exec_time = m_start_time.elapsed();
	m_fetch_results = new std::list<mail_result>;
	if (m_fetch_results) {
	  load_result_list(res, m_fetch_results);
	  make_list(qlv);
	  delete m_fetch_results;
	  m_fetch_results=NULL;
	}
      }
      else {
	DBG_PRINTF(2, "PQexec error");
	m_exec_time=-1;
	m_errmsg = PQerrorMessage(c);
	QMessageBox::warning(NULL, APP_NAME, QObject::tr("Unable to execute query.") + QString("\n")+ m_errmsg);
      }
      if (res)
	PQclear(res);
#endif
    }
    else if (r==0) {
      QMessageBox::information(NULL, APP_NAME, QObject::tr("Fetch error"));
    }
    else if (r==2) {
      QMessageBox::information(NULL, APP_NAME, QObject::tr("No results"));
    }
  }
  catch (int e) {
    r=e;
  }
  return r;
}

int
msgs_filter::exec_time() const
{
  return m_exec_time;
}

void
msgs_filter::make_list(mail_listview* qlv)
{
  if (!m_fetch_results)
    return;       // No result

  bool refetch=false;
  if (m_list_msgs.size()>0) {
    /* the list already contains some messages. That means
       that we'll test the messages coming from the db against
       those already in the list to avoid duplicates */
    refetch=true;
  }


  std::list<mail_result>::const_iterator iter = m_fetch_results->begin();

  for (; iter != m_fetch_results->end(); ++iter) {
    if (refetch) {
      if (qlv->find(iter->m_id)!=NULL)
	continue;		// avoid duplicates
    }

    mail_msg* msg = new mail_msg(iter->m_id, iter->m_from, iter->m_subject,
				 date(iter->m_date));
    msg->setThread(iter->m_thread_id);
    msg->set_orig_status(iter->m_status);
    msg->setStatus(iter->m_status);
    msg->setInReplyTo((mail_id_t)iter->m_in_replyto);
    msg->set_sender_name(iter->m_sender_name);
    msg->set_flags(iter->m_flags);
    msg->set_priority(iter->m_pri);

    m_list_msgs.push_back(msg);
    // adjust bounds: FIXME: do that outside of the loop
    m_date_bound = iter->m_date;
    m_mail_id_bound = iter->m_id;
  }

  qlv->insert_list(m_list_msgs);
}

/*
  Dialog used to fill in the criteria for a database fetch

  if 'open_new' is true, open a new msg_list_window with the results.
  if false, the caller has to provide a slot for the 'fetch_done' signal
  that will be emitted when the results are available.
*/
msg_select_dialog::msg_select_dialog(bool open_new/*=true*/) : QDialog(0)
{
  setWindowTitle(tr("Query selection"));
  setWindowIcon(UI_ICON(FT_ICON16_NEW_QUERY));
  m_waiting_for_results = false;
  m_new_selection=open_new;

  QVBoxLayout* topLayout = new QVBoxLayout(this);
  //  QVBox* box=new QVBox(this,"vbox");
  QLabel* lb=new QLabel(tr("Fill in one or more selection criteria:"), this);
  topLayout->addWidget(lb);

  QGridLayout* gridLayout = new QGridLayout();
  topLayout->addLayout(gridLayout);

  int nRow=0;
  m_wAddrType=new QComboBox(this);
  // each index in the combobox must match an entry in the enum
  // msgs_filter::recipient_type
  m_wAddrType->addItem("From");
  m_wAddrType->addItem("To");
  m_wAddrType->addItem("Cc");
  m_wAddrType->addItem("Any");
  connect(m_wAddrType, SIGNAL(activated(int)), SLOT(addr_type_changed(int)));
  gridLayout->addWidget(m_wAddrType,nRow,0);
  m_wAddress=new combobox_addresses(this);
  m_wAddress->set_address_type(mail_address::addrFrom);
  gridLayout->addWidget(m_wAddress, nRow, 1);

  nRow++;
  QLabel* lDate2 = new QLabel(tr("Dates between:"), this);
  gridLayout->addWidget(lDate2, nRow, 0);
  QWidget* hbdate = new QWidget;
  m_chk_datemin = new QCheckBox(tr("min"));
  m_wmin_date = new QDateTimeEdit;
  m_chk_datemax = new QCheckBox(tr("max"));
  m_wmax_date = new QDateTimeEdit;
  QHBoxLayout* hldate = new QHBoxLayout;
  hldate->setSpacing(2);
  hldate->addWidget(m_chk_datemin);
  hldate->addWidget(m_wmin_date);
  hldate->addWidget(m_chk_datemax);
  hldate->addWidget(m_wmax_date);
  hbdate->setLayout(hldate);

  gridLayout->addWidget(hbdate, nRow, 1);

  QString df = get_config().get_string("date_format");
  QString dorder="yyyy.MM.dd";
  
  if (df.startsWith("DD/MM/YYYY")) {
    dorder="dd.MM.yyyy";		// european-style date
  }
  m_wmax_date->setDisplayFormat(dorder);
  m_wmin_date->setDisplayFormat(dorder);

  {
    nRow++;
    QLabel* lDate=new QLabel(tr("Not older than:"), this);
    gridLayout->addWidget(lDate,nRow,0);
    QHBoxLayout* hbd = new QHBoxLayout();
    m_wdate_spin = new QSpinBox(this);
    m_wdate_spin->setMinimum(0);
    m_wdate_spin->setSpecialValueText(tr("(Ignore)"));
    hbd->addWidget(m_wdate_spin);

    m_wdate_button_group = new QButtonGroup(this);
    QHBoxLayout* interv_unit_layout = new QHBoxLayout;
    QRadioButton* r_day = new QRadioButton(tr("days"));
    QRadioButton* r_week = new QRadioButton(tr("weeks"));
    QRadioButton* r_month = new QRadioButton(tr("months"));
    m_wdate_button_group->addButton(r_day, 0);
    m_wdate_button_group->addButton(r_week, 1);
    m_wdate_button_group->addButton(r_month, 2);
    interv_unit_layout->addWidget(r_day);
    interv_unit_layout->addWidget(r_week);
    interv_unit_layout->addWidget(r_month);
  
    hbd->addLayout(interv_unit_layout);
    hbd->setStretchFactor(interv_unit_layout, 0);
    hbd->addStretch(1);
    r_day->setChecked(true);
    gridLayout->addLayout(hbd, nRow, 1);
  }

  nRow++;
  QLabel* lTo = new QLabel(tr("To:"), this);
  gridLayout->addWidget(lTo,nRow,0);
  m_wTo = new combobox_addresses(this);
  gridLayout->addWidget(m_wTo, nRow, 1);

  nRow++;
  QLabel* lSubject=new QLabel(tr("Subject:"), this);
  gridLayout->addWidget(lSubject,nRow,0);
  m_wSubject=new QLineEdit(this);
  gridLayout->addWidget(m_wSubject, nRow, 1);

  nRow++;
  QLabel* lString=new QLabel(tr("Contains:"), this);
  gridLayout->addWidget(lString,nRow,0);
  m_wString=new QLineEdit(this);
  gridLayout->addWidget(m_wString, nRow, 1);

  nRow++;
  QLabel* lSql=new QLabel(tr("SQL statement:"), this);
  gridLayout->addWidget(lSql,nRow,0);
  QHBoxLayout* hbz = new QHBoxLayout();
  QIcon zoom_icon(FT_MAKE_ICON(FT_ICON16_ZOOM_PAGE));
  m_wSqlStmt=new QLineEdit();
  hbz->addWidget(m_wSqlStmt);
  m_zoom_button = new QToolButton();
  hbz->addWidget(m_zoom_button);
  m_zoom_button->setIcon(zoom_icon);
  m_zoom_button->setToolTip(tr("Zoom"));
  gridLayout->addLayout(hbz, nRow, 1);
  connect(m_zoom_button, SIGNAL(clicked()), this, SLOT(zoom_on_sql()));

  // Selection by tag
  nRow++;
  QLabel* lTag = new QLabel(tr("Contains tag:"), this);
  gridLayout->addWidget(lTag,nRow,0);

  m_qtag_sel = new tag_selector(this);
  m_qtag_sel->init(true);
  gridLayout->addWidget(m_qtag_sel, nRow, 1);

  nRow++;
  gridLayout->addWidget(new QLabel(tr("Status:"), this), nRow,0);
  {
    QHBoxLayout* hb=new QHBoxLayout();
    hb->setSpacing(5);
    m_wStatus=new QLineEdit("status");
    hb->addWidget(m_wStatus);
    m_wStatus->setReadOnly(true);
    m_wStatus->setText(tr("Any"));
    m_wStatusMoreButton = new QPushButton(tr("Edit..."));
    hb->addWidget(m_wStatusMoreButton);
    gridLayout->addLayout(hb, nRow, 1);
    connect(m_wStatusMoreButton, SIGNAL(clicked()), this, SLOT(more_status()));
    // the default choice for the status is "either" for both set
    // and unset bits (=no filtering against status)
    m_status_set_mask = 0;
    m_status_unset_mask = 0; //mail_msg::statusArchived;
  }

  nRow++;
  gridLayout->addWidget(new QLabel(tr("Limit to:"),this), nRow, 0);
  // Limit to: [max results] messages (3 widgets)
  QHBoxLayout* hbox_lt=new QHBoxLayout();
  hbox_lt->setSpacing(10);
  m_wMaxResults=new QLineEdit();
  hbox_lt->addWidget(m_wMaxResults);
  QFontMetrics fm(m_wMaxResults->font());
  m_wMaxResults->setMaximumWidth(fm.width("000000")+15);

  m_wMaxResults->setText(get_config().get_string("max_messages_retrieved"));
  hbox_lt->addWidget(new QLabel(tr("messages")));
  hbox_lt->addStretch(10);
  gridLayout->addLayout(hbox_lt, nRow, 1);

  nRow++;
  m_trash_checkbox = new QCheckBox(tr("In trash"),this);
  gridLayout->addWidget(m_trash_checkbox, nRow, 1);

  nRow++;
  QHBoxLayout* hbox=new QHBoxLayout;
  hbox->setMargin(10);
  hbox->setSpacing(20);

  m_wOkButton = new QPushButton(tr("OK"));
  hbox->addWidget(m_wOkButton);
  m_wOkButton->setDefault(true);

  m_wCancelButton = new QPushButton(tr("Cancel"));
  hbox->addWidget(m_wCancelButton);
  QPushButton* help_btn = new QPushButton(tr("Help"));
  hbox->addWidget(help_btn);
  connect(help_btn, SIGNAL(clicked()), this, SLOT(help()));

  gridLayout->addLayout(hbox, nRow, 0, 1, -1);

  connect(m_wOkButton, SIGNAL(clicked()), this, SLOT(ok()));
  connect(m_wCancelButton, SIGNAL(clicked()), this, SLOT(cancel()));
}

QString
msg_select_dialog::str_status_mask()
{
  struct st_status {
    const char* name;
    int value;
  };
  static const st_status status_tab[] = {
    {QT_TR_NOOP("Rd"), mail_msg::statusRead},
    {QT_TR_NOOP("Rp"), mail_msg::statusReplied},
    {QT_TR_NOOP("F"), mail_msg::statusFwded},
    {QT_TR_NOOP("T"), mail_msg::statusTrashed},
    {QT_TR_NOOP("A"), mail_msg::statusArchived},
    {QT_TR_NOOP("O"), mail_msg::statusOutgoing},
    {QT_TR_NOOP("C"), mail_msg::statusComposed},
    {QT_TR_NOOP("S"), mail_msg::statusSent}
  };

  QString s;
  const QChar sep='+';
  for (uint i=0; i<sizeof(status_tab)/sizeof(status_tab[0]); i++) {
    if (m_status_set_mask & status_tab[i].value) {
      if (!s.isEmpty())
	s.append(sep);
      s.append(tr(status_tab[i].name));
    }
    if (m_status_unset_mask & status_tab[i].value) {
      if (!s.isEmpty())
	s.append(sep);
      s.append("!" + tr(status_tab[i].name));
    }
  }
  if (s.isEmpty())
    s="Any";
  return s;
}

status_dialog::status_dialog(QWidget* parent): QDialog(parent)
{
  QVBoxLayout* topLayout = new QVBoxLayout(this);
  m_statusBox = new select_status_box(true, this);
  topLayout->addWidget(m_statusBox);

  QHBoxLayout* hbox=new QHBoxLayout;
  hbox->setMargin(10);
  hbox->setSpacing(20);
  QPushButton* wOk=new QPushButton(tr("OK"));
  QPushButton* wCancel=new QPushButton(tr("Cancel"));
  hbox->addWidget(wOk);
  hbox->addWidget(wCancel);
  connect(wOk,SIGNAL(clicked()), this, SLOT(accept()));
  connect(wCancel,SIGNAL(clicked()), this, SLOT(reject()));
  topLayout->addLayout(hbox);
}

// Pops up the full status selection panel
void
msg_select_dialog::more_status()
{
  status_dialog* statusDlg=new status_dialog(this);

  statusDlg->m_statusBox->set_mask(m_status_set_mask, m_status_unset_mask);
  int r=statusDlg->exec();
  if (r==QDialog::Accepted) {
    m_status_set_mask=statusDlg->m_statusBox->mask_yes();
    m_status_unset_mask=statusDlg->m_statusBox->mask_no();
    m_wStatus->setText(str_status_mask());
  }
  delete statusDlg;
}

// Pops up a dialog for full text SQL editing
void
msg_select_dialog::zoom_on_sql()
{
  sql_editor* w=new sql_editor(this);
  QString initial_txt = m_wSqlStmt->text();
  w->set_text(initial_txt);
  int ret=w->exec();
  if (ret && w->get_text() != initial_txt) {
    m_wSqlStmt->setText(w->get_text());
  }
  w->close();
}

void
msg_select_dialog::to_filter(msgs_filter* filter)
{
  filter->m_body_substring = m_wString->text();
  filter->m_subject = m_wSubject->text();
  filter->m_sql_stmt = m_wSqlStmt->text();
  filter->m_sAddress = m_wAddress->currentText();
  filter->m_nAddrType = m_wAddrType->currentIndex();
  filter->m_tag_id = m_qtag_sel->current_tag_id();

  filter->m_addr_to = m_wTo->currentText();
  filter->m_in_trash = m_trash_checkbox->isChecked();
  if (m_chk_datemax->isChecked())
    filter->m_date_max = m_wmax_date->date();
  else
    filter->m_date_max = QDate();
  if (m_chk_datemin->isChecked())
    filter->m_date_min = m_wmin_date->date();
  else
    filter->m_date_min = QDate();
  bool ok;
  int idate = m_wdate_spin->text().toInt(&ok);
  if (ok) {
    QAbstractButton* b = m_wdate_button_group->checkedButton();
    if (b && idate>0) {
      int d=0;
      filter->m_newer_details.days=0;
      filter->m_newer_details.weeks=0;
      filter->m_newer_details.months=0;
      switch(m_wdate_button_group->id(b)) {
      case 0:
	d=idate;
	filter->m_newer_details.days=idate;
	break;
      case 1:
	d=idate*7;
	filter->m_newer_details.weeks=idate;
	break;
      case 2:
	d=idate*30;
	filter->m_newer_details.months=idate;
	break;
      }
      filter->m_newer_than=d;
    }
  }
  if (m_wMaxResults->text().isEmpty())
    filter->m_max_results=0;	// no limit
  else {
    filter->m_max_results=m_wMaxResults->text().toUInt(&ok);
    if (!ok) {
      QMessageBox::information(this, "Error", tr("Error: non-numeric value for the maximum number of messages"));
      return;
    }
  }
  filter->m_status_set=m_status_set_mask;
  filter->m_status_unset=m_status_unset_mask;
}

void
msg_select_dialog::filter_to_dialog(const msgs_filter* filter)
{
  m_wmin_date->setDate(filter->m_date_min);
  if (!filter->m_date_min.isNull())
    m_chk_datemin->setChecked(true);
  m_wmax_date->setDate(filter->m_date_max);
  if (!filter->m_date_max.isNull())
    m_chk_datemax->setChecked(true);
  m_wString->setText(filter->m_body_substring);
  m_wSubject->setText(filter->m_subject);
  m_wSqlStmt->setText(filter->m_sql_stmt);
  m_wAddress->addItem(filter->m_sAddress);
  m_wAddrType->setCurrentIndex(filter->m_nAddrType);
  m_trash_checkbox->setChecked(filter->m_in_trash);

  // tags
  if (filter->m_tag_id!=0) {
    m_qtag_sel->set_current_tag_id(filter->m_tag_id);
  }

  m_wTo->addItem(filter->m_addr_to);
  if (filter->m_max_results > 0) {
    m_wMaxResults->setText(QString("%1").arg(filter->m_max_results));
  }
  else
    m_wMaxResults->setText(QString::null);

  if (filter->m_newer_than>0) {
    int id=0,d=0;
    if (filter->m_newer_details.days) {
      id=0; d=filter->m_newer_details.days;
    }
    else if (filter->m_newer_details.weeks) {
      id=1; d=filter->m_newer_details.weeks;
    }
    else if (filter->m_newer_details.months) {
      id=2; d=filter->m_newer_details.months;
    }
    QAbstractButton* b = m_wdate_button_group->button(id);
    if (b) {
      QRadioButton* rb = dynamic_cast<QRadioButton*>(b);
      rb->setChecked(true);
      m_wdate_spin->setValue(d);
    }
  }
  m_status_set_mask = filter->m_status_set;
  m_status_unset_mask = filter->m_status_unset;
  m_wStatus->setText(str_status_mask());
}

void
msg_select_dialog::timer_done()
{
  if (m_waiting_for_results && m_thread.isFinished()) {
    m_waiting_for_results=false;
    DBG_PRINTF(5,"done");
    QApplication::restoreOverrideCursor();

    if (m_filter.m_fetch_results && m_filter.m_fetch_results->size() > 0) {
      if (m_new_selection) {
	msg_list_window* w = new msg_list_window(&m_filter, 0);
	w->show();
      }
      else {
	emit fetch_done(&m_filter);
      }
      m_thread.release();
      close();
    }
    else {
      m_waiting_for_results = false;
      m_timer->stop();
      delete m_timer;
      if (!m_thread.m_errstr.isEmpty()) {
	QMessageBox::information(this, APP_NAME, m_thread.m_errstr);
      }
      else
	QMessageBox::information(this, APP_NAME, tr("No results"));
      m_wCancelButton->setText(tr("Cancel"));
      enable_inputs(true);
      m_thread.release();
    }
  }
}

void
msg_select_dialog::enable_inputs(bool enable)
{
  QWidget* widgets[] = {
    m_wAddress, m_wAddrType, m_qtag_sel, m_wTo,
    m_wSubject, m_wString, m_wSqlStmt, m_wStatus, m_wMaxResults,
    m_wOkButton, m_wStatusMoreButton, m_wmin_date, m_wmax_date,
    m_wdate_spin, m_chk_datemax, m_chk_datemin,
    m_zoom_button
  };
  for (uint i=0; i<sizeof(widgets)/sizeof(widgets[0]); i++)
    widgets[i]->setEnabled(enable);
}

void
msg_select_dialog::ok()
{
  //  msgs_filter cFilter;
  to_filter(&m_filter);
  int r = m_filter.asynchronous_fetch(&m_thread);
  // at this point, the query is currently being run in m_thread,
  // and we'll check for its completion in timer_done()
  if (r==1) {
    m_timer = new QTimer(this);
    connect(m_timer, SIGNAL(timeout()), this, SLOT(timer_done()));
    m_waiting_for_results = true;
    m_timer->start(100);		// check results every 1/10s

    //    QIconSet ico_stop(FT_MAKE_ICON(FT_ICON16_STOP));
    m_wCancelButton->setText(tr("Abort"));
    //    m_wCancelButton->setIconSet(ico_stop);
    enable_inputs(false);
    const QCursor cursor(Qt::WaitCursor);
    QApplication::setOverrideCursor(cursor);
  }
  else if (r==0) {
    QMessageBox::information(this, APP_NAME, tr("Fetch error"));
  }
  else if (r==2) {
    QMessageBox::information(this, APP_NAME, tr("No results"));
  }
}

void
msg_select_dialog::cancel()
{
  if (m_waiting_for_results) {
    // stop the query but don't close the dialog
    m_waiting_for_results = false;
    m_thread.cancel();
    m_thread.release();
    m_wCancelButton->setText(tr("Cancel"));
//    m_wCancelButton->setIconSet(QIconSet()); // empty pixmap
    QApplication::restoreOverrideCursor();
    enable_inputs(true);
  }
  else {
    // close the dialog
    close();
  }
}

void
msg_select_dialog::help()
{
  helper::show_help("query selection");
}

void
msg_select_dialog::addr_type_changed(int index)
{
  DBG_PRINTF(5,"addr_type_changed index=%d, curitem=%d",
	     index, m_wAddrType->currentIndex());
  int type;
  switch (index) {
  case 0:
    type=mail_address::addrFrom;
    break;
  case 1:
    type=mail_address::addrTo;
    break;
  case 2:
    type=mail_address::addrCc;
    break;
  default:
    type=0;
    break;
  }
  m_wAddress->set_address_type(type);
}

//static
select_status_box::st_status select_status_box::m_status_tab[] = {
  {QT_TR_NOOP("Read"), mail_msg::statusRead},
  {QT_TR_NOOP("Replied"), mail_msg::statusReplied},
  {QT_TR_NOOP("Forwarded"), mail_msg::statusFwded},
  {QT_TR_NOOP("Trashed"), mail_msg::statusTrashed},
  {QT_TR_NOOP("Archived"), mail_msg::statusArchived},
  {QT_TR_NOOP("Outgoing"), mail_msg::statusOutgoing},
  {QT_TR_NOOP("Sent"), mail_msg::statusSent},
  {QT_TR_NOOP("Composed"), mail_msg::statusComposed}
};

select_status_box::select_status_box(bool either, QWidget* parent):
  QFrame (parent)
{
  //  setTitle("Status");
//    setFrameStyle( QFrame::Box | QFrame::Plain );
//    setLineWidth (2);
//    setMargin(10);

  m_either = either;
  QGridLayout* grid = new QGridLayout(this);
  grid->setMargin(4);
  grid->setSpacing(4);
  int button_id=0;
  for (uint i=0; i<sizeof(m_status_tab)/sizeof(m_status_tab[0]); i++) {
    grid->addWidget(new QLabel(tr(m_status_tab[i].name)), i, 0);
    QCheckBox* b1=new QCheckBox(tr("Yes"));
    QCheckBox* b2=new QCheckBox(tr("No"));
    QCheckBox* b3=new QCheckBox(tr("Either"));
    grid->addWidget(b1, i, 1);
    grid->addWidget(b2, i, 2);
    if (!either)
      b3->hide();
    else
      grid->addWidget(b3, i, 3);
    QButtonGroup* g=new QButtonGroup;
    g->setExclusive(TRUE);
    g->addButton(b1, button_id++);
    g->addButton(b2, button_id++);
    g->addButton(b3, button_id++);
    connect(g, SIGNAL(buttonClicked(int)),SLOT(status_changed(int)));
    m_button_groups.push_back(g);
  }
  m_mask_set=m_mask_unset=0;
}

void
select_status_box::set_mask (int mask_yes, int mask_no)
{
  DBG_PRINTF(3, "mask_yes=%x, mask_no=%x", mask_yes, mask_no);
  const int buttons_per_status = 3;
  for (uint i=0; i<sizeof(m_status_tab)/sizeof(m_status_tab[0]); i++) {
    QButtonGroup* g=m_button_groups[i];
    int v=m_status_tab[i].value;
    QAbstractButton* button;
    if (v & mask_yes) {
      button = g->button(i*buttons_per_status+0); // Yes
      button->setChecked(true);
    }
    else if (v & mask_no) {
      button = g->button(i*buttons_per_status+1); // No
      button->setChecked(true);
    }
    else if (m_either) {
      button = g->button(i*buttons_per_status+2); // Either
      button->setChecked(true);
    }
  }
  m_mask_set=mask_yes;
  m_mask_unset=mask_no;
}

// Ignore the "either" choice and just get the value of the status
int
select_status_box::status() const
{
  return m_mask_set;
}

int
select_status_box::mask_yes() const
{
  return m_mask_set;
}

int
select_status_box::mask_no() const
{
  return m_mask_unset;
}

void
select_status_box::status_changed(int id)
{
  const int buttons_per_status = 3;
  int status = id / buttons_per_status;		// 2 or 3 buttons per status
  QButtonGroup* g=m_button_groups[status];
  if (g) {
    QAbstractButton* b=g->button(id);
    if (b && b->isChecked()) {
      int v=m_status_tab[status].value;
      switch (id % buttons_per_status) {
      case 0:			// Yes
	m_mask_set |= v;
	m_mask_unset &= ~v;
	break;
      case 1:			// No
	m_mask_set &= ~v;
	m_mask_unset |= v;
	break;
      case 2:			// Either
	m_mask_set &= ~v;
	m_mask_unset &= ~v;
	break;
      default:
	DBG_PRINTF(5,"id out of (0,1,2)");
	break;
      }
    }
  }
}

void
combobox_addresses::keyPressEvent(QKeyEvent* e)
{
  // space in the email input control is a shortcut for
  // the action of completing the address
  if (e->modifiers()!=0 || e->key()!=Qt::Key_Space) {
    QComboBox::keyPressEvent(e);
    return;
  }
  mail_address_list l;

  QString search_type;
  if (m_address_type == mail_address::addrFrom)
    search_type = "last_recv_from";
  else if (m_address_type == mail_address::addrTo)
    search_type = "last_sent_to";

  if (l.fetchLike(currentText(), search_type)) {
    //if (l.fetch_completions(currentText())) {
    clear();
    mail_address_list::const_iterator iter;
    for (iter = l.begin(); iter != l.end(); iter++) {
      this->addItem(iter->get());
    }
    if (l.size() > 1)
      showPopup();
  }
}


HTML source code generated by GNU Source-Highlight plus some custom post-processing

List of all available source files