Added `-[NSArray validateAsPropertyList]` and `-[NSDictionary validateAsPropertyList...
[adiumx.git] / Source / ESAddressBookIntegrationPlugin.m
blob4bf4890ad081a299843418208a9c6442e13c6abc
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 <Adium/AIUserIcons.h>
27 #import <AIUtilities/AIAttributedStringAdditions.h>
28 #import <AIUtilities/AIDictionaryAdditions.h>
29 #import <AIUtilities/AIMutableOwnerArray.h>
30 #import <AIUtilities/AIStringAdditions.h>
31 #import <AIUtilities/OWAddressBookAdditions.h>
32 #import <AIUtilities/AIFileManagerAdditions.h>
34 #import "AIAddressBookUserIconSource.h"
36 #define IMAGE_LOOKUP_INTERVAL   0.01
37 #define SHOW_IN_AB_CONTEXTUAL_MENU_TITLE AILocalizedString(@"Show In Address Book", "Show In Address Book Contextual Menu")
38 #define EDIT_IN_AB_CONTEXTUAL_MENU_TITLE AILocalizedString(@"Edit In Address Book", "Edit In Address Book Contextual Menu")
39 #define ADD_TO_AB_CONTEXTUAL_MENU_TITLE AILocalizedString(@"Add To Address Book", "Add To Address Book Contextual Menu")
41 #define CONTACT_ADDED_SUCCESS_TITLE             AILocalizedString(@"Success", "Title of a panel shown after adding successfully adding a contact to the address book.")
42 #define CONTACT_ADDED_SUCCESS_Message   AILocalizedString(@"%@ had been successfully added to the Address Book.\nWould you like to edit the card now?", nil)
43 #define CONTACT_ADDED_ERROR_TITLE               AILocalizedString(@"Error", nil)
44 #define CONTACT_ADDED_ERROR_Message             AILocalizedString(@"An error had occurred while adding %@ to the Address Book.", nil)
46 #define KEY_ADDRESS_BOOK_ACTIONS_INSTALLED      @"Adium:Installed Adress Book Actions 1.2"
48 @interface ESAddressBookIntegrationPlugin(PRIVATE)
49 + (ABPerson *)_searchForUID:(NSString *)UID serviceID:(NSString *)serviceID;
51 - (void)updateAllContacts;
52 - (void)updateSelfIncludingIcon:(BOOL)includeIcon;
53 - (void)preferencesChanged:(NSNotification *)notification;
54 - (NSString *)nameForPerson:(ABPerson *)person phonetic:(NSString **)phonetic;
55 - (void)rebuildAddressBookDict;
56 - (void)queueDelayedFetchOfImageForPerson:(ABPerson *)person object:(AIListObject *)inObject;
57 - (void)showInAddressBook;
58 - (void)editInAddressBook;
59 - (void)addToAddressBookDict:(NSArray *)people;
60 - (void)removeFromAddressBookDict:(NSArray *)UIDs;
61 - (void)installAddressBookActions;
62 @end
64 /*!
65  * @class ESAddressBookIntegrationPlugin
66  * @brief Provides Apple Address Book integration
67  *
68  * This class allows Adium to seamlessly interact with the Apple Address Book, pulling names and icons, storing icons
69  * if desired, and generating metaContacts based on screen name grouping.  It relies upon cards having screen names listed
70  * in the appropriate service fields in the address book.
71  */
72 @implementation ESAddressBookIntegrationPlugin
74 static  ABAddressBook           *sharedAddressBook = nil;
75 static  NSMutableDictionary     *addressBookDict = nil;
77 static  NSDictionary            *serviceDict = nil;
79 NSString* serviceIDForOscarUID(NSString *UID);
80 NSString* serviceIDForJabberUID(NSString *UID);
82 /*!
83  * @brief Install plugin
84  *
85  * This plugin finishes installing in adiumFinishedLaunching:
86  */
87 - (void)installPlugin
89     meTag = -1;
90     addressBookDict = nil;
91         createMetaContacts = NO;
92         
93     //Configure our preferences
94     [[adium preferenceController] registerDefaults:[NSDictionary dictionaryNamed:AB_DISPLAYFORMAT_DEFAULT_PREFS 
95                                                                                                                                                 forClass:[self class]]  
96                                                                                   forGroup:PREF_GROUP_ADDRESSBOOK];
98         //We want the enableImport preference immediately (without waiting for the preferences observer to be registered in adiumFinishedLaunching:)
99         enableImport = [[[adium preferenceController] preferenceForKey:KEY_AB_ENABLE_IMPORT
100                                                                                                                          group:PREF_GROUP_ADDRESSBOOK] boolValue];
101     advancedPreferences = [[ESAddressBookIntegrationAdvancedPreferences preferencePane] retain];
102         
103     //Services dictionary
104     serviceDict = [[NSDictionary dictionaryWithObjectsAndKeys:kABAIMInstantProperty,@"AIM",
105                                                                 kABJabberInstantProperty,@"Jabber",
106                                                                 kABMSNInstantProperty,@"MSN",
107                                                                 kABYahooInstantProperty,@"Yahoo!",
108                                                                 kABICQInstantProperty,@"ICQ",nil] retain];
109         
110         //Shared Address Book
111         [sharedAddressBook release]; sharedAddressBook = [[ABAddressBook sharedAddressBook] retain];
112         
113         //Create our contextual menus
114         showInABContextualMenuItem = [[[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:SHOW_IN_AB_CONTEXTUAL_MENU_TITLE
115                                                                                                                                                            action:@selector(showInAddressBook)
116                                                                                                                                                 keyEquivalent:@""] autorelease];
117         [showInABContextualMenuItem setTarget:self];
118         
119         editInABContextualMenuItem = [[[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:EDIT_IN_AB_CONTEXTUAL_MENU_TITLE
120                                                                                                                                                                            action:@selector(editInAddressBook)
121                                                                                                                                                                 keyEquivalent:@""] autorelease];
122         [editInABContextualMenuItem setTarget:self];
123         [editInABContextualMenuItem setKeyEquivalentModifierMask:NSAlternateKeyMask];
124         [editInABContextualMenuItem setAlternate:YES];
125         
126         addToABContexualMenuItem = [[[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:ADD_TO_AB_CONTEXTUAL_MENU_TITLE
127                                                                                                                                                                          action:@selector(addToAddressBook)
128                                                                                                                                                           keyEquivalent:@""] autorelease];
129         [addToABContexualMenuItem setTarget:self];
130         
131         //Install our menues
132         [[adium menuController] addContextualMenuItem:addToABContexualMenuItem toLocation:Context_Contact_Action];
133         [[adium menuController] addContextualMenuItem:showInABContextualMenuItem toLocation:Context_Contact_Action];
134         [[adium menuController] addContextualMenuItem:editInABContextualMenuItem toLocation:Context_Contact_Action];
135         
136         [self installAddressBookActions];
137         
138         //Wait for Adium to finish launching before we build the address book so the contact list will be ready
139         [[adium notificationCenter] addObserver:self
140                                                                    selector:@selector(adiumFinishedLaunching:)
141                                                                            name:AIApplicationDidFinishLoadingNotification
142                                                                          object:nil];
143         
144         //Update self immediately so the information is available to plugins and interface elements as they load
145         [self updateSelfIncludingIcon:YES];     
148 - (void)installAddressBookActions
150         NSNumber                *installedActions = [[NSUserDefaults standardUserDefaults] objectForKey:KEY_ADDRESS_BOOK_ACTIONS_INSTALLED];
151         
152         if (!installedActions || ![installedActions boolValue]) {
153                 NSEnumerator  *enumerator = [[NSArray arrayWithObjects:@"AIM", @"MSN", @"Yahoo", @"ICQ", @"Jabber", @"SMS", nil] objectEnumerator];
154                 NSString          *name;
155                 NSFileManager *fileManager = [NSFileManager defaultManager];
156                 NSArray           *libraryDirectoryArray;
157                 NSString          *libraryDirectory, *pluginDirectory;
159                 libraryDirectoryArray = NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, YES);
160                 if ([libraryDirectoryArray count]) {
161                         libraryDirectory = [libraryDirectoryArray objectAtIndex:0];
163                 } else {
164                         //Ridiculous safety since everyone should have a Library folder...
165                         libraryDirectory = [NSHomeDirectory() stringByAppendingPathComponent:@"Library"];
166                         [fileManager createDirectoryAtPath:libraryDirectory attributes:nil];
167                 }
169                 pluginDirectory = [[libraryDirectory stringByAppendingPathComponent:@"Address Book Plug-Ins"] stringByAppendingPathComponent:@"/"];
170                 [fileManager createDirectoryAtPath:pluginDirectory attributes:nil];
171                 
172                 while ((name = [enumerator nextObject])) {
173                         NSString *fullName = [NSString stringWithFormat:@"AdiumAddressBookAction_%@",name];
174                         NSString *path = [[NSBundle bundleForClass:[self class]] pathForResource:fullName ofType:@"scpt"];
176                         if (path) {
177                                 NSString *destination = [pluginDirectory stringByAppendingPathComponent:[fullName stringByAppendingPathExtension:@"scpt"]];
178                                 [fileManager trashFileAtPath:destination];
179                                 [fileManager copyPath:path
180                                                            toPath:destination
181                                                           handler:NULL];
182                                 
183                                 //Remove the old xtra if installed
184                                 [fileManager trashFileAtPath:[pluginDirectory stringByAppendingPathComponent:
185                                         [NSString stringWithFormat:@"%@-Adium.scpt",name]]];
186                         } else {
187                                 AILogWithSignature(@"Warning: Could not find %@",self, fullName);
188                         }
189                 }
191                 [[NSUserDefaults standardUserDefaults] setObject:[NSNumber numberWithBool:YES]
192                                                                                                   forKey:KEY_ADDRESS_BOOK_ACTIONS_INSTALLED];
193         }
197  * @brief Uninstall plugin
198  */
199 - (void)uninstallPlugin
201     [[adium contactController] unregisterListObjectObserver:self];
202     [[adium notificationCenter] removeObserver:self];
206  * @brief Deallocate
207  */
208 - (void)dealloc
210     [serviceDict release]; serviceDict = nil;
212         [sharedAddressBook release]; sharedAddressBook = nil;
213         [addressBookUserIconSource release]; addressBookUserIconSource = nil;
215         [[adium preferenceController] unregisterPreferenceObserver:self];
216         [[adium notificationCenter] removeObserver:self];
218         [super dealloc];
222  * @brief Adium finished launching
224  * Register our observers for the address book changing externally and for the account list changing.
225  * Register our preference observers. This will trigger initial building of the address book dictionary.
226  */
227 - (void)adiumFinishedLaunching:(NSNotification *)notification
228 {       
229     //Observe external address book changes
230     [[NSNotificationCenter defaultCenter] addObserver:self
231                                                                                          selector:@selector(addressBookChanged:)
232                                                                                                  name:kABDatabaseChangedExternallyNotification
233                                                                                            object:nil];
235     //Observe account changes
236     [[adium notificationCenter] addObserver:self
237                                                                    selector:@selector(accountListChanged:)
238                                                                            name:Account_ListChanged
239                                                                          object:nil];
241     //Observe preferences changes
242     id<AIPreferenceController> preferenceController = [adium preferenceController];
243         [preferenceController registerPreferenceObserver:self forGroup:PREF_GROUP_ADDRESSBOOK];
244         [preferenceController registerPreferenceObserver:self forGroup:PREF_GROUP_USERICONS];
245         
246         addressBookUserIconSource = [[AIAddressBookUserIconSource alloc] init];
247         [AIUserIcons registerUserIconSource:addressBookUserIconSource];
251  * @brief Used as contacts are created and icons are changed.
253  * When first created, load a contact's address book information from our dict.
254  * When an icon as a status object changes, if desired, write the changed icon out to the appropriate AB card.
255  */
256 - (NSSet *)updateListObject:(AIListObject *)inObject keys:(NSSet *)inModifiedKeys silent:(BOOL)silent
258         //Just stop here if we don't have an address book dict to work with
259         if (!addressBookDict) return nil;
260         
261         //We handle accounts separately; doing updates here causes chaos in addition to being inefficient.
262         if ([inObject isKindOfClass:[AIAccount class]]) return nil;
263         
264         NSSet           *modifiedAttributes = nil;
265         
266     if (inModifiedKeys == nil) { //Only perform this when updating for all list objects
267         ABPerson *person = [[self class] personForListObject:inObject];
268                 
269                 if ([AIUserIcons userIconSource:addressBookUserIconSource changeWouldBeRelevantForObject:inObject]) {
270                         //This object's user icon would be changed if we have a new icon Update!
271                         [addressBookUserIconSource queueDelayedFetchOfImageForPerson:person object:inObject];
272                 }
274                 if (person) {
275                         if (enableImport) {
276                                 //Load the name if appropriate
277                                 AIMutableOwnerArray *displayNameArray, *phoneticNameArray;
278                                 NSString                        *displayName, *phoneticName = nil;
279                                 
280                                 displayNameArray = [inObject displayArrayForKey:@"Display Name"];
281                                 
282                                 displayName = [self nameForPerson:person phonetic:&phoneticName];
283                                 
284                                 //Apply the values 
285                                 NSString *oldValue = [displayNameArray objectWithOwner:self];
286                                 if (!oldValue || ![oldValue isEqualToString:displayName]) {
287                                         [displayNameArray setObject:displayName withOwner:self];
288                                         modifiedAttributes = [NSSet setWithObject:@"Display Name"];
289                                 }
290                                 
291                                 if (phoneticName) {
292                                         phoneticNameArray = [inObject displayArrayForKey:@"Phonetic Name"];
294                                         //Apply the values 
295                                         oldValue = [phoneticNameArray objectWithOwner:self];
296                                         if (!oldValue || ![oldValue isEqualToString:phoneticName]) {
297                                                 [phoneticNameArray setObject:phoneticName withOwner:self];
298                                                 modifiedAttributes = [NSSet setWithObjects:@"Display Name", @"Phonetic Name", nil];
299                                         }
300                                 } else {
301                                         phoneticNameArray = [inObject displayArrayForKey:@"Phonetic Name"
302                                                                                                                           create:NO];
303                                         //Clear any stored value
304                                         if ([phoneticNameArray objectWithOwner:self]) {
305                                                 [displayNameArray setObject:nil withOwner:self];
306                                                 modifiedAttributes = [NSSet setWithObjects:@"Display Name", @"Phonetic Name", nil];
307                                         }                                       
308                                 }
310                         } else {
311                                 AIMutableOwnerArray *displayNameArray, *phoneticNameArray;
312                                 
313                                 displayNameArray = [inObject displayArrayForKey:@"Display Name"
314                                                                                                                  create:NO];
316                                 //Clear any stored value
317                                 if ([displayNameArray objectWithOwner:self]) {
318                                         [displayNameArray setObject:nil withOwner:self];
319                                         modifiedAttributes = [NSSet setWithObject:@"Display Name"];
320                                 }
321                                 
322                                 phoneticNameArray = [inObject displayArrayForKey:@"Phonetic Name"
323                                                                                                                   create:NO];
324                                 //Clear any stored value
325                                 if ([phoneticNameArray objectWithOwner:self]) {
326                                         [displayNameArray setObject:nil withOwner:self];
327                                         modifiedAttributes = [NSSet setWithObjects:@"Display Name", @"Phonetic Name", nil];
328                                 }                                       
329                                 
330                         }
332                         //If we changed anything, request an update of the alias / long display name
333                         if (modifiedAttributes) {
334                                 [[adium notificationCenter] postNotificationName:Contact_ApplyDisplayName
335                                                                                                                   object:inObject
336                                                                                                                 userInfo:[NSDictionary dictionaryWithObject:[NSNumber numberWithBool:silent]
337                                                                                                                                                                                          forKey:@"Notify"]];
338                         }
339                 }
340                 
341     } else if (automaticSync && [inModifiedKeys containsObject:KEY_USER_ICON]) {
342         
343                 //Only update when the serverside icon changes if there is no Adium preference overriding it
344                 if (![AIUserIcons manuallySetUserIconDataForObject:inObject]) {
345                         //Find the person
346                         ABPerson *person = [[self class] personForListObject:inObject];
347                         
348                         if (person && (person != [sharedAddressBook me])) {                             
349                                 [person setImageData:[inObject userIconData]];
351                                 [[sharedAddressBook class] cancelPreviousPerformRequestsWithTarget:sharedAddressBook
352                                                                                                                                                   selector:@selector(save)
353                                                                                                                                                         object:nil];
354                                 [sharedAddressBook performSelector:@selector(save)
355                                                                                 withObject:nil
356                                                                                 afterDelay:5.0];
357                         }
358                 }
359     }
360     
361     return modifiedAttributes;
365  * @brief Return the name of an ABPerson in the way Adium should display it
367  * @param person An <tt>ABPerson</tt>
368  * @param phonetic A pointer to an <tt>NSString</tt> which will be filled with the phonetic display name if available
369  * @result A string based on the first name, last name, and/or nickname of the person, as specified via preferences.
370  */
371 - (NSString *)nameForPerson:(ABPerson *)person phonetic:(NSString **)phonetic
373         NSString *firstName, *middleName, *lastName, *phoneticFirstName, *phoneticLastName;     
374         NSString *nickName;
375         NSString *displayName = nil;
376         NSNumber *flags;
377         
378         // If the record is for a company, return the company name if present
379         if ((flags = [person valueForProperty:kABPersonFlags])) {
380                 if (([flags intValue] & kABShowAsMask) == kABShowAsCompany) {
381                         NSString *companyName = [person valueForProperty:kABOrganizationProperty];
382                         if (companyName && [companyName length]) {
383                                 return companyName;
384                         }
385                 }
386         }
387                 
388         firstName = [person valueForProperty:kABFirstNameProperty];
389         middleName = [person valueForProperty:kABMiddleNameProperty];
390         lastName = [person valueForProperty:kABLastNameProperty];
391         phoneticFirstName = [person valueForProperty:kABFirstNamePhoneticProperty];
392         phoneticLastName = [person valueForProperty:kABLastNamePhoneticProperty];
393         
394         //
395         if (useMiddleName && middleName)
396                 firstName = [NSString stringWithFormat:@"%@ %@", firstName, middleName];
398         if (useNickName && (nickName = [person valueForProperty:kABNicknameProperty])) {
399                 displayName = nickName;
401         } else if (!lastName || (displayFormat == First)) {  
402                 /* If no last name is available, use the first name */
403                 displayName = firstName;
404                 if (phonetic != NULL) *phonetic = phoneticFirstName;
406         } else if (!firstName) {
407                 /* If no first name is available, use the last name */
408                 displayName = lastName;
409                 if (phonetic != NULL) *phonetic = phoneticLastName;
411         } else {
412                 BOOL havePhonetic = ((phonetic != NULL) && (phoneticFirstName || phoneticLastName));
414                 /* Look to the preference setting */
415                 switch (displayFormat) {
416                         case FirstLast:
417                                 displayName = [NSString stringWithFormat:@"%@ %@",firstName,lastName];
418                                 if (havePhonetic) {
419                                         *phonetic = [NSString stringWithFormat:@"%@ %@",
420                                                 (phoneticFirstName ? phoneticFirstName : firstName),
421                                                 (phoneticLastName ? phoneticLastName : lastName)];
422                                 }
423                                 break;
424                         case LastFirst:
425                                 displayName = [NSString stringWithFormat:@"%@, %@",lastName,firstName]; 
426                                 if (havePhonetic) {
427                                         *phonetic = [NSString stringWithFormat:@"%@, %@",
428                                                 (phoneticLastName ? phoneticLastName : lastName),
429                                                 (phoneticFirstName ? phoneticFirstName : firstName)];
430                                 }
431                                 break;
432                         case LastFirstNoComma:
433                                 displayName = [NSString stringWithFormat:@"%@ %@",lastName,firstName]; 
434                                 if (havePhonetic) {
435                                         *phonetic = [NSString stringWithFormat:@"%@ %@",
436                                                 (phoneticLastName ? phoneticLastName : lastName),
437                                                 (phoneticFirstName ? phoneticFirstName : firstName)];
438                                 }                                       
439                                 break;
440                         case FirstLastInitial:
441                                 displayName = [NSString stringWithFormat:@"%@ %@",firstName,[lastName substringToIndex:1]]; 
442                                 if (havePhonetic) {
443                                         *phonetic = [NSString stringWithFormat:@"%@ %@",
444                                                                  (phoneticFirstName ? phoneticFirstName : firstName),
445                                                                  [lastName substringToIndex:1]];
446                                 }
447                         case First:
448                                 //No action; handled before we reach the switch statement
449                                 break;
450                 }
451         }
453         return displayName;
457  * @brief Observe preference changes
459  * On first call, this method builds the addressBookDict. Subsequently, it rebuilds the dict only if the "create metaContacts"
460  * option is toggled, as metaContacts are created while building the dict.
462  * If the user set a new image as a preference for an object, write it out to the contact's AB card if desired.
463  */
464 - (void)preferencesChangedForGroup:(NSString *)group key:(NSString *)key
465                                                         object:(AIListObject *)object preferenceDict:(NSDictionary *)prefDict firstTime:(BOOL)firstTime
467     if ([group isEqualToString:PREF_GROUP_ADDRESSBOOK]) {
468                 BOOL                    oldCreateMetaContacts = createMetaContacts;
469                 
470         //load new displayFormat
471                 enableImport = [[prefDict objectForKey:KEY_AB_ENABLE_IMPORT] boolValue];
472                 displayFormat = [[prefDict objectForKey:KEY_AB_DISPLAYFORMAT] intValue];
473         automaticSync = [[prefDict objectForKey:KEY_AB_IMAGE_SYNC] boolValue];
474         useNickName = [[prefDict objectForKey:KEY_AB_USE_NICKNAME] boolValue];
475                 useMiddleName = [[prefDict objectForKey:KEY_AB_USE_MIDDLE] boolValue];
477                 createMetaContacts = [[prefDict objectForKey:KEY_AB_CREATE_METACONTACTS] boolValue];
478                 
479                 if (firstTime) {
480                         //Build the address book dictionary, which will also trigger metacontact grouping as appropriate
481                         [self rebuildAddressBookDict];
482                         
483                         //Register ourself as a listObject observer, which will update all objects
484                         [[adium contactController] registerListObjectObserver:self];
485                         
486                         //Note: we don't need to call updateSelfIncludingIcon: because it was already done in installPlugin                     
487                 } else {
488                         //This isn't the first time through
490                         //If we weren't creating meta contacts before but we are now
491                         if (!oldCreateMetaContacts && createMetaContacts) {
492                                 /*
493                                  Build the address book dictionary, which will also trigger metacontact grouping as appropriate
494                                  Delay to the next run loop to give better UI responsiveness
495                                  */
496                                 [self performSelector:@selector(rebuildAddressBookDict)
497                                                    withObject:nil
498                                                    afterDelay:0.0001];
499                         }
500                         
501                         //Update all contacts, which will update objects and then our "me" card information
502                         [self updateAllContacts];
503                 }
505     } else if (automaticSync && ([group isEqualToString:PREF_GROUP_USERICONS]) && object) {
506                 //Set an icon to the address book
507                 ABPerson *person = [[self class] personForListObject:object];
508                 
509                 if (person) {                   
510                         [person setImageData:[object userIconData]];
511                 }
512         }
516  * @brief Returns the appropriate service for the property.
518  * @param property - an ABPerson property.
519  */
520 + (AIService *)serviceFromProperty:(NSString *)property
522         NSString        *serviceID = nil;
523         
524         if ([property isEqualToString:kABAIMInstantProperty])
525                 serviceID = @"AIM";
526         
527         else if ([property isEqualToString:kABICQInstantProperty])
528                 serviceID = @"ICQ";
529         
530         else if ([property isEqualToString:kABMSNInstantProperty])
531                 serviceID = @"MSN";
532         
533         else if ([property isEqualToString:kABJabberInstantProperty])
534                 serviceID = @"Jabber";
535         
536         else if ([property isEqualToString:kABYahooInstantProperty])
537                 serviceID = @"Yahoo!";
539         return (serviceID ? [[[AIObject sharedAdiumInstance] accountController] firstServiceWithServiceID:serviceID] : nil);
543  * @brief Returns the appropriate property for the service.
544  */
545 + (NSString *)propertyFromService:(AIService *)inService
547         NSString *result;
548         NSString *serviceID = [inService serviceID];
550         result = [serviceDict objectForKey:serviceID];
552         //Check for some special cases
553         if (!result) {
554                 if ([serviceID isEqualToString:@"GTalk"]) {
555                         result = kABJabberInstantProperty;
556                 } else if ([serviceID isEqualToString:@"LiveJournal"]) {
557                         result = kABJabberInstantProperty;
558                 } else if ([serviceID isEqualToString:@"Mac"]) {
559                         result = kABAIMInstantProperty;
560                 }
561         }
562         
563         return result;
567  * @brief Called when the address book completes an asynchronous image lookup
569  * @param inData NSData representing an NSImage
570  * @param tag A tag indicating the lookup with which this call is associated.
571  */
572 - (void)consumeImageData:(NSData *)inData forTag:(int)tag
574         if (tag == meTag) {
575                 [[adium preferenceController] setPreference:inData
576                                                                                          forKey:KEY_DEFAULT_USER_ICON 
577                                                                                           group:GROUP_ACCOUNT_STATUS];
578                 meTag = -1;
579         }
581                 
582 #pragma mark Searching
584  * @brief Find an ABPerson corresponding to an AIListObject
586  * @param inObject The object for which it search
587  * @result An ABPerson is one is found, or nil if none is found
588  */
589 + (ABPerson *)personForListObject:(AIListObject *)inObject
591         ABPerson        *person = nil;
592         NSString        *uniqueID = [inObject preferenceForKey:KEY_AB_UNIQUE_ID group:PREF_GROUP_ADDRESSBOOK];
593         ABRecord        *record = nil;
594         
595         if (uniqueID)
596                 record = [sharedAddressBook recordForUniqueId:uniqueID];
597         
598         if (record && [record isKindOfClass:[ABPerson class]]) {
599                 person = (ABPerson *)record;
600         } else {
601                 if ([inObject isKindOfClass:[AIMetaContact class]]) {
602                         NSEnumerator    *enumerator;
603                         AIListContact   *listContact;
604                         
605                         //Search for an ABPerson for each listContact within the metaContact; first one we find is
606                         //the lucky winner.
607                         enumerator = [[(AIMetaContact *)inObject listContacts] objectEnumerator];
608                         while ((listContact = [enumerator nextObject]) && (person == nil)) {
609                                 person = [self personForListObject:listContact];
610                         }
611                         
612                 } else {
613                         NSString                *UID = [inObject UID];
614                         NSString                *serviceID = [[inObject service] serviceID];
615                         
616                         person = [self _searchForUID:UID serviceID:serviceID];
617                         
618                         /* If we don't find anything yet, look at alternative service possibilities:
619                          *    AIM <--> ICQ
620                          */
621                         if (!person) {
622                                 if ([serviceID isEqualToString:@"AIM"]) {
623                                         person = [self _searchForUID:UID serviceID:@"ICQ"];
624                                 } else if ([serviceID isEqualToString:@"ICQ"]) {
625                                         person = [self _searchForUID:UID serviceID:@"AIM"];
626                                 }
627                         }
628                 }
629         }
630         return person;
634  * @brief Find an ABPerson for a given UID and serviceID combination
635  * 
636  * Uses our addressBookDict cache created in rebuildAddressBook.
638  * @param UID The UID for the contact
639  * @param serviceID The serviceID for the contact
640  * @result A corresponding <tt>ABPerson</tt>
641  */
642 + (ABPerson *)_searchForUID:(NSString *)UID serviceID:(NSString *)serviceID
644         ABPerson                *person = nil;
645         NSDictionary    *dict;
646         
647         if ([serviceID isEqualToString:@"Mac"]) {
648                 dict = [addressBookDict objectForKey:@"AIM"];
650         } else if ([serviceID isEqualToString:@"GTalk"]) {
651                 dict = [addressBookDict objectForKey:@"Jabber"];
653         } else if ([serviceID isEqualToString:@"LiveJournal"]) {
654                 dict = [addressBookDict objectForKey:@"Jabber"];
655                 
656         } else if ([serviceID isEqualToString:@"Yahoo! Japan"]) {
657                 dict = [addressBookDict objectForKey:@"Yahoo!"];
658                 
659         } else {
660                 dict = [addressBookDict objectForKey:serviceID];
661         } 
662         
663         if (dict) {
664                 NSString *uniqueID = [dict objectForKey:[UID compactedString]];
665                 if (uniqueID) {
666                         person = (ABPerson *)[sharedAddressBook recordForUniqueId:uniqueID];
667                 }
668         }
669         
670         return person;
673 #pragma mark -
675 - (NSSet *)contactsForPerson:(ABPerson *)person
677         NSArray                 *allServiceKeys = [serviceDict allKeys];
678         NSString                *serviceID;
679         NSMutableSet    *contactSet = [NSMutableSet set];
680         NSEnumerator    *servicesEnumerator;
681         ABMultiValue    *emails;
682         int                             i, emailsCount;
684         //An ABPerson may have multiple emails; iterate through them looking for @mac.com addresses
685         {
686                 emails = [person valueForProperty:kABEmailProperty];
687                 emailsCount = [emails count];
688                 
689                 for (i = 0; i < emailsCount ; i++) {
690                         NSString        *email;
691                         
692                         email = [emails valueAtIndex:i];
693                         if ([email hasSuffix:@"@mac.com"]) {
694                                 //Retrieve all appropriate contacts
695                                 NSSet   *contacts = [[adium contactController] allContactsWithService:[[adium accountController] firstServiceWithServiceID:@"Mac"]
696                                                                                                                                                                   UID:email
697                                                                                                                                                  existingOnly:YES];
699                                 //Add them to our set
700                                 [contactSet unionSet:contacts];
701                         }
702                 }
703         }
704         
705         //Now go through the instant messaging keys
706         servicesEnumerator = [allServiceKeys objectEnumerator];
707         while ((serviceID = [servicesEnumerator nextObject])) {
708                 NSString                *addressBookKey = [serviceDict objectForKey:serviceID];
709                 ABMultiValue    *names;
710                 int                             nameCount;
712                 //An ABPerson may have multiple names; iterate through them
713                 names = [person valueForProperty:addressBookKey];
714                 nameCount = [names count];
715                 
716                 //Continue to the next serviceID immediately if no names are found
717                 if (nameCount == 0) continue;
718                 
719                 BOOL                                    isOSCAR = ([serviceID isEqualToString:@"AIM"] || 
720                                                                                    [serviceID isEqualToString:@"ICQ"]);
721                 BOOL                                    isJabber = [serviceID isEqualToString:@"Jabber"] ||
722                                            [serviceID isEqualToString:@"XMPP"];
723                 
724                 for (i = 0 ; i < nameCount ; i++) {
725                         NSString        *UID = [[names valueAtIndex:i] compactedString];
726                         if ([UID length]) {
727                                 if (isOSCAR) {
728                                         serviceID = serviceIDForOscarUID(UID);
729                                         
730                                 } else if (isJabber) {
731                                         serviceID = serviceIDForJabberUID(UID);
732                                 }
733                                 
734                                 NSSet   *contacts = [[adium contactController] allContactsWithService:[[adium accountController] firstServiceWithServiceID:serviceID]
735                                                                                                                                                                   UID:UID
736                                                                                                                                                  existingOnly:YES];
737                                 
738                                 //Add them to our set
739                                 [contactSet unionSet:contacts];
740                         }
741                 }
742         }
744         return contactSet;
747 #pragma mark Address book changed
749  * @brief Address book changed externally
751  * As a result we add/remove people to/from our address book dictionary cache and update all contacts based on it
752  */
753 - (void)addressBookChanged:(NSNotification *)notification
755         /* In case of a single person, these will be NSStrings.
756          * In case of more then one, they are will be NSArrays containing NSStrings.
757          */     
758         id                              addedPeopleUniqueIDs, modifiedPeopleUniqueIDs, deletedPeopleUniqueIDs;
759         NSMutableSet    *allModifiedPeople = [[NSMutableSet alloc] init];
760         ABPerson                *me = [sharedAddressBook me];
761         BOOL                    modifiedMe = NO;;
763         //Delay listObjectNotifications to speed up metaContact creation
764         [[adium contactController] delayListObjectNotifications];
766         //Addition of new records
767         if ((addedPeopleUniqueIDs = [[notification userInfo] objectForKey:kABInsertedRecords])) {
768                 NSArray *peopleToAdd;
770                 if ([addedPeopleUniqueIDs isKindOfClass:[NSArray class]]) {
771                         //We are dealing with multiple records
772                         peopleToAdd = [sharedAddressBook peopleFromUniqueIDs:(NSArray *)addedPeopleUniqueIDs];
773                 } else {
774                         //We have only one record
775                         peopleToAdd = [NSArray arrayWithObject:(ABPerson *)[sharedAddressBook recordForUniqueId:addedPeopleUniqueIDs]];
776                 }
778                 [allModifiedPeople addObjectsFromArray:peopleToAdd];
779                 [self addToAddressBookDict:peopleToAdd];
780         }
781         
782         //Modification of existing records
783         if ((modifiedPeopleUniqueIDs = [[notification userInfo] objectForKey:kABUpdatedRecords])) {
784                 NSArray *peopleToAdd;
786                 if ([modifiedPeopleUniqueIDs isKindOfClass:[NSArray class]]) {
787                         //We are dealing with multiple records
788                         [self removeFromAddressBookDict:modifiedPeopleUniqueIDs];
789                         peopleToAdd = [sharedAddressBook peopleFromUniqueIDs:modifiedPeopleUniqueIDs];
790                 } else {
791                         //We have only one record
792                         [self removeFromAddressBookDict:[NSArray arrayWithObject:modifiedPeopleUniqueIDs]];
793                         peopleToAdd = [NSArray arrayWithObject:(ABPerson *)[sharedAddressBook recordForUniqueId:modifiedPeopleUniqueIDs]];
794                 }
795                 
796                 [allModifiedPeople addObjectsFromArray:peopleToAdd];
797                 [self addToAddressBookDict:peopleToAdd];
798         }
799         
800         //Deletion of existing records
801         if ((deletedPeopleUniqueIDs = [[notification userInfo] objectForKey:kABDeletedRecords])) {
802                 if ([deletedPeopleUniqueIDs isKindOfClass:[NSArray class]]) {
803                         //We are dealing with multiple records
804                         [self removeFromAddressBookDict:deletedPeopleUniqueIDs];
805                 } else {
806                         //We have only one record
807                         [self removeFromAddressBookDict:[NSArray arrayWithObject:deletedPeopleUniqueIDs]];
808                 }
809                 
810                 //Note: We have no way of retrieving the records of people who were removed, so we really can't do much here.
811         }
812         
813         NSEnumerator    *peopleEnumerator;
814         ABPerson                *person;
815         
816         //Do appropriate updates for each updated ABPerson
817         peopleEnumerator = [allModifiedPeople objectEnumerator];
818         while ((person = [peopleEnumerator nextObject])) {
819                 if (person == me) {
820                         modifiedMe = YES;
821                 }
823                 //It's tempting to not do this if (person == me), but the 'me' contact may also be in the contact list
824                 [[adium contactController] updateContacts:[self contactsForPerson:person]
825                                                                           forObserver:self];
826         }
828         //Update us if appropriate
829         if (modifiedMe) {
830                 [self updateSelfIncludingIcon:YES];
831         }
832         
833         //Stop delaying list object notifications since we are done
834         [[adium contactController] endListObjectNotificationsDelay];
835         [allModifiedPeople release];
839  * @brief Update all existing contacts and accounts
840  */
841 - (void)updateAllContacts
843         [[adium contactController] updateAllListObjectsForObserver:self];
844     [self updateSelfIncludingIcon:YES];
848  * @brief Account list changed: Update all existing accounts
849  */
850 - (void)accountListChanged:(NSNotification *)notification
852         [self updateSelfIncludingIcon:NO];
856  * @brief Update all existing accounts
858  * We use the "me" card to determine the default icon and account display name
859  */
860 - (void)updateSelfIncludingIcon:(BOOL)includeIcon
862         @try
863         {
864         //Begin loading image data for the "me" address book entry, if one exists
865         ABPerson *me;
866         if ((me = [sharedAddressBook me])) {
867                         
868                         //Default buddy icon
869                         if (includeIcon) {
870                                 //Begin the image load
871                                 meTag = [me beginLoadingImageDataForClient:self];
872                         }
873                         
874                         //Set account display names
875                         if (enableImport) {
876                                 NSString                *myDisplayName, *myPhonetic = nil;
877                                 
878                                 myDisplayName = [self nameForPerson:me phonetic:&myPhonetic];
879                                 
880                                 NSEnumerator    *accountsArray = [[[adium accountController] accounts] objectEnumerator];
881                                 AIAccount               *account;
882                                 
883                                 while ((account = [accountsArray nextObject])) {
884                                         if (![account isTemporary]) {
885                                                 [[account displayArrayForKey:@"Display Name"] setObject:myDisplayName
886                                                                                                                                           withOwner:self
887                                                                                                                                   priorityLevel:Low_Priority];
888                                                 
889                                                 if (myPhonetic) {
890                                                         [[account displayArrayForKey:@"Phonetic Name"] setObject:myPhonetic
891                                                                                                                                                    withOwner:self
892                                                                                                                                            priorityLevel:Low_Priority];                                                                         
893                                                 }
894                                         }
895                                 }
897                                 [[adium preferenceController] registerDefaults:[NSDictionary dictionaryWithObject:[[NSAttributedString stringWithString:myDisplayName] dataRepresentation]
898                                                                                                                                                                                    forKey:KEY_ACCOUNT_DISPLAY_NAME]
899                                                                                                           forGroup:GROUP_ACCOUNT_STATUS];
900                         }
901         }
902         }
903         @catch(id exc)
904         {
905                 NSLog(@"ABIntegration: Caught %@", exc);
906         }
909 #pragma mark Address book caching
911  * @brief rebuild our address book lookup dictionary
912  */
913 - (void)rebuildAddressBookDict
915         //Delay listObjectNotifications to speed up metaContact creation
916         [[adium contactController] delayListObjectNotifications];
917         
918         [addressBookDict release]; addressBookDict = [[NSMutableDictionary alloc] init];
919         
920         [self addToAddressBookDict:[sharedAddressBook people]];
922         //Stop delaying list object notifications since we are done
923         [[adium contactController] endListObjectNotificationsDelay];
928  * @brief Service ID for an OSCAR UID
930  * If we are on an OSCAR service we need to resolve our serviceID into the appropriate string
931  * because we may have a .Mac, an ICQ, or an AIM name in the field
932  */
933 NSString* serviceIDForOscarUID(NSString *UID)
935         NSString        *serviceID;
937         const char      firstCharacter = [UID characterAtIndex:0];
938         
939         //Determine service based on UID
940         if ([UID hasSuffix:@"@mac.com"]) {
941                 serviceID = @"Mac";
942         } else if (firstCharacter >= '0' && firstCharacter <= '9') {
943                 serviceID = @"ICQ";
944         } else {
945                 serviceID = @"AIM";
946         }
947         
948         return serviceID;
952  * @brief Service ID for a Jabber UID
954  * If we are on the Jabber server, we need to distinguish between Google Talk (GTalk), LiveJournal, and the rest of the
955  * Jabber world. serviceID is already Jabber, so we only need to change if we have a special UID.
956  */
957 NSString* serviceIDForJabberUID(NSString *UID)
959         NSString        *serviceID;
961         if ([UID hasSuffix:@"@gmail.com"] ||
962                 [UID hasSuffix:@"@googlemail.com"]) {
963                 serviceID = @"GTalk";
964         } else if ([UID hasSuffix:@"@livejournal.com"]) {
965                 serviceID = @"LiveJournal";
966         } else {
967                 serviceID = @"Jabber";
968         }
969         
970         return serviceID;
975  * @brief add people to our address book lookup dictionary
977  * Rather than continually searching the address book, a lookup dictionary addressBookDict provides an quick and easy
978  * way to look up a unique record ID for an ABPerson based on the service and UID of a contact. addressBookDict contains
979  * NSDictionary objects keyed by service ID. Each of these NSDictionary objects contains unique record IDs keyed by compacted
980  * (that is, no spaces and no all lowercase) UID. This means we can search while ignoring spaces, which normal AB searching
981  * does not allow.
983  * In the process of building we look for cards which have multiple screen names listed and, if desired, automatically
984  * create metaContacts baesd on this information.
985  */
986 - (void)addToAddressBookDict:(NSArray *)people
988         NSEnumerator            *peopleEnumerator;
989         NSArray                         *allServiceKeys = [serviceDict allKeys];
990         ABPerson                        *person;
991         
992         peopleEnumerator = [people objectEnumerator];
993         while ((person = [peopleEnumerator nextObject])) {
994                 NSEnumerator            *servicesEnumerator;
995                 NSString                        *serviceID;
996                 
997                 NSMutableArray          *UIDsArray = [NSMutableArray array];
998                 NSMutableArray          *servicesArray = [NSMutableArray array];
999                 
1000                 NSMutableDictionary     *dict;
1001                 ABMultiValue            *emails;
1002                 int                                     i, emailsCount;
1003                 
1004                 //An ABPerson may have multiple emails; iterate through them looking for @mac.com addresses
1005                 {
1006                         emails = [person valueForProperty:kABEmailProperty];
1007                         emailsCount = [emails count];
1008                         
1009                         for (i = 0; i < emailsCount ; i++) {
1010                                 NSString        *email;
1011                                 
1012                                 email = [emails valueAtIndex:i];
1013                                 if ([email hasSuffix:@"@mac.com"]) {
1014                                         
1015                                         //@mac.com UIDs go into the AIM dictionary
1016                                         if (!(dict = [addressBookDict objectForKey:@"AIM"])) {
1017                                                 dict = [[[NSMutableDictionary alloc] init] autorelease];
1018                                                 [addressBookDict setObject:dict forKey:@"AIM"];
1019                                         }
1020                                         
1021                                         [dict setObject:[person uniqueId] forKey:email];
1022                                         
1023                                         //Internally we distinguish them as .Mac addresses (for metaContact purposes below)
1024                                         [UIDsArray addObject:email];
1025                                         [servicesArray addObject:@"Mac"];
1026                                 }
1027                         }
1028                 }
1029                 
1030                 //Now go through the instant messaging keys
1031                 servicesEnumerator = [allServiceKeys objectEnumerator];
1032                 while ((serviceID = [servicesEnumerator nextObject])) {
1033                         NSString                        *addressBookKey = [serviceDict objectForKey:serviceID];
1034                         ABMultiValue            *names;
1035                         int                                     nameCount;
1037                         //An ABPerson may have multiple names; iterate through them
1038                         names = [person valueForProperty:addressBookKey];
1039                         nameCount = [names count];
1040                         
1041                         //Continue to the next serviceID immediately if no names are found
1042                         if (nameCount == 0) continue;
1043                         
1044                         //One or more names were found, so we'll need a dictionary
1045                         if (!(dict = [addressBookDict objectForKey:serviceID])) {
1046                                 dict = [[NSMutableDictionary alloc] init];
1047                                 [addressBookDict setObject:dict forKey:serviceID];
1048                                 [dict release];
1049                         }
1051                         BOOL                                    isOSCAR = ([serviceID isEqualToString:@"AIM"] || 
1052                                                                                            [serviceID isEqualToString:@"ICQ"]);
1053                         BOOL                                    isJabber = [serviceID isEqualToString:@"Jabber"] ||
1054                                                [serviceID isEqualToString:@"XMPP"];
1056                         for (i = 0 ; i < nameCount ; i++) {
1057                                 NSString        *UID = [[names valueAtIndex:i] compactedString];
1058                                 if ([UID length]) {
1059                                         [dict setObject:[person uniqueId] forKey:UID];
1060                                         
1061                                         [UIDsArray addObject:UID];
1062                                         
1063                                         if (isOSCAR) {
1064                                                 serviceID = serviceIDForOscarUID(UID);
1065                                                 
1066                                         } else if (isJabber) {
1067                                                 serviceID = serviceIDForJabberUID(UID);
1068                                         }
1069                                         
1070                                         [servicesArray addObject:serviceID];
1071                                 }
1072                         }
1073                 }
1074                 
1075                 if (([UIDsArray count] > 1) && createMetaContacts) {
1076                         /* Got a record with multiple names. Group the names together, adding them to the meta contact. */
1077                         [[adium contactController] groupUIDs:UIDsArray 
1078                                                                          forServices:servicesArray];
1079                 }
1080         }
1084  * @brief remove people from our address book lookup dictionary
1085  */
1086 - (void)removeFromAddressBookDict:(NSArray *)uniqueIDs
1088         NSEnumerator            *enumerator;
1089         NSArray                         *allServiceKeys = [serviceDict allKeys];
1090         NSString                        *uniqueID;
1091         
1092         enumerator = [uniqueIDs objectEnumerator];
1093         while ((uniqueID = [enumerator nextObject])) {
1094                 NSEnumerator            *servicesEnumerator;
1095                 NSString                        *serviceID;
1096                 NSMutableDictionary     *dict;
1097                 
1098                 //The same person may have multiple services; iterate through them and remove each one.
1099                 servicesEnumerator = [allServiceKeys objectEnumerator];
1100                 while ((serviceID = [servicesEnumerator nextObject])) {
1101                         NSEnumerator *keysEnumerator;
1102                         NSString *key;
1103                         
1104                         dict = [addressBookDict objectForKey:serviceID];
1105                         
1106                         keysEnumerator = [[dict allKeysForObject:uniqueID] objectEnumerator];
1107                         
1108                         //The same person may have multiple accounts from the same service; we should remove them all.
1109                         while ((key = [keysEnumerator nextObject])) {
1110                                 [dict removeObjectForKey:key];
1111                         }
1112                 }
1113         }       
1116 #pragma mark AB contextual menu
1118  * @brief Validate menu item
1119  */
1120 - (BOOL)validateMenuItem:(NSMenuItem *)menuItem
1122         BOOL    hasABEntry = ([[self class] personForListObject:[[adium menuController] currentContextMenuObject]] != nil);
1123         BOOL    result = NO;
1124         
1125         if ([menuItem isEqual:showInABContextualMenuItem] || [menuItem isEqual:editInABContextualMenuItem])
1126                 result = hasABEntry;
1127         else if ([menuItem isEqual:addToABContexualMenuItem])
1128                 result = !hasABEntry;
1129         
1130         return result;
1134  * @brief Shows the selected contact in Address Book
1135  */
1136 - (void)showInAddressBook
1138         ABPerson *selectedPerson = [[self class] personForListObject:[[adium menuController] currentContextMenuObject]];
1139         NSString *url = [NSString stringWithFormat:@"addressbook://%@", [selectedPerson uniqueId]];
1140         [[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:url]];
1144  * @brief Edits the selected contact in Address Book
1145  */
1146 - (void)editInAddressBook
1148         ABPerson *selectedPerson = [[self class] personForListObject:[[adium menuController] currentContextMenuObject]];
1149         NSString *url = [NSString stringWithFormat:@"addressbook://%@?edit", [selectedPerson uniqueId]];
1150         [[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:url]];
1153 - (void)addToAddressBook
1155         AIListObject                    *contact = [[adium menuController] currentContextMenuObject];
1156         NSString                                *serviceProperty = [ESAddressBookIntegrationPlugin propertyFromService:[contact service]];
1157         
1158         if (serviceProperty) {
1159                 ABPerson                                *person = [[ABPerson alloc] init];
1160                 
1161                 //Set the name
1162                 [person setValue:[contact displayName] forKey:kABFirstNameProperty];
1163                 [person setValue:[contact phoneticName] forKey:kABFirstNamePhoneticProperty];
1164                 
1165                 NSString                                *UID = [contact formattedUID];
1166         
1167                 NSEnumerator * containedContactEnu = [contact isKindOfClass:[AIMetaContact class]] ? [[(AIMetaContact *)contact listContacts] objectEnumerator] : [[NSArray arrayWithObject:contact] objectEnumerator];
1168                 AIListObject *c;
1169                 ABMutableMultiValue             *multiValue;
1170                 
1171                 while((c = [containedContactEnu nextObject]))
1172                 {
1173                         multiValue = [[ABMutableMultiValue alloc] init];
1174                         UID = [c formattedUID];
1175                         serviceProperty = [ESAddressBookIntegrationPlugin propertyFromService:[c service]];
1176                         
1177                         //Set the IM property
1178                         [multiValue addValue:UID withLabel:serviceProperty];
1179                         [person setValue:multiValue forKey:serviceProperty];
1180                         
1181                         [multiValue release];
1182                 }
1183                 
1184                 
1185                 //Set the image
1186                 [person setImageData:[contact userIconData]];
1187                 
1188                 //Set the notes
1189                 [person setValue:[contact notes] forKey:kABNoteProperty];
1190                 
1191                 //Add our newly created person to the AB database
1192                 if ([sharedAddressBook addRecord:person] && [sharedAddressBook save]) {
1193                         //Save the uid of the new person
1194                         [contact setPreference:[person uniqueId]
1195                                                         forKey:KEY_AB_UNIQUE_ID
1196                                                          group:PREF_GROUP_ADDRESSBOOK];
1197                         
1198                         //Ask the user whether it would like to edit the new contact
1199                         int result = NSRunAlertPanel(CONTACT_ADDED_SUCCESS_TITLE,
1200                                                                                  CONTACT_ADDED_SUCCESS_Message,
1201                                                                                  AILocalizedString(@"Yes", nil),
1202                                                                                  AILocalizedString(@"No", nil), nil, UID);
1203                         
1204                         if (result == NSOKButton) {
1205                                 NSString *url = [[NSString alloc] initWithFormat:@"addressbook://%@?edit", [person uniqueId]];
1206                                 [[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:url]];
1207                                 [url release];
1208                         }
1209                 } else {
1210                         NSRunAlertPanel(CONTACT_ADDED_ERROR_TITLE, CONTACT_ADDED_ERROR_Message, nil, nil, nil);
1211                 }
1212                 
1213                 //Clean up
1214                 [person release];
1215         }
1218 @end