org/sonews/daemon/command/PostCommand.java
author cli
Fri Aug 21 17:33:15 2009 +0200 (2009-08-21)
changeset 18 7e527fdf0fa8
parent 15 f2293e8566f5
child 19 91dc9acb03ed
permissions -rw-r--r--
Fix for #549.
     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         break;
   183       }
   184       default:
   185       {
   186         // Should never happen
   187         Log.get().severe("PostCommand::processLine(): already finished...");
   188       }
   189     }
   190   }
   191   
   192   /**
   193    * Article is a control message and needs special handling.
   194    * @param article
   195    */
   196   private void controlMessage(NNTPConnection conn, Article article)
   197     throws IOException
   198   {
   199     String[] ctrl = article.getHeader(Headers.CONTROL)[0].split(" ");
   200     if(ctrl.length == 2) // "cancel <mid>"
   201     {
   202       try
   203       {
   204         StorageManager.current().delete(ctrl[1]);
   205         
   206         // Move cancel message to "control" group
   207         article.setHeader(Headers.NEWSGROUPS, "control");
   208         StorageManager.current().addArticle(article);
   209         conn.println("240 article cancelled");
   210       }
   211       catch(StorageBackendException ex)
   212       {
   213         Log.get().severe(ex.toString());
   214         conn.println("500 internal server error");
   215       }
   216     }
   217     else
   218     {
   219       conn.println("441 unknown control header");
   220     }
   221   }
   222   
   223   private void supersedeMessage(NNTPConnection conn, Article article)
   224     throws IOException
   225   {
   226     try
   227     {
   228       String oldMsg = article.getHeader(Headers.SUPERSEDES)[0];
   229       StorageManager.current().delete(oldMsg);
   230       StorageManager.current().addArticle(article);
   231       conn.println("240 article replaced");
   232     }
   233     catch(StorageBackendException ex)
   234     {
   235       Log.get().severe(ex.toString());
   236       conn.println("500 internal server error");
   237     }
   238   }
   239   
   240   private void postArticle(NNTPConnection conn, Article article)
   241     throws IOException
   242   {
   243     if(article.getHeader(Headers.CONTROL)[0].length() > 0)
   244     {
   245       controlMessage(conn, article);
   246     }
   247     else if(article.getHeader(Headers.SUPERSEDES)[0].length() > 0)
   248     {
   249       supersedeMessage(conn, article);
   250     }
   251     else // Post the article regularily
   252     {
   253       // Circle check; note that Path can already contain the hostname here
   254       String host = Config.inst().get(Config.HOSTNAME, "localhost");
   255       if(article.getHeader(Headers.PATH)[0].contains(host + "!"))
   256       {
   257         Log.get().info(article.getMessageID() + " skipped for host " + host);
   258         conn.println("441 I know this article already");
   259         return;
   260       }
   261 
   262       // Try to create the article in the database or post it to
   263       // appropriate mailing list
   264       try
   265       {
   266         boolean success = false;
   267         String[] groupnames = article.getHeader(Headers.NEWSGROUPS)[0].split(",");
   268         for(String groupname : groupnames)
   269         {          
   270           Group group = StorageManager.current().getGroup(groupname);
   271           if(group != null && !group.isDeleted())
   272           {
   273             if(group.isMailingList() && !conn.isLocalConnection())
   274             {
   275               // Send to mailing list; the Dispatcher writes 
   276               // statistics to database
   277               Dispatcher.toList(article, group.getName());
   278               success = true;
   279             }
   280             else
   281             {
   282               // Store in database
   283               if(!StorageManager.current().isArticleExisting(article.getMessageID()))
   284               {
   285                 StorageManager.current().addArticle(article);
   286 
   287                 // Log this posting to statistics
   288                 Stats.getInstance().mailPosted(
   289                   article.getHeader(Headers.NEWSGROUPS)[0]);
   290               }
   291               success = true;
   292             }
   293           }
   294         } // end for
   295 
   296         if(success)
   297         {
   298           conn.println("240 article posted ok");
   299           FeedManager.queueForPush(article);
   300         }
   301         else
   302         {
   303           conn.println("441 newsgroup not found");
   304         }
   305       }
   306       catch(AddressException ex)
   307       {
   308         Log.get().warning(ex.getMessage());
   309         conn.println("441 invalid sender address");
   310       }
   311       catch(MessagingException ex)
   312       {
   313         // A MessageException is thrown when the sender email address is
   314         // invalid or something is wrong with the SMTP server.
   315         System.err.println(ex.getLocalizedMessage());
   316         conn.println("441 " + ex.getClass().getCanonicalName() + ": " + ex.getLocalizedMessage());
   317       }
   318       catch(StorageBackendException ex)
   319       {
   320         ex.printStackTrace();
   321         conn.println("500 internal server error");
   322       }
   323     }
   324   }
   325 
   326 }