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;
}
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;
}
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;
}
}
}
Aggregations