korganizer

kogroupware.cpp

00001 /*
00002   This file is part of the Groupware/KOrganizer integration.
00003 
00004   Requires the Qt and KDE widget libraries, available at no cost at
00005   http://www.trolltech.com and http://www.kde.org respectively
00006 
00007   Copyright (c) 2002-2004 Klarälvdalens Datakonsult AB
00008         <info@klaralvdalens-datakonsult.se>
00009 
00010   This program is free software; you can redistribute it and/or modify
00011   it under the terms of the GNU General Public License as published by
00012   the Free Software Foundation; either version 2 of the License, or
00013   (at your option) any later version.
00014 
00015   This program is distributed in the hope that it will be useful,
00016   but WITHOUT ANY WARRANTY; without even the implied warranty of
00017   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
00018   GNU General Public License for more details.
00019 
00020   You should have received a copy of the GNU General Public License
00021   along with this program; if not, write to the Free Software
00022   Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
00023   MA  02110-1301, USA.
00024 
00025   In addition, as a special exception, the copyright holders give
00026   permission to link the code of this program with any edition of
00027   the Qt library by Trolltech AS, Norway (or with modified versions
00028   of Qt that use the same license as Qt), and distribute linked
00029   combinations including the two.  You must obey the GNU General
00030   Public License in all respects for all of the code used other than
00031   Qt.  If you modify this file, you may extend this exception to
00032   your version of the file, but you are not obligated to do so.  If
00033   you do not wish to do so, delete this exception statement from
00034   your version.
00035 */
00036 
00037 #include "kogroupware.h"
00038 #include "freebusymanager.h"
00039 #include "calendarview.h"
00040 #include "mailscheduler.h"
00041 #include "koprefs.h"
00042 #include "koincidenceeditor.h"
00043 #include <libemailfunctions/email.h>
00044 #include <libkcal/attendee.h>
00045 #include <libkcal/journal.h>
00046 #include <libkcal/incidenceformatter.h>
00047 #include <kdebug.h>
00048 #include <kmessagebox.h>
00049 #include <kstandarddirs.h>
00050 #include <kdirwatch.h>
00051 #include <qfile.h>
00052 #include <qregexp.h>
00053 #include <qdir.h>
00054 #include <qtimer.h>
00055 
00056 FreeBusyManager *KOGroupware::mFreeBusyManager = 0;
00057 
00058 KOGroupware *KOGroupware::mInstance = 0;
00059 
00060 KOGroupware *KOGroupware::create( CalendarView *view,
00061                                   KCal::CalendarResources *calendar )
00062 {
00063   if( !mInstance )
00064     mInstance = new KOGroupware( view, calendar );
00065   return mInstance;
00066 }
00067 
00068 KOGroupware *KOGroupware::instance()
00069 {
00070   // Doesn't create, that is the task of create()
00071   Q_ASSERT( mInstance );
00072   return mInstance;
00073 }
00074 
00075 
00076 KOGroupware::KOGroupware( CalendarView* view, KCal::CalendarResources* cal )
00077    : QObject( 0, "kmgroupware_instance" ), mView( view ), mCalendar( cal ), mDoNotNotify( false )
00078 {
00079   // Set up the dir watch of the three incoming dirs
00080   KDirWatch* watcher = KDirWatch::self();
00081   watcher->addDir( locateLocal( "data", "korganizer/income.accepted/" ) );
00082   watcher->addDir( locateLocal( "data", "korganizer/income.tentative/" ) );
00083   watcher->addDir( locateLocal( "data", "korganizer/income.counter/" ) );
00084   watcher->addDir( locateLocal( "data", "korganizer/income.cancel/" ) );
00085   watcher->addDir( locateLocal( "data", "korganizer/income.reply/" ) );
00086   watcher->addDir( locateLocal( "data", "korganizer/income.delegated/" ) );
00087   connect( watcher, SIGNAL( dirty( const QString& ) ),
00088            this, SLOT( incomingDirChanged( const QString& ) ) );
00089   // Now set the ball rolling
00090   QTimer::singleShot( 0, this, SLOT(initialCheckForChanges()) );
00091 
00092   // Initialize
00093   lastUsedDialogAnswer = KMessageBox::Yes;
00094 }
00095 
00096 void KOGroupware::initialCheckForChanges()
00097 {
00098   incomingDirChanged( locateLocal( "data", "korganizer/income.accepted/" ) );
00099   incomingDirChanged( locateLocal( "data", "korganizer/income.tentative/" ) );
00100   incomingDirChanged( locateLocal( "data", "korganizer/income.counter/" ) );
00101   incomingDirChanged( locateLocal( "data", "korganizer/income.cancel/" ) );
00102   incomingDirChanged( locateLocal( "data", "korganizer/income.reply/" ) );
00103   incomingDirChanged( locateLocal( "data", "korganizer/income.delegated/" ) );
00104 }
00105 
00106 void KOGroupware::slotViewNewIncidenceChanger( IncidenceChangerBase* changer )
00107 {
00108     // Call slot perhapsUploadFB if an incidence was added, changed or removed
00109     connect( changer, SIGNAL( incidenceAdded( Incidence* ) ),
00110              mFreeBusyManager, SLOT( slotPerhapsUploadFB() ) );
00111     connect( changer, SIGNAL( incidenceChanged( Incidence*, Incidence*, KOGlobals::WhatChanged ) ),
00112              mFreeBusyManager, SLOT( slotPerhapsUploadFB() ) );
00113     connect( changer, SIGNAL( incidenceDeleted( Incidence * ) ),
00114              mFreeBusyManager, SLOT( slotPerhapsUploadFB() ) );
00115 }
00116 
00117 FreeBusyManager *KOGroupware::freeBusyManager()
00118 {
00119   if ( !mFreeBusyManager ) {
00120     mFreeBusyManager = new FreeBusyManager( this, "freebusymanager" );
00121     mFreeBusyManager->setCalendar( mCalendar );
00122     connect( mCalendar, SIGNAL( calendarChanged() ),
00123              mFreeBusyManager, SLOT( slotPerhapsUploadFB() ) );
00124     connect( mView, SIGNAL( newIncidenceChanger( IncidenceChangerBase* ) ),
00125              this, SLOT( slotViewNewIncidenceChanger( IncidenceChangerBase* ) ) );
00126     slotViewNewIncidenceChanger( mView->incidenceChanger() );
00127   }
00128 
00129   return mFreeBusyManager;
00130 }
00131 
00132 void KOGroupware::incomingDirChanged( const QString& path )
00133 {
00134   const QString incomingDirName = locateLocal( "data","korganizer/" )
00135                                   + "income.";
00136   if ( !path.startsWith( incomingDirName ) ) {
00137     kdDebug(5850) << "incomingDirChanged: Wrong dir " << path << endl;
00138     return;
00139   }
00140   QString action = path.mid( incomingDirName.length() );
00141   while ( action.length() > 0 && action[ action.length()-1 ] == '/' )
00142     // Strip slashes at the end
00143     action.truncate( action.length()-1 );
00144 
00145   // Handle accepted invitations
00146   QDir dir( path );
00147   const QStringList files = dir.entryList( QDir::Files );
00148   if ( files.isEmpty() )
00149     // No more files here
00150     return;
00151 
00152   // Read the file and remove it
00153   QFile f( path + "/" + files[0] );
00154   if (!f.open(IO_ReadOnly)) {
00155     kdError(5850) << "Can't open file '" << files[0] << "'" << endl;
00156     return;
00157   }
00158   QTextStream t(&f);
00159   t.setEncoding( QTextStream::UnicodeUTF8 );
00160   QString receiver = KPIM::getFirstEmailAddress( t.readLine() );
00161   QString iCal = t.read();
00162 
00163   f.remove();
00164 
00165   ScheduleMessage *message = mFormat.parseScheduleMessage( mCalendar, iCal );
00166   if ( !message ) {
00167     QString errorMessage;
00168     if (mFormat.exception())
00169       errorMessage = i18n( "Error message: %1" ).arg( mFormat.exception()->message() );
00170     kdDebug(5850) << "MailScheduler::retrieveTransactions() Error parsing "
00171                   << errorMessage << endl;
00172     KMessageBox::detailedError( mView,
00173         i18n("Error while processing an invitation or update."),
00174         errorMessage );
00175     return;
00176   }
00177 
00178   KCal::Scheduler::Method method =
00179     static_cast<KCal::Scheduler::Method>( message->method() );
00180   KCal::ScheduleMessage::Status status = message->status();
00181   KCal::Incidence* incidence =
00182     dynamic_cast<KCal::Incidence*>( message->event() );
00183   if(!incidence) {
00184     delete message;
00185     return;
00186   }
00187   KCal::MailScheduler scheduler( mCalendar );
00188   if ( action.startsWith( "accepted" ) || action.startsWith( "tentative" )
00189        || action.startsWith( "delegated" ) || action.startsWith( "counter" ) ) {
00190     // Find myself and set my status. This can't be done in the scheduler,
00191     // since this does not know the choice I made in the KMail bpf
00192     KCal::Attendee::List attendees = incidence->attendees();
00193     KCal::Attendee::List::ConstIterator it;
00194     for ( it = attendees.begin(); it != attendees.end(); ++it ) {
00195       if( (*it)->email() == receiver ) {
00196         if ( action.startsWith( "accepted" ) )
00197           (*it)->setStatus( KCal::Attendee::Accepted );
00198         else if ( action.startsWith( "tentative" ) )
00199           (*it)->setStatus( KCal::Attendee::Tentative );
00200         else if ( KOPrefs::instance()->outlookCompatCounterProposals() && action.startsWith( "counter" ) )
00201           (*it)->setStatus( KCal::Attendee::Tentative );
00202         else if ( action.startsWith( "delegated" ) )
00203           (*it)->setStatus( KCal::Attendee::Delegated );
00204         break;
00205       }
00206     }
00207     if ( KOPrefs::instance()->outlookCompatCounterProposals() || !action.startsWith( "counter" ) )
00208       scheduler.acceptTransaction( incidence, method, status, receiver );
00209   } else if ( action.startsWith( "cancel" ) )
00210     // Delete the old incidence, if one is present
00211     scheduler.acceptTransaction( incidence, KCal::Scheduler::Cancel, status, receiver );
00212   else if ( action.startsWith( "reply" ) ) {
00213     if ( method != Scheduler::Counter ) {
00214       scheduler.acceptTransaction( incidence, method, status );
00215     } else {
00216       // accept counter proposal
00217       scheduler.acceptCounterProposal( incidence );
00218       // send update to all attendees
00219       sendICalMessage( mView, Scheduler::Request, incidence, KOGlobals::INCIDENCEEDITED, false );
00220     }
00221   } else
00222     kdError(5850) << "Unknown incoming action " << action << endl;
00223 
00224   if ( action.startsWith( "counter" ) ) {
00225     mView->editIncidence( incidence, QDate(), true );
00226     KOIncidenceEditor *tmp = mView->editorDialog( incidence );
00227     tmp->selectInvitationCounterProposal( true );
00228   }
00229   mView->updateView();
00230 }
00231 
00232 class KOInvitationFormatterHelper : public InvitationFormatterHelper
00233 {
00234   public:
00235     virtual QString generateLinkURL( const QString &id ) { return "kmail:groupware_request_" + id; }
00236 };
00237 
00238 /* This function sends mails if necessary, and makes sure the user really
00239  * want to change his calendar.
00240  *
00241  * Return true means accept the changes
00242  * Return false means revert the changes
00243  */
00244 bool KOGroupware::sendICalMessage( QWidget* parent,
00245                                    KCal::Scheduler::Method method,
00246                                    Incidence* incidence,
00247                                    KOGlobals::HowChanged action,
00248                                    bool attendeeStatusChanged,
00249                                    bool useLastDialogAnswer )
00250 {
00251   // If there are no attendees, don't bother
00252   if ( incidence->attendees().isEmpty() )
00253     return true;
00254 
00255   bool isOrganizer = KOPrefs::instance()->thatIsMe( incidence->organizer().email() );
00256   int rc = 0;
00257   /*
00258    * There are two scenarios:
00259    * o "we" are the organizer, where "we" means any of the identities or mail
00260    *   addresses known to Kontact/PIM. If there are attendees, we need to mail
00261    *   them all, even if one or more of them are also "us". Otherwise there
00262    *   would be no way to invite a resource or our boss, other identities we
00263    *   also manage.
00264    * o "we: are not the organizer, which means we changed the completion status
00265    *   of a todo, or we changed our attendee status from, say, tentative to
00266    *   accepted. In both cases we only mail the organizer. All other changes
00267    *   bring us out of sync with the organizer, so we won't mail, if the user
00268    *   insists on applying them.
00269    */
00270 
00271   if ( isOrganizer ) {
00272     /* We are the organizer. If there is more than one attendee, or if there is
00273      * only one, and it's not the same as the organizer, ask the user to send
00274      * mail. */
00275     if ( incidence->attendees().count() > 1
00276         || incidence->attendees().first()->email() != incidence->organizer().email() ) {
00277 
00278       QString txt;
00279       switch( action ) {
00280       case KOGlobals::INCIDENCEEDITED:
00281         txt = i18n( "You changed the invitation \"%1\".\n"
00282                     "Do you want to email the attendees an update message?" ).
00283               arg( incidence->summary() );
00284         break;
00285       case KOGlobals::INCIDENCEDELETED:
00286         Q_ASSERT( incidence->type() == "Event" || incidence->type() == "Todo" );
00287         if ( incidence->type() == "Event" ) {
00288           txt = i18n( "You removed the invitation \"%1\".\n"
00289                       "Do you want to email the attendees that the event is canceled?" ).
00290                 arg( incidence->summary() );
00291         } else if ( incidence->type() == "Todo" ) {
00292           txt = i18n( "You removed the invitation \"%1\".\n"
00293                       "Do you want to email the attendees that the todo is canceled?" ).
00294                 arg( incidence->summary() );
00295         }
00296         break;
00297       case KOGlobals::INCIDENCEADDED:
00298         if ( incidence->type() == "Event" ) {
00299           txt = i18n( "The event \"%1\" includes other people.\n"
00300                       "Do you want to email the invitation to the attendees?" ).
00301                 arg( incidence->summary() );
00302         } else if ( incidence->type() == "Todo" ) {
00303           txt = i18n( "The todo \"%1\" includes other people.\n"
00304                       "Do you want to email the invitation to the attendees?" ).
00305                 arg( incidence->summary() );
00306         } else {
00307           txt = i18n( "This incidence includes other people. "
00308                       "Should an email be sent to the attendees?" );
00309         }
00310         break;
00311       default:
00312         kdError() << "Unsupported HowChanged action" << int( action ) << endl;
00313         break;
00314       }
00315 
00316       if ( useLastDialogAnswer ) {
00317         rc = lastUsedDialogAnswer;
00318       } else {
00319         lastUsedDialogAnswer = rc = KMessageBox::questionYesNo(
00320           parent, txt, i18n( "Group Scheduling Email" ),
00321           KGuiItem( i18n( "Send Email" ) ), KGuiItem( i18n( "Do Not Send" ) ) );
00322       }
00323     } else {
00324       return true;
00325     }
00326   } else if( incidence->type() == "Todo" ) {
00327     if( method == Scheduler::Request )
00328       // This is an update to be sent to the organizer
00329       method = Scheduler::Reply;
00330 
00331     if ( useLastDialogAnswer ) {
00332       rc = lastUsedDialogAnswer;
00333     } else {
00334       // Ask if the user wants to tell the organizer about the current status
00335       const QString txt = i18n( "Do you want to send a status update to the "
00336                           "organizer of this task?");
00337       lastUsedDialogAnswer = rc = KMessageBox::questionYesNo( parent, txt, QString::null, i18n("Send Update"), i18n("Do Not Send") );
00338     }
00339   } else if ( incidence->type() == "Event" ) {
00340     QString txt;
00341     if ( attendeeStatusChanged && method == Scheduler::Request ) {
00342       txt = i18n( "Your status as an attendee of this event changed. "
00343                   "Do you want to send a status update to the event organizer?" );
00344       method = Scheduler::Reply;
00345       if ( useLastDialogAnswer ) {
00346         rc = lastUsedDialogAnswer;
00347       } else {
00348         lastUsedDialogAnswer = rc = KMessageBox::questionYesNo( parent, txt, QString::null, i18n("Send Update"), i18n("Do Not Send") );
00349       }
00350     } else {
00351       if( action == KOGlobals::INCIDENCEDELETED ) {
00352         const QStringList myEmails = KOPrefs::instance()->allEmails();
00353         bool askConfirmation = false;
00354         for ( QStringList::ConstIterator it = myEmails.begin(); it != myEmails.end(); ++it ) {
00355           QString email = *it;
00356           Attendee *me = incidence->attendeeByMail(email);
00357           if (me && (me->status()==KCal::Attendee::Accepted || me->status()==KCal::Attendee::Delegated)) {
00358             askConfirmation = true;
00359             break;
00360           }
00361         }
00362 
00363         if ( !askConfirmation ) {
00364           return true;
00365         }
00366 
00367         txt = i18n( "You had previously accepted an invitation to this event. "
00368                     "Do you want to send an updated response to the organizer "
00369                     "declining the invitation?" );
00370         if ( useLastDialogAnswer ) {
00371           rc = lastUsedDialogAnswer;
00372         } else {
00373           lastUsedDialogAnswer = rc = KMessageBox::questionYesNo(
00374             parent, txt, i18n( "Group Scheduling Email" ),
00375             KGuiItem( i18n( "Send Update" ) ), KGuiItem( i18n( "Do Not Send" ) ) );
00376           setDoNotNotify( rc == KMessageBox::No );
00377         }
00378       } else {
00379         if ( useLastDialogAnswer ) {
00380           rc = lastUsedDialogAnswer;
00381         } else {
00382           txt = i18n( "You are not the organizer of this event. Editing it will "
00383                     "bring your calendar out of sync with the organizer's calendar. "
00384                     "Do you really want to edit it?" );
00385           lastUsedDialogAnswer = rc = KMessageBox::warningYesNo( parent, txt );
00386         }
00387         return ( rc == KMessageBox::Yes );
00388       }
00389     }
00390   } else {
00391     kdWarning(5850) << "Groupware messages for Journals are not implemented yet!" << endl;
00392     return true;
00393   }
00394 
00395   if ( rc == KMessageBox::Yes ) {
00396     // We will be sending out a message here. Now make sure there is
00397     // some summary
00398     if( incidence->summary().isEmpty() )
00399       incidence->setSummary( i18n("<No summary given>") );
00400 
00401     // Send the mail
00402     KCal::MailScheduler scheduler( mCalendar );
00403     scheduler.performTransaction( incidence, method );
00404 
00405     return true;
00406   } else if ( rc == KMessageBox::No ) {
00407     return true;
00408   } else {
00409     return false;
00410   }
00411 }
00412 
00413 void KOGroupware::sendCounterProposal(KCal::Calendar *calendar, KCal::Event * oldEvent, KCal::Event * newEvent) const
00414 {
00415   if ( !oldEvent || !newEvent || *oldEvent == *newEvent || !KOPrefs::instance()->mUseGroupwareCommunication )
00416     return;
00417   if ( KOPrefs::instance()->outlookCompatCounterProposals() ) {
00418     Incidence* tmp = oldEvent->clone();
00419     tmp->setSummary( i18n("Counter proposal: %1").arg( newEvent->summary() ) );
00420     tmp->setDescription( newEvent->description() );
00421     tmp->addComment( i18n("Proposed new meeting time: %1 - %2").
00422                      arg( IncidenceFormatter::dateToString( newEvent->dtStart() ),
00423                           IncidenceFormatter::dateToString( newEvent->dtEnd() ) ) );
00424     KCal::MailScheduler scheduler( calendar );
00425     scheduler.performTransaction( tmp, Scheduler::Reply );
00426     delete tmp;
00427   } else {
00428     KCal::MailScheduler scheduler( calendar );
00429     scheduler.performTransaction( newEvent, Scheduler::Counter );
00430   }
00431 }
00432 
00433 #include "kogroupware.moc"
KDE Home | KDE Accessibility Home | Description of Access Keys