mirror of
https://github.com/moparisthebest/davmail
synced 2024-12-14 11:42:23 -05:00
Caldav: major refactoring, move all ICS methods to Event inner class
git-svn-id: http://svn.code.sf.net/p/davmail/code/trunk@863 3d1905a2-6b24-0410-a738-b14d5a86fcbd
This commit is contained in:
parent
ca1cb26fd5
commit
1ba600e818
@ -1634,6 +1634,13 @@ public class ExchangeSession {
|
|||||||
protected String href;
|
protected String href;
|
||||||
protected String permanentUrl;
|
protected String permanentUrl;
|
||||||
protected String etag;
|
protected String etag;
|
||||||
|
protected String contentClass;
|
||||||
|
protected String noneMatch;
|
||||||
|
/**
|
||||||
|
* ICS content
|
||||||
|
*/
|
||||||
|
protected String icsBody;
|
||||||
|
|
||||||
|
|
||||||
protected MimePart getCalendarMimePart(MimeMultipart multiPart) throws IOException, MessagingException {
|
protected MimePart getCalendarMimePart(MimeMultipart multiPart) throws IOException, MessagingException {
|
||||||
MimePart bodyPart = null;
|
MimePart bodyPart = null;
|
||||||
@ -1721,210 +1728,35 @@ public class ExchangeSession {
|
|||||||
public String getEtag() {
|
public String getEtag() {
|
||||||
return etag;
|
return etag;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected String fixTimezoneId(String line, String validTimezoneId) {
|
||||||
|
return StringUtil.replaceToken(line, "TZID=", ":", validTimezoneId);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
protected void splitExDate(ICSBufferedWriter result, String line) {
|
||||||
* Search calendar messages in provided folder.
|
int cur = line.lastIndexOf(':') + 1;
|
||||||
*
|
String start = line.substring(0, cur);
|
||||||
* @param folderPath Exchange folder path
|
|
||||||
* @return list of calendar messages as Event objects
|
for (int next = line.indexOf(',', cur); next != -1; next = line.indexOf(',', cur)) {
|
||||||
* @throws IOException on error
|
String val = line.substring(cur, next);
|
||||||
*/
|
result.writeLine(start + val);
|
||||||
public List<Event> getEventMessages(String folderPath) throws IOException {
|
|
||||||
List<Event> result;
|
cur = next + 1;
|
||||||
try {
|
|
||||||
String searchQuery = "Select \"DAV:getetag\", \"http://schemas.microsoft.com/exchange/permanenturl\"" +
|
|
||||||
" FROM Scope('SHALLOW TRAVERSAL OF \"" + folderPath + "\"')\n" +
|
|
||||||
" WHERE \"DAV:contentclass\" = 'urn:content-classes:calendarmessage'\n" +
|
|
||||||
" AND (NOT \"" + scheduleStateProperty.getNamespace().getURI() + scheduleStateProperty.getName() + "\" = 'CALDAV:schedule-processed')\n" +
|
|
||||||
" ORDER BY \"urn:schemas:calendar:dtstart\" DESC\n";
|
|
||||||
result = getEvents(folderPath, searchQuery);
|
|
||||||
} catch (HttpException e) {
|
|
||||||
// failover to DAV:comment property on some Exchange servers
|
|
||||||
if (DEFAULT_SCHEDULE_STATE_PROPERTY.equals(scheduleStateProperty)) {
|
|
||||||
scheduleStateProperty = DavPropertyName.create("comment", Namespace.getNamespace("DAV:"));
|
|
||||||
result = getEventMessages(folderPath);
|
|
||||||
} else {
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
result.writeLine(start + line.substring(cur));
|
||||||
* Search calendar events in provided folder.
|
|
||||||
*
|
|
||||||
* @param folderPath Exchange folder path
|
|
||||||
* @return list of calendar events
|
|
||||||
* @throws IOException on error
|
|
||||||
*/
|
|
||||||
public List<Event> getAllEvents(String folderPath) throws IOException {
|
|
||||||
int caldavPastDelay = Settings.getIntProperty("davmail.caldavPastDelay", Integer.MAX_VALUE);
|
|
||||||
String dateCondition = "";
|
|
||||||
if (caldavPastDelay != Integer.MAX_VALUE) {
|
|
||||||
Calendar cal = Calendar.getInstance();
|
|
||||||
cal.add(Calendar.DAY_OF_MONTH, -caldavPastDelay);
|
|
||||||
dateCondition = " AND \"urn:schemas:calendar:dtstart\" > '" + formatSearchDate(cal.getTime()) + "'\n";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
String searchQuery = "Select \"DAV:getetag\", \"http://schemas.microsoft.com/exchange/permanenturl\"" +
|
protected String getAllDayLine(String line) throws IOException {
|
||||||
" FROM Scope('SHALLOW TRAVERSAL OF \"" + folderPath + "\"')\n" +
|
int keyIndex = line.indexOf(';');
|
||||||
" WHERE (" +
|
int valueIndex = line.lastIndexOf(':');
|
||||||
// VTODO events have a null instancetype
|
int valueEndIndex = line.lastIndexOf('T');
|
||||||
" \"urn:schemas:calendar:instancetype\" is null OR" +
|
if (valueIndex < 0 || valueEndIndex < 0) {
|
||||||
" \"urn:schemas:calendar:instancetype\" = 1\n" +
|
throw new DavMailException("EXCEPTION_INVALID_ICS_LINE", line);
|
||||||
" OR (\"urn:schemas:calendar:instancetype\" = 0\n" +
|
|
||||||
dateCondition +
|
|
||||||
" )) AND \"DAV:contentclass\" = 'urn:content-classes:appointment'\n" +
|
|
||||||
" ORDER BY \"urn:schemas:calendar:dtstart\" DESC\n";
|
|
||||||
return getEvents(folderPath, searchQuery);
|
|
||||||
}
|
}
|
||||||
|
String dateValue = line.substring(valueIndex + 1, valueEndIndex);
|
||||||
|
String key = line.substring(0, Math.max(keyIndex, valueIndex));
|
||||||
/**
|
return key + ";VALUE=DATE:" + dateValue;
|
||||||
* Search calendar events or messages in provided folder matching the search query.
|
|
||||||
*
|
|
||||||
* @param folderPath Exchange folder path
|
|
||||||
* @param searchQuery Exchange search query
|
|
||||||
* @return list of calendar messages as Event objects
|
|
||||||
* @throws IOException on error
|
|
||||||
*/
|
|
||||||
protected List<Event> getEvents(String folderPath, String searchQuery) throws IOException {
|
|
||||||
List<Event> events = new ArrayList<Event>();
|
|
||||||
MultiStatusResponse[] responses = DavGatewayHttpClientFacade.executeSearchMethod(httpClient, URIUtil.encodePath(folderPath), searchQuery);
|
|
||||||
for (MultiStatusResponse response : responses) {
|
|
||||||
events.add(buildEvent(response));
|
|
||||||
}
|
|
||||||
return events;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get event named eventName in folder
|
|
||||||
*
|
|
||||||
* @param folderPath Exchange folder path
|
|
||||||
* @param eventName event name
|
|
||||||
* @return event object
|
|
||||||
* @throws IOException on error
|
|
||||||
*/
|
|
||||||
public Event getEvent(String folderPath, String eventName) throws IOException {
|
|
||||||
String eventPath = folderPath + '/' + eventName;
|
|
||||||
return getEvent(eventPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get event by url
|
|
||||||
*
|
|
||||||
* @param eventPath Event path
|
|
||||||
* @return event object
|
|
||||||
* @throws IOException on error
|
|
||||||
*/
|
|
||||||
public Event getEvent(String eventPath) throws IOException {
|
|
||||||
MultiStatusResponse[] responses = DavGatewayHttpClientFacade.executePropFindMethod(httpClient, URIUtil.encodePath(eventPath), 0, EVENT_REQUEST_PROPERTIES);
|
|
||||||
if (responses.length == 0) {
|
|
||||||
throw new DavMailException("EXCEPTION_EVENT_NOT_FOUND");
|
|
||||||
}
|
|
||||||
return buildEvent(responses[0]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Delete event named eventName in folder
|
|
||||||
*
|
|
||||||
* @param folderPath Exchange folder path
|
|
||||||
* @param eventName event name
|
|
||||||
* @return HTTP status
|
|
||||||
* @throws IOException on error
|
|
||||||
*/
|
|
||||||
public int deleteEvent(String folderPath, String eventName) throws IOException {
|
|
||||||
String eventPath = URIUtil.encodePath(folderPath + '/' + eventName);
|
|
||||||
int status;
|
|
||||||
if (inboxUrl.endsWith(folderPath)) {
|
|
||||||
// do not delete calendar messages, mark read and processed
|
|
||||||
ArrayList<DavProperty> list = new ArrayList<DavProperty>();
|
|
||||||
list.add(new DefaultDavProperty(scheduleStateProperty, "CALDAV:schedule-processed"));
|
|
||||||
list.add(new DefaultDavProperty(DavPropertyName.create("read", URN_SCHEMAS_HTTPMAIL), "1"));
|
|
||||||
PropPatchMethod patchMethod = new PropPatchMethod(eventPath, list);
|
|
||||||
DavGatewayHttpClientFacade.executeMethod(httpClient, patchMethod);
|
|
||||||
status = HttpStatus.SC_OK;
|
|
||||||
} else {
|
|
||||||
status = DavGatewayHttpClientFacade.executeDeleteMethod(httpClient, eventPath);
|
|
||||||
}
|
|
||||||
return status;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected Event buildEvent(MultiStatusResponse calendarResponse) throws URIException {
|
|
||||||
Event event = new Event();
|
|
||||||
event.href = URIUtil.decode(calendarResponse.getHref());
|
|
||||||
event.permanentUrl = getPropertyIfExists(calendarResponse.getProperties(HttpStatus.SC_OK), "permanenturl", SCHEMAS_EXCHANGE);
|
|
||||||
event.etag = getPropertyIfExists(calendarResponse.getProperties(HttpStatus.SC_OK), "getetag", Namespace.getNamespace("DAV:"));
|
|
||||||
return event;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static int dumpIndex;
|
|
||||||
private String defaultSound = "Basso";
|
|
||||||
|
|
||||||
protected void dumpICS(String icsBody, boolean fromServer, boolean after) {
|
|
||||||
String logFileDirectory = Settings.getLogFileDirectory();
|
|
||||||
|
|
||||||
// additional setting to activate ICS dump (not available in GUI)
|
|
||||||
int dumpMax = Settings.getIntProperty("davmail.dumpICS");
|
|
||||||
if (dumpMax > 0) {
|
|
||||||
if (dumpIndex > dumpMax) {
|
|
||||||
// Delete the oldest dump file
|
|
||||||
final int oldest = dumpIndex - dumpMax;
|
|
||||||
try {
|
|
||||||
File[] oldestFiles = (new File(logFileDirectory)).listFiles(new FilenameFilter() {
|
|
||||||
public boolean accept(File dir, String name) {
|
|
||||||
if (name.endsWith(".ics")) {
|
|
||||||
int dashIndex = name.indexOf('-');
|
|
||||||
if (dashIndex > 0) {
|
|
||||||
try {
|
|
||||||
int fileIndex = Integer.parseInt(name.substring(0, dashIndex));
|
|
||||||
return fileIndex < oldest;
|
|
||||||
} catch (NumberFormatException nfe) {
|
|
||||||
// ignore
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
for (File file : oldestFiles) {
|
|
||||||
if (!file.delete()) {
|
|
||||||
LOGGER.warn("Unable to delete " + file.getAbsolutePath());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (Exception ex) {
|
|
||||||
LOGGER.warn("Error deleting ics dump: " + ex.getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
StringBuilder filePath = new StringBuilder();
|
|
||||||
filePath.append(logFileDirectory).append('/')
|
|
||||||
.append(dumpIndex)
|
|
||||||
.append(after ? "-to" : "-from")
|
|
||||||
.append((after ^ fromServer) ? "-server" : "-client")
|
|
||||||
.append(".ics");
|
|
||||||
if ((icsBody != null) && (icsBody.length() > 0)) {
|
|
||||||
FileWriter fileWriter = null;
|
|
||||||
try {
|
|
||||||
fileWriter = new FileWriter(filePath.toString());
|
|
||||||
fileWriter.write(icsBody);
|
|
||||||
} catch (IOException e) {
|
|
||||||
LOGGER.error(e);
|
|
||||||
} finally {
|
|
||||||
if (fileWriter != null) {
|
|
||||||
try {
|
|
||||||
fileWriter.close();
|
|
||||||
} catch (IOException e) {
|
|
||||||
LOGGER.error(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected String fixICS(String icsBody, boolean fromServer) throws IOException {
|
protected String fixICS(String icsBody, boolean fromServer) throws IOException {
|
||||||
@ -2170,110 +2002,69 @@ public class ExchangeSession {
|
|||||||
return result.toString();
|
return result.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
protected void dumpICS(String icsBody, boolean fromServer, boolean after) {
|
||||||
* Replace iCal4 (Snow Leopard) principal paths with mailto expression
|
String logFileDirectory = Settings.getLogFileDirectory();
|
||||||
*
|
|
||||||
* @param value attendee value or ics line
|
// additional setting to activate ICS dump (not available in GUI)
|
||||||
* @return fixed value
|
int dumpMax = Settings.getIntProperty("davmail.dumpICS");
|
||||||
*/
|
if (dumpMax > 0) {
|
||||||
protected String replaceIcal4Principal(String value) {
|
if (dumpIndex > dumpMax) {
|
||||||
if (value.contains("/principals/__uuids__/")) {
|
// Delete the oldest dump file
|
||||||
return value.replaceAll("/principals/__uuids__/([^/]*)__AT__([^/]*)/", "mailto:$1@$2");
|
final int oldest = dumpIndex - dumpMax;
|
||||||
} else {
|
try {
|
||||||
return value;
|
File[] oldestFiles = (new File(logFileDirectory)).listFiles(new FilenameFilter() {
|
||||||
|
public boolean accept(File dir, String name) {
|
||||||
|
if (name.endsWith(".ics")) {
|
||||||
|
int dashIndex = name.indexOf('-');
|
||||||
|
if (dashIndex > 0) {
|
||||||
|
try {
|
||||||
|
int fileIndex = Integer.parseInt(name.substring(0, dashIndex));
|
||||||
|
return fileIndex < oldest;
|
||||||
|
} catch (NumberFormatException nfe) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
for (File file : oldestFiles) {
|
||||||
|
if (!file.delete()) {
|
||||||
|
LOGGER.warn("Unable to delete " + file.getAbsolutePath());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception ex) {
|
||||||
|
LOGGER.warn("Error deleting ics dump: " + ex.getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected String fixTimezoneId(String line, String validTimezoneId) {
|
StringBuilder filePath = new StringBuilder();
|
||||||
return StringUtil.replaceToken(line, "TZID=", ":", validTimezoneId);
|
filePath.append(logFileDirectory).append('/')
|
||||||
|
.append(dumpIndex)
|
||||||
|
.append(after ? "-to" : "-from")
|
||||||
|
.append((after ^ fromServer) ? "-server" : "-client")
|
||||||
|
.append(".ics");
|
||||||
|
if ((icsBody != null) && (icsBody.length() > 0)) {
|
||||||
|
FileWriter fileWriter = null;
|
||||||
|
try {
|
||||||
|
fileWriter = new FileWriter(filePath.toString());
|
||||||
|
fileWriter.write(icsBody);
|
||||||
|
} catch (IOException e) {
|
||||||
|
LOGGER.error(e);
|
||||||
|
} finally {
|
||||||
|
if (fileWriter != null) {
|
||||||
|
try {
|
||||||
|
fileWriter.close();
|
||||||
|
} catch (IOException e) {
|
||||||
|
LOGGER.error(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
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 keyIndex = line.indexOf(';');
|
|
||||||
int valueIndex = line.lastIndexOf(':');
|
|
||||||
int valueEndIndex = line.lastIndexOf('T');
|
|
||||||
if (valueIndex < 0 || valueEndIndex < 0) {
|
|
||||||
throw new DavMailException("EXCEPTION_INVALID_ICS_LINE", line);
|
|
||||||
}
|
|
||||||
String dateValue = line.substring(valueIndex + 1, valueEndIndex);
|
|
||||||
String key = line.substring(0, Math.max(keyIndex, valueIndex));
|
|
||||||
return key + ";VALUE=DATE:" + dateValue;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Event result object to hold HTTP status and event etag from an event creation/update.
|
|
||||||
*/
|
|
||||||
public static class EventResult {
|
|
||||||
/**
|
|
||||||
* HTTP status
|
|
||||||
*/
|
|
||||||
public int status;
|
|
||||||
/**
|
|
||||||
* Event etag from response HTTP header
|
|
||||||
*/
|
|
||||||
public String etag;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Build and send the MIME message for the provided ICS event.
|
|
||||||
*
|
|
||||||
* @param icsBody event in iCalendar format
|
|
||||||
* @return HTTP status
|
|
||||||
* @throws IOException on error
|
|
||||||
*/
|
|
||||||
public int sendEvent(String icsBody) throws IOException {
|
|
||||||
String messageUrl = draftsUrl + '/' + UUID.randomUUID().toString() + ".EML";
|
|
||||||
int status = internalCreateOrUpdateEvent(messageUrl, "urn:content-classes:calendarmessage", icsBody, null, null).status;
|
|
||||||
if (status != HttpStatus.SC_CREATED) {
|
|
||||||
return status;
|
|
||||||
} else {
|
|
||||||
MoveMethod method = new MoveMethod(URIUtil.encodePath(messageUrl), URIUtil.encodePath(sendmsgUrl), true);
|
|
||||||
status = DavGatewayHttpClientFacade.executeHttpMethod(httpClient, method);
|
|
||||||
if (status != HttpStatus.SC_OK) {
|
|
||||||
throw DavGatewayHttpClientFacade.buildHttpException(method);
|
|
||||||
}
|
|
||||||
return status;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Create or update event on the Exchange server
|
|
||||||
*
|
|
||||||
* @param folderPath Exchange folder path
|
|
||||||
* @param eventName event name
|
|
||||||
* @param icsBody event body in iCalendar format
|
|
||||||
* @param etag previous event etag to detect concurrent updates
|
|
||||||
* @param noneMatch if-none-match header value
|
|
||||||
* @return HTTP response event result (status and etag)
|
|
||||||
* @throws IOException on error
|
|
||||||
*/
|
|
||||||
public EventResult createOrUpdateEvent(String folderPath, String eventName, String icsBody, String etag, String noneMatch) throws IOException {
|
|
||||||
String messageUrl = folderPath + '/' + eventName;
|
|
||||||
return internalCreateOrUpdateEvent(messageUrl, "urn:content-classes:appointment", icsBody, etag, noneMatch);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected String getICSMethod(String icsBody) {
|
|
||||||
String icsMethod = StringUtil.getToken(icsBody, "METHOD:", "\r");
|
|
||||||
if (icsMethod == null) {
|
|
||||||
// default method is REQUEST
|
|
||||||
icsMethod = "REQUEST";
|
|
||||||
}
|
|
||||||
return icsMethod;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected String getICSValue(String icsBody, String prefix, String defval) throws IOException {
|
protected String getICSValue(String icsBody, String prefix, String defval) throws IOException {
|
||||||
@ -2311,7 +2102,7 @@ public class ExchangeSession {
|
|||||||
return getICSValue(icsBody, "DESCRIPTION:", "");
|
return getICSValue(icsBody, "DESCRIPTION:", "");
|
||||||
}
|
}
|
||||||
|
|
||||||
static class Participants {
|
class Participants {
|
||||||
String attendees;
|
String attendees;
|
||||||
String organizer;
|
String organizer;
|
||||||
}
|
}
|
||||||
@ -2320,12 +2111,11 @@ public class ExchangeSession {
|
|||||||
* Parse ics event for attendees and organizer.
|
* Parse ics event for attendees and organizer.
|
||||||
* For notifications, only include attendees with RSVP=TRUE or PARTSTAT=NEEDS-ACTION
|
* For notifications, only include attendees with RSVP=TRUE or PARTSTAT=NEEDS-ACTION
|
||||||
*
|
*
|
||||||
* @param icsBody ics event
|
|
||||||
* @param isNotification get only notified attendees
|
* @param isNotification get only notified attendees
|
||||||
* @return participants
|
* @return participants
|
||||||
* @throws IOException on error
|
* @throws IOException on error
|
||||||
*/
|
*/
|
||||||
protected Participants getParticipants(String icsBody, boolean isNotification) throws IOException {
|
protected Participants getParticipants(boolean isNotification) throws IOException {
|
||||||
HashSet<String> attendees = new HashSet<String>();
|
HashSet<String> attendees = new HashSet<String>();
|
||||||
String organizer = null;
|
String organizer = null;
|
||||||
BufferedReader reader = null;
|
BufferedReader reader = null;
|
||||||
@ -2380,12 +2170,21 @@ public class ExchangeSession {
|
|||||||
return participants;
|
return participants;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected EventResult internalCreateOrUpdateEvent(String messageUrl, String contentClass, String icsBody, String etag, String noneMatch) throws IOException {
|
protected String getICSMethod(String icsBody) {
|
||||||
String uid = UUID.randomUUID().toString();
|
String icsMethod = StringUtil.getToken(icsBody, "METHOD:", "\r");
|
||||||
|
if (icsMethod == null) {
|
||||||
|
// default method is REQUEST
|
||||||
|
icsMethod = "REQUEST";
|
||||||
|
}
|
||||||
|
return icsMethod;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected EventResult createOrUpdate() throws IOException {
|
||||||
|
String boundary = UUID.randomUUID().toString();
|
||||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||||
OutputStreamWriter writer = new OutputStreamWriter(baos, "ASCII");
|
OutputStreamWriter writer = new OutputStreamWriter(baos, "ASCII");
|
||||||
int status = 0;
|
int status = 0;
|
||||||
PutMethod putmethod = new PutMethod(URIUtil.encodePath(messageUrl));
|
PutMethod putmethod = new PutMethod(URIUtil.encodePath(href));
|
||||||
putmethod.setRequestHeader("Translate", "f");
|
putmethod.setRequestHeader("Translate", "f");
|
||||||
putmethod.setRequestHeader("Overwrite", "f");
|
putmethod.setRequestHeader("Overwrite", "f");
|
||||||
if (etag != null) {
|
if (etag != null) {
|
||||||
@ -2408,7 +2207,7 @@ public class ExchangeSession {
|
|||||||
writer.write("\r\n");
|
writer.write("\r\n");
|
||||||
if ("urn:content-classes:calendarmessage".equals(contentClass)) {
|
if ("urn:content-classes:calendarmessage".equals(contentClass)) {
|
||||||
// need to parse attendees and organizer to build recipients
|
// need to parse attendees and organizer to build recipients
|
||||||
Participants participants = getParticipants(icsBody, true);
|
Participants participants = getParticipants(true);
|
||||||
String recipients;
|
String recipients;
|
||||||
if (email.equalsIgnoreCase(participants.organizer)) {
|
if (email.equalsIgnoreCase(participants.organizer)) {
|
||||||
// current user is organizer => notify all
|
// current user is organizer => notify all
|
||||||
@ -2433,7 +2232,7 @@ public class ExchangeSession {
|
|||||||
writer.write("Subject: " + MimeUtility.encodeText(getICSSummary(icsBody), "UTF-8", null) + "\r\n");
|
writer.write("Subject: " + MimeUtility.encodeText(getICSSummary(icsBody), "UTF-8", null) + "\r\n");
|
||||||
|
|
||||||
// need to parse attendees and organizer to build recipients
|
// need to parse attendees and organizer to build recipients
|
||||||
Participants participants = getParticipants(icsBody, false);
|
Participants participants = getParticipants(false);
|
||||||
// storing appointment, full recipients header
|
// storing appointment, full recipients header
|
||||||
writer.write("To: ");
|
writer.write("To: ");
|
||||||
if (participants.attendees != null) {
|
if (participants.attendees != null) {
|
||||||
@ -2464,13 +2263,13 @@ public class ExchangeSession {
|
|||||||
writer.write("MIME-Version: 1.0\r\n" +
|
writer.write("MIME-Version: 1.0\r\n" +
|
||||||
"Content-Type: multipart/alternative;\r\n" +
|
"Content-Type: multipart/alternative;\r\n" +
|
||||||
"\tboundary=\"----=_NextPart_");
|
"\tboundary=\"----=_NextPart_");
|
||||||
writer.write(uid);
|
writer.write(boundary);
|
||||||
writer.write("\"\r\n" +
|
writer.write("\"\r\n" +
|
||||||
"\r\n" +
|
"\r\n" +
|
||||||
"This is a multi-part message in MIME format.\r\n" +
|
"This is a multi-part message in MIME format.\r\n" +
|
||||||
"\r\n" +
|
"\r\n" +
|
||||||
"------=_NextPart_");
|
"------=_NextPart_");
|
||||||
writer.write(uid);
|
writer.write(boundary);
|
||||||
|
|
||||||
// Write a part of the message that contains the
|
// Write a part of the message that contains the
|
||||||
// ICS description so that invites contain the description text
|
// ICS description so that invites contain the description text
|
||||||
@ -2486,7 +2285,7 @@ public class ExchangeSession {
|
|||||||
baos.write(description.getBytes("UTF-8"));
|
baos.write(description.getBytes("UTF-8"));
|
||||||
writer.write("\r\n" +
|
writer.write("\r\n" +
|
||||||
"------=_NextPart_" +
|
"------=_NextPart_" +
|
||||||
uid);
|
boundary);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2503,7 +2302,7 @@ public class ExchangeSession {
|
|||||||
writer.flush();
|
writer.flush();
|
||||||
baos.write(fixICS(icsBody, false).getBytes("UTF-8"));
|
baos.write(fixICS(icsBody, false).getBytes("UTF-8"));
|
||||||
writer.write("\r\n------=_NextPart_");
|
writer.write("\r\n------=_NextPart_");
|
||||||
writer.write(uid);
|
writer.write(boundary);
|
||||||
writer.write("--\r\n");
|
writer.write("--\r\n");
|
||||||
writer.close();
|
writer.close();
|
||||||
putmethod.setRequestEntity(new ByteArrayRequestEntity(baos.toByteArray(), "message/rfc822"));
|
putmethod.setRequestEntity(new ByteArrayRequestEntity(baos.toByteArray(), "message/rfc822"));
|
||||||
@ -2512,9 +2311,9 @@ public class ExchangeSession {
|
|||||||
status = httpClient.executeMethod(putmethod);
|
status = httpClient.executeMethod(putmethod);
|
||||||
if (status == HttpURLConnection.HTTP_OK) {
|
if (status == HttpURLConnection.HTTP_OK) {
|
||||||
if (etag != null) {
|
if (etag != null) {
|
||||||
LOGGER.debug("Updated event " + messageUrl);
|
LOGGER.debug("Updated event " + href);
|
||||||
} else {
|
} else {
|
||||||
LOGGER.warn("Overwritten event " + messageUrl);
|
LOGGER.warn("Overwritten event " + href);
|
||||||
}
|
}
|
||||||
} else if (status != HttpURLConnection.HTTP_CREATED) {
|
} else if (status != HttpURLConnection.HTTP_CREATED) {
|
||||||
LOGGER.warn("Unable to create or update message " + status + ' ' + putmethod.getStatusLine());
|
LOGGER.warn("Unable to create or update message " + status + ' ' + putmethod.getStatusLine());
|
||||||
@ -2538,13 +2337,230 @@ public class ExchangeSession {
|
|||||||
(Settings.getBooleanProperty("davmail.forceActiveSyncUpdate"))) {
|
(Settings.getBooleanProperty("davmail.forceActiveSyncUpdate"))) {
|
||||||
ArrayList<DavProperty> propertyList = new ArrayList<DavProperty>();
|
ArrayList<DavProperty> propertyList = new ArrayList<DavProperty>();
|
||||||
propertyList.add(new DefaultDavProperty(DavPropertyName.create("contentclass", Namespace.getNamespace("DAV:")), contentClass));
|
propertyList.add(new DefaultDavProperty(DavPropertyName.create("contentclass", Namespace.getNamespace("DAV:")), contentClass));
|
||||||
PropPatchMethod propPatchMethod = new PropPatchMethod(URIUtil.encodePath(messageUrl), propertyList);
|
PropPatchMethod propPatchMethod = new PropPatchMethod(URIUtil.encodePath(href), propertyList);
|
||||||
int patchStatus = DavGatewayHttpClientFacade.executeHttpMethod(httpClient, propPatchMethod);
|
int patchStatus = DavGatewayHttpClientFacade.executeHttpMethod(httpClient, propPatchMethod);
|
||||||
if (patchStatus != HttpStatus.SC_MULTI_STATUS) {
|
if (patchStatus != HttpStatus.SC_MULTI_STATUS) {
|
||||||
LOGGER.warn("Unable to patch event to trigger activeSync push");
|
LOGGER.warn("Unable to patch event to trigger activeSync push");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return eventResult;
|
return eventResult;
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Search calendar messages in provided folder.
|
||||||
|
*
|
||||||
|
* @param folderPath Exchange folder path
|
||||||
|
* @return list of calendar messages as Event objects
|
||||||
|
* @throws IOException on error
|
||||||
|
*/
|
||||||
|
public List<Event> getEventMessages(String folderPath) throws IOException {
|
||||||
|
List<Event> result;
|
||||||
|
try {
|
||||||
|
String searchQuery = "Select \"DAV:getetag\", \"http://schemas.microsoft.com/exchange/permanenturl\"" +
|
||||||
|
" FROM Scope('SHALLOW TRAVERSAL OF \"" + folderPath + "\"')\n" +
|
||||||
|
" WHERE \"DAV:contentclass\" = 'urn:content-classes:calendarmessage'\n" +
|
||||||
|
" AND (NOT \"" + scheduleStateProperty.getNamespace().getURI() + scheduleStateProperty.getName() + "\" = 'CALDAV:schedule-processed')\n" +
|
||||||
|
" ORDER BY \"urn:schemas:calendar:dtstart\" DESC\n";
|
||||||
|
result = getEvents(folderPath, searchQuery);
|
||||||
|
} catch (HttpException e) {
|
||||||
|
// failover to DAV:comment property on some Exchange servers
|
||||||
|
if (DEFAULT_SCHEDULE_STATE_PROPERTY.equals(scheduleStateProperty)) {
|
||||||
|
scheduleStateProperty = DavPropertyName.create("comment", Namespace.getNamespace("DAV:"));
|
||||||
|
result = getEventMessages(folderPath);
|
||||||
|
} else {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Search calendar events in provided folder.
|
||||||
|
*
|
||||||
|
* @param folderPath Exchange folder path
|
||||||
|
* @return list of calendar events
|
||||||
|
* @throws IOException on error
|
||||||
|
*/
|
||||||
|
public List<Event> getAllEvents(String folderPath) throws IOException {
|
||||||
|
int caldavPastDelay = Settings.getIntProperty("davmail.caldavPastDelay", Integer.MAX_VALUE);
|
||||||
|
String dateCondition = "";
|
||||||
|
if (caldavPastDelay != Integer.MAX_VALUE) {
|
||||||
|
Calendar cal = Calendar.getInstance();
|
||||||
|
cal.add(Calendar.DAY_OF_MONTH, -caldavPastDelay);
|
||||||
|
dateCondition = " AND \"urn:schemas:calendar:dtstart\" > '" + formatSearchDate(cal.getTime()) + "'\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
String searchQuery = "Select \"DAV:getetag\", \"http://schemas.microsoft.com/exchange/permanenturl\"" +
|
||||||
|
" FROM Scope('SHALLOW TRAVERSAL OF \"" + folderPath + "\"')\n" +
|
||||||
|
" WHERE (" +
|
||||||
|
// VTODO events have a null instancetype
|
||||||
|
" \"urn:schemas:calendar:instancetype\" is null OR" +
|
||||||
|
" \"urn:schemas:calendar:instancetype\" = 1\n" +
|
||||||
|
" OR (\"urn:schemas:calendar:instancetype\" = 0\n" +
|
||||||
|
dateCondition +
|
||||||
|
" )) AND \"DAV:contentclass\" = 'urn:content-classes:appointment'\n" +
|
||||||
|
" ORDER BY \"urn:schemas:calendar:dtstart\" DESC\n";
|
||||||
|
return getEvents(folderPath, searchQuery);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Search calendar events or messages in provided folder matching the search query.
|
||||||
|
*
|
||||||
|
* @param folderPath Exchange folder path
|
||||||
|
* @param searchQuery Exchange search query
|
||||||
|
* @return list of calendar messages as Event objects
|
||||||
|
* @throws IOException on error
|
||||||
|
*/
|
||||||
|
protected List<Event> getEvents(String folderPath, String searchQuery) throws IOException {
|
||||||
|
List<Event> events = new ArrayList<Event>();
|
||||||
|
MultiStatusResponse[] responses = DavGatewayHttpClientFacade.executeSearchMethod(httpClient, URIUtil.encodePath(folderPath), searchQuery);
|
||||||
|
for (MultiStatusResponse response : responses) {
|
||||||
|
events.add(buildEvent(response));
|
||||||
|
}
|
||||||
|
return events;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get event named eventName in folder
|
||||||
|
*
|
||||||
|
* @param folderPath Exchange folder path
|
||||||
|
* @param eventName event name
|
||||||
|
* @return event object
|
||||||
|
* @throws IOException on error
|
||||||
|
*/
|
||||||
|
public Event getEvent(String folderPath, String eventName) throws IOException {
|
||||||
|
String eventPath = folderPath + '/' + eventName;
|
||||||
|
return getEvent(eventPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get event by url
|
||||||
|
*
|
||||||
|
* @param eventPath Event path
|
||||||
|
* @return event object
|
||||||
|
* @throws IOException on error
|
||||||
|
*/
|
||||||
|
public Event getEvent(String eventPath) throws IOException {
|
||||||
|
MultiStatusResponse[] responses = DavGatewayHttpClientFacade.executePropFindMethod(httpClient, URIUtil.encodePath(eventPath), 0, EVENT_REQUEST_PROPERTIES);
|
||||||
|
if (responses.length == 0) {
|
||||||
|
throw new DavMailException("EXCEPTION_EVENT_NOT_FOUND");
|
||||||
|
}
|
||||||
|
return buildEvent(responses[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete event named eventName in folder
|
||||||
|
*
|
||||||
|
* @param folderPath Exchange folder path
|
||||||
|
* @param eventName event name
|
||||||
|
* @return HTTP status
|
||||||
|
* @throws IOException on error
|
||||||
|
*/
|
||||||
|
public int deleteEvent(String folderPath, String eventName) throws IOException {
|
||||||
|
String eventPath = URIUtil.encodePath(folderPath + '/' + eventName);
|
||||||
|
int status;
|
||||||
|
if (inboxUrl.endsWith(folderPath)) {
|
||||||
|
// do not delete calendar messages, mark read and processed
|
||||||
|
ArrayList<DavProperty> list = new ArrayList<DavProperty>();
|
||||||
|
list.add(new DefaultDavProperty(scheduleStateProperty, "CALDAV:schedule-processed"));
|
||||||
|
list.add(new DefaultDavProperty(DavPropertyName.create("read", URN_SCHEMAS_HTTPMAIL), "1"));
|
||||||
|
PropPatchMethod patchMethod = new PropPatchMethod(eventPath, list);
|
||||||
|
DavGatewayHttpClientFacade.executeMethod(httpClient, patchMethod);
|
||||||
|
status = HttpStatus.SC_OK;
|
||||||
|
} else {
|
||||||
|
status = DavGatewayHttpClientFacade.executeDeleteMethod(httpClient, eventPath);
|
||||||
|
}
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected Event buildEvent(MultiStatusResponse calendarResponse) throws URIException {
|
||||||
|
Event event = new Event();
|
||||||
|
event.href = URIUtil.decode(calendarResponse.getHref());
|
||||||
|
event.permanentUrl = getPropertyIfExists(calendarResponse.getProperties(HttpStatus.SC_OK), "permanenturl", SCHEMAS_EXCHANGE);
|
||||||
|
event.etag = getPropertyIfExists(calendarResponse.getProperties(HttpStatus.SC_OK), "getetag", Namespace.getNamespace("DAV:"));
|
||||||
|
return event;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int dumpIndex;
|
||||||
|
private String defaultSound = "Basso";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Replace iCal4 (Snow Leopard) principal paths with mailto expression
|
||||||
|
*
|
||||||
|
* @param value attendee value or ics line
|
||||||
|
* @return fixed value
|
||||||
|
*/
|
||||||
|
protected String replaceIcal4Principal(String value) {
|
||||||
|
if (value.contains("/principals/__uuids__/")) {
|
||||||
|
return value.replaceAll("/principals/__uuids__/([^/]*)__AT__([^/]*)/", "mailto:$1@$2");
|
||||||
|
} else {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Event result object to hold HTTP status and event etag from an event creation/update.
|
||||||
|
*/
|
||||||
|
public static class EventResult {
|
||||||
|
/**
|
||||||
|
* HTTP status
|
||||||
|
*/
|
||||||
|
public int status;
|
||||||
|
/**
|
||||||
|
* Event etag from response HTTP header
|
||||||
|
*/
|
||||||
|
public String etag;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build and send the MIME message for the provided ICS event.
|
||||||
|
*
|
||||||
|
* @param icsBody event in iCalendar format
|
||||||
|
* @return HTTP status
|
||||||
|
* @throws IOException on error
|
||||||
|
*/
|
||||||
|
public int sendEvent(String icsBody) throws IOException {
|
||||||
|
String messageUrl = draftsUrl + '/' + UUID.randomUUID().toString() + ".EML";
|
||||||
|
int status = internalCreateOrUpdateEvent(messageUrl, "urn:content-classes:calendarmessage", icsBody, null, null).status;
|
||||||
|
if (status != HttpStatus.SC_CREATED) {
|
||||||
|
return status;
|
||||||
|
} else {
|
||||||
|
MoveMethod method = new MoveMethod(URIUtil.encodePath(messageUrl), URIUtil.encodePath(sendmsgUrl), true);
|
||||||
|
status = DavGatewayHttpClientFacade.executeHttpMethod(httpClient, method);
|
||||||
|
if (status != HttpStatus.SC_OK) {
|
||||||
|
throw DavGatewayHttpClientFacade.buildHttpException(method);
|
||||||
|
}
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create or update event on the Exchange server
|
||||||
|
*
|
||||||
|
* @param folderPath Exchange folder path
|
||||||
|
* @param eventName event name
|
||||||
|
* @param icsBody event body in iCalendar format
|
||||||
|
* @param etag previous event etag to detect concurrent updates
|
||||||
|
* @param noneMatch if-none-match header value
|
||||||
|
* @return HTTP response event result (status and etag)
|
||||||
|
* @throws IOException on error
|
||||||
|
*/
|
||||||
|
public EventResult createOrUpdateEvent(String folderPath, String eventName, String icsBody, String etag, String noneMatch) throws IOException {
|
||||||
|
String messageUrl = folderPath + '/' + eventName;
|
||||||
|
return internalCreateOrUpdateEvent(messageUrl, "urn:content-classes:appointment", icsBody, etag, noneMatch);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected EventResult internalCreateOrUpdateEvent(String messageUrl, String contentClass, String icsBody, String etag, String noneMatch) throws IOException {
|
||||||
|
Event event = new Event();
|
||||||
|
event.contentClass = contentClass;
|
||||||
|
event.icsBody = icsBody;
|
||||||
|
event.href = messageUrl;
|
||||||
|
event.etag = etag;
|
||||||
|
event.noneMatch = noneMatch;
|
||||||
|
return event.createOrUpdate();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
Loading…
Reference in New Issue
Block a user