summaryrefslogtreecommitdiffstats
path: root/comm/mailnews/addrbook/src/nsAbOSXCard.mm
blob: ab77242490b5c98a543686d8f7fab97d0de5b442 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

#include "nsAbOSXCard.h"
#include "nsAbOSXDirectory.h"
#include "nsAbOSXUtils.h"
#include "nsIAbManager.h"
#include "nsObjCExceptions.h"
#include "nsServiceManagerUtils.h"

#include <AddressBook/AddressBook.h>

NS_IMPL_ISUPPORTS_INHERITED(nsAbOSXCard, nsAbCardProperty, nsIAbOSXCard)

#ifdef DEBUG
static ABPropertyType GetPropertType(ABRecord* aCard, NSString* aProperty) {
  ABPropertyType propertyType = kABErrorInProperty;
  if ([aCard isKindOfClass:[ABPerson class]])
    propertyType = [ABPerson typeOfProperty:aProperty];
  else if ([aCard isKindOfClass:[ABGroup class]])
    propertyType = [ABGroup typeOfProperty:aProperty];
  return propertyType;
}
#endif

static void SetStringProperty(nsAbOSXCard* aCard, const nsString& aValue, const char* aMemberName,
                              bool aNotify, nsIAbManager* aAbManager) {
  nsString oldValue;
  nsresult rv = aCard->GetPropertyAsAString(aMemberName, oldValue);
  if (NS_FAILED(rv)) oldValue.Truncate();

  if (!aNotify) {
    aCard->SetPropertyAsAString(aMemberName, aValue);
  } else if (!oldValue.Equals(aValue)) {
    aCard->SetPropertyAsAString(aMemberName, aValue);
  }
}

static void SetStringProperty(nsAbOSXCard* aCard, NSString* aValue, const char* aMemberName,
                              bool aNotify, nsIAbManager* aAbManager) {
  nsAutoString value;
  if (aValue) AppendToString(aValue, value);

  SetStringProperty(aCard, value, aMemberName, aNotify, aAbManager);
}

static void MapStringProperty(nsAbOSXCard* aCard, ABRecord* aOSXCard, NSString* aProperty,
                              const char* aMemberName, bool aNotify, nsIAbManager* aAbManager) {
  NS_ASSERTION(aProperty, "This is bad! You asked for an unresolved symbol.");
  NS_ASSERTION(GetPropertType(aOSXCard, aProperty) == kABStringProperty, "Wrong type!");

  SetStringProperty(aCard, [aOSXCard valueForProperty:aProperty], aMemberName, aNotify, aAbManager);
}

static ABMutableMultiValue* GetMultiValue(ABRecord* aCard, NSString* aProperty) {
  NS_ASSERTION(aProperty, "This is bad! You asked for an unresolved symbol.");
  NS_ASSERTION(GetPropertType(aCard, aProperty) & kABMultiValueMask, "Wrong type!");

  return [aCard valueForProperty:aProperty];
}

static void MapDate(nsAbOSXCard* aCard, NSDate* aDate, const char* aYearPropName,
                    const char* aMonthPropName, const char* aDayPropName, bool aNotify,
                    nsIAbManager* aAbManager) {
  // XXX Should we pass a format and timezone?
  NSCalendarDate* date = [aDate dateWithCalendarFormat:nil timeZone:nil];

  nsAutoString value;
  value.AppendInt(static_cast<int32_t>([date yearOfCommonEra]));
  SetStringProperty(aCard, value, aYearPropName, aNotify, aAbManager);
  value.Truncate();
  value.AppendInt(static_cast<int32_t>([date monthOfYear]));
  SetStringProperty(aCard, value, aMonthPropName, aNotify, aAbManager);
  value.Truncate();
  value.AppendInt(static_cast<int32_t>([date dayOfMonth]));
  SetStringProperty(aCard, value, aDayPropName, aNotify, aAbManager);
}

static bool MapMultiValue(nsAbOSXCard* aCard, ABRecord* aOSXCard, const nsAbOSXPropertyMap& aMap,
                          bool aNotify, nsIAbManager* aAbManager) {
  ABMultiValue* value = GetMultiValue(aOSXCard, aMap.mOSXProperty);
  if (value) {
    unsigned int j;
    unsigned int count = [value count];
    for (j = 0; j < count; ++j) {
      if ([[value labelAtIndex:j] isEqualToString:aMap.mOSXLabel]) {
        NSString* stringValue = (aMap.mOSXKey) ? [[value valueAtIndex:j] objectForKey:aMap.mOSXKey]
                                               : [value valueAtIndex:j];

        SetStringProperty(aCard, stringValue, aMap.mPropertyName, aNotify, aAbManager);

        return true;
      }
    }
  }
  // String wasn't found, set value of card to empty if it was set previously
  SetStringProperty(aCard, EmptyString(), aMap.mPropertyName, aNotify, aAbManager);

  return false;
}

// Maps Address Book's instant messenger name to the corresponding nsIAbCard field name.
static const char* InstantMessengerFieldName(NSString* aInstantMessengerName) {
  if ([aInstantMessengerName isEqualToString:@"AIMInstant"]) {
    return "_AimScreenName";
  }
  if ([aInstantMessengerName isEqualToString:@"GoogleTalkInstant"]) {
    return "_GoogleTalk";
  }
  if ([aInstantMessengerName isEqualToString:@"ICQInstant"]) {
    return "_ICQ";
  }
  if ([aInstantMessengerName isEqualToString:@"JabberInstant"]) {
    return "_JabberId";
  }
  if ([aInstantMessengerName isEqualToString:@"MSNInstant"]) {
    return "_MSN";
  }
  if ([aInstantMessengerName isEqualToString:@"QQInstant"]) {
    return "_QQ";
  }
  if ([aInstantMessengerName isEqualToString:@"SkypeInstant"]) {
    return "_Skype";
  }
  if ([aInstantMessengerName isEqualToString:@"YahooInstant"]) {
    return "_Yahoo";
  }

  // Fall back to AIM for everything else.
  // We don't have nsIAbCard fields for FacebookInstant and GaduGaduInstant.
  return "_AimScreenName";
}

nsresult nsAbOSXCard::Init(const char* aUri) {
  if (strncmp(aUri, NS_ABOSXCARD_URI_PREFIX, sizeof(NS_ABOSXCARD_URI_PREFIX) - 1) != 0)
    return NS_ERROR_FAILURE;

  mURI = aUri;

  // Extract the UID part.
  mUID = Substring(mURI, 16, mURI.Length());
  // Now make sure we don't use the `:ABPerson` on the end, so that
  // we don't expose it to extensions etc.
  int32_t pos = mUID.RFindChar(':');
  if (pos != kNotFound) {
    mUID = Substring(mUID, 0, pos);
  }
  // Also lower case so that we match other UIDs generated by the address book.
  ToLowerCase(mUID);

  return Update(false);
}

nsresult nsAbOSXCard::GetURI(nsACString& aURI) {
  if (mURI.IsEmpty()) return NS_ERROR_NOT_INITIALIZED;

  aURI = mURI;
  return NS_OK;
}

NS_IMETHODIMP nsAbOSXCard::GetUID(nsACString& uid) {
  uid = mUID;
  return NS_OK;
}

NS_IMETHODIMP nsAbOSXCard::SetUID(const nsACString& aUID) {
  // The UIDs are obtained from the OS X contacts and cannot be changed.
  return NS_ERROR_FAILURE;
}

nsresult nsAbOSXCard::Update(bool aNotify) {
  NS_OBJC_BEGIN_TRY_BLOCK_RETURN;

  ABAddressBook* addressBook = [ABAddressBook sharedAddressBook];

  const char* uid = &((mURI.get())[16]);
  ABRecord* card = [addressBook recordForUniqueId:[NSString stringWithUTF8String:uid]];
  NS_ENSURE_TRUE(card, NS_ERROR_FAILURE);

  nsCOMPtr<nsIAbManager> abManager;
  nsresult rv;
  if (aNotify) {
    abManager = do_GetService("@mozilla.org/abmanager;1", &rv);
    NS_ENSURE_SUCCESS(rv, rv);
  }

  if ([card isKindOfClass:[ABGroup class]]) {
    m_IsMailList = true;
    m_MailListURI.AssignLiteral(NS_ABOSXDIRECTORY_URI_PREFIX);
    m_MailListURI.Append(uid);
    MapStringProperty(this, card, kABGroupNameProperty, "DisplayName", aNotify, abManager);
    MapStringProperty(this, card, kABGroupNameProperty, "LastName", aNotify, abManager);

    return NS_OK;
  }

  bool foundHome = false, foundWork = false;

  uint32_t i;
  for (i = 0; i < nsAbOSXUtils::kPropertyMapSize; ++i) {
    const nsAbOSXPropertyMap& propertyMap = nsAbOSXUtils::kPropertyMap[i];
    if (!propertyMap.mOSXProperty) continue;

    if (propertyMap.mOSXLabel) {
      if (MapMultiValue(this, card, propertyMap, aNotify, abManager) &&
          propertyMap.mOSXProperty == kABAddressProperty) {
        if (propertyMap.mOSXLabel == kABAddressHomeLabel)
          foundHome = true;
        else
          foundWork = true;
      }
    } else {
      MapStringProperty(this, card, propertyMap.mOSXProperty, propertyMap.mPropertyName, aNotify,
                        abManager);
    }
  }

  int flags = 0;
  if (kABPersonFlags) flags = [[card valueForProperty:kABPersonFlags] intValue];

#define SET_STRING(_value, _name, _notify, _session) \
  SetStringProperty(this, _value, #_name, _notify, _session)

  // If kABShowAsCompany is set we use the company name as display name.
  if (kABPersonFlags && (flags & kABShowAsCompany)) {
    nsString company;
    nsresult rv = GetPropertyAsAString(kCompanyProperty, company);
    if (NS_FAILED(rv)) company.Truncate();
    SET_STRING(company, DisplayName, aNotify, abManager);
  } else {
    // Use the order used in the OS X address book to set DisplayName.
    int order = kABPersonFlags && (flags & kABNameOrderingMask);
    if (kABPersonFlags && (order == kABDefaultNameOrdering)) {
      order = [addressBook defaultNameOrdering];
    }

    nsAutoString displayName, tempName;
    if (kABPersonFlags && (order == kABFirstNameFirst)) {
      GetFirstName(tempName);
      displayName.Append(tempName);

      GetLastName(tempName);

      // Only append a space if the last name and the first name are not empty
      if (!tempName.IsEmpty() && !displayName.IsEmpty()) displayName.Append(' ');

      displayName.Append(tempName);
    } else {
      GetLastName(tempName);
      displayName.Append(tempName);

      GetFirstName(tempName);

      // Only append a space if the last name and the first name are not empty
      if (!tempName.IsEmpty() && !displayName.IsEmpty()) displayName.Append(' ');

      displayName.Append(tempName);
    }
    SET_STRING(displayName, DisplayName, aNotify, abManager);
  }

  ABMultiValue* value = GetMultiValue(card, kABEmailProperty);
  if (value) {
    unsigned int count = [value count];
    if (count > 0) {
      unsigned int j = [value indexForIdentifier:[value primaryIdentifier]];

      if (j < count) SET_STRING([value valueAtIndex:j], PrimaryEmail, aNotify, abManager);

      // If j is 0 (first in the list) we want the second in the list
      // (index 1), if j is anything else we want the first in the list
      // (index 0).
      j = (j == 0);
      if (j < count) SET_STRING([value valueAtIndex:j], SecondEmail, aNotify, abManager);
    }
  }

  // We map the first home address we can find and the first work address
  // we can find. If we find none, we map the primary address to the home
  // address.
  if (!foundHome && !foundWork) {
    value = GetMultiValue(card, kABAddressProperty);
    if (value) {
      unsigned int count = [value count];
      unsigned int j = [value indexForIdentifier:[value primaryIdentifier]];

      if (j < count) {
        NSDictionary* address = [value valueAtIndex:j];
        if (address) {
          SET_STRING([address objectForKey:kABAddressStreetKey], HomeAddress, aNotify, abManager);
          SET_STRING([address objectForKey:kABAddressCityKey], HomeCity, aNotify, abManager);
          SET_STRING([address objectForKey:kABAddressStateKey], HomeState, aNotify, abManager);
          SET_STRING([address objectForKey:kABAddressZIPKey], HomeZipCode, aNotify, abManager);
          SET_STRING([address objectForKey:kABAddressCountryKey], HomeCountry, aNotify, abManager);
        }
      }
    }
  }
  // This was kABAIMInstantProperty previously, but it was deprecated in OS X 10.7.
  value = GetMultiValue(card, kABInstantMessageProperty);
  if (value) {
    unsigned int count = [value count];
    for (size_t i = 0; i < count; i++) {
      id imValue = [value valueAtIndex:i];
      // Depending on the macOS version, imValue can be an NSString or an NSDictionary.
      if ([imValue isKindOfClass:[NSString class]]) {
        if (i == [value indexForIdentifier:[value primaryIdentifier]]) {
          SET_STRING(imValue, _AimScreenName, aNotify, abManager);
        }
      } else if ([imValue isKindOfClass:[NSDictionary class]]) {
        NSString* instantMessageService = [imValue objectForKey:@"InstantMessageService"];
        const char* fieldName = InstantMessengerFieldName(instantMessageService);
        NSString* userName = [imValue objectForKey:@"InstantMessageUsername"];
        SetStringProperty(this, userName, fieldName, aNotify, abManager);
      }
    }
  }

#define MAP_DATE(_date, _name, _notify, _session) \
  MapDate(this, _date, #_name "Year", #_name "Month", #_name "Day", _notify, _session)

  NSDate* date = [card valueForProperty:kABBirthdayProperty];
  if (date) MAP_DATE(date, Birth, aNotify, abManager);

  if (kABOtherDatesProperty) {
    value = GetMultiValue(card, kABOtherDatesProperty);
    if (value) {
      unsigned int j, count = [value count];
      for (j = 0; j < count; ++j) {
        if ([[value labelAtIndex:j] isEqualToString:kABAnniversaryLabel]) {
          date = [value valueAtIndex:j];
          if (date) {
            MAP_DATE(date, Anniversary, aNotify, abManager);

            break;
          }
        }
      }
    }
  }
#undef MAP_DATE
#undef SET_STRING

  date = [card valueForProperty:kABModificationDateProperty];
  if (date) SetPropertyAsUint32("LastModifiedDate", uint32_t([date timeIntervalSince1970]));
  // XXX No way to notify about this?

  return NS_OK;

  NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
}