root/trunk/LogicMail/src/org/logicprobe/LogicMail/util/Connection.java

Revision 266, 19.0 kB (checked in by octorian, 3 weeks ago)

Initial IMAP IDLE implementation

  • Property svn:eol-style set to native
  • Property svn:keywords set to Author Date Id Revision
Line 
1/*-
2 * Copyright (c) 2006, Derek Konigsberg
3 * All rights reserved.
4 *
5 * Redistribution and use in source and binary forms, with or without
6 * modification, are permitted provided that the following conditions
7 * are met:
8 *
9 * 1. Redistributions of source code must retain the above copyright
10 *    notice, this list of conditions and the following disclaimer.
11 * 2. Redistributions in binary form must reproduce the above copyright
12 *    notice, this list of conditions and the following disclaimer in the
13 *    documentation and/or other materials provided with the distribution.
14 * 3. Neither the name of the project nor the names of its
15 *    contributors may be used to endorse or promote products derived
16 *    from this software without specific prior written permission.
17 *
18 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
19 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
20 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
21 * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
22 * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
23 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
24 * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
25 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
26 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
27 * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
28 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
29 * OF THE POSSIBILITY OF SUCH DAMAGE.
30 */
31
32/******************************************************************************
33 * This class has been modified to integrate J2ME socket connection code,
34 * since LogicMail has no need for a dual-use implementation.  The bulk of
35 * this code is a combination of Connection.java and the J2ME ConnectionImpl.java
36 * from Mail4ME, and is covered under the copyright shown below.  Any code not
37 * originating from Mail4ME is covered under the copyright shown at the top of
38 * this source file.
39 *
40 * Mail4ME - Mail for the Java 2 Micro Edition
41 *
42 * A lightweight, J2ME- (and also J2SE-) compatible package for sending and
43 * receiving Internet mail messages using the SMTP and POP3 protocols.
44 *
45 * Copyright (c) 2000-2002 Jorg Pleumann <joerg@pleumann.de>
46 *
47 * Mail4ME is part of the EnhydraME family of projects. See the following web
48 * sites for more information:
49 *
50 * -> http://mail4me.enhydra.org
51 * -> http://me.enhydra.org
52 *
53 * Mail4ME is distributed under the Enhydra Public License (EPL), which is
54 * discussed in great detail here:
55 *
56 * -> http://www.enhydra.org/software/license/index.html
57 *
58 * Have fun!
59 ******************************************************************************/
60package org.logicprobe.LogicMail.util;
61
62import java.io.IOException;
63import java.io.InputStream;
64import java.io.OutputStream;
65import java.util.Vector;
66import javax.microedition.io.SocketConnection;
67import javax.microedition.io.StreamConnection;
68import javax.microedition.io.Connector;
69import net.rim.device.api.system.EventLogger;
70import net.rim.device.api.ui.UiApplication;
71import net.rim.device.api.ui.component.Dialog;
72import org.logicprobe.LogicMail.AppInfo;
73import org.logicprobe.LogicMail.conf.GlobalConfig;
74import org.logicprobe.LogicMail.conf.MailSettings;
75
76/**
77 * Is the abstract base class for socket connections used inside the SMTP,
78 * POP3 and IMAP protocols of the mail package. The rationale for using this class
79 * is the difference in how networking is handled in J2ME and J2SE: While J2SE
80 * has java.net.Socket, J2ME uses the Generic Connection Framework with its
81 * javax.io.Connector class as a central means to open sockets or HTTP
82 * connection. Unfortunately, both methods are totally incompatible with each
83 * other, so a common abstraction has to be found to make the mail package
84 * work in both environments.
85 * <p>
86 * The Connection class is such an abstraction. It provides abstract
87 * versions of methods that open and close sockets, read from them and write
88 * to them. It also provides a static factory method that is able to instantiate
89 * one of several sub-classes of Connection that match the different run-time
90 * environments. These sub-classes are all called ConnectionImpl, and they are
91 * loaded "by name" from the j2me, j2se, or http packages, respectively. This
92 * is the only way to get rid of compile-time dependencies on these classes.
93 */
94public class Connection {
95    private String serverName;
96    private int serverPort;
97    private boolean useSSL;
98    private boolean deviceSide;
99    private StreamConnection socket;
100    private String localAddress;
101    private GlobalConfig globalConfig;
102    protected InputStream input;
103    protected OutputStream output;
104    private boolean useWiFi;
105    private int fakeAvailable = -1;
106   
107    /**
108     * Byte array holding carriage return and line feed
109     */
110    private static final byte[] CRLF = new byte[] {13, 10};
111   
112    /**
113     * Provides a buffer used for incoming data.
114     */
115    private byte[] buffer = new byte[128];
116   
117    /**
118     * Holds a list of open connections
119     */
120    private static Vector openConnections = new Vector();
121   
122    public Connection(String serverName, int serverPort, boolean useSSL, boolean deviceSide) {
123        this.serverName = serverName;
124        this.serverPort = serverPort;
125        this.useSSL = useSSL;
126        this.deviceSide = deviceSide;
127        this.input = null;
128        this.output = null;
129        this.socket = null;
130        this.globalConfig = MailSettings.getInstance().getGlobalConfig();
131    }
132   
133    /**
134     * Opens a connection.
135     */
136    public void open() throws IOException {
137        if(input != null || output != null || socket != null) {
138            close();
139        }
140
141        synchronized(openConnections) {
142            if(!openConnections.contains(this)) {
143                openConnections.addElement(this);
144            }
145        }
146       
147        String protocolStr = (useSSL ? "ssl" : "socket");
148        // This parameter, which allows bypassing the MDS proxy, should probably
149        // be a global user configurable option
150        String paramStr = (deviceSide ? ";deviceside=true" : "");
151
152        useWiFi = false;
153        if(globalConfig.getWifiMode() == GlobalConfig.WIFI_PROMPT) {
154            UiApplication.getUiApplication().invokeAndWait(new Runnable() {
155                public void run() {
156                    useWiFi = (Dialog.ask(Dialog.D_YES_NO, "Connect through WiFi?") == Dialog.YES);
157                }
158            });
159        }
160        else if(globalConfig.getWifiMode() == GlobalConfig.WIFI_ALWAYS) {
161            useWiFi = true;
162        }
163       
164        if(useWiFi) {
165            paramStr = paramStr + ";ConnectionUID=S TCP-WiFi;ConnectionSetup=delayed;retrynocontext=true";
166        }
167       
168        String connectStr = protocolStr + "://" + serverName + ":" + serverPort + paramStr;
169       
170        if(EventLogger.getMinimumLevel() >= EventLogger.INFORMATION) {
171            String msg = "Opening connection:\r\n"+connectStr+"\r\n";
172            EventLogger.logEvent(AppInfo.GUID, msg.getBytes(), EventLogger.INFORMATION);
173        }
174       
175        socket = (StreamConnection)Connector.open(connectStr, Connector.READ_WRITE, true);
176        input = socket.openDataInputStream();
177        output = socket.openDataOutputStream();
178        localAddress = ((SocketConnection)socket).getLocalAddress();
179
180        if(EventLogger.getMinimumLevel() >= EventLogger.INFORMATION) {
181            String msg =
182                "Connection established:\r\n"+
183                "Socket: "+socket.getClass().toString()+"\r\n"+
184                "Local address: "+localAddress+"\r\n";
185            EventLogger.logEvent(AppInfo.GUID, msg.getBytes(), EventLogger.INFORMATION);
186        }
187    }
188   
189    /**
190     * Closes a connection.
191     */
192    public void close() throws IOException {
193        try {
194            if(input != null) {
195                input.close();
196                input = null;
197            }
198        } catch (Exception exp) {
199            input = null;
200        }
201        try {
202            if(output != null) {
203                output.close();
204                output = null;
205            }
206        } catch (Exception exp) {
207            output = null;
208        }
209        try {
210            if(socket != null) {
211                socket.close();
212                socket = null;
213            }
214        } catch (Exception exp) {
215            socket = null;
216        }
217
218        synchronized(openConnections) {
219            if(openConnections.contains(this)) {
220                openConnections.removeElement(this);
221            }
222        }
223       
224        EventLogger.logEvent(AppInfo.GUID, "Connection closed".getBytes(), EventLogger.INFORMATION);
225    }
226   
227    /**
228     * Determine whether open connections exist
229     *
230     * @return True if there are open connections
231     */
232    public static boolean hasOpenConnections() {
233        boolean result;
234        synchronized(openConnections) {
235            result = !openConnections.isEmpty();
236        }
237        return result;
238    }
239   
240    /**
241     * Close all open connections
242     */
243    public static void closeAllConnections() {
244        synchronized(openConnections) {
245            int size = openConnections.size();
246            for(int i = 0; i < size; i++) {
247                try {
248                    ((Connection)openConnections.elementAt(i)).close();
249                } catch (IOException e) { }
250            }
251            openConnections.removeAllElements();
252        }
253    }
254   
255    /**
256     * Determine whether we are currently connected
257     * @return True if connected
258     */
259    public boolean isConnected() {
260        if(socket != null)
261            return true;
262        else
263            return false;
264    }
265   
266    /**
267     * Get the local address to which this connection is bound
268     * @return Local address
269     */
270    public String getLocalAddress() {
271        return localAddress;
272    }
273   
274    /**
275     * Get the server name used when this connection was created
276     * @return Server name
277     */
278    public String getServerName() {
279        return serverName;
280    }
281   
282    /**
283     * Sends a string to the server. This method is used internally for
284     * all outgoing communication to the server. The main thing it does
285     * it terminate the line with a CR/LF. If there are occurrences of CR or
286     * LF inside the string, the method ensures that proper CR/LF sequences
287     * are sent for them, since this is what most Internet protocols expect.
288     *
289     * @see #receive
290     */
291    public void send(String s) throws IOException {
292        byte[] bytes = s.getBytes();
293        int length = bytes.length;
294       
295        /**
296         * Special case for empty strings: Only CR/LF is sent.
297         */
298        if (s.length() == 0) {
299            if(globalConfig.getConnDebug()) {
300                EventLogger.logEvent(AppInfo.GUID, "[SEND]".getBytes(), EventLogger.DEBUG_INFO);
301            }
302           
303            output.write(CRLF, 0, 2);
304        }
305        /**
306         * The usual case goes here.
307         */
308        else {
309            int i = 0;
310            while (i < length) {
311                int j = i;
312               
313                /**
314                 * Find next occurrence of a line separator or the end of the
315                 * string.
316                 */
317                while ((j < length) && (bytes[j] != 0x0A) && (bytes[j] != 0x0D)) {
318                    j++;
319                }
320               
321                if(globalConfig.getConnDebug()) {
322                    EventLogger.logEvent(AppInfo.GUID, ("[SEND] " + s.substring(i, j)).getBytes(), EventLogger.DEBUG_INFO);
323                }
324               
325                /**
326                 * Write the string up to there and terminate it properly.
327                 */
328                output.write((s.substring(i, j)+"\r\n").getBytes());
329               
330                /**
331                 * If we stopped at a CR, ignore a possibly following LF.
332                 */
333                if ((j < length - 1) && (bytes[j] == 0x0D) && (bytes[j + 1] == 0x0A)) {
334                    j++;
335                }
336               
337                i = j + 1;
338            }
339        }
340        output.flush();
341    }
342   
343    /**
344     * Sends a string to the server, terminating it with a CRLF.
345     * No cleanup is performed, as it is expected that the string
346     * is a prepared protocol command.
347     */
348    public void sendCommand(String s) throws IOException {
349        if(globalConfig.getConnDebug()) {
350            EventLogger.logEvent(AppInfo.GUID, ("[SEND CMD] " + s).getBytes(), EventLogger.DEBUG_INFO);
351        }
352
353        if(s == null) {
354            output.write(CRLF, 0, 2);
355        }
356        else {
357            output.write((s+"\r\n").getBytes());
358        }
359        output.flush();
360    }
361   
362    /**
363     * Sends a string to the server. This method is used to bypass all
364     * the processing done by the normal send method, and is most useful
365     * for bulk transmissions.  It writes the provided string to the socket
366     * in a single command, followed by a flush.
367     *
368     * @see #send
369     */
370    public void sendRaw(String s) throws IOException {
371        byte[] bytes = s.getBytes();
372       
373        if(globalConfig.getConnDebug()) {
374            EventLogger.logEvent(AppInfo.GUID, ("[SEND RAW]\r\n" + s).getBytes(), EventLogger.DEBUG_INFO);
375        }
376       
377        output.write(bytes, 0, bytes.length);
378       
379        output.flush();
380    }
381
382    /**
383     * Returns the number of bytes available for reading.
384     * Used to poll the connection without blocking.
385     *
386     * @see InputStream#available()
387     */
388    public int available() throws IOException {
389        if(fakeAvailable == -1) {
390                return input.available();
391        }
392        else {
393                return fakeAvailable;
394        }
395    }
396   
397    /**
398     * Receives a string from the server. This method is used internally for
399     * incoming communication from the server. The main thing it does is
400     * ensuring that only complete lines are returned to the application, that is,
401     * lines that were terminated at least by a CR. LFs are ignored completely.
402     * Neither CRs nor LFs are returned as part of the result.
403     *
404     * @see #send
405     */
406    public String receive() throws IOException {
407        /**
408         * A note on how this method works and why it is designed the
409         * way it is: A previous implementation tried to read multiple
410         * bytes from the InputStream, searched a possible line terminator
411         * in them, and appended characters to a temporary result string.
412         * That didn't work very well, because the code had to deal
413         * with a large number of temporary strings that grew as more
414         * characters belonging to a line were read. The result was
415         * a high level of heap fragmentation, which made applications
416         * unstable on MIDP devices (that don't provide compacting GC,
417         * that is).
418         *
419         * This new implementation seems to work better: Is reads
420         * characters from the InputStream one-by-one, putting them
421         * in a buffer of fixed size that is large enough to hold a
422         * "normal" line belonging to an e-mail. When a terminator
423         * is found, a new String object is created from that buffer.
424         * Since the buffer is of fixed size and used during the whole
425         * lifetime of the Connection object, and the String is the
426         * actual String that makes it into the final Message object,
427         * there are no temporary Strings needed -- at least not in the
428         * common case: If a mail server tends to send whole messages
429         * without line terminators (and thus disregards for example
430         * the POP3 specification that dictates a maximum response of
431         * 512 characters), temporary Strings will again be necessary,
432         * but there's not much one can about that.
433         */
434        StringBuffer resultBuffer = new StringBuffer();
435        boolean stop = false;
436        int actualAvailable = input.available();
437        int readBytes = 0;
438        int count;
439       
440        /**
441         * The "stop" flag will be set as soon as we have received
442         * a complete line, that is, a line terminated by CR/LF. Until
443         * this is the case, the following block is repeated.
444         */
445        while (!stop) {
446            count = 0;
447           
448            /**
449             * The inner block reads single bytes from the InputStream
450             * until, again, a line terminator is read or the buffer is
451             * full.
452             */
453            while (true) {
454                int actual = input.read(buffer, count, 1);
455               
456                /**
457                 * If -1 is returned, the InputStream is already closed,
458                 * probably because the connection is broken, or the server
459                 * didn't like is. Try to close the connection, but ignore
460                 * any errors that might result from this.
461                 */
462                if (actual == -1) {
463                    EventLogger.logEvent(AppInfo.GUID, "Unable to read from socket, closing connection".getBytes(), EventLogger.INFORMATION);
464                    try {
465                        close();
466                    } catch (IOException e) { }
467                   
468                    throw new IOException("Connection closed");
469                }
470               
471                /**
472                 * If no bytes have been received, we wait a little
473                 * while (by yielding processing time to other threads).
474                 */
475                else if (actual == 0) {
476                    try {
477                        Thread.yield();
478                    } catch (Exception e) { }
479                }
480               
481                /**
482                 * If a byte has been read, examine it and put it in the
483                 * buffer.
484                 */
485                // Note: We really should look for CRLF, and not use this
486                // approach which screws up on mid-line LFs. (DK)
487                else {
488                    byte b = buffer[count];
489                    readBytes++;
490                   
491                    /**
492                     * Ignore all CRs.
493                     */
494                    if (b == 0x0D) {
495                        /* Ignore CRs */
496                    }
497                    /**
498                     * Take LF as line separator.
499                     */
500                    else if (b == 0x0A) {
501                        stop = true;
502                        break;
503                    }
504                    /**
505                     * Everything else makes it into the buffer.
506                     */
507                    else {
508                        count++;
509                        if (count == buffer.length) {
510                            break;
511                        }
512                    }
513                }
514            }
515            resultBuffer.append(new String(buffer, 0, count));
516        }
517       
518        if(globalConfig.getConnDebug()) {
519            EventLogger.logEvent(AppInfo.GUID, ("[RECV] " + resultBuffer.toString()).getBytes(), EventLogger.DEBUG_INFO);
520        }
521        if(actualAvailable > readBytes) {
522                fakeAvailable = actualAvailable - readBytes;
523        }
524        else {
525                fakeAvailable = -1;
526        }
527       
528        return resultBuffer.toString();
529    }
530}
Note: See TracBrowser for help on using the browser.