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.
2 Copyright (c) The Growl Project, 2004-2005
5 Redistribution and use in source and binary forms, with or without modification,
6 are permitted provided that the following conditions are met:
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.
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.
32 // Created by Adam Iser on Mon Jul 26 2004.
33 // Copyright (c) 2004-2005 The Growl Project. All rights reserved.
37 #import "Message+GrowlMail.h"
39 #import "MailHeaders.h"
40 #import "MessageFrameworkHeaders.h"
41 #import <objc/objc-class.h>
49 #define AUTO_THRESHOLD 10
51 #define MAX_NOTIFICATION_THREADS 5
53 static int activeNotificationThreads = 0;
55 //#define GROWL_MAIL_DEBUG
57 NSBundle *GMGetGrowlMailBundle(void) {
58 return [NSBundle bundleForClass:[GrowlMail class]];
61 @implementation GrowlMail
63 static int messageCopies = 0;
65 #pragma mark Panic buttons
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];
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__);
77 NSLog(@"Furthermore, the caller provided a more specific message: %@", specificWarning);
79 [self shutDownGrowlMail];
82 #pragma mark Boring bookkeeping stuff
87 // this image is leaked
88 NSImage *image = [[NSImage alloc] initByReferencingFile:[GMGetGrowlMailBundle() pathForImageResource:@"GrowlMail"]];
89 [image setName:@"GrowlMail"];
91 [GrowlMail registerBundle];
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",
101 [[NSUserDefaults standardUserDefaults] registerDefaults:defaultsDictionary];
102 [defaultsDictionary release];
104 NSLog(@"Loaded GrowlMail %@", [GMGetGrowlMailBundle() objectForInfoDictionaryKey:(NSString *)kCFBundleVersionKey]);
107 + (BOOL) hasPreferencesPanel {
111 + (NSString *) preferencesOwnerClassName {
112 return @"GrowlMailPreferencesModule";
115 + (NSString *) preferencesPanelName {
120 if ((self = [super init])) {
121 NSString *privateFrameworksPath = [GMGetGrowlMailBundle() privateFrameworksPath];
122 NSString *growlBundlePath = [privateFrameworksPath stringByAppendingPathComponent:@"Growl.framework"];
124 NSBundle *growlBundle = [NSBundle bundleWithPath:growlBundlePath];
126 if ([growlBundle load]) {
127 // Register ourselves as a Growl delegate
128 [GrowlApplicationBridge setGrowlDelegate:self];
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]);
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.");
140 [[NSNotificationCenter defaultCenter] addObserver:self
141 selector:@selector(messageStoreDidAddMessages:)
142 name:@"MessageStoreMessagesAdded_inMainThread_"
144 [[NSNotificationCenter defaultCenter] addObserver:self
145 selector:@selector(monitoredActivityStarted:)
146 name:@"MonitoredActivityStarted_inMainThread_"
148 [[NSNotificationCenter defaultCenter] addObserver:self
149 selector:@selector(monitoredActivityEnded:)
150 name:@"MonitoredActivityEnded_inMainThread_"
153 #ifdef GROWL_MAIL_DEBUG
155 [[NSNotificationCenter defaultCenter] addObserver:self
156 selector:@selector(showAllNotifications:)
157 name:nil object:nil];
162 NSLog(@"Could not load Growl.framework, GrowlMail disabled");
169 - (void)showAllNotifications:(NSNotification *)notification
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);
178 - (void)monitoredActivityStarted:(NSNotification *)notification
180 if ([[[notification object] description] isEqualToString:@"Copying messages"]) {
182 #ifdef GROWL_MAIL_DEBUG
183 NSLog(@"Copying a message: messageCopies is now %i", messageCopies);
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?!"];
190 - (void)monitoredActivityEnded:(NSNotification *)notification
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!"];
196 #ifdef GROWL_MAIL_DEBUG
197 NSLog(@"Finished copying a message: messageCopies is now %i", messageCopies);
203 [self shutDownGrowlMail];
204 [[NSNotificationCenter defaultCenter] removeObserver:self];
209 #pragma mark GrowlApplicationBridge delegate methods
211 - (NSString *) applicationNameForGrowl {
215 - (NSImage *) applicationIconForGrowl {
216 return [NSImage imageNamed:@"NSApplicationIcon"];
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:"];
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];
237 [NSApp activateIgnoringOtherApps:YES];
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,
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,
252 NSArray *defaultNotifications = [NSArray arrayWithObject:NEW_MAIL_NOTIFICATION];
254 NSDictionary *ticket = [NSDictionary dictionaryWithObjectsAndKeys:
255 allowedNotifications, GROWL_NOTIFICATIONS_ALL,
256 defaultNotifications, GROWL_NOTIFICATIONS_DEFAULT,
257 humanReadableNames, GROWL_NOTIFICATIONS_HUMAN_READABLE_NAMES,
259 #ifdef GROWL_MAIL_DEBUG
260 NSLog(@"%s: Returning Growl dictionary %@", __PRETTY_FUNCTION__, ticket);
266 #pragma mark Mail notification handlers
268 + (void)showNotificationForMessage:(Message *)message
270 if (activeNotificationThreads < MAX_NOTIFICATION_THREADS) {
271 activeNotificationThreads++;
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.
281 * Blocking the main thread is, of course, out of the question.
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.
286 [NSThread detachNewThreadSelector:@selector(GMShowNotificationPart1)
290 [self performSelector:@selector(showNotificationForMessage:)
296 + (void)didFinishNotificationForMessage:(Message *)message
298 #pragma unused(message)
299 activeNotificationThreads--;
302 - (void)messageStoreDidAddMessages:(NSNotification *)notification {
303 if (!GMIsEnabled()) return;
305 #ifdef GROWL_MAIL_DEBUG
306 NSLog(@"%s called", __PRETTY_FUNCTION__);
310 #ifdef GROWL_MAIL_DEBUG
311 NSLog(@"Ignoring because %i message copies are in process", messageCopies);
316 Library *store = [notification object];
318 [self shutDownGrowlMailAndWarn:[NSString stringWithFormat:@"'%@' notification has no object", [notification name]]];
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.
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.
328 NSDictionary *userInfo = [notification userInfo];
329 if (!userInfo) [self shutDownGrowlMailAndWarn:@"Notification had no userInfo"];
331 NSArray *mailboxes = [userInfo objectForKey:@"mailboxes"];
332 #ifdef GROWL_MAIL_DEBUG
333 NSLog(@"%s: Adding messages to mailboxes %@", __PRETTY_FUNCTION__, mailboxes);
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;
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"];
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]]];
361 NSSet *mailboxesSet = [NSSet setWithArray:mailboxes];
362 NSMutableSet *mailboxesNotIgnored = [[mailboxesSet mutableCopy] autorelease];
363 [mailboxesNotIgnored minusSet:mailboxesToIgnore];
364 if ([mailboxesNotIgnored count] == 0U)
367 NSArray *messages = [userInfo objectForKey:@"messages"];
368 if (!messages) [self shutDownGrowlMailAndWarn:@"Notification's userInfo has no messages"];
370 #ifdef GROWL_MAIL_DEBUG
371 NSLog(@"%s: Mail added messages [1] to mailboxes [2].\n[1]: %@\n[2]: %@", __PRETTY_FUNCTION__, messages, mailboxes);
374 unsigned count = [messages count];
376 int summaryMode = GMSummaryMode();
377 if (summaryMode == MODE_AUTO) {
378 if (count >= AUTO_THRESHOLD)
379 summaryMode = MODE_SUMMARY;
381 summaryMode = MODE_SINGLE;
384 #ifdef GROWL_MAIL_DEBUG
385 NSLog(@"Got %i new messages. Summary mode was %i and is now %i", count, GMSummaryMode(), summaryMode);
388 Class Message_class = [Message class];
390 switch (summaryMode) {
393 NSEnumerator *messagesEnum = [messages objectEnumerator];
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])
401 MailAccount *account = [mailbox account];
402 if (![self isAccountEnabled:account])
405 if (![message isKindOfClass:Message_class])
406 [self shutDownGrowlMailAndWarn:[NSString stringWithFormat:@"Message in notification was not a Message; it is %@", message]];
408 [[self class] showNotificationForMessage:message];
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"];
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];
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])
431 MailAccount *account = [mailbox account];
432 if (![self isAccountEnabled:account])
435 if (([message isJunk]) || [junkMailboxUids containsObject:[message mailbox]])
436 [accountJunkSummary addObject:account];
438 [accountSummary addObject:account];
440 NSString *title = NSLocalizedStringFromTableInBundle(@"New mail", NULL, GMGetGrowlMailBundle(), "");
441 NSString *titleJunk = NSLocalizedStringFromTableInBundle(@"New junk mail", NULL, GMGetGrowlMailBundle(), "");
442 NSString *description;
444 MailAccount *account;
446 NSEnumerator *accountSummaryEnum = [accountSummary objectEnumerator];
447 while ((account = [accountSummaryEnum nextObject])) {
448 if (![self isAccountEnabled:account])
451 unsigned summaryCount = [accountSummary countForObject:account];
453 if (summaryCount == 1) {
454 description = [NSString stringWithFormat:NSLocalizedStringFromTableInBundle(@"%@ \n 1 new mail", NULL, GMGetGrowlMailBundle(), "%@ is an account name"), [account displayName]];
456 description = [NSString stringWithFormat:NSLocalizedStringFromTableInBundle(@"%@ \n %u new mails", NULL, GMGetGrowlMailBundle(), "%@ is an account name; %u becomes a number"), [account displayName], summaryCount];
458 [GrowlApplicationBridge notifyWithTitle:title
459 description:description
460 notificationName:NEW_MAIL_NOTIFICATION
464 clickContext:@""]; // non-nil click context
468 NSEnumerator *accountJunkSummaryEnum = [accountJunkSummary objectEnumerator];
469 while ((account = [accountJunkSummaryEnum nextObject])) {
470 if (![self isAccountEnabled:account])
473 unsigned summaryCount = [accountJunkSummary countForObject:account];
475 if (summaryCount == 1) {
476 description = [NSString stringWithFormat:NSLocalizedStringFromTableInBundle(@"%@ \n 1 new mail", NULL, GMGetGrowlMailBundle(), "%@ is an account name"), [account displayName]];
478 description = [NSString stringWithFormat:NSLocalizedStringFromTableInBundle(@"%@ \n %u new mails", NULL, GMGetGrowlMailBundle(), "%@ is an account name; %u becomes a number"), [account displayName], summaryCount];
480 [GrowlApplicationBridge notifyWithTitle:titleJunk
481 description:description
482 notificationName:NEW_JUNK_MAIL_NOTIFICATION
486 clickContext:@""]; // non-nil click context
494 #pragma mark Preferences
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]];
502 isEnabled = [value boolValue];
507 - (void) setAccount:(MailAccount *)account enabled:(BOOL)yesOrNo {
508 NSDictionary *accountSettings = [[NSUserDefaults standardUserDefaults] objectForKey:@"GMAccounts"];
509 NSMutableDictionary *newSettings = [[accountSettings mutableCopy] autorelease];
511 newSettings = [NSMutableDictionary dictionaryWithCapacity:1U];
512 [newSettings setObject:[NSNumber numberWithBool:yesOrNo] forKey:[account path]];
513 [[NSUserDefaults standardUserDefaults] setObject:newSettings forKey:@"GMAccounts"];
518 BOOL GMIsEnabled(void) {
519 NSNumber *enabledNum = [[NSUserDefaults standardUserDefaults] objectForKey:@"GMEnableGrowlMailBundle"];
520 return enabledNum ? [enabledNum boolValue] : YES;
523 int GMSummaryMode(void) {
524 NSNumber *summaryModeNum = [[NSUserDefaults standardUserDefaults] objectForKey:@"GMSummaryMode"];
525 return summaryModeNum ? [summaryModeNum intValue] : MODE_AUTO;
528 BOOL GMInboxOnly(void) {
529 NSNumber *inboxOnlyNum = [[NSUserDefaults standardUserDefaults] objectForKey:@"GMInboxOnly"];
530 return inboxOnlyNum ? [inboxOnlyNum boolValue] : YES;
533 NSString *GMTitleFormatString(void) {
534 NSString *titleFormat = [[NSUserDefaults standardUserDefaults] stringForKey:@"GMTitleFormat"];
535 return titleFormat ? titleFormat : @"(%account) %sender";
538 NSString *GMDescriptionFormatString(void) {
539 NSString *descriptionFormat = [[NSUserDefaults standardUserDefaults] stringForKey:@"GMDescriptionFormat"];
540 return descriptionFormat ? descriptionFormat : @"%subject\n%body";