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