use of com.zimbra.cs.mailbox.calendar.Invite in project zm-mailbox by Zimbra.
the class Appointment method getBusyTimesString.
private static String getBusyTimesString(OperationContext octxt, Mailbox mbox, List<Conflict> list, TimeZone tz, Locale lc, boolean hasMoreConflicts) throws ServiceException {
StringBuilder sb = new StringBuilder();
int conflictCount = 0;
for (Conflict avail : list) {
if (!avail.isBusy())
continue;
// List conflicting appointments and their organizers.
FreeBusy fb = avail.getFreeBusy();
List<FBInstance> instances = new ArrayList<FBInstance>();
for (Iterator<Interval> iter = fb.iterator(); iter.hasNext(); ) {
Interval interval = iter.next();
// busy intervals only
if (Conflict.isBusy(interval.getStatus())) {
// busy appointments only
for (FBInstance fbinst : interval.getInstances()) {
if (Conflict.isBusy(fbinst.getFreeBusy()))
instances.add(fbinst);
}
}
}
for (FBInstance instance : instances) {
Date startDate = new Date(instance.getStartTime());
Date endDate = new Date(instance.getEndTime());
String start = CalendarMailSender.formatDateTime(startDate, tz, lc);
sb.append(" * ").append(start);
String end;
if (DateTimeUtil.sameDay(startDate, endDate, tz)) {
end = CalendarMailSender.formatTime(endDate, tz, lc);
sb.append(" - ").append(end);
} else {
end = CalendarMailSender.formatDateTime(endDate, tz, lc);
sb.append("\r\n - ").append(end);
}
int apptId = instance.getApptId();
long recurIdDt = instance.getRecurIdDt();
CalendarItem appt = mbox.getCalendarItemById(octxt, apptId);
Invite inv = appt.getInviteForRecurId(recurIdDt);
if (inv != null && inv.hasOrganizer()) {
ZOrganizer organizer = inv.getOrganizer();
String orgDispName;
if (organizer.hasCn())
orgDispName = organizer.getCn() + " <" + organizer.getAddress() + ">";
else
orgDispName = organizer.getAddress();
sb.append(L10nUtil.getMessage(MsgKey.calendarResourceConflictScheduledBy, lc, orgDispName));
}
sb.append("\r\n");
conflictCount++;
}
}
if (hasMoreConflicts)
sb.append(" * ...\r\n");
return sb.toString();
}
use of com.zimbra.cs.mailbox.calendar.Invite in project zm-mailbox by Zimbra.
the class ItemActionHelper method executeRemote.
private void executeRemote() throws ServiceException, IOException {
Account target = Provisioning.getInstance().get(Key.AccountBy.id, mIidFolder.getAccountId());
AuthToken at = getAuthToken();
String pxyAuthToken = Provisioning.onLocalServer(target) ? null : at.getProxyAuthToken();
ZAuthToken zat = null;
if (pxyAuthToken == null) {
zat = at.toZAuthToken();
zat.resetProxyAuthToken();
} else {
zat = new ZAuthToken(pxyAuthToken);
}
ZMailbox.Options zoptions = new ZMailbox.Options(zat, AccountUtil.getSoapUri(target));
zoptions.setNoSession(true);
zoptions.setTargetAccount(target.getId());
zoptions.setTargetAccountBy(Key.AccountBy.id);
ZMailbox zmbx = ZMailbox.getMailbox(zoptions);
// check for mountpoints before going any further...
ZFolder zfolder = zmbx.getFolderById(mIidFolder.toString(mAuthenticatedAccount));
if (zfolder instanceof ZMountpoint) {
ItemId iidTarget = new ItemId(((ZMountpoint) zfolder).getCanonicalRemoteId(), mAuthenticatedAccount.getId());
if (!mIidFolder.equals(iidTarget)) {
mIidFolder = iidTarget;
if (++mHopCount > com.zimbra.soap.ZimbraSoapContext.MAX_HOP_COUNT)
throw MailServiceException.TOO_MANY_HOPS(mIidRequestedFolder);
schedule();
return;
}
}
boolean deleteOriginal = mOperation != Op.COPY;
String folderStr = mIidFolder.toString();
mCreatedIds = new ArrayList<String>(itemIds.length);
boolean toSpam = mIidFolder.getId() == Mailbox.ID_FOLDER_SPAM;
boolean toMailbox = !toSpam && mIidFolder.getId() != Mailbox.ID_FOLDER_TRASH;
for (MailItem item : mMailbox.getItemById(mOpCtxt, itemIds, type)) {
if (item == null) {
continue;
}
List<Message> msgs = null;
if (item instanceof Conversation) {
msgs = mMailbox.getMessagesByConversation(mOpCtxt, item.getId(), SortBy.DATE_ASC, -1);
}
if (deleteOriginal) {
if (msgs != null) {
// determine which of the conversation's component messages are actually able to be moved
boolean permDenied = false;
for (Iterator<Message> it = msgs.iterator(); it.hasNext(); ) {
Message msg = it.next();
if (!TargetConstraint.checkItem(mTargetConstraint, msg)) {
it.remove();
} else if (!canDelete(msg)) {
it.remove();
permDenied = true;
}
}
// stop here if no messages would be moved...
if (msgs.isEmpty()) {
if (permDenied) {
throw ServiceException.PERM_DENIED("cannot delete any messages in " + item.getType() + " " + item.getId());
}
// all messages were excluded by the TargetConstraint, so there's no failure...
continue;
}
} else {
if (!canDelete(item)) {
throw ServiceException.PERM_DENIED("cannot delete existing copy of " + item.getType() + " " + item.getId());
}
}
}
boolean fromSpam = item.inSpam();
if ((fromSpam && toMailbox) || (!fromSpam && toSpam)) {
try {
Folder dest = mMailbox.getFolderById(mOpCtxt, mIidFolder.getId());
SpamReport report = new SpamReport(toSpam, "remote " + mOperation, dest.getPath());
Folder source = mMailbox.getFolderById(mOpCtxt, item.getFolderId());
report.setSourceFolderPath(source.getPath());
report.setDestAccountName(target.getName());
SpamHandler.getInstance().handle(mOpCtxt, mMailbox, item.getId(), item.getType(), report);
} catch (OutOfMemoryError e) {
Zimbra.halt("out of memory", e);
} catch (Throwable t) {
ZimbraLog.mailop.info("could not train spam filter: " + new ItemId(item).toString(), t);
}
}
// since we can't apply tags to a remote object, hardwiring "tags" to null below...
String flags = (mOperation == Op.UPDATE && mFlags != null ? mFlags : item.getFlagString());
String name = ((mOperation == Op.RENAME || mOperation == Op.UPDATE) && mName != null ? mName : item.getName());
String createdId = null;
InputStream in = null;
switch(item.getType()) {
case CONTACT:
Contact ct = (Contact) item;
Map<String, ZMailbox.ZAttachmentInfo> attachments = new HashMap<String, ZMailbox.ZAttachmentInfo>();
for (Contact.Attachment att : ct.getAttachments()) {
String attachmentId = zmbx.uploadAttachment(att.getFilename(), att.getContent(), att.getContentType(), 0);
ZMailbox.ZAttachmentInfo info = new ZMailbox.ZAttachmentInfo().setAttachmentId(attachmentId);
attachments.put(att.getName(), info);
}
Map<String, String> fields = ct.getFields();
Map<String, String> members = new HashMap<String, String>();
for (String key : fields.keySet()) {
if (ContactConstants.A_groupMember.equals(key)) {
String memberEncoded = fields.get(key);
ContactGroup group = ContactGroup.init(memberEncoded);
for (Member m : group.getMembers()) {
members.put(m.getValue(), m.getType().getSoapEncoded());
}
break;
}
}
fields.remove(ContactConstants.A_groupMember);
ZContact contact = zmbx.createContact(folderStr, null, fields, attachments, members);
createdId = contact.getId();
mCreatedIds.add(createdId);
break;
case MESSAGE:
try {
in = StoreManager.getInstance().getContent(item.getBlob());
createdId = zmbx.addMessage(folderStr, flags, null, item.getDate(), in, item.getSize(), true);
} finally {
ByteUtil.closeStream(in);
}
mCreatedIds.add(createdId);
break;
case VIRTUAL_CONVERSATION:
case CONVERSATION:
for (Message msg : msgs) {
flags = (mOperation == Op.UPDATE && mFlags != null ? mFlags : msg.getFlagString());
try {
in = StoreManager.getInstance().getContent(msg.getBlob());
createdId = zmbx.addMessage(folderStr, flags, null, msg.getDate(), in, msg.getSize(), true);
} finally {
ByteUtil.closeStream(in);
}
mCreatedIds.add(createdId);
}
break;
case DOCUMENT:
Document doc = (Document) item;
SoapHttpTransport transport = new SoapHttpTransport(zoptions.getUri());
try {
in = StoreManager.getInstance().getContent(doc.getBlob());
String uploadId = zmbx.uploadContentAsStream(name, in, doc.getContentType(), doc.getSize(), 4000, true);
// instead of using convenience method from ZMailbox
// we need to hand marshall the request and set the
// response protocol explicitly to what was requested
// from the client.
Element req = new XMLElement(MailConstants.SAVE_DOCUMENT_REQUEST);
Element edoc = req.addUniqueElement(MailConstants.E_DOC);
edoc.addAttribute(MailConstants.A_NAME, name);
edoc.addAttribute(MailConstants.A_FOLDER, folderStr);
edoc.addAttribute(MailConstants.A_FLAGS, flags);
Element upload = edoc.addElement(MailConstants.E_UPLOAD);
upload.addAttribute(MailConstants.A_ID, uploadId);
transport.setResponseProtocol(mResponseProtocol);
transport.setAuthToken(zat);
Element response = transport.invoke(req);
createdId = response.getElement(MailConstants.E_DOC).getAttribute(MailConstants.A_ID);
} finally {
ByteUtil.closeStream(in);
transport.shutdown();
}
mCreatedIds.add(createdId);
break;
case APPOINTMENT:
case TASK:
CalendarItem cal = (CalendarItem) item;
// private calendar item may not be moved by non-owner unless permission was granted
if (!cal.isPublic()) {
boolean asAdmin = mOpCtxt != null ? mOpCtxt.isUsingAdminPrivileges() : false;
if (!cal.allowPrivateAccess(mAuthenticatedAccount, asAdmin))
throw ServiceException.PERM_DENIED("you do not have permission to move/copy a private calendar item from the current folder/mailbox");
}
// Move the item to remote mailbox using SetAppointmentRequest/SetTaskRequest.
QName qname = (item.getType() == MailItem.Type.TASK ? MailConstants.SET_TASK_REQUEST : MailConstants.SET_APPOINTMENT_REQUEST);
Element request = new Element.XMLElement(qname).addAttribute(MailConstants.A_FOLDER, folderStr).addAttribute(MailConstants.A_FLAGS, flags);
ToXML.encodeAlarmTimes(request, cal);
Invite invDefault = cal.getDefaultInviteOrNull();
// Takeover as organizer if we're doing a MOVE and source mailbox is the organizer.
// Don't takeover in a COPY operation.
boolean takeoverAsOrganizer = false;
boolean blockMove = false;
if (Op.MOVE.equals(mOperation)) {
Invite inv = invDefault;
if (inv == null) {
// no default invite; let's use the first invite
Invite[] invs = cal.getInvites();
if (invs != null && invs.length > 0)
inv = invs[0];
}
takeoverAsOrganizer = inv != null && inv.isOrganizer();
blockMove = takeoverAsOrganizer && inv.hasOtherAttendees();
}
if (blockMove) {
throw MailServiceException.INVALID_REQUEST("This operation requires change of organizer and it is not permitted", null);
}
if (invDefault != null) {
addCalendarPart(request.addUniqueElement(MailConstants.A_DEFAULT), cal, invDefault, zmbx, target, takeoverAsOrganizer);
}
for (Invite inv : cal.getInvites()) {
if (inv == null || inv == invDefault)
continue;
String elem = inv.isCancel() ? MailConstants.E_CAL_CANCEL : MailConstants.E_CAL_EXCEPT;
addCalendarPart(request.addElement(elem), cal, inv, zmbx, target, takeoverAsOrganizer);
}
ToXML.encodeCalendarReplies(request, cal);
createdId = zmbx.invoke(request).getAttribute(MailConstants.A_CAL_ID);
mCreatedIds.add(createdId);
break;
default:
throw MailServiceException.CANNOT_COPY(item.getId());
}
try {
if (deleteOriginal && !mIdFormatter.formatItemId(item).equals(createdId)) {
if (msgs == null) {
mMailbox.delete(mOpCtxt, item.getId(), item.getType());
} else {
for (Message msg : msgs) mMailbox.delete(mOpCtxt, msg.getId(), msg.getType());
}
}
} catch (ServiceException e) {
if (e.getCode() != ServiceException.PERM_DENIED)
throw e;
// something funky happened permissions-wise between the getEffectivePermissions check and here...
ZimbraLog.misc.info("could not delete original item " + item.getId() + "; treating operation as a copy instead");
}
}
}
use of com.zimbra.cs.mailbox.calendar.Invite in project zm-mailbox by Zimbra.
the class ItemActionHelper method addCalendarPart.
private void addCalendarPart(Element parent, CalendarItem cal, Invite inv, ZMailbox zmbx, Account target, boolean takeoverAsOrganizer) throws ServiceException {
parent.addAttribute(MailConstants.A_CAL_PARTSTAT, inv.getPartStat());
Element m = parent.addUniqueElement(MailConstants.E_MSG);
Pair<MimeMessage, Integer> spinfo = cal.getSubpartMessageData(inv.getMailItemId());
if (spinfo != null) {
try {
ByteArrayOutputStream baos = new ByteArrayOutputStream(spinfo.getSecond());
spinfo.getFirst().writeTo(baos);
String uploadId = zmbx.uploadAttachment("message", baos.toByteArray(), MimeConstants.CT_MESSAGE_RFC822, 6000);
m.addAttribute(MailConstants.A_ATTACHMENT_ID, uploadId);
} catch (IOException ioe) {
ZimbraLog.misc.info("could not read subpart message for part " + inv.getComponentNum() + " of item " + cal.getId(), ioe);
} catch (MessagingException me) {
ZimbraLog.misc.info("could not read subpart message for part " + inv.getComponentNum() + " of item " + cal.getId(), me);
}
}
if (takeoverAsOrganizer && inv.isOrganizer() && inv.hasOrganizer()) {
Invite invCopy = inv.newCopy();
invCopy.setInviteId(inv.getMailItemId());
// Increment SEQUENCE and bring DTSTAMP current because we're changing organizer.
invCopy.setSeqNo(inv.getSeqNo() + 1);
invCopy.setDtStamp(System.currentTimeMillis());
ZOrganizer org = invCopy.getOrganizer();
org.setAddress(target.getName());
org.setCn(target.getDisplayName());
Account authAcct = mOpCtxt != null ? mOpCtxt.getAuthenticatedUser() : target;
if (authAcct == null || authAcct.equals(target))
org.setSentBy(null);
else
org.setSentBy(authAcct.getName());
inv = invCopy;
}
// explicitly add the invite metadata here
ToXML.encodeInvite(m, mIdFormatter, getOpCtxt(), cal, inv, true);
}
use of com.zimbra.cs.mailbox.calendar.Invite in project zm-mailbox by Zimbra.
the class ModifyCalendarItem method handle.
@Override
public Element handle(Element request, Map<String, Object> context) throws ServiceException {
ZimbraSoapContext zsc = getZimbraSoapContext(context);
Account acct = getRequestedAccount(zsc);
Mailbox mbox = getRequestedMailbox(zsc);
OperationContext octxt = getOperationContext(zsc, context);
// proxy handling
Element msgElem = request.getElement(MailConstants.E_MSG);
String folderStr = msgElem.getAttribute(MailConstants.A_FOLDER, null);
ItemId iid = new ItemId(request.getAttribute(MailConstants.A_ID), zsc);
if (!iid.belongsTo(acct)) {
// Proxy it.
if (folderStr != null) {
// make sure that the folder ID is fully qualified
ItemId folderFQ = new ItemId(folderStr, zsc);
msgElem.addAttribute(MailConstants.A_FOLDER, folderFQ.toString());
}
return proxyRequest(request, context, iid.getAccountId());
}
// Check if moving to a different mailbox.
boolean isInterMboxMove = false;
ItemId iidFolder = null;
if (folderStr != null) {
iidFolder = new ItemId(folderStr, zsc);
isInterMboxMove = !iidFolder.belongsTo(mbox);
}
MailSendQueue sendQueue = new MailSendQueue();
Element response = getResponseElement(zsc);
int compNum = (int) request.getAttributeLong(MailConstants.A_CAL_COMP, 0);
mbox.lock.lock();
try {
CalendarItem calItem = mbox.getCalendarItemById(octxt, iid.getId());
if (calItem == null) {
throw MailServiceException.NO_SUCH_CALITEM(iid.toString(), "Could not find calendar item");
}
// Reject the request if calendar item is under trash or is being moved to trash.
if (calItem.inTrash()) {
throw ServiceException.INVALID_REQUEST("cannot modify a calendar item under trash", null);
}
if (!isInterMboxMove && iidFolder != null) {
if (iidFolder.getId() != calItem.getFolderId()) {
Folder destFolder = mbox.getFolderById(octxt, iidFolder.getId());
if (destFolder.inTrash()) {
throw ServiceException.INVALID_REQUEST("cannot combine with a move to trash", null);
}
}
}
// Conflict detection. Do it only if requested by client. (for backward compat)
int modSeq = (int) request.getAttributeLong(MailConstants.A_MODIFIED_SEQUENCE, 0);
int revision = (int) request.getAttributeLong(MailConstants.A_REVISION, 0);
if (modSeq != 0 && revision != 0 && (modSeq < calItem.getModifiedSequence() || revision < calItem.getSavedSequence())) {
throw MailServiceException.INVITE_OUT_OF_DATE(iid.toString());
}
Invite inv = calItem.getInvite(iid.getSubpartId(), compNum);
if (inv == null) {
throw MailServiceException.INVITE_OUT_OF_DATE(iid.toString());
}
Invite seriesInv = calItem.getDefaultInviteOrNull();
int folderId = calItem.getFolderId();
if (!isInterMboxMove && iidFolder != null) {
folderId = iidFolder.getId();
}
modifyCalendarItem(zsc, octxt, request, acct, mbox, folderId, calItem, inv, seriesInv, response, isInterMboxMove, sendQueue);
} finally {
mbox.lock.release();
sendQueue.send();
}
// Inter-mailbox move if necessary.
if (isInterMboxMove) {
CalendarItem calItem = mbox.getCalendarItemById(octxt, iid.getId());
List<Integer> ids = new ArrayList<Integer>(1);
ids.add(calItem.getId());
ItemActionHelper.MOVE(octxt, mbox, zsc.getResponseProtocol(), ids, calItem.getType(), null, iidFolder);
}
return response;
}
use of com.zimbra.cs.mailbox.calendar.Invite 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