use of com.zimbra.cs.mailbox.calendar.Alarm in project zm-mailbox by Zimbra.
the class CalendarUtils method parseInviteElementCommon.
/**
* UID, DTSTAMP, and SEQUENCE **MUST** be set by caller
*
* @param account
* user receiving invite
* @param element
* invite XML element
* @param newInv
* Invite we are currently building up
* @param oldTzMap
* time zone map from A DIFFERENT invite; if this method is
* called during modify operation, this map contains time zones
* before the modification; null if called during create
* operation
* @return
* @throws ServiceException
*/
private static void parseInviteElementCommon(Account account, MailItem.Type type, Element element, Invite newInv, boolean recurrenceIdAllowed, boolean recurAllowed) throws ServiceException {
//zdsync
String invId = element.getAttribute(MailConstants.A_ID, null);
Element compElem = element.getOptionalElement(MailConstants.E_INVITE_COMPONENT);
if (compElem != null) {
element = compElem;
}
//zdsync
String dts = element.getAttribute(MailConstants.A_CAL_DATETIME, null);
TimeZoneMap tzMap = newInv.getTimeZoneMap();
parseTimeZones(element.getParent(), tzMap);
newInv.setItemType(type);
// UID
String uid = element.getAttribute(MailConstants.A_UID, null);
if (uid != null && uid.length() > 0)
newInv.setUid(uid);
// RECURRENCE-ID
if (recurrenceIdAllowed) {
Element e = element.getOptionalElement(MailConstants.E_CAL_EXCEPTION_ID);
if (e != null) {
ParsedDateTime dt = parseDateTime(e, tzMap);
RecurId recurId = new RecurId(dt, RecurId.RANGE_NONE);
newInv.setRecurId(recurId);
}
} else {
if (element.getOptionalElement(MailConstants.E_CAL_EXCEPTION_ID) != null) {
throw ServiceException.INVALID_REQUEST("May not specify an <exceptId> in this request", null);
}
}
String name = element.getAttribute(MailConstants.A_NAME, "");
String location = element.getAttribute(MailConstants.A_CAL_LOCATION, "");
// CATEGORIES
for (Iterator<Element> catIter = element.elementIterator(MailConstants.E_CAL_CATEGORY); catIter.hasNext(); ) {
String cat = catIter.next().getText();
newInv.addCategory(cat);
}
// COMMENTs
for (Iterator<Element> cmtIter = element.elementIterator(MailConstants.E_CAL_COMMENT); cmtIter.hasNext(); ) {
String cmt = cmtIter.next().getText();
newInv.addComment(cmt);
}
// CONTACTs
for (Iterator<Element> cnIter = element.elementIterator(MailConstants.E_CAL_CONTACT); cnIter.hasNext(); ) {
String contact = cnIter.next().getTextTrim();
newInv.addContact(contact);
}
// GEO
Element geoElem = element.getOptionalElement(MailConstants.E_CAL_GEO);
if (geoElem != null) {
Geo geo = Geo.parse(geoElem);
newInv.setGeo(geo);
}
// URL
String url = element.getAttribute(MailConstants.A_CAL_URL, null);
newInv.setUrl(url);
// SEQUENCE
int seq = (int) element.getAttributeLong(MailConstants.A_CAL_SEQUENCE, 0);
newInv.setSeqNo(seq);
// SUMMARY (aka Name or Subject)
newInv.setName(name);
// DESCRIPTION
Element descElem = element.getOptionalElement(MailConstants.E_CAL_DESCRIPTION);
String desc = descElem != null ? descElem.getText() : null;
Element descHtmlElem = element.getOptionalElement(MailConstants.E_CAL_DESC_HTML);
String descHtml = descHtmlElem != null ? descHtmlElem.getText() : null;
newInv.setDescription(desc, descHtml);
boolean allDay = element.getAttributeBool(MailConstants.A_CAL_ALLDAY, false);
newInv.setIsAllDayEvent(allDay);
// DTSTART
Element startElem;
if (newInv.isTodo())
startElem = element.getOptionalElement(MailConstants.E_CAL_START_TIME);
else
startElem = element.getElement(MailConstants.E_CAL_START_TIME);
if (startElem != null) {
ParsedDateTime dt = parseDtElement(startElem, tzMap, newInv);
// fixup for bug 30121
if (allDay && dt.hasTime()) {
// If this is supposed to be an all-day event but DTSTART has time part, clear the time part.
dt.setHasTime(false);
} else if (!allDay && !dt.hasTime()) {
// If the event isn't marked as all-day but DTSTART is date-only, the client simply forgot
// to mark it all-day. Do all-day implicitly.
allDay = true;
newInv.setIsAllDayEvent(allDay);
}
newInv.setDtStart(dt);
}
// DTEND (for VEVENT) or DUE (for VTODO)
Element endElem = element.getOptionalElement(MailConstants.E_CAL_END_TIME);
if (endElem != null) {
ParsedDateTime dt = parseDtElement(endElem, tzMap, newInv);
// fixup for bug 30121
if (allDay && dt.hasTime()) {
// If this is supposed to be an all-day event but DTEND has time part, clear the time part.
dt.setHasTime(false);
} else if (!allDay && !dt.hasTime()) {
// If the event isn't marked as all-day but DTEND is date-only, the client simply forgot
// to mark it all-day. Do all-day implicitly.
allDay = true;
newInv.setIsAllDayEvent(allDay);
}
if (allDay && !newInv.isTodo()) {
// HACK ALERT: okay, campers, here's the deal.
// By definition, our end dates are EXCLUSIVE: DTEND is not
// included.. eg a meeting 7-8pm actually stops at 7:59
//
// This makes sense for normal appointments, but apparently
// this rule is confusing to people when making
// all-day-events
//
// For all-day-events, people want to say that a 1-day-long
// appointment starts on 11/1 and ends on 11/1, for example
// this is inconsistent (and incompatible with RFC2445) but
// it is what people want. Sooo, we to a bit of a hacky
// translation when sending/receiving all-day-events.
//
dt = dt.add(ParsedDuration.ONE_DAY);
}
newInv.setDtEnd(dt);
} else {
// DURATION
Element d = element.getOptionalElement(MailConstants.E_CAL_DURATION);
if (d != null) {
ParsedDuration pd = ParsedDuration.parse(d);
newInv.setDuration(pd);
}
}
// LOCATION
newInv.setLocation(location);
// STATUS
String status = element.getAttribute(MailConstants.A_CAL_STATUS, newInv.isEvent() ? IcalXmlStrMap.STATUS_CONFIRMED : IcalXmlStrMap.STATUS_NEEDS_ACTION);
validateAttr(IcalXmlStrMap.sStatusMap, MailConstants.A_CAL_STATUS, status);
newInv.setStatus(status);
// CLASS
String classProp = element.getAttribute(MailConstants.A_CAL_CLASS, IcalXmlStrMap.CLASS_PUBLIC);
validateAttr(IcalXmlStrMap.sClassMap, MailConstants.A_CAL_CLASS, classProp);
newInv.setClassProp(classProp);
// PRIORITY
String priority = element.getAttribute(MailConstants.A_CAL_PRIORITY, null);
newInv.setPriority(priority);
if (newInv.isEvent()) {
// FreeBusy
String fb = element.getAttribute(MailConstants.A_APPT_FREEBUSY, null);
if (fb != null) {
newInv.setFreeBusy(fb);
// Intended F/B takes precedence over TRANSP.
if (IcalXmlStrMap.FBTYPE_FREE.equals(fb))
newInv.setTransparency(IcalXmlStrMap.TRANSP_TRANSPARENT);
else
newInv.setTransparency(IcalXmlStrMap.TRANSP_OPAQUE);
} else {
// TRANSP is examined only when intended F/B is not supplied.
String transp = element.getAttribute(MailConstants.A_APPT_TRANSPARENCY, IcalXmlStrMap.TRANSP_OPAQUE);
validateAttr(IcalXmlStrMap.sTranspMap, MailConstants.A_APPT_TRANSPARENCY, transp);
newInv.setTransparency(transp);
// If opaque, don't set intended f/b because there are multiple possibilities.
if (newInv.isTransparent())
newInv.setFreeBusy(IcalXmlStrMap.FBTYPE_FREE);
}
}
if (newInv.isTodo()) {
// PERCENT-COMPLETE
String pctComplete = element.getAttribute(MailConstants.A_TASK_PERCENT_COMPLETE, null);
newInv.setPercentComplete(pctComplete);
// COMPLETED
String completed = element.getAttribute(MailConstants.A_TASK_COMPLETED, null);
if (completed != null) {
try {
ParsedDateTime c = ParsedDateTime.parseUtcOnly(completed);
newInv.setCompleted(c.getUtcTime());
} catch (ParseException e) {
throw ServiceException.INVALID_REQUEST("Invalid COMPLETED value: " + completed, e);
}
} else if (status.equals(IcalXmlStrMap.STATUS_COMPLETED)) {
newInv.setCompleted(System.currentTimeMillis());
} else {
newInv.setCompleted(0);
}
}
// ATTENDEEs
boolean hasAttendees = false;
for (Iterator<Element> iter = element.elementIterator(MailConstants.E_CAL_ATTENDEE); iter.hasNext(); ) {
ZAttendee at = ZAttendee.parse(iter.next());
newInv.addAttendee(at);
hasAttendees = true;
}
if (hasAttendees && newInv.getMethod().equals(ICalTok.PUBLISH.toString())) {
newInv.setMethod(ICalTok.REQUEST.toString());
}
// ORGANIZER
Element orgElt = element.getOptionalElement(MailConstants.E_CAL_ORGANIZER);
if (orgElt != null) {
ZOrganizer org = ZOrganizer.parse(orgElt);
newInv.setOrganizer(org);
}
// Once we have organizer and attendee information, we can tell if this account is the
// organizer in this invite or not.
newInv.setIsOrganizer(account);
if (!newInv.isCancel()) {
// draft flag
// True means invite has changes that haven't been sent to attendees.
boolean draft = element.getAttributeBool(MailConstants.A_CAL_DRAFT, false);
newInv.setDraft(draft);
// neverSent flag
// True means attendees have never been notified for this invite.
boolean neverSent = element.getAttributeBool(MailConstants.A_CAL_NEVER_SENT, false);
newInv.setNeverSent(neverSent);
}
// RECUR
Element recur = element.getOptionalElement(MailConstants.A_CAL_RECUR);
if (recur != null) {
if (!recurAllowed) {
throw ServiceException.INVALID_REQUEST("No <recur> allowed in an exception", null);
}
// Ensure DTSTART is set if doing recurrence.
ParsedDateTime st = newInv.getStartTime();
if (st == null) {
ParsedDateTime et = newInv.getEndTime();
if (et != null) {
if (et.hasTime())
st = et.add(ParsedDuration.NEGATIVE_ONE_SECOND);
else
st = et.add(ParsedDuration.NEGATIVE_ONE_DAY);
newInv.setDtStart(st);
} else {
// Both DTSTART and DTEND are unspecified. Recurrence makes no sense!
throw ServiceException.INVALID_REQUEST("recurrence used without DTSTART", null);
}
}
Recurrence.IRecurrence recurrence = parseRecur(recur, tzMap, newInv.getStartTime(), newInv.getEndTime(), newInv.getDuration(), newInv.getRecurId());
newInv.setRecurrence(recurrence);
}
// VALARMs
Iterator<Element> alarmsIter = element.elementIterator(MailConstants.E_CAL_ALARM);
while (alarmsIter.hasNext()) {
Alarm alarm = Alarm.parse(alarmsIter.next());
if (alarm != null)
newInv.addAlarm(alarm);
}
List<ZProperty> xprops = parseXProps(element);
for (ZProperty prop : xprops) newInv.addXProp(prop);
newInv.validateDuration();
//zdsync: must set this only after recur is processed
if (invId != null) {
try {
int invIdInt = Integer.parseInt(invId);
newInv.setInviteId(invIdInt);
} catch (NumberFormatException e) {
// ignore if invId is not a number, e.g. refers to a remote account
}
}
if (dts != null) {
newInv.setDtStamp(Long.parseLong(dts));
}
Element fragment = element.getOptionalElement(MailConstants.E_FRAG);
if (fragment != null) {
newInv.setFragment(fragment.getText());
}
}
use of com.zimbra.cs.mailbox.calendar.Alarm in project zm-mailbox by Zimbra.
the class ToXML method encodeInviteComponent.
public static Element encodeInviteComponent(Element parent, ItemIdFormatter ifmt, OperationContext octxt, CalendarItem calItem, /* may be null */
ItemId calId, /* may be null */
Invite invite, int fields, boolean neuter) throws ServiceException {
boolean allFields = true;
if (fields != NOTIFY_FIELDS) {
allFields = false;
if (!needToOutput(fields, Change.INVITE)) {
return parent;
}
}
Element e = parent.addElement(MailConstants.E_INVITE_COMPONENT);
e.addAttribute(MailConstants.A_CAL_METHOD, invite.getMethod());
e.addAttribute(MailConstants.A_CAL_COMPONENT_NUM, invite.getComponentNum());
e.addAttribute(MailConstants.A_CAL_RSVP, invite.getRsvp());
boolean allowPrivateAccess = calItem != null ? allowPrivateAccess(octxt, calItem) : true;
if (allFields) {
if (invite.isPublic() || allowPrivateAccess) {
String priority = invite.getPriority();
if (priority != null) {
e.addAttribute(MailConstants.A_CAL_PRIORITY, priority);
}
e.addAttribute(MailConstants.A_NAME, invite.getName());
e.addAttribute(MailConstants.A_CAL_LOCATION, invite.getLocation());
List<String> categories = invite.getCategories();
if (categories != null) {
for (String cat : categories) {
e.addElement(MailConstants.E_CAL_CATEGORY).setText(cat);
}
}
List<String> comments = invite.getComments();
if (comments != null) {
for (String cmt : comments) {
e.addElement(MailConstants.E_CAL_COMMENT).setText(cmt);
}
}
List<String> contacts = invite.getContacts();
if (contacts != null) {
for (String contact : contacts) {
e.addElement(MailConstants.E_CAL_CONTACT).setText(contact);
}
}
Geo geo = invite.getGeo();
if (geo != null) {
geo.toXml(e);
}
// Percent Complete (VTODO)
if (invite.isTodo()) {
String pct = invite.getPercentComplete();
if (pct != null)
e.addAttribute(MailConstants.A_TASK_PERCENT_COMPLETE, pct);
long completed = invite.getCompleted();
if (completed != 0) {
ParsedDateTime c = ParsedDateTime.fromUTCTime(completed);
e.addAttribute(MailConstants.A_TASK_COMPLETED, c.getDateTimePartString());
}
}
// Attendee(s)
List<ZAttendee> attendees = invite.getAttendees();
for (ZAttendee at : attendees) {
at.toXml(e);
}
// Alarms
Iterator<Alarm> alarmsIter = invite.alarmsIterator();
while (alarmsIter.hasNext()) {
Alarm alarm = alarmsIter.next();
alarm.toXml(e);
}
// x-prop
encodeXProps(e, invite.xpropsIterator());
// fragment
String fragment = invite.getFragment();
if (!Strings.isNullOrEmpty(fragment)) {
e.addAttribute(MailConstants.E_FRAG, fragment, Element.Disposition.CONTENT);
}
if (!invite.hasBlobPart()) {
e.addAttribute(MailConstants.A_CAL_NO_BLOB, true);
}
// Description (plain and html)
String desc = invite.getDescription();
if (desc != null) {
Element descElem = e.addElement(MailConstants.E_CAL_DESCRIPTION);
descElem.setText(desc);
}
String descHtml = invite.getDescriptionHtml();
BrowserDefang defanger = DefangFactory.getDefanger(MimeConstants.CT_TEXT_HTML);
if (descHtml != null) {
try {
descHtml = StringUtil.stripControlCharacters(descHtml);
descHtml = defanger.defang(descHtml, neuter);
Element descHtmlElem = e.addElement(MailConstants.E_CAL_DESC_HTML);
descHtmlElem.setText(descHtml);
} catch (IOException ex) {
ZimbraLog.calendar.warn("Unable to defang HTML for SetAppointmentRequest", ex);
}
}
if (invite.isEvent()) {
if (calItem != null && calItem instanceof Appointment) {
Instance inst = Instance.fromInvite(calItem.getId(), invite);
Appointment appt = (Appointment) calItem;
e.addAttribute(MailConstants.A_APPT_FREEBUSY_ACTUAL, appt.getEffectiveFreeBusyActual(invite, inst));
}
e.addAttribute(MailConstants.A_APPT_FREEBUSY, invite.getFreeBusy());
e.addAttribute(MailConstants.A_APPT_TRANSPARENCY, invite.getTransparency());
}
// Organizer
if (invite.hasOrganizer()) {
ZOrganizer org = invite.getOrganizer();
org.toXml(e);
}
e.addAttribute(MailConstants.A_CAL_URL, invite.getUrl());
}
if (invite.isOrganizer()) {
e.addAttribute(MailConstants.A_CAL_ISORG, true);
}
boolean isRecurring = false;
e.addAttribute("x_uid", invite.getUid());
e.addAttribute(MailConstants.A_UID, invite.getUid());
e.addAttribute(MailConstants.A_CAL_SEQUENCE, invite.getSeqNo());
//zdsync
e.addAttribute(MailConstants.A_CAL_DATETIME, invite.getDTStamp());
String itemId = null;
if (calId != null) {
itemId = calId.toString(ifmt);
} else if (calItem != null) {
itemId = ifmt.formatItemId(calItem);
}
if ((itemId != null) && !("0".equals(itemId))) {
e.addAttribute(MailConstants.A_CAL_ID, /* calItemId */
itemId);
if (invite.isEvent()) {
// for backward compat
e.addAttribute(MailConstants.A_APPT_ID_DEPRECATE_ME, /* apptId */
itemId);
}
if (calItem != null) {
ItemId ciFolderId = new ItemId(calItem.getMailbox(), calItem.getFolderId());
e.addAttribute(MailConstants.A_CAL_ITEM_FOLDER, /* ciFolder */
ifmt.formatItemId(ciFolderId));
}
}
Recurrence.IRecurrence recur = invite.getRecurrence();
if (recur != null) {
isRecurring = true;
Element recurElt = e.addElement(MailConstants.E_CAL_RECUR);
recur.toXml(recurElt);
}
e.addAttribute(MailConstants.A_CAL_STATUS, invite.getStatus());
e.addAttribute(MailConstants.A_CAL_CLASS, invite.getClassProp());
boolean allDay = invite.isAllDayEvent();
boolean isException = invite.hasRecurId();
if (isException) {
e.addAttribute(MailConstants.A_CAL_IS_EXCEPTION, true);
RecurId rid = invite.getRecurId();
e.addAttribute(MailConstants.A_CAL_RECURRENCE_ID_Z, rid.getDtZ());
encodeRecurId(e, rid, allDay);
}
boolean forceUTC = DebugConfig.calendarForceUTC && !isRecurring && !isException && !allDay;
ParsedDateTime dtStart = invite.getStartTime();
if (dtStart != null) {
encodeDtStart(e, dtStart, allDay, forceUTC);
}
ParsedDateTime dtEnd = invite.getEndTime();
if (dtEnd != null) {
encodeDtEnd(e, dtEnd, allDay, invite.isTodo(), forceUTC);
}
ParsedDuration dur = invite.getDuration();
if (dur != null) {
dur.toXml(e);
}
if (allDay) {
e.addAttribute(MailConstants.A_CAL_ALLDAY, true);
}
if (invite.isDraft()) {
e.addAttribute(MailConstants.A_CAL_DRAFT, true);
}
if (invite.isNeverSent()) {
e.addAttribute(MailConstants.A_CAL_NEVER_SENT, true);
}
}
return e;
}
use of com.zimbra.cs.mailbox.calendar.Alarm in project zm-mailbox by Zimbra.
the class ToXML method alarmDataToJaxb.
public static AlarmDataInfo alarmDataToJaxb(CalendarItem calItem, AlarmData alarmData) {
AlarmDataInfo alarm = new AlarmDataInfo();
long nextAlarm = getNextAlarmTime(calItem);
if (nextAlarm < Long.MAX_VALUE) {
alarm.setNextAlarm(nextAlarm);
}
long alarmInstStart = alarmData.getNextInstanceStart();
if (alarmInstStart != 0) {
alarm.setAlarmInstanceStart(alarmInstStart);
}
int alarmInvId = alarmData.getInvId();
int alarmCompNum = alarmData.getCompNum();
Invite alarmInv = calItem.getInvite(alarmInvId, alarmCompNum);
if (alarmInv != null) {
// Some info on the meeting instance the reminder is for.
// These allow the UI to display tooltip and issue a Get
// call on the correct meeting instance.
alarm.setName(alarmInv.getName());
alarm.setLocation(alarmInv.getLocation());
alarm.setInvId(alarmInvId);
alarm.setComponentNum(alarmCompNum);
}
Alarm alarmObj = alarmData.getAlarm();
if (alarmObj != null) {
alarm.setAlarm(alarmObj.toJaxb());
}
return alarm;
}
use of com.zimbra.cs.mailbox.calendar.Alarm in project zm-mailbox by Zimbra.
the class CalItemReminderService method scheduleNextReminders.
/**
* Schedules next reminders for the calendar item.
*
* @param calItem
* @param email
* @param sms
*/
static void scheduleNextReminders(CalendarItem calItem, boolean email, boolean sms) {
try {
CalendarItem.AlarmData alarmData = calItem.getNextEmailAlarm();
if (alarmData == null)
return;
boolean emailAlarmExists = true;
boolean smsAlarmExists = false;
Alarm emailAlarm = alarmData.getAlarm();
List<ZAttendee> recipients = emailAlarm.getAttendees();
if (recipients != null && !recipients.isEmpty()) {
emailAlarmExists = false;
Account acct = calItem.getAccount();
String defaultEmailAddress = acct.getPrefCalendarReminderEmail();
String defaultDeviceAddress = acct.getCalendarReminderDeviceEmail();
for (ZAttendee recipient : recipients) {
if (recipient.getAddress().equals(defaultEmailAddress)) {
emailAlarmExists = true;
}
if (recipient.getAddress().equals(defaultDeviceAddress)) {
smsAlarmExists = true;
}
}
}
if (emailAlarmExists && email) {
scheduleReminder(new CalItemEmailReminderTask(), calItem, alarmData);
}
if (smsAlarmExists && sms) {
scheduleReminder(new CalItemSmsReminderTask(), calItem, alarmData);
}
} catch (ServiceException e) {
ZimbraLog.scheduler.error("Error in scheduling reminder task", e);
}
}
use of com.zimbra.cs.mailbox.calendar.Alarm 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