source: tags/1.3.0/UContact.pas

Last change on this file was 113, checked in by chronos, 2 years ago
  • Modified: Version 1.3.0 release releated changes.

Merged revision(s) 112 from trunk:

  • Fixed: Contact photo was not saved correctly.
  • Fixed: URL contact photo was incorrectly encoded with BASE64 encoding.
  • Fixed: Disabled not correctly working autodetection of max. line length.
  • Fixed: Tab order in Contact name details window.
File size: 54.2 KB
Line 
1unit UContact;
2
3{$mode delphi}{$H+}
4
5interface
6
7uses
8 Classes, SysUtils, fgl, Dialogs, UDataFile, LazUTF8, Base64, Graphics;
9
10type
11 TNamePartKind = (npNone, npPrefix, npFirst, npMiddle, npLast, npSuffix);
12
13 TNamePart = record
14 Index: Integer;
15 Text: string;
16 PartKind: TNamePartKind;
17 NamePart: ^TNamePart;
18 Previous: ^TNamePart;
19 Next: ^TNamePart;
20 end;
21
22 TNameParts = array of TNamePart;
23
24 { TNameDetails }
25
26 TNameDetails = class
27 private
28 function GetAsNameParts: TNameParts;
29 function GetDetail(NamePartKind: TNamePartKind): string;
30 function IsSuffix(Text: string): Boolean;
31 public
32 Prefix: string;
33 First: string;
34 Middle: string;
35 Last: string;
36 Suffix: string;
37 procedure Split(FullName: string);
38 function GetCombined: string;
39 end;
40
41 TContactsFile = class;
42
43 TErrorEvent = procedure (Text: string; Line: Integer) of object;
44
45 TDataType = (dtNone, dtString, dtInteger, dtDate, dtDateTime, dtImage, dtStringList);
46
47 TContactFieldIndex = (cfNone, cfFirstName, cfMiddleName, cfLastName, cfTitleBefore,
48 cfTitleAfter, cfFullName,
49 cfTel, cfTelCell, cfTelFax, cfTelPager, cfTelHome2, cfTelVoip, cfTelMain,
50 cfTelHome, cfTelCellHome, cfTelFaxHome, cfTelPagerHome,
51 cfTelWork, cfTelCellWork, cfTelFaxWork, cfTelPagerWork,
52 cfEmail, cfUid, cfUrl, cfUrlHome, cfUrlWork,
53 cfEmailHome, cfEmailWork, cfEmailInternet, cfNickName, cfNote, cfRole, cfTitle,
54 cfCategories, cfOrganization, cfDepartment,
55 cfHomeAddressStreet, cfHomeAddressStreetExtended, cfHomeAddressCity, cfHomeAddressCountry,
56 cfHomeAddressPostalCode, cfHomeAddressRegion, cfHomeAddressPostOfficeBox,
57 cfWorkAddressStreet, cfWorkAddressStreetExtended, cfWorkAddressCity, cfWorkAddressCountry,
58 cfWorkAddressPostalCode, cfWorkAddressRegion, cfWorkAddressPostOfficeBox,
59 cfXTimesContacted, cfXLastTimeContacted, cfPhoto, cfDayOfBirth, cfRevision,
60 cfVersion, cfAnniversary, cfGender, cfLogo,
61 cfJabber, cfIcq, cfWindowsLive, cfGoogleTalk, cfAim, cfQq, cfYahoo, cfIrc,
62 cfSkype, cfMsn, cfGroupWise, cfGaduGadu,
63 cfTwitter, cfFacebook, cfInstagram, cfSnapchat, cfMatrix, cfYoutube,
64 cfPeerTube, cfLinkedIn, cfMastodon, cfMySpace, cfReddit);
65
66 TContactFieldIndexes = TFPGList<TContactFieldIndex>;
67
68 TContactFilterItem = class
69 FieldIndex: TContactFieldIndex;
70 Value: string;
71 end;
72
73 { TContactFilterItems }
74
75 TContactFilterItems = class(TFPGObjectList<TContactFilterItem>)
76 function AddNew(FieldIndex: TContactFieldIndex; Value: string): TContactFilterItem;
77 end;
78
79 TContactFields = class;
80
81 { TContactField }
82
83 TContactField = class
84 SysName: string;
85 Groups: TStringArray;
86 NoGroups: TStringArray;
87 Title: string;
88 Index: TContactFieldIndex;
89 ValueIndex: Integer;
90 DataType: TDataType;
91 Alternatives: TContactFields;
92 function AddAlternative(Name: string; Groups: array of string; NoGroups:
93 array of string): TContactField;
94 function GroupsContain(Name: string): Boolean;
95 function Match(ASysName: string; AGroups: TStringArray): Boolean;
96 constructor Create;
97 destructor Destroy; override;
98 end;
99
100 { TContactFields }
101
102 TContactFields = class(TFPGObjectList<TContactField>)
103 private
104 Indexes: array[TContactFieldIndex] of TContactField;
105 IndexesUpdated: Boolean;
106 public
107 procedure UpdateIndexes;
108 function AddNew(Name: string; Groups: array of string; NoGroups: array of string;
109 Title: string; Index: TContactFieldIndex; DataType:
110 TDataType = dtNone; ValueIndex: Integer = -1): TContactField;
111 function GetBySysName(SysName: string): TContactField;
112 function GetBySysNameGroups(SysName: string; Groups: TStringArray): TContactField;
113 function GetByIndex(Index: TContactFieldIndex): TContactField;
114 procedure LoadToStrings(AItems: TStrings);
115 end;
116
117 { TContactProperty }
118
119 TContactProperty = class
120 private
121 function GetValueItem(Index: Integer): string;
122 procedure SetValueItem(Index: Integer; AValue: string);
123 public
124 Name: string;
125 Attributes: TStringList;
126 Value: string;
127 Encoding: string;
128 Charset: string;
129 procedure EvaluateAttributes;
130 function GetDecodedValue: string;
131 function GetEncodedValue: string;
132 function MatchNameGroups(AName: string; Groups: TStringArray;
133 NoGroups: TStringArray): Boolean;
134 procedure Assign(Source: TContactProperty);
135 constructor Create;
136 destructor Destroy; override;
137 property ValueItem[Index: Integer]: string read GetValueItem write SetValueItem;
138 end;
139
140 { TContactProperties }
141
142 TContactProperties = class(TFPGObjectList<TContactProperty>)
143 function AddNew(Name, Value: string): TContactProperty;
144 procedure Assign(Source: TContactProperties);
145 procedure AssignToList(List: TFPGObjectList<TObject>);
146 function GetByName(Name: string): TContactProperty;
147 function GetByNameGroups(Name: string; Groups: TStringArray;
148 NoGroups: TStringArray): TContactProperty;
149 function GetByNameGroupsMultiple(Name: string; Groups: TStringArray;
150 NoGroups: TStringArray): TContactProperties;
151 end;
152
153 { TContact }
154
155 TContact = class
156 private
157 FModified: Boolean;
158 FOnModify: TNotifyEvent;
159 class var FFields: TContactFields;
160 function GetField(Index: TContactFieldIndex): string;
161 procedure SetField(Index: TContactFieldIndex; AValue: string);
162 procedure SetModified(AValue: Boolean);
163 procedure DoOnModify;
164 procedure DetectMaxLineLength(Text: string);
165 public
166 Properties: TContactProperties;
167 ContactsFile: TContactsFile;
168 class function GetFields: TContactFields; static;
169 function HasField(FieldIndex: TContactFieldIndex): Boolean;
170 function FullNameToFileName: string;
171 function GetProperty(Field: TContactField): TContactProperty; overload;
172 function GetProperty(FieldIndex: TContactFieldIndex): TContactProperty; overload;
173 procedure Assign(Source: TContact);
174 function UpdateFrom(Source: TContact): Boolean;
175 constructor Create;
176 destructor Destroy; override;
177 class destructor Destroy2;
178 procedure SaveToStrings(Output: TStrings);
179 function LoadFromStrings(Lines: TStrings; var StartLine: Integer): Boolean;
180 procedure SaveToFile(FileName: string);
181 procedure LoadFromFile(FileName: string);
182 property Fields[Index: TContactFieldIndex]: string read GetField write SetField;
183 property Modified: Boolean read FModified write SetModified;
184 property OnModify: TNotifyEvent read FOnModify write FOnModify;
185 end;
186
187 TGetContactEvent = function (Contact: TContact): TContact of object;
188
189 { TContacts }
190
191 TContacts = class(TFPGObjectList<TContact>)
192 ContactsFile: TContactsFile;
193 procedure Assign(Source: TContacts);
194 procedure AddContacts(Contacts: TContacts);
195 procedure InsertContacts(Index: Integer; Contacts: TContacts);
196 procedure AssignToList(List: TFPGObjectList<TObject>);
197 function AddNew: TContact;
198 function Search(Text: string; FieldIndex: TContactFieldIndex): TContact;
199 function CountByField(FieldIndex: TContactFieldIndex): Integer;
200 procedure Merge(Contact: TContact; FieldIndex: TContactFieldIndex);
201 function ToString: ansistring; override;
202 end;
203
204 { TContactsFile }
205
206 TContactsFile = class(TDataFile)
207 private
208 FMaxLineLength: Integer;
209 FOnError: TErrorEvent;
210 procedure Error(Text: string; Line: Integer);
211 function NewItem(Key, Value: string): string;
212 public
213 Contacts: TContacts;
214 function GetFileName: string; override;
215 function GetFileExt: string; override;
216 function GetFileFilter: string; override;
217 procedure SaveToStrings(Output: TStrings);
218 procedure LoadFromStrings(Lines: TStrings);
219 procedure SaveToFile(FileName: string); override;
220 procedure LoadFromFile(FileName: string); override;
221 constructor Create; override;
222 destructor Destroy; override;
223 published
224 property OnError: TErrorEvent read FOnError write FOnError;
225 property MaxLineLength: Integer read FMaxLineLength write FMaxLineLength;
226 end;
227
228const
229 VCardFileExt = '.vcf';
230 VCardBegin = 'BEGIN:VCARD';
231 VCardEnd = 'END:VCARD';
232 VCardBase64 = 'BASE64';
233 VCardQuotedPrintable = 'QUOTED-PRINTABLE';
234
235
236implementation
237
238uses
239 UQuotedPrintable, UCommon;
240
241const
242 DefaultMaxLineLength = 75;
243
244resourcestring
245 SVCardFile = 'vCard file';
246 SFieldIndexRedefined = 'Field index %d redefined';
247 SExpectedVCardBegin = 'Expected vCard begin';
248 SFieldIndexNotDefined = 'Field index not defined';
249 SContactHasNoParent = 'Contact has no parent';
250 SExpectedProperty = 'Expected contact property';
251 SVersion = 'Version';
252 SLastName = 'Last Name';
253 SFirstName = 'First Name';
254 SMiddleName = 'Middle Name';
255 STitleBefore = 'Title Before';
256 STitleAfter = 'Title After';
257 SFullName = 'Full Name';
258 STelephone = 'Telephone';
259 SMobilePhone = 'Mobile phone';
260 SPager = 'Pager';
261 SFax = 'Fax';
262 SHomePhone = 'Home phone';
263 SHomeMobile = 'Home mobile';
264 SHomeFax = 'Home fax';
265 SHomePager = 'Home pager';
266 SWorkPhone = 'Work phone';
267 SWorkFax = 'Work fax';
268 SWorkPager = 'Work pager';
269 SWorkMobile = 'Work mobile';
270 SHomePhone2 = 'Home phone 2';
271 SVoipPhone = 'VoIP phone';
272 SMainPhone = 'Main phone';
273 SEmail = 'E-mail';
274 SHomeEmail = 'Home E-mail';
275 SWorkEmail = 'Work E-mail';
276 SInternetEmail = 'Internet E-mail';
277 SNickName = 'Nick name';
278 SNote = 'Note';
279 SRole = 'Role';
280 STitle = 'Title';
281 SCategories = 'Categories';
282 SOrganization = 'Organization';
283 SDepartement = 'Departement';
284 SHomeAddressPostOfficeBox = 'Home address post office box';
285 SHomeAddressStreetExtended = 'Home address extended street';
286 SHomeAddressStreet = 'Home address street';
287 SHomeAddressCity = 'Home address city';
288 SHomeAddressRegion = 'Home address region';
289 SHomeAddressPostalCode = 'Home address postal code';
290 SHomeAddressCountry = 'Home address country';
291 SWorkAddressPostOfficeBox = 'Work address post office box';
292 SWorkAddressStreetExtended = 'Work address extended street';
293 SWorkAddressStreet = 'Work address street';
294 SWorkAddressCity = 'Work address city';
295 SWorkAddressRegion = 'Work address region';
296 SWorkAddressPostalCode = 'Work address postal code';
297 SWorkAddressCountry = 'Work address country';
298 STimesContacted = 'Times Contacted';
299 SLastTimeContacted = 'Last Time Contacted';
300 SPhoto = 'Photo';
301 SLogo = 'Logo';
302 SJabber = 'Jabber';
303 SDayOfBirth = 'Day of birth';
304 SAnniversary = 'Anniversary';
305 SRevision = 'Revision';
306 SUniqueIdentifier = 'Unique identifier';
307 SWebAddress = 'Web address';
308 SWebAddressHome = 'Web address home';
309 SWebAddressWork = 'Web address work';
310 SGender = 'Gender';
311 // Chat
312 SMsn = 'MSN';
313 SGoogleTalk = 'Google Talk';
314 SWindowsLive = 'Windows Live';
315 SAim = 'AIM';
316 SQq = 'QQ';
317 SIrc = 'IRC';
318 SIcq = 'ICQ';
319 SYahoo = 'Yahoo!';
320 SSkype = 'Skype';
321 SMatrix = 'Matrix';
322 SGroupWise = 'GroupWise';
323 SGaduGadu = 'GaduGadu';
324 // Social
325 STwitter = 'Twitter';
326 SFacebook = 'Facebook';
327 SInstagram = 'Instagram';
328 SMastodon = 'Mastodon';
329 SSnapchat = 'Snapchat';
330 SLinkedIn = 'LinkedIn';
331 SYouTube = 'YouTube';
332 SPeerTube = 'PeerTube';
333 SReddit = 'Reddit';
334 SMySpace = 'MySpace';
335
336function GetNext(var Text: string; Separator: string): string;
337begin
338 if Pos(Separator, Text) > 0 then begin
339 Result := Copy(Text, 1, Pos(Separator, Text) - 1);
340 Delete(Text, 1, Length(Result) + Length(Separator));
341 end else begin
342 Result := Text;
343 Text := '';
344 end;
345end;
346
347function IsAsciiString(Text: string): Boolean;
348var
349 I: Integer;
350begin
351 Result := True;
352 for I := 1 to Length(Text) do
353 if Ord(Text[I]) > 128 then begin
354 Result := False;
355 Break;
356 end;
357end;
358
359function StartsWith(Text, What: string): Boolean;
360begin
361 Result := Copy(Text, 1, Length(Text)) = What;
362end;
363
364function EndsWith(Text, What: string): Boolean;
365begin
366 Result := Copy(Text, Length(Text) - Length(What) + 1, MaxInt) = What;
367end;
368
369function EncodeEscaped(Text: string): string;
370var
371 I: Integer;
372 O: Integer;
373 InNewLine: Boolean;
374begin
375 Result := '';
376 I := 1;
377 O := 1;
378 InNewLine := False;
379 SetLength(Result, Length(Text)); // Preallocate string
380 while I <= Length(Text) do begin
381 if Text[I] in [',', '\', ';'] then begin
382 InNewLine := False;
383 Result[O] := '\';
384 SetLength(Result, Length(Result) + 1);
385 Inc(O);
386 Result[O] := Text[I];
387 Inc(O);
388 end else
389 if Text[I] in [#13, #10] then begin
390 if not InNewLine then begin
391 Result[O] := '\';
392 Inc(O);
393 SetLength(Result, Length(Result) + 1);
394 Result[O] := 'n';
395 Inc(O);
396 InNewLine := True;
397 end;
398 end else begin
399 InNewLine := False;
400 Result[O] := Text[I];
401 Inc(O);
402 end;
403 Inc(I);
404 end;
405 SetLength(Result, O - 1);
406end;
407
408function DecodeEscaped(Text: string): string;
409var
410 I: Integer;
411 O: Integer;
412 Escaped: Boolean;
413begin
414 Result := '';
415 I := 1;
416 O := 1;
417 Escaped := False;
418 SetLength(Result, Length(Text)); // Preallocate string
419 while I <= Length(Text) do begin
420 if Escaped then begin
421 if Text[I] = 'n' then begin
422 Result[O] := #13;
423 Inc(O);
424 Result[O] := #10;
425 Inc(O);
426 end else begin
427 Result[O] := Text[I];
428 Inc(O);
429 end;
430 Escaped := False;
431 end else begin
432 if Text[I] = '\' then begin
433 Escaped := True;
434 end else begin
435 Result[O] := Text[I];
436 Inc(O);
437 end;
438 end;
439 Inc(I);
440 end;
441 SetLength(Result, O - 1);
442end;
443
444{ TNameDetails }
445
446function IsNumber(Text: string): Boolean;
447var
448 Value: Integer;
449begin
450 Result := TryStrToInt(Text, Value);
451end;
452
453function IsRomanNumber(Text: string): Boolean;
454var
455 I: Integer;
456begin
457 Result := True;
458 for I := 1 to Length(Text) do
459 if not (Text[I] in ['I', 'V', 'X', 'L', 'C', 'D', 'M']) then begin
460 Result := False;
461 Break;
462 end;
463end;
464
465procedure SearchPart(var NameParts: TNameParts; var NamePart: TNamePart);
466var
467 I: Integer;
468begin
469 for I := 0 to Length(NameParts) - 1 do begin
470 if (NameParts[I].PartKind = npNone) and (NameParts[I].Text = NamePart.Text) then begin
471 NameParts[I].PartKind := NamePart.PartKind;
472 NameParts[I].NamePart := @NamePart;
473 NamePart.NamePart := @NameParts[I];
474 Break;
475 end;
476 end;
477end;
478
479procedure SearchPartBackward(var NameParts: TNameParts; var NamePart: TNamePart);
480var
481 I: Integer;
482begin
483 for I := Length(NameParts) - 1 downto 0 do begin
484 if (NameParts[I].PartKind = npNone) and (NameParts[I].Text = NamePart.Text) then begin
485 NameParts[I].PartKind := NamePart.PartKind;
486 NameParts[I].NamePart := @NamePart;
487 NamePart.NamePart := @NameParts[I];
488 Break;
489 end;
490 end;
491end;
492
493function UsedInNameParts(NamePart: TNamePartKind; NameParts: TNameParts): Boolean;
494var
495 I: Integer;
496begin
497 I := 0;
498 while (I < Length(NameParts)) and (NameParts[I].PartKind <> NamePart) do Inc(I);
499 Result := I < Length(NameParts);
500end;
501
502function TNameDetails.GetAsNameParts: TNameParts;
503var
504 I: Integer;
505 K: TNamePartKind;
506 Parts: TStringArray;
507begin
508 Result := Default(TNameParts);
509 for K := Low(TNamePartKind) to High(TNamePartKind) do begin
510 if GetDetail(K) <> '' then begin
511 Parts := Explode(' ', GetDetail(K));
512 for I := 0 to Length(Parts) - 1 do begin
513 SetLength(Result, Length(Result) + 1);
514 Result[Length(Result) - 1].Text := Parts[I];
515 Result[Length(Result) - 1].PartKind := K;
516 Result[Length(Result) - 1].Index := Length(Result) - 1;
517 end;
518 end;
519 end;
520
521 // Update previous and next links
522 for I := 0 to Length(Result) - 1 do begin
523 if I > 0 then
524 Result[I].Previous := @Result[I - 1];
525 if (I + 1) < Length(Result) then
526 Result[I].Next := @Result[I + 1];
527 end;
528end;
529
530function TNameDetails.GetDetail(NamePartKind: TNamePartKind): string;
531begin
532 case NamePartKind of
533 npPrefix: Result := Prefix;
534 npFirst: Result := First;
535 npMiddle: Result := Middle;
536 npLast: Result := Last;
537 npSuffix: Result := Suffix;
538 end;
539end;
540
541function TNameDetails.IsSuffix(Text: string): Boolean;
542begin
543 Result := (Pos('.', Text) > 0) or IsNumber(Text) or
544 IsRomanNumber(Text);
545end;
546
547procedure TNameDetails.Split(FullName: string);
548var
549 Parts: TStringArray;
550 NewNameParts: TNameParts;
551 OldNameParts: TNameParts;
552 I: Integer;
553 J: Integer;
554 Text: string;
555 NextKind: TNamePartKind;
556begin
557 OldNameParts := GetAsNameParts;
558
559 Text := FullName;
560 while Pos(' ', FullName) > 0 do
561 FullName := StringReplace(FullName, ' ', ' ', [rfReplaceAll]);
562 Text := Trim(Text);
563
564 Parts := Explode(' ', Text);
565 NewNameParts := Default(TNameParts);
566 SetLength(NewNameParts, Length(Parts));
567 for I := 0 to Length(NewNameParts) - 1 do begin
568 NewNameParts[I].Index := I;
569 NewNameParts[I].PartKind := npNone;
570 NewNameParts[I].Text := Parts[I];
571 if I > 0 then
572 NewNameParts[I].Previous := @NewNameParts[I - 1];
573 if (I + 1) < Length(NewNameParts) then
574 NewNameParts[I].Next := @NewNameParts[I + 1];
575 end;
576
577 // Match existing parts
578 for I := 0 to Length(OldNameParts) - 1 do begin
579 if OldNameParts[I].Text <> '' then
580 SearchPart(NewNameParts, OldNameParts[I]);
581 end;
582
583 // Check incorrect matches
584 for I := 0 to Length(OldNameParts) - 1 do begin
585 for J := I + 1 to Length(OldNameParts) - 1 do
586 if Assigned(OldNameParts[I].NamePart) and Assigned(OldNameParts[J].NamePart) and
587 (OldNameParts[I].NamePart^.Index >= OldNameParts[J].NamePart^.Index) then begin
588 if Abs(I - OldNameParts[I].NamePart^.Index) >
589 Abs(J - OldNameParts[J].NamePart^.Index) then begin
590 OldNameParts[I].NamePart^.PartKind := npNone;
591 OldNameParts[I].NamePart^.NamePart := nil;
592 OldNameParts[I].NamePart := nil;
593 end else begin
594 OldNameParts[J].NamePart^.PartKind := npNone;
595 OldNameParts[J].NamePart^.NamePart := nil;
596 OldNameParts[J].NamePart := nil;
597 end;
598 end;
599 end;
600 for I := Length(OldNameParts) - 1 downto 0 do begin
601 for J := I - 1 downto 0 do
602 if Assigned(OldNameParts[I].NamePart) and Assigned(OldNameParts[J].NamePart) and
603 (OldNameParts[I].NamePart^.Index <= OldNameParts[J].NamePart^.Index) then begin
604 if Abs(I - OldNameParts[I].NamePart^.Index) >
605 Abs(J - OldNameParts[J].NamePart^.Index) then begin
606 OldNameParts[I].NamePart^.PartKind := npNone;
607 OldNameParts[I].NamePart^.NamePart := nil;
608 OldNameParts[I].NamePart := nil;
609 end else begin
610 OldNameParts[J].NamePart^.PartKind := npNone;
611 OldNameParts[J].NamePart^.NamePart := nil;
612 OldNameParts[J].NamePart := nil;
613 end;
614 end;
615 end;
616
617 // Match existing parts backqards
618 for I := Length(OldNameParts) - 1 downto 0 do begin
619 if (OldNameParts[I].Text <> '') and not Assigned(OldNameParts[I].NamePart) then
620 SearchPartBackward(NewNameParts, OldNameParts[I]);
621 end;
622
623 // Match uncertain parts
624 for I := 0 to Length(OldNameParts) - 1 do
625 if not Assigned(OldNameParts[I].NamePart) then begin
626 if Assigned(OldNameParts[I].Next) and
627 Assigned(OldNameParts[I].Next^.NamePart) and
628 Assigned(OldNameParts[I].Next^.NamePart^.Previous) and
629 (OldNameParts[I].Next^.NamePart^.Previous^.PartKind = npNone) then begin
630 OldNameParts[I].NamePart := OldNameParts[I].Next^.NamePart^.Previous;
631 OldNameParts[I].Next^.NamePart^.Previous^.NamePart := @OldNameParts[I];
632 OldNameParts[I].Next^.NamePart^.Previous^.PartKind := OldNameParts[I].PartKind;
633 end else
634 if Assigned(OldNameParts[I].Previous) and
635 Assigned(OldNameParts[I].Previous^.NamePart) and
636 Assigned(OldNameParts[I].Previous^.NamePart^.Next) and
637 (OldNameParts[I].Previous^.NamePart^.Next^.PartKind = npNone) then begin
638 OldNameParts[I].NamePart := OldNameParts[I].Previous^.NamePart^.Next;
639 OldNameParts[I].Previous^.NamePart^.Next^.NamePart := @OldNameParts[I];
640 OldNameParts[I].Previous^.NamePart^.Next^.PartKind := OldNameParts[I].PartKind;
641 end;
642 end;
643
644 // Mark new unknown parts according existing parts
645 for I := Length(Parts) - 1 downto 0 do
646 if (NewNameParts[I].PartKind = npNone) and
647 Assigned(NewNameParts[I].Next) and
648 (NewNameParts[I].Next^.PartKind <> npNone) then begin
649 if (NewNameParts[I].Next^.PartKind = npFirst) and
650 EndsWith(NewNameParts[I].Text, '.') then begin
651 NewNameParts[I].PartKind := npPrefix;
652 end else NewNameParts[I].PartKind := NewNameParts[I].Next^.PartKind;
653 end;
654
655 // Mark unknown parts according to neighbouring parts
656 for I := 0 to Length(Parts) - 1 do
657 if (NewNameParts[I].PartKind = npNone) and
658 Assigned(NewNameParts[I].Previous) and
659 (NewNameParts[I].Previous^.PartKind <> npNone) then begin
660 if (NewNameParts[I].Previous^.PartKind in [npLast, npMiddle]) and
661 IsSuffix(NewNameParts[I].Text) then begin
662 NewNameParts[I].PartKind := npSuffix;
663 end else
664 if (NewNameParts[I].Previous^.PartKind = npFirst) and
665 (Last = '') then begin
666 NewNameParts[I].PartKind := npLast;
667 end else
668 if (NewNameParts[I].Previous^.PartKind = npLast) and
669 (Middle = '') then begin
670 NewNameParts[I].PartKind := npLast;
671 NewNameParts[I].Previous^.PartKind := npMiddle;
672 end else
673 if (NewNameParts[I].Previous^.PartKind = npPrefix) then begin
674 NewNameParts[I].PartKind := npFirst;
675 end else
676 NewNameParts[I].PartKind := NewNameParts[I].Previous^.PartKind;
677 end;
678
679 // Mark remaining unknown parts based on defined filling sequence
680 NextKind := npFirst;
681 for I := 0 to Length(Parts) - 1 do
682 if NewNameParts[I].PartKind = npNone then begin
683 if EndsWith(NewNameParts[I].Text, '.') and (NextKind = npFirst) then begin
684 NewNameParts[I].PartKind := npPrefix;
685 end else
686 if (NextKind = npMiddle) and IsSuffix(NewNameParts[I].Text) then begin
687 NewNameParts[I].PartKind := npSuffix;
688 NextKind := npSuffix;
689 end else
690 if NextKind = npMiddle then begin
691 NewNameParts[I].Previous^.PartKind := npMiddle;
692 NewNameParts[I].PartKind := npLast;
693 end else begin
694 NewNameParts[I].PartKind := NextKind;
695 if NextKind = npFirst then NextKind := npLast
696 else if NextKind = npLast then NextKind := npMiddle;
697 end;
698 end;
699
700 // Combine multiple parts to base parts
701 Prefix := '';
702 First := '';
703 Middle := '';
704 Last := '';
705 Suffix := '';
706 for I := 0 to Length(Parts) - 1 do
707 case NewNameParts[I].PartKind of
708 npPrefix: Prefix := Trim(Prefix + ' ' + Parts[I]);
709 npFirst: First := Trim(First + ' ' + Parts[I]);
710 npMiddle: Middle := Trim(Middle + ' ' + Parts[I]);
711 npLast: Last := Trim(Last + ' ' + Parts[I]);
712 npSuffix: Suffix := Trim(Suffix + ' ' + Parts[I]);
713 end;
714
715{
716 // Title Prefix
717 while (Length(Parts) > 0) and EndsWith(Parts[0], '.') do begin
718 Prefix := Trim(Prefix + ' ' + Parts[0]);
719 Delete(Parts, 0, 1);
720 end;
721
722 // Title Suffix
723 if ProcessAfter then
724 for I := 0 to High(Parts) do
725 if (Pos('.', Parts[I]) > 0) or IsNumber(Parts[I]) or IsRomanNumber(Parts[I]) then begin
726 for J := I to High(Parts) do
727 Suffix := Trim(Suffix + ' ' + Parts[J]);
728 SetLength(Parts, I);
729 Break;
730 end;
731
732 if Length(Parts) = 0 then begin
733 end else
734 if Length(Parts) = 1 then begin
735 First := Parts[0];
736 end else
737 if Length(Parts) = 2 then begin
738 First := Parts[0];
739 Last := Parts[1];
740 end else begin
741 First := Parts[0];
742 for I := 0 to Length(Parts) - 3 do
743 Middle := Trim(Middle + ' ' + Parts[I + 1]);
744 Last := Parts[High(Parts)];
745 end;}
746end;
747
748function TNameDetails.GetCombined: string;
749begin
750 Result := '';
751 if Prefix <> '' then Result := Result + ' ' + Prefix;
752 if First <> '' then Result := Result + ' ' + First;
753 if Middle <> '' then Result := Result + ' ' + Middle;
754 if Last <> '' then Result := Result + ' ' + Last;
755 if Suffix <> '' then Result := Result + ' ' + Suffix;
756 Result := Trim(Result);
757end;
758
759{ TContactFilterItems }
760
761function TContactFilterItems.AddNew(FieldIndex: TContactFieldIndex;
762 Value: string): TContactFilterItem;
763begin
764 Result := TContactFilterItem.Create;
765 Result.FieldIndex := FieldIndex;
766 Result.Value := Value;
767 Add(Result);
768end;
769
770{ TContactField }
771
772function TContactField.AddAlternative(Name: string; Groups: array of string;
773 NoGroups: array of string): TContactField;
774begin
775 Result := Alternatives.AddNew(Name, Groups, NoGroups, Title, Index, DataType, ValueIndex);
776end;
777
778function TContactField.GroupsContain(Name: string): Boolean;
779var
780 I: Integer;
781begin
782 Result := False;
783 for I := 0 to Length(Groups) - 1 do
784 if Groups[I] = Name then begin
785 Result := True;
786 Break;
787 end;
788end;
789
790function TContactField.Match(ASysName: string; AGroups: TStringArray): Boolean;
791var
792 I: Integer;
793begin
794 Result := ASysName = SysName;
795 if Result then begin
796 for I := 0 to Length(AGroups) - 1 do begin
797 if not GroupsContain(AGroups[I]) then begin
798 Result := False;
799 Break;
800 end;
801 end;
802 end;
803end;
804
805constructor TContactField.Create;
806begin
807 Alternatives := TContactFields.Create;
808end;
809
810destructor TContactField.Destroy;
811begin
812 FreeAndNil(Alternatives);
813 inherited;
814end;
815
816{ TContactProperties }
817
818function TContactProperties.AddNew(Name, Value: string): TContactProperty;
819begin
820 Result := TContactProperty.Create;
821 Result.Name := Name;
822 Result.Value := Value;
823 Add(Result);
824end;
825
826procedure TContactProperties.Assign(Source: TContactProperties);
827var
828 I: Integer;
829begin
830 while Count < Source.Count do
831 Add(TContactProperty.Create);
832 while Count > Source.Count do
833 Delete(Count - 1);
834 for I := 0 to Count - 1 do
835 Items[I].Assign(Source.Items[I]);
836end;
837
838procedure TContactProperties.AssignToList(List: TFPGObjectList<TObject>);
839var
840 I: Integer;
841begin
842 while List.Count > Count do List.Delete(List.Count - 1);
843 while List.Count < Count do List.Add(nil);
844 for I := 0 to Count - 1 do
845 List[I] := Items[I];
846end;
847
848function TContactProperties.GetByName(Name: string): TContactProperty;
849var
850 I: Integer;
851begin
852 I := 0;
853 while (I < Count) and (Items[I].Name <> Name) and (not EndsWith(Items[I].Name, '.' + Name)) do Inc(I);
854 if I < Count then Result := Items[I]
855 else Result := nil;
856end;
857
858function TContactProperties.GetByNameGroups(Name: string; Groups: TStringArray;
859 NoGroups: TStringArray): TContactProperty;
860var
861 I: Integer;
862begin
863 I := 0;
864 while (I < Count) and not Items[I].MatchNameGroups(Name, Groups, NoGroups) do Inc(I);
865 if I < Count then Result := Items[I]
866 else Result := nil;
867end;
868
869function TContactProperties.GetByNameGroupsMultiple(Name: string;
870 Groups: TStringArray; NoGroups: TStringArray): TContactProperties;
871var
872 I: Integer;
873begin
874 Result := TContactProperties.Create(False);
875 for I := 0 to Count - 1 do
876 if Items[I].MatchNameGroups(Name, Groups, NoGroups) then
877 Result.Add(Items[I]);
878end;
879
880{ TContactProperty }
881
882function TContactProperty.GetValueItem(Index: Integer): string;
883var
884 List: TStringList;
885begin
886 List := TStringList.Create;
887 try
888 List.Delimiter := ';';
889 List.NameValueSeparator := '=';
890 List.StrictDelimiter := True;
891 List.DelimitedText := Value;
892 if Index < List.Count then
893 Result := List.Strings[Index]
894 else Result := '';
895 finally
896 List.Free;
897 end;
898end;
899
900procedure TContactProperty.SetValueItem(Index: Integer; AValue: string);
901var
902 List: TStringList;
903begin
904 List := TStringList.Create;
905 try
906 List.Delimiter := ';';
907 List.NameValueSeparator := '=';
908 List.StrictDelimiter := True;
909 List.DelimitedText := Value;
910
911 // Extend subitems count
912 while List.Count <= Index do
913 List.Add('');
914
915 List.Strings[Index] := AValue;
916
917 // Remove empty items
918 while (List.Count > 0) and (List.Strings[List.Count - 1] = '') do
919 List.Delete(List.Count - 1);
920
921 Value := List.DelimitedText;
922 finally
923 List.Free;
924 end;
925end;
926
927procedure TContactProperty.EvaluateAttributes;
928var
929 I: Integer;
930begin
931 if Attributes.IndexOf(VCardBase64) <> -1 then begin
932 Encoding := VCardBase64;
933 Value := GetDecodedValue;
934 end else
935 if Attributes.IndexOfName('ENCODING') <> -1 then begin
936 Encoding := Attributes.Values['ENCODING'];
937 if (Encoding = 'B') or (Encoding = 'b') then Encoding := VCardBase64;
938 if (Encoding = 'Q') or (Encoding = 'q') then Encoding := VCardQuotedPrintable;
939 if (Encoding = VCardQuotedPrintable) or (Encoding = VCardBase64) then begin
940 Value := GetDecodedValue;
941 Attributes.Delete(Attributes.IndexOfName('ENCODING'));
942 end else
943 end else Encoding := '';
944
945 if Attributes.IndexOfName('CHARSET') <> -1 then
946 Charset := Attributes.Values['CHARSET']
947 else Charset := '';
948
949 // Simplify TYPE attribute from TYPE=VALUE into VALUE
950 for I := 0 to Attributes.Count - 1 do begin
951 if Attributes.Names[I] = 'TYPE' then
952 Attributes.Strings[I] := Attributes.Values['TYPE'];
953 if Attributes.Names[I] = 'type' then
954 Attributes.Strings[I] := Attributes.Values['type'];
955 end;
956end;
957
958function TContactProperty.GetDecodedValue: string;
959begin
960 if Encoding = VCardBase64 then begin
961 Result := DecodeStringBase64(Value);
962 end else
963 if Encoding = VCardQuotedPrintable then begin
964 Result := DecodeQuotedPrintable(Value, True);
965 end
966 else Result := '';
967end;
968
969function TContactProperty.GetEncodedValue: string;
970begin
971 if Encoding = VCardBase64 then begin
972 Result := EncodeStringBase64(Value);
973 end else
974 if Encoding = VCardQuotedPrintable then begin
975 Result := EncodeQuotedPrintable(Value, True);
976 end
977 else Result := '';
978end;
979
980function TContactProperty.MatchNameGroups(AName: string; Groups: TStringArray;
981 NoGroups: TStringArray): Boolean;
982var
983 I: Integer;
984begin
985 Result := (Name = AName) or EndsWith(Name, '.' + AName);
986 if Result and (Length(Groups) > 0) then begin
987 for I := 0 to Length(Groups) - 1 do
988 if (Attributes.IndexOf(Groups[I]) = -1) and
989 (Attributes.IndexOf('TYPE=' + Groups[I]) = -1) then begin
990 Result := False;
991 Break;
992 end;
993 end;
994 if Result and (Length(NoGroups) > 0) then begin
995 for I := 0 to Length(NoGroups) - 1 do
996 if (Attributes.IndexOf(NoGroups[I]) <> -1) or
997 (Attributes.IndexOf('TYPE=' + NoGroups[I]) <> -1) then begin
998 Result := False;
999 Break;
1000 end;
1001 end;
1002end;
1003
1004procedure TContactProperty.Assign(Source: TContactProperty);
1005begin
1006 Name := Source.Name;
1007 Attributes.Assign(Source.Attributes);
1008 Value := Source.Value;
1009 Encoding := Source.Encoding;
1010 Charset := Source.Charset;
1011end;
1012
1013constructor TContactProperty.Create;
1014begin
1015 Attributes := TStringList.Create;
1016 Attributes.Delimiter := ';';
1017 Attributes.NameValueSeparator := '=';
1018 Attributes.StrictDelimiter := True;
1019end;
1020
1021destructor TContactProperty.Destroy;
1022begin
1023 FreeAndNil(Attributes);
1024 inherited;
1025end;
1026
1027{ TContacts }
1028
1029procedure TContacts.Assign(Source: TContacts);
1030var
1031 I: Integer;
1032begin
1033 while Count < Source.Count do
1034 Add(TContact.Create);
1035 while Count > Source.Count do
1036 Delete(Count - 1);
1037 for I := 0 to Count - 1 do begin
1038 Items[I].Assign(Source.Items[I]);
1039 Items[I].ContactsFile := ContactsFile;
1040 end;
1041end;
1042
1043procedure TContacts.AddContacts(Contacts: TContacts);
1044var
1045 I: Integer;
1046 NewContact: TContact;
1047begin
1048 for I := 0 to Contacts.Count - 1 do begin
1049 NewContact := TContact.Create;
1050 NewContact.Assign(Contacts[I]);
1051 NewContact.ContactsFile := ContactsFile;
1052 Add(NewContact);
1053 end;
1054end;
1055
1056procedure TContacts.InsertContacts(Index: Integer; Contacts: TContacts);
1057var
1058 I: Integer;
1059 NewContact: TContact;
1060begin
1061 for I := 0 to Contacts.Count - 1 do begin
1062 NewContact := TContact.Create;
1063 NewContact.Assign(Contacts[I]);
1064 NewContact.ContactsFile := ContactsFile;
1065 Insert(Index, NewContact);
1066 Inc(Index);
1067 end;
1068end;
1069
1070procedure TContacts.AssignToList(List: TFPGObjectList<TObject>);
1071var
1072 I: Integer;
1073begin
1074 while List.Count > Count do List.Delete(List.Count - 1);
1075 while List.Count < Count do List.Add(nil);
1076 for I := 0 to Count - 1 do
1077 List[I] := Items[I];
1078end;
1079
1080function TContacts.AddNew: TContact;
1081begin
1082 Result := TContact.Create;
1083 Result.ContactsFile := ContactsFile;
1084 Add(Result);
1085end;
1086
1087function TContacts.Search(Text: string; FieldIndex: TContactFieldIndex): TContact;
1088var
1089 I: Integer;
1090begin
1091 Result := nil;
1092 for I := 0 to Count - 1 do
1093 if Items[I].Fields[FieldIndex] = Text then begin
1094 Result := Items[I];
1095 Break;
1096 end;
1097end;
1098
1099function TContacts.CountByField(FieldIndex: TContactFieldIndex): Integer;
1100var
1101 I: Integer;
1102begin
1103 Result := 0;
1104 for I := 0 to Count - 1 do
1105 if Items[I].HasField(FieldIndex) then
1106 Inc(Result);
1107end;
1108
1109procedure TContacts.Merge(Contact: TContact; FieldIndex: TContactFieldIndex);
1110var
1111 NewContact: TContact;
1112begin
1113 NewContact := Search(Contact.Fields[FieldIndex], FieldIndex);
1114 if Assigned(NewContact) then begin
1115 NewContact.UpdateFrom(Contact);
1116 end else begin
1117 NewContact := TContact.Create;
1118 NewContact.Assign(Contact);
1119 NewContact.ContactsFile := ContactsFile;
1120 Add(NewContact);
1121 end;
1122end;
1123
1124function TContacts.ToString: ansistring;
1125var
1126 I: Integer;
1127begin
1128 Result := '';
1129 for I := 0 to Count - 1 do begin
1130 if I > 0 then Result := Result + ', ';
1131 Result := Result + Items[I].Fields[cfFullName];
1132 end;
1133end;
1134
1135{ TContactFields }
1136
1137procedure TContactFields.UpdateIndexes;
1138var
1139 I: Integer;
1140 Index: TContactFieldIndex;
1141begin
1142 for Index := Low(TContactFieldIndex) to High(TContactFieldIndex) do
1143 Indexes[Index] := nil;
1144 for I := 0 to Count - 1 do
1145 if not Assigned(Indexes[Items[I].Index]) then Indexes[Items[I].Index] := Items[I]
1146 else raise Exception.Create(Format(SFieldIndexRedefined, [Integer(Items[I].Index)]));
1147 IndexesUpdated := True;
1148end;
1149
1150function TContactFields.AddNew(Name: string; Groups: array of string;
1151 NoGroups: array of string; Title: string; Index: TContactFieldIndex;
1152 DataType: TDataType = dtNone; ValueIndex: Integer = -1): TContactField;
1153var
1154 I: Integer;
1155begin
1156 Result := TContactField.Create;
1157 Result.SysName := Name;
1158 SetLength(Result.Groups, Length(Groups));
1159 for I := 0 to Length(Groups) - 1 do
1160 Result.Groups[I] := Groups[I];
1161 SetLength(Result.NoGroups, Length(NoGroups));
1162 for I := 0 to Length(NoGroups) - 1 do
1163 Result.NoGroups[I] := NoGroups[I];
1164 Result.Title := Title;
1165 Result.Index := Index;
1166 Result.ValueIndex := ValueIndex;
1167 Result.DataType := DataType;
1168 Add(Result);
1169 IndexesUpdated := False;
1170end;
1171
1172function TContactFields.GetBySysName(SysName: string): TContactField;
1173var
1174 I: Integer;
1175begin
1176 I := 0;
1177 while (I < Count) and (Items[I].SysName <> SysName) do Inc(I);
1178 if I < Count then Result := Items[I]
1179 else Result := nil;
1180end;
1181
1182function TContactFields.GetBySysNameGroups(SysName: string; Groups: TStringArray
1183 ): TContactField;
1184var
1185 I: Integer;
1186begin
1187 I := 0;
1188 while (I < Count) and not Items[I].Match(SysName, Groups) do Inc(I);
1189 if I < Count then Result := Items[I]
1190 else Result := nil;
1191end;
1192
1193function TContactFields.GetByIndex(Index: TContactFieldIndex): TContactField;
1194var
1195 I: Integer;
1196begin
1197 if IndexesUpdated then Result := Indexes[Index]
1198 else begin
1199 I := 0;
1200 while (I < Count) and (Items[I].Index <> Index) do Inc(I);
1201 if I < Count then Result := Items[I]
1202 else Result := nil;
1203 end;
1204end;
1205
1206procedure TContactFields.LoadToStrings(AItems: TStrings);
1207var
1208 I: Integer;
1209begin
1210 AItems.BeginUpdate;
1211 try
1212 while AItems.Count < Count do AItems.Add('');
1213 while AItems.Count > Count do AItems.Delete(AItems.Count - 1);
1214 for I := 0 to Count - 1 do begin
1215 AItems.Objects[I] := Items[I];
1216 AItems[I] := Items[I].Title;
1217 end;
1218 SortStrings(AItems);
1219 finally
1220 AItems.EndUpdate;
1221 end;
1222end;
1223
1224{ TContact }
1225
1226class function TContact.GetFields: TContactFields;
1227begin
1228 if not Assigned(FFields) then begin
1229 FFields := TContactFields.Create;
1230 with FFields do begin
1231 AddNew('VERSION', [], [], SVersion, cfVersion, dtString);
1232 AddNew('N', [], [], SLastName, cfLastName, dtString, 0);
1233 AddNew('N', [], [], SFirstName, cfFirstName, dtString, 1);
1234 AddNew('N', [], [], SMiddleName, cfMiddleName, dtString, 2);
1235 AddNew('N', [], [], STitleBefore, cfTitleBefore, dtString, 3);
1236 AddNew('N', [], [], STitleAfter, cfTitleAfter, dtString, 4);
1237 AddNew('FN', [], [], SFullName, cfFullName, dtString);
1238 AddNew('TEL', [], ['CELL', 'FAX', 'PAGER', 'WORK', 'HOME'], STelephone, cfTel, dtString);
1239 AddNew('TEL', ['CELL'], ['WORK', 'HOME'], SMobilePhone, cfTelCell, dtString);
1240 AddNew('TEL', ['FAX'], ['WORK', 'HOME'], SFax, cfTelFax, dtString);
1241 AddNew('TEL', ['PAGER'], ['WORK', 'HOME'], SPager, cfTelPager, dtString);
1242 AddNew('TEL', ['HOME'], ['CELL', 'FAX', 'PAGER'], SHomePhone, cfTelHome, dtString);
1243 AddNew('TEL', ['HOME', 'CELL'], [], SHomeMobile, cfTelCellHome, dtString);
1244 AddNew('TEL', ['HOME', 'FAX'], [], SHomeFax, cfTelFaxHome, dtString);
1245 AddNew('TEL', ['HOME', 'PAGER'], [], SHomePager, cfTelPagerHome, dtString);
1246 AddNew('TEL', ['WORK'], ['CELL', 'FAX', 'PAGER'], SWorkPhone, cfTelWork, dtString);
1247 AddNew('TEL', ['WORK', 'CELL'], [], SWorkMobile, cfTelCellWork, dtString);
1248 AddNew('TEL', ['WORK', 'FAX'], [], SWorkFax, cfTelFaxWork, dtString);
1249 AddNew('TEL', ['WORK', 'PAGER'], [], SWorkPager, cfTelPagerWork, dtString);
1250 AddNew('TEL', ['HOME2'], [], SHomePhone2, cfTelHome2, dtString);
1251 AddNew('TEL', ['VOIP'], [], SVoipPhone, cfTelVoip, dtString);
1252 AddNew('TEL', ['MAIN'], [], SMainPhone, cfTelMain, dtString);
1253 AddNew('EMAIL', [], ['HOME', 'WORK', 'INTERNET'], SEmail, cfEmail, dtString);
1254 AddNew('EMAIL', ['HOME'], [], SHomeEmail, cfEmailHome, dtString);
1255 AddNew('EMAIL', ['WORK'], [], SWorkEmail, cfEmailWork, dtString);
1256 AddNew('EMAIL', ['INTERNET'], [], SInternetEmail, cfEmailInternet, dtString);
1257 with AddNew('NICKNAME', [], [], SNickName, cfNickName, dtString) do
1258 AddAlternative('X-NICKNAME', [], []);
1259 AddNew('NOTE', [], [], SNote, cfNote, dtString);
1260 AddNew('ROLE', [], [], SRole, cfRole, dtString);
1261 AddNew('TITLE', [], [], STitle, cfTitle, dtString);
1262 AddNew('CATEGORIES', [], [], SCategories, cfCategories, dtString);
1263 AddNew('ORG', [], [], SOrganization, cfOrganization, dtString, 0);
1264 AddNew('ORG', [], [], SDepartement, cfDepartment, dtString, 1);
1265 AddNew('ADR', ['HOME'], [], SHomeAddressPostOfficeBox, cfHomeAddressPostOfficeBox, dtString, 0);
1266 AddNew('ADR', ['HOME'], [], SHomeAddressStreetExtended, cfHomeAddressStreetExtended, dtString, 1);
1267 AddNew('ADR', ['HOME'], [], SHomeAddressStreet, cfHomeAddressStreet, dtString, 2);
1268 AddNew('ADR', ['HOME'], [], SHomeAddressCity, cfHomeAddressCity, dtString, 3);
1269 AddNew('ADR', ['HOME'], [], SHomeAddressRegion, cfHomeAddressRegion, dtString, 4);
1270 AddNew('ADR', ['HOME'], [], SHomeAddressPostalCode, cfHomeAddressPostalCode, dtString, 5);
1271 AddNew('ADR', ['HOME'], [], SHomeAddressCountry, cfHomeAddressCountry, dtString, 6);
1272 AddNew('ADR', ['WORK'], [], SWorkAddressPostOfficeBox, cfWorkAddressPostOfficeBox, dtString, 0);
1273 AddNew('ADR', ['WORK'], [], SWorkAddressStreetExtended, cfWorkAddressStreetExtended, dtString, 1);
1274 AddNew('ADR', ['WORK'], [], SWorkAddressStreet, cfWorkAddressStreet, dtString, 2);
1275 AddNew('ADR', ['WORK'], [], SWorkAddressCity, cfWorkAddressCity, dtString, 3);
1276 AddNew('ADR', ['WORK'], [], SWorkAddressRegion, cfWorkAddressRegion, dtString, 4);
1277 AddNew('ADR', ['WORK'], [], SWorkAddressPostalCode, cfWorkAddressPostalCode, dtString, 5);
1278 AddNew('ADR', ['WORK'], [], SWorkAddressCountry, cfWorkAddressCountry, dtString, 6);
1279 AddNew('X-TIMES_CONTACTED', [], [], STimesContacted, cfXTimesContacted, dtString);
1280 AddNew('X-LAST_TIME_CONTACTED', [], [], SLastTimeContacted, cfXLastTimeContacted, dtString);
1281 AddNew('PHOTO', [], [], SPhoto, cfPhoto, dtImage);
1282 AddNew('LOGO', [], [], SLogo, cfLogo, dtImage);
1283 AddNew('BDAY', [], [], SDayOfBirth, cfDayOfBirth, dtDate);
1284 with AddNew('ANNIVERSARY', [], [], SAnniversary, cfAnniversary, dtDate) do
1285 AddAlternative('X-EVOLUTION-ANNIVERSARY', [], []);
1286 AddNew('REV', [], [], SRevision, cfRevision, dtString);
1287 AddNew('UID', [], [], SUniqueIdentifier, cfUid, dtString);
1288 AddNew('URL', [], ['HOME', 'WORK'], SWebAddress, cfUrl, dtString);
1289 AddNew('URL', ['HOME'], [], SWebAddressHome, cfUrlHome, dtString);
1290 AddNew('URL', ['WORK'], [], SWebAddressWork, cfUrlWork, dtString);
1291 with AddNew('GENDER', [], [], SGender, cfGender, dtString) do
1292 AddAlternative('X-CENTRUM-CZ-SEX', [], []);
1293 // Chat
1294 AddNew('X-MATRIX', [], [], SMatrix, cfMatrix, dtString);
1295 AddNew('X-JABBER', [], [], SJabber, cfJabber, dtString);
1296 AddNew('X-AIM', [], [], SAim, cfAim, dtString);
1297 AddNew('X-Windows Live', [], [], SWindowsLive, cfWindowsLive, dtString);
1298 AddNew('X-YAHOO', [], [], SYahoo, cfYahoo, dtString);
1299 with AddNew('X-SKYPE-USERNAME', [], [], SSkype, cfSkype, dtString) do begin
1300 AddAlternative('X-SKYPE', [], []);
1301 AddAlternative('X-CENTRUM-CZ-SKYPE', [], []);
1302 end;
1303 AddNew('X-QQ', [], [], SQq, cfQq, dtString);
1304 AddNew('X-GOOGLE-TALK', [], [], SGoogleTalk, cfGoogleTalk, dtString);
1305 with AddNew('X-ICQ', [], [], SIcq, cfIcq, dtString) do
1306 AddAlternative('X-CENTRUM-CZ-ICQ', [], []);
1307 AddNew('X-IRC', [], [], SIrc, cfIrc, dtString);
1308 with AddNew('X-MSN', [], [], SMsn, cfMsn, dtString) do
1309 AddAlternative('X-CENTRUM-CZ-MSN', [], []);
1310 AddNew('X-GROUPWISE', [], [], SGroupWise, cfGroupWise, dtString);
1311 AddNew('X-GADUGADU', [], [], SGaduGadu, cfGaduGadu, dtString);
1312 // Social
1313 with AddNew('X-TWITTER', [], [], STwitter, cfTwitter, dtString) do
1314 AddAlternative('X-SOCIALPROFILE', ['TWITTER'], []);
1315 with AddNew('X-FACEBOOK', [], [], SFacebook, cfFacebook, dtString) do
1316 AddAlternative('X-SOCIALPROFILE', ['FACEBOOK'], []);
1317 with AddNew('X-MASTODON', [], [], SMastodon, cfMastodon, dtString) do
1318 AddAlternative('X-SOCIALPROFILE', ['MASTODON'], []);
1319 with AddNew('X-YOUTUBE', [], [], SYouTube, cfYouTube, dtString) do
1320 AddAlternative('X-SOCIALPROFILE', ['YOUTUBE'], []);
1321 with AddNew('X-PEERTUBE', [], [], SPeerTube, cfPeerTube, dtString) do
1322 AddAlternative('X-SOCIALPROFILE', ['PEERTUBE'], []);
1323 with AddNew('X-LINKEDIN', [], [], SLinkedIn, cfLinkedIn, dtString) do
1324 AddAlternative('X-SOCIALPROFILE', ['LINKEDIN'], []);
1325 with AddNew('X-SNAPCHAT', [], [], SSnapchat, cfSnapchat, dtString) do
1326 AddAlternative('X-SOCIALPROFILE', ['SNAPCHAT'], []);
1327 with AddNew('X-INSTAGRAM', [], [], SInstagram, cfInstagram, dtString) do
1328 AddAlternative('X-SOCIALPROFILE', ['INSTAGRAM'], []);
1329 with AddNew('X-REDDIT', [], [], SReddit, cfReddit, dtString) do
1330 AddAlternative('X-SOCIALPROFILE', ['REDDIT'], []);
1331 with AddNew('X-MYSPACE', [], [], SMySpace, cfMySpace, dtString) do
1332 AddAlternative('X-SOCIALPROFILE', ['MYSPACE'], []);
1333 UpdateIndexes;
1334 end;
1335 end;
1336 Result := FFields;
1337end;
1338
1339function TContact.GetField(Index: TContactFieldIndex): string;
1340var
1341 Prop: TContactProperty;
1342 Field: TContactField;
1343begin
1344 if not Assigned(ContactsFile) then
1345 raise Exception.Create(SContactHasNoParent);
1346 Field := GetFields.GetByIndex(Index);
1347 if Assigned(Field) then begin
1348 Prop := GetProperty(Field);
1349 if Assigned(Prop) then begin
1350 if Field.ValueIndex <> -1 then begin
1351 Result := DecodeEscaped(Prop.ValueItem[Field.ValueIndex])
1352 end else begin
1353 if Field.DataType = dtString then Result := DecodeEscaped(Prop.Value)
1354 else Result := Prop.Value;
1355 end;
1356 end else Result := '';
1357 end else raise Exception.Create(SFieldIndexNotDefined);
1358end;
1359
1360procedure TContact.SetField(Index: TContactFieldIndex; AValue: string);
1361var
1362 Prop: TContactProperty;
1363 Field: TContactField;
1364 I: Integer;
1365begin
1366 if not Assigned(ContactsFile) then
1367 raise Exception.Create(SContactHasNoParent);
1368 Field := GetFields.GetByIndex(Index);
1369 if Assigned(Field) then begin
1370 Prop := GetProperty(Field);
1371 if (not Assigned(Prop)) and (AValue <> '') then begin
1372 Prop := TContactProperty.Create;
1373 Prop.Name := Field.SysName;
1374 for I := 0 to Length(Field.Groups) - 1 do
1375 Prop.Attributes.Add(Field.Groups[I]);
1376 Properties.Add(Prop);
1377 end;
1378 if Assigned(Prop) then begin
1379 if Field.ValueIndex <> -1 then begin
1380 Prop.ValueItem[Field.ValueIndex] := EncodeEscaped(AValue);
1381 end else begin
1382 if Field.DataType = dtString then Prop.Value := EncodeEscaped(AValue)
1383 else Prop.Value := AValue;
1384 end;
1385
1386 // Remove if empty
1387 if Prop.Value = '' then begin
1388 Properties.Remove(Prop);
1389 end;
1390 end;
1391 Modified := True;
1392 end else raise Exception.Create(SFieldIndexNotDefined);
1393end;
1394
1395procedure TContact.SetModified(AValue: Boolean);
1396begin
1397 if FModified = AValue then Exit;
1398 FModified := AValue;
1399 DoOnModify;
1400end;
1401
1402procedure TContact.DoOnModify;
1403begin
1404 if Assigned(FOnModify) then FOnModify(Self);
1405end;
1406
1407procedure TContact.DetectMaxLineLength(Text: string);
1408var
1409 LineLength: Integer;
1410begin
1411 LineLength := UTF8Length(Text);
1412 if LineLength > 1 then begin
1413 // Count one character less for folded line
1414 if Text[1] = ' ' then
1415 Dec(LineLength);
1416 end;
1417 if LineLength > ContactsFile.MaxLineLength then
1418 ContactsFile.MaxLineLength := LineLength;
1419end;
1420
1421function TContact.HasField(FieldIndex: TContactFieldIndex): Boolean;
1422var
1423 Field: TContactField;
1424begin
1425 if not Assigned(ContactsFile) then raise Exception.Create(SContactHasNoParent);
1426 Field := GetFields.GetByIndex(FieldIndex);
1427 if Assigned(Field) then begin
1428 Result := Assigned(GetProperty(Field));
1429 end else raise Exception.Create(SFieldIndexNotDefined);
1430end;
1431
1432function TContact.FullNameToFileName: string;
1433var
1434 I: Integer;
1435begin
1436 Result := Fields[cfFullName];
1437 for I := 1 to Length(Result) do begin
1438 if Result[I] in [':', '/', '\', '.', '"', '*', '|', '?', '<', '>'] then
1439 Result[I] := '_';
1440 end;
1441end;
1442
1443function TContact.GetProperty(Field: TContactField): TContactProperty;
1444var
1445 I: Integer;
1446begin
1447 Result := Properties.GetByNameGroups(Field.SysName, Field.Groups, Field.NoGroups);
1448 I := 0;
1449 while (not Assigned(Result)) and (I < Field.Alternatives.Count) do begin
1450 Result := Properties.GetByNameGroups(Field.Alternatives[I].SysName,
1451 Field.Alternatives[I].Groups, Field.Alternatives[I].NoGroups);
1452 if Assigned(Result) then Break;
1453 Inc(I);
1454 end;
1455end;
1456
1457function TContact.GetProperty(FieldIndex: TContactFieldIndex): TContactProperty;
1458var
1459 Field: TContactField;
1460begin
1461 if not Assigned(ContactsFile) then raise Exception.Create(SContactHasNoParent);
1462 Field := GetFields.GetByIndex(FieldIndex);
1463 if Assigned(Field) then begin
1464 Result := GetProperty(Field);
1465 end else Result := nil;
1466end;
1467
1468procedure TContact.Assign(Source: TContact);
1469begin
1470 Properties.Assign(Source.Properties);
1471 FModified := Source.FModified;
1472end;
1473
1474function TContact.UpdateFrom(Source: TContact): Boolean;
1475var
1476 I: Integer;
1477begin
1478 if not Assigned(ContactsFile) then raise Exception.Create(SContactHasNoParent);
1479 Result := False;
1480 for I := 0 to GetFields.Count - 1 do begin
1481 if (Source.Fields[GetFields[I].Index] <> '') and
1482 (Source.Fields[GetFields[I].Index] <>
1483 Fields[GetFields[I].Index]) then begin
1484 Result := True;
1485 Fields[GetFields[I].Index] := Source.Fields[GetFields[I].Index];
1486 end;
1487 end;
1488end;
1489
1490constructor TContact.Create;
1491begin
1492 Properties := TContactProperties.Create;
1493end;
1494
1495destructor TContact.Destroy;
1496begin
1497 FreeAndNil(Properties);
1498 inherited;
1499end;
1500
1501class destructor TContact.Destroy2;
1502begin
1503 FreeAndNil(FFields);
1504end;
1505
1506procedure TContact.SaveToStrings(Output: TStrings);
1507var
1508 I: Integer;
1509 NameText: string;
1510 Value2: string;
1511 LineIndex: Integer;
1512 OutText: string;
1513 CutText: string;
1514 LinePrefix: string;
1515 CutLength: Integer;
1516begin
1517 with Output do begin
1518 Add(VCardBegin);
1519 for I := 0 to Properties.Count - 1 do
1520 with Properties[I] do begin
1521 NameText := Name;
1522 if Attributes.Count > 0 then
1523 NameText := NameText + ';' + Attributes.DelimitedText;
1524 if Encoding <> '' then begin
1525 Value2 := GetEncodedValue;
1526 NameText := NameText + ';ENCODING=' + Encoding;
1527 end else Value2 := Value;
1528 if Pos(LineEnding, Value2) > 0 then begin
1529 Add(NameText + ':' + GetNext(Value2, LineEnding));
1530 while Pos(LineEnding, Value2) > 0 do begin
1531 Add(' ' + GetNext(Value2, LineEnding));
1532 end;
1533 Add(' ' + GetNext(Value2, LineEnding));
1534 Add('');
1535 end else begin
1536 OutText := NameText + ':' + Value2;
1537 LineIndex := 0;
1538 LinePrefix := '';
1539 while True do begin
1540 if UTF8Length(OutText) > ContactsFile.MaxLineLength then begin
1541 CutLength := ContactsFile.MaxLineLength;
1542 if Encoding = VCardQuotedPrintable then begin
1543 Dec(CutLength); // There will be softline break at the end
1544 // Do not cut encoded items at the end of line
1545 if ((CutLength - 1) >= 1) and (OutText[CutLength - 1] = QuotedPrintableEscapeCharacter) then
1546 Dec(CutLength, 2)
1547 else if OutText[CutLength] = QuotedPrintableEscapeCharacter then
1548 Dec(CutLength, 1);
1549 end;
1550
1551 CutText := UTF8Copy(OutText, 1, CutLength);
1552 System.Delete(OutText, 1, Length(CutText));
1553 if Encoding = VCardQuotedPrintable then
1554 CutText := CutText + QuotedPrintableEscapeCharacter; // Add soft line break
1555 Add(LinePrefix + CutText);
1556 if Encoding <> VCardQuotedPrintable then
1557 LinePrefix := ' ';
1558 Inc(LineIndex);
1559 Continue;
1560 end else begin
1561 Add(LinePrefix + OutText);
1562 Break;
1563 end;
1564 end;
1565 end;
1566 end;
1567 Add(VCardEnd);
1568 end;
1569end;
1570
1571function TContact.LoadFromStrings(Lines: TStrings; var StartLine: Integer): Boolean;
1572type
1573 TParseState = (psNone, psInside, psFinished);
1574var
1575 ParseState: TParseState;
1576 Line: string;
1577 Line2: string;
1578 Value: string;
1579 I: Integer;
1580 NewProperty: TContactProperty;
1581 CommandPart: string;
1582 Names: string;
1583 QuotedPrintableMultiLine: Boolean;
1584begin
1585 Result := False;
1586 ParseState := psNone;
1587 I := StartLine;
1588 while I < Lines.Count do begin
1589 Line := Lines[I];
1590 DetectMaxLineLength(Line);
1591
1592 if Line = '' then begin
1593 // Skip empty lines
1594 end else
1595 if ParseState = psNone then begin
1596 if Line = VCardBegin then begin
1597 ParseState := psInside;
1598 end else begin
1599 ContactsFile.Error(SExpectedVCardBegin, I + 1);
1600 Break;
1601 end;
1602 end else
1603 if ParseState = psInside then begin
1604 if Line = VCardEnd then begin
1605 ParseState := psFinished;
1606 Inc(I);
1607 Result := True;
1608 Break;
1609 end else
1610 if Pos(':', Line) > 0 then begin
1611 CommandPart := GetNext(Line, ':');
1612 Names := CommandPart;
1613 QuotedPrintableMultiLine := Pos('encoding=quoted-printable', LowerCase(CommandPart)) > 0;
1614 Value := Line;
1615 while True do begin
1616 Inc(I);
1617 if I >= Lines.Count then Break;
1618 Line2 := Lines[I];
1619 DetectMaxLineLength(Line2);
1620 if (Length(Line2) > 0) and (Line2[1] = ' ') then begin
1621 Value := Value + Copy(Line2, 2, MaxInt);
1622 end else
1623 if QuotedPrintableMultiLine and (Length(Value) > 0) and
1624 (Value[Length(Value)] = QuotedPrintableEscapeCharacter) then begin
1625 SetLength(Value, Length(Value) - 1);
1626 Value := Value + Line2;
1627 end else begin
1628 Dec(I);
1629 Break;
1630 end;
1631 end;
1632 NewProperty := Properties.GetByName(Names);
1633 if not Assigned(NewProperty) then begin
1634 NewProperty := TContactProperty.Create;
1635 Properties.Add(NewProperty);
1636 end;
1637 NewProperty.Attributes.DelimitedText := Names;
1638 if NewProperty.Attributes.Count > 0 then begin
1639 NewProperty.Name := NewProperty.Attributes[0];
1640 NewProperty.Attributes.Delete(0);
1641 end;
1642 NewProperty.Value := Value;
1643 NewProperty.EvaluateAttributes;
1644 end else begin
1645 ContactsFile.Error(SExpectedProperty, I + 1);
1646 Break;
1647 end;
1648 end;
1649 Inc(I);
1650 end;
1651 if Result then StartLine := I;
1652end;
1653
1654procedure TContact.SaveToFile(FileName: string);
1655var
1656 Lines: TStringList;
1657begin
1658 Lines := TStringList.Create;
1659 try
1660 SaveToStrings(Lines);
1661 Lines.SaveToFile(FileName);
1662 finally
1663 Lines.Free;
1664 end;
1665end;
1666
1667procedure TContact.LoadFromFile(FileName: string);
1668var
1669 Lines: TStringList;
1670 StartLine: Integer;
1671begin
1672 Lines := TStringList.Create;
1673 try
1674 Lines.LoadFromFile(FileName);
1675 {$IF FPC_FULLVERSION>=30200}
1676 if (Length(Lines.Text) > 0) and (Pos(VCardBegin, Lines.Text) = 0) then begin
1677 Lines.LoadFromFile(FileName, TEncoding.Unicode);
1678 if (Length(Lines.Text) > 0) and (Pos(VCardBegin, Lines.Text) = 0) then begin
1679 Lines.LoadFromFile(FileName, TEncoding.BigEndianUnicode);
1680 end;
1681 end;
1682 {$ENDIF}
1683 StartLine := 0;
1684 LoadFromStrings(Lines, StartLine);
1685 finally
1686 Lines.Free;
1687 end;
1688end;
1689
1690{ TContactsFile }
1691
1692procedure TContactsFile.Error(Text: string; Line: Integer);
1693begin
1694 if Assigned(FOnError) then FOnError(Text, Line);
1695end;
1696
1697function TContactsFile.GetFileName: string;
1698begin
1699 Result := SVCardFile;
1700end;
1701
1702function TContactsFile.GetFileExt: string;
1703begin
1704 Result := VCardFileExt;
1705end;
1706
1707function TContactsFile.GetFileFilter: string;
1708begin
1709 Result := GetFileName + ' (' + GetFileExt + ')|*' + GetFileExt + '|' + inherited;
1710end;
1711
1712procedure TContactsFile.SaveToStrings(Output: TStrings);
1713var
1714 I: Integer;
1715begin
1716 for I := 0 to Contacts.Count - 1 do
1717 Contacts[I].SaveToStrings(Output);
1718end;
1719
1720procedure TContactsFile.LoadFromStrings(Lines: TStrings);
1721var
1722 Contact: TContact;
1723 I: Integer;
1724begin
1725 Contacts.Clear;
1726 //MaxLineLength := 10;
1727
1728 I := 0;
1729 while I < Lines.Count do begin
1730 Contact := TContact.Create;
1731 Contact.ContactsFile := Self;
1732 if Contact.LoadFromStrings(Lines, I) then begin
1733 Contacts.Add(Contact);
1734 end else begin
1735 FreeAndNil(Contact);
1736 Inc(I);
1737 end;
1738 end;
1739end;
1740
1741function TContactsFile.NewItem(Key, Value: string): string;
1742var
1743 Charset: string;
1744begin
1745 if not IsAsciiString(Value) then Charset := ';CHARSET=UTF-8'
1746 else Charset := '';
1747 Result := Key + Charset + ':' + Value;
1748end;
1749
1750procedure TContactsFile.SaveToFile(FileName: string);
1751var
1752 Lines: TStringList;
1753begin
1754 inherited;
1755 Lines := TStringList.Create;
1756 try
1757 SaveToStrings(Lines);
1758 Lines.SaveToFile(FileName);
1759 finally
1760 Lines.Free;
1761 end
1762end;
1763
1764procedure TContactsFile.LoadFromFile(FileName: string);
1765var
1766 Lines: TStringList;
1767begin
1768 inherited;
1769 Lines := TStringList.Create;
1770 Lines.LoadFromFile(FileName);
1771 {$IF FPC_FULLVERSION>=30200}
1772 if (Length(Lines.Text) > 0) and (Pos(VCardBegin, Lines.Text) = 0) then begin
1773 Lines.LoadFromFile(FileName, TEncoding.Unicode);
1774 if (Length(Lines.Text) > 0) and (Pos(VCardBegin, Lines.Text) = 0) then begin
1775 Lines.LoadFromFile(FileName, TEncoding.BigEndianUnicode);
1776 end;
1777 end;
1778 {$ENDIF}
1779 try
1780 LoadFromStrings(Lines);
1781 finally
1782 Lines.Free;
1783 end;
1784end;
1785
1786constructor TContactsFile.Create;
1787begin
1788 inherited;
1789 Contacts := TContacts.Create;
1790 Contacts.ContactsFile := Self;
1791 MaxLineLength := DefaultMaxLineLength;
1792end;
1793
1794destructor TContactsFile.Destroy;
1795begin
1796 FreeAndNil(Contacts);
1797 inherited;
1798end;
1799
1800end.
1801
Note: See TracBrowser for help on using the repository browser.