eds backend fixed to match last vala release.
[platform/upstream/folks.git] / backends / eds / lib / edsf-persona.vala
1 /*
2  * Copyright (C) 2011 Collabora Ltd.
3  *
4  * This library is free software: you can redistribute it and/or modify
5  * it under the terms of the GNU Lesser General Public License as published by
6  * the Free Software Foundation, either version 2.1 of the License, or
7  * (at your option) any later version.
8  *
9  * This library is distributed in the hope that it will be useful,
10  * but WITHOUT ANY WARRANTY; without even the implied warranty of
11  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12  * GNU Lesser General Public License for more details.
13  *
14  * You should have received a copy of the GNU Lesser General Public License
15  * along with this library.  If not, see <http://www.gnu.org/licenses/>.
16  *
17  * Authors:
18  *       Travis Reitter <travis.reitter@collabora.co.uk>
19  *       Marco Barisione <marco.barisione@collabora.co.uk>
20  *       Raul Gutierrez Segales <raul.gutierrez.segales@collabora.co.uk>
21  */
22
23 using E;
24 using Folks;
25 using Gee;
26 using GLib;
27 using Xml;
28
29 /**
30  * A persona subclass which represents a single EDS contact.
31  */
32 public class Edsf.Persona : Folks.Persona,
33     AntiLinkable,
34     AvatarDetails,
35     BirthdayDetails,
36     EmailDetails,
37     FavouriteDetails,
38     GenderDetails,
39     GroupDetails,
40     ImDetails,
41     LocalIdDetails,
42     NameDetails,
43     NoteDetails,
44     PhoneDetails,
45     RoleDetails,
46     UrlDetails,
47     PostalAddressDetails,
48     WebServiceDetails
49 {
50   /* The following 4 definitions are used by the tests */
51   /**
52    * vCard field names for telephone numbers.
53    *
54    * @since 0.6.0
55    */
56   public static const string[] phone_fields = {
57     "assistant_phone", "business_phone", "business_phone_2", "callback_phone",
58     "car_phone", "company_phone", "home_phone", "home_phone_2", "isdn_phone",
59     "mobile_phone", "other_phone", "primary_phone"
60   };
61   /**
62    * vCard field names for postal addresses.
63    *
64    * @since 0.6.0
65    */
66   public static const string[] address_fields = {
67     "address_home", "address_other", "address_work"
68   };
69   /**
70    * vCard field names for e-mail addresses.
71    *
72    * @since 0.6.0
73    */
74   public static const string[] email_fields = {
75     "email_1", "email_2", "email_3", "email_4"
76   };
77
78   [Deprecated]
79   /**
80    * vCard field names for miscellaneous URIs.
81    *
82    * @since 0.6.0
83    */
84   public static const string[] url_properties = {
85     "blog_url", "fburl", "homepage_url", "video_url"
86   };
87
88   /* Some types of URLs are represented in EDS using custom vCard fields rather
89    * than the X-URIS field. Here are mappings between the custom vCard field
90    * names which EDS uses, and the TYPE values which folks uses which map to
91    * them. */
92   private struct UrlTypeMapping
93     {
94       string vcard_field_name;
95       string folks_type;
96     }
97
98   internal static const UrlTypeMapping[] _url_properties =
99     {
100       { "homepage_url", UrlFieldDetails.PARAM_TYPE_HOME_PAGE },
101       { "blog_url", UrlFieldDetails.PARAM_TYPE_BLOG },
102       { "fburl", "x-free-busy" },
103       { "video_url", "x-video" }
104     };
105
106   /**
107    * The vCard attribute used to specify a Contact's gender
108    *
109    * Based on:
110    * [[http://tools.ietf.org/html/draft-ietf-vcarddav-vcardrev-22]]
111    *
112    * Note that the above document is a draft and the gender property
113    * is still considered experimental, hence the "X-" prefix in the
114    * attribute name. So this might change.
115    *
116    * @since 0.6.0
117    */
118   public static const string gender_attribute_name = "X-GENDER";
119
120   /**
121    * The value used to define the male gender for the
122    * X-GENDER vCard property.
123    *
124    * Based on:
125    * [[http://tools.ietf.org/html/draft-ietf-vcarddav-vcardrev-22]]
126    *
127    * @since 0.6.0
128    */
129   public static const string gender_male = "M";
130
131   /**
132    * The value used to define the female gender for the
133    * X-GENDER vCard property.
134    *
135    * Based on:
136    * [[http://tools.ietf.org/html/draft-ietf-vcarddav-vcardrev-22]]
137    *
138    * @since 0.6.0
139    */
140   public static const string gender_female = "F";
141
142   private const string[] _linkable_properties = { "im-addresses",
143                                                   "local-ids",
144                                                   "web-service-addresses" };
145
146   private HashSet<PhoneFieldDetails> _phone_numbers;
147   private Set<PhoneFieldDetails> _phone_numbers_ro;
148   private HashSet<EmailFieldDetails> _email_addresses;
149   private Set<EmailFieldDetails> _email_addresses_ro;
150   private HashSet<NoteFieldDetails> _notes;
151   private Set<NoteFieldDetails> _notes_ro;
152   private static HashTable<string, E.ContactField>? _im_eds_map = null;
153
154   private HashSet<PostalAddressFieldDetails> _postal_addresses;
155   private Set<PostalAddressFieldDetails> _postal_addresses_ro;
156
157   private HashSet<string> _local_ids;
158   private Set<string> _local_ids_ro;
159
160   private HashMultiMap<string, WebServiceFieldDetails> _web_service_addresses;
161
162   private bool _is_favourite;
163
164   private E.Contact _contact; /* should be set on construct */
165
166   /**
167    * The e-d-s contact represented by this Persona
168    */
169   public E.Contact contact
170     {
171       get { return this._contact; }
172       construct { this._contact = value; }
173     }
174
175   /**
176    * {@inheritDoc}
177    */
178   [CCode (notify = false)]
179   public MultiMap<string, WebServiceFieldDetails> web_service_addresses
180     {
181       get { return this._web_service_addresses; }
182       set { this.change_web_service_addresses.begin (value); }
183     }
184
185   /**
186    * {@inheritDoc}
187    *
188    * @since 0.6.2
189    */
190   public async void change_web_service_addresses (
191       MultiMap<string, WebServiceFieldDetails> web_service_addresses)
192           throws PropertyError
193     {
194       yield ((Edsf.PersonaStore) this.store)._set_web_service_addresses (this,
195           web_service_addresses);
196     }
197
198   /**
199    * IDs used to link {@link Edsf.Persona}s.
200    */
201   [CCode (notify = false)]
202   public Set<string> local_ids
203     {
204       get
205         {
206           if (this._local_ids.contains (this.iid) == false)
207             {
208               this._local_ids.add (this.iid);
209             }
210           return this._local_ids_ro;
211         }
212       set { this.change_local_ids.begin (value); }
213     }
214
215   /**
216    * {@inheritDoc}
217    *
218    * @since 0.6.2
219    */
220   public async void change_local_ids (Set<string> local_ids)
221       throws PropertyError
222     {
223       yield ((Edsf.PersonaStore) this.store)._set_local_ids (this, local_ids);
224     }
225
226   /**
227    * The postal addresses of the contact.
228    *
229    * A list of postal addresses associated to the contact.
230    *
231    * @since 0.6.0
232    */
233   [CCode (notify = false)]
234   public Set<PostalAddressFieldDetails> postal_addresses
235     {
236       get { return this._postal_addresses_ro; }
237       set { this.change_postal_addresses.begin (value); }
238     }
239
240   /**
241    * {@inheritDoc}
242    *
243    * @since 0.6.2
244    */
245   public async void change_postal_addresses (
246       Set<PostalAddressFieldDetails> postal_addresses) throws PropertyError
247     {
248       yield ((Edsf.PersonaStore) this.store)._set_postal_addresses (this,
249           postal_addresses);
250     }
251
252   /**
253    * {@inheritDoc}
254    *
255    * @since 0.6.0
256    */
257   [CCode (notify = false)]
258   public Set<PhoneFieldDetails> phone_numbers
259     {
260       get { return this._phone_numbers_ro; }
261       set { this.change_phone_numbers.begin (value); }
262     }
263
264   /**
265    * {@inheritDoc}
266    *
267    * @since 0.6.2
268    */
269   public async void change_phone_numbers (
270       Set<PhoneFieldDetails> phone_numbers) throws PropertyError
271     {
272       yield ((Edsf.PersonaStore) this.store)._set_phones (this, phone_numbers);
273     }
274
275   /**
276    * {@inheritDoc}
277    *
278    * @since 0.6.0
279    */
280   [CCode (notify = false)]
281   public Set<EmailFieldDetails> email_addresses
282     {
283       get { return this._email_addresses_ro; }
284       set { this.change_email_addresses.begin (value); }
285     }
286
287   /**
288    * {@inheritDoc}
289    *
290    * @since 0.6.2
291    */
292   public async void change_email_addresses (
293       Set<EmailFieldDetails> email_addresses) throws PropertyError
294     {
295       yield ((Edsf.PersonaStore) this.store)._set_emails (this,
296           email_addresses);
297     }
298
299   /**
300    * {@inheritDoc}
301    *
302    * @since 0.6.0
303    */
304   [CCode (notify = false)]
305   public Set<NoteFieldDetails> notes
306     {
307       get { return this._notes_ro; }
308       set { this.change_notes.begin (value); }
309     }
310
311   /**
312    * {@inheritDoc}
313    *
314    * @since 0.6.2
315    */
316   public async void change_notes (Set<NoteFieldDetails> notes)
317       throws PropertyError
318     {
319       yield ((Edsf.PersonaStore) this.store)._set_notes (this, notes);
320     }
321
322   /**
323    * {@inheritDoc}
324    *
325    * @since 0.6.0
326    */
327   public override string[] linkable_properties
328     {
329       get { return Persona._linkable_properties; }
330     }
331
332   /**
333    * {@inheritDoc}
334    *
335    * @since 0.6.0
336    */
337   public override string[] writeable_properties
338     {
339       get { return this.store.always_writeable_properties; }
340     }
341
342   private LoadableIcon? _avatar = null;
343   /**
344    * An avatar for the Persona.
345    *
346    * See {@link Folks.AvatarDetails.avatar}.
347    *
348    * @since 0.6.0
349    */
350   [CCode (notify = false)]
351   public LoadableIcon? avatar
352     {
353       get { return this._avatar; }
354       set { this.change_avatar.begin (value); }
355     }
356
357   /**
358    * {@inheritDoc}
359    *
360    * @since 0.6.2
361    */
362   public async void change_avatar (LoadableIcon? avatar) throws PropertyError
363     {
364       yield ((Edsf.PersonaStore) this.store)._set_avatar (this, avatar);
365     }
366
367   private StructuredName? _structured_name = null;
368   /**
369    * {@inheritDoc}
370    *
371    * @since 0.6.0
372    */
373   [CCode (notify = false)]
374   public StructuredName? structured_name
375     {
376       get { return this._structured_name; }
377       set { this.change_structured_name.begin (value); }
378     }
379
380   /**
381    * {@inheritDoc}
382    *
383    * @since 0.6.2
384    */
385   public async void change_structured_name (StructuredName? structured_name)
386       throws PropertyError
387     {
388       yield ((Edsf.PersonaStore) this.store)._set_structured_name (this,
389           structured_name);
390     }
391
392   /**
393    * The e-d-s contact uid
394    *
395    * @since 0.6.0
396    */
397   public string contact_id { get; construct; }
398
399   private string _full_name = "";
400   /**
401    * {@inheritDoc}
402    *
403    * @since 0.6.0
404    */
405   [CCode (notify = false)]
406   public string full_name
407     {
408       get { return this._full_name; }
409       set { this.change_full_name.begin (value); }
410     }
411
412   /**
413    * {@inheritDoc}
414    *
415    * @since 0.6.2
416    */
417   public async void change_full_name (string full_name) throws PropertyError
418     {
419       yield ((Edsf.PersonaStore) this.store)._set_full_name (this, full_name);
420     }
421
422   private string _nickname = "";
423   /**
424    * {@inheritDoc}
425    *
426    * @since 0.6.0
427    */
428   [CCode (notify = false)]
429   public string nickname
430     {
431       get { return this._nickname; }
432       set { this.change_nickname.begin (value); }
433     }
434
435   /**
436    * {@inheritDoc}
437    *
438    * @since 0.6.2
439    */
440   public async void change_nickname (string nickname) throws PropertyError
441     {
442       yield ((Edsf.PersonaStore) this.store)._set_nickname (this, nickname);
443     }
444
445   private Gender _gender;
446   /**
447    * {@inheritDoc}
448    *
449    * @since 0.6.0
450    */
451   [CCode (notify = false)]
452   public Gender gender
453     {
454       get { return this._gender; }
455       set { this.change_gender.begin (value); }
456     }
457
458   /**
459    * {@inheritDoc}
460    *
461    * @since 0.6.2
462    */
463   public async void change_gender (Gender gender) throws PropertyError
464     {
465       yield ((Edsf.PersonaStore) this.store)._set_gender (this, gender);
466     }
467
468   private HashSet<UrlFieldDetails> _urls;
469   private Set<UrlFieldDetails> _urls_ro;
470   /**
471    * {@inheritDoc}
472    *
473    * @since 0.6.0
474    */
475   [CCode (notify = false)]
476   public Set<UrlFieldDetails> urls
477     {
478       get { return this._urls_ro; }
479       set { this.change_urls.begin (value); }
480     }
481
482   /**
483    * {@inheritDoc}
484    *
485    * @since 0.6.2
486    */
487   public async void change_urls (Set<UrlFieldDetails> urls) throws PropertyError
488     {
489       yield ((Edsf.PersonaStore) this.store)._set_urls (this, urls);
490     }
491
492   private HashMultiMap<string, ImFieldDetails> _im_addresses =
493       new HashMultiMap<string, ImFieldDetails> (null, null,
494           (GLib.HashFunc) ImFieldDetails.hash,
495           (GLib.EqualFunc) ImFieldDetails.equal);
496
497   /**
498    * {@inheritDoc}
499    *
500    * @since 0.6.0
501    */
502   [CCode (notify = false)]
503   public MultiMap<string, ImFieldDetails> im_addresses
504     {
505       get { return this._im_addresses; }
506       set { this.change_im_addresses.begin (value); }
507     }
508
509   /**
510    * {@inheritDoc}
511    *
512    * @since 0.6.2
513    */
514   public async void change_im_addresses (
515       MultiMap<string, ImFieldDetails> im_addresses) throws PropertyError
516     {
517       yield ((Edsf.PersonaStore) this.store)._set_im_fds (this, im_addresses);
518     }
519
520   private HashSet<string> _groups;
521   private Set<string> _groups_ro;
522
523   /**
524    * {@inheritDoc}
525    *
526    * @since 0.6.0
527    */
528   [CCode (notify = false)]
529   public Set<string> groups
530     {
531       get { return this._groups_ro; }
532       set { this.change_groups.begin (value); }
533     }
534
535   /**
536    * {@inheritDoc}
537    *
538    * @since 0.6.0
539    */
540   public async void change_group (string group, bool is_member)
541       throws GLib.Error
542     {
543       /* Nothing to do? */
544       if ((is_member == true && this._groups.contains (group) == true) ||
545           (is_member == false && this._groups.contains (group) == false))
546         {
547           return;
548         }
549
550       /* Replace the current set of groups with a modified one. */
551       var new_groups = new HashSet<string> ();
552       foreach (var category_name in this._groups)
553         {
554           new_groups.add (category_name);
555         }
556
557       if (is_member == false)
558         {
559           new_groups.remove (group);
560         }
561       else
562         {
563           new_groups.add (group);
564         }
565
566       yield this.change_groups (new_groups);
567     }
568
569   /**
570    * {@inheritDoc}
571    *
572    * @since 0.6.2
573    */
574   public async void change_groups (Set<string> groups) throws PropertyError
575     {
576       yield ((Edsf.PersonaStore) this.store)._set_groups (this, groups);
577     }
578
579   /**
580    * {@inheritDoc}
581    *
582    * e-d-s has no equivalent field, so this is unsupported.
583    *
584    * @since 0.6.2
585    */
586   [CCode (notify = false)]
587   public string? calendar_event_id
588     {
589       get { return null; } /* unsupported */
590       set { this.change_calendar_event_id.begin (value); } /* not writeable */
591     }
592
593   /* We cache the timezone we use for converting birthdays to UTC since creating
594    * it requires mmapping /etc/localtime, which means lots of syscalls. */
595   private static TimeZone _local_time_zone = new TimeZone.local ();
596
597   private DateTime? _birthday = null;
598   /**
599    * {@inheritDoc}
600    *
601    * @since 0.6.2
602    */
603   [CCode (notify = false)]
604   public DateTime? birthday
605     {
606       get { return this._birthday; }
607       set { this.change_birthday.begin (value); }
608     }
609
610   /**
611    * {@inheritDoc}
612    *
613    * @since 0.6.2
614    */
615   public async void change_birthday (DateTime? bday)
616       throws PropertyError
617     {
618       yield ((Edsf.PersonaStore) this.store)._set_birthday (this,
619           bday);
620     }
621
622   private HashSet<RoleFieldDetails> _roles;
623   private Set<RoleFieldDetails> _roles_ro;
624
625   /**
626    * {@inheritDoc}
627    *
628    * @since 0.6.2
629    */
630   [CCode (notify = false)]
631   public Set<RoleFieldDetails> roles
632     {
633       get { return this._roles_ro; }
634       set { this.change_roles.begin (value); }
635     }
636
637   /**
638    * {@inheritDoc}
639    *
640    * @since 0.6.2
641    */
642   public async void change_roles (Set<RoleFieldDetails> roles)
643       throws PropertyError
644     {
645       yield ((Edsf.PersonaStore) this.store)._set_roles (this, roles);
646     }
647
648   /**
649    * Whether this contact is a user-defined favourite.
650    *
651    * @since 0.6.5
652    */
653   [CCode (notify = false)]
654   public bool is_favourite
655       {
656         get { return this._is_favourite; }
657         set { this.change_is_favourite.begin (value); }
658       }
659
660   /**
661    * {@inheritDoc}
662    *
663    * @since 0.6.5
664    */
665   public async void change_is_favourite (bool is_favourite) throws PropertyError
666     {
667       if (this._is_favourite == is_favourite)
668         {
669           return;
670         }
671
672       yield ((Edsf.PersonaStore) this.store)._set_is_favourite (this,
673           is_favourite);
674     }
675
676   private HashSet<string> _anti_links;
677   private Set<string> _anti_links_ro;
678
679   /**
680    * {@inheritDoc}
681    *
682    * @since 0.7.3
683    */
684   [CCode (notify = false)]
685   public Set<string> anti_links
686     {
687       get { return this._anti_links_ro; }
688       set { this.change_anti_links.begin (value); }
689     }
690
691   /**
692    * {@inheritDoc}
693    *
694    * @since 0.7.3
695    */
696   public async void change_anti_links (Set<string> anti_links)
697       throws PropertyError
698     {
699       yield ((Edsf.PersonaStore) this.store)._set_anti_links (this, anti_links);
700     }
701
702   private bool _in_google_personal_group;
703
704   /**
705    * Whether this contact is in the “My Contacts” section of the user’s address
706    * book, rather than the “Other” section.
707    *
708    * @since 0.7.3
709    */
710   [CCode (notify = false)]
711   public bool in_google_personal_group
712     {
713       get { return this._in_google_personal_group; }
714     }
715
716   /**
717    * Build a IID.
718    *
719    * @param store_id the {@link PersonaStore.id}
720    * @param contact the Contact
721    * @return a valid IID
722    *
723    * @since 0.6.0
724    */
725   internal static string build_iid_from_contact (string store_id,
726       E.Contact contact)
727     {
728       var contact_id =
729           Edsf.Persona._get_property_from_contact<string> (contact, "id");
730       return Edsf.Persona.build_iid (store_id, (!) (contact_id ?? ""));
731     }
732
733   /**
734    * Build a IID.
735    *
736    * @param store_id the {@link PersonaStore.id}
737    * @param contact_id the id belonging to the Contact
738    * @return a valid IID
739    *
740    * @since 0.6.0
741    */
742   internal static string build_iid (string store_id, string contact_id)
743     {
744       return "%s:%s".printf (store_id, contact_id);
745     }
746
747
748   /**
749    * Create a new persona.
750    *
751    * Create a new persona for the {@link PersonaStore} `store`, representing
752    * the EDS contact given by `contact`.
753    *
754    * @param store the store which will contain the persona
755    * @param contact the EDS contact being represented by the persona
756    *
757    * @since 0.6.0
758    */
759   public Persona (PersonaStore store, E.Contact contact)
760     {
761       var _contact_id =
762           Edsf.Persona._get_property_from_contact<string> (contact, "id");
763       var contact_id = (!) (_contact_id ?? "");
764
765       var uid = Folks.Persona.build_uid (BACKEND_NAME, store.id, contact_id);
766       var iid = Edsf.Persona.build_iid (store.id, contact_id);
767       var is_user = BookClient.is_self (contact);
768       var _full_name =
769           Edsf.Persona._get_property_from_contact<string> (contact,
770               "full_name");
771       var full_name = (!) (_full_name ?? "");
772
773       Object (display_id: full_name,
774               uid: uid,
775               iid: iid,
776               store: store,
777               is_user: is_user,
778               contact_id: contact_id,
779               contact: contact);
780     }
781
782   construct
783     {
784       debug ("Creating new Edsf.Persona with IID '%s'", this.iid);
785
786       this._gender = Gender.UNSPECIFIED;
787       this._phone_numbers = new HashSet<PhoneFieldDetails> (
788           (GLib.HashFunc) PhoneFieldDetails.hash,
789           (GLib.EqualFunc) PhoneFieldDetails.equal);
790       this._phone_numbers_ro = this._phone_numbers.read_only_view;
791       this._email_addresses = new HashSet<EmailFieldDetails> (
792           (GLib.HashFunc) EmailFieldDetails.hash,
793           (GLib.EqualFunc) EmailFieldDetails.equal);
794       this._email_addresses_ro = this._email_addresses.read_only_view;
795       this._notes = new HashSet<NoteFieldDetails> (
796           (GLib.HashFunc) NoteFieldDetails.hash,
797           (GLib.EqualFunc) NoteFieldDetails.equal);
798       this._notes_ro = this._notes.read_only_view;
799       this._urls = new HashSet<UrlFieldDetails> (
800           (GLib.HashFunc) UrlFieldDetails.hash,
801           (GLib.EqualFunc) UrlFieldDetails.equal);
802       this._urls_ro = this._urls.read_only_view;
803       this._postal_addresses = new HashSet<PostalAddressFieldDetails> (
804           (GLib.HashFunc) PostalAddressFieldDetails.hash,
805           (GLib.EqualFunc) PostalAddressFieldDetails.equal);
806       this._postal_addresses_ro = this._postal_addresses.read_only_view;
807       this._local_ids = new HashSet<string> ();
808       this._local_ids_ro = this._local_ids.read_only_view;
809       this._web_service_addresses =
810         new HashMultiMap<string, WebServiceFieldDetails> (
811             null, null,
812             (GLib.HashFunc) WebServiceFieldDetails.hash,
813             (GLib.EqualFunc) WebServiceFieldDetails.equal);
814       this._groups = new HashSet<string> ();
815       this._groups_ro = this._groups.read_only_view;
816       this._roles = new HashSet<RoleFieldDetails> (
817           (GLib.HashFunc) RoleFieldDetails.hash,
818           (GLib.EqualFunc) RoleFieldDetails.equal);
819       this._roles_ro = this._roles.read_only_view;
820       this._anti_links = new HashSet<string> ();
821       this._anti_links_ro = this._anti_links.read_only_view;
822
823       this._update (this._contact);
824     }
825
826   /**
827    * {@inheritDoc}
828    *
829    * @since 0.6.0
830    */
831   public override void linkable_property_to_links (string prop_name,
832       Folks.Persona.LinkablePropertyCallback callback)
833     {
834       if (prop_name == "im-addresses")
835         {
836           foreach (var protocol in this._im_addresses.get_keys ())
837             {
838               var im_fds = this._im_addresses.get (protocol);
839
840               foreach (var im_fd in im_fds)
841                   callback (protocol + ":" + im_fd.value);
842             }
843         }
844       else if (prop_name == "local-ids")
845         {
846           /* Note: we need to use this.local_ids and not this._local_ids,
847            * otherwise this can have a different  behaviour depending
848            * on the state of the current Persona depending on whether
849            * this.local_ids was called before or not. */
850           foreach (var id in this.local_ids)
851             {
852               callback (id);
853             }
854         }
855       else if (prop_name == "web-service-addresses")
856         {
857           foreach (var web_service in this.web_service_addresses.get_keys ())
858             {
859               var web_service_addresses =
860                   this._web_service_addresses.get (web_service);
861
862               foreach (var ws_fd in web_service_addresses)
863                   callback (web_service + ":" + ws_fd.value);
864             }
865         }
866       else
867         {
868           /* Chain up */
869           base.linkable_property_to_links (prop_name, callback);
870         }
871     }
872
873   ~Persona ()
874     {
875       debug ("Destroying Edsf.Persona '%s': %p", this.uid, this);
876     }
877
878   /**
879    * Update attribs of the persona.
880    */
881   internal void _update (E.Contact updated_contact)
882     {
883       this.freeze_notify ();
884
885       /* We get a new E.Contact instance from EDS containing all the updates,
886        * so replace our existing contact with it. */
887       this._contact = updated_contact;
888       this.notify_property ("contact");
889
890       this._update_names ();
891       this._update_avatar ();
892       this._update_urls ();
893       this._update_phones ();
894       this._update_addresses ();
895       this._update_emails ();
896
897       /* Note: because we assume certain e-mail addresses
898        * (@gmail, @msn, etc) to also be IM IDs we /must/
899        * update the latter after we've taken care of the former.
900        */
901       this._update_im_addresses ();
902
903       this._update_groups ();
904       this._update_notes ();
905       this._update_local_ids ();
906       this._update_web_services_addresses ();
907       this._update_gender ();
908       this._update_birthday ();
909       this._update_roles ();
910       this._update_favourite ();
911       this._update_anti_links ();
912
913       this.thaw_notify ();
914     }
915
916   private void _update_params (AbstractFieldDetails details,
917       E.VCardAttribute attr)
918     {
919       foreach (unowned E.VCardAttributeParam param in attr.get_params ())
920         {
921           string param_name = param.get_name ().down ();
922           foreach (unowned string param_value in param.get_values ())
923             {
924               if (param_name == AbstractFieldDetails.PARAM_TYPE)
925                 {
926                   details.add_parameter (param_name, param_value.down ());
927                 }
928               else
929                 {
930                   details.add_parameter (param_name, param_value);
931                 }
932             }
933         }
934     }
935
936   private void _update_gender ()
937     {
938       var gender = Gender.UNSPECIFIED;
939       var gender_attr =
940           this.contact.get_attribute (Edsf.Persona.gender_attribute_name);
941
942       if (gender_attr != null)
943         {
944           var val = ((!) gender_attr).get_value ();
945           if (val != null)
946             {
947               switch (((!) val).up ())
948                 {
949                   case Edsf.Persona.gender_male:
950                     gender = Gender.MALE;
951                     break;
952                   case Edsf.Persona.gender_female:
953                     gender = Gender.FEMALE;
954                     break;
955                   default:
956                     /* Unspecified, as above */
957                     break;
958                 }
959             }
960         }
961
962       if (this._gender != gender)
963         {
964           this._gender = gender;
965           this.notify_property ("gender");
966         }
967     }
968
969   private void _update_birthday ()
970     {
971       var _bday = this._get_property<E.ContactDate> ("birth_date");
972
973       if (_bday != null)
974         {
975           var bday = (!) _bday;
976
977           /* Since e-d-s stores birthdays as a plain date, we take the
978            * given date in local time and convert it to UTC as mandated
979            * by the BirthdayDetails interface.
980            * We cache the timezone since creating it requires mmapping
981            * /etc/localtime, which means lots of syscalls. */
982           var d = new DateTime (Persona._local_time_zone,
983               (int) bday.year, (int) bday.month, (int) bday.day, 0, 0, 0.0);
984           if (this._birthday == null ||
985               (this._birthday != null &&
986                   !((!) this._birthday).equal (d.to_utc ())))
987             {
988               this._birthday = d.to_utc ();
989               this.notify_property ("birthday");
990             }
991         }
992       else
993         {
994           if (this._birthday != null)
995             {
996               this._birthday = null;
997               this.notify_property ("birthday");
998             }
999         }
1000     }
1001
1002   private void _update_roles ()
1003     {
1004       var new_roles = new HashSet<RoleFieldDetails> (
1005           (GLib.HashFunc) RoleFieldDetails.hash,
1006           (GLib.EqualFunc) RoleFieldDetails.equal);
1007
1008       var default_role_fd = this._get_default_role ();
1009       if (default_role_fd != null)
1010         {
1011           new_roles.add ((!) default_role_fd);
1012         }
1013
1014       var vcard = (E.VCard) this.contact;
1015       foreach (unowned E.VCardAttribute attr in vcard.get_attributes ())
1016         {
1017           if (attr.get_name () != "X-ROLES")
1018             continue;
1019
1020           var val = attr.get_value ();
1021           if (val == null || (!) val == "")
1022              {
1023               continue;
1024             }
1025
1026           var role = new Role ("", "");
1027           role.role = (!) val;
1028           var role_fd = new RoleFieldDetails (role);
1029
1030           foreach (unowned E.VCardAttributeParam param in
1031               attr.get_params ())
1032             {
1033               string param_name = param.get_name ().down ();
1034
1035               if (param_name == "organisation_name")
1036                 {
1037                   foreach (unowned string param_value in
1038                       param.get_values ())
1039                     {
1040                       role.organisation_name = param_value;
1041                       break;
1042                     }
1043                 }
1044               else if (param_name == "title")
1045                 {
1046                   foreach (unowned string param_value in
1047                       param.get_values ())
1048                     {
1049                       role.title = param_value;
1050                       break;
1051                     }
1052                 }
1053               else
1054                 {
1055                   foreach (unowned string param_value in
1056                       param.get_values ())
1057                     {
1058                       role_fd.add_parameter (param_name, param_value);
1059                     }
1060                 }
1061             }
1062
1063             new_roles.add (role_fd);
1064         }
1065
1066       if (!Folks.Internal.equal_sets<RoleFieldDetails> (new_roles, this._roles))
1067         {
1068           this._roles = new_roles;
1069           this._roles_ro = new_roles.read_only_view;
1070           this.notify_property ("roles");
1071         }
1072     }
1073
1074   private RoleFieldDetails? _get_default_role ()
1075     {
1076       RoleFieldDetails? _default_role = null;
1077
1078       var org = this._get_property<string> ("org");
1079       var org_unit = this._get_property<string> ("org_unit");
1080       var office = this._get_property<string> ("office");
1081       var title = this._get_property<string> ("title");
1082       var role = this._get_property<string> ("role");
1083       var manager = this._get_property<string> ("manager");
1084       var assistant = this._get_property<string> ("assistant");
1085
1086       if (org != null ||
1087           org_unit != null ||
1088           office != null ||
1089           title != null ||
1090           role != null ||
1091           manager != null ||
1092           assistant != null)
1093         {
1094           var new_role = new Role (title, org);
1095           if (role != null && (!) role != "")
1096             new_role.role = (!) role;
1097
1098           /* Check if it's non-empty. */
1099           if (!new_role.is_empty ())
1100             {
1101               var default_role = new RoleFieldDetails (new_role);
1102
1103               if (org_unit != null && org_unit != "")
1104                 default_role.set_parameter ("org_unit", (!) org_unit);
1105
1106               if (office != null && office != "")
1107                 default_role.set_parameter ("office", (!) office);
1108
1109               if (manager != null && manager != "")
1110                 default_role.set_parameter ("manager", (!) manager);
1111
1112               if (assistant != null && manager != "")
1113                 default_role.set_parameter ("assistant", (!) assistant);
1114
1115               _default_role = default_role;
1116             }
1117         }
1118
1119       return _default_role;
1120     }
1121
1122   private void _update_web_services_addresses ()
1123     {
1124       var new_services = new HashMultiMap<string, WebServiceFieldDetails> (
1125           null, null,
1126           (GLib.HashFunc) WebServiceFieldDetails.hash,
1127           (GLib.EqualFunc) WebServiceFieldDetails.equal);
1128
1129       var services = this.contact.get_attribute ("X-FOLKS-WEB-SERVICES-IDS");
1130       if (services != null)
1131         {
1132           foreach (var service in ((!) services).get_params ())
1133             {
1134               var service_name = service.get_name ().down ();
1135               foreach (var service_id in service.get_values ())
1136                 {
1137                   if (service_id == "")
1138                     {
1139                       continue;
1140                     }
1141
1142                   new_services.set (service_name,
1143                       new WebServiceFieldDetails (service_id));
1144                 }
1145             }
1146         }
1147
1148       if (!Utils.multi_map_str_afd_equal (new_services,
1149               this._web_service_addresses))
1150         {
1151           this._web_service_addresses = new_services;
1152           this.notify_property ("web-service-addresses");
1153         }
1154     }
1155
1156   private void _update_emails ()
1157     {
1158       var new_email_addresses = new HashSet<EmailFieldDetails> (
1159           (GLib.HashFunc) EmailFieldDetails.hash,
1160           (GLib.EqualFunc) EmailFieldDetails.equal);
1161
1162       var attrs = this.contact.get_attributes (E.ContactField.EMAIL);
1163       foreach (var attr in attrs)
1164         {
1165           var val = attr.get_value ();
1166           if (val == null || (!) val == "")
1167             {
1168               continue;
1169             }
1170
1171           var email_fd = new EmailFieldDetails ((!) val);
1172           this._update_params (email_fd, attr);
1173           new_email_addresses.add (email_fd);
1174         }
1175
1176       if (!Folks.Internal.equal_sets<EmailFieldDetails> (new_email_addresses,
1177               this._email_addresses))
1178         {
1179          this._email_addresses = new_email_addresses;
1180          this._email_addresses_ro = new_email_addresses.read_only_view;
1181          this.notify_property ("email-addresses");
1182        }
1183     }
1184
1185   private void _update_notes ()
1186     {
1187       var new_notes = new HashSet<NoteFieldDetails> (
1188           (GLib.HashFunc) NoteFieldDetails.hash,
1189           (GLib.EqualFunc) NoteFieldDetails.equal);
1190
1191       var n = this._get_property<string> ("note");
1192       if (n != null && n != "")
1193         {
1194           var note = new NoteFieldDetails ((!) n);
1195           new_notes.add (note);
1196         }
1197
1198       if (!Folks.Internal.equal_sets<NoteFieldDetails> (new_notes, this._notes))
1199         {
1200           this._notes = new_notes;
1201           this._notes_ro = this._notes.read_only_view;
1202           this.notify_property ("notes");
1203         }
1204     }
1205
1206   private void _update_names ()
1207     {
1208       var _full_name = this._get_property<string> ("full_name");
1209
1210       if (_full_name == null)
1211         {
1212           _full_name = "";
1213         }
1214
1215       var full_name = (!) _full_name;
1216
1217       if (this._full_name != full_name)
1218         {
1219           this._full_name = full_name;
1220           this.notify_property ("full-name");
1221         }
1222
1223       var _nickname = this._get_property<string> ("nickname");
1224
1225       if (_nickname == null)
1226         {
1227           _nickname = "";
1228         }
1229
1230       var nickname = (!) _nickname;
1231
1232       if (this._nickname != nickname)
1233         {
1234           this._nickname = nickname;
1235           this.notify_property ("nickname");
1236         }
1237
1238       StructuredName? structured_name = null;
1239       var _cn = this._get_property<E.ContactName> ("name");
1240       if (_cn != null)
1241         {
1242           var cn = (!) _cn;
1243
1244           string family_name = cn.family;
1245           string given_name  = cn.given;
1246           string additional_names = cn.additional;
1247           string prefixes = cn.prefixes;
1248           string suffixes = cn.suffixes;
1249           structured_name = new StructuredName (family_name, given_name,
1250                                                 additional_names, prefixes,
1251                                                 suffixes);
1252         }
1253
1254       if (structured_name != null && !((!) structured_name).is_empty ())
1255         {
1256           this._structured_name = (!) structured_name;
1257           this.notify_property ("structured-name");
1258         }
1259       else if (this._structured_name != null)
1260         {
1261           this._structured_name = null;
1262           this.notify_property ("structured-name");
1263         }
1264     }
1265
1266   private LoadableIcon? _contact_photo_to_loadable_icon (ContactPhoto? _p)
1267     {
1268       if (_p == null)
1269         {
1270           return null;
1271         }
1272
1273       var p = (!) _p;
1274
1275       switch (p.type)
1276         {
1277           case ContactPhotoType.URI:
1278             var uri = p.get_uri ();
1279             if (uri == null)
1280               {
1281                 return null;
1282               }
1283
1284             return new FileIcon (File.new_for_uri ((!) uri));
1285           case ContactPhotoType.INLINED:
1286             var data = p.get_inlined ();
1287             var mime_type = p.get_mime_type ();
1288             if (data == null || mime_type == null)
1289               {
1290                 return null;
1291               }
1292
1293             return new Edsf.MemoryIcon ((!) mime_type, (!) data);
1294           default:
1295             return null;
1296         }
1297     }
1298
1299   private void _update_avatar ()
1300     {
1301       var p = this._get_property<E.ContactPhoto> ("photo");
1302
1303       var cache = AvatarCache.dup ();
1304
1305       // Convert the ContactPhoto to a LoadableIcon and store or update it.
1306       var new_avatar = this._contact_photo_to_loadable_icon (p);
1307
1308       if (this._avatar != null && new_avatar == null)
1309         {
1310           // Remove the old cached avatar, ignoring errors.
1311           cache.remove_avatar.begin (this.uid, (obj, res) =>
1312             {
1313               try
1314                 {
1315                   cache.remove_avatar.end (res);
1316                 }
1317               catch (GLib.Error e1) {}
1318
1319               this._avatar = null;
1320               this.notify_property ("avatar");
1321             });
1322         }
1323       else if ((this._avatar == null && new_avatar != null) ||
1324           (this._avatar != null && new_avatar != null &&
1325            ((!) this._avatar).equal (new_avatar) == false))
1326         {
1327           /* Store the new avatar in the cache. new_avatar is guaranteed to be
1328            * non-null. */
1329           cache.store_avatar.begin (this.uid, (!) new_avatar, (obj, res) =>
1330             {
1331               try
1332                 {
1333                   cache.store_avatar.end (res);
1334                   this._avatar = new_avatar;
1335                   this.notify_property ("avatar");
1336                 }
1337               catch (GLib.Error e2)
1338                 {
1339                   warning ("Couldn't cache avatar for Edsf.Persona '%s': %s",
1340                       this.uid, e2.message);
1341                   new_avatar = null; /* failure */
1342                 }
1343             });
1344         }
1345     }
1346
1347   private void _update_urls ()
1348     {
1349       var new_urls = new HashSet<UrlFieldDetails> ();
1350
1351       /* First we get the standard Evo urls.. */
1352       foreach (var mapping in Persona._url_properties)
1353         {
1354           var url_property = mapping.vcard_field_name;
1355           var folks_type = mapping.folks_type;
1356
1357           var u = this._get_property<string> (url_property);
1358           if (u != null && u != "")
1359             {
1360               var fd_u = new UrlFieldDetails ((!) u);
1361               fd_u.set_parameter (AbstractFieldDetails.PARAM_TYPE, folks_type);
1362               new_urls.add (fd_u);
1363             }
1364         }
1365
1366       /* Now we go for extra URLs */
1367       var vcard = (E.VCard) this.contact;
1368       foreach (unowned E.VCardAttribute attr in vcard.get_attributes ())
1369         {
1370           if (attr.get_name () == "X-URIS")
1371             {
1372               var val = attr.get_value ();
1373               if (val == null || (!) val == "")
1374                 {
1375                   continue;
1376                 }
1377
1378               var url_fd = new UrlFieldDetails ((!) val);
1379               this._update_params (url_fd, attr);
1380               new_urls.add (url_fd);
1381             }
1382         }
1383
1384       if (!Utils.set_afd_equal (new_urls, this._urls))
1385         {
1386           this._urls = new_urls;
1387           this._urls_ro = new_urls.read_only_view;
1388           this.notify_property ("urls");
1389         }
1390     }
1391
1392   private void _update_im_addresses ()
1393     {
1394       var im_eds_map = Persona._get_im_eds_map ();
1395       var new_im_addresses = new HashMultiMap<string, ImFieldDetails> (null,
1396           null, (GLib.HashFunc) ImFieldDetails.hash,
1397           (GLib.EqualFunc) ImFieldDetails.equal);
1398
1399       foreach (var im_proto in im_eds_map.get_keys ())
1400         {
1401           var addresses = this.contact.get_attributes (
1402               im_eds_map.lookup (im_proto));
1403           foreach (var attr in addresses)
1404             {
1405               try
1406                 {
1407                   var addr = attr.get_value ();
1408                   if (addr == null || (!) addr == "")
1409                     {
1410                       continue;
1411                     }
1412
1413                   string normalised_addr =
1414                     (owned) ImDetails.normalise_im_address ((!) addr, im_proto);
1415
1416                   if (normalised_addr == "")
1417                     {
1418                       continue;
1419                     }
1420
1421                   var im_fd = new ImFieldDetails (normalised_addr);
1422                   new_im_addresses.set (im_proto, im_fd);
1423                 }
1424               catch (Folks.ImDetailsError e)
1425                 {
1426                   GLib.warning (
1427                       "Problem when trying to normalise address: %s\n",
1428                       e.message);
1429                 }
1430             }
1431         }
1432
1433       /* We consider some e-mail addresses to be IM IDs too. This
1434        * is pretty much a hack to make sure e-d-s contacts are
1435        * automatically linked with their corresponding Telepathy
1436        * Persona. As an undesired side effect we might end up having
1437        * IM addresses that aren't actually used as such (i.e.: people
1438        * who don't actually use GMail or MSN addresses for IM).
1439        *
1440        * See bgo#657142
1441        */
1442       foreach (var email in this.email_addresses)
1443         {
1444           var _proto = this._im_proto_from_addr (email.value);
1445           if (_proto != null)
1446             {
1447               var proto = (!) _proto;
1448
1449               /* Has this already been added? */
1450               var exists = false;
1451               Collection<ImFieldDetails>? current_im_addrs =
1452                   new_im_addresses.get (proto);
1453               if (current_im_addrs != null)
1454                 {
1455                   foreach (var cur_im in (!) current_im_addrs)
1456                     {
1457                       if (cur_im.value == email.value)
1458                         {
1459                           exists = true;
1460                           break;
1461                         }
1462                     }
1463                 }
1464
1465               if (exists)
1466                 continue;
1467
1468               try
1469                 {
1470                   string normalised_addr =
1471                     (owned) ImDetails.normalise_im_address (email.value, proto);
1472                   var im_fd = new ImFieldDetails (normalised_addr);
1473                   new_im_addresses.set (proto, im_fd);
1474                 }
1475               catch (Folks.ImDetailsError e)
1476                 {
1477                   GLib.warning (
1478                       "Problem when trying to normalise address: %s\n",
1479                       e.message);
1480                 }
1481             }
1482         }
1483
1484       if (!Utils.multi_map_str_afd_equal (new_im_addresses,
1485               this._im_addresses))
1486         {
1487           this._im_addresses = new_im_addresses;
1488           this.notify_property ("im-addresses");
1489         }
1490     }
1491
1492   private void _update_groups ()
1493     {
1494       var category_names =
1495           this._contact.get<GLib.List<string>> (E.ContactField.CATEGORY_LIST);
1496       var new_categories = new HashSet<string> ();
1497       var added_categories = new LinkedList<string> ();
1498
1499       foreach (var category_name in category_names)
1500         {
1501           /* Skip the “Starred in Android” group for Google personas; we handle
1502            * it later. */
1503           if (((Edsf.PersonaStore) store)._is_google_contacts_address_book () &&
1504               category_name == Edsf.PersonaStore.android_favourite_group_name)
1505             {
1506               continue;
1507             }
1508
1509           new_categories.add (category_name);
1510
1511           /* Is this a new category? */
1512           if (!this._groups.contains (category_name))
1513             {
1514               added_categories.add (category_name);
1515             }
1516         }
1517
1518       /* Work out which categories have been removed. */
1519       var removed_categories = new LinkedList<string> ();
1520
1521       foreach (var category_name in this._groups)
1522         {
1523           /* Skip the “Starred in Android” group for Google personas; we handle
1524            * it later. */
1525           if (((Edsf.PersonaStore) store)._is_google_contacts_address_book () &&
1526               category_name == Edsf.PersonaStore.android_favourite_group_name)
1527             {
1528               continue;
1529             }
1530
1531           if (!new_categories.contains (category_name))
1532             {
1533               removed_categories.add (category_name);
1534             }
1535         }
1536
1537       /* Check our new set of system groups if this is a Google address book. */
1538       var store = (Edsf.PersonaStore) this.store;
1539       var in_google_personal_group = false;
1540
1541       if (store._is_google_contacts_address_book ())
1542         {
1543           var vcard = (E.VCard) this.contact;
1544           unowned E.VCardAttribute? attr =
1545              vcard.get_attribute ("X-GOOGLE-SYSTEM-GROUP-IDS");
1546           if (attr != null)
1547             {
1548               unowned GLib.List<string> vals = attr.get_values ();
1549
1550               /* If we're in the GDATA_CONTACTS_GROUP_CONTACTS group, then
1551                * we're in the user's "My Contacts" address book, as opposed
1552                * to their "Other" address book. */
1553               foreach (var system_group_id in vals)
1554                 {
1555                   if (system_group_id == "Contacts")
1556                     {
1557                       in_google_personal_group = true;
1558                       break;
1559                     }
1560                 }
1561             }
1562         }
1563
1564       /* Check whether our favourite status needs updating. */
1565       var old_is_favourite = this._is_favourite;
1566
1567       if (store._is_google_contacts_address_book ())
1568         {
1569           this._is_favourite = false;
1570
1571           foreach (var category_name in category_names)
1572             {
1573               /* We link the “Starred in Android” group to Google Contacts
1574                * address books. See: bgo#661490. */
1575               if (category_name ==
1576                   Edsf.PersonaStore.android_favourite_group_name)
1577                 {
1578                   this._is_favourite = true;
1579                 }
1580             }
1581         }
1582
1583       /* Notify if anything's changed. */
1584       this.freeze_notify ();
1585
1586       if (added_categories.size != 0 || removed_categories.size != 0)
1587         {
1588           this.notify_property ("groups");
1589         }
1590       if (this._is_favourite != old_is_favourite)
1591         {
1592           this.notify_property ("is-favourite");
1593         }
1594       if (in_google_personal_group != this._in_google_personal_group)
1595         {
1596           this._in_google_personal_group = in_google_personal_group;
1597           this.notify_property ("in-google-personal-group");
1598         }
1599
1600       this.thaw_notify ();
1601    }
1602
1603   /**
1604    * build a table of im protocols / im protocol aliases
1605    */
1606   internal static HashTable<string, E.ContactField> _get_im_eds_map ()
1607     {
1608       HashTable<string, E.ContactField> retval;
1609
1610       lock (Edsf.Persona._im_eds_map)
1611         {
1612           if (Edsf.Persona._im_eds_map == null)
1613             {
1614               var table =
1615                   new HashTable<string, E.ContactField> (str_hash, str_equal);
1616
1617               table.insert ("aim", ContactField.IM_AIM);
1618               table.insert ("yahoo", ContactField.IM_YAHOO);
1619               table.insert ("groupwise", ContactField.IM_GROUPWISE);
1620               table.insert ("jabber", ContactField.IM_JABBER);
1621               table.insert ("msn", ContactField.IM_MSN);
1622               table.insert ("icq", ContactField.IM_ICQ);
1623               table.insert ("gadugadu", ContactField.IM_GADUGADU);
1624               table.insert ("skype", ContactField.IM_SKYPE);
1625
1626               Edsf.Persona._im_eds_map = table;
1627             }
1628
1629           retval = (!) Edsf.Persona._im_eds_map;
1630         }
1631
1632       return retval;
1633     }
1634
1635   private void _update_phones ()
1636     {
1637       var new_phone_numbers = new HashSet<PhoneFieldDetails> (
1638           (GLib.HashFunc) PhoneFieldDetails.hash,
1639           (GLib.EqualFunc) PhoneFieldDetails.equal);
1640
1641       var attrs = this.contact.get_attributes (E.ContactField.TEL);
1642       foreach (var attr in attrs)
1643         {
1644           var val = attr.get_value ();
1645           if (val == null || (!) val == "")
1646             {
1647               continue;
1648             }
1649
1650           var phone_fd = new PhoneFieldDetails ((!) val);
1651           this._update_params (phone_fd, attr);
1652           new_phone_numbers.add (phone_fd);
1653         }
1654
1655       if (!Folks.Internal.equal_sets<PhoneFieldDetails>  (new_phone_numbers,
1656               this._phone_numbers))
1657         {
1658           this._phone_numbers = new_phone_numbers;
1659           this._phone_numbers_ro = new_phone_numbers.read_only_view;
1660           this.notify_property ("phone-numbers");
1661         }
1662    }
1663
1664   private PostalAddress _postal_address_from_attribute (E.VCardAttribute attr)
1665     {
1666       unowned GLib.List<string>? values = attr.get_values();
1667       unowned GLib.List<string>? l = values;
1668
1669       var address_format = "";
1670       var po_box = "";
1671       var extension = "";
1672       var street = "";
1673       var locality = "";
1674       var region = "";
1675       var postal_code = "";
1676       var country = "";
1677
1678       if (l != null)
1679         {
1680           po_box = ((!) l).data;
1681           l = ((!) l).next;
1682         }
1683       if (l != null)
1684         {
1685           extension = ((!) l).data;
1686           l = ((!) l).next;
1687         }
1688       if (l != null)
1689         {
1690           street = ((!) l).data;
1691           l = ((!) l).next;
1692         }
1693       if (l != null)
1694         {
1695           locality = ((!) l).data;
1696           l = ((!) l).next;
1697         }
1698       if (l != null)
1699         {
1700           region = ((!) l).data;
1701           l = ((!) l).next;
1702         }
1703       if (l != null)
1704         {
1705           postal_code = ((!) l).data;
1706           l = ((!) l).next;
1707         }
1708       if (l != null)
1709         {
1710           country = ((!) l).data;
1711           l = ((!) l).next;
1712         }
1713
1714       return new PostalAddress (po_box, extension, street,
1715                                 locality, region, postal_code, country,
1716                                 address_format, null);
1717     }
1718
1719   /*
1720    * TODO: we should check if addresses corresponding to different types
1721    *       are the same and if so instantiate only one PostalAddress
1722    *       (with the given types).
1723    */
1724   private void _update_addresses ()
1725     {
1726       var new_postal_addresses = new HashSet<PostalAddressFieldDetails> (
1727           (GLib.HashFunc) PhoneFieldDetails.hash,
1728           (GLib.EqualFunc) PhoneFieldDetails.equal);
1729
1730       var attrs = this.contact.get_attributes (E.ContactField.ADDRESS);
1731       foreach (unowned E.VCardAttribute attr in attrs)
1732         {
1733           var address = this._postal_address_from_attribute (attr);
1734           if (address.is_empty ())
1735             {
1736               continue;
1737             }
1738
1739           var pa_fd = new PostalAddressFieldDetails (address);
1740           this._update_params (pa_fd, attr);
1741           new_postal_addresses.add (pa_fd);
1742         }
1743
1744       if (!Folks.Internal.equal_sets<PostalAddressFieldDetails> (
1745               new_postal_addresses,
1746               this._postal_addresses))
1747         {
1748           this._postal_addresses = new_postal_addresses;
1749           this._postal_addresses_ro = new_postal_addresses.read_only_view;
1750           this.notify_property ("phone-numbers");
1751         }
1752
1753       this.notify_property ("postal-addresses");
1754     }
1755
1756   private void _update_local_ids ()
1757     {
1758       var new_local_ids = new HashSet<string> ();
1759
1760       var ids = this.contact.get_attribute ("X-FOLKS-CONTACTS-IDS");
1761       if (ids != null)
1762         {
1763           unowned GLib.List<string> ids_v = ((!) ids).get_values ();
1764
1765           foreach (var local_id in ids_v)
1766             {
1767               if (local_id != "")
1768                 {
1769                   new_local_ids.add (local_id);
1770                 }
1771             }
1772         }
1773
1774       /* Make sure it includes our local id */
1775       new_local_ids.add (this.iid);
1776
1777       if (!Folks.Internal.equal_sets<string> (new_local_ids, this.local_ids))
1778         {
1779           this._local_ids = new_local_ids;
1780           this._local_ids_ro = this._local_ids.read_only_view;
1781           this.notify_property ("local-ids");
1782         }
1783     }
1784
1785   private void _update_favourite ()
1786     {
1787       bool is_fav = false;
1788
1789       var fav = this.contact.get_attribute ("X-FOLKS-FAVOURITE");
1790       if (fav != null)
1791         {
1792           var val = ((!) fav).get_value ();
1793           if (val != null && ((!) val).down () == "true")
1794             {
1795               is_fav = true;
1796             }
1797         }
1798
1799       if (is_fav != this._is_favourite)
1800         {
1801           this._is_favourite = is_fav;
1802           this.notify_property ("is-favourite");
1803         }
1804     }
1805
1806   private void _update_anti_links ()
1807     {
1808       var new_anti_links = new HashSet<string> ();
1809
1810       var vcard = (E.VCard) this.contact;
1811       foreach (unowned E.VCardAttribute attr in vcard.get_attributes ())
1812         {
1813           if (attr.get_name () != Edsf.PersonaStore.anti_links_attribute_name)
1814             {
1815               continue;
1816             }
1817
1818           var val = attr.get_value ();
1819           if (val == null || (!) val == "")
1820              {
1821               continue;
1822             }
1823
1824           new_anti_links.add ((!) val);
1825         }
1826
1827       if (!Folks.Internal.equal_sets<string> (new_anti_links, this._anti_links))
1828         {
1829           this._anti_links = new_anti_links;
1830           this._anti_links_ro = new_anti_links.read_only_view;
1831           this.notify_property ("anti-links");
1832         }
1833     }
1834
1835   internal static T? _get_property_from_contact<T> (E.Contact contact,
1836       string prop_name)
1837     {
1838       T? prop_value = null;
1839       prop_value = contact.get<T> (E.Contact.field_id (prop_name));
1840       return prop_value;
1841     }
1842
1843   private T? _get_property<T> (string prop_name)
1844     {
1845       return Edsf.Persona._get_property_from_contact<T> (this.contact,
1846           prop_name);
1847     }
1848
1849   private string? _im_proto_from_addr (string addr)
1850     {
1851       if (addr.index_of ("@") == -1)
1852         return null;
1853
1854       var tokens = addr.split ("@", 2);
1855
1856       if (tokens.length != 2)
1857         return null;
1858
1859       var domain = tokens[1];
1860       if (domain.index_of (".") == -1)
1861         return null;
1862
1863       tokens = domain.split (".", 2);
1864
1865       if (tokens.length != 2)
1866         return null;
1867
1868       domain = tokens[0];
1869
1870       if (domain == "msn" ||
1871           domain == "hotmail" ||
1872           domain == "live")
1873         return "msn";
1874       else if (domain == "gmail" ||
1875           domain == "googlemail")
1876         return "jabber";
1877       else if (domain == "yahoo")
1878         return "yahoo";
1879
1880       return null;
1881     }
1882 }