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