Manitou-Mail logo title

Source file: src/mbox_file.cpp

/* Copyright (C) 2004-2012 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 "db.h"
#include "db_listener.h"
#include "sqlstream.h"

#include <QFile>
#include <QFileInfo>
#include <QFileDialog>
#include <QMessageBox>
#include <QBoxLayout>
#include <QGridLayout>
#include <QDebug>
#include <QLineEdit>
#include <QCheckBox>
#include <QPushButton>
#include <QLabel>
#include <QProgressBar>

#include "mbox_file.h"

void
import_mbox_file()
{
  mbox_import_window* w = new mbox_import_window();
  w->show();
}

mbox_file::mbox_file()
{
}

// returns true if the file can be opened for reading and it has From_
// starting its first line.
//static
bool
mbox_file::check_format(const QString filename)
{
  bool format_ok=false;
  QFile f(filename);
  if (!f.open(QIODevice::ReadOnly)) {
    QString errmsg = QIODevice::tr(f.errorString().toAscii().constData());
    QMessageBox::critical(NULL, QObject::tr("Open error"), QObject::tr("Can't open file '%2': %1").arg(errmsg).arg(filename));
    return false;
  }

  const int maxlength=1024;
  char buf[maxlength];
  qint64 sz = f.readLine(buf, maxlength);
  if (sz!=-1) {
    if (sz<maxlength-1 && strncmp(buf, "From ", 5)==0) {
      format_ok=true;
    }
    else {
      QMessageBox::critical(NULL, QObject::tr("Unrecognized format"),
			    tr("No From line found at the start of the file. This file cannot be imported as an mbox."));
      
    }
  }
  f.close();
  return format_ok;
}

bool
mbox_file::launch_import_job(int import_id)
{
  db_cnx db;
  try {
    db.begin_transaction();
    sql_stream sj("INSERT INTO jobs_queue(job_type, job_args) VALUES('import_mailbox', :p1)", db);
    sj << QString("%1").arg(import_id);
    sql_stream sn("NOTIFY job_request", db);
    db.commit_transaction();
  }
  catch(db_excpt& p) {
    DBEXCPT(p);
    return false;
  }
  return true;
}

int
mbox_file::import(const mbox_import_options& opt)
{
  int import_id=0;
  m_abort_requested = false;
  db_cnx db;

  try {
    QFileInfo inf(opt.filename);
    QString filename = inf.baseName();
    sql_stream s("INSERT INTO import_mbox(tag_id,mail_status,apply_filters,filename,auto_purge) VALUES(:p1,:p2,:p3,:p4,:p5) RETURNING import_id", db);
    if (opt.tag_id>0)
      s << opt.tag_id;
    else
      s << sql_null();
    s << opt.mail_status << (opt.apply_filters?"Y":"N") << filename;
    s << (opt.auto_purge?"Y":"N");

    if (!s.eos()) {
      s >> import_id;
    }
    m_filename = opt.filename;

    if (!database_import(import_id)) {
      db.begin_transaction();
      sql_stream sd1("DELETE FROM import_message WHERE import_id=:p1", db);
      sd1 << import_id;
      sql_stream sd("DELETE FROM import_mbox WHERE import_id=:p1", db);
      sd << import_id;
      import_id=0; // import aborted or failed=>don't return the ID
      db.commit_transaction();
    }
  }
  catch(db_excpt& p) {
    DBEXCPT(p);
    import_id=0;
  }
  return import_id;
}

void
mbox_file::abort_import()
{
  DBG_PRINTF(3, "mbox upload abort requested");
  m_abort_requested = true;
}

bool
mbox_file::database_import(int import_id)
{
  db_cnx db;
  bool ret=true;
  m_uploaded_count=0;
  try {
    PGconn* pgconn = db.connection();
    int mail_number=0;
    const char* query = "INSERT INTO import_message(import_id,mail_number,encoded_mail,status) VALUES($1,$2,$3,0)";

    QFile f(m_filename);
    if (!f.open(QIODevice::ReadOnly)) {
      QString errmsg = QIODevice::tr(f.errorString().toAscii().constData());
      QMessageBox::critical(NULL, QObject::tr("Open error"), QObject::tr("Can't open file '%2': %1").arg(errmsg).arg(m_filename));
      return false;
    }

    const Oid param_types[] = {23, 23, 17}; // int4,int4,bytea
    // TODO: have a non-reusable name for the prepared statement
    PGresult* res_prep = PQprepare(pgconn, "ins_message_import",
				   query, 3, param_types);
    if (!res_prep || PQresultStatus(res_prep)!=PGRES_COMMAND_OK) {
      QMessageBox::critical(NULL, QObject::tr("Database error"), QObject::tr("Unable to prepare query: %1 %2").arg(query).arg(PQerrorMessage(pgconn)));
      return false;
    }

    QByteArray msg_bytes;
    qint64 total_size = f.size();
    qint64 read_size=0;

    db.begin_transaction();

    while (!f.atEnd() && f.error()==QFile::NoError && !m_abort_requested) {
      QApplication::processEvents();
      /* According to rfc-2822, a line must not exceed 998 bytes+cr+lf in length.
	 We truncate lines larger than 1023 to avoid importing large bogus
	 contents into the database */
      const int maxlength=1024;
      char buf[maxlength];
      qint64 sz = f.readLine(buf, maxlength);
      if (sz!=-1) { // else we expect f.error() will get us out of the loop
	read_size += sz;
	if (sz<maxlength-1 && strncmp(buf, "From ", 5)==0) {
	  /* /^From / both begins the first message and mark the end of
	     the previous message */
	  if (mail_number>0) {
	    import_message(import_id, mail_number, msg_bytes, pgconn);
	    emit progress_report(read_size*100.0/total_size);
	    mail_number++;
	    DBG_PRINTF(5, "Imported message number %d, import_id=%d", mail_number, import_id);
	    msg_bytes.clear();
	  }
	  else
	    mail_number=1;
	}
	else {
	  // DBG_PRINTF(2, "append: %s", buf);
	  msg_bytes.append(buf, sz);
	}
      }
    }

    if (!m_abort_requested) {

      if (f.error()!=QFile::NoError) {
	DBG_PRINTF(1, "mbox import has finished with QFile error=%d", f.error());
	ret=false;
      }

      if (msg_bytes.size()>0 && f.atEnd() && f.error()==QFile::NoError) {
	/* if mail_number is zero then we didn't find any line starting
	   with From followed by space. That would mean that the file is
	   not in mbox format. We still import it to allow for a single
	   mail file. */
	if (mail_number==0)
	  mail_number=1;
	import_message(import_id, mail_number, msg_bytes, pgconn);
      }

      emit progress_report(100);
    }
    else
      ret=false;

    if (res_prep)
      PQclear(res_prep);

    sql_stream srel("DEALLOCATE ins_message_import", db);
    if (ret) {
      db.commit_transaction();
      m_uploaded_count = mail_number;
    }
    else {
      db.rollback_transaction();
    }

    if (f.error()!=QFile::NoError) {
      QMessageBox::critical(NULL, tr("File error"), f.errorString());
    }
    f.close();
  }
  catch(db_excpt& p) {
    DBEXCPT(p);
  }
  return ret;
}

void
mbox_file::import_message(int import_id, int mail_number, const QByteArray& bytes,
			  PGconn* pgconn)
{
  QByteArray p1=QString("%1").arg(import_id).toAscii();
  QByteArray p2=QString("%1").arg(mail_number).toAscii();
  const char* param_values[3] = {p1.constData(), p2.constData(), bytes.constData()};
  int param_lengths[3] = {0,0, bytes.length()};
  int param_formats[3]={0,0,1};

  PGresult* res = PQexecPrepared(pgconn, "ins_message_import", 3, param_values, param_lengths, param_formats, 0);
  
  if (!res || PQresultStatus(res)!=PGRES_COMMAND_OK) {
    QMessageBox::critical(NULL, QObject::tr("Database error"), QObject::tr("Unable to execute prepared statement: %1").arg(PQerrorMessage(pgconn)));
    return;
  }

  if (res)
    PQclear(res);
}

mbox_import_window::mbox_import_window(QWidget* parent): QWidget(parent)
{
  m_listener = NULL;
  m_import_id = 0;

  setAttribute(Qt::WA_DeleteOnClose);
  setWindowTitle(tr("Import mbox file"));
  QVBoxLayout* top_layout = new QVBoxLayout(this);
  QGridLayout* gl = new QGridLayout;
  top_layout->addLayout(gl);

  QFont smaller_font = font();
  smaller_font.setPointSize((smaller_font.pointSize()*8)/10);

  int row=0;
  gl->addWidget(new QLabel(tr("Mbox file:")), row, 0);
  m_filename = new QLineEdit();
  connect(m_filename, SIGNAL(textChanged(const QString&)), this, SLOT(reset_progress()));

  m_filename->setMinimumWidth(250);
  QHBoxLayout* hbl1 = new QHBoxLayout;
  hbl1->addWidget(m_filename);
  QPushButton* btn_browse = new QPushButton(tr("Browse"));
  hbl1->addWidget(btn_browse);
  gl->addLayout(hbl1, row, 1);
  connect(btn_browse, SIGNAL(clicked()), this, SLOT(browse_file()));

  row++;
  QHBoxLayout* hbl2 = new QHBoxLayout;
  m_cbox_status = new QComboBox;
  m_cbox_status->addItem(tr("New (unread)"), QVariant(mail_msg::statusNew));
  m_cbox_status->addItem(tr("Read"), QVariant(mail_msg::statusRead));
  m_cbox_status->addItem(tr("Archived"), QVariant(mail_msg::statusArchived+mail_msg::statusRead));
  m_cbox_status->setCurrentIndex(2); // archived by default

  gl->addWidget(new QLabel(tr("Set status:")), row, 0);
  hbl2->addWidget(m_cbox_status, 1);

  hbl2->addWidget(new QLabel(tr("Apply filters:")));
  m_apply_filters = new QCheckBox();
  m_apply_filters->setChecked(false);
  hbl2->addWidget(m_apply_filters);

  hbl2->addStretch(10);
  gl->addLayout(hbl2, row, 1);


  {
    row++;
    gl->addWidget(new QLabel(tr("Assign tag:")), row, 0);
    QHBoxLayout* hbl3 = new QHBoxLayout;
    m_cbox_tag = new tag_selector(this);
    m_cbox_tag->init(true);
    hbl3->addWidget(m_cbox_tag);
    hbl3->addStretch(10);
    gl->addLayout(hbl3, row, 1);
  }

  row++;
  m_progress_bar = new QProgressBar();
  m_progress_bar->setRange(0,100);
  gl->addWidget(new QLabel(tr("Progress:")), row, 0);
  QHBoxLayout* pbar1_layout = new QHBoxLayout;
  pbar1_layout->addWidget(m_progress_bar, 15);
  m_btn_abort_upload = new QPushButton(tr("Abort"));
  m_btn_abort_upload->setEnabled(false);
  pbar1_layout->addWidget(m_btn_abort_upload);
  downsize_button(m_btn_abort_upload);
    
  gl->addLayout(pbar1_layout, row, 1);

 
  row++;
  QHBoxLayout* hbl3 = new QHBoxLayout;
  hbl3->addStretch(10);
  m_btn_start = new QPushButton(tr("Start import"));
  connect(m_btn_start, SIGNAL(clicked()), this, SLOT(start()));
  m_btn_start->setEnabled(false);
  hbl3->addWidget(m_btn_start);
  hbl3->addStretch(10);
  gl->addLayout(hbl3, row, 0, 1, 2); // colspan=2

  top_layout->addStretch(1);
}

mbox_import_window::~mbox_import_window()
{
  if (m_listener) {
    db_cnx db;
    DBG_PRINTF(4, "delete listener=%p", m_listener);
    delete m_listener;
  }
}

void
mbox_import_window::closeEvent(QCloseEvent* e)
{
  Q_UNUSED(e);
  if (m_import_id && m_import_running) {
    QMessageBox::information(this, tr("Import"), tr("The import will continue until completion at the server"));
  }
}

void
mbox_import_window::reset_progress()
{
  m_btn_start->setEnabled(true);
  m_progress_bar->setValue(0);
  m_btn_abort_upload->setEnabled(false);
}

void
mbox_import_window::browse_file()
{
  QString path=QFileDialog::getOpenFileName(this, tr("Location of mbox file"));
  if (!path.isEmpty())
    m_filename->setText(path);
}

void
mbox_import_window::update_progress(double percent)
{
  m_progress_bar->setValue((int)percent);
  m_progress_bar->repaint();
  QApplication::flush();
}

void
mbox_import_window::update_progress_import()
{
  db_cnx db;
  try {
    sql_stream s("SELECT completion,status FROM import_mbox WHERE import_id=:p1", db);
    s << m_import_id;
    float percent;
    int status;
    if (!s.eof()) {
      s >> percent >> status;
      if (status==3) { // finished
	m_import_running=false;
	m_btn_abort_upload->setEnabled(false);
	m_btn_start->setEnabled(true);
      }
      DBG_PRINTF(2, "pbar import=%d", (int)(percent*100));
      m_progress_bar->setValue((int)(percent*100));
      m_progress_bar->repaint();
      QApplication::flush();
    }
  }
  catch(db_excpt& p) {
  }
}

void
mbox_import_window::start()
{
  m_import_running=false;
  mbox_import_options opt;
  opt.filename = m_filename->text();
  opt.tag_id = m_cbox_tag->current_tag_id();
  opt.apply_filters = m_apply_filters->isChecked();
  int idx = m_cbox_status->currentIndex();
  opt.mail_status = (idx>=0) ? m_cbox_status->itemData(idx).toInt() : 0;

  mbox_file imp;
  m_progress_bar->setValue(0);
  m_progress_bar->show();
  m_progress_bar->setFormat(tr("Upload %p%"));
  connect(&imp, SIGNAL(progress_report(double)), this, SLOT(update_progress(double)));
  connect(m_btn_abort_upload, SIGNAL(clicked()), &imp, SLOT(abort_import()));
  connect(m_btn_abort_upload, SIGNAL(clicked()), this, SLOT(upload_aborted()));
  m_btn_abort_upload->setEnabled(true);

  db_cnx db;
  if (!m_listener) {
    m_listener = new db_listener(db, "mbox_import_progress");
    connect(m_listener, SIGNAL(notified()), this, SLOT(update_progress_import()));
  }
  connect(m_btn_abort_upload, SIGNAL(clicked()), this, SLOT(upload_aborted()));
  m_btn_start->setEnabled(false);
  m_import_id = imp.import(opt);

  if (m_import_id) {
    // Upload has finished and is successful, now we just keep track of the
    // server-side import
    disconnect(m_btn_abort_upload, SIGNAL(clicked()));
    connect(m_btn_abort_upload, SIGNAL(clicked()), this, SLOT(abort_import()));
    m_progress_bar->setFormat(tr("Server-side import %p%"));
    m_progress_bar->setValue(0);
    m_import_running=true;
  }
}

void
mbox_import_window::upload_aborted()
{
  m_progress_bar->setValue(0);
  m_btn_abort_upload->setEnabled(false);
  reset_progress();
}

void
mbox_import_window::downsize_button(QPushButton* btn)
{
  QFont font = btn->font();
  font.setPointSize((font.pointSize()*8)/10);
  btn->setFont(font);
}

void
mbox_import_window::abort_import()
{
  DBG_PRINTF(3, "abort_import()");
  db_cnx db;
  try {
    sql_stream s("UPDATE import_mbox SET status=2 WHERE import_id=:p1", db);
    s << m_import_id;
    m_import_running=false;
    reset_progress();
  }
  catch(db_excpt& p) {
    DBEXCPT(p);
  }
}

void
mbox_import_window::delete_imported_messages()
{
  db_cnx db;
  try {
    sql_stream s("SELECT mail_id FROM import_message WHERE import_id=:p1 ORDER BY mail_id", db);
    s << m_import_id;
    QList<mail_id_t> list;
    while (!s.eos()) {
      mail_id_t id;
      s >> id;
      list.append(id);
    }
    if (!list.isEmpty()) {
      db.begin_transaction();
      bool success=true;
      for (int i=0; i<list.size() && success; i++) {
	mail_msg msg;
	msg.set_mail_id(list.at(i));
	success = msg.mdelete();
      }
      if (success)
	db.commit_transaction();
      else
	db.rollback_transaction();
    }
  }
  catch(db_excpt& p) {
    DBEXCPT(p);
  }
}

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

List of all available source files