diff options
Diffstat (limited to 'plugins/kmail/bodypartformatter/text_calendar.cpp')
-rw-r--r-- | plugins/kmail/bodypartformatter/text_calendar.cpp | 715 |
1 files changed, 715 insertions, 0 deletions
diff --git a/plugins/kmail/bodypartformatter/text_calendar.cpp b/plugins/kmail/bodypartformatter/text_calendar.cpp new file mode 100644 index 000000000..ba9b16eb0 --- /dev/null +++ b/plugins/kmail/bodypartformatter/text_calendar.cpp @@ -0,0 +1,715 @@ +/* + This file is part of kdepim. + + Copyright (c) 2004 Cornelius Schumacher <schumacher@kde.org> + Copyright (c) 2007 Volker Krause <vkrause@kde.org> + + This program is free software; you can redistribute it and/or modify it + under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + + In addition, as a special exception, the copyright holders give + permission to link the code of this program with any edition of + the Qt library by Trolltech AS, Norway (or with modified versions + of Qt that use the same license as Qt), and distribute linked + combinations including the two. You must obey the GNU General + Public License in all respects for all of the code used other than + Qt. If you modify this file, you may extend this exception to + your version of the file, but you are not obligated to do so. If + you do not wish to do so, delete this exception statement from + your version. +*/ + +#include "attendeeselector.h" +#include "delegateselector.h" + +#include <interfaces/bodypartformatter.h> +#include <interfaces/bodypart.h> +#include <interfaces/bodyparturlhandler.h> +#include <khtmlparthtmlwriter.h> + +#include <libkcal/calendarlocal.h> +#include <libkcal/calendarresources.h> +#include <libkcal/icalformat.h> +#include <libkcal/attendee.h> +#include <libkcal/incidence.h> +#include <libkcal/incidenceformatter.h> + +#include <kpimprefs.h> // for the timezone + +#include <kmail/callback.h> +#include <kmail/kmmessage.h> +#include <kmail/kmcommands.h> + +#include <email.h> + +#include <kglobal.h> +#include <kinputdialog.h> +#include <klocale.h> +#include <kstringhandler.h> +#include <kglobalsettings.h> +#include <kiconloader.h> +#include <kdebug.h> +#include <kdcopservicestarter.h> +#include <kmessagebox.h> +#include <kstandarddirs.h> +#include <kapplication.h> +#include <ktempfile.h> + +#include <qurl.h> +#include <qdir.h> +#include <qtextstream.h> + +#include <kdepimmacros.h> + +#include <dcopclient.h> +#include <dcopref.h> + +#include "kcalendariface_stub.h" + +using namespace KCal; + +namespace { + +class CalendarManager +{ + public: + CalendarManager(); + ~CalendarManager(); + static KCal::Calendar* calendar(); + + private: + KCal::CalendarResources* mCalendar; + static CalendarManager* mSelf; +}; + +static KStaticDeleter<CalendarManager> sCalendarDeleter; +CalendarManager* CalendarManager::mSelf = 0; + +CalendarManager::CalendarManager() +{ + mCalendar = new CalendarResources( KPimPrefs::timezone() ); + mCalendar->readConfig(); + mCalendar->load(); + bool multipleKolabResources = false; + CalendarResourceManager *mgr = mCalendar->resourceManager(); + for ( CalendarResourceManager::ActiveIterator it = mgr->activeBegin(); it != mgr->activeEnd(); ++it ) { + if ( (*it)->type() == "imap" || (*it)->type() == "kolab" ) { + const QStringList subResources = (*it)->subresources(); + QMap<QString, int> prefixSet; // KDE4: QSet + for ( QStringList::ConstIterator subIt = subResources.begin(); subIt != subResources.end(); ++subIt ) { + if ( !(*subIt).contains( "/.INBOX.directory/" ) ) + // we don't care about shared folders + continue; + prefixSet.insert( (*subIt).left( (*subIt).find( "/.INBOX.directory/" ) ), 0 ); + } + if ( prefixSet.count() > 1 ) + multipleKolabResources = true; + } + } + if ( multipleKolabResources ) { + kdDebug() << k_funcinfo << "disabling calendar lookup because multiple active Kolab resources" << endl; + delete mCalendar; + mCalendar = 0; + } +} + +CalendarManager::~CalendarManager() +{ + delete mCalendar; + mSelf = 0; +} + +KCal::Calendar* CalendarManager::calendar() +{ + if ( !mSelf ) { + sCalendarDeleter.setObject( mSelf, new CalendarManager() ); + } + return mSelf->mCalendar; +} + + +class KMInvitationFormatterHelper : public KCal::InvitationFormatterHelper +{ + public: + KMInvitationFormatterHelper( KMail::Interface::BodyPart *bodyPart ) : mBodyPart( bodyPart ) {} + virtual QString generateLinkURL( const QString &id ) { return mBodyPart->makeLink( id ); } + KCal::Calendar* calendar() const { return CalendarManager::calendar(); } + private: + KMail::Interface::BodyPart *mBodyPart; +}; + +class Formatter : public KMail::Interface::BodyPartFormatter +{ + public: + Result format( KMail::Interface::BodyPart *bodyPart, + KMail::HtmlWriter *writer ) const + { + if ( !writer ) + // Guard against crashes in createReply() + return Ok; + CalendarLocal cl( KPimPrefs::timezone() ); + KMInvitationFormatterHelper helper( bodyPart ); + QString source; + /* If the bodypart does not have a charset specified, we need to fall back to + utf8, not the KMail fallback encoding, so get the contents as binary and decode + explicitely. */ + if ( bodyPart->contentTypeParameter( "charset").isEmpty() ) { + const QByteArray &ba = bodyPart->asBinary(); + source = QString::fromUtf8(ba); + } else { + source = bodyPart->asText(); + } + QString html = IncidenceFormatter::formatICalInvitation( source, &cl, &helper ); + + if ( html.isEmpty() ) return AsIcon; + writer->queue( html ); + + return Ok; + } +}; + +static QString directoryForStatus( Attendee::PartStat status ) +{ + QString dir; + switch ( status ) { + case Attendee::Accepted: + dir = "accepted"; + break; + case Attendee::Tentative: + dir = "tentative"; + break; + case Attendee::Declined: + dir = "cancel"; + break; + case Attendee::Delegated: + dir = "delegated"; + break; + default: + break; + } + return dir; +} + +class UrlHandler : public KMail::Interface::BodyPartURLHandler +{ + public: + UrlHandler() + { + kdDebug() << "UrlHandler() (iCalendar)" << endl; + } + + Incidence* icalToString( const QString& iCal ) const + { + CalendarLocal calendar( KPimPrefs::timezone() ) ; + ICalFormat format; + ScheduleMessage *message = + format.parseScheduleMessage( &calendar, iCal ); + if ( !message ) + //TODO: Error message? + return 0; + return dynamic_cast<Incidence*>( message->event() ); + } + + + Attendee *findMyself( Incidence* incidence, const QString& receiver ) const + { + Attendee::List attendees = incidence->attendees(); + Attendee::List::ConstIterator it; + Attendee* myself = 0; + // Find myself. There will always be all attendees listed, even if + // only I need to answer it. + for ( it = attendees.begin(); it != attendees.end(); ++it ) { + // match only the email part, not the name + if( KPIM::compareEmail( (*it)->email(), receiver, false ) ) { + // We are the current one, and even the receiver, note + // this and quit searching. + myself = (*it); + break; + } + } + return myself; + } + + static bool heuristicalRSVP( Incidence *incidence ) + { + bool rsvp = true; // better send superfluously than not at all + Attendee::List attendees = incidence->attendees(); + Attendee::List::ConstIterator it; + for ( it = attendees.begin(); it != attendees.end(); ++it ) { + if ( it == attendees.begin() ) { + rsvp = (*it)->RSVP(); // use what the first one has + } else { + if ( (*it)->RSVP() != rsvp ) { + rsvp = true; // they differ, default + break; + } + } + } + return rsvp; + } + + static Attendee::Role heuristicalRole( Incidence *incidence ) + { + Attendee::Role role = Attendee::OptParticipant; + Attendee::List attendees = incidence->attendees(); + Attendee::List::ConstIterator it; + for ( it = attendees.begin(); it != attendees.end(); ++it ) { + if ( it == attendees.begin() ) { + role = (*it)->role(); // use what the first one has + } else { + if ( (*it)->role() != role ) { + role = Attendee::OptParticipant; // they differ, default + break; + } + } + } + return role; + + } + + Attendee* setStatusOnMyself( Incidence* incidence, Attendee* myself, + Attendee::PartStat status, const QString &receiver ) const + { + Attendee* newMyself = 0; + QString name; + QString email; + KPIM::getNameAndMail( receiver, name, email ); + if ( name.isEmpty() && myself ) name = myself->name(); + if ( email.isEmpty()&& myself ) email = myself->email(); + Q_ASSERT( !email.isEmpty() ); // delivery must be possible + newMyself = new Attendee( name, + email, + true, // RSVP, otherwise we would not be here + status, + myself ? myself->role() : heuristicalRole( incidence ), + myself ? myself->uid() : QString::null ); + if ( myself ) { + newMyself->setDelegate( myself->delegate() ); + newMyself->setDelegator( myself->delegator() ); + } + + // Make sure only ourselves is in the event + incidence->clearAttendees(); + if( newMyself ) + incidence->addAttendee( newMyself ); + return newMyself; + } + + enum MailType { + Answer, + Delegation, + Forward, + DeclineCounter + }; + + bool mail( Incidence* incidence, KMail::Callback& callback, + Attendee::PartStat status, + Scheduler::Method method = Scheduler::Reply, + const QString &to = QString::null, MailType type = Answer ) const + { + ICalFormat format; + format.setTimeZone( KPimPrefs::timezone(), false ); + QString msg = format.createScheduleMessage( incidence, method ); + QString summary = incidence->summary(); + if ( summary.isEmpty() ) + summary = i18n( "Incidence with no summary" ); + QString subject; + switch ( type ) { + case Answer: + subject = i18n( "Answer: %1" ).arg( summary ); + break; + case Delegation: + subject = i18n( "Delegated: %1" ).arg( summary ); + break; + case Forward: + subject = i18n( "Forwarded: %1" ).arg( summary ); + break; + case DeclineCounter: + // ### string freeze + //subject = i18n( "Declined Counter Proposal: %1" ).arg( summary ); + subject = i18n( "Answer: %1" ).arg( summary ); + break; + } + + QString recv = to; + if ( recv.isEmpty() ) + recv = incidence->organizer().fullName(); + QString statusString = directoryForStatus( status ); //it happens to return the right strings + return callback.mailICal( recv, msg, subject, statusString, type != Forward ); + } + + void ensureKorganizerRunning() const + { + QString error; + QCString dcopService; + int result = KDCOPServiceStarter::self()->findServiceFor( "DCOP/Organizer", QString::null, QString::null, &error, &dcopService ); + if ( result == 0 ) { + // OK, so korganizer (or kontact) is running. Now ensure the object we want is available + // [that's not the case when kontact was already running, but korganizer not loaded into it...] + static const char* const dcopObjectId = "KOrganizerIface"; + QCString dummy; + if ( !kapp->dcopClient()->findObject( dcopService, dcopObjectId, "", QByteArray(), dummy, dummy ) ) { + DCOPRef ref( dcopService, dcopService ); // talk to the KUniqueApplication or its kontact wrapper + DCOPReply reply = ref.call( "load()" ); + if ( reply.isValid() && (bool)reply ) { + kdDebug() << "Loaded " << dcopService << " successfully" << endl; + Q_ASSERT( kapp->dcopClient()->findObject( dcopService, dcopObjectId, "", QByteArray(), dummy, dummy ) ); + } else + kdWarning() << "Error loading " << dcopService << endl; + } + + // We don't do anything with it, we just need it to be running so that it handles + // the incoming directory. + } + else + kdWarning() << "Couldn't start DCOP/Organizer: " << dcopService << " " << error << endl; + } + + bool saveFile( const QString& receiver, const QString& iCal, + const QString& type ) const + { + KTempFile file( locateLocal( "data", "korganizer/income." + type + '/', + true ) ); + QTextStream* ts = file.textStream(); + if ( !ts ) { + KMessageBox::error( 0, i18n("Could not save file to KOrganizer") ); + return false; + } + ts->setEncoding( QTextStream::UnicodeUTF8 ); + (*ts) << receiver << '\n' << iCal; + file.close(); + + // Now ensure that korganizer is running; otherwise start it, to prevent surprises + // (https://intevation.de/roundup/kolab/issue758) + ensureKorganizerRunning(); + + return true; + } + + bool handleInvitation( const QString& iCal, Attendee::PartStat status, + KMail::Callback &callback ) const + { + bool ok = true; + const QString receiver = callback.receiver(); + + if ( receiver.isEmpty() ) + // Must be some error. Still return true though, since we did handle it + return true; + + // get comment for tentative acceptance + Incidence* incidence = icalToString( iCal ); + + if ( callback.askForComment( status ) ) { + bool ok = false; + QString comment = KInputDialog::getMultiLineText( i18n("Reaction to Invitation"), + i18n("Comment:"), QString(), &ok ); + if ( !ok ) + return true; + if ( !comment.isEmpty() ) + incidence->addComment( comment ); + } + + // First, save it for KOrganizer to handle + QString dir = directoryForStatus( status ); + if ( dir.isEmpty() ) + return true; // unknown status + if ( status != Attendee::Delegated ) // we do that below for delegated incidences + saveFile( receiver, iCal, dir ); + + QString delegateString; + bool delegatorRSVP = false; + if ( status == Attendee::Delegated ) { + DelegateSelector dlg; + if ( dlg.exec() == QDialog::Rejected ) + return true; + delegateString = dlg.delegate(); + delegatorRSVP = dlg.rsvp(); + if ( delegateString.isEmpty() ) + return true; + if ( KPIM::compareEmail( delegateString, incidence->organizer().email(), false ) ) { + KMessageBox::sorry( 0, i18n("Delegation to organizer is not possible.") ); + return true; + } + } + + if( !incidence ) return false; + Attendee *myself = findMyself( incidence, receiver ); + + // find our delegator, we need to inform him as well + QString delegator; + if ( myself && !myself->delegator().isEmpty() ) { + Attendee::List attendees = incidence->attendees(); + for ( Attendee::List::ConstIterator it = attendees.constBegin(); it != attendees.constEnd(); ++it ) { + if( KPIM::compareEmail( (*it)->fullName(), myself->delegator(), false ) && (*it)->status() == Attendee::Delegated ) { + delegator = (*it)->fullName(); + delegatorRSVP = (*it)->RSVP(); + break; + } + } + } + + if ( ( myself && myself->RSVP() ) || heuristicalRSVP( incidence ) ) { + Attendee* newMyself = setStatusOnMyself( incidence, myself, status, receiver ); + if ( newMyself && status == Attendee::Delegated ) { + newMyself->setDelegate( delegateString ); + newMyself->setRSVP( delegatorRSVP ); + } + ok = mail( incidence, callback, status ); + + // check if we need to inform our delegator about this as well + if ( newMyself && (status == Attendee::Accepted || status == Attendee::Declined) + && !delegator.isEmpty() ) { + if ( delegatorRSVP || status == Attendee::Declined ) + ok = mail( incidence, callback, status, Scheduler::Reply, delegator ); + } + + } else if ( !myself && (status != Attendee::Declined) ) { + // forwarded invitation + Attendee* newMyself = 0; + QString name; + QString email; + KPIM::getNameAndMail( receiver, name, email ); + if ( !email.isEmpty() ) { + newMyself = new Attendee( name, + email, + true, // RSVP, otherwise we would not be here + status, + heuristicalRole( incidence ), + QString::null ); + incidence->clearAttendees(); + incidence->addAttendee( newMyself ); + ok = mail( incidence, callback, status, Scheduler::Reply ); + } + } else { + if ( callback.deleteInvitationAfterReply() ) + ( new KMDeleteMsgCommand( callback.getMsg()->getMsgSerNum() ) )->start(); + } + delete incidence; + + // create invitation for the delegate (same as the original invitation + // with the delegate as additional attendee), we also use that for updating + // our calendar + if ( status == Attendee::Delegated ) { + incidence = icalToString( iCal ); + myself = findMyself( incidence, receiver ); + myself->setStatus( status ); + myself->setDelegate( delegateString ); + QString name, email; + KPIM::getNameAndMail( delegateString, name, email ); + Attendee *delegate = new Attendee( name, email, true ); + delegate->setDelegator( receiver ); + incidence->addAttendee( delegate ); + + ICalFormat format; + format.setTimeZone( KPimPrefs::timezone(), false ); + QString iCal = format.createScheduleMessage( incidence, Scheduler::Request ); + saveFile( receiver, iCal, dir ); + + ok = mail( incidence, callback, status, Scheduler::Request, delegateString, Delegation ); + } + return ok; + } + + void showCalendar( const QDate &date ) const + { + ensureKorganizerRunning(); + // raise korganizer part in kontact or the korganizer app + kapp->dcopClient()->send( "korganizer", "korganizer", "newInstance()", QByteArray() ); + QByteArray arg; + QDataStream s( arg, IO_WriteOnly ); + s << QString( "kontact_korganizerplugin" ); + kapp->dcopClient()->send( "kontact", "KontactIface", "selectPlugin(QString)", arg ); + + KCalendarIface_stub *iface = new KCalendarIface_stub( kapp->dcopClient(), "korganizer", "CalendarIface" ); + iface->showEventView(); + iface->showDate( date ); + delete iface; + } + + bool handleIgnore( const QString&, KMail::Callback& c ) const + { + // simply move the message to trash + ( new KMDeleteMsgCommand( c.getMsg()->getMsgSerNum() ) )->start(); + return true; + } + + bool handleDeclineCounter( const QString &iCal, KMail::Callback &callback ) const + { + const QString receiver = callback.receiver(); + if ( receiver.isEmpty() ) + return true; + Incidence* incidence = icalToString( iCal ); + if ( callback.askForComment( Attendee::Declined ) ) { + bool ok = false; + // ### string freeze + QString comment = KInputDialog::getMultiLineText( i18n("Reaction to Invitation") /* i18n("Decline Counter Proposal") */, + i18n("Comment:"), QString(), &ok ); + if ( !ok ) + return true; + if ( !comment.isEmpty() ) + incidence->addComment( comment ); + } + return mail( incidence, callback, Attendee::NeedsAction, Scheduler::Declinecounter, + callback.sender(), DeclineCounter ); + } + + bool counterProposal( const QString &iCal, KMail::Callback &callback ) const + { + const QString receiver = callback.receiver(); + if ( receiver.isEmpty() ) + return true; + saveFile( receiver, iCal, "counter" ); + // Don't delete the invitation here in any case, if the counter proposal + // is declined you might need it again. + return true; + } + + bool handleClick( KMail::Interface::BodyPart *part, + const QString &path, KMail::Callback& c ) const + { + QString iCal; + /* If the bodypart does not have a charset specified, we need to fall back to + utf8, not the KMail fallback encoding, so get the contents as binary and decode + explicitely. */ + if ( part->contentTypeParameter( "charset").isEmpty() ) { + const QByteArray &ba = part->asBinary(); + iCal = QString::fromUtf8(ba); + } else { + iCal = part->asText(); + } + bool result = false; + if ( path == "accept" ) + result = handleInvitation( iCal, Attendee::Accepted, c ); + if ( path == "accept_conditionally" ) + result = handleInvitation( iCal, Attendee::Tentative, c ); + if ( path == "counter" ) + result = counterProposal( iCal, c ); + if ( path == "ignore" ) + result = handleIgnore( iCal, c ); + if ( path == "decline" ) + result = handleInvitation( iCal, Attendee::Declined, c ); + if ( path == "decline_counter" ) { + result = handleDeclineCounter( iCal, c ); + } + if ( path == "delegate" ) + result = handleInvitation( iCal, Attendee::Delegated, c ); + if ( path == "forward" ) { + Incidence* incidence = icalToString( iCal ); + AttendeeSelector dlg; + if ( dlg.exec() == QDialog::Rejected ) + return true; + QString fwdTo = dlg.attendees().join( ", " ); + if ( fwdTo.isEmpty() ) + return true; + result = mail( incidence, c, Attendee::Delegated /*### right ?*/, + Scheduler::Request, fwdTo, Forward ); + } + if ( path == "check_calendar" ) { + Incidence* incidence = icalToString( iCal ); + showCalendar( incidence->dtStart().date() ); + } + if ( path == "reply" || path == "cancel" || path == "accept_counter" ) { + // These should just be saved with their type as the dir + const QString p = (path == "accept_counter" ? QString("reply") : path); + if ( saveFile( "Receiver Not Searched", iCal, p ) ) { + if ( c.deleteInvitationAfterReply() ) + ( new KMDeleteMsgCommand( c.getMsg()->getMsgSerNum() ) )->start(); + result = true; + } + } + if ( result ) + c.closeIfSecondaryWindow(); + return result; + } + + bool handleContextMenuRequest( KMail::Interface::BodyPart *, + const QString &, + const QPoint & ) const + { + return false; + } + + QString statusBarMessage( KMail::Interface::BodyPart *, + const QString &path ) const + { + if ( !path.isEmpty() ) { + if ( path == "accept" ) + return i18n("Accept incidence"); + if ( path == "accept_conditionally" ) + return i18n( "Accept incidence conditionally" ); +// ### string freeze +// if ( path == "accept_counter" ) +// return i18n( "Accept counter proposal" ); + if ( path == "counter" ) + return i18n( "Create a counter proposal..." ); + if ( path == "ignore" ) + return i18n( "Throw mail away" ); + if ( path == "decline" ) + return i18n( "Decline incidence" ); +// ### string freeze +// if ( path == "decline_counter" ) +// return i18n( "Decline counter proposal" ); + if ( path == "check_calendar" ) + return i18n("Check my calendar..." ); + if ( path == "reply" ) + return i18n( "Enter incidence into my calendar" ); + if ( path == "delegate" ) + return i18n( "Delegate incidence" ); + if ( path == "forward" ) + return i18n( "Forward incidence" ); + if ( path == "cancel" ) + return i18n( "Remove incidence from my calendar" ); + } + + return QString::null; + } +}; + +class Plugin : public KMail::Interface::BodyPartFormatterPlugin +{ + public: + const KMail::Interface::BodyPartFormatter *bodyPartFormatter( int idx ) const + { + if ( idx == 0 ) return new Formatter(); + else return 0; + } + + const char *type( int idx ) const + { + if ( idx == 0 ) return "text"; + else return 0; + } + + const char *subtype( int idx ) const + { + if ( idx == 0 ) return "calendar"; + else return 0; + } + + const KMail::Interface::BodyPartURLHandler * urlHandler( int idx ) const + { + if ( idx == 0 ) return new UrlHandler(); + else return 0; + } +}; + +} + +extern "C" +KDE_EXPORT KMail::Interface::BodyPartFormatterPlugin * +libkmail_bodypartformatter_text_calendar_create_bodypart_formatter_plugin() +{ + KGlobal::locale()->insertCatalogue( "kmail_text_calendar_plugin" ); + return new Plugin(); +} |