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