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