Skip to content

Instantly share code, notes, and snippets.

@kikujin
Last active June 30, 2023 03:39
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save kikujin/c89052c66615f14d678a768ee9066451 to your computer and use it in GitHub Desktop.
Save kikujin/c89052c66615f14d678a768ee9066451 to your computer and use it in GitHub Desktop.
import java.nio.file.Path;
public interface EmlExtractionListener {
void emlWritten(Path emlPath);
void invalidEmlWritten(Path invalidEmlPath);
void operationCanceled();
}
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.IntStream;
// This class is not thread safe except for cancel() method
public class EmlExtractor {
private static final String DOT_EML = ".eml";
private static final String DOT_VMG = ".vmg";
private static final String BEGIN_TEXT = "BEGIN:VBODY";
private static final String END_TEXT = "END:VBODY";
private static final int CR = '\r';
private static final int LF = '\n';
private static final int BEGIN_TEXT_SIZE = BEGIN_TEXT.length();
private static final int[] BEGIN_UBYTES =
IntStream.range(0, BEGIN_TEXT.length())
.map(i -> BEGIN_TEXT.codePointAt(i))
.toArray();
private static final int[] END_UBYTES =
IntStream.range(0, END_TEXT.length())
.map(i -> END_TEXT.codePointAt(i))
.toArray();
private List<EmlExtractionListener> listeners = new ArrayList<>();
private volatile boolean canceled;
public boolean addListener(EmlExtractionListener listener) {
return listeners.add(listener);
}
public boolean removeListener(EmlExtractionListener listener) {
return listeners.remove(listener);
}
public void clearListeners() {
listeners.clear();
}
// thread safe
public void cancel() {
canceled = true;
}
public void extractAndWriteEml(Path vmg, Path destDir)
throws IOException {
canceled = false;
String vmgFile = vmg.getFileName().toString();
String emlHolder = vmgFile.substring(
0, vmgFile.length() - DOT_VMG.length()) + "_%05d";
try (BufferedInputStream in =
new BufferedInputStream(Files.newInputStream(vmg))) {
int fileNo = 1;
int beginUbytesIndex = 0;
int b;
while ((b = in.read()) >= 0) {
if (b == CR || b == LF) {
if (beginUbytesIndex == BEGIN_TEXT_SIZE) {
if (canceled) {
notifyOperationCanceled();
return;
}
Path emlPath;
do {
String emlName = String.format(emlHolder, fileNo++);
emlPath = destDir.resolve(emlName + DOT_EML);
} while (Files.exists(emlPath));
boolean valid = writeEml(in, emlPath, (b == CR));
notifyEmlWritten(emlPath);
if (!valid) {
notifyInvalidEmlWritten(emlPath);
}
}
beginUbytesIndex = 0;
continue;
} else if (beginUbytesIndex < 0){
continue;
} else if (beginUbytesIndex == BEGIN_TEXT_SIZE
|| b != BEGIN_UBYTES[beginUbytesIndex++]){
beginUbytesIndex = -1;
}
}
}
}
private boolean writeEml(InputStream in, Path emlPath, boolean afterCR)
throws IOException {
int bufSize = END_UBYTES.length;
int[] buf = new int[bufSize];
int index = 0;
int b = in.read();
if (afterCR && b == LF) {
b = in.read();
}
try (BufferedOutputStream out =
new BufferedOutputStream(Files.newOutputStream(emlPath))) {
do {
if (b == LF || b == CR) {
if (index == bufSize) {
return true;
}
if (index > 0) {
writeBuf(out, buf, index);
}
index = 0;
} else if (index == bufSize) {
writeBuf(out, buf, index);
index = -1;
} else if (index >= 0){
if (b == END_UBYTES[index]) {
buf[index++] = b;
continue;
} else {
writeBuf(out, buf, index);
index = -1;
}
}
out.write(b);
} while ((b = in.read()) >= 0);
return false;
}
}
private void writeBuf(OutputStream out, int[] buf, int endExclusive)
throws IOException {
for (int i = 0; i < endExclusive; i++) {
out.write(buf[i]);
}
}
private void notifyEmlWritten(Path emlPath) {
for (EmlExtractionListener listener : listeners) {
listener.emlWritten(emlPath);
}
}
private void notifyInvalidEmlWritten(Path invalidEmlPath) {
for (EmlExtractionListener listener : listeners) {
listener.invalidEmlWritten(invalidEmlPath);
}
}
private void notifyOperationCanceled() {
for (EmlExtractionListener listener : listeners) {
listener.operationCanceled();
}
}
}
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Insets;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.CancellationException;
import java.util.concurrent.ExecutionException;
import java.util.stream.Collectors;
import javax.swing.JButton;
import javax.swing.JFileChooser;
import javax.swing.JFrame;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
import javax.swing.JTextField;
import javax.swing.SwingUtilities;
import javax.swing.SwingWorker;
import javax.swing.UIManager;
import javax.swing.UIManager.LookAndFeelInfo;
import javax.swing.filechooser.FileFilter;
public class EmlExtractorGUI extends JFrame {
private static final String DOT_VMG = ".vmg";
private static final String END_VBODY = "END:VBODY";
private static final String COUNT_MARK = ".";
private final JTextField destTF = new JTextField(DEST_TF_MESSAGE);
private final JButton destButton = new JButton(DEST_BTN_LABEL);
private final JTextField srcTF = new JTextField(SRC_TF_MESSAGE);
private final JButton srcButton = new JButton(SRC_BTN_LABEL);
private final JButton extractButton = new JButton(EXT_BTN_LABEL);
private final JTextArea textArea = new JTextArea(10, 60);
private final JFileChooser fileChooser = new JFileChooser();
private Path src;
private Path dest;
private EmlExtractionTask extractionTask; //SwingWorker
public EmlExtractorGUI() {
super(TITLE);
srcTF.setEditable(false);
destTF.setEditable(false);
textArea.setEditable(false);
fileChooser.setAcceptAllFileFilterUsed(false);
fileChooser.addChoosableFileFilter(new FileFilterImpl());
srcButton.addActionListener(e -> selectSrc());
destButton.addActionListener(e -> selectDest());
extractButton.addActionListener(e -> extract());
setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);
addWindowListener(new CloseOperation());
layOut();
pack();
}
// thread safe
private void showMessageDialog(Object message) {
runOnEDT(() -> JOptionPane.showMessageDialog(this, message));
}
// thread safe
private void display(String text) {
runOnEDT(() -> textArea.append(text));
}
// thread safe
private void runOnEDT(Runnable task) {
if (SwingUtilities.isEventDispatchThread()) {
task.run();
} else {
SwingUtilities.invokeLater(task);
}
}
private boolean isRunning() {
return extractButton.getText().equals(CANCEL_BTN_LABEL);
}
private void layOut() {
JPanel panel = new JPanel();
panel.setLayout(new GridBagLayout());
GridBagConstraints gbc = new GridBagConstraints();
gbc.insets = new Insets(10, 10, 0, 0);
gbc.anchor = GridBagConstraints.WEST;
gbc.weightx = 1;
gbc.fill = GridBagConstraints.HORIZONTAL;
panel.add(srcTF, gbc);
gbc.gridx = 1;
gbc.weightx = 0;
gbc.insets = new Insets(10, 10, 0, 10);
gbc.fill = GridBagConstraints.NONE;
panel.add(srcButton, gbc);
gbc.gridx = 0;
gbc.gridy = 1;
gbc.insets = new Insets(5, 10, 0, 0);
gbc.anchor = GridBagConstraints.WEST;
gbc.fill = GridBagConstraints.HORIZONTAL;
panel.add(destTF, gbc);
gbc.gridx = 1;
gbc.insets = new Insets(5, 10, 0, 10);
gbc.fill = GridBagConstraints.NONE;
panel.add(destButton, gbc);
gbc.gridy = 2;
gbc.gridx = 1;
gbc.fill = GridBagConstraints.HORIZONTAL;
gbc.insets = new Insets(5, 10, 0, 10);
panel.add(extractButton, gbc);
gbc.gridy = 3;
gbc.gridx = 0;
gbc.gridwidth = 2;
gbc.weighty = 1;
gbc.fill = GridBagConstraints.NONE;
gbc.insets = new Insets(5, 10, 10, 10);
gbc.fill = GridBagConstraints.BOTH;
panel.add(new JScrollPane(textArea,
JScrollPane.VERTICAL_SCROLLBAR_ALWAYS,
JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS), gbc);
add(panel);
}
private void selectSrc() {
fileChooser.setFileSelectionMode(
JFileChooser.FILES_AND_DIRECTORIES);
int selected = fileChooser.showOpenDialog(this);
if (selected != JFileChooser.APPROVE_OPTION) {
return;
}
src = fileChooser.getSelectedFile().toPath();
srcTF.setText(src.toString());
}
private void selectDest() {
fileChooser.setFileSelectionMode(
JFileChooser.DIRECTORIES_ONLY);
int selected = fileChooser.showOpenDialog(this);
if (selected != JFileChooser.APPROVE_OPTION) {
return;
}
dest = fileChooser.getSelectedFile().toPath();
destTF.setText(dest.toString());
}
private void extract() {
if (isRunning()) {
extractionTask.cancelTask();
return;
}
if (src == null) {
showMessageDialog(SRC_TF_MESSAGE);
return;
}
if (dest == null) {
showMessageDialog(DEST_TF_MESSAGE);
return;
}
try {
List<Path> vmgList;
if (Files.isDirectory(src)) {
vmgList = Files.list(src)
.filter(path -> path.toString().toLowerCase()
.endsWith(DOT_VMG))
.sorted()
.collect(Collectors.toList());
if (vmgList.isEmpty()) {
showMessageDialog(VMG_EMPTY_MESSAGE);
return;
}
} else {
vmgList = Arrays.asList(src);
}
textArea.setText("");
extractionTask = new EmlExtractionTask(vmgList, dest);
extractionTask.execute();
extractButton.setText(CANCEL_BTN_LABEL);
} catch (IOException e) {
e.printStackTrace();
showMessageDialog(e);
return;
}
}
private class EmlExtractionTask extends SwingWorker<Void, Path>
implements EmlExtractionListener {
final List<Path> vmgList;
final Path destDir;
final EmlExtractor emlExtractor = new EmlExtractor();
final List<Path> invalidEmlList = new ArrayList<>();
final List<Path> emlList = new ArrayList<>();
int writtenEmlCount;
volatile boolean canceled;
EmlExtractionTask(List<Path> vmgList, Path destDir) {
this.vmgList = Objects.requireNonNull(vmgList);
this.destDir = destDir;
}
@Override
protected Void doInBackground() throws Exception {//worker thread
emlExtractor.addListener(this);
for (Path vmg : vmgList) {
if (canceled) {
return null;
}
emlExtractor.extractAndWriteEml(vmg, destDir);
}
return null;
}
@Override
protected void done() { // EDT
extractButton.setText(EXT_BTN_LABEL);
try {
get(); // for forming happens-before
} catch (ExecutionException e) {
showMessageDialog(e.getCause());
display(INCOMPLETELY_MESSAGE);
} catch (InterruptedException | CancellationException e) {
// never
} finally {
showResult();
extractionTask = null;
}
}
@Override
public void emlWritten(Path emlPath) { // worker thread
emlList.add(emlPath);
String mark = (++writtenEmlCount % 50 == 0)
? (COUNT_MARK + "\n") : COUNT_MARK;
display(mark);
}
@Override
public void invalidEmlWritten(Path invalidEmlPath) { // worker thread
invalidEmlList.add(invalidEmlPath);
}
@Override
public void operationCanceled() {}
void cancelTask() { // EDT
canceled = true;
emlExtractor.cancel();
}
//call after forming happens-before relationship between EDT and workerT
private void showResult() { // EDT
StringBuilder sb = new StringBuilder();
if (canceled) {
sb.append(CANCELED_MESSAGE);
}
sb.append(String.format(DONE_MESSAGE_HOLDER, writtenEmlCount));
if (!invalidEmlList.isEmpty()) {
sb.append(INVALID_EML_MESSAGE);
for (Path eml : invalidEmlList) {
sb.append(eml.toString());
sb.append("\n");
}
}
display(sb.toString());
}
}
private class CloseOperation extends WindowAdapter {
@Override
public void windowClosing(WindowEvent e) {
if (isRunning()) {
int result = JOptionPane.showConfirmDialog(
EmlExtractorGUI.this, CLOSING_DLG_MESSAGE,
CLOSING_DLG_TITLE, JOptionPane.YES_NO_OPTION);
if (result == JOptionPane.NO_OPTION) {
return;
}
}
dispose();
System.exit(0);
}
}
private class FileFilterImpl extends FileFilter {
@Override
public String getDescription() {
int mode = fileChooser.getFileSelectionMode();
if (mode == JFileChooser.DIRECTORIES_ONLY) {
return DIRECTORY;
}
return VMG_OR_DIRECTORY;
}
@Override
public boolean accept(File file) {
if (file.isDirectory()) {
return true;
}
String path = file.getName();
int dotIndex = path.lastIndexOf('.');
if (dotIndex < 0) {
return false;
}
String ext = path.substring(dotIndex).toLowerCase();
if (ext.equals(DOT_VMG)) {
return true;
}
return false;
}
}
private static final String TITLE = "vmgファイルからemlファイルを抽出";
private static final String DEST_TF_MESSAGE = "出力先フォルダを選択してください。";
private static final String DEST_BTN_LABEL = "出力先選択";
private static final String SRC_TF_MESSAGE =
"vmgファイルまたはvmgファイルを含むフォルダを選択してください。";
private static final String SRC_BTN_LABEL = "入力先選択";
private static final String EXT_BTN_LABEL = "EML抽出";
private static final String CANCEL_BTN_LABEL = "中止";
private static final String VMG_EMPTY_MESSAGE =
"選択されている入力先フォルダにvmgファイルがありません。";
private static final String DIRECTORY = "ディレクトリ";
private static final String VMG_OR_DIRECTORY = "vmgファイルまたはフォルダ";
private static final String DONE_MESSAGE_HOLDER =
"\n%d個のEMLファイルを生成しました。" + "\n";
private static final String INVALID_EML_MESSAGE = "\n以下のファイルはvmgファイルに "
+ END_VBODY + " が見つからなかった為、正しく抽出されていません。\n";
private static final String CANCELED_MESSAGE = "\n処理は中断されました。\n";
private static final String INCOMPLETELY_MESSAGE =
"\nエラーが発生した為、抽出されていないEMLファイルが存在する可能性が高いです。\n";
private static final String CLOSING_DLG_MESSAGE = "現在処理中ですが終了しますか?";
private static final String CLOSING_DLG_TITLE = "終了確認";
public static void main(String[] args) {
SwingUtilities.invokeLater(() -> {
try {
for (LookAndFeelInfo info : UIManager
.getInstalledLookAndFeels()) {
if ("Nimbus".equals(info.getName())) {
UIManager.setLookAndFeel(info.getClassName());
break;
}
}
} catch (Exception e) {
System.out.println(e);
}
JFrame frame = new EmlExtractorGUI();
frame.setVisible(true);
});
}
}
Copyright (c) 2019 kikujin
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment