Extras/GrowlMail/Message+GrowlMail.m
author Peter Hosey <hg@boredzo.org>
Sat Jun 06 21:30:17 2009 -0700 (2009-06-06)
changeset 4209 f8a902d33769
parent 4172 65a0bcf23292
child 4465 845fac57d61b
permissions -rw-r--r--
Refactored GrowlMail to have a separate singleton notifier object, in order to make the mail-bundle class less dependent on being a singleton. (Assumptions are bad, especially when you're working with private/undocumented APIs.)
     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 "GrowlMailNotifier.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 	GrowlMailNotifier *notifier = [GrowlMailNotifier sharedNotifier];
    61 	NSString *titleFormat = [notifier titleFormat];
    62 	NSString *descriptionFormat = [notifier descriptionFormat];
    63 
    64 	if ([titleFormat rangeOfString:@"%body"].location != NSNotFound ||
    65 			[descriptionFormat rangeOfString:@"%body"].location != NSNotFound) {
    66 		/* We will need the body */
    67 		messageBody = [self messageBodyIfAvailable];
    68 		int nonBlockingAttempts = 0;
    69 		while (!messageBody && nonBlockingAttempts < 3) {
    70 			/* No message body available yet, but we need one */
    71 			nonBlockingAttempts++;
    72 			[NSThread sleepUntilDate:[NSDate dateWithTimeIntervalSinceNow:(0.5 * nonBlockingAttempts)]];
    73 			
    74 			/* 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 */
    75 			messageBody = [self messageBodyIfAvailable];
    76 		}
    77 
    78 		/* Already tried three times (3 seconds); this time, block this thread to get it. */ 
    79 		if (!messageBody) messageBody = [self messageBody];
    80 	}
    81 
    82 	[self performSelectorOnMainThread:@selector(GMShowNotificationPart2:)
    83 						   withObject:messageBody
    84 						waitUntilDone:NO];
    85 
    86 	[pool release];
    87 }
    88 
    89 - (void) GMShowNotificationPart2:(MessageBody *)messageBody {
    90 	NSString *account = (NSString *)[[[self mailbox] account] displayName];
    91 	NSString *sender = [self sender];
    92 	NSString *senderAddress = [sender uncommentedAddress];
    93 	NSString *subject = (NSString *)[self subject];
    94 	NSString *body;
    95 	GrowlMailNotifier *notifier = [GrowlMailNotifier sharedNotifier];
    96 	NSString *titleFormat = [notifier titleFormat];
    97 	NSString *descriptionFormat = [notifier descriptionFormat];
    98 
    99 	/* The fullName selector is not available in Mail.app 2.0. */
   100 	if ([sender respondsToSelector:@selector(fullName)])
   101 		sender = [sender fullName];
   102 	else if ([sender addressComment])
   103 		sender = [sender addressComment];
   104 
   105 	if (messageBody) {
   106 		NSString *originalBody = nil;
   107 		/* stringForIndexing selector: Mail.app 3.0 in OS X 10.4, not in 10.5. */
   108 		if ([messageBody respondsToSelector:@selector(stringForIndexing)])
   109 			originalBody = [messageBody stringForIndexing];
   110 		else if ([messageBody respondsToSelector:@selector(attributedString)])
   111 			originalBody = [[messageBody attributedString] string];
   112 		else if ([messageBody respondsToSelector:@selector(stringValueForJunkEvaluation:)])
   113 			originalBody = [messageBody stringValueForJunkEvaluation:NO];
   114 		if (originalBody) {
   115 			NSMutableString *transformedBody = [[[originalBody stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]] mutableCopy] autorelease];
   116 			unsigned lengthWithoutWhitespace = [transformedBody length];
   117 			[transformedBody trimStringToFirstNLines:4U];
   118 			unsigned length = [transformedBody length];
   119 			if (length > 200U) {
   120 				[transformedBody deleteCharactersInRange:NSMakeRange(200U, length - 200U)];
   121 				length = 200U;
   122 			}
   123 			if (length != lengthWithoutWhitespace)
   124 				[transformedBody appendString:[NSString stringWithUTF8String:"\xE2\x80\xA6"]];
   125 			body = (NSString *)transformedBody;
   126 		} else {
   127 			body = @"";	
   128 		}
   129 	} else {
   130 		body = @"";
   131 	}
   132 
   133 	NSArray *keywords = [NSArray arrayWithObjects:
   134 		@"%sender",
   135 		@"%subject",
   136 		@"%body",
   137 		@"%account",
   138 		nil];
   139 	NSArray *values = [NSArray arrayWithObjects:
   140 		(sender ? sender : @""),
   141 		(subject ? subject : @""),
   142 		(body ? body : @""),
   143 		(account ? account : @""),
   144 		 nil];
   145 	NSString *title = [titleFormat stringByReplacingKeywords:keywords withValues:values];
   146 	NSString *description = [descriptionFormat stringByReplacingKeywords:keywords withValues:values];
   147 
   148 	/*
   149 	NSLog(@"Subject: '%@'", subject);
   150 	NSLog(@"Sender: '%@'", sender);
   151 	NSLog(@"Account: '%@'", account);
   152 	NSLog(@"Body: '%@'", body);
   153 	*/
   154 
   155 	/*
   156 	 * MailAddressManager fetches images asynchronously so they might arrive
   157 	 * after we have sent our notification.
   158 	 */
   159 	/*
   160 	MailAddressManager *addressManager = [MailAddressManager addressManager];
   161 	[addressManager fetchImageForAddress:senderAddress];
   162 	NSImage *image = [addressManager imageForMailAddress:senderAddress];
   163 	*/
   164 	ABSearchElement *personSearch = [ABPerson searchElementForProperty:kABEmailProperty
   165 																 label:nil
   166 																   key:nil
   167 																 value:senderAddress
   168 															comparison:kABEqualCaseInsensitive];
   169 
   170 	NSData *image = nil;
   171 	NSEnumerator *matchesEnum = [[[ABAddressBook sharedAddressBook] recordsMatchingSearchElement:personSearch] objectEnumerator];
   172 	ABPerson *person;
   173 	while ((!image) && (person = [matchesEnum nextObject]))
   174 		image = [person imageData];
   175 
   176 	//no matches in the Address Book with an icon, so use Mail's icon instead.
   177 	if (!image)
   178 		image = [[NSImage imageNamed:@"NSApplicationIcon"] TIFFRepresentation];
   179 
   180 	NSString *notificationName;
   181 	if ([self isJunk] || ([[MailAccount junkMailboxUids] containsObject:[self mailbox]])) {
   182 		notificationName = NEW_JUNK_MAIL_NOTIFICATION;
   183 	} else {
   184 		if ([self respondsToSelector:@selector(type)] && [self type] == MESSAGE_TYPE_NOTE) {
   185 			notificationName = NEW_NOTE_NOTIFICATION;
   186 		} else {
   187 			notificationName = NEW_MAIL_NOTIFICATION;
   188 		}
   189 	}
   190 
   191 	NSString *clickContext = [self messageID];
   192 
   193 	[GrowlApplicationBridge notifyWithTitle:title
   194 								description:description
   195 						   notificationName:notificationName
   196 								   iconData:image
   197 								   priority:0
   198 								   isSticky:NO
   199 							   clickContext:clickContext];	// non-nil click context
   200 
   201 	[notifier didFinishNotificationForMessage:self];
   202 }
   203 
   204 @end
   205 
   206 @implementation NSString (GrowlMail_KeywordReplacing)
   207 
   208 - (NSString *) stringByReplacingKeywords:(NSArray *)keywords
   209                               withValues:(NSArray *)values
   210 {
   211 	NSParameterAssert([keywords count] == [values count]);
   212 	NSMutableString *str = [[self mutableCopy] autorelease];
   213 
   214 	NSEnumerator *keywordsEnum = [keywords objectEnumerator], *valuesEnum = [values objectEnumerator];
   215 	NSString *keyword, *value;
   216 	while ((keyword = [keywordsEnum nextObject]) && (value = [valuesEnum nextObject])) {
   217 		[str replaceOccurrencesOfString:keyword
   218 		                     withString:value
   219 		                        options:0
   220 		                          range:NSMakeRange(0, [str length])];
   221 	}
   222 	return str;
   223 }
   224 
   225 @end
   226 
   227 @implementation NSMutableString (GrowlMail_LineOrientedTruncation)
   228 
   229 - (void) trimStringToFirstNLines:(unsigned)n {
   230 	NSRange range;
   231 	unsigned end;
   232 	unsigned length;
   233 
   234 	range.location = 0;
   235 	range.length = 0;
   236 	for (unsigned i=0U; i<n; ++i)
   237 		[self getLineStart:NULL end:&range.location contentsEnd:&end forRange:range];
   238 
   239 	length = [self length];
   240 	if (length > end)
   241 		[self deleteCharactersInRange:NSMakeRange(end, length - end)];
   242 }
   243 
   244 @end