use of com.zimbra.cs.mailbox.calendar.ZAttendee in project zm-mailbox by Zimbra.
the class TestCalAttendeesDiff method testAttendeesDiff.
@Test
public void testAttendeesDiff() throws Exception {
Provisioning prov = Provisioning.getInstance();
Account requester = prov.get(AccountBy.name, REQUESTER);
List<ZAttendee> oldList = new ArrayList<ZAttendee>();
oldList.add(new ZAttendee(ZIMBRA_GROUP));
oldList.add(new ZAttendee(EXTERNAL_GROUP));
// in ZIMBRA_GROUP
oldList.add(new ZAttendee(USER_L1));
// in ZIMBRA_GROUP via alias
oldList.add(new ZAttendee(USER_L2));
// not in any group
oldList.add(new ZAttendee(USER_L3));
// in ZIMBRA_GROUP
oldList.add(new ZAttendee(USER_R1));
// in EXTERNAL_GROUP
oldList.add(new ZAttendee(USER_R2));
// not in any group
oldList.add(new ZAttendee(USER_R3));
// New attendee list has all individual attendees removed.
List<ZAttendee> newList = new ArrayList<ZAttendee>();
newList.add(new ZAttendee(ZIMBRA_GROUP));
newList.add(new ZAttendee(EXTERNAL_GROUP));
Set<String> diff;
// Which attendees were really removed after considering group memberships?
diff = toAddressSet(CalendarUtils.getRemovedAttendees(oldList, newList, true, requester));
assertTrue(USER_L3 + " was removed", diff.contains(USER_L3));
assertTrue(USER_R3 + " was removed", diff.contains(USER_R3));
assertEquals("number of attendees removed", 2, diff.size());
// Do a simple diff.
diff = toAddressSet(CalendarUtils.getRemovedAttendees(oldList, newList, false, requester));
assertTrue(USER_L1 + " was removed", diff.contains(USER_L1));
assertTrue(USER_L2 + " was removed", diff.contains(USER_L2));
assertTrue(USER_L3 + " was removed", diff.contains(USER_L3));
assertTrue(USER_R1 + " was removed", diff.contains(USER_R1));
assertTrue(USER_R2 + " was removed", diff.contains(USER_R2));
assertTrue(USER_R3 + " was removed", diff.contains(USER_R3));
assertEquals("number of attendees removed", 6, diff.size());
}
use of com.zimbra.cs.mailbox.calendar.ZAttendee in project zm-mailbox by Zimbra.
the class TestCalendarMailSender method testSameDomainPrefCalendarAllowedTargetsForInviteDeniedAutoReply.
public void testSameDomainPrefCalendarAllowedTargetsForInviteDeniedAutoReply() throws Exception {
Account organizerAccount = TestUtil.createAccount(ORGANIZERACCT);
Account senderAccount = organizerAccount;
Account attendeeAccount = TestUtil.createAccount(ATTENDEEACCT);
Mailbox mbox = TestUtil.getMailbox(ATTENDEEACCT);
String senderEmail = senderAccount.getName();
Map<String, Object> prefs = Maps.newHashMap();
prefs.put(Provisioning.A_zimbraPrefCalendarSendInviteDeniedAutoReply, ProvisioningConstants.TRUE);
prefs.put(Provisioning.A_zimbraPrefCalendarAllowedTargetsForInviteDeniedAutoReply, "sameDomain");
Provisioning.getInstance().modifyAttrs(attendeeAccount, prefs, true, null);
ZAttendee matchingAttendee = new ZAttendee(attendeeAccount.getName(), attendeeAccount.getCn(), /* cn */
senderAccount.getName(), /* sentBy */
null, /* dir */
null, /* language */
IcalXmlStrMap.CUTYPE_INDIVIDUAL, /* cutype */
null, /* role */
IcalXmlStrMap.PARTSTAT_NEEDS_ACTION, /* ptst */
true, /* rsvp */
null, /* member */
null, /* delegatedTo */
null, /* delegatedFrom */
null);
boolean allowed;
allowed = CalendarMailSender.allowInviteAutoDeclinedNotification(mbox, attendeeAccount, senderEmail, senderAccount, true, /* applyToCalendar */
matchingAttendee);
assertTrue(String.format("Should be allowed to send auto-decline because %s=true & %s=sameDomain", Provisioning.A_zimbraPrefCalendarSendInviteDeniedAutoReply, Provisioning.A_zimbraPrefCalendarAllowedTargetsForInviteDeniedAutoReply), allowed);
Provisioning prov = Provisioning.getInstance();
prov.createDomain(DIFFDOMAIN, new HashMap<String, Object>());
senderAccount = prov.createAccount(DIFFDOMAINACCT, "test123", null);
senderEmail = senderAccount.getName();
allowed = CalendarMailSender.allowInviteAutoDeclinedNotification(mbox, attendeeAccount, senderEmail, senderAccount, true, /* applyToCalendar */
matchingAttendee);
assertFalse(String.format("Should NOT be allowed to send auto-decline to diff domain because %s=true & %s=sameDomain", Provisioning.A_zimbraPrefCalendarSendInviteDeniedAutoReply, Provisioning.A_zimbraPrefCalendarAllowedTargetsForInviteDeniedAutoReply), allowed);
senderAccount = null;
allowed = CalendarMailSender.allowInviteAutoDeclinedNotification(mbox, attendeeAccount, senderEmail, senderAccount, true, /* applyToCalendar */
matchingAttendee);
assertFalse(String.format("Should NOT be allowed to send auto-decline to non-internal because %s=true & %s=sameDomain", Provisioning.A_zimbraPrefCalendarSendInviteDeniedAutoReply, Provisioning.A_zimbraPrefCalendarAllowedTargetsForInviteDeniedAutoReply), allowed);
}
use of com.zimbra.cs.mailbox.calendar.ZAttendee in project zm-mailbox by Zimbra.
the class TestCalendarMailSender method testInviteAutoDeclinePrefsUnset.
public void testInviteAutoDeclinePrefsUnset() throws Exception {
Account organizerAccount = TestUtil.createAccount(ORGANIZERACCT);
Account senderAccount = organizerAccount;
Account attendeeAccount = TestUtil.createAccount(ATTENDEEACCT);
Mailbox mbox = TestUtil.getMailbox(ATTENDEEACCT);
String senderEmail = senderAccount.getName();
ZAttendee matchingAttendee = new ZAttendee(attendeeAccount.getName(), attendeeAccount.getCn(), /* cn */
senderAccount.getName(), /* sentBy */
null, /* dir */
null, /* language */
IcalXmlStrMap.CUTYPE_INDIVIDUAL, /* cutype */
null, /* role */
IcalXmlStrMap.PARTSTAT_NEEDS_ACTION, /* ptst */
true, /* rsvp */
null, /* member */
null, /* delegatedTo */
null, /* delegatedFrom */
null);
boolean allowed = CalendarMailSender.allowInviteAutoDeclinedNotification(mbox, attendeeAccount, senderEmail, senderAccount, true, /* applyToCalendar */
matchingAttendee);
assertFalse(String.format("Should NOT be allowed to send auto-decline because default value for %s is false", Provisioning.A_zimbraPrefCalendarSendInviteDeniedAutoReply), allowed);
}
use of com.zimbra.cs.mailbox.calendar.ZAttendee 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