package com.wilutions.itol;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URI;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.security.MessageDigest;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Properties;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.stream.Collectors;
import javax.xml.bind.DatatypeConverter;
import com.wilutions.com.BackgTask;
import com.wilutions.com.Dispatch;
import com.wilutions.itol.db.Attachment;
import com.wilutions.itol.db.AttachmentBlacklistItem;
import com.wilutions.itol.db.Config;
import com.wilutions.itol.db.Default;
import com.wilutions.itol.db.IdName;
import com.wilutions.itol.db.Issue;
import com.wilutions.itol.db.MsgFileFormat;
import com.wilutions.itol.db.ProgressCallback;
import com.wilutions.itol.db.Property;
import com.wilutions.mslib.outlook.Application;
import com.wilutions.mslib.outlook.MailItem;
import com.wilutions.mslib.outlook.OlAttachmentType;
import com.wilutions.mslib.outlook.OlBodyFormat;
import com.wilutions.mslib.outlook.OlSaveAsType;
import com.wilutions.mslib.outlook.PropertyAccessor;
import javafx.scene.image.Image;
public class MailAttachmentHelper {
private List<Runnable> resourcesToRelease = new ArrayList<Runnable>();
private File __tempDir;
private final static Logger log = Logger.getLogger("MailAttachmentHelper");
public final static String FILE_URL_PREFIX = "file:/";
public MailAttachmentHelper() {
}
/**
* Convert email attachments to issue attachments.
* @param mailItem
* @param issue
* @throws Exception
*/
public void initialUpdate(IssueMailItem mailItem, Issue issue) throws Exception {
if (log.isLoggable(Level.FINE)) log.fine("initialUpdate(mailItem=" + mailItem + ", issue=" + issue);
// Observed that Outlook/ITOL hangs while updating attachments.
// The JavaFX thread hung in an OLE call somewhere at mailItem.get...
// To avoid a deadlock, execute the function in background and wait up to 30s.
long t1 = System.currentTimeMillis();
Executor executor = BackgTask.getExecutor();
CompletableFuture<Void> future = CompletableFuture.supplyAsync(() -> {
try {
initialUpdateBackg(mailItem, issue);
} catch (Exception e) {
throw new RuntimeException(e);
}
return null;
}, executor);
future.get(30, TimeUnit.SECONDS);
long t2 = System.currentTimeMillis();
log.info("[" + (t2-t1) + "] MailAttachmentHelper.initialUpdate");
if (log.isLoggable(Level.FINE)) log.fine(")initialUpdate");
}
private void initialUpdateBackg(IssueMailItem mailItem, Issue issue) throws Exception {
if (log.isLoggable(Level.FINE)) log.fine("initialUpdate(mailItem=" + mailItem + ", issue=" + issue);
releaseResources();
if (issue != null) {
boolean isNew = issue.getId().isEmpty();
String newNotes = issue.getPropertyString(Property.NOTES, "");
if (isNew || newNotes.length() != 0) {
initialUpdateNewIssueAttachments(mailItem, issue);
}
}
if (log.isLoggable(Level.FINE)) log.fine(")initialUpdate");
}
private File getTempDir() {
if (__tempDir == null) {
__tempDir = new File(Globals.getTempDir(), Long.toString(System.currentTimeMillis()));
__tempDir.mkdirs();
}
return __tempDir;
}
private void initialUpdateNewIssueAttachments(IssueMailItem mailItem, Issue issue) throws Exception {
if (log.isLoggable(Level.FINE)) log.fine("initialUpdateNewIssueAttachments(");
String ext = getConfigMsgFileExt();
if (log.isLoggable(Level.FINE)) log.fine("ext=" + ext);
if (!ext.equals(MsgFileFormat.NOTHING.getId())) {
List<Attachment> attachments = new ArrayList<Attachment>(issue.getAttachments());
// The code below should add mail attachments as issue attachments.
boolean addAttachments = false;
// The code below should add the mail as an issue attachment.
boolean addMail = false;
// Add attachments embedded in the mail body as issue attachments.
boolean addEmbeddedAttachments = false;
// Shortcut for option "Only Attachments"
boolean addOnlyAttachments = ext.equals(MsgFileFormat.ONLY_ATTACHMENTS.getId());
if (log.isLoggable(Level.FINE)) log.fine("addOnlyAttachments=" + addOnlyAttachments);
if (addOnlyAttachments) {
addAttachments = true;
addEmbeddedAttachments = true;
addMail = false;
}
else {
// If the mail should be added as MSG (isContainerFormat), it already includes all mail attachments.
// Thus, the attachments must not be added to save space on the server.
// On the other hand, if the mail body should be converted to JIRA markup, embedded attachments
// must be added to the issue explicitly. Otherwise, thumbnails of embedded images are not available.
OlSaveAsType saveAsType = MsgFileTypes.getMsgFileType(ext);
if (log.isLoggable(Level.FINE)) log.fine("saveAsType=" + saveAsType);
addAttachments = !MsgFileTypes.isContainerFormat(saveAsType);
addEmbeddedAttachments = true;
addMail = true;
}
// The mail body is empty, if this function is called from menu items "New issue" or "New Subtask".
if (log.isLoggable(Level.FINE)) log.fine("#mail.body=" + mailItem.getBody().length());
if (mailItem.getBody().isEmpty()) {
addMail = false;
}
// Maybe add mail as an issue attachment.
if (log.isLoggable(Level.FINE)) log.fine("addMail=" + addMail);
if (addMail) {
MailAtt mailAtt = new MailAtt(mailItem, ext);
attachments.add(mailAtt);
}
// Maybe add mail attachments as issue attachments
if (log.isLoggable(Level.FINE)) log.fine("addAttachments=" + addAttachments);
if (addAttachments || addEmbeddedAttachments) {
// Embedded RTF attachments have a special format - not PNG, JPG, BMP.
// To add this attachments to the issue, the code adds the entire mail as RTF file, if it has not been added above.
boolean addBodyToIncludeEmbeddedAttachments = !addMail;
boolean isRTFBody = mailItem.getBodyFormat() == OlBodyFormat.olFormatRichText;
if (log.isLoggable(Level.FINE)) log.fine("addBodyToIncludeEmbeddedAttachments=" + addBodyToIncludeEmbeddedAttachments + ", isRTFBody=" + isRTFBody);
IssueAttachments mailAtts = mailItem.getAttachments();
int n = mailAtts.getCount();
if (log.isLoggable(Level.FINE)) log.fine("add #attachments=" + n);
for (int i = 1; i <= n; i++) {
com.wilutions.mslib.outlook.Attachment matt = mailAtts.getItem(i);
MailAttAtt attatt = new MailAttAtt(matt);
if (log.isLoggable(Level.FINE)) log.fine("attachment[" + i + "]=" + attatt);
// Skip attachments from blacklist.
boolean isBlacklistAttachment = isBlacklistAttachment(attatt);
if (log.isLoggable(Level.FINE)) log.fine("isBlacklistAttachment=" + isBlacklistAttachment);
if (isBlacklistAttachment) continue;
OlAttachmentType attachmentType = matt.getType();
// Get advanced attachment properties to find out, whether the attachment is
// embedded in the mail body.
// https://social.msdn.microsoft.com/Forums/vstudio/en-US/d6d339d2-ebc3-4332-9801-15a53020df94/embedded-images-attachments-with-html-based-emails?forum=vsto
PropertyAccessor mattProps = matt.getPropertyAccessor();
Object contentId = mattProps.GetProperty("http://schemas.microsoft.com/mapi/proptag/0x3712001E");
Object mimeType = matt.getPropertyAccessor().GetProperty("http://schemas.microsoft.com/mapi/proptag/0x370E001E");
if (log.isLoggable(Level.FINE)) log.fine("contentId=" + contentId + ", mimeType=" + mimeType);
boolean isEmbeddedAttachment = contentId != null && !contentId.equals("");
if (log.isLoggable(Level.FINE)) log.fine("isEmbeddedAttachment=" + isEmbeddedAttachment);
if (log.isLoggable(Level.FINE)) log.fine("attachmentType=" + attachmentType);
if (isRTFBody && attachmentType == OlAttachmentType.olOLE) {
if (addBodyToIncludeEmbeddedAttachments) {
if (log.isLoggable(Level.FINE)) log.fine("add mail as attachment to include embedded attachments");
addBodyToIncludeEmbeddedAttachments = false;
MailAtt mailAtt = new MailAtt(mailItem, MsgFileFormat.RTF.getId());
attachments.add(mailAtt);
}
}
else if (addAttachments || isEmbeddedAttachment) {
if (log.isLoggable(Level.FINE)) log.fine("add attachment");
attatt.setLastModified(mailItem.getReceivedTime());
attachments.add(attatt);
}
}
}
issue.setAttachments(attachments);
}
if (log.isLoggable(Level.FINE)) log.fine(")initialUpdateNewIssueAttachments");
}
public Attachment makeMailAttachment(IssueMailItem mailItem) throws IOException {
String ext = getConfigMsgFileExt();
MailAtt mailAtt = new MailAtt(mailItem, ext);
return mailAtt;
}
public void releaseResources() {
for (Runnable run : resourcesToRelease) {
try {
run.run();
}
catch (Throwable ignored) {
}
}
if (__tempDir != null) {
__tempDir.delete();
__tempDir = null;
}
}
public static Attachment createFromFile(File file) {
return new FileAtt(file);
}
public static String getFileName(String path) {
String fname = path;
if (path != null && path.length() != 0) {
int p = path.lastIndexOf(File.separatorChar);
fname = path.substring(p + 1);
}
return fname;
}
public static String getFileNameWithoutExt(String path) {
String fname = path;
if (path != null && path.length() != 0) {
int p = path.lastIndexOf(File.separatorChar);
fname = path.substring(p + 1);
p = fname.lastIndexOf('.');
if (p >= 0) {
fname = fname.substring(0, p);
}
}
return fname;
}
public static String getFileExt(String path) {
String ext = "";
if (path != null && path.length() != 0) {
int p = path.lastIndexOf('.');
if (p >= 0) {
ext = path.substring(p + 1).toLowerCase();
}
}
return ext;
}
public static String getFileContentType(File file) {
String contentType = "";
try {
contentType = Files.probeContentType(file.toPath());
} catch (IOException ignore) {}
if (contentType == null || contentType.isEmpty()) {
String fname = file.getName();
String ext = ".";
int p = fname.lastIndexOf('.');
if (p >= 0) {
ext = fname.substring(p);
}
contentType = ContentTypes.getContentType(ext.toLowerCase());
}
return contentType;
}
public static String makeAttachmentSizeString(long contentLength) {
String ret = "";
if (contentLength >= 0) {
final String[] dims = new String[] { "Bytes", "KB", "MB", "GB", "TB" };
int dimIdx = 0;
long c = contentLength, nb = 0;
for (int i = 0; i < dims.length; i++) {
nb = c;
c = (long) Math.floor(c / 1000);
if (c == 0) {
dimIdx = i;
break;
}
}
ret = nb + " " + dims[dimIdx];
}
return ret;
}
/**
* Issue attachment for mail attachment.
*/
public class MailAttAtt extends Attachment {
private final com.wilutions.mslib.outlook.Attachment matt;
private final File dir = getTempDir();
private MailAttAtt(com.wilutions.mslib.outlook.Attachment matt) {
this.matt = matt;
String fname = "";
try {
fname = matt.getFileName();
}
catch (Exception e) {
// Attachments embedded in a RTF mail body throw an exception here.
fname = String.valueOf(System.identityHashCode(matt));
}
super.setSubject(fname);
super.setFileName(fname);
super.setContentType(getFileContentType(new File(getTempDir(), fname)));
super.setContentLength(matt.getSize());
super.setLastModified(new Date());
}
@Override
public InputStream getStream() {
File f = save();
try {
return new FileInputStream(f);
}
catch (FileNotFoundException e) {
return null;
}
}
@Override
public String getUrl() {
save();
return super.getUrl();
}
private File save() {
if (getLocalFile() == null) {
final File mattFile = new File(dir, getFileName());
super.setLocalFile(mattFile);
resourcesToRelease.add(() -> mattFile.delete());
log.info("Save attachment to " + mattFile);
matt.SaveAsFile(mattFile.getAbsolutePath());
super.setContentLength(mattFile.length());
super.setUrl(mattFile.toURI().toString());
}
return getLocalFile();
}
public String getThumbnailUrl() {
String thurl = super.getThumbnailUrl();
if (Default.isEmpty(thurl)) {
if (!ThumbnailHelper.getImageFileType(new File(getFileName())).isEmpty()) {
save();
File thumbnailFile = ThumbnailHelper.makeThumbnail(getLocalFile());
thurl = thumbnailFile != null ? thumbnailFile.toURI().toString() : "";
setThumbnailUrl(thurl);
}
}
return thurl;
}
}
/**
* Issue attachment for mail. Must be public, otherwise it cannot be viewed
* in the table.
*/
public class MailAtt extends Attachment {
private final IssueMailItem mailItem;
private final String ext;
private final File dir = getTempDir();
private MailAtt(IssueMailItem mailItem, String ext) {
this.mailItem = mailItem;
this.ext = ext;
setSubject(mailItem.getSubject());
setContentLength(-1);
setLastModified(mailItem.getReceivedTime());
}
@Override
public void setSubject(String subject) {
OlSaveAsType saveAsType = MsgFileTypes.getMsgFileType(ext);
String msgFileName = MsgFileTypes.makeMsgFileName(subject, saveAsType);
super.setSubject(subject);
super.setContentType(getFileContentType(new File(dir, msgFileName)));
super.setContentLength(-1);
super.setFileName(msgFileName);
}
@Override
public InputStream getStream() {
File f = save();
try {
return new FileInputStream(f);
}
catch (FileNotFoundException e) {
return null;
}
}
@Override
public String getUrl() {
save();
return super.getUrl();
}
@Override
public long getContentLength() {
return super.getContentLength();
}
private File save() {
final File msgFile = new File(dir, getFileName());
try {
if (getContentLength() < 0) {
OlSaveAsType saveAsType = MsgFileTypes.getMsgFileType(ext);
resourcesToRelease.add(() -> msgFile.delete());
long t1 = System.currentTimeMillis();
mailItem.SaveAs(msgFile.getAbsolutePath(), saveAsType);
long t2 = System.currentTimeMillis();
log.info("[" + (t2-t1) + "] Save mail to " + msgFile);
super.setContentLength(msgFile.length());
super.setUrl(msgFile.toURI().toString());
super.setLocalFile(msgFile);
}
}
catch (Exception e) {
log.log(Level.SEVERE, "Failed to save attachment=" + this + " to local file.", e);
}
return msgFile;
}
}
/**
* Issue attachment for file from filesystem.
*/
public static class FileAtt extends Attachment {
private FileAtt(File file) {
super.setSubject(file.getName());
super.setFileName(file.getName());
super.setUrl(file.toURI().toString());
super.setLocalFile(file);
super.setContentLength(file.length());
super.setContentType(getFileContentType(file));
File thumbnailFile = ThumbnailHelper.makeThumbnail(file);
String thurl = thumbnailFile != null ? thumbnailFile.toURI().toString() : "";
super.setThumbnailUrl(thurl);
super.setLastModified(new Date(file.lastModified()));
}
@Override
public InputStream getStream() {
InputStream ret = null;
try {
ret = new FileInputStream(getLocalFile());
}
catch (FileNotFoundException e) {
log.log(Level.SEVERE, "Failed to get stream for attachment=" + this, e);
}
return ret;
}
}
private static String getConfigMsgFileExt() {
IdName type = Globals.getAppInfo().getConfig().getMsgFileFormat();
return type.getId();
}
public void showAttachment(Attachment att, ProgressCallback cb) throws Exception {
// Download the entire file into a temp dir. Opening the URL with Desktop.browse()
// would start a browser first, which in turn downloads the file.
URI url = downloadAttachment(att, cb);
IssueApplication.showDocument(url.toString());
}
public URI downloadAttachment(Attachment att, ProgressCallback cb) throws Exception {
if (log.isLoggable(Level.FINE)) log.fine("downloadAttachment(att=" + att);
String url = att.getUrl();
// Local file added from file system (new file, not uploaded)
if (url.startsWith(FILE_URL_PREFIX)) {
//;
}
else {
// Already downloaded attachment?
if (att.getLocalFile() != null && att.getLocalFile().exists()) {
if (log.isLoggable(Level.FINE)) log.fine("already downloaded file=" + att.getLocalFile());
url = att.getLocalFile().toURI().toString();
}
else {
if (log.isLoggable(Level.FINE)) log.fine("download from url=" + url);
String fileName = Globals.getIssueService().downloadAttachment(url, cb);
File srcFile = new File(fileName);
if (log.isLoggable(Level.FINE)) log.fine("received file=" + srcFile);
if (srcFile.exists()) {
for (int retries = 0; retries < 100; retries++) {
File destFile = makeTempFile(getTempDir(), att.getFileName(), retries);
destFile.delete();
boolean succ = srcFile.renameTo(destFile);
if (log.isLoggable(Level.FINE)) log.fine("move to=" + destFile + ", succ=" + succ);
if (succ) {
att.setLocalFile(destFile);
fileName = destFile.getAbsolutePath();
url = destFile.toURI().toString();
break;
}
}
}
}
}
if (cb != null) cb.setFinished();
if (log.isLoggable(Level.FINE)) log.fine(")downloadAttachment=" + url);
return new URI(url);
}
private boolean compareFiles(File lhs, File rhs, ProgressCallback cb) {
return compareFilesContents(lhs, rhs, cb);
}
/**
* Compare up to 8000 bytes at the beginning and end of the given files.
* @param lhs File 1
* @param rhs File 2
* @param cb ProgressCallback
* @return true, if the first and last 8000 bytes are equal.
*/
private boolean compareFilesContents(File lhs, File rhs, ProgressCallback cb) {
cb.setTotal(1.0);
boolean ret = lhs.exists() && rhs.exists() && lhs.length() == rhs.length();
if (ret) {
final int maxBytes = 8000; // less than default buffer size of BufferedInputStream
ByteBuffer bbufL = ByteBuffer.allocate(maxBytes);
ByteBuffer bbufR = ByteBuffer.allocate(maxBytes);
try (
FileChannel fcL = FileChannel.open(lhs.toPath(), StandardOpenOption.READ);
FileChannel fcR = FileChannel.open(rhs.toPath(), StandardOpenOption.READ)
) {
// Read up to 8000 bytes from the beginning
int len = 0;
do { len = fcL.read(bbufL); }
while (len >= 0 && bbufL.hasRemaining());
do { len = fcR.read(bbufR); }
while (len >= 0 && bbufR.hasRemaining());
cb.incrProgress(0.5);
// Compare
bbufL.flip(); bbufR.flip();
ret = bbufL.compareTo(bbufR) == 0;
if (ret && fcL.size() > maxBytes) {
// Read up to last 8000 bytes.
// Move position to the last 8000 bytes
long pos = Math.max(maxBytes, fcL.size() - maxBytes);
fcL.position(pos);
fcR.position(pos);
bbufL.clear();
bbufR.clear();
// Read last bytes
do { len = fcL.read(bbufL); }
while (len >= 0 && bbufL.hasRemaining());
do { len = fcR.read(bbufR); }
while (len >= 0 && bbufR.hasRemaining());
cb.incrProgress(0.5);
// Compare
bbufL.flip(); bbufR.flip();
ret = bbufL.compareTo(bbufR) == 0;
}
}
catch (Exception e) {
log.log(Level.WARNING, "Failed to compare file content, file1=" + lhs + ", file2=" + rhs, e);
// Assume files are equal. This avoids copying rhs on lhs in exportAttachment.
}
}
cb.setFinished();
return ret;
}
// private byte[] getFileHash(File file, ProgressCallback cb) {
// byte[] digest = null;
// cb.setTotal(file.length());
// try {
// MessageDigest md = MessageDigest.getInstance("MD5");
// try (InputStream is = Files.newInputStream(file.toPath());
// DigestInputStream dis = new DigestInputStream(is, md))
// {
// byte[] buf = new byte[10*1000];
// int len = 0;
// while ((len = dis.read(buf)) > 0) {
// cb.incrProgress(len);
// }
// }
// digest = md.digest();
// }
// catch (Exception e) {
// digest = new byte[16];
// }
// cb.setFinished();
// return digest;
// }
private File exportAttachment(File dir, Application outlookApplication, Attachment att, ProgressCallback cb) throws Exception {
if (log.isLoggable(Level.FINE)) log.fine("exportAttachment(dir=" + dir + ", att=" + att);
File ret = null;
try {
// Download into temp dir.
ProgressCallback cbDownload = cb.createChild("Download " + att.getFileName(), 0.5);
URI url = downloadAttachment(att, cbDownload);
File tempFile = new File(url);
// Is the issue attachment a mail?
ProgressCallback cbSave = cb.createChild("Save " + att.getFileName(), 0.5);
boolean attachmentIsMail = tempFile.getName().toLowerCase().endsWith(MsgFileFormat.MSG.getId());
if (log.isLoggable(Level.FINE)) log.fine("attachmentIsMail=" + attachmentIsMail);
if (attachmentIsMail) {
try {
// Load mail into Outlook.MailItem object
IssueMailItem mailItem = loadMailItem(outlookApplication, tempFile);
IssueAttachments mailAtts = mailItem.getAttachments();
int nbOfAttachments = mailAtts.getCount();
cbSave.setTotal(nbOfAttachments + 1);
// Export mail as RTF
{
MailAtt matt = new MailAtt(mailItem, MsgFileFormat.RTF.getId());
File destFile = makeUniqueExportFileName(dir, new File(dir, matt.getFileName()), cbSave.createChild(0.5));
if (log.isLoggable(Level.INFO)) log.info("Export mail to destFile=" + destFile);
if (!destFile.exists()) {
mailItem.SaveAs(destFile.getAbsolutePath(), OlSaveAsType.olRTF);
destFile.setLastModified(mailItem.getReceivedTime().getTime());
}
cbSave.incrProgress(0.5);
ret = destFile;
}
// Export mail attachments
for (int i = 1; i <= nbOfAttachments; i++) {
com.wilutions.mslib.outlook.Attachment matt = mailAtts.getItem(i);
File destFile = makeUniqueExportFileName(dir, new File(dir, matt.getFileName()), cbSave.createChild(0.5));
if (log.isLoggable(Level.INFO)) log.info("Export mail attachment to destFile=" + destFile);
if (!destFile.exists()) {
matt.SaveAsFile(destFile.getAbsolutePath());
destFile.setLastModified(mailItem.getReceivedTime().getTime());
}
cbSave.incrProgress(0.5);
}
}
catch (Exception e) {
log.log(Level.WARNING, "Failed to export mail " + tempFile, e);
}
}
else {
// Make unique file name
File destFile = makeUniqueExportFileName(dir, tempFile, cbSave.createChild(0.5));
if (log.isLoggable(Level.INFO)) log.info("Export issue attachment to destFile=" + destFile);
// Copy to dest dir.
if (!destFile.exists()) {
Files.copy(tempFile.toPath(), destFile.toPath());
destFile.setLastModified(att.getLastModified().getTime());
}
cbSave.incrProgress(0.5);
ret = destFile;
}
}
finally {
cb.setFinished();
}
if (log.isLoggable(Level.FINE)) log.fine(")exportAttachment=" + ret);
return ret;
}
/**
* Export attachments to export directory.
* @param issue Issue
* @param selectedItems Attachments to be exported
* @param cb ProgressCallback
*/
public void exportAttachments(Issue issue, List<Attachment> selectedItems, ProgressCallback cb) throws Exception {
if (log.isLoggable(Level.FINE)) log.fine("exportAttachments(" + issue + ", #selectedItems=" + selectedItems.size() );
// Get destination directory
File exportDirectory = null;
{
String exportDirectoryName = Globals.getAppInfo().getConfig().getExportAttachmentsDirectory();
// Build sub-directory: issue ID or NEW-<now>
String subdir = issue.getId();
if (subdir.isEmpty()) {
DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss");
subdir = issue.getProject().getId() + "-NEW-" + dateFormat.format(new Date());
}
exportDirectory = new File(new File(exportDirectoryName), subdir);
if (log.isLoggable(Level.FINE)) log.fine("export directory=" + exportDirectory + ", exists=" + exportDirectory.exists());
if (exportDirectory.exists()) {
if (!exportDirectory.isDirectory()) {
throw new IllegalStateException("Export destination=" + exportDirectory + " is not a directory.");
}
}
else {
if (!exportDirectory.mkdirs()) {
throw new IllegalStateException("Export destination=" + exportDirectory + " cannot be created.");
}
}
}
// Properties file of exported attachments.
ExportedAttachmentsPropertiesFile exportedAttachments = new ExportedAttachmentsPropertiesFile(exportDirectory);
// Prepare progress object: compute total number of bytes to export.
long totalBytes = selectedItems.stream().collect(Collectors.summingLong((att) -> att.getContentLength())).longValue();
if (log.isLoggable(Level.INFO)) log.info("Export issue=" + issue.getId() + " " + selectedItems.size() + " attachments of totalBytes=" + totalBytes + " to directory=" + exportDirectory);
// Show that export process has started
cb.incrProgress(0.1);
// Export
ProgressCallback cbExportAll = cb.createChild(0.9);
Application outlookApplication = Globals.getThisAddin().getApplication();
for (Attachment att : selectedItems) {
if (cb.isCancelled()) break;
// Move progress by this fraction.
double progressRatio = (double)att.getContentLength() / (double)totalBytes;
// Attachment already exported? lookup file name in .contents file.
File alreadyExportedFile = exportedAttachments.get(att);
if (log.isLoggable(Level.FINE)) log.fine("att=" + att + " already exported to=" + alreadyExportedFile);
if (exportedAttachments.get(att) == null) {
// Export attachment
try {
ProgressCallback childProgress = cbExportAll.createChild("Export " + att.getFileName(), progressRatio);
File exportedFile = exportAttachment(exportDirectory, outlookApplication, att, childProgress);
exportedAttachments.add(att, exportedFile);
} catch (Exception e) {
log.log(Level.WARNING, "Attachment could not be exported.", e);
}
}
else {
cbExportAll.incrProgress(progressRatio);
}
}
cb.setFinished();
// Open export directory in Windows Explorer
if (!cb.isCancelled()) {
openExportDirectory(issue, exportDirectory);
}
if (log.isLoggable(Level.FINE)) log.fine(")exportAttachments");
}
/**
* Open export directory by configured program.
* @param issue
* @param exportDirectory
* @throws Exception
*/
private void openExportDirectory(Issue issue, File exportDirectory) throws Exception {
if (log.isLoggable(Level.FINE)) log.fine("openExportDirectory(issue=" + issue + ", exportDirectory=" + exportDirectory);
String exportProgram = Globals.getAppInfo().getConfig().getExportAttachmentsProgram();
if (Default.value(exportProgram).isEmpty()) {
String url = exportDirectory.toURI().toString();
IssueApplication.showDocument(url);
}
else {
String cmd = exportProgram
.replace(Config.PLACEHODER_EXPORT_DIRECTORY, exportDirectory.getAbsolutePath())
.replace(Config.PLACEHODER_ISSUE_ID, issue.getId())
.replace(Config.PLACEHODER_ISSUE_ID, issue.getProject().getId());
try {
log.info("Open export directory: " + cmd);
Runtime.getRuntime().exec(cmd);
} catch (Exception e) {
log.log(Level.WARNING, "Failed to open export directory.", e);
throw e;
}
}
if (log.isLoggable(Level.FINE)) log.fine(")openExportDirectory");
}
/**
* This class handles a properties file with exported attachments.
*/
private static class ExportedAttachmentsPropertiesFile {
final static String FILENAME = ".exported-attachments";
Properties props = new Properties();
File exportDirectory;
ExportedAttachmentsPropertiesFile(File exportDir) {
this.exportDirectory = exportDir;
load();
}
private synchronized void load() {
try (InputStream fis = new FileInputStream(new File(exportDirectory, FILENAME))) {
props.load(fis);
}
catch (Exception ignored) {}
}
private synchronized void store() {
try (OutputStream fos = new FileOutputStream(new File(exportDirectory, FILENAME))) {
props.store(fos, "Exported Issue Attachments");
}
catch (Exception ignored) {}
}
private String getAttachmentId(Attachment att) {
String id = att.getId();
if (id.isEmpty()) {
id = att.getFileName(); // new attachment
}
return id;
}
synchronized void add(Attachment att, File exportFile) {
if (!exportFile.getParentFile().equals(exportDirectory)) throw new IllegalArgumentException("Export file=" + exportFile + " must be stored in export directory=" + exportDirectory);
String id = getAttachmentId(att);
props.setProperty(id, exportFile.getName());
store();
}
synchronized File get(Attachment att) {
File ret = null;
String id = getAttachmentId(att);
String fname = props.getProperty(id);
if (!Default.value(fname).isEmpty()) {
ret = new File(exportDirectory, fname);
}
return ret;
}
}
/**
* Load file into Outlook MailItem object.
* @param outlookApplication Outlook application object.
* @param tempFile MSG file
* @return MailItem object
* @throws Exception
*/
private IssueMailItem loadMailItem(Application outlookApplication, File tempFile) throws Exception {
if (log.isLoggable(Level.FINE)) log.fine("loadMailItem(" + tempFile);
IssueMailItem mailItem = null;
MailItem mailItemDisp = null;
File mailFile = tempFile;
int maxRetries = 100;
// We need a retry loop, because the MSG file could already be opened.
for (int retries = 0; retries < maxRetries; retries++) {
try {
// Create MailItem object from file.
if (log.isLoggable(Level.FINE)) log.fine("OpenSharedItem(" + tempFile + ")");
mailItemDisp = Dispatch.as(outlookApplication.getSession().OpenSharedItem(mailFile.getAbsolutePath()), MailItem.class);
mailItem = new IssueMailItemImpl(mailItemDisp);
break;
}
catch (Exception e) {
if (log.isLoggable(Level.FINE)) log.fine("failed: " + e);
if (retries == maxRetries-1) throw e;
// Copy MSG file to an unique file.
for (; retries < maxRetries && mailFile.exists(); retries++) {
mailFile = makeTempFile(tempFile.getParentFile(), tempFile.getName(), retries);
}
final File fmailFile = mailFile;
Files.copy(tempFile.toPath(), fmailFile.toPath());
resourcesToRelease.add(() -> fmailFile.delete());
// for-loop: try to open the copied MSG file.
}
}
if (log.isLoggable(Level.FINE)) log.fine(")loadMailItem=" + mailItem);
return mailItem;
}
private File makeUniqueExportFileName(File dir, File tempFile, ProgressCallback cb) {
File destFile = tempFile;
if (!tempFile.getParent().equals(dir)) { // should always un-equal: export directory should not be the same as the temp directory.
for (int retries = 0; retries < 1000; retries++) {
destFile = makeTempFile(dir, tempFile.getName(), retries);
if (compareFiles(tempFile, destFile, cb)) {
break;
}
if (!destFile.exists()) {
break;
}
}
}
cb.setFinished();
return destFile;
}
private File makeTempFile(File tempDir, String fname, int retries) {
if (retries != 0) {
String unique = Integer.toString(retries);
int p = fname.lastIndexOf('.');
if (p >= 0) {
fname = fname.substring(0, p) + "_" + unique + fname.substring(p);
}
}
File destFile = new File(tempDir, fname);
return destFile;
}
/**
* Return thumbnail image of attachment.
* Downloads the image if thumbnailUrl is not empty from the sever.
* @param attachment
* @param cb
* @return Thumbnail image or null, if there is no thumbnail available.
*/
public static Image getThumbnailImage(Attachment attachment, ProgressCallback cb) {
if (log.isLoggable(Level.FINE)) log.fine("getThumbnailImage(" + attachment);
Image ret = attachment.getThumbnailImage();
if (ret == null) {
String thumbnailUrl = attachment.getThumbnailUrl();
if (log.isLoggable(Level.FINE)) log.fine("thumbnailUrl=" + thumbnailUrl);
try {
if (Default.value(thumbnailUrl).isEmpty()) {
if (attachment.getId().isEmpty()) {
if (attachment.getLocalFile() != null) {
cb.incrProgress(0.2);
File thumbnailFile = ThumbnailHelper.makeThumbnail(attachment.getLocalFile());
if (thumbnailFile != null) {
attachment.setThumbnailUrl(thumbnailFile.toURI().toString());
if (log.isLoggable(Level.FINE)) log.fine("thumbnailUrl=" + attachment.getThumbnailUrl());
ret = new Image(attachment.getThumbnailUrl());
}
cb.setFinished();
}
}
}
else {
// Use download function since Image constructor does not follow redirections.
if (!thumbnailUrl.startsWith(MailAttachmentHelper.FILE_URL_PREFIX)) {
String fpath = Globals.getIssueService().downloadAttachment(thumbnailUrl, cb);
File thumbnailFile = new File(fpath);
thumbnailUrl = thumbnailFile.toURI().toString();
attachment.setThumbnailUrl(thumbnailUrl);
if (log.isLoggable(Level.FINE)) log.fine("thumbnailUrl=" + attachment.getThumbnailUrl());
}
Image image = new Image(thumbnailUrl);
attachment.setThumbnailImage(image);
}
}
catch (Exception e) {
log.log(Level.WARNING, "Failed to download thumbnail=" + thumbnailUrl + " for attachment=" + attachment, e);
}
}
if (log.isLoggable(Level.FINE)) log.fine(")getThumbnailImage=" + ret);
return ret;
}
public static String getFileChecksum(File file) throws Exception {
byte[] b = Files.readAllBytes(Paths.get(file.toURI()));
byte[] hash = MessageDigest.getInstance("MD5").digest(b);
String ret = DatatypeConverter.printHexBinary(hash);
return ret;
}
private boolean isBlacklistAttachment(Attachment att) throws Exception {
boolean ret = false;
long size = att.getContentLength();
for (AttachmentBlacklistItem blackItem : Globals.getAppInfo().getConfig().getBlacklist()) {
// For performance reasons, check the size first before saving the attachment and
// computing the MD5 hash.
// Since the file size returned from Outlook is a bit larger than the real file size,
// we cannot check of equal size. We have to add a tolerance.
if (Math.abs(blackItem.getSize() - size) < 10000) {
att.getStream().close(); // save attachment to local file
String hash = MailAttachmentHelper.getFileChecksum(att.getLocalFile());
ret = hash.equals(blackItem.getHash());
if (ret) break;
}
}
return ret;
}
public static void addBlacklistItem(String name, File file) throws Exception {
if (log.isLoggable(Level.FINE)) log.log(Level.FINE, "addBlacklistItem(" + file);
String hash = MailAttachmentHelper.getFileChecksum(file);
AttachmentBlacklistItem item = new AttachmentBlacklistItem(name, file.length(), hash);
if (log.isLoggable(Level.INFO)) log.info("Add blacklist item=" + item);
boolean found = false;
for (AttachmentBlacklistItem blackItem : Globals.getAppInfo().getConfig().getBlacklist()) {
found = blackItem.getHash().equals(hash);
if (found) break;
}
if (!found) {
Globals.getAppInfo().getConfig().getBlacklist().add(item);
}
if (log.isLoggable(Level.FINE)) log.log(Level.FINE, ")addBlacklistItem");
}
}