diff --git a/src/java/davmail/AbstractConnection.java b/src/java/davmail/AbstractConnection.java new file mode 100644 index 00000000..38489d0b --- /dev/null +++ b/src/java/davmail/AbstractConnection.java @@ -0,0 +1,80 @@ +package davmail; + +import davmail.exchange.ExchangeSession; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.net.Socket; + +/** + * Generic connection common to pop3 and smtp implementations + */ +public class AbstractConnection extends Thread { + + protected Socket client; + protected BufferedReader in; + protected OutputStream os; + // exchange server url + protected String url; + // user name and password initialized through connection + protected String userName = null; + protected String password = null; + // connection state + protected int state = 0; + // Exchange session proxy + protected ExchangeSession session; + + // Initialize the streams and start the thread + public AbstractConnection(String url, Socket clientSocket) { + this.url = url; + client = clientSocket; + try { + in = new BufferedReader(new InputStreamReader(client.getInputStream())); + os = client.getOutputStream(); + } catch (IOException e) { + try { + client.close(); + } catch (IOException e2) { + DavGatewayTray.error("Exception while getting socket streams",e2); + } + DavGatewayTray.error("Exception while getting socket streams", e); + return; + } + // start the thread + this.start(); + } + + public void sendClient(String message) throws IOException { + sendClient(null, message); + } + + public void sendClient(String prefix, String message) throws IOException { + StringBuffer logBuffer = new StringBuffer("> "); + if (prefix != null) { + logBuffer.append(prefix); + os.write(prefix.getBytes()); + } + logBuffer.append(message); + DavGatewayTray.debug(logBuffer.toString()); + os.write(message.getBytes()); + os.write('\r'); + os.write('\n'); + os.flush(); + } + + /** + * Read a line from the client connection. + * Log message to stdout + * @return command line or null + * @throws IOException + */ + public String readClient() throws IOException { + String line = in.readLine(); + DavGatewayTray.debug("< "+line); + DavGatewayTray.switchIcon(); + return line; + } + +} diff --git a/src/java/davmail/AbstractServer.java b/src/java/davmail/AbstractServer.java new file mode 100644 index 00000000..33120d51 --- /dev/null +++ b/src/java/davmail/AbstractServer.java @@ -0,0 +1,63 @@ +package davmail; + +import java.net.ServerSocket; +import java.net.Socket; +import java.io.IOException; + +/** + * Generic abstract server common to SMTP and POP3 implementations + */ +public abstract class AbstractServer extends Thread { + protected int port; + protected String url; + protected ServerSocket serverSocket; + + /** + * Create a ServerSocket to listen for connections. + * Start the thread. + */ + public AbstractServer(String url, int port) { + this.port = port; + this.url = url; + try { + serverSocket = new ServerSocket(port); + } catch (IOException e) { + fail(e, "Exception creating server socket"); + } + } + + // Exit with an error message, when an exception occurs. + public static void fail(Exception e, String msg) { + System.err.println(msg + ": " + e); + System.exit(1); + } + + /** + * The body of the server thread. Loop forever, listening for and + * accepting connections from clients. For each connection, + * create a Connection object to handle communication through the + * new Socket. + */ + public void run() { + try { + //noinspection InfiniteLoopStatement + while (true) { + Socket clientSocket = serverSocket.accept(); + DavGatewayTray.debug("Connection from " + clientSocket.getInetAddress() + " on port " + port); + // only accept localhost connections for security reasons + if (clientSocket.getInetAddress().toString().indexOf("127.0.0.1") > 0) { + createConnectionHandler(url, clientSocket); + } else { + clientSocket.close(); + DavGatewayTray.warn("Connection from external client refused"); + } + System.gc(); + } + } catch (IOException e) { + fail(e, "Exception while listening for connections"); + } + } + + public abstract void createConnectionHandler(String url, Socket clientSocket); +} + diff --git a/src/java/davmail/DavGateway.java b/src/java/davmail/DavGateway.java new file mode 100644 index 00000000..a241f319 --- /dev/null +++ b/src/java/davmail/DavGateway.java @@ -0,0 +1,52 @@ +package davmail; + +import davmail.imap.ImapServer; +import davmail.pop.PopServer; +import davmail.smtp.SmtpServer; + +/** + * DavGateway main class + */ +public class DavGateway { + protected static final String USAGE_MESSAGE = "Usage : java davmail.DavGateway url [smtplistenport] [pop3listenport] [imaplistenport]"; + + /** + * Start the gateway, listen on spécified smtp and pop3 ports + */ + public static void main(String[] args) { + + int smtpPort = SmtpServer.DEFAULT_PORT; + int popPort = PopServer.DEFAULT_PORT; + int imapPort = ImapServer.DEFAULT_PORT; + String url; + + if (args.length >= 1) { + url = args[0]; + try { + if (args.length >= 2) { + smtpPort = Integer.parseInt(args[1]); + } + if (args.length >= 3) { + popPort = Integer.parseInt(args[2]); + } + if (args.length >= 4) { + imapPort = Integer.parseInt(args[3]); + } + DavGatewayTray.init(); + + SmtpServer smtpServer = new SmtpServer(url, smtpPort); + PopServer popServer = new PopServer(url, popPort); + ImapServer imapServer = new ImapServer(url, imapPort); + smtpServer.start(); + popServer.start(); + imapServer.start(); + DavGatewayTray.info("Listening on ports " + smtpPort + " "+popPort+" "+imapPort); + } catch (NumberFormatException e) { + System.out.println(DavGateway.USAGE_MESSAGE); + } + } else { + System.out.println(DavGateway.USAGE_MESSAGE); + } + } + +} diff --git a/src/java/davmail/DavGatewayTray.java b/src/java/davmail/DavGatewayTray.java new file mode 100644 index 00000000..62751932 --- /dev/null +++ b/src/java/davmail/DavGatewayTray.java @@ -0,0 +1,145 @@ +package davmail; + +import org.apache.log4j.Logger; +import org.apache.log4j.Priority; + +import java.awt.*; +import java.awt.event.ActionListener; +import java.awt.event.ActionEvent; +import java.net.URL; + +/** + * Tray icon handler + */ +public class DavGatewayTray { + protected static final Logger logger = Logger.getLogger("davmail"); + + // lock for synchronized block + protected static final Object lock = new Object(); + + protected static TrayIcon trayIcon = null; + protected static Image image = null; + protected static Image image2 = null; + + public static void switchIcon() { + try { + synchronized (lock) { + if (trayIcon.getImage() == image) { + trayIcon.setImage(image2); + } else { + trayIcon.setImage(image); + } + } + } catch (NoClassDefFoundError e) { + // ignore, jdk <= 1.6 + } + + } + + public static void resetIcon() { + try { + synchronized (lock) { + trayIcon.setImage(image); + } + } catch (NoClassDefFoundError e) { + // ignore, jdk <= 1.6 + } + } + + protected static void displayMessage(String message, Priority priority) { + synchronized (lock) { + if (trayIcon != null) { + TrayIcon.MessageType messageType = null; + if (priority == Priority.INFO) { + messageType = TrayIcon.MessageType.INFO; + } else if (priority == Priority.WARN) { + messageType = TrayIcon.MessageType.WARNING; + } else if (priority == Priority.ERROR) { + messageType = TrayIcon.MessageType.ERROR; + } + if (messageType != null) { + trayIcon.displayMessage("DavMail gateway", message, messageType); + } + trayIcon.setToolTip("DavMail gateway \n"+message); + } + logger.log(priority, message); + } + + } + + public static void debug(String message) { + displayMessage(message, Priority.DEBUG); + } + + public static void info(String message) { + displayMessage(message, Priority.INFO); + } + + public static void warn(String message) { + displayMessage(message, Priority.WARN); + } + + public static void error(String message) { + displayMessage(message, Priority.ERROR); + } + + public static void debug(String message, Exception e) { + debug(message + " " + e + " " + e.getMessage()); + } + + public static void info(String message, Exception e) { + info(message + " " + e + " " + e.getMessage()); + } + + public static void warn(String message, Exception e) { + warn(message + " " + e + " " + e.getMessage()); + } + + public static void error(String message, Exception e) { + error(message + " " + e + " " + e.getMessage()); + } + + public static void init() { + try { + if (SystemTray.isSupported()) { + // get the SystemTray instance + SystemTray tray = SystemTray.getSystemTray(); + // load an image + ClassLoader classloader = DavGatewayTray.class.getClassLoader(); + URL imageUrl = classloader.getResource("tray.png"); + image = Toolkit.getDefaultToolkit().getImage(imageUrl); + URL imageUrl2 = classloader.getResource("tray2.png"); + image2 = Toolkit.getDefaultToolkit().getImage(imageUrl2); + // create a action listener to listen for default action executed on the tray icon + ActionListener listener = new ActionListener() { + public void actionPerformed(ActionEvent e) { + SystemTray.getSystemTray().remove(trayIcon); + System.exit(0); + } + }; + // create a popup menu + PopupMenu popup = new PopupMenu(); + // create menu item for the default action + MenuItem defaultItem = new MenuItem("Exit"); + defaultItem.addActionListener(listener); + popup.add(defaultItem); + /// ... add other items + // construct a TrayIcon + trayIcon = new TrayIcon(image, "DavMail Gateway", popup); + // set the TrayIcon properties + trayIcon.addActionListener(listener); + // ... + // add the tray image + try { + tray.add(trayIcon); + } catch (AWTException e) { + System.err.println(e); + } + } + + } catch (NoClassDefFoundError e) { + DavGatewayTray.warn("JDK 1.6 needed for system tray support"); + } + + } +} diff --git a/src/java/davmail/exchange/ExchangeSession.java b/src/java/davmail/exchange/ExchangeSession.java new file mode 100644 index 00000000..abac8944 --- /dev/null +++ b/src/java/davmail/exchange/ExchangeSession.java @@ -0,0 +1,1125 @@ +package davmail.exchange; + +import org.apache.commons.httpclient.*; +import org.apache.commons.httpclient.methods.GetMethod; +import org.apache.commons.httpclient.methods.HeadMethod; +import org.apache.commons.httpclient.methods.PostMethod; +import org.apache.commons.httpclient.methods.PutMethod; +import org.apache.commons.httpclient.util.URIUtil; +import org.apache.webdav.lib.Property; +import org.apache.webdav.lib.ResponseEntity; +import org.apache.webdav.lib.WebdavResource; +import org.apache.log4j.Logger; +import org.jdom.Attribute; +import org.jdom.JDOMException; +import org.jdom.input.DOMBuilder; +import org.w3c.tidy.Tidy; + +import javax.mail.MessagingException; +import javax.mail.internet.MimeUtility; +import java.io.*; +import java.net.URL; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.*; + +/** + * Exchange session through Outlook Web Access (DAV) + */ +public class ExchangeSession { + protected static final Logger logger = Logger.getLogger("davmail.exchange.ExchangeSession"); + + /** + * exchange message properties needed to rebuild mime message + */ + protected static final Vector messageRequestProperties = new Vector(); + + static { + messageRequestProperties.add("DAV:uid"); + messageRequestProperties.add("urn:schemas:httpmail:subject"); + messageRequestProperties.add("urn:schemas:mailheader:mime-version"); + messageRequestProperties.add("urn:schemas:mailheader:content-class"); + messageRequestProperties.add("urn:schemas:httpmail:hasattachment"); + + // needed only when full headers not found + messageRequestProperties.add("urn:schemas:mailheader:received"); + messageRequestProperties.add("urn:schemas:mailheader:date"); + messageRequestProperties.add("urn:schemas:mailheader:message-id"); + messageRequestProperties.add("urn:schemas:mailheader:thread-topic"); + messageRequestProperties.add("urn:schemas:mailheader:thread-index"); + messageRequestProperties.add("urn:schemas:mailheader:from"); + messageRequestProperties.add("urn:schemas:mailheader:to"); + messageRequestProperties.add("urn:schemas:httpmail:priority"); + + // full headers + messageRequestProperties.add("http://schemas.microsoft.com/mapi/proptag/x0007D001E"); + // mail body + messageRequestProperties.add("http://schemas.microsoft.com/mapi/proptag/x01000001E"); + // html body + messageRequestProperties.add("urn:schemas:httpmail:htmldescription"); + // same as htmldescription, remove + // messageRequestProperties.add("http://schemas.microsoft.com/mapi/proptag/x01013001E"); + // size + messageRequestProperties.add("http://schemas.microsoft.com/mapi/proptag/x0e080003"); + // only for calendar events + messageRequestProperties.add("urn:schemas:calendar:location"); + messageRequestProperties.add("urn:schemas:calendar:dtstart"); + messageRequestProperties.add("urn:schemas:calendar:dtend"); + messageRequestProperties.add("urn:schemas:calendar:instancetype"); + messageRequestProperties.add("urn:schemas:calendar:busystatus"); + messageRequestProperties.add("urn:schemas:calendar:meetingstatus"); + messageRequestProperties.add("urn:schemas:calendar:alldayevent"); + messageRequestProperties.add("urn:schemas:calendar:responserequested"); + // TODO : full headers rebuild with cc + messageRequestProperties.add("urn:schemas:mailheader:cc"); + + } + + public static HashMap priorities = new HashMap(); + + static { + priorities.put("-2", "5 (Lowest)"); + priorities.put("-1", "4 (Low)"); + priorities.put("1", "2 (High)"); + priorities.put("2", "1 (Highest)"); + } + + public static final String CONTENT_TYPE_HEADER = "content-type: "; + public static final String CONTENT_TRANSFER_ENCODING_HEADER = "content-transfer-encoding: "; + + /** + * Date parser from Exchange format + */ + public final SimpleDateFormat dateParser; + /** + * Date formatter to MIME format + */ + public final SimpleDateFormat dateFormatter; + + protected String url; + + /** + * Various standard mail boxes Urls + */ + protected String inboxUrl; + protected String deleteditemsUrl; + protected String sendmsgUrl; + protected String draftsUrl; + + /** + * Base user mailboxes path (used to select folder) + */ + protected String mailPath; + protected String currentFolderUrl; + WebdavResource wdr = null; + + /** + * Create an exchange session for the given URL. + * The session is not actually established until a call to login() + * + * @param url Outlook Web Access URL + */ + public ExchangeSession(String url) { + this.url = url; + // SimpleDateFormat are not thread safe, need to create one instance for + // each session + dateParser = new java.text.SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS"); + dateParser.setTimeZone(new SimpleTimeZone(0, "GMT")); + dateFormatter = new java.text.SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss Z", Locale.ENGLISH); + } + + public void login(String userName, String password) throws Exception { + try { + // TODO : support different ports + URL urlObject = new URL(url); + // webdavresource is unable to create the correct url type + HttpURL httpURL; + if (url.startsWith("http://")) { + httpURL = new HttpURL(userName, password, + urlObject.getHost(), urlObject.getPort()); + } else if (url.startsWith("https://")) { + httpURL = new HttpsURL(userName, password, + urlObject.getHost(), urlObject.getPort()); + } else { + throw new IllegalArgumentException("Invalid URL: " + url); + } + wdr = new WebdavResource(httpURL, WebdavResource.NOACTION, 0); + + // set httpclient timeout to 30 seconds + //wdr.retrieveSessionInstance().setTimeout(30000); + + // get proxy configuration from system properties + String proxyHost = System.getProperty("http.proxyHost"); + String proxyPort = System.getProperty("http.proxyPort"); + String proxyUser = System.getProperty("http.proxyUser"); + String proxyPassword = System.getProperty("http.proxyPassword"); + + // get the internal HttpClient instance + HttpClient httpClient = wdr.retrieveSessionInstance(); + +/* // Only available in newer HttpClient releases, not compatible with slide library + List authPrefs = new ArrayList(); + authPrefs.add(AuthPolicy.BASIC); + httpClient.getParams().setParameter(AuthPolicy.AUTH_SCHEME_PRIORITY,authPrefs); +*/ + // do not send basic auth automatically + httpClient.getState().setAuthenticationPreemptive(false); + + // configure proxy + if (proxyHost != null) { + httpClient.getHostConfiguration().setProxy(proxyHost, Integer.parseInt(proxyPort)); + if (proxyUser != null) { + // detect ntlm authentication (windows domain name in user name) + int backslashindex = proxyUser.indexOf("\\"); + if (backslashindex > 0) { + httpClient.getState().setProxyCredentials(null, proxyHost, + new NTCredentials(proxyUser.substring(backslashindex + 1), + proxyPassword, null, + proxyUser.substring(0, backslashindex))); + } else { + httpClient.getState().setProxyCredentials(null, proxyHost, + new UsernamePasswordCredentials(proxyUser, proxyPassword)); + } + } + } + + // get webmail root url (will follow redirects) + // providing credentials + HttpMethod initmethod = new GetMethod(url); + wdr.executeHttpRequestMethod(httpClient, + initmethod); + if (initmethod.getPath().indexOf("exchweb/bin") > 0) { + logger.debug("** Form based authentication detected"); + + PostMethod logonMethod = new PostMethod( + "/exchweb/bin/auth/owaauth.dll?" + + "ForcedBasic=false&Basic=false&Private=true" + + "&Language=No_Value" + ); + logonMethod.addParameter("destination", url); + logonMethod.addParameter("flags", "4"); +// logonMethod.addParameter("visusername", userName.substring(userName.lastIndexOf('\\'))); + logonMethod.addParameter("username", userName); + logonMethod.addParameter("password", password); +// logonMethod.addParameter("SubmitCreds", "Log On"); +// logonMethod.addParameter("forcedownlevel", "0"); + logonMethod.addParameter("trusted", "4"); + + wdr.executeHttpRequestMethod(wdr.retrieveSessionInstance(), + logonMethod); + Header locationHeader = logonMethod.getResponseHeader( + "Location"); + + if (logonMethod.getStatusCode() != 302 || + locationHeader == null || + !url.equals(locationHeader.getValue())) { + throw new HttpException("Authentication failed"); + } + + } + + // User now authenticated, get various session information + HttpMethod method = new GetMethod(url); + int status = wdr.executeHttpRequestMethod(wdr. + retrieveSessionInstance(), method); + if (status != HttpStatus.SC_MULTI_STATUS + && status != HttpStatus.SC_OK) { + HttpException ex = new HttpException(); + ex.setReasonCode(status); + throw ex; + } + + // TODO : catch exception + // get user mail URL from html body (multi frame) + String body = method.getResponseBodyAsString(); + int beginIndex = body.indexOf(url); + if (beginIndex < 0) { + throw new HttpException(url + "not found in body"); + } + body = body.substring(beginIndex); + int endIndex = body.indexOf('"'); + if (endIndex < 0) { + throw new HttpException(url + "not found in body"); + } + body = body.substring(url.length(), endIndex); + // got base http mailbox http url + mailPath = "/exchange/" + body; + wdr.setPath(mailPath); +// wdr.propfindMethod(0); + + // Retrieve inbox and trash URLs + Vector reqProps = new Vector(); + reqProps.add("urn:schemas:httpmail:inbox"); + reqProps.add("urn:schemas:httpmail:deleteditems"); + reqProps.add("urn:schemas:httpmail:sendmsg"); + reqProps.add("urn:schemas:httpmail:drafts"); + + Enumeration inboxEnum = wdr.propfindMethod(0, reqProps); + if (!inboxEnum.hasMoreElements()) { + throw new IOException("Unable to get inbox"); + } + ResponseEntity inboxResponse = (ResponseEntity) inboxEnum. + nextElement(); + Enumeration inboxPropsEnum = inboxResponse.getProperties(); + if (!inboxPropsEnum.hasMoreElements()) { + throw new IOException("Unable to get inbox"); + } + while (inboxPropsEnum.hasMoreElements()) { + Property inboxProp = (Property) inboxPropsEnum.nextElement(); + if ("inbox".equals(inboxProp.getLocalName())) { + inboxUrl = URIUtil.decode(inboxProp.getPropertyAsString()); + } + if ("deleteditems".equals(inboxProp.getLocalName())) { + deleteditemsUrl = URIUtil.decode(inboxProp. + getPropertyAsString()); + } + if ("sendmsg".equals(inboxProp.getLocalName())) { + sendmsgUrl = URIUtil.decode(inboxProp. + getPropertyAsString()); + } + if ("drafts".equals(inboxProp.getLocalName())) { + draftsUrl = URIUtil.decode(inboxProp. + getPropertyAsString()); + } + } + + // set current folder to Inbox + currentFolderUrl = inboxUrl; + + logger.debug("Inbox URL : " + inboxUrl); + logger.debug("Trash URL : " + deleteditemsUrl); + logger.debug("Send URL : " + sendmsgUrl); + deleteditemsUrl = URIUtil.getPath(deleteditemsUrl); + wdr.setPath(URIUtil.getPath(inboxUrl)); + + } catch (Exception exc) { + logger.error("Exchange login exception ", exc); + try { + System.err.println( + wdr.getStatusCode() + " " + wdr.getStatusMessage()); + } catch (Exception e) { + logger.error("Exception getting status from " + wdr); + } + throw exc; + } + + } + + /** + * Close session. + * This will only close http client, not the actual Exchange session + * + * @throws IOException + */ + public void close() throws IOException { + wdr.close(); + } + + /** + * Create message in current folder + */ + public void createMessage(String subject, String messageBody) throws IOException { + createMessage(currentFolderUrl, subject, messageBody); + } + + /** + * Create message in specified folder. + * Will overwrite an existing message with same subject in the same folder + */ + public void createMessage(String folderUrl, String subject, String messageBody) throws IOException { + String messageUrl = URIUtil.encodePathQuery(folderUrl + "/" + subject + ".EML"); + + PutMethod putmethod = new PutMethod(messageUrl); + putmethod.setRequestHeader("Content-Type", "message/rfc822"); + putmethod.setRequestBody(messageBody); + + int code = wdr.retrieveSessionInstance().executeMethod(putmethod); + + if (code == 200) { + logger.warn("Overwritten message " + messageUrl); + } else if (code != 201) { + throw new IOException("Unable to create message " + code + " " + putmethod.getStatusLine()); + } + } + + protected Message buildMessage(ResponseEntity responseEntity) throws URIException { + Message message = new Message(); + message.messageUrl = URIUtil.decode(responseEntity.getHref()); + Enumeration propertiesEnum = responseEntity.getProperties(); + while (propertiesEnum.hasMoreElements()) { + Property prop = (Property) propertiesEnum.nextElement(); + String localName = prop.getLocalName(); + if ("x0007D001E".equals(localName)) { + message.fullHeaders = prop.getPropertyAsString(); + } else if ("x01000001E".equals(localName)) { + message.body = prop.getPropertyAsString(); + } else if ("x0e080003".equals(localName)) { + message.size = Integer.parseInt(prop.getPropertyAsString()); + } else if ("htmldescription".equals(localName)) { + message.htmlBody = prop.getPropertyAsString(); + } else if ("uid".equals(localName)) { + message.uid = prop.getPropertyAsString(); + } else if ("content-class".equals(prop.getLocalName())) { + message.contentClass = prop.getPropertyAsString(); + } else if (("hasattachment").equals(prop.getLocalName())) { + message.hasAttachment = "1".equals(prop.getPropertyAsString()); + } else if ("received".equals(prop.getLocalName())) { + message.received = prop.getPropertyAsString(); + } else if ("date".equals(prop.getLocalName())) { + message.date = prop.getPropertyAsString(); + } else if ("message-id".equals(prop.getLocalName())) { + message.messageId = prop.getPropertyAsString(); + } else if ("thread-topic".equals(prop.getLocalName())) { + message.threadTopic = prop.getPropertyAsString(); + } else if ("thread-index".equals(prop.getLocalName())) { + message.threadIndex = prop.getPropertyAsString(); + } else if ("from".equals(prop.getLocalName())) { + message.from = prop.getPropertyAsString(); + } else if ("to".equals(prop.getLocalName())) { + message.to = prop.getPropertyAsString(); + } else if ("subject".equals(prop.getLocalName())) { + message.subject = prop.getPropertyAsString(); + } else if ("priority".equals(prop.getLocalName())) { + String priorityLabel = priorities.get(prop.getPropertyAsString()); + if (priorityLabel != null) { + message.priority = priorityLabel; + } + } + } + message.preProcessHeaders(); + + + return message; + } + + public Message getMessage(String messageUrl) throws IOException { + + // // TODO switch according to Log4J log level + + wdr.setDebug(4); + wdr.propfindMethod(messageUrl, 0); + + Enumeration messageEnum = wdr.propfindMethod(messageUrl, 0, messageRequestProperties); + wdr.setDebug(0); + + // 201 created in some cases ?!? + if ((wdr.getStatusCode() != 200 && wdr.getStatusCode() != 201) || !messageEnum.hasMoreElements()) { + throw new IOException("Unable to get message: " + wdr.getStatusCode() + + " " + wdr.getStatusMessage()); + } + ResponseEntity entity = (ResponseEntity) messageEnum.nextElement(); + + return buildMessage(entity); + + } + + public List getAllMessages() throws IOException { + List messages = new ArrayList(); + wdr.setDebug(4); + wdr.propfindMethod(currentFolderUrl, 1); + + Enumeration folderEnum = wdr.propfindMethod(currentFolderUrl, 1, messageRequestProperties); + wdr.setDebug(0); + while (folderEnum.hasMoreElements()) { + ResponseEntity entity = (ResponseEntity) folderEnum.nextElement(); + + Message message = buildMessage(entity); + if ("urn:content-classes:message".equals(message.contentClass) || + "urn:content-classes:calendarmessage".equals(message.contentClass)) { + messages.add(message); + } + } + return messages; + } + + public void sendMessage(BufferedReader reader) throws IOException { + String subject = "davmailtemp"; + String line = reader.readLine(); + // TODO : use reader directly instead of buffer + StringBuffer mailBuffer = new StringBuffer(); + while (!".".equals(line)) { + mailBuffer.append(line); + mailBuffer.append("\n"); + line = reader.readLine(); + + // patch thunderbird html in reply for correct outlook display + if (line.startsWith(" reqProps = new Vector(); + reqProps.add("urn:schemas:httpmail:unreadcount"); + reqProps.add("DAV:childcount"); + Enumeration folderEnum = wdr.propfindMethod(folder.folderUrl, 0, reqProps); + if (folderEnum.hasMoreElements()) { + ResponseEntity entity = (ResponseEntity) folderEnum.nextElement(); + Enumeration propertiesEnum = entity.getProperties(); + while (propertiesEnum.hasMoreElements()) { + Property prop = (Property) propertiesEnum.nextElement(); + if ("unreadcount".equals(prop.getLocalName())) { + folder.unreadCount = Integer.parseInt(prop.getPropertyAsString()); + } + if ("childcount".equals(prop.getLocalName())) { + folder.childCount = Integer.parseInt(prop.getPropertyAsString()); + } + } + + } else { + throw new IOException("Folder not found :" + folder.folderUrl); + } + currentFolderUrl = folder.folderUrl; + return folder; + } + + public class Folder { + public String folderUrl; + public int childCount; + public int unreadCount; + } + + public class Message { + public static final String CONTENT_TYPE_HEADER = "Content-Type: "; + public static final String CONTENT_TRANSFER_ENCODING_HEADER = "Content-Transfer-Encoding: "; + public String messageUrl; + public String uid; + public int size; + public String fullHeaders; + public String body; + public String htmlBody; + public String contentClass; + public boolean hasAttachment; + public String to; + public String date; + public String messageId; + public String received; + public String threadIndex; + public String threadTopic; + public String from; + public String subject; + public String priority; + + protected Map attachmentsMap; + + // attachment index used during write + protected int attachmentIndex; + + protected String getReceived() { + StringTokenizer st = new StringTokenizer(received, "\r\n"); + StringBuffer result = new StringBuffer(); + while (st.hasMoreTokens()) { + result.append("Received: ").append(st.nextToken()).append("\r\n"); + } + return result.toString(); + } + + protected void preProcessHeaders() { + // only handle exchange messages + // TODO : handle calendar messages + if (!"urn:content-classes:message".equals(contentClass) && + !"urn:content-classes:calendarmessage".equals(contentClass) + ) { + return; + } + + // fullHeaders seem empty sometimes, rebuild it + if (fullHeaders == null || fullHeaders.length() == 0) { + try { + if (date.length() > 0) { + Date parsedDate = dateParser.parse(date); + date = dateFormatter.format(parsedDate); + } + fullHeaders = "Skipped header\n" + getReceived() + + "MIME-Version: 1.0\n" + + "Content-Type: application/ms-tnef;\n" + + "\tname=\"winmail.dat\"\n" + + "Content-Transfer-Encoding: binary\n" + + "Content-class: " + contentClass + "\n" + + "Subject: " + MimeUtility.encodeText(subject) + "\n" + + "Date: " + date + "\n" + + "Message-ID: " + messageId + "\n" + + "Thread-Topic: " + MimeUtility.encodeText(threadTopic) + "\n" + + "Thread-Index: " + threadIndex + "\n" + + "From: " + from + "\n" + + "To: " + to + "\n"; + if (priority != null) { + fullHeaders += "X-Priority: " + priority + "\n"; + } + } catch (ParseException e) { + throw new RuntimeException("Unable to rebuild header " + e.getMessage()); + } catch (UnsupportedEncodingException e) { + throw new RuntimeException("Unable to rebuild header " + e.getMessage()); + } + } + StringBuffer result = new StringBuffer(); + boolean mstnefDetected = false; + String boundary = null; + try { + BufferedReader reader = new BufferedReader(new StringReader(fullHeaders)); + String line; + line = reader.readLine(); + while (line != null && line.length() > 0) { + // patch exchange Content type + if (line.equals(CONTENT_TYPE_HEADER + "application/ms-tnef;")) { + if (hasAttachment) { + boundary = "----_=_NextPart_001_" + uid; + String contentType = "multipart/mixed"; + // use multipart/related with inline images + if (htmlBody != null && htmlBody.indexOf("src=\"cid:") > 0) { + contentType = "multipart/related"; + } + line = CONTENT_TYPE_HEADER + contentType + ";\n\tboundary=\"" + boundary + "\""; + } else { + line = CONTENT_TYPE_HEADER + "text/html"; + } + // skip winmail.dat + reader.readLine(); + mstnefDetected = true; + + } else if (line.startsWith(CONTENT_TRANSFER_ENCODING_HEADER)) { + if (hasAttachment && mstnefDetected) { + line = null; + } + } + + if (line != null) { + result.append(line); + result.append("\n"); + } + line = reader.readLine(); + } + + // exchange message : create mime part headers + if (boundary != null) { + attachmentsMap = getAttachmentsUrls(messageUrl); + // TODO : test actual header values + result.append("\n--").append(boundary) + .append("\nContent-Type: text/html") + .append("\nContent-Transfer-Encoding: 7bit") + .append("\n\n"); + + for (String attachmentName : attachmentsMap.keySet()) { + // ignore indexed attachments + int parsedAttachmentIndex = 0; + try { + parsedAttachmentIndex = Integer.parseInt(attachmentName); + } catch (Exception e) {/* ignore */} + if (parsedAttachmentIndex == 0) { + String attachmentContentType = getAttachmentContentType(attachmentsMap.get(attachmentName)); + String attachmentContentEncoding = "base64"; + if (attachmentContentType.startsWith("text/")) { + attachmentContentEncoding = "quoted-printable"; + } else if (attachmentContentType.startsWith("message/rfc822")) { + attachmentContentEncoding = "7bit"; + } + + result.append("\n--").append(boundary) + .append("\nContent-Type: ") + .append(attachmentContentType) + .append(";") + .append("\n\tname=\"").append(attachmentName).append("\""); + int attachmentIdStartIndex = htmlBody.indexOf("cid:" + attachmentName); + if (attachmentIdStartIndex > 0) { + int attachmentIdEndIndex = htmlBody.indexOf('"', attachmentIdStartIndex); + if (attachmentIdEndIndex > 0) { + result.append("\nContent-ID: <") + .append(htmlBody.substring(attachmentIdStartIndex + 4, attachmentIdEndIndex)) + .append(">"); + } + } + result.append("\nContent-Transfer-Encoding: ").append(attachmentContentEncoding) + .append("\n\n"); + } + } + + // end parts + result.append("--").append(boundary).append("--\n"); + } + if (mstnefDetected) { + fullHeaders = result.toString(); + } + + } catch (IOException e) { + throw new RuntimeException("Unable to preprocess headers " + e.getMessage()); + } + } + + + public void write(OutputStream os) throws IOException { + // TODO : filter submessage headers in fullHeaders + BufferedReader reader = new BufferedReader(new StringReader(fullHeaders)); + // skip first line + reader.readLine(); + MimeHeader mimeHeader = new MimeHeader(); + mimeHeader.processHeaders(reader, os); + // non MIME message without attachments, append body + if (mimeHeader.boundary == null) { + os.write('\r'); + os.write('\n'); + writeBody(os, mimeHeader); + + if (hasAttachment) { + os.write("**warning : missing attachments**".getBytes()); + } + } else { + attachmentIndex = 0; + + attachmentsMap = getAttachmentsUrls(messageUrl); + writeMimeMessage(reader, os, mimeHeader, attachmentsMap); + } + os.flush(); + } + + public void writeMimeMessage(BufferedReader reader, OutputStream os, MimeHeader mimeHeader, Map attachmentsMap) throws IOException { + String line; + // with alternative, there are two body forms (plain+html) + if ("multipart/alternative".equals(mimeHeader.contentType)) { + attachmentIndex--; + } + + while (((line = reader.readLine()) != null) && !line.equals(mimeHeader.boundary + "--")) { + os.write(line.getBytes()); + os.write('\r'); + os.write('\n'); + + // detect part boundary start + if (line.equals(mimeHeader.boundary)) { + // process current part header + MimeHeader partHeader = new MimeHeader(); + partHeader.processHeaders(reader, os); + + // detect inner mime message + if (partHeader.contentType != null + && partHeader.contentType.startsWith("multipart") + && partHeader.boundary != null) { + writeMimeMessage(reader, os, partHeader, attachmentsMap); + } + // body part + else if (attachmentIndex <= 0) { + attachmentIndex++; + writeBody(os, partHeader); + } else { + String attachmentUrl = attachmentsMap.get(partHeader.name); + // try to get attachment by index, only if no name found + if (attachmentUrl == null && partHeader.name == null) { + attachmentUrl = attachmentsMap.get(String.valueOf(attachmentIndex)); + } + if (attachmentUrl == null) { + // only warn, could happen depending on IIS config + //throw new IOException("Attachment " + partHeader.name + " not found in " + messageUrl); + logger.warn("Attachment " + partHeader.name + " not found in " + messageUrl); + } else { + attachmentIndex++; + writeAttachment(os, partHeader, attachmentUrl); + } + } + } + } + // write mime end marker + if (line != null) { + os.write(line.getBytes()); + os.write('\r'); + os.write('\n'); + } + } + + protected void writeAttachment(OutputStream os, MimeHeader mimeHeader, String attachmentUrl) throws IOException { + try { + OutputStream quotedOs; + try { + quotedOs = (MimeUtility.encode(os, mimeHeader.contentTransferEncoding)); + } catch (MessagingException e) { + throw new IOException(e.getMessage()); + } + String decodedPath = URIUtil.decode(attachmentUrl); + + if ("message/rfc822".equals(mimeHeader.contentType)) { + // messages are not available at the attachment URL, but + // directly under the main message + String messageAttachmentPath = decodedPath; + int index = decodedPath.toLowerCase().lastIndexOf(".eml"); + if (index > 0) { + messageAttachmentPath = decodedPath.substring(0, index + 4); + } + + Message attachedMessage = getMessage(messageAttachmentPath); + attachedMessage.write(quotedOs); + } else { + + GetMethod method = new GetMethod(URIUtil.encodePathQuery(decodedPath)); + wdr.retrieveSessionInstance().executeMethod(method); + + // encode attachment + BufferedInputStream bis = new BufferedInputStream(method.getResponseBodyAsStream()); + byte[] buffer = new byte[4096]; + int count; + while ((count = bis.read(buffer)) >= 0) { + quotedOs.write(buffer, 0, count); + } + bis.close(); + quotedOs.flush(); + os.write('\r'); + os.write('\n'); + } + + } catch (HttpException e) { + throw new IOException(e.getMessage()); + } + } + + protected void writeBody(OutputStream os, MimeHeader mimeHeader) throws IOException { + OutputStream quotedOs; + try { + quotedOs = (MimeUtility.encode(os, mimeHeader.contentTransferEncoding)); + } catch (MessagingException e) { + throw new IOException(e.getMessage()); + } + String currentBody; + if ("text/html".equals(mimeHeader.contentType)) { + currentBody = htmlBody; + // patch charset if null and html body encoded + if (mimeHeader.charset == null) { + String delimiter = " 0) { + mimeHeader.charset = htmlBody.substring(startIndex, endIndex); + } + } + } + + } else { + currentBody = body; + } + if (mimeHeader.charset != null) { + try { + quotedOs.write(currentBody.getBytes(MimeUtility.javaCharset(mimeHeader.charset))); + } catch (UnsupportedEncodingException uee) { + // TODO : try to decode other encodings + quotedOs.write(currentBody.getBytes()); + } + } else { + quotedOs.write(currentBody.getBytes()); + } + quotedOs.flush(); + os.write('\r'); + os.write('\n'); + } + + public void delete() throws IOException { + // TODO : refactor + String destination = deleteditemsUrl + messageUrl.substring(messageUrl.lastIndexOf("/")); + logger.debug("Deleting : " + messageUrl + " to " + destination); +/* +// first try without webdav library + GetMethod moveMethod = new GetMethod(URIUtil.encodePathQuery(messageUrl)) { + public String getName() { + return "MOVE"; + } + }; + moveMethod.addRequestHeader("Destination", URIUtil.encodePathQuery(destination)); + moveMethod.addRequestHeader("Overwrite", "F"); + wdr.retrieveSessionInstance().executeMethod(moveMethod); + if (moveMethod.getStatusCode() == 412) { + int count = 2; + // name conflict, try another name + while (wdr.getStatusCode() == 412) { + moveMethod = new GetMethod(URIUtil.encodePathQuery(messageUrl)) { + public String getName() { + return "MOVE"; + } + }; + moveMethod.addRequestHeader("Destination", URIUtil.encodePathQuery(destination.substring(0, destination.lastIndexOf('.')) + "-" + count++ + ".eml")); + moveMethod.addRequestHeader("Overwrite", "F"); + } + + } + */ + wdr.moveMethod(messageUrl, destination); + if (wdr.getStatusCode() == 412) { + int count = 2; + // name conflict, try another name + while (wdr.getStatusCode() == 412) { + wdr.moveMethod(messageUrl, destination.substring(0, destination.lastIndexOf('.')) + "-" + count++ + ".eml"); + } + } + + logger.debug("Deleted to :" + destination + " " + wdr.getStatusCode() + " " + wdr.getStatusMessage()); + + } + + public void printHeaders(OutputStream os) throws IOException { + String line; + BufferedReader reader = new BufferedReader(new StringReader(fullHeaders)); + // skip first line + reader.readLine(); + line = reader.readLine(); + while (line != null && line.length() > 0) { + os.write(line.getBytes()); + os.write('\r'); + os.write('\n'); + line = reader.readLine(); + } + } + + /** + * Custom head method to force connection close (needed when attachment filtered by IIS) + */ + protected class ConnectionCloseHeadMethod extends HeadMethod { + public ConnectionCloseHeadMethod(String url) { + super(url); + } + + public boolean isConnectionCloseForced() { + // force connection if attachment not found + return getStatusCode() == 404; + } + + } + + protected String getAttachmentContentType(String attachmentUrl) { + String result; + try { + String decodedPath = URIUtil.decode(attachmentUrl); + logger.debug("Head " + decodedPath); + + ConnectionCloseHeadMethod method = new ConnectionCloseHeadMethod(URIUtil.encodePathQuery(decodedPath)); + wdr.retrieveSessionInstance().executeMethod(method); + if (method.getStatusCode() == 404) { + method.releaseConnection(); + System.err.println("Unable to retrieve attachment"); + } + result = method.getResponseHeader("Content-Type").getValue(); + method.releaseConnection(); + + } catch (Exception e) { + throw new RuntimeException("Exception retrieving " + attachmentUrl + " : " + e + " " + e.getCause()); + } + return result; + + } + + public Map getAttachmentsUrls(String messageUrl) throws IOException { + if (attachmentsMap != null) { + // do not load attachments twice + return attachmentsMap; + } else { + + GetMethod getMethod = new GetMethod(URIUtil.encodePathQuery(messageUrl + "?Cmd=Open")); + wdr.retrieveSessionInstance().executeMethod(getMethod); + if (getMethod.getStatusCode() != 200) { + throw new IOException("Unable to get attachments: " + getMethod.getStatusCode() + + " " + getMethod.getStatusLine()); + } + + InputStream in = getMethod.getResponseBodyAsStream(); + + Tidy tidy = new Tidy(); + tidy.setXmlTags(false); //treat input not XML + tidy.setQuiet(true); + tidy.setShowWarnings(false); + tidy.setDocType("omit"); + + DOMBuilder builder = new DOMBuilder(); + XmlDocument xmlDocument = new XmlDocument(); + try { + xmlDocument.load(builder.build(tidy.parseDOM(in, null))); + } catch (IOException ex1) { + ex1.printStackTrace(); + } catch (JDOMException ex1) { + ex1.printStackTrace(); + } + // Release the connection. + getMethod.releaseConnection(); + + Map attachmentsMap = new HashMap(); + int attachmentIndex = 2; + List list = xmlDocument.getNodes("//table[@id='idAttachmentWell']//a/@href"); + for (Attribute element : list) { + String attachmentHref = element.getValue(); + if (!"#".equals(attachmentHref)) { + final String ATTACH_QUERY = "?attach=1"; + if (attachmentHref.endsWith(ATTACH_QUERY)) { + attachmentHref = attachmentHref.substring(0, attachmentHref.length() - ATTACH_QUERY.length()); + } + // url is encoded + attachmentHref = URIUtil.decode(attachmentHref); + if (attachmentHref.startsWith(messageUrl)) { + String attachmentName = attachmentHref.substring(messageUrl.length() + 1); + int slashIndex = attachmentName.indexOf('/'); + if (slashIndex >= 0) { + attachmentName = attachmentName.substring(0, slashIndex); + } + // attachmentName is now right for Exchange message, need to handle external MIME messages + final String MULTIPART_STRING = "1_multipart_xF8FF_"; + if (attachmentName.startsWith(MULTIPART_STRING)) { + attachmentName = attachmentName.substring(MULTIPART_STRING.length()); + int underscoreIndex = attachmentName.indexOf('_'); + if (underscoreIndex >= 0) { + attachmentName = attachmentName.substring(underscoreIndex + 1); + } + } + // decode slashes + attachmentName = attachmentName.replaceAll("_xF8FF_", "/"); + + attachmentsMap.put(attachmentName, attachmentHref); + logger.debug("Attachment " + attachmentIndex + " : " + attachmentName); + attachmentsMap.put(String.valueOf(attachmentIndex++), attachmentHref); + } else { + logger.warn("Message URL : " + messageUrl + " is not a substring of attachment URL : " + attachmentHref); + } + } + } + + // get inline images + List imgList = xmlDocument.getNodes("//img/@src"); + for (Attribute element : imgList) { + String attachmentHref = element.getValue(); + if (attachmentHref.startsWith("1_multipart")) { + attachmentHref = URIUtil.decode(attachmentHref); + if (attachmentHref.endsWith("?Security=3")) { + attachmentHref = attachmentHref.substring(0, attachmentHref.indexOf('?')); + } + String attachmentName = attachmentHref.substring(attachmentHref.lastIndexOf('/') + 1); + if (attachmentName.charAt(1) == '_') { + attachmentName = attachmentName.substring(2); + } + // exclude inline external images + if (!attachmentHref.startsWith("http://") && !attachmentHref.startsWith("https://")) { + attachmentsMap.put(attachmentName, messageUrl + "/" + attachmentHref); + logger.debug("Inline image attachment " + attachmentIndex + " : " + attachmentName); + attachmentsMap.put(String.valueOf(attachmentIndex++), attachmentHref); + } + } + } + + + return attachmentsMap; + } + } + + } + + class MimeHeader { + + public String contentType = null; + public String charset = null; + public String contentTransferEncoding = null; + public String boundary = null; + public String name = null; + + public void processHeaders(BufferedReader reader, OutputStream os) throws IOException { + String line; + line = reader.readLine(); + while (line != null && line.length() > 0) { + + os.write(line.getBytes()); + os.write('\r'); + os.write('\n'); + + String lineToCompare = line.toLowerCase(); + + if (lineToCompare.startsWith(CONTENT_TYPE_HEADER)) { + String contentTypeHeader = line.substring(CONTENT_TYPE_HEADER.length()); + // handle multi-line header + StringBuffer header = new StringBuffer(contentTypeHeader); + while (line.trim().endsWith(";")) { + line = reader.readLine(); + header.append(line); + + os.write(line.getBytes()); + os.write('\r'); + os.write('\n'); + } + // decode header with accented file name (URL encoded) + int encodedIndex = header.indexOf("*="); + if (encodedIndex >= 0) { + StringBuffer decodedBuffer = new StringBuffer(); + decodedBuffer.append(header.substring(0, encodedIndex)); + decodedBuffer.append('='); + int encodedDataIndex = header.indexOf("''"); + if (encodedDataIndex >= 0) { + String encodedData = header.substring(encodedDataIndex + 2); + String encodedDataCharset = header.substring(encodedIndex + 2, encodedDataIndex); + decodedBuffer.append(URIUtil.decode(encodedData, encodedDataCharset)); + header = decodedBuffer; + } + } + StringTokenizer tokenizer = new StringTokenizer(header.toString(), ";"); + // first part is Content type + if (tokenizer.hasMoreTokens()) { + contentType = tokenizer.nextToken().trim(); + } + while (tokenizer.hasMoreTokens()) { + String token = tokenizer.nextToken().trim(); + int equalsIndex = token.indexOf('='); + if (equalsIndex > 0) { + String tokenName = token.substring(0, equalsIndex); + if ("charset".equals(tokenName)) { + charset = token.substring(equalsIndex + 1); + if (charset.startsWith("\"")) { + charset = charset.substring(1, charset.lastIndexOf("\"")); + } + } else if ("name".equals(tokenName.toLowerCase())) { + name = token.substring(equalsIndex + 1); + if (name.startsWith("\"")) { + name = name.substring(1, name.lastIndexOf("\"")); + } + } else if ("boundary".equals(tokenName)) { + boundary = token.substring(equalsIndex + 1); + if (boundary.startsWith("\"")) { + boundary = boundary.substring(1, boundary.lastIndexOf("\"")); + } + boundary = "--" + boundary; + } + } + } + } else if (lineToCompare.startsWith(CONTENT_TRANSFER_ENCODING_HEADER)) { + contentTransferEncoding = line.substring(CONTENT_TRANSFER_ENCODING_HEADER.length()).trim(); + } + line = reader.readLine(); + } + os.write('\r'); + os.write('\n'); + } + } +} diff --git a/src/java/davmail/exchange/XmlDocument.java b/src/java/davmail/exchange/XmlDocument.java new file mode 100644 index 00000000..e6b7a9d0 --- /dev/null +++ b/src/java/davmail/exchange/XmlDocument.java @@ -0,0 +1,139 @@ +package davmail.exchange; + +import java.io.*; +import java.util.List; + +import org.jdom.*; +import org.jdom.input.SAXBuilder; +import org.jdom.output.Format; +import org.jdom.output.XMLOutputter; +import org.jdom.xpath.XPath; + +public class XmlDocument { + private Document document; + + public String getAttribute(String xpath) { + String result = null; + try { + XPath xpathExpr = XPath.newInstance(xpath); + Attribute attribute = (Attribute) xpathExpr.selectSingleNode( + document); + if (attribute != null) { + result = attribute.getValue(); + } + } catch (JDOMException ex) { + // TODO handle exception + } + + return result; + } + + public String getValue(String xpath) { + String result = null; + try { + XPath xpathExpr = XPath.newInstance(xpath); + Element element = (Element) xpathExpr.selectSingleNode(document); + if (element != null) { + result = element.getText(); + } + } catch (JDOMException ex) { + // TODO handle exception + } + + return result; + } + + public String getXmlValue(String xpath) { + String result = null; + try { + XPath xpathExpr = XPath.newInstance(xpath); + Element element = (Element) xpathExpr.selectSingleNode(document); + if (element != null) { + XMLOutputter outputter = new XMLOutputter(); + StringWriter xmlWriter = new StringWriter(); + outputter.output(element, xmlWriter); + result = xmlWriter.toString(); + } + } catch (IOException ex) { + // TODO handle exception + } catch (JDOMException ex) { + // TODO handle exception + } + + return result; + } + + public List getContent(String xpath) { + List result = null; + XPath xpathExpr; + try { + xpathExpr = XPath.newInstance(xpath); + Element element = (Element) xpathExpr.selectSingleNode(document); + if (element != null) { + result = element.getContent(); + } + } catch (JDOMException ex) { + // TODO handle exception + } + return result; + } + + public List getNodes(String xpath) { + List result = null; + try { + XPath xpathExpr = XPath.newInstance(xpath); + result = xpathExpr.selectNodes(document); + } catch (JDOMException ex) { + // TODO handle exception + } + return result; + } + + public String toString() { + if (document == null) { + return null; + } + StringWriter writer = new StringWriter(); + + XMLOutputter outputter = new XMLOutputter(); + outputter.setFormat(Format.getPrettyFormat()); + try { + outputter.output(document, writer); + } catch (IOException ex) { + // TODO : handle exception + } + return writer.toString(); + } + + public XmlDocument() { + } + + public void load(String location) throws JDOMException, IOException { + document = new SAXBuilder().build(location); + } + + public void load(InputStream stream, String dtd) throws JDOMException, IOException { + document = new SAXBuilder().build(stream, dtd); + } + + public void load(Document value) throws JDOMException, IOException { + document = value; + } + + public String toString(Element element) { + if (document == null) { + return null; + } + StringWriter writer = new StringWriter(); + + XMLOutputter outputter = new XMLOutputter(); + outputter.setFormat(Format.getPrettyFormat()); + try { + outputter.output(element, writer); + } catch (IOException ex) { + // TODO: handle Exception + } + return writer.toString(); + } + +} diff --git a/src/java/davmail/imap/ImapConnection.java b/src/java/davmail/imap/ImapConnection.java new file mode 100644 index 00000000..41d8fdfc --- /dev/null +++ b/src/java/davmail/imap/ImapConnection.java @@ -0,0 +1,185 @@ +package davmail.imap; + +import java.net.Socket; +import java.util.StringTokenizer; +import java.util.List; +import java.io.IOException; + +import davmail.AbstractConnection; +import davmail.DavGatewayTray; +import davmail.exchange.ExchangeSession; + +/** + * Dav Gateway smtp connection implementation. + * Still alpha code : need to find a way to handle message ids + */ +public class ImapConnection extends AbstractConnection { + protected static final int INITIAL = 0; + protected static final int AUTHENTICATED = 1; + + // Initialize the streams and start the thread + public ImapConnection(String url, Socket clientSocket) { + super(url, clientSocket); + } + + public void run() { + String line; + StringTokenizer tokens; + try { + sendClient("* OK Davmail Imap Server ready"); + for (; ;) { + line = readClient(); + // unable to read line, connection closed ? + if (line == null) { + break; + } + + tokens = new StringTokenizer(line); + if (tokens.hasMoreTokens()) { + String commandId = tokens.nextToken(); + if (tokens.hasMoreTokens()) { + String command = tokens.nextToken(); + + if ("LOGOUT".equalsIgnoreCase(command)) { + sendClient("* BYE Closing connection"); + sendClient(commandId + " OK Completed"); + break; + } + if ("capability".equalsIgnoreCase(command)) { + sendClient("* CAPABILITY IMAP4REV1"); + sendClient(commandId + " OK CAPABILITY completed"); + } else if ("login".equalsIgnoreCase(command)) { + parseCredentials(tokens); + session = new ExchangeSession(url); + try { + session.login(userName, password); + sendClient(commandId + " OK Authenticated"); + state = AUTHENTICATED; + } catch (Exception e) { + DavGatewayTray.error("Authentication failed",e); + sendClient(commandId + " NO LOGIN failed"); + state = INITIAL; + } + } else { + if (state != AUTHENTICATED) { + sendClient(commandId + " BAD command authentication required"); + } else { + if ("lsub".equalsIgnoreCase(command)) { + /* TODO : implement + 2 lsub "" "*" +* LSUB () "/" INBOX/sent-mail +* LSUB () "/" Trash +* LSUB () "/" INBOX/spam +* LSUB () "/" Envoy&AOk-s +* LSUB () "/" Drafts +2 OK LSUB completed + */ + sendClient(commandId + " OK LSUB completed"); + } else if ("list".equalsIgnoreCase(command)) { + /* TODO : implement + */ + sendClient(commandId + " OK LIST completed"); + } else if ("select".equalsIgnoreCase(command)) { + if (tokens.hasMoreTokens()) { + String folderName = removeQuotes(tokens.nextToken()); + ExchangeSession.Folder folder = session.selectFolder(folderName); + sendClient("* " + folder.childCount + " EXISTS"); + sendClient("* " + folder.unreadCount + " RECENT"); + // TODO : implement, compute session message ids + //sendClient("* [UNSEEN 1] first unseen message in inbox"); + sendClient(commandId + " OK [READ-WRITE] SELECT completed"); + } else { + sendClient(commandId + " BAD command unrecognized"); + } + } else if ("uid".equalsIgnoreCase(command)) { + if (tokens.hasMoreTokens() && "fetch".equalsIgnoreCase(tokens.nextToken())) { + if (tokens.hasMoreTokens()) { + String parameter = tokens.nextToken(); + if ("1:*".equals(parameter)) { + List messages = session.getAllMessages(); + for (ExchangeSession.Message message : messages) { + sendClient("* FETCH (UID " + message.uid + " FLAGS ())"); + } + sendClient(commandId + " OK UID FETCH completed"); + } else { + sendClient(commandId + " BAD command unrecognized"); + } + } else { + sendClient(commandId + " BAD command unrecognized"); + } + } else { + sendClient(commandId + " BAD command unrecognized"); + } + } else { + sendClient(commandId + " BAD command unrecognized"); + } + } + } + + } else { + sendClient(commandId + " BAD missing command"); + } + } else { + sendClient("BAD Null command"); + } + } + + + os.flush(); + } catch (IOException e) { + DavGatewayTray.error("Exception handling client",e); + } finally { + try { + client.close(); + } catch (IOException e2) { + DavGatewayTray.debug("Exception closing client",e2); + } + try { + if (session != null) { + session.close(); + } + } catch (IOException e3) { + DavGatewayTray.debug("Exception closing gateway",e3); + } + + } + DavGatewayTray.resetIcon(); + } + + /** + * Decode SMTP credentials + */ + protected void parseCredentials(StringTokenizer tokens) throws IOException { + if (tokens.hasMoreTokens()) { + userName = removeQuotes(tokens.nextToken()); + } else { + throw new IOException("Invalid credentials"); + } + + if (tokens.hasMoreTokens()) { + password = removeQuotes(tokens.nextToken()); + } else { + throw new IOException("Invalid credentials"); + } + int backslashindex = userName.indexOf("\\"); + if (backslashindex > 0) { + userName = userName.substring(0, backslashindex) + userName.substring(backslashindex + 1); + } + } + + protected String removeQuotes(String value) { + String result = value; + if (result.startsWith("\"")) { + result = result.substring(1); + } + if (result.endsWith("\"")) { + result = result.substring(0, result.length() - 1); + } + return result; + } + + public void sendMessage(StringBuffer buffer) throws Exception { + // TODO implement + } +} + diff --git a/src/java/davmail/imap/ImapServer.java b/src/java/davmail/imap/ImapServer.java new file mode 100644 index 00000000..5ed0e8d7 --- /dev/null +++ b/src/java/davmail/imap/ImapServer.java @@ -0,0 +1,26 @@ +package davmail.imap; + + +import java.net.Socket; + +import davmail.AbstractServer; + +/** + * Pop3 server + */ +public class ImapServer extends AbstractServer { + public final static int DEFAULT_PORT = 143; + + /** + * Create a ServerSocket to listen for connections. + * Start the thread. + */ + public ImapServer(String url, int port) { + super(url, (port == 0) ? ImapServer.DEFAULT_PORT : port); + } + + public void createConnectionHandler(String url, Socket clientSocket) { + new ImapConnection(url, clientSocket); + } + +} diff --git a/src/java/davmail/pop/PopConnection.java b/src/java/davmail/pop/PopConnection.java new file mode 100644 index 00000000..a971ca28 --- /dev/null +++ b/src/java/davmail/pop/PopConnection.java @@ -0,0 +1,212 @@ +package davmail.pop; + +import davmail.AbstractConnection; +import davmail.DavGatewayTray; +import davmail.exchange.ExchangeSession; + +import java.net.Socket; +import java.net.UnknownHostException; +import java.util.*; +import java.io.IOException; + +/** + * Dav Gateway pop connection implementation + */ +public class PopConnection extends AbstractConnection { + protected static final int INITIAL = 0; + protected static final int USER = 1; + protected static final int AUTHENTICATED = 2; + private List messages; + + // Initialize the streams and start the thread + public PopConnection(String url, Socket clientSocket) { + super(url, clientSocket); + } + + public long getTotalMessagesLength() { + int result = 0; + for (ExchangeSession.Message message : messages) { + result += message.size; + } + return result; + } + + public void printList() throws IOException { + int i = 1; + for (ExchangeSession.Message message : messages) { + sendClient(i++ + " " + message.size); + } + sendClient("."); + } + + public void printUidList() throws IOException { + int i = 1; + for (ExchangeSession.Message message : messages) { + sendClient(i++ + " " + message.uid); + } + sendClient("."); + } + + + public void run() { + String line; + StringTokenizer tokens; + + try { + sendOK("DavMail POP ready at " + new Date()); + + for (; ;) { + line = readClient(); + // unable to read line, connection closed ? + if (line == null) { + break; + } + + + tokens = new StringTokenizer(line); + if (tokens.hasMoreTokens()) { + String command = tokens.nextToken(); + + if ("QUIT".equalsIgnoreCase(command)) { + sendOK("Bye"); + break; + } else if ("USER".equalsIgnoreCase(command)) { + userName = null; + password = null; + session = null; + if (tokens.hasMoreTokens()) { + userName = tokens.nextToken(); + sendOK("USER : " + userName); + state = USER; + } else { + sendERR("invalid syntax"); + state = INITIAL; + } + } else if ("PASS".equalsIgnoreCase(command)) { + if (state != USER) { + sendERR("invalid state"); + state = INITIAL; + } else if (!tokens.hasMoreTokens()) { + sendERR("invalid syntax"); + } else { + password = tokens.nextToken(); + try { + session = new ExchangeSession(url); + session.login(userName, password); + messages = session.getAllMessages(); + sendOK("PASS"); + state = AUTHENTICATED; + } catch (UnknownHostException e) { + DavGatewayTray.error("Connection failed",e); + sendERR("Connection failed : "+e+" "+e.getMessage()); + // close connection + break; + } catch (Exception e) { + DavGatewayTray.error("Authentication failed",e); + sendERR("authentication failed : "+e+" "+e.getMessage()); + } + } + } else if ("CAPA".equalsIgnoreCase(command)) { + sendERR("unknown command"); + } else if (state != AUTHENTICATED) { + sendERR("invalid state not authenticated"); + } else { + if ("STAT".equalsIgnoreCase(command)) { + sendOK(messages.size() + " " + + getTotalMessagesLength()); + } else if ("LIST".equalsIgnoreCase(command)) { + sendOK(messages.size() + + " messages (" + getTotalMessagesLength() + + " octets)"); + printList(); + } else if ("UIDL".equalsIgnoreCase(command)) { + sendOK(messages.size() + + " messages (" + getTotalMessagesLength() + + " octets)"); + printUidList(); + } else if ("RETR".equalsIgnoreCase(command)) { + if (tokens.hasMoreTokens()) { + try { + int messageNumber = Integer.valueOf(tokens. + nextToken()) - 1; + sendOK(""); + messages.get(messageNumber).write(os); + sendClient(""); + sendClient("."); + } catch (Exception e) { + DavGatewayTray.error("Error retreiving message",e); + sendERR("error retreiving message " + e.getMessage()); + } + } else { + sendERR("invalid message number"); + } + } else if ("DELE".equalsIgnoreCase(command)) { + if (tokens.hasMoreTokens()) { + try { + int messageNumber = Integer.valueOf(tokens. + nextToken()) - 1; + messages.get(messageNumber).delete(); + sendOK("DELETE"); + } catch (Exception e) { + DavGatewayTray.error("Error deleting message",e); + sendERR("error deleting message"); + } + } else { + sendERR("invalid message number"); + } + } else if ("TOP".equalsIgnoreCase(command)) { + try { + int firstMessage = Integer.valueOf(tokens. + nextToken()) - 1; + int lastMessage = Integer.valueOf(tokens. + nextToken()) - 1; + for (int i = firstMessage; i <= lastMessage; i++) { + messages.get(i).printHeaders(os); + sendClient(""); + sendClient("."); + } + sendOK("TOP"); + } catch (Exception e) { + sendERR("error retreiving top of messages"); + } + } else if ("RSET".equalsIgnoreCase(command)) { + sendOK("RSET"); + } else { + sendERR("unknown command"); + } + } + + } else { + sendERR("unknown command"); + } + + os.flush(); + } + } catch (IOException e) { + DavGatewayTray.error("Exception handling client",e); + } finally { + try { + client.close(); + } catch (IOException e2) { + DavGatewayTray.debug("Exception closing client",e2); + } + try { + if (session != null) { + session.close(); + } + } catch (IOException e3) { + DavGatewayTray.debug("Exception closing gateway",e3); + } + + } + DavGatewayTray.resetIcon(); + } + + public void sendOK(String message) throws IOException { + sendClient("+OK ", message); + } + + public void sendERR(String message) throws IOException { + sendClient("-ERR ", message); + } +} diff --git a/src/java/davmail/pop/PopServer.java b/src/java/davmail/pop/PopServer.java new file mode 100644 index 00000000..3bb2794e --- /dev/null +++ b/src/java/davmail/pop/PopServer.java @@ -0,0 +1,26 @@ +package davmail.pop; + + +import davmail.AbstractServer; + +import java.net.Socket; + +/** + * Pop3 server + */ +public class PopServer extends AbstractServer { + public final static int DEFAULT_PORT = 110; + + /** + * Create a ServerSocket to listen for connections. + * Start the thread. + */ + public PopServer(String url, int port) { + super(url, (port == 0) ? PopServer.DEFAULT_PORT : port); + } + + public void createConnectionHandler(String url, Socket clientSocket) { + new PopConnection(url, clientSocket); + } + +} diff --git a/src/java/davmail/smtp/SmtpConnection.java b/src/java/davmail/smtp/SmtpConnection.java new file mode 100644 index 00000000..eb051a65 --- /dev/null +++ b/src/java/davmail/smtp/SmtpConnection.java @@ -0,0 +1,158 @@ +package davmail.smtp; + +import davmail.AbstractConnection; +import davmail.DavGatewayTray; +import davmail.exchange.ExchangeSession; +import sun.misc.BASE64Decoder; + +import java.io.IOException; +import java.net.Socket; +import java.util.Date; +import java.util.StringTokenizer; + +/** + * Dav Gateway smtp connection implementation + */ +public class SmtpConnection extends AbstractConnection { + protected static final int INITIAL = 0; + protected static final int AUTHENTICATED = 1; + protected static final int STARTMAIL = 2; + protected static final int RECIPIENT = 3; + protected static final int MAILDATA = 4; + + // Initialize the streams and start the thread + public SmtpConnection(String url, Socket clientSocket) { + super(url, clientSocket); + } + + public void run() { + String line; + StringTokenizer tokens; + + + try { + sendClient("220 DavMail SMTP ready at " + new Date()); + for (; ;) { + line = readClient(); + // unable to read line, connection closed ? + if (line == null) { + break; + } + + tokens = new StringTokenizer(line); + if (tokens.hasMoreTokens()) { + String command = tokens.nextToken(); + + if ("QUIT".equalsIgnoreCase(command)) { + sendClient("221 Closing connection"); + break; + } else if ("EHLO".equals(command)) { + // inform server that AUTH is supported + // actually it is mandatory (only way to get credentials) + sendClient("250-AUTH LOGIN PLAIN"); + sendClient("250 Hello"); + } else if ("HELO".equals(command)) { + sendClient("250 Hello"); + } else if ("AUTH".equals(command)) { + if (tokens.hasMoreElements()) { + String authType = tokens.nextToken(); + if ("PLAIN".equals(authType) && tokens.hasMoreElements()) { + decodeCredentials(tokens.nextToken()); + session = new ExchangeSession(url); + try { + session.login(userName, password); + sendClient("235 OK Authenticated"); + state = AUTHENTICATED; + } catch (Exception e) { + DavGatewayTray.warn("Authentication failed",e); + sendClient("554 Authenticated failed"); + state = INITIAL; + } + } else { + sendClient("451 Error : unknown authentication type"); + } + } else { + sendClient("451 Error : authentication type not specified"); + } + } else if ("MAIL".equals(command)) { + if (state == AUTHENTICATED) { + state = STARTMAIL; + sendClient("250 Sender OK"); + } else { + state = INITIAL; + sendClient("503 Bad sequence of commands"); + } + } else if ("RCPT".equals(command)) { + if (state == STARTMAIL || state == RECIPIENT) { + state = RECIPIENT; + sendClient("250 Recipient OK"); + } else { + state = AUTHENTICATED; + sendClient("503 Bad sequence of commands"); + } + } else if ("DATA".equals(command)) { + if (state == RECIPIENT) { + state = MAILDATA; + sendClient("354 Start mail input; end with ."); + + try { + session.sendMessage(in); + state = AUTHENTICATED; + sendClient("250 Queued mail for delivery"); + } catch (Exception e) { + DavGatewayTray.error("Authentication failed",e); + state = AUTHENTICATED; + sendClient("451 Error : " + e.getMessage()); + } + + } else { + state = AUTHENTICATED; + sendClient("503 Bad sequence of commands"); + } + } + + } else { + sendClient("500 Unrecognized command"); + } + + os.flush(); + } + } catch (IOException e) { + DavGatewayTray.error("Exception handling client",e); + } finally { + try { + client.close(); + } catch (IOException e2) { + DavGatewayTray.debug("Exception closing client",e2); + } + try { + if (session != null) { + session.close(); + } + } catch (IOException e3) { + DavGatewayTray.debug("Exception closing gateway",e3); + } + } + DavGatewayTray.resetIcon(); + } + + /** + * Decode SMTP credentials + * + * @param encodedCredentials smtp encoded credentials + * @throws java.io.IOException + */ + protected void decodeCredentials(String encodedCredentials) throws IOException { + BASE64Decoder decoder = new BASE64Decoder(); + String decodedCredentials = new String(decoder.decodeBuffer(encodedCredentials)); + int index = decodedCredentials.indexOf((char) 0, 1); + if (index > 0) { + userName = decodedCredentials.substring(1, index); + password = decodedCredentials.substring(index + 1); + } else { + throw new IOException("Invalid credentials"); + } + } + +} + diff --git a/src/java/davmail/smtp/SmtpServer.java b/src/java/davmail/smtp/SmtpServer.java new file mode 100644 index 00000000..218d3472 --- /dev/null +++ b/src/java/davmail/smtp/SmtpServer.java @@ -0,0 +1,22 @@ +package davmail.smtp; + +import java.net.Socket; + +import davmail.AbstractServer; + +public class SmtpServer extends AbstractServer { + public final static int DEFAULT_PORT = 25; + + /** + * Create a ServerSocket to listen for connections. + * Start the thread. + */ + public SmtpServer(String url, int port) { + super(url, (port == 0) ? SmtpServer.DEFAULT_PORT : port); + } + + public void createConnectionHandler(String url, Socket clientSocket) { + new SmtpConnection(url, clientSocket); + } + +} diff --git a/src/java/log4j.properties b/src/java/log4j.properties new file mode 100644 index 00000000..a250a82b --- /dev/null +++ b/src/java/log4j.properties @@ -0,0 +1,22 @@ +# Set root logger level to DEBUG and its only appender to ConsoleAppender. +log4j.rootLogger=DEBUG, ConsoleAppender, FileAppender + +log4j.logger.httpclient.wire=WARN, ConsoleAppender +log4j.logger.org.apache.commons.httpclient=WARN, ConsoleAppender + +# ConsoleAppender is set to be a ConsoleAppender. +log4j.appender.ConsoleAppender=org.apache.log4j.ConsoleAppender + +# ConsoleAppender uses PatternLayout. +log4j.appender.ConsoleAppender.layout=org.apache.log4j.PatternLayout +log4j.appender.ConsoleAppender.layout.ConversionPattern=%-4r [%t] %-5p %c %x - %m%n + +#log4j.appender.FileAppender=org.apache.log4j.FileAppender +log4j.appender.FileAppender=org.apache.log4j.DailyRollingFileAppender +log4j.appender.FileAppender.DatePattern='.'yyyy-MM-dd + +# Path and name of log file +log4j.appender.FileAppender.File=davmail.log +# ConsoleAppender uses PatternLayout. +log4j.appender.FileAppender.layout=org.apache.log4j.PatternLayout +log4j.appender.FileAppender.layout.ConversionPattern=%d{ISO8601} - %m%n \ No newline at end of file diff --git a/src/java/tray.png b/src/java/tray.png new file mode 100644 index 00000000..9d3f243a Binary files /dev/null and b/src/java/tray.png differ diff --git a/src/java/tray2.png b/src/java/tray2.png new file mode 100644 index 00000000..91684279 Binary files /dev/null and b/src/java/tray2.png differ