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