source: tags/1.1.0/UContact.pas

Last change on this file was 66, checked in by chronos, 2 years ago
  • Fixed: Missing Version field definition causing error in contacts generation form.
  • Fixed: Default profile image location detection under Linux.
  • Fixed: Languages were not loaded in snap and deb Linux packages.
File size: 35.7 KB
Line 
1unit UContact;
2
3{$mode delphi}{$H+}
4
5interface
6
7uses
8 Classes, SysUtils, fgl, Dialogs, UDataFile, LazUTF8, Base64;
9
10type
11 TContactsFile = class;
12
13 TErrorEvent = procedure (Text: string; Line: Integer) of object;
14
15 TDataType = (dtNone, dtString, dtInteger, dtDate, dtDateTime, dtImage, dtStringList);
16
17 TContactFieldIndex = (cfFirstName, cfMiddleName, cfLastName, cfTitleBefore,
18 cfTitleAfter, cfFullName,
19 cfTel, cfTelCell, cfTelFax, cfTelPager, cfTelHome2, cfTelVoip, cfTelMain,
20 cfTelHome, cfTelCellHome, cfTelFaxHome, cfTelPagerHome,
21 cfTelWork, cfTelCellWork, cfTelFaxWork, cfTelPagerWork,
22 cfEmail, cfUid, cfUrl, cfUrlHome, cfUrlWork,
23 cfEmailHome, cfEmailWork, cfEmailInternet, cfNickName, cfNote, cfRole, cfTitle,
24 cfCategories, cfOrganization, cfDepartment,
25 cfHomeAddressStreet, cfHomeAddressStreetExtended, cfHomeAddressCity, cfHomeAddressCountry,
26 cfHomeAddressPostalCode, cfHomeAddressRegion, cfHomeAddressPostOfficeBox,
27 cfWorkAddressStreet, cfWorkAddressStreetExtended, cfWorkAddressCity, cfWorkAddressCountry,
28 cfWorkAddressPostalCode, cfWorkAddressRegion, cfWorkAddressPostOfficeBox,
29 cfXTimesContacted, cfXLastTimeContacted, cfPhoto, cfDayOfBirth, cfRevision,
30 cfVersion, cfAnniversary, cfGender,
31 cfJabber, cfIcq, cfWindowsLive, cfGoogleTalk, cfAim, cfQq, cfYahoo, cfIrc,
32 cfSkype, cfMsn,
33 cfTwitter, cfFacebook, cfInstagram, cfSnapchat, cfMatrix, cfYoutube,
34 cfPeerTube, cfLinkedIn, cfMastodon, cfMySpace, cfReddit);
35
36 TContactFields = class;
37
38 { TContactField }
39
40 TContactField = class
41 SysName: string;
42 Groups: TStringArray;
43 NoGroups: TStringArray;
44 Title: string;
45 Index: TContactFieldIndex;
46 ValueIndex: Integer;
47 DataType: TDataType;
48 Alternatives: TContactFields;
49 function AddAlternative(Name: string; Groups: array of string; NoGroups:
50 array of string): TContactField;
51 function GroupsContain(Name: string): Boolean;
52 function Match(ASysName: string; AGroups: TStringArray): Boolean;
53 constructor Create;
54 destructor Destroy; override;
55 end;
56
57 { TContactFields }
58
59 TContactFields = class(TFPGObjectList<TContactField>)
60 function AddNew(Name: string; Groups: array of string; NoGroups: array of string;
61 Title: string; Index: TContactFieldIndex; DataType:
62 TDataType = dtNone; ValueIndex: Integer = -1): TContactField;
63 function GetBySysName(SysName: string): TContactField;
64 function GetBySysNameGroups(SysName: string; Groups: TStringArray): TContactField;
65 function GetByIndex(Index: TContactFieldIndex): TContactField;
66 procedure LoadToStrings(AItems: TStrings);
67 end;
68
69 { TContactProperty }
70
71 TContactProperty = class
72 private
73 function GetValueItem(Index: Integer): string;
74 procedure SetValueItem(Index: Integer; AValue: string);
75 public
76 Name: string;
77 Attributes: TStringList;
78 Value: string;
79 Encoding: string;
80 Charset: string;
81 procedure EvaluateAttributes;
82 function GetDecodedValue: string;
83 function GetEncodedValue: string;
84 function MatchNameGroups(AName: string; Groups: TStringArray;
85 NoGroups: TStringArray): Boolean;
86 procedure Assign(Source: TContactProperty);
87 constructor Create;
88 destructor Destroy; override;
89 property ValueItem[Index: Integer]: string read GetValueItem write SetValueItem;
90 end;
91
92 { TContactProperties }
93
94 TContactProperties = class(TFPGObjectList<TContactProperty>)
95 procedure AssignToList(List: TFPGObjectList<TObject>);
96 function GetByName(Name: string): TContactProperty;
97 function GetByNameGroups(Name: string; Groups: TStringArray;
98 NoGroups: TStringArray): TContactProperty;
99 function GetByNameGroupsMultiple(Name: string; Groups: TStringArray;
100 NoGroups: TStringArray): TContactProperties;
101 end;
102
103 { TContact }
104
105 TContact = class
106 private
107 function GetField(Index: TContactFieldIndex): string;
108 procedure SetField(Index: TContactFieldIndex; AValue: string);
109 public
110 Properties: TContactProperties;
111 Parent: TContactsFile;
112 function FullNameToFileName: string;
113 function GetProperty(Field: TContactField): TContactProperty; overload;
114 function GetProperty(FieldIndex: TContactFieldIndex): TContactProperty; overload;
115 procedure Assign(Source: TContact);
116 function UpdateFrom(Source: TContact): Boolean;
117 constructor Create;
118 destructor Destroy; override;
119 procedure SaveToStrings(Output: TStrings);
120 function LoadFromStrings(Lines: TStrings; StartLine: Integer = 0): Integer;
121 procedure SaveToFile(FileName: string);
122 procedure LoadFromFile(FileName: string);
123 property Fields[Index: TContactFieldIndex]: string read GetField write SetField;
124 end;
125
126 { TContacts }
127
128 TContacts = class(TFPGObjectList<TContact>)
129 ContactsFile: TContactsFile;
130 procedure AssignToList(List: TFPGObjectList<TObject>);
131 function AddNew: TContact;
132 function Search(FullName: string): TContact;
133 function ToString: ansistring; override;
134 end;
135
136 { TContactsFile }
137
138 TContactsFile = class(TDataFile)
139 private
140 FOnError: TErrorEvent;
141 procedure InitFields;
142 procedure Error(Text: string; Line: Integer);
143 function NewItem(Key, Value: string): string;
144 public
145 Fields: TContactFields;
146 Contacts: TContacts;
147 function GetFileName: string; override;
148 function GetFileExt: string; override;
149 function GetFileFilter: string; override;
150 procedure SaveToFile(FileName: string); override;
151 procedure LoadFromFile(FileName: string); override;
152 constructor Create; override;
153 destructor Destroy; override;
154 property OnError: TErrorEvent read FOnError write FOnError;
155 end;
156
157const
158 VCardFileExt = '.vcf';
159
160
161implementation
162
163uses
164 UQuotedPrintable;
165
166const
167 VCardBegin = 'BEGIN:VCARD';
168 VCardEnd = 'END:VCARD';
169
170resourcestring
171 SVCardFile = 'vCard file';
172 SExpectedVCardBegin = 'Expected vCard begin';
173 SFieldIndexNotDefined = 'Field index not defined';
174 SContactHasNoParent = 'Contact has no parent';
175 SExpectedProperty = 'Expected contact property';
176 SVersion = 'Version';
177 SLastName = 'Last Name';
178 SFirstName = 'First Name';
179 SMiddleName = 'Middle Name';
180 STitleBefore = 'Title Before';
181 STitleAfter = 'Title After';
182 SFullName = 'Full Name';
183 STelephone = 'Telephone';
184 SMobilePhone = 'Mobile phone';
185 SPager = 'Pager';
186 SFax = 'Fax';
187 SHomePhone = 'Home phone';
188 SHomeMobile = 'Home mobile';
189 SHomeFax = 'Home fax';
190 SHomePager = 'Home pager';
191 SWorkPhone = 'Work phone';
192 SWorkFax = 'Work fax';
193 SWorkPager = 'Work pager';
194 SWorkMobile = 'Work mobile';
195 SHomePhone2 = 'Home phone 2';
196 SVoipPhone = 'VoIP phone';
197 SMainPhone = 'Main phone';
198 SEmail = 'E-mail';
199 SHomeEmail = 'Home E-mail';
200 SWorkEmail = 'Work E-mail';
201 SInternetEmail = 'Internet E-mail';
202 SNickName = 'Nick name';
203 SNote = 'Note';
204 SRole = 'Role';
205 STitle = 'Title';
206 SCategories = 'Categories';
207 SOrganization = 'Organization';
208 SDepartement = 'Departement';
209 SHomeAddressPostOfficeBox = 'Home address post office box';
210 SHomeAddressStreetExtended = 'Home address extended street';
211 SHomeAddressStreet = 'Home address street';
212 SHomeAddressCity = 'Home address city';
213 SHomeAddressRegion = 'Home address region';
214 SHomeAddressPostalCode = 'Home address postal code';
215 SHomeAddressCountry = 'Home address country';
216 SWorkAddressPostOfficeBox = 'Work address post office box';
217 SWorkAddressStreetExtended = 'Work address extended street';
218 SWorkAddressStreet = 'Work address street';
219 SWorkAddressCity = 'Work address city';
220 SWorkAddressRegion = 'Work address region';
221 SWorkAddressPostalCode = 'Work address postal code';
222 SWorkAddressCountry = 'Work address country';
223 STimesContacted = 'Times Contacted';
224 SLastTimeContacted = 'Last Time Contacted';
225 SPhoto = 'Photo';
226 SJabber = 'Jabber';
227 SDayOfBirth = 'Day of birth';
228 SAnniversary = 'Anniversary';
229 SRevision = 'Revision';
230 SUniqueIdentifier = 'Unique identifier';
231 SWebAddress = 'Web address';
232 SWebAddressHome = 'Web address home';
233 SWebAddressWork = 'Web address work';
234 SGender = 'Gender';
235 // Chat
236 SMsn = 'MSN';
237 SGoogleTalk = 'Google Talk';
238 SWindowsLive = 'Windows Live';
239 SAim = 'AIM';
240 SQq = 'QQ';
241 SIrc = 'IRC';
242 SIcq = 'ICQ';
243 SYahoo = 'Yahoo!';
244 SSkype = 'Skype';
245 SMatrix = 'Matrix';
246 // Social
247 STwitter = 'Twitter';
248 SFacebook = 'Facebook';
249 SInstagram = 'Instagram';
250 SMastodon = 'Mastodon';
251 SSnapchat = 'Snapchat';
252 SLinkedIn = 'LinkedIn';
253 SYouTube = 'YouTube';
254 SPeerTube = 'PeerTube';
255 SReddit = 'Reddit';
256 SMySpace = 'MySpace';
257
258function GetNext(var Text: string; Separator: string): string;
259begin
260 if Pos(Separator, Text) > 0 then begin
261 Result := Copy(Text, 1, Pos(Separator, Text) - 1);
262 Delete(Text, 1, Length(Result) + Length(Separator));
263 end else begin
264 Result := Text;
265 Text := '';
266 end;
267end;
268
269function IsAsciiString(Text: string): Boolean;
270var
271 I: Integer;
272begin
273 Result := True;
274 for I := 1 to Length(Text) do
275 if Ord(Text[I]) > 128 then begin
276 Result := False;
277 Break;
278 end;
279end;
280
281function EndsWith(Text, What: string): Boolean;
282begin
283 Result := Copy(Text, Length(Text) - Length(What) + 1, MaxInt) = What;
284end;
285
286function EncodeEscaped(Text: string): string;
287var
288 I: Integer;
289 O: Integer;
290begin
291 Result := '';
292 I := 1;
293 O := 1;
294 SetLength(Result, Length(Text)); // Preallocate string
295 while I <= Length(Text) do begin
296 if Text[I] in [',', '\', ';'] then begin
297 Result[O] := '\';
298 Inc(O);
299 Result[O] := Text[I];
300 SetLength(Result, Length(Result) + 1);
301 Inc(O);
302 end else begin
303 Result[O] := Text[I];
304 Inc(O);
305 end;
306 Inc(I);
307 end;
308 SetLength(Result, O - 1);
309end;
310
311function DecodeEscaped(Text: string): string;
312var
313 I: Integer;
314 O: Integer;
315 Escaped: Boolean;
316begin
317 Result := '';
318 I := 1;
319 O := 1;
320 Escaped := False;
321 SetLength(Result, Length(Text)); // Preallocate string
322 while I <= Length(Text) do begin
323 if Escaped then begin
324 Result[O] := Text[I];
325 Inc(O);
326 Escaped := False;
327 end else begin
328 if Text[I] = '\' then begin
329 Escaped := True;
330 end else begin
331 Result[O] := Text[I];
332 Inc(O);
333 end;
334 end;
335 Inc(I);
336 end;
337 SetLength(Result, O - 1);
338end;
339
340{ TContactField }
341
342function TContactField.AddAlternative(Name: string; Groups: array of string;
343 NoGroups: array of string): TContactField;
344begin
345 Result := Alternatives.AddNew(Name, Groups, NoGroups, Title, Index, DataType, ValueIndex);
346end;
347
348function TContactField.GroupsContain(Name: string): Boolean;
349var
350 I: Integer;
351begin
352 Result := False;
353 for I := 0 to Length(Groups) - 1 do
354 if Groups[I] = Name then begin
355 Result := True;
356 Break;
357 end;
358end;
359
360function TContactField.Match(ASysName: string; AGroups: TStringArray): Boolean;
361var
362 I: Integer;
363begin
364 Result := ASysName = SysName;
365 if Result then begin
366 for I := 0 to Length(AGroups) - 1 do begin
367 if not GroupsContain(AGroups[I]) then begin
368 Result := False;
369 Break;
370 end;
371 end;
372 end;
373end;
374
375constructor TContactField.Create;
376begin
377 Alternatives := TContactFields.Create;
378end;
379
380destructor TContactField.Destroy;
381begin
382 FreeAndNil(Alternatives);
383 inherited;
384end;
385
386{ TContactProperties }
387
388procedure TContactProperties.AssignToList(List: TFPGObjectList<TObject>);
389var
390 I: Integer;
391begin
392 while List.Count > Count do List.Delete(List.Count - 1);
393 while List.Count < Count do List.Add(nil);
394 for I := 0 to Count - 1 do
395 List[I] := Items[I];
396end;
397
398function TContactProperties.GetByName(Name: string): TContactProperty;
399var
400 I: Integer;
401begin
402 I := 0;
403 while (I < Count) and (Items[I].Name <> Name) and (not EndsWith(Items[I].Name, '.' + Name)) do Inc(I);
404 if I < Count then Result := Items[I]
405 else Result := nil;
406end;
407
408function TContactProperties.GetByNameGroups(Name: string; Groups: TStringArray;
409 NoGroups: TStringArray): TContactProperty;
410var
411 I: Integer;
412begin
413 I := 0;
414 while (I < Count) and not Items[I].MatchNameGroups(Name, Groups, NoGroups) do Inc(I);
415 if I < Count then Result := Items[I]
416 else Result := nil;
417end;
418
419function TContactProperties.GetByNameGroupsMultiple(Name: string;
420 Groups: TStringArray; NoGroups: TStringArray): TContactProperties;
421var
422 I: Integer;
423begin
424 Result := TContactProperties.Create(False);
425 for I := 0 to Count - 1 do
426 if Items[I].MatchNameGroups(Name, Groups, NoGroups) then
427 Result.Add(Items[I]);
428end;
429
430{ TContactProperty }
431
432function TContactProperty.GetValueItem(Index: Integer): string;
433var
434 List: TStringList;
435begin
436 List := TStringList.Create;
437 try
438 List.Delimiter := ';';
439 List.NameValueSeparator := '=';
440 List.StrictDelimiter := True;
441 List.DelimitedText := Value;
442 if Index < List.Count then
443 Result := List.Strings[Index]
444 else Result := '';
445 finally
446 List.Free;
447 end;
448end;
449
450procedure TContactProperty.SetValueItem(Index: Integer; AValue: string);
451var
452 List: TStringList;
453begin
454 List := TStringList.Create;
455 try
456 List.Delimiter := ';';
457 List.NameValueSeparator := '=';
458 List.StrictDelimiter := True;
459 List.DelimitedText := Value;
460
461 // Extend subitems count
462 while List.Count <= Index do
463 List.Add('');
464
465 List.Strings[Index] := AValue;
466
467 // Remove empty items
468 while (List.Count > 0) and (List.Strings[List.Count - 1] = '') do
469 List.Delete(List.Count - 1);
470
471 Value := List.DelimitedText;
472 finally
473 List.Free;
474 end;
475end;
476
477procedure TContactProperty.EvaluateAttributes;
478var
479 I: Integer;
480begin
481 if Attributes.IndexOf('BASE64') <> -1 then
482 Encoding := 'BASE64'
483 else
484 if Attributes.IndexOfName('ENCODING') <> -1 then begin
485 Encoding := Attributes.Values['ENCODING'];
486 if (Encoding = 'B') or (Encoding = 'b') then Encoding := 'BASE64';
487 if (Encoding = 'Q') or (Encoding = 'q') then Encoding := 'QUOTED-PRINTABLE';
488 if (Encoding = 'QUOTED-PRINTABLE') or (Encoding = 'BASE64') then begin
489 Value := GetDecodedValue;
490 Attributes.Delete(Attributes.IndexOfName('ENCODING'));
491 end else
492 end else Encoding := '';
493
494 if Attributes.IndexOfName('CHARSET') <> -1 then
495 Charset := Attributes.Values['CHARSET']
496 else Charset := '';
497
498 // Simplify TYPE attribute from TYPE=VALUE into VALUE
499 for I := 0 to Attributes.Count - 1 do begin
500 if Attributes.Names[I] = 'TYPE' then
501 Attributes.Strings[I] := Attributes.Values['TYPE'];
502 if Attributes.Names[I] = 'type' then
503 Attributes.Strings[I] := Attributes.Values['type'];
504 end;
505end;
506
507function TContactProperty.GetDecodedValue: string;
508begin
509 if Encoding = 'BASE64' then begin
510 Result := DecodeStringBase64(Value);
511 end else
512 if Encoding = 'QUOTED-PRINTABLE' then begin
513 Result := DecodeQuotedPrintable(Value);
514 end
515 else Result := '';
516end;
517
518function TContactProperty.GetEncodedValue: string;
519begin
520 if Encoding = 'BASE64' then begin
521 Result := EncodeStringBase64(Value);
522 end else
523 if Encoding = 'QUOTED-PRINTABLE' then begin
524 Result := EncodeQuotedPrintable(Value);
525 end
526 else Result := '';
527end;
528
529function TContactProperty.MatchNameGroups(AName: string; Groups: TStringArray;
530 NoGroups: TStringArray): Boolean;
531var
532 I: Integer;
533begin
534 Result := (Name = AName) or EndsWith(Name, '.' + AName);
535 if Result and (Length(Groups) > 0) then begin
536 for I := 0 to Length(Groups) - 1 do
537 if (Attributes.IndexOf(Groups[I]) = -1) and
538 (Attributes.IndexOf('TYPE=' + Groups[I]) = -1) then begin
539 Result := False;
540 Break;
541 end;
542 end;
543 if Result and (Length(NoGroups) > 0) then begin
544 for I := 0 to Length(NoGroups) - 1 do
545 if (Attributes.IndexOf(NoGroups[I]) <> -1) or
546 (Attributes.IndexOf('TYPE=' + NoGroups[I]) <> -1) then begin
547 Result := False;
548 Break;
549 end;
550 end;
551end;
552
553procedure TContactProperty.Assign(Source: TContactProperty);
554begin
555 Name := Source.Name;
556 Attributes.Assign(Source.Attributes);
557 Value := Source.Value;
558 Encoding := Source.Encoding;
559 Charset := Source.Charset;
560end;
561
562constructor TContactProperty.Create;
563begin
564 Attributes := TStringList.Create;
565 Attributes.Delimiter := ';';
566 Attributes.NameValueSeparator := '=';
567 Attributes.StrictDelimiter := True;
568end;
569
570destructor TContactProperty.Destroy;
571begin
572 FreeAndNil(Attributes);
573 inherited;
574end;
575
576{ TContacts }
577
578procedure TContacts.AssignToList(List: TFPGObjectList<TObject>);
579var
580 I: Integer;
581begin
582 while List.Count > Count do List.Delete(List.Count - 1);
583 while List.Count < Count do List.Add(nil);
584 for I := 0 to Count - 1 do
585 List[I] := Items[I];
586end;
587
588function TContacts.AddNew: TContact;
589begin
590 Result := TContact.Create;
591 Result.Parent := ContactsFile;
592 Add(Result);
593end;
594
595function TContacts.Search(FullName: string): TContact;
596var
597 Contact: TContact;
598begin
599 Result := nil;
600 for Contact in Self do
601 if Contact.Fields[cfFullName] = FullName then begin
602 Result := Contact;
603 Break;
604 end;
605end;
606
607function TContacts.ToString: ansistring;
608var
609 I: Integer;
610begin
611 Result := '';
612 for I := 0 to Count - 1 do begin
613 if I > 0 then Result := Result + ', ';
614 Result := Result + Items[I].Fields[cfFullName];
615 end;
616end;
617
618{ TContactFields }
619
620function TContactFields.AddNew(Name: string; Groups: array of string;
621 NoGroups: array of string; Title: string; Index: TContactFieldIndex;
622 DataType: TDataType = dtNone; ValueIndex: Integer = -1): TContactField;
623var
624 I: Integer;
625begin
626 Result := TContactField.Create;
627 Result.SysName := Name;
628 SetLength(Result.Groups, Length(Groups));
629 for I := 0 to Length(Groups) - 1 do
630 Result.Groups[I] := Groups[I];
631 SetLength(Result.NoGroups, Length(NoGroups));
632 for I := 0 to Length(NoGroups) - 1 do
633 Result.NoGroups[I] := NoGroups[I];
634 Result.Title := Title;
635 Result.Index := Index;
636 Result.ValueIndex := ValueIndex;
637 Result.DataType := DataType;
638 Add(Result);
639end;
640
641function TContactFields.GetBySysName(SysName: string): TContactField;
642var
643 I: Integer;
644begin
645 I := 0;
646 while (I < Count) and (Items[I].SysName <> SysName) do Inc(I);
647 if I < Count then Result := Items[I]
648 else Result := nil;
649end;
650
651function TContactFields.GetBySysNameGroups(SysName: string; Groups: TStringArray
652 ): TContactField;
653var
654 I: Integer;
655begin
656 I := 0;
657 while (I < Count) and not Items[I].Match(SysName, Groups) do Inc(I);
658 if I < Count then Result := Items[I]
659 else Result := nil;
660end;
661
662function TContactFields.GetByIndex(Index: TContactFieldIndex): TContactField;
663var
664 I: Integer;
665begin
666 I := 0;
667 while (I < Count) and (Items[I].Index <> Index) do Inc(I);
668 if I < Count then Result := Items[I]
669 else Result := nil;
670end;
671
672procedure TContactFields.LoadToStrings(AItems: TStrings);
673var
674 I: Integer;
675begin
676 while AItems.Count < Count do AItems.Add('');
677 while AItems.Count > Count do AItems.Delete(AItems.Count - 1);
678 for I := 0 to Count - 1 do
679 AItems[I] := Items[I].Title;
680end;
681
682{ TContact }
683
684function TContact.GetField(Index: TContactFieldIndex): string;
685var
686 Prop: TContactProperty;
687 Field: TContactField;
688begin
689 if not Assigned(Parent) then raise Exception.Create(SContactHasNoParent);
690 Field := Parent.Fields.GetByIndex(Index);
691 if Assigned(Field) then begin
692 Prop := GetProperty(Field);
693 if Assigned(Prop) then begin
694 Field := Parent.Fields.GetByIndex(Index);
695 if Field.ValueIndex <> -1 then begin
696 Result := DecodeEscaped(Prop.ValueItem[Field.ValueIndex])
697 end else Result := Prop.Value;
698 end else Result := '';
699 end else raise Exception.Create(SFieldIndexNotDefined);
700end;
701
702procedure TContact.SetField(Index: TContactFieldIndex; AValue: string);
703var
704 Prop: TContactProperty;
705 Field: TContactField;
706 I: Integer;
707begin
708 if not Assigned(Parent) then raise Exception.Create(SContactHasNoParent);
709 Field := Parent.Fields.GetByIndex(Index);
710 if Assigned(Field) then begin
711 Prop := GetProperty(Field);
712 if (not Assigned(Prop)) and (AValue <> '') then begin
713 Prop := TContactProperty.Create;
714 Prop.Name := Field.SysName;
715 for I := 0 to Length(Field.Groups) - 1 do
716 Prop.Attributes.Add(Field.Groups[I]);
717 Properties.Add(Prop);
718 end;
719 if Assigned(Prop) then begin
720 if Field.ValueIndex <> -1 then begin
721 Prop.ValueItem[Field.ValueIndex] := EncodeEscaped(AValue);
722 end else Prop.Value := AValue;
723
724 // Remove if empty
725 if Prop.Value = '' then begin
726 Properties.Remove(Prop);
727 end;
728 end;
729 end else raise Exception.Create(SFieldIndexNotDefined);
730end;
731
732function TContact.FullNameToFileName: string;
733var
734 I: Integer;
735begin
736 Result := Fields[cfFullName];
737 for I := 1 to Length(Result) do begin
738 if Result[I] in [':', '/', '\', '.', '"', '*', '|', '?', '<', '>'] then
739 Result[I] := '_';
740 end;
741end;
742
743function TContact.GetProperty(Field: TContactField): TContactProperty;
744var
745 I: Integer;
746begin
747 Result := Properties.GetByNameGroups(Field.SysName, Field.Groups, Field.NoGroups);
748 I := 0;
749 while (not Assigned(Result)) and (I < Field.Alternatives.Count) do begin
750 Result := Properties.GetByNameGroups(Field.Alternatives[I].SysName,
751 Field.Alternatives[I].Groups, Field.Alternatives[I].NoGroups);
752 if Assigned(Result) then Break;
753 Inc(I);
754 end;
755end;
756
757function TContact.GetProperty(FieldIndex: TContactFieldIndex): TContactProperty;
758var
759 Field: TContactField;
760begin
761 if not Assigned(Parent) then raise Exception.Create(SContactHasNoParent);
762 Field := Parent.Fields.GetByIndex(FieldIndex);
763 if Assigned(Field) then begin
764 Result := GetProperty(Field);
765 end else Result := nil;
766end;
767
768procedure TContact.Assign(Source: TContact);
769var
770 I: Integer;
771begin
772 while Properties.Count < Source.Properties.Count do
773 Properties.Add(TContactProperty.Create);
774 while Properties.Count > Source.Properties.Count do
775 Properties.Delete(Properties.Count - 1);
776 for I := 0 to Properties.Count - 1 do
777 Properties[I].Assign(Source.Properties[I]);
778end;
779
780function TContact.UpdateFrom(Source: TContact): Boolean;
781var
782 I: Integer;
783begin
784 if not Assigned(Parent) then raise Exception.Create(SContactHasNoParent);
785 Result := False;
786 for I := 0 to Parent.Fields.Count - 1 do begin
787 if (Source.Fields[Parent.Fields[I].Index] <> '') and
788 (Source.Fields[Parent.Fields[I].Index] <>
789 Fields[Parent.Fields[I].Index]) then begin
790 Result := True;
791 Fields[Parent.Fields[I].Index] := Source.Fields[Parent.Fields[I].Index];
792 end;
793 end;
794end;
795
796constructor TContact.Create;
797begin
798 Properties := TContactProperties.Create;
799end;
800
801destructor TContact.Destroy;
802begin
803 FreeAndNil(Properties);
804 inherited;
805end;
806
807procedure TContact.SaveToStrings(Output: TStrings);
808var
809 I: Integer;
810 NameText: string;
811 Value2: string;
812 LineIndex: Integer;
813 OutText: string;
814 LinePrefix: string;
815 CutLength: Integer;
816const
817 MaxLineLength = 73;
818begin
819 with Output do begin
820 Add(VCardBegin);
821 for I := 0 to Properties.Count - 1 do
822 with Properties[I] do begin
823 NameText := Name;
824 if Attributes.Count > 0 then
825 NameText := NameText + ';' + Attributes.DelimitedText;
826 if Encoding <> '' then begin
827 Value2 := GetEncodedValue;
828 NameText := NameText + ';ENCODING=' + Encoding;
829 end else Value2 := Value;
830 if Pos(LineEnding, Value2) > 0 then begin
831 Add(NameText + ':' + GetNext(Value2, LineEnding));
832 while Pos(LineEnding, Value2) > 0 do begin
833 Add(' ' + GetNext(Value2, LineEnding));
834 end;
835 Add(' ' + GetNext(Value2, LineEnding));
836 Add('');
837 end else begin
838 OutText := NameText + ':' + Value2;
839 LineIndex := 0;
840 LinePrefix := '';
841 while True do begin
842 if Length(OutText) > MaxLineLength then begin
843 CutLength := MaxLineLength;
844 if Encoding = 'QUOTED-PRINTABLE' then begin
845 // Do not cut encoded items
846 if ((CutLength - 2) >= 1) and (OutText[CutLength - 2] = '=') then
847 Dec(CutLength, 2)
848 else if ((CutLength - 1) >= 1) and (OutText[CutLength - 1] = '=') then
849 Dec(CutLength, 1);
850 end;
851 Add(LinePrefix + Copy(OutText, 1, CutLength));
852 LinePrefix := ' ';
853 System.Delete(OutText, 1, CutLength);
854 Inc(LineIndex);
855 Continue;
856 end else begin
857 Add(LinePrefix + OutText);
858 Break;
859 end;
860 end;
861 if LinePrefix <> '' then Add('');
862 end;
863 end;
864 Add(VCardEnd);
865 end;
866end;
867
868function TContact.LoadFromStrings(Lines: TStrings; StartLine: Integer = 0): Integer;
869type
870 TParseState = (psNone, psInside, psFinished);
871var
872 ParseState: TParseState;
873 Line: string;
874 Value: string;
875 I: Integer;
876 NewProperty: TContactProperty;
877 CommandPart: string;
878 Names: string;
879begin
880 ParseState := psNone;
881 I := StartLine;
882 while I < Lines.Count do begin
883 Line := Trim(Lines[I]);
884 if Line = '' then begin
885 // Skip empty lines
886 end else
887 if ParseState = psNone then begin
888 if Line = VCardBegin then begin
889 ParseState := psInside;
890 end else begin
891 Parent.Error(SExpectedVCardBegin, I + 1);
892 I := -1;
893 Break;
894 end;
895 end else
896 if ParseState = psInside then begin
897 if Line = VCardEnd then begin
898 ParseState := psFinished;
899 Inc(I);
900 Break;
901 end else
902 if Pos(':', Line) > 0 then begin
903 CommandPart := GetNext(Line, ':');
904 Names := CommandPart;
905 Value := Line;
906 while True do begin
907 Inc(I);
908 if (Length(Lines[I]) > 0) and (Lines[I][1] = ' ') then begin
909 Value := Value + Trim(Lines[I]);
910 end else
911 if (Length(Lines[I]) > 0) and (Length(Value) > 0) and (Value[Length(Value)] = '=') and
912 (Lines[I][1] = '=') then begin
913 Value := Value + Copy(Trim(Lines[I]), 2, MaxInt);
914 end else begin
915 Dec(I);
916 Break;
917 end;
918 end;
919 NewProperty := Properties.GetByName(Names);
920 if not Assigned(NewProperty) then begin
921 NewProperty := TContactProperty.Create;
922 Properties.Add(NewProperty);
923 end;
924 NewProperty.Attributes.DelimitedText := Names;
925 if NewProperty.Attributes.Count > 0 then begin
926 NewProperty.Name := NewProperty.Attributes[0];
927 NewProperty.Attributes.Delete(0);
928 end;
929 NewProperty.Value := Value;
930 NewProperty.EvaluateAttributes;
931 end else begin
932 Parent.Error(SExpectedProperty, I + 1);
933 I := -1;
934 Break;
935 end;
936 end;
937 Inc(I);
938 end;
939 Result := I;
940end;
941
942procedure TContact.SaveToFile(FileName: string);
943var
944 Lines: TStringList;
945begin
946 Lines := TStringList.Create;
947 try
948 SaveToStrings(Lines);
949 Lines.SaveToFile(FileName);
950 finally
951 Lines.Free;
952 end;
953end;
954
955procedure TContact.LoadFromFile(FileName: string);
956var
957 Lines: TStringList;
958begin
959 Lines := TStringList.Create;
960 try
961 Lines.LoadFromFile(FileName);
962 {$IF FPC_FULLVERSION>=30200}
963 if (Length(Lines.Text) > 0) and (Pos(VCardBegin, Lines.Text) = 0) then begin
964 Lines.LoadFromFile(FileName, TEncoding.Unicode);
965 if (Length(Lines.Text) > 0) and (Pos(VCardBegin, Lines.Text) = 0) then begin
966 Lines.LoadFromFile(FileName, TEncoding.BigEndianUnicode);
967 end;
968 end;
969 {$ENDIF}
970 LoadFromStrings(Lines);
971 finally
972 Lines.Free;
973 end;
974end;
975
976{ TContactsFile }
977
978procedure TContactsFile.InitFields;
979begin
980 with Fields do begin
981 AddNew('VERSION', [], [], SVersion, cfVersion, dtString);
982 AddNew('N', [], [], SLastName, cfLastName, dtString, 0);
983 AddNew('N', [], [], SFirstName, cfFirstName, dtString, 1);
984 AddNew('N', [], [], SMiddleName, cfMiddleName, dtString, 2);
985 AddNew('N', [], [], STitleBefore, cfTitleBefore, dtString, 3);
986 AddNew('N', [], [], STitleAfter, cfTitleAfter, dtString, 4);
987 AddNew('FN', [], [], SFullName, cfFullName, dtString);
988 AddNew('TEL', [], ['CELL', 'FAX', 'PAGER', 'WORK', 'HOME'], STelephone, cfTel, dtString);
989 AddNew('TEL', ['CELL'], ['WORK', 'HOME'], SMobilePhone, cfTelCell, dtString);
990 AddNew('TEL', ['FAX'], ['WORK', 'HOME'], SFax, cfTelFax, dtString);
991 AddNew('TEL', ['PAGER'], ['WORK', 'HOME'], SPager, cfTelPager, dtString);
992 AddNew('TEL', ['HOME'], ['CELL', 'FAX', 'PAGER'], SHomePhone, cfTelHome, dtString);
993 AddNew('TEL', ['HOME', 'CELL'], [], SHomeMobile, cfTelCellHome, dtString);
994 AddNew('TEL', ['HOME', 'FAX'], [], SHomeFax, cfTelFaxHome, dtString);
995 AddNew('TEL', ['HOME', 'PAGER'], [], SHomePager, cfTelPagerHome, dtString);
996 AddNew('TEL', ['WORK'], ['CELL', 'FAX', 'PAGER'], SWorkPhone, cfTelWork, dtString);
997 AddNew('TEL', ['WORK', 'CELL'], [], SWorkMobile, cfTelCellWork, dtString);
998 AddNew('TEL', ['WORK', 'FAX'], [], SWorkFax, cfTelFaxWork, dtString);
999 AddNew('TEL', ['WORK', 'PAGER'], [], SWorkPager, cfTelPagerWork, dtString);
1000 AddNew('TEL', ['HOME2'], [], SHomePhone2, cfTelHome2, dtString);
1001 AddNew('TEL', ['VOIP'], [], SVoipPhone, cfTelVoip, dtString);
1002 AddNew('TEL', ['MAIN'], [], SMainPhone, cfTelMain, dtString);
1003 AddNew('EMAIL', [], ['HOME', 'WORK', 'INTERNET'], SEmail, cfEmail, dtString);
1004 AddNew('EMAIL', ['HOME'], [], SHomeEmail, cfEmailHome, dtString);
1005 AddNew('EMAIL', ['WORK'], [], SWorkEmail, cfEmailWork, dtString);
1006 AddNew('EMAIL', ['INTERNET'], [], SInternetEmail, cfEmailInternet, dtString);
1007 with AddNew('NICKNAME', [], [], SNickName, cfNickName, dtString) do
1008 AddAlternative('X-NICKNAME', [], []);
1009 AddNew('NOTE', [], [], SNote, cfNote, dtString);
1010 AddNew('ROLE', [], [], SRole, cfRole, dtString);
1011 AddNew('TITLE', [], [], STitle, cfTitle, dtString);
1012 AddNew('CATEGORIES', [], [], SCategories, cfCategories, dtString);
1013 AddNew('ORG', [], [], SOrganization, cfOrganization, dtString, 0);
1014 AddNew('ORG', [], [], SDepartement, cfDepartment, dtString, 1);
1015 AddNew('ADR', ['HOME'], [], SHomeAddressPostOfficeBox, cfHomeAddressPostOfficeBox, dtString, 0);
1016 AddNew('ADR', ['HOME'], [], SHomeAddressStreetExtended, cfHomeAddressStreetExtended, dtString, 1);
1017 AddNew('ADR', ['HOME'], [], SHomeAddressStreet, cfHomeAddressStreet, dtString, 2);
1018 AddNew('ADR', ['HOME'], [], SHomeAddressCity, cfHomeAddressCity, dtString, 3);
1019 AddNew('ADR', ['HOME'], [], SHomeAddressRegion, cfHomeAddressRegion, dtString, 4);
1020 AddNew('ADR', ['HOME'], [], SHomeAddressPostalCode, cfHomeAddressPostalCode, dtString, 5);
1021 AddNew('ADR', ['HOME'], [], SHomeAddressCountry, cfHomeAddressCountry, dtString, 6);
1022 AddNew('ADR', ['WORK'], [], SWorkAddressPostOfficeBox, cfWorkAddressPostOfficeBox, dtString, 0);
1023 AddNew('ADR', ['WORK'], [], SWorkAddressStreetExtended, cfWorkAddressStreetExtended, dtString, 1);
1024 AddNew('ADR', ['WORK'], [], SWorkAddressStreet, cfWorkAddressStreet, dtString, 2);
1025 AddNew('ADR', ['WORK'], [], SWorkAddressCity, cfWorkAddressCity, dtString, 3);
1026 AddNew('ADR', ['WORK'], [], SWorkAddressRegion, cfWorkAddressRegion, dtString, 4);
1027 AddNew('ADR', ['WORK'], [], SWorkAddressPostalCode, cfWorkAddressPostalCode, dtString, 5);
1028 AddNew('ADR', ['WORK'], [], SWorkAddressCountry, cfWorkAddressCountry, dtString, 6);
1029 AddNew('X-TIMES_CONTACTED', [], [], STimesContacted, cfXTimesContacted, dtString);
1030 AddNew('X-LAST_TIME_CONTACTED', [], [], SLastTimeContacted, cfXLastTimeContacted, dtString);
1031 AddNew('PHOTO', [], [], SPhoto, cfPhoto, dtImage);
1032 AddNew('BDAY', [], [], SDayOfBirth, cfDayOfBirth, dtDate);
1033 AddNew('ANNIVERSARY', [], [], SAnniversary, cfAnniversary, dtDate);
1034 AddNew('REV', [], [], SRevision, cfRevision, dtString);
1035 AddNew('UID', [], [], SUniqueIdentifier, cfUid, dtString);
1036 AddNew('URL', [], ['HOME', 'WORK'], SWebAddress, cfUrl, dtString);
1037 AddNew('URL', ['HOME'], [], SWebAddressHome, cfUrlHome, dtString);
1038 AddNew('URL', ['WORK'], [], SWebAddressWork, cfUrlWork, dtString);
1039 with AddNew('GENDER', [], [], SGender, cfGender, dtString) do
1040 AddAlternative('X-CENTRUM-CZ-SEX', [], []);
1041 // Chat
1042 AddNew('X-MATRIX', [], [], SMatrix, cfMatrix, dtString);
1043 AddNew('X-JABBER', [], [], SJabber, cfJabber, dtString);
1044 AddNew('X-AIM', [], [], SAim, cfAim, dtString);
1045 AddNew('X-Windows Live', [], [], SWindowsLive, cfWindowsLive, dtString);
1046 AddNew('X-YAHOO', [], [], SYahoo, cfYahoo, dtString);
1047 with AddNew('X-SKYPE-USERNAME', [], [], SSkype, cfSkype, dtString) do begin
1048 AddAlternative('X-SKYPE', [], []);
1049 AddAlternative('X-CENTRUM-CZ-SKYPE', [], []);
1050 end;
1051 AddNew('X-QQ', [], [], SQq, cfQq, dtString);
1052 AddNew('X-GOOGLE-TALK', [], [], SGoogleTalk, cfGoogleTalk, dtString);
1053 with AddNew('X-ICQ', [], [], SIcq, cfIcq, dtString) do
1054 AddAlternative('X-CENTRUM-CZ-ICQ', [], []);
1055 AddNew('X-IRC', [], [], SIrc, cfIrc, dtString);
1056 with AddNew('X-MSN', [], [], SMsn, cfMsn, dtString) do
1057 AddAlternative('X-CENTRUM-CZ-MSN', [], []);
1058 // Social
1059 with AddNew('X-TWITTER', [], [], STwitter, cfTwitter, dtString) do
1060 AddAlternative('X-SOCIALPROFILE', ['TWITTER'], []);
1061 with AddNew('X-FACEBOOK', [], [], SFacebook, cfFacebook, dtString) do
1062 AddAlternative('X-SOCIALPROFILE', ['FACEBOOK'], []);
1063 with AddNew('X-MASTODON', [], [], SMastodon, cfMastodon, dtString) do
1064 AddAlternative('X-SOCIALPROFILE', ['MASTODON'], []);
1065 with AddNew('X-YOUTUBE', [], [], SYouTube, cfYouTube, dtString) do
1066 AddAlternative('X-SOCIALPROFILE', ['YOUTUBE'], []);
1067 with AddNew('X-PEERTUBE', [], [], SPeerTube, cfPeerTube, dtString) do
1068 AddAlternative('X-SOCIALPROFILE', ['PEERTUBE'], []);
1069 with AddNew('X-LINKEDIN', [], [], SLinkedIn, cfLinkedIn, dtString) do
1070 AddAlternative('X-SOCIALPROFILE', ['LINKEDIN'], []);
1071 with AddNew('X-SNAPCHAT', [], [], SSnapchat, cfSnapchat, dtString) do
1072 AddAlternative('X-SOCIALPROFILE', ['SNAPCHAT'], []);
1073 with AddNew('X-INSTAGRAM', [], [], SInstagram, cfInstagram, dtString) do
1074 AddAlternative('X-SOCIALPROFILE', ['INSTAGRAM'], []);
1075 with AddNew('X-REDDIT', [], [], SReddit, cfReddit, dtString) do
1076 AddAlternative('X-SOCIALPROFILE', ['REDDIT'], []);
1077 with AddNew('X-MYSPACE', [], [], SMySpace, cfMySpace, dtString) do
1078 AddAlternative('X-SOCIALPROFILE', ['MYSPACE'], []);
1079 end;
1080end;
1081
1082procedure TContactsFile.Error(Text: string; Line: Integer);
1083begin
1084 if Assigned(FOnError) then FOnError(Text, Line);
1085end;
1086
1087function TContactsFile.GetFileName: string;
1088begin
1089 Result := SVCardFile;
1090end;
1091
1092function TContactsFile.GetFileExt: string;
1093begin
1094 Result := VCardFileExt;
1095end;
1096
1097function TContactsFile.GetFileFilter: string;
1098begin
1099 Result := GetFileName + ' (' + GetFileExt + ')|*' + GetFileExt + '|' + inherited;
1100end;
1101
1102function TContactsFile.NewItem(Key, Value: string): string;
1103var
1104 Charset: string;
1105begin
1106 if not IsAsciiString(Value) then Charset := ';CHARSET=UTF-8'
1107 else Charset := '';
1108 Result := Key + Charset + ':' + Value;
1109end;
1110
1111procedure TContactsFile.SaveToFile(FileName: string);
1112var
1113 Output: TStringList;
1114 I: Integer;
1115begin
1116 inherited;
1117 Output := TStringList.Create;
1118 try
1119 for I := 0 to Contacts.Count - 1 do
1120 Contacts[I].SaveToStrings(Output);
1121 Output.SaveToFile(FileName);
1122 finally
1123 Output.Free;
1124 end
1125end;
1126
1127procedure TContactsFile.LoadFromFile(FileName: string);
1128var
1129 Lines: TStringList;
1130 Contact: TContact;
1131 I: Integer;
1132 NewI: Integer;
1133begin
1134 inherited;
1135 Contacts.Clear;
1136 Lines := TStringList.Create;
1137 Lines.LoadFromFile(FileName);
1138 {$IF FPC_FULLVERSION>=30200}
1139 if (Length(Lines.Text) > 0) and (Pos(VCardBegin, Lines.Text) = 0) then begin
1140 Lines.LoadFromFile(FileName, TEncoding.Unicode);
1141 if (Length(Lines.Text) > 0) and (Pos(VCardBegin, Lines.Text) = 0) then begin
1142 Lines.LoadFromFile(FileName, TEncoding.BigEndianUnicode);
1143 end;
1144 end;
1145 {$ENDIF}
1146 try
1147 I := 0;
1148 while I < Lines.Count do begin
1149 Contact := TContact.Create;
1150 Contact.Parent := Self;
1151 NewI := Contact.LoadFromStrings(Lines, I);
1152 if NewI <= Lines.Count then begin
1153 if NewI <> -1 then begin
1154 Contacts.Add(Contact);
1155 I := NewI;
1156 end else begin
1157 FreeAndNil(Contact);
1158 Inc(I);
1159 end;
1160 end else begin
1161 FreeAndNil(Contact);
1162 Break;
1163 end;
1164 end;
1165 finally
1166 Lines.Free;
1167 end;
1168end;
1169
1170constructor TContactsFile.Create;
1171begin
1172 inherited;
1173 Contacts := TContacts.Create;
1174 Contacts.ContactsFile := Self;
1175 Fields := TContactFields.Create;
1176 InitFields;
1177end;
1178
1179destructor TContactsFile.Destroy;
1180begin
1181 FreeAndNil(Fields);
1182 FreeAndNil(Contacts);
1183 inherited;
1184end;
1185
1186end.
1187
Note: See TracBrowser for help on using the repository browser.