| /* |
| * Licensed to the Apache Software Foundation (ASF) under one or more |
| * contributor license agreements. See the NOTICE file distributed with |
| * this work for additional information regarding copyright ownership. |
| * The ASF licenses this file to You 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 examples.mail; |
| |
| import java.io.BufferedWriter; |
| import java.io.File; |
| import java.io.FileWriter; |
| import java.io.IOException; |
| import java.net.URI; |
| import java.text.ParseException; |
| import java.text.SimpleDateFormat; |
| import java.util.ArrayList; |
| import java.util.Date; |
| import java.util.Iterator; |
| import java.util.List; |
| import java.util.TimeZone; |
| import java.util.regex.Matcher; |
| import java.util.regex.Pattern; |
| |
| import org.apache.commons.net.PrintCommandListener; |
| import org.apache.commons.net.ProtocolCommandEvent; |
| import org.apache.commons.net.imap.IMAP.IMAPChunkListener; |
| import org.apache.commons.net.imap.IMAP; |
| import org.apache.commons.net.imap.IMAPClient; |
| import org.apache.commons.net.imap.IMAPReply; |
| |
| /** |
| * This is an example program demonstrating how to use the IMAP[S]Client class. |
| * This program connects to a IMAP[S] server and exports selected messages from a folder into an mbox file. |
| * <p> |
| * Usage: IMAPExportMbox imap[s]://user:password@host[:port]/folder/path <mboxfile> [sequence-set] [item-names] |
| * <p> |
| * An example sequence-set might be: |
| * <ul> |
| * <li>11,2,3:10,20:*</li> |
| * <li>1:* - this is the default</li> |
| * </ul> |
| * <p> |
| * Some example item-names might be: |
| * <ul> |
| * <li>BODY.PEEK[HEADER]</li> |
| * <li>'BODY.PEEK[HEADER.FIELDS (SUBJECT)]'</li> |
| * <li>ALL</li> |
| * <li>ENVELOPE</li> |
| * <li>'(INTERNALDATE BODY.PEEK[])' - this is the default</li> |
| * </ul> |
| * <p> |
| * For example:<br> |
| * IMAPExportMbox imaps://username:password@imap.googlemail.com/messages_for_export exported.mbox 1:10,20<br> |
| * IMAPExportMbox imaps://username:password@imap.googlemail.com/messages_for_export exported.mbox 3 ENVELOPE<br> |
| * <p> |
| * Note that the sequence-set is passed unmodified to the FETCH command. |
| * The item names are wrapped in parentheses if more than one is provided. |
| * Otherwise, the parameter is assumed to be wrapped if necessary. |
| * Parameters with spaces must be quoted otherwise the OS shell will normally treat them as separate parameters. |
| * Also the listener that writes the mailbox only captures the multi-line responses. |
| * It does not capture the output from ENVELOPE commands. |
| */ |
| public final class IMAPExportMbox |
| { |
| |
| private static final String CRLF = "\r\n"; |
| private static final String LF = "\n"; |
| private static final String EOL_DEFAULT = System.getProperty("line.separator"); |
| |
| private static final Pattern PATFROM = Pattern.compile(">*From "); // unescaped From_ |
| // e.g. * nnn (INTERNALDATE "27-Oct-2013 07:43:24 +0000" BODY[] {nn} ...) |
| private static final Pattern PATID = // INTERNALDATE |
| Pattern.compile(".*INTERNALDATE \"(\\d\\d-\\w{3}-\\d{4} \\d\\d:\\d\\d:\\d\\d [+-]\\d+)\""); |
| private static final int PATID_DATE_GROUP = 1; |
| |
| private static final Pattern PATSEQ = Pattern.compile("\\* (\\d+) "); // Sequence number |
| private static final int PATSEQ_SEQUENCE_GROUP = 1; |
| |
| // e.g. * 382 EXISTS |
| private static final Pattern PATEXISTS = Pattern.compile("\\* (\\d+) EXISTS"); // Response from SELECT |
| |
| // AAAC NO [TEMPFAIL] FETCH Temporary failure on server [CODE: WBL] |
| private static final Pattern PATTEMPFAIL = Pattern.compile("[A-Z]{4} NO \\[TEMPFAIL\\] FETCH .*"); |
| |
| private static final int CONNECT_TIMEOUT = 10; // Seconds |
| private static final int READ_TIMEOUT = 10; |
| |
| public static void main(String[] args) throws IOException |
| { |
| int connect_timeout = CONNECT_TIMEOUT; |
| int read_timeout = READ_TIMEOUT; |
| |
| int argIdx = 0; |
| String eol = EOL_DEFAULT; |
| boolean printHash = false; |
| boolean printMarker = false; |
| int retryWaitSecs = 0; |
| |
| for(argIdx = 0; argIdx < args.length; argIdx++) { |
| if (args[argIdx].equals("-c")) { |
| connect_timeout = Integer.parseInt(args[++argIdx]); |
| } else if (args[argIdx].equals("-r")) { |
| read_timeout = Integer.parseInt(args[++argIdx]); |
| } else if (args[argIdx].equals("-R")) { |
| retryWaitSecs = Integer.parseInt(args[++argIdx]); |
| } else if (args[argIdx].equals("-LF")) { |
| eol = LF; |
| } else if (args[argIdx].equals("-CRLF")) { |
| eol = CRLF; |
| } else if (args[argIdx].equals("-.")) { |
| printHash = true; |
| } else if (args[argIdx].equals("-X")) { |
| printMarker = true; |
| } else { |
| break; |
| } |
| } |
| |
| final int argCount = args.length - argIdx; |
| |
| if (argCount < 2) |
| { |
| System.err.println("Usage: IMAPExportMbox [-LF|-CRLF] [-c n] [-r n] [-R n] [-.] [-X]" + |
| " imap[s]://user:password@host[:port]/folder/path [+|-]<mboxfile> [sequence-set] [itemnames]"); |
| System.err.println("\t-LF | -CRLF set end-of-line to LF or CRLF (default is the line.separator system property)"); |
| System.err.println("\t-c connect timeout in seconds (default 10)"); |
| System.err.println("\t-r read timeout in seconds (default 10)"); |
| System.err.println("\t-R temporary failure retry wait in seconds (default 0; i.e. disabled)"); |
| System.err.println("\t-. print a . for each complete message received"); |
| System.err.println("\t-X print the X-IMAP line for each complete message received"); |
| System.err.println("\tthe mboxfile is where the messages are stored; use '-' to write to standard output."); |
| System.err.println("\tPrefix filename with '+' to append to the file. Prefix with '-' to allow overwrite."); |
| System.err.println("\ta sequence-set is a list of numbers/number ranges e.g. 1,2,3-10,20:* - default 1:*"); |
| System.err.println("\titemnames are the message data item name(s) e.g. BODY.PEEK[HEADER.FIELDS (SUBJECT)]" + |
| " or a macro e.g. ALL - default (INTERNALDATE BODY.PEEK[])"); |
| System.exit(1); |
| } |
| |
| final URI uri = URI.create(args[argIdx++]); |
| final String file = args[argIdx++]; |
| String sequenceSet = argCount > 2 ? args[argIdx++] : "1:*"; |
| final String itemNames; |
| // Handle 0, 1 or multiple item names |
| if (argCount > 3) { |
| if (argCount > 4) { |
| StringBuilder sb = new StringBuilder(); |
| sb.append("("); |
| for(int i=4; i <= argCount; i++) { |
| if (i>4) { |
| sb.append(" "); |
| } |
| sb.append(args[argIdx++]); |
| } |
| sb.append(")"); |
| itemNames = sb.toString(); |
| } else { |
| itemNames = args[argIdx++]; |
| } |
| } else { |
| itemNames = "(INTERNALDATE BODY.PEEK[])"; |
| } |
| |
| final boolean checkSequence = sequenceSet.matches("\\d+:(\\d+|\\*)"); // are we expecting a sequence? |
| final MboxListener chunkListener; |
| if (file.equals("-")) { |
| chunkListener = null; |
| } else if (file.startsWith("+")) { |
| final File mbox = new File(file.substring(1)); |
| System.out.println("Appending to file " + mbox); |
| chunkListener = new MboxListener( |
| new BufferedWriter(new FileWriter(mbox, true)), eol, printHash, printMarker, checkSequence); |
| } else if (file.startsWith("-")) { |
| final File mbox = new File(file.substring(1)); |
| System.out.println("Writing to file " + mbox); |
| chunkListener = new MboxListener( |
| new BufferedWriter(new FileWriter(mbox, false)), eol, printHash, printMarker, checkSequence); |
| } else { |
| final File mbox = new File(file); |
| if (mbox.exists() && mbox.length() > 0) { |
| throw new IOException("mailbox file: " + mbox + " already exists and is non-empty!"); |
| } |
| System.out.println("Creating file " + mbox); |
| chunkListener = new MboxListener(new BufferedWriter(new FileWriter(mbox)), eol, printHash, printMarker, checkSequence); |
| } |
| |
| String path = uri.getPath(); |
| if (path == null || path.length() < 1) { |
| throw new IllegalArgumentException("Invalid folderPath: '" + path + "'"); |
| } |
| String folder = path.substring(1); // skip the leading / |
| |
| // suppress login details |
| final PrintCommandListener listener = new PrintCommandListener(System.out, true) { |
| @Override |
| public void protocolReplyReceived(ProtocolCommandEvent event) { |
| if (event.getReplyCode() != IMAPReply.PARTIAL){ // This is dealt with by the chunk listener |
| super.protocolReplyReceived(event); |
| } |
| } |
| }; |
| |
| // Connect and login |
| final IMAPClient imap = IMAPUtils.imapLogin(uri, connect_timeout * 1000, listener); |
| |
| String maxIndexInFolder = null; |
| |
| try { |
| |
| imap.setSoTimeout(read_timeout * 1000); |
| |
| if (!imap.select(folder)){ |
| throw new IOException("Could not select folder: " + folder); |
| } |
| |
| for(String line : imap.getReplyStrings()) { |
| maxIndexInFolder = matches(line, PATEXISTS, 1); |
| if (maxIndexInFolder != null) { |
| break; |
| } |
| } |
| |
| if (chunkListener != null) { |
| imap.setChunkListener(chunkListener); |
| } // else the command listener displays the full output without processing |
| |
| |
| while(true) { |
| boolean ok = imap.fetch(sequenceSet, itemNames); |
| // If the fetch failed, can we retry? |
| if (!ok && retryWaitSecs > 0 && chunkListener != null && checkSequence) { |
| final String replyString = imap.getReplyString(); //includes EOL |
| if (startsWith(replyString, PATTEMPFAIL)) { |
| System.err.println("Temporary error detected, will retry in " + retryWaitSecs + "seconds"); |
| sequenceSet = (chunkListener.lastSeq+1)+":*"; |
| try { |
| Thread.sleep(retryWaitSecs * 1000); |
| } catch (InterruptedException e) { |
| // ignored |
| } |
| } else { |
| throw new IOException("FETCH " + sequenceSet + " " + itemNames+ " failed with " + replyString); |
| } |
| } else { |
| break; |
| } |
| } |
| |
| } catch (IOException ioe) { |
| String count = chunkListener == null ? "?" : Integer.toString(chunkListener.total); |
| System.err.println( |
| "FETCH " + sequenceSet + " " + itemNames + " failed after processing " + count + " complete messages "); |
| if (chunkListener != null) { |
| System.err.println("Last complete response seen: "+chunkListener.lastFetched); |
| } |
| throw ioe; |
| } finally { |
| |
| if (printHash) { |
| System.err.println(); |
| } |
| |
| if (chunkListener != null) { |
| chunkListener.close(); |
| final Iterator<String> missingIds = chunkListener.missingIds.iterator(); |
| if (missingIds.hasNext()) { |
| StringBuilder sb = new StringBuilder(); |
| for(;;) { |
| sb.append(missingIds.next()); |
| if (!missingIds.hasNext()) { |
| break; |
| } |
| sb.append(","); |
| } |
| System.err.println("*** Missing ids: " + sb.toString()); |
| } |
| } |
| imap.logout(); |
| imap.disconnect(); |
| } |
| if (chunkListener != null) { |
| System.out.println("Processed " + chunkListener.total + " messages."); |
| } |
| if (maxIndexInFolder != null) { |
| System.out.println("Folder contained " + maxIndexInFolder + " messages."); |
| } |
| } |
| |
| private static boolean startsWith(String input, Pattern pat) { |
| Matcher m = pat.matcher(input); |
| return m.lookingAt(); |
| } |
| |
| private static String matches(String input, Pattern pat, int index) { |
| Matcher m = pat.matcher(input); |
| if (m.lookingAt()) { |
| return m.group(index); |
| } |
| return null; |
| } |
| |
| private static class MboxListener implements IMAPChunkListener { |
| |
| private final BufferedWriter bw; |
| volatile int total = 0; |
| volatile String lastFetched; |
| volatile List<String> missingIds = new ArrayList<String>(); |
| volatile long lastSeq = -1; |
| private final String eol; |
| private final SimpleDateFormat DATE_FORMAT // for mbox From_ lines |
| = new SimpleDateFormat("EEE MMM dd HH:mm:ss YYYY"); |
| |
| // e.g. INTERNALDATE "27-Oct-2013 07:43:24 +0000" |
| private final SimpleDateFormat IDPARSE // for parsing INTERNALDATE |
| = new SimpleDateFormat("dd-MMM-yyyy HH:mm:ss Z"); |
| private final boolean printHash; |
| private final boolean printMarker; |
| private final boolean checkSequence; |
| |
| MboxListener(BufferedWriter bw, String eol, boolean printHash, boolean printMarker, boolean checkSequence) |
| throws IOException { |
| this.eol = eol; |
| this.printHash = printHash; |
| this.printMarker = printMarker; |
| DATE_FORMAT.setTimeZone(TimeZone.getTimeZone("GMT")); |
| this.bw = bw; |
| this.checkSequence = checkSequence; |
| } |
| |
| @Override |
| public boolean chunkReceived(IMAP imap) { |
| final String[] replyStrings = imap.getReplyStrings(); |
| Date received = new Date(); |
| final String firstLine = replyStrings[0]; |
| Matcher m = PATID.matcher(firstLine); |
| if (m.lookingAt()) { // found a match |
| String date = m.group(PATID_DATE_GROUP); |
| try { |
| received=IDPARSE.parse(date); |
| } catch (ParseException e) { |
| System.err.println(e); |
| } |
| } else { |
| System.err.println("No timestamp found in: " + firstLine + " - using current time"); |
| } |
| String replyTo = "MAILER-DAEMON"; // default |
| for(int i=1; i< replyStrings.length - 1; i++) { |
| final String line = replyStrings[i]; |
| if (line.startsWith("Return-Path: ")) { |
| String[] parts = line.split(" ", 2); |
| replyTo = parts[1]; |
| if (replyTo.startsWith("<")) { |
| replyTo = replyTo.substring(1,replyTo.length()-1); // drop <> wrapper |
| } else { |
| System.err.println("Unexpected Return-path:" + line+ " in " + firstLine); |
| } |
| break; |
| } |
| } |
| try { |
| // Add initial mbox header line |
| bw.append("From "); |
| bw.append(replyTo); |
| bw.append(' '); |
| bw.append(DATE_FORMAT.format(received)); |
| bw.append(eol); |
| // Debug |
| bw.append("X-IMAP-Response: ").append(firstLine).append(eol); |
| if (printMarker) { |
| System.err.println("[" + total + "] " + firstLine); |
| } |
| // Skip first and last lines |
| for(int i=1; i< replyStrings.length - 1; i++) { |
| final String line = replyStrings[i]; |
| if (startsWith(line, PATFROM)) { |
| bw.append('>'); // Escape a From_ line |
| } |
| bw.append(line); |
| bw.append(eol); |
| } |
| // The last line ends with the trailing closing ")" which needs to be stripped |
| String lastLine = replyStrings[replyStrings.length-1]; |
| final int lastLength = lastLine.length(); |
| if (lastLength > 1) { // there's some content, we need to save it |
| bw.append(lastLine, 0, lastLength-1); |
| bw.append(eol); |
| } |
| bw.append(eol); // blank line between entries |
| } catch (IOException e) { |
| e.printStackTrace(); |
| throw new RuntimeException(e); // chunkReceived cannot throw a checked Exception |
| } |
| lastFetched = firstLine; |
| total++; |
| if (checkSequence) { |
| m = PATSEQ.matcher(firstLine); |
| if (m.lookingAt()) { // found a match |
| final long msgSeq = Long.parseLong(m.group(PATSEQ_SEQUENCE_GROUP)); // Cannot fail to parse |
| if (lastSeq != -1) { |
| long missing = msgSeq - lastSeq - 1; |
| if (missing != 0) { |
| for(long j = lastSeq + 1; j < msgSeq; j++) { |
| missingIds.add(String.valueOf(j)); |
| } |
| System.err.println( |
| "*** Sequence error: current=" + msgSeq + " previous=" + lastSeq + " Missing=" + missing); |
| } |
| } |
| lastSeq = msgSeq; |
| } |
| } |
| if (printHash) { |
| System.err.print("."); |
| } |
| return true; |
| } |
| |
| public void close() throws IOException { |
| if (bw != null) { |
| bw.close(); |
| } |
| } |
| } |
| } |