
org.frankframework.filesystem.ExchangeFileSystem Maven / Gradle / Ivy
/*
Copyright 2019-2024 WeAreFrank!
Licensed 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
http://www.apache.org/licenses/LICENSE-2.0
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.frankframework.filesystem;
import java.io.UnsupportedEncodingException;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.file.DirectoryStream;
import java.util.Collections;
import java.util.Date;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.stream.Collectors;
import com.microsoft.aad.msal4j.ClientCredentialFactory;
import com.microsoft.aad.msal4j.ClientCredentialParameters;
import com.microsoft.aad.msal4j.ConfidentialClientApplication;
import com.microsoft.aad.msal4j.IAuthenticationResult;
import jakarta.annotation.Nullable;
import jakarta.mail.internet.InternetAddress;
import lombok.Getter;
import lombok.Setter;
import microsoft.exchange.webservices.data.autodiscover.IAutodiscoverRedirectionUrl;
import microsoft.exchange.webservices.data.core.ExchangeService;
import microsoft.exchange.webservices.data.core.PropertySet;
import microsoft.exchange.webservices.data.core.WebProxy;
import microsoft.exchange.webservices.data.core.enumeration.misc.ConnectingIdType;
import microsoft.exchange.webservices.data.core.enumeration.misc.ExchangeVersion;
import microsoft.exchange.webservices.data.core.enumeration.misc.error.ServiceError;
import microsoft.exchange.webservices.data.core.enumeration.property.WellKnownFolderName;
import microsoft.exchange.webservices.data.core.enumeration.search.SortDirection;
import microsoft.exchange.webservices.data.core.enumeration.service.DeleteMode;
import microsoft.exchange.webservices.data.core.exception.service.local.ServiceLocalException;
import microsoft.exchange.webservices.data.core.exception.service.local.ServiceVersionException;
import microsoft.exchange.webservices.data.core.exception.service.remote.ServiceResponseException;
import microsoft.exchange.webservices.data.core.service.folder.Folder;
import microsoft.exchange.webservices.data.core.service.item.EmailMessage;
import microsoft.exchange.webservices.data.core.service.item.Item;
import microsoft.exchange.webservices.data.core.service.response.ResponseMessage;
import microsoft.exchange.webservices.data.core.service.schema.EmailMessageSchema;
import microsoft.exchange.webservices.data.core.service.schema.FolderSchema;
import microsoft.exchange.webservices.data.core.service.schema.ItemSchema;
import microsoft.exchange.webservices.data.credential.WebProxyCredentials;
import microsoft.exchange.webservices.data.misc.ImpersonatedUserId;
import microsoft.exchange.webservices.data.property.complex.Attachment;
import microsoft.exchange.webservices.data.property.complex.AttachmentCollection;
import microsoft.exchange.webservices.data.property.complex.EmailAddress;
import microsoft.exchange.webservices.data.property.complex.EmailAddressCollection;
import microsoft.exchange.webservices.data.property.complex.FileAttachment;
import microsoft.exchange.webservices.data.property.complex.FolderId;
import microsoft.exchange.webservices.data.property.complex.InternetMessageHeader;
import microsoft.exchange.webservices.data.property.complex.InternetMessageHeaderCollection;
import microsoft.exchange.webservices.data.property.complex.ItemAttachment;
import microsoft.exchange.webservices.data.property.complex.ItemId;
import microsoft.exchange.webservices.data.property.complex.Mailbox;
import microsoft.exchange.webservices.data.property.complex.MessageBody;
import microsoft.exchange.webservices.data.property.complex.MimeContent;
import microsoft.exchange.webservices.data.search.FindFoldersResults;
import microsoft.exchange.webservices.data.search.FindItemsResults;
import microsoft.exchange.webservices.data.search.FolderView;
import microsoft.exchange.webservices.data.search.ItemView;
import microsoft.exchange.webservices.data.search.filter.SearchFilter;
import org.apache.commons.lang3.StringUtils;
import org.frankframework.configuration.ConfigurationException;
import org.frankframework.core.SenderException;
import org.frankframework.encryption.HasKeystore;
import org.frankframework.encryption.HasTruststore;
import org.frankframework.encryption.KeystoreType;
import org.frankframework.receivers.ExchangeMailListener;
import org.frankframework.stream.Message;
import org.frankframework.util.CredentialFactory;
import org.frankframework.util.SpringUtils;
import org.frankframework.util.StringUtil;
import org.frankframework.xml.SaxElementBuilder;
import org.springframework.context.ApplicationContext;
/**
* Implementation of a {@link IBasicFileSystem} of an Exchange Mail Inbox.
*
* To make use of modern authentication:
*
* - Create an application in Azure AD -> App Registrations. For more information please read {@link "https://learn.microsoft.com/en-us/exchange/client-developer/exchange-web-services/how-to-authenticate-an-ews-application-by-using-oauth"}
* - Request the required API permissions within desired scope
https://outlook.office365.com/
in Azure AD -> App Registrations -> MyApp -> API Permissions.
* - Create a secret for your application in Azure AD -> App Registrations -> MyApp -> Certificates and Secrets
* - Configure the clientSecret directly as password or as the password of a JAAS entry referred to by authAlias. Only available upon creation of your secret in the previous step.
* - Configure the clientId directly as username or as the username of a JAAS entry referred to by authAlias which could be retrieved from Azure AD -> App Registrations -> MyApp -> Overview
* - Configure the tenantId which could be retrieved from Azure AD -> App Registrations -> MyApp -> Overview
* - Make sure your application is able to reach
https://login.microsoftonline.com
. Required for token retrieval.
*
*
*
* N.B. MS Exchange is susceptible to problems with invalid XML characters, like .
* To work around these problems, a special streaming XMLInputFactory is configured in
* META-INF/services/javax.xml.stream.XMLInputFactory as org.frankframework.xml.StaxParserFactory
*
* @author Peter Leeuwenburgh (as {@link ExchangeMailListener})
* @author Gerrit van Brakel
*/
public class ExchangeFileSystem extends MailFileSystemBase implements HasKeystore, HasTruststore {
private final @Getter String domain = "Exchange";
private final @Getter ClassLoader configurationClassLoader = Thread.currentThread().getContextClassLoader();
private @Getter @Setter ApplicationContext applicationContext;
private final @Getter String name = "ExchangeFileSystem";
private @Getter String mailAddress;
private @Getter String mailboxObjectSeparator = "|";
private @Getter boolean validateAllRedirectUrls = true;
private @Getter String url;
private @Getter String filter;
private @Getter String proxyHost = null;
private @Getter int proxyPort = 8080;
private @Getter String proxyUsername = null;
private @Getter String proxyPassword = null;
private @Getter String proxyAuthAlias = null;
private @Getter String proxyDomain = null;
private static final String AUTHORITY = "https://login.microsoftonline.com/";
private static final String SCOPE = "https://outlook.office365.com/.default";
private static final String ANCHOR_HEADER = "X-AnchorMailbox";
private @Getter String clientId = null;
private @Getter String clientSecret = null;
private @Getter String tenantId = null;
/* SSL */
private @Getter @Setter String keystore;
private @Getter @Setter String keystoreAuthAlias;
private @Getter @Setter String keystorePassword;
private @Getter @Setter KeystoreType keystoreType = KeystoreType.PKCS12;
private @Getter @Setter String keystoreAlias;
private @Getter @Setter String keystoreAliasAuthAlias;
private @Getter @Setter String keystoreAliasPassword;
private @Getter @Setter String keyManagerAlgorithm = null;
private @Getter @Setter String truststore = null;
private @Getter @Setter String truststoreAuthAlias;
private @Getter @Setter String truststorePassword = null;
private @Getter @Setter KeystoreType truststoreType = KeystoreType.JKS;
private @Getter @Setter String trustManagerAlgorithm = null;
private @Getter @Setter boolean allowSelfSignedCertificates = false;
private @Getter @Setter boolean verifyHostname = true;
private @Getter @Setter boolean ignoreCertificateExpiredException = false;
private @Getter @Setter boolean enableConnectionTracing = false;
private @Getter CredentialFactory credentials = null;
private @Getter CredentialFactory proxyCredentials = null;
private ConfidentialClientApplication client;
private MsalClientAdapter msalClientAdapter;
private ExecutorService executor;
private ClientCredentialParameters clientCredentialParam;
private FolderId baseFolderId;
@Override
public void configure() throws ConfigurationException {
log.debug("Configuring the ExchangeFileSystem spring bean");
if (StringUtils.isNotEmpty(getFilter())) {
if (!getFilter().equalsIgnoreCase("NDR")) {
throw new ConfigurationException("illegal value for filter [" + getFilter() + "], must be 'NDR' or empty");
}
}
if (StringUtils.isEmpty(getUrl()) && StringUtils.isEmpty(getMailAddress())) {
throw new ConfigurationException("either url or mailAddress needs to be specified");
}
if (StringUtils.isNotEmpty(getTenantId())) {
credentials = new CredentialFactory(getAuthAlias(), getClientId(), getClientSecret());
} else {
credentials = new CredentialFactory(getAuthAlias(), getUsername(), getPassword());
}
if (StringUtils.isNotEmpty(getProxyHost()) && (StringUtils.isNotEmpty(getProxyAuthAlias()) || StringUtils.isNotEmpty(getProxyUsername()) || StringUtils.isNotEmpty(getProxyPassword()))) {
proxyCredentials = new CredentialFactory(getProxyAuthAlias(), getProxyUsername(), getProxyPassword());
}
if (StringUtils.isNotEmpty(getTenantId())) {
msalClientAdapter = applicationContext != null ? SpringUtils.createBean(applicationContext, MsalClientAdapter.class) : new MsalClientAdapter();
msalClientAdapter.setProxyHost(getProxyHost());
msalClientAdapter.setProxyPort(getProxyPort());
CredentialFactory proxyCf = getProxyCredentials();
if (proxyCf != null) {
msalClientAdapter.setProxyUsername(proxyCf.getUsername());
msalClientAdapter.setProxyPassword(proxyCf.getPassword());
}
msalClientAdapter.setKeystore(getKeystore());
msalClientAdapter.setKeystoreType(getKeystoreType());
msalClientAdapter.setKeystoreAuthAlias(getKeystoreAuthAlias());
msalClientAdapter.setKeystorePassword(getKeystorePassword());
msalClientAdapter.setKeystoreAlias(getKeystoreAlias());
msalClientAdapter.setKeystoreAliasAuthAlias(getKeystoreAliasAuthAlias());
msalClientAdapter.setKeystoreAliasPassword(getKeystoreAliasPassword());
msalClientAdapter.setKeyManagerAlgorithm(getKeyManagerAlgorithm());
msalClientAdapter.setTruststore(getTruststore());
msalClientAdapter.setTruststoreType(getTruststoreType());
msalClientAdapter.setTruststoreAuthAlias(getTruststoreAuthAlias());
msalClientAdapter.setTruststorePassword(getTruststorePassword());
msalClientAdapter.setTrustManagerAlgorithm(getTrustManagerAlgorithm());
msalClientAdapter.setVerifyHostname(isVerifyHostname());
msalClientAdapter.setAllowSelfSignedCertificates(isAllowSelfSignedCertificates());
msalClientAdapter.setIgnoreCertificateExpiredException(isIgnoreCertificateExpiredException());
msalClientAdapter.configure();
clientCredentialParam = ClientCredentialParameters.builder(
Collections.singleton(SCOPE)
).tenant(getTenantId()).build();
}
}
@Override
public void open() throws FileSystemException {
log.debug("Opening the ExchangeFileSystem");
super.open();
if (msalClientAdapter != null) {
executor = Executors.newSingleThreadExecutor(); //Create a new Executor in the same thread(context) to avoid SecurityExceptions when setting a ClassLoader on the Runnable.
CredentialFactory cf = getCredentials();
try {
msalClientAdapter.open();
client = ConfidentialClientApplication.builder(
cf.getUsername(),
ClientCredentialFactory.createFromSecret(cf.getPassword()))
.authority(AUTHORITY + getTenantId())
.httpClient(msalClientAdapter)
.executorService(executor)
.build();
} catch (MalformedURLException | SenderException e) {
throw new FileSystemException("Failed to initialize MSAL ConfidentialClientApplication.", e);
}
}
baseFolderId = getBaseFolderId(getMailAddress(), getBaseFolder());
}
@Override
public void close() throws FileSystemException {
log.debug("Closing the ExchangeFileSystem");
try {
super.close();
if (msalClientAdapter != null) {
msalClientAdapter.close();
client = null;
}
} finally {
if (executor != null) {
executor.shutdown();
executor = null;
}
}
}
public FolderId getBaseFolderId(String emailAddress, String baseFolderName) throws FileSystemException {
log.debug("searching inbox ");
FolderId inboxId;
if (StringUtils.isNotEmpty(emailAddress)) {
Mailbox mailbox = new Mailbox(emailAddress);
inboxId = new FolderId(WellKnownFolderName.Inbox, mailbox);
} else {
inboxId = new FolderId(WellKnownFolderName.Inbox);
}
log.debug("determined inbox [{}] foldername [{}]", ()->inboxId, inboxId::getFolderName);
FolderId basefolderId;
if (StringUtils.isNotEmpty(baseFolderName)) {
try {
basefolderId = findFolder(inboxId, baseFolderName);
} catch (Exception e) {
throw new FolderNotFoundException("Could not find baseFolder [" + baseFolderName + "] as subfolder of [" + inboxId.getFolderName() + "]", e);
}
if (basefolderId == null) {
log.debug("Could not get baseFolder [{}] as subfolder of [{}]", ()->baseFolderName, inboxId::getFolderName);
basefolderId = findFolder(null, baseFolderName);
}
if (basefolderId == null) {
throw new FolderNotFoundException("Could not find baseFolder [" + baseFolderName + "]");
}
} else {
basefolderId = inboxId;
}
return basefolderId;
}
@Override
protected ExchangeService createConnection() throws FileSystemException {
log.debug("Creating new connection to the ExchangeFileSystem");
ExchangeService exchangeService = new ExchangeService(ExchangeVersion.Exchange2010_SP2);
if (enableConnectionTracing) {
log.debug("Enabling tracing on the Exchange connection");
exchangeService.setTraceEnabled(true);
}
setCredentialsOnService(exchangeService);
if (StringUtils.isNotEmpty(getMailAddress())) {
setMailboxOnService(exchangeService, getMailAddress());
}
if (StringUtils.isNotEmpty(getProxyHost())) {
setProxyOnService(exchangeService);
}
if (StringUtils.isEmpty(getUrl())) {
configureUrlAutodiscovery(exchangeService);
} else {
exchangeService.setUrl(getUriFromUrl(getUrl()));
}
log.debug("using url [{}]", exchangeService.getUrl());
return exchangeService;
}
private URI getUriFromUrl(String mailUrl) throws FileSystemException {
try {
return new URI(mailUrl);
} catch (URISyntaxException e) {
throw new FileSystemException("cannot set URL [" + mailUrl + "]", e);
}
}
private void configureUrlAutodiscovery(ExchangeService exchangeService) throws FileSystemException {
log.debug("performing autodiscovery for [{}]", this::getMailAddress);
RedirectionUrlCallback redirectionUrlCallback = new RedirectionUrlCallback() {
@Override
public boolean autodiscoverRedirectionUrlValidationCallback(String redirectionUrl) {
if (isValidateAllRedirectUrls()) {
log.debug("validated redirection url [{}]", redirectionUrl);
return true;
}
log.debug("did not validate redirection url [{}]", redirectionUrl);
return super.autodiscoverRedirectionUrlValidationCallback(redirectionUrl);
}
};
try {
exchangeService.autodiscoverUrl(getMailAddress(), redirectionUrlCallback);
//TODO call setUrl() here to avoid repeated autodiscovery
} catch (Exception e) {
throw new FileSystemException("cannot autodiscover for [" + getMailAddress() + "]", e);
}
}
private void setProxyOnService(ExchangeService exchangeService) {
CredentialFactory proxyCf = getProxyCredentials();
WebProxyCredentials webProxyCredentials = proxyCf != null ? new WebProxyCredentials(proxyCf.getUsername(), proxyCf.getPassword(), getProxyDomain()) : null;
WebProxy webProxy = new WebProxy(getProxyHost(), getProxyPort(), webProxyCredentials);
exchangeService.setWebProxy(webProxy);
}
private void setCredentialsOnService(ExchangeService exchangeService) throws FileSystemException {
if (client == null) {
throw new FileSystemException("No client available to authenticate with.");
}
CompletableFuture future = client.acquireToken(clientCredentialParam);
try {
String token = future.get().accessToken();
// use OAuth Bearer token authentication
exchangeService.getHttpHeaders().put("Authorization", "Bearer " + token);
} catch (InterruptedException | ExecutionException e) {
Thread.currentThread().interrupt();
throw new FileSystemException("Could not generate access token!", e);
}
}
public FolderId findFolder(ExchangeObjectReference reference) throws FileSystemException {
return findFolder(reference.getBaseFolderId(), reference.getObjectName());
}
/**
* find a folder for list(), getNumberOfFilesInFolder(), folderExists.
* If folderName is empty, the result defaults to the baseFolder.
* If baseFolder is null, the folder is searched in the root of the message folder hierarchy.
* If the folder is not found, null is returned.
*/
public FolderId findFolder(FolderId baseFolderId, String folderName) throws FileSystemException {
if (StringUtils.isEmpty(folderName)) {
return baseFolderId;
}
ExchangeObjectReference targetFolder = asObjectReference(folderName, baseFolderId);
ExchangeService exchangeService = getConnection(targetFolder);
boolean invalidateConnectionOnRelease = false;
try {
return findFolder(exchangeService, targetFolder);
} catch (FileSystemException e) {
invalidateConnectionOnRelease = true;
throw e;
} finally {
releaseConnection(exchangeService, invalidateConnectionOnRelease);
}
}
private FolderId findFolder(ExchangeService exchangeService, ExchangeObjectReference targetFolder) throws FileSystemException {
return findFolder(exchangeService, targetFolder, false);
}
private FolderId findFolder(ExchangeService exchangeService, ExchangeObjectReference targetFolder, boolean createFolder) throws FileSystemException {
FindFoldersResults findFoldersResultsIn;
FolderView folderViewIn = new FolderView(10);
SearchFilter searchFilterIn = new SearchFilter.IsEqualTo(FolderSchema.DisplayName, targetFolder.getObjectName());
try {
if (targetFolder.getBaseFolderId() != null) {
findFoldersResultsIn = exchangeService.findFolders(targetFolder.getBaseFolderId(), searchFilterIn, folderViewIn);
} else {
findFoldersResultsIn = exchangeService.findFolders(WellKnownFolderName.MsgFolderRoot, searchFilterIn, folderViewIn);
}
} catch (Exception e) {
throw new FolderNotFoundException("Cannot find folder [" + targetFolder.getObjectName() + "]", e);
}
if (findFoldersResultsIn.getTotalCount() == 0) {
log.debug("no folder found with name [{}] in basefolder [{}]", targetFolder::getObjectName, targetFolder::getBaseFolderId);
if (createFolder) {
log.debug("creating folder with name [{}] in basefolder [{}]", targetFolder::getObjectName, targetFolder::getBaseFolderId);
createFolder(targetFolder.getOriginalReference());
return findFolder(exchangeService, targetFolder, false);
}
return null;
}
if (findFoldersResultsIn.getTotalCount() > 1) {
if (log.isDebugEnabled()) {
for (Folder folder : findFoldersResultsIn.getFolders()) {
try {
log.debug("found folder [{}]", folder.getDisplayName());
} catch (ServiceLocalException e) {
log.warn("could not display foldername", e);
}
}
}
throw new FileSystemException("multiple folders found with name [" + targetFolder.getObjectName() + "]");
}
return findFoldersResultsIn.getFolders().get(0).getId();
}
@Override
public ExchangeMessageReference toFile(@Nullable String filename) throws FileSystemException {
log.debug("Get EmailMessage for reference [{}]", filename);
ExchangeObjectReference reference = asObjectReference(filename);
ExchangeService exchangeService = getConnection(reference);
boolean invalidateConnectionOnRelease = false;
try {
ItemId itemId = ItemId.getItemIdFromString(reference.getObjectName());
// TODO: check if this bind can be left out
return ExchangeMessageReference.of(reference, EmailMessage.bind(exchangeService, itemId));
} catch (Exception e) {
invalidateConnectionOnRelease = true;
throw new FileSystemException("Cannot convert filename [" + filename + "] into an ItemId", e);
} finally {
releaseConnection(exchangeService, invalidateConnectionOnRelease);
}
}
@Override
public ExchangeMessageReference toFile(@Nullable String folder, @Nullable String filename) throws FileSystemException {
return toFile(filename);
}
private boolean itemExistsInFolder(ExchangeService exchangeService, FolderId folderId, String itemId) throws Exception {
ItemView view = new ItemView(1);
view.getOrderBy().add(ItemSchema.DateTimeReceived, SortDirection.Ascending);
SearchFilter searchFilter = new SearchFilter.IsEqualTo(ItemSchema.Id, itemId);
FindItemsResults- findResults;
findResults = exchangeService.findItems(folderId, searchFilter, view);
return findResults.getTotalCount() != 0;
}
@Override
public boolean exists(ExchangeMessageReference f) throws FileSystemException {
log.trace("Check if message exists: message [{}]", f);
ExchangeService exchangeService = getConnection(f.getMailFolder());
boolean invalidateConnectionOnRelease = false;
try {
// TODO: check if this bind can be left out
EmailMessage emailMessage = EmailMessage.bind(exchangeService, f.getMessage().getId());
return itemExistsInFolder(exchangeService, emailMessage.getParentFolderId(), f.getMessage().getId().toString());
} catch (ServiceResponseException e) {
ServiceError errorCode = e.getErrorCode();
if (errorCode == ServiceError.ErrorItemNotFound) {
return false;
}
throw new FileSystemException(e);
} catch (Exception e) {
invalidateConnectionOnRelease = true;
throw new FileSystemException(e);
} finally {
releaseConnection(exchangeService, invalidateConnectionOnRelease);
}
}
@Override
public boolean isFolder(ExchangeMessageReference exchangeMessageReference) {
return false; // Currently only supports messages
}
@Override
public DirectoryStream
list(final String folder, TypeFilter filter) throws FileSystemException {
if (!isOpen()) {
return null;
}
if (filter.includeFolders()) {
throw new FileSystemException("Filtering on folders is not supported");
}
final ExchangeObjectReference reference = asObjectReference(folder);
final ExchangeService exchangeService = getConnection(reference);
boolean invalidateConnectionOnRelease = false;
boolean closeConnectionOnExit = true;
try {
FolderId folderId = findFolder(reference);
if (folderId == null) {
throw new FolderNotFoundException("Cannot find folder [" + folder + "]");
}
ItemView view = new ItemView(getMaxNumberOfMessagesToList() < 0 ? 100 : getMaxNumberOfMessagesToList());
view.getOrderBy().add(ItemSchema.DateTimeReceived, SortDirection.Ascending);
FindItemsResults- findResults;
if ("NDR".equalsIgnoreCase(getFilter())) {
SearchFilter searchFilterBounce = new SearchFilter.IsEqualTo(ItemSchema.ItemClass, "REPORT.IPM.Note.NDR");
findResults = exchangeService.findItems(folderId, searchFilterBounce, view);
} else {
findResults = exchangeService.findItems(folderId, view);
}
if (findResults.getTotalCount() == 0) {
return FileSystemUtils.getDirectoryStream((Iterator
) null);
}
Iterator- itemIterator = findResults.getItems().iterator();
DirectoryStream
result = FileSystemUtils.getDirectoryStream(new Iterator<>() {
@Override
public boolean hasNext() {
return itemIterator.hasNext();
}
@Override
public ExchangeMessageReference next() {
return ExchangeMessageReference.of(reference, (EmailMessage) itemIterator.next());
}
}, () -> releaseConnection(exchangeService, false));
closeConnectionOnExit = false;
return result;
} catch (Exception e) {
invalidateConnectionOnRelease = true;
throw new FileSystemException("Cannot list messages in folder [" + folder + "]", e);
} finally {
if (closeConnectionOnExit) {
releaseConnection(exchangeService, invalidateConnectionOnRelease);
}
}
}
@Override
public int getNumberOfFilesInFolder(String foldername) throws FileSystemException {
if (!isOpen()) {
return -1;
}
ExchangeObjectReference reference = asObjectReference(foldername);
ExchangeService exchangeService = getConnection(reference);
FolderId folderId = findFolder(exchangeService, reference);
if (folderId == null) {
throw new FolderNotFoundException("Cannot find folder [" + foldername + "]");
}
boolean invalidateConnectionOnRelease = false;
try {
Folder folder = Folder.bind(exchangeService, folderId);
return folder.getTotalCount();
} catch (Exception e) {
invalidateConnectionOnRelease = true;
throw new FileSystemException("Cannot list messages in folder [" + foldername + "]", e);
} finally {
releaseConnection(exchangeService, invalidateConnectionOnRelease);
}
}
@Override
public boolean folderExists(String folder) throws FileSystemException {
FolderId folderId = findFolder(asObjectReference(folder));
return folderId != null;
}
@Override
public Message readFile(ExchangeMessageReference f, String charset) throws FileSystemException {
EmailMessage emailMessage = f.getMessage();
try {
if (emailMessage.getId() != null) {
PropertySet ps = new PropertySet(
ItemSchema.DateTimeReceived,
EmailMessageSchema.From,
ItemSchema.Subject,
ItemSchema.DateTimeSent,
ItemSchema.LastModifiedTime,
ItemSchema.Size
);
if (isReadMimeContents()) {
ps.add(ItemSchema.MimeContent);
} else {
ps.add(ItemSchema.Body);
}
emailMessage.load(ps);
}
if (isReadMimeContents()) {
MimeContent mc = emailMessage.getMimeContent();
return new Message(mc.getContent(), charset);
}
return new Message(MessageBody.getStringFromMessageBody(emailMessage.getBody()), FileSystemUtils.getContext(this, f));
} catch (FileSystemException e) {
throw e;
} catch (Exception e) {
throw new FileSystemException(e);
}
}
@Override
public void deleteFile(ExchangeMessageReference f) throws FileSystemException {
try {
f.getMessage().delete(DeleteMode.MoveToDeletedItems);
} catch (Exception e) {
throw new FileSystemException("Could not delete Exchange Message [" + getCanonicalNameOrErrorMessage(f) + "]: " + e.getMessage());
}
}
@Override
public ExchangeMessageReference moveFile(ExchangeMessageReference f, String destinationFolder, boolean createFolder) throws FileSystemException {
ExchangeObjectReference reference = asObjectReference(destinationFolder);
ExchangeService exchangeService = getConnection(reference);
boolean invalidateConnectionOnRelease = false;
try {
FolderId destinationFolderId = findFolder(exchangeService, reference, createFolder);
return ExchangeMessageReference.of(reference, (EmailMessage) f.getMessage().move(destinationFolderId));
} catch (Exception e) {
invalidateConnectionOnRelease = true;
throw new FileSystemException(e);
} finally {
releaseConnection(exchangeService, invalidateConnectionOnRelease);
}
}
@Override
public ExchangeMessageReference copyFile(ExchangeMessageReference f, String destinationFolder, boolean createFolder) throws FileSystemException {
ExchangeObjectReference reference = asObjectReference(destinationFolder);
ExchangeService exchangeService = getConnection(reference);
boolean invalidateConnectionOnRelease = false;
try {
FolderId destinationFolderId = findFolder(exchangeService, reference, createFolder);
return ExchangeMessageReference.of(reference, (EmailMessage) f.getMessage().copy(destinationFolderId));
} catch (Exception e) {
invalidateConnectionOnRelease = true;
throw new FileSystemException(e);
} finally {
releaseConnection(exchangeService, invalidateConnectionOnRelease);
}
}
@Override
public long getFileSize(ExchangeMessageReference f) throws FileSystemException {
try {
return f.getMessage().getSize();
} catch (ServiceLocalException e) {
throw new FileSystemException("Could not determine size", e);
}
}
@Override
public String getName(ExchangeMessageReference f) {
try {
final ItemId itemId = f.getMessage().getId();
if (itemId == null) {
return null; // attachments don't have an id, but appear to be loaded at the same time as the main message
}
return itemId.toString();
} catch (ServiceLocalException e) {
throw new RuntimeException("Could not determine Name", e);
}
}
@Override
public String getParentFolder(ExchangeMessageReference f) throws FileSystemException {
ExchangeService exchangeService = getConnection(f.getMailFolder());
boolean invalidateConnectionOnRelease = false;
try {
FolderId folderId = f.getMessage().getParentFolderId();
Folder folder = Folder.bind(exchangeService, folderId);
return folder.getDisplayName();
} catch (Exception e) {
invalidateConnectionOnRelease = true;
throw new FileSystemException(e);
} finally {
releaseConnection(exchangeService, invalidateConnectionOnRelease);
}
}
@Override
public String getCanonicalName(ExchangeMessageReference f) throws FileSystemException {
try {
final ItemId itemId = f.getMessage().getId();
if (itemId == null) {
return null; // attachments don't have an id, but appear to be loaded at the same time as the main message
}
return itemId.getUniqueId();
} catch (ServiceLocalException e) {
throw new FileSystemException("Could not determine CanonicalName", e);
}
}
@Override
public Date getModificationTime(ExchangeMessageReference f) throws FileSystemException {
try {
return f.getMessage().getLastModifiedTime();
} catch (ServiceLocalException e) {
throw new FileSystemException("Could not determine modification time", e);
}
}
private String cleanAddress(EmailAddress address) {
String personal = address.getName();
String email = address.getAddress();
InternetAddress iaddress;
try {
iaddress = new InternetAddress(email, personal);
} catch (UnsupportedEncodingException e) {
return address.toString();
}
return iaddress.toUnicodeString();
}
private List asList(EmailAddressCollection addressCollection) {
if (addressCollection == null) {
return Collections.emptyList();
}
return addressCollection
.getItems()
.stream()
.map(this::cleanAddress)
.collect(Collectors.toList());
}
@Override
@Nullable
public Map getAdditionalFileProperties(ExchangeMessageReference f) throws FileSystemException {
final EmailMessage emailMessage = f.getMessage();
try {
final ItemId emailMessageId = emailMessage.getId();
if (emailMessageId != null) {
emailMessage.load(PropertySet.FirstClassProperties);
}
final Map result = new LinkedHashMap<>();
result.put(TO_RECIPIENTS_KEY, asList(emailMessage.getToRecipients()));
result.put(CC_RECIPIENTS_KEY, asList(emailMessage.getCcRecipients()));
result.put(BCC_RECIPIENTS_KEY, asList(emailMessage.getBccRecipients()));
result.put(FROM_ADDRESS_KEY, getFrom(emailMessage));
result.put(SENDER_ADDRESS_KEY, getSender(emailMessage));
result.put(REPLY_TO_RECIPIENTS_KEY, getReplyTo(emailMessage));
result.put(DATETIME_SENT_KEY, getDateTimeSent(emailMessage));
result.put(DATETIME_RECEIVED_KEY, getDateTimeReceived(emailMessage));
result.putAll(extractInternetMessageHeaders(emailMessage, emailMessageId));
result.put(BEST_REPLY_ADDRESS_KEY, MailFileSystemUtils.findBestReplyAddress(result, getReplyAddressFields()));
return result;
} catch (Exception e) {
throw new FileSystemException(e);
}
}
private Map extractInternetMessageHeaders(EmailMessage emailMessage, ItemId emailMessageId) {
final InternetMessageHeaderCollection internetMessageHeaders;
try {
internetMessageHeaders = emailMessage.getInternetMessageHeaders();
} catch (ServiceLocalException e) {
log.warn("Message [{}] Cannot load message headers: {}", emailMessageId, e.getMessage());
return Collections.emptyMap();
}
if (internetMessageHeaders == null) {
return Collections.emptyMap();
}
final Map result = new LinkedHashMap<>();
for (InternetMessageHeader internetMessageHeader : internetMessageHeaders) {
if (!result.containsKey(internetMessageHeader.getName())) {
result.put(internetMessageHeader.getName(), internetMessageHeader.getValue());
continue;
}
// Multiple occurrences found of header, collate into a list
final Object curEntry = result.get(internetMessageHeader.getName());
final List