Extras/GrowlMail/GrowlMail.m
author Peter Hosey
Wed Feb 25 01:02:44 2009 -0800 (2009-02-25)
changeset 4172 65a0bcf23292
parent 4170 caae0ed9a580
child 4178 9ea7cd00514b
permissions -rw-r--r--
Fix GrowlMail's non-summary mode to not ask for the message body's attributed string on a secondary thread. This fixes an occasional crash with Safari 3, and a 100%-of-the-time crash with Safari 4 beta.

The fix is simply to only retrieve the message body object on a secondary thread, and then once we have it (or have given up on getting it), perform the rest of the job on the main thread.
     1 /*
     2  Copyright (c) The Growl Project, 2004-2005
     3  All rights reserved.
     4 
     5  Redistribution and use in source and binary forms, with or without modification,
     6  are permitted provided that the following conditions are met:
     7 
     8  1. Redistributions of source code must retain the above copyright
     9  notice, this list of conditions and the following disclaimer.
    10  2. Redistributions in binary form must reproduce the above copyright
    11  notice, this list of conditions and the following disclaimer in the
    12  documentation and/or other materials provided with the distribution.
    13  3. Neither the name of Growl nor the names of its contributors
    14  may be used to endorse or promote products derived from this software
    15  without specific prior written permission.
    16 
    17  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
    18  ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
    19  WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
    20  IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
    21  INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
    22  BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
    23  DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
    24  LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
    25  OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
    26  OF THE POSSIBILITY OF SUCH DAMAGE.
    27 */
    28 //
    29 //  GrowlMail.m
    30 //  GrowlMail
    31 //
    32 //  Created by Adam Iser on Mon Jul 26 2004.
    33 //  Copyright (c) 2004-2005 The Growl Project. All rights reserved.
    34 //
    35 
    36 #import "GrowlMail.h"
    37 #import "Message+GrowlMail.h"
    38 
    39 #import "MailHeaders.h"
    40 #import "MessageFrameworkHeaders.h"
    41 #import <objc/objc-class.h>
    42 
    43 typedef enum {
    44 	MODE_AUTO = 0,
    45 	MODE_SINGLE = 1,
    46 	MODE_SUMMARY = 2
    47 } GrowlMailModeType;
    48 
    49 #define AUTO_THRESHOLD	10
    50 
    51 #define	MAX_NOTIFICATION_THREADS	5
    52 
    53 static int	activeNotificationThreads = 0;
    54 
    55 //#define GROWL_MAIL_DEBUG
    56 
    57 NSBundle *GMGetGrowlMailBundle(void) {
    58 	return [NSBundle bundleForClass:[GrowlMail class]];
    59 }
    60 
    61 @implementation GrowlMail
    62 
    63 static int messageCopies = 0;
    64 
    65 #pragma mark Panic buttons
    66 
    67 //The purpose of this method is to shut down GrowlMail completely: we should not be notified of any messages, nor notify the user of any messages, after this message is called.
    68 - (void) shutDownGrowlMail {
    69 	[[NSNotificationCenter defaultCenter] removeObserver:self];
    70 	[GrowlApplicationBridge setGrowlDelegate:nil];
    71 }
    72 
    73 //This is a suicide pill. GrowlMail sends itself this message any time it detects a change in Mail's implementation, such as a missing method or an object of the wrong class.
    74 - (void) shutDownGrowlMailAndWarn:(NSString *)specificWarning {
    75 	NSLog(NSLocalizedString(@"WARNING: Mail is not behaving in the way that GrowlMail expects. This is probably because GrowlMail is incompatible with the version of Mail you're using. GrowlMail will now turn itself off. Please check the Growl website for a new version. If you're a programmer and want to debug this error, run gdb, load Mail, set a breakpoint on %s, and run.", /*comment*/ nil), __PRETTY_FUNCTION__);
    76 	if (specificWarning)
    77 		NSLog(@"Furthermore, the caller provided a more specific message: %@", specificWarning);
    78 
    79 	[self shutDownGrowlMail];
    80 }
    81 
    82 #pragma mark Boring bookkeeping stuff
    83 
    84 + (void) initialize {
    85 	[super initialize];
    86 
    87 	// this image is leaked
    88 	NSImage *image = [[NSImage alloc] initByReferencingFile:[GMGetGrowlMailBundle() pathForImageResource:@"GrowlMail"]];
    89 	[image setName:@"GrowlMail"];
    90 
    91 	[GrowlMail registerBundle];
    92 
    93 	NSNumber *automatic = [NSNumber numberWithInt:MODE_AUTO];
    94 	NSDictionary *defaultsDictionary = [[NSDictionary alloc] initWithObjectsAndKeys:
    95 		@"(%account) %sender",         @"GMTitleFormat",
    96 		@"%subject\n%body",            @"GMDescriptionFormat",
    97 		automatic,                     @"GMSummaryMode",
    98 		[NSNumber numberWithBool:YES], @"GMEnableGrowlMailBundle",
    99 		[NSNumber numberWithBool:NO],  @"GMInboxOnly",
   100 		nil];
   101 	[[NSUserDefaults standardUserDefaults] registerDefaults:defaultsDictionary];
   102 	[defaultsDictionary release];
   103 
   104 	NSLog(@"Loaded GrowlMail %@", [GMGetGrowlMailBundle() objectForInfoDictionaryKey:(NSString *)kCFBundleVersionKey]);
   105 }
   106 
   107 + (BOOL) hasPreferencesPanel {
   108 	return YES;
   109 }
   110 
   111 + (NSString *) preferencesOwnerClassName {
   112 	return @"GrowlMailPreferencesModule";
   113 }
   114 
   115 + (NSString *) preferencesPanelName {
   116 	return @"GrowlMail";
   117 }
   118 
   119 - (id) init {
   120 	if ((self = [super init])) {
   121 		NSString *privateFrameworksPath = [GMGetGrowlMailBundle() privateFrameworksPath];
   122 		NSString *growlBundlePath = [privateFrameworksPath stringByAppendingPathComponent:@"Growl.framework"];
   123 
   124 		NSBundle *growlBundle = [NSBundle bundleWithPath:growlBundlePath];
   125 		if (growlBundle) {
   126 			if ([growlBundle load]) {
   127 				// Register ourselves as a Growl delegate
   128 				[GrowlApplicationBridge setGrowlDelegate:self];
   129 
   130 				if ([GrowlApplicationBridge respondsToSelector:@selector(frameworkInfoDictionary)]) {
   131 					NSDictionary *infoDictionary = [GrowlApplicationBridge frameworkInfoDictionary];
   132 					NSLog(@"Using Growl.framework %@ (%@)",
   133 						  [infoDictionary objectForKey:@"CFBundleShortVersionString"],
   134 						  [infoDictionary objectForKey:(NSString *)kCFBundleVersionKey]);
   135 				} else {
   136 					NSLog(@"Using a version of Growl.framework older than 1.1. One of the other installed Mail plugins should be updated to Growl.framework 1.1 or later.");
   137 				}
   138 			}
   139 
   140 			[[NSNotificationCenter defaultCenter] addObserver:self
   141 													 selector:@selector(messageStoreDidAddMessages:)
   142 														 name:@"MessageStoreMessagesAdded_inMainThread_"
   143 													   object:nil];
   144 			[[NSNotificationCenter defaultCenter] addObserver:self
   145 													 selector:@selector(monitoredActivityStarted:)
   146 														 name:@"MonitoredActivityStarted_inMainThread_"
   147 													   object:nil];
   148 			[[NSNotificationCenter defaultCenter] addObserver:self
   149 													 selector:@selector(monitoredActivityEnded:)
   150 														 name:@"MonitoredActivityEnded_inMainThread_"
   151 													   object:nil];
   152 			
   153 #ifdef GROWL_MAIL_DEBUG
   154 			/*
   155 			[[NSNotificationCenter defaultCenter] addObserver:self
   156 													 selector:@selector(showAllNotifications:)
   157 														 name:nil object:nil];
   158 			 */
   159 #endif
   160 			
   161 		} else {
   162 			NSLog(@"Could not load Growl.framework, GrowlMail disabled");
   163 		}
   164 	}
   165 
   166 	return self;
   167 }
   168 
   169 - (void)showAllNotifications:(NSNotification *)notification
   170 {
   171 	if (([[notification name] rangeOfString:@"NSWindow"].location == NSNotFound) &&
   172 		([[notification name] rangeOfString:@"NSMouse"].location == NSNotFound) &&
   173 		([[notification name] rangeOfString:@"_NSThread"].location == NSNotFound)) {
   174 		NSLog(@"%@", notification);
   175 	}
   176 }
   177 
   178 - (void)monitoredActivityStarted:(NSNotification *)notification
   179 {
   180 	if ([[[notification object] description] isEqualToString:@"Copying messages"]) {
   181 		messageCopies++;
   182 #ifdef GROWL_MAIL_DEBUG
   183 		NSLog(@"Copying a message: messageCopies is now %i", messageCopies);
   184 #endif
   185 		if (messageCopies <= 0)
   186 			[self shutDownGrowlMailAndWarn:@"Number of message-copying operations overflowed. How on earth did you accomplish starting more than 2 billion copying operations at a time?!"];
   187 	}
   188 }
   189 
   190 - (void)monitoredActivityEnded:(NSNotification *)notification
   191 {
   192 	if ([[[notification object] description] isEqualToString:@"Copying messages"]) {
   193 		if (messageCopies <= 0)
   194 			[self shutDownGrowlMailAndWarn:@"Number of message-copying operations went below 0. It is not possible to have a negative number of copying operations!"];
   195 		messageCopies--;
   196 #ifdef GROWL_MAIL_DEBUG
   197 		NSLog(@"Finished copying a message: messageCopies is now %i", messageCopies);
   198 #endif
   199 	}
   200 }
   201 
   202 - (void) dealloc {
   203 	[self shutDownGrowlMail];
   204 	[[NSNotificationCenter defaultCenter] removeObserver:self];
   205 
   206 	[super dealloc];
   207 }
   208 
   209 #pragma mark GrowlApplicationBridge delegate methods
   210 
   211 - (NSString *) applicationNameForGrowl {
   212 	return @"GrowlMail";
   213 }
   214 
   215 - (NSImage *) applicationIconForGrowl {
   216 	return [NSImage imageNamed:@"NSApplicationIcon"];
   217 }
   218 
   219 - (void) growlNotificationWasClicked:(NSString *)clickContext {
   220 	if ([clickContext length]) {
   221 		//Make sure we have all the methods we need.
   222 		if (!class_getClassMethod([Library class], @selector(messageWithMessageID:)))
   223 			[self shutDownGrowlMailAndWarn:@"Library does not respond to +messageWithMessageID:"];
   224 		if (!class_getInstanceMethod([SingleMessageViewer class], @selector(initForViewingMessage:showAllHeaders:viewingState:fromDefaults:)))
   225 			[self shutDownGrowlMailAndWarn:@"SingleMessageViewer does not respond to -initForViewingMessage:showAllHeaders:viewingState:fromDefaults:"];
   226 		if (!class_getInstanceMethod([SingleMessageViewer class], @selector(showAndMakeKey:)))
   227 			[self shutDownGrowlMailAndWarn:@"SingleMessageViewer does not respond to -showAndMakeKey:"];
   228 
   229 		Message *message = [Library messageWithMessageID:clickContext];
   230 		MessageViewingState *viewingState = [[MessageViewingState alloc] init];
   231 		SingleMessageViewer *messageViewer = [[SingleMessageViewer alloc] initForViewingMessage:message showAllHeaders:NO viewingState:viewingState fromDefaults:NO];
   232 		[viewingState release];
   233 		[messageViewer showAndMakeKey:YES];
   234 		[messageViewer release];
   235 		[Library markMessageAsViewed:message];
   236 	}
   237 	[NSApp activateIgnoringOtherApps:YES];
   238 }
   239 
   240 - (NSDictionary *) registrationDictionaryForGrowl {
   241 	// Register our ticket with Growl
   242 	NSArray *allowedNotifications = [NSArray arrayWithObjects:
   243 		NEW_MAIL_NOTIFICATION,
   244 		NEW_JUNK_MAIL_NOTIFICATION,
   245 		NEW_NOTE_NOTIFICATION,
   246 		nil];
   247 	NSDictionary *humanReadableNames = [NSDictionary dictionaryWithObjectsAndKeys:
   248 										NSLocalizedStringFromTableInBundle(@"New mail", nil, GMGetGrowlMailBundle(), ""), NEW_MAIL_NOTIFICATION,
   249 										NSLocalizedStringFromTableInBundle(@"New junk mail", nil, GMGetGrowlMailBundle(), ""), NEW_JUNK_MAIL_NOTIFICATION,
   250 										NSLocalizedStringFromTableInBundle(@"New note", nil, GMGetGrowlMailBundle(), ""), NEW_NOTE_NOTIFICATION,
   251 										nil];
   252 	NSArray *defaultNotifications = [NSArray arrayWithObject:NEW_MAIL_NOTIFICATION];
   253 
   254 	NSDictionary *ticket = [NSDictionary dictionaryWithObjectsAndKeys:
   255 		allowedNotifications, GROWL_NOTIFICATIONS_ALL,
   256 		defaultNotifications, GROWL_NOTIFICATIONS_DEFAULT,
   257 		humanReadableNames, GROWL_NOTIFICATIONS_HUMAN_READABLE_NAMES,
   258 		nil];
   259 #ifdef GROWL_MAIL_DEBUG
   260 	NSLog(@"%s: Returning Growl dictionary %@", __PRETTY_FUNCTION__, ticket);
   261 #endif
   262 
   263 	return ticket;
   264 }
   265 
   266 #pragma mark Mail notification handlers
   267 
   268 + (void)showNotificationForMessage:(Message *)message
   269 {
   270 	if (activeNotificationThreads < MAX_NOTIFICATION_THREADS) { 
   271 		activeNotificationThreads++;
   272 		
   273 		/* Why use a thread?
   274 		 *
   275 		 * If we want the message body, it may not be immediately available.
   276 		 * It can be retrieved without blocking if it's available, which we initially try.
   277 		 * However, if we really, really want it, we may have to request it in a blocking fashion:
   278 		 *		for example, if the user doesn't read the message and doesn't have Mail set to download it automatically,
   279 		 *		we'll never get it without blocking.
   280 		 *
   281 		 * Blocking the main thread is, of course, out of the question.
   282 		 *
   283 		 * We're making some assumptions about Mail's internals, but the fact that notifications are posted on auxiliary threads
   284 		 * and then again with a _inMainThread_ suffix on the main thread indicates that threads are being used for mail access elsewhere.
   285 		 */
   286 		[NSThread detachNewThreadSelector:@selector(GMShowNotificationPart1)
   287 								 toTarget:message
   288 							   withObject:nil];
   289 	} else {
   290 		[self performSelector:@selector(showNotificationForMessage:)
   291 				   withObject:message
   292 				   afterDelay:2.0];
   293 	}
   294 }
   295 
   296 + (void)didFinishNotificationForMessage:(Message *)message
   297 {
   298 #pragma unused(message)
   299 	activeNotificationThreads--;	
   300 }
   301 
   302 - (void)messageStoreDidAddMessages:(NSNotification *)notification {
   303 	if (!GMIsEnabled()) return;
   304 
   305 #ifdef GROWL_MAIL_DEBUG
   306 	NSLog(@"%s called", __PRETTY_FUNCTION__);
   307 #endif
   308 	
   309 	if (messageCopies) {
   310 #ifdef GROWL_MAIL_DEBUG
   311 		NSLog(@"Ignoring because %i message copies are in process", messageCopies);
   312 #endif
   313 		return;
   314 	}
   315 
   316 	Library *store = [notification object];
   317 	if (!store) {
   318 		[self shutDownGrowlMailAndWarn:[NSString stringWithFormat:@"'%@' notification has no object", [notification name]]];
   319 	}
   320 	if ([store isKindOfClass:[LibraryStore class]]) {
   321 		//As of Tiger, this is normal; this notification is posted a couple times (perhaps once per inbox) with a LibraryStore object.
   322 		//This is not the notification we're looking for; we don't need to see its papers. We will move along now.
   323 		return;
   324 	}
   325 	//We don't actually use the store. We only retrieve it and examine it at all because we know we don't want the one with a LibraryStore as its object.
   326 	//The rest of the handler should be able to work just fine without proving anything else about the store, since it doesn't use the store.
   327 
   328 	NSDictionary *userInfo = [notification userInfo];
   329 	if (!userInfo) [self shutDownGrowlMailAndWarn:@"Notification had no userInfo"];
   330 
   331 	NSArray *mailboxes = [userInfo objectForKey:@"mailboxes"];
   332 #ifdef GROWL_MAIL_DEBUG
   333 	NSLog(@"%s: Adding messages to mailboxes %@", __PRETTY_FUNCTION__, mailboxes);
   334 #endif
   335 
   336 	//As of Tiger, it's normal for about half of these notifications to not have any mailboxes. We simply ignore the notification in this case.
   337 	if (!(mailboxes && [mailboxes count])) return;
   338 
   339 	//Ignore a notification if we're ignoring all of the mailboxes involved.
   340 	Class MailAccount_class = [MailAccount class];
   341 	if (!class_getClassMethod(MailAccount_class, @selector(draftMailboxUids)))
   342 		[self shutDownGrowlMailAndWarn:@"MailAccount does not respond to +draftMailboxUids"];
   343 	if (!class_getClassMethod(MailAccount_class, @selector(outboxMailboxUids)))
   344 		[self shutDownGrowlMailAndWarn:@"MailAccount does not respond to +outboxMailboxUids"];
   345 	if (!class_getClassMethod(MailAccount_class, @selector(sentMessagesMailboxUids)))
   346 		[self shutDownGrowlMailAndWarn:@"MailAccount does not respond to +sentMessagesMailboxUids"];
   347 	if (!class_getClassMethod(MailAccount_class, @selector(trashMailboxUids)))
   348 		[self shutDownGrowlMailAndWarn:@"MailAccount does not respond to +trashMailboxUids"];
   349 	//We need this method to support the Inbox Only preference.
   350 	if (!class_getClassMethod(MailAccount_class, @selector(inboxMailboxUids)))
   351 		[self shutDownGrowlMailAndWarn:@"MailAccount does not respond to +inboxMailboxUids"];
   352 
   353 	//Ignore messages being written.
   354 	NSMutableSet *mailboxesToIgnore = [NSMutableSet setWithArray:[MailAccount draftMailboxUids]];
   355 	//Ignore messages being sent.
   356 	[mailboxesToIgnore unionSet:[NSSet setWithArray:[MailAccount outboxMailboxUids]]];
   357 	[mailboxesToIgnore unionSet:[NSSet setWithArray:[MailAccount sentMessagesMailboxUids]]];
   358 	//Ignore messages being deleted.
   359 	[mailboxesToIgnore unionSet:[NSSet setWithArray:[MailAccount trashMailboxUids]]];
   360 
   361 	NSSet *mailboxesSet = [NSSet setWithArray:mailboxes];
   362 	NSMutableSet *mailboxesNotIgnored = [[mailboxesSet mutableCopy] autorelease];
   363 	[mailboxesNotIgnored minusSet:mailboxesToIgnore];
   364 	if ([mailboxesNotIgnored count] == 0U)
   365 		return;
   366 
   367 	NSArray *messages = [userInfo objectForKey:@"messages"];
   368 	if (!messages) [self shutDownGrowlMailAndWarn:@"Notification's userInfo has no messages"];
   369 	
   370 #ifdef GROWL_MAIL_DEBUG
   371 	NSLog(@"%s: Mail added messages [1] to mailboxes [2].\n[1]: %@\n[2]: %@", __PRETTY_FUNCTION__, messages, mailboxes);
   372 #endif
   373 	
   374 	unsigned count = [messages count];
   375 
   376 	int summaryMode = GMSummaryMode();
   377 	if (summaryMode == MODE_AUTO) {
   378 		if (count >= AUTO_THRESHOLD)
   379 			summaryMode = MODE_SUMMARY;
   380 		else
   381 			summaryMode = MODE_SINGLE;
   382 	}
   383 
   384 #ifdef GROWL_MAIL_DEBUG
   385 	NSLog(@"Got %i new messages. Summary mode was %i and is now %i", count, GMSummaryMode(), summaryMode);
   386 #endif
   387 
   388 	Class Message_class = [Message class];
   389 
   390 	switch (summaryMode) {
   391 		default:
   392 		case MODE_SINGLE: {
   393 			NSEnumerator *messagesEnum = [messages objectEnumerator];
   394 			Message *message;
   395 			while ((message = [messagesEnum nextObject])) {
   396 				MailboxUid *mailbox = [message mailbox];
   397 				//If this mailbox is not an inbox, and we only care about inboxes, then skip this message.
   398 				if (GMInboxOnly() && ![[MailAccount inboxMailboxUids] containsObject:mailbox])
   399 					continue;
   400 
   401 				MailAccount *account = [mailbox account];
   402 				if (![self isAccountEnabled:account])
   403 					continue;
   404 
   405 				if (![message isKindOfClass:Message_class])
   406 					[self shutDownGrowlMailAndWarn:[NSString stringWithFormat:@"Message in notification was not a Message; it is %@", message]];
   407 
   408 				[[self class] showNotificationForMessage:message];
   409 			}
   410 			break;
   411 		}
   412 		case MODE_SUMMARY: {
   413 			if (!class_getClassMethod([MailAccount class], @selector(mailAccounts)))
   414 				[self shutDownGrowlMailAndWarn:@"MailAccount does not respond to +mailAccounts"];
   415 			if (!class_getInstanceMethod(Message_class, @selector(mailbox)))
   416 				[self shutDownGrowlMailAndWarn:@"Message does not respond to -mailbox"];
   417 
   418 			NSArray *accounts = [MailAccount mailAccounts];
   419 			unsigned accountsCount = [accounts count];
   420 			NSCountedSet *accountSummary = [NSCountedSet setWithCapacity:accountsCount];
   421 			NSCountedSet *accountJunkSummary = [NSCountedSet setWithCapacity:accountsCount];
   422 			NSEnumerator *messagesEnum = [messages objectEnumerator];
   423 			NSArray *junkMailboxUids = [MailAccount junkMailboxUids];
   424 			Message *message;
   425 			while ((message = [messagesEnum nextObject])) {
   426 				MailboxUid *mailbox = [message mailbox];
   427 				//If this mailbox is not an inbox, and we only care about inboxes, then skip this message.
   428 				if (GMInboxOnly() && ![[MailAccount inboxMailboxUids] containsObject:mailbox])
   429 					continue;
   430 
   431 				MailAccount *account = [mailbox account];
   432 				if (![self isAccountEnabled:account])
   433 					continue;
   434 
   435 				if (([message isJunk]) || [junkMailboxUids containsObject:[message mailbox]])
   436 					[accountJunkSummary addObject:account];
   437 				else
   438 					[accountSummary addObject:account];
   439 			}
   440 			NSString *title = NSLocalizedStringFromTableInBundle(@"New mail", NULL, GMGetGrowlMailBundle(), "");
   441 			NSString *titleJunk = NSLocalizedStringFromTableInBundle(@"New junk mail", NULL, GMGetGrowlMailBundle(), "");
   442 			NSString *description;
   443 
   444 			MailAccount *account;
   445 
   446 			NSEnumerator *accountSummaryEnum = [accountSummary objectEnumerator];
   447 			while ((account = [accountSummaryEnum nextObject])) {
   448 				if (![self isAccountEnabled:account])
   449 					continue;
   450 
   451 				unsigned summaryCount = [accountSummary countForObject:account];
   452 				if (summaryCount) {
   453 					if (summaryCount == 1) {
   454 						description = [NSString stringWithFormat:NSLocalizedStringFromTableInBundle(@"%@ \n 1 new mail", NULL, GMGetGrowlMailBundle(), "%@ is an account name"), [account displayName]];
   455 					} else {
   456 						description = [NSString stringWithFormat:NSLocalizedStringFromTableInBundle(@"%@ \n %u new mails", NULL, GMGetGrowlMailBundle(), "%@ is an account name; %u becomes a number"), [account displayName], summaryCount];
   457 					}
   458 					[GrowlApplicationBridge notifyWithTitle:title
   459 												description:description
   460 										   notificationName:NEW_MAIL_NOTIFICATION
   461 												   iconData:nil
   462 												   priority:0
   463 												   isSticky:NO
   464 											   clickContext:@""];	// non-nil click context
   465 				}
   466 			}
   467 
   468 			NSEnumerator *accountJunkSummaryEnum = [accountJunkSummary objectEnumerator];
   469 			while ((account = [accountJunkSummaryEnum nextObject])) {
   470 				if (![self isAccountEnabled:account])
   471 					continue;
   472 
   473 				unsigned summaryCount = [accountJunkSummary countForObject:account];
   474 				if (summaryCount) {
   475 					if (summaryCount == 1) {
   476 						description = [NSString stringWithFormat:NSLocalizedStringFromTableInBundle(@"%@ \n 1 new mail", NULL, GMGetGrowlMailBundle(), "%@ is an account name"), [account displayName]];
   477 					} else {
   478 						description = [NSString stringWithFormat:NSLocalizedStringFromTableInBundle(@"%@ \n %u new mails", NULL, GMGetGrowlMailBundle(), "%@ is an account name; %u becomes a number"), [account displayName], summaryCount];
   479 					}					
   480 					[GrowlApplicationBridge notifyWithTitle:titleJunk
   481 												description:description
   482 										   notificationName:NEW_JUNK_MAIL_NOTIFICATION
   483 												   iconData:nil
   484 												   priority:0
   485 												   isSticky:NO
   486 											   clickContext:@""];	// non-nil click context
   487 				}
   488 			}
   489 			break;
   490 		}
   491 	}
   492 }
   493 
   494 #pragma mark Preferences
   495 
   496 - (BOOL) isAccountEnabled:(MailAccount *)account {
   497 	BOOL isEnabled = YES;
   498 	NSDictionary *accountSettings = [[NSUserDefaults standardUserDefaults] objectForKey:@"GMAccounts"];
   499 	if (accountSettings) {
   500 		NSNumber *value = [accountSettings objectForKey:[account path]];
   501 		if (value)
   502 			isEnabled = [value boolValue];
   503 	}
   504 	return isEnabled;
   505 }
   506 
   507 - (void) setAccount:(MailAccount *)account enabled:(BOOL)yesOrNo {
   508 	NSDictionary *accountSettings = [[NSUserDefaults standardUserDefaults] objectForKey:@"GMAccounts"];
   509 	NSMutableDictionary *newSettings = [[accountSettings mutableCopy] autorelease];
   510 	if (!newSettings)
   511 		newSettings = [NSMutableDictionary dictionaryWithCapacity:1U];
   512 	[newSettings setObject:[NSNumber numberWithBool:yesOrNo] forKey:[account path]];
   513 	[[NSUserDefaults standardUserDefaults] setObject:newSettings forKey:@"GMAccounts"];
   514 }
   515 
   516 @end
   517 
   518 BOOL GMIsEnabled(void) {
   519 	NSNumber *enabledNum = [[NSUserDefaults standardUserDefaults] objectForKey:@"GMEnableGrowlMailBundle"];
   520 	return enabledNum ? [enabledNum boolValue] : YES;
   521 }
   522 
   523 int GMSummaryMode(void) {
   524 	NSNumber *summaryModeNum = [[NSUserDefaults standardUserDefaults] objectForKey:@"GMSummaryMode"];
   525 	return summaryModeNum ? [summaryModeNum intValue] : MODE_AUTO;
   526 }
   527 
   528 BOOL GMInboxOnly(void) {
   529 	NSNumber *inboxOnlyNum = [[NSUserDefaults standardUserDefaults] objectForKey:@"GMInboxOnly"];
   530 	return inboxOnlyNum ? [inboxOnlyNum boolValue] : YES;
   531 }
   532 
   533 NSString *GMTitleFormatString(void) {
   534 	NSString *titleFormat = [[NSUserDefaults standardUserDefaults] stringForKey:@"GMTitleFormat"];
   535 	return titleFormat ? titleFormat : @"(%account) %sender";
   536 }
   537 
   538 NSString *GMDescriptionFormatString(void) {
   539 	NSString *descriptionFormat = [[NSUserDefaults standardUserDefaults] stringForKey:@"GMDescriptionFormat"];
   540 	return descriptionFormat ? descriptionFormat : @"%subject\n%body";
   541 }