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