1 package org
.argeo
.slc
.mail
;
3 import static java
.lang
.System
.Logger
.Level
.DEBUG
;
4 import static java
.lang
.System
.Logger
.Level
.ERROR
;
5 import static org
.argeo
.slc
.mail
.EmailUtils
.describe
;
7 import java
.io
.BufferedReader
;
8 import java
.io
.BufferedWriter
;
10 import java
.io
.FileOutputStream
;
11 import java
.io
.IOException
;
12 import java
.io
.InputStream
;
13 import java
.io
.OutputStream
;
14 import java
.lang
.System
.Logger
;
15 import java
.nio
.charset
.StandardCharsets
;
16 import java
.nio
.file
.Files
;
17 import java
.nio
.file
.Path
;
18 import java
.nio
.file
.Paths
;
19 import java
.nio
.file
.StandardCopyOption
;
20 import java
.time
.Instant
;
21 import java
.util
.Date
;
22 import java
.util
.Enumeration
;
23 import java
.util
.Properties
;
25 import javax
.mail
.FetchProfile
;
26 import javax
.mail
.Folder
;
27 import javax
.mail
.Message
;
28 import javax
.mail
.MessagingException
;
29 import javax
.mail
.Multipart
;
30 import javax
.mail
.Session
;
31 import javax
.mail
.Store
;
32 import javax
.mail
.URLName
;
33 import javax
.mail
.internet
.InternetHeaders
;
34 import javax
.mail
.internet
.MimeBodyPart
;
35 import javax
.mail
.internet
.MimeMessage
;
36 import javax
.mail
.search
.HeaderTerm
;
37 import javax
.mail
.util
.SharedFileInputStream
;
39 import com
.sun
.mail
.imap
.IMAPFolder
;
40 import com
.sun
.mail
.mbox
.MboxFolder
;
41 import com
.sun
.mail
.mbox
.MboxMessage
;
43 /** Migrates emails from one storage to the another one. */
44 public class EmailMigration
{
45 private final static Logger logger
= System
.getLogger(EmailMigration
.class.getName());
47 private String targetBaseDir
;
48 private String sourceServer
;
49 private String sourceUsername
;
50 private String sourcePassword
;
52 public void process() throws MessagingException
, IOException
{
53 Path baseDir
= Paths
.get(targetBaseDir
).resolve(sourceUsername
).resolve("mbox");
55 Store sourceStore
= null;
57 Properties sourceProperties
= System
.getProperties();
58 sourceProperties
.setProperty("mail.store.protocol", "imaps");
60 Session sourceSession
= Session
.getDefaultInstance(sourceProperties
, null);
61 // session.setDebug(true);
62 sourceStore
= sourceSession
.getStore("imaps");
63 sourceStore
.connect(sourceServer
, sourceUsername
, sourcePassword
);
65 Folder defaultFolder
= sourceStore
.getDefaultFolder();
66 migrateFolders(baseDir
, defaultFolder
);
68 // Always start with Inbox
69 // Folder inboxFolder = sourceStore.getFolder(EmailUtils.INBOX);
70 // migrateFolder(baseDir, inboxFolder);
72 if (sourceStore
!= null)
78 protected void migrateFolders(Path baseDir
, Folder sourceFolder
) throws MessagingException
, IOException
{
79 folders
: for (Folder folder
: sourceFolder
.list()) {
80 String folderName
= folder
.getName();
82 if ((folder
.getType() & Folder
.HOLDS_MESSAGES
) != 0) {
83 // Make it configurable
91 migrateFolder(baseDir
, folder
);
93 if ((folder
.getType() & Folder
.HOLDS_FOLDERS
) != 0) {
94 migrateFolders(baseDir
.resolve(folder
.getName()), folder
);
99 protected void migrateFolder(Path baseDir
, Folder sourceFolder
) throws MessagingException
, IOException
{
101 String folderName
= sourceFolder
.getName();
102 sourceFolder
.open(Folder
.READ_ONLY
);
104 Folder targetFolder
= null;
106 int messageCount
= sourceFolder
.getMessageCount();
107 logger
.log(DEBUG
, folderName
+ " - Message count : " + messageCount
);
108 if (messageCount
== 0)
110 // logger.log(DEBUG, folderName + " - Unread Messages : " + sourceFolder.getUnreadMessageCount());
112 boolean saveAsFiles
= false;
115 Message messages
[] = sourceFolder
.getMessages();
117 for (int i
= 0; i
< messages
.length
; ++i
) {
118 // logger.log(DEBUG, "MESSAGE #" + (i + 1) + ":");
119 Message msg
= messages
[i
];
120 // String from = "unknown";
121 // if (msg.getReplyTo().length >= 1) {
122 // from = msg.getReplyTo()[0].toString();
123 // } else if (msg.getFrom().length >= 1) {
124 // from = msg.getFrom()[0].toString();
126 String subject
= msg
.getSubject();
127 Instant sentDate
= msg
.getSentDate().toInstant();
128 // logger.log(DEBUG, "Saving ... " + subject + " from " + from + " (" + sentDate + ")");
129 String fileName
= sentDate
+ " " + subject
;
130 Path file
= baseDir
.resolve(fileName
);
131 savePartsAsFiles(msg
.getContent(), file
);
134 long begin
= System
.currentTimeMillis();
135 targetFolder
= migrateFolderToMbox(baseDir
, sourceFolder
);
136 long duration
= System
.currentTimeMillis() - begin
;
137 logger
.log(DEBUG
, folderName
+ " - Migration of " + messageCount
+ " messages took " + (duration
/ 1000)
138 + " s (" + (duration
/ messageCount
) + " ms per message)");
141 sourceFolder
.close();
142 if (targetFolder
!= null)
143 targetFolder
.close();
147 protected Folder
migrateFolderToMbox(Path baseDir
, Folder sourceFolder
) throws MessagingException
, IOException
{
148 String folderName
= sourceFolder
.getName();
149 if (sourceFolder
.getName().equals(EmailUtils
.INBOX_UPPER_CASE
))
150 folderName
= EmailUtils
.INBOX
;// Inbox
152 Path targetDir
= baseDir
;// .resolve("mbox");
153 Files
.createDirectories(targetDir
);
155 if (((sourceFolder
.getType() & Folder
.HOLDS_FOLDERS
) != 0) && sourceFolder
.list().length
!= 0) {
156 Path dir
= targetDir
.resolve(folderName
);
157 Files
.createDirectories(dir
);
158 targetPath
= dir
.resolve("_Misc");
160 targetPath
= targetDir
.resolve(folderName
);
162 if (!Files
.exists(targetPath
))
163 Files
.createFile(targetPath
);
164 URLName targetUrlName
= new URLName("mbox:" + targetPath
.toString());
165 Properties targetProperties
= new Properties();
166 // targetProperties.setProperty("mail.mime.address.strict", "false");
167 Session targetSession
= Session
.getDefaultInstance(targetProperties
);
168 Folder targetFolder
= targetSession
.getFolder(targetUrlName
);
169 targetFolder
.open(Folder
.READ_WRITE
);
171 int lastSourceNumber
;
172 int currentTargetMessageCount
= targetFolder
.getMessageCount();
173 if (currentTargetMessageCount
!= 0) {
174 MimeMessage lastTargetMessage
= (MimeMessage
) targetFolder
.getMessage(currentTargetMessageCount
);
175 logger
.log(DEBUG
, folderName
+ " - Last target message " + describe(lastTargetMessage
));
176 Date lastTargetSent
= lastTargetMessage
.getReceivedDate();
177 Message
[] lastSourceMessage
= sourceFolder
178 .search(new HeaderTerm(EmailUtils
.MESSAGE_ID
, lastTargetMessage
.getMessageID()));
179 if (lastSourceMessage
.length
== 0)
180 throw new IllegalStateException("No message found with message ID " + lastTargetMessage
.getMessageID());
181 if (lastSourceMessage
.length
!= 1) {
182 for (Message msg
: lastSourceMessage
) {
183 logger
.log(ERROR
, "Message " + describe(msg
));
186 throw new IllegalStateException(
187 lastSourceMessage
.length
+ " messages found with received date " + lastTargetSent
.toInstant());
189 lastSourceNumber
= lastSourceMessage
[0].getMessageNumber();
191 lastSourceNumber
= 0;
193 logger
.log(DEBUG
, folderName
+ " - Last source message number " + lastSourceNumber
);
195 int countToRetrieve
= sourceFolder
.getMessageCount() - lastSourceNumber
;
196 // for (int i = startNumber; i < messageCount; i++) {
197 // long begin = System.currentTimeMillis();
198 // Message message = sourceFolder.getMessage(i);
199 // targetFolder.appendMessages(new Message[] { message });
200 // long duration = System.currentTimeMillis() - begin;
201 // logger.log(DEBUG, "Message " + i + " migrated in " + duration + " ms");
204 FetchProfile fetchProfile
= new FetchProfile();
205 fetchProfile
.add(FetchProfile
.Item
.FLAGS
);
206 fetchProfile
.add(FetchProfile
.Item
.ENVELOPE
);
207 fetchProfile
.add(FetchProfile
.Item
.CONTENT_INFO
);
208 fetchProfile
.add(FetchProfile
.Item
.SIZE
);
209 if (sourceFolder
instanceof IMAPFolder
) {
210 // IMAPFolder sourceImapFolder = (IMAPFolder) sourceFolder;
211 fetchProfile
.add(IMAPFolder
.FetchProfileItem
.HEADERS
);
212 fetchProfile
.add(IMAPFolder
.FetchProfileItem
.MESSAGE
);
216 int batchCount
= countToRetrieve
/ batchSize
;
217 if (countToRetrieve
% batchSize
!= 0)
218 batchCount
= batchCount
+ 1;
219 // int batchCount = 2; // for testing
220 for (int i
= 0; i
< batchCount
; i
++) {
221 long begin
= System
.currentTimeMillis();
223 int start
= lastSourceNumber
+ i
* batchSize
+ 1;
224 int end
= lastSourceNumber
+ (i
+ 1) * batchSize
;
225 if (end
>= (lastSourceNumber
+ countToRetrieve
+ 1))
226 end
= lastSourceNumber
+ countToRetrieve
;
227 Message
[] sourceMessages
= sourceFolder
.getMessages(start
, end
);
228 sourceFolder
.fetch(sourceMessages
, fetchProfile
);
229 // targetFolder.appendMessages(sourceMessages);
230 // sourceFolder.copyMessages(sourceMessages,targetFolder);
232 Message
[] targetMessages
= new Message
[sourceMessages
.length
];
233 for (int j
= 0; j
< sourceMessages
.length
; j
++) {
234 MimeMessage sourceMm
= (MimeMessage
) sourceMessages
[j
];
235 InternetHeaders ih
= new InternetHeaders();
236 for (Enumeration
<String
> e
= sourceMm
.getAllHeaderLines(); e
.hasMoreElements();) {
237 ih
.addHeaderLine(e
.nextElement());
239 // Flags flags = sourceMm.getFlags();
240 // StringBuilder status = new StringBuilder();
241 // if (flags.contains(Flags.Flag.SEEN))
242 // status.append('R');
243 // if (!flags.contains(Flags.Flag.RECENT))
244 // status.append('O');
245 // if (status.length() > 0 && ih.getHeader("X-Status") == null)
246 // ih.setHeader("X-Status", status.toString());
248 Path tmpFileSource
= Files
.createTempFile("argeo-mbox-source", ".txt");
249 Path tmpFileTarget
= Files
.createTempFile("argeo-mbox-target", ".txt");
250 // logger.log(DEBUG, "tmpFileSource " + tmpFileSource + ", tmpFileTarget " +
252 Files
.copy(sourceMm
.getRawInputStream(), tmpFileSource
, StandardCopyOption
.REPLACE_EXISTING
);
254 // we use ISO_8859_1 because it is more robust than US_ASCII with regard to
255 // missing characters
256 try (BufferedReader reader
= Files
.newBufferedReader(tmpFileSource
, StandardCharsets
.ISO_8859_1
);
257 BufferedWriter writer
= Files
.newBufferedWriter(tmpFileTarget
, StandardCharsets
.ISO_8859_1
);) {
261 while ((line
= reader
.readLine()) != null) {
263 if (line
.startsWith("From ")) {
264 writer
.write(">" + line
);
265 logger
.log(DEBUG
, "Fix line " + lineNumber
+ " in " + EmailUtils
.describe(sourceMm
)
272 } catch (IOException e
) {
273 logger
.log(ERROR
, "Error around line " + lineNumber
+ " of " + tmpFileSource
);
278 MboxMessage mboxMessage
= new MboxMessage((MboxFolder
) targetFolder
, ih
,
279 new SharedFileInputStream(tmpFileTarget
.toFile()), sourceMm
.getMessageNumber(),
280 EmailUtils
.getUnixFrom(sourceMm
), true);
281 targetMessages
[j
] = mboxMessage
;
284 Files
.delete(tmpFileSource
);
285 Files
.delete(tmpFileTarget
);
287 targetFolder
.appendMessages(targetMessages
);
288 // Message[] targetMessages = targetFolder.getMessages(start, end);
289 // for (int j = 0; j < sourceMessages.length; j++) {
290 // EmailUtils.setHeadersFromFlags((MimeMessage) targetMessages[j], sourceMessages[j].getFlags());
291 //// Flags flags = sourceMessages[j].getFlags();
292 //// targetMessages[j].setFlags(flags, true);
293 // targetMessages[j].saveChanges();
296 String describeLast
= describe(sourceMessages
[sourceMessages
.length
- 1]);
298 // if (i % 10 == 9) {
299 // free memory from fetched messages
300 sourceFolder
.close();
301 targetFolder
.close();
303 sourceFolder
.open(Folder
.READ_ONLY
);
304 targetFolder
.open(Folder
.READ_WRITE
);
305 // logger.log(DEBUG, "Open/close folder in order to free memory");
308 long duration
= System
.currentTimeMillis() - begin
;
309 logger
.log(DEBUG
, folderName
+ " - batch " + i
+ " took " + (duration
/ 1000) + " s, "
310 + (duration
/ (end
- start
+ 1)) + " ms per message. Last message " + describeLast
);
316 /** Save body parts and attachments as plain files. */
317 protected void savePartsAsFiles(Object content
, Path fileBase
) throws IOException
, MessagingException
{
318 OutputStream out
= null;
319 InputStream in
= null;
321 if (content
instanceof Multipart
) {
322 Multipart multi
= ((Multipart
) content
);
323 int parts
= multi
.getCount();
324 for (int j
= 0; j
< parts
; ++j
) {
325 MimeBodyPart part
= (MimeBodyPart
) multi
.getBodyPart(j
);
326 if (part
.getContent() instanceof Multipart
) {
327 // part-within-a-part, do some recursion...
328 savePartsAsFiles(part
.getContent(), fileBase
);
330 String extension
= "";
331 if (part
.isMimeType("text/html")) {
334 if (part
.isMimeType("text/plain")) {
337 // Try to get the name of the attachment
338 extension
= part
.getDataHandler().getName();
341 String filename
= fileBase
+ "." + extension
;
342 System
.out
.println("... " + filename
);
343 out
= new FileOutputStream(new File(filename
));
344 in
= part
.getInputStream();
346 while ((k
= in
.read()) != -1) {
363 public void setTargetBaseDir(String targetBaseDir
) {
364 this.targetBaseDir
= targetBaseDir
;
367 public void setSourceServer(String sourceServer
) {
368 this.sourceServer
= sourceServer
;
371 public void setSourceUsername(String sourceUsername
) {
372 this.sourceUsername
= sourceUsername
;
375 public void setSourcePassword(String sourcePassword
) {
376 this.sourcePassword
= sourcePassword
;
379 public static void main(String args
[]) throws Exception
{
381 throw new IllegalArgumentException(
382 "usage: <target base dir> <source IMAP server> <source username> <source password>");
383 String targetBaseDir
= args
[0];
384 String sourceServer
= args
[1];
385 String sourceUsername
= args
[2];
386 String sourcePassword
= args
[3];
388 EmailMigration emailMigration
= new EmailMigration();
389 emailMigration
.setTargetBaseDir(targetBaseDir
);
390 emailMigration
.setSourceServer(sourceServer
);
391 emailMigration
.setSourceUsername(sourceUsername
);
392 emailMigration
.setSourcePassword(sourcePassword
);
394 emailMigration
.process();