/** * Translate a delimted list of tag names to a delimited list of correponding local tagIds * * @throws ServiceException */ public String localTagsFromNames(String tagNames, String inDelim, String outDelim) throws ServiceException { if (tagNames != null && tagNames.length() > 0) { StringBuilder sb = new StringBuilder(); String[] names = tagNames.split(inDelim); for (String name : names) { if (name.trim().length() <= 0) { continue; } Integer tagId = localIdsByName.get(name); if (tagId == null) { try { Tag tag = mbox.getTagByName(null, name); tagId = tag.getId(); localIdsByName.put(name, tagId); } catch (MailServiceException mse) { if (MailServiceException.NO_SUCH_TAG.equals(mse.getCode())) { OfflineLog.offline.debug( "message has tag [" + name + "] which is not visible locally"); continue; } else { throw mse; } } } sb.append(tagId).append(outDelim); } if (sb.length() >= outDelim.length()) { sb.setLength(sb.length() - outDelim.length()); } return sb.toString(); } else { return tagNames; } }
@Override public synchronized void unlock( OperationContext octxt, int itemId, MailItem.Type type, String accountId) throws ServiceException { Account acct = getLockAccount(accountId); boolean success = false; try { beginTransaction("unlock", octxt); MailItem item = getItemById(itemId, type); if (acct == null && item instanceof Document) { // if owner and accountId are the same it's ok // hacky like the lock code, but this case is just when lock owned by grantee outside ZD Document doc = (Document) item; if (doc.lockOwner == null) return; if (!doc.lockOwner.equalsIgnoreCase(accountId)) { throw MailServiceException.CANNOT_UNLOCK(doc.mId, doc.lockOwner); } doc.lockOwner = null; doc.lockTimestamp = 0; doc.markItemModified(Change.LOCK); doc.saveMetadata(); } else { item.unlock(acct); } success = true; } finally { endTransaction(success); } }
@Override public synchronized MailItem lock( OperationContext octxt, int itemId, MailItem.Type type, String accountId) throws ServiceException { Account acct = getLockAccount(accountId); boolean success = false; try { beginTransaction("lock", octxt); MailItem item = getItemById(itemId, type); if (acct == null && item instanceof Document) { // bit of a hack here; basically we need to be able to record lock owner as a remote acct // since it can be locked by a grantee that does not exist in ZD // rather than setting up a fake account we'll just manually set lock owner Document doc = (Document) item; if (doc.lockOwner != null && !doc.lockOwner.equalsIgnoreCase(accountId)) { throw MailServiceException.CANNOT_LOCK(doc.mId, doc.lockOwner); } doc.lockOwner = accountId; doc.lockTimestamp = System.currentTimeMillis(); doc.markItemModified(Change.LOCK); doc.saveMetadata(); } else { item.lock(acct); } success = true; return item; } finally { endTransaction(success); } }
/** * Determines the set of {@link Message}s to be deleted from this <code>Conversation</code>. * Assembles a new {@link PendingDelete} object encapsulating the data on the items to be deleted. * * <p>A message will be deleted unless: * * <ul> * <li>The caller lacks {@link ACL#RIGHT_DELETE} permission on the <code>Message</code>. * <li>The caller has specified a {@link MailItem.TargetConstraint} that explicitly excludes the * <code>Message</code>. * <li>The caller has specified the maximum change number they know about, and the * (modification/content) change number on the <code>Message</code> is greater. * </ul> * * As a result of all these constraints, no messages may actually be deleted. * * @throws ServiceException The following error codes are possible: * <ul> * <li><code>mail.MODIFY_CONFLICT</code> - if the caller specified a max change number and a * modification check, and the modified change number of the <code>Message</code> is * greater * <li><code>service.FAILURE</code> - if there's a database failure fetching the message * list * </ul> */ @Override PendingDelete getDeletionInfo() throws ServiceException { PendingDelete info = new PendingDelete(); info.rootId = mId; info.itemIds.add(getType(), mId); if (mData.size == 0) return info; List<Message> msgs = getMessages(); TargetConstraint tcon = mMailbox.getOperationTargetConstraint(); boolean excludeModify = false, excludeAccess = false; for (Message child : msgs) { // silently skip explicitly excluded messages, PERMISSION_DENIED messages, and MODIFY_CONFLICT // messages if (!TargetConstraint.checkItem(tcon, child)) continue; else if (!child.canAccess(ACL.RIGHT_DELETE)) excludeAccess = true; else if (!child.checkChangeID()) excludeModify = true; else info.add(child.getDeletionInfo()); } int totalDeleted = info.itemIds.size(); if (totalDeleted == 1) { // if all messages have been excluded, some for "error" reasons, throw an exception if (excludeAccess) throw ServiceException.PERM_DENIED("you do not have sufficient permissions"); if (excludeModify) throw MailServiceException.MODIFY_CONFLICT(); } if (totalDeleted != msgs.size() + 1) info.incomplete = true; return info; }
/** please call this *after* adding the child row to the DB */ @Override void addChild(MailItem child) throws ServiceException { if (!(child instanceof Message)) throw MailServiceException.CANNOT_PARENT(); Message msg = (Message) child; super.addChild(msg); // update inherited flags int oldFlags = mData.flags; mData.flags |= msg.getInternalFlagBitmask(); if (mData.flags != oldFlags) markItemModified(Change.MODIFIED_FLAGS); // update inherited tags long oldTags = mData.tags; mData.tags |= msg.getTagBitmask(); if (mData.tags != oldTags) markItemModified(Change.MODIFIED_TAGS); // update unread counts if (msg.isUnread()) { markItemModified(Change.MODIFIED_UNREAD); updateUnread( child.mData.unreadCount, child.isTagged(Flag.ID_FLAG_DELETED) ? child.mData.unreadCount : 0); } markItemModified(Change.MODIFIED_SIZE | Change.MODIFIED_SENDERS | Change.MODIFIED_METADATA); MetadataCallback.duringConversationAdd(mExtendedData, msg); // FIXME: this ordering is to work around the fact that when getSenderList has to // recalc the metadata, it uses the already-updated DB message state to do it... mData.date = mMailbox.getOperationTimestamp(); mData.contentChanged(mMailbox); if (!mMailbox.hasListeners(Session.Type.SOAP)) { instantiateSenderList(); mData.size++; try { if (mSenderList != null) mSenderList.add(msg); } catch (SenderList.RefreshException slre) { mSenderList = null; } saveMetadata(); } else { boolean recalculated = loadSenderList(); if (!recalculated) { mData.size++; try { mSenderList.add(msg); saveMetadata(); } catch (SenderList.RefreshException slre) { recalculateMetadata(); } } } }
void setConversationId(OperationContext octxt, int msgId, int convId) throws ServiceException { // we're not allowing any magic -- we are being completely literal about the target conv id if (convId <= 0 && convId != -msgId) { throw MailServiceException.NO_SUCH_CONV(convId); } boolean success = false; try { beginTransaction("setConversationId", octxt); Message msg = getMessageById(msgId); Conversation oldConv = (Conversation) msg.getParent(); if (convId == msg.getConversationId()) { success = true; if (oldConv != null && (oldConv.getSize() < 1 || oldConv.getUnreadCount() < msg.getUnreadCount())) { OfflineLog.offline.error( "Conversation size/unread inconsistent for conversation " + oldConv); } return; } try { Conversation newConv; if (convId <= 0) { // moving from a real conv to a virtual one newConv = VirtualConversation.create(this, msg); } else { // moving to an existing real conversation newConv = getConversationById(convId); } DbMailItem.setParent(msg, newConv); if (convId > 0) { newConv.addChild(msg); } msg.markItemModified(Change.PARENT); msg.mData.parentId = convId; msg.metadataChanged(); } catch (MailServiceException.NoSuchItemException nsie) { // real conversation didn't exist; create it! createConversation(convId, msg); } // and now we can update (and possibly delete) the old conversation oldConv.removeChild(msg); success = true; } finally { endTransaction(success); } }
/** * Create a mapping from remote to local tagId * * @throws ServiceException */ public void mapTag(int remote, int id) throws ServiceException { if (!validateId(id)) { throw MailServiceException.NO_SUCH_TAG(id); } mapTagInternal(remote, id); }
/** * Moves all the conversation's {@link Message}s to a different {@link Folder}. Persists the * change to the database and the in-memory cache. Updates all relevant unread counts, folder * sizes, etc. * * <p>Messages moved to the Trash folder are automatically marked read. Conversations moved to the * Junk folder will not receive newly-delivered messages. * * <p>Messages in the conversation are omitted from this operation if one or more of the following * applies: * * <ul> * <li>The caller lacks {@link ACL#RIGHT_WRITE} permission on the <code>Message</code>. * <li>The caller has specified a {@link MailItem.TargetConstraint} that explicitly excludes the * <code>Message</code>. * <li>The caller has specified the maximum change number they know about, and the * (modification/content) change number on the <code>Message</code> is greater. * </ul> * * As a result of all these constraints, no messages may actually be moved. * * @perms {@link ACL#RIGHT_INSERT} on the target folder, {@link ACL#RIGHT_DELETE} on the messages' * source folders */ @Override boolean move(Folder target) throws ServiceException { if (!target.canContain(TYPE_MESSAGE)) throw MailServiceException.CANNOT_CONTAIN(); markItemModified(Change.UNMODIFIED); List<Message> msgs = getMessages(); TargetConstraint tcon = mMailbox.getOperationTargetConstraint(); boolean toTrash = target.inTrash(); int oldUnread = 0; for (Message msg : msgs) if (msg.isUnread()) oldUnread++; // if mData.unread is wrong, what to do? right now, always use the calculated value mData.unreadCount = oldUnread; boolean excludeAccess = false; List<Integer> markedRead = new ArrayList<Integer>(); List<Message> moved = new ArrayList<Message>(); List<Message> indexUpdated = new ArrayList<Message>(); for (Message msg : msgs) { Folder source = msg.getFolder(); // skip messages that don't need to be moved, or that the client can't modify, doesn't know // about, or has explicitly excluded if (source.getId() == target.getId()) { continue; } else if (!source.canAccess(ACL.RIGHT_DELETE)) { excludeAccess = true; continue; } else if (target.getId() != Mailbox.ID_FOLDER_TRASH && target.getId() != Mailbox.ID_FOLDER_SPAM && !target.canAccess(ACL.RIGHT_INSERT)) { excludeAccess = true; continue; } else if (!msg.checkChangeID() || !TargetConstraint.checkItem(tcon, msg)) { continue; } boolean isDeleted = msg.isTagged(Flag.ID_FLAG_DELETED); if (msg.isUnread()) { if (!toTrash || msg.inTrash()) { source.updateUnread(-1, isDeleted ? -1 : 0); target.updateUnread(1, isDeleted ? 1 : 0); } else { // unread messages moved from Mailbox to Trash need to be marked read: // update cached unread counts (message, conversation, folder, tags) msg.updateUnread(-1, isDeleted ? -1 : 0); // note that we need to update this message in the DB markedRead.add(msg.getId()); } } // moved an item out of the spam folder, need to index it if (msg.inSpam() && !target.inSpam()) { if (msg.isIndexed() && msg.getIndexId() != -1) { msg.indexIdChanged(msg.getId()); indexUpdated.add(msg); } } // if a draft is being moved to Trash then remove any "send-later" info from it if (toTrash && msg.isDraft()) msg.setDraftAutoSendTime(0); // handle folder message counts source.updateSize(-1, isDeleted ? -1 : 0, -msg.getTotalSize()); target.updateSize(1, isDeleted ? 1 : 0, msg.getTotalSize()); moved.add(msg); msg.folderChanged(target, 0); } // mark unread messages moved from Mailbox to Trash/Spam as read in the DB if (!markedRead.isEmpty()) DbMailItem.alterUnread(target.getMailbox(), markedRead, false); if (moved.isEmpty()) { if (excludeAccess) throw ServiceException.PERM_DENIED("you do not have sufficient permissions"); } else { // moving a conversation to spam closes it if (target.inSpam()) detach(); if (ZimbraLog.mailop.isInfoEnabled()) { StringBuilder ids = new StringBuilder(); for (int i = 0; i < moved.size(); i++) { if (i > 0) { ids.append(','); } ids.append(moved.get(i).getId()); } ZimbraLog.mailop.info( "Moving %s to %s. Affected message ids: %s.", getMailopContext(this), getMailopContext(target), ids); } DbMailItem.setFolder(moved, target); if (!indexUpdated.isEmpty()) { DbMailItem.setIndexIds(mMailbox, indexUpdated); for (Message msg : indexUpdated) { mMailbox.queueForIndexing(msg, false, null); } } } return !moved.isEmpty(); }
/** * Tags or untags all messages in the conversation. Persists the change to the database and cache. * If the conversation includes at least one unread {@link Message} whose tagged state is * changing, updates the {@link Tag}'s unread count appropriately. * * <p>Messages in the conversation are omitted from this operation if one or more of the following * applies: * * <ul> * <li>The caller lacks {@link ACL#RIGHT_WRITE} permission on the <code>Message</code>. * <li>The caller has specified a {@link MailItem.TargetConstraint} that explicitly excludes the * <code>Message</code>. * <li>The caller has specified the maximum change number they know about, and the * (modification/content) change number on the <code>Message</code> is greater. * </ul> * * As a result of all these constraints, no messages may actually be tagged/untagged. * * @perms {@link ACL#RIGHT_WRITE} on all the messages */ @Override void alterTag(Tag tag, boolean add) throws ServiceException { if (tag == null) throw ServiceException.FAILURE("missing tag argument", null); if (!add && !isTagged(tag)) return; if (tag.getId() == Flag.ID_FLAG_UNREAD) throw ServiceException.FAILURE("unread state must be set with alterUnread", null); // don't let the user tag things as "has attachments" or "draft" if (tag instanceof Flag && (tag.getBitmask() & Flag.FLAG_SYSTEM) != 0) throw MailServiceException.CANNOT_TAG(tag, this); markItemModified(tag instanceof Flag ? Change.MODIFIED_FLAGS : Change.MODIFIED_TAGS); TargetConstraint tcon = mMailbox.getOperationTargetConstraint(); boolean excludeAccess = false; List<Message> msgs = getMessages(); List<Integer> targets = new ArrayList<Integer>(msgs.size()); for (Message msg : msgs) { // skip messages that don't need to be changed, or that the client can't modify, doesn't know // about, or has explicitly excluded if (msg.isTagged(tag) == add) { continue; } else if (!msg.canAccess(ACL.RIGHT_WRITE)) { excludeAccess = true; continue; } else if (!msg.checkChangeID() || !TargetConstraint.checkItem(tcon, msg)) { continue; } else if (add && !tag.canTag(msg)) { throw MailServiceException.CANNOT_TAG(tag, this); } targets.add(msg.getId()); msg.tagChanged(tag, add); // since we're adding/removing a tag, the tag's unread count may change int delta = add ? 1 : -1; if (tag.trackUnread() && msg.isUnread()) tag.updateUnread(delta, isTagged(Flag.ID_FLAG_DELETED) ? delta : 0); // if we're adding/removing the \Deleted flag, update the folder and tag "deleted" and // "deleted unread" counts if (tag.getId() == Flag.ID_FLAG_DELETED) { getFolder().updateSize(0, delta, 0); // note that Message.updateUnread() calls updateTagUnread() if (msg.isUnread()) msg.updateUnread(0, delta); } } if (targets.isEmpty()) { if (excludeAccess) throw ServiceException.PERM_DENIED("you do not have sufficient permissions"); } else { if (ZimbraLog.mailop.isDebugEnabled()) { String operation = add ? "Setting" : "Unsetting"; ZimbraLog.mailop.debug( "%s %s for %s. Affected ids: %s", operation, getMailopContext(tag), getMailopContext(this), StringUtil.join(",", targets)); } recalculateCounts(msgs); DbMailItem.alterTag(tag, targets, add); } }