korganizer

kogroupware.cpp
00001 /*
00002   This file is part of the Groupware/KOrganizer integration.
00003 
00004   Requires the TQt 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 TQt library by Trolltech AS, Norway (or with modified versions
00028   of TQt that use the same license as TQt), 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   TQt.  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 <tqfile.h>
00052 #include <tqregexp.h>
00053 #include <tqdir.h>
00054 #include <tqtimer.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    : TQObject( 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, TQT_SIGNAL( dirty( const TQString& ) ),
00088            this, TQT_SLOT( incomingDirChanged( const TQString& ) ) );
00089   // Now set the ball rolling
00090   TQTimer::singleShot( 0, this, TQT_SLOT(initialCheckForChanges()) );
00091 }
00092 
00093 void KOGroupware::initialCheckForChanges()
00094 {
00095   incomingDirChanged( locateLocal( "data", "korganizer/income.accepted/" ) );
00096   incomingDirChanged( locateLocal( "data", "korganizer/income.tentative/" ) );
00097   incomingDirChanged( locateLocal( "data", "korganizer/income.counter/" ) );
00098   incomingDirChanged( locateLocal( "data", "korganizer/income.cancel/" ) );
00099   incomingDirChanged( locateLocal( "data", "korganizer/income.reply/" ) );
00100   incomingDirChanged( locateLocal( "data", "korganizer/income.delegated/" ) );
00101 }
00102 
00103 void KOGroupware::slotViewNewIncidenceChanger( IncidenceChangerBase* changer )
00104 {
00105     // Call slot perhapsUploadFB if an incidence was added, changed or removed
00106     connect( changer, TQT_SIGNAL( incidenceAdded( Incidence* ) ),
00107              mFreeBusyManager, TQT_SLOT( slotPerhapsUploadFB() ) );
00108     connect( changer, TQT_SIGNAL( incidenceChanged( Incidence*, Incidence*, KOGlobals::WhatChanged ) ),
00109              mFreeBusyManager, TQT_SLOT( slotPerhapsUploadFB() ) );
00110     connect( changer, TQT_SIGNAL( incidenceDeleted( Incidence * ) ),
00111              mFreeBusyManager, TQT_SLOT( slotPerhapsUploadFB() ) );
00112 }
00113 
00114 FreeBusyManager *KOGroupware::freeBusyManager()
00115 {
00116   if ( !mFreeBusyManager ) {
00117     mFreeBusyManager = new FreeBusyManager( this, "freebusymanager" );
00118     mFreeBusyManager->setCalendar( mCalendar );
00119     connect( mCalendar, TQT_SIGNAL( calendarChanged() ),
00120              mFreeBusyManager, TQT_SLOT( slotPerhapsUploadFB() ) );
00121     connect( mView, TQT_SIGNAL( newIncidenceChanger( IncidenceChangerBase* ) ),
00122              this, TQT_SLOT( slotViewNewIncidenceChanger( IncidenceChangerBase* ) ) );
00123     slotViewNewIncidenceChanger( mView->incidenceChanger() );
00124   }
00125 
00126   return mFreeBusyManager;
00127 }
00128 
00129 void KOGroupware::incomingDirChanged( const TQString& path )
00130 {
00131   const TQString incomingDirName = locateLocal( "data","korganizer/" )
00132                                   + "income.";
00133   if ( !path.startsWith( incomingDirName ) ) {
00134     kdDebug(5850) << "incomingDirChanged: Wrong dir " << path << endl;
00135     return;
00136   }
00137   TQString action = path.mid( incomingDirName.length() );
00138   while ( action.length() > 0 && action[ action.length()-1 ] == '/' )
00139     // Strip slashes at the end
00140     action.truncate( action.length()-1 );
00141 
00142   // Handle accepted invitations
00143   TQDir dir( path );
00144   const TQStringList files = dir.entryList( TQDir::Files );
00145   if ( files.isEmpty() )
00146     // No more files here
00147     return;
00148 
00149   // Read the file and remove it
00150   TQFile f( path + "/" + files[0] );
00151   if (!f.open(IO_ReadOnly)) {
00152     kdError(5850) << "Can't open file '" << files[0] << "'" << endl;
00153     return;
00154   }
00155   TQTextStream t(&f);
00156   t.setEncoding( TQTextStream::UnicodeUTF8 );
00157   TQString receiver = KPIM::getFirstEmailAddress( t.readLine() );
00158   TQString iCal = t.read();
00159 
00160   f.remove();
00161 
00162   ScheduleMessage *message = mFormat.parseScheduleMessage( mCalendar, iCal );
00163   if ( !message ) {
00164     TQString errorMessage;
00165     if (mFormat.exception())
00166       errorMessage = i18n( "Error message: %1" ).arg( mFormat.exception()->message() );
00167     kdDebug(5850) << "MailScheduler::retrieveTransactions() Error parsing "
00168                   << errorMessage << endl;
00169     KMessageBox::detailedError( mView,
00170         i18n("Error while processing an invitation or update."),
00171         errorMessage );
00172     return;
00173   }
00174 
00175   KCal::Scheduler::Method method =
00176     static_cast<KCal::Scheduler::Method>( message->method() );
00177   KCal::ScheduleMessage::Status status = message->status();
00178   KCal::Incidence* incidence =
00179     dynamic_cast<KCal::Incidence*>( message->event() );
00180   if(!incidence) {
00181     delete message;
00182     return;
00183   }
00184   KCal::MailScheduler scheduler( mCalendar );
00185   if ( action.startsWith( "accepted" ) || action.startsWith( "tentative" )
00186        || action.startsWith( "delegated" ) || action.startsWith( "counter" ) ) {
00187     // Find myself and set my status. This can't be done in the scheduler,
00188     // since this does not know the choice I made in the KMail bpf
00189     KCal::Attendee::List attendees = incidence->attendees();
00190     KCal::Attendee::List::ConstIterator it;
00191     for ( it = attendees.begin(); it != attendees.end(); ++it ) {
00192       if( (*it)->email() == receiver ) {
00193         if ( action.startsWith( "accepted" ) )
00194           (*it)->setStatus( KCal::Attendee::Accepted );
00195         else if ( action.startsWith( "tentative" ) )
00196           (*it)->setStatus( KCal::Attendee::Tentative );
00197         else if ( KOPrefs::instance()->outlookCompatCounterProposals() && action.startsWith( "counter" ) )
00198           (*it)->setStatus( KCal::Attendee::Tentative );
00199         else if ( action.startsWith( "delegated" ) )
00200           (*it)->setStatus( KCal::Attendee::Delegated );
00201         break;
00202       }
00203     }
00204     if ( KOPrefs::instance()->outlookCompatCounterProposals() || !action.startsWith( "counter" ) )
00205       scheduler.acceptTransaction( incidence, method, status, receiver );
00206   } else if ( action.startsWith( "cancel" ) )
00207     // Delete the old incidence, if one is present
00208     scheduler.acceptTransaction( incidence, KCal::Scheduler::Cancel, status, receiver );
00209   else if ( action.startsWith( "reply" ) ) {
00210     if ( method != Scheduler::Counter ) {
00211       scheduler.acceptTransaction( incidence, method, status );
00212     } else {
00213       // accept counter proposal
00214       scheduler.acceptCounterProposal( incidence );
00215       // send update to all attendees
00216       sendICalMessage( mView, Scheduler::Request, incidence, KOGlobals::INCIDENCEEDITED, false );
00217     }
00218   } else
00219     kdError(5850) << "Unknown incoming action " << action << endl;
00220 
00221   if ( action.startsWith( "counter" ) ) {
00222     mView->editIncidence( incidence, TQDate(), true );
00223     KOIncidenceEditor *tmp = mView->editorDialog( incidence );
00224     tmp->selectInvitationCounterProposal( true );
00225   }
00226   mView->updateView();
00227 }
00228 
00229 class KOInvitationFormatterHelper : public InvitationFormatterHelper
00230 {
00231   public:
00232     virtual TQString generateLinkURL( const TQString &id ) { return "kmail:groupware_request_" + id; }
00233 };
00234 
00235 /* This function sends mails if necessary, and makes sure the user really
00236  * want to change his calendar.
00237  *
00238  * Return true means accept the changes
00239  * Return false means revert the changes
00240  */
00241 bool KOGroupware::sendICalMessage( TQWidget* parent,
00242                                    KCal::Scheduler::Method method,
00243                                    Incidence* incidence,
00244                                    KOGlobals::HowChanged action,
00245                                    bool attendeeStatusChanged,
00246                                    int dontAskForGroupware )
00247 {
00248   // If there are no attendees, don't bother
00249   if( incidence->attendees().isEmpty() )
00250     return true;
00251 
00252   bool isOrganizer = KOPrefs::instance()->thatIsMe( incidence->organizer().email() );
00253   int rc = 0;
00254   /*
00255    * There are two scenarios:
00256    * o "we" are the organizer, where "we" means any of the identities or mail
00257    *   addresses known to Kontact/PIM. If there are attendees, we need to mail
00258    *   them all, even if one or more of them are also "us". Otherwise there
00259    *   would be no way to invite a resource or our boss, other identities we
00260    *   also manage.
00261    * o "we: are not the organizer, which means we changed the completion status
00262    *   of a todo, or we changed our attendee status from, say, tentative to
00263    *   accepted. In both cases we only mail the organizer. All other changes
00264    *   bring us out of sync with the organizer, so we won't mail, if the user
00265    *   insists on applying them.
00266    */
00267 
00268   if ( dontAskForGroupware == 1 ) {
00269     rc = KMessageBox::Yes;
00270   }
00271   else if ( dontAskForGroupware == 2 ) {
00272     rc = KMessageBox::No;
00273   }
00274   else {
00275     if ( isOrganizer ) {
00276       /* We are the organizer. If there is more than one attendee, or if there is
00277        * only one, and it's not the same as the organizer, ask the user to send
00278        * mail. */
00279       if ( incidence->attendees().count() > 1
00280           || incidence->attendees().first()->email() != incidence->organizer().email() ) {
00281 
00282         TQString txt;
00283         switch( action ) {
00284         case KOGlobals::INCIDENCEEDITED:
00285           txt = i18n( "You changed the invitation \"%1\".\n"
00286                       "Do you want to email the attendees an update message?" ).
00287                 arg( incidence->summary() );
00288           break;
00289         case KOGlobals::INCIDENCEDELETED:
00290           Q_ASSERT( incidence->type() == "Event" || incidence->type() == "Todo" );
00291           if ( incidence->type() == "Event" ) {
00292             txt = i18n( "You removed the invitation \"%1\".\n"
00293                         "Do you want to email the attendees that the event is canceled?" ).
00294                   arg( incidence->summary() );
00295           } else if ( incidence->type() == "Todo" ) {
00296             txt = i18n( "You removed the invitation \"%1\".\n"
00297                         "Do you want to email the attendees that the todo is canceled?" ).
00298                   arg( incidence->summary() );
00299           }
00300           break;
00301         case KOGlobals::INCIDENCEADDED:
00302           if ( incidence->type() == "Event" ) {
00303             txt = i18n( "The event \"%1\" includes other people.\n"
00304                         "Do you want to email the invitation to the attendees?" ).
00305                   arg( incidence->summary() );
00306           } else if ( incidence->type() == "Todo" ) {
00307             txt = i18n( "The todo \"%1\" includes other people.\n"
00308                         "Do you want to email the invitation to the attendees?" ).
00309                   arg( incidence->summary() );
00310           } else {
00311             txt = i18n( "This incidence includes other people. "
00312                         "Should an email be sent to the attendees?" );
00313           }
00314           break;
00315         default:
00316           kdError() << "Unsupported HowChanged action" << int( action ) << endl;
00317           break;
00318         }
00319 
00320         rc = KMessageBox::questionYesNo(
00321                parent, txt, i18n( "Group Scheduling Email" ),
00322                KGuiItem( i18n( "Send Email" ) ), KGuiItem( i18n( "Do Not Send" ) ) );
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       // Ask if the user wants to tell the organizer about the current status
00332       TQString txt = i18n( "Do you want to send a status update to the "
00333                           "organizer of this task?");
00334       rc = KMessageBox::questionYesNo( parent, txt, TQString(), i18n("Send Update"), i18n("Do Not Send") );
00335     } else if( incidence->type() == "Event" ) {
00336       TQString txt;
00337       if ( attendeeStatusChanged && method == Scheduler::Request ) {
00338         txt = i18n( "Your status as an attendee of this event changed. "
00339                     "Do you want to send a status update to the event organizer?" );
00340         method = Scheduler::Reply;
00341         rc = KMessageBox::questionYesNo( parent, txt, TQString(), i18n("Send Update"), i18n("Do Not Send") );
00342       } else {
00343         if( action == KOGlobals::INCIDENCEDELETED ) {
00344           const TQStringList myEmails = KOPrefs::instance()->allEmails();
00345           bool askConfirmation = false;
00346           for ( TQStringList::ConstIterator it = myEmails.begin(); it != myEmails.end(); ++it ) {
00347             TQString email = *it;
00348             Attendee *me = incidence->attendeeByMail(email);
00349             if (me && (me->status()==KCal::Attendee::Accepted || me->status()==KCal::Attendee::Delegated)) {
00350               askConfirmation = true;
00351               break;
00352             }
00353           }
00354 
00355           if ( !askConfirmation ) {
00356             return true;
00357           }
00358 
00359           txt = i18n( "You had previously accepted an invitation to this event. "
00360                       "Do you want to send an updated response to the organizer "
00361                       "declining the invitation?" );
00362           rc = KMessageBox::questionYesNo(
00363             parent, txt, i18n( "Group Scheduling Email" ),
00364             KGuiItem( i18n( "Send Update" ) ), KGuiItem( i18n( "Do Not Send" ) ) );
00365           setDoNotNotify( rc == KMessageBox::No );
00366         } else {
00367           txt = i18n( "You are not the organizer of this event. Editing it will "
00368                       "bring your calendar out of sync with the organizer's calendar. "
00369                       "Do you really want to edit it?" );
00370           rc = KMessageBox::warningYesNo( parent, txt );
00371           return ( rc == KMessageBox::Yes );
00372         }
00373       }
00374     } else {
00375       kdWarning(5850) << "Groupware messages for Journals are not implemented yet!" << endl;
00376       return true;
00377     }
00378   }
00379 
00380   if ( rc == KMessageBox::Yes ) {
00381     // We will be sending out a message here. Now make sure there is
00382     // some summary
00383     if( incidence->summary().isEmpty() )
00384       incidence->setSummary( i18n("<No summary given>") );
00385 
00386     // Send the mail
00387     KCal::MailScheduler scheduler( mCalendar );
00388     scheduler.performTransaction( incidence, method );
00389 
00390     return true;
00391   } else if ( rc == KMessageBox::No ) {
00392     return true;
00393   } else {
00394     return false;
00395   }
00396 }
00397 
00398 void KOGroupware::sendCounterProposal(KCal::Calendar *calendar, KCal::Event * oldEvent, KCal::Event * newEvent) const
00399 {
00400   if ( !oldEvent || !newEvent || *oldEvent == *newEvent || !KOPrefs::instance()->mUseGroupwareCommunication )
00401     return;
00402   if ( KOPrefs::instance()->outlookCompatCounterProposals() ) {
00403     Incidence* tmp = oldEvent->clone();
00404     tmp->setSummary( i18n("Counter proposal: %1").arg( newEvent->summary() ) );
00405     tmp->setDescription( newEvent->description() );
00406     tmp->addComment( i18n("Proposed new meeting time: %1 - %2").
00407                      arg( IncidenceFormatter::dateToString( newEvent->dtStart() ),
00408                           IncidenceFormatter::dateToString( newEvent->dtEnd() ) ) );
00409     KCal::MailScheduler scheduler( calendar );
00410     scheduler.performTransaction( tmp, Scheduler::Reply );
00411     delete tmp;
00412   } else {
00413     KCal::MailScheduler scheduler( calendar );
00414     scheduler.performTransaction( newEvent, Scheduler::Counter );
00415   }
00416 }
00417 
00418 #include "kogroupware.moc"