com.zimbra.qa.unittest.TestCalDav.java Source code

Java tutorial

Introduction

Here is the source code for com.zimbra.qa.unittest.TestCalDav.java

Source

/*
 * ***** BEGIN LICENSE BLOCK *****
 * Zimbra Collaboration Suite Server
 * Copyright (C) 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.qa.unittest;

import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.StringWriter;
import java.io.UnsupportedEncodingException;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import javax.xml.XMLConstants;
import javax.xml.bind.DatatypeConverter;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpression;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;

import net.fortuna.ical4j.model.TimeZoneRegistry;
import net.fortuna.ical4j.model.TimeZoneRegistryFactory;

import org.apache.commons.httpclient.Header;
import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.HttpMethod;
import org.apache.commons.httpclient.HttpStatus;
import org.apache.commons.httpclient.methods.ByteArrayRequestEntity;
import org.apache.commons.httpclient.methods.DeleteMethod;
import org.apache.commons.httpclient.methods.EntityEnclosingMethod;
import org.apache.commons.httpclient.methods.GetMethod;
import org.apache.commons.httpclient.methods.PostMethod;
import org.apache.commons.httpclient.methods.PutMethod;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

import com.google.common.base.Strings;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.io.Closeables;
import com.zimbra.client.ZFolder;
import com.zimbra.client.ZFolder.View;
import com.zimbra.client.ZMailbox;
import com.zimbra.client.ZMessage;
import com.zimbra.common.account.Key.AccountBy;
import com.zimbra.common.account.ZAttrProvisioning;
import com.zimbra.common.calendar.ICalTimeZone;
import com.zimbra.common.calendar.ParsedDateTime;
import com.zimbra.common.calendar.WellKnownTimeZones;
import com.zimbra.common.calendar.ZCalendar;
import com.zimbra.common.calendar.ZCalendar.ICalTok;
import com.zimbra.common.calendar.ZCalendar.ZComponent;
import com.zimbra.common.calendar.ZCalendar.ZParameter;
import com.zimbra.common.calendar.ZCalendar.ZProperty;
import com.zimbra.common.calendar.ZCalendar.ZVCalendar;
import com.zimbra.common.httpclient.HttpClientUtil;
import com.zimbra.common.localconfig.DebugConfig;
import com.zimbra.common.localconfig.LC;
import com.zimbra.common.mime.MimeConstants;
import com.zimbra.common.service.ServiceException;
import com.zimbra.common.soap.Element;
import com.zimbra.common.soap.W3cDomUtil;
import com.zimbra.common.soap.XmlParseException;
import com.zimbra.common.util.ByteUtil;
import com.zimbra.common.util.Constants;
import com.zimbra.common.util.ZimbraLog;
import com.zimbra.cs.account.Account;
import com.zimbra.cs.account.DistributionList;
import com.zimbra.cs.account.MailTarget;
import com.zimbra.cs.account.Provisioning;
import com.zimbra.cs.account.Server;
import com.zimbra.cs.dav.DavElements;
import com.zimbra.cs.dav.DavProtocol;
import com.zimbra.cs.dav.resource.UrlNamespace;
import com.zimbra.cs.dav.service.DavServlet;
import com.zimbra.cs.ldap.LdapUtil;
import com.zimbra.cs.mailbox.MailItem;
import com.zimbra.cs.mailbox.Mailbox;
import com.zimbra.soap.account.message.ModifyPrefsRequest;
import com.zimbra.soap.account.message.ModifyPrefsResponse;
import com.zimbra.soap.account.type.Pref;
import com.zimbra.soap.mail.message.CreateMountpointRequest;
import com.zimbra.soap.mail.message.CreateMountpointResponse;
import com.zimbra.soap.mail.message.SearchRequest;
import com.zimbra.soap.mail.message.SearchResponse;
import com.zimbra.soap.mail.type.ContactInfo;
import com.zimbra.soap.mail.type.NewMountpointSpec;
import com.zimbra.soap.type.SearchHit;

import org.junit.After;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.fail;

public class TestCalDav {

    static final TimeZoneRegistry tzRegistry = TimeZoneRegistryFactory.getInstance().createRegistry();
    private static String NAME_PREFIX = "TestCalDav";
    private static TestUtil.UserInfo[] users = { new TestUtil.UserInfo("dav0user"),
            new TestUtil.UserInfo("dav1user"), new TestUtil.UserInfo("dav2user"), new TestUtil.UserInfo("dav3user"),
            new TestUtil.UserInfo("dav4user") };
    private static String DL1 = "davdistlist1";
    private static Server localServer = null;
    private static final Provisioning prov = Provisioning.getInstance();

    public static class MkColMethod extends EntityEnclosingMethod {
        @Override
        public String getName() {
            return "MKCOL";
        }

        public MkColMethod(String uri) {
            super(uri);
        }
    }

    public static class PropPatchMethod extends EntityEnclosingMethod {
        @Override
        public String getName() {
            return "PROPPATCH";
        }

        public PropPatchMethod(String uri) {
            super(uri);
        }
    }

    public static class PropFindMethod extends EntityEnclosingMethod {
        @Override
        public String getName() {
            return "PROPFIND";
        }

        public PropFindMethod(String uri) {
            super(uri);
        }
    }

    public static class ReportMethod extends EntityEnclosingMethod {
        @Override
        public String getName() {
            return "REPORT";
        }

        public ReportMethod(String uri) {
            super(uri);
        }
    }

    private static final Map<String, String> caldavNSMap;
    static {
        Map<String, String> aMap = Maps.newHashMapWithExpectedSize(2);
        aMap.put("D", DavElements.WEBDAV_NS_STRING);
        aMap.put("C", DavElements.CALDAV_NS_STRING);
        aMap.put("CS", DavElements.CS_NS_STRING);
        caldavNSMap = Collections.unmodifiableMap(aMap);
    }

    public static class NamespaceContextForXPath implements javax.xml.namespace.NamespaceContext {
        private final Map<String, String> nsMap;

        public NamespaceContextForXPath(Map<String, String> nsMap) {
            this.nsMap = nsMap;
        }

        public static NamespaceContextForXPath forCalDAV() {
            return new NamespaceContextForXPath(caldavNSMap);
        }

        @Override
        public String getNamespaceURI(String prefix) {
            if (prefix == null) {
                throw new NullPointerException("Null prefix");
            }

            String nsURI = nsMap.get(prefix);
            if (nsURI == null) {
                nsURI = XMLConstants.NULL_NS_URI;
                ZimbraLog.test.info("NamespaceContextForXPath.getNamespaceURI(prefix) - Unexpected prefix %s",
                        prefix);
            }
            return nsURI;
        }

        /**
         * Not used by XPath
         */
        @Override
        public String getPrefix(String uri) {
            throw new UnsupportedOperationException();
        }

        /**
         * Not used by XPath
         */
        @Override
        public Iterator getPrefixes(String uri) {
            throw new UnsupportedOperationException();
        }
    }

    public static ParsedDateTime parsedDateTime(int year, int month, int day, int hour, int min,
            ICalTimeZone icalTimeZone) {
        GregorianCalendar date = new GregorianCalendar();
        date.set(java.util.Calendar.YEAR, year);
        date.set(java.util.Calendar.MONTH, month);
        date.set(java.util.Calendar.DAY_OF_MONTH, month);
        date.set(java.util.Calendar.HOUR_OF_DAY, hour);
        date.set(java.util.Calendar.MINUTE, min);
        date.set(java.util.Calendar.SECOND, 0);
        return ParsedDateTime.fromUTCTime(date.getTimeInMillis(), icalTimeZone);
    }

    public static ZProperty attendee(MailTarget mailTarget, ICalTok role, ICalTok cutype, ICalTok partstat) {
        ZProperty att = new ZProperty(ICalTok.ATTENDEE, "mailto:" + mailTarget.getName());
        String displayName = mailTarget.getAttr(Provisioning.A_displayName);
        if (Strings.isNullOrEmpty(displayName)) {
            displayName = mailTarget.getName().substring(0, mailTarget.getName().indexOf("@"));
        }
        att.addParameter(new ZParameter(ICalTok.CN, displayName));
        att.addParameter(new ZParameter(ICalTok.ROLE, role.toString()));
        att.addParameter(new ZParameter(ICalTok.CUTYPE, cutype.toString()));
        att.addParameter(new ZParameter(ICalTok.PARTSTAT, partstat.toString()));
        return att;
    }

    public static ZProperty organizer(Account acct) {
        ZProperty org = new ZProperty(ICalTok.ORGANIZER, "mailto:" + acct.getName());
        String displayName = acct.getDisplayName();
        if (Strings.isNullOrEmpty(displayName)) {
            displayName = acct.getName().substring(0, acct.getName().indexOf("@"));
        }
        org.addParameter(new ZParameter(ICalTok.CN, displayName));
        return org;
    }

    public String exampleCancelIcal(Account organizer, Account attendee1, Account attendee2) throws IOException {
        ZVCalendar vcal = new ZVCalendar();
        vcal.addVersionAndProdId();

        vcal.addProperty(new ZProperty(ICalTok.METHOD, ICalTok.CANCEL.toString()));
        ICalTimeZone tz = ICalTimeZone.lookupByTZID("Africa/Harare");
        vcal.addComponent(tz.newToVTimeZone());
        ZComponent vevent = new ZComponent(ICalTok.VEVENT);
        ParsedDateTime dtstart = parsedDateTime(2020, java.util.Calendar.APRIL, 1, 9, 0, tz);
        vevent.addProperty(dtstart.toProperty(ICalTok.DTSTART, false));
        ParsedDateTime dtend = parsedDateTime(2020, java.util.Calendar.APRIL, 1, 13, 0, tz);
        vevent.addProperty(dtend.toProperty(ICalTok.DTEND, false));
        vevent.addProperty(new ZProperty(ICalTok.DTSTAMP, "20140108T224700Z"));
        vevent.addProperty(new ZProperty(ICalTok.SUMMARY, "Meeting for fun"));
        vevent.addProperty(new ZProperty(ICalTok.UID, "d123f102-42a7-4283-b025-3376dabe53b3"));
        vevent.addProperty(new ZProperty(ICalTok.STATUS, ICalTok.CANCELLED.toString()));
        vevent.addProperty(new ZProperty(ICalTok.SEQUENCE, "1"));
        vevent.addProperty(organizer(organizer));
        vevent.addProperty(attendee(attendee1, ICalTok.REQ_PARTICIPANT, ICalTok.INDIVIDUAL, ICalTok.NEEDS_ACTION));
        vevent.addProperty(attendee(attendee2, ICalTok.REQ_PARTICIPANT, ICalTok.INDIVIDUAL, ICalTok.NEEDS_ACTION));
        vcal.addComponent(vevent);
        StringWriter calWriter = new StringWriter();
        vcal.toICalendar(calWriter);
        String icalString = calWriter.toString();
        Closeables.closeQuietly(calWriter);
        return icalString;
    }

    public static class HttpMethodExecutor {
        public int respCode;
        public int statusCode;
        public String statusLine;
        public Header[] respHeaders;
        public byte[] responseBodyBytes;

        public HttpMethodExecutor(HttpClient client, HttpMethod method, int expectedCode) throws IOException {
            try {
                respCode = HttpClientUtil.executeMethod(client, method);
                statusCode = method.getStatusCode();
                statusLine = method.getStatusLine().toString();

                respHeaders = method.getResponseHeaders();
                StringBuilder hdrsSb = new StringBuilder();
                for (Header hdr : respHeaders) {
                    hdrsSb.append(hdr.toString());
                }
                try (InputStream responseStream = method.getResponseBodyAsStream()) {
                    if (responseStream == null) {
                        responseBodyBytes = null;
                        ZimbraLog.test.debug("RESPONSE (no content):\n%s\n%s\n\n", statusLine, hdrsSb);
                    } else {
                        responseBodyBytes = ByteUtil.getContent(responseStream, -1);
                        ZimbraLog.test.debug("RESPONSE:\n%s\n%s\n%s\n\n", statusLine, hdrsSb,
                                new String(responseBodyBytes));
                    }
                }
                assertEquals("Response code", expectedCode, respCode);
                assertEquals("Status code", expectedCode, statusCode);
            } catch (IOException e) {
                ZimbraLog.test.debug("Exception thrown", e);
                fail("Unexpected Exception" + e);
                throw e;
            } finally {
                method.releaseConnection();
            }
        }

        public static HttpMethodExecutor execute(HttpClient client, HttpMethod method, int expectedCode)
                throws IOException {
            return new HttpMethodExecutor(client, method, expectedCode);
        }

        public String getResponseAsString() {
            return new String(responseBodyBytes);
        }
    }

    @Test
    public void testBadBasicAuth() throws Exception {
        Account dav1 = users[1].create();
        String calFolderUrl = getFolderUrl(dav1, "Calendar").replaceAll("@", "%40");
        HttpClient client = new HttpClient();
        GetMethod method = new GetMethod(calFolderUrl);
        addBasicAuthHeaderForUser(method, dav1, "badPassword");
        HttpMethodExecutor.execute(client, method, HttpStatus.SC_UNAUTHORIZED);
    }

    @Test
    public void testPostToSchedulingOutbox() throws Exception {
        Account dav1 = users[1].create();
        Account dav2 = users[2].create();
        Account dav3 = users[3].create();
        String url = getSchedulingOutboxUrl(dav1, dav1);
        HttpClient client = new HttpClient();
        PostMethod method = new PostMethod(url);
        addBasicAuthHeaderForUser(method, dav1);
        method.addRequestHeader("Content-Type", "text/calendar");
        method.addRequestHeader("Originator", "mailto:" + dav1.getName());
        method.addRequestHeader("Recipient", "mailto:" + dav2.getName());
        method.addRequestHeader("Recipient", "mailto:" + dav3.getName());

        method.setRequestEntity(new ByteArrayRequestEntity(exampleCancelIcal(dav1, dav2, dav3).getBytes(),
                MimeConstants.CT_TEXT_CALENDAR));

        HttpMethodExecutor.execute(client, method, HttpStatus.SC_OK);
    }

    @Test
    public void testBadPostToSchedulingOutbox() throws Exception {
        Account dav1 = users[1].create();
        Account dav2 = users[2].create();
        Account dav3 = users[3].create();
        String url = getSchedulingOutboxUrl(dav2, dav2);
        HttpClient client = new HttpClient();
        PostMethod method = new PostMethod(url);
        addBasicAuthHeaderForUser(method, dav2);
        method.addRequestHeader("Content-Type", "text/calendar");
        method.addRequestHeader("Originator", "mailto:" + dav2.getName());
        method.addRequestHeader("Recipient", "mailto:" + dav3.getName());

        method.setRequestEntity(new ByteArrayRequestEntity(exampleCancelIcal(dav1, dav2, dav3).getBytes(),
                MimeConstants.CT_TEXT_CALENDAR));

        HttpMethodExecutor.execute(client, method, HttpStatus.SC_BAD_REQUEST);
    }

    public static void addBasicAuthHeaderForUser(HttpMethod method, Account acct, String password)
            throws UnsupportedEncodingException {
        String basicAuthEncoding = DatatypeConverter
                .printBase64Binary(String.format("%s:%s", acct.getName(), password).getBytes("UTF-8"));
        method.addRequestHeader("Authorization", "Basic " + basicAuthEncoding);
    }

    public static void addBasicAuthHeaderForUser(HttpMethod method, Account acct)
            throws UnsupportedEncodingException {
        addBasicAuthHeaderForUser(method, acct, "test123");
    }

    public static StringBuilder getLocalServerRoot() throws ServiceException {
        StringBuilder sb = new StringBuilder();
        sb.append(TestUtil.getBaseUrl(localServer));
        return sb;
    }

    public static String getSchedulingOutboxUrl(Account auth, Account target) throws ServiceException {
        StringBuilder sb = getLocalServerRoot();
        sb.append(UrlNamespace.getSchedulingOutboxUrl(auth.getName(), target.getName()));
        return sb.toString();
    }

    public static String getSchedulingInboxUrl(Account auth, Account target) throws ServiceException {
        StringBuilder sb = getLocalServerRoot();
        sb.append(UrlNamespace.getSchedulingInboxUrl(auth.getName(), target.getName()));
        return sb.toString();
    }

    public static String getFolderUrl(Account auth, String folderName) throws ServiceException {
        StringBuilder sb = getLocalServerRoot();
        sb.append(UrlNamespace.getFolderUrl(auth.getName(), folderName));
        return sb.toString();
    }

    public static String getPrincipalUrl(Account auth) throws ServiceException {
        StringBuilder sb = getLocalServerRoot();
        sb.append(UrlNamespace.getPrincipalUrl(auth));
        return sb.toString();
    }

    public static String getCalendarProxyReadUrl(Account target) throws ServiceException {
        StringBuilder sb = getLocalServerRoot();
        sb.append(UrlNamespace.getCalendarProxyReadUrl(target, target));
        return sb.toString();
    }

    public static String getCalendarProxyWriteUrl(Account target) throws ServiceException {
        StringBuilder sb = getLocalServerRoot();
        sb.append(UrlNamespace.getCalendarProxyWriteUrl(target, target));
        return sb.toString();
    }

    static final String calendar_query_etags_by_vevent = "<calendar-query xmlns:D=\"DAV:\" xmlns=\"urn:ietf:params:xml:ns:caldav\">\n"
            + "  <D:prop>\n" + "    <D:getetag/>\n" + "  </D:prop>\n" + "  <filter>\n"
            + "    <comp-filter name=\"VCALENDAR\">\n" + "      <comp-filter name=\"VEVENT\"/>\n"
            + "    </comp-filter>\n" + "  </filter>\n" + "</calendar-query>";

    public static Document calendarQuery(String url, Account acct) throws IOException, XmlParseException {
        ReportMethod method = new ReportMethod(url);
        addBasicAuthHeaderForUser(method, acct);
        HttpClient client = new HttpClient();
        TestCalDav.HttpMethodExecutor executor;
        method.addRequestHeader("Content-Type", MimeConstants.CT_TEXT_XML);
        method.setRequestEntity(
                new ByteArrayRequestEntity(calendar_query_etags_by_vevent.getBytes(), MimeConstants.CT_TEXT_XML));
        executor = new TestCalDav.HttpMethodExecutor(client, method, HttpStatus.SC_MULTI_STATUS);
        String respBody = new String(executor.responseBodyBytes, MimeConstants.P_CHARSET_UTF8);
        Document doc = W3cDomUtil.parseXMLToDoc(respBody);
        org.w3c.dom.Element docElement = doc.getDocumentElement();
        assertEquals("Report node name", DavElements.P_MULTISTATUS, docElement.getLocalName());
        return doc;
    }

    /**
     * @param acct
     * @param UID - null or empty if don't care
     * @param expected - false if don't expect a matching item to be in collection within timeout time
     * @return href of first matching item found
     * @throws ServiceException
     * @throws IOException
     */
    public static String waitForItemInCalendarCollectionByUID(String url, Account acct, String UID,
            boolean expected, int timeout_millis) throws ServiceException, IOException {
        int orig_timeout_millis = timeout_millis;
        while (timeout_millis > 0) {
            Document doc = calendarQuery(url, acct);
            XPath xpath = XPathFactory.newInstance().newXPath();
            xpath.setNamespaceContext(TestCalDav.NamespaceContextForXPath.forCalDAV());
            XPathExpression xPathExpr;
            try {
                xPathExpr = xpath.compile("/D:multistatus/D:response/D:href/text()");
                NodeList result = (NodeList) xPathExpr.evaluate(doc, XPathConstants.NODESET);
                if (1 <= result.getLength()) {
                    for (int ndx = 0; ndx < result.getLength(); ndx++) {
                        Node item = result.item(ndx);
                        String nodeValue = item.getNodeValue();
                        if ((Strings.isNullOrEmpty(UID)) || (nodeValue.contains(UID))) {
                            if (!expected) {
                                fail(String.format(
                                        "item with UID '%s' unexpectedly arrived in collection '%s' within %d millisecs",
                                        Strings.nullToEmpty(UID), url, orig_timeout_millis - timeout_millis));

                            }
                            return nodeValue;
                        }
                    }
                }
            } catch (XPathExpressionException e1) {
                ZimbraLog.test.debug("xpath problem", e1);
            }
            try {
                if (timeout_millis > TestUtil.DEFAULT_WAIT) {
                    Thread.sleep(TestUtil.DEFAULT_WAIT);
                    timeout_millis = timeout_millis - TestUtil.DEFAULT_WAIT;
                } else {
                    Thread.sleep(timeout_millis);
                    timeout_millis = 0;

                }
            } catch (InterruptedException e) {
                ZimbraLog.test.debug("sleep got interrupted", e);
            }
        }
        if (expected) {
            fail(String.format("item with UID '%s' didn't arrive in collection '%s' within %d millisecs",
                    Strings.nullToEmpty(UID), url, orig_timeout_millis));
        }
        return null;

    }

    /**
     * Note: as currently we don't show replies in the scheduling inbox, this ONLY works for requests
     *
     * @param acct
     * @param UID - null or empty if don't care
     * @return href of first matching item found
     * @throws ServiceException
     * @throws IOException
     */
    public static String waitForNewSchedulingRequestByUID(Account acct, String UID)
            throws ServiceException, IOException {
        String url = getSchedulingInboxUrl(acct, acct);
        return waitForItemInCalendarCollectionByUID(url, acct, UID, true, 10000);
    }

    @Test
    public void testCalendarQueryOnInbox() throws Exception {
        Account dav1 = users[1].create();
        String url = getSchedulingInboxUrl(dav1, dav1);
        Document doc = calendarQuery(url, dav1);
        org.w3c.dom.Element rootElem = doc.getDocumentElement();
        assertFalse("response when there are no items should have no child elements", rootElem.hasChildNodes());

        // Send an invite from user1 and check tags.
        ZMailbox organizer = users[0].getZMailbox();
        String subject = NAME_PREFIX + " testInvite request 1";
        Date startDate = new Date(System.currentTimeMillis() + Constants.MILLIS_PER_DAY);
        Date endDate = new Date(startDate.getTime() + Constants.MILLIS_PER_HOUR);
        TestUtil.createAppointment(organizer, subject, dav1.getName(), startDate, endDate);

        waitForNewSchedulingRequestByUID(dav1, "");
        doc = calendarQuery(url, dav1);
        rootElem = doc.getDocumentElement();
        assertTrue("response should have child elements", rootElem.hasChildNodes());
    }

    @Test
    public void testCalendarQueryOnOutbox() throws Exception {
        Account dav1 = users[1].create();
        ZMailbox dav1mbox = users[0].getZMailbox();
        String url = getSchedulingOutboxUrl(dav1, dav1);
        Document doc = calendarQuery(url, dav1);
        org.w3c.dom.Element rootElem = doc.getDocumentElement();
        assertFalse("response when there are no items should have no child elements", rootElem.hasChildNodes());

        // Send an invite to user2 and check tags.
        ZMailbox recipient = users[0].getZMailbox();
        String subject = NAME_PREFIX + " testInvite request 1";
        Date startDate = new Date(System.currentTimeMillis() + Constants.MILLIS_PER_DAY);
        Date endDate = new Date(startDate.getTime() + Constants.MILLIS_PER_HOUR);
        TestUtil.createAppointment(dav1mbox, subject, recipient.getName(), startDate, endDate);

        doc = calendarQuery(url, dav1);
        rootElem = doc.getDocumentElement();
        // We didn't support this report when we only offered caldav-schedule and caldav-auto-schedule
        // doesn't require calendar-query support on the outbox - so we just treat it as always empty.
        assertFalse("response for items in outbox should have no child elements, even though we sent an invite",
                rootElem.hasChildNodes());
    }

    @Test
    public void testPropFindSupportedReportSetOnInbox() throws Exception {
        Account user1 = users[0].create();
        checkPropFindSupportedReportSet(user1, getSchedulingInboxUrl(user1, user1),
                UrlNamespace.getSchedulingInboxUrl(user1.getName(), user1.getName()));
    }

    @Test
    public void testPropFindSupportedReportSetOnOutbox() throws Exception {
        Account user1 = users[0].create();
        checkPropFindSupportedReportSet(user1, getSchedulingOutboxUrl(user1, user1),
                UrlNamespace.getSchedulingOutboxUrl(user1.getName(), user1.getName()));
    }

    String propFindSupportedReportSet = "<x0:propfind xmlns:x0=\"DAV:\" xmlns:x1=\"urn:ietf:params:xml:ns:caldav\">\n"
            + "  <x0:prop>\n" + "    <x0:supported-report-set/>\n" + "  </x0:prop>\n" + "</x0:propfind>";

    public void checkPropFindSupportedReportSet(Account user, String fullurl, String shorturl) throws Exception {
        PropFindMethod method = new PropFindMethod(fullurl);
        addBasicAuthHeaderForUser(method, user);
        HttpClient client = new HttpClient();
        TestCalDav.HttpMethodExecutor executor;
        String respBody;
        Element respElem;
        method.addRequestHeader("Content-Type", MimeConstants.CT_TEXT_XML);
        method.setRequestEntity(
                new ByteArrayRequestEntity(propFindSupportedReportSet.getBytes(), MimeConstants.CT_TEXT_XML));
        executor = new TestCalDav.HttpMethodExecutor(client, method, HttpStatus.SC_MULTI_STATUS);
        respBody = new String(executor.responseBodyBytes, MimeConstants.P_CHARSET_UTF8);
        respElem = Element.XMLElement.parseXML(respBody);
        assertEquals("name of top element in response", DavElements.P_MULTISTATUS, respElem.getName());
        assertTrue("top element response should have child elements", respElem.hasChildren());
        checkSupportedReportSet(respElem, shorturl);
    }

    private void checkSupportedReportSet(Element respElem, String shorturl) {
        boolean supportsCalendarQuery = false;
        for (Element r : respElem.listElements()) {
            assertEquals("name of sub-element in response", DavElements.P_RESPONSE, r.getName());
            for (Element respEntry : r.listElements()) {
                if (DavElements.P_HREF.equals(respEntry.getName())) {
                    String hrefText = respEntry.getText();
                    // assertTrue(hrefText + " should end with /Inbox/", hrefText.endsWith("/Inbox/"));
                    // assertTrue(hrefText + " should start with /dav/", hrefText.startsWith("/dav/"));
                    assertEquals("HREF", shorturl.replaceAll("@", "%40"), hrefText);
                } else if (DavElements.P_PROPSTAT.equals(respEntry.getName())) {
                    for (Element psEntry : respEntry.listElements()) {
                        if (DavElements.P_STATUS.equals(psEntry.getName())) {
                            assertEquals("propstat/status", "HTTP/1.1 200 OK", psEntry.getText());
                        } else if (DavElements.P_PROP.equals(psEntry.getName())) {
                            for (Element propEntry : psEntry.listElements()) {
                                if (DavElements.P_SUPPORTED_REPORT_SET.equals(propEntry.getName())) {
                                    for (Element suppRepSetEntry : propEntry.listElements()) {
                                        assertEquals("supported-report-set child", DavElements.P_SUPPORTED_REPORT,
                                                suppRepSetEntry.getName());
                                        for (Element suppRepEntry : suppRepSetEntry.listElements()) {
                                            assertEquals("supported-report child", DavElements.P_REPORT,
                                                    suppRepEntry.getName());
                                            for (Element repEntry : suppRepEntry.listElements()) {
                                                if (DavElements.E_CALENDAR_QUERY.equals(repEntry.getQName())) {
                                                    supportsCalendarQuery = true;
                                                }
                                            }
                                        }
                                    }
                                } else {
                                    fail("Unexpected element '" + propEntry.getName() + "' under "
                                            + DavElements.P_PROP);
                                }
                            }
                        } else {
                            fail("Unexpected element '" + psEntry.getName() + "' under " + DavElements.P_PROPSTAT);
                        }
                    }
                } else {
                    fail("Unexpected element '" + respEntry.getName() + "' under " + DavElements.P_RESPONSE);
                }
            }
        }
        assertTrue("calendar-query report should be advertised", supportsCalendarQuery);
    }

    final String propFindSupportedCalendarComponentSet = "<D:propfind xmlns:D=\"DAV:\" xmlns:C=\"urn:ietf:params:xml:ns:caldav\">\n"
            + "  <D:prop>\n" + "     <C:supported-calendar-component-set/>\n" + "  </D:prop>\n" + "</D:propfind>";

    public void checkPropFindSupportedCalendarComponentSet(Account user, String fullurl, String shorturl,
            String[] compNames) throws Exception {
        PropFindMethod method = new PropFindMethod(fullurl);
        addBasicAuthHeaderForUser(method, user);
        HttpClient client = new HttpClient();
        TestCalDav.HttpMethodExecutor executor;
        String respBody;
        method.addRequestHeader("Content-Type", MimeConstants.CT_TEXT_XML);
        method.setRequestEntity(new ByteArrayRequestEntity(propFindSupportedCalendarComponentSet.getBytes(),
                MimeConstants.CT_TEXT_XML));
        executor = new TestCalDav.HttpMethodExecutor(client, method, HttpStatus.SC_MULTI_STATUS);
        respBody = new String(executor.responseBodyBytes, MimeConstants.P_CHARSET_UTF8);
        Document doc = W3cDomUtil.parseXMLToDoc(respBody);
        XPath xpath = XPathFactory.newInstance().newXPath();
        xpath.setNamespaceContext(TestCalDav.NamespaceContextForXPath.forCalDAV());
        XPathExpression xPathExpr;
        String text;
        NodeList result;
        xPathExpr = xpath.compile("/D:multistatus/D:response/D:href/text()");
        result = (NodeList) xPathExpr.evaluate(doc, XPathConstants.NODESET);
        text = (String) xPathExpr.evaluate(doc, XPathConstants.STRING);
        assertEquals("HREF", shorturl.replaceAll("@", "%40"), text);
        xPathExpr = xpath.compile("/D:multistatus/D:response/D:propstat/D:status/text()");
        text = (String) xPathExpr.evaluate(doc, XPathConstants.STRING);
        assertEquals("status", "HTTP/1.1 200 OK", text);
        xPathExpr = xpath
                .compile("/D:multistatus/D:response/D:propstat/D:prop/C:supported-calendar-component-set/C:comp");
        result = (NodeList) xPathExpr.evaluate(doc, XPathConstants.NODESET);
        assertEquals("Number of comp nodes under supported-calendar-component-set", compNames.length,
                result.getLength());
        List<String> names = Arrays.asList(compNames);
        for (int ndx = 0; ndx < result.getLength(); ndx++) {
            org.w3c.dom.Element child = (org.w3c.dom.Element) result.item(ndx);
            String name = child.getAttribute("name");
            assertNotNull("comp should have a 'name' attribute", name);
            assertTrue(String.format("comp 'name' attribute '%s' should be one of the expected names", name),
                    names.contains(name));
        }
    }

    private final String[] componentsForBothTasksAndEvents = { "VEVENT", "VTODO", "VFREEBUSY" };
    private final String[] eventComponents = { "VEVENT", "VFREEBUSY" };
    private final String[] todoComponents = { "VTODO", "VFREEBUSY" };

    @Test
    public void testPropFindSupportedCalendarComponentSetOnInbox() throws Exception {
        Account user1 = users[0].create();
        checkPropFindSupportedCalendarComponentSet(user1, getSchedulingInboxUrl(user1, user1),
                UrlNamespace.getSchedulingInboxUrl(user1.getName(), user1.getName()),
                componentsForBothTasksAndEvents);
    }

    @Test
    public void testPropFindSupportedCalendarComponentSetOnOutbox() throws Exception {
        Account user1 = users[0].create();
        checkPropFindSupportedCalendarComponentSet(user1, getSchedulingOutboxUrl(user1, user1),
                UrlNamespace.getSchedulingOutboxUrl(user1.getName(), user1.getName()),
                componentsForBothTasksAndEvents);
    }

    @Test
    public void testPropFindSupportedCalendarComponentSetOnCalendar() throws Exception {
        Account user1 = users[0].create();
        checkPropFindSupportedCalendarComponentSet(user1, getFolderUrl(user1, "Calendar"),
                UrlNamespace.getFolderUrl(user1.getName(), "Calendar"), eventComponents);
    }

    @Test
    public void testPropFindSupportedCalendarComponentSetOnTasks() throws Exception {
        Account user1 = users[0].create();
        checkPropFindSupportedCalendarComponentSet(user1, getFolderUrl(user1, "Tasks"),
                UrlNamespace.getFolderUrl(user1.getName(), "Tasks"), todoComponents);
    }

    /**
     *  dav - sending http error 302 because: wrong url - redirecting to:
     *  http://pan.local:7070/dav/dav1@pan.local/Calendar/d123f102-42a7-4283-b025-3376dabe53b3.ics
     *  com.zimbra.cs.dav.DavException: wrong url - redirecting to:
     *  http://pan.local:7070/dav/dav1@pan.local/Calendar/d123f102-42a7-4283-b025-3376dabe53b3.ics
     *      at com.zimbra.cs.dav.resource.CalendarCollection.createItem(CalendarCollection.java:431)
     *      at com.zimbra.cs.dav.service.method.Put.handle(Put.java:49)
     *      at com.zimbra.cs.dav.service.DavServlet.service(DavServlet.java:322)
     */
    @Test
    public void testCreateUsingClientChosenName() throws ServiceException, IOException {
        Account dav1 = users[1].create();
        String davBaseName = "clientInvented.now";
        String calFolderUrl = getFolderUrl(dav1, "Calendar");
        String url = String.format("%s%s", calFolderUrl, davBaseName);
        HttpClient client = new HttpClient();
        PutMethod putMethod = new PutMethod(url);
        addBasicAuthHeaderForUser(putMethod, dav1);
        putMethod.addRequestHeader("Content-Type", "text/calendar");

        putMethod.setRequestEntity(new ByteArrayRequestEntity(simpleEvent(dav1), MimeConstants.CT_TEXT_CALENDAR));
        if (DebugConfig.enableDAVclientCanChooseResourceBaseName) {
            HttpMethodExecutor.execute(client, putMethod, HttpStatus.SC_CREATED);
        } else {
            HttpMethodExecutor.execute(client, putMethod, HttpStatus.SC_MOVED_TEMPORARILY);
            // Not testing much in this mode but...
            return;
        }

        doGetMethod(url, dav1, HttpStatus.SC_OK);

        PropFindMethod propFindMethod = new PropFindMethod(getFolderUrl(dav1, "Calendar"));
        addBasicAuthHeaderForUser(propFindMethod, dav1);
        TestCalDav.HttpMethodExecutor executor;
        String respBody;
        Element respElem;
        propFindMethod.addRequestHeader("Content-Type", MimeConstants.CT_TEXT_XML);
        propFindMethod.addRequestHeader("Depth", "1");
        propFindMethod.setRequestEntity(
                new ByteArrayRequestEntity(propFindEtagResType.getBytes(), MimeConstants.CT_TEXT_XML));
        executor = new TestCalDav.HttpMethodExecutor(client, propFindMethod, HttpStatus.SC_MULTI_STATUS);
        respBody = new String(executor.responseBodyBytes, MimeConstants.P_CHARSET_UTF8);
        respElem = Element.XMLElement.parseXML(respBody);
        assertEquals("name of top element in propfind response", DavElements.P_MULTISTATUS, respElem.getName());
        assertTrue("propfind response should have child elements", respElem.hasChildren());
        Iterator<Element> iter = respElem.elementIterator();
        boolean hasCalendarHref = false;
        boolean hasCalItemHref = false;
        while (iter.hasNext()) {
            Element child = iter.next();
            if (DavElements.P_RESPONSE.equals(child.getName())) {
                Iterator<Element> hrefIter = child.elementIterator(DavElements.P_HREF);
                while (hrefIter.hasNext()) {
                    Element href = hrefIter.next();
                    calFolderUrl.endsWith(href.getText());
                    hasCalendarHref = hasCalendarHref || calFolderUrl.endsWith(href.getText());
                    hasCalItemHref = hasCalItemHref || url.endsWith(href.getText());
                }
            }
        }
        assertTrue("propfind response contained entry for calendar", hasCalendarHref);
        assertTrue("propfind response contained entry for calendar entry ", hasCalItemHref);
        doDeleteMethod(url, dav1, HttpStatus.SC_NO_CONTENT);
    }

    public HttpMethodExecutor doIcalPut(String url, Account authAcct, byte[] vcalendar, int expected)
            throws IOException {
        HttpClient client = new HttpClient();
        PutMethod putMethod = new PutMethod(url);
        addBasicAuthHeaderForUser(putMethod, authAcct);
        putMethod.addRequestHeader("Content-Type", "text/calendar");

        putMethod.setRequestEntity(new ByteArrayRequestEntity(vcalendar, MimeConstants.CT_TEXT_CALENDAR));
        return HttpMethodExecutor.execute(client, putMethod, expected);
    }

    public HttpMethodExecutor doGetMethod(String url, Account authAcct, int expected) throws IOException {
        HttpClient client = new HttpClient();
        GetMethod getMethod = new GetMethod(url);
        addBasicAuthHeaderForUser(getMethod, authAcct);
        return HttpMethodExecutor.execute(client, getMethod, expected);
    }

    public HttpMethodExecutor doDeleteMethod(String url, Account authAcct, int expected) throws IOException {
        HttpClient client = new HttpClient();
        DeleteMethod deleteMethod = new DeleteMethod(url);
        addBasicAuthHeaderForUser(deleteMethod, authAcct);
        return HttpMethodExecutor.execute(client, deleteMethod, expected);
    }

    /** Mostly checking that if attendees cease to exist (even via DLs) then modification and cancel iTip
     * messages still work to the remaining attendees.
     */
    @Test
    public void testCreateModifyDeleteAttendeeModifyAndCancel() throws ServiceException, IOException {
        Account dav1 = users[1].create();
        Account dav2 = users[2].create();
        Account dav3 = users[3].create();
        Account dav4 = users[4].create();
        DistributionList dl = TestUtil.createDistributionList(DL1);
        String[] members = { dav4.getName() };
        prov.addMembers(dl, members);
        List<MailTarget> attendees = Lists.newArrayList();
        attendees.add(dav1);
        attendees.add(dav2);
        attendees.add(dav3);
        attendees.add(dl);
        ZVCalendar vCal = simpleMeeting(dav1, attendees, "1", 8);
        ZProperty uidProp = vCal.getComponent(ICalTok.VEVENT).getProperty(ICalTok.UID);
        String uid = uidProp.getValue();
        String davBaseName = uid + ".ics";
        String url = String.format("%s%s", getFolderUrl(dav1, "Calendar"), davBaseName);
        doIcalPut(url, dav1, zvcalendarToBytes(vCal), HttpStatus.SC_CREATED);
        String inboxhref = TestCalDav.waitForNewSchedulingRequestByUID(dav2, uid);
        assertTrue("Found meeting request for newly created item", inboxhref.contains(uid));
        doDeleteMethod(getLocalServerRoot().append(inboxhref).toString(), dav2, HttpStatus.SC_NO_CONTENT);

        // attendee via DL
        inboxhref = TestCalDav.waitForNewSchedulingRequestByUID(dav4, uid);
        assertTrue("Found meeting request for newly created item", inboxhref.contains(uid));
        doDeleteMethod(getLocalServerRoot().append(inboxhref).toString(), dav4, HttpStatus.SC_NO_CONTENT);

        vCal = simpleMeeting(dav1, attendees, uid, "2", 9);
        doIcalPut(url, dav1, zvcalendarToBytes(vCal), HttpStatus.SC_CREATED);
        inboxhref = TestCalDav.waitForNewSchedulingRequestByUID(dav2, uid);
        assertTrue("Found meeting request for newly created item", inboxhref.contains(uid));
        doDeleteMethod(getLocalServerRoot().append(inboxhref).toString(), dav2, HttpStatus.SC_NO_CONTENT);

        // attendee via DL
        inboxhref = TestCalDav.waitForNewSchedulingRequestByUID(dav4, uid);
        assertTrue("Found meeting request for newly created item", inboxhref.contains(uid));
        doDeleteMethod(getLocalServerRoot().append(inboxhref).toString(), dav4, HttpStatus.SC_NO_CONTENT);

        // Test that iTip handling still happens when some of the attendees no longer exist.
        users[3].cleanup();
        users[4].cleanup(); // attendee via DL
        vCal = simpleMeeting(dav1, attendees, uid, "3", 10);
        doIcalPut(url, dav1, zvcalendarToBytes(vCal), HttpStatus.SC_CREATED);
        inboxhref = TestCalDav.waitForNewSchedulingRequestByUID(dav2, uid);
        assertTrue("Found meeting request for newly created item", inboxhref.contains(uid));
        doDeleteMethod(getLocalServerRoot().append(inboxhref).toString(), dav2, HttpStatus.SC_NO_CONTENT);
        String dav2Url = String.format("%s%s", getFolderUrl(dav2, "Calendar"), davBaseName);
        doGetMethod(dav2Url, dav2, HttpStatus.SC_OK);

        // Cancel meeting by deleting it
        doDeleteMethod(url, dav1, HttpStatus.SC_NO_CONTENT);

        inboxhref = TestCalDav.waitForNewSchedulingRequestByUID(dav2, uid);
        assertTrue("Found meeting request for newly created item", inboxhref.contains(uid));
        doDeleteMethod(getLocalServerRoot().append(inboxhref).toString(), dav2, HttpStatus.SC_NO_CONTENT);
        // The associated calendar item should have been deleted as a result of the Cancel
        doGetMethod(dav2Url, dav2, HttpStatus.SC_NOT_FOUND);
    }

    private static String androidSeriesMeetingTemplate = "BEGIN:VCALENDAR\n" + "VERSION:2.0\n"
            + "PRODID:-//dmfs.org//mimedir.icalendar//EN\n" + "BEGIN:VTIMEZONE\n" + "TZID:Europe/London\n"
            + "X-LIC-LOCATION:Europe/London\n" + "BEGIN:DAYLIGHT\n" + "TZOFFSETFROM:+0000\n" + "TZOFFSETTO:+0100\n"
            + "TZNAME:BST\n" + "DTSTART:19700329T010000\n" + "RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU\n"
            + "END:DAYLIGHT\n" + "BEGIN:STANDARD\n" + "TZOFFSETFROM:+0100\n" + "TZOFFSETTO:+0000\n" + "TZNAME:GMT\n"
            + "DTSTART:19701025T020000\n" + "RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU\n" + "END:STANDARD\n"
            + "END:VTIMEZONE\n" + "BEGIN:VEVENT\n" + "DTSTART;TZID=Europe/London:20141022T190000\n"
            + "DESCRIPTION:Giggle\n" + "SUMMARY:testAndroidMeetingSeries\n" + "RRULE:FREQ=DAILY;COUNT=15;WKST=MO\n"
            + "LOCATION:Egham Leisure Centre\\, Vicarage Road\\, Egham\\, United Kingdom\n" + "TRANSP:OPAQUE\n"
            + "STATUS:CONFIRMED\n" + "ATTENDEE;PARTSTAT=ACCEPTED;RSVP=TRUE;ROLE=REQ-PARTICIPANT:mailto:%%ORG%%\n"
            + "ATTENDEE;PARTSTAT=NEEDS-ACTION;RSVP=TRUE;ROLE=REQ-PARTICIPANT:mailto:%%ATT%%\n" + "DURATION:PT1H\n"
            + "LAST-MODIFIED:20141021T145905Z\n" + "DTSTAMP:20141021T145905Z\n" + "ORGANIZER:mailto:%%ORG%%\n"
            + "CREATED:20141021T145905Z\n" + "UID:%%UID%%\n" + "BEGIN:VALARM\n" + "TRIGGER;VALUE=DURATION:-PT15M\n"
            + "ACTION:DISPLAY\n" + "DESCRIPTION:Default Event Notification\n"
            + "X-WR-ALARMUID:790cd474-6135-4705-b1a0-24d4b4fc3cf5\n" + "END:VALARM\n" + "END:VEVENT\n"
            + "END:VCALENDAR\n";
    public String androidSeriesMeetingUid = "6db50587-d283-49a1-9cf4-63aa27406829";

    @Test
    public void testAndroidMeetingSeries() throws Exception {
        Account dav1 = users[1].create();
        Account dav2 = users[2].create();
        users[2].getZMailbox(); // Force creation of mailbox - shouldn't be needed
        String calFolderUrl = getFolderUrl(dav1, "Calendar").replaceAll("@", "%40");
        String url = String.format("%s%s.ics", calFolderUrl, androidSeriesMeetingUid);
        HttpClient client = new HttpClient();
        PutMethod putMethod = new PutMethod(url);
        addBasicAuthHeaderForUser(putMethod, dav1);
        putMethod.addRequestHeader("Content-Type", "text/calendar");

        String body = androidSeriesMeetingTemplate.replace("%%ORG%%", dav1.getName())
                .replace("%%ATT%%", dav2.getName()).replace("%%UID%%", androidSeriesMeetingUid);
        putMethod.setRequestEntity(new ByteArrayRequestEntity(body.getBytes(), MimeConstants.CT_TEXT_CALENDAR));
        HttpMethodExecutor.execute(client, putMethod, HttpStatus.SC_CREATED);

        String inboxhref = TestCalDav.waitForNewSchedulingRequestByUID(dav2, androidSeriesMeetingUid);
        assertTrue("Found meeting request for newly created item", inboxhref.contains(androidSeriesMeetingUid));

        GetMethod getMethod = new GetMethod(url);
        addBasicAuthHeaderForUser(getMethod, dav1);
        HttpMethodExecutor exe = HttpMethodExecutor.execute(client, getMethod, HttpStatus.SC_OK);
        String etag = null;
        for (Header hdr : exe.respHeaders) {
            if (DavProtocol.HEADER_ETAG.equals(hdr.getName())) {
                etag = hdr.getValue();
            }
        }
        assertNotNull("ETag from get", etag);

        // Check that we fail if the etag is wrong
        putMethod = new PutMethod(url);
        addBasicAuthHeaderForUser(putMethod, dav1);
        putMethod.addRequestHeader("Content-Type", "text/calendar");
        putMethod.addRequestHeader(DavProtocol.HEADER_IF_MATCH, "willNotMatch");
        putMethod.setRequestEntity(new ByteArrayRequestEntity(body.getBytes(), MimeConstants.CT_TEXT_CALENDAR));
        HttpMethodExecutor.execute(client, putMethod, HttpStatus.SC_PRECONDITION_FAILED);

        PropFindMethod propFindMethod = new PropFindMethod(getFolderUrl(dav1, "Calendar"));
        addBasicAuthHeaderForUser(propFindMethod, dav1);
        TestCalDav.HttpMethodExecutor executor;
        String respBody;
        Element respElem;
        propFindMethod.addRequestHeader("Content-Type", MimeConstants.CT_TEXT_XML);
        propFindMethod.addRequestHeader("Depth", "1");
        propFindMethod.setRequestEntity(
                new ByteArrayRequestEntity(propFindEtagResType.getBytes(), MimeConstants.CT_TEXT_XML));
        executor = new TestCalDav.HttpMethodExecutor(client, propFindMethod, HttpStatus.SC_MULTI_STATUS);
        respBody = new String(executor.responseBodyBytes, MimeConstants.P_CHARSET_UTF8);
        respElem = Element.XMLElement.parseXML(respBody);
        assertEquals("name of top element in propfind response", DavElements.P_MULTISTATUS, respElem.getName());
        assertTrue("propfind response should have child elements", respElem.hasChildren());
        Iterator<Element> iter = respElem.elementIterator();
        boolean hasCalendarHref = false;
        boolean hasCalItemHref = false;
        while (iter.hasNext()) {
            Element child = iter.next();
            if (DavElements.P_RESPONSE.equals(child.getName())) {
                Iterator<Element> hrefIter = child.elementIterator(DavElements.P_HREF);
                while (hrefIter.hasNext()) {
                    Element href = hrefIter.next();
                    calFolderUrl.endsWith(href.getText());
                    hasCalendarHref = hasCalendarHref || calFolderUrl.endsWith(href.getText());
                    hasCalItemHref = hasCalItemHref || url.endsWith(href.getText());
                }
            }
        }
        assertTrue("propfind response contained entry for calendar", hasCalendarHref);
        assertTrue("propfind response contained entry for calendar entry ", hasCalItemHref);

        DeleteMethod deleteMethod = new DeleteMethod(url);
        addBasicAuthHeaderForUser(deleteMethod, dav1);
        HttpMethodExecutor.execute(client, deleteMethod, HttpStatus.SC_NO_CONTENT);
    }

    static String propFindEtagResType = "<x0:propfind xmlns:x0=\"DAV:\">" + " <x0:prop>" + "  <x0:getetag/>"
            + "  <x0:resourcetype/>" + " </x0:prop>" + "</x0:propfind>";

    public String zvcalendarToString(ZVCalendar vcal) throws IOException {
        StringWriter calWriter = new StringWriter();
        vcal.toICalendar(calWriter);
        String icalString = calWriter.toString();
        Closeables.closeQuietly(calWriter);
        return icalString;
    }

    public byte[] zvcalendarToBytes(ZVCalendar vcal) throws IOException {
        return zvcalendarToString(vcal).getBytes();
    }

    public byte[] simpleEvent(Account organizer) throws IOException {
        ZVCalendar vcal = new ZVCalendar();
        vcal.addVersionAndProdId();

        vcal.addProperty(new ZProperty(ICalTok.METHOD, ICalTok.PUBLISH.toString()));
        ICalTimeZone tz = ICalTimeZone.lookupByTZID("Africa/Harare");
        vcal.addComponent(tz.newToVTimeZone());
        ZComponent vevent = new ZComponent(ICalTok.VEVENT);
        ParsedDateTime dtstart = parsedDateTime(2020, java.util.Calendar.APRIL, 1, 9, 0, tz);
        vevent.addProperty(dtstart.toProperty(ICalTok.DTSTART, false));
        ParsedDateTime dtend = parsedDateTime(2020, java.util.Calendar.APRIL, 1, 13, 0, tz);
        vevent.addProperty(dtend.toProperty(ICalTok.DTEND, false));
        vevent.addProperty(new ZProperty(ICalTok.DTSTAMP, "20140108T224700Z"));
        vevent.addProperty(new ZProperty(ICalTok.SUMMARY, "Simple Event"));
        vevent.addProperty(new ZProperty(ICalTok.UID, "d1102-42a7-4283-b025-3376dabe53b3"));
        vevent.addProperty(new ZProperty(ICalTok.STATUS, ICalTok.CONFIRMED.toString()));
        vevent.addProperty(new ZProperty(ICalTok.SEQUENCE, "1"));
        // vevent.addProperty(organizer(organizer));
        vcal.addComponent(vevent);
        return zvcalendarToBytes(vcal);
    }

    public ZVCalendar simpleMeeting(Account organizer, List<MailTarget> attendees, String seq, int startHour)
            throws IOException {
        return simpleMeeting(organizer, attendees, LdapUtil.generateUUID(), seq, startHour);
    }

    public ZVCalendar simpleMeeting(Account organizer, List<MailTarget> attendees, String uid, String seq,
            int startHour) throws IOException {
        ZVCalendar vcal = new ZVCalendar();
        vcal.addVersionAndProdId();
        vcal.addProperty(new ZProperty(ICalTok.METHOD, ICalTok.PUBLISH.toString()));
        ICalTimeZone tz = ICalTimeZone.lookupByTZID("Africa/Harare");
        vcal.addComponent(tz.newToVTimeZone());
        ZComponent vevent = new ZComponent(ICalTok.VEVENT);
        ParsedDateTime dtstart = parsedDateTime(2020, java.util.Calendar.APRIL, 1, startHour, 0, tz);
        vevent.addProperty(dtstart.toProperty(ICalTok.DTSTART, false));
        ParsedDateTime dtend = parsedDateTime(2020, java.util.Calendar.APRIL, 1, startHour + 4, 0, tz);
        vevent.addProperty(dtend.toProperty(ICalTok.DTEND, false));
        vevent.addProperty(new ZProperty(ICalTok.DTSTAMP, "20150108T224700Z"));
        vevent.addProperty(new ZProperty(ICalTok.SUMMARY, "Simple Meeting"));
        vevent.addProperty(new ZProperty(ICalTok.UID, uid));
        vevent.addProperty(new ZProperty(ICalTok.STATUS, ICalTok.CONFIRMED.toString()));
        vevent.addProperty(new ZProperty(ICalTok.SEQUENCE, seq));
        vevent.addProperty(organizer(organizer));
        for (MailTarget att : attendees) {
            if (att.getId().equals(organizer.getId())) {
                vevent.addProperty(attendee(att, ICalTok.REQ_PARTICIPANT, ICalTok.INDIVIDUAL, ICalTok.ACCEPTED));
            } else {
                vevent.addProperty(
                        attendee(att, ICalTok.REQ_PARTICIPANT, ICalTok.INDIVIDUAL, ICalTok.NEEDS_ACTION));
            }
        }
        vcal.addComponent(vevent);
        return vcal;
    }

    @Test
    public void testSimpleMkcol() throws Exception {
        Account dav1 = users[1].create();
        StringBuilder url = getLocalServerRoot();
        url.append(DavServlet.DAV_PATH).append("/").append(dav1.getName()).append("/simpleMkcol/");
        MkColMethod method = new MkColMethod(url.toString());
        addBasicAuthHeaderForUser(method, dav1);
        HttpClient client = new HttpClient();
        HttpMethodExecutor.execute(client, method, HttpStatus.SC_CREATED);
    }

    @Test
    public void testMkcol4addressBook() throws Exception {
        String xml = "<D:mkcol xmlns:D=\"DAV:\" xmlns:C=\"urn:ietf:params:xml:ns:carddav\">" + "     <D:set>"
                + "       <D:prop>" + "         <D:resourcetype>" + "           <D:collection/>"
                + "           <C:addressbook/>" + "         </D:resourcetype>"
                + "         <D:displayname>OtherContacts</D:displayname>"
                + "         <C:addressbook-description xml:lang=\"en\">Extra Contacts</C:addressbook-description>"
                + "       </D:prop>" + "     </D:set>" + "</D:mkcol>";
        Account dav1 = users[1].create();
        StringBuilder url = getLocalServerRoot();
        url.append(DavServlet.DAV_PATH).append("/").append(dav1.getName()).append("/OtherContacts/");
        MkColMethod method = new MkColMethod(url.toString());
        addBasicAuthHeaderForUser(method, dav1);
        HttpClient client = new HttpClient();
        method.addRequestHeader("Content-Type", MimeConstants.CT_TEXT_XML);
        method.setRequestEntity(new ByteArrayRequestEntity(xml.getBytes(), MimeConstants.CT_TEXT_XML));
        HttpMethodExecutor.execute(client, method, HttpStatus.SC_MULTI_STATUS);

        ZMailbox.Options options = new ZMailbox.Options();
        options.setAccount(dav1.getName());
        options.setAccountBy(AccountBy.name);
        options.setPassword(TestUtil.DEFAULT_PASSWORD);
        options.setUri(TestUtil.getSoapUrl());
        options.setNoSession(true);
        ZMailbox mbox = ZMailbox.getMailbox(options);
        ZFolder folder = mbox.getFolderByPath("/OtherContacts");
        assertEquals("OtherContacts", folder.getName());
        assertEquals("OtherContacts default view", View.contact, folder.getDefaultView());
    }

    public String makeFreeBusyRequestIcal(Account organizer, List<Account> attendees, Date start, Date end)
            throws IOException {
        ZVCalendar vcal = new ZVCalendar();
        vcal.addVersionAndProdId();

        vcal.addProperty(new ZProperty(ICalTok.METHOD, ICalTok.REQUEST.toString()));
        ZComponent vfreebusy = new ZComponent(ICalTok.VFREEBUSY);
        ParsedDateTime dtstart = ParsedDateTime.fromUTCTime(start.getTime());
        vfreebusy.addProperty(dtstart.toProperty(ICalTok.DTSTART, false));
        ParsedDateTime dtend = ParsedDateTime.fromUTCTime(end.getTime());
        vfreebusy.addProperty(dtend.toProperty(ICalTok.DTEND, false));
        vfreebusy.addProperty(new ZProperty(ICalTok.DTSTAMP, "20140108T224700Z"));
        vfreebusy.addProperty(new ZProperty(ICalTok.UID, "d123f102-42a7-4283-b025-3376dabe53b3"));
        vfreebusy.addProperty(organizer(organizer));
        for (Account attendee : attendees) {
            vfreebusy.addProperty(new ZProperty(ICalTok.ATTENDEE, "mailto:" + attendee.getName()));
        }
        vcal.addComponent(vfreebusy);
        StringWriter calWriter = new StringWriter();
        vcal.toICalendar(calWriter);
        String icalString = calWriter.toString();
        Closeables.closeQuietly(calWriter);
        return icalString;
    }

    public HttpMethodExecutor doFreeBusyCheck(Account organizer, List<Account> attendees, Date start, Date end)
            throws ServiceException, IOException {
        HttpClient client = new HttpClient();
        String outboxurl = getSchedulingOutboxUrl(organizer, organizer);
        PostMethod postMethod = new PostMethod(outboxurl);
        postMethod.addRequestHeader("Content-Type", "text/calendar");
        postMethod.addRequestHeader("Originator", "mailto:" + organizer.getName());
        for (Account attendee : attendees) {
            postMethod.addRequestHeader("Recipient", "mailto:" + attendee.getName());
        }

        addBasicAuthHeaderForUser(postMethod, organizer);
        String fbIcal = makeFreeBusyRequestIcal(organizer, attendees, start, end);
        postMethod.setRequestEntity(new ByteArrayRequestEntity(fbIcal.getBytes(), MimeConstants.CT_TEXT_CALENDAR));

        return HttpMethodExecutor.execute(client, postMethod, HttpStatus.SC_OK);
    }

    public HttpMethodExecutor doPropPatch(Account account, String url, String body) throws IOException {
        HttpClient client = new HttpClient();
        PropPatchMethod propPatchMethod = new PropPatchMethod(url);
        addBasicAuthHeaderForUser(propPatchMethod, account);
        propPatchMethod.addRequestHeader("Content-Type", MimeConstants.CT_TEXT_XML);
        propPatchMethod.setRequestEntity(new ByteArrayRequestEntity(body.getBytes(), MimeConstants.CT_TEXT_XML));
        return HttpMethodExecutor.execute(client, propPatchMethod, HttpStatus.SC_MULTI_STATUS);
    }

    /**
     * http://tools.ietf.org/html/draft-desruisseaux-caldav-sched-03#section-5.3.1
     * 5.3. Scheduling Inbox Properties
     * 5.3.1. CALDAV:calendar-free-busy-set Property
     * Purpose:  Identify the calendars that contribute to the free-busy information for the calendar user associated
     * with the scheduling Inbox.
     *
     * If the list is empty - NO calendars affect freebusy.
     * If the list is not empty, each listed calendar affects freebusy.
     * Bug 85275 - Apple Calendar specifies URLs with "@" encoded as %40 - causing us to drop all calendar from FB set
     */
    @Test
    public void testPropPatchCalendarFreeBusySetSettingUsingEscapedUrls() throws Exception {
        String disableFreeBusyXml = "<A:propertyupdate xmlns:A=\"DAV:\">" + "  <A:set>" + "    <A:prop>"
                + "      <B:calendar-free-busy-set xmlns:B=\"urn:ietf:params:xml:ns:caldav\"/>" + "    </A:prop>"
                + "  </A:set>" + "</A:propertyupdate>";
        String enableFreeBusyTemplateXml = "<A:propertyupdate xmlns:A=\"DAV:\">" + "  <A:set>" + "    <A:prop>"
                + "      <B:calendar-free-busy-set xmlns:B=\"urn:ietf:params:xml:ns:caldav\">"
                + "        <A:href>/dav/%s/Tasks/</A:href>" + "        <A:href>/dav/%s/Calendar/</A:href>"
                + "      </B:calendar-free-busy-set>" + "    </A:prop>" + "  </A:set>" + "</A:propertyupdate>";

        Account dav1 = users[1].create();

        // Create an event in Dav1's calendar
        ZMailbox organizer = users[1].getZMailbox();
        String subject = NAME_PREFIX + " testInvite request 1";
        Date startDate = new Date(System.currentTimeMillis() + Constants.MILLIS_PER_DAY);
        Date endDate = new Date(startDate.getTime() + Constants.MILLIS_PER_HOUR);
        Date fbStartDate = new Date(startDate.getTime() - (Constants.MILLIS_PER_DAY * 2));
        Date fbEndDate = new Date(endDate.getTime() + (Constants.MILLIS_PER_DAY * 3));
        String busyTentativeMarker = "FREEBUSY;FBTYPE=BUSY";

        // seed an appointment in dav1's calendar
        TestUtil.createAppointment(organizer, subject, dav1.getName(), startDate, endDate);

        String fbResponse;
        fbResponse = doFreeBusyCheck(dav1, Lists.newArrayList(dav1), fbStartDate, fbEndDate).getResponseAsString();
        assertTrue(
                String.format("First FB check Response [%s] should contain [%s]", fbResponse, busyTentativeMarker),
                fbResponse.contains(busyTentativeMarker));

        String inboxurl = getSchedulingInboxUrl(dav1, dav1);
        doPropPatch(dav1, inboxurl, disableFreeBusyXml);

        fbResponse = doFreeBusyCheck(dav1, Lists.newArrayList(dav1), fbStartDate, fbEndDate).getResponseAsString();
        assertFalse(String.format("2nd FB check after disabling - Response [%s] should NOT contain [%s]",
                fbResponse, busyTentativeMarker), fbResponse.contains(busyTentativeMarker));

        String enableWithRawAt = String.format(enableFreeBusyTemplateXml, dav1.getName(), dav1.getName());
        String encodedName = dav1.getName().replace("@", "%40");
        String enableWithEncodedAt = String.format(enableFreeBusyTemplateXml, encodedName, encodedName);

        doPropPatch(dav1, inboxurl, enableWithRawAt);

        fbResponse = doFreeBusyCheck(dav1, Lists.newArrayList(dav1), fbStartDate, fbEndDate).getResponseAsString();
        assertTrue(String.format("3rd FB check after enabling Response [%s] should contain [%s]", fbResponse,
                busyTentativeMarker), fbResponse.contains(busyTentativeMarker));

        doPropPatch(dav1, inboxurl, disableFreeBusyXml);

        fbResponse = doFreeBusyCheck(dav1, Lists.newArrayList(dav1), fbStartDate, fbEndDate).getResponseAsString();
        assertFalse(String.format("4th FB check after disabling - Response [%s] should NOT contain [%s]",
                fbResponse, busyTentativeMarker), fbResponse.contains(busyTentativeMarker));

        doPropPatch(dav1, inboxurl, enableWithEncodedAt);

        fbResponse = doFreeBusyCheck(dav1, Lists.newArrayList(dav1), fbStartDate, fbEndDate).getResponseAsString();
        assertTrue(String.format("4th FB check after enabling (encoded urls) Response [%s] should contain [%s]",
                fbResponse, busyTentativeMarker), fbResponse.contains(busyTentativeMarker));
    }

    private static String VtimeZoneGMT_0600_0500 = "BEGIN:VCALENDAR\n" + "BEGIN:VTIMEZONE\n"
            + "TZID:GMT-06.00/-05.00\n" + "BEGIN:STANDARD\n" + "DTSTART:16010101T010000\n" + "TZOFFSETTO:-0600\n"
            + "TZOFFSETFROM:-0500\n" + "RRULE:FREQ=YEARLY;INTERVAL=1;BYMONTH=11;BYDAY=1SU;WKST=MO\n"
            + "END:STANDARD\n" + "BEGIN:DAYLIGHT\n" + "DTSTART:16010101T030000\n" + "TZOFFSETTO:-0500\n"
            + "TZOFFSETFROM:-0600\n" + "RRULE:FREQ=YEARLY;INTERVAL=1;BYMONTH=3;BYDAY=2SU;WKST=MO\n"
            + "END:DAYLIGHT\n" + "END:VTIMEZONE\n" + "END:VCALENDAR\n";

    @Test
    public void testFuzzyTimeZoneMatchGMT_06() throws Exception {
        try (ByteArrayInputStream bais = new ByteArrayInputStream(VtimeZoneGMT_0600_0500.getBytes())) {
            ZVCalendar tzcal = ZCalendar.ZCalendarBuilder.build(bais, MimeConstants.P_CHARSET_UTF8);
            assertNotNull("tzcal", tzcal);
            ZComponent tzcomp = tzcal.getComponent(ICalTok.VTIMEZONE);
            assertNotNull("tzcomp", tzcomp);
            ICalTimeZone tz = ICalTimeZone.fromVTimeZone(tzcomp, false /* skipLookup */,
                    ICalTimeZone.TZID_NAME_ASSIGNMENT_BEHAVIOR.ALWAYS_KEEP);
            ICalTimeZone matchtz = ICalTimeZone.lookupMatchingWellKnownTZ(tz);
            assertEquals("ID of Timezone which fuzzy matches GMT=06.00/-05.00", "America/Chicago", matchtz.getID());
        }
    }

    private static String VtimeZoneGMT_0800_0700 = "BEGIN:VCALENDAR\n" + "BEGIN:VTIMEZONE\n"
            + "TZID:GMT-08.00/-07.00\n" + "BEGIN:STANDARD\n" + "DTSTART:16010101T010000\n" + "TZOFFSETTO:-0800\n"
            + "TZOFFSETFROM:-0700\n" + "RRULE:FREQ=YEARLY;INTERVAL=1;BYMONTH=11;BYDAY=1SU;WKST=MO\n"
            + "END:STANDARD\n" + "BEGIN:DAYLIGHT\n" + "DTSTART:16010101T030000\n" + "TZOFFSETTO:-0700\n"
            + "TZOFFSETFROM:-0800\n" + "RRULE:FREQ=YEARLY;INTERVAL=1;BYMONTH=3;BYDAY=2SU;WKST=MO\n"
            + "END:DAYLIGHT\n" + "END:VTIMEZONE\n" + "END:VCALENDAR\n";

    @Test
    public void testFuzzyTimeZoneMatchGMT_08() throws Exception {
        try (ByteArrayInputStream bais = new ByteArrayInputStream(VtimeZoneGMT_0800_0700.getBytes())) {
            ZVCalendar tzcal = ZCalendar.ZCalendarBuilder.build(bais, MimeConstants.P_CHARSET_UTF8);
            assertNotNull("tzcal", tzcal);
            ZComponent tzcomp = tzcal.getComponent(ICalTok.VTIMEZONE);
            assertNotNull("tzcomp", tzcomp);
            ICalTimeZone tz = ICalTimeZone.fromVTimeZone(tzcomp, false /* skipLookup */,
                    ICalTimeZone.TZID_NAME_ASSIGNMENT_BEHAVIOR.ALWAYS_KEEP);
            ICalTimeZone matchtz = ICalTimeZone.lookupMatchingWellKnownTZ(tz);
            assertEquals("ID of Timezone which fuzzy matches GMT=08.00/-07.00", "America/Los_Angeles",
                    matchtz.getID());
        }
    }

    public static String LOTUS_NOTES_WITH_BAD_GMT_TZID = "BEGIN:VCALENDAR\r\n" + "X-LOTUS-CHARSET:UTF-8\r\n"
            + "VERSION:2.0\r\n" + "PRODID:-//Lotus Development Corporation//NONSGML Notes 8.5.3//EN_C\r\n"
            + "METHOD:REQUEST\r\n" + "BEGIN:VTIMEZONE\r\n" + "TZID:GMT\r\n" + "BEGIN:STANDARD\r\n"
            + "DTSTART:19501029T020000\r\n" + "TZOFFSETFROM:+0100\r\n" + "TZOFFSETTO:+0000\r\n"
            + "RRULE:FREQ=YEARLY;BYMINUTE=0;BYHOUR=2;BYDAY=-1SU;BYMONTH=10\r\n" + "END:STANDARD\r\n"
            + "BEGIN:DAYLIGHT\r\n" + "DTSTART:19500326T020000\r\n" + "TZOFFSETFROM:+0000\r\n"
            + "TZOFFSETTO:+0100\r\n" + "RRULE:FREQ=YEARLY;BYMINUTE=0;BYHOUR=2;BYDAY=-1SU;BYMONTH=3\r\n"
            + "END:DAYLIGHT\r\n" + "END:VTIMEZONE\r\n" + "BEGIN:VEVENT\r\n"
            + "DTSTART;TZID=\"GMT\":20150721T140000\r\n" + "DTEND;TZID=\"GMT\":20150721T150000\r\n"
            + "TRANSP:OPAQUE\r\n" + "DTSTAMP:20150721T072350Z\r\n" + "SEQUENCE:0\r\n"
            + "ATTENDEE;ROLE=CHAIR;PARTSTAT=ACCEPTED;CN=\"Administrator/zimbra\"\r\n"
            + " ;RSVP=FALSE:mailto:administrator@example.com\r\n"
            + "ATTENDEE;ROLE=REQ-PARTICIPANT;PARTSTAT=NEEDS-ACTION;RSVP=TRUE\r\n"
            + " :mailto:fred.flintstone@example.com\r\n" + "CLASS:PUBLIC\r\n" + "SUMMARY:new meeting\r\n"
            + "ORGANIZER;CN=\"Administrator/zimbra\":mailto:administrator@example.com\r\n"
            + "UID:F0197AA9F439EFC888257E890026367E-Lotus_Notes_Generated\r\n" + "X-LOTUS-BROADCAST:FALSE\r\n"
            + "X-LOTUS-UPDATE-SEQ:1\r\n"
            + "X-LOTUS-UPDATE-WISL:$S:1;$L:1;$B:1;$R:1;$E:1;$W:1;$O:1;$M:1;RequiredAttendees:1;INetRequiredNames:1;AltRequiredNames:1;StorageRequiredNames:1;OptionalAttendees:1;INetOptionalNames:1;AltOptionalNames:1;StorageOptionalNames:1\r\n"
            + "X-LOTUS-NOTESVERSION:2\r\n" + "X-LOTUS-NOTICETYPE:I\r\n" + "X-LOTUS-APPTTYPE:3\r\n"
            + "X-LOTUS-CHILD-UID:F0197AA9F439EFC888257E890026367E\r\n" + "END:VEVENT\r\n" + "END:VCALENDAR\r\n";

    @Test
    public void testLondonTimeZoneCalledGMTkeepSameName() throws Exception {
        try (ByteArrayInputStream bais = new ByteArrayInputStream(LOTUS_NOTES_WITH_BAD_GMT_TZID.getBytes())) {
            ZVCalendar tzcal = ZCalendar.ZCalendarBuilder.build(bais, MimeConstants.P_CHARSET_UTF8);
            assertNotNull("tzcal", tzcal);
            ZComponent tzcomp = tzcal.getComponent(ICalTok.VTIMEZONE);
            assertNotNull("tzcomp", tzcomp);
            ICalTimeZone tz = ICalTimeZone.fromVTimeZone(tzcomp, false /* skipLookup */,
                    ICalTimeZone.TZID_NAME_ASSIGNMENT_BEHAVIOR.ALWAYS_KEEP);
            assertEquals("ID that London Timezone originally with TZID=GMT maps to", "GMT", tz.getID());
            assertEquals("that London Timezone originally with TZID=GMT maps to this daylightTzname", "GMT/BST",
                    tz.getDaylightTzname());
        }
    }

    @Test
    public void testLondonTimeZoneCalledGMT() throws Exception {
        try (ByteArrayInputStream bais = new ByteArrayInputStream(LOTUS_NOTES_WITH_BAD_GMT_TZID.getBytes())) {
            ZVCalendar tzcal = ZCalendar.ZCalendarBuilder.build(bais, MimeConstants.P_CHARSET_UTF8);
            assertNotNull("tzcal", tzcal);
            ZComponent tzcomp = tzcal.getComponent(ICalTok.VTIMEZONE);
            assertNotNull("tzcomp", tzcomp);
            ICalTimeZone tz = ICalTimeZone.fromVTimeZone(tzcomp, false /* skipLookup */,
                    ICalTimeZone.TZID_NAME_ASSIGNMENT_BEHAVIOR.KEEP_IF_DOESNT_CLASH);
            assertEquals("ID that London Timezone originally with TZID=GMT maps to", "Europe/London", tz.getID());
            assertEquals("that London Timezone originally with TZID=GMT maps to this daylightTzname", "GMT/BST",
                    tz.getDaylightTzname());
        }
    }

    private void attendeeDeleteFromCalendar(boolean suppressReply) throws Exception {
        Account dav1 = users[1].create();
        users[2].create();
        String url = getSchedulingInboxUrl(dav1, dav1);
        ReportMethod method = new ReportMethod(url);
        addBasicAuthHeaderForUser(method, dav1);

        ZMailbox organizer = users[2].getZMailbox();
        users[1].getZMailbox(); // Force creation of mailbox - shouldn't be needed
        String subject = String.format("%s %s", NAME_PREFIX,
                suppressReply ? "testInvite which shouldNOT be replied to" : "testInvite to be auto-declined");
        Date startDate = new Date(System.currentTimeMillis() + Constants.MILLIS_PER_DAY);
        Date endDate = new Date(startDate.getTime() + Constants.MILLIS_PER_HOUR);
        TestUtil.createAppointment(organizer, subject, dav1.getName(), startDate, endDate);

        // Wait for appointment to arrive
        String href = waitForNewSchedulingRequestByUID(dav1, "");
        assertNotNull("href for inbox invitation", href);
        String uid = href.substring(href.lastIndexOf('/') + 1);
        uid = uid.substring(0, uid.indexOf(',') - 1);
        String calFolderUrl = getFolderUrl(dav1, "Calendar");
        String delurl = waitForItemInCalendarCollectionByUID(calFolderUrl, dav1, uid, true, 5000);
        StringBuilder sb = getLocalServerRoot().append(delurl);
        DeleteMethod delMethod = new DeleteMethod(sb.toString());
        addBasicAuthHeaderForUser(delMethod, dav1);
        if (suppressReply) {
            delMethod.addRequestHeader(DavProtocol.HEADER_SCHEDULE_REPLY, "F");
        }

        HttpClient client = new HttpClient();
        HttpMethodExecutor.execute(client, delMethod, HttpStatus.SC_NO_CONTENT);
        List<ZMessage> msgs;
        if (suppressReply) {
            // timeout may be a bit short but don't want long time wastes in test suite.
            msgs = TestUtil.waitForMessages(organizer, "is:invite is:unread inid:2 after:\"-1month\"", 0, 2000);
            if (msgs != null) {
                assertEquals("Should be no DECLINE reply msg", 0, msgs.size());
            }
        } else {
            msgs = TestUtil.waitForMessages(organizer, "is:invite is:unread inid:2 after:\"-1month\"", 1, 10000);
            assertNotNull("inbox DECLINE reply msgs", msgs);
            assertEquals("Should be 1 DECLINE reply msg", 1, msgs.size());
            assertNotNull("inbox DECLINE reply msg invite", msgs.get(0).getInvite());
        }
    }

    @Test
    public void testAttendeeAutoDecline() throws Exception {
        attendeeDeleteFromCalendar(false /* suppressReply */);
    }

    @Test
    public void testAttendeeSuppressedAutoDecline() throws Exception {
        attendeeDeleteFromCalendar(true /* suppressReply */);
    }

    public static String simpleVcard = "BEGIN:VCARD\r\n" + "VERSION:3.0\r\n" + "FN:TestCal\r\n"
            + "N:Dog;Scruffy\r\n" + "EMAIL;TYPE=INTERNET,PREF:scruffy@example.com\r\n" + "UID:SCRUFF1\r\n"
            + "END:VCARD\r\n";

    @Test
    public void testCreateContactWithIfNoneMatchTesting() throws ServiceException, IOException {
        Account dav1 = users[1].create();
        String davBaseName = "SCRUFF1.vcf"; // Based on UID
        String contactsFolderUrl = getFolderUrl(dav1, "Contacts");
        String url = String.format("%s%s", contactsFolderUrl, davBaseName);
        HttpClient client = new HttpClient();
        PutMethod putMethod = new PutMethod(url);
        addBasicAuthHeaderForUser(putMethod, dav1);
        putMethod.addRequestHeader("Content-Type", "text/vcard");

        putMethod.setRequestEntity(new ByteArrayRequestEntity(simpleVcard.getBytes(), MimeConstants.CT_TEXT_VCARD));
        // Bug 84246 this used to fail with 409 Conflict because we used to require an If-None-Match header
        HttpMethodExecutor.execute(client, putMethod, HttpStatus.SC_CREATED);

        // Check that trying to put the same thing again when we don't expect it to exist (i.e. Using If-None-Match
        // header) will fail.
        putMethod = new PutMethod(url);
        addBasicAuthHeaderForUser(putMethod, dav1);
        putMethod.addRequestHeader("Content-Type", "text/vcard");
        putMethod.addRequestHeader(DavProtocol.HEADER_IF_NONE_MATCH, "*");
        putMethod.setRequestEntity(new ByteArrayRequestEntity(simpleVcard.getBytes(), MimeConstants.CT_TEXT_VCARD));
        HttpMethodExecutor.execute(client, putMethod, HttpStatus.SC_PRECONDITION_FAILED);
    }

    private static String rachelVcard = "BEGIN:VCARD\n" + "VERSION:3.0\n"
            + "PRODID:-//BusyMac LLC//BusyContacts 1.0.2//EN\n" + "FN:La Rochelle\n" + "N:Rochelle;La;;;\n"
            + "EMAIL;TYPE=internet:rachel@fun.org\n" + "CATEGORIES:BlueGroup\n" + "REV:2015-04-04T13:55:56Z\n"
            + "UID:07139DE2-EA7B-46CB-A970-C4DF7F72D9AE\n" + "X-BUSYMAC-MODIFIED-BY:Gren Elliot\n"
            + "X-CREATED:2015-04-04T13:55:25Z\n" + "END:VCARD\n";

    private static String blueGroupCreate = "BEGIN:VCARD\n" + "VERSION:3.0\n"
            + "PRODID:-//BusyMac LLC//BusyContacts 1.0.2//EN\n" + "FN:BlueGroup\n" + "N:BlueGroup\n"
            + "REV:2015-04-04T13:55:56Z\n" + "UID:F53A6F96-566F-46CC-8D48-A5263FAB5E38\n"
            + "X-ADDRESSBOOKSERVER-KIND:group\n"
            + "X-ADDRESSBOOKSERVER-MEMBER:urn:uuid:07139DE2-EA7B-46CB-A970-C4DF7F72D9AE\n" + "END:VCARD\n";

    private static String parisVcard = "BEGIN:VCARD\n" + "VERSION:3.0\n" + "FN:Paris Match\n" + "N:Match;Paris;;;\n"
            + "EMAIL;TYPE=internet:match@paris.fr\n" + "CATEGORIES:BlueGroup\n" + "REV:2015-04-04T13:56:50Z\n"
            + "UID:BE43F16D-336E-4C3E-BAE6-22B8F245A986\n" + "END:VCARD\n";

    private static String blueGroupModify = "BEGIN:VCARD\n" + "VERSION:3.0\n"
            + "PRODID:-//BusyMac LLC//BusyContacts 1.0.2//EN\n" + "FN:BlueGroup\n" + "N:BlueGroup\n"
            + "REV:2015-04-04T13:56:50Z\n" + "UID:F53A6F96-566F-46CC-8D48-A5263FAB5E38\n"
            + "X-ADDRESSBOOKSERVER-KIND:group\n"
            + "X-ADDRESSBOOKSERVER-MEMBER:urn:uuid:BE43F16D-336E-4C3E-BAE6-22B8F245A986\n"
            + "X-ADDRESSBOOKSERVER-MEMBER:urn:uuid:07139DE2-EA7B-46CB-A970-C4DF7F72D9AE\n" + "END:VCARD\n";

    @Test
    public void testAppleStyleGroup() throws ServiceException, IOException {
        Account dav1 = users[1].create();
        String contactsFolderUrl = getFolderUrl(dav1, "Contacts");
        HttpClient client = new HttpClient();

        PostMethod postMethod = new PostMethod(contactsFolderUrl);
        addBasicAuthHeaderForUser(postMethod, dav1);
        postMethod.addRequestHeader("Content-Type", "text/vcard");
        postMethod
                .setRequestEntity(new ByteArrayRequestEntity(rachelVcard.getBytes(), MimeConstants.CT_TEXT_VCARD));
        HttpMethodExecutor.execute(client, postMethod, HttpStatus.SC_CREATED);

        postMethod = new PostMethod(contactsFolderUrl);
        addBasicAuthHeaderForUser(postMethod, dav1);
        postMethod.addRequestHeader("Content-Type", "text/vcard");
        postMethod.setRequestEntity(
                new ByteArrayRequestEntity(blueGroupCreate.getBytes(), MimeConstants.CT_TEXT_VCARD));
        HttpMethodExecutor exe = HttpMethodExecutor.execute(client, postMethod, HttpStatus.SC_CREATED);
        String groupLocation = null;
        for (Header hdr : exe.respHeaders) {
            if ("Location".equals(hdr.getName())) {
                groupLocation = hdr.getValue();
            }
        }
        assertNotNull("Location Header returned when creating Group", groupLocation);

        postMethod = new PostMethod(contactsFolderUrl);
        addBasicAuthHeaderForUser(postMethod, dav1);
        postMethod.addRequestHeader("Content-Type", "text/vcard");
        postMethod.setRequestEntity(new ByteArrayRequestEntity(parisVcard.getBytes(), MimeConstants.CT_TEXT_VCARD));
        HttpMethodExecutor.execute(client, postMethod, HttpStatus.SC_CREATED);

        String url = String.format("%s%s", contactsFolderUrl, "F53A6F96-566F-46CC-8D48-A5263FAB5E38.vcf");
        PutMethod putMethod = new PutMethod(url);
        addBasicAuthHeaderForUser(putMethod, dav1);
        putMethod.addRequestHeader("Content-Type", "text/vcard");
        putMethod.setRequestEntity(
                new ByteArrayRequestEntity(blueGroupModify.getBytes(), MimeConstants.CT_TEXT_VCARD));
        HttpMethodExecutor.execute(client, putMethod, HttpStatus.SC_NO_CONTENT);

        GetMethod getMethod = new GetMethod(url);
        addBasicAuthHeaderForUser(getMethod, dav1);
        getMethod.addRequestHeader("Content-Type", "text/vcard");
        exe = HttpMethodExecutor.execute(client, getMethod, HttpStatus.SC_OK);
        String respBody = new String(exe.responseBodyBytes, MimeConstants.P_CHARSET_UTF8);
        String[] expecteds = { "X-ADDRESSBOOKSERVER-KIND:group",
                "X-ADDRESSBOOKSERVER-MEMBER:urn:uuid:BE43F16D-336E-4C3E-BAE6-22B8F245A986",
                "X-ADDRESSBOOKSERVER-MEMBER:urn:uuid:07139DE2-EA7B-46CB-A970-C4DF7F72D9AE" };
        for (String expected : expecteds) {
            assertTrue(String.format("GET should contain '%s'\nBODY=%s", expected, respBody),
                    respBody.contains(expected));
        }

        // members are actually stored in a different way.  Make sure it isn't a fluke
        // that the GET response contained the correct members by checking that the members
        // appear where expected in a search hit.
        SearchRequest searchRequest = new SearchRequest();
        searchRequest.setSortBy("dateDesc");
        searchRequest.setLimit(8);
        searchRequest.setSearchTypes("contact");
        searchRequest.setQuery("in:Contacts");
        ZMailbox mbox = users[1].getZMailbox();
        SearchResponse searchResp = mbox.invokeJaxb(searchRequest);
        assertNotNull("JAXB SearchResponse object", searchResp);
        List<SearchHit> hits = searchResp.getSearchHits();
        assertNotNull("JAXB SearchResponse hits", hits);
        assertEquals("JAXB SearchResponse hits", 3, hits.size());
        boolean seenGroup = false;
        for (SearchHit hit : hits) {
            ContactInfo contactInfo = (ContactInfo) hit;
            if ("BlueGroup".equals(contactInfo.getFileAs())) {
                seenGroup = true;
                assertEquals("Number of members of group in search hit", 2,
                        contactInfo.getContactGroupMembers().size());
            }
            ZimbraLog.test.info("Hit %s class=%s", hit, hit.getClass().getName());
        }
        assertTrue("Seen group", seenGroup);
    }

    private static String smallBusyMacAttach = "BEGIN:VCARD\r\n" + "VERSION:3.0\r\n"
            + "PRODID:-//BusyMac LLC//BusyContacts 1.0.2//EN\r\n" + "FN:John Smith\r\n" + "N:Smith;John;;;\r\n"
            + "REV:2015-04-05T09:51:09Z\r\n" + "UID:99E01E16-03B3-4487-AAEF-AEB496852C06\r\n"
            + "X-BUSYMAC-ATTACH;ENCODING=b;X-FILENAME=favicon.ico:AAABAAEAEBAAAAEAIABoBAAA\r\n"
            + " FgAAACgAAAAQAAAAIAAAAAEAIAAAAAAAQAQAABMLAAATCwAAAAAAAAAAAAAAAAAAw4cAY8OHAM\r\n"
            + " nDhwD8w4cA/8OHAP/DhwD/w4cA/8OHAP/DhwD/w4cA/8OHAP/DhwD8w4cAycOHAGMAAAAAw4cA\r\n"
            + " Y8OHAP/DhwD/w4cA/8OHAP/DhwD/w4cA/8OHAP/DhwD/w4cA/8OHAP/DhwD/w4cA/8OHAP/Dhw\r\n"
            + " D/w4cAY8OHAMnDhwD/w4cA/7yYSv/y5Mb/8uXH//Llx//z5sr/8+bK//Pmyv/z58v/8+bK/8qq\r\n"
            + " Y//DhwD/w4cA/8OHAMnDhwDhw4cA/8OHAP++q4D///////////////7////+//////////////\r\n"
            + " /////////Yyan/w4cA/8OHAP/DhwDhw4cA4cOHAP/DhwD/t4QR/9/azv//////5t3K/9StVv/b\r\n"
            + " t2b/27dm/9u3Z//cuGn/wpAh/8OHAP/DhwD/w4cA4cOHAOHDhwD/w4cA/8OHAP+2jzr/+fj2//\r\n"
            + " n49f/BnU7/w4cA/8OHAP/DhwD/w4cA/8OHAP/DhwD/w4cA/8OHAOHDhwDhw4cA/8OHAP/DhwD/\r\n"
            + " w4cA/7ihbf//////8u/p/8GRJv/DhwD/w4cA/8OHAP/DhwD/w4cA/8OHAP/DhwDhw4cA4cOHAP\r\n"
            + " /DhwD/w4cA/8OHAP/BhgP/0siz///////d1L//wYgI/8OHAP/DhwD/w4cA/8OHAP/DhwD/w4cA\r\n"
            + " 4cOHAOHDhwD/w4cA/8OHAP/DhwD/w4cA/7eIIP/n49v//////8e0iP/DhwD/w4cA/8OHAP/Dhw\r\n"
            + " D/w4cA/8OHAOHDhwDhw4cA/8OHAP/DhwD/w4cA/8OHAP/DhwD/rItA//39/P/6+vj/w6BQ/8OH\r\n"
            + " AP/DhwD/w4cA/8OHAP/DhwDhw4cA4cOHAP/DhwD/w4cA/8OHAP/DhwD/w4cA/8OHAP+8p3r//v\r\n"
            + " 79/+3p4v+8ix3/w4cA/8OHAP/DhwD/w4cA4cOHAOHDhwD/w4cA/8CHB//VsFz/3rxx/926bf/c\r\n"
            + " uWv/xadh//Ht5///////1suz/7+HCv/DhwD/w4cA/8OHAOHDhwDhw4cA/8OHAP+wjT//+/r5//\r\n"
            + " /////////////////////+/v7///////7+/v+8n17/w4cA/8OHAP/DhwDhw4cAycOHAP/DhwD/\r\n"
            + " t4gd/+bYuP/16tP/9OjP//Toz//06M//8+fN//Pozv/t4MH/vZIx/8OHAP/DhwD/w4cAycOHAG\r\n"
            + " DDhwD/w4cA/8OHAP/DhwD/w4cA/8OHAP/DhwD/w4cA/8OHAP/DhwD/w4cA/8OHAP/DhwD/w4cA\r\n"
            + " /8OHAGAAAAAAw4cAWsOHAMnDhwD8w4cA/8OHAP/DhwD/w4cA/8OHAP/DhwD/w4cA/8OHAP/Dhw\r\n"
            + " D8w4cAycOHAFoAAAAAgAEAAAAAAAAAAAAAAABoQAAAAAAAAPC/AAAAAAAAAAAAAAAAAAAiQAAA\r\n"
            + " AAAAAAAAAAAAAAAAAAAAAAAAgAEAAA==\r\n" + "X-BUSYMAC-MODIFIED-BY:Gren Elliot\r\n"
            + "X-CUSTOM:one two three four five six seven eight nine ten eleven twelve thirteen fourteen fifteen\r\n"
            + "X-CUSTOM:Here are my simple\\nmultiline\\nnotes\r\n"
            + "X-CUSTOM;TYPE=pref:semi-colon\\;seperated\\;\"stuff\"\\;here\r\n"
            + "X-CUSTOM:comma\\,\"stuff\"\\,'there'\\,too\r\n" + "X-HOBBY:my backslash\\\\ hobbies\r\n"
            + "X-CREATED:2015-04-05T09:50:44Z\r\n" + "END:VCARD\r\n";

    @Test
    public void testXBusyMacAttach() throws ServiceException, IOException {
        Account dav1 = users[1].create();
        String contactsFolderUrl = getFolderUrl(dav1, "Contacts");
        HttpClient client = new HttpClient();

        PostMethod postMethod = new PostMethod(contactsFolderUrl);
        addBasicAuthHeaderForUser(postMethod, dav1);
        postMethod.addRequestHeader("Content-Type", "text/vcard");
        postMethod.setRequestEntity(
                new ByteArrayRequestEntity(smallBusyMacAttach.getBytes(), MimeConstants.CT_TEXT_VCARD));
        HttpMethodExecutor exe = HttpMethodExecutor.execute(client, postMethod, HttpStatus.SC_CREATED);
        String location = null;
        for (Header hdr : exe.respHeaders) {
            if ("Location".equals(hdr.getName())) {
                location = hdr.getValue();
            }
        }
        assertNotNull("Location Header returned when creating", location);
        String url = String.format("%s%s", contactsFolderUrl, location.substring(location.lastIndexOf('/') + 1));
        GetMethod getMethod = new GetMethod(url);
        addBasicAuthHeaderForUser(getMethod, dav1);
        getMethod.addRequestHeader("Content-Type", "text/vcard");
        exe = HttpMethodExecutor.execute(client, getMethod, HttpStatus.SC_OK);
        String respBody = new String(exe.responseBodyBytes, MimeConstants.P_CHARSET_UTF8);
        String[] expecteds = { "\r\nX-BUSYMAC-ATTACH;X-FILENAME=favicon.ico;ENCODING=B:AAABAAEAEBAAAAEAIABoBA\r\n",
                "\r\n AAFgAAACgAAAAQAAAAIAAAAAEAIAAAAAAAQAQAABMLAAATCwAAAAAAAAAAAAAAAAAAw4cAY8\r\n",
                "\r\nX-BUSYMAC-MODIFIED-BY:Gren Elliot\r\n",
                "\r\nX-CUSTOM:one two three four five six seven eight nine ten eleven twelve t\r\n hirteen fourteen fifteen",
                "\r\nX-CUSTOM:Here are my simple\\Nmultiline\\Nnotes\r\n",
                "\r\nX-CUSTOM;TYPE=pref:semi-colon\\;seperated\\;\"stuff\"\\;here\r\n",
                "\r\nX-CUSTOM:comma\\,\"stuff\"\\,'there'\\,too\r\n", "\r\nX-HOBBY:my backslash\\\\ hobbies\r\n",
                "\r\nX-CREATED:2015-04-05T09:50:44Z\r\n" };
        for (String expected : expecteds) {
            assertTrue(String.format("GET should contain '%s'\nBODY=%s", expected, respBody),
                    respBody.contains(expected));
        }

        SearchRequest searchRequest = new SearchRequest();
        searchRequest.setSortBy("dateDesc");
        searchRequest.setLimit(8);
        searchRequest.setSearchTypes("contact");
        searchRequest.setQuery("in:Contacts");
        ZMailbox mbox = users[1].getZMailbox();
        SearchResponse searchResp = mbox.invokeJaxb(searchRequest);
        assertNotNull("JAXB SearchResponse object", searchResp);
        List<SearchHit> hits = searchResp.getSearchHits();
        assertNotNull("JAXB SearchResponse hits", hits);
        assertEquals("JAXB SearchResponse hits", 1, hits.size());
    }

    public static final String expandPropertyGroupMemberSet = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
            + "<A:expand-property xmlns:A=\"DAV:\">" + "  <A:property name=\"group-member-set\" namespace=\"DAV:\">"
            + "    <A:property name=\"email-address-set\" namespace=\"http://calendarserver.org/ns/\"/>"
            + "    <A:property name=\"calendar-user-address-set\" namespace=\"urn:ietf:params:xml:ns:caldav\"/>"
            + "    <A:property name=\"displayname\" namespace=\"DAV:\"/>" + "  </A:property>"
            + "</A:expand-property>";

    public static final String expandPropertyDelegateFor = "<A:expand-property xmlns:A=\"DAV:\">"
            + "  <A:property name=\"calendar-proxy-write-for\" namespace=\"http://calendarserver.org/ns/\">"
            + "    <A:property name=\"displayname\" namespace=\"DAV:\"/>"
            + "    <A:property name=\"calendar-user-address-set\" namespace=\"urn:ietf:params:xml:ns:caldav\"/>"
            + "    <A:property name=\"email-address-set\" namespace=\"http://calendarserver.org/ns/\"/>"
            + "  </A:property>"
            + "  <A:property name=\"calendar-proxy-read-for\" namespace=\"http://calendarserver.org/ns/\">"
            + "    <A:property name=\"displayname\" namespace=\"DAV:\"/>"
            + "    <A:property name=\"calendar-user-address-set\" namespace=\"urn:ietf:params:xml:ns:caldav\"/>"
            + "    <A:property name=\"email-address-set\" namespace=\"http://calendarserver.org/ns/\"/>"
            + "  </A:property>" + "</A:expand-property>";

    public static String propPatchGroupMemberSetTemplate = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
            + "<A:propertyupdate xmlns:A=\"DAV:\">" + "    <A:set>" + "        <A:prop>"
            + "            <A:group-member-set>" + "                <A:href>%%MEMBER%%</A:href>"
            + "            </A:group-member-set>" + "        </A:prop>" + "    </A:set>" + "</A:propertyupdate>";

    private void setZimbraPrefAppleIcalDelegationEnabled(ZMailbox mbox, Boolean val) throws ServiceException {
        ModifyPrefsRequest modPrefsReq = new ModifyPrefsRequest();
        Pref pref = Pref.createPrefWithNameAndValue(ZAttrProvisioning.A_zimbraPrefAppleIcalDelegationEnabled,
                val.toString().toUpperCase());
        modPrefsReq.addPref(pref);
        ModifyPrefsResponse modPrefsResp = mbox.invokeJaxb(modPrefsReq);
        assertNotNull("null ModifyPrefs Response for forwarding calendar invites/no auto-add", modPrefsResp);
    }

    /**
     * http://svn.calendarserver.org/repository/calendarserver/CalendarServer/trunk/doc/Extensions/caldav-proxy.txt
     * This is an Apple standard implemented by Apple Mac OSX at least up to Yosemite and offers a fairly simple
     * sharing model for calendars.  The model is simpler than Zimbra's native model and there are mismatches,
     * for instance Zimbra requires the proposed delegate to accept shares.
     */
    @Test
    public void testAppleCaldavProxyFunctions() throws ServiceException, IOException {
        Account sharer = users[3].create();
        Account sharee1 = users[1].create();
        Account sharee2 = users[2].create();
        ZMailbox mboxSharer = TestUtil.getZMailbox(sharer.getName());
        ZMailbox mboxSharee1 = TestUtil.getZMailbox(sharee1.getName());
        ZMailbox mboxSharee2 = TestUtil.getZMailbox(sharee2.getName());
        setZimbraPrefAppleIcalDelegationEnabled(mboxSharer, true);
        setZimbraPrefAppleIcalDelegationEnabled(mboxSharee1, true);
        setZimbraPrefAppleIcalDelegationEnabled(mboxSharee2, true);
        // Test PROPPATCH to "calendar-proxy-read" URL
        setGroupMemberSet(TestCalDav.getCalendarProxyReadUrl(sharer), sharer, sharee2);
        // Test PROPPATCH to "calendar-proxy-write" URL
        setGroupMemberSet(TestCalDav.getCalendarProxyWriteUrl(sharer), sharer, sharee1);

        // verify that adding new members to groups triggered notification messages
        List<ZMessage> msgs = TestUtil.waitForMessages(mboxSharee1,
                "in:inbox subject:\"Share Created: Calendar shared by \"", 1, 10000);
        assertNotNull(String.format("Notification msgs for %s", sharee1.getName()), msgs);
        assertEquals(String.format("num msgs for %s", sharee1.getName()), 1, msgs.size());

        msgs = TestUtil.waitForMessages(mboxSharee2, "in:inbox subject:\"Share Created: Calendar shared by \"", 1,
                10000);
        assertNotNull(String.format("Notification msgs for %s", sharee2.getName()), msgs);
        assertEquals(String.format("num msgs for %s", sharee2.getName()), 1, msgs.size());
        // Simulate acceptance of the shares (would normally need to be done in ZWC
        createCalendarMountPoint(mboxSharee1, sharer);
        createCalendarMountPoint(mboxSharee2, sharer);
        Document doc = delegateForExpandProperty(sharee1);
        XPath xpath = XPathFactory.newInstance().newXPath();
        xpath.setNamespaceContext(TestCalDav.NamespaceContextForXPath.forCalDAV());
        XPathExpression xPathExpr;
        try {
            String xpathS = "/D:multistatus/D:response/D:href/text()";
            xPathExpr = xpath.compile(xpathS);
            NodeList result = (NodeList) xPathExpr.evaluate(doc, XPathConstants.NODESET);
            assertEquals(String.format("num XPath nodes for %s for %s", xpathS, sharee1.getName()), 1,
                    result.getLength());
            String text = (String) xPathExpr.evaluate(doc, XPathConstants.STRING);
            assertEquals("HREF for account owner", UrlNamespace.getPrincipalUrl(sharee1).replaceAll("@", "%40"),
                    text);

            xpathS = "/D:multistatus/D:response/D:propstat/D:prop/CS:calendar-proxy-write-for/D:response/D:href/text()";
            xPathExpr = xpath.compile(xpathS);
            result = (NodeList) xPathExpr.evaluate(doc, XPathConstants.NODESET);
            assertEquals(String.format("num XPath nodes for %s for %s", xpathS, sharee1.getName()), 1,
                    result.getLength());
            text = (String) xPathExpr.evaluate(doc, XPathConstants.STRING);
            assertEquals("HREF for sharer", UrlNamespace.getPrincipalUrl(sharer).replaceAll("@", "%40"), text);
        } catch (XPathExpressionException e1) {
            ZimbraLog.test.debug("xpath problem", e1);
        }
        // Check that proxy write has sharee1 in it
        doc = groupMemberSetExpandProperty(sharer, sharee1, true);
        // Check that proxy read has sharee2 in it
        doc = groupMemberSetExpandProperty(sharer, sharee2, false);
        String davBaseName = "notAllowed@There";
        String url = String.format("%s%s",
                getFolderUrl(sharee1, "Shared Calendar").replaceAll(" ", "%20").replaceAll("@", "%40"),
                davBaseName);
        HttpMethodExecutor exe = doIcalPut(url, sharee1, simpleEvent(sharer), HttpStatus.SC_MOVED_TEMPORARILY);
        String location = null;
        for (Header hdr : exe.respHeaders) {
            if ("Location".equals(hdr.getName())) {
                location = hdr.getValue();
            }
        }
        assertNotNull("Location Header not returned when creating", location);
        url = String.format("%s%s",
                getFolderUrl(sharee1, "Shared Calendar").replaceAll(" ", "%20").replaceAll("@", "%40"),
                location.substring(location.lastIndexOf('/') + 1));
        doIcalPut(url, sharee1, simpleEvent(sharer), HttpStatus.SC_CREATED);
    }

    public static Document groupMemberSetExpandProperty(Account acct, Account member, boolean proxyWrite)
            throws IOException, ServiceException {
        String url = proxyWrite ? TestCalDav.getCalendarProxyWriteUrl(acct)
                : TestCalDav.getCalendarProxyReadUrl(acct);
        url = url.replaceAll("@", "%40");
        String href = proxyWrite ? UrlNamespace.getCalendarProxyWriteUrl(acct, acct)
                : UrlNamespace.getCalendarProxyReadUrl(acct, acct);
        href = href.replaceAll("@", "%40");
        ReportMethod method = new ReportMethod(url);
        addBasicAuthHeaderForUser(method, acct);
        HttpClient client = new HttpClient();
        TestCalDav.HttpMethodExecutor executor;
        method.addRequestHeader("Content-Type", MimeConstants.CT_TEXT_XML);
        method.setRequestEntity(new ByteArrayRequestEntity(TestCalDav.expandPropertyGroupMemberSet.getBytes(),
                MimeConstants.CT_TEXT_XML));
        executor = new TestCalDav.HttpMethodExecutor(client, method, HttpStatus.SC_MULTI_STATUS);
        String respBody = new String(executor.responseBodyBytes, MimeConstants.P_CHARSET_UTF8);
        Document doc = W3cDomUtil.parseXMLToDoc(respBody);
        org.w3c.dom.Element docElement = doc.getDocumentElement();
        assertEquals("Report node name", DavElements.P_MULTISTATUS, docElement.getLocalName());
        XPath xpath = XPathFactory.newInstance().newXPath();
        xpath.setNamespaceContext(TestCalDav.NamespaceContextForXPath.forCalDAV());
        XPathExpression xPathExpr;
        try {
            String xpathS = "/D:multistatus/D:response/D:href/text()";
            xPathExpr = xpath.compile(xpathS);
            String text = (String) xPathExpr.evaluate(doc, XPathConstants.STRING);
            assertEquals("HREF for response", href, text);

            xpathS = "/D:multistatus/D:response/D:propstat/D:prop/D:group-member-set/D:response/D:href/text()";
            xPathExpr = xpath.compile(xpathS);
            text = (String) xPathExpr.evaluate(doc, XPathConstants.STRING);
            assertEquals("HREF for sharee", UrlNamespace.getPrincipalUrl(member).replaceAll("@", "%40"), text);
        } catch (XPathExpressionException e1) {
            ZimbraLog.test.debug("xpath problem", e1);
        }
        return doc;
    }

    public static Document delegateForExpandProperty(Account acct) throws IOException, ServiceException {
        ReportMethod method = new ReportMethod(getPrincipalUrl(acct));
        addBasicAuthHeaderForUser(method, acct);
        HttpClient client = new HttpClient();
        TestCalDav.HttpMethodExecutor executor;
        method.addRequestHeader("Content-Type", MimeConstants.CT_TEXT_XML);
        method.setRequestEntity(
                new ByteArrayRequestEntity(expandPropertyDelegateFor.getBytes(), MimeConstants.CT_TEXT_XML));
        executor = new TestCalDav.HttpMethodExecutor(client, method, HttpStatus.SC_MULTI_STATUS);
        String respBody = new String(executor.responseBodyBytes, MimeConstants.P_CHARSET_UTF8);
        Document doc = W3cDomUtil.parseXMLToDoc(respBody);
        org.w3c.dom.Element docElement = doc.getDocumentElement();
        assertEquals("Report node name", DavElements.P_MULTISTATUS, docElement.getLocalName());
        return doc;
    }

    public static CreateMountpointResponse createCalendarMountPoint(ZMailbox mboxSharee, Account sharer)
            throws ServiceException {
        NewMountpointSpec mpSpec = new NewMountpointSpec("Shared Calendar");
        mpSpec.setFlags("#");
        mpSpec.setRemoteId(Mailbox.ID_FOLDER_CALENDAR);
        mpSpec.setColor((byte) 4);
        mpSpec.setOwnerId(sharer.getId());
        mpSpec.setFolderId(Integer.valueOf(Mailbox.ID_FOLDER_USER_ROOT).toString());
        mpSpec.setDefaultView(MailItem.Type.APPOINTMENT.toString());
        CreateMountpointRequest createMpReq = new CreateMountpointRequest(mpSpec);
        CreateMountpointResponse createMpResp = mboxSharee.invokeJaxb(createMpReq);
        assertNotNull(String.format("CreateMountPointResponse for %s's Calendar on mbox %s", sharer.getName(),
                mboxSharee.getName()), createMpResp);
        return createMpResp;
    }

    public static Document setGroupMemberSet(String url, Account acct, Account memberAcct)
            throws IOException, XmlParseException {
        PropPatchMethod method = new PropPatchMethod(url);
        addBasicAuthHeaderForUser(method, acct);
        HttpClient client = new HttpClient();
        TestCalDav.HttpMethodExecutor executor;
        method.addRequestHeader("Content-Type", MimeConstants.CT_TEXT_XML);
        String body = TestCalDav.propPatchGroupMemberSetTemplate.replace("%%MEMBER%%",
                UrlNamespace.getPrincipalUrl(memberAcct, memberAcct));
        method.setRequestEntity(new ByteArrayRequestEntity(body.getBytes(), MimeConstants.CT_TEXT_XML));
        executor = new TestCalDav.HttpMethodExecutor(client, method, HttpStatus.SC_MULTI_STATUS);
        String respBody = new String(executor.responseBodyBytes, MimeConstants.P_CHARSET_UTF8);
        Document doc = W3cDomUtil.parseXMLToDoc(respBody);
        org.w3c.dom.Element docElement = doc.getDocumentElement();
        assertEquals("Report node name", DavElements.P_MULTISTATUS, docElement.getLocalName());
        return doc;
    }

    @BeforeClass
    public static void beforeClass() throws Exception {
        localServer = prov.getLocalServer();
    }

    @Before
    public void setUp() throws Exception {
        cleanUp();
        if (!TestUtil.fromRunUnitTests) {
            TestUtil.cliSetup();
            String tzFilePath = LC.timezone_file.value();
            File tzFile = new File(tzFilePath);
            WellKnownTimeZones.loadFromFile(tzFile);
        }
    }

    @After
    public void tearDown() throws Exception {
        cleanUp();
    }

    private void cleanUp() throws Exception {
        TestUtil.UserInfo.cleanup(users);
        try {
            TestUtil.deleteDistributionList(DL1);
        } catch (Exception e) {
            //ignore
        }
    }

    /**
     * @param args
     */
    public static void main(String[] args) throws Exception {
        try {
            TestUtil.runTest(TestCalDav.class);
        } catch (Exception e) {
            System.out.println(e.getMessage());
        }
    }
}