001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017
018package examples.mail;
019
020import java.io.BufferedReader;
021import java.io.File;
022import java.io.FileReader;
023import java.io.IOException;
024import java.net.URI;
025import java.util.ArrayList;
026import java.util.BitSet;
027import java.util.List;
028import java.util.regex.Matcher;
029import java.util.regex.Pattern;
030
031import org.apache.commons.net.imap.IMAPClient;
032
033/**
034 * This is an example program demonstrating how to use the IMAP[S]Client class.
035 * This program connects to a IMAP[S] server and imports messages into the folder from an mbox file.
036 * <p>
037 * Usage: IMAPImportMbox imap[s]://user:password@host[:port]/folder/path <mboxfile> [selectors]
038 * <p>
039 * An example selector might be:
040 * <ul>
041 * <li>1,2,3,7-10</li>
042 * <li>-142986- : this is useful for retrieving messages by apmail number, which appears as From xyz-return-142986-apmail-...</li>
043 * </ul>
044 * <p>
045 * For example:<br>
046 * IMAPImportMbox imaps://user:pass@imap.googlemail.com/imported_messages 201401.mbox 1-10,20 -142986-
047 */
048public final class IMAPImportMbox
049{
050
051    private static final String CRLF = "\r\n";
052    private static final Pattern PATFROM = Pattern.compile(">+From "); // escaped From
053
054    public static void main(String[] args) throws IOException
055    {
056        if (args.length < 2)
057        {
058            System.err.println("Usage: IMAPImportMbox imap[s]://user:password@host[:port]/folder/path <mboxfile> [selectors]");
059            System.err.println("\tWhere: a selector is a list of numbers/number ranges - 1,2,3-10" +
060                               " - or a list of strings to match in the initial From line");
061            System.exit(1);
062        }
063
064        final URI uri      = URI.create(args[0]);
065        final String file  = args[1];
066
067        final File mbox = new File(file);
068        if (!mbox.isFile() || !mbox.canRead()) {
069            throw new IOException("Cannot read mailbox file: " + mbox);
070        }
071
072        String path = uri.getPath();
073        if (path == null || path.length() < 1) {
074            throw new IllegalArgumentException("Invalid folderPath: '" + path + "'");
075        }
076        String folder = path.substring(1); // skip the leading /
077
078        List<String> contains = new ArrayList<String>(); // list of strings to find
079        BitSet msgNums = new BitSet(); // list of message numbers
080
081        for(int i = 2; i < args.length; i++) {
082            String arg = args[i];
083            if (arg.matches("\\d+(-\\d+)?(,\\d+(-\\d+)?)*")) { // number,m-n
084                for(String entry : arg.split(",")) {
085                    String []parts = entry.split("-");
086                    if (parts.length == 2) { // m-n
087                        int low = Integer.parseInt(parts[0]);
088                        int high = Integer.parseInt(parts[1]);
089                        for(int j=low; j <= high; j++) {
090                            msgNums.set(j);
091                        }
092                    } else {
093                        msgNums.set(Integer.parseInt(entry));
094                    }
095                }
096            } else {
097                contains.add(arg); // not a number/number range
098            }
099        }
100//        System.out.println(msgNums.toString());
101//        System.out.println(java.util.Arrays.toString(contains.toArray()));
102
103        // Connect and login
104        final IMAPClient imap = IMAPUtils.imapLogin(uri, 10000, null);
105
106        int total = 0;
107        int loaded = 0;
108        try {
109            imap.setSoTimeout(6000);
110
111            final BufferedReader br = new BufferedReader(new FileReader(file)); // TODO charset?
112
113            String line;
114            StringBuilder sb = new StringBuilder();
115            boolean wanted = false; // Skip any leading rubbish
116            while((line=br.readLine())!=null) {
117                if (line.startsWith("From ")) { // start of message; i.e. end of previous (if any)
118                    if (process(sb, imap, folder, total)) { // process previous message (if any)
119                        loaded++;
120                    }
121                    sb.setLength(0);
122                    total ++;
123                    wanted = wanted(total, line, msgNums, contains);
124                } else if (startsWith(line, PATFROM)) { // Unescape ">+From " in body text
125                    line = line.substring(1);
126                }
127                // TODO process first Received: line to determine arrival date?
128                if (wanted) {
129                    sb.append(line);
130                    sb.append(CRLF);
131                }
132            }
133            br.close();
134            if (wanted && process(sb, imap, folder, total)) { // last message (if any)
135                loaded++;
136            }
137        } catch (IOException e) {
138            System.out.println(imap.getReplyString());
139            e.printStackTrace();
140            System.exit(10);
141            return;
142        } finally {
143            imap.logout();
144            imap.disconnect();
145        }
146        System.out.println("Processed " + total + " messages, loaded " + loaded);
147    }
148
149    private static boolean startsWith(String input, Pattern pat) {
150        Matcher m = pat.matcher(input);
151        return m.lookingAt();
152    }
153
154    private static boolean process(final StringBuilder sb, final IMAPClient imap, final String folder
155            ,final int msgNum) throws IOException {
156        final int length = sb.length();
157        boolean haveMessage = length > 2;
158        if (haveMessage) {
159            System.out.println("MsgNum: " + msgNum +" Length " + length);
160            sb.setLength(length-2); // drop trailing CRLF
161            String msg = sb.toString();
162            if (!imap.append(folder, null, null, msg)) {
163                throw new IOException("Failed to import message: " + msgNum + " " + imap.getReplyString());
164            }
165        }
166        return haveMessage;
167    }
168
169    /**
170     * Is the message wanted?
171     *
172     * @param msgNum the message number
173     * @param line the From line
174     * @param msgNums the list of wanted message numbers
175     * @param contains the list of strings to be contained
176     * @return true if the message is wanted
177     */
178    private static boolean wanted(int msgNum, String line, BitSet msgNums, List<String> contains) {
179        return (msgNums.isEmpty() && contains.isEmpty()) // no selectors
180             || msgNums.get(msgNum) // matches message number
181             || listContains(contains, line); // contains string
182    }
183
184    /**
185     * Is at least one entry in the list contained in the string?
186     * @param contains the list of strings to look for
187     * @param string the String to check against
188     * @return true if at least one entry in the contains list is contained in the string
189     */
190    private static boolean listContains(List<String> contains, String string) {
191        for(String entry : contains) {
192            if (string.contains(entry)) {
193                return true;
194            }
195        }
196        return false;
197    }
198
199}