0766fa20d9d0fa8f9f55c39575003eedecbeca70
[qwerkisync] / DBBackends / RtcomEventLogger.cpp
1 /*
2  * Copyright (C) 2011, Jamie Thompson
3  *
4  * This program is free software; you can redistribute it and/or
5  * modify it under the terms of the GNU General Public
6  * License as published by the Free Software Foundation; either
7  * version 3 of the License, or (at your option) any later version.
8  *
9  * This program is distributed in the hope that it will be useful,
10  * but WITHOUT ANY WARRANTY; without even the implied warranty of
11  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
12  * General Public License for more details.
13  *
14  * You should have received a copy of the GNU General Public
15  * License along with this program; If not, see
16  * <http://www.gnu.org/licenses/>.
17  */
18
19 #include "RtcomEventLogger.h"
20
21 #include "EventProcessors/iEventProcessor.h"
22 #include "EventTypes/eEventTypes.h"
23 #include "EventTypes/iEvent.h"
24 #include "EventTypes/PhoneCall.h"
25 #include "EventTypes/SMS.h"
26 #include "RtcomEventLoggerComponents/TriggerDisabler.h"
27 #include "Settings.h"
28
29 #include <QDebug>
30 #include <QMutex>
31 #include <QWaitCondition>
32
33 // For reindexing
34 #include <QDir>
35 #include <QPair>
36 #include <QStringList>
37 #include <QtSql/QSqlDatabase>
38 #include <QtSql/QSqlQuery>
39 #include <QVariant>
40
41 #include <uuid/uuid.h>
42
43 #include <rtcom-eventlogger/event.h>
44 #include <rtcom-eventlogger/eventlogger.h>
45
46 #include <stdexcept>
47
48 using namespace DBBackends;
49 using namespace EventTypes;
50
51 QDebug operator<<(QDebug, RTComElEvent &);
52 QDebug operator<<(QDebug, RTComElAttachment &);
53 QDebug operator<<(QDebug, GList &);
54 QDebug operator<<(QDebug, QList<RTComElAttachment*> &);
55
56 RtcomEventLogger::RtcomEventLogger(const Settings &settings) :
57         m_Settings(settings)
58 {
59         RTComEl *el(rtcom_el_new());
60         if(NULL != el)
61         {
62                 // Grab the service IDs we want to work with
63                 m_ServiceIDs.insert(EVENT_TYPE_CALL, rtcom_el_get_service_id(el, "RTCOM_EL_SERVICE_CALL"));
64                 //m_ServiceIDs.insert(EVENT_TYPE_CHAT, rtcom_el_get_service_id(el, "RTCOM_EL_SERVICE_CHAT"));
65                 m_ServiceIDs.insert(EVENT_TYPE_SMS, rtcom_el_get_service_id(el, "RTCOM_EL_SERVICE_SMS"));
66                 //m_ServiceIDs.insert(EVENT_TYPE_MMS, rtcom_el_get_service_id(el, "RTCOM_EL_SERVICE_MMS"));
67
68                 // Remove any service IDs that weren't found
69                 foreach(EventTypes::eEventTypes service, m_ServiceIDs.keys())
70                         if(m_ServiceIDs.value(service) == -1)
71                                 m_ServiceIDs.remove(service);
72
73                 g_object_unref(el);
74         }
75         else
76                 qDebug() << "Failed to create event logger.";
77 }
78
79 RtcomEventLogger::RtcomEventLogger(const Settings &settings, const EventTypes::RtcomEvent &event) :
80         m_Settings(settings)
81 {
82 }
83
84 void RtcomEventLogger::Process(EventProcessors::iEventProcessor &processor)
85 {
86         // Initialise the event logger
87         RTComEl *el = rtcom_el_new();
88         if(NULL != el)
89         {
90                 foreach(eEventTypes service, m_ServiceIDs.keys())
91                         ProcessService(processor, service, *el);
92
93                 g_object_unref(el);
94         }
95         else
96                 qDebug() << "Failed to create event logger.";
97 }
98
99 void RtcomEventLogger::ProcessService(EventProcessors::iEventProcessor &processor, const EventTypes::eEventTypes service, const RTComEl &el)
100 {
101         RTComEl *el_nonconst(const_cast<RTComEl *>(&el));
102
103         bool incoming = CurrentSettings().ShouldProcess( Settings::INCOMING, service);
104         bool outgoing = CurrentSettings().ShouldProcess( Settings::OUTGOING, service);
105
106         if(incoming || outgoing)
107         {
108                 // Initialise a query
109                 RTComElQuery *query = rtcom_el_query_new(el_nonconst);
110                 if(query != NULL)
111                 {
112                         // Prepare it...
113                         bool prepared = false;
114                         if(incoming && outgoing)
115                         {
116                                 prepared = rtcom_el_query_prepare(query,
117                                         "service-id",
118                                         m_ServiceIDs.value(service),
119                                         RTCOM_EL_OP_EQUAL,
120
121                                         NULL);
122                         }
123                         else
124                         {
125                                 prepared = rtcom_el_query_prepare(query,
126                                         "service-id",
127                                         m_ServiceIDs.value(service),
128                                         RTCOM_EL_OP_EQUAL,
129
130                                         "outgoing",
131                                         incoming ? 0 : 1,
132                                         RTCOM_EL_OP_EQUAL,
133
134                                         NULL);
135                         }
136
137                         qDebug() << "SQL:\n" << rtcom_el_query_get_sql(query);
138
139                         if(prepared)
140                         {
141                                 RTComElIter *it = rtcom_el_get_events(el_nonconst, query);
142                                 if(it != NULL)
143                                 {
144                                         if(rtcom_el_iter_first(it))
145                                         {
146                                                 int eventCount = 0;
147                                                 qDebug() << "Getting event count...";
148                                                 while(rtcom_el_iter_next(it))
149                                                         ++eventCount;
150
151                                                 // Reset the iterator and grab the actual values
152                                                 qDebug() << "Resetting iterator...";
153                                                 g_object_unref(it);
154                                                 it = rtcom_el_get_events(el_nonconst, query);
155                                                 if(it != NULL)
156                                                 {
157                                                         if(rtcom_el_iter_first(it))
158                                                         {
159                                                                 int idx = 0;
160                                                                 qDebug() << "Getting events...";
161                                                                 do
162                                                                 {
163                                                                         ++idx;
164                                                                         qDebug() << "Event #" << idx;
165
166                                                                         RTComElEvent revent;
167                                                                         memset(&revent, 0, sizeof(revent));
168
169                                                                         if(rtcom_el_iter_get_full(it, &revent))
170                                                                         {
171                                                                                 qDebug() << revent;
172
173                                                                                 QList<RTComElAttachment *> rattachments;
174                                                                                 RTComElAttachIter *at_it = rtcom_el_iter_get_attachments(it);
175                                                                                 if(at_it != NULL)
176                                                                                 {
177                                                                                         qDebug() << "Attachments OK";
178                                                                                         if(rtcom_el_attach_iter_first(at_it))
179                                                                                         {
180                                                                                                 qDebug() << "Getting events...";
181
182                                                                                                 do
183                                                                                                 {
184                                                                                                         rattachments.append(rtcom_el_attach_iter_get(at_it));
185                                                                                                         qDebug() << "Attachment ID #" << rattachments.last()->id << endl;
186                                                                                                         qDebug() << "desc: " << rattachments.last()->desc << endl;
187                                                                                                         qDebug() << "path: " << rattachments.last()->path << endl;
188                                                                                                 }while(rtcom_el_attach_iter_next(at_it));
189                                                                                         }
190                                                                                 }
191
192                                                                                 EventTypes::iEvent *const newEvent(CreateEvent(revent, rattachments));
193                                                                                 processor.Process(*newEvent);
194                                                                                 delete newEvent;
195
196                                                                                 processor.EmitEventProcessed(idx, eventCount);
197                                                                         }
198
199                                                                         rtcom_el_event_free_contents(&revent);
200                                                                 }
201                                                                 while(rtcom_el_iter_next(it));
202                                                                 qDebug() << "...all events retrieved.";
203                                                         }
204                                                 }
205                                                 else
206                                                         qDebug() << "Failed to reset iterator";
207                                         }
208                                         else
209                                                 qDebug() << "Failed to start iterator";
210                                 }
211                                 else
212                                         qDebug() << "Failed to get iterator. Do you have any events?";
213                         }
214                         else
215                                 qDebug() << "Failed to prepare the query.";
216
217                         g_object_unref(query);
218                 }
219                 else
220                         qDebug() << "Failed to create query.";
221         }
222         else
223                 qDebug() << "Nothing to do for " << m_ServiceIDs.value(service);
224 }
225
226 EventTypes::iEvent *const RtcomEventLogger::CreateEvent(RTComElEvent &revent, QList<RTComElAttachment*> &rattachments)
227 {
228         if(m_ServiceIDs.contains(EVENT_TYPE_CALL) && revent.fld_service_id == m_ServiceIDs.value(EVENT_TYPE_CALL))
229                 return new EventTypes::PhoneCall(CurrentSettings(), revent, rattachments);
230
231         //if(m_ServiceIDs.contains(EVENT_TYPE_CHAT) && revent.fld_service_id == m_ServiceIDs.value(EVENT_TYPE_CHAT))
232         //      return new EventTypes::Chat(CurrentSettings(), revent, rattachments);
233
234         if(m_ServiceIDs.contains(EVENT_TYPE_SMS) && revent.fld_service_id == m_ServiceIDs.value(EVENT_TYPE_SMS))
235                 return new EventTypes::SMS(CurrentSettings(), revent, rattachments);
236
237         //if(m_ServiceIDs.contains(EVENT_TYPE_MMS) && revent.fld_service_id == m_ServiceIDs.value(EVENT_TYPE_MMS))
238         //      return new EventTypes::MMS(CurrentSettings(), revent, rattachments);
239
240         return NULL;
241 }
242
243 void RtcomEventLogger::PreInsert()
244 {
245         m_TriggerDisabler = new RtcomEventLoggerComponents::TriggerDisabler(CurrentSettings());
246 }
247
248 void RtcomEventLogger::Insert(EventTypes::iEvent &event, const NumberToNameLookup &numberToNameLookup)
249 {
250         if(EventTypes::RtcomEvent *rtcomEvent = dynamic_cast<EventTypes::RtcomEvent *>(&event))
251         {
252                 const uint UUID_STR_LEN(36);
253
254                 _RTComEl *el(rtcom_el_new());
255                 if(NULL != el)
256                 {
257                         // Convert our objects into RTCom structs
258                         RTComElEvent *revent(rtcomEvent->toRTComEvent(numberToNameLookup));
259                         GList *rattachments(event.Attachments().toRTComAttachments());
260
261                         GError *error(NULL);
262
263                         // Generate the headers for the event
264                         GHashTable *rheaders(g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free));
265                         uuid_t uuid;
266                         char key[UUID_STR_LEN + 1];
267                         uuid_generate_random(uuid);
268                         uuid_unparse(uuid, key);
269                         g_hash_table_insert(rheaders, g_strdup ("message-token"), key);
270                         qDebug() << "headers: " << rheaders;
271
272                         qDebug() << "Inserting event:";
273                         qDebug() << *revent;
274                         qDebug() << *rattachments;
275
276                         // Add the event
277                         QDateTime startTime(QDateTime::currentDateTimeUtc());
278                         int newEventID(-1);
279                         int currentBackoffInMillisecs(5);
280                         while(((newEventID = rtcom_el_add_event_full(el, revent, rheaders, rattachments, &error)) == -1)
281                                   && startTime.msecsTo(QDateTime::currentDateTimeUtc()) < 10000)
282                         {
283                                 if(error != NULL)
284                                 {
285                                         qDebug() << "err: " << error->message;
286                                         g_error_free(error);
287                                         error = NULL;
288                                 }
289
290                                 // Don't hammer the DB when there's an error. Give it literally just a moment before retrying.
291                                 QMutex mutex;
292                                 mutex.lock();
293
294                                 QWaitCondition waitCondition;
295                                 waitCondition.wait(&mutex, currentBackoffInMillisecs);
296
297                                 mutex.unlock();
298
299                                 // Exponential backoff...
300                                 currentBackoffInMillisecs *= 2;
301                         }
302
303                         if(-1 == newEventID)
304                         {
305                                 qDebug() << "Unable to insert event due to error.";
306                                 qDebug() << *revent << "\n";
307                         }
308                         else
309                         {
310                                 qDebug() << "new id: " << newEventID;
311                                 InsertedIDs().append(newEventID);
312                         }
313
314                         // Release the attachments
315                         g_list_foreach (rattachments, (GFunc) rtcom_el_free_attachment, NULL);
316                         g_list_free (rattachments);
317
318                         rtcom_el_event_free_contents(revent);
319                         rtcom_el_event_free(revent);
320                 }
321                 else
322                         qDebug() << "Unable to initalise eventlogger for insertion.";
323
324                 g_object_unref(el);
325         }
326
327         return;
328 }
329
330 void RtcomEventLogger::PostInsert()
331 {
332         // Our new events get the specified storage times ignored, and some things
333         // use these, so bodge them for now.
334         UpdateInsertedStorageTimes();
335
336         // Reorder the DB IDs as Nokia are guilty of both premature
337         // optimisation as well as closed source UIs...
338         Reindex();
339
340         delete m_TriggerDisabler;
341 }
342
343 void RtcomEventLogger::ClearInsertedIDs()
344 {
345         InsertedIDs().clear();
346 }
347
348 void RtcomEventLogger::UpdateInsertedStorageTimes()
349 {
350         // Set up the database connection...
351         QSqlDatabase db(QSqlDatabase::addDatabase("QSQLITE"));
352
353         db.setDatabaseName(CurrentSettings().DBPath());
354         if(db.open())
355         {
356                 // Update storage time as some software uses it...
357                 QSqlQuery * updateStorageTimeQuery(new QSqlQuery(db));
358                 if(updateStorageTimeQuery != NULL)
359                 {
360                         updateStorageTimeQuery->setForwardOnly( true );
361
362                         if(db.transaction())
363                         {
364                                 try
365                                 {
366                                         QString sqlUpdateStorageTime(QString("UPDATE events SET storage_time = start_time WHERE id IN (%1)")
367                                                 .arg(IntsToStringList(InsertedIDs()).join(",")));
368                                         if (!updateStorageTimeQuery->exec(sqlUpdateStorageTime))
369                                         {
370                                                 qDebug() << "Query Failed: " << sqlUpdateStorageTime;
371                                                 throw std::exception();
372                                         }
373
374                                         qDebug() << "Committing.";
375                                         db.commit();
376                                 }
377                                 catch(...)
378                                 {
379                                         qDebug() << "Rolling back.";
380                                         db.rollback();
381                                 }
382                         }
383                         else
384                                 qDebug() << "Unable to start transaction.";
385                 }
386         }
387         else
388                 throw std::runtime_error("Cannot open database: Unable to establish database connection");
389 }
390
391 // Reorder the DB IDs as Nokia are guilty of both premature
392 // optimisation as well as closed source UIs...
393 // NOTE: The InsertedID list will be invalid after this so call it last...
394 void RtcomEventLogger::Reindex()
395 {
396         // Set up the database connection...
397         QSqlDatabase db(QSqlDatabase::addDatabase("QSQLITE"));
398
399         db.setDatabaseName(CurrentSettings().DBPath());
400         if(db.open())
401         {
402                 // Reorder the evnts by their start time
403                 uint changesRequired(0);
404                 do
405                 {
406                         // Note the smallest event ID found, so we have a place to start.
407                         int min(0);
408
409                         // The required ID changes ( current, correct );
410                         QHash<int, int> mapping;
411
412                         // Grab the current records, and determine what changes need to
413                         // happen to get to the sorted results
414                         {
415                                 qDebug() << "DB Opened";
416
417                                 QSqlQuery * dbq1(new QSqlQuery( db )), * dbq2(new QSqlQuery( db ));
418
419                                 dbq1->setForwardOnly( true );
420                                 dbq2->setForwardOnly( true );
421
422                                 QString s1("SELECT id, event_type_id, start_time, end_time "
423                                                    " FROM Events");
424                                 QString s2("SELECT id, event_type_id, start_time, end_time "
425                                                    " FROM Events ORDER BY start_time ASC");
426
427                                 if ( dbq1->exec( s1 ) && dbq2->exec( s2 ))
428                                 {
429                                         qDebug() << "Query OK, " << dbq1->numRowsAffected() << " & " << dbq2->numRowsAffected() << " rows affected.";
430
431                                         while( dbq1->next() && dbq2->next())
432                                         {
433                                                 int one (dbq1->value( 0 ).value< int >());
434                                                 int two (dbq2->value( 0 ).value< int >());
435                                                 //uint startTime( m_dbq->value( 1 ).value< uint >() );
436                                                 //uint endTime( m_dbq->value( 2 ).value< uint >() );
437
438                                                 //qDebug() << "Event: " << type << ", " << startTime << ", " << endTime << "";
439                                                 //qDebug() << "( " << one << ", " << two << " )";
440
441                                                 if(two != one)
442                                                 {
443                                                         if(min == 0)
444                                                                 min = one;
445
446                                                         //qDebug() << "( " << one << ", " << two << " )";
447                                                         mapping.insert(one, two);
448                                                 }
449                                         }
450                                 }
451                                 else
452                                 {
453                                         qDebug() << "SQL EXEC Error: "<< "EXEC query failed";
454                                         qDebug() << "Query1: " << s1;
455                                         qDebug() << "Query2: " << s1;
456                                 }
457
458                                 // Clear up database connections
459                                 if ( dbq1 != NULL )
460                                 {
461                                         qDebug() << "Cleaning up connection 1";
462
463                                         dbq1->finish();
464
465                                         delete dbq1;
466                                         dbq1 = NULL;
467                                 }
468
469                                 if ( dbq2 != NULL )
470                                 {
471                                         qDebug() << "Cleaning up connection 2";
472
473                                         dbq2->finish();
474
475                                         delete dbq2;
476                                         dbq2 = NULL;
477                                 }
478                         }
479
480                         QList<int> sequence;
481                         int val(min);
482                         sequence.append(0);
483                         sequence.append(val);
484                         //qDebug().nospace() << "val1: " << val << ", ";
485
486                         while((val = mapping[val]) && val != min)
487                         {
488                                 sequence.append(val);
489                                 //qDebug().nospace() << val << ", ";
490                         }
491                         sequence.append(0);
492
493                         //qDebug().nospace() << "seq: ";
494                         QList<QPair<int,int> > updates;
495                         int last(sequence.first());
496                         foreach(int seq, sequence)
497                         {
498                                 if(seq != last)
499                                 {
500                                         //qDebug().nospace() << seq << ", " << last << ", ";
501                                         updates.append(QPair<int,int>(seq, last));
502                                 }
503
504                                 last = seq;
505                         }
506
507                         // Used to keep iterating until no changes are required.
508                         // TODO: Shouldn't be required, but is. One to revisit later.
509                         changesRequired = updates.count();
510
511                         for( QList<QPair<int,int> >::const_iterator it(updates.constBegin()); it != updates.constEnd(); ++it)
512                         {
513                                 //qDebug().nospace() << (*it).first << ", " << (*it).second;
514                         }
515
516                         QList<QString> tables = QList<QString>() << "Events" << "Attachments" << "Headers" << "GroupCache";
517                         QString query;
518                         for( QList<QString>::const_iterator currentTable(tables.constBegin()); currentTable != tables.constEnd(); ++currentTable)
519                         {
520                                 QString curquery = "UPDATE %3 set %4 = %1 WHERE %4 = %2;";
521                                 for( QList<QPair<int,int> >::const_iterator currentUpdate(updates.constBegin()); currentUpdate != updates.constEnd(); ++currentUpdate)
522                                 {
523                                         query.append(
524                                                 curquery
525                                                         .arg((*currentUpdate).second)
526                                                         .arg((*currentUpdate).first)
527                                                         .arg((*currentTable))
528                                                         .arg((*currentTable) == "Events" ? "id" : "event_id")
529                                                 ).append("\n");
530
531                                         //qDebug().nospace() << (*it).first << ", " << (*it).second;
532                                 }
533                         }
534
535                         //qDebug() << query;
536
537                         QSqlQuery * UpdateQuery(new QSqlQuery( db ));
538                         if(UpdateQuery != NULL)
539                         {
540                                 UpdateQuery->setForwardOnly( true );
541
542                                 if(db.transaction())
543                                 {
544                                         QStringList statements = query.trimmed().split(";", QString::SkipEmptyParts);
545                                         try
546                                         {
547                                                 for( QStringList::const_iterator currentStatement(statements.constBegin()); currentStatement != statements.constEnd(); ++currentStatement)
548                                                 {
549                                                         if (!UpdateQuery->exec(*currentStatement))
550                                                         {
551                                                                 qDebug() << "Query Failed: " << *currentStatement;
552                                                                 throw std::exception();
553                                                         }
554                                                 }
555
556                                                 qDebug() << "Committing.";
557                                                 db.commit();
558                                         }
559                                         catch(...)
560                                         {
561                                                 qDebug() << "Rolling back.";
562                                                 db.rollback();
563                                         }
564                                 }
565                                 else
566                                         qDebug() << "Unable to start transaction.";
567                         }
568                 }while(changesRequired > 0);
569
570                 qDebug() << "Closing.";
571                 db.close();
572                 QSqlDatabase::removeDatabase( "QSQLITE" );
573         }
574         else
575                 throw std::runtime_error("Cannot open database: Unable to establish database connection");
576
577         return;
578 }
579
580 QStringList RtcomEventLogger::IntsToStringList(QList<uint> &values)
581 {
582         QStringList returnValues;
583         returnValues.reserve(values.count());
584
585         foreach(uint value, values)
586                 returnValues.append(QString::number(value));
587
588         return returnValues;
589 }
590
591 QDebug operator<<(QDebug dbg, RTComElEvent &event)
592 {
593         dbg.nospace() << "\tid:\t\t" << event.fld_id << "\n";
594         dbg.nospace() << "\tservice_id:\t" << event.fld_service_id << "\n";
595         dbg.nospace() << "\tservice:\t" << event.fld_service << "\n";
596         dbg.nospace() << "\tevt_typ_id:\t" << event.fld_event_type_id << "\n";
597         dbg.nospace() << "\tevt_typ:\t" << event.fld_event_type << "\n";
598         dbg.nospace() << "\tstore-time:\t" << QDateTime::fromTime_t(event.fld_storage_time).toUTC() << "\n";
599         dbg.nospace() << "\tstart-time:\t" << QDateTime::fromTime_t(event.fld_start_time).toUTC() << "\n";
600         dbg.nospace() << "\tend-time:\t" << QDateTime::fromTime_t(event.fld_end_time).toUTC() << "\n";
601         dbg.nospace() << "\tis-read:\t" << (event.fld_is_read ? "true" : "false") << "\n";
602         dbg.nospace() << "\tdirection:\t" << (event.fld_outgoing ? "Outgoing" : "Incoming") << "\n";
603         dbg.nospace() << "\tflags:\t\t" << "0x" << QString::number(event.fld_flags, 16) << "\n";
604         dbg.nospace() << "\tbytes sent:\t" << event.fld_bytes_sent << "\n";
605         dbg.nospace() << "\tbytes recv:\t" << event.fld_bytes_received << "\n";
606         dbg.nospace() << "\tlocal-uid:\t" << event.fld_local_uid << "\n";
607         dbg.nospace() << "\tlocal-name:\t" << event.fld_local_name << "\n";
608         dbg.nospace() << "\tremote-uid:\t" << event.fld_remote_uid << "\n";
609         dbg.nospace() << "\tremote-name:\t" << event.fld_remote_name << "\n";
610         dbg.nospace() << "\tremote-ebid:\t" << event.fld_remote_ebook_uid << "\n";
611         dbg.nospace() << "\tchannel:\t\t" << event.fld_channel << "\n";
612         dbg.nospace() << "\tfree-text:\t" << event.fld_free_text << "\n";
613         dbg.nospace() << "\tgroup-uid:\t" << event.fld_group_uid << "\n";
614
615         return dbg;
616 }
617
618 QDebug operator<<(QDebug dbg, RTComElAttachment &attachment)
619 {
620         dbg.nospace() << "Event-id:\t" << attachment.event_id << "\n";
621         dbg.nospace() << "Path:\t" << attachment.path << "\n";
622         dbg.nospace() << "Desc:\t" << attachment.desc << "\n";
623
624         return dbg;
625 }
626
627 QDebug operator<<(QDebug dbg, GList &attachments)
628 {
629         dbg.nospace() << "Attachments" << "\n";
630
631         for (GList *attachment(&attachments); NULL != attachment; attachment = attachment->next)
632         {
633                 qDebug() << *(RTComElAttachment*)attachment->data;
634         }
635
636         dbg.nospace() << "\n";
637
638         return dbg;
639 }
640
641 QDebug operator<<(QDebug dbg, QList<RTComElAttachment *> &attachments)
642 {
643         dbg.nospace() << "Attachments" << "\n";
644
645         foreach(RTComElAttachment *attachment, attachments)
646                 dbg.nospace() << *attachment << "\n";
647
648         dbg.nospace() << "\n";
649
650         return dbg;
651 }