-package org.argeo.slc.mail;
-
-import static java.lang.System.Logger.Level.DEBUG;
-import static java.lang.System.Logger.Level.ERROR;
-import static org.argeo.slc.mail.EmailUtils.describe;
-
-import java.io.BufferedReader;
-import java.io.BufferedWriter;
-import java.io.File;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.lang.System.Logger;
-import java.nio.charset.StandardCharsets;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.nio.file.StandardCopyOption;
-import java.time.Instant;
-import java.util.Date;
-import java.util.Enumeration;
-import java.util.Properties;
-
-import javax.mail.FetchProfile;
-import javax.mail.Folder;
-import javax.mail.Message;
-import javax.mail.MessagingException;
-import javax.mail.Multipart;
-import javax.mail.Session;
-import javax.mail.Store;
-import javax.mail.URLName;
-import javax.mail.internet.InternetHeaders;
-import javax.mail.internet.MimeBodyPart;
-import javax.mail.internet.MimeMessage;
-import javax.mail.search.HeaderTerm;
-import javax.mail.util.SharedFileInputStream;
-
-import com.sun.mail.imap.IMAPFolder;
-import com.sun.mail.mbox.MboxFolder;
-import com.sun.mail.mbox.MboxMessage;
-
-/** Migrates emails from one storage to the another one. */
-public class EmailMigration {
- private final static Logger logger = System.getLogger(EmailMigration.class.getName());
-
-// private String targetBaseDir;
-
- private String sourceServer;
- private String sourceUsername;
- private String sourcePassword;
-
- private String targetServer;
- private String targetUsername;
- private String targetPassword;
-
- public void process() throws MessagingException, IOException {
-// Path baseDir = Paths.get(targetBaseDir).resolve(sourceUsername).resolve("mbox");
-
- Store sourceStore = null;
- try {
- Properties sourceProperties = System.getProperties();
- sourceProperties.setProperty("mail.store.protocol", "imaps");
-
- Session sourceSession = Session.getInstance(sourceProperties, null);
- // session.setDebug(true);
- sourceStore = sourceSession.getStore("imaps");
- sourceStore.connect(sourceServer, sourceUsername, sourcePassword);
-
- Folder defaultFolder = sourceStore.getDefaultFolder();
-// migrateFolders(baseDir, defaultFolder);
-
- // Always start with Inbox
-// Folder inboxFolder = sourceStore.getFolder(EmailUtils.INBOX);
-// migrateFolder(baseDir, inboxFolder);
-
- Properties targetProperties = System.getProperties();
- targetProperties.setProperty("mail.imap.starttls.enable", "true");
- targetProperties.setProperty("mail.imap.auth", "true");
-
- Session targetSession = Session.getInstance(targetProperties, null);
- // session.setDebug(true);
- Store targetStore = targetSession.getStore("imap");
- targetStore.connect(targetServer, targetUsername, targetPassword);
-
-// Folder targetFolder = targetStore.getFolder(EmailUtils.INBOX);
-// logger.log(DEBUG, "Source message count " + inboxFolder.getMessageCount());
-// logger.log(DEBUG, "Target message count " + targetFolder.getMessageCount());
-
- migrateFolders(defaultFolder, targetStore);
- } finally {
- if (sourceStore != null)
- sourceStore.close();
-
- }
- }
-
- protected void migrateFolders(Folder sourceFolder, Store targetStore) throws MessagingException, IOException {
- folders: for (Folder folder : sourceFolder.list()) {
- String folderName = folder.getName();
-
- String folderFullName = folder.getFullName();
-
- // GMail specific
- if (folderFullName.equals("[Gmail]")) {
- migrateFolders(folder, targetStore);
- continue folders;
- }
- if (folderFullName.startsWith("[Gmail]")) {
- // Make it configurable
- switch (folderName) {
- case "All Mail":
- case "Important":
- case "Spam":
- continue folders;
- default:
- // does nothing
- }
- folderFullName = folder.getName();
- }
-
- int messageCount = (folder.getType() & Folder.HOLDS_MESSAGES) != 0 ? folder.getMessageCount() : 0;
- boolean hasSubFolders = (folder.getType() & Folder.HOLDS_FOLDERS) != 0 ? folder.list().length != 0 : false;
- Folder targetFolder;
- if (hasSubFolders) {// has sub-folders
- if (messageCount == 0) {
- targetFolder = targetStore.getFolder(folderFullName);
- if (!targetFolder.exists()) {
- targetFolder.create(Folder.HOLDS_FOLDERS);
- logger.log(DEBUG, "Created HOLDS_FOLDERS folder " + targetFolder.getFullName());
- }
- } else {// also has messages
- Folder parentFolder = targetStore.getFolder(folderFullName);
- if (!parentFolder.exists()) {
- parentFolder.create(Folder.HOLDS_FOLDERS);
- logger.log(DEBUG, "Created HOLDS_FOLDERS folder " + parentFolder.getFullName());
- }
- String miscFullName = folderFullName + "/_Misc";
- targetFolder = targetStore.getFolder(miscFullName);
- if (!targetFolder.exists()) {
- targetFolder.create(Folder.HOLDS_MESSAGES);
- logger.log(DEBUG, "Created HOLDS_MESSAGES folder " + targetFolder.getFullName());
- }
- }
- } else {// no sub-folders
- if (messageCount == 0) { // empty
- logger.log(DEBUG, "Skip empty folder " + folderFullName);
- continue folders;
- }
- targetFolder = targetStore.getFolder(folderFullName);
- if (!targetFolder.exists()) {
- targetFolder.create(Folder.HOLDS_MESSAGES);
- logger.log(DEBUG, "Created HOLDS_MESSAGES folder " + targetFolder.getFullName());
- }
- }
-
- if (messageCount != 0) {
-
- targetFolder.open(Folder.READ_WRITE);
- try {
- long begin = System.currentTimeMillis();
- folder.open(Folder.READ_ONLY);
- migrateFolder(folder, targetFolder);
- long duration = System.currentTimeMillis() - begin;
- logger.log(DEBUG, folderFullName + " - Migration of " + messageCount + " messages took "
- + (duration / 1000) + " s (" + (duration / messageCount) + " ms per message)");
- } finally {
- folder.close();
- targetFolder.close();
- }
- }
-
- // recursive
- if (hasSubFolders) {
- migrateFolders(folder, targetStore);
- }
- }
- }
-
- protected void migrateFoldersToFs(Path baseDir, Folder sourceFolder) throws MessagingException, IOException {
- folders: for (Folder folder : sourceFolder.list()) {
- String folderName = folder.getName();
-
- if ((folder.getType() & Folder.HOLDS_MESSAGES) != 0) {
- // Make it configurable
- switch (folderName) {
- case "All Mail":
- case "Important":
- continue folders;
- default:
- // doe nothing
- }
- migrateFolderToFs(baseDir, folder);
- }
- if ((folder.getType() & Folder.HOLDS_FOLDERS) != 0) {
- migrateFoldersToFs(baseDir.resolve(folder.getName()), folder);
- }
- }
- }
-
- protected void migrateFolderToFs(Path baseDir, Folder sourceFolder) throws MessagingException, IOException {
-
- String folderName = sourceFolder.getName();
- sourceFolder.open(Folder.READ_ONLY);
-
- Folder targetFolder = null;
- try {
- int messageCount = sourceFolder.getMessageCount();
- logger.log(DEBUG, folderName + " - Message count : " + messageCount);
- if (messageCount == 0)
- return;
-// logger.log(DEBUG, folderName + " - Unread Messages : " + sourceFolder.getUnreadMessageCount());
-
- boolean saveAsFiles = false;
-
- if (saveAsFiles) {
- Message messages[] = sourceFolder.getMessages();
-
- for (int i = 0; i < messages.length; ++i) {
-// logger.log(DEBUG, "MESSAGE #" + (i + 1) + ":");
- Message msg = messages[i];
-// String from = "unknown";
-// if (msg.getReplyTo().length >= 1) {
-// from = msg.getReplyTo()[0].toString();
-// } else if (msg.getFrom().length >= 1) {
-// from = msg.getFrom()[0].toString();
-// }
- String subject = msg.getSubject();
- Instant sentDate = msg.getSentDate().toInstant();
-// logger.log(DEBUG, "Saving ... " + subject + " from " + from + " (" + sentDate + ")");
- String fileName = sentDate + " " + subject;
- Path file = baseDir.resolve(fileName);
- savePartsAsFiles(msg.getContent(), file);
- }
- }
- else {
- long begin = System.currentTimeMillis();
- targetFolder = openMboxTargetFolder(sourceFolder, baseDir);
- migrateFolder(sourceFolder, targetFolder);
- long duration = System.currentTimeMillis() - begin;
- logger.log(DEBUG, folderName + " - Migration of " + messageCount + " messages took " + (duration / 1000)
- + " s (" + (duration / messageCount) + " ms per message)");
- }
- } finally {
- sourceFolder.close();
- if (targetFolder != null)
- targetFolder.close();
- }
- }
-
- protected Folder migrateFolder(Folder sourceFolder, Folder targetFolder) throws MessagingException, IOException {
- String folderName = targetFolder.getName();
-
- int lastSourceNumber;
- int currentTargetMessageCount = targetFolder.getMessageCount();
- if (currentTargetMessageCount != 0) {
- MimeMessage lastTargetMessage = (MimeMessage) targetFolder.getMessage(currentTargetMessageCount);
- logger.log(DEBUG, folderName + " - Last target message " + describe(lastTargetMessage));
- Date lastTargetSent = lastTargetMessage.getReceivedDate();
- Message[] lastSourceMessage = sourceFolder
- .search(new HeaderTerm(EmailUtils.MESSAGE_ID, lastTargetMessage.getMessageID()));
- if (lastSourceMessage.length == 0)
- throw new IllegalStateException("No message found with message ID " + lastTargetMessage.getMessageID());
- if (lastSourceMessage.length != 1) {
- for (Message msg : lastSourceMessage) {
- logger.log(ERROR, "Message " + describe(msg));
-
- }
- throw new IllegalStateException(
- lastSourceMessage.length + " messages found with received date " + lastTargetSent.toInstant());
- }
- lastSourceNumber = lastSourceMessage[0].getMessageNumber();
- } else {
- lastSourceNumber = 0;
- }
- logger.log(DEBUG, folderName + " - Last source message number " + lastSourceNumber);
-
- int countToRetrieve = sourceFolder.getMessageCount() - lastSourceNumber;
-
- FetchProfile fetchProfile = new FetchProfile();
- fetchProfile.add(FetchProfile.Item.FLAGS);
- fetchProfile.add(FetchProfile.Item.ENVELOPE);
- fetchProfile.add(FetchProfile.Item.CONTENT_INFO);
- fetchProfile.add(FetchProfile.Item.SIZE);
- if (sourceFolder instanceof IMAPFolder) {
- // IMAPFolder sourceImapFolder = (IMAPFolder) sourceFolder;
- fetchProfile.add(IMAPFolder.FetchProfileItem.HEADERS);
- fetchProfile.add(IMAPFolder.FetchProfileItem.MESSAGE);
- }
-
- int batchSize = 100;
- int batchCount = countToRetrieve / batchSize;
- if (countToRetrieve % batchSize != 0)
- batchCount = batchCount + 1;
- // int batchCount = 2; // for testing
- for (int i = 0; i < batchCount; i++) {
- long begin = System.currentTimeMillis();
-
- int start = lastSourceNumber + i * batchSize + 1;
- int end = lastSourceNumber + (i + 1) * batchSize;
- if (end >= (lastSourceNumber + countToRetrieve + 1))
- end = lastSourceNumber + countToRetrieve;
- Message[] sourceMessages = sourceFolder.getMessages(start, end);
- sourceFolder.fetch(sourceMessages, fetchProfile);
- // targetFolder.appendMessages(sourceMessages);
- // sourceFolder.copyMessages(sourceMessages,targetFolder);
-
- copyMessages(sourceMessages, targetFolder);
-// copyMessagesToMbox(sourceMessages, targetFolder);
-
- String describeLast = describe(sourceMessages[sourceMessages.length - 1]);
-
-// if (i % 10 == 9) {
- // free memory from fetched messages
- sourceFolder.close();
- targetFolder.close();
-
- sourceFolder.open(Folder.READ_ONLY);
- targetFolder.open(Folder.READ_WRITE);
-// logger.log(DEBUG, "Open/close folder in order to free memory");
-// }
-
- long duration = System.currentTimeMillis() - begin;
- logger.log(DEBUG, folderName + " - batch " + i + " took " + (duration / 1000) + " s, "
- + (duration / (end - start + 1)) + " ms per message. Last message " + describeLast);
- }
-
- return targetFolder;
- }
-
- protected Folder openMboxTargetFolder(Folder sourceFolder, Path baseDir) throws MessagingException, IOException {
- String folderName = sourceFolder.getName();
- if (sourceFolder.getName().equals(EmailUtils.INBOX_UPPER_CASE))
- folderName = EmailUtils.INBOX;// Inbox
-
- Path targetDir = baseDir;// .resolve("mbox");
- Files.createDirectories(targetDir);
- Path targetPath;
- if (((sourceFolder.getType() & Folder.HOLDS_FOLDERS) != 0) && sourceFolder.list().length != 0) {
- Path dir = targetDir.resolve(folderName);
- Files.createDirectories(dir);
- targetPath = dir.resolve("_Misc");
- } else {
- targetPath = targetDir.resolve(folderName);
- }
- if (!Files.exists(targetPath))
- Files.createFile(targetPath);
- URLName targetUrlName = new URLName("mbox:" + targetPath.toString());
- Properties targetProperties = new Properties();
- // targetProperties.setProperty("mail.mime.address.strict", "false");
- Session targetSession = Session.getDefaultInstance(targetProperties);
- Folder targetFolder = targetSession.getFolder(targetUrlName);
- targetFolder.open(Folder.READ_WRITE);
-
- return targetFolder;
- }
-
- protected void copyMessages(Message[] sourceMessages, Folder targetFolder) throws MessagingException {
- targetFolder.appendMessages(sourceMessages);
- }
-
- protected void copyMessagesToMbox(Message[] sourceMessages, Folder targetFolder)
- throws MessagingException, IOException {
- Message[] targetMessages = new Message[sourceMessages.length];
- for (int j = 0; j < sourceMessages.length; j++) {
- MimeMessage sourceMm = (MimeMessage) sourceMessages[j];
- InternetHeaders ih = new InternetHeaders();
- for (Enumeration<String> e = sourceMm.getAllHeaderLines(); e.hasMoreElements();) {
- ih.addHeaderLine(e.nextElement());
- }
- Path tmpFileSource = Files.createTempFile("argeo-mbox-source", ".txt");
- Path tmpFileTarget = Files.createTempFile("argeo-mbox-target", ".txt");
- Files.copy(sourceMm.getRawInputStream(), tmpFileSource, StandardCopyOption.REPLACE_EXISTING);
-
- // we use ISO_8859_1 because it is more robust than US_ASCII with regard to
- // missing characters
- try (BufferedReader reader = Files.newBufferedReader(tmpFileSource, StandardCharsets.ISO_8859_1);
- BufferedWriter writer = Files.newBufferedWriter(tmpFileTarget, StandardCharsets.ISO_8859_1);) {
- int lineNumber = 0;
- String line = null;
- try {
- while ((line = reader.readLine()) != null) {
- lineNumber++;
- if (line.startsWith("From ")) {
- writer.write(">" + line);
- logger.log(DEBUG,
- "Fix line " + lineNumber + " in " + EmailUtils.describe(sourceMm) + ": " + line);
- } else {
- writer.write(line);
- }
- writer.newLine();
- }
- } catch (IOException e) {
- logger.log(ERROR, "Error around line " + lineNumber + " of " + tmpFileSource);
- throw e;
- }
- }
-
- MboxMessage mboxMessage = new MboxMessage((MboxFolder) targetFolder, ih,
- new SharedFileInputStream(tmpFileTarget.toFile()), sourceMm.getMessageNumber(),
- EmailUtils.getUnixFrom(sourceMm), true);
- targetMessages[j] = mboxMessage;
-
- // clean up
- Files.delete(tmpFileSource);
- Files.delete(tmpFileTarget);
- }
- targetFolder.appendMessages(targetMessages);
-
- }
-
- /** Save body parts and attachments as plain files. */
- protected void savePartsAsFiles(Object content, Path fileBase) throws IOException, MessagingException {
- OutputStream out = null;
- InputStream in = null;
- try {
- if (content instanceof Multipart) {
- Multipart multi = ((Multipart) content);
- int parts = multi.getCount();
- for (int j = 0; j < parts; ++j) {
- MimeBodyPart part = (MimeBodyPart) multi.getBodyPart(j);
- if (part.getContent() instanceof Multipart) {
- // part-within-a-part, do some recursion...
- savePartsAsFiles(part.getContent(), fileBase);
- } else {
- String extension = "";
- if (part.isMimeType("text/html")) {
- extension = "html";
- } else {
- if (part.isMimeType("text/plain")) {
- extension = "txt";
- } else {
- // Try to get the name of the attachment
- extension = part.getDataHandler().getName();
- }
- }
- String filename = fileBase + "." + extension;
- System.out.println("... " + filename);
- out = new FileOutputStream(new File(filename));
- in = part.getInputStream();
- int k;
- while ((k = in.read()) != -1) {
- out.write(k);
- }
- }
- }
- }
- } finally {
- if (in != null) {
- in.close();
- }
- if (out != null) {
- out.flush();
- out.close();
- }
- }
- }
-
- public void setSourceServer(String sourceServer) {
- this.sourceServer = sourceServer;
- }
-
- public void setSourceUsername(String sourceUsername) {
- this.sourceUsername = sourceUsername;
- }
-
- public void setSourcePassword(String sourcePassword) {
- this.sourcePassword = sourcePassword;
- }
-
- public void setTargetServer(String targetServer) {
- this.targetServer = targetServer;
- }
-
- public void setTargetUsername(String targetUsername) {
- this.targetUsername = targetUsername;
- }
-
- public void setTargetPassword(String targetPassword) {
- this.targetPassword = targetPassword;
- }
-
- public static void main(String args[]) throws Exception {
- if (args.length < 6)
- throw new IllegalArgumentException(
- "usage: <source IMAP server> <source username> <source password> <target IMAP server> <target username> <target password>");
- String sourceServer = args[0];
- String sourceUsername = args[1];
- String sourcePassword = args[2];
- String targetServer = args[3];
- String targetUsername = args[4];
- String targetPassword = args[5];
-
- EmailMigration emailMigration = new EmailMigration();
- emailMigration.setSourceServer(sourceServer);
- emailMigration.setSourceUsername(sourceUsername);
- emailMigration.setSourcePassword(sourcePassword);
- emailMigration.setTargetServer(targetServer);
- emailMigration.setTargetUsername(targetUsername);
- emailMigration.setTargetPassword(targetPassword);
-
- emailMigration.process();
- }
-}