org/sonews/daemon/command/PostCommand.java
author chris <chris@marvin>
Fri Jun 26 16:48:50 2009 +0200 (2009-06-26)
changeset 1 6fceb66e1ad7
child 3 2fdc9cc89502
permissions -rw-r--r--
Hooray... sonews/0.5.0 final

HG: Enter commit message. Lines beginning with 'HG:' are removed.
HG: Remove all lines to abort the collapse operation.
     1 /*
     2  *   SONEWS News Server
     3  *   see AUTHORS for the list of contributors
     4  *
     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.
     9  *
    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.
    14  *
    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/>.
    17  */
    18 
    19 package org.sonews.daemon.command;
    20 
    21 import java.io.IOException;
    22 
    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;
    42 
    43 /**
    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
    46  * to handle.
    47  * @author Christian Lins
    48  * @since sonews/0.5.0
    49  */
    50 public class PostCommand extends AbstractCommand
    51 {
    52   
    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();
    62   
    63   public PostCommand(final NNTPConnection conn)
    64   {
    65     super(conn);
    66   }
    67 
    68   @Override
    69   public boolean hasFinished()
    70   {
    71     return this.state == PostState.Finished;
    72   }
    73 
    74   /**
    75    * Process the given line String. line.trim() was called by NNTPConnection.
    76    * @param line
    77    * @throws java.io.IOException
    78    * @throws java.sql.SQLException
    79    */
    80   @Override // TODO: Refactor this method to reduce complexity!
    81   public void processLine(String line)
    82     throws IOException, SQLException
    83   {
    84     switch(state)
    85     {
    86       case WaitForLineOne:
    87       {
    88         if(line.equalsIgnoreCase("POST"))
    89         {
    90           printStatus(340, "send article to be posted. End with <CR-LF>.<CR-LF>");
    91           state = PostState.ReadingHeaders;
    92         }
    93         else
    94         {
    95           printStatus(500, "invalid command usage");
    96         }
    97         break;
    98       }
    99       case ReadingHeaders:
   100       {
   101         strHead.append(line);
   102         strHead.append(NNTPConnection.NEWLINE);
   103         
   104         if("".equals(line) || ".".equals(line))
   105         {
   106           // we finally met the blank line
   107           // separating headers from body
   108           
   109           try
   110           {
   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())));
   115 
   116             // add the header entries for the article
   117             article.setHeaders(headers);
   118           }
   119           catch (MessagingException e)
   120           {
   121             e.printStackTrace();
   122             printStatus(500, "posting failed - invalid header");
   123             state = PostState.Finished;
   124             break;
   125           }
   126 
   127           // Change charset for reading body; 
   128           // for multipart messages UTF-8 is returned
   129           connection.setCurrentCharset(article.getBodyCharset());
   130           
   131           state = PostState.ReadingBody;
   132           
   133           if(".".equals(line))
   134           {
   135             // Post an article without body
   136             postArticle(article);
   137             state = PostState.Finished;
   138           }
   139         }
   140         break;
   141       }
   142       case ReadingBody:
   143       {
   144         if(".".equals(line))
   145         {    
   146           // Set some headers needed for Over command
   147           headers.setHeader(Headers.LINES, Integer.toString(lineCount));
   148           headers.setHeader(Headers.BYTES, Long.toString(bodySize));
   149           
   150           if(strBody.length() >= 2)
   151           {
   152             strBody.deleteCharAt(strBody.length() - 1); // Remove last newline
   153             strBody.deleteCharAt(strBody.length() - 1); // Remove last CR
   154           }
   155           article.setBody(strBody.toString()); // set the article body
   156           
   157           postArticle(article);
   158           state = PostState.Finished;
   159         }
   160         else
   161         {
   162           bodySize += line.length() + 1;
   163           lineCount++;
   164           
   165           // Add line to body buffer
   166           strBody.append(line);
   167           strBody.append(NNTPConnection.NEWLINE);
   168           
   169           if(bodySize > maxBodySize)
   170           {
   171             printStatus(500, "article is too long");
   172             state = PostState.Finished;
   173             break;
   174           }
   175           
   176           // Check if this message is a MIME-multipart message and needs a
   177           // charset change
   178           try
   179           {
   180             line = line.toLowerCase(Locale.ENGLISH);
   181             if(line.startsWith(Headers.CONTENT_TYPE))
   182             {
   183               int idxStart = line.indexOf("charset=") + "charset=".length();
   184               int idxEnd   = line.indexOf(";", idxStart);
   185               if(idxEnd < 0)
   186               {
   187                 idxEnd = line.length();
   188               }
   189 
   190               if(idxStart > 0)
   191               {
   192                 String charsetName = line.substring(idxStart, idxEnd);
   193                 if(charsetName.length() > 0 && charsetName.charAt(0) == '"')
   194                 {
   195                   charsetName = charsetName.substring(1, charsetName.length() - 1);
   196                 }
   197 
   198                 try
   199                 {
   200                   connection.setCurrentCharset(Charset.forName(charsetName));
   201                 }
   202                 catch(IllegalCharsetNameException ex)
   203                 {
   204                   Log.msg("PostCommand: " + ex, false);
   205                 }
   206                 catch(UnsupportedCharsetException ex)
   207                 {
   208                   Log.msg("PostCommand: " + ex, false);
   209                 }
   210               } // if(idxStart > 0)
   211             }
   212           }
   213           catch(Exception ex)
   214           {
   215             ex.printStackTrace();
   216           }
   217         }
   218         break;
   219       }
   220       default:
   221         Log.msg("PostCommand::processLine(): already finished...", false);
   222     }
   223   }
   224   
   225   /**
   226    * Article is a control message and needs special handling.
   227    * @param article
   228    */
   229   private void controlMessage(Article article)
   230     throws IOException
   231   {
   232     String[] ctrl = article.getHeader(Headers.CONTROL)[0].split(" ");
   233     if(ctrl.length == 2) // "cancel <mid>"
   234     {
   235       try
   236       {
   237         Database.getInstance().delete(ctrl[1]);
   238         
   239         // Move cancel message to "control" group
   240         article.setHeader(Headers.NEWSGROUPS, "control");
   241         Database.getInstance().addArticle(article);
   242         printStatus(240, "article cancelled");
   243       }
   244       catch(SQLException ex)
   245       {
   246         Log.msg(ex, false);
   247         printStatus(500, "internal server error");
   248       }
   249     }
   250     else
   251     {
   252       printStatus(441, "unknown Control header");
   253     }
   254   }
   255   
   256   private void supersedeMessage(Article article)
   257     throws IOException
   258   {
   259     try
   260     {
   261       String oldMsg = article.getHeader(Headers.SUPERSEDES)[0];
   262       Database.getInstance().delete(oldMsg);
   263       Database.getInstance().addArticle(article);
   264       printStatus(240, "article replaced");
   265     }
   266     catch(SQLException ex)
   267     {
   268       Log.msg(ex, false);
   269       printStatus(500, "internal server error");
   270     }
   271   }
   272   
   273   private void postArticle(Article article) 
   274     throws IOException
   275   {
   276     if(article.getHeader(Headers.CONTROL)[0].length() > 0)
   277     {
   278       controlMessage(article);
   279     }
   280     else if(article.getHeader(Headers.SUPERSEDES)[0].length() > 0)
   281     {
   282       supersedeMessage(article);
   283     }
   284     else // Post the article regularily
   285     {
   286       // Try to create the article in the database or post it to
   287       // appropriate mailing list
   288       try
   289       {
   290         boolean success = false;
   291         String[] groupnames = article.getHeader(Headers.NEWSGROUPS)[0].split(",");
   292         for(String groupname : groupnames)
   293         {
   294           Group group = Database.getInstance().getGroup(groupname);
   295           if(group != null)
   296           {
   297             if(group.isMailingList() && !connection.isLocalConnection())
   298             {
   299               // Send to mailing list; the Dispatcher writes 
   300               // statistics to database
   301               Dispatcher.toList(article);
   302               success = true;
   303             }
   304             else
   305             {
   306               // Store in database
   307               if(!Database.getInstance().isArticleExisting(article.getMessageID()))
   308               {
   309                 Database.getInstance().addArticle(article);
   310 
   311                 // Log this posting to statistics
   312                 Stats.getInstance().mailPosted(
   313                   article.getHeader(Headers.NEWSGROUPS)[0]);
   314               }
   315               success = true;
   316             }
   317           }
   318         } // end for
   319 
   320         if(success)
   321         {
   322           printStatus(240, "article posted ok");
   323           FeedManager.queueForPush(article);
   324         }
   325         else
   326         {
   327           printStatus(441, "newsgroup not found");
   328         }
   329       }
   330       catch(AddressException ex)
   331       {
   332         Log.msg(ex.getMessage(), true);
   333         printStatus(441, "invalid sender address");
   334       }
   335       catch(MessagingException ex)
   336       {
   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());
   341       }
   342       catch(SQLException ex)
   343       {
   344         ex.printStackTrace();
   345         printStatus(500, "internal server error");
   346       }
   347     }
   348   }
   349 
   350 }