Source file: src/attachment.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 "attachment.h"
#include "app_config.h"
#include <fstream>
#include <QFile>
#include <QFileInfo>
#include <QDir>
#include <QRegExp>
#include <QTextCodec>
#include <QProcess>
#include <QMessageBox>
#include <QTimer>
#include <QUuid>
#include "sha1.h"
#include "unistd.h"
#ifdef Q_OS_WIN
#include <windows.h>
#include <shlwapi.h>
#include <tchar.h>
#endif
attachment::attachment() :
m_Id(0), m_data(NULL), m_descFetched(false), m_inMemory(false)
{
}
attachment::attachment(const attachment& a)
{
m_data = a.m_data;
m_Id = a.m_Id;
m_filename = a.m_filename;
m_mime_type = a.m_mime_type;
m_size = a.m_size;
m_inMemory = a.m_inMemory;
m_descFetched = a.m_descFetched;
m_charset = a.m_charset;
m_mime_content_id = a.m_mime_content_id;
}
attachment::~attachment()
{
free_data();
}
bool
attachment::is_binary()
{
const unsigned char* contents = (unsigned char*)get_contents();
for (uint i=0; i<m_size; i++) {
if (contents[i]!='\t' && contents[i]!='\r' && contents[i]!='\n' &&
(contents[i]<0x20 || contents[i]>=0x7f))
return true;
}
return false;
}
QString
attachment::default_os_application()
{
#ifdef Q_OS_WIN
QString ext = extension(filename());
if (!ext.isEmpty()) {
ext.prepend(".");
DWORD dwSize = 255;
TCHAR sBuffer[MAX_PATH] = {0};
wchar_t* exta = new wchar_t[ext.length()+1];
ext.toWCharArray(exta);
exta[ext.length()]='\0';
HRESULT hr = AssocQueryString((ASSOCF)0, ASSOCSTR_FRIENDLYAPPNAME, exta, L"Open", sBuffer, &dwSize);
delete exta;
return QString::fromWCharArray(sBuffer);
}
#endif
return QString::null;
}
QString
attachment::sha1_to_base64(unsigned int digest[5])
{
QString res;
const char* alpha = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
unsigned int mask;
unsigned int idx;
// for each 6 bits block, i being the highest bit number of the block
for (int i=32*5-1; i>0; i-=6) {
if (i%32 >= 5) {
// 6 bits block doesn't cross word boundary
mask = 0x3f << ((i%32)-5);
idx = (digest[4-i/32] & mask) >> ((i%32)-5);
}
else {
// 6 bits block crosses word boundary
unsigned int nbd=5-(i%32);
mask = 0x3f >> nbd;
idx = (digest[4-i/32] & mask) << nbd;
if (i/32>0) { // if we're not at the rightmost word
mask = ((1<<(nbd+1))-1) << (32-nbd);
idx += (digest[1+4-i/32] & mask) >> (32-nbd);
}
}
res.append(alpha[idx]);
}
return res;
}
void
attachment::compute_sha1_fp()
{
if (m_filename.length()>0) {
get_size_from_file();
}
QFile f(m_filename);
if (f.open(QIODevice::ReadOnly)) {
SHA1 sha1;
sha1.Reset();
while (!f.atEnd() && !f.error()) {
unsigned char buf[8192];
qint64 n_read;
while ((n_read=f.read((char*)buf, sizeof(buf))) >0) {
sha1.Input(buf, (unsigned int)n_read);
}
}
if (!f.error()) {
unsigned int digest[5];
sha1.Result(digest);
m_sha1_b64 = sha1_to_base64(digest);
}
f.close();
}
}
QString
attachment::application() const
{
db_cnx db;
try {
sql_stream s("SELECT program_name FROM programs WHERE content_type=':p1' AND conf_name=:p2", db);
s << m_mime_type << get_config().name();
QString prog;
if (!s.eof()) {
s >> prog;
}
else {
sql_stream s2("SELECT program_name FROM programs WHERE content_type=':p1' AND conf_name is null", db);
s2 << m_mime_type;
if (!s2.eof()) {
s2 >> prog;
}
}
return prog;
}
catch(db_excpt& p) {
DBEXCPT(p);
return QString::null;
}
}
QString
attachment::get_temp_location()
{
QString fname;
if (m_filename.isEmpty()) {
/* try to find a proper extension for the filename we generate,
since viewer programs may rely on it. It's also required for
the '<winshell>' pseudo-viewer to have a chance to work. */
QString extension;
if (!m_mime_type.isEmpty()) {
QMap<QString,QString> extension_map;
fetch_filename_suffixes(extension_map); // TODO: cache this
QMap<QString,QString>::const_iterator it = extension_map.constBegin();
for (; it != extension_map.constEnd(); ++it) {
if (it.value() == m_mime_type) {
extension = it.key();
break;
}
}
}
fname = QString("attch-%1").arg(m_Id);
if (!extension.isEmpty()) {
fname.append('.');
fname.append(extension);
}
}
else {
fname = QString("attch-%1-%2").arg(m_Id).arg(m_filename);
}
QString dirname=get_config().get_string("attachments_directory");
QDir dir;
if (!dirname.isEmpty()) {
dir = QDir(dirname);
if (!dir.exists())
dir = QDir::temp();
}
else
dir = QDir::temp();
return dir.absoluteFilePath(fname);
}
void
attachment::launch_os_viewer(const QString document_path)
{
#ifdef Q_OS_WIN
SHELLEXECUTEINFO shellInfo;
memset(&shellInfo, 0, sizeof(SHELLEXECUTEINFO));
shellInfo.cbSize = sizeof(SHELLEXECUTEINFO);
shellInfo.fMask = SEE_MASK_NOCLOSEPROCESS;
shellInfo.lpFile = (LPCTSTR)document_path.utf16();
shellInfo.lpParameters = NULL;
shellInfo.nShow = SW_SHOW;
::ShellExecuteEx(&shellInfo);
#endif
}
void
attachment::launch_external_viewer(const QString document_path)
{
QString command_string=application().trimmed();
QRegExp qr=QRegExp("\\$1");
if (!command_string.isEmpty()) {
#ifdef Q_OS_WIN
if (command_string == "<winshell>") {
/* As a special case for Windows, <winshell> means that the
attachment's name itself is passed to ShellExecute. It assumes
that it triggers the association at the OS level between the file
extension and a program */
launch_os_viewer(document_path);
return;
}
#endif
QString q_doc_path = document_path;
if (document_path.indexOf(" ")>=0) {
// enclose between quotes if the path contains spaces
q_doc_path.prepend('"');
q_doc_path.append('"');
}
if (command_string.indexOf(qr) >=0) // replace $1 if specified
command_string.replace(qr, q_doc_path);
else {
command_string.append(" ");
command_string.append(q_doc_path);
}
if (!QProcess::startDetached(command_string)) {
QMessageBox::warning(NULL, "Error", QApplication::tr("Unable to run command:\n%1").arg(command_string));
}
}
}
void
attachment::set_contents(const char* contents, uint size)
{
m_data = (char*) malloc(size+1);
if (!m_data)
return;
memcpy (m_data, contents, size);
m_data[size] = '\0';
m_size = size;
}
void
attachment::set_contents_ptr (char* contents, uint size)
{
m_data = contents;
m_size = size;
}
void
attachment::append_decoded_contents(QString& body)
{
char* contents = get_contents();
if (!contents)
return;
if (charset().isEmpty() || charset()==QString("us-ascii")
|| charset()==QString("US-ASCII")) {
body.append(contents);
}
else {
QTextCodec* codec = QTextCodec::codecForName(charset().toLatin1());
if (!codec) {
body.append(contents);
}
else {
if (m_data)
body.append(codec->toUnicode(m_data, m_size));
}
}
}
char*
attachment::get_contents()
{
if (m_data)
return m_data;
db_cnx db;
try {
Oid lobjId;
if (size()==0)
return NULL;
sql_stream s("SELECT content FROM attachment_contents WHERE attachment_id=:p1", db);
s << m_Id;
if (!s.eos()) {
s >> lobjId;
}
else
return NULL; // oops, no (more) content here
// we allocate one more byte for a '\0' terminator, so that
// the attachment content can be used as a C string if needed
m_data = (char*) malloc(size()+1);
if (!m_data)
return NULL;
db.begin_transaction();
PGconn* c=db.connection();
int lobj_fd = lo_open(c, lobjId, INV_READ);
if (lobj_fd < 0) {
DBG_PRINTF(2, "failed to open large object %u", (uint)lobjId);
throw db_excpt("lo_open", db);
}
lo_read(c, lobj_fd, m_data, size());
lo_close(c, lobj_fd);
db.commit_transaction();
m_data[size()]='\0';
return m_data;
}
catch(db_excpt& p) {
if (m_data) {
free(m_data);
m_data=NULL;
}
db.rollback_transaction();
DBEXCPT(p);
return NULL;
}
}
void
attachment::streamout_content(std::ofstream& of)
{
if (m_data) {
of.write(m_data,size());
return;
}
if (size()==0) {
of.write("", 0);
return;
}
db_cnx db;
try {
sql_stream s("SELECT content FROM attachment_contents WHERE attachment_id=:p1", db);
s << m_Id;
Oid lobjId;
if (!s.eos()) {
s >> lobjId;
}
else
return; // nothing to read from
db.begin_transaction();
PGconn* c=db.connection();
int lobj_fd = lo_open(c, lobjId, INV_READ);
if (lobj_fd < 0) {
DBG_PRINTF(4, "lo_open returns %d", lobj_fd);
throw db_excpt("lo_open", db);
}
char data[8192];
unsigned int nread;
do {
nread = lo_read(c, lobj_fd, data, sizeof(data));
of.write(data, nread);
} while (nread==sizeof(data));
lo_close(c, lobj_fd);
db.commit_transaction();
}
catch(db_excpt& p) {
db.rollback_transaction();
DBEXCPT(p);
}
}
int
attachment::open_lo(struct lo_ctxt* slo)
{
/* allocate a connection on the heap rather than on the stack
because we'll need it through all the use of the 'slo' context */
db_cnx* db = new db_cnx();
slo->db=db;
PGconn* c=db->connection();
try {
sql_stream s("SELECT content FROM attachment_contents WHERE attachment_id=:p1", *db);
s << m_Id;
Oid lobjId;
if (!s.eos()) {
s >> lobjId;
}
else {
slo->lfd=-1;
return 0;
}
db->begin_transaction();
int lobj_fd = lo_open(c, lobjId, INV_READ);
DBG_PRINTF(4, "lo_open returns %d", lobj_fd);
if (lobj_fd < 0) {
slo->lfd=-1;
throw db_excpt("lo_open", *db);
}
slo->eof = false;
slo->db = db;
slo->lfd = lobj_fd;
slo->size=size();
slo->chunk_size=8192;
return 1;
}
catch(db_excpt& p) {
db->rollback_transaction();
DBEXCPT(p);
return 0;
}
}
/* Transfer a chunk of data from a already opened large object to an
ofstream */
int
attachment::streamout_chunk(struct lo_ctxt* slo, std::ofstream& of)
{
char data[8192];
unsigned int nread;
PGconn* c = slo->db->connection();
nread = lo_read(c, slo->lfd, data, sizeof(data));
of.write(data, nread);
return (nread==sizeof(data)); // true if not done yet
}
/*
Release a large object context
*/
void
attachment::close_lo(struct lo_ctxt* slo)
{
if (slo->lfd>=0) {
PGconn* c = slo->db->connection();
DBG_PRINTF(4, "lo_close(%d)", slo->lfd);
lo_close(c, slo->lfd);
slo->db->commit_transaction();
}
delete slo->db;
slo->db=NULL;
}
bool
attachment::fetch()
{
if (m_descFetched)
return true;
#ifdef WITH_PGSQL
return true;
#endif
}
void
attachment::free_data()
{
if (m_data) {
free (m_data);
m_data=NULL;
}
}
bool
attachment::store(uint mail_id, ui_feedback* ui)
{
db_cnx db;
try {
db.begin_transaction();
if (!db.next_seq_val("seq_attachment_id", &m_Id))
return false;
if (m_filename.length()>0) {
get_size_from_file();
compute_sha1_fp();
}
sql_stream s("INSERT INTO attachments(attachment_id, mail_id, content_type, content_size, filename,mime_content_id) VALUES (:p1, :p2, ':p3', :p4, ':p5', :cid)", db);
QFileInfo fi(m_filename);
s << m_Id << mail_id << m_mime_type << m_size << fi.fileName();
if (!m_mime_content_id.isEmpty())
s << m_mime_content_id;
else
s << sql_null();
if (!import_file_content(ui)) {
DBG_PRINTF(2, "Error while importing file contents: %s", m_filename.toLocal8Bit().constData());
db.rollback_transaction();
return false;
}
db.commit_transaction();
}
catch(db_excpt& p) {
db.rollback_transaction();
DBEXCPT(p);
return false;
}
return true;
}
QString // [static]
attachment::guess_mime_type(const QString filename)
{
typedef QMap<QString,QString> mt;
static mt extension_map;
if (extension_map.isEmpty()) {
fetch_filename_suffixes(extension_map);
}
QString ext = extension(filename);
if (!ext.isEmpty()) {
mt::const_iterator it = extension_map.constFind(ext);
if (it != extension_map.constEnd())
return it.value();
}
return QString("application/octet-stream"); // nothing found
}
// static
QString
attachment::extension(const QString filename)
{
int dotpos = filename.lastIndexOf('.');
if (dotpos >=0 && dotpos+1 < filename.length())
return filename.mid(dotpos+1);
else
return QString::null;
}
void
attachment::get_size_from_file()
{
if (QFile::exists(m_filename)) {
QFile f(m_filename);
m_size=(uint)f.size();
}
else
m_size=0;
}
//static
bool
attachment::fetch_filename_suffixes(QMap<QString,QString>& m)
{
db_cnx db;
try {
sql_stream s("SELECT suffix, mime_type FROM mime_types", db);
QString suffix;
QString mime_type;
while (!s.eos()) {
s >> suffix >> mime_type;
m[suffix]=mime_type;
}
}
catch(db_excpt& p) {
DBEXCPT(p);
return false;
}
return true;
}
void
attachment::create_mime_content_id()
{
QUuid uid = QUuid::createUuid();
QString str = uid.toString();
str.replace(QChar('{'), "").replace(QChar('}'), "");
m_mime_content_id = str + "@mm";
}
/*
Insert the contents of a file into the ATTACHMENT_CONTENTS table
members updated: m_size, m_Id
*/
bool
attachment::import_file_content(ui_feedback* ui)
{
db_cnx db;
try {
db.begin_transaction();
Oid lobj_id=0;
/* Look up the attachment by its fingerprint.
If it's already found in the db, we create a new reference to its large-object id
without importing a new copy of the contents */
if (!m_sha1_b64.isEmpty()) {
sql_stream sfp("SELECT content FROM attachment_contents WHERE fingerprint=:p1", db);
sfp << m_sha1_b64;
if (!sfp.eos()) {
sfp >> lobj_id;
}
}
if (m_filename.length()>0) {
QByteArray qb_fname = QFile::encodeName(m_filename);
if (lobj_id==0) {
if (!ui) {
// if not reporting progress, import in one go
lobj_id = lo_import(db.connection(), qb_fname.constData());
if (lobj_id==0) {
throw QString("Error lo_import filename=%1").arg(qb_fname.constData());
}
}
else {
QFileInfo fi(m_filename);
int size=0;
const int max_size = (1U<<31)-1; // 2GB: max size of a PG large object
if (fi.exists()) {
if (fi.size() > max_size)
size = max_size;
else
size = (int)fi.size();
}
else {
DBG_PRINTF(2, "Error: file '%s' does not exist", qb_fname.constData());
db.rollback_transaction();
return false;
}
const int chunk_size = 65536/8;
ui->set_maximum((size+chunk_size-1)/chunk_size);
ui->set_status_text(QObject::tr("Uploading file %1").arg(fi.fileName()));
PGconn* cnx=db.connection();
lobj_id = lo_creat(cnx, INV_READ | INV_WRITE);
if (lobj_id<=0)
throw QString("lo_creat failed");
int fd = lo_open(cnx, lobj_id, INV_WRITE);
if (fd<0)
throw QString("Error lo_open lobj_id=%1").arg(lobj_id);
QFile file(m_filename);
char buffer[chunk_size];
qint64 rd=0;
qint64 written=0;
int i=0;
if (!file.open(QIODevice::ReadOnly))
throw QString("Error opening file");
while (rd>=0 && !file.atEnd() && !file.error() && written<max_size) {
rd=file.read(buffer, chunk_size);
if (rd>0) {
int nb = lo_write(cnx, fd, buffer, rd); // returns negative on failure
if (nb>=0)
written += nb;
else {
throw QString("Error lo_write lobj_id=%1").arg(lobj_id);
}
}
ui->set_value(++i);
}
lo_close(cnx, fd);
}
}
}
else if (m_size>0 && m_data!=NULL) {
if (lobj_id==0) {
lobj_id = lo_creat(db.connection(), INV_READ | INV_WRITE);
int lobj_fd = lo_open (db.connection(), lobj_id, INV_WRITE);
lo_write(db.connection(), lobj_fd, m_data, m_size);
lo_close(db.connection(), lobj_fd);
}
}
else {
DBG_PRINTF(2,"no filename and no size or no data");
db.rollback_transaction();
return true; // nothing to store
}
sql_stream s("INSERT INTO attachment_contents(attachment_id, content, fingerprint) VALUES (:p1,:p2,:p3)", db);
s << m_Id << (unsigned long)lobj_id;
if (!m_sha1_b64.isEmpty())
s << m_sha1_b64;
else
s << sql_null();
db.commit_transaction();
}
catch(db_excpt& p) {
db.rollback_transaction();
DBEXCPT(p);
return false;
}
catch(QString err) {
DBG_PRINTF(2, err.toLocal8Bit().constData());
db.rollback_transaction();
return false;
}
return true;
}
attachments_list::attachments_list() : m_mailId(0), m_bFetched(false)
{
}
attachments_list::~attachments_list()
{
}
bool
attachments_list::fetch()
{
if (m_bFetched)
return true;
try {
db_cnx db;
sql_stream s("SELECT attachment_id,content_type,content_size,filename,charset,mime_content_id FROM attachments WHERE mail_id=:p1 ORDER BY attachment_id", db);
s << m_mailId;
while (!s.eos()) {
attachment attch;
int id, size;
QString filename, content_type, charset, mime_content_id;
s >> id >> content_type >> size >> filename >> charset >> mime_content_id;
attch.setAll(id, size, filename, content_type, charset);
attch.set_mime_content_id(mime_content_id);
push_back(attch);
}
}
catch(db_excpt& p) {
DBEXCPT(p);
return false;
}
m_bFetched=true;
return true;
}
attachment*
attachments_list::get_by_content_id(const QString mime_content_id)
{
if (mime_content_id.isEmpty())
return NULL;
std::list<attachment>::iterator it;
for (it=begin(); it!=end(); it++) {
DBG_PRINTF(5, "attch %d,mime_content_id=%s", (*it).getId(), (*it).mime_content_id().toLocal8Bit().constData());
if ((*it).mime_content_id() == mime_content_id)
return &(*it);
}
DBG_PRINTF(2,"attachment not found by content_id");
return NULL;
}
bool
attachments_list::store(ui_feedback* ui)
{
std::list<attachment>::iterator it;
for (it=begin(); it!=end(); it++) {
if (!(*it).store(m_mailId, ui))
return false;
}
return true;
}
attachment_viewer::attachment_viewer()
{
}
attachment_viewer::~attachment_viewer()
{
}
attch_viewer_list::attch_viewer_list() : m_fetched(false)
{
}
attch_viewer_list::~attch_viewer_list()
{
}
bool
attch_viewer_list::fetch(const QString& conf_name, bool force)
{
if (m_fetched && !force)
return true;
try {
db_cnx db;
sql_stream s("SELECT program_name,content_type FROM programs WHERE conf_name=:p2 OR conf_name IS NULL", db);
s << conf_name;
while (!s.eos()) {
attachment_viewer v;
s >> v.m_program >> v.m_mime_type;
push_back(v);
}
m_fetched=true;
}
catch(db_excpt& p) {
DBEXCPT(p);
return false;
}
return true;
}
bool
attachment::open()
{
if (open_lo(&m_lo))
return true;
else
return false;
}
qint64
attachment::read(qint64 size, char* buf)
{
if (size==0) return 0;
PGconn* c = m_lo.db->connection();
int r = lo_read(c, m_lo.lfd, buf, (size_t)size);
DBG_PRINTF(4, "attachment::read requested %d bytes, read %d bytes on lfd=%d",
(size_t)size, r, m_lo.lfd);
if (r<size || r==0) {
m_lo.eof=true;
return r;
}
else {
return (qint64)r;
}
}
bool
attachment::eof() const
{
return m_lo.eof;
}
void
attachment::close()
{
close_lo(&m_lo);
}
attachment_network_reply*
attachment::network_reply(const QNetworkRequest& req, QObject* parent)
{
return new attachment_network_reply(req, this, parent);
}
attachment_network_reply::attachment_network_reply(const QNetworkRequest &req, attachment* a, QObject* parent) : QNetworkReply(parent)
{
// created with all the data at once
m_a=a;
if (!a->open()) {
setError(ContentNotFoundError, "Content not found in database");
DBG_PRINTF(2, "error in opening attachment (attachment_id=%d)", a->getId());
}
else {
DBG_PRINTF(5, "attachment opened (attachment_id=%d)", a->getId());
setRequest(req);
setOperation(QNetworkAccessManager::GetOperation);
setReadBufferSize(0);
open(QIODevice::ReadOnly/* | QIODevice::Unbuffered*/);
setError(NoError, "No Error");
}
QTimer::singleShot(0, this, SLOT(go()));
}
attachment_network_reply::~attachment_network_reply()
{
DBG_PRINTF(4, "~attachment_network_reply()");
}
qint64
attachment_network_reply::readData(char* data, qint64 size)
{
DBG_PRINTF(4, "readData size=%ld", size);
if (m_a->eof()) {
DBG_PRINTF(4, "eof on attachment");
return -1;
}
qint64 res=m_a->read(size, data);
DBG_PRINTF(4, "read %ld bytes", res);
if (!m_a->eof() && res==size) {
QTimer::singleShot(0, this, SLOT(go_ready_read())); // emit readyRead();
}
if (m_a->eof()) {
QTimer::singleShot(0, this, SLOT(go_finished())); //emit finished();
}
return res;
}
bool
attachment_network_reply::atEnd() const
{
bool b=m_a->eof();
DBG_PRINTF(4, "atEnd returns %d", b);
return m_a->eof();
}
/* The base QIODevice::isSequential() returns false. */
bool
attachment_network_reply::isSequential() const
{
return true;
}
void
attachment_network_reply::abort()
{
DBG_PRINTF(4, "abort");
m_a->close();
}
qint64
attachment_network_reply::bytesAvailable() const
{
qint64 ret=(!m_a->eof()?32768:0) + QNetworkReply::bytesAvailable();
DBG_PRINTF(3, "bytesAvailable returning %lld", ret);
return ret;
}
void attachment_network_reply::go()
{
setAttribute(QNetworkRequest::HttpStatusCodeAttribute, 200);
setAttribute(QNetworkRequest::HttpReasonPhraseAttribute, "OK");
emit metaDataChanged();
NetworkError err = error();
if (err != NoError) {
/* We no longer emit error(err) here otherwise the load of the document
containing the attachment apparently never finishes.
This happens in particular for empty attachments */
DBG_PRINTF(4, "emit finished");
emit finished();
}
else {
DBG_PRINTF(4, "emit readyRead");
emit readyRead();
}
}
void
attachment_network_reply::go_finished()
{
DBG_PRINTF(4, "go_finished");
emit finished();
}
void
attachment_network_reply::go_ready_read()
{
DBG_PRINTF(4, "go_ready_read");
emit readyRead();
}
HTML source code generated by GNU Source-Highlight plus some custom post-processing
List of all available source files