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