1/*
2**  ConsoleWindowController.m
3**
4**  Copyright (c) 2001-2007 Ludovic Marcotte
5**  Copyright (C) 2015-2016 Riccardo Mottola
6**
7**  Author: Ludovic Marcotte <ludovic@Sophos.ca>
8**          Riccardo Mottola <rm@gnu.org>
9**
10**  This program is free software; you can redistribute it and/or modify
11**  it under the terms of the GNU General Public License as published by
12**  the Free Software Foundation; either version 2 of the License, or
13**  (at your option) any later version.
14**
15**  This program is distributed in the hope that it will be useful,
16**  but WITHOUT ANY WARRANTY; without even the implied warranty of
17**  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
18**  GNU General Public License for more details.
19**
20** You should have received a copy of the GNU General Public License
21** along with this program.  If not, see <http://www.gnu.org/licenses/>.
22*/
23
24#import "ConsoleWindowController.h"
25
26#import "Constants.h"
27#import "GNUMail.h"
28#import "MailWindowController.h"
29#import "MailboxManagerController.h"
30#import "Task.h"
31#import "TaskManager.h"
32
33#import <Pantomime/CWFolder.h>
34#import <Pantomime/CWIMAPFolder.h>
35#import <Pantomime/CWIMAPStore.h>
36#import <Pantomime/CWMessage.h>
37#import <Pantomime/CWTCPConnection.h>
38#import <Pantomime/CWURLName.h>
39
40//
41// NOTE: For a good descriptions on what are the available "tasks" and how
42//       each one of them is used, please read the documentation on top
43//       of the TaskManager.m file.
44//
45
46static ConsoleWindowController *singleInstance = nil;
47
48static NSMutableArray *progressIndicators = nil;
49static NSProgressIndicator *progress = nil;
50
51static NSImage *restart = nil;
52static NSImage *stop = nil;
53
54
55//
56//
57//
58@interface ProgressIndicatorCell : NSCell
59{
60  Task *_task;
61}
62- (void) setTask: (Task *) theTask;
63@end
64
65
66@implementation ProgressIndicatorCell
67
68- (id) copyWithZone: (NSZone *) theZone
69{
70  return [super copyWithZone:theZone];
71}
72
73- (void) drawWithFrame: (NSRect) cellFrame
74		inView: (NSView *) controlView
75{
76  NSString *aString;
77  float f, w;
78  NSUInteger i;
79
80  [super drawWithFrame: cellFrame  inView: controlView];
81
82  if (_task == nil)
83    {
84      return;
85    }
86
87  if (_task->op == RECEIVE_IMAP)
88    {
89      f = (float)_task->received_count/(float)_task->total_count;
90    }
91  else
92    {
93      f = _task->current_size/_task->total_size;
94    }
95
96
97  cellFrame.size.width = cellFrame.size.width-40;
98  cellFrame.origin.x += 1.5;
99  w = cellFrame.size.width;
100
101  //
102  // We draw the text above our progress indicator.
103  //
104  switch (_task->op)
105    {
106    case RECEIVE_IMAP:
107      aString = [NSString stringWithFormat: _(@"Receiving IMAP - %@"), [_task key]];
108      break;
109
110    case RECEIVE_POP3:
111      aString = [NSString stringWithFormat: _(@"Receiving POP3 - %@"), [_task key]];
112      break;
113
114    case RECEIVE_UNIX:
115      aString = [NSString stringWithFormat: _(@"Receiving UNIX - %@"), [_task key]];
116      break;
117
118    case SEND_SENDMAIL:
119      aString = [NSString stringWithFormat: _(@"Sending Mailer - %@"), ([_task sendingKey] ? [_task sendingKey] : [_task key])];
120      break;
121
122    case SEND_SMTP:
123      aString = [NSString stringWithFormat: _(@"Sending SMTP - %@"), ([_task sendingKey] ? [_task sendingKey] : [_task key])];
124      break;
125
126    case SAVE_ASYNC:
127      aString = _(@"Saving message to the mailbox...");
128      break;
129
130    case LOAD_ASYNC:
131      aString = _(@"Loading message from the mailbox...");
132      break;
133
134    case CONNECT_ASYNC:
135      aString = [NSString stringWithFormat: _(@"Connecting to IMAP server %@..."), [_task key]];
136      break;
137
138    case SEARCH_ASYNC:
139      aString = [NSString stringWithFormat: _(@"Searching in account %@..."), [_task key]];
140      break;
141
142    case OPEN_ASYNC:
143      aString = [NSString stringWithFormat: _(@"Opening mailbox on %@..."), [_task key]];
144      break;
145
146    case EXPUNGE_ASYNC:
147      aString = [NSString stringWithFormat: _(@"Compacting mailbox on %@..."), [_task key]];
148      break;
149
150    default:
151      aString = nil;
152    }
153
154  if (aString)
155    {
156      NSMutableAttributedString *s;
157
158      s = [[NSMutableAttributedString alloc] initWithString: aString];
159      [s addAttribute: NSFontAttributeName
160	 value: [NSFont boldSystemFontOfSize: [NSFont smallSystemFontSize]]
161	 range: NSMakeRange(0, [s length])];
162      [s drawAtPoint: NSMakePoint(cellFrame.origin.x,cellFrame.origin.y+2)];
163      RELEASE(s);
164    }
165
166
167  //
168  // We now draw our progress indicator
169  //
170  cellFrame.size.height = 12;
171  cellFrame.origin.y += 18;
172  i = [[[TaskManager singleInstance] allTasks] indexOfObject: _task];
173
174  if (i >= [progressIndicators count])
175   {
176     progress = [[NSProgressIndicator alloc] init];
177     [progress setIndeterminate: NO];
178     [progress setMinValue: 0];
179     [progress setMaxValue: 1.0];
180     [progress setDoubleValue: 0];
181     [progressIndicators addObject: progress];
182     RELEASE(progress);
183   }
184
185  progress = [progressIndicators objectAtIndex: i];
186  [progress setFrame: cellFrame];
187  if ([progress superview] != controlView)
188    {
189      [controlView addSubview: progress];
190    }
191
192  [progress setDoubleValue: f];
193
194  cellFrame.origin.x -= 1.5;
195
196
197  //
198  // We draw the text below our progress indicator.
199  //
200  aString = nil;
201
202  if (_task->is_running)
203    {
204      if (_task->op == RECEIVE_POP3 && _task->total_count)
205	{
206	  aString = [NSString stringWithFormat: _(@"Received message %d of %d"), _task->received_count, _task->total_count];
207	}
208      if (_task->op == RECEIVE_IMAP && _task->total_count)
209	{
210	  aString = [NSString stringWithFormat: _(@"Got status for mailbox %d (%-.*@) of %d"),
211			      _task->received_count,
212			      20, [_task subtitle],
213			      _task->total_count];
214	}
215      else if (_task->op == SEND_SMTP || _task->op == SAVE_ASYNC || _task->op == LOAD_ASYNC)
216	{
217	  aString = [NSString stringWithFormat: _(@"Completed %0.1fKB of %0.1fKB."),
218			      (_task->current_size > _task->total_size ? _task->total_size : _task->current_size),
219			      _task->total_size];
220	}
221    }
222  else
223    {
224      aString = [NSString stringWithFormat: _(@"Suspended - Scheduled to run at %@"),
225			  [[_task date] descriptionWithCalendarFormat: @"%H:%M:%S"
226					timeZone: nil
227					locale: nil]];
228    }
229
230  if (aString)
231    {
232      NSMutableAttributedString *s;
233
234      s = [[NSMutableAttributedString alloc] initWithString: aString];
235      [s addAttribute: NSFontAttributeName
236	 value: [NSFont systemFontOfSize: [NSFont smallSystemFontSize]]
237	 range: NSMakeRange(0, [s length])];
238      [s drawAtPoint: NSMakePoint(cellFrame.origin.x,cellFrame.origin.y+15)];
239      RELEASE(s);
240    }
241
242
243  //
244  //
245  //
246  if (_task->is_running)
247    {
248      [stop compositeToPoint: NSMakePoint(w+6,cellFrame.origin.y+23) operation: NSCompositeSourceAtop];
249    }
250  else
251    {
252      [restart compositeToPoint: NSMakePoint(w+6,cellFrame.origin.y+23) operation: NSCompositeSourceAtop];
253    }
254}
255
256
257- (void) setTask: (Task *) theTask
258{
259  _task = theTask;
260}
261
262@end
263
264
265//
266//
267//
268@interface ConsoleWindowController (Private)
269- (void) _startAnimation;
270- (void) _startTask;
271- (void) _stopAnimation;
272- (void) _stopTask;
273@end
274
275//
276//
277//
278@interface ConsoleMessage : NSObject
279{
280  @public
281    NSString *message;
282    NSCalendarDate *date;
283}
284
285- (id) initWithMessage: (NSString *) theMessage;
286
287@end
288
289
290//
291//
292//
293@implementation ConsoleWindowController
294
295- (id) initWithWindowNibName: (NSString *) windowNibName
296{
297  self = [super initWithWindowNibName: windowNibName];
298
299  [[self window] setTitle: _(@"GNUMail Console")];
300
301  // We finally set our autosave window frame name and restore the one from the user's defaults.
302  [[self window] setFrameAutosaveName: @"ConsoleWindow"];
303  [[self window] setFrameUsingName: @"ConsoleWindow"];
304
305  // We set the custom cell for the Status column
306  [[tasksTableView tableColumnWithIdentifier: @"Status"] setDataCell: AUTORELEASE([[ProgressIndicatorCell alloc] init])];
307  [tasksTableView setIntercellSpacing: NSZeroSize];
308
309  // We initialize our static ivars
310  restart = RETAIN([NSImage imageNamed: @"restart_32.tiff"]);
311  stop = RETAIN([NSImage imageNamed: @"stop_32.tiff"]);
312
313  progressIndicators = [[NSMutableArray alloc] init];
314
315  // We remove the header / corner view from our tables
316  [tasksTableView setHeaderView: nil];
317  [tasksTableView setCornerView: nil];
318  [messagesTableView setHeaderView: nil];
319  [messagesTableView setCornerView: nil];
320
321  return self;
322}
323
324//
325//
326//
327- (void) dealloc
328{
329#ifdef MACOSX
330  [tasksTableView setDataSource: nil];
331  [messagesTableView setDataSource: nil];
332#endif
333
334  RELEASE(allMessages);
335  RELEASE(restart);
336  RELEASE(stop);
337
338  RELEASE(progressIndicators);
339
340  [super dealloc];
341}
342
343
344//
345// action methods
346//
347- (IBAction) clickedOnTableView: (id) sender
348{
349  NSPoint aPoint;
350  float x, y;
351  int row;
352
353  row = [tasksTableView clickedRow];
354
355  aPoint = [[[[NSApp currentEvent] window] contentView] convertPoint: [[NSApp currentEvent] locationInWindow]
356							toView: [tasksTableView enclosingScrollView]];
357
358  // We the location of our start/stop button on the clickedRow
359  x = [[tasksTableView enclosingScrollView] frame].size.width-36;
360  y = row*46+7;
361
362  if (NSPointInRect(aPoint, NSMakeRect(x,y,32,32)))
363    {
364      if (((Task *)[[[TaskManager singleInstance] allTasks] objectAtIndex: row])->is_running)
365	{
366	  [self _stopTask];
367	}
368      else
369	{
370	  [self _startTask];
371	}
372    }
373}
374
375//
376//
377//
378- (NSMenu *) dataView: (id) aDataView
379    contextMenuForRow: (int) theRow
380{
381  Task *aTask;
382
383  if (theRow >= 0 && [tasksTableView numberOfRows] > 0 &&
384      (aTask = [[[TaskManager singleInstance] allTasks] objectAtIndex: theRow]) &&
385      aTask->op != LOAD_ASYNC &&
386      aTask->op != SAVE_ASYNC)
387    {
388      [[menu itemAtIndex: 0] setEnabled: YES]; // Start / Stop
389      [[menu itemAtIndex: 1] setEnabled: YES]; // Delete
390      [[menu itemAtIndex: 2] setEnabled: YES]; // Save in Drafts
391
392      if (aTask->is_running)
393	{
394	  [[menu itemAtIndex: 0] setTitle: _(@"Stop")];
395	  [[menu itemAtIndex: 0] setAction: @selector(_stopTask)];
396	}
397      else
398	{
399	  [[menu itemAtIndex: 0] setTitle: _(@"Start")];
400	  [[menu itemAtIndex: 0] setAction: @selector(_startTask)];
401	}
402    }
403  else
404    {
405      [[menu itemAtIndex: 0] setEnabled: NO]; // Start / Stop
406      [[menu itemAtIndex: 1] setEnabled: NO]; // Delete
407      [[menu itemAtIndex: 2] setEnabled: NO]; // Save in Drafts
408    }
409
410  return menu;
411}
412
413- (IBAction) deleteClicked: (id) sender
414{
415  int aRow;
416
417  aRow = [tasksTableView selectedRow];
418
419  if (aRow >= 0)
420    {
421      Task *aTask;
422
423      // No need to call reloadData here since it's called in -removeTask.
424      aTask = [[[TaskManager singleInstance] allTasks] objectAtIndex: aRow];
425
426      if (aTask->is_running)
427	{
428	  NSRunInformationalAlertPanel(_(@"Delete error!"),
429				       _(@"You can't delete a running task. Stop it first."),
430				       _(@"OK"),
431				       NULL,
432				       NULL,
433				       NULL);
434	  return;
435	}
436
437      [[TaskManager singleInstance] removeTask: aTask];
438    }
439  else
440    {
441      NSBeep();
442    }
443}
444
445
446//
447//
448//
449- (IBAction) saveClicked: (id) sender
450{
451  int aRow;
452
453  aRow = [tasksTableView selectedRow];
454
455  if (aRow >= 0)
456    {
457      CWURLName *theURLName;
458      NSData *aData;
459      Task *aTask;
460
461      aTask = [[[TaskManager singleInstance] allTasks] objectAtIndex: aRow];
462
463      if (aTask->is_running)
464	{
465	  NSRunInformationalAlertPanel(_(@"Save error!"),
466				       _(@"You can't save the message in Drafts if the task is running. Stop it first."),
467				       _(@"OK"),
468				       NULL,
469				       NULL,
470				       NULL);
471	  return;
472	}
473
474      // We finally get our CWURLName object.
475      theURLName = [[CWURLName alloc] initWithString: [[[[[NSUserDefaults standardUserDefaults] objectForKey: @"ACCOUNTS"] objectForKey: [aTask key]]
476							 objectForKey: @"MAILBOXES"] objectForKey: @"DRAFTSFOLDERNAME"]
477				      path: [[NSUserDefaults standardUserDefaults] objectForKey: @"LOCALMAILDIR"]];
478
479      if ([[aTask message] respondsToSelector: @selector(isEqualToData:)])
480	{
481	  aData = [aTask message];
482	}
483      else
484	{
485	  aData = [[aTask message] dataValue];
486	}
487
488      [[MailboxManagerController singleInstance] addMessage: aData
489						 toFolder: theURLName];
490      RELEASE(theURLName);
491    }
492  else
493    {
494      NSBeep();
495    }
496}
497
498
499
500//
501// delegate methods
502//
503- (void) windowWillClose: (NSNotification *) theNotification
504{
505  // Do nothing
506}
507
508
509//
510//
511//
512- (void) windowDidLoad
513{
514  NSMenuItem *aMenuItem;
515
516  allMessages = [[NSMutableArray alloc] init];
517
518  // We set up our context menu
519  menu = [[NSMenu alloc] init];
520  [menu setAutoenablesItems: NO];
521
522  aMenuItem = [[NSMenuItem alloc] initWithTitle: _(@"Stop") action: NULL  keyEquivalent: @""];
523  [aMenuItem setTarget: self];
524  [menu addItem: aMenuItem];
525  RELEASE(aMenuItem);
526
527  aMenuItem = [[NSMenuItem alloc] initWithTitle: _(@"Delete") action: @selector(deleteClicked:)  keyEquivalent: @""];
528  [aMenuItem setTarget: self];
529  [menu addItem: aMenuItem];
530  RELEASE(aMenuItem);
531
532  aMenuItem = [[NSMenuItem alloc] initWithTitle: _(@"Save in Drafts") action: @selector(saveClicked:)  keyEquivalent: @""];
533  [aMenuItem setTarget: self];
534  [menu addItem: aMenuItem];
535  RELEASE(aMenuItem);
536
537  [tasksTableView setAction: @selector(clickedOnTableView:)];
538  [tasksTableView reloadData];
539  [messagesTableView reloadData];
540}
541
542
543//
544// Data Source methods
545//
546- (NSInteger) numberOfRowsInTableView: (NSTableView *)aTableView
547{
548  if (aTableView == tasksTableView)
549    {
550      return [[[TaskManager singleInstance] allTasks] count];
551    }
552  else
553    {
554      return [allMessages count];
555    }
556}
557
558
559//
560//
561//
562- (id)           tableView: (NSTableView *) aTableView
563 objectValueForTableColumn: (NSTableColumn *) aTableColumn
564		       row: (NSInteger) rowIndex
565{
566  if (aTableView == messagesTableView)
567    {
568      ConsoleMessage *aMessage;
569
570      aMessage = [allMessages objectAtIndex: rowIndex];
571
572      if ([[aTableColumn identifier] isEqualToString: @"Message Date"])
573        {
574          return [aMessage->date descriptionWithCalendarFormat: _(@"%H:%M:%S")
575			  timeZone: [aMessage->date timeZone]
576			  locale: nil];
577        }
578      else
579        {
580          return aMessage->message;
581        }
582    }
583
584  return nil;
585}
586
587
588//
589//
590//
591- (void) tableView: (NSTableView *)aTableView
592   willDisplayCell: (id)aCell
593    forTableColumn: (NSTableColumn *)aTableColumn
594               row: (NSInteger)rowIndex
595{
596  if (aTableView == tasksTableView && [[aTableColumn identifier] isEqualToString: @"Status"])
597    {
598      [(ProgressIndicatorCell *)[aTableColumn dataCell] setTask: [[[TaskManager singleInstance] allTasks] objectAtIndex: rowIndex]];
599    }
600  else if (aTableView == messagesTableView)
601    {
602      if ([[aTableColumn identifier] isEqualToString: @"Date"])
603	{
604	  [aCell setAlignment: NSRightTextAlignment];
605	}
606      [aCell setFont: [NSFont systemFontOfSize: [NSFont smallSystemFontSize]]];
607    }
608}
609
610
611//
612//
613//
614- (NSString *) tableView: (NSTableView *)aTableView
615	  toolTipForCell: (NSCell *)aCell
616		    rect: (NSRectPointer)rect
617	     tableColumn:(NSTableColumn *)aTableColumn
618		     row:(NSInteger)row
619	   mouseLocation:(NSPoint)mouseLocation
620{
621  if (aTableView == messagesTableView)
622    {
623      ConsoleMessage *aMessage;
624
625      aMessage = [allMessages objectAtIndex: row];
626
627      return [NSString stringWithFormat: _(@"%@ (%@)"), aMessage->message,
628		       [aMessage->date descriptionWithCalendarFormat: _(@"%H:%M:%S")
629				timeZone: [aMessage->date timeZone]
630				locale: nil]];
631    }
632
633  return nil;
634}
635
636//
637// access / mutation method
638//
639- (NSTableView *) tasksTableView
640{
641  return tasksTableView;
642}
643
644
645//
646//
647//
648- (id) progressIndicators
649{
650  return progressIndicators;
651}
652
653
654//
655// Other methods
656//
657- (void) addConsoleMessage: (NSString *) theString
658{
659  ConsoleMessage *aMessage;
660
661  aMessage = [[ConsoleMessage alloc] initWithMessage: theString];
662
663  [allMessages insertObject: aMessage  atIndex: 0];
664  RELEASE(aMessage);
665
666  // We never keep more than 25 object in our array
667  if ([allMessages count] > 25)
668    {
669      [allMessages removeLastObject];
670    }
671
672  [messagesTableView reloadData];
673}
674
675//
676//
677//
678- (void) reload
679{
680  NSUInteger count;
681  NSUInteger i;
682
683  [tasksTableView reloadData];
684
685  count = [[[TaskManager singleInstance] allTasks] count];
686
687  // Remove all unused progress indicators from table view
688  for (i = count; i < [progressIndicators count]; i++)
689    {
690      [[progressIndicators objectAtIndex: i] removeFromSuperview];
691    }
692
693  while (count--)
694    {
695      if (((Task *)[[[TaskManager singleInstance] allTasks] objectAtIndex: count])->is_running)
696	{
697	  [self _startAnimation];
698	  return;
699	}
700    }
701
702  [self _stopAnimation];
703}
704
705//
706//
707//
708- (void) restoreImage
709{
710  MailWindowController *aController;
711  NSUInteger count;
712
713  count = [[GNUMail allMailWindows] count];
714
715  while (count--)
716    {
717      aController = (MailWindowController *)[[[GNUMail allMailWindows] objectAtIndex: count] windowController];
718
719      // We verify if we are using a secure (SSL) connection or not
720      if ([[aController folder] isKindOfClass: [CWIMAPFolder class]] &&
721	  [(CWTCPConnection *)[(CWIMAPStore *)[[aController folder] store] connection] isSSL])
722	{
723	  [aController->icon setImage: [NSImage imageNamed: @"pgp-mail-small.tiff"]];
724	}
725      else
726	{
727	  [aController->icon setImage: nil];
728	}
729    }
730}
731
732//
733// class methods
734//
735+ (id) singleInstance
736{
737  if (singleInstance == nil)
738    {
739      singleInstance = [[ConsoleWindowController alloc] initWithWindowNibName: @"ConsoleWindow"];
740    }
741
742  return singleInstance;
743}
744
745@end
746
747
748//
749//
750//
751@implementation ConsoleWindowController (Private)
752
753- (void) _startAnimation
754{
755  NSUInteger count;
756
757  count = [[GNUMail allMailWindows] count];
758
759  while (count--)
760    {
761      [((MailWindowController *)[[[GNUMail allMailWindows] objectAtIndex: count] windowController])->progressIndicator startAnimation: self];
762    }
763}
764
765- (void) _startTask
766{
767  NSInteger count, row;
768
769  count = (NSInteger)[[[TaskManager singleInstance] allTasks] count];
770  row = [tasksTableView selectedRow];
771
772  if (row >= 0 && row < count)
773    {
774      Task *aTask;
775
776      aTask = [[[TaskManager singleInstance] allTasks] objectAtIndex: row];
777      [aTask setDate: [NSDate date]];
778      aTask->immediate = YES;
779      [[TaskManager singleInstance] nextTask];
780      [[menu itemAtIndex: 0] setTitle: _(@"Stop")];
781      [[menu itemAtIndex: 0] setAction: @selector(_stopTask)];
782      [self reload];
783    }
784}
785
786- (void) _stopAnimation
787{
788  MailWindowController *aController;
789  NSUInteger count;
790
791  count = [[GNUMail allMailWindows] count];
792
793  while (count--)
794    {
795      aController = (MailWindowController *)[[[GNUMail allMailWindows] objectAtIndex: count] windowController];
796      [aController->progressIndicator stopAnimation: self];
797      [aController updateStatusLabel];
798    }
799
800  [self restoreImage];
801}
802
803- (void) _stopTask
804{
805  NSInteger count, row;
806
807  count = (NSInteger)[[[TaskManager singleInstance] allTasks] count];
808  row = [tasksTableView selectedRow];
809
810  if (row >= 0 && row < count)
811    {
812      [[TaskManager singleInstance] stopTask: [[[TaskManager singleInstance] allTasks] objectAtIndex: row]];
813      [[menu itemAtIndex: 0] setTitle: _(@"Start")];
814      [[menu itemAtIndex: 0] setAction: @selector(_startTask)];
815      [tasksTableView setNeedsDisplay: YES];
816    }
817}
818
819
820@end
821
822
823
824//
825//
826//
827@implementation ConsoleMessage
828
829- (id) initWithMessage: (NSString *) theMessage
830{
831  self = [super init];
832
833  message = RETAIN(theMessage);
834  date = RETAIN([NSCalendarDate calendarDate]);
835
836  return self;
837}
838
839- (void) dealloc
840{
841  RELEASE(message);
842  RELEASE(date);
843  [super dealloc];
844}
845
846@end
847