Author: geirm Date: Tue Mar 22 14:53:53 2005 New Revision: 158693 URL: http://svn.apache.org/viewcvs?view=rev&rev=158693 Log: simple working version of primitive SMTP transport
Added: geronimo/trunk/sandbox/mail/src/java/org/apache/geronimo/mail/smtp/MalformedSMTPReplyException.java geronimo/trunk/sandbox/mail/src/java/org/apache/geronimo/mail/smtp/SMTPReply.java geronimo/trunk/sandbox/mail/src/java/org/apache/geronimo/mail/smtp/SMTPTransportException.java Modified: geronimo/trunk/sandbox/mail/src/java/org/apache/geronimo/mail/smtp/SMTPTransport.java Added: geronimo/trunk/sandbox/mail/src/java/org/apache/geronimo/mail/smtp/MalformedSMTPReplyException.java URL: http://svn.apache.org/viewcvs/geronimo/trunk/sandbox/mail/src/java/org/apache/geronimo/mail/smtp/MalformedSMTPReplyException.java?view=auto&rev=158693 ============================================================================== --- geronimo/trunk/sandbox/mail/src/java/org/apache/geronimo/mail/smtp/MalformedSMTPReplyException.java (added) +++ geronimo/trunk/sandbox/mail/src/java/org/apache/geronimo/mail/smtp/MalformedSMTPReplyException.java Tue Mar 22 14:53:53 2005 @@ -0,0 +1,38 @@ +/** + * + * Copyright 2005 The Apache Software Foundation + * + * Licensed 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.mail.smtp; + +/** + * Exception for when a SMTP reply string has a problem + * + * @version $Id: $ + */ +class MalformedSMTPReplyException extends Exception { + + MalformedSMTPReplyException() { + super(); + } + + MalformedSMTPReplyException(String msg) { + super(msg); + } + + MalformedSMTPReplyException(String msg, Throwable t) { + super(msg, t); + } +} Added: geronimo/trunk/sandbox/mail/src/java/org/apache/geronimo/mail/smtp/SMTPReply.java URL: http://svn.apache.org/viewcvs/geronimo/trunk/sandbox/mail/src/java/org/apache/geronimo/mail/smtp/SMTPReply.java?view=auto&rev=158693 ============================================================================== --- geronimo/trunk/sandbox/mail/src/java/org/apache/geronimo/mail/smtp/SMTPReply.java (added) +++ geronimo/trunk/sandbox/mail/src/java/org/apache/geronimo/mail/smtp/SMTPReply.java Tue Mar 22 14:53:53 2005 @@ -0,0 +1,85 @@ +/** + * + * Copyright 2005 The Apache Software Foundation + * + * Licensed 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.mail.smtp; + +/** + * Util class to represent a reply from a SMTP server + * + * @version $Id: $ + */ +class SMTPReply { + + private final int code; + private final String message; + private final char firstChar; + + SMTPReply(String s) + throws MalformedSMTPReplyException{ + + /* + * first three must be the return code + */ + + if (s == null || s.length() < 3) { + throw new MalformedSMTPReplyException("Too Short! : " + s); + } + + try { + firstChar = s.charAt(0); + code = Integer.parseInt(s.substring(0, 3)); + + /* + * message should be separated by a space + */ + if (s.length() > 4) { + message = s.substring(4); + } + else { + message = ""; + } + } + catch (NumberFormatException e) { + throw new MalformedSMTPReplyException("error in parsing code", e); + } + } + + int getCode() { + return this.code; + } + + String getMessage() { + return this.message; + } + + /** + * Indicates if reply is an error condition + * + * @return + */ + boolean isError() { + if (firstChar == '5' || firstChar == '4') { + return true; + } + + return false; + } + + public String toString() { + return "CODE = " + getCode() + " : MSG = " + getMessage(); + } +} Modified: geronimo/trunk/sandbox/mail/src/java/org/apache/geronimo/mail/smtp/SMTPTransport.java URL: http://svn.apache.org/viewcvs/geronimo/trunk/sandbox/mail/src/java/org/apache/geronimo/mail/smtp/SMTPTransport.java?view=diff&r1=158692&r2=158693 ============================================================================== --- geronimo/trunk/sandbox/mail/src/java/org/apache/geronimo/mail/smtp/SMTPTransport.java (original) +++ geronimo/trunk/sandbox/mail/src/java/org/apache/geronimo/mail/smtp/SMTPTransport.java Tue Mar 22 14:53:53 2005 @@ -1,6 +1,6 @@ /** * - * Copyright 2003-2004 The Apache Software Foundation + * Copyright 2003-2005 The Apache Software Foundation * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,21 +22,69 @@ import javax.mail.MessagingException; import javax.mail.Session; import javax.mail.URLName; +import javax.mail.internet.MimeMessage; +import javax.mail.internet.InternetAddress; +import javax.mail.internet.MimeMultipart; +import javax.mail.internet.MimeBodyPart; import org.apache.geronimo.mail.AbstractTransport; +import java.net.Socket; +import java.net.InetSocketAddress; +import java.net.SocketException; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.Properties; +import java.util.Date; + /** + * Simple implementation of SMTP transport. Just does plain RFC821-ish + * delivery. + * + * Supported properties : + * + * <ul> + * <li> mail.host : to set the server to deliver to. Default = localhost</li> + * <li> mail.smtp.port : to set the port. Default = 25</li> + * <li> mail.smtp.locahost : name to use for HELO/EHLO - default getHostName()</li> + * </ul> + * + * There is no way to indicate failure for a given recipient (it's possible to have a + * recipient address rejected). The sun impl throws exceptions even if others successful), + * but maybe we do a different way... + * + * TODO : lots. ESMTP, user/pass, indicate failure, etc... + * * @version $Rev$ $Date$ */ public class SMTPTransport extends AbstractTransport { /** + * constants for EOL termination + */ + private static final char CR = 0x0D; + private static final char LF = 0x0A; + + /** + * property key for SMTP server to talk to + */ + private static final String MAIL_HOST = "mail.host"; + private static final String MAIL_SMTP_LOCALHOST = "mail.smtp.localhost"; + private static final String MAIL_SMTP_PORT = "mail.smtp.port"; + + private static final int MIN_MILLIS = 1000 * 60; + private static final String DEFAULT_MAIL_HOST = "localhost"; + private static final int DEFAULT_MAIL_SMTP_PORT = 25; + + /** * @param session * @param name */ public SMTPTransport(Session session, URLName name) { super(session, name); - // TODO Auto-generated constructor stub } /* (non-Javadoc) @@ -44,8 +92,536 @@ */ public void sendMessage(Message message, Address[] addresses) throws MessagingException { - // TODO Auto-generated method stub + /* + * do it and ignore the return + */ + sendMessage(addresses, message); + } + + public SendStatus[] sendMessage(Address[] addresses, Message message) + throws MessagingException { + + /* + * don't bother me w/ null messages or no addreses + */ + + if (message == null ) { + throw new MessagingException("Null message"); + } + + if (addresses == null || addresses.length == 0) { + throw new MessagingException("Null or empty address array"); + } + + + SendStatus[] stat = new SendStatus[addresses.length]; + + try { + + /* + * create socket and connect to server. + */ + + Socket s = getConnectedSocket(); + + /* + * receive welcoming message + */ + if (!getWelcome(s)) { + throw new MessagingException("Error in getting welcome msg"); + } + + /* + * say hello + */ + if (!sendHelo(s)) { + throw new MessagingException("Error in saying HELO to server"); + } + + /* + * send sender + */ + if (!sendMailFrom(s, message.getFrom())) { + throw new MessagingException("Error in setting the MAIL FROM"); + } + + /* + * send recipients. Only send if not null or "", and just ignore + * (but log) any errors + */ + + for (int i=0; i < addresses.length; i++) { + String to = addresses[i].toString(); + + int status = SendStatus.SUCCESS; + + if (to != null && !"".equals(to)) { + if (!sendRcptTo(s, to)) { + + // this means it didn't like our recipient. I say we keep + // going + + if (this.session.getDebug()) { + this.session.getDebugOut().println("ERROR setting recipient " + to); + } + + status = SendStatus.FAIL; + } + } + else { + status = SendStatus.FAIL; + } + + stat[i] = new SendStatus(status, to); + } + + /* + * send data + */ + if (!sendData(s, message)) { + throw new MessagingException("Error sending data"); + } + + /* + * say goodbye + */ + + sendQuit(s); + + try { + s.close(); + } + catch (IOException e) { + // + // TODO - should we just eat this? We have delivered the msg... + // + e.printStackTrace(); + } + } + catch (SMTPTransportException e) { + throw new MessagingException("error", e); + } + catch (MalformedSMTPReplyException e) { + throw new MessagingException("error", e); + } + + return stat; } + /** + * 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 + * + * @param s + * @param msg + * @return + * @throws SMTPTransportException + * @throws MalformedSMTPReplyException + */ + protected boolean sendData(Socket s, Message msg) + throws SMTPTransportException, MalformedSMTPReplyException { + + if (msg == null) { + throw new SMTPTransportException("invalid message"); + } + + /* + * send the DATA command + */ + sendLine(s, "DATA"); + + SMTPReply line = new SMTPReply(receiveLine(s, 5 * MIN_MILLIS)); + + if (this.session.getDebug()) { + this.session.getDebugOut().println(line); + } + + if (line.isError()) { + return false; + } + + /* + * now the data... I could look at the type, but + */ + + try { + OutputStream os = s.getOutputStream(); + + // todo - be smarter here and send in chunks to let the other side + // digest easier. There's a 3 min recommended timeout per chunk... + + msg.writeTo(os); + os.flush(); + } + catch (IOException e) { + throw new SMTPTransportException(e); + } + catch(MessagingException e) { + throw new SMTPTransportException(e); + } + + /* + * now to finish + */ + sendLine(s, ""); + sendLine(s, "."); + + line = new SMTPReply(receiveLine(s, 10 * MIN_MILLIS)); + + return !line.isError(); + } + + /** + * Sends the QUIT message and receieves the response + * + * @param s + * @return + * @throws SMTPTransportException + * @throws MalformedSMTPReplyException + */ + protected boolean sendQuit(Socket s) + throws SMTPTransportException, MalformedSMTPReplyException { + + sendLine(s, "QUIT"); + + SMTPReply line = new SMTPReply(receiveLine(s, 5 * MIN_MILLIS)); + + return !line.isError(); + } + + /** + * Sets a receiver address for the current message + * + * @param s + * @param addr + * @return + * @throws SMTPTransportException + * @throws MalformedSMTPReplyException + */ + protected boolean sendRcptTo(Socket s, String addr) + throws SMTPTransportException, MalformedSMTPReplyException { + + if (addr == null || "".equals(addr)) { + throw new SMTPTransportException("invalid address"); + } + + String msg = "RCPT TO: " + addr; + + sendLine(s, msg); + + SMTPReply line = new SMTPReply(receiveLine(s, 5 * MIN_MILLIS)); + + return !line.isError(); + } + + /** + * Set the sender for this mail. + * @param s + * @param from + * @return + * @throws SMTPTransportException + * @throws MalformedSMTPReplyException + */ + protected boolean sendMailFrom(Socket s, Address[] from) + throws SMTPTransportException, MalformedSMTPReplyException { + + if (from == null || from.length == 0) { + throw new SMTPTransportException("no FROM address"); + } + + /* + * TODO - what do we do w/ more than one from??? + */ + + String msg = "MAIL FROM: " + from[0].toString(); + + sendLine(s, msg); + + SMTPReply line = new SMTPReply(receiveLine(s, 5 * MIN_MILLIS)); + + return !line.isError(); + } + + /** + * Sends the initiating "HELO" message. We're keeping it simple, just + * identifying ourselves as we dont' require service extensions, and + * want to keep it simple for now + * + * @param s socket we are talking on. It's assumed to be open and in + * right state for this message + * @return + * @throws SMTPTransportException + * @throws MalformedSMTPReplyException + */ + protected boolean sendHelo(Socket s) + throws SMTPTransportException, MalformedSMTPReplyException { + + String fqdm = null; + + try { + fqdm = InetAddress.getLocalHost().getHostName(); + } + catch (UnknownHostException e) { + // fine, we're misconfigured - ignore + } + + if (fqdm == null) { + fqdm = session.getProperty(MAIL_SMTP_LOCALHOST); + } + + if (fqdm == null) { + throw new SMTPTransportException("Can't get local hostname. " + + " Please correctly configure JDK/DNS or set mail.smtp.localhost"); + } + + sendLine(s, "HELO " + fqdm); + + SMTPReply line = new SMTPReply(receiveLine(s, 5 * MIN_MILLIS)); + + return !line.isError(); + } + + /** + * Get the servers welcome blob from the wire.... + * + * @param s + * @return + * @throws SMTPTransportException + * @throws MalformedSMTPReplyException + */ + protected boolean getWelcome(Socket s) + throws SMTPTransportException, MalformedSMTPReplyException { + + SMTPReply line = new SMTPReply(receiveLine(s, 5 * MIN_MILLIS)); + + return !line.isError(); + } + + /** + * Sends a message down the socket and terminates with the + * appropriate CRLF + * + * @param s + * @param data + * @throws SMTPTransportException + */ + protected void sendLine(Socket s, String data) + throws SMTPTransportException { + + if (s == null) { + throw new SMTPTransportException("bonehead..."); + } + + if (!s.isConnected()) { + throw new SMTPTransportException("no connection"); + } + + try { + OutputStream out = s.getOutputStream(); + + out.write(data.getBytes()); + out.write(CR); + out.write(LF); + out.flush(); + + if (this.session.getDebug()) { + this.session.getDebugOut().println("sent: " + data); + } + } + catch (IOException e) { + throw new SMTPTransportException(e); + } + } + + /** + * Receives one line from the server. A line is a sequence of bytes + * terminated by a CRLF + * + * @param s socket to receive from + * @return the line from the server as String + */ + protected String receiveLine(Socket s, int delayMillis) + throws SMTPTransportException { + + if (s == null) { + throw new SMTPTransportException("bonehead..."); + } + + if (!s.isConnected()) { + throw new SMTPTransportException("no connection"); + } + + int timeout = 0; + + try { + + /* + * for now, read byte for byte, looking for a CRLF + */ + + timeout = s.getSoTimeout(); + + s.setSoTimeout(delayMillis); + + InputStream is = s.getInputStream(); + + StringBuffer buff = new StringBuffer(); + + int c; + boolean crFound = false, lfFound = false; + + while((c = is.read()) != -1 && crFound == false && lfFound == false) { + buff.append((char) c); + + if (c == CR) { + crFound = true; + } + if (c == LF) { + lfFound = true; + } + } + + if (this.session.getDebug()) { + this.session.getDebugOut().println("received : " + buff.toString()); + } + + return buff.toString(); + } + catch (SocketException e) { + throw new SMTPTransportException(e); + } + catch (IOException e) { + throw new SMTPTransportException(e); + } + finally { + try { + s.setSoTimeout(timeout); + } + catch(SocketException e) { + // ignore - was just trying to do the decent thing... + } + } + } + + + /** + * Creates and returns a connected socket + * + * @return + * @throws MessagingException + */ + protected Socket getConnectedSocket() + throws MessagingException { + + Socket s = new Socket(); + + String mail_host = this.session.getProperty(MAIL_HOST); + + if (mail_host == null || "".equals(mail_host)) { + mail_host = DEFAULT_MAIL_HOST; + } + + String portString = this.session.getProperty(MAIL_SMTP_PORT); + + int port = DEFAULT_MAIL_SMTP_PORT; + + if (portString != null && !"".equals(portString)) { + + try { + port = Integer.parseInt(portString); + } + catch (NumberFormatException e) { + // ignore - we don't care, leave as default + } + } + + try { + + if (this.session.getDebug()) { + this.session.getDebugOut().println("connecting to " + mail_host); + } + + s.connect(new InetSocketAddress(mail_host, port)); + + if (this.session.getDebug()) { + this.session.getDebugOut().println("connected to " + mail_host); + } + } + catch (IOException e) { + + if (this.session.getDebug()) { + this.session.getDebugOut().println("error connecting to " + mail_host); + } + + throw new MessagingException("Error connecting to " + mail_host, e); + } + + return s; + } + + public static void main(String[] args) + throws Exception { + + Properties props = new Properties(); + props.setProperty("mail.host", "mail.optonline.net"); + + Session session = Session.getDefaultInstance(props); + + session.setDebug(true); + + SMTPTransport t = new SMTPTransport(session, null); + + MimeMessage msg = new MimeMessage(session); + + MimeMultipart mmpt = new MimeMultipart(); + MimeBodyPart b1 = new MimeBodyPart(); + b1.setContent("body 1", "text/plain"); + + mmpt.addBodyPart(b1); + + b1 = new MimeBodyPart(); + b1.setContent("body2", "text/plain"); + mmpt.addBodyPart(b1); + + Address[] list = new Address[1]; + list[0] = new InternetAddress("[EMAIL PROTECTED]"); +// list[1] = new InternetAddress("[EMAIL PROTECTED]"); + + msg.setSubject("From SMTPTransport : this is the subject"); + msg.setFrom(new InternetAddress("[EMAIL PROTECTED]")); +// msg.setContent("This is the content (via SMTPTransport)", "text/plain"); + msg.setRecipients(Message.RecipientType.TO, list); + msg.setContent(mmpt); + + msg.setSentDate(new Date()); + + t.sendMessage(msg, list); + } + + /** + * Simple holder class for the address/send status duple, as we can + * have mixed success for a set of addresses and a message + * + */ + public class SendStatus { + public final static int SUCCESS = 0; + public final static int FAIL = 1; + final int status; + final String address; + + public SendStatus(int s, String a) { + this.status = s; + this.address = a; + } + + public int getStatus() { + return this.status; + } + + public String getAddress() { + return this.address; + } + } } Added: geronimo/trunk/sandbox/mail/src/java/org/apache/geronimo/mail/smtp/SMTPTransportException.java URL: http://svn.apache.org/viewcvs/geronimo/trunk/sandbox/mail/src/java/org/apache/geronimo/mail/smtp/SMTPTransportException.java?view=auto&rev=158693 ============================================================================== --- geronimo/trunk/sandbox/mail/src/java/org/apache/geronimo/mail/smtp/SMTPTransportException.java (added) +++ geronimo/trunk/sandbox/mail/src/java/org/apache/geronimo/mail/smtp/SMTPTransportException.java Tue Mar 22 14:53:53 2005 @@ -0,0 +1,42 @@ +/** + * + * Copyright 2005 The Apache Software Foundation + * + * Licensed 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.mail.smtp; + +/** + * General purpose Exception + * + * @version $Id: $ + */ +class SMTPTransportException extends Exception { + + SMTPTransportException() { + super(); + } + + SMTPTransportException(String s) { + super(s); + } + + SMTPTransportException(String s, Throwable t) { + super(s,t); + } + + SMTPTransportException(Throwable t) { + super(t); + } +}