Caldav: merged contribution from Dan Foody,
- iCal4 (OSX Snow Leopard fixes - convert sound to display alarms and back - remove additional organizer attendee line - remove RSVP=TRUE if PARTSTAT is not NEEDS-ACTION - add dump ICS logging feature - add a text/plain body to calendar messages - create a subject for calendar messages - fixed some encoding issues in Dan's code git-svn-id: http://svn.code.sf.net/p/davmail/code/trunk@688 3d1905a2-6b24-0410-a738-b14d5a86fcbd
This commit is contained in:
parent
e0ce94ab7e
commit
d68eb2d9ff
|
@ -45,6 +45,7 @@ import javax.mail.MessagingException;
|
||||||
import javax.mail.internet.MimeMessage;
|
import javax.mail.internet.MimeMessage;
|
||||||
import javax.mail.internet.MimeMultipart;
|
import javax.mail.internet.MimeMultipart;
|
||||||
import javax.mail.internet.MimePart;
|
import javax.mail.internet.MimePart;
|
||||||
|
import javax.mail.internet.MimeUtility;
|
||||||
import java.io.*;
|
import java.io.*;
|
||||||
import java.net.HttpURLConnection;
|
import java.net.HttpURLConnection;
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
|
@ -1620,6 +1621,41 @@ public class ExchangeSession {
|
||||||
return event;
|
return event;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static int dumpIndex = 1;
|
||||||
|
private static String defaultSound = "Basso";
|
||||||
|
|
||||||
|
protected void dumpICS(String icsBody, boolean fromServer, boolean after) {
|
||||||
|
// additional setting to activate ICS dump (not available in GUI)
|
||||||
|
if (Settings.getBooleanProperty("davmail.dumpICS")) {
|
||||||
|
StringBuilder filePath = new StringBuilder();
|
||||||
|
filePath.append(Settings.getLogFileDirectory()).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 {
|
||||||
// first pass : detect
|
// first pass : detect
|
||||||
class AllDayState {
|
class AllDayState {
|
||||||
|
@ -1627,15 +1663,21 @@ public class ExchangeSession {
|
||||||
boolean hasCdoAllDay;
|
boolean hasCdoAllDay;
|
||||||
boolean isCdoAllDay;
|
boolean isCdoAllDay;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dumpIndex++;
|
||||||
|
dumpICS(icsBody, fromServer, false);
|
||||||
|
|
||||||
// Convert event class from and to iCal
|
// Convert event class from and to iCal
|
||||||
// See https://trac.calendarserver.org/browser/CalendarServer/trunk/doc/Extensions/caldav-privateevents.txt
|
// See https://trac.calendarserver.org/browser/CalendarServer/trunk/doc/Extensions/caldav-privateevents.txt
|
||||||
boolean isAppleiCal = false;
|
boolean isAppleiCal = false;
|
||||||
boolean hasOrganizer = false;
|
|
||||||
boolean hasAttendee = false;
|
boolean hasAttendee = false;
|
||||||
boolean hasCdoBusyStatus = false;
|
boolean hasCdoBusyStatus = false;
|
||||||
String transp = null;
|
String transp = null;
|
||||||
String validTimezoneId = null;
|
String validTimezoneId = null;
|
||||||
String eventClass = null;
|
String eventClass = null;
|
||||||
|
String organizer = null;
|
||||||
|
String action = null;
|
||||||
|
boolean sound = false;
|
||||||
|
|
||||||
List<AllDayState> allDayStates = new ArrayList<AllDayState>();
|
List<AllDayState> allDayStates = new ArrayList<AllDayState>();
|
||||||
AllDayState currentAllDayState = new AllDayState();
|
AllDayState currentAllDayState = new AllDayState();
|
||||||
|
@ -1657,13 +1699,27 @@ public class ExchangeSession {
|
||||||
allDayStates.add(currentAllDayState);
|
allDayStates.add(currentAllDayState);
|
||||||
currentAllDayState = new AllDayState();
|
currentAllDayState = new AllDayState();
|
||||||
} else if ("PRODID".equals(key) && line.contains("iCal")) {
|
} else if ("PRODID".equals(key) && line.contains("iCal")) {
|
||||||
|
// detect iCal created events
|
||||||
isAppleiCal = true;
|
isAppleiCal = true;
|
||||||
} else if (isAppleiCal && "X-CALENDARSERVER-ACCESS".equals(key)) {
|
} else if (isAppleiCal && "X-CALENDARSERVER-ACCESS".equals(key)) {
|
||||||
eventClass = value;
|
eventClass = value;
|
||||||
} else if (!isAppleiCal && "CLASS".equals(key)) {
|
} else if (!isAppleiCal && "CLASS".equals(key)) {
|
||||||
eventClass = value;
|
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;
|
||||||
|
// Set the default sound to whatever this event contains
|
||||||
|
// (under assumption that the user has the same sound set
|
||||||
|
// for all events)
|
||||||
|
defaultSound = value;
|
||||||
} else if (key.startsWith("ORGANIZER")) {
|
} else if (key.startsWith("ORGANIZER")) {
|
||||||
hasOrganizer = true;
|
if (value.startsWith("MAILTO:")) {
|
||||||
|
organizer = value.substring(7);
|
||||||
|
} else {
|
||||||
|
organizer = value;
|
||||||
|
}
|
||||||
} else if (key.startsWith("ATTENDEE")) {
|
} else if (key.startsWith("ATTENDEE")) {
|
||||||
hasAttendee = true;
|
hasAttendee = true;
|
||||||
} else if ("TRANSP".equals(key)) {
|
} else if ("TRANSP".equals(key)) {
|
||||||
|
@ -1742,6 +1798,54 @@ public class ExchangeSession {
|
||||||
// Apple iCal doesn't understand this key, and it's entourage
|
// Apple iCal doesn't understand this key, and it's entourage
|
||||||
// specific (i.e. not needed by any caldav client): strip it out
|
// specific (i.e. not needed by any caldav client): strip it out
|
||||||
continue;
|
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)) {
|
||||||
|
// Use the default iCal alarm action instead
|
||||||
|
// of the alarm Action exchange (and blackberry) understand.
|
||||||
|
// This is a bit of a hack because we don't know what type
|
||||||
|
// of alarm an iCal user really wants - but we know what the
|
||||||
|
// default is, and can setup the default action type
|
||||||
|
|
||||||
|
result.writeLine("ACTION:AUDIO");
|
||||||
|
|
||||||
|
if (!sound) {
|
||||||
|
// Add default sound into the audio alarm
|
||||||
|
result.writeLine("ATTACH;VALUE=URI:" + defaultSound);
|
||||||
|
}
|
||||||
|
|
||||||
|
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:")) {
|
} else if (line.startsWith("CLASS:")) {
|
||||||
if (isAppleiCal) {
|
if (isAppleiCal) {
|
||||||
continue;
|
continue;
|
||||||
|
@ -1758,15 +1862,22 @@ public class ExchangeSession {
|
||||||
} else if (fromServer && line.startsWith("ORGANIZER") && !hasAttendee) {
|
} else if (fromServer && line.startsWith("ORGANIZER") && !hasAttendee) {
|
||||||
continue;
|
continue;
|
||||||
// add organizer line to all events created in Exchange for active sync
|
// add organizer line to all events created in Exchange for active sync
|
||||||
} else if (!fromServer && "END:VEVENT".equals(line) && !hasOrganizer) {
|
} else if (!fromServer && "END:VEVENT".equals(line) && organizer == null) {
|
||||||
result.writeLine("ORGANIZER:MAILTO:" + email);
|
result.writeLine("ORGANIZER:MAILTO:" + email);
|
||||||
|
} else if (organizer != null && line.startsWith("ATTENDEE") && line.contains(organizer)) {
|
||||||
|
// Ignore organizer as attendee
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
result.writeLine(line);
|
result.writeLine(line);
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
reader.close();
|
reader.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String resultString = result.toString();
|
||||||
|
dumpICS(resultString, fromServer, true);
|
||||||
|
|
||||||
return result.toString();
|
return result.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1872,6 +1983,44 @@ public class ExchangeSession {
|
||||||
return icsBody.substring(startIndex, endIndex);
|
return icsBody.substring(startIndex, endIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected String getICSValue(String icsBody, String prefix, String defval) throws IOException {
|
||||||
|
BufferedReader reader = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
reader = new ICSBufferedReader(new StringReader(icsBody));
|
||||||
|
String line;
|
||||||
|
while ((line = reader.readLine()) != null) {
|
||||||
|
if (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 {
|
||||||
|
String description = getICSValue(icsBody, "DESCRIPTION:", "");
|
||||||
|
|
||||||
|
if ("reminder".equalsIgnoreCase(description)) {
|
||||||
|
// Ignore this as a description text because
|
||||||
|
// it's likely part of the valarm segment
|
||||||
|
// (the default valarm description from outlook is "reminder")
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
return description;
|
||||||
|
}
|
||||||
|
|
||||||
static class Participants {
|
static class Participants {
|
||||||
String attendees;
|
String attendees;
|
||||||
String organizer;
|
String organizer;
|
||||||
|
@ -1971,6 +2120,9 @@ public class ExchangeSession {
|
||||||
recipients = participants.organizer;
|
recipients = participants.organizer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Make sure invites have a proper subject line
|
||||||
|
writer.write("Subject: " + MimeUtility.encodeText(getICSSummary(icsBody), "UTF-8", null) + "\r\n");
|
||||||
|
|
||||||
writer.write("To: ");
|
writer.write("To: ");
|
||||||
writer.write(recipients);
|
writer.write(recipients);
|
||||||
writer.write("\r\n");
|
writer.write("\r\n");
|
||||||
|
@ -1979,6 +2131,9 @@ public class ExchangeSession {
|
||||||
status = HttpStatus.SC_NO_CONTENT;
|
status = HttpStatus.SC_NO_CONTENT;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
// Make sure invites have a proper subject line
|
||||||
|
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(icsBody, false);
|
||||||
// storing appointment, full recipients header
|
// storing appointment, full recipients header
|
||||||
|
@ -2013,6 +2168,26 @@ public class ExchangeSession {
|
||||||
"\r\n" +
|
"\r\n" +
|
||||||
"------=_NextPart_");
|
"------=_NextPart_");
|
||||||
writer.write(uid);
|
writer.write(uid);
|
||||||
|
|
||||||
|
// Write a part of the message that contains the
|
||||||
|
// ICS description so that invites contain the description text
|
||||||
|
|
||||||
|
String description = getICSDescription(icsBody);
|
||||||
|
|
||||||
|
if (description.length() > 0) {
|
||||||
|
writer.write("\r\n" +
|
||||||
|
"Content-Type: text/plain;\r\n" +
|
||||||
|
"\tcharset=\"utf-8\"\r\n" +
|
||||||
|
"content-transfer-encoding: 8bit\r\n" +
|
||||||
|
"\r\n");
|
||||||
|
writer.flush();
|
||||||
|
baos.write(description.getBytes("UTF-8"));
|
||||||
|
writer.write("\r\n" +
|
||||||
|
"------=_NextPart_" +
|
||||||
|
uid);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
writer.write("\r\n" +
|
writer.write("\r\n" +
|
||||||
"Content-class: ");
|
"Content-class: ");
|
||||||
writer.write(contentClass);
|
writer.write(contentClass);
|
||||||
|
|
|
@ -225,3 +225,4 @@ UI_UNTRUSTED_CERTIFICATE=Server provided an untrusted certificate,\n you can cho
|
||||||
UI_UNTRUSTED_CERTIFICATE_HTML=<html><b>Server provided an untrusted certificate,<br> you can choose to accept or deny access</b></html>
|
UI_UNTRUSTED_CERTIFICATE_HTML=<html><b>Server provided an untrusted certificate,<br> you can choose to accept or deny access</b></html>
|
||||||
UI_VALID_FROM=Valid from
|
UI_VALID_FROM=Valid from
|
||||||
UI_VALID_UNTIL=Valid until
|
UI_VALID_UNTIL=Valid until
|
||||||
|
MEETING_REQUEST=Meeting request
|
|
@ -224,4 +224,5 @@ UI_CLIENT_CERTIFICATE=Certificat client
|
||||||
UI_LOG_FILE_PATH=Chemin du fichier de traces :
|
UI_LOG_FILE_PATH=Chemin du fichier de traces :
|
||||||
LOG_GATEWAY_INTERRUPTED=Arrêt de la passerelle DavMail en cours
|
LOG_GATEWAY_INTERRUPTED=Arrêt de la passerelle DavMail en cours
|
||||||
LOG_GATEWAY_STOP=Passerelle DavMail arrêtée
|
LOG_GATEWAY_STOP=Passerelle DavMail arrêtée
|
||||||
LOG_INVALID_TIMEZONE=Fuseau horaire invalide : {0}
|
LOG_INVALID_TIMEZONE=Fuseau horaire invalide : {0}
|
||||||
|
MEETING_REQUEST=Invitation
|
Loading…
Reference in New Issue