001/****************************************************************
002 * Licensed to the Apache Software Foundation (ASF) under one   *
003 * or more contributor license agreements.  See the NOTICE file *
004 * distributed with this work for additional information        *
005 * regarding copyright ownership.  The ASF licenses this file   *
006 * to you under the Apache License, Version 2.0 (the            *
007 * "License"); you may not use this file except in compliance   *
008 * with the License.  You may obtain a copy of the License at   *
009 *                                                              *
010 *   http://www.apache.org/licenses/LICENSE-2.0                 *
011 *                                                              *
012 * Unless required by applicable law or agreed to in writing,   *
013 * software distributed under the License is distributed on an  *
014 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY       *
015 * KIND, either express or implied.  See the License for the    *
016 * specific language governing permissions and limitations      *
017 * under the License.                                           *
018 ****************************************************************/
019
020package org.apache.james.mpt.helper;
021
022import java.io.IOException;
023import java.io.InputStream;
024import java.net.InetSocketAddress;
025import java.nio.ByteBuffer;
026import java.nio.CharBuffer;
027import java.nio.channels.ReadableByteChannel;
028import java.nio.channels.SocketChannel;
029import java.nio.channels.WritableByteChannel;
030import java.nio.charset.Charset;
031import java.util.Locale;
032
033import org.apache.commons.lang.StringUtils;
034
035public class ScriptBuilder {
036
037    public static ScriptBuilder open(String host, int port) throws Exception {
038        InetSocketAddress address = new InetSocketAddress(host, port);
039        SocketChannel socket = SocketChannel.open(address);
040        socket.configureBlocking(false);
041        Client client = new Client(socket, socket);
042        return new ScriptBuilder(client);
043    }
044
045    private int tagCount = 0;
046
047    private boolean uidSearch = false;
048
049    private boolean peek = false;
050
051    private int messageNumber = 1;
052
053    private String user = "imapuser";
054
055    private String password = "password";
056
057    private String mailbox = "testmailbox";
058
059    private String file = "rfc822.mail";
060
061    private String basedir = "/org/apache/james/imap/samples/";
062
063    private boolean createdMailbox = false;
064
065    private final Client client;
066
067    private Fetch fetch = new Fetch();
068
069    private Search search = new Search();
070
071    private String partialFetch = "";
072
073    public ScriptBuilder(Client client) {
074        super();
075        this.client = client;
076    }
077
078    public final boolean isPeek() {
079        return peek;
080    }
081
082    public final void setPeek(boolean peek) {
083        this.peek = peek;
084    }
085
086    public final boolean isUidSearch() {
087        return uidSearch;
088    }
089
090    public final void setUidSearch(boolean uidSearch) {
091        this.uidSearch = uidSearch;
092    }
093
094    public final String getBasedir() {
095        return basedir;
096    }
097
098    public final void setBasedir(String basedir) {
099        this.basedir = basedir;
100    }
101
102    public final String getFile() {
103        return file;
104    }
105
106    public final void setFile(String file) {
107        this.file = file;
108    }
109
110    private InputStream openFile() throws Exception {
111        InputStream result = this.getClass()
112                .getResourceAsStream(basedir + file);
113        return new IgnoreHeaderInputStream(result);
114    }
115
116    public final Fetch getFetch() {
117        return fetch;
118    }
119
120    public final void setFetch(Fetch fetch) {
121        this.fetch = fetch;
122    }
123
124    public final Fetch resetFetch() {
125        this.fetch = new Fetch();
126        return fetch;
127    }
128
129    public final int getMessageNumber() {
130        return messageNumber;
131    }
132
133    public final void setMessageNumber(int messageNumber) {
134        this.messageNumber = messageNumber;
135    }
136
137    public final String getMailbox() {
138        return mailbox;
139    }
140
141    public final ScriptBuilder setMailbox(String mailbox) {
142        this.mailbox = mailbox;
143        return this;
144    }
145
146    public final String getPassword() {
147        return password;
148    }
149
150    public final void setPassword(String password) {
151        this.password = password;
152    }
153
154    public final String getUser() {
155        return user;
156    }
157
158    public final void setUser(String user) {
159        this.user = user;
160    }
161
162    public void login() throws Exception {
163        command("LOGIN " + user + " " + password);
164    }
165
166    private void command(String command) throws Exception {
167        tag();
168        write(command);
169        lineEnd();
170        response();
171    }
172
173    public ScriptBuilder rename(String to) throws Exception {
174        return rename(getMailbox(), to);
175    }
176
177    public ScriptBuilder rename(String from, String to) throws Exception {
178        command("RENAME " + from + " " + to);
179        return this;
180    }
181
182    public ScriptBuilder select() throws Exception {
183        command("SELECT " + mailbox);
184        return this;
185    }
186
187    public ScriptBuilder create() throws Exception {
188        command("CREATE " + mailbox);
189        createdMailbox = true;
190        return this;
191    }
192
193    public ScriptBuilder flagDeleted() throws Exception {
194        return flagDeleted(messageNumber);
195    }
196    
197    public ScriptBuilder flagDeleted(int messageNumber) throws Exception {
198        store(new Flags().deleted().msn(messageNumber));
199        return this;
200    }
201
202    public ScriptBuilder expunge() throws Exception {
203        command("EXPUNGE");
204        return this;
205    }
206
207    public void delete() throws Exception {
208        if (createdMailbox) {
209            command("DELETE " + mailbox);
210        }
211    }
212
213    public void search() throws Exception {
214        search.setUidSearch(uidSearch);
215        command(search.command());
216        search = new Search();
217    }
218
219    public ScriptBuilder all() {
220        search.all();
221        return this;
222    }
223
224    public ScriptBuilder answered() {
225        search.answered();
226        return this;
227    }
228
229    public ScriptBuilder bcc(String address) {
230        search.bcc(address);
231        return this;
232    }
233
234    public ScriptBuilder before(int year, int month, int day) {
235        search.before(year, month, day);
236        return this;
237    }
238
239    public ScriptBuilder body(String text) {
240        search.body(text);
241        return this;
242    }
243
244    public ScriptBuilder cc(String address) {
245        search.cc(address);
246        return this;
247    }
248
249    public ScriptBuilder deleted() {
250        search.deleted();
251        return this;
252    }
253
254    public ScriptBuilder draft() {
255        search.draft();
256        return this;
257    }
258
259    public ScriptBuilder flagged() {
260        search.flagged();
261        return this;
262    }
263
264    public ScriptBuilder from(String address) {
265        search.from(address);
266        return this;
267    }
268
269    public ScriptBuilder header(String field, String value) {
270        search.header(field, value);
271        return this;
272    }
273
274    public ScriptBuilder keyword(String flag) {
275        search.keyword(flag);
276        return this;
277    }
278
279    public ScriptBuilder larger(long size) {
280        search.larger(size);
281        return this;
282    }
283
284    public ScriptBuilder NEW() {
285        search.NEW();
286        return this;
287    }
288
289    public ScriptBuilder not() {
290        search.not();
291        return this;
292    }
293
294    public ScriptBuilder old() {
295        search.old();
296        return this;
297    }
298
299    public ScriptBuilder on(int year, int month, int day) {
300        search.on(year, month, day);
301        return this;
302    }
303
304    public ScriptBuilder or() {
305        search.or();
306        return this;
307    }
308
309    public ScriptBuilder recent() {
310        search.recent();
311        return this;
312    }
313
314    public ScriptBuilder seen() {
315        search.seen();
316        return this;
317    }
318
319    public ScriptBuilder sentbefore(int year, int month, int day) {
320        search.sentbefore(year, month, day);
321        return this;
322    }
323
324    public ScriptBuilder senton(int year, int month, int day) {
325        search.senton(year, month, day);
326        return this;
327    }
328
329    public ScriptBuilder sentsince(int year, int month, int day) {
330        search.sentsince(year, month, day);
331        return this;
332    }
333
334    public ScriptBuilder since(int year, int month, int day) {
335        search.since(year, month, day);
336        return this;
337    }
338
339    public ScriptBuilder smaller(int size) {
340        search.smaller(size);
341        return this;
342    }
343
344    public ScriptBuilder subject(String address) {
345        search.subject(address);
346        return this;
347    }
348
349    public ScriptBuilder text(String text) {
350        search.text(text);
351        return this;
352    }
353
354    public ScriptBuilder to(String address) {
355        search.to(address);
356        return this;
357    }
358
359    public ScriptBuilder uid() {
360        search.uid();
361        return this;
362    }
363
364    public ScriptBuilder unanswered() {
365        search.unanswered();
366        return this;
367    }
368
369    public ScriptBuilder undeleted() {
370        search.undeleted();
371        return this;
372    }
373
374    public ScriptBuilder undraft() {
375        search.undraft();
376        return this;
377    }
378
379    public ScriptBuilder unflagged() {
380        search.unflagged();
381        return this;
382    }
383
384    public ScriptBuilder unkeyword(String flag) {
385        search.unkeyword(flag);
386        return this;
387    }
388
389    public ScriptBuilder unseen() {
390        search.unseen();
391        return this;
392    }
393
394    public ScriptBuilder openParen() {
395        search.openParen();
396        return this;
397    }
398
399    public ScriptBuilder closeParen() {
400        search.closeParen();
401        return this;
402    }
403
404    public ScriptBuilder msn(int low, int high) {
405        search.msn(low, high);
406        return this;
407    }
408
409    public ScriptBuilder msnAndUp(int limit) {
410        search.msnAndUp(limit);
411        return this;
412    }
413
414    public ScriptBuilder msnAndDown(int limit) {
415        search.msnAndDown(limit);
416        return this;
417    }
418
419    public Flags flags() {
420        return new Flags();
421    }
422
423    public void store(Flags flags) throws Exception {
424        String command = flags.command();
425        command(command);
426    }
427
428    public Search getSearch() throws Exception {
429        return search;
430    }
431
432    public ScriptBuilder partial(long start, long octets) {
433        partialFetch = "<" + start + "." + octets + ">";
434        return this;
435    }
436
437    public ScriptBuilder fetchSection(String section) throws Exception {
438        StringBuffer command = new StringBuffer("FETCH ");
439        command.append(messageNumber);
440        if (peek) {
441            command.append(" (BODY.PEEK[");
442        } else {
443            command.append(" (BODY[");
444        }
445        command.append(section).append("]").append(partialFetch).append(")");
446        command(command.toString());
447        return this;
448    }
449
450    public void fetchAllMessages() throws Exception {
451        final String command = fetch.command();
452        command(command);
453    }
454
455    public ScriptBuilder list() throws Exception {
456        command("LIST \"\" \"*\"");
457        return this;
458    }
459
460    public void fetchBody() throws Exception {
461
462    }
463
464    public void fetch() throws Exception {
465        final String command = fetch.command(messageNumber);
466        command(command);
467    }
468
469    public void fetchFlags() throws Exception {
470        final String command = "FETCH " + messageNumber + " (FLAGS)";
471        command(command);
472    }
473
474    public void append() throws Exception {
475        tag();
476        write("APPEND " + mailbox);
477        write(openFile());
478        lineEnd();
479        response();
480    }
481
482    private void write(InputStream in) throws Exception {
483        client.write(in);
484    }
485
486    private void response() throws Exception {
487        client.readResponse();
488    }
489
490    private void tag() throws Exception {
491        client.lineStart();
492        write("A" + ++tagCount + " ");
493    }
494
495    private void lineEnd() throws Exception {
496        client.lineEnd();
497    }
498
499    private void write(String phrase) throws Exception {
500        client.write(phrase);
501    }
502
503    public void close() throws Exception {
504        client.close();
505    }
506
507    public void logout() throws Exception {
508        command("LOGOUT");
509    }
510
511    public void quit() throws Exception {
512        delete();
513        logout();
514        close();
515    }
516
517    public static final class Flags {
518        private final StringBuffer flags;
519
520        private final StringBuffer msn;
521
522        private boolean first;
523
524        private boolean silent;
525
526        private boolean add;
527
528        private boolean subtract;
529
530        public Flags() {
531            add = false;
532            subtract = false;
533            silent = false;
534            first = true;
535            flags = new StringBuffer("(");
536            msn = new StringBuffer();
537        }
538
539        public Flags msn(long number) {
540            msn.append(number);
541            msn.append(' ');
542            return this;
543        }
544
545        public Flags range(long low, long high) {
546            msn.append(low);
547            msn.append(':');
548            msn.append(high);
549            msn.append(' ');
550            return this;
551        }
552
553        public Flags rangeTill(long number) {
554            msn.append("*:");
555            msn.append(number);
556            msn.append(' ');
557            return this;
558        }
559
560        public Flags rangeFrom(long number) {
561            msn.append(number);
562            msn.append(":* ");
563            return this;
564        }
565
566        public Flags add() {
567            add = true;
568            subtract = false;
569            return this;
570        }
571
572        public Flags subtract() {
573            add = false;
574            subtract = true;
575            return this;
576        }
577
578        public Flags silent() {
579            silent = true;
580            return this;
581        }
582
583        public Flags deleted() {
584            return append("\\DELETED");
585        }
586
587        public Flags flagged() {
588            return append("\\FLAGGED");
589        }
590
591        public Flags answered() {
592            return append("\\ANSWERED");
593        }
594
595        public Flags seen() {
596            return append("\\SEEN");
597        }
598
599        public Flags draft() {
600            return append("\\DRAFT");
601        }
602
603        public String command() {
604            String flags;
605            if (add) {
606                flags = " +FLAGS ";
607            } else if (subtract) {
608                flags = " -FLAGS ";
609            } else {
610                flags = " FLAGS ";
611            }
612            if (silent) {
613                flags = flags + ".SILENT";
614            }
615            return "STORE " + msn + flags + this.flags + ")";
616        }
617
618        private Flags append(String term) {
619            if (first) {
620                first = false;
621            } else {
622                flags.append(' ');
623            }
624            flags.append(term);
625            return this;
626        }
627    }
628
629    public static final class Search {
630
631        private StringBuffer buffer;
632
633        private boolean first;
634
635        private boolean uidSearch = false;
636
637        public Search() {
638            clear();
639        }
640
641        public boolean isUidSearch() {
642            return uidSearch;
643        }
644
645        public void setUidSearch(boolean uidSearch) {
646            this.uidSearch = uidSearch;
647        }
648
649        public String command() {
650            if (uidSearch) {
651                return buffer.insert(0, "UID SEARCH ").toString();
652            } else {
653                return buffer.insert(0, "SEARCH ").toString();
654            }
655        }
656
657        public void clear() {
658            buffer = new StringBuffer();
659            first = true;
660        }
661
662        private Search append(long term) {
663            return append(Long.valueOf(term).toString());
664        }
665
666        private Search append(String term) {
667            if (first) {
668                first = false;
669            } else {
670                buffer.append(' ');
671            }
672            buffer.append(term);
673            return this;
674        }
675
676        private Search date(int year, int month, int day) {
677            append(day);
678            switch (month) {
679                case 1:
680                    buffer.append("-Jan-");
681                    break;
682                case 2:
683                    buffer.append("-Feb-");
684                    break;
685                case 3:
686                    buffer.append("-Mar-");
687                    break;
688                case 4:
689                    buffer.append("-Apr-");
690                    break;
691                case 5:
692                    buffer.append("-May-");
693                    break;
694                case 6:
695                    buffer.append("-Jun-");
696                    break;
697                case 7:
698                    buffer.append("-Jul-");
699                    break;
700                case 8:
701                    buffer.append("-Aug-");
702                    break;
703                case 9:
704                    buffer.append("-Sep-");
705                    break;
706                case 10:
707                    buffer.append("-Oct-");
708                    break;
709                case 11:
710                    buffer.append("-Nov-");
711                    break;
712                case 12:
713                    buffer.append("-Dec-");
714                    break;
715            }
716            buffer.append(year);
717            return this;
718        }
719
720        public Search all() {
721            return append("ALL");
722        }
723
724        public Search answered() {
725            return append("ANSWERED");
726        }
727
728        public Search bcc(String address) {
729            return append("BCC " + address);
730        }
731
732        public Search before(int year, int month, int day) {
733            return append("BEFORE").date(year, month, day);
734        }
735
736        public Search body(String text) {
737            return append("BODY").append(text);
738        }
739
740        public Search cc(String address) {
741            return append("CC").append(address);
742        }
743
744        public Search deleted() {
745            return append("DELETED");
746        }
747
748        public Search draft() {
749            return append("DRAFT");
750        }
751
752        public Search flagged() {
753            return append("FLAGGED");
754        }
755
756        public Search from(String address) {
757            return append("FROM").append(address);
758        }
759
760        public Search header(String field, String value) {
761            return append("HEADER").append(field).append(value);
762        }
763
764        public Search keyword(String flag) {
765            return append("KEYWORD").append(flag);
766        }
767
768        public Search larger(long size) {
769            return append("LARGER").append(size);
770        }
771
772        public Search NEW() {
773            return append("NEW");
774        }
775
776        public Search not() {
777            return append("NOT");
778        }
779
780        public Search old() {
781            return append("OLD");
782        }
783
784        public Search on(int year, int month, int day) {
785            return append("ON").date(year, month, day);
786        }
787
788        public Search or() {
789            return append("OR");
790        }
791
792        public Search recent() {
793            return append("RECENT");
794        }
795
796        public Search seen() {
797            return append("SEEN");
798        }
799
800        public Search sentbefore(int year, int month, int day) {
801            return append("SENTBEFORE").date(year, month, day);
802        }
803
804        public Search senton(int year, int month, int day) {
805            return append("SENTON").date(year, month, day);
806        }
807
808        public Search sentsince(int year, int month, int day) {
809            return append("SENTSINCE").date(year, month, day);
810        }
811
812        public Search since(int year, int month, int day) {
813            return append("SINCE").date(year, month, day);
814        }
815
816        public Search smaller(int size) {
817            return append("SMALLER").append(size);
818        }
819
820        public Search subject(String address) {
821            return append("SUBJECT").append(address);
822        }
823
824        public Search text(String text) {
825            return append("TEXT").append(text);
826        }
827
828        public Search to(String address) {
829            return append("TO").append(address);
830        }
831
832        public Search uid() {
833            return append("UID");
834        }
835
836        public Search unanswered() {
837            return append("UNANSWERED");
838        }
839
840        public Search undeleted() {
841            return append("UNDELETED");
842        }
843
844        public Search undraft() {
845            return append("UNDRAFT");
846        }
847
848        public Search unflagged() {
849            return append("UNFLAGGED");
850        }
851
852        public Search unkeyword(String flag) {
853            return append("UNKEYWORD").append(flag);
854        }
855
856        public Search unseen() {
857            return append("UNSEEN");
858        }
859
860        public Search openParen() {
861            return append("(");
862        }
863
864        public Search closeParen() {
865            return append(")");
866        }
867
868        public Search msn(int low, int high) {
869            return append(low + ":" + high);
870        }
871
872        public Search msnAndUp(int limit) {
873            return append(limit + ":*");
874        }
875
876        public Search msnAndDown(int limit) {
877            return append("*:" + limit);
878        }
879    }
880
881    public static final class Fetch {
882
883        static final String[] COMPREHENSIVE_HEADERS = { "DATE", "FROM",
884                "TO", "CC", "SUBJECT", "REFERENCES", "IN-REPLY-TO",
885                "MESSAGE-ID", "MIME-VERSION", "CONTENT-TYPE", "X-MAILING-LIST",
886                "X-LOOP", "LIST-ID", "LIST-POST", "MAILING-LIST", "ORIGINATOR",
887                "X-LIST", "SENDER", "RETURN-PATH", "X-BEENTHERE" };
888
889        static final String[] SELECT_HEADERS = { "DATE", "FROM", "TO",
890                "ORIGINATOR", "X-LIST" };
891
892        private boolean flagsFetch = false;
893
894        private boolean rfc822Size = false;
895
896        private boolean rfc = false;
897
898        private boolean rfcText = false;
899
900        private boolean rfcHeaders = false;
901
902        private boolean internalDate = false;
903
904        private boolean uid = false;
905
906        private String body = null;
907
908        private boolean bodyFetch = false;
909
910        private boolean bodyStructureFetch = false;
911
912        public boolean isBodyFetch() {
913            return bodyFetch;
914        }
915
916        public Fetch setBodyFetch(boolean bodyFetch) {
917            this.bodyFetch = bodyFetch;
918            return this;
919        }
920
921        public boolean isBodyStructureFetch() {
922            return bodyStructureFetch;
923        }
924
925        public Fetch setBodyStructureFetch(boolean bodyStructureFetch) {
926            this.bodyStructureFetch = bodyStructureFetch;
927            return this;
928        }
929
930        public String command(int messageNumber) {
931            return "FETCH " + messageNumber + "(" + fetchData() + ")";
932        }
933
934        public String command() {
935            return "FETCH 1:* (" + fetchData() + ")";
936        }
937
938        public boolean isFlagsFetch() {
939            return flagsFetch;
940        }
941
942        public Fetch setFlagsFetch(boolean flagsFetch) {
943            this.flagsFetch = flagsFetch;
944            return this;
945        }
946
947        public boolean isUid() {
948            return uid;
949        }
950
951        public Fetch setUid(boolean uid) {
952            this.uid = uid;
953            return this;
954        }
955
956        public boolean isRfc822Size() {
957            return rfc822Size;
958        }
959
960        public Fetch setRfc822Size(boolean rfc822Size) {
961            this.rfc822Size = rfc822Size;
962            return this;
963        }
964
965        public boolean isRfc() {
966            return rfc;
967        }
968
969        public Fetch setRfc(boolean rfc) {
970            this.rfc = rfc;
971            return this;
972        }
973
974        public boolean isRfcHeaders() {
975            return rfcHeaders;
976        }
977
978        public Fetch setRfcHeaders(boolean rfcHeaders) {
979            this.rfcHeaders = rfcHeaders;
980            return this;
981        }
982
983        public boolean isRfcText() {
984            return rfcText;
985        }
986
987        public Fetch setRfcText(boolean rfcText) {
988            this.rfcText = rfcText;
989            return this;
990        }
991
992        public boolean isInternalDate() {
993            return internalDate;
994        }
995
996        public Fetch setInternalDate(boolean internalDate) {
997            this.internalDate = internalDate;
998            return this;
999        }
1000
1001        public String getBody() {
1002            return body;
1003        }
1004
1005        public void setBody(String bodyPeek) {
1006            this.body = bodyPeek;
1007        }
1008
1009        public void bodyPeekCompleteMessage() {
1010            setBody(buildBody(true, ""));
1011        }
1012
1013        public void bodyPeekNotHeaders(String[] fields) {
1014            setBody(buildBody(true, buildHeaderFields(fields, true)));
1015        }
1016
1017        public Fetch bodyPeekHeaders(String[] fields) {
1018            setBody(buildBody(true, buildHeaderFields(fields, false)));
1019            return this;
1020        }
1021
1022        public String buildBody(boolean peek, String section) {
1023            StringBuffer result;
1024            if (peek) {
1025                result = new StringBuffer("BODY.PEEK[");
1026            } else {
1027                result = new StringBuffer("BODY[");
1028            }
1029            result.append(section).append("]");
1030            return result.toString();
1031        }
1032
1033        public String buildHeaderFields(String[] fields, boolean not) {
1034            StringBuffer result;
1035            if (not) {
1036                result = new StringBuffer("HEADER.FIELDS.NOT (");
1037            } else {
1038                result = new StringBuffer("HEADER.FIELDS (");
1039            }
1040            for (int i = 0; i < fields.length; i++) {
1041                if (i > 0) {
1042                    result.append(" ");
1043                }
1044                result.append(fields[i]);
1045            }
1046            result.append(")");
1047            return result.toString();
1048        }
1049
1050        public String fetchData() {
1051            final StringBuffer buffer = new StringBuffer();
1052            boolean first = true;
1053            if (flagsFetch) {
1054                first = add(buffer, first, "FLAGS");
1055            }
1056            if (rfc822Size) {
1057                first = add(buffer, first, "RFC822.SIZE");
1058            }
1059            if (rfc) {
1060                first = add(buffer, first, "RFC822");
1061            }
1062            if (rfcHeaders) {
1063                first = add(buffer, first, "RFC822.HEADER");
1064            }
1065            if (rfcText) {
1066                first = add(buffer, first, "RFC822.TEXT");
1067            }
1068            if (internalDate) {
1069                first = add(buffer, first, "INTERNALDATE");
1070            }
1071            if (uid) {
1072                first = add(buffer, first, "UID");
1073            }
1074            if (bodyFetch) {
1075                first = add(buffer, first, "BODY");
1076            }
1077            if (bodyStructureFetch) {
1078                first = add(buffer, first, "BODYSTRUCTURE");
1079            }
1080            add(buffer, first, body);
1081            return buffer.toString();
1082        }
1083
1084        private boolean add(StringBuffer buffer, boolean first,
1085                String atom) {
1086            if (atom != null) {
1087                if (first) {
1088                    first = false;
1089                } else {
1090                    buffer.append(" ");
1091                }
1092                buffer.append(atom);
1093            }
1094            return first;
1095        }
1096    }
1097
1098    public static final class Client {
1099
1100        private static final Charset ASCII = Charset.forName("us-ascii");
1101
1102        private final Out out;
1103
1104        private final ReadableByteChannel source;
1105
1106        private final WritableByteChannel sump;
1107
1108        private final ByteBuffer inBuffer = ByteBuffer.allocate(256);
1109
1110        private final ByteBuffer outBuffer = ByteBuffer.allocate(262144);
1111
1112        private final ByteBuffer crlf;
1113
1114        private boolean isLineTagged = false;
1115
1116        private int continuationBytes = 0;
1117
1118        public Client(ReadableByteChannel source, WritableByteChannel sump)
1119                throws Exception {
1120            super();
1121            this.source = source;
1122            this.sump = sump;
1123            this.out = new Out();
1124            byte[] crlf = { '\r', '\n' };
1125            this.crlf = ByteBuffer.wrap(crlf);
1126            inBuffer.flip();
1127            readLine();
1128        }
1129
1130        public void write(InputStream in) throws Exception {
1131            outBuffer.clear();
1132            int next = in.read();
1133            while (next != -1) {
1134                if (next == '\n') {
1135                    outBufferNext((byte) '\r');
1136                    outBufferNext((byte) '\n');
1137                } else if (next == '\r') {
1138                    outBufferNext((byte) '\r');
1139                    outBufferNext((byte) '\n');
1140                    next = in.read();
1141                    if (next == '\n') {
1142                        next = in.read();
1143                    } else if (next != -1) {
1144                        outBufferNext((byte) next);
1145                    }
1146                } else {
1147                    outBufferNext((byte) next);
1148                }
1149                next = in.read();
1150            }
1151
1152            writeOutBuffer();
1153        }
1154
1155        public void outBufferNext(byte next) throws Exception {
1156            outBuffer.put(next);
1157        }
1158
1159        private void writeOutBuffer() throws Exception {
1160            outBuffer.flip();
1161            int count = outBuffer.limit();
1162            String continuation = " {" + count + "+}";
1163            write(continuation);
1164            lineEnd();
1165            out.client();
1166            while (outBuffer.hasRemaining()) {
1167                final byte next = outBuffer.get();
1168                print(next);
1169                if (next == '\n') {
1170                    out.client();
1171                }
1172            }
1173            outBuffer.rewind();
1174            while (outBuffer.hasRemaining()) {
1175                sump.write(outBuffer);
1176            }
1177        }
1178
1179        public void readResponse() throws Exception {
1180            isLineTagged = false;
1181            while (!isLineTagged) {
1182                readLine();
1183            }
1184        }
1185
1186        private byte next() throws Exception {
1187            byte result;
1188            if (inBuffer.hasRemaining()) {
1189                result = inBuffer.get();
1190                print(result);
1191            } else {
1192                inBuffer.compact();
1193                int i = 0;
1194                while ((i = source.read(inBuffer)) == 0)
1195                    ;
1196                if (i == -1)
1197                    throw new RuntimeException("Unexpected EOF");
1198                inBuffer.flip();
1199                result = next();
1200            }
1201            return result;
1202        }
1203
1204        private void print(char next) {
1205            out.print(next);
1206        }
1207
1208        private void print(byte next) {
1209            print((char) next);
1210        }
1211
1212        public void lineStart() throws Exception {
1213            out.client();
1214        }
1215
1216        public void write(String phrase) throws Exception {
1217            out.print(phrase);
1218            final ByteBuffer buffer = ASCII.encode(phrase);
1219            writeRemaining(buffer);
1220        }
1221
1222        public void writeLine(String line) throws Exception {
1223            lineStart();
1224            write(line);
1225            lineEnd();
1226        }
1227
1228        private void writeRemaining(ByteBuffer buffer) throws IOException {
1229            while (buffer.hasRemaining()) {
1230                sump.write(buffer);
1231            }
1232        }
1233
1234        public void lineEnd() throws Exception {
1235            out.lineEnd();
1236            crlf.rewind();
1237            writeRemaining(crlf);
1238        }
1239
1240        private void readLine() throws Exception {
1241            out.server();
1242
1243            final byte next = next();
1244            isLineTagged = next != '*';
1245            readRestOfLine(next);
1246        }
1247
1248        private void readRestOfLine(byte next) throws Exception {
1249            while (next != '\r') {
1250                if (next == '{') {
1251                    startContinuation();
1252                }
1253                next = next();
1254            }
1255            next();
1256        }
1257
1258        private void startContinuation() throws Exception {
1259            continuationBytes = 0;
1260            continuation();
1261        }
1262
1263        private void continuation() throws Exception {
1264            byte next = next();
1265            switch (next) {
1266                case '0':
1267                    continuationDigit(0);
1268                    break;
1269                case '1':
1270                    continuationDigit(1);
1271                    break;
1272                case '2':
1273                    continuationDigit(2);
1274                    break;
1275                case '3':
1276                    continuationDigit(3);
1277                    break;
1278                case '4':
1279                    continuationDigit(4);
1280                    break;
1281                case '5':
1282                    continuationDigit(5);
1283                    break;
1284                case '6':
1285                    continuationDigit(6);
1286                    break;
1287                case '7':
1288                    continuationDigit(7);
1289                    break;
1290                case '8':
1291                    continuationDigit(8);
1292                    break;
1293                case '9':
1294                    continuationDigit(9);
1295                    break;
1296                case '+':
1297                    next();
1298                    next();
1299                    readContinuation();
1300                    break;
1301                default:
1302                    next();
1303                    next();
1304                    readContinuation();
1305                    break;
1306            }
1307        }
1308
1309        private void readContinuation() throws Exception {
1310            out.server();
1311            while (continuationBytes-- > 0) {
1312                int next = next();
1313                if (next == '\n') {
1314                    out.server();
1315                }
1316            }
1317        }
1318
1319        private void continuationDigit(int digit) throws Exception {
1320            continuationBytes = 10 * continuationBytes + digit;
1321            continuation();
1322        }
1323
1324        public void close() throws Exception {
1325            source.close();
1326            sump.close();
1327        }
1328    }
1329
1330    private static final class Out {
1331        private static final String OK_APPEND_COMPLETED = "OK APPEND completed.";
1332
1333        private static final String[] IGNORE_LINES_STARTING_WITH = {
1334                "S: \\* OK \\[PERMANENTFLAGS", "C: A22 LOGOUT",
1335                "S: \\* BYE Logging out", "S: \\* OK Dovecot ready\\." };
1336
1337        private static final String[] IGNORE_LINES_CONTAINING = {
1338                "OK Logout completed.", "LOGIN imapuser password",
1339                "OK Logged in", "LOGOUT" };
1340
1341        private final CharBuffer lineBuffer = CharBuffer.allocate(131072);
1342
1343        private boolean isClient = false;
1344
1345        public void client() {
1346            lineBuffer.put("C: ");
1347            isClient = true;
1348        }
1349
1350        public void print(char next) {
1351            if (!isClient) {
1352                escape(next);
1353            }
1354            lineBuffer.put(next);
1355        }
1356
1357        private void escape(char next) {
1358            if (next == '\\' || next == '*' || next == '.' || next == '['
1359                    || next == ']' || next == '+' || next == '(' || next == ')'
1360                    || next == '{' || next == '}' || next == '?') {
1361                lineBuffer.put('\\');
1362            }
1363        }
1364
1365        public void server() {
1366            lineBuffer.put("S: ");
1367            isClient = false;
1368        }
1369
1370        public void print(String phrase) {
1371            if (!isClient) {
1372                phrase = StringUtils.replace(phrase, "\\", "\\\\");
1373                phrase = StringUtils.replace(phrase, "*", "\\*");
1374                phrase = StringUtils.replace(phrase, ".", "\\.");
1375                phrase = StringUtils.replace(phrase, "[", "\\[");
1376                phrase = StringUtils.replace(phrase, "]", "\\]");
1377                phrase = StringUtils.replace(phrase, "+", "\\+");
1378                phrase = StringUtils.replace(phrase, "(", "\\(");
1379                phrase = StringUtils.replace(phrase, ")", "\\)");
1380                phrase = StringUtils.replace(phrase, "}", "\\}");
1381                phrase = StringUtils.replace(phrase, "{", "\\{");
1382                phrase = StringUtils.replace(phrase, "?", "\\?");
1383            }
1384            lineBuffer.put(phrase);
1385        }
1386
1387        public void lineEnd() {
1388            lineBuffer.flip();
1389            final String text = lineBuffer.toString();
1390            String[] lines = text.split("\r\n");
1391            for (String line : lines) {
1392                String chompedLine = StringUtils.chomp(line);
1393                if (!ignoreLine(chompedLine)) {
1394                    final String[] words = StringUtils.split(chompedLine);
1395                    if (words.length > 3 && "S:".equalsIgnoreCase(words[0]) && "OK".equalsIgnoreCase(words[2])) {
1396                        final int commandWordIndex;
1397                        if (words[3] == null || !words[3].startsWith("\\[")) {
1398                            commandWordIndex = 3;
1399                        } else {
1400                            int wordsCount = 3;
1401                            while (wordsCount < words.length) {
1402                                if (words[wordsCount++].endsWith("]")) {
1403                                    break;
1404                                }
1405                            }
1406                            commandWordIndex = wordsCount;
1407                        }
1408                        final String command = words[commandWordIndex];
1409                        final String commandOkPhrase;
1410                        if ("CREATE".equalsIgnoreCase(command)) {
1411                            commandOkPhrase = "OK CREATE completed.";
1412                        } else if ("FETCH".equalsIgnoreCase(command)) {
1413                            commandOkPhrase = "OK FETCH completed.";
1414                        } else if ("APPEND".equalsIgnoreCase(command)) {
1415                            commandOkPhrase = OK_APPEND_COMPLETED;
1416                        } else if ("DELETE".equalsIgnoreCase(command)) {
1417                            commandOkPhrase = "OK DELETE completed.";
1418                        } else if ("STORE".equalsIgnoreCase(command)) {
1419                            commandOkPhrase = "OK STORE completed.";
1420                        } else if ("RENAME".equalsIgnoreCase(command)) {
1421                            commandOkPhrase = "OK RENAME completed.";
1422                        } else if ("EXPUNGE".equalsIgnoreCase(command)) {
1423                            commandOkPhrase = "OK EXPUNGE completed.";
1424                        } else if ("LIST".equalsIgnoreCase(command)) {
1425                            commandOkPhrase = "OK LIST completed.";
1426                        } else if ("SELECT".equalsIgnoreCase(command)) {
1427                            if (commandWordIndex == 3) {
1428                                commandOkPhrase = "OK SELECT completed.";
1429                            } else {
1430                                commandOkPhrase = "OK " + words[3].toUpperCase(Locale.US) + " SELECT completed.";
1431                            }
1432                        } else {
1433                            commandOkPhrase = null;
1434                        }
1435                        if (commandOkPhrase != null) {
1436                            chompedLine = words[0] + " " + words[1] + " " + commandOkPhrase;
1437                        }
1438                    }
1439                    chompedLine = StringUtils.replace(chompedLine, "\\\\Seen \\\\Draft",
1440                        "\\\\Draft \\\\Seen");
1441                    chompedLine = StringUtils.replace(chompedLine, "\\\\Flagged \\\\Deleted",
1442                        "\\\\Deleted \\\\Flagged");
1443                    chompedLine = StringUtils.replace(chompedLine, "\\\\Flagged \\\\Draft",
1444                        "\\\\Draft \\\\Flagged");
1445                    chompedLine = StringUtils.replace(chompedLine, "\\\\Seen \\\\Recent",
1446                        "\\\\Recent \\\\Seen");
1447                    chompedLine = StringUtils.replace(chompedLine, "\\] First unseen\\.",
1448                        "\\](.)*");
1449                    if (chompedLine.startsWith("S: \\* OK \\[UIDVALIDITY ")) {
1450                        chompedLine = "S: \\* OK \\[UIDVALIDITY \\d+\\]";
1451                    } else if (chompedLine.startsWith("S: \\* OK \\[UIDNEXT")) {
1452                        chompedLine = "S: \\* OK \\[PERMANENTFLAGS \\(\\\\Answered \\\\Deleted \\\\Draft \\\\Flagged \\\\Seen\\)\\]";
1453                    }
1454
1455                    System.out.println(chompedLine);
1456                }
1457            }
1458            lineBuffer.clear();
1459        }
1460
1461        private boolean ignoreLine(String line) {
1462            boolean result = false;
1463            for (String entry : IGNORE_LINES_CONTAINING) {
1464                if (line.indexOf(entry) > 0) {
1465                    result = true;
1466                    break;
1467                }
1468            }
1469            for (int i = 0; i < IGNORE_LINES_STARTING_WITH.length && !result; i++) {
1470                if (line.startsWith(IGNORE_LINES_STARTING_WITH[i])) {
1471                    result = true;
1472                    break;
1473                }
1474            }
1475            return result;
1476        }
1477    }
1478
1479    private static final class IgnoreHeaderInputStream extends InputStream {
1480
1481        private boolean isFinishedHeaders = false;
1482
1483        private final InputStream delegate;
1484
1485        public IgnoreHeaderInputStream(InputStream delegate) {
1486            super();
1487            this.delegate = delegate;
1488        }
1489
1490        public int read() throws IOException {
1491            final int result;
1492            final int next = delegate.read();
1493            if (isFinishedHeaders) {
1494                result = next;
1495            } else {
1496                switch (next) {
1497                    case -1:
1498                        isFinishedHeaders = true;
1499                        result = next;
1500                        break;
1501                    case '#':
1502                        readLine();
1503                        result = read();
1504                        break;
1505
1506                    case '\r':
1507                    case '\n':
1508                    case ' ':
1509                    case '\t':
1510                        result = read();
1511                        break;
1512
1513                    default:
1514                        isFinishedHeaders = true;
1515                        result = next;
1516                        break;
1517                }
1518            }
1519            return result;
1520        }
1521
1522        private void readLine() throws IOException {
1523            int next = delegate.read();
1524            while (next != -1 && next != '\r' && next != '\n') {
1525                next = delegate.read();
1526            }
1527        }
1528    }
1529}