src/org/sonews/mlgw/SMTPTransport.java
author cli
Wed Sep 14 22:39:22 2011 +0200 (2011-09-14)
changeset 61 da0c30d28e22
parent 59 68a6825a4f4d
permissions -rwxr-xr-x
SMTPSender should now support PLAIN SMTP Authentication (issue #8)
     1 /*
     2  *   SONEWS News Server
     3  *   see AUTHORS for the list of contributors
     4  *
     5  *   This program is free software: you can redistribute it and/or modify
     6  *   it under the terms of the GNU General Public License as published by
     7  *   the Free Software Foundation, either version 3 of the License, or
     8  *   (at your option) any later version.
     9  *
    10  *   This program is distributed in the hope that it will be useful,
    11  *   but WITHOUT ANY WARRANTY; without even the implied warranty of
    12  *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    13  *   GNU General Public License for more details.
    14  *
    15  *   You should have received a copy of the GNU General Public License
    16  *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
    17  */
    18 package org.sonews.mlgw;
    19 
    20 import java.io.BufferedOutputStream;
    21 import java.io.BufferedReader;
    22 import java.io.IOException;
    23 import java.io.InputStreamReader;
    24 import java.io.UnsupportedEncodingException;
    25 import java.net.Socket;
    26 import java.net.UnknownHostException;
    27 import java.util.ArrayList;
    28 import java.util.List;
    29 import org.apache.commons.codec.binary.Base64;
    30 import org.sonews.config.Config;
    31 import org.sonews.storage.Article;
    32 import org.sonews.util.io.ArticleInputStream;
    33 
    34 /**
    35  * Connects to a SMTP server and sends a given Article to it.
    36  * @author Christian Lins
    37  * @since sonews/1.0
    38  */
    39 class SMTPTransport {
    40 
    41 	public static final String NEWLINE = "\r\n";
    42 
    43 	protected BufferedReader in;
    44 	protected BufferedOutputStream out;
    45 	protected Socket socket;
    46 
    47 	public SMTPTransport(String host, int port)
    48 			throws IOException, UnknownHostException {
    49 		this.socket = new Socket(host, port);
    50 		this.in = new BufferedReader(
    51 				new InputStreamReader(socket.getInputStream()));
    52 		this.out = new BufferedOutputStream(socket.getOutputStream());
    53 
    54 		// Read HELO from server
    55 		String line = this.in.readLine();
    56 		if (line == null || !line.startsWith("220 ")) {
    57 			throw new IOException("Invalid HELO from server: " + line);
    58 		}
    59 	}
    60 
    61 	public void close()
    62 			throws IOException {
    63 		this.out.write("QUIT".getBytes("UTF-8"));
    64 		this.out.flush();
    65 		this.in.readLine();
    66 
    67 		this.socket.close();
    68 	}
    69 
    70 	private byte[] createCredentials() throws UnsupportedEncodingException {
    71 		String user = Config.inst().get(Config.MLSEND_USER, "");
    72 		String pass = Config.inst().get(Config.MLSEND_PASSWORD, "");
    73 		StringBuilder credBuf = new StringBuilder();
    74 		credBuf.append(user);
    75 		credBuf.append("\u0000");
    76 		credBuf.append(pass);
    77 		return Base64.encodeBase64(credBuf.toString().getBytes("UTF-8"));
    78 	}
    79 
    80 	private void ehlo(String hostname) throws IOException {
    81 		StringBuilder strBuf = new StringBuilder();
    82 		strBuf.append("EHLO ");
    83 		strBuf.append(hostname);
    84 		strBuf.append(NEWLINE);
    85 
    86 		// Send EHLO to server
    87 		this.out.write(strBuf.toString().getBytes("UTF-8"));
    88 		this.out.flush();
    89 
    90 		List<String> ehloReplies = readReply("250");
    91 
    92 		// TODO: Check for supported methods
    93 
    94 		// Do a PLAIN login
    95 		strBuf = new StringBuilder();
    96 		strBuf.append("AUTH PLAIN");
    97 		strBuf.append(NEWLINE);
    98 
    99 		// Send AUTH to server
   100 		this.out.write(strBuf.toString().getBytes("UTF-8"));
   101 		this.out.flush();
   102 
   103 		readReply("334");
   104 
   105 		// Send PLAIN credentials to server
   106 		this.out.write(createCredentials());
   107 		this.out.flush();
   108 
   109 		// Read reply of successful login
   110 		readReply("235");
   111 	}
   112 
   113 	private void helo(String hostname) throws IOException {
   114 		StringBuilder heloStr = new StringBuilder();
   115 		heloStr.append("HELO ");
   116 		heloStr.append(hostname);
   117 		heloStr.append(NEWLINE);
   118 
   119 		// Send HELO to server
   120 		this.out.write(heloStr.toString().getBytes("UTF-8"));
   121 		this.out.flush();
   122 
   123 		// Read reply
   124 		readReply("250");
   125 	}
   126 
   127 	public void login() throws IOException {
   128 		String hostname = Config.inst().get(Config.HOSTNAME, "localhost");
   129 		String auth = Config.inst().get(Config.MLSEND_AUTH, "none");
   130 		if(auth.equals("none")) {
   131 			helo(hostname);
   132 		} else {
   133 			ehlo(hostname);
   134 		}
   135 	}
   136 
   137 	/**
   138 	 * Read one or more exspected reply lines.
   139 	 * @param expectedReply
   140 	 * @return
   141 	 * @throws IOException If the reply of the server does not fit the exspected
   142 	 * reply code.
   143 	 */
   144 	private List<String> readReply(String expectedReply) throws IOException {
   145 		List<String> replyStrings = new ArrayList<String>();
   146 
   147 		for(;;) {
   148 			String line = this.in.readLine();
   149 			if (line == null || !line.startsWith(expectedReply)) {
   150 				throw new IOException("Unexpected reply: " + line);
   151 			}
   152 
   153 			replyStrings.add(line);
   154 
   155 			if(line.charAt(3) == ' ') { // Last reply line
   156 				break;
   157 			}
   158 		}
   159 
   160 		return replyStrings;
   161 	}
   162 
   163 	public void send(Article article, String mailFrom, String rcptTo)
   164 			throws IOException {
   165 		assert (article != null);
   166 		assert (mailFrom != null);
   167 		assert (rcptTo != null);
   168 
   169 		this.out.write(("MAIL FROM: " + mailFrom).getBytes("UTF-8"));
   170 		this.out.flush();
   171 		String line = this.in.readLine();
   172 		if (line == null || !line.startsWith("250 ")) {
   173 			throw new IOException("Unexpected reply: " + line);
   174 		}
   175 
   176 		this.out.write(("RCPT TO: " + rcptTo).getBytes("UTF-8"));
   177 		this.out.flush();
   178 		line = this.in.readLine();
   179 		if (line == null || !line.startsWith("250 ")) {
   180 			throw new IOException("Unexpected reply: " + line);
   181 		}
   182 
   183 		this.out.write("DATA".getBytes("UTF-8"));
   184 		this.out.flush();
   185 		line = this.in.readLine();
   186 		if (line == null || !line.startsWith("354 ")) {
   187 			throw new IOException("Unexpected reply: " + line);
   188 		}
   189 
   190 		ArticleInputStream artStream = new ArticleInputStream(article);
   191 		for (int b = artStream.read(); b >= 0; b = artStream.read()) {
   192 			this.out.write(b);
   193 		}
   194 
   195 		// Flush the binary stream; important because otherwise the output
   196 		// will be mixed with the PrintWriter.
   197 		this.out.flush();
   198 		this.out.write("\r\n.\r\n".getBytes("UTF-8"));
   199 		this.out.flush();
   200 		line = this.in.readLine();
   201 		if (line == null || !line.startsWith("250 ")) {
   202 			throw new IOException("Unexpected reply: " + line);
   203 		}
   204 	}
   205 }