davmail/src/java/davmail/exchange/ExchangeSession.java

3613 lines
136 KiB
Java

/*
* DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway
* Copyright (C) 2009 Mickael Guessant
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
package davmail.exchange;
import davmail.BundleMessage;
import davmail.Settings;
import davmail.exception.DavMailAuthenticationException;
import davmail.exception.DavMailException;
import davmail.exception.WebdavNotAvailableException;
import davmail.http.DavGatewayHttpClientFacade;
import davmail.http.DavGatewayOTPPrompt;
import davmail.ui.NotificationDialog;
import davmail.util.StringUtil;
import org.apache.commons.httpclient.*;
import org.apache.commons.httpclient.methods.GetMethod;
import org.apache.commons.httpclient.methods.PostMethod;
import org.apache.commons.httpclient.params.HttpClientParams;
import org.apache.commons.httpclient.util.URIUtil;
import org.apache.log4j.Logger;
import org.htmlcleaner.CommentNode;
import org.htmlcleaner.ContentNode;
import org.htmlcleaner.HtmlCleaner;
import org.htmlcleaner.TagNode;
import javax.imageio.ImageIO;
import javax.mail.MessagingException;
import javax.mail.internet.*;
import javax.mail.util.SharedByteArrayInputStream;
import java.awt.image.BufferedImage;
import java.io.*;
import java.net.ConnectException;
import java.net.NoRouteToHostException;
import java.net.UnknownHostException;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.*;
/**
* Exchange session through Outlook Web Access (DAV)
*/
public abstract class ExchangeSession {
protected static final Logger LOGGER = Logger.getLogger("davmail.exchange.ExchangeSession");
/**
* Reference GMT timezone to format dates
*/
public static final SimpleTimeZone GMT_TIMEZONE = new SimpleTimeZone(0, "GMT");
protected static final Set<String> USER_NAME_FIELDS = new HashSet<String>();
static {
USER_NAME_FIELDS.add("username");
USER_NAME_FIELDS.add("txtUserName");
USER_NAME_FIELDS.add("userid");
USER_NAME_FIELDS.add("SafeWordUser");
USER_NAME_FIELDS.add("user_name");
}
protected static final Set<String> PASSWORD_FIELDS = new HashSet<String>();
static {
PASSWORD_FIELDS.add("password");
PASSWORD_FIELDS.add("txtUserPass");
PASSWORD_FIELDS.add("pw");
PASSWORD_FIELDS.add("basicPassword");
}
protected static final Set<String> TOKEN_FIELDS = new HashSet<String>();
static {
TOKEN_FIELDS.add("SafeWordPassword");
TOKEN_FIELDS.add("passcode");
}
protected static final int FREE_BUSY_INTERVAL = 15;
protected static final String PUBLIC_ROOT = "/public/";
protected static final String CALENDAR = "calendar";
protected static final String TASKS = "tasks";
/**
* Contacts folder logical name
*/
public static final String CONTACTS = "contacts";
protected static final String ADDRESSBOOK = "addressbook";
protected static final String INBOX = "INBOX";
protected static final String LOWER_CASE_INBOX = "inbox";
protected static final String SENT = "Sent";
protected static final String SENDMSG = "##DavMailSubmissionURI##";
protected static final String DRAFTS = "Drafts";
protected static final String TRASH = "Trash";
protected static final String JUNK = "Junk";
protected static final String UNSENT = "Unsent Messages";
static {
// Adjust Mime decoder settings
System.setProperty("mail.mime.ignoreunknownencoding", "true");
System.setProperty("mail.mime.decodetext.strict", "false");
}
protected String publicFolderUrl;
/**
* Base user mailboxes path (used to select folder)
*/
protected String mailPath;
protected String rootPath;
protected String email;
protected String alias;
/**
* Lower case Caldav path to current user mailbox.
* /users/<i>email</i>
*/
protected String currentMailboxPath;
protected final HttpClient httpClient;
protected String userName;
/**
* A OTP pre-auth page may require a different username.
*/
private String preAuthUsername;
protected String serverVersion;
protected static final String YYYY_MM_DD_HH_MM_SS = "yyyy/MM/dd HH:mm:ss";
private static final String YYYYMMDD_T_HHMMSS_Z = "yyyyMMdd'T'HHmmss'Z'";
protected static final String YYYY_MM_DD_T_HHMMSS_Z = "yyyy-MM-dd'T'HH:mm:ss'Z'";
private static final String YYYY_MM_DD = "yyyy-MM-dd";
private static final String YYYY_MM_DD_T_HHMMSS_SSS_Z = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'";
/**
* Logon form user name fields.
*/
private final List<String> userNameInputs = new ArrayList<String>();
/**
* Logon form password field, default is password.
*/
private String passwordInput = null;
/**
* Tells if, during the login navigation, an OTP pre-auth page has been found.
*/
private boolean otpPreAuthFound = false;
/**
* Lets the user try again a couple of times to enter the OTP pre-auth key before giving up.
*/
private int otpPreAuthRetries = 0;
/**
* Maximum number of times the user can try to input again the OTP pre-auth key before giving up.
*/
private static final int MAX_OTP_RETRIES = 3;
/**
* Create an exchange session for the given URL.
* The session is established for given userName and password
*
* @param url Exchange url
* @param userName user login name
* @param password user password
* @throws IOException on error
*/
public ExchangeSession(String url, String userName, String password) throws IOException {
this.userName = userName;
try {
httpClient = DavGatewayHttpClientFacade.getInstance(url);
// set private connection pool
DavGatewayHttpClientFacade.createMultiThreadedHttpConnectionManager(httpClient);
boolean isBasicAuthentication = isBasicAuthentication(httpClient, url);
// clear cookies created by authentication test
httpClient.getState().clearCookies();
// The user may have configured an OTP pre-auth username. It is processed
// so early because OTP pre-auth may disappear in the Exchange LAN and this
// helps the user to not change is account settings in mail client at each network change.
if (preAuthUsername == null) {
// Searches for the delimiter in configured username for the pre-auth user.
// The double-quote is not allowed inside email addresses anyway.
int doubleQuoteIndex = this.userName.indexOf('"');
if (doubleQuoteIndex > 0) {
preAuthUsername = this.userName.substring(0, doubleQuoteIndex);
this.userName = this.userName.substring(doubleQuoteIndex + 1);
} else {
// No doublequote: the pre-auth user is the full username, or it is not used at all.
preAuthUsername = this.userName;
}
}
DavGatewayHttpClientFacade.setCredentials(httpClient, userName, password);
// get webmail root url
// providing credentials
// manually follow redirect
HttpMethod method = DavGatewayHttpClientFacade.executeFollowRedirects(httpClient, url);
if (!this.isAuthenticated(method)) {
if (isBasicAuthentication) {
int status = method.getStatusCode();
if (status == HttpStatus.SC_UNAUTHORIZED) {
method.releaseConnection();
throw new DavMailAuthenticationException("EXCEPTION_AUTHENTICATION_FAILED");
} else if (status != HttpStatus.SC_OK) {
method.releaseConnection();
throw DavGatewayHttpClientFacade.buildHttpException(method);
}
// workaround for basic authentication on /exchange and form based authentication at /owa
if ("/owa/auth/logon.aspx".equals(method.getPath())) {
method = formLogin(httpClient, method, userName, password);
}
} else {
method = formLogin(httpClient, method, userName, password);
}
}
// avoid 401 roundtrips, only if NTLM is disabled and basic authentication enabled
if (isBasicAuthentication && !DavGatewayHttpClientFacade.hasNTLMorNegotiate(httpClient)) {
httpClient.getParams().setParameter(HttpClientParams.PREEMPTIVE_AUTHENTICATION, true);
}
buildSessionInfo(method);
} catch (DavMailAuthenticationException exc) {
LOGGER.error(exc.getMessage());
throw exc;
} catch (ConnectException exc) {
BundleMessage message = new BundleMessage("EXCEPTION_CONNECT", exc.getClass().getName(), exc.getMessage());
ExchangeSession.LOGGER.error(message);
throw new DavMailException("EXCEPTION_DAVMAIL_CONFIGURATION", message);
} catch (UnknownHostException exc) {
BundleMessage message = new BundleMessage("EXCEPTION_CONNECT", exc.getClass().getName(), exc.getMessage());
ExchangeSession.LOGGER.error(message);
throw new DavMailException("EXCEPTION_DAVMAIL_CONFIGURATION", message);
} catch (WebdavNotAvailableException exc) {
throw exc;
} catch (IOException exc) {
LOGGER.error(BundleMessage.formatLog("EXCEPTION_EXCHANGE_LOGIN_FAILED", exc));
throw new DavMailException("EXCEPTION_EXCHANGE_LOGIN_FAILED", exc);
}
LOGGER.debug("Session " + this + " created");
}
/**
* Format date to exchange search format.
*
* @param date date object
* @return formatted search date
*/
public abstract String formatSearchDate(Date date);
/**
* Return standard zulu date formatter.
*
* @return zulu date formatter
*/
public static SimpleDateFormat getZuluDateFormat() {
SimpleDateFormat dateFormat = new SimpleDateFormat(YYYYMMDD_T_HHMMSS_Z, Locale.ENGLISH);
dateFormat.setTimeZone(GMT_TIMEZONE);
return dateFormat;
}
protected static SimpleDateFormat getVcardBdayFormat() {
SimpleDateFormat dateFormat = new SimpleDateFormat(YYYY_MM_DD, Locale.ENGLISH);
dateFormat.setTimeZone(GMT_TIMEZONE);
return dateFormat;
}
protected static SimpleDateFormat getExchangeZuluDateFormat() {
SimpleDateFormat dateFormat = new SimpleDateFormat(YYYY_MM_DD_T_HHMMSS_Z, Locale.ENGLISH);
dateFormat.setTimeZone(GMT_TIMEZONE);
return dateFormat;
}
protected static SimpleDateFormat getExchangeZuluDateFormatMillisecond() {
SimpleDateFormat dateFormat = new SimpleDateFormat(YYYY_MM_DD_T_HHMMSS_SSS_Z, Locale.ENGLISH);
dateFormat.setTimeZone(GMT_TIMEZONE);
return dateFormat;
}
protected static Date parseDate(String dateString) throws ParseException {
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMdd");
dateFormat.setTimeZone(GMT_TIMEZONE);
return dateFormat.parse(dateString);
}
/**
* Test if the session expired.
*
* @return true this session expired
* @throws NoRouteToHostException on error
* @throws UnknownHostException on error
*/
public boolean isExpired() throws NoRouteToHostException, UnknownHostException {
boolean isExpired = false;
try {
getFolder("");
} catch (UnknownHostException exc) {
throw exc;
} catch (NoRouteToHostException exc) {
throw exc;
} catch (IOException e) {
isExpired = true;
}
return isExpired;
}
/**
* Test authentication mode : form based or basic.
*
* @param url exchange base URL
* @param httpClient httpClient instance
* @return true if basic authentication detected
*/
protected boolean isBasicAuthentication(HttpClient httpClient, String url) {
return DavGatewayHttpClientFacade.getHttpStatus(httpClient, url) == HttpStatus.SC_UNAUTHORIZED;
}
protected String getAbsoluteUri(HttpMethod method, String path) throws URIException {
URI uri = method.getURI();
if (path != null) {
// reset query string
uri.setQuery(null);
if (path.startsWith("/")) {
// path is absolute, replace method path
uri.setPath(path);
} else if (path.startsWith("http://") || path.startsWith("https://")) {
return path;
} else {
// relative path, build new path
String currentPath = method.getPath();
int end = currentPath.lastIndexOf('/');
if (end >= 0) {
uri.setPath(currentPath.substring(0, end + 1) + path);
} else {
throw new URIException(uri.getURI());
}
}
}
return uri.getURI();
}
protected String getScriptBasedFormURL(HttpMethod initmethod, String pathQuery) throws URIException {
URI initmethodURI = initmethod.getURI();
int queryIndex = pathQuery.indexOf('?');
if (queryIndex >= 0) {
if (queryIndex > 0) {
// update path
String newPath = pathQuery.substring(0, queryIndex);
if (newPath.startsWith("/")) {
// absolute path
initmethodURI.setPath(newPath);
} else {
String currentPath = initmethodURI.getPath();
int folderIndex = currentPath.lastIndexOf('/');
if (folderIndex >= 0) {
// replace relative path
initmethodURI.setPath(currentPath.substring(0, folderIndex + 1) + newPath);
} else {
// should not happen
initmethodURI.setPath('/' + newPath);
}
}
}
initmethodURI.setQuery(pathQuery.substring(queryIndex + 1));
}
return initmethodURI.getURI();
}
/**
* Try to find logon method path from logon form body.
*
* @param httpClient httpClient instance
* @param initmethod form body http method
* @return logon method
* @throws IOException on error
*/
protected HttpMethod buildLogonMethod(HttpClient httpClient, HttpMethod initmethod) throws IOException {
HttpMethod logonMethod = null;
// create an instance of HtmlCleaner
HtmlCleaner cleaner = new HtmlCleaner();
// A OTP token authentication form in a previous page could have username fields with different names
userNameInputs.clear();
try {
TagNode node = cleaner.clean(initmethod.getResponseBodyAsStream());
List forms = node.getElementListByName("form", true);
TagNode logonForm = null;
// select form
if (forms.size() == 1) {
logonForm = (TagNode) forms.get(0);
} else if (forms.size() > 1) {
for (Object form : forms) {
if ("logonForm".equals(((TagNode) form).getAttributeByName("name"))) {
logonForm = ((TagNode) form);
}
}
}
if (logonForm != null) {
String logonMethodPath = logonForm.getAttributeByName("action");
// workaround for broken form with empty action
if (logonMethodPath != null && logonMethodPath.length() == 0) {
logonMethodPath = "/owa/auth.owa";
}
logonMethod = new PostMethod(getAbsoluteUri(initmethod, logonMethodPath));
// retrieve lost inputs attached to body
List inputList = node.getElementListByName("input", true);
for (Object input : inputList) {
String type = ((TagNode) input).getAttributeByName("type");
String name = ((TagNode) input).getAttributeByName("name");
String value = ((TagNode) input).getAttributeByName("value");
if ("hidden".equalsIgnoreCase(type) && name != null && value != null) {
((PostMethod) logonMethod).addParameter(name, value);
}
// custom login form
if (USER_NAME_FIELDS.contains(name)) {
userNameInputs.add(name);
} else if (PASSWORD_FIELDS.contains(name)) {
passwordInput = name;
} else if ("addr".equals(name)) {
// this is not a logon form but a redirect form
HttpMethod newInitMethod = DavGatewayHttpClientFacade.executeFollowRedirects(httpClient, logonMethod);
logonMethod = buildLogonMethod(httpClient, newInitMethod);
} else if (TOKEN_FIELDS.contains(name)) {
// one time password, ask it to the user
((PostMethod) logonMethod).addParameter(name, DavGatewayOTPPrompt.getOneTimePassword());
} else if ("otc".equals(name)) {
// captcha image, get image and ask user
String pinsafeUser = getAliasFromLogin();
if (pinsafeUser == null) {
pinsafeUser = userName;
}
GetMethod getMethod = new GetMethod("/PINsafeISAFilter.dll?username=" + pinsafeUser);
try {
int status = httpClient.executeMethod(getMethod);
if (status != HttpStatus.SC_OK) {
throw DavGatewayHttpClientFacade.buildHttpException(getMethod);
}
BufferedImage captchaImage = ImageIO.read(getMethod.getResponseBodyAsStream());
((PostMethod) logonMethod).addParameter(name, DavGatewayOTPPrompt.getCaptchaValue(captchaImage));
} finally {
getMethod.releaseConnection();
}
}
}
} else {
List frameList = node.getElementListByName("frame", true);
if (frameList.size() == 1) {
String src = ((TagNode) frameList.get(0)).getAttributeByName("src");
if (src != null) {
LOGGER.debug("Frames detected in form page, try frame content");
initmethod.releaseConnection();
HttpMethod newInitMethod = DavGatewayHttpClientFacade.executeFollowRedirects(httpClient, src);
logonMethod = buildLogonMethod(httpClient, newInitMethod);
}
} else {
// another failover for script based logon forms (Exchange 2007)
List scriptList = node.getElementListByName("script", true);
for (Object script : scriptList) {
List contents = ((TagNode) script).getChildren();
for (Object content : contents) {
if (content instanceof CommentNode) {
String scriptValue = ((CommentNode) content).getCommentedContent();
String sUrl = StringUtil.getToken(scriptValue, "var a_sUrl = \"", "\"");
String sLgn = StringUtil.getToken(scriptValue, "var a_sLgnQS = \"", "\"");
if (sLgn == null) {
sLgn = StringUtil.getToken(scriptValue, "var a_sLgn = \"", "\"");
}
if (sUrl != null && sLgn != null) {
String src = getScriptBasedFormURL(initmethod, sLgn + sUrl);
LOGGER.debug("Detected script based logon, redirect to form at " + src);
HttpMethod newInitMethod = DavGatewayHttpClientFacade.executeFollowRedirects(httpClient, src);
logonMethod = buildLogonMethod(httpClient, newInitMethod);
}
} else if (content instanceof ContentNode) {
// Microsoft Forefront Unified Access Gateway redirect
String scriptValue = ((ContentNode) content).getContent().toString();
String location = StringUtil.getToken(scriptValue, "window.location.replace(\"", "\"");
if (location != null) {
LOGGER.debug("Post logon redirect to: " + location);
logonMethod = DavGatewayHttpClientFacade.executeFollowRedirects(httpClient, location);
}
}
}
}
}
}
} catch (IOException e) {
LOGGER.error("Error parsing login form at " + initmethod.getURI());
} finally {
initmethod.releaseConnection();
}
return logonMethod;
}
protected HttpMethod postLogonMethod(HttpClient httpClient, HttpMethod logonMethod, String userName, String password) throws IOException {
setAuthFormFields(logonMethod, httpClient, password);
// add exchange 2010 PBack cookie in compatibility mode
httpClient.getState().addCookie(new Cookie(httpClient.getHostConfiguration().getHost(), "PBack", "0", "/", null, false));
logonMethod = DavGatewayHttpClientFacade.executeFollowRedirects(httpClient, logonMethod);
// test form based authentication
checkFormLoginQueryString(logonMethod);
// workaround for post logon script redirect
if (!isAuthenticated(logonMethod)) {
// try to get new method from script based redirection
logonMethod = buildLogonMethod(httpClient, logonMethod);
if (logonMethod != null) {
if (otpPreAuthFound && otpPreAuthRetries < MAX_OTP_RETRIES) {
// A OTP pre-auth page has been found, it is needed to restart the login process.
// This applies to both the case the user entered a good OTP code (the usual login process
// takes place) and the case the user entered a wrong OTP code (another code will be asked to him).
// The user has up to MAX_OTP_RETRIES chances to input a valid OTP key.
return postLogonMethod(httpClient, logonMethod, userName, password);
}
// if logonMethod is not null, try to follow redirection
logonMethod = DavGatewayHttpClientFacade.executeFollowRedirects(httpClient, logonMethod);
checkFormLoginQueryString(logonMethod);
// also check cookies
if (!isAuthenticated(logonMethod)) {
throwAuthenticationFailed();
}
} else {
// authentication failed
throwAuthenticationFailed();
}
}
// check for language selection form
if (logonMethod != null && "/owa/languageselection.aspx".equals(logonMethod.getPath())) {
// need to submit form
logonMethod = submitLanguageSelectionForm(logonMethod);
}
return logonMethod;
}
protected void setAuthFormFields(HttpMethod logonMethod, HttpClient httpClient, String password) throws IllegalArgumentException {
String userNameInput;
if (userNameInputs.size() == 2) {
String userid;
// multiple username fields, split userid|username on |
int pipeIndex = userName.indexOf('|');
if (pipeIndex < 0) {
LOGGER.debug("Multiple user fields detected, please use userid|username as user name in client, except when userid is username");
userid = userName;
} else {
userid = userName.substring(0, pipeIndex);
userName = userName.substring(pipeIndex + 1);
// adjust credentials
DavGatewayHttpClientFacade.setCredentials(httpClient, userName, password);
}
((PostMethod) logonMethod).removeParameter("userid");
((PostMethod) logonMethod).addParameter("userid", userid);
userNameInput = "username";
} else if (userNameInputs.size() == 1) {
// simple username field
userNameInput = userNameInputs.get(0);
} else {
// should not happen
userNameInput = "username";
}
// make sure username and password fields are empty
((PostMethod) logonMethod).removeParameter(userNameInput);
if (passwordInput != null) {
((PostMethod) logonMethod).removeParameter(passwordInput);
}
((PostMethod) logonMethod).removeParameter("trusted");
((PostMethod) logonMethod).removeParameter("flags");
if (passwordInput == null) {
// This is a OTP pre-auth page. A different username may be required.
otpPreAuthFound = true;
otpPreAuthRetries++;
((PostMethod) logonMethod).addParameter(userNameInput, preAuthUsername);
} else {
otpPreAuthFound = false;
otpPreAuthRetries = 0;
// This is a regular Exchange login page
((PostMethod) logonMethod).addParameter(userNameInput, userName);
((PostMethod) logonMethod).addParameter(passwordInput, password);
((PostMethod) logonMethod).addParameter("trusted", "4");
((PostMethod) logonMethod).addParameter("flags", "4");
}
}
protected HttpMethod formLogin(HttpClient httpClient, HttpMethod initmethod, String userName, String password) throws IOException {
LOGGER.debug("Form based authentication detected");
HttpMethod logonMethod = buildLogonMethod(httpClient, initmethod);
if (logonMethod == null) {
LOGGER.debug("Authentication form not found at " + initmethod.getURI() + ", trying default url");
logonMethod = new PostMethod("/owa/auth/owaauth.dll");
}
logonMethod = postLogonMethod(httpClient, logonMethod, userName, password);
return logonMethod;
}
protected HttpMethod submitLanguageSelectionForm(HttpMethod logonMethod) throws IOException {
PostMethod postLanguageFormMethod;
// create an instance of HtmlCleaner
HtmlCleaner cleaner = new HtmlCleaner();
try {
TagNode node = cleaner.clean(logonMethod.getResponseBodyAsStream());
List forms = node.getElementListByName("form", true);
TagNode languageForm;
// select form
if (forms.size() == 1) {
languageForm = (TagNode) forms.get(0);
} else {
throw new IOException("Form not found");
}
String languageMethodPath = languageForm.getAttributeByName("action");
postLanguageFormMethod = new PostMethod(getAbsoluteUri(logonMethod, languageMethodPath));
List inputList = languageForm.getElementListByName("input", true);
for (Object input : inputList) {
String name = ((TagNode) input).getAttributeByName("name");
String value = ((TagNode) input).getAttributeByName("value");
if (name != null && value != null) {
postLanguageFormMethod.addParameter(name, value);
}
}
List selectList = languageForm.getElementListByName("select", true);
for (Object select : selectList) {
String name = ((TagNode) select).getAttributeByName("name");
List optionList = ((TagNode) select).getElementListByName("option", true);
String value = null;
for (Object option : optionList) {
if (((TagNode) option).getAttributeByName("selected") != null) {
value = ((TagNode) option).getAttributeByName("value");
break;
}
}
if (name != null && value != null) {
postLanguageFormMethod.addParameter(name, value);
}
}
} catch (IOException e) {
String errorMessage = "Error parsing language selection form at " + logonMethod.getURI();
LOGGER.error(errorMessage);
throw new IOException(errorMessage);
} finally {
logonMethod.releaseConnection();
}
return DavGatewayHttpClientFacade.executeFollowRedirects(httpClient, postLanguageFormMethod);
}
/**
* Look for session cookies.
*
* @return true if session cookies are available
*/
protected boolean isAuthenticated(HttpMethod method) {
boolean authenticated = false;
if (method.getStatusCode() == HttpStatus.SC_OK
&& "/ews/services.wsdl".equalsIgnoreCase(method.getPath())) {
// direct EWS access returned wsdl
authenticated = true;
} else {
// check cookies
for (Cookie cookie : httpClient.getState().getCookies()) {
// Exchange 2003 cookies
if (cookie.getName().startsWith("cadata") || "sessionid".equals(cookie.getName())
// Exchange 2007 cookie
|| "UserContext".equals(cookie.getName())
// Direct EWS access
|| "exchangecookie".equals(cookie.getName())
) {
authenticated = true;
break;
}
}
}
return authenticated;
}
protected void checkFormLoginQueryString(HttpMethod logonMethod) throws DavMailAuthenticationException {
String queryString = logonMethod.getQueryString();
if (queryString != null && (queryString.contains("reason=2") || queryString.contains("reason=4"))) {
logonMethod.releaseConnection();
throwAuthenticationFailed();
}
}
protected void throwAuthenticationFailed() throws DavMailAuthenticationException {
if (this.userName != null && this.userName.contains("\\")) {
throw new DavMailAuthenticationException("EXCEPTION_AUTHENTICATION_FAILED");
} else {
throw new DavMailAuthenticationException("EXCEPTION_AUTHENTICATION_FAILED_RETRY");
}
}
protected abstract void buildSessionInfo(HttpMethod method) throws DavMailException;
/**
* Create message in specified folder.
* Will overwrite an existing message with same subject in the same folder
*
* @param folderPath Exchange folder path
* @param messageName message name
* @param properties message properties (flags)
* @param mimeMessage MIME message
* @throws IOException when unable to create message
*/
public abstract void createMessage(String folderPath, String messageName, HashMap<String, String> properties, MimeMessage mimeMessage) throws IOException;
/**
* Update given properties on message.
*
* @param message Exchange message
* @param properties Webdav properties map
* @throws IOException on error
*/
public abstract void updateMessage(Message message, Map<String, String> properties) throws IOException;
/**
* Delete Exchange message.
*
* @param message Exchange message
* @throws IOException on error
*/
public abstract void deleteMessage(Message message) throws IOException;
/**
* Get raw MIME message content
*
* @param message Exchange message
* @return message body
* @throws IOException on error
*/
protected abstract byte[] getContent(Message message) throws IOException;
protected static final Set<String> POP_MESSAGE_ATTRIBUTES = new HashSet<String>();
static {
POP_MESSAGE_ATTRIBUTES.add("uid");
POP_MESSAGE_ATTRIBUTES.add("imapUid");
POP_MESSAGE_ATTRIBUTES.add("messageSize");
}
/**
* Return folder message list with id and size only (for POP3 listener).
*
* @param folderName Exchange folder name
* @return folder message list
* @throws IOException on error
*/
public MessageList getAllMessageUidAndSize(String folderName) throws IOException {
return searchMessages(folderName, POP_MESSAGE_ATTRIBUTES, null);
}
protected static final Set<String> IMAP_MESSAGE_ATTRIBUTES = new HashSet<String>();
static {
IMAP_MESSAGE_ATTRIBUTES.add("permanenturl");
IMAP_MESSAGE_ATTRIBUTES.add("urlcompname");
IMAP_MESSAGE_ATTRIBUTES.add("uid");
IMAP_MESSAGE_ATTRIBUTES.add("messageSize");
IMAP_MESSAGE_ATTRIBUTES.add("imapUid");
IMAP_MESSAGE_ATTRIBUTES.add("junk");
IMAP_MESSAGE_ATTRIBUTES.add("flagStatus");
IMAP_MESSAGE_ATTRIBUTES.add("messageFlags");
IMAP_MESSAGE_ATTRIBUTES.add("lastVerbExecuted");
IMAP_MESSAGE_ATTRIBUTES.add("read");
IMAP_MESSAGE_ATTRIBUTES.add("deleted");
IMAP_MESSAGE_ATTRIBUTES.add("date");
IMAP_MESSAGE_ATTRIBUTES.add("lastmodified");
// OSX IMAP requests content-class
IMAP_MESSAGE_ATTRIBUTES.add("contentclass");
IMAP_MESSAGE_ATTRIBUTES.add("keywords");
}
protected static final Set<String> UID_MESSAGE_ATTRIBUTES = new HashSet<String>();
static {
UID_MESSAGE_ATTRIBUTES.add("uid");
}
/**
* Get all folder messages.
*
* @param folderPath Exchange folder name
* @return message list
* @throws IOException on error
*/
public MessageList searchMessages(String folderPath) throws IOException {
return searchMessages(folderPath, IMAP_MESSAGE_ATTRIBUTES, null);
}
/**
* Search folder for messages matching conditions, with attributes needed by IMAP listener.
*
* @param folderName Exchange folder name
* @param condition search filter
* @return message list
* @throws IOException on error
*/
public MessageList searchMessages(String folderName, Condition condition) throws IOException {
return searchMessages(folderName, IMAP_MESSAGE_ATTRIBUTES, condition);
}
/**
* Search folder for messages matching conditions, with given attributes.
*
* @param folderName Exchange folder name
* @param attributes requested Webdav attributes
* @param condition search filter
* @return message list
* @throws IOException on error
*/
public abstract MessageList searchMessages(String folderName, Set<String> attributes, Condition condition) throws IOException;
/**
* Get server version (Exchange2003, Exchange2007 or Exchange2010)
*
* @return server version
*/
public String getServerVersion() {
return serverVersion;
}
@SuppressWarnings({"JavaDoc"})
public enum Operator {
Or, And, Not, IsEqualTo,
IsGreaterThan, IsGreaterThanOrEqualTo,
IsLessThan, IsLessThanOrEqualTo,
IsNull, IsTrue, IsFalse,
Like, StartsWith, Contains
}
/**
* Exchange search filter.
*/
public static interface Condition {
/**
* Append condition to buffer.
*
* @param buffer search filter buffer
*/
void appendTo(StringBuilder buffer);
/**
* True if condition is empty.
*
* @return true if condition is empty
*/
boolean isEmpty();
/**
* Test if the contact matches current condition.
*
* @param contact Exchange Contact
* @return true if contact matches condition
*/
boolean isMatch(ExchangeSession.Contact contact);
}
/**
* Attribute condition.
*/
public abstract static class AttributeCondition implements Condition {
protected final String attributeName;
protected final Operator operator;
protected final String value;
protected AttributeCondition(String attributeName, Operator operator, String value) {
this.attributeName = attributeName;
this.operator = operator;
this.value = value;
}
public boolean isEmpty() {
return false;
}
/**
* Get attribute name.
*
* @return attribute name
*/
public String getAttributeName() {
return attributeName;
}
/**
* Condition value.
*
* @return value
*/
public String getValue() {
return value;
}
}
/**
* Multiple condition.
*/
public abstract static class MultiCondition implements Condition {
protected final Operator operator;
protected final List<Condition> conditions;
protected MultiCondition(Operator operator, Condition... conditions) {
this.operator = operator;
this.conditions = new ArrayList<Condition>();
for (Condition condition : conditions) {
if (condition != null) {
this.conditions.add(condition);
}
}
}
/**
* Conditions list.
*
* @return conditions
*/
public List<Condition> getConditions() {
return conditions;
}
/**
* Condition operator.
*
* @return operator
*/
public Operator getOperator() {
return operator;
}
/**
* Add a new condition.
*
* @param condition single condition
*/
public void add(Condition condition) {
if (condition != null) {
conditions.add(condition);
}
}
public boolean isEmpty() {
boolean isEmpty = true;
for (Condition condition : conditions) {
if (!condition.isEmpty()) {
isEmpty = false;
break;
}
}
return isEmpty;
}
public boolean isMatch(ExchangeSession.Contact contact) {
if (operator == Operator.And) {
for (Condition condition : conditions) {
if (!condition.isMatch(contact)) {
return false;
}
}
return true;
} else if (operator == Operator.Or) {
for (Condition condition : conditions) {
if (condition.isMatch(contact)) {
return true;
}
}
return false;
} else {
return false;
}
}
}
/**
* Not condition.
*/
public abstract static class NotCondition implements Condition {
protected final Condition condition;
protected NotCondition(Condition condition) {
this.condition = condition;
}
public boolean isEmpty() {
return condition.isEmpty();
}
public boolean isMatch(ExchangeSession.Contact contact) {
return !condition.isMatch(contact);
}
}
/**
* Single search filter condition.
*/
public abstract static class MonoCondition implements Condition {
protected final String attributeName;
protected final Operator operator;
protected MonoCondition(String attributeName, Operator operator) {
this.attributeName = attributeName;
this.operator = operator;
}
public boolean isEmpty() {
return false;
}
public boolean isMatch(ExchangeSession.Contact contact) {
String actualValue = contact.get(attributeName);
return (operator == Operator.IsNull && actualValue == null) ||
(operator == Operator.IsFalse && "false".equals(actualValue)) ||
(operator == Operator.IsTrue && "true".equals(actualValue));
}
}
/**
* And search filter.
*
* @param condition search conditions
* @return condition
*/
public abstract MultiCondition and(Condition... condition);
/**
* Or search filter.
*
* @param condition search conditions
* @return condition
*/
public abstract MultiCondition or(Condition... condition);
/**
* Not search filter.
*
* @param condition search condition
* @return condition
*/
public abstract Condition not(Condition condition);
/**
* Equals condition.
*
* @param attributeName logical Exchange attribute name
* @param value attribute value
* @return condition
*/
public abstract Condition isEqualTo(String attributeName, String value);
/**
* Equals condition.
*
* @param attributeName logical Exchange attribute name
* @param value attribute value
* @return condition
*/
public abstract Condition isEqualTo(String attributeName, int value);
/**
* MIME header equals condition.
*
* @param headerName MIME header name
* @param value attribute value
* @return condition
*/
public abstract Condition headerIsEqualTo(String headerName, String value);
/**
* Greater than or equals condition.
*
* @param attributeName logical Exchange attribute name
* @param value attribute value
* @return condition
*/
public abstract Condition gte(String attributeName, String value);
/**
* Greater than condition.
*
* @param attributeName logical Exchange attribute name
* @param value attribute value
* @return condition
*/
public abstract Condition gt(String attributeName, String value);
/**
* Lower than condition.
*
* @param attributeName logical Exchange attribute name
* @param value attribute value
* @return condition
*/
public abstract Condition lt(String attributeName, String value);
/**
* Lower than or equals condition.
*
* @param attributeName logical Exchange attribute name
* @param value attribute value
* @return condition
*/
@SuppressWarnings({"UnusedDeclaration"})
public abstract Condition lte(String attributeName, String value);
/**
* Contains condition.
*
* @param attributeName logical Exchange attribute name
* @param value attribute value
* @return condition
*/
public abstract Condition contains(String attributeName, String value);
/**
* Starts with condition.
*
* @param attributeName logical Exchange attribute name
* @param value attribute value
* @return condition
*/
public abstract Condition startsWith(String attributeName, String value);
/**
* Is null condition.
*
* @param attributeName logical Exchange attribute name
* @return condition
*/
public abstract Condition isNull(String attributeName);
/**
* Is true condition.
*
* @param attributeName logical Exchange attribute name
* @return condition
*/
public abstract Condition isTrue(String attributeName);
/**
* Is false condition.
*
* @param attributeName logical Exchange attribute name
* @return condition
*/
public abstract Condition isFalse(String attributeName);
/**
* Search mail and generic folders under given folder.
* Exclude calendar and contacts folders
*
* @param folderName Exchange folder name
* @param recursive deep search if true
* @return list of folders
* @throws IOException on error
*/
public List<Folder> getSubFolders(String folderName, boolean recursive) throws IOException {
Condition folderCondition = null;
if (!Settings.getBooleanProperty("davmail.imapIncludeSpecialFolders", false)) {
folderCondition = or(isEqualTo("folderclass", "IPF.Note"), isNull("folderclass"));
}
List<Folder> results = getSubFolders(folderName, folderCondition,
recursive);
// need to include base folder in recursive search, except on root
if (recursive && folderName.length() > 0) {
results.add(getFolder(folderName));
}
return results;
}
/**
* Search calendar folders under given folder.
*
* @param folderName Exchange folder name
* @param recursive deep search if true
* @return list of folders
* @throws IOException on error
*/
public List<Folder> getSubCalendarFolders(String folderName, boolean recursive) throws IOException {
return getSubFolders(folderName, isEqualTo("folderclass", "IPF.Appointment"), recursive);
}
/**
* Search folders under given folder matching filter.
*
* @param folderName Exchange folder name
* @param condition search filter
* @param recursive deep search if true
* @return list of folders
* @throws IOException on error
*/
public abstract List<Folder> getSubFolders(String folderName, Condition condition, boolean recursive) throws IOException;
/**
* Delete oldest messages in trash.
* keepDelay is the number of days to keep messages in trash before delete
*
* @throws IOException when unable to purge messages
*/
public void purgeOldestTrashAndSentMessages() throws IOException {
int keepDelay = Settings.getIntProperty("davmail.keepDelay");
if (keepDelay != 0) {
purgeOldestFolderMessages(TRASH, keepDelay);
}
// this is a new feature, default is : do nothing
int sentKeepDelay = Settings.getIntProperty("davmail.sentKeepDelay");
if (sentKeepDelay != 0) {
purgeOldestFolderMessages(SENT, sentKeepDelay);
}
}
protected void purgeOldestFolderMessages(String folderPath, int keepDelay) throws IOException {
Calendar cal = Calendar.getInstance();
cal.add(Calendar.DAY_OF_MONTH, -keepDelay);
LOGGER.debug("Delete messages in " + folderPath + " not modified since " + cal.getTime());
MessageList messages = searchMessages(folderPath, UID_MESSAGE_ATTRIBUTES,
lt("lastmodified", formatSearchDate(cal.getTime())));
for (Message message : messages) {
message.delete();
}
}
protected void convertResentHeader(MimeMessage mimeMessage, String headerName) throws MessagingException {
String[] resentHeader = mimeMessage.getHeader("Resent-" + headerName);
if (resentHeader != null) {
mimeMessage.removeHeader("Resent-" + headerName);
mimeMessage.removeHeader(headerName);
for (String value : resentHeader) {
mimeMessage.addHeader(headerName, value);
}
}
}
protected String lastSentMessageId;
/**
* Send message in reader to recipients.
* Detect visible recipients in message body to determine bcc recipients
*
* @param rcptToRecipients recipients list
* @param mimeMessage mime message
* @throws IOException on error
* @throws MessagingException on error
*/
public void sendMessage(List<String> rcptToRecipients, MimeMessage mimeMessage) throws IOException, MessagingException {
// detect duplicate send command
String messageId = mimeMessage.getMessageID();
if (lastSentMessageId != null && lastSentMessageId.equals(messageId)) {
LOGGER.debug("Dropping message id " + messageId + ": already sent");
return;
}
lastSentMessageId = messageId;
convertResentHeader(mimeMessage, "From");
convertResentHeader(mimeMessage, "To");
convertResentHeader(mimeMessage, "Cc");
convertResentHeader(mimeMessage, "Bcc");
convertResentHeader(mimeMessage, "Message-Id");
// do not allow send as another user on Exchange 2003
if ("Exchange2003".equals(serverVersion) || Settings.getBooleanProperty("davmail.smtpStripFrom", false)) {
mimeMessage.removeHeader("From");
}
// remove visible recipients from list
Set<String> visibleRecipients = new HashSet<String>();
List<InternetAddress> recipients = getAllRecipients(mimeMessage);
for (InternetAddress address : recipients) {
visibleRecipients.add((address.getAddress().toLowerCase()));
}
for (String recipient : rcptToRecipients) {
if (!visibleRecipients.contains(recipient.toLowerCase())) {
mimeMessage.addRecipient(javax.mail.Message.RecipientType.BCC, new InternetAddress(recipient));
}
}
sendMessage(mimeMessage);
}
static final String[] RECIPIENT_HEADERS = {"to", "cc", "bcc"};
protected List<InternetAddress> getAllRecipients(MimeMessage mimeMessage) throws MessagingException {
List<InternetAddress> recipientList = new ArrayList<InternetAddress>();
for (String recipientHeader : RECIPIENT_HEADERS) {
final String recipientHeaderValue = mimeMessage.getHeader(recipientHeader, ",");
if (recipientHeaderValue != null) {
// parse headers in non strict mode
recipientList.addAll(Arrays.asList(InternetAddress.parseHeader(recipientHeaderValue, false)));
}
}
return recipientList;
}
/**
* Send Mime message.
*
* @param mimeMessage MIME message
* @throws IOException on error
* @throws MessagingException on error
*/
public abstract void sendMessage(MimeMessage mimeMessage) throws IOException, MessagingException;
/**
* Get folder object.
* Folder name can be logical names INBOX, Drafts, Trash or calendar,
* or a path relative to user base folder or absolute path.
*
* @param folderPath folder path
* @return Folder object
* @throws IOException on error
*/
public ExchangeSession.Folder getFolder(String folderPath) throws IOException {
Folder folder = internalGetFolder(folderPath);
if (isMainCalendar(folderPath)) {
Folder taskFolder = internalGetFolder(TASKS);
folder.ctag += taskFolder.ctag;
}
return folder;
}
protected abstract Folder internalGetFolder(String folderName) throws IOException;
/**
* Check folder ctag and reload messages as needed.
*
* @param currentFolder current folder
* @return true if folder changed
* @throws IOException on error
*/
public boolean refreshFolder(Folder currentFolder) throws IOException {
Folder newFolder = getFolder(currentFolder.folderPath);
if (currentFolder.ctag == null || !currentFolder.ctag.equals(newFolder.ctag)) {
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Contenttag changed on " + currentFolder.folderPath + ' '
+ currentFolder.ctag + " => " + newFolder.ctag + ", reloading messages");
}
currentFolder.hasChildren = newFolder.hasChildren;
currentFolder.noInferiors = newFolder.noInferiors;
currentFolder.unreadCount = newFolder.unreadCount;
currentFolder.ctag = newFolder.ctag;
currentFolder.etag = newFolder.etag;
if (newFolder.uidNext > currentFolder.uidNext) {
currentFolder.uidNext = newFolder.uidNext;
}
currentFolder.loadMessages();
return true;
} else {
return false;
}
}
/**
* Create Exchange message folder.
*
* @param folderName logical folder name
* @return status
* @throws IOException on error
*/
public int createMessageFolder(String folderName) throws IOException {
return createFolder(folderName, "IPF.Note", null);
}
/**
* Create Exchange calendar folder.
*
* @param folderName logical folder name
* @param properties folder properties
* @return status
* @throws IOException on error
*/
public int createCalendarFolder(String folderName, Map<String, String> properties) throws IOException {
return createFolder(folderName, "IPF.Appointment", properties);
}
/**
* Create Exchange contact folder.
*
* @param folderName logical folder name
* @param properties folder properties
* @return status
* @throws IOException on error
*/
public int createContactFolder(String folderName, Map<String, String> properties) throws IOException {
return createFolder(folderName, "IPF.Contact", properties);
}
/**
* Create Exchange folder with given folder class.
*
* @param folderName logical folder name
* @param folderClass folder class
* @param properties folder properties
* @return status
* @throws IOException on error
*/
public abstract int createFolder(String folderName, String folderClass, Map<String, String> properties) throws IOException;
/**
* Update Exchange folder properties.
*
* @param folderName logical folder name
* @param properties folder properties
* @return status
* @throws IOException on error
*/
public abstract int updateFolder(String folderName, Map<String, String> properties) throws IOException;
/**
* Delete Exchange folder.
*
* @param folderName logical folder name
* @throws IOException on error
*/
public abstract void deleteFolder(String folderName) throws IOException;
/**
* Copy message to target folder
*
* @param message Exchange message
* @param targetFolder target folder
* @throws IOException on error
*/
public abstract void copyMessage(Message message, String targetFolder) throws IOException;
/**
* Move message to target folder
*
* @param message Exchange message
* @param targetFolder target folder
* @throws IOException on error
*/
public abstract void moveMessage(Message message, String targetFolder) throws IOException;
/**
* Move folder to target name.
*
* @param folderName current folder name/path
* @param targetName target folder name/path
* @throws IOException on error
*/
public abstract void moveFolder(String folderName, String targetName) throws IOException;
/**
* Move item from source path to target path.
*
* @param sourcePath item source path
* @param targetPath item target path
* @throws IOException on error
*/
public abstract void moveItem(String sourcePath, String targetPath) throws IOException;
protected abstract void moveToTrash(Message message) throws IOException;
/**
* Convert keyword value to IMAP flag.
*
* @param value keyword value
* @return IMAP flag
*/
public String convertKeywordToFlag(String value) {
// first test for keyword in settings
Properties flagSettings = Settings.getSubProperties("davmail.imapFlags");
Enumeration flagSettingsEnum = flagSettings.propertyNames();
while (flagSettingsEnum.hasMoreElements()) {
String key = (String) flagSettingsEnum.nextElement();
if (value.equalsIgnoreCase(flagSettings.getProperty(key))) {
return key;
}
}
ResourceBundle flagBundle = ResourceBundle.getBundle("imapflags");
Enumeration<String> flagBundleEnum = flagBundle.getKeys();
while (flagBundleEnum.hasMoreElements()) {
String key = flagBundleEnum.nextElement();
if (value.equalsIgnoreCase(flagBundle.getString(key))) {
return key;
}
}
// fall back to raw value
return value;
}
/**
* Convert IMAP flag to keyword value.
*
* @param value IMAP flag
* @return keyword value
*/
public String convertFlagToKeyword(String value) {
// first test for flag in settings
Properties flagSettings = Settings.getSubProperties("davmail.imapFlags");
String flagValue = flagSettings.getProperty(value);
if (flagValue != null) {
return flagValue;
}
// fall back to predefined flags
ResourceBundle flagBundle = ResourceBundle.getBundle("imapflags");
try {
return flagBundle.getString(value);
} catch (MissingResourceException e) {
// ignore
}
// fall back to raw value
return value;
}
/**
* Exchange folder with IMAP properties
*/
public class Folder {
/**
* Logical (IMAP) folder path.
*/
public String folderPath;
/**
* Display Name.
*/
public String displayName;
/**
* Folder class (PR_CONTAINER_CLASS).
*/
public String folderClass;
/**
* Folder message count.
*/
public int count;
/**
* Folder unread message count.
*/
public int unreadCount;
/**
* true if folder has subfolders (DAV:hassubs).
*/
public boolean hasChildren;
/**
* true if folder has no subfolders (DAV:nosubs).
*/
public boolean noInferiors;
/**
* Folder content tag (to detect folder content changes).
*/
public String ctag;
/**
* Folder etag (to detect folder object changes).
*/
public String etag;
/**
* Next IMAP uid
*/
public long uidNext;
/**
* recent count
*/
public int recent;
/**
* Folder message list, empty before loadMessages call.
*/
public ExchangeSession.MessageList messages;
/**
* Permanent uid (PR_SEARCH_KEY) to IMAP UID map.
*/
private final HashMap<String, Long> permanentUrlToImapUidMap = new HashMap<String, Long>();
/**
* Get IMAP folder flags.
*
* @return folder flags in IMAP format
*/
public String getFlags() {
if (noInferiors) {
return "\\NoInferiors";
} else if (hasChildren) {
return "\\HasChildren";
} else {
return "\\HasNoChildren";
}
}
/**
* Load folder messages.
*
* @throws IOException on error
*/
public void loadMessages() throws IOException {
messages = ExchangeSession.this.searchMessages(folderPath, null);
fixUids(messages);
recent = 0;
for (Message message : messages) {
if (message.recent) {
recent++;
}
}
long computedUidNext = 1;
if (!messages.isEmpty()) {
computedUidNext = messages.get(messages.size() - 1).getImapUid() + 1;
}
if (computedUidNext > uidNext) {
uidNext = computedUidNext;
}
}
/**
* Search messages in folder matching query.
*
* @param condition search query
* @return message list
* @throws IOException on error
*/
public MessageList searchMessages(Condition condition) throws IOException {
MessageList localMessages = ExchangeSession.this.searchMessages(folderPath, condition);
fixUids(localMessages);
return localMessages;
}
/**
* Restore previous uids changed by a PROPPATCH (flag change).
*
* @param messages message list
*/
protected void fixUids(MessageList messages) {
boolean sortNeeded = false;
for (Message message : messages) {
if (permanentUrlToImapUidMap.containsKey(message.getPermanentId())) {
long previousUid = permanentUrlToImapUidMap.get(message.getPermanentId());
if (message.getImapUid() != previousUid) {
LOGGER.debug("Restoring IMAP uid " + message.getImapUid() + " -> " + previousUid + " for message " + message.getPermanentId());
message.setImapUid(previousUid);
sortNeeded = true;
}
} else {
// add message to uid map
permanentUrlToImapUidMap.put(message.getPermanentId(), message.getImapUid());
}
}
if (sortNeeded) {
Collections.sort(messages);
}
}
/**
* Folder message count.
*
* @return message count
*/
public int count() {
if (messages == null) {
return count;
} else {
return messages.size();
}
}
/**
* Compute IMAP uidnext.
*
* @return max(messageuids)+1
*/
public long getUidNext() {
return uidNext;
}
/**
* Get message at index.
*
* @param index message index
* @return message
*/
public Message get(int index) {
return messages.get(index);
}
/**
* Get current folder messages imap uids and flags
*
* @return imap uid list
*/
public TreeMap<Long, String> getImapFlagMap() {
TreeMap<Long, String> imapFlagMap = new TreeMap<Long, String>();
for (ExchangeSession.Message message : messages) {
imapFlagMap.put(message.getImapUid(), message.getImapFlags());
}
return imapFlagMap;
}
/**
* Calendar folder flag.
*
* @return true if this is a calendar folder
*/
public boolean isCalendar() {
return "IPF.Appointment".equals(folderClass);
}
/**
* Contact folder flag.
*
* @return true if this is a calendar folder
*/
public boolean isContact() {
return "IPF.Contact".equals(folderClass);
}
/**
* Task folder flag.
*
* @return true if this is a task folder
*/
public boolean isTask() {
return "IPF.Task".equals(folderClass);
}
/**
* drop cached message
*/
public void clearCache() {
messages.cachedMimeBody = null;
messages.cachedMimeMessage = null;
messages.cachedMessageImapUid = 0;
}
}
/**
* Exchange message.
*/
public abstract class Message implements Comparable<Message> {
/**
* enclosing message list
*/
public MessageList messageList;
/**
* Message url.
*/
public String messageUrl;
/**
* Message permanent url (does not change on message move).
*/
public String permanentUrl;
/**
* Message uid.
*/
public String uid;
/**
* Message content class.
*/
public String contentClass;
/**
* Message keywords (categories).
*/
public String keywords;
/**
* Message IMAP uid, unique in folder (x0e230003).
*/
public long imapUid;
/**
* MAPI message size.
*/
public int size;
/**
* Message date (urn:schemas:mailheader:date).
*/
public String date;
/**
* Message flag: read.
*/
public boolean read;
/**
* Message flag: deleted.
*/
public boolean deleted;
/**
* Message flag: junk.
*/
public boolean junk;
/**
* Message flag: flagged.
*/
public boolean flagged;
/**
* Message flag: recent.
*/
public boolean recent;
/**
* Message flag: draft.
*/
public boolean draft;
/**
* Message flag: answered.
*/
public boolean answered;
/**
* Message flag: fowarded.
*/
public boolean forwarded;
/**
* Unparsed message content.
*/
protected SharedByteArrayInputStream mimeBody;
/**
* Message content parsed in a MIME message.
*/
protected MimeMessage mimeMessage;
/**
* Get permanent message id.
* permanentUrl over WebDav or IitemId over EWS
*
* @return permanent id
*/
public abstract String getPermanentId();
/**
* IMAP uid , unique in folder (x0e230003)
*
* @return IMAP uid
*/
public long getImapUid() {
return imapUid;
}
/**
* Set IMAP uid.
*
* @param imapUid new uid
*/
public void setImapUid(long imapUid) {
this.imapUid = imapUid;
}
/**
* Exchange uid.
*
* @return uid
*/
public String getUid() {
return uid;
}
/**
* Return message flags in IMAP format.
*
* @return IMAP flags
*/
public String getImapFlags() {
StringBuilder buffer = new StringBuilder();
if (read) {
buffer.append("\\Seen ");
}
if (deleted) {
buffer.append("\\Deleted ");
}
if (recent) {
buffer.append("\\Recent ");
}
if (flagged) {
buffer.append("\\Flagged ");
}
if (junk) {
buffer.append("Junk ");
}
if (draft) {
buffer.append("\\Draft ");
}
if (answered) {
buffer.append("\\Answered ");
}
if (forwarded) {
buffer.append("$Forwarded ");
}
if (keywords != null) {
for (String keyword : keywords.split(",")) {
buffer.append(convertKeywordToFlag(keyword)).append(" ");
}
}
return buffer.toString().trim();
}
/**
* Load message content in a Mime message
*
* @throws IOException on error
* @throws MessagingException on error
*/
public void loadMimeMessage() throws IOException, MessagingException {
if (mimeMessage == null) {
// try to get message content from cache
if (this.imapUid == messageList.cachedMessageImapUid) {
mimeBody = messageList.cachedMimeBody;
mimeMessage = messageList.cachedMimeMessage;
LOGGER.debug("Got message content for " + imapUid + " from cache");
} else {
// load and parse message
mimeBody = new SharedByteArrayInputStream(getContent(this));
mimeMessage = new MimeMessage(null, mimeBody);
mimeBody.reset();
// workaround for Exchange 2003 ActiveSync bug
if (mimeMessage.getHeader("MAIL FROM") != null) {
mimeBody = (SharedByteArrayInputStream) mimeMessage.getRawInputStream();
mimeMessage = new MimeMessage(null, mimeBody);
mimeBody.reset();
}
LOGGER.debug("Downloaded full message content for IMAP UID " + imapUid + " (" + mimeBody.available() + " bytes)");
}
}
}
/**
* Get message content as a Mime message.
*
* @return mime message
* @throws IOException on error
* @throws MessagingException on error
*/
public MimeMessage getMimeMessage() throws IOException, MessagingException {
loadMimeMessage();
return mimeMessage;
}
public Enumeration getMatchingHeaderLinesFromHeaders(String[] headerNames) throws MessagingException, IOException {
Enumeration result = null;
if (mimeMessage == null) {
// message not loaded, try to get headers only
InputStream headers = getMimeHeaders();
if (headers != null) {
InternetHeaders internetHeaders = new InternetHeaders(headers);
if (internetHeaders.getHeader("Subject") == null) {
// invalid header content
return null;
}
if (headerNames == null) {
result = internetHeaders.getAllHeaderLines();
} else {
result = internetHeaders.getMatchingHeaderLines(headerNames);
}
}
}
return result;
}
public Enumeration getMatchingHeaderLines(String[] headerNames) throws MessagingException, IOException {
Enumeration result = getMatchingHeaderLinesFromHeaders(headerNames);
if (result == null) {
if (headerNames == null) {
result = getMimeMessage().getAllHeaderLines();
} else {
result = getMimeMessage().getMatchingHeaderLines(headerNames);
}
}
return result;
}
protected abstract InputStream getMimeHeaders();
/**
* Get message body size.
*
* @return mime message size
* @throws IOException on error
* @throws MessagingException on error
*/
public int getMimeMessageSize() throws IOException, MessagingException {
loadMimeMessage();
mimeBody.reset();
return mimeBody.available();
}
/**
* Get message body input stream.
*
* @return mime message InputStream
* @throws IOException on error
* @throws MessagingException on error
*/
public InputStream getRawInputStream() throws IOException, MessagingException {
loadMimeMessage();
mimeBody.reset();
return mimeBody;
}
/**
* Drop mime message to avoid keeping message content in memory,
* keep a single message in MessageList cache to handle chunked fetch.
*/
public void dropMimeMessage() {
// update single message cache
if (mimeMessage != null) {
messageList.cachedMessageImapUid = imapUid;
messageList.cachedMimeBody = mimeBody;
messageList.cachedMimeMessage = mimeMessage;
}
// drop curent message body to save memory
mimeMessage = null;
mimeBody = null;
}
public boolean isLoaded() {
return mimeMessage != null;
}
/**
* Delete message.
*
* @throws IOException on error
*/
public void delete() throws IOException {
deleteMessage(this);
}
/**
* Move message to trash, mark message read.
*
* @throws IOException on error
*/
public void moveToTrash() throws IOException {
markRead();
ExchangeSession.this.moveToTrash(this);
}
/**
* Mark message as read.
*
* @throws IOException on error
*/
public void markRead() throws IOException {
HashMap<String, String> properties = new HashMap<String, String>();
properties.put("read", "1");
updateMessage(this, properties);
}
/**
* Comparator to sort messages by IMAP uid
*
* @param message other message
* @return imapUid comparison result
*/
public int compareTo(Message message) {
long compareValue = (imapUid - message.imapUid);
if (compareValue > 0) {
return 1;
} else if (compareValue < 0) {
return -1;
} else {
return 0;
}
}
/**
* Override equals, compare IMAP uids
*
* @param message other message
* @return true if IMAP uids are equal
*/
@Override
public boolean equals(Object message) {
return message instanceof Message && imapUid == ((Message) message).imapUid;
}
/**
* Override hashCode, return imapUid hashcode.
*
* @return imapUid hashcode
*/
@Override
public int hashCode() {
return (int) (imapUid ^ (imapUid >>> 32));
}
public String removeFlag(String flag) {
if (keywords != null) {
final String exchangeFlag = convertFlagToKeyword(flag);
Set<String> keywordSet = new HashSet<String>();
String[] keywordArray = keywords.split(",");
for (String value : keywordArray) {
if (!value.equalsIgnoreCase(exchangeFlag)) {
keywordSet.add(value);
}
}
keywords = StringUtil.join(keywordSet, ",");
}
return keywords;
}
public String addFlag(String flag) {
final String exchangeFlag = convertFlagToKeyword(flag);
HashSet<String> keywordSet = new HashSet<String>();
boolean hasFlag = false;
if (keywords != null) {
String[] keywordArray = keywords.split(",");
for (String value : keywordArray) {
keywordSet.add(value);
if (value.equalsIgnoreCase(exchangeFlag)) {
hasFlag = true;
}
}
}
if (!hasFlag) {
keywordSet.add(exchangeFlag);
}
keywords = StringUtil.join(keywordSet, ",");
return keywords;
}
public String setFlags(HashSet<String> flags) {
HashSet<String> keywordSet = new HashSet<String>();
for (String flag : flags) {
keywordSet.add(convertFlagToKeyword(flag));
}
keywords = StringUtil.join(keywordSet, ",");
return keywords;
}
}
/**
* Message list, includes a single messsage cache
*/
public static class MessageList extends ArrayList<Message> {
/**
* Cached message content parsed in a MIME message.
*/
protected transient MimeMessage cachedMimeMessage;
/**
* Cached message uid.
*/
protected transient long cachedMessageImapUid;
/**
* Cached unparsed message
*/
protected transient SharedByteArrayInputStream cachedMimeBody;
}
/**
* Generic folder item.
*/
public abstract static class Item extends HashMap<String, String> {
protected String folderPath;
protected String itemName;
protected String permanentUrl;
/**
* Display name.
*/
public String displayName;
/**
* item etag
*/
public String etag;
protected String noneMatch;
/**
* Build item instance.
*
* @param folderPath folder path
* @param itemName item name class
* @param etag item etag
* @param noneMatch none match flag
*/
public Item(String folderPath, String itemName, String etag, String noneMatch) {
this.folderPath = folderPath;
this.itemName = itemName;
this.etag = etag;
this.noneMatch = noneMatch;
}
/**
* Default constructor.
*/
protected Item() {
}
/**
* Return item content type
*
* @return content type
*/
public abstract String getContentType();
/**
* Retrieve item body from Exchange
*
* @return item body
* @throws HttpException on error
*/
public abstract String getBody() throws IOException;
/**
* Get event name (file name part in URL).
*
* @return event name
*/
public String getName() {
return itemName;
}
/**
* Get event etag (last change tag).
*
* @return event etag
*/
public String getEtag() {
return etag;
}
/**
* Set item href.
*
* @param href item href
*/
public void setHref(String href) {
int index = href.lastIndexOf('/');
if (index >= 0) {
folderPath = href.substring(0, index);
itemName = href.substring(index + 1);
} else {
throw new IllegalArgumentException(href);
}
}
/**
* Return item href.
*
* @return item href
*/
public String getHref() {
return folderPath + '/' + itemName;
}
}
/**
* Contact object
*/
public abstract class Contact extends Item {
/**
* @inheritDoc
*/
public Contact(String folderPath, String itemName, Map<String, String> properties, String etag, String noneMatch) {
super(folderPath, itemName.endsWith(".vcf") ? itemName.substring(0, itemName.length() - 3) + "EML" : itemName, etag, noneMatch);
this.putAll(properties);
}
/**
* @inheritDoc
*/
protected Contact() {
}
/**
* Convert EML extension to vcf.
*
* @return item name
*/
@Override
public String getName() {
String name = super.getName();
if (name.endsWith(".EML")) {
name = name.substring(0, name.length() - 3) + "vcf";
}
return name;
}
/**
* Set contact name
*
* @param name contact name
*/
public void setName(String name) {
this.itemName = name;
}
/**
* Compute vcard uid from name.
*
* @return uid
* @throws URIException on error
*/
protected String getUid() throws URIException {
String uid = getName();
int dotIndex = uid.lastIndexOf('.');
if (dotIndex > 0) {
uid = uid.substring(0, dotIndex);
}
return URIUtil.encodePath(uid);
}
@Override
public String getContentType() {
return "text/vcard";
}
@Override
public String getBody() throws HttpException {
// build RFC 2426 VCard from contact information
VCardWriter writer = new VCardWriter();
writer.startCard();
writer.appendProperty("UID", getUid());
// common name
writer.appendProperty("FN", get("cn"));
// RFC 2426: Family Name, Given Name, Additional Names, Honorific Prefixes, and Honorific Suffixes
writer.appendProperty("N", get("sn"), get("givenName"), get("middlename"), get("personaltitle"), get("namesuffix"));
writer.appendProperty("TEL;TYPE=cell", get("mobile"));
writer.appendProperty("TEL;TYPE=work", get("telephoneNumber"));
writer.appendProperty("TEL;TYPE=home", get("homePhone"));
writer.appendProperty("TEL;TYPE=fax", get("facsimiletelephonenumber"));
writer.appendProperty("TEL;TYPE=pager", get("pager"));
writer.appendProperty("TEL;TYPE=car", get("othermobile"));
writer.appendProperty("TEL;TYPE=home,fax", get("homefax"));
writer.appendProperty("TEL;TYPE=isdn", get("internationalisdnnumber"));
writer.appendProperty("TEL;TYPE=msg", get("otherTelephone"));
// The structured type value corresponds, in sequence, to the post office box; the extended address;
// the street address; the locality (e.g., city); the region (e.g., state or province);
// the postal code; the country name
writer.appendProperty("ADR;TYPE=home",
get("homepostofficebox"), null, get("homeStreet"), get("homeCity"), get("homeState"), get("homePostalCode"), get("homeCountry"));
writer.appendProperty("ADR;TYPE=work",
get("postofficebox"), get("roomnumber"), get("street"), get("l"), get("st"), get("postalcode"), get("co"));
writer.appendProperty("ADR;TYPE=other",
get("otherpostofficebox"), null, get("otherstreet"), get("othercity"), get("otherstate"), get("otherpostalcode"), get("othercountry"));
writer.appendProperty("EMAIL;TYPE=work", get("smtpemail1"));
writer.appendProperty("EMAIL;TYPE=home", get("smtpemail2"));
writer.appendProperty("EMAIL;TYPE=other", get("smtpemail3"));
writer.appendProperty("ORG", get("o"), get("department"));
writer.appendProperty("URL;TYPE=work", get("businesshomepage"));
writer.appendProperty("URL;TYPE=home", get("personalHomePage"));
writer.appendProperty("TITLE", get("title"));
writer.appendProperty("NOTE", get("description"));
writer.appendProperty("CUSTOM1", get("extensionattribute1"));
writer.appendProperty("CUSTOM2", get("extensionattribute2"));
writer.appendProperty("CUSTOM3", get("extensionattribute3"));
writer.appendProperty("CUSTOM4", get("extensionattribute4"));
writer.appendProperty("ROLE", get("profession"));
writer.appendProperty("NICKNAME", get("nickname"));
writer.appendProperty("X-AIM", get("im"));
writer.appendProperty("BDAY", convertZuluDateToBday(get("bday")));
writer.appendProperty("ANNIVERSARY", convertZuluDateToBday(get("anniversary")));
String gender = get("gender");
if ("1".equals(gender)) {
writer.appendProperty("SEX", "2");
} else if ("2".equals(gender)) {
writer.appendProperty("SEX", "1");
}
writer.appendProperty("CATEGORIES", get("keywords"));
writer.appendProperty("FBURL", get("fburl"));
if ("1".equals(get("private"))) {
writer.appendProperty("CLASS", "PRIVATE");
}
writer.appendProperty("X-ASSISTANT", get("secretarycn"));
writer.appendProperty("X-MANAGER", get("manager"));
writer.appendProperty("X-SPOUSE", get("spousecn"));
writer.appendProperty("REV", get("lastmodified"));
if ("true".equals(get("haspicture"))) {
try {
ContactPhoto contactPhoto = getContactPhoto(this);
writer.writeLine("PHOTO;BASE64;TYPE=\"" + contactPhoto.contentType + "\";ENCODING=\"b\":");
writer.writeLine(contactPhoto.content, true);
} catch (IOException e) {
LOGGER.warn("Unable to get photo from contact " + this.get("cn"));
}
}
writer.endCard();
return writer.toString();
}
}
/**
* Calendar event object.
*/
public abstract class Event extends Item {
protected String contentClass;
protected String subject;
protected VCalendar vCalendar;
/**
* @inheritDoc
*/
public Event(String folderPath, String itemName, String contentClass, String itemBody, String etag, String noneMatch) throws IOException {
super(folderPath, itemName, etag, noneMatch);
this.contentClass = contentClass;
fixICS(itemBody.getBytes("UTF-8"), false);
// fix task item name
if (vCalendar.isTodo() && this.itemName.endsWith(".ics")) {
this.itemName = itemName.substring(0, itemName.length() - 3) + "EML";
}
}
/**
* @inheritDoc
*/
protected Event() {
}
@Override
public String getContentType() {
return "text/calendar;charset=UTF-8";
}
@Override
public String getBody() throws IOException {
if (vCalendar == null) {
fixICS(getEventContent(), true);
}
return vCalendar.toString();
}
protected HttpException buildHttpException(Exception e) {
String message = "Unable to get event " + getName() + " subject: " + subject + " at " + permanentUrl + ": " + e.getMessage();
LOGGER.warn(message);
return new HttpException(message);
}
/**
* Retrieve item body from Exchange
*
* @return item content
* @throws HttpException on error
*/
public abstract byte[] getEventContent() throws IOException;
protected static final String TEXT_CALENDAR = "text/calendar";
protected static final String APPLICATION_ICS = "application/ics";
protected boolean isCalendarContentType(String contentType) {
return TEXT_CALENDAR.regionMatches(true, 0, contentType, 0, TEXT_CALENDAR.length()) ||
APPLICATION_ICS.regionMatches(true, 0, contentType, 0, APPLICATION_ICS.length());
}
protected MimePart getCalendarMimePart(MimeMultipart multiPart) throws IOException, MessagingException {
MimePart bodyPart = null;
for (int i = 0; i < multiPart.getCount(); i++) {
String contentType = multiPart.getBodyPart(i).getContentType();
if (isCalendarContentType(contentType)) {
bodyPart = (MimePart) multiPart.getBodyPart(i);
break;
} else if (contentType.startsWith("multipart")) {
Object content = multiPart.getBodyPart(i).getContent();
if (content instanceof MimeMultipart) {
bodyPart = getCalendarMimePart((MimeMultipart) content);
}
}
}
return bodyPart;
}
/**
* Load ICS content from MIME message input stream
*
* @param mimeInputStream mime message input stream
* @return mime message ics attachment body
* @throws IOException on error
* @throws MessagingException on error
*/
protected byte[] getICS(InputStream mimeInputStream) throws IOException, MessagingException {
byte[] result;
MimeMessage mimeMessage = new MimeMessage(null, mimeInputStream);
String[] contentClassHeader = mimeMessage.getHeader("Content-class");
// task item, return null
if (contentClassHeader != null && contentClassHeader.length > 0 && "urn:content-classes:task".equals(contentClassHeader[0])) {
return null;
}
Object mimeBody = mimeMessage.getContent();
MimePart bodyPart = null;
if (mimeBody instanceof MimeMultipart) {
bodyPart = getCalendarMimePart((MimeMultipart) mimeBody);
} else if (isCalendarContentType(mimeMessage.getContentType())) {
// no multipart, single body
bodyPart = mimeMessage;
}
if (bodyPart != null) {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
bodyPart.getDataHandler().writeTo(baos);
baos.close();
result = baos.toByteArray();
} else {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
mimeMessage.writeTo(baos);
baos.close();
throw new DavMailException("EXCEPTION_INVALID_MESSAGE_CONTENT", new String(baos.toByteArray(), "UTF-8"));
}
return result;
}
protected void fixICS(byte[] icsContent, boolean fromServer) throws IOException {
if (LOGGER.isDebugEnabled() && fromServer) {
dumpIndex++;
String icsBody = new String(icsContent, "UTF-8");
dumpICS(icsBody, fromServer, false);
LOGGER.debug("Vcalendar body received from server:\n" + icsBody);
}
vCalendar = new VCalendar(icsContent, getEmail(), getVTimezone());
vCalendar.fixVCalendar(fromServer);
if (LOGGER.isDebugEnabled() && !fromServer) {
String resultString = vCalendar.toString();
LOGGER.debug("Fixed Vcalendar body to server:\n" + resultString);
dumpICS(resultString, fromServer, true);
}
}
protected void dumpICS(String icsBody, boolean fromServer, boolean after) {
String logFileDirectory = Settings.getLogFileDirectory();
// additional setting to activate ICS dump (not available in GUI)
int dumpMax = Settings.getIntProperty("davmail.dumpICS");
if (dumpMax > 0) {
if (dumpIndex > dumpMax) {
// Delete the oldest dump file
final int oldest = dumpIndex - dumpMax;
try {
File[] oldestFiles = (new File(logFileDirectory)).listFiles(new FilenameFilter() {
public boolean accept(File dir, String name) {
if (name.endsWith(".ics")) {
int dashIndex = name.indexOf('-');
if (dashIndex > 0) {
try {
int fileIndex = Integer.parseInt(name.substring(0, dashIndex));
return fileIndex < oldest;
} catch (NumberFormatException nfe) {
// ignore
}
}
}
return false;
}
});
for (File file : oldestFiles) {
if (!file.delete()) {
LOGGER.warn("Unable to delete " + file.getAbsolutePath());
}
}
} catch (Exception ex) {
LOGGER.warn("Error deleting ics dump: " + ex.getMessage());
}
}
StringBuilder filePath = new StringBuilder();
filePath.append(logFileDirectory).append('/')
.append(dumpIndex)
.append(after ? "-to" : "-from")
.append((after ^ fromServer) ? "-server" : "-client")
.append(".ics");
if ((icsBody != null) && (icsBody.length() > 0)) {
OutputStreamWriter writer = null;
try {
writer = new OutputStreamWriter(new FileOutputStream(filePath.toString()), "UTF-8");
writer.write(icsBody);
} catch (IOException e) {
LOGGER.error(e);
} finally {
if (writer != null) {
try {
writer.close();
} catch (IOException e) {
LOGGER.error(e);
}
}
}
}
}
}
/**
* Build Mime body for event or event message.
*
* @return mimeContent as byte array or null
* @throws IOException on error
*/
public byte[] createMimeContent() throws IOException {
String boundary = UUID.randomUUID().toString();
ByteArrayOutputStream baos = new ByteArrayOutputStream();
MimeOutputStreamWriter writer = new MimeOutputStreamWriter(baos);
writer.writeHeader("Content-Transfer-Encoding", "7bit");
writer.writeHeader("Content-class", contentClass);
// append date
writer.writeHeader("Date", new Date());
// Make sure invites have a proper subject line
String vEventSubject = vCalendar.getFirstVeventPropertyValue("SUMMARY");
if (vEventSubject == null) {
vEventSubject = BundleMessage.format("MEETING_REQUEST");
}
// Write a part of the message that contains the
// ICS description so that invites contain the description text
String description = vCalendar.getFirstVeventPropertyValue("DESCRIPTION");
// handle notifications
if ("urn:content-classes:calendarmessage".equals(contentClass)) {
// need to parse attendees and organizer to build recipients
VCalendar.Recipients recipients = vCalendar.getRecipients(true);
String to;
String cc;
String notificationSubject;
if (email.equalsIgnoreCase(recipients.organizer)) {
// current user is organizer => notify all
to = recipients.attendees;
cc = recipients.optionalAttendees;
notificationSubject = subject;
} else {
String status = vCalendar.getAttendeeStatus();
// notify only organizer
to = recipients.organizer;
cc = null;
notificationSubject = (status != null) ? (BundleMessage.format(status) + vEventSubject) : subject;
description = "";
}
// Allow end user notification edit
if (Settings.getBooleanProperty("davmail.caldavEditNotifications")) {
// create notification edit dialog
NotificationDialog notificationDialog = new NotificationDialog(to,
cc, notificationSubject, description);
if (!notificationDialog.getSendNotification()) {
LOGGER.debug("Notification canceled by user");
return null;
}
// get description from dialog
to = notificationDialog.getTo();
cc = notificationDialog.getCc();
notificationSubject = notificationDialog.getSubject();
description = notificationDialog.getBody();
}
// do not send notification if no recipients found
if ((to == null || to.length() == 0) && (cc == null || cc.length() == 0)) {
return null;
}
writer.writeHeader("To", to);
writer.writeHeader("Cc", cc);
writer.writeHeader("Subject", notificationSubject);
if (LOGGER.isDebugEnabled()) {
StringBuilder logBuffer = new StringBuilder("Sending notification ");
if (to != null) {
logBuffer.append("to: ").append(to);
}
if (cc != null) {
logBuffer.append("cc: ").append(cc);
}
LOGGER.debug(logBuffer.toString());
}
} else {
// need to parse attendees and organizer to build recipients
VCalendar.Recipients recipients = vCalendar.getRecipients(false);
// storing appointment, full recipients header
if (recipients.attendees != null) {
writer.writeHeader("To", recipients.attendees);
} else {
// use current user as attendee
writer.writeHeader("To", email);
}
writer.writeHeader("Cc", recipients.optionalAttendees);
if (recipients.organizer != null) {
writer.writeHeader("From", recipients.organizer);
} else {
writer.writeHeader("From", email);
}
}
if (vCalendar.getMethod() == null) {
vCalendar.setPropertyValue("METHOD", "REQUEST");
}
writer.writeHeader("MIME-Version", "1.0");
writer.writeHeader("Content-Type", "multipart/alternative;\r\n" +
"\tboundary=\"----=_NextPart_" + boundary + '\"');
writer.writeLn();
writer.writeLn("This is a multi-part message in MIME format.");
writer.writeLn();
writer.writeLn("------=_NextPart_" + boundary);
if (description != null && description.length() > 0) {
writer.writeHeader("Content-Type", "text/plain;\r\n" +
"\tcharset=\"utf-8\"");
writer.writeHeader("content-transfer-encoding", "8bit");
writer.writeLn();
writer.flush();
baos.write(description.getBytes("UTF-8"));
writer.writeLn();
writer.writeLn("------=_NextPart_" + boundary);
}
writer.writeHeader("Content-class", contentClass);
writer.writeHeader("Content-Type", "text/calendar;\r\n" +
"\tmethod=" + vCalendar.getMethod() + ";\r\n" +
"\tcharset=\"utf-8\""
);
writer.writeHeader("Content-Transfer-Encoding", "8bit");
writer.writeLn();
writer.flush();
baos.write(vCalendar.toString().getBytes("UTF-8"));
writer.writeLn();
writer.writeLn("------=_NextPart_" + boundary + "--");
writer.close();
return baos.toByteArray();
}
/**
* Create or update item
*
* @return action result
* @throws IOException on error
*/
public abstract ItemResult createOrUpdate() throws IOException;
}
protected abstract Set<String> getItemProperties();
/**
* Search contacts in provided folder.
*
* @param folderPath Exchange folder path
* @return list of contacts
* @throws IOException on error
*/
public List<ExchangeSession.Contact> getAllContacts(String folderPath) throws IOException {
return searchContacts(folderPath, ExchangeSession.CONTACT_ATTRIBUTES, isEqualTo("outlookmessageclass", "IPM.Contact"), 0);
}
/**
* Search contacts in provided folder matching the search query.
*
* @param folderPath Exchange folder path
* @param attributes requested attributes
* @param condition Exchange search query
* @param maxCount maximum item count
* @return list of contacts
* @throws IOException on error
*/
public abstract List<Contact> searchContacts(String folderPath, Set<String> attributes, Condition condition, int maxCount) throws IOException;
/**
* Search calendar messages in provided folder.
*
* @param folderPath Exchange folder path
* @return list of calendar messages as Event objects
* @throws IOException on error
*/
public abstract List<Event> getEventMessages(String folderPath) throws IOException;
/**
* Search calendar events in provided folder.
*
* @param folderPath Exchange folder path
* @return list of calendar events
* @throws IOException on error
*/
public List<Event> getAllEvents(String folderPath) throws IOException {
List<Event> results = searchEvents(folderPath, getCalendarItemCondition(getPastDelayCondition("dtstart")));
if (!Settings.getBooleanProperty("davmail.caldavDisableTasks", false) && isMainCalendar(folderPath)) {
// retrieve tasks from main tasks folder
results.addAll(searchTasksOnly(TASKS));
}
return results;
}
protected abstract Condition getCalendarItemCondition(Condition dateCondition);
protected Condition getPastDelayCondition(String attribute) {
int caldavPastDelay = Settings.getIntProperty("davmail.caldavPastDelay");
Condition dateCondition = null;
if (caldavPastDelay != 0) {
Calendar cal = Calendar.getInstance();
cal.add(Calendar.DAY_OF_MONTH, -caldavPastDelay);
dateCondition = gt(attribute, formatSearchDate(cal.getTime()));
}
return dateCondition;
}
protected Condition getRangeCondition(String timeRangeStart, String timeRangeEnd) throws IOException {
try {
SimpleDateFormat parser = getZuluDateFormat();
ExchangeSession.MultiCondition andCondition = and();
if (timeRangeStart != null) {
andCondition.add(gt("dtend", formatSearchDate(parser.parse(timeRangeStart))));
}
if (timeRangeEnd != null) {
andCondition.add(lt("dtstart", formatSearchDate(parser.parse(timeRangeEnd))));
}
return andCondition;
} catch (ParseException e) {
throw new IOException(e + " " + e.getMessage());
}
}
/**
* Search events between start and end.
*
* @param folderPath Exchange folder path
* @param timeRangeStart date range start in zulu format
* @param timeRangeEnd date range start in zulu format
* @return list of calendar events
* @throws IOException on error
*/
public List<Event> searchEvents(String folderPath, String timeRangeStart, String timeRangeEnd) throws IOException {
Condition dateCondition = getRangeCondition(timeRangeStart, timeRangeEnd);
Condition condition = getCalendarItemCondition(dateCondition);
return searchEvents(folderPath, condition);
}
/**
* Search events between start and end, exclude tasks.
*
* @param folderPath Exchange folder path
* @param timeRangeStart date range start in zulu format
* @param timeRangeEnd date range start in zulu format
* @return list of calendar events
* @throws IOException on error
*/
public List<Event> searchEventsOnly(String folderPath, String timeRangeStart, String timeRangeEnd) throws IOException {
Condition dateCondition = getRangeCondition(timeRangeStart, timeRangeEnd);
return searchEvents(folderPath, getCalendarItemCondition(dateCondition));
}
/**
* Search tasks only (VTODO).
*
* @param folderPath Exchange folder path
* @return list of tasks
* @throws IOException on error
*/
public List<Event> searchTasksOnly(String folderPath) throws IOException {
return searchEvents(folderPath, and(isEqualTo("outlookmessageclass", "IPM.Task"),
or(isNull("datecompleted"), getPastDelayCondition("datecompleted"))));
}
/**
* Search calendar events in provided folder.
*
* @param folderPath Exchange folder path
* @param filter search filter
* @return list of calendar events
* @throws IOException on error
*/
public List<Event> searchEvents(String folderPath, Condition filter) throws IOException {
Condition privateCondition = null;
if (isSharedFolder(folderPath) && Settings.getBooleanProperty("davmail.excludePrivateEvents", true)) {
LOGGER.debug("Shared or public calendar: exclude private events");
privateCondition = isEqualTo("sensitivity", 0);
}
return searchEvents(folderPath, getItemProperties(),
and(filter, privateCondition));
}
/**
* Search calendar events or messages in provided folder matching the search query.
*
* @param folderPath Exchange folder path
* @param attributes requested attributes
* @param condition Exchange search query
* @return list of calendar messages as Event objects
* @throws IOException on error
*/
public abstract List<Event> searchEvents(String folderPath, Set<String> attributes, Condition condition) throws IOException;
/**
* convert vcf extension to EML.
*
* @param itemName item name
* @return EML item name
*/
protected String convertItemNameToEML(String itemName) {
if (itemName.endsWith(".vcf")) {
return itemName.substring(0, itemName.length() - 3) + "EML";
} else {
return itemName;
}
}
/**
* Get item named eventName in folder
*
* @param folderPath Exchange folder path
* @param itemName event name
* @return event object
* @throws IOException on error
*/
public abstract Item getItem(String folderPath, String itemName) throws IOException;
/**
* Contact picture
*/
public static class ContactPhoto {
/**
* Contact picture content type (always image/jpeg on read)
*/
public String contentType;
/**
* Base64 encoded picture content
*/
public String content;
}
/**
* Retrieve contact photo attached to contact
*
* @param contact address book contact
* @return contact photo
* @throws IOException on error
*/
public abstract ContactPhoto getContactPhoto(Contact contact) throws IOException;
/**
* Delete event named itemName in folder
*
* @param folderPath Exchange folder path
* @param itemName item name
* @throws IOException on error
*/
public abstract void deleteItem(String folderPath, String itemName) throws IOException;
/**
* Mark event processed named eventName in folder
*
* @param folderPath Exchange folder path
* @param itemName item name
* @throws IOException on error
*/
public abstract void processItem(String folderPath, String itemName) throws IOException;
private static int dumpIndex;
/**
* Replace iCal4 (Snow Leopard) principal paths with mailto expression
*
* @param value attendee value or ics line
* @return fixed value
*/
protected String replaceIcal4Principal(String value) {
if (value != null && value.contains("/principals/__uuids__/")) {
return value.replaceAll("/principals/__uuids__/([^/]*)__AT__([^/]*)/", "mailto:$1@$2");
} else {
return value;
}
}
/**
* Event result object to hold HTTP status and event etag from an event creation/update.
*/
public static class ItemResult {
/**
* HTTP status
*/
public int status;
/**
* Event etag from response HTTP header
*/
public String etag;
}
/**
* Build and send the MIME message for the provided ICS event.
*
* @param icsBody event in iCalendar format
* @return HTTP status
* @throws IOException on error
*/
public abstract int sendEvent(String icsBody) throws IOException;
/**
* Create or update item (event or contact) on the Exchange server
*
* @param folderPath Exchange folder path
* @param itemName event name
* @param itemBody event body in iCalendar format
* @param etag previous event etag to detect concurrent updates
* @param noneMatch if-none-match header value
* @return HTTP response event result (status and etag)
* @throws IOException on error
*/
public ItemResult createOrUpdateItem(String folderPath, String itemName, String itemBody, String etag, String noneMatch) throws IOException {
if (itemBody.startsWith("BEGIN:VCALENDAR")) {
return internalCreateOrUpdateEvent(folderPath, itemName, "urn:content-classes:appointment", itemBody, etag, noneMatch);
} else if (itemBody.startsWith("BEGIN:VCARD")) {
return createOrUpdateContact(folderPath, itemName, itemBody, etag, noneMatch);
} else {
throw new IOException(BundleMessage.format("EXCEPTION_INVALID_MESSAGE_CONTENT", itemBody));
}
}
static final String[] VCARD_N_PROPERTIES = {"sn", "givenName", "middlename", "personaltitle", "namesuffix"};
static final String[] VCARD_ADR_HOME_PROPERTIES = {"homepostofficebox", null, "homeStreet", "homeCity", "homeState", "homePostalCode", "homeCountry"};
static final String[] VCARD_ADR_WORK_PROPERTIES = {"postofficebox", "roomnumber", "street", "l", "st", "postalcode", "co"};
static final String[] VCARD_ADR_OTHER_PROPERTIES = {"otherpostofficebox", null, "otherstreet", "othercity", "otherstate", "otherpostalcode", "othercountry"};
static final String[] VCARD_ORG_PROPERTIES = {"o", "department"};
protected void convertContactProperties(Map<String, String> properties, String[] contactProperties, List<String> values) {
for (int i = 0; i < values.size() && i < contactProperties.length; i++) {
if (contactProperties[i] != null) {
properties.put(contactProperties[i], values.get(i));
}
}
}
protected ItemResult createOrUpdateContact(String folderPath, String itemName, String itemBody, String etag, String noneMatch) throws IOException {
// parse VCARD body to build contact property map
Map<String, String> properties = new HashMap<String, String>();
properties.put("outlookmessageclass", "IPM.Contact");
VObject vcard = new VObject(new ICSBufferedReader(new StringReader(itemBody)));
for (VProperty property : vcard.getProperties()) {
if ("FN".equals(property.getKey())) {
properties.put("cn", property.getValue());
properties.put("subject", property.getValue());
properties.put("fileas", property.getValue());
} else if ("N".equals(property.getKey())) {
convertContactProperties(properties, VCARD_N_PROPERTIES, property.getValues());
} else if ("NICKNAME".equals(property.getKey())) {
properties.put("nickname", property.getValue());
} else if ("TEL".equals(property.getKey())) {
if (property.hasParam("TYPE", "cell") || property.hasParam("X-GROUP", "cell")) {
properties.put("mobile", property.getValue());
} else if (property.hasParam("TYPE", "work") || property.hasParam("X-GROUP", "work")) {
properties.put("telephoneNumber", property.getValue());
} else if (property.hasParam("TYPE", "home") || property.hasParam("X-GROUP", "home")) {
properties.put("homePhone", property.getValue());
} else if (property.hasParam("TYPE", "fax")) {
if (property.hasParam("TYPE", "home")) {
properties.put("homefax", property.getValue());
} else {
properties.put("facsimiletelephonenumber", property.getValue());
}
} else if (property.hasParam("TYPE", "pager")) {
properties.put("pager", property.getValue());
} else if (property.hasParam("TYPE", "car")) {
properties.put("othermobile", property.getValue());
} else {
properties.put("otherTelephone", property.getValue());
}
} else if ("ADR".equals(property.getKey())) {
// address
if (property.hasParam("TYPE", "home")) {
convertContactProperties(properties, VCARD_ADR_HOME_PROPERTIES, property.getValues());
} else if (property.hasParam("TYPE", "work")) {
convertContactProperties(properties, VCARD_ADR_WORK_PROPERTIES, property.getValues());
// any other type goes to other address
} else {
convertContactProperties(properties, VCARD_ADR_OTHER_PROPERTIES, property.getValues());
}
} else if ("EMAIL".equals(property.getKey())) {
if (property.hasParam("TYPE", "home")) {
properties.put("email2", property.getValue());
properties.put("smtpemail2", property.getValue());
} else if (property.hasParam("TYPE", "other")) {
properties.put("email3", property.getValue());
properties.put("smtpemail3", property.getValue());
} else {
properties.put("email1", property.getValue());
properties.put("smtpemail1", property.getValue());
}
} else if ("ORG".equals(property.getKey())) {
convertContactProperties(properties, VCARD_ORG_PROPERTIES, property.getValues());
} else if ("URL".equals(property.getKey())) {
if (property.hasParam("TYPE", "work")) {
properties.put("businesshomepage", property.getValue());
} else if (property.hasParam("TYPE", "home")) {
properties.put("personalHomePage", property.getValue());
} else {
// default: set personal home page
properties.put("personalHomePage", property.getValue());
}
} else if ("TITLE".equals(property.getKey())) {
properties.put("title", property.getValue());
} else if ("NOTE".equals(property.getKey())) {
properties.put("description", property.getValue());
} else if ("CUSTOM1".equals(property.getKey())) {
properties.put("extensionattribute1", property.getValue());
} else if ("CUSTOM2".equals(property.getKey())) {
properties.put("extensionattribute2", property.getValue());
} else if ("CUSTOM3".equals(property.getKey())) {
properties.put("extensionattribute3", property.getValue());
} else if ("CUSTOM4".equals(property.getKey())) {
properties.put("extensionattribute4", property.getValue());
} else if ("ROLE".equals(property.getKey())) {
properties.put("profession", property.getValue());
} else if ("X-AIM".equals(property.getKey())) {
properties.put("im", property.getValue());
} else if ("BDAY".equals(property.getKey())) {
properties.put("bday", convertBDayToZulu(property.getValue()));
} else if ("ANNIVERSARY".equals(property.getKey()) || "X-ANNIVERSARY".equals(property.getKey())) {
properties.put("anniversary", convertBDayToZulu(property.getValue()));
} else if ("CATEGORIES".equals(property.getKey())) {
properties.put("keywords", property.getValue());
} else if ("CLASS".equals(property.getKey())) {
if ("PUBLIC".equals(property.getValue())) {
properties.put("sensitivity", "0");
properties.put("private", "false");
} else {
properties.put("sensitivity", "2");
properties.put("private", "true");
}
} else if ("SEX".equals(property.getKey())) {
String propertyValue = property.getValue();
if ("1".equals(propertyValue)) {
properties.put("gender", "2");
} else if ("2".equals(propertyValue)) {
properties.put("gender", "1");
}
} else if ("FBURL".equals(property.getKey())) {
properties.put("fburl", property.getValue());
} else if ("X-ASSISTANT".equals(property.getKey())) {
properties.put("secretarycn", property.getValue());
} else if ("X-MANAGER".equals(property.getKey())) {
properties.put("manager", property.getValue());
} else if ("X-SPOUSE".equals(property.getKey())) {
properties.put("spousecn", property.getValue());
} else if ("PHOTO".equals(property.getKey())) {
properties.put("photo", property.getValue());
properties.put("haspicture", "true");
}
}
LOGGER.debug("Create or update contact " + itemName + ": " + properties);
// reset missing properties to null
for (String key : CONTACT_ATTRIBUTES) {
if (!"imapUid".equals(key) && !"etag".equals(key) && !"urlcompname".equals(key)
&& !"lastmodified".equals(key) && !"sensitivity".equals(key) &&
!properties.containsKey(key)) {
properties.put(key, null);
}
}
return internalCreateOrUpdateContact(folderPath, itemName, properties, etag, noneMatch);
}
protected String convertZuluDateToBday(String value) {
String result = null;
if (value != null && value.length() > 0) {
try {
SimpleDateFormat parser = ExchangeSession.getZuluDateFormat();
Calendar cal = Calendar.getInstance();
cal.setTime(parser.parse(value));
cal.add(Calendar.HOUR_OF_DAY, 12);
result = ExchangeSession.getVcardBdayFormat().format(cal.getTime());
} catch (ParseException e) {
LOGGER.warn("Invalid date: " + value);
}
}
return result;
}
protected String convertBDayToZulu(String value) {
String result = null;
if (value != null && value.length() > 0) {
try {
SimpleDateFormat parser;
if (value.length() == 10) {
parser = ExchangeSession.getVcardBdayFormat();
} else if (value.length() == 15) {
parser = new SimpleDateFormat("yyyyMMdd'T'HHmmss", Locale.ENGLISH);
parser.setTimeZone(GMT_TIMEZONE);
} else {
parser = ExchangeSession.getExchangeZuluDateFormat();
}
result = ExchangeSession.getExchangeZuluDateFormatMillisecond().format(parser.parse(value));
} catch (ParseException e) {
LOGGER.warn("Invalid date: " + value);
}
}
return result;
}
protected abstract ItemResult internalCreateOrUpdateContact(String folderPath, String itemName, Map<String, String> properties, String etag, String noneMatch) throws IOException;
protected abstract ItemResult internalCreateOrUpdateEvent(String folderPath, String itemName, String contentClass, String icsBody, String etag, String noneMatch) throws IOException;
/**
* Get current Exchange alias name from login name
*
* @return user name
*/
public String getAliasFromLogin() {
// login is email, not alias
if (this.userName.indexOf('@') >= 0) {
return null;
}
String result = this.userName;
// remove domain name
int index = Math.max(result.indexOf('\\'), result.indexOf('/'));
if (index >= 0) {
result = result.substring(index + 1);
}
return result;
}
protected String getEmailSuffixFromHostname() {
String domain = httpClient.getHostConfiguration().getHost();
int start = domain.lastIndexOf('.', domain.lastIndexOf('.') - 1);
if (start >= 0) {
return '@' + domain.substring(start + 1);
} else {
return '@' + domain;
}
}
/**
* Test if folderPath is inside user mailbox.
*
* @param folderPath absolute folder path
* @return true if folderPath is a public or shared folder
*/
public abstract boolean isSharedFolder(String folderPath);
/**
* Test if folderPath is main calendar.
*
* @param folderPath absolute folder path
* @return true if folderPath is a public or shared folder
*/
public abstract boolean isMainCalendar(String folderPath);
static final String MAILBOX_BASE = "/cn=";
protected void getEmailAndAliasFromOptions() {
synchronized (httpClient.getState()) {
Cookie[] currentCookies = httpClient.getState().getCookies();
// get user mail URL from html body
BufferedReader optionsPageReader = null;
GetMethod optionsMethod = new GetMethod("/owa/?ae=Options&t=About");
try {
DavGatewayHttpClientFacade.executeGetMethod(httpClient, optionsMethod, false);
optionsPageReader = new BufferedReader(new InputStreamReader(optionsMethod.getResponseBodyAsStream(), "UTF-8"));
String line;
// find email and alias
//noinspection StatementWithEmptyBody
while ((line = optionsPageReader.readLine()) != null
&& (line.indexOf('[') == -1
|| line.indexOf('@') == -1
|| line.indexOf(']') == -1)
&& !line.toLowerCase().contains(MAILBOX_BASE)) {
}
if (line != null) {
int start = line.toLowerCase().lastIndexOf(MAILBOX_BASE) + MAILBOX_BASE.length();
int end = line.indexOf('<', start);
alias = line.substring(start, end);
end = line.lastIndexOf(']');
start = line.lastIndexOf('[', end) + 1;
email = line.substring(start, end);
}
} catch (IOException e) {
// restore cookies on error
httpClient.getState().addCookies(currentCookies);
LOGGER.error("Error parsing options page at " + optionsMethod.getPath());
} finally {
if (optionsPageReader != null) {
try {
optionsPageReader.close();
} catch (IOException e) {
LOGGER.error("Error parsing options page at " + optionsMethod.getPath());
}
}
optionsMethod.releaseConnection();
}
}
}
/**
* Get current user email
*
* @return user email
*/
public String getEmail() {
return email;
}
/**
* Get current user alias
*
* @return user email
*/
public String getAlias() {
return alias;
}
/**
* Search global address list
*
* @param condition search filter
* @param returningAttributes returning attributes
* @param sizeLimit size limit
* @return matching contacts from gal
* @throws IOException on error
*/
public abstract Map<String, Contact> galFind(Condition condition, Set<String> returningAttributes, int sizeLimit) throws IOException;
/**
* Full Contact attribute list
*/
public static final Set<String> CONTACT_ATTRIBUTES = new HashSet<String>();
static {
CONTACT_ATTRIBUTES.add("imapUid");
CONTACT_ATTRIBUTES.add("etag");
CONTACT_ATTRIBUTES.add("urlcompname");
CONTACT_ATTRIBUTES.add("extensionattribute1");
CONTACT_ATTRIBUTES.add("extensionattribute2");
CONTACT_ATTRIBUTES.add("extensionattribute3");
CONTACT_ATTRIBUTES.add("extensionattribute4");
CONTACT_ATTRIBUTES.add("bday");
CONTACT_ATTRIBUTES.add("anniversary");
CONTACT_ATTRIBUTES.add("businesshomepage");
CONTACT_ATTRIBUTES.add("personalHomePage");
CONTACT_ATTRIBUTES.add("cn");
CONTACT_ATTRIBUTES.add("co");
CONTACT_ATTRIBUTES.add("department");
CONTACT_ATTRIBUTES.add("smtpemail1");
CONTACT_ATTRIBUTES.add("smtpemail2");
CONTACT_ATTRIBUTES.add("smtpemail3");
CONTACT_ATTRIBUTES.add("facsimiletelephonenumber");
CONTACT_ATTRIBUTES.add("givenName");
CONTACT_ATTRIBUTES.add("homeCity");
CONTACT_ATTRIBUTES.add("homeCountry");
CONTACT_ATTRIBUTES.add("homePhone");
CONTACT_ATTRIBUTES.add("homePostalCode");
CONTACT_ATTRIBUTES.add("homeState");
CONTACT_ATTRIBUTES.add("homeStreet");
CONTACT_ATTRIBUTES.add("homepostofficebox");
CONTACT_ATTRIBUTES.add("l");
CONTACT_ATTRIBUTES.add("manager");
CONTACT_ATTRIBUTES.add("mobile");
CONTACT_ATTRIBUTES.add("namesuffix");
CONTACT_ATTRIBUTES.add("nickname");
CONTACT_ATTRIBUTES.add("o");
CONTACT_ATTRIBUTES.add("pager");
CONTACT_ATTRIBUTES.add("personaltitle");
CONTACT_ATTRIBUTES.add("postalcode");
CONTACT_ATTRIBUTES.add("postofficebox");
CONTACT_ATTRIBUTES.add("profession");
CONTACT_ATTRIBUTES.add("roomnumber");
CONTACT_ATTRIBUTES.add("secretarycn");
CONTACT_ATTRIBUTES.add("sn");
CONTACT_ATTRIBUTES.add("spousecn");
CONTACT_ATTRIBUTES.add("st");
CONTACT_ATTRIBUTES.add("street");
CONTACT_ATTRIBUTES.add("telephoneNumber");
CONTACT_ATTRIBUTES.add("title");
CONTACT_ATTRIBUTES.add("description");
CONTACT_ATTRIBUTES.add("im");
CONTACT_ATTRIBUTES.add("middlename");
CONTACT_ATTRIBUTES.add("lastmodified");
CONTACT_ATTRIBUTES.add("otherstreet");
CONTACT_ATTRIBUTES.add("otherstate");
CONTACT_ATTRIBUTES.add("otherpostofficebox");
CONTACT_ATTRIBUTES.add("otherpostalcode");
CONTACT_ATTRIBUTES.add("othercountry");
CONTACT_ATTRIBUTES.add("othercity");
CONTACT_ATTRIBUTES.add("haspicture");
CONTACT_ATTRIBUTES.add("keywords");
CONTACT_ATTRIBUTES.add("othermobile");
CONTACT_ATTRIBUTES.add("otherTelephone");
CONTACT_ATTRIBUTES.add("gender");
CONTACT_ATTRIBUTES.add("private");
CONTACT_ATTRIBUTES.add("sensitivity");
CONTACT_ATTRIBUTES.add("fburl");
}
/**
* Get freebusy data string from Exchange.
*
* @param attendee attendee email address
* @param start start date in Exchange zulu format
* @param end end date in Exchange zulu format
* @param interval freebusy interval in minutes
* @return freebusy data or null
* @throws IOException on error
*/
protected abstract String getFreeBusyData(String attendee, String start, String end, int interval) throws IOException;
/**
* Get freebusy info for attendee between start and end date.
*
* @param attendee attendee email
* @param startDateValue start date
* @param endDateValue end date
* @return FreeBusy info
* @throws IOException on error
*/
public FreeBusy getFreebusy(String attendee, String startDateValue, String endDateValue) throws IOException {
// replace ical encoded attendee name
attendee = replaceIcal4Principal(attendee);
// then check that email address is valid to avoid InvalidSmtpAddress error
if (attendee == null || attendee.indexOf('@') < 0 || attendee.charAt(attendee.length() - 1) == '@') {
return null;
}
if (attendee.startsWith("mailto:") || attendee.startsWith("MAILTO:")) {
attendee = attendee.substring("mailto:".length());
}
SimpleDateFormat exchangeZuluDateFormat = getExchangeZuluDateFormat();
SimpleDateFormat icalDateFormat = getZuluDateFormat();
Date startDate;
Date endDate;
try {
if (startDateValue.length() == 8) {
startDate = parseDate(startDateValue);
} else {
startDate = icalDateFormat.parse(startDateValue);
}
if (endDateValue.length() == 8) {
endDate = parseDate(endDateValue);
} else {
endDate = icalDateFormat.parse(endDateValue);
}
} catch (ParseException e) {
throw new DavMailException("EXCEPTION_INVALID_DATES", e.getMessage());
}
FreeBusy freeBusy = null;
String fbdata = getFreeBusyData(attendee, exchangeZuluDateFormat.format(startDate), exchangeZuluDateFormat.format(endDate), FREE_BUSY_INTERVAL);
if (fbdata != null) {
freeBusy = new FreeBusy(icalDateFormat, startDate, fbdata);
}
if (freeBusy != null && freeBusy.knownAttendee) {
return freeBusy;
} else {
return null;
}
}
/**
* Exchange to iCalendar Free/Busy parser.
* Free time returns 0, Tentative returns 1, Busy returns 2, and Out of Office (OOF) returns 3
*/
public static final class FreeBusy {
final SimpleDateFormat icalParser;
boolean knownAttendee = true;
static final HashMap<Character, String> FBTYPES = new HashMap<Character, String>();
static {
FBTYPES.put('1', "BUSY-TENTATIVE");
FBTYPES.put('2', "BUSY");
FBTYPES.put('3', "BUSY-UNAVAILABLE");
}
final HashMap<String, StringBuilder> busyMap = new HashMap<String, StringBuilder>();
StringBuilder getBusyBuffer(char type) {
String fbType = FBTYPES.get(Character.valueOf(type));
StringBuilder buffer = busyMap.get(fbType);
if (buffer == null) {
buffer = new StringBuilder();
busyMap.put(fbType, buffer);
}
return buffer;
}
void startBusy(char type, Calendar currentCal) {
if (type == '4') {
knownAttendee = false;
} else if (type != '0') {
StringBuilder busyBuffer = getBusyBuffer(type);
if (busyBuffer.length() > 0) {
busyBuffer.append(',');
}
busyBuffer.append(icalParser.format(currentCal.getTime()));
}
}
void endBusy(char type, Calendar currentCal) {
if (type != '0' && type != '4') {
getBusyBuffer(type).append('/').append(icalParser.format(currentCal.getTime()));
}
}
FreeBusy(SimpleDateFormat icalParser, Date startDate, String fbdata) {
this.icalParser = icalParser;
if (fbdata.length() > 0) {
Calendar currentCal = Calendar.getInstance(TimeZone.getTimeZone("UTC"));
currentCal.setTime(startDate);
startBusy(fbdata.charAt(0), currentCal);
for (int i = 1; i < fbdata.length() && knownAttendee; i++) {
currentCal.add(Calendar.MINUTE, FREE_BUSY_INTERVAL);
char previousState = fbdata.charAt(i - 1);
char currentState = fbdata.charAt(i);
if (previousState != currentState) {
endBusy(previousState, currentCal);
startBusy(currentState, currentCal);
}
}
currentCal.add(Calendar.MINUTE, FREE_BUSY_INTERVAL);
endBusy(fbdata.charAt(fbdata.length() - 1), currentCal);
}
}
/**
* Append freebusy information to buffer.
*
* @param buffer String buffer
*/
public void appendTo(StringBuilder buffer) {
for (Map.Entry<String, StringBuilder> entry : busyMap.entrySet()) {
buffer.append("FREEBUSY;FBTYPE=").append(entry.getKey())
.append(':').append(entry.getValue()).append((char) 13).append((char) 10);
}
}
}
protected VObject vTimezone;
/**
* Load and return current user OWA timezone.
*
* @return current timezone
*/
public VObject getVTimezone() {
if (vTimezone == null) {
// need to load Timezone info from OWA
loadVtimezone();
}
return vTimezone;
}
protected abstract void loadVtimezone();
/**
* Return internal HttpClient instance
*
* @return http client
*/
public HttpClient getHttpClient() {
return httpClient;
}
}