Extras/GrowlMail/GrowlMail.m
author Evan Schoenberg
Sun Aug 24 17:16:35 2008 -0400 (2008-08-24)
changeset 4175 b19d240b576a
parent 4056 8ba86acdc221
child 4178 9ea7cd00514b
permissions -rw-r--r--
Don't display Growl notifications for emails which are already marked as read when they are received.

This is a huge usability improvement for my use case and I imagine for that of many others, as I read emails on my phone and other computers throughout the day and don't like my screen flooded with useless notifications when I return and check email.
     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(showNotification)
   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 				if (![message respondsToSelector:@selector(isRead)] || ![message isRead]) {
   409 					/* Don't display read messages */
   410 					[[self class] showNotificationForMessage:message];
   411 				}
   412 			}
   413 			break;
   414 		}
   415 		case MODE_SUMMARY: {
   416 			if (!class_getClassMethod([MailAccount class], @selector(mailAccounts)))
   417 				[self shutDownGrowlMailAndWarn:@"MailAccount does not respond to +mailAccounts"];
   418 			if (!class_getInstanceMethod(Message_class, @selector(mailbox)))
   419 				[self shutDownGrowlMailAndWarn:@"Message does not respond to -mailbox"];
   420 
   421 			NSArray *accounts = [MailAccount mailAccounts];
   422 			unsigned accountsCount = [accounts count];
   423 			NSCountedSet *accountSummary = [NSCountedSet setWithCapacity:accountsCount];
   424 			NSCountedSet *accountJunkSummary = [NSCountedSet setWithCapacity:accountsCount];
   425 			NSEnumerator *messagesEnum = [messages objectEnumerator];
   426 			NSArray *junkMailboxUids = [MailAccount junkMailboxUids];
   427 			Message *message;
   428 			while ((message = [messagesEnum nextObject])) {
   429 				MailboxUid *mailbox = [message mailbox];
   430 				//If this mailbox is not an inbox, and we only care about inboxes, then skip this message.
   431 				if (GMInboxOnly() && ![[MailAccount inboxMailboxUids] containsObject:mailbox])
   432 					continue;
   433 
   434 				MailAccount *account = [mailbox account];
   435 				if (![self isAccountEnabled:account])
   436 					continue;
   437 
   438 				if (([message isJunk]) || [junkMailboxUids containsObject:[message mailbox]])
   439 					[accountJunkSummary addObject:account];
   440 				else
   441 					[accountSummary addObject:account];
   442 			}
   443 			NSString *title = NSLocalizedStringFromTableInBundle(@"New mail", NULL, GMGetGrowlMailBundle(), "");
   444 			NSString *titleJunk = NSLocalizedStringFromTableInBundle(@"New junk mail", NULL, GMGetGrowlMailBundle(), "");
   445 			NSString *description;
   446 
   447 			MailAccount *account;
   448 
   449 			NSEnumerator *accountSummaryEnum = [accountSummary objectEnumerator];
   450 			while ((account = [accountSummaryEnum nextObject])) {
   451 				if (![self isAccountEnabled:account])
   452 					continue;
   453 
   454 				unsigned summaryCount = [accountSummary countForObject:account];
   455 				if (summaryCount) {
   456 					if (summaryCount == 1) {
   457 						description = [NSString stringWithFormat:NSLocalizedStringFromTableInBundle(@"%@ \n 1 new mail", NULL, GMGetGrowlMailBundle(), "%@ is an account name"), [account displayName]];
   458 					} else {
   459 						description = [NSString stringWithFormat:NSLocalizedStringFromTableInBundle(@"%@ \n %u new mails", NULL, GMGetGrowlMailBundle(), "%@ is an account name; %u becomes a number"), [account displayName], summaryCount];
   460 					}
   461 					[GrowlApplicationBridge notifyWithTitle:title
   462 												description:description
   463 										   notificationName:NEW_MAIL_NOTIFICATION
   464 												   iconData:nil
   465 												   priority:0
   466 												   isSticky:NO
   467 											   clickContext:@""];	// non-nil click context
   468 				}
   469 			}
   470 
   471 			NSEnumerator *accountJunkSummaryEnum = [accountJunkSummary objectEnumerator];
   472 			while ((account = [accountJunkSummaryEnum nextObject])) {
   473 				if (![self isAccountEnabled:account])
   474 					continue;
   475 
   476 				unsigned summaryCount = [accountJunkSummary countForObject:account];
   477 				if (summaryCount) {
   478 					if (summaryCount == 1) {
   479 						description = [NSString stringWithFormat:NSLocalizedStringFromTableInBundle(@"%@ \n 1 new mail", NULL, GMGetGrowlMailBundle(), "%@ is an account name"), [account displayName]];
   480 					} else {
   481 						description = [NSString stringWithFormat:NSLocalizedStringFromTableInBundle(@"%@ \n %u new mails", NULL, GMGetGrowlMailBundle(), "%@ is an account name; %u becomes a number"), [account displayName], summaryCount];
   482 					}					
   483 					[GrowlApplicationBridge notifyWithTitle:titleJunk
   484 												description:description
   485 										   notificationName:NEW_JUNK_MAIL_NOTIFICATION
   486 												   iconData:nil
   487 												   priority:0
   488 												   isSticky:NO
   489 											   clickContext:@""];	// non-nil click context
   490 				}
   491 			}
   492 			break;
   493 		}
   494 	}
   495 }
   496 
   497 #pragma mark Preferences
   498 
   499 - (BOOL) isAccountEnabled:(MailAccount *)account {
   500 	BOOL isEnabled = YES;
   501 	NSDictionary *accountSettings = [[NSUserDefaults standardUserDefaults] objectForKey:@"GMAccounts"];
   502 	if (accountSettings) {
   503 		NSNumber *value = [accountSettings objectForKey:[account path]];
   504 		if (value)
   505 			isEnabled = [value boolValue];
   506 	}
   507 	return isEnabled;
   508 }
   509 
   510 - (void) setAccount:(MailAccount *)account enabled:(BOOL)yesOrNo {
   511 	NSDictionary *accountSettings = [[NSUserDefaults standardUserDefaults] objectForKey:@"GMAccounts"];
   512 	NSMutableDictionary *newSettings = [[accountSettings mutableCopy] autorelease];
   513 	if (!newSettings)
   514 		newSettings = [NSMutableDictionary dictionaryWithCapacity:1U];
   515 	[newSettings setObject:[NSNumber numberWithBool:yesOrNo] forKey:[account path]];
   516 	[[NSUserDefaults standardUserDefaults] setObject:newSettings forKey:@"GMAccounts"];
   517 }
   518 
   519 @end
   520 
   521 BOOL GMIsEnabled(void) {
   522 	NSNumber *enabledNum = [[NSUserDefaults standardUserDefaults] objectForKey:@"GMEnableGrowlMailBundle"];
   523 	return enabledNum ? [enabledNum boolValue] : YES;
   524 }
   525 
   526 int GMSummaryMode(void) {
   527 	NSNumber *summaryModeNum = [[NSUserDefaults standardUserDefaults] objectForKey:@"GMSummaryMode"];
   528 	return summaryModeNum ? [summaryModeNum intValue] : MODE_AUTO;
   529 }
   530 
   531 BOOL GMInboxOnly(void) {
   532 	NSNumber *inboxOnlyNum = [[NSUserDefaults standardUserDefaults] objectForKey:@"GMInboxOnly"];
   533 	return inboxOnlyNum ? [inboxOnlyNum boolValue] : YES;
   534 }
   535 
   536 NSString *GMTitleFormatString(void) {
   537 	NSString *titleFormat = [[NSUserDefaults standardUserDefaults] stringForKey:@"GMTitleFormat"];
   538 	return titleFormat ? titleFormat : @"(%account) %sender";
   539 }
   540 
   541 NSString *GMDescriptionFormatString(void) {
   542 	NSString *descriptionFormat = [[NSUserDefaults standardUserDefaults] stringForKey:@"GMDescriptionFormat"];
   543 	return descriptionFormat ? descriptionFormat : @"%subject\n%body";
   544 }