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    
020    package org.apache.geronimo.javamail.store.nntp;
021    
022    import java.io.ByteArrayOutputStream;
023    import java.io.IOException;
024    import java.io.InputStream;
025    import java.util.Enumeration;
026    
027    import javax.mail.Flags;
028    import javax.mail.IllegalWriteException;
029    import javax.mail.MessagingException;
030    import javax.mail.Session;
031    import javax.mail.internet.InternetHeaders;
032    import javax.mail.internet.MimeMessage;
033    
034    import org.apache.geronimo.javamail.transport.nntp.NNTPConnection;
035    import org.apache.geronimo.javamail.transport.nntp.NNTPReply;
036    import org.apache.geronimo.javamail.transport.nntp.StringListInputStream;
037    
038    /**
039     * NNTP implementation of javax.mail.internet.MimeMessage
040     * 
041     * Only the most basic information is given and Message objects created here is
042     * a light-weight reference to the actual Message As per the JavaMail spec items
043     * from the actual message will get filled up on demand
044     * 
045     * If some other items are obtained from the server as a result of one call,
046     * then the other details are also processed and filled in. For ex if RETR is
047     * called then header information will also be processed in addition to the
048     * content
049     * 
050     * @version $Rev: 437941 $ $Date: 2006-08-28 23:56:02 -0400 (Mon, 28 Aug 2006) $
051     */
052    public class NNTPMessage extends MimeMessage {
053        // the server message identifer
054        String messageID = null;
055    
056        // our attached session
057        protected Session session;
058    
059        // the Store we're stored in (which manages the connection and other stuff).
060        protected NNTPStore store;
061    
062        // our active connection.
063        protected NNTPConnection connection;
064    
065        // used to force loading of headers
066        protected boolean headersLoaded = false;
067    
068        // use to force content loading
069        protected boolean contentLoaded = false;
070    
071        /**
072         * Contruct an NNTPMessage instance.
073         * 
074         * @param folder
075         *            The hosting folder for the message.
076         * @param store
077         *            The Store owning the article (and folder).
078         * @param msgnum
079         *            The article message number.
080         * @param messageID
081         *            The article messageID (as assigned by the server).
082         * 
083         * @exception MessagingException
084         */
085        NNTPMessage(NNTPFolder folder, NNTPStore store, int msgnum, String messageID) throws MessagingException {
086            super(folder, msgnum);
087            this.messageID = messageID;
088            this.store = store;
089            this.session = ((NNTPStore) store).getSession();
090            // get the active connection from the store...all commands are sent
091            // there
092            this.connection = ((NNTPStore) store).getConnection();
093    
094            // get our flag set from the folder.
095            flags = folder.getPermanentFlags();
096            // now check our initial SEEN state and set the flags appropriately
097            if (folder.isSeen(msgnum)) {
098                flags.add(Flags.Flag.SEEN);
099            } else {
100                flags.remove(Flags.Flag.SEEN);
101            }
102        }
103    
104        /**
105         * Retrieve the size of the message content. The content will be retrieved
106         * from the server, if necessary.
107         * 
108         * @return The size of the content.
109         * @exception MessagingException
110         */
111        public int getSize() throws MessagingException {
112            // make sure we've retrieved the message content and continue with the
113            // superclass version.
114            loadContent();
115            return super.getSize();
116        }
117    
118        /**
119         * Get a line count for the NNTP message. This is potentially stored in the
120         * Lines article header. If not there, we return a default of -1.
121         * 
122         * @return The header line count estimate, or -1 if not retrieveable.
123         * @exception MessagingException
124         */
125        public int getLineCount() throws MessagingException {
126            String[] headers = getHeader("Lines");
127    
128            // hopefully, there's only a single one of these. No sensible way of
129            // interpreting
130            // multiples.
131            if (headers.length == 1) {
132                try {
133                    return Integer.parseInt(headers[0].trim());
134    
135                } catch (NumberFormatException e) {
136                    // ignore
137                }
138            }
139            // dunno...and let them know I don't know.
140            return -1;
141        }
142    
143        /**
144         * @see javax.mail.internet.MimeMessage#getContentStream()
145         */
146        protected InputStream getContentStream() throws MessagingException {
147            // get the article information.
148            loadArticle();
149            return super.getContentStream();
150        }
151    
152        /***************************************************************************
153         * Following is a set of methods that deal with headers These methods are
154         * just overrides on the superclass methods to allow lazy loading of the
155         * header information.
156         **************************************************************************/
157    
158        public String[] getHeader(String name) throws MessagingException {
159            loadHeaders();
160            return headers.getHeader(name);
161        }
162    
163        public String getHeader(String name, String delimiter) throws MessagingException {
164            loadHeaders();
165            return headers.getHeader(name, delimiter);
166        }
167    
168        public Enumeration getAllHeaders() throws MessagingException {
169            loadHeaders();
170            return headers.getAllHeaders();
171        }
172    
173        public Enumeration getMatchingHeaders(String[] names) throws MessagingException {
174            loadHeaders();
175            return headers.getMatchingHeaders(names);
176        }
177    
178        public Enumeration getNonMatchingHeaders(String[] names) throws MessagingException {
179            loadHeaders();
180            return headers.getNonMatchingHeaders(names);
181        }
182    
183        public Enumeration getAllHeaderLines() throws MessagingException {
184            loadHeaders();
185            return headers.getAllHeaderLines();
186        }
187    
188        public Enumeration getMatchingHeaderLines(String[] names) throws MessagingException {
189            loadHeaders();
190            return headers.getMatchingHeaderLines(names);
191        }
192    
193        public Enumeration getNonMatchingHeaderLines(String[] names) throws MessagingException {
194            loadHeaders();
195            return headers.getNonMatchingHeaderLines(names);
196        }
197    
198        // the following are overrides for header modification methods. These
199        // messages are read only,
200        // so the headers cannot be modified.
201        public void addHeader(String name, String value) throws MessagingException {
202            throw new IllegalWriteException("NNTP messages are read-only");
203        }
204    
205        public void setHeader(String name, String value) throws MessagingException {
206            throw new IllegalWriteException("NNTP messages are read-only");
207        }
208    
209        public void removeHeader(String name) throws MessagingException {
210            throw new IllegalWriteException("NNTP messages are read-only");
211        }
212    
213        public void addHeaderLine(String line) throws MessagingException {
214            throw new IllegalWriteException("IMAP messages are read-only");
215        }
216    
217        /**
218         * We cannot modify these messages
219         */
220        public void saveChanges() throws MessagingException {
221            throw new IllegalWriteException("NNTP messages are read-only");
222        }
223    
224        /**
225         * Retrieve the message headers from the NNTP server.
226         * 
227         * @exception MessagingException
228         */
229        public void loadHeaders() throws MessagingException {
230            // don't retrieve if already loaded.
231            if (headersLoaded) {
232                return;
233            }
234    
235            NNTPReply reply = connection.sendCommand("HEAD " + messageID, NNTPReply.HEAD_FOLLOWS);
236    
237            if (reply.getCode() == NNTPReply.HEAD_FOLLOWS) {
238                try {
239                    // wrap a stream around the reply data and read as headers.
240                    updateHeaders(new StringListInputStream(reply.getData()));
241                } catch (IOException e) {
242                    throw new MessagingException("Error retrieving article headers from server", e);
243                }
244            } else {
245                throw new MessagingException("Error retrieving article headers from server: " + reply);
246            }
247        }
248    
249        /**
250         * Update the message headers from an input stream.
251         * 
252         * @param in
253         *            The InputStream source for the header information.
254         * 
255         * @exception MessagingException
256         */
257        public void updateHeaders(InputStream in) throws MessagingException {
258            // wrap a stream around the reply data and read as headers.
259            headers = new InternetHeaders(in);
260            headersLoaded = true;
261        }
262    
263        /**
264         * Load just the message content from the NNTP server.
265         * 
266         * @exception MessagingException
267         */
268        public void loadContent() throws MessagingException {
269            if (contentLoaded) {
270                return;
271            }
272    
273            NNTPReply reply = connection.sendCommand("BODY " + messageID, NNTPReply.BODY_FOLLOWS);
274    
275            if (reply.getCode() == NNTPReply.BODY_FOLLOWS) {
276                try {
277                    InputStream in = new StringListInputStream(reply.getData());
278                    updateContent(in);
279                } catch (IOException e) {
280                    throw new MessagingException("Error retrieving article body from server", e);
281                }
282            } else {
283                throw new MessagingException("Error retrieving article body from server: " + reply);
284            }
285        }
286    
287        /**
288         * Load the entire article from the NNTP server. This updates both the
289         * headers and the content.
290         * 
291         * @exception MessagingException
292         */
293        public void loadArticle() throws MessagingException {
294            // if the headers are already loaded, retrieve the content portion.
295            if (headersLoaded) {
296                loadContent();
297                return;
298            }
299    
300            // we need to retrieve everything.
301            NNTPReply reply = connection.sendCommand("ARTICLE " + messageID, NNTPReply.ARTICLE_FOLLOWS);
302    
303            if (reply.getCode() == NNTPReply.ARTICLE_FOLLOWS) {
304                try {
305                    InputStream in = new StringListInputStream(reply.getData());
306                    // update both the headers and the content.
307                    updateHeaders(in);
308                    updateContent(in);
309                } catch (IOException e) {
310                    throw new MessagingException("Error retrieving article from server", e);
311                }
312            } else {
313                throw new MessagingException("Error retrieving article from server: " + reply);
314            }
315        }
316    
317        /**
318         * Update the article content from an input stream.
319         * 
320         * @param in
321         *            The content data source.
322         * 
323         * @exception MessagingException
324         */
325        public void updateContent(InputStream in) throws MessagingException {
326            try {
327                ByteArrayOutputStream out = new ByteArrayOutputStream();
328    
329                byte[] buffer = new byte[4096];
330    
331                // copy the content data from the stream into a byte buffer for the
332                // content.
333                while (true) {
334                    int read = in.read(buffer);
335                    if (read == -1) {
336                        break;
337                    }
338                    out.write(buffer, 0, read);
339                }
340    
341                content = out.toByteArray();
342                contentLoaded = true;
343            } catch (IOException e) {
344                throw new MessagingException("Error retrieving message body from server", e);
345            }
346        }
347    
348        /**
349         * Get the server assigned messageid for the article.
350         * 
351         * @return The server assigned message id.
352         */
353        public String getMessageId() {
354            return messageID;
355        }
356    
357        /**
358         * Override of setFlags(). We need to ensure that if the SEEN flag is set or
359         * cleared, that the newsrc file correctly reflects the current state.
360         * 
361         * @param flag
362         *            The flag being set.
363         * @param newvalue
364         *            The new flag value.
365         * 
366         * @exception MessagingException
367         */
368        public void setFlags(Flags flag, boolean newvalue) throws MessagingException {
369            // if this is the SEEN flag, make sure we shadow this in the newsrc
370            // file.
371            if (flag.contains(Flags.Flag.SEEN)) {
372                ((NNTPFolder) folder).setSeen(msgnum, newvalue);
373            }
374            // have the superclass do the real flag setting.
375            super.setFlags(flag, newvalue);
376        }
377    }