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 }