org/sonews/daemon/command/PostCommand.java
author cli
Thu Aug 20 16:57:38 2009 +0200 (2009-08-20)
changeset 14 efce4ec25564
parent 3 2fdc9cc89502
child 15 f2293e8566f5
permissions -rw-r--r--
Fix #548: API change; changed parameter type of Storage.getGroupsForList()
     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 import java.io.ByteArrayInputStream;
    23 import java.io.ByteArrayOutputStream;
    24 import java.sql.SQLException;
    25 import java.util.Arrays;
    26 import javax.mail.MessagingException;
    27 import javax.mail.internet.AddressException;
    28 import javax.mail.internet.InternetHeaders;
    29 import org.sonews.config.Config;
    30 import org.sonews.util.Log;
    31 import org.sonews.mlgw.Dispatcher;
    32 import org.sonews.storage.Article;
    33 import org.sonews.storage.Group;
    34 import org.sonews.daemon.NNTPConnection;
    35 import org.sonews.storage.Headers;
    36 import org.sonews.storage.StorageBackendException;
    37 import org.sonews.storage.StorageManager;
    38 import org.sonews.feed.FeedManager;
    39 import org.sonews.util.Stats;
    40 
    41 /**
    42  * Implementation of the POST command. This command requires multiple lines
    43  * from the client, so the handling of asynchronous reading is a little tricky
    44  * to handle.
    45  * @author Christian Lins
    46  * @since sonews/0.5.0
    47  */
    48 public class PostCommand implements Command
    49 {
    50   
    51   private final Article article   = new Article();
    52   private int           lineCount = 0;
    53   private long          bodySize  = 0;
    54   private InternetHeaders headers = null;
    55   private long          maxBodySize  = 
    56     Config.inst().get(Config.ARTICLE_MAXSIZE, 128) * 1024L; // Size in bytes
    57   private PostState     state     = PostState.WaitForLineOne;
    58   private final ByteArrayOutputStream bufBody   = new ByteArrayOutputStream();
    59   private final StringBuilder         strHead   = new StringBuilder();
    60 
    61   @Override
    62   public String[] getSupportedCommandStrings()
    63   {
    64     return new String[]{"POST"};
    65   }
    66 
    67   @Override
    68   public boolean hasFinished()
    69   {
    70     return this.state == PostState.Finished;
    71   }
    72 
    73   @Override
    74   public boolean isStateful()
    75   {
    76     return true;
    77   }
    78 
    79   /**
    80    * Process the given line String. line.trim() was called by NNTPConnection.
    81    * @param line
    82    * @throws java.io.IOException
    83    * @throws java.sql.SQLException
    84    */
    85   @Override // TODO: Refactor this method to reduce complexity!
    86   public void processLine(NNTPConnection conn, String line, byte[] raw)
    87     throws IOException, StorageBackendException
    88   {
    89     switch(state)
    90     {
    91       case WaitForLineOne:
    92       {
    93         if(line.equalsIgnoreCase("POST"))
    94         {
    95           conn.println("340 send article to be posted. End with <CR-LF>.<CR-LF>");
    96           state = PostState.ReadingHeaders;
    97         }
    98         else
    99         {
   100           conn.println("500 invalid command usage");
   101         }
   102         break;
   103       }
   104       case ReadingHeaders:
   105       {
   106         strHead.append(line);
   107         strHead.append(NNTPConnection.NEWLINE);
   108         
   109         if("".equals(line) || ".".equals(line))
   110         {
   111           // we finally met the blank line
   112           // separating headers from body
   113           
   114           try
   115           {
   116             // Parse the header using the InternetHeader class from JavaMail API
   117             headers = new InternetHeaders(
   118               new ByteArrayInputStream(strHead.toString().trim()
   119                 .getBytes(conn.getCurrentCharset())));
   120 
   121             // add the header entries for the article
   122             article.setHeaders(headers);
   123           }
   124           catch (MessagingException e)
   125           {
   126             e.printStackTrace();
   127             conn.println("500 posting failed - invalid header");
   128             state = PostState.Finished;
   129             break;
   130           }
   131 
   132           // Change charset for reading body; 
   133           // for multipart messages UTF-8 is returned
   134           //conn.setCurrentCharset(article.getBodyCharset());
   135           
   136           state = PostState.ReadingBody;
   137           
   138           if(".".equals(line))
   139           {
   140             // Post an article without body
   141             postArticle(conn, article);
   142             state = PostState.Finished;
   143           }
   144         }
   145         break;
   146       }
   147       case ReadingBody:
   148       {
   149         if(".".equals(line))
   150         {    
   151           // Set some headers needed for Over command
   152           headers.setHeader(Headers.LINES, Integer.toString(lineCount));
   153           headers.setHeader(Headers.BYTES, Long.toString(bodySize));
   154 
   155           byte[] body = bufBody.toByteArray();
   156           if(body.length >= 2)
   157           {
   158             // Remove trailing CRLF
   159             body = Arrays.copyOf(body, body.length - 2);
   160           }
   161           article.setBody(body); // set the article body
   162           
   163           postArticle(conn, article);
   164           state = PostState.Finished;
   165         }
   166         else
   167         {
   168           bodySize += line.length() + 1;
   169           lineCount++;
   170           
   171           // Add line to body buffer
   172           bufBody.write(raw, 0, raw.length);
   173           bufBody.write(NNTPConnection.NEWLINE.getBytes());
   174           
   175           if(bodySize > maxBodySize)
   176           {
   177             conn.println("500 article is too long");
   178             state = PostState.Finished;
   179             break;
   180           }
   181           
   182           // Check if this message is a MIME-multipart message and needs a
   183           // charset change
   184           /*try
   185           {
   186             line = line.toLowerCase(Locale.ENGLISH);
   187             if(line.startsWith(Headers.CONTENT_TYPE))
   188             {
   189               int idxStart = line.indexOf("charset=") + "charset=".length();
   190               int idxEnd   = line.indexOf(";", idxStart);
   191               if(idxEnd < 0)
   192               {
   193                 idxEnd = line.length();
   194               }
   195 
   196               if(idxStart > 0)
   197               {
   198                 String charsetName = line.substring(idxStart, idxEnd);
   199                 if(charsetName.length() > 0 && charsetName.charAt(0) == '"')
   200                 {
   201                   charsetName = charsetName.substring(1, charsetName.length() - 1);
   202                 }
   203 
   204                 try
   205                 {
   206                   conn.setCurrentCharset(Charset.forName(charsetName));
   207                 }
   208                 catch(IllegalCharsetNameException ex)
   209                 {
   210                   Log.msg("PostCommand: " + ex, false);
   211                 }
   212                 catch(UnsupportedCharsetException ex)
   213                 {
   214                   Log.msg("PostCommand: " + ex, false);
   215                 }
   216               } // if(idxStart > 0)
   217             }
   218           }
   219           catch(Exception ex)
   220           {
   221             ex.printStackTrace();
   222           }*/
   223         }
   224         break;
   225       }
   226       default:
   227       {
   228         // Should never happen
   229         Log.msg("PostCommand::processLine(): already finished...", false);
   230       }
   231     }
   232   }
   233   
   234   /**
   235    * Article is a control message and needs special handling.
   236    * @param article
   237    */
   238   private void controlMessage(NNTPConnection conn, Article article)
   239     throws IOException
   240   {
   241     String[] ctrl = article.getHeader(Headers.CONTROL)[0].split(" ");
   242     if(ctrl.length == 2) // "cancel <mid>"
   243     {
   244       try
   245       {
   246         StorageManager.current().delete(ctrl[1]);
   247         
   248         // Move cancel message to "control" group
   249         article.setHeader(Headers.NEWSGROUPS, "control");
   250         StorageManager.current().addArticle(article);
   251         conn.println("240 article cancelled");
   252       }
   253       catch(StorageBackendException ex)
   254       {
   255         Log.msg(ex, false);
   256         conn.println("500 internal server error");
   257       }
   258     }
   259     else
   260     {
   261       conn.println("441 unknown control header");
   262     }
   263   }
   264   
   265   private void supersedeMessage(NNTPConnection conn, Article article)
   266     throws IOException
   267   {
   268     try
   269     {
   270       String oldMsg = article.getHeader(Headers.SUPERSEDES)[0];
   271       StorageManager.current().delete(oldMsg);
   272       StorageManager.current().addArticle(article);
   273       conn.println("240 article replaced");
   274     }
   275     catch(StorageBackendException ex)
   276     {
   277       Log.msg(ex, false);
   278       conn.println("500 internal server error");
   279     }
   280   }
   281   
   282   private void postArticle(NNTPConnection conn, Article article)
   283     throws IOException
   284   {
   285     if(article.getHeader(Headers.CONTROL)[0].length() > 0)
   286     {
   287       controlMessage(conn, article);
   288     }
   289     else if(article.getHeader(Headers.SUPERSEDES)[0].length() > 0)
   290     {
   291       supersedeMessage(conn, article);
   292     }
   293     else // Post the article regularily
   294     {
   295       // Try to create the article in the database or post it to
   296       // appropriate mailing list
   297       try
   298       {
   299         boolean success = false;
   300         String[] groupnames = article.getHeader(Headers.NEWSGROUPS)[0].split(",");
   301         for(String groupname : groupnames)
   302         {          
   303           Group group = StorageManager.current().getGroup(groupname);
   304           if(group != null && !group.isDeleted())
   305           {
   306             if(group.isMailingList() && !conn.isLocalConnection())
   307             {
   308               // Send to mailing list; the Dispatcher writes 
   309               // statistics to database
   310               Dispatcher.toList(article, group.getName());
   311               success = true;
   312             }
   313             else
   314             {
   315               // Store in database
   316               if(!StorageManager.current().isArticleExisting(article.getMessageID()))
   317               {
   318                 StorageManager.current().addArticle(article);
   319 
   320                 // Log this posting to statistics
   321                 Stats.getInstance().mailPosted(
   322                   article.getHeader(Headers.NEWSGROUPS)[0]);
   323               }
   324               success = true;
   325             }
   326           }
   327         } // end for
   328 
   329         if(success)
   330         {
   331           conn.println("240 article posted ok");
   332           FeedManager.queueForPush(article);
   333         }
   334         else
   335         {
   336           conn.println("441 newsgroup not found");
   337         }
   338       }
   339       catch(AddressException ex)
   340       {
   341         Log.msg(ex.getMessage(), true);
   342         conn.println("441 invalid sender address");
   343       }
   344       catch(MessagingException ex)
   345       {
   346         // A MessageException is thrown when the sender email address is
   347         // invalid or something is wrong with the SMTP server.
   348         System.err.println(ex.getLocalizedMessage());
   349         conn.println("441 " + ex.getClass().getCanonicalName() + ": " + ex.getLocalizedMessage());
   350       }
   351       catch(StorageBackendException ex)
   352       {
   353         ex.printStackTrace();
   354         conn.println("500 internal server error");
   355       }
   356     }
   357   }
   358 
   359 }