org/sonews/daemon/command/PostCommand.java
author cli
Sat May 01 14:27:30 2010 +0200 (2010-05-01)
changeset 28 15d14b110240
parent 19 91dc9acb03ed
permissions -rw-r--r--
Make mailinglist gateway more reliable, probably.
     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 String impliedCapability()
    75   {
    76     return null;
    77   }
    78 
    79   @Override
    80   public boolean isStateful()
    81   {
    82     return true;
    83   }
    84 
    85   /**
    86    * Process the given line String. line.trim() was called by NNTPConnection.
    87    * @param line
    88    * @throws java.io.IOException
    89    * @throws java.sql.SQLException
    90    */
    91   @Override // TODO: Refactor this method to reduce complexity!
    92   public void processLine(NNTPConnection conn, String line, byte[] raw)
    93     throws IOException, StorageBackendException
    94   {
    95     switch(state)
    96     {
    97       case WaitForLineOne:
    98       {
    99         if(line.equalsIgnoreCase("POST"))
   100         {
   101           conn.println("340 send article to be posted. End with <CR-LF>.<CR-LF>");
   102           state = PostState.ReadingHeaders;
   103         }
   104         else
   105         {
   106           conn.println("500 invalid command usage");
   107         }
   108         break;
   109       }
   110       case ReadingHeaders:
   111       {
   112         strHead.append(line);
   113         strHead.append(NNTPConnection.NEWLINE);
   114         
   115         if("".equals(line) || ".".equals(line))
   116         {
   117           // we finally met the blank line
   118           // separating headers from body
   119           
   120           try
   121           {
   122             // Parse the header using the InternetHeader class from JavaMail API
   123             headers = new InternetHeaders(
   124               new ByteArrayInputStream(strHead.toString().trim()
   125                 .getBytes(conn.getCurrentCharset())));
   126 
   127             // add the header entries for the article
   128             article.setHeaders(headers);
   129           }
   130           catch (MessagingException e)
   131           {
   132             e.printStackTrace();
   133             conn.println("500 posting failed - invalid header");
   134             state = PostState.Finished;
   135             break;
   136           }
   137 
   138           // Change charset for reading body; 
   139           // for multipart messages UTF-8 is returned
   140           //conn.setCurrentCharset(article.getBodyCharset());
   141           
   142           state = PostState.ReadingBody;
   143           
   144           if(".".equals(line))
   145           {
   146             // Post an article without body
   147             postArticle(conn, article);
   148             state = PostState.Finished;
   149           }
   150         }
   151         break;
   152       }
   153       case ReadingBody:
   154       {
   155         if(".".equals(line))
   156         {    
   157           // Set some headers needed for Over command
   158           headers.setHeader(Headers.LINES, Integer.toString(lineCount));
   159           headers.setHeader(Headers.BYTES, Long.toString(bodySize));
   160 
   161           byte[] body = bufBody.toByteArray();
   162           if(body.length >= 2)
   163           {
   164             // Remove trailing CRLF
   165             body = Arrays.copyOf(body, body.length - 2);
   166           }
   167           article.setBody(body); // set the article body
   168           
   169           postArticle(conn, article);
   170           state = PostState.Finished;
   171         }
   172         else
   173         {
   174           bodySize += line.length() + 1;
   175           lineCount++;
   176           
   177           // Add line to body buffer
   178           bufBody.write(raw, 0, raw.length);
   179           bufBody.write(NNTPConnection.NEWLINE.getBytes());
   180           
   181           if(bodySize > maxBodySize)
   182           {
   183             conn.println("500 article is too long");
   184             state = PostState.Finished;
   185             break;
   186           }
   187         }
   188         break;
   189       }
   190       default:
   191       {
   192         // Should never happen
   193         Log.get().severe("PostCommand::processLine(): already finished...");
   194       }
   195     }
   196   }
   197   
   198   /**
   199    * Article is a control message and needs special handling.
   200    * @param article
   201    */
   202   private void controlMessage(NNTPConnection conn, Article article)
   203     throws IOException
   204   {
   205     String[] ctrl = article.getHeader(Headers.CONTROL)[0].split(" ");
   206     if(ctrl.length == 2) // "cancel <mid>"
   207     {
   208       try
   209       {
   210         StorageManager.current().delete(ctrl[1]);
   211         
   212         // Move cancel message to "control" group
   213         article.setHeader(Headers.NEWSGROUPS, "control");
   214         StorageManager.current().addArticle(article);
   215         conn.println("240 article cancelled");
   216       }
   217       catch(StorageBackendException ex)
   218       {
   219         Log.get().severe(ex.toString());
   220         conn.println("500 internal server error");
   221       }
   222     }
   223     else
   224     {
   225       conn.println("441 unknown control header");
   226     }
   227   }
   228   
   229   private void supersedeMessage(NNTPConnection conn, Article article)
   230     throws IOException
   231   {
   232     try
   233     {
   234       String oldMsg = article.getHeader(Headers.SUPERSEDES)[0];
   235       StorageManager.current().delete(oldMsg);
   236       StorageManager.current().addArticle(article);
   237       conn.println("240 article replaced");
   238     }
   239     catch(StorageBackendException ex)
   240     {
   241       Log.get().severe(ex.toString());
   242       conn.println("500 internal server error");
   243     }
   244   }
   245   
   246   private void postArticle(NNTPConnection conn, Article article)
   247     throws IOException
   248   {
   249     if(article.getHeader(Headers.CONTROL)[0].length() > 0)
   250     {
   251       controlMessage(conn, article);
   252     }
   253     else if(article.getHeader(Headers.SUPERSEDES)[0].length() > 0)
   254     {
   255       supersedeMessage(conn, article);
   256     }
   257     else // Post the article regularily
   258     {
   259       // Circle check; note that Path can already contain the hostname here
   260       String host = Config.inst().get(Config.HOSTNAME, "localhost");
   261       if(article.getHeader(Headers.PATH)[0].indexOf(host + "!", 1) > 0)
   262       {
   263         Log.get().info(article.getMessageID() + " skipped for host " + host);
   264         conn.println("441 I know this article already");
   265         return;
   266       }
   267 
   268       // Try to create the article in the database or post it to
   269       // appropriate mailing list
   270       try
   271       {
   272         boolean success = false;
   273         String[] groupnames = article.getHeader(Headers.NEWSGROUPS)[0].split(",");
   274         for(String groupname : groupnames)
   275         {          
   276           Group group = StorageManager.current().getGroup(groupname);
   277           if(group != null && !group.isDeleted())
   278           {
   279             if(group.isMailingList() && !conn.isLocalConnection())
   280             {
   281               // Send to mailing list; the Dispatcher writes 
   282               // statistics to database
   283               Dispatcher.toList(article, group.getName());
   284               success = true;
   285             }
   286             else
   287             {
   288               // Store in database
   289               if(!StorageManager.current().isArticleExisting(article.getMessageID()))
   290               {
   291                 StorageManager.current().addArticle(article);
   292 
   293                 // Log this posting to statistics
   294                 Stats.getInstance().mailPosted(
   295                   article.getHeader(Headers.NEWSGROUPS)[0]);
   296               }
   297               success = true;
   298             }
   299           }
   300         } // end for
   301 
   302         if(success)
   303         {
   304           conn.println("240 article posted ok");
   305           FeedManager.queueForPush(article);
   306         }
   307         else
   308         {
   309           conn.println("441 newsgroup not found");
   310         }
   311       }
   312       catch(AddressException ex)
   313       {
   314         Log.get().warning(ex.getMessage());
   315         conn.println("441 invalid sender address");
   316       }
   317       catch(MessagingException ex)
   318       {
   319         // A MessageException is thrown when the sender email address is
   320         // invalid or something is wrong with the SMTP server.
   321         System.err.println(ex.getLocalizedMessage());
   322         conn.println("441 " + ex.getClass().getCanonicalName() + ": " + ex.getLocalizedMessage());
   323       }
   324       catch(StorageBackendException ex)
   325       {
   326         ex.printStackTrace();
   327         conn.println("500 internal server error");
   328       }
   329     }
   330   }
   331 
   332 }