com.zimbra.cs.dav.resource.ScheduleOutbox.java Source code

Java tutorial

Introduction

Here is the source code for com.zimbra.cs.dav.resource.ScheduleOutbox.java

Source

/*
 * ***** BEGIN LICENSE BLOCK *****
 * Zimbra Collaboration Suite Server
 * Copyright (C) 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014, 2016 Synacor, Inc.
 *
 * This program is free software: you can redistribute it and/or modify it under
 * the terms of the GNU General Public License as published by the Free Software Foundation,
 * version 2 of the License.
 *
 * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
 * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
 * See the GNU General Public License for more details.
 * You should have received a copy of the GNU General Public License along with this program.
 * If not, see <https://www.gnu.org/licenses/>.
 * ***** END LICENSE BLOCK *****
 */
package com.zimbra.cs.dav.resource;

import java.io.IOException;
import java.io.InputStream;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Date;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.List;
import java.util.Set;

import javax.mail.Address;
import javax.mail.internet.AddressException;
import javax.mail.internet.InternetAddress;
import javax.mail.internet.MimeMessage;
import javax.servlet.http.HttpServletResponse;

import org.dom4j.Element;

import com.google.common.base.Strings;
import com.google.common.collect.Sets;
import com.google.common.io.Closeables;
import com.zimbra.common.account.Key.AccountBy;
import com.zimbra.common.calendar.ParsedDateTime;
import com.zimbra.common.calendar.ParsedDuration;
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.mime.MimeConstants;
import com.zimbra.common.mime.shim.JavaMailInternetAddress;
import com.zimbra.common.service.ServiceException;
import com.zimbra.common.util.ZimbraLog;
import com.zimbra.cs.account.Account;
import com.zimbra.cs.account.Provisioning;
import com.zimbra.cs.dav.DavContext;
import com.zimbra.cs.dav.DavElements;
import com.zimbra.cs.dav.DavException;
import com.zimbra.cs.dav.DavProtocol;
import com.zimbra.cs.dav.caldav.CalDavUtils;
import com.zimbra.cs.fb.FreeBusy;
import com.zimbra.cs.fb.FreeBusyQuery;
import com.zimbra.cs.index.SortBy;
import com.zimbra.cs.mailbox.CalendarItem;
import com.zimbra.cs.mailbox.Folder;
import com.zimbra.cs.mailbox.Mailbox;
import com.zimbra.cs.mailbox.MailboxManager;
import com.zimbra.cs.mailbox.calendar.CalendarMailSender;
import com.zimbra.cs.mailbox.calendar.FriendlyCalendaringDescription;
import com.zimbra.cs.mailbox.calendar.IcalXmlStrMap;
import com.zimbra.cs.mailbox.calendar.Invite;
import com.zimbra.cs.mailbox.calendar.ZAttendee;
import com.zimbra.cs.mailbox.calendar.ZOrganizer;
import com.zimbra.cs.util.AccountUtil;
import com.zimbra.cs.util.AccountUtil.AccountAddressMatcher;

public class ScheduleOutbox extends CalendarCollection {

    public ScheduleOutbox(DavContext ctxt, Folder f) throws DavException, ServiceException {
        super(ctxt, f);
        addResourceType(DavElements.E_SCHEDULE_OUTBOX);
    }

    @Override
    public void handlePost(DavContext ctxt) throws DavException, IOException, ServiceException {
        DelegationInfo delegationInfo = new DelegationInfo(
                ctxt.getRequest().getHeader(DavProtocol.HEADER_ORIGINATOR));
        Enumeration<String> recipients = ctxt.getRequest().getHeaders(DavProtocol.HEADER_RECIPIENT);

        InputStream in = ctxt.getUpload().getInputStream();

        ZCalendar.ZVCalendar vcalendar = ZCalendar.ZCalendarBuilder.build(in, MimeConstants.P_CHARSET_UTF8);
        Closeables.closeQuietly(in);
        Iterator<ZComponent> iter = vcalendar.getComponentIterator();
        ZComponent req = null;
        while (iter.hasNext()) {
            req = iter.next();
            if (req.getTok() != ICalTok.VTIMEZONE)
                break;
            req = null;
        }
        if (req == null) {
            throw new DavException("empty request", HttpServletResponse.SC_BAD_REQUEST);
        }
        ZimbraLog.dav.debug("originator: %s", delegationInfo.getOriginator());

        boolean isVEventOrVTodo = ICalTok.VEVENT.equals(req.getTok()) || ICalTok.VTODO.equals(req.getTok());
        boolean isOrganizerMethod = false, isCancel = false;
        if (isVEventOrVTodo) {
            String method = vcalendar.getPropVal(ICalTok.METHOD, null);
            if (method != null) {
                isOrganizerMethod = Invite.isOrganizerMethod(method);
                isCancel = ICalTok.CANCEL.toString().equalsIgnoreCase(method);
                ;
            }

            CalDavUtils.removeAttendeeForOrganizer(req); // Apple iCal fixup
        }

        // Get organizer and list of attendees. (mailto:email values)
        ArrayList<String> attendees = new ArrayList<String>();
        String organizer = null;
        for (Iterator<ZProperty> propsIter = req.getPropertyIterator(); propsIter.hasNext();) {
            ZProperty prop = propsIter.next();
            ICalTok token = prop.getToken();
            if (ICalTok.ATTENDEE.equals(token)) {
                String val = prop.getValue();
                if (val != null) {
                    attendees.add(val.trim());
                }
            } else if (ICalTok.ORGANIZER.equals(token)) {
                String val = prop.getValue();
                if (val != null) {
                    organizer = val.trim();
                    String addr = CalDavUtils.stripMailto(organizer);
                    // Rewrite the alias to primary address
                    Account acct = Provisioning.getInstance().get(AccountBy.name, addr);
                    if (acct != null) {
                        String newAddr = acct.getName();
                        if (!addr.equals(newAddr)) {
                            organizer = "mailto:" + newAddr;
                            prop.setValue(organizer);
                        }
                    }
                }
            }
        }

        // Keep originator address consistent with the address used in ORGANIZER/ATTENDEE.
        // Apple iCal is very inconsistent about the user's identity when the account has aliases.
        if (isVEventOrVTodo && delegationInfo.getOriginator() != null && ctxt.getAuthAccount() != null) {
            AccountAddressMatcher acctMatcher = new AccountAddressMatcher(ctxt.getAuthAccount());
            if (acctMatcher.matches(delegationInfo.getOriginatorEmail())) {
                if (isOrganizerMethod) {
                    if (organizer != null) {
                        String organizerEmail = CalDavUtils.stripMailto(organizer);
                        if (!organizerEmail.equalsIgnoreCase(delegationInfo.getOriginatorEmail())
                                && acctMatcher.matches(organizerEmail)) {
                            delegationInfo.setOriginator(organizer);
                            ZimbraLog.dav.debug(
                                    "changing originator to %s to match address/alias used in ORGANIZER",
                                    delegationInfo.getOriginator());
                        }
                    }
                } else {
                    for (String at : attendees) {
                        String atEmail = CalDavUtils.stripMailto(at);
                        if (delegationInfo.getOriginatorEmail().equalsIgnoreCase(atEmail)) {
                            break;
                        } else if (acctMatcher.matches(atEmail)) {
                            delegationInfo.setOriginator(at);
                            ZimbraLog.dav.debug("changing originator to %s to match address/alias used in ATTENDEE",
                                    delegationInfo.getOriginator());
                            break;
                        }
                    }
                }
            }
        }

        // Get the recipients.
        ArrayList<String> rcptArray = new ArrayList<String>();
        while (recipients.hasMoreElements()) {
            String rcptHdr = recipients.nextElement();
            String[] rcpts = null;
            if (rcptHdr.indexOf(',') > 0) {
                rcpts = rcptHdr.split(",");
            } else {
                rcpts = new String[] { rcptHdr };
            }
            for (String rcpt : rcpts) {
                if (rcpt != null) {
                    rcpt = rcpt.trim();
                    if (rcpt.length() != 0) {
                        // Workaround for Apple iCal: Ignore attendees with address "invalid:nomail".
                        if (rcpt.equalsIgnoreCase("invalid:nomail")) {
                            continue;
                        }
                        if (isVEventOrVTodo) {
                            // Workaround for Apple iCal: Never send REQUEST/CANCEL notification to organizer.
                            // iCal can sometimes do that when organizer account has aliases.
                            if (isOrganizerMethod && rcpt.equalsIgnoreCase(organizer)) {
                                continue;
                            }
                            // bug 49987: Workaround for Apple iCal
                            // iCal sends cancel notice to all original attendees when some attendees are removed from the
                            // appointment.  As a result the appointment is cancelled from the calendars of all original
                            // attendees.  Counter this bad behavior by filtering out any recipients who aren't listed
                            // as ATTENDEE in the CANCEL component being sent.  (iCal does that part correctly, at least.)
                            if (isCancel) {
                                boolean isAttendee = false;
                                // Rcpt must be an attendee of the cancel component.
                                for (String at : attendees) {
                                    if (rcpt.equalsIgnoreCase(at)) {
                                        isAttendee = true;
                                        break;
                                    }
                                }
                                if (!isAttendee) {
                                    ZimbraLog.dav.info(
                                            "Ignoring non-attendee recipient '%s' of CANCEL request; likely a client bug",
                                            rcpt);
                                    continue;
                                }
                            }
                        }
                        // All checks passed.
                        rcptArray.add(rcpt);
                    }
                }
            }
        }

        Element scheduleResponse = ctxt.getDavResponse().getTop(DavElements.E_SCHEDULE_RESPONSE);
        for (String rcpt : rcptArray) {
            ZimbraLog.dav.debug("recipient email: " + rcpt);
            Element resp = scheduleResponse.addElement(DavElements.E_CALDAV_RESPONSE);
            switch (req.getTok()) {
            case VFREEBUSY:
                handleFreebusyRequest(ctxt, req, delegationInfo.getOriginator(), rcpt, resp);
                break;
            case VEVENT:
                // adjustOrganizer works around issues where we don't have delegation enabled but clients write
                // to the shared calendar as if it was the delegate's calendar instead of the owner.
                // Note assumption that clients won't generate replies on behalf of owner as they won't be aware
                // that they should be working on behalf of the owner and will only look for their own attendee
                // records.
                if (isOrganizerMethod) {
                    adjustOrganizer(ctxt, vcalendar, req, delegationInfo);
                }
                validateRequest(isOrganizerMethod, delegationInfo, organizer, ctxt, req);
                handleEventRequest(ctxt, vcalendar, req, delegationInfo, rcpt, resp);
                break;
            default:
                throw new DavException("unrecognized request: " + req.getTok(), HttpServletResponse.SC_BAD_REQUEST);
            }
        }
    }

    /**
     * Check for illegal requests like trying to CANCEL a meeting when not the organizer or a delegate for
     * the organizer
     * e.g. Bug 85875 Mac OS X/10.8.5 Calendar sometimes sending CANCEL when ATTENDEE deletes an instance,
     *      resulting in other attendees getting invalid CANCELs
     */
    private void validateRequest(boolean isOrganizerMethod, DelegationInfo delegationInfo, String organizer,
            DavContext ctxt, ZComponent req) throws ServiceException, DavException {
        if ((!isOrganizerMethod) || (organizer != null && organizer.equals(delegationInfo.getOriginator()))) {
            return;
        }
        // If here, only the ORGANIZER or a delegate acting as the ORGANIZER should be able to do this
        AccountAddressMatcher acctMatcher = new AccountAddressMatcher(ctxt.getAuthAccount());
        if (!acctMatcher.matches(delegationInfo.getOriginatorEmail())) {
            throw new DavException(String.format(
                    "invalid POST to scheduling outbox '%s'. originator '%s' is not authorized account or ORGANIZER",
                    ctxt.getRequest().getRequestURI(), delegationInfo.getOriginatorEmail()),
                    HttpServletResponse.SC_BAD_REQUEST);
        }
        String organizerEmail = CalDavUtils.stripMailto(organizer);
        Mailbox mbox = MailboxManager.getInstance().getMailboxByAccount(ctxt.getAuthAccount());
        List<com.zimbra.cs.mailbox.Mountpoint> sharedCalendars = mbox
                .getCalendarMountpoints(ctxt.getOperationContext(), SortBy.NONE);
        if (sharedCalendars != null) {
            for (com.zimbra.cs.mailbox.Mountpoint sharedCalendar : sharedCalendars) {
                Account acct = Provisioning.getInstance().get(AccountBy.id, sharedCalendar.getOwnerId());
                if (acct != null) {
                    acctMatcher = new AccountAddressMatcher(acct);
                    if (acctMatcher.matches(organizerEmail)) {
                        return;
                    }
                }
            }
        }
        // We haven't found a shared calendar for the ORGANIZER that we are a delegate for.
        throw new DavException(
                String.format("invalid POST to scheduling outbox '%s'. '%s' cannot act as ORGANIZER '%s'",
                        ctxt.getRequest().getRequestURI(), delegationInfo.getOriginatorEmail(), organizerEmail),
                HttpServletResponse.SC_BAD_REQUEST);
    }

    private void handleFreebusyRequest(DavContext ctxt, ZComponent vfreebusy, String originator, String rcpt,
            Element resp) throws DavException, ServiceException {
        ZProperty dtstartProp = vfreebusy.getProperty(ICalTok.DTSTART);
        ZProperty dtendProp = vfreebusy.getProperty(ICalTok.DTEND);
        ZProperty durationProp = vfreebusy.getProperty(ICalTok.DURATION);
        if (dtstartProp == null || dtendProp == null && durationProp == null)
            throw new DavException("missing dtstart or dtend/duration in the schedule request",
                    HttpServletResponse.SC_BAD_REQUEST, null);
        long start, end;
        try {
            ParsedDateTime startTime = ParsedDateTime.parseUtcOnly(dtstartProp.getValue());
            start = startTime.getUtcTime();
            if (dtendProp != null) {
                end = ParsedDateTime.parseUtcOnly(dtendProp.getValue()).getUtcTime();
            } else {
                ParsedDuration dur = ParsedDuration.parse(durationProp.getValue());
                ParsedDateTime endTime = startTime.add(dur);
                end = endTime.getUtcTime();
            }
        } catch (ParseException pe) {
            throw new DavException("can't parse date", HttpServletResponse.SC_BAD_REQUEST, pe);
        }

        ZimbraLog.dav.debug("rcpt: " + rcpt + ", start: " + new Date(start) + ", end: " + new Date(end));

        FreeBusy fb = null;
        if (ctxt.isFreebusyEnabled()) {
            FreeBusyQuery fbQuery = new FreeBusyQuery(ctxt.getRequest(), ctxt.getAuthAccount(), start, end, null);
            fbQuery.addEmailAddress(getAddressFromPrincipalURL(rcpt), FreeBusyQuery.CALENDAR_FOLDER_ALL);
            java.util.Collection<FreeBusy> fbResult = fbQuery.getResults();
            if (fbResult.size() > 0)
                fb = fbResult.iterator().next();
        }
        if (fb != null) {
            String fbMsg = fb.toVCalendar(FreeBusy.Method.REPLY, originator, rcpt, null);
            resp.addElement(DavElements.E_RECIPIENT).addElement(DavElements.E_HREF).setText(rcpt);
            resp.addElement(DavElements.E_REQUEST_STATUS).setText("2.0;Success");
            resp.addElement(DavElements.E_CALENDAR_DATA).setText(fbMsg);
        } else {
            resp.addElement(DavElements.E_RECIPIENT).addElement(DavElements.E_HREF).setText(rcpt);
            resp.addElement(DavElements.E_REQUEST_STATUS).setText("5.3;No f/b for the user");
        }
    }

    private void handleEventRequest(DavContext ctxt, ZCalendar.ZVCalendar cal, ZComponent req,
            DelegationInfo delegationInfo, String rcpt, Element resp) throws ServiceException, DavException {
        if (!DavResource.isSchedulingEnabled()) {
            resp.addElement(DavElements.E_RECIPIENT).addElement(DavElements.E_HREF).setText(rcpt);
            resp.addElement(DavElements.E_REQUEST_STATUS).setText("5.3;No scheduling for the user");
            return;
        }

        ArrayList<Address> recipients = new java.util.ArrayList<Address>();
        InternetAddress from, sender, to;
        Account target = null;
        try {
            sender = new JavaMailInternetAddress(delegationInfo.getOriginatorEmail());
            Provisioning prov = Provisioning.getInstance();
            if (ctxt.getActingAsDelegateFor() != null) {
                target = prov.getAccountByName(ctxt.getActingAsDelegateFor());
            }
            if (target != null) {
                from = AccountUtil.getFriendlyEmailAddress(target);
            } else {
                if (delegationInfo.getOwnerEmail() != null) {
                    from = new JavaMailInternetAddress(delegationInfo.getOwnerEmail());
                } else {
                    target = getMailbox(ctxt).getAccount();
                    if (AccountUtil.addressMatchesAccount(target, delegationInfo.getOriginatorEmail())) {
                        // Make sure we don't use two different aliases for From and Sender.
                        // This is a concern with Apple iCal, which picks a random alias as originator.
                        from = sender;
                    } else {
                        from = AccountUtil.getFriendlyEmailAddress(target);
                    }
                }
            }
            if (sender.getAddress() != null && sender.getAddress().equalsIgnoreCase(from.getAddress())) {
                sender = null;
            }
            to = new JavaMailInternetAddress(CalDavUtils.stripMailto(rcpt));
            recipients.add(to);
        } catch (AddressException e) {
            resp.addElement(DavElements.E_RECIPIENT).addElement(DavElements.E_HREF).setText(rcpt);
            resp.addElement(DavElements.E_REQUEST_STATUS).setText("3.7;" + rcpt);
            return;
        }
        String status = req.getPropVal(ICalTok.STATUS, "");
        String method = cal.getPropVal(ICalTok.METHOD, "REQUEST");
        String subject = "";
        if (method.equals("REQUEST")) {
            ZProperty organizerProp = req.getProperty(ICalTok.ORGANIZER);
            if (organizerProp != null) {
                String organizerStr = this.getAddressFromPrincipalURL(new ZOrganizer(organizerProp).getAddress());
                if (!AccountUtil.addressMatchesAccount(getMailbox(ctxt).getAccount(), organizerStr)) {
                    ZimbraLog.dav.debug("scheduling appointment on behalf of %s", organizerStr);
                }
            }
        } else if (method.equals("REPLY")) {
            ZProperty attendeeProp = req.getProperty(ICalTok.ATTENDEE);
            if (attendeeProp == null)
                throw new DavException("missing property ATTENDEE", HttpServletResponse.SC_BAD_REQUEST);
            ZAttendee attendee = new ZAttendee(attendeeProp);
            String partStat = attendee.getPartStat();
            if (partStat.equals(IcalXmlStrMap.PARTSTAT_ACCEPTED)) {
                subject = "Accept: ";
            } else if (partStat.equals(IcalXmlStrMap.PARTSTAT_TENTATIVE)) {
                subject = "Tentative: ";
            } else if (partStat.equals(IcalXmlStrMap.PARTSTAT_DECLINED)) {
                subject = "Decline: ";
            }
        }

        if (status.equals("CANCELLED"))
            subject = "Cancelled: ";
        subject += req.getPropVal(ICalTok.SUMMARY, "");
        String uid = req.getPropVal(ICalTok.UID, null);
        if (uid == null) {
            resp.addElement(DavElements.E_RECIPIENT).addElement(DavElements.E_HREF).setText(rcpt);
            resp.addElement(DavElements.E_REQUEST_STATUS).setText("3.1;UID");
            return;
        }
        try {
            List<Invite> components = Invite.createFromCalendar(ctxt.getAuthAccount(), null, cal, false);
            FriendlyCalendaringDescription friendlyDesc = new FriendlyCalendaringDescription(components,
                    ctxt.getAuthAccount());
            String desc = friendlyDesc.getAsPlainText();
            String descHtml = req.getDescriptionHtml();
            if ((descHtml == null) || (descHtml.length() == 0))
                descHtml = friendlyDesc.getAsHtml();
            Mailbox mbox = MailboxManager.getInstance().getMailboxByAccount(ctxt.getAuthAccount());
            MimeMessage mm = CalendarMailSender.createCalendarMessage(target, from, sender, recipients, subject,
                    desc, descHtml, uid, cal);
            mbox.getMailSender().setSendPartial(true).sendMimeMessage(ctxt.getOperationContext(), mbox, true, mm,
                    null, null, null, null, false);
        } catch (ServiceException e) {
            resp.addElement(DavElements.E_RECIPIENT).addElement(DavElements.E_HREF).setText(rcpt);
            resp.addElement(DavElements.E_REQUEST_STATUS).setText("5.1");
            return;
        }
        resp.addElement(DavElements.E_RECIPIENT).addElement(DavElements.E_HREF).setText(rcpt);
        resp.addElement(DavElements.E_REQUEST_STATUS).setText("2.0;Success");
    }

    /*
     * to workaround the pre release iCal bugs
     */
    protected String getAddressFromPrincipalURL(String url) throws ServiceException, DavException {
        url = url.trim();
        if (url.startsWith("http://")) {
            // iCal sets the organizer field to be the URL of
            // CalDAV account.
            //     ORGANIZER:http://jylee-macbook:7070/service/dav/user1
            int pos = url.indexOf("/service/dav/");
            if (pos != -1) {
                int start = pos + 13;
                int end = url.indexOf("/", start);
                String userId = (end == -1) ? url.substring(start) : url.substring(start, end);
                Account organizer = Provisioning.getInstance().get(AccountBy.name, userId);
                if (organizer == null)
                    throw new DavException("user not found: " + userId, HttpServletResponse.SC_BAD_REQUEST, null);
                return organizer.getName();
            }
        } else if (url.toLowerCase().startsWith("mailto:")) {
            // iCal sometimes prefixes the email addr with more than one mailto:
            while (url.toLowerCase().startsWith("mailto:")) {
                url = url.substring(7);
            }
        }
        return url;
    }

    /**
     * For Vanilla CalDAV access where Apple style delegation has not been enabled, attempts by the delegate
     * to use a shared calendar acting as themselves are translated to appear as if acting as a delegate,
     * otherwise the experience can be very poor.
     * @throws ServiceException
     */
    private void adjustOrganizer(DavContext ctxt, ZCalendar.ZVCalendar cal, ZComponent req,
            DelegationInfo delegationInfo) throws ServiceException {
        // BusyCal 2.5.3 seems to post with the wrong organizer even if ical delegation is switched on - even though
        // it uses the right ORGANIZER in the calendar entry.
        // if (ctxt.useIcalDelegation()) { return; }
        Mailbox mbox = MailboxManager.getInstance().getMailboxByAccount(ctxt.getAuthAccount());
        String uid = req.getPropVal(ICalTok.UID, null);
        CalendarItem matchingCalendarEntry = mbox.getCalendarItemByUid(ctxt.getOperationContext(), uid);
        if (matchingCalendarEntry == null) {
            List<com.zimbra.cs.mailbox.Mountpoint> sharedCalendars = mbox
                    .getCalendarMountpoints(ctxt.getOperationContext(), SortBy.NONE);
            if (sharedCalendars == null) {
                return; // Can't work out anything useful
            }
            Set<Account> accts = Sets.newHashSet();
            for (com.zimbra.cs.mailbox.Mountpoint sharedCalendar : sharedCalendars) {
                accts.add(Provisioning.getInstance().get(AccountBy.id, sharedCalendar.getOwnerId()));
            }
            for (Account acct : accts) {
                Mailbox sbox = MailboxManager.getInstance().getMailboxByAccount(acct);
                matchingCalendarEntry = sbox.getCalendarItemByUid(ctxt.getOperationContext(), uid);
                if (matchingCalendarEntry != null) {
                    break;
                }
            }
        }
        if (matchingCalendarEntry == null) {
            return;
        }
        Invite[] invites = matchingCalendarEntry.getInvites();
        if (invites == null) {
            return;
        }
        for (Invite inv : invites) {
            ZOrganizer org = inv.getOrganizer();
            if (org != null) {
                delegationInfo.setOwner(org.getAddress());
                if (Strings.isNullOrEmpty(org.getCn())) {
                    Account ownerAcct = Provisioning.getInstance().get(AccountBy.name, org.getAddress());
                    if (!Strings.isNullOrEmpty(ownerAcct.getDisplayName())) {
                        delegationInfo.setOwnerCn(ownerAcct.getDisplayName());
                    }
                } else {
                    delegationInfo.setOwnerCn(org.getCn());
                }
                break;
            }
        }
        if (delegationInfo.getOwner() == null) {
            return;
        }
        AccountAddressMatcher acctMatcher = new AccountAddressMatcher(ctxt.getAuthAccount());
        boolean originatorIsCalEntryOrganizer = acctMatcher.matches(delegationInfo.getOwnerEmail());
        if (originatorIsCalEntryOrganizer) {
            return;
        }
        for (ZComponent component : cal.getComponents()) {
            ZProperty organizerProp = component.getProperty(ICalTok.ORGANIZER);
            if (organizerProp != null) {
                organizerProp.setValue(delegationInfo.getOwner());
                ZParameter cn = organizerProp.getParameter(ICalTok.CN);
                if (cn == null) {
                    organizerProp.addParameter(new ZParameter(ICalTok.CN, delegationInfo.getOwnerCn()));
                } else {
                    cn.setValue(delegationInfo.getOwnerCn());
                }
                ZParameter sentBy = organizerProp.getParameter(ICalTok.SENT_BY);
                if (sentBy == null) {
                    organizerProp.addParameter(new ZParameter(ICalTok.SENT_BY, delegationInfo.getOriginator()));
                } else {
                    sentBy.setValue(delegationInfo.getOriginator());
                }
            }
        }
    }

    private class DelegationInfo {
        /** The originator of this iTip request - who might be the organizer or an attendee */
        private String originator;
        private String originatorEmail;
        /** The organizer of the matching calendar entry if appropriate */
        private String owner;
        private String ownerEmail;
        private String ownerCn;

        DelegationInfo(String originator) {
            this.setOriginator(originator);
        }

        String getOriginator() {
            return originator;
        }

        void setOriginator(String originator) {
            this.originator = originator;
            this.originatorEmail = CalDavUtils.stripMailto(originator);
        }

        String getOriginatorEmail() {
            return originatorEmail;
        }

        String getOwner() {
            return owner;
        }

        void setOwner(String owner) {
            this.owner = owner;
            this.ownerEmail = CalDavUtils.stripMailto(owner);
        }

        String getOwnerEmail() {
            return ownerEmail;
        }

        String getOwnerCn() {
            return Strings.isNullOrEmpty(ownerCn) ? owner : ownerCn;
        }

        void setOwnerCn(String ownerCn) {
            this.ownerCn = ownerCn;
        }
    }

}