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