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