Search in sources :

Example 21 with RecurId

use of com.zimbra.cs.mailbox.calendar.RecurId in project zm-mailbox by Zimbra.

the class Appointment method processPartStat.

@Override
protected String processPartStat(Invite invite, MimeMessage mmInv, boolean forCreate, String defaultPartStat) throws ServiceException {
    Mailbox mbox = getMailbox();
    OperationContext octxt = mbox.getOperationContext();
    CreateCalendarItemPlayer player = octxt != null ? (CreateCalendarItemPlayer) octxt.getPlayer() : null;
    long opTime = octxt != null ? octxt.getTimestamp() : System.currentTimeMillis();
    Account account = getMailbox().getAccount();
    boolean onBehalfOf = false;
    Account authAcct = account;
    if (octxt != null) {
        Account authuser = octxt.getAuthenticatedUser();
        if (authuser != null) {
            onBehalfOf = !account.getId().equalsIgnoreCase(authuser.getId());
            if (onBehalfOf)
                authAcct = authuser;
        }
    }
    boolean asAdmin = octxt != null ? octxt.isUsingAdminPrivileges() : false;
    boolean allowPrivateAccess = allowPrivateAccess(authAcct, asAdmin);
    String partStat = defaultPartStat;
    if (player != null) {
        String p = player.getCalendarItemPartStat();
        if (p != null)
            partStat = p;
    }
    // See if we have RSVP=FALSE for the attendee.  Let's assume RSVP was requested unless it is
    // explicitly set to FALSE.
    boolean rsvpRequested = true;
    ZAttendee attendee = invite.getMatchingAttendee(account);
    if (attendee != null) {
        Boolean rsvp = attendee.getRsvp();
        if (rsvp != null)
            rsvpRequested = rsvp.booleanValue();
    }
    RedoLogProvider redoProvider = RedoLogProvider.getInstance();
    // Don't send reply emails if we're not on master (in redo-driven master/replica setup).
    // Don't send reply emails if we're replaying redo for reasons other than crash recovery.
    // In other words, we DO want to send emails during crash recovery, because we're completing
    // an interrupted transaction.  But we don't send emails during redo reply phase of restore.
    // Also don't send emails for cancel invites.  (Organizer doesn't expect reply for cancels.)
    // And don't send emails for task requests.
    // Don't send reply emails from a system account. (e.g. archiving, galsync, ham/spam)
    boolean needReplyEmail = rsvpRequested && redoProvider.isMaster() && (player == null || redoProvider.getRedoLogManager().getInCrashRecovery()) && invite.hasOrganizer() && !invite.isCancel() && !invite.isTodo() && !account.isIsSystemResource();
    if (invite.isOrganizer()) {
        // Organizer always accepts.
        partStat = IcalXmlStrMap.PARTSTAT_ACCEPTED;
    } else if (account instanceof CalendarResource && octxt == null) {
        // Auto accept/decline processing should only occur during email delivery.  In particular,
        // don't do it if we're here during ics import.  We're in email delivery if octxt == null.
        // (There needs to be a better way to determine that...)
        boolean replySent = false;
        CalendarResource resource = (CalendarResource) account;
        Locale lc;
        Account organizer = invite.getOrganizerAccount();
        if (organizer != null)
            lc = organizer.getLocale();
        else
            lc = resource.getLocale();
        if (resource.autoAcceptDecline() || resource.autoDeclineIfBusy() || resource.autoDeclineRecurring()) {
            boolean replyListUpdated = false;
            // If auto-accept is enabled, assume it'll be accepted until it gets declined.
            if (resource.autoAcceptDecline())
                partStat = IcalXmlStrMap.PARTSTAT_ACCEPTED;
            if (isRecurring() && resource.autoDeclineRecurring()) {
                // Decline because resource is configured to decline all recurring appointments.
                partStat = IcalXmlStrMap.PARTSTAT_DECLINED;
                if (needReplyEmail) {
                    String reason = L10nUtil.getMessage(MsgKey.calendarResourceDeclineReasonRecurring, lc);
                    Invite replyInv = makeReplyInvite(account, authAcct, lc, onBehalfOf, allowPrivateAccess, invite, invite.getRecurId(), CalendarMailSender.VERB_DECLINE);
                    CalendarMailSender.sendResourceAutoReply(octxt, mbox, true, CalendarMailSender.VERB_DECLINE, false, reason + "\r\n", this, invite, new Invite[] { replyInv }, mmInv);
                    replySent = true;
                }
            } else if (resource.autoDeclineIfBusy()) {
                // Auto decline is enabled.  Let's check for conflicts.
                int maxNumConflicts = resource.getMaxNumConflictsAllowed();
                int maxPctConflicts = resource.getMaxPercentConflictsAllowed();
                ConflictCheckResult checkResult = checkAvailability(opTime, invite, maxNumConflicts, maxPctConflicts);
                if (checkResult != null) {
                    List<Conflict> conflicts = checkResult.getConflicts();
                    if (conflicts.size() > 0) {
                        if (invite.isRecurrence() && !checkResult.tooManyConflicts()) {
                            // There are some conflicts, but within resource's allowed limit.
                            if (resource.autoAcceptDecline()) {
                                // Let's accept partially.  (Accept the series and decline conflicting instances.)
                                List<Invite> replyInvites = new ArrayList<Invite>();
                                // the REPLY for the ACCEPT of recurrence series
                                Invite acceptInv = makeReplyInvite(account, authAcct, lc, onBehalfOf, allowPrivateAccess, invite, invite.getRecurId(), CalendarMailSender.VERB_ACCEPT);
                                for (Conflict conflict : conflicts) {
                                    Instance inst = conflict.getInstance();
                                    InviteInfo invInfo = inst.getInviteInfo();
                                    Invite inv = getInvite(invInfo.getMsgId(), invInfo.getComponentId());
                                    RecurId rid = inst.makeRecurId(inv);
                                    // Record the decline status in reply list.
                                    getReplyList().modifyPartStat(resource, rid, null, resource.getName(), null, null, IcalXmlStrMap.PARTSTAT_DECLINED, false, invite.getSeqNo(), opTime);
                                    replyListUpdated = true;
                                    // Make REPLY VEVENT for the declined instance.
                                    Invite replyInv = makeReplyInvite(account, authAcct, lc, onBehalfOf, allowPrivateAccess, inv, rid, CalendarMailSender.VERB_DECLINE);
                                    replyInvites.add(replyInv);
                                }
                                if (needReplyEmail) {
                                    ICalTimeZone tz = chooseReplyTZ(invite);
                                    // Send one email to accept the series.
                                    String declinedInstances = getDeclinedTimesString(octxt, mbox, conflicts, invite.isAllDayEvent(), tz, lc);
                                    String msg = L10nUtil.getMessage(MsgKey.calendarResourceDeclinedInstances, lc) + "\r\n\r\n" + declinedInstances;
                                    CalendarMailSender.sendResourceAutoReply(octxt, mbox, true, CalendarMailSender.VERB_ACCEPT, true, msg, this, invite, new Invite[] { acceptInv }, mmInv);
                                    // Send another email to decline instances, all in one email.
                                    String conflictingTimes = getBusyTimesString(octxt, mbox, conflicts, tz, lc, false);
                                    msg = L10nUtil.getMessage(MsgKey.calendarResourceDeclinedInstances, lc) + "\r\n\r\n" + declinedInstances + "\r\n" + L10nUtil.getMessage(MsgKey.calendarResourceDeclineReasonConflict, lc) + "\r\n\r\n" + conflictingTimes;
                                    CalendarMailSender.sendResourceAutoReply(octxt, mbox, true, CalendarMailSender.VERB_DECLINE, true, msg, this, invite, replyInvites.toArray(new Invite[0]), mmInv);
                                    replySent = true;
                                }
                            } else {
                            // Auto-accept is not enabled.  Auto-decline is enabled, but there weren't
                            // enough conflicting instances to decline outright.  So we just stay
                            // silent and let the human admin deal with it.  This case is rather
                            // ambiguous, and can be avoided by configuring the resource to allow
                            // zero conflicting instance.
                            }
                        } else {
                            // Too many conflicts.  Decline outright.
                            partStat = IcalXmlStrMap.PARTSTAT_DECLINED;
                            if (needReplyEmail) {
                                ICalTimeZone tz = chooseReplyTZ(invite);
                                String msg = L10nUtil.getMessage(MsgKey.calendarResourceDeclineReasonConflict, lc) + "\r\n\r\n" + getBusyTimesString(octxt, mbox, conflicts, tz, lc, checkResult.hasMoreConflicts());
                                Invite replyInv = makeReplyInvite(account, authAcct, lc, onBehalfOf, allowPrivateAccess, invite, invite.getRecurId(), CalendarMailSender.VERB_DECLINE);
                                CalendarMailSender.sendResourceAutoReply(octxt, mbox, true, CalendarMailSender.VERB_DECLINE, false, msg, this, invite, new Invite[] { replyInv }, mmInv);
                                replySent = true;
                            }
                        }
                    }
                }
            }
            if (!replySent && IcalXmlStrMap.PARTSTAT_ACCEPTED.equals(partStat)) {
                if (needReplyEmail) {
                    Invite replyInv = makeReplyInvite(account, authAcct, lc, onBehalfOf, allowPrivateAccess, invite, invite.getRecurId(), CalendarMailSender.VERB_ACCEPT);
                    CalendarMailSender.sendResourceAutoReply(octxt, mbox, true, CalendarMailSender.VERB_ACCEPT, false, null, this, invite, new Invite[] { replyInv }, mmInv);
                }
            }
            // Record the final outcome in the replies list.
            if (IcalXmlStrMap.PARTSTAT_NEEDS_ACTION.equals(partStat)) {
                getReplyList().modifyPartStat(resource, invite.getRecurId(), null, resource.getName(), null, null, partStat, false, invite.getSeqNo(), opTime);
                replyListUpdated = true;
            }
            if (forCreate && replyListUpdated)
                saveMetadata();
        }
    }
    CreateCalendarItemRecorder recorder = (CreateCalendarItemRecorder) mbox.getRedoRecorder();
    recorder.setCalendarItemPartStat(partStat);
    invite.updateMyPartStat(account, partStat);
    if (forCreate) {
        Invite defaultInvite = getDefaultInviteOrNull();
        if (defaultInvite != null && !defaultInvite.equals(invite) && !partStat.equals(defaultInvite.getPartStat())) {
            defaultInvite.updateMyPartStat(account, partStat);
            saveMetadata();
        }
    }
    return partStat;
}
Also used : Locale(java.util.Locale) Account(com.zimbra.cs.account.Account) InviteInfo(com.zimbra.cs.mailbox.calendar.InviteInfo) FBInstance(com.zimbra.cs.fb.FreeBusy.FBInstance) ArrayList(java.util.ArrayList) RecurId(com.zimbra.cs.mailbox.calendar.RecurId) CreateCalendarItemPlayer(com.zimbra.cs.redolog.op.CreateCalendarItemPlayer) CreateCalendarItemRecorder(com.zimbra.cs.redolog.op.CreateCalendarItemRecorder) RedoLogProvider(com.zimbra.cs.redolog.RedoLogProvider) ZAttendee(com.zimbra.cs.mailbox.calendar.ZAttendee) CalendarResource(com.zimbra.cs.account.CalendarResource) Invite(com.zimbra.cs.mailbox.calendar.Invite) ICalTimeZone(com.zimbra.common.calendar.ICalTimeZone)

Example 22 with RecurId

use of com.zimbra.cs.mailbox.calendar.RecurId in project zm-mailbox by Zimbra.

the class CalendarItem method recurrenceIdsMatch.

/**
     * Returns true if RECURRENCE-ID of two invites are equal or null.
     * @param inv1
     * @param inv2
     * @return
     */
private static boolean recurrenceIdsMatch(Invite inv1, Invite inv2) {
    RecurId r1 = inv1.getRecurId();
    RecurId r2 = inv2.getRecurId();
    if (r1 != null)
        return r1.equals(r2);
    else
        return r2 == null;
}
Also used : RecurId(com.zimbra.cs.mailbox.calendar.RecurId)

Example 23 with RecurId

use of com.zimbra.cs.mailbox.calendar.RecurId in project zm-mailbox by Zimbra.

the class CalendarItem method processNewInviteRequestOrCancel.

/**
     *
     * @param pm
     * @param newInvite
     * @param folderId
     * @param nextAlarm
     * @param preserveAlarms
     * @param discardExistingInvites
     * @param batch - if true this call will not update the recurrence and may not persist to the data.
     *                The caller needs to persist the data by calling setContent().
     * @return
     * @throws ServiceException
     */
private boolean processNewInviteRequestOrCancel(ParsedMessage pm, Invite newInvite, int folderId, long nextAlarm, boolean preserveAlarms, boolean discardExistingInvites, boolean batch) throws ServiceException {
    // trace logging
    if (!newInvite.hasRecurId())
        ZimbraLog.calendar.info("Modifying CalendarItem: id=%d, folderId=%d, method=%s, subject=\"%s\", UID=%s", mId, getFolderId(), newInvite.getMethod(), newInvite.isPublic() ? newInvite.getName() : "(private)", mUid);
    else
        ZimbraLog.calendar.info("Modifying CalendarItem: id=%d, folderId=%d, method=%s, subject=\"%s\", UID=%s, recurId=%s", mId, getFolderId(), newInvite.getMethod(), newInvite.isPublic() ? newInvite.getName() : "(private)", mUid, newInvite.getRecurId().getDtZ());
    newInvite.sanitize(false);
    OperationContext octxt = getMailbox().getOperationContext();
    Account authAccount = octxt != null ? octxt.getAuthenticatedUser() : null;
    boolean asAdmin = octxt != null ? octxt.isUsingAdminPrivileges() : false;
    boolean isCancel = newInvite.isCancel();
    boolean requirePrivateCheck = requirePrivateCheck(newInvite);
    short rightsNeeded = isCancel ? (short) (ACL.RIGHT_DELETE | ACL.RIGHT_WRITE) : ACL.RIGHT_WRITE;
    if (!canAccess(rightsNeeded, authAccount, asAdmin, requirePrivateCheck))
        throw ServiceException.PERM_DENIED("you do not have sufficient permissions on this calendar item");
    // Don't allow moving a private appointment on behalf of another user,
    // unless that other user is a calendar resource.
    boolean isCalendarResource = getMailbox().getAccount() instanceof CalendarResource;
    boolean denyPrivateAccess = requirePrivateCheck ? !allowPrivateAccess(authAccount, asAdmin) : false;
    if (!newInvite.isPublic() || !isPublic()) {
        if (folderId != getFolderId()) {
            Folder folder = getMailbox().getFolderById(folderId);
            if (!allowPrivateAccess(folder, authAccount, asAdmin)) {
                denyPrivateAccess = true;
                if (!isCalendarResource)
                    throw ServiceException.PERM_DENIED("you do not have permission to update/cancel private calendar item in target folder");
            }
        }
    }
    // Do not allow organizer to be changed. (bug 74400)
    boolean organizerChanged = organizerChangeCheck(newInvite, true);
    ZOrganizer newOrganizer = newInvite.getOrganizer();
    // of the invite.
    if (isCancel) {
        boolean cancelAll;
        boolean outdated;
        if (!newInvite.hasRecurId()) {
            cancelAll = true;
            // Canceling series.  Check the sequencing requirement to make sure the invite isn't outdated.
            Invite series = getInvite((RecurId) null);
            // If series invite is not found, assume cancel is not outdated.
            outdated = series != null && !newInvite.isSameOrNewerVersion(series);
        } else {
            // Canceling an instance.  It's a total cancel only if mInvites has one invite and it matches
            // the recurrence id.  (subject to sequencing requirements)
            cancelAll = false;
            outdated = false;
            Invite curr = getInvite(newInvite.getRecurId());
            if (curr != null) {
                if (newInvite.isSameOrNewerVersion(curr)) {
                    cancelAll = true;
                    // See if there any non-cancel invites besides the one being canceled.
                    for (Invite inv : mInvites) {
                        if (!inv.equals(curr) && !inv.isCancel()) {
                            cancelAll = false;
                            break;
                        }
                    }
                } else {
                    // There is already a newer invite.  Ignore the cancel.
                    outdated = true;
                }
            }
        }
        if (outdated) {
            ZimbraLog.calendar.info("Ignoring outdated cancel request");
            return false;
        }
        if (cancelAll) {
            Folder trash = mMailbox.getFolderById(Mailbox.ID_FOLDER_TRASH);
            move(trash);
            // If we have revisions enabled we need to force metadata write to db because version field changed.
            if (getMaxRevisions() != 1)
                saveMetadata();
            return true;
        }
    }
    // Clear all replies if replacing appointment in trash folder with a new invite.  All existing invites are
    // being discarded, and so all existing replies must be discarded as well.
    Folder folder = getMailbox().getFolderById(folderId);
    if (!isCancel && discardExistingInvites && inTrash() && !folder.inTrash()) {
        mReplyList.mReplies.clear();
    }
    // Handle change to the series that involves time and/or recurrence.  In Exchange compatibility mode,
    // time/recurrence change blows away all exception instances.  In non-compat mode (old ZCS behavior),
    // look for change in the start time and shift the time part of exceptions' RECURRENCE-ID by the same delta.
    boolean needRecurrenceIdUpdate = false;
    ParsedDateTime oldDtStart = null;
    ParsedDuration dtStartMovedBy = null;
    ArrayList<Invite> toUpdate = new ArrayList<Invite>();
    if (!discardExistingInvites && !isCancel && newInvite.isRecurrence()) {
        Invite defInv = getDefaultInviteOrNull();
        if (defInv != null && defInv.isRecurrence()) {
            if (!getAccount().isCalendarKeepExceptionsOnSeriesTimeChange()) {
                // Exchange compatibility mode
                InviteChanges ic = new InviteChanges(defInv, newInvite);
                if (ic.isExceptionRemovingChange()) {
                    discardExistingInvites = true;
                }
            } else {
                // old ZCS behavior
                // Be careful.  If invites got delivered out of order, we may have defInv that's not
                // a series.  Imagine 1st invite received was an exception and 2nd was the series.
                // In that situation we simply skip the DTSTART shift calculation.
                oldDtStart = defInv.getStartTime();
                ParsedDateTime newDtStart = newInvite.getStartTime();
                //if (newDtStart != null && oldDtStart != null && !newDtStart.sameTime(oldDtStart))
                if (newDtStart != null && oldDtStart != null && !newDtStart.equals(oldDtStart)) {
                    // Find the series frequency.
                    Frequency freq = null;
                    IRecurrence recurrence = newInvite.getRecurrence();
                    if (recurrence != null) {
                        Iterator rulesIter = recurrence.addRulesIterator();
                        if (rulesIter != null) {
                            for (; rulesIter.hasNext(); ) {
                                Object ruleObj = rulesIter.next();
                                if (ruleObj instanceof SimpleRepeatingRule) {
                                    SimpleRepeatingRule series = (SimpleRepeatingRule) ruleObj;
                                    ZRecur recur = series.getRule();
                                    freq = recur.getFrequency();
                                    break;
                                }
                            }
                        }
                    }
                    // Maximum allowed delta depends on the frequency.
                    ParsedDuration deltaLimit = null;
                    if (freq != null) {
                        switch(freq) {
                            case DAILY:
                                deltaLimit = ParsedDuration.ONE_DAY;
                                break;
                            case WEEKLY:
                            case MONTHLY:
                            case YEARLY:
                                // Do the RECURRENCE-ID adjustment only when DTSTART moved by 7 days or less.
                                // If it moved by more, it gets too complicated to figure out what the old RECURRENCE-ID
                                // should be in the new series.  Just blow away all exceptions.
                                deltaLimit = ParsedDuration.ONE_WEEK;
                                break;
                            default:
                                // Secondly/minutely/hourly rules are too frequent to allow recurrence id shifting.
                                break;
                        }
                    }
                    if (deltaLimit != null) {
                        ParsedDuration delta = newDtStart.difference(oldDtStart);
                        if (delta.abs().compareTo(deltaLimit) < 0) {
                            needRecurrenceIdUpdate = true;
                            dtStartMovedBy = delta;
                        }
                    }
                }
            }
        }
    }
    // found, inherit from the series invite.
    if (!discardExistingInvites && preserveAlarms) {
        Invite localSeries = null;
        Invite alarmSourceInv = null;
        for (Invite inv : mInvites) {
            if (recurrenceIdsMatch(inv, newInvite)) {
                alarmSourceInv = inv;
                break;
            }
            if (!inv.hasRecurId())
                localSeries = inv;
        }
        if (alarmSourceInv == null)
            alarmSourceInv = localSeries;
        if (alarmSourceInv != null) {
            newInvite.clearAlarms();
            for (Iterator<Alarm> alarmIter = alarmSourceInv.alarmsIterator(); alarmIter.hasNext(); ) {
                newInvite.addAlarm(alarmIter.next());
            }
        }
    }
    // Is this a series update invite from ZCO?  If so, we have to treat all exceptions as local-only
    // and make them snap to series.
    boolean zcoSeriesUpdate = false;
    ZProperty xzDiscardExcepts = newInvite.getXProperty(ICalTok.X_ZIMBRA_DISCARD_EXCEPTIONS.toString());
    if (xzDiscardExcepts != null)
        zcoSeriesUpdate = xzDiscardExcepts.getBoolValue();
    // Is this an update to the series with UNTIL in the rule?  If so, we need to remove exceptions
    // whose RECURRENCE-ID come later than UNTIL. (bug 11870)
    long seriesUntil = Long.MAX_VALUE;
    if (!isCancel && !newInvite.hasRecurId()) {
        ParsedDateTime dtStart = newInvite.getStartTime();
        IRecurrence recur = newInvite.getRecurrence();
        if (recur != null && dtStart != null) {
            ICalTimeZone tz = dtStart.getTimeZone();
            // Find the repeating rule.
            Iterator<?> iter = recur.addRulesIterator();
            if (iter != null) {
                for (; iter.hasNext(); ) {
                    IRecurrence cur = (IRecurrence) iter.next();
                    if (cur.getType() == Recurrence.TYPE_REPEATING) {
                        ZRecur rrule = ((Recurrence.SimpleRepeatingRule) cur).getRule();
                        ParsedDateTime until = rrule.getUntil();
                        if (until != null)
                            seriesUntil = Math.min(until.getDateForRecurUntil(tz).getTime(), seriesUntil);
                    }
                }
            }
        }
    }
    // Check if exception instances are made obsolete by updated recurrence rule.  (bug 47061)
    Set<String> obsoletedRecurIdZs = new HashSet<String>();
    if (!isCancel && newInvite.isRecurrence()) {
        Invite seriesInv = null;
        // Find the range of existing exception instances.
        long rangeStart = Long.MAX_VALUE;
        long rangeEnd = Long.MIN_VALUE;
        for (Invite inv : mInvites) {
            if (inv.hasRecurId()) {
                RecurId rid = inv.getRecurId();
                ParsedDateTime ridDt = rid.getDt();
                if (ridDt != null) {
                    // Turn Outlook-style all-day RecurId to standard-style.
                    if (inv.isAllDayEvent() && ridDt.hasTime() && ridDt.hasZeroTime()) {
                        ParsedDateTime ridDtFixed = (ParsedDateTime) ridDt.clone();
                        ridDtFixed.setHasTime(false);
                        rid = new RecurId(ridDtFixed, rid.getRange());
                        ridDt = rid.getDt();
                    }
                    // Adjust start time if necessary.
                    RecurId adjustedRid;
                    long adjustedT;
                    if (dtStartMovedBy != null) {
                        ParsedDateTime dt = ridDt.add(dtStartMovedBy);
                        adjustedRid = new RecurId(dt, rid.getRange());
                        adjustedT = dt.getUtcTime();
                    } else {
                        adjustedRid = rid;
                        adjustedT = ridDt.getUtcTime();
                    }
                    rangeStart = Math.min(rangeStart, adjustedT);
                    rangeEnd = Math.max(rangeEnd, adjustedT);
                    // initially all instances considered obsolete
                    obsoletedRecurIdZs.add(adjustedRid.getDtZ());
                }
            } else {
                seriesInv = inv;
            }
        }
        // Extend the range by a day on both ends to compensate for all-day appointments.
        // 25 hours to accommodate DST onset dates
        long millisIn25Hours = 25 * 60 * 60 * 1000;
        if (rangeStart != Long.MAX_VALUE)
            rangeStart -= millisIn25Hours;
        if (rangeEnd != Long.MIN_VALUE)
            rangeEnd += millisIn25Hours;
        if (rangeStart != Long.MAX_VALUE && rangeEnd != Long.MIN_VALUE && rangeStart <= rangeEnd) {
            // so the final instance is included in the range
            ++rangeEnd;
            IRecurrence recur = newInvite.getRecurrence();
            if (recur instanceof RecurrenceRule) {
                RecurrenceRule rrule = (RecurrenceRule) recur;
                List<Instance> instances = rrule.expandInstances(getId(), rangeStart, rangeEnd);
                if (instances != null) {
                    for (Instance inst : instances) {
                        Invite refInv = seriesInv != null ? seriesInv : newInvite;
                        RecurId rid = inst.makeRecurId(refInv);
                        // Turn Outlook-style all-day RecurId to standard-style.
                        if (refInv.isAllDayEvent() && rid.getDt() != null) {
                            ParsedDateTime ridDtFixed = (ParsedDateTime) rid.getDt().clone();
                            ridDtFixed.setHasTime(false);
                            rid = new RecurId(ridDtFixed, rid.getRange());
                        }
                        // "Un-obsolete" the surviving recurrence ids.
                        obsoletedRecurIdZs.remove(rid.getDtZ());
                    }
                }
            } else if (recur != null) {
                // This shouldn't happen.
                ZimbraLog.calendar.warn("Expected RecurrenceRule object, but got " + recur.getClass().getName());
            }
        }
    }
    boolean addNewOne = true;
    boolean replaceExceptionBodyWithSeriesBody = false;
    boolean modifiedCalItem = false;
    // the invite which has been made obsolete by the new one coming in
    Invite prev = null;
    // Invites to remove from our blob store
    ArrayList<Invite> toRemove = new ArrayList<Invite>();
    // indexes to remove from mInvites
    ArrayList<Integer> idxsToRemove = new ArrayList<Integer>();
    // get current size because we may add to the list in the loop
    int numInvitesCurrent = mInvites.size();
    for (int i = 0; i < numInvitesCurrent; i++) {
        Invite cur = mInvites.get(i);
        // If request is a cancellation of entire appointment, simply add each invite to removal list.
        if (isCancel && !newInvite.hasRecurId()) {
            addNewOne = false;
            modifiedCalItem = true;
            toRemove.add(cur);
            idxsToRemove.add(0, i);
            continue;
        }
        // Use DTSTART for comparison rather than RECURRENCE-ID.
        if (!isCancel && cur.hasRecurId()) {
            ParsedDateTime instDtStart = cur.getStartTime();
            if (instDtStart != null && instDtStart.getUtcTime() > seriesUntil) {
                modifiedCalItem = true;
                toRemove.add(cur);
                idxsToRemove.add(0, i);
                continue;
            }
        }
        // Remove exceptions obsoleted by changed RRULE.  (bug 47061)
        if (cur.hasRecurId() && !obsoletedRecurIdZs.isEmpty()) {
            RecurId rid = cur.getRecurId();
            if (rid != null && rid.getDt() != null) {
                // Turn Outlook-style all-day RecurId to standard-style.
                ParsedDateTime ridDt = rid.getDt();
                if (cur.isAllDayEvent() && ridDt.hasTime() && ridDt.hasZeroTime()) {
                    ParsedDateTime ridDtFixed = (ParsedDateTime) ridDt.clone();
                    ridDtFixed.setHasTime(false);
                    rid = new RecurId(ridDtFixed, rid.getRange());
                }
                // Adjust start time if necessary.
                RecurId adjustedRid;
                if (dtStartMovedBy != null) {
                    ParsedDateTime dt = rid.getDt().add(dtStartMovedBy);
                    adjustedRid = new RecurId(dt, rid.getRange());
                } else {
                    adjustedRid = rid;
                }
                if (obsoletedRecurIdZs.contains(adjustedRid.getDtZ())) {
                    modifiedCalItem = true;
                    toRemove.add(cur);
                    idxsToRemove.add(0, i);
                    continue;
                }
            }
        }
        boolean matchingRecurId = recurrenceIdsMatch(cur, newInvite);
        if (discardExistingInvites || matchingRecurId) {
            if (discardExistingInvites || newInvite.isSameOrNewerVersion(cur)) {
                // Invite is local-only only if both old and new are local-only.
                newInvite.setLocalOnly(cur.isLocalOnly() && newInvite.isLocalOnly());
                toRemove.add(cur);
                // add to FRONT of list, so when we iterate for the removals we go from HIGHER TO LOWER
                // that way the numbers all match up as the list contracts!
                idxsToRemove.add(0, Integer.valueOf(i));
                boolean invalidateReplies = false;
                if (!discardExistingInvites) {
                    InviteChanges invChg = new InviteChanges(cur, newInvite);
                    invalidateReplies = invChg.isReplyInvalidatingChange();
                }
                if (discardExistingInvites || invalidateReplies) {
                    // clean up any old REPLYs that have been made obsolete by this new invite
                    mReplyList.removeObsoleteEntries(newInvite.getRecurId(), newInvite.getSeqNo(), newInvite.getDTStamp());
                } else {
                    // If the change is minor, don't discard earlier replies.  Organizer may have incremented the
                    // sequence unnecessarily, and we have to cope with this by bumping up the sequence in the
                    // replies accordingly.
                    mReplyList.upgradeEntriesToNewSeq(newInvite.getRecurId(), newInvite.getSeqNo(), newInvite.getDTStamp());
                }
                prev = cur;
                modifiedCalItem = true;
                if (isCancel && !newInvite.hasRecurId()) {
                    // can't CANCEL just the recurId=null entry -- we must delete the whole appointment
                    addNewOne = false;
                }
            } else {
                // perhaps delivered out of order.  Ignore it.
                return false;
            }
        } else if (!isCancel) {
            modifiedCalItem = true;
            boolean addToUpdateList = false;
            if (organizerChanged) {
                // If organizer is changing on any invite, change it on all invites.
                cur.setOrganizer(newOrganizer);
                addToUpdateList = true;
            }
            if (needRecurrenceIdUpdate) {
                // Adjust RECURRENCE-ID by the delta in series DTSTART, if recurrence id value has the
                // same time of day as old DTSTART.
                RecurId rid = cur.getRecurId();
                if (rid != null && rid.getDt() != null && oldDtStart != null) {
                    ParsedDateTime ridDt = rid.getDt();
                    if (ridDt.sameTime(oldDtStart)) {
                        ParsedDateTime dt = rid.getDt().add(dtStartMovedBy);
                        RecurId newRid = new RecurId(dt, rid.getRange());
                        cur.setRecurId(newRid);
                        // used in RECURRENCE-ID and adjust DTEND accordingly.
                        if (cur.isCancel()) {
                            cur.setDtStart(dt);
                            ParsedDateTime dtEnd = cur.getEndTime();
                            if (dtEnd != null) {
                                ParsedDateTime dtEndMoved = dtEnd.add(dtStartMovedBy);
                                cur.setDtEnd(dtEndMoved);
                            }
                        }
                        addToUpdateList = true;
                    }
                }
            }
            // organizer) are left alone.
            if (!newInvite.hasRecurId() && cur.hasRecurId() && (zcoSeriesUpdate || cur.isLocalOnly())) {
                if (cur.isCancel()) {
                    // Local-only cancellations are undone by update to the series.
                    toRemove.add(cur);
                    // add to FRONT of list, so when we iterate for the removals we go from HIGHER TO LOWER
                    // that way the numbers all match up as the list contracts!
                    idxsToRemove.add(0, Integer.valueOf(i));
                    // clean up any old REPLYs that have been made obsolete by this new invite
                    mReplyList.removeObsoleteEntries(newInvite.getRecurId(), newInvite.getSeqNo(), newInvite.getDTStamp());
                    addToUpdateList = false;
                } else {
                    replaceExceptionBodyWithSeriesBody = true;
                    // Recreate invite with data from newInvite, but preserve alarm info.
                    Invite copy = newInvite.newCopy();
                    // It's still local-only.
                    copy.setLocalOnly(true);
                    copy.setMailItemId(cur.getMailItemId());
                    copy.setComponentNum(cur.getComponentNum());
                    copy.setSeqNo(cur.getSeqNo());
                    copy.setDtStamp(cur.getDTStamp());
                    copy.setRecurId(cur.getRecurId());
                    // because we're only dealing with exceptions
                    copy.setRecurrence(null);
                    ParsedDateTime start = cur.getRecurId().getDt();
                    if (start != null) {
                        // snap back to series start time
                        copy.setDtStart(start);
                        ParsedDuration dur = cur.getDuration();
                        if (dur != null) {
                            copy.setDtEnd(null);
                            copy.setDuration(dur);
                        } else {
                            copy.setDuration(null);
                            dur = cur.getEffectiveDuration();
                            ParsedDateTime end = null;
                            if (dur != null)
                                end = start.add(dur);
                            copy.setDtEnd(end);
                        }
                    } else {
                        copy.setDtStart(null);
                        copy.setDtEnd(cur.getEndTime());
                        copy.setDuration(null);
                    }
                    copy.clearAlarms();
                    for (Iterator<Alarm> iter = cur.alarmsIterator(); iter.hasNext(); ) {
                        copy.addAlarm(iter.next());
                    }
                    // Series was updated, so change this exception's partstat to NEEDS-ACTION.
                    ZAttendee me = copy.getMatchingAttendee(getAccount());
                    if (me != null)
                        me.setPartStat(IcalXmlStrMap.PARTSTAT_NEEDS_ACTION);
                    mInvites.set(i, copy);
                    addToUpdateList = true;
                }
            }
            if (addToUpdateList)
                toUpdate.add(cur);
        }
    }
    boolean callProcessPartStat = false;
    if (addNewOne) {
        newInvite.setCalendarItem(this);
        // unless that other user is a calendar resource.
        if (denyPrivateAccess && prev != null && !prev.isPublic() && !isCalendarResource)
            throw ServiceException.PERM_DENIED("you do not have sufficient permissions on this calendar item");
        if (prev != null && !newInvite.isOrganizer() && newInvite.sentByMe()) {
            // A non-organizer attendee is modifying data on his/her
            // appointment/task.  Any information that is tracked in
            // metadata rather than in the iCal MIME part must be
            // carried over from the last invite to the new one.
            newInvite.setPartStat(prev.getPartStat());
            newInvite.setRsvp(prev.getRsvp());
            newInvite.getCalendarItem().saveMetadata();
        // No need to mark invite as modified item in mailbox as
        // it has already been marked as a created item.
        } else {
            callProcessPartStat = true;
        }
        newInvite.setClassPropSetByMe(newInvite.sentByMe());
        // retain the value and therefore don't allow the organizer to override it.
        if (prev != null && !newInvite.isOrganizer() && !newInvite.sentByMe()) {
            if (!prev.isPublic() && prev.classPropSetByMe()) {
                newInvite.setClassProp(prev.getClassProp());
                newInvite.setClassPropSetByMe(true);
            }
        }
        mInvites.add(newInvite);
        // the appointment/task stores an uber-tzmap, for its uber-recurrence
        // this might give us problems if we had two invites with conflicting TZ
        // defs....should be very unlikely
        mTzMap.add(newInvite.getTimeZoneMap());
        // TIM: don't write the blob until the end of the function (so we only do one write for the update)
        //            modifyBlob(toRemove, replaceExistingInvites, toUpdate, pm, newInvite, locator, isCancel, !denyPrivateAccess);
        modifiedCalItem = true;
    } else {
    // TIM: don't write the blob until the end of the function (so we only do one write for the update)
    //            modifyBlob(toRemove, replaceExistingInvites, toUpdate, null, null, locator, isCancel, !denyPrivateAccess);
    }
    // now remove the inviteid's from our list
    for (Iterator<Integer> iter = idxsToRemove.iterator(); iter.hasNext(); ) {
        assert (modifiedCalItem);
        Integer i = iter.next();
        mInvites.remove(i.intValue());
    }
    // Check if there are any surviving non-cancel invites after applying the update.
    // Also check for changes in flags.
    int oldFlags = mData.getFlags();
    int newFlags = mData.getFlags() & ~(Flag.BITMASK_ATTACHED | Flag.BITMASK_DRAFT | Flag.BITMASK_HIGH_PRIORITY | Flag.BITMASK_LOW_PRIORITY);
    boolean hasSurvivingRequests = false;
    for (Invite cur : mInvites) {
        String method = cur.getMethod();
        if (method.equals(ICalTok.REQUEST.toString()) || method.equals(ICalTok.PUBLISH.toString())) {
            hasSurvivingRequests = true;
            if (cur.hasAttachment())
                newFlags |= Flag.BITMASK_ATTACHED;
            if (cur.isDraft())
                newFlags |= Flag.BITMASK_DRAFT;
            if (cur.isHighPriority())
                newFlags |= Flag.BITMASK_HIGH_PRIORITY;
            if (cur.isLowPriority())
                newFlags |= Flag.BITMASK_LOW_PRIORITY;
        }
    }
    if (newFlags != oldFlags) {
        mData.setFlags(newFlags);
        modifiedCalItem = true;
    }
    if (!hasSurvivingRequests) {
        if (!isCancel)
            ZimbraLog.calendar.warn("Invalid state: deleting calendar item " + getId() + " in mailbox " + getMailboxId() + " while processing a non-cancel request");
        else
            ZimbraLog.calendar.warn("Invalid state: deleting calendar item " + getId() + " in mailbox " + getMailboxId() + " because it has no invite after applying cancel invite");
        // delete this appointment/task from the table,
        delete();
        // it doesn't have anymore REQUESTs!
        return false;
    } else {
        if (nextAlarm > 0 && mAlarmData != null && mAlarmData.getNextAtBase() != nextAlarm)
            modifiedCalItem = true;
        if (modifiedCalItem) {
            if (!batch && !updateRecurrence(nextAlarm)) {
                // no default invite!  This appointment/task no longer valid
                ZimbraLog.calendar.warn("Invalid state: deleting calendar item " + getId() + " in mailbox " + getMailboxId() + " because it has no invite");
                delete();
                return false;
            } else {
                if (callProcessPartStat) {
                    // processPartStat() must be called after
                    // updateRecurrence() has been called.  (bug 8072)
                    processPartStat(newInvite, pm != null ? pm.getMimeMessage() : null, false, newInvite.getPartStat());
                }
                if (getFolderId() != folderId) {
                    // Move appointment/task to a different folder.
                    move(folder);
                }
                // Did the appointment have a blob before the change?
                boolean hadBlobPart = false;
                Invite[] oldInvs = getInvites();
                if (oldInvs != null) {
                    for (Invite oldInv : oldInvs) {
                        if (oldInv.hasBlobPart()) {
                            hadBlobPart = true;
                            break;
                        }
                    }
                }
                // Update blob if adding a new ParsedMessage or if there is already a blob, in which
                // case we may have to delete a section from it.
                boolean newInvHasBlobPart = newInvite.hasBlobPart();
                if (hadBlobPart || newInvHasBlobPart) {
                    if (addNewOne) {
                        modifyBlob(toRemove, discardExistingInvites, toUpdate, pm, newInvite, isCancel, !denyPrivateAccess, true, replaceExceptionBodyWithSeriesBody);
                    } else {
                        if (!newInvHasBlobPart)
                            // force existing MIME part to be removed
                            toRemove.add(newInvite);
                        modifyBlob(toRemove, discardExistingInvites, toUpdate, null, null, isCancel, !denyPrivateAccess, true, replaceExceptionBodyWithSeriesBody);
                    }
                // TIM: modifyBlob will save the metadata for us as a side-effect
                //                      saveMetadata();
                } else {
                    markItemModified(Change.INVITE);
                    try {
                        if (batch) {
                            persistBatchedChanges = true;
                        } else {
                            // call setContent here so that MOD_CONTENT is updated...this is required
                            // for the index entry to be correctly updated (bug 39463)
                            setContent(null, null);
                        }
                    } catch (IOException e) {
                        throw ServiceException.FAILURE("IOException", e);
                    }
                }
                // remove the item if all the instances are canceled.
                Invite defInvite = getDefaultInviteOrNull();
                if (defInvite != null) {
                    Collection<Instance> instances = expandInstances(0, Long.MAX_VALUE, false);
                    if (instances.isEmpty()) {
                        ZimbraLog.calendar.warn("Deleting calendar item " + getId() + " in mailbox " + getMailboxId() + " because it has no invite after applying request/cancel invite");
                        delete();
                        return true;
                    }
                }
                Callback cb = getCallback();
                if (cb != null)
                    cb.modified(this);
                return true;
            }
        } else {
            if (getFolderId() != folderId) {
                // Move appointment/task to a different folder.
                move(folder);
            }
            return false;
        }
    }
}
Also used : Account(com.zimbra.cs.account.Account) InviteChanges(com.zimbra.cs.mailbox.calendar.InviteChanges) ParsedDuration(com.zimbra.common.calendar.ParsedDuration) ArrayList(java.util.ArrayList) RecurId(com.zimbra.cs.mailbox.calendar.RecurId) IRecurrence(com.zimbra.cs.mailbox.calendar.Recurrence.IRecurrence) Iterator(java.util.Iterator) ParsedDateTime(com.zimbra.common.calendar.ParsedDateTime) SimpleRepeatingRule(com.zimbra.cs.mailbox.calendar.Recurrence.SimpleRepeatingRule) CalendarResource(com.zimbra.cs.account.CalendarResource) HashSet(java.util.HashSet) RecurrenceRule(com.zimbra.cs.mailbox.calendar.Recurrence.RecurrenceRule) ZOrganizer(com.zimbra.cs.mailbox.calendar.ZOrganizer) IOException(java.io.IOException) ZRecur(com.zimbra.cs.mailbox.calendar.ZRecur) Alarm(com.zimbra.cs.mailbox.calendar.Alarm) ZAttendee(com.zimbra.cs.mailbox.calendar.ZAttendee) ZProperty(com.zimbra.common.calendar.ZCalendar.ZProperty) Frequency(com.zimbra.cs.mailbox.calendar.ZRecur.Frequency) Invite(com.zimbra.cs.mailbox.calendar.Invite) ICalTimeZone(com.zimbra.common.calendar.ICalTimeZone)

Aggregations

RecurId (com.zimbra.cs.mailbox.calendar.RecurId)23 Invite (com.zimbra.cs.mailbox.calendar.Invite)14 ParsedDateTime (com.zimbra.common.calendar.ParsedDateTime)11 Element (com.zimbra.common.soap.Element)11 Account (com.zimbra.cs.account.Account)10 CalendarItem (com.zimbra.cs.mailbox.CalendarItem)9 ZAttendee (com.zimbra.cs.mailbox.calendar.ZAttendee)9 ItemId (com.zimbra.cs.service.util.ItemId)9 Mailbox (com.zimbra.cs.mailbox.Mailbox)8 ZOrganizer (com.zimbra.cs.mailbox.calendar.ZOrganizer)8 ArrayList (java.util.ArrayList)7 MimeMessage (javax.mail.internet.MimeMessage)6 ICalTimeZone (com.zimbra.common.calendar.ICalTimeZone)5 ParsedDuration (com.zimbra.common.calendar.ParsedDuration)5 OperationContext (com.zimbra.cs.mailbox.OperationContext)5 ZimbraSoapContext (com.zimbra.soap.ZimbraSoapContext)5 TimeZoneMap (com.zimbra.common.calendar.TimeZoneMap)4 CalendarResource (com.zimbra.cs.account.CalendarResource)4 IRecurrence (com.zimbra.cs.mailbox.calendar.Recurrence.IRecurrence)4 Geo (com.zimbra.common.calendar.Geo)3