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