@Override
  public ItemId fileInto(String folderPath, Collection<ActionFlag> flagActions, String[] tags)
      throws ServiceException {
    Message source = getMessage();

    // See if the message is already in the target folder.
    Folder targetFolder = null;
    try {
      targetFolder = mailbox.getFolderByPath(octxt, folderPath);
    } catch (NoSuchItemException ignored) {
    }
    if (targetFolder != null && source.getFolderId() == targetFolder.getId()) {
      ZimbraLog.filter.debug(
          "Ignoring fileinto action for message %d.  It is already in %s.", messageId, folderPath);
      updateTagsAndFlagsIfNecessary(source, flagActions, tags);
      return null;
    }

    ZimbraLog.filter.info("Copying existing message %d to folder %s.", messageId, folderPath);
    if (isLocalExistingFolder(folderPath)) {
      // Copy item into to a local folder.
      Folder target = mailbox.getFolderByPath(octxt, folderPath);
      Message newMsg =
          (Message) mailbox.copy(octxt, messageId, MailItem.Type.MESSAGE, target.getId());
      filtered = true;
      filed = true;

      // Apply flags and tags
      mailbox.setTags(
          octxt,
          newMsg.getId(),
          MailItem.Type.MESSAGE,
          FilterUtil.getFlagBitmask(flagActions, source.getFlagBitmask()),
          FilterUtil.getTagsUnion(source.getTags(), tags));
      return new ItemId(mailbox, messageId);
    }

    ItemId id =
        FilterUtil.addMessage(
            new DeliveryContext(),
            mailbox,
            getParsedMessage(),
            mailbox.getAccount().getName(),
            folderPath,
            false,
            FilterUtil.getFlagBitmask(flagActions, source.getFlagBitmask()),
            tags,
            Mailbox.ID_AUTO_INCREMENT,
            octxt);
    if (id != null) {
      filtered = true;
      filed = true;
    }
    return id;
  }
Beispiel #2
0
  // Caller is responsible for filtering out Appointments/Tasks marked private if the requester
  // is not the mailbox owner.
  public Iterator<? extends MailItem> getMailItems(
      UserServletContext context, long startTime, long endTime, long chunkSize)
      throws ServiceException {
    if (context.requestedItems != null) {
      return context.getRequestedItems();
    }

    assert (context.target != null);
    String query = context.getQueryString();
    if (query != null) {
      if (context.target instanceof Folder) {
        Folder f = (Folder) context.target;
        if (f.getId() != Mailbox.ID_FOLDER_USER_ROOT) {
          query = "in:" + f.getPath() + " " + query;
        }
      }
      Set<MailItem.Type> types;
      if (context.getTypesString() == null) {
        types = getDefaultSearchTypes();
      } else {
        try {
          types = MailItem.Type.setOf(context.getTypesString());
        } catch (IllegalArgumentException e) {
          throw MailServiceException.INVALID_TYPE(e.getMessage());
        }
      }
      ZimbraQueryResults results =
          context.targetMailbox.index.search(
              context.opContext,
              query,
              types,
              SortBy.DATE_DESC,
              context.getOffset() + context.getLimit());
      return new QueryResultIterator(results);
    } else if (context.target instanceof Folder) {
      Collection<? extends MailItem> items =
          getMailItemsFromFolder(context, (Folder) context.target, startTime, endTime, chunkSize);
      return items != null ? items.iterator() : null;
    } else {
      ArrayList<MailItem> result = new ArrayList<MailItem>();
      result.add(context.target);
      return result.iterator();
    }
  }
Beispiel #3
0
 protected Collection<? extends MailItem> getMailItemsFromFolder(
     UserServletContext context, Folder folder, long startTime, long endTime, long chunkSize)
     throws ServiceException {
   switch (folder.getDefaultView()) {
     case APPOINTMENT:
     case TASK:
       return context.targetMailbox.getCalendarItemsForRange(
           context.opContext, startTime, endTime, folder.getId(), null);
     case CONTACT:
       return context.targetMailbox.getContactList(
           context.opContext, folder.getId(), SortBy.NAME_ASC);
     case DOCUMENT:
     case WIKI:
       return context.targetMailbox.getDocumentList(
           context.opContext, folder.getId(), SortBy.NAME_ASC);
     default:
       return context.targetMailbox.getItemList(
           context.opContext, MailItem.Type.MESSAGE, folder.getId());
   }
 }
  /** Verifies serializing, deserializing, and replaying for folder. */
  @Test
  public void redoFolder() throws Exception {
    Mailbox mbox =
        MailboxManager.getInstance().getMailboxByAccountId(MockProvisioning.DEFAULT_ACCOUNT_ID);

    // Create folder.
    Folder folder =
        mbox.createFolder(
            null, "/redo", new Folder.FolderOptions().setDefaultView(MailItem.Type.MESSAGE));
    assertEquals(0, folder.getRetentionPolicy().getKeepPolicy().size());
    assertEquals(0, folder.getRetentionPolicy().getPurgePolicy().size());

    // Create RedoableOp.
    RetentionPolicy rp =
        new RetentionPolicy(
            Arrays.asList(Policy.newSystemPolicy("123")),
            Arrays.asList(Policy.newUserPolicy("45m")));
    SetRetentionPolicy redoPlayer =
        new SetRetentionPolicy(mbox.getId(), MailItem.Type.FOLDER, folder.getId(), rp);

    // Serialize, deserialize, and redo.
    byte[] data = redoPlayer.testSerialize();
    redoPlayer = new SetRetentionPolicy();
    redoPlayer.setMailboxId(mbox.getId());
    redoPlayer.testDeserialize(data);
    redoPlayer.redo();
    folder = mbox.getFolderById(null, folder.getId());
    assertEquals(1, folder.getRetentionPolicy().getKeepPolicy().size());
    assertEquals(1, folder.getRetentionPolicy().getPurgePolicy().size());
    assertEquals("45m", folder.getRetentionPolicy().getPurgePolicy().get(0).getLifetime());
    assertEquals("123", folder.getRetentionPolicy().getKeepPolicy().get(0).getId());
  }
  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);
  }
  private void deliverMessageToLocalMailboxes(
      Blob blob, BlobInputStream bis, byte[] data, MimeMessage mm, LmtpEnvelope env)
      throws ServiceException, IOException {

    List<LmtpAddress> recipients = env.getLocalRecipients();
    String envSender = env.getSender().getEmailAddress();

    boolean shared = recipients.size() > 1;
    List<Integer> targetMailboxIds = new ArrayList<Integer>(recipients.size());

    Map<LmtpAddress, RecipientDetail> rcptMap =
        new HashMap<LmtpAddress, RecipientDetail>(recipients.size());
    try {
      // Examine attachments indexing option for all recipients and
      // prepare ParsedMessage versions needed.  Parsing is done before
      // attempting delivery to any recipient.  Therefore, parse error
      // will result in non-delivery to all recipients.

      // ParsedMessage for users with attachments indexing
      ParsedMessage pmAttachIndex = null;
      // ParsedMessage for users without attachments indexing
      ParsedMessage pmNoAttachIndex = null;

      // message id for logging
      String msgId = null;

      for (LmtpAddress recipient : recipients) {
        String rcptEmail = recipient.getEmailAddress();

        Account account;
        Mailbox mbox;
        boolean attachmentsIndexingEnabled;
        try {
          account = Provisioning.getInstance().get(AccountBy.name, rcptEmail);
          if (account == null) {
            ZimbraLog.mailbox.warn("No account found delivering mail to " + rcptEmail);
            continue;
          }
          mbox = MailboxManager.getInstance().getMailboxByAccount(account);
          if (mbox == null) {
            ZimbraLog.mailbox.warn("No mailbox found delivering mail to " + rcptEmail);
            continue;
          }
          attachmentsIndexingEnabled = mbox.attachmentsIndexingEnabled();
        } catch (ServiceException se) {
          if (se.isReceiversFault()) {
            ZimbraLog.mailbox.info("Recoverable exception getting mailbox for " + rcptEmail, se);
            rcptMap.put(
                recipient, new RecipientDetail(null, null, null, false, DeliveryAction.defer));
          } else {
            ZimbraLog.mailbox.warn("Unrecoverable exception getting mailbox for " + rcptEmail, se);
          }
          continue;
        }

        if (account != null && mbox != null) {
          ParsedMessageOptions pmo;
          if (mm != null) {
            pmo =
                new ParsedMessageOptions()
                    .setContent(mm)
                    .setDigest(blob.getDigest())
                    .setSize(blob.getRawSize());
          } else {
            pmo = new ParsedMessageOptions(blob, data);
          }

          ParsedMessage pm;
          if (attachmentsIndexingEnabled) {
            if (pmAttachIndex == null) {
              pmo.setAttachmentIndexing(true);
              ZimbraLog.lmtp.debug(
                  "Creating ParsedMessage from %s with attachment indexing enabled",
                  data == null ? "file" : "memory");
              pmAttachIndex = new ParsedMessage(pmo);
            }
            pm = pmAttachIndex;
          } else {
            if (pmNoAttachIndex == null) {
              pmo.setAttachmentIndexing(false);
              ZimbraLog.lmtp.debug(
                  "Creating ParsedMessage from %s with attachment indexing disabled",
                  data == null ? "file" : "memory");
              pmNoAttachIndex = new ParsedMessage(pmo);
            }
            pm = pmNoAttachIndex;
          }

          msgId = pm.getMessageID();

          if (account.isPrefMailLocalDeliveryDisabled()) {
            ZimbraLog.lmtp.debug("Local delivery disabled for account %s", rcptEmail);
            rcptMap.put(
                recipient, new RecipientDetail(account, mbox, pm, false, DeliveryAction.discard));
            continue;
          }

          // For non-shared delivery (i.e. only one recipient),
          // always deliver regardless of backup mode.
          DeliveryAction da = DeliveryAction.deliver;
          boolean endSharedDelivery = false;
          if (shared) {
            if (mbox.beginSharedDelivery()) {
              endSharedDelivery = true;
            } else {
              // Skip delivery to mailboxes in backup mode.
              da = DeliveryAction.defer;
            }
          }
          rcptMap.put(recipient, new RecipientDetail(account, mbox, pm, endSharedDelivery, da));
          if (da == DeliveryAction.deliver) {
            targetMailboxIds.add(mbox.getId());
          }
        }
      }

      ZimbraLog.removeAccountFromContext();
      if (ZimbraLog.lmtp.isInfoEnabled()) {
        ZimbraLog.lmtp.info(
            "Delivering message: size=%s, nrcpts=%d, sender=%s, msgid=%s",
            env.getSize() == 0 ? "unspecified" : Integer.toString(env.getSize()) + " bytes",
            recipients.size(),
            env.getSender(),
            msgId == null ? "" : msgId);
      }

      DeliveryContext sharedDeliveryCtxt = new DeliveryContext(shared, targetMailboxIds);
      sharedDeliveryCtxt.setIncomingBlob(blob);

      // We now know which addresses are valid and which ParsedMessage
      // version each recipient needs.  Deliver!
      for (LmtpAddress recipient : recipients) {
        String rcptEmail = recipient.getEmailAddress();
        LmtpReply reply = LmtpReply.TEMPORARY_FAILURE;
        RecipientDetail rd = rcptMap.get(recipient);
        if (rd.account != null) ZimbraLog.addAccountNameToContext(rd.account.getName());
        if (rd.mbox != null) ZimbraLog.addMboxToContext(rd.mbox.getId());

        boolean success = false;
        try {
          if (rd != null) {
            switch (rd.action) {
              case discard:
                ZimbraLog.lmtp.info(
                    "accepted and discarded message from=%s,to=%s: local delivery is disabled",
                    envSender, rcptEmail);
                if (rd.account.getPrefMailForwardingAddress() != null) {
                  // mail forwarding is set up
                  for (LmtpCallback callback : callbacks) {
                    ZimbraLog.lmtp.debug("Executing callback %s", callback.getClass().getName());
                    callback.forwardWithoutDelivery(
                        rd.account, rd.mbox, envSender, rcptEmail, rd.pm);
                  }
                }
                reply = LmtpReply.DELIVERY_OK;
                break;
              case deliver:
                Account account = rd.account;
                Mailbox mbox = rd.mbox;
                ParsedMessage pm = rd.pm;
                List<ItemId> addedMessageIds = null;
                ReentrantLock lock = mailboxDeliveryLocks.get(mbox.getId());
                boolean acquiredLock;
                try {
                  // Wait for the lock, up to the timeout
                  acquiredLock =
                      lock.tryLock(LC.zimbra_mailbox_lock_timeout.intValue(), TimeUnit.SECONDS);
                } catch (InterruptedException e) {
                  acquiredLock = false;
                }
                if (!acquiredLock) {
                  ZimbraLog.lmtp.info(
                      "try again for message from=%s,to=%s: another mail delivery in progress.",
                      envSender, rcptEmail);
                  reply = LmtpReply.TEMPORARY_FAILURE;
                  break;
                }
                try {
                  if (dedupe(pm, mbox)) {
                    // message was already delivered to this mailbox
                    ZimbraLog.lmtp.info(
                        "Not delivering message with duplicate Message-ID %s", pm.getMessageID());
                  } else if (recipient.getSkipFilters()) {
                    msgId = pm.getMessageID();
                    int folderId = Mailbox.ID_FOLDER_INBOX;
                    if (recipient.getFolder() != null) {
                      try {
                        Folder folder = mbox.getFolderByPath(null, recipient.getFolder());
                        folderId = folder.getId();
                      } catch (ServiceException se) {
                        if (se.getCode().equals(MailServiceException.NO_SUCH_FOLDER)) {
                          Folder folder =
                              mbox.createFolder(
                                  null,
                                  recipient.getFolder(),
                                  new Folder.FolderOptions().setDefaultView(MailItem.Type.MESSAGE));
                          folderId = folder.getId();
                        } else {
                          throw se;
                        }
                      }
                    }
                    int flags = Flag.BITMASK_UNREAD;
                    if (recipient.getFlags() != null) {
                      flags = Flag.toBitmask(recipient.getFlags());
                    }
                    DeliveryOptions dopt = new DeliveryOptions().setFolderId(folderId);
                    dopt.setFlags(flags).setTags(recipient.getTags()).setRecipientEmail(rcptEmail);
                    Message msg = mbox.addMessage(null, pm, dopt, sharedDeliveryCtxt);
                    addedMessageIds = Lists.newArrayList(new ItemId(msg));
                  } else if (!DebugConfig.disableIncomingFilter) {
                    // Get msgid first, to avoid having to reopen and reparse the blob
                    // file if Mailbox.addMessageInternal() closes it.
                    pm.getMessageID();
                    addedMessageIds =
                        RuleManager.applyRulesToIncomingMessage(
                            null,
                            mbox,
                            pm,
                            (int) blob.getRawSize(),
                            rcptEmail,
                            sharedDeliveryCtxt,
                            Mailbox.ID_FOLDER_INBOX,
                            false);
                  } else {
                    pm.getMessageID();
                    DeliveryOptions dopt =
                        new DeliveryOptions().setFolderId(Mailbox.ID_FOLDER_INBOX);
                    dopt.setFlags(Flag.BITMASK_UNREAD).setRecipientEmail(rcptEmail);
                    Message msg = mbox.addMessage(null, pm, dopt, sharedDeliveryCtxt);
                    addedMessageIds = Lists.newArrayList(new ItemId(msg));
                  }
                  success = true;
                  if (addedMessageIds != null && addedMessageIds.size() > 0) {
                    addToDedupeCache(pm, mbox);
                  }
                } finally {
                  lock.unlock();
                }

                if (addedMessageIds != null && addedMessageIds.size() > 0) {
                  // Execute callbacks
                  for (LmtpCallback callback : callbacks) {
                    for (ItemId id : addedMessageIds) {
                      if (id.belongsTo(mbox)) {
                        // Message was added to the local mailbox, as opposed to a mountpoint.
                        ZimbraLog.lmtp.debug(
                            "Executing callback %s", callback.getClass().getName());
                        try {
                          Message msg = mbox.getMessageById(null, id.getId());
                          callback.afterDelivery(account, mbox, envSender, rcptEmail, msg);
                        } catch (Throwable t) {
                          if (t instanceof OutOfMemoryError) {
                            Zimbra.halt("LMTP callback failed", t);
                          } else {
                            ZimbraLog.lmtp.warn("LMTP callback threw an exception", t);
                          }
                        }
                      }
                    }
                  }
                }
                reply = LmtpReply.DELIVERY_OK;
                break;
              case defer:
                // Delivery to mailbox skipped.  Let MTA retry again later.
                // This case happens for shared delivery to a mailbox in
                // backup mode.
                ZimbraLog.lmtp.info(
                    "try again for message from=%s,to=%s: mailbox skipped", envSender, rcptEmail);
                reply = LmtpReply.TEMPORARY_FAILURE;
                break;
            }
          } else {
            // Account or mailbox not found.
            ZimbraLog.lmtp.info(
                "rejecting message from=%s,to=%s: account or mailbox not found",
                envSender, rcptEmail);
            reply = LmtpReply.PERMANENT_FAILURE;
          }
        } catch (ServiceException e) {
          if (e.getCode().equals(MailServiceException.QUOTA_EXCEEDED)) {
            ZimbraLog.lmtp.info("rejecting message from=%s,to=%s: overquota", envSender, rcptEmail);
            if (config.isPermanentFailureWhenOverQuota()) {
              reply = LmtpReply.PERMANENT_FAILURE_OVER_QUOTA;
            } else {
              reply = LmtpReply.TEMPORARY_FAILURE_OVER_QUOTA;
            }
          } else if (e.isReceiversFault()) {
            ZimbraLog.lmtp.info("try again for message from=%s,to=%s", envSender, rcptEmail, e);
            reply = LmtpReply.TEMPORARY_FAILURE;
          } else {
            ZimbraLog.lmtp.info("rejecting message from=%s,to=%s", envSender, rcptEmail, e);
            reply = LmtpReply.PERMANENT_FAILURE;
          }
        } catch (Exception e) {
          reply = LmtpReply.TEMPORARY_FAILURE;
          ZimbraLog.lmtp.warn("try again for message from=%s,to=%s", envSender, rcptEmail, e);
        } finally {
          if (rd.action == DeliveryAction.deliver && !success) {
            // Message was not delivered.  Remove it from the dedupe
            // cache so we don't dedupe it on LMTP retry.
            removeFromDedupeCache(msgId, rd.mbox);
          }
          recipient.setDeliveryStatus(reply);
          if (shared && rd != null && rd.esd) {
            rd.mbox.endSharedDelivery();
            rd.esd = false;
          }
        }
      }

      // If this message is being streamed from disk, cache it
      ParsedMessage mimeSource = pmAttachIndex != null ? pmAttachIndex : pmNoAttachIndex;
      MailboxBlob mblob = sharedDeliveryCtxt.getMailboxBlob();
      if (mblob != null && mimeSource != null) {
        if (bis == null) {
          bis = mimeSource.getBlobInputStream();
        }
        if (bis != null) {
          try {
            // Update the MimeMessage with the blob that's stored inside the mailbox,
            // since the incoming blob will be deleted.
            Blob storedBlob = mblob.getLocalBlob();
            bis.fileMoved(storedBlob.getFile());
            MessageCache.cacheMessage(
                mblob.getDigest(), mimeSource.getOriginalMessage(), mimeSource.getMimeMessage());
          } catch (IOException e) {
            ZimbraLog.lmtp.warn("Unable to cache message for " + mblob, e);
          }
        }
      }
    } finally {
      // If there were any stray exceptions after the call to
      // beginSharedDelivery that caused endSharedDelivery to be not
      // called, we check and fix those here.
      if (shared) {
        for (RecipientDetail rd : rcptMap.values()) {
          if (rd.esd && rd.mbox != null) rd.mbox.endSharedDelivery();
        }
      }
    }
  }