Manitou-Mail logo title

Source file: src/message.cpp

/* Copyright (C) 2004-2014 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 "sqlstream.h"
#include "sqlquery.h"
#include "users.h"
#include "msg_status_cache.h"
#include "identities.h"
#include "app_config.h"
#include "mail_displayer.h"
#include "ui_feedback.h"

#include <QUuid>
#include <QObject>

mail_msg::mail_msg() :
  m_nMailId(0),
  m_thread_id(0),
  m_pri(0),
  m_identity_id(0),
  m_body_fetched(false),
  m_body_html_fetched(false),
  m_body_fetched_length(0),
  m_body_length(0),
  m_bHeaderFetched(false),
  m_tags_fetched(false),
  m_mailnote_in_db(false),
  m_nInReplyTo(0),
  m_rawsize(0)
{
}

mail_msg::mail_msg(mail_id_t id,const QString& from,
		   const QString& subject, const date date):
  m_nMailId(id),
  m_thread_id(0),
  m_pri(0),
  m_identity_id(0),
  m_body_fetched(false),
  m_body_html_fetched(false),
  m_body_fetched_length(0),
  m_body_length(0),
  m_bHeaderFetched(false),
  m_tags_fetched(false),
  m_mailnote_in_db(false),
  m_nInReplyTo(0),
  m_rawsize(0)
{
  m_sFrom=from;
  m_sSubject=subject;
  m_cDate=date;
  m_Attachments.setMailId(id);
  m_header.setMailId(id);
}

mail_msg::mail_msg(const mail_result& r):
  m_nMailId(r.m_id),
  m_thread_id(r.m_thread_id),
  m_pri(r.m_pri),
  m_identity_id(0),
  m_body_fetched(false),
  m_body_html_fetched(false),
  m_body_fetched_length(0),
  m_body_length(0),
  m_bHeaderFetched(false),
  m_tags_fetched(false),
  m_mailnote_in_db(false),
  m_nInReplyTo(r.m_in_replyto),
  m_rawsize(0)

{
  m_sFrom = r.m_from;
  m_sSubject = r.m_subject;
  m_cDate = date(r.m_date);
  m_Attachments.setMailId(r.m_id);
  m_header.setMailId(r.m_id);

  set_orig_status(r.m_status);
  setStatus(r.m_status);
  set_sender_name(r.m_sender_name);
  set_flags(r.m_flags);
}

void
mail_msg::set_identity_id(int id)
{
  m_identity_id = id;
}

bool
mail_msg::fetch_body_text(bool partial)
{
  if (!GetId())
    return false;

  // don't fetch from db when m_sBody already contains the requested contents
  if (!m_body_fetched || (!partial && m_body_fetched_length<m_body_length))
  {
    db_cnx db;
    try {
      const int maxsz=30000;
      QString part;
      if (partial)
	part = QString("substr(bodytext,1,%1)").arg(maxsz);
      else
	part="bodytext";
      sql_stream s(QString("SELECT %1 FROM body WHERE mail_id=:p1").arg(part), db);
      s << get_id();
      if (!s.eos()) {
	s >> m_sBody;
	m_body_fetched_length = m_sBody.length();
	if (partial) {
	  if (m_body_fetched_length==maxsz) {
	    sql_stream s1("SELECT length(bodytext) FROM body WHERE mail_id=:p1", db);
	    s1 << get_id();
	    if (!s1.eos()) {
	      s1 >> m_body_length;
	    }
	    else {
	      // should not happen: the entry in the body table has
	      // disappeared between the 2 statements. in this case,
	      // let's consider that the text has been fetched entirely
	      m_body_length = m_body_fetched_length;
	    }
	  }
	  else {
	    m_body_length = m_body_fetched_length;
	  }
	}
	else {
	  m_body_length = m_body_fetched_length;
	}
      }
      else {
	// no entry in body table
	m_sBody.truncate(0);
	m_body_fetched_length = m_body_length = 0;
      }
      m_body_fetched=true;
    }
    catch (db_excpt p) {
      DBEXCPT(p);
      m_sBody=QString("");
      return false;
    }
  }
  return true;
}


QString&
mail_msg::get_body_text(bool partial)
{
  fetch_body_text(partial);
  return m_sBody;
}

bool
mail_msg::update_body(const QString& txt)
{
  if (!get_id()) return false;
  db_cnx db;
  try {
    sql_stream s("UPDATE body SET bodytext=:p1 WHERE mail_id=:p2", db);
    s << txt << get_id();
    if (s.affected_rows()==0) {
      sql_stream s1("INSERT INTO body(mail_id,bodytext) VALUES(:p1,:p2)", db);
      s1 << get_id() << txt;
    }
    if (m_body_fetched)
      m_sBody = txt;
  }
  catch (db_excpt& p) {
    DBEXCPT(p);
    return false;
  }
  return true;
}

bool
mail_msg::bounce()
{
  return true;			// FIXME TODO
}


void
mail_msg::set_quoted_body(const QString& squote, const QString& sprepend,
			  body_format format)
{
  if (format == format_html) {
    QString html = "<html><body>";
    html.append(mail_displayer::htmlize(sprepend));
    html.append("<p><blockquote style=\"border-left: 1px solid rgb(204, 204, 204); margin: 0pt 0pt 0pt 0.8ex; padding-left: 1ex;\">");
    html.append(squote);
    html.append("</blockquote></body></html>");
    set_body_html(html);
    return;
  }

  // Format = format_plain_text
  QString& body = get_body_text();
  uint npos = 0;
  uint nend = squote.length();
  body = sprepend;
  const QChar lf = QChar('\n');
  QString prefix="> ";
  if (get_config().exists("reply_quote_prefix"))
    prefix = get_config().get_string("reply_quote_prefix");
  // if the prefix is insanely long, truncate it
  if (prefix.length()>76) {
    prefix.truncate(76);
  }
  // Reformat the text to have lines shorter than 78 characters when possible
  const int width = 78-prefix.length();

  while (npos < nend) {
    int nposlf = squote.indexOf('\n',npos);
    if (nposlf == -1)
      nposlf = nend;
    QString para = squote.mid(npos, nposlf-npos);
    // Reformat only non-quoted text and paragraphs longer than :width
    if (para.length() > width && para.at(0) != '>') {
      QChar sep = ' ';
      QStringList list = para.split(sep);
      QString line;

      for (int i=0; i < list.size(); i++) {
	QString word = list.at(i);
	if (line.length() + word.length() < width) {
	  if (!line.isEmpty())
	    line.append(sep);
	}
	else {
	  // flush current line before adding current word
	  if (line.length() > 0) {
	    body.append(prefix);
	    body.append(line);
	    body.append(lf);
	    line.truncate(0);
	  }
	}
	// append the word even if it's longer than :width
	line.append(word);
      } // end of for each word

      if (!line.isEmpty()) {
	// flush remaining line
	body.append(prefix);
	body.append(line);
	body.append(lf);
      }  
    } // end of paragraph reformating
    else {
      body.append(prefix);
      body.append(para);
      body.append(lf);
    }
    npos = nposlf + 1;
  }
  body.append(lf);
}

const QString&
mail_msg::get_headers()
{
  if (!m_bHeaderFetched && GetId()) {
    if (header().fetch()) {
      m_sHeaders=header().m_lines;
      m_bHeaderFetched=true;
    }
  }
  return m_sHeaders;
}

bool
mail_msg::store_tags()
{
  std::list<uint>::const_iterator iter;
  db_cnx db;
  try {
    sql_stream s1("INSERT INTO mail_tags(mail_id,tag,agent) VALUES (:p1,:p2,:p3)", db);
    for (iter=m_tags.begin(); iter!=m_tags.end(); iter++) {
      s1 << GetId() << *iter << user::current_user_id();
    }
  }
  catch(db_excpt& p) {
    DBEXCPT(p);
    return false;
  }
  return true;
}

// returns true if the <tag_id> tag is currently on
bool
mail_msg::hasTag(int tag_id) const
{
  std::list<uint>::const_iterator iter;
  for (iter = m_tags.begin(); iter != m_tags.end(); iter++) {
    if (*iter == (uint)tag_id)
      return true;
  }
  return false;
}

// scan the headers and return a particular line
// starting with <headerType>
QString
mail_msg::get_header(const QString headerType)
{
  const QString& h=get_headers();
  uint nPos=0;
  uint nEnd=h.length();
  uint nHlen=headerType.length();

  QByteArray qba = headerType.toLatin1();
  const char* hType=qba.constData();
  while (nPos<nEnd) {
    // end of current line
    int nPosLf=h.indexOf('\n',nPos);
    if (nPosLf==-1)
      nPosLf=nEnd;
    QString t=h.mid(nPos,nHlen);
    if (!strcasecmp(t.toLatin1().constData()/*thisheader*/, hType)) {
      return h.mid(nPos+nHlen, nPosLf-(nPos+nHlen));
    }
    nPos=nPosLf+1;
  }
  return "";
}

/*
  Try to find to what identity the message (presumably incoming) was sent.
*/
QString
mail_msg::lookup_dest_identity()
{
  QString result;
  static identities m_ids;	// NON MT-SAFE
  if (!m_ids.fetch())
    return result;
  int ident = identity_id();

  if (ident==0) {
    QString to=get_header("To:").trimmed();
    QString cc=get_header("Cc:").trimmed();
    if (!cc.isEmpty()) {
      to += QString(",") + cc;
    }
    std::list<QString> emails_list;
    std::list<QString> names_list;
    mail_address::ExtractAddresses(to, emails_list, names_list);
    std::list<QString>::const_iterator iter1;
    for (iter1 = emails_list.begin(); iter1!=emails_list.end(); ++iter1) {
      identities::const_iterator iit = m_ids.find(*iter1);
      if (iit != m_ids.end()) {
	m_identity_id = iit->second.m_identity_id;
	result = *iter1;
	break;
      }
    }
  }
  else {
    mail_identity* pi = m_ids.get_by_id(ident);
    if (pi) {
      result = pi->m_email_addr;
    }
  }
  return result;
}


int
mail_msg::identity_id()
{
  if (m_identity_id)
    return m_identity_id;
  db_cnx db;
  try {
    sql_stream s("SELECT identity_id FROM mail WHERE mail_id=:p1", db);
    s << get_id();
    if (!s.eos()) {
      s >> m_identity_id;
    }
  }
  catch(db_excpt& p) {
    DBEXCPT(p);
  }
  return m_identity_id;
}

// Add or remove a tag in the database and in memory
bool
mail_msg::set_tag (uint id, bool set /*=true*/)
{
  db_cnx db;
  try {
    if (set) {
      sql_stream s1("INSERT INTO mail_tags(mail_id,tag,agent) VALUES (:p1,:p2,:p4)", db);
      s1 << GetId() << id << user::current_user_id();
    }
    else {
      sql_stream s2("DELETE FROM mail_tags WHERE mail_id=:p1 AND tag=:p2", db);
      s2 << GetId() << id;
    }
  }
  catch(db_excpt& p) {
    /* If there's an unique index violation when inserting, we want to
       ignore it (it means that the tag was already set for that
       message) */
    if (!p.unique_constraint_violation()) {
      DBEXCPT(p);
      return false;
    }
  }
  if (set)
    m_tags.push_back(id);
  else
    m_tags.remove(id);
  return true;
}

//static
int
mail_msg::toggle_tags_set(std::set<mail_msg*>& mset, uint tag_id, bool on)
{
  int result=0;
  if (mset.empty())
    return true;
  QString in_list;
  mail_id_to_select_in(mset, in_list);
  db_cnx db;
  try {
    if (on) {
      QString q1=QString("INSERT INTO mail_tags(mail_id,tag,agent) SELECT m.mail_id,:p1,:a FROM (SELECT mail_id FROM mail_tags WHERE tag=:p2) as mt RIGHT JOIN mail AS m ON mt.mail_id=m.mail_id WHERE mt.mail_id IS NULL AND m.mail_id IN %1").arg(in_list);
      sql_stream s1(q1, db);
      s1 << tag_id << user::current_user_id() << tag_id;
      result=s1.affected_rows();
    }
    else {
      QString q2=QString("DELETE FROM mail_tags WHERE tag=:p1 AND mail_id IN %1").arg(in_list);
      sql_stream s2(q2, db);
      s2 << tag_id;
      result=s2.affected_rows();
    }
  }
  catch(db_excpt& p) {
    DBEXCPT(p);
  }
  return result;
}

bool
mail_msg::fetchNote()
{
  bool result=true;
  db_cnx db;
  try {
    sql_stream s("SELECT note FROM notes WHERE mail_id=:id", db);
    s << GetId();
    if (!s.eof()) {
      s >> m_mail_note;
      m_mailnote_in_db=true;
    }
    else
      m_mail_note=QString::null;
  }
  catch(db_excpt& p) {
    DBEXCPT(p);
    result=false;
  }
  return result;
}

bool
mail_msg::trash()
{
  bool result=true;
  db_cnx db;
  try {
    db.begin_transaction();
    sql_stream s("SELECT trash_msg(:id,:userid)", db);
    s << getId() << user::current_user_id();
    s >> m_status;
    m_db_status |= m_status;
    db.commit_transaction();
  }
  catch(db_excpt& p) {
    db.rollback_transaction();
    DBEXCPT(p);
    result=false;
  }
  return result;
}

//static
bool
mail_msg::trash_set(std::set<mail_msg*>& mset)
{
  if (mset.empty())
    return true;
  bool result=true;
  db_cnx db;
  try {
    db.begin_transaction();
    QString in_list;
    std::set<mail_msg*>::iterator it;
    mail_id_to_sql_array(mset, in_list);
    sql_stream sql(QString("SELECT trash_msg_set(%1, :ope)").arg(in_list), db);
    sql << user::current_user_id();
    for (it=mset.begin(); it!=mset.end(); ++it) {
      mail_msg* m = *it;
      m->set_orig_status(m->status() | statusTrashed);
    }
    db.commit_transaction();
  }
  catch(db_excpt& p) {
    db.rollback_transaction();
    DBEXCPT(p);
    result=false;
  }
  return result;
}

bool
mail_msg::untrash()
{
  bool result=true;
  db_cnx db;
  try {
    db.begin_transaction();
    sql_stream sql("SELECT untrash_msg(:id, :ope)", db);
    sql << get_id() << user::current_user_id();
    sql >> m_status;
    m_db_status = m_status;
    db.commit_transaction();
  }
  catch(db_excpt& p) {
    db.rollback_transaction();
    DBEXCPT(p);
    result=false;
  }
  return result;
}

bool
mail_msg::mdelete()
{
  bool result=true;
  db_cnx db;
  try {
    db.begin_transaction();
    sql_stream s("SELECT delete_msg(:p1)", db);
    s << getId();
    db.commit_transaction();
  }
  catch(db_excpt& p) {
    db.rollback_transaction();
    DBEXCPT(p);
    result=false;
  }
  return result;
}

bool
mail_msg::store_note()
{
  bool result=true;
  const char *query=NULL;
  try {
    db_cnx db;
    if (m_mailnote_in_db) {
      if (m_mail_note.isEmpty()) {
	sql_stream s("DELETE FROM notes WHERE mail_id=:p1", db);
	s << GetId();
	m_flags &= ~flag_has_note;
      }
      else {
	query="UPDATE notes SET last_changed=now(), note=:p1 WHERE mail_id=:p2";
      }
    }
    else {
      query="INSERT INTO notes(note,mail_id,last_changed) VALUES(:p1,:p2,now())";
      m_flags |= flag_has_note;
    }
    if (query) {
      sql_stream s(query, db);
      s << m_mail_note << GetId();
    }
  }
  catch(db_excpt& p) {
    DBEXCPT(p);
    result=false;
  }
  return result;
}

const std::list<uint>&
mail_msg::get_cached_tags() const
{
  // FIXME: see what we could do if m_tags_fetched is false
  return m_tags;
}

std::list<uint>&
mail_msg::get_tags()
{
  if (m_tags_fetched) {
    return m_tags;
  }
  db_cnx db;
  try {
    sql_stream s ("SELECT tag FROM mail_tags WHERE mail_id=:p1", db);
    s << GetId();
    while (!s.eof()) {
      uint tid;
      s >> tid;
      m_tags.push_back(tid);
    }
  }
  catch (db_excpt& p) {
    DBEXCPT(p);
  }
  m_tags_fetched=true;
  return m_tags;
}

// update the message status in the database
bool
mail_msg::update_status(bool force/*=false*/)
{
  bool result = true;
  if (force || m_db_status != m_status) {
    db_cnx db;
    try {
      const char* query = "UPDATE mail SET status=:p1,mod_user_id=:o WHERE mail_id=:p2";
      sql_stream s(query, db);
      s << m_status << user::current_user_id() << getId();
      m_db_status = m_status;
      msg_status_cache::update(get_id(), m_status);
    }
    catch(db_excpt& p) {
      DBEXCPT (p);
      result = false;
    }
  }
  return result;
}

/*
  Set 'dest' to "(mail_id1, mail_id2, mail_id3,...)"
  where mail_idN come from the 's' set of mail_msg objects
*/
//static
void
mail_msg::mail_id_to_select_in(const std::set<mail_msg*>& s, QString& dest)
{
  std::set<mail_msg*>::iterator it = s.begin();
  dest.truncate(0);
  if (it==s.end())
    return;
  dest.append('(');
  for (; it!=s.end(); ++it) {
    if (it!=s.begin())
      dest.append(',');
    dest.append(QString("%1").arg((*it)->get_id()));
  }
  dest.append(')');
}

//static
void
mail_msg::mail_id_to_sql_array(const std::set<mail_msg*>& s, QString& dest)
{
  std::set<mail_msg*>::iterator it = s.begin();
  dest.truncate(0);
  if (it==s.end())
    dest= "'{}'::int[]";
  else {
    dest.append("'{");
    for (; it!=s.end(); ++it) {
      if (it!=s.begin())
	dest.append(',');
      dest.append(QString("%1").arg((*it)->get_id()));
    }
    dest.append("}'::int[]");
  }
}

/* Update the status of a set of mails */
// static
bool
mail_msg::set_or_with_status(std::set<mail_msg*>& s, uint or_mask)
{
  std::set<mail_msg*> mail_set;		// to update in MAIL
  std::set<mail_msg*> trashed_mail_set;	// to update in TRASHED_MAIL
  bool result = true;

  std::set<mail_msg*>::iterator it = s.begin();
  for (it=s.begin(); it!=s.end(); ++it) {
    mail_msg* m = *it;
    if (m->status() & statusTrashed)
      trashed_mail_set.insert(m);
    else
      mail_set.insert(m);
  }

  db_cnx db;
  try {
    QString in_list;
    // process MAIL
    if (!mail_set.empty()) {
      mail_id_to_select_in(mail_set, in_list);
      QString query = QString("UPDATE mail SET status=status|:p1, mod_user_id=:o WHERE mail_id IN %1").arg(in_list);
      sql_stream sql(query, db);
      sql << or_mask << user::current_user_id();
    }
    // process TRASHED_MAIL
    if (!trashed_mail_set.empty()) {
      mail_id_to_sql_array(trashed_mail_set, in_list);
      QString query = QString("SELECT trash_msg_set(%1, :p1)").arg(in_list);
      sql_stream sql(query, db);
      sql << user::current_user_id();
    }
    // do this only if no db exception has occurred
    for (it=s.begin(); it!=s.end(); ++it) {
      (*it)->set_orig_status((*it)->status() | or_mask);
    }
  }
  catch(db_excpt& p) {
    DBEXCPT (p);
    result = false;
  }
  return result;
}

bool
mail_msg::update_priority()
{
  bool result = true;
  db_cnx db;
  try {
    const char* query;
    query="UPDATE mail SET priority=:p1 WHERE mail_id=:p2";
    sql_stream s(query, db);
    s << m_pri << get_id();
  }
  catch(db_excpt& p) {
    DBEXCPT (p);
    result = false;
  }
  return result;
}

void
mail_msg::fetch_status ()
{
  db_cnx db;
  try {
    sql_stream s ("SELECT status,mod_user_id FROM mail WHERE mail_id=:p2", db);
    s << get_id();
    if (!s.eos()) {
      s >> m_db_status >> m_user_id_status;
      m_status = m_db_status;
      msg_status_cache::update(get_id(), m_status);
    }
  }
  catch(db_excpt& p) {
    DBEXCPT (p);
  }
}

void
mail_msg::refresh()
{
  db_cnx db;
  try {
    sql_stream s ("SELECT status,mod_user_id,thread_id,flags FROM mail WHERE mail_id=:p2", db);
    s << get_id();
    if (!s.eos()) {
      s >> m_db_status >> m_user_id_status >> m_thread_id >> m_flags;
      m_status = m_db_status;
      msg_status_cache::update(get_id(), m_status);
    }
  }
  catch(db_excpt& p) {
    DBEXCPT (p);
  }
}

void
mail_msg::build_message_id()
{
  QUuid uid = QUuid::createUuid();
  QString str=uid.toString();
  str.replace(QChar('{'), "").replace(QChar('}'), "");
  m_header.setMessageId(str+"@mm");
}

// Store the new message into the database
bool
mail_msg::store(ui_feedback* ui)
{
  bool result=false;
  PGresult* res;
  db_cnx db;
  PGconn* c=db.connection();
  try {
    db.begin_transaction();
    if (!m_nMailId) {
      sql_stream s("SELECT nextval('seq_mail_id')", db);
      s >> m_nMailId;
      build_message_id();
    }
    sql_write_fields fields(db);
    fields.add("mail_id", (int)GetId());
    fields.add("sender", m_header.m_sender);
    fields.add("sender_fullname", m_header.m_sender_fullname);
    fields.add("recipients", m_header.recipients_list());
    fields.add_if_not_empty("subject", m_header.m_subject, 1000);
    fields.add_if_not_empty("message_id", m_header.m_messageId, 100);
    fields.add_no_quote("msg_date", "now()");
    fields.add("mod_user_id", user::current_user_id());
    //    fields.add_no_quote("msg_day", "extract(days from now()-to_date('01/01/1970','DD/MM/YYYY'))");
    fields.add_no_quote("sender_date", "now()");
    fields.add_if_not_zero("in_reply_to", m_nInReplyTo);
    fields.add("flags", m_Attachments.size()>0?1:0);
    if (m_nInReplyTo) {
      /* if it's a reply, then get the thread_id in order to put it
	 into the thread */
      sql_stream st ("SELECT coalesce(thread_id,0) FROM mail WHERE mail_id=:p1", db);
      st << m_nInReplyTo;
      if (!st.eos()) {
	st >> m_thread_id;
	if (!m_thread_id) {
	  /* the message we're replying to has no thread: open one */
	  sql_stream st1 ("SELECT nextval('seq_thread_id')", db);
	  if (!st1.eos()) {
	    st1 >> m_thread_id;
	    // and link the original message to that thread
	    sql_stream sru ("UPDATE mail SET thread_id=:p1 WHERE mail_id=:p2", db);
	    sru << m_thread_id << m_nInReplyTo;
	  }
	}
	fields.add_if_not_zero ("thread_id", m_thread_id);
      }
      // update the status of the message we're replying to
      sql_stream sr ("UPDATE mail SET status=(status | :p1) WHERE mail_id=:p2", db);
      sr << statusReplied+statusArchived << m_nInReplyTo;
    }
    else if (!forwardOf().empty()) {
      const std::vector<mail_id_t>& v = forwardOf();
      sql_stream sr ("UPDATE mail SET status=(status | :p1) WHERE mail_id=:p2", db);
      for (uint ifwd=0; ifwd < v.size(); ifwd++) {
	// Update statuses of forwarded messages
	sr << statusFwded+statusArchived << v[ifwd];
      }
    }
    fields.add("status", statusRead + statusOutgoing);
    if (m_identity_id != 0)
      fields.add("identity_id", m_identity_id);

    QString sq = QString("INSERT INTO mail(%1) VALUES (%2)").arg(fields.fields()).arg(fields.values());
    const char* query;
    QByteArray qquery;
    if (db.datab()->encoding()=="UTF8")
      qquery=sq.toUtf8();
    else
      qquery=sq.toLocal8Bit();
    query=(const char*)qquery;
    DBG_PRINTF(5, "%s\n", query);
    res=PQexec(c,query);
    if (!res || PQresultStatus(res)!=PGRES_COMMAND_OK)
      throw 1;
    if (res)
      PQclear(res);

    msg_status_cache::update(get_id(), statusRead + statusOutgoing);

    if (m_body_html.isEmpty()) {
      // plain text only
      sql_stream sb("INSERT INTO body(mail_id,bodytext) VALUES (:p1,:p2)", db);
      sb << get_id() << m_sBody;
    }
    else {
      // plain text + html
      sql_stream sb("INSERT INTO body(mail_id,bodytext,bodyhtml) VALUES (:p1,:p2,:p3)", db);
      sb << get_id() << m_sBody << m_body_html;
    }

    result=store_tags();
    if (result) {
      mail_header& h=header();
      h.setMailId(GetId());
      result=h.store();
    }
    if (!m_mail_note.isEmpty()) {
      result = (result && store_note());
    }
    m_Attachments.setMailId(m_nMailId);
    result=(result && m_Attachments.store(ui));
    if (result)
      db.commit_transaction();
    else {
      DBG_PRINTF(2, "rollback");
      db.rollback_transaction();
    }
  }
  catch(db_excpt& p) {
    DBG_PRINTF(2, "rollback caused by db_excpt");
    db.rollback_transaction();
    DBEXCPT(p);
    result=false;
  }
  catch(int errcode) {
    DBG_PRINTF(2, "rollback caused by other reason (%d)", errcode);
    db.rollback_transaction();
    DBEXCPT(c);
    result=false;
  }
  return result;
}

/*
  Returns the attachment for the HTML part of the body, if there is
  one, otherwise returns NULL
 */
attachment*
mail_msg::body_html_attached_part()
{
  attachments_list& attchs = this->attachments();
  attchs.fetch();
  if (attchs.size() > 0) {
    attachments_list::iterator iter;
    for (iter=attchs.begin(); iter!=attchs.end(); iter++) {
      // we consider that the first unnamed html part is what we're looking for
      // later we'll rely on a better support for multipart/alternative in the db
      if (iter->filename().isEmpty() && iter->mime_type()=="text/html") {
	return &(*iter);
      }
    }
  }
  return NULL;
}

void
mail_msg::set_tags (const std::list<uint>& l)
{
  m_tags = l;
  m_tags_fetched=true;
}

/* Extract the text that can be quoted from a message when there is no
   selected text */
void
mail_msg::find_text_to_quote(QString& text_to_quote)
{
  if (get_body_text().isEmpty()) {
    /* The body to quote is empty.
       If there are unnamed text attachments, use them instead for quoting */
    text_to_quote.truncate(0);
    if (!attachments().empty()) {
      attachments_list::iterator iter=attachments().begin();
      for (; iter!=attachments().end(); ++iter)	{
	if ((iter->mime_type() == "text/plain" || iter->mime_type() == "text")
	    && iter->filename().isEmpty())  {
	  const char* contents=iter->get_contents();
	  if (contents)
	    text_to_quote.append(contents);
	}
      }
    }
  }
  else {
    text_to_quote = get_body_text();
  }
}

void
mail_msg::streamout_body(std::ofstream& o)
{
  QString& b=get_body_text();
  o.write (b.toLatin1().constData(), b.length()); 	// TODO: which encoding?
}


mail_msg
mail_msg::setup_reply(const QString& quoted_text, int whom_to, body_format format)
{
  mail_msg msg;
  mail_header replyHeader;

  QRegExp qRe("^re\\s*:\\s*", Qt::CaseInsensitive);
  // prepends "Re: " if it's not there already
  if (qRe.indexIn(Subject())==0)
    replyHeader.m_subject = Subject();
  else
    replyHeader.m_subject = QString("Re: ") + Subject();

  QString reply_to = get_header("Reply-To:");
  if (!reply_to.isEmpty()) {
    replyHeader.m_to = reply_to.trimmed();
  }
  else {
    // reply to sender
    replyHeader.m_to = get_header("From:").trimmed();
    if (replyHeader.m_to.isEmpty()) {
      replyHeader.m_to = From();
    }
  }

  if (whom_to==3) {
    // reply to mailing-list
    QString mailto=get_header("List-Post:").trimmed();
    QRegExp q("^<mailto:(.*)>$", Qt::CaseInsensitive);
    if (q.indexIn(mailto)==0) {
      mailto = q.cap(1);
    }
    replyHeader.m_to = mailto.isEmpty() ? From() : mailto;
  }

  // reply-to takes precedence over reply-to-all
  if (whom_to==2 && reply_to.isEmpty()) {
    // reply to all
    replyHeader.m_cc = get_header("To:").trimmed();
    QString old_cc = get_header ("Cc:").trimmed();
    if (!old_cc.isEmpty()) {
      if (!replyHeader.m_cc.isEmpty())
	replyHeader.m_cc += QString(",") + old_cc;
      else
	replyHeader.m_cc = old_cc;
    }

    // Break down the Cc fields into email+names components
    std::list<QString> emails_list;
    std::list<QString> names_list;
    mail_address::ExtractAddresses(replyHeader.m_cc.toLatin1().constData(),
				   emails_list, names_list);

    // Reassemble the list of addresses, skipping those that belong to us
    std::list<QString>::const_iterator iter1,iter2;
    QString expurged_cc;
    identities our_identities;
    our_identities.fetch();
    for (iter1 = emails_list.begin(), iter2 = names_list.begin();
	 iter1!=emails_list.end() && iter2!=names_list.end();
	 ++iter1, ++iter2) {
      identities::iterator iit = our_identities.find(*iter1);
      if (iit == our_identities.end()) { // not ours
	if (!expurged_cc.isEmpty())
	  expurged_cc += ", ";
	mail_address addr;
	addr.set_email(*iter1);
	addr.set_name(*iter2);
	expurged_cc += addr.email_and_name();
      }
    }
    DBG_PRINTF(5,"expurged_cc=%s\n", expurged_cc.toLatin1().constData());
    replyHeader.m_cc = expurged_cc;
  }
  QString us = lookup_dest_identity();
  replyHeader.m_envelope_from = us;
  replyHeader.m_inReplyTo = get_header("Message-Id:").trimmed();
  QString sender_name;	// to which the original message will be attributed
  QString sFrom=get_header("From:");

  if (!sFrom.isEmpty()) {
    // extract the sender's name from the headers
    std::list<QString> addrs;
    std::list<QString> names;
    int r=mail_address::ExtractAddresses(sFrom.toLatin1().constData(), addrs, names);
    if (!r && names.size()>0)
      sender_name=names.front();
  }
  if (sender_name.isEmpty()) {
    // else get it from the database
    mail_address_list from;
    if (from.fetchFromMail(GetId(), (int)mail_address::addrFrom) && from.size()>0)
      sender_name = from.begin()->name();
  }
  // else get it verbatim from the From: line
  if (sender_name.isEmpty())
    sender_name = sFrom;

  msg.set_header(replyHeader);

  // In-Reply-To (this message)
  msg.setInReplyTo(GetId());

  if (get_config().get_number("reply_copy_tags")) {
    /* assign to the reply the tags of the original mail */
    msg.set_tags (get_tags());
  }

  QString attrib;
  if (get_config().exists("reply_attribution_string")) {
    attrib = get_config().get_string("reply_attribution_string");
    attrib.replace("%F", sender_name);
    attrib.append("\n\n");
  }
  else {
    // default attribution string
    attrib = QString("\t ") + sender_name + " writes\n\n";
  }
  msg.set_quoted_body(quoted_text, attrib, format);
  return msg;
}

mail_msg
mail_msg::setup_forward()
{
  mail_msg msg;
  mail_header& fwd_header=msg.header();

  app_config& conf = get_config();
  QString subj_forw = conf.get_string("forward_subject_prefix");
  if (subj_forw.isEmpty()) {
    subj_forw = QObject::tr("[Forwarded]");
  }
  fwd_header.m_subject = subj_forw + QString(" ") + subject();

  // Auto-selection of the address to forward to, based on the initial To address
  if (conf.get_bool("use_forward_addresses_table")) {
    try {
      db_cnx db;
      sql_stream s(QString("SELECT forward_to FROM forward_addresses fa, mail_addresses ma, addresses a WHERE ma.mail_id=:p1 AND ma.addr_type=%1 AND ma.addr_id=a.addr_id AND a.email_addr=fa.to_email_addr").arg(mail_address::addrTo), db);
      s << get_id();
      if (!s.eos()) {
	s >> fwd_header.m_to;
      }
    }
    catch (db_excpt p) {
      DBEXCPT(p);
    }
  }

  QString sf_quote = conf.get_string("forward_start_quote");
  if (sf_quote.isEmpty()) {
    sf_quote=QObject::tr("------------------------- start of forwarded message ------------------------");
  }
  QString ef_quote = conf.get_string("forward_end_quote");
  if (ef_quote.isEmpty()) {
    ef_quote=QObject::tr("------------------------- end of forwarded message ---------------------------");
  }
  QString fwd_body;
  find_text_to_quote(fwd_body);

  QString quoted_header = this->forwarded_header_excerpt();
  fwd_body = sf_quote + "\n" + quoted_header + "\n" + fwd_body + "\n" + ef_quote + "\n";
  
  msg.set_body_text(fwd_body);
  msg.set_fwded_mail_id(this->get_id());

  return msg;
}

QString
mail_msg::forwarded_header_excerpt()
{
  static const char *hkeep[] = {
    "date:", "from:", "to:", "cc:", "reply-to:", "bcc:", "subject:"
  };

  const QString& h = this->get_headers();
  uint npos=0;
  uint nend=h.length();
  QString output;

  const int nh=sizeof(hkeep)/sizeof(hkeep[0]);
  QString slines[nh];

  while (npos<nend) {
    // position of end of current line
    int npos_lf=h.indexOf('\n',npos);
    if (npos_lf==-1)
      npos_lf=nend;

    for (int i=0; i<nh; i++) {
      uint nhlen=strlen(hkeep[i]);
      if (h.mid(npos,nhlen).toLower() == hkeep[i]) {
	slines[i]= h.mid(npos,nhlen) + h.mid(npos+nhlen, 1+npos_lf-(npos+nhlen));
	break;
      }
    }
    npos=npos_lf+1;
  }
  // order the headers as in hkeep and not as they appear in the
  // original message
  for (int j=0; j<nh; j++) {
    if (slines[j].length()>0)
      output+=slines[j];
  }
  return output;
}

QString&
mail_msg::get_body_html()
{
  fetch_body_html();
  return m_body_html;
}

bool
mail_msg::fetch_body_html()
{
  if (!GetId())
    return false;

  if (!m_body_html_fetched) {
    db_cnx db;
    try {
      sql_stream s("SELECT bodyhtml FROM body WHERE mail_id=:p1", db);
      s << get_id();
      if (!s.eos()) {
	s >> m_body_html;
      }
      m_body_html_fetched = true;
    }
    catch (db_excpt p) {
      DBEXCPT(p);
      m_body_html = QString("");
      return false;
    }
  }
  return true;

}


bool
mail_msg::get_rawsize(int* size)
{
  if (m_rawsize>0) {
    *size=m_rawsize;
    return true;
  }
  if (!get_id())
    return false;

  db_cnx db;
  try {
    sql_stream s("SELECT raw_size FROM mail WHERE mail_id=:p1", db);
    s << get_id();
    if (!s.eos()) {
      s >> m_rawsize;
      *size = m_rawsize;
    }
  }
  catch (db_excpt p) {
    DBEXCPT(p);
    return false;
  }
  return true;
}


/*
  unit in ("days", "hours", "minutes")
*/
bool
mail_msg::get_msg_age(const QString unit, int* age)
{
  if (!get_id())
    return false;

  db_cnx db;
  try {
    sql_stream s("SELECT cast(extract(epoch from now())-extract(epoch from sender_date) AS integer) FROM mail WHERE mail_id=:p1", db);
    s << get_id();
    if (!s.eos()) {
      int i;
      s >> i;
      if (unit=="minutes")
	*age = i/60;
      else if (unit=="hours")
	*age = i/3600;
      else if (unit=="days")
	*age = i/86400;
      else
	*age = 0;
    }
    else
      *age=0;
  }
  catch (db_excpt p) {
    DBEXCPT(p);
    return false;
  }
  return true;  
}

/*
  unit in ("days", "hours", "minutes")
*/
bool
mail_msg::get_sender_timestamp(time_t* t)
{
  if (!get_id())
    return false;

  db_cnx db;
  try {
    sql_stream s("SELECT extract(epoch from sender_date) FROM mail WHERE mail_id=:p1", db);
    s << get_id();
    if (!s.eos()) {
      float sd;
      s >> sd;
      *t = (time_t)sd;
    }
  }
  catch (db_excpt p) {
    DBEXCPT(p);
    return false;
  }
  return true;  
}

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

List of all available source files