Extras/GrowlMail/Message+GrowlMail.m
author Peter Hosey
Wed Feb 25 01:02:44 2009 -0800 (2009-02-25)
changeset 4172 65a0bcf23292
parent 4171 2918a61fe9b3
child 4209 f8a902d33769
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.
tick@796
    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
*/
ingmarstein@563
    28
//
ingmarstein@563
    29
//  Message+GrowlMail.m
ingmarstein@563
    30
//  GrowlMail
ingmarstein@563
    31
//
ingmarstein@563
    32
//  Created by Ingmar Stein on 27.10.04.
ingmarstein@563
    33
//
ingmarstein@563
    34
ingmarstein@563
    35
#import "Message+GrowlMail.h"
ingmarstein@596
    36
#import "GrowlMail.h"
ingmarstein@1983
    37
#import <AddressBook/AddressBook.h>
boredzo@1278
    38
#import <Growl/Growl.h>
ingmarstein@563
    39
evands@4001
    40
@interface NSString (GrowlMail_KeywordReplacing)
evands@4001
    41
evands@4001
    42
- (NSString *) stringByReplacingKeywords:(NSArray *)keywords
evands@4001
    43
                              withValues:(NSArray *)values;
evands@4001
    44
evands@4001
    45
@end
evands@4001
    46
evands@4001
    47
@interface NSMutableString (GrowlMail_LineOrientedTruncation)
evands@4001
    48
evands@4001
    49
- (void) trimStringToFirstNLines:(unsigned)n;
evands@4001
    50
evands@4001
    51
@end
evands@4001
    52
evands@4001
    53
@implementation Message (GrowlMail)
Peter@4171
    54
Peter@4172
    55
- (void) GMShowNotificationPart1 {
evands@4001
    56
	NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
evands@4001
    57
Peter@4172
    58
	MessageBody *messageBody = nil;
Peter@4172
    59
evands@4001
    60
	NSString *titleFormat = (NSString *)GMTitleFormatString();
evands@4001
    61
	NSString *descriptionFormat = (NSString *)GMDescriptionFormatString();
evands@4001
    62
evands@4001
    63
	if ([titleFormat rangeOfString:@"%body"].location != NSNotFound ||
evands@4001
    64
			[descriptionFormat rangeOfString:@"%body"].location != NSNotFound) {
ingmarstein@3191
    65
		/* We will need the body */
Peter@4172
    66
		messageBody = [self messageBodyIfAvailable];
evands@4001
    67
		int nonBlockingAttempts = 0;
evands@4001
    68
		while (!messageBody && nonBlockingAttempts < 3) {
ingmarstein@3192
    69
			/* No message body available yet, but we need one */
evands@4001
    70
			nonBlockingAttempts++;
evands@4001
    71
			[NSThread sleepUntilDate:[NSDate dateWithTimeIntervalSinceNow:(0.5 * nonBlockingAttempts)]];
evands@4001
    72
			
evands@4001
    73
			/* We'd prefer to let whatever Mail process might want the message body get it on its own terms rather than blocking on this thread */
evands@4001
    74
			messageBody = [self messageBodyIfAvailable];
ingmarstein@3192
    75
		}
ingmarstein@3192
    76
evands@4001
    77
		/* Already tried three times (3 seconds); this time, block this thread to get it. */ 
evands@4001
    78
		if (!messageBody) messageBody = [self messageBody];
Peter@4172
    79
	}
Peter@4172
    80
Peter@4172
    81
	[self performSelectorOnMainThread:@selector(GMShowNotificationPart2:)
Peter@4172
    82
						   withObject:messageBody
Peter@4172
    83
						waitUntilDone:NO];
Peter@4172
    84
Peter@4172
    85
	[pool release];
Peter@4172
    86
}
Peter@4172
    87
Peter@4172
    88
- (void) GMShowNotificationPart2:(MessageBody *)messageBody {
Peter@4172
    89
	NSString *account = (NSString *)[[[self mailbox] account] displayName];
Peter@4172
    90
	NSString *sender = [self sender];
Peter@4172
    91
	NSString *senderAddress = [sender uncommentedAddress];
Peter@4172
    92
	NSString *subject = (NSString *)[self subject];
Peter@4172
    93
	NSString *body;
Peter@4172
    94
	NSString *titleFormat = (NSString *)GMTitleFormatString();
Peter@4172
    95
	NSString *descriptionFormat = (NSString *)GMDescriptionFormatString();
ingmarstein@622
    96
ingmarstein@622
    97
	/* The fullName selector is not available in Mail.app 2.0. */
ingmarstein@2249
    98
	if ([sender respondsToSelector:@selector(fullName)])
ingmarstein@622
    99
		sender = [sender fullName];
ingmarstein@2249
   100
	else if ([sender addressComment])
ingmarstein@622
   101
		sender = [sender addressComment];
ingmarstein@2249
   102
Peter@4172
   103
	if (messageBody) {
Peter@4172
   104
		NSString *originalBody = nil;
Peter@4172
   105
		/* stringForIndexing selector: Mail.app 3.0 in OS X 10.4, not in 10.5. */
Peter@4172
   106
		if ([messageBody respondsToSelector:@selector(stringForIndexing)])
Peter@4172
   107
			originalBody = [messageBody stringForIndexing];
Peter@4172
   108
		else if ([messageBody respondsToSelector:@selector(attributedString)])
Peter@4172
   109
			originalBody = [[messageBody attributedString] string];
Peter@4172
   110
		else if ([messageBody respondsToSelector:@selector(stringValueForJunkEvaluation:)])
Peter@4172
   111
			originalBody = [messageBody stringValueForJunkEvaluation:NO];
Peter@4172
   112
		if (originalBody) {
Peter@4172
   113
			NSMutableString *transformedBody = [[[originalBody stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]] mutableCopy] autorelease];
Peter@4172
   114
			unsigned lengthWithoutWhitespace = [transformedBody length];
Peter@4172
   115
			[transformedBody trimStringToFirstNLines:4U];
Peter@4172
   116
			unsigned length = [transformedBody length];
Peter@4172
   117
			if (length > 200U) {
Peter@4172
   118
				[transformedBody deleteCharactersInRange:NSMakeRange(200U, length - 200U)];
Peter@4172
   119
				length = 200U;
Peter@4172
   120
			}
Peter@4172
   121
			if (length != lengthWithoutWhitespace)
Peter@4172
   122
				[transformedBody appendString:[NSString stringWithUTF8String:"\xE2\x80\xA6"]];
Peter@4172
   123
			body = (NSString *)transformedBody;
Peter@4172
   124
		} else {
Peter@4172
   125
			body = @"";	
Peter@4172
   126
		}
Peter@4172
   127
	} else {
Peter@4172
   128
		body = @"";
Peter@4172
   129
	}
Peter@4172
   130
evands@4001
   131
	NSArray *keywords = [NSArray arrayWithObjects:
evands@4001
   132
		@"%sender",
evands@4001
   133
		@"%subject",
evands@4001
   134
		@"%body",
evands@4001
   135
		@"%account",
evands@4001
   136
		nil];
evands@4001
   137
	NSArray *values = [NSArray arrayWithObjects:
evands@4001
   138
		(sender ? sender : @""),
evands@4001
   139
		(subject ? subject : @""),
evands@4001
   140
		(body ? body : @""),
evands@4001
   141
		(account ? account : @""),
evands@4001
   142
		 nil];
evands@4001
   143
	NSString *title = [titleFormat stringByReplacingKeywords:keywords withValues:values];
evands@4001
   144
	NSString *description = [descriptionFormat stringByReplacingKeywords:keywords withValues:values];
ingmarstein@2311
   145
ingmarstein@2311
   146
	/*
ingmarstein@1983
   147
	NSLog(@"Subject: '%@'", subject);
ingmarstein@1983
   148
	NSLog(@"Sender: '%@'", sender);
ingmarstein@1983
   149
	NSLog(@"Account: '%@'", account);
ingmarstein@1983
   150
	NSLog(@"Body: '%@'", body);
ingmarstein@3190
   151
	*/
ingmarstein@3190
   152
ingmarstein@1983
   153
	/*
ingmarstein@1983
   154
	 * MailAddressManager fetches images asynchronously so they might arrive
ingmarstein@1983
   155
	 * after we have sent our notification.
ingmarstein@1983
   156
	 */
ingmarstein@1983
   157
	/*
ingmarstein@581
   158
	MailAddressManager *addressManager = [MailAddressManager addressManager];
ingmarstein@1983
   159
	[addressManager fetchImageForAddress:senderAddress];
ingmarstein@1983
   160
	NSImage *image = [addressManager imageForMailAddress:senderAddress];
ingmarstein@1983
   161
	*/
ingmarstein@1983
   162
	ABSearchElement *personSearch = [ABPerson searchElementForProperty:kABEmailProperty
ingmarstein@1983
   163
																 label:nil
ingmarstein@1983
   164
																   key:nil
ingmarstein@1983
   165
																 value:senderAddress
ingmarstein@1983
   166
															comparison:kABEqualCaseInsensitive];
ingmarstein@2249
   167
ingmarstein@2306
   168
	NSData *image = nil;
ingmarstein@2282
   169
	NSEnumerator *matchesEnum = [[[ABAddressBook sharedAddressBook] recordsMatchingSearchElement:personSearch] objectEnumerator];
boredzo@2279
   170
	ABPerson *person;
boredzo@2279
   171
	while ((!image) && (person = [matchesEnum nextObject]))
boredzo@2281
   172
		image = [person imageData];
boredzo@2278
   173
ingmarstein@2282
   174
	//no matches in the Address Book with an icon, so use Mail's icon instead.
ingmarstein@2282
   175
	if (!image)
ingmarstein@2306
   176
		image = [[NSImage imageNamed:@"NSApplicationIcon"] TIFFRepresentation];
ingmarstein@2249
   177
evands@4001
   178
	NSString *notificationName;
evands@4001
   179
	if ([self isJunk] || ([[MailAccount junkMailboxUids] containsObject:[self mailbox]])) {
evands@4001
   180
		notificationName = NEW_JUNK_MAIL_NOTIFICATION;
evands@3763
   181
	} else {
evands@3763
   182
		if ([self respondsToSelector:@selector(type)] && [self type] == MESSAGE_TYPE_NOTE) {
evands@4001
   183
			notificationName = NEW_NOTE_NOTIFICATION;
evands@3763
   184
		} else {
evands@4001
   185
			notificationName = NEW_MAIL_NOTIFICATION;
evands@3763
   186
		}
evands@3763
   187
	}
ingmarstein@2311
   188
ingmarstein@2562
   189
	NSString *clickContext = [self messageID];
ingmarstein@2562
   190
evands@4001
   191
	[GrowlApplicationBridge notifyWithTitle:title
evands@4001
   192
								description:description
evands@4001
   193
						   notificationName:notificationName
ingmarstein@2221
   194
								   iconData:image
ingmarstein@2103
   195
								   priority:0
ingmarstein@2103
   196
								   isSticky:NO
ingmarstein@2562
   197
							   clickContext:clickContext];	// non-nil click context
evands@4001
   198
evands@4001
   199
	[GrowlMail didFinishNotificationForMessage:self];
ingmarstein@563
   200
}
ingmarstein@3192
   201
evands@4001
   202
@end
evands@4001
   203
evands@4001
   204
@implementation NSString (GrowlMail_KeywordReplacing)
evands@4001
   205
evands@4001
   206
- (NSString *) stringByReplacingKeywords:(NSArray *)keywords
evands@4001
   207
                              withValues:(NSArray *)values
evands@4001
   208
{
evands@4001
   209
	NSParameterAssert([keywords count] == [values count]);
evands@4001
   210
	NSMutableString *str = [[self mutableCopy] autorelease];
evands@4001
   211
evands@4001
   212
	NSEnumerator *keywordsEnum = [keywords objectEnumerator], *valuesEnum = [values objectEnumerator];
evands@4001
   213
	NSString *keyword, *value;
evands@4001
   214
	while ((keyword = [keywordsEnum nextObject]) && (value = [valuesEnum nextObject])) {
evands@4001
   215
		[str replaceOccurrencesOfString:keyword
evands@4001
   216
		                     withString:value
evands@4001
   217
		                        options:0
evands@4001
   218
		                          range:NSMakeRange(0, [str length])];
evands@4001
   219
	}
evands@4001
   220
	return str;
ingmarstein@3192
   221
}
ingmarstein@3192
   222
ingmarstein@563
   223
@end
evands@4001
   224
evands@4001
   225
@implementation NSMutableString (GrowlMail_LineOrientedTruncation)
evands@4001
   226
evands@4001
   227
- (void) trimStringToFirstNLines:(unsigned)n {
evands@4001
   228
	NSRange range;
evands@4001
   229
	unsigned end;
evands@4001
   230
	unsigned length;
evands@4001
   231
evands@4001
   232
	range.location = 0;
evands@4001
   233
	range.length = 0;
evands@4001
   234
	for (unsigned i=0U; i<n; ++i)
evands@4001
   235
		[self getLineStart:NULL end:&range.location contentsEnd:&end forRange:range];
evands@4001
   236
evands@4001
   237
	length = [self length];
evands@4001
   238
	if (length > end)
evands@4001
   239
		[self deleteCharactersInRange:NSMakeRange(end, length - end)];
evands@4001
   240
}
evands@4001
   241
evands@4001
   242
@end