1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 package davmail.exchange;
20
21 import davmail.BundleMessage;
22 import davmail.Settings;
23 import davmail.exception.DavMailException;
24 import davmail.exception.HttpNotFoundException;
25 import davmail.http.URIUtil;
26 import davmail.ui.NotificationDialog;
27 import davmail.util.StringUtil;
28 import org.apache.log4j.Logger;
29
30 import javax.mail.MessagingException;
31 import javax.mail.internet.InternetAddress;
32 import javax.mail.internet.InternetHeaders;
33 import javax.mail.internet.MimeMessage;
34 import javax.mail.internet.MimeMultipart;
35 import javax.mail.internet.MimePart;
36 import javax.mail.util.SharedByteArrayInputStream;
37 import java.io.ByteArrayOutputStream;
38 import java.io.File;
39 import java.io.IOException;
40 import java.io.InputStream;
41 import java.io.OutputStreamWriter;
42 import java.io.StringReader;
43 import java.net.NoRouteToHostException;
44 import java.net.UnknownHostException;
45 import java.nio.charset.StandardCharsets;
46 import java.nio.file.Files;
47 import java.nio.file.Paths;
48 import java.text.ParseException;
49 import java.text.SimpleDateFormat;
50 import java.util.ArrayList;
51 import java.util.Arrays;
52 import java.util.Calendar;
53 import java.util.Collections;
54 import java.util.Date;
55 import java.util.Enumeration;
56 import java.util.HashMap;
57 import java.util.HashSet;
58 import java.util.List;
59 import java.util.Locale;
60 import java.util.Map;
61 import java.util.Properties;
62 import java.util.ResourceBundle;
63 import java.util.Set;
64 import java.util.SimpleTimeZone;
65 import java.util.TimeZone;
66 import java.util.TreeMap;
67 import java.util.UUID;
68
69
70
71
72 public abstract class ExchangeSession {
73
74 protected static final Logger LOGGER = Logger.getLogger("davmail.exchange.ExchangeSession");
75
76
77
78
79 public static final SimpleTimeZone GMT_TIMEZONE = new SimpleTimeZone(0, "GMT");
80
81 protected static final int FREE_BUSY_INTERVAL = 15;
82
83 protected static final String PUBLIC_ROOT = "/public/";
84 protected static final String CALENDAR = "calendar";
85 protected static final String TASKS = "tasks";
86
87
88
89 public static final String CONTACTS = "contacts";
90 protected static final String ADDRESSBOOK = "addressbook";
91 protected static final String ARCHIVE = "Archive";
92 protected static final String INBOX = "INBOX";
93 protected static final String LOWER_CASE_INBOX = "inbox";
94 protected static final String MIXED_CASE_INBOX = "Inbox";
95 protected static final String SENT = "Sent";
96 protected static final String SENDMSG = "##DavMailSubmissionURI##";
97 protected static final String DRAFTS = "Drafts";
98 protected static final String TRASH = "Trash";
99 protected static final String JUNK = "Junk";
100 protected static final String UNSENT = "Unsent Messages";
101
102 protected static final List<String> SPECIAL = Arrays.asList(SENT, DRAFTS, TRASH, JUNK);
103
104 static {
105
106 System.setProperty("mail.mime.ignoreunknownencoding", "true");
107 System.setProperty("mail.mime.decodetext.strict", "false");
108 }
109
110 protected String publicFolderUrl;
111
112
113
114
115 protected String mailPath;
116 protected String rootPath;
117 protected String email;
118 protected String alias;
119
120
121
122
123 protected String currentMailboxPath;
124
125 protected String userName;
126
127 protected String serverVersion;
128
129 protected static final String YYYY_MM_DD_HH_MM_SS = "yyyy/MM/dd HH:mm:ss";
130 private static final String YYYYMMDD_T_HHMMSS_Z = "yyyyMMdd'T'HHmmss'Z'";
131 protected static final String YYYY_MM_DD_T_HHMMSS_Z = "yyyy-MM-dd'T'HH:mm:ss'Z'";
132 private static final String YYYY_MM_DD = "yyyy-MM-dd";
133 private static final String YYYY_MM_DD_T_HHMMSS_SSS_Z = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'";
134
135 public ExchangeSession() {
136
137 }
138
139
140
141
142
143 public abstract void close();
144
145
146
147
148
149
150
151 public abstract String formatSearchDate(Date date);
152
153
154
155
156
157
158 public static SimpleDateFormat getZuluDateFormat() {
159 SimpleDateFormat dateFormat = new SimpleDateFormat(YYYYMMDD_T_HHMMSS_Z, Locale.ENGLISH);
160 dateFormat.setTimeZone(GMT_TIMEZONE);
161 return dateFormat;
162 }
163
164 protected static SimpleDateFormat getVcardBdayFormat() {
165 SimpleDateFormat dateFormat = new SimpleDateFormat(YYYY_MM_DD, Locale.ENGLISH);
166 dateFormat.setTimeZone(GMT_TIMEZONE);
167 return dateFormat;
168 }
169
170 protected static SimpleDateFormat getExchangeDateFormat(String value) {
171 SimpleDateFormat dateFormat;
172 if (value.length() == 8) {
173 dateFormat = new SimpleDateFormat("yyyyMMdd", Locale.ENGLISH);
174 dateFormat.setTimeZone(GMT_TIMEZONE);
175 } else if (value.length() == 15) {
176 dateFormat = new SimpleDateFormat("yyyyMMdd'T'HHmmss", Locale.ENGLISH);
177 dateFormat.setTimeZone(GMT_TIMEZONE);
178 } else if (value.length() == 16) {
179 dateFormat = new SimpleDateFormat("yyyyMMdd'T'HHmmss'Z'", Locale.ENGLISH);
180 dateFormat.setTimeZone(GMT_TIMEZONE);
181 } else {
182 dateFormat = ExchangeSession.getExchangeZuluDateFormat();
183 }
184 return dateFormat;
185 }
186
187 protected static SimpleDateFormat getExchangeZuluDateFormat() {
188 SimpleDateFormat dateFormat = new SimpleDateFormat(YYYY_MM_DD_T_HHMMSS_Z, Locale.ENGLISH);
189 dateFormat.setTimeZone(GMT_TIMEZONE);
190 return dateFormat;
191 }
192
193 protected static SimpleDateFormat getExchangeZuluDateFormatMillisecond() {
194 SimpleDateFormat dateFormat = new SimpleDateFormat(YYYY_MM_DD_T_HHMMSS_SSS_Z, Locale.ENGLISH);
195 dateFormat.setTimeZone(GMT_TIMEZONE);
196 return dateFormat;
197 }
198
199 protected static Date parseDate(String dateString) throws ParseException {
200 SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMdd");
201 dateFormat.setTimeZone(GMT_TIMEZONE);
202 return dateFormat.parse(dateString);
203 }
204
205
206
207
208
209
210
211
212
213 public boolean isExpired() throws NoRouteToHostException, UnknownHostException {
214 boolean isExpired = false;
215 try {
216 getFolder("");
217 } catch (UnknownHostException | NoRouteToHostException exc) {
218 throw exc;
219 } catch (IOException e) {
220 isExpired = true;
221 }
222
223 return isExpired;
224 }
225
226 protected abstract void buildSessionInfo(java.net.URI uri) throws IOException;
227
228
229
230
231
232
233
234
235
236
237
238 public abstract Message createMessage(String folderPath, String messageName, HashMap<String, String> properties, MimeMessage mimeMessage) throws IOException;
239
240
241
242
243
244
245
246
247 public abstract void updateMessage(Message message, Map<String, String> properties) throws IOException;
248
249
250
251
252
253
254
255
256 public abstract void deleteMessage(Message message) throws IOException;
257
258
259
260
261
262
263
264
265 protected abstract byte[] getContent(Message message) throws IOException;
266
267 protected static final Set<String> POP_MESSAGE_ATTRIBUTES = new HashSet<>();
268
269 static {
270 POP_MESSAGE_ATTRIBUTES.add("uid");
271 POP_MESSAGE_ATTRIBUTES.add("imapUid");
272 POP_MESSAGE_ATTRIBUTES.add("messageSize");
273 }
274
275
276
277
278
279
280
281
282 public MessageList getAllMessageUidAndSize(String folderName) throws IOException {
283 return searchMessages(folderName, POP_MESSAGE_ATTRIBUTES, null);
284 }
285
286 protected static final Set<String> IMAP_MESSAGE_ATTRIBUTES = new HashSet<>();
287
288 static {
289 IMAP_MESSAGE_ATTRIBUTES.add("permanenturl");
290 IMAP_MESSAGE_ATTRIBUTES.add("urlcompname");
291 IMAP_MESSAGE_ATTRIBUTES.add("uid");
292 IMAP_MESSAGE_ATTRIBUTES.add("messageSize");
293 IMAP_MESSAGE_ATTRIBUTES.add("imapUid");
294 IMAP_MESSAGE_ATTRIBUTES.add("junk");
295 IMAP_MESSAGE_ATTRIBUTES.add("flagStatus");
296 IMAP_MESSAGE_ATTRIBUTES.add("messageFlags");
297 IMAP_MESSAGE_ATTRIBUTES.add("lastVerbExecuted");
298 IMAP_MESSAGE_ATTRIBUTES.add("read");
299 IMAP_MESSAGE_ATTRIBUTES.add("deleted");
300 IMAP_MESSAGE_ATTRIBUTES.add("date");
301 IMAP_MESSAGE_ATTRIBUTES.add("lastmodified");
302
303 IMAP_MESSAGE_ATTRIBUTES.add("contentclass");
304 IMAP_MESSAGE_ATTRIBUTES.add("keywords");
305 }
306
307 protected static final Set<String> UID_MESSAGE_ATTRIBUTES = new HashSet<>();
308
309 static {
310 UID_MESSAGE_ATTRIBUTES.add("uid");
311 }
312
313
314
315
316
317
318
319
320 public MessageList searchMessages(String folderPath) throws IOException {
321 return searchMessages(folderPath, IMAP_MESSAGE_ATTRIBUTES, null);
322 }
323
324
325
326
327
328
329
330
331
332 public MessageList searchMessages(String folderName, Condition condition) throws IOException {
333 return searchMessages(folderName, IMAP_MESSAGE_ATTRIBUTES, condition);
334 }
335
336
337
338
339
340
341
342
343
344
345 public abstract MessageList searchMessages(String folderName, Set<String> attributes, Condition condition) throws IOException;
346
347
348
349
350
351
352 public String getServerVersion() {
353 return serverVersion;
354 }
355
356 public enum Operator {
357 Or, And, Not, IsEqualTo,
358 IsGreaterThan, IsGreaterThanOrEqualTo,
359 IsLessThan, IsLessThanOrEqualTo,
360 IsNull, IsTrue, IsFalse,
361 Like, StartsWith, Contains
362 }
363
364
365
366
367 public interface Condition {
368
369
370
371
372
373 void appendTo(StringBuilder buffer);
374
375
376
377
378
379
380 boolean isEmpty();
381
382
383
384
385
386
387
388 boolean isMatch(ExchangeSession.Contact contact);
389 }
390
391
392
393
394 public abstract static class AttributeCondition implements Condition {
395 protected final String attributeName;
396 protected final Operator operator;
397 protected final String value;
398
399 protected AttributeCondition(String attributeName, Operator operator, String value) {
400 this.attributeName = attributeName;
401 this.operator = operator;
402 this.value = value;
403 }
404
405 public boolean isEmpty() {
406 return false;
407 }
408
409
410
411
412
413
414 public String getAttributeName() {
415 return attributeName;
416 }
417
418
419
420
421
422
423 public String getValue() {
424 return value;
425 }
426
427 }
428
429
430
431
432 public abstract static class MultiCondition implements Condition {
433 protected final Operator operator;
434 protected final List<Condition> conditions;
435
436 protected MultiCondition(Operator operator, Condition... conditions) {
437 this.operator = operator;
438 this.conditions = new ArrayList<>();
439 for (Condition condition : conditions) {
440 if (condition != null) {
441 this.conditions.add(condition);
442 }
443 }
444 }
445
446
447
448
449
450
451 public List<Condition> getConditions() {
452 return conditions;
453 }
454
455
456
457
458
459
460 public Operator getOperator() {
461 return operator;
462 }
463
464
465
466
467
468
469 public void add(Condition condition) {
470 if (condition != null) {
471 conditions.add(condition);
472 }
473 }
474
475 public boolean isEmpty() {
476 boolean isEmpty = true;
477 for (Condition condition : conditions) {
478 if (!condition.isEmpty()) {
479 isEmpty = false;
480 break;
481 }
482 }
483 return isEmpty;
484 }
485
486 public boolean isMatch(ExchangeSession.Contact contact) {
487 if (operator == Operator.And) {
488 for (Condition condition : conditions) {
489 if (!condition.isMatch(contact)) {
490 return false;
491 }
492 }
493 return true;
494 } else if (operator == Operator.Or) {
495 for (Condition condition : conditions) {
496 if (condition.isMatch(contact)) {
497 return true;
498 }
499 }
500 return false;
501 } else {
502 return false;
503 }
504 }
505
506 }
507
508
509
510
511 public abstract static class NotCondition implements Condition {
512 protected final Condition condition;
513
514 protected NotCondition(Condition condition) {
515 this.condition = condition;
516 }
517
518 public boolean isEmpty() {
519 return condition.isEmpty();
520 }
521
522 public boolean isMatch(ExchangeSession.Contact contact) {
523 return !condition.isMatch(contact);
524 }
525 }
526
527
528
529
530 public abstract static class MonoCondition implements Condition {
531 protected final String attributeName;
532 protected final Operator operator;
533
534 protected MonoCondition(String attributeName, Operator operator) {
535 this.attributeName = attributeName;
536 this.operator = operator;
537 }
538
539 public boolean isEmpty() {
540 return false;
541 }
542
543 public boolean isMatch(ExchangeSession.Contact contact) {
544 String actualValue = contact.get(attributeName);
545 return (operator == Operator.IsNull && actualValue == null) ||
546 (operator == Operator.IsFalse && "false".equals(actualValue)) ||
547 (operator == Operator.IsTrue && "true".equals(actualValue));
548 }
549 }
550
551
552
553
554
555
556
557 public abstract MultiCondition and(Condition... condition);
558
559
560
561
562
563
564
565 public abstract MultiCondition or(Condition... condition);
566
567
568
569
570
571
572
573 public abstract Condition not(Condition condition);
574
575
576
577
578
579
580
581
582 public abstract Condition isEqualTo(String attributeName, String value);
583
584
585
586
587
588
589
590
591 public abstract Condition isEqualTo(String attributeName, int value);
592
593
594
595
596
597
598
599
600 public abstract Condition headerIsEqualTo(String headerName, String value);
601
602
603
604
605
606
607
608
609 public abstract Condition gte(String attributeName, String value);
610
611
612
613
614
615
616
617
618 public abstract Condition gt(String attributeName, String value);
619
620
621
622
623
624
625
626
627 public abstract Condition lt(String attributeName, String value);
628
629
630
631
632
633
634
635
636 @SuppressWarnings({"UnusedDeclaration"})
637 public abstract Condition lte(String attributeName, String value);
638
639
640
641
642
643
644
645
646 public abstract Condition contains(String attributeName, String value);
647
648
649
650
651
652
653
654
655 public abstract Condition startsWith(String attributeName, String value);
656
657
658
659
660
661
662
663 public abstract Condition isNull(String attributeName);
664
665
666
667
668
669
670
671 public abstract Condition exists(String attributeName);
672
673
674
675
676
677
678
679 public abstract Condition isTrue(String attributeName);
680
681
682
683
684
685
686
687 public abstract Condition isFalse(String attributeName);
688
689
690
691
692
693
694
695
696
697
698 public List<Folder> getSubFolders(String folderName, boolean recursive, boolean wildcard) throws IOException {
699 MultiCondition folderCondition = and();
700 if (!Settings.getBooleanProperty("davmail.imapIncludeSpecialFolders", false)) {
701 folderCondition.add(or(isEqualTo("folderclass", "IPF.Note"),
702 isEqualTo("folderclass", "IPF.Note.Microsoft.Conversation"),
703 isNull("folderclass")));
704 }
705 if (wildcard) {
706 folderCondition.add(startsWith("displayname", folderName));
707 folderName = "";
708 }
709 List<Folder> results = getSubFolders(folderName, folderCondition,
710 recursive);
711
712 if (recursive && !folderName.isEmpty()) {
713 results.add(getFolder(folderName));
714 }
715
716 return results;
717 }
718
719
720
721
722
723
724
725
726
727 public List<Folder> getSubCalendarFolders(String folderName, boolean recursive) throws IOException {
728 return getSubFolders(folderName, isEqualTo("folderclass", "IPF.Appointment"), recursive);
729 }
730
731
732
733
734
735
736
737
738
739
740 public abstract List<Folder> getSubFolders(String folderName, Condition condition, boolean recursive) throws IOException;
741
742
743
744
745
746
747
748 public void purgeOldestTrashAndSentMessages() throws IOException {
749 int keepDelay = Settings.getIntProperty("davmail.keepDelay");
750 if (keepDelay != 0) {
751 purgeOldestFolderMessages(TRASH, keepDelay);
752 }
753
754 int sentKeepDelay = Settings.getIntProperty("davmail.sentKeepDelay");
755 if (sentKeepDelay != 0) {
756 purgeOldestFolderMessages(SENT, sentKeepDelay);
757 }
758 }
759
760 protected void purgeOldestFolderMessages(String folderPath, int keepDelay) throws IOException {
761 Calendar cal = Calendar.getInstance();
762 cal.add(Calendar.DAY_OF_MONTH, -keepDelay);
763 LOGGER.debug("Delete messages in " + folderPath + " not modified since " + cal.getTime());
764
765 MessageList messages = searchMessages(folderPath, UID_MESSAGE_ATTRIBUTES,
766 lt("lastmodified", formatSearchDate(cal.getTime())));
767
768 for (Message message : messages) {
769 message.delete();
770 }
771 }
772
773 protected void convertResentHeader(MimeMessage mimeMessage, String headerName) throws MessagingException {
774 String[] resentHeader = mimeMessage.getHeader("Resent-" + headerName);
775 if (resentHeader != null) {
776 mimeMessage.removeHeader("Resent-" + headerName);
777 mimeMessage.removeHeader(headerName);
778 for (String value : resentHeader) {
779 mimeMessage.addHeader(headerName, value);
780 }
781 }
782 }
783
784 protected String lastSentMessageId;
785
786
787
788
789
790
791
792
793
794
795 public void sendMessage(List<String> rcptToRecipients, MimeMessage mimeMessage) throws IOException, MessagingException {
796
797 String messageId = mimeMessage.getMessageID();
798 if (lastSentMessageId != null && lastSentMessageId.equals(messageId)) {
799 LOGGER.debug("Dropping message id " + messageId + ": already sent");
800 return;
801 }
802 lastSentMessageId = messageId;
803
804 convertResentHeader(mimeMessage, "From");
805 convertResentHeader(mimeMessage, "To");
806 convertResentHeader(mimeMessage, "Cc");
807 convertResentHeader(mimeMessage, "Bcc");
808 convertResentHeader(mimeMessage, "Message-Id");
809
810
811 if ("Exchange2003".equals(serverVersion) || Settings.getBooleanProperty("davmail.smtpStripFrom", false)) {
812 mimeMessage.removeHeader("From");
813 }
814
815
816 Set<String> visibleRecipients = new HashSet<>();
817 List<InternetAddress> recipients = getAllRecipients(mimeMessage);
818 for (InternetAddress address : recipients) {
819 visibleRecipients.add((address.getAddress().toLowerCase()));
820 }
821 for (String recipient : rcptToRecipients) {
822 if (!visibleRecipients.contains(recipient.toLowerCase())) {
823 mimeMessage.addRecipient(javax.mail.Message.RecipientType.BCC, new InternetAddress(recipient));
824 }
825 }
826 sendMessage(mimeMessage);
827
828 }
829
830 static final String[] RECIPIENT_HEADERS = {"to", "cc", "bcc"};
831
832 protected List<InternetAddress> getAllRecipients(MimeMessage mimeMessage) throws MessagingException {
833 List<InternetAddress> recipientList = new ArrayList<>();
834 for (String recipientHeader : RECIPIENT_HEADERS) {
835 final String recipientHeaderValue = mimeMessage.getHeader(recipientHeader, ",");
836 if (recipientHeaderValue != null) {
837
838 recipientList.addAll(Arrays.asList(InternetAddress.parseHeader(recipientHeaderValue, false)));
839 }
840
841 }
842 return recipientList;
843 }
844
845
846
847
848
849
850
851
852 public abstract void sendMessage(MimeMessage mimeMessage) throws IOException, MessagingException;
853
854
855
856
857
858
859
860
861
862
863 public ExchangeSession.Folder getFolder(String folderPath) throws IOException {
864 Folder folder = internalGetFolder(folderPath);
865 if (isMainCalendar(folderPath)) {
866 Folder taskFolder = internalGetFolder(TASKS);
867 folder.ctag += taskFolder.ctag;
868 }
869 return folder;
870 }
871
872 protected abstract Folder internalGetFolder(String folderName) throws IOException;
873
874
875
876
877
878
879
880
881 public boolean refreshFolder(Folder currentFolder) throws IOException {
882 Folder newFolder = getFolder(currentFolder.folderPath);
883 if (currentFolder.ctag == null || !currentFolder.ctag.equals(newFolder.ctag)
884
885 || !(currentFolder.messageCount == newFolder.messageCount)
886 ) {
887 if (LOGGER.isDebugEnabled()) {
888 LOGGER.debug("Contenttag or count changed on " + currentFolder.folderPath +
889 " ctag: " + currentFolder.ctag + " => " + newFolder.ctag +
890 " count: " + currentFolder.messageCount + " => " + newFolder.messageCount
891 + ", reloading messages");
892 }
893 currentFolder.hasChildren = newFolder.hasChildren;
894 currentFolder.noInferiors = newFolder.noInferiors;
895 currentFolder.unreadCount = newFolder.unreadCount;
896 currentFolder.ctag = newFolder.ctag;
897 currentFolder.etag = newFolder.etag;
898 if (newFolder.uidNext > currentFolder.uidNext) {
899 currentFolder.uidNext = newFolder.uidNext;
900 }
901 currentFolder.loadMessages();
902 return true;
903 } else {
904 return false;
905 }
906 }
907
908
909
910
911
912
913
914 public void createMessageFolder(String folderName) throws IOException {
915 createFolder(folderName, "IPF.Note", null);
916 }
917
918
919
920
921
922
923
924
925
926 public int createCalendarFolder(String folderName, Map<String, String> properties) throws IOException {
927 return createFolder(folderName, "IPF.Appointment", properties);
928 }
929
930
931
932
933
934
935
936
937 public void createContactFolder(String folderName, Map<String, String> properties) throws IOException {
938 createFolder(folderName, "IPF.Contact", properties);
939 }
940
941
942
943
944
945
946
947
948
949
950 public abstract int createFolder(String folderName, String folderClass, Map<String, String> properties) throws IOException;
951
952
953
954
955
956
957
958
959
960 public abstract int updateFolder(String folderName, Map<String, String> properties) throws IOException;
961
962
963
964
965
966
967
968 public abstract void deleteFolder(String folderName) throws IOException;
969
970
971
972
973
974
975
976
977 public abstract void copyMessage(Message message, String targetFolder) throws IOException;
978
979 public void copyMessages(List<Message> messages, String targetFolder) throws IOException {
980 for (Message message : messages) {
981 copyMessage(message, targetFolder);
982 }
983 }
984
985
986
987
988
989
990
991
992
993 public abstract void moveMessage(Message message, String targetFolder) throws IOException;
994
995 public void moveMessages(List<Message> messages, String targetFolder) throws IOException {
996 for (Message message : messages) {
997 moveMessage(message, targetFolder);
998 }
999 }
1000
1001
1002
1003
1004
1005
1006
1007
1008 public abstract void moveFolder(String folderName, String targetName) throws IOException;
1009
1010
1011
1012
1013
1014
1015
1016
1017 public abstract void moveItem(String sourcePath, String targetPath) throws IOException;
1018
1019 protected abstract void moveToTrash(Message message) throws IOException;
1020
1021
1022
1023
1024
1025
1026
1027 public String convertKeywordToFlag(String value) {
1028
1029 Properties flagSettings = Settings.getSubProperties("davmail.imapFlags");
1030 Enumeration<?> flagSettingsEnum = flagSettings.propertyNames();
1031 while (flagSettingsEnum.hasMoreElements()) {
1032 String key = (String) flagSettingsEnum.nextElement();
1033 if (value.equalsIgnoreCase(flagSettings.getProperty(key))) {
1034 return key;
1035 }
1036 }
1037
1038 ResourceBundle flagBundle = ResourceBundle.getBundle("imapflags");
1039 Enumeration<String> flagBundleEnum = flagBundle.getKeys();
1040 while (flagBundleEnum.hasMoreElements()) {
1041 String key = flagBundleEnum.nextElement();
1042 if (value.equalsIgnoreCase(flagBundle.getString(key))) {
1043 return key;
1044 }
1045 }
1046
1047
1048 return value;
1049 }
1050
1051
1052
1053
1054
1055
1056
1057 public String convertFlagToKeyword(String value) {
1058
1059 Properties flagSettings = Settings.getSubProperties("davmail.imapFlags");
1060
1061 for (String key : flagSettings.stringPropertyNames()) {
1062 if (key.equalsIgnoreCase(value)) {
1063 return flagSettings.getProperty(key);
1064 }
1065 }
1066
1067
1068 ResourceBundle flagBundle = ResourceBundle.getBundle("imapflags");
1069 for (String key : flagBundle.keySet()) {
1070 if (key.equalsIgnoreCase(value)) {
1071 return flagBundle.getString(key);
1072 }
1073 }
1074
1075
1076 return value;
1077 }
1078
1079
1080
1081
1082
1083
1084
1085 public String convertFlagsToKeywords(HashSet<String> flags) {
1086 HashSet<String> keywordSet = new HashSet<>();
1087 for (String flag : flags) {
1088 keywordSet.add(decodeKeyword(convertFlagToKeyword(flag)));
1089 }
1090 return StringUtil.join(keywordSet, ",");
1091 }
1092
1093 protected String decodeKeyword(String keyword) {
1094 String result = keyword;
1095 if (keyword.contains("_x0028_") || keyword.contains("_x0029_")) {
1096 result = result.replaceAll("_x0028_", "(")
1097 .replaceAll("_x0029_", ")");
1098 }
1099 return result;
1100 }
1101
1102 protected String encodeKeyword(String keyword) {
1103 String result = keyword;
1104 if (keyword.indexOf('(') >= 0|| keyword.indexOf(')') >= 0) {
1105 result = result.replaceAll("\\(", "_x0028_")
1106 .replaceAll("\\)", "_x0029_" );
1107 }
1108 return result;
1109 }
1110
1111
1112
1113
1114 public class Folder {
1115
1116
1117
1118 public String folderPath;
1119
1120
1121
1122
1123 public String displayName;
1124
1125
1126
1127 public String folderClass;
1128
1129
1130
1131 public int messageCount;
1132
1133
1134
1135 public int unreadCount;
1136
1137
1138
1139 public boolean hasChildren;
1140
1141
1142
1143 public boolean noInferiors;
1144
1145
1146
1147 public String ctag;
1148
1149
1150
1151 public String etag;
1152
1153
1154
1155 public long uidNext;
1156
1157
1158
1159 public int recent;
1160
1161
1162
1163
1164 public ExchangeSession.MessageList messages;
1165
1166
1167
1168 private final HashMap<String, Long> permanentUrlToImapUidMap = new HashMap<>();
1169
1170
1171
1172
1173
1174
1175 public String getFlags() {
1176 String specialFlag = "";
1177 if (isSpecial()) {
1178 specialFlag = "\\" + folderPath + " ";
1179 }
1180 if (noInferiors) {
1181 return specialFlag + "\\NoInferiors";
1182 } else if (hasChildren) {
1183 return specialFlag + "\\HasChildren";
1184 } else {
1185 return specialFlag + "\\HasNoChildren";
1186 }
1187 }
1188
1189
1190
1191
1192
1193 public boolean isSpecial() {
1194 return SPECIAL.contains(folderPath);
1195 }
1196
1197
1198
1199
1200
1201
1202 public void loadMessages() throws IOException {
1203 messages = ExchangeSession.this.searchMessages(folderPath, null);
1204 fixUids(messages);
1205 recent = 0;
1206 for (Message message : messages) {
1207 if (message.recent) {
1208 recent++;
1209 }
1210 }
1211 long computedUidNext = 1;
1212 if (!messages.isEmpty()) {
1213 computedUidNext = messages.get(messages.size() - 1).getImapUid() + 1;
1214 }
1215 if (computedUidNext > uidNext) {
1216 uidNext = computedUidNext;
1217 }
1218 }
1219
1220
1221
1222
1223
1224
1225
1226
1227 public MessageList searchMessages(Condition condition) throws IOException {
1228 MessageList localMessages = ExchangeSession.this.searchMessages(folderPath, condition);
1229 fixUids(localMessages);
1230 return localMessages;
1231 }
1232
1233
1234
1235
1236
1237
1238 protected void fixUids(MessageList messages) {
1239 boolean sortNeeded = false;
1240 for (Message message : messages) {
1241 if (permanentUrlToImapUidMap.containsKey(message.getPermanentId())) {
1242 long previousUid = permanentUrlToImapUidMap.get(message.getPermanentId());
1243 if (message.getImapUid() != previousUid) {
1244 LOGGER.debug("Restoring IMAP uid " + message.getImapUid() + " -> " + previousUid + " for message " + message.getPermanentId());
1245 message.setImapUid(previousUid);
1246 sortNeeded = true;
1247 }
1248 } else {
1249
1250 permanentUrlToImapUidMap.put(message.getPermanentId(), message.getImapUid());
1251 }
1252 }
1253 if (sortNeeded) {
1254 Collections.sort(messages);
1255 }
1256 }
1257
1258
1259
1260
1261
1262
1263 public int count() {
1264 if (messages == null) {
1265 return messageCount;
1266 } else {
1267 return messages.size();
1268 }
1269 }
1270
1271
1272
1273
1274
1275
1276 public long getUidNext() {
1277 return uidNext;
1278 }
1279
1280
1281
1282
1283
1284
1285
1286 public Message get(int index) {
1287 return messages.get(index);
1288 }
1289
1290
1291
1292
1293
1294
1295 public TreeMap<Long, String> getImapFlagMap() {
1296 TreeMap<Long, String> imapFlagMap = new TreeMap<>();
1297 for (ExchangeSession.Message message : messages) {
1298 imapFlagMap.put(message.getImapUid(), message.getImapFlags());
1299 }
1300 return imapFlagMap;
1301 }
1302
1303
1304
1305
1306
1307
1308 public boolean isCalendar() {
1309 return "IPF.Appointment".equals(folderClass);
1310 }
1311
1312
1313
1314
1315
1316
1317 public boolean isContact() {
1318 return "IPF.Contact".equals(folderClass);
1319 }
1320
1321
1322
1323
1324
1325
1326 public boolean isTask() {
1327 return "IPF.Task".equals(folderClass);
1328 }
1329
1330
1331
1332
1333 public void clearCache() {
1334 messages.cachedMimeContent = null;
1335 messages.cachedMimeMessage = null;
1336 messages.cachedMessageImapUid = 0;
1337 }
1338 }
1339
1340
1341
1342
1343 public abstract class Message implements Comparable<Message> {
1344
1345
1346
1347 public MessageList messageList;
1348
1349
1350
1351 public String messageUrl;
1352
1353
1354
1355 public String permanentUrl;
1356
1357
1358
1359 public String uid;
1360
1361
1362
1363 public String contentClass;
1364
1365
1366
1367 public String keywords;
1368
1369
1370
1371 public long imapUid;
1372
1373
1374
1375 public int size;
1376
1377
1378
1379 public String date;
1380
1381
1382
1383
1384 public boolean read;
1385
1386
1387
1388 public boolean deleted;
1389
1390
1391
1392 public boolean junk;
1393
1394
1395
1396 public boolean flagged;
1397
1398
1399
1400 public boolean recent;
1401
1402
1403
1404 public boolean draft;
1405
1406
1407
1408 public boolean answered;
1409
1410
1411
1412 public boolean forwarded;
1413
1414
1415
1416
1417 protected byte[] mimeContent;
1418
1419
1420
1421
1422 protected MimeMessage mimeMessage;
1423
1424
1425
1426
1427
1428
1429
1430 public abstract String getPermanentId();
1431
1432
1433
1434
1435
1436
1437 public long getImapUid() {
1438 return imapUid;
1439 }
1440
1441
1442
1443
1444
1445
1446 public void setImapUid(long imapUid) {
1447 this.imapUid = imapUid;
1448 }
1449
1450
1451
1452
1453
1454
1455 public String getUid() {
1456 return uid;
1457 }
1458
1459
1460
1461
1462
1463
1464 public String getImapFlags() {
1465 StringBuilder buffer = new StringBuilder();
1466 if (read) {
1467 buffer.append("\\Seen ");
1468 }
1469 if (deleted) {
1470 buffer.append("\\Deleted ");
1471 }
1472 if (recent) {
1473 buffer.append("\\Recent ");
1474 }
1475 if (flagged) {
1476 buffer.append("\\Flagged ");
1477 }
1478 if (junk) {
1479 buffer.append("Junk ");
1480 }
1481 if (draft) {
1482 buffer.append("\\Draft ");
1483 }
1484 if (answered) {
1485 buffer.append("\\Answered ");
1486 }
1487 if (forwarded) {
1488 buffer.append("$Forwarded ");
1489 }
1490 if (keywords != null) {
1491 for (String keyword : keywords.split(",")) {
1492 buffer.append(encodeKeyword(convertKeywordToFlag(keyword))).append(" ");
1493 }
1494 }
1495 return buffer.toString().trim();
1496 }
1497
1498
1499
1500
1501
1502
1503
1504 public void loadMimeMessage() throws IOException, MessagingException {
1505 if (mimeMessage == null) {
1506
1507 if (this.imapUid == messageList.cachedMessageImapUid
1508
1509 && messageList.cachedMimeContent != null
1510 && messageList.cachedMimeMessage != null) {
1511 mimeContent = messageList.cachedMimeContent;
1512 mimeMessage = messageList.cachedMimeMessage;
1513 LOGGER.debug("Got message content for " + imapUid + " from cache");
1514 } else {
1515
1516 mimeContent = getContent(this);
1517 mimeMessage = new MimeMessage(null, new SharedByteArrayInputStream(mimeContent));
1518
1519 if (mimeMessage.getHeader("MAIL FROM") != null) {
1520
1521 byte[] mimeContentCopy = new byte[((SharedByteArrayInputStream) mimeMessage.getRawInputStream()).available()];
1522 int offset = mimeContent.length - mimeContentCopy.length;
1523
1524 System.arraycopy(mimeContent, offset, mimeContentCopy, 0, mimeContentCopy.length);
1525 mimeContent = mimeContentCopy;
1526 mimeMessage = new MimeMessage(null, new SharedByteArrayInputStream(mimeContent));
1527 }
1528 LOGGER.debug("Downloaded full message content for IMAP UID " + imapUid + " (" + mimeContent.length + " bytes)");
1529 }
1530 }
1531 }
1532
1533
1534
1535
1536
1537
1538
1539
1540 public MimeMessage getMimeMessage() throws IOException, MessagingException {
1541 loadMimeMessage();
1542 return mimeMessage;
1543 }
1544
1545 public Enumeration<?> getMatchingHeaderLinesFromHeaders(String[] headerNames) throws MessagingException {
1546 Enumeration<?> result = null;
1547 if (mimeMessage == null) {
1548
1549 InputStream headers = getMimeHeaders();
1550 if (headers != null) {
1551 InternetHeaders internetHeaders = new InternetHeaders(headers);
1552 if (internetHeaders.getHeader("Subject") == null) {
1553
1554 return null;
1555 }
1556 if (headerNames == null) {
1557 result = internetHeaders.getAllHeaderLines();
1558 } else {
1559 result = internetHeaders.getMatchingHeaderLines(headerNames);
1560 }
1561 }
1562 }
1563 return result;
1564 }
1565
1566 public Enumeration<?> getMatchingHeaderLines(String[] headerNames) throws MessagingException, IOException {
1567 Enumeration<?> result = getMatchingHeaderLinesFromHeaders(headerNames);
1568 if (result == null) {
1569 if (headerNames == null) {
1570 result = getMimeMessage().getAllHeaderLines();
1571 } else {
1572 result = getMimeMessage().getMatchingHeaderLines(headerNames);
1573 }
1574
1575 }
1576 return result;
1577 }
1578
1579 protected abstract InputStream getMimeHeaders();
1580
1581
1582
1583
1584
1585
1586
1587
1588 public int getMimeMessageSize() throws IOException, MessagingException {
1589 loadMimeMessage();
1590 return mimeContent.length;
1591 }
1592
1593
1594
1595
1596
1597
1598
1599
1600 public InputStream getRawInputStream() throws IOException, MessagingException {
1601 loadMimeMessage();
1602 return new SharedByteArrayInputStream(mimeContent);
1603 }
1604
1605
1606
1607
1608
1609
1610 public void dropMimeMessage() {
1611
1612 if (mimeMessage != null) {
1613 messageList.cachedMessageImapUid = imapUid;
1614 messageList.cachedMimeContent = mimeContent;
1615 messageList.cachedMimeMessage = mimeMessage;
1616 }
1617
1618 mimeMessage = null;
1619 mimeContent = null;
1620 }
1621
1622 public boolean isLoaded() {
1623
1624 if (imapUid == messageList.cachedMessageImapUid) {
1625 mimeContent = messageList.cachedMimeContent;
1626 mimeMessage = messageList.cachedMimeMessage;
1627 }
1628 return mimeMessage != null;
1629 }
1630
1631
1632
1633
1634
1635
1636 public void delete() throws IOException {
1637 deleteMessage(this);
1638 }
1639
1640
1641
1642
1643
1644
1645 public void moveToTrash() throws IOException {
1646 markRead();
1647
1648 ExchangeSession.this.moveToTrash(this);
1649 }
1650
1651
1652
1653
1654
1655
1656 public void markRead() throws IOException {
1657 HashMap<String, String> properties = new HashMap<>();
1658 properties.put("read", "1");
1659 updateMessage(this, properties);
1660 }
1661
1662
1663
1664
1665
1666
1667
1668 public int compareTo(Message message) {
1669 long compareValue = (imapUid - message.imapUid);
1670 if (compareValue > 0) {
1671 return 1;
1672 } else if (compareValue < 0) {
1673 return -1;
1674 } else {
1675 return 0;
1676 }
1677 }
1678
1679
1680
1681
1682
1683
1684
1685 @Override
1686 public boolean equals(Object message) {
1687 return message instanceof Message && imapUid == ((Message) message).imapUid;
1688 }
1689
1690
1691
1692
1693
1694
1695 @Override
1696 public int hashCode() {
1697 return Long.hashCode(imapUid);
1698 }
1699
1700 public String removeFlag(String flag) {
1701 if (keywords != null) {
1702 final String exchangeFlag = convertFlagToKeyword(flag);
1703 Set<String> keywordSet = new HashSet<>();
1704 String[] keywordArray = keywords.split(",");
1705 for (String value : keywordArray) {
1706 if (!value.equalsIgnoreCase(exchangeFlag)) {
1707 keywordSet.add(value);
1708 }
1709 }
1710 keywords = StringUtil.join(keywordSet, ",");
1711 }
1712 return keywords;
1713 }
1714
1715 public String addFlag(String flag) {
1716 final String exchangeFlag = convertFlagToKeyword(flag);
1717 HashSet<String> keywordSet = new HashSet<>();
1718 boolean hasFlag = false;
1719 if (keywords != null) {
1720 String[] keywordArray = keywords.split(",");
1721 for (String value : keywordArray) {
1722 keywordSet.add(value);
1723 if (value.equalsIgnoreCase(exchangeFlag)) {
1724 hasFlag = true;
1725 }
1726 }
1727 }
1728 if (!hasFlag) {
1729 keywordSet.add(exchangeFlag);
1730 }
1731 keywords = StringUtil.join(keywordSet, ",");
1732 return keywords;
1733 }
1734
1735 public String setFlags(HashSet<String> flags) {
1736 keywords = convertFlagsToKeywords(flags);
1737 return keywords;
1738 }
1739
1740 }
1741
1742
1743
1744
1745 public static class MessageList extends ArrayList<Message> {
1746
1747
1748
1749 protected transient MimeMessage cachedMimeMessage;
1750
1751
1752
1753 protected transient long cachedMessageImapUid;
1754
1755
1756
1757 protected transient byte[] cachedMimeContent;
1758
1759 }
1760
1761
1762
1763
1764 public abstract static class Item extends HashMap<String, String> {
1765 protected String folderPath;
1766 protected String itemName;
1767 protected String permanentUrl;
1768
1769
1770
1771 public String displayName;
1772
1773
1774
1775 public String etag;
1776 protected String noneMatch;
1777
1778
1779
1780
1781
1782
1783
1784
1785
1786 public Item(String folderPath, String itemName, String etag, String noneMatch) {
1787 this.folderPath = folderPath;
1788 this.itemName = itemName;
1789 this.etag = etag;
1790 this.noneMatch = noneMatch;
1791 }
1792
1793
1794
1795
1796 protected Item() {
1797 }
1798
1799
1800
1801
1802
1803
1804 public abstract String getContentType();
1805
1806
1807
1808
1809
1810
1811
1812 public abstract String getBody() throws IOException;
1813
1814
1815
1816
1817
1818
1819 public String getName() {
1820 return itemName;
1821 }
1822
1823
1824
1825
1826
1827
1828 public String getEtag() {
1829 return etag;
1830 }
1831
1832
1833
1834
1835
1836
1837 public void setHref(String href) {
1838 int index = href.lastIndexOf('/');
1839 if (index >= 0) {
1840 folderPath = href.substring(0, index);
1841 itemName = href.substring(index + 1);
1842 } else {
1843 throw new IllegalArgumentException(href);
1844 }
1845 }
1846
1847
1848
1849
1850
1851
1852 public String getHref() {
1853 return folderPath + '/' + itemName;
1854 }
1855
1856 public void setItemName(String itemName) {
1857 this.itemName = itemName;
1858 }
1859 }
1860
1861
1862
1863
1864 public abstract class Contact extends Item {
1865
1866 protected ArrayList<String> distributionListMembers = null;
1867 protected String vCardVersion;
1868
1869 public Contact(String folderPath, String itemName, Map<String, String> properties, String etag, String noneMatch) {
1870 super(folderPath, itemName.endsWith(".vcf") ? itemName.substring(0, itemName.length() - 3) + "EML" : itemName, etag, noneMatch);
1871 this.putAll(properties);
1872 }
1873
1874 protected Contact() {
1875 }
1876
1877 public void setVCardVersion(String vCardVersion) {
1878 this.vCardVersion = vCardVersion;
1879 }
1880
1881 public abstract ItemResult createOrUpdate() throws IOException;
1882
1883
1884
1885
1886
1887
1888 @Override
1889 public String getName() {
1890 String name = super.getName();
1891 if (name.endsWith(".EML")) {
1892 name = name.substring(0, name.length() - 3) + "vcf";
1893 }
1894 return name;
1895 }
1896
1897
1898
1899
1900
1901
1902 public void setName(String name) {
1903 this.itemName = name;
1904 }
1905
1906
1907
1908
1909
1910
1911 public String getUid() {
1912 String uid = getName();
1913 int dotIndex = uid.lastIndexOf('.');
1914 if (dotIndex > 0) {
1915 uid = uid.substring(0, dotIndex);
1916 }
1917 return URIUtil.encodePath(uid);
1918 }
1919
1920 @Override
1921 public String getContentType() {
1922 return "text/vcard";
1923 }
1924
1925 public void addMember(String member) {
1926 if (distributionListMembers == null) {
1927 distributionListMembers = new ArrayList<>();
1928 }
1929 distributionListMembers.add(member);
1930 }
1931
1932
1933 @Override
1934 public String getBody() {
1935
1936 VCardWriter writer = new VCardWriter();
1937 writer.startCard(vCardVersion);
1938 writer.appendProperty("UID", getUid());
1939
1940 String cn = get("cn");
1941 if (cn == null) {
1942 cn = get("displayname");
1943 }
1944 String sn = get("sn");
1945 if (sn == null) {
1946 sn = cn;
1947 }
1948 writer.appendProperty("FN", cn);
1949
1950 writer.appendProperty("N", sn, get("givenName"), get("middlename"), get("personaltitle"), get("namesuffix"));
1951
1952 if (distributionListMembers != null) {
1953 writer.appendProperty("KIND", "group");
1954 for (String member : distributionListMembers) {
1955 writer.appendProperty("MEMBER", member);
1956 }
1957 }
1958
1959 writer.appendProperty("TEL;TYPE=cell", get("mobile"));
1960 writer.appendProperty("TEL;TYPE=work", get("telephoneNumber"));
1961 writer.appendProperty("TEL;TYPE=home", get("homePhone"));
1962 writer.appendProperty("TEL;TYPE=fax", get("facsimiletelephonenumber"));
1963 writer.appendProperty("TEL;TYPE=pager", get("pager"));
1964 writer.appendProperty("TEL;TYPE=car", get("othermobile"));
1965 writer.appendProperty("TEL;TYPE=home,fax", get("homefax"));
1966 writer.appendProperty("TEL;TYPE=isdn", get("internationalisdnnumber"));
1967 writer.appendProperty("TEL;TYPE=msg", get("otherTelephone"));
1968
1969
1970
1971
1972 writer.appendProperty("ADR;TYPE=home",
1973 get("homepostofficebox"), null, get("homeStreet"), get("homeCity"), get("homeState"), get("homePostalCode"), get("homeCountry"));
1974 writer.appendProperty("ADR;TYPE=work",
1975 get("postofficebox"), get("roomnumber"), get("street"), get("l"), get("st"), get("postalcode"), get("co"));
1976 writer.appendProperty("ADR;TYPE=other",
1977 get("otherpostofficebox"), null, get("otherstreet"), get("othercity"), get("otherstate"), get("otherpostalcode"), get("othercountry"));
1978
1979 writer.appendProperty("EMAIL;TYPE=work", get("smtpemail1"));
1980 writer.appendProperty("EMAIL;TYPE=home", get("smtpemail2"));
1981 writer.appendProperty("EMAIL;TYPE=other", get("smtpemail3"));
1982
1983 writer.appendProperty("ORG", get("o"), get("department"));
1984 writer.appendProperty("URL;TYPE=work", get("businesshomepage"));
1985 writer.appendProperty("URL;TYPE=home", get("personalHomePage"));
1986 writer.appendProperty("TITLE", get("title"));
1987 writer.appendProperty("NOTE", get("description"));
1988
1989 writer.appendProperty("CUSTOM1", get("extensionattribute1"));
1990 writer.appendProperty("CUSTOM2", get("extensionattribute2"));
1991 writer.appendProperty("CUSTOM3", get("extensionattribute3"));
1992 writer.appendProperty("CUSTOM4", get("extensionattribute4"));
1993
1994 writer.appendProperty("ROLE", get("profession"));
1995 writer.appendProperty("NICKNAME", get("nickname"));
1996 writer.appendProperty("X-AIM", get("im"));
1997
1998 writer.appendProperty("BDAY", convertZuluDateToBday(get("bday")));
1999 writer.appendProperty("ANNIVERSARY", convertZuluDateToBday(get("anniversary")));
2000
2001 String gender = get("gender");
2002 if ("1".equals(gender)) {
2003 writer.appendProperty("SEX", "2");
2004 } else if ("2".equals(gender)) {
2005 writer.appendProperty("SEX", "1");
2006 }
2007
2008 writer.appendProperty("CATEGORIES", get("keywords"));
2009
2010 writer.appendProperty("FBURL", get("fburl"));
2011
2012 if ("1".equals(get("private"))) {
2013 writer.appendProperty("CLASS", "PRIVATE");
2014 }
2015
2016 writer.appendProperty("X-ASSISTANT", get("secretarycn"));
2017 writer.appendProperty("X-MANAGER", get("manager"));
2018 writer.appendProperty("X-SPOUSE", get("spousecn"));
2019
2020 writer.appendProperty("REV", get("lastmodified"));
2021
2022 ContactPhoto contactPhoto = null;
2023
2024 if (Settings.getBooleanProperty("davmail.carddavReadPhoto", true)) {
2025 if (("true".equals(get("haspicture")))) {
2026 try {
2027 contactPhoto = getContactPhoto(this);
2028 } catch (IOException e) {
2029 LOGGER.warn("Unable to get photo from contact " + this.get("cn"));
2030 }
2031 }
2032
2033 if (contactPhoto == null) {
2034 contactPhoto = getADPhoto(get("smtpemail1"));
2035 }
2036 }
2037
2038 if (contactPhoto != null) {
2039 writer.writeLine("PHOTO;TYPE=" + contactPhoto.contentType + ";ENCODING=BASE64:");
2040 writer.writeLine(contactPhoto.content, true);
2041 }
2042
2043 writer.appendProperty("KEY1;X509;ENCODING=BASE64", get("msexchangecertificate"));
2044 writer.appendProperty("KEY2;X509;ENCODING=BASE64", get("usersmimecertificate"));
2045
2046 writer.endCard();
2047 return writer.toString();
2048 }
2049 }
2050
2051
2052
2053
2054 public abstract class Event extends Item {
2055 protected String contentClass;
2056 protected String subject;
2057 protected VCalendar vCalendar;
2058
2059 public Event(String folderPath, String itemName, String contentClass, String itemBody, String etag, String noneMatch) throws IOException {
2060 super(folderPath, itemName, etag, noneMatch);
2061 this.contentClass = contentClass;
2062 fixICS(itemBody.getBytes(StandardCharsets.UTF_8), false);
2063
2064 if (vCalendar.isTodo() && this.itemName.endsWith(".ics")) {
2065 this.itemName = itemName.substring(0, itemName.length() - 3) + "EML";
2066 }
2067 }
2068
2069 protected Event() {
2070 }
2071
2072 @Override
2073 public String getContentType() {
2074 return "text/calendar;charset=UTF-8";
2075 }
2076
2077 @Override
2078 public String getBody() throws IOException {
2079 if (vCalendar == null) {
2080 fixICS(getEventContent(), true);
2081 }
2082 return vCalendar.toString();
2083 }
2084
2085 protected HttpNotFoundException buildHttpNotFoundException(Exception e) {
2086 String message = "Unable to get event " + getName() + " subject: " + subject + " at " + permanentUrl + ": " + e.getMessage();
2087 LOGGER.warn(message);
2088 return new HttpNotFoundException(message);
2089 }
2090
2091
2092
2093
2094
2095
2096
2097 public abstract byte[] getEventContent() throws IOException;
2098
2099 protected static final String TEXT_CALENDAR = "text/calendar";
2100 protected static final String APPLICATION_ICS = "application/ics";
2101
2102 protected boolean isCalendarContentType(String contentType) {
2103 return TEXT_CALENDAR.regionMatches(true, 0, contentType, 0, TEXT_CALENDAR.length()) ||
2104 APPLICATION_ICS.regionMatches(true, 0, contentType, 0, APPLICATION_ICS.length());
2105 }
2106
2107 protected MimePart getCalendarMimePart(MimeMultipart multiPart) throws IOException, MessagingException {
2108 MimePart bodyPart = null;
2109 for (int i = 0; i < multiPart.getCount(); i++) {
2110 String contentType = multiPart.getBodyPart(i).getContentType();
2111 if (isCalendarContentType(contentType)) {
2112 bodyPart = (MimePart) multiPart.getBodyPart(i);
2113 break;
2114 } else if (contentType.startsWith("multipart")) {
2115 Object content = multiPart.getBodyPart(i).getContent();
2116 if (content instanceof MimeMultipart) {
2117 bodyPart = getCalendarMimePart((MimeMultipart) content);
2118 }
2119 }
2120 }
2121
2122 return bodyPart;
2123 }
2124
2125
2126
2127
2128
2129
2130
2131
2132
2133 protected byte[] getICS(InputStream mimeInputStream) throws IOException, MessagingException {
2134 byte[] result;
2135 MimeMessage mimeMessage = new MimeMessage(null, mimeInputStream);
2136 String[] contentClassHeader = mimeMessage.getHeader("Content-class");
2137
2138 if (contentClassHeader != null && contentClassHeader.length > 0 && "urn:content-classes:task".equals(contentClassHeader[0])) {
2139 return null;
2140 }
2141 Object mimeBody = mimeMessage.getContent();
2142 MimePart bodyPart = null;
2143 if (mimeBody instanceof MimeMultipart) {
2144 bodyPart = getCalendarMimePart((MimeMultipart) mimeBody);
2145 } else if (isCalendarContentType(mimeMessage.getContentType())) {
2146
2147 bodyPart = mimeMessage;
2148 }
2149
2150
2151 if (bodyPart != null) {
2152 try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
2153 bodyPart.getDataHandler().writeTo(baos);
2154 result = baos.toByteArray();
2155 }
2156 } else {
2157 try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
2158 mimeMessage.writeTo(baos);
2159 throw new DavMailException("EXCEPTION_INVALID_MESSAGE_CONTENT", new String(baos.toByteArray(), StandardCharsets.UTF_8));
2160 }
2161 }
2162 return result;
2163 }
2164
2165 protected void fixICS(byte[] icsContent, boolean fromServer) throws IOException {
2166 if (LOGGER.isDebugEnabled() && fromServer) {
2167 dumpIndex++;
2168 String icsBody = new String(icsContent, StandardCharsets.UTF_8);
2169 ICSCalendarValidator.ValidationResult vr = ICSCalendarValidator.validateWithDetails(icsBody);
2170 dumpICS(icsBody, true, false);
2171 LOGGER.debug("Vcalendar body ValidationResult: "+ vr.isValid() +" "+ vr.showReason());
2172 LOGGER.debug("Vcalendar body received from server:\n" + icsBody);
2173 }
2174 vCalendar = new VCalendar(icsContent, getEmail(), getVTimezone());
2175 vCalendar.fixVCalendar(fromServer);
2176 if (LOGGER.isDebugEnabled() && !fromServer) {
2177 String resultString = vCalendar.toString();
2178 ICSCalendarValidator.ValidationResult vr = ICSCalendarValidator.validateWithDetails(resultString);
2179 LOGGER.debug("Fixed Vcalendar body ValidationResult: "+ vr.isValid() +" "+ vr.showReason());
2180 LOGGER.debug("Fixed Vcalendar body to server:\n" + resultString);
2181 dumpICS(resultString, false, true);
2182 }
2183 }
2184
2185 protected void dumpICS(String icsBody, boolean fromServer, boolean after) {
2186 String logFileDirectory = Settings.getLogFileDirectory();
2187
2188
2189 int dumpMax = Settings.getIntProperty("davmail.dumpICS");
2190 if (dumpMax > 0) {
2191 if (dumpIndex > dumpMax) {
2192
2193 final int oldest = dumpIndex - dumpMax;
2194 try {
2195 File[] oldestFiles = (new File(logFileDirectory)).listFiles((dir, name) -> {
2196 if (name.endsWith(".ics")) {
2197 int dashIndex = name.indexOf('-');
2198 if (dashIndex > 0) {
2199 try {
2200 int fileIndex = Integer.parseInt(name.substring(0, dashIndex));
2201 return fileIndex < oldest;
2202 } catch (NumberFormatException nfe) {
2203
2204 }
2205 }
2206 }
2207 return false;
2208 });
2209 if (oldestFiles != null) {
2210 for (File file : oldestFiles) {
2211 if (!file.delete()) {
2212 LOGGER.warn("Unable to delete " + file.getAbsolutePath());
2213 }
2214 }
2215 }
2216 } catch (Exception ex) {
2217 LOGGER.warn("Error deleting ics dump: " + ex.getMessage());
2218 }
2219 }
2220
2221 StringBuilder filePath = new StringBuilder();
2222 filePath.append(logFileDirectory).append('/')
2223 .append(dumpIndex)
2224 .append(after ? "-to" : "-from")
2225 .append((after ^ fromServer) ? "-server" : "-client")
2226 .append(".ics");
2227 if ((icsBody != null) && (!icsBody.isEmpty())) {
2228 try (OutputStreamWriter writer = new OutputStreamWriter(Files.newOutputStream(Paths.get(filePath.toString())), StandardCharsets.UTF_8))
2229 {
2230 writer.write(icsBody);
2231 } catch (IOException e) {
2232 LOGGER.error(e);
2233 }
2234
2235
2236 }
2237 }
2238
2239 }
2240
2241
2242
2243
2244
2245
2246
2247 public byte[] createMimeContent() throws IOException {
2248 String boundary = UUID.randomUUID().toString();
2249 ByteArrayOutputStream baos = new ByteArrayOutputStream();
2250 MimeOutputStreamWriter writer = new MimeOutputStreamWriter(baos);
2251
2252 writer.writeHeader("Content-Transfer-Encoding", "7bit");
2253 writer.writeHeader("Content-class", contentClass);
2254
2255 writer.writeHeader("Date", new Date());
2256
2257
2258 String vEventSubject = vCalendar.getFirstVeventPropertyValue("SUMMARY");
2259 if (vEventSubject == null) {
2260 vEventSubject = BundleMessage.format("MEETING_REQUEST");
2261 }
2262
2263
2264
2265 String description = vCalendar.getFirstVeventPropertyValue("DESCRIPTION");
2266
2267
2268 if ("urn:content-classes:calendarmessage".equals(contentClass)) {
2269
2270 VCalendar.Recipients recipients = vCalendar.getRecipients(true);
2271 String to;
2272 String cc;
2273 String notificationSubject;
2274 if (email.equalsIgnoreCase(recipients.organizer)) {
2275
2276 to = recipients.attendees;
2277 cc = recipients.optionalAttendees;
2278 notificationSubject = subject;
2279 } else {
2280 String status = vCalendar.getAttendeeStatus();
2281
2282 to = recipients.organizer;
2283 cc = null;
2284 notificationSubject = (status != null) ? (BundleMessage.format(status) + vEventSubject) : subject;
2285 description = "";
2286 }
2287
2288
2289 if (Settings.getBooleanProperty("davmail.caldavEditNotifications")) {
2290
2291 NotificationDialog notificationDialog = new NotificationDialog(to,
2292 cc, notificationSubject, description);
2293 if (!notificationDialog.getSendNotification()) {
2294 LOGGER.debug("Notification canceled by user");
2295 return null;
2296 }
2297
2298 to = notificationDialog.getTo();
2299 cc = notificationDialog.getCc();
2300 notificationSubject = notificationDialog.getSubject();
2301 description = notificationDialog.getBody();
2302 }
2303
2304
2305 if ((to == null || to.isEmpty()) && (cc == null || cc.isEmpty())) {
2306 return null;
2307 }
2308
2309 writer.writeHeader("To", to);
2310 writer.writeHeader("Cc", cc);
2311 writer.writeHeader("Subject", notificationSubject);
2312
2313
2314 if (LOGGER.isDebugEnabled()) {
2315 StringBuilder logBuffer = new StringBuilder("Sending notification ");
2316 if (to != null) {
2317 logBuffer.append("to: ").append(to);
2318 }
2319 if (cc != null) {
2320 logBuffer.append("cc: ").append(cc);
2321 }
2322 LOGGER.debug(logBuffer.toString());
2323 }
2324 } else {
2325
2326 VCalendar.Recipients recipients = vCalendar.getRecipients(false);
2327
2328 if (recipients.attendees != null) {
2329 writer.writeHeader("To", recipients.attendees);
2330 } else {
2331
2332 writer.writeHeader("To", email);
2333 }
2334 writer.writeHeader("Cc", recipients.optionalAttendees);
2335
2336 if (recipients.organizer != null) {
2337 writer.writeHeader("From", recipients.organizer);
2338 } else {
2339 writer.writeHeader("From", email);
2340 }
2341 }
2342 if (vCalendar.getMethod() == null) {
2343 vCalendar.setPropertyValue("METHOD", "REQUEST");
2344 }
2345 writer.writeHeader("MIME-Version", "1.0");
2346 writer.writeHeader("Content-Type", "multipart/alternative;\r\n" +
2347 "\tboundary=\"----=_NextPart_" + boundary + '\"');
2348 writer.writeLn();
2349 writer.writeLn("This is a multi-part message in MIME format.");
2350 writer.writeLn();
2351 writer.writeLn("------=_NextPart_" + boundary);
2352
2353 if (description != null && !description.isEmpty()) {
2354 writer.writeHeader("Content-Type", "text/plain;\r\n" +
2355 "\tcharset=\"utf-8\"");
2356 writer.writeHeader("content-transfer-encoding", "8bit");
2357 writer.writeLn();
2358 writer.flush();
2359 baos.write(description.getBytes(StandardCharsets.UTF_8));
2360 writer.writeLn();
2361 writer.writeLn("------=_NextPart_" + boundary);
2362 }
2363 writer.writeHeader("Content-class", contentClass);
2364 writer.writeHeader("Content-Type", "text/calendar;\r\n" +
2365 "\tmethod=" + vCalendar.getMethod() + ";\r\n" +
2366 "\tcharset=\"utf-8\""
2367 );
2368 writer.writeHeader("Content-Transfer-Encoding", "8bit");
2369 writer.writeLn();
2370 writer.flush();
2371 baos.write(vCalendar.toString().getBytes(StandardCharsets.UTF_8));
2372 writer.writeLn();
2373 writer.writeLn("------=_NextPart_" + boundary + "--");
2374 writer.close();
2375 return baos.toByteArray();
2376 }
2377
2378
2379
2380
2381
2382
2383
2384 public abstract ItemResult createOrUpdate() throws IOException;
2385
2386 }
2387
2388 protected abstract Set<String> getItemProperties();
2389
2390
2391
2392
2393
2394
2395
2396
2397
2398 public List<ExchangeSession.Contact> getAllContacts(String folderPath, boolean includeDistList) throws IOException {
2399 return searchContacts(folderPath, ExchangeSession.CONTACT_ATTRIBUTES, isEqualTo("outlookmessageclass", "IPM.Contact"), 0);
2400 }
2401
2402
2403
2404
2405
2406
2407
2408
2409
2410
2411
2412
2413 public abstract List<Contact> searchContacts(String folderPath, Set<String> attributes, Condition condition, int maxCount) throws IOException;
2414
2415
2416
2417
2418
2419
2420
2421
2422 public abstract List<Event> getEventMessages(String folderPath) throws IOException;
2423
2424
2425
2426
2427
2428
2429
2430
2431 public List<Event> getAllEvents(String folderPath) throws IOException {
2432 List<Event> results = searchEvents(folderPath, getCalendarItemCondition(getPastDelayCondition("dtstart")));
2433
2434 if (!Settings.getBooleanProperty("davmail.caldavDisableTasks", false) && isMainCalendar(folderPath)) {
2435
2436 results.addAll(searchTasksOnly(TASKS));
2437 }
2438
2439 return results;
2440 }
2441
2442 protected abstract Condition getCalendarItemCondition(Condition dateCondition);
2443
2444 protected Condition getPastDelayCondition(String attribute) {
2445 int caldavPastDelay = Settings.getIntProperty("davmail.caldavPastDelay");
2446 Condition dateCondition = null;
2447 if (caldavPastDelay != 0) {
2448 Calendar cal = Calendar.getInstance();
2449 cal.add(Calendar.DAY_OF_MONTH, -caldavPastDelay);
2450 dateCondition = gt(attribute, formatSearchDate(cal.getTime()));
2451 }
2452 return dateCondition;
2453 }
2454
2455 protected Condition getRangeCondition(String timeRangeStart, String timeRangeEnd) throws IOException {
2456 try {
2457 SimpleDateFormat parser = getZuluDateFormat();
2458 ExchangeSession.MultiCondition andCondition = and();
2459 if (timeRangeStart != null) {
2460 andCondition.add(gt("dtend", formatSearchDate(parser.parse(timeRangeStart))));
2461 }
2462 if (timeRangeEnd != null) {
2463 andCondition.add(lt("dtstart", formatSearchDate(parser.parse(timeRangeEnd))));
2464 }
2465 return andCondition;
2466 } catch (ParseException e) {
2467 throw new IOException(e + " " + e.getMessage());
2468 }
2469 }
2470
2471
2472
2473
2474
2475
2476
2477
2478
2479
2480 public List<Event> searchEvents(String folderPath, String timeRangeStart, String timeRangeEnd) throws IOException {
2481 Condition dateCondition = getRangeCondition(timeRangeStart, timeRangeEnd);
2482 Condition condition = getCalendarItemCondition(dateCondition);
2483
2484 return searchEvents(folderPath, condition);
2485 }
2486
2487
2488
2489
2490
2491
2492
2493
2494
2495
2496 public List<Event> searchEventsOnly(String folderPath, String timeRangeStart, String timeRangeEnd) throws IOException {
2497 Condition dateCondition = getRangeCondition(timeRangeStart, timeRangeEnd);
2498 return searchEvents(folderPath, getCalendarItemCondition(dateCondition));
2499 }
2500
2501
2502
2503
2504
2505
2506
2507
2508 public List<Event> searchTasksOnly(String folderPath) throws IOException {
2509 return searchEvents(folderPath, and(isEqualTo("outlookmessageclass", "IPM.Task"),
2510 or(isNull("datecompleted"), getPastDelayCondition("datecompleted"))));
2511 }
2512
2513
2514
2515
2516
2517
2518
2519
2520
2521 public List<Event> searchEvents(String folderPath, Condition filter) throws IOException {
2522
2523 Condition privateCondition = null;
2524 if (isSharedFolder(folderPath) && Settings.getBooleanProperty("davmail.excludePrivateEvents", true)) {
2525 LOGGER.debug("Shared or public calendar: exclude private events");
2526 privateCondition = isEqualTo("sensitivity", 0);
2527 }
2528
2529 return searchEvents(folderPath, getItemProperties(),
2530 and(filter, privateCondition));
2531 }
2532
2533
2534
2535
2536
2537
2538
2539
2540
2541
2542 public abstract List<Event> searchEvents(String folderPath, Set<String> attributes, Condition condition) throws IOException;
2543
2544
2545
2546
2547
2548
2549
2550 protected String convertItemNameToEML(String itemName) {
2551 if (itemName.endsWith(".vcf")) {
2552 return itemName.substring(0, itemName.length() - 3) + "EML";
2553 } else {
2554 return itemName;
2555 }
2556 }
2557
2558
2559
2560
2561
2562
2563
2564
2565
2566 public abstract Item getItem(String folderPath, String itemName) throws IOException;
2567
2568
2569
2570
2571 public static class ContactPhoto {
2572
2573
2574
2575 public String contentType;
2576
2577
2578
2579 public String content;
2580 }
2581
2582
2583
2584
2585
2586
2587
2588
2589 public abstract ContactPhoto getContactPhoto(Contact contact) throws IOException;
2590
2591
2592
2593
2594
2595
2596
2597 public ContactPhoto getADPhoto(String email) {
2598 return null;
2599 }
2600
2601
2602
2603
2604
2605
2606
2607
2608 public abstract void deleteItem(String folderPath, String itemName) throws IOException;
2609
2610
2611
2612
2613
2614
2615
2616
2617 public abstract void processItem(String folderPath, String itemName) throws IOException;
2618
2619
2620 private static int dumpIndex;
2621
2622
2623
2624
2625
2626
2627
2628 protected String replaceIcal4Principal(String value) {
2629 if (value != null && value.contains("/principals/__uuids__/")) {
2630 return value.replaceAll("/principals/__uuids__/([^/]*)__AT__([^/]*)/", "mailto:$1@$2");
2631 } else {
2632 return value;
2633 }
2634 }
2635
2636
2637
2638
2639 public static class ItemResult {
2640
2641
2642
2643 public int status;
2644
2645
2646
2647 public String etag;
2648
2649
2650
2651 public String itemName;
2652 }
2653
2654
2655
2656
2657
2658
2659
2660
2661 public abstract int sendEvent(String icsBody) throws IOException;
2662
2663
2664
2665
2666
2667
2668
2669
2670
2671
2672
2673
2674 public ItemResult createOrUpdateItem(String folderPath, String itemName, String itemBody, String etag, String noneMatch) throws IOException {
2675 if (itemBody.startsWith("BEGIN:VCALENDAR")) {
2676 return internalCreateOrUpdateEvent(folderPath, itemName, "urn:content-classes:appointment", itemBody, etag, noneMatch);
2677 } else if (itemBody.startsWith("BEGIN:VCARD")) {
2678 return createOrUpdateContact(folderPath, itemName, itemBody, etag, noneMatch);
2679 } else {
2680 throw new IOException(BundleMessage.format("EXCEPTION_INVALID_MESSAGE_CONTENT", itemBody));
2681 }
2682 }
2683
2684 static final String[] VCARD_N_PROPERTIES = {"sn", "givenName", "middlename", "personaltitle", "namesuffix"};
2685 static final String[] VCARD_ADR_HOME_PROPERTIES = {"homepostofficebox", null, "homeStreet", "homeCity", "homeState", "homePostalCode", "homeCountry"};
2686 static final String[] VCARD_ADR_WORK_PROPERTIES = {"postofficebox", "roomnumber", "street", "l", "st", "postalcode", "co"};
2687 static final String[] VCARD_ADR_OTHER_PROPERTIES = {"otherpostofficebox", null, "otherstreet", "othercity", "otherstate", "otherpostalcode", "othercountry"};
2688 static final String[] VCARD_ORG_PROPERTIES = {"o", "department"};
2689
2690 protected void convertContactProperties(Map<String, String> properties, String[] contactProperties, List<String> values) {
2691 for (int i = 0; i < values.size() && i < contactProperties.length; i++) {
2692 if (contactProperties[i] != null) {
2693 properties.put(contactProperties[i], values.get(i));
2694 }
2695 }
2696 }
2697
2698 protected ItemResult createOrUpdateContact(String folderPath, String itemName, String itemBody, String etag, String noneMatch) throws IOException {
2699
2700 Map<String, String> properties = new HashMap<>();
2701
2702 VObject vcard = new VObject(new ICSBufferedReader(new StringReader(itemBody)));
2703 if ("group".equalsIgnoreCase(vcard.getPropertyValue("KIND"))) {
2704 properties.put("outlookmessageclass", "IPM.DistList");
2705 properties.put("displayname", vcard.getPropertyValue("FN"));
2706 } else {
2707 properties.put("outlookmessageclass", "IPM.Contact");
2708
2709 for (VProperty property : vcard.getProperties()) {
2710 if ("FN".equals(property.getKey())) {
2711 properties.put("cn", property.getValue());
2712 properties.put("subject", property.getValue());
2713 properties.put("fileas", property.getValue());
2714
2715 } else if ("N".equals(property.getKey())) {
2716 convertContactProperties(properties, VCARD_N_PROPERTIES, property.getValues());
2717 } else if ("NICKNAME".equals(property.getKey())) {
2718 properties.put("nickname", property.getValue());
2719 } else if ("TEL".equals(property.getKey())) {
2720 if (property.hasParam("TYPE", "cell") || property.hasParam("X-GROUP", "cell")) {
2721 properties.put("mobile", property.getValue());
2722 } else if (property.hasParam("TYPE", "work") || property.hasParam("X-GROUP", "work")) {
2723 properties.put("telephoneNumber", property.getValue());
2724 } else if (property.hasParam("TYPE", "home") || property.hasParam("X-GROUP", "home")) {
2725 properties.put("homePhone", property.getValue());
2726 } else if (property.hasParam("TYPE", "fax")) {
2727 if (property.hasParam("TYPE", "home")) {
2728 properties.put("homefax", property.getValue());
2729 } else {
2730 properties.put("facsimiletelephonenumber", property.getValue());
2731 }
2732 } else if (property.hasParam("TYPE", "pager")) {
2733 properties.put("pager", property.getValue());
2734 } else if (property.hasParam("TYPE", "car")) {
2735 properties.put("othermobile", property.getValue());
2736 } else {
2737 properties.put("otherTelephone", property.getValue());
2738 }
2739 } else if ("ADR".equals(property.getKey())) {
2740
2741 if (property.hasParam("TYPE", "home")) {
2742 convertContactProperties(properties, VCARD_ADR_HOME_PROPERTIES, property.getValues());
2743 } else if (property.hasParam("TYPE", "work")) {
2744 convertContactProperties(properties, VCARD_ADR_WORK_PROPERTIES, property.getValues());
2745
2746 } else {
2747 convertContactProperties(properties, VCARD_ADR_OTHER_PROPERTIES, property.getValues());
2748 }
2749 } else if ("EMAIL".equals(property.getKey())) {
2750 if (property.hasParam("TYPE", "home")) {
2751 properties.put("email2", property.getValue());
2752 properties.put("smtpemail2", property.getValue());
2753 } else if (property.hasParam("TYPE", "other")) {
2754 properties.put("email3", property.getValue());
2755 properties.put("smtpemail3", property.getValue());
2756 } else {
2757 properties.put("email1", property.getValue());
2758 properties.put("smtpemail1", property.getValue());
2759 }
2760 } else if ("ORG".equals(property.getKey())) {
2761 convertContactProperties(properties, VCARD_ORG_PROPERTIES, property.getValues());
2762 } else if ("URL".equals(property.getKey())) {
2763 if (property.hasParam("TYPE", "work")) {
2764 properties.put("businesshomepage", property.getValue());
2765 } else if (property.hasParam("TYPE", "home")) {
2766 properties.put("personalHomePage", property.getValue());
2767 } else {
2768
2769 properties.put("personalHomePage", property.getValue());
2770 }
2771 } else if ("TITLE".equals(property.getKey())) {
2772 properties.put("title", property.getValue());
2773 } else if ("NOTE".equals(property.getKey())) {
2774 properties.put("description", property.getValue());
2775 } else if ("CUSTOM1".equals(property.getKey())) {
2776 properties.put("extensionattribute1", property.getValue());
2777 } else if ("CUSTOM2".equals(property.getKey())) {
2778 properties.put("extensionattribute2", property.getValue());
2779 } else if ("CUSTOM3".equals(property.getKey())) {
2780 properties.put("extensionattribute3", property.getValue());
2781 } else if ("CUSTOM4".equals(property.getKey())) {
2782 properties.put("extensionattribute4", property.getValue());
2783 } else if ("ROLE".equals(property.getKey())) {
2784 properties.put("profession", property.getValue());
2785 } else if ("X-AIM".equals(property.getKey())) {
2786 properties.put("im", property.getValue());
2787 } else if ("BDAY".equals(property.getKey())) {
2788 properties.put("bday", convertBDayToZulu(property.getValue()));
2789 } else if ("ANNIVERSARY".equals(property.getKey()) || "X-ANNIVERSARY".equals(property.getKey())) {
2790 properties.put("anniversary", convertBDayToZulu(property.getValue()));
2791 } else if ("CATEGORIES".equals(property.getKey())) {
2792 properties.put("keywords", property.getValue());
2793 } else if ("CLASS".equals(property.getKey())) {
2794 if ("PUBLIC".equals(property.getValue())) {
2795 properties.put("sensitivity", "0");
2796 properties.put("private", "false");
2797 } else {
2798 properties.put("sensitivity", "2");
2799 properties.put("private", "true");
2800 }
2801 } else if ("SEX".equals(property.getKey())) {
2802 String propertyValue = property.getValue();
2803 if ("1".equals(propertyValue)) {
2804 properties.put("gender", "2");
2805 } else if ("2".equals(propertyValue)) {
2806 properties.put("gender", "1");
2807 }
2808 } else if ("FBURL".equals(property.getKey())) {
2809 properties.put("fburl", property.getValue());
2810 } else if ("X-ASSISTANT".equals(property.getKey())) {
2811 properties.put("secretarycn", property.getValue());
2812 } else if ("X-MANAGER".equals(property.getKey())) {
2813 properties.put("manager", property.getValue());
2814 } else if ("X-SPOUSE".equals(property.getKey())) {
2815 properties.put("spousecn", property.getValue());
2816 } else if ("PHOTO".equals(property.getKey())) {
2817 properties.put("photo", property.getValue());
2818 properties.put("haspicture", "true");
2819 } else if ("KEY1".equals(property.getKey())) {
2820 properties.put("msexchangecertificate", property.getValue());
2821 } else if ("KEY2".equals(property.getKey())) {
2822 properties.put("usersmimecertificate", property.getValue());
2823 }
2824 }
2825 LOGGER.debug("Create or update contact " + itemName + ": " + properties);
2826
2827 for (String key : CONTACT_ATTRIBUTES) {
2828 if (!"imapUid".equals(key) && !"etag".equals(key) && !"urlcompname".equals(key)
2829 && !"lastmodified".equals(key) && !"sensitivity".equals(key)
2830 && !properties.containsKey(key)) {
2831 properties.put(key, null);
2832 }
2833 }
2834 }
2835
2836 Contact contact = buildContact(folderPath, itemName, properties, etag, noneMatch);
2837 for (VProperty property : vcard.getProperties()) {
2838 if ("MEMBER".equals(property.getKey())) {
2839 String member = property.getValue();
2840 if (member.startsWith("urn:uuid:")) {
2841 Item item = getItem(folderPath, member.substring(9) + ".EML");
2842 if (item != null) {
2843 if (item.get("smtpemail1") != null) {
2844 member = "mailto:" + item.get("smtpemail1");
2845 } else if (item.get("smtpemail2") != null) {
2846 member = "mailto:" + item.get("smtpemail2");
2847 } else if (item.get("smtpemail3") != null) {
2848 member = "mailto:" + item.get("smtpemail3");
2849 }
2850 }
2851 }
2852 contact.addMember(member);
2853 }
2854 }
2855 return contact.createOrUpdate();
2856 }
2857
2858 protected String convertZuluDateToBday(String value) {
2859 String result = null;
2860 if (value != null && !value.isEmpty()) {
2861 try {
2862 SimpleDateFormat parser = ExchangeSession.getZuluDateFormat();
2863 Calendar cal = Calendar.getInstance();
2864 cal.setTime(parser.parse(value));
2865 cal.add(Calendar.HOUR_OF_DAY, 12);
2866 result = ExchangeSession.getVcardBdayFormat().format(cal.getTime());
2867 } catch (ParseException e) {
2868 LOGGER.warn("Invalid date: " + value);
2869 }
2870 }
2871 return result;
2872 }
2873
2874 protected String convertBDayToZulu(String value) {
2875 String result = null;
2876 if (value != null && !value.isEmpty()) {
2877 try {
2878 SimpleDateFormat parser;
2879 if (value.length() == 10) {
2880 parser = ExchangeSession.getVcardBdayFormat();
2881 } else if (value.length() == 15) {
2882 parser = new SimpleDateFormat("yyyyMMdd'T'HHmmss", Locale.ENGLISH);
2883 parser.setTimeZone(GMT_TIMEZONE);
2884 } else {
2885 parser = ExchangeSession.getExchangeZuluDateFormat();
2886 }
2887 result = ExchangeSession.getExchangeZuluDateFormatMillisecond().format(parser.parse(value));
2888 } catch (ParseException e) {
2889 LOGGER.warn("Invalid date: " + value);
2890 }
2891 }
2892
2893 return result;
2894 }
2895
2896
2897 protected abstract Contact buildContact(String folderPath, String itemName, Map<String, String> properties, String etag, String noneMatch) throws IOException;
2898
2899 protected abstract ItemResult internalCreateOrUpdateEvent(String folderPath, String itemName, String contentClass, String icsBody, String etag, String noneMatch) throws IOException;
2900
2901
2902
2903
2904
2905
2906 public String getAliasFromLogin() {
2907
2908 if (this.userName.indexOf('@') >= 0) {
2909 return null;
2910 }
2911 String result = this.userName;
2912
2913 int index = Math.max(result.indexOf('\\'), result.indexOf('/'));
2914 if (index >= 0) {
2915 result = result.substring(index + 1);
2916 }
2917 return result;
2918 }
2919
2920
2921
2922
2923
2924
2925
2926 public abstract boolean isSharedFolder(String folderPath);
2927
2928
2929
2930
2931
2932
2933
2934 public abstract boolean isMainCalendar(String folderPath) throws IOException;
2935
2936 protected static final String MAILBOX_BASE = "/cn=";
2937
2938
2939
2940
2941
2942
2943 public String getEmail() {
2944 return email;
2945 }
2946
2947
2948
2949
2950
2951
2952 public String getAlias() {
2953 return alias;
2954 }
2955
2956
2957
2958
2959
2960
2961
2962
2963
2964
2965 public abstract Map<String, Contact> galFind(Condition condition, Set<String> returningAttributes, int sizeLimit) throws IOException;
2966
2967
2968
2969
2970 public static final Set<String> CONTACT_ATTRIBUTES = new HashSet<>();
2971
2972 static {
2973 CONTACT_ATTRIBUTES.add("imapUid");
2974 CONTACT_ATTRIBUTES.add("etag");
2975 CONTACT_ATTRIBUTES.add("urlcompname");
2976
2977 CONTACT_ATTRIBUTES.add("extensionattribute1");
2978 CONTACT_ATTRIBUTES.add("extensionattribute2");
2979 CONTACT_ATTRIBUTES.add("extensionattribute3");
2980 CONTACT_ATTRIBUTES.add("extensionattribute4");
2981 CONTACT_ATTRIBUTES.add("bday");
2982 CONTACT_ATTRIBUTES.add("anniversary");
2983 CONTACT_ATTRIBUTES.add("businesshomepage");
2984 CONTACT_ATTRIBUTES.add("personalHomePage");
2985 CONTACT_ATTRIBUTES.add("cn");
2986 CONTACT_ATTRIBUTES.add("co");
2987 CONTACT_ATTRIBUTES.add("department");
2988 CONTACT_ATTRIBUTES.add("smtpemail1");
2989 CONTACT_ATTRIBUTES.add("smtpemail2");
2990 CONTACT_ATTRIBUTES.add("smtpemail3");
2991 CONTACT_ATTRIBUTES.add("facsimiletelephonenumber");
2992 CONTACT_ATTRIBUTES.add("givenName");
2993 CONTACT_ATTRIBUTES.add("homeCity");
2994 CONTACT_ATTRIBUTES.add("homeCountry");
2995 CONTACT_ATTRIBUTES.add("homePhone");
2996 CONTACT_ATTRIBUTES.add("homePostalCode");
2997 CONTACT_ATTRIBUTES.add("homeState");
2998 CONTACT_ATTRIBUTES.add("homeStreet");
2999 CONTACT_ATTRIBUTES.add("homepostofficebox");
3000 CONTACT_ATTRIBUTES.add("l");
3001 CONTACT_ATTRIBUTES.add("manager");
3002 CONTACT_ATTRIBUTES.add("mobile");
3003 CONTACT_ATTRIBUTES.add("namesuffix");
3004 CONTACT_ATTRIBUTES.add("nickname");
3005 CONTACT_ATTRIBUTES.add("o");
3006 CONTACT_ATTRIBUTES.add("pager");
3007 CONTACT_ATTRIBUTES.add("personaltitle");
3008 CONTACT_ATTRIBUTES.add("postalcode");
3009 CONTACT_ATTRIBUTES.add("postofficebox");
3010 CONTACT_ATTRIBUTES.add("profession");
3011 CONTACT_ATTRIBUTES.add("roomnumber");
3012 CONTACT_ATTRIBUTES.add("secretarycn");
3013 CONTACT_ATTRIBUTES.add("sn");
3014 CONTACT_ATTRIBUTES.add("spousecn");
3015 CONTACT_ATTRIBUTES.add("st");
3016 CONTACT_ATTRIBUTES.add("street");
3017 CONTACT_ATTRIBUTES.add("telephoneNumber");
3018 CONTACT_ATTRIBUTES.add("title");
3019 CONTACT_ATTRIBUTES.add("description");
3020 CONTACT_ATTRIBUTES.add("im");
3021 CONTACT_ATTRIBUTES.add("middlename");
3022 CONTACT_ATTRIBUTES.add("lastmodified");
3023 CONTACT_ATTRIBUTES.add("otherstreet");
3024 CONTACT_ATTRIBUTES.add("otherstate");
3025 CONTACT_ATTRIBUTES.add("otherpostofficebox");
3026 CONTACT_ATTRIBUTES.add("otherpostalcode");
3027 CONTACT_ATTRIBUTES.add("othercountry");
3028 CONTACT_ATTRIBUTES.add("othercity");
3029 CONTACT_ATTRIBUTES.add("haspicture");
3030 CONTACT_ATTRIBUTES.add("keywords");
3031 CONTACT_ATTRIBUTES.add("othermobile");
3032 CONTACT_ATTRIBUTES.add("otherTelephone");
3033 CONTACT_ATTRIBUTES.add("gender");
3034 CONTACT_ATTRIBUTES.add("private");
3035 CONTACT_ATTRIBUTES.add("sensitivity");
3036 CONTACT_ATTRIBUTES.add("fburl");
3037 CONTACT_ATTRIBUTES.add("msexchangecertificate");
3038 CONTACT_ATTRIBUTES.add("usersmimecertificate");
3039 }
3040
3041 protected static final Set<String> DISTRIBUTION_LIST_ATTRIBUTES = new HashSet<>();
3042
3043 static {
3044 DISTRIBUTION_LIST_ATTRIBUTES.add("imapUid");
3045 DISTRIBUTION_LIST_ATTRIBUTES.add("etag");
3046 DISTRIBUTION_LIST_ATTRIBUTES.add("urlcompname");
3047
3048 DISTRIBUTION_LIST_ATTRIBUTES.add("cn");
3049 DISTRIBUTION_LIST_ATTRIBUTES.add("members");
3050 }
3051
3052
3053
3054
3055
3056
3057
3058
3059
3060
3061
3062 protected abstract String getFreeBusyData(String attendee, String start, String end, int interval) throws IOException;
3063
3064
3065
3066
3067
3068
3069
3070
3071
3072
3073 public FreeBusy getFreebusy(String attendee, String startDateValue, String endDateValue) throws IOException {
3074
3075 attendee = replaceIcal4Principal(attendee);
3076
3077
3078 if (attendee == null || attendee.indexOf('@') < 0 || attendee.charAt(attendee.length() - 1) == '@') {
3079 return null;
3080 }
3081
3082 if (attendee.startsWith("mailto:") || attendee.startsWith("MAILTO:")) {
3083 attendee = attendee.substring("mailto:".length());
3084 }
3085
3086 SimpleDateFormat exchangeZuluDateFormat = getExchangeZuluDateFormat();
3087 SimpleDateFormat icalDateFormat = getZuluDateFormat();
3088
3089 Date startDate;
3090 Date endDate;
3091 try {
3092 if (startDateValue.length() == 8) {
3093 startDate = parseDate(startDateValue);
3094 } else {
3095 startDate = icalDateFormat.parse(startDateValue);
3096 }
3097 if (endDateValue.length() == 8) {
3098 endDate = parseDate(endDateValue);
3099 } else {
3100 endDate = icalDateFormat.parse(endDateValue);
3101 }
3102 } catch (ParseException e) {
3103 throw new DavMailException("EXCEPTION_INVALID_DATES", e.getMessage());
3104 }
3105
3106 FreeBusy freeBusy = null;
3107 String fbdata = getFreeBusyData(attendee, exchangeZuluDateFormat.format(startDate), exchangeZuluDateFormat.format(endDate), FREE_BUSY_INTERVAL);
3108 if (fbdata != null) {
3109 freeBusy = new FreeBusy(icalDateFormat, startDate, fbdata);
3110 }
3111
3112 if (freeBusy != null && freeBusy.knownAttendee) {
3113 return freeBusy;
3114 } else {
3115 return null;
3116 }
3117 }
3118
3119
3120
3121
3122
3123 public static final class FreeBusy {
3124 final SimpleDateFormat icalParser;
3125 boolean knownAttendee = true;
3126 static final HashMap<Character, String> FBTYPES = new HashMap<>();
3127
3128 static {
3129 FBTYPES.put('1', "BUSY-TENTATIVE");
3130 FBTYPES.put('2', "BUSY");
3131 FBTYPES.put('3', "BUSY-UNAVAILABLE");
3132 }
3133
3134 final HashMap<String, StringBuilder> busyMap = new HashMap<>();
3135
3136 StringBuilder getBusyBuffer(char type) {
3137 String fbType = FBTYPES.get(type);
3138 return busyMap.computeIfAbsent(fbType, k -> new StringBuilder());
3139 }
3140
3141 void startBusy(char type, Calendar currentCal) {
3142 if (type == '4') {
3143 knownAttendee = false;
3144 } else if (type != '0') {
3145 StringBuilder busyBuffer = getBusyBuffer(type);
3146 if (busyBuffer.length() > 0) {
3147 busyBuffer.append(',');
3148 }
3149 busyBuffer.append(icalParser.format(currentCal.getTime()));
3150 }
3151 }
3152
3153 void endBusy(char type, Calendar currentCal) {
3154 if (type != '0' && type != '4') {
3155 getBusyBuffer(type).append('/').append(icalParser.format(currentCal.getTime()));
3156 }
3157 }
3158
3159 FreeBusy(SimpleDateFormat icalParser, Date startDate, String fbdata) {
3160 this.icalParser = icalParser;
3161 if (!fbdata.isEmpty()) {
3162 Calendar currentCal = Calendar.getInstance(TimeZone.getTimeZone("UTC"));
3163 currentCal.setTime(startDate);
3164
3165 startBusy(fbdata.charAt(0), currentCal);
3166 for (int i = 1; i < fbdata.length() && knownAttendee; i++) {
3167 currentCal.add(Calendar.MINUTE, FREE_BUSY_INTERVAL);
3168 char previousState = fbdata.charAt(i - 1);
3169 char currentState = fbdata.charAt(i);
3170 if (previousState != currentState) {
3171 endBusy(previousState, currentCal);
3172 startBusy(currentState, currentCal);
3173 }
3174 }
3175 currentCal.add(Calendar.MINUTE, FREE_BUSY_INTERVAL);
3176 endBusy(fbdata.charAt(fbdata.length() - 1), currentCal);
3177 }
3178 }
3179
3180
3181
3182
3183
3184
3185 public void appendTo(StringBuilder buffer) {
3186 for (Map.Entry<String, StringBuilder> entry : busyMap.entrySet()) {
3187 buffer.append("FREEBUSY;FBTYPE=").append(entry.getKey())
3188 .append(':').append(entry.getValue()).append((char) 13).append((char) 10);
3189 }
3190 }
3191 }
3192
3193 protected VObject vTimezone;
3194
3195
3196
3197
3198
3199
3200 public VObject getVTimezone() {
3201 if (vTimezone == null) {
3202
3203 loadVtimezone();
3204 }
3205 return vTimezone;
3206 }
3207
3208 public void clearVTimezone() {
3209 vTimezone = null;
3210 }
3211
3212 protected abstract void loadVtimezone();
3213
3214 public static final Map<String, String> vTodoToTaskStatusMap = new HashMap<>();
3215 public static final Map<String, String> taskTovTodoStatusMap = new HashMap<>();
3216 static {
3217
3218 taskTovTodoStatusMap.put("InProgress", "IN-PROCESS");
3219 taskTovTodoStatusMap.put("Completed", "COMPLETED");
3220 taskTovTodoStatusMap.put("WaitingOnOthers", "NEEDS-ACTION");
3221 taskTovTodoStatusMap.put("Deferred", "CANCELLED");
3222
3223
3224 vTodoToTaskStatusMap.put("IN-PROCESS", "InProgress");
3225 vTodoToTaskStatusMap.put("COMPLETED", "Completed");
3226 vTodoToTaskStatusMap.put("NEEDS-ACTION", "WaitingOnOthers");
3227 vTodoToTaskStatusMap.put("CANCELLED", "Deferred");
3228
3229 }
3230
3231 protected static final Map<String, String> importanceToPriorityMap = new HashMap<>();
3232
3233 static {
3234 importanceToPriorityMap.put("High", "1");
3235 importanceToPriorityMap.put("Normal", "5");
3236 importanceToPriorityMap.put("Low", "9");
3237 }
3238
3239 protected static final Map<String, String> priorityToImportanceMap = new HashMap<>();
3240
3241 static {
3242
3243 priorityToImportanceMap.put("0", "Normal");
3244
3245 priorityToImportanceMap.put("1", "High");
3246 priorityToImportanceMap.put("2", "High");
3247 priorityToImportanceMap.put("3", "High");
3248 priorityToImportanceMap.put("4", "Normal");
3249 priorityToImportanceMap.put("5", "Normal");
3250 priorityToImportanceMap.put("6", "Normal");
3251 priorityToImportanceMap.put("7", "Low");
3252 priorityToImportanceMap.put("8", "Low");
3253 priorityToImportanceMap.put("9", "Low");
3254 }
3255
3256 protected String convertPriorityFromExchange(String exchangeImportanceValue) {
3257 String value = null;
3258 if (exchangeImportanceValue != null) {
3259 value = importanceToPriorityMap.get(exchangeImportanceValue);
3260 }
3261 return value;
3262 }
3263
3264 protected String convertPriorityToExchange(String vTodoPriorityValue) {
3265 String value = null;
3266 if (vTodoPriorityValue != null) {
3267 value = priorityToImportanceMap.get(vTodoPriorityValue);
3268 }
3269 return value;
3270 }
3271
3272
3273
3274
3275
3276
3277 protected String convertClassFromExchange(String sensitivity) {
3278 String eventClass;
3279 if ("private".equals(sensitivity)) {
3280 eventClass = "PRIVATE";
3281 } else if ("confidential".equals(sensitivity)) {
3282 eventClass = "CONFIDENTIAL";
3283 } else if ("personal".equals(sensitivity)) {
3284 eventClass = "PRIVATE";
3285 } else {
3286
3287 eventClass = "PUBLIC";
3288 }
3289 return eventClass;
3290 }
3291
3292 protected String convertClassToExchange(String eventClass) {
3293 String sensitivity;
3294 if ("PRIVATE".equals(eventClass)) {
3295 sensitivity = "Private";
3296 } else if ("CONFIDENTIAL".equals(eventClass)) {
3297 sensitivity = "Confidential";
3298 } else {
3299
3300 sensitivity = "Normal";
3301 }
3302 return sensitivity;
3303 }
3304
3305 }