use of com.zimbra.cs.mailbox.calendar.RecurId in project zm-mailbox by Zimbra.
the class CalendarCollection method createItemFromInvites.
/**
* @param name Preferred DAV basename for the new item - including ".ics" if appropriate.
* @param allowUpdate - PUTs are allowed to update a pre-existing item. POSTs to the containing collection are not.
*/
public DavResource createItemFromInvites(DavContext ctxt, Account account, String name, List<Invite> invites, boolean allowUpdate) throws DavException, IOException {
boolean useEtag = allowUpdate;
try {
String user = account.getName();
/*
* Some of the CalDAV clients do not behave very well when it comes to etags.
* chandler doesn't set User-Agent header, doesn't understand If-None-Match or If-Match headers.
* evolution 2.8 always sets If-None-Match although we return etag in REPORT.
* ical correctly understands etag and sets If-Match for existing etags, but does not use If-None-Match
* for new resource creation.
*/
HttpServletRequest req = ctxt.getRequest();
String etag = null;
if (useEtag) {
etag = req.getHeader(DavProtocol.HEADER_IF_MATCH);
useEtag = (etag != null);
}
String baseName = HttpUtil.urlUnescape(name);
boolean acceptableClientChosenBasename = DebugConfig.enableDAVclientCanChooseResourceBaseName && baseName.equals(name);
if (name.endsWith(CalendarObject.CAL_EXTENSION)) {
name = name.substring(0, name.length() - CalendarObject.CAL_EXTENSION.length());
// Unescape the name (It was encoded in DavContext intentionally)
name = HttpUtil.urlUnescape(name);
}
String uid = findEventUid(invites);
Mailbox mbox = MailboxManager.getInstance().getMailboxByAccount(account);
CalendarItem origCalItem = null;
// Is the basename of the path client assigned rather than following the standard pattern?
Integer itemId = null;
if (acceptableClientChosenBasename) {
itemId = DavNames.get(this.mMailboxId, this.mId, baseName);
}
if (itemId != null) {
try {
MailItem mailItem = mbox.getItemById(ctxt.getOperationContext(), itemId, MailItem.Type.UNKNOWN);
if (mailItem != null && mailItem instanceof CalendarItem) {
origCalItem = (CalendarItem) mailItem;
}
} catch (ServiceException se) {
}
}
if (origCalItem == null) {
if (uid.equals(name)) {
origCalItem = mbox.getCalendarItemByUid(ctxt.getOperationContext(), name);
} else {
/* the basename of the path doesn't fit our preferred naming convention. */
origCalItem = mbox.getCalendarItemByUid(ctxt.getOperationContext(), uid);
String redirectUrl = null;
if (origCalItem != null) {
if (this.mId != origCalItem.getFolderId()) {
// In another folder, ignore
origCalItem = null;
} else {
// The item exists, but doesn't have this name - UID conflict.
if (acceptableClientChosenBasename) {
redirectUrl = hrefForCalendarItem(origCalItem, user, uid);
} else {
redirectUrl = defaultUrlForCalendarItem(user, uid);
}
throw new DavException.UidConflict("An item with the same UID already exists in the calendar", redirectUrl);
}
}
if ((origCalItem == null) && (!DebugConfig.enableDAVclientCanChooseResourceBaseName)) {
redirectUrl = defaultUrlForCalendarItem(user, uid);
}
if (allowUpdate && (redirectUrl != null)) {
/* SC_FOUND - Status code (302) indicating that the resource reside temporarily under a
* different URI. Since the redirection might be altered on occasion, the client should
* continue to use the Request-URI for future requests.(HTTP/1.1) To represent the status code
* (302), it is recommended to use this variable. Used to be called SC_MOVED_TEMPORARILY
*/
// sets status to SC_FOUND
ctxt.getResponse().sendRedirect(redirectUrl);
StringBuilder wrongUrlMsg = new StringBuilder();
wrongUrlMsg.append("wrong url - redirecting to:\n").append(redirectUrl);
throw new DavException(wrongUrlMsg.toString(), HttpServletResponse.SC_FOUND, null);
}
}
}
if (origCalItem == null && useEtag) {
throw new DavException("event not found", HttpServletResponse.SC_NOT_FOUND, null);
}
if (origCalItem != null && !allowUpdate) {
throw new DavException.UidConflict("An item with the same UID already exists in the calendar", hrefForCalendarItem(origCalItem, user, uid));
}
boolean isNewItem = true;
if (useEtag) {
String itemEtag = MailItemResource.getEtag(origCalItem);
if (!itemEtag.equals(etag)) {
throw new DavException(String.format("CalDAV client has stale event: event has different etag (%s) vs %s", itemEtag, etag), HttpServletResponse.SC_PRECONDITION_FAILED);
}
isNewItem = false;
}
// prepare to call Mailbox.setCalendarItem()
int flags = 0;
String[] tags = null;
List<ReplyInfo> replies = null;
Invite[] origInvites = null;
if (origCalItem != null) {
flags = origCalItem.getFlagBitmask();
tags = origCalItem.getTags();
replies = origCalItem.getAllReplies();
origInvites = origCalItem.getInvites();
}
SetCalendarItemData scidDefault = new SetCalendarItemData();
SetCalendarItemData[] scidExceptions = null;
int idxExceptions = 0;
boolean first = true;
for (Invite i : invites) {
// check for valid uid.
if (i.getUid() == null)
i.setUid(uid);
adjustOrganizer(ctxt, i);
// Carry over the MimeMessage/ParsedMessage to preserve any attachments.
// CalDAV clients don't support attachments, and on edit we have to either
// retain existing attachments or drop them. Retaining is better.
ParsedMessage oldPm = null;
if (origCalItem != null) {
Invite oldInv = origCalItem.getInvite(i.getRecurId());
if (oldInv == null && i.hasRecurId()) {
// It's a new exception instance. Inherit from series.
oldInv = origCalItem.getInvite((RecurId) null);
}
if (oldInv != null) {
MimeMessage mmInv = origCalItem.getSubpartMessage(oldInv.getMailItemId());
oldPm = mmInv != null ? new ParsedMessage(mmInv, false) : null;
}
}
if (first) {
scidDefault.invite = i;
scidDefault.message = oldPm;
first = false;
} else {
SetCalendarItemData scid = new SetCalendarItemData();
scid.invite = i;
scid.message = oldPm;
if (scidExceptions == null) {
scidExceptions = new SetCalendarItemData[invites.size() - 1];
}
scidExceptions[idxExceptions++] = scid;
}
// For attendee case, update replies list with matching ATTENDEE from the invite.
if (!i.isOrganizer() && replies != null) {
ZAttendee at = i.getMatchingAttendee(account);
if (at != null) {
AccountAddressMatcher acctMatcher = new AccountAddressMatcher(account);
ReplyInfo newReply = null;
for (Iterator<ReplyInfo> replyIter = replies.iterator(); replyIter.hasNext(); ) {
ReplyInfo reply = replyIter.next();
if (acctMatcher.matches(reply.getAttendee().getAddress())) {
RecurId ridR = reply.getRecurId(), ridI = i.getRecurId();
if ((ridR == null && ridI == null) || (ridR != null && ridR.equals(ridI))) {
// matching RECURRENCE-ID
// No need to compare SEQUENCE and DTSTAMP of existing reply and new invite.
// We're just going to take what the caldav client sent, even if it's older
// than the existing reply.
replyIter.remove();
if (!IcalXmlStrMap.PARTSTAT_NEEDS_ACTION.equalsIgnoreCase(at.getPartStat())) {
newReply = new ReplyInfo(at, i.getSeqNo(), i.getDTStamp(), ridI);
}
break;
}
}
}
if (newReply != null) {
replies.add(newReply);
}
}
}
}
CalendarItem newCalItem = null;
AutoScheduler autoScheduler = AutoScheduler.getAutoScheduler(mbox, this.getCalendarMailbox(ctxt), origInvites, mId, flags, tags, scidDefault, scidExceptions, replies, ctxt);
if (autoScheduler == null) {
newCalItem = mbox.setCalendarItem(ctxt.getOperationContext(), mId, flags, tags, scidDefault, scidExceptions, replies, CalendarItem.NEXT_ALARM_KEEP_CURRENT);
} else {
// Note: This also sets the calendar item
newCalItem = autoScheduler.doSchedulingActions();
}
if (newCalItem == null) {
throw new DavException("cannot create icalendar item - corrupt ICAL?", HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
}
if (!uid.equals(name)) {
if (acceptableClientChosenBasename) {
DavNames.put(DavNames.DavName.create(this.mMailboxId, newCalItem.getFolderId(), baseName), newCalItem.getId());
}
}
return new CalendarObject.LocalCalendarObject(ctxt, newCalItem, isNewItem);
} catch (BadOrganizerException.DiffOrganizerInComponentsException e) {
throw new DavException.NeedSameOrganizerInAllComponents(e.getMessage());
} catch (BadOrganizerException e) {
// FORBIDDEN if we aren't going to be able to cope with the data
throw new DavException(e.getMessage(), HttpServletResponse.SC_FORBIDDEN, e);
} catch (ServiceException e) {
if (e.getCode().equals(ServiceException.FORBIDDEN)) {
throw new DavException(e.getMessage(), HttpServletResponse.SC_FORBIDDEN, e);
} else {
throw new DavException("cannot create icalendar item", HttpServletResponse.SC_INTERNAL_SERVER_ERROR, e);
}
}
}
use of com.zimbra.cs.mailbox.calendar.RecurId 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.RecurId in project zm-mailbox by Zimbra.
the class AddCalendarItemInvite 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);
AddInviteParser parser = new AddInviteParser();
SetCalendarItemData scid = SetCalendarItem.getSetCalendarItemData(zsc, octxt, acct, mbox, request, parser);
Invite inv = scid.invite;
CalendarItem calItem = mbox.getCalendarItemByUid(octxt, inv.getUid());
int folderId = inv.isTodo() ? Mailbox.ID_FOLDER_TASKS : Mailbox.ID_FOLDER_CALENDAR;
if (calItem != null) {
int f = calItem.getFolderId();
if (f != Mailbox.ID_FOLDER_TRASH && f != Mailbox.ID_FOLDER_SPAM)
folderId = f;
}
// it's the correct organizer.
if (!inv.hasOrganizer() && inv.hasOtherAttendees()) {
if (scid.message == null) {
ZimbraLog.calendar.info("Got malformed invite without organizer. Clearing attendees to prevent inadvertent cancels.");
inv.clearAttendees();
} else {
String fromEmail = scid.message.getSenderEmail(true);
if (fromEmail != null) {
boolean dangerousSender = false;
// Is sender == recipient? If so, clear attendees.
String intendedForAddress;
try {
intendedForAddress = scid.message.getMimeMessage().getHeader(CalendarMailSender.X_ZIMBRA_CALENDAR_INTENDED_FOR, null);
} catch (MessagingException e) {
throw ServiceException.FAILURE("error parsing message", e);
}
if (intendedForAddress != null && intendedForAddress.length() > 0) {
if (intendedForAddress.equalsIgnoreCase(fromEmail)) {
ZimbraLog.calendar.info("Got malformed invite without organizer. Clearing attendees to prevent inadvertent cancels.");
inv.clearAttendees();
dangerousSender = true;
}
} else if (AccountUtil.addressMatchesAccount(acct, fromEmail)) {
ZimbraLog.calendar.info("Got malformed invite without organizer. Clearing attendees to prevent inadvertent cancels.");
inv.clearAttendees();
dangerousSender = true;
}
if (!dangerousSender) {
ZOrganizer org = new ZOrganizer(fromEmail, null);
String senderEmail = scid.message.getSenderEmail(false);
if (senderEmail != null && !senderEmail.equalsIgnoreCase(fromEmail))
org.setSentBy(senderEmail);
inv.setOrganizer(org);
ZimbraLog.calendar.info("Got malformed invite that lists attendees without specifying an organizer. " + "Defaulting organizer to: " + org.toString());
}
}
}
}
// trace logging
String calItemIdStr = calItem != null ? Integer.toString(calItem.getId()) : "(new)";
if (!inv.hasRecurId())
ZimbraLog.calendar.info("<AddCalendarItemInvite> id=%s, folderId=%d, subject=\"%s\", UID=%s", calItemIdStr, folderId, inv.isPublic() ? inv.getName() : "(private)", inv.getUid());
else
ZimbraLog.calendar.info("<AddCalendarItemInvite> id=%s, folderId=%d, subject=\"%s\", UID=%s, recurId=%s", calItemIdStr, folderId, inv.isPublic() ? inv.getName() : "(private)", inv.getUid(), inv.getRecurId().getDtZ());
Element response = getResponseElement(zsc);
if (calItem != null) {
// If the calendar item already has the invite, no need to add again.
RecurId rid = scid.invite.getRecurId();
Invite matchingInv = calItem.getInvite(rid);
if (matchingInv != null && matchingInv.isSameOrNewerVersion(scid.invite)) {
response.addAttribute(MailConstants.A_CAL_ID, calItem.getId());
response.addAttribute(MailConstants.A_CAL_INV_ID, matchingInv.getMailItemId());
response.addAttribute(MailConstants.A_CAL_COMPONENT_NUM, matchingInv.getComponentNum());
return response;
}
}
AddInviteData aid = mbox.addInvite(octxt, inv, folderId, scid.message, false, false, true);
if (aid != null) {
calItem = mbox.getCalendarItemById(octxt, aid.calItemId);
if (calItem != null) {
Invite[] invs = calItem.getInvites(aid.invId);
if (invs != null && invs.length > 0) {
response.addAttribute(MailConstants.A_CAL_ID, aid.calItemId);
response.addAttribute(MailConstants.A_CAL_INV_ID, aid.invId);
response.addAttribute(MailConstants.A_CAL_COMPONENT_NUM, invs[0].getComponentNum());
}
}
}
return response;
}
use of com.zimbra.cs.mailbox.calendar.RecurId in project zm-mailbox by Zimbra.
the class SendInviteReply method handle.
@Override
public Element handle(Element request, Map<String, Object> context) throws ServiceException {
ZimbraSoapContext zsc = getZimbraSoapContext(context);
Mailbox mbox = getRequestedMailbox(zsc);
Account acct = getRequestedAccount(zsc);
Account authAcct = getAuthenticatedAccount(zsc);
boolean isAdmin = zsc.isUsingAdminPrivileges();
OperationContext octxt = getOperationContext(zsc, context);
boolean onBehalfOf = isOnBehalfOfRequest(zsc);
ItemId iid = new ItemId(request.getAttribute(MailConstants.A_ID), zsc);
int compNum = (int) request.getAttributeLong(MailConstants.A_CAL_COMPONENT_NUM);
String verbStr = request.getAttribute(MailConstants.A_VERB);
Verb verb = CalendarMailSender.parseVerb(verbStr);
boolean isDecline = CalendarMailSender.VERB_DECLINE.equals(verb);
boolean updateOrg = request.getAttributeBool(MailConstants.A_CAL_UPDATE_ORGANIZER, true);
// Get the identity/persona being used in the reply. It is set at the request level, but
// let's also look for it in the <m> child element too, because that is the precedent in
// the SendMsg request. For SendInviteReply we have to insist it at request level because
// <m> is an optional element.
String identityId = request.getAttribute(MailConstants.A_IDENTITY_ID, null);
if (identityId == null) {
Element msgElem = request.getOptionalElement(MailConstants.E_MSG);
if (msgElem != null)
identityId = msgElem.getAttribute(MailConstants.A_IDENTITY_ID, null);
}
Element response = getResponseElement(zsc);
boolean intendedForMe = true;
Invite oldInv = null;
int calItemId;
int inviteMsgId;
CalendarItem calItem = null;
boolean wasInTrash = false;
// calendar item (id="aaaa-nnnn") --- work in both cases
if (iid.hasSubpart()) {
// directly accepting the calendar item
calItemId = iid.getId();
inviteMsgId = iid.getSubpartId();
calItem = safeGetCalendarItemById(mbox, octxt, iid.getId());
if (calItem == null)
throw MailServiceException.NO_SUCH_CALITEM(iid.toString(), "Could not find calendar item");
oldInv = calItem.getInvite(inviteMsgId, compNum);
} else {
// accepting the message: go find the calendar item and then the invite
inviteMsgId = iid.getId();
Message msg = mbox.getMessageById(octxt, inviteMsgId);
Message.CalendarItemInfo info = msg.getCalendarItemInfo(compNum);
if (info == null)
throw MailServiceException.NO_SUCH_CALITEM(iid.toString(), "Could not find calendar item");
String intendedFor = msg.getCalendarIntendedFor();
Account intendedAcct = null;
if (intendedFor != null) {
try {
InternetAddress intendedForAddr = new JavaMailInternetAddress(intendedFor);
intendedAcct = Provisioning.getInstance().get(AccountBy.name, intendedForAddr.getAddress());
} catch (AddressException e) {
throw ServiceException.INVALID_REQUEST("The intended account " + intendedFor + " is invalid", e);
}
if (intendedAcct == null) {
throw ServiceException.INVALID_REQUEST("The intended account " + intendedFor + " was not found", null);
}
// Special case: intended account = me.
if (intendedAcct.equals(mbox.getAccount()))
intendedAcct = null;
else
intendedForMe = false;
}
if (intendedAcct != null) {
// trace logging: let's just indicate we're replying to a remote appointment
ZimbraLog.calendar.info("<SendInviteReply> (remote mbox) id=%s, verb=%s, notifyOrg=%s", new ItemIdFormatter(zsc).formatItemId(iid), verb.toString(), Boolean.toString(updateOrg));
// Replying to a remote appointment
calItem = null;
calItemId = 0;
ZMailbox zmbx = getRemoteZMailbox(octxt, authAcct, intendedAcct);
// Try to add the appointment to remote mailbox.
AddInviteResult addInviteResult = sendAddInvite(zmbx, octxt, msg);
if (addInviteResult == null)
throw MailServiceException.INVITE_OUT_OF_DATE(iid.toString());
// Forward the reply request.
remoteSendInviteReply(zmbx, request, addInviteResult);
} else {
// Replying to a local appointment
if (info.getInvite() != null) {
calItem = mbox.getCalendarItemByUid(octxt, info.getInvite().getUid());
wasInTrash = calItem != null && calItem.inTrash();
if (calItem != null && !wasInTrash) {
Invite newInv = info.getInvite();
// If appointment exists, check if our invite has been outdated.
Invite curr = calItem.getInvite(newInv.getRecurId());
if (curr != null && !newInv.isSameOrNewerVersion(curr))
throw MailServiceException.INVITE_OUT_OF_DATE(iid.toString());
}
Invite inv = info.getInvite().newCopy();
Invite.setDefaultAlarm(inv, acct);
inv.setMailItemId(inviteMsgId);
// (TODO: Is it better to delete the existing appointment/instance when declining?)
if (calItem != null || !isDecline) {
// Add the invite. This will either create or update the appointment.
int folder;
boolean untrashing = wasInTrash && !isDecline;
if (calItem == null || untrashing) {
// If appointment/task doesn't exist, create in default folder.
// If it exists but is in Trash and is not a decline, move it out of Trash.
// If it's in trash and we're declining, leave it in trash.
folder = inv.isTodo() ? Mailbox.ID_FOLDER_TASKS : Mailbox.ID_FOLDER_CALENDAR;
} else {
folder = calItem.getFolderId();
}
ParsedMessage pm = new ParsedMessage(msg.getMimeMessage(false), false);
AddInviteData aid = mbox.addInvite(octxt, inv, folder, pm, false, untrashing, true);
if (aid == null)
throw ServiceException.FAILURE("Could not create/update calendar item", null);
calItemId = aid.calItemId;
// Refetch updated item.
calItem = safeGetCalendarItemById(mbox, octxt, aid.calItemId);
if (calItem == null)
throw ServiceException.FAILURE("Could not refetch created/updated calendar item", null);
} else {
calItemId = 0;
}
oldInv = inv;
} else if (info.calItemCreated()) {
// legacy case (before we added Invite info to Message metadata)
calItem = safeGetCalendarItemById(mbox, octxt, info.getCalendarItemId());
if (calItem == null)
throw ServiceException.FAILURE("Missing invite data", null);
// Must be for this mailbox
calItemId = info.getCalendarItemId().getId();
wasInTrash = calItem.inTrash();
oldInv = calItem.getInvite(inviteMsgId, compNum);
} else {
throw ServiceException.FAILURE("Missing invite data", null);
}
}
}
if (intendedForMe) {
if (oldInv == null)
throw MailServiceException.INVITE_OUT_OF_DATE(iid.toString());
if (calItem != null && (mbox.getEffectivePermissions(octxt, calItemId, MailItem.Type.UNKNOWN) & ACL.RIGHT_ACTION) == 0) {
throw ServiceException.PERM_DENIED("You do not have ACTION rights for CalendarItem " + calItemId);
}
// check if invite organizer requested rsvp or not
updateOrg = updateOrg && oldInv.getRsvp();
// Don't allow creating/editing a private appointment on behalf of another user,
// unless that other user is a calendar resource.
boolean allowPrivateAccess = calItem != null ? calItem.allowPrivateAccess(authAcct, isAdmin) : true;
boolean isCalendarResource = acct instanceof CalendarResource;
if (!allowPrivateAccess && !oldInv.isPublic() && !isCalendarResource)
throw ServiceException.PERM_DENIED("Cannot reply to a private appointment/task on behalf of another user");
// see if there is a specific Exception being referenced by this reply...
Element exc = request.getOptionalElement(MailConstants.E_CAL_EXCEPTION_ID);
ParsedDateTime exceptDt = null;
if (exc != null) {
TimeZoneMap tzmap = oldInv.getTimeZoneMap();
Element tzElem = request.getOptionalElement(MailConstants.E_CAL_TZ);
ICalTimeZone tz = null;
if (tzElem != null) {
tz = CalendarUtils.parseTzElement(tzElem);
tzmap.add(tz);
}
exceptDt = CalendarUtils.parseDateTime(exc, tzmap);
} else if (oldInv.hasRecurId()) {
exceptDt = oldInv.getRecurId().getDt();
}
// trace logging
String calItemIdStr = calItem != null ? Integer.toString(calItem.getId()) : "none";
String folderIdStr = calItem != null ? Integer.toString(calItem.getFolderId()) : "none";
if (exceptDt == null)
ZimbraLog.calendar.info("<SendInviteReply> id=%s, folderId=%s, verb=%s, notifyOrg=%s, subject=\"%s\", UID=%s", calItemIdStr, folderIdStr, verb.toString(), Boolean.toString(updateOrg), oldInv.isPublic() ? oldInv.getName() : "(private)", oldInv.getUid());
else
ZimbraLog.calendar.info("<SendInviteReply> id=%s, folderId=%s, verb=%s, notifyOrg=%s, subject=\"%s\", UID=%s, recurId=%s", calItemIdStr, folderIdStr, verb.toString(), Boolean.toString(updateOrg), oldInv.isPublic() ? oldInv.getName() : "(private)", oldInv.getUid(), exceptDt.getUtcString());
// exception instance first. Then reply to it.
if (calItem != null && oldInv.isRecurrence() && exceptDt != null) {
Invite localException = oldInv.makeInstanceInvite(exceptDt);
long now = octxt != null ? octxt.getTimestamp() : System.currentTimeMillis();
localException.setDtStamp(now);
String partStat = verb.getXmlPartStat();
localException.setPartStat(partStat);
ZAttendee at = localException.getMatchingAttendee(acct, identityId);
if (at != null)
at.setPartStat(partStat);
// Carry over the MimeMessage/ParsedMessage to preserve any attachments.
MimeMessage mmInv = calItem.getSubpartMessage(oldInv.getMailItemId());
ParsedMessage pm = mmInv != null ? new ParsedMessage(mmInv, false) : null;
int folder;
boolean untrashing = wasInTrash && !isDecline;
if (untrashing) {
// If it exists but is in Trash and is not a decline, move it out of Trash.
// If it's in trash and we're declining, leave it in trash.
folder = localException.isTodo() ? Mailbox.ID_FOLDER_TASKS : Mailbox.ID_FOLDER_CALENDAR;
} else {
folder = calItem.getFolderId();
}
mbox.addInvite(octxt, localException, folder, pm, true, untrashing, true);
// Refetch the updated calendar item and set oldInv to refetched local exception instance.
calItem = safeGetCalendarItemById(mbox, octxt, calItemId);
if (calItem == null)
throw MailServiceException.NO_SUCH_CALITEM(iid.toString(), "Could not find calendar item");
oldInv = calItem.getInvite(new RecurId(exceptDt, RecurId.RANGE_NONE));
}
if (updateOrg && oldInv.hasOrganizer()) {
Locale locale;
Account organizer = oldInv.getOrganizerAccount();
if (organizer != null)
locale = organizer.getLocale();
else
locale = !onBehalfOf ? acct.getLocale() : authAcct.getLocale();
String subject;
if (!allowPrivateAccess && !oldInv.isPublic())
subject = L10nUtil.getMessage(MsgKey.calendarSubjectWithheld, locale);
else
subject = oldInv.getName();
String replySubject = CalendarMailSender.getReplySubject(verb, subject, locale);
CalSendData csd = new CalSendData();
csd.mOrigId = new ItemId(mbox, oldInv.getMailItemId());
csd.mReplyType = MailSender.MSGTYPE_REPLY;
csd.mInvite = CalendarMailSender.replyToInvite(acct, identityId, authAcct, onBehalfOf, allowPrivateAccess, oldInv, verb, replySubject, exceptDt);
ZVCalendar iCal = csd.mInvite.newToICalendar(true);
ParseMimeMessage.MimeMessageData parsedMessageData = new ParseMimeMessage.MimeMessageData();
// did they specify a custom <m> message? If so, then we don't have to build one...
Element msgElem = request.getOptionalElement(MailConstants.E_MSG);
if (msgElem != null) {
String text = ParseMimeMessage.getTextPlainContent(msgElem);
String html = ParseMimeMessage.getTextHtmlContent(msgElem);
iCal.addDescription(text, html);
MimeBodyPart[] mbps = new MimeBodyPart[1];
mbps[0] = CalendarMailSender.makeICalIntoMimePart(iCal);
// the <inv> element is *NOT* allowed -- we always build it manually
// based on the params to the <SendInviteReply> and stick it in the
// mbps (additionalParts) parameter...
csd.mMm = ParseMimeMessage.parseMimeMsgSoap(zsc, octxt, mbox, msgElem, mbps, ParseMimeMessage.NO_INV_ALLOWED_PARSER, parsedMessageData);
} else {
// build a default "Accepted" response
if (!(acct instanceof CalendarResource)) {
csd.mMm = CalendarMailSender.createDefaultReply(acct, identityId, authAcct, identityId, isAdmin, onBehalfOf, calItem, oldInv, null, replySubject, verb, null, iCal);
} else {
// different template for calendar resources
RecurId rid = oldInv.getRecurId();
ParsedDateTime ridDt = rid != null ? rid.getDt() : null;
Invite replyInv = CalendarMailSender.replyToInvite(acct, authAcct, onBehalfOf, allowPrivateAccess, oldInv, verb, replySubject, ridDt);
MimeMessage mmInv = calItem.getSubpartMessage(oldInv.getMailItemId());
csd.mMm = CalendarMailSender.createResourceAutoReply(octxt, identityId, identityId, mbox, verb, false, null, calItem, oldInv, new Invite[] { replyInv }, mmInv, true);
}
}
int apptFolderId;
if (calItem != null)
apptFolderId = calItem.getFolderId();
else
apptFolderId = oldInv.isTodo() ? Mailbox.ID_FOLDER_TASKS : Mailbox.ID_FOLDER_CALENDAR;
MailSendQueue sendQueue = new MailSendQueue();
try {
sendCalendarMessage(zsc, octxt, apptFolderId, acct, mbox, csd, response, sendQueue);
} finally {
sendQueue.send();
}
}
RecurId recurId = null;
if (exceptDt != null) {
recurId = new RecurId(exceptDt, RecurId.RANGE_NONE);
}
ZAttendee me = oldInv.getMatchingAttendee(acct);
String cnStr = null;
String addressStr = acct.getName();
String role = IcalXmlStrMap.ROLE_OPT_PARTICIPANT;
int seqNo = oldInv.getSeqNo();
long dtStamp = oldInv.getDTStamp();
if (me != null) {
if (me.hasCn()) {
cnStr = me.getCn();
}
addressStr = me.getAddress();
if (me.hasRole()) {
role = me.getRole();
}
}
if (calItem != null)
mbox.modifyPartStat(octxt, calItemId, recurId, cnStr, addressStr, null, role, verb.getXmlPartStat(), Boolean.FALSE, seqNo, dtStamp);
}
// move the invite to the Trash if the user wants it
if (deleteInviteOnReply(acct)) {
try {
if (onBehalfOf) {
// HACK: Run the move in the context of the organizer
// mailbox because the authenticated account doesn't
// have rights on Inbox and Trash folders.
octxt = new OperationContext(mbox);
}
mbox.move(octxt, inviteMsgId, MailItem.Type.MESSAGE, Mailbox.ID_FOLDER_TRASH);
} catch (MailServiceException.NoSuchItemException nsie) {
ZimbraLog.calendar.debug("can't move nonexistent invite to Trash: " + inviteMsgId);
}
}
return response;
}
use of com.zimbra.cs.mailbox.calendar.RecurId 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;
}
Aggregations