Caldav: major refactoring of event content handling and notifications

git-svn-id: http://svn.code.sf.net/p/davmail/code/trunk@1322 3d1905a2-6b24-0410-a738-b14d5a86fcbd
This commit is contained in:
mguessan 2010-07-31 22:52:45 +00:00
parent bba9c3616a
commit bd687b813f
8 changed files with 292 additions and 344 deletions

View File

@ -39,8 +39,6 @@ import javax.mail.Address;
import javax.mail.MessagingException;
import javax.mail.internet.InternetAddress;
import javax.mail.internet.MimeMessage;
import javax.mail.internet.MimeMultipart;
import javax.mail.internet.MimePart;
import javax.mail.util.SharedByteArrayInputStream;
import java.io.*;
import java.net.NoRouteToHostException;
@ -1821,15 +1819,15 @@ public abstract class ExchangeSession {
*/
public abstract class Event extends Item {
protected String contentClass;
protected String itemBody;
protected VCalendar vCalendar;
/**
* @inheritDoc
*/
public Event(String folderPath, String itemName, String contentClass, String itemBody, String etag, String noneMatch) {
public Event(String folderPath, String itemName, String contentClass, String itemBody, String etag, String noneMatch) throws IOException {
super(folderPath, itemName, etag, noneMatch);
this.contentClass = contentClass;
this.itemBody = itemBody;
fixICS(itemBody.getBytes("UTF-8"), false);
}
/**
@ -1843,116 +1841,36 @@ public abstract class ExchangeSession {
return "text/calendar;charset=UTF-8";
}
@Override
public String getBody() throws IOException {
if (vCalendar == null) {
fixICS(getEventContent(), true);
}
return vCalendar.toString();
}
/**
* Load ICS content from MIME message input stream
* Retrieve item body from Exchange
*
* @param mimeInputStream mime message input stream
* @return mime message ics attachment body
* @throws IOException on error
* @throws MessagingException on error
* @return item content
* @throws HttpException on error
*/
protected String getICS(InputStream mimeInputStream) throws IOException, MessagingException {
String result;
MimeMessage mimeMessage = new MimeMessage(null, mimeInputStream);
Object mimeBody = mimeMessage.getContent();
MimePart bodyPart = null;
if (mimeBody instanceof MimeMultipart) {
bodyPart = getCalendarMimePart((MimeMultipart) mimeBody);
} else if (isCalendarContentType(mimeMessage.getContentType())) {
// no multipart, single body
bodyPart = mimeMessage;
}
if (bodyPart != null) {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
bodyPart.getDataHandler().writeTo(baos);
baos.close();
result = new String(baos.toByteArray(), "UTF-8");
} else {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
mimeMessage.writeTo(baos);
baos.close();
throw new DavMailException("EXCEPTION_INVALID_MESSAGE_CONTENT", new String(baos.toByteArray(), "UTF-8"));
}
return result;
}
protected static final String TEXT_CALENDAR = "text/calendar";
protected static final String APPLICATION_ICS = "application/ics";
protected boolean isCalendarContentType(String contentType) {
return TEXT_CALENDAR.regionMatches(true, 0, contentType, 0, TEXT_CALENDAR.length()) ||
APPLICATION_ICS.regionMatches(true, 0, contentType, 0, APPLICATION_ICS.length());
}
protected MimePart getCalendarMimePart(MimeMultipart multiPart) throws IOException, MessagingException {
MimePart bodyPart = null;
for (int i = 0; i < multiPart.getCount(); i++) {
String contentType = multiPart.getBodyPart(i).getContentType();
if (isCalendarContentType(contentType)) {
bodyPart = (MimePart) multiPart.getBodyPart(i);
break;
} else if (contentType.startsWith("multipart")) {
Object content = multiPart.getBodyPart(i).getContent();
if (content instanceof MimeMultipart) {
bodyPart = getCalendarMimePart((MimeMultipart) content);
}
}
}
return bodyPart;
}
protected String fixTimezoneId(String line, String validTimezoneId) {
return StringUtil.replaceToken(line, "TZID=", ":", validTimezoneId);
}
protected void splitExDate(ICSBufferedWriter result, String line) {
int cur = line.lastIndexOf(':') + 1;
String start = line.substring(0, cur);
for (int next = line.indexOf(',', cur); next != -1; next = line.indexOf(',', cur)) {
String val = line.substring(cur, next);
result.writeLine(start + val);
cur = next + 1;
}
result.writeLine(start + line.substring(cur));
}
protected String getAllDayLine(String line) throws IOException {
int valueIndex = line.lastIndexOf(':');
int valueEndIndex = line.lastIndexOf('T');
if (valueIndex < 0 || valueEndIndex < 0) {
throw new DavMailException("EXCEPTION_INVALID_ICS_LINE", line);
}
int keyIndex = line.indexOf(';');
if (keyIndex == -1) {
keyIndex = valueIndex;
}
String dateValue = line.substring(valueIndex + 1, valueEndIndex);
String key = line.substring(0, Math.min(keyIndex, valueIndex));
return key + ";VALUE=DATE:" + dateValue;
}
protected String fixICS(String icsBody, boolean fromServer) throws IOException {
dumpIndex++;
dumpICS(icsBody, fromServer, false);
public abstract byte[] getEventContent() throws IOException;
protected void fixICS(byte[] icsContent, boolean fromServer) throws IOException {
if (LOGGER.isDebugEnabled() && fromServer) {
LOGGER.debug("Vcalendar body received from server:\n" +icsBody);
dumpIndex++;
String icsBody = new String(icsContent);
dumpICS(icsBody, fromServer, false);
LOGGER.debug("Vcalendar body received from server:\n" + icsBody);
}
VCalendar vCalendar = new VCalendar(icsBody, getEmail(), getVTimezone());
vCalendar = new VCalendar(icsContent, getEmail(), getVTimezone());
vCalendar.fixVCalendar(fromServer);
String resultString = vCalendar.toString();
if (LOGGER.isDebugEnabled() && !fromServer) {
LOGGER.debug("Fixed Vcalendar body to server:\n" +resultString);
String resultString = vCalendar.toString();
LOGGER.debug("Fixed Vcalendar body to server:\n" + resultString);
dumpICS(resultString, fromServer, true);
}
dumpICS(resultString, fromServer, true);
return resultString;
}
protected void dumpICS(String icsBody, boolean fromServer, boolean after) {
@ -2020,126 +1938,6 @@ public abstract class ExchangeSession {
}
protected String getICSValue(String icsBody, String prefix, String defval) throws IOException {
// only return values in VEVENT section, not VALARM
Stack<String> sectionStack = new Stack<String>();
BufferedReader reader = null;
try {
reader = new ICSBufferedReader(new StringReader(icsBody));
String line;
while ((line = reader.readLine()) != null) {
if (line.startsWith("BEGIN:")) {
sectionStack.push(line);
} else if (line.startsWith("END:") && !sectionStack.isEmpty()) {
sectionStack.pop();
} else if (!sectionStack.isEmpty() && "BEGIN:VEVENT".equals(sectionStack.peek()) && line.startsWith(prefix)) {
return line.substring(prefix.length());
}
}
} finally {
if (reader != null) {
reader.close();
}
}
return defval;
}
protected String getICSSummary(String icsBody) throws IOException {
return getICSValue(icsBody, "SUMMARY:", BundleMessage.format("MEETING_REQUEST"));
}
protected String getICSDescription(String icsBody) throws IOException {
return getICSValue(icsBody, "DESCRIPTION:", "");
}
class Participants {
String attendees;
String optionalAttendees;
String organizer;
}
/**
* Parse ics event for attendees and organizer.
* For notifications, only include attendees with RSVP=TRUE or PARTSTAT=NEEDS-ACTION
*
* @param isNotification get only notified attendees
* @return participants
* @throws IOException on error
*/
protected Participants getParticipants(boolean isNotification) throws IOException {
HashSet<String> attendees = new HashSet<String>();
HashSet<String> optionalAttendees = new HashSet<String>();
String organizer = null;
BufferedReader reader = null;
try {
reader = new ICSBufferedReader(new StringReader(itemBody));
String line;
while ((line = reader.readLine()) != null) {
int index = line.indexOf(':');
if (index >= 0) {
String key = line.substring(0, index);
String value = line.substring(index + 1);
int semiColon = key.indexOf(';');
if (semiColon >= 0) {
key = key.substring(0, semiColon);
}
if ("ORGANIZER".equals(key) || "ATTENDEE".equals(key)) {
int colonIndex = value.indexOf(':');
if (colonIndex >= 0) {
value = value.substring(colonIndex + 1);
}
value = replaceIcal4Principal(value);
if ("ORGANIZER".equals(key)) {
organizer = value;
// exclude current user and invalid values from recipients
// also exclude no action attendees
} else if (!email.equalsIgnoreCase(value) && value.indexOf('@') >= 0
// return all attendees for user calendar folder, filter for notifications
&& (!isNotification
// notify attendee if reply explicitly requested
|| (line.indexOf("RSVP=TRUE") >= 0)
|| (
// workaround for iCal bug: do not notify if reply explicitly not requested
!(line.indexOf("RSVP=FALSE") >= 0) &&
((line.indexOf("PARTSTAT=NEEDS-ACTION") >= 0
// need to include other PARTSTATs participants for CANCEL notifications
|| line.indexOf("PARTSTAT=ACCEPTED") >= 0
|| line.indexOf("PARTSTAT=DECLINED") >= 0
|| line.indexOf("PARTSTAT=TENTATIVE") >= 0))
))) {
if (line.indexOf("ROLE=OPT-PARTICIPANT") >= 0) {
optionalAttendees.add(value);
} else {
attendees.add(value);
}
}
}
}
}
} finally {
if (reader != null) {
reader.close();
}
}
Participants participants = new Participants();
participants.attendees = StringUtil.join(attendees, ", ");
participants.optionalAttendees = StringUtil.join(optionalAttendees, ", ");
participants.organizer = organizer;
return participants;
}
protected String getICSMethod(String icsBody) {
String icsMethod = StringUtil.getToken(icsBody, "METHOD:", "\r");
if (icsMethod == null) {
// default method is REQUEST
icsMethod = "REQUEST";
}
return icsMethod;
}
/**
* Build Mime body for event or event message.
*
@ -2150,7 +1948,7 @@ public abstract class ExchangeSession {
String boundary = UUID.randomUUID().toString();
ByteArrayOutputStream baos = new ByteArrayOutputStream();
MimeOutputStreamWriter writer = new MimeOutputStreamWriter(baos);
String method = getICSMethod(itemBody);
String method = vCalendar.getMethod();
writer.writeHeader("Content-Transfer-Encoding", "7bit");
writer.writeHeader("Content-class", contentClass);
@ -2158,53 +1956,54 @@ public abstract class ExchangeSession {
writer.writeHeader("Date", new Date());
// Make sure invites have a proper subject line
writer.writeHeader("Subject", getICSSummary(itemBody));
// TODO: get current user attendee status, i18n
String subject = vCalendar.getFirstVevent().getPropertyValue("SUMMARY");
if (subject == null) {
subject = BundleMessage.format("MEETING_REQUEST");
}
writer.writeHeader("Subject", subject);
if ("urn:content-classes:calendarmessage".equals(contentClass)) {
// need to parse attendees and organizer to build recipients
Participants participants = getParticipants(true);
if (email.equalsIgnoreCase(participants.organizer)) {
VCalendar.Recipients recipients = vCalendar.getRecipients(true);
if (email.equalsIgnoreCase(recipients.organizer)) {
// current user is organizer => notify all
writer.writeHeader("To", participants.attendees);
writer.writeHeader("Cc", participants.optionalAttendees);
writer.writeHeader("To", recipients.attendees);
writer.writeHeader("Cc", recipients.optionalAttendees);
// do not send notification if no recipients found
if (participants.attendees == null && participants.optionalAttendees == null) {
if (recipients.attendees == null && recipients.optionalAttendees == null) {
return null;
}
} else {
// notify only organizer
writer.writeHeader("To", participants.organizer);
writer.writeHeader("To", recipients.organizer);
// do not send notification if no recipients found
if (participants.organizer == null) {
if (recipients.organizer == null) {
return null;
}
}
} else {
// need to parse attendees and organizer to build recipients
Participants participants = getParticipants(false);
VCalendar.Recipients recipients = vCalendar.getRecipients(false);
// storing appointment, full recipients header
if (participants.attendees != null) {
writer.writeHeader("To", participants.attendees);
if (recipients.attendees != null) {
writer.writeHeader("To", recipients.attendees);
} else {
// use current user as attendee
writer.writeHeader("To", email);
}
writer.writeHeader("Cc", participants.optionalAttendees);
writer.writeHeader("Cc", recipients.optionalAttendees);
if (participants.organizer != null) {
writer.writeHeader("From", participants.organizer);
if (recipients.organizer != null) {
writer.writeHeader("From", recipients.organizer);
} else {
writer.writeHeader("From", email);
}
// if not organizer, set REPLYTIME to force Outlook in attendee mode
if (participants.organizer != null && !email.equalsIgnoreCase(participants.organizer)) {
if (itemBody.indexOf("METHOD:") < 0) {
itemBody = itemBody.replaceAll("BEGIN:VCALENDAR", "BEGIN:VCALENDAR\r\nMETHOD:REQUEST");
}
if (itemBody.indexOf("X-MICROSOFT-CDO-REPLYTIME") < 0) {
itemBody = itemBody.replaceAll("END:VEVENT", "X-MICROSOFT-CDO-REPLYTIME:" +
getZuluDateFormat().format(new Date()) + "\r\nEND:VEVENT");
if (recipients.organizer != null && !email.equalsIgnoreCase(recipients.organizer)) {
if (method == null) {
vCalendar.setPropertyValue("METHOD", "REQUEST");
}
}
}
@ -2218,9 +2017,9 @@ public abstract class ExchangeSession {
// Write a part of the message that contains the
// ICS description so that invites contain the description text
String description = getICSDescription(itemBody).replaceAll("\\\\[Nn]", "\r\n");
String description = vCalendar.getFirstVevent().getPropertyValue("DESCRIPTION");
if (description.length() > 0) {
if (description != null && description.length() > 0) {
writer.writeHeader("Content-Type", "text/plain;\r\n" +
"\tcharset=\"utf-8\"");
writer.writeHeader("content-transfer-encoding", "8bit");
@ -2238,7 +2037,7 @@ public abstract class ExchangeSession {
writer.writeHeader("Content-Transfer-Encoding", "8bit");
writer.writeLn();
writer.flush();
baos.write(fixICS(itemBody, false).getBytes("UTF-8"));
baos.write(vCalendar.toString().getBytes("UTF-8"));
writer.writeLn();
writer.writeLn("------=_NextPart_" + boundary + "--");
writer.close();
@ -2251,21 +2050,7 @@ public abstract class ExchangeSession {
* @return action result
* @throws IOException on error
*/
public ItemResult createOrUpdate() throws IOException {
byte[] mimeContent = createMimeContent();
ItemResult itemResult;
if (mimeContent != null) {
itemResult = createOrUpdate(mimeContent);
} else {
itemResult = new ItemResult();
itemResult.status = HttpStatus.SC_NO_CONTENT;
}
return itemResult;
}
protected abstract ItemResult createOrUpdate(byte[] mimeContent) throws IOException;
public abstract ItemResult createOrUpdate() throws IOException;
}
@ -2546,7 +2331,7 @@ public abstract class ExchangeSession {
properties.put("outlookmessageclass", "IPM.Contact");
VObject vcard = new VObject(new ICSBufferedReader(new StringReader(itemBody)));
for (VProperty property:vcard.getProperties()) {
for (VProperty property : vcard.getProperties()) {
if ("FN".equals(property.getKey())) {
properties.put("cn", property.getValue());
properties.put("subject", property.getValue());
@ -3129,7 +2914,7 @@ public abstract class ExchangeSession {
}
FreeBusy freeBusy = null;
String fbdata = getFreeBusyData(attendee, exchangeZuluDateFormat.format(startDate), exchangeZuluDateFormat.format(endDate), FREE_BUSY_INTERVAL);
String fbdata = getFreeBusyData(attendee, exchangeZuluDateFormat.format(startDate), exchangeZuluDateFormat.format(endDate), FREE_BUSY_INTERVAL);
if (fbdata != null) {
freeBusy = new FreeBusy(icalDateFormat, startDate, fbdata);
}
@ -3220,21 +3005,6 @@ public abstract class ExchangeSession {
}
}
/**
* Timezone structure
*/
public static final class VTimezone {
/**
* Timezone iCalendar body
*/
public String timezoneBody;
/**
* Timezone id
*/
public String timezoneId;
}
protected VObject vTimezone;
/**

View File

@ -19,11 +19,13 @@
package davmail.exchange;
import davmail.Settings;
import davmail.util.StringUtil;
import org.apache.log4j.Logger;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.StringReader;
import java.io.*;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
/**
* VCalendar object.
@ -67,6 +69,18 @@ public class VCalendar extends VObject {
this(new ICSBufferedReader(new StringReader(vCalendarBody)), email, vTimezone);
}
/**
* Create VCalendar object from string;
*
* @param vCalendarContent item content
* @param email current user email
* @param vTimezone user OWA timezone
* @throws IOException on error
*/
public VCalendar(byte[] vCalendarContent, String email, VObject vTimezone) throws IOException {
this(new ICSBufferedReader(new InputStreamReader(new ByteArrayInputStream(vCalendarContent), "UTF-8")), email, vTimezone);
}
@Override
protected void addVObject(VObject vObject) {
super.addVObject(vObject);
@ -83,28 +97,19 @@ public class VCalendar extends VObject {
return dtstart != null && dtstart.hasParam("VALUE", "DATE");
}
protected boolean hasCdoAllDay(VObject vObject) {
return vObject.getProperty("X-MICROSOFT-CDO-ALLDAYEVENT") != null;
}
protected boolean hasCdoBusyStatus(VObject vObject) {
return vObject.getProperty("X-MICROSOFT-CDO-BUSYSTATUS") != null;
}
protected boolean isCdoAllDay(VObject vObject) {
return "TRUE".equals(vObject.getPropertyValue("X-MICROSOFT-CDO-ALLDAYEVENT"));
}
protected boolean isAppleiCal() {
return getPropertyValue("PRODID").contains("iCal");
}
protected String getOrganizer() {
String organizer = firstVevent.getPropertyValue("ORGANIZER");
if (organizer.startsWith("MAILTO:")) {
return organizer.substring(7);
protected String getEmailValue(VProperty property) {
if (property == null) {
return null;
}
String propertyValue = property.getValue();
if (propertyValue != null && (propertyValue.startsWith("MAILTO:") || propertyValue.startsWith("mailto:"))) {
return propertyValue.substring(7);
} else {
return organizer;
return propertyValue;
}
}
@ -113,23 +118,21 @@ public class VCalendar extends VObject {
}
protected void fixVCalendar(boolean fromServer) {
// append missing method
if (getProperty("METHOD") == null) {
setPropertyValue("METHOD", "PUBLISH");
}
// iCal 4 global private flag
if (fromServer) {
setPropertyValue("X-CALENDARSERVER-ACCESS", getCalendarServerAccess());
}
// iCal 4 global X-CALENDARSERVER-ACCESS
String calendarServerAccess = getPropertyValue("X-CALENDARSERVER-ACCESS");
String now = ExchangeSession.getZuluDateFormat().format(new Date());
// TODO: patch timezone for iPhone
// iterate over vObjects
for (VObject vObject : vObjects) {
if ("VEVENT".equals(vObject.type)) {
if (calendarServerAccess != null) {
vObject.setPropertyValue("CLASS", getEventClass(calendarServerAccess));
// iCal 3, get X-CALENDARSERVER-ACCESS from local VEVENT
} else if (vObject.getPropertyValue("X-CALENDARSERVER-ACCESS") != null) {
vObject.setPropertyValue("CLASS", getEventClass(vObject.getPropertyValue("X-CALENDARSERVER-ACCESS")));
}
@ -143,17 +146,24 @@ public class VCalendar extends VObject {
setClientAllday(vObject.getProperty("DTSTART"));
setClientAllday(vObject.getProperty("DTEND"));
}
vObject.setPropertyValue("TRANSP",
!"FREE".equals(vObject.getPropertyValue("X-MICROSOFT-CDO-BUSYSTATUS")) ? "OPAQUE" : "TRANSPARENT");
String cdoBusyStatus = vObject.getPropertyValue("X-MICROSOFT-CDO-BUSYSTATUS");
if (cdoBusyStatus != null) {
vObject.setPropertyValue("TRANSP",
!"FREE".equals(cdoBusyStatus) ? "OPAQUE" : "TRANSPARENT");
}
// TODO splitExDate
// Apple iCal doesn't understand this key, and it's entourage
// specific (i.e. not needed by any caldav client): strip it out
vObject.removeProperty("X-ENTOURAGE_UUID");
splitExDate(vObject);
} else {
// add organizer line to all events created in Exchange for active sync
if (vObject.getPropertyValue("ORGANIZER") == null) {
String organizer = getEmailValue(vObject.getProperty("ORGANIZER"));
if (organizer == null) {
vObject.setPropertyValue("ORGANIZER", "MAILTO:" + email);
} else if (!email.equalsIgnoreCase(organizer) && vObject.getProperty("X-MICROSOFT-CDO-REPLYTIME") == null) {
vObject.setPropertyValue("X-MICROSOFT-CDO-REPLYTIME", now);
}
// set OWA allday flag
vObject.setPropertyValue("X-MICROSOFT-CDO-ALLDAYEVENT", isAllDay(vObject) ? "TRUE" : "FALSE");
@ -169,15 +179,31 @@ public class VCalendar extends VObject {
fixAttendees(vObject, fromServer);
// TODO handle BUSYSTATUS
fixAlarm(vObject, fromServer);
}
}
}
private void setServerAllday(VProperty property) {
protected void splitExDate(VObject vObject) {
List<VProperty> exDateProperties = vObject.getProperties("EXDATE");
if (exDateProperties != null) {
for (VProperty property : exDateProperties) {
String value = property.getValue();
if (value.indexOf(',') >= 0) {
// split property
vObject.removeProperty(property);
for (String singleValue : value.split(",")) {
VProperty singleProperty = new VProperty("EXDATE", singleValue);
singleProperty.setParams(property.getParams());
vObject.addProperty(singleProperty);
}
}
}
}
}
protected void setServerAllday(VProperty property) {
// set TZID param
if (!property.hasParam("TZID")) {
property.addParam("TZID", vTimezone.getPropertyValue("TZID"));
@ -288,6 +314,7 @@ public class VCalendar extends VObject {
* Convert X-CALENDARSERVER-ACCESS to CLASS.
* see http://svn.calendarserver.org/repository/calendarserver/CalendarServer/trunk/doc/Extensions/caldav-privateevents.txt
*
* @param calendarServerAccess X-CALENDARSERVER-ACCESS value
* @return CLASS value
*/
protected String getEventClass(String calendarServerAccess) {
@ -303,6 +330,7 @@ public class VCalendar extends VObject {
/**
* Convert CLASS to X-CALENDARSERVER-ACCESS.
* see http://svn.calendarserver.org/repository/calendarserver/CalendarServer/trunk/doc/Extensions/caldav-privateevents.txt *
*
* @return X-CALENDARSERVER-ACCESS value
*/
protected String getCalendarServerAccess() {
@ -316,4 +344,54 @@ public class VCalendar extends VObject {
}
}
public VObject getFirstVevent() {
return firstVevent;
}
class Recipients {
String attendees;
String optionalAttendees;
String organizer;
}
public Recipients getRecipients(boolean isNotification) {
HashSet<String> attendees = new HashSet<String>();
HashSet<String> optionalAttendees = new HashSet<String>();
// get recipients from first VEVENT
List<VProperty> attendeeProperties = firstVevent.getProperties("ATTENDEE");
if (attendeeProperties != null) {
for (VProperty property : attendeeProperties) {
// exclude current user and invalid values from recipients
// also exclude no action attendees
String attendeeEmail = getEmailValue(property);
if (!email.equalsIgnoreCase(attendeeEmail) && attendeeEmail.indexOf('@') >= 0
// return all attendees for user calendar folder, filter for notifications
&& (!isNotification
// notify attendee if reply explicitly requested
|| (property.hasParam("RSVP", "TRUE"))
|| (
// workaround for iCal bug: do not notify if reply explicitly not requested
!(property.hasParam("RSVP", "FALSE")) &&
((property.hasParam("PARTSTAT", "NEEDS-ACTION")
// need to include other PARTSTATs participants for CANCEL notifications
|| property.hasParam("PARTSTAT", "ACCEPTED")
|| property.hasParam("PARTSTAT", "DECLINED")
|| property.hasParam("PARTSTAT", "TENTATIVE")))
))) {
if (property.hasParam("ROLE", "OPT-PARTICIPANT")) {
optionalAttendees.add(attendeeEmail);
} else {
attendees.add(attendeeEmail);
}
}
}
}
Recipients recipients = new Recipients();
recipients.organizer = getEmailValue(firstVevent.getProperty("ORGANIZER"));
recipients.attendees = StringUtil.join(attendees, ", ");
recipients.optionalAttendees = StringUtil.join(optionalAttendees, ", ");
return recipients;
}
}

View File

@ -160,6 +160,22 @@ public class VObject {
return null;
}
public List<VProperty> getProperties(String name) {
List<VProperty> result = null;
if (properties != null) {
for (VProperty property : properties) {
if (property.getKey().equalsIgnoreCase(name)) {
if (result == null) {
result = new ArrayList<VProperty>();
}
result.add(property);
}
}
}
return result;
}
public String getPropertyValue(String name) {
VProperty property = getProperty(name);
if (property != null) {
@ -184,11 +200,17 @@ public class VObject {
}
public void removeProperty(String name) {
if (vObjects != null) {
if (properties != null) {
VProperty property = getProperty(name);
if (property != null) {
vObjects.remove(property);
properties.remove(property);
}
}
}
public void removeProperty(VProperty property) {
if (properties != null) {
properties.remove(property);
}
}
}

View File

@ -29,7 +29,7 @@ public class VProperty {
KEY, PARAM_NAME, PARAM_VALUE, QUOTED_PARAM_VALUE, VALUE, BACKSLASH
}
protected class Param {
protected static class Param {
String name;
List<String> values;
@ -53,6 +53,12 @@ public class VProperty {
protected List<Param> params;
protected List<String> values;
/**
* Create VProperty for key and value.
*
* @param name property name
* @param value property value
*/
public VProperty(String name, String value) {
setKey(name);
setValue(value);
@ -197,6 +203,11 @@ public class VProperty {
return params != null && getParam(paramName) != null;
}
/**
* Remove param from property.
*
* @param paramName param name
*/
public void removeParam(String paramName) {
if (params != null) {
Param param = getParam(paramName);
@ -220,7 +231,7 @@ public class VProperty {
}
protected void addParam(String paramName, String paramValue) {
List paramValues = new ArrayList();
List<String> paramValues = new ArrayList<String>();
paramValues.add(paramValue);
addParam(paramName, paramValues);
}
@ -249,6 +260,14 @@ public class VProperty {
return null;
}
protected List<Param> getParams() {
return params;
}
protected void setParams(List<Param> params) {
this.params = params;
}
protected void setValue(String value) {
if (value == null) {
values = null;
@ -280,7 +299,7 @@ public class VProperty {
if (c == '\\') {
//noinspection AssignmentToForLoopParameter
c = value.charAt(++i);
if (c == 'n') {
if (c == 'n' || c == 'N') {
c = '\n';
} else if (c == 'r') {
c = '\r';

View File

@ -47,6 +47,9 @@ import org.apache.jackrabbit.webdav.property.DavPropertySet;
import org.w3c.dom.Node;
import javax.mail.MessagingException;
import javax.mail.internet.MimeMessage;
import javax.mail.internet.MimeMultipart;
import javax.mail.internet.MimePart;
import java.io.*;
import java.net.URL;
import java.text.ParseException;
@ -763,12 +766,72 @@ public class DavExchangeSession extends ExchangeSession {
/**
* @inheritDoc
*/
public Event(String folderPath, String itemName, String contentClass, String itemBody, String etag, String noneMatch) {
public Event(String folderPath, String itemName, String contentClass, String itemBody, String etag, String noneMatch) throws IOException {
super(folderPath, itemName, contentClass, itemBody, etag, noneMatch);
}
protected String getICSFromInternetContentProperty() throws IOException, DavException, MessagingException {
String result = null;
protected static final String TEXT_CALENDAR = "text/calendar";
protected static final String APPLICATION_ICS = "application/ics";
protected boolean isCalendarContentType(String contentType) {
return TEXT_CALENDAR.regionMatches(true, 0, contentType, 0, TEXT_CALENDAR.length()) ||
APPLICATION_ICS.regionMatches(true, 0, contentType, 0, APPLICATION_ICS.length());
}
protected MimePart getCalendarMimePart(MimeMultipart multiPart) throws IOException, MessagingException {
MimePart bodyPart = null;
for (int i = 0; i < multiPart.getCount(); i++) {
String contentType = multiPart.getBodyPart(i).getContentType();
if (isCalendarContentType(contentType)) {
bodyPart = (MimePart) multiPart.getBodyPart(i);
break;
} else if (contentType.startsWith("multipart")) {
Object content = multiPart.getBodyPart(i).getContent();
if (content instanceof MimeMultipart) {
bodyPart = getCalendarMimePart((MimeMultipart) content);
}
}
}
return bodyPart;
}
/**
* Load ICS content from MIME message input stream
*
* @param mimeInputStream mime message input stream
* @return mime message ics attachment body
* @throws IOException on error
* @throws MessagingException on error
*/
protected byte[] getICS(InputStream mimeInputStream) throws IOException, MessagingException {
byte[] result;
MimeMessage mimeMessage = new MimeMessage(null, mimeInputStream);
Object mimeBody = mimeMessage.getContent();
MimePart bodyPart = null;
if (mimeBody instanceof MimeMultipart) {
bodyPart = getCalendarMimePart((MimeMultipart) mimeBody);
} else if (isCalendarContentType(mimeMessage.getContentType())) {
// no multipart, single body
bodyPart = mimeMessage;
}
if (bodyPart != null) {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
bodyPart.getDataHandler().writeTo(baos);
baos.close();
result = baos.toByteArray();
} else {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
mimeMessage.writeTo(baos);
baos.close();
throw new DavMailException("EXCEPTION_INVALID_MESSAGE_CONTENT", new String(baos.toByteArray(), "UTF-8"));
}
return result;
}
protected byte[] getICSFromInternetContentProperty() throws IOException, DavException, MessagingException {
byte[] result = null;
// PropFind PR_INTERNET_CONTENT
DavPropertyNameSet davPropertyNameSet = new DavPropertyNameSet();
davPropertyNameSet.add(Field.getPropertyName("internetContent"));
@ -798,8 +861,8 @@ public class DavExchangeSession extends ExchangeSession {
* @throws HttpException on error
*/
@Override
public String getBody() throws IOException {
String result;
public byte[] getEventContent() throws IOException {
byte[] result;
LOGGER.debug("Get event: " + permanentUrl);
// try to get PR_INTERNET_CONTENT
try {
@ -822,7 +885,7 @@ public class DavExchangeSession extends ExchangeSession {
} catch (MessagingException e) {
throw buildHttpException(e);
}
return fixICS(result, true);
return result;
}
protected PutMethod internalCreateOrUpdate(String encodedHref, byte[] mimeContent) throws IOException {
@ -849,7 +912,8 @@ public class DavExchangeSession extends ExchangeSession {
* @inheritDoc
*/
@Override
protected ItemResult createOrUpdate(byte[] mimeContent) throws IOException {
public ItemResult createOrUpdate() throws IOException {
byte[] mimeContent = createMimeContent();
String encodedHref = URIUtil.encodePath(getHref());
PutMethod putMethod = internalCreateOrUpdate(encodedHref, mimeContent);
int status = putMethod.getStatusCode();

View File

@ -214,7 +214,7 @@ public class EwsExchangeSession extends ExchangeSession {
}
/**
* Get item MIME content.
* Get item content.
*
* @param itemId EWS item id
* @return item content as byte array
@ -826,18 +826,14 @@ public class EwsExchangeSession extends ExchangeSession {
/**
* @inheritDoc
*/
protected Event(String folderPath, String itemName, String contentClass, String itemBody, String etag, String noneMatch) {
protected Event(String folderPath, String itemName, String contentClass, String itemBody, String etag, String noneMatch) throws IOException {
super(folderPath, itemName, contentClass, itemBody, etag, noneMatch);
}
@Override
public ItemResult createOrUpdate() throws IOException {
return createOrUpdate(fixICS(itemBody, false).getBytes("UTF-8"));
}
byte[] itemContent = Base64.encodeBase64(vCalendar.toString().getBytes("UTF-8"));
@Override
protected ItemResult createOrUpdate(byte[] mimeContent) throws IOException {
ItemResult itemResult = new ItemResult();
EWSMethod createOrUpdateItemMethod;
@ -865,7 +861,7 @@ public class EwsExchangeSession extends ExchangeSession {
if (currentItemId != null) {
Set<FieldUpdate> updates = new HashSet<FieldUpdate>();
updates.add(new FieldUpdate(Field.get("mimeContent"), String.valueOf(Base64.encodeBase64(mimeContent))));
updates.add(new FieldUpdate(Field.get("mimeContent"), String.valueOf(Base64.encodeBase64(itemContent))));
// update
createOrUpdateItemMethod = new UpdateItemMethod(MessageDisposition.SaveOnly,
ConflictResolution.AlwaysOverwrite,
@ -875,7 +871,7 @@ public class EwsExchangeSession extends ExchangeSession {
// create
EWSMethod.Item newItem = new EWSMethod.Item();
newItem.type = "CalendarItem";
newItem.mimeContent = Base64.encodeBase64(mimeContent);
newItem.mimeContent = itemContent;
HashSet<FieldUpdate> updates = new HashSet<FieldUpdate>();
// force urlcompname
updates.add(Field.createFieldUpdate("urlcompname", convertItemNameToEML(itemName)));
@ -908,16 +904,17 @@ public class EwsExchangeSession extends ExchangeSession {
}
@Override
public String getBody() throws IOException {
String result;
LOGGER.debug("Get event: " + permanentUrl);
public byte[] getEventContent() throws IOException {
byte[] content;
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Get event: " + folderPath + '/' + itemName);
}
try {
byte[] content = getContent(itemId);
result = new String(content);
content = getContent(itemId);
} catch (IOException e) {
throw buildHttpException(e);
}
return fixICS(result, true);
return content;
}
}

View File

@ -16,7 +16,6 @@ EXCEPTION_INVALID_DATE=Invalid date: {0}
EXCEPTION_INVALID_DATES=Invalid dates: {0}
EXCEPTION_INVALID_FOLDER_URL=Invalid folder URL: {0}
EXCEPTION_INVALID_HEADER=Invalid header: {0}, HTTPS connection to an HTTP listener ?
EXCEPTION_INVALID_ICS_LINE=Invalid ICS line: {0}
EXCEPTION_INVALID_KEEPALIVE=Invalid Keep-Alive: {0}
EXCEPTION_INVALID_MAIL_PATH=Invalid mail path: {0}
EXCEPTION_INVALID_MESSAGE_CONTENT=Invalid message content: {0}

View File

@ -15,7 +15,6 @@ EXCEPTION_INVALID_DATE=Date invalide {0}
EXCEPTION_INVALID_DATES=Dates invalides : {0}
EXCEPTION_INVALID_FOLDER_URL=URL du dossier invalide : {0}
EXCEPTION_INVALID_HEADER=Entête invalide : {0}, connexion HTTPS sur le service HTTP ?
EXCEPTION_INVALID_ICS_LINE=Ligne ICS invalide : {0}
EXCEPTION_INVALID_KEEPALIVE=Keep-Alive invalide : {0}
EXCEPTION_INVALID_MAIL_PATH=Chemin de messagerie invalide : {0}
EXCEPTION_INVALID_MESSAGE_CONTENT=Contenu du message invalide : {0}