1
0
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:
mguessan 2010-07-30 15:09:44 +00:00
parent 5bacc44dea
commit bba9c3616a
8 changed files with 269 additions and 644 deletions

View File

@ -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);
}

View File

@ -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();

View File

@ -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() {

View File

@ -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);
}
}
}
}

View File

@ -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);

View File

@ -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);
}

View File

@ -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;
}

View File

@ -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"));
}
}