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

Revision 614, 23.5 KB (checked in by octorian, 8 weeks ago)

Addition of ConnectionFactory implementation for 5.0 (#146)

  • 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 net.rim.device.api.crypto.tls.tls10.TLS10Connection;
63import net.rim.device.api.i18n.ResourceBundle;
64import net.rim.device.api.system.EventLogger;
65import net.rim.device.api.util.DataBuffer;
66import net.rim.device.cldc.io.ssl.TLSException;
67
68import org.logicprobe.LogicMail.AppInfo;
69import org.logicprobe.LogicMail.LogicMailResource;
70import org.logicprobe.LogicMail.conf.ConnectionConfig;
71import org.logicprobe.LogicMail.conf.GlobalConfig;
72import org.logicprobe.LogicMail.conf.MailSettings;
73
74import java.io.DataInputStream;
75import java.io.DataOutputStream;
76import java.io.IOException;
77import java.io.InputStream;
78import java.io.OutputStream;
79
80import javax.microedition.io.SocketConnection;
81import javax.microedition.io.StreamConnection;
82
83
84/**
85 * Is the abstract base class for socket connections used inside the SMTP,
86 * POP3 and IMAP protocols of the mail package. The rationale for using this class
87 * is the difference in how networking is handled in J2ME and J2SE: While J2SE
88 * has java.net.Socket, J2ME uses the Generic Connection Framework with its
89 * javax.io.Connector class as a central means to open sockets or HTTP
90 * connection. Unfortunately, both methods are totally incompatible with each
91 * other, so a common abstraction has to be found to make the mail package
92 * work in both environments.
93 * <p>
94 * The Connection class is such an abstraction. It provides abstract
95 * versions of methods that open and close sockets, read from them and write
96 * to them. It also provides a static factory method that is able to instantiate
97 * one of several sub-classes of Connection that match the different run-time
98 * environments. These sub-classes are all called ConnectionImpl, and they are
99 * loaded "by name" from the j2me, j2se, or http packages, respectively. This
100 * is the only way to get rid of compile-time dependencies on these classes.
101 */
102public abstract class Connection {
103    protected static ResourceBundle resources = ResourceBundle.getBundle(LogicMailResource.BUNDLE_ID, LogicMailResource.BUNDLE_NAME);
104   
105    /** Select everything except WiFi */
106    protected static final int TRANSPORT_AUTO = 0xFE;
107    /** Select WiFi */
108    protected static final int TRANSPORT_WIFI = 0x01;
109    /** Select Direct TCP */
110    protected static final int TRANSPORT_DIRECT_TCP = 0x02;
111    /** Select MDS */
112    protected static final int TRANSPORT_MDS = 0x04;
113    /** Select WAP 2.0 */
114    protected static final int TRANSPORT_WAP2 = 0x08;
115
116    /**
117     * Byte array holding carriage return and line feed
118     */
119    private static final byte[] CRLF = new byte[] { 13, 10 };
120
121    private UtilFactory utilFactory;
122    protected String serverName;
123    protected int serverPort;
124    protected boolean useSSL;
125    protected int transports;
126    private StreamConnection socket;
127    private String localAddress;
128    protected GlobalConfig globalConfig;
129    protected InputStream input;
130    protected OutputStream output;
131    protected boolean useWiFi;
132    private int fakeAvailable = -1;
133    private int bytesSent = 0;
134    private int bytesReceived = 0;
135
136    /**
137     * Provides a buffer used for incoming data.
138     */
139    private byte[] buffer = new byte[128];
140
141    /**
142     * Provides a dynamic buffer for building results
143     */
144    DataBuffer resultBuffer = new DataBuffer();
145
146    /**
147     * Initializes a new connection object.
148     *
149     * @param connectionConfig Configuration data for the connection
150     */
151    protected Connection(ConnectionConfig connectionConfig) {
152        this.globalConfig = MailSettings.getInstance().getGlobalConfig();
153        this.utilFactory = UtilFactory.getInstance();
154       
155        this.serverName = connectionConfig.getServerName();
156        this.serverPort = connectionConfig.getServerPort();
157        this.useSSL = (connectionConfig.getServerSecurity() == ConnectionConfig.SECURITY_SSL);
158       
159        int transportType;
160        boolean enableWiFi;
161        if(connectionConfig.getTransportType() == ConnectionConfig.TRANSPORT_GLOBAL) {
162            transportType = globalConfig.getTransportType();
163            enableWiFi = globalConfig.getEnableWiFi();
164        }
165        else {
166            transportType = connectionConfig.getTransportType();
167            enableWiFi = connectionConfig.getEnableWiFi();
168        }
169
170        // Populate the bit-flags for the selected transport types
171        // based on the configuration parameters.
172        switch(transportType) {
173        case ConnectionConfig.TRANSPORT_AUTO:
174            transports = Connection.TRANSPORT_AUTO;
175            break;
176        case ConnectionConfig.TRANSPORT_DIRECT_TCP:
177            transports = Connection.TRANSPORT_DIRECT_TCP;
178            break;
179        case ConnectionConfig.TRANSPORT_MDS:
180            transports = Connection.TRANSPORT_MDS;
181            break;
182        case ConnectionConfig.TRANSPORT_WAP2:
183            transports = Connection.TRANSPORT_WAP2;
184            break;
185        default:
186            // Should only get here in rare cases of invalid configuration
187            // data, so we select full automatic with WiFi.
188            transports = Connection.TRANSPORT_AUTO;
189            enableWiFi = true;
190            break;
191        }
192        if(enableWiFi) { transports |= Connection.TRANSPORT_WIFI; }
193       
194        this.input = null;
195        this.output = null;
196        this.socket = null;
197    }
198   
199    /**
200     * Opens a connection.
201     */
202    public synchronized void open() throws IOException {
203        if ((input != null) || (output != null) || (socket != null)) {
204            close();
205        }
206       
207        utilFactory.addOpenConnection(this);
208
209        socket = openStreamConnection();
210        if(socket == null) {
211            throw new IOException(resources.getString(LogicMailResource.ERROR_UNABLE_TO_OPEN_CONNECTION));
212        }
213       
214        input = socket.openDataInputStream();
215        output = socket.openDataOutputStream();
216        localAddress = ((SocketConnection) socket).getLocalAddress();
217        bytesSent = 0;
218        bytesReceived = 0;
219
220        if (EventLogger.getMinimumLevel() >= EventLogger.INFORMATION) {
221            String msg = "Connection established:\r\n" + "Socket: " +
222            socket.getClass().toString() + "\r\n" + "Local address: " +
223            localAddress + "\r\n";
224            EventLogger.logEvent(AppInfo.GUID, msg.getBytes(),
225                    EventLogger.INFORMATION);
226        }
227    }
228   
229    /**
230     * Open a stream connection.
231     * This method should encapsulate all platform-specific logic for opening
232     * network connections.
233     *
234     * @return the stream connection
235     *
236     * @throws IOException Signals that an I/O exception has occurred.
237     */
238    protected abstract StreamConnection openStreamConnection() throws IOException;
239
240    /**
241     * Closes a connection.
242     */
243    public synchronized void close() throws IOException {
244        try {
245            if (input != null) {
246                input.close();
247                input = null;
248            }
249        } catch (Exception exp) {
250            input = null;
251        }
252
253        try {
254            if (output != null) {
255                output.close();
256                output = null;
257            }
258        } catch (Exception exp) {
259            output = null;
260        }
261
262        try {
263            if (socket != null) {
264                socket.close();
265                socket = null;
266            }
267        } catch (Exception exp) {
268            socket = null;
269        }
270
271        utilFactory.removeOpenConnection(this);
272
273        EventLogger.logEvent(AppInfo.GUID, "Connection closed".getBytes(),
274                EventLogger.INFORMATION);
275    }
276
277    /**
278     * Determine whether we are currently connected
279     * @return True if connected
280     */
281    public boolean isConnected() {
282        if (socket != null) {
283            return true;
284        } else {
285            return false;
286        }
287    }
288
289    /**
290     * Get the local address to which this connection is bound
291     * @return Local address
292     */
293    public String getLocalAddress() {
294        return localAddress;
295    }
296
297    /**
298     * Get the server name used when this connection was created
299     * @return Server name
300     */
301    public String getServerName() {
302        return serverName;
303    }
304
305    /**
306     * Gets the number of bytes that have been sent since the
307     * connection was opened.
308     * <p>
309     * The counter is not synchronized, so it should only be
310     * called from the same thread as the send and receive
311     * methods.
312     * </p>
313     * @return bytes sent
314     */
315    public int getBytesSent() {
316        return bytesSent;
317    }
318
319    /**
320     * Gets the number of bytes that have been received since the
321     * connection was opened.
322     * <p>
323     * The counter is not synchronized, so it should only be
324     * called from the same thread as the send and receive
325     * methods.
326     * </p>
327     * @return bytes received
328     */
329    public int getBytesReceived() {
330        return bytesReceived;
331    }
332
333    /**
334     * Sends a string to the server. This method is used internally for
335     * all outgoing communication to the server. The main thing it does
336     * it terminate the line with a CR/LF. If there are occurrences of CR or
337     * LF inside the string, the method ensures that proper CR/LF sequences
338     * are sent for them, since this is what most Internet protocols expect.
339     *
340     * @see #receive
341     */
342    public synchronized void send(String s) throws IOException {
343        byte[] bytes = s.getBytes();
344        int length = bytes.length;
345
346        /**
347         * Special case for empty strings: Only CR/LF is sent.
348         */
349        if (s.length() == 0) {
350            if (globalConfig.getConnDebug()) {
351                EventLogger.logEvent(AppInfo.GUID, "[SEND]".getBytes(),
352                        EventLogger.DEBUG_INFO);
353            }
354
355            output.write(CRLF, 0, 2);
356            bytesSent += 2;
357        }
358        /**
359         * The usual case goes here.
360         */
361        else {
362            int i = 0;
363
364            while (i < length) {
365                int j = i;
366
367                /**
368                 * Find next occurrence of a line separator or the end of the
369                 * string.
370                 */
371                while ((j < length) && (bytes[j] != 0x0A) &&
372                        (bytes[j] != 0x0D)) {
373                    j++;
374                }
375
376                if (globalConfig.getConnDebug()) {
377                    EventLogger.logEvent(AppInfo.GUID,
378                            ("[SEND] " + s.substring(i, j)).getBytes(),
379                            EventLogger.DEBUG_INFO);
380                }
381
382                /**
383                 * Write the string up to there and terminate it properly.
384                 */
385                byte[] buf = (s.substring(i, j) + "\r\n").getBytes();
386                output.write(buf);
387                bytesSent += buf.length;
388
389                /**
390                 * If we stopped at a CR, ignore a possibly following LF.
391                 */
392                if ((j < (length - 1)) && (bytes[j] == 0x0D) &&
393                        (bytes[j + 1] == 0x0A)) {
394                    j++;
395                }
396
397                i = j + 1;
398            }
399        }
400
401        output.flush();
402    }
403
404    /**
405     * Sends a string to the server, terminating it with a CRLF.
406     * No cleanup is performed, as it is expected that the string
407     * is a prepared protocol command.
408     */
409    public synchronized void sendCommand(String s) throws IOException {
410        if (globalConfig.getConnDebug()) {
411            EventLogger.logEvent(AppInfo.GUID, ("[SEND CMD] " + s).getBytes(),
412                    EventLogger.DEBUG_INFO);
413        }
414
415        if (s == null) {
416            output.write(CRLF, 0, 2);
417            bytesSent += 2;
418        } else {
419            byte[] buf = (s + "\r\n").getBytes();
420            output.write(buf);
421            bytesSent += buf.length;
422        }
423
424        output.flush();
425    }
426
427    /**
428     * Sends a string to the server. This method is used to bypass all
429     * the processing done by the normal send method, and is most useful
430     * for bulk transmissions.  It writes the provided string to the socket
431     * in a single command, followed by a flush.
432     *
433     * @see #send
434     */
435    public synchronized void sendRaw(String s) throws IOException {
436        byte[] buf = s.getBytes();
437
438        if (globalConfig.getConnDebug()) {
439            EventLogger.logEvent(AppInfo.GUID,
440                    ("[SEND RAW]\r\n" + s).getBytes(), EventLogger.DEBUG_INFO);
441        }
442
443        output.write(buf, 0, buf.length);
444        bytesSent += buf.length;
445
446        output.flush();
447    }
448
449    /**
450     * Returns the number of bytes available for reading.
451     * Used to poll the connection without blocking.
452     *
453     * @see InputStream#available()
454     */
455    public int available() throws IOException {
456        if (fakeAvailable == -1) {
457            return input.available();
458        } else {
459            return fakeAvailable;
460        }
461    }
462
463    /**
464     * Receives a string from the server. This method is used internally for
465     * incoming communication from the server. The main thing it does is
466     * ensuring that only complete lines are returned to the application, that is,
467     * lines that were terminated at least by a CR. LFs are ignored completely.
468     * Neither CRs nor LFs are returned as part of the result.
469     *
470     * @see #send
471     */
472    public synchronized String receive() throws IOException {
473        /**
474         * A note on how this method works and why it is designed the
475         * way it is: A previous implementation tried to read multiple
476         * bytes from the InputStream, searched a possible line terminator
477         * in them, and appended characters to a temporary result string.
478         * That didn't work very well, because the code had to deal
479         * with a large number of temporary strings that grew as more
480         * characters belonging to a line were read. The result was
481         * a high level of heap fragmentation, which made applications
482         * unstable on MIDP devices (that don't provide compacting GC,
483         * that is).
484         *
485         * This new implementation seems to work better: Is reads
486         * characters from the InputStream one-by-one, putting them
487         * in a buffer of fixed size that is large enough to hold a
488         * "normal" line belonging to an e-mail. When a terminator
489         * is found, a new String object is created from that buffer.
490         * Since the buffer is of fixed size and used during the whole
491         * lifetime of the Connection object, and the String is the
492         * actual String that makes it into the final Message object,
493         * there are no temporary Strings needed -- at least not in the
494         * common case: If a mail server tends to send whole messages
495         * without line terminators (and thus disregards for example
496         * the POP3 specification that dictates a maximum response of
497         * 512 characters), temporary Strings will again be necessary,
498         * but there's not much one can about that.
499         */
500        boolean stop = false;
501        resultBuffer.reset();
502
503        int actualAvailable = input.available();
504        int readBytes = 0;
505        int count;
506
507        /**
508         * The "stop" flag will be set as soon as we have received
509         * a complete line, that is, a line terminated by CR/LF. Until
510         * this is the case, the following block is repeated.
511         */
512        while (!stop) {
513            count = 0;
514
515            /**
516             * The inner block reads single bytes from the InputStream
517             * until, again, a line terminator is read or the buffer is
518             * full.
519             */
520            while (true) {
521                int actual = input.read(buffer, count, 1);
522
523                /**
524                 * If -1 is returned, the InputStream is already closed,
525                 * probably because the connection is broken, or the server
526                 * didn't like is. Try to close the connection, but ignore
527                 * any errors that might result from this.
528                 */
529                if (actual == -1) {
530                    EventLogger.logEvent(AppInfo.GUID,
531                            "Unable to read from socket, closing connection".getBytes(),
532                            EventLogger.INFORMATION);
533
534                    try {
535                        close();
536                    } catch (IOException e) {
537                    }
538
539                    throw new IOException("Connection closed");
540                }
541                /**
542                 * If no bytes have been received, we wait a little
543                 * while (by yielding processing time to other threads).
544                 */
545                else if (actual == 0) {
546                    try {
547                        Thread.yield();
548                    } catch (Exception e) {
549                    }
550                }
551                /**
552                 * If a byte has been read, examine it and put it in the
553                 * buffer.
554                 */
555
556                // Note: We really should look for CRLF, and not use this
557                // approach which screws up on mid-line LFs. (DK)
558                else {
559                    bytesReceived += actual;
560
561                    byte b = buffer[count];
562                    readBytes++;
563
564                    /**
565                     * Ignore all CRs.
566                     */
567                    if (b == 0x0D) {
568                        /* Ignore CRs */
569                    }
570                    /**
571                     * Take LF as line separator.
572                     */
573                    else if (b == 0x0A) {
574                        stop = true;
575
576                        break;
577                    }
578                    /**
579                     * Everything else makes it into the buffer.
580                     */
581                    else {
582                        count++;
583
584                        if (count == buffer.length) {
585                            break;
586                        }
587                    }
588                }
589            }
590
591            resultBuffer.write(buffer, 0, count);
592        }
593
594        String result = new String(resultBuffer.toArray());
595
596        if (globalConfig.getConnDebug()) {
597            EventLogger.logEvent(AppInfo.GUID,
598                    ("[RECV] " + result).getBytes(),
599                    EventLogger.DEBUG_INFO);
600        }
601
602        if (actualAvailable > readBytes) {
603            fakeAvailable = actualAvailable - readBytes;
604        } else {
605            fakeAvailable = -1;
606        }
607
608        return result;
609    }
610
611    /**
612     * Switches the underlying connection to SSL mode, as commonly done after
613     * sending a <tt>STARTTLS</tt> command to the server.
614     *
615     * @throws IOException Signals that an I/O exception has occurred.
616     */
617    public void startTLS() throws IOException {
618        // Shortcut the method if we're already in SSL mode
619        if(socket instanceof TLS10Connection) { return; }
620
621        try {
622            TLS10Connection tlsSocket = new TLS10Connection(
623                    new StreamConnectionWrapper(
624                            socket,
625                            (DataInputStream)input,
626                            (DataOutputStream)output),
627                            serverName + ':' + serverPort,
628                            true);
629
630            socket = tlsSocket;
631            input = socket.openDataInputStream();
632            output = socket.openDataOutputStream();
633        } catch (IOException e) {
634            EventLogger.logEvent(AppInfo.GUID,
635                    ("Unable to switch to TLS mode: " + e.getMessage()).getBytes(), EventLogger.ERROR);
636            throw new IOException("Unable to switch to TLS mode");
637        } catch (TLSException e) {
638            EventLogger.logEvent(AppInfo.GUID,
639                    ("Unable to switch to TLS mode: " + e.getMessage()).getBytes(), EventLogger.ERROR);
640            throw new IOException("Unable to switch to TLS mode");
641        }
642    }
643
644    /**
645     * Decorator to wrap an existing stream connection so its I/O streams
646     * can be reopened without throwing exceptions.
647     */
648    private static class StreamConnectionWrapper implements StreamConnection {
649        private StreamConnection stream;
650        private DataInputStream dataInputStream;
651        private DataOutputStream dataOutputStream;
652
653        public StreamConnectionWrapper(StreamConnection stream, DataInputStream dataInputStream, DataOutputStream dataOutputStream) {
654            this.stream = stream;
655            this.dataInputStream = dataInputStream;
656            this.dataOutputStream = dataOutputStream;
657        }
658
659        public DataInputStream openDataInputStream() throws IOException {
660            return dataInputStream;
661        }
662        public InputStream openInputStream() throws IOException {
663            return dataInputStream;
664        }
665        public void close() throws IOException {
666            stream.close();
667        }
668        public DataOutputStream openDataOutputStream() throws IOException {
669            return dataOutputStream;
670        }
671        public OutputStream openOutputStream() throws IOException {
672            return dataOutputStream;
673        }
674    }
675}
Note: See TracBrowser for help on using the browser.