/**
* Licensed to Apereo under one or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information regarding copyright ownership. Apereo
* licenses this file to you under the Apache License, Version 2.0 (the "License"); you may not use
* this file except in compliance with the License. You may obtain a copy of the License at the
* following location:
*
* <p>http://www.apache.org/licenses/LICENSE-2.0
*
* <p>Unless required by applicable law or agreed to in writing, software distributed under the
* License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jasig.portlet.emailpreview.dao.exchange;
import com.microsoft.exchange.messages.BaseRequestType;
import com.microsoft.exchange.messages.BaseResponseMessageType;
import com.microsoft.exchange.messages.DeleteItem;
import com.microsoft.exchange.messages.FindFolder;
import com.microsoft.exchange.messages.FindFolderResponseMessageType;
import com.microsoft.exchange.messages.FindItem;
import com.microsoft.exchange.messages.FindItemResponseMessageType;
import com.microsoft.exchange.messages.FolderInfoResponseMessageType;
import com.microsoft.exchange.messages.GetFolder;
import com.microsoft.exchange.messages.GetItem;
import com.microsoft.exchange.messages.ItemInfoResponseMessageType;
import com.microsoft.exchange.messages.ResponseMessageType;
import com.microsoft.exchange.messages.UpdateItem;
import com.microsoft.exchange.messages.UpdateItemResponseMessageType;
import com.microsoft.exchange.types.*;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.xml.bind.JAXBElement;
import javax.xml.namespace.QName;
import javax.xml.transform.TransformerException;
import net.sf.ehcache.Cache;
import net.sf.ehcache.Element;
import org.jasig.portlet.emailpreview.AccountSummary;
import org.jasig.portlet.emailpreview.EmailMessage;
import org.jasig.portlet.emailpreview.EmailMessageContent;
import org.jasig.portlet.emailpreview.EmailPreviewException;
import org.jasig.portlet.emailpreview.ExchangeEmailMessage;
import org.jasig.portlet.emailpreview.ExchangeFolderDto;
import org.jasig.portlet.emailpreview.MailStoreConfiguration;
import org.jasig.portlet.emailpreview.caching.IMailAccountCacheKeyGenerator;
import org.jasig.portlet.emailpreview.caching.IMessageCacheKeyGenerator;
import org.jasig.portlet.emailpreview.caching.MailAccountCacheKeyGeneratorImpl;
import org.jasig.portlet.emailpreview.caching.UsernameItemCacheKeyGeneratorImpl;
import org.jasig.portlet.emailpreview.dao.IMailAccountDao;
import org.jasig.portlet.emailpreview.service.ICredentialsProvider;
import org.jasig.portlet.emailpreview.service.link.IEmailLinkService;
import org.jasig.portlet.emailpreview.service.link.ILinkServiceRegistry;
import org.jasig.portlet.emailpreview.util.MessageUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.oxm.Marshaller;
import org.springframework.ws.WebServiceMessage;
import org.springframework.ws.client.WebServiceClientException;
import org.springframework.ws.client.core.WebServiceMessageCallback;
import org.springframework.ws.client.core.WebServiceOperations;
import org.springframework.ws.soap.SoapHeaderElement;
import org.springframework.ws.soap.SoapMessage;
import org.springframework.ws.soap.client.core.SoapActionCallback;
import org.springframework.xml.transform.StringResult;
/**
* DAO that uses Exchange Web Services to access messages.
*
* @author James Wennmacher, jwennmacher@unicon.net
*/
// TODO: Efficiency of the NTLM authenticated connections can be significantly improved by saving the HttpContext
// into HttpSession so subsequent requests might use the same connection and already have gone through the
// multi-step authentication process. Another approach might be to alter the http client to use NTLM auth first.
// See http://hc.apache.org/httpcomponents-client-ga/tutorial/html/authentication.html#ntlm
public class ExchangeAccountDaoImpl implements IMailAccountDao<ExchangeFolderDto> {
protected static final String ROOT_SOAP_ACTION =
"http://schemas.microsoft.com/exchange/services/2006/messages/";
protected static final String FIND_FOLDER_SOAP_ACTION = ROOT_SOAP_ACTION + "FindFolder";
protected static final String GET_FOLDER_SOAP_ACTION = ROOT_SOAP_ACTION + "GetFolder";
protected static final String FIND_ITEM_SOAP_ACTION = ROOT_SOAP_ACTION + "FindItem";
protected static final String GET_ITEM_SOAP_ACTION = ROOT_SOAP_ACTION + "GetItem";
protected static final String DELETE_ITEM_SOAP_ACTION = ROOT_SOAP_ACTION + "DeleteItem";
protected static final String UPDATE_ITEM_SOAP_ACTION = ROOT_SOAP_ACTION + "UpdateItem";
protected static final QName REQUEST_SERVER_VERSION_QNAME =
new QName(
"http://schemas.microsoft.com/exchange/services/2006/types",
"RequestServerVersion",
"ns3");
private final Logger log = LoggerFactory.getLogger(this.getClass());
private static final ObjectFactory typeObjectFactory = new ObjectFactory();
private Marshaller marshaller;
@Autowired(required = true)
private ILinkServiceRegistry linkServiceRegistry;
private WebServiceOperations webServiceOperations;
@Autowired private IExchangeAutoDiscoverDao autoDiscoveryDao;
@Autowired(required = true)
private MessageUtils messageUtils;
@Autowired(required = true)
private ICredentialsProvider credentialsService;
private List<String> regexFoldernameExclusionPatterns;
private List<Pattern> foldernameExclusions = new ArrayList<Pattern>();
@Autowired
@Qualifier("exchangeChangeKeyCache")
private Cache idCache; // Used for internal caching of itemIds to changeKeys
private IMessageCacheKeyGenerator idCacheKeyGenerator = new UsernameItemCacheKeyGeneratorImpl();
@Autowired
@Qualifier("exchangeFolderCache")
private Cache folderCache;
private IMailAccountCacheKeyGenerator folderCacheKeyGenerator =
new MailAccountCacheKeyGeneratorImpl();
private String folderCacheKeyPrefix = "ExchangeFolders";
public void setRegexFoldernameExclusionPatterns(List<String> regexFoldernameExclusionPatterns) {
this.regexFoldernameExclusionPatterns = regexFoldernameExclusionPatterns;
foldernameExclusions = new ArrayList<Pattern>();
for (String pattern : regexFoldernameExclusionPatterns) {
foldernameExclusions.add(Pattern.compile(pattern));
}
}
public void setFolderCache(Cache folderCache) {
this.folderCache = folderCache;
}
public void setFolderCacheKeyGenerator(IMailAccountCacheKeyGenerator folderCacheKeyGenerator) {
this.folderCacheKeyGenerator = folderCacheKeyGenerator;
}
public void setFolderCacheKeyPrefix(String folderCacheKeyPrefix) {
this.folderCacheKeyPrefix = folderCacheKeyPrefix;
}
public void setIdCache(Cache idCache) {
this.idCache = idCache;
}
public void setIdCacheKeyGenerator(IMessageCacheKeyGenerator idCacheKeyGenerator) {
this.idCacheKeyGenerator = idCacheKeyGenerator;
}
public void setWebServiceOperations(WebServiceOperations webServiceOperations) {
this.webServiceOperations = webServiceOperations;
}
public void setLinkServiceRegistry(ILinkServiceRegistry linkServiceRegistry) {
this.linkServiceRegistry = linkServiceRegistry;
}
public void setMessageUtils(MessageUtils messageUtils) {
this.messageUtils = messageUtils;
}
public void setCredentialsService(ICredentialsProvider credentialsService) {
this.credentialsService = credentialsService;
}
public void setAutoDiscoveryDao(IExchangeAutoDiscoverDao autoDiscoveryDao) {
this.autoDiscoveryDao = autoDiscoveryDao;
}
public void setMarshaller(Marshaller marshaller) {
this.marshaller = marshaller;
}
// ----------------------------------------------------------
// getEmailSummaries
// ----------------------------------------------------------
@Override
public AccountSummary fetchAccountSummaryFromStore(
MailStoreConfiguration config,
String username,
String mailAccount,
String folder,
int start,
int max) {
try {
long startTime = System.currentTimeMillis();
FolderType folderType = getFolder(folder, config);
List<ExchangeEmailMessage> messages = getMailboxItemSummaries(folderType, start, max, config);
if (log.isDebugEnabled()) {
long elapsedTime = System.currentTimeMillis() - startTime;
int messagesToDisplayCount = messages.size();
log.debug(
"Finished looking up email account summary. Inbox size: "
+ folderType.getTotalCount()
+ " Unread message count: "
+ folderType.getUnreadCount()
+ " Total elapsed time: "
+ elapsedTime
+ "ms "
+ " Time per displayed message: "
+ (messagesToDisplayCount == 0 ? 0 : (elapsedTime / messagesToDisplayCount))
+ "ms");
}
IEmailLinkService linkService =
linkServiceRegistry.getEmailLinkService(config.getLinkServiceKey());
String inboxUrl = null;
if (linkService != null) {
inboxUrl = linkService.getInboxUrl(config);
}
insertChangeKeysIntoCache(messages);
//todo need to get deleteSupported or assume true, maybe quota
return new AccountSummary(
inboxUrl,
messages,
folderType.getUnreadCount(),
folderType.getTotalCount(),
start,
max,
true,
null);
} catch (EmailPreviewException e) {
return new AccountSummary(e);
} catch (WebServiceClientException e) {
return new AccountSummary(e);
}
}
// ----------------------------------------------------------
// getFolder
// ----------------------------------------------------------
private GetFolder createGetFolderSoapMessage(String folderName, MailStoreConfiguration config) {
// GetFolder: see http://msdn.microsoft.com/en-us/library/aa580274%28v=exchg.80%29.aspx
// Construct the SOAP request object to use
GetFolder msg = new GetFolder();
// If the folder is the inbox, look it up directly by name. Otherwise must get the folderId
// from cache or fetching it.
NonEmptyArrayOfBaseFolderIdsType folderList = new NonEmptyArrayOfBaseFolderIdsType();
if (DistinguishedFolderIdNameType.INBOX.value().equalsIgnoreCase(folderName)) {
DistinguishedFolderIdType inboxFolderId = new DistinguishedFolderIdType();
inboxFolderId.setId(DistinguishedFolderIdNameType.INBOX);
folderList.getFolderIdsAndDistinguishedFolderIds().add(inboxFolderId);
} else {
// Retrieve folder id from cache or fetch
String folderId = retrieveFolderId(folderName, config);
if (folderId == null) {
throw new EmailPreviewException("Invalid folder name '" + folderName + "'");
}
FolderIdType folderIdType = new FolderIdType();
folderIdType.setId(folderId);
folderList.getFolderIdsAndDistinguishedFolderIds().add(folderIdType);
}
msg.setFolderIds(folderList);
FolderResponseShapeType shapeType = new FolderResponseShapeType();
shapeType.setBaseShape(DefaultShapeNamesType.DEFAULT);
msg.setFolderShape(shapeType);
return msg;
}
private FolderType getFolder(String folderName, MailStoreConfiguration config) {
FolderInfoResponseMessageType response =
(FolderInfoResponseMessageType)
sendMessageAndExtractSingleResponse(
createGetFolderSoapMessage(folderName, config), GET_FOLDER_SOAP_ACTION, config);
List<BaseFolderType> folders =
response.getFolders().getFoldersAndCalendarFoldersAndContactsFolders();
if (folders.size() != 1) {
log.error("Expected 1 folder to find folder, received " + folders.size());
throw new EmailPreviewException("Multiple folders returned when querying for inbox");
}
FolderType folder = (FolderType) folders.get(0);
return folder;
}
// -------------------------- findItems ---------------------------
private FindItem createFindItemsSoapMessage(FolderType folder, int start, int fetchSize) {
// Construct the SOAP request object to use
FindItem msg = new FindItem();
NonEmptyArrayOfBaseFolderIdsType folderList = new NonEmptyArrayOfBaseFolderIdsType();
folderList.getFolderIdsAndDistinguishedFolderIds().add(folder.getFolderId());
msg.setParentFolderIds(folderList);
msg.setTraversal(ItemQueryTraversalType.SHALLOW);
ItemResponseShapeType shapeType = new ItemResponseShapeType();
// EMAILPLT-159: Use ALL_PROPERTIES because meeting requests will not have a dateSent without it.
// Ran tests and it seemed to add around 250ms to the response time which is not great but not bad.
// Since we cache aggressively, it is probably not worth adding all the specific properties we need and
// testing under all conditions.
shapeType.setBaseShape(DefaultShapeNamesType.ALL_PROPERTIES);
addAdditionalPropertyReplied(shapeType);
msg.setItemShape(shapeType);
IndexedPageViewType paging = new IndexedPageViewType();
int totalMessageCount = folder.getTotalCount();
int entriesToReturn = Math.min(fetchSize, totalMessageCount - start);
entriesToReturn = entriesToReturn > 0 ? entriesToReturn : 1;
paging.setOffset(start);
paging.setMaxEntriesReturned(entriesToReturn);
paging.setBasePoint(IndexBasePointType.BEGINNING);
msg.setIndexedPageItemView(paging);
FieldOrderType order = new FieldOrderType();
order.setOrder(SortDirectionType.DESCENDING);
PathToUnindexedFieldType orderField = new PathToUnindexedFieldType();
orderField.setFieldURI(UnindexedFieldURIType.ITEM_DATE_TIME_RECEIVED);
order.setPath(typeObjectFactory.createFieldURI(orderField));
NonEmptyArrayOfFieldOrdersType ordersType = new NonEmptyArrayOfFieldOrdersType();
ordersType.getFieldOrders().add(order);
msg.setSortOrder(ordersType);
return msg;
}
private void addAdditionalPropertyReplied(ItemResponseShapeType shapeType) {
// For replied-to flag. See PR_LAST_VERB_EXECUTED. See
// http://social.msdn.microsoft.com/Forums/en-US/outlookdev/thread/a965e87b-1051-45e2-b093-35cba4b82e05
// http://msdn.microsoft.com/en-us/library/cc433482%28v=EXCHG.80%29.aspx
// http://www.outlookforums.com/threads/24025-ews-soap-how-tell-if-message-has-been-forwarded-replied/
NonEmptyArrayOfPathsToElementType props = new NonEmptyArrayOfPathsToElementType();
PathToExtendedFieldType prLastVerbExecuted = new PathToExtendedFieldType();
prLastVerbExecuted.setPropertyTag("0x1081");
prLastVerbExecuted.setPropertyType(MapiPropertyTypeType.INTEGER);
props.getPaths().add(typeObjectFactory.createExtendedFieldURI(prLastVerbExecuted));
shapeType.setAdditionalProperties(props);
}
private List<ExchangeEmailMessage> getMailboxItemSummaries(
FolderType folder, int start, int fetchSize, MailStoreConfiguration config) {
FindItem soapMessage = createFindItemsSoapMessage(folder, start, fetchSize);
FindItemResponseMessageType response =
(FindItemResponseMessageType)
sendMessageAndExtractSingleResponse(soapMessage, FIND_ITEM_SOAP_ACTION, config);
FindItemParentType rootFolder = response.getRootFolder();
List<ItemType> items = rootFolder.getItems().getItemsAndMessagesAndCalendarItems();
List<ExchangeEmailMessage> messages = new ArrayList<ExchangeEmailMessage>();
int messageNumber = start;
String contentType = null; //sensible default
boolean deleted = false; //sensible default
for (ItemType itemType : items) {
ExchangeEmailMessage message;
Date dateSent =
itemType.getDateTimeSent() != null
? new Date(itemType.getDateTimeSent().toGregorianCalendar().getTimeInMillis())
: new Date();
if (itemType instanceof MessageType) {
MessageType item = (MessageType) itemType;
// From can be null if you have a draft email that isn't filled out
String from = item.getFrom() != null ? item.getFrom().getMailbox().getName() : "";
boolean answered = false; //sensible default
if (item.getExtendedProperties().size() > 0) {
ExtendedPropertyType prLastVerbExecuted = item.getExtendedProperties().iterator().next();
String propValue = prLastVerbExecuted.getValue();
// From MS-OXOMG protocol document: 102 = ReplyToSender, 103 = ReplyToAll, 104 = Forward
answered = "102".equals(propValue) || "103".equals(propValue);
}
message =
new ExchangeEmailMessage(
messageNumber,
item.getItemId().getId(),
item.getItemId().getChangeKey(),
messageUtils.cleanHTML(from),
messageUtils.cleanHTML(item.getSubject()),
dateSent,
!item.isIsRead(),
answered,
deleted,
item.isHasAttachments(),
contentType,
null,
null,
null,
null);
// EMAILPLT-162 Can add importance someday to model using
// boolean highImportance = item.getImportance() != null ? item.getImportance().value().equals(ImportanceChoicesType.HIGH.value()) : false;
} else {
log.debug("Found message of type {} in exchange folder", itemType.getClass());
boolean isRead = false; // have to pick a default
String from = ""; // have to pick a default
String subject = messageUtils.cleanHTML(itemType.getSubject());
if (itemType instanceof PostItemType) {
PostItemType item = (PostItemType) itemType;
isRead = item.isIsRead();
from =
item.getFrom() != null && item.getFrom().getMailbox() != null
? messageUtils.cleanHTML(item.getFrom().getMailbox().getName())
: from;
} else if (itemType instanceof CalendarItemType) {
CalendarItemType item = (CalendarItemType) itemType;
from =
item.getOrganizer() != null && item.getOrganizer().getMailbox() != null
? messageUtils.cleanHTML(item.getOrganizer().getMailbox().getName())
: from;
} else if (itemType instanceof DistributionListType) {
// Do nothing
} else if (itemType instanceof TaskType) {
// Do nothing
} else if (itemType instanceof ContactItemType) {
// Do nothing
}
// Create a place holder to represent this message.
message =
new ExchangeEmailMessage(
messageNumber,
itemType.getItemId().getId(),
itemType.getItemId().getChangeKey(),
from,
subject,
dateSent,
!isRead,
false,
deleted,
itemType.isHasAttachments(),
contentType,
null,
null,
null,
null);
}
messages.add(message);
messageNumber++;
}
return messages;
}
// ----------------------------------------------------------
// get Email message
// ----------------------------------------------------------
@Override
public EmailMessage getMessage(MailStoreConfiguration storeConfig, String uuid) {
ItemInfoResponseMessageType response =
(ItemInfoResponseMessageType)
sendMessageAndExtractSingleResponse(
createGetItemSoapMessage(uuid, DefaultShapeNamesType.ALL_PROPERTIES),
GET_ITEM_SOAP_ACTION,
storeConfig);
ExchangeEmailMessage msg;
ItemType itemType = response.getItems().getItemsAndMessagesAndCalendarItems().get(0);
boolean answered = false; // Sensible default
boolean deleted = false; // Sensible default
Date dateSent =
itemType.getDateTimeSent() != null
? new Date(itemType.getDateTimeSent().toGregorianCalendar().getTimeInMillis())
: new Date();
if (itemType instanceof MessageType) {
MessageType message = (MessageType) itemType;
String sender = getOriginatorEmailAddress(message);
String contentType = message.getBody().getBodyType().value();
EmailMessageContent content =
new EmailMessageContent(
messageUtils.cleanHTML(message.getBody().getValue()),
BodyTypeType.HTML.equals(message.getBody().getBodyType()));
String toRecipients = getToRecipients(message);
String ccRecipients = getCcRecipients(message);
String bccRecipients = getBccRecipients(message);
msg =
new ExchangeEmailMessage(
0,
message.getItemId().getId(),
message.getItemId().getChangeKey(),
sender,
messageUtils.cleanHTML(message.getSubject()),
dateSent,
!message.isIsRead(),
answered,
deleted,
message.isHasAttachments(),
contentType,
content,
toRecipients,
ccRecipients,
bccRecipients);
} else {
// For message detail, default to 'isRead' for non-MessageType messages so the auto-mark message as read
// feature won't attempt to set read=true status on a message type that doesn't support it and cause
// an error.
boolean isRead = true;
String sender = ""; // have to pick a default
String subject = messageUtils.cleanHTML(itemType.getSubject());
String toRecipients = "";
String ccRecipients = "";
String bccRecipients = "";
String contentString =
itemType.getBody() != null
? messageUtils.cleanHTML(itemType.getBody().getValue())
: "Exchange message of type "
+ itemType.getClass().getSimpleName()
+ " cannot be displayed. Please view using email client or web viewer.";
String contentType =
itemType.getBody() != null && itemType.getBody().getBodyType() != null
? itemType.getBody().getBodyType().value()
: "text/plain";
EmailMessageContent content =
new EmailMessageContent(contentString, BodyTypeType.HTML.equals(contentType));
if (itemType instanceof PostItemType) {
PostItemType item = (PostItemType) itemType;
isRead = item.isIsRead();
sender =
item.getFrom() != null && item.getFrom().getMailbox() != null
? messageUtils.cleanHTML(item.getFrom().getMailbox().getName())
: sender;
} else if (itemType instanceof CalendarItemType) {
CalendarItemType item = (CalendarItemType) itemType;
sender =
item.getOrganizer() != null && item.getOrganizer().getMailbox() != null
? messageUtils.cleanHTML(item.getOrganizer().getMailbox().getName())
: sender;
} else if (itemType instanceof DistributionListType) {
// Do nothing
} else if (itemType instanceof TaskType) {
// Do nothing
} else if (itemType instanceof ContactItemType) {
// Do nothing
}
msg =
new ExchangeEmailMessage(
0,
itemType.getItemId().getId(),
itemType.getItemId().getChangeKey(),
sender,
subject,
dateSent,
!isRead,
answered,
deleted,
itemType.isHasAttachments(),
contentType,
content,
toRecipients,
ccRecipients,
bccRecipients);
}
// Insert the changeKey into cache in case the message read status is changed again.
insertChangeKeyIntoCache(msg.getMessageId(), msg.getExchangeChangeKey());
return msg;
}
private String getToRecipients(MessageType message) {
return getRecipients(message.getToRecipients());
}
private String getCcRecipients(MessageType message) {
return getRecipients(message.getCcRecipients());
}
private String getBccRecipients(MessageType message) {
return getRecipients(message.getBccRecipients());
}
private String getRecipients(ArrayOfRecipientsType addrs) {
StringBuilder str = new StringBuilder();
if (addrs != null) {
for (EmailAddressType addr : addrs.getMailboxes()) {
str.append(formatEmailAddress(addr));
str.append("; ");
}
// Delete the trailing ; space
str.deleteCharAt(str.length() - 1);
str.deleteCharAt(str.length() - 1);
}
return str.toString();
}
// Returns originator's email address. Should be the from, but if not specified check the
// sender just in case to try and return something useful.
private String getOriginatorEmailAddress(MessageType message) {
if (message.getFrom() != null) {
return formatEmailAddress(message.getFrom().getMailbox());
} else if (message.getSender() != null) {
return formatEmailAddress(message.getSender().getMailbox());
}
return "Not specified";
}
private String formatEmailAddress(EmailAddressType emailAddr) {
return emailAddr.getName() + " <" + emailAddr.getEmailAddress() + ">";
}
public ItemIdType getMessageChangeKey(String uuid, MailStoreConfiguration config) {
ItemInfoResponseMessageType response =
(ItemInfoResponseMessageType)
sendMessageAndExtractSingleResponse(
createGetItemSoapMessage(uuid, DefaultShapeNamesType.ID_ONLY),
GET_ITEM_SOAP_ACTION,
config);
MessageType message =
(MessageType) response.getItems().getItemsAndMessagesAndCalendarItems().get(0);
return message.getItemId();
}
private GetItem createGetItemSoapMessage(String uuid, DefaultShapeNamesType itemShape) {
// Construct the SOAP request object to use
GetItem msg = new GetItem();
NonEmptyArrayOfBaseItemIdsType itemList = new NonEmptyArrayOfBaseItemIdsType();
ItemIdType item = new ItemIdType();
item.setId(uuid);
itemList.getItemIdsAndOccurrenceItemIdsAndRecurringMasterItemIds().add(item);
msg.setItemIds(itemList);
ItemResponseShapeType shape = new ItemResponseShapeType();
shape.setBaseShape(itemShape);
shape.setIncludeMimeContent(true);
shape.setBodyType(BodyTypeResponseType.BEST);
addAdditionalPropertyReplied(shape);
msg.setItemShape(shape);
return msg;
}
// ----------------------------------------------------------
// delete messages
// ----------------------------------------------------------
@Override
public boolean deleteMessages(MailStoreConfiguration storeConfig, String[] uuids) {
sendMessageAndExtractSingleResponse(
createDeleteItemsSoapMessage(uuids), DELETE_ITEM_SOAP_ACTION, storeConfig);
return true;
}
private DeleteItem createDeleteItemsSoapMessage(String[] uuids) {
DeleteItem msg = new DeleteItem();
msg.setDeleteType(DisposalType.MOVE_TO_DELETED_ITEMS);
NonEmptyArrayOfBaseItemIdsType itemList = new NonEmptyArrayOfBaseItemIdsType();
for (String uuid : uuids) {
ItemIdType item = new ItemIdType();
item.setId(uuid);
itemList.getItemIdsAndOccurrenceItemIdsAndRecurringMasterItemIds().add(item);
}
msg.setItemIds(itemList);
return msg;
}
// ----------------------------------------------------------
// set message read status
// ----------------------------------------------------------
@Override
public boolean setMessageReadStatus(
MailStoreConfiguration storeConfig, String[] uuids, boolean read) {
UpdateItemResponseMessageType soapResponse =
(UpdateItemResponseMessageType)
sendMessageAndExtractSingleResponse(
createUpdateItemSoapMessage(uuids, read, storeConfig),
UPDATE_ITEM_SOAP_ACTION,
storeConfig);
List<ItemType> updatedItems = soapResponse.getItems().getItemsAndMessagesAndCalendarItems();
// The changeKeys will have changed, so update them in cache.
for (ItemType updatedItem : updatedItems) {
ItemIdType itemId = updatedItem.getItemId();
insertChangeKeyIntoCache(itemId);
}
return true;
}
private UpdateItem createUpdateItemSoapMessage(
String[] uuids, boolean read, MailStoreConfiguration config) {
UpdateItem soapMessage = new UpdateItem();
// Object indicating change field isRead to value of read
SetItemFieldType change = new SetItemFieldType();
PathToUnindexedFieldType field = new PathToUnindexedFieldType();
field.setFieldURI(UnindexedFieldURIType.MESSAGE_IS_READ);
change.setPath(typeObjectFactory.createFieldURI(field));
MessageType message = new MessageType();
message.setIsRead(read);
change.setMessage(message);
NonEmptyArrayOfItemChangeDescriptionsType changes =
new NonEmptyArrayOfItemChangeDescriptionsType();
changes.getAppendToItemFieldsAndSetItemFieldsAndDeleteItemFields().add(change);
NonEmptyArrayOfItemChangesType changeList = new NonEmptyArrayOfItemChangesType();
for (String uuid : uuids) {
ItemChangeType itemChange = new ItemChangeType();
itemChange.setItemId(getItemIdType(uuid, config));
itemChange.setUpdates(changes);
changeList.getItemChanges().add(itemChange);
}
soapMessage.setItemChanges(changeList);
soapMessage.setMessageDisposition(MessageDispositionType.SAVE_ONLY);
soapMessage.setConflictResolution(ConflictResolutionType.ALWAYS_OVERWRITE);
return soapMessage;
}
// ----------------------------------------------------------
// get inbox folders
// ----------------------------------------------------------
@Override
public List<ExchangeFolderDto> getAllUserInboxFolders(MailStoreConfiguration storeConfig) {
String key =
folderCacheKeyGenerator.getKey(
credentialsService.getUsername(), storeConfig.getMailAccount(), folderCacheKeyPrefix);
Element element = folderCache.get(key);
if (element != null) {
return (List<ExchangeFolderDto>) element.getObjectValue();
}
log.debug("User {} folders not in cache. Fetching all folders", storeConfig.getMailAccount());
FindFolderResponseMessageType response =
(FindFolderResponseMessageType)
sendMessageAndExtractSingleResponse(
createFindFoldersSoapMessage(), FIND_FOLDER_SOAP_ACTION, storeConfig);
FindFolderParentType rootFolder = response.getRootFolder();
List<BaseFolderType> folderList =
rootFolder.getFolders().getFoldersAndCalendarFoldersAndContactsFolders();
List<ExchangeFolderDto> folders = new ArrayList<ExchangeFolderDto>(folderList.size());
// Create a list of the folders, removing those that match the exclusion patterns or are not BaseFolders
// (using the class removes Calendar, Contacts, Tasks, and Search Folders).
for (BaseFolderType baseFolder : folderList) {
if (baseFolder.getClass().equals(FolderType.class)) {
if (!matchesExcludedFolders(baseFolder)) {
FolderType exchangeFolder = (FolderType) baseFolder;
ExchangeFolderDto folder =
new ExchangeFolderDto(
exchangeFolder.getFolderId().getId(),
exchangeFolder.getDisplayName(),
exchangeFolder.getTotalCount(),
exchangeFolder.getUnreadCount());
folders.add(folder);
}
}
}
folderCache.put(new Element(key, folders));
return folders;
}
private String retrieveFolderId(String folderName, MailStoreConfiguration config) {
List<ExchangeFolderDto> folders;
// Retrieve from cache or fetch
String key =
folderCacheKeyGenerator.getKey(
credentialsService.getUsername(), config.getMailAccount(), folderCacheKeyPrefix);
Element element = folderCache.get(key);
if (element != null) {
folders = (List<ExchangeFolderDto>) element.getObjectValue();
} else {
folders = getAllUserInboxFolders(config);
}
for (ExchangeFolderDto folder : folders) {
if (folderName.equals(folder.getName())) {
return folder.getId();
}
}
return null;
}
private boolean matchesExcludedFolders(BaseFolderType folder) {
String folderName = folder.getDisplayName();
for (Pattern pattern : foldernameExclusions) {
Matcher matcher = pattern.matcher(folderName);
if (matcher.matches()) {
return true;
}
}
return false;
}
private FindFolder createFindFoldersSoapMessage() {
// Construct the SOAP request object to use
FindFolder msg = new FindFolder();
// folder = root
NonEmptyArrayOfBaseFolderIdsType folderList = new NonEmptyArrayOfBaseFolderIdsType();
DistinguishedFolderIdType inboxFolderId = new DistinguishedFolderIdType();
inboxFolderId.setId(DistinguishedFolderIdNameType.MSGFOLDERROOT);
folderList.getFolderIdsAndDistinguishedFolderIds().add(inboxFolderId);
msg.setParentFolderIds(folderList);
msg.setTraversal(FolderQueryTraversalType.DEEP);
FolderResponseShapeType shapeType = new FolderResponseShapeType();
shapeType.setBaseShape(DefaultShapeNamesType.DEFAULT);
msg.setFolderShape(shapeType);
return msg;
}
// ----------------------------------------------------------
// common send message and parse response
// ----------------------------------------------------------
private BaseResponseMessageType sendSoapRequest(
BaseRequestType soapRequest, String soapAction, MailStoreConfiguration config) {
String uri = autoDiscoveryDao.getEndpointUri(config);
try {
final WebServiceMessageCallback actionCallback = new SoapActionCallback(soapAction);
final WebServiceMessageCallback customCallback =
new WebServiceMessageCallback() {
@Override
public void doWithMessage(WebServiceMessage message)
throws IOException, TransformerException {
actionCallback.doWithMessage(message);
SoapMessage soap = (SoapMessage) message;
SoapHeaderElement version =
soap.getEnvelope().getHeader().addHeaderElement(REQUEST_SERVER_VERSION_QNAME);
version.addAttribute(new QName("Version"), "Exchange2007_SP1");
}
};
if (log.isDebugEnabled()) {
StringResult message = new StringResult();
try {
marshaller.marshal(soapRequest, message);
log.debug(
"Attempting to send SOAP request to {}\nSoap Action: {}\nSoap message body"
+ " (not exact, log org.apache.http.wire to see actual message):\n{}",
uri,
soapAction,
message);
} catch (IOException ex) {
log.debug("IOException attempting to display soap response", ex);
}
}
// use the request to retrieve data from the Exchange server
BaseResponseMessageType response =
(BaseResponseMessageType)
webServiceOperations.marshalSendAndReceive(uri, soapRequest, customCallback);
if (log.isDebugEnabled()) {
StringResult messageResponse = new StringResult();
try {
marshaller.marshal(response, messageResponse);
log.debug(
"Soap response body (not exact, log org.apache.http.wire to see actual message):\n{}",
messageResponse);
} catch (IOException ex) {
log.debug("IOException attempting to display soap response", ex);
}
}
return response;
} catch (WebServiceClientException e) {
// todo should we bother catching/wrapping? I think runtime exceptions should be caught at service layer
throw new EmailPreviewException(e);
}
//todo figure out if we can catch authentication exceptions to return them separate. Useful?
}
private ResponseMessageType sendMessageAndExtractSingleResponse(
BaseRequestType soapRequest, String soapAction, MailStoreConfiguration config) {
BaseResponseMessageType soapResponse = sendSoapRequest(soapRequest, soapAction, config);
boolean warning = false;
boolean error = false;
StringBuilder msg = new StringBuilder();
List<JAXBElement<? extends ResponseMessageType>> responseMessages =
soapResponse
.getResponseMessages()
.getCreateItemResponseMessagesAndDeleteItemResponseMessagesAndGetItemResponseMessages();
for (JAXBElement<? extends ResponseMessageType> resp : responseMessages) {
if (ResponseClassType.ERROR.equals(resp.getValue().getResponseClass())) {
error = true;
msg.append("Error: ")
.append(resp.getValue().getResponseCode().value())
.append(": ")
.append(resp.getValue().getMessageText())
.append("\n");
} else if (ResponseClassType.WARNING.equals(resp.getValue().getResponseClass())) {
warning = true;
msg.append("Warning: ")
.append(resp.getValue().getResponseCode().value())
.append(": ")
.append(resp.getValue().getMessageText())
.append("\n");
}
}
if (warning || error) {
StringBuilder errorMessage =
new StringBuilder(
"Unexpected response from soap action: "
+ soapAction
+ ".\nSoap Request: "
+ soapRequest.toString()
+ "\n");
errorMessage.append(msg);
if (error) {
throw new EmailPreviewException(
"Error performing Exchange web service action "
+ soapAction
+ ". Error code is "
+ errorMessage.toString());
}
log.warn(
"Received warning response to soap request "
+ soapAction
+ ". Error text is:\n"
+ errorMessage.toString());
throw new EmailPreviewException(
"Unable to perform "
+ soapAction
+ " operation; try again later. Message text: "
+ errorMessage.toString());
}
// Currently all message requests return only one value, except a multi-delete or multi-readUpdate
// in which we only care about having an error or not.
return responseMessages.get(0).getValue();
}
// --------------- changeKey Cache support
private void insertChangeKeysIntoCache(List<ExchangeEmailMessage> messages) {
for (ExchangeEmailMessage message : messages) {
insertChangeKeyIntoCache(message.getMessageId(), message.getExchangeChangeKey());
}
}
private void insertChangeKeyIntoCache(String uuid, String changeKey) {
String key = idCacheKeyGenerator.getKey(credentialsService.getUsername(), uuid);
idCache.put(new Element(key, changeKey));
}
private void insertChangeKeyIntoCache(ItemIdType itemId) {
insertChangeKeyIntoCache(itemId.getId(), itemId.getChangeKey());
}
private ItemIdType getItemIdType(String uuid, MailStoreConfiguration config) {
String key = idCacheKeyGenerator.getKey(credentialsService.getUsername(), uuid);
Element changeKey = idCache.get(key);
if (changeKey == null) {
ItemIdType itemId = getMessageChangeKey(uuid, config);
insertChangeKeyIntoCache(uuid, itemId.getChangeKey());
return itemId;
}
ItemIdType itemIdType = new ItemIdType();
itemIdType.setId(uuid);
itemIdType.setChangeKey((String) changeKey.getObjectValue());
return itemIdType;
}
}