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