Extras/GrowlMail/GrowlMailNotifier.m
author Peter Hosey <hg@boredzo.org>
Sat Jun 06 21:30:17 2009 -0700 (2009-06-06)
changeset 4209 f8a902d33769
child 4210 c36ddc8e6b22
permissions -rw-r--r--
Refactored GrowlMail to have a separate singleton notifier object, in order to make the mail-bundle class less dependent on being a singleton. (Assumptions are bad, especially when you're working with private/undocumented APIs.)
     1 //
     2 //  GrowlMailNotifier.m
     3 //  GrowlMail
     4 //
     5 //  Created by Peter Hosey on 2009-05-10.
     6 //  Copyright 2009 Peter Hosey. All rights reserved.
     7 //
     8 
     9 #import "GrowlMailNotifier.h"
    10 #import "GrowlMail.h"
    11 #import "Message+GrowlMail.h"
    12 #import <objc/objc-runtime.h>
    13 
    14 #import "MessageFrameworkHeaders.h"
    15 
    16 #define AUTO_THRESHOLD	10
    17 
    18 #define	MAX_NOTIFICATION_THREADS	5
    19 
    20 static int activeNotificationThreads = 0;
    21 
    22 static int messageCopies = 0;
    23 
    24 static GrowlMailNotifier *sharedNotifier = nil;
    25 
    26 static BOOL notifierEnabled = YES;
    27 
    28 @implementation GrowlMailNotifier
    29 
    30 #pragma mark Panic buttons
    31 
    32 //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.
    33 - (void) shutDownGrowlMail {
    34 	[[NSNotificationCenter defaultCenter] removeObserver:self];
    35 	[GrowlApplicationBridge setGrowlDelegate:nil];
    36 
    37 	//Prevent ourselves from re-enabling later.
    38 	notifierEnabled = NO;
    39 }
    40 
    41 //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.
    42 - (void) shutDownGrowlMailAndWarn:(NSString *)specificWarning {
    43 	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__);
    44 	if (specificWarning)
    45 		NSLog(@"Furthermore, the caller provided a more specific message: %@", specificWarning);
    46 
    47 	[self shutDownGrowlMail];
    48 }
    49 
    50 #pragma mark The circle of life
    51 
    52 + (id) sharedNotifier {
    53 	if (!sharedNotifier) {
    54 		//-init and -dealloc will each assign to sharedNotifier.
    55 		[[[GrowlMailNotifier alloc] init] autorelease];
    56 	}
    57 	return sharedNotifier;
    58 }
    59 
    60 - (id) init {
    61 	if (sharedNotifier) {
    62 		[self release];
    63 		return [sharedNotifier retain];
    64 	}
    65 
    66 	//No shared notifier yet; someone is trying to create one. If we previously disabled ourselves, abort this attempt.
    67 	if (!notifierEnabled) {
    68 		[self release];
    69 		return nil;
    70 	}
    71 
    72 	if((self = [super init])) {
    73 		NSNumber *automatic = [NSNumber numberWithInt:GrowlMailSummaryModeAutomatic];
    74 		NSDictionary *defaultsDictionary = [NSDictionary dictionaryWithObjectsAndKeys:
    75 			@"(%account) %sender",         @"GMTitleFormat",
    76 			@"%subject\n%body",            @"GMDescriptionFormat",
    77 			automatic,                     @"GMSummaryMode",
    78 			[NSNumber numberWithBool:YES], @"GMEnableGrowlMailBundle",
    79 			[NSNumber numberWithBool:NO],  @"GMInboxOnly",
    80 			nil];
    81 		[[NSUserDefaults standardUserDefaults] registerDefaults:defaultsDictionary];
    82 
    83 		[GrowlApplicationBridge setGrowlDelegate:self];
    84 
    85 		[[NSNotificationCenter defaultCenter] addObserver:self
    86 												 selector:@selector(messageStoreDidAddMessages:)
    87 													 name:@"MessageStoreMessagesAdded_inMainThread_"
    88 												   object:nil];
    89 		[[NSNotificationCenter defaultCenter] addObserver:self
    90 												 selector:@selector(monitoredActivityStarted:)
    91 													 name:@"MonitoredActivityStarted_inMainThread_"
    92 												   object:nil];
    93 		[[NSNotificationCenter defaultCenter] addObserver:self
    94 												 selector:@selector(monitoredActivityEnded:)
    95 													 name:@"MonitoredActivityEnded_inMainThread_"
    96 												   object:nil];
    97 		
    98 #ifdef GROWL_MAIL_DEBUG
    99 		/*
   100 		[[NSNotificationCenter defaultCenter] addObserver:self
   101 												 selector:@selector(showAllNotifications:)
   102 													 name:nil object:nil];
   103 		 */
   104 #endif
   105 		sharedNotifier = self;
   106 	}
   107 	return self;
   108 }
   109 
   110 - (void) dealloc {
   111 	[self shutDownGrowlMail];
   112 	[[NSNotificationCenter defaultCenter] removeObserver:self];
   113 	sharedNotifier = nil;
   114 
   115 	[super dealloc];
   116 }
   117 
   118 #pragma mark GrowlApplicationBridge delegate methods
   119 
   120 - (NSString *) applicationNameForGrowl {
   121 	return @"GrowlMail";
   122 }
   123 
   124 - (NSImage *) applicationIconForGrowl {
   125 	return [NSImage imageNamed:@"NSApplicationIcon"];
   126 }
   127 
   128 - (void) growlNotificationWasClicked:(NSString *)clickContext {
   129 	if ([clickContext length]) {
   130 		//Make sure we have all the methods we need.
   131 		if (!class_getClassMethod([Library class], @selector(messageWithMessageID:)))
   132 			[self shutDownGrowlMailAndWarn:@"Library does not respond to +messageWithMessageID:"];
   133 		if (!class_getInstanceMethod([SingleMessageViewer class], @selector(initForViewingMessage:showAllHeaders:viewingState:fromDefaults:)))
   134 			[self shutDownGrowlMailAndWarn:@"SingleMessageViewer does not respond to -initForViewingMessage:showAllHeaders:viewingState:fromDefaults:"];
   135 		if (!class_getInstanceMethod([SingleMessageViewer class], @selector(showAndMakeKey:)))
   136 			[self shutDownGrowlMailAndWarn:@"SingleMessageViewer does not respond to -showAndMakeKey:"];
   137 
   138 		Message *message = [Library messageWithMessageID:clickContext];
   139 		MessageViewingState *viewingState = [[MessageViewingState alloc] init];
   140 		SingleMessageViewer *messageViewer = [[SingleMessageViewer alloc] initForViewingMessage:message showAllHeaders:NO viewingState:viewingState fromDefaults:NO];
   141 		[viewingState release];
   142 		[messageViewer showAndMakeKey:YES];
   143 		[messageViewer release];
   144 		[Library markMessageAsViewed:message];
   145 	}
   146 	[NSApp activateIgnoringOtherApps:YES];
   147 }
   148 
   149 - (NSDictionary *) registrationDictionaryForGrowl {
   150 	// Register our ticket with Growl
   151 	NSArray *allowedNotifications = [NSArray arrayWithObjects:
   152 		NEW_MAIL_NOTIFICATION,
   153 		NEW_JUNK_MAIL_NOTIFICATION,
   154 		NEW_NOTE_NOTIFICATION,
   155 		nil];
   156 	NSDictionary *humanReadableNames = [NSDictionary dictionaryWithObjectsAndKeys:
   157 										NSLocalizedStringFromTableInBundle(@"New mail", nil, GMGetGrowlMailBundle(), ""), NEW_MAIL_NOTIFICATION,
   158 										NSLocalizedStringFromTableInBundle(@"New junk mail", nil, GMGetGrowlMailBundle(), ""), NEW_JUNK_MAIL_NOTIFICATION,
   159 										NSLocalizedStringFromTableInBundle(@"New note", nil, GMGetGrowlMailBundle(), ""), NEW_NOTE_NOTIFICATION,
   160 										nil];
   161 	NSArray *defaultNotifications = [NSArray arrayWithObject:NEW_MAIL_NOTIFICATION];
   162 
   163 	NSDictionary *ticket = [NSDictionary dictionaryWithObjectsAndKeys:
   164 		allowedNotifications, GROWL_NOTIFICATIONS_ALL,
   165 		defaultNotifications, GROWL_NOTIFICATIONS_DEFAULT,
   166 		humanReadableNames, GROWL_NOTIFICATIONS_HUMAN_READABLE_NAMES,
   167 		nil];
   168 #ifdef GROWL_MAIL_DEBUG
   169 	NSLog(@"%s: Returning Growl dictionary %@", __PRETTY_FUNCTION__, ticket);
   170 #endif
   171 
   172 	return ticket;
   173 }
   174 
   175 #pragma mark Mail notification handlers
   176 
   177 + (void)showNotificationForMessage:(Message *)message
   178 {
   179 	if (activeNotificationThreads < MAX_NOTIFICATION_THREADS) { 
   180 		activeNotificationThreads++;
   181 		
   182 		/* Why use a thread?
   183 		 *
   184 		 * If we want the message body, it may not be immediately available.
   185 		 * It can be retrieved without blocking if it's available, which we initially try.
   186 		 * However, if we really, really want it, we may have to request it in a blocking fashion:
   187 		 *		for example, if the user doesn't read the message and doesn't have Mail set to download it automatically,
   188 		 *		we'll never get it without blocking.
   189 		 *
   190 		 * Blocking the main thread is, of course, out of the question.
   191 		 *
   192 		 * We're making some assumptions about Mail's internals, but the fact that notifications are posted on auxiliary threads
   193 		 * and then again with a _inMainThread_ suffix on the main thread indicates that threads are being used for mail access elsewhere.
   194 		 */
   195 		[NSThread detachNewThreadSelector:@selector(GMShowNotificationPart1)
   196 								 toTarget:message
   197 							   withObject:nil];
   198 	} else {
   199 		[self performSelector:@selector(showNotificationForMessage:)
   200 				   withObject:message
   201 				   afterDelay:2.0];
   202 	}
   203 }
   204 
   205 - (void)didFinishNotificationForMessage:(Message *)message
   206 {
   207 #pragma unused(message)
   208 	activeNotificationThreads--;	
   209 }
   210 
   211 - (void)messageStoreDidAddMessages:(NSNotification *)notification {
   212 	if (![self isEnabled]) return;
   213 
   214 #ifdef GROWL_MAIL_DEBUG
   215 	NSLog(@"%s called", __PRETTY_FUNCTION__);
   216 #endif
   217 	
   218 	if (messageCopies) {
   219 #ifdef GROWL_MAIL_DEBUG
   220 		NSLog(@"Ignoring because %i message copies are in process", messageCopies);
   221 #endif
   222 		return;
   223 	}
   224 
   225 	Library *store = [notification object];
   226 	if (!store) {
   227 		[self shutDownGrowlMailAndWarn:[NSString stringWithFormat:@"'%@' notification has no object", [notification name]]];
   228 	}
   229 	if ([store isKindOfClass:[LibraryStore class]]) {
   230 		//As of Tiger, this is normal; this notification is posted a couple times (perhaps once per inbox) with a LibraryStore object.
   231 		//This is not the notification we're looking for; we don't need to see its papers. We will move along now.
   232 		return;
   233 	}
   234 	//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.
   235 	//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.
   236 
   237 	NSDictionary *userInfo = [notification userInfo];
   238 	if (!userInfo) [self shutDownGrowlMailAndWarn:@"Notification had no userInfo"];
   239 
   240 	NSArray *mailboxes = [userInfo objectForKey:@"mailboxes"];
   241 #ifdef GROWL_MAIL_DEBUG
   242 	NSLog(@"%s: Adding messages to mailboxes %@", __PRETTY_FUNCTION__, mailboxes);
   243 #endif
   244 
   245 	//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.
   246 	if (!(mailboxes && [mailboxes count])) return;
   247 
   248 	//Ignore a notification if we're ignoring all of the mailboxes involved.
   249 	Class MailAccount_class = [MailAccount class];
   250 	if (!class_getClassMethod(MailAccount_class, @selector(draftMailboxUids)))
   251 		[self shutDownGrowlMailAndWarn:@"MailAccount does not respond to +draftMailboxUids"];
   252 	if (!class_getClassMethod(MailAccount_class, @selector(outboxMailboxUids)))
   253 		[self shutDownGrowlMailAndWarn:@"MailAccount does not respond to +outboxMailboxUids"];
   254 	if (!class_getClassMethod(MailAccount_class, @selector(sentMessagesMailboxUids)))
   255 		[self shutDownGrowlMailAndWarn:@"MailAccount does not respond to +sentMessagesMailboxUids"];
   256 	if (!class_getClassMethod(MailAccount_class, @selector(trashMailboxUids)))
   257 		[self shutDownGrowlMailAndWarn:@"MailAccount does not respond to +trashMailboxUids"];
   258 	//We need this method to support the Inbox Only preference.
   259 	if (!class_getClassMethod(MailAccount_class, @selector(inboxMailboxUids)))
   260 		[self shutDownGrowlMailAndWarn:@"MailAccount does not respond to +inboxMailboxUids"];
   261 
   262 	//Ignore messages being written.
   263 	NSMutableSet *mailboxesToIgnore = [NSMutableSet setWithArray:[MailAccount draftMailboxUids]];
   264 	//Ignore messages being sent.
   265 	[mailboxesToIgnore unionSet:[NSSet setWithArray:[MailAccount outboxMailboxUids]]];
   266 	[mailboxesToIgnore unionSet:[NSSet setWithArray:[MailAccount sentMessagesMailboxUids]]];
   267 	//Ignore messages being deleted.
   268 	[mailboxesToIgnore unionSet:[NSSet setWithArray:[MailAccount trashMailboxUids]]];
   269 
   270 	NSSet *mailboxesSet = [NSSet setWithArray:mailboxes];
   271 	NSMutableSet *mailboxesNotIgnored = [[mailboxesSet mutableCopy] autorelease];
   272 	[mailboxesNotIgnored minusSet:mailboxesToIgnore];
   273 	if ([mailboxesNotIgnored count] == 0U)
   274 		return;
   275 
   276 	NSArray *messages = [userInfo objectForKey:@"messages"];
   277 	if (!messages) [self shutDownGrowlMailAndWarn:@"Notification's userInfo has no messages"];
   278 	
   279 #ifdef GROWL_MAIL_DEBUG
   280 	NSLog(@"%s: Mail added messages [1] to mailboxes [2].\n[1]: %@\n[2]: %@", __PRETTY_FUNCTION__, messages, mailboxes);
   281 #endif
   282 	
   283 	unsigned count = [messages count];
   284 
   285 	int summaryMode = [self summaryMode];
   286 	if (summaryMode == GrowlMailSummaryModeAutomatic) {
   287 		if (count >= AUTO_THRESHOLD)
   288 			summaryMode = GrowlMailSummaryModeAlways;
   289 		else
   290 			summaryMode = GrowlMailSummaryModeDisabled;
   291 	}
   292 
   293 #ifdef GROWL_MAIL_DEBUG
   294 	NSLog(@"Got %i new messages. Summary mode was %i and is now %i", count, [self summaryMode], summaryMode);
   295 #endif
   296 
   297 	Class Message_class = [Message class];
   298 
   299 	switch (summaryMode) {
   300 		default:
   301 		case GrowlMailSummaryModeDisabled: {
   302 			NSEnumerator *messagesEnum = [messages objectEnumerator];
   303 			Message *message;
   304 			while ((message = [messagesEnum nextObject])) {
   305 				MailboxUid *mailbox = [message mailbox];
   306 				//If this mailbox is not an inbox, and we only care about inboxes, then skip this message.
   307 				if ([self inboxOnly] && ![[MailAccount inboxMailboxUids] containsObject:mailbox])
   308 					continue;
   309 
   310 				MailAccount *account = [mailbox account];
   311 				if (![self isAccountEnabled:account])
   312 					continue;
   313 
   314 				if (![message isKindOfClass:Message_class])
   315 					[self shutDownGrowlMailAndWarn:[NSString stringWithFormat:@"Message in notification was not a Message; it is %@", message]];
   316 
   317 				if (![message respondsToSelector:@selector(isRead)] || ![message isRead]) {
   318 					/* Don't display read messages */
   319 					[[self class] showNotificationForMessage:message];
   320 				}
   321 			}
   322 			break;
   323 		}
   324 		case GrowlMailSummaryModeAlways: {
   325 			if (!class_getClassMethod([MailAccount class], @selector(mailAccounts)))
   326 				[self shutDownGrowlMailAndWarn:@"MailAccount does not respond to +mailAccounts"];
   327 			if (!class_getInstanceMethod(Message_class, @selector(mailbox)))
   328 				[self shutDownGrowlMailAndWarn:@"Message does not respond to -mailbox"];
   329 
   330 			NSArray *accounts = [MailAccount mailAccounts];
   331 			unsigned accountsCount = [accounts count];
   332 			NSCountedSet *accountSummary = [NSCountedSet setWithCapacity:accountsCount];
   333 			NSCountedSet *accountJunkSummary = [NSCountedSet setWithCapacity:accountsCount];
   334 			NSEnumerator *messagesEnum = [messages objectEnumerator];
   335 			NSArray *junkMailboxUids = [MailAccount junkMailboxUids];
   336 			Message *message;
   337 			while ((message = [messagesEnum nextObject])) {
   338 				MailboxUid *mailbox = [message mailbox];
   339 				//If this mailbox is not an inbox, and we only care about inboxes, then skip this message.
   340 				if ([self inboxOnly] && ![[MailAccount inboxMailboxUids] containsObject:mailbox])
   341 					continue;
   342 
   343 				MailAccount *account = [mailbox account];
   344 				if (![self isAccountEnabled:account])
   345 					continue;
   346 
   347 				if (([message isJunk]) || [junkMailboxUids containsObject:[message mailbox]])
   348 					[accountJunkSummary addObject:account];
   349 				else
   350 					[accountSummary addObject:account];
   351 			}
   352 			NSString *title = NSLocalizedStringFromTableInBundle(@"New mail", NULL, GMGetGrowlMailBundle(), "");
   353 			NSString *titleJunk = NSLocalizedStringFromTableInBundle(@"New junk mail", NULL, GMGetGrowlMailBundle(), "");
   354 			NSString *description;
   355 
   356 			MailAccount *account;
   357 
   358 			NSEnumerator *accountSummaryEnum = [accountSummary objectEnumerator];
   359 			while ((account = [accountSummaryEnum nextObject])) {
   360 				if (![self isAccountEnabled:account])
   361 					continue;
   362 
   363 				unsigned summaryCount = [accountSummary countForObject:account];
   364 				if (summaryCount) {
   365 					if (summaryCount == 1) {
   366 						description = [NSString stringWithFormat:NSLocalizedStringFromTableInBundle(@"%@ \n 1 new mail", NULL, GMGetGrowlMailBundle(), "%@ is an account name"), [account displayName]];
   367 					} else {
   368 						description = [NSString stringWithFormat:NSLocalizedStringFromTableInBundle(@"%@ \n %u new mails", NULL, GMGetGrowlMailBundle(), "%@ is an account name; %u becomes a number"), [account displayName], summaryCount];
   369 					}
   370 					[GrowlApplicationBridge notifyWithTitle:title
   371 												description:description
   372 										   notificationName:NEW_MAIL_NOTIFICATION
   373 												   iconData:nil
   374 												   priority:0
   375 												   isSticky:NO
   376 											   clickContext:@""];	// non-nil click context
   377 				}
   378 			}
   379 
   380 			NSEnumerator *accountJunkSummaryEnum = [accountJunkSummary objectEnumerator];
   381 			while ((account = [accountJunkSummaryEnum nextObject])) {
   382 				if (![self isAccountEnabled:account])
   383 					continue;
   384 
   385 				unsigned summaryCount = [accountJunkSummary countForObject:account];
   386 				if (summaryCount) {
   387 					if (summaryCount == 1) {
   388 						description = [NSString stringWithFormat:NSLocalizedStringFromTableInBundle(@"%@ \n 1 new mail", NULL, GMGetGrowlMailBundle(), "%@ is an account name"), [account displayName]];
   389 					} else {
   390 						description = [NSString stringWithFormat:NSLocalizedStringFromTableInBundle(@"%@ \n %u new mails", NULL, GMGetGrowlMailBundle(), "%@ is an account name; %u becomes a number"), [account displayName], summaryCount];
   391 					}					
   392 					[GrowlApplicationBridge notifyWithTitle:titleJunk
   393 												description:description
   394 										   notificationName:NEW_JUNK_MAIL_NOTIFICATION
   395 												   iconData:nil
   396 												   priority:0
   397 												   isSticky:NO
   398 											   clickContext:@""];	// non-nil click context
   399 				}
   400 			}
   401 			break;
   402 		}
   403 	}
   404 }
   405 
   406 - (void)showAllNotifications:(NSNotification *)notification
   407 {
   408 	if (([[notification name] rangeOfString:@"NSWindow"].location == NSNotFound) &&
   409 		([[notification name] rangeOfString:@"NSMouse"].location == NSNotFound) &&
   410 		([[notification name] rangeOfString:@"_NSThread"].location == NSNotFound)) {
   411 		NSLog(@"%@", notification);
   412 	}
   413 }
   414 
   415 - (void)monitoredActivityStarted:(NSNotification *)notification
   416 {
   417 	if ([[[notification object] description] isEqualToString:@"Copying messages"]) {
   418 		messageCopies++;
   419 #ifdef GROWL_MAIL_DEBUG
   420 		NSLog(@"Copying a message: messageCopies is now %i", messageCopies);
   421 #endif
   422 		if (messageCopies <= 0)
   423 			[self shutDownGrowlMailAndWarn:@"Number of message-copying operations overflowed. How on earth did you accomplish starting more than 2 billion copying operations at a time?!"];
   424 	}
   425 }
   426 
   427 - (void)monitoredActivityEnded:(NSNotification *)notification
   428 {
   429 	if ([[[notification object] description] isEqualToString:@"Copying messages"]) {
   430 		if (messageCopies <= 0)
   431 			[self shutDownGrowlMailAndWarn:@"Number of message-copying operations went below 0. It is not possible to have a negative number of copying operations!"];
   432 		messageCopies--;
   433 #ifdef GROWL_MAIL_DEBUG
   434 		NSLog(@"Finished copying a message: messageCopies is now %i", messageCopies);
   435 #endif
   436 	}
   437 }
   438 
   439 #pragma mark Preferences
   440 
   441 - (BOOL) isAccountEnabled:(MailAccount *)account {
   442 	BOOL isEnabled = YES;
   443 	NSDictionary *accountSettings = [[NSUserDefaults standardUserDefaults] objectForKey:@"GMAccounts"];
   444 	if (accountSettings) {
   445 		NSNumber *value = [accountSettings objectForKey:[account path]];
   446 		if (value)
   447 			isEnabled = [value boolValue];
   448 	}
   449 	return isEnabled;
   450 }
   451 
   452 - (void) setAccount:(MailAccount *)account enabled:(BOOL)yesOrNo {
   453 	NSDictionary *accountSettings = [[NSUserDefaults standardUserDefaults] objectForKey:@"GMAccounts"];
   454 	NSMutableDictionary *newSettings = [[accountSettings mutableCopy] autorelease];
   455 	if (!newSettings)
   456 		newSettings = [NSMutableDictionary dictionaryWithCapacity:1U];
   457 	[newSettings setObject:[NSNumber numberWithBool:yesOrNo] forKey:[account path]];
   458 	[[NSUserDefaults standardUserDefaults] setObject:newSettings forKey:@"GMAccounts"];
   459 }
   460 
   461 #pragma mark Accessors
   462 
   463 - (BOOL) isEnabled {
   464 	NSNumber *enabledNum = [[NSUserDefaults standardUserDefaults] objectForKey:@"GMEnableGrowlMailBundle"];
   465 	return enabledNum ? [enabledNum boolValue] : YES;
   466 }
   467 - (GrowlMailSummaryMode) summaryMode {
   468 	NSNumber *summaryModeNum = [[NSUserDefaults standardUserDefaults] objectForKey:@"GMSummaryMode"];
   469 	return summaryModeNum ? [summaryModeNum intValue] : GrowlMailSummaryModeAutomatic;
   470 }
   471 - (BOOL) inboxOnly {
   472 	NSNumber *inboxOnlyNum = [[NSUserDefaults standardUserDefaults] objectForKey:@"GMInboxOnly"];
   473 	return inboxOnlyNum ? [inboxOnlyNum boolValue] : YES;
   474 }
   475 
   476 - (NSString *) titleFormat {
   477 	NSString *titleFormat = [[NSUserDefaults standardUserDefaults] stringForKey:@"GMTitleFormat"];
   478 	return titleFormat ? titleFormat : @"(%account) %sender";
   479 }
   480 - (NSString *) descriptionFormat {
   481 	NSString *descriptionFormat = [[NSUserDefaults standardUserDefaults] stringForKey:@"GMDescriptionFormat"];
   482 	return descriptionFormat ? descriptionFormat : @"%subject\n%body";
   483 }
   484 
   485 @end