mirror of
https://github.com/moparisthebest/davmail
synced 2024-12-13 19:22:22 -05:00
Caldav: switch to new VCalendar parser/patcher
git-svn-id: http://svn.code.sf.net/p/davmail/code/trunk@1321 3d1905a2-6b24-0410-a738-b14d5a86fcbd
This commit is contained in:
parent
5bacc44dea
commit
bba9c3616a
@ -684,7 +684,7 @@ public class CaldavConnection extends AbstractConnection {
|
|||||||
appendItemResponse(response, request, item);
|
appendItemResponse(response, request, item);
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
wireLogger.debug(e);
|
wireLogger.debug(e.getMessage(), e);
|
||||||
DavGatewayTray.warn(new BundleMessage("LOG_ITEM_NOT_AVAILABLE", eventName, href));
|
DavGatewayTray.warn(new BundleMessage("LOG_ITEM_NOT_AVAILABLE", eventName, href));
|
||||||
notFound.add(href);
|
notFound.add(href);
|
||||||
}
|
}
|
||||||
|
@ -1867,7 +1867,7 @@ public abstract class ExchangeSession {
|
|||||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||||
bodyPart.getDataHandler().writeTo(baos);
|
bodyPart.getDataHandler().writeTo(baos);
|
||||||
baos.close();
|
baos.close();
|
||||||
result = fixICS(new String(baos.toByteArray(), "UTF-8"), true);
|
result = new String(baos.toByteArray(), "UTF-8");
|
||||||
} else {
|
} else {
|
||||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||||
mimeMessage.writeTo(baos);
|
mimeMessage.writeTo(baos);
|
||||||
@ -1937,265 +1937,19 @@ public abstract class ExchangeSession {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected String fixICS(String icsBody, boolean fromServer) throws IOException {
|
protected String fixICS(String icsBody, boolean fromServer) throws IOException {
|
||||||
// first pass : detect
|
|
||||||
class AllDayState {
|
|
||||||
boolean isAllDay;
|
|
||||||
boolean hasCdoAllDay;
|
|
||||||
boolean isCdoAllDay;
|
|
||||||
}
|
|
||||||
|
|
||||||
dumpIndex++;
|
dumpIndex++;
|
||||||
dumpICS(icsBody, fromServer, false);
|
dumpICS(icsBody, fromServer, false);
|
||||||
|
|
||||||
// Convert event class from and to iCal
|
if (LOGGER.isDebugEnabled() && fromServer) {
|
||||||
// See https://trac.calendarserver.org/browser/CalendarServer/trunk/doc/Extensions/caldav-privateevents.txt
|
LOGGER.debug("Vcalendar body received from server:\n" +icsBody);
|
||||||
boolean isAppleiCal = false;
|
|
||||||
boolean hasAttendee = false;
|
|
||||||
boolean hasCdoBusyStatus = false;
|
|
||||||
// detect ics event with empty timezone (all day from Lightning)
|
|
||||||
boolean hasTimezone = false;
|
|
||||||
String transp = null;
|
|
||||||
String validTimezoneId = null;
|
|
||||||
String eventClass = null;
|
|
||||||
String organizer = null;
|
|
||||||
String action = null;
|
|
||||||
String method = null;
|
|
||||||
boolean sound = false;
|
|
||||||
|
|
||||||
List<AllDayState> allDayStates = new ArrayList<AllDayState>();
|
|
||||||
AllDayState currentAllDayState = new AllDayState();
|
|
||||||
BufferedReader reader = null;
|
|
||||||
try {
|
|
||||||
reader = new ICSBufferedReader(new StringReader(icsBody));
|
|
||||||
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);
|
|
||||||
if ("DTSTART;VALUE=DATE".equals(key)) {
|
|
||||||
currentAllDayState.isAllDay = true;
|
|
||||||
} else if ("X-MICROSOFT-CDO-ALLDAYEVENT".equals(key)) {
|
|
||||||
currentAllDayState.hasCdoAllDay = true;
|
|
||||||
currentAllDayState.isCdoAllDay = "TRUE".equals(value);
|
|
||||||
} else if ("END:VEVENT".equals(line)) {
|
|
||||||
allDayStates.add(currentAllDayState);
|
|
||||||
currentAllDayState = new AllDayState();
|
|
||||||
} else if ("PRODID".equals(key) && line.contains("iCal")) {
|
|
||||||
// detect iCal created events
|
|
||||||
isAppleiCal = true;
|
|
||||||
} else if (isAppleiCal && "X-CALENDARSERVER-ACCESS".equals(key)) {
|
|
||||||
eventClass = value;
|
|
||||||
} else if (!isAppleiCal && "CLASS".equals(key)) {
|
|
||||||
eventClass = value;
|
|
||||||
} else if ("ACTION".equals(key)) {
|
|
||||||
action = value;
|
|
||||||
} else if ("ATTACH;VALUES=URI".equals(key)) {
|
|
||||||
// This is a marker that this event has an alarm with sound
|
|
||||||
sound = true;
|
|
||||||
} else if (key.startsWith("ORGANIZER")) {
|
|
||||||
if (value.startsWith("MAILTO:")) {
|
|
||||||
organizer = value.substring(7);
|
|
||||||
} else {
|
|
||||||
organizer = value;
|
|
||||||
}
|
}
|
||||||
} else if (key.startsWith("ATTENDEE")) {
|
VCalendar vCalendar = new VCalendar(icsBody, getEmail(), getVTimezone());
|
||||||
hasAttendee = true;
|
|
||||||
} else if ("TRANSP".equals(key)) {
|
|
||||||
transp = value;
|
|
||||||
} else if (line.startsWith("TZID:(GMT") ||
|
|
||||||
// additional test for Outlook created recurring events
|
|
||||||
line.startsWith("TZID:GMT ")) {
|
|
||||||
try {
|
|
||||||
validTimezoneId = ResourceBundle.getBundle("timezones").getString(value);
|
|
||||||
} catch (MissingResourceException mre) {
|
|
||||||
LOGGER.warn(new BundleMessage("LOG_INVALID_TIMEZONE", value));
|
|
||||||
}
|
|
||||||
} else if ("X-MICROSOFT-CDO-BUSYSTATUS".equals(key)) {
|
|
||||||
hasCdoBusyStatus = true;
|
|
||||||
} else if ("BEGIN:VTIMEZONE".equals(line)) {
|
|
||||||
hasTimezone = true;
|
|
||||||
} else if ("METHOD".equals(key)) {
|
|
||||||
method = value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
if (reader != null) {
|
|
||||||
reader.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// second pass : fix
|
|
||||||
int count = 0;
|
|
||||||
ICSBufferedWriter result = new ICSBufferedWriter();
|
|
||||||
try {
|
|
||||||
reader = new ICSBufferedReader(new StringReader(icsBody));
|
|
||||||
String line;
|
|
||||||
|
|
||||||
while ((line = reader.readLine()) != null) {
|
|
||||||
// remove empty properties
|
|
||||||
if ("CLASS:".equals(line) || "LOCATION:".equals(line)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
// fix invalid exchange timezoneid
|
|
||||||
if (validTimezoneId != null && line.indexOf(";TZID=") >= 0) {
|
|
||||||
line = fixTimezoneId(line, validTimezoneId);
|
|
||||||
}
|
|
||||||
if (!fromServer && "BEGIN:VCALENDAR".equals(line) && method == null) {
|
|
||||||
result.writeLine(line);
|
|
||||||
// append missing method
|
|
||||||
if (method == null) {
|
|
||||||
result.writeLine("METHOD:PUBLISH");
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (fromServer && line.startsWith("PRODID:") && eventClass != null) {
|
|
||||||
result.writeLine(line);
|
|
||||||
// set global calendarserver access for iCal 4
|
|
||||||
if ("PRIVATE".equalsIgnoreCase(eventClass)) {
|
|
||||||
result.writeLine("X-CALENDARSERVER-ACCESS:CONFIDENTIAL");
|
|
||||||
} else if ("CONFIDENTIAL".equalsIgnoreCase(eventClass)) {
|
|
||||||
result.writeLine("X-CALENDARSERVER-ACCESS:PRIVATE");
|
|
||||||
} else if (eventClass != null) {
|
|
||||||
result.writeLine("X-CALENDARSERVER-ACCESS:" + eventClass);
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (!fromServer && "BEGIN:VEVENT".equals(line) && !hasTimezone) {
|
|
||||||
result.write(ExchangeSession.this.getVTimezone().timezoneBody);
|
|
||||||
hasTimezone = true;
|
|
||||||
}
|
|
||||||
if (!fromServer && currentAllDayState.isAllDay && "X-MICROSOFT-CDO-ALLDAYEVENT:FALSE".equals(line)) {
|
|
||||||
line = "X-MICROSOFT-CDO-ALLDAYEVENT:TRUE";
|
|
||||||
} else if (!fromServer && "END:VEVENT".equals(line)) {
|
|
||||||
if (!hasCdoBusyStatus) {
|
|
||||||
result.writeLine("X-MICROSOFT-CDO-BUSYSTATUS:" + (!"TRANSPARENT".equals(transp) ? "BUSY" : "FREE"));
|
|
||||||
}
|
|
||||||
if (currentAllDayState.isAllDay && !currentAllDayState.hasCdoAllDay) {
|
|
||||||
result.writeLine("X-MICROSOFT-CDO-ALLDAYEVENT:TRUE");
|
|
||||||
}
|
|
||||||
// add organizer line to all events created in Exchange for active sync
|
|
||||||
if (organizer == null) {
|
|
||||||
result.writeLine("ORGANIZER:MAILTO:" + email);
|
|
||||||
}
|
|
||||||
if (isAppleiCal) {
|
|
||||||
if ("CONFIDENTIAL".equalsIgnoreCase(eventClass)) {
|
|
||||||
result.writeLine("CLASS:PRIVATE");
|
|
||||||
} else if ("PRIVATE".equalsIgnoreCase(eventClass)) {
|
|
||||||
result.writeLine("CLASS:CONFIDENTIAL");
|
|
||||||
} else if (eventClass != null) {
|
|
||||||
result.writeLine("CLASS:" + eventClass);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (!fromServer && line.startsWith("X-MICROSOFT-CDO-BUSYSTATUS:")) {
|
|
||||||
line = "X-MICROSOFT-CDO-BUSYSTATUS:" + (!"TRANSPARENT".equals(transp) ? "BUSY" : "FREE");
|
|
||||||
} else if (!fromServer && !currentAllDayState.isAllDay && "X-MICROSOFT-CDO-ALLDAYEVENT:TRUE".equals(line)) {
|
|
||||||
line = "X-MICROSOFT-CDO-ALLDAYEVENT:FALSE";
|
|
||||||
} else if (fromServer && currentAllDayState.isCdoAllDay && line.startsWith("DTSTART") && !line.startsWith("DTSTART;VALUE=DATE")) {
|
|
||||||
line = getAllDayLine(line);
|
|
||||||
} else if (fromServer && currentAllDayState.isCdoAllDay && line.startsWith("DTEND") && !line.startsWith("DTEND;VALUE=DATE")) {
|
|
||||||
line = getAllDayLine(line);
|
|
||||||
} else if (!fromServer && currentAllDayState.isAllDay && line.startsWith("DTSTART") && line.startsWith("DTSTART;VALUE=DATE")) {
|
|
||||||
line = "DTSTART;TZID=\"" + ExchangeSession.this.getVTimezone().timezoneId + "\":" + line.substring(19) + "T000000";
|
|
||||||
} else if (!fromServer && currentAllDayState.isAllDay && line.startsWith("DTEND") && line.startsWith("DTEND;VALUE=DATE")) {
|
|
||||||
line = "DTEND;TZID=\"" + ExchangeSession.this.getVTimezone().timezoneId + "\":" + line.substring(17) + "T000000";
|
|
||||||
} else if (line.startsWith("TZID:") && validTimezoneId != null) {
|
|
||||||
line = "TZID:" + validTimezoneId;
|
|
||||||
} else if ("BEGIN:VEVENT".equals(line)) {
|
|
||||||
currentAllDayState = allDayStates.get(count++);
|
|
||||||
// remove calendarserver access
|
|
||||||
} else if (line.startsWith("X-CALENDARSERVER-ACCESS:")) {
|
|
||||||
continue;
|
|
||||||
} else if (line.startsWith("EXDATE;TZID=") || line.startsWith("EXDATE:")) {
|
|
||||||
// Apple iCal doesn't support EXDATE with multiple exceptions
|
|
||||||
// on one line. Split into multiple EXDATE entries (which is
|
|
||||||
// also legal according to the caldav standard).
|
|
||||||
splitExDate(result, line);
|
|
||||||
continue;
|
|
||||||
} else if (line.startsWith("X-ENTOURAGE_UUID:")) {
|
|
||||||
// Apple iCal doesn't understand this key, and it's entourage
|
|
||||||
// specific (i.e. not needed by any caldav client): strip it out
|
|
||||||
continue;
|
|
||||||
} else if (fromServer && line.startsWith("ATTENDEE;")
|
|
||||||
&& (line.indexOf(email) >= 0)) {
|
|
||||||
// If this is coming from the server, strip out RSVP for this
|
|
||||||
// user as an attendee where the partstat is something other
|
|
||||||
// than PARTSTAT=NEEDS-ACTION since the RSVP confuses iCal4 into
|
|
||||||
// thinking the attendee has not replied
|
|
||||||
|
|
||||||
int rsvpSuffix = line.indexOf("RSVP=TRUE;");
|
|
||||||
int rsvpPrefix = line.indexOf(";RSVP=TRUE");
|
|
||||||
|
|
||||||
if (((rsvpSuffix >= 0) || (rsvpPrefix >= 0))
|
|
||||||
&& (line.indexOf("PARTSTAT=") >= 0)
|
|
||||||
&& (line.indexOf("PARTSTAT=NEEDS-ACTION") < 0)) {
|
|
||||||
|
|
||||||
// Strip out the "RSVP" line from the calendar entry
|
|
||||||
if (rsvpSuffix >= 0) {
|
|
||||||
line = line.substring(0, rsvpSuffix) + line.substring(rsvpSuffix + 10);
|
|
||||||
} else {
|
|
||||||
line = line.substring(0, rsvpPrefix) + line.substring(rsvpPrefix + 10);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
} else if (line.startsWith("ACTION:")) {
|
|
||||||
if (fromServer && "DISPLAY".equals(action)
|
|
||||||
// convert DISPLAY to AUDIO only if user defined an alarm sound
|
|
||||||
&& Settings.getProperty("davmail.caldavAlarmSound") != null) {
|
|
||||||
// Convert alarm to audio for iCal
|
|
||||||
result.writeLine("ACTION:AUDIO");
|
|
||||||
|
|
||||||
if (!sound) {
|
|
||||||
// Add defined sound into the audio alarm
|
|
||||||
result.writeLine("ATTACH;VALUE=URI:" + Settings.getProperty("davmail.caldavAlarmSound"));
|
|
||||||
}
|
|
||||||
|
|
||||||
continue;
|
|
||||||
} else if (!fromServer && "AUDIO".equals(action)) {
|
|
||||||
// Use the alarm action that exchange (and blackberry) understand
|
|
||||||
// (exchange and blackberry don't understand audio actions)
|
|
||||||
|
|
||||||
result.writeLine("ACTION:DISPLAY");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Don't recognize this type of action: pass it through
|
|
||||||
|
|
||||||
} else if (line.startsWith("CLASS:")) {
|
|
||||||
if (!fromServer && isAppleiCal) {
|
|
||||||
continue;
|
|
||||||
} else {
|
|
||||||
// still set calendarserver access inside event for iCal 3
|
|
||||||
if ("PRIVATE".equalsIgnoreCase(eventClass)) {
|
|
||||||
result.writeLine("X-CALENDARSERVER-ACCESS:CONFIDENTIAL");
|
|
||||||
} else if ("CONFIDENTIAL".equalsIgnoreCase(eventClass)) {
|
|
||||||
result.writeLine("X-CALENDARSERVER-ACCESS:PRIVATE");
|
|
||||||
} else {
|
|
||||||
result.writeLine("X-CALENDARSERVER-ACCESS:" + eventClass);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// remove organizer line if user is organizer for iPhone
|
|
||||||
} else if (fromServer && line.startsWith("ORGANIZER") && !hasAttendee) {
|
|
||||||
continue;
|
|
||||||
} else if (organizer != null && line.startsWith("ATTENDEE") && line.contains(organizer)) {
|
|
||||||
// Ignore organizer as attendee
|
|
||||||
continue;
|
|
||||||
} else if (!fromServer && line.startsWith("ATTENDEE")) {
|
|
||||||
line = replaceIcal4Principal(line);
|
|
||||||
}
|
|
||||||
|
|
||||||
result.writeLine(line);
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
reader.close();
|
|
||||||
}
|
|
||||||
String resultString = result.toString();
|
|
||||||
|
|
||||||
/* new experimental code
|
|
||||||
VCalendar vCalendar = new VCalendar(icsBody, getEmail());
|
|
||||||
vCalendar.fixVCalendar(fromServer);
|
vCalendar.fixVCalendar(fromServer);
|
||||||
resultString = vCalendar.toString();
|
String resultString = vCalendar.toString();
|
||||||
*/
|
if (LOGGER.isDebugEnabled() && !fromServer) {
|
||||||
|
LOGGER.debug("Fixed Vcalendar body to server:\n" +resultString);
|
||||||
|
}
|
||||||
dumpICS(resultString, fromServer, true);
|
dumpICS(resultString, fromServer, true);
|
||||||
|
|
||||||
return resultString;
|
return resultString;
|
||||||
@ -3481,14 +3235,14 @@ public abstract class ExchangeSession {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected VTimezone vTimezone;
|
protected VObject vTimezone;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Load and return current user OWA timezone.
|
* Load and return current user OWA timezone.
|
||||||
*
|
*
|
||||||
* @return current timezone
|
* @return current timezone
|
||||||
*/
|
*/
|
||||||
public VTimezone getVTimezone() {
|
public VObject getVTimezone() {
|
||||||
if (vTimezone == null) {
|
if (vTimezone == null) {
|
||||||
// need to load Timezone info from OWA
|
// need to load Timezone info from OWA
|
||||||
loadVtimezone();
|
loadVtimezone();
|
||||||
|
@ -19,6 +19,7 @@
|
|||||||
package davmail.exchange;
|
package davmail.exchange;
|
||||||
|
|
||||||
import davmail.Settings;
|
import davmail.Settings;
|
||||||
|
import org.apache.log4j.Logger;
|
||||||
|
|
||||||
import java.io.BufferedReader;
|
import java.io.BufferedReader;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
@ -28,6 +29,7 @@ import java.io.StringReader;
|
|||||||
* VCalendar object.
|
* VCalendar object.
|
||||||
*/
|
*/
|
||||||
public class VCalendar extends VObject {
|
public class VCalendar extends VObject {
|
||||||
|
protected static final Logger LOGGER = Logger.getLogger(VCalendar.class);
|
||||||
protected VObject firstVevent;
|
protected VObject firstVevent;
|
||||||
protected VObject vTimezone;
|
protected VObject vTimezone;
|
||||||
protected String email;
|
protected String email;
|
||||||
@ -37,25 +39,32 @@ public class VCalendar extends VObject {
|
|||||||
*
|
*
|
||||||
* @param reader stream reader
|
* @param reader stream reader
|
||||||
* @param email current user email
|
* @param email current user email
|
||||||
|
* @param vTimezone user OWA timezone
|
||||||
* @throws IOException on error
|
* @throws IOException on error
|
||||||
*/
|
*/
|
||||||
public VCalendar(BufferedReader reader, String email) throws IOException {
|
public VCalendar(BufferedReader reader, String email, VObject vTimezone) throws IOException {
|
||||||
super(reader);
|
super(reader);
|
||||||
if (!"VCALENDAR".equals(type)) {
|
if (!"VCALENDAR".equals(type)) {
|
||||||
throw new IOException("Invalid type: " + type);
|
throw new IOException("Invalid type: " + type);
|
||||||
}
|
}
|
||||||
this.email = email;
|
this.email = email;
|
||||||
|
// set OWA timezone information
|
||||||
|
if (this.vTimezone == null) {
|
||||||
|
this.vObjects.add(0, vTimezone);
|
||||||
|
this.vTimezone = vTimezone;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create VCalendar object from reader;
|
* Create VCalendar object from string;
|
||||||
*
|
*
|
||||||
* @param vCalendarBody item body
|
* @param vCalendarBody item body
|
||||||
* @param email current user email
|
* @param email current user email
|
||||||
|
* @param vTimezone user OWA timezone
|
||||||
* @throws IOException on error
|
* @throws IOException on error
|
||||||
*/
|
*/
|
||||||
public VCalendar(String vCalendarBody, String email) throws IOException {
|
public VCalendar(String vCalendarBody, String email, VObject vTimezone) throws IOException {
|
||||||
this(new ICSBufferedReader(new StringReader(vCalendarBody)), email);
|
this(new ICSBufferedReader(new StringReader(vCalendarBody)), email, vTimezone);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -124,7 +133,24 @@ public class VCalendar extends VObject {
|
|||||||
} else if (vObject.getPropertyValue("X-CALENDARSERVER-ACCESS") != null) {
|
} else if (vObject.getPropertyValue("X-CALENDARSERVER-ACCESS") != null) {
|
||||||
vObject.setPropertyValue("CLASS", getEventClass(vObject.getPropertyValue("X-CALENDARSERVER-ACCESS")));
|
vObject.setPropertyValue("CLASS", getEventClass(vObject.getPropertyValue("X-CALENDARSERVER-ACCESS")));
|
||||||
}
|
}
|
||||||
if (!fromServer) {
|
if (fromServer) {
|
||||||
|
// remove organizer line for event without attendees for iPhone
|
||||||
|
if (getProperty("ATTENDEE") == null) {
|
||||||
|
vObject.setPropertyValue("ORGANIZER", null);
|
||||||
|
}
|
||||||
|
// detect allday and update date properties
|
||||||
|
if (isCdoAllDay(vObject)) {
|
||||||
|
setClientAllday(vObject.getProperty("DTSTART"));
|
||||||
|
setClientAllday(vObject.getProperty("DTEND"));
|
||||||
|
}
|
||||||
|
vObject.setPropertyValue("TRANSP",
|
||||||
|
!"FREE".equals(vObject.getPropertyValue("X-MICROSOFT-CDO-BUSYSTATUS")) ? "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");
|
||||||
|
} else {
|
||||||
// add organizer line to all events created in Exchange for active sync
|
// add organizer line to all events created in Exchange for active sync
|
||||||
if (vObject.getPropertyValue("ORGANIZER") == null) {
|
if (vObject.getPropertyValue("ORGANIZER") == null) {
|
||||||
vObject.setPropertyValue("ORGANIZER", "MAILTO:" + email);
|
vObject.setPropertyValue("ORGANIZER", "MAILTO:" + email);
|
||||||
@ -134,12 +160,11 @@ public class VCalendar extends VObject {
|
|||||||
vObject.setPropertyValue("X-MICROSOFT-CDO-BUSYSTATUS",
|
vObject.setPropertyValue("X-MICROSOFT-CDO-BUSYSTATUS",
|
||||||
!"TRANSPARENT".equals(vObject.getPropertyValue("TRANSP")) ? "BUSY" : "FREE");
|
!"TRANSPARENT".equals(vObject.getPropertyValue("TRANSP")) ? "BUSY" : "FREE");
|
||||||
|
|
||||||
} else {
|
if (isAllDay(vObject)) {
|
||||||
// remove organizer line for event without attendees for iPhone
|
// convert date values to outlook compatible values
|
||||||
if (getProperty("ATTENDEE") == null) {
|
setServerAllday(vObject.getProperty("DTSTART"));
|
||||||
vObject.setPropertyValue("ORGANIZER", null);
|
setServerAllday(vObject.getProperty("DTEND"));
|
||||||
}
|
}
|
||||||
// TODO: handle transparent ?
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fixAttendees(vObject, fromServer);
|
fixAttendees(vObject, fromServer);
|
||||||
@ -152,7 +177,39 @@ public class VCalendar extends VObject {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void fixAlarm(VObject vObject, boolean fromServer) {
|
private void setServerAllday(VProperty property) {
|
||||||
|
// set TZID param
|
||||||
|
if (!property.hasParam("TZID")) {
|
||||||
|
property.addParam("TZID", vTimezone.getPropertyValue("TZID"));
|
||||||
|
}
|
||||||
|
// remove VALUE
|
||||||
|
property.removeParam("VALUE");
|
||||||
|
String value = property.getValue();
|
||||||
|
if (value.length() != 8) {
|
||||||
|
LOGGER.warn("Invalid date value in allday event: " + value);
|
||||||
|
}
|
||||||
|
property.setValue(property.getValue() + "T000000");
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void setClientAllday(VProperty property) {
|
||||||
|
// set VALUE=DATE param
|
||||||
|
if (!property.hasParam("VALUE")) {
|
||||||
|
property.addParam("VALUE", "DATE");
|
||||||
|
}
|
||||||
|
// remove TZID
|
||||||
|
property.removeParam("TZID");
|
||||||
|
String value = property.getValue();
|
||||||
|
int tIndex = value.indexOf('T');
|
||||||
|
if (tIndex >= 0) {
|
||||||
|
value = value.substring(0, tIndex);
|
||||||
|
} else {
|
||||||
|
LOGGER.warn("Invalid date value in allday event: " + value);
|
||||||
|
}
|
||||||
|
property.setValue(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void fixAlarm(VObject vObject, boolean fromServer) {
|
||||||
|
if (vObject.vObjects != null) {
|
||||||
for (VObject vAlarm : vObject.vObjects) {
|
for (VObject vAlarm : vObject.vObjects) {
|
||||||
if ("VALARM".equals(vAlarm.type)) {
|
if ("VALARM".equals(vAlarm.type)) {
|
||||||
String action = vAlarm.getPropertyValue("ACTION");
|
String action = vAlarm.getPropertyValue("ACTION");
|
||||||
@ -177,6 +234,7 @@ public class VCalendar extends VObject {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Replace iCal4 (Snow Leopard) principal paths with mailto expression
|
* Replace iCal4 (Snow Leopard) principal paths with mailto expression
|
||||||
@ -193,10 +251,21 @@ public class VCalendar extends VObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void fixAttendees(VObject vObject, boolean fromServer) {
|
private void fixAttendees(VObject vObject, boolean fromServer) {
|
||||||
if (!fromServer) {
|
|
||||||
if (vObject.properties != null) {
|
if (vObject.properties != null) {
|
||||||
for (VProperty property : vObject.properties) {
|
for (VProperty property : vObject.properties) {
|
||||||
if ("ATTENDEE".equalsIgnoreCase(property.getKey())) {
|
if ("ATTENDEE".equalsIgnoreCase(property.getKey())) {
|
||||||
|
if (fromServer) {
|
||||||
|
// If this is coming from the server, strip out RSVP for this
|
||||||
|
// user as an attendee where the partstat is something other
|
||||||
|
// than PARTSTAT=NEEDS-ACTION since the RSVP confuses iCal4 into
|
||||||
|
// thinking the attendee has not replied
|
||||||
|
if (isCurrentUser(property) && property.hasParam("RSVP", "TRUE")) {
|
||||||
|
VProperty.Param partstat = property.getParam("PARTSTAT");
|
||||||
|
if (partstat == null || !"NEEDS-ACTION".equals(partstat.getValue())) {
|
||||||
|
property.removeParam("RSVP");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
property.setValue(replaceIcal4Principal(property.getValue()));
|
property.setValue(replaceIcal4Principal(property.getValue()));
|
||||||
|
|
||||||
// ignore attendee as organizer
|
// ignore attendee as organizer
|
||||||
@ -207,14 +276,17 @@ public class VCalendar extends VObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
// TODO patch RSVP
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean isCurrentUser(VProperty property) {
|
||||||
|
return property.getValue().equalsIgnoreCase("mailto:" + email);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convert X-CALENDARSERVER-ACCESS to CLASS.
|
* Convert X-CALENDARSERVER-ACCESS to CLASS.
|
||||||
|
* see http://svn.calendarserver.org/repository/calendarserver/CalendarServer/trunk/doc/Extensions/caldav-privateevents.txt
|
||||||
*
|
*
|
||||||
* @return CLASS value
|
* @return CLASS value
|
||||||
*/
|
*/
|
||||||
@ -230,7 +302,7 @@ public class VCalendar extends VObject {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Convert CLASS to X-CALENDARSERVER-ACCESS.
|
* 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
|
* @return X-CALENDARSERVER-ACCESS value
|
||||||
*/
|
*/
|
||||||
protected String getCalendarServerAccess() {
|
protected String getCalendarServerAccess() {
|
||||||
|
@ -20,6 +20,7 @@ package davmail.exchange;
|
|||||||
|
|
||||||
import java.io.BufferedReader;
|
import java.io.BufferedReader;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.io.StringReader;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@ -73,6 +74,17 @@ public class VObject {
|
|||||||
this(new VProperty(reader.readLine()), reader);
|
this(new VProperty(reader.readLine()), reader);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create VCalendar object from string;
|
||||||
|
*
|
||||||
|
* @param itemBody item body
|
||||||
|
* @throws IOException on error
|
||||||
|
*/
|
||||||
|
public VObject(String itemBody) throws IOException {
|
||||||
|
this(new ICSBufferedReader(new StringReader(itemBody)));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
protected void handleLine(String line, BufferedReader reader) throws IOException {
|
protected void handleLine(String line, BufferedReader reader) throws IOException {
|
||||||
VProperty property = new VProperty(line);
|
VProperty property = new VProperty(line);
|
||||||
// inner object
|
// inner object
|
||||||
@ -158,6 +170,9 @@ public class VObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void setPropertyValue(String name, String value) {
|
public void setPropertyValue(String name, String value) {
|
||||||
|
if (value == null) {
|
||||||
|
removeProperty(name);
|
||||||
|
} else {
|
||||||
VProperty property = getProperty(name);
|
VProperty property = getProperty(name);
|
||||||
if (property == null) {
|
if (property == null) {
|
||||||
property = new VProperty(name, value);
|
property = new VProperty(name, value);
|
||||||
@ -165,6 +180,15 @@ public class VObject {
|
|||||||
} else {
|
} else {
|
||||||
property.setValue(value);
|
property.setValue(value);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void removeProperty(String name) {
|
||||||
|
if (vObjects != null) {
|
||||||
|
VProperty property = getProperty(name);
|
||||||
|
if (property != null) {
|
||||||
|
vObjects.remove(property);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -39,6 +39,14 @@ public class VProperty {
|
|||||||
}
|
}
|
||||||
values.addAll(paramValues);
|
values.addAll(paramValues);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getValue() {
|
||||||
|
if (values != null && !values.isEmpty()) {
|
||||||
|
return values.get(0);
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected String key;
|
protected String key;
|
||||||
@ -189,6 +197,15 @@ public class VProperty {
|
|||||||
return params != null && getParam(paramName) != null;
|
return params != null && getParam(paramName) != null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void removeParam(String paramName) {
|
||||||
|
if (params != null) {
|
||||||
|
Param param = getParam(paramName);
|
||||||
|
if (param != null) {
|
||||||
|
params.remove(param);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
protected boolean containsIgnoreCase(List<String> stringCollection, String value) {
|
protected boolean containsIgnoreCase(List<String> stringCollection, String value) {
|
||||||
for (String collectionValue : stringCollection) {
|
for (String collectionValue : stringCollection) {
|
||||||
if (value.equalsIgnoreCase(collectionValue)) {
|
if (value.equalsIgnoreCase(collectionValue)) {
|
||||||
@ -199,7 +216,7 @@ public class VProperty {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected void addParam(String paramName) {
|
protected void addParam(String paramName) {
|
||||||
addParam(paramName, (String)null);
|
addParam(paramName, (String) null);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void addParam(String paramName, String paramValue) {
|
protected void addParam(String paramName, String paramValue) {
|
||||||
@ -330,7 +347,7 @@ public class VProperty {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected void appendParamValue(StringBuilder buffer, String value) {
|
protected void appendParamValue(StringBuilder buffer, String value) {
|
||||||
if (Math.max(value.indexOf(';'), value.indexOf(',')) >= 0) {
|
if (value.indexOf(';') >= 0 || value.indexOf(',') >= 0 || value.indexOf('(') >= 0 || value.indexOf('/') >= 0) {
|
||||||
buffer.append('"').append(value).append('"');
|
buffer.append('"').append(value).append('"');
|
||||||
} else {
|
} else {
|
||||||
buffer.append(value);
|
buffer.append(value);
|
||||||
|
@ -24,6 +24,7 @@ import davmail.exception.DavMailAuthenticationException;
|
|||||||
import davmail.exception.DavMailException;
|
import davmail.exception.DavMailException;
|
||||||
import davmail.exception.HttpNotFoundException;
|
import davmail.exception.HttpNotFoundException;
|
||||||
import davmail.exchange.ExchangeSession;
|
import davmail.exchange.ExchangeSession;
|
||||||
|
import davmail.exchange.VObject;
|
||||||
import davmail.http.DavGatewayHttpClientFacade;
|
import davmail.http.DavGatewayHttpClientFacade;
|
||||||
import davmail.ui.tray.DavGatewayTray;
|
import davmail.ui.tray.DavGatewayTray;
|
||||||
import davmail.util.IOUtil;
|
import davmail.util.IOUtil;
|
||||||
@ -1377,7 +1378,6 @@ public class DavExchangeSession extends ExchangeSession {
|
|||||||
@Override
|
@Override
|
||||||
protected void loadVtimezone() {
|
protected void loadVtimezone() {
|
||||||
try {
|
try {
|
||||||
VTimezone userTimezone = new VTimezone();
|
|
||||||
// create temporary folder
|
// create temporary folder
|
||||||
String folderPath = getFolderPath("davmailtemp");
|
String folderPath = getFolderPath("davmailtemp");
|
||||||
createCalendarFolder(folderPath, null);
|
createCalendarFolder(folderPath, null);
|
||||||
@ -1403,14 +1403,14 @@ public class DavExchangeSession extends ExchangeSession {
|
|||||||
propertyList.add(Field.createDavProperty("instancetype", "0"));
|
propertyList.add(Field.createDavProperty("instancetype", "0"));
|
||||||
|
|
||||||
// get forced timezone id from settings
|
// get forced timezone id from settings
|
||||||
userTimezone.timezoneId = Settings.getProperty("davmail.timezoneId");
|
String timezoneId = Settings.getProperty("davmail.timezoneId");
|
||||||
if (userTimezone.timezoneId == null) {
|
if (timezoneId == null) {
|
||||||
// get timezoneid from OWA settings
|
// get timezoneid from OWA settings
|
||||||
userTimezone.timezoneId = getTimezoneIdFromExchange();
|
timezoneId = getTimezoneIdFromExchange();
|
||||||
}
|
}
|
||||||
// without a timezoneId, use Exchange timezone
|
// without a timezoneId, use Exchange timezone
|
||||||
if (userTimezone.timezoneId != null) {
|
if (timezoneId != null) {
|
||||||
propertyList.add(Field.createDavProperty("timezoneid", userTimezone.timezoneId));
|
propertyList.add(Field.createDavProperty("timezoneid", timezoneId));
|
||||||
}
|
}
|
||||||
String patchMethodUrl = URIUtil.encodePath(folderPath) + '/' + UUID.randomUUID().toString() + ".EML";
|
String patchMethodUrl = URIUtil.encodePath(folderPath) + '/' + UUID.randomUUID().toString() + ".EML";
|
||||||
PropPatchMethod patchMethod = new PropPatchMethod(URIUtil.encodePath(patchMethodUrl), propertyList);
|
PropPatchMethod patchMethod = new PropPatchMethod(URIUtil.encodePath(patchMethodUrl), propertyList);
|
||||||
@ -1429,10 +1429,9 @@ public class DavExchangeSession extends ExchangeSession {
|
|||||||
getMethod.setRequestHeader("Translate", "f");
|
getMethod.setRequestHeader("Translate", "f");
|
||||||
try {
|
try {
|
||||||
httpClient.executeMethod(getMethod);
|
httpClient.executeMethod(getMethod);
|
||||||
userTimezone.timezoneBody = "BEGIN:VTIMEZONE" +
|
this.vTimezone = new VObject("BEGIN:VTIMEZONE" +
|
||||||
StringUtil.getToken(getMethod.getResponseBodyAsString(), "BEGIN:VTIMEZONE", "END:VTIMEZONE") +
|
StringUtil.getToken(getMethod.getResponseBodyAsString(), "BEGIN:VTIMEZONE", "END:VTIMEZONE") +
|
||||||
"END:VTIMEZONE\r\n";
|
"END:VTIMEZONE\r\n");
|
||||||
userTimezone.timezoneId = StringUtil.getToken(userTimezone.timezoneBody, "TZID:", "\r\n");
|
|
||||||
} finally {
|
} finally {
|
||||||
getMethod.releaseConnection();
|
getMethod.releaseConnection();
|
||||||
}
|
}
|
||||||
@ -1440,7 +1439,6 @@ public class DavExchangeSession extends ExchangeSession {
|
|||||||
|
|
||||||
// delete temporary folder
|
// delete temporary folder
|
||||||
deleteFolder("davmailtemp");
|
deleteFolder("davmailtemp");
|
||||||
this.vTimezone = userTimezone;
|
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
LOGGER.warn("Unable to get VTIMEZONE info: " + e, e);
|
LOGGER.warn("Unable to get VTIMEZONE info: " + e, e);
|
||||||
}
|
}
|
||||||
|
@ -33,9 +33,9 @@ import java.util.*;
|
|||||||
public class TestExchangeSessionCalendar extends AbstractExchangeSessionTestCase {
|
public class TestExchangeSessionCalendar extends AbstractExchangeSessionTestCase {
|
||||||
|
|
||||||
public void testGetVtimezone() {
|
public void testGetVtimezone() {
|
||||||
ExchangeSession.VTimezone timezone = session.getVTimezone();
|
VObject timezone = session.getVTimezone();
|
||||||
assertNotNull(timezone.timezoneId);
|
assertNotNull(timezone);
|
||||||
assertNotNull(timezone.timezoneBody);
|
assertNotNull(timezone.getPropertyValue("TZID"));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void testDumpVtimezones() throws IOException {
|
public void testDumpVtimezones() throws IOException {
|
||||||
@ -63,10 +63,10 @@ public class TestExchangeSessionCalendar extends AbstractExchangeSessionTestCase
|
|||||||
};
|
};
|
||||||
for (int i = 1; i < 100; i++) {
|
for (int i = 1; i < 100; i++) {
|
||||||
Settings.setProperty("davmail.timezoneId", String.valueOf(i));
|
Settings.setProperty("davmail.timezoneId", String.valueOf(i));
|
||||||
ExchangeSession.VTimezone timezone = session.getVTimezone();
|
VObject timezone = session.getVTimezone();
|
||||||
if (timezone.timezoneId != null) {
|
if (timezone.getProperty("TZID") != null) {
|
||||||
properties.put(timezone.timezoneId.replaceAll("\\\\", ""), String.valueOf(i));
|
properties.put(timezone.getPropertyValue("TZID").replaceAll("\\\\", ""), String.valueOf(i));
|
||||||
System.out.println(timezone.timezoneId + '=' + i);
|
System.out.println(timezone.getPropertyValue("TZID") + '=' + i);
|
||||||
}
|
}
|
||||||
session.vTimezone = null;
|
session.vTimezone = null;
|
||||||
}
|
}
|
||||||
|
@ -18,344 +18,45 @@
|
|||||||
*/
|
*/
|
||||||
package davmail.exchange;
|
package davmail.exchange;
|
||||||
|
|
||||||
import davmail.Settings;
|
|
||||||
import davmail.exception.DavMailException;
|
|
||||||
import davmail.util.StringUtil;
|
|
||||||
import junit.framework.TestCase;
|
import junit.framework.TestCase;
|
||||||
|
|
||||||
import java.io.BufferedReader;
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.StringReader;
|
import java.io.StringReader;
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.MissingResourceException;
|
|
||||||
import java.util.ResourceBundle;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Test ExchangeSession event conversion.
|
* Test ExchangeSession event conversion.
|
||||||
*/
|
*/
|
||||||
|
@SuppressWarnings({"UseOfSystemOutOrSystemErr"})
|
||||||
public class TestExchangeSessionEvent extends TestCase {
|
public class TestExchangeSessionEvent extends TestCase {
|
||||||
String email = "user@company.com";
|
static String email = "user@company.com";
|
||||||
|
static VObject vTimeZone;
|
||||||
|
|
||||||
/*
|
static {
|
||||||
X-CALENDARSERVER-ACCESS conversion
|
|
||||||
see http://svn.calendarserver.org/repository/calendarserver/CalendarServer/trunk/doc/Extensions/caldav-privateevents.txt
|
|
||||||
|
|
||||||
X-CALENDARSERVER-ACCESS -> CLASS
|
|
||||||
NONE -> NONE
|
|
||||||
PUBLIC -> PUBLIC
|
|
||||||
PRIVATE -> CONFIDENTIAL
|
|
||||||
CONFIDENTIAL -> PRIVATE
|
|
||||||
RESTRICTED -> PRIVATE
|
|
||||||
|
|
||||||
CLASS -> X-CALENDARSERVER-ACCESS
|
|
||||||
NONE -> NONE
|
|
||||||
PUBLIC -> PUBLIC
|
|
||||||
PRIVATE -> CONFIDENTIAL
|
|
||||||
CONFIDENTIAL -> PRIVATE
|
|
||||||
|
|
||||||
iCal 3 sends X-CALENDARSERVER-ACCESS inside VEVENT, iCal 4 uses global X-CALENDARSERVER-ACCESS
|
|
||||||
*/
|
|
||||||
|
|
||||||
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 fixTimezoneId(String line, String validTimezoneId) {
|
|
||||||
return StringUtil.replaceToken(line, "TZID=", ":", validTimezoneId);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected String replaceIcal4Principal(String value) {
|
|
||||||
if (value.contains("/principals/__uuids__/")) {
|
|
||||||
return value.replaceAll("/principals/__uuids__/([^/]*)__AT__([^/]*)/", "mailto:$1@$2");
|
|
||||||
} else {
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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 oldfixICS(String icsBody, boolean fromServer) throws IOException {
|
|
||||||
// first pass : detect
|
|
||||||
class AllDayState {
|
|
||||||
boolean isAllDay;
|
|
||||||
boolean hasCdoAllDay;
|
|
||||||
boolean isCdoAllDay;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert event class from and to iCal
|
|
||||||
// See https://trac.calendarserver.org/browser/CalendarServer/trunk/doc/Extensions/caldav-privateevents.txt
|
|
||||||
boolean isAppleiCal = false;
|
|
||||||
boolean hasAttendee = false;
|
|
||||||
boolean hasCdoBusyStatus = false;
|
|
||||||
// detect ics event with empty timezone (all day from Lightning)
|
|
||||||
boolean hasTimezone = false;
|
|
||||||
String transp = null;
|
|
||||||
String validTimezoneId = null;
|
|
||||||
String eventClass = null;
|
|
||||||
String organizer = null;
|
|
||||||
String action = null;
|
|
||||||
String method = null;
|
|
||||||
boolean sound = false;
|
|
||||||
|
|
||||||
List<AllDayState> allDayStates = new ArrayList<AllDayState>();
|
|
||||||
AllDayState currentAllDayState = new AllDayState();
|
|
||||||
BufferedReader reader = null;
|
|
||||||
try {
|
try {
|
||||||
reader = new ICSBufferedReader(new StringReader(icsBody));
|
vTimeZone = new VObject(new ICSBufferedReader(new StringReader("BEGIN:VTIMEZONE\n" +
|
||||||
String line;
|
"TZID:(GMT+01.00) Paris/Madrid/Brussels/Copenhagen\n" +
|
||||||
while ((line = reader.readLine()) != null) {
|
"X-MICROSOFT-CDO-TZID:3\n" +
|
||||||
int index = line.indexOf(':');
|
"BEGIN:STANDARD\n" +
|
||||||
if (index >= 0) {
|
"DTSTART:16010101T030000\n" +
|
||||||
String key = line.substring(0, index);
|
"TZOFFSETFROM:+0200\n" +
|
||||||
String value = line.substring(index + 1);
|
"TZOFFSETTO:+0100\n" +
|
||||||
if ("DTSTART;VALUE=DATE".equals(key)) {
|
"RRULE:FREQ=YEARLY;WKST=MO;INTERVAL=1;BYMONTH=10;BYDAY=-1SU\n" +
|
||||||
currentAllDayState.isAllDay = true;
|
"END:STANDARD\n" +
|
||||||
} else if ("X-MICROSOFT-CDO-ALLDAYEVENT".equals(key)) {
|
"BEGIN:DAYLIGHT\n" +
|
||||||
currentAllDayState.hasCdoAllDay = true;
|
"DTSTART:16010101T020000\n" +
|
||||||
currentAllDayState.isCdoAllDay = "TRUE".equals(value);
|
"TZOFFSETFROM:+0100\n" +
|
||||||
} else if ("END:VEVENT".equals(line)) {
|
"TZOFFSETTO:+0200\n" +
|
||||||
allDayStates.add(currentAllDayState);
|
"RRULE:FREQ=YEARLY;WKST=MO;INTERVAL=1;BYMONTH=3;BYDAY=-1SU\n" +
|
||||||
currentAllDayState = new AllDayState();
|
"END:DAYLIGHT\n" +
|
||||||
} else if ("PRODID".equals(key) && line.contains("iCal")) {
|
"END:VTIMEZONE")));
|
||||||
// detect iCal created events
|
} catch (IOException e) {
|
||||||
isAppleiCal = true;
|
e.printStackTrace();
|
||||||
} else if (isAppleiCal && "X-CALENDARSERVER-ACCESS".equals(key)) {
|
|
||||||
eventClass = value;
|
|
||||||
} else if (!isAppleiCal && "CLASS".equals(key)) {
|
|
||||||
eventClass = value;
|
|
||||||
} else if ("ACTION".equals(key)) {
|
|
||||||
action = value;
|
|
||||||
} else if ("ATTACH;VALUES=URI".equals(key)) {
|
|
||||||
// This is a marker that this event has an alarm with sound
|
|
||||||
sound = true;
|
|
||||||
} else if (key.startsWith("ORGANIZER")) {
|
|
||||||
if (value.startsWith("MAILTO:")) {
|
|
||||||
organizer = value.substring(7);
|
|
||||||
} else {
|
|
||||||
organizer = value;
|
|
||||||
}
|
}
|
||||||
} else if (key.startsWith("ATTENDEE")) {
|
|
||||||
hasAttendee = true;
|
|
||||||
} else if ("TRANSP".equals(key)) {
|
|
||||||
transp = value;
|
|
||||||
} else if (line.startsWith("TZID:(GMT") ||
|
|
||||||
// additional test for Outlook created recurring events
|
|
||||||
line.startsWith("TZID:GMT ")) {
|
|
||||||
try {
|
|
||||||
validTimezoneId = ResourceBundle.getBundle("timezones").getString(value);
|
|
||||||
} catch (MissingResourceException mre) {
|
|
||||||
//LOGGER.warn(new BundleMessage("LOG_INVALID_TIMEZONE", value));
|
|
||||||
}
|
|
||||||
} else if ("X-MICROSOFT-CDO-BUSYSTATUS".equals(key)) {
|
|
||||||
hasCdoBusyStatus = true;
|
|
||||||
} else if ("BEGIN:VTIMEZONE".equals(line)) {
|
|
||||||
hasTimezone = true;
|
|
||||||
} else if ("METHOD".equals(key)) {
|
|
||||||
method = value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
if (reader != null) {
|
|
||||||
reader.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// second pass : fix
|
|
||||||
int count = 0;
|
|
||||||
ICSBufferedWriter result = new ICSBufferedWriter();
|
|
||||||
try {
|
|
||||||
reader = new ICSBufferedReader(new StringReader(icsBody));
|
|
||||||
String line;
|
|
||||||
|
|
||||||
while ((line = reader.readLine()) != null) {
|
|
||||||
// remove empty properties
|
|
||||||
if ("CLASS:".equals(line) || "LOCATION:".equals(line)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
// fix invalid exchange timezoneid
|
|
||||||
if (validTimezoneId != null && line.indexOf(";TZID=") >= 0) {
|
|
||||||
line = fixTimezoneId(line, validTimezoneId);
|
|
||||||
}
|
|
||||||
if (!fromServer && "BEGIN:VCALENDAR".equals(line) && method == null) {
|
|
||||||
result.writeLine(line);
|
|
||||||
// append missing method
|
|
||||||
if (method == null) {
|
|
||||||
result.writeLine("METHOD:PUBLISH");
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (fromServer && line.startsWith("PRODID:") && eventClass != null) {
|
|
||||||
result.writeLine(line);
|
|
||||||
// set global calendarserver access for iCal 4
|
|
||||||
if ("PRIVATE".equalsIgnoreCase(eventClass)) {
|
|
||||||
result.writeLine("X-CALENDARSERVER-ACCESS:CONFIDENTIAL");
|
|
||||||
} else if ("CONFIDENTIAL".equalsIgnoreCase(eventClass)) {
|
|
||||||
result.writeLine("X-CALENDARSERVER-ACCESS:PRIVATE");
|
|
||||||
} else if (eventClass != null) {
|
|
||||||
result.writeLine("X-CALENDARSERVER-ACCESS:" + eventClass);
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (!fromServer && "BEGIN:VEVENT".equals(line) && !hasTimezone) {
|
|
||||||
result.write("BEGIN:VTIMEZONE\nFAKE:FAKE\nEND:VTIMEZONE\n");
|
|
||||||
hasTimezone = true;
|
|
||||||
}
|
|
||||||
if (!fromServer && currentAllDayState.isAllDay && "X-MICROSOFT-CDO-ALLDAYEVENT:FALSE".equals(line)) {
|
|
||||||
line = "X-MICROSOFT-CDO-ALLDAYEVENT:TRUE";
|
|
||||||
} else if (!fromServer && "END:VEVENT".equals(line)) {
|
|
||||||
if (!hasCdoBusyStatus) {
|
|
||||||
result.writeLine("X-MICROSOFT-CDO-BUSYSTATUS:" + (!"TRANSPARENT".equals(transp) ? "BUSY" : "FREE"));
|
|
||||||
}
|
|
||||||
if (currentAllDayState.isAllDay && !currentAllDayState.hasCdoAllDay) {
|
|
||||||
result.writeLine("X-MICROSOFT-CDO-ALLDAYEVENT:TRUE");
|
|
||||||
}
|
|
||||||
// add organizer line to all events created in Exchange for active sync
|
|
||||||
if (organizer == null) {
|
|
||||||
result.writeLine("ORGANIZER:MAILTO:" + email);
|
|
||||||
}
|
|
||||||
if (isAppleiCal) {
|
|
||||||
if ("CONFIDENTIAL".equalsIgnoreCase(eventClass)) {
|
|
||||||
result.writeLine("CLASS:PRIVATE");
|
|
||||||
} else if ("PRIVATE".equalsIgnoreCase(eventClass)) {
|
|
||||||
result.writeLine("CLASS:CONFIDENTIAL");
|
|
||||||
} else if (eventClass != null) {
|
|
||||||
result.writeLine("CLASS:" + eventClass);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (!fromServer && line.startsWith("X-MICROSOFT-CDO-BUSYSTATUS:")) {
|
|
||||||
line = "X-MICROSOFT-CDO-BUSYSTATUS:" + (!"TRANSPARENT".equals(transp) ? "BUSY" : "FREE");
|
|
||||||
} else if (!fromServer && !currentAllDayState.isAllDay && "X-MICROSOFT-CDO-ALLDAYEVENT:TRUE".equals(line)) {
|
|
||||||
line = "X-MICROSOFT-CDO-ALLDAYEVENT:FALSE";
|
|
||||||
} else if (fromServer && currentAllDayState.isCdoAllDay && line.startsWith("DTSTART") && !line.startsWith("DTSTART;VALUE=DATE")) {
|
|
||||||
line = getAllDayLine(line);
|
|
||||||
} else if (fromServer && currentAllDayState.isCdoAllDay && line.startsWith("DTEND") && !line.startsWith("DTEND;VALUE=DATE")) {
|
|
||||||
line = getAllDayLine(line);
|
|
||||||
} else if (!fromServer && currentAllDayState.isAllDay && line.startsWith("DTSTART") && line.startsWith("DTSTART;VALUE=DATE")) {
|
|
||||||
line = "DTSTART;TZID=\"" + "OWATZID" + "\":" + line.substring(19) + "T000000";
|
|
||||||
} else if (!fromServer && currentAllDayState.isAllDay && line.startsWith("DTEND") && line.startsWith("DTEND;VALUE=DATE")) {
|
|
||||||
line = "DTEND;TZID=\"" + "OWATZID" + "\":" + line.substring(17) + "T000000";
|
|
||||||
} else if (line.startsWith("TZID:") && validTimezoneId != null) {
|
|
||||||
line = "TZID:" + validTimezoneId;
|
|
||||||
} else if ("BEGIN:VEVENT".equals(line)) {
|
|
||||||
currentAllDayState = allDayStates.get(count++);
|
|
||||||
// remove calendarserver access
|
|
||||||
} else if (line.startsWith("X-CALENDARSERVER-ACCESS:")) {
|
|
||||||
continue;
|
|
||||||
} else if (line.startsWith("EXDATE;TZID=") || line.startsWith("EXDATE:")) {
|
|
||||||
// Apple iCal doesn't support EXDATE with multiple exceptions
|
|
||||||
// on one line. Split into multiple EXDATE entries (which is
|
|
||||||
// also legal according to the caldav standard).
|
|
||||||
splitExDate(result, line);
|
|
||||||
continue;
|
|
||||||
} else if (line.startsWith("X-ENTOURAGE_UUID:")) {
|
|
||||||
// Apple iCal doesn't understand this key, and it's entourage
|
|
||||||
// specific (i.e. not needed by any caldav client): strip it out
|
|
||||||
continue;
|
|
||||||
} else if (fromServer && line.startsWith("ATTENDEE;")
|
|
||||||
&& (line.indexOf(email) >= 0)) {
|
|
||||||
// If this is coming from the server, strip out RSVP for this
|
|
||||||
// user as an attendee where the partstat is something other
|
|
||||||
// than PARTSTAT=NEEDS-ACTION since the RSVP confuses iCal4 into
|
|
||||||
// thinking the attendee has not replied
|
|
||||||
|
|
||||||
int rsvpSuffix = line.indexOf("RSVP=TRUE;");
|
|
||||||
int rsvpPrefix = line.indexOf(";RSVP=TRUE");
|
|
||||||
|
|
||||||
if (((rsvpSuffix >= 0) || (rsvpPrefix >= 0))
|
|
||||||
&& (line.indexOf("PARTSTAT=") >= 0)
|
|
||||||
&& (line.indexOf("PARTSTAT=NEEDS-ACTION") < 0)) {
|
|
||||||
|
|
||||||
// Strip out the "RSVP" line from the calendar entry
|
|
||||||
if (rsvpSuffix >= 0) {
|
|
||||||
line = line.substring(0, rsvpSuffix) + line.substring(rsvpSuffix + 10);
|
|
||||||
} else {
|
|
||||||
line = line.substring(0, rsvpPrefix) + line.substring(rsvpPrefix + 10);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
|
||||||
} else if (line.startsWith("ACTION:")) {
|
|
||||||
if (fromServer && "DISPLAY".equals(action)
|
|
||||||
// convert DISPLAY to AUDIO only if user defined an alarm sound
|
|
||||||
&& Settings.getProperty("davmail.caldavAlarmSound") != null) {
|
|
||||||
// Convert alarm to audio for iCal
|
|
||||||
result.writeLine("ACTION:AUDIO");
|
|
||||||
|
|
||||||
if (!sound) {
|
|
||||||
// Add defined sound into the audio alarm
|
|
||||||
result.writeLine("ATTACH;VALUE=URI:" + Settings.getProperty("davmail.caldavAlarmSound"));
|
|
||||||
}
|
|
||||||
|
|
||||||
continue;
|
|
||||||
} else if (!fromServer && "AUDIO".equals(action)) {
|
|
||||||
// Use the alarm action that exchange (and blackberry) understand
|
|
||||||
// (exchange and blackberry don't understand audio actions)
|
|
||||||
|
|
||||||
result.writeLine("ACTION:DISPLAY");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Don't recognize this type of action: pass it through
|
|
||||||
|
|
||||||
} else if (line.startsWith("CLASS:")) {
|
|
||||||
if (!fromServer && isAppleiCal) {
|
|
||||||
continue;
|
|
||||||
} else {
|
|
||||||
// still set calendarserver access inside event for iCal 3
|
|
||||||
if ("PRIVATE".equalsIgnoreCase(eventClass)) {
|
|
||||||
result.writeLine("X-CALENDARSERVER-ACCESS:CONFIDENTIAL");
|
|
||||||
} else if ("CONFIDENTIAL".equalsIgnoreCase(eventClass)) {
|
|
||||||
result.writeLine("X-CALENDARSERVER-ACCESS:PRIVATE");
|
|
||||||
} else {
|
|
||||||
result.writeLine("X-CALENDARSERVER-ACCESS:" + eventClass);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// remove organizer line if user is organizer for iPhone
|
|
||||||
} else if (fromServer && line.startsWith("ORGANIZER") && !hasAttendee) {
|
|
||||||
continue;
|
|
||||||
} else if (organizer != null && line.startsWith("ATTENDEE") && line.contains(organizer)) {
|
|
||||||
// Ignore organizer as attendee
|
|
||||||
continue;
|
|
||||||
} else if (!fromServer && line.startsWith("ATTENDEE")) {
|
|
||||||
line = replaceIcal4Principal(line);
|
|
||||||
}
|
|
||||||
|
|
||||||
result.writeLine(line);
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
reader.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
String resultString = result.toString();
|
|
||||||
|
|
||||||
return resultString;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected String fixICS(String icsBody, boolean fromServer) throws IOException {
|
protected String fixICS(String icsBody, boolean fromServer) throws IOException {
|
||||||
VCalendar vCalendar = new VCalendar(icsBody, email);
|
VCalendar vCalendar = new VCalendar(icsBody, email, vTimeZone);
|
||||||
vCalendar.fixVCalendar(fromServer);
|
vCalendar.fixVCalendar(fromServer);
|
||||||
return vCalendar.toString();
|
return vCalendar.toString();
|
||||||
}
|
}
|
||||||
@ -491,4 +192,63 @@ public class TestExchangeSessionEvent extends TestCase {
|
|||||||
System.out.println(toServer);
|
System.out.println(toServer);
|
||||||
assertTrue(toServer.contains("ACTION:DISPLAY"));
|
assertTrue(toServer.contains("ACTION:DISPLAY"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void testReceiveAllDay() throws IOException {
|
||||||
|
String itemBody = "BEGIN:VCALENDAR\n" +
|
||||||
|
vTimeZone +
|
||||||
|
"BEGIN:VEVENT\n" +
|
||||||
|
"DTSTART;TZID=\"(GMT+01.00) Paris/Madrid/Brussels/Copenhagen\":20100615T000000\n" +
|
||||||
|
"DTEND;TZID=\"(GMT+01.00) Paris/Madrid/Brussels/Copenhagen\":20100616T000000\n" +
|
||||||
|
"X-MICROSOFT-CDO-ALLDAYEVENT:TRUE\n" +
|
||||||
|
"END:VEVENT\n" +
|
||||||
|
"END:VCALENDAR";
|
||||||
|
String toClient = fixICS(itemBody, true);
|
||||||
|
System.out.println(toClient);
|
||||||
|
// OWA created allday events have the X-MICROSOFT-CDO-ALLDAYEVENT set to true and always 000000 in event time
|
||||||
|
// just remove the TZID, add VALUE=DATE param and set a date only value
|
||||||
|
assertTrue(toClient.contains("DTSTART;VALUE=DATE:20100615"));
|
||||||
|
assertTrue(toClient.contains("DTEND;VALUE=DATE:20100616"));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testSendAllDay() throws IOException {
|
||||||
|
String itemBody = "BEGIN:VCALENDAR\n" +
|
||||||
|
"BEGIN:VEVENT\n" +
|
||||||
|
"DTSTART;VALUE=DATE:20100615\n" +
|
||||||
|
"DTEND;VALUE=DATE:20100616\n" +
|
||||||
|
"END:VEVENT\n" +
|
||||||
|
"END:VCALENDAR";
|
||||||
|
String toServer = fixICS(itemBody, false);
|
||||||
|
System.out.println(toServer);
|
||||||
|
// Client created allday event have no timezone and no time information in date values
|
||||||
|
// first set the X-MICROSOFT-CDO-ALLDAYEVENT flag for OWA
|
||||||
|
assertTrue(toServer.contains("X-MICROSOFT-CDO-ALLDAYEVENT:TRUE"));
|
||||||
|
// then patch TZID for Outlook (need to retrieve OWA TZID
|
||||||
|
assertTrue(toServer.contains("BEGIN:VTIMEZONE"));
|
||||||
|
assertTrue(toServer.contains("TZID:" + vTimeZone.getPropertyValue("TZID")));
|
||||||
|
assertTrue(toServer.contains("DTSTART;TZID=\"" + vTimeZone.getPropertyValue("TZID") + "\":20100615T000000"));
|
||||||
|
assertTrue(toServer.contains("DTEND;TZID=\"" + vTimeZone.getPropertyValue("TZID") + "\":20100616T000000"));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testRsvp() throws IOException {
|
||||||
|
String itemBody = "BEGIN:VCALENDAR\n" +
|
||||||
|
"BEGIN:VEVENT\n" +
|
||||||
|
"ATTENDEE;PARTSTAT=ACCEPTED;RSVP=TRUE:MAILTO:" + email + "\n" +
|
||||||
|
"END:VEVENT\n" +
|
||||||
|
"END:VCALENDAR";
|
||||||
|
String toClient = fixICS(itemBody, true);
|
||||||
|
System.out.println(toClient);
|
||||||
|
assertTrue(toClient.contains("ATTENDEE;PARTSTAT=ACCEPTED:MAILTO:" + email));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testExdate() throws IOException {
|
||||||
|
String itemBody = "BEGIN:VCALENDAR\n" +
|
||||||
|
"BEGIN:VEVENT\n" +
|
||||||
|
"EXDATE;TZID=\"Europe/Paris\":20100809T150000,20100823T150000\n" +
|
||||||
|
"END:VEVENT\n" +
|
||||||
|
"END:VCALENDAR";
|
||||||
|
String toClient = fixICS(itemBody, true);
|
||||||
|
System.out.println(toClient);
|
||||||
|
assertTrue(toClient.contains("EXDATE;TZID=\"Europe/Paris\":20100823T150000"));
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user