The Address Book can feed us giant images; after syncing my iPhone and getting a...
[adiumx.git] / Source / ESAddressBookIntegrationPlugin.m
blob0549c351c3cd5a386ea4c923c233a7cae0a6aaa6
1 /* 
2  * Adium is the legal property of its developers, whose names are listed in the copyright file included
3  * with this source distribution.
4  * 
5  * This program is free software; you can redistribute it and/or modify it under the terms of the GNU
6  * General Public License as published by the Free Software Foundation; either version 2 of the License,
7  * or (at your option) any later version.
8  * 
9  * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
10  * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General
11  * Public License for more details.
12  * 
13  * You should have received a copy of the GNU General Public License along with this program; if not,
14  * write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
15  */
17 #import "ESAddressBookIntegrationPlugin.h"
18 #import <Adium/AIAccountControllerProtocol.h>
19 #import <Adium/AIContactControllerProtocol.h>
20 #import <Adium/AIPreferenceControllerProtocol.h>
21 #import <Adium/AIMenuControllerProtocol.h>
22 #import <Adium/AIAccount.h>
23 #import <Adium/AIListObject.h>
24 #import <Adium/AIMetaContact.h>
25 #import <Adium/AIService.h>
26 #import <AIUtilities/AIAttributedStringAdditions.h>
27 #import <AIUtilities/AIDictionaryAdditions.h>
28 #import <AIUtilities/AIMutableOwnerArray.h>
29 #import <AIUtilities/AIStringAdditions.h>
30 #import <AIUtilities/OWAddressBookAdditions.h>
31 #import <AIUtilities/AIFileManagerAdditions.h>
33 #define IMAGE_LOOKUP_INTERVAL   0.01
34 #define SHOW_IN_AB_CONTEXTUAL_MENU_TITLE AILocalizedString(@"Show In Address Book", "Show In Address Book Contextual Menu")
35 #define EDIT_IN_AB_CONTEXTUAL_MENU_TITLE AILocalizedString(@"Edit In Address Book", "Edit In Address Book Contextual Menu")
36 #define ADD_TO_AB_CONTEXTUAL_MENU_TITLE AILocalizedString(@"Add To Address Book", "Add To Address Book Contextual Menu")
38 #define CONTACT_ADDED_SUCCESS_TITLE             AILocalizedString(@"Success", "Title of a panel shown after adding successfully adding a contact to the address book.")
39 #define CONTACT_ADDED_SUCCESS_Message   AILocalizedString(@"%@ had been successfully added to the Address Book.\nWould you like to edit the card now?", nil)
40 #define CONTACT_ADDED_ERROR_TITLE               AILocalizedString(@"Error", nil)
41 #define CONTACT_ADDED_ERROR_Message             AILocalizedString(@"An error had occurred while adding %@ to the Address Book.", nil)
43 @interface ESAddressBookIntegrationPlugin(PRIVATE)
44 - (void)updateAllContacts;
45 - (void)updateSelfIncludingIcon:(BOOL)includeIcon;
46 - (void)preferencesChanged:(NSNotification *)notification;
47 - (NSString *)nameForPerson:(ABPerson *)person phonetic:(NSString **)phonetic;
48 - (ABPerson *)personForListObject:(AIListObject *)inObject;
49 - (ABPerson *)_searchForUID:(NSString *)UID serviceID:(NSString *)serviceID;
50 - (void)rebuildAddressBookDict;
51 - (void)queueDelayedFetchOfImageForPerson:(ABPerson *)person object:(AIListObject *)inObject;
52 - (void)showInAddressBook;
53 - (void)editInAddressBook;
54 - (void)addToAddressBookDict:(NSArray *)people;
55 - (void)removeFromAddressBookDict:(NSArray *)UIDs;
56 - (void)installAddressBookActions;
57 @end
59 /*!
60  * @class ESAddressBookIntegrationPlugin
61  * @brief Provides Apple Address Book integration
62  *
63  * This class allows Adium to seamlessly interact with the Apple Address Book, pulling names and icons, storing icons
64  * if desired, and generating metaContacts based on screen name grouping.  It relies upon cards having screen names listed
65  * in the appropriate service fields in the address book.
66  */
67 @implementation ESAddressBookIntegrationPlugin
69 static  ABAddressBook   *sharedAddressBook = nil;
70 static  NSDictionary    *serviceDict = nil;
72 NSString* serviceIDForOscarUID(NSString *UID);
73 NSString* serviceIDForJabberUID(NSString *UID);
75 /*!
76  * @brief Install plugin
77  *
78  * This plugin finishes installing in adiumFinishedLaunching:
79  */
80 - (void)installPlugin
82     meTag = -1;
83     addressBookDict = nil;
84         listObjectArrayForImageData = nil;
85         personArrayForImageData = nil;
86         imageLookupTimer = nil;
87         createMetaContacts = NO;
89         //Tracking dictionary for asynchronous image loads
90     trackingDict = [[NSMutableDictionary alloc] init];
91     trackingDictPersonToTagNumber = [[NSMutableDictionary alloc] init];
92     trackingDictTagNumberToPerson = [[NSMutableDictionary alloc] init];
93         
94     //Configure our preferences
95     [[adium preferenceController] registerDefaults:[NSDictionary dictionaryNamed:AB_DISPLAYFORMAT_DEFAULT_PREFS 
96                                                                                                                                                 forClass:[self class]]  
97                                                                                   forGroup:PREF_GROUP_ADDRESSBOOK];
99         //We want the enableImport preference immediately (without waiting for the preferences observer to be registered in adiumFinishedLaunching:)
100         enableImport = [[[adium preferenceController] preferenceForKey:KEY_AB_ENABLE_IMPORT
101                                                                                                                          group:PREF_GROUP_ADDRESSBOOK] boolValue];
102     advancedPreferences = [[ESAddressBookIntegrationAdvancedPreferences preferencePane] retain];
103         
104     //Services dictionary
105     serviceDict = [[NSDictionary dictionaryWithObjectsAndKeys:kABAIMInstantProperty,@"AIM",
106                                                                 kABJabberInstantProperty,@"Jabber",
107                                                                 kABMSNInstantProperty,@"MSN",
108                                                                 kABYahooInstantProperty,@"Yahoo!",
109                                                                 kABICQInstantProperty,@"ICQ",nil] retain];
110         
111         //Shared Address Book
112         [sharedAddressBook release]; sharedAddressBook = [[ABAddressBook sharedAddressBook] retain];
113         
114         //Create our contextual menus
115         showInABContextualMenuItem = [[[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:SHOW_IN_AB_CONTEXTUAL_MENU_TITLE
116                                                                                                                                                            action:@selector(showInAddressBook)
117                                                                                                                                                 keyEquivalent:@""] autorelease];
118         [showInABContextualMenuItem setTarget:self];
119         
120         editInABContextualMenuItem = [[[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:EDIT_IN_AB_CONTEXTUAL_MENU_TITLE
121                                                                                                                                                                            action:@selector(editInAddressBook)
122                                                                                                                                                                 keyEquivalent:@""] autorelease];
123         [editInABContextualMenuItem setTarget:self];
124         [editInABContextualMenuItem setKeyEquivalentModifierMask:NSAlternateKeyMask];
125         [editInABContextualMenuItem setAlternate:YES];
126         
127         addToABContexualMenuItem = [[[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:ADD_TO_AB_CONTEXTUAL_MENU_TITLE
128                                                                                                                                                                          action:@selector(addToAddressBook)
129                                                                                                                                                           keyEquivalent:@""] autorelease];
130         [addToABContexualMenuItem setTarget:self];
131         
132         //Install our menues
133         [[adium menuController] addContextualMenuItem:addToABContexualMenuItem toLocation:Context_Contact_Action];
134         [[adium menuController] addContextualMenuItem:showInABContextualMenuItem toLocation:Context_Contact_Action];
135         [[adium menuController] addContextualMenuItem:editInABContextualMenuItem toLocation:Context_Contact_Action];
136         
137         [self installAddressBookActions];
138         
139         //Wait for Adium to finish launching before we build the address book so the contact list will be ready
140         [[adium notificationCenter] addObserver:self
141                                                                    selector:@selector(adiumFinishedLaunching:)
142                                                                            name:AIApplicationDidFinishLoadingNotification
143                                                                          object:nil];
144         
145         //Update self immediately so the information is available to plugins and interface elements as they load
146         [self updateSelfIncludingIcon:YES];
149 - (void)installAddressBookActions
151         NSNumber                *installedActions = [[NSUserDefaults standardUserDefaults] objectForKey:@"Adium:Installed Adress Book Actions"];
152         
153         if (!installedActions || ![installedActions boolValue]) {
154                 NSEnumerator  *enumerator = [[NSArray arrayWithObjects:@"AIM", @"MSN", @"Yahoo", @"ICQ", @"Jabber", @"SMS", nil] objectEnumerator];
155                 NSString          *name;
156                 NSFileManager *fileManager = [NSFileManager defaultManager];
157                 NSArray           *libraryDirectoryArray;
158                 NSString          *libraryDirectory, *pluginDirectory;
160                 libraryDirectoryArray = NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, YES);
161                 if ([libraryDirectoryArray count]) {
162                         libraryDirectory = [libraryDirectoryArray objectAtIndex:0];
164                 } else {
165                         //Ridiculous safety since everyone should have a Library folder...
166                         libraryDirectory = [NSHomeDirectory() stringByAppendingPathComponent:@"Library"];
167                         [fileManager createDirectoryAtPath:libraryDirectory attributes:nil];
168                 }
170                 pluginDirectory = [[libraryDirectory stringByAppendingPathComponent:@"Address Book Plug-Ins"] stringByAppendingPathComponent:@"/"];
171                 [fileManager createDirectoryAtPath:pluginDirectory attributes:nil];
172                 
173                 while ((name = [enumerator nextObject])) {
174                         NSString *fullName = [NSString stringWithFormat:@"AdiumAddressBookAction_%@",name];
175                         NSString *path = [[NSBundle bundleForClass:[self class]] pathForResource:fullName ofType:@"scpt"];
177                         if (path) {
178                                 [fileManager copyPath:path
179                                                            toPath:[pluginDirectory stringByAppendingPathComponent:[fullName stringByAppendingPathExtension:@"scpt"]]
180                                                           handler:NULL];
181                                 
182                                 //Remove the old xtra if installed
183                                 [fileManager trashFileAtPath:[pluginDirectory stringByAppendingPathComponent:
184                                         [NSString stringWithFormat:@"%@-Adium.scpt",name]]];
185                         } else {
186                                 AILogWithSignature(@"Warning: Could not find %@",self, fullName);
187                         }
188                 }
190                 [[NSUserDefaults standardUserDefaults] setObject:[NSNumber numberWithBool:YES]
191                                                                                                   forKey:@"Adium:Installed Adress Book Actions"];
192         }
196  * @brief Uninstall plugin
197  */
198 - (void)uninstallPlugin
200     [[adium contactController] unregisterListObjectObserver:self];
201     [[adium notificationCenter] removeObserver:self];
205  * @brief Deallocate
206  */
207 - (void)dealloc
209     [serviceDict release]; serviceDict = nil;
210     [trackingDict release]; trackingDict = nil;
211         [trackingDictPersonToTagNumber release]; trackingDictPersonToTagNumber = nil;
212         [trackingDictTagNumberToPerson release]; trackingDictTagNumberToPerson = nil;
214         [sharedAddressBook release]; sharedAddressBook = nil;
216         [[adium preferenceController] unregisterPreferenceObserver:self];
217         [[adium notificationCenter] removeObserver:self];
219         [super dealloc];
223  * @brief Adium finished launching
225  * Register our observers for the address book changing externally and for the account list changing.
226  * Register our preference observers. This will trigger initial building of the address book dictionary.
227  */
228 - (void)adiumFinishedLaunching:(NSNotification *)notification
229 {       
230     //Observe external address book changes
231     [[NSNotificationCenter defaultCenter] addObserver:self
232                                                                                          selector:@selector(addressBookChanged:)
233                                                                                                  name:kABDatabaseChangedExternallyNotification
234                                                                                            object:nil];
236     //Observe account changes
237     [[adium notificationCenter] addObserver:self
238                                                                    selector:@selector(accountListChanged:)
239                                                                            name:Account_ListChanged
240                                                                          object:nil];
242     //Observe preferences changes
243     id<AIPreferenceController> preferenceController = [adium preferenceController];
244         [preferenceController registerPreferenceObserver:self forGroup:PREF_GROUP_ADDRESSBOOK];
245         [preferenceController registerPreferenceObserver:self forGroup:PREF_GROUP_USERICONS];
249  * @brief Used as contacts are created and icons are changed.
251  * When first created, load a contact's address book information from our dict.
252  * When an icon as a status object changes, if desired, write the changed icon out to the appropriate AB card.
253  */
254 - (NSSet *)updateListObject:(AIListObject *)inObject keys:(NSSet *)inModifiedKeys silent:(BOOL)silent
256         //Just stop here if we don't have an address book dict to work with
257         if (!addressBookDict) return nil;
258         
259         //We handle accounts separately; doing updates here causes chaos in addition to being inefficient.
260         if ([inObject isKindOfClass:[AIAccount class]]) return nil;
261         
262         NSSet           *modifiedAttributes = nil;
263         
264     if (inModifiedKeys == nil) { //Only perform this when updating for all list objects
265         ABPerson *person = [self personForListObject:inObject];
266                 
267                 if (person) {
268                         [self queueDelayedFetchOfImageForPerson:person object:inObject];
270                         if (enableImport) {
271                                 //Load the name if appropriate
272                                 AIMutableOwnerArray *displayNameArray, *phoneticNameArray;
273                                 NSString                        *displayName, *phoneticName = nil;
274                                 
275                                 displayNameArray = [inObject displayArrayForKey:@"Display Name"];
276                                 
277                                 displayName = [self nameForPerson:person phonetic:&phoneticName];
278                                 
279                                 //Apply the values 
280                                 NSString *oldValue = [displayNameArray objectWithOwner:self];
281                                 if (!oldValue || ![oldValue isEqualToString:displayName]) {
282                                         [displayNameArray setObject:displayName withOwner:self];
283                                         modifiedAttributes = [NSSet setWithObject:@"Display Name"];
284                                 }
285                                 
286                                 if (phoneticName) {
287                                         phoneticNameArray = [inObject displayArrayForKey:@"Phonetic Name"];
289                                         //Apply the values 
290                                         oldValue = [phoneticNameArray objectWithOwner:self];
291                                         if (!oldValue || ![oldValue isEqualToString:phoneticName]) {
292                                                 [phoneticNameArray setObject:phoneticName withOwner:self];
293                                                 modifiedAttributes = [NSSet setWithObjects:@"Display Name", @"Phonetic Name", nil];
294                                         }
295                                 } else {
296                                         phoneticNameArray = [inObject displayArrayForKey:@"Phonetic Name"
297                                                                                                                           create:NO];
298                                         //Clear any stored value
299                                         if ([phoneticNameArray objectWithOwner:self]) {
300                                                 [displayNameArray setObject:nil withOwner:self];
301                                                 modifiedAttributes = [NSSet setWithObjects:@"Display Name", @"Phonetic Name", nil];
302                                         }                                       
303                                 }
305                         } else {
306                                 AIMutableOwnerArray *displayNameArray, *phoneticNameArray;
307                                 
308                                 displayNameArray = [inObject displayArrayForKey:@"Display Name"
309                                                                                                                  create:NO];
311                                 //Clear any stored value
312                                 if ([displayNameArray objectWithOwner:self]) {
313                                         [displayNameArray setObject:nil withOwner:self];
314                                         modifiedAttributes = [NSSet setWithObject:@"Display Name"];
315                                 }
316                                 
317                                 phoneticNameArray = [inObject displayArrayForKey:@"Phonetic Name"
318                                                                                                                   create:NO];
319                                 //Clear any stored value
320                                 if ([phoneticNameArray objectWithOwner:self]) {
321                                         [displayNameArray setObject:nil withOwner:self];
322                                         modifiedAttributes = [NSSet setWithObjects:@"Display Name", @"Phonetic Name", nil];
323                                 }                                       
324                                 
325                         }
327                         //If we changed anything, request an update of the alias / long display name
328                         if (modifiedAttributes) {
329                                 [[adium notificationCenter] postNotificationName:Contact_ApplyDisplayName
330                                                                                                                   object:inObject
331                                                                                                                 userInfo:[NSDictionary dictionaryWithObject:[NSNumber numberWithBool:silent]
332                                                                                                                                                                                          forKey:@"Notify"]];
333                         }
334                 }
335                 
336     } else if (automaticSync && [inModifiedKeys containsObject:KEY_USER_ICON]) {
337         
338                 //Only update when the serverside icon changes if there is no Adium preference overriding it
339                 if (![inObject preferenceForKey:KEY_USER_ICON group:PREF_GROUP_USERICONS ignoreInheritedValues:YES]) {
340                         //Find the person
341                         ABPerson *person = [self personForListObject:inObject];
342                         
343                         if (person && (person != [sharedAddressBook me])) {
344                                 //Set the person's image to the inObject's serverside User Icon.
345                                 NSData  *userIconData = [inObject statusObjectForKey:@"UserIconData"];
346                                 if (!userIconData) {
347                                         userIconData = [[inObject statusObjectForKey:KEY_USER_ICON] TIFFRepresentation];
348                                 }
349                                 
350                                 [person setImageData:userIconData];
351                                 
352                                 [[sharedAddressBook class] cancelPreviousPerformRequestsWithTarget:sharedAddressBook
353                                                                                                                                                   selector:@selector(save)
354                                                                                                                                                         object:nil];
355                                 [sharedAddressBook performSelector:@selector(save)
356                                                                                 withObject:nil
357                                                                                 afterDelay:5.0];
358                         }
359                 }
360     }
361     
362     return modifiedAttributes;
366  * @brief Return the name of an ABPerson in the way Adium should display it
368  * @param person An <tt>ABPerson</tt>
369  * @param phonetic A pointer to an <tt>NSString</tt> which will be filled with the phonetic display name if available
370  * @result A string based on the first name, last name, and/or nickname of the person, as specified via preferences.
371  */
372 - (NSString *)nameForPerson:(ABPerson *)person phonetic:(NSString **)phonetic
374         NSString *firstName, *middleName, *lastName, *phoneticFirstName, *phoneticLastName;     
375         NSString *nickName;
376         NSString *displayName = nil;
377         NSNumber *flags;
378         
379         // If the record is for a company, return the company name if present
380         if ((flags = [person valueForProperty:kABPersonFlags])) {
381                 if (([flags intValue] & kABShowAsMask) == kABShowAsCompany) {
382                         NSString *companyName = [person valueForProperty:kABOrganizationProperty];
383                         if (companyName && [companyName length]) {
384                                 return companyName;
385                         }
386                 }
387         }
388                 
389         firstName = [person valueForProperty:kABFirstNameProperty];
390         middleName = [person valueForProperty:kABMiddleNameProperty];
391         lastName = [person valueForProperty:kABLastNameProperty];
392         phoneticFirstName = [person valueForProperty:kABFirstNamePhoneticProperty];
393         phoneticLastName = [person valueForProperty:kABLastNamePhoneticProperty];
394         
395         //
396         if (useMiddleName && middleName)
397                 firstName = [NSString stringWithFormat:@"%@ %@", firstName, middleName];
399         if (useNickName && (nickName = [person valueForProperty:kABNicknameProperty])) {
400                 displayName = nickName;
402         } else if (!lastName || (displayFormat == First)) {  
403                 /* If no last name is available, use the first name */
404                 displayName = firstName;
405                 if (phonetic != NULL) *phonetic = phoneticFirstName;
407         } else if (!firstName) {
408                 /* If no first name is available, use the last name */
409                 displayName = lastName;
410                 if (phonetic != NULL) *phonetic = phoneticLastName;
412         } else {
413                 BOOL havePhonetic = ((phonetic != NULL) && (phoneticFirstName || phoneticLastName));
415                 /* Look to the preference setting */
416                 switch (displayFormat) {
417                         case FirstLast:
418                                 displayName = [NSString stringWithFormat:@"%@ %@",firstName,lastName];
419                                 if (havePhonetic) {
420                                         *phonetic = [NSString stringWithFormat:@"%@ %@",
421                                                 (phoneticFirstName ? phoneticFirstName : firstName),
422                                                 (phoneticLastName ? phoneticLastName : lastName)];
423                                 }
424                                 break;
425                         case LastFirst:
426                                 displayName = [NSString stringWithFormat:@"%@, %@",lastName,firstName]; 
427                                 if (havePhonetic) {
428                                         *phonetic = [NSString stringWithFormat:@"%@, %@",
429                                                 (phoneticLastName ? phoneticLastName : lastName),
430                                                 (phoneticFirstName ? phoneticFirstName : firstName)];
431                                 }
432                                 break;
433                         case LastFirstNoComma:
434                                 displayName = [NSString stringWithFormat:@"%@ %@",lastName,firstName]; 
435                                 if (havePhonetic) {
436                                         *phonetic = [NSString stringWithFormat:@"%@ %@",
437                                                 (phoneticLastName ? phoneticLastName : lastName),
438                                                 (phoneticFirstName ? phoneticFirstName : firstName)];
439                                 }                                       
440                                 break;
441                         case FirstLastInitial:
442                                 displayName = [NSString stringWithFormat:@"%@ %@",firstName,[lastName substringToIndex:1]]; 
443                                 if (havePhonetic) {
444                                         *phonetic = [NSString stringWithFormat:@"%@ %@",
445                                                                  (phoneticFirstName ? phoneticFirstName : firstName),
446                                                                  [lastName substringToIndex:1]];
447                                 }
448                         case First:
449                                 //No action; handled before we reach the switch statement
450                                 break;
451                 }
452         }
454         return displayName;
458  * @brief Observe preference changes
460  * On first call, this method builds the addressBookDict. Subsequently, it rebuilds the dict only if the "create metaContacts"
461  * option is toggled, as metaContacts are created while building the dict.
463  * If the user set a new image as a preference for an object, write it out to the contact's AB card if desired.
464  */
465 - (void)preferencesChangedForGroup:(NSString *)group key:(NSString *)key
466                                                         object:(AIListObject *)object preferenceDict:(NSDictionary *)prefDict firstTime:(BOOL)firstTime
468     if ([group isEqualToString:PREF_GROUP_ADDRESSBOOK]) {
469                 BOOL                    oldCreateMetaContacts = createMetaContacts;
470                 
471         //load new displayFormat
472                 enableImport = [[prefDict objectForKey:KEY_AB_ENABLE_IMPORT] boolValue];
473                 displayFormat = [[prefDict objectForKey:KEY_AB_DISPLAYFORMAT] intValue];
474         automaticSync = [[prefDict objectForKey:KEY_AB_IMAGE_SYNC] boolValue];
475         useNickName = [[prefDict objectForKey:KEY_AB_USE_NICKNAME] boolValue];
476                 useMiddleName = [[prefDict objectForKey:KEY_AB_USE_MIDDLE] boolValue];
477                 preferAddressBookImages = [[prefDict objectForKey:KEY_AB_PREFER_ADDRESS_BOOK_IMAGES] boolValue];
478                 useABImages = [[prefDict objectForKey:KEY_AB_USE_IMAGES] boolValue];
480                 createMetaContacts = [[prefDict objectForKey:KEY_AB_CREATE_METACONTACTS] boolValue];
481                 
482                 if (firstTime) {
483                         //Build the address book dictionary, which will also trigger metacontact grouping as appropriate
484                         [self rebuildAddressBookDict];
485                         
486                         //Register ourself as a listObject observer, which will update all objects
487                         [[adium contactController] registerListObjectObserver:self];
488                         
489                         //Note: we don't need to call updateSelfIncludingIcon: because it was already done in installPlugin                     
490                 } else {
491                         //This isn't the first time through
493                         //If we weren't creating meta contacts before but we are now
494                         if (!oldCreateMetaContacts && createMetaContacts) {
495                                 /*
496                                  Build the address book dictionary, which will also trigger metacontact grouping as appropriate
497                                  Delay to the next run loop to give better UI responsiveness
498                                  */
499                                 [self performSelector:@selector(rebuildAddressBookDict)
500                                                    withObject:nil
501                                                    afterDelay:0.0001];
502                         }
503                         
504                         //Update all contacts, which will update objects and then our "me" card information
505                         [self updateAllContacts];
506                 }
508     } else if (automaticSync && ([group isEqualToString:PREF_GROUP_USERICONS]) && object) {
509                 //Find the person
510                 ABPerson *person = [self personForListObject:object];
511                 
512                 if (person) {
513                         //Set the person's image to the inObject's serverside User Icon.
514                         NSData  *imageData = [object preferenceForKey:KEY_USER_ICON
515                                                                                                         group:PREF_GROUP_USERICONS
516                                                                         ignoreInheritedValues:YES];
517                         
518                         //If the pref is now nil, we should restore the address book back to the serverside icon if possible
519                         if (!imageData) {
520                                 imageData = [[object statusObjectForKey:KEY_USER_ICON] TIFFRepresentation];
521                         }
522                         
523                         [person setImageData:imageData];
524                 }
525         }
529  * @brief Returns the appropriate service for the property.
531  * @param property - an ABPerson property.
532  */
533 + (AIService *)serviceFromProperty:(NSString *)property
535         NSString        *serviceID = nil;
536         
537         if ([property isEqualToString:kABAIMInstantProperty])
538                 serviceID = @"AIM";
539         
540         else if ([property isEqualToString:kABICQInstantProperty])
541                 serviceID = @"ICQ";
542         
543         else if ([property isEqualToString:kABMSNInstantProperty])
544                 serviceID = @"MSN";
545         
546         else if ([property isEqualToString:kABJabberInstantProperty])
547                 serviceID = @"Jabber";
548         
549         else if ([property isEqualToString:kABYahooInstantProperty])
550                 serviceID = @"Yahoo!";
552         return (serviceID ? [[[AIObject sharedAdiumInstance] accountController] firstServiceWithServiceID:serviceID] : nil);
556  * @brief Returns the appropriate property for the service.
557  */
558 + (NSString *)propertyFromService:(AIService *)inService
560         NSString *result;
561         NSString *serviceID = [inService serviceID];
563         result = [serviceDict objectForKey:serviceID];
565         //Check for some special cases
566         if (!result) {
567                 if ([serviceID isEqualToString:@"GTalk"]) {
568                         result = kABJabberInstantProperty;
569                 } else if ([serviceID isEqualToString:@"LiveJournal"]) {
570                         result = kABJabberInstantProperty;
571                 } else if ([serviceID isEqualToString:@"Mac"]) {
572                         result = kABAIMInstantProperty;
573                 }
574         }
575         
576         return result;
579 #pragma mark Image data
582  * @brief Called when the address book completes an asynchronous image lookup
584  * @param inData NSData representing an NSImage
585  * @param tag A tag indicating the lookup with which this call is associated. We use a tracking dictionary, trackingDict, to associate this int back to a usable object.
586  */
587 - (void)consumeImageData:(NSData *)inData forTag:(int)tag
589         if (tag == meTag) {
590                 [[adium preferenceController] setPreference:inData
591                                                                                          forKey:KEY_DEFAULT_USER_ICON 
592                                                                                           group:GROUP_ACCOUNT_STATUS];
593                 meTag = -1;
594                 
595         } else if (useABImages) {
596                 NSNumber                *tagNumber;
597                 NSImage                 *image;
598                 AIListObject    *listObject;
599 //              AIListContact   *parentContact;
600                 NSString                *uniqueID;
601                 id                              setOrObject;
602                 
603                 tagNumber = [NSNumber numberWithInt:tag];
604                 
605                 //Apply the image to the appropriate listObject
606                 image = (inData ? [[[NSImage alloc] initWithData:inData] autorelease] : nil);
608                 //Address book can feed us giant images, which we really don't want to keep around
609                 NSSize size = [image size];
610                 if (size.width > 96 || size.height > 96)
611                         image = [image imageByScalingToSize:NSMakeSize(96, 96)];
612                 [image setDataRetained:YES];
614                 //Get the object from our tracking dictionary
615                 setOrObject = [trackingDict objectForKey:tagNumber];
616                 
617                 if ([setOrObject isKindOfClass:[AIListObject class]]) {
618                         listObject = (AIListObject *)setOrObject;
619                         
620                         //Apply the image at the appropriate priority
621                         [listObject setDisplayUserIcon:image
622                                                                  withOwner:self
623                                                          priorityLevel:(preferAddressBookImages ? High_Priority : Low_Priority)];
625                         /*
626                         parentContact = [listObject parentContact];
627                          */
628                         
629                 } else /*if ([setOrObject isKindOfClass:[NSSet class]])*/{
630                         NSEnumerator    *enumerator;
631 //                      BOOL                    checkedForMetaContact = NO;
633                         //Apply the image to each listObject at the appropriate priority
634                         enumerator = [(NSSet *)setOrObject objectEnumerator];
635                         while ((listObject = [enumerator nextObject])) {
636                                 
637                                 /*
638                                 //These objects all have the same unique ID so will all also have the same meta contact; just check once
639                                 if (!checkedForMetaContact) {
640                                         parentContact = [listObject parentContact];
641                                         if (parentContact == listObject) parentContact = nil;
642                                         checkedForMetaContact = YES;
643                                 }
644                                 */
645                                 
646                                 [listObject setDisplayUserIcon:image
647                                                                          withOwner:self
648                                                                  priorityLevel:(preferAddressBookImages ? High_Priority : Low_Priority)];
649                         }
650                 }
651                 
652                 /*
653                 if (parentContact) {
654                         [parentContact setDisplayUserIcon:image
655                                                                         withOwner:self
656                                                                 priorityLevel:(preferAddressBookImages ? High_Priority : Low_Priority)];                        
657                 }
658                 */
659                 
660                 //No further need for the dictionary entries
661                 [trackingDict removeObjectForKey:tagNumber];
662                 
663                 if ((uniqueID = [trackingDictTagNumberToPerson objectForKey:tagNumber])) {
664                         [trackingDictPersonToTagNumber removeObjectForKey:uniqueID];
665                         [trackingDictTagNumberToPerson removeObjectForKey:tagNumber];
666                 }
667         }
671  * @brief Queue an asynchronous image fetch for person associated with inObject
673  * Image lookups are done asynchronously.  This allows other processing to be done between image calls, improving the perceived
674  * speed.  [Evan: I have seen one instance of this being problematic. My localhost loop was broken due to odd network problems,
675  *                      and the asynchronous lookup therefore hung the problem.  Submitted as radar 3977541.]
677  * We load from the same ABPerson for multiple AIListObjects, one for each service/UID combination times
678  * the number of accounts on that service.  We therefore aggregate the lookups to lower the address book search
679  * and image/data creation overhead.
681  * @param person The ABPerson to fetch the image from
682  * @param inObject The AIListObject with which to ultimately associate the image
683  */
684 - (void)queueDelayedFetchOfImageForPerson:(ABPerson *)person object:(AIListObject *)inObject
686         int                             tag;
687         NSNumber                *tagNumber;
688         NSString                *uniqueId;
689         
690         uniqueId = [person uniqueId];
691         
692         //Check if we already have a tag for the loading of another object with the same
693         //internalObjectID
694         if ((tagNumber = [trackingDictPersonToTagNumber objectForKey:uniqueId])) {
695                 id                              previousValue;
696                 NSMutableSet    *objectSet;
697                 
698                 previousValue = [trackingDict objectForKey:tagNumber];
699                 
700                 if ([previousValue isKindOfClass:[AIListObject class]]) {
701                         //If the old value is just a listObject, create an array with the old object
702                         //and the new object
703                         objectSet = [NSMutableSet setWithObjects:previousValue,inObject,nil];
704                         
705                         //Store the array in the tracking dict
706                         [trackingDict setObject:objectSet forKey:tagNumber];
707                         
708                 } else /*if ([previousValue isKindOfClass:[NSMutableArray class]])*/{
709                         //Add the new object to the previously-created array
710                         [(NSMutableSet *)previousValue addObject:inObject];
711                 }
712                 
713         } else {
714                 //Begin the image load
715                 tag = [person beginLoadingImageDataForClient:self];
716                 tagNumber = [NSNumber numberWithInt:tag];
717                 
718                 //We need to be able to take a tagNumber and retrieve the object
719                 [trackingDict setObject:inObject forKey:tagNumber];
720                 
721                 //We also want to take a person's uniqueID and potentially find an existing tag number
722                 [trackingDictPersonToTagNumber setObject:tagNumber forKey:uniqueId];
723                 [trackingDictTagNumberToPerson setObject:uniqueId forKey:tagNumber];
724         }
727 #pragma mark Searching
729  * @brief Find an ABPerson corresponding to an AIListObject
731  * @param inObject The object for which it search
732  * @result An ABPerson is one is found, or nil if none is found
733  */
734 - (ABPerson *)personForListObject:(AIListObject *)inObject
736         ABPerson        *person = nil;
737         NSString        *uniqueID = [inObject preferenceForKey:KEY_AB_UNIQUE_ID group:PREF_GROUP_ADDRESSBOOK];
738         ABRecord        *record = nil;
739         
740         if (uniqueID)
741                 record = [sharedAddressBook recordForUniqueId:uniqueID];
742         
743         if (record && [record isKindOfClass:[ABPerson class]]) {
744                 person = (ABPerson *)record;
745         } else {
746                 if ([inObject isKindOfClass:[AIMetaContact class]]) {
747                         NSEnumerator    *enumerator;
748                         AIListContact   *listContact;
749                         
750                         //Search for an ABPerson for each listContact within the metaContact; first one we find is
751                         //the lucky winner.
752                         enumerator = [[(AIMetaContact *)inObject listContacts] objectEnumerator];
753                         while ((listContact = [enumerator nextObject]) && (person == nil)) {
754                                 person = [self personForListObject:listContact];
755                         }
756                         
757                 } else {
758                         NSString                *UID = [inObject UID];
759                         NSString                *serviceID = [[inObject service] serviceID];
760                         
761                         person = [self _searchForUID:UID serviceID:serviceID];
762                         
763                         /* If we don't find anything yet, look at alternative service possibilities:
764                          *    AIM <--> ICQ
765                          */
766                         if (!person) {
767                                 if ([serviceID isEqualToString:@"AIM"]) {
768                                         person = [self _searchForUID:UID serviceID:@"ICQ"];
769                                 } else if ([serviceID isEqualToString:@"ICQ"]) {
770                                         person = [self _searchForUID:UID serviceID:@"AIM"];
771                                 }
772                         }
773                 }
774         }
775         return person;
779  * @brief Find an ABPerson for a given UID and serviceID combination
780  * 
781  * Uses our addressBookDict cache created in rebuildAddressBook.
783  * @param UID The UID for the contact
784  * @param serviceID The serviceID for the contact
785  * @result A corresponding <tt>ABPerson</tt>
786  */
787 - (ABPerson *)_searchForUID:(NSString *)UID serviceID:(NSString *)serviceID
789         ABPerson                *person = nil;
790         NSDictionary    *dict;
791         
792         if ([serviceID isEqualToString:@"Mac"]) {
793                 dict = [addressBookDict objectForKey:@"AIM"];
795         } else if ([serviceID isEqualToString:@"GTalk"]) {
796                 dict = [addressBookDict objectForKey:@"Jabber"];
798         } else if ([serviceID isEqualToString:@"LiveJournal"]) {
799                 dict = [addressBookDict objectForKey:@"Jabber"];
800                 
801         } else if ([serviceID isEqualToString:@"Yahoo! Japan"]) {
802                 dict = [addressBookDict objectForKey:@"Yahoo!"];
803                 
804         } else {
805                 dict = [addressBookDict objectForKey:serviceID];
806         } 
807         
808         if (dict) {
809                 NSString *uniqueID = [dict objectForKey:[UID compactedString]];
810                 if (uniqueID) {
811                         person = (ABPerson *)[sharedAddressBook recordForUniqueId:uniqueID];
812                 }
813         }
814         
815         return person;
818 - (NSSet *)contactsForPerson:(ABPerson *)person
820         NSArray                 *allServiceKeys = [serviceDict allKeys];
821         NSString                *serviceID;
822         NSMutableSet    *contactSet = [NSMutableSet set];
823         NSEnumerator    *servicesEnumerator;
824         ABMultiValue    *emails;
825         int                             i, emailsCount;
827         //An ABPerson may have multiple emails; iterate through them looking for @mac.com addresses
828         {
829                 emails = [person valueForProperty:kABEmailProperty];
830                 emailsCount = [emails count];
831                 
832                 for (i = 0; i < emailsCount ; i++) {
833                         NSString        *email;
834                         
835                         email = [emails valueAtIndex:i];
836                         if ([email hasSuffix:@"@mac.com"]) {
837                                 //Retrieve all appropriate contacts
838                                 NSSet   *contacts = [[adium contactController] allContactsWithService:[[adium accountController] firstServiceWithServiceID:@"Mac"]
839                                                                                                                                                                   UID:email
840                                                                                                                                                  existingOnly:YES];
842                                 //Add them to our set
843                                 [contactSet unionSet:contacts];
844                         }
845                 }
846         }
847         
848         //Now go through the instant messaging keys
849         servicesEnumerator = [allServiceKeys objectEnumerator];
850         while ((serviceID = [servicesEnumerator nextObject])) {
851                 NSString                *addressBookKey = [serviceDict objectForKey:serviceID];
852                 ABMultiValue    *names;
853                 int                             nameCount;
855                 //An ABPerson may have multiple names; iterate through them
856                 names = [person valueForProperty:addressBookKey];
857                 nameCount = [names count];
858                 
859                 //Continue to the next serviceID immediately if no names are found
860                 if (nameCount == 0) continue;
861                 
862                 BOOL                                    isOSCAR = ([serviceID isEqualToString:@"AIM"] || 
863                                                                                    [serviceID isEqualToString:@"ICQ"]);
864                 BOOL                                    isJabber = [serviceID isEqualToString:@"Jabber"] ||
865                                            [serviceID isEqualToString:@"XMPP"];
866                 
867                 for (i = 0 ; i < nameCount ; i++) {
868                         NSString        *UID = [[names valueAtIndex:i] compactedString];
869                         if ([UID length]) {
870                                 if (isOSCAR) {
871                                         serviceID = serviceIDForOscarUID(UID);
872                                         
873                                 } else if (isJabber) {
874                                         serviceID = serviceIDForJabberUID(UID);
875                                 }
876                                 
877                                 NSSet   *contacts = [[adium contactController] allContactsWithService:[[adium accountController] firstServiceWithServiceID:serviceID]
878                                                                                                                                                                   UID:UID
879                                                                                                                                                  existingOnly:YES];
880                                 
881                                 //Add them to our set
882                                 [contactSet unionSet:contacts];
883                         }
884                 }
885         }
887         return contactSet;
890 #pragma mark Address book changed
892  * @brief Address book changed externally
894  * As a result we add/remove people to/from our address book dictionary cache and update all contacts based on it
895  */
896 - (void)addressBookChanged:(NSNotification *)notification
898         /* In case of a single person, these will be NSStrings.
899          * In case of more then one, they are will be NSArrays containing NSStrings.
900          */     
901         id                              addedPeopleUniqueIDs, modifiedPeopleUniqueIDs, deletedPeopleUniqueIDs;
902         NSMutableSet    *allModifiedPeople = [[NSMutableSet alloc] init];
903         ABPerson                *me = [sharedAddressBook me];
904         BOOL                    modifiedMe = NO;;
906         //Delay listObjectNotifications to speed up metaContact creation
907         [[adium contactController] delayListObjectNotifications];
909         //Addition of new records
910         if ((addedPeopleUniqueIDs = [[notification userInfo] objectForKey:kABInsertedRecords])) {
911                 NSArray *peopleToAdd;
913                 if ([addedPeopleUniqueIDs isKindOfClass:[NSArray class]]) {
914                         //We are dealing with multiple records
915                         peopleToAdd = [sharedAddressBook peopleFromUniqueIDs:(NSArray *)addedPeopleUniqueIDs];
916                 } else {
917                         //We have only one record
918                         peopleToAdd = [NSArray arrayWithObject:(ABPerson *)[sharedAddressBook recordForUniqueId:addedPeopleUniqueIDs]];
919                 }
921                 [allModifiedPeople addObjectsFromArray:peopleToAdd];
922                 [self addToAddressBookDict:peopleToAdd];
923         }
924         
925         //Modification of existing records
926         if ((modifiedPeopleUniqueIDs = [[notification userInfo] objectForKey:kABUpdatedRecords])) {
927                 NSArray *peopleToAdd;
929                 if ([modifiedPeopleUniqueIDs isKindOfClass:[NSArray class]]) {
930                         //We are dealing with multiple records
931                         [self removeFromAddressBookDict:modifiedPeopleUniqueIDs];
932                         peopleToAdd = [sharedAddressBook peopleFromUniqueIDs:modifiedPeopleUniqueIDs];
933                 } else {
934                         //We have only one record
935                         [self removeFromAddressBookDict:[NSArray arrayWithObject:modifiedPeopleUniqueIDs]];
936                         peopleToAdd = [NSArray arrayWithObject:(ABPerson *)[sharedAddressBook recordForUniqueId:modifiedPeopleUniqueIDs]];
937                 }
938                 
939                 [allModifiedPeople addObjectsFromArray:peopleToAdd];
940                 [self addToAddressBookDict:peopleToAdd];
941         }
942         
943         //Deletion of existing records
944         if ((deletedPeopleUniqueIDs = [[notification userInfo] objectForKey:kABDeletedRecords])) {
945                 if ([deletedPeopleUniqueIDs isKindOfClass:[NSArray class]]) {
946                         //We are dealing with multiple records
947                         [self removeFromAddressBookDict:deletedPeopleUniqueIDs];
948                 } else {
949                         //We have only one record
950                         [self removeFromAddressBookDict:[NSArray arrayWithObject:deletedPeopleUniqueIDs]];
951                 }
952                 
953                 //Note: We have no way of retrieving the records of people who were removed, so we really can't do much here.
954         }
955         
956         NSEnumerator    *peopleEnumerator;
957         ABPerson                *person;
958         
959         //Do appropriate updates for each updated ABPerson
960         peopleEnumerator = [allModifiedPeople objectEnumerator];
961         while ((person = [peopleEnumerator nextObject])) {
962                 if (person == me) {
963                         modifiedMe = YES;
964                 }
966                 //It's tempting to not do this if (person == me), but the 'me' contact may also be in the contact list
967                 [[adium contactController] updateContacts:[self contactsForPerson:person]
968                                                                           forObserver:self];
969         }
971         //Update us if appropriate
972         if (modifiedMe) {
973                 [self updateSelfIncludingIcon:YES];
974         }
975         
976         //Stop delaying list object notifications since we are done
977         [[adium contactController] endListObjectNotificationsDelay];
978         [allModifiedPeople release];
982  * @brief Update all existing contacts and accounts
983  */
984 - (void)updateAllContacts
986         [[adium contactController] updateAllListObjectsForObserver:self];
987     [self updateSelfIncludingIcon:YES];
991  * @brief Account list changed: Update all existing accounts
992  */
993 - (void)accountListChanged:(NSNotification *)notification
995         [self updateSelfIncludingIcon:NO];
999  * @brief Update all existing accounts
1001  * We use the "me" card to determine the default icon and account display name
1002  */
1003 - (void)updateSelfIncludingIcon:(BOOL)includeIcon
1005         @try
1006         {
1007         //Begin loading image data for the "me" address book entry, if one exists
1008         ABPerson *me;
1009         if ((me = [sharedAddressBook me])) {
1010                         
1011                         //Default buddy icon
1012                         if (includeIcon) {
1013                                 //Begin the image load
1014                                 meTag = [me beginLoadingImageDataForClient:self];
1015                         }
1016                         
1017                         //Set account display names
1018                         if (enableImport) {
1019                                 NSString                *myDisplayName, *myPhonetic = nil;
1020                                 
1021                                 myDisplayName = [self nameForPerson:me phonetic:&myPhonetic];
1022                                 
1023                                 NSEnumerator    *accountsArray = [[[adium accountController] accounts] objectEnumerator];
1024                                 AIAccount               *account;
1025                                 
1026                                 while ((account = [accountsArray nextObject])) {
1027                                         if (![account isTemporary]) {
1028                                                 [[account displayArrayForKey:@"Display Name"] setObject:myDisplayName
1029                                                                                                                                           withOwner:self
1030                                                                                                                                   priorityLevel:Low_Priority];
1031                                                 
1032                                                 if (myPhonetic) {
1033                                                         [[account displayArrayForKey:@"Phonetic Name"] setObject:myPhonetic
1034                                                                                                                                                    withOwner:self
1035                                                                                                                                            priorityLevel:Low_Priority];                                                                         
1036                                                 }
1037                                         }
1038                                 }
1040                                 [[adium preferenceController] registerDefaults:[NSDictionary dictionaryWithObject:[[NSAttributedString stringWithString:myDisplayName] dataRepresentation]
1041                                                                                                                                                                                    forKey:KEY_ACCOUNT_DISPLAY_NAME]
1042                                                                                                           forGroup:GROUP_ACCOUNT_STATUS];
1043                         }
1044         }
1045         }
1046         @catch(id exc)
1047         {
1048                 NSLog(@"ABIntegration: Caught %@", exc);
1049         }
1052 #pragma mark Address book caching
1054  * @brief rebuild our address book lookup dictionary
1055  */
1056 - (void)rebuildAddressBookDict
1058         //Delay listObjectNotifications to speed up metaContact creation
1059         [[adium contactController] delayListObjectNotifications];
1060         
1061         [addressBookDict release]; addressBookDict = [[NSMutableDictionary alloc] init];
1062         
1063         [self addToAddressBookDict:[sharedAddressBook people]];
1065         //Stop delaying list object notifications since we are done
1066         [[adium contactController] endListObjectNotificationsDelay];
1071  * @brief Service ID for an OSCAR UID
1073  * If we are on an OSCAR service we need to resolve our serviceID into the appropriate string
1074  * because we may have a .Mac, an ICQ, or an AIM name in the field
1075  */
1076 NSString* serviceIDForOscarUID(NSString *UID)
1078         NSString        *serviceID;
1080         const char      firstCharacter = [UID characterAtIndex:0];
1081         
1082         //Determine service based on UID
1083         if ([UID hasSuffix:@"@mac.com"]) {
1084                 serviceID = @"Mac";
1085         } else if (firstCharacter >= '0' && firstCharacter <= '9') {
1086                 serviceID = @"ICQ";
1087         } else {
1088                 serviceID = @"AIM";
1089         }
1090         
1091         return serviceID;
1095  * @brief Service ID for a Jabber UID
1097  * If we are on the Jabber server, we need to distinguish between Google Talk (GTalk), LiveJournal, and the rest of the
1098  * Jabber world. serviceID is already Jabber, so we only need to change if we have a special UID.
1099  */
1100 NSString* serviceIDForJabberUID(NSString *UID)
1102         NSString        *serviceID;
1104         if ([UID hasSuffix:@"@gmail.com"] ||
1105                 [UID hasSuffix:@"@googlemail.com"]) {
1106                 serviceID = @"GTalk";
1107         } else if ([UID hasSuffix:@"@livejournal.com"]) {
1108                 serviceID = @"LiveJournal";
1109         } else {
1110                 serviceID = @"Jabber";
1111         }
1112         
1113         return serviceID;
1118  * @brief add people to our address book lookup dictionary
1120  * Rather than continually searching the address book, a lookup dictionary addressBookDict provides an quick and easy
1121  * way to look up a unique record ID for an ABPerson based on the service and UID of a contact. addressBookDict contains
1122  * NSDictionary objects keyed by service ID. Each of these NSDictionary objects contains unique record IDs keyed by compacted
1123  * (that is, no spaces and no all lowercase) UID. This means we can search while ignoring spaces, which normal AB searching
1124  * does not allow.
1126  * In the process of building we look for cards which have multiple screen names listed and, if desired, automatically
1127  * create metaContacts baesd on this information.
1128  */
1129 - (void)addToAddressBookDict:(NSArray *)people
1131         NSEnumerator            *peopleEnumerator;
1132         NSArray                         *allServiceKeys = [serviceDict allKeys];
1133         ABPerson                        *person;
1134         
1135         peopleEnumerator = [people objectEnumerator];
1136         while ((person = [peopleEnumerator nextObject])) {
1137                 NSEnumerator            *servicesEnumerator;
1138                 NSString                        *serviceID;
1139                 
1140                 NSMutableArray          *UIDsArray = [NSMutableArray array];
1141                 NSMutableArray          *servicesArray = [NSMutableArray array];
1142                 
1143                 NSMutableDictionary     *dict;
1144                 ABMultiValue            *emails;
1145                 int                                     i, emailsCount;
1146                 
1147                 //An ABPerson may have multiple emails; iterate through them looking for @mac.com addresses
1148                 {
1149                         emails = [person valueForProperty:kABEmailProperty];
1150                         emailsCount = [emails count];
1151                         
1152                         for (i = 0; i < emailsCount ; i++) {
1153                                 NSString        *email;
1154                                 
1155                                 email = [emails valueAtIndex:i];
1156                                 if ([email hasSuffix:@"@mac.com"]) {
1157                                         
1158                                         //@mac.com UIDs go into the AIM dictionary
1159                                         if (!(dict = [addressBookDict objectForKey:@"AIM"])) {
1160                                                 dict = [[[NSMutableDictionary alloc] init] autorelease];
1161                                                 [addressBookDict setObject:dict forKey:@"AIM"];
1162                                         }
1163                                         
1164                                         [dict setObject:[person uniqueId] forKey:email];
1165                                         
1166                                         //Internally we distinguish them as .Mac addresses (for metaContact purposes below)
1167                                         [UIDsArray addObject:email];
1168                                         [servicesArray addObject:@"Mac"];
1169                                 }
1170                         }
1171                 }
1172                 
1173                 //Now go through the instant messaging keys
1174                 servicesEnumerator = [allServiceKeys objectEnumerator];
1175                 while ((serviceID = [servicesEnumerator nextObject])) {
1176                         NSString                        *addressBookKey = [serviceDict objectForKey:serviceID];
1177                         ABMultiValue            *names;
1178                         int                                     nameCount;
1180                         //An ABPerson may have multiple names; iterate through them
1181                         names = [person valueForProperty:addressBookKey];
1182                         nameCount = [names count];
1183                         
1184                         //Continue to the next serviceID immediately if no names are found
1185                         if (nameCount == 0) continue;
1186                         
1187                         //One or more names were found, so we'll need a dictionary
1188                         if (!(dict = [addressBookDict objectForKey:serviceID])) {
1189                                 dict = [[NSMutableDictionary alloc] init];
1190                                 [addressBookDict setObject:dict forKey:serviceID];
1191                                 [dict release];
1192                         }
1194                         BOOL                                    isOSCAR = ([serviceID isEqualToString:@"AIM"] || 
1195                                                                                            [serviceID isEqualToString:@"ICQ"]);
1196                         BOOL                                    isJabber = [serviceID isEqualToString:@"Jabber"] ||
1197                                                [serviceID isEqualToString:@"XMPP"];
1199                         for (i = 0 ; i < nameCount ; i++) {
1200                                 NSString        *UID = [[names valueAtIndex:i] compactedString];
1201                                 if ([UID length]) {
1202                                         [dict setObject:[person uniqueId] forKey:UID];
1203                                         
1204                                         [UIDsArray addObject:UID];
1205                                         
1206                                         if (isOSCAR) {
1207                                                 serviceID = serviceIDForOscarUID(UID);
1208                                                 
1209                                         } else if (isJabber) {
1210                                                 serviceID = serviceIDForJabberUID(UID);
1211                                         }
1212                                         
1213                                         [servicesArray addObject:serviceID];
1214                                 }
1215                         }
1216                 }
1217                 
1218                 if (([UIDsArray count] > 1) && createMetaContacts) {
1219                         /* Got a record with multiple names. Group the names together, adding them to the meta contact. */
1220                         [[adium contactController] groupUIDs:UIDsArray 
1221                                                                          forServices:servicesArray];
1222                 }
1223         }
1227  * @brief remove people from our address book lookup dictionary
1228  */
1229 - (void)removeFromAddressBookDict:(NSArray *)uniqueIDs
1231         NSEnumerator            *enumerator;
1232         NSArray                         *allServiceKeys = [serviceDict allKeys];
1233         NSString                        *uniqueID;
1234         
1235         enumerator = [uniqueIDs objectEnumerator];
1236         while ((uniqueID = [enumerator nextObject])) {
1237                 NSEnumerator            *servicesEnumerator;
1238                 NSString                        *serviceID;
1239                 NSMutableDictionary     *dict;
1240                 
1241                 //The same person may have multiple services; iterate through them and remove each one.
1242                 servicesEnumerator = [allServiceKeys objectEnumerator];
1243                 while ((serviceID = [servicesEnumerator nextObject])) {
1244                         NSEnumerator *keysEnumerator;
1245                         NSString *key;
1246                         
1247                         dict = [addressBookDict objectForKey:serviceID];
1248                         
1249                         keysEnumerator = [[dict allKeysForObject:uniqueID] objectEnumerator];
1250                         
1251                         //The same person may have multiple accounts from the same service; we should remove them all.
1252                         while ((key = [keysEnumerator nextObject])) {
1253                                 [dict removeObjectForKey:key];
1254                         }
1255                 }
1256         }       
1259 #pragma mark AB contextual menu
1261  * @brief Validate menu item
1262  */
1263 - (BOOL)validateMenuItem:(NSMenuItem *)menuItem
1265         BOOL    hasABEntry = ([self personForListObject:[[adium menuController] currentContextMenuObject]] != nil);
1266         BOOL    result = NO;
1267         
1268         if ([menuItem isEqual:showInABContextualMenuItem] || [menuItem isEqual:editInABContextualMenuItem])
1269                 result = hasABEntry;
1270         else if ([menuItem isEqual:addToABContexualMenuItem])
1271                 result = !hasABEntry;
1272         
1273         return result;
1277  * @brief Shows the selected contact in Address Book
1278  */
1279 - (void)showInAddressBook
1281         ABPerson *selectedPerson = [self personForListObject:[[adium menuController] currentContextMenuObject]];
1282         NSString *url = [NSString stringWithFormat:@"addressbook://%@", [selectedPerson uniqueId]];
1283         [[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:url]];
1287  * @brief Edits the selected contact in Address Book
1288  */
1289 - (void)editInAddressBook
1291         ABPerson *selectedPerson = [self personForListObject:[[adium menuController] currentContextMenuObject]];
1292         NSString *url = [NSString stringWithFormat:@"addressbook://%@?edit", [selectedPerson uniqueId]];
1293         [[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:url]];
1296 - (void)addToAddressBook
1298         AIListObject                    *contact = [[adium menuController] currentContextMenuObject];
1299         NSString                                *serviceProperty = [ESAddressBookIntegrationPlugin propertyFromService:[contact service]];
1300         
1301         if (serviceProperty) {
1302                 ABPerson                                *person = [[ABPerson alloc] init];
1303                 
1304                 //Set the name
1305                 [person setValue:[contact displayName] forKey:kABFirstNameProperty];
1306                 [person setValue:[contact phoneticName] forKey:kABFirstNamePhoneticProperty];
1307                 
1308                 NSString                                *UID = [contact formattedUID];
1309         
1310                 NSEnumerator * containedContactEnu = [contact isKindOfClass:[AIMetaContact class]] ? [[(AIMetaContact *)contact listContacts] objectEnumerator] : [[NSArray arrayWithObject:contact] objectEnumerator];
1311                 AIListObject *c;
1312                 ABMutableMultiValue             *multiValue;
1313                 
1314                 while((c = [containedContactEnu nextObject]))
1315                 {
1316                         multiValue = [[ABMutableMultiValue alloc] init];
1317                         UID = [c formattedUID];
1318                         serviceProperty = [ESAddressBookIntegrationPlugin propertyFromService:[c service]];
1319                         
1320                         //Set the IM property
1321                         [multiValue addValue:UID withLabel:serviceProperty];
1322                         [person setValue:multiValue forKey:serviceProperty];
1323                         
1324                         [multiValue release];
1325                 }
1326                 
1327                 
1328                 //Set the image
1329                 [person setImageData:[contact userIconData]];
1330                 
1331                 //Set the notes
1332                 [person setValue:[contact notes] forKey:kABNoteProperty];
1333                 
1334                 //Add our newly created person to the AB database
1335                 if ([sharedAddressBook addRecord:person] && [sharedAddressBook save]) {
1336                         //Save the uid of the new person
1337                         [contact setPreference:[person uniqueId]
1338                                                         forKey:KEY_AB_UNIQUE_ID
1339                                                          group:PREF_GROUP_ADDRESSBOOK];
1340                         
1341                         //Ask the user whether it would like to edit the new contact
1342                         int result = NSRunAlertPanel(CONTACT_ADDED_SUCCESS_TITLE,
1343                                                                                  CONTACT_ADDED_SUCCESS_Message,
1344                                                                                  AILocalizedString(@"Yes", nil),
1345                                                                                  AILocalizedString(@"No", nil), nil, UID);
1346                         
1347                         if (result == NSOKButton) {
1348                                 NSString *url = [[NSString alloc] initWithFormat:@"addressbook://%@?edit", [person uniqueId]];
1349                                 [[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:url]];
1350                                 [url release];
1351                         }
1352                 } else {
1353                         NSRunAlertPanel(CONTACT_ADDED_ERROR_TITLE, CONTACT_ADDED_ERROR_Message, nil, nil, nil);
1354                 }
1355                 
1356                 //Clean up
1357                 [person release];
1358         }
1361 @end