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.util.ArrayList;
023    import java.util.HashMap;
024    import java.util.List;
025    import java.util.Map;
026    import java.util.StringTokenizer;
027    
028    import javax.mail.FetchProfile;
029    import javax.mail.FolderNotFoundException;
030    import javax.mail.Message;
031    import javax.mail.MessagingException;
032    
033    import org.apache.geronimo.javamail.store.nntp.newsrc.NNTPNewsrcGroup;
034    import org.apache.geronimo.javamail.transport.nntp.NNTPReply;
035    
036    /**
037     * The NNTP implementation of the javax.mail.Folder Note that only INBOX is
038     * supported in NNTP
039     * <p>
040     * <url>http://www.faqs.org/rfcs/rfc1939.html</url>
041     * </p>
042     * 
043     * @see javax.mail.Folder
044     * 
045     * @version $Rev: 686231 $ $Date: 2008-08-15 10:24:20 -0400 (Fri, 15 Aug 2008) $
046     */
047    public class NNTPGroupFolder extends NNTPFolder {
048    
049        // holders for status information returned by the GROUP command.
050        protected int firstArticle = -1;
051    
052        protected int lastArticle = -1;
053    
054        // retrieved articles, mapped by article number.
055        Map articles;
056    
057        // information stored in the newsrc group.
058        NNTPNewsrcGroup groupInfo;
059    
060        /**
061         * Construct a "real" folder representing an NNTP news group.
062         * 
063         * @param parent
064         *            The parent root folder.
065         * @param store
066         *            The Store this folder is attached to.
067         * @param name
068         *            The folder name.
069         * @param groupInfo
070         *            The newsrc group information attached to the newsrc database.
071         *            This contains subscription and article "SEEN" information.
072         */
073        protected NNTPGroupFolder(NNTPRootFolder parent, NNTPStore store, String name, NNTPNewsrcGroup groupInfo) {
074            super(store);
075            // the name and the full name are the same.
076            this.name = name;
077            this.fullName = name;
078            // set the parent appropriately.
079            this.parent = parent = parent;
080            this.groupInfo = groupInfo;
081        }
082    
083        /**
084         * Ping the server and update the group count, first, and last information.
085         * 
086         * @exception MessagingException
087         */
088        private void updateGroupStats() throws MessagingException {
089            // ask the server for information about the group. This is a one-line
090            // reponse with status on
091            // the group, if it exists.
092            NNTPReply reply = connection.sendCommand("GROUP " + name);
093    
094            // explicitly not there?
095            if (reply.getCode() == NNTPReply.NO_SUCH_NEWSGROUP) {
096                throw new FolderNotFoundException(this, "Folder does not exist on server: " + reply);
097            } else if (reply.getCode() != NNTPReply.GROUP_SELECTED) {
098                throw new MessagingException("Error requesting group information: " + reply);
099            }
100    
101            // we've gotten back a good response, now parse out the group specifics
102            // from the
103            // status response.
104    
105            StringTokenizer tokenizer = new StringTokenizer(reply.getMessage());
106    
107            // we should have a least 3 tokens here, in the order "count first
108            // last".
109    
110            // article count
111            if (tokenizer.hasMoreTokens()) {
112                String count = tokenizer.nextToken();
113                try {
114                    messageCount = Integer.parseInt(count);
115                } catch (NumberFormatException e) {
116                    // ignore
117                }
118            }
119    
120            // first article number
121            if (tokenizer.hasMoreTokens()) {
122                String first = tokenizer.nextToken();
123                try {
124                    firstArticle = Integer.parseInt(first);
125                } catch (NumberFormatException e) {
126                    // ignore
127                }
128            }
129    
130            // last article number.
131            if (tokenizer.hasMoreTokens()) {
132                String last = tokenizer.nextToken();
133                try {
134                    lastArticle = Integer.parseInt(last);
135                } catch (NumberFormatException e) {
136                    // ignore
137                }
138            }
139        }
140    
141        /**
142         * Test to see if this folder actually exists. This pings the server for
143         * information about the GROUP and updates the article count and index
144         * information.
145         * 
146         * @return true if the newsgroup exists on the server, false otherwise.
147         * @exception MessagingException
148         */
149        public boolean exists() throws MessagingException {
150    
151            try {
152                // update the group statistics. If the folder doesn't exist, we'll
153                // get an exception that we
154                // can turn into a false reply.
155                updateGroupStats();
156                // updated ok, so it must be there.
157                return true;
158            } catch (FolderNotFoundException e) {
159                return false;
160            }
161        }
162    
163        /**
164         * Ping the NNTP server to check if a newsgroup has any new messages.
165         * 
166         * @return True if the server has new articles from the last time we
167         *         checked. Also returns true if this is the first time we've
168         *         checked.
169         * @exception MessagingException
170         */
171        public boolean hasNewMessages() throws MessagingException {
172            int oldLast = lastArticle;
173            updateGroupStats();
174    
175            return lastArticle > oldLast;
176        }
177    
178        /**
179         * Open the folder for use. This retrieves article count information from
180         * the server.
181         * 
182         * @exception MessagingException
183         */
184        protected void openFolder() throws MessagingException {
185            // update the group specifics, especially the message count.
186            updateGroupStats();
187    
188            // get a cache for retrieved articles
189            articles = new HashMap();
190        }
191    
192        /**
193         * Close the folder, which also clears out the article caches.
194         * 
195         * @exception MessagingException
196         */
197        public void closeFolder() throws MessagingException {
198            // get ride of any retrieve articles, and flip over the open for
199            // business sign.
200            articles = null;
201        }
202    
203        /**
204         * Checks wether the message is in cache, if not will create a new message
205         * object and return it.
206         * 
207         * @see javax.mail.Folder#getMessage(int)
208         */
209        public Message getMessage(int msgNum) throws MessagingException {
210            // Can only be performed on an Open folder
211            checkOpen();
212    
213            // get an object form to look up in the retrieve messages list (oh how I
214            // wish there was
215            // something like Map that could use integer keys directly!).
216            Integer key = new Integer(msgNum);
217            NNTPMessage message = (NNTPMessage) articles.get(key);
218            if (message != null) {
219                // piece of cake!
220                return message;
221            }
222    
223            // we need to suck a message down from the server.
224            // but first, make sure the group is still valid.
225            updateGroupStats();
226    
227            // just send a STAT command to this message. Right now, all we want is
228            // existance proof. We'll
229            // retrieve the other bits when requested.
230            NNTPReply reply = connection.sendCommand("STAT " + Integer.toString(msgNum));
231            if (reply.getCode() != NNTPReply.REQUEST_TEXT_SEPARATELY) {
232                throw new MessagingException("Error retrieving article from NNTP server: " + reply);
233            }
234    
235            // we need to parse out the message id.
236            String response = reply.getMessage();
237    
238            int idStart = response.indexOf('<');
239            int idEnd = response.indexOf('>');
240    
241            // NB:  The "<" and ">" delimiters are required elements of the message id, not just 
242            // delimiters for the sake of the command.  We need to keep these around 
243            message = new NNTPMessage(this, (NNTPStore) store, msgNum, response.substring(idStart, idEnd + 1));
244    
245            // add this to the article cache.
246            articles.put(key, message);
247    
248            return message;
249        }
250    
251        /**
252         * Retrieve all articles in the group.
253         * 
254         * @return An array of all messages in the group.
255         */
256        public Message[] getMessages() throws MessagingException {
257            // Can only be performed on an Open folder
258            checkOpen();
259            
260            // we're going to try first with XHDR, which will allow us to retrieve
261            // everything in one shot. If that
262            // fails, we'll fall back on issing STAT commands for the entire article
263            // range.
264            NNTPReply reply = connection.sendCommand("XHDR Message-ID " + Integer.toString(firstArticle) + "-"
265                    + Integer.toString(lastArticle), NNTPReply.HEAD_FOLLOWS);
266    
267            List messages = new ArrayList();
268    
269            if (reply.getCode() == NNTPReply.HEAD_FOLLOWS) {
270                List lines = reply.getData();
271    
272                for (int i = 0; i < lines.size(); i++) {
273                    String line = (String) lines.get(i);
274    
275                    try {
276                        int pos = line.indexOf(' ');
277                        int articleID = Integer.parseInt(line.substring(0, pos));
278                        String messageID = line.substring(pos + 1);
279                        Integer key = new Integer(articleID);
280                        // see if we have this message cached, If not, create it.
281                        Message message = (Message)articles.get(key);
282                        if (message == null) {
283                            message = new NNTPMessage(this, (NNTPStore) store, key.intValue(), messageID);
284                            articles.put(key, message);
285                        }
286    
287                        messages.add(message);
288    
289                    } catch (NumberFormatException e) {
290                        // should never happen, but just skip this entry if it does.
291                    }
292                }
293            } else {
294                // grumble, we need to stat each article id to see if it
295                // exists....lots of round trips.
296                for (int i = firstArticle; i <= lastArticle; i++) {
297                    try {
298                        messages.add(getMessage(i));
299                    } catch (MessagingException e) {
300                        // just assume if there is an error, it's because the
301                        // message id doesn't exist.
302                    }
303                }
304            }
305    
306            return (Message[]) messages.toArray(new Message[0]);
307        }
308    
309        /**
310         * @see javax.mail.Folder#fetch(javax.mail.Message[],
311         *      javax.mail.FetchProfile)
312         * 
313         * The JavaMail API recommends that this method be overrident to provide a
314         * meaningfull implementation.
315         */
316        public void fetch(Message[] msgs, FetchProfile fp) throws MessagingException {
317            // Can only be performed on an Open folder
318            checkOpen();
319    
320            for (int i = 0; i < msgs.length; i++) {
321                Message msg = msgs[i];
322                // we can only perform this operation for NNTPMessages.
323                if (msg == null || !(msg instanceof NNTPMessage)) {
324                    // we can't fetch if it's the wrong message type
325                    continue;
326                }
327    
328                // fetching both the headers and body?
329                if (fp.contains(FetchProfile.Item.ENVELOPE) && fp.contains(FetchProfile.Item.CONTENT_INFO)) {
330    
331                    // retrive everything
332                    ((NNTPMessage) msg).loadArticle();
333                }
334                // headers only?
335                else if (fp.contains(FetchProfile.Item.ENVELOPE)) {
336                    ((NNTPMessage) msg).loadHeaders();
337                } else if (fp.contains(FetchProfile.Item.CONTENT_INFO)) {
338                    ((NNTPMessage) msg).loadContent();
339                }
340            }
341        }
342    
343        /**
344         * Return the subscription status of this folder.
345         * 
346         * @return true if the folder is marked as subscribed, false for
347         *         unsubscribed.
348         */
349        public boolean isSubscribed() {
350            return groupInfo.isSubscribed();
351        }
352    
353        /**
354         * Set or clear the subscription status of a file.
355         * 
356         * @param flag
357         *            The new subscription state.
358         */
359        public void setSubscribed(boolean flag) {
360            groupInfo.setSubscribed(flag);
361        }
362    
363        /**
364         * Return the "seen" state for an article in a folder.
365         * 
366         * @param article
367         *            The article number.
368         * 
369         * @return true if the article is marked as seen in the newsrc file, false
370         *         for unseen files.
371         */
372        public boolean isSeen(int article) {
373            return groupInfo.isArticleSeen(article);
374        }
375    
376        /**
377         * Set the seen state for an article in a folder.
378         * 
379         * @param article
380         *            The article number.
381         * @param flag
382         *            The new seen state.
383         */
384        public void setSeen(int article, boolean flag) {
385            if (flag) {
386                groupInfo.markArticleSeen(article);
387            } else {
388                groupInfo.markArticleUnseen(article);
389            }
390        }
391    }