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