private String getText(CalendarItem calItem, Invite invite, Locale locale, TimeZone tz)
      throws ServiceException {
    DateFormat dateTimeFormat =
        DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT, locale);
    dateTimeFormat.setTimeZone(tz);
    DateFormat onlyDateFormat = DateFormat.getDateInstance(DateFormat.SHORT, locale);
    onlyDateFormat.setTimeZone(tz);

    String formattedStart;
    String formattedEnd;
    if (calItem.getType() == MailItem.Type.APPOINTMENT) {
      Date start = new Date(new Long(getProperty(NEXT_INST_START_PROP_NAME)));
      formattedStart = dateTimeFormat.format(start);
      Date end = invite.getEffectiveDuration().addToDate(start);
      formattedEnd = dateTimeFormat.format(end);
    } else {
      // start date and due date is optional for tasks
      formattedStart =
          invite.getStartTime() == null
              ? ""
              : onlyDateFormat.format(invite.getStartTime().getDate());
      formattedEnd =
          invite.getEndTime() == null ? "" : onlyDateFormat.format(invite.getEndTime().getDate());
    }

    String location = invite.getLocation();
    if (StringUtil.isNullOrEmpty(location))
      location = L10nUtil.getMessage(L10nUtil.MsgKey.noLocation);

    String organizer = null;
    ZOrganizer zOrganizer = invite.getOrganizer();
    if (zOrganizer != null)
      organizer = zOrganizer.hasCn() ? zOrganizer.getCn() : zOrganizer.getAddress();
    if (organizer == null) organizer = "";

    String folder = calItem.getMailbox().getFolderById(null, calItem.getFolderId()).getName();

    return L10nUtil.getMessage(
        calItem.getType() == MailItem.Type.APPOINTMENT
            ? L10nUtil.MsgKey.apptReminderSmsText
            : L10nUtil.MsgKey.taskReminderSmsText,
        locale,
        calItem.getSubject(),
        formattedStart,
        formattedEnd,
        location,
        organizer,
        folder);
  }
 @Override
 protected void sendReminder(CalendarItem calItem, Invite invite) throws Exception {
   Account account = calItem.getAccount();
   Locale locale = account.getLocale();
   TimeZone tz = Util.getAccountTimeZone(account);
   MimeMessage mm = new Mime.FixedMimeMessage(JMSession.getSmtpSession(account));
   String to = account.getAttr(Provisioning.A_zimbraCalendarReminderDeviceEmail);
   if (to == null) {
     ZimbraLog.scheduler.info(
         "Unable to send calendar reminder sms since %s is not set",
         Provisioning.A_zimbraCalendarReminderDeviceEmail);
     return;
   }
   mm.setRecipient(javax.mail.Message.RecipientType.TO, new JavaMailInternetAddress(to));
   mm.setText(getText(calItem, invite, locale, tz), MimeConstants.P_CHARSET_UTF8);
   mm.saveChanges();
   MailSender mailSender = calItem.getMailbox().getMailSender();
   mailSender.setSaveToSent(false);
   mailSender.sendMimeMessage(null, calItem.getMailbox(), mm);
 }
  static SetCalendarItemData getSetCalendarItemData(
      ZimbraSoapContext zsc,
      OperationContext octxt,
      Account acct,
      Mailbox mbox,
      Element e,
      ParseMimeMessage.InviteParser parser)
      throws ServiceException {
    String partStatStr =
        e.getAttribute(MailConstants.A_CAL_PARTSTAT, IcalXmlStrMap.PARTSTAT_NEEDS_ACTION);

    // <M>
    Element msgElem = e.getElement(MailConstants.E_MSG);

    // check to see whether the entire message has been uploaded under separate cover
    String attachmentId = msgElem.getAttribute(MailConstants.A_ATTACHMENT_ID, null);
    Element contentElement = msgElem.getOptionalElement(MailConstants.E_CONTENT);

    InviteParserResult ipr = null;

    MimeMessage mm = null;
    if (attachmentId != null) {
      ParseMimeMessage.MimeMessageData mimeData = new ParseMimeMessage.MimeMessageData();
      mm = SendMsg.parseUploadedMessage(zsc, attachmentId, mimeData);
    } else if (contentElement != null) {
      mm = ParseMimeMessage.importMsgSoap(msgElem);
    } else {
      CalSendData dat = handleMsgElement(zsc, octxt, msgElem, acct, mbox, parser);
      mm = dat.mMm;
      ipr = parser.getResult();
    }

    if (ipr == null && msgElem.getOptionalElement(MailConstants.E_INVITE) != null) {
      ipr = parser.parse(zsc, octxt, mbox.getAccount(), msgElem.getElement(MailConstants.E_INVITE));
      // Get description texts out of the MimeMessage and set in the parsed invite.  Do it only if
      // the MimeMessage has text parts.  This prevents discarding texts when they're specified only
      // in the <inv> but not in mime parts.
      if (ipr != null && ipr.mInvite != null && mm != null) {
        String desc = Invite.getDescription(mm, MimeConstants.CT_TEXT_PLAIN);
        String descHtml = Invite.getDescription(mm, MimeConstants.CT_TEXT_HTML);
        if ((desc != null && desc.length() > 0) || (descHtml != null && descHtml.length() > 0))
          ipr.mInvite.setDescription(desc, descHtml);
      }
    }

    ParsedMessage pm = new ParsedMessage(mm, mbox.attachmentsIndexingEnabled());

    Invite inv = (ipr == null ? null : ipr.mInvite);
    if (inv == null || inv.getDTStamp() == -1) { // zdsync if -1 for 4.5 back compat
      CalendarPartInfo cpi = pm.getCalendarPartInfo();
      ZVCalendar cal = null;
      if (cpi != null && CalendarItem.isAcceptableInvite(mbox.getAccount(), cpi)) cal = cpi.cal;
      if (cal == null)
        throw ServiceException.FAILURE("SetCalendarItem could not build an iCalendar object", null);
      boolean sentByMe = false; // not applicable in the SetCalendarItem case
      Invite iCalInv = Invite.createFromCalendar(acct, pm.getFragment(), cal, sentByMe).get(0);

      if (inv == null) {
        inv = iCalInv;
      } else {
        inv.setDtStamp(iCalInv.getDTStamp()); // zdsync
        inv.setFragment(iCalInv.getFragment()); // zdsync
      }
    }
    inv.setPartStat(partStatStr);

    SetCalendarItemData sadata = new SetCalendarItemData();
    sadata.invite = inv;
    sadata.message = pm;
    return sadata;
  }
  @Override
  public Element handle(Element request, Map<String, Object> context) throws ServiceException {
    ZimbraSoapContext zsc = getZimbraSoapContext(context);
    Mailbox mbox = getRequestedMailbox(zsc);
    OperationContext octxt = getOperationContext(zsc, context);
    ItemIdFormatter ifmt = new ItemIdFormatter(zsc);

    int flags = Flag.toBitmask(request.getAttribute(MailConstants.A_FLAGS, null));
    String[] tags = TagUtil.parseTags(request, mbox, octxt);

    mbox.lock.lock();
    try {
      int defaultFolder =
          getItemType() == MailItem.Type.TASK
              ? Mailbox.ID_FOLDER_TASKS
              : Mailbox.ID_FOLDER_CALENDAR;
      String defaultFolderStr = Integer.toString(defaultFolder);
      String folderIdStr = request.getAttribute(MailConstants.A_FOLDER, defaultFolderStr);
      ItemId iidFolder = new ItemId(folderIdStr, zsc);
      Folder folder = mbox.getFolderById(octxt, iidFolder.getId());
      SetCalendarItemParseResult parsed =
          parseSetAppointmentRequest(request, zsc, octxt, folder, getItemType(), false);

      CalendarItem calItem =
          mbox.setCalendarItem(
              octxt,
              iidFolder.getId(),
              flags,
              tags,
              parsed.defaultInv,
              parsed.exceptions,
              parsed.replies,
              parsed.nextAlarm);

      Element response = getResponseElement(zsc);

      if (parsed.defaultInv != null) {
        response
            .addElement(MailConstants.A_DEFAULT)
            .addAttribute(
                MailConstants.A_ID, ifmt.formatItemId(parsed.defaultInv.invite.getMailItemId()));
      }

      if (parsed.exceptions != null) {
        for (SetCalendarItemData cur : parsed.exceptions) {
          Element e = response.addElement(MailConstants.E_CAL_EXCEPT);
          e.addAttribute(MailConstants.A_CAL_RECURRENCE_ID, cur.invite.getRecurId().toString());
          e.addAttribute(MailConstants.A_ID, ifmt.formatItemId(cur.invite.getMailItemId()));
        }
      }
      String itemId = ifmt.formatItemId(calItem == null ? 0 : calItem.getId());
      response.addAttribute(MailConstants.A_CAL_ID, itemId);
      try {
        Element inv =
            request
                .getElement(MailConstants.A_DEFAULT)
                .getElement(MailConstants.E_MSG)
                .getElement(MailConstants.E_INVITE);
        Element comp = inv.getOptionalElement(MailConstants.E_INVITE_COMPONENT);
        if (comp != null) {
          inv = comp;
        }
        String reqCalItemId = inv.getAttribute(MailConstants.A_CAL_ID);
        String uid = inv.getAttribute(MailConstants.A_UID);
        boolean uidSame =
            (calItem == null
                || (calItem.getUid() == null && uid == null)
                || (calItem.getUid() != null
                    && (calItem.getUid().equals(uid)
                        || (Invite.isOutlookUid(calItem.getUid())
                            && calItem
                                .getUid()
                                .equalsIgnoreCase(
                                    uid))))); // new or same as requested, or Outlook and
        // case-insensitive equal
        if (ZimbraLog.calendar.isInfoEnabled()) {
          StringBuilder logBuf = new StringBuilder();
          if (!reqCalItemId.equals(itemId)) {
            logBuf
                .append("Mapped requested id ")
                .append(reqCalItemId)
                .append(" -> ")
                .append(itemId);
          }
          if (!uidSame) {
            logBuf
                .append(" ?? requested UID ")
                .append(uid)
                .append(" differs from mapped ")
                .append(calItem.getUid());
            ZimbraLog.calendar.warn(logBuf.toString());
          } else if (logBuf.length() > 0) {
            ZimbraLog.calendar.info(logBuf.toString());
          }
        }
        assert (uidSame);
      } catch (ServiceException se) {
        // one of the elements we wanted to use doesn't exist; ignore; no log/assertion possible
      }
      if (!parsed.isTodo)
        response.addAttribute(MailConstants.A_APPT_ID_DEPRECATE_ME, itemId); // for backward compat

      return response;
    } finally {
      mbox.lock.release();
    }
  }
  private void compile() throws ServiceException {
    assert clauses != null;
    if (clauses.isEmpty()) {
      return;
    }

    // Convert list of Queries into list of QueryOperations, then optimize them.
    // this generates all of the query operations
    operation = parseTree.compile(mailbox);
    ZimbraLog.search.debug("OP=%s", operation);

    // expand the is:local and is:remote parts into in:(LIST)'s
    operation = operation.expandLocalRemotePart(mailbox);
    ZimbraLog.search.debug("AFTEREXP=%s", operation);

    // optimize the query down
    operation = operation.optimize(mailbox);
    ZimbraLog.search.debug("OPTIMIZED=%s", operation);
    if (operation == null || operation instanceof NoTermQueryOperation) {
      operation = new NoResultsQueryOperation();
      return;
    }

    // Use the OperationContext to update the set of visible referenced folders, local AND remote.
    Set<QueryTarget> targets = operation.getQueryTargets();
    assert (operation instanceof UnionQueryOperation
        || QueryTarget.getExplicitTargetCount(targets) <= 1);

    // easiest to treat the query two unions: one the LOCAL and one REMOTE parts
    UnionQueryOperation remoteOps = new UnionQueryOperation();
    UnionQueryOperation localOps = new UnionQueryOperation();

    if (operation instanceof UnionQueryOperation) {
      UnionQueryOperation union = (UnionQueryOperation) operation;
      // separate out the LOCAL vs REMOTE parts...
      for (QueryOperation op : union.operations) {
        Set<QueryTarget> set = op.getQueryTargets();

        // this assertion OK because we have already distributed multi-target query ops
        // during the optimize() step
        assert (QueryTarget.getExplicitTargetCount(set) <= 1);

        // the assertion above is critical: the code below all assumes
        // that we only have ONE target (ie we've already distributed if necessary)

        if (QueryTarget.hasExternalTarget(set)) {
          remoteOps.add(op);
        } else {
          localOps.add(op);
        }
      }
    } else {
      // single target: might be local, might be remote
      Set<QueryTarget> set = operation.getQueryTargets();
      // this assertion OK because we have already distributed multi-target query ops
      // during the optimize() step
      assert (QueryTarget.getExplicitTargetCount(set) <= 1);

      if (QueryTarget.hasExternalTarget(set)) {
        remoteOps.add(operation);
      } else {
        localOps.add(operation);
      }
    }

    // Handle the REMOTE side:
    if (!remoteOps.operations.isEmpty()) {
      // Since optimize() has already been run, we know that each of our ops
      // only has one target (or none).  Find those operations which have
      // an external target and wrap them in RemoteQueryOperations

      // iterate backwards so we can remove/add w/o screwing iteration
      for (int i = remoteOps.operations.size() - 1; i >= 0; i--) {
        QueryOperation op = remoteOps.operations.get(i);
        Set<QueryTarget> set = op.getQueryTargets();

        // this assertion OK because we have already distributed multi-target query ops
        // during the optimize() step
        assert (QueryTarget.getExplicitTargetCount(set) <= 1);

        // the assertion above is critical: the code below all assumes
        // that we only have ONE target (ie we've already distributed if necessary)

        if (QueryTarget.hasExternalTarget(set)) {
          remoteOps.operations.remove(i);
          boolean foundOne = false;
          // find a remoteOp to add this one to
          for (QueryOperation remoteOp : remoteOps.operations) {
            if (remoteOp instanceof RemoteQueryOperation) {
              if (((RemoteQueryOperation) remoteOp).tryAddOredOperation(op)) {
                foundOne = true;
                break;
              }
            }
          }
          if (!foundOne) {
            RemoteQueryOperation remoteOp = new RemoteQueryOperation();
            remoteOp.tryAddOredOperation(op);
            remoteOps.operations.add(i, remoteOp);
          }
        }
      }

      // ...we need to call setup on every RemoteQueryOperation we end up with...
      for (QueryOperation remoteOp : remoteOps.operations) {
        assert (remoteOp instanceof RemoteQueryOperation);
        try {
          ((RemoteQueryOperation) remoteOp).setup(protocol, octxt.getAuthToken(), params);
        } catch (Exception e) {
          ZimbraLog.search.info("Ignoring %s during RemoteQuery generation for %s", e, remoteOps);
        }
      }
    }

    // For the LOCAL parts of the query, do permission checks, do trash/spam exclusion
    if (!localOps.operations.isEmpty()) {
      ZimbraLog.search.debug("LOCAL_IN=%s", localOps);

      Account authAcct = octxt != null ? octxt.getAuthenticatedUser() : mailbox.getAccount();

      // Now, for all the LOCAL PARTS of the query, add the trash/spam exclusion part
      boolean includeTrash = false;
      boolean includeSpam = false;
      if (params.inDumpster()) {
        // Always include trash and spam for dumpster searches.  Excluding spam is a client side
        // choice.
        includeTrash = true;
        includeSpam = true;
        if (!mailbox.hasFullAdminAccess(
            octxt)) { // If the requester is not an admin, limit to recent date range.
          long now = octxt != null ? octxt.getTimestamp() : System.currentTimeMillis();
          long mdate = now - authAcct.getDumpsterUserVisibleAge();
          IntersectionQueryOperation and = new IntersectionQueryOperation();
          DBQueryOperation db = new DBQueryOperation();
          db.addMDateRange(mdate, false, -1L, false, true);
          and.addQueryOp((QueryOperation) localOps.clone());
          and.addQueryOp(db);
          localOps.operations.clear();
          localOps.operations.add(and);
        }
      } else {
        includeTrash = authAcct.isPrefIncludeTrashInSearch();
        ;
        includeSpam = authAcct.isPrefIncludeSpamInSearch();
      }
      if (!includeTrash || !includeSpam) {
        // First check that we aren't specifically looking for items in one of these.
        // For instance, in ZWC, if "Include Shared Items" is selected, the Trash list may currently
        // use a
        // search similar to : 'in:"trash" (inid:565 OR is:local)'
        // where 565 is the folder ID for a shared folder.  We don't want to end up doing a search
        // for items
        // that are both in "trash" and NOT in "trash"...
        for (Query q : clauses) {
          if (q.toString().equalsIgnoreCase("Q(IN:Trash)")) {
            includeTrash = true;
          }
          if (q.toString().equalsIgnoreCase("Q(IN:Junk)")) {
            includeSpam = true;
          }
        }
      }
      if (!includeTrash || !includeSpam) {
        List<QueryOperation> toAdd = new ArrayList<QueryOperation>();
        for (Iterator<QueryOperation> iter = localOps.operations.iterator(); iter.hasNext(); ) {
          QueryOperation cur = iter.next();
          if (!cur.hasSpamTrashSetting()) {
            QueryOperation newOp = cur.ensureSpamTrashSetting(mailbox, includeTrash, includeSpam);
            if (newOp != cur) {
              iter.remove();
              toAdd.add(newOp);
            }
          }
        }
        localOps.operations.addAll(toAdd);
        ZimbraLog.search.debug("LOCAL_AFTER_TRASH/SPAM/DUMPSTER=%s", localOps);
      }

      // Check to see if we need to filter out private appointment data
      boolean allowPrivateAccess = true;
      if (octxt != null) {
        allowPrivateAccess =
            AccessManager.getInstance()
                .allowPrivateAccess(
                    octxt.getAuthenticatedUser(),
                    mailbox.getAccount(),
                    octxt.isUsingAdminPrivileges());
      }

      //
      // bug 28892 - ACL.RIGHT_PRIVATE support:
      //
      // Basically, if ACL.RIGHT_PRIVATE is set somewhere, and if we're excluding private items from
      // search, then we need to run the query twice -- once over the whole mailbox with
      // private items excluded and then UNION it with a second run, this time only in the
      // RIGHT_PRIVATE enabled folders, with private items enabled.
      //
      UnionQueryOperation clonedLocal = null;
      Set<Folder> hasFolderRightPrivateSet = new HashSet<Folder>();

      // ...don't do any of this if they aren't asking for a calendar type...
      Set<MailItem.Type> types = params.getTypes();
      boolean hasCalendarType =
          types.contains(MailItem.Type.APPOINTMENT) || types.contains(MailItem.Type.TASK);
      if (hasCalendarType && !allowPrivateAccess && getTextOperationCount(localOps) > 0) {
        // the searcher is NOT allowed to see private items globally....lets check
        // to see if there are any individual folders that they DO have rights to...
        // if there are any, then we'll need to run special searches in those
        // folders
        Set<Folder> allVisibleFolders = mailbox.getVisibleFolders(octxt);
        if (allVisibleFolders == null) {
          allVisibleFolders = new HashSet<Folder>();
          allVisibleFolders.addAll(mailbox.getFolderList(octxt, SortBy.NONE));
        }
        for (Folder f : allVisibleFolders) {
          if (f.getType() == MailItem.Type.FOLDER
              && CalendarItem.allowPrivateAccess(f, authAcct, false)) {
            hasFolderRightPrivateSet.add(f);
          }
        }
        if (!hasFolderRightPrivateSet.isEmpty()) {
          clonedLocal = (UnionQueryOperation) localOps.clone();
        }
      }

      Set<Folder> visibleFolders = mailbox.getVisibleFolders(octxt);

      localOps = handleLocalPermissionChecks(localOps, visibleFolders, allowPrivateAccess);

      ZimbraLog.search.debug("LOCAL_AFTER_PERM_CHECKS=%s", localOps);

      if (!hasFolderRightPrivateSet.isEmpty()) {
        ZimbraLog.search.debug("CLONED_LOCAL_BEFORE_PERM=%s", clonedLocal);

        // now we're going to setup the clonedLocal tree
        // to run with private access ALLOWED, over the set of folders
        // that have RIGHT_PRIVATE (note that we build this list from the visible
        // folder list, so we are
        clonedLocal = handleLocalPermissionChecks(clonedLocal, hasFolderRightPrivateSet, true);

        ZimbraLog.search.debug("CLONED_LOCAL_AFTER_PERM=%s", clonedLocal);

        // clonedLocal should only have the single INTERSECT in it
        assert (clonedLocal.operations.size() == 1);

        QueryOperation optimizedClonedLocal = clonedLocal.optimize(mailbox);
        ZimbraLog.search.debug("CLONED_LOCAL_AFTER_OPTIMIZE=%s", optimizedClonedLocal);

        UnionQueryOperation withPrivateExcluded = localOps;
        localOps = new UnionQueryOperation();
        localOps.add(withPrivateExcluded);
        localOps.add(optimizedClonedLocal);

        ZimbraLog.search.debug("LOCAL_WITH_CLONED=%s", localOps);

        //
        // we should end up with:
        //
        // localOps =
        //    UNION(withPrivateExcluded,
        //          UNION(INTERSECT(clonedLocal,
        //                          UNION(hasFolderRightPrivateList)
        //                         )
        //                )
        //          )
        //
      }
    }

    UnionQueryOperation union = new UnionQueryOperation();
    union.add(localOps);
    union.add(remoteOps);
    ZimbraLog.search.debug("BEFORE_FINAL_OPT=%s", union);
    operation = union.optimize(mailbox);

    ZimbraLog.search.debug("COMPILED=%s", operation);
  }