mirror of
https://github.com/moparisthebest/davmail
synced 2024-12-13 03:02:22 -05:00
Caldav: major refactoring of event content handling and notifications
git-svn-id: http://svn.code.sf.net/p/davmail/code/trunk@1322 3d1905a2-6b24-0410-a738-b14d5a86fcbd
This commit is contained in:
parent
bba9c3616a
commit
bd687b813f
@ -39,8 +39,6 @@ import javax.mail.Address;
|
||||
import javax.mail.MessagingException;
|
||||
import javax.mail.internet.InternetAddress;
|
||||
import javax.mail.internet.MimeMessage;
|
||||
import javax.mail.internet.MimeMultipart;
|
||||
import javax.mail.internet.MimePart;
|
||||
import javax.mail.util.SharedByteArrayInputStream;
|
||||
import java.io.*;
|
||||
import java.net.NoRouteToHostException;
|
||||
@ -1821,15 +1819,15 @@ public abstract class ExchangeSession {
|
||||
*/
|
||||
public abstract class Event extends Item {
|
||||
protected String contentClass;
|
||||
protected String itemBody;
|
||||
protected VCalendar vCalendar;
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public Event(String folderPath, String itemName, String contentClass, String itemBody, String etag, String noneMatch) {
|
||||
public Event(String folderPath, String itemName, String contentClass, String itemBody, String etag, String noneMatch) throws IOException {
|
||||
super(folderPath, itemName, etag, noneMatch);
|
||||
this.contentClass = contentClass;
|
||||
this.itemBody = itemBody;
|
||||
fixICS(itemBody.getBytes("UTF-8"), false);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -1843,116 +1841,36 @@ public abstract class ExchangeSession {
|
||||
return "text/calendar;charset=UTF-8";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getBody() throws IOException {
|
||||
if (vCalendar == null) {
|
||||
fixICS(getEventContent(), true);
|
||||
}
|
||||
return vCalendar.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Load ICS content from MIME message input stream
|
||||
* Retrieve item body from Exchange
|
||||
*
|
||||
* @param mimeInputStream mime message input stream
|
||||
* @return mime message ics attachment body
|
||||
* @throws IOException on error
|
||||
* @throws MessagingException on error
|
||||
* @return item content
|
||||
* @throws HttpException on error
|
||||
*/
|
||||
protected String getICS(InputStream mimeInputStream) throws IOException, MessagingException {
|
||||
String result;
|
||||
MimeMessage mimeMessage = new MimeMessage(null, mimeInputStream);
|
||||
Object mimeBody = mimeMessage.getContent();
|
||||
MimePart bodyPart = null;
|
||||
if (mimeBody instanceof MimeMultipart) {
|
||||
bodyPart = getCalendarMimePart((MimeMultipart) mimeBody);
|
||||
} else if (isCalendarContentType(mimeMessage.getContentType())) {
|
||||
// no multipart, single body
|
||||
bodyPart = mimeMessage;
|
||||
}
|
||||
|
||||
if (bodyPart != null) {
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
bodyPart.getDataHandler().writeTo(baos);
|
||||
baos.close();
|
||||
result = new String(baos.toByteArray(), "UTF-8");
|
||||
} else {
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
mimeMessage.writeTo(baos);
|
||||
baos.close();
|
||||
throw new DavMailException("EXCEPTION_INVALID_MESSAGE_CONTENT", new String(baos.toByteArray(), "UTF-8"));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
protected static final String TEXT_CALENDAR = "text/calendar";
|
||||
protected static final String APPLICATION_ICS = "application/ics";
|
||||
|
||||
protected boolean isCalendarContentType(String contentType) {
|
||||
return TEXT_CALENDAR.regionMatches(true, 0, contentType, 0, TEXT_CALENDAR.length()) ||
|
||||
APPLICATION_ICS.regionMatches(true, 0, contentType, 0, APPLICATION_ICS.length());
|
||||
}
|
||||
|
||||
protected MimePart getCalendarMimePart(MimeMultipart multiPart) throws IOException, MessagingException {
|
||||
MimePart bodyPart = null;
|
||||
for (int i = 0; i < multiPart.getCount(); i++) {
|
||||
String contentType = multiPart.getBodyPart(i).getContentType();
|
||||
if (isCalendarContentType(contentType)) {
|
||||
bodyPart = (MimePart) multiPart.getBodyPart(i);
|
||||
break;
|
||||
} else if (contentType.startsWith("multipart")) {
|
||||
Object content = multiPart.getBodyPart(i).getContent();
|
||||
if (content instanceof MimeMultipart) {
|
||||
bodyPart = getCalendarMimePart((MimeMultipart) content);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return bodyPart;
|
||||
}
|
||||
|
||||
protected String fixTimezoneId(String line, String validTimezoneId) {
|
||||
return StringUtil.replaceToken(line, "TZID=", ":", validTimezoneId);
|
||||
}
|
||||
|
||||
protected void splitExDate(ICSBufferedWriter result, String line) {
|
||||
int cur = line.lastIndexOf(':') + 1;
|
||||
String start = line.substring(0, cur);
|
||||
|
||||
for (int next = line.indexOf(',', cur); next != -1; next = line.indexOf(',', cur)) {
|
||||
String val = line.substring(cur, next);
|
||||
result.writeLine(start + val);
|
||||
|
||||
cur = next + 1;
|
||||
}
|
||||
|
||||
result.writeLine(start + line.substring(cur));
|
||||
}
|
||||
|
||||
protected String getAllDayLine(String line) throws IOException {
|
||||
int valueIndex = line.lastIndexOf(':');
|
||||
int valueEndIndex = line.lastIndexOf('T');
|
||||
if (valueIndex < 0 || valueEndIndex < 0) {
|
||||
throw new DavMailException("EXCEPTION_INVALID_ICS_LINE", line);
|
||||
}
|
||||
int keyIndex = line.indexOf(';');
|
||||
if (keyIndex == -1) {
|
||||
keyIndex = valueIndex;
|
||||
}
|
||||
String dateValue = line.substring(valueIndex + 1, valueEndIndex);
|
||||
String key = line.substring(0, Math.min(keyIndex, valueIndex));
|
||||
return key + ";VALUE=DATE:" + dateValue;
|
||||
}
|
||||
|
||||
protected String fixICS(String icsBody, boolean fromServer) throws IOException {
|
||||
|
||||
dumpIndex++;
|
||||
dumpICS(icsBody, fromServer, false);
|
||||
public abstract byte[] getEventContent() throws IOException;
|
||||
|
||||
protected void fixICS(byte[] icsContent, boolean fromServer) throws IOException {
|
||||
if (LOGGER.isDebugEnabled() && fromServer) {
|
||||
LOGGER.debug("Vcalendar body received from server:\n" +icsBody);
|
||||
dumpIndex++;
|
||||
String icsBody = new String(icsContent);
|
||||
dumpICS(icsBody, fromServer, false);
|
||||
LOGGER.debug("Vcalendar body received from server:\n" + icsBody);
|
||||
}
|
||||
VCalendar vCalendar = new VCalendar(icsBody, getEmail(), getVTimezone());
|
||||
vCalendar = new VCalendar(icsContent, getEmail(), getVTimezone());
|
||||
vCalendar.fixVCalendar(fromServer);
|
||||
String resultString = vCalendar.toString();
|
||||
if (LOGGER.isDebugEnabled() && !fromServer) {
|
||||
LOGGER.debug("Fixed Vcalendar body to server:\n" +resultString);
|
||||
String resultString = vCalendar.toString();
|
||||
LOGGER.debug("Fixed Vcalendar body to server:\n" + resultString);
|
||||
dumpICS(resultString, fromServer, true);
|
||||
}
|
||||
dumpICS(resultString, fromServer, true);
|
||||
|
||||
return resultString;
|
||||
}
|
||||
|
||||
protected void dumpICS(String icsBody, boolean fromServer, boolean after) {
|
||||
@ -2020,126 +1938,6 @@ public abstract class ExchangeSession {
|
||||
|
||||
}
|
||||
|
||||
protected String getICSValue(String icsBody, String prefix, String defval) throws IOException {
|
||||
// only return values in VEVENT section, not VALARM
|
||||
Stack<String> sectionStack = new Stack<String>();
|
||||
BufferedReader reader = null;
|
||||
|
||||
try {
|
||||
reader = new ICSBufferedReader(new StringReader(icsBody));
|
||||
String line;
|
||||
while ((line = reader.readLine()) != null) {
|
||||
if (line.startsWith("BEGIN:")) {
|
||||
sectionStack.push(line);
|
||||
} else if (line.startsWith("END:") && !sectionStack.isEmpty()) {
|
||||
sectionStack.pop();
|
||||
} else if (!sectionStack.isEmpty() && "BEGIN:VEVENT".equals(sectionStack.peek()) && line.startsWith(prefix)) {
|
||||
return line.substring(prefix.length());
|
||||
}
|
||||
}
|
||||
|
||||
} finally {
|
||||
if (reader != null) {
|
||||
reader.close();
|
||||
}
|
||||
}
|
||||
|
||||
return defval;
|
||||
}
|
||||
|
||||
protected String getICSSummary(String icsBody) throws IOException {
|
||||
return getICSValue(icsBody, "SUMMARY:", BundleMessage.format("MEETING_REQUEST"));
|
||||
}
|
||||
|
||||
protected String getICSDescription(String icsBody) throws IOException {
|
||||
return getICSValue(icsBody, "DESCRIPTION:", "");
|
||||
}
|
||||
|
||||
class Participants {
|
||||
String attendees;
|
||||
String optionalAttendees;
|
||||
String organizer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse ics event for attendees and organizer.
|
||||
* For notifications, only include attendees with RSVP=TRUE or PARTSTAT=NEEDS-ACTION
|
||||
*
|
||||
* @param isNotification get only notified attendees
|
||||
* @return participants
|
||||
* @throws IOException on error
|
||||
*/
|
||||
protected Participants getParticipants(boolean isNotification) throws IOException {
|
||||
HashSet<String> attendees = new HashSet<String>();
|
||||
HashSet<String> optionalAttendees = new HashSet<String>();
|
||||
String organizer = null;
|
||||
BufferedReader reader = null;
|
||||
try {
|
||||
reader = new ICSBufferedReader(new StringReader(itemBody));
|
||||
String line;
|
||||
while ((line = reader.readLine()) != null) {
|
||||
int index = line.indexOf(':');
|
||||
if (index >= 0) {
|
||||
String key = line.substring(0, index);
|
||||
String value = line.substring(index + 1);
|
||||
int semiColon = key.indexOf(';');
|
||||
if (semiColon >= 0) {
|
||||
key = key.substring(0, semiColon);
|
||||
}
|
||||
if ("ORGANIZER".equals(key) || "ATTENDEE".equals(key)) {
|
||||
int colonIndex = value.indexOf(':');
|
||||
if (colonIndex >= 0) {
|
||||
value = value.substring(colonIndex + 1);
|
||||
}
|
||||
value = replaceIcal4Principal(value);
|
||||
if ("ORGANIZER".equals(key)) {
|
||||
organizer = value;
|
||||
// exclude current user and invalid values from recipients
|
||||
// also exclude no action attendees
|
||||
} else if (!email.equalsIgnoreCase(value) && value.indexOf('@') >= 0
|
||||
// return all attendees for user calendar folder, filter for notifications
|
||||
&& (!isNotification
|
||||
// notify attendee if reply explicitly requested
|
||||
|| (line.indexOf("RSVP=TRUE") >= 0)
|
||||
|| (
|
||||
// workaround for iCal bug: do not notify if reply explicitly not requested
|
||||
!(line.indexOf("RSVP=FALSE") >= 0) &&
|
||||
((line.indexOf("PARTSTAT=NEEDS-ACTION") >= 0
|
||||
// need to include other PARTSTATs participants for CANCEL notifications
|
||||
|| line.indexOf("PARTSTAT=ACCEPTED") >= 0
|
||||
|| line.indexOf("PARTSTAT=DECLINED") >= 0
|
||||
|| line.indexOf("PARTSTAT=TENTATIVE") >= 0))
|
||||
))) {
|
||||
if (line.indexOf("ROLE=OPT-PARTICIPANT") >= 0) {
|
||||
optionalAttendees.add(value);
|
||||
} else {
|
||||
attendees.add(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
if (reader != null) {
|
||||
reader.close();
|
||||
}
|
||||
}
|
||||
Participants participants = new Participants();
|
||||
participants.attendees = StringUtil.join(attendees, ", ");
|
||||
participants.optionalAttendees = StringUtil.join(optionalAttendees, ", ");
|
||||
participants.organizer = organizer;
|
||||
return participants;
|
||||
}
|
||||
|
||||
protected String getICSMethod(String icsBody) {
|
||||
String icsMethod = StringUtil.getToken(icsBody, "METHOD:", "\r");
|
||||
if (icsMethod == null) {
|
||||
// default method is REQUEST
|
||||
icsMethod = "REQUEST";
|
||||
}
|
||||
return icsMethod;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build Mime body for event or event message.
|
||||
*
|
||||
@ -2150,7 +1948,7 @@ public abstract class ExchangeSession {
|
||||
String boundary = UUID.randomUUID().toString();
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
MimeOutputStreamWriter writer = new MimeOutputStreamWriter(baos);
|
||||
String method = getICSMethod(itemBody);
|
||||
String method = vCalendar.getMethod();
|
||||
|
||||
writer.writeHeader("Content-Transfer-Encoding", "7bit");
|
||||
writer.writeHeader("Content-class", contentClass);
|
||||
@ -2158,53 +1956,54 @@ public abstract class ExchangeSession {
|
||||
writer.writeHeader("Date", new Date());
|
||||
|
||||
// Make sure invites have a proper subject line
|
||||
writer.writeHeader("Subject", getICSSummary(itemBody));
|
||||
// TODO: get current user attendee status, i18n
|
||||
String subject = vCalendar.getFirstVevent().getPropertyValue("SUMMARY");
|
||||
if (subject == null) {
|
||||
subject = BundleMessage.format("MEETING_REQUEST");
|
||||
}
|
||||
writer.writeHeader("Subject", subject);
|
||||
|
||||
if ("urn:content-classes:calendarmessage".equals(contentClass)) {
|
||||
// need to parse attendees and organizer to build recipients
|
||||
Participants participants = getParticipants(true);
|
||||
if (email.equalsIgnoreCase(participants.organizer)) {
|
||||
VCalendar.Recipients recipients = vCalendar.getRecipients(true);
|
||||
if (email.equalsIgnoreCase(recipients.organizer)) {
|
||||
// current user is organizer => notify all
|
||||
writer.writeHeader("To", participants.attendees);
|
||||
writer.writeHeader("Cc", participants.optionalAttendees);
|
||||
writer.writeHeader("To", recipients.attendees);
|
||||
writer.writeHeader("Cc", recipients.optionalAttendees);
|
||||
// do not send notification if no recipients found
|
||||
if (participants.attendees == null && participants.optionalAttendees == null) {
|
||||
if (recipients.attendees == null && recipients.optionalAttendees == null) {
|
||||
return null;
|
||||
}
|
||||
} else {
|
||||
// notify only organizer
|
||||
writer.writeHeader("To", participants.organizer);
|
||||
writer.writeHeader("To", recipients.organizer);
|
||||
// do not send notification if no recipients found
|
||||
if (participants.organizer == null) {
|
||||
if (recipients.organizer == null) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
// need to parse attendees and organizer to build recipients
|
||||
Participants participants = getParticipants(false);
|
||||
VCalendar.Recipients recipients = vCalendar.getRecipients(false);
|
||||
// storing appointment, full recipients header
|
||||
if (participants.attendees != null) {
|
||||
writer.writeHeader("To", participants.attendees);
|
||||
if (recipients.attendees != null) {
|
||||
writer.writeHeader("To", recipients.attendees);
|
||||
} else {
|
||||
// use current user as attendee
|
||||
writer.writeHeader("To", email);
|
||||
}
|
||||
writer.writeHeader("Cc", participants.optionalAttendees);
|
||||
writer.writeHeader("Cc", recipients.optionalAttendees);
|
||||
|
||||
if (participants.organizer != null) {
|
||||
writer.writeHeader("From", participants.organizer);
|
||||
if (recipients.organizer != null) {
|
||||
writer.writeHeader("From", recipients.organizer);
|
||||
} else {
|
||||
writer.writeHeader("From", email);
|
||||
}
|
||||
// if not organizer, set REPLYTIME to force Outlook in attendee mode
|
||||
if (participants.organizer != null && !email.equalsIgnoreCase(participants.organizer)) {
|
||||
if (itemBody.indexOf("METHOD:") < 0) {
|
||||
itemBody = itemBody.replaceAll("BEGIN:VCALENDAR", "BEGIN:VCALENDAR\r\nMETHOD:REQUEST");
|
||||
}
|
||||
if (itemBody.indexOf("X-MICROSOFT-CDO-REPLYTIME") < 0) {
|
||||
itemBody = itemBody.replaceAll("END:VEVENT", "X-MICROSOFT-CDO-REPLYTIME:" +
|
||||
getZuluDateFormat().format(new Date()) + "\r\nEND:VEVENT");
|
||||
if (recipients.organizer != null && !email.equalsIgnoreCase(recipients.organizer)) {
|
||||
if (method == null) {
|
||||
vCalendar.setPropertyValue("METHOD", "REQUEST");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -2218,9 +2017,9 @@ public abstract class ExchangeSession {
|
||||
|
||||
// Write a part of the message that contains the
|
||||
// ICS description so that invites contain the description text
|
||||
String description = getICSDescription(itemBody).replaceAll("\\\\[Nn]", "\r\n");
|
||||
String description = vCalendar.getFirstVevent().getPropertyValue("DESCRIPTION");
|
||||
|
||||
if (description.length() > 0) {
|
||||
if (description != null && description.length() > 0) {
|
||||
writer.writeHeader("Content-Type", "text/plain;\r\n" +
|
||||
"\tcharset=\"utf-8\"");
|
||||
writer.writeHeader("content-transfer-encoding", "8bit");
|
||||
@ -2238,7 +2037,7 @@ public abstract class ExchangeSession {
|
||||
writer.writeHeader("Content-Transfer-Encoding", "8bit");
|
||||
writer.writeLn();
|
||||
writer.flush();
|
||||
baos.write(fixICS(itemBody, false).getBytes("UTF-8"));
|
||||
baos.write(vCalendar.toString().getBytes("UTF-8"));
|
||||
writer.writeLn();
|
||||
writer.writeLn("------=_NextPart_" + boundary + "--");
|
||||
writer.close();
|
||||
@ -2251,21 +2050,7 @@ public abstract class ExchangeSession {
|
||||
* @return action result
|
||||
* @throws IOException on error
|
||||
*/
|
||||
public ItemResult createOrUpdate() throws IOException {
|
||||
byte[] mimeContent = createMimeContent();
|
||||
ItemResult itemResult;
|
||||
if (mimeContent != null) {
|
||||
itemResult = createOrUpdate(mimeContent);
|
||||
} else {
|
||||
itemResult = new ItemResult();
|
||||
itemResult.status = HttpStatus.SC_NO_CONTENT;
|
||||
}
|
||||
|
||||
return itemResult;
|
||||
|
||||
}
|
||||
|
||||
protected abstract ItemResult createOrUpdate(byte[] mimeContent) throws IOException;
|
||||
public abstract ItemResult createOrUpdate() throws IOException;
|
||||
|
||||
}
|
||||
|
||||
@ -2546,7 +2331,7 @@ public abstract class ExchangeSession {
|
||||
properties.put("outlookmessageclass", "IPM.Contact");
|
||||
|
||||
VObject vcard = new VObject(new ICSBufferedReader(new StringReader(itemBody)));
|
||||
for (VProperty property:vcard.getProperties()) {
|
||||
for (VProperty property : vcard.getProperties()) {
|
||||
if ("FN".equals(property.getKey())) {
|
||||
properties.put("cn", property.getValue());
|
||||
properties.put("subject", property.getValue());
|
||||
@ -3129,7 +2914,7 @@ public abstract class ExchangeSession {
|
||||
}
|
||||
|
||||
FreeBusy freeBusy = null;
|
||||
String fbdata = getFreeBusyData(attendee, exchangeZuluDateFormat.format(startDate), exchangeZuluDateFormat.format(endDate), FREE_BUSY_INTERVAL);
|
||||
String fbdata = getFreeBusyData(attendee, exchangeZuluDateFormat.format(startDate), exchangeZuluDateFormat.format(endDate), FREE_BUSY_INTERVAL);
|
||||
if (fbdata != null) {
|
||||
freeBusy = new FreeBusy(icalDateFormat, startDate, fbdata);
|
||||
}
|
||||
@ -3220,21 +3005,6 @@ public abstract class ExchangeSession {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Timezone structure
|
||||
*/
|
||||
public static final class VTimezone {
|
||||
/**
|
||||
* Timezone iCalendar body
|
||||
*/
|
||||
public String timezoneBody;
|
||||
/**
|
||||
* Timezone id
|
||||
*/
|
||||
public String timezoneId;
|
||||
|
||||
}
|
||||
|
||||
protected VObject vTimezone;
|
||||
|
||||
/**
|
||||
|
@ -19,11 +19,13 @@
|
||||
package davmail.exchange;
|
||||
|
||||
import davmail.Settings;
|
||||
import davmail.util.StringUtil;
|
||||
import org.apache.log4j.Logger;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.StringReader;
|
||||
import java.io.*;
|
||||
import java.util.Date;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* VCalendar object.
|
||||
@ -67,6 +69,18 @@ public class VCalendar extends VObject {
|
||||
this(new ICSBufferedReader(new StringReader(vCalendarBody)), email, vTimezone);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create VCalendar object from string;
|
||||
*
|
||||
* @param vCalendarContent item content
|
||||
* @param email current user email
|
||||
* @param vTimezone user OWA timezone
|
||||
* @throws IOException on error
|
||||
*/
|
||||
public VCalendar(byte[] vCalendarContent, String email, VObject vTimezone) throws IOException {
|
||||
this(new ICSBufferedReader(new InputStreamReader(new ByteArrayInputStream(vCalendarContent), "UTF-8")), email, vTimezone);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void addVObject(VObject vObject) {
|
||||
super.addVObject(vObject);
|
||||
@ -83,28 +97,19 @@ public class VCalendar extends VObject {
|
||||
return dtstart != null && dtstart.hasParam("VALUE", "DATE");
|
||||
}
|
||||
|
||||
protected boolean hasCdoAllDay(VObject vObject) {
|
||||
return vObject.getProperty("X-MICROSOFT-CDO-ALLDAYEVENT") != null;
|
||||
}
|
||||
|
||||
protected boolean hasCdoBusyStatus(VObject vObject) {
|
||||
return vObject.getProperty("X-MICROSOFT-CDO-BUSYSTATUS") != null;
|
||||
}
|
||||
|
||||
protected boolean isCdoAllDay(VObject vObject) {
|
||||
return "TRUE".equals(vObject.getPropertyValue("X-MICROSOFT-CDO-ALLDAYEVENT"));
|
||||
}
|
||||
|
||||
protected boolean isAppleiCal() {
|
||||
return getPropertyValue("PRODID").contains("iCal");
|
||||
}
|
||||
|
||||
protected String getOrganizer() {
|
||||
String organizer = firstVevent.getPropertyValue("ORGANIZER");
|
||||
if (organizer.startsWith("MAILTO:")) {
|
||||
return organizer.substring(7);
|
||||
protected String getEmailValue(VProperty property) {
|
||||
if (property == null) {
|
||||
return null;
|
||||
}
|
||||
String propertyValue = property.getValue();
|
||||
if (propertyValue != null && (propertyValue.startsWith("MAILTO:") || propertyValue.startsWith("mailto:"))) {
|
||||
return propertyValue.substring(7);
|
||||
} else {
|
||||
return organizer;
|
||||
return propertyValue;
|
||||
}
|
||||
}
|
||||
|
||||
@ -113,23 +118,21 @@ public class VCalendar extends VObject {
|
||||
}
|
||||
|
||||
protected void fixVCalendar(boolean fromServer) {
|
||||
// append missing method
|
||||
if (getProperty("METHOD") == null) {
|
||||
setPropertyValue("METHOD", "PUBLISH");
|
||||
}
|
||||
// iCal 4 global private flag
|
||||
if (fromServer) {
|
||||
setPropertyValue("X-CALENDARSERVER-ACCESS", getCalendarServerAccess());
|
||||
}
|
||||
|
||||
// iCal 4 global X-CALENDARSERVER-ACCESS
|
||||
String calendarServerAccess = getPropertyValue("X-CALENDARSERVER-ACCESS");
|
||||
String now = ExchangeSession.getZuluDateFormat().format(new Date());
|
||||
|
||||
// TODO: patch timezone for iPhone
|
||||
// iterate over vObjects
|
||||
for (VObject vObject : vObjects) {
|
||||
if ("VEVENT".equals(vObject.type)) {
|
||||
if (calendarServerAccess != null) {
|
||||
vObject.setPropertyValue("CLASS", getEventClass(calendarServerAccess));
|
||||
// iCal 3, get X-CALENDARSERVER-ACCESS from local VEVENT
|
||||
} else if (vObject.getPropertyValue("X-CALENDARSERVER-ACCESS") != null) {
|
||||
vObject.setPropertyValue("CLASS", getEventClass(vObject.getPropertyValue("X-CALENDARSERVER-ACCESS")));
|
||||
}
|
||||
@ -143,17 +146,24 @@ public class VCalendar extends VObject {
|
||||
setClientAllday(vObject.getProperty("DTSTART"));
|
||||
setClientAllday(vObject.getProperty("DTEND"));
|
||||
}
|
||||
vObject.setPropertyValue("TRANSP",
|
||||
!"FREE".equals(vObject.getPropertyValue("X-MICROSOFT-CDO-BUSYSTATUS")) ? "OPAQUE" : "TRANSPARENT");
|
||||
String cdoBusyStatus = vObject.getPropertyValue("X-MICROSOFT-CDO-BUSYSTATUS");
|
||||
if (cdoBusyStatus != null) {
|
||||
vObject.setPropertyValue("TRANSP",
|
||||
!"FREE".equals(cdoBusyStatus) ? "OPAQUE" : "TRANSPARENT");
|
||||
}
|
||||
|
||||
// TODO splitExDate
|
||||
// Apple iCal doesn't understand this key, and it's entourage
|
||||
// specific (i.e. not needed by any caldav client): strip it out
|
||||
vObject.removeProperty("X-ENTOURAGE_UUID");
|
||||
|
||||
splitExDate(vObject);
|
||||
} else {
|
||||
// add organizer line to all events created in Exchange for active sync
|
||||
if (vObject.getPropertyValue("ORGANIZER") == null) {
|
||||
String organizer = getEmailValue(vObject.getProperty("ORGANIZER"));
|
||||
if (organizer == null) {
|
||||
vObject.setPropertyValue("ORGANIZER", "MAILTO:" + email);
|
||||
} else if (!email.equalsIgnoreCase(organizer) && vObject.getProperty("X-MICROSOFT-CDO-REPLYTIME") == null) {
|
||||
vObject.setPropertyValue("X-MICROSOFT-CDO-REPLYTIME", now);
|
||||
}
|
||||
// set OWA allday flag
|
||||
vObject.setPropertyValue("X-MICROSOFT-CDO-ALLDAYEVENT", isAllDay(vObject) ? "TRUE" : "FALSE");
|
||||
@ -169,15 +179,31 @@ public class VCalendar extends VObject {
|
||||
|
||||
fixAttendees(vObject, fromServer);
|
||||
|
||||
// TODO handle BUSYSTATUS
|
||||
|
||||
fixAlarm(vObject, fromServer);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private void setServerAllday(VProperty property) {
|
||||
protected void splitExDate(VObject vObject) {
|
||||
List<VProperty> exDateProperties = vObject.getProperties("EXDATE");
|
||||
if (exDateProperties != null) {
|
||||
for (VProperty property : exDateProperties) {
|
||||
String value = property.getValue();
|
||||
if (value.indexOf(',') >= 0) {
|
||||
// split property
|
||||
vObject.removeProperty(property);
|
||||
for (String singleValue : value.split(",")) {
|
||||
VProperty singleProperty = new VProperty("EXDATE", singleValue);
|
||||
singleProperty.setParams(property.getParams());
|
||||
vObject.addProperty(singleProperty);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected void setServerAllday(VProperty property) {
|
||||
// set TZID param
|
||||
if (!property.hasParam("TZID")) {
|
||||
property.addParam("TZID", vTimezone.getPropertyValue("TZID"));
|
||||
@ -288,6 +314,7 @@ public class VCalendar extends VObject {
|
||||
* Convert X-CALENDARSERVER-ACCESS to CLASS.
|
||||
* see http://svn.calendarserver.org/repository/calendarserver/CalendarServer/trunk/doc/Extensions/caldav-privateevents.txt
|
||||
*
|
||||
* @param calendarServerAccess X-CALENDARSERVER-ACCESS value
|
||||
* @return CLASS value
|
||||
*/
|
||||
protected String getEventClass(String calendarServerAccess) {
|
||||
@ -303,6 +330,7 @@ public class VCalendar extends VObject {
|
||||
/**
|
||||
* Convert CLASS to X-CALENDARSERVER-ACCESS.
|
||||
* see http://svn.calendarserver.org/repository/calendarserver/CalendarServer/trunk/doc/Extensions/caldav-privateevents.txt *
|
||||
*
|
||||
* @return X-CALENDARSERVER-ACCESS value
|
||||
*/
|
||||
protected String getCalendarServerAccess() {
|
||||
@ -316,4 +344,54 @@ public class VCalendar extends VObject {
|
||||
}
|
||||
}
|
||||
|
||||
public VObject getFirstVevent() {
|
||||
return firstVevent;
|
||||
}
|
||||
|
||||
class Recipients {
|
||||
String attendees;
|
||||
String optionalAttendees;
|
||||
String organizer;
|
||||
}
|
||||
|
||||
public Recipients getRecipients(boolean isNotification) {
|
||||
|
||||
HashSet<String> attendees = new HashSet<String>();
|
||||
HashSet<String> optionalAttendees = new HashSet<String>();
|
||||
|
||||
// get recipients from first VEVENT
|
||||
List<VProperty> attendeeProperties = firstVevent.getProperties("ATTENDEE");
|
||||
if (attendeeProperties != null) {
|
||||
for (VProperty property : attendeeProperties) {
|
||||
// exclude current user and invalid values from recipients
|
||||
// also exclude no action attendees
|
||||
String attendeeEmail = getEmailValue(property);
|
||||
if (!email.equalsIgnoreCase(attendeeEmail) && attendeeEmail.indexOf('@') >= 0
|
||||
// return all attendees for user calendar folder, filter for notifications
|
||||
&& (!isNotification
|
||||
// notify attendee if reply explicitly requested
|
||||
|| (property.hasParam("RSVP", "TRUE"))
|
||||
|| (
|
||||
// workaround for iCal bug: do not notify if reply explicitly not requested
|
||||
!(property.hasParam("RSVP", "FALSE")) &&
|
||||
((property.hasParam("PARTSTAT", "NEEDS-ACTION")
|
||||
// need to include other PARTSTATs participants for CANCEL notifications
|
||||
|| property.hasParam("PARTSTAT", "ACCEPTED")
|
||||
|| property.hasParam("PARTSTAT", "DECLINED")
|
||||
|| property.hasParam("PARTSTAT", "TENTATIVE")))
|
||||
))) {
|
||||
if (property.hasParam("ROLE", "OPT-PARTICIPANT")) {
|
||||
optionalAttendees.add(attendeeEmail);
|
||||
} else {
|
||||
attendees.add(attendeeEmail);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Recipients recipients = new Recipients();
|
||||
recipients.organizer = getEmailValue(firstVevent.getProperty("ORGANIZER"));
|
||||
recipients.attendees = StringUtil.join(attendees, ", ");
|
||||
recipients.optionalAttendees = StringUtil.join(optionalAttendees, ", ");
|
||||
return recipients;
|
||||
}
|
||||
}
|
||||
|
@ -160,6 +160,22 @@ public class VObject {
|
||||
return null;
|
||||
}
|
||||
|
||||
public List<VProperty> getProperties(String name) {
|
||||
List<VProperty> result = null;
|
||||
if (properties != null) {
|
||||
for (VProperty property : properties) {
|
||||
if (property.getKey().equalsIgnoreCase(name)) {
|
||||
if (result == null) {
|
||||
result = new ArrayList<VProperty>();
|
||||
}
|
||||
result.add(property);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public String getPropertyValue(String name) {
|
||||
VProperty property = getProperty(name);
|
||||
if (property != null) {
|
||||
@ -184,11 +200,17 @@ public class VObject {
|
||||
}
|
||||
|
||||
public void removeProperty(String name) {
|
||||
if (vObjects != null) {
|
||||
if (properties != null) {
|
||||
VProperty property = getProperty(name);
|
||||
if (property != null) {
|
||||
vObjects.remove(property);
|
||||
properties.remove(property);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void removeProperty(VProperty property) {
|
||||
if (properties != null) {
|
||||
properties.remove(property);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -29,7 +29,7 @@ public class VProperty {
|
||||
KEY, PARAM_NAME, PARAM_VALUE, QUOTED_PARAM_VALUE, VALUE, BACKSLASH
|
||||
}
|
||||
|
||||
protected class Param {
|
||||
protected static class Param {
|
||||
String name;
|
||||
List<String> values;
|
||||
|
||||
@ -53,6 +53,12 @@ public class VProperty {
|
||||
protected List<Param> params;
|
||||
protected List<String> values;
|
||||
|
||||
/**
|
||||
* Create VProperty for key and value.
|
||||
*
|
||||
* @param name property name
|
||||
* @param value property value
|
||||
*/
|
||||
public VProperty(String name, String value) {
|
||||
setKey(name);
|
||||
setValue(value);
|
||||
@ -197,6 +203,11 @@ public class VProperty {
|
||||
return params != null && getParam(paramName) != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove param from property.
|
||||
*
|
||||
* @param paramName param name
|
||||
*/
|
||||
public void removeParam(String paramName) {
|
||||
if (params != null) {
|
||||
Param param = getParam(paramName);
|
||||
@ -220,7 +231,7 @@ public class VProperty {
|
||||
}
|
||||
|
||||
protected void addParam(String paramName, String paramValue) {
|
||||
List paramValues = new ArrayList();
|
||||
List<String> paramValues = new ArrayList<String>();
|
||||
paramValues.add(paramValue);
|
||||
addParam(paramName, paramValues);
|
||||
}
|
||||
@ -249,6 +260,14 @@ public class VProperty {
|
||||
return null;
|
||||
}
|
||||
|
||||
protected List<Param> getParams() {
|
||||
return params;
|
||||
}
|
||||
|
||||
protected void setParams(List<Param> params) {
|
||||
this.params = params;
|
||||
}
|
||||
|
||||
protected void setValue(String value) {
|
||||
if (value == null) {
|
||||
values = null;
|
||||
@ -280,7 +299,7 @@ public class VProperty {
|
||||
if (c == '\\') {
|
||||
//noinspection AssignmentToForLoopParameter
|
||||
c = value.charAt(++i);
|
||||
if (c == 'n') {
|
||||
if (c == 'n' || c == 'N') {
|
||||
c = '\n';
|
||||
} else if (c == 'r') {
|
||||
c = '\r';
|
||||
|
@ -47,6 +47,9 @@ import org.apache.jackrabbit.webdav.property.DavPropertySet;
|
||||
import org.w3c.dom.Node;
|
||||
|
||||
import javax.mail.MessagingException;
|
||||
import javax.mail.internet.MimeMessage;
|
||||
import javax.mail.internet.MimeMultipart;
|
||||
import javax.mail.internet.MimePart;
|
||||
import java.io.*;
|
||||
import java.net.URL;
|
||||
import java.text.ParseException;
|
||||
@ -763,12 +766,72 @@ public class DavExchangeSession extends ExchangeSession {
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public Event(String folderPath, String itemName, String contentClass, String itemBody, String etag, String noneMatch) {
|
||||
public Event(String folderPath, String itemName, String contentClass, String itemBody, String etag, String noneMatch) throws IOException {
|
||||
super(folderPath, itemName, contentClass, itemBody, etag, noneMatch);
|
||||
}
|
||||
|
||||
protected String getICSFromInternetContentProperty() throws IOException, DavException, MessagingException {
|
||||
String result = null;
|
||||
protected static final String TEXT_CALENDAR = "text/calendar";
|
||||
protected static final String APPLICATION_ICS = "application/ics";
|
||||
|
||||
protected boolean isCalendarContentType(String contentType) {
|
||||
return TEXT_CALENDAR.regionMatches(true, 0, contentType, 0, TEXT_CALENDAR.length()) ||
|
||||
APPLICATION_ICS.regionMatches(true, 0, contentType, 0, APPLICATION_ICS.length());
|
||||
}
|
||||
|
||||
protected MimePart getCalendarMimePart(MimeMultipart multiPart) throws IOException, MessagingException {
|
||||
MimePart bodyPart = null;
|
||||
for (int i = 0; i < multiPart.getCount(); i++) {
|
||||
String contentType = multiPart.getBodyPart(i).getContentType();
|
||||
if (isCalendarContentType(contentType)) {
|
||||
bodyPart = (MimePart) multiPart.getBodyPart(i);
|
||||
break;
|
||||
} else if (contentType.startsWith("multipart")) {
|
||||
Object content = multiPart.getBodyPart(i).getContent();
|
||||
if (content instanceof MimeMultipart) {
|
||||
bodyPart = getCalendarMimePart((MimeMultipart) content);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return bodyPart;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load ICS content from MIME message input stream
|
||||
*
|
||||
* @param mimeInputStream mime message input stream
|
||||
* @return mime message ics attachment body
|
||||
* @throws IOException on error
|
||||
* @throws MessagingException on error
|
||||
*/
|
||||
protected byte[] getICS(InputStream mimeInputStream) throws IOException, MessagingException {
|
||||
byte[] result;
|
||||
MimeMessage mimeMessage = new MimeMessage(null, mimeInputStream);
|
||||
Object mimeBody = mimeMessage.getContent();
|
||||
MimePart bodyPart = null;
|
||||
if (mimeBody instanceof MimeMultipart) {
|
||||
bodyPart = getCalendarMimePart((MimeMultipart) mimeBody);
|
||||
} else if (isCalendarContentType(mimeMessage.getContentType())) {
|
||||
// no multipart, single body
|
||||
bodyPart = mimeMessage;
|
||||
}
|
||||
|
||||
if (bodyPart != null) {
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
bodyPart.getDataHandler().writeTo(baos);
|
||||
baos.close();
|
||||
result = baos.toByteArray();
|
||||
} else {
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
mimeMessage.writeTo(baos);
|
||||
baos.close();
|
||||
throw new DavMailException("EXCEPTION_INVALID_MESSAGE_CONTENT", new String(baos.toByteArray(), "UTF-8"));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
protected byte[] getICSFromInternetContentProperty() throws IOException, DavException, MessagingException {
|
||||
byte[] result = null;
|
||||
// PropFind PR_INTERNET_CONTENT
|
||||
DavPropertyNameSet davPropertyNameSet = new DavPropertyNameSet();
|
||||
davPropertyNameSet.add(Field.getPropertyName("internetContent"));
|
||||
@ -798,8 +861,8 @@ public class DavExchangeSession extends ExchangeSession {
|
||||
* @throws HttpException on error
|
||||
*/
|
||||
@Override
|
||||
public String getBody() throws IOException {
|
||||
String result;
|
||||
public byte[] getEventContent() throws IOException {
|
||||
byte[] result;
|
||||
LOGGER.debug("Get event: " + permanentUrl);
|
||||
// try to get PR_INTERNET_CONTENT
|
||||
try {
|
||||
@ -822,7 +885,7 @@ public class DavExchangeSession extends ExchangeSession {
|
||||
} catch (MessagingException e) {
|
||||
throw buildHttpException(e);
|
||||
}
|
||||
return fixICS(result, true);
|
||||
return result;
|
||||
}
|
||||
|
||||
protected PutMethod internalCreateOrUpdate(String encodedHref, byte[] mimeContent) throws IOException {
|
||||
@ -849,7 +912,8 @@ public class DavExchangeSession extends ExchangeSession {
|
||||
* @inheritDoc
|
||||
*/
|
||||
@Override
|
||||
protected ItemResult createOrUpdate(byte[] mimeContent) throws IOException {
|
||||
public ItemResult createOrUpdate() throws IOException {
|
||||
byte[] mimeContent = createMimeContent();
|
||||
String encodedHref = URIUtil.encodePath(getHref());
|
||||
PutMethod putMethod = internalCreateOrUpdate(encodedHref, mimeContent);
|
||||
int status = putMethod.getStatusCode();
|
||||
|
@ -214,7 +214,7 @@ public class EwsExchangeSession extends ExchangeSession {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get item MIME content.
|
||||
* Get item content.
|
||||
*
|
||||
* @param itemId EWS item id
|
||||
* @return item content as byte array
|
||||
@ -826,18 +826,14 @@ public class EwsExchangeSession extends ExchangeSession {
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
protected Event(String folderPath, String itemName, String contentClass, String itemBody, String etag, String noneMatch) {
|
||||
protected Event(String folderPath, String itemName, String contentClass, String itemBody, String etag, String noneMatch) throws IOException {
|
||||
super(folderPath, itemName, contentClass, itemBody, etag, noneMatch);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ItemResult createOrUpdate() throws IOException {
|
||||
return createOrUpdate(fixICS(itemBody, false).getBytes("UTF-8"));
|
||||
}
|
||||
byte[] itemContent = Base64.encodeBase64(vCalendar.toString().getBytes("UTF-8"));
|
||||
|
||||
|
||||
@Override
|
||||
protected ItemResult createOrUpdate(byte[] mimeContent) throws IOException {
|
||||
ItemResult itemResult = new ItemResult();
|
||||
EWSMethod createOrUpdateItemMethod;
|
||||
|
||||
@ -865,7 +861,7 @@ public class EwsExchangeSession extends ExchangeSession {
|
||||
|
||||
if (currentItemId != null) {
|
||||
Set<FieldUpdate> updates = new HashSet<FieldUpdate>();
|
||||
updates.add(new FieldUpdate(Field.get("mimeContent"), String.valueOf(Base64.encodeBase64(mimeContent))));
|
||||
updates.add(new FieldUpdate(Field.get("mimeContent"), String.valueOf(Base64.encodeBase64(itemContent))));
|
||||
// update
|
||||
createOrUpdateItemMethod = new UpdateItemMethod(MessageDisposition.SaveOnly,
|
||||
ConflictResolution.AlwaysOverwrite,
|
||||
@ -875,7 +871,7 @@ public class EwsExchangeSession extends ExchangeSession {
|
||||
// create
|
||||
EWSMethod.Item newItem = new EWSMethod.Item();
|
||||
newItem.type = "CalendarItem";
|
||||
newItem.mimeContent = Base64.encodeBase64(mimeContent);
|
||||
newItem.mimeContent = itemContent;
|
||||
HashSet<FieldUpdate> updates = new HashSet<FieldUpdate>();
|
||||
// force urlcompname
|
||||
updates.add(Field.createFieldUpdate("urlcompname", convertItemNameToEML(itemName)));
|
||||
@ -908,16 +904,17 @@ public class EwsExchangeSession extends ExchangeSession {
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getBody() throws IOException {
|
||||
String result;
|
||||
LOGGER.debug("Get event: " + permanentUrl);
|
||||
public byte[] getEventContent() throws IOException {
|
||||
byte[] content;
|
||||
if (LOGGER.isDebugEnabled()) {
|
||||
LOGGER.debug("Get event: " + folderPath + '/' + itemName);
|
||||
}
|
||||
try {
|
||||
byte[] content = getContent(itemId);
|
||||
result = new String(content);
|
||||
content = getContent(itemId);
|
||||
} catch (IOException e) {
|
||||
throw buildHttpException(e);
|
||||
}
|
||||
return fixICS(result, true);
|
||||
return content;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -16,7 +16,6 @@ EXCEPTION_INVALID_DATE=Invalid date: {0}
|
||||
EXCEPTION_INVALID_DATES=Invalid dates: {0}
|
||||
EXCEPTION_INVALID_FOLDER_URL=Invalid folder URL: {0}
|
||||
EXCEPTION_INVALID_HEADER=Invalid header: {0}, HTTPS connection to an HTTP listener ?
|
||||
EXCEPTION_INVALID_ICS_LINE=Invalid ICS line: {0}
|
||||
EXCEPTION_INVALID_KEEPALIVE=Invalid Keep-Alive: {0}
|
||||
EXCEPTION_INVALID_MAIL_PATH=Invalid mail path: {0}
|
||||
EXCEPTION_INVALID_MESSAGE_CONTENT=Invalid message content: {0}
|
||||
|
@ -15,7 +15,6 @@ EXCEPTION_INVALID_DATE=Date invalide {0}
|
||||
EXCEPTION_INVALID_DATES=Dates invalides : {0}
|
||||
EXCEPTION_INVALID_FOLDER_URL=URL du dossier invalide : {0}
|
||||
EXCEPTION_INVALID_HEADER=Entête invalide : {0}, connexion HTTPS sur le service HTTP ?
|
||||
EXCEPTION_INVALID_ICS_LINE=Ligne ICS invalide : {0}
|
||||
EXCEPTION_INVALID_KEEPALIVE=Keep-Alive invalide : {0}
|
||||
EXCEPTION_INVALID_MAIL_PATH=Chemin de messagerie invalide : {0}
|
||||
EXCEPTION_INVALID_MESSAGE_CONTENT=Contenu du message invalide : {0}
|
||||
|
Loading…
Reference in New Issue
Block a user