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
This commit is contained in:
mguessan 2008-11-27 00:56:28 +00:00
parent 20ef4a9da0
commit 64e0688f0d
16 changed files with 998 additions and 40 deletions

View File

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

View File

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

View File

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

View File

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

View File

@ -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<String, String> parseHeaders() throws IOException {
HashMap<String, String> headers = new HashMap<String, String>();
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<String, String> 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<String, String> 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<String, String> 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("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
buffer.append("<D:multistatus xmlns:D=\"DAV:\" xmlns:C=\"urn:ietf:params:xml:ns:caldav\">\n");
buffer.append(" <D:response>\n");
buffer.append(" <D:href>/user</D:href>\n");
buffer.append(" <D:propstat>\n");
buffer.append(" <D:prop>\n");
buffer.append(" <C:calendar-home-set>\n");
buffer.append(" <D:href>/calendar</D:href>\n");
buffer.append(" </C:calendar-home-set>");
buffer.append(" <C:calendar-user-address-set>\n");
// TODO
buffer.append(" <D:href>mailto:" + session.getEmail() + "</D:href>\n");
buffer.append(" </C:calendar-user-address-set>");
buffer.append(" <C:schedule-inbox-URL>\n");
buffer.append(" <D:href>/inbox</D:href>\n");
buffer.append(" </C:schedule-inbox-URL>");
buffer.append(" <C:schedule-outbox-URL>\n");
buffer.append(" <D:href>/outbox</D:href>\n");
buffer.append(" </C:schedule-outbox-URL>");
buffer.append(" </D:prop>\n");
buffer.append(" <D:status>HTTP/1.1 200 OK</D:status>\n");
buffer.append(" </D:propstat>\n");
buffer.append(" </D:response>\n");
buffer.append("</D:multistatus>\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("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
buffer.append("<D:multistatus xmlns:D=\"DAV:\" xmlns:CS=\"http://calendarserver.org/ns/\">\n");
buffer.append(" <D:response>\n");
buffer.append(" <D:href>/calendar</D:href>\n");
buffer.append(" <D:propstat>\n");
buffer.append(" <D:prop>\n");
// TODO : parse request
if (body.indexOf("resourcetype") >= 0) {
buffer.append(" <D:resourcetype>\n");
buffer.append(" <D:collection/>\n");
buffer.append(" <C:calendar xmlns:C=\"urn:ietf:params:xml:ns:caldav\"/>\n");
buffer.append(" </D:resourcetype>\n");
}
if (body.indexOf("owner") >= 0) {
buffer.append(" <D:owner>\n");
buffer.append(" <D:href>/user</D:href>\n");
buffer.append(" </D:owner>\n");
}
if (body.indexOf("getctag") >= 0) {
buffer.append(" <CS:getctag>")
.append(base64Encode(session.getCalendarEtag()))
.append("</CS:getctag>\n");
}
buffer.append(" </D:prop>\n");
buffer.append(" <D:status>HTTP/1.1 200 OK</D:status>\n");
buffer.append(" </D:propstat>\n");
buffer.append(" </D:response>\n");
buffer.append("</D:multistatus>\n");
HashMap<String, String> responseHeaders = new HashMap<String, String>();
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<String> properties = new HashSet<String>();
// TODO : parse body
if (body.indexOf("D:getetag") >= 0) {
properties.add("getetag");
}
if (body.indexOf("calendar-data") >= 0) {
properties.add("calendar-data");
}
List<ExchangeSession.Event> events;
List<String> notFound = new ArrayList<String>();
if (body.indexOf("calendar-multiget") >= 0) {
events = new ArrayList<ExchangeSession.Event>();
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("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
"<D:multistatus xmlns:D=\"DAV:\">\n");
for (ExchangeSession.Event event : events) {
String eventPath = event.getPath().replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
buffer.append("<D:response>\n");
buffer.append(" <D:href>/calendar").append(eventPath).append("</D:href>\n");
buffer.append(" <D:propstat>\n");
buffer.append(" <D:prop>\n");
if (properties.contains("calendar-data")) {
String ics = event.getICS();
if (ics != null && ics.length() > 0) {
ics = ics.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
buffer.append(" <C:calendar-data xmlns:C=\"urn:ietf:params:xml:ns:caldav\"\n");
buffer.append(" C:content-type=\"text/calendar\" C:version=\"2.0\">");
buffer.append(ics);
buffer.append("</C:calendar-data>\n");
}
}
if (properties.contains("getetag")) {
buffer.append(" <D:getetag>").append(event.getEtag()).append("</D:getetag>\n");
}
buffer.append(" </D:prop>\n");
buffer.append(" <D:status>HTTP/1.1 200 OK</D:status>\n");
buffer.append(" </D:propstat>\n");
if (notFound.size() > 0) {
buffer.append(" <D:propstat>\n");
for (String href : notFound) {
buffer.append(" <D:href>").append(href).append("</D:href>\n");
}
buffer.append(" <D:status>HTTP/1.1 404 Not Found</D:status>\n");
buffer.append(" </D:propstat>\n");
}
buffer.append(" </D:response>").append((char) 13).append((char) 10);
}
buffer.append("</D:multistatus>");
// 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<String,String> valueMap = new HashMap<String,String>();
Map<String,String> keyMap = new HashMap<String,String>();
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 = "<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n" +
" <C:schedule-response xmlns:D=\"DAV:\"\n" +
" xmlns:C=\"urn:ietf:params:xml:ns:caldav\">\n" +
" <C:response>\n" +
" <C:recipient>\n" +
" <D:href>"+valueMap.get("ATTENDEE")+"</D:href>\n" +
" </C:recipient>\n" +
" <C:request-status>2.0;Success</C:request-status>\n" +
" <C:calendar-data>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" +
"</C:calendar-data>\n" +
" </C:response>\n" +
" </C:schedule-response>";
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<String, String> headers = new HashMap<String, String>();
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<String, String> headers = new HashMap<String, String>();
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<String, String> 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");
}
}
}

View File

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

View File

@ -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<Event> getAllEvents() throws IOException {
List<Event> events = new ArrayList<Event>();
String searchRequest = "<?xml version=\"1.0\"?>\n" +
"<d:searchrequest xmlns:d=\"DAV:\">\n" +
" <d:sql> 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" +
" </d:sql>\n" +
"</d:searchrequest>";
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<String> EVENT_REQUEST_PROPERTIES = new Vector<String>();
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<String, String> 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("<a:fbdata>");
int endIndex = body.lastIndexOf("</a:fbdata>");
if (startIndex >= 0 && endIndex >= 0) {
String fbdata = body.substring(startIndex + "<a:fbdata>".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;
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -40,6 +40,11 @@
<td>Local SMTP server port to configure in POP client configuration</td>
<td>25</td>
</tr>
<tr>
<td>Local Caldav HTTP port</td>
<td>Local Caldav server port to configure in Caldav (calendar) client configuration</td>
<td>80</td>
</tr>
<tr>
<td>Keep Delay</td>
<td>Number of days to keep messages in Exchange trash folder before actual deletion</td>