blob: 7ec2efb46d6cc606ed0a15b853e2d0627833aefb [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.geronimo.javamail.transport.nntp;
import java.io.BufferedReader;
import java.io.BufferedOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintStream;
import java.io.PrintWriter;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.InetAddress;
import java.net.Socket;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.StringTokenizer;
import javax.mail.Message;
import javax.mail.MessagingException;
import javax.mail.Session;
import org.apache.geronimo.javamail.authentication.ClientAuthenticator;
import org.apache.geronimo.javamail.authentication.AuthenticatorFactory;
import org.apache.geronimo.javamail.util.MailConnection;
import org.apache.geronimo.javamail.util.MIMEOutputStream;
import org.apache.geronimo.javamail.util.ProtocolProperties;
import org.apache.geronimo.mail.util.Base64;
import org.apache.geronimo.mail.util.SessionUtil;
/**
* Simple implementation of NNTP transport. Just does plain RFC977-ish delivery.
*
* @version $Rev$ $Date$
*/
public class NNTPConnection extends MailConnection {
/**
* constants for EOL termination
*/
protected static final char CR = '\r';
protected static final char LF = '\n';
/**
* property keys for protocol properties.
*/
protected static final int DEFAULT_NNTP_PORT = 119;
// does the server support posting?
protected boolean postingAllowed = true;
// different authentication mechanisms
protected boolean authInfoUserAllowed = false;
protected boolean authInfoSaslAllowed = false;
// the last response line received from the server.
protected NNTPReply lastServerResponse = null;
// the welcome string from the server.
protected String welcomeString = null;
// input reader wrapped around the socket input stream
protected BufferedReader reader;
// output writer wrapped around the socket output stream.
protected PrintWriter writer;
/**
* Normal constructor for an NNTPConnection() object.
*
* @param props The property bundle for this protocol instance.
*/
public NNTPConnection(ProtocolProperties props) {
super(props);
}
/**
* Connect to the server and do the initial handshaking.
*
* @param host The target host name.
* @param port The target port
* @param username The connection username (can be null)
* @param password The authentication password (can be null).
*
* @return true if we were able to obtain a connection and
* authenticate.
* @exception MessagingException
*/
public boolean protocolConnect(String host, int port, String username, String password) throws MessagingException {
super.protocolConnect(host, port, username, password);
// create socket and connect to server.
getConnection();
// receive welcoming message
getWelcome();
return true;
}
/**
* Create a transport connection object and connect it to the
* target server.
*
* @exception MessagingException
*/
protected void getConnection() throws MessagingException
{
try {
// do all of the non-protocol specific set up. This will get our socket established
// and ready use.
super.getConnection();
} catch (IOException e) {
throw new MessagingException("Unable to obtain a connection to the NNTP server", e);
}
// The NNTP protocol is inherently a string-based protocol, so we get
// string readers/writers for the connection streams. Note that we explicitly
// set the encoding to ensure that an inappropriate native encoding is not picked up.
try {
reader = new BufferedReader(new InputStreamReader(inputStream, "ISO8859-1"));
writer = new PrintWriter(new OutputStreamWriter(new BufferedOutputStream(outputStream), "ISO8859-1"));
} catch (UnsupportedEncodingException e) {
}
}
/**
* Close the connection. On completion, we'll be disconnected from the
* server and unable to send more data.
*
* @exception MessagingException
*/
public void close() throws MessagingException {
// if we're already closed, get outta here.
if (socket == null) {
return;
}
try {
// say goodbye
sendQuit();
} finally {
// and close up the connection. We do this in a finally block to
// make sure the connection
// is shut down even if quit gets an error.
closeServerConnection();
// get rid of our response processor too.
reader = null;
writer = null;
}
}
public String toString() {
return "NNTPConnection host: " + serverHost + " port: " + serverPort;
}
/**
* Get the servers welcome blob from the wire....
*/
public void getWelcome() throws MessagingException {
NNTPReply line = getReply();
//
if (line.isError()) {
throw new MessagingException("Error connecting to news server: " + line.getMessage());
}
// remember we can post.
if (line.getCode() == NNTPReply.POSTING_ALLOWED) {
postingAllowed = true;
} else {
postingAllowed = false;
}
// the NNTP store will want to use the welcome string, so save it.
welcomeString = line.getMessage();
// find out what extensions this server supports.
getExtensions();
}
/**
* Sends the QUIT message and receieves the response
*/
public void sendQuit() throws MessagingException {
sendLine("QUIT");
}
/**
* Tell the server to switch to a named group.
*
* @param name
* The name of the target group.
*
* @return The server response to the GROUP command.
*/
public NNTPReply selectGroup(String name) throws MessagingException {
// send the GROUP command
return sendCommand("GROUP " + name);
}
/**
* Ask the server what extensions it supports.
*
* @return True if the command was accepted ok, false for any errors.
* @exception MessagingException
*/
protected void getExtensions() throws MessagingException {
NNTPReply reply = sendCommand("LIST EXTENSIONS", NNTPReply.EXTENSIONS_SUPPORTED);
// we get a 202 code back. The first line is just a greeting, and
// extensions are delivered as data
// lines terminated with a "." line.
if (reply.getCode() != NNTPReply.EXTENSIONS_SUPPORTED) {
return;
}
// get a fresh extension mapping table.
capabilities = new HashMap();
authentications = new ArrayList();
// get the extension data lines.
List extensions = reply.getData();
// process all of the continuation lines
for (int i = 0; i < extensions.size(); i++) {
// go process the extention
processExtension((String) extensions.get(i));
}
}
/**
* Process an extension string passed back as the LIST EXTENSIONS response.
*
* @param extension
* The string value of the extension (which will be of the form
* "NAME arguments").
*/
protected void processExtension(String extension) {
String extensionName = extension.toUpperCase();
String argument = "";
int delimiter = extension.indexOf(' ');
// if we have a keyword with arguments, parse them out and add to the
// argument map.
if (delimiter != -1) {
extensionName = extension.substring(0, delimiter).toUpperCase();
argument = extension.substring(delimiter + 1);
}
// add this to the map so it can be tested later.
capabilities.put(extensionName, argument);
// we need to determine which authentication mechanisms are supported here
if (extensionName.equals("AUTHINFO")) {
StringTokenizer tokenizer = new StringTokenizer(argument);
while (tokenizer.hasMoreTokens()) {
// we only know how to do USER or SASL
String mechanism = tokenizer.nextToken().toUpperCase();
if (mechanism.equals("SASL")) {
authInfoSaslAllowed = true;
}
else if (mechanism.equals("USER")) {
authInfoUserAllowed = true;
}
}
}
// special case for some older servers.
else if (extensionName.equals("SASL")) {
// The security mechanisms are blank delimited tokens.
StringTokenizer tokenizer = new StringTokenizer(argument);
while (tokenizer.hasMoreTokens()) {
String mechanism = tokenizer.nextToken().toUpperCase();
authentications.add(mechanism);
}
}
}
/**
* Retrieve any argument information associated with a extension reported
* back by the server on the EHLO command.
*
* @param name
* The name of the target server extension.
*
* @return Any argument passed on a server extension. Returns null if the
* extension did not include an argument or the extension was not
* supported.
*/
public String extensionParameter(String name) {
if (capabilities != null) {
return (String) capabilities.get(name);
}
return null;
}
/**
* Tests whether the target server supports a named extension.
*
* @param name
* The target extension name.
*
* @return true if the target server reported on the EHLO command that is
* supports the targer server, false if the extension was not
* supported.
*/
public boolean supportsExtension(String name) {
// this only returns null if we don't have this extension
return extensionParameter(name) != null;
}
/**
* Sends the data in the message down the socket. This presumes the server
* is in the right place and ready for getting the DATA message and the data
* right place in the sequence
*/
public synchronized void sendPost(Message msg) throws MessagingException {
// send the POST command
NNTPReply line = sendCommand("POST");
if (line.getCode() != NNTPReply.SEND_ARTICLE) {
throw new MessagingException("Server rejected POST command: " + line);
}
// we've received permission to send the data, so ask the message to
// write itself out.
try {
// the data content has two requirements we need to meet by
// filtering the
// output stream. Requirement 1 is to conicalize any line breaks.
// All line
// breaks will be transformed into properly formed CRLF sequences.
//
// Requirement 2 is to perform byte-stuff for any line that begins
// with a "."
// so that data is not confused with the end-of-data marker (a
// "\r\n.\r\n" sequence.
//
// The MIME output stream performs those two functions on behalf of
// the content
// writer.
MIMEOutputStream mimeOut = new MIMEOutputStream(outputStream);
msg.writeTo(mimeOut);
// now to finish, we send a CRLF sequence, followed by a ".".
mimeOut.writeSMTPTerminator();
// and flush the data to send it along
mimeOut.flush();
} catch (IOException e) {
throw new MessagingException("I/O error posting message", e);
} catch (MessagingException e) {
throw new MessagingException("Exception posting message", e);
}
// use a longer time out here to give the server time to process the
// data.
line = new NNTPReply(receiveLine());
if (line.getCode() != NNTPReply.POSTED_OK) {
throw new MessagingException("Server rejected POST command: " + line);
}
}
/**
* Issue a command and retrieve the response. If the given success indicator
* is received, the command is returning a longer response, terminated by a
* "crlf.crlf" sequence. These lines are attached to the reply.
*
* @param command
* The command to issue.
* @param success
* The command reply that indicates additional data should be
* retrieved.
*
* @return The command reply.
*/
public synchronized NNTPReply sendCommand(String command, int success) throws MessagingException {
NNTPReply reply = sendCommand(command);
if (reply.getCode() == success) {
reply.retrieveData(reader);
}
return reply;
}
/**
* Send a command to the server, returning the first response line back as a
* reply.
*
* @param data
* The data to send.
*
* @return A reply object with the reply line.
* @exception MessagingException
*/
public NNTPReply sendCommand(String data) throws MessagingException {
sendLine(data);
NNTPReply reply = getReply();
// did the server just inform us we need to authenticate? The spec
// allows this
// response to be sent at any time, so we need to try to authenticate
// and then retry the command.
if (reply.getCode() == NNTPReply.AUTHINFO_REQUIRED || reply.getCode() == NNTPReply.AUTHINFO_SIMPLE_REQUIRED) {
debugOut("Authentication required received from server.");
// authenticate with the server, if necessary
processAuthentication(reply.getCode());
// if we've safely authenticated, we can reissue the command and
// process the response.
sendLine(data);
reply = getReply();
}
return reply;
}
/**
* Send a command to the server, returning the first response line back as a
* reply.
*
* @param data
* The data to send.
*
* @return A reply object with the reply line.
* @exception MessagingException
*/
public NNTPReply sendAuthCommand(String data) throws MessagingException {
sendLine(data);
return getReply();
}
/**
* Sends a message down the socket and terminates with the appropriate CRLF
*/
public void sendLine(String data) throws MessagingException {
if (socket == null || !socket.isConnected()) {
throw new MessagingException("no connection");
}
try {
outputStream.write(data.getBytes("ISO8859-1"));
outputStream.write(CR);
outputStream.write(LF);
outputStream.flush();
} catch (IOException e) {
throw new MessagingException(e.toString());
}
}
/**
* Get a reply line for an NNTP command.
*
* @return An NNTP reply object from the stream.
*/
public NNTPReply getReply() throws MessagingException {
lastServerResponse = new NNTPReply(receiveLine());
return lastServerResponse;
}
/**
* Retrieve the last response received from the NNTP server.
*
* @return The raw response string (including the error code) returned from
* the NNTP server.
*/
public String getLastServerResponse() {
if (lastServerResponse == null) {
return "";
}
return lastServerResponse.getReply();
}
/**
* Receives one line from the server. A line is a sequence of bytes
* terminated by a CRLF
*
* @return the line from the server as String
*/
public String receiveLine() throws MessagingException {
if (socket == null || !socket.isConnected()) {
throw new MessagingException("no connection");
}
try {
String line = reader.readLine();
if (line == null) {
throw new MessagingException("Unexpected end of stream");
}
return line;
} catch (IOException e) {
throw new MessagingException("Error reading from server", e);
}
}
/**
* Authenticate with the server, if necessary (or possible).
*/
protected void processAuthentication(int request) throws MessagingException {
// we need to authenticate, but we don't have userid/password
// information...fail this
// immediately.
if (username == null || password == null) {
throw new MessagingException("Server requires user authentication");
}
if (request == NNTPReply.AUTHINFO_SIMPLE_REQUIRED) {
processAuthinfoSimple();
} else {
if (!processSaslAuthentication()) {
processAuthinfoUser();
}
}
}
/**
* Process an AUTHINFO SIMPLE command. Not widely used, but if the server
* asks for it, we can respond.
*
* @exception MessagingException
*/
protected void processAuthinfoSimple() throws MessagingException {
NNTPReply reply = sendAuthCommand("AUTHINFO SIMPLE");
if (reply.getCode() != NNTPReply.AUTHINFO_CONTINUE) {
throw new MessagingException("Error authenticating with server using AUTHINFO SIMPLE");
}
reply = sendAuthCommand(username + " " + password);
if (reply.getCode() != NNTPReply.AUTHINFO_ACCEPTED) {
throw new MessagingException("Error authenticating with server using AUTHINFO SIMPLE");
}
}
/**
* Process SASL-type authentication.
*
* @return Returns true if the server support a SASL authentication mechanism and
* accepted reponse challenges.
* @exception MessagingException
*/
protected boolean processSaslAuthentication() throws MessagingException {
// only do this if permitted
if (!authInfoSaslAllowed) {
return false;
}
// if unable to get an appropriate authenticator, just fail it.
ClientAuthenticator authenticator = getSaslAuthenticator();
if (authenticator == null) {
throw new MessagingException("Unable to obtain SASL authenticator");
}
// go process the login.
return processLogin(authenticator);
}
/**
* Attempt to retrieve a SASL authenticator for this
* protocol.
*
* @return A SASL authenticator, or null if a suitable one
* was not located.
*/
protected ClientAuthenticator getSaslAuthenticator() {
return AuthenticatorFactory.getAuthenticator(props, selectSaslMechanisms(), serverHost, username, password, authid, realm);
}
/**
* Process a login using the provided authenticator object.
*
* NB: This method is synchronized because we have a multi-step process going on
* here. No other commands should be sent to the server until we complete.
*
* @return Returns true if the server support a SASL authentication mechanism and
* accepted reponse challenges.
* @exception MessagingException
*/
protected synchronized boolean processLogin(ClientAuthenticator authenticator) throws MessagingException {
debugOut("Authenticating for user: " + username + " using " + authenticator.getMechanismName());
// if the authenticator has some initial data, we compose a command
// containing the initial data.
if (authenticator.hasInitialResponse()) {
StringBuffer command = new StringBuffer();
// the auth command initiates the handshaking.
command.append("AUTHINFO SASL ");
// and tell the server which mechanism we're using.
command.append(authenticator.getMechanismName());
command.append(" ");
// and append the response data
try {
command.append(new String(Base64.encode(authenticator.evaluateChallenge(null)), "US-ASCII"));
} catch (UnsupportedEncodingException e) {
}
// send the command now
sendLine(command.toString());
}
// we just send an auth command with the command type.
else {
StringBuffer command = new StringBuffer();
// the auth command initiates the handshaking.
command.append("AUTHINFO SASL");
// and tell the server which mechanism we're using.
command.append(authenticator.getMechanismName());
// send the command now
sendLine(command.toString());
}
// now process the challenge sequence. We get a 235 response back when
// the server accepts the
// authentication, and a 334 indicates we have an additional challenge.
while (true) {
// get the next line, and if it is an error response, return now.
NNTPReply line = getReply();
// if we get a completion return, we've passed muster, so give an
// authentication response.
if (line.getCode() == NNTPReply.AUTHINFO_ACCEPTED || line.getCode() == NNTPReply.AUTHINFO_ACCEPTED_FINAL) {
debugOut("Successful SMTP authentication");
return true;
}
// we have an additional challenge to process.
else if (line.getCode() == NNTPReply.AUTHINFO_CHALLENGE) {
// Does the authenticator think it is finished? We can't answer
// an additional challenge,
// so fail this.
if (authenticator.isComplete()) {
debugOut("Extra authentication challenge " + line);
return false;
}
// we're passed back a challenge value, Base64 encoded.
try {
byte[] challenge = Base64.decode(line.getMessage().getBytes("ISO8859-1"));
// have the authenticator evaluate and send back the encoded
// response.
sendLine(new String(Base64.encode(authenticator.evaluateChallenge(challenge)), "US-ASCII"));
} catch (UnsupportedEncodingException e) {
}
}
// completion or challenge are the only responses we know how to
// handle. Anything else must
// be a failure.
else {
debugOut("Authentication failure " + line);
return false;
}
}
}
/**
* Process an AUTHINFO USER command. Most common form of NNTP
* authentication.
*
* @exception MessagingException
*/
protected void processAuthinfoUser() throws MessagingException {
// only do this if allowed by the server
if (!authInfoUserAllowed) {
return;
}
NNTPReply reply = sendAuthCommand("AUTHINFO USER " + username);
// accepted without a password (uncommon, but allowed), we're done
if (reply.getCode() == NNTPReply.AUTHINFO_ACCEPTED) {
return;
}
// the only other non-error response is continue.
if (reply.getCode() != NNTPReply.AUTHINFO_CONTINUE) {
throw new MessagingException("Error authenticating with server using AUTHINFO USER: " + reply);
}
// now send the password. We expect an accepted response.
reply = sendAuthCommand("AUTHINFO PASS " + password);
if (reply.getCode() != NNTPReply.AUTHINFO_ACCEPTED) {
throw new MessagingException("Error authenticating with server using AUTHINFO SIMPLE");
}
}
/**
* Indicate whether posting is allowed for a given server.
*
* @return True if the server allows posting, false if the server is
* read-only.
*/
public boolean isPostingAllowed() {
return postingAllowed;
}
/**
* Retrieve the welcome string sent back from the server.
*
* @return The server provided welcome string.
*/
public String getWelcomeString() {
return welcomeString;
}
}