From 64e0688f0d099317c4fab1ba476438be8e7cd7c7 Mon Sep 17 00:00:00 2001 From: mguessan Date: Thu, 27 Nov 2008 00:56:28 +0000 Subject: [PATCH] Caldav (Calendar) support with free/busy and rename threads git-svn-id: http://svn.code.sf.net/p/davmail/code/trunk@179 3d1905a2-6b24-0410-a738-b14d5a86fcbd --- src/java/davmail/AbstractConnection.java | 54 +- src/java/davmail/AbstractServer.java | 6 +- src/java/davmail/DavGateway.java | 28 +- src/java/davmail/Settings.java | 1 + src/java/davmail/caldav/CaldavConnection.java | 462 ++++++++++++++++++ src/java/davmail/caldav/CaldavServer.java | 29 ++ .../davmail/exchange/ExchangeSession.java | 430 +++++++++++++++- src/java/davmail/imap/ImapConnection.java | 2 +- src/java/davmail/imap/ImapServer.java | 2 +- src/java/davmail/pop/PopConnection.java | 2 +- src/java/davmail/pop/PopServer.java | 2 +- src/java/davmail/smtp/SmtpConnection.java | 2 +- src/java/davmail/smtp/SmtpServer.java | 4 +- src/java/davmail/tray/SwtGatewayTray.java | 2 +- src/java/davmail/ui/SettingsFrame.java | 7 +- src/site/xdoc/gettingstarted.xml | 5 + 16 files changed, 998 insertions(+), 40 deletions(-) create mode 100644 src/java/davmail/caldav/CaldavConnection.java create mode 100644 src/java/davmail/caldav/CaldavServer.java diff --git a/src/java/davmail/AbstractConnection.java b/src/java/davmail/AbstractConnection.java index 1c787978..9b362687 100644 --- a/src/java/davmail/AbstractConnection.java +++ b/src/java/davmail/AbstractConnection.java @@ -2,6 +2,7 @@ package davmail; import davmail.exchange.ExchangeSession; import davmail.tray.DavGatewayTray; +import davmail.smtp.SmtpConnection; import java.io.BufferedReader; import java.io.IOException; @@ -9,6 +10,8 @@ import java.io.InputStreamReader; import java.io.OutputStream; import java.net.Socket; +import org.apache.commons.httpclient.util.Base64; + /** * Generic connection common to pop3 and smtp implementations */ @@ -26,11 +29,12 @@ public class AbstractConnection extends Thread { protected ExchangeSession session; // Initialize the streams and start the thread - public AbstractConnection(Socket clientSocket) { + public AbstractConnection(String name, Socket clientSocket) { + super(name+"-"+clientSocket.getPort()); this.client = clientSocket; try { //noinspection IOResourceOpenedButNotSafelyClosed - in = new BufferedReader(new InputStreamReader(client.getInputStream())); + in = new BufferedReader(new InputStreamReader(client.getInputStream(), "UTF-8")); os = client.getOutputStream(); } catch (IOException e) { close(); @@ -38,10 +42,21 @@ public class AbstractConnection extends Thread { } } + /** + * Send message to client followed by CRLF. + * @param message message + * @throws IOException on error + */ public void sendClient(String message) throws IOException { sendClient(null, message); } + /** + * Send prefix and message to client followed by CRLF. + * @param prefix prefix + * @param message message + * @throws IOException on error + */ public void sendClient(String prefix, String message) throws IOException { StringBuffer logBuffer = new StringBuffer("> "); if (prefix != null) { @@ -56,6 +71,19 @@ public class AbstractConnection extends Thread { os.flush(); } + /** + * Send only bytes to client. + * @param messageBytes content + * @throws IOException on error + */ + public void sendClient(byte[] messageBytes) throws IOException { + StringBuffer logBuffer = new StringBuffer("> "); + logBuffer.append(new String(messageBytes)); + DavGatewayTray.debug(logBuffer.toString()); + os.write(messageBytes); + os.flush(); + } + /** * Read a line from the client connection. * Log message to stdout @@ -65,10 +93,17 @@ public class AbstractConnection extends Thread { */ public String readClient() throws IOException { String line = in.readLine(); - if (line != null && !line.startsWith("PASS")) { - DavGatewayTray.debug("< " + line); - } else { - DavGatewayTray.debug("< PASS ********"); + // TODO : add basic authorization check + if (line != null) { + if (line.startsWith("PASS")) { + DavGatewayTray.debug("< PASS ********"); + } else if (state == SmtpConnection.PASSWORD){ + DavGatewayTray.debug("< ********"); + } else if (line.startsWith("Authorization:")){ + DavGatewayTray.debug("< Authorization: ********"); + } else { + DavGatewayTray.debug("< " + line); + } } DavGatewayTray.switchIcon(); return line; @@ -106,4 +141,11 @@ public class AbstractConnection extends Thread { } } + protected String base64Encode(String value) { + return new String(Base64.encode(value.getBytes())); + } + + protected String base64Decode(String value) throws IOException { + return new String(Base64.decode(value.getBytes())); + } } diff --git a/src/java/davmail/AbstractServer.java b/src/java/davmail/AbstractServer.java index c12c3f73..9f37eb09 100644 --- a/src/java/davmail/AbstractServer.java +++ b/src/java/davmail/AbstractServer.java @@ -18,11 +18,13 @@ public abstract class AbstractServer extends Thread { * Create a ServerSocket to listen for connections. * Start the thread. * + * @param name thread name * @param port tcp socket chosen port * @param defaultPort tcp socket default port * @throws java.io.IOException unable to create server socket */ - public AbstractServer(int port, int defaultPort) throws IOException { + public AbstractServer(String name, int port, int defaultPort) throws IOException { + super(name); if (port == 0) { this.port = defaultPort; } else { @@ -52,6 +54,8 @@ public abstract class AbstractServer extends Thread { //noinspection InfiniteLoopStatement while (true) { clientSocket = serverSocket.accept(); + // set default timeout to 5 minutes + clientSocket.setSoTimeout(300000); DavGatewayTray.debug("Connection from " + clientSocket.getInetAddress() + " on port " + port); // only accept localhost connections for security reasons if (Settings.getBooleanProperty("davmail.allowRemote") || diff --git a/src/java/davmail/DavGateway.java b/src/java/davmail/DavGateway.java index d78985f0..34b602f8 100644 --- a/src/java/davmail/DavGateway.java +++ b/src/java/davmail/DavGateway.java @@ -1,19 +1,19 @@ package davmail; -import davmail.http.DavGatewaySSLProtocolSocketFactory; +import davmail.caldav.CaldavServer; import davmail.http.DavGatewayHttpClientFacade; +import davmail.http.DavGatewaySSLProtocolSocketFactory; import davmail.pop.PopServer; import davmail.smtp.SmtpServer; import davmail.tray.DavGatewayTray; - -import java.io.IOException; -import java.io.BufferedReader; -import java.io.InputStreamReader; - import org.apache.commons.httpclient.HttpClient; import org.apache.commons.httpclient.HttpStatus; import org.apache.commons.httpclient.methods.GetMethod; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; + /** * DavGateway main class */ @@ -23,6 +23,7 @@ public class DavGateway { private static SmtpServer smtpServer; private static PopServer popServer; + private static CaldavServer caldavServer; /** * Start the gateway, listen on spécified smtp and pop3 ports @@ -52,14 +53,21 @@ public class DavGateway { if (popPort == 0) { popPort = PopServer.DEFAULT_PORT; } + int caldavPort = Settings.getIntProperty("davmail.caldavPort"); + if (caldavPort == 0) { + caldavPort = CaldavServer.DEFAULT_PORT; + } try { smtpServer = new SmtpServer(smtpPort); popServer = new PopServer(popPort); + caldavServer = new CaldavServer(caldavPort); smtpServer.start(); popServer.start(); + caldavServer.start(); String message = "DavMail gateway listening on SMTP port " + smtpPort + + ", Caldav port " + caldavPort + " and POP port " + popPort; String releasedVersion = getReleasedVersion(); String currentVersion = getCurrentVersion(); @@ -93,6 +101,14 @@ public class DavGateway { DavGatewayTray.warn("Exception waiting for listener to die", e); } } + if (caldavServer != null) { + caldavServer.close(); + try { + caldavServer.join(); + } catch (InterruptedException e) { + DavGatewayTray.warn("Exception waiting for listener to die", e); + } + } } public static String getCurrentVersion() { diff --git a/src/java/davmail/Settings.java b/src/java/davmail/Settings.java index 1c3a721e..ecb95abb 100644 --- a/src/java/davmail/Settings.java +++ b/src/java/davmail/Settings.java @@ -49,6 +49,7 @@ public class Settings { SETTINGS.put("davmail.url", "http://exchangeServer/exchange/"); SETTINGS.put("davmail.popPort", "1110"); SETTINGS.put("davmail.smtpPort", "1025"); + SETTINGS.put("davmail.caldavPort", "1080"); SETTINGS.put("davmail.keepDelay", "30"); SETTINGS.put("davmail.allowRemote", "false"); SETTINGS.put("davmail.bindAddress", ""); diff --git a/src/java/davmail/caldav/CaldavConnection.java b/src/java/davmail/caldav/CaldavConnection.java new file mode 100644 index 00000000..ad079fcd --- /dev/null +++ b/src/java/davmail/caldav/CaldavConnection.java @@ -0,0 +1,462 @@ +package davmail.caldav; + +import davmail.AbstractConnection; +import davmail.Settings; +import davmail.exchange.ExchangeSession; +import davmail.exchange.ExchangeSessionFactory; +import davmail.exchange.NetworkDownException; +import davmail.tray.DavGatewayTray; +import org.apache.commons.httpclient.HttpException; +import org.apache.commons.httpclient.HttpStatus; +import org.apache.commons.httpclient.util.URIUtil; + +import javax.xml.stream.*; +import java.io.IOException; +import java.io.StringReader; +import java.io.BufferedReader; +import java.net.Socket; +import java.net.SocketTimeoutException; +import java.util.*; +import java.text.SimpleDateFormat; + +/** + * Handle a caldav connection. + */ +public class CaldavConnection extends AbstractConnection { + protected boolean closed = false; + + // Initialize the streams and start the thread + public CaldavConnection(Socket clientSocket) { + super("CaldavConnection", clientSocket); + } + + protected Map parseHeaders() throws IOException { + HashMap headers = new HashMap(); + String line; + while ((line = readClient()) != null && line.length() > 0) { + int index = line.indexOf(':'); + if (index <= 0) { + throw new IOException("Invalid header: " + line); + } + headers.put(line.substring(0, index).toLowerCase(), line.substring(index + 1).trim()); + } + return headers; + } + + protected String getContent(String contentLength) throws IOException { + if (contentLength == null || contentLength.length() == 0) { + return null; + } else { + int size; + try { + size = Integer.parseInt(contentLength); + } catch (NumberFormatException e) { + throw new IOException("Invalid content length: " + contentLength); + } + char[] buffer = new char[size]; + int actualSize = in.read(buffer); + if (actualSize < 0) { + throw new IOException("End of stream reached reading content"); + } + String result = new String(buffer, 0, actualSize); + DavGatewayTray.debug("< " + result); + return result; + } + } + + protected void setSocketTimeout(String keepAliveValue) throws IOException { + if (keepAliveValue != null || keepAliveValue.length() > 0) { + int keepAlive; + try { + keepAlive = Integer.parseInt(keepAliveValue); + } catch (NumberFormatException e) { + throw new IOException("Invalid Keep-Alive: " + keepAliveValue); + } + if (keepAlive > 300) { + keepAlive = 300; + } + client.setSoTimeout(keepAlive * 1000); + DavGatewayTray.debug("Set socket timeout to " + keepAlive + " seconds"); + } + } + + public void run() { + String line; + StringTokenizer tokens; + + try { + while (!closed) { + line = readClient(); + // unable to read line, connection closed ? + if (line == null) { + break; + } + tokens = new StringTokenizer(line); + if (tokens.hasMoreTokens()) { + String command = tokens.nextToken(); + Map headers = parseHeaders(); + if (tokens.hasMoreTokens()) { + String path = tokens.nextToken(); + String content = getContent(headers.get("content-length")); + setSocketTimeout(headers.get("keep-alive")); + if ("OPTIONS".equals(command)) { + sendOptions(); + } else if (!headers.containsKey("authorization")) { + sendUnauthorized(); + } else { + decodeCredentials(headers.get("authorization")); + // authenticate only once + if (session == null) { + session = ExchangeSessionFactory.getInstance(userName, password); + } + handleRequest(command, path, headers, content); + } + } else { + sendErr(HttpStatus.SC_NOT_IMPLEMENTED, "Invalid URI"); + } + } + + os.flush(); + } + } catch (SocketTimeoutException e) { + DavGatewayTray.debug("Closing connection on timeout"); + } catch (IOException e) { + DavGatewayTray.error(e); + try { + sendErr(HttpStatus.SC_INTERNAL_SERVER_ERROR, e); + } catch (IOException e2) { + DavGatewayTray.debug("Exception sending error to client", e2); + } + } finally { + close(); + } + DavGatewayTray.resetIcon(); + } + + protected int getDepth(Map headers) { + int result = 0; + String depthValue = headers.get("depth"); + if (depthValue != null) { + try { + result = Integer.valueOf(depthValue); + } catch (NumberFormatException e) { + DavGatewayTray.warn("Invalid depth value: " + depthValue); + } + } + return result; + } + + public void handleRequest(String command, String path, Map headers, String body) throws IOException { + int depth = getDepth(headers); + if ("OPTIONS".equals(command)) { + sendOptions(); + } else if ("PROPFIND".equals(command) && "/user/".equals(path)) { + StringBuilder buffer = new StringBuilder(); + buffer.append("\n"); + buffer.append("\n"); + buffer.append(" \n"); + buffer.append(" /user\n"); + buffer.append(" \n"); + buffer.append(" \n"); + buffer.append(" \n"); + buffer.append(" /calendar\n"); + buffer.append(" "); + + buffer.append(" \n"); + // TODO + buffer.append(" mailto:" + session.getEmail() + "\n"); + buffer.append(" "); + + buffer.append(" \n"); + buffer.append(" /inbox\n"); + buffer.append(" "); + + buffer.append(" \n"); + buffer.append(" /outbox\n"); + buffer.append(" "); + + buffer.append(" \n"); + buffer.append(" HTTP/1.1 200 OK\n"); + buffer.append(" \n"); + buffer.append(" \n"); + buffer.append("\n"); + sendHttpResponse(HttpStatus.SC_MULTI_STATUS, null, "text/xml;charset=UTF-8", buffer.toString(), true); + } else if ("PROPFIND".equals(command) && "/calendar/".equals(path)) { + if (depth != 0 || body == null) { + throw new IOException("Unsupported operation: " + command + " " + path); + } + try { + StringBuilder buffer = new StringBuilder(); + buffer.append("\n"); + buffer.append("\n"); + buffer.append(" \n"); + buffer.append(" /calendar\n"); + buffer.append(" \n"); + buffer.append(" \n"); + // TODO : parse request + if (body.indexOf("resourcetype") >= 0) { + buffer.append(" \n"); + buffer.append(" \n"); + buffer.append(" \n"); + buffer.append(" \n"); + } + if (body.indexOf("owner") >= 0) { + buffer.append(" \n"); + buffer.append(" /user\n"); + buffer.append(" \n"); + } + if (body.indexOf("getctag") >= 0) { + buffer.append(" ") + .append(base64Encode(session.getCalendarEtag())) + .append("\n"); + } + buffer.append(" \n"); + buffer.append(" HTTP/1.1 200 OK\n"); + buffer.append(" \n"); + buffer.append(" \n"); + buffer.append("\n"); + + HashMap responseHeaders = new HashMap(); + sendHttpResponse(HttpStatus.SC_MULTI_STATUS, responseHeaders, "text/xml;charset=UTF-8", buffer.toString(), true); + + } catch (IOException e) { + sendUnauthorized(); + } + + } else if ("REPORT".equals(command)) { + if (!"/calendar/".equals(path) || depth != 1 || body == null) { + throw new IOException("Unsupported operation: " + command + " " + path); + } + HashSet properties = new HashSet(); + // TODO : parse body + if (body.indexOf("D:getetag") >= 0) { + properties.add("getetag"); + } + if (body.indexOf("calendar-data") >= 0) { + properties.add("calendar-data"); + } + List events; + List notFound = new ArrayList(); + if (body.indexOf("calendar-multiget") >= 0) { + events = new ArrayList(); + try { + XMLInputFactory inputFactory = XMLInputFactory.newInstance(); + inputFactory.setProperty(XMLInputFactory.IS_COALESCING, Boolean.TRUE); + inputFactory.setProperty(XMLInputFactory.IS_REPLACING_ENTITY_REFERENCES, Boolean.TRUE); + + XMLStreamReader reader = inputFactory.createXMLStreamReader(new StringReader(body)); + boolean inHref = false; + while (reader.hasNext()) { + int event = reader.next(); + if (event == XMLStreamConstants.START_ELEMENT && "href".equals(reader.getLocalName())) { + inHref = true; + } else if (event == XMLStreamConstants.CHARACTERS && inHref) { + try { + events.add(session.getEvent(URIUtil.decode(reader.getText().substring("/calendar/".length())))); + } catch (HttpException e) { + notFound.add(reader.getText().substring("/calendar/".length())); + } + inHref = false; + } + } + } catch (XMLStreamException e) { + throw new IOException(e.getMessage()); + } + } else { + events = session.getAllEvents(); + } + + StringBuilder buffer = new StringBuilder(); + buffer.append("\n" + + "\n"); + for (ExchangeSession.Event event : events) { + + String eventPath = event.getPath().replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">"); + buffer.append("\n"); + buffer.append(" /calendar").append(eventPath).append("\n"); + buffer.append(" \n"); + buffer.append(" \n"); + if (properties.contains("calendar-data")) { + String ics = event.getICS(); + if (ics != null && ics.length() > 0) { + ics = ics.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">"); + buffer.append(" "); + buffer.append(ics); + buffer.append("\n"); + } + } + if (properties.contains("getetag")) { + buffer.append(" ").append(event.getEtag()).append("\n"); + } + buffer.append(" \n"); + buffer.append(" HTTP/1.1 200 OK\n"); + buffer.append(" \n"); + if (notFound.size() > 0) { + buffer.append(" \n"); + for (String href : notFound) { + buffer.append(" ").append(href).append("\n"); + } + buffer.append(" HTTP/1.1 404 Not Found\n"); + buffer.append(" \n"); + } + buffer.append(" ").append((char) 13).append((char) 10); + + } + buffer.append(""); + + // TODO : remove + sendHttpResponse(HttpStatus.SC_MULTI_STATUS, null, "text/xml;charset=UTF-8", buffer.toString(), true); + } else if ("PUT".equals(command) && path.startsWith("/calendar/")) { + String etag = headers.get("if-match"); + int status = session.createOrUpdateEvent(path.substring("/calendar/".length()), body, etag); + sendHttpResponse(status, true); + + } else if ("POST".equals(command) && path.startsWith("/outbox")) { + Map valueMap = new HashMap(); + Map keyMap = new HashMap(); + BufferedReader reader = new BufferedReader(new StringReader(body)); + String line; + String key = null; + while ((line = reader.readLine()) != null) { + if (line.startsWith(" ") & "ATTENDEE".equals(key)) { + valueMap.put(key, valueMap.get(key)+line.substring(1)); + } else { + int index = line.indexOf(':'); + if (index <= 0) { + throw new IOException("Invalid request: " + body); + } + String fullkey = line.substring(0, index); + String value = line.substring(index+1); + int semicolonIndex = fullkey.indexOf(";"); + if (semicolonIndex > 0) { + key = fullkey.substring(0, semicolonIndex); + } else { + key = fullkey; + } + valueMap.put(key, value); + keyMap.put(key, fullkey); + } + } + String response = "\n" + + " \n" + + " \n" + + " \n" + + " "+valueMap.get("ATTENDEE")+"\n" + + " \n" + + " 2.0;Success\n" + + " BEGIN:VCALENDAR\n" + + "VERSION:2.0\n" + + "PRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN\n" + + "METHOD:REPLY\n" + + "BEGIN:VFREEBUSY\n" + + "DTSTAMP:" + valueMap.get("DTSTAMP") + "\n" + + "ORGANIZER:" + valueMap.get("ORGANIZER") + "\n" + + "DTSTART:" + valueMap.get("DTSTART") + "\n" + + "DTEND:" + valueMap.get("DTEND") + "\n" + + "UID:" + valueMap.get("UID") + "\n" + + keyMap.get("ATTENDEE")+";" + valueMap.get("ATTENDEE") + "\n" + + "FREEBUSY;FBTYPE=BUSY-UNAVAILABLE:" + session.getFreebusy(valueMap) + "\n" + + "END:VFREEBUSY\n" + + "END:VCALENDAR" + + "\n" + + " \n" + + " "; + sendHttpResponse(HttpStatus.SC_OK, null, "text/xml;charset=UTF-8", response, true); + + } else if ("DELETE".equals(command) && path.startsWith("/calendar/")) { + int status = session.deleteEvent(path.substring("/calendar/".length())); + sendHttpResponse(status, true); + } else { + sendErr(HttpStatus.SC_BAD_REQUEST, "Unsupported command: " + command); + } + + } + + + public void sendErr(int status, Exception e) throws IOException { + String message = e.getMessage(); + if (message == null) { + message = e.toString(); + } + sendErr(status, message); + } + + public void sendErr(int status, String message) throws IOException { + sendHttpResponse(status, null, "text/plain;charset=UTF-8", message, false); + } + + public void sendOptions() throws IOException { + HashMap headers = new HashMap(); + headers.put("Allow", "OPTIONS, GET, PROPFIND, PUT, POST"); + headers.put("DAV", "1, 2, 3, access-control, calendar-access, ticket, calendar-schedule"); + sendHttpResponse(HttpStatus.SC_OK, headers, null, null, true); + } + + public void sendUnauthorized() throws IOException { + HashMap headers = new HashMap(); + headers.put("WWW-Authenticate", "Basic realm=\"" + Settings.getProperty("davmail.url") + "\""); + sendHttpResponse(HttpStatus.SC_UNAUTHORIZED, headers, null, null, true); + } + + public void sendHttpResponse(int status, boolean keepAlive) throws IOException { + sendHttpResponse(status, null, null, null, keepAlive); + } + + public void sendHttpResponse(int status, Map headers, String contentType, String content, boolean keepAlive) throws IOException { + sendClient("HTTP/1.1 " + status + " " + HttpStatus.getStatusText(status)); + sendClient("Server: DavMail Gateway"); + SimpleDateFormat formatter = new java.text.SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss Z", Locale.ENGLISH); + sendClient("Date: " + formatter.format(new java.util.Date())); + if (headers != null) { + for (String header : headers.keySet()) { + sendClient(header + ": " + headers.get(header)); + } + } + if (contentType != null) { + sendClient("Content-Type: " + contentType); + } + sendClient("Connection: " + (keepAlive ? "keep-alive" : "close")); + closed = !keepAlive; + if (content != null && content.length() > 0) { + sendClient("Content-Length: " + content.getBytes("UTF-8").length); + } else { + sendClient("Content-Length: 0"); + } + sendClient(""); + if (content != null && content.length() > 0) { + sendClient(content.getBytes("UTF-8")); + } + } + + /** + * Decode HTTP credentials + * + * @param authorization http authorization header value + * @throws java.io.IOException if invalid credentials + */ + protected void decodeCredentials(String authorization) throws IOException { + int index = authorization.indexOf(' '); + if (index > 0) { + String mode = authorization.substring(0, index).toLowerCase(); + if (!"basic".equals(mode)) { + throw new IOException("Unsupported authorization mode: " + mode); + } + String encodedCredentials = authorization.substring(index + 1); + String decodedCredentials = base64Decode(encodedCredentials); + index = decodedCredentials.indexOf(':'); + if (index > 0) { + userName = decodedCredentials.substring(0, index); + password = decodedCredentials.substring(index + 1); + } else { + throw new IOException("Invalid credentials"); + } + } else { + throw new IOException("Invalid credentials"); + } + + } + +} + diff --git a/src/java/davmail/caldav/CaldavServer.java b/src/java/davmail/caldav/CaldavServer.java new file mode 100644 index 00000000..e9c55866 --- /dev/null +++ b/src/java/davmail/caldav/CaldavServer.java @@ -0,0 +1,29 @@ +package davmail.caldav; + +import davmail.AbstractServer; +import davmail.AbstractConnection; +import davmail.pop.PopConnection; + +import java.io.IOException; +import java.net.Socket; + +/** + * Calendar server, handle HTTP Caldav requests. + */ +public class CaldavServer extends AbstractServer { + public static final int DEFAULT_PORT = 80; + + /** + * Create a ServerSocket to listen for connections. + * Start the thread. + * @param port pop listen port, 80 if not defined (0) + * @throws java.io.IOException on error + */ + public CaldavServer(int port) throws IOException { + super("CaldavServer", port, CaldavServer.DEFAULT_PORT); + } + + public AbstractConnection createConnectionHandler(Socket clientSocket) { + return new CaldavConnection(clientSocket); + } +} \ No newline at end of file diff --git a/src/java/davmail/exchange/ExchangeSession.java b/src/java/davmail/exchange/ExchangeSession.java index b584554b..477970c3 100644 --- a/src/java/davmail/exchange/ExchangeSession.java +++ b/src/java/davmail/exchange/ExchangeSession.java @@ -1,6 +1,7 @@ package davmail.exchange; import davmail.Settings; +import davmail.tray.DavGatewayTray; import davmail.http.DavGatewayHttpClientFacade; import org.apache.commons.httpclient.Header; import org.apache.commons.httpclient.HttpClient; @@ -18,24 +19,20 @@ import org.apache.log4j.Logger; import org.apache.webdav.lib.Property; import org.apache.webdav.lib.ResponseEntity; import org.apache.webdav.lib.WebdavResource; +import org.apache.webdav.lib.methods.SearchMethod; +import org.apache.webdav.lib.methods.PropFindMethod; import javax.mail.internet.MimeUtility; -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStreamReader; -import java.io.OutputStream; -import java.io.OutputStreamWriter; +import javax.xml.stream.XMLStreamReader; +import javax.xml.stream.XMLInputFactory; +import javax.xml.stream.XMLStreamException; +import javax.xml.stream.XMLStreamConstants; +import java.io.*; import java.net.HttpURLConnection; import java.net.URL; import java.text.ParseException; import java.text.SimpleDateFormat; -import java.util.ArrayList; -import java.util.Calendar; -import java.util.Date; -import java.util.Enumeration; -import java.util.List; -import java.util.SimpleTimeZone; -import java.util.Vector; +import java.util.*; /** * Exchange session through Outlook Web Access (DAV) @@ -75,6 +72,7 @@ public class ExchangeSession { private String deleteditemsUrl; private String sendmsgUrl; private String draftsUrl; + private String calendarUrl; /** * Base user mailboxes path (used to select folder) @@ -92,6 +90,7 @@ public class ExchangeSession { // each session dateParser = new java.text.SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS"); dateParser.setTimeZone(new SimpleTimeZone(0, "GMT")); + LOGGER.debug("Session " + this + " created"); } /** @@ -229,6 +228,7 @@ public class ExchangeSession { } void login(String userName, String password) throws IOException { + LOGGER.debug("Session " + this + " login"); try { baseUrl = Settings.getProperty("davmail.url"); @@ -280,7 +280,12 @@ public class ExchangeSession { String queryString = method.getQueryString(); if (queryString != null && queryString.endsWith("reason=2")) { method.releaseConnection(); - throw new HttpException("Authentication failed: invalid user or password"); + if (userName != null && userName.contains("\\")) { + throw new HttpException("Authentication failed: invalid user or password"); + } else { + throw new HttpException("Authentication failed: invalid user or password, " + + "retry with domain\\user"); + } } mailPath = getMailPath(method); @@ -298,6 +303,7 @@ public class ExchangeSession { reqProps.add("urn:schemas:httpmail:deleteditems"); reqProps.add("urn:schemas:httpmail:sendmsg"); reqProps.add("urn:schemas:httpmail:drafts"); + reqProps.add("urn:schemas:httpmail:calendar"); Enumeration foldersEnum = wdr.propfindMethod(0, reqProps); if (!foldersEnum.hasMoreElements()) { @@ -326,6 +332,10 @@ public class ExchangeSession { draftsUrl = URIUtil.decode(inboxProp. getPropertyAsString()); } + if ("calendar".equals(inboxProp.getLocalName())) { + calendarUrl = URIUtil.decode(inboxProp. + getPropertyAsString()); + } } // set current folder to Inbox @@ -378,6 +388,7 @@ public class ExchangeSession { * @throws IOException if unable to close Webdav context */ public void close() throws IOException { + LOGGER.debug("Session " + this + " closed"); wdr.close(); } @@ -405,15 +416,20 @@ public class ExchangeSession { String messageUrl = URIUtil.encodePathQuery(folderUrl + "/" + subject + ".EML"); PutMethod putmethod = new PutMethod(messageUrl); + // TODO : test, bcc ? + putmethod.setRequestHeader("Translate", "f"); putmethod.setRequestHeader("Content-Type", "message/rfc822"); putmethod.setRequestBody(messageBody); + try { + int code = wdr.retrieveSessionInstance().executeMethod(putmethod); - int code = wdr.retrieveSessionInstance().executeMethod(putmethod); - - if (code == HttpURLConnection.HTTP_OK) { - LOGGER.warn("Overwritten message " + messageUrl); - } else if (code != HttpURLConnection.HTTP_CREATED) { - throw new IOException("Unable to create message " + code + " " + putmethod.getStatusLine()); + if (code == HttpURLConnection.HTTP_OK) { + LOGGER.warn("Overwritten message " + messageUrl); + } else if (code != HttpURLConnection.HTTP_CREATED) { + throw new IOException("Unable to create message " + code + " " + putmethod.getStatusLine()); + } + } finally { + putmethod.releaseConnection(); } } @@ -708,4 +724,380 @@ public class ExchangeSession { } + public WebdavResource getWebDavResource() throws IOException { + return wdr; + } + + public class Event { + protected String href; + protected String etag; + + public String getICS() throws IOException { + DavGatewayTray.debug("Get event: " + href); + StringBuilder buffer = new StringBuilder(); + GetMethod method = new GetMethod(URIUtil.encodePath(href)); + method.setRequestHeader("Content-Type", "text/xml; charset=utf-8"); + method.setRequestHeader("Translate", "f"); + BufferedReader eventReader = null; + try { + int status = wdr.retrieveSessionInstance().executeMethod(method); + if (status != HttpStatus.SC_OK) { + DavGatewayTray.warn("Unable to get event at " + href + " status: " + status); + } + eventReader = new BufferedReader(new InputStreamReader(method.getResponseBodyAsStream(), "UTF-8")); + String line; + boolean inbody = false; + while ((line = eventReader.readLine()) != null) { + if ("BEGIN:VCALENDAR".equals(line)) { + inbody = true; + } + if (inbody) { + buffer.append(line); + buffer.append((char) 13); + buffer.append((char) 10); + } + if ("END:VCALENDAR".equals(line)) { + inbody = false; + } + } + + } finally { + if (eventReader != null) { + try { + eventReader.close(); + } catch (IOException e) { + LOGGER.error("Error parsing event at " + method.getPath()); + } + } + method.releaseConnection(); + } + return buffer.toString(); + } + + public String getPath() throws URIException { + return href.substring(calendarUrl.length()); + } + + public String getEtag() { + return etag; + } + } + + public List getAllEvents() throws IOException { + List events = new ArrayList(); + String searchRequest = "\n" + + "\n" + + " Select \"DAV:getetag\"" + + " FROM Scope('SHALLOW TRAVERSAL OF \"" + calendarUrl + "\"')\n" + + " WHERE NOT \"urn:schemas:calendar:instancetype\" = 1\n" + + " AND \"DAV:contentclass\" = 'urn:content-classes:appointment'\n" + + " AND \"urn:schemas:calendar:dtstart\" > '2008/11/01 00:00:00'\n" + + " ORDER BY \"urn:schemas:calendar:dtstart\" ASC\n" + + " \n" + + ""; + SearchMethod searchMethod = new SearchMethod(calendarUrl, searchRequest); + try { + int status = wdr.retrieveSessionInstance().executeMethod(searchMethod); + // Also accept OK sent by buggy servers. + if (status != HttpStatus.SC_MULTI_STATUS + && status != HttpStatus.SC_OK) { + HttpException ex = new HttpException(); + ex.setReasonCode(status); + throw ex; + } + + Enumeration calendarEnum = searchMethod.getResponses(); + while (calendarEnum.hasMoreElements()) { + ResponseEntity calendarResponse = (ResponseEntity) calendarEnum. + nextElement(); + String href = calendarResponse.getHref(); + Event event = new Event(); + event.href = URIUtil.decode(href); + String contentclass = null; + Enumeration propertiesEnumeration = calendarResponse.getProperties(); + while (propertiesEnumeration.hasMoreElements()) { + Property property = (Property) propertiesEnumeration.nextElement(); + if ("getetag".equals(property.getLocalName())) { + event.etag = property.getPropertyAsString(); + } + /* + if ("contentclass".equals(property.getLocalName())) { + contentclass = property.getPropertyAsString(); + } + */ + } + // filter folder and non appointment objects + //if ("urn:content-classes:appointment".equals(contentclass)) { + events.add(event); + //} + } + } finally { + searchMethod.releaseConnection(); + } + return events; + } + + public Event getEvent(String path) throws IOException { + // TODO : refactor with getAllEvents + Event event = new Event(); + final Vector EVENT_REQUEST_PROPERTIES = new Vector(); + EVENT_REQUEST_PROPERTIES.add("DAV:getetag"); + + //wdr.setDebug(4); + Enumeration calendarEnum = wdr.propfindMethod(calendarUrl + "/" + path, 0, EVENT_REQUEST_PROPERTIES); + //wdr.setDebug(0); + if (!calendarEnum.hasMoreElements()) { + throw new IOException("Unable to get calendar event"); + } + ResponseEntity calendarResponse = (ResponseEntity) calendarEnum. + nextElement(); + String href = calendarResponse.getHref(); + event.href = URIUtil.decode(href); + Enumeration propertiesEnumeration = calendarResponse.getProperties(); + while (propertiesEnumeration.hasMoreElements()) { + Property property = (Property) propertiesEnumeration.nextElement(); + if ("getetag".equals(property.getLocalName())) { + event.etag = property.getPropertyAsString(); + } + } + return event; + } + + public int createOrUpdateEvent(String path, String icsBody, String etag) throws IOException { + String messageUrl = URIUtil.encodePathQuery(calendarUrl + "/" + URIUtil.decode(path)); + String uid = path.substring(0, path.lastIndexOf(".")); + PutMethod putmethod = new PutMethod(messageUrl); + putmethod.setRequestHeader("Translate", "f"); + putmethod.setRequestHeader("Overwrite", "f"); + if (etag != null) { + // TODO + putmethod.setRequestHeader("If-Match", etag); + } + putmethod.setRequestHeader("Content-Type", "message/rfc822"); + StringBuilder body = new StringBuilder(); + body.append("Content-Transfer-Encoding: 7bit\n" + + "Content-class: urn:content-classes:appointment\n" + + "MIME-Version: 1.0\n" + + "Content-Type: multipart/alternative;\n" + + "\tboundary=\"----=_NextPart_" + uid + "\"\n" + + "\n" + + "This is a multi-part message in MIME format.\n" + + "\n" + + "------=_NextPart_" + uid + "\n" + + "Content-class: urn:content-classes:appointment\n" + + "Content-Type: text/calendar;\n" + + "\tmethod=REQUEST;\n" + + "\tcharset=\"utf-8\"\n" + + "Content-Transfer-Encoding: 8bit\n\n"); + body.append(new String(icsBody.getBytes("UTF-8"), "ISO-8859-1")); + body.append("------=_NextPart_" + uid + "--\n"); + putmethod.setRequestBody(body.toString()); + int status; + try { + status = wdr.retrieveSessionInstance().executeMethod(putmethod); + + if (status == HttpURLConnection.HTTP_OK) { + LOGGER.warn("Overwritten event " + messageUrl); + } else if (status != HttpURLConnection.HTTP_CREATED) { + throw new IOException("Unable to create message " + status + " " + putmethod.getStatusLine()); + } + } finally { + putmethod.releaseConnection(); + } + return status; + } + + public int deleteEvent(String path) throws IOException { + //wdr.setDebug(4); + wdr.deleteMethod(calendarUrl + "/" + path); + //wdr.setDebug(0); + int status = wdr.getStatusCode(); + if (status == HttpStatus.SC_NOT_FOUND) { + status = HttpStatus.SC_OK; + } + return status; + } + + public String getCalendarEtag() throws IOException { + String etag = null; + //wdr.setDebug(4); + Enumeration calendarEnum = wdr.propfindMethod(calendarUrl, 0); + //wdr.setDebug(0); + if (!calendarEnum.hasMoreElements()) { + throw new IOException("Unable to get calendar object"); + } + while (calendarEnum.hasMoreElements()) { + ResponseEntity calendarResponse = (ResponseEntity) calendarEnum. + nextElement(); + Enumeration propertiesEnumeration = calendarResponse.getProperties(); + while (propertiesEnumeration.hasMoreElements()) { + Property property = (Property) propertiesEnumeration.nextElement(); + if ("http://schemas.microsoft.com/repl/".equals(property.getNamespaceURI()) + && "contenttag".equals(property.getLocalName())) { + etag = property.getPropertyAsString(); + } + } + } + if (etag == null) { + throw new IOException("Unable to get calendar etag"); + } + return etag; + } + + /** + * Get current Exchange user name + * + * @return user name + * @throws java.io.IOException on error + */ + public String getUserName() throws IOException { + int index = mailPath.lastIndexOf("/", mailPath.length() - 2); + if (index >= 0 && mailPath.endsWith("/")) { + return mailPath.substring(index + 1, mailPath.length() - 1); + } else { + throw new IOException("Invalid mail path: " + mailPath); + } + } + + /** + * Get current user email + * + * @return user email + * @throws java.io.IOException on error + */ + public String getEmail() throws IOException { + String email = null; + GetMethod getMethod = new GetMethod("/public/?Cmd=galfind&AN=" + getUserName()); + // force XML response with Internet Explorer header + getMethod.setRequestHeader("User-Agent", "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 1.1.4322)"); + XMLStreamReader reader = null; + try { + int status = wdr.retrieveSessionInstance().executeMethod(getMethod); + if (status != HttpStatus.SC_OK) { + throw new IOException("Unable to get user email from: " + getMethod.getPath()); + } + XMLInputFactory inputFactory = XMLInputFactory.newInstance(); + inputFactory.setProperty(XMLInputFactory.IS_COALESCING, Boolean.TRUE); + inputFactory.setProperty(XMLInputFactory.IS_REPLACING_ENTITY_REFERENCES, Boolean.TRUE); + + reader = inputFactory.createXMLStreamReader(getMethod.getResponseBodyAsStream()); + boolean inEM = false; + while (reader.hasNext()) { + int event = reader.next(); + if (event == XMLStreamConstants.START_ELEMENT && "EM".equals(reader.getLocalName())) { + inEM = true; + } else if (event == XMLStreamConstants.CHARACTERS && inEM) { + email = reader.getText(); + inEM = false; + } + } + } catch (XMLStreamException e) { + throw new IOException(e.getMessage()); + } finally { + try { + reader.close(); + } catch (XMLStreamException e) { + LOGGER.error(e); + } + getMethod.releaseConnection(); + } + if (email == null) { + throw new IOException("Unable to get user email from: " + getMethod.getPath()); + } + + return email; + } + + public String getFreebusy(Map valueMap) throws IOException { + String result = null; + + String startDateValue = valueMap.get("DTSTART"); + String endDateValue = valueMap.get("DTEND"); + String attendee = valueMap.get("ATTENDEE"); + if (attendee.startsWith("mailto:")) { + attendee = attendee.substring("mailto:".length()); + } + int interval = 30; + + SimpleDateFormat icalParser = new SimpleDateFormat("yyyyMMdd'T'HHmmss'Z'"); + icalParser.setTimeZone(new SimpleTimeZone(0, "GMT")); + + SimpleDateFormat shortIcalParser = new SimpleDateFormat("yyyyMMdd"); + shortIcalParser.setTimeZone(new SimpleTimeZone(0, "GMT")); + + SimpleDateFormat owaFormatter = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'"); + owaFormatter.setTimeZone(new SimpleTimeZone(0, "GMT")); + + String url = null; + Date startDate = null; + Date endDate = null; + try { + if (startDateValue.length() == 8) { + startDate = shortIcalParser.parse(startDateValue); + } else { + startDate = icalParser.parse(startDateValue); + } + if (endDateValue.length() == 8) { + endDate = shortIcalParser.parse(endDateValue); + } else { + endDate = icalParser.parse(endDateValue); + } + url = "/public/?cmd=freebusy" + + "&start=" + owaFormatter.format(startDate) + + "&end=" + owaFormatter.format(endDate) + + "&interval=" + interval + + "&u=SMTP:" + attendee; + } catch (ParseException e) { + throw new IOException(e.getMessage()); + } + + GetMethod getMethod = new GetMethod(url); + getMethod.setRequestHeader("User-Agent", "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 1.1.4322)"); + getMethod.setRequestHeader("Content-Type", "text/xml"); + + try { + int status = wdr.retrieveSessionInstance().executeMethod(getMethod); + if (status != HttpStatus.SC_OK) { + throw new IOException("Unable to get free-busy from: " + getMethod.getPath()); + } + // TODO : parse + String body = getMethod.getResponseBodyAsString(); + int startIndex = body.lastIndexOf(""); + int endIndex = body.lastIndexOf(""); + if (startIndex >= 0 && endIndex >= 0) { + String fbdata = body.substring(startIndex + "".length(), endIndex); + Calendar currentCal = Calendar.getInstance(TimeZone.getTimeZone("UTC")); + currentCal.setTime(startDate); + + StringBuilder busyBuffer = new StringBuilder(); + boolean isBusy = fbdata.charAt(0) != '0'; + if (isBusy) { + busyBuffer.append(icalParser.format(currentCal.getTime())); + } + for (int i = 1; i < fbdata.length(); i++) { + currentCal.add(Calendar.MINUTE, interval); + if (isBusy && fbdata.charAt(i) == '0') { + // busy -> non busy + busyBuffer.append('/').append(icalParser.format(currentCal.getTime())); + } else if (!isBusy && fbdata.charAt(i) != '0') { + // non busy -> busy + if (busyBuffer.length() > 0) { + busyBuffer.append(','); + } + busyBuffer.append(icalParser.format(currentCal.getTime())); + } + isBusy = fbdata.charAt(i) != '0'; + } + result = busyBuffer.toString(); + } + } finally { + getMethod.releaseConnection(); + } + if (result == null) { + throw new IOException("Unable to get user email from: " + getMethod.getPath()); + } + + return result; + } + } diff --git a/src/java/davmail/imap/ImapConnection.java b/src/java/davmail/imap/ImapConnection.java index f9f9078b..46da6a6c 100644 --- a/src/java/davmail/imap/ImapConnection.java +++ b/src/java/davmail/imap/ImapConnection.java @@ -20,7 +20,7 @@ public class ImapConnection extends AbstractConnection { // Initialize the streams and start the thread public ImapConnection(Socket clientSocket) { - super(clientSocket); + super("ImapConnection", clientSocket); } public void run() { diff --git a/src/java/davmail/imap/ImapServer.java b/src/java/davmail/imap/ImapServer.java index 6e1004ac..9515bb51 100644 --- a/src/java/davmail/imap/ImapServer.java +++ b/src/java/davmail/imap/ImapServer.java @@ -18,7 +18,7 @@ public class ImapServer extends AbstractServer { * Start the thread. */ public ImapServer(int port) throws IOException { - super(port, ImapServer.DEFAULT_PORT); + super("ImapServer", port, ImapServer.DEFAULT_PORT); } public AbstractConnection createConnectionHandler(Socket clientSocket) { diff --git a/src/java/davmail/pop/PopConnection.java b/src/java/davmail/pop/PopConnection.java index 7a0aed07..6e9da001 100644 --- a/src/java/davmail/pop/PopConnection.java +++ b/src/java/davmail/pop/PopConnection.java @@ -25,7 +25,7 @@ public class PopConnection extends AbstractConnection { // Initialize the streams and start the thread public PopConnection(Socket clientSocket) { - super(clientSocket); + super("PopConnection", clientSocket); } public long getTotalMessagesLength() { diff --git a/src/java/davmail/pop/PopServer.java b/src/java/davmail/pop/PopServer.java index 1c1329e0..76e50210 100644 --- a/src/java/davmail/pop/PopServer.java +++ b/src/java/davmail/pop/PopServer.java @@ -19,7 +19,7 @@ public class PopServer extends AbstractServer { * @param port pop listen port, 110 if not defined (0) */ public PopServer(int port) throws IOException { - super(port, PopServer.DEFAULT_PORT); + super("PopServer", port, PopServer.DEFAULT_PORT); } public AbstractConnection createConnectionHandler(Socket clientSocket) { diff --git a/src/java/davmail/smtp/SmtpConnection.java b/src/java/davmail/smtp/SmtpConnection.java index b9dd9139..df3bf2f6 100644 --- a/src/java/davmail/smtp/SmtpConnection.java +++ b/src/java/davmail/smtp/SmtpConnection.java @@ -23,7 +23,7 @@ public class SmtpConnection extends AbstractConnection { // Initialize the streams and start the thread public SmtpConnection(Socket clientSocket) { - super(clientSocket); + super("SmtpConnection", clientSocket); } public void run() { diff --git a/src/java/davmail/smtp/SmtpServer.java b/src/java/davmail/smtp/SmtpServer.java index ff569639..e1eaf0c5 100644 --- a/src/java/davmail/smtp/SmtpServer.java +++ b/src/java/davmail/smtp/SmtpServer.java @@ -12,9 +12,11 @@ public class SmtpServer extends AbstractServer { /** * Create a ServerSocket to listen for connections. * Start the thread. + * @param port smtp port + * @throws java.io.IOException on error */ public SmtpServer(int port) throws IOException { - super(port, SmtpServer.DEFAULT_PORT); + super("SmtpServer", port, SmtpServer.DEFAULT_PORT); } public AbstractConnection createConnectionHandler(Socket clientSocket) { diff --git a/src/java/davmail/tray/SwtGatewayTray.java b/src/java/davmail/tray/SwtGatewayTray.java index 2348366b..918420a5 100644 --- a/src/java/davmail/tray/SwtGatewayTray.java +++ b/src/java/davmail/tray/SwtGatewayTray.java @@ -128,7 +128,7 @@ public class SwtGatewayTray implements DavGatewayTrayInterface { DavGatewayTray.warn("Unable to set look and feel"); } - new Thread() { + new Thread("SWT") { public void run() { display = new Display(); shell = new Shell(display); diff --git a/src/java/davmail/ui/SettingsFrame.java b/src/java/davmail/ui/SettingsFrame.java index 6d481304..16bd424f 100644 --- a/src/java/davmail/ui/SettingsFrame.java +++ b/src/java/davmail/ui/SettingsFrame.java @@ -21,6 +21,7 @@ public class SettingsFrame extends JFrame { protected JTextField urlField; protected JTextField popPortField; protected JTextField smtpPortField; + protected JTextField caldavPortField; protected JTextField keepDelayField; JCheckBox enableProxyField; @@ -50,13 +51,14 @@ public class SettingsFrame extends JFrame { } protected JPanel getSettingsPanel() { - JPanel settingsPanel = new JPanel(new GridLayout(4, 2)); + JPanel settingsPanel = new JPanel(new GridLayout(5, 2)); settingsPanel.setBorder(BorderFactory.createTitledBorder("Gateway")); urlField = new JTextField(Settings.getProperty("davmail.url"), 17); urlField.setToolTipText("Base outlook web access URL"); popPortField = new JTextField(Settings.getProperty("davmail.popPort"), 4); smtpPortField = new JTextField(Settings.getProperty("davmail.smtpPort"), 4); + caldavPortField = new JTextField(Settings.getProperty("davmail.caldavPort"), 4); keepDelayField = new JTextField(Settings.getProperty("davmail.keepDelay"), 4); keepDelayField.setToolTipText("Number of days to keep messages in trash"); @@ -64,6 +66,7 @@ public class SettingsFrame extends JFrame { addSettingComponent(settingsPanel, "OWA url: ", urlField); addSettingComponent(settingsPanel, "Local POP port: ", popPortField); addSettingComponent(settingsPanel, "Local SMTP port: ", smtpPortField); + addSettingComponent(settingsPanel, "Caldav HTTP port: ", caldavPortField); addSettingComponent(settingsPanel, "Keep Delay: ", keepDelayField); return settingsPanel; } @@ -150,6 +153,7 @@ public class SettingsFrame extends JFrame { urlField.setText(Settings.getProperty("davmail.url")); popPortField.setText(Settings.getProperty("davmail.popPort")); smtpPortField.setText(Settings.getProperty("davmail.smtpPort")); + caldavPortField.setText(Settings.getProperty("davmail.caldavPort")); keepDelayField.setText(Settings.getProperty("davmail.keepDelay")); boolean enableProxy = Settings.getBooleanProperty("davmail.enableProxy"); enableProxyField.setSelected(enableProxy); @@ -215,6 +219,7 @@ public class SettingsFrame extends JFrame { Settings.setProperty("davmail.url", urlField.getText()); Settings.setProperty("davmail.popPort", popPortField.getText()); Settings.setProperty("davmail.smtpPort", smtpPortField.getText()); + Settings.setProperty("davmail.caldavPort", caldavPortField.getText()); Settings.setProperty("davmail.keepDelay", keepDelayField.getText()); Settings.setProperty("davmail.enableProxy", String.valueOf(enableProxyField.isSelected())); Settings.setProperty("davmail.proxyHost", httpProxyField.getText()); diff --git a/src/site/xdoc/gettingstarted.xml b/src/site/xdoc/gettingstarted.xml index 279832a6..1f81405e 100644 --- a/src/site/xdoc/gettingstarted.xml +++ b/src/site/xdoc/gettingstarted.xml @@ -40,6 +40,11 @@ Local SMTP server port to configure in POP client configuration 25 + + Local Caldav HTTP port + Local Caldav server port to configure in Caldav (calendar) client configuration + 80 + Keep Delay Number of days to keep messages in Exchange trash folder before actual deletion