3 * see AUTHORS for the list of contributors
5 * This program is free software: you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation, either version 3 of the License, or
8 * (at your option) any later version.
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
15 * You should have received a copy of the GNU General Public License
16 * along with this program. If not, see <http://www.gnu.org/licenses/>.
19 package org.sonews.daemon.command;
21 import java.io.IOException;
23 import java.io.ByteArrayInputStream;
24 import java.nio.charset.Charset;
25 import java.nio.charset.IllegalCharsetNameException;
26 import java.nio.charset.UnsupportedCharsetException;
27 import java.sql.SQLException;
28 import java.util.Locale;
29 import javax.mail.MessagingException;
30 import javax.mail.internet.AddressException;
31 import javax.mail.internet.InternetHeaders;
32 import org.sonews.daemon.Config;
33 import org.sonews.util.Log;
34 import org.sonews.mlgw.Dispatcher;
35 import org.sonews.daemon.storage.Article;
36 import org.sonews.daemon.storage.Database;
37 import org.sonews.daemon.storage.Group;
38 import org.sonews.daemon.NNTPConnection;
39 import org.sonews.daemon.storage.Headers;
40 import org.sonews.feed.FeedManager;
41 import org.sonews.util.Stats;
44 * Implementation of the POST command. This command requires multiple lines
45 * from the client, so the handling of asynchronous reading is a little tricky
47 * @author Christian Lins
50 public class PostCommand extends AbstractCommand
53 private final Article article = new Article();
54 private int lineCount = 0;
55 private long bodySize = 0;
56 private InternetHeaders headers = null;
57 private long maxBodySize =
58 Config.getInstance().get(Config.ARTICLE_MAXSIZE, 128) * 1024L; // Size in bytes
59 private PostState state = PostState.WaitForLineOne;
60 private final StringBuilder strBody = new StringBuilder();
61 private final StringBuilder strHead = new StringBuilder();
63 public PostCommand(final NNTPConnection conn)
69 public boolean hasFinished()
71 return this.state == PostState.Finished;
75 * Process the given line String. line.trim() was called by NNTPConnection.
77 * @throws java.io.IOException
78 * @throws java.sql.SQLException
80 @Override // TODO: Refactor this method to reduce complexity!
81 public void processLine(String line)
82 throws IOException, SQLException
88 if(line.equalsIgnoreCase("POST"))
90 printStatus(340, "send article to be posted. End with <CR-LF>.<CR-LF>");
91 state = PostState.ReadingHeaders;
95 printStatus(500, "invalid command usage");
101 strHead.append(line);
102 strHead.append(NNTPConnection.NEWLINE);
104 if("".equals(line) || ".".equals(line))
106 // we finally met the blank line
107 // separating headers from body
111 // Parse the header using the InternetHeader class from JavaMail API
112 headers = new InternetHeaders(
113 new ByteArrayInputStream(strHead.toString().trim()
114 .getBytes(connection.getCurrentCharset())));
116 // add the header entries for the article
117 article.setHeaders(headers);
119 catch (MessagingException e)
122 printStatus(500, "posting failed - invalid header");
123 state = PostState.Finished;
127 // Change charset for reading body;
128 // for multipart messages UTF-8 is returned
129 connection.setCurrentCharset(article.getBodyCharset());
131 state = PostState.ReadingBody;
135 // Post an article without body
136 postArticle(article);
137 state = PostState.Finished;
146 // Set some headers needed for Over command
147 headers.setHeader(Headers.LINES, Integer.toString(lineCount));
148 headers.setHeader(Headers.BYTES, Long.toString(bodySize));
150 if(strBody.length() >= 2)
152 strBody.deleteCharAt(strBody.length() - 1); // Remove last newline
153 strBody.deleteCharAt(strBody.length() - 1); // Remove last CR
155 article.setBody(strBody.toString()); // set the article body
157 postArticle(article);
158 state = PostState.Finished;
162 bodySize += line.length() + 1;
165 // Add line to body buffer
166 strBody.append(line);
167 strBody.append(NNTPConnection.NEWLINE);
169 if(bodySize > maxBodySize)
171 printStatus(500, "article is too long");
172 state = PostState.Finished;
176 // Check if this message is a MIME-multipart message and needs a
180 line = line.toLowerCase(Locale.ENGLISH);
181 if(line.startsWith(Headers.CONTENT_TYPE))
183 int idxStart = line.indexOf("charset=") + "charset=".length();
184 int idxEnd = line.indexOf(";", idxStart);
187 idxEnd = line.length();
192 String charsetName = line.substring(idxStart, idxEnd);
193 if(charsetName.length() > 0 && charsetName.charAt(0) == '"')
195 charsetName = charsetName.substring(1, charsetName.length() - 1);
200 connection.setCurrentCharset(Charset.forName(charsetName));
202 catch(IllegalCharsetNameException ex)
204 Log.msg("PostCommand: " + ex, false);
206 catch(UnsupportedCharsetException ex)
208 Log.msg("PostCommand: " + ex, false);
210 } // if(idxStart > 0)
215 ex.printStackTrace();
221 Log.msg("PostCommand::processLine(): already finished...", false);
226 * Article is a control message and needs special handling.
229 private void controlMessage(Article article)
232 String[] ctrl = article.getHeader(Headers.CONTROL)[0].split(" ");
233 if(ctrl.length == 2) // "cancel <mid>"
237 Database.getInstance().delete(ctrl[1]);
239 // Move cancel message to "control" group
240 article.setHeader(Headers.NEWSGROUPS, "control");
241 Database.getInstance().addArticle(article);
242 printStatus(240, "article cancelled");
244 catch(SQLException ex)
247 printStatus(500, "internal server error");
252 printStatus(441, "unknown Control header");
256 private void supersedeMessage(Article article)
261 String oldMsg = article.getHeader(Headers.SUPERSEDES)[0];
262 Database.getInstance().delete(oldMsg);
263 Database.getInstance().addArticle(article);
264 printStatus(240, "article replaced");
266 catch(SQLException ex)
269 printStatus(500, "internal server error");
273 private void postArticle(Article article)
276 if(article.getHeader(Headers.CONTROL)[0].length() > 0)
278 controlMessage(article);
280 else if(article.getHeader(Headers.SUPERSEDES)[0].length() > 0)
282 supersedeMessage(article);
284 else // Post the article regularily
286 // Try to create the article in the database or post it to
287 // appropriate mailing list
290 boolean success = false;
291 String[] groupnames = article.getHeader(Headers.NEWSGROUPS)[0].split(",");
292 for(String groupname : groupnames)
294 Group group = Database.getInstance().getGroup(groupname);
297 if(group.isMailingList() && !connection.isLocalConnection())
299 // Send to mailing list; the Dispatcher writes
300 // statistics to database
301 Dispatcher.toList(article);
307 if(!Database.getInstance().isArticleExisting(article.getMessageID()))
309 Database.getInstance().addArticle(article);
311 // Log this posting to statistics
312 Stats.getInstance().mailPosted(
313 article.getHeader(Headers.NEWSGROUPS)[0]);
322 printStatus(240, "article posted ok");
323 FeedManager.queueForPush(article);
327 printStatus(441, "newsgroup not found");
330 catch(AddressException ex)
332 Log.msg(ex.getMessage(), true);
333 printStatus(441, "invalid sender address");
335 catch(MessagingException ex)
337 // A MessageException is thrown when the sender email address is
338 // invalid or something is wrong with the SMTP server.
339 System.err.println(ex.getLocalizedMessage());
340 printStatus(441, ex.getClass().getCanonicalName() + ": " + ex.getLocalizedMessage());
342 catch(SQLException ex)
344 ex.printStackTrace();
345 printStatus(500, "internal server error");