Make GrowlMail's main suicide-pill method into a public function. The core method lives on, and the function calls it through the shiny new singleton-instance static variable that I added in the previous commit.
5 // Created by Peter Hosey on 2009-05-10.
6 // Copyright 2009 Peter Hosey. All rights reserved.
9 #import "GrowlMailNotifier.h"
11 #import "Message+GrowlMail.h"
12 #import <objc/objc-runtime.h>
14 #import "MessageFrameworkHeaders.h"
16 #define AUTO_THRESHOLD 10
18 #define MAX_NOTIFICATION_THREADS 5
20 static int activeNotificationThreads = 0;
22 static int messageCopies = 0;
24 static GrowlMailNotifier *sharedNotifier = nil;
26 static BOOL notifierEnabled = YES;
28 @implementation GrowlMailNotifier
30 #pragma mark Panic buttons
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];
38 #pragma mark The circle of life
40 + (id) sharedNotifier {
41 if (!sharedNotifier) {
42 //-init and -dealloc will each assign to sharedNotifier.
43 [[[GrowlMailNotifier alloc] init] autorelease];
45 return sharedNotifier;
51 return [sharedNotifier retain];
54 //No shared notifier yet; someone is trying to create one. If we previously disabled ourselves, abort this attempt.
55 if (!notifierEnabled) {
60 if((self = [super init])) {
61 NSNumber *automatic = [NSNumber numberWithInt:GrowlMailSummaryModeAutomatic];
62 NSDictionary *defaultsDictionary = [NSDictionary dictionaryWithObjectsAndKeys:
63 @"(%account) %sender", @"GMTitleFormat",
64 @"%subject\n%body", @"GMDescriptionFormat",
65 automatic, @"GMSummaryMode",
66 [NSNumber numberWithBool:YES], @"GMEnableGrowlMailBundle",
67 [NSNumber numberWithBool:NO], @"GMInboxOnly",
69 [[NSUserDefaults standardUserDefaults] registerDefaults:defaultsDictionary];
71 [GrowlApplicationBridge setGrowlDelegate:self];
73 [[NSNotificationCenter defaultCenter] addObserver:self
74 selector:@selector(messageStoreDidAddMessages:)
75 name:@"MessageStoreMessagesAdded_inMainThread_"
77 [[NSNotificationCenter defaultCenter] addObserver:self
78 selector:@selector(monitoredActivityStarted:)
79 name:@"MonitoredActivityStarted_inMainThread_"
81 [[NSNotificationCenter defaultCenter] addObserver:self
82 selector:@selector(monitoredActivityEnded:)
83 name:@"MonitoredActivityEnded_inMainThread_"
86 #ifdef GROWL_MAIL_DEBUG
88 [[NSNotificationCenter defaultCenter] addObserver:self
89 selector:@selector(showAllNotifications:)
93 sharedNotifier = self;
99 [self shutDownGrowlMail];
100 [[NSNotificationCenter defaultCenter] removeObserver:self];
101 sharedNotifier = nil;
106 #pragma mark GrowlApplicationBridge delegate methods
108 - (NSString *) applicationNameForGrowl {
112 - (NSImage *) applicationIconForGrowl {
113 return [NSImage imageNamed:@"NSApplicationIcon"];
116 - (void) growlNotificationWasClicked:(NSString *)clickContext {
117 if ([clickContext length]) {
118 //Make sure we have all the methods we need.
119 if (!class_getClassMethod([Library class], @selector(messageWithMessageID:)))
120 GMShutDownGrowlMailAndWarn(@"Library does not respond to +messageWithMessageID:");
121 if (!class_getInstanceMethod([SingleMessageViewer class], @selector(initForViewingMessage:showAllHeaders:viewingState:fromDefaults:)))
122 GMShutDownGrowlMailAndWarn(@"SingleMessageViewer does not respond to -initForViewingMessage:showAllHeaders:viewingState:fromDefaults:");
123 if (!class_getInstanceMethod([SingleMessageViewer class], @selector(showAndMakeKey:)))
124 GMShutDownGrowlMailAndWarn(@"SingleMessageViewer does not respond to -showAndMakeKey:");
126 Message *message = [Library messageWithMessageID:clickContext];
127 MessageViewingState *viewingState = [[MessageViewingState alloc] init];
128 SingleMessageViewer *messageViewer = [[SingleMessageViewer alloc] initForViewingMessage:message showAllHeaders:NO viewingState:viewingState fromDefaults:NO];
129 [viewingState release];
130 [messageViewer showAndMakeKey:YES];
131 [messageViewer release];
132 [Library markMessageAsViewed:message];
134 [NSApp activateIgnoringOtherApps:YES];
137 - (NSDictionary *) registrationDictionaryForGrowl {
138 // Register our ticket with Growl
139 NSArray *allowedNotifications = [NSArray arrayWithObjects:
140 NEW_MAIL_NOTIFICATION,
141 NEW_JUNK_MAIL_NOTIFICATION,
142 NEW_NOTE_NOTIFICATION,
144 NSDictionary *humanReadableNames = [NSDictionary dictionaryWithObjectsAndKeys:
145 NSLocalizedStringFromTableInBundle(@"New mail", nil, GMGetGrowlMailBundle(), ""), NEW_MAIL_NOTIFICATION,
146 NSLocalizedStringFromTableInBundle(@"New junk mail", nil, GMGetGrowlMailBundle(), ""), NEW_JUNK_MAIL_NOTIFICATION,
147 NSLocalizedStringFromTableInBundle(@"New note", nil, GMGetGrowlMailBundle(), ""), NEW_NOTE_NOTIFICATION,
149 NSArray *defaultNotifications = [NSArray arrayWithObject:NEW_MAIL_NOTIFICATION];
151 NSDictionary *ticket = [NSDictionary dictionaryWithObjectsAndKeys:
152 allowedNotifications, GROWL_NOTIFICATIONS_ALL,
153 defaultNotifications, GROWL_NOTIFICATIONS_DEFAULT,
154 humanReadableNames, GROWL_NOTIFICATIONS_HUMAN_READABLE_NAMES,
156 #ifdef GROWL_MAIL_DEBUG
157 NSLog(@"%s: Returning Growl dictionary %@", __PRETTY_FUNCTION__, ticket);
163 #pragma mark Mail notification handlers
165 + (void)showNotificationForMessage:(Message *)message
167 if (activeNotificationThreads < MAX_NOTIFICATION_THREADS) {
168 activeNotificationThreads++;
172 * If we want the message body, it may not be immediately available.
173 * It can be retrieved without blocking if it's available, which we initially try.
174 * However, if we really, really want it, we may have to request it in a blocking fashion:
175 * for example, if the user doesn't read the message and doesn't have Mail set to download it automatically,
176 * we'll never get it without blocking.
178 * Blocking the main thread is, of course, out of the question.
180 * We're making some assumptions about Mail's internals, but the fact that notifications are posted on auxiliary threads
181 * and then again with a _inMainThread_ suffix on the main thread indicates that threads are being used for mail access elsewhere.
183 [NSThread detachNewThreadSelector:@selector(GMShowNotificationPart1)
187 [self performSelector:@selector(showNotificationForMessage:)
193 - (void)didFinishNotificationForMessage:(Message *)message
195 #pragma unused(message)
196 activeNotificationThreads--;
199 - (void)messageStoreDidAddMessages:(NSNotification *)notification {
200 if (![self isEnabled]) return;
202 #ifdef GROWL_MAIL_DEBUG
203 NSLog(@"%s called", __PRETTY_FUNCTION__);
207 #ifdef GROWL_MAIL_DEBUG
208 NSLog(@"Ignoring because %i message copies are in process", messageCopies);
213 Library *store = [notification object];
215 GMShutDownGrowlMailAndWarn([NSString stringWithFormat:@"'%@' notification has no object", [notification name]]);
217 if ([store isKindOfClass:[LibraryStore class]]) {
218 //As of Tiger, this is normal; this notification is posted a couple times (perhaps once per inbox) with a LibraryStore object.
219 //This is not the notification we're looking for; we don't need to see its papers. We will move along now.
222 //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.
223 //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.
225 NSDictionary *userInfo = [notification userInfo];
226 if (!userInfo) GMShutDownGrowlMailAndWarn(@"Notification had no userInfo");
228 NSArray *mailboxes = [userInfo objectForKey:@"mailboxes"];
229 #ifdef GROWL_MAIL_DEBUG
230 NSLog(@"%s: Adding messages to mailboxes %@", __PRETTY_FUNCTION__, mailboxes);
233 //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.
234 if (!(mailboxes && [mailboxes count])) return;
236 //Ignore a notification if we're ignoring all of the mailboxes involved.
237 Class MailAccount_class = [MailAccount class];
238 if (!class_getClassMethod(MailAccount_class, @selector(draftMailboxUids)))
239 GMShutDownGrowlMailAndWarn(@"MailAccount does not respond to +draftMailboxUids");
240 if (!class_getClassMethod(MailAccount_class, @selector(outboxMailboxUids)))
241 GMShutDownGrowlMailAndWarn(@"MailAccount does not respond to +outboxMailboxUids");
242 if (!class_getClassMethod(MailAccount_class, @selector(sentMessagesMailboxUids)))
243 GMShutDownGrowlMailAndWarn(@"MailAccount does not respond to +sentMessagesMailboxUids");
244 if (!class_getClassMethod(MailAccount_class, @selector(trashMailboxUids)))
245 GMShutDownGrowlMailAndWarn(@"MailAccount does not respond to +trashMailboxUids");
246 //We need this method to support the Inbox Only preference.
247 if (!class_getClassMethod(MailAccount_class, @selector(inboxMailboxUids)))
248 GMShutDownGrowlMailAndWarn(@"MailAccount does not respond to +inboxMailboxUids");
250 //Ignore messages being written.
251 NSMutableSet *mailboxesToIgnore = [NSMutableSet setWithArray:[MailAccount draftMailboxUids]];
252 //Ignore messages being sent.
253 [mailboxesToIgnore unionSet:[NSSet setWithArray:[MailAccount outboxMailboxUids]]];
254 [mailboxesToIgnore unionSet:[NSSet setWithArray:[MailAccount sentMessagesMailboxUids]]];
255 //Ignore messages being deleted.
256 [mailboxesToIgnore unionSet:[NSSet setWithArray:[MailAccount trashMailboxUids]]];
258 NSSet *mailboxesSet = [NSSet setWithArray:mailboxes];
259 NSMutableSet *mailboxesNotIgnored = [[mailboxesSet mutableCopy] autorelease];
260 [mailboxesNotIgnored minusSet:mailboxesToIgnore];
261 if ([mailboxesNotIgnored count] == 0U)
264 NSArray *messages = [userInfo objectForKey:@"messages"];
265 if (!messages) GMShutDownGrowlMailAndWarn(@"Notification's userInfo has no messages");
267 #ifdef GROWL_MAIL_DEBUG
268 NSLog(@"%s: Mail added messages [1] to mailboxes [2].\n[1]: %@\n[2]: %@", __PRETTY_FUNCTION__, messages, mailboxes);
271 unsigned count = [messages count];
273 int summaryMode = [self summaryMode];
274 if (summaryMode == GrowlMailSummaryModeAutomatic) {
275 if (count >= AUTO_THRESHOLD)
276 summaryMode = GrowlMailSummaryModeAlways;
278 summaryMode = GrowlMailSummaryModeDisabled;
281 #ifdef GROWL_MAIL_DEBUG
282 NSLog(@"Got %i new messages. Summary mode was %i and is now %i", count, [self summaryMode], summaryMode);
285 Class Message_class = [Message class];
287 switch (summaryMode) {
289 case GrowlMailSummaryModeDisabled: {
290 NSEnumerator *messagesEnum = [messages objectEnumerator];
292 while ((message = [messagesEnum nextObject])) {
293 MailboxUid *mailbox = [message mailbox];
294 //If this mailbox is not an inbox, and we only care about inboxes, then skip this message.
295 if ([self inboxOnly] && ![[MailAccount inboxMailboxUids] containsObject:mailbox])
298 MailAccount *account = [mailbox account];
299 if (![self isAccountEnabled:account])
302 if (![message isKindOfClass:Message_class])
303 GMShutDownGrowlMailAndWarn([NSString stringWithFormat:@"Message in notification was not a Message; it is %@", message]);
305 if (![message respondsToSelector:@selector(isRead)] || ![message isRead]) {
306 /* Don't display read messages */
307 [[self class] showNotificationForMessage:message];
312 case GrowlMailSummaryModeAlways: {
313 if (!class_getClassMethod([MailAccount class], @selector(mailAccounts)))
314 GMShutDownGrowlMailAndWarn(@"MailAccount does not respond to +mailAccounts");
315 if (!class_getInstanceMethod(Message_class, @selector(mailbox)))
316 GMShutDownGrowlMailAndWarn(@"Message does not respond to -mailbox");
318 NSArray *accounts = [MailAccount mailAccounts];
319 unsigned accountsCount = [accounts count];
320 NSCountedSet *accountSummary = [NSCountedSet setWithCapacity:accountsCount];
321 NSCountedSet *accountJunkSummary = [NSCountedSet setWithCapacity:accountsCount];
322 NSEnumerator *messagesEnum = [messages objectEnumerator];
323 NSArray *junkMailboxUids = [MailAccount junkMailboxUids];
325 while ((message = [messagesEnum nextObject])) {
326 MailboxUid *mailbox = [message mailbox];
327 //If this mailbox is not an inbox, and we only care about inboxes, then skip this message.
328 if ([self inboxOnly] && ![[MailAccount inboxMailboxUids] containsObject:mailbox])
331 MailAccount *account = [mailbox account];
332 if (![self isAccountEnabled:account])
335 if (([message isJunk]) || [junkMailboxUids containsObject:[message mailbox]])
336 [accountJunkSummary addObject:account];
338 [accountSummary addObject:account];
340 NSString *title = NSLocalizedStringFromTableInBundle(@"New mail", NULL, GMGetGrowlMailBundle(), "");
341 NSString *titleJunk = NSLocalizedStringFromTableInBundle(@"New junk mail", NULL, GMGetGrowlMailBundle(), "");
342 NSString *description;
344 MailAccount *account;
346 NSEnumerator *accountSummaryEnum = [accountSummary objectEnumerator];
347 while ((account = [accountSummaryEnum nextObject])) {
348 if (![self isAccountEnabled:account])
351 unsigned summaryCount = [accountSummary countForObject:account];
353 if (summaryCount == 1) {
354 description = [NSString stringWithFormat:NSLocalizedStringFromTableInBundle(@"%@ \n 1 new mail", NULL, GMGetGrowlMailBundle(), "%@ is an account name"), [account displayName]];
356 description = [NSString stringWithFormat:NSLocalizedStringFromTableInBundle(@"%@ \n %u new mails", NULL, GMGetGrowlMailBundle(), "%@ is an account name; %u becomes a number"), [account displayName], summaryCount];
358 [GrowlApplicationBridge notifyWithTitle:title
359 description:description
360 notificationName:NEW_MAIL_NOTIFICATION
364 clickContext:@""]; // non-nil click context
368 NSEnumerator *accountJunkSummaryEnum = [accountJunkSummary objectEnumerator];
369 while ((account = [accountJunkSummaryEnum nextObject])) {
370 if (![self isAccountEnabled:account])
373 unsigned summaryCount = [accountJunkSummary countForObject:account];
375 if (summaryCount == 1) {
376 description = [NSString stringWithFormat:NSLocalizedStringFromTableInBundle(@"%@ \n 1 new mail", NULL, GMGetGrowlMailBundle(), "%@ is an account name"), [account displayName]];
378 description = [NSString stringWithFormat:NSLocalizedStringFromTableInBundle(@"%@ \n %u new mails", NULL, GMGetGrowlMailBundle(), "%@ is an account name; %u becomes a number"), [account displayName], summaryCount];
380 [GrowlApplicationBridge notifyWithTitle:titleJunk
381 description:description
382 notificationName:NEW_JUNK_MAIL_NOTIFICATION
386 clickContext:@""]; // non-nil click context
394 - (void)showAllNotifications:(NSNotification *)notification
396 if (([[notification name] rangeOfString:@"NSWindow"].location == NSNotFound) &&
397 ([[notification name] rangeOfString:@"NSMouse"].location == NSNotFound) &&
398 ([[notification name] rangeOfString:@"_NSThread"].location == NSNotFound)) {
399 NSLog(@"%@", notification);
403 - (void)monitoredActivityStarted:(NSNotification *)notification
405 if ([[[notification object] description] isEqualToString:@"Copying messages"]) {
407 #ifdef GROWL_MAIL_DEBUG
408 NSLog(@"Copying a message: messageCopies is now %i", messageCopies);
410 if (messageCopies <= 0)
411 GMShutDownGrowlMailAndWarn(@"Number of message-copying operations overflowed. How on earth did you accomplish starting more than 2 billion copying operations at a time?!");
415 - (void)monitoredActivityEnded:(NSNotification *)notification
417 if ([[[notification object] description] isEqualToString:@"Copying messages"]) {
418 if (messageCopies <= 0)
419 GMShutDownGrowlMailAndWarn(@"Number of message-copying operations went below 0. It is not possible to have a negative number of copying operations!");
421 #ifdef GROWL_MAIL_DEBUG
422 NSLog(@"Finished copying a message: messageCopies is now %i", messageCopies);
427 #pragma mark Preferences
429 - (BOOL) isAccountEnabled:(MailAccount *)account {
430 BOOL isEnabled = YES;
431 NSDictionary *accountSettings = [[NSUserDefaults standardUserDefaults] objectForKey:@"GMAccounts"];
432 if (accountSettings) {
433 NSNumber *value = [accountSettings objectForKey:[account path]];
435 isEnabled = [value boolValue];
440 - (void) setAccount:(MailAccount *)account enabled:(BOOL)yesOrNo {
441 NSDictionary *accountSettings = [[NSUserDefaults standardUserDefaults] objectForKey:@"GMAccounts"];
442 NSMutableDictionary *newSettings = [[accountSettings mutableCopy] autorelease];
444 newSettings = [NSMutableDictionary dictionaryWithCapacity:1U];
445 [newSettings setObject:[NSNumber numberWithBool:yesOrNo] forKey:[account path]];
446 [[NSUserDefaults standardUserDefaults] setObject:newSettings forKey:@"GMAccounts"];
449 #pragma mark Accessors
452 NSNumber *enabledNum = [[NSUserDefaults standardUserDefaults] objectForKey:@"GMEnableGrowlMailBundle"];
453 return enabledNum ? [enabledNum boolValue] : YES;
455 - (GrowlMailSummaryMode) summaryMode {
456 NSNumber *summaryModeNum = [[NSUserDefaults standardUserDefaults] objectForKey:@"GMSummaryMode"];
457 return summaryModeNum ? [summaryModeNum intValue] : GrowlMailSummaryModeAutomatic;
460 NSNumber *inboxOnlyNum = [[NSUserDefaults standardUserDefaults] objectForKey:@"GMInboxOnly"];
461 return inboxOnlyNum ? [inboxOnlyNum boolValue] : YES;
464 - (NSString *) titleFormat {
465 NSString *titleFormat = [[NSUserDefaults standardUserDefaults] stringForKey:@"GMTitleFormat"];
466 return titleFormat ? titleFormat : @"(%account) %sender";
468 - (NSString *) descriptionFormat {
469 NSString *descriptionFormat = [[NSUserDefaults standardUserDefaults] stringForKey:@"GMDescriptionFormat"];
470 return descriptionFormat ? descriptionFormat : @"%subject\n%body";
473 #pragma mark Panic buttons
475 //This is a suicide pill. GrowlMail calls this function any time it detects a change in Mail's implementation, such as a missing method or an object of the wrong class.
476 void GMShutDownGrowlMailAndWarn(NSString *specificWarning) {
477 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__);
479 NSLog(@"Furthermore, the caller provided a more specific message: %@", specificWarning);
481 [sharedNotifier shutDownGrowlMail];
483 //Prevent ourselves from re-enabling later.
484 notifierEnabled = NO;