mirror of
https://github.com/moparisthebest/davmail
synced 2024-12-13 03:02: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);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
wireLogger.debug(e);
|
||||
wireLogger.debug(e.getMessage(), e);
|
||||
DavGatewayTray.warn(new BundleMessage("LOG_ITEM_NOT_AVAILABLE", eventName, href));
|
||||
notFound.add(href);
|
||||
}
|
||||
|
@ -1867,7 +1867,7 @@ public abstract class ExchangeSession {
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
bodyPart.getDataHandler().writeTo(baos);
|
||||
baos.close();
|
||||
result = fixICS(new String(baos.toByteArray(), "UTF-8"), true);
|
||||
result = new String(baos.toByteArray(), "UTF-8");
|
||||
} else {
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
mimeMessage.writeTo(baos);
|
||||
@ -1937,265 +1937,19 @@ public abstract class ExchangeSession {
|
||||
}
|
||||
|
||||
protected String fixICS(String icsBody, boolean fromServer) throws IOException {
|
||||
// first pass : detect
|
||||
class AllDayState {
|
||||
boolean isAllDay;
|
||||
boolean hasCdoAllDay;
|
||||
boolean isCdoAllDay;
|
||||
}
|
||||
|
||||
dumpIndex++;
|
||||
dumpICS(icsBody, fromServer, false);
|
||||
|
||||
// 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 {
|
||||
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;
|
||||
if (LOGGER.isDebugEnabled() && fromServer) {
|
||||
LOGGER.debug("Vcalendar body received from server:\n" +icsBody);
|
||||
}
|
||||
} 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(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 vCalendar = new VCalendar(icsBody, getEmail(), getVTimezone());
|
||||
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);
|
||||
|
||||
return resultString;
|
||||
@ -3481,14 +3235,14 @@ public abstract class ExchangeSession {
|
||||
|
||||
}
|
||||
|
||||
protected VTimezone vTimezone;
|
||||
protected VObject vTimezone;
|
||||
|
||||
/**
|
||||
* Load and return current user OWA timezone.
|
||||
*
|
||||
* @return current timezone
|
||||
*/
|
||||
public VTimezone getVTimezone() {
|
||||
public VObject getVTimezone() {
|
||||
if (vTimezone == null) {
|
||||
// need to load Timezone info from OWA
|
||||
loadVtimezone();
|
||||
|
@ -19,6 +19,7 @@
|
||||
package davmail.exchange;
|
||||
|
||||
import davmail.Settings;
|
||||
import org.apache.log4j.Logger;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
@ -28,6 +29,7 @@ import java.io.StringReader;
|
||||
* VCalendar object.
|
||||
*/
|
||||
public class VCalendar extends VObject {
|
||||
protected static final Logger LOGGER = Logger.getLogger(VCalendar.class);
|
||||
protected VObject firstVevent;
|
||||
protected VObject vTimezone;
|
||||
protected String email;
|
||||
@ -37,25 +39,32 @@ public class VCalendar extends VObject {
|
||||
*
|
||||
* @param reader stream reader
|
||||
* @param email current user email
|
||||
* @param vTimezone user OWA timezone
|
||||
* @throws IOException on error
|
||||
*/
|
||||
public VCalendar(BufferedReader reader, String email) throws IOException {
|
||||
public VCalendar(BufferedReader reader, String email, VObject vTimezone) throws IOException {
|
||||
super(reader);
|
||||
if (!"VCALENDAR".equals(type)) {
|
||||
throw new IOException("Invalid type: " + type);
|
||||
}
|
||||
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 email current user email
|
||||
* @param vTimezone user OWA timezone
|
||||
* @throws IOException on error
|
||||
*/
|
||||
public VCalendar(String vCalendarBody, String email) throws IOException {
|
||||
this(new ICSBufferedReader(new StringReader(vCalendarBody)), email);
|
||||
public VCalendar(String vCalendarBody, String email, VObject vTimezone) throws IOException {
|
||||
this(new ICSBufferedReader(new StringReader(vCalendarBody)), email, vTimezone);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -124,7 +133,24 @@ public class VCalendar extends VObject {
|
||||
} else if (vObject.getPropertyValue("X-CALENDARSERVER-ACCESS") != null) {
|
||||
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
|
||||
if (vObject.getPropertyValue("ORGANIZER") == null) {
|
||||
vObject.setPropertyValue("ORGANIZER", "MAILTO:" + email);
|
||||
@ -134,12 +160,11 @@ public class VCalendar extends VObject {
|
||||
vObject.setPropertyValue("X-MICROSOFT-CDO-BUSYSTATUS",
|
||||
!"TRANSPARENT".equals(vObject.getPropertyValue("TRANSP")) ? "BUSY" : "FREE");
|
||||
|
||||
} else {
|
||||
// remove organizer line for event without attendees for iPhone
|
||||
if (getProperty("ATTENDEE") == null) {
|
||||
vObject.setPropertyValue("ORGANIZER", null);
|
||||
if (isAllDay(vObject)) {
|
||||
// convert date values to outlook compatible values
|
||||
setServerAllday(vObject.getProperty("DTSTART"));
|
||||
setServerAllday(vObject.getProperty("DTEND"));
|
||||
}
|
||||
// TODO: handle transparent ?
|
||||
}
|
||||
|
||||
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) {
|
||||
if ("VALARM".equals(vAlarm.type)) {
|
||||
String action = vAlarm.getPropertyValue("ACTION");
|
||||
@ -177,6 +234,7 @@ public class VCalendar extends VObject {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
if (!fromServer) {
|
||||
if (vObject.properties != null) {
|
||||
for (VProperty property : vObject.properties) {
|
||||
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()));
|
||||
|
||||
// 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.
|
||||
* see http://svn.calendarserver.org/repository/calendarserver/CalendarServer/trunk/doc/Extensions/caldav-privateevents.txt
|
||||
*
|
||||
* @return CLASS value
|
||||
*/
|
||||
@ -230,7 +302,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() {
|
||||
|
@ -20,6 +20,7 @@ package davmail.exchange;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.StringReader;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
@ -73,6 +74,17 @@ public class VObject {
|
||||
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 {
|
||||
VProperty property = new VProperty(line);
|
||||
// inner object
|
||||
@ -158,6 +170,9 @@ public class VObject {
|
||||
}
|
||||
|
||||
public void setPropertyValue(String name, String value) {
|
||||
if (value == null) {
|
||||
removeProperty(name);
|
||||
} else {
|
||||
VProperty property = getProperty(name);
|
||||
if (property == null) {
|
||||
property = new VProperty(name, value);
|
||||
@ -165,6 +180,15 @@ public class VObject {
|
||||
} else {
|
||||
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);
|
||||
}
|
||||
|
||||
public String getValue() {
|
||||
if (values != null && !values.isEmpty()) {
|
||||
return values.get(0);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected String key;
|
||||
@ -189,6 +197,15 @@ public class VProperty {
|
||||
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) {
|
||||
for (String collectionValue : stringCollection) {
|
||||
if (value.equalsIgnoreCase(collectionValue)) {
|
||||
@ -330,7 +347,7 @@ public class VProperty {
|
||||
}
|
||||
|
||||
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('"');
|
||||
} else {
|
||||
buffer.append(value);
|
||||
|
@ -24,6 +24,7 @@ import davmail.exception.DavMailAuthenticationException;
|
||||
import davmail.exception.DavMailException;
|
||||
import davmail.exception.HttpNotFoundException;
|
||||
import davmail.exchange.ExchangeSession;
|
||||
import davmail.exchange.VObject;
|
||||
import davmail.http.DavGatewayHttpClientFacade;
|
||||
import davmail.ui.tray.DavGatewayTray;
|
||||
import davmail.util.IOUtil;
|
||||
@ -1377,7 +1378,6 @@ public class DavExchangeSession extends ExchangeSession {
|
||||
@Override
|
||||
protected void loadVtimezone() {
|
||||
try {
|
||||
VTimezone userTimezone = new VTimezone();
|
||||
// create temporary folder
|
||||
String folderPath = getFolderPath("davmailtemp");
|
||||
createCalendarFolder(folderPath, null);
|
||||
@ -1403,14 +1403,14 @@ public class DavExchangeSession extends ExchangeSession {
|
||||
propertyList.add(Field.createDavProperty("instancetype", "0"));
|
||||
|
||||
// get forced timezone id from settings
|
||||
userTimezone.timezoneId = Settings.getProperty("davmail.timezoneId");
|
||||
if (userTimezone.timezoneId == null) {
|
||||
String timezoneId = Settings.getProperty("davmail.timezoneId");
|
||||
if (timezoneId == null) {
|
||||
// get timezoneid from OWA settings
|
||||
userTimezone.timezoneId = getTimezoneIdFromExchange();
|
||||
timezoneId = getTimezoneIdFromExchange();
|
||||
}
|
||||
// without a timezoneId, use Exchange timezone
|
||||
if (userTimezone.timezoneId != null) {
|
||||
propertyList.add(Field.createDavProperty("timezoneid", userTimezone.timezoneId));
|
||||
if (timezoneId != null) {
|
||||
propertyList.add(Field.createDavProperty("timezoneid", timezoneId));
|
||||
}
|
||||
String patchMethodUrl = URIUtil.encodePath(folderPath) + '/' + UUID.randomUUID().toString() + ".EML";
|
||||
PropPatchMethod patchMethod = new PropPatchMethod(URIUtil.encodePath(patchMethodUrl), propertyList);
|
||||
@ -1429,10 +1429,9 @@ public class DavExchangeSession extends ExchangeSession {
|
||||
getMethod.setRequestHeader("Translate", "f");
|
||||
try {
|
||||
httpClient.executeMethod(getMethod);
|
||||
userTimezone.timezoneBody = "BEGIN:VTIMEZONE" +
|
||||
this.vTimezone = new VObject("BEGIN:VTIMEZONE" +
|
||||
StringUtil.getToken(getMethod.getResponseBodyAsString(), "BEGIN:VTIMEZONE", "END:VTIMEZONE") +
|
||||
"END:VTIMEZONE\r\n";
|
||||
userTimezone.timezoneId = StringUtil.getToken(userTimezone.timezoneBody, "TZID:", "\r\n");
|
||||
"END:VTIMEZONE\r\n");
|
||||
} finally {
|
||||
getMethod.releaseConnection();
|
||||
}
|
||||
@ -1440,7 +1439,6 @@ public class DavExchangeSession extends ExchangeSession {
|
||||
|
||||
// delete temporary folder
|
||||
deleteFolder("davmailtemp");
|
||||
this.vTimezone = userTimezone;
|
||||
} catch (IOException e) {
|
||||
LOGGER.warn("Unable to get VTIMEZONE info: " + e, e);
|
||||
}
|
||||
|
@ -33,9 +33,9 @@ import java.util.*;
|
||||
public class TestExchangeSessionCalendar extends AbstractExchangeSessionTestCase {
|
||||
|
||||
public void testGetVtimezone() {
|
||||
ExchangeSession.VTimezone timezone = session.getVTimezone();
|
||||
assertNotNull(timezone.timezoneId);
|
||||
assertNotNull(timezone.timezoneBody);
|
||||
VObject timezone = session.getVTimezone();
|
||||
assertNotNull(timezone);
|
||||
assertNotNull(timezone.getPropertyValue("TZID"));
|
||||
}
|
||||
|
||||
public void testDumpVtimezones() throws IOException {
|
||||
@ -63,10 +63,10 @@ public class TestExchangeSessionCalendar extends AbstractExchangeSessionTestCase
|
||||
};
|
||||
for (int i = 1; i < 100; i++) {
|
||||
Settings.setProperty("davmail.timezoneId", String.valueOf(i));
|
||||
ExchangeSession.VTimezone timezone = session.getVTimezone();
|
||||
if (timezone.timezoneId != null) {
|
||||
properties.put(timezone.timezoneId.replaceAll("\\\\", ""), String.valueOf(i));
|
||||
System.out.println(timezone.timezoneId + '=' + i);
|
||||
VObject timezone = session.getVTimezone();
|
||||
if (timezone.getProperty("TZID") != null) {
|
||||
properties.put(timezone.getPropertyValue("TZID").replaceAll("\\\\", ""), String.valueOf(i));
|
||||
System.out.println(timezone.getPropertyValue("TZID") + '=' + i);
|
||||
}
|
||||
session.vTimezone = null;
|
||||
}
|
||||
|
@ -18,344 +18,45 @@
|
||||
*/
|
||||
package davmail.exchange;
|
||||
|
||||
import davmail.Settings;
|
||||
import davmail.exception.DavMailException;
|
||||
import davmail.util.StringUtil;
|
||||
import junit.framework.TestCase;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.StringReader;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.MissingResourceException;
|
||||
import java.util.ResourceBundle;
|
||||
|
||||
/**
|
||||
* Test ExchangeSession event conversion.
|
||||
*/
|
||||
@SuppressWarnings({"UseOfSystemOutOrSystemErr"})
|
||||
public class TestExchangeSessionEvent extends TestCase {
|
||||
String email = "user@company.com";
|
||||
static String email = "user@company.com";
|
||||
static VObject vTimeZone;
|
||||
|
||||
/*
|
||||
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;
|
||||
static {
|
||||
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;
|
||||
vTimeZone = new VObject(new ICSBufferedReader(new StringReader("BEGIN:VTIMEZONE\n" +
|
||||
"TZID:(GMT+01.00) Paris/Madrid/Brussels/Copenhagen\n" +
|
||||
"X-MICROSOFT-CDO-TZID:3\n" +
|
||||
"BEGIN:STANDARD\n" +
|
||||
"DTSTART:16010101T030000\n" +
|
||||
"TZOFFSETFROM:+0200\n" +
|
||||
"TZOFFSETTO:+0100\n" +
|
||||
"RRULE:FREQ=YEARLY;WKST=MO;INTERVAL=1;BYMONTH=10;BYDAY=-1SU\n" +
|
||||
"END:STANDARD\n" +
|
||||
"BEGIN:DAYLIGHT\n" +
|
||||
"DTSTART:16010101T020000\n" +
|
||||
"TZOFFSETFROM:+0100\n" +
|
||||
"TZOFFSETTO:+0200\n" +
|
||||
"RRULE:FREQ=YEARLY;WKST=MO;INTERVAL=1;BYMONTH=3;BYDAY=-1SU\n" +
|
||||
"END:DAYLIGHT\n" +
|
||||
"END:VTIMEZONE")));
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
} 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 {
|
||||
VCalendar vCalendar = new VCalendar(icsBody, email);
|
||||
VCalendar vCalendar = new VCalendar(icsBody, email, vTimeZone);
|
||||
vCalendar.fixVCalendar(fromServer);
|
||||
return vCalendar.toString();
|
||||
}
|
||||
@ -491,4 +192,63 @@ public class TestExchangeSessionEvent extends TestCase {
|
||||
System.out.println(toServer);
|
||||
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