com.zimbra.cs.fb.ExchangeFreeBusyProvider.java Source code

Java tutorial

Introduction

Here is the source code for com.zimbra.cs.fb.ExchangeFreeBusyProvider.java

Source

/*
 * ***** BEGIN LICENSE BLOCK *****
 * Zimbra Collaboration Suite Server
 * Copyright (C) 2008, 2009, 2010, 2011, 2012, 2013, 2014, 2015, 2016 Synacor, Inc.
 *
 * 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,
 * version 2 of the License.
 *
 * 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, see <https://www.gnu.org/licenses/>.
 * ***** END LICENSE BLOCK *****
 */
package com.zimbra.cs.fb;

import java.io.IOException;
import java.io.InputStream;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.EnumSet;
import java.util.GregorianCalendar;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.apache.commons.httpclient.Credentials;
import org.apache.commons.httpclient.Header;
import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.HttpMethod;
import org.apache.commons.httpclient.HttpState;
import org.apache.commons.httpclient.UsernamePasswordCredentials;
import org.apache.commons.httpclient.auth.AuthPolicy;
import org.apache.commons.httpclient.auth.AuthScope;
import org.apache.commons.httpclient.methods.ByteArrayRequestEntity;
import org.apache.commons.httpclient.methods.GetMethod;
import org.apache.commons.httpclient.methods.PostMethod;

import com.zimbra.common.account.Key.AccountBy;
import com.zimbra.common.httpclient.HttpClientUtil;
import com.zimbra.common.localconfig.LC;
import com.zimbra.common.service.ServiceException;
import com.zimbra.common.soap.Element;
import com.zimbra.common.soap.XmlParseException;
import com.zimbra.common.util.ByteUtil;
import com.zimbra.common.util.Constants;
import com.zimbra.common.util.DateUtil;
import com.zimbra.common.util.ZimbraHttpConnectionManager;
import com.zimbra.common.util.ZimbraLog;
import com.zimbra.cs.account.Account;
import com.zimbra.cs.account.Domain;
import com.zimbra.cs.account.Provisioning;
import com.zimbra.cs.httpclient.HttpProxyUtil;
import com.zimbra.cs.mailbox.MailItem;
import com.zimbra.cs.mailbox.calendar.IcalXmlStrMap;

public class ExchangeFreeBusyProvider extends FreeBusyProvider {

    public static final String USER_AGENT = "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 1.1.4322)";
    //public static final String FORM_AUTH_PATH = "/exchweb/bin/auth/owaauth.dll";  // specified in LC.calendar_exchange_form_auth_url
    public static final int MULTI_STATUS = 207;
    public static final String TYPE_WEBDAV = "webdav";

    public enum AuthScheme {
        basic, form
    };

    public static class ServerInfo {
        public boolean enabled;
        public String url;
        public String org;
        public String cn;
        public String authUsername;
        public String authPassword;
        public AuthScheme scheme;
    }

    public static interface ExchangeUserResolver {
        public ServerInfo getServerInfo(String emailAddr);
    }

    private static class BasicUserResolver implements ExchangeUserResolver {
        @Override
        public ServerInfo getServerInfo(String emailAddr) {
            String url = getAttr(Provisioning.A_zimbraFreebusyExchangeURL, emailAddr);
            String user = getAttr(Provisioning.A_zimbraFreebusyExchangeAuthUsername, emailAddr);
            String pass = getAttr(Provisioning.A_zimbraFreebusyExchangeAuthPassword, emailAddr);
            String scheme = getAttr(Provisioning.A_zimbraFreebusyExchangeAuthScheme, emailAddr);
            if (url == null || user == null || pass == null || scheme == null)
                return null;

            ServerInfo info = new ServerInfo();
            info.url = url;
            info.authUsername = user;
            info.authPassword = pass;
            info.scheme = AuthScheme.valueOf(scheme);
            info.org = getAttr(Provisioning.A_zimbraFreebusyExchangeUserOrg, emailAddr);
            try {
                Account acct = null;
                if (emailAddr != null) {
                    acct = Provisioning.getInstance().get(AccountBy.name, emailAddr);
                }
                if (acct != null) {
                    String fps[] = acct.getMultiAttr(Provisioning.A_zimbraForeignPrincipal);
                    if (fps != null && fps.length > 0) {
                        for (String fp : fps) {
                            if (fp.startsWith(Provisioning.FP_PREFIX_AD)) {
                                int idx = fp.indexOf(':');
                                if (idx != -1) {
                                    info.cn = fp.substring(idx + 1);
                                    break;
                                }
                            }
                        }
                    }
                }
            } catch (ServiceException se) {
                info.cn = null;
            }
            String exchangeType = getAttr(Provisioning.A_zimbraFreebusyExchangeServerType, emailAddr);
            info.enabled = TYPE_WEBDAV.equals(exchangeType);
            return info;
        }

        // first lookup account/cos, then domain, then globalConfig.
        static String getAttr(String attr, String emailAddr) {
            String val = null;
            if (attr == null)
                return val;
            try {
                Provisioning prov = Provisioning.getInstance();
                if (emailAddr != null) {
                    Account acct = prov.get(AccountBy.name, emailAddr);
                    if (acct != null) {
                        val = acct.getAttr(attr, null);
                        if (val != null)
                            return val;
                        Domain dom = prov.getDomain(acct);
                        if (dom != null)
                            val = dom.getAttr(attr, null);
                        if (val != null)
                            return val;
                    }
                }
                val = prov.getConfig().getAttr(attr, null);
            } catch (ServiceException se) {
                ZimbraLog.fb.error("can't get attr " + attr, se);
            }
            return val;
        }
    }

    private static ArrayList<ExchangeUserResolver> sRESOLVERS;
    static {
        sRESOLVERS = new ArrayList<ExchangeUserResolver>();

        registerResolver(new BasicUserResolver(), 0);
        register(new ExchangeFreeBusyProvider());
    }

    public static void registerResolver(ExchangeUserResolver r, int priority) {
        synchronized (sRESOLVERS) {
            sRESOLVERS.ensureCapacity(priority + 1);
            sRESOLVERS.add(priority, r);
        }
    }

    @Override
    public FreeBusyProvider getInstance() {
        return new ExchangeFreeBusyProvider();
    }

    @Override
    public void addFreeBusyRequest(Request req) throws FreeBusyUserNotFoundException {
        ServerInfo info = null;
        for (ExchangeUserResolver resolver : sRESOLVERS) {
            String email = req.email;
            if (req.requestor != null)
                email = req.requestor.getName();
            info = resolver.getServerInfo(email);
            if (info != null)
                break;
        }
        if (info == null)
            throw new FreeBusyUserNotFoundException();
        if (!info.enabled)
            throw new FreeBusyUserNotFoundException();
        addRequest(info, req);
    }

    private void addRequest(ServerInfo info, Request req) {
        ArrayList<Request> r = mRequests.get(info.url);
        if (r == null) {
            r = new ArrayList<Request>();
            mRequests.put(info.url, r);
        }
        req.data = info;
        r.add(req);
    }

    private Map<String, ArrayList<Request>> mRequests;

    @Override
    public List<FreeBusy> getResults() {
        ArrayList<FreeBusy> ret = new ArrayList<FreeBusy>();
        for (Map.Entry<String, ArrayList<Request>> entry : mRequests.entrySet()) {
            try {
                ret.addAll(this.getFreeBusyForHost(entry.getKey(), entry.getValue()));
            } catch (IOException e) {
                ZimbraLog.fb.error("error communicating with " + entry.getKey(), e);
                return getEmptyList(entry.getValue());
            }
        }
        return ret;
    }

    private static final String EXCHANGE = "EXCHANGE";
    private static final String HEADER_USER_AGENT = "User-Agent";
    private static final String HEADER_TRANSLATE = "Translate";

    @Override
    public String foreignPrincipalPrefix() {
        return Provisioning.FP_PREFIX_AD;
    }

    @Override
    public String getName() {
        return EXCHANGE;
    }

    @Override
    public Set<MailItem.Type> registerForItemTypes() {
        return EnumSet.of(MailItem.Type.APPOINTMENT);
    }

    @Override
    public boolean registerForMailboxChanges() {
        return registerForMailboxChanges(null);
    }

    @Override
    public boolean registerForMailboxChanges(String accountId) {
        String email = null;
        try {
            Account account = null;
            if (accountId != null)
                account = Provisioning.getInstance().getAccountById(accountId);
            if (account != null)
                email = account.getName();
        } catch (ServiceException se) {
            ZimbraLog.fb.warn("cannot fetch account", se);
        }
        return getServerInfo(email) != null;
    }

    private long getTimeInterval(String attr, String accountId, long defaultValue) throws ServiceException {
        Provisioning prov = Provisioning.getInstance();
        if (accountId != null) {
            Account acct = prov.get(AccountBy.id, accountId);
            if (acct != null) {
                return acct.getTimeInterval(attr, defaultValue);
            }
        }
        return prov.getConfig().getTimeInterval(attr, defaultValue);
    }

    @Override
    public long cachedFreeBusyStartTime(String accountId) {
        Calendar cal = GregorianCalendar.getInstance();
        int curYear = cal.get(Calendar.YEAR);
        try {
            long dur = getTimeInterval(Provisioning.A_zimbraFreebusyExchangeCachedIntervalStart, accountId, 0);
            cal.setTimeInMillis(System.currentTimeMillis() - dur);
        } catch (ServiceException se) {
            // set to 1 week ago
            cal.setTimeInMillis(System.currentTimeMillis() - Constants.MILLIS_PER_WEEK);
        }
        // normalize the time to 00:00:00
        cal.set(Calendar.HOUR_OF_DAY, 0);
        cal.set(Calendar.MINUTE, 0);
        cal.set(Calendar.SECOND, 0);
        if (cal.get(Calendar.YEAR) < curYear) {
            // Exchange accepts FB info for only one calendar year. If the start date falls in the previous year
            // change it to beginning of the current year.
            cal.set(curYear, 0, 1);
        }
        return cal.getTimeInMillis();
    }

    @Override
    public long cachedFreeBusyEndTime(String accountId) {
        long duration = Constants.MILLIS_PER_MONTH * 2;
        Calendar cal = GregorianCalendar.getInstance();
        try {
            duration = getTimeInterval(Provisioning.A_zimbraFreebusyExchangeCachedInterval, accountId, duration);
        } catch (ServiceException se) {
        }
        cal.setTimeInMillis(cachedFreeBusyStartTime(accountId) + duration);
        // normalize the time to 00:00:00
        cal.set(Calendar.HOUR_OF_DAY, 0);
        cal.set(Calendar.MINUTE, 0);
        cal.set(Calendar.SECOND, 0);
        return cal.getTimeInMillis();
    }

    @Override
    public long cachedFreeBusyStartTime() {
        return cachedFreeBusyStartTime(null);
    }

    @Override
    public long cachedFreeBusyEndTime() {
        return cachedFreeBusyEndTime(null);
    }

    @Override
    public boolean handleMailboxChange(String accountId) {
        String email = getEmailAddress(accountId);
        ServerInfo serverInfo = getServerInfo(email);
        if (email == null || !serverInfo.enabled) {
            return true; // no retry
        }
        FreeBusy fb;
        try {
            fb = getFreeBusy(accountId, FreeBusyQuery.CALENDAR_FOLDER_ALL);
        } catch (ServiceException se) {
            ZimbraLog.fb.warn("can't get freebusy for account " + accountId, se);
            // retry the request if it's receivers fault.
            return !se.isReceiversFault();
        }
        if (email == null || fb == null) {
            ZimbraLog.fb.warn("account not found / incorrect / wrong host: " + accountId);
            return true; // no retry
        }
        if (serverInfo == null || serverInfo.org == null || serverInfo.cn == null) {
            ZimbraLog.fb.warn("no exchange server info for user " + email);
            return true; // no retry
        }
        ExchangeMessage msg = new ExchangeMessage(serverInfo.org, serverInfo.cn, email);
        String url = serverInfo.url + msg.getUrl();

        HttpMethod method = null;

        try {
            ZimbraLog.fb.debug("POST " + url);
            method = msg.createMethod(url, fb);
            method.setRequestHeader(HEADER_TRANSLATE, "f");
            int status = sendRequest(method, serverInfo);
            if (status != MULTI_STATUS) {
                InputStream resp = method.getResponseBodyAsStream();
                String respStr = (resp == null ? "" : new String(ByteUtil.readInput(resp, 1024, 1024), "UTF-8"));
                ZimbraLog.fb.error("cannot modify resource at %s : http error %d, buf (%s)", url, status, respStr);
                return false; // retry
            }
        } catch (IOException ioe) {
            ZimbraLog.fb.error("error communicating with " + serverInfo.url, ioe);
            return false; // retry
        } finally {
            method.releaseConnection();
        }
        return true;
    }

    private int sendRequest(HttpMethod method, ServerInfo info) throws IOException {
        method.setDoAuthentication(true);
        method.setRequestHeader(HEADER_USER_AGENT, USER_AGENT);
        HttpClient client = ZimbraHttpConnectionManager.getExternalHttpConnMgr().newHttpClient();
        HttpProxyUtil.configureProxy(client);
        switch (info.scheme) {
        case basic:
            basicAuth(client, info);
            break;
        case form:
            formAuth(client, info);
            break;
        }
        return HttpClientUtil.executeMethod(client, method);
    }

    private boolean basicAuth(HttpClient client, ServerInfo info) {
        HttpState state = new HttpState();
        Credentials cred = new UsernamePasswordCredentials(info.authUsername, info.authPassword);
        state.setCredentials(AuthScope.ANY, cred);
        client.setState(state);
        ArrayList<String> authPrefs = new ArrayList<String>();
        authPrefs.add(AuthPolicy.BASIC);
        client.getParams().setParameter(AuthPolicy.AUTH_SCHEME_PRIORITY, authPrefs);
        return true;
    }

    private boolean formAuth(HttpClient client, ServerInfo info) throws IOException {
        StringBuilder buf = new StringBuilder();
        buf.append("destination=");
        buf.append(URLEncoder.encode(info.url, "UTF-8"));
        buf.append("&username=");
        buf.append(info.authUsername);
        buf.append("&password=");
        buf.append(URLEncoder.encode(info.authPassword, "UTF-8"));
        buf.append("&flags=0");
        buf.append("&SubmitCreds=Log On");
        buf.append("&trusted=0");
        String url = info.url + LC.calendar_exchange_form_auth_url.value();
        PostMethod method = new PostMethod(url);
        ByteArrayRequestEntity re = new ByteArrayRequestEntity(buf.toString().getBytes(), "x-www-form-urlencoded");
        method.setRequestEntity(re);
        HttpState state = new HttpState();
        client.setState(state);
        try {
            int status = HttpClientUtil.executeMethod(client, method);
            if (status >= 400) {
                ZimbraLog.fb.error("form auth to Exchange returned an error: " + status);
                return false;
            }
        } finally {
            method.releaseConnection();
        }
        return true;
    }

    ExchangeFreeBusyProvider() {
        mRequests = new HashMap<String, ArrayList<Request>>();
    }

    public List<FreeBusy> getFreeBusyForHost(String host, ArrayList<Request> req) throws IOException {
        ArrayList<FreeBusy> ret = new ArrayList<FreeBusy>();
        int fb_interval = LC.exchange_free_busy_interval_min.intValueWithinRange(5, 1444);
        Request r = req.get(0);
        ServerInfo serverInfo = (ServerInfo) r.data;
        if (serverInfo == null) {
            ZimbraLog.fb.warn("no exchange server info for user " + r.email);
            return ret;
        }
        if (!serverInfo.enabled) {
            return ret;
        }
        String url = constructGetUrl(serverInfo, req);
        ZimbraLog.fb.debug("fetching fb from url=" + url);
        HttpMethod method = new GetMethod(url);

        Element response = null;
        try {
            int status = sendRequest(method, serverInfo);
            if (status != 200)
                return getEmptyList(req);
            if (ZimbraLog.fb.isDebugEnabled()) {
                Header cl = method.getResponseHeader("Content-Length");
                int contentLength = 10240;
                if (cl != null)
                    contentLength = Integer.valueOf(cl.getValue());
                String buf = new String(com.zimbra.common.util.ByteUtil.readInput(method.getResponseBodyAsStream(),
                        contentLength, contentLength), "UTF-8");
                ZimbraLog.fb.debug(buf);
                response = Element.parseXML(buf);
            } else
                response = Element.parseXML(method.getResponseBodyAsStream());
        } catch (XmlParseException e) {
            ZimbraLog.fb.warn("error parsing fb response from exchange", e);
            return getEmptyList(req);
        } catch (IOException e) {
            ZimbraLog.fb.warn("error parsing fb response from exchange", e);
            return getEmptyList(req);
        } finally {
            method.releaseConnection();
        }
        for (Request re : req) {
            String fb = getFbString(response, re.email);
            ret.add(new ExchangeUserFreeBusy(fb, re.email, fb_interval, re.start, re.end));
        }
        return ret;
    }

    private String constructGetUrl(ServerInfo info, ArrayList<Request> req) {
        // http://exchange/public/?params..
        //   cmd      = freebusy
        //   start    = [ISO8601date]
        //   end      = [ISO8601date]
        //   interval = [minutes]
        //   u        = SMTP:[emailAddr]
        int fb_interval = LC.exchange_free_busy_interval_min.intValueWithinRange(5, 1444);
        long start = Request.offsetInterval(req.get(0).start, fb_interval);
        StringBuilder buf = new StringBuilder(info.url);
        buf.append("/public/?cmd=freebusy");
        buf.append("&start=").append(DateUtil.toISO8601(new Date(start)));
        buf.append("&end=").append(DateUtil.toISO8601(new Date(req.get(0).end)));
        buf.append("&interval=").append(fb_interval);
        for (Request r : req)
            buf.append("&u=SMTP:").append(r.email);
        return buf.toString();
    }

    private String getFbString(Element fbxml, String emailAddr) {
        String ret = "";
        Element node = fbxml.getOptionalElement("recipients");
        if (node != null) {
            for (Element e : node.listElements("item")) {
                Element email = e.getOptionalElement("email");
                if (email != null && email.getText().trim().equalsIgnoreCase(emailAddr)) {
                    Element fb = e.getOptionalElement("fbdata");
                    if (fb != null) {
                        ret = fb.getText();
                    }
                    break;
                }
            }
        }
        return ret;
    }

    public ServerInfo getServerInfo(String emailAddr) {
        ServerInfo serverInfo = null;
        for (ExchangeUserResolver r : sRESOLVERS) {
            serverInfo = r.getServerInfo(emailAddr);
            if (serverInfo != null)
                break;
        }
        return serverInfo;
    }

    public static int checkAuth(ServerInfo info, Account requestor) throws ServiceException, IOException {
        ExchangeFreeBusyProvider prov = new ExchangeFreeBusyProvider();
        ArrayList<Request> req = new ArrayList<Request>();
        req.add(new Request(requestor, requestor.getName(), prov.cachedFreeBusyStartTime(),
                prov.cachedFreeBusyEndTime(), FreeBusyQuery.CALENDAR_FOLDER_ALL));
        String url = prov.constructGetUrl(info, req);

        HttpMethod method = new GetMethod(url);
        try {
            return prov.sendRequest(method, info);
        } finally {
            method.releaseConnection();
        }
    }

    public static class ExchangeUserFreeBusy extends FreeBusy {
        /*
        <a:response xmlns:a="WM">
            <a:recipients>
                <a:item>
                    <a:displayname>All Attendees</a:displayname>
                    <a:type>1</a:type>
                    <a:fbdata>000020000000000000000000000000000000000022220000</a:fbdata>
                </a:item>
                <a:item>
                    <a:displayname>test user</a:displayname>
                    <a:email type="SMTP">testuser@2k3-ex2k3.local</a:email>
                    <a:type>1</a:type>
                    <a:fbdata>000020000000000000000000000000000000000022220000</a:fbdata>
                </a:item>
            </a:recipients>
        </a:response>
         */
        private final char FREE = '0';
        private final char TENTATIVE = '1';
        private final char BUSY = '2';
        private final char UNAVAILABLE = '3';
        private final char NODATA = '4';

        protected ExchangeUserFreeBusy(String fb, String emailAddr, int interval, long start, long end) {
            super(emailAddr, start, end);
            parseInterval(fb, emailAddr, interval, start, end);
        }

        private void parseInterval(String fb, String emailAddr, int interval, long start, long end) {
            long intervalInMillis = interval * 60L * 1000L;
            long maskedStart = Request.offsetInterval(start, interval);
            if (maskedStart < start) {
                end = maskedStart + intervalInMillis;
            } else {
                end = start + intervalInMillis;
            }
            for (int i = 0; i < fb.length(); i++) {
                switch (fb.charAt(i)) {
                case TENTATIVE:
                    mList.addInterval(new Interval(start, end, IcalXmlStrMap.FBTYPE_BUSY_TENTATIVE));
                    break;
                case BUSY:
                    mList.addInterval(new Interval(start, end, IcalXmlStrMap.FBTYPE_BUSY));
                    break;
                case UNAVAILABLE:
                    mList.addInterval(new Interval(start, end, IcalXmlStrMap.FBTYPE_BUSY_UNAVAILABLE));
                    break;
                case NODATA:
                    mList.addInterval(new Interval(start, end, IcalXmlStrMap.FBTYPE_NODATA));
                    break;
                case FREE:
                default:
                    break;
                }
                start = end;
                end = start + intervalInMillis;
            }
        }
    }
}